Skip to content

Commit c0d6eac

Browse files
authored
feat: --events-bq-query-file event source for bqaa-revalidate-extractors (#75 follow-up) (#151)
* feat: --events-bq-query-file event source for bqaa-revalidate-extractors (#75 follow-up) The CLI now accepts a BigQuery event source in addition to --events-jsonl; the two are mutually exclusive and exactly one must be supplied. Promised follow-up to the local-only landing of PR #150. Contract: the SQL must produce exactly one column named ``event_json`` (STRING) per row, containing a JSON-encoded event dict — same shape --events-jsonl consumes line-by-line. The CLI does NOT auto-shape bigquery.Row objects; the query writer controls projection (typically via ``TO_JSON_STRING(STRUCT(...))``). Explicit non-goal: row-shape auto-inference would couple the CLI to every caller's table schema; the single-column contract keeps the CLI predictable. New flags: * --events-bq-query-file PATH (mutex with --events-jsonl; argparse mutex group with required=True so "both" / "neither" both return exit 2 via the existing _CliError boundary). * --bq-project PROJECT_ID — OPTIONAL. When absent the BigQuery client falls back to Application Default Credentials / environment for project inference. If both the flag and the inferred project are empty, the CLI exits 2 with "Set --bq-project explicitly" rather than confusing the operator with a downstream BigQuery API error. * --bq-location LOC — defaults to "US"; ignored under --events-jsonl. Error handling — every failure mode surfaces as a clean exit 2 with the file path and (for row-level errors) the 0-based row index named, so an operator can find the offender with ``LIMIT N OFFSET row_index``: * BigQuery raises (auth, syntax, table-not-found, permission) → "BigQuery query failed: <Type>: <message>" * Row missing the event_json column → "row N: missing required column 'event_json'" * event_json value isn't a STRING (struct projected without TO_JSON_STRING) → "row N: 'event_json' must be STRING" * event_json STRING isn't valid JSON → "row N: invalid JSON in 'event_json'" * event_json decodes to non-object (array, scalar) → "row N: 'event_json' decodes to <type>, expected a JSON object" * Empty / whitespace-only .sql file → "is empty" * Invalid UTF-8 in .sql file → "not valid UTF-8" Implementation notes: * Client construction is centralized behind ``_make_bq_client(project, location)`` so unit tests inject fakes via ``monkeypatch.setattr(cli_revalidate, "_make_bq_client", ...)`` rather than wiring through every call site. The real factory tries ``bigquery.Client(project=...)`` first; falls back to ``bigquery.Client()`` if project is None; raises _CliError if client.project ends up falsy. * ``_load_events_from_bq`` reads the .sql file (with the same UTF-8 / OSError wrapping pattern as _load_jsonl / _load_thresholds), constructs the client via the factory, runs ``client.query(sql).result()`` inside a single try/except that catches Exception (not BaseException, so KeyboardInterrupt / SystemExit still propagate), then iterates rows with row-indexed validation. * ``_load_config`` dispatches on whichever event-source flag is set; argparse's mutex group enforces exactly one. Tests: 20 -> 31 in tests/test_extractor_compilation_cli_revalidate.py. * TestCliUsageErrors gains two cases for the mutex (both / neither event sources). * New TestCliEventsBQ class (9 cases): happy path, ADC project inference, no-project-anywhere, query exceptions, every row-shape failure mode (missing column, non-string, invalid JSON, non-dict decode), empty .sql file. Live BQ test in tests/test_extractor_compilation_cli_revalidate_bq_live.py, gated behind BQAA_RUN_LIVE_TESTS=1 + BQAA_RUN_LIVE_BQ_REVALIDATE_TESTS=1 + PROJECT_ID + DATASET_ID. Creates a temp table, inserts two event_json rows shaped like BKA decisions, runs the CLI, asserts the report is written with both events as compiled_unchanged + parity_matches, deletes the table on the way out. Docs: CLI doc page describes the contract, error mapping, project resolution, and adds a BigQuery usage example. docs/README.md index entry + CHANGELOG entry updated. * fix(revalidate-cli): wrap BQ client construction + enforce exactly-one-column + fix stale example SQL Addresses PR #151 round-1 reviewer findings. P1 - BigQuery client construction now wrapped in its own try/except. Previously ``_make_bq_client(...)`` ran OUTSIDE the try that wraps query failures, so auth / ADC / invalid- credentials / network failures escaped as raw tracebacks despite the docs promising clean CLI errors. The construction wrap distinguishes the failure mode from query-time failures via a separate prefix: * "BigQuery client construction failed: ..." for auth/ADC errors before ``client.query`` runs. * "BigQuery query failed: ..." for syntax/permission/table- not-found at query time. Our own ``_CliError`` from project-inference failure inside the factory passes through unchanged (no re-wrapping). P2 - "Exactly one column named event_json" contract is now enforced. Previously a query returning ``event_json, extra_col`` was silently accepted because the row loop only reads ``row["event_json"]``. Now validated against the first row's keys before iteration; mismatch fails with exit 2 listing the offending column set so an operator can fix the SQL. Check is via ``sorted(rows[0].keys())`` (works for both ``bigquery.Row`` and dict fakes). Empty result sets are a no-op — nothing to validate, harness produces a zero-event report. P3 - Stale example SQL fixed. The doc previously used ``WHERE event_timestamp BETWEEN @start_time AND @end_time``, but the CLI doesn't accept query parameters; copying the example as-is would fail at BigQuery. Replaced with literal ``TIMESTAMP('2026-05-01')`` / ``TIMESTAMP('2026-05-12')`` and explicit doc text: "The SQL file must be fully self-contained — the CLI does not accept query parameters, so substitute concrete literals before invoking." Tests: 31 -> 33 in tests/test_extractor_compilation_cli_revalidate.py. * test_bq_client_construction_failure_returns_two: monkey-patches _make_bq_client to raise RuntimeError; asserts exit 2 with "BigQuery client construction failed" prefix + the underlying message. * test_bq_extra_column_rejected: row with extra_col key fails with "exactly one column ... got ['event_json', 'extra_col']"; report is NOT written. All 32 CLI tests + 1 entry-point skip pass; pyink + isort clean on the touched files. * fix(revalidate-cli): validate column contract via job.schema (catches zero-row wrong-schema case) PR #151 round-2 reviewer finding. P2 - The previous "exactly one column" check only ran when rows is non-empty. A query like ``SELECT event_json, extra_col FROM t WHERE FALSE`` returns zero rows but still has the wrong schema; the CLI silently skipped validation and wrote a successful zero-event report — a misleading "all clear" signal when the SQL itself is broken. Fix: derive column names via a new ``_query_result_column_names(job, rows)`` helper that prefers ``job.schema`` (real ``bigquery.QueryJob`` populates this regardless of row count) and falls back to ``sorted(rows[0].keys())`` only when the job lacks a usable schema (test fakes that don't simulate it). Returns ``None`` on the degenerate "fake + zero rows + no schema" case, which the caller treats as "skip the contract check" — that preserves existing fake-based tests without weakening the real-BigQuery path (production always has schema). The fake test client now supports an optional ``schema=`` parameter (list of objects exposing ``.name``) so tests can simulate both correct and incorrect schemas independently of the row payload. Tests: 33 -> 35. * test_bq_extra_column_rejected_on_empty_result_set: zero rows + ``schema=[event_json, extra_col]`` → exit 2 with the column-set listed; report NOT written. The bug reproducer. * test_bq_correct_schema_empty_result_set_succeeds: zero rows + ``schema=[event_json]`` → exit 0 with a zero-event report. Locks the design choice that an empty-but-well-shaped result is a valid revalidation outcome, not an error. Docs: failure-mode table notes the schema-based check; contract section + test list updated.
1 parent c172bcb commit c0d6eac

6 files changed

Lines changed: 1365 additions & 19 deletions

File tree

CHANGELOG.md

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

1010
### Added
1111

12+
- **``--events-bq-query-file`` for ``bqaa-revalidate-extractors``**
13+
(issue [#75](https://github.com/GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK/issues/75)
14+
CLI follow-up). The CLI now accepts a BigQuery event
15+
source in addition to ``--events-jsonl``; the two are
16+
mutually exclusive and exactly one must be supplied.
17+
Contract: the SQL must produce exactly one column named
18+
``event_json`` (STRING) per row, containing a JSON-encoded
19+
event dict — same shape ``--events-jsonl`` consumes
20+
line-by-line. The CLI does NOT auto-shape
21+
``bigquery.Row`` objects; the query writer controls
22+
projection via ``TO_JSON_STRING(STRUCT(...))``. Row-level
23+
errors (missing column, non-string value, malformed JSON,
24+
non-dict decode) surface as exit 2 with the 0-based row
25+
index named so an operator can find the offender with
26+
``LIMIT N OFFSET row_index``. BigQuery-side exceptions
27+
(auth, syntax, table-not-found, permission) are caught and
28+
surfaced with type + message — no traceback escapes.
29+
``--bq-project`` is optional: the BigQuery client falls
30+
back to Application Default Credentials / environment for
31+
project inference; if both are absent the CLI exits 2 with
32+
``Set --bq-project explicitly`` rather than confusing the
33+
operator with a downstream API error. ``--bq-location``
34+
defaults to ``US``. Client construction is centralized
35+
behind ``_make_bq_client(project, location)`` so unit
36+
tests inject in-memory fakes via ``monkeypatch.setattr``
37+
rather than wiring through every call site. CI tests
38+
(11 new cases) cover the happy path, ADC inference,
39+
no-project-anywhere, query exceptions, every row-shape
40+
failure mode, the mutex on event sources (both / neither),
41+
and the empty-SQL-file edge. Live BQ test
42+
(``tests/test_extractor_compilation_cli_revalidate_bq_live.py``)
43+
is gated behind ``BQAA_RUN_LIVE_BQ_REVALIDATE_TESTS=1``;
44+
it creates a temp table, inserts two ``event_json`` rows,
45+
runs the CLI, asserts the report is written with both
46+
events as compiled_unchanged + parity_matches, deletes
47+
the table on the way out.
1248
- **``bqaa-revalidate-extractors`` CLI** in
1349
`bigquery_agent_analytics.extractor_compilation.cli_revalidate`
1450
and

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ architecture, rationale, and implementation plans behind key SDK features.
5252
| [extractor_compilation_runtime_registry.md](extractor_compilation_runtime_registry.md) | Runtime extractor-registry adapter (issue #75 PR C2.c.1): `build_runtime_extractor_registry(...)` glues C2.a's `discover_bundles` + C2.b's `run_with_fallback` into one call, returning a `WrappedRegistry` with an `extractors` dict ready for `run_structured_extractors` plus `bundles_without_fallback` (compiled-only, skipped) and `fallbacks_without_bundle` (no usable compiled registry entry — "never built" *and* "rejected by discovery"; cross-reference `discovery.failures` for the reason). Compiled-only event_types are skipped and recorded (fail-closed); fallback-only event_types pass through unchanged. Non-callable fallbacks are rejected at build time with `TypeError` naming the event_type. The `on_outcome(event_type, outcome)` callback fires on every wrapped invocation (denominator metric); callback exceptions propagate. Out of scope: actual orchestrator call-site swap (C2.c.2), BQ mirror (C2.c.3), revalidation (C2.d). |
5353
| [extractor_compilation_orchestrator_swap.md](extractor_compilation_orchestrator_swap.md) | Orchestrator call-site swap (issue #75 PR C2.c.2): `OntologyGraphManager.from_bundles_root(...)` classmethod that builds the runtime registry internally and constructs a manager whose `extractors` dict is the wrapped registry, so existing `run_structured_extractors` calls inside `extract_graph` pick up compiled-with-fallback behavior with no other code changes. Adds `manager.runtime_registry: WrappedRegistry | None` audit handle (non-None when bundle-wired). Mirrors `from_ontology_binding` arg shape; existing `__init__` and `from_ontology_binding` paths are unchanged. Compiled-only event_types without a matching fallback are NOT registered (fail-closed). Out of scope: BQ mirror (C2.c.3), revalidation (C2.d). |
5454
| [extractor_compilation_bq_bundle_mirror.md](extractor_compilation_bq_bundle_mirror.md) | BigQuery-table bundle mirror (issue #75 PR C2.c.3): `publish_bundles_to_bq(bundle_root, store, ...)` + `sync_bundles_from_bq(store, dest_dir, ...)`. Mirror is a publish/sync utility, NOT a runtime loader — the runtime path stays `sync_bundles_from_bq → discover_bundles → from_bundles_root`. Both functions call `load_bundle` as a gate: publish refuses bundles that wouldn't load at the runtime; sync writes to a side-by-side **staging directory** and `load_bundle`-validates the staged copy before performing a **staged replace** of the target (the rmtree+move pair is not strictly atomic — a crash between the two leaves the bundle absent on disk and is recoverable by re-sync — but the load-bundle-failure direction *is* atomic, so a bad mirror row never destroys a previously-good local bundle). Strict bundle-shape (exactly `manifest.json` + the manifest's `module_filename`) plus shape-check on the manifest's `module_filename` (bare filename only — no separators, no `..`, no NUL; otherwise `manifest_row_unreadable`). Path-safety rejects traversal / absolute / backslash / NUL. `duplicate_fingerprint` rejects publish-side cases where two subdirs claim the same fingerprint (neither published). `duplicate_row` rejects two rows sharing the same `(fingerprint, bundle_path)` at sync. `malformed_row` shape check. Idempotent republish via DELETE+INSERT in `BigQueryBundleStore.publish_rows` (NOT a single atomic transaction; a transient INSERT failure is recoverable by re-running publish). `publish_rows` raises `ValueError` on duplicate input pairs as defense in depth. `BundleStore` Protocol for testability; `BigQueryBundleStore` is the concrete impl. Stable `MirrorFailure` codes; per-bundle problems accumulate, store exceptions propagate. Out of scope: GCS signed URLs, caching, garbage collection, multi-region. |
55-
| [extractor_compilation_revalidate_cli.md](extractor_compilation_revalidate_cli.md) | `bqaa-revalidate-extractors` CLI (Phase C operationalization): one-shot binary that wraps `revalidate_compiled_extractors` for local inputs. Flags: `--bundles-root`, `--events-jsonl`, `--reference-extractors-module`, `--thresholds-json` (optional), `--report-out`. Reference module exposes `EXTRACTORS` dict + `RESOLVED_GRAPH` (+ optional `SPEC`) so the CLI doesn't need ontology/binding flags. Fingerprint auto-detected from the first bundle's manifest; mixed fingerprints fail-closed. Exit codes: `0` pass / `1` threshold violation / `2` usage-or-input error. Report JSON includes both the raw `RevalidationReport` and the `ThresholdCheckResult`. Out of scope: `--events-bq-query` (follow-up PR), scheduled execution, BQ persistence. |
55+
| [extractor_compilation_revalidate_cli.md](extractor_compilation_revalidate_cli.md) | `bqaa-revalidate-extractors` CLI (Phase C operationalization): one-shot binary that wraps `revalidate_compiled_extractors`. Event source flags are mutually exclusive: `--events-jsonl` for local JSONL files OR `--events-bq-query-file` for BigQuery (SQL must return one column named `event_json` STRING per row). `--bq-project` is optional with ADC fallback. Other flags: `--bundles-root`, `--reference-extractors-module`, `--thresholds-json` (optional), `--report-out`. Reference module exposes `EXTRACTORS` dict + `RESOLVED_GRAPH` (+ optional `SPEC`) so the CLI doesn't need ontology/binding flags. Fingerprint auto-detected from the first bundle's manifest; mixed fingerprints fail-closed. Exit codes: `0` pass / `1` threshold violation / `2` usage-or-input error. Report JSON includes both the raw `RevalidationReport` and the `ThresholdCheckResult`. Out of scope: pagination strategy for ultra-large corpora, scheduled execution, BQ persistence, auto-row-shape inference (explicit non-goal — the `event_json` contract keeps the CLI predictable). |
5656
| [extractor_compilation_revalidation.md](extractor_compilation_revalidation.md) | Revalidation harness (issue #75 PR C2.d): `revalidate_compiled_extractors(events, compiled_extractors, reference_extractors, resolved_graph, ...)` drives `run_with_fallback` (with a no-op fallback) over a batch of events AND calls the reference extractor directly, aggregating outcomes into a `RevalidationReport` with **two orthogonal dimensions**: runtime decision (`compiled_unchanged` / `compiled_filtered` / `fallback_for_event`, plus `compiled_path_faults` split out so bundle bugs are distinguishable from ontology drift) and agreement against reference (`parity_match` / `parity_divergence` / `parity_not_checked`). Parity uses three comparators: `_compare_nodes` and `_compare_span_handling` from `measurement.py` plus `_compare_edges` in `revalidation.py` (same edge_id set with matching relationship_name / endpoints / property-set per shared edge; duplicate edge_ids on either side reported as a divergence rather than silently collapsed by dict keying). The parity dimension catches **schema-valid but semantically wrong** outputs the schema-only check would miss. **Every failure mode on the reference side becomes a parity divergence, never a batch abort**: exceptions, non-`StructuredExtractionResult` returns (including `None`), and comparator crashes all funnel into the divergence channel with a descriptive string. `check_thresholds(report, RevalidationThresholds(...))` evaluates policy gates; threshold rates are validated to `[0, 1]` at construction so a typo like `=5` (intended as 5%) fails loud. JSON-serializable for persistence; deterministic. Out of scope: scheduled orchestration, BQ persistence, CLI, sampling strategy. |
5757

5858
## Deployment Surfaces

0 commit comments

Comments
 (0)