Repository navigation
feat(engine-contract): write capabilities, ensure_unique_key, conformance suite (contract 0.8.0) #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat(engine-contract): write capabilities, ensure_unique_key, conformance suite (contract 0.8.0) #75
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d991551
feat(engine-contract): ensure_unique_key and DuplicateKeyError
carolsimone ed03da6
feat(engine-contract): opt-in write capabilities and ensure_table pri…
carolsimone 4ee0330
feat(engine-contract): conformance suite for the write capabilities
carolsimone 6cd1808
chore(engine-contract): version 0.8.0 and pins
carolsimone 28b6402
fix(engine-contract): reject floating-point primary keys in ensure_un…
carolsimone eae4b8f
refactor(engine-contract): isolate reusable conformance tests
carolsimone File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| """The primary-key invariant every upsert relies on, checked once for every engine. | ||
|
|
||
| An upsert whose own rows repeat a key cannot be written consistently: DuckLake's | ||
| MERGE inserts both rows of a repeated new key and keeps the last row of a | ||
| repeated existing one, Trino fails only when two rows match one existing row, | ||
| and postgres's ON CONFLICT fails within one statement but keeps the last row | ||
| across statements. ``UpsertCapable.upsert`` therefore calls | ||
| :func:`ensure_unique_key` before any engine sees the rows. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Sequence | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: # pragma: no cover | ||
| import pyarrow # type: ignore[import-untyped] | ||
|
|
||
| # How many repeated keys the error message names before summarizing the rest. | ||
| _SHOWN_KEYS = 5 | ||
|
|
||
|
|
||
| class DuplicateKeyError(ValueError): | ||
| """The rows to upsert repeat a primary key; nothing was written.""" | ||
|
|
||
|
|
||
| def ensure_unique_key(data: "pyarrow.Table", primary_key: Sequence[str]) -> None: | ||
| """Raise :class:`DuplicateKeyError` if two rows of *data* share a *primary_key* value. | ||
|
|
||
| Groups the rows by the key columns. NULL counts as a value here; the | ||
| contract requires every key column to be ``nullable: false`` and the | ||
| harness's ``conform()`` rejects NULLs in such columns before this runs. | ||
| The message names the first repeated keys, in order of first appearance, | ||
| with their row counts. Only methods of *data* are called, so this module | ||
| needs no pyarrow import. | ||
|
|
||
| Raises: | ||
| ValueError: If *primary_key* is empty, names a column *data* lacks, or | ||
| includes a floating-point column (a warehouse treats ``0.0`` and | ||
| ``-0.0`` as one key, which this check cannot see). | ||
| DuplicateKeyError: If a key value appears in more than one row. | ||
| """ | ||
| key = list(primary_key) | ||
| if not key: | ||
| raise ValueError("primary_key must name at least one column") | ||
| missing = [name for name in key if name not in data.column_names] | ||
| if missing: | ||
| raise ValueError( | ||
| f"primary key column(s) {missing} are not in the data's columns {data.column_names}" | ||
| ) | ||
| floating = [ | ||
| name | ||
| for name in key | ||
| if str(data.schema.field(name).type) in {"halffloat", "float", "double"} | ||
| ] | ||
| if floating: | ||
| raise ValueError( | ||
| f"primary key column(s) {floating} are floating-point; floating-point " | ||
| "columns cannot be a primary key because the warehouse treats values like " | ||
| "0.0 and -0.0 as equal while this check would see them as distinct" | ||
| ) | ||
| if data.num_rows < 2: | ||
| return | ||
| # Positional names: a key column called "count_all" must not collide with | ||
| # the aggregate's own output column. | ||
| positional = [f"k{index}" for index in range(len(key))] | ||
| counts = ( | ||
| data.select(key) | ||
| .rename_columns(positional) | ||
| .group_by(positional, use_threads=False) | ||
| .aggregate([([], "count_all")]) | ||
| ) | ||
| if counts.num_rows == data.num_rows: | ||
| return | ||
| repeated = [row for row in counts.to_pylist() if row["count_all"] > 1] | ||
| shown = "; ".join( | ||
| f"({', '.join(str(row[name]) for name in positional)}) appears {row['count_all']} times" | ||
| for row in repeated[:_SHOWN_KEYS] | ||
| ) | ||
| more = ( | ||
| f"; and {len(repeated) - _SHOWN_KEYS} more repeated key(s)" | ||
| if len(repeated) > _SHOWN_KEYS | ||
| else "" | ||
| ) | ||
| raise DuplicateKeyError( | ||
| f"primary key ({', '.join(key)}) is not unique in the rows to write: " | ||
| f"{shown}{more}; nothing was written" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| """Reusable test support for warehouse adapter authors. | ||
|
|
||
| Install ``continuo-engine-contract[testing]`` to use the conformance suite. | ||
| """ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High · bug: signed-zero keys bypass the uniqueness check
Arrow groups
0.0and-0.0separately. Consequentlyensure_unique_key(pa.table({'id': [0.0, -0.0]}), ['id'])returns successfully even though the warehouse compares these keys equal.This input also passes
conform()for a non-nullableDOUBLE PRECISIONkey. In an in-memory DuckDB reproduction, aMERGEinto an empty table inserted both rows, and SQLGROUP BY idreported one key with count 2. The template method therefore allows duplicate warehouse keys to reach_upsert, violating the invariant this guard is meant to enforce.Normalize floating key equality (including signed zero) before grouping, or reject unsupported floating key types, and add a regression test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in b47e4da.
ensure_unique_keynow rejects floating-point key columns (halffloat/float/double) with aValueErrorbefore grouping. Normalizing signed zero would needpyarrow.compute, but the contract package keeps pyarrow out of its runtime imports (sqlglot-only), so rejection is the right option — andensure_unique_keyis the single enforcement point theUpsertCapable.upserttemplate runs for every engine. Regression tests cover 0.0/-0.0, float32, a composite key with one float column, and that decimal128 keys still pass.