This is an automated email from the ASF dual-hosted git repository.

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new dfae28fc7 refactor(catalog/sql): share table and view listing logic 
(#2297)
dfae28fc7 is described below

commit dfae28fc73e2f6c905ddd72c720d6421cd128f62
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 25 10:10:03 2026 +0100

    refactor(catalog/sql): share table and view listing logic (#2297)
---
 catalog/sql/listing.go      | 132 ++++++++++++++++++++++++++++++++++++++++++++
 catalog/sql/listing_test.go | 115 ++++++++++++++++++++++++++++++++++++++
 catalog/sql/sql.go          | 124 -----------------------------------------
 3 files changed, 247 insertions(+), 124 deletions(-)

diff --git a/catalog/sql/listing.go b/catalog/sql/listing.go
new file mode 100644
index 000000000..1fc751f59
--- /dev/null
+++ b/catalog/sql/listing.go
@@ -0,0 +1,132 @@
+// 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"
+       "fmt"
+       "iter"
+       "strings"
+
+       "github.com/apache/iceberg-go/catalog"
+       "github.com/apache/iceberg-go/table"
+       "github.com/uptrace/bun"
+)
+
+// ListTables returns the table identifiers in a namespace.
+func (c *Catalog) ListTables(ctx context.Context, namespace table.Identifier) 
iter.Seq2[table.Identifier, error] {
+       tables, err := c.listTablesAll(ctx, namespace)
+
+       return listedIdentifiers(tables, err)
+}
+
+func (c *Catalog) listTablesAll(ctx context.Context, namespace 
table.Identifier) ([]table.Identifier, error) {
+       return c.listObjectsAll(ctx, namespace, TableType)
+}
+
+// ListViews returns a list of view identifiers in the catalog.
+func (c *Catalog) ListViews(ctx context.Context, namespace table.Identifier) 
iter.Seq2[table.Identifier, error] {
+       views, err := c.listViewsAll(ctx, namespace)
+
+       return listedIdentifiers(views, err)
+}
+
+func (c *Catalog) listViewsAll(ctx context.Context, namespace 
table.Identifier) ([]table.Identifier, error) {
+       return c.listObjectsAll(ctx, namespace, ViewType)
+}
+
+// listedIdentifiers converts an eager listing into the catalog's iterator API.
+// Errors are yielded once; callers can also stop iteration early.
+func listedIdentifiers(identifiers []table.Identifier, err error) 
iter.Seq2[table.Identifier, error] {
+       return func(yield func(table.Identifier, error) bool) {
+               if err != nil {
+                       yield(table.Identifier{}, err)
+
+                       return
+               }
+
+               for _, ident := range identifiers {
+                       if !yield(ident, nil) {
+                               return
+                       }
+               }
+       }
+}
+
+// listObjectsAll handles the shared namespace resolution and identifier 
decoding
+// for tables and views while keeping their different schema-version filters.
+func (c *Catalog) listObjectsAll(ctx context.Context, namespace 
table.Identifier, objectType string) ([]table.Identifier, error) {
+       // Legacy V0 catalogs have no iceberg_type column and cannot contain 
views.
+       // Keep their ListViews behavior independent of namespace existence.
+       if c.isV0() && objectType == ViewType {
+               return nil, nil
+       }
+
+       ns := namespaceToString(namespace)
+       if len(namespace) > 0 {
+               var exists bool
+               var err error
+               ns, exists, err = c.resolveNamespaceKey(ctx, namespace)
+               if err != nil {
+                       return nil, err
+               }
+               if !exists {
+                       return nil, fmt.Errorf("%w: %s", 
catalog.ErrNoSuchNamespace, strings.Join(namespace, "."))
+               }
+       }
+
+       objects, err := withReadTx(ctx, c.db, func(ctx context.Context, tx 
bun.Tx) ([]sqlIcebergTable, error) {
+               var objects []sqlIcebergTable
+               sel := tx.NewSelect().Model(&objects).
+                       Where("catalog_name = ?", c.name).
+                       Where("table_namespace = ?", ns)
+
+               switch {
+               case c.isV0():
+                       sel = sel.ExcludeColumn("iceberg_type")
+               case objectType == ViewType:
+                       sel = sel.Where("iceberg_type = ?", ViewType)
+               default:
+                       // V1 must still list tables from before the 
iceberg_type migration.
+                       sel = sel.Where("(iceberg_type = ? OR iceberg_type IS 
NULL)", TableType)
+               }
+
+               err := sel.Scan(ctx)
+
+               return objects, err
+       })
+       if err != nil {
+               kind := "tables"
+               if objectType == ViewType {
+                       kind = "views"
+               }
+
+               return nil, fmt.Errorf("error listing %s for namespace '%s': 
%w", kind, namespace, err)
+       }
+
+       ret := make([]table.Identifier, len(objects))
+       for i, obj := range objects {
+               namespace, err := namespaceFromString(obj.TableNamespace)
+               if err != nil {
+                       return nil, err
+               }
+               ret[i] = append(namespace, obj.TableName)
+       }
+
+       return ret, nil
+}
diff --git a/catalog/sql/listing_test.go b/catalog/sql/listing_test.go
new file mode 100644
index 000000000..4e7cc6c5e
--- /dev/null
+++ b/catalog/sql/listing_test.go
@@ -0,0 +1,115 @@
+// 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_test
+
+import (
+       "context"
+
+       "github.com/apache/iceberg-go"
+       "github.com/apache/iceberg-go/catalog"
+       "github.com/apache/iceberg-go/table"
+)
+
+func (s *SqliteCatalogTestSuite) 
TestListTablesAndViewsShareNamespaceButNotRows() {
+       ctx := context.Background()
+       cat := s.getCatalogSqlite()
+       ns := table.Identifier{databaseName()}
+       s.Require().NoError(cat.CreateNamespace(ctx, ns, nil))
+
+       table1 := table.Identifier{ns[0], "listed_table_1"}
+       table2 := table.Identifier{ns[0], "listed_table_2"}
+       viewID := table.Identifier{ns[0], "listed_view"}
+
+       _, err := cat.CreateTable(ctx, table1, tableSchemaNested)
+       s.Require().NoError(err)
+       _, err = cat.CreateTable(ctx, table2, tableSchemaNested)
+       s.Require().NoError(err)
+
+       viewSchema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       s.Require().NoError(cat.CreateView(ctx, viewID, viewSchema, "SELECT 1", 
nil))
+
+       // A legacy V0 table migrated to V1 can have a NULL iceberg_type.
+       // It must remain visible as a table without appearing in the view list.
+       sqldb := s.getDB()
+       defer sqldb.Close()
+       result, err := sqldb.ExecContext(ctx,
+               `UPDATE iceberg_tables SET iceberg_type = NULL `+
+                       `WHERE catalog_name = ? AND table_namespace = ? AND 
table_name = ?`,
+               cat.Name(), ns[0], table2[1])
+       s.Require().NoError(err)
+       rows, err := result.RowsAffected()
+       s.Require().NoError(err)
+       s.Equal(int64(1), rows)
+
+       var tables []table.Identifier
+       for identifier, listErr := range cat.ListTables(ctx, ns) {
+               s.Require().NoError(listErr)
+               tables = append(tables, identifier)
+       }
+       s.ElementsMatch([]table.Identifier{table1, table2}, tables)
+
+       var views []table.Identifier
+       for identifier, listErr := range cat.ListViews(ctx, ns) {
+               s.Require().NoError(listErr)
+               views = append(views, identifier)
+       }
+       s.Equal([]table.Identifier{viewID}, views)
+
+       // Listing iterators must honor a consumer stopping after the first 
item.
+       seen := 0
+       for _, listErr := range cat.ListTables(ctx, ns) {
+               s.Require().NoError(listErr)
+               seen++
+
+               break
+       }
+       s.Equal(1, seen)
+
+       // Missing namespace errors must be delivered once through either 
iterator.
+       missing := table.Identifier{"missing_" + ns[0]}
+       tableErrors := 0
+       for _, listErr := range cat.ListTables(ctx, missing) {
+               s.ErrorIs(listErr, catalog.ErrNoSuchNamespace)
+               tableErrors++
+       }
+       s.Equal(1, tableErrors)
+
+       viewErrors := 0
+       for _, listErr := range cat.ListViews(ctx, missing) {
+               s.ErrorIs(listErr, catalog.ErrNoSuchNamespace)
+               viewErrors++
+       }
+       s.Equal(1, viewErrors)
+}
+
+func (s *SqliteCatalogTestSuite) TestV0ListViewsIgnoresMissingNamespace() {
+       sqldb := s.getDB()
+       s.createLegacyV0Catalog(sqldb)
+       cat := s.loadCatalogV0NoOptIn()
+
+       // V0 has no iceberg_type column or views. Listing must not query a
+       // missing namespace just to discover that the view list is empty.
+       var seen int
+       for _, err := range cat.ListViews(context.Background(), 
table.Identifier{"missing"}) {
+               s.Require().NoError(err)
+               seen++
+       }
+       s.Zero(seen)
+}
diff --git a/catalog/sql/sql.go b/catalog/sql/sql.go
index 5a52b9300..1b1e216b0 100644
--- a/catalog/sql/sql.go
+++ b/catalog/sql/sql.go
@@ -23,7 +23,6 @@ import (
        "encoding/json"
        "errors"
        "fmt"
-       "iter"
        "log"
        "maps"
        "slices"
@@ -1418,67 +1417,6 @@ func validateNamespacePropertyUpdates(removals []string, 
updates iceberg.Propert
        return nil
 }
 
-func (c *Catalog) ListTables(ctx context.Context, namespace table.Identifier) 
iter.Seq2[table.Identifier, error] {
-       tables, err := c.listTablesAll(ctx, namespace)
-       if err != nil {
-               return func(yield func(table.Identifier, error) bool) {
-                       yield(table.Identifier{}, err)
-               }
-       }
-
-       return func(yield func(table.Identifier, error) bool) {
-               for _, t := range tables {
-                       if !yield(t, nil) {
-                               return
-                       }
-               }
-       }
-}
-
-func (c *Catalog) listTablesAll(ctx context.Context, namespace 
table.Identifier) ([]table.Identifier, error) {
-       ns := namespaceToString(namespace)
-       if len(namespace) > 0 {
-               var exists bool
-               var err error
-               ns, exists, err = c.resolveNamespaceKey(ctx, namespace)
-               if err != nil {
-                       return nil, err
-               }
-               if !exists {
-                       return nil, fmt.Errorf("%w: %s", 
catalog.ErrNoSuchNamespace, strings.Join(namespace, "."))
-               }
-       }
-
-       tables, err := withReadTx(ctx, c.db, func(ctx context.Context, tx 
bun.Tx) ([]sqlIcebergTable, error) {
-               var tables []sqlIcebergTable
-               sel := tx.NewSelect().Model(&tables).
-                       Where("catalog_name = ?", c.name).
-                       Where("table_namespace = ?", ns)
-               if c.isV0() {
-                       sel = sel.ExcludeColumn("iceberg_type")
-               } else {
-                       sel = sel.Where("(iceberg_type = ? OR iceberg_type IS 
NULL)", TableType)
-               }
-               err := sel.Scan(ctx)
-
-               return tables, err
-       })
-       if err != nil {
-               return nil, fmt.Errorf("error listing tables for namespace 
'%s': %w", namespace, err)
-       }
-
-       ret := make([]table.Identifier, len(tables))
-       for i, t := range tables {
-               namespace, err := namespaceFromString(t.TableNamespace)
-               if err != nil {
-                       return nil, err
-               }
-               ret[i] = append(namespace, t.TableName)
-       }
-
-       return ret, nil
-}
-
 func (c *Catalog) ListNamespaces(ctx context.Context, parent table.Identifier) 
([]table.Identifier, error) {
        tableQuery := c.db.NewSelect().Model((*sqlIcebergTable)(nil)).
                Column("table_namespace").Where("catalog_name = ?", c.name)
@@ -1698,68 +1636,6 @@ func (c *Catalog) CreateView(ctx context.Context, 
identifier table.Identifier, s
        return err
 }
 
-// ListViews returns a list of view identifiers in the catalog.
-func (c *Catalog) ListViews(ctx context.Context, namespace table.Identifier) 
iter.Seq2[table.Identifier, error] {
-       views, err := c.listViewsAll(ctx, namespace)
-       if err != nil {
-               return func(yield func(table.Identifier, error) bool) {
-                       yield(table.Identifier{}, err)
-               }
-       }
-
-       return func(yield func(table.Identifier, error) bool) {
-               for _, v := range views {
-                       if !yield(v, nil) {
-                               return
-                       }
-               }
-       }
-}
-
-func (c *Catalog) listViewsAll(ctx context.Context, namespace 
table.Identifier) ([]table.Identifier, error) {
-       if c.isV0() {
-               return nil, nil
-       }
-
-       ns := namespaceToString(namespace)
-       if len(namespace) > 0 {
-               var exists bool
-               var err error
-               ns, exists, err = c.resolveNamespaceKey(ctx, namespace)
-               if err != nil {
-                       return nil, err
-               }
-               if !exists {
-                       return nil, fmt.Errorf("%w: %s", 
catalog.ErrNoSuchNamespace, strings.Join(namespace, "."))
-               }
-       }
-
-       views, err := withReadTx(ctx, c.db, func(ctx context.Context, tx 
bun.Tx) ([]sqlIcebergTable, error) {
-               var views []sqlIcebergTable
-               err := tx.NewSelect().Model(&views).
-                       Where("catalog_name = ?", c.name).
-                       Where("table_namespace = ?", ns).
-                       Where("iceberg_type = ?", ViewType).
-                       Scan(ctx)
-
-               return views, err
-       })
-       if err != nil {
-               return nil, fmt.Errorf("error listing views for namespace '%s': 
%w", namespace, err)
-       }
-
-       ret := make([]table.Identifier, len(views))
-       for i, v := range views {
-               namespace, err := namespaceFromString(v.TableNamespace)
-               if err != nil {
-                       return nil, err
-               }
-               ret[i] = append(namespace, v.TableName)
-       }
-
-       return ret, nil
-}
-
 // DropView deletes a view from the catalog.
 func (c *Catalog) DropView(ctx context.Context, identifier table.Identifier) 
error {
        if c.isV0() {

Reply via email to