Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c6ee423
feat: extend sub clause to accept multiple comma-separated substitutions
ruizmaa May 14, 2026
5293182
fix: regenerate parser from updated .g4 sources
ruizmaa May 14, 2026
46f8b6e
test: add sub clause multiple substitutions contract tests
ruizmaa May 14, 2026
260b053
test: add validation for malformed sub expressions in grammar contract
ruizmaa May 15, 2026
424d0ce
test: renamed test file from test_sub_multiple.py to test_sub.py
ruizmaa May 15, 2026
d0dc783
fix: remove PRECONDITION_ELEMENT token and collapse into varRef
ruizmaa May 15, 2026
cad4a24
fix: regenerate parser from updated .g4 sources
ruizmaa May 15, 2026
6a41348
test: add precondition element grammar contract tests
ruizmaa May 15, 2026
db50547
fix: relax not to prefix unary operator, parentheses now optional
ruizmaa May 15, 2026
094fc37
fix: regenerate parser from updated .g4 sources
ruizmaa May 15, 2026
db56b98
test: add not operator grammar contract tests
ruizmaa May 15, 2026
d2e4bdf
fix: rename test function for clarity on valid forms of unary not
ruizmaa May 15, 2026
1bdb50a
test: add validation tests for not binary forms and explicit parenthe…
ruizmaa May 15, 2026
36e6f34
feat: add ESCAPED_IDENTIFIER token to support backtick escaping
ruizmaa May 19, 2026
2df1519
fix: regenerate parser from updated .g4 sources
ruizmaa May 19, 2026
43edf3c
test: add escaped identifier grammar contract tests
ruizmaa May 19, 2026
28d25e7
feat: restrict rename clause to propertyCode only
ruizmaa May 19, 2026
37b7e64
fix: regenerate parser from updated .g4 sources
ruizmaa May 19, 2026
51c2e8f
test: add rename clause restriction contract tests
ruizmaa May 19, 2026
6a8f91b
ruff fix
ruizmaa May 19, 2026
1cd8263
feat: allow operationRef as head of cellAddress
ruizmaa May 19, 2026
e13643f
fix: regenerate parser from updated .g4 sources
ruizmaa May 19, 2026
c67e073
test: add operation cell address contract tests
ruizmaa May 19, 2026
7e7dea8
feat: relax time_shift numberPeriods to accept integer expressions
ruizmaa May 20, 2026
e6e789d
fix: regenerate ANTLR4 parser from updated grammar
ruizmaa May 20, 2026
f19cbc6
test: add tests for integer expression support in time_shift
ruizmaa May 20, 2026
1b85b66
fix: correct time_shift integer expression handling
ruizmaa May 20, 2026
f06d922
fix mypy: add toJSON() to AST
ruizmaa May 20, 2026
4c7cd51
fix ruff: complejity of the function _extract_time_shifts
ruizmaa May 20, 2026
423b430
fix ruff
ruizmaa May 20, 2026
3765940
fix: restrict time_shift numberPeriods to Integer only, reject floats
ruizmaa May 20, 2026
9eb25b0
fix: add test for string shift producing string constant, ensuring ty…
ruizmaa May 20, 2026
c6babf7
fix: remove unused import of Number from scalar types
ruizmaa May 20, 2026
05a02cd
refactor: update semantic check scripts to use database instead of XLSX
ruizmaa May 22, 2026
1a09015
fix(dpm_xl): reject duplicate property_code in sub clause
andres-sole May 25, 2026
6cb57e3
refactor(serialization): single-loop SubOp emission
andres-sole May 25, 2026
3cd72fb
chore(tests): tighten malformed sub tests
andres-sole May 25, 2026
014671d
Merge remote-tracking branch 'origin/master' into a2
andres-sole May 25, 2026
549078a
test(meili): restore assert_called_once() guard on migrate_csv mock
guillermo-garcia-1 May 25, 2026
c35fc93
Merge branch 'a5' into cr-48/grammar-docs-consistency
ruizmaa May 25, 2026
2a4f1b9
fix: removed dummy file
andres-sole May 25, 2026
6e0a647
Merge branch 'a2' of github.com:Meaningful-Data/dpmcore into a2
andres-sole May 25, 2026
0287e64
Merge branch 'a10' into cr-48/grammar-docs-consistency
ruizmaa May 25, 2026
a2fb638
Merge branch 'a13-a14' into cr-48/grammar-docs-consistency
ruizmaa May 25, 2026
2804f31
Merge branch 'a4' into cr-48/grammar-docs-consistency
ruizmaa May 25, 2026
829e393
Merge remote-tracking branch 'origin/cr-48/grammar-docs-consistency' …
andres-sole May 25, 2026
e9bf42e
fix: restore backtick stripping and PRECONDITION_ELEMENT lost in merge
ruizmaa May 25, 2026
7d89804
fix: remove dead TIME_PERIOD_LITERAL and TIME_INTERVAL_LITERAL branch…
ruizmaa May 25, 2026
2a83e4d
Merge branch 'cr-48/grammar-docs-consistency' into a2
andres-sole May 26, 2026
9645126
Merge pull request #57 from Meaningful-Data/a2
andres-sole May 26, 2026
5f2f75b
Merge branch 'cr-48/grammar-docs-consistency' into a12
andres-sole May 26, 2026
90bfb61
Merge branch 'cr-48/grammar-docs-consistency' into a12
andres-sole May 26, 2026
103a87d
Merge pull request #66 from Meaningful-Data/a12
andres-sole May 26, 2026
56ffddb
Merge branch 'cr-48/grammar-docs-consistency' into a1
andres-sole May 26, 2026
a7ba757
Merge pull request #59 from Meaningful-Data/a1
andres-sole May 26, 2026
61b87d6
fix(a5): remove PRECONDITION_ELEMENT token, collapse into varRef
ruizmaa May 26, 2026
cf107b9
fix(promotion): allow null as default for Mixed-type cells
ruizmaa May 26, 2026
76e33e3
test(semantic): add test for null default on Mixed type
ruizmaa May 26, 2026
5c38934
Merge pull request #70 from Meaningful-Data/fix/3-6-null-default-mixe…
ruizmaa May 26, 2026
fde4e55
fix(semantic): resolve UNKNOWN semantic crashes
ruizmaa May 26, 2026
ef70a4a
fix(3-6): accept String default for cells whose type promotes to String
ruizmaa May 27, 2026
284698d
Merge pull request #77 from Meaningful-Data/fix/3-6-string-default-bi…
ruizmaa May 27, 2026
9c75304
Merge pull request #78 from Meaningful-Data/fix/unknown-error
ruizmaa May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 171 additions & 87 deletions scripts/check_semantic.py
Original file line number Diff line number Diff line change
@@ -1,116 +1,200 @@
"""Check DPM-XL expression semantics for all rules in a validations xlsx."""
"""Check DPM-XL expression semantics for all OperationVersion rules in the DB."""

import argparse
import csv
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any

import openpyxl
from sqlalchemy import create_engine
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker

from dpmcore.services.semantic import SemanticService

_DEFAULT_XLSX = (
Path(__file__).resolve().parents[1]
/ "tests"
/ "fixtures"
/ "validations_export.xlsx"
)
_DEFAULT_DB = (
Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "test_data.db"
)


def _check_rows(
rows: list[dict[str, Any]],
svc: SemanticService,
) -> tuple[list[tuple[str, str, str, str, str]], int]:
items = [
(
str(row.get("Code") or ""),
col,
str(row.get("StartRelease") or "").strip(),
str(row.get(col)).strip(),
)
for row in rows
for col in ("Expression", "Precondition")
if row.get(col) is not None and str(row.get(col)).strip()
]
total = len(items)
width = len(str(total))
failures: list[tuple[str, str, str, str, str]] = []
for i, (code, col, release, expr) in enumerate(items, start=1):
result = svc.validate(expr, release_code=release or None)
prefix = f"[{i:{width}}/{total}] {code} | {col} | release {release}"
if result.is_valid:
print(f"{prefix} | PASS")
else:
error = result.error_message or ""
print(f"{prefix} | FAIL: {error}")
print(f" Expression: {expr}")
failures.append((code, col, release, expr, error))
return failures, total
WIDTH = 80
BAR = "=" * WIDTH

_TTY = sys.stdout.isatty()
_GREEN = "\033[32m" if _TTY else ""
_RED = "\033[31m" if _TTY else ""
_DIM = "\033[2m" if _TTY else ""
_RESET = "\033[0m" if _TTY else ""


def _fmt_size(path: Path) -> str:
mb = path.stat().st_size / (1024 * 1024)
return f"{mb:.1f} MB"


def _fmt_duration(seconds: float) -> str:
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
if h:
return f"{h}h {m}m {s}s"
if m:
return f"{m}m {s}s"
return f"{s}s"



def _load_rows(db_path: Path):
engine = create_engine(f"sqlite:///{db_path}")
with engine.connect() as conn:
rows = conn.execute(
text(
"""
SELECT
ov.OperationVID,
o.Code,
r.Code AS ReleaseCode,
ov.Expression
FROM OperationVersion ov
JOIN Operation o ON o.OperationID = ov.OperationID
LEFT JOIN Release r ON r.ReleaseID = ov.StartReleaseID
WHERE ov.Expression IS NOT NULL
AND trim(ov.Expression) != ''
ORDER BY o.Code, ov.OperationVID
"""
)
).fetchall()
engine.dispose()
return rows


def main() -> int:
"""Run semantic checks and print results to stdout."""
parser = argparse.ArgumentParser(
description="Check DPM-XL expression semantics in a validations xlsx.",
)
parser.add_argument(
"--xlsx",
default=str(_DEFAULT_XLSX),
help=f"Path to the validations xlsx (default: {_DEFAULT_XLSX})",
description="Check DPM-XL expression semantics for all rules in the DB.",
)
parser.add_argument(
"--db",
default=str(_DEFAULT_DB),
type=Path,
default=_DEFAULT_DB,
help=f"Path to the SQLite DPM database (default: {_DEFAULT_DB})",
)
parser.add_argument(
"--csv",
type=Path,
default=None,
help="Path for the failures CSV output (default: <db_stem>_failures.csv next to the DB)",
)
args = parser.parse_args()

print("Semantic check")
print(f" xlsx: {args.xlsx}")
print(f" db: {args.db}")
db_path: Path = args.db.resolve()
if not db_path.exists():
print(f"ERROR: database not found: {db_path}", file=sys.stderr)
return 1

csv_path: Path = (
args.csv.resolve()
if args.csv
else db_path.parent / f"{db_path.stem}_failures.csv"
)

# ------------------------------------------------------------------
# Header
# ------------------------------------------------------------------
print(f"\n{BAR}")
print(" DPM Semantic Validation Runner")
print(BAR)

rows = _load_rows(db_path)
total = len(rows)
total_str = f"{total:,}"
counter_width = len(str(total))

print(f" Database : {db_path}")
print(f" Size : {_fmt_size(db_path)}")
print(f" Validations: {total:,}")
print(f" CSV output : {csv_path}")
print(f" Started : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(BAR)
print()

engine = create_engine(f"sqlite:///{args.db}")
Session = sessionmaker(bind=engine)
session = Session()

try:
wb = openpyxl.load_workbook(args.xlsx)
ws = wb["Validations"]
headers = [cell.value for cell in ws[1]]
rows: list[dict[str, Any]] = [
dict(zip(headers, row, strict=False))
for row in ws.iter_rows(min_row=2, values_only=True)
]

svc = SemanticService(session)
failures, total = _check_rows(rows, svc)

passed = total - len(failures)
failed = len(failures)
print(
f"\n{total} expressions checked — {passed} passed, {failed} failed"
)

if failures:
width_f = len(str(len(failures)))
print("\nFailed expressions:")
for j, (code, col, release, expr, error) in enumerate(failures, 1):
print(
f" [{j:{width_f}}/{len(failures)}]"
f" {code} | {col} | release {release} | FAIL: {error}"
)
print(f" Expression: {expr}")

return 1 if failures else 0
finally:
session.close()
engine.dispose()
# ------------------------------------------------------------------
# Run
# ------------------------------------------------------------------
engine = create_engine(f"sqlite:///{db_path}")
session = sessionmaker(bind=engine)()
svc = SemanticService(session)

passed = 0
failures = []

t_start = time.monotonic()

for i, (operation_vid, op_code, release_code, expression) in enumerate(rows, 1):
label = op_code or str(operation_vid)
release = release_code or ""

result = svc.validate(expression.strip(), release_code=release or None)

idx = f"[{i:{counter_width},}/{total_str}]"
preview = expression.replace("\n", " ").strip()
line = f"{idx} {label:<12} {_DIM}{preview}{_RESET}"

if result.is_valid:
passed += 1
print(f"{line} {_GREEN}PASS{_RESET}")
else:
err = result.error_message or "(no message)"
err_code = result.error_code or ""
failures.append((i, operation_vid, label, release, expression.strip(), err_code, err))
print(f"{line} {_RED}FAIL{_RESET}")
print(f" {_DIM}└─{_RESET} {err}")

session.close()
engine.dispose()

elapsed = time.monotonic() - t_start
failed = len(failures)
pct_pass = passed / total * 100 if total else 0.0
pct_fail = failed / total * 100 if total else 0.0

# ------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------
print()
print(BAR)
print(" SUMMARY")
print(BAR)
print(f" Database : {db_path}")
print(f" Size : {_fmt_size(db_path)}")
print(f" Total : {total:,}")
print(f" {_GREEN}Passed{_RESET} : {passed:,} ({pct_pass:.1f}%)")
print(f" {_RED}Failed{_RESET} : {failed:,} ({pct_fail:.1f}%)")
print(f" Duration : {_fmt_duration(elapsed)}")
print(BAR)

# ------------------------------------------------------------------
# Failures list
# ------------------------------------------------------------------
if failures:
print()
print(BAR)
print(" FAILURES")
print(BAR)
for idx, operation_vid, label, release, expression, err_code, error in failures:
idx_str = f"[{idx:{counter_width},}/{total_str}]"
release_info = f" | release: {release}" if release else ""
print(f"\n{idx_str} {label}{release_info}")
print(f" Expression : {expression}")
print(f" Error : {error}")
print()
print(BAR)

with csv_path.open("w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(["index", "operation_vid", "code", "release", "error_code", "error", "expression"])
for idx, operation_vid, label, release, expression, err_code, error in failures:
writer.writerow([idx, operation_vid, label, release, err_code, error, expression])
print(f"\n CSV written: {csv_path}")

return 1 if failures else 0


if __name__ == "__main__":
Expand Down
Loading
Loading