741 lines
27 KiB
Python
741 lines
27 KiB
Python
"""
|
||
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) |