The AskSQL engine: schema catalog, AST-based read-only SQL guard, the natural-language-to-SQL pipeline, and the model provider resolver. No database drivers and no UI; those live in the adapter packages.
npm i @asksql/core @asksql/postgres pg @ai-sdk/groq
explainSchema(question) answers schema questions in plain language, grounded in
the real catalog: what tables exist, how they relate, and advisory suggestions -
add a column, create an index, improve performance - returned as DDL proposals
it never executes. Off-topic questions are declined rather than answered from
general knowledge. Answers are checked against the catalog; invented table or
column names are rejected and retried.
import { createAskSql, resolveModel, AskSqlError } from '@asksql/core';
import { PostgresConnector } from '@asksql/postgres';
const model = await resolveModel({
provider: 'groq', // openai | anthropic | google | azure | groq | nvidia | ollama | openai-compatible
model: 'openai/gpt-oss-20b', // an example: use whatever your provider lists at /models
apiKey: process.env.GROQ_API_KEY,
});
const engine = createAskSql({
connectors: [new PostgresConnector({ id: 'main', name: 'Main', connectionString: process.env.DATABASE_URL })],
model,
});
const answer = await engine.ask('How many customers signed up this month?');
console.log(answer.sql); // review before running
const result = await answer.run(); // guarded, read-only execution
console.table(result.rows);
By default the model receives only your schema and the question, never your data. Every generated
statement passes a deterministic AST guard before it can run. Writes, DDL and stacked statements are
refused, and a row LIMIT is injected automatically.
Failures throw an AskSqlError with a stable code and a safe, plain-language userMessage
(internal detail stays in detail, out of toJSON()):
try {
await engine.ask('…');
} catch (err) {
if (AskSqlError.is(err)) {
// e.g. LLM_AUTH, LLM_UNREACHABLE, GUARD_BLOCKED, CONFIG_ERROR, DB_QUERY_ERROR
console.error(err.code, err.userMessage);
} else {
throw err;
}
}
resolveModel(...) returns a model the engine can use. Official API hosts are pre-seeded per provider,
so baseURL is only needed to override a default (an OpenAI-compatible gateway, or a non-local Ollama).
PROVIDER_API_HOST exposes the defaults.
Full documentation: https://github.com/rahulmahadik/AskSQL
API reference: rahulmahadik.github.io/AskSQL
MongoDB is non-SQL: import createMongoAskSql from @asksql/core/mongo with the
@asksql/mongodb connector - the same ask/guard/run flow, answering with a guarded
aggregation pipeline instead of SQL.
model also accepts a plain async function
(({ system, prompt, signal }) => string | AsyncIterable<string>) - an escape hatch
for custom gateways with no AI SDK involved.
Beyond ask -> approve -> run, all optional:
engine.explain(sql) describes what a statement
does, grounded in the schema.config.onEvent (and per-ask onEvent) emits stage + token events
across the pipeline (catalog, prune, prompt, llm, extract, guard, repair, execute,
done) for live UIs.AbortSignal to any ask/run/explain and Postgres/MySQL cancel the
running query at the database.GROUP BY, an aggregate nested
inside another (AVG(SUM(x))), and a one-to-many join that inflates a SUM.config.history records every attempt (status, duration), backed by an
in-memory store.config.pruner) before prompting.allowDataInPrompt (default off) is the
opt-in for cell values, and it now gates three channels, not one: sampled column values, stripped
from the catalog so a connector that samples cannot leak them into any prompt (the first prompt, a
repair, explain, or explainSchema); the key NAMES inside a JSON column, where the default states
how many recur but not which, because a map with a stable key set is structurally identical to a
record; and the distinct values named in a coded-column repair, where the default attaches a caveat
for the reader instead. The MongoDB engine takes the same option for the values its document
sampling infers. Declared enum labels come from the schema and are kept either way. Query results
are never sent on any path.Prompts, model sampling, guard policy, and grounding (glossary, few-shots) are configurable without forking; see docs/configuration.md.