From 85b227278e813128eda4b9d67368045c2f3c4961 Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 11:50:53 +0100 Subject: [PATCH 1/7] Fixed empty/only-comments AST generation --- src/vtlengine/AST/ASTConstructor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vtlengine/AST/ASTConstructor.py b/src/vtlengine/AST/ASTConstructor.py index 4954cad1c..994661b16 100644 --- a/src/vtlengine/AST/ASTConstructor.py +++ b/src/vtlengine/AST/ASTConstructor.py @@ -65,7 +65,10 @@ def visitStart(self, ctx: Parser.StartContext): for statement in statements: statements_nodes.append(self.visitStatement(statement)) - token_info = extract_token_info(ctx) + if ctx.stop is None: + token_info = {"column_start": 0, "column_stop": 0, "line_start": 1, "line_stop": 1} + else: + token_info = extract_token_info(ctx) start_node = Start(children=statements_nodes, **token_info) From e1631c4ddaa82156d3a10cb9d11da1665930214e Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 11:51:24 +0100 Subject: [PATCH 2/7] Added related tests --- tests/AST/test_AST.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/AST/test_AST.py b/tests/AST/test_AST.py index bf49fd12e..e5d83bd31 100644 --- a/tests/AST/test_AST.py +++ b/tests/AST/test_AST.py @@ -948,3 +948,26 @@ def test_rule_name_not_in_ruleset(): """ ast = create_ast(text=script) assert len(ast.children) == 1 + + +empty_script_params = [ + "", + "//Comment", + "/*Comment*/", +] + + +@pytest.mark.parametrize("script", empty_script_params) +def test_create_ast_empty_script(script): + ast = create_ast(text=script) + assert isinstance(ast, Start) + assert ast.children == [] + + +@pytest.mark.parametrize("script", empty_script_params) +def test_create_ast_with_comments_empty_script(script): + from vtlengine.AST import Comment + + ast = create_ast_with_comments(text=script) + assert isinstance(ast, Start) + assert all(isinstance(child, Comment) for child in ast.children) From 4a09b90babe41fa7619bf328bede9288001683e8 Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 13:18:56 +0100 Subject: [PATCH 3/7] Fixed errorlevel as boolean handling on ASTString --- .../AST/ASTConstructorModules/Terminals.py | 5 ++++- src/vtlengine/AST/ASTString.py | 16 ++++++++-------- src/vtlengine/AST/__init__.py | 12 ++++++------ src/vtlengine/Operators/Validation.py | 14 ++++++++------ 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/vtlengine/AST/ASTConstructorModules/Terminals.py b/src/vtlengine/AST/ASTConstructorModules/Terminals.py index 0f0f3d24e..49c6cb756 100644 --- a/src/vtlengine/AST/ASTConstructorModules/Terminals.py +++ b/src/vtlengine/AST/ASTConstructorModules/Terminals.py @@ -635,7 +635,10 @@ def visitErCode(self, ctx: Parser.ErCodeContext): ctx_list = list(ctx.getChildren()) try: - return str(self.visitConstant(ctx_list[1]).value) + value = self.visitConstant(ctx_list[1]).value + if isinstance(value, bool): + return value + return str(value) except Exception: raise Exception(f"Error code must be a string, line {ctx_list[1].getSymbol().line}") diff --git a/src/vtlengine/AST/ASTString.py b/src/vtlengine/AST/ASTString.py index 71ad5ccd0..e70b68e96 100644 --- a/src/vtlengine/AST/ASTString.py +++ b/src/vtlengine/AST/ASTString.py @@ -134,10 +134,10 @@ def visit_HRuleset(self, node: AST.HRuleset) -> None: self.vtl_script += f"define hierarchical ruleset {node.name}({signature}) is{nl}" for _i, rule in enumerate(node.rules): rule_str = f"{tab}{self.visit(rule)}" - if rule.erCode: + if rule.erCode is not None: rule_str += f"{nl}{tab}errorcode {_handle_literal(rule.erCode)}" - if rule.erLevel: - rule_str += f"{nl}{tab}errorlevel {rule.erLevel}" + if rule.erLevel is not None: + rule_str += f"{nl}{tab}errorlevel {_handle_literal(rule.erLevel)}" rules_strs.append(rule_str) rules_sep = f";{nl * 2}" if len(rules_strs) > 1 else "" rules = rules_sep.join(rules_strs) @@ -146,10 +146,10 @@ def visit_HRuleset(self, node: AST.HRuleset) -> None: else: for rule in node.rules: rule_str = self.visit(rule) - if rule.erCode: + if rule.erCode is not None: rule_str += f" errorcode {_handle_literal(rule.erCode)}" - if rule.erLevel: - rule_str += f" errorlevel {rule.erLevel}" + if rule.erLevel is not None: + rule_str += f" errorlevel {_handle_literal(rule.erLevel)}" rules_strs.append(rule_str) rules_sep = "; " if len(rules_strs) > 1 else "" rules = rules_sep.join(rules_strs) @@ -195,7 +195,7 @@ def visit_DPRule(self, node: AST.DPRule) -> str: if node.erCode is not None: lines.append(f"{tab * 3}errorcode {_handle_literal(node.erCode)}") if node.erLevel is not None: - lines.append(f"{tab * 3}errorlevel {node.erLevel}") + lines.append(f"{tab * 3}errorlevel {_handle_literal(node.erLevel)}") return nl.join(lines) else: vtl_script = "" @@ -205,7 +205,7 @@ def visit_DPRule(self, node: AST.DPRule) -> str: if node.erCode is not None: vtl_script += f" errorcode {_handle_literal(node.erCode)}" if node.erLevel is not None: - vtl_script += f" errorlevel {node.erLevel}" + vtl_script += f" errorlevel {_handle_literal(node.erLevel)}" return vtl_script def visit_DPRIdentifier(self, node: AST.DPRIdentifier) -> str: diff --git a/src/vtlengine/AST/__init__.py b/src/vtlengine/AST/__init__.py index 990a54971..36600c2ac 100644 --- a/src/vtlengine/AST/__init__.py +++ b/src/vtlengine/AST/__init__.py @@ -484,8 +484,8 @@ class Validation(AST): op: str validation: AST - error_code: Optional[str] - error_level: Optional[Union[int, str]] + error_code: Optional[Union[str, int, float, bool]] + error_level: Optional[Union[str, int, float, bool]] imbalance: Optional[AST] invalid: bool @@ -631,8 +631,8 @@ class HRule(AST): name: Optional[str] rule: HRBinOp - erCode: Optional[str] - erLevel: Optional[Union[int, str]] + erCode: Optional[Union[str, int, float, bool]] + erLevel: Optional[Union[str, int, float, bool]] __eq__ = AST.ast_equality @@ -645,8 +645,8 @@ class DPRule(AST): name: Optional[str] rule: HRBinOp - erCode: Optional[str] - erLevel: Optional[Union[int, str]] + erCode: Optional[Union[str, int, float, bool]] + erLevel: Optional[Union[str, int, float, bool]] __eq__ = AST.ast_equality diff --git a/src/vtlengine/Operators/Validation.py b/src/vtlengine/Operators/Validation.py index bba9735d4..88d7c6964 100644 --- a/src/vtlengine/Operators/Validation.py +++ b/src/vtlengine/Operators/Validation.py @@ -26,8 +26,8 @@ def validate( cls, validation_element: Dataset, imbalance_element: Optional[Dataset], - error_code: Optional[str], - error_level: Optional[Union[int, str]], + error_code: Optional[Union[str, int, float, bool]], + error_level: Optional[Union[str, int, float, bool]], invalid: bool, ) -> Dataset: dataset_name = VirtualCounter._new_ds_name() @@ -91,8 +91,8 @@ def evaluate( cls, validation_element: Dataset, imbalance_element: Optional[Dataset], - error_code: Optional[str], - error_level: Optional[Union[int, str]], + error_code: Optional[Union[str, int, float, bool]], + error_level: Optional[Union[str, int, float, bool]], invalid: bool, ) -> Dataset: result = cls.validate( @@ -116,11 +116,13 @@ def evaluate( bool_col = result.data[validation_measure_name] is_false = bool_col.fillna(True) == False # noqa: E712 result.data["errorcode"] = pd.Series(None, index=result.data.index, dtype="string[pyarrow]") - result.data.loc[is_false, "errorcode"] = error_code + ec_value = str(error_code) if error_code is not None else None + result.data.loc[is_false, "errorcode"] = ec_value errorlevel_dtype = result.components["errorlevel"].data_type.dtype() result.data["errorlevel"] = pd.Series(None, index=result.data.index, dtype=errorlevel_dtype) if error_level is not None: - result.data.loc[is_false, "errorlevel"] = error_level + el_value = int(error_level) if isinstance(error_level, bool) else error_level + result.data.loc[is_false, "errorlevel"] = el_value if invalid: result.data = result.data[result.data[validation_measure_name] == False] From 24092b2e4629964d06cb0b588982920d34bf284d Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 13:52:42 +0100 Subject: [PATCH 4/7] Fixed linting errors --- src/vtlengine/Operators/Validation.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vtlengine/Operators/Validation.py b/src/vtlengine/Operators/Validation.py index 88d7c6964..032fcce7c 100644 --- a/src/vtlengine/Operators/Validation.py +++ b/src/vtlengine/Operators/Validation.py @@ -37,7 +37,9 @@ def validate( if measure.data_type != Boolean: raise SemanticError("1-1-10-1", op=cls.op, op_type="validation", me_type="Boolean") error_level_type = None - if error_level is None or isinstance(error_level, int): + if isinstance(error_level, bool): + error_level_type = Boolean + elif error_level is None or isinstance(error_level, int): error_level_type = Integer elif isinstance(error_level, str): error_level_type = String # type: ignore[assignment] @@ -121,8 +123,7 @@ def evaluate( errorlevel_dtype = result.components["errorlevel"].data_type.dtype() result.data["errorlevel"] = pd.Series(None, index=result.data.index, dtype=errorlevel_dtype) if error_level is not None: - el_value = int(error_level) if isinstance(error_level, bool) else error_level - result.data.loc[is_false, "errorlevel"] = el_value + result.data.loc[is_false, "errorlevel"] = error_level if invalid: result.data = result.data[result.data[validation_measure_name] == False] @@ -160,7 +161,9 @@ def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: s ] non_null_levels = [el for el in error_levels if el is not None] - if len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels): + if all(isinstance(el, bool) for el in non_null_levels) and len(non_null_levels) > 0: + error_level_type = Boolean + elif len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels): error_level_type = Number elif all(isinstance(el, str) for el in non_null_levels): error_level_type = String # type: ignore[assignment] From eb3540c4172628b01d73532c04274107291b4acf Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 13:53:41 +0100 Subject: [PATCH 5/7] Added related tests --- .../data/DataSet/input/GH_598_2-1.csv | 5 ++ .../data/DataSet/output/GH_598_2-1.csv | 3 ++ .../data/DataStructure/input/GH_598_2-1.json | 27 ++++++++++ .../data/DataStructure/output/GH_598_2-1.json | 45 ++++++++++++++++ tests/DatapointRulesets/data/vtl/GH_598_2.vtl | 6 +++ .../test_datapoint_rulesets.py | 43 ++++++++++++++++ .../data/DataSet/input/GH_598_1-1.csv | 7 +++ .../data/DataSet/output/GH_598_1-1.csv | 3 ++ .../data/DataStructure/input/GH_598_1-1.json | 27 ++++++++++ .../data/DataStructure/output/GH_598_1-1.json | 51 +++++++++++++++++++ tests/Hierarchical/data/vtl/GH_598_1.vtl | 6 +++ tests/Hierarchical/test_hierarchical.py | 43 ++++++++++++++++ .../data/DataSet/input/GH_598_3-1.csv | 5 ++ .../data/DataSet/output/GH_598_3-1.csv | 2 + .../data/DataStructure/input/GH_598_3-1.json | 27 ++++++++++ .../data/DataStructure/output/GH_598_3-1.json | 45 ++++++++++++++++ tests/Validation/data/vtl/GH_598_3.vtl | 1 + tests/Validation/test_validation.py | 38 ++++++++++++++ 18 files changed, 384 insertions(+) create mode 100644 tests/DatapointRulesets/data/DataSet/input/GH_598_2-1.csv create mode 100644 tests/DatapointRulesets/data/DataSet/output/GH_598_2-1.csv create mode 100644 tests/DatapointRulesets/data/DataStructure/input/GH_598_2-1.json create mode 100644 tests/DatapointRulesets/data/DataStructure/output/GH_598_2-1.json create mode 100644 tests/DatapointRulesets/data/vtl/GH_598_2.vtl create mode 100644 tests/Hierarchical/data/DataSet/input/GH_598_1-1.csv create mode 100644 tests/Hierarchical/data/DataSet/output/GH_598_1-1.csv create mode 100644 tests/Hierarchical/data/DataStructure/input/GH_598_1-1.json create mode 100644 tests/Hierarchical/data/DataStructure/output/GH_598_1-1.json create mode 100644 tests/Hierarchical/data/vtl/GH_598_1.vtl create mode 100644 tests/Validation/data/DataSet/input/GH_598_3-1.csv create mode 100644 tests/Validation/data/DataSet/output/GH_598_3-1.csv create mode 100644 tests/Validation/data/DataStructure/input/GH_598_3-1.json create mode 100644 tests/Validation/data/DataStructure/output/GH_598_3-1.json create mode 100644 tests/Validation/data/vtl/GH_598_3.vtl diff --git a/tests/DatapointRulesets/data/DataSet/input/GH_598_2-1.csv b/tests/DatapointRulesets/data/DataSet/input/GH_598_2-1.csv new file mode 100644 index 000000000..c93f7a146 --- /dev/null +++ b/tests/DatapointRulesets/data/DataSet/input/GH_598_2-1.csv @@ -0,0 +1,5 @@ +Id_1,Id_2,Me_1 +A,1,10 +A,2,-5 +B,1,100 +B,2,30 diff --git a/tests/DatapointRulesets/data/DataSet/output/GH_598_2-1.csv b/tests/DatapointRulesets/data/DataSet/output/GH_598_2-1.csv new file mode 100644 index 000000000..efa161630 --- /dev/null +++ b/tests/DatapointRulesets/data/DataSet/output/GH_598_2-1.csv @@ -0,0 +1,3 @@ +Id_1,Id_2,ruleid,Me_1,errorcode,errorlevel +A,2,rule_1,-5.0,True,True +B,2,rule_2,30.0,False,False diff --git a/tests/DatapointRulesets/data/DataStructure/input/GH_598_2-1.json b/tests/DatapointRulesets/data/DataStructure/input/GH_598_2-1.json new file mode 100644 index 000000000..a20b58429 --- /dev/null +++ b/tests/DatapointRulesets/data/DataStructure/input/GH_598_2-1.json @@ -0,0 +1,27 @@ +{ + "datasets": [ + { + "name": "DS_1", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "Integer", + "nullable": false + }, + { + "name": "Me_1", + "role": "Measure", + "type": "Number", + "nullable": true + } + ] + } + ] +} diff --git a/tests/DatapointRulesets/data/DataStructure/output/GH_598_2-1.json b/tests/DatapointRulesets/data/DataStructure/output/GH_598_2-1.json new file mode 100644 index 000000000..ed586e7c3 --- /dev/null +++ b/tests/DatapointRulesets/data/DataStructure/output/GH_598_2-1.json @@ -0,0 +1,45 @@ +{ + "datasets": [ + { + "name": "DS_r", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "Integer", + "nullable": false + }, + { + "name": "ruleid", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Me_1", + "role": "Measure", + "type": "Number", + "nullable": true + }, + { + "name": "errorcode", + "role": "Measure", + "type": "String", + "nullable": true + }, + { + "name": "errorlevel", + "role": "Measure", + "type": "Boolean", + "nullable": true + } + ] + } + ] +} diff --git a/tests/DatapointRulesets/data/vtl/GH_598_2.vtl b/tests/DatapointRulesets/data/vtl/GH_598_2.vtl new file mode 100644 index 000000000..171db56df --- /dev/null +++ b/tests/DatapointRulesets/data/vtl/GH_598_2.vtl @@ -0,0 +1,6 @@ +define datapoint ruleset dp_bool (variable Id_1 as X, Me_1 as M) is + rule_1: when X = "A" then M > 0 errorcode true errorlevel true; + rule_2: when X = "B" then M > 50 errorcode false errorlevel false +end datapoint ruleset; + +DS_r <- check_datapoint(DS_1, dp_bool); diff --git a/tests/DatapointRulesets/test_datapoint_rulesets.py b/tests/DatapointRulesets/test_datapoint_rulesets.py index 1d8434e0e..bcf51f3ba 100644 --- a/tests/DatapointRulesets/test_datapoint_rulesets.py +++ b/tests/DatapointRulesets/test_datapoint_rulesets.py @@ -1,6 +1,8 @@ from pathlib import Path from tests.Helper import TestHelper +from vtlengine.API import create_ast +from vtlengine.AST.ASTString import ASTString class TestDataPointRuleset(TestHelper): @@ -420,3 +422,44 @@ def test_14(self): self.NewSemanticExceptionTest( code=code, number_inputs=number_inputs, exception_code=message ) + + def test_GH_598_2(self): + """ + check_datapoint with boolean errorcode and errorlevel constants. + Dataset --> Dataset + Status: OK + + define datapoint ruleset dp_bool (variable Id_1 as X, Me_1 as M) is + rule_1: when X = "A" then M > 0 errorcode true errorlevel true; + rule_2: when X = "B" then M > 50 errorcode false errorlevel false + end datapoint ruleset; + + DS_r <- check_datapoint(DS_1, dp_bool); + + Git Branch: cr-596. + Goal: Verify boolean constants work as errorcode/errorlevel in datapoint + rulesets, including AST round-trip. + """ + code = "GH_598_2" + number_inputs = 1 + references_names = ["1"] + + self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names) + + def test_GH_598_2_roundtrip(self): + """ + AST round-trip for check_datapoint with boolean errorcode/errorlevel. + + Git Branch: cr-596. + Goal: Verify that rendering the AST back to VTL and re-parsing produces the + same result when boolean constants are used in errorcode/errorlevel. + """ + code = "GH_598_2" + number_inputs = 1 + references_names = ["1"] + + text = self.LoadVTL(code) + rendered = ASTString().render(create_ast(text)) + self.BaseTest( + code=code, number_inputs=number_inputs, references_names=references_names, text=rendered + ) diff --git a/tests/Hierarchical/data/DataSet/input/GH_598_1-1.csv b/tests/Hierarchical/data/DataSet/input/GH_598_1-1.csv new file mode 100644 index 000000000..2e7361820 --- /dev/null +++ b/tests/Hierarchical/data/DataSet/input/GH_598_1-1.csv @@ -0,0 +1,7 @@ +Id_1,Id_2,Me_1 +B,XX,100 +C,XX,80 +D,XX,30 +N,XX,200 +A,XX,190 +L,XX,10 diff --git a/tests/Hierarchical/data/DataSet/output/GH_598_1-1.csv b/tests/Hierarchical/data/DataSet/output/GH_598_1-1.csv new file mode 100644 index 000000000..5a9e87326 --- /dev/null +++ b/tests/Hierarchical/data/DataSet/output/GH_598_1-1.csv @@ -0,0 +1,3 @@ +Id_1,Id_2,ruleid,Me_1,errorcode,errorlevel,imbalance +B,XX,1,100.0,True,True,50.0 +N,XX,2,200.0,False,False,20.0 diff --git a/tests/Hierarchical/data/DataStructure/input/GH_598_1-1.json b/tests/Hierarchical/data/DataStructure/input/GH_598_1-1.json new file mode 100644 index 000000000..50a362378 --- /dev/null +++ b/tests/Hierarchical/data/DataStructure/input/GH_598_1-1.json @@ -0,0 +1,27 @@ +{ + "datasets": [ + { + "name": "DS_1", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Me_1", + "role": "Measure", + "type": "Number", + "nullable": true + } + ] + } + ] +} diff --git a/tests/Hierarchical/data/DataStructure/output/GH_598_1-1.json b/tests/Hierarchical/data/DataStructure/output/GH_598_1-1.json new file mode 100644 index 000000000..5d681c006 --- /dev/null +++ b/tests/Hierarchical/data/DataStructure/output/GH_598_1-1.json @@ -0,0 +1,51 @@ +{ + "datasets": [ + { + "name": "DS_r", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "ruleid", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Me_1", + "role": "Measure", + "type": "Number", + "nullable": true + }, + { + "name": "errorcode", + "role": "Measure", + "type": "String", + "nullable": true + }, + { + "name": "errorlevel", + "role": "Measure", + "type": "Boolean", + "nullable": true + }, + { + "name": "imbalance", + "role": "Measure", + "type": "Number", + "nullable": true + } + ] + } + ] +} diff --git a/tests/Hierarchical/data/vtl/GH_598_1.vtl b/tests/Hierarchical/data/vtl/GH_598_1.vtl new file mode 100644 index 000000000..2bb632d28 --- /dev/null +++ b/tests/Hierarchical/data/vtl/GH_598_1.vtl @@ -0,0 +1,6 @@ +define hierarchical ruleset hr_bool (variable rule Id_1) is + B = C - D errorcode true errorlevel true; + N = A - L errorcode false errorlevel false +end hierarchical ruleset; + +DS_r <- check_hierarchy(DS_1, hr_bool rule Id_1); diff --git a/tests/Hierarchical/test_hierarchical.py b/tests/Hierarchical/test_hierarchical.py index 727d4071a..37225fe76 100644 --- a/tests/Hierarchical/test_hierarchical.py +++ b/tests/Hierarchical/test_hierarchical.py @@ -3,6 +3,8 @@ import pytest from tests.Helper import TestHelper +from vtlengine.API import create_ast +from vtlengine.AST.ASTString import ASTString class HierarchicalHelper(TestHelper): @@ -1437,6 +1439,47 @@ def test_GL_566_1(self): self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names) + def test_GH_598_1(self): + """ + check_hierarchy with boolean errorcode and errorlevel constants. + Dataset --> Dataset + Status: OK + + define hierarchical ruleset hr_bool (variable rule Id_1) is + B = C - D errorcode true errorlevel true; + N = A - L errorcode false errorlevel false + end hierarchical ruleset; + + DS_r <- check_hierarchy(DS_1, hr_bool rule Id_1); + + Git Branch: cr-596. + Goal: Verify boolean constants work as errorcode/errorlevel in hierarchical + rulesets, including AST round-trip. + """ + code = "GH_598_1" + number_inputs = 1 + references_names = ["1"] + + self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names) + + def test_GH_598_1_roundtrip(self): + """ + AST round-trip for check_hierarchy with boolean errorcode/errorlevel. + + Git Branch: cr-596. + Goal: Verify that rendering the AST back to VTL and re-parsing produces the + same result when boolean constants are used in errorcode/errorlevel. + """ + code = "GH_598_1" + number_inputs = 1 + references_names = ["1"] + + text = self.LoadVTL(code) + rendered = ASTString().render(create_ast(text)) + self.BaseTest( + code=code, number_inputs=number_inputs, references_names=references_names, text=rendered + ) + class HierarchicalRollUpOperatorsTest(HierarchicalHelper): """ diff --git a/tests/Validation/data/DataSet/input/GH_598_3-1.csv b/tests/Validation/data/DataSet/input/GH_598_3-1.csv new file mode 100644 index 000000000..c93f7a146 --- /dev/null +++ b/tests/Validation/data/DataSet/input/GH_598_3-1.csv @@ -0,0 +1,5 @@ +Id_1,Id_2,Me_1 +A,1,10 +A,2,-5 +B,1,100 +B,2,30 diff --git a/tests/Validation/data/DataSet/output/GH_598_3-1.csv b/tests/Validation/data/DataSet/output/GH_598_3-1.csv new file mode 100644 index 000000000..006ac0b2b --- /dev/null +++ b/tests/Validation/data/DataSet/output/GH_598_3-1.csv @@ -0,0 +1,2 @@ +Id_1,Id_2,bool_var,imbalance,errorcode,errorlevel +A,2,False,,True,False diff --git a/tests/Validation/data/DataStructure/input/GH_598_3-1.json b/tests/Validation/data/DataStructure/input/GH_598_3-1.json new file mode 100644 index 000000000..a20b58429 --- /dev/null +++ b/tests/Validation/data/DataStructure/input/GH_598_3-1.json @@ -0,0 +1,27 @@ +{ + "datasets": [ + { + "name": "DS_1", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "Integer", + "nullable": false + }, + { + "name": "Me_1", + "role": "Measure", + "type": "Number", + "nullable": true + } + ] + } + ] +} diff --git a/tests/Validation/data/DataStructure/output/GH_598_3-1.json b/tests/Validation/data/DataStructure/output/GH_598_3-1.json new file mode 100644 index 000000000..911194762 --- /dev/null +++ b/tests/Validation/data/DataStructure/output/GH_598_3-1.json @@ -0,0 +1,45 @@ +{ + "datasets": [ + { + "name": "DS_r", + "DataStructure": [ + { + "name": "Id_1", + "role": "Identifier", + "type": "String", + "nullable": false + }, + { + "name": "Id_2", + "role": "Identifier", + "type": "Integer", + "nullable": false + }, + { + "name": "bool_var", + "role": "Measure", + "type": "Boolean", + "nullable": true + }, + { + "name": "imbalance", + "role": "Measure", + "type": "Number", + "nullable": true + }, + { + "name": "errorcode", + "role": "Measure", + "type": "String", + "nullable": true + }, + { + "name": "errorlevel", + "role": "Measure", + "type": "Boolean", + "nullable": true + } + ] + } + ] +} diff --git a/tests/Validation/data/vtl/GH_598_3.vtl b/tests/Validation/data/vtl/GH_598_3.vtl new file mode 100644 index 000000000..86b6d3c64 --- /dev/null +++ b/tests/Validation/data/vtl/GH_598_3.vtl @@ -0,0 +1 @@ +DS_r <- check(DS_1#Me_1 > 0 errorcode true errorlevel false invalid); diff --git a/tests/Validation/test_validation.py b/tests/Validation/test_validation.py index a69a4dae9..0a8e751b7 100644 --- a/tests/Validation/test_validation.py +++ b/tests/Validation/test_validation.py @@ -1,6 +1,8 @@ from pathlib import Path from tests.Helper import TestHelper +from vtlengine.API import create_ast +from vtlengine.AST.ASTString import ASTString class ValidationHelper(TestHelper): @@ -451,3 +453,39 @@ def test_GH_427_2(self): references_names = ["1"] self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names) + + def test_GH_598_3(self): + """ + Check with boolean errorcode and errorlevel constants. + Dataset --> Dataset + Status: OK + + DS_r <- check(DS_1#Me_1 > 0 errorcode true errorlevel false invalid); + + Git Branch: cr-596. + Goal: Verify boolean constants work as errorcode/errorlevel in the check + operator, including AST round-trip. + """ + code = "GH_598_3" + number_inputs = 1 + references_names = ["1"] + + self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names) + + def test_GH_598_3_roundtrip(self): + """ + AST round-trip for check with boolean errorcode/errorlevel. + + Git Branch: cr-596. + Goal: Verify that rendering the AST back to VTL and re-parsing produces the + same result when boolean constants are used in errorcode/errorlevel. + """ + code = "GH_598_3" + number_inputs = 1 + references_names = ["1"] + + text = self.LoadVTL(code) + rendered = ASTString().render(create_ast(text)) + self.BaseTest( + code=code, number_inputs=number_inputs, references_names=references_names, text=rendered + ) From 1e77a8fa4521a98a140e01f714c2cb2d78bc734a Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 16 Mar 2026 13:57:24 +0100 Subject: [PATCH 6/7] Fixed mypy errors --- src/vtlengine/Operators/Validation.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vtlengine/Operators/Validation.py b/src/vtlengine/Operators/Validation.py index 032fcce7c..7c89d6273 100644 --- a/src/vtlengine/Operators/Validation.py +++ b/src/vtlengine/Operators/Validation.py @@ -1,5 +1,5 @@ from copy import copy -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Type, Union import pandas as pd @@ -8,6 +8,7 @@ Boolean, Integer, Number, + ScalarType, String, check_unary_implicit_promotion, ) @@ -36,13 +37,13 @@ def validate( measure = validation_element.get_measures()[0] if measure.data_type != Boolean: raise SemanticError("1-1-10-1", op=cls.op, op_type="validation", me_type="Boolean") - error_level_type = None + error_level_type: Optional[Type[ScalarType]] = None if isinstance(error_level, bool): error_level_type = Boolean elif error_level is None or isinstance(error_level, int): error_level_type = Integer elif isinstance(error_level, str): - error_level_type = String # type: ignore[assignment] + error_level_type = String else: error_level_type = String @@ -81,7 +82,7 @@ def validate( result_components["errorlevel"] = Component( name="errorlevel", - data_type=error_level_type, # type: ignore[arg-type] + data_type=error_level_type, role=Role.MEASURE, nullable=True, ) @@ -153,7 +154,7 @@ def _generate_result_data(cls, rule_info: Dict[str, Any]) -> pd.DataFrame: @classmethod def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: str) -> Dataset: - error_level_type = None + error_level_type: Optional[Type[ScalarType]] = None error_levels = [ rule_data.get("errorlevel") for rule_data in rule_info.values() @@ -166,9 +167,9 @@ def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: s elif len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels): error_level_type = Number elif all(isinstance(el, str) for el in non_null_levels): - error_level_type = String # type: ignore[assignment] + error_level_type = String else: - error_level_type = String # type: ignore[assignment] + error_level_type = String dataset_name = VirtualCounter._new_ds_name() result_components = {comp.name: comp for comp in dataset_element.get_identifiers()} result_components["ruleid"] = Component( @@ -196,7 +197,7 @@ def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: s ) result_components["errorlevel"] = Component( name="errorlevel", - data_type=error_level_type, # type: ignore[arg-type] + data_type=error_level_type, role=Role.MEASURE, nullable=True, ) From 8b8bb5989fdd1ca543d2d7aa66a8937d7a2edfe8 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 17 Mar 2026 10:27:20 +0100 Subject: [PATCH 7/7] Minor fix --- src/vtlengine/AST/ASTConstructorModules/Terminals.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vtlengine/AST/ASTConstructorModules/Terminals.py b/src/vtlengine/AST/ASTConstructorModules/Terminals.py index 49c6cb756..0f0f3d24e 100644 --- a/src/vtlengine/AST/ASTConstructorModules/Terminals.py +++ b/src/vtlengine/AST/ASTConstructorModules/Terminals.py @@ -635,10 +635,7 @@ def visitErCode(self, ctx: Parser.ErCodeContext): ctx_list = list(ctx.getChildren()) try: - value = self.visitConstant(ctx_list[1]).value - if isinstance(value, bool): - return value - return str(value) + return str(self.visitConstant(ctx_list[1]).value) except Exception: raise Exception(f"Error code must be a string, line {ctx_list[1].getSymbol().line}")