feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs
This commit is contained in:
+174
-18
@@ -13,6 +13,57 @@ from typing import Optional
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _strip_schema_qualifiers(text: str) -> str:
|
||||
"""DB2 schema-qualified table refs (SCHEMA.TABLE) → TABLE.
|
||||
|
||||
SQLite has no schema objects, so gixsql-generated SQL such as
|
||||
`FROM SALARYDB.EMP_MASTER` fails at OPEN time ('no such table:
|
||||
SALARYDB.EMP_MASTER', SQLCODE != 0) and the program ABENDs before its
|
||||
main loop. Strip the qualifier from table positions only
|
||||
(after FROM/INTO/UPDATE/JOIN). Column aliases (E.EMP-ID, B.DEPT-CODE)
|
||||
and host-var INTO (:DBV-X) are untouched. Program-agnostic: handles
|
||||
ANY <schema>.<table>, incl. multi-part (DB2INST1.PAYROLL.TIMESHEET).
|
||||
"""
|
||||
def _fix(m: re.Match) -> str:
|
||||
kw = m.group(1)
|
||||
table = m.group(2).rsplit('.', 1)[-1]
|
||||
return f"{kw} {table}"
|
||||
return re.sub(
|
||||
r'\b(FROM|INTO|UPDATE|JOIN)\s+([\w-]+(?:\.[\w-]+)+)',
|
||||
_fix, text, flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_schema_qualifiers(text: str) -> str:
|
||||
"""Strip schema qualifiers inside EXEC SQL blocks (before gixpp).
|
||||
|
||||
Applied to the source BEFORE gixpp so the generated SQL string literals
|
||||
reference plain table names. Only EXEC SQL blocks are touched; other
|
||||
text (data literals, comments) is left unchanged. Mirrors
|
||||
`_normalize_current_timestamp`.
|
||||
"""
|
||||
def _fix_block(m: re.Match) -> str:
|
||||
return _strip_schema_qualifiers(m.group(0))
|
||||
return re.sub(r'(?is)EXEC SQL\b.*?\bEND-EXEC', _fix_block, text)
|
||||
|
||||
|
||||
def _normalize_current_timestamp(text: str) -> str:
|
||||
"""SQLite backend: DB2 `CURRENT TIMESTAMP` → `CURRENT_TIMESTAMP` inside EXEC SQL.
|
||||
|
||||
Applied to the source BEFORE gixpp. gixpp wraps long SQL across continuation
|
||||
lines and can split the token (`CURRENT TIMES` / `TAMP`), which defeats a
|
||||
post-gixpp same-line regex; normalizing the source first makes the generated
|
||||
SQL string literals concatenate to the SQLite-valid `CURRENT_TIMESTAMP`.
|
||||
Only EXEC SQL blocks are touched; other text (data literals, comments) is
|
||||
left unchanged.
|
||||
"""
|
||||
def _fix_sql(m: re.Match) -> str:
|
||||
block = m.group(0)
|
||||
return re.sub(r'\bCURRENT\s+TIMESTAMP\b', 'CURRENT_TIMESTAMP',
|
||||
block, flags=re.IGNORECASE)
|
||||
return re.sub(r'(?is)EXEC SQL\b.*?\bEND-EXEC', _fix_sql, text)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GixsqlBuildResult:
|
||||
success: bool
|
||||
@@ -103,13 +154,13 @@ class GixsqlCobolRunner:
|
||||
return (
|
||||
" 01 SQLCA.\n"
|
||||
" 05 SQLCAID PIC X(8).\n"
|
||||
" 05 SQLCABC PIC S9(9) COMP.\n"
|
||||
" 05 SQLCODE PIC S9(9) COMP.\n"
|
||||
" 05 SQLCABC PIC S9(9) COMP-5.\n"
|
||||
" 05 SQLCODE PIC S9(9) COMP-5.\n"
|
||||
" 05 SQLERRM.\n"
|
||||
" 49 SQLERRML PIC S9(4) COMP.\n"
|
||||
" 49 SQLERRML PIC S9(4) COMP-5.\n"
|
||||
" 49 SQLERRMC PIC X(256).\n"
|
||||
" 05 SQLERRP PIC X(8).\n"
|
||||
" 05 SQLERRD PIC S9(9) COMP OCCURS 6.\n"
|
||||
" 05 SQLERRD PIC S9(9) COMP-5 OCCURS 6.\n"
|
||||
" 05 SQLWARN.\n"
|
||||
" 10 SQLWARN0 PIC X(1).\n"
|
||||
" 10 SQLWARN1 PIC X(1).\n"
|
||||
@@ -140,6 +191,10 @@ class GixsqlCobolRunner:
|
||||
"""
|
||||
text = src_path.read_text(encoding="utf-8-sig")
|
||||
|
||||
# 0. Strip ALL comment lines BEFORE any EXEC SQL transforms (regex may match
|
||||
# 'EXEC SQL' inside Japanese comments, pulling comment text into SQL strings)
|
||||
text = re.sub(r'^[ \t]{0,10}\*.*\n?', '', text, flags=re.MULTILINE)
|
||||
|
||||
# 1. Replace EXEC SQL INCLUDE SQLCA → COPY SQLCA
|
||||
text = re.sub(r'EXEC SQL INCLUDE SQLCA END-EXEC\.', ' COPY SQLCA.', text, flags=re.IGNORECASE)
|
||||
# Transform EXEC SQL CONNECT TO 'literal' → use WS variables (gixpp requires :variable not literal)
|
||||
@@ -154,17 +209,10 @@ class GixsqlCobolRunner:
|
||||
r"CONNECT\s+TO\s+'[^']*'",
|
||||
f"CONNECT TO :{conn_var} USER :{usr_var}",
|
||||
inner,
|
||||
flags=re.IGNORECASE
|
||||
flags=re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
# Short absolute path under C:\Temp\gix\ (no Chinese chars, fits col 72).
|
||||
# The orchestrator creates the DB at the same path so they match.
|
||||
from pathlib import Path as _Path
|
||||
pid = _Path(src_path).stem
|
||||
gix_root = _Path("C:/Temp/gix")
|
||||
gix_root.mkdir(parents=True, exist_ok=True)
|
||||
db_path = gix_root / f"{pid}.db"
|
||||
conn_val = "sqlite:///" + str(db_path).replace("\\", "/")
|
||||
return (f"MOVE '{conn_val}' TO {conn_var}\n"
|
||||
# Orchestrator copies DB to CWD/kin.
|
||||
return (f"MOVE 'sqlite://kin' TO {conn_var}\n"
|
||||
f" MOVE 'gix' TO {usr_var}\n"
|
||||
f" EXEC SQL\n"
|
||||
f" {new_inner.strip()}\n"
|
||||
@@ -185,9 +233,6 @@ class GixsqlCobolRunner:
|
||||
if hasattr(self, '_copybook_dirs') and self._copybook_dirs:
|
||||
text = self._expand_all_copies(text, self._copybook_dirs)
|
||||
|
||||
# 3. Strip ALL comment lines (* in any column 7-11)
|
||||
text = re.sub(r'^[ \t]{0,10}\*.*\n?', '', text, flags=re.MULTILINE)
|
||||
|
||||
# 4. Collapse multiple spaces between keywords
|
||||
lines = []
|
||||
for line in text.splitlines(keepends=True):
|
||||
@@ -246,8 +291,29 @@ class GixsqlCobolRunner:
|
||||
text, count=1, flags=re.MULTILINE | re.IGNORECASE
|
||||
)
|
||||
|
||||
# 8. SQLite backend: DB2 `CURRENT TIMESTAMP` → `CURRENT_TIMESTAMP` inside
|
||||
# EXEC SQL. Must run before gixpp (gixpp can split the token across
|
||||
# continuation lines, defeating the post-gixpp same-line patch).
|
||||
text = _normalize_current_timestamp(text)
|
||||
|
||||
# 9. SQLite backend: DB2 schema-qualified table refs (SCHEMA.TABLE) →
|
||||
# TABLE inside EXEC SQL. gixsql would otherwise emit
|
||||
# `FROM SALARYDB.EMP_MASTER`, which SQLite cannot resolve.
|
||||
text = _normalize_schema_qualifiers(text)
|
||||
|
||||
norm_path = src_path.parent / f"{src_path.stem}_norm.cbl"
|
||||
norm_path.write_text(text, encoding="utf-8")
|
||||
# Save a copy in runtime for diagnosis
|
||||
try:
|
||||
debug_dir = Path(__file__).parent.parent / "runtime" / src_path.stem / "pre_src"
|
||||
debug_dir.mkdir(parents=True, exist_ok=True)
|
||||
(debug_dir / f"{src_path.stem}_norm.cbl").write_text(text, encoding="utf-8")
|
||||
(debug_dir / f"{src_path.stem}_pre.cbl").write_text(
|
||||
(src_path.parent / f"{src_path.stem}_pre.cbl").read_text(encoding="utf-8"),
|
||||
encoding="utf-8"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return pre_path, norm_path
|
||||
|
||||
def preprocess(self, src_path: str | Path, out_dir: str | Path,
|
||||
@@ -273,6 +339,80 @@ class GixsqlCobolRunner:
|
||||
raise RuntimeError(f"gixpp failed (rc={r.returncode}): {err}")
|
||||
return str(out_path)
|
||||
|
||||
def _patch_sql_identifiers(self, pp_path: Path) -> None:
|
||||
"""Translate DB2 hyphenated identifiers to underscores inside GIXSQL SQL strings.
|
||||
|
||||
gixsql emits the SQL text verbatim from the COBOL source (e.g.
|
||||
"INSERT INTO EMP-MASTER (EMP-ID, ...)"), but SQLite cannot parse bare
|
||||
hyphenated identifiers. Convert '-' -> '_' only inside the SQL string
|
||||
literals (VALUE "..." lines and & "..." continuation lines), leaving
|
||||
COBOL-level identifiers (cursor names, host variables, work items) untouched.
|
||||
"""
|
||||
text = pp_path.read_text(encoding="utf-8")
|
||||
|
||||
def _fix_line(m: re.Match) -> str:
|
||||
return m.group(1) + m.group(2).replace('-', '_') + m.group(3)
|
||||
|
||||
# SQL start lines: GIXSQL ... VALUE "SQL TEXT"
|
||||
text = re.sub(
|
||||
r'^(GIXSQL.*?VALUE\s+")([^"]*)(")',
|
||||
_fix_line, text, flags=re.MULTILINE
|
||||
)
|
||||
# SQL continuation lines: GIXSQL & "SQL TEXT"
|
||||
text = re.sub(
|
||||
r'^(GIXSQL\s*&\s*")([^"]*)(")',
|
||||
_fix_line, text, flags=re.MULTILINE
|
||||
)
|
||||
# SQLite accepts CURRENT_TIMESTAMP (no space); gixsql emits CURRENT TIMESTAMP
|
||||
text = re.sub(r'\bCURRENT\s+TIMESTAMP\b', 'CURRENT_TIMESTAMP', text, flags=re.IGNORECASE)
|
||||
pp_path.write_text(text, encoding="utf-8")
|
||||
|
||||
def _patch_sqlcode_normalize(self, pp_path: Path) -> None:
|
||||
"""Inject SQLITE-constraint → -803 mapping after each SQL execution.
|
||||
|
||||
gixsql+SQLite reports duplicate-key (PRIMARY KEY / UNIQUE) violations as
|
||||
SQLCODE=-1555 / -2067 / -19, whereas the COBOL programs follow DB2
|
||||
semantics (SQLCODE = -803). Without this mapping, `IF SQLCODE = -803`
|
||||
branches are unreachable under gixsql+SQLite. The injected code is
|
||||
program-agnostic: it only rewrites the constraint-violation codes.
|
||||
"""
|
||||
text = pp_path.read_text(encoding="utf-8")
|
||||
mapping = (
|
||||
' IF SQLCODE = -1555\n'
|
||||
' MOVE -803 TO SQLCODE\n'
|
||||
' END-IF\n'
|
||||
' IF SQLCODE = -2067\n'
|
||||
' MOVE -803 TO SQLCODE\n'
|
||||
' END-IF\n'
|
||||
' IF SQLCODE = -19\n'
|
||||
' MOVE -803 TO SQLCODE\n'
|
||||
' END-IF\n'
|
||||
)
|
||||
text = re.sub(
|
||||
r'(GIXSQLEndSQL\s*\n?.*?END-CALL\.?)\s*\n(\s*)(IF SQLCODE\s+)',
|
||||
lambda m: m.group(1) + '\n' + m.group(2) + mapping + m.group(2) + m.group(3),
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
pp_path.write_text(text, encoding="utf-8")
|
||||
|
||||
def _patch_sqlcode_override(self, pp_path: Path) -> None:
|
||||
"""Inject MOVE 0 TO SQLCODE after each GIXSQLEndSQL call.
|
||||
|
||||
Workaround for gixsql DLL bug: GIXSQLExecParams/GIXSQLExec fail with
|
||||
'Can't find a connection' even though the connection is valid.
|
||||
Setting SQLCODE=0 lets the program flow through success paths for
|
||||
coverage measurement. The DB state is not validated in coverage runs.
|
||||
"""
|
||||
text = pp_path.read_text(encoding="utf-8")
|
||||
text = re.sub(
|
||||
r'(GIXSQLEndSQL\s*\n?.*?END-CALL\.?)\s*\n(\s*)(IF SQLCODE\s+(?:NOT\s+)?=\s+0)',
|
||||
r'\1\n\2 MOVE 0 TO SQLCODE\n\2\3',
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
pp_path.write_text(text, encoding="utf-8")
|
||||
|
||||
def compile(self, pp_path: str | Path, exe_path: str | Path,
|
||||
copybook_dirs: list[str | Path] | None = None,
|
||||
extra_srcs: list[str | Path] | None = None) -> GixsqlBuildResult:
|
||||
@@ -282,6 +422,19 @@ class GixsqlCobolRunner:
|
||||
exe_dir = exe_path.parent
|
||||
exe_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# SQL is now executed correctly (hyphen->underscore identifiers), so the
|
||||
# SQLCODE=0 override workaround is no longer needed. It masked real SQL
|
||||
# errors and caused infinite FETCH loops (SQLCODE forced to 0 prevents
|
||||
# "PERFORM UNTIL SQLCODE NOT = 0" from terminating at EOF=100).
|
||||
# self._patch_sqlcode_override(pp_path)
|
||||
|
||||
# Translate DB2 hyphenated identifiers -> underscores in SQL strings
|
||||
self._patch_sql_identifiers(pp_path)
|
||||
|
||||
# Map gixsql SQLite constraint codes (-1555/-2067/-19) -> DB2 -803 so
|
||||
# `IF SQLCODE = -803` branches are reachable under gixsql+SQLite.
|
||||
self._patch_sqlcode_normalize(pp_path)
|
||||
|
||||
# Functions that the preprocessed COBOL actually CALLs
|
||||
gixsql_k = [
|
||||
"-K", "GIXSQLStartSQL",
|
||||
@@ -331,7 +484,8 @@ class GixsqlCobolRunner:
|
||||
input_dir: str | Path | None = None,
|
||||
timeout: int = 30,
|
||||
cobol_lib_path: str | Path | None = None,
|
||||
env_overrides: dict[str, str] | None = None) -> GixsqlRunResult:
|
||||
env_overrides: dict[str, str] | None = None,
|
||||
command_args: list[str] | None = None) -> GixsqlRunResult:
|
||||
"""Step 3: COBOL DB プログラム実行"""
|
||||
exe_path = Path(exe_path)
|
||||
work_dir = Path(work_dir)
|
||||
@@ -388,6 +542,8 @@ class GixsqlCobolRunner:
|
||||
dst.write_bytes(f.read_bytes())
|
||||
|
||||
cmd = [str(exe_path)]
|
||||
if command_args:
|
||||
cmd.extend(command_args)
|
||||
logger.info(f" run: {' '.join(cmd)} (cwd={work_dir}, db={db_path})")
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=timeout,
|
||||
|
||||
Reference in New Issue
Block a user