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


##########
catalog/sql/sql.go:
##########
@@ -211,6 +246,10 @@ func NewCatalog(name string, db *sql.DB, dialect 
SupportedDialect, props iceberg
                return cat, cat.ensureTablesExist()
        }
 
+       if err := cat.detectSchemaVersion(context.Background()); err != nil {

Review Comment:
   [major] With `init_catalog_tables=false`, this path only detects the 
existing schema and never calls `migrateV0Schema`, so `jdbc.schema-version=V1` 
does not actually migrate an existing V0 catalog. Please run one 
detect-and-maybe-migrate on open, and gate only `CreateSQLTables` behind 
`init_catalog_tables`.



##########
catalog/sql/sql.go:
##########
@@ -247,12 +286,141 @@ func (c *Catalog) DropSQLTables(ctx context.Context) 
error {
 }
 
 func (c *Catalog) ensureTablesExist() error {
-       return c.CreateSQLTables(context.Background())
+       ctx := context.Background()
+       if err := c.CreateSQLTables(ctx); err != nil {
+               return err
+       }
+
+       return c.migrateV0Schema(ctx)
+}
+
+func (c *Catalog) detectSchemaVersion(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("detecting catalog schema version: %w", err)
+       }
+
+       if hasCol {
+               c.schemaVersion = schemaV1
+       } else {
+               c.schemaVersion = schemaV0

Review Comment:
   [major] A missing `iceberg_tables` table is collapsed into `schemaV0` 
because the column probe returns false for both legacy V0 and table-absent 
catalogs. If a caller opens with `init_catalog_tables=false` and later calls 
`CreateSQLTables`, the physical schema is V1 while this `Catalog` stays in V0 
mode and writes omit `iceberg_type`. Please probe table existence separately 
and either treat table-absent as fresh V1 or fail/document this path.



##########
catalog/sql/sql.go:
##########
@@ -1013,6 +1225,10 @@ func (c *Catalog) DropView(ctx context.Context, 
identifier table.Identifier) err
 
 // CheckViewExists returns true if a view exists in the catalog.
 func (c *Catalog) CheckViewExists(ctx context.Context, identifier 
table.Identifier) (bool, error) {
+       if c.isV0() {
+               return false, errViewsUnsupportedOnV0

Review Comment:
   [minor] `CheckViewExists` should return `(false, nil)` on V0 rather than an 
unsupported-view error, mirroring `CheckTableExists` existence-check semantics. 
Consider making `ListViews` return empty on V0 as well.



##########
catalog/sql/sql.go:
##########
@@ -247,12 +286,141 @@ func (c *Catalog) DropSQLTables(ctx context.Context) 
error {
 }
 
 func (c *Catalog) ensureTablesExist() error {
-       return c.CreateSQLTables(context.Background())
+       ctx := context.Background()
+       if err := c.CreateSQLTables(ctx); err != nil {
+               return err
+       }
+
+       return c.migrateV0Schema(ctx)
+}
+
+func (c *Catalog) detectSchemaVersion(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("detecting catalog schema version: %w", err)
+       }
+
+       if hasCol {
+               c.schemaVersion = schemaV1
+       } else {
+               c.schemaVersion = schemaV0
+       }
+
+       return nil
+}
+
+func (c *Catalog) migrateV0Schema(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("V0 schema migration: column probe failed: 
%w", err)
+       }
+       if hasCol {
+               c.schemaVersion = schemaV1
+
+               return nil
+       }
+
+       if !strings.EqualFold(c.props.Get(SchemaVersionKey, ""), 
SchemaVersionV1) {

Review Comment:
   [major] `strings.EqualFold(..., "V1")` lets lowercase `v1` trigger the 
irreversible ALTER, which diverges from Java's case-sensitive handling. Please 
compare exactly to `SchemaVersionV1`, or explicitly document and test the 
intended case-insensitive behavior.



##########
catalog/sql/sql.go:
##########
@@ -247,12 +286,141 @@ func (c *Catalog) DropSQLTables(ctx context.Context) 
error {
 }
 
 func (c *Catalog) ensureTablesExist() error {
-       return c.CreateSQLTables(context.Background())
+       ctx := context.Background()
+       if err := c.CreateSQLTables(ctx); err != nil {
+               return err
+       }
+
+       return c.migrateV0Schema(ctx)
+}
+
+func (c *Catalog) detectSchemaVersion(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("detecting catalog schema version: %w", err)
+       }
+
+       if hasCol {
+               c.schemaVersion = schemaV1
+       } else {
+               c.schemaVersion = schemaV0
+       }
+
+       return nil
+}
+
+func (c *Catalog) migrateV0Schema(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("V0 schema migration: column probe failed: 
%w", err)
+       }
+       if hasCol {
+               c.schemaVersion = schemaV1
+
+               return nil
+       }
+
+       if !strings.EqualFold(c.props.Get(SchemaVersionKey, ""), 
SchemaVersionV1) {
+               log.Printf("WARNING: iceberg_tables is on the legacy V0 schema 
(no iceberg_type column); "+
+                       "set %q=%q to migrate to V1. Operating in V0-compatible 
mode (column-free queries).",
+                       SchemaVersionKey, SchemaVersionV1)
+               c.schemaVersion = schemaV0
+
+               return nil
+       }
+
+       ddl, err := addIcebergTypeColumnDDL(c.db.Dialect().Name())
+       if err != nil {
+               return err
+       }
+
+       if _, err := c.db.ExecContext(ctx, ddl); err != nil {
+               has, reprobeErr := c.icebergTypeColumnExists(ctx)
+               if has {
+                       c.schemaVersion = schemaV1
+
+                       return nil
+               }
+               if reprobeErr != nil {
+                       return fmt.Errorf("migration of V0 schema failed: %w 
(re-probe also failed: %v)", err, reprobeErr)
+               }
+
+               return fmt.Errorf("migration of V0 schema failed: %w", err)
+       }
+
+       c.schemaVersion = schemaV1
+
+       return nil
+}
+
+func addIcebergTypeColumnDDL(name dialect.Name) (string, error) {
+       switch name {
+       case dialect.PG, dialect.SQLite:
+               return "ALTER TABLE iceberg_tables ADD COLUMN iceberg_type 
VARCHAR", nil
+       case dialect.MySQL:
+               return "ALTER TABLE iceberg_tables ADD COLUMN iceberg_type 
VARCHAR(255)", nil
+       case dialect.MSSQL:
+               return "ALTER TABLE iceberg_tables ADD iceberg_type 
VARCHAR(255)", nil
+       case dialect.Oracle:
+               return "ALTER TABLE iceberg_tables ADD (iceberg_type 
VARCHAR2(255))", nil
+       default:
+               return "", fmt.Errorf("unsupported dialect for V0 migration: 
%s", name)
+       }
+}
+
+func icebergTypeColumnExistsQuery(name dialect.Name) (string, error) {
+       switch name {
+       case dialect.PG:
+               return `SELECT 1 FROM information_schema.columns
+                        WHERE table_schema = current_schema()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.MySQL:
+               return `SELECT 1 FROM information_schema.columns
+                        WHERE table_schema = DATABASE()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.MSSQL:
+               return `SELECT TOP 1 1 FROM information_schema.columns
+                        WHERE table_schema = SCHEMA_NAME()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type'`, nil
+       case dialect.SQLite:
+               return `SELECT 1 FROM pragma_table_info('iceberg_tables')
+                        WHERE name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.Oracle:
+               return `SELECT 1 FROM user_tab_columns
+                        WHERE UPPER(table_name) = 'ICEBERG_TABLES'
+                          AND UPPER(column_name) = 'ICEBERG_TYPE'`, nil
+       default:
+               return "", fmt.Errorf("unsupported dialect for V0 migration: 
%s", name)
+       }
+}
+
+func (c *Catalog) icebergTypeColumnExists(ctx context.Context) (bool, error) {
+       query, err := icebergTypeColumnExistsQuery(c.db.Dialect().Name())
+       if err != nil {
+               return false, err
+       }
+
+       var exists int
+       if err := c.db.QueryRowContext(ctx, query).Scan(&exists); err != nil {

Review Comment:
   [minor] The schema probe bypasses `withReadTx`, unlike the other read paths 
in this catalog. Please wrap this in `withReadTx` as well, or document why the 
schema-version probe intentionally runs outside the transaction helper.



##########
catalog/sql/sql.go:
##########
@@ -61,8 +62,37 @@ const (
        DialectKey           = "sql.dialect"
        DriverKey            = "sql.driver"
        initCatalogTablesKey = "init_catalog_tables"
+
+       // SchemaVersionKey opts in to migrating a legacy V0 catalog (one that 
lacks
+       // the iceberg_type column) up to the V1 schema.
+       //
+       // When set to SchemaVersionV1 the catalog issues the one-way ALTER 
TABLE
+       // that adds the iceberg_type column; otherwise it stays on V0 and emits
+       // column-free queries that never reference iceberg_type.
+       SchemaVersionKey = "jdbc.schema-version"
+)
+
+const (
+       SchemaVersionV1 = "V1"
 )
 
+// schemaVer tracks which physical layout of the iceberg_tables table the 
catalog is operating against.
+//
+// schemaV1 has the iceberg_type column. schemaV0 is the legacy JDBC default 
that lacks the column entirely.
+// Queries against a V0 catalog must never reference iceberg_type
+//
+// The zero value is schemaV1
+type schemaVer int
+
+const (
+       schemaV1 schemaVer = iota

Review Comment:
   [minor] The zero value of `schemaVer` is currently V1, which is the less 
safe default if construction or probing is skipped. Please make `schemaV0` the 
iota zero value and set V1 only after a successful probe or migration.



##########
catalog/sql/sql.go:
##########
@@ -247,12 +286,141 @@ func (c *Catalog) DropSQLTables(ctx context.Context) 
error {
 }
 
 func (c *Catalog) ensureTablesExist() error {
-       return c.CreateSQLTables(context.Background())
+       ctx := context.Background()
+       if err := c.CreateSQLTables(ctx); err != nil {
+               return err
+       }
+
+       return c.migrateV0Schema(ctx)
+}
+
+func (c *Catalog) detectSchemaVersion(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("detecting catalog schema version: %w", err)
+       }
+
+       if hasCol {
+               c.schemaVersion = schemaV1
+       } else {
+               c.schemaVersion = schemaV0
+       }
+
+       return nil
+}
+
+func (c *Catalog) migrateV0Schema(ctx context.Context) error {
+       hasCol, err := c.icebergTypeColumnExists(ctx)
+       if err != nil {
+               return fmt.Errorf("V0 schema migration: column probe failed: 
%w", err)
+       }
+       if hasCol {
+               c.schemaVersion = schemaV1
+
+               return nil
+       }
+
+       if !strings.EqualFold(c.props.Get(SchemaVersionKey, ""), 
SchemaVersionV1) {
+               log.Printf("WARNING: iceberg_tables is on the legacy V0 schema 
(no iceberg_type column); "+
+                       "set %q=%q to migrate to V1. Operating in V0-compatible 
mode (column-free queries).",
+                       SchemaVersionKey, SchemaVersionV1)
+               c.schemaVersion = schemaV0
+
+               return nil
+       }
+
+       ddl, err := addIcebergTypeColumnDDL(c.db.Dialect().Name())
+       if err != nil {
+               return err
+       }
+
+       if _, err := c.db.ExecContext(ctx, ddl); err != nil {
+               has, reprobeErr := c.icebergTypeColumnExists(ctx)
+               if has {
+                       c.schemaVersion = schemaV1
+
+                       return nil
+               }
+               if reprobeErr != nil {
+                       return fmt.Errorf("migration of V0 schema failed: %w 
(re-probe also failed: %v)", err, reprobeErr)
+               }
+
+               return fmt.Errorf("migration of V0 schema failed: %w", err)
+       }
+
+       c.schemaVersion = schemaV1
+
+       return nil
+}
+
+func addIcebergTypeColumnDDL(name dialect.Name) (string, error) {
+       switch name {
+       case dialect.PG, dialect.SQLite:
+               return "ALTER TABLE iceberg_tables ADD COLUMN iceberg_type 
VARCHAR", nil
+       case dialect.MySQL:
+               return "ALTER TABLE iceberg_tables ADD COLUMN iceberg_type 
VARCHAR(255)", nil
+       case dialect.MSSQL:
+               return "ALTER TABLE iceberg_tables ADD iceberg_type 
VARCHAR(255)", nil
+       case dialect.Oracle:
+               return "ALTER TABLE iceberg_tables ADD (iceberg_type 
VARCHAR2(255))", nil
+       default:
+               return "", fmt.Errorf("unsupported dialect for V0 migration: 
%s", name)
+       }
+}
+
+func icebergTypeColumnExistsQuery(name dialect.Name) (string, error) {
+       switch name {
+       case dialect.PG:
+               return `SELECT 1 FROM information_schema.columns
+                        WHERE table_schema = current_schema()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.MySQL:
+               return `SELECT 1 FROM information_schema.columns
+                        WHERE table_schema = DATABASE()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.MSSQL:
+               return `SELECT TOP 1 1 FROM information_schema.columns
+                        WHERE table_schema = SCHEMA_NAME()
+                          AND table_name = 'iceberg_tables'
+                          AND column_name = 'iceberg_type'`, nil
+       case dialect.SQLite:
+               return `SELECT 1 FROM pragma_table_info('iceberg_tables')
+                        WHERE name = 'iceberg_type' LIMIT 1`, nil
+       case dialect.Oracle:
+               return `SELECT 1 FROM user_tab_columns

Review Comment:
   [nit] The Oracle column probe should also limit to one row, e.g. add `AND 
ROWNUM = 1`, to match the other dialect probes' bounded existence checks.



##########
catalog/sql/sql_test.go:
##########
@@ -322,6 +336,358 @@ func (s *SqliteCatalogTestSuite) 
TestCreationAllTablesExist() {
        s.confirmTablesExist(sqldb)
 }
 
+func (s *SqliteCatalogTestSuite) TestV0SchemaFromIcebergIsMigrated() {
+       sqldb := s.getDB()
+       s.confirmNoTables(sqldb)
+
+       _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
+               "catalog_name" VARCHAR NOT NULL,
+               "table_namespace" VARCHAR NOT NULL,
+               "table_name" VARCHAR NOT NULL,
+               "metadata_location" VARCHAR,
+               "previous_metadata_location" VARCHAR,
+               PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(`CREATE TABLE "iceberg_namespace_properties" (
+               "catalog_name" VARCHAR NOT NULL,
+               "namespace" VARCHAR NOT NULL,
+               "property_key" VARCHAR NOT NULL,
+               "property_value" VARCHAR,
+               PRIMARY KEY ("catalog_name", "namespace", "property_key"))`)
+       s.Require().NoError(err)
+
+       const (
+               catName = "default"
+               nsName  = "test_v0_ns"
+               tblName = "test_v0_tbl"
+               metaLoc = "file:///does/not/matter/00000-v0.metadata.json"
+       )
+       _, err = sqldb.Exec(
+               `INSERT INTO "iceberg_namespace_properties" `+
+                       `("catalog_name", "namespace", "property_key", 
"property_value") VALUES (?, ?, ?, ?)`,
+               catName, nsName, "exists", "true")
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(
+               `INSERT INTO "iceberg_tables" `+
+                       `("catalog_name", "table_namespace", "table_name", 
"metadata_location") VALUES (?, ?, ?, ?)`,
+               catName, nsName, tblName, metaLoc)
+       s.Require().NoError(err)
+
+       _ = s.loadCatalogWithV1Migration()
+
+       columnPresent := func() bool {
+               var present int
+               qErr := sqldb.QueryRow(
+                       `SELECT 1 FROM pragma_table_info('iceberg_tables') 
WHERE name = 'iceberg_type'`,
+               ).Scan(&present)
+               s.Require().NoError(qErr, "migration must add iceberg_type 
column")
+
+               return present == 1
+       }
+       s.True(columnPresent(), "migration must add iceberg_type column")
+
+       rowType := func() sql.NullString {
+               var got sql.NullString
+               qErr := sqldb.QueryRow(
+                       `SELECT iceberg_type FROM iceberg_tables WHERE 
catalog_name = ? AND table_namespace = ? AND table_name = ?`,
+                       catName, nsName, tblName,
+               ).Scan(&got)
+               s.Require().NoError(qErr)
+
+               return got
+       }
+       s.False(rowType().Valid, "pre-V0 row should keep iceberg_type IS NULL 
after migration")
+
+       // Re-opening with the same V1 opt-in must be idempotent: the column 
stays
+       // present and the legacy row's iceberg_type is left NULL (not re-added 
or stamped).
+       _ = s.loadCatalogWithV1Migration()
+       s.True(columnPresent(), "column must remain present after a second 
open")
+       s.False(rowType().Valid, "legacy row must remain NULL after a second 
open")
+}
+
+func (s *SqliteCatalogTestSuite) TestV0SchemaNotMigratedWithoutOptIn() {
+       sqldb := s.getDB()
+       s.confirmNoTables(sqldb)
+
+       _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
+               "catalog_name" VARCHAR NOT NULL,
+               "table_namespace" VARCHAR NOT NULL,
+               "table_name" VARCHAR NOT NULL,
+               "metadata_location" VARCHAR,
+               "previous_metadata_location" VARCHAR,
+               PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(`CREATE TABLE "iceberg_namespace_properties" (
+               "catalog_name" VARCHAR NOT NULL,
+               "namespace" VARCHAR NOT NULL,
+               "property_key" VARCHAR NOT NULL,
+               "property_value" VARCHAR,
+               PRIMARY KEY ("catalog_name", "namespace", "property_key"))`)
+       s.Require().NoError(err)
+
+       // Without jdbc.schema-version=V1, opening a legacy V0 catalog must NOT 
issue
+       // the one-way ALTER; the column stays absent.
+       _ = s.loadCatalogForTableCreation()
+
+       var present int
+       err = sqldb.QueryRow(
+               `SELECT 1 FROM pragma_table_info('iceberg_tables') WHERE name = 
'iceberg_type'`,
+       ).Scan(&present)
+       s.ErrorIs(err, sql.ErrNoRows, "iceberg_type column must not be added 
without the V1 opt-in")
+}
+
+func (s *SqliteCatalogTestSuite) 
TestV0SchemaListAndDropAcceptNullIcebergType() {
+       sqldb := s.getDB()
+       s.confirmNoTables(sqldb)
+
+       _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
+               "catalog_name" VARCHAR NOT NULL,
+               "table_namespace" VARCHAR NOT NULL,
+               "table_name" VARCHAR NOT NULL,
+               "metadata_location" VARCHAR,
+               "previous_metadata_location" VARCHAR,
+               PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(`CREATE TABLE "iceberg_namespace_properties" (
+               "catalog_name" VARCHAR NOT NULL,
+               "namespace" VARCHAR NOT NULL,
+               "property_key" VARCHAR NOT NULL,
+               "property_value" VARCHAR,
+               PRIMARY KEY ("catalog_name", "namespace", "property_key"))`)
+       s.Require().NoError(err)
+
+       const (
+               catName = "default"
+               nsName  = "legacy_ns"
+               tblName = "legacy_tbl"
+               metaLoc = "file:///legacy/00000-v0.metadata.json"
+       )
+       _, err = sqldb.Exec(
+               `INSERT INTO "iceberg_namespace_properties" `+
+                       `("catalog_name", "namespace", "property_key", 
"property_value") VALUES (?, ?, ?, ?)`,
+               catName, nsName, "exists", "true")
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(
+               `INSERT INTO "iceberg_tables" `+
+                       `("catalog_name", "table_namespace", "table_name", 
"metadata_location") VALUES (?, ?, ?, ?)`,
+               catName, nsName, tblName, metaLoc)
+       s.Require().NoError(err)
+
+       // Migrate to V1: this adds the iceberg_type column and leaves the 
legacy row
+       // with iceberg_type IS NULL, which the read path must still tolerate.
+       cat := s.loadCatalogWithV1Migration()
+       ctx := context.Background()
+
+       var listed []table.Identifier
+       for ident, err := range cat.ListTables(ctx, table.Identifier{nsName}) {
+               s.Require().NoError(err)
+               listed = append(listed, ident)
+       }
+       s.Require().Len(listed, 1, "ListTables must return rows with 
iceberg_type IS NULL")
+       s.Equal(tblName, listed[0][len(listed[0])-1])
+
+       s.Require().NoError(cat.DropTable(ctx, table.Identifier{nsName, 
tblName}))
+
+       var remaining int
+       err = sqldb.QueryRow(
+               `SELECT COUNT(*) FROM iceberg_tables WHERE catalog_name = ? AND 
table_namespace = ? AND table_name = ?`,
+               catName, nsName, tblName,
+       ).Scan(&remaining)
+       s.Require().NoError(err)
+       s.Equal(0, remaining, "DropTable must delete rows with iceberg_type IS 
NULL")
+}
+
+func (s *SqliteCatalogTestSuite) 
TestV0SchemaLoadCommitRenameAcceptNullIcebergType() {
+       sqldb := s.getDB()
+
+       cat := s.getCatalogSqlite()
+       ctx := context.Background()
+
+       // This is a proxy for a genuine V0 row: we create the row at V1 (so the
+       // iceberg_type column already exists) and then manually NULL it, 
rather than
+       // starting from a truly column-less V0 schema migrated up. It 
exercises the
+       // same NULL-tolerant predicate the read/commit/rename paths rely on.
+
+       tblID := s.randomTableIdentifier()
+       ns := catalog.NamespaceFromIdent(tblID)
+       s.Require().NoError(cat.CreateNamespace(ctx, ns, nil))
+
+       tbl, err := cat.CreateTable(ctx, tblID, tableSchemaNested)
+       s.Require().NoError(err)
+       s.Equal(tblID, tbl.Identifier())
+
+       nullIcebergType := func(ident table.Identifier) {
+               res, exErr := sqldb.Exec(
+                       `UPDATE iceberg_tables SET iceberg_type = NULL `+
+                               `WHERE catalog_name = ? AND table_namespace = ? 
AND table_name = ?`,
+                       cat.Name(),
+                       strings.Join(catalog.NamespaceFromIdent(ident), "."),
+                       catalog.TableNameFromIdent(ident),
+               )
+               s.Require().NoError(exErr)
+               n, raErr := res.RowsAffected()
+               s.Require().NoError(raErr)
+               s.Require().Equal(int64(1), n, "test setup: expected exactly 
one row to be NULLed")
+       }
+       nullIcebergType(tblID)
+
+       loaded, err := cat.LoadTable(ctx, tblID)
+       s.Require().NoError(err, "LoadTable must accept rows with iceberg_type 
IS NULL")
+       s.Equal(tblID, loaded.Identifier())
+
+       tx := loaded.NewTransaction()
+       
s.Require().NoError(tx.SetProperties(iceberg.Properties{"v0.commit.test": 
"ok"}))
+       _, err = tx.Commit(ctx)
+       s.Require().NoError(err, "CommitTable must accept rows with 
iceberg_type IS NULL")
+
+       var icebergType sql.NullString
+       err = sqldb.QueryRow(
+               `SELECT iceberg_type FROM iceberg_tables WHERE catalog_name = ? 
AND table_namespace = ? AND table_name = ?`,
+               cat.Name(),
+               strings.Join(ns, "."),
+               catalog.TableNameFromIdent(tblID),
+       ).Scan(&icebergType)
+       s.Require().NoError(err)
+       s.True(icebergType.Valid, "CommitTable on a V0 row must stamp 
iceberg_type back to TABLE")
+       s.Equal(sqlcat.TableType, icebergType.String)
+
+       nullIcebergType(tblID)
+
+       toID := s.randomTableIdentifier()
+       toNs := catalog.NamespaceFromIdent(toID)
+       s.Require().NoError(cat.CreateNamespace(ctx, toNs, nil))
+
+       renamed, err := cat.RenameTable(ctx, tblID, toID)
+       s.Require().NoError(err, "RenameTable must accept rows with 
iceberg_type IS NULL")
+       s.Equal(toID, renamed.Identifier())
+}
+
+func (s *SqliteCatalogTestSuite) createLegacyV0Catalog(sqldb *sql.DB) {
+       _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
+               "catalog_name" VARCHAR NOT NULL,
+               "table_namespace" VARCHAR NOT NULL,
+               "table_name" VARCHAR NOT NULL,
+               "metadata_location" VARCHAR,
+               "previous_metadata_location" VARCHAR,
+               PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
+       s.Require().NoError(err)
+       _, err = sqldb.Exec(`CREATE TABLE "iceberg_namespace_properties" (
+               "catalog_name" VARCHAR NOT NULL,
+               "namespace" VARCHAR NOT NULL,
+               "property_key" VARCHAR NOT NULL,
+               "property_value" VARCHAR,
+               PRIMARY KEY ("catalog_name", "namespace", "property_key"))`)
+       s.Require().NoError(err)
+}
+
+func (s *SqliteCatalogTestSuite) loadCatalogV0NoOptIn() *sqlcat.Catalog {
+       cat, err := catalog.Load(context.Background(), "default", 
iceberg.Properties{
+               "uri":             s.catalogUri(),
+               sqlcat.DriverKey:  sqliteshim.ShimName,
+               sqlcat.DialectKey: string(sqlcat.SQLite),
+               "type":            "sql",
+               "warehouse":       "file://" + s.warehouse,
+       })
+       s.Require().NoError(err)
+
+       return cat.(*sqlcat.Catalog)
+}
+
+func (s *SqliteCatalogTestSuite) icebergTypeColumnAbsent(sqldb *sql.DB) {
+       var present int
+       err := sqldb.QueryRow(
+               `SELECT 1 FROM pragma_table_info('iceberg_tables') WHERE name = 
'iceberg_type'`,
+       ).Scan(&present)
+       s.ErrorIs(err, sql.ErrNoRows, "iceberg_type column must remain absent 
on a V0 catalog")
+}
+
+func (s *SqliteCatalogTestSuite) TestV0SchemaFullLifecycleWithoutOptIn() {
+       sqldb := s.getDB()
+       s.confirmNoTables(sqldb)
+       s.createLegacyV0Catalog(sqldb)
+
+       cat := s.loadCatalogV0NoOptIn()
+       ctx := context.Background()
+
+       s.icebergTypeColumnAbsent(sqldb)
+
+       tblID := s.randomTableIdentifier()
+       ns := catalog.NamespaceFromIdent(tblID)
+       s.Require().NoError(cat.CreateNamespace(ctx, ns, nil))
+
+       created, err := cat.CreateTable(ctx, tblID, tableSchemaNested)
+       s.Require().NoError(err, "CreateTable must work on a column-less V0 
schema")
+       s.Equal(tblID, created.Identifier())
+
+       loaded, err := cat.LoadTable(ctx, tblID)
+       s.Require().NoError(err, "LoadTable must work on a column-less V0 
schema")
+       s.Equal(tblID, loaded.Identifier())
+
+       var listed []table.Identifier
+       for ident, iterErr := range cat.ListTables(ctx, ns) {
+               s.Require().NoError(iterErr)
+               listed = append(listed, ident)
+       }
+       s.Require().Len(listed, 1, "ListTables must work on a column-less V0 
schema")
+       s.Equal(tblID, listed[0])
+
+       tx := loaded.NewTransaction()
+       
s.Require().NoError(tx.SetProperties(iceberg.Properties{"v0.commit.test": 
"ok"}))
+       _, err = tx.Commit(ctx)
+       s.Require().NoError(err, "CommitTable must work on a column-less V0 
schema")
+
+       toID := s.randomTableIdentifier()
+       toNs := catalog.NamespaceFromIdent(toID)
+       s.Require().NoError(cat.CreateNamespace(ctx, toNs, nil))
+
+       renamed, err := cat.RenameTable(ctx, tblID, toID)
+       s.Require().NoError(err, "RenameTable must work on a column-less V0 
schema")
+       s.Equal(toID, renamed.Identifier())
+
+       s.Require().NoError(cat.DropTable(ctx, toID), "DropTable must work on a 
column-less V0 schema")
+       _, err = cat.LoadTable(ctx, toID)
+       s.ErrorIs(err, catalog.ErrNoSuchTable)
+
+       s.icebergTypeColumnAbsent(sqldb)
+}
+
+func (s *SqliteCatalogTestSuite) TestV0SchemaViewOperationsUnsupported() {
+       sqldb := s.getDB()
+       s.confirmNoTables(sqldb)
+       s.createLegacyV0Catalog(sqldb)
+
+       cat := s.loadCatalogV0NoOptIn()
+       ctx := context.Background()
+
+       nsName := databaseName()
+       s.Require().NoError(cat.CreateNamespace(ctx, []string{nsName}, nil))
+
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       viewID := []string{nsName, tableName()}
+
+       err := cat.CreateView(ctx, viewID, schema, "SELECT 1", nil)
+       s.ErrorContains(err, "V0", "CreateView must be rejected on a V0 
catalog")
+
+       _, err = cat.CheckViewExists(ctx, viewID)
+       s.ErrorContains(err, "V0", "CheckViewExists must be rejected on a V0 
catalog")
+
+       _, err = cat.LoadView(ctx, viewID)
+       s.ErrorContains(err, "V0", "LoadView must be rejected on a V0 catalog")
+
+       err = cat.DropView(ctx, viewID)
+       s.ErrorContains(err, "V0", "DropView must be rejected on a V0 catalog")
+
+       for _, listErr := range cat.ListViews(ctx, []string{nsName}) {

Review Comment:
   [nit] This `ListViews` assertion can pass vacuously if the iterator yields 
no values. Please assert that the iterator produced the expected error (or 
collect and assert one error) so the V0 behavior is actually exercised.



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