Skip to content

feat(vortex): write DuckLake data files in the vortex format (INSERT/CTAS) - #2

Merged
moshap-firebolt merged 4 commits into
mainfrom
firebolt/vortex-write
Aug 5, 2026
Merged

moshap-firebolt merged 4 commits into
mainfrom
firebolt/vortex-write

Conversation

@moshap-firebolt

@moshap-firebolt moshap-firebolt commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #1 (vortex reads). Completes the round trip: INSERT / CREATE TABLE AS into a table with data_file_format='vortex' now writes real .vortex files and reads them back.

What changed

  • GetCopyOptions: non-parquet formats use the CHANGED_ROWS_AND_FILE_LIST copy return type (vortex's C-API COPY can't report per-file statistics) and a single explicit output file path (vortex doesn't implement rotate_next_file, so DuckDB's directory+rotation naming would hand it the table directory instead of a file).
  • AddWrittenFiles: parse {count, files[]} for non-parquet writes and fstat the file for its size; the recorded format is resolved once on DuckLakeInsertGlobalState so INSERT, CTAS, flush and compaction all record it.
  • metadata: guard the column-stats INSERT/UPDATE and the global-stats readback for files that carry no column statistics, and keep such files through filter pushdown instead of pruning them (no min/max to prune on). All no-ops for parquet, which always has stats.

Test — test/sql/vortex/vortex_write.test (49 assertions): INSERT + CTAS round trip incl. types, NULLs, nested lists, filter pushdown, deletes and updates.

Verified — vortex_write 49/49; full DuckLake sql suite 91550/91551. The single failure is an unrelated ducklake_max_retry_count RESET-default artifact of the DuckDB pin (no settings code in this diff).

CI — excludes windows arches (DuckDB v1.5.1 vendored fmt vs current MSVC) and makes the debug build manual-only (from-scratch debug build exceeds the runner time limit on this fork).

🤖 Generated with Claude Code


Note

Cursor Bugbot is generating a summary for commit 9f7762b. Configure here.

…CTAS)

Completes the vortex round trip: INSERT/CTAS into a table whose data_file_format
is 'vortex' now writes real .vortex files and reads them back.

- GetCopyOptions: non-parquet formats use the CHANGED_ROWS_AND_FILE_LIST copy
  return type (vortex's C-API COPY can't report per-file statistics) and a
  single explicit output file path (vortex doesn't implement rotate_next_file,
  so DuckDB's directory+rotation naming would hand it the table dir, not a file)
- AddWrittenFiles: parse {count, files[]} for non-parquet writes and fstat the
  file for its size; resolve the recorded format once on DuckLakeInsertGlobalState
  so INSERT, CTAS, flush and compaction all record it
- metadata: guard the column-stats INSERT/UPDATE and global-stats readback for
  files that carry no column statistics (statless formats); keep such files
  through filter pushdown instead of pruning them (they have no min/max to prune
  on). These are no-ops for parquet, which always has stats.
- test/sql/vortex/vortex_write.test: INSERT + CTAS round trip incl. types,
  NULLs, nested lists, filter pushdown, deletes and updates (49 assertions)
- CI: exclude windows arches (DuckDB v1.5.1 vendored fmt vs current MSVC) and
  make the debug build manual-only (from-scratch debug build exceeds the runner
  time limit on this fork)

Verified: vortex_write 49, full DuckLake sql suite 91550/91551 (the one failure
is an unrelated ducklake_max_retry_count RESET-default artifact of the DuckDB pin,
not touched by this change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/storage/ducklake_insert.cpp
Comment thread src/storage/ducklake_insert.cpp
Comment thread src/storage/ducklake_insert.cpp
Bugbot on #2 flagged three cases where a non-parquet (statless) write breaks a
DuckLake assumption that files carry column statistics. Reject them with clear
errors instead of crashing or silently corrupting:

- partitioned writes: need one file per partition value + recorded
  partition_values, which the single-file non-parquet path cannot provide
- flushing inlined data: derives begin_snapshot / row_id_start from the written
  file's snapshot_id / row_id column stats, absent for non-parquet
- NOT NULL columns: enforced by inspecting written null-count stats, absent for
  non-parquet, so nulls could otherwise slip into a NOT NULL column

Extends vortex_write.test to cover the three rejections (58 assertions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/storage/ducklake_insert.cpp Outdated
Addresses the SQLite/Postgres CI failure and the orphan-file review comment.

- filter pushdown: the previous statless-file fix referenced the stats CTE
  twice (IN ... OR NOT IN ...) while it stayed NOT MATERIALIZED, which
  double-evaluated the subquery and broke the sqlite/postgres metadata backends
  ("Attempted to access index 0 within vector of size 0"). Instead, build the
  CTE as a LEFT JOIN from ducklake_data_file to its column stats: files with no
  stats row appear with NULL stats and are kept by the existing null checks,
  with a single CTE reference. Works on all backends.

- move the flush and NOT NULL write guards from AddWrittenFiles (which runs
  after PhysicalCopyToFile has already written the file, leaving an orphan on
  disk) into GetCopyOptions, alongside the partition guard, so unsupported
  non-parquet writes fail before anything is written. NOT NULL is detected via
  a new DuckLakeCopyInput.has_not_null_columns flag; flush via the
  WRITE_ROW_ID_AND_SNAPSHOT_ID virtual-column marker.

Verified: vortex_write 58, full DuckLake sql suite 91559/91560 (only the
unrelated max_retry_count RESET artifact), rejected inserts leave no orphan
.vortex files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b5310ed. Configure here.

Comment thread src/storage/ducklake_insert.cpp
…deploy

Bugbot on #2 noted the flush guard also blocked compaction: both flush and
merge_adjacent_files set WRITE_ROW_ID_AND_SNAPSHOT_ID, so keying the guard on
virtual_columns rejected vortex compaction with a misleading flush error.

- distinguish the two with explicit DuckLakeCopyInput flags (is_flush /
  is_compaction) set by their respective callers, instead of virtual_columns
- flush stays rejected (needs begin_snapshot / row_id_start recovered from the
  written file's stats). Compaction is also rejected for now, but with its own
  message: its directory+rotation output model does not compose with the
  single-file path used for non-parquet writes (it otherwise produced a
  malformed <file>.vortex/<file>.parquet path). Both fail in GetCopyOptions,
  before any write, so no orphan files.
- CI: exclude windows from the deploy matrix too (it is already excluded from
  the build), fixing the failed nightly-deploy job.

vortex_write.test covers the compaction rejection (62 assertions). Parquet
compaction unaffected (compaction suite 1844 assertions pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@moshap-firebolt
moshap-firebolt merged commit 4b385ae into main Aug 5, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant