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>
54 lines
2.1 KiB
Python
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}")
|