"""SELECT 的 WHERE 宿主变量经 MOVE 链解析到输入记录键的测试。 根因:`MOVE R02EMP-ID TO DBV-EMP-ID` 在程序里先执行,运行时 SQL 查询键是 输入记录键(R02EMP-ID)。但 build_db_input 直接读记录中的 DBV-EMP-ID (生成器的独立合成值,如 J 前缀),与输入键不一致,导致 DB 预置行查不到、 正常路径(SQLCODE=0)永远无法覆盖。修复:WHERE 宿主变量若有 MOVE 链, 应优先使用输入记录键。 """ 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_select(table="EMP-MASTER"): return [{ "type": "exec_sql_select", "table": table, "select_list": "EMP_NAME", "into_vars": ["DBV-EMPLOYEE-NAME"], "where": "EMP_ID = :DBV-EMP-ID", "where_constraints": [ {"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="} ], }] def _declared_columns(): return { "EMP-MASTER": [ {"name": "EMP-ID", "db_type": "CHAR", "size": 8}, {"name": "EMP_NAME", "db_type": "CHAR", "size": 40}, ], } def test_select_where_hostvar_moved_from_input_uses_input_key(): """WHERE 宿主变量 DBV-EMP-ID 由 MOVE R02EMP-ID 赋值时, DB 预置行主键必须用输入记录键(R02EMP-ID),而非 DBV-EMP-ID 的合成值。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), # SQL 成功路径 ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_select(), _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP-ID'].strip() == 'A0000001' def test_select_where_hostvar_no_move_keeps_record_value(): """WHERE 宿主变量没有 MOVE 链时,维持原行为(用记录中宿主变量的值)。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}] db_input = build_db_input( branch_paths, [], {}, _sql_meta_select(), _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP-ID'].strip() == 'J0000001' def test_select_where_hostvar_chain_through_two_moves(): """MOVE 链可跨多跳解析(A <- B <- C),最终用输入记录键。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'WK-EMP-ID': 'M0000001', 'DBV-EMP-ID': 'J0000001'}] assignments = { 'WK-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['WK-EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_select(), _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP-ID'].strip() == 'A0000001' def test_select_where_hostvar_skip_sql_error_path(): """SQL 失败路径(SQLCODE<>0)仍不生成预置行。""" branch_paths = [ ([('SQLCODE', '<>', '0', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_select(), _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert not rows def test_select_uses_declared_columns_with_underscore_key(): """declared_columns 以下划线键(EMP_MASTER)声明时,SELECT 行生成 也要命中,使用声明的 CHAR(8) 列宽,避免回退到 10 字符推断导致 存储值带尾随空格、与运行时 8 字符键失配。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001'}] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } declared = { "EMP_MASTER": [ {"name": "EMP_ID", "db_type": "CHAR", "size": 8}, {"name": "EMP_NAME", "db_type": "CHAR", "size": 40}, ], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_select(), declared, records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP_ID'] == 'A0000001' assert len(rows[0]['EMP_ID']) == 8 def test_update_where_hostvar_moved_from_input_uses_input_key(): """UPDATE/DELETE 的 WHERE 宿主变量同样经 MOVE 链解析到输入记录键。""" sql_meta = [{ "type": "exec_sql_update", "table": "EMP-MASTER", "columns": ["STATUS"], "host_vars": ["DBV-STATUS"], "where": "EMP_ID = :DBV-EMP-ID", "where_constraints": [ {"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="} ], }] branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), ] records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001', 'DBV-STATUS': 'M'}] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, sql_meta, _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 1 assert rows[0]['EMP-ID'].strip() == 'A0000001' def _sql_meta_two_selects(): """两个 SELECT:EMP-MASTER 在前(运行时先查),OVT-MONTHLY 在后。""" return [ { "type": "exec_sql_select", "table": "EMP-MASTER", "select_list": "EMP_NAME", "into_vars": ["DBV-EMPLOYEE-NAME"], "where": "EMP_ID = :DBV-EMP-ID", "where_constraints": [ {"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="} ], "pos": 100, }, { "type": "exec_sql_select", "table": "OVT-MONTHLY", "select_list": "SUM(OVT_HOURS)", "into_vars": ["DBV-OVT-HOURS"], "where": "EMP_ID = :DBV-EMP-ID AND YEAR_MONTH = :WRK-YEAR-MONTH", "where_constraints": [ {"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="}, {"col": "YEAR-MONTH", "type": "host_var", "host_var": "WRK-YEAR-MONTH", "op": "="}, ], "pos": 200, }, ] def _declared_two_tables(): return { "EMP-MASTER": [ {"name": "EMP-ID", "db_type": "CHAR", "size": 8}, {"name": "EMP_NAME", "db_type": "CHAR", "size": 40}, ], "OVT-MONTHLY": [ {"name": "EMP-ID", "db_type": "CHAR", "size": 8}, {"name": "YEAR-MONTH", "db_type": "CHAR", "size": 6}, ], } def test_multiple_selects_drop_last_table_for_last_ok_path(): """两个 SELECT 时,为最后一条 SQL 成功路径丢弃最后一张表(OVT)的行, 使该记录到达下游查询时无匹配行 → 覆盖下游 SELECT 的 no-data 分支 (IF SQLCODE = 0 的 ELSE 分支)。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), # path0 sql ok ([('SQLCODE', '=', '0', True)], {}), # path1 sql ok (last) ] records = [ {'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001', 'WRK-YEAR-MONTH': 'B00001', 'DBV-OVT-HOURS': '00101'}, {'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002', 'WRK-YEAR-MONTH': 'B00002', 'DBV-OVT-HOURS': '00102'}, ] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_two_selects(), _declared_two_tables(), records=records, ) emp = db_input.get('EMP-MASTER', []) ovt = db_input.get('OVT-MONTHLY', []) assert len(emp) == 2, emp # 两条路径都种 EMP(前置查询必须命中) assert len(ovt) == 1, ovt # 最后一条路径的 OVT 行被丢弃 assert emp[0]['EMP-ID'].strip() == 'A0000001' assert ovt[0]['EMP-ID'].strip() == 'A0000001' def test_single_select_keeps_all_rows(): """只有一个 SELECT 时不触发丢弃(保持原行为)。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), ([('SQLCODE', '=', '0', True)], {}), ] records = [ {'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001'}, {'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002'}, ] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_select(), _declared_columns(), records=records, ) rows = db_input.get('EMP-MASTER', []) assert len(rows) == 2, rows def test_drop_requires_two_sql_ok_paths(): """只有一条 SQL 成功路径时不丢弃最后一张表(避免该表行被清空)。""" branch_paths = [ ([('SQLCODE', '=', '0', True)], {}), # 唯一的 sql ok 路径 ([('SQLCODE', '<>', '0', True)], {}), # 失败路径 ] records = [ {'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001', 'WRK-YEAR-MONTH': 'B00001', 'DBV-OVT-HOURS': '00101'}, {'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002', 'WRK-YEAR-MONTH': 'B00002', 'DBV-OVT-HOURS': '00102'}, ] assignments = { 'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}], } db_input = build_db_input( branch_paths, [], assignments, _sql_meta_two_selects(), _declared_two_tables(), records=records, ) assert len(db_input.get('EMP-MASTER', [])) == 1 assert len(db_input.get('OVT-MONTHLY', [])) == 1, db_input.get('OVT-MONTHLY')