diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e2d13..8e66a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Contracts may set `mode_type` — `replace` (the default), `append` or `upsert` — and, for `upsert` only, `primary_key`: a non-empty list of output columns, each spelled exactly as in `output_columns` and declared - `nullable: false`, names unique case-insensitively. + `nullable: false`, names unique case-insensitively, and none a + floating-point (`DOUBLE PRECISION`) column — the warehouse collapses `0.0` + and `-0.0`, so a float key is rejected at validation rather than failing at + write time. `python-csv` nodes allow only `replace`. `continuo-runtime validate`, `merge` and `run` enforce these rules; the harness's write path for the new modes follows in a later entry. diff --git a/continuo_python_runtime/contract/loader.py b/continuo_python_runtime/contract/loader.py index c5f8927..b37fd6b 100644 --- a/continuo_python_runtime/contract/loader.py +++ b/continuo_python_runtime/contract/loader.py @@ -155,7 +155,10 @@ def _validate_primary_key( output column (postgres quotes identifiers, so ``Rate_Date`` is not ``rate_date``) declared ``nullable: false``, because a NULL key never matches an existing row. Repeats are refused case-insensitively too, so a - key cannot name two columns that differ only in case. + key cannot name two columns that differ only in case. A floating-point + column is refused as well: the warehouse treats ``0.0`` and ``-0.0`` as + one key, so ``ensure_unique_key`` would reject the rows at write time -- + failing here instead keeps an unwritable upsert out of the contract. """ if mode_type != "upsert": if "primary_key" in raw: @@ -194,6 +197,11 @@ def _validate_primary_key( raise ContractError( f"{label}: 'primary_key' column {name!r} must be declared nullable: false" ) + if parse_sql_type(column.type).base == "DOUBLE_PRECISION": + raise ContractError( + f"{label}: 'primary_key' column {name!r} is floating-point " + f"({column.type}); a floating-point column cannot be a primary key" + ) return tuple(raw_key) diff --git a/tests/contract/test_loader.py b/tests/contract/test_loader.py index 94475ef..675461f 100644 --- a/tests/contract/test_loader.py +++ b/tests/contract/test_loader.py @@ -853,6 +853,16 @@ def test_primary_key_column_must_be_not_null(): parse_node(_upsert_entry(primary_key=["rate_date", "rate"]), "c.yml") +def test_primary_key_column_cannot_be_floating_point(): + """ensure_unique_key rejects float keys at write time; refuse them at validation.""" + entry = _upsert_entry(primary_key=["rate_date", "score"]) + entry["output_columns"].append({"name": "score", "type": "DOUBLE PRECISION", "nullable": False}) + with pytest.raises( + ContractError, match="'primary_key' column 'score' is floating-point" + ): + parse_node(entry, "c.yml") + + def test_primary_key_names_must_match_output_columns_exactly(): """Review Focus 2: postgres quotes identifiers, so Rate_Date is not rate_date.""" with pytest.raises(