Skip to content

@askdb/core API reference

Reference · @askdb/core

@askdb/core is the dialect-agnostic NL-to-SQL pipeline. It owns schema loading, prompt construction, SQL validation, and tenant-scope enforcement. Everything else in the AskDB ecosystem composes it.

The main pipeline function. Accepts a natural-language question and a loaded schema; returns validated SQL.

import { ask } from "@askdb/core";
const { sql } = await ask({
question: "How many users signed up last week?",
schema, // from loadSchema()
model, // AI SDK LanguageModel
dialect: "postgres",
});
FieldTypeRequiredDefaultDescription
questionstringyesThe natural-language question.
schemaAnyNormalizedSchemayesLoaded schema from loadSchema() or loadSchemaFromJson().
modelAskDbLanguageModelyesAn AI SDK LanguageModel instance (e.g. openai("gpt-4o-mini")).
dialectAskDialectInputyesA built-in dialect id, DialectSpec, or custom AskDialect. See Dialects.
modeAskDbModeV1no"schema_only"Trust boundary. See Modes.
explainbooleannofalseWhen true, populates result.explain with heuristic guardrail metadata.
omitSensitiveIdentifiersFromNlToSqlPromptbooleannofalseWhen true, strips sensitive table/column names from the NL→SQL DDL entirely. Default (false) includes them with a (sensitive) tag so the model can still ground queries.
tenantScopeTenantScopenoRequired when the schema has a tenant-policy.md. The pipeline fails closed if a policy exists and this is absent.
tenantSqlModeTenantSqlOutputModeno"sql-only""sql-only" inlines literal tenant values; "sql-params" converts them to positional $N parameters.
parameterizebooleannotrueAsk the model for unbound SQL + a parameter manifest. Still exactly one model call. Set false to save output tokens. Inert for custom AskDialect implementations.
retrieverRetrievernoRAG retriever from @askdb/rag. When supplied and schema chunk count exceeds retrievalThresholdChunks, retrieval replaces the full DDL in the prompt.
retrievalKnumberno8Top-k passed to the retriever.
retrievalThresholdChunksnumberno30Chunk count below which the full DDL is used even if a retriever is supplied.
totalSchemaChunkCountnumbernoInfinityTotal chunk count from buildSchemaIndex. Pass result.stats.chunksTotal here for the threshold check to be meaningful; omit to always use the retriever when one is supplied.
loggerAskDbLoggernoStructured logger. Attach one for observability — it emits AskDbLogEvent entries at each pipeline stage.
depsAskGenerateDepsno{ generateText? } — inject a mock generateText for unit tests.
FieldTypeWhen present
sqlstringAlways. The model’s bound, validated SQL. Never overwritten by rebind failures.
unboundSqlstring | undefinedWhen parameterize is on and the model returned a consistent unbound block + manifest. Driver markers ($N / ? / @pN).
paramsQueryParameterValue[] | undefinedPositional values for unboundSql. Prefer this over tenantParams when present.
parametersQueryParameterBinding[] | undefinedNamed bindings for form UIs (includes runtime value, markers, indices).
preparedQueryPreparedQuery | undefinedDefinitions + named template only (no runtime values). Input to bindPreparedQuery().
usageAskUsage | undefinedWhen the model provider reports token counts.
explainunknown | undefinedWhen explain: true. Heuristic guardrail metadata — which read-only rules fired and why.
tenantGuardrailTenantGuardrailResult | undefinedWhen a tenant policy is active. Confirms the generated SQL references tenant-scoped tables correctly.
tenantParamsunknown[] | undefinedWhen tenantSqlMode: "sql-params" and the SQL has tenant placeholders. Tenant-only positional values.
tenantBindingsTenantBinding[] | undefinedWhen a tenant policy is active. Structured binding metadata for audit.

Pure, synchronous local rebind — no model call. Returns both a ready-to-run sql and unboundSql + params for driver binding. Checks names, types, and cardinality only; does not authorize tenant IDs.

import { bindPreparedQuery } from "@askdb/core";
const rebound = bindPreparedQuery(result.preparedQuery!, {
state_name: "Utah",
":tenant_agency_ids": authorizedAgencyIds,
});
await pool.query(rebound.sql);
// or: await pool.query(rebound.unboundSql, rebound.params);

List parameters are arity-stable in unboundSql only on PostgreSQL/CockroachDB (= ANY($n)). Elsewhere a changed list length changes the marker count — rebind rather than swapping the array. Map SQL Server values via parameters[].markers (@p0-based).

type AskUsage = {
promptTokens: number | null;
completionTokens: number | null;
totalTokens: number | null;
};

Populated from the model provider’s usage report. Use for cost tracking or quota enforcement. All fields are null when the provider does not report that counter.


import { loadSchema } from "@askdb/core";
const schema = loadSchema("./my-app.schema"); // synchronous

Autodetects and loads a v2 schema artifact. Accepts:

  • A schema directory (<name>.schema/) — the output of askdb introspect
  • A bundled JSON file (*.bundle.json or any JSON with bundled: true) — the output of askdb bundle
  • A direct path to a schema.json file

Returns NormalizedSchemaV2. Throws SchemaParseError on failure.

loadSchema is synchronous — load once at startup and keep the result in memory, not per request.

import { loadSchemaFromJson } from "@askdb/core";
const schema = loadSchemaFromJson(bundleJsonString);

Parses a raw JSON string. Accepts the same formats as loadSchema (bundled JSON or bare schema.json blob). Useful when the schema is passed as a string over a network boundary (e.g., the HTTP API’s schemaJson request field).


The dialect option accepts three forms:

dialect: "postgres" // or "mysql" | "sqlite" | "sqlserver" | "cockroachdb" | "mariadb"
IdEngine
"postgres"PostgreSQL — " quotes, ILIKE, ::type cast, NOW(), || concat
"cockroachdb"CockroachDB — PostgreSQL-wire-compatible; reuses the Postgres prompt
"mysql"MySQL — ` quotes, CONCAT(), LIKE, CAST(), NOW()
"mariadb"MariaDB — MySQL-protocol-compatible; reuses the MySQL prompt
"sqlite"SQLite — dynamic typing, LIKE, date('now'), || concat, LIMIT
"sqlserver"SQL Server (T-SQL) — [bracket] or " quotes, GETDATE(), +/CONCAT(), CAST()/CONVERT()

The engine adapter packages (@askdb/postgres, @askdb/mysql, etc.) export convenience dialect constants (POSTGRES_DIALECT, MYSQL_DIALECT, …), but passing the string id to ask() is sufficient — you don’t need to install the engine adapter just for SQL generation.

Customize how one of the built-in dialects prompts and validates — for example, a PostgreSQL-compatible warehouse that needs extra guidance in the prompt or extra forbidden keywords. id must be one of the six built-in ids (the spec describes a variant of that engine family, not a brand-new engine — for that, use AskDialect).

import { type DialectSpec } from "@askdb/core";
const redshiftFlavor: DialectSpec = {
id: "postgres", // the built-in family this variant belongs to
displayName: "Amazon Redshift",
promptBrief: "Amazon Redshift SQL. PostgreSQL syntax with some differences: ...",
identifierQuote: '"',
extraForbiddenKeywords: ["COPY", "UNLOAD"],
extraValidate(sql) {
// throw SqlValidationError for dialect-specific rules
},
};
FieldTypeRequiredDescription
idDialectIdyesOne of the six built-in ids — the engine family this spec belongs to.
displayNamestringyesHuman-readable name.
promptBriefstringyesOne-paragraph guidance for the model about this dialect’s syntax.
identifierQuote'"' | '’`yesIdentifier quoting style.
extraForbiddenKeywordsreadonly string[]noKeywords to forbid in addition to the shared read-only denylist.
extraValidate(sql: string) => voidnoDialect-specific SQL validator. Throw SqlValidationError for failures.

Full escape hatch for custom NL→SQL generators — agentic flows, tool-calling, fine-tuned models, non-SELECT targets. 95 % of consumers should use a built-in id or DialectSpec instead.

import { type AskDialect } from "@askdb/core";
const myDialect: AskDialect = {
async generate(question, schema, model, options) {
// call your own generation logic
return { sql: "SELECT ...", usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120 } };
},
};

The generate method receives:

  • question: string
  • schema: AnyNormalizedSchema
  • model: AskDbLanguageModel
  • options?: AskDialectGenerateOptions{ logger, explain, omitSensitiveIdentifiersFromNlToSqlPrompt, generateText, prebuiltDdl, tenantPolicy, tenantScope }

It must return AskDialectGenerateResult:

FieldTypeDescription
sqlstringThe generated SQL.
explainunknown | undefinedGuardrail metadata when options.explain is true.
tenantGuardrailTenantGuardrailResult | undefinedTenant validation result, if your generator runs it.
usageAskUsage | undefinedToken usage, if your generator has access to it.

AskDbModeV1 controls which post-generation guardrails are active.

ValueDescription
"schema_only" (default)Schema-grounding only. The model sees the DDL; no post-execute paths are activated. Use this for all production read-only queries.
"bounded_results"Reserved for a future post-execute extension (summarizing bounded tabular results through the model). Today it produces exactly the same SQL as schema_only — see Modes and dialects.
import { DEFAULT_ASKDB_MODE, parseAskDbModeV1 } from "@askdb/core";
const mode = parseAskDbModeV1(process.env.ASKDB_MODE); // throws on invalid values

import { createAskDbLogger } from "@askdb/core";
const logger = createAskDbLogger({
correlationId: requestId,
level: "info",
logFile: "./logs/askdb.json",
});

Returns an AskDbLogger that emits structured JSON for each pipeline stage. Pass it to ask() via options.logger.

OptionTypeRequiredDefaultDescription
correlationIdstringyesAttached to every log line. Pass the request id.
levelAskDbLogLevelno"silent"Pino log level. "silent" suppresses all output.
logFilestringnoAppend JSON logs to this path. Parent directories are created automatically.
logStdoutbooleannofalseDuplicate logs to stdout in addition to stderr/file.

AskDbLogLevel: "fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent"

The emitted log events are typed as AskDbLogEvent constants (e.g. PipelineMode, PipelineGenerateStart, TenantScopeValidated). Import AskDbLogEvent from @askdb/core to filter logs by event kind.


These types are needed when your schema has a tenant policy. See the multi-tenancy guide for the full workflow.

  • "sql-only" — inlines literal tenant values directly in the SQL string
  • "sql-params" — converts tenant values to positional $N parameters; use with parameterized queries
type TenantBinding = {
placeholder: string; // e.g. ":tenant_org_ids"
rootLabel: string; // domain label (e.g. "organization")
rootId: string; // tenant root id
ids: string[]; // resolved id values
};

Passed as options.tenantScope to ask(). Carries enforceable access plus optional advisory context.

// IDs access — user can see records for their specific org ids
const scope: TenantScope = {
access: {
kind: "ids",
tenantRoot: "table:public.organizations", // stable table ID from the policy
ids: ["org-1", "org-2"],
},
context: { role: "regional_manager", label: "Jane Smith" },
};
// Global / super access
const adminScope: TenantScope = {
access: { kind: "global", reason: "support investigation" },
};

Access kinds:

KindDescription
"ids"User can access specific root-entity ids.
"subtree"User can access a subtree rooted at specific ids.
"multi_root"User has access across multiple tenant roots, each with their own id set.
"global"Super / admin access. Requires a reason string for audit.