Repository navigation
feat(duckdb): append and upsert - #77
Conversation
fdd7a69 to
efbe27b
Compare
The write-modes shape check compares nullability; duckdb_columns() and information_schema both report the NOT NULL that ensure_table declared on the stack's DuckLake (postgres catalog, MinIO data). Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
LakeWarehouse declares TableIntrospection. The new LakeGateway.columns reads duckdb_columns() with the same pre-filter-then-confirm identifier matching as table_exists, and table_columns raises LookupError for a missing table. The conformance suite runs against the DuckLake stack from here on. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
LakeWarehouse declares AppendCapable: insert_arrow inside one gateway transaction, nothing for zero rows. Passes the append conformance cases on the DuckLake stack. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
DdlRenderer.merge_from renders MERGE INTO the target on the key, overwriting the other columns of a matched row and inserting the rest, with no WHEN MATCHED branch when every column is in the key. LakeGateway.merge_arrow runs it from a registered view that is always unregistered; errors stay redacted. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
LakeWarehouse declares UpsertCapable: a MERGE from the registered Arrow data inside one gateway transaction, nothing for zero rows. ensure_table accepts the key and ignores it because DuckLake has no constraints. Passes the upsert conformance cases on the DuckLake stack. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
The engine-contract conformance suite moved to continuo_engine_contract.testing.conformance in #75; the duckdb integration test still imported the pre-move path and failed collection once #75 merged to main. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
52344d8 to
0497fc4
Compare
| from continuo_engine_contract.port import LiveColumn # type: ignore[import-untyped] | ||
|
|
||
| from ..application.ports import LakeConflictError, LakeGateway | ||
| from ..domain.columns import ColumnDefinition |
There was a problem hiding this comment.
we don't use relative import, only absolutes. Let's avoid this inconsistencies.
There was a problem hiding this comment.
LiveColumn is a cross-package import — it comes from continuo_engine_contract, a separate distribution — so it can't be relative; it's absolute for the same reason the postgres adapter writes from continuo_engine_contract.port import LiveColumn. The relative imports in this file (from ..application..., from ..domain...) are intra-package and match the whole duckdb adapter, which uses relative imports throughout its layers (application/domain/infrastructure, ~9 files); the test suite imports the package absolutely.
So there isn't a one-line fix on this line: either the file is already consistent (cross-package absolute, intra-package relative), or you want the adapter's intra-package imports converted to absolute package-wide — a change to shipped 0.8.0 code that I'd land as its own commit/PR rather than fold into the write-modes feature. Which did you mean?
There was a problem hiding this comment.
Why mixing up relative and absolute in a single repository? What's easier to understand a standardised codebase or something that mix up things all the time?
There was a problem hiding this comment.
Agreed — standardised, not mixed. Done in 0525ffd: converted all 27 relative intra-package imports in the duckdb adapter to absolute (from continuo_duckdb_adapter...). It was the only package in the repo using relative imports; the runtime (52), the engine contract (3) and the postgres/trino adapters all use absolute, so the adapter now matches. No behaviour change; mypy, ruff and the duckdb suite (264) pass.
The cross-package LiveColumn line stays absolute as before — it has to be.
carolsimone
left a comment
There was a problem hiding this comment.
AI Code Review (OCR delegation mode)
Scope: main..HEAD (merge-base 722f4f3, 10 commits). 53 files changed; 21 reviewable after exclusions (docs, Dockerfiles, requirements pins, and test paths excluded by policy).
Coverage: 21/21 reviewable files examined (100%). No files skipped.
Review rules applied: correctness, security, error handling, concurrency, resource management, boundary cases, performance, maintainability (per ocr delegate rule checklists for Python sources and config files).
Findings
No High, Medium, or Low findings to report. Points specifically verified:
- SQL construction —
DdlRenderer.merge_fromroutes every identifier throughquote_identifier(double-quote escaped) and the MERGE source is a UUID-named registered view; postgres_insert_rows/_on_conflictusepsycopg2.sql.Identifierand correctly escape literal%forexecute_values' template reparse, including inEXCLUDED."pct%"(covered by the awkward-identifiers conformance case). - Key invariant centralization —
ensure_unique_keyinkeys.pyruns in theUpsertCapable.upserttemplate method before any engine sees rows; empty/missing/float keys rejected, positionalk{n}renames avoid collision with thecount_allaggregate, and the pre-group_byNULL concern is moot since the contract requires key columnsnullable: falseandconform()rejects NULLs upstream. - Contract validation —
_validate_primary_keyenforces upsert-only keys, exact column spelling (correct for postgres's quoted identifiers), case-insensitive duplicate rejection, andnullable: falseon key columns. Wire entries inmerge.pyonly carrymode_type/primary_keywhen non-default, keeping hashes andcontract_version = 1stable for replace nodes. - Transaction discipline — postgres
_transactioncontextmanager preserves the prior BEGIN/COMMIT/ROLLBACK shape (rollback failure only logged, original error re-raised); duckdbappend/_upsertskip 0-row writes so DuckLake records no empty snapshot. - Capability model —
TableIntrospection/AppendCapable/UpsertCapableare opt-in ABCs; both adapters implement all abstract members of the capabilities they declare. Trino (bumped to contract 0.8.0 without new capabilities) is unaffected:ensure_table's newprimary_keyparameter defaults to(), and the harness currently rejects non-replace modes before any adapter call. - Harness interim guard — non-replace modes raise
LoadErrorbefore the script runs or anything is written; cannot silently replace a table meant for append/upsert.
Verification
tests(excl. integration): 650 passed. 3 failures intests/test_image_smoke_validation.pyare environment-only — they shell out todocker runfor images not present locally (repository does not exist/ TLS handshake timeout); unrelated to this diff.contract/tests: 163 passed.adapters/duckdb/tests(excl. integration): 263 passed.- Real-engine integration suites (postgres/duckdb in Docker) not run locally; per
CLAUDE.mdthey should pass in CI before merge.
AI Code Review — Low-severity follow-ups (non-blocking)Posted at the author's request; these were below the reporting threshold of the main review. Nothing here blocks merge.
|
An empty key renders `ON ()`, invalid SQL. UpsertCapable.upsert already rejects an empty key through ensure_unique_key before any engine is called, so this is unreachable in the port flow; the guard makes the renderer self-defending if it is ever called directly. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
|
Dispositions: 2 — 1 — 3 — 4 — introspection matches views — deferring. The runtime creates tables, never views in the output schema, so a view colliding with the table name isn't something it produces. It also spans both duckdb ( 5 — |
The duckdb adapter was the only package in the repo using relative intra-package imports (27 of them). The runtime, the engine contract and the postgres/trino adapters all use absolute imports. Standardise the duckdb adapter to match: every `from .` / `from ..` becomes `from continuo_duckdb_adapter...`. No behaviour change; imports resolve identically. Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw
The three items noted in #77/#78 review threads as merged-code cleanups: - loader.py: quote mode_type and kind (repr) in the "not allowed for kind" error, matching the sibling "must be one of" error; test updated. - postgres adapter.py: ensure_table docstring port-signature ref 0.6.0 -> 0.8.0. - keys.py: ensure_unique_key docstring no longer claims first-appearance order for the repeated-key message; pyarrow grouping order is not contractual. Claude-Session: https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Fourth runtime PR of the write-modes series (spec R4). Depends on the engine-port PR (#75, contract 0.8.0); stacked on #76 (Postgres) locally — base is
feat/write-modes-postgres; retarget tomainas the stack merges.NOT NULLthroughduckdb_columns()andinformation_schema; pinned by a probe test.LakeWarehouse(WarehouseAdapter, AppendCapable, UpsertCapable):table_columnsthrough the newLakeGateway.columns(),append(insert_arrowin one transaction),_upsert(MERGE INTOfrom the registered Arrow view in one transaction; noWHEN MATCHEDwhen every column is in the key). Zero rows open no transaction.ensure_table(..., primary_key=...)is accepted and ignored: DuckLake rejects PRIMARY KEY/UNIQUE.🤖 Generated with Claude Code
https://claude.ai/code/session_0137E9XmyUXhwPTRrtNfivDw