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


##########
catalog/sql/sql.go:
##########
@@ -1269,9 +1269,46 @@ func (c *Catalog) CreateNamespace(ctx context.Context, 
namespace table.Identifie
                        })
                }
 
-               _, err := tx.NewInsert().Model(&toInsert).Exec(ctx)
-               if err != nil {
-                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, err)
+               // A concurrent writer may have won the race; return the 
sentinel in the same
+               // form as the pre-check above. This must stay the first read 
on the outer tx
+               // so MySQL's REPEATABLE READ snapshot lands after the winner 
committed.
+               recheck := func(insertErr error) error {
+                       _, exists, checkErr := c.resolveNamespaceKeyInTx(ctx, 
tx, namespace)
+                       if checkErr == nil && exists {
+                               return fmt.Errorf("%w: %s", 
catalog.ErrNamespaceAlreadyExists, strings.Join(namespace, "."))
+                       }
+                       if checkErr != nil {
+                               return fmt.Errorf("error inserting namespace 
properties for namespace '%s': %w", namespace, errors.Join(insertErr, checkErr))
+                       }
+
+                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, insertErr)
+               }
+
+               // Only Postgres aborts the whole tx on a failed insert, so 
only it needs a
+               // savepoint to keep the re-check runnable; Oracle has no 
RELEASE SAVEPOINT.
+               if tx.Dialect().Name() == dialect.PG {
+                       sp, err := tx.BeginTx(ctx, nil)
+                       if err != nil {
+                               return fmt.Errorf("error creating savepoint for 
namespace '%s': %w", namespace, err)
+                       }
+
+                       if _, err = sp.NewInsert().Model(&toInsert).Exec(ctx); 
err != nil {
+                               if rbErr := sp.Rollback(); rbErr != nil {
+                                       return fmt.Errorf("error inserting 
namespace properties for namespace '%s': %w", namespace, errors.Join(err, 
rbErr))
+                               }
+
+                               return recheck(err)
+                       }
+
+                       if err = sp.Commit(); err != nil {
+                               return fmt.Errorf("error releasing savepoint 
for namespace '%s': %w", namespace, err)

Review Comment:
   **minor** — Postgres happy path (SAVEPOINT/RELEASE SAVEPOINT) is never 
executed by any test
   
   Both pgAbortDriver tests force the insert to fail, so sp.Commit() at 1303 
and the 'return nil' at 1307 never run. This is newly added code on the hot 
path of every successful CreateNamespace against Postgres, and there is no real 
Postgres anywhere in CI (grep over .github/workflows and dev/ finds none). A 
success-path assertion through the existing pg-dialect-over-sqlite harness 
would cover it cheaply.



##########
catalog/sql/sql.go:
##########
@@ -1269,9 +1269,46 @@ func (c *Catalog) CreateNamespace(ctx context.Context, 
namespace table.Identifie
                        })
                }
 
-               _, err := tx.NewInsert().Model(&toInsert).Exec(ctx)
-               if err != nil {
-                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, err)
+               // A concurrent writer may have won the race; return the 
sentinel in the same
+               // form as the pre-check above. This must stay the first read 
on the outer tx
+               // so MySQL's REPEATABLE READ snapshot lands after the winner 
committed.
+               recheck := func(insertErr error) error {
+                       _, exists, checkErr := c.resolveNamespaceKeyInTx(ctx, 
tx, namespace)
+                       if checkErr == nil && exists {
+                               return fmt.Errorf("%w: %s", 
catalog.ErrNamespaceAlreadyExists, strings.Join(namespace, "."))
+                       }
+                       if checkErr != nil {
+                               return fmt.Errorf("error inserting namespace 
properties for namespace '%s': %w", namespace, errors.Join(insertErr, checkErr))
+                       }
+
+                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, insertErr)
+               }
+
+               // Only Postgres aborts the whole tx on a failed insert, so 
only it needs a
+               // savepoint to keep the re-check runnable; Oracle has no 
RELEASE SAVEPOINT.
+               if tx.Dialect().Name() == dialect.PG {
+                       sp, err := tx.BeginTx(ctx, nil)
+                       if err != nil {
+                               return fmt.Errorf("error creating savepoint for 
namespace '%s': %w", namespace, err)
+                       }
+
+                       if _, err = sp.NewInsert().Model(&toInsert).Exec(ctx); 
err != nil {
+                               if rbErr := sp.Rollback(); rbErr != nil {
+                                       return fmt.Errorf("error inserting 
namespace properties for namespace '%s': %w", namespace, errors.Join(err, 
rbErr))
+                               }
+
+                               return recheck(err)
+                       }
+
+                       if err = sp.Commit(); err != nil {
+                               return fmt.Errorf("error releasing savepoint 
for namespace '%s': %w", namespace, err)
+                       }
+
+                       return nil
+               }
+
+               if _, err := tx.NewInsert().Model(&toInsert).Exec(ctx); err != 
nil {
+                       return recheck(err)

Review Comment:
   **major** — Non-Postgres recovery branch has no deterministic test; the PR's 
own repro dialect is unpinned
   
   The Postgres gate added in 25fd793 moved the deterministic driver test 
entirely onto the PG branch, so the SQLite/MySQL/Oracle/MSSQL dispatch at 
1310-1312 is covered only by the scheduling-dependent 
TestCreateNamespaceConcurrent. This re-opens `laskoviymishka`'s round-1 blocker 
for exactly the dialect in the PR description's repro (the quoted error is 
SQLite's 1555). Add a SQLite-dialect deterministic test using a NON-aborting 
driver. Note a dialect parameter on newAbortCatalog will NOT work: 
pgAbortDriver unconditionally sets `aborted` on the insert and only ROLLBACK TO 
SAVEPOINT clears it, so a SQLite-dialect run never clears it and the re-check 
fails with errEmulatedAborted. It needs a separate ~40-line driver reusing 
isNamespaceInsert/isNamespaceExistsProbe/boolRows.
   
   <details><summary>Evidence</summary>
   
   ```text
   go tool cover: 'catalog/sql/sql.go:1311.4,1312.1 1 0' (count 0). Reverting 
1310-1312 to the pre-PR raw error leaves 'go test ./catalog/sql/ -count=1' => 
ok. Detection rate measured over 15 independent single runs: 13/15. Throwaway 
probe with a non-aborting dupDriver at SQLite dialect: unmodified head => 
'race-loser err = namespace already exists: my-iceberg-db-mpifixpfynaunvszosha' 
PASS; reverted mutant => 'error inserting namespace properties ...: UNIQUE 
constraint failed' FAIL.
   ```
   
   </details>



##########
catalog/sql/sql.go:
##########
@@ -1269,9 +1269,46 @@ func (c *Catalog) CreateNamespace(ctx context.Context, 
namespace table.Identifie
                        })
                }
 
-               _, err := tx.NewInsert().Model(&toInsert).Exec(ctx)
-               if err != nil {
-                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, err)
+               // A concurrent writer may have won the race; return the 
sentinel in the same
+               // form as the pre-check above. This must stay the first read 
on the outer tx
+               // so MySQL's REPEATABLE READ snapshot lands after the winner 
committed.
+               recheck := func(insertErr error) error {
+                       _, exists, checkErr := c.resolveNamespaceKeyInTx(ctx, 
tx, namespace)
+                       if checkErr == nil && exists {
+                               return fmt.Errorf("%w: %s", 
catalog.ErrNamespaceAlreadyExists, strings.Join(namespace, "."))
+                       }
+                       if checkErr != nil {
+                               return fmt.Errorf("error inserting namespace 
properties for namespace '%s': %w", namespace, errors.Join(insertErr, checkErr))
+                       }
+
+                       return fmt.Errorf("error inserting namespace properties 
for namespace '%s': %w", namespace, insertErr)
+               }
+
+               // Only Postgres aborts the whole tx on a failed insert, so 
only it needs a
+               // savepoint to keep the re-check runnable; Oracle has no 
RELEASE SAVEPOINT.

Review Comment:
   **nit** — Gate comment conflates two independent justifications
   
   'Only Postgres aborts the whole tx on a failed insert, so only it needs a 
savepoint to keep the re-check runnable; Oracle has no RELEASE SAVEPOINT.' 
These are two separate facts: the first justifies why PG needs the savepoint, 
the second why it must not be applied universally. As written, Oracle reads 
like the reason for a Postgres-only gate. Also, the 'only Postgres' claim is 
asserted for MSSQL without evidence (SQL Server's behavior depends on 
XACT_ABORT); scoping the sentence to the dialects actually reasoned about would 
be more honest.



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