Skip to content

@askdb/core API reference: checks

Reference · @askdb/core

ask() and bindPreparedQuery() run the read-only, tenant and sensitive checks for you, and report them in their verdict. These functions run one check on SQL that doesn’t come through either.

validateSensitiveReferences(sql, schema, options?)

Section titled “validateSensitiveReferences(sql, schema, options?)”

Reports the sensitive tables and columns a SQL statement references. Pure, synchronous, no model call.

sensitive: true otherwise only affects the prompt — formatSchemaForNlToSql either tags identifiers (sensitive) or, with omitSensitiveIdentifiersFromNlToSqlPrompt, withholds them. Both act on what the model sees. This function is the enforcement path: it inspects SQL that is about to execute. Hosts that cache, store, or replay generated SQL should call it on every execution path, not just at generation time.

import { validateSensitiveReferences } from "@askdb/core";
// Cache-replay path — no model in the loop.
const { passed, references } = validateSensitiveReferences(cachedSql, schema);
if (!passed) {
throw new Error(`refusing to replay: ${references.map((r) => r.column).join(", ")}`);
}

ask() runs this check on the model’s SQL and reports it in result.verdict and result.sensitiveGuardrail, and bindPreparedQuery() runs it again on every rebind. Use the standalone function for statements that don’t come through either, such as SQL you store and replay yourself.

FieldTypeMeaning
passedbooleantrue only when nothing sensitive was referenced and table scope was fully resolved.
referencesSensitiveReference[]Every sensitive identifier the statement touches.
unresolvedScopeSensitiveScopeReport | undefinedPresent when the statement’s FROM/JOIN scope could not be resolved with confidence.
type SensitiveReference = {
table: string; // table name as spelled in the schema artifact
schema?: string; // namespace, when the artifact records one
column: string; // column name, or "*" when matchKind is "table"
matchKind: "qualified" | "unqualified" | "table";
};
  • "qualified" — table.column, or alias.column where the alias binds to the owning table. Also alias.* and whole-row references such as row_to_json(alias) or to_jsonb(alias), which reach every sensitive column of that table.
  • "unqualified" — a bare column, counted only because the owning table is in the statement’s scope. Also a bare SELECT *, which reaches every sensitive column of the tables in that SELECT’s own FROM/JOIN (not a subquery’s).
  • "table" — the sensitive table itself is a FROM/JOIN target. Catches SELECT * FROM secrets, where no column is named.
OptionTypeDefaultMeaning
mode"warn" | "strict""warn"See Modes.
dialectPick<DialectSpec, "id" | "backslashEscapes">—A DialectSpec, or { id } with an optional backslashEscapes. Lex the SQL the way that engine does (see How the checks read SQL). Without it, references are collected under every built-in engine’s reading, which is conservative and can over-report in rare cases.

An unqualified name is not a substring search. FROM/JOIN targets and their aliases are resolved first — including inside CTEs and derived tables — and bare names are matched only against the columns of tables actually in scope.

This matters most with table-level sensitive, which promotes every column of that table into the match set. A schema with a sensitive audit or changelog table that has an id column would otherwise flag SELECT s.id FROM students s. It does not:

validateSensitiveReferences(
"SELECT s.id, s.first_name FROM people.students s",
schema,
); // → { passed: true, references: [] }
validateSensitiveReferences("SELECT email, password FROM identity.users", schema);
// → references: [{ table: "users", schema: "identity", column: "password", matchKind: "unqualified" }]

{ mode: "warn" } (default) returns the references without throwing. { mode: "strict" } throws SensitiveReferenceError extends AskDbError whenever passed is false, carrying .rule ("SENSITIVE_TABLE_REFERENCED" | "SENSITIVE_COLUMN_REFERENCED" | "UNRESOLVED_TABLE_SCOPE"), .references, and .unresolvedScope.

When the check can tell it couldn’t bind names to tables, it reports that in unresolvedScope rather than passing. It can’t always tell: a table in a FROM form it doesn’t recognize is skipped without a report (see The sensitive check). unresolvedScope.issues contains one or more of:

IssueCauseEffect
NO_TABLE_SOURCENo FROM/JOIN target resolved, so unqualified names cannot be bound.Unqualified matching widens to every sensitive column.
UNKNOWN_QUALIFIERA qualifier.column whose qualifier is not a known table, alias, or CTE.That column is matched against every sensitive column.
OPAQUE_TABLE_SOURCEA table source that is not a relation name (table function, table-valued expression).Reported only; no widening — such a source exposes no schema columns.
UNTERMINATED_TOKENA string, quoted identifier, or comment never closes.Reported only; the rest of the statement could not be scanned.
UNICODE_ESCAPED_IDENTIFIERA name the check can’t match as written. Either a Postgres Unicode-escaped name (U&"p\0061ssword"), which Postgres decodes, raised when the SQL may be read as Postgres: postgres, cockroachdb, a custom AskDialect, or no dialect. Or a name with a character outside printable ASCII that the schema doesn’t spell exactly ([ssn]), which SQL Server’s collation may read as another name, raised under sqlserver, a custom AskDialect, or no dialect.Reported only; the check can’t tell which column the name reads.
STRING_CONTINUATIONAn E'…' string continued on a later line, which Postgres reads with backslash escapes. Raised when the SQL may be read as Postgres.Reported only; a column after it may be hidden from the check.
ESCAPED_BYTE_STRINGA b'…' byte string (lower-case b) with a backslash, or continued on a later line, which CockroachDB reads with backslash escapes. Raised when the SQL may be read as Postgres.Reported only; a column after it may be hidden from the check.

unresolvedScope.widened says whether matching was widened, and .message explains why. strict mode treats unresolved scope as a failure: passed is false, so it throws SensitiveReferenceError (rule: "UNRESOLVED_TABLE_SCOPE" when no sensitive reference was found).

The check is heuristic, not a SQL parser — a review and enforcement aid, not a substitute for database-side column privileges.

true when the schema marks at least one table or column sensitive. Cheap pre-check for hosts that want to skip the sensitive check entirely on schemas with no markers — ask() uses it for exactly that, which is why result.sensitiveGuardrail is absent on unmarked schemas.

Renders a SensitiveReference as schema.table.column (or table.column when the artifact has no namespace). askdb ask uses it for its Warning: line, and ask() uses it for the sensitiveColumns field of the askdb.pipeline.sensitive_sql_warning log event, next to sensitiveColumnCount.

Runs the read-only check on sql under a DialectSpec’s rules. It returns the statement, trimmed, and throws SqlValidationError when it fails: SqlValidationError rules lists each rule. ask() runs it for a built-in dialect or a DialectSpec, and a custom AskDialect calls it itself.

validateTenantGuardrails(sql, policy, scope, options?)

Section titled “validateTenantGuardrails(sql, policy, scope, options?)”

Runs the tenant check on SQL that still has its :tenant_<root>_ids placeholders, and returns a TenantGuardrailResult: { passed, warnings }. Under a strict policy, a failure throws TenantGuardrailError instead. TenantGuardrailError rules lists each warning’s rule, and What the tenant check requires says what passes. options is a ValidateTenantGuardrailsOptions: dialect, a DialectSpec or its id and backslashEscapes, which ask() passes when it has one.

TypeWhat it is
ValidateSensitiveReferencesOptionsvalidateSensitiveReferences()’s options, as in Options.
SensitiveGuardrailModeIts mode: "warn" or "strict".
SensitiveMatchKindA reference’s matchKind: "qualified", "unqualified" or "table".
SensitiveScopeIssueOne entry of unresolvedScope.issues, as in Conservative failure.
TenantGuardrailResultvalidateTenantGuardrails()’s result, and ask()’s result.tenantGuardrail.
TenantGuardrailWarningOne entry of its warnings: { rule, tableId, message }.
ValidateTenantGuardrailsOptionsvalidateTenantGuardrails()’s options.