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


##########
catalog/sql/sql.go:
##########
@@ -793,6 +846,14 @@ func (c *Catalog) CreateTable(ctx context.Context, ident 
table.Identifier, sc *i
                return nil
        })
        if err != nil {
+               if collisionErr := c.checkIdentifierAvailableInCatalog(ctx, ns, 
tblIdent, ident); errors.Is(collisionErr, catalog.ErrTableAlreadyExists) ||

Review Comment:
   When the in-tx `checkIdentifierAvailable` already returned a sentinel, this 
opens a second read tx and re-derives the same collision — an extra round-trip 
on the contention-free duplicate case, which is the common one. I'd only 
recheck when the error isn't already one of ours:
   
   ```go
   if !errors.Is(err, catalog.ErrTableAlreadyExists) && !errors.Is(err, 
catalog.ErrViewAlreadyExists) {
       if collisionErr := c.checkIdentifierAvailableInCatalog(ctx, ns, 
tblIdent, ident); ... {
           err = collisionErr
       }
   }
   ```
   
   Same thing over in `CreateView`.



##########
catalog/sql/sql.go:
##########
@@ -736,6 +735,56 @@ func checkValidNamespace(ident table.Identifier) error {
        return nil
 }
 
+func removeUncommittedMetadata(ctx context.Context, metadataLocation string, 
loadFS table.FSysF) error {
+       fs, err := loadFS(context.WithoutCancel(ctx))
+       if err != nil {
+               return fmt.Errorf("failed to load filesystem while removing 
uncommitted metadata %s: %w", metadataLocation, err)
+       }
+
+       if err := fs.Remove(metadataLocation); err != nil {
+               return fmt.Errorf("failed to remove uncommitted metadata %s: 
%w", metadataLocation, err)
+       }
+
+       return nil
+}
+
+func (c *Catalog) checkIdentifierAvailable(
+       ctx context.Context, tx bun.Tx, namespace, name string, identifier 
table.Identifier,
+) error {
+       entry := &sqlIcebergTable{
+               CatalogName:    c.name,
+               TableNamespace: namespace,
+               TableName:      name,
+       }
+       query := tx.NewSelect().Model(entry).WherePK()
+       if c.isV0() {
+               query = query.ExcludeColumn("iceberg_type")
+       }
+
+       err := query.Scan(ctx)
+       if errors.Is(err, sql.ErrNoRows) {
+               return nil
+       }
+       if err != nil {
+               return fmt.Errorf("error checking identifier availability: %w", 
err)
+       }
+
+       identifierName := strings.Join(identifier, ".")
+       if !c.isV0() && entry.IcebergType.String == ViewType {

Review Comment:
   This is correct today — a NULL `iceberg_type` on a legacy row gives 
`Valid:false, String:""`, and `"" == ViewType` is false, so we fall through to 
the table sentinel. But it leans on the zero-value of `String` when `Valid` is 
false, and it's the only NullString read in this file that doesn't gate on 
`.Valid` first.
   
   I'd add the explicit check so a future driver that puts garbage in `String` 
on a NULL column can't misclassify a legacy table row as a view:
   
   ```go
   if !c.isV0() && entry.IcebergType.Valid && entry.IcebergType.String == 
ViewType {
   ```



##########
catalog/catalog.go:
##########
@@ -135,7 +135,8 @@ type Catalog interface {
        // RenameTable tells the catalog to rename a given table by the 
identifiers
        // provided, and then loads and returns the destination table
        RenameTable(ctx context.Context, from, to table.Identifier) 
(*table.Table, error)
-       // CheckTableExists returns if the table exists
+       // CheckTableExists reports whether the identifier is registered as a 
table in
+       // the catalog. It does not validate that the table metadata file is 
readable.

Review Comment:
   This reads as an interface-wide guarantee, but the REST impl falls back to 
`LoadTable` when the server has no HEAD endpoint, which does fetch and parse 
the metadata file.
   
   Since the SQL impl already carries its own doc comment saying this just 
below, I'd scope the strict wording there and keep the interface doc looser 
here. wdyt?



##########
catalog/sql/sql_test.go:
##########
@@ -2123,6 +2123,135 @@ func (s *SqliteCatalogTestSuite) TestCreateView() {
        s.True(exists)
 }
 
+func (s *SqliteCatalogTestSuite) TestCreateTableConflictsWithView() {
+       ctx := context.Background()
+       db := s.getCatalogSqlite()
+       identifier := s.randomTableIdentifier()
+       s.Require().NoError(db.CreateNamespace(ctx, 
catalog.NamespaceFromIdent(identifier), nil))
+       s.Require().NoError(db.CreateView(ctx, identifier, tableSchemaNested, 
"SELECT 1", nil))
+
+       metadataDir := filepath.Join(s.warehouse, identifier[0]+".db", 
identifier[1], "metadata")
+       before, err := os.ReadDir(metadataDir)
+       s.Require().NoError(err)
+
+       _, err = db.CreateTable(ctx, identifier, tableSchemaNested)
+       s.ErrorIs(err, catalog.ErrViewAlreadyExists)
+
+       after, err := os.ReadDir(metadataDir)
+       s.Require().NoError(err)
+       s.Len(after, len(before))
+}
+
+func (s *SqliteCatalogTestSuite) TestCreateViewConflictsWithTable() {
+       ctx := context.Background()
+       db := s.getCatalogSqlite()
+       identifier := s.randomTableIdentifier()
+       s.Require().NoError(db.CreateNamespace(ctx, 
catalog.NamespaceFromIdent(identifier), nil))
+       _, err := db.CreateTable(ctx, identifier, tableSchemaNested)
+       s.Require().NoError(err)
+
+       metadataDir := filepath.Join(s.warehouse, identifier[0]+".db", 
identifier[1], "metadata")
+       before, err := os.ReadDir(metadataDir)
+       s.Require().NoError(err)
+
+       err = db.CreateView(ctx, identifier, tableSchemaNested, "SELECT 1", nil)
+       s.ErrorIs(err, catalog.ErrTableAlreadyExists)
+
+       after, err := os.ReadDir(metadataDir)
+       s.Require().NoError(err)
+       s.Len(after, len(before))
+}
+
+func (s *SqliteCatalogTestSuite) 
TestConcurrentTableViewCollisionReturnsCatalogSentinel() {
+       ctx := context.Background()
+       sqlDB := s.getDB()
+       _, err := sqlDB.Exec("PRAGMA journal_mode=WAL")
+       s.Require().NoError(err)
+       s.Require().NoError(sqlDB.Close())
+
+       db := s.getCatalogSqlite()
+       identifier := s.randomTableIdentifier()
+       s.Require().NoError(db.CreateNamespace(ctx, 
catalog.NamespaceFromIdent(identifier), nil))
+
+       type createResult struct {
+               objectType string
+               err        error
+       }
+       start := make(chan struct{})
+       results := make(chan createResult, 2)
+       go func() {
+               <-start
+               _, err := db.CreateTable(ctx, identifier, tableSchemaNested)
+               results <- createResult{objectType: sqlcat.TableType, err: err}
+       }()
+       go func() {
+               <-start
+               err := db.CreateView(ctx, identifier, tableSchemaNested, 
"SELECT 1", nil)
+               results <- createResult{objectType: sqlcat.ViewType, err: err}
+       }()
+       close(start)
+
+       first, second := <-results, <-results

Review Comment:
   If both creates happen to fail, the swap leaves `first.err` non-nil and this 
just blows up here with no hint about what actually broke. Worth asserting 
exactly one of the two succeeded (or at least a message on this line) so a 
regression points at the real failure. wdyt?



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