How Databricks Forge generates, validates, and repairs LLM-produced SQL across every surface in the application.
Databricks Forge generates SQL via LLM calls in 10 different surfaces (pipeline use case SQL, dashboards, Genie spaces, the Ask Forge assistant, etc.). Every surface follows the same grounded generation philosophy:
The LLM only sees real metadata, never invents.
The defence-in-depth strategy is:
- Prompt grounding -- inject actual table/column schemas into the prompt with explicit instructions that these are the ONLY valid identifiers
- Static column validation -- extract column references from the generated SQL and check them against the known schema
- EXPLAIN dry-run -- execute
EXPLAIN <sql>against the warehouse to catch runtime errors (type mismatches, ambiguous columns, missing tables) - Fix cycle -- if validation fails, send the SQL + error back to the LLM with the correct schema for a single repair attempt
- LLM review -- a dedicated review endpoint (
serving-endpoint-review, defaultdatabricks-gpt-5-4) performs multi-dimensional quality assessment (correctness, performance, readability, security, Databricks idiom adherence) vialib/ai/sql-reviewer.ts. This separates generation (creative) from review (critical). When the review endpoint is configured, surfaces that opt in get a post-validation review pass that can auto-fix quality issues EXPLAIN cannot catch (suboptimal joins, non-idiomatic patterns, readability). Feature-gated per surface viaisReviewEnabled(). - Rejection -- if the fix and review both fail, reject the SQL (return null, drop the dataset, or filter the expression)
Not every surface implements all six tiers. The minimum bar is prompt grounding + static validation. See section 6 for the per-surface matrix.
All metadata comes from Unity Catalog information_schema queries in
lib/queries/metadata.ts:
| Function | Returns | Source |
|---|---|---|
listTables(catalog, schema?) |
TableInfo[] |
information_schema.tables |
listColumns(catalog, schema?) |
ColumnInfo[] |
information_schema.columns |
listForeignKeys(catalog, schema?) |
ForeignKey[] |
information_schema.table_constraints + key_column_usage + constraint_column_usage |
listMetricViews(catalog, schema?) |
MetricViewInfo[] |
information_schema.tables where table_type = 'METRIC_VIEW' |
fetchTableInfoBatch(fqns) |
TableInfo[] |
Per-table information_schema lookup |
fetchColumnsBatch(fqns) |
ColumnInfo[] |
Per-table information_schema.columns lookup |
TableInfo { catalog, schema, tableName, fqn, comment, tableType?, dataSourceFormat? }
ColumnInfo { tableFqn, columnName, dataType, ordinalPosition, isNullable, comment? }
ForeignKey { constraintName, tableFqn, columnName, referencedTableFqn, referencedColumnName }
MetricViewInfo { catalog, schema, name, fqn, comment? }
The unified container used by most consumers:
MetadataSnapshot {
tables: TableInfo[]
columns: ColumnInfo[]
foreignKeys: ForeignKey[]
metricViews: MetricViewInfo[]
schemaMarkdown: string // pre-rendered for prompt injection
tableCount, columnCount // summary stats
cachedAt: string // ISO timestamp
}
| Consumer | Source |
|---|---|
| Pipeline (SQL gen, use case gen) | PipelineContext.metadata -- built by the metadata-extraction step (step 2) |
| Genie engine | MetadataSnapshot passed to engine, built from table-selection pass |
| Dashboard engine | MetadataSnapshot passed to engine from pipeline context |
| Dashboard adhoc engine | fetchTableInfoBatch + fetchColumnsBatch called directly |
| Ask Forge assistant | tableEnrichments from RAG context builder (Lakebase + embedding retrieval) |
| Function | Location | Format | Used by |
|---|---|---|---|
buildSchemaMarkdown(tables, columns) |
lib/queries/metadata.ts |
### catalog.schema.table -- comment - columnName (TYPE) -- comment |
Pipeline SQL gen, use case gen, metadata extraction |
buildForeignKeyMarkdown(fks) |
lib/queries/metadata.ts |
- table.col -> ref_table.ref_col |
Pipeline SQL gen |
buildCompactColumnsBlock(metadata) |
lib/genie/schema-allowlist.ts |
catalog.schema.table: col1, col2, col3 |
Genie passes |
buildSchemaContextBlock(metadata) |
lib/genie/schema-allowlist.ts |
**table** -- comment - \col` (TYPE) -- comment` |
Genie passes |
buildColumnSchemas(columns, fqns) |
lib/dashboard/adhoc-engine.ts |
table: col1 (TYPE), col2 (TYPE) |
Adhoc dashboard |
In the pipeline SQL generation step, schema is scoped to each use case's
tablesInvolved array. Only columns from those specific tables are injected:
for (const fqn of uc.tablesInvolved) {
const cols = columnsByTable.get(fqn) ?? [];
involvedColumns.push(...cols);
}
const schemaMarkdown = buildSchemaMarkdown(involvedTables, involvedColumns);
When sampleRowsPerTable > 0 is configured, fetchSampleData() in
lib/pipeline/sample-data.ts queries up to N rows per table and formats them
as markdown tables. Cell values are truncated to 60 chars. Tables without
SELECT permission are skipped (metadata-only fallback).
The schema is injected via the {directly_involved_schema} template variable.
Every SQL generation prompt includes a critical instruction block:
CRITICAL: The tables and columns listed above are the ONLY ones available. Do NOT invent, guess, or hallucinate any table or column names. If a column does not appear above, it does not exist.
| Rule set | Length | Used by |
|---|---|---|
DATABRICKS_SQL_RULES |
Full (~40 lines) | Pipeline SQL gen, pipeline SQL fix, Ask Forge assistant, dashboard design |
DATABRICKS_SQL_RULES_COMPACT |
Compact (~10 lines) | Genie semantic expressions, benchmark generation, trusted assets |
- No MEDIAN() -- use
PERCENTILE_APPROX(col, 0.5) - No nested window functions -- compute in CTE first, then aggregate
- DECIMAL(18,2) for financial/monetary calculations
- Backtick-quote column names with spaces:
`Net Cash Flow` - Use names EXACTLY as in schema -- never transform to snake_case
- Three-part table names:
catalog.schema.table - No AI functions in metric views -- non-deterministic and expensive
- COLLATE UTF8_LCASE for case-insensitive comparisons
- Filter early, aggregate late
This module is the single source of truth for column-reference extraction and validation. It consolidates logic that was previously duplicated between the pipeline validator and the Genie schema-allowlist.
Extracts all alias.column references from SQL, both plain identifiers and
backtick-quoted identifiers:
- Plain:
alias.column_namevia/\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b/g - Quoted:
alias.`column with spaces`via/\b([a-zA-Z_]\w*)\.([^]+)/g`
Returns { prefix, column, position, isQuoted } for each reference.
Detects all SQL aliases from the backtick-stripped SQL:
- Table aliases:
FROM/JOIN <fqn> [AS] <alias> - Column aliases:
AS <alias> - CTE names:
WITH cte AS (and chained, cte AS (
Returns { tableAliases, columnAliases, cteNames, all } (all as Set<string>).
Core validation function. Extracts column references, filters out:
- SQL keywords (via
SQL_KEYWORDS) - FQN parts (catalog, schema, table name fragments from
options.tablesInvolved) - Known aliases and CTE names
- AI function return fields (when
options.allowAiFunctionFieldsis true)
Then flags any column that does not appear in knownColumns.
Returns { valid: boolean, unknownColumns: string[], warnings: string[] }.
Set<string> containing "result", "errormessage", "parsed_result", and
"ai_result" (lowercased). These are runtime struct fields and common aliases
used with ai_query() when failOnError => false. They are not table columns
and must be allowlisted to avoid false-positive hallucination flags.
The parsed_result and ai_result entries support the recommended
from_json() pattern for structured AI output (see section 9).
Re-exported from lib/genie/schema-allowlist.ts. Canonical set of ~200 SQL
keywords used to avoid false positives (e.g. t.count where count is a
SQL keyword, not a column name).
| # | Surface | Prompt grounding | Static column check | EXPLAIN | Fix cycle | LLM Review | Rejection |
|---|---|---|---|---|---|---|---|
| 1 | Pipeline SQL gen | Yes | Yes | Yes | Yes (LLM) | Yes (opt-in) | Return null |
| 2 | Ask Forge assistant | Yes | Yes | Yes | Yes (LLM) | Yes (opt-in) | Keep original |
| 3 | Dashboard engine | Yes | Yes | No | No | Yes (review+fix) | Drop dataset |
| 4 | Ad-hoc dashboard | Yes | Yes | No | No | Yes (review+fix) | Drop dataset |
| 5 | Genie: semantic expressions | Yes | Yes | No | No | Yes (batch) | Filter out |
| 6 | Genie: benchmarks | Yes | Yes | No | No | Yes (review+fix) | Filter out |
| 7 | Genie: trusted assets | Yes | Yes | No | No | Yes (review+fix) | Filter out |
| 8 | Genie: example queries | Yes | Yes | No | No | No | Filter out |
| 9 | Genie: join inference | Yes | Yes | No | No | No | Filter out |
| 10 | Genie: metric views | Yes | Yes (YAML) | Yes (DDL) | Yes (LLM) | No | Drop proposal |
| 11 | Fabric: DAX-to-SQL | No | No | No | No | Yes (batch) | Confidence downgrade |
| 12 | Genie: optimize | No | No | No | No | Yes (review) | Reject suggestion |
- SQL starts with a recognised keyword (
--,WITH,SELECT,CREATE,FROM) - At least one expected table from
tablesInvolvedappears in the SQL
Uses the shared module (validateColumnReferences or findInvalidIdentifiers
via validateSqlExpression) to check that every alias.column reference
exists in the known schema. Catches:
- Completely hallucinated column names
- Column names with typos or incorrect casing
- Backtick-quoted column names with spaces that don't exist
- AI function return fields are allowlisted to avoid false positives
EXPLAIN <sql> against the SQL Warehouse catches runtime errors that static
analysis cannot detect:
- Type mismatches (
CASTerrors) - Ambiguous column references (columns in multiple joined tables)
- Missing tables (tables that exist but the user has no access to)
- Invalid function usage
Limitation: Pipe syntax (|>) is valid Databricks SQL but not supported
by EXPLAIN. Queries using pipe syntax skip this tier.
When validation fails, the SQL and error message are sent back to the LLM with the correct schema for a single repair attempt. See section 7.
If the fix attempt also fails, the SQL is rejected:
- Pipeline:
generateSqlForUseCase()returnsnull - Dashboard: dataset is dropped from the design before assembly
- Genie: expression/query is filtered out of the pass output
- Assistant: original SQL is kept in the response (best-effort)
Prompt: USE_CASE_SQL_FIX_PROMPT in lib/ai/templates.ts
Flow:
validateSqlOutput()detects unknown columnsbuildColumnViolationMessage()constructs a detailed error listing the hallucinated columns and the valid columns per tableattemptSqlFix()sends the original SQL + error + schema to the LLM- The fix response is cleaned and re-validated
- If unknown columns remain, the SQL is rejected
- If the fix passes static validation, it's checked with EXPLAIN
- If EXPLAIN fails, the SQL is rejected
Key prompt rules:
- Fix errors only -- do not change business logic UNLESS the error is a hallucinated column
- Removing references to non-existent columns is a valid fix -- do NOT substitute with guessed alternatives
- Use column names EXACTLY as in the schema
DATABRICKS_SQL_RULESis appended for full SQL quality guidance
Prompt: buildSqlFixPrompt() in lib/assistant/sql-proposer.ts
Flow:
- Static pre-check via
validateColumnReferences()catches hallucinations before EXPLAIN round-trip - If static check passes,
validateSql()runs EXPLAIN - If either fails with a column error,
buildSqlFixPrompt()constructs a fix prompt with the exact error and correct column schema - Single fix attempt via
chatCompletion() - Fixed SQL is rechecked with EXPLAIN
- If the fix passes, the original SQL block in the response is replaced
Prompt: inline repair prompt in lib/genie/passes/metric-view-proposals.ts
Flow:
validateMetricViewYaml()checks column references in the YAML- If invalid,
repairProposal()sends the YAML + validation issues + schema back to the LLM - DDL is generated and validated with
executeSQL()(dry run)
| Surface | Validation function | Known-columns source | Fix cycle |
|---|---|---|---|
| Pipeline SQL gen | validateColumnReferences |
ColumnInfo[] from metadata |
Yes |
| Ask Forge | validateColumnReferences + validateSql |
tableEnrichments |
Yes |
| Dashboard engine | validateSqlExpression |
SchemaAllowlist from MetadataSnapshot |
No |
| Ad-hoc dashboard | validateSqlExpression |
SchemaAllowlist from fetched metadata |
No |
| Genie passes (5) | validateSqlExpression |
SchemaAllowlist from MetadataSnapshot |
No |
| Genie metric views | validateMetricViewYaml |
Column references in YAML | Yes |
Choose the metadata source that fits your consumer:
// Option A: Full MetadataSnapshot (from pipeline or Genie context)
const { metadata } = ctx;
// Option B: Fetch specific tables
const [tableInfos, columns] = await Promise.all([
fetchTableInfoBatch(tables),
fetchColumnsBatch(tables),
]);
// Option C: From assistant context
const knownColumns = new Set(
context.tableEnrichments.flatMap((t) =>
t.columns.map((c) => c.name.toLowerCase())
)
);// For validateColumnReferences (flat column set)
const knownColumns = new Set(
columns.map((c) => c.columnName.toLowerCase())
);
// For validateSqlExpression (structured allowlist)
import { buildSchemaAllowlist, validateSqlExpression } from "@/lib/genie/schema-allowlist";
const allowlist = buildSchemaAllowlist(metadata);// Option A: Direct column validation (pipeline / assistant pattern)
import { validateColumnReferences } from "@/lib/validation/sql-columns";
const result = validateColumnReferences(sql, knownColumns, {
tablesInvolved: ["catalog.schema.table"],
allowAiFunctionFields: true,
});
if (!result.valid) {
// result.unknownColumns contains the hallucinated columns
}
// Option B: Schema allowlist validation (Genie / dashboard pattern)
const isValid = validateSqlExpression(allowlist, sql, "context:name", true);
if (!isValid) {
// SQL was logged and should be filtered out
}// Filter pattern (dashboards / Genie)
const validDatasets = datasets.filter((ds) =>
validateSqlExpression(allowlist, ds.sql, `my-consumer:${ds.name}`, true)
);
// Fix pattern (pipeline / assistant)
if (result.unknownColumns.length > 0) {
const fixedSql = await attemptFix(sql, result, schema);
if (!fixedSql) return null; // reject
}ai_query() with failOnError => false returns
STRUCT<result: STRING, errorMessage: STRING>. The result field is always
STRING regardless of the responseFormat parameter. Attempting to access
nested struct fields directly (e.g. ai_result.result.field) causes
INVALID_EXTRACT_BASE_FIELD_TYPE errors.
Correct pattern for structured output:
- Include JSON format instructions in the prompt text via CONCAT
- Call
ai_query(endpoint, prompt, modelParameters => ..., failOnError => false) AS ai_result - Parse:
from_json(ai_result.result, 'STRUCT<field1: TYPE, ...>') AS parsed_result - Access:
parsed_result.field1,parsed_result.field2; errors:ai_result.errorMessage
Do NOT use responseFormat with failOnError => false -- it does not
change the result field from STRING to a nested struct.
The shared validation module allowlists result, errormessage,
parsed_result, and ai_result via AI_FUNCTION_RETURN_FIELDS to avoid
false-positive hallucination flags on these runtime aliases. If new AI
functions or parsing patterns introduce additional aliases, update this set.
Both the shared module and the Genie validator handle backtick-quoted columns
(e.g. alias.`Age Group`). The shared module extracts these via a
dedicated regex pass that runs on the original SQL before backtick stripping.
When a CTE defines a computed column (e.g. SUM(amount) AS total_revenue),
total_revenue is a valid column reference in subsequent CTEs but does NOT
exist in the source schema. The validator treats it as a known alias (detected
by the AS <alias> regex) and does not flag it.
- Pipe syntax (
|>) is valid Databricks SQL butEXPLAINdoes not support it. Queries using pipe syntax skip EXPLAIN validation. - Complex CTEs: EXPLAIN may not catch all column errors in deeply nested CTEs where column resolution depends on runtime evaluation.
- Permissions: EXPLAIN requires SELECT access to the referenced tables. Tables the user cannot access will fail EXPLAIN even if the SQL is correct.
If tablesInvolved references a table that has no entries in
metadata.columns (e.g. due to permissions or information_schema gaps), the
validator logs a warning and skips column validation for that use case. The
schema markdown shows (schema not available).
buildSchemaMarkdown emits all columns for all tables with no cap. For very
wide tables (hundreds of columns), this can consume significant prompt tokens.
The trade-off is intentional: hiding columns caused hallucinations. If token
limits become an issue, the solution is to reduce the number of tables per
use case, not to cap columns.
Unity Catalog information_schema
│
▼
listTables / listColumns / listForeignKeys / listMetricViews
│
▼
MetadataSnapshot { tables, columns, foreignKeys, metricViews }
│
├──▶ Pipeline context (full snapshot)
│ └──▶ per-use-case scoping (tablesInvolved)
│ └──▶ buildSchemaMarkdown → prompt injection
│
├──▶ Genie engine (full snapshot)
│ └──▶ buildSchemaAllowlist → SchemaAllowlist
│ └──▶ buildSchemaContextBlock → prompt injection
│
├──▶ Dashboard engine (full snapshot or fetched)
│ └──▶ buildSchemaAllowlist → SchemaAllowlist
│
└──▶ Assistant (tableEnrichments from RAG)
└──▶ Set<string> of column names
LLM generates SQL
│
▼
[Tier 1] Structural checks
(starts with SELECT/WITH, references expected tables)
│
▼
[Tier 2] Static column validation
(extractColumnReferences → check against knownColumns)
│
├── valid ──▶ [Tier 3] EXPLAIN dry-run
│ │
│ ├── passes ──▶ ✅ Accept SQL
│ │
│ └── fails ──▶ [Tier 4] Fix cycle
│ │
│ ├── fix passes ──▶ ✅ Accept fixed SQL
│ │
│ └── fix fails ──▶ ❌ Reject
│
└── invalid ──▶ [Tier 4] Fix cycle
│
├── fix passes + re-validates ──▶ ✅ Accept
│
└── fix fails ──▶ ❌ Reject
lib/validation/sql-columns.ts (shared module)
┌─────────────────────────────────────────────┐
│ extractColumnReferences (backtick-aware) │
│ extractSqlAliases (table/col/CTE aliases) │
│ validateColumnReferences (core validator) │
│ AI_FUNCTION_RETURN_FIELDS (allowlist) │
│ SQL_KEYWORDS (re-exported) │
└──────────┬──────────────────────┬────────────┘
│ │
▼ ▼
Pipeline sql-generation.ts Genie schema-allowlist.ts
validateSqlOutput() findInvalidIdentifiers()
+ fix cycle validateSqlExpression()
│ │
│ ┌──────────┼──────────────────┐
│ │ │ │
│ ▼ ▼ ▼
│ Dashboard Genie passes (5) Deploy validation
│ engine semantic-expressions join-diagnostics
│ adhoc-engine benchmark-generation
│ trusted-assets
│ example-query-generation
│ join-inference
│
▼
Assistant engine.ts
validateColumnReferences()
+ validateSql (EXPLAIN)
+ fix cycle