Skip to content
1 change: 1 addition & 0 deletions src/vtlengine/API/_InternalApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def _build_component(component: Dict[str, Any]) -> VTL_Component:
data_type=scalar_type,
role=role,
nullable=nullable,
value_domain=component.get("subset"),
)


Expand Down
6 changes: 6 additions & 0 deletions src/vtlengine/Exceptions/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@
"description": "Raised when there are no applicable rules in a Hierarchy Roll-up "
"due to missing '=' operators.",
},
"1-1-10-11": {
"message": "At op {op}: Component {comp} is defined on value domain {found} but the "
"ruleset signature expects {expected}.",
"description": "Raised when a component mapped to a value-domain-signature ruleset is "
"defined on a different value domain than the one in the signature.",
},
# General Operators
"2-1-12-1": {
"message": "At op {op}: Create a null Measure without a Scalar type is not allowed. "
Expand Down
52 changes: 52 additions & 0 deletions src/vtlengine/Interpreter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,35 @@ def visit_HROperation(self, node: AST.HROperation) -> Any: # noqa: C901
)
cond_info[cond_comp] = cond_components[i]

if hr_info["node"].signature_type == "valuedomain":
rule_comp = dataset.components.get(component) if component else None
if (
rule_comp is not None
and rule_comp.value_domain is not None
and rule_comp.value_domain != hr_info["signature"]
):
raise SemanticError(
"1-1-10-11",
op=node.op,
comp=component,
found=rule_comp.value_domain,
expected=hr_info["signature"],
)
for i, cond_vd in enumerate(hr_info["condition"]):
cond_comp_obj = dataset.components.get(cond_components[i])
if (
cond_comp_obj is not None
and cond_comp_obj.value_domain is not None
and cond_comp_obj.value_domain != cond_vd
):
raise SemanticError(
"1-1-10-11",
op=node.op,
comp=cond_components[i],
found=cond_comp_obj.value_domain,
expected=cond_vd,
)

if node.op == HIERARCHY:
aux = []
for rule in hr_info["rules"]:
Expand Down Expand Up @@ -1249,6 +1278,7 @@ def visit_HROperation(self, node: AST.HROperation) -> Any: # noqa: C901
dataset_element=dataset,
rule_info=rule_output_values,
output=output,
value_domains=self.value_domains,
)
return Hierarchy.validate(dataset, output)

Expand Down Expand Up @@ -1291,6 +1321,26 @@ def visit_DPValidation(self, node: AST.DPValidation) -> Any:
expected=dpr_info["params"][i],
found=comp_name,
)
if (
dpr_info is not None
and dpr_info.get("signature_type") == "valuedomain"
and dpr_info.get("params")
):
vd_names = dpr_info["params"]
for i, comp_name in enumerate(node.components):
comp = dataset_element.components[comp_name]
if (
comp.value_domain is not None
and i < len(vd_names)
and comp.value_domain != vd_names[i]
):
raise SemanticError(
"1-1-10-11",
op=CHECK_DATAPOINT,
comp=comp_name,
found=comp.value_domain,
expected=vd_names[i],
)

# Get output mode with default
output = node.output.value if node.output else "invalid"
Expand Down Expand Up @@ -1323,6 +1373,7 @@ def visit_DPValidation(self, node: AST.DPValidation) -> Any:
dataset_element=dataset_element,
rule_info=rule_output_values,
output=output,
value_domains=self.value_domains,
)

def visit_DPRule(self, node: AST.DPRule) -> Any:
Expand Down Expand Up @@ -1378,6 +1429,7 @@ def visit_Validation(self, node: AST.Validation) -> Dataset:
error_code=node.error_code,
error_level=node.error_level,
invalid=node.invalid,
value_domains=self.value_domains,
)

def visit_EvalOp(self, node: AST.EvalOp) -> Dataset:
Expand Down
9 changes: 7 additions & 2 deletions src/vtlengine/Model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class Component:
data_type: Type[ScalarType]
role: Role
nullable: bool
value_domain: Optional[str] = None

def __post_init__(self) -> None:
if self.role == Role.IDENTIFIER and self.nullable:
Expand All @@ -175,7 +176,7 @@ def __eq__(self, other: Any) -> bool:
return self.to_dict() == other.to_dict()

def copy(self) -> "Component":
return Component(self.name, self.data_type, self.role, self.nullable)
return Component(self.name, self.data_type, self.role, self.nullable, self.value_domain)

@classmethod
def from_json(cls, json_str: Any) -> "Component":
Expand All @@ -186,19 +187,23 @@ def from_json(cls, json_str: Any) -> "Component":
SCALAR_TYPES[data_type_value],
Role(json_str["role"]),
json_str["nullable"],
json_str.get("subset"),
)

def to_dict(self) -> Dict[str, Any]:
data_type = self.data_type
if not inspect.isclass(self.data_type):
data_type = self.data_type.__class__ # type: ignore[assignment]
return {
result: Dict[str, Any] = {
"name": self.name,
"type": DataTypes.SCALAR_TYPES_CLASS_REVERSE[data_type],
# Need to check here for NoneType as UDO argument has it
"role": self.role.value if self.role is not None else None, # type: ignore[redundant-expr]
"nullable": self.nullable,
}
if self.value_domain is not None:
result["subset"] = self.value_domain
return result

def to_json(self) -> str:
return json.dumps(self.to_dict())
Expand Down
68 changes: 36 additions & 32 deletions src/vtlengine/Operators/Validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from copy import copy
from typing import Any, Dict, Optional, Type, Union
from typing import Any, Dict, Optional, Tuple, Type, Union

from vtlengine.AST.Grammar.tokens import CHECK, CHECK_HIERARCHY
from vtlengine.DataTypes import (
Expand All @@ -11,11 +11,25 @@
check_unary_implicit_promotion,
)
from vtlengine.Exceptions import SemanticError
from vtlengine.Model import Component, Dataset, Role
from vtlengine.Model import Component, Dataset, Role, ValueDomain
from vtlengine.Operators import Operator
from vtlengine.Utils.__Virtual_Assets import VirtualCounter


def resolve_error_types(
value_domains: Optional[Dict[str, ValueDomain]],
) -> Tuple[Type[ScalarType], Type[ScalarType]]:
"""Resolve the (errorcode, errorlevel) output types per VTL 2.2.

errorcode -> type of the ``errorcode_vd`` Value Domain if provided, else String.
errorlevel -> type of the ``errorlevel_vd`` Value Domain if provided, else Integer.
"""
vds = value_domains or {}
errorcode_type = vds["errorcode_vd"].type if "errorcode_vd" in vds else String
errorlevel_type = vds["errorlevel_vd"].type if "errorlevel_vd" in vds else Integer
return errorcode_type, errorlevel_type


# noinspection PyTypeChecker
class Check(Operator):
op = CHECK
Expand All @@ -28,22 +42,15 @@ def validate(
error_code: Optional[Union[str, int, float, bool]],
error_level: Optional[Union[str, int, float, bool]],
invalid: bool,
value_domains: Optional[Dict[str, ValueDomain]] = None,
) -> Dataset:
dataset_name = VirtualCounter._new_ds_name()
if len(validation_element.get_measures()) != 1:
raise SemanticError("1-1-10-1", op=cls.op, op_type="validation", me_type="Boolean")
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: 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
else:
error_level_type = String
error_code_type, error_level_type = resolve_error_types(value_domains)

imbalance_measure = None
if imbalance_element is not None:
Expand Down Expand Up @@ -75,7 +82,7 @@ def validate(
result_components["imbalance"].name = "imbalance"

result_components["errorcode"] = Component(
name="errorcode", data_type=String, role=Role.MEASURE, nullable=True
name="errorcode", data_type=error_code_type, role=Role.MEASURE, nullable=True
)

result_components["errorlevel"] = Component(
Expand All @@ -91,23 +98,14 @@ def validate(
# noinspection PyTypeChecker
class Validation(Operator):
@classmethod
def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: str) -> Dataset:
error_level_type: Optional[Type[ScalarType]] = None
error_levels = [
rule_data.get("errorlevel")
for rule_data in rule_info.values()
if "errorlevel" in rule_data
]
non_null_levels = [el for el in error_levels if el is not None]

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
else:
error_level_type = String
def validate(
cls,
dataset_element: Dataset,
rule_info: Dict[str, Any],
output: str,
value_domains: Optional[Dict[str, ValueDomain]] = None,
) -> Dataset:
error_code_type, error_level_type = resolve_error_types(value_domains)
dataset_name = VirtualCounter._new_ds_name()
result_components = {comp.name: comp for comp in dataset_element.get_identifiers()}
result_components["ruleid"] = Component(
Expand All @@ -131,7 +129,7 @@ def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: s
),
}
result_components["errorcode"] = Component(
name="errorcode", data_type=String, role=Role.MEASURE, nullable=True
name="errorcode", data_type=error_code_type, role=Role.MEASURE, nullable=True
)
result_components["errorlevel"] = Component(
name="errorlevel",
Expand All @@ -151,8 +149,14 @@ class Check_Hierarchy(Validation):
op = CHECK_HIERARCHY

@classmethod
def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: str) -> Dataset:
result = super().validate(dataset_element, rule_info, output)
def validate(
cls,
dataset_element: Dataset,
rule_info: Dict[str, Any],
output: str,
value_domains: Optional[Dict[str, ValueDomain]] = None,
) -> Dataset:
result = super().validate(dataset_element, rule_info, output, value_domains)
result.components["imbalance"] = Component(
name="imbalance", data_type=Number, role=Role.MEASURE, nullable=True
)
Expand Down
8 changes: 2 additions & 6 deletions src/vtlengine/ViralPropagation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,11 @@ def get_existing(self, signature_type: str, target: str) -> Optional["ViralPropa
return rules.get(target)

def rule_for(self, component: Any) -> Optional["ViralPropagationRule"]:
"""Resolve the rule for a component: variable-level overrides value-domain-level.

``component.value_domain`` may not exist yet (added in a later phase); use
getattr so this is forward-compatible.
"""
"""Resolve the rule for a component: variable-level overrides value-domain-level."""
rule = self._variable_rules.get(component.name)
if rule is not None:
return rule
value_domain = getattr(component, "value_domain", None)
value_domain = component.value_domain
if value_domain is not None:
return self._valuedomain_rules.get(value_domain)
return None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from vtlengine.DataTypes import String as StringType
from vtlengine.DataTypes.TimeHandling import TimePeriodHandler
from vtlengine.duckdb_transpiler.Transpiler.sql_builder import quote_name
from vtlengine.Model import Component, Dataset, Role
from vtlengine.Model import Component, Dataset, Role, ValueDomain
from vtlengine.Operators.Join import merged_viral_attribute_names
from vtlengine.Operators.Validation import resolve_error_types


def _try_normalize_time_period(value: str) -> Optional[str]:
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(
**self.output_datasets,
}
self.scalars: Dict[str, Any] = scalars or {}
self.value_domains: Dict[str, ValueDomain] = {}
self.current_assignment: str = ""
self._in_clause: bool = False
self._current_dataset: Optional[Dataset] = None
Expand Down Expand Up @@ -533,7 +535,6 @@ def _build_validation_structure(self, node: AST.Validation) -> Optional[Dataset]
val_comps = self._identifiers_dict(inner_ds)
self._add_error_measures(
val_comps,
errorlevel_type=Integer,
with_ruleid=False,
with_bool_var=True,
)
Expand Down Expand Up @@ -564,19 +565,19 @@ def _add_error_measures(
self,
comps: Dict[str, Component],
*,
errorlevel_type: Any = Number,
with_ruleid: bool = True,
with_imbalance: bool = True,
with_bool_var: bool = False,
) -> None:
"""Append the standard validation/hierarchy error-reporting measures."""
errorcode_type, errorlevel_type = resolve_error_types(self.value_domains)
if with_bool_var:
comps["bool_var"] = self._make_comp("bool_var", Boolean)
if with_imbalance:
comps["imbalance"] = self._make_comp("imbalance", Number)
if with_ruleid:
comps["ruleid"] = self._make_comp("ruleid", StringType, Role.IDENTIFIER, False)
comps["errorcode"] = self._make_comp("errorcode", StringType)
comps["errorcode"] = self._make_comp("errorcode", errorcode_type)
comps["errorlevel"] = self._make_comp("errorlevel", errorlevel_type)

# =========================================================================
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-10-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-11-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-12-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-13-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-14-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Additional/data/DataStructure/output/11-15-DS_r.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{
"name": "errorlevel",
"role": "Measure",
"type": "Number",
"type": "Integer",
"nullable": true
}
]
Expand Down
Loading
Loading