Skip to content

@askdb/core API reference: bindPreparedQuery()

Reference · @askdb/core

bindPreparedQuery() reruns a query ask() returned, with new values and no model call. It checks the stored template again under the schema and scope you pass now. Rerun a query with new values walks through it.

bindPreparedQuery(prepared, values, guardrails)

Section titled “bindPreparedQuery(prepared, values, guardrails)”

Local rebind of a stored preparedQuery with new business values, without a model call. Synchronous. It re-runs the SQL checks on the template under the schema, tenant scope and modes you pass now, then renders the template. It returns sql with the business values inlined (with a tenant scope in the default tenantSqlMode: "sql-params", run it with tenantParams; in "sql-only" the tenant IDs are inlined too), unboundSql + params for driver binding, bindings for the business parameters, and the verdict. sql and unboundSql keep the trailing ; of namedSql, if it has one; remove it before you wrap the statement (see Row limits).

import { bindPreparedQuery } from "@askdb/core";
const rebound = bindPreparedQuery(
result.preparedQuery!,
{ state_name: "Utah" },
{ schema, tenantScope }, // the same schema and the caller's scope, as for ask()
);
await pool.query(rebound.sql, rebound.tenantParams ?? []);
// or: await pool.query(rebound.unboundSql, rebound.params);

The third argument, BindGuardrails, is required:

FieldTypeRequiredDefaultDescription
schemaAnyNormalizedSchemayes—The schema to check the template under: its tenant policy and its sensitive markers.
tenantScopeTenantScopewith a tenant policy—The caller’s scope, as for ask(). Tenant IDs are rendered from it. A subtree scope must be expanded first with expandTenantScope().
sensitiveGuardrailMode"warn" | "strict" | "off"no"warn"As for ask(): "warn" binds and reports sensitive references in verdict; "strict" refuses them.
acceptWarningsReadonlyArray<"tenant">no—With a policy whose enforcement is warn, a template that fails the tenant check is refused unless this includes "tenant".
tenantSqlModeTenantSqlOutputModeno"sql-params"As for ask(): "sql-params" puts tenant markers in sql and the IDs in tenantParams, with the markers of the template’s built-in dialect (a rebind always has one, so ask()’s custom-dialect rule never applies); "sql-only" inlines them as escaped literals and refuses a tenant ID that isn’t printable ASCII or contains a backslash. Any other value throws TenantScopeError (TENANT_SQL_MODE_REQUIRED).
dialectDialectSpec | BuiltInDialectIdfor a "host" template, or one stored without dialectRulesthe built-in specThe dialect rules to check and render under: the DialectSpec you passed to ask(), or a built-in id. Its id must be the template’s dialect.

Which dialect rules a rebind uses. ask() records them in preparedQuery.dialectRules: "builtin" when it ran with a built-in id or the exported constant (POSTGRES_DIALECT and the others), "host" for any other DialectSpec object, such as { ...POSTGRES_DIALECT, backslashEscapes: true }. A "builtin" template rebinds under the built-in spec unless you pass dialect. A "host" template needs the same spec as dialect, so your backslashEscapes, blockedFunctions, extraForbiddenKeywords and extraValidate apply at rebind too. A template stored without dialectRules, before 1.0.0-beta.46, needs dialect as well: pass the spec ask() ran with, or the built-in id, such as "postgres", if it ran with the built-in rules. If your app loads two copies of @askdb/core, such as ESM beside CJS, a constant from the other copy is a different object, so its templates are "host": pass dialect (ADR 0032).

Tenant IDs come from tenantScope, never from values. Each :tenant_<root>_ids placeholder is filled in both forms, as ask() fills it. In sql, it becomes driver markers numbered from the first slot, with the IDs in tenantParams (the default, "sql-params"), or escaped literals in "sql-only" (col = :tenant_agency_ids becomes col IN ('7', '8') with several IDs). In unboundSql, it becomes driver markers after the business markers, with the IDs in params (appended for $N / @pN, interleaved in source order for ?, and bindings[].indices remapped). Run each form with its own array, never the two concatenated.

Every check uses the bind-time inputs, so a template refused today may have bound yesterday: a tightened policy, or a scope other than the one it was produced under, applies at the next rebind. It throws:

  • QueryParameterError (MISSING_GUARDRAIL_CONTEXT) without the third argument or its schema; (DIALECT_SPEC_REQUIRED) without dialect for a "host" template or one stored without dialectRules; (DIALECT_UNSUPPORTED) for a dialect whose id isn’t the template’s; (TENANT_VALUE_SUPPLIED) for a :tenant_* key in values; (TENANT_PARAM_ALIGNMENT) when the tenant markers can’t be lined up with the business params; (INVALID_DECLARATION) for a parameter declaring a type or cardinality AskDB doesn’t support; and the existing reasons for a wrong, missing or misplaced business value.
  • TenantScopeError: as in ask() for a missing or invalid scope; SUBTREE_NOT_RESOLVABLE for an unexpanded subtree; UNRESOLVED_TENANT_PLACEHOLDER for a placeholder the scope has no IDs for. That includes a template scoped through another root, any placeholder under a global scope, and any placeholder when the schema has no tenant policy. UNESCAPABLE_TENANT_ID in "sql-only" mode for a tenant ID that isn’t printable ASCII or contains a backslash. TENANT_SQL_MODE_REQUIRED for a tenantSqlMode that is neither mode.
  • SqlValidationError, TenantGuardrailError or SensitiveReferenceError for a deny verdict (see Guardrail verdict). A refused tenant warning throws TenantGuardrailError with a message naming acceptWarnings.

@askdb/client users call askdb.bind(), which supplies the client’s schema and expands a subtree scope.

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).

TypeWhat it is
PreparedQueryThe template ask() returns as result.preparedQuery: definitions and the named SQL, no runtime values. Store it as is.
BindGuardrailsThe third argument, as in the table above.
BoundQueryThe return value: { sql, tenantParams?, unboundSql, params, bindings, verdict }. Run sql with tenantParams, or unboundSql with params.
QueryParameterValueOne value in values: string, number or boolean. A list parameter takes an array of them.
QueryParameterTypeA parameter’s declared type: "string", "number", "boolean", "date" or "datetime".
QueryParameterBindingOne entry of bindings: a business parameter with its value, markers and indices.
QueryParamSlotOne entry of params: a QueryParameterValue, or an array of them for a list bound to one marker (listBinding: "array").

A refused value or template throws QueryParameterError; QueryParameterError reasons lists each reason.