Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["asksql-vscode", "asksql-browser-extension"]
"ignore": ["asksql-vscode", "asksql-browser-extension"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
2 changes: 1 addition & 1 deletion .github/workflows/browser-extension-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
working-directory: packages/browser-extension
run: node scripts/package-zip.mjs
- name: Upload extension artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: asksql-browser-extension
path: packages/browser-extension/asksql-browser-extension.zip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/browser-extension-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
console.log('zip manifest is key-free, version ' + m.version);
})"
- name: Attach zip to GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: packages/browser-extension/asksql-browser-extension.zip
# Opt-in once Partner Center credentials exist: set the repository
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ jobs:
cat /tmp/privacy-body.html
echo '</body></html>'
} > docs/api/privacy.html
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: docs/api
# typedoc emits .nojekyll, and v4 onwards drops dotfiles unless asked. Actions-based
# Pages serves the artifact verbatim so nothing here runs Jekyll, but keeping the file
# means the same output also works if this ever moves to branch-based Pages.
include-hidden-files: true
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
12 changes: 8 additions & 4 deletions .github/workflows/jetbrains-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew test buildPlugin verifyPlugin --stacktrace
- name: Upload plugin zip
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: asksql-jetbrains-plugin
path: packages/jetbrains/build/distributions/*.zip
Expand Down Expand Up @@ -113,10 +113,14 @@ jobs:
- run: node export-vectors.mjs
- name: Fail if generated vectors differ from committed ones
run: |
git diff --exit-code -- vectors/ || {
echo "::error::Golden parity vectors changed — a new @asksql/core release altered guard/prompt behavior. Port the change, regenerate, and commit the new vectors."
# --porcelain, not `git diff`: a NEW vector file is untracked, and git diff
# ignores untracked files, so a forgotten commit would pass silently.
changed="$(git status --porcelain -- vectors/)"
if [ -n "$changed" ]; then
echo "$changed"
echo "::error::Golden parity vectors changed or are missing from the commit — a new @asksql/core release altered guard/prompt behavior. Port the change, regenerate, and commit the new vectors."
exit 1
}
fi

root-monorepo-unaffected:
needs: isolation-guard
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/jetbrains-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
PUBLISH_TOKEN: ${{ secrets.JETBRAINS_PUBLISH_TOKEN }}

- name: Attach signed zip to the GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: packages/jetbrains/build/distributions/*-signed.zip
fail_on_unmatched_files: true
2 changes: 1 addition & 1 deletion .github/workflows/vscode-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
working-directory: packages/vscode
run: npx --yes @vscode/vsce@3.9.2 package --no-dependencies
- name: Upload VSIX artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: asksql-vscode-vsix
path: packages/vscode/*.vsix
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ nohup.out
*.tsbuildinfo
examples/**/dist/
examples/plain-html/asksql-widget.js
# Built the same way, by build-widget.mjs; the example documents the command.
examples/express-postgres/public/asksql-widget.js

# Internal-only working docs (design/BRD, plan, coverage + validation logs,
# edge-case catalog). Kept locally; never published to the open-source repo.
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ git push
git tag v0.1.2 && git push origin v0.1.2
```

`changeset:version` runs `tools/release-preflight.mjs` first, which refuses to continue if any
package would be bumped to a **major** that no changeset asked for. That is not hypothetical:
changesets majors any package whose *peer* dependency receives a non-patch bump, so a changeset
saying `'@asksql/server': minor` once produced `1.0.0` because `@asksql/sqlite` went up a minor.
npm versions cannot be withdrawn, so this is checked before the numbers are written rather than
after. If a major genuinely is intended, declare it in a changeset and the preflight passes.

Two rules keep that trap shut, both enforced by `tests/peer-ranges.test.ts`:

- **Never use `workspace:*` (or `workspace:~`) in `peerDependencies`.** pnpm replaces it with the
exact current version on publish, so `@asksql/server` would demand one precise connector build
and every connector release would put consumers into a peer conflict. Use `workspace:>=0.1.0`:
what a connector must satisfy is the `Connector` interface from `@asksql/core`, which is a
normal dependency, so any published connector version is genuinely compatible.
- **Keep `onlyUpdatePeerDependentsWhenOutOfRange` set** in `.changeset/config.json`. Without it
changesets ignores the range entirely and majors on any non-patch peer bump.

The tag triggers `.github/workflows/release.yml`, which installs, builds, runs the
tests, then waits for an approval before publishing all nine packages with provenance.
The VS Code extension is `private: true`, so changesets skips it - it is versioned and
Expand Down
66 changes: 53 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

[![CI](https://github.com/rahulmahadik/AskSQL/actions/workflows/ci.yml/badge.svg)](https://github.com/rahulmahadik/AskSQL/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@asksql/core?label=%40asksql%2Fcore)](https://www.npmjs.com/package/@asksql/core)
[![VS Code Marketplace](https://vsmarketplacebadges.dev/version-short/RahulMahadik.asksql-vscode.svg?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=RahulMahadik.asksql-vscode)
[![JetBrains Marketplace](https://img.shields.io/jetbrains/plugin/v/33126?label=JetBrains)](https://plugins.jetbrains.com/plugin/33126-asksql)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

All packages are published on npm under the [`@asksql`](https://www.npmjs.com/org/asksql) scope.
Expand Down Expand Up @@ -122,8 +124,10 @@ New here? This is the whole thing, fully local - no cloud, no API key, no server
SQLite file and a model running on your own machine through [Ollama](https://ollama.com).

```bash
# 1. Install: the engine + the SQLite adapter (and its driver) + the local-model SDK
npm i @asksql/core @asksql/sqlite better-sqlite3 @ai-sdk/openai-compatible
# 1. Install: the engine + the SQLite adapter + the local-model SDK.
# Node 22.5+ uses the built-in node:sqlite, so there is no driver to build. On older
# Node, or if you prefer that driver, add better-sqlite3 to this line.
npm i @asksql/core @asksql/sqlite @ai-sdk/openai-compatible

# 2. Get a local model (install Ollama first from ollama.com), then pull a small coder model:
ollama pull qwen2.5-coder:7b
Expand All @@ -149,10 +153,12 @@ query CSV / Parquet files in the browser use `@asksql/duckdb`. The rest of this
each of those. Prefer a cloud model instead of Ollama? Swap step 2 for one `@ai-sdk/*` package
and an API key - see [Install only what your mode needs](#install-only-what-your-mode-needs).

**Schema Q&A**: turn on `answerSchemaQuestions` (or call `engine.explainSchema`) and
**Schema Q&A** (on by default in the IDE extensions; `engine.explainSchema` in code):
questions that aren't a data query - "how do these tables relate?", "how should I add a
phone column?", "suggest an index" - get grounded prose answers, with any proposed
INSERT/UPDATE/DELETE/DDL returned as text that is never executed.
INSERT/UPDATE/DELETE/DDL returned as text that is never executed. Ask something with
nothing to do with data and AskSQL says so plainly rather than guessing; ask about
databases in general, or about another engine, and it answers for the one you're on.

## Packages

Expand All @@ -175,7 +181,7 @@ Every setup is **three parts**, and you install only the ones you use:

1. **Engine** - `@asksql/core` (always).
2. **Data layer** - a database adapter + its driver (`@asksql/postgres` + `pg`, `@asksql/mysql`
+ `mysql2`, `@asksql/sqlite` + `better-sqlite3`), **or** `@asksql/duckdb` for browser
+ `mysql2`, `@asksql/sqlite` on its own from Node 22.5), **or** `@asksql/duckdb` for browser
file-analytics. Plus `@asksql/server` when you run the sidecar, and `@asksql/react` for the UI.
3. **Model-provider SDK** - one `@ai-sdk/*` package for the LLM you picked (see the table below).

Expand Down Expand Up @@ -382,12 +388,14 @@ optional peer, needed only by `startAskSqlMcpServer`. See

Beyond ask -> approve -> run, the engine and server ship these (all optional):

- **EXPLAIN in plain language** - `engine.explain(sql)` / `useAskSql().planFor()` / server
`POST /explain` return a natural-language read of the query plan.
- **Explain a query in plain language** - `engine.explain(sql)` / server `POST /explain` describe
what a statement does, grounded in the schema. `useAskSql().planFor()` is separate: it runs
`EXPLAIN` through the guard and returns the database's own plan.
- **Streaming progress** - `config.onEvent` (and per-ask `onEvent`) emits stage + token events
across the pipeline (`catalog`, `prompt`, `llm`, `guard`, `execute`, ...) for live UIs.
- **Cancellation** - pass an `AbortSignal` to any ask/run/explain; `useAskSql().cancel()` stops
in flight, and Postgres/MySQL cancel the running query at the database.
- **Cancellation** - pass an `AbortSignal` to any ask/run/explain and Postgres/MySQL cancel the
running query at the database. Through `@asksql/server` the signal is not yet threaded, so
`useAskSql().cancel()` stops the browser request but not the query behind it.
- **Hallucination floor** - before a query runs, the engine deterministically checks every
referenced table *and* column against your schema; if the model invents or mis-guesses a
column (a common small-model slip), it is handed the real column list and re-asked, so the
Expand All @@ -399,14 +407,16 @@ Beyond ask -> approve -> run, the engine and server ship these (all optional):
server's `suggestFixOnError` option (default on; set false to disable the extra model call).
- **Follow-up context** - prior turns are threaded into the prompt so "now break that down by
month" works; the UI sends the last few turns automatically.
- **Query history + audit** - `config.history` records every attempt (status, tokens, duration)
behind a paginated `GET /history`; `config.audit` is a pluggable sink with the guard verdict.
- **Query history + audit** - `config.history` records every attempt (status, duration) and
`@asksql/server` serves it at a paginated `GET /history`, backed by an in-memory store;
`config.audit` is a pluggable sink with the guard verdict.
- **Saved queries** - `useSavedQueries` / `SavedQueryStore` pin and reuse questions
(localStorage-backed, SSR-safe).
- **Schema pruning + token budget** - large catalogs are pruned to the most relevant tables
under a token budget (`config.pruner`) before prompting.
- **Privacy by default** - only the schema is ever sent; `allowDataInPrompt` (default off) is
the explicit opt-in to include sampled cell values in repair prompts.
- **Privacy by default** - only the schema is ever sent. `allowDataInPrompt` (default off) is the
opt-in for sampled cell values; with it off they are stripped from the catalog before any prompt
is built, so a connector that samples cannot leak them.
- **Server hardening** - `GET /health`, a request-body size cap (`maxBodyBytes`), and built-in
`cors` handling on the Express adapter.

Expand Down Expand Up @@ -486,6 +496,36 @@ more inconsistently-named your schema, the more model capability you need:
In our testing the **7B** (for example `qwen2.5-coder:7b`) is the sweet spot for accuracy
against speed, and it is easy to run locally.

### Measured, and reproducible

Load the fixtures in `packages/postgres/test/fixture.sql` and `packages/mysql/test/fixture.sql`,
build the workspace (`pnpm install && pnpm build`), then run
`node tools/benchmark/run.mjs qwen2.5-coder:1.5b qwen2.5-coder:7b qwen2.5-coder:14b`. It asks seven
data questions, executes the SQL that comes back, and scores a question right only when the rows
the database returned contain the expected value **and** number the same as a right answer would -
so a `SELECT *` that happens to include the word is still wrong. Plus seven questions that test
whether AskSQL stays in its lane.

| Model | SQL correct | Blocked by the guard | Scope correct | DELETE request | Median ask | Median schema answer |
|---|---|---|---|---|---|---|
| `qwen2.5-coder:1.5b` | 5/7 | 2 | 7/7 | statement + note | 1.1s | 0.5s |
| `qwen2.5-coder:7b` | 7/7 | 0 | 7/7 | statement + note | 2.7s | 1.6s |
| `qwen2.5-coder:14b` | 7/7 | 0 | 7/7 | statement + note | 4.6s | 3.6s |

*Apple M4 Pro, 24 GB, Ollama 0.20.3, 2026-08-01.* The **DELETE request** column is not a model
score: every model returned the statement as text, and the "AskSQL is read-only and never executed
this" note beside it is appended by AskSQL, not written by the model. It is there to show the
safety net firing on all three.

Read it for what it is: a small schema and a handful of questions, not a Spider-style benchmark,
and latency is whatever your machine does. What it does show is the shape of the trade-off - a 7B
matched a 14B here at roughly half the latency, which is why it is the default recommendation.

The **blocked** column is the interesting one. Those are not wrong answers: the 1.5B invented a
`product_id` column on a view, and AskSQL refused to run the query, told the user which columns
that view really has, and left the database untouched. A small model fails loudly here rather
than returning a confident wrong number.

Practical guidance: **review the generated SQL** (it is always shown first; set
`requireApproval` to force a click), give heavy analytics a more capable local model, and treat
the numbers on complex multi-join aggregations as draft until you have sanity-checked them.
Expand Down
24 changes: 16 additions & 8 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,15 @@ AST guard is the sole barrier there.

### Can I ask general questions about the schema - or how to change it?

Yes, if you turn on **Answer schema questions** (off by default, in the extension/plugin settings).
With it on, a question that isn't a data query - "summarize this database", "how are customers and
orders related?", or even "how would I add an index on email?" / "what column tracks loyalty points?"
- is answered in plain language from the schema instead of erroring.
Yes. **Answer schema questions** is on by default in the extension and the plugin (and is the
`answerSchemaQuestions` option in code). A question that isn't a data query - "summarize this
database", "how are customers and orders related?", or even "how would I add an index on email?" /
"what column tracks loyalty points?" - is answered in plain language from the schema instead of
erroring. This works on MongoDB connections too, in MongoDB terms (collections, `$lookup`).

Ask something with nothing to do with data and AskSQL says so in one line rather than guessing;
general database questions - modelling, indexing, or how another engine would express something -
are answered for the engine you are connected to.

Two guarantees hold. It is **grounded**: it only names tables, columns, and relationships that exist;
any name it can't find is flagged, and an ungrounded answer is regenerated once. And it stays
Expand Down Expand Up @@ -136,16 +141,19 @@ capable model. See "Accuracy depends on the model and the question" in the READM

### Which local model should I use?

A coder-tuned model is what you want. In our testing against a real database, a **7B**
(`qwen2.5-coder:7b`) was the sweet spot - it matched a 14B on accuracy while running about twice
as fast, and it is a comfortable size to run locally. Rough guidance:
A coder-tuned model is what you want. Use **`qwen2.5-coder:7b`** unless you have a reason not
to. Across PostgreSQL, MySQL and MongoDB questions on a real database, a 7B answered every one
correctly - the same as a 14B - at roughly half the latency (median ~3s against ~6s), and it is
a comfortable size to run locally. Rough guidance:

- **7B** (for example `qwen2.5-coder:7b`) - the recommended default: good on multi-join
analytics, light enough for most machines.
- **14B** (`qwen2.5-coder:14b`) - a bit more headroom on the hardest questions, at higher memory
and latency.
- **1.5B-3B** - fast and fine for simple, single-table or small-schema questions, but it slips
on complex joins or large, messily-named schemas, so use it only for lightweight cases.
on complex joins or large, messily-named schemas, so use it only for lightweight cases. It
invents column names more often; AskSQL blocks those before the database sees them and tells
you which columns the table really has, so the failure is safe rather than silent.

You can point AskSQL at any Ollama, MLX, or OpenAI-compatible local runtime, and any coder model
in that size range works. One thing that helps every size: a schema with **consistent, clear
Expand Down
54 changes: 54 additions & 0 deletions examples/express-postgres/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AskSQL - Express + Postgres sidecar</title>
<style>
body {
margin: 0;
padding: 48px 24px;
font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #1c2333;
background: #f7f9fc;
}
main { max-width: 640px; margin: 0 auto; }
h1 { margin: 0 0 8px; font-size: 28px; }
p { color: #45516b; }
code { background: #eaeef6; border-radius: 4px; padding: 2px 6px; font-size: 14px; }
ul { color: #45516b; }
</style>
</head>
<body>
<main>
<h1>Your app, with AskSQL bolted on</h1>
<p>
This page holds no database credentials. It talks to the sidecar this same Express
process mounts at <code>/asksql</code>, which owns the Postgres connection.
</p>
<p>Open the bubble in the corner and try:</p>
<ul>
<li>How many customers are there?</li>
<li>Total revenue from paid orders</li>
<li>Which region has the most customers?</li>
</ul>
<p>
Every generated query is read-only: the guard rejects anything else before it reaches
the database, so the worst case is a question that cannot be answered.
</p>
</main>

<!-- One script tag, one mount call. asksql-widget.js is a build artifact (gitignored);
produce it with:
node examples/plain-html/build-widget.mjs
cp examples/plain-html/asksql-widget.js examples/express-postgres/public/ -->
<script src="./asksql-widget.js"></script>
<script>
AskSQL.mount({
serverUrl: '/asksql',
title: 'Ask the Shop DB',
suggestions: ['How many customers are there?', 'Total revenue from paid orders'],
});
</script>
</body>
</html>
Loading