zeroshade commented on code in PR #1635:
URL: https://github.com/apache/iceberg-go/pull/1635#discussion_r3937950400


##########
catalog/sql/sql_test.go:
##########
@@ -1911,6 +1917,384 @@ func (s *SqliteCatalogTestSuite) 
TestLoadEmptyNamespaceProperties() {
        }
 }
 
+func (s *SqliteCatalogTestSuite) TestCreateNamespaceConcurrent() {
+       // Two callers creating the same namespace: exactly one succeeds and 
the other
+       // gets ErrNamespaceAlreadyExists, not the driver's duplicate-key error.
+       const writers = 8
+
+       // A busy timeout so the writers queue on the sqlite lock instead of 
failing
+       // with SQLITE_BUSY, which is a different contention problem to this 
one.
+       loaded, err := catalog.Load(context.Background(), "default", 
iceberg.Properties{
+               "uri":             s.catalogUri() + "?_pragma=" + 
url.QueryEscape("busy_timeout(10000)"),
+               sqlcat.DriverKey:  sqliteshim.ShimName,
+               sqlcat.DialectKey: string(sqlcat.SQLite),
+               "type":            "sql",
+               "warehouse":       "file://" + s.warehouse,
+       })
+       s.Require().NoError(err)
+
+       cat := loaded.(*sqlcat.Catalog)
+       ctx := context.Background()
+       namespace := table.Identifier{databaseName()}
+
+       start := make(chan struct{})
+       errs := make(chan error, writers)
+
+       var wg sync.WaitGroup
+       for range writers {
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       <-start
+                       errs <- cat.CreateNamespace(ctx, namespace, nil)
+               }()
+       }
+       close(start)
+       wg.Wait()
+       close(errs)
+
+       created, alreadyExists := 0, 0
+       var unexpected []error
+       for err := range errs {
+               switch {
+               case err == nil:
+                       created++
+               case errors.Is(err, catalog.ErrNamespaceAlreadyExists):
+                       alreadyExists++
+               default:
+                       unexpected = append(unexpected, err)
+               }
+       }
+
+       s.Empty(unexpected, "want nil or ErrNamespaceAlreadyExists")
+       s.Equal(1, created)
+       s.Equal(writers-1, alreadyExists)
+}
+
+// insertFailure selects how the emulated namespace insert fails.
+type insertFailure int
+
+const (
+       // failUnique: insert trips the unique constraint and the namespace 
exists on
+       // the re-check -- the raced create-if-absent that the fix must recover.
+       failUnique insertFailure = iota
+       // failUnrelated: insert fails for an unrelated reason and the 
namespace does
+       // not exist -- the original error must survive, not become "already 
exists".
+       failUnrelated
+       // insertSucceeds: the insert commits, exercising the savepoint happy 
path
+       // (SAVEPOINT -> insert -> RELEASE SAVEPOINT) on the Postgres branch.
+       insertSucceeds
+)
+
+var (
+       errEmulatedUnique    = errors.New("UNIQUE constraint failed: 
iceberg_namespace_properties.catalog_name, 
iceberg_namespace_properties.namespace, 
iceberg_namespace_properties.property_key")
+       errEmulatedUnrelated = errors.New("disk I/O error")
+       errEmulatedAborted   = errors.New("current transaction is aborted, 
commands ignored until end of transaction block")
+)
+
+// pgAbortDriver emulates Postgres for the create race: after the namespace
+// insert fails it refuses statements until a ROLLBACK TO SAVEPOINT clears it.
+type pgAbortDriver struct {
+       base            driver.Driver
+       mode            insertFailure
+       insertAttempted atomic.Bool
+       aborted         atomic.Bool
+}
+
+func (d *pgAbortDriver) Open(dsn string) (driver.Conn, error) {
+       conn, err := d.base.Open(dsn)
+       if err != nil {
+               return nil, err
+       }
+
+       return &pgAbortConn{Conn: conn, drv: d}, nil
+}
+
+type pgAbortConn struct {
+       driver.Conn
+       drv *pgAbortDriver
+}
+
+// These match bun's emitted SQL: an "INSERT"/"EXISTS" statement naming the
+// namespace table, and its "ROLLBACK TO SAVEPOINT" prefix.
+func isNamespaceInsert(query string) bool {
+       return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(query)), 
"INSERT") &&
+               strings.Contains(query, "iceberg_namespace_properties")
+}
+
+func isNamespaceExistsProbe(query string) bool {
+       return strings.Contains(strings.ToUpper(query), "EXISTS") &&
+               strings.Contains(query, "iceberg_namespace_properties")
+}
+
+func isRollbackToSavepoint(query string) bool {
+       return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(query)), 
"ROLLBACK TO SAVEPOINT")
+}
+
+func (c *pgAbortConn) ExecContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Result, error) {
+       execer, ok := c.Conn.(driver.ExecerContext)
+       if !ok {
+               return nil, driver.ErrSkip
+       }
+
+       if c.drv.aborted.Load() {
+               if isRollbackToSavepoint(query) {
+                       c.drv.aborted.Store(false)
+
+                       return execer.ExecContext(ctx, query, args)
+               }
+
+               return nil, errEmulatedAborted
+       }
+
+       if isNamespaceInsert(query) {
+               c.drv.insertAttempted.Store(true)
+               switch c.drv.mode {
+               case insertSucceeds:
+                       return execer.ExecContext(ctx, query, args)
+               case failUnrelated:
+                       c.drv.aborted.Store(true)
+
+                       return nil, errEmulatedUnrelated
+               default:
+                       c.drv.aborted.Store(true)
+
+                       return nil, errEmulatedUnique
+               }
+       }
+
+       return execer.ExecContext(ctx, query, args)
+}
+
+func (c *pgAbortConn) QueryContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Rows, error) {
+       queryer, ok := c.Conn.(driver.QueryerContext)
+       if !ok {
+               return nil, driver.ErrSkip
+       }
+
+       if c.drv.aborted.Load() {
+               return nil, errEmulatedAborted
+       }
+
+       // Postgres probes information_schema for schema-version detection; 
sqlite has
+       // no such view, so answer as the V1 table this suite creates (column 
present).
+       if strings.Contains(query, "information_schema") {
+               return &boolRows{val: true}, nil
+       }
+
+       // The namespace exists on the re-check only in the unique-violation 
case: a
+       // concurrent winner committed the row. An unrelated failure leaves it 
absent.
+       if isNamespaceExistsProbe(query) {
+               return &boolRows{val: c.drv.insertAttempted.Load() && 
c.drv.mode == failUnique}, nil
+       }
+
+       return queryer.QueryContext(ctx, query, args)
+}
+
+func (c *pgAbortConn) BeginTx(ctx context.Context, opts driver.TxOptions) 
(driver.Tx, error) {
+       beginTx, ok := c.Conn.(driver.ConnBeginTx)
+       if !ok {
+               return nil, driver.ErrBadConn
+       }
+
+       return beginTx.BeginTx(ctx, opts)
+}
+
+func (c *pgAbortConn) PrepareContext(ctx context.Context, query string) 
(driver.Stmt, error) {
+       prepCtx, ok := c.Conn.(driver.ConnPrepareContext)
+       if !ok {
+               return nil, driver.ErrBadConn
+       }
+
+       return prepCtx.PrepareContext(ctx, query)
+}
+
+// boolRows is a single-row, single-column result carrying a SQL EXISTS answer.
+type boolRows struct {
+       val  bool
+       done bool
+}
+
+func (r *boolRows) Columns() []string { return []string{"exists"} }
+func (r *boolRows) Close() error      { return nil }
+func (r *boolRows) Next(dest []driver.Value) error {
+       // bun's Exists() scans SELECT EXISTS via QueryRowContext.Scan(&bool), 
so it
+       // keys on the value: emit one 0/1 row, not an empty result (that is 
ErrNoRows).
+       if r.done {
+               return io.EOF
+       }
+       r.done = true
+       if r.val {
+               dest[0] = int64(1)
+       } else {
+               dest[0] = int64(0)
+       }
+
+       return nil
+}
+
+func (s *SqliteCatalogTestSuite) newAbortCatalog(mode insertFailure) 
(*sqlcat.Catalog, *pgAbortDriver) {
+       base, err := sql.Open(sqliteshim.ShimName, ":memory:")
+       s.Require().NoError(err)
+       s.Require().NoError(base.Close())
+
+       // drvName derives from databaseName() (unique per call); sql.Register 
panics
+       // on a duplicate name, so this must not become a fixed string.
+       drvName := "sqlite-pgabort-" + databaseName()
+       drv := &pgAbortDriver{base: base.Driver(), mode: mode}
+       sql.Register(drvName, drv)
+
+       sqldb, err := sql.Open(drvName, s.catalogUri())
+       s.Require().NoError(err)
+       s.T().Cleanup(func() { _ = sqldb.Close() })
+       // Single connection so the driver's abort state spans the pre-check 
and the
+       // transaction that follows it.
+       sqldb.SetMaxOpenConns(1)
+
+       // Postgres dialect so the savepoint-backed recovery path runs; the 
driver
+       // above emulates Postgres aborting the tx over an sqlite base.
+       cat, err := sqlcat.NewCatalog("default", sqldb, sqlcat.Postgres, 
iceberg.Properties{"warehouse": "file://" + s.warehouse})
+       s.Require().NoError(err)
+
+       return cat, drv
+}
+
+// dupInsertDriver emulates a dialect that rolls back only the failing 
statement
+// (SQLite): the namespace insert trips a unique violation without poisoning 
the
+// tx, so the re-check runs on the same tx and recovers the sentinel.
+type dupInsertDriver struct {
+       base            driver.Driver
+       insertAttempted atomic.Bool
+}
+
+func (d *dupInsertDriver) Open(dsn string) (driver.Conn, error) {
+       conn, err := d.base.Open(dsn)
+       if err != nil {
+               return nil, err
+       }
+
+       return &dupInsertConn{Conn: conn, drv: d}, nil
+}
+
+type dupInsertConn struct {
+       driver.Conn
+       drv *dupInsertDriver
+}
+
+func (c *dupInsertConn) ExecContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Result, error) {
+       execer, ok := c.Conn.(driver.ExecerContext)
+       if !ok {
+               return nil, driver.ErrSkip
+       }
+       if isNamespaceInsert(query) {
+               c.drv.insertAttempted.Store(true)
+
+               return nil, errEmulatedUnique
+       }
+
+       return execer.ExecContext(ctx, query, args)
+}
+
+func (c *dupInsertConn) QueryContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Rows, error) {
+       queryer, ok := c.Conn.(driver.QueryerContext)
+       if !ok {
+               return nil, driver.ErrSkip
+       }
+       // After the insert, the namespace exists on the re-check; before it, 
absent.
+       if isNamespaceExistsProbe(query) {
+               return &boolRows{val: c.drv.insertAttempted.Load()}, nil
+       }
+
+       return queryer.QueryContext(ctx, query, args)
+}
+
+func (c *dupInsertConn) BeginTx(ctx context.Context, opts driver.TxOptions) 
(driver.Tx, error) {
+       beginTx, ok := c.Conn.(driver.ConnBeginTx)
+       if !ok {
+               return nil, driver.ErrBadConn
+       }
+
+       return beginTx.BeginTx(ctx, opts)
+}
+
+func (c *dupInsertConn) PrepareContext(ctx context.Context, query string) 
(driver.Stmt, error) {
+       prepCtx, ok := c.Conn.(driver.ConnPrepareContext)
+       if !ok {
+               return nil, driver.ErrBadConn
+       }
+
+       return prepCtx.PrepareContext(ctx, query)
+}
+
+func (s *SqliteCatalogTestSuite) newDupCatalog() (*sqlcat.Catalog, 
*dupInsertDriver) {
+       base, err := sql.Open(sqliteshim.ShimName, ":memory:")
+       s.Require().NoError(err)
+       s.Require().NoError(base.Close())
+
+       drvName := "sqlite-dupinsert-" + databaseName()
+       drv := &dupInsertDriver{base: base.Driver()}
+       sql.Register(drvName, drv)
+
+       sqldb, err := sql.Open(drvName, s.catalogUri())
+       s.Require().NoError(err)
+       s.T().Cleanup(func() { _ = sqldb.Close() })
+       sqldb.SetMaxOpenConns(1)
+
+       // SQLite dialect so the non-Postgres recovery branch (plain insert + 
re-check)
+       // runs; the driver rolls back only the failing statement, so no 
savepoint.
+       cat, err := sqlcat.NewCatalog("default", sqldb, sqlcat.SQLite, 
iceberg.Properties{"warehouse": "file://" + s.warehouse})
+       s.Require().NoError(err)
+
+       return cat, drv
+}
+
+// Pins the non-Postgres branch: on SQLite a lost insert race recovers the
+// sentinel via the re-check without a savepoint.
+func (s *SqliteCatalogTestSuite) TestCreateNamespaceLosesInsertRaceSQLite() {
+       namespace := table.Identifier{databaseName()}
+       cat, drv := s.newDupCatalog()
+
+       err := cat.CreateNamespace(context.Background(), namespace, nil)
+       s.ErrorIs(err, catalog.ErrNamespaceAlreadyExists)
+       s.Contains(err.Error(), namespace[0])
+       s.True(drv.insertAttempted.Load(), "the emulated insert must have run")
+}
+
+// Exercises the Postgres savepoint happy path: SAVEPOINT -> insert -> RELEASE.
+func (s *SqliteCatalogTestSuite) TestCreateNamespaceSucceedsThroughSavepoint() 
{
+       namespace := table.Identifier{databaseName()}
+       cat, drv := s.newAbortCatalog(insertSucceeds)
+
+       err := cat.CreateNamespace(context.Background(), namespace, nil)
+       s.Require().NoError(err)
+       s.True(drv.insertAttempted.Load(), "the emulated insert must have run")

Review Comment:
   **major** — Savepoint happy-path test cannot distinguish RELEASE SAVEPOINT 
from ROLLBACK TO SAVEPOINT — a silent data-loss mutant survives
   
   TestCreateNamespaceSucceedsThroughSavepoint asserts only 
s.Require().NoError(err) and drv.insertAttempted, neither of which depends on 
whether the savepoint was released or rolled back. The whole point of the new 
Postgres branch is that sp.Commit() emits RELEASE SAVEPOINT (bun v1.2.18 
db.go:670) so the insert survives into the outer tx; if it instead emitted 
ROLLBACK TO SAVEPOINT (db.go:691) CreateNamespace would return nil having 
created nothing — silent data loss on the hot path of every successful Postgres 
CreateNamespace, with no real Postgres anywhere in CI. Fix is one assertion: 
the pgAbortDriver only intercepts EXISTS probes (isNamespaceExistsProbe), so a 
plain COUNT(*) on iceberg_namespace_properties, or CheckNamespaceExists via a 
non-intercepting handle, passes through to the real sqlite base and pins it.
   
   <details><summary>Evidence</summary>
   
   ```text
   Mutation `sp.Commit()` -> `sp.Rollback()` at sql.go:1303. PR's own test: 'ok 
github.com/apache/iceberg-go/catalog/sql 0.753s'; full suite also 'ok ... 
0.980s'. Probe asserting persistence: unmutated 'PROBE: persisted namespace 
property rows = 1' PASS; mutated 'PROBE: persisted namespace property rows = 0' 
/ '"0" is not positive' / 'CreateNamespace returned nil but persisted no 
namespace rows' FAIL. Confirmed bun Tx.BeginTx sets a non-empty savepoint name 
(db.go:753-776), so Commit/Rollback really are RELEASE/ROLLBACK TO, not 
whole-tx operations.
   ```
   
   </details>



-- 
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