diff --git a/scripts/convert_to_10g.bat b/scripts/convert_to_10g.bat new file mode 100644 index 0000000..cfe26fb --- /dev/null +++ b/scripts/convert_to_10g.bat @@ -0,0 +1,19 @@ +@echo off +REM ========================================================================= +REM convert_to_10g.bat +REM Converteste PACK_CONTAFIN.pck -> PACK_CONTAFIN_ORACLE_10G.pck +REM (elimina FORALL/BULK_ROWCOUNT, inlocuieste cu FOR LOOP Oracle 10g) +REM +REM UTILIZARE (din radacina proiectului): +REM scripts\convert_to_10g.bat -- conversie normala +REM scripts\convert_to_10g.bat --dry-run -- doar afiseaza diff +REM scripts\convert_to_10g.bat --no-backup -- fara fisier .bak +REM scripts\convert_to_10g.bat --input alt.pck --output out.pck +REM +REM Toate argumentele sunt transmise direct catre scriptul Python. +REM ========================================================================= + +REM Schimba directorul de lucru la radacina proiectului (parintele lui scripts\) +cd /d "%~dp0.." + +python scripts\convert_to_10g.py %* diff --git a/scripts/convert_to_10g.py b/scripts/convert_to_10g.py new file mode 100644 index 0000000..58c52d0 --- /dev/null +++ b/scripts/convert_to_10g.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +""" +Convert PACK_CONTAFIN.pck from Oracle 21c (FORALL/BULK) to Oracle 10g compatible syntax. + +Replaces 3 FORALL blocks in SCRIE_JC_2007 with equivalent FOR LOOPs: + T1: FORALL UPDATE -> FOR LOOP UPDATE + T2: FOR + SQL%BULK_ROWCOUNT -> inline SQL%ROWCOUNT (folds into T1's loop) + T3: FORALL INSERT -> FOR LOOP INSERT + T4: add END LOOP after last INSERT value + T5: FORALL DELETE -> FOR LOOP DELETE + T6: add END LOOP after last DELETE condition + +Exit codes: 0=ok, 1=validation failed, 2=IO error + +--------------------------------------------------------------------------- +UTILIZARE / USAGE +--------------------------------------------------------------------------- + +Linux / WSL: + python3 scripts/convert_to_10g.py # conversie normala + python3 scripts/convert_to_10g.py --dry-run # doar arata diff, nu scrie + python3 scripts/convert_to_10g.py --no-backup # fara fisier .bak + python3 scripts/convert_to_10g.py --input alt.pck --output out.pck + +Windows (din radacina proiectului): + scripts\\convert_to_10g.bat + scripts\\convert_to_10g.bat --dry-run + scripts\\convert_to_10g.bat --input alt.pck --output out.pck + +Argumente optionale: + --dry-run Afiseaza diff-ul fara sa scrie nimic pe disk + --no-backup Nu crea fisierul .bak inainte de scriere + --input FILE Fisier sursa (default: input/PACK_CONTAFIN.pck) + --output FILE Fisier destinatie (default: input/PACK_CONTAFIN_ORACLE_10G.pck) +--------------------------------------------------------------------------- +""" + +import argparse +import difflib +import os +import sys +import tempfile + +# --------------------------------------------------------------------------- +# Transformations (exact string, \n normalized — CRLF handled at I/O layer) +# --------------------------------------------------------------------------- + +TRANSFORMS = [ + { + "id": "T1", + "desc": "FORALL UPDATE -> FOR LOOP UPDATE", + "old": ( + " -- BULK UPDATE all rows at once using FORALL\n" + " FORALL i IN 1 .. S.COUNT\n" + " UPDATE JC2007 J" + ), + "new": ( + " -- UPDATE all rows using FOR loop (Oracle 10g compatible)\n" + " FOR i IN 1 .. S.COUNT LOOP\n" + " UPDATE JC2007 J" + ), + "expected_count": 1, + }, + { + "id": "T2", + "desc": "FOR + SQL%BULK_ROWCOUNT -> inline SQL%ROWCOUNT", + "old": ( + " AND NVL(J.ID_SUCURSALA, -99) = NVL(S(i).ID_SUCURSALA, -99);\n" + "\n" + " -- Populate SI and SD collections for INSERT and DELETE\n" + " FOR i IN 1 .. S.COUNT LOOP\n" + " IF SQL%BULK_ROWCOUNT(i) = 0 THEN" + ), + "new": ( + " AND NVL(J.ID_SUCURSALA, -99) = NVL(S(i).ID_SUCURSALA, -99);\n" + "\n" + " -- Check result of UPDATE and populate SI/SD collections\n" + " IF SQL%ROWCOUNT = 0 THEN" + ), + "expected_count": 1, + }, + { + "id": "T3", + "desc": "FORALL INSERT -> FOR LOOP INSERT", + "old": ( + " -- BULK INSERT for new rows\n" + " IF SI.COUNT > 0 THEN\n" + " FORALL i IN 1 .. SI.COUNT\n" + " INSERT INTO JC2007" + ), + "new": ( + " -- INSERT for new rows (Oracle 10g compatible)\n" + " IF SI.COUNT > 0 THEN\n" + " FOR i IN 1 .. SI.COUNT LOOP\n" + " INSERT INTO JC2007" + ), + "expected_count": 1, + }, + { + "id": "T4", + "desc": "Add END LOOP after last INSERT value", + "old": ( + " SI(i).ID_SUCURSALA);\n" + " END IF;\n" + "\n" + " -- BULK DELETE for rows where all values became 0 after UPDATE" + ), + "new": ( + " SI(i).ID_SUCURSALA);\n" + " END LOOP;\n" + " END IF;\n" + "\n" + " -- DELETE for rows where all values became 0 after UPDATE (Oracle 10g compatible)" + ), + "expected_count": 1, + }, + { + "id": "T5", + "desc": "FORALL DELETE -> FOR LOOP DELETE", + "old": ( + " FORALL i IN 1 .. SD.COUNT\n" + " DELETE FROM JC2007 J" + ), + "new": ( + " FOR i IN 1 .. SD.COUNT LOOP\n" + " DELETE FROM JC2007 J" + ), + "expected_count": 1, + }, + { + "id": "T6", + "desc": "Add END LOOP after last DELETE condition", + "old": ( + " AND J.ROTN100 = 0;\n" + " END IF;" + ), + "new": ( + " AND J.ROTN100 = 0;\n" + " END LOOP;\n" + " END IF;" + ), + "expected_count": 1, + }, +] + + +# --------------------------------------------------------------------------- +# I/O helpers +# --------------------------------------------------------------------------- + +def load_file(path): + """Load file, detect CRLF, return (text_with_LF, used_crlf, encoding_used).""" + for enc in ("utf-8-sig", "utf-8", "cp1250"): + try: + with open(path, "rb") as fh: + raw = fh.read() + text = raw.decode(enc) + used_crlf = b"\r\n" in raw + # Normalize to LF for processing + text = text.replace("\r\n", "\n").replace("\r", "\n") + return text, used_crlf, enc + except (UnicodeDecodeError, LookupError): + continue + print(f"ERROR: Cannot decode {path} with utf-8-sig / utf-8 / cp1250", file=sys.stderr) + sys.exit(2) + + +def write_atomic(path, text, used_crlf, encoding, no_backup): + """Write atomically via temp file; optionally keep a .bak first.""" + if used_crlf: + text = text.replace("\n", "\r\n") + + encoded = text.encode(encoding if encoding != "utf-8-sig" else "utf-8") + + # Backup existing output + if not no_backup and os.path.exists(path): + bak = path + ".bak" + try: + import shutil + shutil.copy2(path, bak) + print(f" Backup: {bak}") + except OSError as exc: + print(f"WARNING: Could not create backup: {exc}", file=sys.stderr) + + # Atomic write via temp file in same directory + dir_ = os.path.dirname(os.path.abspath(path)) + fd, tmp = tempfile.mkstemp(dir=dir_, suffix=".tmp") + try: + with os.fdopen(fd, "wb") as fh: + fh.write(encoded) + os.replace(tmp, path) + except OSError as exc: + try: + os.unlink(tmp) + except OSError: + pass + print(f"ERROR writing {path}: {exc}", file=sys.stderr) + sys.exit(2) + + +# --------------------------------------------------------------------------- +# Validation phases +# --------------------------------------------------------------------------- + +def count_forall(text): + """Count FORALL keyword occurrences that are actual statements (not comments).""" + count = 0 + for line in text.splitlines(): + stripped = line.lstrip() + if stripped.upper().startswith("FORALL "): + count += 1 + return count + + +def phase1_validate(text): + """Verify all patterns exist with expected counts. Returns True or exits.""" + ok = True + for t in TRANSFORMS: + found = text.count(t["old"]) + if found != t["expected_count"]: + print( + f" FAIL [{t['id']}] {t['desc']}: " + f"expected {t['expected_count']} occurrence(s), found {found}" + ) + ok = False + else: + print(f" OK [{t['id']}] {t['desc']}: found {found}x") + return ok + + +def phase3_validate(text): + """Verify no FORALL statements remain. Returns True or prints error.""" + remaining = count_forall(text) + if remaining != 0: + print(f" FAIL: {remaining} FORALL statement(s) remain in output!") + return False + print(" OK No FORALL statements in output.") + return True + + +# --------------------------------------------------------------------------- +# Transforms +# --------------------------------------------------------------------------- + +def apply_transforms(text): + """Apply all 6 transformations sequentially.""" + for t in TRANSFORMS: + text = text.replace(t["old"], t["new"], 1) + return text + + +# --------------------------------------------------------------------------- +# Diff display +# --------------------------------------------------------------------------- + +def show_diff(original, converted, src_label, dst_label): + diff = list( + difflib.unified_diff( + original.splitlines(keepends=True), + converted.splitlines(keepends=True), + fromfile=src_label, + tofile=dst_label, + n=3, + ) + ) + if not diff: + print(" (no differences)") + return + print(f" {len(diff)} diff lines") + print() + for line in diff: + sys.stdout.write(line) + print() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def parse_args(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( + "--input", + default=os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "input", + "PACK_CONTAFIN.pck", + ), + help="Source file (default: input/PACK_CONTAFIN.pck)", + ) + p.add_argument( + "--output", + default=None, + help="Output file (default: input/PACK_CONTAFIN_ORACLE_10G.pck)", + ) + p.add_argument("--dry-run", action="store_true", help="Show diff only, do not write") + p.add_argument("--no-backup", action="store_true", help="Skip .bak creation") + return p.parse_args() + + +def main(): + args = parse_args() + + input_path = args.input + output_path = args.output or os.path.join( + os.path.dirname(input_path), "PACK_CONTAFIN_ORACLE_10G.pck" + ) + + print(f"Input: {input_path}") + print(f"Output: {output_path}") + print() + + # Load + try: + original, used_crlf, encoding = load_file(input_path) + except FileNotFoundError: + print(f"ERROR: Input file not found: {input_path}", file=sys.stderr) + sys.exit(2) + + forall_count = count_forall(original) + print(f"Input FORALL statements: {forall_count} (encoding={encoding}, crlf={used_crlf})") + print() + + # Idempotency check + if forall_count == 0: + print("Already converted (0 FORALL statements). Nothing to do.") + sys.exit(0) + + # Phase 1 — pre-validate + print("Phase 1: Pre-validate patterns") + if not phase1_validate(original): + print() + print("ERROR: One or more patterns not found. Aborting without changes.") + sys.exit(1) + print() + + # Apply transforms + converted = apply_transforms(original) + + # Phase 3 — post-validate + print("Phase 3: Post-validate output") + if not phase3_validate(converted): + print() + print("ERROR: FORALL statements remain after conversion. Aborting.") + sys.exit(1) + print() + + # Show diff + print("Diff:") + show_diff(original, converted, input_path, output_path) + + if args.dry_run: + print("Dry-run mode: no files written.") + sys.exit(0) + + # Write + write_atomic(output_path, converted, used_crlf, encoding, args.no_backup) + print(f"Written: {output_path}") + print("Done.") + sys.exit(0) + + +if __name__ == "__main__": + main()