Upgrade notes by release
Reference · Releases
What each AskDB release asks of your app, newest first. Each section lists only the changes that need something from you, and the package CHANGELOGs list everything else. Upgrade AskDB says how to apply them.
1.0.0-rc.1
Section titled “1.0.0-rc.1”Released with askdb, @askdb/ai, @askdb/client, @askdb/config, @askdb/core, @askdb/http-api, @askdb/introspect, @askdb/mysql, @askdb/postgres, @askdb/prisma, @askdb/sqlite and @askdb/sqlserver 1.0.0-rc.1, the first release candidate for 1.0, and @askdb/studio, @askdb/enrich and @askdb/rag 0.2.0-rc.1. The package CHANGELOGs list everything else that changed.
Move your AskDB ranges to the 1.0 line
Section titled “Move your AskDB ranges to the 1.0 line”The 12 packages on the 1.0 line now share one version, and every release publishes all 12 at that version, even a package that didn’t change (#354). Seven of them jump from 0.x:
| Package | Before | From this release |
|---|---|---|
@askdb/ai | 0.1.0-beta.* | 1.0.0-rc.1 |
@askdb/introspect | 0.3.0-beta.* | 1.0.0-rc.1 |
@askdb/mysql | 0.1.0-beta.* | 1.0.0-rc.1 |
@askdb/postgres | 0.2.0-beta.* | 1.0.0-rc.1 |
@askdb/prisma | 0.2.0-beta.* | 1.0.0-rc.1 |
@askdb/sqlite | 0.1.0-beta.* | 1.0.0-rc.1 |
@askdb/sqlserver | 0.1.0-beta.* | 1.0.0-rc.1 |
A range written for a 0.x version doesn’t match 1.0.0-rc.1. With "@askdb/ai": "^0.1.0-beta.9" in your package.json, npm update stops at the newest 0.1.0-beta.* and never installs 1.0.0-rc.1. Install every AskDB package your app lists at @latest in one step, as step 3 of Before you upgrade shows. That also moves the ranges in your package.json to the 1.0 line.
- From
1.0.0on, a breaking change to any of the 12 comes only in a new major version, of all 12 together. The exception is the experimental surfaces of the 1.0 packages, which can change in a minor version: thestudioandragblocks ofdefineConfig(), theaskdb studio,enrich,bundleandragcommands,askdb init’s RAG and Studio flags, the@askdb/config/scaffoldentry point, and the@askdb/client/chartsentry point. 1.0.0-rc.1freezes the public API of the 12. Later release candidates fix bugs. When a fix asks something of your app, it gets its own section on this page. The schema artifact format is the exception: Schema v2 can still change in a release candidate before1.0.0, and such a change gets its own section here too.- Studio,
@askdb/enrichand@askdb/ragstay on0.x, because their APIs can still change. A breaking change to one of them comes in a minor version, such as0.3.0, and gets its own section here.
Versioning and stability lists both sets, the experimental surfaces and the dist-tags.
Separate keywords, names and numbers with ASCII spaces under sqlserver
Section titled “Separate keywords, names and numbers with ASCII spaces under sqlserver”Under sqlserver, the tenant check now also refuses code SQL Server may split into tokens differently (#623). A direct validateTenantGuardrails() call with the built-in sqlserver dialect now reports NOT_SELECT_OR_WITH for a character outside plain code (such as a non-breaking space), a bare money sign (such as $ or €), a \, a number written against a word, or a ? written against a name, as the read-only check already did. Through ask() nothing changes, since that check runs first. Separate keywords, names and numbers with ASCII spaces, and keep other characters inside string literals.
Handle the new unknown_key loader warning
Section titled “Handle the new unknown_key loader warning”loadSchema() now warns about a key schema.json doesn’t define, such as a misspelled "sensitve": true, with a new SchemaV2Warning kind: { kind: "unknown_key", id, key, suggestion? } (#690). The key is still ignored, so a misspelled sensitive still marks nothing: fix the key the warning names. If your code switches over a warning’s kind exhaustively, add a case for unknown_key. askdb ask and askdb bundle print it on stderr, and Studio lists it under Settings → Schema Warnings. readSchemaDirectory() now keeps schema.json as written, unknown keys included, so a bundle warns like its directory. Loader warnings describes it.
1.0.0-beta.55
Section titled “1.0.0-beta.55”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.55, @askdb/studio 0.2.0-beta.48, @askdb/enrich 0.2.0-beta.26, @askdb/introspect 0.3.0-beta.29, @askdb/rag 0.2.0-beta.35, @askdb/postgres 0.2.0-beta.31, @askdb/prisma 0.2.0-beta.29, @askdb/mysql and @askdb/sqlite 0.1.0-beta.30, and @askdb/sqlserver 0.1.0-beta.31.
Security: upgrade for GHSA-p2v7-pv8j-c264 and GHSA-j363-683j-2pvw
Section titled “Security: upgrade for GHSA-p2v7-pv8j-c264 and GHSA-j363-683j-2pvw”Two advisories fix places where the tenant check could pass a tenant-scoped query that returned rows belonging to tenants outside the caller’s scope. Both needed the generated SQL to run without database-level tenant enforcement. GHSA-p2v7-pv8j-c264 also needed a way to influence the model’s reply. GHSA-j363-683j-2pvw needs no unusual SQL, so a model’s own mistake can trigger it.
- GHSA-p2v7-pv8j-c264: the tenant check could read a name differently from the engine, so a tenant-scoped query could pass without an effective tenant predicate.
- GHSA-j363-683j-2pvw: the tenant check counted a join without checking which columns it equated, so a tenant-scoped query could pass without an effective tenant predicate (#400). The fix is partial: read the Postgres way, a column list after a joined table’s alias (
JOIN clients c (sub_agency_id, x)) renames that table’s columns, and the check still reads them by the policy’s names, so a join path can pass on the wrong column. Known limits lists it.
What to do:
- Upgrade
@askdb/coreto 1.0.0-beta.55, and every AskDB package you install that depends on it:askdband@askdb/http-api1.0.0-beta.55,@askdb/studio0.2.0-beta.48,@askdb/introspect0.3.0-beta.29,@askdb/enrich0.2.0-beta.26,@askdb/rag0.2.0-beta.35,@askdb/postgres0.2.0-beta.31,@askdb/prisma0.2.0-beta.29,@askdb/mysqland@askdb/sqlite0.1.0-beta.30, and@askdb/sqlserver0.1.0-beta.31. These pin@askdb/coreexactly (@askdb/prismathrough@askdb/introspect), so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - Expect some correct queries to be refused. Join a path table on the columns the policy declares and Quote a table or column name CockroachDB lower-cases into another list them, with the change each needs.
- Check your SQL Server URLs too. Apart from the advisories, this release changes how
encryptandtrustServerCertificateread: Check the TLS settings in your SQL Server URLs. - Keep a fallback to
sql. Also apart from the advisories,ask()drops its reuse artifacts in more cases: Fall back tosqlwhen the reuse artifacts are absent. - If you can’t upgrade at once, enforce tenant isolation in the database (for example row-level security), as Run generated SQL safely recommends. It limits the rows a statement can read, whatever its text.
Join a path table on the columns the policy declares
Section titled “Join a path table on the columns the policy declares”Under strict enforcement, some joins that passed now throw TenantGuardrailError (MISSING_TENANT_PREDICATE) (#400). A table scoped through a join path passed when each join column’s name appeared somewhere and the path’s root was filtered, so a join on the wrong ID column passed; so did a root filtered through another table’s column with no join between them. Now each hop must equate the declared columns (appointments.client_id = clients.id) on the reads joined, in the same SELECT, up to a filtered read, and a table along the path counts only through the path’s next step. A path whose steps stop short of a root’s table (only shipments.order_id → orders.id, with orders filtered on its own column) proves nothing now: extend the path to the root. A scoped table joined to a filtered root’s ID, which was refused, now passes. A join through a CTE or derived table, or to a source that only borrows a policy table’s name as its alias (JOIN lookup_states clients), isn’t followed, and col IN (SELECT …) no longer carries a predicate out of its subquery, so id IN (SELECT id FROM orders WHERE agency_id = …) is refused: filter the read where it is. In a SELECT with an outer join, no ON counts, for a tenant predicate or for a join: the outer join keeps rows an ON never matched. Put the predicate in WHERE, and write a join path as JOIN when WHERE filters its joined side (appointments a LEFT JOIN clients c ON a.client_id = c.id WHERE c.id IN (…) is refused; with JOIN it passes). The prompt now tells the model to put tenant predicates and join-path equalities in WHERE in a query with an outer join or OUTER APPLY, and to filter a scoped table on the side an outer join may leave unmatched in a derived table or CTE, where a filter in WHERE would drop the unmatched rows. A qualified column named left, right, full or outer (o.full) marks its SELECT the same way: quote it (o."full").
Under sqlite, an ON that relies on a comma-joined read, or on a read joined after it, is refused too: SQLite lets an ON see those reads, and the check doesn’t. Write the join out (JOIN … ON in order), or move the predicate to WHERE.
In a join the check relies on, each table counts only where every engine the dialect covers reads it as the schema’s table. Quoted, it has to be spelled as the schema spells it. Unquoted, any ASCII case works on Postgres, CockroachDB and SQLite, but on MySQL, MariaDB and SQL Server it has to be spelled as the schema spells it, since their servers can keep a table name’s case. So JOIN "CLIENTS" c, which Postgres reads as another table than clients, no longer counts, and neither does an unquoted name with an upper-case non-ASCII letter under postgres or cockroachdb. Write each table as the schema spells it.
The check is still a heuristic, and some shapes still pass, such as an unqualified predicate in an ON that counts for a read the join doesn’t see where a name is spelled like a join word (NATURAL or USING as a SQL Server alias). Known limits lists them. Keep row-level security in the database as the tenant boundary.
Quote a table or column name CockroachDB lower-cases into another
Section titled “Quote a table or column name CockroachDB lower-cases into another”An unquoted name whose letters, lower-cased, aren’t in Unicode normalization form C is now refused (UNICODE_ESCAPED_IDENTIFIER) unless the schema spells it exactly: CockroachDB lower-cases such a name and then normalizes it, so J followed by U+030C reads as ǰ, and ΠΡΩΤΕΪ́ΝΗ as πρωτεΐνη. It affects about thirty letters, ΐ and ΰ among them in modern Greek. Write the name as the schema spells it. The schema’s spelling now excuses such a name, or one with İ, ı, ſ or the Kelvin sign, only when it is quoted, or lower-case and in NFC: CockroachDB reads an unquoted İTEMS as items, whatever the schema spells. Quote it as the schema spells it ("İTEMS"). A table name ending in a capital sigma (ΟΣ) is now matched to οσ before a column list and in a derived table’s items, as CockroachDB reads it.
Fall back to sql when the reuse artifacts are absent
Section titled “Fall back to sql when the reuse artifacts are absent”With parameterize on, ask() now drops unboundSql, params, parameters and preparedQuery whenever they differ from the checked sql in a literal, a quoted name, or how tokens join (#359). The consistency check compared the two forms lower-cased with whitespace collapsed, so a manifest value of "colorado" passed against a bound 'Colorado', and unboundSql with params could run something other than the checked sql on a case-sensitive comparison. It now compares tokens under the dialect’s lexer: strings, quoted names, numbers, parameters and comments must match exactly, a keyword or unquoted name compares as the engine folds it, and tokens that touch on one side must touch on the other. sql itself is unchanged; only the reuse artifacts are dropped. If your code runs unboundSql with params, fall back to sql when they’re absent, as Executing the result shows.
Check the TLS settings in your SQL Server URLs
Section titled “Check the TLS settings in your SQL Server URLs”AskDB now reads encrypt and trustServerCertificate the same way in mssql:// and sqlserver:// URLs (#596). true, yes and 1 mean true, and false, no and 0 mean false, in any case. Any other encrypt value turns encryption on, and any other trustServerCertificate value leaves the server certificate untrusted.
sqlserver://withencrypt=yesorencrypt=1now requires TLS. Before, onlyencrypt=trueturned encryption on, so these strings connected without it. A server that doesn’t offer TLS now refuses the connection. Writeencrypt=falseif you meant to connect without encryption.sqlserver://withtrustServerCertificate=yesor=1now trusts the server certificate, as=truedoes. Before, the certificate was checked.- An unknown value no longer trusts the certificate. In an
mssql://URL, atrustServerCertificatevalue other thanfalse,noor0used to trust it. Now onlytrue,yesand1do. In asqlserver://URL, an unknownencryptvalue used to leave encryption off. Now it turns encryption on.
An ADO.NET string whose value holds ://, such as a password, now has its spaced keys read too (#595). Before, Trust Server Certificate=True was dropped from such a string, so the certificate was checked.
1.0.0-beta.54
Section titled “1.0.0-beta.54”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.54, @askdb/studio 0.2.0-beta.47, @askdb/client 1.0.0-beta.14, @askdb/enrich 0.2.0-beta.25, @askdb/introspect 0.3.0-beta.28, @askdb/rag 0.2.0-beta.34, @askdb/postgres 0.2.0-beta.30, @askdb/prisma 0.2.0-beta.28, @askdb/mysql and @askdb/sqlite 0.1.0-beta.29, and @askdb/sqlserver 0.1.0-beta.30.
Security: upgrade for GHSA-rx3m-m5jr-pwch
Section titled “Security: upgrade for GHSA-rx3m-m5jr-pwch”GHSA-rx3m-m5jr-pwch: the tenant check could read a name’s letters differently from the engine, so a tenant-scoped query could pass without an effective tenant predicate. The engine could then return rows belonging to tenants outside the caller’s scope. Getting there needed a way to influence the model’s reply. The generated SQL also had to run without database-level tenant enforcement.
What to do:
- Upgrade
@askdb/coreto 1.0.0-beta.54, and every AskDB package you install that depends on it:askdband@askdb/http-api1.0.0-beta.54,@askdb/studio0.2.0-beta.47,@askdb/introspect0.3.0-beta.28,@askdb/enrich0.2.0-beta.25,@askdb/rag0.2.0-beta.34,@askdb/postgres0.2.0-beta.30,@askdb/prisma0.2.0-beta.28,@askdb/mysqland@askdb/sqlite0.1.0-beta.29, and@askdb/sqlserver0.1.0-beta.30. These pin@askdb/coreexactly (@askdb/prismathrough@askdb/introspect), so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - Expect some correct queries to be refused. Write a tenant column with non-ASCII letters as the schema spells it lists them, with the change each needs.
- If you can’t upgrade at once, enforce tenant isolation in the database (for example row-level security), as Run generated SQL safely recommends. It limits the rows a statement can read, whatever its text.
Write a tenant column with non-ASCII letters as the schema spells it
Section titled “Write a tenant column with non-ASCII letters as the schema spells it”A tenant column, a polymorphic discriminator or a HAVING predicate’s GROUP BY key whose name has non-ASCII letters now counts only when every engine read the same way reads it as the schema’s column; otherwise the predicate is refused (MISSING_TENANT_PREDICATE or MISSING_TYPE_DISCRIMINATOR):
- Postgres and CockroachDB: quote a column with an upper-case non-ASCII letter (
c."Égency_id"). Unquoted, Postgres keeps the letter and CockroachDB lower-cases it, so they read different columns. - MySQL, MariaDB and SQLite: write a non-ASCII letter in the schema’s case. Only ASCII letters match in any case.
A schema whose column names are ASCII is unaffected by that rule. Three more changes reach any schema (UNICODE_ESCAPED_IDENTIFIER or MISSING_TENANT_PREDICATE):
- Aliases and qualifiers on Postgres and CockroachDB: an unquoted alias or qualifier with an upper-case non-ASCII letter (
Ö.agency_id) ties a predicate to no read. Quote it, as the alias is ("Ö".agency_id), or write it in lower case. İ,ı,ſ, the Kelvin sign and names not in NFC: a name holding one of these letters, or not in Unicode normalization form C as written, is refused on every engine unless the schema spells it exactly. A model’s own alias withı(common in Turkish, as insayı) is refused too; the schema’s own Turkish names, written as the schema spells them, pass.- Tables whose name starts or ends with a non-ASCII letter: the check now sees
ürünorcaféas the scoped table it is, so SQL that read one without a tenant predicate, and passed, is refused.
1.0.0-beta.53
Section titled “1.0.0-beta.53”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.53, @askdb/studio 0.2.0-beta.46, @askdb/enrich 0.2.0-beta.24, @askdb/introspect 0.3.0-beta.27, @askdb/rag 0.2.0-beta.33, @askdb/postgres 0.2.0-beta.29, @askdb/prisma 0.2.0-beta.27, @askdb/mysql and @askdb/sqlite 0.1.0-beta.28, and @askdb/sqlserver 0.1.0-beta.29.
Security: upgrade for GHSA-9w7p-c3w8-4839 and GHSA-mmrm-rfxf-ccc8
Section titled “Security: upgrade for GHSA-9w7p-c3w8-4839 and GHSA-mmrm-rfxf-ccc8”Two advisories fix places where the tenant check could pass a tenant-scoped query that returned rows belonging to tenants outside the caller’s scope. Both needed the generated SQL to run without database-level tenant enforcement. GHSA-9w7p-c3w8-4839 also needed a way to influence the model’s reply. GHSA-mmrm-rfxf-ccc8 needs no unusual SQL, so a model’s own mistake can trigger it.
- GHSA-9w7p-c3w8-4839: the tenant check could read some table names, column names,
HAVINGclauses and conditions differently from the engine, so a tenant-scoped query could pass without an effective tenant predicate. - GHSA-mmrm-rfxf-ccc8: the tenant check tied a predicate to reads it doesn’t filter, so a tenant-scoped query could pass without an effective tenant predicate.
What to do:
- Upgrade
@askdb/coreto 1.0.0-beta.53, and every AskDB package you install that depends on it:askdband@askdb/http-api1.0.0-beta.53,@askdb/studio0.2.0-beta.46,@askdb/introspect0.3.0-beta.27,@askdb/enrich0.2.0-beta.24,@askdb/rag0.2.0-beta.33,@askdb/postgres0.2.0-beta.29,@askdb/prisma0.2.0-beta.27,@askdb/mysqland@askdb/sqlite0.1.0-beta.28, and@askdb/sqlserver0.1.0-beta.29. These pin@askdb/coreexactly (@askdb/prismathrough@askdb/introspect), so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - Expect some correct queries to be refused. Give each read of a scoped table its own tenant predicate and Put a tenant predicate in
WHEREwhen a query groups list them, with the change each needs. - If you can’t upgrade at once, enforce tenant isolation in the database (for example row-level security), as Run generated SQL safely recommends. It limits the rows a statement can read, whatever its text.
Give each read of a scoped table its own tenant predicate
Section titled “Give each read of a scoped table its own tenant predicate”Under strict enforcement, some SQL that passed before now throws TenantGuardrailError (MISSING_TENANT_PREDICATE) (#399). The tenant check looked for a predicate per statement or UNION branch. So a predicate inside a CTE, derived table or subquery could cover an unfiltered read of the same table outside it, and a predicate on one reference could cover another. Now a CTE body, a derived table and a subquery are each their own SELECT, and a qualified predicate counts only for the read it names. A read inside a derived table or CTE still passes when every reference to it is filtered on the tenant column. AskDB now refuses some correct queries:
- a root table joined for a label from a CTE, derived table or scalar subquery whose own
SELECTdoesn’t filter it; - a read inside a correlated subquery that only the outer query filters;
- a source that computes over its rows (an aggregate, a window,
LIMIT), filtered only outside it; - a source whose body is a set operation (
UNION,INTERSECT,EXCEPT), filtered only outside it; - a read with a clause right after its table or alias (
orders TABLESAMPLE …, SQL Server’sorders o (NOLOCK)withoutWITH), filtered only through that name; - a self-join where a read has no alias, a quoted alias or sits in parentheses, or that is filtered unqualified (
FROM orders a, orders b WHERE agency_id = …): give each read a plain alias and its own predicate (WHERE a.agency_id = … AND b.agency_id = …); - on MySQL, MariaDB and SQL Server, a predicate whose qualifier spells the alias in another case (
FROM orders O WHERE o.agency_id = …).
For each, add the predicate inside that SELECT (in each branch of a set operation), filter through an alias or unqualified, and write an alias the same way each time.
The check is still a heuristic, and some shapes still pass: a root filtered through another table’s tenant column isn’t checked for the join between them (#400), and an unqualified predicate in an ON can count for a read the join doesn’t see where a name is spelled like a join word (NATURAL or USING as a SQL Server alias). Known limits lists them. Keep row-level security in the database as the tenant boundary.
Check what your business context and column notes tell the model
Section titled “Check what your business context and column notes tell the model”The prompt ask() sends now carries each table’s ## Business context section, under the table after its common query language, and each column’s ## Column notes item, on that column’s line (#894, #898). With a retriever, it carries the business context retrieval picks, and every listed column’s note. Before, they reached neither prompt: a retrieved business-context or column chunk only picked its table, though the schema contract said its text would reach the model. A retrieved concept also keeps its label now, and a tenant-policy section its name.
Your language model provider now receives this text, not only your embedding provider. Read those sections in your tables/*.md files for anything the model shouldn’t see. They follow the rules for common query language: a sensitive table’s business context and a sensitive column’s note never reach the prompt, and with modes.omitSensitiveFromPrompt (--omit-sensitive-from-prompt) any of it that names a sensitive column is withheld, a note together with the rest of its column’s annotation. loadSchema() now returns them as a table’s businessContext and a column’s note. Privacy model lists what the model sees. The index doesn’t need rebuilding. If your tests compare formatSchemaV2ForNlToSql()’s or synthesizeRetrievedDdl()’s output, or loadSchema()’s result, expect the new lines and fields.
Check the foreign keys and concepts the prompt now sends
Section titled “Check the foreign keys and concepts the prompt now sends”The prompt ask() sends now lists each table’s foreign keys and, without a retriever, your concepts (#898). Before, foreign keys reached the model only through retrieval with emitRelationships: true, and concepts only through retrieval. A foreign key prints after its table’s columns, as FK (user_id) -> public.users (id), and the concepts follow the tables, each with its label, synonyms, links and description. With a retriever, the prompt lists foreign keys only between the tables it shows, and still only the concepts retrieval picks, now in the same shape. A retrieved relationship chunk (emitRelationships: true) no longer adds a -- relationships -- line: its foreign key prints under its table.
Your language model provider now receives your foreign keys and concepts without a retriever. Read concepts.md for anything the model shouldn’t see. A foreign key or concept never names an untracked table, and with modes.omitSensitiveFromPrompt (--omit-sensitive-from-prompt) a foreign key with a sensitive column or table on either side, and a concept that names or links one, are left out. The prompt grows by about 18 tokens per foreign key and 37 per concept, so past about 22 tables, consider retrieval. If your tests compare formatSchemaV2ForNlToSql()’s or synthesizeRetrievedDdl()’s output, expect the new lines.
Put a tenant predicate in WHERE when a query groups
Section titled “Put a tenant predicate in WHERE when a query groups”A tenant predicate in HAVING now counts only when its column is one the SELECT groups by: HAVING filters groups, and on a column that isn’t grouped, SQLite and MariaDB take one row’s value for the whole group. Read the MySQL way (mysql, mariadb, a dialect id that is no built-in engine, or none), an unqualified column that a select-list item is aliased to doesn’t count either, since MySQL and MariaDB read it as that item. Under strict, such SQL throws TenantGuardrailError (MISSING_TENANT_PREDICATE). Move the predicate to WHERE, or group by the tenant column.
Two more shapes that passed are refused: a tenant predicate written as a BETWEEN’s upper bound (0 BETWEEN 0 AND agency_id = …; write it as its own conjunct), and, under sqlite, a custom id or none, a read written FROM ('orders') with no predicate, which SQLite reads as the table.
The tenant column and a polymorphic discriminator now count only when written as the schema spells them; otherwise the predicate is refused (MISSING_TENANT_PREDICATE or MISSING_TYPE_DISCRIMINATOR):
- Postgres and CockroachDB: on a schema with upper-case column names (camelCase, as Prisma writes them), write the column quoted, as
c."agencyId". An unquotedagencyidoragencyIdfolds to lower case and names another column. A schema with lower-case column names is unaffected. - SQL Server: write the column as the schema spells it (
AgencyId, notagencyid). A database’s collation may compare names case-sensitively, and the check can’t see it, so a column in another case is refused even on a case-insensitive database. - SQLite: a non-ASCII letter must be in the schema’s case, since SQLite folds only ASCII letters.
- A custom dialect or none: all of the above apply, so a column in another case than the schema is refused whatever the engine. Set
dialectto avoid that.
1.0.0-beta.52
Section titled “1.0.0-beta.52”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.52, @askdb/studio 0.2.0-beta.45, @askdb/config 1.0.0-beta.20, @askdb/client 1.0.0-beta.13, @askdb/ai 0.1.0-beta.12, @askdb/enrich 0.2.0-beta.23, @askdb/introspect 0.3.0-beta.26, @askdb/rag 0.2.0-beta.32, @askdb/postgres 0.2.0-beta.28, @askdb/prisma 0.2.0-beta.26, @askdb/mysql and @askdb/sqlite 0.1.0-beta.27, and @askdb/sqlserver 0.1.0-beta.28.
Security: upgrade for GHSA-rhwf-7742-8hwj, GHSA-rpcp-322q-q738 and GHSA-cjwq-phcm-mwg7
Section titled “Security: upgrade for GHSA-rhwf-7742-8hwj, GHSA-rpcp-322q-q738 and GHSA-cjwq-phcm-mwg7”Three advisories fix places where the tenant check read SQL differently from the database engine. In each, a tenant-scoped query could pass the check while the engine read it without an effective tenant predicate, and returned rows belonging to tenants outside the caller’s scope. Getting there needed a way to influence the model’s reply, and SQL run without database-level tenant enforcement.
- GHSA-rhwf-7742-8hwj: the tenant check read a number or
$Nparameter written right against a keyword (1.0OR,$1OR) as one word, so a tenant-scoped query could pass with its predicate beside anORthe engine reads. - GHSA-rpcp-322q-q738: the tenant check could match a name, a placeholder, a column or a join differently from the database engine, so a tenant-scoped query could pass without an effective tenant predicate.
- GHSA-cjwq-phcm-mwg7: the tenant check ended a SQL Server name or number where SQL Server doesn’t, so a tenant-scoped query could pass with its predicate beside an
ORthe engine reads (#623).
What to do:
- Upgrade
@askdb/coreto 1.0.0-beta.52, and every AskDB package you install that depends on it:askdband@askdb/http-api1.0.0-beta.52,@askdb/studio0.2.0-beta.45,@askdb/introspect0.3.0-beta.26,@askdb/enrich0.2.0-beta.23,@askdb/rag0.2.0-beta.32,@askdb/postgres0.2.0-beta.28,@askdb/prisma0.2.0-beta.26,@askdb/mysqland@askdb/sqlite0.1.0-beta.27, and@askdb/sqlserver0.1.0-beta.28. These pin@askdb/coreexactly (@askdb/prismathrough@askdb/introspect), so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - On SQL Server, expect a non-ASCII name to be refused. Under
sqlserver, a dialect id that is no built-in engine, or none, the tenant check refuses a name that holds a character outside printable ASCII (UNICODE_ESCAPED_IDENTIFIER), and the sensitive check reports it as unresolved scope, unless the schema spells it exactly. So a non-ASCII alias or CTE name such as[Città]is refused unless the schema has that name. - Expect these refusals too, from GHSA-rpcp-322q-q738’s fixes, in its words. Wherever the SQL may be read as Postgres, a read whose alias has a column list, or that sits inside a parenthesized
FROMitem whose alias has one, counts as unfiltered (MISSING_TENANT_PREDICATE), and a quoted tenant column counts only when spelled as the schema spells it. A join is outer whenLEFT,RIGHT,FULLorOUTERappears among its words, whatever hint stands beside it, so CockroachDB’sLEFT STRAIGHT JOIN,LEFT LOOKUP JOINandLEFT INVERTED JOINare outer joins, and anONthat closes an earlier join is read as that join’s. Undersqlite, a dialect id that is no built-in engine, or none, a'…'right afterFROM,JOINorIN, after a comma in aFROMlist, or beside a.is read as that table’s name by the tenant and sensitive checks. - If you can’t upgrade at once, enforce tenant isolation in the database (for example row-level security), as Run generated SQL safely recommends. Each advisory lists the narrower workarounds for its cases.
Replace the removed internal exports
Section titled “Replace the removed internal exports”@askdb/core (#852) and @askdb/config (#830) no longer export the internal helpers below. Their API pages listed them as internal, with no stability promise, and no AskDB package outside their own imported them. They still run inside the package, so ask(), loadSchema() and bootstrapAskDbEnv() behave as before. If your code imports one, use the replacement in its row.
| Removed | Use instead |
|---|---|
flattenAskDbConfig(config) (@askdb/config) | defineConfig(config).entries, the same map. |
isAskDbEnvProjection(value), ASKDB_ENV_PROJECTION (@askdb/config) | None. loadAskDbConfigProjection() loads a config file written either way and returns its projection. |
v2TableSchema, v2ColumnSchema, v2TableFrontmatterSchema, v2ColumnFrontmatterSchema, v2ConceptsFrontmatterSchema, v2ConceptSchema, tenantRootSchema, hierarchyEdgeSchema, scopedTableSchema, polymorphicTableSchema, enforcementModeSchema, tenantAccessSchema | loadSchema() or loadSchemaFromJson() to check a whole schema artifact. parseTableMarkdown(), parseConceptsMarkdown() or parseTenantPolicyMarkdown() to check one file. validateTenantScope() to check a TenantScope. The types, such as V2Table and TenantRoot, stay exported. |
RECOGNIZED_H2_SECTIONS, TENANT_POLICY_H2_SECTIONS | The types RecognizedH2Section and TenantPolicyH2Section, which name the keys of a parsed file’s sections. |
assertNlToSqlInputs(), buildNlToSqlSystemPrompt() | None. generateSelectSql() checks the inputs and sends the system prompt. It’s the generation step ask() runs for a built-in dialect or a DialectSpec. |
buildSelectGuardrailExplanation() | ask({ explain: true }), which returns the summary as result.explain for a built-in dialect or a DialectSpec. |
escapeSqlLiteral() | None. Pass values to your driver as bound parameters. |
extractTenantPlaceholders() | resolveTenantSql(), whose bindings name each placeholder it replaced. |
parseAskDbSchemaJson(), normalizeAskDbSchema() | loadNormalizedSchemaFromJson(), which runs both steps on a v1 schema. |
Replace the unused exports removed before 1.0
Section titled “Replace the unused exports removed before 1.0”@askdb/ai, @askdb/config, @askdb/core and @askdb/introspect no longer export the values below (#654). Nothing outside their own package used them, so they leave before 1.0 freezes the API (ADR 0031). Each still works inside its package, except bootstrapAskDbRuntime and the deprecated placeholderForRoot, which are removed.
| Removed | Use instead |
|---|---|
aiKeyMissingMessage(context) (@askdb/ai) | registry.keyMissingMessage(context), which names the registry’s own providers. |
aiProviderMissingMessage(provider) (@askdb/ai) | None. A registry throws this message when it has no adapter for the provider; registry.hasProvider(provider) checks first. |
isReasoningEffort(value) (@askdb/ai) | REASONING_EFFORTS from @askdb/ai, the four values to check a string against. |
bootstrapAskDbRuntime(options) (@askdb/config) | bootstrapAskDbEnv(options), the same function. The not-initialized error now names only bootstrapAskDbEnv(). |
ASKDB_CONFIG_EXTENSION_PRECEDENCE (@askdb/config) | discoverAskDbConfigPath(cwd), which returns the file this order picks. The config API reference lists the order. |
DEFAULT_HTTP_API_REQUEST_TIMEOUT_MS (@askdb/config) | getAskDbRuntimeConfig().httpApi.requestTimeoutMs, the resolved value; the default is 60000. |
DEFAULT_STUDIO_EXECUTE_TIMEOUT_MS, DEFAULT_STUDIO_EXECUTE_MAX_ROWS (@askdb/config) | getAskDbRuntimeConfig().studio.execute.timeoutMs and .maxRows; the defaults are 30000 and 500. |
DEFAULT_PGVECTOR_INDEX_STRATEGY (@askdb/config) | "hnsw", the default for rag.storeConfig.pgvector.indexStrategy. |
DEFAULT_MOCK_RAG_EMBEDDING_DIMENSIONS (@askdb/config) | 64, the mock embedder’s width unless askdb rag --dimensions asks for another. |
renderAskDbAiConfigScaffold(input) and the types AskDbAiConfigScaffold, AskDbScaffoldEnvVar (@askdb/config/scaffold) | planAskDbScaffold(answers, versions): its configSource holds the ai block, and envVars lists the variables the config reads. |
placeholderForRoot(label) (@askdb/core) | placeholderForTenantRoot(root), which also handles a label with no ASCII letter or digit. |
ENRICHMENT_SYSTEM_PROMPT, buildEnrichmentUserPrompt(), parseCandidates() (@askdb/core) | suggestEnrichment(target, context, model, deps?), which sends the prompt and splits the reply. deps.generateText replaces the model call when you need to make it yourself. |
connectorProviderMissingMessage(provider) (@askdb/introspect) | None. A connector registry throws this message when it has no adapter for the provider; registry.hasProvider(provider) checks first. |
A JavaScript config loads with Node’s own loader
Section titled “A JavaScript config loads with Node’s own loader”@askdb/config loads askdb.config.js, .cjs and .mjs with Node’s own loader, and compiles only a TypeScript config (.ts, .mts, .cts) with jiti (#833). A JavaScript config now loads from inside an app’s bundle, webpack included; see Bundle an app that uses AskDB.
- A
.jsconfig that relied on jiti may now fail, with Node’s own error. jiti accepted ES module syntax in a file Node reads as CommonJS, such asexport defaultinaskdb.config.jsbeside apackage.jsonwith"type": "commonjs". Node now throwsSyntaxError: Unexpected token 'export'. Rename the file toaskdb.config.mjs, or writemodule.exports = { … }. A JavaScript config that imports TypeScript written for a compiler, such as a.tsfile whose imports name./other.jsfor./other.ts, fails too: rename the config toaskdb.config.ts. - Node’s rules decide the module format, as for any file Node loads:
.cjsis CommonJS,.mjsis an ES module, and.jsfollows the nearestpackage.json. A TypeScript config loads as before. - An ES module config that throws while it loads loads again only after a restart. Node keeps the failed module, so a process that loads the config again, as Studio does after you edit
.env, gets an error that says to restart it. A CommonJS or TypeScript config loads again once fixed.
Fix an unknown studio.execute.provider, which now fails to load
Section titled “Fix an unknown studio.execute.provider, which now fails to load”A studio.execute.provider that isn’t postgres, mysql, sqlite or sqlserver now fails to load with askdb.config: invalid studio.execute.provider "postgress" (expected one of: postgres, mysql, sqlite, sqlserver). (#751). Before, Studio quietly ran Execute against the introspection provider, or postgres, so a typo could point it at an engine you didn’t name. Fix the value, or remove the key to keep that fallback: an unset provider still uses the introspection provider when it’s a live engine, else postgres.
Set dialect for a schema whose engine AskDB doesn’t ship, in Studio
Section titled “Set dialect for a schema whose engine AskDB doesn’t ship, in Studio”Studio’s Playground now refuses a schema whose provider has no AskDB dialect, as the CLI and the HTTP API already did (#886, ADR 0039). It answers 422 with @askdb/client’s DialectNotSupportedError message and generates nothing. Before, it generated PostgreSQL SQL and checked it under PostgreSQL’s rules, which can pass SQL your engine reads differently. The workspace still loads for browsing and enrichment, and the Get the code panel shows the reason instead of code. If your engine speaks a dialect AskDB ships, set dialect in askdb.config.ts, such as "postgres" for a PostgreSQL-compatible warehouse.
Other changes you may notice in beta.52
Section titled “Other changes you may notice in beta.52”- A tenant policy with a table named
orderorgroupno longer refuses every sorted query (#828). The tenant check read theORDERofORDER BY(andGROUPofGROUP BY) as a reference to a table of that name, so understrictenforcement a sorted, tenant-scoped query was refused withUNKNOWN_TABLE_REFERENCEDorMISSING_TENANT_PREDICATE. It no longer reads those keywords as a table; a quoted or qualified"order", ororderinFROM/JOIN, still counts as the table. If you worked around this by renaming the table or droppingORDER BY, you can undo that. - On SQL Server, the tenant check ends a name or number where SQL Server does (#623). SQL Server ends a number after
0xor1E, and a name at a non-breaking space (U+00A0), U+200B or U+0085, so a keyword written right after one is a keyword: it reads1=0xOR 1=1as1=0x OR 1=1. The tenant check now reads those ends the same way undersqlserver, and under a dialect id that is no built-in engine or no dialect, which may be SQL Server. AnORwritten that way is required to keep the tenant predicate ANDed in, as one after a space always was.ask()already refused these shapes through the read-only check, so the change shows in a direct call tovalidateTenantGuardrails(). - A
$Nparameter written against a word is refused where the SQL may be read as Postgres (#665). The read-only check read$1INTOand$1FORas one parameter, so it never saw theINTOorFOR. Postgres 15 and later reject that SQL, but a server that ends the parameter at its last digit would run the keyword. It now refuses the shape withSQL_FORBIDDEN_KEYWORDunderpostgres,cockroachdband a dialect id that is no built-in engine. If a stored template writes a parameter that way, put a space after it. - Internal: every SQL check reads through the shared lexer (#815, ADR 0010). One module now sets the readings each check takes for each dialect, and the tenant check’s separate scanner is gone, so a lexer fix lands in one place. #815 itself changes no public API and no verdict beyond #828.
askdb initinstalls in a one-package project whosepnpm-workspace.yamlholds only settings (#863), such asminimumReleaseAgeExclude, which pnpm also keeps in that file. It used to skip the install there and print thepnpm addcommand instead. It still skips at a workspace root: apnpm-workspace.yamlthat lists at least one package underpackages, or apackage.jsonwithworkspaces. Studio’s guided setup reads the file the same way.askdb initand Studio’s setup read a pgvector store’s URL fromPGVECTOR_URL, notASKDB_PGVECTOR_URL, when you don’t name a variable. That’s the name the rest of these docs use.--pgvector-env, or the variable field in Studio’s setup, still picks another. A config that already readsenv("ASKDB_PGVECTOR_URL")keeps working: the name is whatever itsenv()call passes.@askdb/clientexportsresolveDialect(schema, options?), the rulecreateAskDb()runs to pick a dialect (#886). A host that calls@askdb/core’sask()itself can pick the dialect the CLI, Studio and the HTTP API pick, and refuse the same schemas.
1.0.0-beta.51
Section titled “1.0.0-beta.51”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.51, @askdb/studio 0.2.0-beta.44, @askdb/enrich 0.2.0-beta.22, @askdb/introspect 0.3.0-beta.25, @askdb/rag 0.2.0-beta.31, @askdb/postgres 0.2.0-beta.27, @askdb/prisma 0.2.0-beta.25, @askdb/mysql and @askdb/sqlite 0.1.0-beta.26, and @askdb/sqlserver 0.1.0-beta.27.
Security: upgrade for GHSA-rmjw-pffc-5hjr
Section titled “Security: upgrade for GHSA-rmjw-pffc-5hjr”A residual of GHSA-8pwj-6hfv-5pm4’s first case. A Postgres or CockroachDB dollar-quoted string may carry non-ASCII letters in its tag ($é$ … $é$), and the engine and AskDB’s shared lexer read it as a string. The tenant check’s scanner read the tag as ASCII-only, so it read such a string as code, and a statement could pass the check while the engine read it with no effective tenant predicate, returning rows belonging to tenants outside the caller’s scope (GHSA-rmjw-pffc-5hjr). This reached the built-in postgres and cockroachdb dialects, a DialectSpec under one, a custom AskDialect, and direct calls to the check. The scanner now reads the tag with the same characters as the shared lexer and the engine.
- Upgrade
@askdb/core,askdband@askdb/http-apito 1.0.0-beta.51, and@askdb/studioto 0.2.0-beta.44. These pin@askdb/coreexactly, so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - One case now passes: under
postgresandcockroachdb, a table named only inside such a string no longer counts as read, as the engine reads it. - No setting closes it on 1.0.0-beta.50 or earlier. Enforce tenant isolation in the database (for example Postgres row-level security), as Run generated SQL safely recommends.
Replace createOpenAiEmbedder from @askdb/rag
Section titled “Replace createOpenAiEmbedder from @askdb/rag”@askdb/rag no longer exports createOpenAiEmbedder or CreateOpenAiEmbedderOptions, and @askdb/rag/embedders/openai is no longer an entry point (#844). Importing either now fails. Pick the replacement by where your embedding settings live:
-
askdb ragor Studio: setrag.embedder: "ai"and anai.embeddingsection, withprovider: "openai"and amodel, inaskdb.config.*. You don’t build an embedder yourself. -
Your own code, with an
askdb.config.*file: build the model from the config’sai.embeddingsection with@askdb/ai’screateEmbeddingModelFromSection, and wrap it withcreateAiSdkEmbedder:import { createAiRegistry } from "@askdb/ai";import { getAskDbRuntimeConfig } from "@askdb/config";import { createAiSdkEmbedder } from "@askdb/rag";const embedding = getAskDbRuntimeConfig().ai.embedding; // set when rag.embedder is "ai"const model = embedding ? await createAiRegistry().createEmbeddingModelFromSection(embedding) : undefined;if (!model) throw new Error('Set rag.embedder: "ai" and ai.embedding, with an API key on its connection.');const embedder = createAiSdkEmbedder({ model }); -
Your own code, without a config file: build the AI SDK model yourself, such as
createOpenAI({ apiKey, baseURL }).embedding("text-embedding-3-small")from@ai-sdk/openai, and pass it tocreateAiSdkEmbedder({ model }). The helper’sdimensionsanduseroptions move tocreateAiSdkEmbedder’sproviderOptions: { openai: { dimensions, user } }.
The helper picked text-embedding-3-small when you set no model. Name that model and keep your buildSchemaIndex() embedderId as it was, left unset if you never set one, and an index you built with the helper stays valid.
Point each tenant-policy column at its entry’s own table
Section titled “Point each tenant-policy column at its entry’s own table”The tenant-policy loader now checks that each column the contract puts on an entry’s own table is on that table (#836): a root’s tenantIdColumn and parent.foreignKey, a hierarchy edge’s foreignKey (on the child), a scopeThrough column, and a polymorphic typeColumn and idColumn. Before, it only checked that schema.json had the column somewhere, so a scopeThrough column on another table loaded without a warning and broke the scoped table’s filter path.
- Under
enforcement: strict, such a policy fails to load withSchemaParseError, naming the file, the column and the table it’s on, inloadSchema(), a bundle, and@askdb/enrich’ssaveTenantPolicy()(Studio’s Save). Underwarn, it loads with the warning. Point the field at the column on the entry’s own table. - A
switchover everyTenantPolicyWarning(inschema.tenantPolicy.warnings) needs a case for the newmisplaced_column_id, withid,tableId(the table the column is on) andcontext. The schema loader’s warning of the same name, inschema.warnings, is unchanged. - If you call
normalizeTenantPolicy()yourself, its third argument is now aMapfrom column ID to table ID, read fromschema.json:new Map(tables.flatMap((t) => t.columns.map((c) => [c.id, t.id]))).
Other changes you may notice in beta.51
Section titled “Other changes you may notice in beta.51”- No schema configured server-side answers
500 generation_not_configuredfromaskdb-http, not400 bad_request(#845), with the cause in the server log (errNameSchemaNotConfiguredError). A request that sends its ownschemaJsonis unaffected. If your client matchedbad_requestfor this case, matchgeneration_not_configured.
1.0.0-beta.50
Section titled “1.0.0-beta.50”Released with askdb, @askdb/core and @askdb/http-api 1.0.0-beta.50, @askdb/studio 0.2.0-beta.43, @askdb/ai 0.1.0-beta.11, @askdb/config 1.0.0-beta.19, @askdb/client 1.0.0-beta.12, @askdb/enrich 0.2.0-beta.21, @askdb/introspect 0.3.0-beta.24, @askdb/rag 0.2.0-beta.30, @askdb/postgres 0.2.0-beta.26, @askdb/prisma 0.2.0-beta.24, @askdb/mysql and @askdb/sqlite 0.1.0-beta.25, and @askdb/sqlserver 0.1.0-beta.26.
Security: upgrade for GHSA-8pwj-6hfv-5pm4
Section titled “Security: upgrade for GHSA-8pwj-6hfv-5pm4”The tenant check looks for each tenant-scoped table’s predicate in the model’s SQL, and skips string literals and comments to find it. In four cases it read the SQL differently from the database engine, so a statement could pass the check while the engine read a table, or dropped a predicate, that the check never saw, and returned rows belonging to tenants outside the caller’s scope (GHSA-8pwj-6hfv-5pm4). With enforcement: strict, ask() returned the statement. Getting there needed a way to steer the model’s reply, and SQL run without database-level tenant enforcement. The check now reads each case the way the engine does:
- A
$inside a name. Postgres, CockroachDB, MySQL, MariaDB and SQLite readx$$yas one name, but the check read its$$as the start of a dollar-quoted string, under every dialect, and read$tag$…$tag$as a string on engines that have no such strings. This reached the built-inpostgres,cockroachdb,mysql,mariadbandsqlitedialects with default settings, and the CLI, the HTTP API and Studio. Beta.50 still read a dollar-quoted string whose tag holds non-ASCII letters ($é$ … $é$) as code: that residual is GHSA-rmjw-pffc-5hjr, fixed in beta.51. - A table named after a keyword. A scoped table written unquoted under a name some engine takes as a plain name, such as
useron MySQL orlock,qualifyandbyon Postgres, wasn’t checked for its predicate. - An unset
backslashEscapeson a MySQL or MariaDBDialectSpec. The check read it as escapes off, while the server and the rest of AskDB read it as on. A spec that spreadsMYSQL_DIALECTorMARIADB_DIALECTkeeps the field and wasn’t affected. - Where a
--comment ends. Postgres and SQL Server end it at a carriage return as well as at a line feed, and the check ended it only at a line feed. This reached a customAskDialect, which skips the read-only check that refuses comments, and direct calls to the checks. On MySQL, MariaDB and SQLite the sensitive-column check had the opposite difference.
What to do:
- Upgrade
@askdb/core,askdband@askdb/http-apito 1.0.0-beta.50, and@askdb/studioto 0.2.0-beta.43. These pin@askdb/coreexactly, so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - Expect more refusals in these cases. A scoped table named after a keyword now counts wherever its name appears outside
ORDER BYandGROUP BY: with a scoped table namedlimit, a query that usesLIMITneeds that table’s tenant predicate too. A table named after a--comment that a carriage return ends is now read as code on Postgres and SQL Server, so a query that reads it without a predicate is refused. Under a customAskDialect, aDO $$ … $$block’s tables are now reported besideNOT_SELECT_OR_WITH. - One case now passes: under
sqlserver, a predicate written after a comment that a carriage return ends counts, as SQL Server reads it. - If you can’t upgrade at once, set
backslashEscapes: trueon a MySQL-familyDialectSpecyou built yourself (falseonly for aNO_BACKSLASH_ESCAPESserver), or spreadMYSQL_DIALECT; and in a customAskDialect, runvalidateSelectSql(spec, sql)with your engine’s built-in spec before executing, since it refuses comments. No setting closes the first two cases. - Enforce tenant isolation in the database (for example Postgres row-level security), as Run generated SQL safely recommends. It limits the rows a statement can read regardless of its text.
Handle INVALID_DECLARATION from bindPreparedQuery()
Section titled “Handle INVALID_DECLARATION from bindPreparedQuery()”bindPreparedQuery() now refuses a stored template whose parameter declares a type or cardinality AskDB doesn’t support. It throws QueryParameterError with the new QueryParameterRejectionReason INVALID_DECLARATION, naming the parameter, before it reads any value (#789).
- An unknown
type, anything butstring,number,boolean,dateordatetime, used to fail as a rawTypeError. - An unknown
cardinalityis a behavior change. A template that declared anything butoneormanyused to bind asone; it now throws. - A
switchover everyQueryParameterRejectionReasonneeds a case for the new member. - Fix the template, not the value: regenerate it with
ask(). A templateask()produced always declares supported values; one edited or built by hand may not.
Take credentials out of a model base URL
Section titled “Take credentials out of a model base URL”A model base URL with a username or password, such as http://user:[email protected]/v1, now fails before any request (#787, ADR 0036). Such a URL never worked: fetch refused it with an error that quoted the whole URL, credentials included, in SqlGenerationError and the askdb.pipeline.failed log.
- A config that sets one fails to load: in any
ai.providerConfig.<provider>.baseUrl, even on a connection no section uses. Theaskdb.config:error names the key. @askdb/aithrows the newAiBaseUrlCredentialsErrorfor one in a config section’sconnection.baseUrl, a base URL env variable, anAiConfig’sbaseURL, or theOPENAI_BASE_URLorANTHROPIC_BASE_URLthe provider SDK would read from the environment. Itssettingnames where the URL came from.- Neither error repeats the URL. Put the key in
apiKey. For other credentials, such as a proxy’s, build the model in code with the AI SDK provider’sheadersoption (seeai.providerConfig).
Replace the removed config keys
Section titled “Replace the removed config keys”The config keys deprecated in beta.44 (@askdb/config 1.0.0-beta.13), and rag.storeConfig.file.autoFlush from beta.47, are removed (#765). A config that still sets one fails to load. The askdb.config: error names each such key and its replacement, and never repeats a value. A key counts as set whatever its value, so model: env("OPENAI_MODEL") on a connection fails even when the variable is unset. Each key, before and after:
| Before | After |
|---|---|
ai: { reasoning: { effort: "low" } } | ai: { language: { reasoning: { effort: "low" } } } |
providerConfig: { openai: { apiKey, model: "gpt-4o" } } | providerConfig: { openai: { apiKey } }, language: { model: "gpt-4o" }. A model on a connection only embeddings use goes to ai.embedding.model. |
providerConfig: { azure: { apiKey, resourceName, modelFamily: "gpt-5" } } (or on foundry) | providerConfig: { azure: { apiKey, resourceName } }, language: { modelFamily: "gpt-5" } |
provider: "mistral", providerConfig: { custom: { apiKey } } | provider: "mistral", providerConfig: { mistral: { apiKey } }. A provider whose id is custom keeps providerConfig.custom. |
rag: { embedder: "openai", … } | ai: { embedding: { provider: "openai", model: "text-embedding-3-small" } }, rag: { embedder: "ai", … } |
rag: { embedder: "ai-sdk", … } | ai: { embedding: { model: "text-embedding-3-small" } }, rag: { embedder: "ai", … }. The embedding provider is ai.provider unless ai.embedding.provider sets another. |
rag: { embedderConfig: { openai: { model, dimension: 1536 } } } | ai: { embedding: { model, dimensions: 1536 } } |
rag: { embedderConfig: { openai: { apiKey, baseUrl } } } | The key and base URL on a connection of the embedding provider in ai.providerConfig. When they differ from its "default" connection, add a named one and set ai.embedding.connection to its name. |
rag: { embedderConfig: {} }, as earlier scaffolds wrote | Remove it. |
storeConfig: { pgvector: { databaseUrl, dimensions: 1536 } } with rag.embedder: "ai" | ai: { embedding: { …, dimensions: 1536 } }, storeConfig: { pgvector: { databaseUrl } } |
storeConfig: { pgvector: { …, dimensions } } with rag.embedder: "mock" | Remove it. The mock embedder’s vectors are 64 wide unless askdb rag --dimensions asks for another width. |
storeConfig: { file: { basePath, autoFlush: true } } | storeConfig: { file: { basePath } }. The file store writes its files after every change. |
Where the old "openai" or "ai-sdk" embedder left the model unset, it used text-embedding-3-small: set that model in ai.embedding to keep the same embeddings. An index keeps working when ai.embedding names the same provider, model and dimensions the old keys did. Otherwise Studio reports it as stale, and you rebuild it.
Read the typed ai sections instead of rt.ai.aiEnv
Section titled “Read the typed ai sections instead of rt.ai.aiEnv”getAskDbRuntimeConfig().ai.aiEnv, ai.language.env and ai.embedding.env are removed (#711). These env maps turned the ai block into the env names @askdb/ai read back. Pass the typed section to the @askdb/ai registry instead. @askdb/client’s createAskDb already does, so a host that only calls createAskDb changes nothing.
| Before | After |
|---|---|
ai.resolveAiConfig(rt.ai.aiEnv) | ai.resolveAiConfigFromSection(rt.ai.language) |
ai.createLanguageModelFromEnv(rt.ai.aiEnv) | ai.createLanguageModelFromSection(rt.ai.language) |
ai.resolveEmbeddingConfig(rt.ai.embedding.env) | ai.resolveEmbeddingConfigFromSection(rt.ai.embedding) |
ai.createEmbeddingModelFromEnv(rt.ai.embedding.env, { dimensions: rt.ai.embedding.dimensions }) | ai.createEmbeddingModelFromSection(rt.ai.embedding). It asks for dimensions when they’re set. |
resolveReasoningEffort(rt.ai.aiEnv, "nlToSql") | resolveReasoningEffortFromSection(rt.ai.language.reasoning, "nlToSql") |
rt.ai.language.env.OPENAI_API_KEY (any rt.ai.language.env key) | rt.ai.language.connection.apiKey (the matching connection setting) |
rt.ai.embedding.env.OPENAI_API_KEY (any rt.ai.embedding.env key) | rt.ai.embedding.connection.apiKey (the matching connection setting) |
rt.ai.language.connection, rt.ai.embedding.connection (the connection name) | rt.ai.language.connection.name, rt.ai.embedding.connection.name |
connectionis now an object: the connection’snameand its settings (apiKey,apiKeySecondary,baseUrl,resourceName,apiVersion), with unset ones left out. The config’ssecondaryApiKeyisapiKeySecondaryhere.rt.ai.languagealso gainsreasoning.- The flat map holds no AI keys.
rt.flat,flattenAskDbConfig()anddefineConfig().entriesno longer carryASKDB_AI_PROVIDER,OPENAI_API_KEY, the reasoning keys or any other AI key. A child process that took its AI settings frommergeAskDbFlatIntoEnvMap()needs them another way, for example by loading the sameaskdb.config.*. rt.rag.embedderis removed, with its typeAskDbRuntimeRagEmbedderConfig. It held the OpenAI embedder’s key, base URL and model, and nothing in AskDB read it. It’s nowundefined, so readingrt.rag.embedder.apiKeythrows, and TypeScript reports the missing field. Readrt.ai.embeddinginstead:connection.apiKey,connection.baseUrlandmodel, for whichever providerai.embeddingnames.rt.ai.embeddingisundefinedunlessrag.embedderis"ai".- If you pass
registrytocreateAskDb, build it with@askdb/ai0.1.0-beta.11 or newer: the client calls the new section methods. With an older one, the firstask()that needs the config’s model throwsModelNotConfiguredError, which names the older@askdb/ai. - Without a config file, nothing changes:
resolveAiConfig(env)and the*FromEnvmethods read an env map as before. Env map precedence lists the variables.
Replace the removed @askdb/config exports
Section titled “Replace the removed @askdb/config exports”| Removed | Use instead |
|---|---|
DEFAULT_OPENAI_CHAT_MODEL, DEFAULT_ANTHROPIC_CHAT_MODEL, DEFAULT_GOOGLE_CHAT_MODEL, DEFAULT_GATEWAY_CHAT_MODEL | DEFAULT_OPENAI_LANGUAGE_MODEL, DEFAULT_ANTHROPIC_LANGUAGE_MODEL, DEFAULT_GOOGLE_LANGUAGE_MODEL, DEFAULT_GATEWAY_LANGUAGE_MODEL |
DEFAULT_RAG_EMBEDDING_MODEL | Set ai.embedding.model: AskDB has no default embedding model. |
defaultRagEmbeddingDimensions() | None: AskDB learns the width from the model (detectEmbeddingDimensions in @askdb/rag). |
OpenaiConfig, AzureConfig, FoundryConfig, AnthropicConfig, GoogleConfig, GatewayConfig | OpenaiConnection, AzureConnection, FoundryConnection, AnthropicConnection, GoogleConnection, GatewayConnection |
OpenaiAiConfig, AzureAiConfig, FoundryAiConfig, AnthropicAiConfig, GoogleAiConfig, GatewayAiConfig | AskDbAiConfig |
OpenaiRagEmbedderConfig | AskDbAiEmbeddingConfig (ai.embedding) and a connection type |
ASKDB_RAG_EMBEDDERS is now ["mock", "ai"], and the AskDbRagEmbedder type narrows to match.
Set an embedding model on @askdb/ai’s env path
Section titled “Set an embedding model on @askdb/ai’s env path”@askdb/ai no longer picks an embedding model when you pass an env map (#651). With an API key and no embedding model, registry.resolveEmbeddingConfig(env) and registry.createEmbeddingModelFromEnv(env) now throw <provider>: no embedding model configured. Set ai.embedding.model in askdb.config.* (or ASKDB_AI_EMBEDDING_MODEL)., as the section path already refuses a missing model. Before, openai and azure fell back to text-embedding-3-small, and gateway to openai/text-embedding-3-small, with a DeprecationWarning (code ASKDB_AI_DEFAULT_EMBEDDING_MODEL). A host with an askdb.config.* file uses the section path, so this changes nothing for it.
- If you saw that warning, set
ASKDB_AI_EMBEDDING_MODEL, or the provider’s embedding model variable, in the env map you pass, to the model the fallback used. An index built with that model keeps working. - If your app picks the model itself, pass it as
resolveEmbeddingConfig(env, { modelDefault }). An env variable still wins over it. ProviderEnvSpechas nodefaultEmbeddingModelfield, and theBUILTIN_AI_PROVIDERSentries no longer carry one. Remove it from a spec your custom adapter declares: TypeScript reports it as an unknown property.
Replace the askdb-rag binary with askdb rag
Section titled “Replace the askdb-rag binary with askdb rag”@askdb/rag no longer ships the askdb-rag binary (#651). Since beta.44 it only printed the askdb rag command to run and exited 1. Now a shell or npx can’t find an askdb-rag command. Install askdb and run npx askdb rag index …, query … or setup-store … instead. Run askdb rag instead of askdb-rag maps its flags.
Replace the removed --embedder values
Section titled “Replace the removed --embedder values”askdb rag --embedder openai and --embedder ai-sdk are removed (#811), as the same rag.embedder values were. Each now fails with exit code 1 and an error naming the replacement, before the command reads the schema or opens a store. Set rag.embedder: "ai" and an ai.embedding section in askdb.config.*, and pass --embedder ai or no --embedder. The aliases already embedded with ai.embedding, so an index they built keeps its embedder id and still matches.
Handle introspection that finds no tables
Section titled “Handle introspection that finds no tables”Introspection now refuses to write an artifact with no tables (#337, ADR 0037). It used to write a schema.json with tables: [], which loadSchema() rejects, and re-introspecting a database that had lost its tables replaced a valid artifact with it.
askdb introspectexits1where it exited0, with--out,--printand--diffalike, and writes nothing. A script that checks the exit code sees the failure now; the message names the schema id, the engine and the filters.introspect()with render options,renderToSchemaV2()andrenderSchemaV2Body()throw the newEmptyIntrospectionErrorwhen the schema renders no tables or views, after filters. Catch it where you introspect from code; it carriesschemaId,providerandfilters.- Studio answers
422with the reason when its introspection or resync finds nothing, and keeps the artifact on disk.
See the troubleshooting entry for the likely causes.
Fix a bundle without a tables object, which now fails to load
Section titled “Fix a bundle without a tables object, which now fails to load”A bundle must carry tables, the markdown of each tables/*.md keyed by file name, {} when there are none (#797). loadSchema() and loadSchemaFromJson() now throw SchemaParseError naming tables and the bundle when it’s missing, isn’t an object, or holds a value that isn’t a string. Before, a missing or null tables threw a raw TypeError, and a non-empty string or array was read as if each character or item were a file, so the error named a tables/0 that doesn’t exist. A tables of [], "", 0 or true loaded as a schema with no table files, and now throws as well. askdb bundle always writes tables, so only a bundle made or edited by hand is affected: set "tables": {}, or rebuild it with askdb bundle.
Fix a schema directory that askdb bundle now refuses
Section titled “Fix a schema directory that askdb bundle now refuses”askdb bundle, and bundleSchemaDirectory() from @askdb/enrich, now check the bundle with the loader ask() uses (#796). bundleSchemaDirectory() throws the loader’s SchemaParseError where it returned the bundle, and askdb bundle exits 1 with that error before it writes anything. A directory the loader refuses used to bundle without complaint, and the bundle failed only when your service loaded it. Examples are a tables/*.md whose front-matter isn’t YAML, a tenant-policy.md that doesn’t parse or doesn’t fit schema.json, and two table files with the same id. Now askdb bundle exits 1, where it exited 0, and writes nothing: a file already at --out stays as it was. Fix the file the error names; if you call bundleSchemaDirectory() yourself, catch SchemaParseError there. A build step that runs askdb bundle now fails there instead of in the service.
Keep a built-in id on a DialectSpec
Section titled “Keep a built-in id on a DialectSpec”ask() now refuses a DialectSpec whose id isn’t one of the six built-in ids, with UnknownDialectError (#717). Core lexes, quotes and binds SQL by id (ADR 0029), so a spec under another id was checked under every engine’s lexer at once: that rejected some valid column names (#672) and couldn’t read the SQL the way the real engine does. bindPreparedQuery() already refused its templates. ask() refuses such a spec before any tenant check, retrieval or model call.
- For a variant of a built-in engine, such as TiDB on MySQL or Redshift on Postgres, spread the built-in spec and keep its
id:{ ...MYSQL_DIALECT, displayName: "TiDB", promptBrief: "…" }. - For any other engine, implement
AskDialectand callvalidateSelectSqlon its SQL yourself. - With a tenant policy, such a spec used to get
TenantScopeError(TENANT_SQL_MODE_REQUIRED). It now getsUnknownDialectErrorfirst.
Set dialect for askdb-http’s own schema when AskDB doesn’t ship its engine
Section titled “Set dialect for askdb-http’s own schema when AskDB doesn’t ship its engine”askdb-http no longer generates PostgreSQL for a schema whose provider has no AskDB dialect (#720). It refuses the request instead, as askdb ask does. Checking SQL for one engine under another engine’s rules can pass SQL the real engine reads differently.
- For the server’s configured schema,
POST /askanswers500 generation_not_configured, and the server log namesDialectNotSupportedError. Setdialectinaskdb.config.tsto the built-in dialect your engine speaks, such as"postgres"for a PostgreSQL-compatible warehouse. Thatdialectapplies to the server’s own schema only. - For a
schemaJsonthe request sends, the server generates and checks SQL for the artifact’s ownprovider, never for the config’sdialect(#809). A request schema that records noprovider, or one AskDB has no dialect for, answers400 bad_request, with a message that names the provider and the supported ids. Send a schema whoseprovideris a built-in dialect id. A caller that relied on the server’sdialectto cover its schema’s engine, such as a MariaDB artifact, which recordsmysql, now gets that recorded engine’s SQL.
Pass the prompt’s schema to buildTenantPromptBlock()
Section titled “Pass the prompt’s schema to buildTenantPromptBlock()”buildTenantPromptBlock(policy, scope, prompt) now requires prompt: the prompt’s schema and, under retrieval, the ids of the tables the retrieved DDL shows (#771, ADR 0038). The block names only those tables. ask() passes it for you, so this only matters if you call the function yourself.
- For a prompt that shows the whole schema, pass
{ schema }. If no table in your schema is markedtracked: false, the block is the same as before. An untracked table can no longer be named in the block, even on purpose. - Under retrieval, pass
{ schema, tableIds }, withsynthesizeRetrievedDdl()’stableIds. buildNlToSqlUserPrompt()throwsAskDbErrorwhen you passprebuiltDdland a tenant policy withoutpromptTableIds(the same ids), or a tenant policy with a v1 schema.
Other changes you may notice in beta.50
Section titled “Other changes you may notice in beta.50”- A connection’s
secondaryApiKeyis used on every provider when the connection has noapiKey. Before, only Azure and Foundry read it; the connection types still declare it only there. - An
ai.languageorai.embeddingprovider that no registered adapter handles now fails with the message that says how to register it, ascreateLanguageModelalready did, instead ofUnknown ASKDB_AI_PROVIDER. getAskDbRuntimeConfig().deprecationsis empty, since no config key is deprecated now. It stays for the next deprecation.- The prompt’s note about a table the question names, but the schema doesn’t have, no longer lists the schema’s tables (#677). The list included tables marked
tracked: falseand, under retrieval, tables retrieval left out, so their names reached the model. The note now names only the word from the question, and the single-table note no longer names the table. Both count tracked tables only, so a question that names an untracked table gets the note too: the table stays in your schema artifact, but the prompt doesn’t list it. If you callnlToSqlAmbiguityNotes()yourself, expect the shorter notes. - The prompt’s tenant-policy block names only the tables the prompt shows (#771). It listed every table in your tenant policy, including tables marked
tracked: falseand, under retrieval, tables retrieval left out. A root whose table isn’t shown now appears by its label and placeholder only. A join path through a tracked table that retrieval left out is still printed, with that table’s name, since the shown table needs it. A scoped table whose only join path runs through an untracked table loses that path, and the prompt tells the model not to read it; the tenant check still requires its predicate. - A schema with a tenant policy gets
500 tenant_scope_not_configuredfromaskdb-http, not500 internal_error(#716). The server still can’t set a tenant scope, so it still returns no SQL and never calls the model. If your client matchedinternal_errorfor this case, match the new code. - Generated SQL that advances a sequence is refused (#825). On MySQL and MariaDB,
NEXTVAL(…)andSETVAL(…)now fail the read-only check withSQL_FORBIDDEN_FUNCTION, and so doesNEXT VALUE FOR …on every dialect: MariaDB and SQL Server advance the sequence, and the change isn’t rolled back with your transaction.PREVIOUS VALUE FORand MariaDB’sLASTVAL()only read and still pass, as does a column namednextval. askdb-httpreads the working directory’s.envfirst, and never one insidenode_modules(#839). In an npm install it used to readnode_modules/.envbefore./.env. An install now reads only the working directory’s.env, so if you kept your settings innode_modules/.envor the package folder, move them to the working directory’s.- A server schema that doesn’t load answers
500 generation_not_configuredfromaskdb-http, not400 schema_parse_error(#840), such as ahost.schemaPaththat doesn’t exist. The message no longer includes the server’s path; the server log has the full error, witherrNameSchemaLoadError. A requestschemaJsonthat doesn’t load still answers400 schema_parse_error. If your client matchedschema_parse_errorfor a server-side schema problem, matchgeneration_not_configured.
1.0.0-beta.49
Section titled “1.0.0-beta.49”Released with askdb 1.0.0-beta.49, @askdb/config 1.0.0-beta.18, @askdb/core 1.0.0-beta.49, @askdb/http-api 1.0.0-beta.49, @askdb/studio 0.2.0-beta.42, @askdb/introspect 0.3.0-beta.23, @askdb/postgres 0.2.0-beta.25, @askdb/mysql and @askdb/sqlite 0.1.0-beta.24, and @askdb/sqlserver 0.1.0-beta.25. Other packages that got a new version list what changed in their CHANGELOG.
Security: upgrade for GHSA-wjgj-mf9h-6g68
Section titled “Security: upgrade for GHSA-wjgj-mf9h-6g68”With tenantSqlMode: "sql-only" on the built-in postgres or cockroachdb dialect, ask() could return SQL whose tenant scoping no longer held once the tenant IDs were inlined, because it did not re-check the rendered statement. A model reply of a particular shape could then read rows belonging to tenants outside the caller’s scope, and ask() returned it as passing its checks (GHSA-wjgj-mf9h-6g68). ask() now runs the read-only check on the sql it returns, as bindPreparedQuery() already did, and refuses a statement that reads differently once rendered with SqlValidationError.
- Upgrade
@askdb/core,askdband@askdb/http-apito 1.0.0-beta.49, and@askdb/studioto 0.2.0-beta.42. These pin@askdb/coreexactly, so check withnpm ls @askdb/coreorpnpm why @askdb/corethat no older copy remains under them.@askdb/clientruns your app’s own@askdb/core. - The default
tenantSqlMode: "sql-params"was never affected — the default since 1.0.0-beta.46, it binds tenant IDs as driver parameters that never enter the SQL text. If you can’t upgrade at once, use it (set it explicitly on 1.0.0-beta.45 or earlier). - Enforce tenant isolation in the database (for example Postgres row-level security), as Run generated SQL safely recommends. It limits the rows a statement can read regardless of its text.
Handle the new SensitiveScopeIssue members
Section titled “Handle the new SensitiveScopeIssue members”The sensitive check now fails closed on two Postgres forms it can’t read, with the new SensitiveScopeIssue members UNICODE_ESCAPED_IDENTIFIER (a name such as U&"p\0061ssword") and STRING_CONTINUATION (an E'…' string continued on a later line) (#707, ADR 0012). They apply whenever the SQL may be read as Postgres, a custom AskDialect included, and come back in sensitiveGuardrail.unresolvedScope with passed: false. With sensitiveGuardrailMode: "strict", ask() and validateSensitiveReferences() throw SensitiveReferenceError.
- A
switchover everySensitiveScopeIssueneeds cases for both. - Built-in dialects reject the same SQL earlier in the read-only check (
SQL_UNICODE_ESCAPED_IDENTIFIER,SQL_STRING_CONTINUATION), so this mainly changes a customAskDialectand direct calls.
Handle SQL_ESCAPED_BYTE_STRING for escaped b'…' byte strings
Section titled “Handle SQL_ESCAPED_BYTE_STRING for escaped b'…' byte strings”CockroachDB reads a b'…' byte string (lower-case b) with backslash escapes, and AskDB’s checks don’t, so a byte string with a backslash or a continuation could hide code from them (#730, ADR 0012). The checks now refuse it:
- The read-only check throws
SqlValidationErrorwith the newSqlValidationRuleCodeSQL_ESCAPED_BYTE_STRING, on Postgres, CockroachDB and a dialect id that is no built-in engine. So doask(),bindPreparedQuery()and Studio’s Execute. - The tenant check reports the new
TenantGuardrailRuleCodeESCAPED_BYTE_STRING, withtableId: "", whenever the SQL may be read as Postgres, a customAskDialectincluded, which skips the read-only check. Your policy’senforcementdecides whether it throws. - The sensitive check reports the new
SensitiveScopeIssueESCAPED_BYTE_STRINGinunresolvedScope, withpassed: false, under the same dialects as the tenant check, and in avalidateSensitiveReferences()call with no dialect. - A
switchover any of these unions needs a case for the new member. - Write the bytes as a hex string cast to
bytea('\x00'::byteainstead ofb'\x00'), which Postgres and CockroachDB both read as bytes. Ab'…'string with no backslash, and aB'…'bit string, pass.
See the troubleshooting entry.
Other changes you may notice in beta.49
Section titled “Other changes you may notice in beta.49”askdb initand Studio’s guided setup install with pnpm in a new pnpm project (#750). With no lockfile, both used npm, which refuses a project thatpnpm initstarted (EBADDEVENGINES). They now readpackage.json’spackageManagerfield, thendevEngines.packageManager, then the tool that ran them (pnpm dlx,yarn dlx,npx,bunx), before falling back to npm. A project with a lockfile installs with that lockfile’s manager, as before. Seeaskdb initfor the order.askdb initinstalls on Windows (#668). Its install always failed there, because Node refuses to run a package manager’s.cmdshim without a shell.- The install command
askdb initprints names the package manager it picked. At a workspace root, or with nopackage.json, it printednpm install --save …. It now prints that manager’s command, with--allow-build=@prisma/enginesfor a Prisma project on pnpm 10.4 or later. A script that matched the old text needs updating. @askdb/config/scaffoldis experimental (ADR 0030). It now also exports the package-manager helpersaskdb initand Studio share, andplanAskDbScaffold, which plans a new project’s config,.env.exampleand installs for both (#711). A minor release of@askdb/configmay change it. Your ownaskdb.config.tsnever imports it.askdb initchecks the env var names and paths you pass.askdb init --connection-env my-urlwroteenv("my-url"), a name a shell can’texport. A name for--connection-env,--ai-key-env,--ai-model-envor--pgvector-envmust now use uppercase letters, digits and underscores, and not start with a digit, and a path must not contain control characters. Otherwiseinitexits 1 before it writes anything, with an error that names the flag. Seeaskdb init.askdb init --database prismaalso installsaiandzod(#760):aiat the newest major@askdb/coreaccepts, andzodat core’s range. Under Yarn Plug’n’Play,askdb introspect --engine prismafailed without them (@askdb/core tried to access ai). Like a database driver,initnow installs them only when yourpackage.jsondoesn’t list them yet: a project onai@^6keeps it, andinitno longer re-adds a driver you already list. Studio’s guided setup installs them too when the project doesn’t have them..env.exampleleaves every value empty.askdb initwrote each connection URL’s format as its value (DATABASE_URL=postgresql://<USERNAME>:…), and Studio wroteNAME= # purpose, which Docker’s--env-filereads as the value. Both now write a# purposecomment above each emptyNAME=, database variables first, with a connection URL’s format in the comment.- Studio’s guided setup writes the same files as
askdb initfor the same answers. Its config’srag.storeConfigspans several lines, asinit’s does; the config loads the same. The variables it lists come ininit’s order, database variables first, and say what each one is for, such aspostgres connection URL, for introspection and Studio execute. It now accepts an env var name that starts with_. - A connector from
createConnector()returns theschemaIdyou pass inConnectorConfig(#775). Only the Prisma adapter passed it on, so Postgres, MySQL, SQLite and SQL Server returned aSqlSchemawithschemaId: "introspected". If your code readsSqlSchema.schemaIdfrom these connectors, it now gets the ID you set.schema.jsondoesn’t change: it takes its ID fromRenderOptions.schemaId, soaskdb introspectand Studio write the same files. askdb init --connection-env <name> --studio-executepoints Studio execute at that variable (#777). It wrotestudio.execute.databaseUrl: env("DATABASE_URL")whatever--connection-envnamed, so execute ran against another database unlessDATABASE_URLwas also set. A config written by an earlierinitkeepsDATABASE_URL; change it to your variable if it differs.- A model call that fails before saying why now names the reason (#786). When the model’s server refuses the connection, such as a local OpenAI-compatible server that isn’t running, reached through
localhost,SqlGenerationError’s message and theaskdb.pipeline.failedlog ended in a blank:Model call failed: … Cannot connect to API:. They now end with each refused address, such asECONNREFUSED [::1]:11434; ECONNREFUSED 127.0.0.1:11434, built from each error’s code, address and port (or its name when it has no code), never from text that could quote a URL or key. A message that already gives its reason is unchanged.
1.0.0-beta.48
Section titled “1.0.0-beta.48”Released with @askdb/client 1.0.0-beta.11, @askdb/config 1.0.0-beta.17 and @askdb/core 1.0.0-beta.48. Other packages that got a new version list what changed in their CHANGELOG.
Fix config values of the wrong type, which now fail to load
Section titled “Fix config values of the wrong type, which now fail to load”@askdb/config now checks each setting once, when the config loads, and builds every runtime value from your config alone (#711). A value of the wrong type fails to load in every tool that loads the config, with an error that names the file and the config key.
A boolean setting must be true or false. logging.logStdout and modes.omitSensitiveFromPrompt now fail on any other value, as studio.execute.enabled, studio.execute.useIntrospectionConnection and httpApi.allowSchemaOverride already did. A string here was read wrong before, without a warning:
| In your config | Before | Now |
|---|---|---|
logging: { logStdout: "true" } | Loaded, and didn’t copy the logs to stdout: only true turned it on. | Fails: AskDB config at /app/askdb.config.ts: logging.logStdout must be a boolean (got "true"). |
modes: { omitSensitiveFromPrompt: "false" } | Loaded, and silently turned omission on: any non-empty string counted as true. | Fails: AskDB config at /app/askdb.config.ts: modes.omitSensitiveFromPrompt must be a boolean (got "false"). |
To read one from the environment, compare the string: omitSensitiveFromPrompt: env("OMIT_SENSITIVE") === "true".
A string setting must be a string, or null for unset, such as logging.logFile, host.schemaPath or a databaseUrl. Any other value fails, false and 0 included. Before, AskDB ignored false or 0 for a few keys, such as logging.logFile and dev.mockSql, and stopped the load with a TypeError that named no key for the rest. Write undefined or null to leave a string setting unset.
Code that reads or installs the runtime config may need a change too:
-
getAskDbRuntimeConfig().logging.levelisAskDbLogLevel | undefined, andmodes.askdbModeisAskDbModeV1 | undefined, notstring. Code that builds anAskDbRuntimeConfigby hand with astring, for example in a test, no longer typechecks. Type the value with the union, or cast one you know is valid:import type { AskDbLogLevel, AskDbModeV1 } from "@askdb/config";const level: AskDbLogLevel = "debug"; // was: const level: string = "debug"const modes = { askdbMode: mode as AskDbModeV1, omitSensitiveFromPrompt: false }; -
setAskDbRuntimeForTests()takes anAskDbConfig, the object you write inaskdb.config.ts, and installs it asbootstrapAskDbEnv()installs a loaded file. ReplacesetAskDbRuntimeForTests({ structured, flat: flattenAskDbConfig(structured) })withsetAskDbRuntimeForTests(structured). If a test put a setting only inflat, set it in the config instead, such asdev: { mockSql }in place ofASKDB_MOCK_SQL. -
createAskDb()reads the default schema fromhost.schemaJson, thenhost.schemaPath, and nowhere else. A runtime config built by hand withASKDB_SCHEMA_JSONorASKDB_SCHEMA_PATHinai.aiEnvand nohostblock now throwsSchemaNotConfiguredError: setstructured.hostinstead. A config loaded from a file works as before.
Run unboundSql with the tenant IDs in params under "sql-only"
Section titled “Run unboundSql with the tenant IDs in params under "sql-only"”tenantSqlMode now governs sql only (#731). Under "sql-only", ask() used to inline the tenant IDs into unboundSql and leave them out of params, while bindPreparedQuery() and askdb.bind() bound them. Now ask() binds them too: unboundSql holds driver markers for the tenant IDs, and params holds their values, as in "sql-params" mode. sql is unchanged, with the IDs inlined as literals.
// ask({ …, tenantScope: { access: { kind: "ids", tenantRoot: "table:public.agencies", ids: ["42"] } }, tenantSqlMode: "sql-only" })result.sql // SELECT * FROM orders WHERE agency_id = '42' AND status = 'open' (unchanged)result.unboundSql // SELECT * FROM orders WHERE agency_id = '42' AND status = $1result.params // ["open"]result.unboundSql // SELECT * FROM orders WHERE agency_id = $2 AND status = $1result.params // ["open", "42"]If you run unboundSql with params, you need no change: the pair still returns the same rows. If you run unboundSql with your own array of business values, switch to params before you upgrade, and check every place that runs unboundSql. With your array, the values no longer line up with the markers: the tenant markers come first in source order on ? dialects, so your first business value can land on a tenant marker. The drivers AskDB documents (pg, mysql2, mssql, better-sqlite3) refuse the short array, so the query fails. node:sqlite doesn’t check the number of values it binds: it binds them in order and the rest as NULL, which can return another tenant’s rows. Always pass params as returned. If you need one SQL string with the IDs inlined, use sql.
Handle SQL_STRING_CONTINUATION for continued E'…' strings
Section titled “Handle SQL_STRING_CONTINUATION for continued E'…' strings”On Postgres, CockroachDB, and a dialect id that is no built-in engine, validateSelectSql() now rejects an E'…' string continued on a later line (E'a', a newline, then 'b'), and so do ask(), bindPreparedQuery() and Studio’s Execute. It throws SqlValidationError with the new rule code SQL_STRING_CONTINUATION (#703, ADR 0012). Postgres reads the continuation with backslash escapes and the checks didn’t, so code after it could pass as a string.
- The tenant check refuses the same SQL with the new
TenantGuardrailRuleCodeSTRING_CONTINUATIONandtableId: "", whenever the SQL may be read as Postgres, a customAskDialectincluded. Your policy’senforcementdecides whether it throws or warns. - A
switchover everySqlValidationRuleCodeorTenantGuardrailRuleCodeneeds a case for the new code. - If you store SQL that continues an
E'…'string and rebind it, the rebind now rejects it. Join the parts with||instead.
See the troubleshooting entry.
Other changes you may notice in beta.48
Section titled “Other changes you may notice in beta.48”- A malformed schema markdown file reports the same error on every load (#742). Before, when one process loaded the same malformed front-matter YAML in
tables/*.md,concepts.mdortenant-policy.mda second time, it reportedInvalid <kind> front-matter(missing fields) instead ofMalformed <kind> front-matter YAML. Loading failed both times.@askdb/corealso no longer keeps every schema markdown file it reads or writes in memory for the life of the process, so a server that accepts per-request schemas (httpApi.allowSchemaOverride) no longer grows with each new one. - A schema markdown body that starts with a
---horizontal rule survives a save (#742). Before,writeTableMarkdown(),writeConceptsMarkdown()andwriteTenantPolicyMarkdown(), and the Studio saves that use them, read the rule as front-matter: the body was emptied, and the file then failed to load. - Schema markdown front-matter accepts YAML only (GHSA-qqx9-qpw2-wcqg). A
tables/*.md,concepts.mdortenant-policy.mdfile whose front-matter isn’t YAML now fails to load withSchemaParseError(Malformed <kind> front-matter YAML). Front-matter that opens with a plain---, as Studio and@askdb/enrichwrite it, loads as before. - The tenant check reads a dialect id that is no built-in engine and sets
backslashEscapes: trueboth ways when it looks forU&"…"names (#703): with and without backslash escapes, reporting a name either reading finds.
1.0.0-beta.47
Section titled “1.0.0-beta.47”Released with @askdb/config 1.0.0-beta.16 and @askdb/core 1.0.0-beta.47. Other packages that got a new version list what changed in their CHANGELOG.
Replace the removed DEFAULT_RAG_FILE_BASE_PATH import
Section titled “Replace the removed DEFAULT_RAG_FILE_BASE_PATH import”@askdb/config no longer exports DEFAULT_RAG_FILE_BASE_PATH (#676). Nothing read it, and its value, ./askdb/rag, was never the file store’s default. If your app imports it, it no longer typechecks or loads.
| Package | Removed export | Use instead |
|---|---|---|
@askdb/config | DEFAULT_RAG_FILE_BASE_PATH | Your own path. With rag.storeConfig.file.basePath unset, the CLI and Studio use <schema dir>/schema, as before. |
Remove the error codes nothing threw
Section titled “Remove the error codes nothing threw”Four codes are gone from @askdb/core’s error-code types, because none of AskDB’s own checks reported them (#682, ADR 0033): INCOMPATIBLE_JOIN_SCOPES and CROSS_TENANT_WITHOUT_GLOBAL from TenantGuardrailRuleCode, and INVALID_NAME and RESERVED_NAME from QueryParameterRejectionReason. Only code that names them breaks, and TypeScript reports where: delete those case branches and comparisons. A custom AskDialect that reported one of the two tenant rules in tenantGuardrail.warnings reports MISSING_TENANT_PREDICATE (a table read without its tenant predicate) or UNPROVABLE_SCOPE instead.
A global tenant scope whose reason is missing, isn’t a string, or is blank now throws TenantScopeError with GLOBAL_WITHOUT_REASON, not INVALID_SCOPE_SHAPE. A blank reason, such as " ", passed before. Give every global scope a reason that says why it sees every tenant, and match GLOBAL_WITHOUT_REASON where you handled INVALID_SCOPE_SHAPE for it.
Handle SQL_UNICODE_ESCAPED_IDENTIFIER for Unicode-escaped names
Section titled “Handle SQL_UNICODE_ESCAPED_IDENTIFIER for Unicode-escaped names”On Postgres and CockroachDB, validateSelectSql() now rejects a Unicode-escaped identifier such as U&"set\005fconfig", and so do ask(), bindPreparedQuery() and Studio’s Execute. It throws SqlValidationError with the new rule code SQL_UNICODE_ESCAPED_IDENTIFIER (#692, ADR 0012). Postgres decodes the escapes, which could spell a blocked function, such as set_config, a scoped table (U&"\006Frders" reads orders) or a sensitive column, and the checks didn’t decode them.
- The tenant check refuses the same names, in any position, with the new
TenantGuardrailRuleCodeUNICODE_ESCAPED_IDENTIFIERandtableId: ""(#704). It applies whenever the SQL may be read as Postgres, a customAskDialectincluded, which skips the read-only check. Your policy’senforcementdecides whether it throws or warns. - A
switchover everySqlValidationRuleCodeorTenantGuardrailRuleCodeneeds a case for the new code. - If you store SQL with a
U&"…"name and rebind it, the rebind now rejects it. Write the name in plain double quotes ("注文数") instead.
See the troubleshooting entry and Safety boundaries.
Return only queries from a custom AskDialect under a tenant policy
Section titled “Return only queries from a custom AskDialect under a tenant policy”With a tenant policy and any scope but global, the tenant check now refuses a statement that doesn’t start with SELECT or WITH, under every dialect (#698). Before, a custom AskDialect could return a DO block, CALL, EXPLAIN or SHOW, and the check passed it: it reads a DO body as a dollar-quoted string, so it saw no tenant-scoped table, and the block could read every tenant’s rows. Now ask() throws TenantGuardrailError under enforcement: strict, and returns a NOT_SELECT_OR_WITH warning in tenantGuardrail under warn. Each statement after a ; counts too, and on SQL Server each one a verb such as EXEC starts with no ;. A leading comment or parenthesis doesn’t hide the first keyword. The same check runs when you rebind a stored template with bindPreparedQuery() or askdb.bind(), so a template with such a statement is refused there too (under warn, unless you pass acceptWarnings: ["tenant"]).
- If your custom
AskDialectreturns queries, you have nothing to change. Built-in dialect ids andDialectSpecs already reject other statements in the read-only check (SQL_NOT_SELECT_OR_WITH). - If it returns another statement on purpose for a schema with a tenant policy and a scope other than
global,ask()no longer returns it understrict. Generate aSELECTorWITHquery instead. - Code that handles every
TenantGuardrailRuleCode, such as an exhaustiveswitch, needs a case forNOT_SELECT_OR_WITH.
Expect UNCHECKABLE_FUNCTION for calls the tenant check can’t read through
Section titled “Expect UNCHECKABLE_FUNCTION for calls the tenant check can’t read through”With a tenant policy and any scope but global, the tenant check now refuses a function whose reads it can’t see, wherever the SQL names it, under every dialect (#702). That is a function that runs a query passed as a string (query_to_xml, ts_stat, crosstab), reads a table named in a string (table_to_xml, schema_to_xml, connectby), runs SQL over another connection (dblink, OPENQUERY), or sets a value row-level security may read (set_config). The full list is in the tenant policy contract. Before, the check passed SELECT table_to_xml('orders', true, false, '') under the built-in Postgres dialect, and it read every tenant’s orders. Now ask() throws TenantGuardrailError under enforcement: strict, and returns an UNCHECKABLE_FUNCTION warning under warn. bindPreparedQuery() and askdb.bind() refuse a stored template with such a call too (under warn, unless you pass acceptWarnings: ["tenant"]).
- A pivot the model writes with
crosstabis refused under a tenant policy, because the check can’t read the querycrosstabruns. Re-ask the question: a pivot written withCASEorFILTERpasses, with the tenant predicate in itsWHEREclause. - A column, alias or parameter named like one of these functions (
crosstab,set_config) is refused too, because the check matches a listed name anywhere in the SQL. If your schema has such a column, every query that names it is refused under a tenant policy: rename the column, or expose it through a view under another name. - Code that handles every
TenantGuardrailRuleCode, such as an exhaustiveswitch, needs a case forUNCHECKABLE_FUNCTION.
Other changes you may notice in beta.47
Section titled “Other changes you may notice in beta.47”httpApi.listen.portis checked when the config loads (#676). A value that isn’t an integer from 0 to 65535, or a string holding one, fails withaskdb.config: invalid httpApi.listen.port, in every tool that loads the config. Before, the HTTP server passed such a value straight tolisten(). A value that reads asNaN, such asNumber(env("PORT"))withPORTunset, now counts as unset: the server uses port 3000 instead of failing to start.rag.storeConfig.file.autoFlushgives a deprecation warning when the config loads (#676). Nothing ever read it, because the file store writes its files after every change. Remove it.logging.logFilestays open for the life of the process (#693).createAskDbLogger()opens each log file once and shares it across loggers, so the HTTP API server no longer leaks a file descriptor on everyPOST /ask. After a rename or delete, logs keep going to the old file: rotate it withcopytruncate, or restart the process after a rotate.
1.0.0-beta.46
Section titled “1.0.0-beta.46”Released with @askdb/client 1.0.0-beta.9, @askdb/config 1.0.0-beta.15, @askdb/enrich 0.2.0-beta.17, @askdb/introspect 0.3.0-beta.20, @askdb/mysql 0.1.0-beta.21, @askdb/prisma 0.2.0-beta.20, @askdb/sqlite 0.1.0-beta.21, @askdb/sqlserver 0.1.0-beta.22 and @askdb/studio 0.2.0-beta.39. Other packages that got a new version list what changed in their CHANGELOG.
Replace imports of removed exports
Section titled “Replace imports of removed exports”These exports are gone, because nothing outside their own package used them and no page documented them (#356, #275, ADR 0031). If your app imports one, it no longer typechecks or loads. Replace each one:
| Package | Removed export | Use instead |
|---|---|---|
@askdb/core | formatSchemaForPrompt(schema, options) | formatSchemaForNlToSql(schema, options).ddl, which returns the same string |
@askdb/config | DEFAULT_LOCAL_POSTGRES_URL | Your own local connection string |
@askdb/enrich | parseListInput(value) | value.split(",").map((s) => s.trim()).filter(Boolean) |
@askdb/enrich | formatList(list) | list?.join(", ") ?? "" |
@askdb/enrich | hasAnyColumnDescribable, isEnumCandidate | Nothing |
@askdb/mysql | MYSQL_CATALOG_SQL | Nothing: createMysqlConnector() runs the catalog queries |
@askdb/sqlite | SQLITE_CATALOG_SQL | Nothing: createSqliteConnector() runs the catalog queries |
@askdb/sqlserver | SQLSERVER_CATALOG_SQL | Nothing: createSqlServerConnector() runs the catalog queries |
@askdb/postgres keeps POSTGRES_TEMPLATE_BUNDLE, because askdb introspect --from-export has you run its templates against the database by hand first. askdb introspect templates --engine postgres prints them.
Run tenant-scoped SQL with its tenantParams
Section titled “Run tenant-scoped SQL with its tenantParams”ask() now binds tenant IDs as driver parameters by default (tenantSqlMode: "sql-params", #231, ADR 0024). If you pass tenantScope without tenantSqlMode, result.sql holds your dialect’s driver markers ($1, ? or @p0) where the tenant IDs used to be inlined, and the IDs are in result.tenantParams. Run each SQL form with its own params array:
// Beforeawait conn.execute(result.sql);
// Afterif (result.unboundSql && result.params) { await conn.execute(result.unboundSql, result.params); // params already holds the tenant IDs} else { await conn.execute(result.sql, result.tenantParams ?? []);}- Never concatenate
paramsandtenantParams. Running the newsqlwithout its params fails at the driver (Postgres:there is no parameter $1; better-sqlite3:Too few parameter values; MySQL: a syntax error). It never returns other tenants’ rows. - To keep the IDs inlined, set
tenantSqlMode: "sql-only". - A custom
AskDialect, or aDialectSpecwhoseidisn’t built in, has no default. With a tenant scope and notenantSqlMode,ask()throwsTenantScopeError(TENANT_SQL_MODE_REQUIRED) before the model call. Set"sql-params"if your driver binds$1-style markers, or"sql-only": on SQL Server,$1is a money constant. A value other than these two throws the same error on every dialect. @askdb/client’saskdb.ask()forwards toask(), so it changes the same way. Studio’s Playground now starts insql-params.
Executing the result describes both SQL forms.
Use sql-params for tenant IDs outside printable ASCII
Section titled “Use sql-params for tenant IDs outside printable ASCII”Wherever AskDB inlines a tenant ID, the ID must now be printable ASCII without a backslash, on every dialect (#360, #371, ADR 0024). That covers tenantSqlMode: "sql-only", in ask() and in a rebind. Any other ID throws TenantScopeError (UNESCAPABLE_TENANT_ID). Before, such an ID could end its literal early on a server whose string settings or connection charset differ from the dialect’s, and the query lost its tenant filter.
- Use
tenantSqlMode: "sql-params", the default inask()and in a rebind, which passes any ID as a driver parameter. - A control character in a tenant ID now throws this
TenantScopeErrorinstead ofQueryParameterError.
Update your bindPreparedQuery() and askdb.bind() calls
Section titled “Update your bindPreparedQuery() and askdb.bind() calls”A rebind changes three ways in this release. Update each call site once:
const hostSpec = { ...POSTGRES_DIALECT, backslashEscapes: true }; // only if you pass your own spec to ask()
// Beforeconst rebound = bindPreparedQuery(stored, { state_name: "Utah" }, { schema, tenantScope });await pool.query(rebound.sql);
// Afterconst rebound = bindPreparedQuery(stored, { state_name: "Utah" }, { schema, tenantScope, dialect: hostSpec, // the spec you gave ask(), or a built-in id such as "postgres" for a template stored before beta.46});await pool.query(rebound.sql, rebound.tenantParams ?? []); // or: pool.query(rebound.unboundSql, rebound.params)- Run the rebound
sqlwithtenantParams(#664, ADR 0024). A rebind now takestenantSqlMode, with the same default asask(). With a tenant scope, itssqlkeeps the business values inlined and holds the tenant IDs as driver markers, and the IDs are in the newtenantParams. Runningsqlwithout them fails at the driver; it never returns other tenants’ rows. To keep inlined IDs, passtenantSqlMode: "sql-only". Without a tenant policy,sqldoesn’t change. - Tenant IDs outside printable ASCII, such as
agência, now rebind in the default mode. In"sql-only", they throwUNESCAPABLE_TENANT_ID, as an ID with a backslash does. - Pass
dialectwhenask()ran with your ownDialectSpec(#660, ADR 0032). A rebind now checks and renders under the dialect rulesask()ran with, whichask()records inpreparedQuery.dialectRules. Before, it always used the built-in spec, so yourbackslashEscapes,blockedFunctions,extraForbiddenKeywordsandextraValidatewere dropped at rebind. A"host"template, even from an unchanged spread, throwsQueryParameterError(DIALECT_SPEC_REQUIRED) withoutdialect. - A template stored before this release, or built by hand, has no
dialectRules, so it throws the same error until you passdialect. Ifask()ran with a built-in id, the id is enough, such asdialect: "postgres". - A
dialectwith anotheridthan the template’s throwsQueryParameterError(DIALECT_UNSUPPORTED). - An app that loads two copies of
@askdb/core, such as ESM beside CJS, gets"host"templates, because the other copy’s constants are different objects. Passdialect. askdb.bind()takes the sametenantSqlModeanddialectoptions.
Fix a strict tenant policy that doesn’t match schema.json
Section titled “Fix a strict tenant policy that doesn’t match schema.json”loadSchema() now checks every ID in tenant-policy.md against schema.json, and a policy with enforcement: strict must match it (#332, ADR 0023). It throws SchemaParseError when the policy names a column or foreign key that schema.json doesn’t have, has a cycle in hierarchy or parent, or names a root in parent, hierarchy, scopeThrough or a polymorphic mapping that roots doesn’t declare. Before, the policy loaded, and only Studio’s Tenancy page showed the problems. The error names tenant-policy.md and each ID: update the policy to match schema.json. See When the policy and the schema disagree.
- Every caller gets the same underlying error:
loadSchema()throws theSchemaParseError,@askdb/clientwraps it inSchemaLoadErroras itscause, and the HTTP API answers 400schema_parse_error. Studio can’t open the workspace until you fix the file (#640). - Under
enforcement: warn, the policy still loads, with the problems inschema.tenantPolicy.warnings.ask()now logs them once per loaded schema asaskdb.tenant.policy_warnings, through theloggeryou pass to it. - A table that
schema.jsondoesn’t have stays a warning in both modes. Its policy entry now reports only the table, not each of its columns.
Build enrichment targets from loadSchema()’s tables
Section titled “Build enrichment targets from loadSchema()’s tables”EnrichmentTarget.table and EnrichmentContext.neighbors now take NormalizedV2Table, the table type in loadSchema()’s result, instead of V2Table, the table as schema.json stores it (#667, ADR 0026). Before, a table read straight from schema.json lacked front-matter sensitivity. With omitSensitiveIdentifiersFromPrompt on, a column marked sensitive only in tables/*.md was named in the prompt, and a suggestion for it wasn’t refused. If your app passes such a table to suggestEnrichment() or buildEnrichmentUserPrompt(), it no longer typechecks. Build the target and context from loadSchema()’s result:
// Beforeconst table = schemaJson.tables.find((t) => t.id === tableId); // a V2Tableawait suggestEnrichment({ kind: "table-description", table }, { schemaId: schemaJson.schemaId }, model, deps);
// Afterimport { loadSchema, suggestEnrichment } from "@askdb/core";import { buildSuggestionContext, buildSuggestionTarget } from "@askdb/enrich";
const schema = loadSchema("./my-app.schema");const source = { scope: "table", tableId, field: "description" } as const;await suggestEnrichment(buildSuggestionTarget(schema, source), buildSuggestionContext(schema, tableId), model, deps);- If you build both with
@askdb/enrich’sbuildSuggestionTarget()andbuildSuggestionContext(), or use Studio, you have nothing to change. - A table you build by hand must set
sensitiveon the table, andsensitiveandprimaryKeyon every column. Mark sensitive each column thattables/*.mdmarks, and every column of a sensitive table, asloadSchema()does. loadSchemaFromJson()of a bareschema.jsontypechecks too, but has no front-matter flags. Load the schema directory or a bundle withloadSchema()instead.
Rename schema files to their exact names
Section titled “Rename schema files to their exact names”loadSchema() now throws SchemaParseError when a schema directory holds a file whose name differs from the expected one only in case, such as Tenant-Policy.md, Concepts.md, Tables/ or tables/orders.MD (#608, ADR 0027). Before, Linux skipped those files: a Tenant-Policy.md loaded the schema with no tenant policy, so ask() didn’t require a tenantScope, and nothing reported it. A tables/*.MD file was skipped on every OS, with every sensitive: true in it. The check now runs on every OS, so a directory that loaded on macOS or Windows with such a name throws there too.
- Rename each file the error names. The error says what to do, for example
rename Tenant-Policy.md to tenant-policy.md. When both spellings exist, which only Linux allows, it says to keep one. - A listed optional file that can’t be read now throws, including a symbolic link whose target is missing. Before, it loaded as if the file were absent.
- A renamed copy of
schema.jsonbeside a wrong-caseTenant-Policy.mdnow throws, as it already did besidetenant-policy.md. askdb bundleandbundleSchemaDirectory()refuse the same directories, and their read errors are nowSchemaParseErrors instead of raw Node errors. Studio can’t open such a workspace until you rename the file.- A path you pass is still read as given, so
loadSchema("<dir>/Schema.json")loads as before.
See the troubleshooting entry.
Install @askdb/prisma to introspect a Prisma schema
Section titled “Install @askdb/prisma to introspect a Prisma schema”askdb and @askdb/studio no longer install @askdb/prisma: it’s now an optional peer dependency (#259). So pnpm add askdb succeeds on pnpm 11, which refused Prisma’s unapproved @prisma/engines build and exited 1. If you introspect a Prisma schema, install @askdb/prisma next to askdb:
npm install @askdb/prismapnpm add @askdb/prismayarn add @askdb/prisma- On pnpm 11, approve the
@prisma/enginesbuild in the same command,pnpm add --allow-build=@prisma/engines @askdb/prisma, or runpnpm approve-buildsafterwards. - Without it,
askdb introspect --engine prismaexits 1 with a message that names the package, and Studio shows the same message instead of resyncing.askdblooks for@askdb/prismain its own install, then in the project it runs in, sonpx askdb introspectuses your project’s copy. askdb init --database prismainstalls it for you.- If you build your own connector registry,
createConnectorRegistry()also takes lazy entries ({ provider, load }) now, which the newregistry.load(provider)imports. A registry of adapters works as before.
See the Prisma guide.
Handle SQL_LOCKING_READ for locking reads
Section titled “Handle SQL_LOCKING_READ for locking reads”validateSelectSql, and so ask(), generateSelectSql() and bindPreparedQuery(), now refuses locking reads on every engine, with the new rule code SQL_LOCKING_READ (#319). Before, FOR SHARE, LOCK IN SHARE MODE and SQL Server’s WITH (UPDLOCK) passed, and your transaction then held their locks.
FOR UPDATEandFOR NO KEY UPDATEnow throwSQL_LOCKING_READinstead ofSQL_FORBIDDEN_KEYWORD. If your code matches the old rule code for them, match the new one.- Newly refused:
FOR SHARE,FOR KEY SHAREandLOCK IN SHARE MODEon every engine, and the SQL Server table hintsUPDLOCK,XLOCK,HOLDLOCK,PAGLOCK,TABLOCK,TABLOCKX,ROWLOCK,SERIALIZABLEandREPEATABLEREAD.NOLOCKandREADPASTstill pass. - On SQL Server, an unquoted name must be printable ASCII and can’t be one of those hint words. Quote it (
[Größe],[rowlock]), and qualify it when it’s the only thing inside parentheses (COUNT(t.[Größe])). The same applies to a dialect id that is no built-in engine. This narrows beta.45’s plain-code rule, which let Unicode letters through. - A
switchover everySqlValidationRuleCodeneeds a case forSQL_LOCKING_READ.
See the troubleshooting entry and Safety boundaries.
Replace a symbolic link at schema.json
Section titled “Replace a symbolic link at schema.json”Every AskDB write into a schema directory now refuses symbolic links (#617, ADR 0028). When schema.json is one, askdb introspect --out, renderToSchemaV2() and introspect() with render options throw Refusing to write schema.json outside the schema directory: "schema.json" is a symbolic link. Before, they wrote to the file the link pointed at. Replace the link with a regular file.
- Studio refuses a linked
playground-history.jsonthe same way. It leaves a linked.gitignoreas it is. - Writing
schema.jsonneeds write access to the output directory, because AskDB writes a temporary file there and renames it over the target. A hard link atschema.jsonis replaced instead of written through. The new file keeps the old one’s permission bits and group where it can, but not an access control list. Only root can give a file to another user, so on macOS and Linux a save by anyone other than the file’s owner makes the saver its owner. On Windows, the rename fails withEPERMwhile another process holdsschema.jsonopen; run the command again. - If you match
@askdb/enrich’s table-save error, it now readsRefusing to write tables/orders.md outside the schema directory: ….
Other changes you may notice in beta.46
Section titled “Other changes you may notice in beta.46”- Config files:
askdb.config.mjsand a plainexport default { … }now load, as the docs said (#264, #265). A config with noaiorintrospectionblock, no default export, or top-levelawaitfails with an error that names the file and what to change, instead of a bareTypeError. - Studio: a
studio.listen.portthat is set but isn’t an integer from 1 to 65535 now stops Studio with an error. Before, Studio bound port 5556 instead. A value that reads asNaN, such asNumber(env("STUDIO_PORT"))with the variable unset, still counts as unset. askdb introspect --outchecks an existingtenant-policy.mdagainst theschema.jsonit just wrote. On stderr, it prints what no longer matches, or, underenforcement: strict, that the schema no longer loads. The exit code stays 0, so read stderr. Understrict, fixtenant-policy.mdbefore anything loads the new schema. Underwarn, the schema loads, with the problems inschema.tenantPolicy.warnings.
1.0.0-beta.45
Section titled “1.0.0-beta.45”Released with @askdb/client 1.0.0-beta.8, @askdb/ai 0.1.0-beta.9, @askdb/config 1.0.0-beta.14, @askdb/enrich 0.2.0-beta.16, @askdb/introspect 0.3.0-beta.19 and @askdb/studio 0.2.0-beta.38. Every other package got a new version too; its CHANGELOG lists what changed.
Replace the removed @askdb/ai-* and @askdb/connectors packages
Section titled “Replace the removed @askdb/ai-* and @askdb/connectors packages”@askdb/ai-openai, @askdb/ai-azure, @askdb/ai-google, @askdb/ai-anthropic and @askdb/connectors are no longer published (#347). Their last versions stay on npm, so an existing install keeps working, but it gets no fixes. Uninstall the one you use, and install @askdb/ai and the AI SDK package for your provider instead:
npm install @askdb/ai @ai-sdk/openaipnpm add @askdb/ai @ai-sdk/openaiyarn add @askdb/ai @ai-sdk/openaiThen drop the old import:
import { openaiProvider } from "@askdb/ai-openai";const askdb = createAskDb({ config, providers: [openaiProvider] });const askdb = createAskDb({ config }); // or providers: ["openai"]
const ai = createAiRegistry([openaiProvider]);const ai = createAiRegistry(["openai"]);If you import from @askdb/connectors, import from @askdb/introspect and @askdb/introspect/kit instead. CONNECTOR_PROVIDERS is now BUILT_IN_CONNECTOR_PROVIDERS, and ConnectorProvider is now ConnectorProviderId. Removed packages lists each package’s last version and its replacement.
Pass the schema and tenant scope to bindPreparedQuery()
Section titled “Pass the schema and tenant scope to bindPreparedQuery()”bindPreparedQuery() now re-runs the guardrails on a stored template before it binds new values (#310, ADR 0010). It takes a required third argument with the schema, and it reads tenant IDs from a tenant scope instead of from values:
// Beforeconst rebound = bindPreparedQuery(result.preparedQuery!, { state_name: "Utah", ":tenant_agency_ids": authorizedAgencyIds,});
// Afterconst rebound = bindPreparedQuery( result.preparedQuery!, { state_name: "Utah" }, { schema, tenantScope }, // the same schema and the caller's scope, as for ask());- Without the third argument, it throws
QueryParameterError(MISSING_GUARDRAIL_CONTEXT). Without a tenant policy,{ schema }is enough. - A
:tenant_*key invaluesthrowsQueryParameterError(TENANT_VALUE_SUPPLIED). Pass the IDs intenantScope. Expand asubtreescope withexpandTenantScope()first. - A template with a
:tenant_*placeholder can’t be rebound under aglobalscope, or with a schema that has no tenant policy. Both throwTenantScopeError(UNRESOLVED_TENANT_PLACEHOLDER). - With a tenant policy whose
enforcementiswarn, a template that fails the tenant check is refused withTenantGuardrailError. To bind it anyway, passacceptWarnings: ["tenant"]. - Stored
PreparedQueryobjects keep working. The checks use the schema and scope you pass now, so a tightened policy applies at the next rebind.
With @askdb/client, call askdb.bind(prepared, values, { tenantScope }) instead. It supplies the client’s schema and expands a subtree scope for you. bindPreparedQuery() lists every error it throws.
Load a schema directory, not a renamed copy of its JSON file
Section titled “Load a schema directory, not a renamed copy of its JSON file”A JSON file inside a schema directory must be named schema.json (#330, ADR 0019). If you pass loadSchema() any other JSON file that has tenant-policy.md, tables/ or concepts.md beside it, such as prod-schema.json, it now throws SchemaParseError. Before, it loaded the file without the tenant policy and without the sensitive: true marks in tables/*.md. A bundle, a JSON file with bundled: true such as prod.bundle.json, still loads as before. To fix it, do one of these:
- Pass the directory to
loadSchema(). - Rename the file to
schema.json. - To load only the physical layer on purpose, pass the file’s content to
loadSchemaFromJson().
The schema.json name now matches in any case. On macOS and Windows, loadSchema("<dir>/Schema.json") used to drop the sibling tenant-policy.md, so ask() didn’t require a tenantScope. It now loads the whole directory: if that schema has a tenant policy, pass a tenantScope to ask().
SQL Server: handle SQL that the read-only check now refuses
Section titled “SQL Server: handle SQL that the read-only check now refuses”On SQL Server, validateSelectSql and ask({ dialect: "sqlserver" }) now refuse a second statement written without a ;, such as SELECT 1 SELECT 2 (#327). T-SQL doesn’t need a ; between statements, so this used to pass. SQL that passed before and now throws SqlValidationError:
SQL_FORBIDDEN_KEYWORDfor an unquoted name that is one of the new denied words, such asbegin,declare,if,while,print,throw,openorclose. Quote the name ([open]).SQL_MULTI_STATEMENTfor a secondSELECTthat noUNION,EXCEPTorINTERSECTjoins, a parenthesized query right after a complete value, alabel:, or code outside strings and quoted names that isn’t plain, such as a non-breaking space or a bare$.- The same checks for a dialect id that is no built-in engine, because its SQL must pass every engine’s checks.
If you validate SQL you didn’t get from ask(), check it against these rules. Fix common errors and Safety boundaries describe the check.
Set modes.askdbMode in config to allow bounded_results
Section titled “Set modes.askdbMode in config to allow bounded_results”@askdb/client now applies config modes.askdbMode, as the loosest mode a call gets (#279, ADR 0025). The CLI and the HTTP API follow the same rule:
- A per-call
bounded_resultsno longer loosens the config.askdb ask --mode bounded_results, a client call’smode: "bounded_results"and an HTTP request’smode: "bounded_results"now runschema_only, with no error, unlessaskdb.config.*setsmodes: { askdbMode: "bounded_results" }. The SQL doesn’t change today, becausebounded_resultsproduces the same SQL asschema_only. - The
askdb.run.startlog line from the CLI and the HTTP API no longer carriesmode. Read it fromaskdb.pipeline.mode, in the same log stream. - An unknown
modein a client call, or an invalidmodes.askdbModein a hand-builtAskDbRuntimeConfig, now throwsAskDbError.
Give an embedded HTTP server a config to load
Section titled “Give an embedded HTTP server a config to load”createAskDbHttpServer() from @askdb/http-api now loads its config when you create it, unless your process already called bootstrapAskDbEnv() (#280):
- With no config loaded yet, it loads
.envandaskdb.config.*fromprocess.cwd(). It throws when it finds noaskdb.config.*there, or can’t read an existing.env. Before, it started anyway and returned500 internal_erroron everyPOST /ask. If your config lives in another directory, callbootstrapAskDbEnv()first. - Omitted
portandhostoptions now fall back tohttpApi.listen.portandhttpApi.listen.host, then to3000and127.0.0.1. If your config setshttpApi.listen, the server now listens there.
Config loading describes the rule.
Replace a symbolic link at concepts.md
Section titled “Replace a symbolic link at concepts.md”@askdb/enrich saves no longer follow symbolic links (#372):
saveConcepts()throwsRefusing to write concepts.md outside the schema directory: "concepts.md" is a symbolic linkwhenconcepts.mdis one. Replace the link with a regular file in the schema directory. Studio’s tenant-policy save refuses a link attenant-policy.mdthe same way.- Saving
concepts.mdortenant-policy.mdneeds write access to the schema directory, because the save writes a temporary file there and renames it over the target. A writable file in a read-only directory now fails withEACCES.
Pass a loaded schema to buildSuggestionTarget() and buildSuggestionContext()
Section titled “Pass a loaded schema to buildSuggestionTarget() and buildSuggestionContext()”@askdb/enrich’s buildSuggestionTarget() and buildSuggestionContext() now take loadSchema()’s result instead of a Workspace (#625, ADR 0026). Before, they read the tables straight from schema.json, so a column marked sensitive in tables/*.md, or through a sensitive table, wasn’t tagged sensitive in the suggestion prompt. Load the schema and pass it where you passed the workspace:
// Beforeconst target = buildSuggestionTarget(workspace, source);const context = buildSuggestionContext(workspace, source.tableId);
// Afterconst schema = loadSchema(workspace.schemaDir); // from @askdb/coreconst target = buildSuggestionTarget(schema, source);const context = buildSuggestionContext(schema, source.tableId);The context no longer lists an untracked table (tracked: false) as a neighbor, since ask()’s prompt leaves it out too. suggestEnrichment() describes the prompt these build.
Other changes you may notice in beta.45
Section titled “Other changes you may notice in beta.45”ask()under aglobalscope throwsTenantScopeError(UNRESOLVED_TENANT_PLACEHOLDER) when the SQL still holds a:tenant_*placeholder, instead of returning it unfilled. Admin SQL without a placeholder is unaffected.- A placeholder that isn’t a lowercase name, such as
:Status, no longer resolves.bindPreparedQuery()throwsQueryParameterError(UNRESOLVED_PLACEHOLDER), andask()returns nounboundSqlorpreparedQueryfor that SQL. - An unknown
sensitiveGuardrailMode, such as"Strict"read from your own config, now refuses a sensitive reference, as"strict"does. Pass exactly"off","warn"or"strict". ask()logsaskdb.pipeline.failedbefore it throws a tenant or sensitive guardrail error, as it already did for a read-only failure. If you alert on that event, expect more of them.- Omitting sensitive identifiers withholds more text (#374). With
omitSensitiveIdentifiersFromNlToSqlPrompt: true, a description, alias, enum value, common query language block or retrieved chunk that names a sensitive column no longer reaches the model. If you build anNlToSqlSchemaFormatStatsobject by hand, such as a test double, set its newmentionDroppedCountfield (0keeps the old meaning). - Studio follows
modes.omitSensitiveFromPromptin the Playground and in Suggest (#601). With it on, the Playground’s prompt leaves out sensitive columns and text that names one, and Suggest on a sensitive column shows an error (400fromPOST /api/suggest) instead of a draft. See Tables and enrichment. - The enrichment suggestion prompt has a new format.
buildEnrichmentUserPrompt()lists the target table the wayask()’s prompt does (TABLE public.users, then lines such as- email text (NOT NULL) (sensitive)), so a test that compares the prompt text needs updating.suggestEnrichment()takes a newomitSensitiveIdentifiersFromPromptoption; pass yourmodes.omitSensitiveFromPrompt. With it on, a sensitive target column throwsSensitiveEnrichmentTargetError. - Results gain
verdict, which lists every finding of the SQL checks.tenantGuardrailandsensitiveGuardrailstay. See Guardrail verdict.
1.0.0-beta.44
Section titled “1.0.0-beta.44”Released with @askdb/client 1.0.0-beta.7, @askdb/ai 0.1.0-beta.8, @askdb/config 1.0.0-beta.13, @askdb/rag 0.2.0-beta.24, @askdb/enrich 0.2.0-beta.15, @askdb/introspect 0.3.0-beta.18 and @askdb/studio 0.2.0-beta.37. Every other package got a new version too; its CHANGELOG lists what changed.
Run Node 22.23.4 or newer
Section titled “Run Node 22.23.4 or newer”Every package’s engines.node is now >=22.23.4, up from >=22.12. better-sqlite3 13, which the @askdb/sqlite and @askdb/studio peer ranges allow, crashes on Node 22.12 and 22.13.
Add ai to your app’s dependencies
Section titled “Add ai to your app’s dependencies”ai (the Vercel AI SDK) is now a peer dependency of @askdb/core, with the range ^6.0.0 || ^7.0.51, so your app owns its version. Add it to your own package.json:
npm install aipnpm add aiyarn add ainpm and pnpm install a missing peer for you, but Yarn doesn’t. That includes installs that only introspect (@askdb/introspect, @askdb/prisma and the engine packages), because @askdb/core loads ai when it’s imported. @askdb/core accepts AI SDK 6 or 7. The config-driven path, @askdb/ai and @askdb/client, needs AI SDK 7.
Install the AI SDK package for your provider
Section titled “Install the AI SDK package for your provider”The OpenAI, Azure OpenAI and Foundry, Google, and Anthropic providers are now built into @askdb/ai, with a new gateway provider for the Vercel AI Gateway. Each provider’s SDK is an optional peer dependency (^4.0.0), so install the one you configure:
npm install @askdb/ai @ai-sdk/openaipnpm add @askdb/ai @ai-sdk/openaiyarn add @askdb/ai @ai-sdk/openaiWithout it, building the model fails with Provider 'openai' requires the optional peer dependency @ai-sdk/openai. Install it: npm i @ai-sdk/openai. The gateway provider needs no extra package. createAskDb({ config }) and createAiRegistry() with no arguments now register every built-in provider, so you can drop providers: [openaiProvider].
If you bundle your app with webpack 5, the build fails with Module not found for every @ai-sdk/* package you didn’t install. Install them, or list the ones you don’t use in externals, as Bundle an app that uses AskDB shows. esbuild needs no change.
Move model settings to ai.language and ai.embedding
Section titled “Move model settings to ai.language and ai.embedding”The ai config now separates provider connections from model choice (ADR 0016). ai.providerConfig holds only connections: keys, endpoints and Azure resources. The language model moves to ai.language, and the embedding model behind rag.embedder: "ai" moves to ai.embedding:
// Beforeai: { provider: "openai", providerConfig: { openai: { apiKey: env("OPENAI_API_KEY"), model: "gpt-4o-mini" } }, reasoning: { effort: "low" },},rag: { embedder: "openai", embedderConfig: { openai: { model: "text-embedding-3-small" } }, store: "pgvector", storeConfig: { pgvector: { databaseUrl: env("PGVECTOR_URL"), dimensions: 1536 } },},
// Afterai: { provider: "openai", providerConfig: { openai: { apiKey: env("OPENAI_API_KEY") } }, language: { model: "gpt-4o-mini", reasoning: { effort: "low" } }, embedding: { model: "text-embedding-3-small", dimensions: 1536 },},rag: { embedder: "ai", store: "pgvector", storeConfig: { pgvector: { databaseUrl: env("PGVECTOR_URL") } },},A config in the old shape still loaded in this release. Each old key was translated at load and reported once as a Node DeprecationWarning with code ASKDB_CONFIG_DEPRECATED, naming its new location, until beta.50 removed them. These old-shape configs failed to load, with an error that said what to change:
- A
rag.embedderConfig.openai.apiKeyor.baseUrlwhose embedding model comes from a provider other thanopenai,azure,foundryorgateway. Before, Studio sent that key to the language model’s provider. rag.embedder: "ai-sdk"with no embedding model, on a provider other thanopenai,azureorfoundry. Setai.embedding.model.rag.embedder: "ai-sdk"with Anthropic as the language model’s provider. Anthropic has no embeddings API: setai.embedding.providerto another provider.- Two different widths in
rag.embedderConfig.openai.dimensionandrag.storeConfig.pgvector.dimensions. - An old key next to its replacement:
ai.reasoningwithai.language.reasoning,ai.providerConfig.customwithai.providerConfig.<provider id>, orrag.embedderConfigwithai.embedding.
With rag.embedder: "ai", ai.embedding.model is required: AskDB no longer picks an embedding model for you. Replace the removed config keys maps every old key to its replacement.
Handle a config without a rag block
Section titled “Handle a config without a rag block”The rag block is now optional: without it, AskDB uses the mock embedder and the in-memory store. In TypeScript, AskDbConfig["rag"] is now optional, so code that reads config.rag or getAskDbRuntimeConfig().structured.rag needs a check. Read getAskDbRuntimeConfig().rag instead: its store and storeConfig are always set. A config that sets ai.embedding with no rag block fails to load.
Run askdb rag instead of askdb-rag
Section titled “Run askdb rag instead of askdb-rag”The RAG command line moved into the askdb package as askdb rag. @askdb/rag’s askdb-rag binary now prints the askdb rag command to run and exits 1, until 1.0.0-beta.50 removes it.
| Before | After |
|---|---|
askdb-rag index … | askdb rag index … (also query and setup-store) |
--api-key <key> | Removed. Put the key on a connection in ai.providerConfig. |
--embedder openai | --embedder ai, with rag.embedder: "ai" and an ai.embedding section in your config |
setup-store with no --dimensions | setup-store --dimensions <n>, your model’s vector width. It used to assume one. |
No --store, which meant the file store | --store file on every command, or rag.store: "file" in your config. askdb rag uses rag.store, which is "memory" when the config has no rag block, so index would build an index that ends with the process, and query would refuse it. |
askdb rag reads askdb.config.*, and its flags still win. The schema directory falls back to introspection.outputDir, and the store flags fall back to rag.store and rag.storeConfig. See askdb rag for every flag.
Re-index your RAG store
Section titled “Re-index your RAG store”The first index run after you upgrade re-embeds every chunk. Chunk ids are now scoped by schema (chunk:<schemaId>:<local-id>), the lock file is at version 2, and AI embedder ids are now ai-sdk:<provider>:<model>:<dimensions>, ending in default when no width is set.
-
If you create the pgvector table from your own migrations and never call
ensureSchema(), add the new column first. Use your table name if it isn’t the default:ALTER TABLE askdb_rag_chunks ADD COLUMN IF NOT EXISTS content_hash text; -
Run
askdb rag index, orbuildSchemaIndex()in your own code. It deletes that schema’s old-format chunk ids. Until you re-index,askdb rag queryrefuses an index thataskdb-rag --embedder openaibuilt. -
In Studio, rebuild an index that shows as stale. An index built without
ai.embedding.dimensionsshows as stale once.
Sensitive-column mentions are now matched across the whole schema (ADR 0017): a note in orders that says “match via users.ssn” is left out of the index by default. Some schemas exclude different chunks after upgrading, and the re-index picks that up. If you wrote your own vector store, implement hashesByPrefix, or describe() so the indexer can trust the lock file’s hashes: a store with neither re-embeds every chunk on each run. Implement idsBySchema too, so a re-index prunes chunks removed from the schema without touching other schemas. The @askdb/rag API describes both.
Rebuild schema bundles that have a tenant policy
Section titled “Rebuild schema bundles that have a tenant policy”askdb bundle now puts tenant-policy.md in the bundle. Bundles built with askdb 1.0.0-beta.43 or earlier left it out, so ask() loaded them as single-tenant and didn’t require a tenantScope. Rebuild every bundle whose schema directory has a tenant-policy.md, then check that loadSchema(bundlePath).tenantPolicy is defined.
Turn off omitSensitiveFromPrompt in config to allow per-call false
Section titled “Turn off omitSensitiveFromPrompt in config to allow per-call false”In @askdb/client, config modes.omitSensitiveFromPrompt: true is now a floor. A per-call omitSensitiveIdentifiersFromNlToSqlPrompt: false no longer turns it off. @askdb/http-api does the same: a request’s omitSensitiveFromPrompt: false can’t override the config. If some calls need sensitive names in the prompt, leave the config false and pass true on the calls that need omission.
Update HTTP API clients for the new status codes
Section titled “Update HTTP API clients for the new status codes”POST /ask on @askdb/http-api changed:
- A failed model call returns
502 sql_generation_errorwith a generic message. Before, it returned400with the provider’s own error text. - Unknown errors return
500 internal_error.422 guardrail_violationis reserved for guardrail errors. omitSensitiveFromPromptandexplainmust be JSON booleans. A string or number, such as"false", returns400 bad_request.- A request that sends
schemaJsongets403 schema_override_disabled, unless the server setshttpApi.allowSchemaOverride: true. - A model call that runs past
httpApi.requestTimeoutMs(default60000) returns502.
See the HTTP API reference for every error code.
Remove the trailing ; before you wrap the SQL
Section titled “Remove the trailing ; before you wrap the SQL”ask() now keeps the model’s single trailing ; in the SQL it returns (ADR 0018). unboundSql, preparedQuery.namedSql and a rebound query keep it too. If you wrap the SQL in another statement, remove the ; first, or the result is a syntax error on every engine:
const statement = sql.replace(/;$/, "").trimEnd();const cappedSql = `SELECT * FROM (${statement}) AS askdb_q LIMIT 1000`;This wrapper works on Postgres and SQLite only. Cap the rows a query returns shows a cap that works on every engine and keeps the query’s ORDER BY.
Other changes you may notice in beta.44
Section titled “Other changes you may notice in beta.44”- dotenv 18:
bootstrapAskDbEnvno longer decrypts a.env.vaultfile whenDOTENV_KEYis set. Load those values another way. - MySQL: a connection URL with no database name now throws, instead of introspecting an empty schema. A foreign key into a database you don’t introspect is now left out, with a
cross_database_fkwarning. - Postgres: foreign keys to and from partitions of a partitioned table are no longer rendered. If you introspect from an export, re-export the
foreign_keystemplate (askdb introspect templates --engine postgres). Where foreign keys reference a partitioned table, the schema lock hash changes once. - Connector types: a connector adapter’s
providerandConnectorProviderare now open string types (ConnectorProviderId). Aswitchover them needs adefaultbranch. - Built-in AI adapters:
createLanguageModelandcreateEmbeddingModelare nowasync. Await them if you call an adapter directly. @askdb/enrichtable saves:saveTable()refuses to write whentables/or the table’s markdown file is a symbolic link, or when the file name would land outsidetables/. It also needs write access totables/, because it saves through a temporary file there. Replace such links with regular files.- Bundles:
bundleSchemaDirectory(), and soaskdb bundle, fails whentenant-policy.md,concepts.mdortables/exists but can’t be read, instead of leaving it out.
Deprecated in beta.44
Section titled “Deprecated in beta.44”| Deprecated | Use instead | Removed in |
|---|---|---|
@askdb/ai-openai, @askdb/ai-azure, @askdb/ai-google, @askdb/ai-anthropic | @askdb/ai and @ai-sdk/<provider> | 1.0.0-beta.45 |
@askdb/connectors | @askdb/introspect and @askdb/introspect/kit | 1.0.0-beta.45 |
The old ai and rag config keys | ai.language, ai.embedding and connections, as Replace the removed config keys maps | 1.0.0-beta.50 |
OpenaiConfig, AzureConfig and the other <Provider>Config types | OpenaiConnection, AzureConnection, … | 1.0.0-beta.50 |
OpenaiAiConfig and the other <Provider>AiConfig types | AskDbAiConfig | 1.0.0-beta.50 |
OpenaiRagEmbedderConfig | ai.embedding and a connection in ai.providerConfig | 1.0.0-beta.50 |
DEFAULT_OPENAI_CHAT_MODEL and the other DEFAULT_*_CHAT_MODEL exports | DEFAULT_*_LANGUAGE_MODEL | 1.0.0-beta.50 |
ProviderEnvSpec.defaultEmbeddingModel (its fallback warned with code ASKDB_AI_DEFAULT_EMBEDDING_MODEL) | ai.embedding.model, or ASKDB_AI_EMBEDDING_MODEL in an env map, as Set an embedding model on @askdb/ai’s env path shows | 1.0.0-beta.50 |
askdb rag --embedder openai, --embedder ai-sdk | --embedder ai | 1.0.0-beta.50 |
The askdb-rag binary | askdb rag | 1.0.0-beta.50 |
createOpenAiEmbedder from @askdb/rag (deprecated in @askdb/rag 0.2.0-beta.17) | createAiSdkEmbedder with an ai.embedding model or your own AI SDK model, as Replace createOpenAiEmbedder from @askdb/rag shows | 1.0.0-beta.51 |
BundledSchemaV2 from @askdb/enrich | BundledSchemaV2 from @askdb/core | not yet set |
Read next
Section titled “Read next”© 2026 Yahya Gilany

