laskoviymishka commented on code in PR #1684:
URL: https://github.com/apache/iceberg-go/pull/1684#discussion_r3739105381


##########
catalog/sql/sql.go:
##########
@@ -455,8 +455,8 @@ func (c *Catalog) CatalogType() catalog.Type {
 func (c *Catalog) Close() error {
        err := c.reporter.Close()
        if c.ownsDB {
-               if dbErr := c.db.Close(); dbErr != nil && err == nil {
-                       err = dbErr
+               if dbErr := c.db.Close(); dbErr != nil {
+                       err = errors.Join(err, dbErr)

Review Comment:
   This is the right fix for the both-fail case, but it also changes the common 
single-failure path: when the reporter closes cleanly and only the DB errors, 
`errors.Join(nil, dbErr)` wraps `dbErr` in a `*joinError` rather than returning 
it directly like the old code did. `errors.Is`/`As` still traverse fine, but 
direct equality and `errors.Unwrap` on the result no longer see `dbErr`.
   
   If we want to keep the old identity for the single-error case:
   
   ```go
   if dbErr := c.db.Close(); dbErr != nil {
       if err != nil {
           err = errors.Join(err, dbErr)
       } else {
           err = dbErr
       }
   }
   ```
   
   Not blocking since the test uses `ErrorIs`, but it's a cheap way to avoid 
surprising any caller doing value comparisons.



##########
catalog/sql/close_test.go:
##########
@@ -0,0 +1,89 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package sql
+
+import (
+       "context"
+       "database/sql"
+       "database/sql/driver"
+       "errors"
+       "testing"
+
+       "github.com/apache/iceberg-go/metrics"
+       "github.com/stretchr/testify/require"
+       "github.com/uptrace/bun"
+       "github.com/uptrace/bun/dialect/sqlitedialect"
+)
+
+const (
+       closeErrorDriverName   = "iceberg_go_sql_close_error"
+       closeErrorReporterName = "iceberg_go_sql_close_reporter"
+)
+
+var closeErrorDBErr error
+
+func init() {
+       sql.Register(closeErrorDriverName, closeErrorDriver{})
+}
+
+type closeErrorDriver struct{}
+
+func (closeErrorDriver) Open(string) (driver.Conn, error) {
+       return closeErrorConn{err: closeErrorDBErr}, nil
+}
+
+type closeErrorConn struct{ err error }
+
+func (c closeErrorConn) Prepare(string) (driver.Stmt, error) { return nil, 
driver.ErrSkip }
+func (c closeErrorConn) Close() error                        { return c.err }
+func (closeErrorConn) Begin() (driver.Tx, error)             { return nil, 
driver.ErrSkip }
+
+type closeErrorReporter struct{ err error }
+
+func (closeErrorReporter) Report(context.Context, metrics.MetricsReport) {}
+func (r closeErrorReporter) Close() error                                { 
return r.err }
+
+func TestCloseReturnsReporterAndDatabaseErrors(t *testing.T) {
+       dbErr := errors.New("database close")
+       reporterErr := errors.New("reporter close")
+
+       closeErrorDBErr = dbErr

Review Comment:
   The reporter side resets itself with `t.Cleanup` (line 70), but 
`closeErrorDBErr` never does, so it leaks across runs: under `go test -count=2` 
the second run starts with the stale error still set. I'd add `t.Cleanup(func() 
{ closeErrorDBErr = nil })` right here to match.
   
   While we're here, `Open` reads this global (line 47) on whatever goroutine 
database/sql calls it from, and we write it here with no synchronization, so 
`go test -race` on this package would flag it. Threading the error through the 
DSN instead of a package global would kill both problems at once, since the 
driver would read it per-connection and there'd be nothing shared to reset. 
wdyt?



##########
catalog/sql/close_test.go:
##########
@@ -0,0 +1,89 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package sql
+
+import (
+       "context"
+       "database/sql"
+       "database/sql/driver"
+       "errors"
+       "testing"
+
+       "github.com/apache/iceberg-go/metrics"
+       "github.com/stretchr/testify/require"
+       "github.com/uptrace/bun"
+       "github.com/uptrace/bun/dialect/sqlitedialect"
+)
+
+const (
+       closeErrorDriverName   = "iceberg_go_sql_close_error"
+       closeErrorReporterName = "iceberg_go_sql_close_reporter"
+)
+
+var closeErrorDBErr error
+
+func init() {
+       sql.Register(closeErrorDriverName, closeErrorDriver{})
+}
+
+type closeErrorDriver struct{}
+
+func (closeErrorDriver) Open(string) (driver.Conn, error) {
+       return closeErrorConn{err: closeErrorDBErr}, nil
+}
+
+type closeErrorConn struct{ err error }
+
+func (c closeErrorConn) Prepare(string) (driver.Stmt, error) { return nil, 
driver.ErrSkip }
+func (c closeErrorConn) Close() error                        { return c.err }
+func (closeErrorConn) Begin() (driver.Tx, error)             { return nil, 
driver.ErrSkip }
+
+type closeErrorReporter struct{ err error }
+
+func (closeErrorReporter) Report(context.Context, metrics.MetricsReport) {}
+func (r closeErrorReporter) Close() error                                { 
return r.err }
+
+func TestCloseReturnsReporterAndDatabaseErrors(t *testing.T) {
+       dbErr := errors.New("database close")
+       reporterErr := errors.New("reporter close")
+
+       closeErrorDBErr = dbErr
+       metrics.Register(closeErrorReporterName, func(map[string]string) 
(metrics.Reporter, error) {
+               return closeErrorReporter{err: reporterErr}, nil
+       })
+       t.Cleanup(func() {
+               metrics.Deregister(closeErrorReporterName)
+       })
+
+       db, err := sql.Open(closeErrorDriverName, "")
+       require.NoError(t, err)
+       require.NoError(t, db.Ping())

Review Comment:
   `db.Ping()` is doing more than it looks here: it's the only thing that opens 
a connection into the pool, which is what gives `bun.DB.Close()` something to 
drain so `closeErrorConn.Close()` actually fires. Worth a one-line comment 
saying so, otherwise someone trimming setup could drop it and quietly lose the 
db-error-path coverage without any test going red.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to