Files
opencode-build/skills/sql-context-builder/SKILL.md
T
2026-06-18 13:59:01 +08:00

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-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

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

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