iceberg: support views (#10069)

* s3tables: tag table entries and exclude views from table listings

* s3tables: add view CRUD operations

* iceberg: support view create, load, exists, drop, and list

* iceberg: support view update

* iceberg: test view error classification and metadata round-trip

* iceberg: pre-check existence and write view metadata only after create

* iceberg: map view namespace-not-found to 404

* iceberg: test view create namespace-404 and duplicate no-clobber

* s3tables: tag view metadata and entry type atomically

CreateView wrote ExtendedKeyMetadata and ExtendedKeyEntryType in two
UpdateEntry calls, so a partial failure could leave a view directory
untagged. Add setExtendedAttributes to set both in one UpdateEntry.

* iceberg: roll back view registration when metadata write fails

The metadata file is written after the catalog registers the view. If
that write fails, drop the just-created view so it doesn't linger
pointing at a missing metadata.json. Reuse the DeleteView path via a
shared dropView helper.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-06-23 15:22:31 -07:00
1 parent 1ca628d3e9
commit 0403e47ef6
13 files changed
+1815 -3

No files matched your search

+34 -3
View File
@@ -164,13 +164,23 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
tablePath := GetTablePath(bucketName, namespaceName, tableName)
// Check if table already exists
// Check if a table or view already exists at this name. Names are unique
// across tables and views in a namespace.
var existingMetadata tableMetadataInternal
var existingIsView bool
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
entry, err := h.lookupEntry(r.Context(), client, tablePath)
if err != nil {
return err
}
if entryType(entry.Extended) == EntryTypeView {
existingIsView = true
return nil
}
data, ok := entry.Extended[ExtendedKeyMetadata]
if !ok {
return fmt.Errorf("%w: %s", ErrAttributeNotFound, ExtendedKeyMetadata)
}
if unmarshalErr := json.Unmarshal(data, &existingMetadata); unmarshalErr != nil {
return fmt.Errorf("failed to parse existing table metadata: %w", unmarshalErr)
}
@@ -178,6 +188,10 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
})
if err == nil {
if existingIsView {
h.writeError(w, http.StatusConflict, ErrCodeTableAlreadyExists, fmt.Sprintf("a view named %s already exists", tableName))
return fmt.Errorf("view name conflict: %s", tableName)
}
tableARN := h.generateTableARN(existingMetadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
h.writeJSON(w, http.StatusOK, &CreateTableResponse{
TableARN: tableARN,
@@ -230,6 +244,11 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
return err
}
// Tag the entry as a table so view listings can exclude it.
if err := h.setExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyEntryType, []byte(EntryTypeTable)); err != nil {
return err
}
// Set tags if provided
if len(req.Tags) > 0 {
tagsBytes, err := json.Marshal(req.Tags)
@@ -519,10 +538,17 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
var metadata tableMetadataInternal
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
entry, err := h.lookupEntry(r.Context(), client, tablePath)
if err != nil {
return err
}
if entryType(entry.Extended) == EntryTypeView {
return filer_pb.ErrNotFound
}
data, ok := entry.Extended[ExtendedKeyMetadata]
if !ok {
return fmt.Errorf("%w: %s", ErrAttributeNotFound, ExtendedKeyMetadata)
}
if err := json.Unmarshal(data, &metadata); err != nil {
return fmt.Errorf("failed to unmarshal table metadata: %w", err)
}
@@ -886,6 +912,11 @@ func (h *S3TablesHandler) listTablesWithClient(r *http.Request, client filer_pb.
continue
}
// Views share the table layout; exclude them from table listings.
if entryType(entry.Entry.Extended) == EntryTypeView {
continue
}
// Read table metadata from extended attribute
data, ok := entry.Entry.Extended[ExtendedKeyMetadata]
if !ok {