snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
536
weed/admin/dash/iceberg_data_preview.go
Normal file
536
weed/admin/dash/iceberg_data_preview.go
Normal file
@@ -0,0 +1,536 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/apache/iceberg-go"
|
||||
"github.com/parquet-go/parquet-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
const (
|
||||
icebergPreviewDefaultRows = 50
|
||||
icebergPreviewMaxRows = 200
|
||||
icebergPreviewMaxFiles = 5
|
||||
icebergPreviewMaxManifests = 100
|
||||
icebergPreviewMaxListed = 500
|
||||
icebergPreviewMaxCellChars = 200
|
||||
icebergPreviewMaxMetaBytes = 64 << 20
|
||||
|
||||
// workerPreviewTimeout bounds one round trip to a worker. A page waiting on
|
||||
// a worker that has gone quiet should say so rather than hang.
|
||||
workerPreviewTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type IcebergDataFileInfo struct {
|
||||
Path string `json:"path"`
|
||||
Format string `json:"format"`
|
||||
RecordCount int64 `json:"record_count"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// IcebergDataPreviewData backs the table data-preview page. PreviewError and
|
||||
// PreviewNotes report per-snapshot problems without failing the whole page.
|
||||
type IcebergDataPreviewData struct {
|
||||
Username string `json:"username"`
|
||||
CatalogName string `json:"catalog_name"`
|
||||
NamespaceName string `json:"namespace_name"`
|
||||
TableName string `json:"table_name"`
|
||||
BucketARN string `json:"bucket_arn"`
|
||||
SnapshotID int64 `json:"snapshot_id"`
|
||||
SnapshotTime time.Time `json:"snapshot_time"`
|
||||
CurrentSnapshotID int64 `json:"current_snapshot_id"`
|
||||
Snapshots []IcebergSnapshotInfo `json:"snapshots"`
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]string `json:"rows"`
|
||||
RowLimit int `json:"row_limit"`
|
||||
ScannedFiles int `json:"scanned_files"`
|
||||
SelectedFile string `json:"selected_file"`
|
||||
DataFiles []IcebergDataFileInfo `json:"data_files"`
|
||||
TotalDataFiles int `json:"total_data_files"`
|
||||
TotalRecords int64 `json:"total_records"`
|
||||
HasDeletes bool `json:"has_deletes"`
|
||||
PreviewNotes []string `json:"preview_notes,omitempty"`
|
||||
PreviewError string `json:"preview_error,omitempty"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
// Format is what the catalog recorded. Anything but ICEBERG means the rows
|
||||
// below, if any, came from a worker rather than from metadata this server read.
|
||||
Format string `json:"format,omitempty"`
|
||||
PreviewedBy string `json:"previewed_by,omitempty"`
|
||||
}
|
||||
|
||||
// GetIcebergTableDataPreview walks the selected snapshot's manifests and reads
|
||||
// sample rows from its Parquet data files. snapshotID 0 means the current
|
||||
// snapshot; selectedFile restricts the preview to one manifest-listed file.
|
||||
func (s *AdminServer) GetIcebergTableDataPreview(ctx context.Context, catalogName, bucketArn, namespace, tableName string, snapshotID int64, selectedFile string, rowLimit int) (IcebergDataPreviewData, error) {
|
||||
if rowLimit < 1 {
|
||||
rowLimit = icebergPreviewDefaultRows
|
||||
}
|
||||
if rowLimit > icebergPreviewMaxRows {
|
||||
rowLimit = icebergPreviewMaxRows
|
||||
}
|
||||
|
||||
data := IcebergDataPreviewData{
|
||||
CatalogName: catalogName,
|
||||
NamespaceName: namespace,
|
||||
TableName: tableName,
|
||||
BucketARN: bucketArn,
|
||||
RowLimit: rowLimit,
|
||||
SelectedFile: selectedFile,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
namespaceParts, err := parseNamespaceInput(namespace)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
var resp s3tables.GetTableResponse
|
||||
req := &s3tables.GetTableRequest{TableBucketARN: bucketArn, Namespace: namespaceParts, Name: tableName}
|
||||
if err := s.executeS3TablesOperation(ctx, "GetTable", req, &resp); err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Format = resp.Format
|
||||
if !strings.EqualFold(resp.Format, s3tables.FormatIceberg) && resp.Format != "" {
|
||||
s.applyWorkerPreview(ctx, &data, bucketArn, namespaceParts, tableName, rowLimit)
|
||||
return data, nil
|
||||
}
|
||||
if resp.Metadata == nil || len(resp.Metadata.FullMetadata) == 0 {
|
||||
data.PreviewError = "Table has no Iceberg metadata."
|
||||
return data, nil
|
||||
}
|
||||
var full icebergFullMetadata
|
||||
if err := json.Unmarshal(resp.Metadata.FullMetadata, &full); err != nil {
|
||||
data.PreviewError = fmt.Sprintf("Failed to parse Iceberg metadata: %v", err)
|
||||
return data, nil
|
||||
}
|
||||
data.Snapshots = snapshotsFromFullMetadata(full.Snapshots)
|
||||
data.CurrentSnapshotID = full.CurrentSnapshotID
|
||||
|
||||
snap := selectPreviewSnapshot(full, snapshotID)
|
||||
if snap == nil {
|
||||
if snapshotID != 0 {
|
||||
data.PreviewError = fmt.Sprintf("Snapshot %d not found.", snapshotID)
|
||||
} else {
|
||||
data.PreviewError = "Table has no snapshots yet."
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
data.SnapshotID = snap.SnapshotID
|
||||
if snap.TimestampMs > 0 {
|
||||
data.SnapshotTime = time.Unix(0, snap.TimestampMs*int64(time.Millisecond))
|
||||
}
|
||||
if snap.ManifestList == "" {
|
||||
data.PreviewError = "Snapshot has no manifest list."
|
||||
return data, nil
|
||||
}
|
||||
|
||||
dataFiles, hasDeletes, notes, err := s.listIcebergDataFiles(ctx, catalogName, namespace, tableName, snap.ManifestList)
|
||||
if err != nil {
|
||||
data.PreviewError = fmt.Sprintf("Failed to read snapshot manifests: %v", err)
|
||||
return data, nil
|
||||
}
|
||||
data.HasDeletes = hasDeletes
|
||||
data.PreviewNotes = notes
|
||||
data.TotalDataFiles = len(dataFiles)
|
||||
for _, f := range dataFiles {
|
||||
data.TotalRecords += f.RecordCount
|
||||
}
|
||||
data.DataFiles = dataFiles
|
||||
if len(dataFiles) > icebergPreviewMaxListed {
|
||||
data.DataFiles = dataFiles[:icebergPreviewMaxListed]
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Listing first %d of %d data files.", icebergPreviewMaxListed, len(dataFiles)))
|
||||
}
|
||||
|
||||
toScan := dataFiles
|
||||
if selectedFile != "" {
|
||||
toScan = nil
|
||||
for _, f := range dataFiles {
|
||||
if f.Path == selectedFile {
|
||||
toScan = []IcebergDataFileInfo{f}
|
||||
break
|
||||
}
|
||||
}
|
||||
if toScan == nil {
|
||||
data.PreviewError = "Requested data file is not part of this snapshot."
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
if len(toScan) == 0 {
|
||||
data.PreviewNotes = append(data.PreviewNotes, "Snapshot contains no data files.")
|
||||
return data, nil
|
||||
}
|
||||
|
||||
for _, f := range toScan {
|
||||
if len(data.Rows) >= rowLimit || data.ScannedFiles >= icebergPreviewMaxFiles {
|
||||
break
|
||||
}
|
||||
if !strings.EqualFold(f.Format, string(iceberg.ParquetFile)) {
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: %s preview is not supported.", path.Base(f.Path), f.Format))
|
||||
continue
|
||||
}
|
||||
filerPath, err := icebergLocationToFilerPath(f.Path, catalogName, namespace, tableName)
|
||||
if err != nil {
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: %v", path.Base(f.Path), err))
|
||||
continue
|
||||
}
|
||||
cols, rows, err := s.readParquetRows(ctx, filerPath, rowLimit-len(data.Rows))
|
||||
if err != nil {
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Failed to read %s: %v", path.Base(f.Path), err))
|
||||
data.ScannedFiles++
|
||||
continue
|
||||
}
|
||||
if data.Columns == nil {
|
||||
data.Columns = cols
|
||||
} else if !slices.Equal(data.Columns, cols) {
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: column layout differs from the first file.", path.Base(f.Path)))
|
||||
continue
|
||||
}
|
||||
data.Rows = append(data.Rows, rows...)
|
||||
data.ScannedFiles++
|
||||
}
|
||||
if hasDeletes {
|
||||
data.PreviewNotes = append(data.PreviewNotes, "Snapshot has row-level delete files; the preview shows raw data-file rows without applying deletes.")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func selectPreviewSnapshot(full icebergFullMetadata, snapshotID int64) *icebergSnapshot {
|
||||
if snapshotID != 0 {
|
||||
for i := range full.Snapshots {
|
||||
if full.Snapshots[i].SnapshotID == snapshotID {
|
||||
return &full.Snapshots[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return selectSnapshotForMetrics(full)
|
||||
}
|
||||
|
||||
func (s *AdminServer) listIcebergDataFiles(ctx context.Context, bucketName, namespaceDir, tableName, manifestListLocation string) (files []IcebergDataFileInfo, hasDeletes bool, notes []string, err error) {
|
||||
listPath, err := icebergLocationToFilerPath(manifestListLocation, bucketName, namespaceDir, tableName)
|
||||
if err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
listBytes, err := s.readFilerFileContent(ctx, listPath)
|
||||
if err != nil {
|
||||
return nil, false, nil, fmt.Errorf("read manifest list %s: %w", manifestListLocation, err)
|
||||
}
|
||||
manifests, err := s3tables.ReadManifestList(listBytes)
|
||||
if err != nil {
|
||||
return nil, false, nil, fmt.Errorf("parse manifest list: %w", err)
|
||||
}
|
||||
|
||||
scanned := 0
|
||||
for _, mf := range manifests {
|
||||
if mf.ManifestContent() != iceberg.ManifestContentData {
|
||||
hasDeletes = true
|
||||
continue
|
||||
}
|
||||
if scanned >= icebergPreviewMaxManifests {
|
||||
notes = append(notes, fmt.Sprintf("Scanned first %d of %d manifests.", icebergPreviewMaxManifests, len(manifests)))
|
||||
break
|
||||
}
|
||||
scanned++
|
||||
mfPath, err := icebergLocationToFilerPath(mf.FilePath(), bucketName, namespaceDir, tableName)
|
||||
if err != nil {
|
||||
notes = append(notes, fmt.Sprintf("Skipped manifest %s: %v", path.Base(mf.FilePath()), err))
|
||||
continue
|
||||
}
|
||||
mfBytes, err := s.readFilerFileContent(ctx, mfPath)
|
||||
if err != nil {
|
||||
notes = append(notes, fmt.Sprintf("Failed to read manifest %s: %v", path.Base(mf.FilePath()), err))
|
||||
continue
|
||||
}
|
||||
entries, err := iceberg.ReadManifest(mf, bytes.NewReader(mfBytes), true)
|
||||
if err != nil {
|
||||
notes = append(notes, fmt.Sprintf("Failed to parse manifest %s: %v", path.Base(mf.FilePath()), err))
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
df := entry.DataFile()
|
||||
files = append(files, IcebergDataFileInfo{
|
||||
Path: df.FilePath(),
|
||||
Format: string(df.FileFormat()),
|
||||
RecordCount: df.Count(),
|
||||
SizeBytes: df.FileSizeBytes(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return files, hasDeletes, notes, nil
|
||||
}
|
||||
|
||||
// icebergLocationToFilerPath maps an Iceberg file location (s3://bucket/...,
|
||||
// an absolute filer path, or a table-relative path like data/x.parquet) to a
|
||||
// filer path, which must stay under the table-buckets root.
|
||||
func icebergLocationToFilerPath(location, bucketName, namespaceDir, tableName string) (string, error) {
|
||||
p := location
|
||||
if idx := strings.Index(p, "://"); idx >= 0 {
|
||||
p = path.Join(s3tables.TablesPath, p[idx+3:])
|
||||
} else if strings.HasPrefix(p, "/") {
|
||||
p = path.Clean(p)
|
||||
} else {
|
||||
tableDir := path.Join(s3tables.TablesPath, bucketName, namespaceDir, tableName)
|
||||
p = path.Join(tableDir, p)
|
||||
if !strings.HasPrefix(p, tableDir+"/") {
|
||||
return "", fmt.Errorf("location %q resolves outside the table directory", location)
|
||||
}
|
||||
}
|
||||
p = path.Clean(p)
|
||||
if !strings.HasPrefix(p, s3tables.TablesPath+"/") {
|
||||
return "", fmt.Errorf("location %q resolves outside %s", location, s3tables.TablesPath)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *AdminServer) lookupFilerEntry(ctx context.Context, fullPath string) (*filer_pb.Entry, error) {
|
||||
dir, name := path.Dir(fullPath), path.Base(fullPath)
|
||||
var entry *filer_pb.Entry
|
||||
err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry = resp.Entry
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("not found: %s", fullPath)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *AdminServer) readFilerFileContent(ctx context.Context, fullPath string) ([]byte, error) {
|
||||
entry, err := s.lookupFilerEntry(ctx, fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(entry.Content) > 0 || len(entry.GetChunks()) == 0 {
|
||||
return entry.Content, nil
|
||||
}
|
||||
size := int64(filer.FileSize(entry))
|
||||
if size > icebergPreviewMaxMetaBytes {
|
||||
return nil, fmt.Errorf("file too large to load (%d bytes)", size)
|
||||
}
|
||||
streamFn, err := filer.PrepareStreamContentWithThrottler(ctx, s.GetMasterClient(), VolumeServerReadJwt, entry.GetChunks(), 0, size, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := streamFn(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *AdminServer) readParquetRows(ctx context.Context, fullPath string, want int) ([]string, [][]string, error) {
|
||||
entry, err := s.lookupFilerEntry(ctx, fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var readerAt io.ReaderAt
|
||||
var size int64
|
||||
if len(entry.Content) > 0 || len(entry.GetChunks()) == 0 {
|
||||
readerAt = bytes.NewReader(entry.Content)
|
||||
size = int64(len(entry.Content))
|
||||
} else {
|
||||
size = int64(filer.FileSize(entry))
|
||||
readerAt = &filerChunkReaderAt{ctx: ctx, server: s, chunks: entry.GetChunks(), size: size}
|
||||
}
|
||||
|
||||
pf, err := parquet.OpenFile(readerAt, size, parquet.SkipPageIndex(true), parquet.SkipBloomFilters(true))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("open parquet: %w", err)
|
||||
}
|
||||
leafColumns := pf.Schema().Columns()
|
||||
cols := make([]string, len(leafColumns))
|
||||
for i, c := range leafColumns {
|
||||
cols[i] = strings.Join(c, ".")
|
||||
}
|
||||
|
||||
reader := parquet.NewReader(pf)
|
||||
defer reader.Close()
|
||||
rowBuf := make([]parquet.Row, 32)
|
||||
var out [][]string
|
||||
for len(out) < want {
|
||||
n, readErr := reader.ReadRows(rowBuf)
|
||||
for i := 0; i < n && len(out) < want; i++ {
|
||||
out = append(out, formatParquetRow(rowBuf[i], len(cols)))
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, nil, readErr
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cols, out, nil
|
||||
}
|
||||
|
||||
func formatParquetRow(row parquet.Row, columnCount int) []string {
|
||||
cells := make([]string, columnCount)
|
||||
seen := make([]bool, columnCount)
|
||||
for _, v := range row {
|
||||
ci := v.Column()
|
||||
if ci < 0 || ci >= columnCount {
|
||||
continue
|
||||
}
|
||||
cell := formatParquetValue(v)
|
||||
if seen[ci] {
|
||||
cells[ci] += ", " + cell
|
||||
} else {
|
||||
cells[ci] = cell
|
||||
seen[ci] = true
|
||||
}
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
func formatParquetValue(v parquet.Value) string {
|
||||
if v.IsNull() {
|
||||
return ""
|
||||
}
|
||||
switch v.Kind() {
|
||||
case parquet.ByteArray, parquet.FixedLenByteArray:
|
||||
b := v.ByteArray()
|
||||
if utf8.Valid(b) {
|
||||
return truncateCell(string(b))
|
||||
}
|
||||
return truncateCell(fmt.Sprintf("0x%x", b))
|
||||
default:
|
||||
return truncateCell(v.String())
|
||||
}
|
||||
}
|
||||
|
||||
func truncateCell(s string) string {
|
||||
if len(s) <= icebergPreviewMaxCellChars {
|
||||
return s
|
||||
}
|
||||
cut := icebergPreviewMaxCellChars
|
||||
for cut > 0 && !utf8.RuneStart(s[cut]) {
|
||||
cut--
|
||||
}
|
||||
return s[:cut] + "…"
|
||||
}
|
||||
|
||||
// filerChunkReaderAt serves random-access reads over a chunked filer entry by
|
||||
// issuing ranged volume-server reads, carrying the read JWT when configured.
|
||||
type filerChunkReaderAt struct {
|
||||
ctx context.Context
|
||||
server *AdminServer
|
||||
chunks []*filer_pb.FileChunk
|
||||
size int64
|
||||
}
|
||||
|
||||
func (r *filerChunkReaderAt) ReadAt(p []byte, off int64) (int, error) {
|
||||
if off < 0 {
|
||||
return 0, fmt.Errorf("negative offset: %d", off)
|
||||
}
|
||||
if off >= r.size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
want := int64(len(p))
|
||||
if off+want > r.size {
|
||||
want = r.size - off
|
||||
}
|
||||
streamFn, err := filer.PrepareStreamContentWithThrottler(r.ctx, r.server.GetMasterClient(), VolumeServerReadJwt, r.chunks, off, want, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Write straight into p; a bytes.Buffer wrapping p would silently
|
||||
// allocate a fresh backing array if it ever grew, dropping bytes the
|
||||
// caller expects in p.
|
||||
w := &sliceWriter{dst: p}
|
||||
if err := streamFn(w); err != nil {
|
||||
return w.n, err
|
||||
}
|
||||
if w.n < len(p) {
|
||||
return w.n, io.EOF
|
||||
}
|
||||
return w.n, nil
|
||||
}
|
||||
|
||||
// sliceWriter writes into a fixed destination slice, discarding any overflow.
|
||||
type sliceWriter struct {
|
||||
dst []byte
|
||||
n int
|
||||
}
|
||||
|
||||
func (w *sliceWriter) Write(p []byte) (int, error) {
|
||||
c := copy(w.dst[w.n:], p)
|
||||
w.n += c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// applyWorkerPreview asks the worker that last described this table for sample
|
||||
// rows. Admin has no reader for a format it does not implement, so this is the
|
||||
// only way the page shows anything but a location.
|
||||
//
|
||||
// The rows are fetched, never cached: they are the table's data rather than a
|
||||
// description of it, and a stale copy sitting in admin would be worse than
|
||||
// asking.
|
||||
func (s *AdminServer) applyWorkerPreview(ctx context.Context, data *IcebergDataPreviewData, bucketArn string, namespaceParts []string, tableName string, rowLimit int) {
|
||||
plugin := s.GetPlugin()
|
||||
if plugin == nil {
|
||||
data.PreviewError = fmt.Sprintf("Reading a %s table needs a plugin worker, and none is configured.", data.Format)
|
||||
return
|
||||
}
|
||||
bucketName, err := s3tables.ParseBucketNameFromARN(bucketArn)
|
||||
if err != nil {
|
||||
data.PreviewError = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
objectID := append(append([]string{bucketName}, namespaceParts...), tableName)
|
||||
requestCtx, cancel := context.WithTimeout(ctx, workerPreviewTimeout)
|
||||
defer cancel()
|
||||
|
||||
response, err := plugin.RequestObjectPreview(requestCtx, objectID, data.Format, rowLimit)
|
||||
if err != nil {
|
||||
data.PreviewError = fmt.Sprintf("Could not read this %s table: %v", data.Format, err)
|
||||
return
|
||||
}
|
||||
|
||||
data.Columns = response.Columns
|
||||
data.Rows = make([][]string, 0, len(response.Rows))
|
||||
for _, row := range response.Rows {
|
||||
cells := make([]string, len(row.Values))
|
||||
for i, value := range row.Values {
|
||||
cells[i] = truncateCell(value)
|
||||
}
|
||||
data.Rows = append(data.Rows, cells)
|
||||
}
|
||||
data.TotalRecords = response.TotalRows
|
||||
data.PreviewedBy = pluginWorkerForObject(plugin, objectID, data.Format)
|
||||
if int64(len(data.Rows)) < response.TotalRows {
|
||||
data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Showing %d of %d rows.", len(data.Rows), response.TotalRows))
|
||||
}
|
||||
}
|
||||
|
||||
// pluginWorkerForObject names the worker that answered, for the page to show.
|
||||
func pluginWorkerForObject(p *plugin.Plugin, objectID []string, format string) string {
|
||||
if observed, ok := p.Observations().GetFormat(objectID, format); ok {
|
||||
return observed.WorkerID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user