Skip to content

Commit 917a8fa

Browse files
refactor(migration_v5): extract ontology-agnostic artifact pipeline (#172)
* refactor(migration_v5): extract ontology-agnostic artifact pipeline The migration v5 demo's TTL → binding/DDL/property-graph pipeline was hard-coded to MAKO. The transformations it applied (FILL_IN resolution, cross-namespace drop, inheritance strip) are general-purpose; only the config (TTL path, namespace IRI, entity allowlist, annotation prefix, graph name) is per-ontology. ## What ships - **``ontology_artifacts.py``** — new generic module. ``OntologyConfig`` dataclass + ``load_ontology``, ``make_binding``, ``make_table_ddl``, ``make_property_graph_sql``, ``regenerate_snapshots`` — all parameterized by config. The three OWL TTL normalizations stay (they apply to any reasonable ontology). Annotation keys use ``{config.annotation_prefix}:`` instead of a hard-coded ``mako_demo:``. - **``mako_artifacts.py``** — now a thin shim (~200 lines, down from 751). Defines ``MAKO_CONFIG`` and re-exports the public API (``regenerate_snapshots``, ``load_mako_ontology``, ``make_binding``, …) with that config bound in. The notebook's ``import mako_artifacts; mako_artifacts.regenerate_snapshots(project, dataset)`` works unchanged. - **``example_ontologies/simple_request_flow.ttl``** + config — tiny domain-neutral TTL (3 entities, 2 relationships) and matching ``OntologyConfig``. Proves the generic pipeline isn't accidentally MAKO-coupled. No runnable agent — smoke fixture only. - **``tests/test_migration_v5_ontology_artifacts.py``** — eight tests: * MAKO regeneration produces byte-identical output to the checked-in snapshots (guards against drift). * Generic pipeline accepts the Simple Request Flow config and produces a valid binding + DDL + property graph. * Per-entity PK naming convention applies to non-MAKO configs. * Graph name and annotation prefix are honored per-config. * ``OntologyConfig`` is frozen + path properties work. ## Doc changes - **``examples/migration_v5/README.md``** — leads with the pluggable contract: what ``OntologyConfig`` packages, the three transformations, the two example ontologies. The MAKO-specific design rationale stays but is reframed as "Reference example: MAKO design decisions". Authorship table updated to call out ``ontology_artifacts.py`` as the generic pipeline and the new ``example_ontologies/`` directory. - **``periodic_materialization/README.md``** — four MAKO references softened to "the demo's" / "the bound ontology" / "the migration v5 demo (MAKO is the canonical reference example, but the pipeline is ontology-agnostic)". ## What does NOT change - ``mako_core.ttl`` — unchanged. - ``mako_demo_agent.py``, ``run_agent.py``, ``reference_extractor.py`` — unchanged. The agent's tools mirror MAKO's decision flow; generalizing them would mean a different demo, not a generalization of this one. - ``examples/migration_v5_demo_notebook.ipynb`` — unchanged. The shim preserves the notebook's import. - Checked-in ``ontology.yaml`` / ``binding.yaml`` / ``table_ddl.sql`` / ``property_graph.sql`` for MAKO — byte-identical after the refactor (asserted by ``test_mako_regenerate_matches_checked_in_snapshots``). - SDK source under ``src/`` — untouched. This is an examples refactor only. ## Verification - ``PYTHONPATH=src pytest tests/test_migration_v5_ontology_artifacts.py tests/test_migration_v5_reference_extractor.py`` — 25/25 pass (8 new + 17 existing). - ``PYTHONPATH=src python examples/migration_v5/mako_artifacts.py`` — runs; ``git diff`` on the four MAKO snapshot files is empty. - ``pyink --check`` + ``isort --check-only`` clean on all touched + new files. * fix(migration_v5): bind declared owl:hasKey PK (PR #172 review) Addresses two findings from the PR #172 review: ## P1 — owl:hasKey ontologies broke make_binding The generic pipeline's first cut hard-coded ``"id"`` as the bound primary-key property name. That worked for MAKO (where the FILL_IN resolver synthesizes an ``id`` property on every entity) but broke any TTL that declared ``owl:hasKey``: the binding ended up declaring an ``id`` property the entity didn't have, and ``load_binding_from_string`` raised ``Entity binding 'X': property 'id' is not declared on this element``. Hard-failed the PR's "any OWL TTL" contract. Fix in ``ontology_artifacts.py``: * New ``_primary_key_property_name(entity)`` helper reads ``entity.keys.primary[0]`` (the normalization passes populate this for every entity — either the synthesized ``id`` or the TTL-declared key). * ``make_binding`` uses it instead of ``"id"`` when emitting the PK property + when skipping the PK in the property iteration loop. * ``make_property_graph_sql`` builds an entity → PK property name lookup from the ontology, then resolves the PK column through that name (not by matching ``p.name == "id"``). The fix preserves MAKO byte-identical output (MAKO entities still have ``keys.primary == ["id"]`` after FILL_IN resolution so the lookup returns the same property). The smoke fixture (``simple_request_flow.ttl``) didn't catch this because it only exercised the FILL_IN path. Updated to declare ``owl:hasKey ( rf:requestId )`` (and same for Action, Outcome) so the existing pluggability test now genuinely exercises the real-key path. Added two new regression tests: * ``test_simple_request_flow_binds_declared_owl_haskey_property`` — binding emits ``{name: requestId, column: request_id}``, not the synthesized ``id``. * ``test_simple_request_flow_property_graph_resolves_owl_haskey_pk_column`` — ``KEY (request_id)`` / ``REFERENCES request (request_id)`` resolve through the declared PK property, not a hard-coded ``id``. ## P2 — README overclaimed periodic materialization pluggability The README and playbook both implied "any OntologyConfig works" with the periodic materialization deploy. The deploy script actually bundles the checked-in MAKO ``binding.yaml`` / ``ontology.yaml`` / ``table_ddl.sql`` (verified in ``deploy_cloud_run_job.sh:421-423``). Running against a different config requires regenerating those snapshots and re-pointing the deploy at the new files — and the deploy script doesn't yet have a CLI flag for that. Softened both README sections to call this out explicitly; flagged the deploy script wiring as a natural follow-up. ## INFORMATIONAL — import mode consistency in tests The test was using package-style imports (``examples.migration_v5.X``) that masked the fact that the shim ``mako_artifacts.py`` only supports sibling-style imports (matching the notebook + ``run_agent.py``). Switched the test to sibling-style only so it exercises the same import surface real callers do. ## Verification * ``PYTHONPATH=src pytest tests/test_migration_v5_ontology_artifacts.py tests/test_migration_v5_reference_extractor.py`` — 27/27 pass (10 new + 17 existing). * ``PYTHONPATH=src python examples/migration_v5/mako_artifacts.py`` — outputs 18/6/7; ``git diff`` on the four MAKO snapshot files is empty. * ``pyink --check`` + ``isort --check-only`` clean. * docs(migration_v5): correct test count in README validation snippet PR #172 follow-up review caught the validation snippet still read ``# → 8 passed`` after the owl:hasKey regression commit added two tests (now 10). * fix(ci): skip ontology-artifacts tests when rdflib is absent CI runs ``pip install -e ".[dev]"`` only — the ``[owl]`` extra that brings in ``rdflib`` isn't installed. The new test module pulls ``ontology_artifacts``, which imports ``bigquery_ontology.owl_importer.import_owl``, which raises ``ImportError: rdflib is required for OWL import`` during collection. Whole module errored across Python 3.10–3.14. Add ``pytest.importorskip("rdflib")`` at the top, mirroring the existing pattern in ``tests/test_owl_import_bridge.py`` and ``tests/bigquery_ontology/test_owl_importer.py``. Locally (with ``[owl]`` installed) all 10 tests still pass; on CI the module skips cleanly instead of erroring collection. --------- Co-authored-by: Haiyuan Cao <haiyuan@google.com>
1 parent 3ef4337 commit 917a8fa

8 files changed

Lines changed: 1426 additions & 683 deletions

File tree

examples/migration_v5/README.md

Lines changed: 69 additions & 33 deletions
Large diffs are not rendered by default.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Pluggability smoke fixtures for the migration v5 artifact pipeline.
16+
17+
Each module in this package defines a small
18+
:class:`ontology_artifacts.OntologyConfig` paired with a TTL
19+
file. The configs aren't meant for production deploys — they
20+
exist to prove the pipeline in
21+
:mod:`ontology_artifacts` is genuinely ontology-agnostic.
22+
23+
For the canonical reference example, see
24+
:mod:`mako_artifacts` in the parent package.
25+
"""
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# =============================================================================
2+
# Simple Request Flow — pluggability smoke fixture for the migration v5 demo
3+
#
4+
# Purpose: prove the ontology pipeline in
5+
# ``examples/migration_v5/ontology_artifacts.py`` is genuinely
6+
# ontology-agnostic. This TTL is intentionally domain-neutral
7+
# (Request → Action → Outcome) and intentionally minimal: three
8+
# entities, two relationships, no cross-namespace imports, no
9+
# inheritance.
10+
#
11+
# Use: ``regenerate_snapshots`` against
12+
# :data:`example_ontologies.simple_request_flow_config.SIMPLE_REQUEST_FLOW_CONFIG`
13+
# produces a valid binding + DDL + property-graph SQL with no
14+
# MAKO involvement. Smoke fixture only — no runnable agent ships
15+
# with this ontology.
16+
#
17+
# Why these three entities: Request is the entry point, Action is the
18+
# response, Outcome is the result. The minimal viable shape for a
19+
# directed graph that exercises ``make_binding`` (heterogeneous edges,
20+
# property-column generation) and ``make_property_graph_sql`` (node +
21+
# edge tables with KEY / SOURCE KEY / DESTINATION KEY).
22+
# =============================================================================
23+
24+
@prefix rf: <https://example.com/request-flow/> .
25+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
26+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
27+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
28+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
29+
@prefix dcterms: <http://purl.org/dc/terms/> .
30+
31+
<https://example.com/request-flow/>
32+
a owl:Ontology ;
33+
owl:versionInfo "0.1.0" ;
34+
dcterms:title "Simple Request Flow — Pluggability Smoke Fixture" ;
35+
dcterms:description "Domain-neutral Request → Action → Outcome ontology used to prove the migration v5 artifact pipeline accepts any OWL TTL." ;
36+
dcterms:license <https://creativecommons.org/licenses/by/4.0/> .
37+
38+
# =============================================================================
39+
# Entities
40+
#
41+
# Each entity declares ``owl:hasKey`` so the pipeline exercises the real-key
42+
# path (not just the FILL_IN synthesis path MAKO uses). The PR review found
43+
# the prior version of this fixture didn't catch a bug where ``make_binding``
44+
# hard-coded ``id`` as the bound PK property; declaring ``owl:hasKey`` here
45+
# makes the regression test in ``tests/test_migration_v5_ontology_artifacts.py``
46+
# meaningful.
47+
# =============================================================================
48+
49+
rf:Request a owl:Class ;
50+
rdfs:label "Request" ;
51+
rdfs:comment "An incoming ask. The entry point of the flow." ;
52+
owl:hasKey ( rf:requestId ) .
53+
54+
rf:Action a owl:Class ;
55+
rdfs:label "Action" ;
56+
rdfs:comment "The work performed in response to a Request." ;
57+
owl:hasKey ( rf:actionId ) .
58+
59+
rf:Outcome a owl:Class ;
60+
rdfs:label "Outcome" ;
61+
rdfs:comment "The result of an Action. Terminal node of the flow." ;
62+
owl:hasKey ( rf:outcomeId ) .
63+
64+
# =============================================================================
65+
# Data properties
66+
# =============================================================================
67+
68+
rf:requestId a owl:DatatypeProperty ;
69+
rdfs:label "request id" ;
70+
rdfs:domain rf:Request ;
71+
rdfs:range xsd:string .
72+
73+
rf:requestText a owl:DatatypeProperty ;
74+
rdfs:label "request text" ;
75+
rdfs:domain rf:Request ;
76+
rdfs:range xsd:string .
77+
78+
rf:requestedAt a owl:DatatypeProperty ;
79+
rdfs:label "requested at" ;
80+
rdfs:domain rf:Request ;
81+
rdfs:range xsd:dateTime .
82+
83+
rf:actionId a owl:DatatypeProperty ;
84+
rdfs:label "action id" ;
85+
rdfs:domain rf:Action ;
86+
rdfs:range xsd:string .
87+
88+
rf:actionType a owl:DatatypeProperty ;
89+
rdfs:label "action type" ;
90+
rdfs:domain rf:Action ;
91+
rdfs:range xsd:string .
92+
93+
rf:executedAt a owl:DatatypeProperty ;
94+
rdfs:label "executed at" ;
95+
rdfs:domain rf:Action ;
96+
rdfs:range xsd:dateTime .
97+
98+
rf:outcomeId a owl:DatatypeProperty ;
99+
rdfs:label "outcome id" ;
100+
rdfs:domain rf:Outcome ;
101+
rdfs:range xsd:string .
102+
103+
rf:outcomeStatus a owl:DatatypeProperty ;
104+
rdfs:label "outcome status" ;
105+
rdfs:domain rf:Outcome ;
106+
rdfs:range xsd:string .
107+
108+
rf:completedAt a owl:DatatypeProperty ;
109+
rdfs:label "completed at" ;
110+
rdfs:domain rf:Outcome ;
111+
rdfs:range xsd:dateTime .
112+
113+
# =============================================================================
114+
# Object properties (relationships)
115+
# =============================================================================
116+
117+
rf:hasAction a owl:ObjectProperty ;
118+
rdfs:label "has action" ;
119+
rdfs:domain rf:Request ;
120+
rdfs:range rf:Action .
121+
122+
rf:producesOutcome a owl:ObjectProperty ;
123+
rdfs:label "produces outcome" ;
124+
rdfs:domain rf:Action ;
125+
rdfs:range rf:Outcome .
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Pluggability smoke fixture: Simple Request Flow ontology config.
16+
17+
Exists to prove the generic artifact pipeline in
18+
:mod:`ontology_artifacts` accepts any OWL TTL — not just
19+
MAKO's. The TTL is domain-neutral (Request → Action →
20+
Outcome) and intentionally simple (three entities, two
21+
relationships, no cross-namespace imports, no inheritance).
22+
23+
The :data:`SIMPLE_REQUEST_FLOW_CONFIG` plugs into
24+
:func:`ontology_artifacts.regenerate_snapshots` exactly the
25+
same way :data:`mako_artifacts.MAKO_CONFIG` does. No runnable
26+
agent ships with this fixture — it's a smoke test for the
27+
pipeline, not an alternate demo.
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import pathlib
33+
34+
from ontology_artifacts import OntologyConfig
35+
36+
_FIXTURE_DIR = pathlib.Path(__file__).parent
37+
38+
SIMPLE_REQUEST_FLOW_CONFIG = OntologyConfig(
39+
ttl_path=_FIXTURE_DIR / "simple_request_flow.ttl",
40+
include_namespace="https://example.com/request-flow/",
41+
entity_allowlist=("Request", "Action", "Outcome"),
42+
annotation_prefix="simple_request_flow",
43+
graph_name="simple_request_flow_graph",
44+
snapshot_dir=_FIXTURE_DIR,
45+
)

0 commit comments

Comments
 (0)