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() {