s3tables: allow hyphens in namespace and table names (#10093)

* s3tables: allow hyphens in namespace and table names

Iceberg REST clients routinely use hyphenated namespace/table names, but the
S3 Tables charset (a-z, 0-9, _) rejected them with 400. Accept '-' as an
interior character (names must still start, and namespaces end, with a letter
or digit), making the catalog conformant for those clients. A permissive
superset of the AWS S3 Tables charset.

* s3tables: allow hyphens in table ARN parsing too

The ARN regexes still excluded '-', so parseTableFromARN rejected ARNs with
hyphenated namespace/table names and existing reject-the-hyphen tests broke.
Widen the ARN patterns to match the validator, retarget those tests at a
still-invalid leading-hyphen name, and cover ARN parsing with hyphens.
This commit is contained in:
Chris Lu
2026-06-24 16:24:45 -07:00
committed by GitHub
parent 1c5f8244a4
commit c15989387b
3 changed files with 48 additions and 11 deletions
@@ -21,7 +21,7 @@ func TestBuildListTablesRequestRejectsInvalidNamespaceQuery(t *testing.T) {
namespace string
}{
{name: "uppercase", namespace: "InvalidNamespace"},
{name: "hyphen", namespace: "invalid-ns"},
{name: "leading hyphen", namespace: "-invalid"},
{name: "slash", namespace: "a/b"},
}
@@ -47,7 +47,7 @@ func TestBuildGetTableRequestRejectsInvalidNamespaceQuery(t *testing.T) {
namespace string
}{
{name: "uppercase", namespace: "InvalidNamespace"},
{name: "hyphen", namespace: "invalid-ns"},
{name: "leading hyphen", namespace: "-invalid"},
{name: "slash", namespace: "a/b"},
}
@@ -72,7 +72,7 @@ func TestHandleRestOperationReturnsBadRequestForInvalidNamespaceQuery(t *testing
namespace string
}{
{name: "uppercase", namespace: "InvalidNamespace"},
{name: "hyphen", namespace: "invalid-ns"},
{name: "leading hyphen", namespace: "-invalid"},
{name: "slash", namespace: "a/b"},
}
@@ -0,0 +1,37 @@
package s3tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNameValidationAllowsHyphens(t *testing.T) {
// Hyphen interior is accepted (Iceberg REST clients use names like these).
require.NoError(t, validateNamespacePart("rest-integration-test"))
if _, err := validateTableName("test-create-table"); err != nil {
t.Fatalf("hyphenated table name rejected: %v", err)
}
// A leading or trailing hyphen on a namespace is still rejected (must start
// and end with a letter or digit), as are characters outside the charset.
require.Error(t, validateNamespacePart("-leading"))
require.Error(t, validateNamespacePart("trailing-"))
require.Error(t, validateNamespacePart("Upper"))
// Table names must still start with a letter or digit and stay in charset.
if _, err := validateTableName("-leading"); err == nil {
t.Fatal("table name with leading hyphen should be rejected")
}
if _, err := validateTableName("Upper"); err == nil {
t.Fatal("uppercase table name should be rejected")
}
}
func TestParseTableFromARNAllowsHyphens(t *testing.T) {
bucket, ns, table, err := parseTableFromARN("arn:aws:s3tables:us-east-1:123456789012:bucket/my-bucket/table/rest-integration-test/test-create-table")
require.NoError(t, err)
require.Equal(t, "my-bucket", bucket)
require.Equal(t, "rest-integration-test", ns)
require.Equal(t, "test-create-table", table)
}
+8 -8
View File
@@ -15,8 +15,8 @@ import (
const (
bucketNamePatternStr = `[a-z0-9-]+`
tableNamespacePatternStr = `[a-z0-9_.]+`
tableNamePatternStr = `[a-z0-9_]+`
tableNamespacePatternStr = `[a-z0-9_.-]+`
tableNamePatternStr = `[a-z0-9_-]+`
)
const (
@@ -344,12 +344,12 @@ func validateNamespacePart(name string) error {
return fmt.Errorf("namespace name must end with a letter or digit")
}
// Allowed characters: a-z, 0-9, _
// Allowed characters: a-z, 0-9, _, - (hyphen interior; start/end checked above)
for _, ch := range name {
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' {
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' {
continue
}
return fmt.Errorf("invalid namespace name: only 'a-z', '0-9', and '_' are allowed")
return fmt.Errorf("invalid namespace name: only 'a-z', '0-9', '_', and '-' are allowed")
}
// Reserved prefix
@@ -411,12 +411,12 @@ func validateTableName(name string) (string, error) {
return "", fmt.Errorf("table name must start with a letter or digit")
}
// Allowed characters: a-z, 0-9, _
// Allowed characters: a-z, 0-9, _, - (start checked above)
for _, ch := range name {
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' {
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' {
continue
}
return "", fmt.Errorf("invalid table name: only 'a-z', '0-9', and '_' are allowed")
return "", fmt.Errorf("invalid table name: only 'a-z', '0-9', '_', and '-' are allowed")
}
return name, nil
}