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

zeroshade 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 60abc85  feat(catalog/sql): Add views related ops (#414)
60abc85 is described below

commit 60abc852c842859a2597e62c20f11d9f60c7fe1d
Author: Dao Thanh Tung <[email protected]>
AuthorDate: Fri Jun 20 16:14:29 2025 +0100

    feat(catalog/sql): Add views related ops (#414)
    
    Signed-off-by: dttung2905 <[email protected]>
---
 README.md                 |   9 +-
 catalog/internal/utils.go | 158 ++++++++++++++++++++++++++++++
 catalog/sql/sql.go        | 241 +++++++++++++++++++++++++++++++++++++++++++++-
 catalog/sql/sql_test.go   | 144 ++++++++++++++++++++++++++-
 4 files changed, 544 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 63d277c..a699e4d 100644
--- a/README.md
+++ b/README.md
@@ -78,10 +78,11 @@ $ cd iceberg-go/cmd/iceberg && go build .
 | Check Namespace Exists      |  X   |      |          |  X   |  X  |
 | Drop Namespace              |  X   |      |          |  X   |  X  |
 | Update Namespace Properties |  X   |      |          |  X   |  X  |
-| Create View                 |  X   |      |          |      |     |
-| List View                   |  X   |      |          |      |     |
-| Drop View                   |  X   |      |          |      |     |
-| Check View Exists           |  X   |      |          |      |     |
+| Create View                 |  X   |      |          |      |  X  |
+| Load View                   |      |      |          |      |  X  |
+| List View                   |  X   |      |          |      |  X  |
+| Drop View                   |  X   |      |          |      |  X  |
+| Check View Exists           |  X   |      |          |      |  X  |
 
 ### Read/Write Data Support
 
diff --git a/catalog/internal/utils.go b/catalog/internal/utils.go
index 81b8cc3..563756a 100644
--- a/catalog/internal/utils.go
+++ b/catalog/internal/utils.go
@@ -28,6 +28,7 @@ import (
        "regexp"
        "strconv"
        "strings"
+       "time"
 
        "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/catalog"
@@ -251,3 +252,160 @@ func UpdateAndStageTable(ctx context.Context, current 
*table.Table, ident table.
                ),
        }, nil
 }
+
+func CreateViewMetadata(
+       ctx context.Context,
+       catalogName string,
+       nsIdent []string,
+       schema *iceberg.Schema,
+       viewSQL string,
+       loc string,
+       props iceberg.Properties,
+) (metadataLocation string, err error) {
+       versionId := int64(1)
+       timestampMs := time.Now().UnixMilli()
+
+       viewVersion := struct {
+               VersionID       int64             `json:"version-id"`
+               TimestampMs     int64             `json:"timestamp-ms"`
+               SchemaID        int               `json:"schema-id"`
+               Summary         map[string]string `json:"summary"`
+               Operation       string            `json:"operation"`
+               Representations []struct {
+                       Type    string `json:"type"`
+                       SQL     string `json:"sql"`
+                       Dialect string `json:"dialect"`
+               } `json:"representations"`
+               DefaultCatalog   string   `json:"default-catalog"`
+               DefaultNamespace []string `json:"default-namespace"`
+       }{
+               VersionID:   versionId,
+               TimestampMs: timestampMs,
+               SchemaID:    schema.ID,
+               Summary:     map[string]string{"sql": viewSQL},
+               Operation:   "create",
+               Representations: []struct {
+                       Type    string `json:"type"`
+                       SQL     string `json:"sql"`
+                       Dialect string `json:"dialect"`
+               }{
+                       {Type: "sql", SQL: viewSQL, Dialect: "default"},
+               },
+               DefaultCatalog:   catalogName,
+               DefaultNamespace: nsIdent,
+       }
+
+       viewVersionBytes, err := json.Marshal(viewVersion)
+       if err != nil {
+               return "", fmt.Errorf("failed to marshal view version: %w", err)
+       }
+
+       if props == nil {
+               props = iceberg.Properties{}
+       }
+       props["view-version"] = string(viewVersionBytes)
+       props["view-format"] = "iceberg"
+       props["view-sql"] = viewSQL
+
+       metadataLocation = loc + "/metadata/view-" + uuid.New().String() + 
".metadata.json"
+
+       viewUUID := uuid.New().String()
+       props["view-uuid"] = viewUUID
+
+       viewMetadata := map[string]interface{}{
+               "view-uuid":          viewUUID,
+               "format-version":     1,
+               "location":           loc,
+               "schema":             schema,
+               "current-version-id": versionId,
+               "versions": map[string]interface{}{
+                       "1": viewVersion,
+               },
+               "properties": props,
+               "version-log": []map[string]interface{}{
+                       {
+                               "timestamp-ms": timestampMs,
+                               "version-id":   versionId,
+                       },
+               },
+       }
+
+       viewMetadataBytes, err := json.Marshal(viewMetadata)
+       if err != nil {
+               return "", fmt.Errorf("failed to marshal view metadata: %w", 
err)
+       }
+
+       fs, err := io.LoadFS(ctx, props, metadataLocation)
+       if err != nil {
+               return "", fmt.Errorf("failed to load filesystem for view 
metadata: %w", err)
+       }
+
+       wfs, ok := fs.(io.WriteFileIO)
+       if !ok {
+               return "", errors.New("filesystem IO does not support writing")
+       }
+
+       out, err := wfs.Create(metadataLocation)
+       if err != nil {
+               return "", fmt.Errorf("failed to create view metadata file: 
%w", err)
+       }
+       defer out.Close()
+
+       if _, err := out.Write(viewMetadataBytes); err != nil {
+               return "", fmt.Errorf("failed to write view metadata: %w", err)
+       }
+
+       return metadataLocation, nil
+}
+
+func LoadViewMetadata(ctx context.Context,
+       props iceberg.Properties,
+       metadataLocation string,
+       viewName string,
+       namespace string,
+) (map[string]interface{}, error) {
+       // Initial metadata with basic information
+       viewMetadata := map[string]interface{}{
+               "name":              viewName,
+               "namespace":         namespace,
+               "metadata-location": metadataLocation,
+       }
+
+       // Load the filesystem
+       fs, err := io.LoadFS(ctx, props, metadataLocation)
+       if err != nil {
+               return nil, fmt.Errorf("error loading view metadata: %w", err)
+       }
+
+       // Open the metadata file
+       inputFile, err := fs.Open(metadataLocation)
+       if err != nil {
+               return viewMetadata, fmt.Errorf("error encountered loading view 
metadata: %w", err)
+       }
+       defer inputFile.Close()
+
+       // Decode the complete metadata
+       var fullViewMetadata map[string]interface{}
+       if err := json.NewDecoder(inputFile).Decode(&fullViewMetadata); err != 
nil {
+               return viewMetadata, fmt.Errorf("error encountered decoding 
view metadata: %w", err)
+       }
+
+       // Update the metadata with name, namespace and location
+       fullViewMetadata["name"] = viewName
+       fullViewMetadata["namespace"] = namespace
+       fullViewMetadata["metadata-location"] = metadataLocation
+
+       if props, ok := 
fullViewMetadata["properties"].(map[string]interface{}); ok {
+               strProps := make(map[string]string)
+               for k, v := range props {
+                       if str, ok := v.(string); ok {
+                               strProps[k] = str
+                       } else if vJson, err := json.Marshal(v); err == nil {
+                               strProps[k] = string(vJson)
+                       }
+               }
+               fullViewMetadata["properties"] = strProps
+       }
+
+       return fullViewMetadata, nil
+}
diff --git a/catalog/sql/sql.go b/catalog/sql/sql.go
index 762a643..8f2f29f 100644
--- a/catalog/sql/sql.go
+++ b/catalog/sql/sql.go
@@ -61,6 +61,11 @@ const (
        initCatalogTablesKey = "init_catalog_tables"
 )
 
+const (
+       TableType = "TABLE"
+       ViewType  = "VIEW"
+)
+
 func init() {
        catalog.Register("sql", catalog.RegistrarFunc(func(ctx context.Context, 
name string, p iceberg.Properties) (c catalog.Catalog, err error) {
                driver, ok := p[DriverKey]
@@ -133,6 +138,7 @@ type sqlIcebergTable struct {
        CatalogName              string `bun:",pk"`
        TableNamespace           string `bun:",pk"`
        TableName                string `bun:",pk"`
+       IcebergType              string // TableType or ViewType
        MetadataLocation         sql.NullString
        PreviousMetadataLocation sql.NullString
 }
@@ -302,6 +308,7 @@ func (c *Catalog) CreateTable(ctx context.Context, ident 
table.Identifier, sc *i
                        TableNamespace:   ns,
                        TableName:        tblIdent,
                        MetadataLocation: sql.NullString{String: 
staged.MetadataLocation(), Valid: true},
+                       IcebergType:      TableType,
                }).Exec(ctx)
                if err != nil {
                        return fmt.Errorf("failed to create table: %w", err)
@@ -345,9 +352,11 @@ func (c *Catalog) CommitTable(ctx context.Context, tbl 
*table.Table, reqs []tabl
                                CatalogName:              c.name,
                                TableNamespace:           strings.Join(ns, "."),
                                TableName:                tblName,
+                               IcebergType:              TableType,
                                MetadataLocation:         sql.NullString{Valid: 
true, String: staged.MetadataLocation()},
                                PreviousMetadataLocation: sql.NullString{Valid: 
true, String: current.MetadataLocation()},
                        }).WherePK().Where("metadata_location = ?", 
current.MetadataLocation()).
+                               Where("iceberg_type = ?", TableType).
                                Exec(ctx)
                        if err != nil {
                                return fmt.Errorf("error updating table 
information: %w", err)
@@ -369,6 +378,7 @@ func (c *Catalog) CommitTable(ctx context.Context, tbl 
*table.Table, reqs []tabl
                        CatalogName:      c.name,
                        TableNamespace:   strings.Join(ns, "."),
                        TableName:        tblName,
+                       IcebergType:      TableType,
                        MetadataLocation: sql.NullString{Valid: true, String: 
staged.MetadataLocation()},
                }).Exec(ctx)
                if err != nil {
@@ -438,7 +448,7 @@ func (c *Catalog) DropTable(ctx context.Context, identifier 
table.Identifier) er
                        CatalogName:    c.name,
                        TableNamespace: ns,
                        TableName:      tbl,
-               }).WherePK().Exec(ctx)
+               }).WherePK().Where("iceberg_type = ?", TableType).Exec(ctx)
                if err != nil {
                        return fmt.Errorf("failed to delete table entry: %w", 
err)
                }
@@ -489,7 +499,7 @@ func (c *Catalog) RenameTable(ctx context.Context, from, to 
table.Identifier) (*
                        CatalogName:    c.name,
                        TableNamespace: fromNs,
                        TableName:      fromTbl,
-               }).WherePK().
+               }).WherePK().Where("iceberg_type = ?", TableType).
                        Set("table_namespace = ?", toNs).
                        Set("table_name = ?", toTbl).
                        Exec(ctx)
@@ -679,6 +689,7 @@ func (c *Catalog) listTablesAll(ctx context.Context, 
namespace table.Identifier)
                err := tx.NewSelect().Model(&tables).
                        Where("catalog_name = ?", c.name).
                        Where("table_namespace = ?", ns).
+                       Where("iceberg_type = ?", TableType).
                        Scan(ctx)
 
                return tables, err
@@ -814,3 +825,229 @@ func (c *Catalog) UpdateNamespaceProperties(ctx 
context.Context, namespace table
 func (c *Catalog) CheckNamespaceExists(ctx context.Context, namespace 
table.Identifier) (bool, error) {
        return c.namespaceExists(ctx, strings.Join(namespace, "."))
 }
+
+// CreateView creates a new view in the catalog.
+func (c *Catalog) CreateView(ctx context.Context, identifier table.Identifier, 
schema *iceberg.Schema, viewSQL string, props iceberg.Properties) error {
+       nsIdent := catalog.NamespaceFromIdent(identifier)
+       viewIdent := catalog.TableNameFromIdent(identifier)
+       ns := strings.Join(nsIdent, ".")
+
+       exists, err := c.namespaceExists(ctx, ns)
+       if err != nil {
+               return err
+       }
+       if !exists {
+               return fmt.Errorf("%w: %s", catalog.ErrNoSuchNamespace, ns)
+       }
+
+       exists, err = c.CheckViewExists(ctx, identifier)
+       if err != nil {
+               return err
+       }
+       if exists {
+               return fmt.Errorf("%w: %s", catalog.ErrViewAlreadyExists, 
identifier)
+       }
+
+       loc, err := internal.ResolveTableLocation(ctx, "", ns, viewIdent, 
c.props, c.LoadNamespaceProperties)
+       if err != nil {
+               return err
+       }
+
+       metadataLocation, err := internal.CreateViewMetadata(ctx, c.name, 
nsIdent, schema, viewSQL, loc, props)
+       if err != nil {
+               return err
+       }
+       err = withWriteTx(ctx, c.db, func(ctx context.Context, tx bun.Tx) error 
{
+               _, err := tx.NewInsert().Model(&sqlIcebergTable{
+                       CatalogName:      c.name,
+                       TableNamespace:   ns,
+                       TableName:        viewIdent,
+                       IcebergType:      ViewType,
+                       MetadataLocation: sql.NullString{String: 
metadataLocation, Valid: true},
+               }).Exec(ctx)
+               if err != nil {
+                       return fmt.Errorf("failed to create view: %w", err)
+               }
+
+               return nil
+       })
+
+       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 len(namespace) > 0 {
+               exists, err := c.namespaceExists(ctx, strings.Join(namespace, 
"."))
+               if err != nil {
+                       return nil, err
+               }
+               if !exists {
+                       return nil, fmt.Errorf("%w: %s", 
catalog.ErrNoSuchNamespace, strings.Join(namespace, "."))
+               }
+       }
+
+       ns := 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 {
+               ret[i] = append(strings.Split(v.TableNamespace, "."), 
v.TableName)
+       }
+
+       return ret, nil
+}
+
+// DropView deletes a view from the catalog.
+func (c *Catalog) DropView(ctx context.Context, identifier table.Identifier) 
error {
+       ns := strings.Join(catalog.NamespaceFromIdent(identifier), ".")
+       viewName := catalog.TableNameFromIdent(identifier)
+
+       metadataLocation := ""
+       view, err := withReadTx(ctx, c.db, func(ctx context.Context, tx bun.Tx) 
(*sqlIcebergTable, error) {
+               v := new(sqlIcebergTable)
+               err := tx.NewSelect().Model(v).
+                       Where("catalog_name = ?", c.name).
+                       Where("table_namespace = ?", ns).
+                       Where("table_name = ?", viewName).
+                       Where("iceberg_type = ?", ViewType).
+                       Scan(ctx)
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, fmt.Errorf("%w: %s", catalog.ErrNoSuchView, 
identifier)
+               }
+               if err != nil {
+                       return nil, fmt.Errorf("error encountered loading view 
%s: %w", identifier, err)
+               }
+
+               return v, nil
+       })
+       if err != nil {
+               return err
+       }
+
+       if view.MetadataLocation.Valid {
+               metadataLocation = view.MetadataLocation.String
+       }
+
+       err = withWriteTx(ctx, c.db, func(ctx context.Context, tx bun.Tx) error 
{
+               res, err := tx.NewDelete().Model(&sqlIcebergTable{
+                       CatalogName:    c.name,
+                       TableNamespace: ns,
+                       TableName:      viewName,
+               }).WherePK().Where("iceberg_type = ?", ViewType).Exec(ctx)
+               if err != nil {
+                       return fmt.Errorf("failed to delete view entry: %w", 
err)
+               }
+
+               n, err := res.RowsAffected()
+               if err != nil {
+                       return fmt.Errorf("error encountered when deleting view 
entry: %w", err)
+               }
+
+               if n == 0 {
+                       return fmt.Errorf("%w: %s", catalog.ErrNoSuchView, 
identifier)
+               }
+
+               return nil
+       })
+       if err != nil {
+               return err
+       }
+
+       if metadataLocation != "" {
+               fs, err := io.LoadFS(ctx, c.props, metadataLocation)
+               if err != nil {
+                       return nil
+               }
+
+               _ = fs.Remove(metadataLocation)
+       }
+
+       return nil
+}
+
+// CheckViewExists returns true if a view exists in the catalog.
+func (c *Catalog) CheckViewExists(ctx context.Context, identifier 
table.Identifier) (bool, error) {
+       ns := strings.Join(catalog.NamespaceFromIdent(identifier), ".")
+       viewName := catalog.TableNameFromIdent(identifier)
+
+       return withReadTx(ctx, c.db, func(ctx context.Context, tx bun.Tx) 
(bool, error) {
+               exists, err := tx.NewSelect().Model(&sqlIcebergTable{
+                       CatalogName:    c.name,
+                       TableNamespace: ns,
+                       TableName:      viewName,
+               }).WherePK().Where("iceberg_type = ?", ViewType).Exists(ctx)
+               if err != nil {
+                       return false, fmt.Errorf("error checking view 
existence: %w", err)
+               }
+
+               return exists, nil
+       })
+}
+
+// LoadView loads a view from the catalog.
+func (c *Catalog) LoadView(ctx context.Context, identifier table.Identifier) 
(map[string]interface{}, error) {
+       ns := strings.Join(catalog.NamespaceFromIdent(identifier), ".")
+       viewName := catalog.TableNameFromIdent(identifier)
+
+       view, err := withReadTx(ctx, c.db, func(ctx context.Context, tx bun.Tx) 
(*sqlIcebergTable, error) {
+               v := new(sqlIcebergTable)
+               err := tx.NewSelect().Model(v).
+                       Where("catalog_name = ?", c.name).
+                       Where("table_namespace = ?", ns).
+                       Where("table_name = ?", viewName).
+                       Where("iceberg_type = ?", ViewType).
+                       Scan(ctx)
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, fmt.Errorf("%w: %s", catalog.ErrNoSuchView, 
identifier)
+               }
+               if err != nil {
+                       return nil, fmt.Errorf("error encountered loading view 
%s: %w", identifier, err)
+               }
+
+               return v, nil
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       if !view.MetadataLocation.Valid {
+               return nil, fmt.Errorf("%w: %s, metadata location is missing", 
catalog.ErrNoSuchView, identifier)
+       }
+
+       viewMetadata, err := internal.LoadViewMetadata(ctx, c.props, 
view.MetadataLocation.String, viewName, ns)
+       if err != nil {
+               return nil, err
+       }
+
+       return viewMetadata, nil
+}
diff --git a/catalog/sql/sql_test.go b/catalog/sql/sql_test.go
index 496f230..b6fd589 100644
--- a/catalog/sql/sql_test.go
+++ b/catalog/sql/sql_test.go
@@ -280,7 +280,8 @@ func (s *SqliteCatalogTestSuite) 
TestCreationOneTableExists() {
        _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
                "catalog_name" VARCHAR NOT NULL, 
                "table_namespace" VARCHAR NOT NULL, 
-               "table_name" VARCHAR NOT NULL, 
+               "table_name" VARCHAR NOT NULL,
+               "iceberg_type" VARCHAR NOT NULL DEFAULT 'TABLE',
                "metadata_location" VARCHAR, 
                "previous_metadata_location" VARCHAR, 
                PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
@@ -298,7 +299,8 @@ func (s *SqliteCatalogTestSuite) 
TestCreationAllTablesExist() {
        _, err := sqldb.Exec(`CREATE TABLE "iceberg_tables" (
                "catalog_name" VARCHAR NOT NULL, 
                "table_namespace" VARCHAR NOT NULL, 
-               "table_name" VARCHAR NOT NULL, 
+               "table_name" VARCHAR NOT NULL,
+               "iceberg_type" VARCHAR,
                "metadata_location" VARCHAR, 
                "previous_metadata_location" VARCHAR, 
                PRIMARY KEY ("catalog_name", "table_namespace", "table_name"))`)
@@ -1041,6 +1043,144 @@ func (s *SqliteCatalogTestSuite) TestCommitTable() {
        }
 }
 
+func (s *SqliteCatalogTestSuite) TestCreateView() {
+       db := s.getCatalogSqlite()
+       s.Require().NoError(db.CreateSQLTables(context.Background()))
+
+       nsName := databaseName()
+       viewName := tableName()
+       s.Require().NoError(db.CreateNamespace(context.Background(), 
[]string{nsName}, nil))
+
+       viewSQL := "SELECT * FROM test_table"
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       s.Require().NoError(db.CreateView(context.Background(), 
[]string{nsName, viewName}, schema, viewSQL, nil))
+
+       exists, err := db.CheckViewExists(context.Background(), 
[]string{nsName, viewName})
+       s.Require().NoError(err)
+       s.True(exists)
+}
+
+func (s *SqliteCatalogTestSuite) TestDropView() {
+       db := s.getCatalogSqlite()
+       s.Require().NoError(db.CreateSQLTables(context.Background()))
+
+       nsName := databaseName()
+       viewName := tableName()
+       s.Require().NoError(db.CreateNamespace(context.Background(), 
[]string{nsName}, nil))
+
+       viewSQL := "SELECT * FROM test_table"
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       s.Require().NoError(db.CreateView(context.Background(), 
[]string{nsName, viewName}, schema, viewSQL, nil))
+
+       exists, err := db.CheckViewExists(context.Background(), 
[]string{nsName, viewName})
+       s.Require().NoError(err)
+       s.True(exists)
+
+       s.Require().NoError(db.DropView(context.Background(), []string{nsName, 
viewName}))
+
+       exists, err = db.CheckViewExists(context.Background(), []string{nsName, 
viewName})
+       s.Require().NoError(err)
+       s.False(exists)
+
+       err = db.DropView(context.Background(), []string{nsName, "nonexistent"})
+       s.Error(err)
+       s.ErrorIs(err, catalog.ErrNoSuchView)
+}
+
+func (s *SqliteCatalogTestSuite) TestCheckViewExists() {
+       db := s.getCatalogSqlite()
+       s.Require().NoError(db.CreateSQLTables(context.Background()))
+
+       nsName := databaseName()
+       viewName := tableName()
+       s.Require().NoError(db.CreateNamespace(context.Background(), 
[]string{nsName}, nil))
+
+       exists, err := db.CheckViewExists(context.Background(), 
[]string{nsName, viewName})
+       s.Require().NoError(err)
+       s.False(exists)
+
+       viewSQL := "SELECT * FROM test_table"
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       s.Require().NoError(db.CreateView(context.Background(), 
[]string{nsName, viewName}, schema, viewSQL, nil))
+
+       exists, err = db.CheckViewExists(context.Background(), []string{nsName, 
viewName})
+       s.Require().NoError(err)
+       s.True(exists)
+}
+
+func (s *SqliteCatalogTestSuite) TestListViews() {
+       db := s.getCatalogSqlite()
+       s.Require().NoError(db.CreateSQLTables(context.Background()))
+
+       nsName := databaseName()
+       s.Require().NoError(db.CreateNamespace(context.Background(), 
[]string{nsName}, nil))
+
+       viewNames := []string{tableName(), tableName(), tableName()}
+       for _, viewName := range viewNames {
+               viewSQL := "SELECT * FROM test_table"
+               schema := iceberg.NewSchema(1, iceberg.NestedField{
+                       ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+               })
+               s.Require().NoError(db.CreateView(context.Background(), 
[]string{nsName, viewName}, schema, viewSQL, nil))
+       }
+
+       var foundViews []table.Identifier
+       viewsIter := db.ListViews(context.Background(), []string{nsName})
+       for view, err := range viewsIter {
+               s.Require().NoError(err)
+               foundViews = append(foundViews, view)
+       }
+
+       s.Require().Len(foundViews, len(viewNames))
+       for _, view := range foundViews {
+               s.Equal(nsName, view[0])
+               s.Contains(viewNames, view[1])
+       }
+
+       viewsIter = db.ListViews(context.Background(), []string{"nonexistent"})
+       for _, err := range viewsIter {
+               s.Error(err)
+               s.ErrorIs(err, catalog.ErrNoSuchNamespace)
+
+               break
+       }
+}
+
+func (s *SqliteCatalogTestSuite) TestLoadView() {
+       db := s.getCatalogSqlite()
+       s.Require().NoError(db.CreateSQLTables(context.Background()))
+
+       nsName := databaseName()
+       viewName := tableName()
+       s.Require().NoError(db.CreateNamespace(context.Background(), 
[]string{nsName}, nil))
+
+       viewSQL := "SELECT * FROM test_table"
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32, 
Required: true,
+       })
+       props := iceberg.Properties{
+               "comment": "Test view",
+               "owner":   "test-user",
+       }
+       s.Require().NoError(db.CreateView(context.Background(), 
[]string{nsName, viewName}, schema, viewSQL, props))
+
+       viewInfo, err := db.LoadView(context.Background(), []string{nsName, 
viewName})
+       s.Require().NoError(err)
+
+       s.Equal(viewName, viewInfo["name"])
+       s.Equal(nsName, viewInfo["namespace"])
+
+       _, err = db.LoadView(context.Background(), []string{nsName, 
"nonexistent"})
+       s.Error(err)
+       s.ErrorIs(err, catalog.ErrNoSuchView)
+}
+
 func TestSqlCatalog(t *testing.T) {
        suite.Run(t, new(SqliteCatalogTestSuite))
 }

Reply via email to