Files
seaweedfs/weed/s3api/s3tables/handler_delete_decouple_test.go
Chris LuandGitHub 1c5f8244a4 s3tables: fix create-after-rename overwriting the renamed table (#10091)
* s3tables: purge decoupled table data without deleting the reused name path

A renamed or created-over-leftover table keeps its data at a location that
differs from its catalog name path. Drop now purges that data location and
clears the marker, instead of recursively deleting the name path, which may
still hold another table's data.

* iceberg: route a table created over a leftover to a unique location

When the default location is occupied by a leftover directory (data kept when
another table was renamed to this name), create the new table at a unique
location so it cannot overwrite that table's metadata. Common case is unchanged.

* iceberg: fail table create when the leftover-path check errors

A transient filer lookup error fell through as "not occupied", routing the
new table back to the default path and risking the very overwrite this check
guards against. Propagate the error and return 500 instead.

* s3tables: assert all catalog xattrs cleared on decoupled drop

Seed the full marker set so the test catches a regression that leaves the
policy, tags, version, or entry-type attribute on the reused name path.

* s3tables: refuse to drop a table whose data path is an ancestor

Corrupt metadata can resolve the data path to the bucket or namespace root,
which the bucket-scope check still admits; a recursive purge there would wipe
sibling tables. Reject an ancestor data path before deleting.
2026-06-24 14:37:04 -07:00

95 lines
4.1 KiB
Go

package s3tables
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func runDeleteTable(t *testing.T, m *Manager, fs *memFilerServer, namespace, name string) error {
t.Helper()
return m.Execute(context.Background(), NewManagerClient(fs.client), "DeleteTable", &DeleteTableRequest{
TableBucketARN: mustBucketARN(t),
Namespace: []string{namespace},
Name: name,
}, nil, "")
}
// A table whose data was decoupled from its name (created over a leftover from a
// rename): catalog marker at ns/newt, data at ns/newt-x, and ns/newt still holds
// another table's leftover data. Dropping it must purge only its own data and
// clear the marker, never the data under the reused name path.
func TestDeleteTableDecoupledKeepsReusedNamePath(t *testing.T) {
fs, m := startRenameManager(t)
newtMeta, _ := json.Marshal(tableMetadataInternal{
Name: "newt",
Namespace: "ns",
Format: "ICEBERG",
OwnerAccountID: DefaultAccountID,
MetadataLocation: "s3://" + renameTestBucket + "/ns/newt-x/metadata/v1.metadata.json",
})
markerKeys := []string{ExtendedKeyMetadata, ExtendedKeyMetadataVersion, ExtendedKeyPolicy, ExtendedKeyTags, ExtendedKeyEntryType}
fs.putEntry(GetNamespacePath(renameTestBucket, "ns"), "newt", map[string][]byte{
ExtendedKeyMetadata: newtMeta,
ExtendedKeyMetadataVersion: []byte("v1"),
ExtendedKeyPolicy: []byte(`{"Version":"2012-10-17"}`),
ExtendedKeyTags: []byte(`{"k":"v"}`),
ExtendedKeyEntryType: []byte(EntryTypeTable),
})
fs.putEntry(GetTablePath(renameTestBucket, "ns", "newt"), "leftover", nil) // another table's data under the name path
fs.putEntry(GetNamespacePath(renameTestBucket, "ns"), "newt-x", nil) // this table's own (decoupled) data
fs.putEntry(GetTablePath(renameTestBucket, "ns", "newt-x"), "metadata", nil)
require.NoError(t, runDeleteTable(t, m, fs, "ns", "newt"))
assert.Nil(t, fs.getEntry(GetNamespacePath(renameTestBucket, "ns"), "newt-x"),
"the table's own data location must be purged")
assert.NotNil(t, fs.getEntry(GetTablePath(renameTestBucket, "ns", "newt"), "leftover"),
"data under the reused name path must survive")
marker := fs.getEntry(GetNamespacePath(renameTestBucket, "ns"), "newt")
require.NotNil(t, marker)
for _, key := range markerKeys {
_, present := marker.Extended[key]
assert.Falsef(t, present, "catalog attribute %s must be cleared", key)
}
}
// A table whose MetadataLocation resolves to an ancestor of its own name path
// (here the namespace root, e.g. from corrupt metadata) must not be deleted: a
// recursive purge of that data path would wipe sibling tables. The delete is
// refused and the namespace's other tables survive.
func TestDeleteTableRefusesAncestorDataPath(t *testing.T) {
fs, m := startRenameManager(t)
badMeta, _ := json.Marshal(tableMetadataInternal{
Name: "badt",
Namespace: "ns",
Format: "ICEBERG",
OwnerAccountID: DefaultAccountID,
MetadataLocation: "s3://" + renameTestBucket + "/ns/metadata/v1.metadata.json",
})
fs.putEntry(GetNamespacePath(renameTestBucket, "ns"), "badt", map[string][]byte{ExtendedKeyMetadata: badMeta})
require.Error(t, runDeleteTable(t, m, fs, "ns", "badt"))
// The sibling table seeded by startRenameManager and its data must survive.
assert.NotNil(t, fs.getEntry(GetNamespacePath(renameTestBucket, "ns"), "t"),
"sibling table marker must survive a refused delete")
assert.NotNil(t, fs.getEntry(GetTablePath(renameTestBucket, "ns", "t"), "data"),
"sibling table data must survive a refused delete")
}
// A normal colocated table (data under its own name path) is removed wholesale.
func TestDeleteTableColocatedRemovesData(t *testing.T) {
fs, m := startRenameManager(t)
require.NoError(t, runDeleteTable(t, m, fs, "ns", "t"))
assert.Nil(t, fs.getEntry(GetNamespacePath(renameTestBucket, "ns"), "t"), "colocated table entry must be deleted")
assert.Nil(t, fs.getEntry(GetTablePath(renameTestBucket, "ns", "t"), "metadata"), "colocated table data must be deleted")
}