compiler.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. """Deterministic RuleSpec-to-plan compilation for production-line release."""
  2. from __future__ import annotations
  3. import hashlib
  4. import json
  5. from typing import Any
  6. from app.core.common.identifiers import ensure_governance_uid
  7. from app.core.data_rules.contracts import read_rule_spec, rule_spec_hash
  8. from app.core.data_rules.expressions import SUPPORTED_BACKENDS, backend_support
  9. COMPILER_VERSION = "dataops-rulespec-1.0"
  10. def _canonical_hash(value: Any) -> str:
  11. canonical = json.dumps(
  12. value,
  13. sort_keys=True,
  14. separators=(",", ":"),
  15. ensure_ascii=False,
  16. )
  17. return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
  18. def _rule_supported_backends(spec: dict[str, Any]) -> frozenset[str]:
  19. supported = SUPPORTED_BACKENDS
  20. for step in spec["steps"]:
  21. ast = step.get("expression_ast")
  22. if ast is not None:
  23. supported = supported & backend_support(ast)
  24. return frozenset(supported)
  25. def compile_rule_plan(
  26. rule_version: dict[str, Any], *, supported_backends: frozenset[str] | None = None
  27. ) -> dict[str, Any]:
  28. """Compile a published rule into a target-neutral immutable plan.
  29. This compiler deliberately emits no SQL or Python. M3 runtime adapters
  30. translate the closed RuleSpec operators for a deployment's bound backend.
  31. """
  32. if not isinstance(rule_version, dict):
  33. raise ValueError("rule version must be an object")
  34. if rule_version.get("status") != "published":
  35. raise ValueError("only published rule versions may be compiled")
  36. try:
  37. version_id = ensure_governance_uid(
  38. {"uid": str(rule_version.get("id"))}
  39. )
  40. except ValueError as exc:
  41. raise ValueError("rule version id must be a valid UUIDv7") from exc
  42. spec = read_rule_spec(rule_version.get("rule_spec"))
  43. digest = rule_spec_hash(spec)
  44. if rule_version.get("spec_hash") != digest:
  45. raise ValueError("published rule version spec hash does not match")
  46. derived_backends = _rule_supported_backends(spec)
  47. if supported_backends is None:
  48. supported = derived_backends
  49. elif (
  50. not isinstance(supported_backends, frozenset)
  51. or not supported_backends <= SUPPORTED_BACKENDS
  52. ):
  53. raise ValueError("supported_backends must be a concrete backend set")
  54. else:
  55. supported = supported_backends & derived_backends
  56. if not supported:
  57. raise ValueError("rule has no supported execution backend")
  58. quality_only = all(step["op"] == "assert" for step in spec["steps"])
  59. if quality_only:
  60. backend = "quality_check"
  61. elif "polars" in supported:
  62. backend = "polars_batch"
  63. elif supported & {"postgresql", "mysql"}:
  64. backend = "sql_pushdown"
  65. else:
  66. raise ValueError("rule has no supported compiler target")
  67. plan = {
  68. "schema_version": "1.0",
  69. "compiler_version": COMPILER_VERSION,
  70. "rule_version_id": version_id,
  71. "rule_spec_hash": digest,
  72. "input_schema_ref": spec["input_schema_ref"],
  73. "output_schema_ref": spec["output_schema_ref"],
  74. "null_policy": spec["null_policy"],
  75. "timezone": spec["timezone"],
  76. "supported_execution_backends": sorted(supported),
  77. "steps": spec["steps"],
  78. }
  79. return {
  80. "backend": backend,
  81. "compiler_version": COMPILER_VERSION,
  82. "plan": plan,
  83. "plan_hash": _canonical_hash(plan),
  84. }