7.3 KiB
7.3 KiB
name, description
| name | description |
|---|---|
| sql-context-builder | Use when assembling the single trusted SQL context that the SQL generator and reviewer will consume. Triggers include "build SQL context", "固化 join key", "table aliases", "列出来自哪张表", "准备写 SQL", "context 已经齐了". Consumes a validated Logic Plan + Field Mapping and produces a normalized SQL Context with stable aliases, frozen join keys, and explicit fact/dimension tagging. The SQL generator MUST NOT re-derive any of this. |
SQL Context Builder
Overview
Freeze every decision that the SQL generator would otherwise re-invent. Aliases, join keys, field sources, types, role tags (fact vs dimension vs filter), and time columns are all pinned here exactly once. The output is the single trusted input for sql-review and for any SQL-emitting step.
Core principle: anything not in the SQL Context does not exist for downstream skills. If a downstream skill needs something new, the request must go back through metadata-validator and logic-planner first.
When to Use
Use when:
logic-plannerreturned statusPLANNED- The user is about to write Spark SQL and you need a normalized context (aliases, join keys, column sources)
- Multiple SQL drafts need to be generated against the same logic — context guarantees they stay consistent
Do NOT use when:
logic-plan.statusisNEED_USER_CONFIRMATION— go back tologic-planner- The SQL is trivial (one table, no join) — you can still build the context, but skip if the user explicitly opts out
- The task is to translate the plan into SQL — that is a separate concern from this skill
Inputs
logic_planfromlogic-planner(statusPLANNED)field_mappingfrommetadata-validator(statusVALIDATED)- Original
validation_resultfor table-level metadata (types, sources)
Workflow
digraph sql_context_builder {
"Read logic_plan + field_mapping + validation_result" [shape=box];
"Assign stable alias to every table" [shape=box];
"Freeze join keys (left_alias.field = right_alias.field)" [shape=box];
"Resolve every field to (alias, column, type, source_file)" [shape=box];
"Tag each field: dimension | metric | filter | time | join_key" [shape=box];
"Mark fact vs dimension tables" [shape=box];
"Verify Logic Plan fields all exist in context" [shape=box];
"All required fields present?" [shape=diamond];
"Emit SQL Context" [shape=box];
"Emit SQL Context + missing_fields" [shape=box];
"Read logic_plan + field_mapping + validation_result" -> "Assign stable alias to every table";
"Assign stable alias to every table" -> "Freeze join keys (left_alias.field = right_alias.field)";
"Freeze join keys (left_alias.field = right_alias.field)" -> "Resolve every field to (alias, column, type, source_file)";
"Resolve every field to (alias, column, type, source_file)" -> "Tag each field: dimension | metric | filter | time | join_key";
"Tag each field: dimension | metric | filter | time | join_key" -> "Mark fact vs dimension tables";
"Mark fact vs dimension tables" -> "Verify Logic Plan fields all exist in context";
"Verify Logic Plan fields all exist in context" -> "All required fields present?";
"All required fields present?" -> "Emit SQL Context" [label="yes"];
"All required fields present?" -> "Emit SQL Context + missing_fields" [label="no"];
}
Rules
Alias Assignment
- One alias per table, never reused.
- Fact table (the one with the primary metric) gets a short alias first (
o,f,t1). - Dimension tables get meaningful aliases (
cfor customer,pfor product,chfor channel). - Aliases are frozen here. The SQL generator and reviewer must not invent new ones.
Join Key Freezing
- For every
logic_plan.steps[kind=join], freeze the exact predicate:left_alias.col1 = right_alias.col2
- Include join type (inner / left / right / full / semi / anti).
- Include cardinality assumption carried over from the Logic Plan.
- If two tables could join on multiple keys, freeze the chosen one here. Other candidates are out of scope for SQL.
Field Resolution
For every field used in the Logic Plan, record:
alias(which table it lives on)column(physical name)type(fromvalidation_result)source_file(which metadata file it came from — for traceability)role: one ofdimension|metric|filter|time|join_key|derived
A field may have multiple roles (e.g. a customer_id that is both join_key and dimension); list all.
Fact vs Dimension Tagging
- The table containing the primary event/measure is
fact. - Tables that enrich (customer, product, channel, city) are
dimension. - One fact per query. If the Logic Plan implies multiple facts, raise a
pending_question— that is a planning issue, not a context issue.
Coverage Check
- Every step in
logic_plan.stepsmust reference fields that exist in this context. - Every
field_mappingentry must appear in this context. - Every
group_byandorder_byfield must be tagged. - Every metric in
aggregatemust have ametrictag. - Every predicate in
filtermust reference afilterortimetag.
Output Contract
sql_context:
aliases:
- alias: o
table: loan_order
role: fact
source_file: metadata/loan_order.json
- alias: c
table: customer_info
role: dimension
source_file: metadata/customer_info.md
joins:
- left: { alias: o, column: customer_id }
right: { alias: c, column: customer_id }
type: left
cardinality_assumption: 1:N
fields:
- alias: o
column: customer_id
type: string
role: [join_key]
source_file: metadata/loan_order.json
- alias: o
column: loan_amount
type: decimal(18,2)
role: [metric]
source_file: metadata/loan_order.json
- alias: o
column: apply_time
type: timestamp
role: [time, filter]
source_file: metadata/loan_order.json
- alias: c
column: city
type: string
role: [dimension]
source_file: metadata/customer_info.md
metrics:
- alias: total_loan
expr: "SUM(o.loan_amount)"
agg: sum
- alias: customer_cnt
expr: "COUNT(DISTINCT o.customer_id)"
agg: count_distinct
dimensions: ["c.city"]
time_grain:
column: o.apply_time
granularity: day
filters:
- "o.status = 'SUCCESS'"
missing_fields: []
status: <READY | NEED_USER_CONFIRMATION>
Forbidden Behaviors
| Rationalization | Reality |
|---|---|
| "I'll let the SQL generator pick aliases" | Aliases are frozen here. Re-aliasing downstream breaks sql-review. |
| "The join key is obvious, skip the freeze" | The whole point of this skill is to freeze join keys. |
| "Field type doesn't matter, it's all strings in SQL" | Type drives aggregations, casts, and partition-pruning decisions. |
| "Derived columns can be invented in SQL" | Every derived column must already appear in the Logic Plan. |
| "Multiple facts? Just join them all" | Multi-fact queries are a Logic Plan issue. Surface and stop. |
Completion Criteria
status: READY only when:
- Every Logic Plan step has matching entries in this context
missing_fieldsis empty- Every metric has a defined
aggandexpr - Every join has a frozen key and a type
- The time grain is set