pipekit/pipekit/engine/merge.py
Paul Trowbridge 3562dfa581 merge: quote merge-key identifiers with dest dialect
Incremental merge interpolated merge-key columns raw, so a key with a
character illegal in a bare identifier (e.g. dcord#) produced
WHERE dcord# IN (...) and Postgres errored at "IN". build_merge_sql now
takes the dest driver and quotes each key via quote_identifier (clean
names stay bare, the rest wrap in the dialect's quotes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:57:58 -04:00

54 lines
2.1 KiB
Python

"""Build the SQL that merges staging → dest for one module.
Three strategies (from SPEC.md §"Merge strategies"):
* ``full`` TRUNCATE dest; INSERT from staging
* ``incremental`` DELETE rows in dest matching merge_key, then INSERT
* ``append`` INSERT only
Generated SQL targets PostgreSQL — the 95% destination in the user's
setup. Moving this into a dest-driver method is a one-line refactor when
a non-PG destination appears.
"""
from __future__ import annotations
class MergeError(ValueError):
pass
def build_merge_sql(*, strategy: str, dest_table: str, staging_table: str,
merge_key: str | None, dest_drv=None) -> str:
# Quote merge-key identifiers with the dest dialect. Keys are user-entered
# and may hold characters illegal in a bare identifier (e.g. '#', spaces,
# mixed case) — PG's quote_identifier leaves clean names bare and wraps the
# rest in "…". Falls back to raw when no driver is supplied.
quote = dest_drv.quote_identifier if dest_drv else (lambda s: s)
if strategy == "full":
return f"TRUNCATE TABLE {dest_table};\nINSERT INTO {dest_table} SELECT * FROM {staging_table};"
if strategy == "append":
return f"INSERT INTO {dest_table} SELECT * FROM {staging_table};"
if strategy == "incremental":
if not merge_key:
raise MergeError("incremental merge requires merge_key")
keys = [quote(k.strip()) for k in merge_key.split(",") if k.strip()]
if not keys:
raise MergeError(f"merge_key is empty after parsing: {merge_key!r}")
if len(keys) == 1:
k = keys[0]
delete = (f"DELETE FROM {dest_table} "
f"WHERE {k} IN (SELECT {k} FROM {staging_table});")
else:
tuple_cols = "(" + ", ".join(keys) + ")"
select_cols = ", ".join(keys)
delete = (f"DELETE FROM {dest_table} "
f"WHERE {tuple_cols} IN (SELECT {select_cols} FROM {staging_table});")
insert = f"INSERT INTO {dest_table} SELECT * FROM {staging_table};"
return delete + "\n" + insert
raise MergeError(f"unknown merge strategy: {strategy!r}")