From 758f179192c0f59de37131bec8be4604e3644749 Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Sat, 4 Apr 2026 20:45:35 +0200 Subject: [PATCH] Generatorer --- Manual/.~lock.manual_kap15_v3.odt# | 1 + Manual/manual_kap15_v3.odt | Bin 0 -> 45939 bytes udpak_semistruktur.py | 20 +- udpak_semistruktur/config.py | 18 +- udpak_semistruktur/ddl.py | 480 +++++++++++++++++++++++++---- xsd_til_yaml.py | 0 xsd_til_yaml_old.py | 471 ++++++++++++++++++++++++++++ 7 files changed, 920 insertions(+), 70 deletions(-) create mode 100644 Manual/.~lock.manual_kap15_v3.odt# create mode 100644 Manual/manual_kap15_v3.odt create mode 100644 xsd_til_yaml.py create mode 100644 xsd_til_yaml_old.py diff --git a/Manual/.~lock.manual_kap15_v3.odt# b/Manual/.~lock.manual_kap15_v3.odt# new file mode 100644 index 0000000..932ab2d --- /dev/null +++ b/Manual/.~lock.manual_kap15_v3.odt# @@ -0,0 +1 @@ +,carsten,carsten-mint,04.04.2026 15:42,file:///home/carsten/.config/libreoffice/4; \ No newline at end of file diff --git a/Manual/manual_kap15_v3.odt b/Manual/manual_kap15_v3.odt new file mode 100644 index 0000000000000000000000000000000000000000..d3ba061ed6a11a0a75509d86ef3bcb8616aab33b GIT binary patch literal 45939 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 G_kRIPcY=2S literal 0 HcmV?d00001 diff --git a/udpak_semistruktur.py b/udpak_semistruktur.py index afa06ca..435ca18 100644 --- a/udpak_semistruktur.py +++ b/udpak_semistruktur.py @@ -70,7 +70,7 @@ def _kør_udtræk(config: dict, global_config: dict) -> None: # Opret DB-forbindelse hvis der er tabel-output har_tabel_output = any( - cfg.get("type") == "tabel" + cfg.get("type") in ("tabel", "tabel_avanceret") for cfg in config.get("output_filer", []) ) conn = None @@ -148,6 +148,24 @@ def _kør_udtræk(config: dict, global_config: dict) -> None: if skrevet: fejl_filer_skrevet.append(skrevet) + elif cfg.get("type") == "tabel_avanceret": + tabel_cfg = cfg.get("tabel", {}) + staging = tabel_cfg.get("staging") + if staging: + kolonner = [k["navn"] for k in cfg["kolonner"]] + indsatte, fejlede = insert_rows_ase( + conn, staging, kolonner, tmp_data["rækker"] + ) + logger.info(f"DB: {indsatte} rækker indsat i {staging}") + if fejlede: + logger.warning(f"DB: {len(fejlede)} rækker fejlede i {staging}") + fejl_base = os.path.join(global_config["output_path"], + staging.replace(".", "_")) + skrevet = _skriv_fejl_fil(tmp_data, global_config.get("fejl_fil_ext"), + fejl_base, cfg["kolonner"], separator, encoding) + if skrevet: + fejl_filer_skrevet.append(skrevet) + # Tjek om alle output-filer gav 0 rækker if samlet_antal_rækker == 0: logger.warning("0 rækker genereret i alle output-filer for dette input.") diff --git a/udpak_semistruktur/config.py b/udpak_semistruktur/config.py index 67f87de..1c0ab27 100644 --- a/udpak_semistruktur/config.py +++ b/udpak_semistruktur/config.py @@ -115,10 +115,12 @@ def _expand_output_groups_new_only(cfg: dict) -> dict: ctype = "fil" elif "tabel_navn" in child: ctype = "tabel" + elif "tabel" in child and isinstance(child["tabel"], dict): + ctype = "tabel_avanceret" else: raise ValueError(f"outputs-element #{cidx+1} i gruppe #{idx+1} mangler 'type' og kan ikke udledes.") - if ctype not in ("fil", "tabel"): + if ctype not in ("fil", "tabel", "tabel_avanceret"): raise ValueError(f"outputs-element #{cidx+1} i gruppe #{idx+1} har ukendt type '{ctype}'.") merged = _deep_merge_dicts(common, child) @@ -126,11 +128,11 @@ def _expand_output_groups_new_only(cfg: dict) -> dict: # Sikr præcis én destination has_file = "fil_navn" in merged has_table = "tabel_navn" in merged + has_avanceret = "tabel" in merged and isinstance(merged.get("tabel"), dict) if has_file and has_table: raise ValueError(f"outputs-element #{cidx+1} i gruppe #{idx+1} må ikke have både 'fil_navn' og 'tabel_navn'.") - if not has_file and not has_table: - raise ValueError(f"outputs-element #{cidx+1} i gruppe #{idx+1} skal have enten 'fil_navn' eller 'tabel_navn'.") - + if not has_file and not has_table and not has_avanceret: + raise ValueError(f"outputs-element #{cidx+1} i gruppe #{idx+1} skal have enten 'fil_navn', 'tabel_navn' eller 'tabel'.") # 'overskrifter' gælder kun for filer; brug gruppens default hvis ikke sat if ctype == "fil": if "overskrifter" not in merged and group_default_headers is not None: @@ -276,13 +278,15 @@ def valider_yaml(yaml_file_path: str) -> dict: for file_cfg in config["output_filer"]: if not isinstance(file_cfg, dict): raise ValueError("Hvert output-element skal være et objekt.") - if ("fil_navn" in file_cfg) == ("tabel_navn" in file_cfg): - raise ValueError("Hvert output skal have præcis én af 'fil_navn' eller 'tabel_navn'.") + har_avanceret = isinstance(file_cfg.get("tabel"), dict) + if not har_avanceret: + if ("fil_navn" in file_cfg) == ("tabel_navn" in file_cfg): + raise ValueError("Hvert output skal have præcis én af 'fil_navn' eller 'tabel_navn'.") if "rod" not in file_cfg: raise ValueError("Hvert output skal have en 'rod'.") if "kolonner" not in file_cfg or not isinstance(file_cfg["kolonner"], list): raise ValueError("Hvert output skal have 'kolonner' som en liste.") - if "tabel_navn" in file_cfg: + if "tabel_navn" in file_cfg or har_avanceret: file_cfg.pop("overskrifter", None) # kun relevant for filer if "hvis_findes" in file_cfg: diff --git a/udpak_semistruktur/ddl.py b/udpak_semistruktur/ddl.py index 44cff2c..27092f4 100644 --- a/udpak_semistruktur/ddl.py +++ b/udpak_semistruktur/ddl.py @@ -31,6 +31,61 @@ 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 + – bruges automatisk når default indeholder case, select eller convert + 2) Med default: "navn" ddl_type DEFAULT 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 # ------------------------------------------------------------ @@ -63,6 +118,10 @@ def _map_yaml_type_to_ase(col: dict, dato_ud_global = "%Y-%m-%d") -> str: - 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) @@ -279,9 +338,9 @@ def run_ddl_mode(args, config: Dict[str, Any], global_config: Dict[str, Any]) -> """ Executes the full DDL-only flow and writes files. - --tmp flaget styrer om der genereres én eller to tabeller: - - Uden --tmp: kun den tabel der er nævnt i YAML - - Med --tmp: både den nævnte tabel og dens modpart (base↔tmp) + 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") @@ -293,93 +352,390 @@ def run_ddl_mode(args, config: Dict[str, Any], global_config: Dict[str, Any]) -> 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") - if not tabel: + + # Spring over hvis hverken nyt eller gammelt flow har tabelinfo + if not har_nyt_flow and not tabel: continue try: - base_tabel, tmp_tabel = split_base_tmp(tabel) - yaml_is_tmp = tabel.lower().endswith("_tmp") - skal_lave_tmp = bool(getattr(args, "tmp", False)) + # ============================================================= + # 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) Primær DDL – brug tabelnavnet præcis som det er i YAML - # --------------------------------------------------------- - primær_conf = deepcopy(file_conf) - ddl_sql_primær = generate_create_table_sql(primær_conf, global_config) - samlet_sql_indhold.append(f"-- Tabel: {tabel}\n{ddl_sql_primær}\n") + # 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 - ddl_primær_name = file_conf.get("ddl_fil_navn") or _default_ddl_filename(tabel) - ddl_primær_name = generer_filnavn(ddl_primær_name, global_config) - ddl_primær_path = os.path.join(outdir, ddl_primær_name) + # 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 - with open(ddl_primær_path, "w", encoding="utf-8") as f: - f.write(ddl_sql_primær) + # 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}") - logger.info(f"[DDL] Skrev {ddl_primær_path}") - antal += 1 + # 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 - # --------------------------------------------------------- - # 2) Sekundær DDL – kun hvis --tmp er givet - # YAML er base → sekundær er tmp - # YAML er _tmp → sekundær er base - # --------------------------------------------------------- - - if skal_lave_tmp: - sekundær_tabel = tmp_tabel if not yaml_is_tmp else base_tabel - sekundær_conf = deepcopy(file_conf) - sekundær_conf["tabel_navn"] = sekundær_tabel + 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}") - ddl_sql_sekundær = generate_create_table_sql(sekundær_conf, global_config) - samlet_sql_indhold.append(f"-- Tabel: {sekundær_tabel}\n{ddl_sql_sekundær}\n") + # ============================================================= + # 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)) - ddl_sekundær_name = generer_filnavn(_default_ddl_filename(sekundær_tabel), global_config) - ddl_sekundær_path = os.path.join(outdir, ddl_sekundær_name) + # 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") - with open(ddl_sekundær_path, "w", encoding="utf-8") as f: - f.write(ddl_sql_sekundær) - - logger.info(f"[DDL] Skrev {ddl_sekundær_path}") + 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 - # --------------------------------------------------------- - # 3) Flyt scripts – kun hvis --flyt er givet - # --------------------------------------------------------- - if getattr(args, "flyt", False): - _skriv_flyt_scripts( - tabel, base_tabel, tmp_tabel, file_conf, outdir, - generate_insert_move_sql, samlet_flyt_indhold - ) + # 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 - # --------------------------------------------------------- - # 4) Flyt scripts kort – kun hvis --flyt_kort er givet - # --------------------------------------------------------- - 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 - ) + # 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 for {tabel}: {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_alt: - f_alt.write("\n".join(samlet_sql_indhold)) + 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 (getattr(args, "flyt", False) or getattr(args, "flyt_kort", False)) and samlet_flyt_indhold: + 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_flyt_alt: - f_flyt_alt.write("\n".join(samlet_flyt_indhold)) + 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) \ No newline at end of file diff --git a/xsd_til_yaml.py b/xsd_til_yaml.py new file mode 100644 index 0000000..e69de29 diff --git a/xsd_til_yaml_old.py b/xsd_til_yaml_old.py new file mode 100644 index 0000000..406fc37 --- /dev/null +++ b/xsd_til_yaml_old.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +""" +xsd_til_yaml.py + +Genererer YAML-skelet fra en XSD-fil til brug med udpak_semistruktur. + +Producerer: + - skabeloner.yaml – feltskabeloner for alle complexTypes + - nøgler.yaml – nøgle-placeholders for overliggende unbounded-niveauer + - udtræk_.yaml – én fil per maxOccurs="unbounded" element + +Kør med: + python3 xsd_til_yaml.py --xsd bankdata.xsd --output ./yaml_output/ +""" + +from __future__ import annotations +import os +import argparse +from xml.etree import ElementTree as ET +from dataclasses import dataclass, field +from typing import Optional + +# XML Schema namespace +XS = "http://www.w3.org/2001/XMLSchema" + + +# ============================================================ +# Datastrukturer +# ============================================================ + +@dataclass +class XsdFelt: + """Repræsenterer ét felt (attribut eller element) i en complexType.""" + navn: str + er_attribut: bool + xsd_type: str = "xs:string" + påkrævet: bool = False + + @property + def felt_ref(self) -> str: + """Returnerer felt-referencen som den skrives i YAML.""" + return f"@{self.navn}" if self.er_attribut else self.navn + + +@dataclass +class XsdKompleksType: + """Repræsenterer en complexType fra XSD.""" + navn: str + felter: list[XsdFelt] = field(default_factory=list) + + +@dataclass +class XsdUdboundElement: + """Repræsenterer et maxOccurs=unbounded element – bliver til en output-fil.""" + element_navn: str # fx "postering" + type_navn: str # fx "PosteringType" + rod_sti: str # fx "bank.kunder.kunde.konti.konto.posteringer.postering" + overliggende: list[str] # navne på overliggende unbounded elementer + # fx ["kunde", "konto"] + + +# ============================================================ +# XSD Parser +# ============================================================ + +class XsdParser: + """Parser en XSD-fil og udtrækker complexTypes og unbounded elementer.""" + + def __init__(self, xsd_sti: str): + self.xsd_sti = xsd_sti + self.tree = ET.parse(xsd_sti) + self.root = self.tree.getroot() + + # Navngivne complexTypes fra XSD + self.komplekse_typer: dict[str, XsdKompleksType] = {} + + # Unbounded elementer med rod-sti og overliggende niveauer + self.unbounded_elementer: list[XsdUdboundElement] = [] + + def parse(self) -> None: + """Kør den komplette parsing.""" + self._parse_navngivne_typer() + self._find_unbounded_elementer() + + def _xs(self, tag: str) -> str: + """Returnerer fuldt kvalificeret XS-tag.""" + return f"{{{XS}}}{tag}" + + def _parse_navngivne_typer(self) -> None: + """Find alle navngivne complexTypes på topniveau.""" + for ct in self.root.findall(self._xs("complexType")): + navn = ct.get("name") + if not navn: + continue + felter = self._udpak_felter(ct) + self.komplekse_typer[navn] = XsdKompleksType(navn=navn, felter=felter) + + def _udpak_felter(self, ct_node) -> list[XsdFelt]: + """Udpakker alle felter (attributter + simple elementer) fra en complexType.""" + felter = [] + + # Attributter – bliver til @navn i YAML + for attr in ct_node.findall(self._xs("attribute")): + navn = attr.get("name") + xsd_type = attr.get("type", "xs:string") + påkrævet = attr.get("use") == "required" + felter.append(XsdFelt( + navn=navn, + er_attribut=True, + xsd_type=xsd_type, + påkrævet=påkrævet + )) + + # Sequence-elementer + for seq in ct_node.findall(self._xs("sequence")): + for el in seq.findall(self._xs("element")): + el_type = el.get("type", "xs:string") + el_navn = el.get("name") + max_occ = el.get("maxOccurs", "1") + + # Spring komplekse typer og unbounded over – de håndteres separat + if max_occ == "unbounded": + continue + if el_type and not el_type.startswith("xs:"): + continue + + felter.append(XsdFelt( + navn=el_navn, + er_attribut=False, + xsd_type=el_type or "xs:string", + påkrævet=True + )) + + return felter + + def _find_unbounded_elementer(self) -> None: + """ + Gennemgår XSD rekursivt og finder alle maxOccurs=unbounded elementer. + Bygger rod-stier og registrerer overliggende unbounded niveauer. + """ + # Find rod-elementet + rod_el = self.root.find(self._xs("element")) + if rod_el is None: + return + + rod_navn = rod_el.get("name", "rod") + rod_type = rod_el.get("type") + + self._traverse( + node=rod_el, + sti=[rod_navn], + overliggende_unbounded=[], + rod_type=rod_type + ) + + def _hent_type_node(self, type_navn: str): + """Henter en navngiven complexType-node fra XSD.""" + for ct in self.root.findall(self._xs("complexType")): + if ct.get("name") == type_navn: + return ct + return None + + def _traverse( + self, + node, + sti: list[str], + overliggende_unbounded: list[str], + rod_type: Optional[str] = None + ) -> None: + """ + Rekursiv gennemgang af XSD-strukturen. + Finder unbounded elementer og bygger rod-stier. + """ + # Find complexType-noden at arbejde med + if rod_type: + ct_node = self._hent_type_node(rod_type) + else: + ct_node = node.find(self._xs("complexType")) + + if ct_node is None: + return + + # Gennemgå sequence-elementer + for seq in ct_node.findall(self._xs("sequence")): + for el in seq.findall(self._xs("element")): + el_navn = el.get("name") + el_type = el.get("type") + max_occ = el.get("maxOccurs", "1") + + ny_sti = sti + [el_navn] + + if max_occ == "unbounded": + # Dette element bliver en output-fil + rod_sti = ".".join(ny_sti) + self.unbounded_elementer.append(XsdUdboundElement( + element_navn=el_navn, + type_navn=el_type or "", + rod_sti=rod_sti, + overliggende=list(overliggende_unbounded) + )) + # Fortsæt rekursivt med dette som nyt overliggende niveau + self._traverse( + node=el, + sti=ny_sti, + overliggende_unbounded=overliggende_unbounded + [el_navn], + rod_type=el_type + ) + else: + # Ikke unbounded – fortsæt ned hvis det er en kompleks type + if el_type and not el_type.startswith("xs:"): + self._traverse( + node=el, + sti=ny_sti, + overliggende_unbounded=overliggende_unbounded, + rod_type=el_type + ) + + +# ============================================================ +# Hjælpefunktion: XSD type → YAML type +# ============================================================ + +def xsd_type_til_yaml(xsd_type: str) -> dict: + """Konverterer en XSD-type til YAML kolonne-attributter.""" + mapping = { + "xs:string": {}, + "xs:decimal": {"type": "decimal", "decimaler": 2}, + "xs:integer": {"type": "integer"}, + "xs:int": {"type": "integer"}, + "xs:boolean": {"type": "boolean"}, + "xs:date": {"type": "date", "dato_ind": "%Y-%m-%d", "dato_ud": "%d-%m-%Y"}, + "xs:dateTime": {"type": "date", "dato_ind": "%Y-%m-%dT%H:%M:%S", "dato_ud": "SYBASE"}, + } + return mapping.get(xsd_type, {}) + + +# ============================================================ +# CLI +# ============================================================ + +def byg_argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Genererer YAML-skelet fra XSD til udpak_semistruktur." + ) + parser.add_argument("--xsd", required=True, help="Sti til XSD-filen") + parser.add_argument("--output", required=True, help="Output-mappe til YAML-filer") + parser.add_argument("--prefix", default="udtræk", + help="Prefix til udtræk-filnavne (standard: udtræk)") + return parser + + +# ============================================================ +# YAML Generatorer +# ============================================================ + +def _yaml_type_linjer(xsd_type: str, indryk: str) -> list[str]: + """Returnerer YAML-linjer for typekonvertering baseret på XSD-type.""" + attrs = xsd_type_til_yaml(xsd_type) + linjer = [] + for k, v in attrs.items(): + if isinstance(v, str): + linjer.append(f'{indryk}{k}: "{v}"') + else: + linjer.append(f'{indryk}{k}: {v}') + return linjer + + +def generer_skabeloner_yaml(xsd: XsdParser, output_mappe: str) -> None: + """Genererer skabeloner.yaml med feltskabeloner for alle relevante complexTypes.""" + linjer = [] + linjer.append("# =============================================================================") + linjer.append("# skabeloner.yaml") + linjer.append("#") + linjer.append("# Auto-genereret feltskabeloner fra XSD.") + linjer.append("# Relative feltnavne – brug prefix_felt naar skabelonen anvendes.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("kolonne_skabeloner:") + linjer.append("") + + relevante = {navn: kt for navn, kt in xsd.komplekse_typer.items() if kt.felter} + + for type_navn, kt in relevante.items(): + skabelon_navn = type_navn.replace("Type", "").lower() + linjer.append(f" # Felter fra {type_navn}") + linjer.append(f" {skabelon_navn}:") + + for felt in kt.felter: + linjer.append(f" - navn: {felt.navn}") + linjer.append(f' felt: "{felt.felt_ref}"') + for type_linje in _yaml_type_linjer(felt.xsd_type, " "): + linjer.append(type_linje) + if felt.påkrævet: + linjer.append(f" påkrævet: true") + linjer.append("") + + sti = os.path.join(output_mappe, "skabeloner.yaml") + with open(sti, "w", encoding="utf-8") as f: + f.write("\n".join(linjer)) + print(f" Skrev: {sti}") + + +def generer_nøgler_yaml(xsd: XsdParser, output_mappe: str) -> None: + """Genererer nøgler.yaml med placeholders for forretningsnøgler.""" + overliggende_niveauer = set() + for el in xsd.unbounded_elementer: + for ov in el.overliggende: + overliggende_niveauer.add(ov) + + if not overliggende_niveauer: + print(" Ingen overliggende niveauer – nøgler.yaml ikke genereret.") + return + + linjer = [] + linjer.append("# =============================================================================") + linjer.append("# nøgler.yaml") + linjer.append("#") + linjer.append("# Nøgle-skabeloner der binder output-filer til overliggende niveauer.") + linjer.append("# UDFYLD: Erstat __FELT__ med den korrekte forretningsnøgle.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("kolonne_skabeloner:") + linjer.append("") + + skrevne = set() + for el in xsd.unbounded_elementer: + if not el.overliggende: + continue + rod_dele = el.rod_sti.split(".") + for ov_navn in el.overliggende: + skabelon_navn = f"{ov_navn}_nøgle" + if skabelon_navn in skrevne: + continue + skrevne.add(skabelon_navn) + try: + idx = rod_dele.index(ov_navn) + ov_rod_sti = ".".join(rod_dele[:idx + 1]) + except ValueError: + ov_rod_sti = f"__ROD_STI_TIL_{ov_navn.upper()}__" + + linjer.append(f" # Nøgle fra {ov_navn}-niveau") + linjer.append(f" # TODO: Angiv den korrekte forretningsnøgle for {ov_navn}") + linjer.append(f" {skabelon_navn}:") + linjer.append(f" - navn: __FELT__") + linjer.append(" felt: __FELT__ # fx '@id' eller 'nr'") + linjer.append(f" rod: {ov_rod_sti}") + linjer.append("") + + sti = os.path.join(output_mappe, "nøgler.yaml") + with open(sti, "w", encoding="utf-8") as f: + f.write("\n".join(linjer)) + print(f" Skrev: {sti}") + + +def generer_udtræk_yaml(xsd: XsdParser, output_mappe: str, prefix: str) -> None: + """Genererer én udtræk-yaml per unbounded element.""" + for el in xsd.unbounded_elementer: + linjer = [] + linjer.append("# =============================================================================") + linjer.append(f"# {prefix}_{el.element_navn}.yaml") + linjer.append("#") + linjer.append(f"# Auto-genereret udtræk for {el.element_navn}.") + if el.overliggende: + linjer.append(f"# Udfyld nøgle-skabeloner i nøgler.yaml inden brug.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("output_filer:") + linjer.append("") + linjer.append(f" - rod: {el.rod_sti}") + linjer.append(f" kolonner:") + + if el.overliggende: + linjer.append(f" # Nøgler fra overliggende niveauer – udfyld i nøgler.yaml") + for ov_navn in el.overliggende: + linjer.append(f" - skabelon: {ov_navn}_nøgle") + linjer.append("") + + if el.type_navn and el.type_navn in xsd.komplekse_typer: + kt = xsd.komplekse_typer[el.type_navn] + if kt.felter: + skabelon_navn = el.type_navn.replace("Type", "").lower() + linjer.append(f" # Felter fra {el.element_navn} – se skabeloner.yaml") + linjer.append(f" - skabelon: {skabelon_navn}") + + linjer.append("") + linjer.append(f" outputs:") + linjer.append(f" - fil_navn: \"{el.element_navn}_{{yyyy}}{{mm}}{{dd}}.txt\"") + linjer.append(f" overskrifter: true") + linjer.append("") + + fil_navn = f"{prefix}_{el.element_navn}.yaml" + sti = os.path.join(output_mappe, fil_navn) + with open(sti, "w", encoding="utf-8") as f: + f.write("\n".join(linjer)) + print(f" Skrev: {sti}") + + +def generer_hoved_config(xsd: XsdParser, output_mappe: str, prefix: str) -> None: + """Genererer en hoved-config der inkluderer alle genererede filer.""" + xsd_basis = os.path.splitext(os.path.basename(xsd.xsd_sti))[0] + linjer = [] + linjer.append("# =============================================================================") + linjer.append(f"# config_{xsd_basis}.yaml") + linjer.append("#") + linjer.append("# Auto-genereret hoved-config. Tilpas config-sektionen.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("include:") + linjer.append(" - skabeloner.yaml") + linjer.append(" - nøgler.yaml") + for el in xsd.unbounded_elementer: + linjer.append(f" - {prefix}_{el.element_navn}.yaml") + linjer.append("") + linjer.append("config:") + linjer.append(f" input_fil: {xsd_basis}.xml") + linjer.append(" output_path: __OUTPUT_PATH__") + linjer.append(" logfil: log_{yyyy}{mm}{dd}.txt") + linjer.append(" log_niveau: info") + linjer.append(" log_output: begge") + linjer.append(" encoding: utf-8") + linjer.append(" separator: \"\\t\"") + linjer.append(" skrivetilstand: w") + linjer.append(" dato_ind: \"%Y-%m-%d\"") + linjer.append(" dato_ud: \"%d-%m-%Y\"") + + fil_navn = f"config_{xsd_basis}.yaml" + sti = os.path.join(output_mappe, fil_navn) + with open(sti, "w", encoding="utf-8") as f: + f.write("\n".join(linjer)) + print(f" Skrev: {sti}") + + +def main(): + parser = byg_argument_parser() + args = parser.parse_args() + + if not os.path.exists(args.xsd): + print(f"FEJL: XSD-filen '{args.xsd}' findes ikke.") + return + + os.makedirs(args.output, exist_ok=True) + + print(f"Parser XSD: {args.xsd}") + xsd = XsdParser(args.xsd) + xsd.parse() + + print(f"\nFandt {len(xsd.komplekse_typer)} komplekse typer:") + for navn in xsd.komplekse_typer: + kt = xsd.komplekse_typer[navn] + print(f" {navn}: {len(kt.felter)} felter") + + print(f"\nFandt {len(xsd.unbounded_elementer)} unbounded elementer:") + for el in xsd.unbounded_elementer: + print(f" {el.element_navn} ({el.rod_sti})") + if el.overliggende: + print(f" Overliggende: {el.overliggende}") + + print(f"\nGenererer YAML-filer i: {args.output}") + generer_skabeloner_yaml(xsd, args.output) + generer_nøgler_yaml(xsd, args.output) + generer_udtræk_yaml(xsd, args.output, args.prefix) + generer_hoved_config(xsd, args.output, args.prefix) + + print("\nFærdig! Husk at:") + print(" 1. Udfyld nøgler.yaml med de korrekte forretningsnøgler") + print(" 2. Ret output_path i hoved-config") + print(" 3. Tjek at rod-stier passer til din XML-struktur") + + +if __name__ == "__main__": + main()