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
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,44 @@ jobs:
chrome_path="$(npx --yes @puppeteer/browsers@latest install chrome@151.0.7922.76 | awk '{ $1=""; sub(/^ /,""); print }')"
node test/e2e-smoke.mjs "$chrome_path"
working-directory: packages/browser-extension

# Real engines, hostile schemas, no model. A mixed-case Postgres schema once failed every query
# and shipped that way, because every database test used tables we had written ourselves.
schema-regression:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: pg
POSTGRES_DB: schema_regression
ports:
- 5433:5432
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 12
mysql:
image: mysql:8
env:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: schema_regression
ports:
- 3307:3306
options: >-
--health-cmd "mysqladmin ping -h127.0.0.1" --health-interval 5s --health-timeout 5s --health-retries 20
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
# The harness exits non-zero if an engine is unreachable, so a silent skip cannot pass here.
- run: pnpm test:schemas -- --require=postgres,mysql,sqlite
env:
ASKSQL_PG_URL: postgres://postgres:pg@127.0.0.1:5433/schema_regression
ASKSQL_MYSQL_HOST: 127.0.0.1
ASKSQL_MYSQL_PORT: '3307'
ASKSQL_MYSQL_USER: root
ASKSQL_MYSQL_PASSWORD: ''
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pnpm test # the full suite (see gating below)
pnpm format:check # prettier, as CI runs it
pnpm coverage # what CI runs instead of `test`; the coverage floor applies only with --coverage
pnpm test:packaged # installs the packed tarballs outside the workspace and exercises every export
pnpm test:schemas # identifier handling against real engines and hostile schemas; needs Postgres and MySQL
pnpm verify # all of the above in the order CI runs them; use this before pushing
```

Expand Down Expand Up @@ -71,6 +72,25 @@ To exercise them, provide:
model. Per-provider model overrides use `ASKSQL_<PROVIDER>_MODEL`.
- **Browser E2E** - a Chrome install; the tests drive it via `puppeteer-core`.

### Schema regression

`pnpm test:schemas` runs the engine's identifier handling against real databases using schemas
built to break the rules: mixed case, reserved words, spaces, unicode, names the SQL parser treats
as keywords. It needs no model, because this class of defect does not need one to surface and a
model would only make the result non-deterministic.

It exists because a mixed-case Postgres schema once failed **every** query and shipped that way for
weeks. Every database test we owned used tables we had written ourselves, so they shared our blind
spots by construction.

Two rules keep it honest, and both matter more than the pass count:

- On an engine that folds unquoted names, each fixture first asserts the **bare form genuinely
fails**. A fixture that passes before the fix is testing nothing.
- An unreachable engine is reported as **skipped, never as a pass**. CI passes
`--require=postgres,mysql,sqlite` so the job cannot go green on the embedded engine alone when the
service containers fail to start.

The security boundary is developed **test-first**: add or extend a case in the
`guard-security` / `guard-fuzz` suites before changing the guard.

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"test:packaged": "node tools/packaged-consumer-test.mjs",
"test:schemas": "node tools/schema-regression.mjs",
"test:real-db:load": "node tools/real-db-load.mjs",
"test:real-db": "node tools/real-db-e2e.mjs",
"test:schema-sweep": "node tools/schema-sweep.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ object IdentifierCase {
return "$quoteChar$name$close"
}

/** Where a literal or comment ends, or -1 when the position starts neither. */
/** A dollar-quoted body is a literal in Postgres and DuckDB, and may contain anything. */
private val DOLLAR_OPEN = Regex("""\$[A-Za-z_]\w*\$|\$\$""")

/** Where a literal or comment ends, or -1 when the position starts neither. */
private fun skipTo(sql: String, i: Int, doubleQuoteIsLiteral: Boolean, backslashEscapes: Boolean = false): Int {
val ch = sql[i]
val next = if (i + 1 < sql.length) sql[i + 1] else ' '
Expand Down
Loading