recon_gen.common.l2.config_table
<prefix>_config_kv — flattened cfg + L2 JSON tree (BC.12).
Originally a 3-column <prefix>_config(as_of, cfg_yaml, l2_yaml)
single-row table (Phase AW). Replaced in BC.12 to dodge Oracle’s
ORA-32368: cannot create JSON materialized view without relational
table — Oracle 19c+ refuses to build a matview whose source is
JSON_TABLE of a CLOB column. The kv flattening lets matviews JOIN
typed projection views (<prefix>_v_config_rails etc.) whose own
bodies are plain self-joins on relational columns; the matview engine
sees a relational source, not a JSON_TABLE-of-CLOB.
Table shape:
<prefix>_config_kv(
node_id BIGINT PRIMARY KEY,
parent_id BIGINT NULL, -- self-ref, NULL for roots
key VARCHAR(255) NULL, -- JSON key / array index
value TEXT NULL -- CLOB on Oracle (holds l2_yaml_raw)
)
Walk semantics (Python-side at populate time):
Top-level scalars (
as_of,l2_yaml_rawopaque provenance) live atparent_id IS NULLas flat rows.Nested structures (rails, limit_schedules, etc.) walk recursively; each container gets a row with
value IS NULL(its descendants carry the data); each scalar leaf gets a row with the scalar value.Array elements:
keyis the stringified index ('0','1'…).Object fields:
keyis the field name.
Operational lifecycle (BC.12 lock):
Once —
schema apply --executecreates<prefix>_config_kv.Every deploy (L2 changes) —
schema apply --executere-creates matviews + repopulates<prefix>_config_kv(TRUNCATE + INSERT-N from the parsed cfg+L2 JSON).Daily (post-ETL) —
data refresh --executeREFRESHes matviews.
Deploy-artifact contract (BL.0.B, 2026-05-27):
<prefix>_config_kv is part of the deployment artifact bundle —
NOT a runtime database. Reads off the table are reads of whatever
the last ``schema apply –execute`` or ``data apply –execute``
wrote. Edits land via re-deploy; never via SQL UPDATE /
INSERT from Studio mutations, ETL pipelines, or operator
patches. The DELETE-then-INSERT at the top of
emit_config_populate_sql / replace_config makes “what’s in
the table” == “what’s in the on-disk yaml at deploy time” by
construction. This is the discipline that obviates the
(prefix, l2_yaml_sha) keying spike the BL.0 audit considered:
since edits always go through the wipe, the temporal-drift case
(“yaml v1 deployed Monday, yaml v2 on disk by Friday, kv still
holds v1”) is operationally impossible — Friday’s deploy wipes
Monday’s rows before INSERTing v2.
Indexed on (parent_id, key) — the typed views’ filter shape. No
index on value (CLOB-incompatible on Oracle without a function-
based index; the typed views walk by parent_id+key, never by value).
Migration from the pre-BC.12 <prefix>_config table: existing
customer deploys had <prefix>_config empty (BC.6 surfaced
production never populated it), so the “migration” is just
schema apply --execute against the v11.19.0 schema — no data
loss because there was no data.
Functions
|
The canonical |
|
Emit the DELETE + INSERT-N-rows SQL that populates |
|
|
|
|
|
Read the current |
|
Read the |
|
Scalar subquery returning the raw |
|
Scalar subquery returning the |
|
Walk parsed cfg + L2 JSON into kv rows. |
|
Deploy event — re-populate kv from parsed cfg+L2 + as_of. |
|
ETL refresh event — update the as_of scalar row. |
- recon_gen.common.l2.config_table.config_table_name(prefix)[source]
The canonical
<prefix>_config_kvtable name.BC.12 renamed from
<prefix>_configto<prefix>_config_kvso the suffix announces the shape change (flattened kv vs the old 3-column shape). Pre-BC.12 deploys had the old table empty (BC.6 finding); no migration needed beyond running the new schema.- Return type:
str- Parameters:
prefix (str)
- recon_gen.common.l2.config_table.emit_config_populate_sql(*, prefix, cfg_json, l2_json, as_of, dialect)[source]
Emit the DELETE + INSERT-N-rows SQL that populates
<prefix>_config_kvfrom parsed cfg+L2 JSON + as_of.Replaces the pre-BC.12 single-row
INSERT INTO <prefix>_config (as_of, cfg_yaml, l2_yaml) VALUES (...).Empties the table first (DELETE — TRUNCATE isn’t atomic with the subsequent INSERTs in every dialect’s transactional semantics, and DELETE is plenty fast for a single-deploy populate), then issues one INSERT per kv row. Each INSERT goes on its own line so the script splitter in
common/db.execute_scriptcan run them individually — dialect drivers don’t all support multi-row VALUES cleanly across vendor lines (and oracledb in particular).Long string values (e.g. the
l2_yaml_rawprovenance row at ~37 KB for sasquatch_pr) are split on Oracle into 4000-byte chunks concatenated viaTO_CLOB(chunk1) || TO_CLOB(chunk2) || ...— Oracle’s literal limit is ORA-01704: string literal too long at 4000 bytes per single-quoted literal, but the concatenation of multiple shorter literals into a CLOB has no such cap. PG + SQLite accept multi-megabyte literals directly; their path is the single- literal form.All input is walker-controlled (parsed JSON values, never operator input flowing through to SQL) — no SQL-injection surface.
- Return type:
str- Parameters:
prefix (str)
cfg_json (str)
l2_json (str)
as_of (datetime)
dialect (Dialect)
- recon_gen.common.l2.config_table.emit_config_table_ddl(prefix, dialect)[source]
CREATE TABLE <prefix>_config_kvDDL for the given dialect.Column shape (BC.12.3 spike-locked):
node_idBIGINT PK — Python-side monotonic counter at populate time (no DB sequence; keeps TRUNCATE + repopulate atomic).parent_idBIGINT NULL — self-ref tonode_id; NULL for root nodes (top-level keys). No FK declared: kv is internal-only and the walker writes parents before children, so the constraint adds no value over correctness-by-construction (and Oracle would force a deferred constraint for batch inserts).keyVARCHAR(255) — JSON field name or stringified array index.valueTEXT/CLOB — scalar leaf value (or NULL for container nodes). CLOB on Oracle so thel2_yaml_rawopaque-provenance row fits (sasquatch_pr’s full L2 JSON is ~37KB; VARCHAR2(4000) would force splitting). The typed projection views coerce CLOB → VARCHAR2 vialob_substrbefore MAX/aggregation (Oracle’s MAX rejects CLOB with ORA-22849).
Index on
(parent_id, key)— the typed views’ filter shape (WHERE parent_id = X AND key = 'Y'). No index onvalue(CLOB-incompatible on Oracle without function-based indexes).- Return type:
str- Parameters:
prefix (str)
dialect (Dialect)
- recon_gen.common.l2.config_table.emit_config_table_drop(prefix, dialect)[source]
DROP TABLE IF EXISTS <prefix>_config_kv;DDL for re-runs.Drops the index implicitly (PG + Oracle + SQLite all drop indexes when the table they belong to is dropped).
- Return type:
str- Parameters:
prefix (str)
dialect (Dialect)
- recon_gen.common.l2.config_table.get_as_of(conn, *, prefix)[source]
Read the current
as_ofvalue back as a Python datetime.Reads from the single
as_ofkv row atparent_id IS NULL. RaisesRuntimeErrorif the row doesn’t exist — the single-row invariant: callreplace_configbeforeget_as_of/set_as_of.- Return type:
datetime- Parameters:
conn (SyncConnection)
prefix (str)
- recon_gen.common.l2.config_table.kv_as_of_as_timestamp_sql(prefix, dialect)[source]
Read the
as_ofkv row and project as TIMESTAMP for date arithmetic (epoch_seconds_betweenand friends).Dialect-specific coercion:
PG:
CAST(... AS TIMESTAMP)— PG accepts the ISO-format text and yields a proper TIMESTAMP.DuckDB:
CAST(... AS TIMESTAMP)— same shape as PG. DuckDB is strict about implicit casts (VARCHAR - TIMESTAMPraises BinderException unless the VARCHAR is explicitly cast), so the cast is load-bearing here even though it looks redundant.Oracle:
TO_TIMESTAMP(DBMS_LOB.SUBSTR(value, 100, 1), 'YYYY-MM-DD HH24:MI:SS')— Oracle won’t CAST CLOB to TIMESTAMP (ORA-00932); DBMS_LOB.SUBSTR converts to VARCHAR2 first, then TO_TIMESTAMP parses the fixed-format string.SQLite: bare text — SQLite has no TIMESTAMP type, and
julianday(text)accepts ISO-format strings natively (so theepoch_seconds_betweenSQLite branch’s(julianday(later) - julianday(earlier)) * 86400works unchanged).
- Return type:
str- Parameters:
prefix (str)
dialect (Dialect)
- recon_gen.common.l2.config_table.kv_as_of_subquery(prefix)[source]
Scalar subquery returning the raw
as_ofvalue (text form).The walker stores
as_ofas a stringifiedYYYY-MM-DD HH:MM:SSleaf; this helper wraps the kv read so callers don’t need to know the storage shape. Pair withkv_as_of_as_timestamp_sqlwhen the consumer needs a typed TIMESTAMP (e.g. for date arithmetic).- Return type:
str- Parameters:
prefix (str)
- recon_gen.common.l2.config_table.kv_root_id_for(prefix, top_key)[source]
Scalar subquery returning the
node_idof the root container keyedtop_key(one of'l2_yaml'/'cfg_yaml'). Used by typed views to anchor their walks.The kv populate guarantees exactly one row per top-level key at
parent_id IS NULL; the subquery returns a single value.- Return type:
str- Parameters:
prefix (str)
top_key (str)
- recon_gen.common.l2.config_table.kv_rows_for(cfg_json, l2_json, *, as_of)[source]
Walk parsed cfg + L2 JSON into kv rows.
Row order:
as_ofscalar first, then the parsed L2 tree, then the parsed cfg tree, then the opaque-provenancel2_yaml_raw/cfg_yaml_rawrows. The walker assignsnode_idmonotonically; callers MUST insert in this order to preserve parent-before-child ordering even without an FK constraint.The L2 tree’s top-level fields (
rails,limit_schedules, etc.) land atparent_id= thel2_yamlcontainer node. The typed projection views walk DOWN from that container viaparent_idJOINs.cfg_jsonis included for downstream symmetry / future cfg-derived views, even though no current matview consumes it. Caller-friendly: pass'{}'to skip cfg-side rows entirely (the walk produces one empty-container row).- Return type:
list[tuple[int,int|None,str|None,str|None]]- Parameters:
cfg_json (str)
l2_json (str)
as_of (datetime)
- recon_gen.common.l2.config_table.replace_config(conn, *, prefix, cfg_json, l2_json, as_of)[source]
Deploy event — re-populate kv from parsed cfg+L2 + as_of.
DELETE + INSERT preserves an idempotent populate without relying on dialect-specific UPSERT. The caller is responsible for serializing cfg + L2 to JSON strings (typically via
dataclasses.asdict+json.dumps, or by reading the source YAML and re-serializing).Cursor flavor: the type annotation is
SyncConnectionfor SQLite-test convenience, but the function is duck-typed against the PEP 249 conn / cursor interface — psycopg2 + oracledb both work.- Return type:
None- Parameters:
conn (SyncConnection)
prefix (str)
cfg_json (str)
l2_json (str)
as_of (datetime)
- recon_gen.common.l2.config_table.set_as_of(conn, *, prefix, as_of=None)[source]
ETL refresh event — update the as_of scalar row.
as_of=None(production default) → updates to CURRENT_TIMESTAMP so matview age formulas pick up “right now” at refresh time. Literal datetime → pinned (tests + backfill scenarios).Assumes the kv table is already populated — call
replace_configonce at deploy/init time before anyset_as_ofcalls.- Return type:
None- Parameters:
conn (SyncConnection)
prefix (str)
as_of (datetime | None)