Exception Reference¶
IntentQL uses a structured exception hierarchy. All exceptions inherit from DSLCompilerError, making it easy to catch all library errors in one place or handle each type separately.
from intentql import (
DSLCompilerError,
SchemaError,
QueryPlanError,
AmbiguousColumnError,
DatabaseExecutionError,
QueryCostError,
)
Hierarchy¶
Exception
└── DSLCompilerError
├── SchemaError ← bad schema.yaml configuration
├── QueryPlanError ← invalid or unresolvable query plan
│ └── AmbiguousColumnError ← unqualified column matches multiple tables
├── DatabaseExecutionError ← valid SQL, but Postgres rejected it
└── QueryCostError ← reserved for future cost-guard use
DSLCompilerError¶
Base class for all IntentQL exceptions. Catch this to handle any library error in one place:
try:
result = intentql.execute_query_plan(...)
except intentql.DSLCompilerError as e:
print(f"IntentQL error: {e}")
SchemaError¶
Raised when schema.yaml is missing, malformed, or contains invalid configuration.
When you'll see it:
schema.yamldoes not exist or is not valid YAML- A table entry is missing
name,db_table, orcolumns - A column entry is missing
nameordb_column - A link references a table not present in
tables - A link's
"on"field was parsed as a boolean (YAML interprets bareon:astrue)
try:
schema = intentql.load_and_validate_schema("config/schema.yaml")
except intentql.SchemaError as e:
print(f"Fix your schema.yaml: {e}")
Common YAML pitfall: bare on: key
YAML parses on as boolean true unless quoted. Always quote it:
QueryPlanError¶
Raised when a QueryPlan is structurally or semantically invalid. This is the most common exception during development.
execute_query_plan and the low-level Compiler now raise the same public intentql.exceptions.QueryPlanError class, so application code can catch one type consistently.
Constructor:
QueryPlanError(
message: str,
*,
code: str = "INVALID_PLAN",
path: str = "$",
suggestion: str | None = None,
validation_errors: list | None = None,
)
Attributes:
| Attribute | Type | Description |
|---|---|---|
message |
str |
Human-readable error description |
code |
str |
Machine-readable error code |
path |
str |
JSONPath error location, e.g. "$.filters[0].field" |
suggestion |
str \| None |
Suggested fix (when available) |
validation_errors |
list \| None |
Structured validation error list |
Method:
error.to_dict()
# {"code": "...", "message": "...", "path": "...", "suggestion": "...", "validation_errors": [...]}
When you'll see it:
datasetreferences a table not inschema.yaml- A
fieldreferences a column not inschema.yaml - A
rollup.metrics[*].fieldreferences a raw column instead of an inner metric alias - Metric aliases are not unique
- Unknown fields are present in the plan (
extra="forbid") aggrequires a field but none was provided
try:
sql, params = Compiler(schema).compile({"dataset": "nonexistent", ...})
except intentql.QueryPlanError as e:
print(e.code) # "INVALID_PLAN"
print(e.path) # "$.dataset"
print(e.message) # "Unknown dataset: nonexistent"
Designed for LLM retry feedback
QueryPlanError is serializable by design. When using plan_with_retry, IntentQL feeds the error dict back to the LLM automatically. In a custom retry loop:
AmbiguousColumnError¶
A subclass of QueryPlanError raised when an unqualified column reference matches columns in multiple joined tables.
Constructor:
Attributes:
| Attribute | Type | Description |
|---|---|---|
column |
str |
The ambiguous column name |
tables |
list[str] |
All tables that contain this column |
Example: When orders is joined with customers and both have customer_id:
This raises AmbiguousColumnError. Fix: qualify the reference:
DatabaseExecutionError¶
Raised when a valid, compiled query fails at the Postgres level.
Constructor:
DatabaseExecutionError(
message: str,
*,
sql: str | None = None,
original: Exception | None = None,
)
Attributes:
| Attribute | Type | Description |
|---|---|---|
sql |
str \| None |
The SQL that triggered the error |
original |
Exception \| None |
The underlying psycopg2/SQLAlchemy exception |
When you'll see it:
- Statement timeout exceeded (
statement_timeout_ms) - Postgres connection failure
- Permission denied on a table
- Type mismatch between a filter value and the column type
try:
result = intentql.execute_query_plan(engine=engine, ..., raise_on_error=True)
except intentql.DatabaseExecutionError as e:
print(f"DB error: {e.message}")
print(f"SQL: {e.sql}")
print(f"Caused by: {e.original}")
QueryCostError¶
Raised when a query exceeds a configured complexity threshold before execution.
Constructor:
Reserved for future use
QueryCostError is defined in the exception hierarchy but is not currently raised by compiler internals. It is reserved for a future cost-based query guard feature.
Production Error Handling Pattern¶
Recommended pattern for a web API endpoint using raise_on_error=True:
import intentql
def handle_question(question: str) -> dict:
try:
plan = planner.plan_with_retry(question)
return intentql.execute_query_plan(
engine=engine,
schema_path="config/schema.yaml",
query_plan=plan,
raise_on_error=True,
)
except intentql.QueryPlanError as e:
# LLM generated an invalid plan; all retries exhausted
# Safe to surface the error code to the client
return {"error": "invalid_query", "detail": e.message, "code": e.code}
except intentql.DatabaseExecutionError as e:
# Valid SQL but DB rejected it (timeout, permissions, type error)
# Log e.sql for debugging; do NOT surface raw SQL to end users
logger.error("DB execution failed", sql=e.sql, cause=str(e.original))
return {"error": "execution_failed", "detail": "Query failed. Please try again."}
except intentql.SchemaError as e:
# Configuration error — should never reach production if schema is tested at startup
logger.critical("Schema configuration error", error=str(e))
return {"error": "configuration_error", "detail": "Internal error"}