nl2sql

LogicalValidatorNode

Overview


Responsibilities

Column resolution is delegated to sqlglot’s optimizer. The node converts the plan into a throw‑away sqlglot expression tree (reusing the generator’s SqlVisitor) and runs sqlglot.optimizer.qualify.qualify() against a schema built from relevant_tables. The LLM contract is unchanged: the planner still emits a PlanModel, never SQL.


Position in Execution Graph

Upstream:

Downstream:

Trigger conditions:

flowchart LR
    Planner[ASTPlannerNode] --> Validator[LogicalValidatorNode] --> Generator[GeneratorNode]
    Validator --> RetryHandler[retry_handler]

Inputs

From SubgraphExecutionState:

Validation performed:


Outputs

Mutations to SubgraphExecutionState:

Side effects:


Internal Flow (Step-by-Step)

  1. If plan is missing, emit MISSING_PLAN and stop.
  2. Run _validate_static() for structural checks:
    • _resolve_plan_tables() maps each plan alias to its schema columns and emits TABLE_NOT_FOUND for unknown tables. This stays hand-written because qualify() silently ignores relations absent from its schema.
    • _validate_columns() builds the plan’s sqlglot tree and runs qualify(..., validate_qualify_columns=True). On failure each distinct column reference is re-probed so every bad reference is reported, and _describe_column_failure() rewrites the optimizer’s SQL-oriented text into plan-oriented feedback the refiner can act on.
  3. Run _validate_policy() for RBAC enforcement. This always runs, even when static validation failed or raised — the security check is never skipped.
  4. If any errors are ERROR/CRITICAL, return with errors.
  5. Otherwise return success reasoning.
  6. On exception, emit VALIDATOR_CRASH.

Contracts & Interfaces

Implements a LangGraph node callable:

def __call__(self, state: SubgraphExecutionState) -> Dict[str, Any]

Key contracts:


Determinism Guarantees


Error Handling

Emits PipelineError with:


Retry + Idempotency


Performance Characteristics


Observability


Configuration


Extension Points


Known Limitations