This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 0dd6e1f9f feat(cmd): list files from a scan plan when the catalog 
requires server-side planning (#2260)
0dd6e1f9f is described below

commit 0dd6e1f9f480c1792b01dbe80a83ca7fb8c6d73c
Author: Revanth <[email protected]>
AuthorDate: Thu Oct 22 10:28:08 2026 -0500

    feat(cmd): list files from a scan plan when the catalog requires 
server-side planning (#2260)
---
 cmd/iceberg/files.go      | 160 +++++++++++++++++++++
 cmd/iceberg/files_test.go | 344 ++++++++++++++++++++++++++++++++++++++++++++++
 cmd/iceberg/main.go       |   8 +-
 cmd/iceberg/output.go     |  41 ++++++
 website/src/cli.md        |  13 ++
 5 files changed, 562 insertions(+), 4 deletions(-)

diff --git a/cmd/iceberg/files.go b/cmd/iceberg/files.go
new file mode 100644
index 000000000..adc75a871
--- /dev/null
+++ b/cmd/iceberg/files.go
@@ -0,0 +1,160 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package main
+
+import (
+       "context"
+       "errors"
+       "fmt"
+
+       "github.com/apache/iceberg-go"
+       "github.com/apache/iceberg-go/catalog"
+       "github.com/apache/iceberg-go/table"
+)
+
+// runFiles lists a table's files. By default it walks the manifests itself,
+// which shows the manifest tree. The files come from a scan plan instead when
+// --scan-planning-mode is remote or auto, or when the catalog requires
+// server-side planning (scan-planning-mode=server), since that directive
+// forbids reading manifests with the table's own credentials.
+func runFiles(ctx context.Context, output Output, cat catalog.Catalog, cmd 
*FilesCmd) {
+       mode := table.ScanPlanningMode(cmd.ScanPlanningMode)
+       if err := validateScanPlanningMode(mode); err != nil {
+               output.Error(err)
+               osExit(1)
+
+               return
+       }
+
+       tbl := loadTable(ctx, output, cat, cmd.TableID)
+
+       planned, err := filesFromScanPlan(tbl, mode, cmd.History)
+       if err != nil {
+               output.Error(err)
+               osExit(1)
+
+               return
+       }
+
+       if !planned {
+               output.Files(tbl, cmd.History)
+
+               return
+       }
+
+       tasks, err := planCurrentFiles(ctx, tbl, mode)
+       if err != nil {
+               output.Error(err)
+               osExit(1)
+
+               return
+       }
+
+       output.PlannedFiles(tbl, tasks)
+}
+
+func validateScanPlanningMode(mode table.ScanPlanningMode) error {
+       switch mode {
+       case "", table.ScanPlanningLocal, table.ScanPlanningRemote, 
table.ScanPlanningAuto:
+               return nil
+       default:
+               return fmt.Errorf("invalid --scan-planning-mode %q: expected 
%q, %q or %q",
+                       mode, table.ScanPlanningLocal, 
table.ScanPlanningRemote, table.ScanPlanningAuto)
+       }
+}
+
+// filesFromScanPlan reports whether files must list a scan plan rather than
+// walk the manifests. A remote or auto mode that conflicts with a `client`
+// directive is left for PlanFiles to reject.
+func filesFromScanPlan(tbl *table.Table, mode table.ScanPlanningMode, history 
bool) (bool, error) {
+       directive, err := tbl.ScanPlanningDirective()
+       if err != nil {
+               return false, err
+       }
+
+       var planned bool
+       switch mode {
+       case "":
+               planned = directive == table.ScanPlanningDirectiveServer
+       case table.ScanPlanningLocal:
+               if directive == table.ScanPlanningDirectiveServer {
+                       return false, fmt.Errorf("the catalog requires 
server-side scan planning (%s=%s), so files cannot read manifests locally",
+                               table.ScanPlanningModeKey, 
table.ScanPlanningDirectiveServer)
+               }
+       default:
+               planned = true
+       }
+
+       if planned && history {
+               if mode == "" {
+                       return false, fmt.Errorf("--history reads every 
snapshot's manifests, which the catalog forbids (%s=%s)",
+                               table.ScanPlanningModeKey, 
table.ScanPlanningDirectiveServer)
+               }
+
+               return false, errors.New("--history reads every snapshot's 
manifests and cannot be combined with --scan-planning-mode " + string(mode))
+       }
+
+       return planned, nil
+}
+
+// planCurrentFiles plans a full scan of the current snapshot. A table without
+// a snapshot has no files, so it is not planned.
+func planCurrentFiles(ctx context.Context, tbl *table.Table, mode 
table.ScanPlanningMode) ([]table.FileScanTask, error) {
+       if tbl.CurrentSnapshot() == nil {
+               return nil, nil
+       }
+
+       var opts []table.ScanOption
+       if mode != "" {
+               opts = append(opts, table.WithScanPlanningMode(mode))
+       }
+
+       scan := tbl.Scan(opts...)
+       defer scan.Close()
+
+       return scan.PlanFiles(ctx)
+}
+
+type plannedFile struct {
+       Path        string   `json:"file-path"`
+       DeleteFiles []string `json:"delete-files,omitempty"`
+}
+
+// plannedDataFiles collapses scan tasks to one entry per data file. A planner
+// may split a large file into several tasks, which share its delete files.
+func plannedDataFiles(tasks []table.FileScanTask) []plannedFile {
+       files := make([]plannedFile, 0, len(tasks))
+       seen := make(map[string]bool, len(tasks))
+       for _, task := range tasks {
+               path := task.File.FilePath()
+               if seen[path] {
+                       continue
+               }
+               seen[path] = true
+
+               file := plannedFile{Path: path}
+               for _, deletes := range [][]iceberg.DataFile{task.DeleteFiles, 
task.EqualityDeleteFiles, task.DeletionVectorFiles} {
+                       for _, df := range deletes {
+                               file.DeleteFiles = append(file.DeleteFiles, 
df.FilePath())
+                       }
+               }
+               files = append(files, file)
+       }
+
+       return files
+}
diff --git a/cmd/iceberg/files_test.go b/cmd/iceberg/files_test.go
new file mode 100644
index 000000000..84c160db4
--- /dev/null
+++ b/cmd/iceberg/files_test.go
@@ -0,0 +1,344 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package main
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "errors"
+       "os"
+       "strings"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+       "github.com/apache/iceberg-go/catalog"
+       iceio "github.com/apache/iceberg-go/io"
+       "github.com/apache/iceberg-go/table"
+       "github.com/pterm/pterm"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+const filesTestMetadata = `{
+    "format-version": 2,
+    "table-uuid": "c8e7f6d4-03fe-4693-9a96-a0705ddf69c2",
+    "location": "mem://default/table",
+    "last-sequence-number": 1,
+    "last-updated-ms": 1602638573590,
+    "last-column-id": 1,
+    "current-schema-id": 0,
+    "schemas": [
+        {"type": "struct", "schema-id": 0, "fields": [{"id": 1, "name": "id", 
"required": true, "type": "int"}]}
+    ],
+    "default-spec-id": 0,
+    "partition-specs": [{"spec-id": 0, "fields": []}],
+    "last-partition-id": 1000,
+    "default-sort-order-id": 0,
+    "sort-orders": [{"order-id": 0, "fields": []}],
+    "properties": {},
+    "current-snapshot-id": 100,
+    "snapshots": [
+        {
+            "snapshot-id": 100,
+            "timestamp-ms": 1602638573590,
+            "sequence-number": 1,
+            "schema-id": 0,
+            "summary": {"operation": "append"}
+        }
+    ],
+    "snapshot-log": [],
+    "refs": {}
+}`
+
+// filesPlanner is a remote scan planner that returns fixed tasks. It also
+// serves as the table's CatalogIO, which is how table.New finds a planner.
+type filesPlanner struct {
+       tasks []table.FileScanTask
+       calls int
+}
+
+func (p *filesPlanner) SupportsRemoteScanPlanning() bool { return true }
+
+func (p *filesPlanner) PlanFiles(context.Context, table.ScanPlanningRequest) 
(table.ScanPlanningResult, error) {
+       p.calls++
+
+       return table.ScanPlanningResult{Tasks: p.tasks}, nil
+}
+
+func (p *filesPlanner) LoadTable(context.Context, table.Identifier) 
(*table.Table, error) {
+       return nil, errors.New("unexpected LoadTable call")
+}
+
+func (p *filesPlanner) CommitTable(context.Context, table.Identifier, 
[]table.Requirement, []table.Update) (table.Metadata, string, error) {
+       return nil, "", errors.New("unexpected CommitTable call")
+}
+
+type filesCatalog struct {
+       catalog.Catalog
+       tbl *table.Table
+}
+
+func (c *filesCatalog) LoadTable(context.Context, table.Identifier) 
(*table.Table, error) {
+       return c.tbl, nil
+}
+
+type filesCapture struct {
+       textOutput
+       lastErr error
+}
+
+func (f *filesCapture) Error(err error) {
+       f.lastErr = err
+}
+
+type filesTableOptions struct {
+       directive  string
+       noSnapshot bool
+       // readManifests lets the table load its FileIO. Otherwise loading it 
fails
+       // the test, which is how the server-directive cases prove files never
+       // walked the manifests. The FileIO is still returned, so a regression
+       // shows up as assertion failures rather than textOutput.Files exiting.
+       readManifests bool
+}
+
+func newFilesTable(t *testing.T, planner *filesPlanner, opts 
filesTableOptions) *table.Table {
+       t.Helper()
+
+       metaJSON := filesTestMetadata
+       if opts.noSnapshot {
+               metaJSON = strings.Replace(metaJSON, `"current-snapshot-id": 
100,`, "", 1)
+       }
+       meta, err := table.ParseMetadataBytes([]byte(metaJSON))
+       require.NoError(t, err)
+
+       fsF := func(ctx context.Context) (iceio.IO, error) {
+               if !opts.readManifests {
+                       t.Error("files read the table's manifests locally")
+               }
+
+               return iceio.LoadFS(ctx, nil, "mem://default/table")
+       }
+
+       return table.New([]string{"db", "events"}, meta, "", fsF, planner,
+               table.WithScanPlanningDirective(opts.directive, opts.directive 
!= ""))
+}
+
+func filesTestDataFile(t *testing.T, content iceberg.ManifestEntryContent, 
path string) iceberg.DataFile {
+       t.Helper()
+
+       builder, err := iceberg.NewDataFileBuilder(*iceberg.UnpartitionedSpec, 
content, path,
+               iceberg.ParquetFile, nil, nil, nil, 10, 100)
+       require.NoError(t, err)
+
+       return builder.Build()
+}
+
+func filesTestTasks(t *testing.T) []table.FileScanTask {
+       t.Helper()
+
+       first := filesTestDataFile(t, iceberg.EntryContentData, 
"mem://default/table/data/a.parquet")
+       posDelete := filesTestDataFile(t, iceberg.EntryContentPosDeletes, 
"mem://default/table/data/a-deletes.parquet")
+       second := filesTestDataFile(t, iceberg.EntryContentData, 
"mem://default/table/data/b.parquet")
+
+       return []table.FileScanTask{
+               {File: first, DeleteFiles: []iceberg.DataFile{posDelete}, 
Start: 0, Length: 50},
+               {File: first, DeleteFiles: []iceberg.DataFile{posDelete}, 
Start: 50, Length: 50},
+               {File: second, Length: 100},
+       }
+}
+
+func runFilesCapture(t *testing.T, tbl *table.Table, cmd *FilesCmd) (out 
string, capture *filesCapture, exitCode int) {
+       t.Helper()
+
+       var buf bytes.Buffer
+       pterm.SetDefaultOutput(&buf)
+       pterm.DisableColor()
+       t.Cleanup(func() { pterm.SetDefaultOutput(os.Stdout) })
+
+       capture = &filesCapture{}
+       exitCode = captureBranchTagExit(func() {
+               runFiles(context.Background(), capture, &filesCatalog{tbl: 
tbl}, cmd)
+       })
+
+       return buf.String(), capture, exitCode
+}
+
+func TestRunFilesListsScanPlan(t *testing.T) {
+       tests := []struct {
+               name      string
+               directive string
+               mode      string
+       }{
+               {name: "server directive", directive: "server"},
+               {name: "server directive with remote mode", directive: 
"server", mode: "remote"},
+               {name: "remote mode", mode: "remote"},
+               {name: "auto mode", mode: "auto"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       planner := &filesPlanner{tasks: filesTestTasks(t)}
+                       tbl := newFilesTable(t, planner, 
filesTableOptions{directive: tt.directive})
+
+                       out, capture, exitCode := runFilesCapture(t, tbl, 
&FilesCmd{TableID: "db.events", ScanPlanningMode: tt.mode})
+
+                       require.NoError(t, capture.lastErr)
+                       assert.Equal(t, 0, exitCode)
+                       assert.Equal(t, 1, planner.calls)
+                       assert.Contains(t, out, "Planned files: db.events")
+                       assert.Contains(t, out, "Snapshot 100, schema 0")
+                       assert.Equal(t, 1, strings.Count(out, "Datafile: 
mem://default/table/data/a.parquet"),
+                               "a file split into several tasks is listed 
once")
+                       assert.Contains(t, out, "Deletefile: 
mem://default/table/data/a-deletes.parquet")
+                       assert.Contains(t, out, "Datafile: 
mem://default/table/data/b.parquet")
+               })
+       }
+}
+
+func TestRunFilesWalksManifestsLocally(t *testing.T) {
+       tests := []struct {
+               name      string
+               directive string
+               mode      string
+       }{
+               {name: "no directive"},
+               {name: "client directive", directive: "client"},
+               {name: "local mode", mode: "local"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       planner := &filesPlanner{}
+                       tbl := newFilesTable(t, planner, 
filesTableOptions{directive: tt.directive, readManifests: true})
+
+                       out, capture, exitCode := runFilesCapture(t, tbl, 
&FilesCmd{TableID: "db.events", ScanPlanningMode: tt.mode})
+
+                       require.NoError(t, capture.lastErr)
+                       assert.Equal(t, 0, exitCode)
+                       assert.Zero(t, planner.calls)
+                       assert.Contains(t, out, "Snapshots: db.events")
+                       assert.Contains(t, out, "Snapshot 100, schema 0")
+               })
+       }
+}
+
+func TestRunFilesRejects(t *testing.T) {
+       tests := []struct {
+               name      string
+               directive string
+               cmd       FilesCmd
+               wantErr   string
+       }{
+               {
+                       name:      "history under server directive",
+                       directive: "server",
+                       cmd:       FilesCmd{History: true},
+                       wantErr:   "--history reads every snapshot's manifests, 
which the catalog forbids (scan-planning-mode=server)",
+               },
+               {
+                       name:    "history with remote mode",
+                       cmd:     FilesCmd{History: true, ScanPlanningMode: 
"remote"},
+                       wantErr: "cannot be combined with --scan-planning-mode 
remote",
+               },
+               {
+                       name:      "local mode under server directive",
+                       directive: "server",
+                       cmd:       FilesCmd{ScanPlanningMode: "local"},
+                       wantErr:   "the catalog requires server-side scan 
planning (scan-planning-mode=server)",
+               },
+               {
+                       name:      "remote mode under client directive",
+                       directive: "client",
+                       cmd:       FilesCmd{ScanPlanningMode: "remote"},
+                       wantErr:   "the catalog requires client-side scan 
planning",
+               },
+               {
+                       name:      "unrecognized directive",
+                       directive: "sometimes",
+                       wantErr:   `unrecognized scan-planning-mode 
"sometimes"`,
+               },
+               {
+                       name:    "unknown mode",
+                       cmd:     FilesCmd{ScanPlanningMode: "server"},
+                       wantErr: `invalid --scan-planning-mode "server"`,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       planner := &filesPlanner{tasks: filesTestTasks(t)}
+                       tbl := newFilesTable(t, planner, 
filesTableOptions{directive: tt.directive})
+                       tt.cmd.TableID = "db.events"
+
+                       _, capture, exitCode := runFilesCapture(t, tbl, &tt.cmd)
+
+                       require.Error(t, capture.lastErr)
+                       assert.Contains(t, capture.lastErr.Error(), tt.wantErr)
+                       assert.Equal(t, 1, exitCode)
+                       assert.Zero(t, planner.calls)
+               })
+       }
+}
+
+func TestRunFilesSkipsPlanningWithoutSnapshot(t *testing.T) {
+       planner := &filesPlanner{tasks: filesTestTasks(t)}
+       tbl := newFilesTable(t, planner, filesTableOptions{directive: "server", 
noSnapshot: true})
+
+       out, capture, exitCode := runFilesCapture(t, tbl, &FilesCmd{TableID: 
"db.events"})
+
+       require.NoError(t, capture.lastErr)
+       assert.Equal(t, 0, exitCode)
+       assert.Zero(t, planner.calls)
+       assert.Contains(t, out, "Planned files: db.events")
+       assert.NotContains(t, out, "Datafile:")
+}
+
+func TestJSONOutputPlannedFiles(t *testing.T) {
+       oldStdout := os.Stdout
+       r, w, err := os.Pipe()
+       require.NoError(t, err)
+       os.Stdout = w
+       defer func() {
+               os.Stdout = oldStdout
+       }()
+
+       tbl := newFilesTable(t, &filesPlanner{}, filesTableOptions{})
+
+       jsonOutput{}.PlannedFiles(tbl, filesTestTasks(t))
+
+       w.Close()
+       var buf bytes.Buffer
+       _, _ = buf.ReadFrom(r)
+
+       var got struct {
+               Snapshot struct {
+                       SnapshotID int64 `json:"snapshot-id"`
+               } `json:"snapshot"`
+               Files []plannedFile `json:"files"`
+       }
+       require.NoError(t, json.Unmarshal(buf.Bytes(), &got))
+       assert.Equal(t, int64(100), got.Snapshot.SnapshotID)
+       assert.Equal(t, []plannedFile{
+               {
+                       Path:        "mem://default/table/data/a.parquet",
+                       DeleteFiles: 
[]string{"mem://default/table/data/a-deletes.parquet"},
+               },
+               {Path: "mem://default/table/data/b.parquet"},
+       }, got.Files)
+}
diff --git a/cmd/iceberg/main.go b/cmd/iceberg/main.go
index 760dfe934..ae6349f9d 100644
--- a/cmd/iceberg/main.go
+++ b/cmd/iceberg/main.go
@@ -82,8 +82,9 @@ type LocationCmd struct {
 }
 
 type FilesCmd struct {
-       TableID string `arg:"positional,required" help:"full path to a table"`
-       History bool   `arg:"--history" help:"show all snapshots"`
+       TableID          string `arg:"positional,required" help:"full path to a 
table"`
+       History          bool   `arg:"--history" help:"show all snapshots"`
+       ScanPlanningMode string `arg:"--scan-planning-mode" help:"local walks 
the manifests; remote or auto lists a scan plan's files (default: follow the 
catalog's scan-planning-mode)"`
 }
 
 type RenameCmd struct {
@@ -336,8 +337,7 @@ func main() {
        case args.Drop != nil:
                runDrop(ctx, output, cat, args.Drop)
        case args.Files != nil:
-               tbl := loadTable(ctx, output, cat, args.Files.TableID)
-               output.Files(tbl, args.Files.History)
+               runFiles(ctx, output, cat, args.Files)
        case args.Rename != nil:
                runRename(ctx, output, cat, args.Rename)
        case args.Properties != nil:
diff --git a/cmd/iceberg/output.go b/cmd/iceberg/output.go
index 8b3095638..2b19987e7 100644
--- a/cmd/iceberg/output.go
+++ b/cmd/iceberg/output.go
@@ -38,6 +38,7 @@ type Output interface {
        Identifiers([]table.Identifier)
        DescribeTable(*table.Table)
        Files(tbl *table.Table, history bool)
+       PlannedFiles(tbl *table.Table, tasks []table.FileScanTask)
        DescribeProperties(iceberg.Properties)
        Info(tbl *table.Table)
        Snapshots(tbl *table.Table)
@@ -192,6 +193,31 @@ func (t textOutput) Files(tbl *table.Table, history bool) {
        pterm.DefaultTree.WithRoot(node).Render()
 }
 
+func (t textOutput) PlannedFiles(tbl *table.Table, tasks []table.FileScanTask) 
{
+       fileTree := pterm.LeveledList{}
+       if snap := tbl.CurrentSnapshot(); snap != nil {
+               fileTree = append(fileTree, pterm.LeveledListItem{
+                       Level: 0,
+                       Text:  fmt.Sprintf("Snapshot %d, schema %s", 
snap.SnapshotID, snapshotSchemaText(snap.SchemaID)),
+               })
+
+               for _, file := range plannedDataFiles(tasks) {
+                       fileTree = append(fileTree, pterm.LeveledListItem{
+                               Level: 1, Text: "Datafile: " + file.Path,
+                       })
+                       for _, del := range file.DeleteFiles {
+                               fileTree = append(fileTree, 
pterm.LeveledListItem{
+                                       Level: 2, Text: "Deletefile: " + del,
+                               })
+                       }
+               }
+       }
+
+       node := putils.TreeFromLeveledList(fileTree)
+       node.Text = "Planned files: " + strings.Join(tbl.Identifier(), ".")
+       pterm.DefaultTree.WithRoot(node).Render()
+}
+
 func (textOutput) DescribeProperties(props iceberg.Properties) {
        data := pterm.TableData{[]string{"Key", "Value"}}
        for k, v := range props {
@@ -312,6 +338,21 @@ func (j jsonOutput) Files(tbl *table.Table, history bool) {
        }
 }
 
+func (j jsonOutput) PlannedFiles(tbl *table.Table, tasks []table.FileScanTask) 
{
+       type dataType struct {
+               Snapshot *table.Snapshot `json:"snapshot"`
+               Files    []plannedFile   `json:"files"`
+       }
+
+       data := dataType{
+               Snapshot: tbl.CurrentSnapshot(),
+               Files:    plannedDataFiles(tasks),
+       }
+       if err := json.NewEncoder(os.Stdout).Encode(data); err != nil {
+               j.Error(err)
+       }
+}
+
 func (j jsonOutput) DescribeProperties(props iceberg.Properties) {
        if err := json.NewEncoder(os.Stdout).Encode(props); err != nil {
                j.Error(err)
diff --git a/website/src/cli.md b/website/src/cli.md
index c5415e69e..ad8ee2986 100644
--- a/website/src/cli.md
+++ b/website/src/cli.md
@@ -175,6 +175,19 @@ iceberg partition-stats --snapshot-id 7234981023498 
my_db.events
 iceberg partition-stats --all my_db.events                    # all snapshots
 ```
 
+### `files` — data files
+
+```shell
+iceberg files my_db.events                              # current snapshot
+iceberg files --history my_db.events                    # all snapshots
+iceberg files --scan-planning-mode remote my_db.events  # from a server-side 
scan plan
+```
+By default `files` reads the manifest list and manifests itself and prints 
them as a tree: snapshot, manifest, then each manifest entry.
+
+With `--scan-planning-mode remote` or `auto`, or when a REST catalog sets 
`scan-planning-mode=server` for the table, `files` plans a scan of the current 
snapshot instead and lists the data files it returns, each with its delete 
files. A scan plan carries no manifests, so the tree has no manifest level, and 
it only shows live files. `--history` needs every snapshot's manifests and 
fails in this mode, as does `--scan-planning-mode local` on a `server` table.
+
+`compact` plans its input with a table scan, so it follows the catalog's 
`scan-planning-mode` as well. Every other command reads table metadata directly.
+
 ### `schema --show-defaults`
 
 ```shell

Reply via email to