laskoviymishka commented on code in PR #2165:
URL: https://github.com/apache/iceberg-go/pull/2165#discussion_r4232826127
##########
catalog/hive/hive.go:
##########
@@ -539,6 +539,14 @@ func (c *Catalog) RenameTable(ctx context.Context, from,
to table.Identifier) (_
table.ErrCommitFailed, fromDB, fromTable,
sourceMetadataLocation, lockedMetadataLocation)
}
+ destExists, err := c.CheckTableExists(ctx, to)
Review Comment:
CheckTableExists here returns isIcebergTable, so a non-Iceberg Hive table
sitting at `to` reads as "not there" and we fall through to AlterTable, which
hands back the raw metastore AlreadyExistsException as a generic rename
failure. That's the same ambiguity this PR is removing, just for a non-Iceberg
destination. I'd check the destination with a raw GetTable and treat any hit as
occupied (isNoSuchObjectError means free), so the ErrTableAlreadyExists mapping
covers both cases.
Worth flagging this is still a pre-check, not a guard: a create or Hive DDL
outside the lock, landing between the check and AlterTable, will still surface
the raw error. Java leans on mapping the AlterTable InvalidOperationException
instead of pre-checking, and doing that as a second layer would close the race
too. I'd treat that part as a follow-up, not a blocker here.
##########
catalog/rest/rest.go:
##########
@@ -1980,7 +1980,7 @@ func (r *Catalog) RenameTable(ctx context.Context, from,
to table.Identifier) (*
}
_, err = doPost[payload, any](ctx, r.baseURI, path, payload{Source:
src, Destination: dst}, r.cl,
- map[int]error{http.StatusNotFound: catalog.ErrNoSuchTable},
allowNoContent())
+ map[int]error{http.StatusNotFound: catalog.ErrNoSuchTable,
http.StatusConflict: catalog.ErrTableAlreadyExists}, allowNoContent())
Review Comment:
The 409 to ErrTableAlreadyExists mapping is right and matches both the REST
spec and PyIceberg. Pre-existing and fine to leave: a 404 here can be a missing
source table or a missing destination namespace, and both map to
ErrNoSuchTable, so the suite can't distinguish them. If we want that precision
later, branch on the error type field.
##########
catalog/catalogtest/catalogtest.go:
##########
@@ -89,6 +89,11 @@ type Config struct {
// are directories with nowhere to keep them. Loading is required of
every
// catalog and is tested either way.
SupportsNamespaceProperties bool
+
+ // SupportsRenameTable reports whether the catalog can rename tables.
+ // Rename tests are skipped when it is false, as they are for the Hadoop
+ // catalog, which does not implement rename.
+ SupportsRenameTable bool
Review Comment:
This mirrors SupportsNamespaceProperties, so the shape is fine. The gap is
that when the flag is false the whole rename suite is skipped, so a catalog
that half-implements rename, or regresses, stays green instead of getting
caught. I'd have the false branch assert something positive instead:
RenameTable returns ErrNotImplemented, or at least an error with the source
left intact, so "unsupported" is a verified behavior rather than a silent
no-op. A pointer to #1097 in this doc comment would also help, since that's
when Hadoop flips to true.
##########
catalog/catalogtest/tables.go:
##########
@@ -0,0 +1,181 @@
+// 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 catalogtest
+
+import (
+ "context"
+ "testing"
+
+ "github.com/apache/iceberg-go/catalog"
+ "github.com/apache/iceberg-go/table"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// listTables collects every table identifier the catalog reports for
namespace.
+func listTables(t *testing.T, cat catalog.Catalog, namespace table.Identifier)
[]table.Identifier {
+ t.Helper()
+
+ var out []table.Identifier
+ for ident, err := range cat.ListTables(context.Background(), namespace)
{
+ require.NoError(t, err)
+ out = append(out, ident)
+ }
+
+ return out
+}
+
+// testListTables asserts that ListTables reflects creates and drops.
+func testListTables(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ assert.Empty(t, listTables(t, cat, namespace), "new namespace should
have no tables")
+
+ _, err := cat.CreateTable(ctx, ident, Schema)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, ident) })
+
+ assert.Equal(t, []table.Identifier{ident}, listTables(t, cat,
namespace))
+
+ require.NoError(t, cat.DropTable(ctx, ident))
+ assert.Empty(t, listTables(t, cat, namespace), "dropped table should
not be listed")
+}
+
+// testDropTable asserts that a dropped table is no longer visible.
+func testDropTable(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ _, err := cat.CreateTable(ctx, ident, Schema)
+ require.NoError(t, err)
+
+ require.NoError(t, cat.DropTable(ctx, ident))
+
+ exists, err := cat.CheckTableExists(ctx, ident)
+ require.NoError(t, err)
+ assert.False(t, exists, "table should not exist after drop")
+
+ _, err = cat.LoadTable(ctx, ident)
+ assert.ErrorIs(t, err, catalog.ErrNoSuchTable)
+}
+
+// testDropMissingTable asserts that dropping a table that was never created
+// reports that the table does not exist.
+func testDropMissingTable(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ assert.ErrorIs(t, cat.DropTable(ctx, ident), catalog.ErrNoSuchTable)
+}
+
+func skipWithoutRenameTable(t *testing.T, cfg Config) {
+ t.Helper()
+ if !cfg.SupportsRenameTable {
+ t.Skip("catalog does not support renaming tables")
+ }
+}
+
+// testRenameTable asserts that a renamed table is reachable only under its new
+// identifier and keeps its schema.
+func testRenameTable(t *testing.T, cfg Config) {
+ skipWithoutRenameTable(t, cfg)
+
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, from := newIdentifiers()
+ to := table.Identifier{namespace[0], "renamed"}
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ _, err := cat.CreateTable(ctx, from, Schema)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, from) })
+
+ _, err = cat.RenameTable(ctx, from, to)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, to) })
+
+ exists, err := cat.CheckTableExists(ctx, from)
+ require.NoError(t, err)
+ assert.False(t, exists, "source table should not exist after rename")
+
+ tbl, err := cat.LoadTable(ctx, to)
+ require.NoError(t, err)
+ assert.Equal(t, to, tbl.Identifier())
+ assert.Equal(t, TableSchema.AsStruct(), tbl.Schema().AsStruct(),
"rename should keep the schema")
Review Comment:
The schema check is good, but rename should preserve table identity too, and
that's the cheaper and more meaningful invariant to pin here. CreateTable's
return value is discarded up on line 119; capture it and assert
Metadata().TableUUID() and MetadataLocation() are unchanged after the rename.
That catches a backend that "renames" by recreating, which schema equality
alone wouldn't.
##########
catalog/catalogtest/tables.go:
##########
@@ -0,0 +1,181 @@
+// 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 catalogtest
+
+import (
+ "context"
+ "testing"
+
+ "github.com/apache/iceberg-go/catalog"
+ "github.com/apache/iceberg-go/table"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// listTables collects every table identifier the catalog reports for
namespace.
+func listTables(t *testing.T, cat catalog.Catalog, namespace table.Identifier)
[]table.Identifier {
+ t.Helper()
+
+ var out []table.Identifier
+ for ident, err := range cat.ListTables(context.Background(), namespace)
{
+ require.NoError(t, err)
+ out = append(out, ident)
+ }
+
+ return out
+}
+
+// testListTables asserts that ListTables reflects creates and drops.
+func testListTables(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ assert.Empty(t, listTables(t, cat, namespace), "new namespace should
have no tables")
+
+ _, err := cat.CreateTable(ctx, ident, Schema)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, ident) })
+
+ assert.Equal(t, []table.Identifier{ident}, listTables(t, cat,
namespace))
+
+ require.NoError(t, cat.DropTable(ctx, ident))
+ assert.Empty(t, listTables(t, cat, namespace), "dropped table should
not be listed")
+}
+
+// testDropTable asserts that a dropped table is no longer visible.
+func testDropTable(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ _, err := cat.CreateTable(ctx, ident, Schema)
+ require.NoError(t, err)
+
+ require.NoError(t, cat.DropTable(ctx, ident))
+
+ exists, err := cat.CheckTableExists(ctx, ident)
+ require.NoError(t, err)
+ assert.False(t, exists, "table should not exist after drop")
+
+ _, err = cat.LoadTable(ctx, ident)
+ assert.ErrorIs(t, err, catalog.ErrNoSuchTable)
+}
+
+// testDropMissingTable asserts that dropping a table that was never created
+// reports that the table does not exist.
+func testDropMissingTable(t *testing.T, cfg Config) {
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, ident := newIdentifiers()
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ assert.ErrorIs(t, cat.DropTable(ctx, ident), catalog.ErrNoSuchTable)
+}
+
+func skipWithoutRenameTable(t *testing.T, cfg Config) {
+ t.Helper()
+ if !cfg.SupportsRenameTable {
+ t.Skip("catalog does not support renaming tables")
+ }
+}
+
+// testRenameTable asserts that a renamed table is reachable only under its new
+// identifier and keeps its schema.
+func testRenameTable(t *testing.T, cfg Config) {
+ skipWithoutRenameTable(t, cfg)
+
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, from := newIdentifiers()
+ to := table.Identifier{namespace[0], "renamed"}
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ _, err := cat.CreateTable(ctx, from, Schema)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, from) })
+
+ _, err = cat.RenameTable(ctx, from, to)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, to) })
+
+ exists, err := cat.CheckTableExists(ctx, from)
+ require.NoError(t, err)
+ assert.False(t, exists, "source table should not exist after rename")
+
+ tbl, err := cat.LoadTable(ctx, to)
+ require.NoError(t, err)
+ assert.Equal(t, to, tbl.Identifier())
+ assert.Equal(t, TableSchema.AsStruct(), tbl.Schema().AsStruct(),
"rename should keep the schema")
+}
+
+// testRenameTableToExisting asserts that renaming onto an existing table is
+// rejected and leaves both tables in place.
+func testRenameTableToExisting(t *testing.T, cfg Config) {
+ skipWithoutRenameTable(t, cfg)
+
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, from := newIdentifiers()
+ to := table.Identifier{namespace[0], "other"}
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ for _, ident := range []table.Identifier{from, to} {
+ _, err := cat.CreateTable(ctx, ident, Schema)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = cat.DropTable(ctx, ident) })
+ }
+
+ _, err := cat.RenameTable(ctx, from, to)
+ assert.ErrorIs(t, err, catalog.ErrTableAlreadyExists)
+
+ for _, ident := range []table.Identifier{from, to} {
+ exists, err := cat.CheckTableExists(ctx, ident)
+ require.NoError(t, err)
+ assert.True(t, exists, "%v should still exist after a rejected
rename", ident)
+ }
+}
+
+// testRenameMissingTable asserts that renaming a table that was never created
+// reports that the table does not exist.
+func testRenameMissingTable(t *testing.T, cfg Config) {
+ skipWithoutRenameTable(t, cfg)
+
+ ctx := context.Background()
+ cat := cfg.NewCatalog(t)
+ namespace, from := newIdentifiers()
+ to := table.Identifier{namespace[0], "renamed"}
+
+ require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+ t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+ _, err := cat.RenameTable(ctx, from, to)
Review Comment:
If you want to push the parity goal further, the paths most likely to hide
cross-catalog divergence are the ones not exercised yet: rename to a missing
destination namespace (Hive checks before locking and returns
ErrNoSuchNamespace, exactly where parity tends to drift) and cross-namespace
rename. Both are natural follow-ups behind the same flag.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]