280 lines
10 KiB
Python
280 lines
10 KiB
Python
"""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')
|