tdfs4ds.agent.sql_qualify¶
tdfs4ds.agent.sql_qualify
¶
Deterministic column qualification for Teradata SQL.
Why this exists¶
The query optimizer's simplification pass flattens nesting: derived tables are merged into their parent, CTEs are inlined, wrapper SELECT layers are removed. That merges two previously-distinct name scopes into one.
A bare (unqualified) column that resolved uniquely in the original nested form can, after the merge, resolve to a different relation — or become outright ambiguous (Teradata Error 3809). Neither outcome is a syntax error the existing gate can see:
- silent rebinding → EXPLAIN compiles,
user_scoreis unaffected, the query now reads a different column. Business logic changed, nothing flagged. - Error 3809 → the syntax-repair loop in
skill_simplify_queryasks the LLM to fix it, and the LLM picks an alias with no information about which one was correct. A coin flip is written into the accepted SQL.
The fix is to remove the degree of freedom before the LLM ever sees the query. If a query compiles, every bare column in it already resolves uniquely in its own scope — Teradata rejects ambiguity outright. So the correct qualification is not a guess, it is derivable: resolve each bare column against the relations visible in its scope, and write the answer down explicitly. A later flatten then cannot rebind it, because the alias travels with the column.
Design rules¶
- Never regenerate the SQL. These process views carry
CURRENT VALIDTIME,NONSEQUENCED VALIDTIME,QUALIFY,PERIOD/INTERVALand UAF constructs. Parsing to an AST and re-emitting would mangle exactly the grammar this codebase has spent dozens of fixes taming — the same class of silent corruption we are trying to prevent. This module only ever insertsalias.at computed character offsets; every other byte is preserved. - Only qualify what is provable. A prefix is written only when the name is a column of exactly one visible relation whose column list is fully known. Not found anywhere, found in two places, or any relation in scope with an unknown column list → leave the token alone. Unknown is always "don't touch", never "guess".
- Fail open, per scope. A scope we cannot analyse is skipped; the rest of the query is still qualified. The worst case is the status quo.
The module is dependency-free (stdlib only) and DB-free — build_schema_map
is the only function that touches Teradata, and it is optional.
_KEYWORDS
module-attribute
¶
_KEYWORDS: set = {'SELECT', 'FROM', 'WHERE', 'GROUP', 'BY', 'ORDER', 'HAVING', 'QUALIFY', 'WITH', 'AS', 'ON', 'USING', 'DISTINCT', 'ALL', 'TOP', 'SAMPLE', 'UNION', 'INTERSECT', 'EXCEPT', 'MINUS', 'RECURSIVE', 'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'CROSS', 'NATURAL', 'INSERT', 'UPDATE', 'DELETE', 'MERGE', 'INTO', 'VALUES', 'SET', 'CREATE', 'REPLACE', 'VIEW', 'TABLE', 'VOLATILE', 'PRIMARY', 'INDEX', 'LOCKING', 'ROW', 'ACCESS', 'FOR', 'LOCK', 'AND', 'OR', 'NOT', 'IN', 'EXISTS', 'BETWEEN', 'LIKE', 'IS', 'NULL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'ASC', 'DESC', 'NULLS', 'FIRST', 'LAST', 'ESCAPE', 'SOME', 'ANY', 'OVER', 'PARTITION', 'ROWS', 'RANGE', 'PRECEDING', 'FOLLOWING', 'UNBOUNDED', 'CURRENT', 'PIVOT', 'UNPIVOT', 'VALIDTIME', 'TRANSACTIONTIME', 'NONSEQUENCED', 'SEQUENCED', 'OF', 'PERIOD', 'BEGIN', 'PORTION', 'CAST', 'INTERVAL', 'DATE', 'TIME', 'TIMESTAMP', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'ZONE', 'INT', 'INTEGER', 'SMALLINT', 'BIGINT', 'BYTEINT', 'DECIMAL', 'NUMERIC', 'FLOAT', 'REAL', 'DOUBLE', 'PRECISION', 'CHAR', 'VARCHAR', 'CHARACTER', 'VARYING', 'CLOB', 'BLOB', 'BYTE', 'VARBYTE', 'NUMBER', 'JSON', 'XML', 'DEFAULT', 'FORMAT', 'TITLE', 'CHARACTERS', 'UPPERCASE', 'CASESPECIFIC', 'COMPRESS', 'TRUE', 'FALSE', 'UNKNOWN', 'EXECUTE', 'FUNCTION', 'RETURNS', 'SCRIPT', 'TRIM', 'BOTH', 'LEADING', 'TRAILING', 'FROM_BYTES', 'SEL'}
_REL_TERMINATORS
module-attribute
¶
_REL_TERMINATORS: set = {'WHERE', 'GROUP', 'ORDER', 'HAVING', 'QUALIFY', 'ON', 'USING', 'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'CROSS', 'NATURAL', 'UNION', 'INTERSECT', 'EXCEPT', 'MINUS', 'SAMPLE', 'WITH', 'SET'}
QualifyReport
¶
Outcome of a qualification pass.
Attributes:
| Name | Type | Description |
|---|---|---|
sql |
The qualified SQL (identical to the input when nothing was provably qualifiable). |
|
changed |
True when at least one prefix was inserted. |
|
qualified |
list
|
List of |
skipped |
list
|
Human-readable reasons a scope or token was left alone. |
ambiguous |
list
|
Bare columns found in 2+ visible relations. These are the landmines: the original compiles, so one of our column lists is wrong OR the name is a select-list alias. Never qualified, always reported. |
__slots__
class-attribute
instance-attribute
¶
_Relation
¶
One entry in a FROM/JOIN clause.
_Scope
¶
A SELECT and the relations visible to it.
__slots__
class-attribute
instance-attribute
¶
__slots__ = ('select_idx', 'depth', 'start_idx', 'end_idx', 'relations', 'out_columns', 'local_aliases', 'usable')
_tokenize
¶
Split SQL into tokens, tracking paren depth.
Comments and whitespace are skipped. String literals and quoted identifiers are emitted as single opaque tokens so their contents can never be mistaken for column references.
_find_matching_close
¶
Index of the ) matching the ( at open_idx (or len(toks)).
_scope_extent
¶
Index one past the last token belonging to this SELECT's scope.
_parse_object_name
¶
Parse a, a.b or a.b.c starting at i.
Returns (fq_name, last_part, next_index) or (None, None, i).
_collect_ctes
¶
Map CTE name → index of the ( opening its body.
Handles WITH a AS (...), b AS (...). A CTE with an explicit column list
(WITH a (c1, c2) AS (...)) is recorded with its declared columns.
_parse_from_relations
¶
_parse_from_relations(toks: list, scope: _Scope, schema: dict, derived_out: dict, ctes: dict, report: QualifyReport) -> None
Populate scope.relations from the FROM/JOIN clauses at scope depth.
_read_alias
¶
Read an optional [AS] alias at i. Returns (alias|None, next).
_lookup_schema
¶
Resolve a table reference against the schema map (fully-qualified first).
_select_list_output
¶
Compute the output column names of a scope's SELECT list.
Returns None when the list contains * (unknown shape) or cannot be
determined. Also populates scope.local_aliases.
_build_scopes
¶
Build every SELECT scope, innermost first, resolving derived outputs.
_is_column_position
¶
True when the token at idx sits where a column reference may appear.
qualify_columns
¶
Prefix every provably-unique bare column reference with its relation alias.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sql
|
str
|
Teradata SQL to qualify. Returned byte-identical except for the
inserted |
required |
schema
|
Optional[dict]
|
Map of |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
QualifyReport
|
class: |
QualifyReport
|
internal failure it is the unmodified input. |
build_schema_map
¶
Fetch column lists for refs via HELP COLUMN.
HELP COLUMN db.obj.* is used rather than DBC.ColumnsV because the
latter reports NULL ColumnType for view columns — and process views are
the common case here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refs
|
Iterable of |
required | |
execute_sql
|
Callable taking a SQL string; defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
dict
|
omitted, which makes their scopes unqualifiable (fail-open) rather than |
dict
|
wrongly qualified. |