"""INSERT 表主键碰撞预置行生成测试(T2)。 gixsql+SQLite 下,`IF SQLCODE = -803` 分支要求运行时 INSERT 失败。 让 INSERT 失败的唯一途径是 DB 中已存在同主键行。因此 build_db_input 必须为"要求 SQLCODE≠0 的路径"生成主键碰撞预置行(主键值取自该记录)。 """ import sys, os sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) from cobol_testgen.to_sql import build_db_input def _sql_meta_insert(table="EMP-MASTER", columns=("EMP-ID", "EMP-NAME"), host_vars=("DBV-EMP-ID", "DBV-EMP-NAME")): return [{ "type": "exec_sql_insert", "table": table, "columns": list(columns), "host_vars": list(host_vars), "sql_text": f"INSERT INTO {table} ({', '.join(columns)}) VALUES ({', '.join(':'+h for h in host_vars)})", }] def test_insert_sql_ok_path_skips_collision_row(): """要求 SQLCODE=0(INSERT 成功)的路径 → 不生成碰撞预置行""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), # SQL ok → skip ] records = [{'DBV-EMP-ID': 'A0000001', 'DBV-EMP-NAME': 'NAME'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_insert(), {'EMP-MASTER': [{'name': 'EMP-ID', 'db_type': 'CHAR', 'size': 8}]}, records=records, insert_pk={'EMP-MASTER': ['EMP-ID']}, ) assert 'EMP-MASTER' not in db_input or db_input['EMP-MASTER'] == [] def test_insert_error_path_generates_pk_collision_row(): """要求 SQLCODE<>0(INSERT 失败)的路径 → 生成主键碰撞预置行""" branch_paths = [ ([('SQLCODE', '<>', '0', True)], {}), # SQL fails → collision row ] records = [{'DBV-EMP-ID': 'A0000001', 'DBV-EMP-NAME': 'NAME'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_insert(), {'EMP-MASTER': [{'name': 'EMP-ID', 'db_type': 'CHAR', 'size': 8}, {'name': 'EMP-NAME', 'db_type': 'CHAR', 'size': 40}]}, records=records, insert_pk={'EMP-MASTER': ['EMP-ID']}, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP-ID'] == 'A0000001' def test_insert_error_path_uses_record_host_var_value(): """碰撞行主键取自记录中对应 host_var 的值""" branch_paths = [ ([('SQLCODE', '=', '-803', True)], {}), ] records = [{'DBV-EMP-ID': 'Z9999999', 'DBV-EMP-NAME': 'DUPE'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_insert(), {'EMP-MASTER': [{'name': 'EMP-ID', 'db_type': 'CHAR', 'size': 8}]}, records=records, insert_pk={'EMP-MASTER': ['EMP-ID']}, ) rows = db_input.get('EMP-MASTER', []) assert rows and rows[0]['EMP-ID'] == 'Z9999999' def test_insert_collision_uses_input_record_pk_not_hostvar(): """碰撞行主键用输入记录主键(R02EMP-ID),而非 expected host_var 值。 generate_records 的 rec 中 DBV-EMP-ID 可能是 DB 预置值(W前缀)或 expected 值,而运行时实际 INSERT 的是输入记录主键(R02EMP-ID)。 碰撞行必须与输入主键一致,运行时 INSERT 才会失败。 """ branch_paths = [ ([('SQLCODE', '=', '-803', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'W0000001'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_insert(), {'EMP-MASTER': [{'name': 'EMP-ID', 'db_type': 'CHAR', 'size': 8}]}, records=records, insert_pk={'EMP-MASTER': ['EMP-ID']}, ) rows = db_input.get('EMP-MASTER', []) assert rows and rows[0]['EMP-ID'] == 'A0000001' def _sql_meta_insert_salary_results(): """KYU06UPD 的 INSERT 语句形态(复合主键 EMP-ID + YEAR-MONTH)。""" return [{ "type": "exec_sql_insert", "table": "SALARY-RESULTS", "columns": ["EMP-ID", "YEAR-MONTH", "GROSS-PAYMENT"], "host_vars": ["DBV-EMP-ID", "DBV-YEAR-MONTH", "DBV-GROSS-PAYMENT"], "sql_text": ("INSERT INTO SALARY-RESULTS (EMP-ID, YEAR-MONTH, GROSS-PAYMENT) " "VALUES (:DBV-EMP-ID, :DBV-YEAR-MONTH, :DBV-GROSS-PAYMENT)"), }] def _declared_salary_results(): return { "SALARY-RESULTS": [ {"name": "EMP-ID", "db_type": "CHAR", "size": 8}, {"name": "YEAR-MONTH", "db_type": "CHAR", "size": 6}, {"name": "GROSS-PAYMENT", "db_type": "DECIMAL", "size": 9}, ], } def _kyu06_assignments(): """KYU06UPD 的主键 MOVE 链: DBV-EMP-ID ← R02EMP-ID(输入键) DBV-YEAR-MONTH ← CNS-YEAR-MONTH-PARM ← WRK-YEAR-MONTH(ACCEPT + IF = SPACES 默认 '202605')""" return { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], 'DBV-YEAR-MONTH': [{'type': 'move', 'source_vars': ['CNS-YEAR-MONTH-PARM']}], 'CNS-YEAR-MONTH-PARM': [{'type': 'move', 'source_vars': ['WRK-YEAR-MONTH']}], 'WRK-YEAR-MONTH': [{'type': 'accept', 'from': 'USER'}, {'type': 'move_literal', 'literal': '202605'}], } def test_insert_collision_composite_pk_matches_runtime_keys(): """复合主键碰撞行必须等于运行时 INSERT 键: EMP-ID 用输入记录键(R02EMP-ID);YEAR-MONTH 用 ACCEPT 后 IF = SPACES 的默认字面量('202605'),而非记录中的合成值('L00007')。 运行时 INSERT 键是 (A0000007, '202605'),碰撞行若用合成 YEAR-MONTH 则永不碰撞,-803 分支永远不可达。 """ branch_paths = [ ([('SQLCODE', '=', '-803', True)], {}), ] records = [{ 'R02EMP-ID': 'A0000007', 'DBV-EMP-ID': 'M0000007', 'DBV-YEAR-MONTH': 'N00007', 'WRK-YEAR-MONTH': 'L00007', }] db_input = build_db_input( branch_paths, [], _kyu06_assignments(), _sql_meta_insert_salary_results(), _declared_salary_results(), records=records, insert_pk={'SALARY-RESULTS': ['EMP_ID', 'YEAR_MONTH'], 'SALARY_RESULTS': ['EMP_ID', 'YEAR_MONTH']}, ) rows = db_input.get('SALARY-RESULTS', []) assert len(rows) == 1 assert rows[0]['EMP-ID'] == 'A0000007' assert rows[0]['YEAR-MONTH'] == '202605' def test_insert_collision_year_month_not_falling_back_to_emp_id(): """无 MOVE 链/ACCEPT 默认时,YEAR-MONTH 碰撞列不应回退到 EMP-ID 输入字段 (旧 bug 会取 R02EMP-ID 截断成 'A00000'),而应使用宿主变量自身记录值。""" branch_paths = [ ([('SQLCODE', '=', '-803', True)], {}), ] records = [{'R02EMP-ID': 'A0000007', 'DBV-EMP-ID': 'M0000007', 'DBV-YEAR-MONTH': 'N00007'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_insert_salary_results(), _declared_salary_results(), records=records, insert_pk={'SALARY-RESULTS': ['EMP_ID', 'YEAR_MONTH'], 'SALARY_RESULTS': ['EMP_ID', 'YEAR_MONTH']}, ) rows = db_input.get('SALARY-RESULTS', []) assert len(rows) == 1 assert rows[0]['YEAR-MONTH'] == 'N00007'