recon_gen.common.sql.dialect
Dialect-specific SQL helpers — Phase P.2 catalog + P.3 Oracle fill + P.3.e cleanup + X.3 SQLite.
Every helper takes an explicit Dialect (no defaults — see “P.3.e
cleanup” below) and returns a dialect-appropriate SQL string. The
helpers split into two output shapes:
Statement helpers — return a fully terminated, self-contained
statement. Postgres branch ends in ;; Oracle branch wraps in
a PL/SQL block ending in END;. Callers concatenate without
appending a separate ;.
drop_table_if_exists
drop_matview_if_exists
drop_index_if_exists
drop_view_if_exists
refresh_matview
analyze_table
Fragment helpers — return an expression-level SQL string with no
trailing ;. Substituted into larger SQL templates by the caller,
which decides where the statement boundary lives.
Type names (
serial_type,timestamp_type,text_type,varchar_type,decimal_type,boolean_type,decimal_type)Casts (
cast,typed_null,to_date,date_trunc_day)Date arithmetic (
epoch_seconds_between,interval_days,date_minus_days)Constraints (
json_check)Other clauses (
with_recursive,create_matview,matview_options)
Phase notes:
- P.2 shipped every helper with a Postgres branch only.
- P.3 filled in the Oracle branches (19c Standard Edition target).
- P.3.e dropped the dialect: Dialect = Dialect.POSTGRES defaults
so call sites must be explicit. Every in-tree caller already passed a dialect by then; defaults were a rollout convenience that’s no longer load-bearing.
_matview_optionsmoved into this module fromcommon.l2.schemain P.3.e — it’s a pure dialect helper and belongs alongsidecreate_matview/refresh_matview.X.3 added the SQLite branch (3.38+, ships with stdlib
sqlite3). SQLite has noCREATE MATERIALIZED VIEW: matviews are emitted asCREATE TABLE … AS SELECTand refreshed byDELETE+INSERT. JSON metadata uses SQLite’s JSON1 functions (json_valid,json_extract); theIS JSONconstraint becomes aCHECK (json_valid(col)). The dialect is the local-iteration loop’s storage — single-file or in-memory, no server required.
Functions
|
Refresh planner statistics on a table or matview. |
|
Signed 64-bit integer column type. |
|
Boolean column type. |
|
Cast |
|
Per-dialect natural case for an unquoted column identifier. |
|
Aggregate text values from a group into a delimited string. |
|
|
|
A SQL date literal that compares correctly on every dialect. |
|
Subtract |
|
Truncate a timestamp expression to its day boundary, preserving a timestamp-shaped result type so downstream JOINs against TIMESTAMP columns don't fall through implicit conversion. |
|
Render a timestamp expression as its |
|
Fixed-precision decimal. |
|
Idempotent DROP INDEX. |
|
Idempotent DROP MATERIALIZED VIEW. |
|
Idempotent DROP TABLE — emits CASCADE so dependent FKs / views drop transitively (where the dialect supports CASCADE). |
|
Idempotent DROP VIEW. |
|
Suffix that makes a constant SELECT valid on both dialects. |
|
Difference between two timestamps in whole + fractional seconds. |
|
Return the row-limiting clause that picks the first row of an ORDER BY — |
|
Row-wise greatest of two or more expressions. |
|
A SQL interval literal of |
|
|
|
|
|
Extract a scalar field from a per-row JSON element (e.g. one iteration of json_array_iterate). |
|
Bounded text type for columns holding JSON metadata documents. |
|
Extract a scalar from a JSON-shaped column via SQL/JSON path. |
|
Coerce a (potentially CLOB-typed) expression to a bounded VARCHAR by extracting its first |
|
The |
|
Per-dialect suffix between |
|
Per-dialect |
|
Day-interval expression for use inside a window-function |
|
|
|
Auto-incrementing 64-bit append-only key. |
|
Unbounded character data. |
|
TZ-naive timestamp, identical on both dialects. |
|
Truncate a timestamp expression to its date component. |
|
Typed NULL literal. |
|
Bounded variable-length character. |
|
Recursive-CTE preamble keyword. |
Classes
|
Target SQL dialect. |
- class recon_gen.common.sql.dialect.Dialect(*values)[source]
Bases:
str,EnumTarget SQL dialect.
Postgres covers Postgres 17+ (the version floor required by the SQL/JSON path syntax we already use). Oracle covers Oracle 19c Standard Edition (the long-term-support version Phase P targets). SQLite covers SQLite 3.38+ (the version floor for the JSON1 functions we depend on; ships with Python’s stdlib
sqlite3). DuckDB is in-process (in-Python wheel), columnar, with native MATERIALIZED VIEW + a vectorized executor that handles correlated-subquery rewrites natively (per the CA.0 spike).Phase CA in progress.
DUCKDBis being added as part of the CA.0 → CA.8 swap that replacesSQLITE. During the migration both enum values coexist; CA.8 deletesSQLITE. Each dialect-dispatch helper gets its DuckDB branch in CA.2; until then,DUCKDBmay silently fall through to the Oracle fallback shape in helpers that haven’t been ported — exercising it without that port will misbehave. The CA.0 audit atdocs/audits/ca_0_duckdb_spike.mdis the source of truth for each helper’s DuckDB binding.- DUCKDB = 'duckdb'
- ORACLE = 'oracle'
- POSTGRES = 'postgres'
- recon_gen.common.sql.dialect.analyze_table(name, dialect)[source]
Refresh planner statistics on a table or matview.
Postgres:
ANALYZE name;. Oracle:BEGIN DBMS_STATS.GATHER_TABLE_STATS(USER, 'name'); END;. SQLite:ANALYZE name;(same syntax as Postgres). Returned string is fully terminated.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.bigint_type(dialect)[source]
Signed 64-bit integer column type.
Postgres + SQLite
BIGINT(SQLite accepts BIGINT as an alias of INTEGER affinity — both store as 64-bit when needed). Oracle has no BIGINT;NUMBER(19)is the canonical signed-64-bit column type (covers the full ±9.2×10^18 BIGINT range).Introduced for AO.1’s money-as-integer-cents migration; the three money columns (amount_money / money / expected_eod_balance) use this type instead of DECIMAL(20,2) so SQLite stores them as exact INTEGER (no REAL float dust) and the read-side
cents_to_dollars_sqlhelper projects back to dollars when needed.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.boolean_type(dialect)[source]
Boolean column type.
Postgres has a native
BOOLEAN; Oracle 19c does not, so the canonical encoding isNUMBER(1)with aCHECK (col IN (0, 1)). SQLite has no native BOOLEAN — usesINTEGER(0/1) by convention. The helper returns just the type name — callers that need the CHECK constraint compose it themselves.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.cast(expr, type_name, dialect)[source]
Cast
exprtotype_name.Postgres
expr::type/ OracleCAST(expr AS type)/ SQLiteCAST(expr AS type)(SQLite uses standard SQL CAST syntax; type-name aliasing remaps Postgres-shape names to SQLite affinity names where they differ).- Return type:
str- Parameters:
expr (str)
type_name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.column_name(name, dialect)[source]
Per-dialect natural case for an unquoted column identifier.
Postgres + SQLite case-fold unquoted identifiers to lowercase; Oracle case-folds to UPPERCASE. Storing column names in the dialect’s natural case lets every reference (DDL column, dataset SQL projection, App2 outer-SELECT alias, QuickSight
Columnsdeclaration) use the same string without quote-juggling — and lets unquoted refs in dataset SQL pick up the matching column on every dialect.Used at every site that emits a column name into either SQL output or the QuickSight Dataset.Columns metadata: callers pass the canonical lowercase column name from the contract, this helper folds to the dialect’s natural case, and the result is the string that both the database and QuickSight will agree on.
The bridge this replaces (
_oracle_lowercase_alias_wrapperincommon/dataset_contract.py) wrapped Oracle dataset SQL in an outer alias-rename to back-port UPPERCASE → quoted-lowercase for QS. The wrapper produced an asymmetry App2’swrap_for_visualcouldn’t follow (ORA-00904: "ACCOUNT_ID": invalid identifier), and every new dialect-port surfaced fresh case-confusion edge cases — see Y.3.f for the full bridge-removal plan.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.concat_agg(column_expr, separator, dialect)[source]
Aggregate text values from a group into a delimited string.
First introduced for AB.3.3’s
_xor_group_violationmatview (fired_railscolumn — comma-separated rail names per Transfer). Dialect mapping:Postgres:
STRING_AGG(col, ',')— null-safe (NULLs skipped), no implicit ordering.Oracle:
LISTAGG(col, ',') WITHIN GROUP (ORDER BY col)— deterministic ordering by value; truncation at 4000 chars by default (acceptable for fired-rails: bounded by leg_rails count).SQLite:
GROUP_CONCAT(col, ',')— null-safe, no implicit ordering (matches PG’s contract).
separatoris wrapped in single quotes; callers pass the bare delimiter (e.g.,','or', ').column_expris interpolated raw — caller’s responsibility to quote / cast as appropriate for the dialect.- Return type:
str- Parameters:
column_expr (str)
separator (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.create_matview(name, body_sql, dialect)[source]
CREATE MATERIALIZED VIEWwith the right options per dialect.Postgres: bare
CREATE MATERIALIZED VIEW name AS body(build- on-create + manual refresh are the defaults). Oracle: explicitBUILD IMMEDIATE REFRESH ON DEMANDso behavior matches the Postgres expectation; without those options Oracle defaults toREFRESH FORCE ON DEMAND(incremental fast-refresh attempt first), which has more setup requirements. SQLite has noCREATE MATERIALIZED VIEW: emitsCREATE TABLE name AS body(the matview becomes a plain table populated at create time). Refresh becomesDELETE FROM name; INSERT INTO name <body>;(seerefresh_matview).Note this helper does NOT add a trailing
;— it’s a one-shot convenience for callers that want the whole CREATE in one string, but most callers in this codebase splice theBUILD IMMEDIATE …suffix into a template viamatview_options(dialect)instead so the SELECT body stays inline + readable.- Return type:
str- Parameters:
name (str)
body_sql (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.date_literal(iso_value, dialect)[source]
A SQL date literal that compares correctly on every dialect.
iso_valueis theYYYY-MM-DDstring form (caller produces it viadate.isoformat()). The helper wraps it in the per-dialect syntax that produces a value comparable against the dialect’s DATE / TIMESTAMP / TEXT-shaped date columns.Postgres + Oracle:
DATE 'YYYY-MM-DD'— the SQL-standard date literal, accepted by both. Compares natively against DATE and coerces correctly against TIMESTAMP columns.SQLite:
'YYYY-MM-DD'— a plain text literal. SQLite has no native DATE type and stores ISO dates as TEXT; lexicographic TEXT comparison happens to be correct for ISO-8601 (‘2030-01-01’ < ‘2030-01-02’ lexically, same as date-wise). The SQL-standardDATE 'literal'form is rejected by SQLite (parsesDATEas a column reference), andCAST('YYYY-MM-DD' AS DATE)coerces to INTEGER 2030 (NUMERIC affinity extracts the leading digits) — both are wrong for SQLite. Use this helper instead of inline string formatting at every audit / matview / dataset SQL site that needs a date literal in a WHERE / CASE WHEN comparison.- Return type:
str- Parameters:
iso_value (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.date_minus_days(date_expr, n, dialect)[source]
Subtract
ndays from a date expression.Postgres uses
date - INTERVAL '<n> day'; Oracle’s DATE arithmetic interpretsdate - nas N days directly. SQLite usesdate(expr, '-N days').- Return type:
str- Parameters:
date_expr (str)
n (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.date_trunc_day(timestamp_expr, dialect)[source]
Truncate a timestamp expression to its day boundary, preserving a timestamp-shaped result type so downstream JOINs against TIMESTAMP columns don’t fall through implicit conversion.
Postgres
DATE_TRUNC('day', expr)returns the same type as the input (TIMESTAMP → TIMESTAMP at 00:00:00). Oracle’sTRUNC(X)on a TIMESTAMP returns a DATE, which loses subseconds + the timestamp shape; wrapping inCAST(... AS TIMESTAMP)puts it back in the timestamp domain so the L1 invariant matviews compare equality the same way on both dialects. SQLite usesdatetime(expr, 'start of day')— returnsYYYY-MM-DD HH:MM:SStext at midnight, sortable + groupable + JOIN-able against the other text-shaped timestamp columns.Distinct from
to_date(which returns DATE-shape on both): usedate_trunc_daywhen the result needs to behave as a timestamp in joins / comparisons; useto_datewhen the result is the final column the dashboard reads as a date.- Return type:
str- Parameters:
timestamp_expr (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.day_text(timestamp_expr, dialect)[source]
Render a timestamp expression as its
YYYY-MM-DDday string.Unlike
date_trunc_day()(which keeps a timestamp-shaped result for JOIN/equality against TIMESTAMP columns), this collapses to a plain text day key for comparisons that must tolerate either side being a string. Used by the Daily Statement balance-date narrow (AO.2/AO.10): the pushed-down<<$pL1DsBalanceDate>>param arrives as an ISO string in every renderer, andTRUNC(<string>)is an ORA-00932 on Oracle — comparing day-text sidesteps it. Pair withSUBSTR(<param>, 1, 10)on the param side (date or datetime → itsYYYY-MM-DDprefix); ISO day strings also compare correctly with</>=lexically.Postgres + Oracle:
TO_CHAR(expr, 'YYYY-MM-DD'). SQLite + DuckDB:strftime('%Y-%m-%d', expr)(DuckDB has noTO_CHARin the core build — strftime ships natively and accepts the same Postgres-style%Y-%m-%dformat spec; SQLite stores datetimes asYYYY-MM-DD HH:MM:SStext → its date portion).- Return type:
str- Parameters:
timestamp_expr (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.decimal_type(precision, scale, dialect)[source]
Fixed-precision decimal.
Postgres
DECIMAL(p,s)/ OracleNUMBER(p,s)/ SQLiteNUMERIC(SQLite is typeless and stores all numerics as one of INTEGER / REAL / TEXT per its dynamic typing rules;NUMERICis the affinity that prefers exact representation).- Return type:
str- Parameters:
precision (int)
scale (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.drop_index_if_exists(name, dialect)[source]
Idempotent DROP INDEX.
Postgres
DROP INDEX IF EXISTS …;/ Oracle PL/SQL block catching ORA-01418 (index does not exist) / SQLite nativeDROP INDEX IF EXISTS …;. Returned string is fully terminated.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.drop_matview_if_exists(name, dialect)[source]
Idempotent DROP MATERIALIZED VIEW.
Postgres
DROP MATERIALIZED VIEW IF EXISTS …;/ Oracle PL/SQL block catching ORA-12003 (matview does not exist). SQLite has noCREATE MATERIALIZED VIEW: matviews are emitted as plain tables (CREATE TABLE name AS SELECT …), so the SQLite branch drops them as ordinary tables (DROP TABLE IF EXISTS name;). Returned string is fully terminated — same convention asdrop_table_if_exists.Oracle half-dropped-state hardening (Y.7-followup): a
CREATE MATERIALIZED VIEWinterrupted mid-flight (e.g. a SIGTERM-killed schema apply) can leave the matview’s container table behind without the matview metadata —DROP MATERIALIZED VIEWthen swallows ORA-12003 (“no such matview”) while the stray table survives, and the nextCREATE MATERIALIZED VIEWhits ORA-00955 (“name already used”). So the Oracle branch emits aDROP TABLEof the same name on the LINE AFTER the matview drop (swallowing ORA-00942 = nothing there, and ORA-12083 = it’s a live container the matview drop already handled). On a healthy matview the table drop is a harmless no-op; on a half-dropped one it’s the cleanup that makes the re-apply idempotent.The two blocks are joined with ``
- `` (not a space): ``split_oracle
_script`` is a line scanner that treats a line beginning
BEGIN/DECLAREand endingEND;as one PL/SQL block — two blocks on one line would be handed tocursor.executeas a singleBEGIN…END; BEGIN…END;string, which Oracle’s PL/SQL parser rejects (PLS-00103 on the secondBEGIN). All callers ``”
“.join`` the drop strings, so an internal newline is fine.
- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.drop_table_if_exists(name, dialect)[source]
Idempotent DROP TABLE — emits CASCADE so dependent FKs / views drop transitively (where the dialect supports CASCADE).
Postgres has native
DROP TABLE IF EXISTS … CASCADE. Oracle 19c needs a PL/SQL block that catches ORA-00942 (table not found). SQLite hasDROP TABLE IF EXISTS …but no CASCADE keyword — SQLite enforces FK-cascading viaPRAGMA foreign_keysplusON DELETE CASCADEdeclarations on the FK; the schema we emit has no FKs, so omitting CASCADE has no behavioral impact.Returned string is fully terminated (Postgres trailing
;, OracleEND;PL/SQL terminator, SQLite trailing;). Callers concatenate directly without appending;to avoid a double-semicolon that Oracle’s PL/SQL parser rejects.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.drop_view_if_exists(name, dialect)[source]
Idempotent DROP VIEW.
Postgres
DROP VIEW IF EXISTS …;/ Oracle PL/SQL block catching ORA-00942 / SQLite nativeDROP VIEW IF EXISTS …;. Returned string is fully terminated.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.dual_from(dialect)[source]
Suffix that makes a constant SELECT valid on both dialects.
Postgres accepts
SELECT 'x' AS colwith no FROM. Oracle 19c requires every SELECT to have a FROM clause; the canonical Oracle pseudo-table for “one row, no real source” isdual. SQLite accepts the bareSELECT(like Postgres).Returns
""on Postgres / SQLite / DuckDB and" FROM dual"on Oracle. Compose inline at the end of the SELECT list:f"SELECT {expr} AS col{dual_from(dialect)}". Combine withWHERE 1=0(works on every dialect) for an empty-row sentinel branch —WHERE FALSEis Postgres-only and breaks Oracle.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.epoch_seconds_between(later, earlier, dialect)[source]
Difference between two timestamps in whole + fractional seconds.
Postgres
EXTRACT(EPOCH FROM (later - earlier)). Oracle has no EPOCH unit; the equivalent for TIMESTAMP arithmetic (which yieldsINTERVAL DAY TO SECOND) is the sum of EXTRACT(DAY/HOUR/MINUTE/SECOND FROM …) terms. SQLite uses(julianday(later) - julianday(earlier)) * 86400— julianday returns the Julian Day Number as a REAL, so subtracting two julianday values yields fractional days; multiplying by 86400 gives seconds.- Return type:
str- Parameters:
later (str)
earlier (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.fetch_first_one_row(dialect)[source]
Return the row-limiting clause that picks the first row of an ORDER BY —
LIMIT 1for Postgres + SQLite,FETCH FIRST 1 ROW ONLYfor Oracle. Oracle 19c rejectsLIMIT; Postgres + SQLite accept both forms butLIMITis the established convention here.- Return type:
str- Parameters:
dialect (Dialect)
- Compose at the tail of a SELECT after
ORDER BY: f”SELECT … ORDER BY x DESC {fetch_first_one_row(dialect)}”
- recon_gen.common.sql.dialect.greatest(*args, dialect)[source]
Row-wise greatest of two or more expressions.
PG / Oracle:
GREATEST(a, b, ...)— the SQL-standard scalar function (not an aggregate). SQLite:MAX(a, b, ...)— SQLite overloadsMAXto be a row-wise scalar when called with 2+ arguments and the column-aggregate when called with 1. Both forms have identical semantics for our use case (clamping expressions likeGREATEST(x - y, 0)).- Return type:
str- Parameters:
args (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.interval_days(n, dialect)[source]
A SQL interval literal of
ndays.Postgres
INTERVAL '<n> day'/ OracleINTERVAL '<n>' DAY. SQLite has no INTERVAL type — date arithmetic uses thedate()function with a'+N days'modifier (seedate_minus_days); a bare interval literal isn’t usable on its own. This helper returns the plain string'<n> days'for SQLite so callers that compose it withdate(expr, '<interval>')work; sites that try to subtract a bare interval (e.g.RANGE BETWEEN INTERVAL ... PRECEDING) should userange_interval_daysinstead, which adapts to SQLite’s numeric-only RANGE frames via a Julian-day projection.- Return type:
str- Parameters:
n (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.json_array_iterate(json_expr, array_path, *, alias, dialect)[source]
LEFT JOINclause that iterates a JSON array withinjson_expratarray_path, exposing each element underalias.value(andalias.keyon SQLite — unused but harmless).Per-dialect renderings (AW.0.b spike confirmed the SQLite shape; PG + Oracle use the SQL/JSON-standard
JSON_TABLEform per the project’s portability constraint — no JSONB, no->>, only SQL/JSON path syntax; PG 17+ required for nativeJSON_TABLE):SQLite:
json_each(<expr>, '<path>')— table-valued function over JSON1; each row haskey+valuecolumns.DuckDB:
json_each(<expr>, '<path>')— same shape as SQLite (DuckDB shipsjson_eachnatively; columns includekeyandvalueplus type metadata the callers don’t read).PG 17+:
JSON_TABLE(<expr>::json, '<path>[*]' COLUMNS (value JSON PATH '$'))— SQL/JSON-standard, no JSONB needed.Oracle 12c+: same JSON_TABLE shape;
VARCHAR2(4000) FORMAT JSON PATH '$'for the per-row value column (Oracle doesn’t have a JSON type pre-21c; uses VARCHAR2 + FORMAT JSON hint).
json_expris the SQL expression yielding the JSON document (typically a scalar subquery like(SELECT l2_yaml FROM <p>_config)).array_pathis the SQL/JSON path to the array (e.g.'$.rails').aliasis the per-row alias the matview SQL uses to reference the iteration (e.g.rail).- Return type:
str- Parameters:
json_expr (str)
array_path (str)
alias (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.json_check(col, dialect)[source]
CHECK (col IS NULL OR col IS JSON)in PG / Oracle; SQLite usesCHECK (col IS NULL OR json_valid(col)).The
IS JSONSQL/JSON-standard constraint is supported in Postgres 16+ and Oracle 12.2+ — bytes-identical there. SQLite + DuckDB have noIS JSONpredicate but both shipjson_valid(text)(returns 1 / true if the argument is well-formed JSON; 0 / false otherwise) — SQLite via the JSON1 extension (built into stdlibsqlite3since 3.38), DuckDB natively.- Return type:
str- Parameters:
col (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.json_field_extract(value_expr, field_path, dialect)[source]
Extract a scalar field from a per-row JSON element (e.g. one iteration of json_array_iterate).
All dialects use SQL/JSON-standard path syntax — no JSONB-specific operators (the project bans ->>, ->, @> per the portability constraint).
SQLite:
json_extract(<value_expr>, '<field_path>')DuckDB:
json_extract_string(<value_expr>, '<field_path>')— unwraps the scalar (DuckDB’sJSON_VALUEreturns quoted JSON form; seejson_valuefor the underlying gotcha).PG 12+ / Oracle 12c+:
JSON_VALUE(<value_expr>, '<field_path>')— SQL/JSON-standard scalar extract.
field_pathis the SQL/JSON path (e.g.'$.name').- Return type:
str- Parameters:
value_expr (str)
field_path (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.json_text_type(dialect)[source]
Bounded text type for columns holding JSON metadata documents.
Diverges from
text_type(which returns PostgresTEXT/ OracleCLOB) by emitting a boundedVARCHAR(4000)/VARCHAR2(4000)so the columns behave like ordinary strings on both dialects. Why bound:Oracle
CLOBcan’t be aggregated (MIN/MAX/GROUP BYreject CLOB with ORA-00932), can’t appear inDISTINCT/ORDER BY, and failsINcomparisons againstVARCHAR2literals. Queries that pick a representativemetadataper transfer viaMAX(metadata)need it bounded.Bounding Postgres to the same 4000-char limit keeps the two dialects symmetric so a “data too long” failure surfaces on either DB instead of leaking past PG and breaking only on Oracle.
4000 chars covers every JSON metadata document the L2 schema emits (typically 5–20 keys with short values). Banks with longer documents either trim at the ETL boundary or enable Oracle’s
MAX_STRING_SIZE=EXTENDED(lifts VARCHAR2 to 32767) and bump this helper.SQLite uses
TEXThere too — it’s typeless under the hood (VARCHAR(N)parses but the length is purely advisory), so matching the symmetric “string-shaped” treatment by emitting plainTEXTkeeps the SQL readable.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.json_value(col, path_expr, dialect)[source]
Extract a scalar from a JSON-shaped column via SQL/JSON path.
PG / Oracle:
JSON_VALUE(col, path_expr)— the SQL/JSON-standard function (Postgres 12+, Oracle 12.2+). SQLite:json_extract(col, path_expr)— the JSON1 extension’s equivalent (built into stdlibsqlite33.38+). DuckDB:json_extract_string(col, path_expr)— DuckDB also hasJSON_VALUEandjson_extractbut their semantics diverge from the SQL/JSON-standard (they return quoted JSON form rather than unwrapped scalar text);json_extract_stringunwraps the scalar, matching the PG/Oracle/SQLite contract. Captured by CA.0 spike audit (docs/audits/ca_0_duckdb_spike.md— JSON portability). All four return scalar TEXT for the path’s leaf; missing paths return NULL.path_expris the SQL expression (already wrapped in quotes / constructed at the call site, e.g."'$.customer_id'"or"'$.' || pKey") — same shape on every dialect, so the helper only swaps the function name.- Return type:
str- Parameters:
col (str)
path_expr (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.lob_substr(expr, n, dialect)[source]
Coerce a (potentially CLOB-typed) expression to a bounded VARCHAR by extracting its first
ncharacters.Needed for BC.12’s
<prefix>_config_kv.value CLOBcolumn: Oracle rejectsMAX(CASE WHEN k='X' THEN value END)with ORA-22849 whenvalueis CLOB (the MAX aggregate doesn’t accept LOB types). Wrap the CLOB read inDBMS_LOB.SUBSTR(value, n, 1)to coerce to VARCHAR2 inside the aggregate. PG + SQLite use plainSUBSTRING/SUBSTRso the same dialect-blind expression shape compiles everywhere.Oracle:
DBMS_LOB.SUBSTR(<expr>, <n>, 1)— extractnchars from offset 1; result is VARCHAR2(n) capped at 4000.PG:
SUBSTRING(<expr> FROM 1 FOR <n>)— SQL-standard substring on TEXT.SQLite:
SUBSTR(<expr>, 1, <n>)— standard.
Callers wrap the matview-consumed leaf fields (
name,max_pending_age_seconds, cap, etc.) in this helper inside the typed projection views’MAX(CASE WHEN ...)aggregates so the matview-source view body stays Oracle-LOB-safe.- Return type:
str- Parameters:
expr (str)
n (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.matview_create_keyword(dialect)[source]
The
CREATE …keyword the matview templates emit per dialect.Postgres + Oracle:
CREATE MATERIALIZED VIEW. SQLite: justCREATE TABLE(matviews land as plain tables — refresh becomes a DELETE + INSERT pair, seerefresh_matview). Used bycommon.l2.schemaso the per-matview template strings can stay one-line + dialect-clean instead of branching at every site.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.matview_options(dialect)[source]
Per-dialect suffix between
CREATE MATERIALIZED VIEW <name>andAS <body>. Postgres takes none; Oracle needsBUILD IMMEDIATE REFRESH COMPLETE ON DEMANDto match Postgres’s build-on-create + manual-REFRESH semantics (without it Oracle defaults toREFRESH FORCE ON DEMAND, which has more setup requirements). SQLite usesCREATE TABLE … AS(seematview_create_keyword); no per-keyword suffix applies, so returns the empty string.Used by
common.l2.schemato splice the suffix into per-matview template strings (so the SELECT body stays inline + readable). Returns the empty string on Postgres + SQLite so the substitution is a no-op.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.order_by_day_expr(day_col, dialect)[source]
Per-dialect
ORDER BYprojection for date-keyed window functions paired withrange_interval_days.PG / Oracle: bare column name (intervals work directly against DATE / TIMESTAMP). SQLite: wrap in
julianday(<col>)so the RANGE frame’s numeric arithmetic lands on the same scale asrange_interval_days(N, SQLITE) = str(N).- Return type:
str- Parameters:
day_col (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.range_interval_days(n, dialect)[source]
Day-interval expression for use inside a window-function
RANGE BETWEEN <expr> PRECEDINGclause.PG / Oracle take ordinary INTERVAL literals — same form
interval_daysreturns. SQLite’sRANGE BETWEENonly accepts numeric expressions (the ORDER BY column must be numeric too), so the call site needs to projectposted_daythroughjulianday()and use a bare integer here. Returnsstr(n)for SQLite so aRANGE BETWEEN N PRECEDINGframe, paired withORDER BY julianday(posted_day), gives the same per-day semantics PG / Oracle deliver via INTERVAL.- Return type:
str- Parameters:
n (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.refresh_matview(name, dialect)[source]
REFRESH MATERIALIZED VIEWper dialect.Postgres:
REFRESH MATERIALIZED VIEW name;. Oracle: a PL/SQL block invokingDBMS_MVIEW.REFRESH('name', method => 'C')—C= complete refresh, matching Postgres semantics. SQLite: NOT a single statement — refresh on a matview-as-table is a DELETE + INSERT pair, but the body SELECT lives in the schema template, not here. The SQLite branch returns a sentinel that callers incommon.l2.schema.refresh_matviews_sqlroute through_emit_sqlite_refresh_block(which knows the SELECT body for each matview). Returned string is fully terminated for PG / Oracle.- Return type:
str- Parameters:
name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.serial_type(dialect)[source]
Auto-incrementing 64-bit append-only key.
Postgres
BIGSERIAL/ OracleNUMBER GENERATED BY DEFAULT AS IDENTITY. DuckDB falls through to a bareBIGINTpaired with aCREATE SEQUENCE+DEFAULT nextval('<seq>')on the column (see_entry_column_declincommon/l2/schema.py; CA.3 lands the runner setup).CB.17.i (2026-06-04) — Oracle moved from
GENERATED ALWAYStoGENERATED BY DEFAULT. The two differ only on explicit-value INSERTs:ALWAYSrejects them with ORA-32795,BY DEFAULTaccepts the supplied value and uses the sequence only when the column is omitted from the INSERT. Production seed paths omit the column either way (no behavior change); the V-overlayclone_base_to_v_sqldoesINSERT INTO v_X SELECT * FROM Xwhich sends the source’s identity values in the column position and broke underALWAYS.BY DEFAULTis the same semantic PostgresBIGSERIALdefaults to (DEFAULT-on-omit, accept-on- supply) and matches DuckDB’s sequence-DEFAULT shape.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.text_type(dialect)[source]
Unbounded character data.
Postgres
TEXT/ DuckDBTEXT/ OracleCLOB/ SQLiteTEXT.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.timestamp_type(dialect)[source]
TZ-naive timestamp, identical on both dialects.
Returns
TIMESTAMPregardless of dialect (P.9a). Timezone normalization is the integrator’s contract — the L2 schema does not store timezone metadata and does not convert across zones. Banks reading from sources in multiple zones MUST normalize at the ETL boundary (typically to UTC or the institution’s local business zone).Why standardized: the prior split helpers (
timestamp_tz_type+pk_safe_timestamp_type) bridged Postgres TIMESTAMPTZ / Oracle TIMESTAMP WITH TIME ZONE for non-PK columns and demoted to plain TIMESTAMP for PK columns (Oracle rejects TZ-aware TIMESTAMPs in PKs with ORA-02329). The split surfaced as a cross-dialect divergence with no compensating value — neither engine performs query-time TZ conversion in a way the dashboards rely on, and the demotion was already happening for half the columns. Unifying on plain TIMESTAMP makes the schema byte- identical between dialects.- Return type:
str- Parameters:
dialect (Dialect)
- recon_gen.common.sql.dialect.to_date(timestamp_expr, dialect)[source]
Truncate a timestamp expression to its date component.
Postgres
expr::date/ OracleTRUNC(expr)/ SQLiteDATE(expr)(SQLite has no nativeDATEtype —DATE()returns the date portion asYYYY-MM-DDtext, which is sortable + groupable the same way the typed counterparts are).- Return type:
str- Parameters:
timestamp_expr (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.typed_null(type_name, dialect)[source]
Typed NULL literal.
Postgres
NULL::type/ OracleCAST(NULL AS type)/ SQLiteCAST(NULL AS type)(same standard SQL CAST as Oracle; type aliasing maps to SQLite affinity names).- Return type:
str- Parameters:
type_name (str)
dialect (Dialect)
- recon_gen.common.sql.dialect.varchar_type(n, dialect)[source]
Bounded variable-length character.
Postgres
VARCHAR(n)/ OracleVARCHAR2(n)/ SQLiteTEXT(SQLite is typeless internally; the(n)would parse but enforce nothing, so emit plainTEXTfor clarity).- Return type:
str- Parameters:
n (int)
dialect (Dialect)
- recon_gen.common.sql.dialect.with_recursive(dialect)[source]
Recursive-CTE preamble keyword.
Postgres requires the explicit
WITH RECURSIVEkeyword. Oracle 19c infers recursion from the CTE body’s self-reference and accepts (but does not require)RECURSIVE— emit justWITHfor portability across older Oracle releases. SQLite requiresWITH RECURSIVE(same as Postgres).- Return type:
str- Parameters:
dialect (Dialect)