Files
udpak_semistruktur/udpak_semistruktur/ddl.py
2026-04-04 20:45:35 +02:00

741 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ddl_tool_ase.py
Sybase ASE helper for:
- DDL generation (CREATE TABLE) for base + tmp tables
- Flyt scripts (DELETE with JOIN placeholder + INSERT/SELECT)
- Collecting combined sql files
Design goals (as requested):
- YAML may contain _tmp table names (because the extractor loads into _tmp)
- DDL generation should produce BOTH:
- base table (without _tmp)
- tmp table (with _tmp)
when YAML ends with _tmp
- If YAML does NOT end with _tmp:
- always generate base DDL
- generate tmp DDL only when --tmp is given
- Flyt scripts always move tmp -> base
- Sybase ASE DELETE must NOT use table aliases
- No join_cols intelligence; only placeholders (ON 1=1 + commented AND lines)
"""
from __future__ import annotations
import os
from copy import deepcopy
from typing import Dict, List, Tuple, Any
from udpak_semistruktur.utils import generer_filnavn
from udpak_semistruktur.logger import hent_logger
logger = hent_logger(__name__)
def _byg_staging_kolonner(file_conf: dict) -> list:
"""
Returnerer kolonner til staging-tabellen.
Det er præcis kolonnerne fra YAML ingen ekstra.
"""
return file_conf.get("kolonner", [])
def _byg_blivende_kolonner(file_conf: dict) -> list:
"""
Returnerer kolonner til den blivende tabel.
Sammensætter: start-ekstra + yaml-kolonner + slut-ekstra.
"""
tabel_cfg = file_conf.get("tabel", {})
ekstra = tabel_cfg.get("ekstra_kolonner", [])
start_kolonner = [k for k in ekstra if k.get("placering") == "start"]
slut_kolonner = [k for k in ekstra if k.get("placering") == "slut"]
yaml_kolonner = file_conf.get("kolonner", [])
return start_kolonner + yaml_kolonner + slut_kolonner
def _map_ekstra_kolonne_til_sql(ekstra_kol: dict) -> str:
"""
Konverterer en ekstra-kolonne til en SQL-kolonnelinje.
Tre mulige former:
1) Beregnet kolonne: "navn" ddl_type AS <udtryk>
bruges automatisk når default indeholder case, select eller convert
2) Med default: "navn" ddl_type DEFAULT <udtryk> NULL
3) Simpel: "navn" ddl_type NULL
Identity-kolonner sættes altid til NOT NULL.
"""
navn = ekstra_kol["navn"]
ddl_type = ekstra_kol.get("ddl_type", "VARCHAR(50)")
default = ekstra_kol.get("default")
påkrævet = ekstra_kol.get("påkrævet", False)
# Identity-kolonner er altid NOT NULL
er_identity = "identity" in ddl_type.lower()
not_null = "NOT NULL" if (påkrævet or er_identity) else "NULL"
if default is not None:
# Beregnet kolonne hvis default indeholder udtryk der kræver AS-syntaks
beregnet_nøgleord = ("case ", "select ", "convert(", "(select ")
er_beregnet = any(kw in default.lower() for kw in beregnet_nøgleord)
if er_beregnet:
return f' "{navn}" {ddl_type} AS {default}'
return f' "{navn}" {ddl_type} DEFAULT {default} {not_null}'
return f' "{navn}" {ddl_type} {not_null}'
# ------------------------------------------------------------
# CLI wiring
# ------------------------------------------------------------
def add_cli_args(parser) -> None:
"""
Add CLI flags used by DDL mode to your argparse parser.
"""
parser.add_argument("--DDL", action="store_true", help="Generate DDL (CREATE TABLE) files")
parser.add_argument("--tmp", action="store_true", help="Also generate _tmp table variants when YAML table is base")
parser.add_argument("--flyt", action="store_true", help="Also generate delete+insert (move) scripts for Sybase ASE")
parser.add_argument("--flyt_kort", action="store_true", help="Generere delete+insert statements, men i kort version.")
def is_enabled(args) -> bool:
return bool(getattr(args, "DDL", False))
# ------------------------------------------------------------
# Name helpers
# ------------------------------------------------------------
def _map_yaml_type_to_ase(col: dict, dato_ud_global = "%Y-%m-%d") -> str:
"""
Map YAML kolonnefelt til Sybase ASE SQL-type.
YAML keys vi kigger efter:
- type: string|integer|float|decimal|boolean|date|hash|id|file
- max_længde / length: int (til varchar)
- precision: int (til decimal)
- decimaler: int (scale til decimal)
Fallback for ukendt/uden type: VARCHAR(255)
"""
# Ekstra-kolonner har ddl_type direkte returner den rå type
if "ddl_type" in col:
return col["ddl_type"]
t = str(col.get("type", "string")).lower()
length = col.get("max_længde", col.get("length", col.get("truncate")))
precision = col.get("precision", 18)
scale = col.get("decimaler", 2)
dato_fmt = col.get("dato_ud", dato_ud_global)
if t in ("string", "hash", "id", "file"):
n = int(length) if length else 50
return f"VARCHAR({n})"
if t in ("integer", "bigint"):
return "INT"
if t in ("float", "decimal"):
return f"DECIMAL({precision},{scale})"
if t == "boolean":
# BIT findes, men TINYINT er ofte mere kompatibelt i ASE
return "TINYINT"
if t == "date":
time_codes = ['%H', '%M', '%S', '%I', '%p', '%f']
if (any(code in dato_fmt for code in time_codes)) or (dato_fmt.upper() == 'SYBASE'):
return "DATETIME"
return "DATE"
# default fallback
return "VARCHAR(50)"
def split_base_tmp(table_name: str) -> Tuple[str, str]:
"""
Returns (base_table, tmp_table).
If table_name ends with _tmp:
base_table = table_name without _tmp
tmp_table = table_name (as-is)
Else:
base_table = table_name
tmp_table = table_name + _tmp
"""
if table_name.lower().endswith("_tmp"):
return table_name[:-4], table_name
return table_name, f"{table_name}_tmp"
def _safe_name(name: str) -> str:
return name.replace(".", "_")
def _default_ddl_filename(table_name: str) -> str:
return f"{_safe_name(table_name)}_create.sql"
def _default_delete_filename(base_table: str) -> str:
return f"{_safe_name(base_table)}_delete.sql"
def _default_flyt_filename(base_table: str) -> str:
return f"{_safe_name(base_table)}_flyt.sql"
def _skriv_flyt_scripts(
tabel: str,
base_tabel: str,
tmp_tabel: str,
file_conf: dict,
outdir: str,
insert_func: callable,
samlet_flyt_indhold: list,
) -> None:
"""Genererer og skriver delete- og flyt-scripts for én tabel."""
kolonner = [kol["navn"] for kol in file_conf.get("kolonner", [])]
delete_sql = generate_delete_join_sql(tabel, kolonner)
flyt_sql = insert_func(tabel, kolonner)
delete_path = os.path.join(outdir, _default_delete_filename(base_tabel))
flyt_path = os.path.join(outdir, _default_flyt_filename(base_tabel))
with open(delete_path, "w", encoding="utf-8") as f:
f.write(delete_sql)
with open(flyt_path, "w", encoding="utf-8") as f:
f.write(flyt_sql)
samlet_flyt_indhold.append(f"-- DELETE: {base_tabel} (match mod {tmp_tabel})\n{delete_sql}\n")
samlet_flyt_indhold.append(f"-- FLYT: {tmp_tabel} -> {base_tabel}\n{flyt_sql}\n")
logger.info(f"[FLYT] Skrev {delete_path}")
logger.info(f"[FLYT] Skrev {flyt_path}")
# ------------------------------------------------------------
# SQL generators (Sybase ASE)
# ------------------------------------------------------------
def generate_create_table_sql(file_config: dict, global_config: dict) -> str:
"""
Genererer CREATE TABLE DDL for én output-fil-konfiguration.
Kræver at file_config['tabel_navn'] er sat.
- Kolonnenavn = kol["navn"]
- Type = YAML 'type' -> ASE type (fallback VARCHAR(255))
- NULL/NOT NULL = 'påkrævet' (True => NOT NULL)
- Optional 'primary_key': true (på en eller flere kolonner) -> PRIMARY KEY
Returnerer en streng med DROP-IF-EXISTS + CREATE TABLE.
"""
table = file_config.get("tabel_navn")
if not table:
raise ValueError("Kan ikke generere DDL: 'tabel_navn' mangler i output_filer-blok.")
cols = file_config.get("kolonner", [])
if not cols:
raise ValueError(f"Kan ikke generere DDL for {table}: 'kolonner' er tom.")
# Byg kolonne-linjer
col_lines = []
pk_cols = []
for col in cols:
name = col["navn"]
sql_type = _map_yaml_type_to_ase(col)
not_null = "NOT NULL" if col.get("påkrævet") else "NULL"
col_lines.append(f' "{name}" {sql_type} {not_null}')
if col.get("primary_key"):
pk_cols.append(name)
# PRIMARY KEY (hvis angivet)
pk_line = ""
if pk_cols:
cols_list = ", ".join(f'"{c}"' for c in pk_cols)
pk_line = f",\n PRIMARY KEY ({cols_list})"
cols_block = ",\n".join(col_lines) + pk_line
# DROP IF EXISTS til ASE (sysobjects)
# (Tilpas evt. schema-adskillelse; her antager vi at table kan være dbo.MinTabel)
schema_qualified = table
table_only = table.split(".")[-1]
drop_part = (
f"IF EXISTS (SELECT 1 FROM sysobjects WHERE name = '{table_only}' AND type = 'U')\n"
f"BEGIN\n"
f" DROP TABLE {schema_qualified}\n"
f"END\nGO\n\n"
)
create_part = (
f"CREATE TABLE {schema_qualified} (\n"
f"{cols_block}\n"
f");\nGO\n"
)
return drop_part + create_part
def generate_delete_join_sql(table_name_from_yaml: str, columns: List[str]) -> str:
"""
Sybase ASE-compatible DELETE with JOIN placeholder.
IMPORTANT: No aliases in DELETE in Sybase ASE.
Output:
DELETE FROM base
FROM base
JOIN tmp
ON 1 = 1
-- AND base.col = tmp.col
"""
base_table, tmp_table = split_base_tmp(table_name_from_yaml)
lines: List[str] = []
lines.append(f"DELETE FROM {base_table}")
lines.append(f"FROM {base_table}")
lines.append(f"JOIN {tmp_table}")
lines.append(" ON 1 = 1")
for col in columns:
lines.append(f" -- AND {base_table}.{col} = {tmp_table}.{col}")
lines.append(" ")
return "\n".join(lines)
def generate_insert_move_sql(table_name_from_yaml: str, columns: List[str]) -> str:
"""
INSERT INTO base SELECT FROM tmp (all columns, explicit list).
"""
base_table, tmp_table = split_base_tmp(table_name_from_yaml)
cols_block = ",\n ".join(columns)
lines: List[str] = []
lines.append(f"INSERT INTO {base_table} (")
lines.append(f" {cols_block}")
lines.append(")")
lines.append("SELECT")
lines.append(f" {cols_block}")
lines.append(f"FROM {tmp_table}")
lines.append(" ")
return "\n".join(lines)
def generate_insert_move_sql_short(table_name_from_yaml: str, columns: List[str]) -> str:
"""
INSERT INTO base SELECT FROM tmp (*).
"""
base_table, tmp_table = split_base_tmp(table_name_from_yaml)
cols_block = ",\n ".join(columns)
lines: List[str] = []
lines.append(f"INSERT INTO {base_table} ")
lines.append("SELECT * ")
lines.append(f"FROM {tmp_table}")
lines.append(" ")
return "\n".join(lines)
# ------------------------------------------------------------
# DDL mode runner
# ------------------------------------------------------------
def run_ddl_mode(args, config: Dict[str, Any], global_config: Dict[str, Any]) -> None:
"""
Executes the full DDL-only flow and writes files.
To flows:
- Nyt flow: file_conf har 'tabel'-sektion med staging/blivende/historik
- Gammelt flow: file_conf har kun 'tabel_navn' (bagudkompatibelt)
"""
outdir = os.path.join(global_config["output_path"], "sql")
os.makedirs(outdir, exist_ok=True)
antal = 0
samlet_sql_indhold: List[str] = []
samlet_flyt_indhold: List[str] = []
output_filer = config.get("output_filer", [])
for file_conf in output_filer:
tabel_cfg = file_conf.get("tabel", {})
har_nyt_flow = bool(tabel_cfg.get("staging") or tabel_cfg.get("blivende"))
tabel = file_conf.get("tabel_navn")
# Spring over hvis hverken nyt eller gammelt flow har tabelinfo
if not har_nyt_flow and not tabel:
continue
try:
# =============================================================
# NYT FLOW tabel-sektion med staging/blivende/historik
# =============================================================
if har_nyt_flow:
staging = tabel_cfg.get("staging")
blivende = tabel_cfg.get("blivende")
historik = tabel_cfg.get("historik")
# 1) Staging DDL
if staging:
sql = generate_create_staging_sql(file_conf, global_config)
sti = os.path.join(outdir, _default_ddl_filename(staging))
with open(sti, "w", encoding="utf-8") as f:
f.write(sql)
samlet_sql_indhold.append(f"-- Staging: {staging}\n{sql}\n")
logger.info(f"[DDL] Skrev staging: {sti}")
antal += 1
# 2) Blivende DDL
if blivende:
sql = generate_create_blivende_sql(file_conf, global_config)
sti = os.path.join(outdir, _default_ddl_filename(blivende))
with open(sti, "w", encoding="utf-8") as f:
f.write(sql)
samlet_sql_indhold.append(f"-- Blivende: {blivende}\n{sql}\n")
logger.info(f"[DDL] Skrev blivende: {sti}")
antal += 1
# 3) Indexes
if blivende:
ix_sql = generate_indexes_sql(file_conf)
if ix_sql:
ix_sti = os.path.join(outdir, f"{_safe_name(blivende)}_indexes.sql")
with open(ix_sti, "w", encoding="utf-8") as f:
f.write(ix_sql)
samlet_sql_indhold.append(f"-- Indexes: {blivende}\n{ix_sql}\n")
logger.info(f"[DDL] Skrev indexes: {ix_sti}")
# 4) Flyt-scripts
if getattr(args, "flyt", False) or getattr(args, "flyt_kort", False):
if historik == "t2":
flyt_sql = generate_t2_flyt_sql(file_conf)
elif historik == "t1":
flyt_sql = generate_t1_flyt_sql(file_conf)
else:
flyt_sql = generate_t1_flyt_sql(file_conf) # default t1
base_navn = blivende or staging
flyt_sti = os.path.join(outdir, _default_flyt_filename(base_navn))
with open(flyt_sti, "w", encoding="utf-8") as f:
f.write(flyt_sql)
samlet_flyt_indhold.append(f"-- FLYT: {staging} -> {blivende}\n{flyt_sql}\n")
logger.info(f"[DDL] Skrev flyt: {flyt_sti}")
# =============================================================
# GAMMELT FLOW kun tabel_navn (bagudkompatibelt)
# =============================================================
else:
base_tabel, tmp_tabel = split_base_tmp(tabel)
yaml_is_tmp = tabel.lower().endswith("_tmp")
skal_lave_tmp = bool(getattr(args, "tmp", False))
# Primær DDL
primær_conf = deepcopy(file_conf)
ddl_sql = generate_create_table_sql(primær_conf, global_config)
samlet_sql_indhold.append(f"-- Tabel: {tabel}\n{ddl_sql}\n")
ddl_navn = file_conf.get("ddl_fil_navn") or _default_ddl_filename(tabel)
ddl_navn = generer_filnavn(ddl_navn, global_config)
ddl_sti = os.path.join(outdir, ddl_navn)
with open(ddl_sti, "w", encoding="utf-8") as f:
f.write(ddl_sql)
logger.info(f"[DDL] Skrev {ddl_sti}")
antal += 1
# Sekundær DDL
if skal_lave_tmp:
sek_tabel = tmp_tabel if not yaml_is_tmp else base_tabel
sek_conf = deepcopy(file_conf)
sek_conf["tabel_navn"] = sek_tabel
sek_sql = generate_create_table_sql(sek_conf, global_config)
samlet_sql_indhold.append(f"-- Tabel: {sek_tabel}\n{sek_sql}\n")
sek_navn = generer_filnavn(_default_ddl_filename(sek_tabel), global_config)
sek_sti = os.path.join(outdir, sek_navn)
with open(sek_sti, "w", encoding="utf-8") as f:
f.write(sek_sql)
logger.info(f"[DDL] Skrev {sek_sti}")
antal += 1
# Flyt-scripts
if getattr(args, "flyt", False):
_skriv_flyt_scripts(
tabel, base_tabel, tmp_tabel, file_conf, outdir,
generate_insert_move_sql, samlet_flyt_indhold
)
if getattr(args, "flyt_kort", False):
_skriv_flyt_scripts(
tabel, base_tabel, tmp_tabel, file_conf, outdir,
generate_insert_move_sql_short, samlet_flyt_indhold
)
except Exception as e:
logger.error(f"[DDL] Fejl: {e}")
raise
# Samlede filer
if antal > 0:
samlet_sti = os.path.join(outdir, "sql_samlet.sql")
with open(samlet_sti, "w", encoding="utf-8") as f:
f.write("\n".join(samlet_sql_indhold))
logger.info(f"[DDL] Skrev samlet fil: {samlet_sti}")
if samlet_flyt_indhold:
samlet_flyt_sti = os.path.join(outdir, "sql_flyt_samlet.sql")
with open(samlet_flyt_sti, "w", encoding="utf-8") as f:
f.write("\n".join(samlet_flyt_indhold))
logger.info(f"[FLYT] Skrev samlet fil: {samlet_flyt_sti}")
logger.info(f"[DDL] FÆRDIG: {antal} fil(er) genereret.")
else:
logger.info("[DDL] Ingen DDL genereret.")
def generate_create_staging_sql(file_conf: dict, global_config: dict) -> str:
"""
Genererer CREATE TABLE DDL for staging-tabellen.
Kun kolonner fra YAML ingen ekstra_kolonner.
Tabelnavnet hentes fra tabel.staging.
"""
tabel_cfg = file_conf.get("tabel", {})
table = tabel_cfg.get("staging")
if not table:
raise ValueError("Kan ikke generere staging DDL: 'tabel.staging' mangler.")
cols = _byg_staging_kolonner(file_conf)
if not cols:
raise ValueError(f"Kan ikke generere DDL for {table}: 'kolonner' er tom.")
col_lines = []
for col in cols:
name = col["navn"]
sql_type = _map_yaml_type_to_ase(col, global_config.get("dato_ud", "%Y-%m-%d"))
not_null = "NOT NULL" if col.get("påkrævet") else "NULL"
col_lines.append(f' "{name}" {sql_type} {not_null}')
cols_block = ",\n".join(col_lines)
table_only = table.split(".")[-1]
drop_part = (
f"IF EXISTS (SELECT 1 FROM sysobjects WHERE name = '{table_only}' AND type = 'U')\n"
f"BEGIN\n"
f" DROP TABLE {table}\n"
f"END\nGO\n\n"
)
create_part = (
f"CREATE TABLE {table} (\n"
f"{cols_block}\n"
f");\nGO\n"
)
return drop_part + create_part
def generate_create_blivende_sql(file_conf: dict, global_config: dict) -> str:
"""
Genererer CREATE TABLE DDL for den blivende tabel.
Kolonner: start-ekstra + yaml-kolonner + slut-ekstra.
Tabelnavnet hentes fra tabel.blivende.
Tilføjer PRIMARY KEY på tekniske_nøgler hvis angivet.
"""
tabel_cfg = file_conf.get("tabel", {})
table = tabel_cfg.get("blivende")
if not table:
raise ValueError("Kan ikke generere blivende DDL: 'tabel.blivende' mangler.")
tekniske_nøgler = tabel_cfg.get("tekniske_nøgler", [])
forretnings_nøgler = tabel_cfg.get("forretnings_nøgler", [])
alle_kolonner = _byg_blivende_kolonner(file_conf)
if not alle_kolonner:
raise ValueError(f"Kan ikke generere DDL for {table}: ingen kolonner.")
col_lines = []
pk_cols = list(tekniske_nøgler) # tekniske nøgler → PRIMARY KEY
for col in alle_kolonner:
name = col["navn"]
if "ddl_type" in col:
# Ekstra-kolonne brug _map_ekstra_kolonne_til_sql
col_lines.append(_map_ekstra_kolonne_til_sql(col))
else:
# Normal YAML-kolonne
sql_type = _map_yaml_type_to_ase(col, global_config.get("dato_ud", "%Y-%m-%d"))
not_null = "NOT NULL" if col.get("påkrævet") else "NULL"
col_lines.append(f' "{name}" {sql_type} {not_null}')
# PRIMARY KEY
pk_line = ""
if pk_cols:
cols_list = ", ".join(f'"{c}"' for c in pk_cols)
pk_line = f",\n PRIMARY KEY ({cols_list})"
cols_block = ",\n".join(col_lines) + pk_line
table_only = table.split(".")[-1]
schema = table.split(".")[0] if "." in table else "dbo"
drop_part = (
f"-- Omdøb eksisterende tabel inden oprettelse af ny:\n"
f"IF EXISTS (SELECT 1 FROM sysobjects WHERE name = '{table_only}' AND type = 'U')\n"
f"BEGIN\n"
f" EXEC sp_rename '{table}', '{table_only}_gammel'\n"
f"END\n"
f"GO\n\n"
f"-- DROP gammel tabel når du er sikker:\n"
f"-- IF EXISTS (SELECT 1 FROM sysobjects WHERE name = '{table_only}_gammel' AND type = 'U')\n"
f"-- BEGIN\n"
f"-- DROP TABLE {schema}.{table_only}_gammel\n"
f"-- END\n"
f"-- GO\n\n"
)
create_part = (
f"CREATE TABLE {table} (\n"
f"{cols_block}\n"
f");\nGO\n"
)
return drop_part + create_part
def generate_indexes_sql(file_conf: dict) -> str:
"""
Genererer CREATE INDEX statements for den blivende tabel.
Automatiske indexes:
- Ét index på forretnings_nøgler (hvis angivet)
- Ét index på forretnings_nøgler + virk_fra (hvis historik og virk_fra angivet)
Eksplicitte indexes:
- Fra tabel.indexes liste af kolonnenavne-lister
"""
tabel_cfg = file_conf.get("tabel", {})
table = tabel_cfg.get("blivende")
if not table:
return ""
table_only = table.split(".")[-1]
forretnings_nøgler = tabel_cfg.get("forretnings_nøgler", [])
historik = tabel_cfg.get("historik")
virk_fra = tabel_cfg.get("virk_fra")
eksplicitte = tabel_cfg.get("indexes", [])
lines = []
ix_nr = 1
# Automatisk index på forretnings_nøgler
if forretnings_nøgler:
cols = ", ".join(f'"{c}"' for c in forretnings_nøgler)
ix_navn = f"ix_{table_only}_bk"
lines.append(f"CREATE INDEX {ix_navn} ON {table} ({cols})")
lines.append("GO\n")
ix_nr += 1
# Automatisk historik-index på forretnings_nøgler + virk_fra
if forretnings_nøgler and historik in ("t1", "t2") and virk_fra:
cols = ", ".join(f'"{c}"' for c in forretnings_nøgler) + f', "{virk_fra}"'
ix_navn = f"ix_{table_only}_bk_virk"
lines.append(f"CREATE INDEX {ix_navn} ON {table} ({cols})")
lines.append("GO\n")
ix_nr += 1
# Eksplicitte indexes fra YAML
for ix_cols in eksplicitte:
if isinstance(ix_cols, list):
cols = ", ".join(f'"{c}"' for c in ix_cols)
ix_navn = f"ix_{table_only}_{ix_nr:02d}"
lines.append(f"CREATE INDEX {ix_navn} ON {table} ({cols})")
lines.append("GO\n")
ix_nr += 1
return "\n".join(lines)
def generate_t1_flyt_sql(file_conf: dict) -> str:
"""
Genererer T1 (overskriv) flyt-script.
DELETE på forretnings_nøgler + INSERT af staging-kolonner.
"""
tabel_cfg = file_conf.get("tabel", {})
staging = tabel_cfg.get("staging")
blivende = tabel_cfg.get("blivende")
forretnings_nøgler = tabel_cfg.get("forretnings_nøgler", [])
staging_kolonner = [k["navn"] for k in _byg_staging_kolonner(file_conf)]
if not staging or not blivende:
raise ValueError("T1 flyt kræver både 'tabel.staging' og 'tabel.blivende'.")
cols_block = ",\n ".join(staging_kolonner)
lines = []
lines.append(f"-- T1: Overskriv eksisterende rækker baseret på forretningsnøgle")
lines.append(f"-- Trin 1: Slet eksisterende rækker der findes i staging")
lines.append(f"DELETE FROM {blivende}")
lines.append(f"FROM {blivende}")
lines.append(f"JOIN {staging}")
if forretnings_nøgler:
join_betingelser = [
f" {blivende}.{k} = {staging}.{k}"
for k in forretnings_nøgler
]
lines.append(f" ON " + "\n AND ".join(join_betingelser))
else:
lines.append(f" ON 1 = 1 -- TILPAS: angiv forretnings_nøgler i YAML")
lines.append("GO\n")
lines.append(f"-- Trin 2: Indsæt nye rækker fra staging")
lines.append(f"INSERT INTO {blivende} (")
lines.append(f" {cols_block}")
lines.append(f")")
lines.append(f"SELECT")
lines.append(f" {cols_block}")
lines.append(f"FROM {staging}")
lines.append("GO\n")
return "\n".join(lines)
def generate_t2_flyt_sql(file_conf: dict) -> str:
"""
Genererer T2 (historik) flyt-script.
Luk eksisterende rækker med UPDATE på virk_til + INSERT nye rækker.
Forudsætter at ny data altid er nyere end eksisterende.
"""
tabel_cfg = file_conf.get("tabel", {})
staging = tabel_cfg.get("staging")
blivende = tabel_cfg.get("blivende")
forretnings_nøgler = tabel_cfg.get("forretnings_nøgler", [])
virk_fra = tabel_cfg.get("virk_fra")
virk_til = tabel_cfg.get("virk_til")
staging_kolonner = [k["navn"] for k in _byg_staging_kolonner(file_conf)]
if not staging or not blivende:
raise ValueError("T2 flyt kræver både 'tabel.staging' og 'tabel.blivende'.")
if not virk_fra or not virk_til:
raise ValueError("T2 flyt kræver 'tabel.virk_fra' og 'tabel.virk_til'.")
cols_block = ",\n ".join(staging_kolonner)
lines = []
lines.append(f"-- T2: Historik luk eksisterende rækker og indsæt nye")
lines.append(f"-- Forudsætning: ny data er altid nyere end eksisterende rækker")
lines.append(f"--")
lines.append(f"-- Trin 1: Luk eksisterende åbne rækker ved at sætte {virk_til}")
lines.append(f"UPDATE {blivende}")
lines.append(f"SET {blivende}.{virk_til} = {staging}.{virk_fra}")
lines.append(f"FROM {blivende}")
lines.append(f"JOIN {staging}")
if forretnings_nøgler:
join_betingelser = [
f" {blivende}.{k} = {staging}.{k}"
for k in forretnings_nøgler
]
lines.append(f" ON " + "\n AND ".join(join_betingelser))
lines.append(f" AND {blivende}.{virk_til} = '9999-12-31' -- kun åbne rækker")
else:
lines.append(f" ON 1 = 1 -- TILPAS: angiv forretnings_nøgler i YAML")
lines.append("GO\n")
lines.append(f"-- Trin 2: Indsæt nye rækker fra staging")
lines.append(f"-- virk_til sættes til '9999-12-31' (åben række)")
lines.append(f"INSERT INTO {blivende} (")
lines.append(f" {cols_block},")
lines.append(f" {virk_til}")
lines.append(f")")
lines.append(f"SELECT")
lines.append(f" {cols_block},")
lines.append(f" '9999-12-31'")
lines.append(f"FROM {staging}")
lines.append("GO\n")
return "\n".join(lines)