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 38d1707d7 fix(catalog/hive): keep the caller's view version in 
CreateView (#2353)
38d1707d7 is described below

commit 38d1707d7bebbc8eefa4f2e063da38e90d681f83
Author: Andrei Tserakhau <[email protected]>
AuthorDate: Sat Oct 31 09:43:45 2026 +0100

    fix(catalog/hive): keep the caller's view version in CreateView (#2353)
    
    Hive's CreateView only used the caller's version to pick a SQL string,
    then rebuilt the view from that one representation. The stored metadata
    lost the other representations and the caller's summary, default
    namespace and default catalog, which was always written as "hive".
    Java's BaseMetastoreViewCatalog keeps what the engine passes.
    
    Add view.CreateViewFromVersion, which writes the caller's version as is
    apart from its IDs, and use it from Hive. CreateView and
    CreateViewWithIOProperties now delegate to it, and the SQL catalog can
    reuse it once its view API matches ViewCatalog. The metastore's view
    text still comes from the Hive-dialect SQL.
    
    Refs #2062
---
 catalog/hive/hive.go      | 11 +++------
 catalog/hive/hive_test.go | 52 ++++++++++++++++++++++++++++++++++++++
 view/view.go              | 53 +++++++++++++++++++++++++--------------
 view/view_test.go         | 63 +++++++++++++++++++++++++++++++++++++++++++++++
 website/src/releases.md   |  1 +
 5 files changed, 153 insertions(+), 27 deletions(-)

diff --git a/catalog/hive/hive.go b/catalog/hive/hive.go
index ffb4b8dd6..025246feb 100644
--- a/catalog/hive/hive.go
+++ b/catalog/hive/hive.go
@@ -309,6 +309,7 @@ func (c *Catalog) RegisterTable(ctx context.Context, 
identifier table.Identifier
 
 // CreateView creates a new view in the catalog. It uses the same signature as 
the REST catalog:
 // identifier, version (with SQL representations), schema, and optional 
CreateViewOpt for location and properties.
+// The metastore's view text uses the Hive-dialect SQL, or the first SQL 
representation.
 // Returns the created *view.View, or an error if the namespace is missing, a 
view/table already exists, or creation fails.
 func (c *Catalog) CreateView(ctx context.Context, identifier table.Identifier, 
version *view.Version, schema *iceberg.Schema, opts ...catalog.CreateViewOpt) 
(*view.View, error) {
        database, viewName, err := identifierToViewName(identifier)
@@ -364,9 +365,6 @@ func (c *Catalog) CreateView(ctx context.Context, 
identifier table.Identifier, v
                return nil, err
        }
 
-       defaultNS := catalog.NamespaceFromIdent(identifier)
-       catalogName := "hive"
-
        ioProps := make(iceberg.Properties)
        if c.opts.props != nil {
                maps.Copy(ioProps, c.opts.props)
@@ -375,9 +373,7 @@ func (c *Catalog) CreateView(ctx context.Context, 
identifier table.Identifier, v
                maps.Copy(ioProps, cfg.Properties)
        }
 
-       createdView, err := view.CreateViewWithIOProperties(
-               ctx, catalogName, identifier, freshSchema, representation, 
defaultNS, loc, ioProps, cfg.Properties,
-       )
+       createdView, err := view.CreateViewFromVersion(ctx, identifier, 
freshSchema, version, loc, ioProps, cfg.Properties)
        if err != nil {
                return nil, err
        }
@@ -399,8 +395,7 @@ func (c *Catalog) CreateView(ctx context.Context, 
identifier table.Identifier, v
        return createdView, nil
 }
 
-// sqlFromVersion selects a SQL representation, preferring the Hive dialect.
-// Preserve its dialect so creating the view does not rewrite it to "default".
+// sqlFromVersion picks the SQL for the metastore's view text, preferring the 
Hive dialect.
 func sqlFromVersion(v *view.Version) (view.Representation, error) {
        if v == nil || len(v.Representations) == 0 {
                return view.Representation{}, errors.New("view version has no 
representations")
diff --git a/catalog/hive/hive_test.go b/catalog/hive/hive_test.go
index ecc666b23..f818bd997 100644
--- a/catalog/hive/hive_test.go
+++ b/catalog/hive/hive_test.go
@@ -2074,6 +2074,58 @@ func TestCreateViewDoesNotPersistCatalogProperties(t 
*testing.T) {
        mockClient.AssertExpectations(t)
 }
 
+func TestCreateViewKeepsCallerVersion(t *testing.T) {
+       for _, tt := range []struct {
+               name           string
+               defaultCatalog string
+       }{
+               {name: "default catalog set", defaultCatalog: "prod"},
+               {name: "default catalog unset"},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert := require.New(t)
+
+                       loc := "file://" + 
filepath.ToSlash(filepath.Join(t.TempDir(), "view_loc"))
+                       mockClient := &mockHiveClient{}
+                       mockClient.On("GetDatabase", mock.Anything, 
"test_database").Return(&hive_metastore.Database{Name: "test_database"}, 
nil).Once()
+                       mockClient.On("GetTable", mock.Anything, 
"test_database", "multi_view").Return(nil, errNoSuchObject).Twice()
+                       mockClient.On("CreateTable", mock.Anything, 
mock.MatchedBy(func(tbl *hive_metastore.Table) bool {
+                               return tbl.ViewOriginalText == "SELECT 
hive_sql" && tbl.ViewExpandedText == "SELECT hive_sql"
+                       })).Return(nil).Once()
+
+                       opts := 
[]view.VersionOpt{view.WithVersionSummary(view.VersionSummary{"engine-version": 
"1.0"})}
+                       if tt.defaultCatalog != "" {
+                               opts = append(opts, 
view.WithDefaultViewCatalog(tt.defaultCatalog))
+                       }
+                       ver, err := view.NewVersion(1, 0, []view.Representation{
+                               view.NewRepresentation(view.Representation{SQL: 
"SELECT trino_sql", Dialect: "trino"}),
+                               view.NewRepresentation(view.Representation{SQL: 
"SELECT hive_sql", Dialect: "hive"}),
+                       }, table.Identifier{"other_db"}, opts...)
+                       assert.NoError(err)
+                       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 
1, Name: "col", Type: iceberg.PrimitiveTypes.Int32, Required: true})
+
+                       cat := NewCatalogWithClient(mockClient, 
iceberg.Properties{})
+                       created, err := cat.CreateView(context.Background(), 
TableIdentifier("test_database", "multi_view"), ver, schema,
+                               catalog.WithViewLocation(loc))
+                       assert.NoError(err)
+
+                       f, err := 
os.Open(strings.TrimPrefix(created.MetadataLocation(), "file://"))
+                       assert.NoError(err)
+                       defer f.Close()
+                       md, err := view.ParseMetadata(f)
+                       assert.NoError(err)
+
+                       got := md.CurrentVersion()
+                       assert.Equal(ver.Representations, got.Representations)
+                       assert.Equal(table.Identifier{"other_db"}, 
got.DefaultNamespace)
+                       assert.Equal(tt.defaultCatalog, got.DefaultCatalog)
+                       assert.Equal("1.0", got.Summary["engine-version"])
+
+                       mockClient.AssertExpectations(t)
+               })
+       }
+}
+
 func TestCreateView_VersionNoSQLRepresentation(t *testing.T) {
        assert := require.New(t)
 
diff --git a/view/view.go b/view/view.go
index 0fdcdcddf..a8bdd6c11 100644
--- a/view/view.go
+++ b/view/view.go
@@ -24,6 +24,7 @@ import (
        "fmt"
        "net/url"
        "slices"
+       "time"
 
        "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/internal"
@@ -148,7 +149,7 @@ func CreateView(
        loc string,
        props iceberg.Properties,
 ) (*View, error) {
-       return createView(ctx, catalogName, viewIdent, schema, representation, 
defaultNS, loc, props, props)
+       return CreateViewWithIOProperties(ctx, catalogName, viewIdent, schema, 
representation, defaultNS, loc, props, props)
 }
 
 // CreateViewWithIOProperties creates a view using ioProps to configure the
@@ -165,35 +166,49 @@ func CreateViewWithIOProperties(
        ioProps iceberg.Properties,
        metadataProps iceberg.Properties,
 ) (*View, error) {
-       return createView(ctx, catalogName, viewIdent, schema, representation, 
defaultNS, loc, ioProps, metadataProps)
+       viewVersion, err := NewVersionFromSQL(
+               InitialVersionID,
+               schema.ID,
+               representation,
+               defaultNS,
+               WithDefaultViewCatalog(catalogName),
+       )
+       if err != nil {
+               return nil, err
+       }
+
+       return CreateViewFromVersion(ctx, viewIdent, schema, viewVersion, loc, 
ioProps, metadataProps)
 }
 
-func createView(
+// CreateViewFromVersion creates a view whose first version is version, kept as
+// given apart from its IDs. A nil default namespace defaults to the view's
+// namespace, a nil summary to the engine name, and a zero timestamp to now.
+func CreateViewFromVersion(
        ctx context.Context,
-       catalogName string,
        viewIdent table.Identifier,
        schema *iceberg.Schema,
-       representation Representation,
-       defaultNS table.Identifier,
+       version *Version,
        loc string,
        ioProps iceberg.Properties,
        metadataProps iceberg.Properties,
 ) (*View, error) {
-       versionId := int64(1)
-
-       builder := NewMetadataBuilder()
+       if version == nil {
+               return nil, fmt.Errorf("%w: view version is required", 
ErrInvalidViewMetadata)
+       }
 
-       viewVersion, err := NewVersionFromSQL(
-               versionId,
-               schema.ID,
-               representation,
-               defaultNS,
-               WithDefaultViewCatalog(catalogName),
-       )
-       if err != nil {
-               return nil, err
+       viewVersion := version.Clone()
+       viewVersion.VersionID = InitialVersionID
+       if viewVersion.DefaultNamespace == nil && len(viewIdent) > 0 {
+               viewVersion.DefaultNamespace = 
slices.Clone(viewIdent[:len(viewIdent)-1])
        }
-       viewMD, err := builder.
+       if viewVersion.Summary == nil {
+               viewVersion.Summary = VersionSummary{viewEngineProperty: 
defaultViewEngine}
+       }
+       if viewVersion.TimestampMS == 0 {
+               viewVersion.TimestampMS = time.Now().UnixMilli()
+       }
+
+       viewMD, err := NewMetadataBuilder().
                SetLoc(loc).
                SetCurrentVersion(
                        viewVersion,
diff --git a/view/view_test.go b/view/view_test.go
index fbfad8fd7..0a9d29a35 100644
--- a/view/view_test.go
+++ b/view/view_test.go
@@ -23,8 +23,10 @@ import (
        "net/url"
        "testing"
 
+       "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/internal/iomock"
        iceio "github.com/apache/iceberg-go/io"
+       "github.com/apache/iceberg-go/table"
        "github.com/stretchr/testify/mock"
        "github.com/stretchr/testify/require"
        "github.com/stretchr/testify/suite"
@@ -146,6 +148,67 @@ func TestCreateViewReturnsMetadataCloseError(t *testing.T) 
{
        require.Nil(t, createdView)
 }
 
+func TestCreateViewFromVersionKeepsVersion(t *testing.T) {
+       version, err := NewVersion(7, 3, []Representation{
+               NewRepresentation(Representation{SQL: "SELECT * FROM t", 
Dialect: "trino"}),
+               NewRepresentation(Representation{SQL: "SELECT * FROM t", 
Dialect: "spark"}),
+       }, table.Identifier{"other_ns"},
+               WithDefaultViewCatalog("prod"),
+               WithVersionSummary(VersionSummary{"engine-version": "467"}),
+               WithTimestampMS(1234),
+       )
+       require.NoError(t, err)
+
+       created, err := CreateViewFromVersion(t.Context(), 
table.Identifier{"ns", "v"}, newTestSchema(0), version,
+               "mem://view-from-version/v", nil, iceberg.Properties{"comment": 
"kept"})
+       require.NoError(t, err)
+
+       fs, err := iceio.LoadFS(t.Context(), nil, created.MetadataLocation())
+       require.NoError(t, err)
+       f, err := fs.Open(t.Context(), created.MetadataLocation())
+       require.NoError(t, err)
+       defer f.Close()
+       md, err := ParseMetadata(f)
+       require.NoError(t, err)
+
+       got := md.CurrentVersion()
+       require.Equal(t, int64(InitialVersionID), got.VersionID)
+       require.Equal(t, md.CurrentSchemaID(), got.SchemaID)
+       require.Equal(t, version.Representations, got.Representations)
+       require.Equal(t, table.Identifier{"other_ns"}, got.DefaultNamespace)
+       require.Equal(t, "prod", got.DefaultCatalog)
+       require.Equal(t, VersionSummary{"engine-name": "iceberg-go", 
"engine-version": "467"}, got.Summary)
+       require.Equal(t, int64(1234), got.TimestampMS)
+       require.Equal(t, "kept", md.Properties()["comment"])
+       require.Equal(t, int64(7), version.VersionID, "the caller's version is 
not modified")
+}
+
+func TestCreateViewFromVersionFillsUnsetFields(t *testing.T) {
+       version := &Version{
+               VersionID:       1,
+               Representations: 
[]Representation{NewRepresentation(Representation{SQL: "select 1", Dialect: 
"spark"})},
+       }
+
+       created, err := CreateViewFromVersion(t.Context(), 
table.Identifier{"ns", "v"}, newTestSchema(0), version,
+               "mem://view-from-version-defaults/v", nil, nil)
+       require.NoError(t, err)
+
+       got := created.CurrentVersion()
+       require.Equal(t, table.Identifier{"ns"}, got.DefaultNamespace)
+       require.Equal(t, VersionSummary{"engine-name": "iceberg-go"}, 
got.Summary)
+       require.Positive(t, got.TimestampMS)
+       require.Empty(t, got.DefaultCatalog)
+       require.Nil(t, version.Summary, "the caller's version is not modified")
+       require.Nil(t, version.DefaultNamespace)
+}
+
+func TestCreateViewFromVersionRejectsNilVersion(t *testing.T) {
+       created, err := CreateViewFromVersion(t.Context(), 
table.Identifier{"ns", "v"}, newTestSchema(0), nil,
+               "mem://view-from-version-nil/v", nil, nil)
+       require.ErrorIs(t, err, ErrInvalidViewMetadata)
+       require.Nil(t, created)
+}
+
 func (t *ViewTestSuite) TestIdentifierReturnsDefensiveCopy() {
        identifier := []string{"namespace", "view"}
        vw := New(identifier, nil, "metadata.json")
diff --git a/website/src/releases.md b/website/src/releases.md
index 598ff1136..b2c461e99 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -62,6 +62,7 @@ Per-release notes (highlights, breaking changes, 
contributors) are published on
 - **Data file block size setter removed.** 
`iceberg.DataFileBuilder.BlockSizeInBytes` is removed. New v1 manifests use the 
Avro schema's 64 MiB default when the field is unset. V2/v3 omit this 
deprecated field, and legacy v1 values remain readable.
 - **View creation APIs take a named SQL representation.** 
`view.NewVersionFromSQL`, `view.CreateView`, `view.CreateViewWithIOProperties`, 
and `sql.Catalog.CreateView` now accept a `view.Representation{SQL: sql, 
Dialect: dialect}` instead of positional SQL/dialect strings. The dialect is 
required; Hive preserves the selected representation's dialect.
 - **View SQL representation construction uses named fields.** Replace 
`view.NewRepresentation(sql, dialect)` with 
`view.NewRepresentation(view.Representation{SQL: sql, Dialect: dialect})`. 
Serialized JSON is unchanged.
+- **Hive views keep the caller's view version.** `hive.Catalog.CreateView` now 
stores every representation, the summary, the default namespace and 
`default-catalog` as passed, instead of one representation with 
`default-catalog` set to `"hive"`.
 - **Reserved table properties now use a read-only API.** Replace references to 
`table.ReservedProperties` with `table.ReservedProperties()`. The function 
returns a fresh slice, so callers cannot change metadata validation by 
modifying it.
 - **Fixed and decimal constructors return errors.** `iceberg.FixedTypeOf` and 
`iceberg.DecimalTypeOf` now return `(type, error)` instead of panicking for 
invalid inputs. Use `MustFixedTypeOf` or `MustDecimalTypeOf` only for 
known-valid values.
 - **SQL UDF representations now use struct literals.** The 
`udf.NewSQLRepresentation(dialect, sql)` function, available since v0.7.0, is 
removed. Use `udf.SQLRepresentation{Dialect: "trino", SQL: "x + 1"}` instead. 
Empty fields and duplicate dialects are checked during metadata validation; 
serialized JSON is unchanged.

Reply via email to