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.