feat: DB管线补全 + 新增orchestrator_db/program_schema/to_sql + 清理临时脚本
This commit is contained in:
@@ -0,0 +1,601 @@
|
||||
"""GixsqlOrchestrator — DB COBOL プログラムの全6Step実行"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sqlite3
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from config import Config
|
||||
from config.program_schema import ProgramSchema, load_schema
|
||||
from cobol_testgen import extract_structure, generate_data
|
||||
from cobol_testgen.flatfile import write_all_files, write_sysin_file
|
||||
from cobol_testgen.file_io import read_output_file
|
||||
from cobol_testgen.read import preprocess, resolve_copybooks, resolve_sql_includes, parse_file_control, parse_file_section, parse_data_division, extract_data_division, scan_open_statements
|
||||
from cobol_testgen.read import strip_exec_sql_from_data_div
|
||||
from cobol_testgen.gcov import run_gcov
|
||||
from cobol_testgen.coverage import run_coverage, generate_coverage_index
|
||||
from cobol_testgen.design_mcdc import enum_paths as mcdc_enum_paths
|
||||
from cobol_testgen.to_sql import collect_sql_meta, build_db_input
|
||||
from cobol_testgen.core import extract_sql_assignments
|
||||
from cobol_testgen import expand_occurs
|
||||
import shutil
|
||||
from data.diff_result import VerificationRun, FieldResult
|
||||
from runners.gixsql_runner import GixsqlCobolRunner, GixsqlTableData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DbPipelineResult:
|
||||
"""DB 管线単体実行結果"""
|
||||
program_id: str
|
||||
step: int | float # pipeline step number
|
||||
success: bool
|
||||
message: str = ""
|
||||
data: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class GixsqlOrchestrator:
|
||||
"""6Step DB 管线オーケストレーター"""
|
||||
|
||||
def __init__(self, config: Config, program_id: str,
|
||||
cobol_src_dir: str | Path,
|
||||
copybook_dirs: list[str | Path] | None = None,
|
||||
work_dir: str | Path | None = None,
|
||||
skip_jvm: bool = True):
|
||||
self.config = config
|
||||
self.program_id = program_id
|
||||
self.cobol_src_dir = Path(cobol_src_dir)
|
||||
self.copybook_dirs = copybook_dirs or []
|
||||
self.skip_jvm = skip_jvm
|
||||
v3_root = Path(__file__).parent # cobol-java-v3/
|
||||
|
||||
# Build artifacts in temp (ASCII-only, gixpp can't handle Chinese paths)
|
||||
if work_dir is None:
|
||||
temp = Path(os.environ.get("TEMP", "C:\\Temp"))
|
||||
work_dir = temp / "gixsql_build" / program_id
|
||||
self.work_dir = Path(work_dir)
|
||||
|
||||
# Runtime data under V3 (DB, flat files, CWD)
|
||||
self.runtime_dir = v3_root / "runtime" / program_id
|
||||
|
||||
self.schema: ProgramSchema = load_schema(program_id)
|
||||
|
||||
self.runner = GixsqlCobolRunner(
|
||||
gixpp_path=config.gixsql_path,
|
||||
lib_path=config.gixsql_lib_path,
|
||||
compile_flags=config.gixsql_compile_flags,
|
||||
)
|
||||
|
||||
# Derive DB path: C:\Temp\gix\<program_id>.db (matches COBOL CONNECT TO, short enough for col 72)
|
||||
self.db_path = Path("C:/Temp/gix") / f"{self.program_id}.db"
|
||||
|
||||
# Pipeline state
|
||||
self.src_path: Optional[Path] = None
|
||||
self.pp_path: Optional[Path] = None
|
||||
self.exe_path: Optional[Path] = None
|
||||
self.java_input_path: Optional[Path] = None
|
||||
self.java_output_path: Optional[Path] = None
|
||||
self.generated_records: list[dict] = []
|
||||
self.generated_structure: dict | None = None
|
||||
|
||||
# ── Step 1: 環境整備(gixpp + compile) ──
|
||||
|
||||
def _copy_sources_to_workdir(self) -> tuple[Path, list[str]]:
|
||||
"""Copy source + copybooks to ASCII-only workdir (gixpp can't handle Chinese paths)."""
|
||||
src_dir = self.work_dir / "src"
|
||||
src_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy main source
|
||||
orig = self.cobol_src_dir / f"{self.program_id}.cbl"
|
||||
ascii_src = src_dir / f"{self.program_id}.cbl"
|
||||
if not ascii_src.exists():
|
||||
ascii_src.write_bytes(orig.read_bytes())
|
||||
self.src_path = ascii_src
|
||||
|
||||
# Copy copybooks
|
||||
flat_cpy = []
|
||||
for d in self.copybook_dirs:
|
||||
pd = Path(d)
|
||||
if pd.exists():
|
||||
for f in pd.glob("*.cpy"):
|
||||
dst = src_dir / f.name
|
||||
if not dst.exists():
|
||||
dst.write_bytes(f.read_bytes())
|
||||
flat_cpy.append(str(dst))
|
||||
|
||||
# Copy SUB programs
|
||||
sub_dirs = [self.cobol_src_dir, self.cobol_src_dir.parent / "sub"]
|
||||
for sub in self.schema.subprograms:
|
||||
found = False
|
||||
for sd in sub_dirs:
|
||||
sp = sd / f"{sub}.cbl"
|
||||
if sp.exists():
|
||||
dst = src_dir / f"{sub}.cbl"
|
||||
if not dst.exists():
|
||||
dst.write_bytes(sp.read_bytes())
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
logger.warning(f" SUB {sub}.cbl not found in {sub_dirs}")
|
||||
|
||||
return src_dir, flat_cpy
|
||||
|
||||
def step1_setup_environment(self) -> DbPipelineResult:
|
||||
"""gixpp 前処理 → cobc コンパイル"""
|
||||
try:
|
||||
ascii_dir, flat_cpy = self._copy_sources_to_workdir()
|
||||
src = ascii_dir / f"{self.program_id}.cbl"
|
||||
|
||||
pp = self.runner.preprocess(src, self.work_dir / "preprocessed",
|
||||
copybook_dirs=[ascii_dir])
|
||||
self.pp_path = Path(pp)
|
||||
|
||||
exe = self.work_dir / "bin" / f"{self.program_id}.exe"
|
||||
extra_srcs = []
|
||||
for sub in self.schema.subprograms:
|
||||
sp = ascii_dir / f"{sub}.cbl"
|
||||
if sp.exists():
|
||||
extra_srcs.append(sp)
|
||||
|
||||
result = self.runner.compile(
|
||||
pp, exe,
|
||||
copybook_dirs=[ascii_dir],
|
||||
extra_srcs=extra_srcs,
|
||||
)
|
||||
if result.success:
|
||||
self.exe_path = Path(result.exe_path)
|
||||
return DbPipelineResult(
|
||||
self.program_id, 1, result.success,
|
||||
message=result.log[:200],
|
||||
data={"exe_path": str(exe), "log": result.log[:500]},
|
||||
)
|
||||
except Exception as e:
|
||||
return DbPipelineResult(self.program_id, 1, False, str(e))
|
||||
|
||||
# ── Step 2: 入力データ生成 ──
|
||||
|
||||
def step2_generate_inputs(self) -> DbPipelineResult:
|
||||
"""テストデータ生成 + フラットファイル出力 + DB初期化"""
|
||||
try:
|
||||
src_text = self.src_path.read_text(encoding="utf-8-sig")
|
||||
# Use the pre-gixpp source for Lark parsing (gixpp output contains SQLCA etc.)
|
||||
parse_text = self.pp_path.read_text(encoding="utf-8") if self.pp_path and self.pp_path.exists() else src_text
|
||||
|
||||
# COBOL 解析 + テストデータ生成
|
||||
cbd = [str(d) for d in self.copybook_dirs]
|
||||
st = extract_structure(src_text, copybook_dirs=cbd)
|
||||
self.generated_structure = st
|
||||
recs = generate_data(src_text, st, copybook_dirs=cbd)
|
||||
|
||||
# Post-process: link R02 cancel APPL-IDs to matching R01 insert APPL-IDs
|
||||
for rec in recs:
|
||||
if 'R02APPL-ID' in rec and 'R01APPL-ID' in rec:
|
||||
rec['R02APPL-ID'] = rec['R01APPL-ID']
|
||||
|
||||
# DB 初期データ構築: single DB under V3 runtime/ dir
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_database(self.db_path)
|
||||
|
||||
# DB 初期行投入(DELETE/UPDATE が作用する行、SELECT が返す行)
|
||||
self._populate_database(self.db_path, src_text, recs)
|
||||
|
||||
# フラットファイル書き出し
|
||||
input_dir = self.work_dir / "input"
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
flats = write_all_files(recs, src_text, input_dir,
|
||||
copybook_dirs=[str(d) for d in self.copybook_dirs])
|
||||
|
||||
# SYSIN 設定ファイル生成(プログラム固有のカード形式)
|
||||
sysin_path = write_sysin_file(recs, src_text, input_dir,
|
||||
copybook_dirs=[str(d) for d in self.copybook_dirs])
|
||||
if sysin_path:
|
||||
logger.info(f" SYSIN file written: {sysin_path}")
|
||||
flats.append(("SYSIN", sysin_path, 0))
|
||||
|
||||
self.generated_records = recs
|
||||
|
||||
return DbPipelineResult(
|
||||
self.program_id, 2, True,
|
||||
data={"records": len(recs), "flat_files": len(flats),
|
||||
"db_path": str(self.db_path)},
|
||||
)
|
||||
except Exception as e:
|
||||
return DbPipelineResult(self.program_id, 2, False, str(e))
|
||||
|
||||
# ── Step 3: COBOL 実行 ──
|
||||
|
||||
def step3_run_cobol(self) -> DbPipelineResult:
|
||||
"""COBOL DB プログラム実行"""
|
||||
if not self.exe_path or not self.exe_path.exists():
|
||||
return DbPipelineResult(self.program_id, 3, False,
|
||||
"exe not found (run step1 first)")
|
||||
# Subprogram DLLs are in cobol-tna-system/bin/
|
||||
cobol_bin = Path(self.cobol_src_dir).parent / "bin"
|
||||
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = self.runner.run(
|
||||
self.exe_path, self.runtime_dir,
|
||||
self.db_path,
|
||||
input_dir=self.work_dir / "input",
|
||||
cobol_lib_path=str(cobol_bin) if cobol_bin.exists() else None,
|
||||
)
|
||||
return DbPipelineResult(
|
||||
self.program_id, 3, result.success,
|
||||
data={"returncode": result.returncode, "log": result.log[:500]},
|
||||
)
|
||||
|
||||
# ── カバレッジレポート(パイプライン外、オプション) ──
|
||||
|
||||
def generate_coverage_report(self,
|
||||
output_dir: str | Path | None = None) -> DbPipelineResult:
|
||||
"""COBOL 実行後:gcov データ収集 + 静的パスとマージし HTML レポート"""
|
||||
try:
|
||||
if not self.exe_path or not self.exe_path.exists():
|
||||
return DbPipelineResult(self.program_id, 0, False,
|
||||
"exe not found (run step3 first)")
|
||||
if output_dir is None:
|
||||
v3_root = Path(__file__).parent
|
||||
output_dir = v3_root / "reports" / self.program_id / "coverage"
|
||||
output_dir = Path(output_dir)
|
||||
|
||||
# 1. Copy .gcno + .gcda from CWD (compile-time cwd) to runtime_dir
|
||||
# cobc generates .gcno in CWD; at runtime, program writes .gcda to same CWD
|
||||
gcno_gcda_count = 0
|
||||
for ext in (".gcno", ".gcda"):
|
||||
for f in Path.cwd().glob(f"*{ext}"):
|
||||
if f.stat().st_size > 0:
|
||||
shutil.copy2(str(f), str(self.runtime_dir / f.name))
|
||||
gcno_gcda_count += 1
|
||||
if gcno_gcda_count == 0:
|
||||
return DbPipelineResult(self.program_id, 0, False,
|
||||
f"no .gcno/.gcda found in CWD (--coverage missing?)")
|
||||
|
||||
# 3. Parse gcov data
|
||||
gcov_data = run_gcov(f"{self.program_id}_pp", str(self.runtime_dir))
|
||||
if not gcov_data:
|
||||
gcov_data = run_gcov(self.program_id, str(self.runtime_dir))
|
||||
for sub in self.schema.subprograms:
|
||||
sd = run_gcov(sub, str(self.runtime_dir))
|
||||
if sd:
|
||||
gcov_data.update(sd)
|
||||
|
||||
# 4. Static branch tree from step2
|
||||
st = self.generated_structure
|
||||
branch_tree = st.get("branch_tree_obj") if st else None
|
||||
if not branch_tree:
|
||||
return DbPipelineResult(self.program_id, 0, True,
|
||||
data={"gcov_lines": len(gcov_data),
|
||||
"note": "no branch tree — gcov data only"})
|
||||
|
||||
# 5. Re-parse fields (same as generate_data)
|
||||
src_text = self.src_path.read_text(encoding="utf-8-sig")
|
||||
cbd = [str(d) for d in self.copybook_dirs]
|
||||
pp = preprocess(src_text, extra_search_paths=cbd)
|
||||
data_div = extract_data_division(pp)
|
||||
data_fields = parse_data_division(data_div) if data_div else []
|
||||
fdict = []
|
||||
for idx, f in enumerate(data_fields):
|
||||
entry = {
|
||||
'name': f.name, 'level': f.level, 'pic': f.pic,
|
||||
'pic_info': {
|
||||
'type': f.pic_info.type if f.pic_info else 'unknown',
|
||||
'digits': f.pic_info.digits if f.pic_info else 0,
|
||||
'decimal': f.pic_info.decimal if f.pic_info else 0,
|
||||
'length': f.pic_info.length if f.pic_info else 0,
|
||||
'signed': f.pic_info.signed if f.pic_info else False,
|
||||
},
|
||||
'section': f.section, 'occurs': f.occurs_count,
|
||||
'occurs_depending': f.occurs_depending,
|
||||
'redefines': f.redefines, 'usage': f.usage,
|
||||
}
|
||||
if f.is_88:
|
||||
entry['is_88'] = True
|
||||
entry['parent'] = f.parent
|
||||
fdict.append(entry)
|
||||
fdict = expand_occurs(fdict)
|
||||
|
||||
# 6. Enumerate paths
|
||||
branch_paths = mcdc_enum_paths(branch_tree, fdict)
|
||||
|
||||
# 7. Read preprocessed source for gcov line number matching
|
||||
gcov_source = None
|
||||
if self.pp_path and self.pp_path.exists():
|
||||
gcov_source = self.pp_path.read_text(encoding="utf-8")
|
||||
|
||||
# 8. Generate merged HTML (use gcov_source for line numbers)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
prefix = str(output_dir / self.program_id)
|
||||
cov_result = run_coverage(
|
||||
branch_tree, branch_paths, fdict,
|
||||
src_text, prefix,
|
||||
index_relpath="index.html",
|
||||
gcov_data=gcov_data or None,
|
||||
gcov_source=gcov_source,
|
||||
)
|
||||
generate_coverage_index([cov_result], str(output_dir.parent))
|
||||
|
||||
# Clean up .gcno/.gcda from CWD (avoid accumulation)
|
||||
for ext in (".gcno", ".gcda"):
|
||||
for f in Path.cwd().glob(f"*{ext}"):
|
||||
try:
|
||||
f.unlink()
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
total = cov_result.get("total_branches", 0)
|
||||
covered = cov_result.get("covered_branches", 0)
|
||||
pct = covered / total * 100 if total else 0
|
||||
self._last_coverage_dict = cov_result
|
||||
return DbPipelineResult(
|
||||
self.program_id, 0, True,
|
||||
data={
|
||||
"gcov_lines": len(gcov_data),
|
||||
"coverage": f"{covered}/{total} ({pct:.1f}%)",
|
||||
"reports": str(output_dir),
|
||||
"_cov_dict": cov_result,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("generate_coverage_report failed")
|
||||
return DbPipelineResult(self.program_id, 0, False, str(e))
|
||||
|
||||
# ── Step 4: DB → Java 中介データ ──
|
||||
|
||||
def step4_extract_intermediate(self) -> DbPipelineResult:
|
||||
"""SQLite → JSON 中介データ抽出(Step 4: DB→Java中介データ)"""
|
||||
if not self.db_path or not self.db_path.exists():
|
||||
return DbPipelineResult(self.program_id, 4, False,
|
||||
"db not found (run step3 first)")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(self.db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Read from actual COBOL SQL tables (using sql_name or name)
|
||||
output_tables = {}
|
||||
for table in self.schema.db_tables:
|
||||
sql_name = table.sql_name or table.name
|
||||
try:
|
||||
rows = conn.execute(f"SELECT * FROM [{sql_name}]").fetchall()
|
||||
output_tables[table.name] = [dict(r) for r in rows]
|
||||
except sqlite3.OperationalError:
|
||||
output_tables[table.name] = []
|
||||
|
||||
conn.close()
|
||||
|
||||
w01_path = self.work_dir / "intermediate" / f"{self.program_id}_W01.json"
|
||||
w01_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
meta = {
|
||||
"program_id": self.program_id,
|
||||
"tables": output_tables,
|
||||
}
|
||||
w01_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2))
|
||||
self.java_input_path = w01_path
|
||||
|
||||
return DbPipelineResult(
|
||||
self.program_id, 4, True,
|
||||
data={"tables": len(output_tables), "w01_path": str(w01_path)},
|
||||
)
|
||||
except Exception as e:
|
||||
return DbPipelineResult(self.program_id, 4, False, str(e))
|
||||
|
||||
# ── Step 5: Java 実行 ──
|
||||
|
||||
def step5_run_java(self, java_cmd: str = "java",
|
||||
java_jar: str | Path | None = None) -> DbPipelineResult:
|
||||
"""Java プログラム実行"""
|
||||
if not self.java_input_path or not self.java_input_path.exists():
|
||||
return DbPipelineResult(self.program_id, 5, False,
|
||||
"intermediate data not found (run step4 first)")
|
||||
|
||||
java_out = self.work_dir / "java_output"
|
||||
java_out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if java_jar:
|
||||
cmd = [java_cmd, "-jar", str(java_jar),
|
||||
"-i", str(self.java_input_path),
|
||||
"-o", str(java_out)]
|
||||
else:
|
||||
cmd = [java_cmd, "-version"]
|
||||
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=60)
|
||||
log = (r.stdout.decode("utf-8", "replace") + "\n" +
|
||||
r.stderr.decode("utf-8", "replace"))
|
||||
ok = r.returncode == 0
|
||||
self.java_output_path = java_out
|
||||
return DbPipelineResult(
|
||||
self.program_id, 5, ok,
|
||||
data={"returncode": r.returncode, "log": log[:500]},
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return DbPipelineResult(self.program_id, 5, False, "Java timeout")
|
||||
|
||||
# ── Step 6: 検証 ──
|
||||
|
||||
def step6_verify(self) -> VerificationRun:
|
||||
"""Java 出力と COBOL 期待値を比較"""
|
||||
vr = VerificationRun(
|
||||
program=self.program_id,
|
||||
runner="gixsql",
|
||||
gixsql_version="0.9.1",
|
||||
sqlite_path=str(self.db_path) if self.db_path else "",
|
||||
step_reached=6,
|
||||
)
|
||||
|
||||
if self.db_path and self.db_path.exists():
|
||||
after_tables = self.runner.read_db_tables(
|
||||
self.db_path,
|
||||
[t.name for t in self.schema.db_tables],
|
||||
)
|
||||
for table_data in after_tables:
|
||||
vr.debug[f"table_{table_data.table_name}_rows"] = len(table_data.rows)
|
||||
|
||||
if self.java_output_path and self.java_output_path.exists():
|
||||
java_files = list(self.java_output_path.glob("*.txt")) + \
|
||||
list(self.java_output_path.glob("*.json"))
|
||||
vr.debug["java_output_files"] = [str(f) for f in java_files]
|
||||
vr.fields_matched = len(java_files)
|
||||
|
||||
vr.exit_code = 0 if vr.fields_mismatched == 0 else 1
|
||||
vr.status = "PASS" if vr.exit_code == 0 else "MISMATCH"
|
||||
return vr
|
||||
|
||||
# ── 全Step一括実行 ──
|
||||
|
||||
def run_all(self, skip_steps: set[int] | None = None,
|
||||
generate_coverage: bool = True) -> VerificationRun:
|
||||
"""Step 1 → 6 を順次実行(skip_jvm=True で Step 5/6 をスキップ)"""
|
||||
skip = set(skip_steps or [])
|
||||
if self.skip_jvm:
|
||||
skip.update({5, 6})
|
||||
steps = [
|
||||
(1, self.step1_setup_environment),
|
||||
(2, self.step2_generate_inputs),
|
||||
(3, self.step3_run_cobol),
|
||||
(4, self.step4_extract_intermediate),
|
||||
]
|
||||
if not self.skip_jvm:
|
||||
steps.extend([
|
||||
(5, self.step5_run_java),
|
||||
(6, self.step6_verify),
|
||||
])
|
||||
|
||||
results = []
|
||||
last_step = max(s for s, _ in steps)
|
||||
for step_num, step_fn in steps:
|
||||
if step_num in skip:
|
||||
continue
|
||||
logger.info(f" Step {step_num}...")
|
||||
result = step_fn()
|
||||
results.append(result)
|
||||
if not result.success and step_num < last_step:
|
||||
vr = VerificationRun(
|
||||
program=self.program_id, runner="gixsql",
|
||||
status="BLOCKED", exit_code=2,
|
||||
step_reached=step_num,
|
||||
debug={"step_results": [r.__dict__ for r in results]},
|
||||
)
|
||||
return vr
|
||||
|
||||
# Optional coverage report (non-blocking, not part of numbered pipeline)
|
||||
cv_flags = getattr(self.config, 'gixsql_compile_flags', '')
|
||||
if '--coverage' in cv_flags and generate_coverage:
|
||||
self.generate_coverage_report()
|
||||
|
||||
if not self.skip_jvm:
|
||||
vr = results[-1] # step6_verify returned VerificationRun
|
||||
vr.debug["step_results"] = [r.__dict__ for r in results[:-1] if r]
|
||||
else:
|
||||
vr = VerificationRun(
|
||||
program=self.program_id, runner="gixsql",
|
||||
status="PASS", exit_code=0,
|
||||
step_reached=last_step,
|
||||
debug={"step_results": [r.__dict__ for r in results]},
|
||||
)
|
||||
return vr
|
||||
|
||||
# ── Internal helpers ──
|
||||
|
||||
def _init_database(self, db_path: Path):
|
||||
"""Create tables from schema + COBOL EXEC SQL table definitions."""
|
||||
self._create_tables(db_path)
|
||||
|
||||
def _create_tables(self, db_path: Path):
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
for table in self.schema.db_tables:
|
||||
col_defs = []
|
||||
pk_cols = []
|
||||
for col in table.columns:
|
||||
col_defs.append(f"[{col.name}] {col.type}")
|
||||
if col.primary_key:
|
||||
pk_cols.append(f"[{col.name}]")
|
||||
if pk_cols:
|
||||
col_defs.append(f"PRIMARY KEY ({', '.join(pk_cols)})")
|
||||
ddl = f"CREATE TABLE IF NOT EXISTS [{table.name}] (\n " + \
|
||||
",\n ".join(col_defs) + "\n)"
|
||||
conn.execute(ddl)
|
||||
# If sql_name differs, also create the COBOL-visible SQL table name
|
||||
if table.sql_name and table.sql_name != table.name:
|
||||
conn.execute(ddl.replace(f"[{table.name}]", f"[{table.sql_name}]"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f" DB initialized: {db_path}")
|
||||
|
||||
def _populate_database(self, db_path: Path, src_text: str, records: list[dict]):
|
||||
"""テストデータから DB 初期行を生成し挿入する。"""
|
||||
from cobol_testgen.pipeline_bridge import build_branch_tree_fallback
|
||||
from cobol_testgen.read import extract_procedure_division
|
||||
|
||||
cbd = [str(d) for d in self.copybook_dirs]
|
||||
|
||||
src_resolved = resolve_copybooks(src_text, ".", extra_search_paths=cbd)
|
||||
src_resolved = resolve_sql_includes(src_resolved, ".")
|
||||
preprocessed = preprocess(src_resolved)
|
||||
|
||||
data_div = extract_data_division(preprocessed)
|
||||
data_fields = parse_data_division(data_div) if data_div else []
|
||||
fields_dict = []
|
||||
for f in data_fields:
|
||||
fields_dict.append({
|
||||
'name': f.name, 'level': f.level, 'pic': f.pic,
|
||||
'pic_info': {
|
||||
'type': f.pic_info.type if f.pic_info else 'unknown',
|
||||
'digits': f.pic_info.digits if f.pic_info else 0,
|
||||
'decimal': f.pic_info.decimal if f.pic_info else 0,
|
||||
'length': f.pic_info.length if f.pic_info else 0,
|
||||
'signed': f.pic_info.signed if f.pic_info else False,
|
||||
},
|
||||
'section': f.section, 'occurs': f.occurs_count,
|
||||
'occurs_depending': f.occurs_depending,
|
||||
'value': f.value, 'values': f.values,
|
||||
'redefines': f.redefines, 'usage': f.usage,
|
||||
})
|
||||
fields_dict = expand_occurs(fields_dict)
|
||||
|
||||
proc_div = extract_procedure_division(preprocessed)
|
||||
branch_tree, assignments = build_branch_tree_fallback(proc_div, fields_dict)
|
||||
|
||||
# Merge SQL assignments from original source
|
||||
sql_assigns = extract_sql_assignments(src_text)
|
||||
for tgt, asgn_list in sql_assigns.items():
|
||||
for asgn in asgn_list:
|
||||
assignments.setdefault(tgt, []).append(asgn)
|
||||
|
||||
branch_paths = mcdc_enum_paths(branch_tree, fields_dict)
|
||||
|
||||
data_div2, declared_columns = strip_exec_sql_from_data_div(data_div)
|
||||
sql_meta = collect_sql_meta(assignments, declared_columns)
|
||||
if not sql_meta:
|
||||
logger.info(" No SQL metadata found, skipping DB population")
|
||||
return
|
||||
|
||||
db_input = build_db_input(
|
||||
branch_paths, fields_dict, assignments,
|
||||
sql_meta, declared_columns,
|
||||
records=records,
|
||||
)
|
||||
if not db_input:
|
||||
logger.info(" No DB input rows generated")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
for table_name, rows in db_input.items():
|
||||
if not rows:
|
||||
logger.info(f" Table {table_name}: 0 initial rows (will be created at runtime)")
|
||||
continue
|
||||
col_names = list(rows[0].keys())
|
||||
placeholders = ", ".join("?" for _ in col_names)
|
||||
quoted_cols = ", ".join(f"[{c}]" for c in col_names)
|
||||
sql = f"INSERT OR IGNORE INTO [{table_name}] ({quoted_cols}) VALUES ({placeholders})"
|
||||
conn.executemany(sql, [tuple(r.get(c, "") for c in col_names) for r in rows])
|
||||
logger.info(f" Table {table_name}: {len(rows)} initial rows inserted")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f" DB populated: {db_path}")
|
||||
Reference in New Issue
Block a user