---
name: sql-context-builder
description: 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-planner` returned status `PLANNED`
- 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.status` is `NEED_USER_CONFIRMATION` — go back to `logic-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

1. `logic_plan` from `logic-planner` (status `PLANNED`)
2. `field_mapping` from `metadata-validator` (status `VALIDATED`)
3. Original `validation_result` for table-level metadata (types, sources)

## Workflow

```dot
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 (`c` for customer, `p` for product, `ch` for 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` (from `validation_result`)
- `source_file` (which metadata file it came from — for traceability)
- `role`: one of `dimension` | `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.steps` must reference fields that exist in this context.
- Every `field_mapping` entry must appear in this context.
- Every `group_by` and `order_by` field must be tagged.
- Every metric in `aggregate` must have a `metric` tag.
- Every predicate in `filter` must reference a `filter` or `time` tag.

## Output Contract

```yaml
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_fields` is empty
- Every metric has a defined `agg` and `expr`
- Every join has a frozen key and a type
- The time grain is set
