contractforge-databricks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contractforge_databricks/__init__.py +172 -0
- contractforge_databricks/adapter.py +69 -0
- contractforge_databricks/annotations/__init__.py +10 -0
- contractforge_databricks/annotations/application.py +52 -0
- contractforge_databricks/annotations/audit.py +49 -0
- contractforge_databricks/annotations/sql.py +142 -0
- contractforge_databricks/api.py +65 -0
- contractforge_databricks/bundles/__init__.py +9 -0
- contractforge_databricks/bundles/assets.py +47 -0
- contractforge_databricks/bundles/project.py +213 -0
- contractforge_databricks/bundles/project_config.py +133 -0
- contractforge_databricks/capabilities/__init__.py +17 -0
- contractforge_databricks/capabilities/builders.py +43 -0
- contractforge_databricks/capabilities/evaluate.py +162 -0
- contractforge_databricks/capabilities/mapping.py +36 -0
- contractforge_databricks/capabilities/models.py +44 -0
- contractforge_databricks/capabilities/runtime.py +111 -0
- contractforge_databricks/capabilities/uc.py +47 -0
- contractforge_databricks/cli.py +196 -0
- contractforge_databricks/cli_deploy.py +98 -0
- contractforge_databricks/cli_governance.py +142 -0
- contractforge_databricks/cli_io.py +91 -0
- contractforge_databricks/cli_maintenance.py +69 -0
- contractforge_databricks/coercion.py +31 -0
- contractforge_databricks/contract_extensions.py +70 -0
- contractforge_databricks/cost/__init__.py +11 -0
- contractforge_databricks/cost/model.py +22 -0
- contractforge_databricks/cost/report.py +65 -0
- contractforge_databricks/cost/sql.py +136 -0
- contractforge_databricks/dashboards/__init__.py +15 -0
- contractforge_databricks/dashboards/control_tables.py +150 -0
- contractforge_databricks/diagnostics/__init__.py +7 -0
- contractforge_databricks/diagnostics/explain.py +40 -0
- contractforge_databricks/environment.py +53 -0
- contractforge_databricks/evidence/__init__.py +98 -0
- contractforge_databricks/evidence/ddl.py +35 -0
- contractforge_databricks/evidence/governance_log.py +175 -0
- contractforge_databricks/evidence/helpers.py +29 -0
- contractforge_databricks/evidence/ops_log.py +210 -0
- contractforge_databricks/evidence/records.py +27 -0
- contractforge_databricks/evidence/run_log.py +74 -0
- contractforge_databricks/evidence/schemas.py +7 -0
- contractforge_databricks/evidence/sql.py +144 -0
- contractforge_databricks/evidence/tables.py +20 -0
- contractforge_databricks/evidence/writer.py +118 -0
- contractforge_databricks/execution/__init__.py +70 -0
- contractforge_databricks/execution/delta_basic.py +57 -0
- contractforge_databricks/execution/hash_diff.py +126 -0
- contractforge_databricks/execution/hash_diff_latest.py +142 -0
- contractforge_databricks/execution/replace_partitions.py +40 -0
- contractforge_databricks/execution/results.py +5 -0
- contractforge_databricks/execution/retry.py +36 -0
- contractforge_databricks/execution/scd2.py +213 -0
- contractforge_databricks/execution/scd2_deletes.py +65 -0
- contractforge_databricks/execution/scd2_late.py +30 -0
- contractforge_databricks/execution/snapshot.py +77 -0
- contractforge_databricks/execution/sql_merge.py +85 -0
- contractforge_databricks/execution/tables.py +98 -0
- contractforge_databricks/execution/windows.py +58 -0
- contractforge_databricks/governance/__init__.py +30 -0
- contractforge_databricks/governance/access.py +185 -0
- contractforge_databricks/governance/application.py +93 -0
- contractforge_databricks/governance/drift.py +49 -0
- contractforge_databricks/governance/runtime.py +60 -0
- contractforge_databricks/governance/sql.py +31 -0
- contractforge_databricks/governance/validation.py +135 -0
- contractforge_databricks/lakeflow/__init__.py +21 -0
- contractforge_databricks/lakeflow/compatibility.py +194 -0
- contractforge_databricks/lakeflow/rendering.py +175 -0
- contractforge_databricks/lineage/__init__.py +7 -0
- contractforge_databricks/lineage/openlineage.py +182 -0
- contractforge_databricks/maintenance/__init__.py +27 -0
- contractforge_databricks/maintenance/retention.py +90 -0
- contractforge_databricks/maintenance/sql.py +68 -0
- contractforge_databricks/metrics/__init__.py +19 -0
- contractforge_databricks/metrics/history.py +21 -0
- contractforge_databricks/metrics/write.py +63 -0
- contractforge_databricks/operations/__init__.py +4 -0
- contractforge_databricks/operations/application.py +38 -0
- contractforge_databricks/operations/sql.py +95 -0
- contractforge_databricks/parity/__init__.py +18 -0
- contractforge_databricks/parity/catalog.py +59 -0
- contractforge_databricks/parity/models.py +7 -0
- contractforge_databricks/parity/scenarios.py +111 -0
- contractforge_databricks/partitioning/__init__.py +3 -0
- contractforge_databricks/partitioning/predicates.py +28 -0
- contractforge_databricks/preparation/__init__.py +47 -0
- contractforge_databricks/preparation/deduplicate.py +87 -0
- contractforge_databricks/preparation/encoding.py +37 -0
- contractforge_databricks/preparation/hashing.py +18 -0
- contractforge_databricks/preparation/pyspark.py +178 -0
- contractforge_databricks/preparation/pyspark_staging.py +70 -0
- contractforge_databricks/preparation/shape.py +209 -0
- contractforge_databricks/preparation/shape_validation.py +94 -0
- contractforge_databricks/preparation/staging.py +17 -0
- contractforge_databricks/preparation/zip_arrays.py +51 -0
- contractforge_databricks/presets/__init__.py +3 -0
- contractforge_databricks/presets/base.py +24 -0
- contractforge_databricks/presets/bronze.py +57 -0
- contractforge_databricks/presets/catalog.py +22 -0
- contractforge_databricks/presets/core.py +134 -0
- contractforge_databricks/presets/gold.py +62 -0
- contractforge_databricks/presets/modifiers.py +51 -0
- contractforge_databricks/presets/runtime.py +22 -0
- contractforge_databricks/presets/silver.py +101 -0
- contractforge_databricks/presets/write_engine.py +57 -0
- contractforge_databricks/quality/__init__.py +41 -0
- contractforge_databricks/quality/evaluation.py +178 -0
- contractforge_databricks/quality/persistence.py +81 -0
- contractforge_databricks/quality/registry.py +134 -0
- contractforge_databricks/quality/results.py +17 -0
- contractforge_databricks/quality/sql.py +113 -0
- contractforge_databricks/rendering/__init__.py +11 -0
- contractforge_databricks/rendering/bundle.py +93 -0
- contractforge_databricks/rendering/markdown.py +50 -0
- contractforge_databricks/rendering/names.py +56 -0
- contractforge_databricks/results.py +15 -0
- contractforge_databricks/runtime/__init__.py +101 -0
- contractforge_databricks/runtime/available_now.py +147 -0
- contractforge_databricks/runtime/bundles.py +211 -0
- contractforge_databricks/runtime/cache.py +20 -0
- contractforge_databricks/runtime/control_tables.py +19 -0
- contractforge_databricks/runtime/deploy.py +197 -0
- contractforge_databricks/runtime/detection.py +114 -0
- contractforge_databricks/runtime/dry_run.py +46 -0
- contractforge_databricks/runtime/errors.py +54 -0
- contractforge_databricks/runtime/file_selection.py +109 -0
- contractforge_databricks/runtime/finalization.py +168 -0
- contractforge_databricks/runtime/governance.py +37 -0
- contractforge_databricks/runtime/hooks.py +45 -0
- contractforge_databricks/runtime/http_file.py +37 -0
- contractforge_databricks/runtime/http_retry.py +15 -0
- contractforge_databricks/runtime/http_safety.py +9 -0
- contractforge_databricks/runtime/json_materialization.py +97 -0
- contractforge_databricks/runtime/lineage.py +164 -0
- contractforge_databricks/runtime/maintenance.py +43 -0
- contractforge_databricks/runtime/merge_validation.py +98 -0
- contractforge_databricks/runtime/metadata.py +21 -0
- contractforge_databricks/runtime/metrics.py +34 -0
- contractforge_databricks/runtime/models.py +32 -0
- contractforge_databricks/runtime/options.py +33 -0
- contractforge_databricks/runtime/orchestration_context.py +185 -0
- contractforge_databricks/runtime/orchestrator.py +147 -0
- contractforge_databricks/runtime/partitioning.py +93 -0
- contractforge_databricks/runtime/quality_quarantine.py +92 -0
- contractforge_databricks/runtime/rest_api.py +46 -0
- contractforge_databricks/runtime/rest_auth.py +21 -0
- contractforge_databricks/runtime/rest_pagination.py +21 -0
- contractforge_databricks/runtime/run_payload.py +177 -0
- contractforge_databricks/runtime/schema.py +106 -0
- contractforge_databricks/runtime/source_metadata.py +30 -0
- contractforge_databricks/runtime/source_registry.py +43 -0
- contractforge_databricks/runtime/source_schema.py +24 -0
- contractforge_databricks/runtime/sources.py +208 -0
- contractforge_databricks/runtime/spark.py +183 -0
- contractforge_databricks/runtime/spark_defaults.py +35 -0
- contractforge_databricks/runtime/storage_auth.py +132 -0
- contractforge_databricks/runtime/streaming.py +131 -0
- contractforge_databricks/runtime/success.py +104 -0
- contractforge_databricks/runtime/utils.py +52 -0
- contractforge_databricks/runtime/watermark.py +71 -0
- contractforge_databricks/runtime/windows.py +184 -0
- contractforge_databricks/runtime/write.py +66 -0
- contractforge_databricks/runtime/write_flow.py +146 -0
- contractforge_databricks/runtime/write_strategy.py +40 -0
- contractforge_databricks/schema/__init__.py +21 -0
- contractforge_databricks/schema/diff.py +11 -0
- contractforge_databricks/schema/policy.py +33 -0
- contractforge_databricks/schema/sync.py +23 -0
- contractforge_databricks/security/__init__.py +21 -0
- contractforge_databricks/security/errors.py +5 -0
- contractforge_databricks/security/redaction.py +5 -0
- contractforge_databricks/security/secrets.py +114 -0
- contractforge_databricks/security/source_policy.py +17 -0
- contractforge_databricks/shapes/__init__.py +3 -0
- contractforge_databricks/shapes/sql.py +123 -0
- contractforge_databricks/sources/__init__.py +67 -0
- contractforge_databricks/sources/artifacts.py +100 -0
- contractforge_databricks/sources/autoloader.py +48 -0
- contractforge_databricks/sources/bounded_streams.py +44 -0
- contractforge_databricks/sources/classification.py +115 -0
- contractforge_databricks/sources/delta_share.py +21 -0
- contractforge_databricks/sources/files.py +48 -0
- contractforge_databricks/sources/http_file.py +46 -0
- contractforge_databricks/sources/interpret.py +76 -0
- contractforge_databricks/sources/jdbc.py +32 -0
- contractforge_databricks/sources/metadata.py +18 -0
- contractforge_databricks/sources/native_passthrough.py +33 -0
- contractforge_databricks/sources/rds_iam.py +15 -0
- contractforge_databricks/sources/rds_iam_runtime.py +191 -0
- contractforge_databricks/sources/rest_api.py +33 -0
- contractforge_databricks/sources/support.py +50 -0
- contractforge_databricks/sources/table_refs.py +65 -0
- contractforge_databricks/sql/__init__.py +4 -0
- contractforge_databricks/sql/identifiers.py +17 -0
- contractforge_databricks/sql/literals.py +36 -0
- contractforge_databricks/state/__init__.py +39 -0
- contractforge_databricks/state/ddl.py +24 -0
- contractforge_databricks/state/migrations.py +146 -0
- contractforge_databricks/state/queries.py +149 -0
- contractforge_databricks/state/sql.py +116 -0
- contractforge_databricks/state/tables.py +9 -0
- contractforge_databricks/state/writer.py +83 -0
- contractforge_databricks/templates/__init__.py +15 -0
- contractforge_databricks/templates/catalog.py +205 -0
- contractforge_databricks/templates/catalog_parity.py +85 -0
- contractforge_databricks/templates/core.py +83 -0
- contractforge_databricks/templates/enrichment.py +175 -0
- contractforge_databricks/transforms/__init__.py +3 -0
- contractforge_databricks/transforms/sql.py +118 -0
- contractforge_databricks/watermark/__init__.py +6 -0
- contractforge_databricks/watermark/sql.py +91 -0
- contractforge_databricks/write_modes/__init__.py +20 -0
- contractforge_databricks/write_modes/registry.py +44 -0
- contractforge_databricks/write_modes/sql.py +33 -0
- contractforge_databricks/write_modes/strategy.py +192 -0
- contractforge_databricks-0.1.0.dist-info/METADATA +34 -0
- contractforge_databricks-0.1.0.dist-info/RECORD +220 -0
- contractforge_databricks-0.1.0.dist-info/WHEEL +4 -0
- contractforge_databricks-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Maintenance CLI commands for the Databricks adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from contractforge_databricks.cost import CostModel, build_operational_cost_report
|
|
9
|
+
from contractforge_databricks.maintenance import build_control_retention_plan
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_maintenance_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
13
|
+
maintenance = subparsers.add_parser("maintenance", help="Render Databricks operational maintenance SQL")
|
|
14
|
+
maintenance_sub = maintenance.add_subparsers(dest="maintenance_command", required=True)
|
|
15
|
+
retention = maintenance_sub.add_parser("ctrl-retention")
|
|
16
|
+
retention.add_argument("--catalog", default="main")
|
|
17
|
+
retention.add_argument("--schema", default="ops")
|
|
18
|
+
retention.add_argument("--retention-days", required=True, type=int)
|
|
19
|
+
retention.add_argument("--target", dest="targets", action="append")
|
|
20
|
+
retention.add_argument("--vacuum", action="store_true")
|
|
21
|
+
retention.add_argument("--vacuum-retention-hours", type=int, default=168)
|
|
22
|
+
retention.add_argument("--indent", type=int, default=2)
|
|
23
|
+
cost = maintenance_sub.add_parser("cost-report")
|
|
24
|
+
cost.add_argument("--catalog", default="main")
|
|
25
|
+
cost.add_argument("--schema", default="ops")
|
|
26
|
+
cost.add_argument("--lookback-days", type=int, default=30)
|
|
27
|
+
cost.add_argument("--group-by", action="append")
|
|
28
|
+
cost.add_argument("--dbu-per-hour", type=float)
|
|
29
|
+
cost.add_argument("--currency-per-dbu", type=float)
|
|
30
|
+
cost.add_argument("--currency", default="USD")
|
|
31
|
+
cost.add_argument("--success-only", action="store_true")
|
|
32
|
+
cost.add_argument("--limit", type=int, default=100)
|
|
33
|
+
cost.add_argument("--indent", type=int, default=2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def maintenance_command(args: argparse.Namespace) -> int:
|
|
37
|
+
if args.maintenance_command == "cost-report":
|
|
38
|
+
return _cost_report(args)
|
|
39
|
+
if args.maintenance_command != "ctrl-retention":
|
|
40
|
+
raise ValueError(f"unsupported maintenance command: {args.maintenance_command}")
|
|
41
|
+
plan = build_control_retention_plan(
|
|
42
|
+
catalog=args.catalog,
|
|
43
|
+
schema=args.schema,
|
|
44
|
+
retention_days=args.retention_days,
|
|
45
|
+
vacuum=args.vacuum,
|
|
46
|
+
vacuum_retention_hours=args.vacuum_retention_hours,
|
|
47
|
+
targets=args.targets,
|
|
48
|
+
)
|
|
49
|
+
print(json.dumps({"status": "DRY_RUN", "plan": plan}, indent=args.indent, sort_keys=True, default=str))
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cost_report(args: argparse.Namespace) -> int:
|
|
54
|
+
report = build_operational_cost_report(
|
|
55
|
+
catalog=args.catalog,
|
|
56
|
+
schema=args.schema,
|
|
57
|
+
lookback_days=args.lookback_days,
|
|
58
|
+
group_by=tuple(args.group_by or ("target_table", "mode", "status")),
|
|
59
|
+
cost_model=CostModel(
|
|
60
|
+
dbu_per_hour=args.dbu_per_hour,
|
|
61
|
+
currency_per_dbu=args.currency_per_dbu,
|
|
62
|
+
currency=args.currency,
|
|
63
|
+
),
|
|
64
|
+
include_failed=not args.success_only,
|
|
65
|
+
query_only=True,
|
|
66
|
+
limit=args.limit,
|
|
67
|
+
)
|
|
68
|
+
print(json.dumps(report, indent=args.indent, sort_keys=True, default=str))
|
|
69
|
+
return 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Small adapter-local coercion helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def mapping(value: object) -> dict[str, Any]:
|
|
10
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mapping_list(value: object) -> list[dict[str, Any]]:
|
|
14
|
+
return [dict(item) for item in value or () if isinstance(item, Mapping)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def string_list(value: object, *, sep: str | None = None) -> list[str]:
|
|
18
|
+
if value is None:
|
|
19
|
+
return []
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
items = value.split(sep) if sep else (value,)
|
|
22
|
+
return [item.strip() for item in items if item.strip()]
|
|
23
|
+
if not isinstance(value, Iterable):
|
|
24
|
+
return [str(value)]
|
|
25
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def string_map(value: object) -> dict[str, str]:
|
|
29
|
+
if not isinstance(value, Mapping):
|
|
30
|
+
return {}
|
|
31
|
+
return {str(key): str(item).lower() if isinstance(item, bool) else str(item) for key, item in value.items()}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Databricks contract extension utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from contractforge_core.planner import PlanningWarning
|
|
9
|
+
|
|
10
|
+
DATABRICKS_EXTENSION_FIELDS = {
|
|
11
|
+
"allow_type_widening",
|
|
12
|
+
"autoloader",
|
|
13
|
+
"cache_source",
|
|
14
|
+
"cluster_columns",
|
|
15
|
+
"delta_properties",
|
|
16
|
+
"encoding",
|
|
17
|
+
"encoding_columns",
|
|
18
|
+
"explain_format",
|
|
19
|
+
"explain_mode",
|
|
20
|
+
"fix_encoding",
|
|
21
|
+
"hooks",
|
|
22
|
+
"lakeflow",
|
|
23
|
+
"lock_enabled",
|
|
24
|
+
"merge_partition_column",
|
|
25
|
+
"merge_strategy",
|
|
26
|
+
"openlineage_enabled",
|
|
27
|
+
"openlineage_namespace",
|
|
28
|
+
"openlineage_producer",
|
|
29
|
+
"optimize_after_write",
|
|
30
|
+
"partition_column",
|
|
31
|
+
"partition_columns",
|
|
32
|
+
"partition_value",
|
|
33
|
+
"replace_partitions_source_complete",
|
|
34
|
+
"write_engine",
|
|
35
|
+
"zorder_columns",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_databricks_contract(contract: dict[str, Any]) -> dict[str, Any]:
|
|
40
|
+
"""Return a defensive copy of a Databricks contract mapping.
|
|
41
|
+
|
|
42
|
+
Databricks-owned execution fields must be declared explicitly under
|
|
43
|
+
``extensions.databricks``. The adapter does not translate top-level aliases.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
return deepcopy(contract)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def databricks_extensions(contract: Any) -> dict[str, Any]:
|
|
50
|
+
extensions = getattr(contract, "extensions", None)
|
|
51
|
+
if not isinstance(extensions, dict):
|
|
52
|
+
return {}
|
|
53
|
+
value = extensions.get("databricks")
|
|
54
|
+
return dict(value) if isinstance(value, dict) else {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def databricks_extension_warnings(contract: Any) -> tuple[PlanningWarning, ...]:
|
|
58
|
+
"""Return warnings for Databricks extension keys the adapter will ignore."""
|
|
59
|
+
|
|
60
|
+
unknown = sorted(set(databricks_extensions(contract)) - DATABRICKS_EXTENSION_FIELDS)
|
|
61
|
+
return tuple(
|
|
62
|
+
PlanningWarning(
|
|
63
|
+
code="DATABRICKS_UNKNOWN_EXTENSION",
|
|
64
|
+
message=(
|
|
65
|
+
f"extensions.databricks.{name} is not a recognized Databricks adapter extension "
|
|
66
|
+
"and will not be honored by planning, rendering or runtime execution."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
for name in unknown
|
|
70
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from contractforge_databricks.cost.model import CostModel
|
|
2
|
+
from contractforge_databricks.cost.report import build_operational_cost_report
|
|
3
|
+
from contractforge_databricks.cost.sql import DEFAULT_COST_GROUP_BY, VALID_COST_GROUP_FIELDS, render_operational_cost_query
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CostModel",
|
|
7
|
+
"DEFAULT_COST_GROUP_BY",
|
|
8
|
+
"VALID_COST_GROUP_FIELDS",
|
|
9
|
+
"build_operational_cost_report",
|
|
10
|
+
"render_operational_cost_query",
|
|
11
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Databricks logical cost model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class CostModel:
|
|
10
|
+
dbu_per_hour: float | None = None
|
|
11
|
+
currency_per_dbu: float | None = None
|
|
12
|
+
currency: str = "USD"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def enabled(self) -> bool:
|
|
16
|
+
return self.dbu_per_hour is not None and self.currency_per_dbu is not None
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def hourly_rate(self) -> float | None:
|
|
20
|
+
if not self.enabled:
|
|
21
|
+
return None
|
|
22
|
+
return float(self.dbu_per_hour or 0.0) * float(self.currency_per_dbu or 0.0)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""JSON-friendly cost report planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from contractforge_databricks.cost.model import CostModel
|
|
8
|
+
from contractforge_databricks.cost.sql import DEFAULT_COST_GROUP_BY, render_operational_cost_query
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_operational_cost_report(
|
|
12
|
+
*,
|
|
13
|
+
catalog: str = "main",
|
|
14
|
+
schema: str = "ops",
|
|
15
|
+
lookback_days: int = 30,
|
|
16
|
+
group_by: tuple[str, ...] = DEFAULT_COST_GROUP_BY,
|
|
17
|
+
cost_model: CostModel | None = None,
|
|
18
|
+
include_failed: bool = True,
|
|
19
|
+
query_only: bool = True,
|
|
20
|
+
runner: Any | None = None,
|
|
21
|
+
limit: int = 100,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
if limit < 1:
|
|
24
|
+
raise ValueError("limit must be greater than or equal to 1")
|
|
25
|
+
model = cost_model or CostModel()
|
|
26
|
+
query = render_operational_cost_query(
|
|
27
|
+
catalog=catalog,
|
|
28
|
+
schema=schema,
|
|
29
|
+
lookback_days=lookback_days,
|
|
30
|
+
group_by=group_by,
|
|
31
|
+
cost_model=model,
|
|
32
|
+
include_failed=include_failed,
|
|
33
|
+
)
|
|
34
|
+
rows = [] if query_only or runner is None else _collect_rows(runner.sql(f"{query}\nLIMIT {int(limit)}"))
|
|
35
|
+
return {
|
|
36
|
+
"status": "QUERY_ONLY" if query_only or runner is None else "SUCCESS",
|
|
37
|
+
"catalog": catalog,
|
|
38
|
+
"schema": schema,
|
|
39
|
+
"lookback_days": lookback_days,
|
|
40
|
+
"group_by": list(group_by),
|
|
41
|
+
"include_failed": include_failed,
|
|
42
|
+
"limit": limit,
|
|
43
|
+
"cost_model": {
|
|
44
|
+
"enabled": model.enabled,
|
|
45
|
+
"dbu_per_hour": model.dbu_per_hour,
|
|
46
|
+
"currency_per_dbu": model.currency_per_dbu,
|
|
47
|
+
"currency": model.currency,
|
|
48
|
+
"hourly_rate": model.hourly_rate,
|
|
49
|
+
},
|
|
50
|
+
"query": query,
|
|
51
|
+
"rows": rows,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _collect_rows(result: Any) -> list[dict[str, Any]]:
|
|
56
|
+
collected = result.collect() if hasattr(result, "collect") else result
|
|
57
|
+
rows = []
|
|
58
|
+
for row in collected or []:
|
|
59
|
+
if hasattr(row, "asDict"):
|
|
60
|
+
rows.append(row.asDict(recursive=True))
|
|
61
|
+
elif isinstance(row, dict):
|
|
62
|
+
rows.append(dict(row))
|
|
63
|
+
else:
|
|
64
|
+
rows.append(dict(row))
|
|
65
|
+
return rows
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Databricks operational cost SQL rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from contractforge_databricks.cost.model import CostModel
|
|
8
|
+
from contractforge_databricks.evidence import evidence_table_names
|
|
9
|
+
from contractforge_databricks.sql import quote_identifier, quote_table_name, sql_string
|
|
10
|
+
|
|
11
|
+
VALID_COST_GROUP_FIELDS = {
|
|
12
|
+
"contract_domain",
|
|
13
|
+
"contract_owner",
|
|
14
|
+
"criticality",
|
|
15
|
+
"layer",
|
|
16
|
+
"target_table",
|
|
17
|
+
"mode",
|
|
18
|
+
"runtime_type",
|
|
19
|
+
"source_connector",
|
|
20
|
+
"source_provider",
|
|
21
|
+
"status",
|
|
22
|
+
}
|
|
23
|
+
DEFAULT_COST_GROUP_BY = ("target_table", "layer", "mode", "status")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render_operational_cost_query(
|
|
27
|
+
*,
|
|
28
|
+
catalog: str = "main",
|
|
29
|
+
schema: str = "ops",
|
|
30
|
+
lookback_days: int = 30,
|
|
31
|
+
group_by: Iterable[str] | None = None,
|
|
32
|
+
cost_model: CostModel | None = None,
|
|
33
|
+
include_failed: bool = True,
|
|
34
|
+
) -> str:
|
|
35
|
+
if lookback_days < 1:
|
|
36
|
+
raise ValueError("lookback_days must be greater than or equal to 1")
|
|
37
|
+
fields = _normalize_group_by(group_by)
|
|
38
|
+
model = cost_model or CostModel()
|
|
39
|
+
_validate_float("dbu_per_hour", model.dbu_per_hour)
|
|
40
|
+
_validate_float("currency_per_dbu", model.currency_per_dbu)
|
|
41
|
+
runs_table = evidence_table_names(catalog, schema)["runs"]
|
|
42
|
+
status_filter = "" if include_failed else "AND status = 'SUCCESS'"
|
|
43
|
+
hourly_rate = "NULL" if model.hourly_rate is None else repr(float(model.hourly_rate))
|
|
44
|
+
return f"""
|
|
45
|
+
WITH base AS (
|
|
46
|
+
SELECT
|
|
47
|
+
target_table,
|
|
48
|
+
layer,
|
|
49
|
+
mode,
|
|
50
|
+
status,
|
|
51
|
+
contract_domain,
|
|
52
|
+
contract_owner,
|
|
53
|
+
runtime_type,
|
|
54
|
+
source_connector,
|
|
55
|
+
source_provider,
|
|
56
|
+
COALESCE(
|
|
57
|
+
get_json_object(operations_json, '$.metadata.criticality'),
|
|
58
|
+
get_json_object(operations_json, '$.criticality'),
|
|
59
|
+
'unknown'
|
|
60
|
+
) AS criticality,
|
|
61
|
+
CAST(COALESCE(rows_read, 0) AS BIGINT) AS rows_read,
|
|
62
|
+
CAST(COALESCE(rows_written, 0) AS BIGINT) AS rows_written,
|
|
63
|
+
CAST(COALESCE(rows_quarantined, 0) AS BIGINT) AS rows_quarantined,
|
|
64
|
+
CAST(COALESCE(duration_seconds, 0.0) AS DOUBLE) AS duration_seconds,
|
|
65
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.read'), '0') AS DOUBLE) AS read_seconds,
|
|
66
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.prepare'), '0') AS DOUBLE) AS prepare_seconds,
|
|
67
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.quality'), '0') AS DOUBLE) AS quality_seconds,
|
|
68
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.schema'), '0') AS DOUBLE) AS schema_seconds,
|
|
69
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.preflight'), '0') AS DOUBLE) AS preflight_seconds,
|
|
70
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.write'), '0') AS DOUBLE) AS write_seconds,
|
|
71
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.maintenance'), '0') AS DOUBLE) AS maintenance_seconds,
|
|
72
|
+
CAST(COALESCE(get_json_object(stage_durations_json, '$.governance'), '0') AS DOUBLE) AS governance_seconds
|
|
73
|
+
FROM {quote_table_name(runs_table)}
|
|
74
|
+
WHERE run_date >= date_sub(current_date(), {int(lookback_days)})
|
|
75
|
+
{status_filter}
|
|
76
|
+
),
|
|
77
|
+
agg AS (
|
|
78
|
+
SELECT
|
|
79
|
+
{_group_select(fields)},
|
|
80
|
+
COUNT(*) AS runs,
|
|
81
|
+
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS successful_runs,
|
|
82
|
+
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed_runs,
|
|
83
|
+
SUM(rows_read) AS rows_read,
|
|
84
|
+
SUM(rows_written) AS rows_written,
|
|
85
|
+
SUM(rows_quarantined) AS rows_quarantined,
|
|
86
|
+
SUM(duration_seconds) AS duration_seconds,
|
|
87
|
+
SUM(read_seconds) AS read_seconds,
|
|
88
|
+
SUM(prepare_seconds) AS prepare_seconds,
|
|
89
|
+
SUM(quality_seconds) AS quality_seconds,
|
|
90
|
+
SUM(schema_seconds) AS schema_seconds,
|
|
91
|
+
SUM(preflight_seconds) AS preflight_seconds,
|
|
92
|
+
SUM(write_seconds) AS write_seconds,
|
|
93
|
+
SUM(maintenance_seconds) AS maintenance_seconds,
|
|
94
|
+
SUM(governance_seconds) AS governance_seconds
|
|
95
|
+
FROM base
|
|
96
|
+
GROUP BY {_group_by(fields)}
|
|
97
|
+
)
|
|
98
|
+
SELECT
|
|
99
|
+
*,
|
|
100
|
+
CASE WHEN duration_seconds > 0 THEN rows_written / duration_seconds ELSE NULL END AS rows_written_per_second,
|
|
101
|
+
CASE WHEN duration_seconds > 0 THEN rows_read / duration_seconds ELSE NULL END AS rows_read_per_second,
|
|
102
|
+
CASE WHEN runs > 0 THEN duration_seconds / runs ELSE NULL END AS avg_duration_seconds,
|
|
103
|
+
{hourly_rate} AS estimated_hourly_rate,
|
|
104
|
+
{sql_string(model.currency)} AS estimated_currency,
|
|
105
|
+
CASE WHEN {hourly_rate} IS NULL THEN NULL ELSE duration_seconds / 3600.0 * {hourly_rate} END AS estimated_compute_cost,
|
|
106
|
+
CASE
|
|
107
|
+
WHEN {hourly_rate} IS NULL OR rows_written <= 0 THEN NULL
|
|
108
|
+
ELSE (duration_seconds / 3600.0 * {hourly_rate}) / (rows_written / 1000000.0)
|
|
109
|
+
END AS estimated_cost_per_million_rows,
|
|
110
|
+
'estimated_from_evidence_runs' AS cost_source
|
|
111
|
+
FROM agg
|
|
112
|
+
ORDER BY estimated_compute_cost DESC NULLS LAST, duration_seconds DESC
|
|
113
|
+
""".strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _normalize_group_by(group_by: Iterable[str] | None) -> tuple[str, ...]:
|
|
117
|
+
fields = tuple(group_by or DEFAULT_COST_GROUP_BY)
|
|
118
|
+
if not fields:
|
|
119
|
+
raise ValueError("group_by must contain at least one field")
|
|
120
|
+
unknown = sorted(set(fields) - VALID_COST_GROUP_FIELDS)
|
|
121
|
+
if unknown:
|
|
122
|
+
raise ValueError(f"unknown group_by fields: {unknown}")
|
|
123
|
+
return fields
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _validate_float(name: str, value: float | None) -> None:
|
|
127
|
+
if value is not None and value < 0:
|
|
128
|
+
raise ValueError(f"{name} must be greater than or equal to 0")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _group_select(fields: tuple[str, ...]) -> str:
|
|
132
|
+
return ",\n ".join(quote_identifier(field) for field in fields)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _group_by(fields: tuple[str, ...]) -> str:
|
|
136
|
+
return ", ".join(quote_identifier(field) for field in fields)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from contractforge_databricks.dashboards.control_tables import (
|
|
2
|
+
DashboardQuery,
|
|
3
|
+
control_dashboard_blueprint,
|
|
4
|
+
control_dashboard_queries,
|
|
5
|
+
render_control_dashboard_artifacts,
|
|
6
|
+
render_control_dashboard_sql,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DashboardQuery",
|
|
11
|
+
"control_dashboard_blueprint",
|
|
12
|
+
"control_dashboard_queries",
|
|
13
|
+
"render_control_dashboard_artifacts",
|
|
14
|
+
"render_control_dashboard_sql",
|
|
15
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Databricks dashboard artifacts over ContractForge control tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contractforge_core.reporting import DashboardQuery
|
|
6
|
+
from contractforge_databricks.sql import quote_table_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def control_dashboard_queries(*, catalog: str = "main", schema: str = "ops", lookback_days: int = 7) -> tuple[DashboardQuery, ...]:
|
|
10
|
+
t = _tables(catalog, schema)
|
|
11
|
+
days = int(lookback_days)
|
|
12
|
+
return (
|
|
13
|
+
_q("q01_executive_kpis", "Control Tower", "kpi_card_strip", f"""
|
|
14
|
+
SELECT count(*) AS total_runs,
|
|
15
|
+
sum(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS successful_runs,
|
|
16
|
+
sum(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed_runs,
|
|
17
|
+
round(100.0 * sum(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) / nullif(count(*), 0), 2) AS success_rate_pct,
|
|
18
|
+
count(DISTINCT target_table) AS active_targets,
|
|
19
|
+
sum(coalesce(rows_read, 0)) AS rows_read,
|
|
20
|
+
sum(coalesce(rows_written, 0)) AS rows_written,
|
|
21
|
+
sum(coalesce(rows_quarantined, 0)) AS rows_quarantined
|
|
22
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days})"""),
|
|
23
|
+
_q("q02_status_trend", "Run Health Trend", "stacked_area", f"""
|
|
24
|
+
SELECT run_date, layer, status, count(*) AS runs, sum(coalesce(rows_written, 0)) AS rows_written
|
|
25
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days})
|
|
26
|
+
GROUP BY run_date, layer, status ORDER BY run_date, layer, status"""),
|
|
27
|
+
_q("q03_latest_target_health", "Target Health Radar", "table_with_conditional_formatting", f"""
|
|
28
|
+
WITH ranked AS (
|
|
29
|
+
SELECT *, row_number() OVER (PARTITION BY target_table ORDER BY run_ts_utc DESC, finished_at_utc DESC) AS rn
|
|
30
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days})
|
|
31
|
+
)
|
|
32
|
+
SELECT target_table, layer, mode, status, quality_status, rows_read, rows_written,
|
|
33
|
+
rows_quarantined, duration_seconds, finished_at_utc, runtime_type, error_message
|
|
34
|
+
FROM ranked WHERE rn = 1 ORDER BY status, target_table"""),
|
|
35
|
+
_q("q04_recent_failures", "Latest Incidents", "table", f"""
|
|
36
|
+
SELECT e.error_ts_utc, e.target_table, r.layer, e.mode, e.error_type, e.error_message, r.run_id
|
|
37
|
+
FROM {t['errors']} e LEFT JOIN {t['runs']} r ON e.run_id = r.run_id
|
|
38
|
+
WHERE e.error_date >= date_sub(current_date(), {days}) ORDER BY e.error_ts_utc DESC"""),
|
|
39
|
+
_q("q05_target_reliability", "Target Reliability Matrix", "heatmap_or_table", f"""
|
|
40
|
+
SELECT target_table, layer, mode, count(*) AS runs,
|
|
41
|
+
sum(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed_runs,
|
|
42
|
+
round(100.0 * sum(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) / nullif(count(*), 0), 2) AS success_rate_pct
|
|
43
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days})
|
|
44
|
+
GROUP BY target_table, layer, mode ORDER BY failed_runs DESC, success_rate_pct ASC"""),
|
|
45
|
+
_q("q06_sla_freshness", "Freshness SLA Board", "table_with_status_colors", f"""
|
|
46
|
+
WITH latest_success AS (
|
|
47
|
+
SELECT target_table, max(finished_at_utc) AS last_success_at_utc FROM {t['runs']} WHERE status = 'SUCCESS' GROUP BY target_table
|
|
48
|
+
), ops AS (
|
|
49
|
+
SELECT *, row_number() OVER (PARTITION BY target_table ORDER BY recorded_at_utc DESC) AS rn FROM {t['operations']}
|
|
50
|
+
)
|
|
51
|
+
SELECT o.target_table, o.criticality, o.expected_frequency, o.freshness_sla_minutes, s.last_success_at_utc,
|
|
52
|
+
CASE WHEN s.last_success_at_utc IS NULL THEN 'NO_SUCCESS'
|
|
53
|
+
WHEN o.freshness_sla_minutes IS NULL THEN 'NO_SLA'
|
|
54
|
+
WHEN (unix_timestamp(current_timestamp()) - unix_timestamp(s.last_success_at_utc)) / 60 > o.freshness_sla_minutes THEN 'BREACHED'
|
|
55
|
+
ELSE 'OK' END AS freshness_status, o.runbook_url
|
|
56
|
+
FROM ops o LEFT JOIN latest_success s ON o.target_table = s.target_table WHERE o.rn = 1"""),
|
|
57
|
+
_q("q07_failure_taxonomy", "Failure Taxonomy", "horizontal_bar", f"""
|
|
58
|
+
SELECT coalesce(error_type, 'unknown') AS error_type, count(*) AS failures, count(DISTINCT target_table) AS affected_targets
|
|
59
|
+
FROM {t['errors']} WHERE error_date >= date_sub(current_date(), {days})
|
|
60
|
+
GROUP BY coalesce(error_type, 'unknown') ORDER BY failures DESC"""),
|
|
61
|
+
_q("q08_error_drilldown", "Error Drilldown", "table", f"SELECT * FROM {t['errors']} WHERE error_date >= date_sub(current_date(), {days}) ORDER BY error_ts_utc DESC"),
|
|
62
|
+
_q("q09_duration_percentiles", "Duration Percentiles by Mode", "grouped_bar", f"""
|
|
63
|
+
SELECT layer, mode, count(*) AS successful_runs, round(avg(duration_seconds), 2) AS avg_duration_seconds,
|
|
64
|
+
round(percentile_approx(duration_seconds, 0.95), 2) AS p95_duration_seconds
|
|
65
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) AND status = 'SUCCESS'
|
|
66
|
+
GROUP BY layer, mode ORDER BY p95_duration_seconds DESC"""),
|
|
67
|
+
_q("q10_stage_duration_breakdown", "Stage Bottlenecks", "stacked_bar", f"SELECT run_id, target_table, stage_durations_json FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) AND status = 'SUCCESS'"),
|
|
68
|
+
_q("q11_throughput_by_target", "Throughput by Target", "scatter_or_table", f"""
|
|
69
|
+
SELECT target_table, layer, mode, sum(rows_written) AS rows_written,
|
|
70
|
+
round(sum(rows_written) / nullif(sum(duration_seconds), 0), 2) AS rows_written_per_second
|
|
71
|
+
FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) AND status = 'SUCCESS'
|
|
72
|
+
GROUP BY target_table, layer, mode ORDER BY rows_written_per_second ASC NULLS LAST"""),
|
|
73
|
+
_q("q12_slowest_runs", "Slowest Successful Runs", "table", f"SELECT run_ts_utc, target_table, mode, duration_seconds, rows_written, run_id FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) AND status = 'SUCCESS' ORDER BY duration_seconds DESC LIMIT 50"),
|
|
74
|
+
_q("q13_quality_summary", "Quality Outcomes", "stacked_bar", f"SELECT status, severity, count(*) AS rule_evaluations, sum(failed_count) AS failed_count FROM {t['quality']} GROUP BY status, severity"),
|
|
75
|
+
_q("q14_quality_rules_hotspots", "Rule Hotspots", "horizontal_bar", f"SELECT target_table, rule_name, status, sum(failed_count) AS failed_count FROM {t['quality']} GROUP BY target_table, rule_name, status ORDER BY failed_count DESC"),
|
|
76
|
+
_q("q15_quarantine_hotspots", "Quarantine Drilldown", "table", f"SELECT target_table, rule_name, count(*) AS quarantined_records FROM {t['quarantine']} GROUP BY target_table, rule_name ORDER BY quarantined_records DESC"),
|
|
77
|
+
_q("q16_effective_rows", "Useful Rows vs Quarantine", "stacked_bar", f"SELECT target_table, sum(rows_read) AS rows_read, sum(rows_written) AS rows_written, sum(rows_quarantined) AS rows_quarantined FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) GROUP BY target_table"),
|
|
78
|
+
_q("q17_stream_kpis", "Stream Control Tower", "kpi_card_strip", f"SELECT count(*) AS stream_runs, sum(batches_processed) AS batches_processed, sum(total_rows_written) AS total_rows_written FROM {t['streams']} WHERE started_at_utc >= current_timestamp() - INTERVAL {days} DAYS"),
|
|
79
|
+
_q("q18_stream_runs", "Stream Runs", "table", f"SELECT stream_run_id, target_table, source_type, trigger, status, batches_processed, total_rows_written, started_at_utc, ended_at_utc FROM {t['streams']} ORDER BY started_at_utc DESC"),
|
|
80
|
+
_q("q19_stream_child_reconciliation", "Parent/Child Reconciliation", "table_with_status_colors", f"SELECT stream_run_id, target_table, status, batches_processed, total_rows_read, total_rows_written FROM {t['streams']} ORDER BY started_at_utc DESC"),
|
|
81
|
+
_q("q20_connector_runtime_matrix", "Connector and Runtime Matrix", "grouped_bar_or_heatmap", f"SELECT source_connector, source_provider, source_format, runtime_type, count(*) AS runs FROM {t['runs']} WHERE run_date >= date_sub(current_date(), {days}) GROUP BY source_connector, source_provider, source_format, runtime_type"),
|
|
82
|
+
_q("q21_operations_coverage", "Operations Coverage", "table_with_completeness_score", f"SELECT target_table, criticality, expected_frequency, freshness_sla_minutes, runbook_url, status FROM {t['operations']}"),
|
|
83
|
+
_q("q22_governance_artifacts", "Governance Artifacts", "table", f"""
|
|
84
|
+
SELECT coalesce(s.target_table, a.target_table, x.target_table) AS target_table,
|
|
85
|
+
s.schema_change_events, a.annotation_events, x.access_events
|
|
86
|
+
FROM (SELECT target_table, count(*) AS schema_change_events FROM {t['schema_changes']} GROUP BY target_table) s
|
|
87
|
+
FULL OUTER JOIN (SELECT target_table, count(*) AS annotation_events FROM {t['annotations']} GROUP BY target_table) a ON s.target_table = a.target_table
|
|
88
|
+
FULL OUTER JOIN (SELECT target_table, count(*) AS access_events FROM {t['access']} GROUP BY target_table) x ON coalesce(s.target_table, a.target_table) = x.target_table"""),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_control_dashboard_sql(*, catalog: str = "main", schema: str = "ops", lookback_days: int = 7) -> str:
|
|
93
|
+
blocks = ["-- ContractForge Operations Command Center", "-- Databricks SQL dashboard queries"]
|
|
94
|
+
for query in control_dashboard_queries(catalog=catalog, schema=schema, lookback_days=lookback_days):
|
|
95
|
+
blocks.extend(["", f"-- {query.name}", f"-- Visualization: {query.visualization}", _clean_sql(query.sql) + ";"])
|
|
96
|
+
return "\n".join(blocks) + "\n"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def control_dashboard_blueprint(*, catalog: str = "main", schema: str = "ops", lookback_days: int = 7) -> dict[str, object]:
|
|
100
|
+
queries = control_dashboard_queries(catalog=catalog, schema=schema, lookback_days=lookback_days)
|
|
101
|
+
return {
|
|
102
|
+
"title": "ContractForge Operations Command Center",
|
|
103
|
+
"data_source": {"catalog": catalog, "schema": schema, "lookback_days": lookback_days},
|
|
104
|
+
"pages": {
|
|
105
|
+
"overview": ["q01_executive_kpis", "q02_status_trend", "q03_latest_target_health", "q04_recent_failures"],
|
|
106
|
+
"reliability": ["q06_sla_freshness", "q05_target_reliability", "q07_failure_taxonomy", "q08_error_drilldown"],
|
|
107
|
+
"performance": ["q09_duration_percentiles", "q10_stage_duration_breakdown", "q11_throughput_by_target", "q12_slowest_runs"],
|
|
108
|
+
"quality": ["q13_quality_summary", "q14_quality_rules_hotspots", "q16_effective_rows", "q15_quarantine_hotspots"],
|
|
109
|
+
"streaming": ["q17_stream_kpis", "q19_stream_child_reconciliation", "q18_stream_runs"],
|
|
110
|
+
"connectors_governance": ["q20_connector_runtime_matrix", "q21_operations_coverage", "q22_governance_artifacts"],
|
|
111
|
+
},
|
|
112
|
+
"queries": [query.__dict__ for query in queries],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def render_control_dashboard_artifacts(*, catalog: str = "main", schema: str = "ops", lookback_days: int = 7) -> dict[str, str]:
|
|
117
|
+
import json
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"control_tables_dashboard.sql": render_control_dashboard_sql(catalog=catalog, schema=schema, lookback_days=lookback_days),
|
|
121
|
+
"control_tables_dashboard_blueprint.json": json.dumps(
|
|
122
|
+
control_dashboard_blueprint(catalog=catalog, schema=schema, lookback_days=lookback_days),
|
|
123
|
+
indent=2,
|
|
124
|
+
sort_keys=True,
|
|
125
|
+
)
|
|
126
|
+
+ "\n",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _tables(catalog: str, schema: str) -> dict[str, str]:
|
|
131
|
+
return {
|
|
132
|
+
"runs": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_runs"),
|
|
133
|
+
"errors": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_errors"),
|
|
134
|
+
"quality": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_quality"),
|
|
135
|
+
"quarantine": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_quarantine"),
|
|
136
|
+
"schema_changes": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_schema_changes"),
|
|
137
|
+
"streams": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_streams"),
|
|
138
|
+
"annotations": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_annotations"),
|
|
139
|
+
"access": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_access"),
|
|
140
|
+
"operations": quote_table_name(f"{catalog}.{schema}.ctrl_ingestion_operations"),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _q(name: str, title: str, visualization: str, sql: str) -> DashboardQuery:
|
|
145
|
+
return DashboardQuery(name=name, title=title, visualization=visualization, sql=_clean_sql(sql))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _clean_sql(sql: str) -> str:
|
|
149
|
+
lines = [line.strip() for line in sql.strip().splitlines()]
|
|
150
|
+
return "\n".join(line for line in lines if line)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Databricks explain-plan diagnostic SQL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contractforge_core.diagnostics import ExplainPlanRecord
|
|
6
|
+
from contractforge_databricks.security import redact_text
|
|
7
|
+
from contractforge_databricks.sql import quote_table_name, sql_string
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_create_explain_table_sql(*, catalog: str = "main", schema: str = "ops") -> str:
|
|
11
|
+
table = f"{catalog}.{schema}.ctrl_ingestion_explain"
|
|
12
|
+
return "\n".join(
|
|
13
|
+
[
|
|
14
|
+
f"CREATE SCHEMA IF NOT EXISTS {quote_table_name(f'{catalog}.{schema}')};",
|
|
15
|
+
"",
|
|
16
|
+
f"CREATE TABLE IF NOT EXISTS {quote_table_name(table)} (",
|
|
17
|
+
" run_id STRING, target_table STRING, source_table STRING, mode STRING,",
|
|
18
|
+
" explain_format STRING, plan_text STRING, captured_at_utc TIMESTAMP",
|
|
19
|
+
")",
|
|
20
|
+
"USING DELTA;",
|
|
21
|
+
"",
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render_explain_insert_sql(
|
|
27
|
+
record: ExplainPlanRecord,
|
|
28
|
+
*,
|
|
29
|
+
catalog: str = "main",
|
|
30
|
+
schema: str = "ops",
|
|
31
|
+
truncate_at: int = 100_000,
|
|
32
|
+
) -> str:
|
|
33
|
+
table = f"{catalog}.{schema}.ctrl_ingestion_explain"
|
|
34
|
+
plan_text = redact_text(record.plan_text)[:truncate_at]
|
|
35
|
+
return (
|
|
36
|
+
f"INSERT INTO {quote_table_name(table)} "
|
|
37
|
+
"(run_id, target_table, source_table, mode, explain_format, plan_text, captured_at_utc) VALUES "
|
|
38
|
+
f"({sql_string(record.run_id)}, {sql_string(record.target_table)}, {sql_string(record.source_name)}, "
|
|
39
|
+
f"{sql_string(record.mode)}, {sql_string(record.explain_format)}, {sql_string(plan_text)}, current_timestamp())"
|
|
40
|
+
)
|