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 362f47eb0 refactor(view)!: require named SQL fields and explicit
dialect (#2324)
362f47eb0 is described below
commit 362f47eb0e3d73fa405a2633a82f91fb72c52e59
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 27 01:49:27 2026 +0100
refactor(view)!: require named SQL fields and explicit dialect (#2324)
* refactor(view)!: require named SQL fields and explicit dialect
Signed-off-by: Minh Vu <[email protected]>
* style(view): satisfy nlreturn lint checks
* fix(view): pass SQL dialect in catalog integration test
* chore: sync release notes with main to resolve conflicts
* fix(view): pass SQL and dialect as a single representation across APIs
---------
Signed-off-by: Minh Vu <[email protected]>
---
catalog/hive/hive.go | 30 ++++++------
catalog/hive/hive_integration_test.go | 9 ++--
catalog/hive/hive_test.go | 14 +++---
catalog/hive/view_dialect_test.go | 73 +++++++++++++++++++++++++++++
catalog/rest/rest_integration_test.go | 6 +--
catalog/rest/rest_test.go | 8 ++--
catalog/sql/drop_namespace_internal_test.go | 3 +-
catalog/sql/listing_test.go | 3 +-
catalog/sql/sql.go | 4 +-
catalog/sql/sql_integration_test.go | 3 +-
catalog/sql/sql_test.go | 33 ++++++-------
view/metadata.go | 20 ++++----
view/metadata_builder_test.go | 31 +++++++-----
view/metadata_test.go | 11 ++++-
view/view.go | 14 +++---
view/view_test.go | 4 +-
website/src/api.md | 2 +-
website/src/releases.md | 4 ++
18 files changed, 186 insertions(+), 86 deletions(-)
diff --git a/catalog/hive/hive.go b/catalog/hive/hive.go
index a759748f5..83de89d18 100644
--- a/catalog/hive/hive.go
+++ b/catalog/hive/hive.go
@@ -359,7 +359,7 @@ func (c *Catalog) CreateView(ctx context.Context,
identifier table.Identifier, v
return nil, err
}
- viewSQL, err := sqlFromVersion(version)
+ representation, err := sqlFromVersion(version)
if err != nil {
return nil, err
}
@@ -376,7 +376,7 @@ func (c *Catalog) CreateView(ctx context.Context,
identifier table.Identifier, v
}
createdView, err := view.CreateViewWithIOProperties(
- ctx, catalogName, identifier, freshSchema, viewSQL, defaultNS,
loc, ioProps, cfg.Properties,
+ ctx, catalogName, identifier, freshSchema, representation,
defaultNS, loc, ioProps, cfg.Properties,
)
if err != nil {
return nil, err
@@ -387,7 +387,7 @@ func (c *Catalog) CreateView(ctx context.Context,
identifier table.Identifier, v
maps.Copy(viewProps, cfg.Properties)
}
- hiveTbl := constructHiveViewTable(database, viewName, loc,
createdView.MetadataLocation(), freshSchema, viewSQL, viewProps)
+ hiveTbl := constructHiveViewTable(database, viewName, loc,
createdView.MetadataLocation(), freshSchema, representation.SQL, viewProps)
if err := c.client.CreateTable(ctx, hiveTbl); err != nil {
if isAlreadyExistsError(err) {
return nil, fmt.Errorf("%w: %s.%s",
catalog.ErrViewAlreadyExists, database, viewName)
@@ -399,28 +399,30 @@ func (c *Catalog) CreateView(ctx context.Context,
identifier table.Identifier, v
return createdView, nil
}
-// sqlFromVersion returns the SQL from the first SQL representation in
version, preferring dialect "hive".
-func sqlFromVersion(v *view.Version) (string, error) {
+// sqlFromVersion selects a SQL representation, preferring the Hive dialect.
+// Preserve its dialect so creating the view does not rewrite it to "default".
+func sqlFromVersion(v *view.Version) (view.Representation, error) {
if v == nil || len(v.Representations) == 0 {
- return "", errors.New("view version has no representations")
+ return view.Representation{}, errors.New("view version has no
representations")
}
- var fallback string
- for _, r := range v.Representations {
+ var fallback *view.Representation
+ for i := range v.Representations {
+ r := &v.Representations[i]
if r.Type != "sql" {
continue
}
if strings.EqualFold(r.Dialect, "hive") {
- return r.SQL, nil
+ return *r, nil
}
- if fallback == "" {
- fallback = r.SQL
+ if fallback == nil {
+ fallback = r
}
}
- if fallback != "" {
- return fallback, nil
+ if fallback != nil {
+ return *fallback, nil
}
- return "", errors.New("view version has no SQL representation")
+ return view.Representation{}, errors.New("view version has no SQL
representation")
}
func (c *Catalog) DropTable(ctx context.Context, identifier table.Identifier)
(err error) {
diff --git a/catalog/hive/hive_integration_test.go
b/catalog/hive/hive_integration_test.go
index a5202f154..6f99ff1e9 100644
--- a/catalog/hive/hive_integration_test.go
+++ b/catalog/hive/hive_integration_test.go
@@ -454,7 +454,7 @@ func TestHiveIntegrationCreateView(t *testing.T) {
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
viewSQL := "SELECT 1 AS col"
- ver, err := view.NewVersionFromSQL(1, 0, viewSQL,
table.Identifier{dbName})
+ ver, err := view.NewVersionFromSQL(1, 0, view.Representation{SQL:
viewSQL, Dialect: "hive"}, table.Identifier{dbName})
assert.NoError(err)
viewLocation := getTestTableLocation() + "/" + dbName + "/" + viewName
@@ -476,6 +476,7 @@ func TestHiveIntegrationCreateView(t *testing.T) {
assert.Len(loaded.CurrentVersion().Representations, 1)
assert.Equal("sql", loaded.CurrentVersion().Representations[0].Type)
assert.Equal(viewSQL, loaded.CurrentVersion().Representations[0].SQL)
+ assert.Equal("hive", loaded.CurrentVersion().Representations[0].Dialect)
}
func TestHiveIntegrationCreateViewThenDrop(t *testing.T) {
@@ -494,7 +495,7 @@ func TestHiveIntegrationCreateViewThenDrop(t *testing.T) {
defer cat.DropNamespace(context.TODO(), DatabaseIdentifier(dbName))
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "id",
Type: iceberg.PrimitiveTypes.Int64, Required: true})
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT 1 AS id",
table.Identifier{dbName})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
1 AS id", Dialect: "hive"}, table.Identifier{dbName})
viewLocation := getTestTableLocation() + "/" + dbName + "/" + viewName
_, err = cat.CreateView(context.TODO(), TableIdentifier(dbName,
viewName), ver, schema,
@@ -541,7 +542,7 @@ func TestHiveIntegrationCreateView_TableConflict(t
*testing.T) {
assert.NoError(err)
defer cat.DropTable(context.TODO(), TableIdentifier(dbName, tableName))
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT * FROM t1",
table.Identifier{dbName})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
* FROM t1", Dialect: "hive"}, table.Identifier{dbName})
viewLocation := getTestTableLocation() + "/" + dbName + "/" + tableName
+ "_view"
_, err = cat.CreateView(context.TODO(), TableIdentifier(dbName,
tableName), ver, schema,
catalog.WithViewLocation(viewLocation),
@@ -566,7 +567,7 @@ func TestHiveIntegrationCreateView_ViewConflict(t
*testing.T) {
defer cat.DropNamespace(context.TODO(), DatabaseIdentifier(dbName))
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT 1 AS col",
table.Identifier{dbName})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
1 AS col", Dialect: "hive"}, table.Identifier{dbName})
viewLocation := getTestTableLocation() + "/" + dbName + "/" + viewName
_, err = cat.CreateView(context.TODO(), TableIdentifier(dbName,
viewName), ver, schema,
diff --git a/catalog/hive/hive_test.go b/catalog/hive/hive_test.go
index 9190b7571..9b231b39d 100644
--- a/catalog/hive/hive_test.go
+++ b/catalog/hive/hive_test.go
@@ -1928,7 +1928,7 @@ func TestCreateView_NamespaceNotFound(t *testing.T) {
cat := NewCatalogWithClient(mockClient, iceberg.Properties{})
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT 1",
table.Identifier{"missing_db"})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
1", Dialect: "hive"}, table.Identifier{"missing_db"})
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
_, err := cat.CreateView(context.Background(),
TableIdentifier("missing_db", "v1"), ver, schema,
catalog.WithViewLocation("file:///tmp/loc"))
@@ -1947,7 +1947,7 @@ func TestCreateView_ViewAlreadyExists(t *testing.T) {
cat := NewCatalogWithClient(mockClient, iceberg.Properties{})
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT 1",
table.Identifier{"test_database"})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
1", Dialect: "hive"}, table.Identifier{"test_database"})
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
_, err := cat.CreateView(context.Background(),
TableIdentifier("test_database", "test_view"), ver, schema,
catalog.WithViewLocation("file:///tmp/loc"))
@@ -1967,7 +1967,7 @@ func TestCreateView_TableAlreadyExists(t *testing.T) {
cat := NewCatalogWithClient(mockClient, iceberg.Properties{})
- ver, _ := view.NewVersionFromSQL(1, 0, "SELECT 1",
table.Identifier{"test_database"})
+ ver, _ := view.NewVersionFromSQL(1, 0, view.Representation{SQL: "SELECT
1", Dialect: "hive"}, table.Identifier{"test_database"})
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
_, err := cat.CreateView(context.Background(),
TableIdentifier("test_database", "test_table"), ver, schema,
catalog.WithViewLocation("file:///tmp/loc"))
@@ -1982,7 +1982,7 @@ func TestCreateView_InvalidIdentifier(t *testing.T) {
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
for _, ident := range []table.Identifier{nil, {}, {"only_db"}} {
- ver, err := view.NewVersionFromSQL(1, 0, "SELECT 1",
table.Identifier{"db"})
+ ver, err := view.NewVersionFromSQL(1, 0,
view.Representation{SQL: "SELECT 1", Dialect: "hive"}, table.Identifier{"db"})
require.NoError(t, err)
_, err = cat.CreateView(context.Background(), ident, ver,
schema)
require.ErrorIs(t, err, catalog.ErrNoSuchView)
@@ -2015,7 +2015,7 @@ func TestCreateView_Success(t *testing.T) {
cat := NewCatalogWithClient(mockClient, iceberg.Properties{})
- ver, err := view.NewVersionFromSQL(1, 0, "SELECT 1 AS col",
table.Identifier{"test_database"})
+ ver, err := view.NewVersionFromSQL(1, 0, view.Representation{SQL:
"SELECT 1 AS col", Dialect: "hive"}, table.Identifier{"test_database"})
assert.NoError(err)
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
@@ -2046,7 +2046,7 @@ func TestCreateViewDoesNotPersistCatalogProperties(t
*testing.T) {
mockClient.On("CreateTable", mock.Anything,
mock.Anything).Return(nil).Once()
cat := NewCatalogWithClient(mockClient, catalogProps)
- ver, err := view.NewVersionFromSQL(1, 0, "SELECT 1 AS col",
table.Identifier{"test_database"})
+ ver, err := view.NewVersionFromSQL(1, 0, view.Representation{SQL:
"SELECT 1 AS col", Dialect: "hive"}, table.Identifier{"test_database"})
assert.NoError(err)
schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: "col",
Type: iceberg.PrimitiveTypes.Int32, Required: true})
@@ -2083,7 +2083,7 @@ func TestCreateView_VersionNoSQLRepresentation(t
*testing.T) {
cat := NewCatalogWithClient(mockClient, iceberg.Properties{})
- ver, err := view.NewVersionFromSQL(1, 0, "SELECT 1",
table.Identifier{"test_database"})
+ ver, err := view.NewVersionFromSQL(1, 0, view.Representation{SQL:
"SELECT 1", Dialect: "hive"}, table.Identifier{"test_database"})
assert.NoError(err)
ver.Representations = nil // no SQL representation
diff --git a/catalog/hive/view_dialect_test.go
b/catalog/hive/view_dialect_test.go
new file mode 100644
index 000000000..e38aae76c
--- /dev/null
+++ b/catalog/hive/view_dialect_test.go
@@ -0,0 +1,73 @@
+// 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 hive
+
+import (
+ "testing"
+
+ "github.com/apache/iceberg-go/view"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSQLFromVersionPreservesDialect(t *testing.T) {
+ tests := []struct {
+ name string
+ version *view.Version
+ wantSQL string
+ wantDialect string
+ wantErr string
+ }{
+ {
+ name: "prefer Hive representation",
+ version: &view.Version{Representations:
[]view.Representation{
+ view.NewRepresentation(view.Representation{SQL:
"SELECT trino", Dialect: "trino"}),
+ view.NewRepresentation(view.Representation{SQL:
"SELECT hive", Dialect: "HIVE"}),
+ }},
+ wantSQL: "SELECT hive",
+ wantDialect: "HIVE",
+ },
+ {
+ name: "keep fallback dialect",
+ version: &view.Version{Representations:
[]view.Representation{
+ view.NewRepresentation(view.Representation{SQL:
"SELECT spark", Dialect: "spark"}),
+ view.NewRepresentation(view.Representation{SQL:
"SELECT trino", Dialect: "trino"}),
+ }},
+ wantSQL: "SELECT spark",
+ wantDialect: "spark",
+ },
+ {name: "nil version", wantErr: "no representations"},
+ {
+ name: "no SQL representation",
+ version: &view.Version{Representations:
[]view.Representation{{Type: "other"}}},
+ wantErr: "no SQL representation",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ representation, err := sqlFromVersion(tt.version)
+ if tt.wantErr != "" {
+ require.ErrorContains(t, err, tt.wantErr)
+
+ return
+ }
+ require.NoError(t, err)
+ require.Equal(t, tt.wantSQL, representation.SQL)
+ require.Equal(t, tt.wantDialect, representation.Dialect)
+ })
+ }
+}
diff --git a/catalog/rest/rest_integration_test.go
b/catalog/rest/rest_integration_test.go
index ea9d66cbd..419021762 100644
--- a/catalog/rest/rest_integration_test.go
+++ b/catalog/rest/rest_integration_test.go
@@ -213,7 +213,7 @@ func (s *RestIntegrationSuite) TestCreateView() {
s.True(exists)
// Create a view
- viewRepr := view.NewRepresentation(fmt.Sprintf("SELECT * FROM %s.%s",
TestNamespaceIdent, "test-table"), "trino")
+ viewRepr := view.NewRepresentation(view.Representation{SQL:
fmt.Sprintf("SELECT * FROM %s.%s", TestNamespaceIdent, "test-table"), Dialect:
"trino"})
viewVersion, err := view.NewVersion(1, 1,
[]view.Representation{viewRepr}, table.Identifier{TestNamespaceIdent})
s.Require().NoError(err)
@@ -254,7 +254,7 @@ func (s *RestIntegrationSuite) TestUpdateView() {
s.True(exists)
// Create a view
- viewRepr := view.NewRepresentation(fmt.Sprintf("SELECT * FROM %s.%s",
TestNamespaceIdent, "test-table"), "trino")
+ viewRepr := view.NewRepresentation(view.Representation{SQL:
fmt.Sprintf("SELECT * FROM %s.%s", TestNamespaceIdent, "test-table"), Dialect:
"trino"})
defaultViewNS := catalog.ToIdentifier(location, TestNamespaceIdent)
viewVersion, err := view.NewVersion(1, 1,
[]view.Representation{viewRepr}, defaultViewNS)
@@ -309,7 +309,7 @@ func (s *RestIntegrationSuite) TestRenameView() {
fromIdent := catalog.ToIdentifier(TestNamespaceIdent, "test-view")
toIdent := catalog.ToIdentifier(TestNamespaceIdent, "renamed-view")
- viewRepr := view.NewRepresentation(fmt.Sprintf("SELECT * FROM %s.%s",
TestNamespaceIdent, "test-table"), "trino")
+ viewRepr := view.NewRepresentation(view.Representation{SQL:
fmt.Sprintf("SELECT * FROM %s.%s", TestNamespaceIdent, "test-table"), Dialect:
"trino"})
viewVersion, err := view.NewVersion(1, 1,
[]view.Representation{viewRepr}, table.Identifier{TestNamespaceIdent})
s.Require().NoError(err)
diff --git a/catalog/rest/rest_test.go b/catalog/rest/rest_test.go
index ef4901c58..a46756c12 100644
--- a/catalog/rest/rest_test.go
+++ b/catalog/rest/rest_test.go
@@ -3491,7 +3491,7 @@ func (r *RestCatalogSuite) TestCreateView200() {
"owner": "admin",
"view-format": "iceberg",
}
- reprs := []view.Representation{view.NewRepresentation(exampleViewSQL,
"default")}
+ reprs :=
[]view.Representation{view.NewRepresentation(view.Representation{SQL:
exampleViewSQL, Dialect: "default"})}
version, err := view.NewVersion(1, 0, reprs, table.Identifier{ns},
view.WithDefaultViewCatalog("default-catalog"),
view.WithTimestampMS(0))
@@ -3555,7 +3555,7 @@ func (r *RestCatalogSuite) TestCreateView409() {
Required: true,
})
sql := "SELECT * FROM table"
- reprs := []view.Representation{view.NewRepresentation(sql, "default")}
+ reprs :=
[]view.Representation{view.NewRepresentation(view.Representation{SQL: sql,
Dialect: "default"})}
version, err := view.NewVersion(1, 1, reprs, table.Identifier{ns})
r.Require().NoError(err)
@@ -3590,7 +3590,7 @@ func (r *RestCatalogSuite) TestCreateView404() {
})
sql := "SELECT * FROM table"
reprs := []view.Representation{
- view.NewRepresentation(sql, "spark"),
+ view.NewRepresentation(view.Representation{SQL: sql, Dialect:
"spark"}),
}
version, err := view.NewVersion(1, 1, reprs, table.Identifier{ns})
r.Require().NoError(err)
@@ -3693,7 +3693,7 @@ func (r *RestCatalogSuite) TestCreateViewLabels() {
schema := iceberg.NewSchemaWithIdentifiers(0, []int{1},
iceberg.NestedField{
ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32,
Required: true,
})
- reprs := []view.Representation{view.NewRepresentation(exampleViewSQL,
"default")}
+ reprs :=
[]view.Representation{view.NewRepresentation(view.Representation{SQL:
exampleViewSQL, Dialect: "default"})}
version, err := view.NewVersion(1, 0, reprs, table.Identifier{ns},
view.WithDefaultViewCatalog("default-catalog"),
view.WithTimestampMS(0))
r.Require().NoError(err)
diff --git a/catalog/sql/drop_namespace_internal_test.go
b/catalog/sql/drop_namespace_internal_test.go
index 0f3afda63..113531879 100644
--- a/catalog/sql/drop_namespace_internal_test.go
+++ b/catalog/sql/drop_namespace_internal_test.go
@@ -31,6 +31,7 @@ import (
"github.com/apache/iceberg-go/catalog"
_ "github.com/apache/iceberg-go/io/gocloud"
"github.com/apache/iceberg-go/table"
+ "github.com/apache/iceberg-go/view"
mysql "github.com/go-sql-driver/mysql"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
@@ -234,7 +235,7 @@ func TestDropNamespaceDoesNotRaceWithObjectCreation(t
*testing.T) {
{
name: "create view",
create: func(_ *testing.T, ctx context.Context, cat
*Catalog, ident table.Identifier) error {
- return cat.CreateView(ctx, ident, schema,
"SELECT 1", nil)
+ return cat.CreateView(ctx, ident, schema,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil)
},
exists: func(ctx context.Context, cat *Catalog, ident
table.Identifier) (bool, error) {
return cat.CheckViewExists(ctx, ident)
diff --git a/catalog/sql/listing_test.go b/catalog/sql/listing_test.go
index 4e7cc6c5e..910101a80 100644
--- a/catalog/sql/listing_test.go
+++ b/catalog/sql/listing_test.go
@@ -23,6 +23,7 @@ import (
"github.com/apache/iceberg-go"
"github.com/apache/iceberg-go/catalog"
"github.com/apache/iceberg-go/table"
+ "github.com/apache/iceberg-go/view"
)
func (s *SqliteCatalogTestSuite)
TestListTablesAndViewsShareNamespaceButNotRows() {
@@ -43,7 +44,7 @@ func (s *SqliteCatalogTestSuite)
TestListTablesAndViewsShareNamespaceButNotRows(
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))
+ s.Require().NoError(cat.CreateView(ctx, viewID, viewSchema,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, 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.
diff --git a/catalog/sql/sql.go b/catalog/sql/sql.go
index c16ba6ae4..57d485313 100644
--- a/catalog/sql/sql.go
+++ b/catalog/sql/sql.go
@@ -1566,7 +1566,7 @@ func (c *Catalog) CheckNamespaceExists(ctx
context.Context, namespace table.Iden
}
// 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 {
+func (c *Catalog) CreateView(ctx context.Context, identifier table.Identifier,
schema *iceberg.Schema, representation view.Representation, props
iceberg.Properties) error {
if c.isV0() {
return errViewsUnsupportedOnV0
}
@@ -1591,7 +1591,7 @@ func (c *Catalog) CreateView(ctx context.Context,
identifier table.Identifier, s
return err
}
- createdView, err := view.CreateView(ctx, c.name, identifier, schema,
viewSQL, nsIdent, loc, props)
+ createdView, err := view.CreateView(ctx, c.name, identifier, schema,
representation, nsIdent, loc, props)
if err != nil {
return err
}
diff --git a/catalog/sql/sql_integration_test.go
b/catalog/sql/sql_integration_test.go
index 802816465..25e3a097f 100644
--- a/catalog/sql/sql_integration_test.go
+++ b/catalog/sql/sql_integration_test.go
@@ -36,6 +36,7 @@ import (
"github.com/apache/iceberg-go/io"
_ "github.com/apache/iceberg-go/io/gocloud"
"github.com/apache/iceberg-go/table"
+ "github.com/apache/iceberg-go/view"
"github.com/stretchr/testify/suite"
)
@@ -237,7 +238,7 @@ func (s *SQLIntegrationSuite) TestCreateView() {
// Create a view
viewSQL := fmt.Sprintf("SELECT * FROM %s.%s", TestNamespaceIdent,
"test-table")
- err = s.cat.CreateView(s.ctx, catalog.ToIdentifier(TestNamespaceIdent,
"test-view"), tableSchemaSimple, viewSQL, iceberg.Properties{"foobar": "baz"})
+ err = s.cat.CreateView(s.ctx, catalog.ToIdentifier(TestNamespaceIdent,
"test-view"), tableSchemaSimple, view.Representation{SQL: viewSQL, Dialect:
"spark"}, iceberg.Properties{"foobar": "baz"})
s.Require().NoError(err)
exists, err = s.cat.CheckViewExists(s.ctx,
catalog.ToIdentifier(TestNamespaceIdent, "test-view"))
diff --git a/catalog/sql/sql_test.go b/catalog/sql/sql_test.go
index 838d6d75a..e5005314f 100644
--- a/catalog/sql/sql_test.go
+++ b/catalog/sql/sql_test.go
@@ -49,6 +49,7 @@ import (
_ "github.com/apache/iceberg-go/io/gocloud"
"github.com/apache/iceberg-go/metrics"
"github.com/apache/iceberg-go/table"
+ "github.com/apache/iceberg-go/view"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -639,7 +640,7 @@ func (s *SqliteCatalogTestSuite)
TestV0SchemaViewOperationsUnsupported() {
})
viewID := []string{nsName, tableName()}
- err := cat.CreateView(ctx, viewID, schema, "SELECT 1", nil)
+ err := cat.CreateView(ctx, viewID, schema, view.Representation{SQL:
"SELECT 1", Dialect: "spark"}, nil)
s.ErrorContains(err, "V0", "CreateView must be rejected on a V0
catalog")
_, err = cat.LoadView(ctx, viewID)
@@ -1319,7 +1320,7 @@ func (s *SqliteCatalogTestSuite)
TestDottedNamespaceUsesOwnLocationProperties()
s.Equal(location+"/orders", createdTable.Location())
viewIdent := append(slices.Clone(namespace), "orders_view")
- s.Require().NoError(cat.CreateView(ctx, viewIdent,
tableSchemaNested, "SELECT * FROM orders", nil))
+ s.Require().NoError(cat.CreateView(ctx, viewIdent,
tableSchemaNested, view.Representation{SQL: "SELECT * FROM orders", Dialect:
"spark"}, nil))
createdView, err := cat.LoadView(ctx, viewIdent)
s.Require().NoError(err)
s.Equal(location+"/orders_view", createdView.Location())
@@ -2117,7 +2118,7 @@ func (s *SqliteCatalogTestSuite)
TestDropNamespaceRejectsViewOnlyNamespace() {
})
s.Require().NoError(cat.CreateNamespace(ctx, namespace, nil))
- s.Require().NoError(cat.CreateView(ctx, viewID, schema, viewSQL, nil))
+ s.Require().NoError(cat.CreateView(ctx, viewID, schema,
view.Representation{SQL: viewSQL, Dialect: "spark"}, nil))
err := cat.DropNamespace(ctx, namespace)
s.ErrorIs(err, catalog.ErrNamespaceNotEmpty)
@@ -2854,8 +2855,8 @@ func (s *SqliteCatalogTestSuite) TestCreateView() {
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))
- s.Require().NoError(db.CreateView(context.Background(),
append(nestedNamespace, viewName), schema, viewSQL, nil))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, nil))
+ s.Require().NoError(db.CreateView(context.Background(),
append(nestedNamespace, viewName), schema, view.Representation{SQL: viewSQL,
Dialect: "spark"}, nil))
exists, err := db.CheckViewExists(context.Background(),
[]string{nsName, viewName})
s.Require().NoError(err)
@@ -2901,7 +2902,7 @@ func TestViewOperationsRejectInvalidIdentifiers(t
*testing.T) {
for _, test := range invalid {
t.Run(test.name, func(t *testing.T) {
- err := cat.CreateView(context.Background(),
test.identifier, nil, "", nil)
+ err := cat.CreateView(context.Background(),
test.identifier, nil, view.Representation{SQL: "", Dialect: "spark"}, nil)
assert.ErrorIs(t, err, catalog.ErrNoSuchView)
_, err = cat.CheckViewExists(context.Background(),
test.identifier)
@@ -2932,7 +2933,7 @@ func (s *SqliteCatalogTestSuite)
TestCreateTableConflictsWithView() {
db := s.getCatalogSqlite()
identifier := s.randomTableIdentifier()
s.Require().NoError(db.CreateNamespace(ctx,
catalog.NamespaceFromIdent(identifier), nil))
- s.Require().NoError(db.CreateView(ctx, identifier, tableSchemaNested,
"SELECT 1", nil))
+ s.Require().NoError(db.CreateView(ctx, identifier, tableSchemaNested,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil))
metadataDir := filepath.Join(s.warehouse, identifier[0]+".db",
identifier[1], "metadata")
before, err := os.ReadDir(metadataDir)
@@ -2958,7 +2959,7 @@ func (s *SqliteCatalogTestSuite)
TestCreateViewConflictsWithTable() {
before, err := os.ReadDir(metadataDir)
s.Require().NoError(err)
- err = db.CreateView(ctx, identifier, tableSchemaNested, "SELECT 1", nil)
+ err = db.CreateView(ctx, identifier, tableSchemaNested,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil)
s.ErrorIs(err, catalog.ErrTableAlreadyExists)
after, err := os.ReadDir(metadataDir)
@@ -2990,7 +2991,7 @@ func (s *SqliteCatalogTestSuite)
TestConcurrentTableViewCollisionReturnsCatalogS
}()
go func() {
<-start
- err := db.CreateView(ctx, identifier, tableSchemaNested,
"SELECT 1", nil)
+ err := db.CreateView(ctx, identifier, tableSchemaNested,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil)
results <- createResult{objectType: sqlcat.ViewType, err: err}
}()
close(start)
@@ -3061,7 +3062,7 @@ func (s *SqliteCatalogTestSuite)
TestConcurrentTableViewCollisionUnderLockConten
}()
go func() {
<-start
- err := db.CreateView(ctx, identifier, tableSchemaNested,
"SELECT 1", nil)
+ err := db.CreateView(ctx, identifier, tableSchemaNested,
view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil)
results <- createResult{objectType: sqlcat.ViewType, err: err}
}()
close(start)
@@ -3112,7 +3113,7 @@ func (s *SqliteCatalogTestSuite)
TestCreateRemovesMetadataIfCatalogInsertFails()
{
name: "view",
create: func(identifier table.Identifier) error {
- return db.CreateView(ctx, identifier,
tableSchemaNested, "SELECT 1", nil)
+ return db.CreateView(ctx, identifier,
tableSchemaNested, view.Representation{SQL: "SELECT 1", Dialect: "spark"}, nil)
},
},
}
@@ -3143,7 +3144,7 @@ func (s *SqliteCatalogTestSuite) TestDropView() {
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))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, nil))
exists, err := db.CheckViewExists(context.Background(),
[]string{nsName, viewName})
s.Require().NoError(err)
@@ -3202,7 +3203,7 @@ func (s *SqliteCatalogTestSuite)
TestDropViewWithInvalidMetadataLocation() {
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))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, nil))
// Manually update the metadata location to a URL with an unsupported
scheme
// This will cause io.LoadFS to fail with "IO for file '...' not
implemented"
@@ -3238,7 +3239,7 @@ func (s *SqliteCatalogTestSuite) TestCheckViewExists() {
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))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, nil))
exists, err = db.CheckViewExists(context.Background(), []string{nsName,
viewName})
s.Require().NoError(err)
@@ -3258,7 +3259,7 @@ func (s *SqliteCatalogTestSuite) TestListViews() {
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))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, nil))
}
var foundViews []table.Identifier
@@ -3312,7 +3313,7 @@ func (s *SqliteCatalogTestSuite) TestLoadView() {
"comment": "Test view",
"owner": "test-user",
}
- s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, viewSQL, props))
+ s.Require().NoError(db.CreateView(context.Background(),
[]string{nsName, viewName}, schema, view.Representation{SQL: viewSQL, Dialect:
"spark"}, props))
viewInfo, err := db.LoadView(context.Background(), []string{nsName,
viewName})
s.Require().NoError(err)
diff --git a/view/metadata.go b/view/metadata.go
index 73c0ecbc0..bf84f14b4 100644
--- a/view/metadata.go
+++ b/view/metadata.go
@@ -109,12 +109,12 @@ type Representation struct {
}
type Representations []Representation
-func NewRepresentation(sql string, dialect string) Representation {
- return Representation{
- Type: "sql",
- SQL: sql,
- Dialect: dialect,
- }
+// NewRepresentation creates a SQL view representation from named fields.
+// The SQL and Dialect fields are validated by NewVersion.
+func NewRepresentation(repr Representation) Representation {
+ repr.Type = "sql"
+
+ return repr
}
type Version struct {
@@ -222,10 +222,10 @@ func validateRepresentation(repr Representation) (string,
error) {
return strings.ToLower(dialect), nil
}
-// NewVersionFromSQL creates a new Version with a single representation
-// using the provided SQL and dialect "default".
-func NewVersionFromSQL(id int64, schemaID int, sql string, defaultNS
table.Identifier, opts ...VersionOpt) (*Version, error) {
- return NewVersion(id, schemaID, Representations{NewRepresentation(sql,
"default")}, defaultNS, opts...)
+// NewVersionFromSQL creates a new Version with one SQL representation.
+// SQL and dialect are passed as named fields to avoid swapping them.
+func NewVersionFromSQL(id int64, schemaID int, representation Representation,
defaultNS table.Identifier, opts ...VersionOpt) (*Version, error) {
+ return NewVersion(id, schemaID,
Representations{NewRepresentation(representation)}, defaultNS, opts...)
}
// Equals checks whether the other Version would behave the same
diff --git a/view/metadata_builder_test.go b/view/metadata_builder_test.go
index 58ce7e853..7e6191fa0 100644
--- a/view/metadata_builder_test.go
+++ b/view/metadata_builder_test.go
@@ -40,7 +40,7 @@ func newTestVersion(versionID int64, schemaID int, opts
...VersionOpt) *Version
version, _ := NewVersion(
versionID,
schemaID,
- Representations{NewRepresentation("select * from table",
"sql")},
+ Representations{NewRepresentation(Representation{SQL: "select *
from table", Dialect: "sql"})},
table.Identifier{"defaultns"},
opts...)
@@ -51,7 +51,7 @@ func newTestVersionWithSQL(versionID int64, schemaID int, sql
string, opts ...Ve
version, _ := NewVersion(
versionID,
schemaID,
- Representations{NewRepresentation(sql, "spark")},
+ Representations{NewRepresentation(Representation{SQL: sql,
Dialect: "spark"})},
table.Identifier{"defaultns"},
opts...)
@@ -246,8 +246,8 @@ func TestNewVersion_RepresentationValidation(t *testing.T) {
func TestNewVersionCopiesInputSlices(t *testing.T) {
representations := Representations{
- NewRepresentation("SELECT 1", "spark"),
- NewRepresentation("SELECT 2", "trino"),
+ NewRepresentation(Representation{SQL: "SELECT 1", Dialect:
"spark"}),
+ NewRepresentation(Representation{SQL: "SELECT 2", Dialect:
"trino"}),
}
defaultNS := table.Identifier{"analytics", "reporting"}
@@ -262,8 +262,8 @@ func TestNewVersionCopiesInputSlices(t *testing.T) {
defaultNS[1] = "schema"
assert.Equal(t, []Representation{
- NewRepresentation("SELECT 1", "spark"),
- NewRepresentation("SELECT 2", "trino"),
+ NewRepresentation(Representation{SQL: "SELECT 1", Dialect:
"spark"}),
+ NewRepresentation(Representation{SQL: "SELECT 2", Dialect:
"trino"}),
}, version.Representations)
assert.Equal(t, table.Identifier{"analytics", "reporting"},
version.DefaultNamespace)
@@ -276,11 +276,18 @@ func TestNewVersionCopiesInputSlices(t *testing.T) {
func TestNewVersionFromSQLCopiesNamespace(t *testing.T) {
defaultNS := table.Identifier{"analytics"}
- version, err := NewVersionFromSQL(1, 0, "SELECT 1", defaultNS)
+ version, err := NewVersionFromSQL(1, 0, Representation{SQL: "SELECT 1",
Dialect: "spark"}, defaultNS)
require.NoError(t, err)
defaultNS[0] = "other"
assert.Equal(t, table.Identifier{"analytics"}, version.DefaultNamespace)
+ assert.Equal(t, "spark", version.Representations[0].Dialect)
+}
+
+func TestNewVersionFromSQLRequiresDialect(t *testing.T) {
+ _, err := NewVersionFromSQL(1, 0, Representation{SQL: "SELECT 1",
Dialect: " "}, table.Identifier{"analytics"})
+ require.ErrorIs(t, err, ErrInvalidViewMetadata)
+ require.ErrorContains(t, err, "dialect is required")
}
func TestAddVersion_InvalidRepresentation_Validation(t *testing.T) {
@@ -741,8 +748,8 @@ func TestDroppingDialectFailsByDefault(t *testing.T) {
schema := newTestSchema(0, "x")
version, _ := NewVersion(1, 0,
Representations{
- NewRepresentation("select \"nested\".\"field\" from
tbl", "trino"),
- NewRepresentation("select `nested`.`field` from tbl",
"spark"),
+ NewRepresentation(Representation{SQL: "select
\"nested\".\"field\" from tbl", Dialect: "trino"}),
+ NewRepresentation(Representation{SQL: "select
`nested`.`field` from tbl", Dialect: "spark"}),
},
table.Identifier{"defaultns"},
)
@@ -766,8 +773,8 @@ func TestDroppingDialectDoesNotFailWhenAllowed(t
*testing.T) {
schema := newTestSchema(0, "x")
version, _ := NewVersion(1, 0,
Representations{
- NewRepresentation("select \"nested\".\"field\" from
tbl", "trino"),
- NewRepresentation("select `nested`.`field` from tbl",
"spark"),
+ NewRepresentation(Representation{SQL: "select
\"nested\".\"field\" from tbl", Dialect: "trino"}),
+ NewRepresentation(Representation{SQL: "select
`nested`.`field` from tbl", Dialect: "spark"}),
},
table.Identifier{"defaultns"},
)
@@ -809,7 +816,7 @@ func TestUnknownRepresentationSurvivesBuilderRoundTrip(t
*testing.T) {
nextVersion, err := NewVersion(
2,
metadata.CurrentSchemaID(),
- Representations{NewRepresentation("SELECT 2", "spark")},
+ Representations{NewRepresentation(Representation{SQL: "SELECT
2", Dialect: "spark"})},
metadata.CurrentVersion().DefaultNamespace,
)
require.NoError(t, err)
diff --git a/view/metadata_test.go b/view/metadata_test.go
index 9d63a86c4..957cb7d28 100644
--- a/view/metadata_test.go
+++ b/view/metadata_test.go
@@ -69,9 +69,16 @@ func TestVersionEquals(t *testing.T) {
assert.True(t, v1.Equals(v2), fmt.Sprintf("Expected the same SchemaID,
Summary, Representation for %v, got %v", v1, v2))
}
+func TestNewRepresentationSetsTypeWithoutMutatingInput(t *testing.T) {
+ input := Representation{SQL: "SELECT 1", Dialect: "spark"}
+ repr := NewRepresentation(input)
+ require.Equal(t, Representation{Type: "sql", SQL: "SELECT 1", Dialect:
"spark"}, repr)
+ require.Empty(t, input.Type)
+}
+
// TestRepresentationSQLJSON keeps the on-disk SQL key unchanged after the Go
API rename.
func TestRepresentationSQLJSON(t *testing.T) {
- repr := NewRepresentation("SELECT 1", "spark")
+ repr := NewRepresentation(Representation{SQL: "SELECT 1", Dialect:
"spark"})
require.Equal(t, "SELECT 1", repr.SQL)
data, err := json.Marshal(repr)
@@ -155,7 +162,7 @@ func TestUnmarshalViewMetadata(t *testing.T) {
assert.Equal(t, 0, md.CurrentVersion().SchemaID)
assert.Equal(t, VersionSummary{"summaryProp": "summaryVal"},
md.CurrentVersion().Summary)
assert.Equal(t, []VersionLogEntry{{TimestampMS: 1000, VersionID: 1}},
md.VersionLog())
- assert.Equal(t, []Representation{NewRepresentation("select * from
ns.tbl", "trino")}, md.CurrentVersion().Representations)
+ assert.Equal(t, []Representation{NewRepresentation(Representation{SQL:
"select * from ns.tbl", Dialect: "trino"})},
md.CurrentVersion().Representations)
assert.Equal(t, iceberg.Properties{"prop": "value"}, md.Properties())
}
diff --git a/view/view.go b/view/view.go
index 4db9dde22..fcf314779 100644
--- a/view/view.go
+++ b/view/view.go
@@ -137,33 +137,35 @@ func NewFromLocation(
//
// Note: This function only supports creating new views with format version 1.
// It does not support updating existing view metadata.
+// representation holds both SQL text and its dialect.
func CreateView(
ctx context.Context,
catalogName string,
viewIdent table.Identifier,
schema *iceberg.Schema,
- viewSQL string,
+ representation Representation,
defaultNS table.Identifier,
loc string,
props iceberg.Properties,
) (*View, error) {
- return createView(ctx, catalogName, viewIdent, schema, viewSQL,
defaultNS, loc, props, props)
+ return createView(ctx, catalogName, viewIdent, schema, representation,
defaultNS, loc, props, props)
}
// CreateViewWithIOProperties creates a view using ioProps to configure the
// filesystem and metadataProps as the properties persisted in view metadata.
+// representation holds both SQL text and its dialect.
func CreateViewWithIOProperties(
ctx context.Context,
catalogName string,
viewIdent table.Identifier,
schema *iceberg.Schema,
- viewSQL string,
+ representation Representation,
defaultNS table.Identifier,
loc string,
ioProps iceberg.Properties,
metadataProps iceberg.Properties,
) (*View, error) {
- return createView(ctx, catalogName, viewIdent, schema, viewSQL,
defaultNS, loc, ioProps, metadataProps)
+ return createView(ctx, catalogName, viewIdent, schema, representation,
defaultNS, loc, ioProps, metadataProps)
}
func createView(
@@ -171,7 +173,7 @@ func createView(
catalogName string,
viewIdent table.Identifier,
schema *iceberg.Schema,
- viewSQL string,
+ representation Representation,
defaultNS table.Identifier,
loc string,
ioProps iceberg.Properties,
@@ -184,7 +186,7 @@ func createView(
viewVersion, err := NewVersionFromSQL(
versionId,
schema.ID,
- viewSQL,
+ representation,
defaultNS,
WithDefaultViewCatalog(catalogName),
)
diff --git a/view/view_test.go b/view/view_test.go
index e1329f27b..fbfad8fd7 100644
--- a/view/view_test.go
+++ b/view/view_test.go
@@ -94,7 +94,7 @@ func (t *ViewTestSuite)
TestCreateViewJoinsTrailingSlashMetadataLocation() {
"test-catalog",
[]string{"ns", "test_view"},
newTestSchema(0),
- "select 1",
+ Representation{SQL: "select 1", Dialect: "spark"},
[]string{"ns"},
"mem://view-create-location/test-view/",
nil,
@@ -137,7 +137,7 @@ func TestCreateViewReturnsMetadataCloseError(t *testing.T) {
"test-catalog",
[]string{"ns", "test_view"},
newTestSchema(0),
- "select 1",
+ Representation{SQL: "select 1", Dialect: "spark"},
[]string{"ns"},
scheme+"://bucket/test-view",
nil,
diff --git a/website/src/api.md b/website/src/api.md
index b52509255..03edf9bef 100644
--- a/website/src/api.md
+++ b/website/src/api.md
@@ -557,7 +557,7 @@ v, err := view.CreateView(
"my-catalog",
table.Identifier{"analytics", "monthly_orders"},
schema,
- "SELECT month, sum(amount) FROM orders GROUP BY month",
+ view.Representation{SQL: "SELECT month, sum(amount) FROM orders GROUP BY
month", Dialect: "spark"},
table.Identifier{"sales"}, // default namespace for
unqualified names
"s3://my-bucket/views/monthly_orders",
iceberg.Properties{},
diff --git a/website/src/releases.md b/website/src/releases.md
index 2af23acce..b24209296 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -42,6 +42,10 @@ Per-release notes (highlights, breaking changes,
contributors) are published on
### Unreleased compatibility notes
+- **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.
+
- **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.