Skip to content

Describe what a column's type leaves out, and say what the provider said - #32

Merged
rahulmahadik merged 7 commits into
mainfrom
develop
Aug 20, 2026
Merged

rahulmahadik merged 7 commits into
mainfrom
develop

Conversation

@rahulmahadik

Copy link
Copy Markdown
Owner

Five commits. Three fix wrong answers a user could not tell were wrong, one closes a privacy gap, one
rewrites the notes a store reviewer is handed.

What a column's type leaves out

An integer column holding a moment carries no unit, and a JSON column carries no key names. Both gaps
produced answers that were wrong with no error raised: epoch milliseconds compared against epoch
seconds matches every row, against a text date matches none, and a guessed JSON key matches nothing.
The schema now states the unit, read from the column's range rather than one end, and names the
accessor that engine actually accepts. A range spanning two units, or a sentinel such as a "never
expires" maximum, yields no hint rather than a wrong one.

A filter comparing a column against a value the column does not hold is confirmed against the
database and reported, instead of answering zero as though nothing matched. Only a literal that
decides the result counts: under OR, NOT, CASE or a partial IN the query returns rows and a caveat
there would contradict the answer beside it.

The judgement is one module per language, shared by every engine, with each engine supplying only its
own probe SQL and accessor. Both are held to one set of 41 golden vectors, because the two
implementations had already drifted.

The keys were the data

A document may use a map where the keys are data: { owed: { "ada@example.com": 120 } }. Walked
naively, every address became a column NAME, and the data opt-in never removed it, because that
switch strips sampled values only. So addresses reached the model on the default path, against what
PRIVACY.md promises. A record repeats its keys across documents and a map does not, so the shape now
decides, judged per child. The same reasoning gates the JSON key names above: a map with a stable key
set is structurally identical to a record, so those names ride the existing cell-value opt-in.

A hosted provider configured with a loopback base URL now refuses rather than sending the API key in
cleartext to whatever is listening on that port. Three of the four surfaces did the latter.

What the provider said

A failing provider reported a generic message, so a retired model, a wrong key and a regional
outage were indistinguishable. The provider's own words are shown, the model list offers only models
that answer, and a reasoning model's private <think> monologue no longer leaks into the answer or
the explanation.

Verification

3136 TS tests, 806 Kotlin, both suites green, plus live runs against SQLite, Postgres, MySQL, DuckDB,
Oracle and MongoDB. Measured on real schemas: MySQL 0/4 to 3/4, Postgres 2/5 to 4/5, Room 5/6.

Versions: core 0.9.0, sqlite 0.6.0, postgres/mysql/duckdb/oracle 0.4.0, mongodb 0.3.0, server 0.6.3,
VS Code 0.8.0, browser extension 0.4.0, JetBrains 0.6.0.

Listing models swallowed every HTTP error and returned an empty list, so a rejected key, a rate
limit and an outage were indistinguishable from a provider with no models. The provider's own
sentence now reaches the reader.

Of the thirteen models one provider lists, six cannot hold a conversation, and the list is
alphabetical, so a broken one sat where the user picks: the query then failed after the model list
had looked healthy. Speech, transcription and classifier models are no longer offered.

Reasoning models narrate before they answer. That monologue was shown as the query's description,
and through Explain and the schema answers, which never pass through the SQL extractor. It is now
removed at the client boundary and filtered out of the token stream as it arrives, so a streaming
host no longer forwards it live. A tag that appears inside an answer is left alone.

A hosted provider configured with a local base URL is refused rather than sent the API key. Leaving
a local provider's base URL in place made "hosted" requests go to this machine, answer without a
key, and report success; the stale setting is now cleared when settings open. The refusal never
echoes the URL, which can embed credentials.

Settings ask for the key before the model, so fetching models has what it needs, and Test Provider
is renamed Test Connection and runs once a model is chosen.
A MongoDB document can use a map where the keys are values: `{ owed: { "ada@example.com": 120 } }`.
Walked as fields, every address became a column NAME, and a name is not removed by the cell-value
opt-in, which strips sampled values only. Those names reached the prompt on the default path.

A record repeats its fields across documents; a map does not, so a parent whose children do not
recur is described by its shape instead. The judgement is per child, so a summary field beside the
keys keeps its name, and shape decides before reuse, so an address goes whatever its frequency.

Four shapes needed separating from a real record and are covered by tests: a root-level map, a key
containing dots whose first segment matches a real field, a map nested in an array element, and a
polymorphic sub-document whose fields are mutually exclusive. Field names that are not ASCII are
kept, and a collection holding one document per integration keeps all of its names.
A column type says nothing about two things a query depends on, and both produced answers that were
wrong with no error raised.

An integer column holding a moment carries no unit. Comparing epoch milliseconds against epoch
seconds matches every row, and against a text date matches none. The unit is now read from the
column's range and stated in the schema; a range spanning two units, or a sentinel such as a "never
expires" maximum, yields no hint rather than a wrong one.

A JSON column carries no key names, so the model invented one and matched nothing. The schema now
names the accessor for the engine and how many keys recur. The key NAMES ride the existing
cell-value opt-in: a map with a stable key set is structurally identical to a record, so no
threshold separates them and the names are treated as data. Lists are described by their element
type and the membership test that engine actually accepts.

A filter comparing a column against a value the column does not hold is confirmed against the
database and reported, instead of answering zero as though nothing matched. Only a literal that
decides the result counts: under OR, NOT, CASE or a partial IN the query returns rows, and a caveat
there would contradict the answer beside it. Identifiers, moments and measurements are left alone,
views are never probed, and the values are named to the model only under the same opt-in.

The judgement is shared: one module per language, with each engine supplying only its own probe SQL
and accessor. Both are held to one set of golden vectors, so the two implementations cannot drift
apart the way they already had. Probes are bounded per table and per query, carry a timeout on every
engine, and never overwrite a comment the schema already had.
Two reviews rejected the extension under "Product is Testable" with identical wording. The notes
now lead with a temporary provider key and a two-minute path that needs no database of the
reviewer's own: save six lines as a CSV, add it as a connection, ask one question, and check the
three numbers it returns. What to do without a key at all is stated too, since most of the UI is
exercisable offline.

They also named a model the provider has since retired, which answers 404. A reviewer following the
instructions would have concluded the product does not work. The notes now name a model that exists
and carry a one-line check to run before submitting, because any provider can retire one at any time.
npm: core 0.9.0, sqlite 0.6.0, postgres 0.4.0, mysql 0.4.0, duckdb 0.4.0, oracle 0.4.0,
mongodb 0.3.0, server 0.6.3. Minor across the engine packages: a JSON hint now takes the
cell-value opt-in, the epoch classifier reads a range rather than one end, and the default
output changed.

VS Code 0.8.0, browser extension 0.4.0 and JetBrains 0.6.0 are bumped by hand: the first two
bundle the engine, and the plugin carries its own provider and settings work.
The plugin's introspectors stamp an integer moment column with the unit it holds, but the prompt
never said what to do with that, so the unit was named and then ignored: milliseconds compared
against a seconds bound match every row. PostgreSQL, MySQL, DuckDB and Oracle now carry the same
note the engine packages do; SQLite already said it at greater length.

The golden vector is regenerated with it. The parity job caught this, and the local run did not,
because the vector on disk was still the old one.
@rahulmahadik
rahulmahadik merged commit 9811535 into main Aug 20, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant