Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
574 lines
18 KiB
Go
574 lines
18 KiB
Go
package lance
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
// handleListTables lists the Lance tables under a namespace. The spec asks for
|
|
// full string identifiers, not bare names, so a recursive listing stays
|
|
// unambiguous.
|
|
func (s *Server) handleListTables(w http.ResponseWriter, r *http.Request) {
|
|
id, delimiter, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucket, ns := id.namespace()
|
|
if bucket == "" {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, "listing tables needs a namespace, not the root")
|
|
return
|
|
}
|
|
|
|
req := &s3tables.ListTablesRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
Namespace: ns,
|
|
ContinuationToken: r.URL.Query().Get("page_token"),
|
|
MaxTables: pageSize(r),
|
|
}
|
|
var resp s3tables.ListTablesResponse
|
|
if err := s.execute(r, "ListTables", req, &resp); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
|
|
includeDeclared := true
|
|
if raw := r.URL.Query().Get("include_declared"); raw != "" {
|
|
includeDeclared = boolQuery(r, "include_declared")
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, ListTablesResponse{
|
|
Tables: s.lanceTableIDs(r, bucket, resp.Tables, delimiter, includeDeclared),
|
|
PageToken: resp.ContinuationToken,
|
|
})
|
|
}
|
|
|
|
// handleListAllTables lists every Lance table the caller can see, across every
|
|
// table bucket.
|
|
func (s *Server) handleListAllTables(w http.ResponseWriter, r *http.Request) {
|
|
delimiter := requestDelimiter(r)
|
|
|
|
var buckets s3tables.ListTableBucketsResponse
|
|
if err := s.execute(r, "ListTableBuckets", &s3tables.ListTableBucketsRequest{MaxBuckets: pageSize(r)}, &buckets); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
|
|
// The spec marks `tables` required, so an empty catalog answers with an
|
|
// empty list rather than null.
|
|
all := []string{}
|
|
for _, bucket := range buckets.TableBuckets {
|
|
var tables s3tables.ListTablesResponse
|
|
req := &s3tables.ListTablesRequest{TableBucketARN: bucketARN(bucket.Name), MaxTables: pageSize(r)}
|
|
if err := s.execute(r, "ListTables", req, &tables); err != nil {
|
|
// One unreadable bucket must not hide the rest; a caller with access
|
|
// to some buckets still gets those.
|
|
glog.V(2).Infof("lance: skipping bucket %s in ListAllTables: %v", bucket.Name, err)
|
|
continue
|
|
}
|
|
all = append(all, s.lanceTableIDs(r, bucket.Name, tables.Tables, delimiter, true)...)
|
|
}
|
|
writeJSON(w, http.StatusOK, ListTablesResponse{Tables: all})
|
|
}
|
|
|
|
// lanceTableIDs keeps the Lance tables out of a listing that also carries
|
|
// Iceberg tables, and drops deregistered ones because they are meant to be
|
|
// invisible until re-registered.
|
|
func (s *Server) lanceTableIDs(r *http.Request, bucket string, summaries []s3tables.TableSummary, delimiter string, includeDeclared bool) []string {
|
|
ids := make([]string, 0, len(summaries))
|
|
for _, summary := range summaries {
|
|
if summary.Format != s3tables.FormatLance {
|
|
continue
|
|
}
|
|
location := summary.MetadataLocation
|
|
if location == "" {
|
|
location = tableLocation(bucket, summary.Namespace, summary.Name)
|
|
}
|
|
deregistered, hasData, err := s.datasetState(r, location)
|
|
if err != nil {
|
|
glog.V(2).Infof("lance: cannot read dataset state for %s: %v", location, err)
|
|
continue
|
|
}
|
|
if deregistered {
|
|
continue
|
|
}
|
|
if !hasData && !includeDeclared {
|
|
continue
|
|
}
|
|
parts := append([]string{bucket}, summary.Namespace...)
|
|
ids = append(ids, identifier(append(parts, summary.Name)).String(delimiter))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// handleDeclareTable records a table that does not exist on storage yet. This
|
|
// is what a Lance client calls on CREATE TABLE, before it writes any data.
|
|
func (s *Server) handleDeclareTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req DeclareTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
location := strings.TrimSuffix(req.Location, "/")
|
|
if location == "" {
|
|
location = tableLocation(bucket, ns, name)
|
|
} else if err := confineLocation(bucket, location); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := s.createTable(r, bucket, ns, name, location); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
if err := s.writeMarker(r, location, reservedMarker); err != nil {
|
|
// The catalog entry is the authority; the marker only mirrors it for
|
|
// clients that read the storage prefix directly.
|
|
glog.V(1).Infof("lance: could not write %s for %s: %v", reservedMarker, location, err)
|
|
}
|
|
// Declaring a name that was deregistered brings it back, the same way
|
|
// registering it does.
|
|
if err := s.removeMarker(r, location, deregisteredMarker); err != nil {
|
|
glog.V(2).Infof("lance: could not clear %s for %s: %v", deregisteredMarker, location, err)
|
|
}
|
|
|
|
options, err := s.storageOptions(r, bucket, location, wants(r, "vend_credentials", req.VendCredentials))
|
|
if err != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal, err.Error())
|
|
return
|
|
}
|
|
// Properties are not persisted for a table, so neither response carries
|
|
// them: null says "this catalog does not keep them", where echoing the
|
|
// request back or answering {} would say they were stored and are empty.
|
|
writeJSON(w, http.StatusOK, DeclareTableResponse{
|
|
Location: location,
|
|
StorageOptions: options,
|
|
})
|
|
}
|
|
|
|
// handleDescribeTable resolves a table to a location, and to credentials when
|
|
// the caller asks for them.
|
|
func (s *Server) handleDescribeTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req DescribeTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if req.Version != nil || req.Tag != "" || req.Branch != "" {
|
|
writeError(w, r, http.StatusNotImplemented, codeUnsupported,
|
|
"this namespace does not resolve versions, tags or branches; the dataset owns them")
|
|
return
|
|
}
|
|
|
|
table, err := s.loadLanceTable(r, bucket, ns, name)
|
|
if err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
location := table.location
|
|
|
|
deregistered, hasData, err := s.datasetState(r, location)
|
|
if err != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal, err.Error())
|
|
return
|
|
}
|
|
if deregistered {
|
|
writeError(w, r, http.StatusNotFound, codeTableNotFound, "table is deregistered")
|
|
return
|
|
}
|
|
|
|
options, err := s.storageOptions(r, bucket, location, wants(r, "vend_credentials", req.VendCredentials))
|
|
if err != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal, err.Error())
|
|
return
|
|
}
|
|
|
|
resp := DescribeTableResponse{
|
|
Location: location,
|
|
}
|
|
if len(options) > 0 {
|
|
resp.StorageOptions = options
|
|
}
|
|
if wants(r, "with_table_uri", req.WithTableURI) {
|
|
resp.TableURI = location
|
|
}
|
|
if wants(r, "check_declared", req.CheckDeclared) {
|
|
onlyDeclared := !hasData
|
|
resp.IsOnlyDeclared = &onlyDeclared
|
|
}
|
|
if wants(r, "load_detailed_metadata", req.LoadDetailedMetadata) {
|
|
resp.Table = name
|
|
resp.Namespace = ns
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// handleTableExists answers with the status code and no body.
|
|
func (s *Server) handleTableExists(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req TableExistsRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
table, err := s.loadLanceTable(r, bucket, ns, name)
|
|
if err != nil {
|
|
if isNotFound(err) {
|
|
writeError(w, r, http.StatusNotFound, codeTableNotFound, "table does not exist")
|
|
return
|
|
}
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
deregistered, _, err := s.datasetState(r, table.location)
|
|
if err != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal, err.Error())
|
|
return
|
|
}
|
|
if deregistered {
|
|
writeError(w, r, http.StatusNotFound, codeTableNotFound, "table is deregistered")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// handleRegisterTable points a table name at an existing dataset.
|
|
func (s *Server) handleRegisterTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req RegisterTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
location := strings.TrimSuffix(req.Location, "/")
|
|
if location == "" {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, "location is required")
|
|
return
|
|
}
|
|
if err := confineLocation(bucket, location); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
mode := normalizeMode(req.Mode, modeCreate)
|
|
if mode != modeCreate && mode != modeOverwrite {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, "mode must be Create or Overwrite")
|
|
return
|
|
}
|
|
|
|
existing, err := s.loadLanceTable(r, bucket, ns, name)
|
|
switch {
|
|
case err != nil && !isNotFound(err):
|
|
writeStorageError(w, r, err)
|
|
return
|
|
case err == nil:
|
|
// A deregistered table is absent as far as the spec is concerned, so
|
|
// registering over it is a re-registration rather than a conflict.
|
|
deregistered, _, stateErr := s.datasetState(r, existing.location)
|
|
if stateErr != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal, stateErr.Error())
|
|
return
|
|
}
|
|
if !deregistered && mode == modeCreate {
|
|
writeError(w, r, http.StatusConflict, codeTableAlreadyExists, "table already exists")
|
|
return
|
|
}
|
|
if existing.location != location {
|
|
// Repointing the name at another dataset is an update. Dropping and
|
|
// recreating the entry would take the old dataset's files with it,
|
|
// because the entry is the directory holding them.
|
|
if err := s.repointTable(r, bucket, ns, name, location, existing.versionToken); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
default:
|
|
if err := s.createTable(r, bucket, ns, name, location); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Registering a deregistered dataset brings it back.
|
|
if err := s.removeMarker(r, location, deregisteredMarker); err != nil {
|
|
glog.V(2).Infof("lance: could not clear %s for %s: %v", deregisteredMarker, location, err)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, RegisterTableResponse{
|
|
Location: location,
|
|
Properties: normalizeProperties(req.Properties),
|
|
})
|
|
}
|
|
|
|
// handleDeregisterTable forgets a table without touching its data.
|
|
func (s *Server) handleDeregisterTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req DeregisterTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
table, err := s.loadLanceTable(r, bucket, ns, name)
|
|
if err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
|
|
// Deregistering is a state, not a deletion. Dropping the catalog entry would
|
|
// take the dataset with it, because the entry is the dataset directory and
|
|
// DeleteTable purges what it holds. The marker is what hides the table, and
|
|
// it is also what a directory-catalog client reads.
|
|
if err := s.writeMarker(r, table.location, deregisteredMarker); err != nil {
|
|
writeError(w, r, http.StatusInternalServerError, codeInternal,
|
|
"could not mark the table deregistered: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, DeregisterTableResponse{
|
|
ID: append(append([]string{bucket}, ns...), name),
|
|
Location: table.location,
|
|
Properties: map[string]string{},
|
|
})
|
|
}
|
|
|
|
// handleDropTable removes the table and its data.
|
|
func (s *Server) handleDropTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req DropTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
|
|
table, err := s.loadLanceTable(r, bucket, ns, name)
|
|
if err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
if err := s.dropTableEntry(r, bucket, ns, name); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, DropTableResponse{
|
|
ID: append(append([]string{bucket}, ns...), name),
|
|
Location: table.location,
|
|
Properties: map[string]string{},
|
|
})
|
|
}
|
|
|
|
// handleRenameTable moves a table's catalog entry. The dataset stays put, which
|
|
// is what the storage layer already does for a renamed Iceberg table.
|
|
func (s *Server) handleRenameTable(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := routeIdentifier(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req RenameTableRequest
|
|
if err := decodeBody(r, &req); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
if len(id) == 0 && len(req.ID) > 0 {
|
|
id = req.ID
|
|
}
|
|
if !checkBodyIdentifier(w, r, id, req.ID) {
|
|
return
|
|
}
|
|
bucket, ns, name, err := id.table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, err.Error())
|
|
return
|
|
}
|
|
destBucket, destNS, destName, err := identifier(req.NewID).table()
|
|
if err != nil {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput, "new_id: "+err.Error())
|
|
return
|
|
}
|
|
if destBucket != bucket {
|
|
writeError(w, r, http.StatusBadRequest, codeInvalidInput,
|
|
"a table cannot move between table buckets")
|
|
return
|
|
}
|
|
|
|
if _, err := s.loadLanceTable(r, bucket, ns, name); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
|
|
renameReq := &s3tables.RenameTableRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
SourceNamespace: ns,
|
|
SourceName: name,
|
|
DestNamespace: destNS,
|
|
DestName: destName,
|
|
}
|
|
var renameResp s3tables.RenameTableResponse
|
|
if err := s.execute(r, "RenameTable", renameReq, &renameResp); err != nil {
|
|
writeStorageError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, RenameTableResponse{})
|
|
}
|
|
|
|
// lanceTable is the catalog's record of one Lance table.
|
|
type lanceTable struct {
|
|
location string
|
|
versionToken string
|
|
}
|
|
|
|
// loadLanceTable reads a table and refuses one that is not a Lance table, so a
|
|
// Lance client never resolves an Iceberg table's location and writes over it.
|
|
func (s *Server) loadLanceTable(r *http.Request, bucket string, ns []string, name string) (*lanceTable, error) {
|
|
req := &s3tables.GetTableRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
Namespace: ns,
|
|
Name: name,
|
|
}
|
|
var resp s3tables.GetTableResponse
|
|
if err := s.execute(r, "GetTable", req, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Format != s3tables.FormatLance {
|
|
return nil, &s3tables.S3TablesError{
|
|
Type: s3tables.ErrCodeNoSuchTable,
|
|
Message: "table " + name + " is not a lance table",
|
|
}
|
|
}
|
|
location := resp.MetadataLocation
|
|
if location == "" {
|
|
location = tableLocation(bucket, resp.Namespace, resp.Name)
|
|
}
|
|
return &lanceTable{location: location, versionToken: resp.VersionToken}, nil
|
|
}
|
|
|
|
func (s *Server) createTable(r *http.Request, bucket string, ns []string, name, location string) error {
|
|
req := &s3tables.CreateTableRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
Namespace: ns,
|
|
Name: name,
|
|
Format: s3tables.FormatLance,
|
|
// A Lance table has no metadata file, so the location the catalog stores
|
|
// is the dataset root itself.
|
|
MetadataLocation: location,
|
|
}
|
|
var resp s3tables.CreateTableResponse
|
|
return s.execute(r, "CreateTable", req, &resp)
|
|
}
|
|
|
|
// repointTable moves an existing entry to another dataset location without
|
|
// touching either dataset's files.
|
|
func (s *Server) repointTable(r *http.Request, bucket string, ns []string, name, location, versionToken string) error {
|
|
req := &s3tables.UpdateTableRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
Namespace: ns,
|
|
Name: name,
|
|
VersionToken: versionToken,
|
|
MetadataLocation: location,
|
|
}
|
|
var resp s3tables.UpdateTableResponse
|
|
return s.execute(r, "UpdateTable", req, &resp)
|
|
}
|
|
|
|
// dropTableEntry removes the catalog entry and the dataset under it. Only
|
|
// DropTable wants this; deregistering and repointing must leave the files.
|
|
func (s *Server) dropTableEntry(r *http.Request, bucket string, ns []string, name string) error {
|
|
req := &s3tables.DeleteTableRequest{
|
|
TableBucketARN: bucketARN(bucket),
|
|
Namespace: ns,
|
|
Name: name,
|
|
}
|
|
return s.execute(r, "DeleteTable", req, nil)
|
|
}
|