Skip to content

Commit 4c3a293

Browse files
authored
feat(cli): add bq-agent-sdk binding-validate + ontology-build --validate-binding flags (#105 PR 2b) (#112)
* feat(cli): add bq-agent-sdk binding-validate + ontology-build flags (#105 PR 2b) Closes the user-facing surface of issue #105. Builds on PR 2a's validate_binding_against_bigquery core (shipped in PR #109). CLI changes (src/bigquery_agent_analytics/cli.py) Standalone command: bq-agent-sdk binding-validate --project-id ... --ontology X.yaml --binding Y.yaml [--location US] [--strict] [--format json|text|table] Loads the ontology + binding via upstream loaders, builds a google.cloud.bigquery.Client, calls validate_binding_against_bigquery(...), prints a structured JSON report on stdout. Warnings always print to stderr (one line each) so CI logs surface advisory drift even when stdout is consumed by a JSON-aware tool. Exit codes: 0 ok, 1 failure(s), 2 unexpected error (load failure, missing flags, etc). ontology-build flags: --validate-binding (default-mode pre-flight) --validate-binding-strict (strict-mode pre-flight) When set, run the validator before phase 2 (extraction). On any failure, the build short-circuits BEFORE any AI.GENERATE call fires, so authoring drift never costs tokens. Default-mode warnings print to stderr but do not block. The two flags are mutually exclusive; both are incompatible with the deprecated --spec-path form because the validator needs the unresolved Ontology + Binding pair, not a combined GraphSpec. Helper _run_binding_preflight() centralizes the load + validate + print + exit logic so it can be invoked from ontology-build without duplicating the flow. Also: --location flag added to ontology-build, threaded through to build_ontology_graph() (which has supported it on the Python side since PR #108 / commit 292320b). The CLI was previously building without forwarding it. Tests (tests/test_cli.py) TestBindingValidate (6 tests): - test_clean_validation_exits_zero - test_failures_exit_one_with_payload - test_warnings_print_to_stderr_but_do_not_flip_exit - test_strict_flag_threaded_through - test_missing_required_flags_exit_2 - test_load_failure_exits_two TestOntologyBuildValidateBindingFlag (5 tests): - test_validate_binding_short_circuits_on_failure_before_build (asserts build_ontology_graph is NEVER called when validation fails — extraction does not start) - test_validate_binding_strict_short_circuits_on_nullable_keys - test_validate_binding_clean_proceeds_to_build - test_validate_binding_with_spec_path_rejected - test_both_flags_rejected (mutual exclusion) All tests use patched validators (no live BQ); the live integration test for the full flow lives in PR 2a (#109). 253/253 tests pass across the touched test files. Docs New: docs/ontology/binding-validation.md - When to run (binding authoring, CI gating, ontology-build). - Standalone CLI examples (default + strict). - ontology-build integration examples. - Failure-code reference table covering all 7 default codes plus the strict-only KEY_COLUMN_NULLABLE. - CI usage pattern (GitHub Actions). - Python API example. - Cross-links to ontology-build.md, binding.md, and #76's planned post-extraction validator. Updated: docs/README.md - Added binding-validation.md to the Ontology Reference table. What's NOT in this PR - Live integration test for the CLI surface (the validator itself has live coverage from PR 2a's TestBindingValidationLive). A literal subprocess-style CLI smoke test could land as a follow-up if someone wants it, but the unit tests cover wiring + exit codes + flag threading comprehensively. * fix+test+docs: lowercase JSON codes, changelog, location + warning-only paths (#105 PR 2b) Four review findings folded in. (1) Failure-code reference now shows lowercase JSON values The CLI emits f.code.value (e.g. 'missing_table'), not the enum attribute name ('MISSING_TABLE'). Docs previously listed the uppercase form, so users copying the table into jq filters would silently match nothing. The reference table now has both columns (Python FailureCode + JSON value), with a sample jq filter using the lowercase form. (2) CHANGELOG entry under [Unreleased] This PR adds a public CLI command (binding-validate), two public ontology-build flags (--validate-binding[-strict]), --location on ontology-build, and the validate_binding_against_bigquery Python API. All four are now called out under [Unreleased] / Added. (3) Test the warning-only path through ontology-build The new test test_validate_binding_warnings_only_proceeds_to_build patches the validator to return a BindingValidationReport with only warnings (no failures), runs ontology-build --validate-binding, and asserts: - exit code 0 - build_ontology_graph IS called (warnings don't short-circuit) - WARN: key_column_nullable appears in the CLI output This covers _run_binding_preflight()'s default-mode advisory branch, complementing the existing failure-short-circuit and clean-success tests. (4) --location now has tests and a doc note Two new tests: - TestBindingValidate::test_location_threaded_to_bigquery_client confirms binding-validate --location=EU constructs a BQ client with location='EU'. - TestOntologyBuildValidateBindingFlag:: test_location_threaded_through_orchestrator confirms ontology-build --location=EU forwards to build_ontology_graph so the orchestrator's BQ client targets the EU multi-region. docs/ontology/ontology-build.md: new 'BigQuery location' section covering --location plus a 'Pre-flight binding validation' section referencing binding-validation.md for the full --validate-binding flag reference. 256/256 tests pass (test_binding_validation.py + test_cli.py + test_ontology_orchestrator.py + test_ontology_materializer.py + test_resolved_spec.py). Autoformat clean. Optional nit (deferred, not blocking): _run_binding_preflight loads ontology + binding once for validation, _load_spec_from_args reloads them for resolve(). Worth a small helper that returns the loaded objects once, but acceptable as-is for PR 2b scope. * docs: avoid naming INFORMATION_SCHEMA as preflight mechanism (#105 PR 2b) Two wording fixes; no behavior change. The validator uses bq_client.get_table(...) metadata calls (binding_validation.py:309), not direct INFORMATION_SCHEMA queries. Two places said the latter: (1) docs/ontology/ontology-build.md: 'pre-flight validator queries INFORMATION_SCHEMA in the same region' -> 'uses the BigQuery client with the requested location to fetch each bound table's metadata'. (2) tests/test_cli.py docstring on test_location_threaded_to_bigquery_client: same correction. The test asserts the location threads through; the wording for *why* location matters now matches the actual mechanism. 256/256 tests still pass. Autoformat clean.
1 parent 8f8a36f commit 4c3a293

6 files changed

Lines changed: 1008 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`bq-agent-sdk binding-validate` CLI** — pre-flight validator that
13+
checks whether a binding YAML's referenced BigQuery tables
14+
physically exist with the columns and types the binding requires,
15+
before extraction wastes ``AI.GENERATE`` tokens. Emits a structured
16+
JSON report (failures + warnings) and exits 0 / 1 / 2. Supports
17+
`--strict` to escalate `KEY_COLUMN_NULLABLE` warnings to hard
18+
failures. See [issue #105](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/issues/105)
19+
and `docs/ontology/binding-validation.md`.
20+
- **`bq-agent-sdk ontology-build --validate-binding` and
21+
`--validate-binding-strict`** opt-in flags. Run the binding
22+
pre-flight before phase 2 (extraction). On any failure, the build
23+
short-circuits before any `AI.GENERATE` call fires; default-mode
24+
warnings print to stderr but don't block. The two flags are
25+
mutually exclusive; both incompatible with the deprecated
26+
`--spec-path` form because the validator needs the unresolved
27+
`Ontology` + `Binding` pair.
28+
- **`bq-agent-sdk ontology-build --location`** — BigQuery location
29+
(e.g. `US`, `EU`) threaded through to `build_ontology_graph()`.
30+
The Python API has supported `location` since 0.2.3; this adds
31+
the matching CLI flag.
32+
- **`validate_binding_against_bigquery(...)` Python API** in
33+
`bigquery_agent_analytics.binding_validation`. Same surface the
34+
CLI calls: takes `Ontology` + `Binding` + `bq_client`, returns a
35+
`BindingValidationReport` with `failures` + `warnings` lists and
36+
an `ok` property. Issue [#105](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/issues/105).
37+
1038
## [0.2.3] - 2026-04-27
1139

1240
### Fixed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ architecture, rationale, and implementation plans behind key SDK features.
3737
| [ontology/cli.md](ontology/cli.md) | CLI design for the `gm` tool (validate, compile, import-owl) |
3838
| [ontology/owl-import.md](ontology/owl-import.md) | OWL import — converting OWL ontologies to YAML format |
3939
| [ontology/ontology-build.md](ontology/ontology-build.md) | `bq-agent-sdk ontology-build` orchestrator + `--skip-property-graph` reference |
40+
| [ontology/binding-validation.md](ontology/binding-validation.md) | `bq-agent-sdk binding-validate` pre-flight + `ontology-build --validate-binding[-strict]` reference |
4041

4142
## Deployment Surfaces
4243

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Binding Validation — Pre-flight Reference
2+
3+
`bq-agent-sdk binding-validate` checks whether the BigQuery tables a binding YAML points at physically exist with the columns and types the binding requires, **before** the SDK starts extraction. Catches the most common authoring error (binding YAML drifted out of sync with physical tables) before extraction wastes `AI.GENERATE` tokens.
4+
5+
## When to run it
6+
7+
- **Before any `ontology-build`** that points at user-pre-defined tables (Terraform / dbt / hand-authored DDL). Use either:
8+
- `bq-agent-sdk binding-validate ...` standalone, or
9+
- `bq-agent-sdk ontology-build --validate-binding ...` to gate the build.
10+
- **In CI** as a pre-flight check that runs on every binding YAML change. Strict mode (`--strict`) is appropriate here — it forces every primary-key column to be REQUIRED.
11+
- **Locally during binding authoring** to catch typos and column-name drift before the first build.
12+
13+
## Standalone CLI
14+
15+
```bash
16+
bq-agent-sdk binding-validate \
17+
--project-id my-project \
18+
--ontology my.ontology.yaml \
19+
--binding my-bq-prod.binding.yaml \
20+
--location US
21+
```
22+
23+
Output is a JSON report on stdout, plus advisory warnings printed to stderr. Exit codes:
24+
25+
| Exit | Meaning |
26+
|---|---|
27+
| `0` | `report.ok` is True. No failures. Warnings (if any) are advisory. |
28+
| `1` | `report.ok` is False. At least one failure. |
29+
| `2` | Unexpected error: missing flags, ontology/binding load failure, etc. |
30+
31+
### Strict mode
32+
33+
```bash
34+
bq-agent-sdk binding-validate \
35+
--project-id my-project \
36+
--ontology my.ontology.yaml \
37+
--binding my-bq-prod.binding.yaml \
38+
--strict
39+
```
40+
41+
Strict mode escalates `KEY_COLUMN_NULLABLE` warnings into hard failures. Use this in CI when you want every primary-key and endpoint-key column to be REQUIRED. Default mode does **not** require `NOT NULL` on key columns because the SDK's own `OntologyMaterializer.create_tables()` emits NULLABLE keys (`ontology_materializer.py:206`) — a default-mode hard failure here would reject SDK-created tables.
42+
43+
## `ontology-build` integration
44+
45+
Two opt-in flags gate the build on a passing pre-flight:
46+
47+
```bash
48+
bq-agent-sdk ontology-build \
49+
--project-id my-project \
50+
--dataset-id my-dataset \
51+
--ontology my.ontology.yaml \
52+
--binding my-bq-prod.binding.yaml \
53+
--session-ids sess-1,sess-2 \
54+
--validate-binding # default mode: warnings advisory; failures short-circuit
55+
```
56+
57+
Or in strict mode:
58+
59+
```bash
60+
bq-agent-sdk ontology-build \
61+
... \
62+
--validate-binding-strict # NULLABLE keys escalate to failures
63+
```
64+
65+
When the validator reports any failure, **the build short-circuits before any `AI.GENERATE` call fires** — no extraction tokens are spent. Default-mode warnings print to stderr but do not block.
66+
67+
The two flags are mutually exclusive. Both are incompatible with the deprecated `--spec-path` form because the validator needs the unresolved `Ontology` + `Binding` pair, not a combined `GraphSpec`.
68+
69+
## Failure-code reference
70+
71+
The validator returns a `BindingValidationReport` with two collections: `failures` (always blocking) and `warnings` (advisory in default mode, escalated under `--strict`). Each entry carries `code`, `binding_element`, `binding_path`, `bq_ref`, `expected`, `observed`, and `detail`.
72+
73+
The CLI emits failure codes as **lowercase strings** in the JSON report (e.g., `"missing_table"`, not `"MISSING_TABLE"`). Use the lowercase form when filtering with `jq` or other JSON tools. The Python `FailureCode` enum exposes both forms — `FailureCode.MISSING_TABLE.value == "missing_table"`.
74+
75+
### Default-mode failure codes (always blocking)
76+
77+
| Python `FailureCode` | JSON `code` value | What it means | Typical fix |
78+
|---|---|---|---|
79+
| `MISSING_TABLE` | `"missing_table"` | The bound `source` table doesn't exist in BigQuery. | Create the table, or fix the binding's `source`. |
80+
| `MISSING_COLUMN` | `"missing_column"` | A bound column (property, key, or SDK metadata column like `session_id` / `extracted_at`) doesn't exist on the table. | Add the column, or fix the binding's `column` mapping. |
81+
| `TYPE_MISMATCH` | `"type_mismatch"` | A bound column exists but its BigQuery type doesn't match the ontology-derived expected type. | Change the BQ column's type, or change the ontology property's type. |
82+
| `ENDPOINT_TYPE_MISMATCH` | `"endpoint_type_mismatch"` | An edge's `from_columns` or `to_columns` entry disagrees with the referenced node's primary-key column type. Two flavors: spec-level (edge vs ontology) and physical (edge vs node's actual storage). | Align the edge endpoint's type with the node's primary-key type. |
83+
| `UNEXPECTED_REPEATED_MODE` | `"unexpected_repeated_mode"` | A scalar property/key column is in REPEATED (ARRAY) mode in BigQuery. | Restructure the table — the SDK can't bind scalar properties to ARRAY columns. |
84+
| `MISSING_DATASET` | `"missing_dataset"` | The bound table's dataset doesn't exist. | Create the dataset, or fix the binding's `target.dataset` / fully-qualified `source`. |
85+
| `INSUFFICIENT_PERMISSIONS` | `"insufficient_permissions"` | The calling identity can't read the table. | Grant `bigquery.tables.get` on the dataset, or run as an identity that already has it. |
86+
87+
### Strict-only code (warning by default, failure under `--strict`)
88+
89+
| Python `FailureCode` | JSON `code` value | What it means | Why it's strict-only |
90+
|---|---|---|---|
91+
| `KEY_COLUMN_NULLABLE` | `"key_column_nullable"` | A primary-key or endpoint-key column is in NULLABLE mode. | The SDK's own `CREATE TABLE IF NOT EXISTS` DDL emits NULLABLE key columns. A default-mode hard failure here would reject SDK-created tables. Use `--strict` in CI to enforce REQUIRED keys when you control the DDL. |
92+
93+
Example `jq` filter for failed builds:
94+
95+
```bash
96+
bq-agent-sdk binding-validate ... --format=json \
97+
| jq '.failures[] | select(.code == "missing_column") | .bq_ref'
98+
```
99+
100+
## CI usage pattern
101+
102+
```yaml
103+
# .github/workflows/binding-validation.yml
104+
- name: Pre-flight binding validation
105+
run: |
106+
bq-agent-sdk binding-validate \
107+
--project-id ${{ secrets.GCP_PROJECT }} \
108+
--ontology my.ontology.yaml \
109+
--binding my-bq-prod.binding.yaml \
110+
--location US \
111+
--strict
112+
```
113+
114+
A failed validation exits 1 and the CI step fails. Combine with the matching `ontology-build --validate-binding-strict` in your deploy step so the same gate runs at build time.
115+
116+
## Python API
117+
118+
The CLI is a thin wrapper around `validate_binding_against_bigquery`. For programmatic use:
119+
120+
```python
121+
from google.cloud import bigquery
122+
from bigquery_ontology import load_ontology, load_binding
123+
from bigquery_agent_analytics.binding_validation import (
124+
validate_binding_against_bigquery,
125+
FailureCode,
126+
)
127+
128+
ontology = load_ontology("my.ontology.yaml")
129+
binding = load_binding("my-bq-prod.binding.yaml", ontology=ontology)
130+
client = bigquery.Client(project="my-project", location="US")
131+
132+
report = validate_binding_against_bigquery(
133+
ontology=ontology,
134+
binding=binding,
135+
bq_client=client,
136+
strict=False,
137+
)
138+
139+
if not report.ok:
140+
for f in report.failures:
141+
print(f"{f.code.value} at {f.binding_path} ({f.bq_ref}): {f.detail}")
142+
raise SystemExit(1)
143+
144+
for w in report.warnings:
145+
print(f"WARN: {w.code.value} at {w.binding_path}")
146+
```
147+
148+
`report.ok` returns `True` iff `report.failures` is empty — warnings do not flip it. Under `strict=True`, `KEY_COLUMN_NULLABLE` warnings become failures with the same code and `report.warnings` is empty (escalated, not duplicated).
149+
150+
## Related
151+
152+
- `docs/ontology/ontology-build.md` — the orchestrator the validator gates.
153+
- `docs/ontology/binding.md` — the binding model the validator validates.
154+
- Issue [#76](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/issues/76) — the planned post-extraction `validate_extracted_graph` validator (different phase, different inputs).

docs/ontology/ontology-build.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,33 @@ The CLI's `property_graph_status` field has three values:
5353

5454
Without `--skip-property-graph`, the existing exit-1 behavior on graph-create failure is preserved exactly.
5555

56-
## When to use this
56+
## BigQuery location
57+
58+
`--location` (e.g. `--location=US`, `--location=EU`) is forwarded to the orchestrator's BigQuery client. Required when the bound tables live outside the default location, especially in combination with `--validate-binding[-strict]` (the pre-flight validator uses the BigQuery client with the requested location to fetch each bound table's metadata).
59+
60+
```
61+
bq-agent-sdk ontology-build \
62+
--project-id my-project \
63+
--dataset-id my-dataset \
64+
--ontology my.ontology.yaml \
65+
--binding my-bq-prod.binding.yaml \
66+
--session-ids sess-1 \
67+
--location EU \
68+
--validate-binding
69+
```
70+
71+
## Pre-flight binding validation
72+
73+
`--validate-binding` and `--validate-binding-strict` run the binding validator before extraction, short-circuiting the build if the binding YAML drifted from the physical tables. See [binding-validation.md](binding-validation.md) for the full reference.
74+
75+
```
76+
bq-agent-sdk ontology-build ... --validate-binding # default mode
77+
bq-agent-sdk ontology-build ... --validate-binding-strict # strict mode
78+
```
79+
80+
The two flags are mutually exclusive and incompatible with the deprecated `--spec-path` form.
81+
82+
## When to use `--skip-property-graph`
5783

5884
- **You already manage `CREATE PROPERTY GRAPH` in Terraform / dbt / a SQL file.** The SDK's `CREATE OR REPLACE PROPERTY GRAPH` would clobber your DDL on every run.
5985
- **Your property graph definition uses DDL details the SDK compiler doesn't emit.** You hand-authored the graph DDL to express custom labels or other DDL details the SDK's compiler doesn't generate.

0 commit comments

Comments
 (0)