From 93b9d98546190e9256e15fce75256fbbf14519e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20Karsl=C4=B1?= Date: Fri, 6 Nov 2026 16:01:35 +0300 Subject: [PATCH 1/2] docs: highlight the under-3 kB core runtime Say that @typesql/core is tiny (under 3 kB minified and gzipped) in the READMEs, the npm description, the docs home page, the introduction and quick start, and add a runtime size row to the comparison pages. --- .changeset/pretty-owls-heal.md | 5 +++++ README.md | 2 +- packages/core/README.md | 2 ++ packages/core/package.json | 2 +- packages/docs/compared-to/drizzle.md | 3 +++ packages/docs/compared-to/kysely.md | 3 +++ packages/docs/compared-to/prisma-typedsql.md | 3 +++ packages/docs/compared-to/prisma.md | 3 +++ packages/docs/compared-to/reylo.md | 3 +++ packages/docs/guide/introduction.md | 2 +- packages/docs/guide/quick-start.md | 2 +- packages/docs/index.md | 2 +- 12 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 .changeset/pretty-owls-heal.md diff --git a/.changeset/pretty-owls-heal.md b/.changeset/pretty-owls-heal.md new file mode 100644 index 00000000..d374ce53 --- /dev/null +++ b/.changeset/pretty-owls-heal.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +The README and the npm description say how small the runtime is: under 3 kB minified and gzipped. diff --git a/README.md b/README.md index 49c7930b..e9b61d63 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Write SQL in TypeScript and get exact types for rows and parameters. TypeSQL parses your SQL inside the type checker: no codegen, no database connection, no build step. -TypeSQL supports PostgreSQL, SQLite, MySQL and MariaDB. +TypeSQL supports PostgreSQL, SQLite, MySQL and MariaDB. The `@typesql/core` runtime is tiny (under 3 kB minified and gzipped) and has no dependencies. npm version npm downloads diff --git a/packages/core/README.md b/packages/core/README.md index 8bee8a7e..fea36731 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -2,6 +2,8 @@ Write SQL in TypeScript and get exact types for rows and parameters. TypeSQL parses your SQL inside the type checker: no codegen, no database connection, no build step. +It supports PostgreSQL, SQLite, MySQL and MariaDB. The runtime is tiny (under 3 kB minified and gzipped) and has no dependencies. + ## Install ```sh diff --git a/packages/core/package.json b/packages/core/package.json index 012b1677..677836f8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@typesql/core", "version": "7.0.1", - "description": "Type-level SQL inference for TypeScript: PostgreSQL, SQLite, MySQL and MariaDB. Zero runtime dependencies.", + "description": "Type-level SQL inference for TypeScript: PostgreSQL, SQLite, MySQL and MariaDB. Tiny (under 3 kB) with zero runtime dependencies.", "license": "MIT", "repository": { "type": "git", diff --git a/packages/docs/compared-to/drizzle.md b/packages/docs/compared-to/drizzle.md index 2e889f1e..a988f120 100644 --- a/packages/docs/compared-to/drizzle.md +++ b/packages/docs/compared-to/drizzle.md @@ -38,8 +38,11 @@ const rows = await sql( | Raw SQL | `` sql`…` ``, typed by the `T` you write | checked and typed from the SQL | | Relations | the relational query API (`db.query…`) | joins and [JSON aggregation](../guide/using-sql#json) in SQL | | Migrations | drizzle-kit generates them from the schema | you [write the DDL](../guide/migrations), and the schema type follows it | +| Runtime size (minified and gzipped) | about 22 kB | `@typesql/core` is tiny (under 3 kB); under 5 kB with `@typesql/pg` | | Databases | PostgreSQL, MySQL, SQLite and more | PostgreSQL, SQLite, MySQL and MariaDB | +Sizes are for drizzle-orm 0.45.4 and `@typesql/pg` 7.0.1: a select with a join on node-postgres, bundled with esbuild, without the driver. + ## Choose Drizzle when - You want the schema, relations and migrations defined in one TypeScript file. diff --git a/packages/docs/compared-to/kysely.md b/packages/docs/compared-to/kysely.md index 9bc10607..a5167c8c 100644 --- a/packages/docs/compared-to/kysely.md +++ b/packages/docs/compared-to/kysely.md @@ -40,8 +40,11 @@ Both give `title` as `string | null`, with no generator and no database connecti | Types come from | your `DB` interface | your schema type | | Raw SQL | `` sql`…` ``, typed by the `T` you write | checked and typed from the SQL | | Dynamic queries | conditional method calls | [fragments](../guide/using-sql#dynamic-sql) | +| Runtime size (minified and gzipped) | about 31 kB | `@typesql/core` is tiny (under 3 kB); under 5 kB with `@typesql/pg` | | Databases | PostgreSQL, MySQL, SQLite, SQL Server and community dialects | PostgreSQL, SQLite, MySQL and MariaDB | +Sizes are for Kysely 0.29.8 and `@typesql/pg` 7.0.1: a select with a join on node-postgres, bundled with esbuild, without the driver. + ## Choose Kysely when - You'd rather compose queries with method calls. diff --git a/packages/docs/compared-to/prisma-typedsql.md b/packages/docs/compared-to/prisma-typedsql.md index 6d7e8ee6..aa668ce0 100644 --- a/packages/docs/compared-to/prisma-typedsql.md +++ b/packages/docs/compared-to/prisma-typedsql.md @@ -43,8 +43,11 @@ const rows = await sql( | Extra step | `prisma generate --sql` after each query change | none | | Nullability | what the database reports | worked out from the SQL (`LEFT JOIN`, `WHERE x IS NOT NULL`, `COALESCE`) | | Dynamic queries | each file is one fixed statement | [fragments](../guide/using-sql#dynamic-sql) | +| Runtime size (minified and gzipped) | Prisma Client: about 66 kB of JavaScript, plus a WebAssembly query compiler of 0.7 to 1.1 MB | `@typesql/core` is tiny (under 3 kB); under 5 kB with `@typesql/pg` | | Databases | PostgreSQL, MySQL, SQLite and others Prisma supports | PostgreSQL, SQLite, MySQL and MariaDB | +Sizes are for Prisma Client 7.10.1 and `@typesql/pg` 7.0.1, without the driver. The query compiler is 0.7 MB with `compilerBuild = "small"` and 1.1 MB with the default build. + ## Choose Prisma TypedSQL when - You already use Prisma ORM and only need a few raw queries. diff --git a/packages/docs/compared-to/prisma.md b/packages/docs/compared-to/prisma.md index b4230171..5ab75fb9 100644 --- a/packages/docs/compared-to/prisma.md +++ b/packages/docs/compared-to/prisma.md @@ -39,8 +39,11 @@ Prisma returns nested objects (each user with a `posts` array). TypeSQL returns | Extra step | `prisma generate` after each schema change | none | | Raw SQL | `$queryRaw`, typed by the type you write, or [TypedSQL](./prisma-typedsql) | checked and typed from the SQL | | Migrations | `prisma migrate` generates them from the schema | you [write the DDL](../guide/migrations), and the schema type follows it | +| Runtime size (minified and gzipped) | about 66 kB of JavaScript, plus a WebAssembly query compiler of 0.7 to 1.1 MB | `@typesql/core` is tiny (under 3 kB); under 5 kB with `@typesql/pg` | | Databases | PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, CockroachDB | PostgreSQL, SQLite, MySQL and MariaDB | +Sizes are for Prisma Client 7.10.1 and `@typesql/pg` 7.0.1, without the driver. The query compiler is 0.7 MB with `compilerBuild = "small"` and 1.1 MB with the default build. + ## Choose Prisma ORM when - You want relations, nested reads and nested writes without writing joins. diff --git a/packages/docs/compared-to/reylo.md b/packages/docs/compared-to/reylo.md index 60d901e1..b8c23d83 100644 --- a/packages/docs/compared-to/reylo.md +++ b/packages/docs/compared-to/reylo.md @@ -44,8 +44,11 @@ const rows = await sql( | Running queries | a preload (`--import reylo/register`) or bundler plugin, with the engine installed at runtime | `sql()` builds `{ text, values }`, and your driver sends it | | Dynamic queries | `CASE` on parameters, and JSON parameters described by a JSON Schema | [fragments](../guide/using-sql#dynamic-sql) | | Migrations | applied with your own tool | applied with your own tool, or [written as DDL](../guide/migrations) that the schema type follows | +| Runtime size | a native engine of 19 MB (Linux x64, unpacked) | `@typesql/core` is tiny (under 3 kB minified and gzipped); under 5 kB with `@typesql/pg` | | Databases | PostgreSQL, MySQL, SQLite, DuckDB (preview) | PostgreSQL, SQLite, MySQL and MariaDB | +Sizes are for Reylo 0.7.0 and `@typesql/pg` 7.0.1. Reylo's engine is the `@reylosql/engine-linux-x64` package. TypeSQL's size is a select with a join on node-postgres, bundled with esbuild, without the driver. + Both work out nullability from outer joins and from `WHERE` conditions that reject `NULL`. ## Choose Reylo when diff --git a/packages/docs/guide/introduction.md b/packages/docs/guide/introduction.md index e26fddb5..2c9a1658 100644 --- a/packages/docs/guide/introduction.md +++ b/packages/docs/guide/introduction.md @@ -72,7 +72,7 @@ TypeSQL is written in TypeScript's type system. When you call `sql("…")` with - **Parameter types.** Inline `${sql.param(value)}`, `$1` and `:name` each take their type from where they are used. - **Compile errors.** An unknown column, a missing `NOT NULL` value or a syntax error shows up on the string, worded like the database's error. - **The whole statement language.** From joins and CTEs to `MERGE` and SQL/JSON. -- **A thin runtime.** `@typesql/core` has no dependencies. Adapters run queries on node-postgres, postgres.js, SQLite (node:sqlite, better-sqlite3, bun:sqlite), MySQL and MariaDB (mysql2, the mariadb connector, PlanetScale, TiDB Cloud), Kysely, Drizzle or Prisma. +- **A tiny runtime.** `@typesql/core` is tiny (under 3 kB minified and gzipped) and has no dependencies. Adapters run queries on node-postgres, postgres.js, SQLite (node:sqlite, better-sqlite3, bun:sqlite), MySQL and MariaDB (mysql2, the mariadb connector, PlanetScale, TiDB Cloud), Kysely, Drizzle or Prisma. - **Editor support.** The optional editor support for [VS Code](./vscode) and [Neovim](./neovim) completes table and column names and highlights the SQL. ## Dialects diff --git a/packages/docs/guide/quick-start.md b/packages/docs/guide/quick-start.md index 0e60457a..e016df94 100644 --- a/packages/docs/guide/quick-start.md +++ b/packages/docs/guide/quick-start.md @@ -44,7 +44,7 @@ Install `@typesql/core` and the adapter for your driver: | [`@typesql/sqlite`](../adapters/sqlite) | node:sqlite, bun:sqlite or better-sqlite3 | | [`@typesql/mysql`](../adapters/mysql) | mysql2, the mariadb connector, PlanetScale or TiDB Cloud serverless | -`@typesql/core` has no runtime dependencies. Its [migrator](./migrations#the-migrator) connects from a `url` with `pg` or `mysql2`, optional peer dependencies, and writes `db.ts` with `typescript`. The packages are ESM-only. `import` and Node's `require()` of ES modules both work, but TypeScript rejects imports from `.cts` files under `node16`/`nodenext` resolution (TS1479). +`@typesql/core` is tiny (under 3 kB minified and gzipped) and has no runtime dependencies. Its [migrator](./migrations#the-migrator) connects from a `url` with `pg` or `mysql2`, optional peer dependencies, and writes `db.ts` with `typescript`. The packages are ESM-only. `import` and Node's `require()` of ES modules both work, but TypeScript rejects imports from `.cts` files under `node16`/`nodenext` resolution (TS1479). If your schema is already described for Kysely, Drizzle or Prisma, install its TypeSQL package instead of writing it again: diff --git a/packages/docs/index.md b/packages/docs/index.md index 70ce6ed8..f27c0760 100644 --- a/packages/docs/index.md +++ b/packages/docs/index.md @@ -5,7 +5,7 @@ layout: home hero: name: "TypeSQL" text: "Typed SQL for TypeScript" - tagline: "Write plain SQL and get exact types for rows and parameters. No codegen, no database connection, no build step." + tagline: "Write plain SQL and get exact types for rows and parameters. No codegen, no database connection, no build step. The runtime is tiny (under 3 kB)." image: src: /logo.svg alt: "TypeSQL logo" From 91eeb35af2f1e850fb18842bab7c0080a4cbb25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20Karsl=C4=B1?= Date: Fri, 6 Nov 2026 18:40:26 +0300 Subject: [PATCH 2/2] refactor: untangle the codebase so a typical change lands in one place Move what changes together into one place, across all packages: - engine/ is a dependency-free kit; MySQL and SQLite share engine/family/; PostgreSQL's own lexer, types and facts live in postgres/engine. - The root entry no longer loads the dialect engines. - @typesql/core/adapter splits into per-dialect drivers, connect, codegen, command and family modules, shared by adapters, ORM bridges and the CLI. - Each migration dialect is one record in migration/dialects. - The language server has one tokenizer and one dialect-facts record, and keyword lists come from @typesql/core/reserved. - Package lists come from manifests (tools/packages.ts); Node and database versions are pinned once; the test runner records each public fact once. - Comments that only restated the code are gone. Bugs found on the way are fixed, each with a scenario: statement typing in all dialects, migration statement scanning, SQLite introspection of tables named like shadow tables, literals on the HTTP MySQL drivers. Change intents describe each user-visible fix. --- .changeset/afraid-mangos-carry.md | 5 + .changeset/ancient-birds-listen.md | 7 + .changeset/angry-kiwis-brush.md | 5 + .changeset/angry-seas-perform.md | 5 + .changeset/big-walls-wait.md | 7 + .changeset/brave-ants-drum.md | 5 + .changeset/brave-wolves-exist.md | 5 + .changeset/bright-boxes-exist.md | 5 + .changeset/calm-tables-yawn.md | 5 + .changeset/clever-tigers-try.md | 5 + .changeset/cool-boats-compete.md | 5 + .changeset/dry-waves-tan.md | 5 + .changeset/eager-pandas-drop.md | 5 + .changeset/early-turkeys-joke.md | 5 + .changeset/fair-trees-share.md | 5 + .changeset/giant-flowers-wave.md | 5 + .changeset/good-frogs-own.md | 6 + .changeset/good-kings-refuse.md | 5 + .changeset/heavy-olives-hear.md | 5 + .changeset/honest-melons-thank.md | 5 + .changeset/itchy-icons-flash.md | 6 + .changeset/itchy-shirts-remain.md | 5 + .changeset/lazy-fireants-swim.md | 5 + .changeset/little-fireants-own.md | 5 + .changeset/loud-eels-hunt.md | 5 + .changeset/loud-wolves-draw.md | 5 + .changeset/mighty-kings-cover.md | 5 + .changeset/mighty-kings-hide.md | 5 + .changeset/new-wolves-kneel.md | 5 + .changeset/nice-bees-jam.md | 7 + .changeset/nice-dodos-type.md | 5 + .changeset/old-taxis-dig.md | 5 + .changeset/polite-turkeys-cough.md | 5 + .changeset/pretty-eggs-rest.md | 5 + .changeset/proud-fireants-tie.md | 5 + .changeset/rare-spiders-boil.md | 5 + .changeset/real-mice-invent.md | 5 + .changeset/real-trains-love.md | 5 + .changeset/sharp-eels-push.md | 5 + .changeset/slow-poets-exist.md | 5 + .changeset/slow-turtles-live.md | 5 + .changeset/small-flowers-juggle.md | 5 + .changeset/sour-hats-enjoy.md | 5 + .changeset/tame-boxes-share.md | 5 + .changeset/tame-spiders-sneeze.md | 5 + .changeset/thick-apes-greet.md | 5 + .changeset/thick-buses-attack.md | 5 + .changeset/thick-geckos-do.md | 5 + .changeset/tiny-cats-learn.md | 5 + .changeset/tough-pumas-count.md | 5 + .changeset/wild-dodos-judge.md | 5 + .changeset/young-roses-sniff.md | 9 + .github/workflows/checks.yml | 13 +- .github/workflows/release.yml | 29 +- .gitignore | 4 - .node-version | 1 + AGENTS.md | 11 +- biome.json | 7 +- knip.json | 91 -- knip.ts | 68 + package.json | 10 +- packages/bench/.gitignore | 1 + packages/bench/run.ts | 28 +- packages/better-auth/src/index.ts | 42 +- packages/better-auth/src/statement.ts | 40 +- packages/cli/src/index.ts | 33 +- packages/cli/src/migrate.ts | 214 +--- packages/core/package.json | 8 + packages/core/src/adapter.ts | 165 +-- packages/core/src/codegen.ts | 218 ++++ packages/core/src/command.ts | 146 +++ packages/core/src/connect.ts | 43 + packages/core/src/create-sql.ts | 94 +- packages/core/src/dialect.ts | 76 +- packages/core/src/empty.ts | 45 - packages/core/src/engine/chars.ts | 21 + packages/core/src/engine/family/ddl.ts | 134 ++ .../core/src/engine/family/diagnostics.ts | 141 +++ packages/core/src/engine/family/functions.ts | 96 ++ packages/core/src/engine/family/json.ts | 81 ++ packages/core/src/engine/family/lex.ts | 154 +++ packages/core/src/engine/family/parse.ts | 151 +++ packages/core/src/engine/family/scope.ts | 196 +++ packages/core/src/engine/family/typer.ts | 19 + packages/core/src/engine/family/types.ts | 95 ++ packages/core/src/engine/narrow.ts | 37 +- packages/core/src/{sql => engine}/next.ts | 0 packages/core/src/engine/num.ts | 50 + packages/core/src/engine/object.ts | 30 +- packages/core/src/engine/params.ts | 2 +- packages/core/src/engine/parse.ts | 102 -- packages/core/src/engine/suggest.ts | 15 +- packages/core/src/engine/tokens.ts | 42 +- packages/core/src/engine/types.ts | 45 +- packages/core/src/engine/write.ts | 91 -- packages/core/src/family.ts | 42 + packages/core/src/index.ts | 9 +- packages/core/src/migration/connect.ts | 293 ----- packages/core/src/migration/context.ts | 80 +- packages/core/src/migration/define.ts | 69 +- packages/core/src/migration/dialects/index.ts | 23 + packages/core/src/migration/dialects/mysql.ts | 192 +++ .../core/src/migration/dialects/postgres.ts | 109 ++ .../core/src/migration/dialects/session.ts | 129 ++ .../core/src/migration/dialects/sqlite.ts | 56 + packages/core/src/migration/files.ts | 222 +++- packages/core/src/migration/generate.ts | 125 +- packages/core/src/migration/index.ts | 9 +- packages/core/src/migration/migrator.ts | 711 ++--------- packages/core/src/migration/node.ts | 21 - packages/core/src/migration/schema.ts | 27 + packages/core/src/migration/statements.ts | 259 ++++ packages/core/src/mysql/dialect.ts | 74 +- packages/core/src/mysql/driver.ts | 299 +++++ packages/core/src/mysql/engine/ast.ts | 8 +- .../core/src/mysql/engine/binder/scope.ts | 256 +--- packages/core/src/mysql/engine/ddl/apply.ts | 201 ++- packages/core/src/mysql/engine/diagnostics.ts | 117 +- packages/core/src/mysql/engine/engine.ts | 23 +- packages/core/src/mysql/engine/index.ts | 4 +- packages/core/src/mysql/engine/infer.ts | 18 +- packages/core/src/mysql/engine/lexer.ts | 235 ++-- packages/core/src/mysql/engine/parser/ddl.ts | 423 +++---- packages/core/src/mysql/engine/parser/dml.ts | 144 +-- packages/core/src/mysql/engine/parser/expr.ts | 406 +++--- .../core/src/mysql/engine/parser/keywords.ts | 519 ++++---- .../core/src/mysql/engine/parser/select.ts | 384 +++--- packages/core/src/mysql/engine/parser/stmt.ts | 62 +- .../core/src/mysql/engine/parser/typename.ts | 122 +- packages/core/src/mysql/engine/parser/util.ts | 127 +- packages/core/src/mysql/engine/project/dml.ts | 99 +- .../core/src/mysql/engine/project/select.ts | 125 +- packages/core/src/mysql/engine/tokens.ts | 76 -- .../core/src/mysql/engine/typer/functions.ts | 436 +++---- packages/core/src/mysql/engine/typer/json.ts | 85 +- .../core/src/mysql/engine/typer/params.ts | 332 +---- .../core/src/mysql/engine/typer/typeof.ts | 246 ++-- packages/core/src/mysql/engine/types.ts | 78 +- packages/core/src/mysql/engine/write.ts | 4 +- packages/core/src/mysql/index.ts | 11 +- packages/core/src/mysql/migrations.ts | 13 - packages/core/src/mysql/schema/index.ts | 39 +- packages/core/src/mysql/sql/types.ts | 34 + packages/core/src/mysql/type-map/index.ts | 65 +- packages/core/src/postgres/dialect.ts | 43 +- .../pg-arrays.ts => postgres/driver.ts} | 109 +- .../src/postgres/engine/binder/clauses.ts | 205 ++- .../core/src/postgres/engine/binder/from.ts | 179 +-- .../src/postgres/engine/binder/resolve.ts | 33 +- packages/core/src/postgres/engine/db.ts | 35 + .../src/postgres/engine/ddl/alter-table.ts | 56 +- .../core/src/postgres/engine/ddl/apply.ts | 13 +- .../core/src/postgres/engine/ddl/column.ts | 22 - .../src/postgres/engine/ddl/create-table.ts | 3 +- .../core/src/postgres/engine/ddl/query.ts | 8 +- .../core/src/postgres/engine/ddl/registry.ts | 30 +- .../core/src/postgres/engine/ddl/relation.ts | 20 +- .../core/src/postgres/engine/ddl/script.ts | 14 +- packages/core/src/postgres/engine/ddl/view.ts | 15 +- .../core/src/postgres/engine/diagnostics.ts | 31 +- packages/core/src/postgres/engine/engine.ts | 24 +- packages/core/src/postgres/engine/index.ts | 53 +- packages/core/src/postgres/engine/infer.ts | 99 +- packages/core/src/postgres/engine/json.ts | 140 +-- .../core/src/postgres/engine/lexer/lex.ts | 52 +- .../core/src/postgres/engine/lexer/tokens.ts | 31 + packages/core/src/postgres/engine/literals.ts | 153 +++ packages/core/src/postgres/engine/ops.ts | 13 + .../core/src/postgres/engine/parser/ddl.ts | 457 ++++--- .../core/src/postgres/engine/parser/dml.ts | 93 +- .../core/src/postgres/engine/parser/expr.ts | 431 +++---- .../core/src/postgres/engine/parser/json.ts | 108 +- .../src/postgres/engine/parser/keywords.ts | 210 ++-- .../core/src/postgres/engine/parser/merge.ts | 43 +- .../core/src/postgres/engine/parser/select.ts | 298 +++-- .../core/src/postgres/engine/parser/stmt.ts | 99 +- .../src/postgres/engine/parser/typename.ts | 99 +- .../core/src/postgres/engine/parser/util.ts | 136 +- .../src/postgres/engine/parser/utility.ts | 50 +- .../core/src/postgres/engine/parser/xml.ts | 106 +- .../core/src/postgres/engine/project/dml.ts | 231 ++-- .../core/src/postgres/engine/project/merge.ts | 72 +- .../src/postgres/engine/project/naming.ts | 2 +- .../src/postgres/engine/project/select.ts | 157 +-- .../core/src/postgres/engine/project/stmt.ts | 2 +- packages/core/src/postgres/engine/shape.ts | 76 +- packages/core/src/postgres/engine/suggest.ts | 26 +- .../core/src/postgres/engine/typer/catalog.ts | 324 ++--- .../src/postgres/engine/typer/operators.ts | 355 ++---- .../core/src/postgres/engine/typer/params.ts | 80 +- .../src/postgres/engine/typer/pg-functions.ts | 6 +- .../core/src/postgres/engine/typer/sqljson.ts | 74 +- .../core/src/postgres/engine/typer/typeof.ts | 162 +-- .../core/src/postgres/engine/typer/xml.ts | 5 +- packages/core/src/postgres/engine/types.ts | 34 + packages/core/src/postgres/engine/util.ts | 27 + packages/core/src/postgres/engine/write.ts | 4 +- packages/core/src/postgres/index.ts | 9 +- packages/core/src/postgres/migrations.ts | 13 - packages/core/src/postgres/schema/column.ts | 9 +- .../core/src/postgres/schema/functions.ts | 28 + packages/core/src/postgres/schema/index.ts | 3 +- .../core/src/postgres/schema/normalize.ts | 45 +- .../core/src/postgres/schema/type-facts.ts | 227 ++++ packages/core/src/postgres/sql/types.ts | 18 + packages/core/src/postgres/type-map/index.ts | 128 +- packages/core/src/reserved.ts | 19 + packages/core/src/runtime/compile.ts | 152 ++- packages/core/src/runtime/json.ts | 4 + packages/core/src/runtime/mysql-params.ts | 36 - packages/core/src/runtime/sqlite-params.ts | 61 - packages/core/src/schema.ts | 142 ++- packages/core/src/sql/api.ts | 163 ++- packages/core/src/sql/call.ts | 81 +- packages/core/src/{engine => sql}/carried.ts | 33 +- packages/core/src/{engine => sql}/contract.ts | 12 + packages/core/src/sql/fragment.ts | 152 ++- packages/core/src/{engine => sql}/infer.ts | 18 +- packages/core/src/sql/inline-fragment.ts | 46 +- packages/core/src/sql/inline.ts | 158 +-- packages/core/src/sql/lexis.ts | 282 +++++ packages/core/src/sql/named.ts | 122 +- packages/core/src/sql/slots.ts | 25 +- packages/core/src/sql/splice.ts | 130 +- packages/core/src/sql/text.ts | 33 +- packages/core/src/sql/write.ts | 119 +- packages/core/src/sqlite/dialect.ts | 48 +- packages/core/src/sqlite/driver.ts | 157 +++ packages/core/src/sqlite/engine/ast.ts | 2 + .../core/src/sqlite/engine/binder/scope.ts | 336 ++--- packages/core/src/sqlite/engine/ddl/apply.ts | 114 +- .../core/src/sqlite/engine/diagnostics.ts | 122 +- packages/core/src/sqlite/engine/engine.ts | 24 +- packages/core/src/sqlite/engine/index.ts | 4 +- packages/core/src/sqlite/engine/infer.ts | 18 +- packages/core/src/sqlite/engine/lexer.ts | 178 +-- packages/core/src/sqlite/engine/parser/ddl.ts | 353 +++--- packages/core/src/sqlite/engine/parser/dml.ts | 99 +- .../core/src/sqlite/engine/parser/expr.ts | 297 ++--- .../core/src/sqlite/engine/parser/keywords.ts | 138 ++- .../core/src/sqlite/engine/parser/select.ts | 173 +-- .../core/src/sqlite/engine/parser/stmt.ts | 64 +- .../core/src/sqlite/engine/parser/util.ts | 147 +-- .../core/src/sqlite/engine/project/dml.ts | 70 +- .../core/src/sqlite/engine/project/select.ts | 151 +-- packages/core/src/sqlite/engine/tokens.ts | 62 - .../core/src/sqlite/engine/typer/functions.ts | 256 ++-- packages/core/src/sqlite/engine/typer/json.ts | 79 +- .../core/src/sqlite/engine/typer/params.ts | 159 +-- .../core/src/sqlite/engine/typer/typeof.ts | 142 ++- packages/core/src/sqlite/engine/types.ts | 81 +- packages/core/src/sqlite/engine/write.ts | 4 +- packages/core/src/sqlite/index.ts | 5 +- packages/core/src/sqlite/migrations.ts | 13 - packages/core/src/sqlite/schema/index.ts | 37 +- packages/core/src/sqlite/sql/types.ts | 18 + packages/core/src/sqlite/type-map/index.ts | 57 +- packages/core/src/transaction.ts | 157 ++- packages/docs/.vitepress/sql-highlighting.ts | 2 +- packages/docs/adapters/mysql.md | 8 +- packages/docs/adapters/sqlite.md | 4 +- packages/docs/guide/introspect.md | 2 +- packages/docs/guide/limits.md | 3 +- packages/docs/guide/migrations.md | 2 +- packages/docs/guide/orms.md | 6 +- packages/docs/guide/schema.md | 4 +- packages/docs/guide/using-sql.md | 2 +- packages/docs/guide/vscode.md | 2 +- packages/docs/internals/releasing.md | 10 +- packages/docs/reference/api.md | 42 +- packages/docs/reference/type-mapping.md | 6 +- packages/drizzle/src/client.ts | 7 +- packages/drizzle/src/column.ts | 67 + packages/drizzle/src/from-drizzle-mysql.ts | 86 +- packages/drizzle/src/from-drizzle-pg.ts | 139 +++ packages/drizzle/src/from-drizzle-sqlite.ts | 61 +- packages/drizzle/src/from-drizzle.ts | 271 ++-- packages/drizzle/src/index.ts | 46 +- packages/drizzle/src/mysql-client.ts | 24 +- packages/kysely/src/client.ts | 84 +- packages/kysely/src/from-kysely-mysql.ts | 91 +- packages/kysely/src/from-kysely-sqlite.ts | 87 +- packages/kysely/src/from-kysely.ts | 269 +++- packages/kysely/src/index.ts | 20 +- packages/language-server/package.json | 19 + packages/language-server/src/call.ts | 227 ++++ packages/language-server/src/complete/call.ts | 278 ----- .../language-server/src/complete/complete.ts | 237 ++-- .../language-server/src/complete/index.ts | 27 - packages/language-server/src/complete/scan.ts | 214 +--- .../language-server/src/complete/schema.ts | 50 +- .../language-server/src/highlight/grammar.ts | 70 ++ .../language-server/src/highlight/index.ts | 212 ++-- packages/language-server/src/highlight/lex.ts | 664 ---------- packages/language-server/src/index.ts | 11 +- packages/language-server/src/literal.ts | 5 +- packages/language-server/src/merge.ts | 34 +- packages/language-server/src/server.ts | 53 +- packages/language-server/src/session.ts | 330 +++-- packages/language-server/src/sql/dialect.ts | 111 ++ packages/language-server/src/sql/lex.ts | 278 +++++ packages/language-server/src/sql/words.ts | 348 ++++++ packages/language-server/tsconfig.build.json | 2 - packages/mysql/package.json | 1 - packages/mysql/src/introspect/cli.ts | 140 +-- packages/mysql/src/introspect/emit.ts | 72 +- packages/mysql/src/literal.ts | 71 -- packages/mysql/src/mariadb.ts | 19 +- packages/mysql/src/mysql2.ts | 18 +- packages/mysql/src/planetscale.ts | 11 +- packages/mysql/src/pool.ts | 108 +- packages/mysql/src/tidb.ts | 16 +- packages/pg/src/client.ts | 48 +- packages/pg/src/introspect/cli.ts | 239 ++-- packages/pg/src/introspect/emit.ts | 69 +- packages/pg/src/type-map.ts | 22 +- packages/postgres/src/client.ts | 5 +- packages/prisma/src/client.ts | 88 +- packages/prisma/src/from-prisma-mysql.ts | 60 +- packages/prisma/src/from-prisma-pg.ts | 82 ++ packages/prisma/src/from-prisma-sqlite.ts | 59 +- packages/prisma/src/from-prisma.ts | 230 +--- packages/prisma/src/generator/emit.ts | 162 +-- packages/prisma/src/generator/index.ts | 20 +- packages/prisma/src/index.ts | 49 +- packages/prisma/src/model.ts | 118 ++ packages/prisma/src/provider.ts | 22 + packages/prisma/src/scalars.ts | 41 + packages/sqlite/src/client.ts | 12 +- packages/sqlite/src/introspect/cli.ts | 98 +- packages/sqlite/src/introspect/emit.ts | 52 +- packages/sqlite/src/introspect/introspect.ts | 6 +- packages/vscode/package.json | 3 +- packages/vscode/scripts/contributes.mts | 230 ++++ packages/vscode/src/contributes.mts | 43 + packages/vscode/src/extension.ts | 165 +-- .../syntaxes/sql-injection.tmLanguage.json | 23 +- packages/vscode/tsconfig.json | 8 +- pnpm-lock.yaml | 1104 ++++++++++++++--- pnpm-workspace.yaml | 1 + tests/README.md | 12 +- tests/package.json | 3 +- tests/run.ts | 34 - tests/scenario.test.ts | 248 ---- .../better-auth-postgres/package.json | 1 - .../better-auth-rejection/expected.snap | 42 - .../better-auth-rejection/package.json | 3 - .../scenarios/better-auth-sqlite/package.json | 1 - tests/scenarios/better-sqlite3/package.json | 1 - tests/scenarios/bun-sqlite/package.json | 1 - .../cli-introspect-database-url/app.ts | 12 + .../cli-introspect-database-url/expected.snap | 47 + .../cli-introspect-database-url/package.json | 26 + .../cli-introspect-database-url/setup.ts | 8 + .../cli-introspect-mariadb/package.json | 1 - .../cli-introspect-mysql/package.json | 1 - .../cli-introspect-postgres/package.json | 1 - .../cli-introspect-sqlite-exclude/app.ts | 4 + .../expected.snap | 40 + .../package.json | 23 + .../cli-introspect-sqlite-exclude/setup.ts | 7 + .../cli-introspect-sqlite-virtual/app.ts | 11 + .../expected.snap | 51 + .../package.json | 25 + .../cli-introspect-sqlite-virtual/setup.ts | 11 + .../cli-introspect-sqlite/package.json | 1 - .../cli-introspect-type-name/expected.snap | 17 + .../cli-introspect-type-name/package.json | 22 + .../cli-introspect-type-name/setup.ts | 6 + tests/scenarios/cli-invalid-command/app.ts | 1 - .../cli-invalid-command/package.json | 5 - tests/scenarios/cli-main-io/app.ts | 15 + tests/scenarios/cli-main-io/expected.snap | 38 + tests/scenarios/cli-main-io/package.json | 19 + tests/scenarios/cli-main-io/setup.ts | 5 + tests/scenarios/cli-make/package.json | 1 - .../cli-migrations-mysql/package.json | 1 - .../cli-migrations-postgres/package.json | 1 - .../cli-migrations-sqlite/package.json | 1 - .../compiler-nullability/expected.snap | 20 - .../compiler-nullability/package.json | 3 - .../compiler-rejection/expected.snap | 32 - .../scenarios/compiler-rejection/package.json | 3 - .../compiler-strict-null-checks/expected.snap | 2 - .../compiler-strict-null-checks/package.json | 3 - tests/scenarios/core-workflows/package.json | 1 - tests/scenarios/dollar-identifiers/app.ts | 48 + .../dollar-identifiers/expected.snap | 35 + .../package.json | 9 +- .../scenarios/drizzle-mysql-serverless/app.ts | 24 + .../drizzle-mysql-serverless/expected.snap | 25 + .../drizzle-mysql-serverless/package.json | 18 + tests/scenarios/drizzle-mysql/package.json | 1 - tests/scenarios/drizzle-pg/package.json | 1 - tests/scenarios/drizzle-postgres/package.json | 1 - tests/scenarios/drizzle-sqlite/package.json | 1 - tests/scenarios/kysely-mysql-databases/app.ts | 42 + .../kysely-mysql-databases/expected.snap | 30 + .../kysely-mysql-databases/package.json | 21 + tests/scenarios/kysely-mysql/package.json | 1 - tests/scenarios/kysely-postgres/package.json | 1 - tests/scenarios/kysely-sqlite-attached/app.ts | 41 + .../kysely-sqlite-attached/expected.snap | 30 + .../kysely-sqlite-attached/package.json | 22 + tests/scenarios/kysely-sqlite/package.json | 1 - .../scenarios/mariadb-connector/package.json | 1 - tests/scenarios/mariadb-mysql2/package.json | 1 - .../scenarios/migrations-mariadb/package.json | 1 - .../migrations-mysql-keyword-names/app.ts | 60 + .../expected.snap | 25 + .../package.json | 19 + .../scenarios/migrations-mysql-values/app.ts | 55 + .../migrations-mysql-values/expected.snap | 25 + .../migrations-mysql-values/package.json | 20 + tests/scenarios/migrations-mysql/package.json | 1 - .../app.ts | 145 +++ .../expected.snap | 29 + .../package.json | 20 + .../migrations-postgres-json-generate/app.ts | 14 + .../expected.snap | 44 + .../migrations/001-settings.ts | 20 + .../package.json | 26 + .../typesql.config.ts | 6 + .../migrations-postgres-values/app.ts | 34 + .../migrations-postgres-values/expected.snap | 29 + .../migrations-postgres-values/package.json | 19 + tests/scenarios/migrations-postgres/app.ts | 1 + .../migrations-postgres/package.json | 1 - .../migrations-sqlite-keyword-names/app.ts | 51 + .../expected.snap | 22 + .../package.json | 18 + .../scenarios/migrations-sqlite/package.json | 1 - tests/scenarios/module-entry-points/app.ts | 130 +- .../module-entry-points/expected.snap | 178 ++- .../module-entry-points/package.json | 24 +- .../mysql-dialect-rejection/expected.snap | 38 - .../mysql-dialect-rejection/package.json | 3 - .../mysql-insert-column-order/app.ts | 86 ++ .../mysql-insert-column-order/expected.snap | 43 + .../mysql-insert-column-order/package.json | 20 + .../mysql-json-array-rejection/app.ts | 10 + .../mysql-json-array-rejection/expected.snap | 31 + .../mysql-json-array-rejection/package.json | 18 + .../mysql-json-object-rejection/app.ts | 31 + .../mysql-json-object-rejection/expected.snap | 80 ++ .../mysql-json-object-rejection/package.json | 18 + tests/scenarios/mysql-json-strings/app.ts | 34 + .../mysql-json-strings/expected.snap | 31 + .../scenarios/mysql-json-strings/package.json | 20 + tests/scenarios/mysql-json/package.json | 1 - .../mysql-multiple-statements/app.ts | 24 + .../mysql-multiple-statements/expected.snap | 27 + .../mysql-multiple-statements/package.json | 20 + .../scenarios/mysql-quick-start/package.json | 1 - tests/scenarios/mysql-quoted-text/app.ts | 65 + .../scenarios/mysql-quoted-text/expected.snap | 48 + .../scenarios/mysql-quoted-text/package.json | 20 + .../mysql-serverless-literals/app.ts | 40 + .../mysql-serverless-literals/expected.snap | 28 + .../mysql-serverless-literals/package.json | 20 + tests/scenarios/mysql-where-narrowing/app.ts | 51 + .../mysql-where-narrowing/expected.snap | 51 + .../mysql-where-narrowing/package.json | 20 + tests/scenarios/node-sqlite/package.json | 1 - tests/scenarios/pg-cast-parameters/app.ts | 80 ++ .../pg-cast-parameters/expected.snap | 45 + .../scenarios/pg-cast-parameters/package.json | 21 + tests/scenarios/pg-char-type/app.ts | 29 + tests/scenarios/pg-char-type/expected.snap | 130 ++ tests/scenarios/pg-char-type/package.json | 18 + .../scenarios/pg-common-type-rejection/app.ts | 27 + .../pg-common-type-rejection/expected.snap | 50 + .../pg-common-type-rejection/package.json | 18 + .../pg-declaration-emit/package.json | 1 - .../pg-declared-function-calls/app.ts | 22 + .../pg-declared-function-calls/expected.snap | 33 + .../pg-declared-function-calls/package.json | 18 + tests/scenarios/pg-driver-values/package.json | 1 - .../pg-duplicate-output-names/package.json | 1 - tests/scenarios/pg-dynamic-sql/package.json | 1 - .../pg-ended-transaction/expected.snap | 13 - .../pg-ended-transaction/package.json | 1 - tests/scenarios/pg-engine-rejection/app.ts | 42 + .../pg-engine-rejection/expected.snap | 228 ++++ .../pg-engine-rejection/package.json | 18 + .../pg-function-call-parameters/app.ts | 38 + .../pg-function-call-parameters/expected.snap | 38 + .../pg-function-call-parameters/package.json | 21 + tests/scenarios/pg-function-overloads/app.ts | 4 +- .../pg-function-overloads/package.json | 1 - .../pg-grouping-sets-parameters/app.ts | 32 + .../pg-grouping-sets-parameters/expected.snap | 26 + .../pg-grouping-sets-parameters/package.json | 21 + .../pg-json-write-rejection/expected.snap | 67 - .../pg-json-write-rejection/package.json | 3 - tests/scenarios/pg-json-writes/package.json | 1 - tests/scenarios/pg-json/package.json | 1 - tests/scenarios/pg-left-join/package.json | 1 - tests/scenarios/pg-meta-key-relations/app.ts | 21 + .../pg-meta-key-relations/expected.snap | 129 ++ .../pg-meta-key-relations/package.json | 18 + tests/scenarios/pg-multiple-statements/app.ts | 22 + .../pg-multiple-statements/expected.snap | 27 + .../pg-multiple-statements/package.json | 21 + .../pg-nested-comments-runtime/app.ts | 38 - .../pg-nested-comments-runtime/expected.snap | 30 - tests/scenarios/pg-nested-comments/app.ts | 30 + .../pg-nested-comments/expected.snap | 10 + .../scenarios/pg-nested-comments/package.json | 1 - tests/scenarios/pg-no-result/expected.snap | 13 - tests/scenarios/pg-no-result/package.json | 1 - tests/scenarios/pg-null-array-elements/app.ts | 48 + .../pg-null-array-elements/expected.snap | 33 + .../pg-null-array-elements/package.json | 21 + .../pg-parameter-inference/package.json | 1 - tests/scenarios/pg-prepared/package.json | 1 - .../pg-quantified-null-elements/app.ts | 70 ++ .../pg-quantified-null-elements/expected.snap | 45 + .../pg-quantified-null-elements/package.json | 21 + tests/scenarios/pg-quick-start/package.json | 1 - tests/scenarios/pg-text-types/app.ts | 15 + tests/scenarios/pg-text-types/expected.snap | 34 + tests/scenarios/pg-text-types/package.json | 21 + tests/scenarios/pg-transactions/package.json | 1 - tests/scenarios/pg-values-defaults/app.ts | 36 + .../pg-values-defaults/expected.snap | 30 + .../scenarios/pg-values-defaults/package.json | 21 + tests/scenarios/pg-where-narrowing/app.ts | 49 + .../pg-where-narrowing/expected.snap | 55 + .../scenarios/pg-where-narrowing/package.json | 21 + tests/scenarios/pg-writes/package.json | 1 - .../postgresjs-workflows/package.json | 1 - tests/scenarios/prisma-client-mysql/app.ts | 31 - .../prisma-client-mysql/expected.snap | 36 - .../prisma-client-mysql/package.json | 24 - .../prisma-client-mysql/prisma.config.ts | 5 - .../prisma-client-mysql/schema.prisma | 14 - tests/scenarios/prisma-client-postgres/app.ts | 23 - .../prisma-client-postgres/expected.snap | 36 - .../prisma-client-postgres/package.json | 26 - .../prisma-client-postgres/prisma.config.ts | 5 - .../prisma-client-postgres/schema.prisma | 14 - tests/scenarios/prisma-client-sqlite/app.ts | 25 - .../prisma-client-sqlite/expected.snap | 36 - .../prisma-client-sqlite/package.json | 26 - .../prisma-client-sqlite/prisma.config.ts | 5 - .../prisma-client-sqlite/schema.prisma | 14 - .../scenarios/prisma-generation-mysql/app.ts | 7 + .../prisma-generation-mysql/expected.snap | 17 +- .../prisma-generation-mysql/package.json | 3 +- .../prisma-generation-mysql/schema.prisma | 5 + .../prisma-generation-postgres/app.ts | 7 + .../prisma-generation-postgres/expected.snap | 15 +- .../prisma-generation-postgres/package.json | 3 +- .../prisma-generation-postgres/schema.prisma | 5 + .../scenarios/prisma-generation-sqlite/app.ts | 7 + .../prisma-generation-sqlite/expected.snap | 15 +- .../prisma-generation-sqlite/package.json | 3 +- .../prisma-generation-sqlite/schema.prisma | 5 + .../app.ts | 0 .../expected.snap | 2 +- .../package.json | 5 +- .../app.ts | 0 .../expected.snap | 4 +- .../package.json | 5 +- .../runtime-inline-reuse/expected.snap | 18 - .../runtime-inline-reuse/package.json | 1 - .../runtime-no-executor/expected.snap | 15 - .../runtime-no-executor/package.json | 1 - tests/scenarios/sql-dialect/app.ts | 35 +- tests/scenarios/sql-dialect/expected.snap | 10 +- tests/scenarios/sql-dialect/package.json | 1 - .../sqlite-aggregate-traversal/package.json | 1 - .../scenarios/sqlite-declaration-emit/app.ts | 9 + .../sqlite-declaration-emit/expected.snap | 1 + .../sqlite-declaration-emit/package.json | 1 - .../scenarios/sqlite-declaration-emit/sql.ts | 27 +- tests/scenarios/sqlite-hidden-columns/app.ts | 26 + .../sqlite-hidden-columns/expected.snap | 65 + .../migrations/001-docs.ts | 7 + .../sqlite-hidden-columns/package.json | 25 + .../sqlite-hidden-columns/typesql.config.ts | 6 + .../sqlite-insert-column-order/app.ts | 80 ++ .../sqlite-insert-column-order/expected.snap | 77 ++ .../sqlite-insert-column-order/package.json | 19 + tests/scenarios/sqlite-json-type-map/app.ts | 22 + .../sqlite-json-type-map/expected.snap | 18 + .../sqlite-json-type-map/package.json | 12 + .../sqlite-json-write-rejection/expected.snap | 111 +- .../sqlite-json-write-rejection/package.json | 3 - .../scenarios/sqlite-json-writes/package.json | 1 - tests/scenarios/sqlite-negated-zero/app.ts | 21 + .../sqlite-negated-zero/expected.snap | 34 + .../sqlite-negated-zero/package.json | 19 + .../sqlite-parameter-inference/package.json | 1 - tests/scenarios/sqlite-quoted-text/app.ts | 56 + .../sqlite-quoted-text/expected.snap | 43 + .../scenarios/sqlite-quoted-text/package.json | 19 + tests/scenarios/sqlite-values-columns/app.ts | 38 + .../sqlite-values-columns/expected.snap | 88 ++ .../sqlite-values-columns/package.json | 18 + tests/scenarios/sqlite-where-narrowing/app.ts | 50 + .../sqlite-where-narrowing/expected.snap | 69 ++ .../sqlite-where-narrowing/package.json | 19 + .../tsconfig.base.json} | 9 +- tests/support/command.ts | 50 +- tests/support/database.ts | 206 ++- tests/support/observe.ts | 43 +- tests/support/paths.ts | 7 + tests/support/scenario.ts | 218 ++++ tests/support/setup.ts | 57 +- tests/support/types.ts | 11 +- tests/vitest.config.ts | 25 +- tools/biome/build.ts | 66 + tools/biome/no-reduce-accumulator-copy.grit | 8 +- tools/biome/shared/function_like.grit | 15 + tools/biome/shared/unparenthesized.grit | 8 + tools/biome/shared/wrapped.grit | 10 + tools/biome/src/no-builtin-instanceof.grit | 15 + .../biome/src/no-chained-type-assertions.grit | 45 + .../no-conditional-empty-object-spread.grit | 18 + .../no-hidden-unknown-and-object-types.grit | 328 +++++ tools/biome/src/no-known-value-widening.grit | 1020 +++++++++++++++ .../biome/src/no-reduce-accumulator-copy.grit | 429 +++++++ .../src/no-reflection-and-module-mocking.grit | 138 +++ tools/biome/src/no-spread-new-set.grit | 8 + tools/biome/src/no-unknown-parameters.grit | 74 ++ .../biome/src/no-unsafe-dictionary-type.grit | 476 +++++++ tools/check-intents.ts | 44 +- tools/gen-pg-functions.ts | 101 +- tools/generated.ts | 30 + tools/github-releases.ts | 21 +- tools/packages.ts | 110 ++ tools/pg-catalog-from-pg.ts | 113 +- tools/servers.ts | 56 + tsconfig.json | 2 +- 636 files changed, 24356 insertions(+), 15514 deletions(-) create mode 100644 .changeset/afraid-mangos-carry.md create mode 100644 .changeset/ancient-birds-listen.md create mode 100644 .changeset/angry-kiwis-brush.md create mode 100644 .changeset/angry-seas-perform.md create mode 100644 .changeset/big-walls-wait.md create mode 100644 .changeset/brave-ants-drum.md create mode 100644 .changeset/brave-wolves-exist.md create mode 100644 .changeset/bright-boxes-exist.md create mode 100644 .changeset/calm-tables-yawn.md create mode 100644 .changeset/clever-tigers-try.md create mode 100644 .changeset/cool-boats-compete.md create mode 100644 .changeset/dry-waves-tan.md create mode 100644 .changeset/eager-pandas-drop.md create mode 100644 .changeset/early-turkeys-joke.md create mode 100644 .changeset/fair-trees-share.md create mode 100644 .changeset/giant-flowers-wave.md create mode 100644 .changeset/good-frogs-own.md create mode 100644 .changeset/good-kings-refuse.md create mode 100644 .changeset/heavy-olives-hear.md create mode 100644 .changeset/honest-melons-thank.md create mode 100644 .changeset/itchy-icons-flash.md create mode 100644 .changeset/itchy-shirts-remain.md create mode 100644 .changeset/lazy-fireants-swim.md create mode 100644 .changeset/little-fireants-own.md create mode 100644 .changeset/loud-eels-hunt.md create mode 100644 .changeset/loud-wolves-draw.md create mode 100644 .changeset/mighty-kings-cover.md create mode 100644 .changeset/mighty-kings-hide.md create mode 100644 .changeset/new-wolves-kneel.md create mode 100644 .changeset/nice-bees-jam.md create mode 100644 .changeset/nice-dodos-type.md create mode 100644 .changeset/old-taxis-dig.md create mode 100644 .changeset/polite-turkeys-cough.md create mode 100644 .changeset/pretty-eggs-rest.md create mode 100644 .changeset/proud-fireants-tie.md create mode 100644 .changeset/rare-spiders-boil.md create mode 100644 .changeset/real-mice-invent.md create mode 100644 .changeset/real-trains-love.md create mode 100644 .changeset/sharp-eels-push.md create mode 100644 .changeset/slow-poets-exist.md create mode 100644 .changeset/slow-turtles-live.md create mode 100644 .changeset/small-flowers-juggle.md create mode 100644 .changeset/sour-hats-enjoy.md create mode 100644 .changeset/tame-boxes-share.md create mode 100644 .changeset/tame-spiders-sneeze.md create mode 100644 .changeset/thick-apes-greet.md create mode 100644 .changeset/thick-buses-attack.md create mode 100644 .changeset/thick-geckos-do.md create mode 100644 .changeset/tiny-cats-learn.md create mode 100644 .changeset/tough-pumas-count.md create mode 100644 .changeset/wild-dodos-judge.md create mode 100644 .changeset/young-roses-sniff.md create mode 100644 .node-version delete mode 100644 knip.json create mode 100644 knip.ts create mode 100644 packages/bench/.gitignore create mode 100644 packages/core/src/codegen.ts create mode 100644 packages/core/src/command.ts create mode 100644 packages/core/src/connect.ts create mode 100644 packages/core/src/engine/family/ddl.ts create mode 100644 packages/core/src/engine/family/diagnostics.ts create mode 100644 packages/core/src/engine/family/functions.ts create mode 100644 packages/core/src/engine/family/json.ts create mode 100644 packages/core/src/engine/family/lex.ts create mode 100644 packages/core/src/engine/family/parse.ts create mode 100644 packages/core/src/engine/family/scope.ts create mode 100644 packages/core/src/engine/family/typer.ts create mode 100644 packages/core/src/engine/family/types.ts rename packages/core/src/{sql => engine}/next.ts (100%) create mode 100644 packages/core/src/engine/num.ts delete mode 100644 packages/core/src/engine/parse.ts delete mode 100644 packages/core/src/engine/write.ts create mode 100644 packages/core/src/family.ts delete mode 100644 packages/core/src/migration/connect.ts create mode 100644 packages/core/src/migration/dialects/index.ts create mode 100644 packages/core/src/migration/dialects/mysql.ts create mode 100644 packages/core/src/migration/dialects/postgres.ts create mode 100644 packages/core/src/migration/dialects/session.ts create mode 100644 packages/core/src/migration/dialects/sqlite.ts delete mode 100644 packages/core/src/migration/node.ts create mode 100644 packages/core/src/migration/schema.ts create mode 100644 packages/core/src/migration/statements.ts create mode 100644 packages/core/src/mysql/driver.ts delete mode 100644 packages/core/src/mysql/engine/tokens.ts delete mode 100644 packages/core/src/mysql/migrations.ts rename packages/core/src/{runtime/pg-arrays.ts => postgres/driver.ts} (60%) create mode 100644 packages/core/src/postgres/engine/db.ts create mode 100644 packages/core/src/postgres/engine/lexer/tokens.ts create mode 100644 packages/core/src/postgres/engine/literals.ts create mode 100644 packages/core/src/postgres/engine/ops.ts create mode 100644 packages/core/src/postgres/engine/types.ts create mode 100644 packages/core/src/postgres/engine/util.ts delete mode 100644 packages/core/src/postgres/migrations.ts create mode 100644 packages/core/src/postgres/schema/functions.ts create mode 100644 packages/core/src/postgres/schema/type-facts.ts create mode 100644 packages/core/src/reserved.ts delete mode 100644 packages/core/src/runtime/mysql-params.ts delete mode 100644 packages/core/src/runtime/sqlite-params.ts rename packages/core/src/{engine => sql}/carried.ts (77%) rename packages/core/src/{engine => sql}/contract.ts (66%) rename packages/core/src/{engine => sql}/infer.ts (88%) create mode 100644 packages/core/src/sql/lexis.ts create mode 100644 packages/core/src/sqlite/driver.ts delete mode 100644 packages/core/src/sqlite/engine/tokens.ts delete mode 100644 packages/core/src/sqlite/migrations.ts create mode 100644 packages/drizzle/src/column.ts create mode 100644 packages/drizzle/src/from-drizzle-pg.ts create mode 100644 packages/language-server/src/call.ts delete mode 100644 packages/language-server/src/complete/call.ts delete mode 100644 packages/language-server/src/complete/index.ts create mode 100644 packages/language-server/src/highlight/grammar.ts delete mode 100644 packages/language-server/src/highlight/lex.ts create mode 100644 packages/language-server/src/sql/dialect.ts create mode 100644 packages/language-server/src/sql/lex.ts create mode 100644 packages/language-server/src/sql/words.ts delete mode 100644 packages/mysql/src/literal.ts create mode 100644 packages/prisma/src/from-prisma-pg.ts create mode 100644 packages/prisma/src/model.ts create mode 100644 packages/prisma/src/provider.ts create mode 100644 packages/prisma/src/scalars.ts create mode 100644 packages/vscode/scripts/contributes.mts create mode 100644 packages/vscode/src/contributes.mts delete mode 100644 tests/run.ts delete mode 100644 tests/scenario.test.ts create mode 100644 tests/scenarios/cli-introspect-database-url/app.ts create mode 100644 tests/scenarios/cli-introspect-database-url/expected.snap create mode 100644 tests/scenarios/cli-introspect-database-url/package.json create mode 100644 tests/scenarios/cli-introspect-database-url/setup.ts create mode 100644 tests/scenarios/cli-introspect-sqlite-exclude/app.ts create mode 100644 tests/scenarios/cli-introspect-sqlite-exclude/expected.snap create mode 100644 tests/scenarios/cli-introspect-sqlite-exclude/package.json create mode 100644 tests/scenarios/cli-introspect-sqlite-exclude/setup.ts create mode 100644 tests/scenarios/cli-introspect-sqlite-virtual/app.ts create mode 100644 tests/scenarios/cli-introspect-sqlite-virtual/expected.snap create mode 100644 tests/scenarios/cli-introspect-sqlite-virtual/package.json create mode 100644 tests/scenarios/cli-introspect-sqlite-virtual/setup.ts create mode 100644 tests/scenarios/cli-introspect-type-name/expected.snap create mode 100644 tests/scenarios/cli-introspect-type-name/package.json create mode 100644 tests/scenarios/cli-introspect-type-name/setup.ts delete mode 100644 tests/scenarios/cli-invalid-command/app.ts create mode 100644 tests/scenarios/cli-main-io/app.ts create mode 100644 tests/scenarios/cli-main-io/expected.snap create mode 100644 tests/scenarios/cli-main-io/package.json create mode 100644 tests/scenarios/cli-main-io/setup.ts create mode 100644 tests/scenarios/dollar-identifiers/app.ts create mode 100644 tests/scenarios/dollar-identifiers/expected.snap rename tests/scenarios/{pg-nested-comments-runtime => dollar-identifiers}/package.json (61%) create mode 100644 tests/scenarios/drizzle-mysql-serverless/app.ts create mode 100644 tests/scenarios/drizzle-mysql-serverless/expected.snap create mode 100644 tests/scenarios/drizzle-mysql-serverless/package.json create mode 100644 tests/scenarios/kysely-mysql-databases/app.ts create mode 100644 tests/scenarios/kysely-mysql-databases/expected.snap create mode 100644 tests/scenarios/kysely-mysql-databases/package.json create mode 100644 tests/scenarios/kysely-sqlite-attached/app.ts create mode 100644 tests/scenarios/kysely-sqlite-attached/expected.snap create mode 100644 tests/scenarios/kysely-sqlite-attached/package.json create mode 100644 tests/scenarios/migrations-mysql-keyword-names/app.ts create mode 100644 tests/scenarios/migrations-mysql-keyword-names/expected.snap create mode 100644 tests/scenarios/migrations-mysql-keyword-names/package.json create mode 100644 tests/scenarios/migrations-mysql-values/app.ts create mode 100644 tests/scenarios/migrations-mysql-values/expected.snap create mode 100644 tests/scenarios/migrations-mysql-values/package.json create mode 100644 tests/scenarios/migrations-postgres-atomic-functions/app.ts create mode 100644 tests/scenarios/migrations-postgres-atomic-functions/expected.snap create mode 100644 tests/scenarios/migrations-postgres-atomic-functions/package.json create mode 100644 tests/scenarios/migrations-postgres-json-generate/app.ts create mode 100644 tests/scenarios/migrations-postgres-json-generate/expected.snap create mode 100644 tests/scenarios/migrations-postgres-json-generate/migrations/001-settings.ts create mode 100644 tests/scenarios/migrations-postgres-json-generate/package.json create mode 100644 tests/scenarios/migrations-postgres-json-generate/typesql.config.ts create mode 100644 tests/scenarios/migrations-postgres-values/app.ts create mode 100644 tests/scenarios/migrations-postgres-values/expected.snap create mode 100644 tests/scenarios/migrations-postgres-values/package.json create mode 100644 tests/scenarios/migrations-sqlite-keyword-names/app.ts create mode 100644 tests/scenarios/migrations-sqlite-keyword-names/expected.snap create mode 100644 tests/scenarios/migrations-sqlite-keyword-names/package.json create mode 100644 tests/scenarios/mysql-insert-column-order/app.ts create mode 100644 tests/scenarios/mysql-insert-column-order/expected.snap create mode 100644 tests/scenarios/mysql-insert-column-order/package.json create mode 100644 tests/scenarios/mysql-json-array-rejection/app.ts create mode 100644 tests/scenarios/mysql-json-array-rejection/expected.snap create mode 100644 tests/scenarios/mysql-json-array-rejection/package.json create mode 100644 tests/scenarios/mysql-json-object-rejection/app.ts create mode 100644 tests/scenarios/mysql-json-object-rejection/expected.snap create mode 100644 tests/scenarios/mysql-json-object-rejection/package.json create mode 100644 tests/scenarios/mysql-json-strings/app.ts create mode 100644 tests/scenarios/mysql-json-strings/expected.snap create mode 100644 tests/scenarios/mysql-json-strings/package.json create mode 100644 tests/scenarios/mysql-multiple-statements/app.ts create mode 100644 tests/scenarios/mysql-multiple-statements/expected.snap create mode 100644 tests/scenarios/mysql-multiple-statements/package.json create mode 100644 tests/scenarios/mysql-quoted-text/app.ts create mode 100644 tests/scenarios/mysql-quoted-text/expected.snap create mode 100644 tests/scenarios/mysql-quoted-text/package.json create mode 100644 tests/scenarios/mysql-serverless-literals/app.ts create mode 100644 tests/scenarios/mysql-serverless-literals/expected.snap create mode 100644 tests/scenarios/mysql-serverless-literals/package.json create mode 100644 tests/scenarios/mysql-where-narrowing/app.ts create mode 100644 tests/scenarios/mysql-where-narrowing/expected.snap create mode 100644 tests/scenarios/mysql-where-narrowing/package.json create mode 100644 tests/scenarios/pg-cast-parameters/app.ts create mode 100644 tests/scenarios/pg-cast-parameters/expected.snap create mode 100644 tests/scenarios/pg-cast-parameters/package.json create mode 100644 tests/scenarios/pg-char-type/app.ts create mode 100644 tests/scenarios/pg-char-type/expected.snap create mode 100644 tests/scenarios/pg-char-type/package.json create mode 100644 tests/scenarios/pg-common-type-rejection/app.ts create mode 100644 tests/scenarios/pg-common-type-rejection/expected.snap create mode 100644 tests/scenarios/pg-common-type-rejection/package.json create mode 100644 tests/scenarios/pg-declared-function-calls/app.ts create mode 100644 tests/scenarios/pg-declared-function-calls/expected.snap create mode 100644 tests/scenarios/pg-declared-function-calls/package.json create mode 100644 tests/scenarios/pg-engine-rejection/app.ts create mode 100644 tests/scenarios/pg-engine-rejection/expected.snap create mode 100644 tests/scenarios/pg-engine-rejection/package.json create mode 100644 tests/scenarios/pg-function-call-parameters/app.ts create mode 100644 tests/scenarios/pg-function-call-parameters/expected.snap create mode 100644 tests/scenarios/pg-function-call-parameters/package.json create mode 100644 tests/scenarios/pg-grouping-sets-parameters/app.ts create mode 100644 tests/scenarios/pg-grouping-sets-parameters/expected.snap create mode 100644 tests/scenarios/pg-grouping-sets-parameters/package.json create mode 100644 tests/scenarios/pg-meta-key-relations/app.ts create mode 100644 tests/scenarios/pg-meta-key-relations/expected.snap create mode 100644 tests/scenarios/pg-meta-key-relations/package.json create mode 100644 tests/scenarios/pg-multiple-statements/app.ts create mode 100644 tests/scenarios/pg-multiple-statements/expected.snap create mode 100644 tests/scenarios/pg-multiple-statements/package.json delete mode 100644 tests/scenarios/pg-nested-comments-runtime/app.ts delete mode 100644 tests/scenarios/pg-nested-comments-runtime/expected.snap create mode 100644 tests/scenarios/pg-null-array-elements/app.ts create mode 100644 tests/scenarios/pg-null-array-elements/expected.snap create mode 100644 tests/scenarios/pg-null-array-elements/package.json create mode 100644 tests/scenarios/pg-quantified-null-elements/app.ts create mode 100644 tests/scenarios/pg-quantified-null-elements/expected.snap create mode 100644 tests/scenarios/pg-quantified-null-elements/package.json create mode 100644 tests/scenarios/pg-text-types/app.ts create mode 100644 tests/scenarios/pg-text-types/expected.snap create mode 100644 tests/scenarios/pg-text-types/package.json create mode 100644 tests/scenarios/pg-values-defaults/app.ts create mode 100644 tests/scenarios/pg-values-defaults/expected.snap create mode 100644 tests/scenarios/pg-values-defaults/package.json create mode 100644 tests/scenarios/pg-where-narrowing/app.ts create mode 100644 tests/scenarios/pg-where-narrowing/expected.snap create mode 100644 tests/scenarios/pg-where-narrowing/package.json delete mode 100644 tests/scenarios/prisma-client-mysql/app.ts delete mode 100644 tests/scenarios/prisma-client-mysql/expected.snap delete mode 100644 tests/scenarios/prisma-client-mysql/package.json delete mode 100644 tests/scenarios/prisma-client-mysql/prisma.config.ts delete mode 100644 tests/scenarios/prisma-client-mysql/schema.prisma delete mode 100644 tests/scenarios/prisma-client-postgres/app.ts delete mode 100644 tests/scenarios/prisma-client-postgres/expected.snap delete mode 100644 tests/scenarios/prisma-client-postgres/package.json delete mode 100644 tests/scenarios/prisma-client-postgres/prisma.config.ts delete mode 100644 tests/scenarios/prisma-client-postgres/schema.prisma delete mode 100644 tests/scenarios/prisma-client-sqlite/app.ts delete mode 100644 tests/scenarios/prisma-client-sqlite/expected.snap delete mode 100644 tests/scenarios/prisma-client-sqlite/package.json delete mode 100644 tests/scenarios/prisma-client-sqlite/prisma.config.ts delete mode 100644 tests/scenarios/prisma-client-sqlite/schema.prisma rename tests/scenarios/{observation-shapes => runner-observation-shapes}/app.ts (100%) rename tests/scenarios/{observation-shapes => runner-observation-shapes}/expected.snap (92%) rename tests/scenarios/{observation-shapes => runner-observation-shapes}/package.json (58%) rename tests/scenarios/{compiler-undeclared-dependency => runner-undeclared-dependency}/app.ts (100%) rename tests/scenarios/{compiler-undeclared-dependency => runner-undeclared-dependency}/expected.snap (71%) rename tests/scenarios/{compiler-undeclared-dependency => runner-undeclared-dependency}/package.json (72%) create mode 100644 tests/scenarios/sqlite-hidden-columns/app.ts create mode 100644 tests/scenarios/sqlite-hidden-columns/expected.snap create mode 100644 tests/scenarios/sqlite-hidden-columns/migrations/001-docs.ts create mode 100644 tests/scenarios/sqlite-hidden-columns/package.json create mode 100644 tests/scenarios/sqlite-hidden-columns/typesql.config.ts create mode 100644 tests/scenarios/sqlite-insert-column-order/app.ts create mode 100644 tests/scenarios/sqlite-insert-column-order/expected.snap create mode 100644 tests/scenarios/sqlite-insert-column-order/package.json create mode 100644 tests/scenarios/sqlite-json-type-map/app.ts create mode 100644 tests/scenarios/sqlite-json-type-map/expected.snap create mode 100644 tests/scenarios/sqlite-json-type-map/package.json create mode 100644 tests/scenarios/sqlite-negated-zero/app.ts create mode 100644 tests/scenarios/sqlite-negated-zero/expected.snap create mode 100644 tests/scenarios/sqlite-negated-zero/package.json create mode 100644 tests/scenarios/sqlite-quoted-text/app.ts create mode 100644 tests/scenarios/sqlite-quoted-text/expected.snap create mode 100644 tests/scenarios/sqlite-quoted-text/package.json create mode 100644 tests/scenarios/sqlite-values-columns/app.ts create mode 100644 tests/scenarios/sqlite-values-columns/expected.snap create mode 100644 tests/scenarios/sqlite-values-columns/package.json create mode 100644 tests/scenarios/sqlite-where-narrowing/app.ts create mode 100644 tests/scenarios/sqlite-where-narrowing/expected.snap create mode 100644 tests/scenarios/sqlite-where-narrowing/package.json rename tests/{tsconfig.json => scenarios/tsconfig.base.json} (72%) create mode 100644 tests/support/paths.ts create mode 100644 tests/support/scenario.ts create mode 100644 tools/biome/build.ts create mode 100644 tools/biome/shared/function_like.grit create mode 100644 tools/biome/shared/unparenthesized.grit create mode 100644 tools/biome/shared/wrapped.grit create mode 100644 tools/biome/src/no-builtin-instanceof.grit create mode 100644 tools/biome/src/no-chained-type-assertions.grit create mode 100644 tools/biome/src/no-conditional-empty-object-spread.grit create mode 100644 tools/biome/src/no-hidden-unknown-and-object-types.grit create mode 100644 tools/biome/src/no-known-value-widening.grit create mode 100644 tools/biome/src/no-reduce-accumulator-copy.grit create mode 100644 tools/biome/src/no-reflection-and-module-mocking.grit create mode 100644 tools/biome/src/no-spread-new-set.grit create mode 100644 tools/biome/src/no-unknown-parameters.grit create mode 100644 tools/biome/src/no-unsafe-dictionary-type.grit create mode 100644 tools/generated.ts create mode 100644 tools/packages.ts create mode 100644 tools/servers.ts diff --git a/.changeset/afraid-mangos-carry.md b/.changeset/afraid-mangos-carry.md new file mode 100644 index 00000000..5ea1ac45 --- /dev/null +++ b/.changeset/afraid-mangos-carry.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +A `Json` value reads by one rule on every database: as `T`, unless the type map reads JSON as a `string`, as SQLite's maps do. A MySQL map with `json: string` (mysql2's `jsonStrings`) now reads `Json` columns as text, and a SQLite map that reads JSON parsed reads them as `T`. diff --git a/.changeset/ancient-birds-listen.md b/.changeset/ancient-birds-listen.md new file mode 100644 index 00000000..4bac9549 --- /dev/null +++ b/.changeset/ancient-birds-listen.md @@ -0,0 +1,7 @@ +--- +"@typesql/pg": patch +"@typesql/mysql": patch +"@typesql/sqlite": patch +--- + +The introspection `main(argv, io?)` takes an `io` with any of `stdout`, `stderr`, `cwd` and `env`, the process's by default. `typesql introspect` reports a failed connection as `cannot connect to PostgreSQL: …` (or MySQL). diff --git a/.changeset/angry-kiwis-brush.md b/.changeset/angry-kiwis-brush.md new file mode 100644 index 00000000..677fbedb --- /dev/null +++ b/.changeset/angry-kiwis-brush.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +MySQL and MariaDB: `WHERE x = ANY (SELECT …)` and `x <> SOME (SELECT …)` make `x` non-null, like `IN (SELECT …)`. diff --git a/.changeset/angry-seas-perform.md b/.changeset/angry-seas-perform.md new file mode 100644 index 00000000..4c899b05 --- /dev/null +++ b/.changeset/angry-seas-perform.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +A file that imports a type such as `Col` or `SchemaOf` from `@typesql/core` no longer loads the type declarations of every dialect's engine. diff --git a/.changeset/big-walls-wait.md b/.changeset/big-walls-wait.md new file mode 100644 index 00000000..2059c22e --- /dev/null +++ b/.changeset/big-walls-wait.md @@ -0,0 +1,7 @@ +--- +"@typesql/pg": patch +"@typesql/mysql": patch +"@typesql/drizzle": patch +--- + +A script of several statements, such as one passed to `sql.as()`, resolves to its last statement's rows and count, as the migrator's `.raw()` does: node-postgres scripts no longer fail with `Cannot read properties of undefined`, and mysql2 scripts (with `multipleStatements`) no longer return every statement's result as rows. diff --git a/.changeset/brave-ants-drum.md b/.changeset/brave-ants-drum.md new file mode 100644 index 00000000..757f0273 --- /dev/null +++ b/.changeset/brave-ants-drum.md @@ -0,0 +1,5 @@ +--- +"@typesql/drizzle": patch +--- + +On TiDB Cloud and PlanetScale serverless, values are written into the statement as escaped literals, as `@typesql/mysql` does: the client's own `?` substitution also replaced a `?` inside a string literal. diff --git a/.changeset/brave-wolves-exist.md b/.changeset/brave-wolves-exist.md new file mode 100644 index 00000000..f044762f --- /dev/null +++ b/.changeset/brave-wolves-exist.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +TypeSQL reads MySQL, MariaDB and SQLite statements by their own rules: in `sql()`, `sql.fragment()`, `sql.raw()` and the `$n` rewriting of the MySQL and SQLite drivers, backtick- and bracket-quoted names, backslash escapes in MySQL strings, `#` comments and block comments, which these databases do not nest, no longer hide or invent `:name` parameters and inline values. diff --git a/.changeset/bright-boxes-exist.md b/.changeset/bright-boxes-exist.md new file mode 100644 index 00000000..8c22587a --- /dev/null +++ b/.changeset/bright-boxes-exist.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +The migrator binds MySQL and MariaDB values as `@typesql/mysql` does: a plain object is refused with `cannot bind [object Object] as a MySQL value; use sql.json() for JSON` instead of being written into the statement as SQL text. With a `url`, it also connects with the mariadb connector when mysql2 is not installed. diff --git a/.changeset/calm-tables-yawn.md b/.changeset/calm-tables-yawn.md new file mode 100644 index 00000000..1a3e6cb9 --- /dev/null +++ b/.changeset/calm-tables-yawn.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +In PostgreSQL, a cast array parameter such as `$1::text[]` has nullable elements, since it accepts and returns them: `SELECT x FROM unnest($1::text[]) AS x` gives `x: string | null`. A cast parameter that accepts `NULL` makes a column nullable in every `SELECT`; before, the result depended on whether the select list held a `$` or a comment. PostgreSQL migrations accept `CREATE FUNCTION` and `CREATE PROCEDURE` with a `BEGIN ATOMIC … END` body. diff --git a/.changeset/clever-tigers-try.md b/.changeset/clever-tigers-try.md new file mode 100644 index 00000000..bc4b15a6 --- /dev/null +++ b/.changeset/clever-tigers-try.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": minor +--- + +`@typesql/core/adapter` holds what the adapters share: connections (`connectPostgres`, `connectMysql`, `connectSqlite`), result normalisers (`pgResult`, `mysqlResult`), value encoders (`jsonText`, `mysqlBind`, `mysqlInline`, `sqliteBindings`), `pooledTransaction`, `createFamilySql`, the schema source writer (`schemaWriter`, `columnSource`, `doc`, …) and the `typesql introspect` shell (`introspectCommand`). `SqlDriver.transaction` also receives, after `client`, `depth` and `fn`, the name of the savepoint to make instead of a transaction of its own (`undefined` when none encloses the client), and a driver's optional `inTransaction(client)` tells that its client is a transaction already; drivers written for 7.0 keep working. A `Dialect` names the lexis of its SQL text (`lexis`). `@typesql/core/migration` exports `loadMigrator`, the config loading of `typesql migrate`. diff --git a/.changeset/cool-boats-compete.md b/.changeset/cool-boats-compete.md new file mode 100644 index 00000000..4ebce350 --- /dev/null +++ b/.changeset/cool-boats-compete.md @@ -0,0 +1,5 @@ +--- +"@typesql/kysely": patch +--- + +With `mysql` and `sqlite`, a `"db.table"` key of a Kysely `DB` is the table `table` of another (or attached) database, as MySQL and SQLite name it in SQL (`FROM db.table`); a SQLite `"main.table"` key is the main database's table. They were flat keys no statement could name. MySQL and SQLite columns are the dialect's `Col`. diff --git a/.changeset/dry-waves-tan.md b/.changeset/dry-waves-tan.md new file mode 100644 index 00000000..a0393f1e --- /dev/null +++ b/.changeset/dry-waves-tan.md @@ -0,0 +1,5 @@ +--- +"typesql": patch +--- + +Without semantic highlighting, names, `:name` parameters, casts and `$tag$` strings with non-ASCII letters are colored, and `--` or `/*` right after an operator starts a comment. diff --git a/.changeset/eager-pandas-drop.md b/.changeset/eager-pandas-drop.md new file mode 100644 index 00000000..1a87b491 --- /dev/null +++ b/.changeset/eager-pandas-drop.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +MySQL, MariaDB and SQLite: `INSERT INTO t VALUES (…)` without a column list no longer types its parameters against the wrong columns when the schema has no `position` flags; it checks the number of values and types the parameters as `unknown`. The columns of an `ON DUPLICATE KEY UPDATE` row alias with a column list (`AS new(a, b)`) now follow the table's positions, and `FIRST` / `AFTER` in a migration's `ALTER TABLE` no longer invent positions for a table without them. diff --git a/.changeset/early-turkeys-joke.md b/.changeset/early-turkeys-joke.md new file mode 100644 index 00000000..2fe1470a --- /dev/null +++ b/.changeset/early-turkeys-joke.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +`typesql migrate generate` writes a PostgreSQL JSON column a migration types with `Json` (in `.raw<…>()`) as `Json` with its import, like `typesql introspect`, instead of a schema file that does not compile. diff --git a/.changeset/fair-trees-share.md b/.changeset/fair-trees-share.md new file mode 100644 index 00000000..082e6b34 --- /dev/null +++ b/.changeset/fair-trees-share.md @@ -0,0 +1,5 @@ +--- +"@typesql/prisma": patch +--- + +The generator writes a MySQL auto-increment column as `Col<"int", { autoIncrement: true }>`, without the `hasDefault: true` that `autoIncrement` implies. diff --git a/.changeset/giant-flowers-wave.md b/.changeset/giant-flowers-wave.md new file mode 100644 index 00000000..2ae3f9a2 --- /dev/null +++ b/.changeset/giant-flowers-wave.md @@ -0,0 +1,5 @@ +--- +"@typesql/drizzle": none +--- + +One conversion of a Drizzle schema module for the three dialects, with each dialect's column types. diff --git a/.changeset/good-frogs-own.md b/.changeset/good-frogs-own.md new file mode 100644 index 00000000..337cbaa6 --- /dev/null +++ b/.changeset/good-frogs-own.md @@ -0,0 +1,6 @@ +--- +"@typesql/core": patch +"@typesql/sqlite": patch +--- + +The schema files of `typesql migrate generate` and `typesql introspect --dialect sqlite` give the hidden columns of a virtual table (FTS5's table-named column and `rank`) `hasDefault: true`, as the schema a migration leaves does: an INSERT without a column list leaves them out. diff --git a/.changeset/good-kings-refuse.md b/.changeset/good-kings-refuse.md new file mode 100644 index 00000000..c7079b00 --- /dev/null +++ b/.changeset/good-kings-refuse.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: `Col<"char">`, as introspection writes it, and `"char"` in DDL and typed literals are the one-byte `"char"` type rather than `char(n)` (`bpchar`). diff --git a/.changeset/heavy-olives-hear.md b/.changeset/heavy-olives-hear.md new file mode 100644 index 00000000..58a27e6c --- /dev/null +++ b/.changeset/heavy-olives-hear.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": minor +--- + +A PostgreSQL migration's `db` reads rows as the migrator's `pg` client returns them, with the new `NodePostgresTypeMap` from `@typesql/core/postgres`: `record`, `void`, `"char"[]` and `aclitem[]` values are strings and intervals have `toPostgres()`. diff --git a/.changeset/honest-melons-thank.md b/.changeset/honest-melons-thank.md new file mode 100644 index 00000000..5b346d97 --- /dev/null +++ b/.changeset/honest-melons-thank.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +On SQLite, which has no `DEFAULT` in a `VALUES` row, `sql.values` rows that give different columns, or a value that can be `undefined` where another row sends one, are a type error instead of a runtime error. diff --git a/.changeset/itchy-icons-flash.md b/.changeset/itchy-icons-flash.md new file mode 100644 index 00000000..2166ac52 --- /dev/null +++ b/.changeset/itchy-icons-flash.md @@ -0,0 +1,6 @@ +--- +"@typesql/kysely": patch +"@typesql/drizzle": patch +--- + +On MySQL and MariaDB, values are bound as `@typesql/mysql` binds them: a plain object is refused with `cannot bind [object Object] as a MySQL value; use sql.json() for JSON` instead of being written into the statement as SQL text. diff --git a/.changeset/itchy-shirts-remain.md b/.changeset/itchy-shirts-remain.md new file mode 100644 index 00000000..9f8a86ce --- /dev/null +++ b/.changeset/itchy-shirts-remain.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +The types of `sql()`, `sql.fragment()`, `sql.combine()`, `sql.and()` and `sql.or()` read MySQL, MariaDB and SQLite text by their own rules too: backtick- and bracket-quoted names, backslash escapes in MySQL strings, `#` comments, `--` without a following space in MySQL and block comments, which these databases do not nest, no longer make `sql()` require or number `:name` and `$n` parameters the database does not have. diff --git a/.changeset/lazy-fireants-swim.md b/.changeset/lazy-fireants-swim.md new file mode 100644 index 00000000..b1e28b5f --- /dev/null +++ b/.changeset/lazy-fireants-swim.md @@ -0,0 +1,5 @@ +--- +"@typesql/sqlite": patch +--- + +`typesql introspect` keeps a table whose name starts like a virtual table's, such as `docs_archive` next to an FTS table `docs`: shadow tables are the ones SQLite reports as such (SQLite 3.37 or later). diff --git a/.changeset/little-fireants-own.md b/.changeset/little-fireants-own.md new file mode 100644 index 00000000..60adef2f --- /dev/null +++ b/.changeset/little-fireants-own.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: arrays with NULL elements keep `(T | null)[]` through `VALUES` and recursive CTEs, and a parameter combined with an aggregate under `GROUP BY ROLLUP`, `CUBE` or `GROUPING SETS` accepts `null`, since the total row's aggregate can be NULL. diff --git a/.changeset/loud-eels-hunt.md b/.changeset/loud-eels-hunt.md new file mode 100644 index 00000000..59c5be2e --- /dev/null +++ b/.changeset/loud-eels-hunt.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: a `$functions` signature's `strict: [n]` checks arguments past the eighth, and an unknown function's message suggests the closest `pg_catalog` function too (`crc33` → `crc32`). diff --git a/.changeset/loud-wolves-draw.md b/.changeset/loud-wolves-draw.md new file mode 100644 index 00000000..69266eae --- /dev/null +++ b/.changeset/loud-wolves-draw.md @@ -0,0 +1,5 @@ +--- +"@typesql/postgres": none +--- + +Take the savepoint name of a nested transaction from @typesql/core. diff --git a/.changeset/mighty-kings-cover.md b/.changeset/mighty-kings-cover.md new file mode 100644 index 00000000..b13d912a --- /dev/null +++ b/.changeset/mighty-kings-cover.md @@ -0,0 +1,5 @@ +--- +"@typesql/language-server": patch +--- + +Completions work in `sql.prepare()` and `sql.raw()` strings, and in a migration's `db()`, `.ddl()`, `.add()` and `.raw()` strings. SQLite strings are read as SQLite: `[name]` and backtick names, `?`, `?1`, `@name` and `$name` parameters, and block comments that don't nest; and `FROM main.` and `FROM temp.` list the tables, as their columns already resolved. In PostgreSQL, backslash escapes in `E'…'` strings no longer end the string early. Names that need quoting come from `@typesql/core`'s keyword lists. diff --git a/.changeset/mighty-kings-hide.md b/.changeset/mighty-kings-hide.md new file mode 100644 index 00000000..fdd3036d --- /dev/null +++ b/.changeset/mighty-kings-hide.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": minor +--- + +`@typesql/core/adapter` exports `familyStatement`: a statement as a PostgreSQL, SQLite or MySQL driver takes it, with `$n` parameters or `?` placeholders. diff --git a/.changeset/new-wolves-kneel.md b/.changeset/new-wolves-kneel.md new file mode 100644 index 00000000..52dc8dd4 --- /dev/null +++ b/.changeset/new-wolves-kneel.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +SQLite: `x / - -0` (and `%`) is nullable, since `- -0` is zero, and `x / +2` is not. diff --git a/.changeset/nice-bees-jam.md b/.changeset/nice-bees-jam.md new file mode 100644 index 00000000..b7972390 --- /dev/null +++ b/.changeset/nice-bees-jam.md @@ -0,0 +1,7 @@ +--- +"@typesql/core": patch +"@typesql/mysql": patch +"@typesql/drizzle": patch +--- + +On the HTTP drivers (TiDB Cloud and PlanetScale serverless), a `'` in a value is written as `''`, so a value cannot end its string on a server with `NO_BACKSLASH_ESCAPES`. Backslashes in values are still corrupted in that mode. diff --git a/.changeset/nice-dodos-type.md b/.changeset/nice-dodos-type.md new file mode 100644 index 00000000..7b542c66 --- /dev/null +++ b/.changeset/nice-dodos-type.md @@ -0,0 +1,5 @@ +--- +"@typesql/better-auth": none +--- + +Read the per-database SQL from one record of the supported dialects. diff --git a/.changeset/old-taxis-dig.md b/.changeset/old-taxis-dig.md new file mode 100644 index 00000000..64ce4a4a --- /dev/null +++ b/.changeset/old-taxis-dig.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: `CASE`, `COALESCE`, `UNION`, `JOIN … USING` and the other constructs that find a common type follow PostgreSQL's implicit casts: of two types that convert to each other the first is kept (`coalesce(oid_col, regclass_col)` is `oid`), and `"char"` with `text` or `time` with `interval` are reported as types that cannot be matched. diff --git a/.changeset/polite-turkeys-cough.md b/.changeset/polite-turkeys-cough.md new file mode 100644 index 00000000..e02569bb --- /dev/null +++ b/.changeset/polite-turkeys-cough.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: `CASE`, `COALESCE`, `GREATEST`, `LEAST` and `ARRAY[]` over values with no common type are errors like `UNION` already was (`CASE types point and integer cannot be matched`), types such as `oid` and `integer` that convert implicitly combine instead of failing, and `= ANY(x)` rejects every non-array `x`. diff --git a/.changeset/pretty-eggs-rest.md b/.changeset/pretty-eggs-rest.md new file mode 100644 index 00000000..a09ff242 --- /dev/null +++ b/.changeset/pretty-eggs-rest.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: `jsonpath`, `regconfig`, `refcursor`, `pg_snapshot` and `txid_snapshot` values are typed `string`, as node-postgres and postgres.js return them, instead of `unknown`. diff --git a/.changeset/proud-fireants-tie.md b/.changeset/proud-fireants-tie.md new file mode 100644 index 00000000..ad28971c --- /dev/null +++ b/.changeset/proud-fireants-tie.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": minor +--- + +A new `@typesql/core/reserved` entry point exports each dialect's reserved words and the schema keys that name no table, for editor tooling such as `@typesql/language-server`. The parsers read the same word lists. diff --git a/.changeset/rare-spiders-boil.md b/.changeset/rare-spiders-boil.md new file mode 100644 index 00000000..64ece365 --- /dev/null +++ b/.changeset/rare-spiders-boil.md @@ -0,0 +1,5 @@ +--- +"@typesql/pg": none +--- + +`PgTypeMap` extends core's `NodePostgresTypeMap`; its types are unchanged. diff --git a/.changeset/real-mice-invent.md b/.changeset/real-mice-invent.md new file mode 100644 index 00000000..6339c0da --- /dev/null +++ b/.changeset/real-mice-invent.md @@ -0,0 +1,5 @@ +--- +"@typesql/prisma": patch +--- + +`executor(prisma)` without a `dialect` throws for a Prisma provider other than postgresql, sqlite and mysql, as the `typesql-prisma` generator does, instead of writing PostgreSQL's placeholders. `FromPrisma`, `FromPrismaMysql` and `FromPrismaSqlite` type columns as the dialect's `Col`. diff --git a/.changeset/real-trains-love.md b/.changeset/real-trains-love.md new file mode 100644 index 00000000..09a17871 --- /dev/null +++ b/.changeset/real-trains-love.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL and SQLite: a `WHERE` condition that rejects NULL in a column of an outer-joined table gives all of that table's columns their declared nullability again, as MySQL already did (`LEFT JOIN posts p … WHERE p.title = $1` makes `p.id` non-null). diff --git a/.changeset/sharp-eels-push.md b/.changeset/sharp-eels-push.md new file mode 100644 index 00000000..8e62a4dd --- /dev/null +++ b/.changeset/sharp-eels-push.md @@ -0,0 +1,5 @@ +--- +"@typesql/sqlite": patch +--- + +`typesql introspect --dialect sqlite --exclude "a, b"` trims the spaces around each pattern, as the other dialects do. diff --git a/.changeset/slow-poets-exist.md b/.changeset/slow-poets-exist.md new file mode 100644 index 00000000..2b16c0a4 --- /dev/null +++ b/.changeset/slow-poets-exist.md @@ -0,0 +1,5 @@ +--- +"@typesql/cli": patch +--- + +`main(argv, io)` writes the output of `typesql introspect` through `io`, as it does for `typesql migrate`, instead of to the process's stdout and stderr. diff --git a/.changeset/slow-turtles-live.md b/.changeset/slow-turtles-live.md new file mode 100644 index 00000000..24da8389 --- /dev/null +++ b/.changeset/slow-turtles-live.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +`x NOT IN (SELECT …)`, `x <> ALL (…)` and SQLite's `x NOT IN ()` no longer make `x` non-null: they are true for a NULL `x` when the subquery or list is empty. diff --git a/.changeset/small-flowers-juggle.md b/.changeset/small-flowers-juggle.md new file mode 100644 index 00000000..2e274832 --- /dev/null +++ b/.changeset/small-flowers-juggle.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +A PostgreSQL or SQLite name with `$` in it (`a$b$`, `x$1`) is read as a name: in PostgreSQL `sql()` text, `a$b$` no longer opens a dollar quote that hid the `:name` parameters and inline values after it, and `x$1` is no longer counted or renumbered as parameter `$1` when a statement runs. diff --git a/.changeset/sour-hats-enjoy.md b/.changeset/sour-hats-enjoy.md new file mode 100644 index 00000000..4e1c472f --- /dev/null +++ b/.changeset/sour-hats-enjoy.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: a statement that reads or writes `"$types"`, `"$functions"` or `"$nullParams"` reports `relation "…" does not exist`, as PostgreSQL does, instead of `"…" is not a table`. diff --git a/.changeset/tame-boxes-share.md b/.changeset/tame-boxes-share.md new file mode 100644 index 00000000..0341f5fe --- /dev/null +++ b/.changeset/tame-boxes-share.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +On MySQL and MariaDB, a plain object for a JSON parameter is a type error, as the drivers refuse it at runtime: pass `sql.json(value)` or JSON text. An inline array or object for a JSON parameter there says `must be passed as sql.json(value)`, and MySQL and SQLite messages name a `Json` column type `json` instead of printing its internal type. diff --git a/.changeset/tame-spiders-sneeze.md b/.changeset/tame-spiders-sneeze.md new file mode 100644 index 00000000..127f0290 --- /dev/null +++ b/.changeset/tame-spiders-sneeze.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +MySQL and MariaDB: the error for a JS array passed for a JSON parameter no longer mentions node-postgres; migration `ddl()` errors show the `SqlError` message instead of internal type names. diff --git a/.changeset/thick-apes-greet.md b/.changeset/thick-apes-greet.md new file mode 100644 index 00000000..722f5b63 --- /dev/null +++ b/.changeset/thick-apes-greet.md @@ -0,0 +1,5 @@ +--- +"typesql": patch +--- + +Completions work in `sql.prepare()` and `sql.raw()` strings, and in a migration's `db()`, `.ddl()`, `.add()` and `.raw()` strings. With semantic highlighting, SQLite strings are colored as SQLite (`[name]` and backtick names, `?`, `@name` and `$name` parameters), and PostgreSQL strings as PostgreSQL: backslash escapes in `E'…'` strings, and `#` and backticks as operators, not a comment and a name. In SQLite, completions after `main.` or `temp.` list the tables, with those the statement uses first. diff --git a/.changeset/thick-buses-attack.md b/.changeset/thick-buses-attack.md new file mode 100644 index 00000000..4702b8db --- /dev/null +++ b/.changeset/thick-buses-attack.md @@ -0,0 +1,5 @@ +--- +"@typesql/language-server": patch +--- + +The package ships its type declarations, which read an optional `typescript` peer (7.1 or later), and exports only its main entry point (the `typesql-language-server` command stays). In SQLite, completions after `main.` or `temp.` list the tables the statement uses first, as after `public.` in PostgreSQL. diff --git a/.changeset/thick-geckos-do.md b/.changeset/thick-geckos-do.md new file mode 100644 index 00000000..193e06f1 --- /dev/null +++ b/.changeset/thick-geckos-do.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +In PostgreSQL, a function call holding a parameter in the values of `INSERT` or `MERGE` no longer fails with an "infinitely circular" error that turned later statements in the file into `any`, nor does `(f(x)).*`. `x = ANY(array)` and `x = ALL(array)` are nullable when the array can hold `NULL` elements. In migrations, `begin` and `end` used as names (`NEW.end`, `AS end`, a parameter or routine called `begin`) no longer hide a later `COMMIT` or reject a valid trigger, and `CASE … END FOR UPDATE` ends the `CASE`. diff --git a/.changeset/tiny-cats-learn.md b/.changeset/tiny-cats-learn.md new file mode 100644 index 00000000..844c5e96 --- /dev/null +++ b/.changeset/tiny-cats-learn.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +`sql.withPlugin()` on the `sql` of `createSql()` keeps its dialect's type, as on the adapters' `sql`: its `.dialect`, and an `Sql` annotation, compile. diff --git a/.changeset/tough-pumas-count.md b/.changeset/tough-pumas-count.md new file mode 100644 index 00000000..17f17d9d --- /dev/null +++ b/.changeset/tough-pumas-count.md @@ -0,0 +1,5 @@ +--- +"@typesql/cli": patch +--- + +`typesql introspect` without `--dialect` or `--url` reads MySQL or MariaDB when `DATABASE_URL` is a `mysql://` or `mariadb://` URL, as for `--url`, instead of connecting to it as PostgreSQL. diff --git a/.changeset/wild-dodos-judge.md b/.changeset/wild-dodos-judge.md new file mode 100644 index 00000000..2de01615 --- /dev/null +++ b/.changeset/wild-dodos-judge.md @@ -0,0 +1,5 @@ +--- +"@typesql/core": patch +--- + +PostgreSQL: a window name used inside `POSITION`, `SUBSTRING`, `TRIM` or `OVERLAY` must be defined, and a built-in function called with a number of arguments none of its overloads takes is an error (`function upper(text, text) does not exist`) instead of an `unknown` column. diff --git a/.changeset/young-roses-sniff.md b/.changeset/young-roses-sniff.md new file mode 100644 index 00000000..1efd4602 --- /dev/null +++ b/.changeset/young-roses-sniff.md @@ -0,0 +1,9 @@ +--- +"@typesql/core": patch +"@typesql/pg": patch +"@typesql/mysql": patch +"@typesql/sqlite": patch +"@typesql/prisma": patch +--- + +`typesql introspect --type-name`, `generateSource({ typeName })` and the Prisma generator's `typeName` reject a name that is not a TypeScript identifier (`typesql introspect` exits with 2) instead of writing a schema file that does not compile. diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 965c0ee2..87f57c1d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -31,11 +31,12 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26 + node-version-file: .node-version - run: pnpm install --frozen-lockfile - run: pnpm lint - run: pnpm build - run: pnpm typecheck + - run: pnpm knip - run: pnpm --filter typesql exec vsce package --no-dependencies --out "$RUNNER_TEMP/typesql.vsix" - run: pnpm docs:build @@ -51,7 +52,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26 + node-version-file: .node-version - run: node tools/check-intents.ts "$BASE" env: BASE: ${{ github.event.pull_request.base.sha || github.event.before }} @@ -71,12 +72,16 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26.8.1 + node-version-file: .node-version - run: pnpm install --frozen-lockfile - - run: pnpm --dir tests typecheck - name: Start PostgreSQL, MySQL and MariaDB run: docker compose -f tests/compose.yaml up --detach --wait - run: pnpm test + - name: Check the sources generated from PostgreSQL + run: | + pnpm --filter @typesql/core --filter @typesql/pg run build + node tools/gen-pg-functions.ts --check + node tools/pg-catalog-from-pg.ts --check - name: Retain failing scenario applications and logs if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84c46963..289275ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,38 +40,31 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26 + node-version-file: .node-version - run: pnpm install --frozen-lockfile - name: Version pending change intents run: | - public() { - pnpm -r ls --depth -1 --json | node -e ' - for (const p of JSON.parse(require("fs").readFileSync(0))) - if (!p.private) console.log(`${p.name}@${p.version}`)' - } - before=$(public) - for pkg in $before; do + before=$(node tools/packages.ts npm) + while read -r pkg dir; do if ! pnpm view "$pkg" version >/dev/null 2>&1; then - name=${pkg%@*} - dir="packages/${name##*/}" if [ -f "$dir/CHANGELOG.md" ]; then echo "::notice::$pkg has an unpublished release; publishing without versioning" exit 0 fi echo "::notice::$pkg is a first release; versioning its pending intents" fi - done + done < <(node tools/packages.ts npm tag dir) pnpm change status pnpm version -r if [ -z "$(git status --porcelain)" ]; then exit 0 fi - extension=$(node -p 'require("./packages/vscode/package.json").version') + read -r extension extension_dir < <(node tools/packages.ts vscode version dir) extension_args=() - if [ "$extension" != "$(git show HEAD:packages/vscode/package.json | node -p 'JSON.parse(require("fs").readFileSync(0)).version')" ]; then + if [ "$extension" != "$(git show "HEAD:$extension_dir/package.json" | node -p 'JSON.parse(require("fs").readFileSync(0)).version')" ]; then extension_args=(--extension "$extension") fi - message=$(comm -13 <(sort <<<"$before") <(public | sort) | node tools/release-message.ts "${extension_args[@]}") + message=$(comm -13 <(sort <<<"$before") <(node tools/packages.ts npm | sort) | node tools/release-message.ts "${extension_args[@]}") git add -A release_commit=$(node tools/github-commit.ts main "$message") # Fetch main, not only the commit, so origin/main contains it and @@ -85,9 +78,7 @@ jobs: - run: pnpm build - name: Publish packages with trusted publishing run: | - mapfile -t packages < <(pnpm -r ls --depth -1 --json | node -e ' - for (const p of JSON.parse(require("fs").readFileSync(0))) - if (!p.private) console.log(p.name)') + mapfile -t packages < <(node tools/packages.ts npm name) filters=() new=() for pkg in "${packages[@]}"; do @@ -137,7 +128,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26 + node-version-file: .node-version - run: pnpm install --frozen-lockfile - run: pnpm docs:deploy env: @@ -172,7 +163,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: package-manager-cache: false - node-version: 26 + node-version-file: .node-version - run: pnpm install --frozen-lockfile - run: pnpm --filter "typesql..." run build - run: pnpm exec vsce package --no-dependencies --out typesql.vsix diff --git a/.gitignore b/.gitignore index 3e82d9d6..4ebe34ee 100644 --- a/.gitignore +++ b/.gitignore @@ -8,12 +8,8 @@ pnpm-publish-summary.json **/.vitepress/cache/ .wrangler/ .cloudflare/ - -!docs/ *.vsix reports/ - .consumer/ .scenario/ tests/.artifacts/ -tests/.vitest/ diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..60bb1e60 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +26.8.1 diff --git a/AGENTS.md b/AGENTS.md index 54346cde..52a7ec7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,22 +3,21 @@ ## Change intents When you change `src/`, `README.md`, `LICENSE` or `package.json` of a published package -(`@typesql/core`, `pg`, `postgres`, `sqlite`, `mysql`, `kysely`, `drizzle`, `prisma`, `cli`, `better-auth`), add an intent in the same commit: +(`@typesql/core`, `pg`, `postgres`, `sqlite`, `mysql`, `kysely`, `drizzle`, `prisma`, `cli`, `better-auth`, `language-server`), add an intent in the same commit: ```sh pnpm change @typesql/core --bump patch --summary "What users now see." pnpm check:intents ``` -- Name only the packages you changed. +- Name only the packages you changed, and `typesql` for a change to `packages/language-server/src`, which the extension bundles. - The VS Code extension (`packages/vscode`, package name `typesql`) is released the same way: - add `pnpm change typesql …` with every user-facing change to it. `pnpm check:intents` does not - catch a missing one. + add `pnpm change typesql …` with every user-facing change to it. - Write the summary like the existing `.changeset/*.md` files. ## Scenario tests and definition of done -`pnpm test` runs the complete Vitest scenario suite locally against a PostgreSQL instance. Use `pnpm test ` for a filtered iteration and run the complete suite before finishing a library or CLI change. CI runs every scenario without snapshot updates. See [tests/README.md](tests/README.md). +`pnpm test` runs the complete Vitest scenario suite locally against the PostgreSQL, MySQL and MariaDB servers from `tests/compose.yaml`. Use `pnpm test ` for a filtered iteration and run the complete suite before finishing a library or CLI change. CI runs every scenario without snapshot updates. See [tests/README.md](tests/README.md). For a bug fix: @@ -28,7 +27,7 @@ For a bug fix: 4. Fix the library and rerun the same scenario without weakening expectations. 5. Run the complete suite and retain the reproduction permanently. -A normal scenario contains `package.json`, `app.ts` and `expected.snap`. Use ordinary package scripts for setup, generation, compilation and execution. Add ordinary application files when needed. Applications contain no test assertions, suppressed expected compiler errors or type-equality helpers. External versions belong in the exact catalog in `pnpm-workspace.yaml` and the root `pnpm-lock.yaml`. Scenarios depend on TypeSQL packages through `workspace:*` and their public exports after building; never use repository source imports, TypeScript path aliases or registry TypeSQL packages. Temporary execution directories reuse installed workspace dependencies. +A normal scenario contains `package.json`, `app.ts` and `expected.snap`. Use ordinary package scripts for setup, generation and execution; the runner compiles `app.ts` with `tests/scenarios/tsconfig.base.json` unless a `compile` script says otherwise. Add ordinary application files when needed. Applications contain no test assertions, suppressed expected compiler errors or type-equality helpers. External versions belong in the exact catalog in `pnpm-workspace.yaml` and the root `pnpm-lock.yaml`. Scenarios depend on TypeSQL packages through `workspace:*` and their public exports after building; never use repository source imports, TypeScript path aliases or registry TypeSQL packages. Temporary execution directories reuse installed workspace dependencies. Snapshot changes require an explicit `--update` and review. Never accept current output merely because it is current. Include nullable and non-nullable cases when changing nullability rules. Do not add another runner, generated SQL corpus, baseline, unit suite or coverage gate. Failures retain their application and logs under `tests/.artifacts/`; dependencies remain linked to the workspace. Report the scenario, stage, command and expected-versus-observed difference. Infrastructure failures are separate from application rejections. diff --git a/biome.json b/biome.json index 5380dd60..593e8ff2 100644 --- a/biome.json +++ b/biome.json @@ -12,11 +12,10 @@ "!!.changeset", "!packages/core/src/postgres/engine/typer/pg-functions.ts", "!packages/core/src/postgres/schema/pg-catalog.ts", - "!packages/bench/src", + "!packages/bench/src/case1.ts", + "!tools/biome/src", "!pnpm-lock.yaml", - "!**/CHANGELOG.md", - "!tests/.artifacts", - "!tests/.vitest" + "!**/CHANGELOG.md" ] }, "formatter": { diff --git a/knip.json b/knip.json deleted file mode 100644 index dcfa1381..00000000 --- a/knip.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "$schema": "https://unpkg.com/knip@6/schema.json", - "ignoreBinaries": ["change", "vsce", "ovsx"], - "ignoreWorkspaces": ["tests/scenarios/*"], - "ignoreIssues": { - "pnpm-workspace.yaml": ["catalog"], - "packages/cli/package.json": ["optionalPeerDependencies"], - "packages/drizzle/src/pg-type.ts": ["duplicates"] - }, - "workspaces": { - ".": { - "entry": ["tools/*.ts"], - "project": ["tools/**/*.ts"], - "ignoreDependencies": ["pg"] - }, - "packages/core": { - "entry": [ - "src/index.ts!", - "src/adapter.ts!", - "src/postgres/index.ts!", - "src/postgres/engine/index.ts!", - "src/sqlite/index.ts!", - "src/sqlite/engine/index.ts!", - "src/mysql/index.ts!", - "src/mysql/engine/index.ts!", - "src/migration/index.ts!", - "src/plugins/index.ts!", - "examples/*.ts" - ], - "project": ["src/**/*.ts!", "examples/**/*.ts"] - }, - "packages/cli": { - "entry": ["src/index.ts!", "src/bin.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/drizzle": { - "entry": ["src/index.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/kysely": { - "entry": ["src/index.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/pg": { - "entry": ["src/index.ts!", "src/introspect/index.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/postgres": { - "entry": ["src/index.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/prisma": { - "entry": ["src/index.ts!", "src/bin.ts!", "src/generator/api.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/sqlite": { - "entry": ["src/index.ts!", "src/introspect/index.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/mysql": { - "entry": [ - "src/index.ts!", - "src/mariadb.ts!", - "src/planetscale.ts!", - "src/tidb.ts!", - "src/introspect/index.ts!" - ], - "project": ["src/**/*.ts!"] - }, - "packages/language-server": { - "entry": ["src/bin.ts!"], - "project": ["src/**/*.ts!"] - }, - "packages/vscode": { - "project": ["src/**/*.ts!"], - "ignoreDependencies": ["ovsx"] - }, - "packages/bench": { - "entry": ["src/case1.ts!"], - "project": ["*.ts", "src/**/*.ts!"] - }, - "packages/docs": { - "entry": ["*.paths.ts", "cloudflare.config.ts", "wrangler.config.ts"], - "project": ["*.ts", ".vitepress/**/*.{ts,mts,vue}"] - }, - "tests": { - "entry": ["scenario.test.ts", "support/observe.ts", "support/runtime.ts"], - "project": ["*.ts", "support/**/*.ts"] - } - } -} diff --git a/knip.ts b/knip.ts new file mode 100644 index 00000000..29a51159 --- /dev/null +++ b/knip.ts @@ -0,0 +1,68 @@ +import { readFileSync } from "node:fs"; +import type { KnipConfig } from "knip"; +import { releasedPackages } from "./tools/packages.ts"; + +interface Manifest { + exports?: Record; + bin?: Record; +} + +/** A published package's entry points are the sources of its exports and bins. */ +function entries(dir: string): string[] { + const manifest: Manifest = JSON.parse( + readFileSync(`${dir}/package.json`, "utf8"), + ); + return [ + ...Object.values(manifest.exports ?? {}).map((e) => e.default), + ...Object.values(manifest.bin ?? {}), + ].map( + (file) => `${file.replace(/^\.\/dist\//, "src/").replace(/\.js$/, ".ts")}!`, + ); +} + +const npm = releasedPackages().filter((p) => p.registry === "npm"); + +export default { + ignoreBinaries: ["change", "vsce", "ovsx"], + ignoreWorkspaces: ["tests/scenarios/*"], + ignoreIssues: { + "pnpm-workspace.yaml": ["catalog"], + "packages/cli/package.json": ["optionalPeerDependencies"], + "packages/drizzle/src/pg-type.ts": ["duplicates"], + }, + workspaces: { + ".": { + entry: ["tools/*.ts"], + project: ["tools/**/*.ts"], + }, + ...Object.fromEntries( + npm.map(({ dir }) => [ + dir, + { entry: entries(dir), project: ["src/**/*.ts!"] }, + ]), + ), + "packages/core": { + entry: [...entries("packages/core"), "examples/*.ts"], + project: ["src/**/*.ts!", "examples/**/*.ts"], + }, + "packages/vscode": { + project: ["src/**/*.{ts,mts}!", "scripts/*.mts"], + ignoreDependencies: ["ovsx"], + }, + "packages/bench": { + entry: ["src/case1.ts!"], + project: ["*.ts", "src/**/*.ts!"], + }, + "packages/docs": { + entry: ["*.paths.ts", "cloudflare.config.ts", "wrangler.config.ts"], + project: ["*.ts", ".vitepress/**/*.{ts,mts,vue}"], + }, + tests: { + entry: [ + "support/scenario.ts", + "support/observe.ts", + "support/runtime.ts", + ], + }, + }, +} satisfies KnipConfig; diff --git a/package.json b/package.json index 223d5f0e..f21c04cf 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "scripts": { "build": "pnpm -r --filter './packages/*' --filter '!@typesql/docs' run build", "typecheck": "pnpm -r --filter './packages/*' run typecheck && tsrs -p tsconfig.json", - "lint": "biome check", - "lint:fix": "biome check --write", + "lint": "node tools/biome/build.ts --check && biome check", + "lint:fix": "node tools/biome/build.ts && biome check --write", "knip": "knip", - "test": "node tests/run.ts", + "test": "pnpm --dir tests test", "bench": "pnpm --filter @typesql/bench run bench", "check:intents": "node tools/check-intents.ts", "release": "gh workflow run release.yml --ref main && echo \"Release started: https://github.com/ismailkarsli/typesql/actions/workflows/release.yml\"", @@ -24,7 +24,11 @@ "devDependencies": { "@biomejs/biome": "catalog:", "@types/node": "catalog:", + "@types/pg": "catalog:", + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", "knip": "catalog:", + "pg": "catalog:", "tsrs": "catalog:", "typescript": "catalog:" } diff --git a/packages/bench/.gitignore b/packages/bench/.gitignore new file mode 100644 index 00000000..914c17f7 --- /dev/null +++ b/packages/bench/.gitignore @@ -0,0 +1 @@ +.check-*/ diff --git a/packages/bench/run.ts b/packages/bench/run.ts index bae8f64d..710418b2 100644 --- a/packages/bench/run.ts +++ b/packages/bench/run.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node // Measures how fast the current code checks and builds the case1 application. import { spawnSync } from "node:child_process"; -import { readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { stripTypeScriptTypes } from "node:module"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -164,21 +164,22 @@ const statements = queries.length; // Check case1 against the built package, as an application would: it reads // every statement's row type, as executing the statement does. The reads are // appended to a copy of case1, not put in a second file, which the compiler -// would check on another checker that infers every statement again. -const checked = "src/case1.check.ts"; -for (const compiler of ["tsrs", "tsc"]) { - const config = join(here, "tsconfig.case1.json"); +// would check on another checker that infers every statement again. The copy +// lives in a scratch directory of this package, so it resolves the package's +// dependencies. +const scratch = mkdtempSync(join(here, ".check-")); +try { + const config = join(scratch, "tsconfig.json"); writeFileSync( config, JSON.stringify({ - extends: "./tsconfig.json", - compilerOptions: { paths: {} }, + extends: "../tsconfig.json", include: [], - files: [checked], + files: ["case1.check.ts"], }), ); writeFileSync( - join(here, checked), + join(scratch, "case1.check.ts"), [ source, 'import type { RowType } from "@typesql/core";', @@ -188,9 +189,9 @@ for (const compiler of ["tsrs", "tsc"]) { "", ].join("\n"), ); - try { + for (const compiler of ["tsrs", "tsc"]) { const proc = spawnSync( - join(here, "../../node_modules/.bin", compiler), + join(here, "node_modules", ".bin", compiler), ["--noEmit", "--extendedDiagnostics", "-p", config], { cwd: here, encoding: "utf8" }, ); @@ -205,10 +206,9 @@ for (const compiler of ["tsrs", "tsc"]) { console.log( `${compiler}: ${checkTimeSec} s check time, ${((checkTimeSec * 1000) / statements).toFixed(2)} ms and ${Math.round(num("Instantiations") / statements)} instantiations per statement`, ); - } finally { - unlinkSync(config); - unlinkSync(join(here, checked)); } +} finally { + rmSync(scratch, { recursive: true, force: true }); } const iterations = 3_000; diff --git a/packages/better-auth/src/index.ts b/packages/better-auth/src/index.ts index 031f6927..b1d1fc72 100644 --- a/packages/better-auth/src/index.ts +++ b/packages/better-auth/src/index.ts @@ -1,5 +1,4 @@ -import type { Query } from "@typesql/core"; -import type { Sql } from "@typesql/core/postgres"; +import type { AnyDialectBrand, DIALECT, Query } from "@typesql/core"; import type { BetterAuthOptions } from "better-auth"; import { type AdapterFactoryConfig, @@ -13,6 +12,7 @@ import { type ModelTarget, } from "better-auth/adapters"; import { + DIALECTS, type Dialect, quote, Statement, @@ -43,12 +43,9 @@ export interface TypesqlAdapterConfig { readonly debugLogs?: DBAdapterDebugLogOption | undefined; } -type Brand = { - readonly name: Dialect; - readonly inputs: (inputs: [never, never, []]) => [unknown, unknown, []]; -}; +type Brand = AnyDialectBrand; -type BrandKey = Extract, symbol>; +type BrandKey = typeof DIALECT; interface Runner { as(query: string, ...params: readonly unknown[]): Query; @@ -67,8 +64,9 @@ export type TypesqlAdapterSql = Runner & /** * A Better Auth database adapter that runs on a TypeSQL `sql` from - * `@typesql/pg`, `@typesql/postgres`, `@typesql/sqlite` or `@typesql/kysely`. - * It reads the database from `sql.dialect`. + * `@typesql/pg`, `@typesql/postgres`, `@typesql/sqlite`, `@typesql/kysely`, + * `@typesql/drizzle` or `@typesql/prisma`. It reads the database from + * `sql.dialect`. * * @example * ```ts @@ -82,7 +80,7 @@ export function typesqlAdapter>( config: TypesqlAdapterConfig = {}, ): (options: BetterAuthOptions) => DBAdapter { const { dialect } = sql; - if (dialect !== "postgres" && dialect !== "sqlite") + if (!Object.hasOwn(DIALECTS, dialect)) throw new TypeError( `typesql: typesqlAdapter() needs a TypeSQL sql for PostgreSQL or SQLite; this sql's dialect is ${String(dialect)}`, ); @@ -91,10 +89,10 @@ export function typesqlAdapter>( adapterName: "TypeSQL Adapter", usePlural: config.usePlural ?? false, debugLogs: config.debugLogs ?? false, - supportsUUIDs: dialect === "postgres", - supportsDates: dialect === "postgres", - supportsBooleans: dialect === "postgres", - supportsJSON: dialect === "postgres", + supportsUUIDs: DIALECTS[dialect].native, + supportsDates: DIALECTS[dialect].native, + supportsBooleans: DIALECTS[dialect].native, + supportsJSON: DIALECTS[dialect].native, supportsArrays: false, customTransformOutput: ({ data, field, fieldAttributes }) => { if (field === "id" || fieldAttributes.references) return data; @@ -183,17 +181,11 @@ function adapter( .map(([name, { modelKey, on, relation, limit }]) => { const many = relation !== "one-to-one"; const rows = `SELECT * FROM ${table(name)} AS "_j" WHERE "_j".${quote(on.to)} = ${table(model)}.${quote(on.from)} LIMIT ${s.param(many ? (limit ?? 100) : 1)}`; - const row = - dialect === "postgres" - ? `to_json("j")` - : `json_object(${columnsOf(modelKey ?? getDefaultModelName(name)) - .map((c) => `${s.param(c)}, "j".${quote(c)}`) - .join(", ")})`; - const value = !many - ? row - : dialect === "postgres" - ? `COALESCE(json_agg(${row}), '[]')` - : `json_group_array(${row})`; + const row = s.dialect.row( + () => columnsOf(modelKey ?? getDefaultModelName(name)), + s, + ); + const value = many ? s.dialect.rows(row) : row; return `, (SELECT ${value} FROM (${rows}) AS "j") AS ${quote(name)}`; }) .join(""); diff --git a/packages/better-auth/src/statement.ts b/packages/better-auth/src/statement.ts index 2751aa67..a53fba2f 100644 --- a/packages/better-auth/src/statement.ts +++ b/packages/better-auth/src/statement.ts @@ -1,13 +1,43 @@ import type { CleanedWhere } from "better-auth/adapters"; -export type Dialect = "postgres" | "sqlite"; +/** Where the databases the adapter runs on differ. */ +interface DialectFacts { + /** UUIDs, dates, booleans and JSON are the database's own types. */ + readonly native: boolean; + /** A row of the joined relation `j`, which has `columns`, as JSON. */ + row(columns: () => readonly string[], s: Statement): string; + /** The JSON rows `row` as a JSON array, `[]` without rows. */ + rows(row: string): string; + /** Whether LIKE ignores case: SQLite's does (for ASCII), and GLOB doesn't. */ + readonly likeIgnoresCase: boolean; +} + +export const DIALECTS = { + postgres: { + native: true, + row: () => 'to_json("j")', + rows: (row) => `COALESCE(json_agg(${row}), '[]')`, + likeIgnoresCase: false, + }, + sqlite: { + native: false, + row: (columns, s) => + `json_object(${columns() + .map((c) => `${s.param(c)}, "j".${quote(c)}`) + .join(", ")})`, + rows: (row) => `json_group_array(${row})`, + likeIgnoresCase: true, + }, +} as const satisfies Readonly>; + +export type Dialect = keyof typeof DIALECTS; export class Statement { - readonly dialect: Dialect; + readonly dialect: DialectFacts; readonly values: unknown[] = []; constructor(dialect: Dialect) { - this.dialect = dialect; + this.dialect = DIALECTS[dialect]; } param(value: V): string { @@ -103,9 +133,9 @@ function match( : operator === "ends_with" ? `${any}${escaped}` : `${any}${escaped}${any}`; - if (s.dialect === "sqlite" && !insensitive) + if (s.dialect.likeIgnoresCase && !insensitive) return `${column} GLOB ${s.param(wrap(value.replace(/[*?[]/g, "[$&]"), "*"))}`; - const like = s.dialect === "postgres" && insensitive ? "ILIKE" : "LIKE"; + const like = insensitive && !s.dialect.likeIgnoresCase ? "ILIKE" : "LIKE"; const pattern = wrap(value.replace(/[!%_]/g, "!$&"), "%"); return `${column} ${like} ${s.param(pattern)} ESCAPE '!'`; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f02dd1cd..a445c4c4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,14 +1,16 @@ import { createRequire } from "node:module"; import { parseArgs } from "node:util"; +import { type CommandIo, commandIo, errorMessage } from "@typesql/core/adapter"; +import type { MigratorConfig } from "@typesql/core/migration"; import { main as mysql } from "@typesql/mysql/introspect"; import { main as postgres } from "@typesql/pg/introspect"; import { main as sqlite } from "@typesql/sqlite/introspect"; -export type Dialect = "postgres" | "sqlite" | "mysql" | "mariadb"; +export type Dialect = MigratorConfig["dialect"]; // MariaDB is introspected like MySQL; the server reports which one it is. const DIALECTS: Readonly< - Record Promise> + Record Promise> > = { postgres, sqlite, mysql, mariadb: mysql }; const USAGE = `Usage: typesql migrate [options] @@ -26,16 +28,6 @@ Run \`typesql migrate --help\` for migration options. Run \`typesql introspect --dialect --help\` for the options of each dialect. `; -interface Io { - readonly stdout: (s: string) => void; - readonly stderr: (s: string) => void; -} - -const defaultIo = (): Io => ({ - stdout: (s) => process.stdout.write(s), - stderr: (s) => process.stderr.write(s), -}); - const isDialect = (s: string): s is Dialect => Object.hasOwn(DIALECTS, s); const parse = (argv: readonly string[]) => @@ -56,8 +48,9 @@ const parse = (argv: readonly string[]) => export async function main( argv: readonly string[], - io: Io = defaultIo(), + given?: CommandIo, ): Promise { + const io = commandIo(given); if (argv[0] === "migrate") { const { main: migrate } = await import("./migrate.js"); return migrate(argv.slice(1), io); @@ -66,9 +59,7 @@ export async function main( try { parsed = parse(argv); } catch (error) { - io.stderr( - `${error instanceof Error ? error.message : String(error)}\n\n${USAGE}`, - ); + io.stderr(`${errorMessage(error)}\n\n${USAGE}`); return 2; } const { values, tokens, positionals } = parsed; @@ -102,10 +93,12 @@ export async function main( io.stdout(USAGE); return 0; } + const url = + typeof values.url === "string" ? values.url : io.env.DATABASE_URL; const scheme = - typeof values.url === "string" - ? /^(mysql|mariadb):/i.exec(values.url)?.[1]?.toLowerCase() - : undefined; + url === undefined + ? undefined + : /^(mysql|mariadb):/i.exec(url)?.[1]?.toLowerCase(); dialect = values.db === undefined ? (scheme ?? "postgres") : "sqlite"; } if (typeof dialect !== "string" || !isDialect(dialect)) { @@ -114,5 +107,5 @@ export async function main( ); return 2; } - return DIALECTS[dialect](rest); + return DIALECTS[dialect](rest, io); } diff --git a/packages/cli/src/migrate.ts b/packages/cli/src/migrate.ts index e59eb9f1..c8fbb4bf 100644 --- a/packages/cli/src/migrate.ts +++ b/packages/cli/src/migrate.ts @@ -1,25 +1,14 @@ -import { stat } from "node:fs/promises"; -import { registerHooks } from "node:module"; -import { dirname, isAbsolute, join, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; +import { dirname } from "node:path"; import { parseArgs } from "node:util"; +import { type CommandIo, commandIo, errorMessage } from "@typesql/core/adapter"; import { - createMigrator, type DownResult, - isMigrator, + loadMigrator, type MigrationError, - type Migrator, - type MigratorConfig, + missingCore, type UpResult, } from "@typesql/core/migration"; -interface Io { - readonly stdout: (text: string) => void; - readonly stderr: (text: string) => void; - readonly cwd?: string; - readonly env?: Readonly>; -} - const COMMANDS = ["latest", "up", "down", "status", "make", "generate"]; const USAGE = `Usage: typesql migrate [options] @@ -69,137 +58,24 @@ const parse = (argv: readonly string[]) => }, }); -let mapped = false; - -/** - * A TypeScript project's files import each other as `./name.js`, which - * TypeScript resolves to `./name.ts`; Node doesn't. While the config loads, - * such an import from a `.ts` file outside node_modules loads the `.ts` file - * when there is no `.js` one, so an array config can import its migrations. - */ -function mapTypeScriptImports(): void { - if (mapped) return; - mapped = true; - registerHooks({ - resolve(specifier, context, nextResolve) { - try { - return nextResolve(specifier, context); - } catch (error) { - const parent = context.parentURL; - if ( - !hasCode(error, "ERR_MODULE_NOT_FOUND") || - !/^\.\.?\/.*\.[cm]?js$/.test(specifier) || - parent === undefined || - !/^file:.*\.[cm]?ts$/.test(parent) || - parent.includes("/node_modules/") - ) - throw error; - return nextResolve(specifier.replace(/js$/, "ts"), context); - } - }, - }); -} - -/** The config file's migrator, with its relative paths resolved beside the file. */ -async function configured( - file: string, - explicit: boolean, -): Promise { - try { - await stat(file); - } catch (error) { - if (!explicit && hasCode(error, "ENOENT")) return undefined; - throw error; - } - mapTypeScriptImports(); - const module: { readonly default?: unknown } = await import( - pathToFileURL(file).href - ); - const migrator = module.default; - if (!isMigrator(migrator)) - throw new Error( - `${file} must default-export a migrator: export default createMigrator({ … }) from @typesql/core/migration`, - ); - const base = dirname(file); - const { config } = migrator; - const paths: Record = {}; - for (const key of ["migrationsDir", "schemaFile", "db"] as const) { - const value = config[key]; - if (value !== undefined && value !== ":memory:" && !isAbsolute(value)) - paths[key] = resolve(base, value); - } - if (config.schemaFile === undefined && config.migrationsDir !== undefined) - paths.schemaFile = join(base, "db.ts"); - return Object.keys(paths).length > 0 ? migrator.with(paths) : migrator; -} - -/** Whether `node_modules/` exists in `directory` or one of its parents. */ -async function installed(directory: string, name: string): Promise { - for (let dir = directory; ; dir = dirname(dir)) { - try { - await stat(join(dir, "node_modules", name, "package.json")); - return true; - } catch {} - if (dirname(dir) === dir) return false; - } -} - -/** Whether `error` carries the `code` Node sets. */ -function hasCode( - error: unknown, - code: string, -): error is { readonly code: string } { - return ( - typeof error === "object" && - error !== null && - "code" in error && - error.code === code - ); -} - -const DIALECTS: readonly string[] = ["postgres", "sqlite", "mysql", "mariadb"]; - -const isDialect = (value: unknown): value is MigratorConfig["dialect"] => - typeof value === "string" && DIALECTS.includes(value); - /** Whether `error` is a MigrationError from any copy of `@typesql/core`. */ function isMigrationError(error: unknown): error is MigrationError { return error instanceof Error && error.name === "MigrationError"; } -// pg rejects a refused `localhost` connection (IPv4 and IPv6) with an -// AggregateError whose own message is empty. -const message = (cause: unknown): string => { - if (!(cause instanceof Error)) return String(cause); - const text = cause.message - ? cause.message - : cause instanceof AggregateError && cause.errors.length > 0 - ? Array.from(new Set(cause.errors.map(message))).join("; ") - : "code" in cause && typeof cause.code === "string" - ? cause.code - : cause.name; - return text.replace(/^typesql: /, ""); -}; - /** 0: done, 1: the command failed, 2: a usage error. */ type ExitCode = 0 | 1 | 2; -/** The config the flags replace. */ -interface Overrides { - dialect?: MigratorConfig["dialect"]; - url?: string; - db?: string; - migrationsDir?: string; - schemaFile?: string; - generate?: boolean; -} - -export async function main(argv: readonly string[], io: Io): Promise { +export async function main( + argv: readonly string[], + given?: CommandIo, +): Promise { + const io = commandIo(given); let parsed: ReturnType; try { parsed = parse(argv); } catch (error) { - io.stderr(`typesql migrate: ${message(error)}\n\n${USAGE}`); + io.stderr(`typesql migrate: ${errorMessage(error)}\n\n${USAGE}`); return 2; } const { values, positionals } = parsed; @@ -220,61 +96,27 @@ export async function main(argv: readonly string[], io: Io): Promise { } const print = (text: string) => io.stdout(`typesql migrate: ${text}\n`); try { - const cwd = io.cwd ?? process.cwd(); - const env = io.env ?? process.env; - const from = (path: string) => resolve(cwd, path); - let migrator = await configured( - from(values.config ?? "typesql.config.ts"), - values.config !== undefined, - ); - const dialect = values.dialect ?? migrator?.config.dialect; - if (!isDialect(dialect)) - throw new Error( - migrator === undefined - ? "no typesql.config.ts: pass --dialect postgres, sqlite, mysql or mariadb, or pass --config" - : `unsupported dialect: ${String(dialect)} (expected postgres, sqlite, mysql or mariadb)`, - ); - const overrides: Overrides = {}; - if (values.dialect !== undefined) overrides.dialect = dialect; - if (values.url !== undefined) overrides.url = values.url; - if (values.db !== undefined) - overrides.db = values.db === ":memory:" ? values.db : from(values.db); - if (values.dir !== undefined) overrides.migrationsDir = from(values.dir); - if (values.out !== undefined) overrides.schemaFile = from(values.out); - if (values.generate !== undefined) overrides.generate = values.generate; - const connected = (config: Partial | undefined) => - config?.url !== undefined || - config?.db !== undefined || - config?.execute !== undefined; - // The URL dialects fall back to DATABASE_URL, as `typesql introspect` does. - if ( - dialect !== "sqlite" && - !connected(overrides) && - !(migrator !== undefined && connected(migrator.config)) && - env.DATABASE_URL - ) - overrides.url = env.DATABASE_URL; - migrator = - migrator === undefined - ? createMigrator({ - migrationsDir: from("migrations"), - schemaFile: from("db.ts"), - ...overrides, - dialect, - }) - : migrator.with(overrides); + const migrator = await loadMigrator({ + cwd: io.cwd, + env: io.env, + config: values.config, + overrides: { + dialect: values.dialect, + url: values.url, + db: values.db, + migrationsDir: values.dir, + schemaFile: values.out, + generate: values.generate, + }, + }); switch (command) { case "make": { const created = await migrator.make(positionals[1] ?? ""); print(`created ${created}`); - // The new file imports migration() from @typesql/core/migration. - // A package manager with strict layouts (pnpm) doesn't expose the - // CLI's own @typesql/core to the project. - if (!(await installed(dirname(created), "@typesql/core"))) - io.stderr( - `typesql migrate: warning: @typesql/core/migration cannot be found from ${dirname(created)}, so the migration can't run and its types are unchecked; add @typesql/core to the project's dependencies (npm install @typesql/core, or pnpm add @typesql/core)\n`, - ); + const missing = await missingCore(dirname(created)); + if (missing !== undefined) + io.stderr(`typesql migrate: warning: ${missing}\n`); return 0; } case "generate": @@ -302,7 +144,7 @@ export async function main(argv: readonly string[], io: Io): Promise { // written is a warning, and the exit code reports the migration. if (write.schemaError !== undefined) io.stderr( - `typesql migrate: warning: the schema file was not updated: ${message(write.schemaError)}\n` + + `typesql migrate: warning: the schema file was not updated: ${errorMessage(write.schemaError)}\n` + "typesql migrate: fix the cause, then run `typesql migrate generate`\n", ); }; @@ -325,7 +167,7 @@ export async function main(argv: readonly string[], io: Io): Promise { report(names, result); return 0; } catch (error) { - io.stderr(`typesql migrate: ${message(error)}\n`); + io.stderr(`typesql migrate: ${errorMessage(error)}\n`); return 1; } } diff --git a/packages/core/package.json b/packages/core/package.json index 677836f8..6345ee26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -52,6 +52,10 @@ "./plugins": { "types": "./dist/plugins/index.d.ts", "default": "./dist/plugins/index.js" + }, + "./reserved": { + "types": "./dist/reserved.d.ts", + "default": "./dist/reserved.js" } }, "files": [ @@ -68,11 +72,15 @@ "typecheck": "tsrs -p tsconfig.json --noEmit" }, "peerDependencies": { + "mariadb": ">=3", "mysql2": ">=3", "pg": ">=8", "typescript": ">=7.1.0-0" }, "peerDependenciesMeta": { + "mariadb": { + "optional": true + }, "mysql2": { "optional": true }, diff --git a/packages/core/src/adapter.ts b/packages/core/src/adapter.ts index 3da07d3f..535a9fae 100644 --- a/packages/core/src/adapter.ts +++ b/packages/core/src/adapter.ts @@ -1,118 +1,79 @@ -import { runTransaction } from "./transaction.js"; +// What adapters build on: the drivers' side of `sql` (executors, +// transactions, values, results and connections), the schema source that +// introspection and generators write, and the `typesql` commands' shell. -export { mapRows, pluginsOf } from "./query.js"; -export { JsonParam } from "./runtime/json.js"; -export { mysqlPositional, mysqlValue } from "./runtime/mysql-params.js"; export { + type ColumnFlags, + colFlags, + colSource, + columnSource, + doc, + globRegExp, + helperImport, + propertyKey, + RESERVED_KEYS, + type SchemaDialect, + type SchemaModule, + schemaMembers, + schemaWriter, +} from "./codegen.js"; +export { + type CommandEnv, + type CommandIo, + commandIo, + databaseUrl, + errorMessage, + type IntrospectOutput, + introspectArgs, + introspectCommand, + listOption, + UsageError, +} from "./command.js"; +export type { Connection } from "./connect.js"; +export { + type DialectFamily, + dialectFamily, + familyStatement, +} from "./family.js"; +export { + connectMysql, + type MySqlClient, + type MySqlResult, + type MySqlValue, + mysqlBind, + mysqlInline, + mysqlPositional, + mysqlResult, + mysqlSplit, + mysqlValue, +} from "./mysql/driver.js"; +export { + connectPostgres, + type PgClient, type PgField, type PgLookup, parsePgArray, parsePgUserArrays, -} from "./runtime/pg-arrays.js"; + pgResult, +} from "./postgres/driver.js"; +export { mapRows, pluginsOf } from "./query.js"; +export type { PositionalStatement, SplitStatement } from "./runtime/compile.js"; +export { JsonParam, jsonText } from "./runtime/json.js"; export { + connectSqlite, paramNumbers, positional, - type SplitStatement, + type SqliteClient, + type SqliteInput, splitParams, + sqliteBindings, sqliteValue, -} from "./runtime/sqlite-params.js"; +} from "./sqlite/driver.js"; export { + createFamilySql, createTransactionalSql, + pooledTransaction, runTransaction, type SqlDriver, + serialTransaction, } from "./transaction.js"; - -const queues = new WeakMap>(); - -export function serialTransaction( - client: WeakKey, - depth: number, - run: (statement: string) => Promise, - fn: () => Promise, -): Promise { - if (depth > 0) - return runTransaction(run, fn, { savepoint: `typesql_${depth}` }); - const current = (queues.get(client) ?? Promise.resolve()).then(() => - runTransaction(run, fn), - ); - queues.set( - client, - current.catch(() => undefined), - ); - return current; -} - -const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/; - -export const propertyKey = (name: string): string => - IDENT.test(name) ? name : JSON.stringify(name); - -export const globRegExp = (glob: string): RegExp => - new RegExp( - `^${glob - .split("*") - .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")) - .join(".*")}$`, - ); - -export function colSource( - type: string, - nullable: boolean, - hasDefault: boolean, -): string { - const flags = [ - ...(nullable ? ["nullable: true"] : []), - ...(hasDefault ? ["hasDefault: true"] : []), - ]; - return flags.length > 0 - ? `Col<${type}, { ${flags.join("; ")} }>` - : `Col<${type}>`; -} - -export function schemaMembers( - relations: readonly R[], - schemaOf: (relation: R) => string, - member: (relation: R, indent: string) => string, -): string[] { - const lines: string[] = []; - const namespaces = new Map(); - for (const relation of relations) { - const schema = schemaOf(relation); - if (schema === "public") lines.push(member(relation, " ")); - else namespaces.set(schema, [...(namespaces.get(schema) ?? []), relation]); - } - for (const [schema, members] of namespaces) { - const body = members.map((relation) => member(relation, " ")); - lines.push(` ${propertyKey(schema)}: {\n${body.join("\n")}\n };`); - } - return lines; -} - -export interface CommandIo { - readonly stdout: (s: string) => void; - readonly stderr: (s: string) => void; -} - -export function introspectArgs( - io: CommandIo, - usage: string, - parse: () => { readonly values: V; readonly positionals: string[] }, -): V | number { - let parsed: ReturnType; - try { - parsed = parse(); - } catch (err) { - io.stderr(`${err instanceof Error ? err.message : err}\n\n${usage}`); - return 2; - } - const { values, positionals } = parsed; - if (values.help || positionals.length === 0) { - (values.help ? io.stdout : io.stderr)(usage); - return values.help ? 0 : 2; - } - if (positionals[0] !== "introspect" || positionals.length > 1) { - io.stderr(`unknown command: ${positionals.join(" ")}\n\n${usage}`); - return 2; - } - return values; -} diff --git a/packages/core/src/codegen.ts b/packages/core/src/codegen.ts new file mode 100644 index 00000000..654c496b --- /dev/null +++ b/packages/core/src/codegen.ts @@ -0,0 +1,218 @@ +// Schema source: the TypeScript that introspection, `typesql migrate +// generate` and the Prisma generator write for a schema. + +import { UsageError } from "./command.js"; +import { DECLARED_KEYS } from "./schema.js"; + +const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + +/** `name` as a property key: bare when it is an identifier, else quoted. */ +export const propertyKey = (name: string): string => + IDENT.test(name) ? name : JSON.stringify(name); + +/** Keys of a schema object that hold TypeSQL's own entries, not relations. */ +export const RESERVED_KEYS: readonly string[] = DECLARED_KEYS; + +/** An anchored name-matching expression where `*` matches any text. */ +export const globRegExp = (glob: string): RegExp => + new RegExp( + `^${glob + .split("*") + .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")) + .join(".*")}$`, + ); + +/** `lines` as a JSDoc comment at `indent`, or "" without lines. */ +export function doc(lines: readonly string[], indent: string): string { + const text = lines + .flatMap((l) => l.split("\n")) + .map((l) => l.replace(/\*\//g, "*\\/").trimEnd()); + if (text.length === 0) return ""; + if (text.length === 1) return `${indent}/** ${text[0]} */\n`; + return `${indent}/**\n${text.map((l) => `${indent} *${l ? ` ${l}` : ""}`).join("\n")}\n${indent} */\n`; +} + +/** The options of a column: what `Col` takes, in the order it is written. */ +export interface ColumnFlags { + readonly nullable?: boolean | undefined; + readonly hasDefault?: boolean | undefined; + readonly position?: number | undefined; + readonly rowid?: boolean | undefined; + readonly autoIncrement?: boolean | undefined; + readonly generated?: boolean | undefined; + readonly hidden?: boolean | undefined; +} + +/** The dialects whose `Col` a schema module imports. */ +export type SchemaDialect = "postgres" | "sqlite" | "mysql"; + +/** + * The flags that make a dialect's `Col` have a default, so that + * `hasDefault: true` beside them is left out. They follow `Col` in + * `@typesql/core/` (src//schema/index.ts). + */ +const IMPLY_DEFAULT: Readonly< + Record +> = { + postgres: [], + sqlite: ["generated"], + mysql: ["autoIncrement", "generated"], +}; + +/** The source of a column's options, `{ nullable: true; … }`, or "" without any. */ +export function colFlags(flags: ColumnFlags, dialect: SchemaDialect): string { + const implied = IMPLY_DEFAULT[dialect].some((flag) => flags[flag] === true); + const written = ORDER.flatMap((name) => { + const value = + name === "hasDefault" ? flags.hasDefault && !implied : flags[name]; + return value === true || typeof value === "number" + ? [`${name}: ${value}`] + : []; + }); + return written.length > 0 ? `{ ${written.join("; ")} }` : ""; +} + +const ORDER = [ + "nullable", + "hasDefault", + "position", + "rowid", + "autoIncrement", + "generated", + "hidden", +] as const satisfies readonly (keyof ColumnFlags)[]; + +/** `Col`, or another column helper of the same shape. */ +export function columnSource( + type: string, + flags: ColumnFlags, + dialect: SchemaDialect, + helper = "Col", +): string { + const options = colFlags(flags, dialect); + return options ? `${helper}<${type}, ${options}>` : `${helper}<${type}>`; +} + +/** + * A PostgreSQL `Col` with its `nullable` and `hasDefault` flags. + * @deprecated Use `columnSource(type, { nullable, hasDefault }, "postgres")`. + */ +export const colSource = ( + type: string, + nullable: boolean, + hasDefault: boolean, +): string => columnSource(type, { nullable, hasDefault }, "postgres"); + +const HELPERS_FROM: Readonly> = { + postgres: "@typesql/core/postgres", + sqlite: "@typesql/core/sqlite", + mysql: "@typesql/core/mysql", +}; + +/** A schema module, around its interface's members. */ +export interface SchemaModule { + /** Its first line's comment: what wrote it. */ + readonly header: string; + /** The interface's lines. */ + readonly members: readonly string[]; + /** The interface's name, an identifier; default `DB`. */ + readonly typeName?: string | undefined; + /** The module the column helpers come from; default the dialect's. */ + readonly importFrom?: string | undefined; + /** More import lines. */ + readonly imports?: readonly string[] | undefined; +} + +/** + * What writes one schema module of `dialect`: its columns, with the + * dialect's helpers, and the module, which imports the helpers they used. + */ +export function schemaWriter(dialect: SchemaDialect) { + const used = new Set(); + const helper = (name: string, args: string, flags: ColumnFlags): string => { + used.add(name); + return columnSource(args, flags, dialect, name); + }; + return { + /** A column of database type `type`: `Col<"type", { … }>`. */ + col: (type: string, flags: ColumnFlags = {}): string => + helper("Col", JSON.stringify(type), flags), + /** A column whose values are one of `values`, a union's source. */ + enum: (values: string, flags: ColumnFlags = {}): string => + helper("Enum", values, flags), + /** + * A column of database type `base` whose values are typed `ts`: + * PostgreSQL's `Json` (a json or jsonb column), the other + * dialects' `Col, …>`. + */ + typed: (base: string, ts: string, flags: ColumnFlags = {}): string => { + if (dialect !== "postgres") { + used.add("Typed"); + return helper("Col", `Typed<${JSON.stringify(base)}, ${ts}>`, flags); + } + if (base !== "json") return helper("Json", ts, flags); + used.add("Json"); + return `Json<${ts}, ${colFlags(flags, dialect) || "{}"}, "json">`; + }, + /** A view of the columns `body`. */ + view: (body: string): string => { + used.add("View"); + return `View<${body}>`; + }, + /** The module, after every column is written. */ + module: (module: SchemaModule): string => { + const typeName = module.typeName ?? "DB"; + if (!IDENT.test(typeName)) + throw new UsageError( + `the schema's type name ${JSON.stringify(typeName)} is not a TypeScript identifier`, + ); + const { members } = module; + return [ + `// ${module.header}`, + ...helperImport(used, module.importFrom ?? HELPERS_FROM[dialect]), + ...(module.imports ?? []), + "", + ...(members.length > 0 + ? [`export interface ${typeName} {`, ...members, "}"] + : [`export interface ${typeName} {}`]), + "", + ].join("\n"); + }, + }; +} + +/** + * The members of a schema object: the `public` relations, then an object + * per other schema with its relations. + */ +export function schemaMembers( + relations: readonly R[], + schemaOf: (relation: R) => string, + member: (relation: R, indent: string) => string, +): string[] { + const lines: string[] = []; + const namespaces = new Map(); + for (const relation of relations) { + const schema = schemaOf(relation); + if (schema === "public") lines.push(member(relation, " ")); + else namespaces.set(schema, [...(namespaces.get(schema) ?? []), relation]); + } + for (const [schema, members] of namespaces) { + const body = members.map((relation) => member(relation, " ")); + lines.push(` ${propertyKey(schema)}: {\n${body.join("\n")}\n };`); + } + return lines; +} + +/** The import of the column helpers a schema module uses, in a fixed order. */ +export function helperImport( + used: ReadonlySet, + from: string, +): string[] { + const helpers = ["Col", "Enum", "Json", "Typed", "View"].filter((h) => + used.has(h), + ); + return helpers.length > 0 + ? [`import type { ${helpers.join(", ")} } from ${JSON.stringify(from)};`] + : []; +} diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts new file mode 100644 index 00000000..f7f1809c --- /dev/null +++ b/packages/core/src/command.ts @@ -0,0 +1,146 @@ +// What the `typesql` commands share: their output, their error messages, and +// the shell of `typesql introspect`, whose dialects live in the adapters. +// Node's modules are reached through process.getBuiltinModule() when a +// command runs, so importing this module needs none. + +/** Where a command writes, and the process it runs in. */ +export interface CommandIo { + readonly stdout: (s: string) => void; + readonly stderr: (s: string) => void; + readonly cwd?: string | undefined; + readonly env?: Readonly> | undefined; +} + +/** A command's io with the process's for what it leaves out. */ +export interface CommandEnv { + readonly stdout: (s: string) => void; + readonly stderr: (s: string) => void; + readonly cwd: string; + readonly env: Readonly>; +} + +/** `io` with the process's output, directory and environment for what it leaves out. */ +export const commandIo = (io?: CommandIo): CommandEnv => ({ + stdout: io?.stdout ?? ((s) => process.stdout.write(s)), + stderr: io?.stderr ?? ((s) => process.stderr.write(s)), + cwd: io?.cwd ?? process.cwd(), + env: io?.env ?? process.env, +}); + +/** + * The message of an error, without its `typesql: ` prefix. pg rejects a + * refused `localhost` connection (IPv4 and IPv6) with an AggregateError + * whose own message is empty: its errors' messages stand for it. + */ +export function errorMessage(cause: unknown): string { + if (!(cause instanceof Error)) return String(cause); + const text = cause.message + ? cause.message + : cause instanceof AggregateError && cause.errors.length > 0 + ? Array.from(new Set(cause.errors.map(errorMessage))).join("; ") + : "code" in cause && typeof cause.code === "string" + ? cause.code + : cause.name; + return text.replace(/^typesql: /, ""); +} + +/** Whether `error` carries the `code` Node and the database drivers set. */ +export function hasCode( + error: unknown, + code: string, +): error is { readonly code: string } { + return ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === code + ); +} + +/** A mistake in a command's arguments: exit code 2. */ +export class UsageError extends Error {} + +/** A repeated, comma-separated option's values, trimmed. */ +export const listOption = (values: readonly string[] | undefined): string[] => + (values ?? []).flatMap((value) => + value + .split(",") + .map((item) => item.trim()) + .filter(Boolean), + ); + +/** The connection URL: `--url`, else `DATABASE_URL`. */ +export function databaseUrl(url: string | undefined, io: CommandEnv): string { + const found = url ?? io.env.DATABASE_URL; + if (!found) throw new UsageError("pass --url or set DATABASE_URL"); + return found; +} + +export function introspectArgs( + io: CommandIo, + usage: string, + parse: () => { readonly values: V; readonly positionals: string[] }, +): V | number { + let parsed: ReturnType; + try { + parsed = parse(); + } catch (err) { + io.stderr(`${errorMessage(err)}\n\n${usage}`); + return 2; + } + const { values, positionals } = parsed; + if (values.help || positionals.length === 0) { + (values.help ? io.stdout : io.stderr)(usage); + return values.help ? 0 : 2; + } + if (positionals[0] !== "introspect" || positionals.length > 1) { + io.stderr(`unknown command: ${positionals.join(" ")}\n\n${usage}`); + return 2; + } + return values; +} + +/** What a dialect's `introspect` writes: the schema's source, and where. */ +export interface IntrospectOutput { + readonly source: string; + /** The file to write; stdout without one. */ + readonly out?: string | undefined; + /** The directory `out` is relative to; default: the working directory. */ + readonly base?: string | undefined; +} + +/** + * `typesql introspect` for one dialect: `parse` reads the arguments, and + * `run` reads the database and writes the schema's source. + */ +export function introspectCommand( + usage: string, + parse: (argv: string[]) => { + readonly values: V; + readonly positionals: string[]; + }, + run: (values: V, io: CommandEnv) => Promise, +): (argv: readonly string[], io?: CommandIo) => Promise { + return async (argv, given) => { + const io = commandIo(given); + const values = introspectArgs(io, usage, () => parse([...argv])); + if (typeof values === "number") return values; + try { + const { source, out, base } = await run(values, io); + if (out === undefined) io.stdout(source); + else { + const { mkdir, writeFile } = + process.getBuiltinModule("node:fs/promises"); + const { dirname, resolve } = process.getBuiltinModule("node:path"); + const file = resolve(base ?? io.cwd, out); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, source); + io.stderr(`typesql introspect: wrote ${out}\n`); + } + return 0; + } catch (err) { + io.stderr(`typesql introspect: ${errorMessage(err)}\n`); + return err instanceof UsageError ? 2 : 1; + } + }; +} diff --git a/packages/core/src/connect.ts b/packages/core/src/connect.ts new file mode 100644 index 00000000..1e548296 --- /dev/null +++ b/packages/core/src/connect.ts @@ -0,0 +1,43 @@ +// What the connections of the migrator and `typesql introspect` share; each +// dialect's driver module opens its own. Drivers load at runtime from +// node_modules, and Node's built-ins through process.getBuiltinModule(), +// which bundlers don't follow as they follow an import: a Worker bundle has +// nothing of them to resolve. + +import type { Executor } from "./query.js"; + +/** A connection: `client` is the driver's own, for code that reads it directly. */ +export interface Connection { + readonly client: Client; + readonly execute: Executor; + close(): Promise; +} + +/** + * A package, imported at runtime from node_modules. Bundlers follow a + * literal import() and would bundle it, but a Worker bundle can't resolve + * `pg`'s Node built-ins. `is` checks the module is the one expected. + */ +export async function runtimeImport( + specifier: string, + is: (module: unknown) => module is Module, +): Promise { + const module: unknown = await import( + /* webpackIgnore: true */ /* @vite-ignore */ specifier + ); + if (!is(module)) + throw new Error(`typesql: ${specifier} is not the expected package`); + return module; +} + +/** What a session's 'error' event ends: the error the next statement rejects with. */ +export interface Lost { + error: Error | undefined; +} + +/** Ending a session the server already ended fails; that is not an error. */ +export async function closing(end: Promise, lost: Lost): Promise { + await end.catch((cause: unknown) => { + if (!lost.error) throw cause; + }); +} diff --git a/packages/core/src/create-sql.ts b/packages/core/src/create-sql.ts index 4c4e7ba6..1496533e 100644 --- a/packages/core/src/create-sql.ts +++ b/packages/core/src/create-sql.ts @@ -1,5 +1,5 @@ import type { Dialect, DialectTypes, SqlOf } from "./dialect.js"; -import { type Empty, empty, optional } from "./empty.js"; +import { type Empty, empty } from "./empty.js"; import { encodeInput, type InputContext, type SqlPlugin } from "./plugin.js"; import { type Executor, SqlQuery } from "./query.js"; import { @@ -9,6 +9,7 @@ import { fail, ident, join, + LEXIS, param, type Spliceable, SqlFragment, @@ -24,22 +25,12 @@ export interface CreateSql { } /** - * The `sql` of `dialect`. The dialect's `sql` type is computed from its type - * parameters; the runtime API cannot be checked against it. + * The `sql` of `dialect`. Its type is computed from the dialect's type + * parameters, and dialects merge their own overloads into `CreateSql`: the + * runtime API cannot be checked against them. */ // biome-ignore lint/nursery/noUnsafeTypeAssertion: see above. -export const createDialectSql = createSqlApi as < - T extends DialectTypes, - DB, - TM = T["typeMap"], ->( - dialect: Dialect, - executor?: Executor, - plugins?: readonly SqlPlugin[], -) => SqlOf; - -// biome-ignore lint/nursery/noUnsafeTypeAssertion: dialects merge their own overloads into `CreateSql`. -export const createSql = createDialectSql as CreateSql; +export const createSql = createSqlApi as CreateSql; const fragmentOf = (compiled: Compiled) => new SqlFragment(compiled.text, compiled.values); @@ -67,28 +58,6 @@ const columns = (value: V): [string, unknown][] => { // parentheses. const GROUPED = new WeakSet(); -const bool = (given: readonly (Spliceable | Empty)[], op: "AND" | "OR") => { - const items = present(given); - const r = join( - items.map((f) => - GROUPED.has(f) ? f : new SqlFragment(`(${f.text})`, f.values), - ), - ` ${op} `, - ); - const fragment = new SqlFragment( - items[1] - ? `(${r.text})` - : items[0] - ? r.text - : op === "AND" - ? "true" - : "false", - r.values, - ); - GROUPED.add(fragment); - return fragment; -}; - const present = (items: readonly (Spliceable | Empty)[]): Spliceable[] => items.filter((item): item is Spliceable => item !== empty); @@ -98,7 +67,34 @@ export function createSqlApi( executor?: Executor, plugins: readonly SqlPlugin[] = [], ) { - const quote = dialect.quote ?? '"'; + const { + quote = `"`, + emptyRow = "DEFAULT VALUES", + valuesDefault, + lexis = LEXIS, + } = dialect; + const bool = (given: readonly (Spliceable | Empty)[], op: "AND" | "OR") => { + const items = present(given); + const r = join( + items.map((f) => + GROUPED.has(f) ? f : new SqlFragment(`(${f.text})`, f.values), + ), + ` ${op} `, + lexis, + ); + const fragment = new SqlFragment( + items[1] + ? `(${r.text})` + : items[0] + ? r.text + : op === "AND" + ? "true" + : "false", + r.values, + ); + GROUPED.add(fragment); + return fragment; + }; const encode = (value: V, context: InputContext) => plugins[0] ? encodeInput(value, plugins, context) : value; const encodeArgs = (args: readonly unknown[], query: string) => { @@ -109,14 +105,16 @@ export function createSqlApi( : fail("a plugin must encode parameters as an array"); }; const call = (query: string, ...rest: readonly unknown[]) => { - const { text, values } = compile(query, encodeArgs(rest, query)); + const { text, values } = compile(lexis, query, encodeArgs(rest, query)); return new SqlQuery(text, values, executor, plugins); }; const api = Object.assign(call, { - withPlugin: (plugin: SqlPlugin) => - createDialectSql(dialect, executor, [...plugins, plugin]), + // The `sql` types of the dialects give withPlugin its type. + // biome-ignore lint/plugin: see above. + withPlugin: (plugin: SqlPlugin): unknown => + createSqlApi(dialect, executor, [...plugins, plugin]), prepare: (query: string) => { - const compiled = compile(query); + const compiled = compile(lexis, query); const prepared = executor?.prepare?.(compiled.text) ?? executor; return (...input: readonly unknown[]) => new SqlQuery( @@ -132,12 +130,12 @@ export function createSqlApi( as: call, param: (value: V) => param(value === empty ? value : encode(value, { kind: "value" })), - optional, + optional: (value: V) => (value === undefined ? empty : value), if: (value: V, then: (value: V) => R) => value ? then(value) : empty, json, fragment: (text: string, ...rest: readonly unknown[]) => - fragmentOf(compile(text, encodeArgs(rest, text))), + fragmentOf(compile(lexis, text, encodeArgs(rest, text))), id: (...parts: readonly string[]) => new SqlFragment(ident(quote, parts), []), combine: ( @@ -146,7 +144,7 @@ export function createSqlApi( ) => { const defined = present(items); return defined[0] - ? join(defined, options?.separator ?? ", ") + ? join(defined, options?.separator ?? ", ", lexis) : fail("sql.combine() got no items"); }, and: (items: readonly (Spliceable | Empty)[]) => bool(items, "AND"), @@ -169,7 +167,7 @@ export function createSqlApi( const cols = Array.from(new Set(rows.flatMap((row) => [...row.keys()]))); if (!cols[0]) { if (rows[1]) fail("sql.values() rows have no values"); - return new SqlFragment(dialect.emptyRow ?? "DEFAULT VALUES", []); + return new SqlFragment(emptyRow, []); } const values: unknown[] = []; const tuples = rows.map( @@ -178,7 +176,7 @@ export function createSqlApi( .map((column) => row.has(column) ? `$${values.push(row.get(column))}` - : dialect.valuesDefault === false + : valuesDefault === false ? fail( `no DEFAULT in VALUES here; give every row "${column}"`, ) @@ -191,7 +189,7 @@ export function createSqlApi( values, ); }, - raw: (text: string) => fragmentOf(compile(text, [])), + raw: (text: string) => fragmentOf(compile(lexis, text, [])), }); return Object.defineProperty(api, "dialect", { value: dialect.name, diff --git a/packages/core/src/dialect.ts b/packages/core/src/dialect.ts index 955fd090..f20fcc9c 100644 --- a/packages/core/src/dialect.ts +++ b/packages/core/src/dialect.ts @@ -1,4 +1,4 @@ -import type { SqlError } from "./errors.js"; +import type { AnyErr, SqlError } from "./errors.js"; import type { SqlPlugin } from "./plugin.js"; /** @@ -15,9 +15,29 @@ export type DialectBrand = { readonly inputs: (inputs: [DB, TM, Plugins]) => [DB, TM, Plugins]; }; -export type StrictNullChecksMessage = +/** + * What the brand of every `sql` of dialect `Name` with `Plugins`, for any + * schema and type map, is assignable to. + */ +export type AnyDialectBrand = { + readonly name: Name; + readonly inputs: ( + inputs: [never, never, Plugins], + ) => [unknown, unknown, Plugins]; +}; + +type StrictNullChecksMessage = "TypeSQL requires strictNullChecks: set strict or strictNullChecks to true in tsconfig.json"; +/** + * The error every statement is without strictNullChecks, and `never` with + * it. `null extends string` holds only without strictNullChecks; it involves + * no type parameter, so TypeScript resolves it once, where it is declared. + */ +export type NonStrict = null extends string + ? SqlError + : never; + /** * What an `sql` is without strictNullChecks: every call is an error saying * so, members included. Without it `null` and `undefined` belong to every @@ -25,18 +45,13 @@ export type StrictNullChecksMessage = * deeply (TS2589). */ interface NonStrictSql { - ( - query: SqlError, - ...params: readonly unknown[] - ): any; + (query: NonStrict, ...params: readonly unknown[]): any; readonly [member: string]: NonStrictSql; } /** - * `Api` with strictNullChecks on, and NonStrictSql without it. - * `null extends string` holds only without strictNullChecks; it involves no - * type parameter, so TypeScript resolves it once, where it is declared, and - * it costs nothing per query. + * `Api` with strictNullChecks on, and NonStrictSql without it. Like + * NonStrict, the check is resolved once and costs nothing per query. */ export type StrictSql = null extends string ? NonStrictSql : Api; @@ -53,6 +68,9 @@ export interface DialectTypes { readonly applyDdl: unknown; } +/** A dialect's `plugins` slot as its `sql` takes it: a plugin list, or none. */ +export type PluginsOf

= P extends readonly SqlPlugin[] ? P : []; + type SqlInputs = { readonly db: DB; readonly tm: TM; @@ -81,12 +99,46 @@ export type ValidateDdlOf = (T & export type ApplyDdlOf = (T & DdlInputs)["applyDdl"]; +/** + * A DDL script checked in dialect `T`: the script, or its error. A `string` + * that isn't a literal is an error before `applyDdl` reads it. + */ +export type DdlCheck< + T extends DialectTypes, + DB, + Q extends string, +> = string extends Q + ? SqlError< + "migrations need a string literal; use .raw() for SQL the type checker cannot see", + null + > + : ApplyDdlOf extends infer R + ? R extends AnyErr + ? R + : Q + : never; + +/** + * The runtime side of a dialect: its name and the facts of its SQL text. A + * fact it leaves out is standard SQL's: `"` quotes names, `DEFAULT VALUES` + * is a row of defaults, and the lexis is `LEXIS` (runtime/compile.ts). + */ export interface Dialect { readonly name: string; readonly types?: T; + /** Whether `sql.values` may write `DEFAULT` for a column a row leaves out. */ readonly valuesDefault?: boolean; - /** The identifier quote `sql.id`, `sql.set` and `sql.values` write; default `"`. */ + /** The identifier quote `sql.id`, `sql.set` and `sql.values` write. */ readonly quote?: '"' | "`"; - /** What `sql.values` writes for a row of defaults; default `DEFAULT VALUES`. */ + /** What `sql.values` writes for a row of defaults. */ readonly emptyRow?: string; + /** + * The text that holds no parameters, as a regular expression source: + * strings, quoted names and comments, then a `$n` parameter, whose number + * is its first capture group. A bare `/*` alternative opens a block comment + * that nests. compile() reads `:name` parameters and inline values outside + * it, and the drivers' `$n` rewriters read `$n` with it. The dialect's + * engine states the same rules for its types (`EngineTypes["lexis"]`). + */ + readonly lexis?: string; } diff --git a/packages/core/src/empty.ts b/packages/core/src/empty.ts index 12673f2d..3982f71b 100644 --- a/packages/core/src/empty.ts +++ b/packages/core/src/empty.ts @@ -1,7 +1,3 @@ -import type { SqlError } from "./errors.js"; -import type { InlineFragment } from "./query.js"; -import type { Slottable } from "./sql/slots.js"; - declare const EMPTY: unique symbol; /** What `sql.optional(value)` gives for `undefined`: DEFAULT in sql.param, a skipped item in sql.combine/and/or. */ @@ -9,47 +5,6 @@ export interface Empty { readonly [EMPTY]: true; } -/** - * The omission of an optional fragment or query, which splices nothing as - * `${…}`. Only it carries the string brand a template keeps: a branded - * omission next to a `string` value (`x ?? omission`, `flag ? omission : x`) - * would be reduced away with the DEFAULT it stands for. - */ -export type EmptyFragment = Empty & InlineFragment; - -/** - * `undefined` becomes the omission and `null` stays SQL `NULL`, as in - * sql.set, sql.values and sql.json. - */ -export type Optional = - | Exclude - | ([Extract] extends [never] - ? never - : [Exclude] extends [never] - ? Empty - : [Exclude] extends [Slottable] - ? EmptyFragment - : Empty); - -/** A `null` fragment would splice the text `null`, so it is an error. */ -export type OptionalInput = [Extract] extends [never] - ? unknown - : [Exclude] extends [never] - ? unknown - : [Exclude] extends [Slottable] - ? SqlError< - "use undefined, not null, to omit a fragment or query with sql.optional()", - null - > - : unknown; - -/** What `sql.if(value, then)` hands `then`: `value` without its falsy members. */ -export type Truthy = Exclude; - /** The sentinel itself; only sql.optional, sql.if and sql.json(undefined) hand it out. */ // biome-ignore lint/nursery/noUnsafeTypeAssertion: `Empty` is a type-only brand; the sentinel is known by identity. export const empty = {} as Empty; - -export const optional = (value: T): Optional => - // biome-ignore lint/nursery/noUnsafeTypeAssertion: `Optional` is a conditional type that the value cannot be checked against. - (value === undefined ? empty : value) as never; diff --git a/packages/core/src/engine/chars.ts b/packages/core/src/engine/chars.ts index db95a6bd..67af328f 100644 --- a/packages/core/src/engine/chars.ts +++ b/packages/core/src/engine/chars.ts @@ -75,3 +75,24 @@ export type NonIdAscii = | "|" | "?" | ":"; + +export type TakeDigits< + S extends string, + Acc extends string, +> = S extends `${infer C extends Digit}${infer R}` + ? TakeDigits + : [Acc, S]; + +/** The identifier characters at the start of `R`. */ +export type IdRun< + R extends string, + Acc extends string = "", +> = R extends `${infer C}${infer R1}` + ? C extends NonIdAscii + ? Acc + : IdRun + : Acc; + +export type TrimEnd = S extends `${infer R}${Ws}` + ? TrimEnd + : S; diff --git a/packages/core/src/engine/family/ddl.ts b/packages/core/src/engine/family/ddl.ts new file mode 100644 index 00000000..69cfcd41 --- /dev/null +++ b/packages/core/src/engine/family/ddl.ts @@ -0,0 +1,134 @@ +// Column lists of the MySQL and SQLite engines: CREATE and ALTER TABLE, and +// the order of a relation's columns. Schema columns carry their position +// (`i`), which orders `SELECT *` and `INSERT … VALUES` without a column list. +import type { Next } from "../next.js"; +import type { IsUnion, Members } from "../object.js"; + +export type LowerAll = { + [I in keyof L]: Lowercase; +}; + +/** `O` without the names `X`. */ +export type Without = O extends readonly [ + infer H extends string, + ...infer R extends readonly string[], +] + ? H extends X + ? Without + : [H, ...Without] + : []; + +/** The columns of `Rel` that `*` lists: all but invisible ones. */ +export type VisibleKeys = { + [K in keyof Rel & string]: Rel[K] extends { readonly hidden: true } + ? never + : K; +}[keyof Rel & string]; + +/** + * The columns `Keys` of `Rel` by position, or `null` when one of several has + * none: a union's member order is not the schema's declaration order. + */ +export type PositionOrder = Rel[Keys & + keyof Rel] extends { readonly i: number } + ? ByIndex, 0, []> + : IsUnion extends true + ? null + : [Keys]; +// Each column's name by its position, built once: walking it is one lookup +// per column, where scanning every column for each position is quadratic. +type PositionMap = { + [K in Keys & keyof Rel as Rel[K] extends { + readonly i: infer I extends number; + } + ? I + : never]: K; +}; +/** Names by position `0…`; a dropped column leaves a gap, and the walk goes on while a position is left. */ +type ByIndex< + M, + N extends number, + Acc extends readonly string[], + Seen extends number = never, +> = N extends keyof M + ? ByIndex, [...Acc, M[N] & string], Seen | N> + : [Exclude] extends [never] + ? Acc + : ByIndex, Acc, Seen>; + +/** `Keys` lowercase by position, or in no particular order without positions. */ +export type ColumnOrder = LowerAll< + PositionOrder extends infer O extends readonly string[] + ? O + : Members +>; + +/** `Keys` lowercase by position, or `null` without positions. */ +export type KnownOrder = + PositionOrder extends infer O extends readonly string[] + ? LowerAll + : null; + +/** `primaryKey` is `false` or how the column is a primary key. */ +type ColumnDef = { readonly name: string; readonly primaryKey: unknown }; + +/** The columns declared `PRIMARY KEY` inline, lowercase. */ +export type ColumnPks< + Cols extends readonly ColumnDef[], + Acc extends readonly string[] = [], +> = Cols extends readonly [ + infer H extends ColumnDef, + ...infer R extends readonly ColumnDef[], +] + ? ColumnPks< + R, + H["primaryKey"] extends false ? Acc : [...Acc, Lowercase] + > + : Acc; + +/** The first column whose name, ignoring case, an earlier one has, or `null`. */ +export type DupName< + Cols extends readonly ColumnDef[], + Seen extends string = never, +> = Cols extends readonly [ + infer H extends ColumnDef, + ...infer R extends readonly ColumnDef[], +] + ? Lowercase extends Seen + ? H["name"] + : DupName> + : null; + +/** Each element `c` of `L` with its position `i`. */ +export type Indexed = { + [I in keyof L]: { + readonly c: L[I]; + readonly i: I extends `${infer N extends number}` ? N : 0; + }; +}; + +/** The position of a column added to `Rel`: after the last one. */ +export type AppendIndex = NextIndex< + { + [P in keyof Rel]: Rel[P] extends { readonly i: infer N extends number } + ? N + : never; + }[keyof Rel] +>; +// Positions are 0…n-1 unless a column was dropped: count up while each one +// exists (one membership check per column), and look for the largest one +// only after a gap. +type NextIndex< + U extends number, + A extends unknown[] = [], + Seen extends number = never, +> = A["length"] extends U + ? NextIndex + : [Exclude] extends [never] + ? A["length"] + : Next>; +type MaxOf = [U] extends [never] + ? 0 + : [Exclude] extends [never] + ? A["length"] + : MaxOf, [...A, 0]>; diff --git a/packages/core/src/engine/family/diagnostics.ts b/packages/core/src/engine/family/diagnostics.ts new file mode 100644 index 00000000..aa45d0d0 --- /dev/null +++ b/packages/core/src/engine/family/diagnostics.ts @@ -0,0 +1,141 @@ +// Where MySQL and SQLite errors point. The binder and typer don't know token +// positions, so an error records what to look for (`NameAt`), and a parser +// error the token it stopped at (`SyntaxAt`); `Located` resolves both against +// the statement's tokens once analysis fails. +import type { SqlError } from "../../errors.js"; +import type { IsRelation, RelCols } from "../../schema.js"; +import type { Next } from "../next.js"; +import type { Best } from "../suggest.js"; +import type { Toks } from "../tokens.js"; +import type { RawOf } from "./lex.js"; + +export type SyntaxAt = { + readonly syntax: I; + readonly tok: Tok; +}; + +/** The kind of name a "Perhaps you meant" hint suggests. */ +export type HintKind = "column" | "table" | "function" | null; + +/** + * Points at the first word or identifier `Find`, and suggests a `Hint` close + * to `Name`. + */ +export type NameAt< + Find extends string, + Hint extends HintKind = null, + Name extends string = Find, +> = { + readonly find: Find; + readonly hint: Hint; + readonly name: Name; +}; + +/** `a.b.c` points at `c`. */ +export type LastPart = N extends `${string}.${infer R}` + ? LastPart + : N; + +/** Where an error about the named column, table or function points. */ +export type NamedAt = NameAt< + LastPart>, + Hint +>; + +/** The dialect's syntax error message: `near[0]`, the token's text, `near[1]`, or `end`. */ +type SyntaxWords = { + readonly near: readonly [string, string]; + readonly end: string; +}; + +type SyntaxMessage = Tok extends { + readonly k: string; +} + ? `${W["near"][0]}${RawOf}${W["near"][1]}` + : W["end"]; + +/** An error with its position in the statement `T`, and a hint for unknown names. */ +export type Located = + E extends SqlError + ? At extends number + ? E + : At extends SyntaxAt + ? SqlError, I> + : At extends NameAt + ? SqlError< + Hinted, H>, + FindName + > + : SqlError + : E; + +/** An error of a DDL script, which has no position unless it is a syntax error. */ +export type Unlocated = + E extends SqlError + ? At extends SyntaxAt + ? SqlError, I> + : At extends NameAt + ? SqlError + : E + : E; + +type Hinted = S extends string + ? `${M}. Perhaps you meant the ${What & string} "${S}"?` + : M; + +type Suggest< + H extends HintKind, + N extends string, + T extends Toks, + DB, + Builtins, +> = H extends "column" + ? Best>, true> + : H extends "table" + ? Best, true> + : H extends "function" + ? Best, true> + : null; + +type RelNames = { + [K in keyof DB & string]: K extends `$${string}` + ? never + : IsRelation extends true + ? K + : never; +}[keyof DB & string]; + +/** The relations the statement names: their columns are the column hints. */ +type Mentioned = T[I] extends { + readonly k: string; +} + ? Mentioned, Acc | RelAt> + : Acc; +type RelAt = Tok extends { + readonly k: "w" | "id"; + readonly v: infer V; +} + ? { + [K in RelNames]: Lowercase extends V ? K : never; + }[RelNames] + : never; + +type ColumnsOf = { + [K in RelNames]: K extends Rels ? RelCols : never; +}[RelNames]; + +type FnNames = + | (Builtins & string) + | (DB extends { readonly $functions: infer F } ? keyof F & string : never); + +type FindName< + T extends Toks, + Name extends string, + I extends number, +> = T[I] extends { + readonly k: string; +} + ? T[I] extends { readonly k: "w" | "id"; readonly v: Name } + ? I + : FindName> + : 0; diff --git a/packages/core/src/engine/family/functions.ts b/packages/core/src/engine/family/functions.ts new file mode 100644 index 00000000..92341a08 --- /dev/null +++ b/packages/core/src/engine/family/functions.ts @@ -0,0 +1,96 @@ +// User-declared functions of the MySQL and SQLite engines: `$functions` in +// the schema and the dialect's augmentable `FunctionCatalog` hold `Sig`s or +// overload lists of them. +import type { Next } from "../next.js"; +import type { GE } from "../num.js"; +import type { Info } from "../types.js"; + +/** The largest argument count: no limit. */ +export type Many = 1000; + +type AnySig = { + readonly args: readonly string[]; + readonly ret: string; +}; + +/** The declaration of function `N` (lowercase) in the schema, else in `Catalog`, else `null`. */ +export type DeclaredFn = ( + DB extends { readonly $functions: infer F } + ? MatchName + : never +) extends infer D + ? [D] extends [never] + ? MatchName extends infer C + ? [C] extends [never] + ? null + : C + : never + : D + : never; +type MatchName = { + [K in keyof O & string]: Lowercase extends N ? O[K] : never; +}[keyof O & string]; + +/** The signature of an overload list that takes `Argc` arguments, else the first. */ +export type PickSig = D extends readonly unknown[] + ? PickLoop + : D; +type PickLoop< + D extends readonly unknown[], + Argc extends number, + I extends number, +> = D[I] extends infer S extends AnySig + ? Accepts extends true + ? S + : PickLoop> + : D[0]; + +/** The arguments a signature needs: all but a trailing `"..."`. */ +export type MinArgs = A extends readonly [ + ...infer H extends readonly string[], + "...", +] + ? H["length"] + : A["length"]; +export type MaxArgs = A extends readonly [ + ...unknown[], + "...", +] + ? Many + : A["length"]; +type Accepts< + A extends readonly string[], + N extends number, +> = A extends readonly [...unknown[], "..."] + ? GE> + : N extends A["length"] + ? true + : false; + +/** The type a declared function takes at argument `I`. */ +type SigArgType = S extends AnySig + ? S["args"][I] extends infer T extends string + ? T extends "..." + ? Repeated + : T + : Repeated + : "unknown"; +type Repeated = S["args"] extends readonly [ + ...unknown[], + infer P extends string, + "...", +] + ? P + : "unknown"; + +/** The parameter slots of a call to a declared function with arguments `L`. */ +export type SigSlots< + S, + L extends readonly unknown[], + Acc extends readonly unknown[] = [], +> = Acc["length"] extends L["length"] + ? Acc + : SigSlots, true>]>; + +export type ArityOk = + GE extends true ? (Max extends Many ? true : GE) : false; diff --git a/packages/core/src/engine/family/json.ts b/packages/core/src/engine/family/json.ts new file mode 100644 index 00000000..245da771 --- /dev/null +++ b/packages/core/src/engine/family/json.ts @@ -0,0 +1,81 @@ +// JSON paths of the MySQL and SQLite engines: the steps of a `$…` path and +// the TypeScript type they reach in a column's declared JSON type. + +import type { TsShaped } from "../../schema.js"; + +/** The declared TypeScript type of a JSON column, as `[T]`, or `null`. */ +export type JsonShapeOf = T extends TsShaped ? [V] : null; + +export type JsonStep = { readonly key: string } | { readonly index: true }; + +/** + * The steps of `S`, a path after its `$`, or `null` for a path the type + * can't follow: one with a step matching `Wild`. + */ +export type JsonSteps< + S extends string, + Wild, + Acc extends readonly JsonStep[] = [], +> = S extends "" + ? Acc + : S extends Wild + ? null + : S extends `.${infer R}` + ? R extends `"${infer K}"${infer R2}` + ? JsonSteps + : KeyRun extends [ + infer K extends string, + infer R2 extends string, + ] + ? JsonSteps + : null + : S extends `[${string}]${infer R}` + ? JsonSteps + : null; +type KeyRun< + S extends string, + Acc extends string, +> = S extends `${infer C}${infer R}` + ? C extends "." | "[" + ? [Acc, S] + : KeyRun + : [Acc, S]; + +/** + * The type path `P` reaches in `V`, and whether it may find nothing: + * `unknown` where the type doesn't say. + */ +export type JsonWalk< + V, + P extends readonly JsonStep[], + Missing extends boolean, +> = P extends readonly [infer H, ...infer R extends readonly JsonStep[]] + ? [Exclude] extends [never] + ? [null, true] + : H extends { readonly key: infer K extends string } + ? Exclude extends infer O + ? O extends readonly unknown[] + ? [unknown, true] + : O extends object + ? K extends keyof O + ? JsonWalk< + O[K], + R, + Absent ? true : false> + > + : string extends keyof O + ? JsonWalk + : [never, true] + : [unknown, true] + : never + : Exclude extends readonly (infer El)[] + ? JsonWalk + : [unknown, true] + : [V, Absent]; +type Absent = Missing extends true + ? true + : null extends V + ? true + : undefined extends V + ? true + : Else; diff --git a/packages/core/src/engine/family/lex.ts b/packages/core/src/engine/family/lex.ts new file mode 100644 index 00000000..2dfb9d23 --- /dev/null +++ b/packages/core/src/engine/family/lex.ts @@ -0,0 +1,154 @@ +// Tokens and lexer leaves shared by the MySQL and SQLite lexers. A token keeps +// the whitespace and comments before it (`g`) and, unless its value is its +// text, its text as written (`r`), so names and messages can quote the +// statement's own text. +import type { + Digit, + HexDigit, + IdRun, + NonIdAscii, + TakeDigits, +} from "../chars.js"; +import type { ToNum } from "../num.js"; + +export type Word = { + readonly k: "w"; + readonly v: V; + readonly r: R; + readonly g: G; +}; +export type Ident< + V extends string, + N extends string, + R extends string, + G extends string, +> = { + readonly k: "id"; + readonly v: V; + readonly n: N; + readonly r: R; + readonly g: G; +}; +export type Num = { + readonly k: "n"; + readonly v: V; + readonly g: G; +}; +export type Str< + V extends string, + R extends string, + G extends string, + F extends string = V, +> = { + readonly k: "s"; + readonly v: V; + readonly r: R; + readonly g: G; + /** The first of adjacent strings (`'a' 'b'`), which MySQL names the column after. */ + readonly f: F; +}; +export type Blob = { + readonly k: "s"; + readonly v: V; + readonly r: R; + readonly g: G; + readonly blob: true; +}; +/** `$N`, with its text as the server receives it. */ +export type Param = { + readonly k: "$"; + readonly v: N; + readonly r: R; + readonly g: G; +}; +export type Op = { + readonly k: "o"; + readonly v: V; + readonly g: G; +}; + +export type RawOf = T extends { readonly r: infer R extends string } + ? R + : T extends { readonly v: infer V extends string } + ? V + : ""; + +export type NameOf = T extends { readonly k: "id"; readonly n: infer N } + ? N + : T extends { readonly k: "w"; readonly r: infer R } + ? R + : T extends { readonly k: "s"; readonly v: infer V } + ? V + : never; + +/** Lexer steps (a token or a whitespace character) per round, which bounds the recursion depth. */ +export type ChunkSize = 200; + +export type LexErr = [ + "err", + M, + Acc, +]; + +export type BadParam = + `TypeSQL parameters are written $1 or :name, not "${T}"`; + +/** The parameter number after a `$` and the text after it, or the error message. */ +export type DollarParam = + R extends `${infer D extends Digit}${infer R1}` + ? TakeDigits extends [ + infer V extends string, + infer R2 extends string, + ] + ? IdRun extends "" + ? [ToNum, R2] + : BadParam<`$${V}${IdRun}`> + : never + : BadParam<`$${IdRun}`>; + +export type IsHex< + S extends string, + Even extends boolean, +> = S extends `${infer C}${infer R}` + ? C extends HexDigit + ? IsHex + : false + : Even; + +/** The text up to the closing quote `Qc`, where a doubled quote is one quote. */ +export type TakeQuoted< + S extends string, + Qc extends string, + Acc extends string, +> = S extends `${infer Body}${Qc}${infer R}` + ? R extends `${Qc}${infer R2}` + ? TakeQuoted + : [`${Acc}${Body}`, R] + : null; + +export type Escape< + S extends string, + Q extends string, + Acc extends string = "", +> = S extends `${infer A}${Q}${infer B}` + ? Escape + : `${Acc}${S}`; + +export type TakeWord< + S extends string, + Acc extends string, +> = S extends `${infer A}${infer B}${infer C}${infer D}${infer R}` + ? A extends NonIdAscii + ? [Acc, S] + : B extends NonIdAscii + ? [`${Acc}${A}`, `${B}${C}${D}${R}`] + : C extends NonIdAscii + ? [`${Acc}${A}${B}`, `${C}${D}${R}`] + : D extends NonIdAscii + ? [`${Acc}${A}${B}${C}`, `${D}${R}`] + : TakeWord + : S extends `${infer C}${infer R}` + ? C extends NonIdAscii + ? [Acc, S] + : TakeWord + : [Acc, S]; diff --git a/packages/core/src/engine/family/parse.ts b/packages/core/src/engine/family/parse.ts new file mode 100644 index 00000000..3e2b1bdb --- /dev/null +++ b/packages/core/src/engine/family/parse.ts @@ -0,0 +1,151 @@ +// Parser leaves shared by the MySQL and SQLite parsers. Identifier rules take +// the dialect's reserved words as `Not`; a syntax error records the token it +// stopped at, and the dialect words the message (see `SyntaxWords`). +import type { SqlError } from "../../errors.js"; +import type { TrimEnd } from "../chars.js"; +import type { Next } from "../next.js"; +import type { Ok, Toks } from "../tokens.js"; +import type { SyntaxAt } from "./diagnostics.js"; +import type { NameOf, RawOf } from "./lex.js"; + +export type SyntaxErr = SqlError< + "", + SyntaxAt +>; + +export type Name = { readonly v: string; readonly n: string }; + +export type IdentAt = T[I] extends { + readonly k: "id"; +} + ? Name & { readonly v: T[I]["v" & keyof T[I]]; readonly n: NameOf } + : T[I] extends { readonly k: "w"; readonly v: infer V } + ? V extends Not + ? null + : { readonly v: V; readonly n: NameOf } + : null; + +export type AliasAt = T[I] extends { + readonly k: "s"; + readonly v: infer V extends string; +} + ? T[I] extends { readonly blob: true } + ? null + : { readonly v: Lowercase; readonly n: V } + : IdentAt; + +export type IsOp< + T extends Toks, + I extends number, + O extends string, +> = T[I] extends { readonly k: "o"; readonly v: O } ? true : false; +export type IsWord< + T extends Toks, + I extends number, + W extends string, +> = T[I] extends { readonly k: "w"; readonly v: W } ? true : false; + +export type Expect< + T extends Toks, + I extends number, + O extends string, +> = T[I] extends { readonly k: "o"; readonly v: O } ? Next : SyntaxErr; + +export type CloseOk = + IsOp extends true ? Ok> : SyntaxErr; + +/** The statement's own text of tokens `I` to `J - 1`. */ +export type SpanText< + T extends Toks, + I extends number, + J extends number, +> = TrimEnd, J, RawOf>>; +type SpanLoop< + T extends Toks, + I extends number, + J extends number, + Acc extends string, +> = I extends J + ? T[I] extends { readonly g: infer G extends string } + ? `${Acc}${G}` + : Acc + : T[I] extends { readonly g: infer G extends string } + ? SpanLoop, J, `${Acc}${G}${RawOf}`> + : Acc; + +export type IdentList = + Expect extends infer J extends number + ? IdentListLoop + : Expect; +type IdentListLoop< + T extends Toks, + I extends number, + Acc extends readonly Name[], + Not, +> = + IdentAt extends infer N extends Name + ? T[Next] extends { readonly k: "o"; readonly v: "," } + ? IdentListLoop>, [...Acc, N], Not> + : CloseOk, [...Acc, N]> + : SyntaxErr; + +export type OptCols = + IsOp extends true ? IdentList : Ok; + +export type NamesV = { + [K in keyof N]: N[K] extends Name ? N[K]["v"] : never; +}; + +export type TableRefAt = + IdentAt extends infer A extends Name + ? IsOp, "."> extends true + ? IdentAt>, Not> extends infer B extends Name + ? Ok<{ schema: A["v"]; name: B["v"] }, Next>>> + : SyntaxErr>> + : Ok<{ schema: null; name: A["v"] }, Next> + : SyntaxErr; + +export type SkipSemis = + IsOp extends true ? SkipSemis> : I; + +/** `-E`: a negated number literal folds into the literal, so `- -0` is `0`. */ +export type Negate = E extends { + readonly t: "num"; + readonly v: infer V extends string; +} + ? V extends `-${infer P}` + ? { t: "num"; v: P } + : { t: "num"; v: `-${V}` } + : { t: "un"; op: "-"; e: E }; + +/** `IF NOT EXISTS`: whether it is there, and the index after it. */ +export type IfNotExists = + IsWord extends true + ? IsWord, "not"> extends true + ? IsWord>, "exists"> extends true + ? [true, Next>>] + : SyntaxErr>> + : SyntaxErr> + : [false, I]; +/** `IF EXISTS`: whether it is there, and the index after it. */ +export type IfExists = + IsWord extends true + ? IsWord, "exists"> extends true + ? [true, Next>] + : SyntaxErr> + : [false, I]; + +/** Skips the optional `COLUMN` of `ALTER TABLE … ADD | DROP | RENAME COLUMN`. */ +export type SkipColumn = + IsWord extends true ? Next : I; + +/** Skips to the end of a statement the schema ignores. */ +export type SkipStatement< + T extends Toks, + I extends number, + What extends string, +> = T[I] extends { readonly k: "o"; readonly v: ";" } + ? Ok<{ t: "noop"; what: What }, I> + : T[I] extends { readonly k: string } + ? SkipStatement, What> + : Ok<{ t: "noop"; what: What }, I>; diff --git a/packages/core/src/engine/family/scope.ts b/packages/core/src/engine/family/scope.ts new file mode 100644 index 00000000..f0ac43dc --- /dev/null +++ b/packages/core/src/engine/family/scope.ts @@ -0,0 +1,196 @@ +// Binder leaves shared by the MySQL and SQLite engines: walking the items of +// a FROM level and building them. +import type { Next } from "../next.js"; +import type { Without } from "./ddl.js"; +import type { Cell, ColMap, ColOf, Item, Level } from "./types.js"; + +export type RefDisplay< + Ref extends { readonly schema: string | null; readonly name: string }, +> = Ref["schema"] extends string + ? `${Ref["schema"]}.${Ref["name"]}` + : Ref["name"]; + +export type CellsMap = { + readonly [E in C[number] as Lowercase]: ColOf< + E["t"]["type"], + E["t"]["nullable"], + E["n"] + >; +}; + +/** + * The key of `O` named `Lc`, ignoring case; `$`-prefixed keys are metadata, + * not tables or columns. + */ +export type FindKey = Lc extends keyof O + ? Lc + : { + [K in keyof O & string]: K extends `$${string}` + ? never + : Lowercase extends Lowercase + ? K + : never; + }[keyof O & string]; + +/** Cells `C` renamed by a column list `N`; a cell past its end keeps its name. */ +export type RenameCells< + C extends readonly Cell[], + N extends readonly { readonly n: string }[], +> = { + [I in keyof C]: { + readonly n: N[I & keyof N] extends { readonly n: infer X extends string } + ? X + : C[I]["n"]; + readonly t: C[I]["t"]; + }; +}; + +/** Whether an item of `L` has a visible column `C`. */ +export type LevelHas = true extends { + [I in keyof L]: C extends keyof L[I]["cols"] + ? C extends L[I]["skip"][number] + ? false + : true + : false; +}[number] + ? true + : false; + +/** The columns of a `NATURAL` join: those of `L`, in order, that `R` has too. */ +export type NaturalCols = + LevelNames extends infer RN extends string + ? FilterNames, RN> + : never; +type LevelNames = { + [I in keyof L]: Exclude; +}[number]; +type LevelOrder< + L extends Level, + I extends number = 0, + Acc extends readonly string[] = [], +> = L[I] extends Item + ? LevelOrder< + L, + Next, + [...Acc, ...Without] + > + : Acc; +type FilterNames = O extends readonly [ + infer H extends string, + ...infer R extends readonly string[], +] + ? H extends Keep + ? [H, ...FilterNames] + : FilterNames + : []; + +/** + * The items of the nullable side of an outer join: every column nullable, + * and `orig` keeps the columns as the relation has them. + */ +export type NullLevel = { + readonly [I in keyof L]: NullItem; +}; +type NullItem = { + readonly [P in keyof It]: P extends "cols" + ? { + readonly [K in keyof It["cols"]]: ColOf< + It["cols"][K]["type"], + true, + It["cols"][K]["n"] + >; + } + : It[P]; +} & { + readonly orig: It extends { readonly orig: infer O extends ColMap } + ? O + : It["cols"]; +}; + +/** Hides the `USING` columns `U` of each item of `L`. */ +export type SkipIn = { + readonly [I in keyof L]: { + readonly [P in keyof L[I]]: P extends "skip" + ? [...L[I]["skip"], ...Extract[]] + : L[I][P]; + }; +}; + +/** Column `C` of whichever items of `L` have it. */ +export type LevelCol = { + [I in keyof L]: C extends keyof L[I]["cols"] ? L[I]["cols"][C] : never; +}[number]; + +/** The visible columns `Name` of `L`, each tagged with its item's index (a union when ambiguous). */ +export type BareIn = { + [I in keyof L]: Name extends keyof L[I]["cols"] + ? Name extends L[I]["skip"][number] + ? never + : Tag + : never; +}[number]; +type Tag = C & { readonly at: I }; + +/** The cells of `*` over level `L`. */ +export type AllCells< + L extends Level, + I extends number, + Acc extends readonly Cell[], +> = L[I] extends Item + ? AllCells< + L, + Next, + [...Acc, ...ItemCells] + > + : Acc; +/** An item's cells; a skipped column the item substitutes (`subst`) is listed as that. */ +export type ItemCells< + It extends Item, + O extends readonly string[], + Skip, + Acc extends readonly Cell[] = [], +> = O extends readonly [ + infer H extends string, + ...infer R extends readonly string[], +] + ? H extends Skip + ? ( + It extends { readonly subst: infer S } + ? H extends keyof S + ? S[H] + : null + : null + ) extends { + readonly type: infer T extends string; + readonly nullable: infer N extends boolean; + readonly n: infer D extends string; + } + ? ItemCells< + It, + R, + Skip, + [ + ...Acc, + { + readonly n: D; + readonly t: { readonly type: T; readonly nullable: N }; + }, + ] + > + : ItemCells + : ItemCells< + It, + R, + Skip, + [ + ...Acc, + { + readonly n: It["cols"][H]["n"]; + readonly t: { + readonly type: It["cols"][H]["type"]; + readonly nullable: It["cols"][H]["nullable"]; + }; + }, + ] + > + : Acc; diff --git a/packages/core/src/engine/family/typer.ts b/packages/core/src/engine/family/typer.ts new file mode 100644 index 00000000..6c0dc571 --- /dev/null +++ b/packages/core/src/engine/family/typer.ts @@ -0,0 +1,19 @@ +// Typer leaves shared by the MySQL and SQLite engines. +import type { ColInfo } from "../types.js"; + +/** Whether one of the types can be `NULL`. */ +export type AnyNull = + true extends L[number]["nullable"] ? true : false; + +/** Whether every one of the types can be `NULL`. */ +export type AllNull = + false extends L[number]["nullable"] ? false : true; + +/** Whether the digits of a number literal, up to its exponent, include a nonzero one. */ +export type NonZeroDigits = V extends `${infer H}${infer R}` + ? H extends "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + ? true + : H extends "e" | "E" + ? false + : NonZeroDigits + : false; diff --git a/packages/core/src/engine/family/types.ts b/packages/core/src/engine/family/types.ts new file mode 100644 index 00000000..5e7f7f59 --- /dev/null +++ b/packages/core/src/engine/family/types.ts @@ -0,0 +1,95 @@ +// The scope model of the MySQL and SQLite engines: the relations a query +// reads, the CTEs it can name, and how it aggregates. +import type { ColInfo, Scope, ScopeItem } from "../types.js"; + +/** A column in scope: its type, and its name as written in the schema or query. */ +export type Col = ColInfo & { readonly n: string }; +export type ColOf = { + readonly type: T; + readonly nullable: N; + readonly n: Name; +}; +/** Columns by lowercase name. */ +export type ColMap = { readonly [lc: string]: Col }; + +/** + * A relation in scope (see `ScopeItem`): `order` lists its columns for `*`, + * `orig` keeps its own columns when an outer join made them nullable, and + * `subst` holds the columns a right `USING` join lists in `*` in place of + * the skipped ones. + */ +export type Item = ScopeItem & { + readonly cols: ColMap; + readonly order: readonly string[]; + readonly orig?: ColMap; + readonly subst?: ColMap; +}; +export type Level = readonly Item[]; + +export type CteMap = { + readonly [lc: string]: { + readonly cols: ColMap; + readonly order: readonly string[]; + }; +}; + +/** `group`: GROUP BY; `whole`: aggregates without GROUP BY, one row. */ +export type AggMode = "none" | "group" | "whole"; + +/** + * What an expression is typed against. `aggScope` is the scope an aggregate's + * arguments read, before grouping made columns nullable; `aliases` are the + * SELECT aliases the clause may name. + */ +export type Ctx = { + readonly db: unknown; + readonly scope: Scope; + readonly aggScope: Scope; + readonly ctes: CteMap; + readonly agg: AggMode; + readonly inAgg: boolean; + readonly aliases: ColMap; +}; + +export type MkCtx< + DB, + S extends Scope, + Ctes extends CteMap, + Agg extends AggMode = "none", + Aliases extends ColMap = {}, + AggScope extends Scope = S, + InAgg extends boolean = false, +> = { + readonly db: DB; + readonly scope: S; + readonly aggScope: AggScope; + readonly ctes: Ctes; + readonly agg: Agg; + readonly inAgg: InAgg; + readonly aliases: Aliases; +}; + +/** The context of an aggregate's arguments. */ +export type AggCtx = MkCtx< + C["db"], + C["aggScope"], + C["ctes"], + "none", + C["aliases"], + C["aggScope"], + true +>; + +/** A result column: its name, type, and the name a derived table gives it (`d`). */ +export type Cell = { + readonly n: string; + readonly t: ColInfo; + readonly d?: string; +}; + +/** The environment of query binding, projection and parameter inference. */ +export type QueryContext = { + readonly db: DB; + readonly scope: S; + readonly ctes: Ctes; +}; diff --git a/packages/core/src/engine/narrow.ts b/packages/core/src/engine/narrow.ts index 50b13dd6..50bb38e5 100644 --- a/packages/core/src/engine/narrow.ts +++ b/packages/core/src/engine/narrow.ts @@ -1,6 +1,10 @@ -import type { Nx } from "./parse.js"; +import type { Next } from "./next.js"; import type { ColMap, ScopeItem, ScopeLevel } from "./types.js"; +/** + * The columns a WHERE, ON or FILTER condition `E` proves non-null: those a + * conjunct rejects NULL in, as `[table or null, column]`. + */ export type NarrowKeys = AndKeys; type AndKeys = E extends { @@ -12,6 +16,7 @@ type AndKeys = E extends { ? AndKeys> : Acc | ConjunctKeys; +// Comparisons that are NULL when an operand is NULL, of every dialect. type StrictBoolOp = | "=" | "==" @@ -81,8 +86,18 @@ type ConjunctKeys = E extends { readonly t: "col" } ? ColKey : ColKey | ColKey | ColKey : E extends { readonly t: "in" | "insub"; readonly e: infer X } - ? ColKey - : E extends { readonly t: "quant"; readonly l: infer X } + ? // `NULL NOT IN ()` and `NULL NOT IN (empty subquery)` are true + E extends { readonly not: false } + ? ColKey + : E extends { readonly list: readonly [unknown, ...unknown[]] } + ? ColKey + : never + : // `NULL <> ALL (empty subquery)` is true + E extends { + readonly t: "quant"; + readonly q: "any"; + readonly l: infer X; + } ? ColKey : never; @@ -96,10 +111,6 @@ type ColKey = E extends { ? ColKey : never; -export type UsingKeys = U extends string - ? readonly [null, U] - : never; - export type NarrowLevel = [Keys] extends [never] ? L : AnyHit extends true @@ -127,7 +138,7 @@ type AnyHit< > = L[I] extends ScopeItem ? true extends KeyHits ? true - : AnyHit> + : AnyHit> : false; type KeyHits = Keys extends readonly [ unknown, @@ -142,9 +153,17 @@ type KeyHits = Keys extends readonly [ : false : false; +// An item an outer join made nullable keeps its own columns in `orig`. A +// condition that rejects NULL in one of its columns drops the rows where it +// had no match, so the columns get back their own nullability. type NarrowItem = { readonly [P in keyof It]: P extends "cols" - ? NarrowCols + ? NarrowCols< + It extends { readonly orig: ColMap } ? It["orig"] : It["cols"], + It["alias"], + It["skip"][number], + Keys + > : It[P]; }; type NarrowCols = { diff --git a/packages/core/src/sql/next.ts b/packages/core/src/engine/next.ts similarity index 100% rename from packages/core/src/sql/next.ts rename to packages/core/src/engine/next.ts diff --git a/packages/core/src/engine/num.ts b/packages/core/src/engine/num.ts new file mode 100644 index 00000000..08756400 --- /dev/null +++ b/packages/core/src/engine/num.ts @@ -0,0 +1,50 @@ +import type { Digit } from "./chars.js"; + +/** A tuple of `N` elements `E`, for arithmetic on small numbers. */ +export type Tup< + N extends number, + E = 0, + A extends unknown[] = [], +> = A["length"] extends N ? A : Tup; + +export type Add = [ + ...Tup, + ...Tup, +]["length"] & + number; + +export type Lt = A extends B + ? false + : Tup extends [...Tup, ...unknown[]] + ? true + : false; + +export type GE = + Tup extends [...Tup, ...unknown[]] ? true : false; + +/** The length of `S`, or `Cap` when it has `Cap` or more characters. */ +export type CappedLen< + S extends string, + Cap extends number, + A extends unknown[] = [], +> = S extends `${string}${infer R}` + ? A["length"] extends Cap + ? Cap + : CappedLen + : A["length"]; + +export type ToNum = S extends `${infer N extends number}` + ? N + : never; + +/** Whether the digits `A` are at most the digits `B`, which have as many. */ +export type DigitsLE< + A extends string, + B extends string, +> = A extends `${infer X extends Digit}${infer AR}` + ? B extends `${infer Y extends Digit}${infer BR}` + ? X extends Y + ? DigitsLE + : GE, ToNum> + : false + : true; diff --git a/packages/core/src/engine/object.ts b/packages/core/src/engine/object.ts index 48751f39..41bb1de0 100644 --- a/packages/core/src/engine/object.ts +++ b/packages/core/src/engine/object.ts @@ -1,14 +1,24 @@ -import type { Simplify } from "./types.js"; +export type Simplify = { [K in keyof T]: T[K] } & {}; type Single = { [P in K]: V }; -type U2I = (U extends unknown ? (x: U) => void : never) extends ( - x: infer I, -) => void +type UnionToIntersection = ( + U extends unknown + ? (x: U) => void + : never +) extends (x: infer I) => void ? I : never; -type Build = [U] extends [never] ? {} : Simplify>; +export type IsUnion = T extends unknown + ? [U] extends [T] + ? false + : true + : never; + +export type Or = A extends true ? true : B extends true ? true : false; + +type Build = [U] extends [never] ? {} : Simplify>; export type Rebuild = Build< { [K in keyof O]: Single }[keyof O] @@ -82,13 +92,3 @@ type LastFour = (U extends unknown ? (x: () => U) => void : never) extends ( ? [D, Exclude, Exclude, Exclude] : never : never; - -export type Tup< - N extends number, - A extends unknown[] = [], -> = A["length"] extends N ? A : Tup; -export type Lt = A extends B - ? false - : Tup extends [...Tup, ...unknown[]] - ? true - : false; diff --git a/packages/core/src/engine/params.ts b/packages/core/src/engine/params.ts index a40ce3fd..4a28b884 100644 --- a/packages/core/src/engine/params.ts +++ b/packages/core/src/engine/params.ts @@ -1,4 +1,4 @@ -import type { Next } from "../sql/next.js"; +import type { Next } from "./next.js"; import type { Toks } from "./tokens.js"; import type { ColInfo } from "./types.js"; diff --git a/packages/core/src/engine/parse.ts b/packages/core/src/engine/parse.ts deleted file mode 100644 index 5183150c..00000000 --- a/packages/core/src/engine/parse.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { SqlError } from "../errors.js"; -import type { Next } from "../sql/next.js"; -import type { - IdTok, - NumTok, - OpTok, - ParamTok, - StrTok, - Tok, - Toks, - WordTok, -} from "./tokens.js"; - -export type Ok = readonly [N, J]; - -export type Nx = Next; -export type Nx2 = Nx>; -export type Nx3 = Nx>; - -export type Fail = SqlError< - Message, - I ->; - -export type Near = T[I] extends infer H - ? H extends WordTok - ? V - : H extends IdTok - ? `"${V}"` - : H extends OpTok - ? V - : H extends NumTok - ? V - : H extends StrTok - ? `'${V}'` - : H extends ParamTok - ? `$${V}` - : "end of input" - : never; - -export type SyntaxErrorAt = Fail< - `syntax error at or near ${Near}`, - I ->; -export type Expected< - What extends string, - T extends Toks, - I extends number, -> = Fail<`syntax error: expected ${What} at or near ${Near}`, I>; - -export type AtEnd = T[I] extends Tok - ? false - : true; - -export type EatWord< - T extends Toks, - I extends number, - W extends string, -> = T[I] extends { readonly k: "w"; readonly v: W } ? Nx : never; -export type EatOp< - T extends Toks, - I extends number, - O extends string, -> = T[I] extends { readonly k: "o"; readonly v: O } ? Nx : never; -export type EatWords< - T extends Toks, - I extends number, - Ws extends readonly string[], -> = Ws extends readonly [ - infer W extends string, - ...infer R extends readonly string[], -] - ? EatWord extends never - ? never - : EatWords, R> - : I; -export type SkipWord = - EatWord extends never ? I : EatWord; -export type SkipWords< - T extends Toks, - I extends number, - Ws extends readonly string[], -> = EatWords extends never ? I : EatWords; - -export type AnyLabel = - LabelName extends infer V extends string ? [V, Nx] : null; -type LabelName = H extends IdTok | WordTok ? V : null; - -export type SkipToCloseParen< - T extends Toks, - I extends number, - Depth extends readonly unknown[] = [], -> = - T[I] extends OpTok<"("> - ? SkipToCloseParen, [...Depth, 0]> - : T[I] extends OpTok<")"> - ? Depth extends readonly [unknown, ...infer D2] - ? SkipToCloseParen, D2> - : Nx - : T[I] extends Tok - ? SkipToCloseParen, Depth> - : SyntaxErrorAt; diff --git a/packages/core/src/engine/suggest.ts b/packages/core/src/engine/suggest.ts index 971cf37f..ddb72ef2 100644 --- a/packages/core/src/engine/suggest.ts +++ b/packages/core/src/engine/suggest.ts @@ -1,16 +1,5 @@ -import type { Lt, Members, Tup } from "./object.js"; - -export type IsRelation = X extends { readonly $view: unknown } - ? true - : X extends object - ? X[keyof X] extends { readonly type: string } - ? true - : false - : false; - -export type RelCols = X extends { readonly $view: infer C } - ? keyof C & string - : keyof X & string; +import type { Lt, Tup } from "./num.js"; +import type { Members } from "./object.js"; export type Best< Name extends string, diff --git a/packages/core/src/engine/tokens.ts b/packages/core/src/engine/tokens.ts index 642830e5..9557b2d3 100644 --- a/packages/core/src/engine/tokens.ts +++ b/packages/core/src/engine/tokens.ts @@ -1,36 +1,10 @@ -export type WordTok = { - readonly k: "w"; - readonly v: V; -}; -export type IdTok = { - readonly k: "id"; - readonly v: V; -}; -export type NumTok = { - readonly k: "n"; - readonly v: V; -}; -export type StrTok = { - readonly k: "s"; - readonly v: V; -}; -export type BitTok = StrTok & { - readonly bits: true; -}; -export type ParamTok = { - readonly k: "$"; - readonly v: N; -}; -export type OpTok = { - readonly k: "o"; - readonly v: V; -}; +/** A token of any dialect's lexer: its kind and value; dialects add fields. */ +export type Tok = { readonly k: string; readonly v: unknown }; +export type Toks = readonly Tok[]; -/** A MySQL user or system variable, `@name` or `@@name`. */ -type VarTok = { - readonly k: "v"; - readonly v: V; -}; +/** A parse result: the node and the index of the next token. */ +export type Ok = readonly [N, J]; -export type Tok = WordTok | IdTok | NumTok | StrTok | ParamTok | OpTok | VarTok; -export type Toks = readonly Tok[]; +export type AtEnd = T[I] extends Tok + ? false + : true; diff --git a/packages/core/src/engine/types.ts b/packages/core/src/engine/types.ts index d188a465..ec3f8938 100644 --- a/packages/core/src/engine/types.ts +++ b/packages/core/src/engine/types.ts @@ -1,4 +1,4 @@ -import type { Next } from "../sql/next.js"; +import type { Next } from "./next.js"; export type ColInfo = { readonly type: string; readonly nullable: boolean }; export type ColMap = { readonly [name: string]: ColInfo }; @@ -16,27 +16,11 @@ export type ScopeItem = { export type ScopeLevel = readonly ScopeItem[]; export type Scope = readonly ScopeLevel[]; -export type CteMap = { readonly [name: string]: ColMap }; - -export type Ctx = { - readonly db: unknown; - readonly scope: Scope; - readonly ctes: CteMap; - readonly grouped: boolean; -}; - -export type Info = { +export type Info = { readonly type: T; readonly nullable: N; }; -export type NullableMap = { - readonly [K in keyof M]: { - readonly type: M[K]["type"]; - readonly nullable: true; - }; -}; - export type ColsToMap = C extends readonly [ { readonly star: infer M extends ColMap }, ] @@ -96,28 +80,3 @@ type SlowMap = C extends readonly [ > : never : { readonly [K in keyof Acc]: Acc[K] }; - -export type Simplify = { [K in keyof T]: T[K] } & {}; - -export type MkCtx< - DB, - S extends Scope, - Ctes extends CteMap, - G extends boolean, -> = { - readonly db: DB; - readonly scope: S; - readonly ctes: Ctes; - readonly grouped: G; -}; - -/** The environment shared by query binding, projection and parameter inference. */ -export type QueryContext< - DB = unknown, - S extends Scope = Scope, - Ctes = CteMap, -> = { - readonly db: DB; - readonly scope: S; - readonly ctes: Ctes; -}; diff --git a/packages/core/src/engine/write.ts b/packages/core/src/engine/write.ts deleted file mode 100644 index 9150cc81..00000000 --- a/packages/core/src/engine/write.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { ColDesc, Table, View } from "../schema.js"; -import type { JsonDefaultValue, JsonValue } from "../sql/inline.js"; -import type { EngineTypes, TsParamTypeOf } from "./contract.js"; - -type IsTable = X extends Table ? true : false; - -export type TableName = - | { - [K in keyof DB & string]: IsTable extends true ? K : never; - }[keyof DB & string] - | { - [NS in keyof DB & string]: IsTable extends true - ? never - : DB[NS] extends View - ? never - : NsTables; - }[keyof DB & string]; -type NsTables = { - [K in keyof N & string]: IsTable extends true ? `${NS}.${K}` : never; -}[keyof N & string]; - -export type RelationName = { - [K in keyof DB & string]: DB[K] extends View
- ? K - : IsTable extends true - ? K - : never; -}[keyof DB & string]; - -export type TableOf = T extends keyof DB - ? DB[T] extends Table - ? DB[T] - : never - : T extends `${infer NS}.${infer N}` - ? NS extends keyof DB - ? N extends keyof DB[NS] - ? DB[NS][N] extends Table - ? DB[NS][N] - : never - : never - : never - : never; - -type ColIn< - E extends EngineTypes, - TM, - DB, - D extends ColDesc, -> = D["type"] extends "null" - ? null - : - | (D["nullable"] extends true - ? TsParamTypeOf | null - : TsParamTypeOf) - | (D["type"] extends E["json"] ? JsonValue | JsonDefaultValue : never); - -export type SetInput = - TableOf extends infer Tbl extends Table - ? { - readonly [K in Writable]?: ColIn | undefined; - } - : never; - -type Writable = { - [K in keyof Tbl]: Tbl[K] extends - | { readonly generated: true } - | { readonly hidden: true } - ? never - : K; -}[keyof Tbl]; - -type Required_ = { - [K in keyof Tbl]: Tbl[K] extends { - readonly nullable: false; - readonly hasDefault: false; - } - ? K - : never; -}[keyof Tbl]; - -export type InsertInput = - TableOf extends infer Tbl extends Table - ? Flat< - { readonly [K in Required_]: ColIn } & { - readonly [K in Exclude, Required_>]?: - | ColIn - | undefined; - } - > - : never; -type Flat = { [K in keyof O]: O[K] }; diff --git a/packages/core/src/family.ts b/packages/core/src/family.ts new file mode 100644 index 00000000..11fe88d2 --- /dev/null +++ b/packages/core/src/family.ts @@ -0,0 +1,42 @@ +import { mysqlPositional } from "./mysql/driver.js"; +import type { PositionalStatement } from "./runtime/compile.js"; +import { positional } from "./sqlite/driver.js"; + +/** + * The dialects whose statements a driver writes alike: MariaDB's are + * MySQL's. + */ +export type DialectFamily = "postgres" | "sqlite" | "mysql"; + +/** The family of the dialect named `name`; PostgreSQL's for any other. */ +export const dialectFamily = (name: string): DialectFamily => + name === "sqlite" + ? name + : name === "mysql" || name === "mariadb" + ? "mysql" + : "postgres"; + +/** + * A statement as a driver of each family takes it: PostgreSQL's keeps its + * `$n` parameters, SQLite's and MySQL's have `?` with the values in order. + * `map` converts each value for the driver; without it, PostgreSQL's are + * sent as they are, and SQLite's and MySQL's as sqliteValue and mysqlValue + * convert them. + */ +export const familyStatement: Readonly< + Record< + DialectFamily, + ( + text: string, + values: readonly V[], + map?: (value: V | undefined) => R, + ) => PositionalStatement + > +> = { + postgres: (text, values, map) => ({ + text, + values: map ? values.map(map) : [...values], + }), + sqlite: positional, + mysql: mysqlPositional, +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 29e5e109..c75c5482 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,15 +1,18 @@ export { type CreateSql, createSql } from "./create-sql.js"; export type { + AnyDialectBrand, ApplyDdlOf, + DIALECT, Dialect, + DialectBrand, DialectTypes, SqlOf, TransactionalSqlOf, ValidateDdlOf, } from "./dialect.js"; -export type { Empty, EmptyFragment, Optional } from "./empty.js"; +export type { Empty } from "./empty.js"; export type { AnyErr, ErrAt, ErrMsg, IsErr, SqlError } from "./errors.js"; -export type { SchemaOf } from "./migration/define.js"; +export type { SchemaOf } from "./migration/schema.js"; export type { IdentityPluginTypes, InputContext, @@ -38,7 +41,6 @@ export { type TEXT, type TextType, } from "./query.js"; - export type { Col, ColDesc, @@ -50,4 +52,5 @@ export type { TypeRegistry, View, } from "./schema.js"; +export type { EmptyFragment, Optional } from "./sql/api.js"; export type { SqlDriver, Transactional } from "./transaction.js"; diff --git a/packages/core/src/migration/connect.ts b/packages/core/src/migration/connect.ts deleted file mode 100644 index 31816420..00000000 --- a/packages/core/src/migration/connect.ts +++ /dev/null @@ -1,293 +0,0 @@ -import type { SQLInputValue, SQLOutputValue } from "node:sqlite"; -import { mysqlPositional, paramNumbers, sqliteValue } from "../adapter.js"; -import type { Executor, ExecutorResult } from "../query.js"; -import { runtimeImport } from "./node.js"; - -interface Connection { - readonly execute: Executor; - close(): Promise; -} - -// pg rejects a refused `localhost` connection (IPv4 and IPv6) with an -// AggregateError whose own message is empty. -const message = (cause: unknown): string => { - if (!(cause instanceof Error)) return String(cause); - if (cause.message) return cause.message; - if (cause instanceof AggregateError && cause.errors.length > 0) - return Array.from(new Set(cause.errors.map(message))).join("; "); - return "code" in cause && typeof cause.code === "string" - ? cause.code - : cause.name; -}; - -type Pg = typeof import("pg"); - -/** Whether `module` is the `pg` package, with its `Client`. */ -function isPg(module: unknown): module is Pg { - return ( - typeof module === "object" && - module !== null && - "default" in module && - typeof module.default === "object" && - module.default !== null && - "Client" in module.default && - typeof module.default.Client === "function" - ); -} - -/** One `pg` client for a migrator call. */ -export async function connectPostgres(url: string): Promise { - if (!URL.canParse(url) && !url.startsWith("/")) - throw new Error( - `typesql: invalid connection URL ${JSON.stringify(url)} (expected postgres://user:password@host:port/database)`, - ); - let Client: Pg["Client"]; - try { - ({ Client } = (await runtimeImport("pg", isPg)).default); - } catch (cause) { - throw new Error( - 'typesql: url connects with the "pg" package, which is not installed: npm install pg', - { cause }, - ); - } - const client = new Client({ connectionString: url }); - // The server can end the session between queries (a timeout, a failover, - // pg_terminate_backend). Without a listener, pg's 'error' event would end - // the process; the next query rejects with the error instead. - let lost: Error | undefined; - client.on("error", (error) => { - lost ??= error; - }); - try { - await client.connect(); - } catch (error) { - throw new Error( - `typesql: cannot connect to PostgreSQL: ${message(error)}`, - { - cause: error, - }, - ); - } - return { - execute: async (sql, values) => { - if (lost) throw lost; - const result = await client - .query(sql, [...values]) - .catch((cause: unknown) => { - // A session ended during a query rejects it with the server's - // FATAL error; later queries get pg's own "connection - // terminated" errors, so the server's message is the one to keep. - if ( - cause instanceof Error && - "severity" in cause && - (cause.severity === "FATAL" || cause.severity === "PANIC") - ) - lost ??= cause; - throw cause; - }); - return Array.isArray(result) - ? (result.at(-1) ?? { rows: [], rowCount: 0 }) - : result; - }, - close: async () => { - await client.end().catch((cause: unknown) => { - if (!lost) throw cause; - }); - }, - }; -} - -type Mysql2 = typeof import("mysql2/promise"); - -/** Whether `module` is `mysql2/promise`, with its `createConnection`. */ -function isMysql2(module: unknown): module is { readonly default: Mysql2 } { - return ( - typeof module === "object" && - module !== null && - "default" in module && - typeof module.default === "object" && - module.default !== null && - "createConnection" in module.default && - typeof module.default.createConnection === "function" - ); -} - -/** mysql2's ResultSetHeader: the result of a statement without rows. */ -interface Header { - readonly affectedRows?: number | bigint; -} - -/** What mysql2 resolves a statement to: its rows, a header, or one of each per statement of a script. */ -type Mysql2Result = readonly unknown[] | Header; - -const isHeader = (item: unknown): item is Header => - typeof item === "object" && item !== null && "affectedRows" in item; - -/** Rows of a statement that returns them, else the affected row count. */ -function mysqlResult(result: Mysql2Result): ExecutorResult { - if (Array.isArray(result)) { - // A multi-statement script's result lists its statements' results - // (rows or headers); the last one stands for the script. - const last = result.at(-1); - if (last !== undefined && result.every(isResult)) return mysqlResult(last); - return { rows: result, rowCount: result.length }; - } - const affected = "affectedRows" in result ? result.affectedRows : undefined; - return { rows: [], rowCount: affected === undefined ? 0 : Number(affected) }; -} - -/** Whether an element of a script's result list is a statement's result. */ -const isResult = (item: unknown): item is Mysql2Result => - Array.isArray(item) || isHeader(item); - -/** One `mysql2` connection for a migrator call, to MySQL or MariaDB. */ -export async function connectMysql(url: string): Promise { - if (!URL.canParse(url) || !/^(mysql|mariadb):$/.test(new URL(url).protocol)) - throw new Error( - `typesql: invalid connection URL ${JSON.stringify(url)} (expected mysql://user:password@host:port/database)`, - ); - let createConnection: Mysql2["createConnection"]; - try { - ({ createConnection } = ( - await runtimeImport("mysql2/promise", isMysql2) - ).default); - } catch (cause) { - throw new Error( - 'typesql: url connects with the "mysql2" package, which is not installed: npm install mysql2', - { cause }, - ); - } - let connection: Awaited>; - try { - // mysql2 reads a mariadb:// URL as a mysql:// one. Multiple statements - // let .raw() run a script, as it does on PostgreSQL. - connection = await createConnection({ - uri: `mysql${url.slice(url.indexOf(":"))}`, - multipleStatements: true, - }); - } catch (error) { - throw new Error(`typesql: cannot connect to MySQL: ${message(error)}`, { - cause: error, - }); - } - // The server can end the session between queries. Without a listener, - // mysql2's 'error' event would end the process; the next query rejects - // with the error instead. - let lost: Error | undefined; - connection.on("error", (error: Error) => { - lost ??= error; - }); - return { - execute: async (sql, values) => { - if (lost) throw lost; - const positional = mysqlPositional(sql, values); - const [result] = await connection.query( - positional.text, - positional.values, - ); - return mysqlResult(result); - }, - close: async () => { - await connection.end().catch((cause: unknown) => { - if (!lost) throw cause; - }); - }, - }; -} - -/** How long a statement waits for another connection's lock. */ -const BUSY_LIMIT = 30_000; - -/** - * Run a statement, waiting while another connection holds the database's - * lock. busy_timeout would wait inside SQLite and block the event loop, so a - * connection of the same process holding the lock could never release it. - */ -async function whenFree(step: () => T): Promise { - const start = Date.now(); - for (let delay = 5; ; delay = Math.min(delay * 2, 100)) { - try { - return step(); - } catch (error) { - // SQLITE_BUSY and its extended codes. - const busy = - typeof error === "object" && - error !== null && - "errcode" in error && - typeof error.errcode === "number" && - (error.errcode & 0xff) === 5; - if (!busy || Date.now() - start >= BUSY_LIMIT) throw error; - } - await new Promise((resolve) => setTimeout(resolve, delay)); - } -} - -/** Whether node:sqlite can bind `value` to a parameter. */ -function isSqliteInput(value: unknown): value is SQLInputValue { - return ( - value === null || - typeof value === "number" || - typeof value === "bigint" || - typeof value === "string" || - ArrayBuffer.isView(value) - ); -} - -/** Parameter `number` (1-based) of `values`, as node:sqlite binds it. */ -function bindable(values: readonly unknown[], number: number): SQLInputValue { - const value = values[number - 1]; - if (isSqliteInput(value)) return value; - throw new TypeError( - `typesql: SQLite cannot bind ${typeof value} to parameter ${number}`, - ); -} - -/** One `node:sqlite` database for a migrator call. */ -export async function connectSqlite(file: string): Promise { - const { DatabaseSync } = process.getBuiltinModule("node:sqlite"); - const db = new DatabaseSync(file); - return { - execute: async (sql, values) => { - let rest = sql; - let rows: Record[] = []; - let rowCount = 0; - while (rest.trim()) { - rest = rest.replace( - /^(?:\s+|;|--[^\r\n]*(?:\r?\n|$)|\/\*[\s\S]*?(?:\*\/|$))+/, - "", - ); - if (!rest) break; - const source = rest; - const statement = await whenFree(() => db.prepare(source)); - const consumed = statement.sourceSQL.length; - if (!consumed) break; - const numbers = paramNumbers(statement.sourceSQL); - const converted = values.map(sqliteValue); - const bindings: Record = {}; - for (const number of numbers) - bindings[`$${number}`] = bindable(converted, number); - const positional = () => - converted.map((_, index) => bindable(converted, index + 1)); - if (statement.columns().length) { - rows = await whenFree(() => - numbers.length - ? statement.all(bindings) - : statement.all(...positional()), - ); - rowCount = rows.length; - } else { - const result = await whenFree(() => - numbers.length - ? statement.run(bindings) - : statement.run(...positional()), - ); - rows = []; - rowCount = Number(result.changes); - } - rest = rest.slice(consumed); - } - return { rows, rowCount }; - }, - close: async () => db.close(), - }; -} diff --git a/packages/core/src/migration/context.ts b/packages/core/src/migration/context.ts index f1553162..a5ac51fd 100644 --- a/packages/core/src/migration/context.ts +++ b/packages/core/src/migration/context.ts @@ -1,16 +1,14 @@ -import { createDialectSql } from "../create-sql.js"; +import { createSqlApi } from "../create-sql.js"; import type { ApplyDdlOf, + Dialect, DialectTypes, - StrictNullChecksMessage, + NonStrict, ValidateDdlOf, } from "../dialect.js"; -import type { EngineTypes } from "../engine/contract.js"; import type { SqlError } from "../errors.js"; -import { mariadb, mysql } from "../mysql/dialect.js"; import type { MariaDbEngine, MySqlEngine } from "../mysql/engine/index.js"; -import type { MariaDb, MySql, Mysql2MariaDbTypeMap } from "../mysql/index.js"; -import { postgres } from "../postgres/dialect.js"; +import type { MariaDb, MySql } from "../mysql/index.js"; // The types come from the public entry points, so the declarations of a // project that exports a migration can name its schema's types (TS2883). import type { PgEngine } from "../postgres/engine/index.js"; @@ -18,18 +16,18 @@ import type { Postgres } from "../postgres/index.js"; import type { Executor } from "../query.js"; import type { SqlApi } from "../sql/api.js"; import type { BoxCallArgs, CallRet, GateCallCheck } from "../sql/call.js"; -import { sqlite } from "../sqlite/dialect.js"; +import type { EngineTypes } from "../sql/contract.js"; import type { SqliteEngine } from "../sqlite/engine/index.js"; import type { Sqlite } from "../sqlite/index.js"; +import type { MariaDbRows, MySqlRows } from "./dialects/mysql.js"; +import type { PostgresRows } from "./dialects/postgres.js"; +import type { SqliteRows } from "./dialects/sqlite.js"; +import type { MissingSchema } from "./schema.js"; -// Without strictNullChecks every query and DDL statement is this error, as -// for `sql` (see StrictSql). -type NonStrict = null extends string - ? SqlError - : never; - -// The strictNullChecks check sits inside the [DB, Q] inference: as the outer -// conditional, the editor's string completion in `.ddl("…")` took about 35s. +// Without strictNullChecks every query and DDL statement is NonStrict's +// error, as for `sql` (see StrictSql). The strictNullChecks check sits inside +// the [DB, Q] inference: as the outer conditional, the editor's string +// completion in `.ddl("…")` took about 35s. type GateDdl = [DB, Q] extends [ infer D, infer Text extends string, @@ -79,40 +77,39 @@ export interface MigrationContext< ): Promise>; } +// Each runs on the connection its migrator dialect opens, so its rows are +// typed as that connection returns them. export type PostgresMigration = MigrationContext< PgEngine, Postgres, - Base + Base, + PostgresRows >; export type SqliteMigration = MigrationContext< SqliteEngine, Sqlite, - Base + Base, + SqliteRows >; export type MySqlMigration = MigrationContext< MySqlEngine, MySql, - Base + Base, + MySqlRows >; -/** MariaDB through mysql2, as the migrator runs it: mysql2's type map. */ export type MariaDbMigration = MigrationContext< MariaDbEngine, MariaDb, Base, - Mysql2MariaDbTypeMap + MariaDbRows >; -export type MigrationDialect = "postgres" | "sqlite" | "mysql" | "mariadb"; - export type AnyMigrationContext = | PostgresMigration | SqliteMigration | MySqlMigration | MariaDbMigration; -declare const MISSING_SCHEMA: unique symbol; -/** The schema of a migration whose `up` doesn't return its context. */ -export type MissingSchema = { readonly [MISSING_SCHEMA]: true }; type MissingSchemaError = SqlError< "the previous migration's up() does not return its migration context, so its schema is unknown; end it with `return db`", 0 @@ -120,35 +117,20 @@ type MissingSchemaError = SqlError< /** * Internal factory: execution is bound to the runner's active transaction. - * `step` runs each `.ddl()` and `.raw()` statement; the runner splits a - * SQLite script into its statements there. + * `script` runs each `.ddl()` and `.raw()` script. */ export function createMigrationContext( - dialect: MigrationDialect, + dialect: Dialect, execute: Executor, - step: (kind: "ddl" | "raw", statement: string) => Promise = async ( - _, - statement, - ) => { - await execute(statement, []); - }, + script: (text: string) => Promise, ): AnyMigrationContext { - // Avoid comparing the recursive SQL APIs when choosing a runtime dialect. - let sql: object; - if (dialect === "postgres") sql = createDialectSql(postgres, execute); - else if (dialect === "sqlite") sql = createDialectSql(sqlite, execute); - else if (dialect === "mysql") sql = createDialectSql(mysql, execute); - else if (dialect === "mariadb") sql = createDialectSql(mariadb, execute); - else throw new Error(`typesql: unsupported migration dialect: ${dialect}`); - const next = (kind: "ddl" | "raw") => async (statement: string) => { - await step(kind, statement); - return createMigrationContext(dialect, execute, step); + // An object: comparing the dialects' recursive SQL APIs is too deep. + const sql: object = createSqlApi(dialect, execute); + const next = async (text: string) => { + await script(text); + return createMigrationContext(dialect, execute, script); }; - Object.assign(sql, { - ddl: next("ddl"), - add: next("ddl"), - raw: next("raw"), - }); + Object.assign(sql, { ddl: next, add: next, raw: next }); if (!isMigrationContext(sql)) throw new Error("typesql: cannot create the migration context"); return sql; diff --git a/packages/core/src/migration/define.ts b/packages/core/src/migration/define.ts index 308090bc..7968f2eb 100644 --- a/packages/core/src/migration/define.ts +++ b/packages/core/src/migration/define.ts @@ -1,12 +1,11 @@ import type { AnyMigrationContext, MariaDbMigration, - MigrationDialect, - MissingSchema, MySqlMigration, PostgresMigration, SqliteMigration, } from "./context.js"; +import { DIALECTS, type DialectName, isDialectName } from "./dialects/index.js"; /** * A migration: `up` takes the schema the previous migration leaves and @@ -29,32 +28,12 @@ export interface MigrationSteps { readonly down: (db: NoInfer) => Promise; } -type ContextSchema = C extends { readonly schema: infer Schema } - ? Schema - : MissingSchema; - -/** Type-only: the schema a migrator or its `latest()` result leaves. */ -export declare const SCHEMA: unique symbol; - -/** - * The schema after a migration: `SchemaOf`. It also takes a - * migrator or its `latest()` result (the schema after the last migration), - * a 3.x migration's `up` function, or a migration context. - */ -export type SchemaOf = M extends { readonly [SCHEMA]: infer Schema } - ? Schema - : M extends { readonly up: (db: never) => Promise } - ? ContextSchema - : M extends (db: never) => Promise - ? ContextSchema - : ContextSchema; - /** Where a migration starts; the runner checks the chain with it. */ const MIGRATION_FROM: unique symbol = Symbol.for("typesql.migration.from"); /** What `migration()` starts from, as its overloads take it. */ type From = - | Dialect + | DialectName | { readonly up: (db: never) => Promise<{ readonly schema: unknown }> } | ((db: never) => Promise<{ readonly schema: unknown }>); @@ -95,27 +74,13 @@ export function migration( from: From, steps: MigrationSteps, ): Migration { - if ( - !DIALECTS.includes(from) && - typeof from !== "function" && - (from === null || - typeof from !== "object" || - !("up" in from) || - typeof from.up !== "function" || - !("down" in from) || - typeof from.down !== "function") - ) + if (!isDialectName(from) && typeof from !== "function" && !isMigration(from)) throw new TypeError( - 'typesql: migration() starts from "postgres", "sqlite", "mysql", "mariadb" or the previous migration', + `typesql: migration() starts from ${Object.keys(DIALECTS) + .map((name) => JSON.stringify(name)) + .join(", ")} or the previous migration`, ); - if ( - steps === null || - typeof steps !== "object" || - !("up" in steps) || - typeof steps.up !== "function" || - !("down" in steps) || - typeof steps.down !== "function" - ) + if (!isMigration(steps)) throw new TypeError( "typesql: migration() takes { up, down } with async functions", ); @@ -130,11 +95,6 @@ export function migration( return Object.freeze(defined); } -export type Dialect = MigrationDialect; - -/** The dialects a migration can start from, as the runtime check reads them. */ -const DIALECTS: readonly unknown[] = ["postgres", "sqlite", "mysql", "mariadb"]; - /** * A migration's `up` or `down` as the runner calls it: with the context of * the migrator's dialect. The runner awaits what it returns. @@ -177,21 +137,8 @@ export function toEntry(name: string, migration: Runnable): Entry { }; } -/** Whether `error` carries the `code` Node and the database drivers set. */ -export function hasCode( - error: unknown, - code: string, -): error is { readonly code: string } { - return ( - typeof error === "object" && - error !== null && - "code" in error && - error.code === code - ); -} - /** Whether `value` has a migration's `up` and `down` functions. */ -export function isMigration(value: unknown): value is Runnable { +export function isMigration(value: V): value is V & Runnable { return ( value !== null && typeof value === "object" && diff --git a/packages/core/src/migration/dialects/index.ts b/packages/core/src/migration/dialects/index.ts new file mode 100644 index 00000000..b2f3cc28 --- /dev/null +++ b/packages/core/src/migration/dialects/index.ts @@ -0,0 +1,23 @@ +import { mariadb, mysql } from "./mysql.js"; +import { postgres } from "./postgres.js"; +import type { MigratorDialect } from "./session.js"; +import { sqlite } from "./sqlite.js"; + +/** The dialects migrations run on: every list of them is this record's keys. */ +export const DIALECTS = { + postgres, + sqlite, + mysql, + mariadb, +} as const satisfies Readonly>; + +/** The name of a dialect migrations run on. */ +export type DialectName = keyof typeof DIALECTS; + +export const isDialectName = (value: unknown): value is DialectName => + typeof value === "string" && Object.hasOwn(DIALECTS, value); + +/** "postgres, sqlite, mysql or mariadb". */ +export const DIALECT_LIST = Object.keys(DIALECTS) + .join(", ") + .replace(/, (?=[^,]*$)/, " or "); diff --git a/packages/core/src/migration/dialects/mysql.ts b/packages/core/src/migration/dialects/mysql.ts new file mode 100644 index 00000000..8175f9db --- /dev/null +++ b/packages/core/src/migration/dialects/mysql.ts @@ -0,0 +1,192 @@ +import { + mariadb as mariadbSql, + mysql as mysqlSql, +} from "../../mysql/dialect.js"; +import type { + DefaultTypeMap, + Mysql2MariaDbTypeMap, +} from "../../mysql/type-map/index.js"; +import type { Executor } from "../../query.js"; +import { + first, + type MigratorDialect, + quoted, + rows, + type Session, + SPLIT, + sha256, + tryRun, +} from "./session.js"; + +/** MySQL's ER_SP_DOES_NOT_EXIST: `RELEASE SAVEPOINT` of a savepoint that isn't there. */ +const noSavepoint = (cause: unknown): boolean => + (typeof cause === "object" && + cause !== null && + "errno" in cause && + cause.errno === 1305) || + /SAVEPOINT \S+ does not exist/i.test( + cause instanceof Error ? cause.message : String(cause), + ); + +/** + * MySQL and MariaDB commit implicitly before and after each DDL statement, + * so the history table is created before any migration's transaction, and a + * migration's transaction may have ended before it fails. A session-level + * lock serializes the commands instead of a transaction's. + */ +async function open( + execute: Executor, + tableName: string, + transaction: boolean, +): Promise { + const location = first( + await rows( + execute, + "SELECT DATABASE() AS db, @@autocommit AS autocommit, CONNECTION_ID() AS id", + ), + ); + if ( + location === undefined || + !("db" in location) || + typeof location.db !== "string" || + !("autocommit" in location) || + !("id" in location) + ) + throw new Error( + "typesql: no database selected: put the database in the connection URL", + ); + // MySQL and MariaDB count identifier length in characters. + if (Array.from(tableName).length > 64) + throw new Error( + "typesql: the migration table name exceeds MySQL's 64-character identifier limit", + ); + const table = `${quoted(location.db, "`")}.${quoted(tableName, "`")}`; + const createTable = `CREATE TABLE IF NOT EXISTS ${table} (name VARCHAR(255) NOT NULL PRIMARY KEY, hash VARCHAR(64) NOT NULL, applied_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)) DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin`; + const connectionId = String(location.id); + /** The session's autocommit before the migrator turned it off. */ + const autocommit = Number(location.autocommit) === 0 ? 0 : 1; + /** The session lock held for the command, released by close(). */ + let sessionLock: string | undefined; + /** + * Whether this call reaches the connection the command started on. A + * BEGIN inside a transaction would commit it there, so the connection's + * id is compared instead. + */ + const sameConnection = async (): Promise => { + const row = first(await rows(execute, "SELECT CONNECTION_ID() AS id")); + return row !== undefined && "id" in row && String(row.id) === connectionId; + }; + if (transaction) { + // A session-level named lock, released by close() or when the + // connection ends. Lock names are limited to 64 characters. MariaDB + // rejects a negative (infinite) timeout, so wait for up to a year. + const digest = await sha256(JSON.stringify([location.db, tableName])); + const name = `typesql_migrate_${Array.from( + new Uint8Array(digest).slice(0, 20), + (byte) => byte.toString(16).padStart(2, "0"), + ).join("")}`; + // The lock belongs to a session: taken on a pooled connection that + // the next call doesn't get, it would stay held. An executor that + // changes connection is refused before taking it, and the lock is + // given back when it still landed elsewhere. + if (!(await sameConnection())) throw new Error(SPLIT); + const locked = first( + await rows( + execute, + "SELECT GET_LOCK($1, 31536000) AS locked, CONNECTION_ID() AS id", + [name], + ), + ); + if ( + locked === undefined || + !("locked" in locked) || + Number(locked.locked) !== 1 + ) + throw new Error("typesql: cannot acquire the migration lock"); + if (!("id" in locked) || String(locked.id) !== connectionId) { + await tryRun(execute, "SELECT RELEASE_LOCK($1)", [name]); + throw new Error(SPLIT); + } + sessionLock = name; + // With autocommit off, the statements after a migration's last DDL + // statement commit together with its history record instead of one + // by one. + try { + await execute("SET autocommit = 0", []); + await execute(createTable, []); + } catch (error) { + await tryRun(execute, "SELECT RELEASE_LOCK($1)", [sessionLock]); + throw error; + } + } else await execute(createTable, []); + return { + table, + createTable, + read: `SELECT name, DATE_FORMAT(applied_at, '%Y-%m-%dT%H:%i:%s.%fZ') AS at FROM ${table} ORDER BY applied_at, name`, + insert: `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', GREATEST(UTC_TIMESTAMP(6), COALESCE(MAX(applied_at) + INTERVAL 1 MICROSECOND, UTC_TIMESTAMP(6))) FROM ${table}`, + created: true, + async begin() { + await execute("START TRANSACTION", []); + await execute("SAVEPOINT typesql_migration", []); + }, + inTransaction: sameConnection, + /** + * Whether the transaction begin() started is still the open one. Its + * savepoint is dropped when the transaction ends: by a statement that + * reached another connection, or by the implicit commit around a DDL + * statement, after which (with autocommit off) the next statement + * started a new transaction that ROLLBACK ends. The probe recreates + * the savepoint, so it can be asked again. + */ + async whole() { + try { + await execute("RELEASE SAVEPOINT typesql_migration", []); + } catch (cause) { + if (noSavepoint(cause)) return false; + throw cause; + } + await execute("SAVEPOINT typesql_migration", []); + return true; + }, + // The connection may be gone; the server then released the lock. + async close() { + if (sessionLock === undefined) return; + await tryRun(execute, `SET autocommit = ${autocommit}`); + await tryRun(execute, "SELECT RELEASE_LOCK($1)", [sessionLock]); + }, + }; +} + +const scan = { + // In MySQL and MariaDB, a stored program's body may hold `;`-separated + // statements inside BEGIN … END. + programs: new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]), + notAtomicBlocks: true, + xa: true, +} as const; + +/** + * The rows of a migration's `db`: what connect()'s mysql2 connection + * returns (the mariadb connector stands in only without mysql2). + */ +export type MySqlRows = DefaultTypeMap; +export type MariaDbRows = Mysql2MariaDbTypeMap; + +export const mysql: MigratorDialect = { + server: "MySQL", + sql: mysqlSql, + target: "url", + connect: async (url) => + (await import("../../mysql/driver.js")).connectMysql(url), + scan, + split: false, + open, + schema: "mysql", +}; + +/** MariaDB runs MySQL's migrator; the dialect names the server. */ +export const mariadb: MigratorDialect = { + ...mysql, + server: "MariaDB", + sql: mariadbSql, +}; diff --git a/packages/core/src/migration/dialects/postgres.ts b/packages/core/src/migration/dialects/postgres.ts new file mode 100644 index 00000000..f24d404b --- /dev/null +++ b/packages/core/src/migration/dialects/postgres.ts @@ -0,0 +1,109 @@ +import { hasCode } from "../../command.js"; +import { postgres as sql } from "../../postgres/dialect.js"; +import type { NodePostgresTypeMap } from "../../postgres/type-map/index.js"; +import type { Executor } from "../../query.js"; +import { + first, + type MigratorDialect, + quoted, + rows, + type Session, + sha256, +} from "./session.js"; + +/** The `xid` column of the first row: a transaction id's text. */ +const xidOf = (records: readonly unknown[]): string | undefined => { + const row = first(records); + return row && "xid" in row && typeof row.xid === "string" + ? row.xid + : undefined; +}; + +async function open( + execute: Executor, + tableName: string, + transaction: boolean, +): Promise { + const location = first( + await rows( + execute, + "SELECT current_schema() AS schema, current_database() AS database, current_setting('max_identifier_length')::integer AS max_identifier_length", + ), + ); + if ( + location === undefined || + !("schema" in location) || + typeof location.schema !== "string" || + !("database" in location) || + typeof location.database !== "string" || + !("max_identifier_length" in location) || + typeof location.max_identifier_length !== "number" + ) + throw new Error( + "typesql: cannot determine the PostgreSQL migration schema", + ); + if ( + new TextEncoder().encode(tableName).length > location.max_identifier_length + ) + throw new Error( + `typesql: the migration table name exceeds PostgreSQL's ${location.max_identifier_length}-byte identifier limit`, + ); + const table = `${quoted(location.schema, '"')}.${quoted(tableName, '"')}`; + const lock = transaction + ? new DataView( + await sha256( + JSON.stringify([location.database, location.schema, tableName]), + ), + ) + .getBigInt64(0) + .toString() + : undefined; + let xid: string | undefined; + return { + table, + createTable: `CREATE TABLE IF NOT EXISTS ${table} (name TEXT PRIMARY KEY NOT NULL, hash TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP)`, + read: `SELECT name, to_char(applied_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS at FROM ${table} ORDER BY applied_at, name`, + insert: `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', greatest(clock_timestamp(), max(applied_at) + interval '1 microsecond') FROM ${table}`, + created: false, + async begin() { + await execute("BEGIN", []); + // The lock ends with the transaction, so it can't stay held on a + // pooled connection. Its void result stays in FROM: Prisma can't + // read a void column. + xid = xidOf( + await rows( + execute, + "SELECT txid_current()::text AS xid FROM pg_advisory_xact_lock($1::bigint)", + [lock], + ), + ); + }, + inTransaction: async () => + xidOf( + await rows(execute, "SELECT txid_current_if_assigned()::text AS xid"), + ) === xid, + // After a failed statement PostgreSQL refuses every one but ROLLBACK + // (in_failed_sql_transaction, from pg, postgres.js or Prisma): the + // transaction is still there. + aborted: (cause) => + hasCode(cause, "25P02") || + /current transaction is aborted/.test( + cause instanceof Error ? cause.message : String(cause), + ), + }; +} + +/** The rows of a migration's `db`: what connect()'s `pg` client returns. */ +export type PostgresRows = NodePostgresTypeMap; + +export const postgres: MigratorDialect = { + server: "PostgreSQL", + sql, + target: "url", + connect: async (url) => + (await import("../../postgres/driver.js")).connectPostgres(url), + scan: { programs: new Set(["FUNCTION", "PROCEDURE"]), atomicBodies: true }, + split: false, + open, + schema: "postgres", +}; diff --git a/packages/core/src/migration/dialects/session.ts b/packages/core/src/migration/dialects/session.ts new file mode 100644 index 00000000..b6519308 --- /dev/null +++ b/packages/core/src/migration/dialects/session.ts @@ -0,0 +1,129 @@ +// What a migration dialect is made of, and the helpers its history uses. + +import type { SchemaDialect } from "../../codegen.js"; +import type { Connection } from "../../connect.js"; +import type { Dialect } from "../../dialect.js"; +import type { Executor, ExecutorResult } from "../../query.js"; +import { ident } from "../../runtime/compile.js"; + +/** + * The history table and the migrations' transactions on the connection of + * one migrator command. + */ +export interface Session { + /** The history table's qualified name. */ + readonly table: string; + /** Creates the history table if it doesn't exist. */ + readonly createTable: string; + /** Reads the history, `name` and `at`, in the order it was applied. */ + readonly read: string; + /** Records migration `$1` after every earlier one (applied_at increases). */ + readonly insert: string; + /** Whether open() created the table, outside any transaction. */ + readonly created: boolean; + /** Starts a migration's transaction, with its lock. */ + begin(): Promise; + /** Whether this call runs in the transaction begin() started. */ + inTransaction(): Promise; + /** + * Whether rolling back now undoes all of the migration. MySQL commits + * implicitly before and after each DDL statement. + */ + whole?(): Promise; + /** Whether a failed inTransaction() means the transaction is there, but aborted. */ + aborted?(cause: unknown): boolean; + /** Releases what the command held on the connection. */ + close?(): Promise; +} + +/** + * The statements of a dialect that end or nest unlike the others, as the + * migrator reads them. Its strings, names and comments are its `sql`'s + * lexis. + */ +interface ScanRules { + /** + * The objects whose CREATE statement has a `BEGIN … END` body that holds + * statements, which a `;` inside doesn't end. + */ + readonly programs: ReadonlySet; + /** + * Only `BEGIN ATOMIC` opens a body, and every `END` closes one or a CASE: + * PostgreSQL's bodies don't nest, and hold no `END IF` or the like. + */ + readonly atomicBodies?: boolean; + /** `BEGIN NOT ATOMIC … END` is a block of statements that runs now (MariaDB). */ + readonly notAtomicBlocks?: boolean; + /** `XA …` statements control a transaction (MySQL, MariaDB). */ + readonly xa?: boolean; +} + +export interface MigratorDialect { + /** The server, as messages name it. */ + readonly server: string; + /** The `sql` a migration's `db` is. */ + readonly sql: Dialect; + /** What the migrator connects with: a `url`, or a `db` file. */ + readonly target: "url" | "db"; + /** One connection for a migrator command. */ + connect(target: string): Promise>; + readonly scan: ScanRules; + /** + * The statements of a `.ddl()` or `.raw()` script, sent one at a time: + * SQLite's prepared statements run only the first one. + */ + readonly split: boolean; + /** The history on one connection; `transaction` is the config's. */ + open( + execute: Executor, + tableName: string, + transaction: boolean, + ): Promise; + /** The `Col` the generated schema module imports. */ + readonly schema: SchemaDialect; +} + +/** A migration's statements reached more than one connection. */ +export const SPLIT = + "typesql: execute didn't keep a transaction across its calls, so migrations can't run in one: give it one dedicated connection, not a pool (Prisma, a pooled Kysely and libsql's client send each call to any connection), or set transaction: false"; + +/** A quoted name that the history table's may be part of. */ +export function quoted(name: string, quote: string): string { + if (name.includes("\0")) + throw new Error("typesql: the migration table name must not contain NUL"); + return ident(quote, [name]); +} + +/** The rows of a statement. */ +export async function rows( + execute: Executor, + text: string, + values: readonly unknown[] = [], +): Promise { + const result: readonly unknown[] | ExecutorResult = await execute( + text, + values, + ); + return "rows" in result ? result.rows : result; +} + +/** The first row, when it is an object. */ +export const first = (records: readonly unknown[]): object | undefined => { + const [row] = records; + return typeof row === "object" && row !== null ? row : undefined; +}; + +/** SHA-256 of `text`, with Web Crypto (Node, Bun, Deno and Workers). */ +export const sha256 = (text: string): Promise => + crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); + +/** Runs `statement`, ignoring a failure: what it cleans up may be gone. */ +export const tryRun = ( + execute: Executor, + statement: string, + values: readonly unknown[] = [], +) => + Promise.resolve(execute(statement, values)).then( + () => {}, + () => {}, + ); diff --git a/packages/core/src/migration/dialects/sqlite.ts b/packages/core/src/migration/dialects/sqlite.ts new file mode 100644 index 00000000..05ccce24 --- /dev/null +++ b/packages/core/src/migration/dialects/sqlite.ts @@ -0,0 +1,56 @@ +import type { Executor } from "../../query.js"; +import { sqlite as sql } from "../../sqlite/dialect.js"; +import type { DefaultTypeMap } from "../../sqlite/type-map/index.js"; +import { + type MigratorDialect, + quoted, + type Session, + tryRun, +} from "./session.js"; + +/** SQLite's error for a BEGIN inside a transaction, as every driver reports it. */ +const NESTED = /cannot start a transaction within a transaction/i; + +async function open(execute: Executor, tableName: string): Promise { + const table = quoted(tableName, '"'); + const now = "strftime('%Y-%m-%d %H:%M:%f', 'now')"; + return { + table, + createTable: `CREATE TABLE IF NOT EXISTS ${table} (name TEXT PRIMARY KEY NOT NULL, hash TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`, + read: `SELECT name, applied_at AS at FROM ${table} ORDER BY applied_at, name`, + insert: `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', CASE WHEN max(applied_at) IS NULL OR ${now} > max(applied_at) THEN ${now} ELSE strftime('%Y-%m-%d %H:%M:%f', max(applied_at), '+0.001 seconds') END FROM ${table}`, + created: false, + async begin() { + await execute("BEGIN IMMEDIATE", []); + }, + // A BEGIN fails inside a transaction. One that succeeds started a + // transaction where there was none; end it. Any other failure is the + // connection's, and the caller's to see. + async inTransaction() { + try { + await execute("BEGIN", []); + } catch (cause) { + if (NESTED.test(cause instanceof Error ? cause.message : String(cause))) + return true; + throw cause; + } + await tryRun(execute, "ROLLBACK"); + return false; + }, + }; +} + +/** The rows of a migration's `db`: what connect()'s `node:sqlite` database returns. */ +export type SqliteRows = DefaultTypeMap; + +export const sqlite: MigratorDialect = { + server: "SQLite", + sql, + target: "db", + connect: async (file) => + (await import("../../sqlite/driver.js")).connectSqlite(file), + scan: { programs: new Set(["TRIGGER"]) }, + split: true, + open, + schema: "sqlite", +}; diff --git a/packages/core/src/migration/files.ts b/packages/core/src/migration/files.ts index cb082387..07822d9d 100644 --- a/packages/core/src/migration/files.ts +++ b/packages/core/src/migration/files.ts @@ -1,11 +1,25 @@ +import { hasCode } from "../command.js"; +import { type Entry, isMigration, isStep, toEntry } from "./define.js"; import { - type Dialect, - type Entry, - hasCode, - isMigration, - isStep, - toEntry, -} from "./define.js"; + DIALECT_LIST, + DIALECTS, + type DialectName, + isDialectName, +} from "./dialects/index.js"; +import { + createMigrator, + isMigrator, + MEMORY, + type Migrator, + type MigratorConfig, + PATH_KEYS, + type PathKey, + SCHEMA_FILE, +} from "./migrator.js"; + +/** What a project that can't import @typesql/core is told to do. */ +export const ADD_CORE = + "add @typesql/core to the project's dependencies (npm install @typesql/core, or pnpm add @typesql/core)"; /** The migration files of a directory, in the order they run. */ export async function migrationFiles(migrationsDir: string): Promise { @@ -32,39 +46,38 @@ export async function migrationFiles(migrationsDir: string): Promise { .sort(); } -/** File URLs of the migration directories whose imports are mapped. */ -const directories = new Set(); +let mapped = false; /** - * Migrations import each other as `./name.js`, which TypeScript resolves to - * `./name.ts`. Node doesn't, so a relative `.js` import that a migration - * makes and that doesn't exist loads the `.ts` file beside it instead. + * A TypeScript project's files import each other as `./name.js`, which + * TypeScript resolves to `./name.ts`; Node doesn't. Once mapped, such an + * import from a `.ts` file outside node_modules (a migration, or a config + * that imports its migrations) loads the `.ts` file when there is no `.js` + * one. */ -function mapMigrationImports(directory: string): void { +function mapTypeScriptImports(): void { // Bun and Deno load the `.ts` file themselves, and have no registerHooks. const { registerHooks } = process.getBuiltinModule("node:module"); - if (typeof registerHooks !== "function") return; - if (directories.size === 0) - registerHooks({ - resolve(specifier, context, nextResolve) { - try { - return nextResolve(specifier, context); - } catch (error) { - const parent = context.parentURL; - if ( - !hasCode(error, "ERR_MODULE_NOT_FOUND") || - !/^\.\.?\/.*\.js$/.test(specifier) || - parent === undefined || - !directories.has(new URL(".", parent).href) - ) - throw error; - return nextResolve(`${specifier.slice(0, -3)}.ts`, context); - } - }, - }); - directories.add( - process.getBuiltinModule("node:url").pathToFileURL(`${directory}/`).href, - ); + if (mapped || typeof registerHooks !== "function") return; + mapped = true; + registerHooks({ + resolve(specifier, context, nextResolve) { + try { + return nextResolve(specifier, context); + } catch (error) { + const parent = context.parentURL; + if ( + !hasCode(error, "ERR_MODULE_NOT_FOUND") || + !/^\.\.?\/.*\.[cm]?js$/.test(specifier) || + parent === undefined || + !/^file:.*\.[cm]?ts$/.test(parent) || + parent.includes("/node_modules/") + ) + throw error; + return nextResolve(specifier.replace(/js$/, "ts"), context); + } + }, + }); } /** What the runner reads of a migration file's exports. */ @@ -84,7 +97,7 @@ export async function loadMigrations(migrationsDir: string): Promise { const { pathToFileURL } = process.getBuiltinModule("node:url"); const directory = resolve(migrationsDir); const names = await migrationFiles(directory); - mapMigrationImports(directory); + mapTypeScriptImports(); const loaded: Entry[] = []; for (const name of names) { let module: MigrationModule; @@ -98,7 +111,7 @@ export async function loadMigrations(migrationsDir: string): Promise { String(error).includes("'@typesql/core") ) throw new Error( - `${"message" in error ? error.message : undefined}; add @typesql/core to the project's dependencies (npm install @typesql/core, or pnpm add @typesql/core)`, + `${"message" in error ? error.message : undefined}; ${ADD_CORE}`, { cause: error }, ); throw error; @@ -131,7 +144,7 @@ export async function loadMigrations(migrationsDir: string): Promise { export async function makeMigration( migrationsDir: string, name: string, - dialect: Dialect, + dialect: DialectName, ): Promise { if (!name.trim() || /[\\/\p{Cc}]/u.test(name)) throw new Error( @@ -183,3 +196,136 @@ export async function makeMigration( await writeFile(file, source, { flag: "wx" }); return file; } + +/** The config file `typesql migrate` reads without `--config`. */ +const CONFIG_FILE = "typesql.config.ts"; + +/** What `typesql migrate` sets of the config: paths relative to `cwd`. */ +export interface MigrateOverrides { + readonly dialect?: string | undefined; + readonly url?: string | undefined; + readonly db?: string | undefined; + readonly migrationsDir?: string | undefined; + readonly schemaFile?: string | undefined; + readonly generate?: boolean | undefined; +} + +/** + * The migrator of `typesql migrate`: the one `config` (default: + * typesql.config.ts in `cwd`, if there is one) default-exports, with its + * relative paths resolved beside it, and `overrides` replacing its config. + * Without a config file, the migrations are in ./migrations and the schema + * file is db.ts. A server dialect without a connection connects to + * DATABASE_URL. + */ +export async function loadMigrator(options: { + readonly cwd: string; + readonly env: Readonly>; + readonly config?: string | undefined; + readonly overrides: MigrateOverrides; +}): Promise { + const { resolve } = process.getBuiltinModule("node:path"); + const { cwd, env, overrides } = options; + const file = resolve(cwd, options.config ?? CONFIG_FILE); + const configured = await loadConfig(file, options.config !== undefined); + const dialect = overrides.dialect ?? configured?.config.dialect; + if (!isDialectName(dialect)) + throw new Error( + configured === undefined + ? `no ${CONFIG_FILE}: pass --dialect ${DIALECT_LIST}, or pass --config` + : `unsupported dialect: ${String(dialect)} (expected ${DIALECT_LIST})`, + ); + const flags: Partial = { dialect }; + for (const [key, value] of Object.entries(overrides)) + if (value !== undefined) Object.assign(flags, { [key]: value }); + const config: Partial = withPaths(flags, (path) => + resolve(cwd, path), + ); + const connected = (c: Partial | undefined) => + c?.url !== undefined || c?.db !== undefined || c?.execute !== undefined; + const url = + DIALECTS[dialect].target === "url" && + !connected(config) && + !connected(configured?.config) + ? env.DATABASE_URL || undefined + : undefined; + return configured === undefined + ? createMigrator({ + migrationsDir: resolve(cwd, "migrations"), + schemaFile: resolve(cwd, SCHEMA_FILE), + ...config, + ...(url && { url }), + dialect, + }) + : configured.with({ ...config, ...(url && { url }) }); +} + +/** The migrator a config file default-exports, its paths resolved beside it. */ +async function loadConfig( + file: string, + explicit: boolean, +): Promise { + const { stat } = process.getBuiltinModule("node:fs/promises"); + const { dirname, resolve } = process.getBuiltinModule("node:path"); + try { + await stat(file); + } catch (error) { + if (!explicit && hasCode(error, "ENOENT")) return undefined; + throw error; + } + mapTypeScriptImports(); + const module: { readonly default?: unknown } = await import( + process.getBuiltinModule("node:url").pathToFileURL(file).href + ); + const migrator = module.default; + if (!isMigrator(migrator)) + throw new Error( + `${file} must default-export a migrator: export default createMigrator({ … }) from @typesql/core/migration`, + ); + const base = dirname(file); + const { config } = migrator; + return migrator.with( + withPaths( + config.schemaFile === undefined && config.migrationsDir !== undefined + ? { ...config, schemaFile: SCHEMA_FILE } + : config, + (path) => resolve(base, path), + ), + ); +} + +/** + * The file paths of `config`, made absolute with `resolve`. A SQLite + * `:memory:` database is not a file. + */ +function withPaths>>( + config: C, + resolve: (path: string) => string, +): C { + const paths: Partial> = {}; + for (const key of PATH_KEYS) { + const value = config[key]; + if (value !== undefined && value !== MEMORY) paths[key] = resolve(value); + } + return { ...config, ...paths }; +} + +/** + * Why the migration file just created in `directory` can't run: it imports + * @typesql/core/migration, which a package manager with strict layouts + * (pnpm) doesn't expose there when only @typesql/cli is installed. + */ +export async function missingCore( + directory: string, +): Promise { + const { stat } = process.getBuiltinModule("node:fs/promises"); + const { dirname, join } = process.getBuiltinModule("node:path"); + for (let dir = directory; ; dir = dirname(dir)) { + try { + await stat(join(dir, "node_modules", "@typesql/core", "package.json")); + return undefined; + } catch {} + if (dirname(dir) === dir) + return `@typesql/core/migration cannot be found from ${directory}, so the migration can't run and its types are unchecked; ${ADD_CORE}`; + } +} diff --git a/packages/core/src/migration/generate.ts b/packages/core/src/migration/generate.ts index 3a324fd6..b7823999 100644 --- a/packages/core/src/migration/generate.ts +++ b/packages/core/src/migration/generate.ts @@ -1,16 +1,18 @@ import type { Checker, Type } from "typescript/sync"; -import type { Dialect } from "./define.js"; -import { migrationFiles } from "./files.js"; +import { + propertyKey as key, + RESERVED_KEYS, + type SchemaDialect, + schemaWriter, +} from "../codegen.js"; +import { DIALECTS, type DialectName } from "./dialects/index.js"; +import { ADD_CORE, migrationFiles } from "./files.js"; type Sync = typeof import("typescript/sync"); type Project = NonNullable< ReturnType["getDefaultProjectForFile"]> >; -const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/; -const key = (name: string): string => - IDENT.test(name) ? name : JSON.stringify(name); -const NOT_RELATIONS = new Set(["$types", "$functions"]); const notComputed = (what: string) => `TypeScript could not compute ${what}; run tsc on the migrations to see why`; @@ -20,7 +22,7 @@ const notComputed = (what: string) => */ export async function writeSchema(options: { readonly migrationsDir: string; - readonly dialect: Dialect; + readonly dialect: DialectName; readonly schemaFile: string; readonly point: number | "last"; }): Promise { @@ -32,18 +34,17 @@ export async function writeSchema(options: { // The schema after the point's last migration, or before the first one. const after = files[count - 1]; const name = after ?? files[0]; - const header = [ - `// Generated by \`typesql migrate generate\`${after === undefined ? "" : ` from ${after}`}. Do not edit.`, - ]; + const header = `Generated by \`typesql migrate generate\`${after === undefined ? "" : ` from ${after}`}. Do not edit.`; + const module = DIALECTS[options.dialect].schema; const source = name === undefined - ? emit(header, options.dialect, new Set(), []) + ? schemaWriter(module).module({ header, members: [] }) : generateSchema( migrationsDir, name, after !== undefined, header, - options.dialect, + module, ); const out = resolve(options.schemaFile); await mkdir(dirname(out), { recursive: true }); @@ -81,8 +82,8 @@ function generateSchema( migrationsDir: string, name: string, after: boolean, - header: readonly string[], - dialect: Dialect, + header: string, + module: SchemaDialect, ): string { const { dirname, resolve } = process.getBuiltinModule("node:path"); const file = resolve(migrationsDir, name); @@ -105,8 +106,7 @@ function generateSchema( // nesting is one file deep however long the history is. for (const dependency of reader.dependencies(file)) reader.warm(dependency); const schema = reader.schema(file, after); - const { used, schema: write } = writer(ts, project.checker, dialect, fail); - const source = emit(header, dialect, used, write(schema)); + const source = writer(ts, project.checker, module, fail)(schema, header); // A last guard: symbol keys such as SqlError's never belong in a schema. if (source.includes("__@")) fail("its schema type contains TypeScript-internal symbol keys"); @@ -116,30 +116,6 @@ function generateSchema( } } -function emit( - header: readonly string[], - dialect: Dialect, - used: ReadonlySet, - lines: readonly string[], -): string { - const helpers = ["Col", "Enum", "Typed", "View"].filter((h) => used.has(h)); - // MariaDB schemas use MySQL's helpers. - const module = dialect === "mariadb" ? "mysql" : dialect; - return [ - ...header, - ...(helpers.length > 0 - ? [ - `import type { ${helpers.join(", ")} } from "@typesql/core/${module}";`, - ] - : []), - "", - ...(lines.length > 0 - ? ["export interface DB {", ...lines, "}"] - : ["export interface DB {}"]), - "", - ].join("\n"); -} - type Specifier = Parameters< Project["program"]["getResolvedModuleFromModuleSpecifier"] >[0]; @@ -201,7 +177,7 @@ function migrationReader( .map((i) => i.specifier); if (missing.length === 0) return undefined; const hint = missing.some((m) => m.startsWith("@typesql/core")) - ? "; add @typesql/core to the project's dependencies (npm install @typesql/core, or pnpm add @typesql/core)" + ? `; ${ADD_CORE}` : ""; return `${what} is untyped because TypeScript cannot find ${missing.map((m) => JSON.stringify(m)).join(", ")} from ${dirname(file)}${hint}`; }; @@ -277,10 +253,10 @@ function migrationReader( function writer( ts: Sync, checker: Checker, - dialect: Dialect, + dialect: SchemaDialect, fail: (reason: string) => never, ) { - const used = new Set(); + const write = schemaWriter(dialect); const print = (t: Type): string => checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation); const literal = (t: Type | undefined) => @@ -331,17 +307,21 @@ function writer( ); }; - const mysql = dialect === "mysql" || dialect === "mariadb"; - /** MySQL's `Typed` (`ENUM(…)` columns among them): its base and `T`. */ + /** + * A `Typed` (MySQL's `ENUM(…)` columns among them, and + * PostgreSQL's `Json`): its base and `T`. + */ const shaped = (t: Type): { base: string; ts: string } | undefined => { - if (!mysql || !t.isIntersectionType()) return undefined; + if (!t.isIntersectionType()) return undefined; const shape = checker.getTypeOfPropertyOfType(t, "shape"); const value = shape && checker.getTypeOfPropertyOfType(shape, "v"); const base = t.getTypes().find((part) => part.isStringLiteralType()); if (!value || literal(checker.getTypeOfPropertyOfType(shape, "k")) !== "ts") return undefined; const name = literal(base); - return typeof name === "string" + // PostgreSQL types the values of JSON columns only. + return typeof name === "string" && + (dialect !== "postgres" || name === "json" || name === "jsonb") ? { base: name, ts: print(value) } : undefined; }; @@ -353,37 +333,19 @@ function writer( const type = literal(typeOf); const typed = typeOf && shaped(typeOf); if (typeof type !== "string" && !typed) return print(t); - const generated = flag(t, "generated") === true; - const hidden = flag(t, "hidden") === true; - const autoIncrement = flag(t, "autoIncrement") === true; const position = flag(t, "i"); - // Flags that imply hasDefault in the dialect's Col. - const impliesDefault = - dialect === "sqlite" - ? generated || hidden - : mysql && (generated || autoIncrement); - const flags = [ - flag(t, "nullable") === true ? "nullable: true" : "", - flag(t, "hasDefault") === true && !impliesDefault - ? "hasDefault: true" - : "", - typeof position === "number" ? `position: ${position}` : "", - flag(t, "rowid") === true ? "rowid: true" : "", - autoIncrement ? "autoIncrement: true" : "", - generated ? "generated: true" : "", - hidden ? "hidden: true" : "", - ].filter(Boolean); - const options = flags.length > 0 ? `, { ${flags.join("; ")} }` : ""; - if (typed?.base === "enum") { - used.add("Enum"); - return `Enum<${typed.ts}${options}>`; - } - used.add("Col"); - if (typed) { - used.add("Typed"); - return `Col${options}>`; - } - return `Col<${JSON.stringify(type)}${options}>`; + const flags = { + nullable: flag(t, "nullable") === true, + hasDefault: flag(t, "hasDefault") === true, + position: typeof position === "number" ? position : undefined, + rowid: flag(t, "rowid") === true, + autoIncrement: flag(t, "autoIncrement") === true, + generated: flag(t, "generated") === true, + hidden: flag(t, "hidden") === true, + }; + if (typed?.base === "enum") return write.enum(typed.ts, flags); + if (typed) return write.typed(typed.base, typed.ts, flags); + return write.col(String(type), flags); }; const relation = (t: Type, indent: string, at: string): string => { @@ -400,9 +362,7 @@ function writer( `${indent}\t${key(c.name)}: ${column(c.type, `column ${at.replace(/^relation /, "")}.${c.name}`)};`, ); const object = `{\n${body.join("\n")}\n${indent}}`; - if (!view) return object; - used.add("View"); - return `View<${object}>`; + return view ? write.view(object) : object; }; const schema = (db: Type): string[] => { @@ -410,7 +370,7 @@ function writer( for (const { name, type } of props(check(db, "the schema"), "the schema")) { const at = `relation ${name}`; check(type, at); - if (NOT_RELATIONS.has(name)) + if (RESERVED_KEYS.includes(name)) lines.push(`\t${key(name)}: ${print(type)};`); else if (viewOf(type) || isTable(type, at)) lines.push(`\t${key(name)}: ${relation(type, "\t", at)};`); @@ -427,5 +387,6 @@ function writer( return lines; }; - return { used, schema }; + return (db: Type, header: string): string => + write.module({ header, members: schema(db) }); } diff --git a/packages/core/src/migration/index.ts b/packages/core/src/migration/index.ts index 8236144f..7df5c8b1 100644 --- a/packages/core/src/migration/index.ts +++ b/packages/core/src/migration/index.ts @@ -5,12 +5,8 @@ export type { PostgresMigration, SqliteMigration, } from "./context.js"; -export { - type Migration, - type MigrationSteps, - migration, - type SchemaOf, -} from "./define.js"; +export { type Migration, type MigrationSteps, migration } from "./define.js"; +export { loadMigrator, type MigrateOverrides, missingCore } from "./files.js"; export { createMigrator, type DownResult, @@ -22,3 +18,4 @@ export { type MigratorConfig, type UpResult, } from "./migrator.js"; +export type { SchemaOf } from "./schema.js"; diff --git a/packages/core/src/migration/migrator.ts b/packages/core/src/migration/migrator.ts index 9de4bf46..29bfadc3 100644 --- a/packages/core/src/migration/migrator.ts +++ b/packages/core/src/migration/migrator.ts @@ -1,16 +1,27 @@ -import type { Executor, ExecutorResult } from "../query.js"; +import { errorMessage } from "../command.js"; +import type { Executor } from "../query.js"; import { createMigrationContext } from "./context.js"; import { - type Dialect, type Entry, - hasCode, isMigration, type Migration, - type SCHEMA, - type SchemaOf, type Step, toEntry, } from "./define.js"; +import { + DIALECT_LIST, + DIALECTS, + type DialectName, + isDialectName, +} from "./dialects/index.js"; +import { + type MigratorDialect, + rows, + SPLIT, + tryRun, +} from "./dialects/session.js"; +import type { SCHEMA, SchemaOf } from "./schema.js"; +import { assertMigrationSql, splitStatements } from "./statements.js"; // `down` takes what `up` returns, so only `any` accepts every migration. // biome-ignore lint/suspicious/noExplicitAny: see above. @@ -62,7 +73,7 @@ type ConfigOf = Omit & { }; export interface MigratorConfig { - readonly dialect: Dialect; + readonly dialect: DialectName; /** * A PostgreSQL, MySQL or MariaDB connection URL; the migrator connects * with `pg` or `mysql2`. @@ -228,6 +239,19 @@ interface Row { const CONNECTIONS = ["url", "db", "execute"] as const; const SOURCES = ["migrationsDir", "migrations"] as const; +/** + * The config keys that name a file or directory: relative to the working + * directory, or, in a config file, to the file. + */ +export const PATH_KEYS = ["migrationsDir", "schemaFile", "db"] as const; +export type PathKey = (typeof PATH_KEYS)[number]; + +/** The schema file of a migrations directory, by default. */ +export const SCHEMA_FILE = "db.ts"; + +/** SQLite's in-memory database: a `db` that is not a file. */ +export const MEMORY = ":memory:"; + /** * The Node-only parts load only when a directory, `url`, `db` or * `generate()` needs them. Bundlers include them: they reach Node's @@ -235,19 +259,12 @@ const SOURCES = ["migrationsDir", "migrations"] as const; * runtime, so a Worker bundle has no Node imports to resolve. */ const loadFiles = () => import("./files.js"); -const loadConnect = () => import("./connect.js"); const loadGenerate = () => import("./generate.js"); -const DIALECTS: readonly Dialect[] = ["postgres", "sqlite", "mysql", "mariadb"]; - -/** MySQL and MariaDB run the same migrator; the dialect names the server. */ -const isMySql = (dialect: Dialect): boolean => - dialect === "mysql" || dialect === "mariadb"; - function validate(config: MigratorConfig): void { - if (!DIALECTS.includes(config.dialect)) + if (!isDialectName(config.dialect)) throw new Error( - `typesql: unsupported migration dialect: ${String(config.dialect)} (expected postgres, sqlite, mysql or mariadb)`, + `typesql: unsupported migration dialect: ${String(config.dialect)} (expected ${DIALECT_LIST})`, ); const sources = SOURCES.filter((key) => config[key] !== undefined); if (sources.length !== 1) @@ -263,21 +280,16 @@ function validate(config: MigratorConfig): void { throw new Error( `typesql: set one of url, db and execute, not ${connections.join(" and ")}`, ); - if (config.url !== undefined && config.dialect === "sqlite") + const { target } = DIALECTS[config.dialect]; + if (config.url !== undefined && target !== "url") throw new Error( - "typesql: url connects to PostgreSQL, MySQL or MariaDB; use db for SQLite", + `typesql: url connects to ${servers("url")}; use db for ${servers("db")}`, ); - if (config.db !== undefined && config.dialect !== "sqlite") + if (config.db !== undefined && target !== "db") throw new Error( - "typesql: db opens a SQLite file; use url for PostgreSQL, MySQL or MariaDB", + `typesql: db opens a ${servers("db")} file; use url for ${servers("url")}`, ); - for (const key of [ - "url", - "db", - "migrationsDir", - "migrationTable", - "schemaFile", - ] as const) { + for (const key of [...PATH_KEYS, "url", "migrationTable"] as const) { const value = config[key]; if (value !== undefined && (typeof value !== "string" || value === "")) throw new Error(`typesql: ${key} must be a non-empty string`); @@ -291,6 +303,14 @@ function validate(config: MigratorConfig): void { } } +/** The servers a connection target reaches: "PostgreSQL, MySQL or MariaDB". */ +const servers = (target: "url" | "db"): string => + Object.values(DIALECTS) + .filter((d) => d.target === target) + .map((d) => d.server) + .join(", ") + .replace(/, (?=[^,]*$)/, " or "); + /** Create a migrator. It connects only when a method needs the database. */ export function createMigrator< Schema = Unset, @@ -314,7 +334,7 @@ export function createMigrator(config: MigratorConfig): RuntimeMigrator { return writeSchema({ migrationsDir: dir, dialect: settled.dialect, - schemaFile: settled.schemaFile ?? "db.ts", + schemaFile: settled.schemaFile ?? SCHEMA_FILE, point, }); }; @@ -449,7 +469,7 @@ const describe = (entries: readonly Entry[], index: number): string => `migration ${index + 1} (${entries[index]?.name ?? "missing"})`; /** Each migration must start where the one before it ends. */ -function checkChain(dialect: Dialect, entries: readonly Entry[]): void { +function checkChain(dialect: DialectName, entries: readonly Entry[]): void { entries.forEach((entry, index) => { const { from } = entry; if (from === undefined) return; @@ -482,34 +502,6 @@ function checkChain(dialect: Dialect, entries: readonly Entry[]): void { }); } -function identifier(name: string, quote: '"' | "`" = '"'): string { - if (name.includes("\0")) - throw new Error("typesql: the migration table name must not contain NUL"); - return `${quote}${name.replaceAll(quote, quote + quote)}${quote}`; -} - -/** Whether an executor returned its rows rather than a result. */ -const isRowList = ( - result: readonly unknown[] | ExecutorResult, -): result is readonly unknown[] => Array.isArray(result); - -async function rows( - execute: Executor, - text: string, - values: readonly unknown[] = [], -): Promise { - const result = await execute(text, values); - return isRowList(result) ? result : result.rows; -} - -/** The `xid` column of the first row. */ -const firstXid = (records: readonly unknown[]) => { - const [row] = records; - return typeof row === "object" && row !== null && "xid" in row - ? row.xid - : undefined; -}; - function readHistory(records: readonly unknown[]): Row[] { return records.map((record) => { if ( @@ -557,20 +549,6 @@ function checkHistory( type Command = "latest" | "up" | "down" | "status"; -/** SHA-256 of `text`, with Web Crypto (Node, Bun, Deno and Workers). */ -const sha256 = (text: string): Promise => - crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); - -/** PostgreSQL's in_failed_sql_transaction, from pg, postgres.js or Prisma. */ -const aborted = (cause: unknown): boolean => - hasCode(cause, "25P02") || - /current transaction is aborted/.test( - cause instanceof Error ? cause.message : String(cause), - ); - -const SPLIT = - "typesql: execute didn't keep a transaction across its calls, so migrations can't run in one: give it one dedicated connection, not a pool (Prisma, a pooled Kysely and libsql's client send each call to any connection), or set transaction: false"; - /** A migration failed; createMigrator adds the schema file to it. */ class Failed extends Error { readonly migration: string; @@ -594,36 +572,6 @@ class Failed extends Error { } } -// pg rejects a refused `localhost` connection (IPv4 and IPv6) with an -// AggregateError whose own message is empty. -const text = (cause: unknown): string => { - if (!(cause instanceof Error)) return String(cause); - const message = cause.message - ? cause.message - : cause instanceof AggregateError && cause.errors.length > 0 - ? Array.from(new Set(cause.errors.map(text))).join("; ") - : "code" in cause && typeof cause.code === "string" - ? cause.code - : cause.name; - return message.replace(/^typesql: /, ""); -}; - -/** The first row, when it is an object. */ -const first = (records: readonly unknown[]): object | undefined => { - const [row] = records; - return typeof row === "object" && row !== null ? row : undefined; -}; - -/** MySQL's ER_SP_DOES_NOT_EXIST: `RELEASE SAVEPOINT` of a savepoint that isn't there. */ -const noSavepoint = (cause: unknown): boolean => - (typeof cause === "object" && - cause !== null && - "errno" in cause && - cause.errno === 1305) || - /SAVEPOINT \S+ does not exist/i.test( - cause instanceof Error ? cause.message : String(cause), - ); - /** * Why a failed migration was not rolled back: with `transaction: false`, * because its statements ran outside the migration's transaction (an @@ -635,284 +583,65 @@ type NotRolledBack = "none" | "split" | "implicit"; /** The history table on one connection, and the transactions around it. */ async function openHistory( execute: Executor, - dialect: Dialect, + dialect: MigratorDialect, tableName: string, transaction: boolean, ) { - const postgres = dialect === "postgres"; - const mysql = isMySql(dialect); - let table = identifier(tableName); - const createTable = (): string => - mysql - ? `CREATE TABLE IF NOT EXISTS ${table} (name VARCHAR(255) NOT NULL PRIMARY KEY, hash VARCHAR(64) NOT NULL, applied_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)) DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin` - : `CREATE TABLE IF NOT EXISTS ${table} (name TEXT PRIMARY KEY NOT NULL, hash TEXT NOT NULL, applied_at ${postgres ? "TIMESTAMPTZ" : "TEXT"} NOT NULL DEFAULT CURRENT_TIMESTAMP)`; - let lock: string | undefined; - /** MySQL: the connection's id, to tell whether a call reaches the same one. */ - let connectionId: unknown; - /** MySQL: the session's autocommit before the migrator turned it off. */ - let autocommit: 0 | 1 = 1; - /** MySQL: the session lock held for the command, released by close(). */ - let sessionLock: string | undefined; - /** - * MySQL: whether this call reaches the connection the command started on. - * A BEGIN inside a transaction would commit it there, so the connection's - * id is compared instead. - */ - const sameConnection = async (): Promise => { - const row = first(await rows(execute, "SELECT CONNECTION_ID() AS id")); - return row !== undefined && "id" in row && String(row.id) === connectionId; - }; - if (mysql) { - const location = first( - await rows( - execute, - "SELECT DATABASE() AS db, @@autocommit AS autocommit, CONNECTION_ID() AS id", - ), - ); - if ( - location === undefined || - !("db" in location) || - typeof location.db !== "string" || - !("autocommit" in location) || - !("id" in location) - ) - throw new Error( - "typesql: no database selected: put the database in the connection URL", - ); - // MySQL and MariaDB count identifier length in characters. - if (Array.from(tableName).length > 64) - throw new Error( - "typesql: the migration table name exceeds MySQL's 64-character identifier limit", - ); - table = `${identifier(location.db, "`")}.${identifier(tableName, "`")}`; - connectionId = String(location.id); - autocommit = Number(location.autocommit) === 0 ? 0 : 1; - if (transaction) { - // A session-level named lock, released by close() or when the - // connection ends. Lock names are limited to 64 characters. MariaDB - // rejects a negative (infinite) timeout, so wait for up to a year. - const digest = await sha256(JSON.stringify([location.db, tableName])); - const name = `typesql_migrate_${Array.from( - new Uint8Array(digest).slice(0, 20), - (byte) => byte.toString(16).padStart(2, "0"), - ).join("")}`; - // The lock belongs to a session: taken on a pooled connection that - // the next call doesn't get, it would stay held. An executor that - // changes connection is refused before taking it, and the lock is - // given back when it still landed elsewhere. - if (!(await sameConnection())) throw new Error(SPLIT); - const locked = first( - await rows( - execute, - "SELECT GET_LOCK($1, 31536000) AS locked, CONNECTION_ID() AS id", - [name], - ), - ); - if ( - locked === undefined || - !("locked" in locked) || - Number(locked.locked) !== 1 - ) - throw new Error("typesql: cannot acquire the migration lock"); - if (!("id" in locked) || String(locked.id) !== connectionId) { - await Promise.resolve(execute("SELECT RELEASE_LOCK($1)", [name])).catch( - () => {}, - ); - throw new Error(SPLIT); - } - sessionLock = name; - // DDL commits implicitly, so the history table is created before - // any migration's transaction. With autocommit off, the statements - // after a migration's last DDL statement commit together with its - // history record instead of one by one. - try { - await execute("SET autocommit = 0", []); - await execute(createTable(), []); - } catch (error) { - await Promise.resolve( - execute("SELECT RELEASE_LOCK($1)", [sessionLock]), - ).catch(() => {}); - throw error; - } - } else await execute(createTable(), []); - } - if (postgres) { - const [location] = await rows( - execute, - "SELECT current_schema() AS schema, current_database() AS database, current_setting('max_identifier_length')::integer AS max_identifier_length", - ); - if ( - typeof location !== "object" || - location === null || - !("schema" in location) || - typeof location.schema !== "string" || - !("database" in location) || - typeof location.database !== "string" || - !("max_identifier_length" in location) || - typeof location.max_identifier_length !== "number" - ) - throw new Error( - "typesql: cannot determine the PostgreSQL migration schema", - ); - if ( - new TextEncoder().encode(tableName).length > - location.max_identifier_length - ) - throw new Error( - `typesql: the migration table name exceeds PostgreSQL's ${location.max_identifier_length}-byte identifier limit`, - ); - table = `${identifier(location.schema)}.${table}`; - if (transaction) { - const digest = await sha256( - JSON.stringify([location.database, location.schema, tableName]), - ); - lock = new DataView(digest).getBigInt64(0).toString(); - } - } - /** The transaction's id, on PostgreSQL. */ - let xid: unknown; - /** - * MySQL: whether the transaction begin() started is still the open one. - * Its savepoint is dropped when the transaction ends: by a statement that - * reached another connection, or by the implicit commit around a DDL - * statement, after which (with autocommit off) the next statement - * started a new transaction that ROLLBACK ends. The probe recreates the - * savepoint, so it can be asked again. - */ - const sinceBegin = async (): Promise => { - try { - await execute("RELEASE SAVEPOINT typesql_migration", []); - } catch (cause) { - if (noSavepoint(cause)) return false; - throw cause; - } - await execute("SAVEPOINT typesql_migration", []); - return true; - }; - /** - * Whether this call runs in the transaction begin() started. An executor - * on a pool, or a client that runs each call on a connection of its own, - * such as libsql's, sends each statement wherever it likes. - */ - const inTransaction = async (): Promise => { - if (postgres) { - return ( - firstXid( - await rows(execute, "SELECT txid_current_if_assigned()::text AS xid"), - ) === xid - ); - } - if (mysql) return sameConnection(); - // A BEGIN fails inside a transaction. One that succeeds started a - // transaction where there was none; end it. - const began = await execute("BEGIN", []).then( - () => true, - () => false, - ); - if (began) await Promise.resolve(execute("ROLLBACK", [])).catch(() => {}); - return !began; - }; + const session = await dialect.open(execute, tableName, transaction); + const whole = () => session.whole?.() ?? true; return { async begin(): Promise { if (!transaction) return; - await execute( - postgres ? "BEGIN" : mysql ? "START TRANSACTION" : "BEGIN IMMEDIATE", - [], - ); + await session.begin(); try { - // The lock ends with the transaction, so it can't stay held on - // a pooled connection. Its void result stays in FROM: Prisma - // can't read a void column. - if (postgres) - xid = firstXid( - await rows( - execute, - "SELECT txid_current()::text AS xid FROM pg_advisory_xact_lock($1::bigint)", - [lock], - ), - ); - if (mysql) await execute("SAVEPOINT typesql_migration", []); - if (!(await inTransaction()) || (mysql && !(await sinceBegin()))) + if (!(await session.inTransaction()) || !(await whole())) throw new Error(SPLIT); } catch (error) { // Leave no transaction, and with it the lock, open on the // caller's connection, where its later writes would be lost. - await Promise.resolve(execute("ROLLBACK", [])).catch(() => {}); + await tryRun(execute, "ROLLBACK"); throw error; } }, async commit(): Promise { if (!transaction) return; - if (!(await inTransaction())) throw new Error(SPLIT); + if (!(await session.inTransaction())) throw new Error(SPLIT); await execute("COMMIT", []); }, /** * Roll back after a failure, and tell whether it was rolled back: not * without a transaction, or when its statements didn't run in it. On * MySQL, a migration that ran a DDL statement is rolled back only - * after its last one: MySQL commits implicitly before and after DDL. + * after its last one. */ async rollback(): Promise { if (!transaction) return "none"; - const intact = await inTransaction().catch((cause: unknown) => { - // After a failed statement PostgreSQL refuses every one but - // ROLLBACK: the transaction is still there. - if (postgres && aborted(cause)) return true; + const intact = await session.inTransaction().catch((cause: unknown) => { + if (session.aborted?.(cause)) return true; throw cause; }); if (intact) { - const whole = mysql ? await sinceBegin() : true; + const all = await whole(); await execute("ROLLBACK", []); - return whole ? true : "implicit"; + return all ? true : "implicit"; } // It may still reach the connection that holds the transaction. - await Promise.resolve(execute("ROLLBACK", [])).catch(() => {}); + await tryRun(execute, "ROLLBACK"); return "split"; }, /** Create the table if needed, and read it in the order it was applied. */ async read(): Promise { - // On MySQL the table was created before the transaction: DDL - // would commit it. - if (!mysql) await execute(createTable(), []); - return readHistory( - await rows( - execute, - postgres - ? `SELECT name, to_char(applied_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS at FROM ${table} ORDER BY applied_at, name` - : mysql - ? `SELECT name, DATE_FORMAT(applied_at, '%Y-%m-%dT%H:%i:%s.%fZ') AS at FROM ${table} ORDER BY applied_at, name` - : `SELECT name, applied_at AS at FROM ${table} ORDER BY applied_at, name`, - ), - ); + if (!session.created) await execute(session.createTable, []); + return readHistory(await rows(execute, session.read)); }, - /** - * Record a migration, after every earlier one (applied_at strictly - * increases). `hash` is unused; 3.x stored a hash of the file there. - */ + /** Record a migration. `hash` is unused; 3.x stored a hash of the file there. */ async insert(name: string): Promise { - await execute( - postgres - ? `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', greatest(clock_timestamp(), max(applied_at) + interval '1 microsecond') FROM ${table}` - : mysql - ? `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', GREATEST(UTC_TIMESTAMP(6), COALESCE(MAX(applied_at) + INTERVAL 1 MICROSECOND, UTC_TIMESTAMP(6))) FROM ${table}` - : `INSERT INTO ${table} (name, hash, applied_at) SELECT $1, '', CASE WHEN max(applied_at) IS NULL OR strftime('%Y-%m-%d %H:%M:%f', 'now') > max(applied_at) THEN strftime('%Y-%m-%d %H:%M:%f', 'now') ELSE strftime('%Y-%m-%d %H:%M:%f', max(applied_at), '+0.001 seconds') END FROM ${table}`, - [name], - ); + await execute(session.insert, [name]); }, async remove(name: string): Promise { - await execute(`DELETE FROM ${table} WHERE name = $1`, [name]); - }, - /** Release what the command held on the connection: MySQL's lock and autocommit. */ - async close(): Promise { - if (sessionLock === undefined) return; - // The connection may be gone; the server then released the lock. - try { - await execute(`SET autocommit = ${autocommit}`, []); - } catch {} - try { - await execute("SELECT RELEASE_LOCK($1)", [sessionLock]); - } catch {} + await execute(`DELETE FROM ${session.table} WHERE name = $1`, [name]); }, + close: async (): Promise => session.close?.(), }; } @@ -920,12 +649,12 @@ async function run( config: MigratorConfig, command: Command, ): Promise<{ names: string[]; applied: number; status: MigrationStatus[] }> { - const { dialect } = config; + const dialect = DIALECTS[config.dialect]; const entries = await load(config); - checkChain(dialect, entries); + checkChain(config.dialect, entries); const transaction = config.transaction ?? true; - const connection = await connect(config); + const connection = await connect(config, dialect); const names: string[] = []; let applied = 0; let status: MigrationStatus[] = []; @@ -938,7 +667,6 @@ async function run( transaction, ); for (;;) { - // The migration this iteration runs, once it is known. let current: { name: string; label: string } | undefined; await history.begin(); try { @@ -978,7 +706,7 @@ async function run( applied += command === "down" ? -1 : 1; } catch (error) { let failure = error; - let message = text(error); + let message = errorMessage(error); let rolledBack = false; try { const outcome = await history.rollback(); @@ -986,7 +714,7 @@ async function run( if (!rolledBack && current !== undefined) message += outcome === "implicit" - ? `; ${SERVERS[dialect]} commits implicitly before and after each DDL statement, so its statements up to its last DDL statement stay applied and only the ones after it were rolled back` + ? `; ${dialect.server} commits implicitly before and after each DDL statement, so its statements up to its last DDL statement stay applied and only the ones after it were rolled back` : outcome === "split" ? "; its statements didn't all run in the migration's transaction, so it was not rolled back and may be partly applied" : "; without a transaction it was not rolled back, and may be partly applied"; @@ -997,7 +725,7 @@ async function run( [error, rollbackError], "typesql: the migration failed and rolling it back failed too", ); - message += `; rolling it back failed too: ${text(rollbackError)}`; + message += `; rolling it back failed too: ${errorMessage(rollbackError)}`; } } if (current === undefined) throw failure; @@ -1021,36 +749,20 @@ async function run( async function connect( config: MigratorConfig, + dialect: MigratorDialect, ): Promise<{ execute: Executor; close(): Promise }> { if (config.execute !== undefined) return { execute: config.execute, close: async () => {} }; - if (config.url !== undefined) { - const { connectPostgres, connectMysql } = await loadConnect(); - return isMySql(config.dialect) - ? connectMysql(config.url) - : connectPostgres(config.url); - } - if (config.db !== undefined) { - const { connectSqlite } = await loadConnect(); - return connectSqlite(config.db); - } + const target = config[dialect.target]; + if (target !== undefined) return dialect.connect(target); throw new Error( - config.dialect === "sqlite" - ? "typesql: set db (or execute) to run SQLite migrations" - : `typesql: set url (or execute) to run ${SERVERS[config.dialect]} migrations`, + `typesql: set ${dialect.target} (or execute) to run ${dialect.server} migrations`, ); } -const SERVERS: Readonly> = { - postgres: "PostgreSQL", - sqlite: "SQLite", - mysql: "MySQL", - mariadb: "MariaDB", -}; - /** Run one `up` or `down` on a guarded connection inside the runner's transaction. */ async function runStep( - dialect: Dialect, + dialect: MigratorDialect, execute: Executor, step: Step, ): Promise { @@ -1078,270 +790,15 @@ async function runStep( }, ); try { - // A prepared SQLite statement runs only the first statement of a - // script (node:sqlite, bun:sqlite, D1), so the runner sends each one. await step( - createMigrationContext( - dialect, - guarded, - dialect === "sqlite" - ? async (_kind, script) => { - for (const statement of sqliteStatements(script)) - await guarded(statement, []); - } - : undefined, - ), + createMigrationContext(dialect.sql, guarded, async (script) => { + for (const statement of dialect.split + ? splitStatements(script, dialect) + : [script]) + await guarded(statement, []); + }), ); } finally { active = false; } } - -/** - * Calls `on` for each token of `sql` outside whitespace and comments: an - * uppercased word, ";", or "" for a quoted string, identifier or any other - * character. `start` tells whether it begins a statement. - */ -function scanSql( - sql: string, - dialect: Dialect, - on: (token: string, end: number, start: boolean) => void, -): void { - const mysql = isMySql(dialect); - let start = true; - for (let i = 0; i < sql.length; ) { - const char = sql.charAt(i); - if (/\s/.test(char)) { - i++; - continue; - } - // MySQL's `--` comment needs whitespace after it (`--x` is subtraction - // of a negative), and `#` opens one too. - if ( - (sql.startsWith("--", i) && - (!mysql || i + 2 >= sql.length || /\s/.test(sql.charAt(i + 2)))) || - (mysql && char === "#") - ) { - const end = /[\r\n]/.exec(sql.slice(i + 1)); - i = end === null ? sql.length : i + 1 + end.index + 1; - continue; - } - if (sql.startsWith("/*", i)) { - let depth = 1; - i += 2; - while (i < sql.length && depth > 0) { - if (sql.startsWith("/*", i) && dialect === "postgres") { - depth++; - i += 2; - } else if (sql.startsWith("*/", i)) { - depth--; - i += 2; - } else i++; - } - continue; - } - let token = ""; - if ( - char === "'" || - char === '"' || - ((dialect === "sqlite" || mysql) && char === "`") || - (dialect === "sqlite" && char === "[") - ) { - const end = char === "[" ? "]" : char; - // MySQL strings take backslash escapes; its quoted identifiers don't. - const escapes = - (mysql && char !== "`") || - (dialect === "postgres" && - char === "'" && - /[eE]/.test(sql[i - 1] ?? "") && - !/[\w$]/.test(sql[i - 2] ?? "")); - i++; - while (i < sql.length) { - if (escapes && sql[i] === "\\") i += 2; - else if (sql[i] === end) { - if (sql[i + 1] === end && char !== "[") i += 2; - else { - i++; - break; - } - } else i++; - } - } else if (char === ";") { - token = ";"; - i++; - } else { - const rest = sql.slice(i); - const tag = - char === "$" && dialect === "postgres" - ? /^\$(?:[a-zA-Z_][a-zA-Z_0-9]*)?\$/.exec(rest)?.[0] - : undefined; - const word = /^[a-zA-Z_][a-zA-Z_0-9$]*/.exec(rest)?.[0]; - if (tag) { - const end = sql.indexOf(tag, i + tag.length); - i = end < 0 ? sql.length : end + tag.length; - } else if (word) { - token = word.toUpperCase(); - i += word.length; - } else i++; - } - on(token, i, start); - start = token === ";"; - } -} - -/** - * The statements of a SQLite script, split at semicolons outside quotes, - * comments and a trigger's BEGIN … END body. - */ -function sqliteStatements(sql: string): string[] { - const statements: string[] = []; - const words: string[] = []; - let begin = 0; - let content = false; - let trigger = false; - scanSql(sql, "sqlite", (token, end, start) => { - if (token === ";") { - // A trigger's body statements end in semicolons too. - if (trigger) return; - if (content) statements.push(sql.slice(begin, end)); - begin = end; - words.length = 0; - content = false; - return; - } - content = true; - if (token === "") return; - if (trigger && start && token === "END") trigger = false; - if (words.length < 4) words.push(token); - if (token === "TRIGGER" && opensTrigger(words)) trigger = true; - }); - if (content) statements.push(sql.slice(begin)); - return statements; -} - -function opensTrigger(words: readonly string[]): boolean { - return /^CREATE (?:TEMP |TEMPORARY )?TRIGGER$/.test(words.join(" ")); -} - -// The kinds of object CREATE makes. In MySQL and MariaDB, a stored program's -// body may hold `;`-separated statements inside BEGIN … END. -const CREATE_KINDS = new Set([ - "TABLE", - "VIEW", - "INDEX", - "DATABASE", - "SCHEMA", - "SEQUENCE", - "USER", - "ROLE", - "SERVER", - "TABLESPACE", - "PACKAGE", - "PROCEDURE", - "FUNCTION", - "TRIGGER", - "EVENT", -]); -const STORED_PROGRAMS = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]); - -const controlsTransaction = (): Error => - new Error( - "typesql: migrations cannot control transactions; each migration runs in its own transaction", - ); - -/** Keep transaction ownership with the runner, including multi-statement raw SQL. */ -function assertMigrationSql(sql: string, dialect: Dialect): void { - const mysql = isMySql(dialect); - const words: string[] = []; - const checkRollback = (): void => { - if ( - words[0] === "ROLLBACK" && - words[1] !== "TO" && - !( - (words[1] === "WORK" || words[1] === "TRANSACTION") && - words[2] === "TO" - ) - ) - throw new Error( - "typesql: migrations cannot roll back their enclosing transaction; use ROLLBACK TO a savepoint", - ); - }; - let trigger = false; - // MySQL and MariaDB: the kind of object a CREATE statement makes, and the - // BEGIN/CASE … END nesting of a compound statement: a stored program's - // body, which runs only when called, or MariaDB's BEGIN NOT ATOMIC block, - // which runs now. - let createKind: string | undefined; - let compound: "program" | "block" | undefined; - let depth = 0; - /** Tokens that belong to the one before them: `END CASE`'s CASE. */ - let skip = 0; - scanSql(sql, dialect, (token, end, start) => { - if (token === ";") { - if (compound !== "program" || depth === 0) checkRollback(); - words.length = 0; - // A `;` inside a compound statement's BEGIN … END doesn't end it. - if (compound !== undefined && depth > 0) return; - createKind = undefined; - compound = undefined; - return; - } - if (skip > 0) { - skip--; - return; - } - const next = sql.slice(end); - if ( - mysql && - start && - compound === undefined && - token === "BEGIN" && - /^\s+NOT\s+ATOMIC\b/i.test(next) - ) { - compound = "block"; - depth = 1; - skip = 2; - return; - } - if (compound !== undefined) { - if (token === "BEGIN" || token === "CASE") depth++; - else if (token === "END") { - if (/^\s+CASE\b/i.test(next)) { - depth--; - skip = 1; - } else if (!/^\s+(?:IF|LOOP|WHILE|REPEAT|FOR)\b/i.test(next)) depth--; - } else if ( - compound === "block" && - start && - /^(START|COMMIT|XA)$/.test(token) - ) - throw controlsTransaction(); - } else if (trigger && start && token === "END") trigger = false; - else if ( - !trigger && - start && - (/^(BEGIN|START|COMMIT|END|ABORT)$/.test(token) || - (mysql && token === "XA")) - ) - throw controlsTransaction(); - if ( - mysql && - compound === undefined && - words[0] === "CREATE" && - createKind === undefined && - CREATE_KINDS.has(token) - ) { - createKind = token; - if (STORED_PROGRAMS.has(token)) { - compound = "program"; - depth = 0; - } - } - if (words.length < 4) words.push(token); - if (words.length === 2 && words[0] === "PREPARE" && token === "TRANSACTION") - throw new Error("typesql: migrations cannot prepare transactions"); - if (dialect === "sqlite" && token === "TRIGGER" && opensTrigger(words)) - trigger = true; - }); - checkRollback(); -} diff --git a/packages/core/src/migration/node.ts b/packages/core/src/migration/node.ts deleted file mode 100644 index ab9fbc9e..00000000 --- a/packages/core/src/migration/node.ts +++ /dev/null @@ -1,21 +0,0 @@ -// The migrator's Node-only modules reach Node's built-ins through -// process.getBuiltinModule(), which bundlers don't follow as they follow an -// import: the modules can be bundled for Node, where it works at runtime, and -// for Workers, which have the modules but never call them. - -/** - * A package, imported at runtime from node_modules. Bundlers follow a - * literal import() and would bundle it, but a Worker bundle can't resolve - * `pg`'s Node built-ins. `is` checks the module is the one expected. - */ -export async function runtimeImport( - specifier: string, - is: (module: unknown) => module is Module, -): Promise { - const module: unknown = await import( - /* webpackIgnore: true */ /* @vite-ignore */ specifier - ); - if (!is(module)) - throw new Error(`typesql: ${specifier} is not the expected package`); - return module; -} diff --git a/packages/core/src/migration/schema.ts b/packages/core/src/migration/schema.ts new file mode 100644 index 00000000..e8a57346 --- /dev/null +++ b/packages/core/src/migration/schema.ts @@ -0,0 +1,27 @@ +// The schema after a migration, as types read it. It names no dialect, so +// the root entry, which exports SchemaOf, loads no dialect's engine. + +declare const MISSING_SCHEMA: unique symbol; +/** The schema of a migration whose `up` doesn't return its context. */ +export type MissingSchema = { readonly [MISSING_SCHEMA]: true }; + +/** The schema of a migration context: its `schema`. */ +type ContextSchema = C extends { readonly schema: infer Schema } + ? Schema + : MissingSchema; + +/** Type-only: the schema a migrator or its `latest()` result leaves. */ +export declare const SCHEMA: unique symbol; + +/** + * The schema after a migration: `SchemaOf`. It also takes a + * migrator or its `latest()` result (the schema after the last migration), + * a 3.x migration's `up` function, or a migration context. + */ +export type SchemaOf = M extends { readonly [SCHEMA]: infer Schema } + ? Schema + : M extends { readonly up: (db: never) => Promise } + ? ContextSchema + : M extends (db: never) => Promise + ? ContextSchema + : ContextSchema; diff --git a/packages/core/src/migration/statements.ts b/packages/core/src/migration/statements.ts new file mode 100644 index 00000000..cdeccfe8 --- /dev/null +++ b/packages/core/src/migration/statements.ts @@ -0,0 +1,259 @@ +// The statements of a migration's scripts, as the migrator reads them: where +// one ends, and whether one controls the transaction the migrator owns. + +import { LEXIS, scan } from "../runtime/compile.js"; +import type { MigratorDialect } from "./dialects/session.js"; + +/** What the statements of a dialect's scripts are read by. */ +type Rules = Pick; + +/** A word, a `;`, or any other character of code. */ +const CODE = /[a-zA-Z_][a-zA-Z_0-9$]*|;|\S/g; + +const PUNCTUATION = new Set([";", "(", ")"]); + +/** + * Calls `on` for each token of `sql` outside whitespace and comments: an + * uppercased word, ";", "(", ")", or "" for a quoted string, identifier, a + * word that can only be a name, or any other character. `start` tells whether it begins a statement. Strings, quoted + * names and comments are the dialect's lexis. + */ +function scanSql( + sql: string, + { sql: dialect }: Rules, + on: (token: string, end: number, start: boolean) => void, +): void { + let start = true; + // The next word is a name: `NEW.end`, `SELECT 1 AS end`. + let name = false; + const token = (text: string, end: number): void => { + on(text, end, start); + start = text === ";"; + name = false; + }; + const code = (from: number, to: number): void => { + for (const m of sql.slice(from, to).matchAll(CODE)) { + const [text] = m; + const word = /^\w/.test(text) && !name ? text.toUpperCase() : ""; + token( + word || (PUNCTUATION.has(text) ? text : ""), + from + m.index + text.length, + ); + name = text === "." || word === "AS"; + } + }; + let last = 0; + scan( + sql, + (m) => { + code(last, m.index); + last = m.index + m[0].length; + if (!/^(?:--|#|\/\*)/.test(m[0])) token("", last); + return ""; + }, + dialect.lexis ?? LEXIS, + ); + code(last, sql.length); +} + +/** The kinds of object CREATE makes. */ +const CREATE_KINDS = new Set([ + "TABLE", + "VIEW", + "INDEX", + "DATABASE", + "SCHEMA", + "SEQUENCE", + "USER", + "ROLE", + "SERVER", + "TABLESPACE", + "PACKAGE", + "PROCEDURE", + "FUNCTION", + "TRIGGER", + "EVENT", +]); + +/** What a statement read so far is, as `;` and BEGIN … END nest in it. */ +interface Statement { + /** The first words since its start or its last `;`. */ + readonly words: string[]; + /** The kind of object a CREATE statement makes. */ + kind: string | undefined; + /** + * A compound statement's kind: a stored program's body (a trigger's + * among them), which runs only when called, or MariaDB's BEGIN NOT + * ATOMIC block, which runs now. + */ + compound: "program" | "block" | undefined; + /** Its BEGIN/CASE … END nesting. */ + depth: number; + /** How many parentheses are open: a parameter list holds no body. */ + parens: number; + /** The word before this one: a routine's name follows its kind. */ + last: string; + /** Tokens that belong to the one before them: `END CASE`'s CASE. */ + skip: number; +} + +/** `END IF` and the like end a control statement; `END FOR UPDATE` ends a CASE. */ +const END_OF_CONTROL = + /^\s+(?:IF|LOOP|WHILE|REPEAT|FOR(?!\s+(?:UPDATE|SHARE)\b))\b/i; + +/** Whether this BEGIN opens a body, not names a parameter or a routine. */ +const opensBody = (s: Statement, next: string, rules: Rules): boolean => + s.parens === 0 && + s.last !== s.kind && + s.last !== "EXISTS" && + (!rules.scan.atomicBodies || /^\s+ATOMIC\b/i.test(next)); + +/** + * Reads `sql` statement by statement: `onToken` sees each token of a + * statement, and `onEnd` the end of each, at a `;` or the end of `sql`, + * with what was read of it. A `;` inside a compound statement's + * BEGIN … END doesn't end it. + */ +function readStatements( + sql: string, + rules: Rules, + onToken: (token: string, start: boolean, statement: Statement) => void, + onEnd: (end: number, statement: Statement) => void, +): void { + const fresh = (): Statement => ({ + words: [], + kind: undefined, + compound: undefined, + depth: 0, + parens: 0, + last: "", + skip: 0, + }); + let statement = fresh(); + scanSql(sql, rules, (token, end, start) => { + const s = statement; + if (token === ";") { + if (s.compound !== undefined && s.depth > 0) { + onToken(token, start, s); + s.words.length = 0; + return; + } + onEnd(end, s); + statement = fresh(); + return; + } + if (s.skip > 0) { + s.skip--; + return; + } + const next = sql.slice(end); + if (token === "(") s.parens++; + else if (token === ")") s.parens--; + if ( + rules.scan.notAtomicBlocks && + start && + s.compound === undefined && + token === "BEGIN" && + /^\s+NOT\s+ATOMIC\b/i.test(next) + ) { + s.compound = "block"; + s.depth = 1; + s.skip = 2; + return; + } + if (s.compound !== undefined) { + if (token === "CASE" || (token === "BEGIN" && opensBody(s, next, rules))) + s.depth++; + else if (token === "END") { + if (rules.scan.atomicBodies) s.depth--; + else if (/^\s+CASE\b/i.test(next)) { + s.depth--; + s.skip = 1; + } else if (!END_OF_CONTROL.test(next)) s.depth--; + } + } else if ( + s.words[0] === "CREATE" && + s.kind === undefined && + CREATE_KINDS.has(token) + ) { + s.kind = token; + if (rules.scan.programs.has(token)) s.compound = "program"; + } + onToken(token, start, s); + if (s.words.length < 4) s.words.push(token); + s.last = token; + }); + onEnd(sql.length, statement); +} + +/** + * The statements of a script, split at semicolons outside quotes, comments + * and compound statements' BEGIN … END bodies. + */ +export function splitStatements(sql: string, rules: Rules): string[] { + const statements: string[] = []; + let begin = 0; + let content = false; + readStatements( + sql, + rules, + (token) => { + if (token !== ";") content = true; + }, + (end) => { + if (content) statements.push(sql.slice(begin, end)); + begin = end; + content = false; + }, + ); + return statements; +} + +const controlsTransaction = (): Error => + new Error( + "typesql: migrations cannot control transactions; each migration runs in its own transaction", + ); + +/** Throws for a ROLLBACK that ends the transaction, not one TO a savepoint. */ +function checkRollback([first, second, third]: readonly string[]): void { + if ( + first === "ROLLBACK" && + second !== "TO" && + !((second === "WORK" || second === "TRANSACTION") && third === "TO") + ) + throw new Error( + "typesql: migrations cannot roll back their enclosing transaction; use ROLLBACK TO a savepoint", + ); +} + +/** Keep transaction ownership with the runner, including multi-statement raw SQL. */ +export function assertMigrationSql(sql: string, rules: Rules): void { + readStatements( + sql, + rules, + (token, start, s) => { + // A `;` inside a block ends one of its statements. + if (token === ";") { + if (s.compound === "block") checkRollback(s.words); + return; + } + if (s.compound === "block") { + if (start && /^(START|COMMIT|XA)$/.test(token)) + throw controlsTransaction(); + } else if ( + s.compound === undefined && + start && + (/^(BEGIN|START|COMMIT|END|ABORT)$/.test(token) || + (rules.scan.xa && token === "XA")) + ) + throw controlsTransaction(); + if ( + s.words.length === 1 && + s.words[0] === "PREPARE" && + token === "TRANSACTION" + ) + throw new Error("typesql: migrations cannot prepare transactions"); + }, + (_end, s) => checkRollback(s.words), + ); +} diff --git a/packages/core/src/mysql/dialect.ts b/packages/core/src/mysql/dialect.ts index 86db30ba..134a0b9f 100644 --- a/packages/core/src/mysql/dialect.ts +++ b/packages/core/src/mysql/dialect.ts @@ -1,19 +1,14 @@ -import type { - DIALECT, - Dialect, - DialectBrand, - DialectTypes, -} from "../dialect.js"; -import type { SqlPlugin } from "../plugin.js"; +import type { DdlCheck, Dialect, DialectTypes, PluginsOf } from "../dialect.js"; import type { Executor } from "../query.js"; -import type { Transactional } from "../transaction.js"; import type { ApplyDdl } from "./engine/infer.js"; -import type { ValidateDdl } from "./migrations.js"; -import type { MariaDbSql, Sql } from "./sql/types.js"; +import type { + MariaDbSql, + MariaDbTransactionalSql, + Sql, + TransactionalSql, +} from "./sql/types.js"; import type { DefaultTypeMap, MariaDbTypeMap } from "./type-map/index.js"; -type PluginsOf

= P extends readonly SqlPlugin[] ? P : []; - export interface MySql extends DialectTypes { readonly typeMap: DefaultTypeMap; readonly sql: Sql>; @@ -22,7 +17,7 @@ export interface MySql extends DialectTypes { this["tm"], PluginsOf >; - readonly validateDdl: ValidateDdl; + readonly validateDdl: DdlCheck; readonly applyDdl: ApplyDdl; } @@ -34,34 +29,12 @@ export interface MariaDb extends DialectTypes { this["tm"], PluginsOf >; - readonly validateDdl: ValidateDdl; + readonly validateDdl: DdlCheck; readonly applyDdl: ApplyDdl; } -// Invariant parameters, as on Sql. -export interface TransactionalSql< - in out DB, - in out TM, - in out Plugins extends readonly SqlPlugin[] = [], -> extends Sql, - Transactional> { - readonly [DIALECT]?: DialectBrand<"mysql", DB, TM, Plugins>; - withPlugin( - plugin: P, - ): TransactionalSql; -} - -export interface MariaDbTransactionalSql< - in out DB, - in out TM, - in out Plugins extends readonly SqlPlugin[] = [], -> extends MariaDbSql, - Transactional> { - readonly [DIALECT]?: DialectBrand<"mariadb", DB, TM, Plugins>; - withPlugin( - plugin: P, - ): MariaDbTransactionalSql; -} +/** DDL script `Q` checked against schema `DB`: the script, or its error. */ +export type ValidateDdl = DdlCheck; /** MySQL and the servers that speak its dialect: Amazon Aurora, PlanetScale, TiDB, Google Cloud SQL, Azure. */ export interface MySqlDialect extends Dialect { @@ -86,14 +59,23 @@ declare module "../create-sql.js" { } } -export const mysql: MySqlDialect = { - name: "mysql", - quote: "`", - emptyRow: "() VALUES ()", -}; +/** + * MySQL's and MariaDB's lexis (see `Dialect`): strings take backslash + * escapes, `--` starts a comment only before whitespace, `#` starts one too, + * and block comments don't nest. `$` belongs to names, so `a$1` and `$1a` + * are names. `MySqlLexis` states its quotes and comments for the types. + */ +export const mysqlLexis = + /(?(value: V) => V | string = jsonText; + +/** + * Rewrites TypeSQL's `$1`, `$2`, … to MySQL's positional `?`, repeating a + * value for each use of its parameter. `map` converts each value for the + * driver; mysqlValue by default. + */ +export function mysqlPositional( + text: string, + values: readonly V[], + map?: (value: V | undefined) => R, +): PositionalStatement { + return positionalAt(text, values, map ?? mysqlValue, mysqlLexis); +} + +/** MySQL text split at its `$n` parameters, each value mapped by `map`. */ +export const mysqlSplit = ( + text: string, + values: readonly V[], + map: (value: V | undefined) => R, +): SplitStatement => splitAt(text, values, map, mysqlLexis); + +/** A value as mysql2 and the mariadb connector bind it. */ +export type MySqlValue = + | string + | number + | bigint + | boolean + | Date + | Buffer + | null; + +/** + * A value as mysql2 and the mariadb connector bind it: `sql.json(…)` is JSON + * text, `undefined` is `NULL`, and a plain `Uint8Array` becomes a `Buffer`, + * which both drivers send as binary. Any other object is refused: the + * drivers would write it into the statement as SQL text. + */ +export function mysqlBind(value: V): MySqlValue { + const v = mysqlValue(value); + if (v === undefined || v === null) return null; + if (Buffer.isBuffer(v) || v instanceof Date) return v; + if (v instanceof Uint8Array) + return Buffer.from(v.buffer, v.byteOffset, v.byteLength); + switch (typeof v) { + case "string": + case "number": + case "bigint": + case "boolean": + return v; + } + throw new TypeError( + `typesql: cannot bind ${Object.prototype.toString.call(v)} as a MySQL value; use sql.json() for JSON`, + ); +} + +/** + * What mysql2 and the mariadb connector resolve a statement to: its rows, + * the OK packet of a statement without rows, or, for a script of several + * statements, a list of those. + */ +export type MySqlResult = + | readonly unknown[] + | { readonly affectedRows?: number | bigint }; + +const isPacket = (item: unknown): item is { affectedRows?: number | bigint } => + typeof item === "object" && + item !== null && + "affectedRows" in item && + "insertId" in item; + +/** Whether an element of a result list is a statement's result, not a row. */ +const isStatementResult = (item: unknown): item is MySqlResult => + Array.isArray(item) || isPacket(item); + +/** + * A MySQL driver's result as an executor's: the rows of a statement that + * returns them, else its affected row count. A script's last statement + * stands for it. + */ +export function mysqlResult(result: MySqlResult): ExecutorResult { + if (Array.isArray(result)) { + const last = result.at(-1); + return last !== undefined && result.every(isStatementResult) + ? mysqlResult(last) + : { rows: result, rowCount: result.length }; + } + const affected = "affectedRows" in result ? result.affectedRows : undefined; + return { rows: [], rowCount: affected === undefined ? 0 : Number(affected) }; +} + +/** A mysql2 or mariadb connection, as the connection hands it out. */ +export interface MySqlClient { + query(sql: string, values?: unknown[]): Promise; + end(): Promise; +} + +/** A MySQL session: a mysql2 or mariadb connection. */ +interface Session extends MySqlClient { + on(event: "error", listener: (error: Error) => void): void; +} + +type Mysql2 = typeof import("mysql2/promise"); + +/** + * What connectMysql reads of the `mariadb` connector. A statement resolves + * to its rows, or to the OK packet of one without rows, which mysqlResult + * reads. + */ +interface MariaDb { + createConnection(uri: string): Promise; +} + +const isMysql2 = (module: unknown): module is { readonly default: Mysql2 } => + typeof module === "object" && + module !== null && + "default" in module && + typeof module.default === "object" && + module.default !== null && + "createConnection" in module.default && + typeof module.default.createConnection === "function"; + +const isMariaDb = (module: unknown): module is MariaDb => + typeof module === "object" && + module !== null && + "createConnection" in module && + typeof module.createConnection === "function"; + +/** A session, and how a statement runs on it. */ +interface Opened { + readonly session: Session; + run(text: string, values: unknown[]): Promise; +} + +/** + * A session with mysql2, else with the mariadb connector. Multiple + * statements let .raw() run a script, as it does on PostgreSQL. + */ +async function openMysql(url: string): Promise { + const rest = url.slice(url.indexOf(":")); + const mysql2 = await runtimeImport("mysql2/promise", isMysql2).catch( + () => undefined, + ); + if (mysql2) { + const connection = await mysql2.default.createConnection({ + uri: `mysql${rest}`, + multipleStatements: true, + }); + return { + session: { + query: (text, values) => connection.query(text, values), + end: () => connection.end(), + on: (event, listener) => { + connection.on(event, listener); + }, + }, + run: async (text, values) => (await connection.query(text, values))[0], + }; + } + let mariadb: MariaDb; + try { + mariadb = await runtimeImport("mariadb", isMariaDb); + } catch (cause) { + throw new Error( + 'typesql: url connects with the "mysql2" package, or the "mariadb" connector, and neither is installed: npm install mysql2', + { cause }, + ); + } + const uri = new URL(`mariadb${rest}`); + uri.searchParams.set("multipleStatements", "true"); + const session = await mariadb.createConnection(uri.href); + return { session, run: (text, values) => session.query(text, values) }; +} + +/** One mysql2 (or mariadb) connection for a MySQL or MariaDB URL. */ +export async function connectMysql( + url: string, +): Promise> { + if (!URL.canParse(url) || !/^(mysql|mariadb):$/.test(new URL(url).protocol)) + throw new Error( + `typesql: invalid connection URL ${JSON.stringify(url)} (expected mysql://user:password@host:port/database)`, + ); + let opened: Opened; + try { + opened = await openMysql(url); + } catch (error) { + if (error instanceof Error && error.message.startsWith("typesql:")) + throw error; + throw new Error( + `typesql: cannot connect to MySQL: ${errorMessage(error)}`, + { + cause: error, + }, + ); + } + // The server can end the session between queries. Without a listener, + // the driver's 'error' event would end the process; the next query + // rejects with the error instead. + const { session, run } = opened; + const lost: Lost = { error: undefined }; + session.on("error", (error: Error) => { + lost.error ??= error; + }); + return { + client: session, + execute: async (sql, values) => { + if (lost.error) throw lost.error; + const positional = mysqlPositional(sql, values, mysqlBind); + return mysqlResult(await run(positional.text, positional.values)); + }, + close: () => closing(session.end(), lost), + }; +} + +const ESCAPES: ReadonlyMap = new Map([ + ["\0", "\\0"], + ["\b", "\\b"], + ["\t", "\\t"], + ["\n", "\\n"], + ["\r", "\\r"], + ["\x1a", "\\Z"], + ["\\", "\\\\"], + // Doubled, not `\\'`: with NO_BACKSLASH_ESCAPES that would end the string. + ["'", "''"], +]); + +function quote(s: string): string { + let out = "'"; + for (const c of s) out += ESCAPES.get(c) ?? c; + return `${out}'`; +} + +const pad = (n: number, width = 2): string => String(n).padStart(width, "0"); + +/** A value as a MySQL literal. Dates are written in UTC, the serverless platforms' time zone. */ +function literal(value: V): string { + const v = mysqlValue(value); + if (v === null || v === undefined) return "NULL"; + switch (typeof v) { + case "string": + return quote(v); + case "boolean": + return v ? "TRUE" : "FALSE"; + case "bigint": + return v < 0n ? `(${v})` : String(v); + case "number": + if (!Number.isFinite(v)) + throw new TypeError(`typesql: MySQL has no ${v} value`); + // `5-$1` must not become the comment-free but odd `5--1`. + return v < 0 ? `(${v})` : String(v); + } + if (v instanceof Date) { + if (Number.isNaN(v.getTime())) + throw new TypeError("typesql: cannot bind an invalid Date"); + return `'${pad(v.getUTCFullYear(), 4)}-${pad(v.getUTCMonth() + 1)}-${pad(v.getUTCDate())} ${pad(v.getUTCHours())}:${pad(v.getUTCMinutes())}:${pad(v.getUTCSeconds())}.${pad(v.getUTCMilliseconds(), 3)}'`; + } + if (v instanceof Uint8Array) { + let hex = ""; + for (const b of v) hex += b.toString(16).padStart(2, "0"); + return `X'${hex}'`; + } + throw new TypeError( + `typesql: cannot bind ${Object.prototype.toString.call(v)} as a MySQL value; use sql.json() for JSON`, + ); +} + +/** + * The HTTP drivers bind nothing on the server, so `$1`, `$2`, … become + * escaped literals in the statement text. + */ +export function mysqlInline(text: string, values: readonly unknown[]): string { + if (!text.includes("$")) return text; + const { parts, values: literals } = mysqlSplit(text, values, literal); + return parts.reduce( + (out, part, i) => `${out}${String(literals[i - 1])}${part}`, + ); +} diff --git a/packages/core/src/mysql/engine/ast.ts b/packages/core/src/mysql/engine/ast.ts index e52a620e..34131b8b 100644 --- a/packages/core/src/mysql/engine/ast.ts +++ b/packages/core/src/mysql/engine/ast.ts @@ -92,13 +92,13 @@ export type InSub = { readonly q: SelectStmt; readonly not: boolean; }; -/** `e op ANY (SELECT …)` / `e op ALL (SELECT …)`. */ +/** `l op ANY (SELECT …)` / `l op ALL (SELECT …)`; `SOME` is `ANY`. */ export type Quantified = { readonly t: "quant"; + readonly l: Expr; readonly op: string; - readonly all: boolean; - readonly e: Expr; - readonly q: SelectStmt; + readonly q: "any" | "all"; + readonly r: SelectStmt; }; export type Like = { readonly t: "like"; diff --git a/packages/core/src/mysql/engine/binder/scope.ts b/packages/core/src/mysql/engine/binder/scope.ts index 00c85217..3a46db5a 100644 --- a/packages/core/src/mysql/engine/binder/scope.ts +++ b/packages/core/src/mysql/engine/binder/scope.ts @@ -1,8 +1,29 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { QueryContext, Scope } from "../../../engine/types.js"; +import type { + ColumnOrder, + KnownOrder, + VisibleKeys, +} from "../../../engine/family/ddl.js"; +import type { NameAt, NamedAt } from "../../../engine/family/diagnostics.js"; +import type { + AllCells, + BareIn, + CellsMap, + FindKey, + ItemCells, + LevelCol, + LevelHas, + NaturalCols, + NullLevel, + RefDisplay, + RenameCells, + SkipIn, +} from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { IsUnion } from "../../../engine/object.js"; +import type { Scope } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; -import type { UnionList } from "../../../sql/splice.js"; import type { Expr, FromItem, @@ -28,16 +49,6 @@ import type { type Env = QueryContext; -export type FindKey = Lc extends keyof O - ? Lc - : { - [K in keyof O & string]: K extends `$${string}` - ? never - : Lowercase extends Lowercase - ? K - : never; - }[keyof O & string]; - export type LookupRelation< DB, Ref extends TableRef, @@ -69,13 +80,9 @@ type IsRel = : false : false; -type RefDisplay = Ref["schema"] extends string - ? `${Ref["schema"]}.${Ref["name"]}` - : Ref["name"]; - export type NoTable = SqlError< `Table '${RefDisplay}' doesn't exist`, - null + NamedAt, "table"> >; type TableCols = { @@ -87,49 +94,8 @@ type TableCols = { (Tbl[K] extends { readonly generated: true } ? { readonly gen: true } : {}); }; -/** Visible columns in table order: by position when the schema has one, else by name. */ -type TableOrder = - VisibleKeys extends infer V extends string - ? [V] extends [never] - ? [] - : Tbl[V & keyof Tbl] extends { readonly i: number } - ? ByIndex, 0, []> - : LowerAll> - : never; -type VisibleKeys = { - [K in keyof Tbl & string]: Tbl[K] extends { readonly hidden: true } - ? never - : K; -}[keyof Tbl & string]; - -/** The columns `INSERT INTO t VALUES (…)` without a column list fills: all but invisible ones. */ -export type InsertOrder = - TableOrder extends infer O extends readonly string[] ? O : []; - -// Each visible column's name by its position, built once: walking it is one -// lookup per column, where scanning every column for each position is -// quadratic. A dropped or invisible column leaves a gap; the walk goes on -// while a position is left. -type PositionMap = { - [K in keyof Tbl & string as K extends Keys - ? Tbl[K] extends { readonly i: infer I extends number } - ? I - : never - : never]: Lowercase; -}; -export type ByIndex< - M, - N extends number, - Acc extends readonly string[], - Seen extends number = never, -> = N extends keyof M - ? ByIndex, [...Acc, M[N] & string], Seen | N> - : [Exclude] extends [never] - ? Acc - : ByIndex, Acc, Seen>; -export type LowerAll = { - [I in keyof L]: Lowercase; -}; +/** The columns `INSERT INTO t VALUES (…)` without a column list fills, in order: all but invisible ones. */ +export type InsertOrder = KnownOrder>; export type RelationItem = Rel extends View @@ -141,7 +107,7 @@ type TableItem = { readonly alias: Alias; readonly cols: TableCols; readonly skip: []; - readonly order: TableOrder; + readonly order: ColumnOrder>; }; type BindTable< @@ -180,14 +146,6 @@ type EntryItem< readonly order: E["order"]; }; -type CellsMap = { - readonly [E in C[number] as Lowercase]: MyColOf< - E["t"]["type"], - E["t"]["nullable"], - E["n"] - >; -}; - /** Derived tables, CTEs and views need unique column names. */ export type DupCell< C extends readonly Cell[], @@ -197,7 +155,7 @@ export type DupCell< ...infer R extends readonly Cell[], ] ? Lowercase extends Seen - ? SqlError<`Duplicate column name '${H["n"]}'`, null> + ? SqlError<`Duplicate column name '${H["n"]}'`, NameAt>> : DupCell> : null; @@ -206,25 +164,13 @@ export type CteEntry = { readonly order: { [I in keyof C]: Lowercase }; }; -export type RenameCells< - C extends readonly Cell[], - N extends readonly { readonly n: string }[], -> = { - [I in keyof C]: { - readonly n: N[I & keyof N] extends { readonly n: infer X extends string } - ? X - : C[I]["n"]; - readonly t: C[I]["t"]; - }; -}; - export type BindFrom< F extends FromItem | null, E extends Env, > = F extends FromItem ? BindItem extends infer L extends MyLevel ? DupAlias extends infer D extends string - ? SqlError<`Not unique table/alias: '${D}'`, null> + ? SqlError<`Not unique table/alias: '${D}'`, NamedAt> : L : BindItem : []; @@ -233,8 +179,8 @@ type DupAlias = L[I] extends MyItem ? L[I]["alias"] extends string ? Lowercase extends Seen ? L[I]["alias"] - : DupAlias, Seen | Lowercase> - : DupAlias, Seen> + : DupAlias, Seen | Lowercase> + : DupAlias, Seen> : null; type BindItem< @@ -378,16 +324,6 @@ type JoinUsing< ? CheckUsing : null; -type LevelHas = true extends { - [I in keyof L]: C extends keyof L[I]["cols"] - ? C extends L[I]["skip"][number] - ? false - : true - : false; -}[number] - ? true - : false; - type CheckUsing< U extends readonly string[], L extends MyLevel, @@ -395,71 +331,10 @@ type CheckUsing< I extends number = 0, > = U[I] extends infer C extends string ? [LevelHas, LevelHas] extends [true, true] - ? CheckUsing> - : SqlError<`Unknown column '${C}' in 'from clause'`, null> + ? CheckUsing> + : SqlError<`Unknown column '${C}' in 'from clause'`, NamedAt> : U; -type NaturalCols = - LevelNames extends infer RN extends string - ? FilterNames, RN> - : never; -type LevelNames = { - [I in keyof L]: Exclude; -}[number]; -type LevelOrder< - L extends MyLevel, - I extends number = 0, - Acc extends readonly string[] = [], -> = L[I] extends MyItem - ? LevelOrder< - L, - Nx, - [...Acc, ...WithoutSkip] - > - : Acc; -type WithoutSkip = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends S - ? WithoutSkip - : [H, ...WithoutSkip] - : []; -type FilterNames = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends Keep - ? [H, ...FilterNames] - : FilterNames - : []; - -type NullItem = { - readonly alias: It["alias"]; - readonly cols: { - readonly [K in keyof It["cols"]]: MyColOf< - It["cols"][K]["type"], - true, - It["cols"][K]["n"] - >; - }; - readonly skip: It["skip"]; - readonly order: It["order"]; - /** The columns before the outer join made them nullable. */ - readonly orig: It extends { readonly orig: infer O } ? O : It["cols"]; -}; -export type NullLevel = { - readonly [I in keyof L]: NullItem; -}; - -type SkipIn = { - readonly [I in keyof L]: { - readonly [P in keyof L[I]]: P extends "skip" - ? [...L[I]["skip"], ...Extract[]] - : L[I][P]; - }; -}; - type Joined< Kind, L extends MyLevel, @@ -500,9 +375,6 @@ type MergedUsing< readonly skip: []; readonly order: U; }; -type LevelCol = { - [I in keyof L]: C extends keyof L[I]["cols"] ? L[I]["cols"][C] : never; -}[number]; export type ResolveColumn< S extends Scope, @@ -519,7 +391,7 @@ export type ResolveColumn< type UnknownColumn = SqlError< `Unknown column '${Disp}'`, - null + NamedAt >; type ResolveBare< @@ -532,22 +404,13 @@ type ResolveBare< ? [M] extends [never] ? ResolveBare : IsUnion extends true - ? SqlError<`Column '${Disp}' is ambiguous`, null> + ? SqlError<`Column '${Disp}' is ambiguous`, NamedAt> : M : never : Name extends keyof Aliases ? Aliases[Name] : UnknownColumn; -type BareIn = { - [I in keyof L]: Name extends keyof L[I]["cols"] - ? Name extends L[I]["skip"][number] - ? never - : Tag - : never; -}[number]; -type Tag = C & { readonly at: I }; - type ResolveQualified< S extends Scope, Tbl extends string, @@ -571,12 +434,6 @@ type ResolveQualified< : never : UnknownColumn; -type IsUnion = T extends unknown - ? [U] extends [T] - ? false - : true - : never; - export type StarCells< L extends MyLevel, Tbl extends string | null, @@ -589,7 +446,7 @@ export type StarCells< : never; }[number] extends infer It ? [It] extends [never] - ? SqlError<`Unknown table '${Tbl}'`, null> + ? SqlError<`Unknown table '${Tbl}'`, NamedAt> : It extends MyItem ? ItemCells : never @@ -597,42 +454,3 @@ export type StarCells< : L extends readonly [] ? SqlError<"No tables used", null> : AllCells; - -type AllCells< - L extends MyLevel, - I extends number, - Acc extends readonly Cell[], -> = L[I] extends MyItem - ? AllCells< - L, - Nx, - [...Acc, ...ItemCells] - > - : Acc; -type ItemCells< - It extends MyItem, - O extends readonly string[], - Skip, - Acc extends readonly Cell[] = [], -> = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends Skip - ? ItemCells - : ItemCells< - It, - R, - Skip, - [ - ...Acc, - { - readonly n: It["cols"][H]["n"]; - readonly t: { - readonly type: It["cols"][H]["type"]; - readonly nullable: It["cols"][H]["nullable"]; - }; - }, - ] - > - : Acc; diff --git a/packages/core/src/mysql/engine/ddl/apply.ts b/packages/core/src/mysql/engine/ddl/apply.ts index d120db3e..0d69460b 100644 --- a/packages/core/src/mysql/engine/ddl/apply.ts +++ b/packages/core/src/mysql/engine/ddl/apply.ts @@ -1,15 +1,31 @@ +import type { + AppendIndex, + ColumnPks, + DupName, + Indexed, + LowerAll, + PositionOrder, + Without, +} from "../../../engine/family/ddl.js"; +import type { + NameAt, + NamedAt, + Unlocated, +} from "../../../engine/family/diagnostics.js"; +import type { FindKey, RenameCells } from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Add } from "../../../engine/num.js"; import type { DelKey, Rebuild, RenameKey, SetKey, + Simplify, } from "../../../engine/object.js"; -import type { Nx } from "../../../engine/parse.js"; import type { Toks } from "../../../engine/tokens.js"; -import type { QueryContext, Simplify } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Shaped, Table, TsShape, View } from "../../../schema.js"; -import type { UnionList } from "../../../sql/splice.js"; import type { AlterAction, AlterTable, @@ -26,22 +42,19 @@ import type { Stmt, TableRef, } from "../ast.js"; -import type { - ByIndex, - DupCell, - FindKey, - LookupRelation, - LowerAll, - NoTable, - RenameCells, -} from "../binder/scope.js"; +import type { DupCell, LookupRelation, NoTable } from "../binder/scope.js"; import type { Lex } from "../lexer.js"; import type { ParseScript } from "../parser/stmt.js"; +import type { SyntaxWords } from "../parser/util.js"; import type { ProjectDml } from "../project/dml.js"; import type { ProjectSelect } from "../project/select.js"; -import type { Cell, Tup } from "../types.js"; +import type { Cell } from "../types.js"; -export type ApplyDdlAll = +export type ApplyDdlAll = Unlocated< + ApplyScript, + SyntaxWords +>; +type ApplyScript = Lex extends infer T extends Toks ? ParseScript extends infer S extends readonly Stmt[] ? ApplyLoop @@ -55,7 +68,7 @@ type ApplyLoop< > = S[I] extends infer St extends Stmt ? ApplyOne extends AnyErr ? ApplyOne - : ApplyLoop, S, Nx> + : ApplyLoop, S, Next> : DB; type TopEnv = QueryContext; @@ -104,7 +117,7 @@ type Exists = type AlreadyExists = SqlError< `Table '${Ref["name"]}' already exists`, - null + NamedAt >; type CreateTableEffect = @@ -120,38 +133,13 @@ type CreateTableEffect = TableFromDefs> >; -type ColumnPks< - Cols extends readonly ColumnDef[], - Acc extends readonly string[] = [], -> = Cols extends readonly [ - infer H extends ColumnDef, - ...infer R extends readonly ColumnDef[], -] - ? ColumnPks< - R, - H["primaryKey"] extends true ? [...Acc, Lowercase] : Acc - > - : Acc; - -type DupName< - Cols extends readonly ColumnDef[], - Seen extends string = never, -> = Cols extends readonly [ - infer H extends ColumnDef, - ...infer R extends readonly ColumnDef[], -] - ? Lowercase extends Seen - ? H["name"] - : DupName> - : null; - type TableFromDefs< Cols extends readonly ColumnDef[], Pk extends readonly string[], > = Cols extends readonly [] ? SqlError<"A table must have at least 1 column", null> : DupName extends infer D extends string - ? SqlError<`Duplicate column name '${D}'`, null> + ? SqlError<`Duplicate column name '${D}'`, NameAt>> : ColumnPks extends infer CP extends readonly string[] ? CP["length"] extends 0 ? BuildTable @@ -160,13 +148,6 @@ type TableFromDefs< : SqlError<"Multiple primary key defined", null> : never; -type Indexed = { - [I in keyof L]: { - readonly c: L[I]; - readonly i: I extends `${infer N extends number}` ? N : 0; - }; -}; - type BuildTable< Cols extends readonly ColumnDef[], Pk extends readonly string[], @@ -317,11 +298,6 @@ type CtasTable< >; } >; -type Add = [ - ...Tup, - ...Tup, -]["length"] & - number; type ViewCols = Rebuild<{ [D in Indexed[number] as D["c"]["n"]]: MkCol< @@ -390,57 +366,12 @@ type AlterLoop< } ? Exists extends true ? AlreadyExists - : AlterLoop, To["name"], Rel, A, Nx> + : AlterLoop, To["name"], Rel, A, Next> : AlterColumns extends AnyErr ? AlterColumns - : AlterLoop, A, Nx> + : AlterLoop, A, Next> : SetKey; -/** Column names in table order, as written in the schema. */ -type OrderOf = Rel[keyof Rel] extends { readonly i: number } - ? ByIndex, 0, []> - : UnionList extends infer L extends readonly string[] - ? L - : []; -type PositionMap = { - [K in keyof Rel & string as Rel[K] extends { - readonly i: infer I extends number; - } - ? I - : never]: K; -}; - -type MaxIndex = { - [P in keyof Rel]: Rel[P] extends { readonly i: infer N extends number } - ? N - : never; -}[keyof Rel]; -// The position after the last column. Positions are 0…n-1 unless a column -// was dropped: count up while each one exists (one membership check per -// column), and look for the largest one only after a gap. -type NextIndex< - U extends number, - A extends unknown[] = [], - Seen extends number = never, -> = A["length"] extends U - ? NextIndex - : [Exclude] extends [never] - ? A["length"] - : Nx>; -type MaxOf = [U] extends [never] - ? 0 - : [Exclude] extends [never] - ? A["length"] - : MaxOf, [...A, 0]>; - -type Without = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends X - ? Without - : [H, ...Without] - : []; type InsertAt< O extends readonly string[], Name extends string, @@ -463,8 +394,6 @@ type AfterLoop< ? [...Acc, H, Name, ...R] : AfterLoop : null; -// Every column's position from the new order, built once, then one lookup -// per column. type Positions = { [D in Indexed[number] as D["c"] & string]: D["i"]; }; @@ -478,20 +407,34 @@ type Reindex = }> : never; -/** `Name` with `Desc` at `P`: appended at the end, or placed FIRST / AFTER with every column renumbered. */ +/** + * `Name` with `Desc` at `P`: appended at the end, or placed FIRST / AFTER with + * every column renumbered. A table written without positions has no order to + * place it in, and stays without one. + */ type Placed< Rel, Name extends string, Desc, P extends ColumnPlace, > = P extends null - ? SetKey> }>> - : InsertAt, Name>, Name, P> extends infer O extends + ? SetKey }>> + : PositionOrder extends infer O extends + readonly string[] + ? InsertAt, Name, P> extends infer O2 extends readonly string[] - ? Reindex, O> + ? Reindex, O2> + : UnknownAfter : P extends { readonly col: infer A extends string } - ? SqlError<`Unknown column '${A}' in '${Name}'`, null> - : never; + ? [Exclude, Name>] extends [never] + ? UnknownAfter + : SetKey + : SetKey; +type UnknownAfter = P extends { + readonly col: infer A extends string; +} + ? SqlError<`Unknown column '${A}' in '${Name}'`, NamedAt> + : never; type AddedCol = ColDesc; @@ -502,10 +445,10 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string ? [Old] extends [never] - ? SqlError<`Unknown column '${F}' in '${K}'`, null> + ? SqlError<`Unknown column '${F}' in '${K}'`, NamedAt> : [Exclude>, Old>] extends [never] ? RenameKey - : SqlError<`Duplicate column name '${To}'`, null> + : SqlError<`Duplicate column name '${To}'`, NameAt>> : never : A extends { readonly t: "add_column"; @@ -521,7 +464,10 @@ type AlterColumns = A extends { : Placed, P> : Ine extends true ? Rel - : SqlError<`Duplicate column name '${C["name"]}'`, null> + : SqlError< + `Duplicate column name '${C["name"]}'`, + NameAt> + > : A extends { readonly t: "change_column"; readonly from: infer F extends string; @@ -530,7 +476,7 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string & keyof Rel ? [Old] extends [never] - ? SqlError<`Unknown column '${F}' in '${K}'`, null> + ? SqlError<`Unknown column '${F}' in '${K}'`, NamedAt> : [Exclude>, Old>] extends [never] ? P extends null ? SetKey< @@ -544,7 +490,10 @@ type AlterColumns = A extends { AddedCol, P > - : SqlError<`Duplicate column name '${C["name"]}'`, null> + : SqlError< + `Duplicate column name '${C["name"]}'`, + NameAt> + > : never : A extends { readonly t: "drop_column"; @@ -573,7 +522,10 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string & keyof Rel ? [Old] extends [never] - ? SqlError<`Unknown column '${N}' in '${K}'`, null> + ? SqlError< + `Unknown column '${N}' in '${K}'`, + NamedAt + > : SetKey> : never : A extends { @@ -583,7 +535,10 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string & keyof Rel ? [Old] extends [never] - ? SqlError<`Unknown column '${N}' in '${K}'`, null> + ? SqlError< + `Unknown column '${N}' in '${K}'`, + NamedAt + > : SetKey> : never : A extends { @@ -600,7 +555,7 @@ type PositionOf = Rel[K] extends { readonly i: infer I extends number; } ? I - : NextIndex>; + : AppendIndex; /** Not tracked: a primary key added by ALTER can't be detected twice. */ type HasPk<_Rel> = false; @@ -622,7 +577,7 @@ type RenameEffect< ? LookupRelation extends [unknown, infer K extends string] ? Exists extends true ? AlreadyExists - : RenameEffect, P, Nx> + : RenameEffect, P, Next> : NoTable : DB; @@ -634,12 +589,12 @@ type DropEffect< ? LookupRelation extends [infer Rel, infer K extends string] ? S["kind"] extends "table" ? Rel extends View

- ? SqlError<`Unknown table '${K}'`, null> - : DropEffect, S, Nx> + ? SqlError<`Unknown table '${K}'`, NamedAt> + : DropEffect, S, Next> : Rel extends View
- ? DropEffect, S, Nx> + ? DropEffect, S, Next> : SqlError<`'${K}' is not VIEW`, null> : S["ifExists"] extends true - ? DropEffect> - : SqlError<`Unknown table '${Ref["name"]}'`, null> + ? DropEffect> + : SqlError<`Unknown table '${Ref["name"]}'`, NamedAt> : DB; diff --git a/packages/core/src/mysql/engine/diagnostics.ts b/packages/core/src/mysql/engine/diagnostics.ts index 405d0812..6452f9e3 100644 --- a/packages/core/src/mysql/engine/diagnostics.ts +++ b/packages/core/src/mysql/engine/diagnostics.ts @@ -1,112 +1,15 @@ -import type { Nx } from "../../engine/parse.js"; -import type { Best, IsRelation, RelCols } from "../../engine/suggest.js"; +import type { Located } from "../../engine/family/diagnostics.js"; import type { Toks } from "../../engine/tokens.js"; -import type { SqlError } from "../../errors.js"; +import type { SyntaxWords } from "./parser/util.js"; import type { FunctionCatalog, Functions } from "./typer/functions.js"; -export type Reposition = - E extends SqlError - ? At extends null - ? SqlError, Locate> - : SqlError - : E; - -type WithHint< - M extends string, - T extends Toks, +/** The error with its position in the statement `T`, and a hint for unknown names. */ +export type Reposition = Located< + E, + T, DB, -> = M extends `Unknown column '${infer Name}'${string}` - ? Hinted< - M, - Best< - LastPart>, - ColumnsOf>, - true - >, - "column" - > - : M extends `Table '${infer Name}' doesn't exist` - ? Hinted>, RelNames, true>, "table"> - : M extends `FUNCTION ${infer Name} does not exist` - ? Hinted, FnNames, true>, "function"> - : M; - -type Hinted = S extends string - ? `${M}. Perhaps you meant the ${What} "${S}"?` - : M; - -type RelNames = { - [K in keyof DB & string]: K extends `$${string}` - ? never - : IsRelation extends true - ? K - : never; -}[keyof DB & string]; - -type Mentioned = T[I] extends { - readonly k: string; -} - ? Mentioned, Acc | RelAt> - : Acc; -type RelAt = Tok extends { - readonly k: "w" | "id"; - readonly v: infer V; -} - ? { - [K in RelNames]: Lowercase extends V ? K : never; - }[RelNames] - : never; - -type ColumnsOf = { - [K in RelNames]: K extends Rels ? RelCols : never; -}[RelNames]; - -type FnNames = - | (keyof Functions & string) - | (keyof FunctionCatalog & string) - | (DB extends { readonly $functions: infer F } ? keyof F & string : never); - -type Locate = M extends - | `Unknown column '${infer Name}'${string}` - | `Column '${infer Name}'${string} ambiguous` - | `Field '${infer Name}' doesn't have a default value` - | `The value specified for generated column '${infer Name}'${string}` - ? FindName>, 0> - : M extends - | `Table '${infer Name}' ${string}` - | `Unknown table '${infer Name}'${string}` - | `Not unique table/alias: '${infer Name}'` - ? FindName>, 0> - : M extends - | `FUNCTION ${infer Name} does not exist` - | `Incorrect parameter count in the call to native function '${infer Name}'` - ? FindName>, 0> - : M extends `Duplicate column name '${infer Name}'` - ? FindName, 0> - : M extends `${string}RETURNING${string}` - ? FindName - : M extends `${string}value count${string}` - ? FindName - : M extends `${string}LIMIT${string}` - ? FindName - : M extends - | `${string}group function${string}` - | `${string}only_full_group_by` - ? 0 - : 0; - -type LastPart = N extends `${string}.${infer R}` - ? LastPart - : N; + Builtins, + SyntaxWords +>; -type FindName< - T extends Toks, - Name extends string, - I extends number, -> = T[I] extends { - readonly k: string; -} - ? T[I] extends { readonly k: "w" | "id"; readonly v: Name } - ? I - : FindName> - : 0; +type Builtins = keyof Functions | keyof FunctionCatalog; diff --git a/packages/core/src/mysql/engine/engine.ts b/packages/core/src/mysql/engine/engine.ts index 99104547..8f28beb4 100644 --- a/packages/core/src/mysql/engine/engine.ts +++ b/packages/core/src/mysql/engine/engine.ts @@ -1,3 +1,4 @@ +import type { ShapeBase } from "../../schema.js"; import type { AnalysisFn, DisplayFn, @@ -5,7 +6,8 @@ import type { ParamFn, RowFn, TokensFn, -} from "../../engine/contract.js"; +} from "../../sql/contract.js"; +import type { Lexis } from "../../sql/lexis.js"; import type { TsParamTypeOf } from "../type-map/index.js"; import type { Analysis, RowFrom } from "./infer.js"; import type { Lex } from "./lexer.js"; @@ -18,8 +20,25 @@ export interface MySqlEngine extends EngineTypes { readonly param: MySqlParam; readonly display: MySqlDisplay; readonly json: "json"; + readonly arrayNote: ""; readonly quote: "`"; readonly emptyRow: "() VALUES ()"; + readonly valuesDefault: true; + readonly lexis: MySqlLexis; +} + +/** + * MySQL's and MariaDB's text rules, as `mysqlLexis`: strings take backslash + * escapes, `--` starts a comment only before whitespace, `#` starts one too, + * and block comments don't nest. + */ +export interface MySqlLexis extends Lexis { + readonly extra: "`" | "#"; + readonly backslash: "'" | '"'; + readonly spacedDash: true; + readonly nest: false; + readonly dollar: false; + readonly names: "`"; } /** MariaDB: the same grammar, with its own types, functions and statements. */ @@ -43,5 +62,5 @@ interface MySqlParam extends ParamFn { readonly out: TsParamTypeOf; } interface MySqlDisplay extends DisplayFn { - readonly out: this["t"]; + readonly out: ShapeBase; } diff --git a/packages/core/src/mysql/engine/index.ts b/packages/core/src/mysql/engine/index.ts index fb52495f..8425bb6d 100644 --- a/packages/core/src/mysql/engine/index.ts +++ b/packages/core/src/mysql/engine/index.ts @@ -1,9 +1,8 @@ export type * from "../../index.js"; -export type { SplicedText } from "../../sql/call.js"; export type * from "../index.js"; export type { Col, Enum } from "../schema/index.js"; export type * as Ast from "./ast.js"; -export type { MariaDbEngine, MySqlEngine } from "./engine.js"; +export type { MariaDbEngine, MySqlEngine, MySqlLexis } from "./engine.js"; export type { Analysis, Analyze, @@ -12,6 +11,7 @@ export type { KindOf, NamesOf, ParamsArgs, + SplicedText, } from "./infer.js"; export type { Lex } from "./lexer.js"; export type { ParseProgram, ParseStatement } from "./parser/stmt.js"; diff --git a/packages/core/src/mysql/engine/infer.ts b/packages/core/src/mysql/engine/infer.ts index 107b1d5a..78615f07 100644 --- a/packages/core/src/mysql/engine/infer.ts +++ b/packages/core/src/mysql/engine/infer.ts @@ -1,8 +1,11 @@ -import type * as Shared from "../../engine/infer.js"; +import type { QueryContext } from "../../engine/family/types.js"; +import type { Simplify } from "../../engine/object.js"; import type { MaxParam, ParamsToMap } from "../../engine/params.js"; import type { Toks } from "../../engine/tokens.js"; -import type { Cols, QueryContext, Simplify } from "../../engine/types.js"; +import type { Cols } from "../../engine/types.js"; import type { AnyErr } from "../../errors.js"; +import type * as Call from "../../sql/call.js"; +import type * as Shared from "../../sql/infer.js"; import type { RewriteNamed } from "../../sql/named.js"; import type { TsOf } from "../type-map/index.js"; import type { DmlStmt, SelectStmt, Stmt } from "./ast.js"; @@ -10,17 +13,17 @@ import type { ApplyDdlAll, ApplyOne } from "./ddl/apply.js"; import type { Reposition } from "./diagnostics.js"; import type { MySqlEngine } from "./engine.js"; import type { Lex } from "./lexer.js"; -import type { ParseProgram } from "./parser/stmt.js"; +import type { ParseProgramAt } from "./parser/stmt.js"; import type { ParamsOfDml, ProjectDml } from "./project/dml.js"; import type { ProjectSelect } from "./project/select.js"; import type { ParamsOfSelect } from "./typer/params.js"; export type Analyze = - RewriteNamed extends infer RN + RewriteNamed extends infer RN ? RN extends [infer Q1 extends string, infer Names] ? Lex extends infer T ? T extends Toks - ? ParseProgram extends infer S + ? ParseProgramAt extends infer S ? S extends Stmt ? ProjectStmt extends infer C ? C extends Cols | null @@ -114,3 +117,8 @@ export type Validate = Shared.Validate< export type KindOf = Shared.KindOf; export type ApplyDdl = ApplyDdlAll; + +export type SplicedText = Call.SplicedText< + Q, + MySqlEngine["lexis"] +>; diff --git a/packages/core/src/mysql/engine/lexer.ts b/packages/core/src/mysql/engine/lexer.ts index 6f6cdccf..a46e2c73 100644 --- a/packages/core/src/mysql/engine/lexer.ts +++ b/packages/core/src/mysql/engine/lexer.ts @@ -1,28 +1,40 @@ import type { Digit, HexDigit, + IdRun, IdStart, NonIdAscii, Ws, } from "../../engine/chars.js"; -import type { SqlError } from "../../errors.js"; -import type { Next } from "../../sql/next.js"; import type { - MyBlob, - MyIdent, - MyNum, - MyOp, - MyParam, - MyStr, - MyVar, - MyWord, -} from "./tokens.js"; - -type Nx = Next; + BadParam, + Blob, + ChunkSize, + DollarParam, + Escape, + Ident, + IsHex, + LexErr, + Num, + Op, + Param, + Str, + TakeQuoted, + TakeWord, + Word, +} from "../../engine/family/lex.js"; +import type { Next } from "../../engine/next.js"; +import type { SqlError } from "../../errors.js"; export type Lex = LexAll; -type ChunkSize = 200; +/** A user variable (`@name`) or system variable (`@@name`). */ +type MyVar = { + readonly k: "v"; + readonly v: V; + readonly r: R; + readonly g: G; +}; type LexAll< S extends string, @@ -48,7 +60,6 @@ type LexAll< : never : never; -type Err = ["err", M, Acc]; type Unrecognized = `You have an error in your SQL syntax near '${T}'`; @@ -61,7 +72,7 @@ type LexChunk< ? ["done", Acc] : B extends ChunkSize ? ["more", Acc, S, G] - : LexStep, G>; + : LexStep, G>; type Punct = "(" | ")" | "," | ";" | "~" | "+" | "*" | "%" | "^"; @@ -78,7 +89,7 @@ type LexStep< : C extends Digit ? LexNumber : C extends Punct - ? LexChunk], B, ""> + ? LexChunk], B, ""> : C extends "'" | '"' ? LexString : C extends "`" @@ -86,7 +97,7 @@ type LexStep< : C extends "." ? R extends `${Digit}${string}` ? LexNumber - : LexChunk], B, ""> + : LexChunk], B, ""> : LexSymbol : never; @@ -124,7 +135,7 @@ type LexLetter< B, G > - : LexChunk, W, G>], B, ""> + : LexChunk, W, G>], B, ""> : never : LexWord; @@ -140,12 +151,12 @@ type LexSymbol< ? R1 extends `${infer Line}\n${infer R2}` ? LexChunk : LexChunk<"", Acc, B, `${G}--${R1}`> - : LexChunk], B, ""> + : LexChunk], B, ""> : R extends `>>${infer R1}` - ? LexChunk>", G>], B, ""> + ? LexChunk>", G>], B, ""> : R extends `>${infer R1}` - ? LexChunk", G>], B, ""> - : LexChunk], B, ""> + ? LexChunk", G>], B, ""> + : LexChunk], B, ""> : C extends "#" ? R extends `${infer Line}\n${infer R2}` ? LexChunk @@ -154,50 +165,44 @@ type LexSymbol< ? R extends `*${infer R1}` ? R1 extends `${infer Body}*/${infer R2}` ? LexChunk - : Err<"Unclosed comment", Acc> - : LexChunk], B, ""> + : LexErr<"Unclosed comment", Acc> + : LexChunk], B, ""> : C extends "|" ? R extends `|${infer R1}` - ? LexChunk], B, ""> - : LexChunk], B, ""> + ? LexChunk], B, ""> + : LexChunk], B, ""> : C extends "&" ? R extends `&${infer R1}` - ? LexChunk], B, ""> - : LexChunk], B, ""> + ? LexChunk], B, ""> + : LexChunk], B, ""> : C extends "<" ? R extends `=>${infer R1}` - ? LexChunk", G>], B, ""> + ? LexChunk", G>], B, ""> : R extends `${infer O extends "=" | ">" | "<"}${infer R1}` - ? LexChunk], B, ""> - : LexChunk], B, ""> + ? LexChunk], B, ""> + : LexChunk], B, ""> : C extends ">" ? R extends `${infer O extends "=" | ">"}${infer R1}` - ? LexChunk${O}`, G>], B, ""> - : LexChunk", G>], B, ""> + ? LexChunk${O}`, G>], B, ""> + : LexChunk", G>], B, ""> : C extends "=" - ? LexChunk + ? LexChunk : C extends "!" ? R extends `=${infer R1}` - ? LexChunk - : LexChunk], B, ""> + ? LexChunk + : LexChunk], B, ""> : C extends ":" ? R extends `=${infer R1}` - ? LexChunk - : Err< - `TypeSQL parameters are written $1 or :name, not ":${IdRun}"`, - Acc - > + ? LexChunk + : LexErr}`>, Acc> : C extends "$" ? LexDollar : C extends "@" ? LexVar : C extends "?" - ? Err< - `TypeSQL parameters are written $1 or :name, not "?"`, - Acc - > + ? LexErr, Acc> : C extends NonIdAscii - ? Err}`>, Acc> + ? LexErr}`>, Acc> : LexWord; type Head = @@ -210,16 +215,10 @@ type LexDollar< Acc extends readonly unknown[], B extends number, G extends string, -> = R extends `${infer D extends Digit}${infer R1}` - ? TakeDigits extends [infer V extends string, infer R2 extends string] - ? IdRun extends "" - ? LexChunk, G>], B, ""> - : Err< - `TypeSQL parameters are written $1 or :name, not "$${V}${IdRun}"`, - Acc - > - : never - : Err<`TypeSQL parameters are written $1 or :name, not "$${IdRun}"`, Acc>; +> = + DollarParam extends [infer N extends number, infer R2 extends string] + ? LexChunk], B, ""> + : LexErr & string, Acc>; /** `@name`, `@'name'`, `@@name`, `@@session.name`. */ type LexVar< @@ -228,11 +227,14 @@ type LexVar< B extends number, G extends string, > = R extends `@${infer R1}` - ? VarName extends [infer N extends string, infer R2 extends string] + ? TakeWord extends [infer N extends string, infer R2 extends string] ? N extends "" - ? Err, Acc> + ? LexErr, Acc> : R2 extends `.${infer R3}` - ? VarName extends [infer N2 extends string, infer R4 extends string] + ? TakeWord extends [ + infer N2 extends string, + infer R4 extends string, + ] ? LexChunk< R4, [...Acc, MyVar<`@@${Lowercase}`, `@@${N}.${N2}`, G>], @@ -250,20 +252,12 @@ type LexVar< B, "" > - : Err, Acc> - : VarName extends [infer N extends string, infer R2 extends string] + : LexErr, Acc> + : TakeWord extends [infer N extends string, infer R2 extends string] ? N extends "" - ? Err, Acc> + ? LexErr, Acc> : LexChunk}`, `@${N}`, G>], B, ""> : never; -type VarName< - S extends string, - Acc extends string = "", -> = S extends `${infer C}${infer R}` - ? C extends NonIdAscii - ? [Acc, S] - : VarName - : [Acc, S]; type LexWord< R extends string, @@ -273,7 +267,7 @@ type LexWord< G extends string, > = TakeWord extends [infer W extends string, infer R2 extends string] - ? LexChunk, W, G>], B, ""> + ? LexChunk, W, G>], B, ""> : never; type LexHexString< @@ -284,17 +278,9 @@ type LexHexString< G extends string, > = R1 extends `${infer Hex}'${infer R2}` ? IsHex extends true - ? LexChunk], B, ""> - : Err, Acc> - : Err, Acc>; -type IsHex< - S extends string, - Even extends boolean, -> = S extends `${infer C}${infer R}` - ? C extends HexDigit - ? IsHex - : false - : Even; + ? LexChunk], B, ""> + : LexErr, Acc> + : LexErr, Acc>; type LexBitString< R1 extends string, @@ -304,9 +290,9 @@ type LexBitString< G extends string, > = R1 extends `${infer Bits}'${infer R2}` ? IsBits extends true - ? LexChunk], B, ""> - : Err, Acc> - : Err, Acc>; + ? LexChunk], B, ""> + : LexErr, Acc> + : LexErr, Acc>; type IsBits = S extends `${infer C}${infer R}` ? C extends "0" | "1" ? IsBits @@ -341,14 +327,14 @@ type LexString< [ ...Acc, Kind extends "blob" - ? MyBlob<`${V}${V2}`, `${Open}${Raw}${Q}${Raw2}`, G> - : MyStr<`${V}${V2}`, `${Open}${Raw}${Q}${Raw2}`, G, V>, + ? Blob<`${V}${V2}`, `${Open}${Raw}${Q}${Raw2}`, G> + : Str<`${V}${V2}`, `${Open}${Raw}${Q}${Raw2}`, G, V>, ], B, "" > : never - : Err}`>, Acc>; + : LexErr}`>, Acc>; /** MySQL joins adjacent string literals: `'a' 'b'` is `'ab'`. */ type AdjacentString< @@ -421,32 +407,14 @@ type LexQuoted< infer R2 extends string, ] ? V extends "" - ? Err, Acc> + ? LexErr, Acc> : LexChunk< R2, - [...Acc, MyIdent, V, `\`${Escape}\``, G>], + [...Acc, Ident, V, `\`${Escape}\``, G>], B, "" > - : Err}`>, Acc>; - -type TakeQuoted< - S extends string, - Qc extends string, - Acc extends string, -> = S extends `${infer Body}${Qc}${infer R}` - ? R extends `${Qc}${infer R2}` - ? TakeQuoted - : [`${Acc}${Body}`, R] - : null; - -type Escape< - S extends string, - Q extends string, - Acc extends string = "", -> = S extends `${infer A}${Q}${infer B}` - ? Escape - : `${Acc}${S}`; + : LexErr}`>, Acc>; /** * A number, or a word that starts with digits (MySQL identifiers may), and the @@ -465,17 +433,17 @@ type LexNumber< ? H extends "" ? LexWord : IdRun extends "" - ? LexChunk], B, ""> + ? LexChunk], B, ""> : LexWord : never : TakeDecimal extends [infer V extends string, infer R2 extends string] ? IdRun extends "" - ? LexChunk], B, ""> + ? LexChunk], B, ""> : V extends `${string}${"." | "e" | "E"}${string}` - ? Err}`>, Acc> + ? LexErr}`>, Acc> : LexWord : TakeDecimal extends infer Bad extends string - ? Err, Acc> + ? LexErr, Acc> : never; type HexRun< S extends string, @@ -522,42 +490,3 @@ type TakeExponent< : [`${V}${E}${X}`, R3] : never : [V, S]; - -type TakeDigits< - S extends string, - Acc extends string, -> = S extends `${infer C extends Digit}${infer R}` - ? TakeDigits - : [Acc, S]; - -type IdRun< - R extends string, - Acc extends string = "", -> = R extends `${infer C}${infer R1}` - ? C extends NonIdAscii - ? Acc - : IdRun - : Acc; - -type TakeWord< - S extends string, - Acc extends string, -> = S extends `${infer A}${infer B}${infer C}${infer D}${infer R}` - ? A extends NonIdAscii - ? [Acc, S] - : B extends NonIdAscii - ? [`${Acc}${A}`, `${B}${C}${D}${R}`] - : C extends NonIdAscii - ? [`${Acc}${A}${B}`, `${C}${D}${R}`] - : D extends NonIdAscii - ? [`${Acc}${A}${B}${C}`, `${D}${R}`] - : TakeWord - : S extends `${infer C}${infer R}` - ? C extends NonIdAscii - ? [Acc, S] - : TakeWord - : [Acc, S]; - -type ToNum = S extends `${infer N extends number}` - ? N - : never; diff --git a/packages/core/src/mysql/engine/parser/ddl.ts b/packages/core/src/mysql/engine/parser/ddl.ts index 286d5372..bba6b9a5 100644 --- a/packages/core/src/mysql/engine/parser/ddl.ts +++ b/packages/core/src/mysql/engine/parser/ddl.ts @@ -1,3 +1,16 @@ +import type { + CloseOk, + IfExists, + IfNotExists, + IsOp, + IsWord, + Name, + SkipColumn, + SkipStatement, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { AlterAction, ColumnDef, @@ -8,45 +21,7 @@ import type { import type { IsSubqueryParen } from "./expr.js"; import type { ParseSubquery } from "./select.js"; import type { ParseColumnType } from "./typename.js"; -import type { - CloseOk, - Ident, - IsOp, - IsWord, - Name, - Nx, - Ok, - OptCols, - SyntaxErr, - TableRefAt, - Toks, -} from "./util.js"; - -type IfNotExists = - IsWord extends true - ? IsWord, "not"> extends true - ? IsWord>, "exists"> extends true - ? [true, Nx>>] - : SyntaxErr>> - : SyntaxErr> - : [false, I]; -type IfExists = - IsWord extends true - ? IsWord, "exists"> extends true - ? [true, Nx>] - : SyntaxErr> - : [false, I]; - -/** Skips to the end of the statement. */ -export type SkipStatement< - T extends Toks, - I extends number, - What extends string, -> = T[I] extends { readonly k: "o"; readonly v: ";" } - ? Ok<{ t: "noop"; what: What }, I> - : T[I] extends { readonly k: string } - ? SkipStatement, What> - : Ok<{ t: "noop"; what: What }, I>; +import type { Ident, OptCols, TableRefAt } from "./util.js"; /** Skips the rest of the input: a stored program body can contain `;`. */ type SkipAll< @@ -54,7 +29,7 @@ type SkipAll< I extends number, What extends string, > = T[I] extends { readonly k: string } - ? SkipAll, What> + ? SkipAll, What> : Ok<{ t: "noop"; what: What }, I>; /** Skips a parenthesized group starting at `(`; returns the index after `)`. */ @@ -63,15 +38,15 @@ type SkipParens< I extends number, Depth extends readonly unknown[] = [], > = T[I] extends { readonly k: "o"; readonly v: "(" } - ? SkipParens, [...Depth, 0]> + ? SkipParens, [...Depth, 0]> : T[I] extends { readonly k: "o"; readonly v: ")" } ? Depth extends readonly [unknown, ...infer D] ? D extends readonly [] - ? Nx - : SkipParens, D> + ? Next + : SkipParens, D> : SyntaxErr : T[I] extends { readonly k: string } - ? SkipParens, Depth> + ? SkipParens, Depth> : SyntaxErr; /** Skips to the `,` or `)` that ends a table element or alter action. */ @@ -85,34 +60,34 @@ type SkipItem = T[I] extends { ? SkipParens extends infer J extends number ? SkipItem : SkipParens - : SkipItem> + : SkipItem> : T[I] extends { readonly k: string } - ? SkipItem> + ? SkipItem> : I; export type ParseCreate = ( IsWord extends true - ? IsWord, "replace"> extends true - ? [true, Nx>] - : SyntaxErr> + ? IsWord, "replace"> extends true + ? [true, Next>] + : SyntaxErr> : [false, I] ) extends [infer OrReplace extends boolean, infer I1 extends number] ? SkipViewOptions extends infer I2 extends number ? IsWord extends true - ? ParseCreateView, OrReplace> + ? ParseCreateView, OrReplace> : ( IsWord extends true - ? Nx + ? Next : I2 ) extends infer J extends number ? IsWord extends true - ? ParseCreateTable> + ? ParseCreateTable> : IsWord extends true - ? ParseCreateIndex> + ? ParseCreateIndex> : IsWord extends true - ? IsWord, "index"> extends true - ? ParseCreateIndex>> - : SyntaxErr> + ? IsWord, "index"> extends true + ? ParseCreateIndex>> + : SyntaxErr> : IsWord< T, J, @@ -150,25 +125,25 @@ type SkipViewOptions = T[I] extends { readonly v: infer W; } ? W extends "algorithm" | "definer" - ? IsOp, "="> extends true - ? SkipDefinerValue>> - : SyntaxErr> + ? IsOp, "="> extends true + ? SkipDefinerValue>> + : SyntaxErr> : W extends "sql" - ? IsWord, "security"> extends true - ? SkipViewOptions>>> - : SyntaxErr> + ? IsWord, "security"> extends true + ? SkipViewOptions>>> + : SyntaxErr> : I : I; /** `user`, `'user'@'host'`, `current_user[()]`. */ -type SkipDefinerValue = T[Nx] extends { +type SkipDefinerValue = T[Next] extends { readonly k: "v"; } - ? SkipViewOptions>> - : IsOp, "("> extends true - ? IsOp>, ")"> extends true - ? SkipViewOptions>>> - : SkipViewOptions> - : SkipViewOptions>; + ? SkipViewOptions>> + : IsOp, "("> extends true + ? IsOp>, ")"> extends true + ? SkipViewOptions>>> + : SkipViewOptions> + : SkipViewOptions>; type ParseCreateTable = IfNotExists extends [infer Ine extends boolean, infer J extends number] @@ -177,18 +152,18 @@ type ParseCreateTable = infer J2 extends number > ? IsWord extends true - ? CreateLike, Ine, Table> + ? CreateLike, Ine, Table> : IsOp extends true - ? IsWord, "like"> extends true - ? CreateLike>, Ine, Table> extends Ok< + ? IsWord, "like"> extends true + ? CreateLike>, Ine, Table> extends Ok< infer S, infer J3 extends number > ? CloseOk - : CreateLike>, Ine, Table> - : IsSubqueryParen> extends true + : CreateLike>, Ine, Table> + : IsSubqueryParen> extends true ? CtasQuery - : TableElements, [], []> extends Ok< + : TableElements, [], []> extends Ok< [ infer Cols extends readonly ColumnDef[], infer Pk extends readonly string[], @@ -209,7 +184,7 @@ type ParseCreateTable = J4 > : never - : TableElements, [], []> + : TableElements, [], []> : SkipTableOptions extends infer J3 extends number ? AtQuery extends true ? CtasQuery @@ -249,10 +224,10 @@ type CtasQuery< Cols extends readonly ColumnDef[], > = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number - ? (IsWord extends true ? Nx : J) extends infer J2 extends + ? (IsWord extends true ? Next : J) extends infer J2 extends number ? ParseSubquery extends Ok< infer S extends SelectStmt, @@ -283,7 +258,7 @@ type SkipTableOptions = ? SkipTableOptions : I : T[I] extends { readonly k: string } - ? SkipTableOptions> + ? SkipTableOptions> : I; type ApplyPk< @@ -306,7 +281,7 @@ type TableElements< infer J extends number > ? IsOp extends true - ? TableElements, [...Cols, ...C2], [...Pk, ...P2]> + ? TableElements, [...Cols, ...C2], [...Pk, ...P2]> : CloseOk : TableElement; /** One column definition or constraint: its columns and primary key names. */ @@ -332,7 +307,7 @@ type IsConstraintStart = | "spatial" | "period" > extends true - ? IsOp, "."> extends true + ? IsOp, "."> extends true ? false : true : false; @@ -340,29 +315,33 @@ type IsConstraintStart = /** A table constraint or index: only `PRIMARY KEY (…)` changes the columns. */ type TableConstraint = ( IsWord extends true - ? IsWord, "primary" | "unique" | "foreign" | "check"> extends true - ? Nx - : Nx> + ? IsWord< + T, + Next, + "primary" | "unique" | "foreign" | "check" + > extends true + ? Next + : Next> : I ) extends infer J extends number ? IsWord extends true - ? IsWord, "key"> extends true - ? KeyParts>> extends Ok< + ? IsWord, "key"> extends true + ? KeyParts>> extends Ok< infer Cols extends readonly string[], infer J2 extends number > ? Ok<[[], Cols], SkipItem> - : KeyParts>> - : SyntaxErr> + : KeyParts>> + : SyntaxErr> : Ok<[[], []], SkipItem> : never; /** `[USING BTREE] (a, b(10) DESC, (expr))`: the plain column names. */ type KeyParts = IsOp extends true - ? KeyPartLoop, []> + ? KeyPartLoop, []> : T[I] extends { readonly k: "w" | "id" } - ? KeyParts> + ? KeyParts> : SyntaxErr; type KeyPartLoop< T extends Toks, @@ -372,13 +351,13 @@ type KeyPartLoop< KeyPart extends [infer C extends readonly string[], infer J] ? J extends number ? IsOp extends true - ? KeyPartLoop, [...Acc, ...C]> + ? KeyPartLoop, [...Acc, ...C]> : CloseOk : J : KeyPart; type KeyPart = Ident extends infer N extends Name - ? [[N["v"]], SkipKeyPart>] + ? [[N["v"]], SkipKeyPart>] : IsOp extends true ? [[], SkipKeyPart] : SyntaxErr; @@ -394,7 +373,7 @@ type SkipKeyPart = T[I] extends { : SkipParens : SyntaxErr : T[I] extends { readonly k: "w"; readonly v: "asc" | "desc" } - ? SkipKeyPart> + ? SkipKeyPart> : SyntaxErr; type ColState = { @@ -425,7 +404,7 @@ type SerialInit = With< type ParseColumnDef = Ident extends infer N extends Name - ? ParseColumnType> extends Ok< + ? ParseColumnType> extends Ok< { readonly type: infer Ty extends string; readonly enumValues: infer Ev; @@ -451,7 +430,7 @@ type ParseColumnDef = J2 > : ColumnAttrs - : ParseColumnType> + : ParseColumnType> : SyntaxErr; type ColumnAttrs< @@ -460,45 +439,47 @@ type ColumnAttrs< S extends ColState, > = T[I] extends { readonly k: "w"; readonly v: infer W extends string } ? W extends "not" - ? IsWord, "null"> extends true - ? ColumnAttrs>, With> - : SyntaxErr> + ? IsWord, "null"> extends true + ? ColumnAttrs>, With> + : SyntaxErr> : W extends "null" ? ColumnAttrs< T, - Nx, + Next, With > : W extends "default" - ? AttrsAt>, With> + ? AttrsAt>, With> : W extends "auto_increment" ? ColumnAttrs< T, - Nx, + Next, With, "hasDefault", true> > : W extends "primary" - ? IsWord, "key"> extends true - ? ColumnAttrs>, With> - : SyntaxErr> + ? IsWord, "key"> extends true + ? ColumnAttrs>, With> + : SyntaxErr> : W extends "key" - ? ColumnAttrs, With> + ? ColumnAttrs, With> : W extends "unique" ? ColumnAttrs< T, - IsWord, "key"> extends true ? Nx> : Nx, + IsWord, "key"> extends true + ? Next> + : Next, S > : W extends "generated" - ? IsWord, "always"> extends true - ? IsWord>, "as"> extends true - ? Generated>>, S> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "always"> extends true + ? IsWord>, "as"> extends true + ? Generated>>, S> + : SyntaxErr>> + : SyntaxErr> : W extends "as" - ? Generated, S> + ? Generated, S> : W extends "invisible" - ? ColumnAttrs, With> + ? ColumnAttrs, With> : W extends | "visible" | "virtual" @@ -511,7 +492,7 @@ type ColumnAttrs< | "unsigned" | "signed" | "zerofill" - ? ColumnAttrs, S> + ? ColumnAttrs, S> : W extends | "comment" | "collate" @@ -524,21 +505,21 @@ type ColumnAttrs< | "compressed" ? ColumnAttrs< T, - IsOp, "="> extends true - ? Nx>> - : Nx>, + IsOp, "="> extends true + ? Next>> + : Next>, S > : W extends "character" - ? IsWord, "set"> extends true - ? ColumnAttrs>>, S> - : SyntaxErr> + ? IsWord, "set"> extends true + ? ColumnAttrs>>, S> + : SyntaxErr> : W extends "on" - ? IsWord, "update"> extends true - ? AttrsAt>>, S> - : SyntaxErr> + ? IsWord, "update"> extends true + ? AttrsAt>>, S> + : SyntaxErr> : W extends "references" - ? TableRefAt> extends Ok< + ? TableRefAt> extends Ok< unknown, infer J extends number > @@ -549,24 +530,24 @@ type ColumnAttrs< : J, S > - : SyntaxErr> + : SyntaxErr> : W extends "constraint" ? ColumnAttrs< T, - IsWord, "check"> extends true - ? Nx - : Nx>, + IsWord, "check"> extends true + ? Next + : Next>, S > : W extends "check" - ? IsOp, "("> extends true + ? IsOp, "("> extends true ? SkipParens< T, - Nx + Next > extends infer J extends number ? ColumnAttrs, S> - : SkipParens> - : SyntaxErr> + : SkipParens> + : SyntaxErr> : Ok : T[I] extends { readonly k: "o"; readonly v: "," | ")" | ";" } ? Ok @@ -580,22 +561,22 @@ type AttrsAt = J extends number type SkipEnforced = IsWord extends true - ? Nx + ? Next : IsWord extends true - ? IsWord, "enforced"> extends true - ? Nx> + ? IsWord, "enforced"> extends true + ? Next> : I : I; /** `REFERENCES t (a) [MATCH x] [ON DELETE action] [ON UPDATE action]`. */ type SkipReference = I extends number ? IsWord extends true - ? SkipReference>, S> + ? SkipReference>, S> : IsWord extends true - ? IsWord, "delete" | "update"> extends true - ? IsWord>, "set" | "no"> extends true - ? SkipReference>>>, S> - : SkipReference>>, S> + ? IsWord, "delete" | "update"> extends true + ? IsWord>, "set" | "no"> extends true + ? SkipReference>>>, S> + : SkipReference>>, S> : ColumnAttrs : ColumnAttrs : I; @@ -614,22 +595,22 @@ type SkipDefault = IsOp extends true ? SkipParens : IsOp extends true - ? T[Nx] extends { readonly k: "n" } - ? Nx> - : SyntaxErr> + ? T[Next] extends { readonly k: "n" } + ? Next> + : SyntaxErr> : T[I] extends { readonly k: "n" | "s" } - ? Nx + ? Next : T[I] extends { readonly k: "w" } ? SkipCallable : SyntaxErr; type SkipCallable = T[I] extends { readonly k: "w"; } - ? IsOp, "("> extends true - ? SkipParens> - : T[Nx] extends { readonly k: "s" } - ? Nx> - : Nx + ? IsOp, "("> extends true + ? SkipParens> + : T[Next] extends { readonly k: "s" } + ? Next> + : Next : SyntaxErr; type ParseCreateView< @@ -641,7 +622,7 @@ type ParseCreateView< ? TableRefAt extends Ok ? OptCols extends Ok ? IsWord extends true - ? ParseSubquery> extends Ok< + ? ParseSubquery> extends Ok< infer S extends SelectStmt, infer J4 extends number > @@ -656,7 +637,7 @@ type ParseCreateView< }, SkipCheckOption > - : ParseSubquery> + : ParseSubquery> : SyntaxErr : OptCols : TableRefAt @@ -665,13 +646,13 @@ type ParseCreateView< type SkipCheckOption = IsWord extends true ? ( - IsWord, "cascaded" | "local"> extends true - ? Nx> - : Nx + IsWord, "cascaded" | "local"> extends true + ? Next> + : Next ) extends infer J extends number ? IsWord extends true - ? IsWord, "option"> extends true - ? Nx> + ? IsWord, "option"> extends true + ? Next> : I : I : I @@ -682,12 +663,12 @@ type ParseCreateIndex = IfNotExists extends [boolean, infer I0 extends number] ? Ident extends Name ? ( - IsWord, "using"> extends true - ? Nx>> - : Nx + IsWord, "using"> extends true + ? Next>> + : Next ) extends infer J extends number ? IsWord extends true - ? TableRefAt> extends Ok< + ? TableRefAt> extends Ok< infer Table, infer J2 extends number > @@ -699,7 +680,7 @@ type ParseCreateIndex = ? Ok<{ t: "create_index"; table: Table }, J3> : never : SyntaxErr - : TableRefAt> + : TableRefAt> : SyntaxErr : never : SyntaxErr @@ -707,14 +688,14 @@ type ParseCreateIndex = export type ParseDrop = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? T[J] extends { readonly k: "w"; readonly v: infer K extends "table" | "view" | "tables"; } - ? IfExists> extends [ + ? IfExists> extends [ infer Ie extends boolean, infer J2 extends number, ] @@ -726,12 +707,12 @@ export type ParseDrop = ( ifExists: Ie; names: Names; }, - IsWord extends true ? Nx : J3 + IsWord extends true ? Next : J3 > : DropNames - : IfExists> + : IfExists> : IsWord extends true - ? ParseDropIndex> + ? ParseDropIndex> : SkipStatement : never; @@ -745,15 +726,15 @@ type DropNames< infer J extends number > ? IsOp extends true - ? DropNames, [...Acc, Ref]> + ? DropNames, [...Acc, Ref]> : Ok<[...Acc, Ref], J> : TableRefAt; type ParseDropIndex = IfExists extends [boolean, infer I0 extends number] ? Ident extends Name - ? IsWord, "on"> extends true - ? TableRefAt>> extends Ok< + ? IsWord, "on"> extends true + ? TableRefAt>> extends Ok< infer Table, infer J extends number > @@ -763,14 +744,14 @@ type ParseDropIndex = > ? Ok<{ t: "drop_index"; table: Table }, J2> : never - : TableRefAt>> - : SyntaxErr> + : TableRefAt>> + : SyntaxErr> : SyntaxErr : IfExists; export type ParseRename = IsWord extends true - ? RenamePairs, []> + ? RenamePairs, []> : SkipStatement; type RenamePairs< T extends Toks, @@ -779,9 +760,9 @@ type RenamePairs< > = TableRefAt extends Ok ? IsWord extends true - ? TableRefAt> extends Ok + ? TableRefAt> extends Ok ? IsOp extends true - ? RenamePairs, [...Acc, { from: From; to: To }]> + ? RenamePairs, [...Acc, { from: From; to: To }]> : Ok< { t: "rename_table"; @@ -789,13 +770,13 @@ type RenamePairs< }, J2 > - : TableRefAt> + : TableRefAt> : SyntaxErr : TableRefAt; export type ParseTruncate = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? TableRefAt extends Ok @@ -805,11 +786,11 @@ export type ParseTruncate = ( export type ParseAlter = ( IsWord extends true - ? Nx + ? Next : I ) extends infer I1 extends number ? IsWord extends true - ? IfExists> extends [boolean, infer J extends number] + ? IfExists> extends [boolean, infer J extends number] ? TableRefAt extends Ok ? AlterActions extends Ok< infer Actions, @@ -818,10 +799,10 @@ export type ParseAlter = ( ? Ok<{ t: "alter_table"; table: Table; actions: Actions }, J3> : AlterActions : TableRefAt - : IfExists> + : IfExists> : SkipViewOptions extends infer I2 extends number ? IsWord extends true - ? ParseCreateView, true> + ? ParseCreateView, true> : SkipStatement : SkipViewOptions : never; @@ -838,18 +819,18 @@ type AlterActions< infer J extends number > ? IsOp extends true - ? AlterActions, [...Acc, ...As]> + ? AlterActions, [...Acc, ...As]> : Ok<[...Acc, ...As], J> : AlterAction_ : Ok; type Place = IsWord extends true - ? Ok<{ t: "first" }, Nx> + ? Ok<{ t: "first" }, Next> : IsWord extends true - ? Ident> extends infer N extends Name - ? Ok<{ t: "after"; col: N["v"] }, Nx>> - : SyntaxErr> + ? Ident> extends infer N extends Name + ? Ok<{ t: "after"; col: N["v"] }, Next>> + : SyntaxErr> : Ok; type ColumnWithPlace = @@ -865,25 +846,22 @@ type ColumnWithPlace = : Place : ParseColumnDef; -type SkipColumn = - IsWord extends true ? Nx : I; - type AlterAction_ = T[I] extends { readonly k: "w"; readonly v: infer W extends string; } ? W extends "add" - ? AddAction> + ? AddAction> : W extends "drop" - ? DropAction> + ? DropAction> : W extends "modify" - ? ChangeAction>, false> + ? ChangeAction>, false> : W extends "change" - ? ChangeAction>, true> + ? ChangeAction>, true> : W extends "rename" - ? RenameAction> + ? RenameAction> : W extends "alter" - ? AlterColumnAction> + ? AlterColumnAction> : Ok<[{ t: "noop" }], SkipItem> : SyntaxErr; @@ -891,12 +869,12 @@ type AlterAction_ = T[I] extends { type ChangeAction = IfExists extends [boolean, infer J1 extends number] ? Ident extends infer N extends Name - ? ColumnWithPlace : J1> extends Ok< + ? ColumnWithPlace : J1> extends Ok< [infer Col, infer Pl], infer J2 extends number > ? Ok<[{ t: "change_column"; from: N["v"]; col: Col; place: Pl }], J2> - : ColumnWithPlace : J1> + : ColumnWithPlace : J1> : SyntaxErr : IfExists; @@ -919,7 +897,7 @@ type AddAction = : AddConstraint : SkipColumn extends infer J extends number ? IsOp extends true - ? AddColumnList, []> + ? AddColumnList, []> : IfNotExists extends [ infer Ine extends boolean, infer J1 extends number, @@ -945,7 +923,7 @@ type AddColumnList< infer J extends number > ? IsOp extends true - ? AddColumnList, [...Acc, A]> + ? AddColumnList, [...Acc, A]> : CloseOk : AddListItem; type AddListItem = @@ -976,9 +954,9 @@ type DropAction = ? Ident extends infer N extends Name ? Ok< [{ t: "drop_column"; name: N["v"]; ifExists: Ie }], - IsWord, "restrict" | "cascade"> extends true - ? Nx> - : Nx + IsWord, "restrict" | "cascade"> extends true + ? Next> + : Next > : SyntaxErr : IfExists @@ -986,21 +964,21 @@ type DropAction = type RenameAction = IsWord extends true - ? Ident> extends infer A extends Name - ? IsWord>, "to"> extends true - ? Ident>>> extends infer B extends Name + ? Ident> extends infer A extends Name + ? IsWord>, "to"> extends true + ? Ident>>> extends infer B extends Name ? Ok< [{ t: "rename_column"; from: A["v"]; to: B["n"] }], - Nx>>> + Next>>> > - : SyntaxErr>>> - : SyntaxErr>> - : SyntaxErr> + : SyntaxErr>>> + : SyntaxErr>> + : SyntaxErr> : IsWord extends true ? Ok<[{ t: "noop" }], SkipItem> : ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? TableRefAt extends Ok @@ -1013,30 +991,31 @@ type AlterColumnAction = ? Ok<[{ t: "noop" }], SkipItem> : SkipColumn extends infer J extends number ? Ident extends infer N extends Name - ? IsWord, "set"> extends true - ? IsWord>, "default"> extends true - ? SkipDefault>>> extends infer J2 extends number + ? IsWord, "set"> extends true + ? IsWord>, "default"> extends true + ? SkipDefault>>> extends infer J2 extends + number ? Ok<[{ t: "column_default"; name: N["v"]; has: true }], J2> - : SkipDefault>>> - : IsWord>, "visible" | "invisible"> extends true + : SkipDefault>>> + : IsWord>, "visible" | "invisible"> extends true ? Ok< [ { t: "column_visibility"; name: N["v"]; - invisible: IsWord>, "invisible">; + invisible: IsWord>, "invisible">; }, ], - Nx>> + Next>> > - : SyntaxErr>> - : IsWord, "drop"> extends true - ? IsWord>, "default"> extends true + : SyntaxErr>> + : IsWord, "drop"> extends true + ? IsWord>, "default"> extends true ? Ok< [{ t: "column_default"; name: N["v"]; has: false }], - Nx>> + Next>> > - : SyntaxErr>> - : SyntaxErr> + : SyntaxErr>> + : SyntaxErr> : SyntaxErr : never; diff --git a/packages/core/src/mysql/engine/parser/dml.ts b/packages/core/src/mysql/engine/parser/dml.ts index 65203eac..83aaa4f8 100644 --- a/packages/core/src/mysql/engine/parser/dml.ts +++ b/packages/core/src/mysql/engine/parser/dml.ts @@ -1,3 +1,13 @@ +import type { + AliasAt, + IsOp, + IsWord, + Name, + NamesV, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { Expr, FromItem, @@ -15,20 +25,7 @@ import type { ParseTargets, ParseValues, } from "./select.js"; -import type { - AliasAt, - Ident, - IdentList, - IsOp, - IsWord, - Name, - NamesV, - Nx, - Ok, - SyntaxErr, - TableRefAt, - Toks, -} from "./util.js"; +import type { Ident, IdentList, TableRefAt } from "./util.js"; type SkipWords< T extends Toks, @@ -37,35 +34,38 @@ type SkipWords< Ignore extends boolean = false, > = T[I] extends { readonly k: "w"; readonly v: infer V } ? V extends W - ? SkipWords, W, V extends "ignore" ? true : Ignore> + ? SkipWords, W, V extends "ignore" ? true : Ignore> : [Ignore, I] : [Ignore, I]; type SkipPartition = IsWord extends true - ? IdentList> extends Ok + ? IdentList> extends Ok ? J - : IdentList> + : IdentList> : I; /** `(a, b)`, `()` or a parenthesized query (no column list) after the table. */ type InsertCols = IsOp extends true - ? IsSubqueryParen> extends true + ? IsSubqueryParen> extends true ? Ok - : IsOp, ")"> extends true - ? Ok<[], Nx>> + : IsOp, ")"> extends true + ? Ok<[], Next>> : IdentList : Ok; export type ParseInsert = SkipWords< T, - Nx, + Next, "low_priority" | "delayed" | "high_priority" | "ignore" > extends [infer Ignore extends boolean, infer J0 extends number] - ? (IsWord extends true ? Nx : J0) extends infer J extends - number + ? ( + IsWord extends true + ? Next + : J0 + ) extends infer J extends number ? TableRefAt extends Ok ? SkipPartition extends infer J3 extends number ? InsertCols extends Ok @@ -105,16 +105,16 @@ export type ParseInsert = type InsertSource = IsWord extends true - ? ParseValues, [], IsWord, "row">> extends Ok< + ? ParseValues, [], IsWord, "row">> extends Ok< infer Rows extends readonly (readonly Expr[])[], infer J extends number > ? Ok<{ t: "values"; rows: Rows }, J> - : ParseValues, [], IsWord, "row">> + : ParseValues, [], IsWord, "row">> : IsWord extends true - ? ParseSet, []> extends Ok + ? ParseSet, []> extends Ok ? Ok<{ t: "set"; set: Set }, J> - : ParseSet, []> + : ParseSet, []> : IsWord extends true ? SelectSource : IsOp extends true @@ -131,27 +131,27 @@ type SelectSource = /** `AS new [(a, b)]` after the inserted rows. */ type RowAlias = IsWord extends true - ? Ident> extends infer N extends Name - ? IsOp>, "("> extends true - ? IdentList>> extends Ok< + ? Ident> extends infer N extends Name + ? IsOp>, "("> extends true + ? IdentList>> extends Ok< infer Cols extends readonly Name[], infer J extends number > ? Ok<{ name: N["v"]; cols: NamesV }, J> - : IdentList>> - : Ok<{ name: N["v"]; cols: null }, Nx>> - : SyntaxErr> + : IdentList>> + : Ok<{ name: N["v"]; cols: null }, Next>> + : SyntaxErr> : Ok; type Odku = IsWord extends true - ? IsWord, "duplicate"> extends true - ? IsWord>, "key"> extends true - ? IsWord>>, "update"> extends true - ? ParseSet>>>, []> - : SyntaxErr>>> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "duplicate"> extends true + ? IsWord>, "key"> extends true + ? IsWord>>, "update"> extends true + ? ParseSet>>>, []> + : SyntaxErr>>> + : SyntaxErr>> + : SyntaxErr> : Ok; /** `a = 1, t.b = DEFAULT, …`. */ @@ -172,42 +172,42 @@ type ParseSet< ? [...Acc, { tbl: Tbl; col: Col; e: E }] extends infer Acc2 extends readonly SetItem[] ? IsOp extends true - ? ParseSet, Acc2> + ? ParseSet, Acc2> : Ok : never : ParseExpr : SetEq : SetTarget; type SetEq = - IsOp extends true - ? Ident>> extends infer B extends Name - ? IsOp>>, "."> extends true - ? Ident>>>> extends infer C extends Name - ? Ok<[B["v"], C["v"]], Nx>>>>> - : SyntaxErr>>>> - : Ok<[A["v"], B["v"]], Nx>>> - : SyntaxErr>> - : Ok<[null, A["v"]], Nx> + ? IsOp, "."> extends true + ? Ident>> extends infer B extends Name + ? IsOp>>, "."> extends true + ? Ident>>>> extends infer C extends Name + ? Ok<[B["v"], C["v"]], Next>>>>> + : SyntaxErr>>>> + : Ok<[A["v"], B["v"]], Next>>> + : SyntaxErr>> + : Ok<[null, A["v"]], Next> : SyntaxErr; type Returning = IsWord extends true - ? ParseTargets, []> extends Ok< + ? ParseTargets, []> extends Ok< infer Tg extends readonly Target[], infer J extends number > ? Ok - : ParseTargets, []> + : ParseTargets, []> : Ok; type OptOrder = IsWord extends true - ? IsWord, "by"> extends true - ? ParseOrderList>, []> - : SyntaxErr> + ? IsWord, "by"> extends true + ? ParseOrderList>, []> + : SyntaxErr> : Ok<[], I>; /** `[ORDER BY …] [LIMIT n]` of a single-table UPDATE or DELETE: no OFFSET. */ @@ -224,7 +224,7 @@ type OrderLimit = : OptOrder; type Where = - IsWord extends true ? ParseExpr> : Ok; + IsWord extends true ? ParseExpr> : Ok; export type ParseUpdate = SkipWords extends [ @@ -236,7 +236,7 @@ export type ParseUpdate = infer J2 extends number > ? IsWord extends true - ? ParseSet, []> extends Ok< + ? ParseSet, []> extends Ok< infer Set extends readonly SetItem[], infer J3 extends number > @@ -259,7 +259,7 @@ export type ParseUpdate = > : OrderLimit : Where - : ParseSet, []> + : ParseSet, []> : SyntaxErr : ParseFrom : never; @@ -276,15 +276,15 @@ type DeleteTargets< > ? SkipDotStar extends infer J2 extends number ? IsOp extends true - ? DeleteTargets, [...Acc, Ref]> + ? DeleteTargets, [...Acc, Ref]> : Ok<[...Acc, Ref], J2> : SkipDotStar : TableRefAt; type SkipDotStar = IsOp extends true - ? IsOp, "*"> extends true - ? Nx> - : SyntaxErr> + ? IsOp, "*"> extends true + ? Next> + : SyntaxErr> : I; export type ParseDelete = @@ -293,22 +293,22 @@ export type ParseDelete = infer J extends number, ] ? IsWord extends true - ? DeleteTargets, []> extends Ok< + ? DeleteTargets, []> extends Ok< infer Targets extends readonly TableRef[], infer J2 extends number > ? IsWord extends true - ? MultiDelete, W, Targets> + ? MultiDelete, W, Targets> : Targets extends readonly [infer Ref extends TableRef] - ? SingleDelete, W, Ref> + ? SingleDelete, W, Ref> : SyntaxErr - : DeleteTargets, []> + : DeleteTargets, []> : DeleteTargets extends Ok< infer Targets extends readonly TableRef[], infer J2 extends number > ? IsWord extends true - ? MultiDelete, W, Targets> + ? MultiDelete, W, Targets> : SyntaxErr : DeleteTargets : never; @@ -316,13 +316,13 @@ export type ParseDelete = /** The optional alias of a single-table DELETE's target. */ type DeleteAlias = IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? Ok>> - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? Ok>> + : SyntaxErr> : Ident extends infer A extends Name ? A["v"] extends "returning" | "partition" | "order" | "limit" ? Ok - : Ok> + : Ok> : Ok; type SingleDelete = diff --git a/packages/core/src/mysql/engine/parser/expr.ts b/packages/core/src/mysql/engine/parser/expr.ts index d5dbf3aa..3969a43f 100644 --- a/packages/core/src/mysql/engine/parser/expr.ts +++ b/packages/core/src/mysql/engine/parser/expr.ts @@ -1,21 +1,21 @@ -import type { Expr, SelectStmt, WindowSpec } from "../ast.js"; -import type { NameOf } from "../tokens.js"; -import type { GE, Tup } from "../types.js"; -import type { NoCall } from "./keywords.js"; -import type { ParseSubquery } from "./select.js"; -import type { ParseCastType } from "./typename.js"; +import type { NameOf } from "../../../engine/family/lex.js"; import type { CloseOk, Expect, - Ident, IsOp, IsWord, Name, - Nx, - Ok, + Negate, SyntaxErr, - Toks, -} from "./util.js"; +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { GE } from "../../../engine/num.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; +import type { Expr, SelectStmt, WindowSpec } from "../ast.js"; +import type { NoCall } from "./keywords.js"; +import type { ParseSubquery } from "./select.js"; +import type { ParseCastType } from "./typename.js"; +import type { Ident } from "./util.js"; /** MySQL's operator precedence, loosest first. */ type Prec = { @@ -60,52 +60,48 @@ type ParsePrefix< readonly v: infer O; } ? O extends "-" | "+" | "~" - ? ParseExprPrec, Prec["unary"]> extends Ok< + ? ParseExprPrec, Prec["unary"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok, J> - : ParseExprPrec, Prec["unary"]> + : ParseExprPrec, Prec["unary"]> : O extends "!" - ? ParseExprPrec, Prec["bang"]> extends Ok< + ? ParseExprPrec, Prec["bang"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok<{ t: "un"; op: "not"; e: E }, J> - : ParseExprPrec, Prec["bang"]> + : ParseExprPrec, Prec["bang"]> : O extends "(" - ? ParseParen> + ? ParseParen> : SyntaxErr : T[I] extends { readonly k: "w"; readonly v: infer W } ? W extends "not" ? GE extends false ? SyntaxErr - : ParseExprPrec, Prec["not"]> extends Ok< + : ParseExprPrec, Prec["not"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok<{ t: "un"; op: "not"; e: E }, J> - : ParseExprPrec, Prec["not"]> + : ParseExprPrec, Prec["not"]> : W extends "binary" - ? ParseExprPrec, Prec["collate"]> extends Ok< + ? ParseExprPrec, Prec["collate"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok<{ t: "un"; op: "binary"; e: E }, J> - : ParseExprPrec, Prec["collate"]> + : ParseExprPrec, Prec["collate"]> : W extends "interval" - ? IsOp, "("> extends true + ? IsOp, "("> extends true ? ParsePrimary - : ParseInterval> + : ParseInterval> : ParsePrimary : ParsePrimary; type Unary = O extends "-" - ? E extends { readonly t: "num"; readonly v: infer V extends string } - ? V extends `-${infer P}` - ? { t: "num"; v: P } - : { t: "num"; v: `-${V}` } - : { t: "un"; op: "-"; e: E } + ? Negate : O extends "+" ? E : { t: "un"; op: O; e: E }; @@ -138,7 +134,7 @@ type ParseInterval = readonly k: "w"; readonly v: infer U extends IntervalUnit; } - ? Ok<{ t: "interval"; e: E; unit: U }, Nx> + ? Ok<{ t: "interval"; e: E; unit: U }, Next> : SyntaxErr : ParseExpr; @@ -177,7 +173,7 @@ type ParseParenList = /** `((SELECT …))`: parentheses that hold a query. */ export type IsSubqueryParen = IsOp extends true - ? IsSubqueryParen> + ? IsSubqueryParen> : IsWord; export type ParseExprList< @@ -187,7 +183,7 @@ export type ParseExprList< > = ParseExpr extends Ok ? IsOp extends true - ? ParseExprList, [...Acc, E]> + ? ParseExprList, [...Acc, E]> : Ok<[...Acc, E], J> : ParseExpr; @@ -195,24 +191,24 @@ type ParsePrimary = T[I] extends { readonly k: "n"; readonly v: infer V extends string; } - ? Ok<{ t: "num"; v: V }, Nx> + ? Ok<{ t: "num"; v: V }, Next> : T[I] extends { readonly k: "s"; readonly v: infer V extends string } ? T[I] extends { readonly blob: true } - ? Ok<{ t: "blob"; v: V }, Nx> + ? Ok<{ t: "blob"; v: V }, Next> : T[I] extends { readonly f: infer F extends string } - ? Ok<{ t: "str"; v: V; f: F }, Nx> - : Ok<{ t: "str"; v: V; f: V }, Nx> + ? Ok<{ t: "str"; v: V; f: F }, Next> + : Ok<{ t: "str"; v: V; f: V }, Next> : T[I] extends { readonly k: "$"; readonly v: infer N extends number } - ? Ok<{ t: "param"; n: N }, Nx> + ? Ok<{ t: "param"; n: N }, Next> : T[I] extends { readonly k: "v"; readonly v: infer V extends string } - ? IsOp, ":="> extends true - ? ParseExprPrec>, Prec["assign"]> extends Ok< + ? IsOp, ":="> extends true + ? ParseExprPrec>, Prec["assign"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok<{ t: "assign"; name: V; e: E }, J> - : ParseExprPrec>, Prec["assign"]> - : Ok<{ t: "var"; name: V }, Nx> + : ParseExprPrec>, Prec["assign"]> + : Ok<{ t: "var"; name: V }, Next> : T[I] extends { readonly k: "w"; readonly v: infer W extends string } ? ParseWordPrimary : T[I] extends { readonly k: "id" } @@ -235,17 +231,17 @@ type ParseWordPrimary< I extends number, W extends string, > = W extends "null" - ? Ok<{ t: "null" }, Nx> + ? Ok<{ t: "null" }, Next> : W extends "true" | "false" - ? Ok<{ t: "bool"; v: W extends "true" ? true : false }, Nx> + ? Ok<{ t: "bool"; v: W extends "true" ? true : false }, Next> : W extends CTime - ? IsOp, "("> extends true - ? CTimeArgs>> extends infer J extends number + ? IsOp, "("> extends true + ? CTimeArgs>> extends infer J extends number ? Ok<{ t: "ctime"; fn: W }, J> - : CTimeArgs>> - : Ok<{ t: "ctime"; fn: W }, Nx> + : CTimeArgs>> + : Ok<{ t: "ctime"; fn: W }, Next> : W extends "date" | "time" | "timestamp" - ? T[Nx] extends { + ? T[Next] extends { readonly k: "s"; readonly v: infer V extends string; } @@ -256,53 +252,53 @@ type ParseWordPrimary< to: W extends "timestamp" ? "datetime" : W; lit: true; }, - Nx> + Next> > : AfterWord : W extends `_${string}` - ? T[Nx] extends { + ? T[Next] extends { readonly k: "s"; readonly v: infer V extends string; } ? W extends "_binary" - ? Ok<{ t: "blob"; v: V }, Nx>> - : Ok<{ t: "str"; v: V }, Nx>> + ? Ok<{ t: "blob"; v: V }, Next>> + : Ok<{ t: "str"; v: V }, Next>> : AfterWord : W extends "cast" - ? ParseCast> + ? ParseCast> : W extends "convert" - ? ParseConvert> + ? ParseConvert> : W extends "case" - ? ParseCase> + ? ParseCase> : W extends "exists" - ? ParseExists> + ? ParseExists> : W extends "match" - ? ParseMatch> + ? ParseMatch> : W extends "row" - ? IsOp, "("> extends true - ? ParseExprList>, []> extends Ok< + ? IsOp, "("> extends true + ? ParseExprList>, []> extends Ok< infer L extends readonly Expr[], infer J extends number > ? CloseOk - : ParseExprList>, []> + : ParseExprList>, []> : AfterWord : W extends "default" - ? IsOp, "("> extends true + ? IsOp, "("> extends true ? ParseCall & string]> - : Ok<{ t: "default" }, Nx> + : Ok<{ t: "default" }, Next> : AfterWord; /** After `CURRENT_TIMESTAMP(`: `)` or a precision then `)`. */ type CTimeArgs = IsOp extends true - ? Nx + ? Next : T[I] extends { readonly k: "n" } - ? Expect, ")"> + ? Expect, ")"> : SyntaxErr; type AfterWord = - IsOp, "("> extends true + IsOp, "("> extends true ? W extends NoCall ? SyntaxErr : ParseCall & string]> @@ -327,7 +323,7 @@ type ParseMatch = > ? Expect extends infer J3 extends number ? IsWord extends true - ? Expect, "("> extends infer J4 extends number + ? Expect, "("> extends infer J4 extends number ? ParseExprPrec extends Ok< infer Ag extends Expr, infer J5 extends number @@ -336,7 +332,7 @@ type ParseMatch = ? Ok<{ t: "match"; cols: Cols; against: Ag }, J6> : SkipToClose : ParseExprPrec - : Expect, "("> + : Expect, "("> : SyntaxErr : Expect : ParseExprList @@ -345,19 +341,19 @@ type SkipToClose = T[I] extends { readonly k: "o"; readonly v: ")"; } - ? Nx + ? Next : T[I] extends { readonly k: "w" } - ? SkipToClose> + ? SkipToClose> : SyntaxErr; type ParseColumnRef = Ident extends infer A extends Name - ? IsOp, "."> extends true - ? Ident>> extends infer B extends Name - ? IsOp>>, "."> extends true - ? Ident>>>> extends infer C extends Name - ? IsOp>>>>, "("> extends true - ? SyntaxErr>>>>> + ? IsOp, "."> extends true + ? Ident>> extends infer B extends Name + ? IsOp>>, "."> extends true + ? Ident>>>> extends infer C extends Name + ? IsOp>>>>, "("> extends true + ? SyntaxErr>>>>> : Ok< { t: "col"; @@ -367,13 +363,13 @@ type ParseColumnRef = n: C["n"]; disp: `${A["n"]}.${B["n"]}.${C["n"]}`; }, - Nx>>>> + Next>>>> > - : SyntaxErr>>>> - : IsOp>>, "("> extends true + : SyntaxErr>>>> + : IsOp>>, "("> extends true ? ParseCall< T, - Nx>, + Next>, [`${A["v"]}.${B["v"]}`, `${A["n"]}.${B["n"]}`] > : Ok< @@ -385,9 +381,9 @@ type ParseColumnRef = n: B["n"]; disp: `${A["n"]}.${B["n"]}`; }, - Nx>> + Next>> > - : SyntaxErr>> + : SyntaxErr>> : Ok< { t: "col"; @@ -397,7 +393,7 @@ type ParseColumnRef = n: A["n"]; disp: A["n"]; }, - Nx + Next > : SyntaxErr; @@ -405,14 +401,14 @@ type ParseCast = Expect extends infer J extends number ? ParseExpr extends Ok ? IsWord extends true - ? ParseCastType> extends Ok< + ? ParseCastType> extends Ok< infer Ty extends string, infer J3 extends number > ? IsWord extends true - ? CloseOk, { t: "cast"; e: E; to: "json" }> + ? CloseOk, { t: "cast"; e: E; to: "json" }> : CloseOk - : ParseCastType> + : ParseCastType> : SyntaxErr : ParseExpr : Expect; @@ -422,23 +418,23 @@ type ParseConvert = Expect extends infer J extends number ? ParseExpr extends Ok ? IsOp extends true - ? ParseCastType> extends Ok< + ? ParseCastType> extends Ok< infer Ty extends string, infer J3 extends number > ? CloseOk - : ParseCastType> + : ParseCastType> : IsWord extends true - ? T[Nx>] extends { readonly k: "o"; readonly v: ")" } + ? T[Next>] extends { readonly k: "o"; readonly v: ")" } ? Ok< { t: "convert"; e: E; - binary: IsWord, "binary">; + binary: IsWord, "binary">; }, - Nx>> + Next>> > - : SyntaxErr>> + : SyntaxErr>> : SyntaxErr : ParseExpr : Expect; @@ -458,32 +454,32 @@ type ParseWhens< Acc extends readonly unknown[], > = IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer C extends Expr, infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer V extends Expr, infer J2 extends number > ? ParseWhens - : ParseExpr> + : ParseExpr> : SyntaxErr - : ParseExpr> + : ParseExpr> : Acc extends readonly [] ? SyntaxErr : IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer E extends Expr, infer J extends number > ? IsWord extends true - ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: E }, Nx> + ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: E }, Next> : SyntaxErr - : ParseExpr> + : ParseExpr> : IsWord extends true - ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: null }, Nx> + ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: null }, Next> : SyntaxErr; /** Functions whose first argument is a unit or keyword, not an expression. */ @@ -494,42 +490,43 @@ type ParseCall = Name extends readonly [ unknown, ] ? N extends "trim" - ? ParseTrim>, Name> + ? ParseTrim>, Name> : N extends "substring" | "substr" | "mid" - ? ParseSubstring>, Name> + ? ParseSubstring>, Name> : N extends "position" - ? ParsePosition>, Name> + ? ParsePosition>, Name> : N extends "extract" - ? ParseExtract>, Name> + ? ParseExtract>, Name> : N extends UnitFirst - ? T[Nx>] extends { readonly k: "w" | "s" } - ? IsOp>>, ","> extends true + ? T[Next>] extends { readonly k: "w" | "s" } + ? IsOp>>, ","> extends true ? CallArgs< T, - Nx>>>, + Next>>>, Name, false, [{ t: "str"; v: "unit" }] > - : SyntaxErr>>> - : SyntaxErr>> - : IsOp>, ")"> extends true - ? CallTail>>, Name, [], false, false, []> - : IsOp>, "*"> extends true - ? Expect>>, ")"> extends infer J extends number + : SyntaxErr>>> + : SyntaxErr>> + : IsOp>, ")"> extends true + ? CallTail>>, Name, [], false, false, []> + : IsOp>, "*"> extends true + ? Expect>>, ")"> extends infer J extends + number ? CallTail - : Expect>>, ")"> + : Expect>>, ")"> : CallArgs< T, IsWord< T, - Nx>, + Next>, "distinct" | "distinctrow" | "all" > extends true - ? Nx>> - : Nx>, + ? Next>> + : Next>, Name, - IsWord>, "distinct" | "distinctrow">, + IsWord>, "distinct" | "distinctrow">, [] > : never; @@ -559,9 +556,9 @@ type CallArgs< /** An optional `ORDER BY` inside a call's parentheses (GROUP_CONCAT). */ type CallOrder = IsWord extends true - ? IsWord, "by"> extends true - ? ParseOrderList>, []> - : SyntaxErr> + ? IsWord, "by"> extends true + ? ParseOrderList>, []> + : SyntaxErr> : Ok<[], I>; /** @@ -573,17 +570,17 @@ type SkipCallSuffix = T[I] extends { readonly v: infer W extends string; } ? W extends "separator" - ? T[Nx] extends { readonly k: "s" } - ? SkipCallSuffix>> - : SyntaxErr> + ? T[Next] extends { readonly k: "s" } + ? SkipCallSuffix>> + : SyntaxErr> : W extends "using" - ? T[Nx] extends { readonly k: "w" | "id" } - ? SkipCallSuffix>> - : SyntaxErr> + ? T[Next] extends { readonly k: "w" | "id" } + ? SkipCallSuffix>> + : SyntaxErr> : W extends "returning" | "as" - ? ParseCastType> extends Ok + ? ParseCastType> extends Ok ? SkipCallSuffix - : ParseCastType> + : ParseCastType> : W extends "null" | "error" | "default" | "on" | "empty" ? SkipJsonHandler : I @@ -596,37 +593,37 @@ type SkipJsonHandler = T[I] extends { : T[I] extends { readonly k: string } ? T[I] extends { readonly k: "o"; readonly v: "(" } ? SyntaxErr - : SkipJsonHandler> + : SkipJsonHandler> : SyntaxErr; /** `TRIM([[BOTH | LEADING | TRAILING] [remstr] FROM] str)`. */ type ParseTrim = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer S extends Expr, infer J2 extends number > ? Expect extends infer J3 extends number ? CallTail : Expect - : ParseExpr> + : ParseExpr> : ParseExpr extends Ok ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer S extends Expr, infer J3 extends number > ? Expect extends infer J4 extends number ? CallTail : Expect - : ParseExpr> + : ParseExpr> : J extends I ? IsOp extends true - ? CallArgs, Name, false, [A]> + ? CallArgs, Name, false, [A]> : Expect extends infer J4 extends number ? CallTail : Expect @@ -638,25 +635,25 @@ type ParseTrim = ( type ParseSubstring = ParseExpr extends Ok ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer P extends Expr, infer J2 extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer L extends Expr, infer J3 extends number > ? Expect extends infer J4 extends number ? CallTail : Expect - : ParseExpr> + : ParseExpr> : Expect extends infer J4 extends number ? CallTail : Expect - : ParseExpr> + : ParseExpr> : IsOp extends true - ? CallArgs, Name, false, [S]> + ? CallArgs, Name, false, [S]> : Expect extends infer J4 extends number ? CallTail : Expect @@ -669,14 +666,14 @@ type ParsePosition = infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer B extends Expr, infer J2 extends number > ? Expect extends infer J3 extends number ? CallTail : Expect - : ParseExpr> + : ParseExpr> : SyntaxErr : ParseExprPrec; @@ -685,16 +682,16 @@ type ParseExtract = T[I] extends { readonly k: "w"; readonly v: infer U extends IntervalUnit; } - ? IsWord, "from"> extends true - ? ParseExpr>> extends Ok< + ? IsWord, "from"> extends true + ? ParseExpr>> extends Ok< infer D extends Expr, infer J extends number > ? Expect extends infer J2 extends number ? CallTail : Expect - : ParseExpr>> - : SyntaxErr> + : ParseExpr>> + : SyntaxErr> : SyntaxErr; type CallTail< @@ -708,26 +705,26 @@ type CallTail< > = SkipNullTreatment> extends infer I2 extends number ? IsWord extends true - ? ParseOver> extends Ok< + ? ParseOver> extends Ok< infer Spec extends WindowSpec, infer J extends number > ? Ok, J> - : ParseOver> + : ParseOver> : Ok, I2> : never; /** `FROM FIRST` / `FROM LAST` (NTH_VALUE). */ type SkipFromFirstLast = IsWord extends true - ? IsWord, "first" | "last"> extends true - ? Nx> + ? IsWord, "first" | "last"> extends true + ? Next> : I : I; /** `RESPECT NULLS` / `IGNORE NULLS`. */ type SkipNullTreatment = IsWord extends true - ? IsWord, "nulls"> extends true - ? Nx> + ? IsWord, "nulls"> extends true + ? Next> : I : I; type MkCall = { @@ -743,9 +740,9 @@ type MkCall = { type ParseOver = Ident extends infer N extends Name - ? Ok<{ name: N["v"]; partition: []; order: [] }, Nx> + ? Ok<{ name: N["v"]; partition: []; order: [] }, Next> : IsOp extends true - ? ParseWindowSpec> + ? ParseWindowSpec> : SyntaxErr; export type ParseWindowSpec = ( @@ -756,7 +753,7 @@ export type ParseWindowSpec = ( "partition" | "order" | "rows" | "range" | "groups" > extends true ? [null, I] - : [N["v"], Nx] + : [N["v"], Next] : [null, I] ) extends [infer Base, infer I1 extends number] ? OptBy extends Ok< @@ -782,11 +779,11 @@ type OptBy< Absent = [], > = IsWord extends true - ? IsWord, "by"> extends true + ? IsWord, "by"> extends true ? W extends "order" - ? ParseOrderList>, []> - : ParseExprList>, []> - : SyntaxErr> + ? ParseOrderList>, []> + : ParseExprList>, []> + : SyntaxErr> : Ok; type SkipFrame< @@ -794,13 +791,13 @@ type SkipFrame< I extends number, Depth extends readonly unknown[], > = T[I] extends { readonly k: "o"; readonly v: "(" } - ? SkipFrame, [...Depth, 0]> + ? SkipFrame, [...Depth, 0]> : T[I] extends { readonly k: "o"; readonly v: ")" } ? Depth extends readonly [unknown, ...infer D] - ? SkipFrame, D> - : Nx + ? SkipFrame, D> + : Next : T[I] extends { readonly k: string } - ? SkipFrame, Depth> + ? SkipFrame, Depth> : SyntaxErr; export type ParseOrderList< @@ -811,11 +808,11 @@ export type ParseOrderList< ParseExpr extends Ok ? ( IsWord extends true - ? Nx + ? Next : J ) extends infer J2 extends number ? IsOp extends true - ? ParseOrderList, [...Acc, E]> + ? ParseOrderList, [...Acc, E]> : Ok<[...Acc, E], J2> : never : ParseExpr; @@ -871,12 +868,12 @@ type InfixOp< BinPrec extends infer P extends number ? GE extends true ? P extends Prec["cmp"] - ? T[Nx] extends { + ? T[Next] extends { readonly k: "w"; readonly v: infer Q extends "any" | "some" | "all"; } - ? IsOp>, "("> extends true - ? ParseQuantified>>, L, Min, O, Q> + ? IsOp>, "("> extends true + ? ParseQuantified>>, L, Min, O, Q> : InfixRight : InfixRight : InfixRight @@ -891,12 +888,12 @@ type InfixRight< O extends string, P extends number, > = - ParseExprPrec, Next1

> extends Ok< + ParseExprPrec, Next

> extends Ok< infer E extends Expr, infer J extends number > ? ParseInfix; l: L; r: E }, Min> - : ParseExprPrec, Next1

>; + : ParseExprPrec, Next

>; /** `x = ANY (SELECT …)`, `x > ALL (SELECT …)`. */ type ParseQuantified< @@ -917,18 +914,16 @@ type ParseQuantified< J2, { t: "quant"; + l: L; op: BinOp; - all: Q extends "all" ? true : false; - e: L; - q: S; + q: Q extends "all" ? "all" : "any"; + r: S; }, Min > : Expect : ParseSubquery; -type Next1

= [...Tup

, 0]["length"] & number; - type LikeOp = "like" | "regexp" | "rlike"; type InfixWord< @@ -939,16 +934,16 @@ type InfixWord< W extends string, > = W extends "or" | "and" | "xor" ? GE extends true - ? ParseExprPrec, Next1> extends Ok< + ? ParseExprPrec, Next> extends Ok< infer E extends Expr, infer J extends number > ? ParseInfix - : ParseExprPrec, Next1> + : ParseExprPrec, Next> : Ok : W extends "div" | "mod" ? GE extends true - ? ParseExprPrec, Next1> extends Ok< + ? ParseExprPrec, Next> extends Ok< infer E extends Expr, infer J extends number > @@ -958,45 +953,46 @@ type InfixWord< { t: "bin"; op: W extends "mod" ? "%" : "div"; l: L; r: E }, Min > - : ParseExprPrec, Next1> + : ParseExprPrec, Next> : Ok : W extends "collate" ? GE extends true - ? T[Nx] extends { readonly k: "w" | "id" | "s" } - ? ParseInfix>, { t: "collate"; e: L }, Min> - : SyntaxErr> + ? T[Next] extends { readonly k: "w" | "id" | "s" } + ? ParseInfix>, { t: "collate"; e: L }, Min> + : SyntaxErr> : Ok : GE extends true ? W extends "is" - ? ParseIs, L, Min> + ? ParseIs, L, Min> : W extends "not" - ? T[Nx] extends { + ? T[Next] extends { readonly k: "w"; readonly v: infer W2 extends string; } ? W2 extends "in" | "between" | LikeOp - ? ParsePredicate>, L, Min, W2, true> + ? ParsePredicate>, L, Min, W2, true> : Ok : Ok : W extends "in" | "between" | LikeOp - ? ParsePredicate, L, Min, W, false> + ? ParsePredicate, L, Min, W, false> : W extends "sounds" - ? IsWord, "like"> extends true - ? ParseExprPrec>, Next1> extends Ok< - infer E extends Expr, - infer J extends number - > + ? IsWord, "like"> extends true + ? ParseExprPrec< + T, + Next>, + Next + > extends Ok ? ParseInfix< T, J, { t: "bin"; op: "sounds"; l: L; r: E }, Min > - : ParseExprPrec>, Next1> + : ParseExprPrec>, Next> : Ok : W extends "member" - ? IsWord, "of"> extends true - ? ParseMemberOf>, L, Min> + ? IsWord, "of"> extends true + ? ParseMemberOf>, L, Min> : Ok : Ok : Ok; @@ -1021,7 +1017,7 @@ type ParseIs< I extends number, L extends Expr, Min extends number, -> = (IsWord extends true ? [true, Nx] : [false, I]) extends [ +> = (IsWord extends true ? [true, Next] : [false, I]) extends [ infer Not extends boolean, infer J extends number, ] @@ -1031,7 +1027,7 @@ type ParseIs< } ? ParseInfix< T, - Nx, + Next, { t: "is"; e: L; what: W extends "unknown" ? "null" : W; not: Not }, Min > @@ -1051,7 +1047,7 @@ type ParsePredicate< infer J extends number > ? IsWord extends true - ? ParseExprPrec, Prec["bitor"]> extends Ok< + ? ParseExprPrec, Prec["bitor"]> extends Ok< infer HiE extends Expr, infer J2 extends number > @@ -1061,18 +1057,18 @@ type ParsePredicate< { t: "between"; e: L; lo: LoE; hi: HiE; not: Not }, Min > - : ParseExprPrec, Prec["bitor"]> + : ParseExprPrec, Prec["bitor"]> : SyntaxErr : ParseExprPrec : W extends "in" ? ParseIn : W extends LikeOp - ? ParseExprPrec> extends Ok< + ? ParseExprPrec> extends Ok< infer Pat extends Expr, infer J extends number > ? IsWord extends true - ? ParseExprPrec, Next1> extends Ok< + ? ParseExprPrec, Next> extends Ok< infer EsE extends Expr, infer J2 extends number > @@ -1082,14 +1078,14 @@ type ParsePredicate< { t: "like"; e: L; pat: Pat; esc: EsE; kind: W; not: Not }, Min > - : ParseExprPrec, Next1> + : ParseExprPrec, Next> : ParseInfix< T, J, { t: "like"; e: L; pat: Pat; esc: null; kind: W; not: Not }, Min > - : ParseExprPrec> + : ParseExprPrec> : never; type ParseIn< @@ -1100,21 +1096,21 @@ type ParseIn< Not extends boolean, > = IsOp extends true - ? IsSubqueryParen> extends true - ? ParseSubquery> extends Ok< + ? IsSubqueryParen> extends true + ? ParseSubquery> extends Ok< infer Q extends SelectStmt, infer J extends number > ? Expect extends infer J2 extends number ? ParseInfix : Expect - : ParseSubquery> - : ParseExprList, []> extends Ok< + : ParseSubquery> + : ParseExprList, []> extends Ok< infer List extends readonly Expr[], infer J extends number > ? Expect extends infer J2 extends number ? ParseInfix : Expect - : ParseExprList, []> + : ParseExprList, []> : SyntaxErr; diff --git a/packages/core/src/mysql/engine/parser/keywords.ts b/packages/core/src/mysql/engine/parser/keywords.ts index b121b48b..2f33bdaf 100644 --- a/packages/core/src/mysql/engine/parser/keywords.ts +++ b/packages/core/src/mysql/engine/parser/keywords.ts @@ -3,235 +3,296 @@ * as MySQL's `rank`, `rows` and `window`) are accepted as names, so a statement * valid on either server parses. */ -export type Reserved = - | "add" - | "all" - | "alter" - | "analyze" - | "and" - | "as" - | "asc" - | "asensitive" - | "before" - | "between" - | "bigint" - | "binary" - | "blob" - | "both" - | "by" - | "call" - | "cascade" - | "case" - | "change" - | "char" - | "character" - | "check" - | "collate" - | "column" - | "condition" - | "constraint" - | "continue" - | "convert" - | "create" - | "cross" - | "current_date" - | "current_time" - | "current_timestamp" - | "current_user" - | "cursor" - | "database" - | "databases" - | "day_hour" - | "day_microsecond" - | "day_minute" - | "day_second" - | "dec" - | "decimal" - | "declare" - | "default" - | "delayed" - | "delete" - | "desc" - | "describe" - | "deterministic" - | "distinct" - | "distinctrow" - | "div" - | "double" - | "drop" - | "dual" - | "each" - | "else" - | "elseif" - | "enclosed" - | "escaped" - | "except" - | "exists" - | "exit" - | "explain" - | "false" - | "fetch" - | "float" - | "float4" - | "float8" - | "for" - | "force" - | "foreign" - | "from" - | "fulltext" - | "grant" - | "group" - | "having" - | "high_priority" - | "hour_microsecond" - | "hour_minute" - | "hour_second" - | "if" - | "ignore" - | "in" - | "index" - | "infile" - | "inner" - | "inout" - | "insensitive" - | "insert" - | "int" - | "int1" - | "int2" - | "int3" - | "int4" - | "int8" - | "integer" - | "intersect" - | "interval" - | "into" - | "is" - | "iterate" - | "join" - | "key" - | "keys" - | "kill" - | "leading" - | "leave" - | "left" - | "like" - | "limit" - | "linear" - | "lines" - | "load" - | "localtime" - | "localtimestamp" - | "lock" - | "long" - | "longblob" - | "longtext" - | "loop" - | "low_priority" - | "match" - | "maxvalue" - | "mediumblob" - | "mediumint" - | "mediumtext" - | "middleint" - | "minute_microsecond" - | "minute_second" - | "mod" - | "modifies" - | "natural" - | "not" - | "no_write_to_binlog" - | "null" - | "numeric" - | "on" - | "optimize" - | "option" - | "optionally" - | "or" - | "order" - | "out" - | "outer" - | "outfile" - | "partition" - | "precision" - | "primary" - | "procedure" - | "purge" - | "range" - | "read" - | "reads" - | "read_write" - | "real" - | "recursive" - | "references" - | "regexp" - | "release" - | "rename" - | "repeat" - | "replace" - | "require" - | "resignal" - | "restrict" - | "return" - | "revoke" - | "right" - | "rlike" - | "schema" - | "schemas" - | "second_microsecond" - | "select" - | "sensitive" - | "separator" - | "set" - | "show" - | "signal" - | "smallint" - | "spatial" - | "specific" - | "sql" - | "sqlexception" - | "sqlstate" - | "sqlwarning" - | "sql_big_result" - | "sql_calc_found_rows" - | "sql_small_result" - | "ssl" - | "starting" - | "straight_join" - | "table" - | "terminated" - | "then" - | "tinyblob" - | "tinyint" - | "tinytext" - | "to" - | "trailing" - | "trigger" - | "true" - | "undo" - | "union" - | "unique" - | "unlock" - | "unsigned" - | "update" - | "usage" - | "use" - | "using" - | "utc_date" - | "utc_time" - | "utc_timestamp" - | "values" - | "varbinary" - | "varchar" - | "varcharacter" - | "varying" - | "when" - | "where" - | "while" - | "with" - | "write" - | "xor" - | "year_month" - | "zerofill"; +export const RESERVED = [ + "add", + "all", + "alter", + "analyze", + "and", + "as", + "asc", + "asensitive", + "before", + "between", + "bigint", + "binary", + "blob", + "both", + "by", + "call", + "cascade", + "case", + "change", + "char", + "character", + "check", + "collate", + "column", + "condition", + "constraint", + "continue", + "convert", + "create", + "cross", + "current_date", + "current_time", + "current_timestamp", + "current_user", + "cursor", + "database", + "databases", + "day_hour", + "day_microsecond", + "day_minute", + "day_second", + "dec", + "decimal", + "declare", + "default", + "delayed", + "delete", + "desc", + "describe", + "deterministic", + "distinct", + "distinctrow", + "div", + "double", + "drop", + "dual", + "each", + "else", + "elseif", + "enclosed", + "escaped", + "except", + "exists", + "exit", + "explain", + "false", + "fetch", + "float", + "float4", + "float8", + "for", + "force", + "foreign", + "from", + "fulltext", + "grant", + "group", + "having", + "high_priority", + "hour_microsecond", + "hour_minute", + "hour_second", + "if", + "ignore", + "in", + "index", + "infile", + "inner", + "inout", + "insensitive", + "insert", + "int", + "int1", + "int2", + "int3", + "int4", + "int8", + "integer", + "intersect", + "interval", + "into", + "is", + "iterate", + "join", + "key", + "keys", + "kill", + "leading", + "leave", + "left", + "like", + "limit", + "linear", + "lines", + "load", + "localtime", + "localtimestamp", + "lock", + "long", + "longblob", + "longtext", + "loop", + "low_priority", + "match", + "maxvalue", + "mediumblob", + "mediumint", + "mediumtext", + "middleint", + "minute_microsecond", + "minute_second", + "mod", + "modifies", + "natural", + "not", + "no_write_to_binlog", + "null", + "numeric", + "on", + "optimize", + "option", + "optionally", + "or", + "order", + "out", + "outer", + "outfile", + "partition", + "precision", + "primary", + "procedure", + "purge", + "range", + "read", + "reads", + "read_write", + "real", + "recursive", + "references", + "regexp", + "release", + "rename", + "repeat", + "replace", + "require", + "resignal", + "restrict", + "return", + "revoke", + "right", + "rlike", + "schema", + "schemas", + "second_microsecond", + "select", + "sensitive", + "separator", + "set", + "show", + "signal", + "smallint", + "spatial", + "specific", + "sql", + "sqlexception", + "sqlstate", + "sqlwarning", + "sql_big_result", + "sql_calc_found_rows", + "sql_small_result", + "ssl", + "starting", + "straight_join", + "table", + "terminated", + "then", + "tinyblob", + "tinyint", + "tinytext", + "to", + "trailing", + "trigger", + "true", + "undo", + "union", + "unique", + "unlock", + "unsigned", + "update", + "usage", + "use", + "using", + "utc_date", + "utc_time", + "utc_timestamp", + "values", + "varbinary", + "varchar", + "varcharacter", + "varying", + "when", + "where", + "while", + "with", + "write", + "xor", + "year_month", + "zerofill", +] as const; +export type Reserved = (typeof RESERVED)[number]; + +/** Words only MySQL 8.4 or only MariaDB reserves, which the parser accepts as names. */ +export const SINGLE_SERVER_RESERVED = [ + "accessible", + "cube", + "cume_dist", + "current_role", + "delete_domain_id", + "dense_rank", + "do_domain_ids", + "empty", + "first_value", + "function", + "general", + "generated", + "get", + "grouping", + "groups", + "ignore_domain_ids", + "ignore_server_ids", + "io_after_gtids", + "io_before_gtids", + "json_table", + "lag", + "last_value", + "lateral", + "lead", + "manual", + "master_bind", + "master_heartbeat_period", + "master_ssl_verify_server_cert", + "nth_value", + "ntile", + "of", + "offset", + "optimizer_costs", + "over", + "page_checksum", + "parallel", + "parse_vcol_expr", + "percent_rank", + "position", + "qualify", + "rank", + "ref_system_id", + "returning", + "row", + "row_number", + "rows", + "slow", + "stats_auto_recalc", + "stats_persistent", + "stats_sample_pages", + "stored", + "system", + "tablesample", + "virtual", + "window", +] as const; /** Reserved words that can't start a function call. */ export type NoCall = diff --git a/packages/core/src/mysql/engine/parser/select.ts b/packages/core/src/mysql/engine/parser/select.ts index 7fafa570..aa47b073 100644 --- a/packages/core/src/mysql/engine/parser/select.ts +++ b/packages/core/src/mysql/engine/parser/select.ts @@ -1,3 +1,16 @@ +import type { + AliasAt, + CloseOk, + Expect, + IsOp, + IsWord, + Name, + NamesV, + SpanText, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { SqlError } from "../../../errors.js"; import type { Cte, @@ -22,39 +35,27 @@ import type { import type { Reserved } from "./keywords.js"; import type { ParseCastType } from "./typename.js"; import type { - AliasAt, BareAlias, - CloseOk, - Expect, Ident, IdentList, - IsOp, - IsWord, - Name, - NamesV, - Nx, - Ok, OptCols, - SpanText, - SyntaxErr, TableAlias, TableRefAt, - Toks, } from "./util.js"; export type ParseSubquery = IsWord extends true - ? ParseWith> extends Ok< + ? ParseWith> extends Ok< infer Wc extends WithClause, infer J extends number > ? ParseSelectAfterWith - : ParseWith> + : ParseWith> : ParseSelectAfterWith; export type ParseWith = IsWord extends true - ? CteLoop, true, []> + ? CteLoop, true, []> : CteLoop; type CteLoop< @@ -64,9 +65,9 @@ type CteLoop< Acc extends readonly Cte[], > = Ident extends infer N extends Name - ? OptCols> extends Ok + ? OptCols> extends Ok ? IsWord extends true - ? Expect, "("> extends infer J1 extends number + ? Expect, "("> extends infer J1 extends number ? ParenSubquery extends Ok< infer S extends SelectStmt, infer J2 extends number @@ -81,13 +82,13 @@ type CteLoop< }, ] extends infer Acc2 extends readonly Cte[] ? IsOp extends true - ? CteLoop, Rec, Acc2> + ? CteLoop, Rec, Acc2> : Ok<{ recursive: Rec; ctes: Acc2 }, J2> : never : ParenSubquery - : Expect, "("> + : Expect, "("> : SyntaxErr - : OptCols> + : OptCols> : SyntaxErr; /** A query followed by its closing parenthesis. */ @@ -134,70 +135,70 @@ type MkSelect = { type OptOrderBy = IsWord extends true - ? IsWord, "by"> extends true - ? ParseOrderList>, []> - : SyntaxErr> + ? IsWord, "by"> extends true + ? ParseOrderList>, []> + : SyntaxErr> : Ok<[], I>; /** `LIMIT n`, `LIMIT off, n`, `LIMIT n OFFSET off`. */ export type ParseLimit = IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Lim extends Expr, infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Off extends Expr, infer J2 extends number > ? Ok<[Lim, Off], J2> - : ParseExpr> + : ParseExpr> : IsOp extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Cnt extends Expr, infer J2 extends number > ? Ok<[Cnt, Lim], J2> - : ParseExpr> + : ParseExpr> : Ok<[Lim, null], J> - : ParseExpr> + : ParseExpr> : Ok<[null, null], I>; /** `FOR UPDATE`, `FOR SHARE`, `LOCK IN SHARE MODE`, with `OF t`, `NOWAIT`, `SKIP LOCKED`. */ type SkipLocking = IsWord extends true - ? IsWord, "update" | "share"> extends true - ? SkipLockOptions>> - : SyntaxErr> + ? IsWord, "update" | "share"> extends true + ? SkipLockOptions>> + : SyntaxErr> : IsWord extends true - ? IsWord, "in"> extends true - ? IsWord>, "share"> extends true - ? IsWord>>, "mode"> extends true - ? SkipLockOptions>>>> - : SyntaxErr>>> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "in"> extends true + ? IsWord>, "share"> extends true + ? IsWord>>, "mode"> extends true + ? SkipLockOptions>>>> + : SyntaxErr>>> + : SyntaxErr>> + : SyntaxErr> : I; type SkipLockOptions = IsWord extends true - ? SkipNames> + ? SkipNames> : IsWord extends true - ? SkipLocking> + ? SkipLocking> : IsWord extends true - ? IsWord, "locked"> extends true - ? SkipLocking>> - : SyntaxErr> + ? IsWord, "locked"> extends true + ? SkipLocking>> + : SyntaxErr> : IsWord extends true - ? T[Nx] extends { readonly k: "n" } - ? SkipLocking>> - : SyntaxErr> + ? T[Next] extends { readonly k: "n" } + ? SkipLocking>> + : SyntaxErr> : SkipLocking; type SkipNames = Ident extends Name - ? IsOp, ","> extends true - ? SkipNames>> - : SkipLockOptions> + ? IsOp, ","> extends true + ? SkipNames>> + : SkipLockOptions> : SyntaxErr; type ParseCompound = @@ -211,9 +212,9 @@ type ParseCompound = /** `ALL` or `DISTINCT` after a set operator. */ type SetQuantifier = IsWord extends true - ? [true, Nx] + ? [true, Next] : IsWord extends true - ? [false, Nx] + ? [false, Next] : [false, I]; type CompoundLoop< @@ -224,7 +225,7 @@ type CompoundLoop< readonly k: "w"; readonly v: infer Op extends "union" | "intersect" | "except"; } - ? SetQuantifier> extends [ + ? SetQuantifier> extends [ infer All extends boolean, infer J extends number, ] @@ -247,7 +248,7 @@ type CompoundLoop< /** INTERSECT binds tighter than UNION and EXCEPT. */ type IntersectRight = IsWord extends true - ? SetQuantifier> extends [ + ? SetQuantifier> extends [ infer All extends boolean, infer J extends number, ] @@ -266,18 +267,18 @@ type IntersectRight = type ParseCore = IsWord extends true - ? ParseSimple> + ? ParseSimple> : IsWord extends true - ? ParseValuesBody> + ? ParseValuesBody> : IsWord extends true - ? ParseTableStmt> + ? ParseTableStmt> : IsOp extends true - ? ParenSubquery> extends Ok< + ? ParenSubquery> extends Ok< infer Q extends SelectStmt, infer J extends number > ? Ok, J> - : ParenSubquery> + : ParenSubquery> : SyntaxErr; /** `(SELECT …)` without ORDER BY or LIMIT is just its body. */ @@ -328,8 +329,8 @@ type ParseValuesBody = /** The text of each item of the first `VALUES` row, which MariaDB names columns after. */ type FirstSpans = SpansLoop< T, - Nx, - Nx, + Next, + Next, [], [] >; @@ -341,18 +342,24 @@ type SpansLoop< Acc extends readonly string[], > = T[I] extends { readonly k: "o"; readonly v: infer O } ? O extends "(" - ? SpansLoop, Start, [...Depth, 0], Acc> + ? SpansLoop, Start, [...Depth, 0], Acc> : O extends ")" ? Depth extends readonly [unknown, ...infer D] - ? SpansLoop, Start, D, Acc> + ? SpansLoop, Start, D, Acc> : [...Acc, SpanText] : O extends "," ? Depth extends readonly [] - ? SpansLoop, Nx, Depth, [...Acc, SpanText]> - : SpansLoop, Start, Depth, Acc> - : SpansLoop, Start, Depth, Acc> + ? SpansLoop< + T, + Next, + Next, + Depth, + [...Acc, SpanText] + > + : SpansLoop, Start, Depth, Acc> + : SpansLoop, Start, Depth, Acc> : T[I] extends { readonly k: string } - ? SpansLoop, Start, Depth, Acc> + ? SpansLoop, Start, Depth, Acc> : Acc; export type ParseValues< @@ -368,7 +375,7 @@ export type ParseValues< > ? Expect extends infer J3 extends number ? IsOp extends true - ? ParseValues, [...Acc, Items], Row> + ? ParseValues, [...Acc, Items], Row> : Acc extends readonly [] ? Ok<[Items], J3> : Items["length"] extends Acc[0]["length"] @@ -387,7 +394,7 @@ type RowOpen< Row extends boolean, > = Row extends true ? IsWord extends true - ? Expect, "("> + ? Expect, "("> : SyntaxErr : Expect; /** The items of a row, which may be empty (`()`). */ @@ -410,9 +417,9 @@ type SkipModifiers< D extends boolean, > = T[I] extends { readonly k: "w"; readonly v: infer W } ? W extends "distinct" | "distinctrow" - ? SkipModifiers, true> + ? SkipModifiers, true> : W extends "all" | SelectModifier - ? SkipModifiers, D> + ? SkipModifiers, D> : [D, I] : [D, I]; @@ -461,13 +468,13 @@ type ParseSimple = /** `FROM DUAL` is no table. */ type OptFrom = IsWord extends true - ? IsWord, "dual"> extends true - ? Ok>> - : ParseFrom> + ? IsWord, "dual"> extends true + ? Ok>> + : ParseFrom> : Ok; type OptWhere = - IsWord extends true ? ParseExpr> : Ok; + IsWord extends true ? ParseExpr> : Ok; type ParseGroup = OptGroupBy extends Ok< @@ -475,37 +482,37 @@ type ParseGroup = infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Having extends Expr, infer J2 extends number > ? Ok<[Group, Rollup, Having], J2> - : ParseExpr> + : ParseExpr> : Ok<[Group, Rollup, null], J> : OptGroupBy; /** `GROUP BY … [WITH ROLLUP]`. */ type OptGroupBy = IsWord extends true - ? IsWord, "by"> extends true - ? ParseOrderList>, []> extends Ok< + ? IsWord, "by"> extends true + ? ParseOrderList>, []> extends Ok< infer Group extends readonly Expr[], infer J extends number > ? IsWord extends true - ? IsWord, "rollup"> extends true - ? Ok<[Group, true], Nx>> - : SyntaxErr> + ? IsWord, "rollup"> extends true + ? Ok<[Group, true], Next>> + : SyntaxErr> : Ok<[Group, false], J> - : ParseOrderList>, []> - : SyntaxErr> + : ParseOrderList>, []> + : SyntaxErr> : Ok<[null, false], I>; type ParseWindows = IsWord extends true - ? Ident> extends Name - ? IsWord>, "as"> extends true - ? WindowLoop, []> + ? Ident> extends Name + ? IsWord>, "as"> extends true + ? WindowLoop, []> : Ok<[], I> : Ok<[], I> : Ok<[], I>; @@ -515,18 +522,18 @@ type WindowLoop< Acc extends readonly NamedWindow[], > = Ident extends infer N extends Name - ? IsWord, "as"> extends true - ? Expect>, "("> extends infer J extends number + ? IsWord, "as"> extends true + ? Expect>, "("> extends infer J extends number ? ParseWindowSpec extends Ok< infer Spec extends WindowSpec, infer J2 extends number > ? IsOp extends true - ? WindowLoop, [...Acc, { name: N["v"]; spec: Spec }]> + ? WindowLoop, [...Acc, { name: N["v"]; spec: Spec }]> : Ok<[...Acc, { name: N["v"]; spec: Spec }], J2> : ParseWindowSpec - : Expect>, "("> - : SyntaxErr> + : Expect>, "("> + : SyntaxErr> : SyntaxErr; export type ParseTargets< @@ -536,27 +543,27 @@ export type ParseTargets< > = ParseTarget extends Ok ? IsOp extends true - ? ParseTargets, [...Acc, Tg]> + ? ParseTargets, [...Acc, Tg]> : Ok<[...Acc, Tg], J> : ParseTarget; type ParseTarget = IsOp extends true - ? Ok<{ t: "star"; schema: null; tbl: null }, Nx> - : IsOp, "."> extends true - ? IsOp>, "*"> extends true + ? Ok<{ t: "star"; schema: null; tbl: null }, Next> + : IsOp, "."> extends true + ? IsOp>, "*"> extends true ? Ident extends infer N extends Name - ? Ok<{ t: "star"; schema: null; tbl: N["v"] }, Nx>>> + ? Ok<{ t: "star"; schema: null; tbl: N["v"] }, Next>>> : SyntaxErr - : IsOp>>, "."> extends true - ? IsOp>>>, "*"> extends true + : IsOp>>, "."> extends true + ? IsOp>>>, "*"> extends true ? Ident extends infer S extends Name - ? Ident>> extends infer N extends Name + ? Ident>> extends infer N extends Name ? Ok< { t: "star"; schema: S["v"]; tbl: N["v"] }, - Nx>>>> + Next>>>> > - : SyntaxErr>> + : SyntaxErr>> : SyntaxErr : ParseExprTarget : ParseExprTarget @@ -565,11 +572,14 @@ type ParseTarget = type ParseExprTarget = ParseExpr extends Ok ? IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: ""; tail: "" }, Nx>> - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? Ok< + { t: "expr"; e: E; alias: A["n"]; span: ""; tail: "" }, + Next> + > + : SyntaxErr> : BareAlias extends infer A extends Name - ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: ""; tail: "" }, Nx> + ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: ""; tail: "" }, Next> : Ok< { t: "expr"; @@ -597,7 +607,7 @@ type TailAfter = : "" : ""; type LastTwo = - Nx extends J ? [P, T[I]] : LastTwo, J, T[I]>; + Next extends J ? [P, T[I]] : LastTwo, J, T[I]>; type LeadingWs< S extends string, Acc extends string, @@ -616,34 +626,34 @@ export type ParseFrom = /** The join's kind, whether it is NATURAL, the index after it and whether a keyword introduced it. */ type JoinOp = IsOp extends true - ? ["cross", false, Nx, false] + ? ["cross", false, Next, false] : ( IsWord extends true - ? [true, Nx] + ? [true, Next] : [false, I] ) extends [infer Nat extends boolean, infer J extends number] ? IsWord extends true - ? ["inner", Nat, Nx, true] + ? ["inner", Nat, Next, true] : T[J] extends { readonly k: "w"; readonly v: infer K extends "left" | "right"; } ? ( - IsWord, "outer"> extends true - ? Nx> - : Nx + IsWord, "outer"> extends true + ? Next> + : Next ) extends infer J2 extends number ? IsWord extends true - ? [K, Nat, Nx, true] + ? [K, Nat, Next, true] : SyntaxErr : never : T[J] extends { readonly k: "w"; readonly v: infer K extends "inner" | "cross"; } - ? IsWord, "join"> extends true - ? [K, Nat, Nx>, true] - : SyntaxErr> + ? IsWord, "join"> extends true + ? [K, Nat, Next>, true] + : SyntaxErr> : Nat extends true ? SyntaxErr : null @@ -705,38 +715,38 @@ type JoinConstraint< type ParseJoinConstraint = IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer E extends Expr, infer J extends number > ? Ok<[E, null], J> - : ParseExpr> + : ParseExpr> : IsWord extends true - ? IdentList> extends Ok< + ? IdentList> extends Ok< infer Cols extends readonly Name[], infer J extends number > ? Ok<[null, NamesV], J> - : IdentList> + : IdentList> : Ok<[null, null], I>; type ParseTableOrSub = IsWord extends true - ? IsOp, "("> extends true - ? ParseDerived>, true> - : SyntaxErr> + ? IsOp, "("> extends true + ? ParseDerived>, true> + : SyntaxErr> : IsOp extends true - ? IsSubqueryParen> extends true - ? ParseDerived, false> - : ParseFrom> extends Ok< + ? IsSubqueryParen> extends true + ? ParseDerived, false> + : ParseFrom> extends Ok< infer F extends FromItem, infer J extends number > ? CloseOk - : ParseFrom> + : ParseFrom> : IsWord extends true - ? IsOp, "("> extends true - ? ParseJsonTable>> + ? IsOp, "("> extends true + ? ParseJsonTable>> : ParseTableName : ParseTableName; @@ -762,7 +772,7 @@ type ParseJsonTable = ParseExpr extends Ok ? Expect extends infer J2 extends number ? T[J2] extends { readonly k: "s" } - ? JsonColumns> extends Ok< + ? JsonColumns> extends Ok< infer Cols extends readonly JsonTableCol[], infer J3 extends number > @@ -771,15 +781,15 @@ type ParseJsonTable = ? Ok<{ t: "jsontable"; doc: Doc; cols: Cols; alias: A }, J5> : AliasOf : Expect - : JsonColumns> + : JsonColumns> : SyntaxErr : Expect : ParseExpr; type JsonColumns = IsWord extends true - ? Expect, "("> extends infer J extends number + ? Expect, "("> extends infer J extends number ? JsonColLoop - : Expect, "("> + : Expect, "("> : SyntaxErr; type JsonColLoop< T extends Toks, @@ -788,64 +798,68 @@ type JsonColLoop< > = JsonCol extends Ok ? IsOp extends true - ? JsonColLoop, [...Acc, C]> + ? JsonColLoop, [...Acc, C]> : CloseOk : JsonCol; /** `name FOR ORDINALITY`, `name type [EXISTS] PATH 'p' [on empty/error]` or `NESTED [PATH] 'p' COLUMNS (…)`. */ type JsonCol = IsWord extends true ? ( - IsWord, "path"> extends true - ? Nx> - : Nx + IsWord, "path"> extends true + ? Next> + : Next ) extends infer J extends number ? T[J] extends { readonly k: "s" } - ? JsonColumns> extends Ok + ? JsonColumns> extends Ok< + infer Cols, + infer J2 extends number + > ? Ok<{ t: "nested"; cols: Cols }, J2> - : JsonColumns> + : JsonColumns> : SyntaxErr : never : Ident extends infer N extends Name - ? IsWord, "for"> extends true - ? IsWord>, "ordinality"> extends true - ? Ok<{ t: "ordinality"; name: N["n"] }, Nx>>> - : SyntaxErr>> - : ParseCastType> extends Ok< + ? IsWord, "for"> extends true + ? IsWord>, "ordinality"> extends true + ? Ok<{ t: "ordinality"; name: N["n"] }, Next>>> + : SyntaxErr>> + : ParseCastType> extends Ok< infer Ty extends string, infer J extends number > ? JsonPathCol, N["n"], Ty> - : ParseCastType> + : ParseCastType> : SyntaxErr; type SkipCollate = - IsWord extends true ? Nx> : I; + IsWord extends true ? Next> : I; type JsonPathCol< T extends Toks, I extends number, N extends string, Ty extends string, -> = (IsWord extends true ? [true, Nx] : [false, I]) extends [ - infer Ex extends boolean, - infer J extends number, -] +> = ( + IsWord extends true + ? [true, Next] + : [false, I] +) extends [infer Ex extends boolean, infer J extends number] ? IsWord extends true - ? T[Nx] extends { readonly k: "s" } - ? SkipJsonOn>> extends infer J2 extends number + ? T[Next] extends { readonly k: "s" } + ? SkipJsonOn>> extends infer J2 extends number ? Ok<{ t: "path"; name: N; type: Ty; exists: Ex }, J2> - : SkipJsonOn>> - : SyntaxErr> + : SkipJsonOn>> + : SyntaxErr> : SyntaxErr : never; /** `{NULL | ERROR | DEFAULT 'x'} ON {EMPTY | ERROR}`, up to twice. */ type SkipJsonOn = IsWord extends true - ? IsWord, "on"> extends true - ? SkipJsonOn>>> - : SyntaxErr> + ? IsWord, "on"> extends true + ? SkipJsonOn>>> + : SyntaxErr> : IsWord extends true - ? IsWord>, "on"> extends true - ? SkipJsonOn>>>> - : SyntaxErr>> + ? IsWord>, "on"> extends true + ? SkipJsonOn>>>> + : SyntaxErr>> : I; type ParseTableName = @@ -868,34 +882,34 @@ type TableTail = type SkipPartition = IsWord extends true - ? IdentList> extends Ok + ? IdentList> extends Ok ? J - : IdentList> + : IdentList> : I; /** `USE INDEX (…)`, `FORCE KEY FOR JOIN (…)`, `IGNORE INDEX FOR ORDER BY (…)`, … */ type SkipIndexHints = IsWord extends true - ? IsWord, "index" | "key"> extends true - ? IndexHintFor>> extends infer J extends number + ? IsWord, "index" | "key"> extends true + ? IndexHintFor>> extends infer J extends number ? IsOp extends true - ? SkipNameList> extends infer J2 extends number + ? SkipNameList> extends infer J2 extends number ? SkipIndexHints - : SkipNameList> + : SkipNameList> : SyntaxErr - : IndexHintFor>> + : IndexHintFor>> : I : I; /** `FOR JOIN`, `FOR ORDER BY`, `FOR GROUP BY` of an index hint. */ type IndexHintFor = IsWord extends true - ? IsWord, "join"> extends true - ? Nx> - : IsWord, "order" | "group"> extends true - ? IsWord>, "by"> extends true - ? Nx>> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "join"> extends true + ? Next> + : IsWord, "order" | "group"> extends true + ? IsWord>, "by"> extends true + ? Next>> + : SyntaxErr>> + : SyntaxErr> : I; /** `(a, PRIMARY, b)`: index names, which may be reserved words. */ @@ -903,22 +917,22 @@ type SkipNameList = T[I] extends { readonly k: "o"; readonly v: ")"; } - ? Nx + ? Next : T[I] extends { readonly k: "w" | "id" } - ? IsOp, ","> extends true - ? SkipNameList>> - : Expect, ")"> + ? IsOp, ","> extends true + ? SkipNameList>> + : Expect, ")"> : SyntaxErr; type AliasOf = IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? [A["v"], Nx>] - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? [A["v"], Next>] + : SyntaxErr> : TableAlias extends infer A extends Name ? IsWindowClause extends true ? [null, I] - : [A["v"], Nx] + : [A["v"], Next] : [null, I]; type WithAlias = @@ -931,7 +945,7 @@ type Aliased = F extends { readonly t: "table"; readonly ref: infer R } type IsWindowClause = IsWord extends true - ? Ident> extends Name - ? IsWord>, "as"> + ? Ident> extends Name + ? IsWord>, "as"> : false : false; diff --git a/packages/core/src/mysql/engine/parser/stmt.ts b/packages/core/src/mysql/engine/parser/stmt.ts index bdadd088..eb5d4ea3 100644 --- a/packages/core/src/mysql/engine/parser/stmt.ts +++ b/packages/core/src/mysql/engine/parser/stmt.ts @@ -1,3 +1,13 @@ +import type { Unlocated } from "../../../engine/family/diagnostics.js"; +import type { + IsOp, + IsWord, + SkipSemis, + SkipStatement, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { AtEnd, Ok, Toks } from "../../../engine/tokens.js"; import type { SqlError } from "../../../errors.js"; import type { Stmt, WithClause } from "../ast.js"; import type { @@ -6,38 +16,37 @@ import type { ParseDrop, ParseRename, ParseTruncate, - SkipStatement, } from "./ddl.js"; import type { ParseDelete, ParseInsert, ParseUpdate } from "./dml.js"; import type { ParseSelectAfterWith, ParseWith } from "./select.js"; -import type { AtEnd, IsOp, IsWord, Nx, Ok, SyntaxErr, Toks } from "./util.js"; +import type { SyntaxWords } from "./util.js"; -export type ParseStatement = T[I] extends { +type StatementAt = T[I] extends { readonly k: "w"; readonly v: infer W extends string; } ? W extends "select" | "values" | "table" ? ParseSelectAfterWith : W extends "with" - ? ParseWithStatement> + ? ParseWithStatement> : W extends "insert" | "replace" ? ParseInsert : W extends "update" - ? ParseUpdate, null> + ? ParseUpdate, null> : W extends "delete" - ? ParseDelete, null> + ? ParseDelete, null> : W extends "create" - ? ParseCreate> + ? ParseCreate> : W extends "alter" - ? ParseAlter> + ? ParseAlter> : W extends "drop" - ? ParseDrop> + ? ParseDrop> : W extends "rename" - ? ParseRename> + ? ParseRename> : W extends "truncate" - ? ParseTruncate> + ? ParseTruncate> : W extends NoopKeyword - ? SkipStatement, W> + ? SkipStatement, W> : SyntaxErr : IsOp extends true ? ParseSelectAfterWith @@ -101,20 +110,17 @@ type ParseWithStatement = : IsOp extends true ? ParseSelectAfterWith : IsWord extends true - ? ParseUpdate, Wc> + ? ParseUpdate, Wc> : IsWord extends true - ? ParseDelete, Wc> + ? ParseDelete, Wc> : SyntaxErr : ParseWith; -type SkipSemis = - IsOp extends true ? SkipSemis> : I; - -export type ParseProgram = +export type ParseProgramAt = SkipSemis extends infer I0 extends number ? AtEnd extends true ? SqlError<"Query was empty", 0> - : ParseStatement extends Ok< + : StatementAt extends Ok< infer S extends Stmt, infer J extends number > @@ -127,7 +133,7 @@ export type ParseProgram = : SqlError<"only one statement is allowed", J2> : never : SyntaxErr - : ParseStatement + : StatementAt : never; export type ParseScript< @@ -138,7 +144,7 @@ export type ParseScript< SkipSemis extends infer I0 extends number ? AtEnd extends true ? Acc - : ParseStatement extends Ok< + : StatementAt extends Ok< infer S extends Stmt, infer J extends number > @@ -147,5 +153,17 @@ export type ParseScript< : IsOp extends true ? ParseScript : SyntaxErr - : ParseStatement + : StatementAt : never; + +/** The statement at token `I`, as `Ok` or the error. */ +export type ParseStatement = Unlocated< + StatementAt, + SyntaxWords +>; + +/** The one statement of `T`. */ +export type ParseProgram = Unlocated< + ParseProgramAt, + SyntaxWords +>; diff --git a/packages/core/src/mysql/engine/parser/typename.ts b/packages/core/src/mysql/engine/parser/typename.ts index 538a9d1c..4ec282c0 100644 --- a/packages/core/src/mysql/engine/parser/typename.ts +++ b/packages/core/src/mysql/engine/parser/typename.ts @@ -1,5 +1,12 @@ -import type { NameOf } from "../tokens.js"; -import type { Expect, IsOp, IsWord, Nx, Ok, SyntaxErr, Toks } from "./util.js"; +import type { NameOf } from "../../../engine/family/lex.js"; +import type { + Expect, + IsOp, + IsWord, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; /** * A column type in `CREATE TABLE`, normalized to the name TypeSQL types it by: @@ -41,40 +48,43 @@ type TypeWords = T[I] extends { readonly v: infer W extends string; } ? W extends "double" - ? IsWord, "precision"> extends true - ? ["double", Nx>] - : ["double", Nx] + ? IsWord, "precision"> extends true + ? ["double", Next>] + : ["double", Next] : W extends "character" | "char" - ? IsWord, "varying"> extends true - ? ["varchar", Nx>] - : [W, Nx] + ? IsWord, "varying"> extends true + ? ["varchar", Next>] + : [W, Next] : W extends "national" - ? T[Nx] extends { + ? T[Next] extends { readonly k: "w"; readonly v: infer W2 extends "char" | "character" | "varchar"; } - ? IsWord>, "varying"> extends true - ? ["varchar", Nx>>] - : [W2, Nx>] - : [W, Nx] + ? IsWord>, "varying"> extends true + ? ["varchar", Next>>] + : [W2, Next>] + : [W, Next] : W extends "nchar" - ? IsWord, "varying" | "varchar"> extends true - ? ["varchar", Nx>] - : ["char", Nx] + ? IsWord, "varying" | "varchar"> extends true + ? ["varchar", Next>] + : ["char", Next] : W extends "long" - ? T[Nx] extends { + ? T[Next] extends { readonly k: "w"; readonly v: infer W2 extends "varchar" | "varbinary"; } - ? [W2 extends "varchar" ? "mediumtext" : "mediumblob", Nx>] - : IsWord, "char"> extends true - ? IsWord>, "varying"> extends true - ? ["mediumtext", Nx>>] - : ["mediumtext", Nx>] - : ["mediumtext", Nx] - : [W, Nx] + ? [ + W2 extends "varchar" ? "mediumtext" : "mediumblob", + Next>, + ] + : IsWord, "char"> extends true + ? IsWord>, "varying"> extends true + ? ["mediumtext", Next>>] + : ["mediumtext", Next>] + : ["mediumtext", Next] + : [W, Next] : T[I] extends { readonly k: "id" } - ? [Lowercase & string>, Nx] + ? [Lowercase & string>, Next] : null; type Canon = W extends @@ -151,24 +161,24 @@ type Canon = W extends type TypeArgs = IsOp extends true - ? T[Nx] extends { readonly k: "n"; readonly v: infer A extends string } - ? IsOp>, ","> extends true - ? T[Nx>>] extends { + ? T[Next] extends { readonly k: "n"; readonly v: infer A extends string } + ? IsOp>, ","> extends true + ? T[Next>>] extends { readonly k: "n"; readonly v: infer B extends string; } - ? Expect>>>, ")"> extends infer J + ? Expect>>>, ")"> extends infer J ? J extends number ? Ok<[A, B], J> : J : never - : SyntaxErr>>> - : Expect>, ")"> extends infer J + : SyntaxErr>>> + : Expect>, ")"> extends infer J ? J extends number ? Ok<[A], J> : J : never - : SyntaxErr> + : SyntaxErr> : Ok<[], I>; type Modifiers< @@ -177,9 +187,9 @@ type Modifiers< U extends boolean, > = T[I] extends { readonly k: "w"; readonly v: infer W extends string } ? W extends "unsigned" - ? Modifiers, true> + ? Modifiers, true> : W extends "signed" | "zerofill" - ? Modifiers, U> + ? Modifiers, U> : [U, I] : [U, I]; @@ -201,16 +211,18 @@ type Final< /** `ENUM('a', 'b')` and `SET('a', 'b')`: the values, joined by `\u0000`. */ type EnumValues = - IsOp extends true ? EnumLoop, C, never> : SyntaxErr; + IsOp extends true + ? EnumLoop, C, never> + : SyntaxErr; type EnumLoop< T extends Toks, I extends number, C extends string, Acc extends string, > = T[I] extends { readonly k: "s"; readonly v: infer V extends string } - ? IsOp, ","> extends true - ? EnumLoop>, C, Acc | V> - : Expect, ")"> extends infer J + ? IsOp, ","> extends true + ? EnumLoop>, C, Acc | V> + : Expect, ")"> extends infer J ? J extends number ? Ok<{ type: C; enumValues: Acc | V }, J> : J @@ -224,25 +236,25 @@ export type ParseCastType = T[I] extends { } ? W extends "signed" | "unsigned" ? ( - IsWord, "integer" | "int"> extends true - ? Nx> - : Nx + IsWord, "integer" | "int"> extends true + ? Next> + : Next ) extends infer J extends number ? Ok : never : W extends "integer" | "int" - ? Ok<"bigint", Nx> + ? Ok<"bigint", Next> : W extends "char" | "nchar" | "varchar" | "character" | "national" ? CastLength< T, - W extends "national" ? Nx> : Nx + W extends "national" ? Next> : Next > extends infer J ? J extends number ? Ok<"varchar", SkipCharset> : J : never : W extends "binary" | "varbinary" - ? CastLength> extends infer J + ? CastLength> extends infer J ? J extends number ? Ok<"varbinary", J> : J @@ -258,11 +270,11 @@ export type ParseCastType = T[I] extends { | "json" | "year" | "dec" - ? CastArgs> extends infer J + ? CastArgs> extends infer J ? J extends number ? Ok< - CastOf>, - IsWord extends true ? Nx : J + CastOf>, + IsWord extends true ? Next : J > : J : never @@ -276,7 +288,7 @@ type CastOf = W extends ? "decimal" : W extends "float" ? IsOp extends true - ? T[Nx] extends { + ? T[Next] extends { readonly v: `${"25" | "26" | "27" | "28" | "29" | "3" | "4" | "5"}${string}`; } ? "double" @@ -285,9 +297,9 @@ type CastOf = W extends : W; type CastLength = IsOp extends true - ? T[Nx] extends { readonly k: "n" } - ? Expect>, ")"> - : SyntaxErr> + ? T[Next] extends { readonly k: "n" } + ? Expect>, ")"> + : SyntaxErr> : I; type CastArgs = TypeArgs extends infer A @@ -297,11 +309,11 @@ type CastArgs = : never; type SkipCharset = IsWord extends true - ? IsWord, "set"> extends true - ? Nx>> + ? IsWord, "set"> extends true + ? Next>> : I : IsWord extends true - ? Nx> + ? Next> : IsWord extends true - ? Nx + ? Next : I; diff --git a/packages/core/src/mysql/engine/parser/util.ts b/packages/core/src/mysql/engine/parser/util.ts index ac8323db..c9b8643d 100644 --- a/packages/core/src/mysql/engine/parser/util.ts +++ b/packages/core/src/mysql/engine/parser/util.ts @@ -1,45 +1,21 @@ -import type { AtEnd, Nx, Ok } from "../../../engine/parse.js"; +import type { + AliasAt, + IdentAt, + IdentList as IdentListOf, + OptCols as OptColsOf, + TableRefAt as TableRefAtOf, +} from "../../../engine/family/parse.js"; import type { Toks } from "../../../engine/tokens.js"; -import type { SqlError } from "../../../errors.js"; -import type { NameOf, RawOf } from "../tokens.js"; import type { NotBareAlias, NotTableAlias, Reserved } from "./keywords.js"; -export type { AtEnd, Nx, Ok } from "../../../engine/parse.js"; - -export type { Toks }; - -export type SyntaxErr = SqlError< - AtEnd extends true - ? "You have an error in your SQL syntax near the end of the statement" - : T[I] extends { readonly k: string } - ? `You have an error in your SQL syntax near '${RawOf}'` - : "You have an error in your SQL syntax near the end of the statement", - I ->; - -export type Name = { readonly v: string; readonly n: string }; - -type IdentAt = T[I] extends { - readonly k: "id"; -} - ? Name & { readonly v: T[I]["v" & keyof T[I]]; readonly n: NameOf } - : T[I] extends { readonly k: "w"; readonly v: infer V } - ? V extends Not - ? null - : { readonly v: V; readonly n: NameOf } - : null; +/** How the server words a syntax error. */ +export type SyntaxWords = { + readonly near: ["You have an error in your SQL syntax near '", "'"]; + readonly end: "You have an error in your SQL syntax near the end of the statement"; +}; export type Ident = IdentAt; -export type AliasAt = T[I] extends { - readonly k: "s"; - readonly v: infer V extends string; -} - ? T[I] extends { readonly blob: true } - ? null - : { readonly v: Lowercase; readonly n: V } - : IdentAt; - export type BareAlias = AliasAt< T, I, @@ -51,71 +27,20 @@ export type TableAlias = AliasAt< NotTableAlias >; -export type IsOp< - T extends Toks, - I extends number, - O extends string, -> = T[I] extends { readonly k: "o"; readonly v: O } ? true : false; -export type IsWord< - T extends Toks, - I extends number, - W extends string, -> = T[I] extends { readonly k: "w"; readonly v: W } ? true : false; - -export type Expect< - T extends Toks, - I extends number, - O extends string, -> = T[I] extends { readonly k: "o"; readonly v: O } ? Nx : SyntaxErr; - -export type CloseOk = - IsOp extends true ? Ok> : SyntaxErr; - -export type SpanText = Trim< - SpanLoop, J, RawOf> +export type IdentList = IdentListOf< + T, + I, + Reserved >; -type SpanLoop< - T extends Toks, - I extends number, - J extends number, - Acc extends string, -> = I extends J - ? T[I] extends { readonly g: infer G extends string } - ? `${Acc}${G}` - : Acc - : T[I] extends { readonly g: infer G extends string } - ? SpanLoop, J, `${Acc}${G}${RawOf}`> - : Acc; -type Space = " " | "\n" | "\t" | "\r" | "\f" | "\v"; -type Trim = S extends `${infer R}${Space}` ? Trim : S; - -export type IdentList = - Expect extends infer J extends number - ? IdentListLoop - : Expect; -type IdentListLoop< - T extends Toks, - I extends number, - Acc extends readonly Name[], -> = - Ident extends infer N extends Name - ? T[Nx] extends { readonly k: "o"; readonly v: "," } - ? IdentListLoop>, [...Acc, N]> - : CloseOk, [...Acc, N]> - : SyntaxErr; - -export type OptCols = - IsOp extends true ? IdentList : Ok; -export type NamesV = { - [K in keyof N]: N[K] extends Name ? N[K]["v"] : never; -}; +export type OptCols = OptColsOf< + T, + I, + Reserved +>; -export type TableRefAt = - Ident extends infer A extends Name - ? IsOp, "."> extends true - ? Ident>> extends infer B extends Name - ? Ok<{ schema: A["v"]; name: B["v"] }, Nx>>> - : SyntaxErr>> - : Ok<{ schema: null; name: A["v"] }, Nx> - : SyntaxErr; +export type TableRefAt = TableRefAtOf< + T, + I, + Reserved +>; diff --git a/packages/core/src/mysql/engine/project/dml.ts b/packages/core/src/mysql/engine/project/dml.ts index 46f048b0..cacd527a 100644 --- a/packages/core/src/mysql/engine/project/dml.ts +++ b/packages/core/src/mysql/engine/project/dml.ts @@ -1,5 +1,9 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, QueryContext, Scope } from "../../../engine/types.js"; +import type { ColumnOrder, VisibleKeys } from "../../../engine/family/ddl.js"; +import type { NameAt, NamedAt } from "../../../engine/family/diagnostics.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { IsUnion } from "../../../engine/object.js"; +import type { ColInfo, Info, Scope } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; import type { @@ -31,7 +35,7 @@ import type { ParamsOfTargets, Slot, } from "../typer/params.js"; -import type { Info, TypeList, TypeOf } from "../typer/typeof.js"; +import type { TypeList, TypeOf } from "../typer/typeof.js"; import type { Cell, FlavorOf, @@ -72,7 +76,10 @@ export type ProjectDml = type NoReturning = FlavorOf extends "mariadb" ? null - : SqlError<"You have an error in your SQL syntax near 'RETURNING'", null>; + : SqlError< + "You have an error in your SQL syntax near 'RETURNING'", + NameAt<"returning"> + >; type Returning< Ts extends readonly Target[], @@ -89,7 +96,7 @@ type RetLoop< ? T extends { readonly t: "star" } ? StarCells<[It], T["tbl"]> extends infer S ? S extends readonly Cell[] - ? RetLoop, [...Acc, ...S]> + ? RetLoop, [...Acc, ...S]> : S : never : T extends { @@ -106,7 +113,7 @@ type RetLoop< Ts, It, C, - Nx, + Next, [ ...Acc, { @@ -131,7 +138,7 @@ type NameOf = { type GeneratedErr = SqlError< `The value specified for generated column '${Name}' in table '${K}' is not allowed.`, - null + NamedAt >; type CheckCols< @@ -142,12 +149,12 @@ type CheckCols< I extends number = 0, > = Cols[I] extends infer C extends string ? [ColOf] extends [never] - ? SqlError<`Unknown column '${C}' in 'field list'`, null> + ? SqlError<`Unknown column '${C}' in 'field list'`, NamedAt> : ColOf extends { readonly generated: true } ? AllDefault extends true - ? CheckCols> + ? CheckCols> : GeneratedErr, K> - : CheckCols> + : CheckCols> : true; /** Generated columns accept only DEFAULT. */ type AllDefault< @@ -184,10 +191,14 @@ type RowsMatch< I extends number = 0, > = Rows[I] extends infer R extends readonly Expr[] ? R["length"] extends N - ? RowsMatch> - : SqlError<`Column count doesn't match value count at row ${Nx}`, null> + ? RowsMatch> + : SqlError< + `Column count doesn't match value count at row ${Next}`, + NameAt<"values"> + > : true; +/** The columns the statement fills, in order: `null` when the table's column order is unknown. */ type InsertCols = S["source"] extends { readonly t: "set"; readonly set: infer Set extends readonly SetItem[]; @@ -196,6 +207,14 @@ type InsertCols = S["source"] extends { : S["columns"] extends readonly string[] ? S["columns"] : InsertOrder; +/** The columns the statement fills, in order when it is known. */ +type FilledCols = + InsertCols extends infer C extends readonly string[] + ? C + : ColumnOrder>; +/** The columns its values are typed against by position: none when the order is unknown. */ +type TypedCols = + InsertCols extends infer C extends readonly string[] ? C : []; type ProjectInsert = Target_ extends infer Tg @@ -213,9 +232,9 @@ type ProjectInsert = : null ) extends infer RE extends AnyErr ? RE - : InsertCols extends infer Cols extends readonly string[] + : FilledCols extends infer Cols extends readonly string[] ? CheckCols< - Cols, + TypedCols, Rel, K, S["source"] extends { @@ -299,7 +318,7 @@ type InsertRest< "You have an error in your SQL syntax near 'ON DUPLICATE'", null > - : OdkuCtx extends infer C + : OdkuCtx extends infer C ? C extends MyCtx ? CheckSet : C @@ -322,6 +341,7 @@ type InsertRest< // table gives that order, as SQLite's `excluded` has. type OdkuCtx< S extends InsertStmt, + Rel, It extends MyItem, DB, Ctes extends MyCteMap, @@ -335,7 +355,7 @@ type OdkuCtx< [It], [ AC extends readonly string[] - ? RenamedItem>, AC, A> + ? RenamedItem, AC, A> : { readonly alias: A; readonly cols: It["cols"]; @@ -347,7 +367,6 @@ type OdkuCtx< Ctes > : Ctx; -type ColsRel = It["cols"]; type RenamedItem< It extends MyItem, @@ -357,8 +376,9 @@ type RenamedItem< > = { readonly alias: A; readonly cols: { - readonly [I in keyof As & `${number}` as Lowercase]: Cols[I & - keyof Cols] extends infer C extends keyof It["cols"] + readonly [I in keyof As & `${number}` as Lowercase]: Lowercase< + Cols[I & keyof Cols] & string + > extends infer C extends keyof It["cols"] ? It["cols"][C] : { readonly type: "unknown"; @@ -378,7 +398,7 @@ type ValuesTyped< ? TypeList, C> extends infer X ? X extends AnyErr ? X - : ValuesTyped> + : ValuesTyped> : never : true; type NonDefault = R extends readonly [ @@ -402,10 +422,13 @@ type MissingRequired = { }[keyof Rel & string] extends infer M extends string ? [M] extends [never] ? true - : SqlError<`Field '${FirstOf}' doesn't have a default value`, null> + : SqlError< + `Field '${FirstOf}' doesn't have a default value`, + NamedAt> + > : never; type FirstOf = - import("../../../sql/splice.js").UnionList extends readonly [ + import("../../../engine/object.js").Members extends readonly [ infer F extends string, ...unknown[], ] @@ -429,12 +452,6 @@ type SetCol = { : never; }[number]; -type IsUnion = T extends unknown - ? [U] extends [T] - ? false - : true - : never; - type CheckSet< Set extends readonly SetItem[], L extends MyLevel, @@ -446,25 +463,28 @@ type CheckSet< ? [Col] extends [never] ? SqlError< `Unknown column '${S["tbl"] extends string ? `${S["tbl"]}.` : ""}${S["col"]}' in 'field list'`, - null + NamedAt > : IsUnion

extends true - ? SqlError<`Column '${S["col"]}' in field list is ambiguous`, null> + ? SqlError< + `Column '${S["col"]}' in field list is ambiguous`, + NamedAt + > : Col extends { readonly gen: true } ? S["e"] extends { readonly t: "default" } - ? CheckSet> + ? CheckSet> : GeneratedErr< Col extends MyCol ? Col["n"] : S["col"], Col extends { readonly tbl: infer T extends string } ? T : K > : S["e"] extends { readonly t: "default" } - ? CheckSet> + ? CheckSet> : HasAgg extends true ? SqlError<"Invalid use of group function", null> : TypeOf extends infer X ? X extends AnyErr ? X - : CheckSet> + : CheckSet> : never : never : true; @@ -477,7 +497,7 @@ type OrderLimitOk< ? S extends { readonly orderBy: readonly [unknown, ...unknown[]] } ? SqlError<`Incorrect usage of ${Verb} and ORDER BY`, null> : S extends { readonly limit: Expr } - ? SqlError<`Incorrect usage of ${Verb} and LIMIT`, null> + ? SqlError<`Incorrect usage of ${Verb} and LIMIT`, NameAt<"limit">> : true : true; @@ -535,8 +555,11 @@ type DeleteTargetsOk< : false : false; }[number] - ? DeleteTargetsOk> - : SqlError<`Unknown table '${R["name"]}' in MULTI DELETE`, null> + ? DeleteTargetsOk> + : SqlError< + `Unknown table '${R["name"]}' in MULTI DELETE`, + NamedAt + > : true; type CheckDelete = ( @@ -591,7 +614,7 @@ type ColParam = C extends keyof It["cols"] type InsertParams = Target_ extends [infer Rel, string] ? Item> extends infer It extends MyItem - ? InsertCols extends infer Cols extends readonly string[] + ? TypedCols extends infer Cols extends readonly string[] ? Ctx extends infer C extends MyCtx ? | (S["source"] extends { @@ -612,7 +635,7 @@ type InsertParams = ? SetParams : never) | (S["odku"] extends readonly SetItem[] - ? OdkuCtx extends infer OC extends MyCtx + ? OdkuCtx extends infer OC extends MyCtx ? SetParams : never : never) diff --git a/packages/core/src/mysql/engine/project/select.ts b/packages/core/src/mysql/engine/project/select.ts index 8d19a133..c3da17c0 100644 --- a/packages/core/src/mysql/engine/project/select.ts +++ b/packages/core/src/mysql/engine/project/select.ts @@ -1,6 +1,10 @@ +import type { NameAt, NamedAt } from "../../../engine/family/diagnostics.js"; +import type { NullLevel, RenameCells } from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; import type { NarrowKeys, NarrowLevel } from "../../../engine/narrow.js"; -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, QueryContext, Scope } from "../../../engine/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Tup } from "../../../engine/num.js"; +import type { ColInfo, Info, Scope } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Cte, @@ -17,12 +21,10 @@ import type { BindFrom, CteEntry, DupCell, - NullLevel, - RenameCells, StarCells, } from "../binder/scope.js"; import type { IsAggCall } from "../typer/functions.js"; -import type { Common, Info, TypeList, TypeOf } from "../typer/typeof.js"; +import type { Common, TypeList, TypeOf } from "../typer/typeof.js"; import type { AggMode, Cell, @@ -32,7 +34,6 @@ import type { MyColOf, MyCteMap, MyCtx, - MyItem, MyLevel, } from "../types.js"; @@ -65,9 +66,12 @@ type LimitOk = E extends null ? true : E extends { readonly t: "num"; readonly v: infer V extends string } ? V extends `${string}${"." | "e" | "E" | "-"}${string}` - ? SqlError<"Incorrect arguments to LIMIT", null> + ? SqlError<"Incorrect arguments to LIMIT", NameAt<"limit">> : true - : SqlError<"You have an error in your SQL syntax near LIMIT", null>; + : SqlError< + "You have an error in your SQL syntax near LIMIT", + NameAt<"limit"> + >; export type BindWith< W, @@ -89,7 +93,7 @@ type CteLoop< Env["scope"], Env["ctes"] & { readonly [K in C["name"]]: CteEntry } >, - Nx + Next > : CteCells : Env["ctes"]; @@ -249,11 +253,17 @@ type CompoundOrder< readonly name: infer N extends string; } ? N extends Lowercase - ? CompoundOrder> - : SqlError<`Unknown column '${E["disp"]}' in 'order clause'`, null> + ? CompoundOrder> + : SqlError< + `Unknown column '${E["disp"]}' in 'order clause'`, + NamedAt + > : E extends { readonly t: "col" } - ? SqlError<`Unknown column '${E["disp"]}' in 'order clause'`, null> - : CompoundOrder> + ? SqlError< + `Unknown column '${E["disp"]}' in 'order clause'`, + NamedAt + > + : CompoundOrder> : true; type PositionOk< @@ -263,21 +273,14 @@ type PositionOk< > = V extends `${infer K extends number}` ? `${K}` extends V ? K extends 0 - ? SqlError<`Unknown column '${V}' in '${Where}'`, null> - : PosGE extends true + ? SqlError<`Unknown column '${V}' in '${Where}'`, NamedAt> + : // A position from a literal may be any size, so only the result's + // columns are counted: `K` is one of 1…N. + `${K}` extends keyof [0, ...Tup] ? true - : SqlError<`Unknown column '${V}' in '${Where}'`, null> + : SqlError<`Unknown column '${V}' in '${Where}'`, NamedAt> : true : true; -// A position from a literal may be any size: the tuple stops at 64, past -// which no result has a column anyway. -type PosGE = - PosTup extends [...PosTup, ...unknown[]] ? true : false; -type PosTup = A["length"] extends N - ? A - : A["length"] extends 64 - ? A - : PosTup; type ProjectValues< B extends ValuesBody, @@ -333,7 +336,7 @@ type ValuesRows< Acc extends readonly (readonly ColInfo[])[], > = Rows[I] extends infer Row extends readonly Expr[] ? TypeList extends infer X extends readonly ColInfo[] - ? ValuesRows, [...Acc, X]> + ? ValuesRows, [...Acc, X]> : TypeList : Acc; @@ -341,7 +344,9 @@ export type HasAgg = E extends { readonly t: string } ? IsAggCall extends true ? true : E extends { readonly t: "sub" | "exists" | "insub" | "quant" } - ? E extends { readonly t: "insub" | "quant"; readonly e: infer X } + ? E extends + | { readonly t: "insub"; readonly e: infer X } + | { readonly t: "quant"; readonly l: infer X } ? HasAgg : false : true extends HasAggIn @@ -370,8 +375,8 @@ type HasAggList< ? true : HasAgg extends true ? true - : HasAggList> - : HasAggList>; + : HasAggList> + : HasAggList>; /** A column reference outside any aggregate (and outside subqueries). */ type BareCol = E extends { @@ -386,7 +391,9 @@ type BareCol = E extends { ? never : E extends { readonly t: "sub" | "exists" } ? never - : E extends { readonly t: "insub" | "quant"; readonly e: infer X } + : E extends + | { readonly t: "insub"; readonly e: infer X } + | { readonly t: "quant"; readonly l: infer X } ? BareCol : BareColIn : never; @@ -431,14 +438,14 @@ type FullGroupBy< ? T extends { readonly t: "expr"; readonly e: infer E } ? BareCol extends infer D extends string ? [D] extends [never] - ? FullGroupBy> + ? FullGroupBy> : SqlError< - `In aggregated query without GROUP BY, expression #${Nx} of SELECT list contains nonaggregated column '${FirstName}'; this is incompatible with sql_mode=only_full_group_by`, + `In aggregated query without GROUP BY, expression #${Next} of SELECT list contains nonaggregated column '${FirstName}'; this is incompatible with sql_mode=only_full_group_by`, null > : never : SqlError< - `In aggregated query without GROUP BY, expression #${Nx} of SELECT list contains nonaggregated column; this is incompatible with sql_mode=only_full_group_by`, + `In aggregated query without GROUP BY, expression #${Next} of SELECT list contains nonaggregated column; this is incompatible with sql_mode=only_full_group_by`, null > : true; @@ -446,7 +453,9 @@ type FullGroupBy< /** Names of windows referenced as `OVER name`, outside subqueries. */ type WindowRefs = E extends { readonly t: string } ? E extends { readonly t: "sub" | "exists" | "insub" | "quant" } - ? E extends { readonly t: "insub" | "quant"; readonly e: infer X } + ? E extends + | { readonly t: "insub"; readonly e: infer X } + | { readonly t: "quant"; readonly l: infer X } ? WindowRefs : never : @@ -483,7 +492,7 @@ type MissingWindow = : null : null; type FirstName = - import("../../../sql/splice.js").UnionList extends readonly [ + import("../../../engine/object.js").Members extends readonly [ infer F extends string, ...unknown[], ] @@ -516,7 +525,7 @@ type ProjectSimpleChecked< : ( S["where"] extends Expr ? NarrowKeys extends infer Keys - ? NarrowLevel, Keys> + ? NarrowLevel : never : L ) extends infer NL extends MyLevel @@ -583,42 +592,6 @@ type ProjectSimpleChecked< : never : BindFrom; -/** - * A WHERE condition that rejects NULL in a column of a table an outer join - * made nullable only keeps rows where that table matched: its columns get - * back their own nullability, as if the join were an inner join. - */ -type RestoreOuter = [Keys] extends [never] - ? L - : { - readonly [I in keyof L]: L[I] extends { - readonly orig: infer O extends MyColMap; - } - ? Matched extends true - ? { - readonly [P in keyof L[I]]: P extends "cols" ? O : L[I][P]; - } - : L[I] - : L[I]; - }; -type Matched = true extends ( - Keys extends readonly [infer T, infer C extends string] - ? C extends keyof It["cols"] - ? T extends null - ? C extends It["skip"][number] - ? false - : true - : It["alias"] extends string - ? Lowercase extends T - ? true - : false - : false - : false - : false -) - ? true - : false; - /** WHERE sees the tables only: MySQL resolves no SELECT alias there. */ type CheckWhere = W extends Expr ? HasAgg extends true @@ -658,7 +631,7 @@ type ProjectTargets< : never ) extends infer S ? S extends readonly Cell[] - ? ProjectTargets, [...Acc, ...S], Ret> + ? ProjectTargets, [...Acc, ...S], Ret> : S : never : T extends { @@ -673,7 +646,7 @@ type ProjectTargets< Ts, L, C, - Nx, + Next, [ ...Acc, { @@ -758,10 +731,10 @@ type ClauseTerms< ? E extends { readonly t: "num"; readonly v: infer V extends string } ? PositionOk extends AnyErr ? PositionOk - : ClauseTerms> + : ClauseTerms> : (W extends "group statement" ? HasAgg : false) extends true ? SqlError<"Can't group on aggregate", null> : TypeOf extends AnyErr ? TypeOf - : ClauseTerms> + : ClauseTerms> : true; diff --git a/packages/core/src/mysql/engine/tokens.ts b/packages/core/src/mysql/engine/tokens.ts deleted file mode 100644 index 4e4a4be7..00000000 --- a/packages/core/src/mysql/engine/tokens.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type MyWord = { - readonly k: "w"; - readonly v: V; - readonly r: R; - readonly g: G; -}; -export type MyIdent< - V extends string, - N extends string, - R extends string, - G extends string, -> = { - readonly k: "id"; - readonly v: V; - readonly n: N; - readonly r: R; - readonly g: G; -}; -export type MyNum = { - readonly k: "n"; - readonly v: V; - readonly g: G; -}; -export type MyStr< - V extends string, - R extends string, - G extends string, - F extends string = V, -> = { - readonly k: "s"; - readonly v: V; - readonly r: R; - readonly g: G; - /** The first of adjacent strings (`'a' 'b'`), which MySQL names the column after. */ - readonly f: F; -}; -export type MyBlob = { - readonly k: "s"; - readonly v: V; - readonly r: R; - readonly g: G; - readonly blob: true; -}; -export type MyParam = { - readonly k: "$"; - readonly v: N; - readonly g: G; -}; -export type MyOp = { - readonly k: "o"; - readonly v: V; - readonly g: G; -}; - -/** The text of a token as the server sees it: parameters are sent as `?`. */ -export type RawOf = T extends { readonly r: infer R extends string } - ? R - : T extends { readonly k: "$" } - ? "?" - : T extends { readonly v: infer V extends string } - ? V - : ""; - -export type NameOf = T extends { readonly k: "id"; readonly n: infer N } - ? N - : T extends { readonly k: "w"; readonly r: infer R } - ? R - : T extends { readonly k: "s"; readonly v: infer V } - ? V - : never; -export type MyVar = { - readonly k: "v"; - readonly v: V; - readonly r: R; - readonly g: G; -}; diff --git a/packages/core/src/mysql/engine/typer/functions.ts b/packages/core/src/mysql/engine/typer/functions.ts index e5db5f00..16838fa5 100644 --- a/packages/core/src/mysql/engine/typer/functions.ts +++ b/packages/core/src/mysql/engine/typer/functions.ts @@ -1,4 +1,11 @@ -import type { GE, Tup } from "../types.js"; +import type { + DeclaredFn, + Many, + MaxArgs, + MinArgs, + PickSig, +} from "../../../engine/family/functions.js"; +import type { Info } from "../../../engine/types.js"; /** * A built-in function: argument counts, how its result type is found and @@ -22,6 +29,7 @@ type F< Null extends NullRule, Kind extends "scalar" | "agg" | "win" = "scalar", Only extends "mysql" | "mariadb" | "both" = "both", + Args = readonly Info<"unknown", true>[], > = { readonly min: Min; readonly max: Max; @@ -29,8 +37,38 @@ type F< readonly null: Null; readonly kind: Kind; readonly only: Only; + readonly args: Args; }; +/** + * The type a parameter takes in each argument position, as in `UPPER($1)`; + * `unknown` without a rule. `same`: the first typed argument's type, for all; + * `if`: a condition, then the type both results share. + */ +type Text = readonly Info<"varchar", true>[]; +type Num = readonly Info<"double", true>[]; +type DateArgs = [Info<"datetime", true>, Info<"unknown", true>]; +type JsonArgs = [Info<"json", true>, ...Info<"unknown", true>[]]; +type Same = "same"; +type IfArgs = "if"; +type Substr = [ + Info<"varchar", true>, + Info<"bigint", true>, + Info<"bigint", true>, +]; +type Pad = [Info<"varchar", true>, Info<"bigint", true>, Info<"varchar", true>]; +type InsertArgs = [ + Info<"varchar", true>, + Info<"bigint", true>, + Info<"bigint", true>, + Info<"varchar", true>, +]; +type Offset = [ + Info<"unknown", true>, + Info<"bigint", false>, + Info<"unknown", true>, +]; + type NullRule = | "never" | "args" @@ -42,18 +80,16 @@ type NullRule = | "temporal" | "lag"; -type Many = 1000; - export interface Functions { // Control flow - if: F<3, 3, "iif", "iif">; - ifnull: F<2, 2, "common", "all">; - nvl: F<2, 2, "common", "all", "scalar", "mariadb">; - nullif: F<2, 2, "nullif", "always">; - coalesce: F<1, Many, "common", "all">; + if: F<3, 3, "iif", "iif", "scalar", "both", IfArgs>; + ifnull: F<2, 2, "common", "all", "scalar", "both", Same>; + nvl: F<2, 2, "common", "all", "scalar", "mariadb", Same>; + nullif: F<2, 2, "nullif", "always", "scalar", "both", Same>; + coalesce: F<1, Many, "common", "all", "scalar", "both", Same>; isnull: F<1, 1, "bool", "never">; - greatest: F<2, Many, "greatest", "args">; - least: F<2, Many, "greatest", "args">; + greatest: F<2, Many, "greatest", "args", "scalar", "both", Same>; + least: F<2, Many, "greatest", "args", "scalar", "both", Same>; interval: F<2, Many, "int", "never">; any_value: F<1, 1, "arg0", "arg0", "scalar", "mysql">; name_const: F<2, 2, "arg1", "arg0">; @@ -62,69 +98,69 @@ export interface Functions { value: F<1, 1, "arg0", "always", "scalar", "mariadb">; // Strings - ascii: F<1, 1, "int", "args">; + ascii: F<1, 1, "int", "args", "scalar", "both", Text>; bin: F<1, 1, "varchar", "args">; - bit_length: F<1, 1, "bigint", "args">; + bit_length: F<1, 1, "bigint", "args", "scalar", "both", Text>; char: F<1, Many, "varbinary", "never">; - char_length: F<1, 1, "intf", "args">; - character_length: F<1, 1, "intf", "args">; + char_length: F<1, 1, "intf", "args", "scalar", "both", Text>; + character_length: F<1, 1, "intf", "args", "scalar", "both", Text>; chr: F<1, 1, "varchar", "args", "scalar", "mariadb">; - concat: F<1, Many, "str", "args">; - concat_ws: F<2, Many, "str", "arg0">; - elt: F<2, Many, "str", "always">; + concat: F<1, Many, "str", "args", "scalar", "both", Text>; + concat_ws: F<2, Many, "str", "arg0", "scalar", "both", Text>; + elt: F<2, Many, "str", "always", "scalar", "both", Text>; export_set: F<3, 5, "varchar", "args">; - field: F<1, Many, "int", "never">; - find_in_set: F<2, 2, "int", "args">; + field: F<1, Many, "int", "never", "scalar", "both", Text>; + find_in_set: F<2, 2, "int", "args", "scalar", "both", Text>; format: F<2, 3, "varchar", "args">; - from_base64: F<1, 1, "varbinary", "always">; - hex: F<1, 1, "varchar", "args">; - insert: F<4, 4, "str", "args">; - instr: F<2, 2, "int", "args">; - lcase: F<1, 1, "str", "args">; - left: F<2, 2, "str", "args">; - length: F<1, 1, "intf", "args">; + from_base64: F<1, 1, "varbinary", "always", "scalar", "both", Text>; + hex: F<1, 1, "varchar", "args", "scalar", "both", Text>; + insert: F<4, 4, "str", "args", "scalar", "both", InsertArgs>; + instr: F<2, 2, "int", "args", "scalar", "both", Text>; + lcase: F<1, 1, "str", "args", "scalar", "both", Text>; + left: F<2, 2, "str", "args", "scalar", "both", Substr>; + length: F<1, 1, "intf", "args", "scalar", "both", Text>; length2: F<1, 1, "intf", "args", "scalar", "mariadb">; - octet_length: F<1, 1, "intf", "args">; - locate: F<2, 3, "int", "args">; - lower: F<1, 1, "str", "args">; - lpad: F<2, 3, "str", "args">; - ltrim: F<1, 1, "str", "args">; + octet_length: F<1, 1, "intf", "args", "scalar", "both", Text>; + locate: F<2, 3, "int", "args", "scalar", "both", Text>; + lower: F<1, 1, "str", "args", "scalar", "both", Text>; + lpad: F<2, 3, "str", "args", "scalar", "both", Pad>; + ltrim: F<1, 1, "str", "args", "scalar", "both", Text>; make_set: F<2, Many, "varchar", "arg0">; - mid: F<2, 3, "str", "args">; + mid: F<2, 3, "str", "args", "scalar", "both", Substr>; oct: F<1, 1, "varchar", "args">; - ord: F<1, 1, "intf", "args">; - position: F<2, 2, "int", "args">; - quote: F<1, 1, "varchar", "never">; - regexp_instr: F<2, 6, "int", "args">; - regexp_like: F<2, 3, "bool", "args", "scalar", "mysql">; - regexp_replace: F<3, 6, "str", "args">; - regexp_substr: F<2, 5, "str", "always">; - repeat: F<2, 2, "str", "args">; - replace: F<3, 3, "str", "args">; - reverse: F<1, 1, "str", "args">; - right: F<2, 2, "str", "args">; - rpad: F<2, 3, "str", "args">; - rtrim: F<1, 1, "str", "args">; - soundex: F<1, 1, "varchar", "args">; - space: F<1, 1, "varchar", "args">; - strcmp: F<2, 2, "int", "args">; - substr: F<2, 3, "str", "args">; - substring: F<2, 3, "str", "args">; - substring_index: F<3, 3, "str", "args">; - to_base64: F<1, 1, "varchar", "args">; - trim: F<1, 2, "str", "args">; - ucase: F<1, 1, "str", "args">; - unhex: F<1, 1, "varbinary", "always">; - upper: F<1, 1, "str", "args">; + ord: F<1, 1, "intf", "args", "scalar", "both", Text>; + position: F<2, 2, "int", "args", "scalar", "both", Text>; + quote: F<1, 1, "varchar", "never", "scalar", "both", Text>; + regexp_instr: F<2, 6, "int", "args", "scalar", "both", Text>; + regexp_like: F<2, 3, "bool", "args", "scalar", "mysql", Text>; + regexp_replace: F<3, 6, "str", "args", "scalar", "both", Text>; + regexp_substr: F<2, 5, "str", "always", "scalar", "both", Text>; + repeat: F<2, 2, "str", "args", "scalar", "both", Substr>; + replace: F<3, 3, "str", "args", "scalar", "both", Text>; + reverse: F<1, 1, "str", "args", "scalar", "both", Text>; + right: F<2, 2, "str", "args", "scalar", "both", Substr>; + rpad: F<2, 3, "str", "args", "scalar", "both", Pad>; + rtrim: F<1, 1, "str", "args", "scalar", "both", Text>; + soundex: F<1, 1, "varchar", "args", "scalar", "both", Text>; + space: F<1, 1, "varchar", "args", "scalar", "both", Num>; + strcmp: F<2, 2, "int", "args", "scalar", "both", Text>; + substr: F<2, 3, "str", "args", "scalar", "both", Substr>; + substring: F<2, 3, "str", "args", "scalar", "both", Substr>; + substring_index: F<3, 3, "str", "args", "scalar", "both", Text>; + to_base64: F<1, 1, "varchar", "args", "scalar", "both", Text>; + trim: F<1, 2, "str", "args", "scalar", "both", Text>; + ucase: F<1, 1, "str", "args", "scalar", "both", Text>; + unhex: F<1, 1, "varbinary", "always", "scalar", "both", Text>; + upper: F<1, 1, "str", "args", "scalar", "both", Text>; weight_string: F<1, 1, "varbinary", "args">; sformat: F<1, Many, "varchar", "args", "scalar", "mariadb">; natural_sort_key: F<1, 1, "varchar", "args", "scalar", "mariadb">; soundslike: F<2, 2, "bool", "args">; // Hashing, encryption, identifiers - md5: F<1, 1, "varchar", "args">; - sha: F<1, 1, "varchar", "args">; - sha1: F<1, 1, "varchar", "args">; + md5: F<1, 1, "varchar", "args", "scalar", "both", Text>; + sha: F<1, 1, "varchar", "args", "scalar", "both", Text>; + sha1: F<1, 1, "varchar", "args", "scalar", "both", Text>; sha2: F<2, 2, "varchar", "always">; crc32: F<1, 1, "int unsigned", "args">; crc32c: F<1, 2, "bigint unsigned", "args", "scalar", "mariadb">; @@ -139,12 +175,12 @@ export interface Functions { uuid_v7: F<0, 0, "uuid", "never", "scalar", "mariadb">; sys_guid: F<0, 0, "varchar", "never", "scalar", "mariadb">; uuid_short: F<0, 0, "bigint unsigned", "never">; - uuid_to_bin: F<1, 2, "varbinary", "args", "scalar", "mysql">; + uuid_to_bin: F<1, 2, "varbinary", "args", "scalar", "mysql", Text>; bin_to_uuid: F<1, 2, "varchar", "args", "scalar", "mysql">; - is_uuid: F<1, 1, "bool", "args", "scalar", "mysql">; - inet_aton: F<1, 1, "bigint unsigned", "always">; - inet_ntoa: F<1, 1, "varchar", "always">; - inet6_aton: F<1, 1, "varbinary", "always">; + is_uuid: F<1, 1, "bool", "args", "scalar", "mysql", Text>; + inet_aton: F<1, 1, "bigint unsigned", "always", "scalar", "both", Text>; + inet_ntoa: F<1, 1, "varchar", "always", "scalar", "both", Num>; + inet6_aton: F<1, 1, "varbinary", "always", "scalar", "both", Text>; inet6_ntoa: F<1, 1, "varchar", "always">; is_ipv4: F<1, 1, "bool", "never">; is_ipv6: F<1, 1, "bool", "never">; @@ -152,43 +188,43 @@ export interface Functions { is_ipv4_mapped: F<1, 1, "bool", "never">; // Numbers - abs: F<1, 1, "abs", "args">; - acos: F<1, 1, "double", "always">; - asin: F<1, 1, "double", "always">; - atan: F<1, 2, "double", "args">; - atan2: F<2, 2, "double", "args">; - ceil: F<1, 1, "ceil", "args">; - ceiling: F<1, 1, "ceil", "args">; + abs: F<1, 1, "abs", "args", "scalar", "both", Num>; + acos: F<1, 1, "double", "always", "scalar", "both", Num>; + asin: F<1, 1, "double", "always", "scalar", "both", Num>; + atan: F<1, 2, "double", "args", "scalar", "both", Num>; + atan2: F<2, 2, "double", "args", "scalar", "both", Num>; + ceil: F<1, 1, "ceil", "args", "scalar", "both", Num>; + ceiling: F<1, 1, "ceil", "args", "scalar", "both", Num>; conv: F<3, 3, "varchar", "always">; - cos: F<1, 1, "double", "args">; - cot: F<1, 1, "double", "always">; - degrees: F<1, 1, "double", "args">; - exp: F<1, 1, "double", "args">; - floor: F<1, 1, "ceil", "args">; - ln: F<1, 1, "double", "always">; - log: F<1, 2, "double", "always">; - log10: F<1, 1, "double", "always">; - log2: F<1, 1, "double", "always">; - mod: F<2, 2, "mod", "always">; + cos: F<1, 1, "double", "args", "scalar", "both", Num>; + cot: F<1, 1, "double", "always", "scalar", "both", Num>; + degrees: F<1, 1, "double", "args", "scalar", "both", Num>; + exp: F<1, 1, "double", "args", "scalar", "both", Num>; + floor: F<1, 1, "ceil", "args", "scalar", "both", Num>; + ln: F<1, 1, "double", "always", "scalar", "both", Num>; + log: F<1, 2, "double", "always", "scalar", "both", Num>; + log10: F<1, 1, "double", "always", "scalar", "both", Num>; + log2: F<1, 1, "double", "always", "scalar", "both", Num>; + mod: F<2, 2, "mod", "always", "scalar", "both", Num>; pi: F<0, 0, "double", "never">; - pow: F<2, 2, "double", "args">; - power: F<2, 2, "double", "args">; - radians: F<1, 1, "double", "args">; + pow: F<2, 2, "double", "args", "scalar", "both", Num>; + power: F<2, 2, "double", "args", "scalar", "both", Num>; + radians: F<1, 1, "double", "args", "scalar", "both", Num>; rand: F<0, 1, "double", "never">; - round: F<1, 2, "round", "args">; - sign: F<1, 1, "int", "args">; - sin: F<1, 1, "double", "args">; - sqrt: F<1, 1, "double", "always">; - tan: F<1, 1, "double", "args">; - truncate: F<2, 2, "round", "args">; + round: F<1, 2, "round", "args", "scalar", "both", Num>; + sign: F<1, 1, "int", "args", "scalar", "both", Num>; + sin: F<1, 1, "double", "args", "scalar", "both", Num>; + sqrt: F<1, 1, "double", "always", "scalar", "both", Num>; + tan: F<1, 1, "double", "args", "scalar", "both", Num>; + truncate: F<2, 2, "round", "args", "scalar", "both", Num>; // Dates and times - adddate: F<2, 2, "dateadd", "always">; - date_add: F<2, 2, "dateadd", "always">; - subdate: F<2, 2, "dateadd", "always">; - date_sub: F<2, 2, "dateadd", "always">; - addtime: F<2, 2, "addtime", "always">; - subtime: F<2, 2, "addtime", "always">; + adddate: F<2, 2, "dateadd", "always", "scalar", "both", DateArgs>; + date_add: F<2, 2, "dateadd", "always", "scalar", "both", DateArgs>; + subdate: F<2, 2, "dateadd", "always", "scalar", "both", DateArgs>; + date_sub: F<2, 2, "dateadd", "always", "scalar", "both", DateArgs>; + addtime: F<2, 2, "addtime", "always", "scalar", "both", DateArgs>; + subtime: F<2, 2, "addtime", "always", "scalar", "both", DateArgs>; convert_tz: F<3, 3, "datetime", "always">; curdate: F<0, 0, "date", "never">; current_date: F<0, 0, "date", "never">; @@ -202,83 +238,83 @@ export interface Functions { utc_date: F<0, 0, "date", "never">; utc_time: F<0, 1, "time", "never">; utc_timestamp: F<0, 1, "datetime", "never">; - date: F<1, 1, "date", "temporal">; - datediff: F<2, 2, "intf", "temporal">; + date: F<1, 1, "date", "temporal", "scalar", "both", DateArgs>; + datediff: F<2, 2, "intf", "temporal", "scalar", "both", DateArgs>; date_format: F<2, 3, "varchar", "temporal">; - day: F<1, 1, "int", "temporal">; - dayofmonth: F<1, 1, "int", "temporal">; - dayname: F<1, 1, "varchar", "temporal">; - dayofweek: F<1, 1, "int", "temporal">; - dayofyear: F<1, 1, "int", "temporal">; + day: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + dayofmonth: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + dayname: F<1, 1, "varchar", "temporal", "scalar", "both", DateArgs>; + dayofweek: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + dayofyear: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; extract: F<2, 2, "int", "temporal">; - from_days: F<1, 1, "date", "args">; + from_days: F<1, 1, "date", "args", "scalar", "both", Num>; from_unixtime: F<1, 2, "fromunix", "always">; get_format: F<2, 2, "varchar", "always">; - hour: F<1, 1, "int", "temporal">; - last_day: F<1, 1, "date", "always">; + hour: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + last_day: F<1, 1, "date", "always", "scalar", "both", DateArgs>; makedate: F<2, 2, "date", "always">; maketime: F<3, 3, "time", "always">; - microsecond: F<1, 1, "int", "temporal">; - minute: F<1, 1, "int", "temporal">; - month: F<1, 1, "int", "temporal">; - monthname: F<1, 1, "varchar", "temporal">; + microsecond: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + minute: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + month: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + monthname: F<1, 1, "varchar", "temporal", "scalar", "both", DateArgs>; period_add: F<2, 2, "int", "args">; period_diff: F<2, 2, "int", "args">; - quarter: F<1, 1, "int", "temporal">; - second: F<1, 1, "int", "temporal">; - sec_to_time: F<1, 1, "time", "args">; + quarter: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + second: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + sec_to_time: F<1, 1, "time", "args", "scalar", "both", Num>; str_to_date: F<2, 2, "strtodate", "always">; - time: F<1, 1, "time", "temporal">; - timediff: F<2, 2, "time", "always">; + time: F<1, 1, "time", "temporal", "scalar", "both", DateArgs>; + timediff: F<2, 2, "time", "always", "scalar", "both", DateArgs>; time_format: F<2, 2, "varchar", "temporal">; - time_to_sec: F<1, 1, "bigint", "temporal">; - timestamp: F<1, 2, "datetime", "temporal">; + time_to_sec: F<1, 1, "bigint", "temporal", "scalar", "both", DateArgs>; + timestamp: F<1, 2, "datetime", "temporal", "scalar", "both", DateArgs>; timestampadd: F<3, 3, "datetime", "always">; timestampdiff: F<3, 3, "bigint", "temporal">; - to_days: F<1, 1, "intf", "temporal">; - to_seconds: F<1, 1, "bigint", "temporal">; - unix_timestamp: F<0, 1, "unixts", "temporal">; - week: F<1, 2, "int", "temporal">; - weekday: F<1, 1, "int", "temporal">; - weekofyear: F<1, 1, "int", "temporal">; - year: F<1, 1, "int", "temporal">; - yearweek: F<1, 2, "int", "temporal">; + to_days: F<1, 1, "intf", "temporal", "scalar", "both", DateArgs>; + to_seconds: F<1, 1, "bigint", "temporal", "scalar", "both", DateArgs>; + unix_timestamp: F<0, 1, "unixts", "temporal", "scalar", "both", DateArgs>; + week: F<1, 2, "int", "temporal", "scalar", "both", DateArgs>; + weekday: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + weekofyear: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + year: F<1, 1, "int", "temporal", "scalar", "both", DateArgs>; + yearweek: F<1, 2, "int", "temporal", "scalar", "both", DateArgs>; // JSON json_array: F<0, Many, "json", "never">; - json_array_append: F<3, Many, "json", "args">; - json_array_insert: F<3, Many, "json", "args">; - json_contains: F<2, 3, "bool", "args">; - json_contains_path: F<3, Many, "bool", "args">; - json_depth: F<1, 1, "int", "args">; - json_extract: F<2, Many, "jsonext", "always">; - json_insert: F<3, Many, "json", "args">; - json_keys: F<1, 2, "json", "always">; - json_length: F<1, 2, "int", "always">; - json_merge: F<2, Many, "json", "args">; - json_merge_patch: F<2, Many, "json", "args">; - json_merge_preserve: F<2, Many, "json", "args">; + json_array_append: F<3, Many, "json", "args", "scalar", "both", JsonArgs>; + json_array_insert: F<3, Many, "json", "args", "scalar", "both", JsonArgs>; + json_contains: F<2, 3, "bool", "args", "scalar", "both", JsonArgs>; + json_contains_path: F<3, Many, "bool", "args", "scalar", "both", JsonArgs>; + json_depth: F<1, 1, "int", "args", "scalar", "both", JsonArgs>; + json_extract: F<2, Many, "json", "always", "scalar", "both", JsonArgs>; + json_insert: F<3, Many, "json", "args", "scalar", "both", JsonArgs>; + json_keys: F<1, 2, "json", "always", "scalar", "both", JsonArgs>; + json_length: F<1, 2, "int", "always", "scalar", "both", JsonArgs>; + json_merge: F<2, Many, "json", "args", "scalar", "both", JsonArgs>; + json_merge_patch: F<2, Many, "json", "args", "scalar", "both", JsonArgs>; + json_merge_preserve: F<2, Many, "json", "args", "scalar", "both", JsonArgs>; json_object: F<0, Many, "json", "never">; - json_overlaps: F<2, 2, "bool", "args">; - json_pretty: F<1, 1, "longtext", "args">; + json_overlaps: F<2, 2, "bool", "args", "scalar", "both", JsonArgs>; + json_pretty: F<1, 1, "longtext", "args", "scalar", "both", JsonArgs>; json_quote: F<1, 1, "varchar", "args">; - json_remove: F<2, Many, "json", "args">; - json_replace: F<3, Many, "json", "args">; + json_remove: F<2, Many, "json", "args", "scalar", "both", JsonArgs>; + json_replace: F<3, Many, "json", "args", "scalar", "both", JsonArgs>; json_schema_valid: F<2, 2, "bool", "args">; json_schema_validation_report: F<2, 2, "json", "args">; - json_search: F<3, Many, "json", "always">; - json_set: F<3, Many, "json", "args">; - json_storage_free: F<1, 1, "int", "args">; - json_storage_size: F<1, 1, "int", "args">; - json_type: F<1, 1, "varchar", "args">; - json_unquote: F<1, 1, "jsonunquote", "args">; - json_valid: F<1, 1, "bool", "args">; - json_value: F<2, 2, "jsonvalue", "always">; + json_search: F<3, Many, "json", "always", "scalar", "both", JsonArgs>; + json_set: F<3, Many, "json", "args", "scalar", "both", JsonArgs>; + json_storage_free: F<1, 1, "int", "args", "scalar", "both", JsonArgs>; + json_storage_size: F<1, 1, "int", "args", "scalar", "both", JsonArgs>; + json_type: F<1, 1, "varchar", "args", "scalar", "both", JsonArgs>; + json_unquote: F<1, 1, "jsonunquote", "args", "scalar", "both", JsonArgs>; + json_valid: F<1, 1, "bool", "args", "scalar", "both", JsonArgs>; + json_value: F<2, 2, "jsonvalue", "always", "scalar", "both", JsonArgs>; json_compact: F<1, 1, "json", "args", "scalar", "mariadb">; json_detailed: F<1, 2, "json", "args", "scalar", "mariadb">; json_loose: F<1, 1, "json", "args", "scalar", "mariadb">; - json_exists: F<2, 2, "bool", "args", "scalar", "mariadb">; - json_query: F<2, 2, "json", "always", "scalar", "mariadb">; + json_exists: F<2, 2, "bool", "args", "scalar", "mariadb", JsonArgs>; + json_query: F<2, 2, "json", "always", "scalar", "mariadb", JsonArgs>; json_normalize: F<1, 1, "json", "args", "scalar", "mariadb">; json_equals: F<2, 2, "bool", "args", "scalar", "mariadb">; json_array_intersect: F<2, 2, "json", "always", "scalar", "mariadb">; @@ -288,21 +324,21 @@ export interface Functions { // Aggregates count: F<1, Many, "bigint", "never", "agg">; - sum: F<1, 1, "sum", "agg", "agg">; - avg: F<1, 1, "avg", "agg", "agg">; - min: F<1, 1, "arg0", "agg", "agg">; - max: F<1, 1, "arg0", "agg", "agg">; - group_concat: F<1, Many, "text", "agg", "agg">; + sum: F<1, 1, "sum", "agg", "agg", "both", Num>; + avg: F<1, 1, "avg", "agg", "agg", "both", Num>; + min: F<1, 1, "arg0", "agg", "agg", "both", Same>; + max: F<1, 1, "arg0", "agg", "agg", "both", Same>; + group_concat: F<1, Many, "text", "agg", "agg", "both", Text>; bit_and: F<1, 1, "bigint unsigned", "never", "agg">; bit_or: F<1, 1, "bigint unsigned", "never", "agg">; bit_xor: F<1, 1, "bigint unsigned", "never", "agg">; - std: F<1, 1, "double", "agg", "agg">; - stddev: F<1, 1, "double", "agg", "agg">; - stddev_pop: F<1, 1, "double", "agg", "agg">; - stddev_samp: F<1, 1, "double", "agg", "agg">; - variance: F<1, 1, "double", "agg", "agg">; - var_pop: F<1, 1, "double", "agg", "agg">; - var_samp: F<1, 1, "double", "agg", "agg">; + std: F<1, 1, "double", "agg", "agg", "both", Num>; + stddev: F<1, 1, "double", "agg", "agg", "both", Num>; + stddev_pop: F<1, 1, "double", "agg", "agg", "both", Num>; + stddev_samp: F<1, 1, "double", "agg", "agg", "both", Num>; + variance: F<1, 1, "double", "agg", "agg", "both", Num>; + var_pop: F<1, 1, "double", "agg", "agg", "both", Num>; + var_samp: F<1, 1, "double", "agg", "agg", "both", Num>; json_arrayagg: F<1, 1, "json", "agg", "agg">; json_objectagg: F<2, 2, "json", "agg", "agg">; grouping: F<1, Many, "int", "never", "agg">; @@ -313,12 +349,12 @@ export interface Functions { dense_rank: F<0, 0, "bigint unsigned", "never", "win">; percent_rank: F<0, 0, "double", "never", "win">; cume_dist: F<0, 0, "double", "never", "win">; - ntile: F<1, 1, "bigint unsigned", "args", "win">; - lag: F<1, 3, "lag", "lag", "win">; - lead: F<1, 3, "lag", "lag", "win">; + ntile: F<1, 1, "bigint unsigned", "args", "win", "both", Num>; + lag: F<1, 3, "lag", "lag", "win", "both", Offset>; + lead: F<1, 3, "lag", "lag", "win", "both", Offset>; first_value: F<1, 1, "arg0", "always", "win">; last_value: F<1, 1, "arg0", "always", "win">; - nth_value: F<2, 2, "arg0", "always", "win">; + nth_value: F<2, 2, "arg0", "always", "win", "both", Offset>; // Information and miscellaneous benchmark: F<2, 2, "int", "never">; @@ -417,7 +453,7 @@ export type IsAggCall = E extends { readonly over: null; readonly args: readonly unknown[]; } - ? DeclaredFn extends infer D + ? DeclaredFn extends infer D ? D extends null ? N extends keyof Functions ? Functions[N]["kind"] extends "agg" @@ -456,58 +492,10 @@ export type FunctionRegistry = { readonly [name: string]: AnySig | readonly AnySig[]; }; -type MatchName = { - [K in keyof O & string]: Lowercase extends N ? O[K] : never; -}[keyof O & string]; - -export type DeclaredFn = ( - DB extends { readonly $functions: infer F } - ? MatchName - : never -) extends infer D - ? [D] extends [never] - ? MatchName extends infer C - ? [C] extends [never] - ? null - : C - : never - : D - : never; - -export type PickSig = D extends readonly unknown[] - ? PickLoop - : D; -type PickLoop< - D extends readonly unknown[], - Argc extends number, - I extends number, -> = D[I] extends infer S extends AnySig - ? Accepts extends true - ? S - : PickLoop, 0]["length"] & number> - : D[0]; - -type Required_ = A extends readonly [ - ...infer H extends readonly string[], - "...", -] - ? H["length"] - : A["length"]; -type Accepts< - A extends readonly string[], - N extends number, -> = A extends readonly [...unknown[], "..."] - ? GE> - : N extends A["length"] - ? true - : false; - export type SigFn = S extends AnySig ? { - readonly min: Required_; - readonly max: S["args"] extends readonly [...unknown[], "..."] - ? Many - : S["args"]["length"]; + readonly min: MinArgs; + readonly max: MaxArgs; readonly ret: S["ret"]; readonly null: S["nullable"] extends true ? "always" @@ -520,15 +508,3 @@ export type SigFn = S extends AnySig readonly only: "both"; } : never; - -export type SigArgType = S extends AnySig - ? S["args"][I] extends infer T extends string - ? T extends "..." - ? S["args"] extends readonly [...unknown[], infer P extends string, "..."] - ? P - : "unknown" - : T - : S["args"] extends readonly [...unknown[], infer P extends string, "..."] - ? P - : "unknown" - : "unknown"; diff --git a/packages/core/src/mysql/engine/typer/json.ts b/packages/core/src/mysql/engine/typer/json.ts index 14bc889b..dd8ae303 100644 --- a/packages/core/src/mysql/engine/typer/json.ts +++ b/packages/core/src/mysql/engine/typer/json.ts @@ -1,88 +1,31 @@ +import type { + JsonStep, + JsonSteps, + JsonWalk, +} from "../../../engine/family/json.js"; import type { Shaped, TsShape } from "../../../schema.js"; -export type JsonShapeOf = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? [V] - : null; - -export type JsonStep = { readonly key: string } | { readonly index: true }; -type Step = JsonStep; +/** MySQL paths with wildcards (`[*]`, `.*`, `**`) find several values. */ +type Wildcard = `[*]${string}` | `**${string}` | `.*${string}`; export type JsonPath = E extends { readonly t: "str"; readonly v: infer P extends string; } ? P extends `$${infer R}` - ? Steps + ? JsonSteps : null : null; -type Steps = S extends "" - ? Acc - : S extends `[*]${string}` | `**${string}` | `.*${string}` - ? null - : S extends `.${infer R}` - ? R extends `"${infer K}"${infer R2}` - ? Steps - : KeyRun extends [ - infer K extends string, - infer R2 extends string, - ] - ? Steps - : null - : S extends `[${string}]${infer R}` - ? Steps - : null; -type KeyRun< - S extends string, - Acc extends string, -> = S extends `${infer C}${infer R}` - ? C extends "." | "[" - ? [Acc, S] - : KeyRun - : [Acc, S]; - -type Walk< - V, - P extends readonly Step[], - Missing extends boolean, -> = P extends readonly [infer H, ...infer R extends readonly Step[]] - ? [Exclude] extends [never] - ? [null, true] - : H extends { readonly key: infer K extends string } - ? Exclude extends infer O - ? O extends readonly unknown[] - ? [unknown, true] - : O extends object - ? K extends keyof O - ? Walk< - O[K], - R, - Absent ? true : false> - > - : string extends keyof O - ? Walk - : [never, true] - : [unknown, true] - : never - : Exclude extends readonly (infer El)[] - ? Walk - : [unknown, true] - : [V, Absent]; -type Absent = Missing extends true - ? true - : null extends V - ? true - : undefined extends V - ? true - : Else; - /** The text `->>` gives for a JSON value: strings as they are, other values as JSON text. */ type TextOf = V extends string ? V : string; -export type JsonGet" | "->>"> = - Walk extends [infer X, infer N extends boolean] +export type JsonGet< + V, + P extends readonly JsonStep[], + Kind extends "->" | "->>", +> = + JsonWalk extends [infer X, infer N extends boolean] ? unknown extends X ? { readonly type: Kind extends "->" ? "json" : "longtext"; diff --git a/packages/core/src/mysql/engine/typer/params.ts b/packages/core/src/mysql/engine/typer/params.ts index 2568e25d..870a8d82 100644 --- a/packages/core/src/mysql/engine/typer/params.ts +++ b/packages/core/src/mysql/engine/typer/params.ts @@ -1,6 +1,12 @@ +import type { + DeclaredFn, + PickSig, + SigSlots, +} from "../../../engine/family/functions.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; import type { NullTest, ParamEntry } from "../../../engine/params.js"; -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, QueryContext, Scope } from "../../../engine/types.js"; +import type { ColInfo, Info, Scope } from "../../../engine/types.js"; import type { Call, CaseExpr, @@ -14,8 +20,8 @@ import type { import type { BindFrom } from "../binder/scope.js"; import type { BindWith } from "../project/select.js"; import type { Cat, MkCtx, MyCteMap, MyCtx, MyLevel } from "../types.js"; -import type { DeclaredFn, PickSig, SigArgType } from "./functions.js"; -import type { Common, Info, SubqueryCol, TypeOf } from "./typeof.js"; +import type { FunctionCatalog, Functions } from "./functions.js"; +import type { Common, SubqueryCol, TypeOf } from "./typeof.js"; type Ctx = MkCtx< DB, @@ -108,38 +114,44 @@ type ParamsComplex = E extends { ? E["e"] extends { readonly t: "row" } ? ParamsOfList<[E["e"], ...E["list"]], C> : ListParams<[E["e"], ...E["list"]], C, false> - : E extends { readonly t: "insub" | "quant" } + : E extends { readonly t: "insub" } ? InSubParams - : E extends { readonly t: "like" } - ? ListParams< - E["esc"] extends Expr - ? [E["e"], E["pat"], E["esc"]] - : [E["e"], E["pat"]], - C, - false, - Info<"varchar", false> - > - : E extends { readonly t: "member" } - ? - | Slot> - | Slot> - : E extends { readonly t: "match" } + : E extends { readonly t: "quant" } + ? InSubParams + : E extends { readonly t: "like" } + ? ListParams< + E["esc"] extends Expr + ? [E["e"], E["pat"], E["esc"]] + : [E["e"], E["pat"]], + C, + false, + Info<"varchar", false> + > + : E extends { readonly t: "member" } ? - | ParamsOfList - | Slot> - : E extends { readonly t: "interval" } - ? Slot, false>> - : E extends { readonly t: "assign" } - ? Slot> - : E extends { readonly t: "case" } - ? CaseParams, C> - : E extends { readonly t: "call" } - ? CallParams, C> - : E extends { readonly t: "sub" | "exists" } - ? ParamsOfSelect - : E extends { readonly t: "row" } - ? ParamsOfList - : never; + | Slot> + | Slot> + : E extends { readonly t: "match" } + ? + | ParamsOfList + | Slot> + : E extends { readonly t: "interval" } + ? Slot< + E["e"], + C, + Info, false> + > + : E extends { readonly t: "assign" } + ? Slot> + : E extends { readonly t: "case" } + ? CaseParams, C> + : E extends { readonly t: "call" } + ? CallParams, C> + : E extends { readonly t: "sub" | "exists" } + ? ParamsOfSelect + : E extends { readonly t: "row" } + ? ParamsOfList + : never; type IntervalParam = U extends | "microsecond" @@ -197,7 +209,7 @@ type RowPairs< I extends number, > = L[I] extends infer A extends Expr ? R[I] extends infer B extends Expr - ? RowPairs, Nx> + ? RowPairs, Next> : M : M; @@ -255,9 +267,9 @@ type FirstSib< ? Sib extends infer S ? S extends ColInfo ? S - : FirstSib> + : FirstSib> : never - : FirstSib> + : FirstSib> : null; export type ParamsOfList = FnArgs< L, @@ -316,7 +328,7 @@ type CaseValues< Ws, Else, C, - Nx, + Next, Sib extends infer S extends ColInfo ? [...Acc, S] : Acc > : Else extends Expr @@ -338,163 +350,6 @@ type CaseLoop = Branch extends { >) : never; -type TextFns = - | "ascii" - | "bit_length" - | "char_length" - | "character_length" - | "concat" - | "concat_ws" - | "elt" - | "field" - | "find_in_set" - | "from_base64" - | "hex" - | "instr" - | "lcase" - | "length" - | "locate" - | "lower" - | "ltrim" - | "md5" - | "octet_length" - | "ord" - | "position" - | "quote" - | "regexp_instr" - | "regexp_like" - | "regexp_replace" - | "regexp_substr" - | "replace" - | "reverse" - | "rtrim" - | "sha" - | "sha1" - | "soundex" - | "strcmp" - | "substring_index" - | "to_base64" - | "trim" - | "ucase" - | "unhex" - | "upper" - | "group_concat" - | "inet_aton" - | "inet6_aton" - | "uuid_to_bin" - | "is_uuid"; -type NumFns = - | "abs" - | "acos" - | "asin" - | "atan" - | "atan2" - | "ceil" - | "ceiling" - | "cos" - | "cot" - | "degrees" - | "exp" - | "floor" - | "ln" - | "log" - | "log10" - | "log2" - | "mod" - | "pow" - | "power" - | "radians" - | "round" - | "sign" - | "sin" - | "sqrt" - | "tan" - | "truncate" - | "sum" - | "avg" - | "std" - | "stddev" - | "stddev_pop" - | "stddev_samp" - | "variance" - | "var_pop" - | "var_samp" - | "space" - | "from_days" - | "sec_to_time" - | "inet_ntoa" - | "ntile"; -type DateFns = - | "date" - | "datediff" - | "day" - | "dayofmonth" - | "dayname" - | "dayofweek" - | "dayofyear" - | "hour" - | "last_day" - | "microsecond" - | "minute" - | "month" - | "monthname" - | "quarter" - | "second" - | "time" - | "timediff" - | "time_to_sec" - | "timestamp" - | "to_days" - | "to_seconds" - | "unix_timestamp" - | "week" - | "weekday" - | "weekofyear" - | "year" - | "yearweek" - | "date_add" - | "date_sub" - | "adddate" - | "subdate" - | "addtime" - | "subtime"; -type SameFns = - | "coalesce" - | "ifnull" - | "nvl" - | "nullif" - | "greatest" - | "least" - | "max" - | "min"; -type JsonFns = - | "json_array_append" - | "json_array_insert" - | "json_contains" - | "json_contains_path" - | "json_depth" - | "json_extract" - | "json_insert" - | "json_keys" - | "json_length" - | "json_merge" - | "json_merge_patch" - | "json_merge_preserve" - | "json_overlaps" - | "json_pretty" - | "json_remove" - | "json_replace" - | "json_search" - | "json_set" - | "json_storage_free" - | "json_storage_size" - | "json_type" - | "json_unquote" - | "json_valid" - | "json_value" - | "json_query" - | "json_exists"; - type CallParams = | FnArgs> | ParamsOfList @@ -505,75 +360,22 @@ type CallParams = ? ParamsOfList<[...P, ...O], C> : never); type CallSlots = - DeclaredFn extends infer D extends {} + DeclaredFn extends infer D extends {} ? SigSlots, E["args"]> - : E["name"] extends SameFns - ? readonly Info< - FirstSib extends infer S extends ColInfo - ? S["type"] - : "unknown", - true - >[] - : E["name"] extends "if" - ? IfSlots - : E["name"] extends - | "substr" - | "substring" - | "mid" - | "left" - | "right" - | "repeat" - | "lpad" - | "rpad" - | "insert" - ? E["name"] extends "lpad" | "rpad" - ? [ - Info<"varchar", true>, - Info<"bigint", true>, - Info<"varchar", true>, - ] - : E["name"] extends "insert" - ? [ - Info<"varchar", true>, - Info<"bigint", true>, - Info<"bigint", true>, - Info<"varchar", true>, - ] - : [ - Info<"varchar", true>, - Info<"bigint", true>, - Info<"bigint", true>, - ] - : E["name"] extends "lag" | "lead" | "nth_value" - ? [ - Info<"unknown", true>, - Info<"bigint", false>, - Info<"unknown", true>, - ] - : E["name"] extends - | "json_object" - | "json_array" - | "json_objectagg" - | "json_arrayagg" - | "json_quote" - ? readonly Info<"unknown", true>[] - : E["name"] extends JsonFns - ? [Info<"json", true>, ...Info<"unknown", true>[]] - : E["name"] extends TextFns - ? readonly Info<"varchar", true>[] - : E["name"] extends NumFns - ? readonly Info<"double", true>[] - : E["name"] extends DateFns - ? [Info<"datetime", true>, Info<"unknown", true>] - : readonly Info<"unknown", true>[]; - -type SigSlots< - S, - L extends readonly unknown[], - Acc extends readonly unknown[] = [], -> = Acc["length"] extends L["length"] - ? Acc - : SigSlots, true>]>; + : E["name"] extends keyof Functions + ? Functions[E["name"]] extends { readonly args: infer A } + ? A extends readonly unknown[] + ? A + : A extends "same" + ? readonly Info< + FirstSib extends infer S extends ColInfo + ? S["type"] + : "unknown", + true + >[] + : IfSlots + : readonly Info<"unknown", true>[] + : readonly Info<"unknown", true>[]; export type FnArgs< L extends readonly Expr[], @@ -587,7 +389,7 @@ export type FnArgs< C, M | Slot>, Ts, - Nx + Next > : M; @@ -612,7 +414,7 @@ type IfValueType< ? IfValueType< L, C, - Nx, + Next, Sib extends infer S extends ColInfo ? [...Acc, S] : Acc > : Common extends infer T diff --git a/packages/core/src/mysql/engine/typer/typeof.ts b/packages/core/src/mysql/engine/typer/typeof.ts index 1e93c28b..4997901d 100644 --- a/packages/core/src/mysql/engine/typer/typeof.ts +++ b/packages/core/src/mysql/engine/typer/typeof.ts @@ -1,5 +1,24 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, Scope } from "../../../engine/types.js"; +import type { + LastPart, + NameAt, + NamedAt, +} from "../../../engine/family/diagnostics.js"; +import type { + ArityOk, + DeclaredFn, + PickSig, +} from "../../../engine/family/functions.js"; +import type { JsonShapeOf, JsonStep } from "../../../engine/family/json.js"; +import type { + AllNull, + AnyNull, + NonZeroDigits, +} from "../../../engine/family/typer.js"; +import type { AggCtx } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { CappedLen, DigitsLE } from "../../../engine/num.js"; +import type { Or } from "../../../engine/object.js"; +import type { ColInfo, Info } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Shaped, TsShape } from "../../../schema.js"; import type { @@ -13,22 +32,10 @@ import type { } from "../ast.js"; import type { ResolveColumn } from "../binder/scope.js"; import type { ProjectSelect } from "../project/select.js"; -import type { - Cat, - Cell, - FlavorOf, - GE, - IsUnsigned, - MkCtx, - MyCtx, -} from "../types.js"; -import type { DeclaredFn, Fn, Functions, PickSig, SigFn } from "./functions.js"; -import type { JsonGet, JsonPath, JsonShapeOf, JsonStep } from "./json.js"; +import type { Cat, Cell, FlavorOf, IsUnsigned, MyCtx } from "../types.js"; +import type { Fn, FunctionCatalog, Functions, SigFn } from "./functions.js"; +import type { JsonGet, JsonPath } from "./json.js"; -export type Info = { - readonly type: T; - readonly nullable: N; -}; /** An integer that came from short literals, which MariaDB types `int`. */ type LitInfo = Info & { readonly lit: true }; @@ -88,14 +95,6 @@ type NumInfo = V extends `${string}${"e" | "E"}${string}` : Info<"decimal"> : never; type Unsign = V extends `-${infer R}` ? Unsign : V; -type Len< - S extends string, - A extends unknown[] = [], -> = S extends `${string}${infer R}` - ? A["length"] extends 21 - ? 21 - : Len - : A["length"]; type StripZeros = S extends `0${infer R extends `${string}${string}`}` ? R extends "" @@ -103,7 +102,7 @@ type StripZeros = : StripZeros : S; type IntLiteral = - Len> extends infer N extends number + CappedLen, 21> extends infer N extends number ? N extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ? "small" : N extends 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 @@ -125,29 +124,6 @@ type IntLiteral = : "decimal" : "decimal" : never; -type Dec = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; -type DecIndex = { - "0": 0; - "1": 1; - "2": 2; - "3": 3; - "4": 4; - "5": 5; - "6": 6; - "7": 7; - "8": 8; - "9": 9; -}; -type DigitsLE< - A extends string, - B extends string, -> = A extends `${infer X extends Dec}${infer AR}` - ? B extends `${infer Y extends Dec}${infer BR}` - ? X extends Y - ? DigitsLE - : GE - : false - : true; type TypeComplex = E extends { readonly t: "bin"; } @@ -197,50 +173,52 @@ type TypeComplex = E extends { > : E extends { readonly t: "in" } ? TypeIn - : E extends { readonly t: "insub" | "quant" } + : E extends { readonly t: "insub" } ? TypeInSub - : E extends { readonly t: "member" } - ? Pred<[E["e"], E["doc"]], C> - : E extends { readonly t: "match" } - ? TypeList< - [...E["cols"], E["against"]], - C - > extends infer X extends AnyErr - ? X - : Info<"double"> - : E extends { readonly t: "sub" } - ? SubqueryCol extends infer X extends - ColInfo - ? Info - : SubqueryCol - : E extends { readonly t: "exists" } - ? ProjectSelect< - E["q"], - C - > extends infer X extends AnyErr - ? X - : Info> - : E extends { readonly t: "assign" } - ? TypeOf extends infer X extends - ColInfo - ? Info - : TypeOf - : E extends { readonly t: "ctime" } - ? Info>> - : E extends { readonly t: "row" } - ? SqlError< - "Operand should contain 1 column(s)", - null - > - : E extends { readonly t: "interval" } + : E extends { readonly t: "quant" } + ? TypeInSub + : E extends { readonly t: "member" } + ? Pred<[E["e"], E["doc"]], C> + : E extends { readonly t: "match" } + ? TypeList< + [...E["cols"], E["against"]], + C + > extends infer X extends AnyErr + ? X + : Info<"double"> + : E extends { readonly t: "sub" } + ? SubqueryCol extends infer X extends + ColInfo + ? Info + : SubqueryCol + : E extends { readonly t: "exists" } + ? ProjectSelect< + E["q"], + C + > extends infer X extends AnyErr + ? X + : Info> + : E extends { readonly t: "assign" } + ? TypeOf extends infer X extends + ColInfo + ? Info + : TypeOf + : E extends { readonly t: "ctime" } + ? Info>> + : E extends { readonly t: "row" } ? SqlError< - "You have an error in your SQL syntax near 'INTERVAL'", + "Operand should contain 1 column(s)", null > - : SqlError< - "You have an error in your SQL syntax near 'DEFAULT'", - null - >; + : E extends { readonly t: "interval" } + ? SqlError< + "You have an error in your SQL syntax near 'INTERVAL'", + null + > + : SqlError< + "You have an error in your SQL syntax near 'DEFAULT'", + null + >; type TypeCast = TypeOf extends infer X extends ColInfo @@ -275,13 +253,6 @@ export type TypeList< : TypeOf : Acc; -type AnyNull = true extends L[number]["nullable"] - ? true - : false; -type AllNull = false extends L[number]["nullable"] - ? false - : true; - type Pred = TypeList extends infer X extends readonly ColInfo[] ? Info, AnyNull> @@ -464,15 +435,6 @@ type NonZero = E extends { ? false : NonZeroDigits : false; -type NonZeroDigits = V extends `${infer H}${infer R}` - ? H extends "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" - ? true - : H extends "e" | "E" - ? false - : NonZeroDigits - : false; - -type Or = A extends true ? true : B extends true ? true : false; type TypeBin = Op extends | "=" @@ -668,7 +630,7 @@ type CaseLoop< ? TypeOf extends AnyErr ? TypeOf : TypeOf extends infer Y extends ColInfo - ? CaseLoop> + ? CaseLoop> : TypeOf : Acc; @@ -759,14 +721,15 @@ type IntCommon = [Extract] extends [never] ? MaxRank< IntRank> > extends infer SR extends number - ? Lt extends true + ? RankLt extends true ? IntOfRank : UR extends 5 ? "decimal" - : IntOfRank> + : IntOfRank> : never : never; -type Lt = A extends B +/** Integer type ranks 1 (tinyint) to 5 (bigint). */ +type RankLt = A extends B ? false : A extends 1 ? true @@ -783,7 +746,7 @@ type Lt = A extends B ? true : false : false; -type Next1 = R extends 1 ? 2 : R extends 2 ? 3 : R extends 3 ? 4 : 5; +type NextRank = R extends 1 ? 2 : R extends 2 ? 3 : R extends 3 ? 4 : 5; type StringCommon = "longtext" extends U ? "longtext" : "mediumtext" extends U @@ -800,7 +763,7 @@ type BinaryCommon = "longblob" extends U | "longtext" : "varbinary"; type FnOf = - DeclaredFn extends infer D + DeclaredFn extends infer D ? D extends null ? E["name"] extends keyof Functions ? Functions[E["name"]] extends infer F extends Fn @@ -819,10 +782,10 @@ type TypeCall = [F["kind"], E["over"]] extends ["agg", null] ? [C["inAgg"], C["agg"]] extends [false, "group" | "whole"] ? CallShape - : "Invalid use of group function" + : SqlError<"Invalid use of group function", null> : CallShape - ) extends infer M extends string - ? SqlError + ) extends infer M extends AnyErr + ? M : ( [F["kind"], E["over"]] extends ["agg", null] ? AggCtx @@ -843,7 +806,14 @@ type TypeCall = : {}) : CallArgTypes : never - : SqlError<`FUNCTION ${E["disp"]} does not exist`, null> + : SqlError< + `FUNCTION ${E["disp"]} does not exist`, + NameAt< + LastPart>, + "function", + Lowercase + > + > : never; /** The second argument of DATE_ADD and friends may be an `INTERVAL`. */ @@ -857,16 +827,10 @@ type CallArgTypes< ? TypeList<[A0, A1["e"]], C> : TypeList; -type AggCtx = ( - C extends { readonly aggScope: infer S extends Scope } - ? S - : C["scope"] -) extends infer S extends Scope - ? MkCtx - : never; - -type Incorrect = - `Incorrect parameter count in the call to native function '${E["disp"]}'`; +type Incorrect = SqlError< + `Incorrect parameter count in the call to native function '${E["disp"]}'`, + NamedAt +>; type CallShape< E extends Call, @@ -877,39 +841,45 @@ type CallShape< ? E["over"] extends null ? null : null - : `You have an error in your SQL syntax near '*'` + : SqlError<`You have an error in your SQL syntax near '*'`, null> : E["name"] extends "count" ? E["args"]["length"] extends 1 ? null : E["distinct"] extends true ? null - : `You have an error in your SQL syntax near ','` + : SqlError<`You have an error in your SQL syntax near ','`, null> : ArityOk extends false ? Incorrect : F["kind"] extends "win" ? E["name"] extends "lag" | "lead" ? E["args"]["length"] extends 3 ? Fl extends "mariadb" - ? `You have an error in your SQL syntax near ','` + ? SqlError<`You have an error in your SQL syntax near ','`, null> : E["over"] extends null - ? `You have an error in your SQL syntax near ')'` + ? SqlError< + `You have an error in your SQL syntax near ')'`, + null + > : null : E["over"] extends null - ? `You have an error in your SQL syntax near ')'` + ? SqlError<`You have an error in your SQL syntax near ')'`, null> : null : E["over"] extends null - ? `You have an error in your SQL syntax near ')'` + ? SqlError<`You have an error in your SQL syntax near ')'`, null> : null : F["kind"] extends "agg" ? null : E["over"] extends null ? E["distinct"] extends true - ? `You have an error in your SQL syntax near 'DISTINCT'` + ? SqlError< + `You have an error in your SQL syntax near 'DISTINCT'`, + null + > : null - : `You have an error in your SQL syntax near 'OVER'`; - -type ArityOk = - GE extends true ? (Max extends 1000 ? true : GE) : false; + : SqlError< + `You have an error in your SQL syntax near 'OVER'`, + null + >; type CheckExtras = TypeList extends AnyErr @@ -1083,9 +1053,7 @@ type NumericRet< : "longtext" : R extends "jsonvalue" ? "varchar" - : R extends "jsonext" - ? "json" - : R; + : R; type TemporalOf = Extract< diff --git a/packages/core/src/mysql/engine/types.ts b/packages/core/src/mysql/engine/types.ts index 41d09e67..4d74c820 100644 --- a/packages/core/src/mysql/engine/types.ts +++ b/packages/core/src/mysql/engine/types.ts @@ -1,4 +1,15 @@ -import type { ColInfo, Scope } from "../../engine/types.js"; +export type { + AggMode, + Cell, + Col as MyCol, + ColMap as MyColMap, + ColOf as MyColOf, + CteMap as MyCteMap, + Ctx as MyCtx, + Item as MyItem, + Level as MyLevel, + MkCtx, +} from "../../engine/family/types.js"; /** The server family: MySQL (and compatible servers) or MariaDB. */ export type Flavor = "mysql" | "mariadb"; @@ -10,58 +21,6 @@ export type FlavorOf = DB extends { readonly $flavor: "mariadb" } ? "mariadb" : "mysql"; -export type MyCol = ColInfo & { readonly n: string }; -export type MyColOf = { - readonly type: T; - readonly nullable: N; - readonly n: Name; -}; -export type MyColMap = { readonly [lc: string]: MyCol }; - -export type MyItem = { - readonly alias: string | null; - readonly cols: MyColMap; - readonly skip: readonly string[]; - readonly order: readonly string[]; -}; -export type MyLevel = readonly MyItem[]; - -export type MyCteMap = { - readonly [lc: string]: { - readonly cols: MyColMap; - readonly order: readonly string[]; - }; -}; - -export type AggMode = "none" | "group" | "whole"; - -export type MyCtx = { - readonly db: unknown; - readonly scope: Scope; - readonly ctes: MyCteMap; - readonly agg: AggMode; - readonly inAgg: boolean; - readonly aliases: MyColMap; -}; - -export type MkCtx< - DB, - S extends Scope, - Ctes extends MyCteMap, - Agg extends AggMode = "none", - Aliases extends MyColMap = {}, - AggScope extends Scope = S, - InAgg extends boolean = false, -> = { - readonly db: DB; - readonly scope: S; - readonly aggScope: AggScope; - readonly ctes: Ctes; - readonly agg: Agg; - readonly inAgg: InAgg; - readonly aliases: Aliases; -}; - type IntName = "tinyint" | "smallint" | "mediumint" | "int" | "bigint"; type StringName = | "char" @@ -130,16 +89,3 @@ export type NormalizeType = T extends { readonly shape: unknown } type StripLength = T extends `${infer B}(${string})${infer R}` ? `${B}${R}` : T; - -export type Cell = { - readonly n: string; - readonly t: ColInfo; - readonly d?: string; -}; - -export type Tup< - N extends number, - A extends unknown[] = [], -> = A["length"] extends N ? A : Tup; -export type GE = - Tup extends [...Tup, ...unknown[]] ? true : false; diff --git a/packages/core/src/mysql/engine/write.ts b/packages/core/src/mysql/engine/write.ts index 226684d1..35f3aa03 100644 --- a/packages/core/src/mysql/engine/write.ts +++ b/packages/core/src/mysql/engine/write.ts @@ -1,11 +1,11 @@ -import type * as Shared from "../../engine/write.js"; +import type * as Shared from "../../sql/write.js"; import type { MySqlEngine } from "./engine.js"; export type { RelationName, TableName, TableOf, -} from "../../engine/write.js"; +} from "../../schema.js"; export type SetInput = Shared.SetInput< MySqlEngine, diff --git a/packages/core/src/mysql/index.ts b/packages/core/src/mysql/index.ts index 020f06c8..e9317629 100644 --- a/packages/core/src/mysql/index.ts +++ b/packages/core/src/mysql/index.ts @@ -1,12 +1,11 @@ export { type MariaDb, type MariaDbDialect, - type MariaDbTransactionalSql, type MySql, type MySqlDialect, mariadb, mysql, - type TransactionalSql, + type ValidateDdl, } from "./dialect.js"; export type { ApplyDdl, @@ -27,7 +26,6 @@ export type { TableName, TableOf, } from "./engine/write.js"; -export type { ValidateDdl } from "./migrations.js"; export type { Col, ColFlags, @@ -37,7 +35,12 @@ export type { Typed, View, } from "./schema/index.js"; -export type { MariaDbSql, Sql } from "./sql/types.js"; +export type { + MariaDbSql, + MariaDbTransactionalSql, + Sql, + TransactionalSql, +} from "./sql/types.js"; export type { BigNumberStringsTypeMap, Bindable, diff --git a/packages/core/src/mysql/migrations.ts b/packages/core/src/mysql/migrations.ts deleted file mode 100644 index 2d934f19..00000000 --- a/packages/core/src/mysql/migrations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AnyErr, SqlError } from "../errors.js"; -import type { ApplyDdl } from "./engine/infer.js"; - -export type ValidateDdl = string extends Q - ? SqlError< - "migrations need a string literal; use .raw() for SQL the type checker cannot see", - null - > - : ApplyDdl extends infer R - ? R extends AnyErr - ? R - : Q - : never; diff --git a/packages/core/src/mysql/schema/index.ts b/packages/core/src/mysql/schema/index.ts index 44382e96..96c5ee02 100644 --- a/packages/core/src/mysql/schema/index.ts +++ b/packages/core/src/mysql/schema/index.ts @@ -1,6 +1,6 @@ -import type { ColOptions, Shaped, TsShape, TypeName } from "../../schema.js"; +import type { FlaggedCol, Typed, TypeName } from "../../schema.js"; -export type { View } from "../../schema.js"; +export type { Typed, View } from "../../schema.js"; export type MySqlTypeName = | "tinyint" @@ -54,35 +54,12 @@ export type ColFlags = { readonly hidden?: boolean; }; -// Options are read through ColOptions (see there) so columns with different -// options compare by these properties. An unset autoIncrement, generated, -// hidden or position is an optional `never` property, so a column that has it -// is not assignable to one that does not. -export type Col = { - readonly type: T; - readonly nullable: ColOptions extends { nullable: true } ? true : false; - readonly hasDefault: ColOptions extends { hasDefault: true } - ? true - : ColOptions extends { generated: true } - ? true - : ColOptions extends { autoIncrement: true } - ? true - : false; -} & (ColOptions extends { readonly position: infer P extends number } - ? { readonly i: P } - : { readonly i?: never }) & - (ColOptions extends { readonly autoIncrement: true } - ? { readonly autoIncrement: true } - : { readonly autoIncrement?: never }) & - (ColOptions extends { readonly generated: true } - ? { readonly generated: true } - : { readonly generated?: never }) & - (ColOptions extends { readonly hidden: true } - ? { readonly hidden: true } - : { readonly hidden?: never }); - -/** A column of type `Base` whose values are typed `T`. */ -export type Typed = Shaped>; +export type Col = FlaggedCol< + T, + F, + "autoIncrement", + true +>; /** A JSON column holding documents of type `T`. */ export type Json = Col, F>; diff --git a/packages/core/src/mysql/sql/types.ts b/packages/core/src/mysql/sql/types.ts index 48f0acfd..722ac000 100644 --- a/packages/core/src/mysql/sql/types.ts +++ b/packages/core/src/mysql/sql/types.ts @@ -1,6 +1,7 @@ import type { DIALECT, DialectBrand, StrictSql } from "../../dialect.js"; import type { SqlPlugin } from "../../plugin.js"; import type { SqlApi } from "../../sql/api.js"; +import type { Transactional } from "../../transaction.js"; import type { MariaDbEngine, MySqlEngine } from "../engine/engine.js"; // Invariant parameters and the [DIALECT] brand: see the PostgreSQL Sql. @@ -12,6 +13,10 @@ export interface Sql< readonly [DIALECT]?: DialectBrand<"mysql", DB, TM, Plugins>; /** The database this `sql` runs on: `"mysql"`. */ readonly dialect: "mysql"; + /** This `sql` with `plugin` after its plugins. */ + withPlugin( + plugin: P, + ): Sql; } export interface MariaDbSql< @@ -22,4 +27,33 @@ export interface MariaDbSql< readonly [DIALECT]?: DialectBrand<"mariadb", DB, TM, Plugins>; /** The database this `sql` runs on: `"mariadb"`. */ readonly dialect: "mariadb"; + /** This `sql` with `plugin` after its plugins. */ + withPlugin( + plugin: P, + ): MariaDbSql; +} + +/** An `sql` with `transaction(fn)`, as adapters create it. */ +export interface TransactionalSql< + in out DB, + in out TM, + in out Plugins extends readonly SqlPlugin[] = [], +> extends Sql, + Transactional> { + readonly [DIALECT]?: DialectBrand<"mysql", DB, TM, Plugins>; + withPlugin( + plugin: P, + ): TransactionalSql; +} + +export interface MariaDbTransactionalSql< + in out DB, + in out TM, + in out Plugins extends readonly SqlPlugin[] = [], +> extends MariaDbSql, + Transactional> { + readonly [DIALECT]?: DialectBrand<"mariadb", DB, TM, Plugins>; + withPlugin( + plugin: P, + ): MariaDbTransactionalSql; } diff --git a/packages/core/src/mysql/type-map/index.ts b/packages/core/src/mysql/type-map/index.ts index cbdfb3db..53d92094 100644 --- a/packages/core/src/mysql/type-map/index.ts +++ b/packages/core/src/mysql/type-map/index.ts @@ -1,3 +1,4 @@ +import type { JsonAs, NullableTs, ParamMapOf, TsShaped } from "../../schema.js"; import type { Cat } from "../engine/types.js"; /** @@ -145,6 +146,7 @@ export interface DefaultParamTypeMap { blob: Uint8Array | string; mediumblob: Uint8Array | string; longblob: Uint8Array | string; + /** JSON text: drivers bind no objects, so a document is `sql.json(value)`. */ json: string; bool: boolean | number; /** `LIMIT` and `OFFSET` take only numbers: drivers send a string quoted. */ @@ -152,9 +154,7 @@ export interface DefaultParamTypeMap { unknown: Bindable; } -export type ParamTypeMapOf = TM extends { readonly $params: infer P } - ? P - : DefaultParamTypeMap; +export type ParamTypeMapOf = ParamMapOf; type Fallback = C extends "int" ? "int" @@ -176,43 +176,44 @@ type Fallback = C extends "int" ? "geometry" : "unknown"; -type TsTypeOf = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? V - : T extends keyof TM - ? TM[T] - : Fallback> extends infer K extends string - ? K extends keyof TM - ? TM[K] - : unknown - : never; +type TsTypeOf = + T extends TsShaped + ? T extends "json" + ? JsonAs, V> + : V + : T extends keyof TM + ? TM[T] + : Fallback> extends infer K extends string + ? K extends keyof TM + ? TM[K] + : unknown + : never; export type TsOf< TM, _DB, T extends { type: string; nullable: boolean }, -> = T["type"] extends "null" - ? null - : T["nullable"] extends true - ? TsTypeOf | null - : TsTypeOf; +> = NullableTs>; export type TsParamTypeOf = ParamTs< TM, ParamTypeMapOf, T >; -type ParamTs = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? V - : T extends "bool" | "limit" | "unknown" - ? T extends keyof PM - ? PM[T] - : unknown - : T extends keyof TM - ? TM[T] | (T extends keyof PM ? PM[T] : never) - : Fallback> extends infer K extends string - ? ParamTs - : never; +// A parameter takes its read type and the parameter map's extras, except +// where the read type is no value a driver binds: JSON, read parsed, binds as +// text. +type ParamTs = + T extends TsShaped + ? T extends "json" + ? JsonAs, V> + : V + : T extends "bool" | "limit" | "unknown" | "json" + ? T extends keyof PM + ? PM[T] + : unknown + : T extends keyof TM + ? TM[T] | (T extends keyof PM ? PM[T] : never) + : Fallback> extends infer K extends string + ? ParamTs + : never; diff --git a/packages/core/src/postgres/dialect.ts b/packages/core/src/postgres/dialect.ts index b76818eb..5a82c24b 100644 --- a/packages/core/src/postgres/dialect.ts +++ b/packages/core/src/postgres/dialect.ts @@ -1,45 +1,23 @@ -import type { - DIALECT, - Dialect, - DialectBrand, - DialectTypes, -} from "../dialect.js"; -import type { SqlPlugin } from "../plugin.js"; +import type { DdlCheck, Dialect, DialectTypes, PluginsOf } from "../dialect.js"; import type { Executor } from "../query.js"; -import type { Transactional } from "../transaction.js"; import type { ApplyDdl } from "./engine/infer.js"; -import type { ValidateDdl } from "./migrations.js"; -import type { Sql } from "./sql/types.js"; +import type { Sql, TransactionalSql } from "./sql/types.js"; import type { DefaultTypeMap } from "./type-map/index.js"; export interface Postgres extends DialectTypes { readonly typeMap: DefaultTypeMap; - readonly sql: Sql< - this["db"], - this["tm"], - this["plugins"] extends readonly SqlPlugin[] ? this["plugins"] : [] - >; + readonly sql: Sql>; readonly transactionalSql: TransactionalSql< this["db"], this["tm"], - this["plugins"] extends readonly SqlPlugin[] ? this["plugins"] : [] + PluginsOf >; - readonly validateDdl: ValidateDdl; + readonly validateDdl: DdlCheck; readonly applyDdl: ApplyDdl; } -// Invariant parameters, as on Sql. -export interface TransactionalSql< - in out DB, - in out TM, - in out Plugins extends readonly SqlPlugin[] = [], -> extends Sql, - Transactional> { - readonly [DIALECT]?: DialectBrand<"postgres", DB, TM, Plugins>; - withPlugin( - plugin: P, - ): TransactionalSql; -} +/** DDL script `Q` checked against schema `DB`: the script, or its error. */ +export type ValidateDdl = DdlCheck; export interface PostgresDialect extends Dialect { readonly name: "postgres"; @@ -57,4 +35,11 @@ declare module "../create-sql.js" { export const postgres: PostgresDialect = { name: "postgres", + // E'' strings take backslash escapes, block comments nest, and $tag$ + // quotes a string up to the same tag. `$` belongs to a name after its + // first character, so `x$1` and `a$b$` are names. PgLexis states these + // rules for the types. + lexis: + /(?\$(?:[A-Za-z_]\w*)?\$)[\s\S]*?(?:\k|$)/ + .source, }; diff --git a/packages/core/src/runtime/pg-arrays.ts b/packages/core/src/postgres/driver.ts similarity index 60% rename from packages/core/src/runtime/pg-arrays.ts rename to packages/core/src/postgres/driver.ts index ae0b9194..54f684b1 100644 --- a/packages/core/src/runtime/pg-arrays.ts +++ b/packages/core/src/postgres/driver.ts @@ -1,3 +1,28 @@ +// The PostgreSQL drivers' side of `sql`: node-postgres results and arrays, +// and the `pg` connection of the migrator and `typesql introspect`. + +import { errorMessage } from "../command.js"; +import { + type Connection, + closing, + type Lost, + runtimeImport, +} from "../connect.js"; + +/** + * node-postgres resolves a script of several statements, which it runs + * when a call has no parameters, to the list of their results: the last + * one stands for the script. A script without statements has none. + */ +export function pgResult( + result: R | readonly R[], +): R | undefined { + return isList(result) ? result.at(-1) : result; +} + +const isList = (result: R | readonly R[]): result is readonly R[] => + Array.isArray(result); + export interface PgField { readonly name: string; readonly dataTypeID: number; @@ -8,17 +33,14 @@ export type PgLookup = ( values: unknown[], ) => Promise<{ readonly rows: readonly unknown[] }>; -/** A parsed array literal: elements, NULLs and nested arrays. */ type PgArray = (E | null | PgArray)[]; -/** An element of a user-defined array type: a boolean, a number, or its text. */ type PgElement = string | number | boolean; type ParseElement = (text: string) => PgElement; type ParseArray = (text: string) => string | PgArray; -/** A row of the LOOKUP query below. */ interface LookupRow { readonly arr: string; readonly elem: string; @@ -164,3 +186,84 @@ export function parsePgArray( }; return list(); } + +/** A `pg` Client, as the connection hands it out. */ +export interface PgClient { + query( + text: string, + values?: unknown[], + ): Promise<{ + readonly rows: unknown[]; + readonly rowCount: number | null; + readonly fields?: readonly PgField[]; + }>; + end(): Promise; +} + +type Pg = typeof import("pg"); + +const isPg = (module: unknown): module is Pg => + typeof module === "object" && + module !== null && + "default" in module && + typeof module.default === "object" && + module.default !== null && + "Client" in module.default && + typeof module.default.Client === "function"; + +/** One `pg` client for a PostgreSQL URL. */ +export async function connectPostgres( + url: string, +): Promise> { + if (!URL.canParse(url) && !url.startsWith("/")) + throw new Error( + `typesql: invalid connection URL ${JSON.stringify(url)} (expected postgres://user:password@host:port/database)`, + ); + let Client: Pg["Client"]; + try { + ({ Client } = (await runtimeImport("pg", isPg)).default); + } catch (cause) { + throw new Error( + 'typesql: url connects with the "pg" package, which is not installed: npm install pg', + { cause }, + ); + } + const client = new Client({ connectionString: url }); + // The server can end the session between queries (a timeout, a failover, + // pg_terminate_backend). Without a listener, pg's 'error' event would end + // the process; the next query rejects with the error instead. + const lost: Lost = { error: undefined }; + client.on("error", (error) => { + lost.error ??= error; + }); + try { + await client.connect(); + } catch (error) { + throw new Error( + `typesql: cannot connect to PostgreSQL: ${errorMessage(error)}`, + { cause: error }, + ); + } + return { + client, + execute: async (sql, values) => { + if (lost.error) throw lost.error; + const result = await client + .query(sql, [...values]) + .catch((cause: unknown) => { + // A session ended during a query rejects it with the server's + // FATAL error; later queries get pg's own "connection + // terminated" errors, so the server's message is the one to keep. + if ( + cause instanceof Error && + "severity" in cause && + (cause.severity === "FATAL" || cause.severity === "PANIC") + ) + lost.error ??= cause; + throw cause; + }); + return pgResult(result) ?? { rows: [], rowCount: 0 }; + }, + close: () => closing(client.end(), lost), + }; +} diff --git a/packages/core/src/postgres/engine/binder/clauses.ts b/packages/core/src/postgres/engine/binder/clauses.ts index a1161554..754c6574 100644 --- a/packages/core/src/postgres/engine/binder/clauses.ts +++ b/packages/core/src/postgres/engine/binder/clauses.ts @@ -1,37 +1,43 @@ -import type { ColInfo, Cols, Ctx } from "../../../engine/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { ColInfo, Cols, ScopeLevel } from "../../../engine/types.js"; import type { AnyErr, ErrMsg, SqlError } from "../../../errors.js"; import type { Expr, NamedWindow, Tag, Target, WindowSpec } from "../ast.js"; -import type { Nx } from "../parser/util.js"; -import type { AnyShaped, BaseOf } from "../shape.js"; +import type { IntLitValue, NumLitType } from "../literals.js"; +import type { NullableLevel, TypeDisplay } from "../shape.js"; import type { LookupFn } from "../typer/catalog.js"; -import type { IntLitValue, NumLitType } from "../typer/operators.js"; import type { TypeList, TypeOf } from "../typer/typeof.js"; +import type { Ctx } from "../types.js"; -type DisplayNames = { - int2: "smallint"; - int4: "integer"; - int8: "bigint"; - float4: "real"; - float8: "double precision"; - bool: "boolean"; - varchar: "character varying"; - bpchar: "character"; - char: '"char"'; - varbit: "bit varying"; - timestamptz: "timestamp with time zone"; - timestamp: "timestamp without time zone"; - timetz: "time with time zone"; - time: "time without time zone"; - lit: "text"; - null: "unknown"; -}; -export type TypeDisplay = T extends AnyShaped - ? BaseOf - : T extends `${infer E}[]` - ? `${TypeDisplay}[]` - : T extends keyof DisplayNames - ? DisplayNames[T] - : T; +/** + * Whether aggregates see a group of rows. A grouping set can be empty (the + * total row of ROLLUP), so an aggregate under GROUPING SETS may see no rows. + */ +export type Grouped = Sets extends false + ? GroupBy extends null + ? false + : true + : false; + +/** The FROM columns as GROUP BY's grouping sets leave them: nullable when a set leaves them out. */ +export type SetsLevel = Sets extends false + ? L + : NullableLevel< + L, + Sets extends readonly unknown[] ? LeftOut : string + >; +type LeftOut< + L extends ScopeLevel, + Is extends readonly unknown[], +> = Is extends readonly [infer H, ...infer R] + ? H extends { readonly t: "col"; readonly name: infer N } + ? N extends LevelNames + ? N | LeftOut + : string + : string + : never; +type LevelNames = { + [I in keyof L]: keyof L[I]["cols"]; +}[number]; export type CheckCondition< Clause extends string, @@ -62,7 +68,7 @@ type OutputExprs< > = Cs[I] extends undefined ? Acc : Cs[I] extends { readonly n: N } - ? OutputExprs> + ? OutputExprs> : Cs[I] extends { readonly star: infer M } ? N extends keyof M ? OutputExprs< @@ -70,10 +76,10 @@ type OutputExprs< Cs, N, [...Acc, { t: "col"; tbl: null; name: N }], - Nx + Next > - : OutputExprs> - : OutputExprs>; + : OutputExprs> + : OutputExprs>; type SameExpr = A extends { readonly t: "col"; @@ -231,7 +237,7 @@ export type ResolveSortKeys< : ResolveSortKey extends infer K ? K extends AnyErr ? K - : ResolveSortKeys> + : ResolveSortKeys> : never; export type CheckGroupBy< @@ -245,7 +251,7 @@ export type CheckGroupBy< ? true : ResolveGroupKey extends AnyErr ? ResolveGroupKey - : CheckGroupBy> + : CheckGroupBy> : true; export type CheckDistinctOnOrder< @@ -269,8 +275,8 @@ type DistinctWalk< : InList extends true ? Skipped extends true ? DistinctOnErr - : DistinctWalk> - : DistinctWalk>; + : DistinctWalk> + : DistinctWalk>; type AllIn< D extends readonly unknown[], Seen extends readonly unknown[], @@ -300,11 +306,11 @@ type DistinctOrderWalk< > = O[I] extends undefined ? true : O[I] extends null - ? DistinctOrderWalk> + ? DistinctOrderWalk> : InList extends true - ? DistinctOrderWalk> + ? DistinctOrderWalk> : [Star, O[I]] extends [true, Tag<"col">] - ? DistinctOrderWalk> + ? DistinctOrderWalk> : SqlError< "for SELECT DISTINCT, ORDER BY expressions must appear in select list", null @@ -318,7 +324,7 @@ export type CheckOutputOrder< ? true : OutputOrderItem extends AnyErr ? OutputOrderItem - : CheckOutputOrder>; + : CheckOutputOrder>; type OutputOrderItem = SignedNum extends [ infer Neg extends boolean, @@ -348,10 +354,10 @@ type OutputNameCount< > = Cs[I] extends undefined ? Acc : Cs[I] extends { readonly n: N } - ? OutputNameCount> + ? OutputNameCount> : Cs[I] extends { readonly star: infer M } - ? OutputNameCount> - : OutputNameCount>; + ? OutputNameCount> + : OutputNameCount>; export type WindowDefs = { readonly [name: string]: boolean }; @@ -369,7 +375,7 @@ export type CheckWindowClause< Ws, C, Defs & { readonly [K in Ws[I]["name"]]: R }, - Nx + Next > : CheckWindowSpec; @@ -419,18 +425,28 @@ type CheckWindowRefsAt< I extends number, > = Es[I] extends undefined ? true - : CheckSpecs, Defs, C> extends AnyErr - ? CheckSpecs, Defs, C> - : CheckWindowRefsAt>; + : CheckSpecs, Defs, C> extends infer R + ? [R] extends [true] + ? CheckWindowRefsAt> + : Exclude + : never; type CheckSpecs< - Ss, + S, Defs extends WindowDefs, C extends Ctx, -> = Ss extends readonly [infer S extends WindowSpec, ...infer R] - ? CheckWindowSpec extends boolean - ? CheckSpecs - : CheckWindowSpec +> = S extends WindowSpec + ? CheckWindowSpec extends infer R + ? R extends boolean + ? true + : R + : never : true; +type Wins = + Calls extends infer K + ? K extends { readonly win: infer S extends WindowSpec } + ? S + : never + : never; type ScalarLeaf = { readonly t: @@ -444,87 +460,6 @@ type ScalarLeaf = { | "typed" | "default"; }; -type Wins = E extends ScalarLeaf - ? Acc - : E extends { - readonly t: "call"; - readonly args: readonly unknown[]; - readonly win: unknown; - } - ? WinsOf extends infer A extends readonly unknown[] - ? E["win"] extends WindowSpec - ? [...A, E["win"]] - : A - : never - : E extends { - readonly t: "bin" | "isdistinct" | "quant"; - readonly l: unknown; - readonly r: unknown; - } - ? Wins> - : E extends { - readonly t: "array" | "row"; - readonly items: readonly unknown[]; - } - ? WinsOf - : E extends { - readonly t: "case"; - readonly operand: unknown; - readonly whens: readonly { - readonly when: unknown; - readonly then: unknown; - }[]; - readonly else: unknown; - } - ? Wins, 0>> - : E extends { - readonly t: "between"; - readonly e: unknown; - readonly lo: unknown; - readonly hi: unknown; - } - ? Wins>> - : E extends { - readonly t: "in"; - readonly e: unknown; - readonly list: readonly unknown[]; - } - ? WinsOf, 0> - : E extends { - readonly t: "like"; - readonly e: unknown; - readonly pat: unknown; - } - ? Wins> - : E extends { - readonly t: "idx"; - readonly e: unknown; - readonly i: unknown; - } - ? Wins> - : E extends { - readonly t: "attz"; - readonly e: unknown; - readonly zone: unknown; - } - ? Wins> - : E extends { readonly e: unknown } - ? Wins - : Acc; -type WinsOf< - L extends readonly unknown[], - Acc, - I extends number, -> = L[I] extends undefined ? Acc : WinsOf, Nx>; -type WhenWins< - L extends readonly { readonly when: unknown; readonly then: unknown }[], - Acc, - I extends number, -> = L[I] extends undefined - ? Acc - : WhenWins>, Nx>; - -/** A bare constant other than an integer: a string, `true`/`false` or `NULL`. */ type IsAggName = LookupFn extends infer O @@ -610,13 +545,11 @@ type Kind = U extends { readonly t: "call"; readonly over: true } ? never : "agg"; -/** `"agg"` and `"win"` for the aggregate and window calls in E, outside subqueries. */ type CallKinds = Kind, DB>; // Collect once without a database context, then classify the calls as needed. type Calls = [U] extends [never] ? Acc : Calls, Acc | Extract>; -/** Aggregate calls outside subqueries. */ type AggCalls = AggCall, DB>; /** diff --git a/packages/core/src/postgres/engine/binder/from.ts b/packages/core/src/postgres/engine/binder/from.ts index ed5171ba..728dd5f0 100644 --- a/packages/core/src/postgres/engine/binder/from.ts +++ b/packages/core/src/postgres/engine/binder/from.ts @@ -1,43 +1,42 @@ -import type { - NarrowKeys, - NarrowLevel, - UsingKeys, -} from "../../../engine/narrow.js"; +import type { NarrowKeys, NarrowLevel } from "../../../engine/narrow.js"; +import type { Next } from "../../../engine/next.js"; import type { ColInfo, ColMap, Cols, ColsToMap, - CteMap, - Ctx, Info, - MkCtx, - QueryContext, Scope, ScopeItem, ScopeLevel, } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; +import type { NumT } from "../../schema/type-facts.js"; import type { ColDef, Expr, FromItem, + FromJsonTable, FromTable, JoinKind, SelectStmt, TableRef, Tag, } from "../ast.js"; -import type { Nx } from "../parser/util.js"; import type { ColumnName } from "../project/naming.js"; import type { ProjectSelect } from "../project/select.js"; -import type { ArrInfo, HasNullElems, NullableMap } from "../shape.js"; -import type { SrfType } from "../typer/catalog.js"; +import type { + ArrInfo, + HasNullElems, + NullableLevel, + TypeDisplay, +} from "../shape.js"; +import type { FunctionCatalog, SrfType } from "../typer/catalog.js"; import type { CommonType } from "../typer/operators.js"; -import type { BindJsonTable } from "../typer/sqljson.js"; import type { TypeList, TypeOf } from "../typer/typeof.js"; import type { XmlTableCols } from "../typer/xml.js"; -import type { CheckCondition, TypeDisplay } from "./clauses.js"; +import type { CteMap, Ctx, MkCtx, QueryContext } from "../types.js"; +import type { CheckCondition } from "./clauses.js"; import type { ResolveTable } from "./resolve.js"; export type BindFrom< @@ -48,7 +47,7 @@ export type BindFrom< > = Items[I] extends undefined ? Acc : BindFromItem extends infer L extends ScopeLevel - ? BindFrom> + ? BindFrom> : BindFromItem; type BindFromItem< @@ -134,10 +133,49 @@ type BindXmlTable< : A : never; +type BindJsonTable< + J extends FromJsonTable["jt"], + Alias extends string | null, + CA extends readonly string[] | null, + Env extends QueryContext, + Sofar extends ScopeLevel, +> = + J["path"] extends Tag<"str"> + ? DuplicateName extends infer D extends string + ? SqlError<`duplicate JSON_TABLE column or path name: ${D}`, null> + : TypeList< + [J["ctx"], ...J["passing"], ...J["defs"]], + MkCtx, + [] + > extends infer Ts + ? Ts extends readonly ColInfo[] + ? CheckAlias< + Alias extends string ? Alias : "json_table", + Sofar + > extends infer A + ? A extends string + ? RenamedLevel + : A + : never + : Ts + : never + : SqlError< + "only string constants are supported in JSON_TABLE path specification", + null + >; +type DuplicateName = Ns extends readonly [ + infer N, + ...infer R extends readonly string[], +] + ? N extends Seen + ? N + : DuplicateName + : null; + type LevelOf = C extends Cols ? [{ readonly alias: A; readonly cols: ColsToMap; readonly skip: [] }] : C; -export type RenamedLevel< +type RenamedLevel< A, Base extends Cols, CA extends readonly string[] | null, @@ -150,7 +188,7 @@ type WithOrd = Ord extends true ? [...Cs, { readonly n: "ordinality"; readonly t: Info<"int8"> }] : Cs; -export type CheckAlias< +type CheckAlias< A extends string, Sofar extends ScopeLevel, > = A extends Sofar[number]["alias"] @@ -180,7 +218,8 @@ type BindTable< : A : never; -type SampleMethods = { +/** TABLESAMPLE methods and the type of their argument. */ +export type SampleMethods = { system: "float4"; bernoulli: "float4"; system_rows: "int8"; @@ -242,17 +281,10 @@ type SampleArgs< null > : TypeOf extends infer A extends ColInfo - ? A["type"] extends - | "int2" - | "int4" - | "int8" - | "numeric" - | "float4" - | "float8" - | "unknown" - ? SampleArgs> + ? A["type"] extends NumT | "unknown" + ? SampleArgs> : Args[I] extends Tag<"str"> - ? SampleArgs> + ? SampleArgs> : SqlError< `argument of ${I extends 0 ? "TABLESAMPLE" : "REPEATABLE"} must be type ${TypeDisplay}, not type ${TypeDisplay}`, null @@ -323,35 +355,6 @@ export type RenameCols< > : [...Acc, ...C]; -type RecordSrf = { - json_each: [ - { readonly n: "key"; readonly t: Info<"text"> }, - { readonly n: "value"; readonly t: Info<"json"> }, - ]; - jsonb_each: [ - { readonly n: "key"; readonly t: Info<"text"> }, - { readonly n: "value"; readonly t: Info<"jsonb"> }, - ]; - json_each_text: [ - { readonly n: "key"; readonly t: Info<"text"> }, - { readonly n: "value"; readonly t: Info<"text", true> }, - ]; - jsonb_each_text: [ - { readonly n: "key"; readonly t: Info<"text"> }, - { readonly n: "value"; readonly t: Info<"text", true> }, - ]; -}; -type RecordFunc = - | "json_to_record" - | "jsonb_to_record" - | "json_to_recordset" - | "jsonb_to_recordset"; -type ValueNamed = - | "json_array_elements" - | "jsonb_array_elements" - | "json_array_elements_text" - | "jsonb_array_elements_text"; - type BindFunc< Call extends Expr, Alias extends string | null, @@ -442,18 +445,30 @@ type FuncBaseCols< ? ColDefsCols : TypeList : TypeList extends infer As extends readonly ColInfo[] - ? Name extends keyof RecordSrf - ? RecordSrf[Name] - : Name extends RecordFunc - ? SqlError< - 'a column definition list is required for functions returning "record"', - null - > - : Name extends "unnest" - ? As extends readonly [ColInfo, ColInfo, ...ColInfo[]] - ? MultiUnnest - : SrfOrScalar - : SrfOrScalar + ? ( + Name extends keyof FunctionCatalog + ? FunctionCatalog[Name] + : null + ) extends infer F + ? F extends { readonly cols: infer Cs extends Cols } + ? Cs + : F extends { readonly coldefs: true } + ? SqlError< + 'a column definition list is required for functions returning "record"', + null + > + : Name extends "unnest" + ? As extends readonly [ColInfo, ColInfo, ...ColInfo[]] + ? MultiUnnest + : SrfOrScalar + : SrfOrScalar< + Name, + As, + Call, + C, + F extends { readonly column: infer N extends string } ? N : Name + > + : never : TypeList; type NonNullInfos = { @@ -465,18 +480,19 @@ type SrfOrScalar< As extends readonly ColInfo[], Call extends Expr, C extends MkCtx, + Column extends string, > = SrfType, C["db"]> extends infer S extends ColInfo - ? [ - { - readonly n: Name extends ValueNamed ? "value" : Name; - readonly t: S; - }, - ] + ? [{ readonly n: Column; readonly t: S }] : TypeOf extends infer T extends ColInfo ? [{ readonly n: Name; readonly t: T }] : TypeOf; +/** + * `unnest(a, b, ...)`, only allowed in FROM: PostgreSQL runs it as + * `ROWS FROM (unnest(a), unnest(b), ...)`, one nullable column per array. + * Typed directly; going through ROWS FROM costs a signature match per array. + */ type MultiUnnest< As extends readonly ColInfo[], Acc extends Cols, @@ -678,15 +694,10 @@ type ApplyNullability< ? NullableLevel : L : L; -type NullableLevel = { - readonly [I in keyof L]: L[I] extends ScopeItem - ? { - readonly alias: L[I]["alias"]; - readonly cols: NullableMap; - readonly skip: L[I]["skip"]; - } - : L[I]; -}; + +type UsingKeys = U extends string + ? readonly [null, U] + : never; type CommonNames = Extract< LevelKeys, diff --git a/packages/core/src/postgres/engine/binder/resolve.ts b/packages/core/src/postgres/engine/binder/resolve.ts index e0c48424..26d2d146 100644 --- a/packages/core/src/postgres/engine/binder/resolve.ts +++ b/packages/core/src/postgres/engine/binder/resolve.ts @@ -1,7 +1,7 @@ +import type { Next } from "../../../engine/next.js"; import type { ColInfo, ColMap, - CteMap, Scope, ScopeItem, ScopeLevel, @@ -9,8 +9,9 @@ import type { import type { SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; import type { TableRef } from "../ast.js"; -import type { Nx } from "../parser/util.js"; +import type { MetaKey } from "../db.js"; import type { RowInfo } from "../shape.js"; +import type { CteMap } from "../types.js"; export type ResolveTable< DB, @@ -26,7 +27,7 @@ export type ResolveTable< : LookupTable : Ref["schema"] extends "public" ? LookupTable - : Ref["schema"] extends keyof DB + : Ref["schema"] extends Exclude ? LookupTable< DB[Ref["schema"]], Ref["name"], @@ -46,8 +47,12 @@ type LookupTable< ? Cols : NS[Name] extends Table ? NS[Name] - : SqlError<`"${Display}" is not a table`, null> + : NotTable : SqlError<`relation "${Display}" does not exist`, null>; +/** A key that is no relation; the schema's own declarations don't exist to SQL. */ +type NotTable = Name extends MetaKey + ? SqlError<`relation "${Display}" does not exist`, null> + : SqlError<`"${Display}" is not a table`, null>; type InPgCatalog = DB extends { readonly pg_catalog: infer PC; @@ -66,13 +71,13 @@ export type ResolveWritableTable< ? SqlError<`cannot modify view "${Ref["name"]}"`, null> : DB[Ref["name"]] extends Table ? DB[Ref["name"]] - : SqlError<`"${Ref["name"]}" is not a table`, null> + : NotTable : Ref["schema"] extends null ? InPgCatalog extends true ? SqlError<`cannot modify view "pg_catalog.${Ref["name"]}"`, null> : SqlError<`relation "${Ref["name"]}" does not exist`, null> : SqlError<`relation "public.${Ref["name"]}" does not exist`, null> - : Ref["schema"] extends keyof DB + : Ref["schema"] extends Exclude ? Ref["name"] extends keyof DB[Ref["schema"]] ? DB[Ref["schema"]][Ref["name"]] extends View
? SqlError<`cannot modify view "${Ref["schema"]}.${Ref["name"]}"`, null> @@ -135,7 +140,7 @@ type ResolveQualifiedAt< ? L[Item]["cols"][Name] : SqlError<`column ${Tbl}.${Name} does not exist`, null> : FindAliasAt extends null - ? ResolveQualifiedAt> + ? ResolveQualifiedAt> : FindAliasAt : never; @@ -149,8 +154,8 @@ type FindAliasAt< : L[I]["alias"] extends Tbl ? Found extends number ? SqlError<`table reference "${Tbl}" is ambiguous`, null> - : FindAliasAt, I> - : FindAliasAt, Found>; + : FindAliasAt, I> + : FindAliasAt, Found>; type ResolveBare< S extends Scope, @@ -169,7 +174,7 @@ type ResolveBareAt< ? FindBare : FindBare extends "ambiguous" ? SqlError<`column reference "${Name}" is ambiguous`, null> - : ResolveBareAt>; + : ResolveBareAt>; export type WholeRow< S extends Scope, Name extends string, @@ -188,7 +193,7 @@ type WholeRowAt< ? FindAliasAt extends infer Item extends number ? RowInfo : FindAliasAt extends null - ? WholeRowAt> + ? WholeRowAt> : FindAliasAt : never; @@ -206,12 +211,12 @@ type FindBareAt< > = I extends L["length"] ? Found : Name extends L[I]["skip"][number] - ? FindBareAt> + ? FindBareAt> : Name extends keyof L[I]["cols"] ? Found extends ColInfo ? "ambiguous" - : FindBareAt> - : FindBareAt>; + : FindBareAt> + : FindBareAt>; export type ExpandStar< S extends Scope, diff --git a/packages/core/src/postgres/engine/db.ts b/packages/core/src/postgres/engine/db.ts new file mode 100644 index 00000000..33d58dc3 --- /dev/null +++ b/packages/core/src/postgres/engine/db.ts @@ -0,0 +1,35 @@ +// How the engine reads and writes the layout of a PostgreSQL schema object: +// relations at the top level (the public schema) or under a namespace key, +// views wrapped in `$view`, and the meta keys below. + +import type { MetaKey as SchemaMetaKey, Table, View } from "../../schema.js"; + +/** + * Keys that are neither relations nor namespaces: declared enums, declared + * functions, and the parameters one statement lets be NULL (see infer.ts). + * MariaDB's flag is no PostgreSQL key. + */ +export type MetaKey = Exclude; + +/** The enums declared in `$types`, by name. */ +export type SchemaTypes = DB extends { readonly $types: infer R } ? R : {}; +/** The functions declared in `$functions`, by name. */ +export type SchemaFunctions = DB extends { readonly $functions: infer F } + ? F + : {}; + +export type IsViewVal = X extends { readonly $view: unknown } ? true : false; + +export type ColsOf = Rel extends { readonly $view: infer C } ? C : Rel; + +export type Rewrap = IsViewVal extends true ? AsView : C; + +export type AsView = C extends Table ? View : { readonly $view: C }; + +export type IsNamespace = X extends { readonly $view: unknown } + ? false + : [keyof X] extends [never] + ? false + : X[keyof X] extends { readonly type: string } + ? false + : true; diff --git a/packages/core/src/postgres/engine/ddl/alter-table.ts b/packages/core/src/postgres/engine/ddl/alter-table.ts index ea5cf8a6..41300342 100644 --- a/packages/core/src/postgres/engine/ddl/alter-table.ts +++ b/packages/core/src/postgres/engine/ddl/alter-table.ts @@ -1,3 +1,4 @@ +import type { Next } from "../../../engine/next.js"; import type { DelKey, Rebuild, @@ -5,7 +6,8 @@ import type { SetKey, } from "../../../engine/object.js"; import type { AnyErr, SqlError } from "../../../errors.js"; -import type { ColumnDef, TableRef } from "../ast.js"; +import type { AlterAction, ColumnDef, TableRef } from "../ast.js"; +import type { ColsOf, IsViewVal, Rewrap } from "../db.js"; import type { DescOf, FirstMissing, @@ -15,15 +17,12 @@ import type { WithType, } from "./column.js"; import type { - ColsOf, DelRel, GetRel, - IsViewVal, NoRelation, NoSchema, PutRel, RenameRel, - Rewrap, SchemaClash, SchemaExists, } from "./relation.js"; @@ -127,22 +126,39 @@ type ViewActionError = As extends readonly [ : ViewActionError : null; +/** + * PostgreSQL's ALTER TABLE pass for each action that changes columns (drops, + * type changes, additions, constraints, defaults), so a statement's actions + * apply in that order whatever their order in the statement. Every such kind + * must be listed. + */ +type PassOf = Passes<{ + drop_column: 0; + drop_not_null: 0; + drop_default: 0; + alter_type: 1; + add_column: 2; + set_not_null: 3; + add_identity: 3; + add_pk: 3; + set_default: 4; +}>; +/** The kinds ApplyAlterTable handles alone, before any pass. */ +type SoleKind = "rename_column" | "rename_table" | "set_schema" | "noop"; +type Passes< + P extends { readonly [K in Exclude]: number }, +> = P; + type RunPasses< Tbl, As, TN extends string, - Ps = [ - "drop_column" | "drop_not_null" | "drop_default", - "alter_type", - "add_column", - "set_not_null" | "add_identity" | "add_pk", - "set_default", - ], -> = Ps extends readonly [infer K, ...infer R] + K extends number = 0, +> = K extends PassOf[keyof PassOf] ? RunPass extends infer T2 ? T2 extends AnyErr ? T2 - : RunPasses + : RunPasses> : never : Tbl; @@ -150,12 +166,14 @@ type RunPass = As extends readonly [ infer A, ...infer R, ] - ? A extends { readonly t: K } - ? ColAction extends infer T2 - ? T2 extends AnyErr - ? T2 - : RunPass - : never + ? A extends { readonly t: infer Kind extends keyof PassOf } + ? PassOf[Kind] extends K + ? ColAction extends infer T2 + ? T2 extends AnyErr + ? T2 + : RunPass + : never + : RunPass : RunPass : Tbl; diff --git a/packages/core/src/postgres/engine/ddl/apply.ts b/packages/core/src/postgres/engine/ddl/apply.ts index 5c3ad3ee..859ec2f0 100644 --- a/packages/core/src/postgres/engine/ddl/apply.ts +++ b/packages/core/src/postgres/engine/ddl/apply.ts @@ -1,20 +1,20 @@ -import type { QueryContext } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { AlterEnum, AlterTable, CreateEnum, CreateFunction, + DeclareCursor, DropSchema, DropTable, DropType, DropView, RenameSchema, - SelectStmt, TableRef, Tag, } from "../ast.js"; -import type { ProjectSelect } from "../project/select.js"; +import type { ProjectStmt } from "../project/stmt.js"; +import type { QueryContext } from "../types.js"; import type { ApplyAlterTable } from "./alter-table.js"; import type { ApplyCreateTableAs } from "./create-table.js"; import type { @@ -35,11 +35,8 @@ import type { export type ApplyDdlNode = S extends Tag<"noop" | "fetch" | "move" | "close_cursor"> ? DB - : S extends { - readonly t: "declare_cursor"; - readonly q: infer Q extends SelectStmt; - } - ? ProjectSelect> extends infer C + : S extends DeclareCursor + ? ProjectStmt> extends infer C ? C extends AnyErr ? C : DB diff --git a/packages/core/src/postgres/engine/ddl/column.ts b/packages/core/src/postgres/engine/ddl/column.ts index 11ed83c6..6d114031 100644 --- a/packages/core/src/postgres/engine/ddl/column.ts +++ b/packages/core/src/postgres/engine/ddl/column.ts @@ -29,28 +29,6 @@ export type WithDefault = MkCol, NullOf, H>; export type WithType = MkCol, DefOf>; -type PgDisplay = { - int2: "smallint"; - int4: "integer"; - int8: "bigint"; - float4: "real"; - float8: "double precision"; - bool: "boolean"; - varchar: "character varying"; - bpchar: "character"; - varbit: "bit varying"; - timestamp: "timestamp without time zone"; - timestamptz: "timestamp with time zone"; - time: "time without time zone"; - timetz: "time with time zone"; -}; - -export type DisplayType = T extends `${infer E}[]` - ? `${DisplayType}[]` - : T extends keyof PgDisplay - ? PgDisplay[T] - : T; - export type TypeConflict = { [K in keyof A & keyof B]: TypeOfCol extends TypeOfCol ? TypeOfCol extends TypeOfCol diff --git a/packages/core/src/postgres/engine/ddl/create-table.ts b/packages/core/src/postgres/engine/ddl/create-table.ts index 847c1478..9f365105 100644 --- a/packages/core/src/postgres/engine/ddl/create-table.ts +++ b/packages/core/src/postgres/engine/ddl/create-table.ts @@ -1,6 +1,7 @@ import type { PickOne, Rebuild } from "../../../engine/object.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { ColumnDef, LikeClause, TableRef } from "../ast.js"; +import type { ColsOf, IsViewVal } from "../db.js"; import type { DefOf, DescOf, @@ -16,9 +17,7 @@ import type { QueryCols, StoredType } from "./query.js"; import type { AlreadyExists, CanCreate, - ColsOf, GetRel, - IsViewVal, NoRelation, PutRel, } from "./relation.js"; diff --git a/packages/core/src/postgres/engine/ddl/query.ts b/packages/core/src/postgres/engine/ddl/query.ts index 755a4717..a3993053 100644 --- a/packages/core/src/postgres/engine/ddl/query.ts +++ b/packages/core/src/postgres/engine/ddl/query.ts @@ -1,13 +1,9 @@ import type { PickOne, Rebuild } from "../../../engine/object.js"; -import type { - ColInfo, - Cols, - ColsToMap, - QueryContext, -} from "../../../engine/types.js"; +import type { ColInfo, Cols, ColsToMap } from "../../../engine/types.js"; import type { SqlError } from "../../../errors.js"; import type { SelectStmt } from "../ast.js"; import type { ProjectSelect } from "../project/select.js"; +import type { QueryContext } from "../types.js"; import type { MkCol, NullOf, TypeOfCol } from "./column.js"; type QueryProjection = Q extends SelectStmt diff --git a/packages/core/src/postgres/engine/ddl/registry.ts b/packages/core/src/postgres/engine/ddl/registry.ts index 0bb0fe28..e19c2f4c 100644 --- a/packages/core/src/postgres/engine/ddl/registry.ts +++ b/packages/core/src/postgres/engine/ddl/registry.ts @@ -5,21 +5,19 @@ import type { SetKey, } from "../../../engine/object.js"; import type { SqlError } from "../../../errors.js"; +import type { Sig } from "../../schema/functions.js"; import type { TableRef } from "../ast.js"; -import type { Sig } from "../typer/catalog.js"; -import type { TypeOfCol, WithType } from "./column.js"; import type { - AlreadyExists, AsView, ColsOf, IsNamespace, IsViewVal, MetaKey, -} from "./relation.js"; - -type Types = DB extends { readonly $types: infer R } ? R : {}; - -type Fns = DB extends { readonly $functions: infer F } ? F : {}; + SchemaFunctions, + SchemaTypes, +} from "../db.js"; +import type { TypeOfCol, WithType } from "./column.js"; +import type { AlreadyExists } from "./relation.js"; export type ApplyCreateFunction< DB, @@ -30,7 +28,7 @@ export type ApplyCreateFunction< DB, "$functions", SetKey< - Fns, + SchemaFunctions, Name, Srf extends true ? Sig<["..."], Ret, { srf: true }> : Sig<["..."], Ret> > @@ -41,14 +39,14 @@ export type ApplyCreateEnum< Name extends string, Vs extends readonly string[], > = - Types extends infer R + SchemaTypes extends infer R ? Name extends keyof R ? SqlError<`type "${Name}" already exists`, null> : SetKey> : never; export type ApplyAlterEnum = - Types extends infer R + SchemaTypes extends infer R ? Name extends keyof R ? Act extends { readonly t: "add_value"; @@ -84,16 +82,16 @@ type RetypeDeep = Rebuild<{ ? RenameKey : K extends "$functions" ? DB[K] - : DB[K] extends { readonly $view: infer C } - ? AsView> + : IsViewVal extends true + ? AsView, F, To>> : IsNamespace extends true ? RetypeNs : RetypeCols; }>; type RetypeNs = Rebuild<{ - [K in keyof NS]: NS[K] extends { readonly $view: infer C } - ? AsView> + [K in keyof NS]: IsViewVal extends true + ? AsView, F, To>> : RetypeCols; }>; @@ -109,7 +107,7 @@ export type ApplyDropTypes = Refs extends readonly [ infer N extends TableRef, ...infer Rest, ] - ? Types extends infer R + ? SchemaTypes extends infer R ? N["name"] extends keyof R ? UsesType extends true ? Casc extends true diff --git a/packages/core/src/postgres/engine/ddl/relation.ts b/packages/core/src/postgres/engine/ddl/relation.ts index edcdcd7c..7aacd411 100644 --- a/packages/core/src/postgres/engine/ddl/relation.ts +++ b/packages/core/src/postgres/engine/ddl/relation.ts @@ -1,7 +1,7 @@ import type { DelKey, RenameKey, SetKey } from "../../../engine/object.js"; import type { SqlError } from "../../../errors.js"; -import type { Table, View } from "../../../schema.js"; import type { TableRef } from "../ast.js"; +import type { IsNamespace, IsViewVal, MetaKey } from "../db.js"; type IsTop = S extends null | "public" ? true : false; @@ -22,16 +22,6 @@ export type NoRelation = MissingRel< SqlError<`relation "${Written}" does not exist`, null> >; -export type IsViewVal = X extends { readonly $view: unknown } ? true : false; - -export type IsNamespace = X extends { readonly $view: unknown } - ? false - : [keyof X] extends [never] - ? false - : X[keyof X] extends { readonly type: string } - ? false - : true; - export type IsSchemaKey = K extends keyof DB ? K extends MetaKey ? false @@ -111,12 +101,6 @@ export type RenameRel = > : DB; -export type ColsOf = Rel extends { readonly $view: infer C } ? C : Rel; - -export type Rewrap = IsViewVal extends true ? AsView : C; - -export type AsView = C extends Table ? View : { readonly $view: C }; - export type ApplyDropRels< DB, Refs, @@ -145,5 +129,3 @@ export type ApplyDropRels< : SqlError<`"${R["name"]}" is not a ${Kind}`, null> : never : DB; - -export type MetaKey = "$types" | "$functions" | "$nullParams"; diff --git a/packages/core/src/postgres/engine/ddl/script.ts b/packages/core/src/postgres/engine/ddl/script.ts index d474975d..994126d8 100644 --- a/packages/core/src/postgres/engine/ddl/script.ts +++ b/packages/core/src/postgres/engine/ddl/script.ts @@ -1,11 +1,13 @@ -import type { OpTok, Tok, Toks } from "../../../engine/tokens.js"; -import type { QueryContext } from "../../../engine/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Tok, Toks } from "../../../engine/tokens.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { DdlStmt, Stmt } from "../ast.js"; import type { Lex } from "../lexer/lex.js"; +import type { OpTok } from "../lexer/tokens.js"; import type { ParseStatement } from "../parser/stmt.js"; -import type { AtEnd, Nx, Ok, SyntaxErrorAt } from "../parser/util.js"; +import type { AtEnd, Ok, SyntaxErrorAt } from "../parser/util.js"; import type { ProjectStmt } from "../project/stmt.js"; +import type { QueryContext } from "../types.js"; import type { ApplyDdlNode } from "./apply.js"; type DdlKind = DdlStmt["t"]; @@ -21,7 +23,7 @@ type ApplyLoop = AtEnd extends true ? DB : T[I] extends OpTok<";"> - ? ApplyLoop> + ? ApplyLoop> : ParseStatement extends infer R ? R extends Ok ? EndsAt extends false @@ -65,7 +67,7 @@ type LocateFrom< ? M extends `${string} specified more than once` ? F extends Start ? F - : FindTok, Nx, F> + : FindTok, Next, F> : F : Start : M extends `cannot drop ${string} ${infer Name} because ${string}` @@ -86,5 +88,5 @@ type FindTok< : T[I] extends OpTok<";"> ? Fallback : T[I] extends Tok - ? FindTok, Fallback> + ? FindTok, Fallback> : Fallback; diff --git a/packages/core/src/postgres/engine/ddl/view.ts b/packages/core/src/postgres/engine/ddl/view.ts index 73836aa4..ed23ad61 100644 --- a/packages/core/src/postgres/engine/ddl/view.ts +++ b/packages/core/src/postgres/engine/ddl/view.ts @@ -1,16 +1,11 @@ import type { PickOne } from "../../../engine/object.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { TableRef } from "../ast.js"; -import type { DisplayType, TypeConflict, TypeOfCol } from "./column.js"; +import type { AsView, ColsOf, IsViewVal } from "../db.js"; +import type { TypeDisplay } from "../shape.js"; +import type { TypeConflict, TypeOfCol } from "./column.js"; import type { QueryCols, ViewTable } from "./query.js"; -import type { - AsView, - CanCreate, - ColsOf, - GetRel, - IsViewVal, - PutRel, -} from "./relation.js"; +import type { CanCreate, GetRel, PutRel } from "./relation.js"; export type ApplyCreateView< DB, @@ -59,7 +54,7 @@ type CheckReplace = [Exclude] extends [never] ? null : K extends keyof Old & keyof New ? SqlError< - `cannot change data type of view column "${K & string}" from ${DisplayType>} to ${DisplayType>}`, + `cannot change data type of view column "${K & string}" from ${TypeDisplay>} to ${TypeDisplay>}`, null > : null diff --git a/packages/core/src/postgres/engine/diagnostics.ts b/packages/core/src/postgres/engine/diagnostics.ts index 0286816b..798fd360 100644 --- a/packages/core/src/postgres/engine/diagnostics.ts +++ b/packages/core/src/postgres/engine/diagnostics.ts @@ -1,9 +1,10 @@ -import type { IdTok, Toks, WordTok } from "../../engine/tokens.js"; +import type { Next } from "../../engine/next.js"; +import type { Toks } from "../../engine/tokens.js"; import type { SqlError } from "../../errors.js"; +import type { IdTok, WordTok } from "./lexer/tokens.js"; +import type { IntLitValue } from "./literals.js"; import type { IntoName } from "./parser/select.js"; -import type { Nx } from "./parser/util.js"; import type { WithHint } from "./suggest.js"; -import type { IntLitValue } from "./typer/operators.js"; export type Reposition = E extends SqlError @@ -79,21 +80,21 @@ type FindTok = T[I] extends { } ? T[I] extends P ? I - : T[Nx] extends P - ? Nx - : T[Nx>] extends P - ? Nx> - : T[Nx>>] extends P - ? Nx>> - : FindTok>>>> + : T[Next] extends P + ? Next + : T[Next>] extends P + ? Next> + : T[Next>>] extends P + ? Next>> + : FindTok>>>> : -1; type FindSelectInto = T[I] extends WordTok<"into"> ? Prev extends WordTok<"insert" | "merge"> - ? FindSelectInto, T[I]> + ? FindSelectInto, T[I]> : IntoName - : FindSelectInto, T[I]>; + : FindSelectInto, T[I]>; type FindNumFrom< T extends Toks, @@ -108,11 +109,11 @@ type FindNumFrom< ? Minus extends number ? Minus : I - : FindNumFrom, N, null> + : FindNumFrom, N, null> : T[I] extends { readonly k: "o"; readonly v: "-" | "(" } ? FindNumFrom< T, - Nx, + Next, N, Minus extends number ? Minus @@ -120,5 +121,5 @@ type FindNumFrom< ? I : null > - : FindNumFrom, N, null> + : FindNumFrom, N, null> : 0; diff --git a/packages/core/src/postgres/engine/engine.ts b/packages/core/src/postgres/engine/engine.ts index eab14aa8..1bcca0f6 100644 --- a/packages/core/src/postgres/engine/engine.ts +++ b/packages/core/src/postgres/engine/engine.ts @@ -5,7 +5,9 @@ import type { ParamFn, RowFn, TokensFn, -} from "../../engine/contract.js"; +} from "../../sql/contract.js"; +import type { Lexis } from "../../sql/lexis.js"; +import type { JsonT } from "../schema/type-facts.js"; import type { TsParamTypeOf } from "../type-map/index.js"; import type { Analysis, RowFrom } from "./infer.js"; import type { Lex } from "./lexer/lex.js"; @@ -17,7 +19,25 @@ export interface PgEngine extends EngineTypes { readonly row: PgRow; readonly param: PgParam; readonly display: PgDisplay; - readonly json: "json" | "jsonb"; + readonly json: JsonT; + readonly arrayNote: " (node-postgres sends a JS array as a Postgres array)"; + readonly quote: '"'; + readonly emptyRow: "DEFAULT VALUES"; + readonly valuesDefault: true; + readonly lexis: PgLexis; +} + +/** + * PostgreSQL's text rules, as `postgres.lexis`: E'' strings take backslash + * escapes, block comments nest, and $tag$ quotes a string up to the same tag. + */ +export interface PgLexis extends Lexis { + readonly extra: never; + readonly backslash: never; + readonly spacedDash: false; + readonly nest: true; + readonly dollar: true; + readonly names: '"'; } interface PgAnalysis extends AnalysisFn { diff --git a/packages/core/src/postgres/engine/index.ts b/packages/core/src/postgres/engine/index.ts index 23a92288..e78bb250 100644 --- a/packages/core/src/postgres/engine/index.ts +++ b/packages/core/src/postgres/engine/index.ts @@ -1,27 +1,13 @@ -export type { NonArrayOrSqlJson } from "../../engine/infer.js"; -export type { - IdTok, - NumTok, - OpTok, - ParamTok, - StrTok, - Tok, - Toks, - WordTok, -} from "../../engine/tokens.js"; export type { ColInfo, ColMap, Cols, - CteMap, - Ctx, Scope, ScopeItem, ScopeLevel, } from "../../engine/types.js"; export type * from "../../index.js"; -export type { SplicedText } from "../../sql/call.js"; -export type { ListRep } from "../../sql/fragment.js"; +export type { NonArrayOrSqlJson } from "../../sql/infer.js"; export type { CheckEnc, DefaultMarker, @@ -31,19 +17,11 @@ export type { JsonDefaultValue, JsonMarker, JsonValue, - RewriteInlineStr, ValueEnc, } from "../../sql/inline.js"; -export type { NamedParams, RewriteNamed } from "../../sql/named.js"; +export type { NamedParams } from "../../sql/named.js"; export type { Slottable } from "../../sql/slots.js"; -export type { - JoinedText, - MaxParamStr, - QuotedIdent, - Splice, - Take, - UnknownTuple, -} from "../../sql/splice.js"; +export type { QuotedIdent, Take, UnknownTuple } from "../../sql/splice.js"; export type { SetRep, ValuesRep } from "../../sql/write.js"; export type * from "../index.js"; export type { @@ -56,21 +34,40 @@ export type { PgScalarName, PgTypeName, Schema, + SigFlags, Table, TypeRegistry, } from "../schema/index.js"; -export type { EnumsOf, TsOf, TsTypeOf } from "../type-map/index.js"; +export type { TsOf, TsTypeOf } from "../type-map/index.js"; export type * as Ast from "./ast.js"; -export type { PgEngine } from "./engine.js"; +export type { SchemaTypes as EnumsOf } from "./db.js"; +export type { PgEngine, PgLexis } from "./engine.js"; export type { Analysis, Analyze, ArgsOf, BuildRow, + JoinedText, KindOf, + ListRep, + MaxParamStr, NamesOf, ParamsArgs, + RewriteInlineStr, + RewriteNamed, + Splice, + SplicedText, } from "./infer.js"; export type { Lex } from "./lexer/lex.js"; +export type { + IdTok, + NumTok, + OpTok, + ParamTok, + StrTok, + Tok, + Toks, + WordTok, +} from "./lexer/tokens.js"; export type { ParseProgram, ParseStatement } from "./parser/stmt.js"; -export type { SigFlags } from "./typer/catalog.js"; +export type { CteMap, Ctx } from "./types.js"; diff --git a/packages/core/src/postgres/engine/infer.ts b/packages/core/src/postgres/engine/infer.ts index 2ae4dfc6..f2c4a7e8 100644 --- a/packages/core/src/postgres/engine/infer.ts +++ b/packages/core/src/postgres/engine/infer.ts @@ -1,8 +1,13 @@ -import type * as Shared from "../../engine/infer.js"; +import type { Simplify } from "../../engine/object.js"; import type { MaxParam, NullTest, ParamsToMap } from "../../engine/params.js"; -import type { IdTok, OpTok, Toks, WordTok } from "../../engine/tokens.js"; -import type { Cols, QueryContext, Simplify } from "../../engine/types.js"; -import type { RewriteNamed } from "../../sql/named.js"; +import type { Toks } from "../../engine/tokens.js"; +import type { Cols } from "../../engine/types.js"; +import type * as Call from "../../sql/call.js"; +import type * as Frag from "../../sql/fragment.js"; +import type * as Shared from "../../sql/infer.js"; +import type * as Inline from "../../sql/inline.js"; +import type * as Named from "../../sql/named.js"; +import type * as Splices from "../../sql/splice.js"; import type { TsOf } from "../type-map/index.js"; import type { DmlStmt, SelectStmt, Stmt } from "./ast.js"; import type { ApplyDdlAll } from "./ddl/script.js"; @@ -14,9 +19,10 @@ import type { ParseProgram } from "./parser/stmt.js"; import type { ParamsOfDml } from "./project/dml.js"; import type { ProjectStmt } from "./project/stmt.js"; import type { ParamsOfSelect, SettleSortKeys } from "./typer/params.js"; +import type { QueryContext } from "./types.js"; export type Analyze = - RewriteNamed extends [infer Q1 extends string, infer Names] + Named.RewriteNamed extends [infer Q1 extends string, infer Names] ? FastSelect extends infer F ? [F] extends [never] ? AnalyzeGeneral @@ -24,14 +30,15 @@ export type Analyze = stmt: infer S extends Stmt; max: infer M extends number; } - ? ProjectStmt> extends infer C + ? ProjectStmt< + S, + QueryContext, [], {}> + > extends infer C ? C extends Cols | null ? { readonly kind: S["t"]; readonly cols: C; - readonly params: M extends 0 - ? {} - : ParamsToMap>; + readonly params: FastParams; readonly max: M; readonly names: Names; readonly stmt: S; @@ -40,7 +47,7 @@ export type Analyze = : never : AnalyzeGeneral : never - : RewriteNamed; + : Named.RewriteNamed; type AnalyzeGeneral = Lex extends infer T @@ -54,7 +61,7 @@ type AnalyzeGeneral = ) extends infer P ? ProjectStmt< S, - QueryContext, [], {}> + QueryContext, [], {}> > extends infer C ? C extends Cols | null ? { @@ -73,12 +80,39 @@ type AnalyzeGeneral = : T : never; -// Only casts read parameter nullability. Retain metadata for an explicit metadata-name lookup too. -type ProjectionDb = [ - Extract | WordTok<"cast"> | IdTok<"$nullParams">>, -] extends [never] - ? DB - : WithNullParams>; +// Only casts read which parameters accept NULL; a text that merely looks like one costs a little. +type ProjectionDb = + Lowercase extends + | `${string}$${string}::${string}` + | `${string}cast${string}$${string}` + ? WithNullParams> + : DB; + +type FastParams = M extends 0 + ? {} + : ParamsToMap>; + +// FastSelect's select list has no `$`, so a cast reaches the output only through +// a FROM item that isn't a table, or another branch of a set operation. +type FastProjectionDb = S extends { + readonly body: { + readonly t: "simple"; + readonly from: infer F extends readonly unknown[]; + }; +} + ? [NotTables] extends [never] + ? DB + : ProjectionDb, Q> + : ProjectionDb, Q>; +type NotTables = I extends { readonly t: "table" } + ? never + : I extends { + readonly t: "join"; + readonly l: infer L; + readonly r: infer R; + } + ? NotTables | NotTables + : I; /** Parameters that accept null, with the same rule the parameter types use. */ type NullParams

= { @@ -97,6 +131,10 @@ type WithNullParams = [N] extends [never] export type Analysis = Analyze; +/** + * `SkipTargets` leaves out the select list, for the fast path only: FastSelect + * takes no statement with a `$` in its select list, so no parameter is there. + */ type ParamsOfStmt< S extends Stmt, DB, @@ -161,3 +199,30 @@ export type Validate = Shared.Validate; export type KindOf = Shared.KindOf; export type ApplyDdl = ApplyDdlAll; + +type Lexis = PgEngine["lexis"]; + +export type RewriteNamed = Named.RewriteNamed; + +export type RewriteInlineStr< + T extends string, + Start extends number, +> = Inline.RewriteInlineStr; + +export type MaxParamStr = Splices.MaxParamStr; + +export type Splice = Splices.Splice; + +export type JoinedText< + Items extends readonly { readonly text: string }[], + Sep extends string, + Off extends number = 0, +> = Splices.JoinedText; + +export type SplicedText = Call.SplicedText; + +export type ListRep< + E, + Sep extends string, + Wrap extends boolean = false, +> = Frag.ListRep; diff --git a/packages/core/src/postgres/engine/json.ts b/packages/core/src/postgres/engine/json.ts index 539cadd1..7a4c0fb5 100644 --- a/packages/core/src/postgres/engine/json.ts +++ b/packages/core/src/postgres/engine/json.ts @@ -1,27 +1,20 @@ +import type { Next } from "../../engine/next.js"; import type { ColInfo, Info } from "../../engine/types.js"; import type { SqlError } from "../../errors.js"; import type { TsShape } from "../../schema.js"; -import type { EnumsOf } from "../type-map/index.js"; +import type { IntT, JsonT, StrT } from "../schema/type-facts.js"; import type { Expr, Tag } from "./ast.js"; -import type { Nx } from "./parser/util.js"; import type { - AnyShaped, ArrInfo, + ArrShape, BaseOf, HasNullElems, - NullElems, + LitToText, + MergeShapes, Shaped, + ShapeOfType, } from "./shape.js"; -import type { - DateTimeT, - IntT, - LitToText, - NumT, - RangeT, - StrT, -} from "./typer/operators.js"; -type ArrShape = { readonly k: "arr"; readonly e: E }; type ValShape = { readonly k: "val"; readonly c: C }; export type LitType = V extends @@ -44,7 +37,7 @@ export type JsonLitCast = LitType extends Shaped<"lit", infer S> ? Shaped : null; export type LitCommon = - BaseOf extends "json" | "jsonb" + BaseOf extends JsonT ? L extends Shaped<"lit", infer SL> ? X extends Shaped ? Shaped, MergeShapes> @@ -52,28 +45,10 @@ export type LitCommon = : BaseOf : BaseOf; -export type MergeShapes = [B] extends [TsShape<[]>] - ? ArrayLike extends true - ? A - : A | B - : [A] extends [TsShape<[]>] - ? ArrayLike extends true - ? B - : A | B - : A | B; -type ArrayLike = [S] extends [ArrShape] - ? true - : [S] extends [TsShape] - ? [] extends T - ? true - : false - : false; - type ValInfo = A["type"] extends "lit" ? Info<"text", A["nullable"]> : A; -/** A value that is not NULL, keeping whether its array elements can be. */ type NonNull = ArrInfo< LitToText, false, @@ -124,7 +99,7 @@ export type BuildObject< { readonly k: "obj"; readonly m: { readonly [K in keyof Acc]: Acc[K] } } > > - : Args[Nx] extends undefined + : Args[Next] extends undefined ? SqlError<"argument list must have even number of elements", null> : Args[I] extends Tag<"null"> ? SqlError<"null value not allowed for object key", null> @@ -133,8 +108,8 @@ export type BuildObject< B, Args, As, - Nx>, - Without & { readonly [P in K]: ValInfo]> } + Next>, + Without & { readonly [P in K]: ValInfo]> } > : Info; type Without = K extends keyof Acc ? Omit : Acc; @@ -145,16 +120,13 @@ type KeyOf = E extends { ? V : null; -type ShapeOfType = - T extends Shaped<"json" | "jsonb" | "record", infer S> ? S : null; - export type JsonArrow< Op extends string, L extends ColInfo, R extends Expr, RT extends ColInfo, > = - L["type"] extends Shaped<"json" | "jsonb", unknown> + L["type"] extends Shaped ? Op extends "#>" ? R extends { readonly t: "str"; readonly v: infer V extends string } ? PathSegs extends infer Segs extends readonly string[] @@ -235,7 +207,7 @@ type Step< : never : S extends { readonly k: "val"; readonly c: infer C extends ColInfo } ? StepVal - : S extends { readonly k: "ts"; readonly v: infer T } + : S extends TsShape ? TsShape> : never; type IndexOk = Kind extends "key" @@ -253,7 +225,7 @@ type StepVal< > = ShapeOfType extends infer S ? S extends null - ? T extends "json" | "jsonb" | "unknown" | "record" + ? T extends JsonT | "unknown" | "record" ? TsShape : T extends `${infer E}[]` ? IndexOk extends true @@ -309,89 +281,3 @@ type Trim = S extends ` ${infer R}` : S extends `${infer R} ` ? Trim : S; - -export type JsonTs = S extends { readonly k: "obj"; readonly m: infer M } - ? [keyof M] extends [never] - ? Record - : { - -readonly [K in keyof M]: M[K] extends ColInfo - ? JsonValueTs - : never; - } - : S extends { readonly k: "arr"; readonly e: infer E extends ColInfo } - ? JsonValueTs[] - : S extends { - readonly k: "tup"; - readonly es: infer Es extends readonly ColInfo[]; - } - ? { -readonly [I in keyof Es]: JsonValueTs } - : S extends { readonly k: "rec"; readonly v: infer V extends ColInfo } - ? { [key: string]: JsonValueTs } - : S extends { readonly k: "val"; readonly c: infer C extends ColInfo } - ? JsonValueTs - : S extends { readonly k: "ts"; readonly v: infer T } - ? T - : unknown; - -type JsonValueTs = C extends ColInfo - ? C["nullable"] extends true - ? JsonOfInfo | null - : JsonOfInfo - : never; -type JsonOfInfo = C extends NullElems - ? C["type"] extends `${infer E}[]` - ? (JsonOfType | null)[] - : JsonOfType - : JsonOfType; - -type JsonStringT = - | StrT - | DateTimeT - | RangeT - | `${string}multirange` - | "uuid" - | "interval" - | "bytea" - | "money" - | "oid" - | "inet" - | "cidr" - | "macaddr" - | "macaddr8" - | "bit" - | "varbit" - | "xml" - | "tsvector" - | "tsquery" - | "jsonpath" - | "regclass" - | "regtype" - | "regproc" - | "regconfig" - | "point" - | "line" - | "lseg" - | "box" - | "path" - | "polygon" - | "circle"; - -type JsonOfType = T extends AnyShaped - ? ShapeOfType extends infer S - ? S extends null - ? unknown - : JsonTs - : never - : T extends NumT - ? number - : T extends "bool" - ? boolean - : T extends "null" - ? null - : T extends `${infer E}[]` - ? JsonOfType[] - : T extends JsonStringT - ? string - : T extends keyof EnumsOf - ? EnumsOf[T] - : unknown; diff --git a/packages/core/src/postgres/engine/lexer/lex.ts b/packages/core/src/postgres/engine/lexer/lex.ts index 27863363..1b574ab1 100644 --- a/packages/core/src/postgres/engine/lexer/lex.ts +++ b/packages/core/src/postgres/engine/lexer/lex.ts @@ -1,11 +1,17 @@ import type { Digit, HexDigit, + IdRun, IdStart, NonIdAscii, + TakeDigits, Upper, Ws, } from "../../../engine/chars.js"; +import type { Next } from "../../../engine/next.js"; +import type { ToNum } from "../../../engine/num.js"; +import type { Tok } from "../../../engine/tokens.js"; +import type { SqlError } from "../../../errors.js"; import type { BitTok, IdTok, @@ -13,11 +19,8 @@ import type { OpTok, ParamTok, StrTok, - Tok, WordTok, -} from "../../../engine/tokens.js"; -import type { SqlError } from "../../../errors.js"; -import type { Next } from "../../../sql/next.js"; +} from "./tokens.js"; import type { CheckEscapes, CheckUEscapes, @@ -25,8 +28,6 @@ import type { UEscape, } from "./unicode.js"; -type Nx = Next; - export type Lex = LexLines; type LexLines< @@ -40,7 +41,7 @@ type LexLines< ? LexCommas : LexCommas, []> extends infer R extends readonly unknown[] - ? LexLines> + ? LexLines> : LexCommas : LexCommas, Acc>; @@ -54,7 +55,7 @@ type LexCommas< ? Part extends `${string}--${string}` ? LexSpaces : LexSpaces extends infer R extends readonly unknown[] - ? LexCommas], Nx> + ? LexCommas], Next> : LexSpaces : LexSpaces; @@ -74,7 +75,7 @@ type LexSpaceLoop< ? LexAll : S extends `${infer Part} ${infer Rest}` ? LexAll extends infer R extends readonly unknown[] - ? LexSpaceLoop> + ? LexSpaceLoop> : LexAll : LexAll; type LexSpacesSlow< @@ -90,7 +91,7 @@ type LexSpacesSlow< ? LexAll : LexAll extends infer R ? R extends readonly unknown[] - ? LexSpacesSlow> + ? LexSpacesSlow> : LexAll : never : LexAll; @@ -108,7 +109,7 @@ type CanSplitLine< type NextLineQuote = B extends ChunkSize ? true : S extends `${Ws}${infer R}` - ? NextLineQuote> + ? NextLineQuote> : S extends `'${string}` ? true : false; @@ -142,7 +143,7 @@ type LexChunk< ? ["done", Acc] : B extends ChunkSize ? ["more", Acc, S] - : LexStep>; + : LexStep>; type Err = ["err", M, Acc]; @@ -365,14 +366,6 @@ type JunkAt = R extends `${infer C}${infer R1}` ? null : `${C}${IdRun}` : null; -type IdRun< - R extends string, - Acc extends string = "", -> = R extends `${infer C}${infer R1}` - ? C extends NonIdAscii - ? Acc - : IdRun - : Acc; type LexWord< R extends string, @@ -545,15 +538,6 @@ type TakeWord< type FoldChar = C extends Upper ? Lowercase : C; -type TakeDigits< - S extends string, - Acc extends string, -> = S extends `${infer C}${infer R}` - ? C extends Digit - ? TakeDigits - : [Acc, S] - : [Acc, S]; - type TakeNumber = S extends `0${infer P extends "x" | "X" | "o" | "O" | "b" | "B"}${infer R}` ? Lowercase

extends "x" @@ -629,16 +613,12 @@ type TakeExponent< : never : [V, S]; -type ToNum = S extends `${infer N extends number}` - ? N - : never; - type TrimIndent = B extends 128 ? S : S extends `\t\t\t\t${infer R}` - ? TrimIndent> + ? TrimIndent> : S extends `\t${infer R}` - ? TrimIndent> + ? TrimIndent> : S extends ` ${infer R}` - ? TrimIndent> + ? TrimIndent> : S; diff --git a/packages/core/src/postgres/engine/lexer/tokens.ts b/packages/core/src/postgres/engine/lexer/tokens.ts new file mode 100644 index 00000000..8f2977c9 --- /dev/null +++ b/packages/core/src/postgres/engine/lexer/tokens.ts @@ -0,0 +1,31 @@ +export type WordTok = { + readonly k: "w"; + readonly v: V; +}; +export type IdTok = { + readonly k: "id"; + readonly v: V; +}; +export type NumTok = { + readonly k: "n"; + readonly v: V; +}; +export type StrTok = { + readonly k: "s"; + readonly v: V; +}; +export type BitTok = StrTok & { + readonly bits: true; +}; +export type ParamTok = { + readonly k: "$"; + readonly v: N; +}; +export type OpTok = { + readonly k: "o"; + readonly v: V; +}; + +/** Every PostgreSQL token. */ +export type Tok = WordTok | IdTok | NumTok | StrTok | ParamTok | OpTok; +export type Toks = readonly Tok[]; diff --git a/packages/core/src/postgres/engine/literals.ts b/packages/core/src/postgres/engine/literals.ts new file mode 100644 index 00000000..d384162f --- /dev/null +++ b/packages/core/src/postgres/engine/literals.ts @@ -0,0 +1,153 @@ +// Numeric literals as PostgreSQL's lexer types them, and their integer values +// (for ORDER BY / GROUP BY positions and error locations). + +import type { Digit } from "../../engine/chars.js"; + +export type NumLitType = V extends `-${infer D}` + ? UnsignedLitType, 1> + : UnsignedLitType, 0>; +type NoUnderscores = V extends `${infer A}_${infer B}` + ? NoUnderscores<`${A}${B}`> + : V; + +type UnsignedLitType = D extends + | `0x${infer H}` + | `0X${infer H}` + ? IntLitType, IntBounds["hex"][Sign]> + : D extends `0o${infer O}` | `0O${infer O}` + ? IntLitType + : D extends `0b${infer B}` | `0B${infer B}` + ? IntLitType + : D extends + | `${string}.${string}` + | `${string}e${string}` + | `${string}E${string}` + ? "numeric" + : IntLitType; + +type IntBounds = { + dec: [ + ["2147483647", "9223372036854775807"], + ["2147483648", "9223372036854775808"], + ]; + hex: [["7fffffff", "7fffffffffffffff"], ["80000000", "8000000000000000"]]; + oct: [ + ["17777777777", "777777777777777777777"], + ["20000000000", "1000000000000000000000"], + ]; + bin: [ + [ + "1111111111111111111111111111111", + "111111111111111111111111111111111111111111111111111111111111111", + ], + [ + "10000000000000000000000000000000", + "1000000000000000000000000000000000000000000000000000000000000000", + ], + ]; +}; + +type IntLitType = + ValueCmp, Max[0]> extends "gt" + ? ValueCmp, Max[1]> extends "gt" + ? "numeric" + : "int8" + : "int4"; +type NoZeros = D extends `0${infer R}` ? NoZeros : D; +type ValueCmp = + LengthCmp extends "eq" ? DigitsCmp : LengthCmp; +type LengthCmp< + A extends string, + B extends string, +> = A extends `${string}${infer AR}` + ? B extends `${string}${infer BR}` + ? LengthCmp + : "gt" + : B extends "" + ? "eq" + : "lt"; +type DigitsCmp = [A, B] extends [ + `${infer AC}${infer AR}`, + `${infer BC}${infer BR}`, +] + ? AC extends BC + ? DigitsCmp + : DigitRank[BC & keyof DigitRank] extends [ + ...DigitRank[AC & keyof DigitRank], + unknown, + ...unknown[], + ] + ? "lt" + : "gt" + : "eq"; +type DigitRank = { + "0": []; + "1": [0]; + "2": [0, 0]; + "3": [0, 0, 0]; + "4": [0, 0, 0, 0]; + "5": [0, 0, 0, 0, 0]; + "6": [0, 0, 0, 0, 0, 0]; + "7": [0, 0, 0, 0, 0, 0, 0]; + "8": [0, 0, 0, 0, 0, 0, 0, 0]; + "9": [0, 0, 0, 0, 0, 0, 0, 0, 0]; + a: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + b: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + c: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + d: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + e: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + f: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +}; + +export type IntLitValue = + Lowercase< + NoUnderscores + > extends `0${infer P extends keyof Radix}${infer Ds}` + ? FromRadix + : FromRadix>, Ten, Digit, "0">; +type Ten = DigitRank["a"]; +type Radix = { + x: [...DigitRank["f"], 0]; + o: DigitRank["8"]; + b: DigitRank["2"]; +}; +type FromRadix< + Ds extends string, + R extends unknown[], + Dg extends keyof DigitRank, + Rev extends string, +> = Ds extends `${infer C extends Dg}${infer Rest}` + ? FromRadix> + : Ds extends "" + ? Reverse + : null; +type MulAdd< + Rev extends string, + R extends unknown[], + Carry extends unknown[], + Out extends string, +> = Rev extends `${infer D extends Digit}${infer Rest}` + ? DivMod10<[...Times, ...Carry], []> extends [ + infer Q extends unknown[], + infer M extends number, + ] + ? MulAdd + : never + : Carry extends [] + ? Out + : MulAdd<"0", R, Carry, Out>; +type Times< + N extends unknown[], + R extends unknown[], + Acc extends unknown[], +> = N extends [unknown, ...infer NR] ? Times : Acc; +type DivMod10 = T extends [ + ...Ten, + ...infer Rest, +] + ? DivMod10 + : [Q, T["length"]]; +type Reverse< + S extends string, + Acc extends string, +> = S extends `${infer C}${infer R}` ? Reverse : Acc; diff --git a/packages/core/src/postgres/engine/ops.ts b/packages/core/src/postgres/engine/ops.ts new file mode 100644 index 00000000..8c476e3b --- /dev/null +++ b/packages/core/src/postgres/engine/ops.ts @@ -0,0 +1,13 @@ +// PostgreSQL's operator groups, shared by the parser (precedence), the typer +// (result types, ANY/ALL, row comparison) and parameter inference. + +/** Comparison operators, one precedence level in the grammar; `!=` is `<>`. */ +export type CompareOp = "=" | "<" | ">" | "<=" | ">=" | "<>" | "!="; +/** Regular expression and LIKE operators (`~~` is LIKE). */ +export type MatchOp = "~" | "~*" | "!~" | "!~*" | "~~" | "~~*" | "!~~" | "!~~*"; +export type ArithOp = "+" | "-" | "*" | "/" | "%"; +/** Operators whose result is never boolean, which ANY/ALL rejects. */ +export type NonBoolOp = ArithOp | "^" | "||"; +/** JSON operators returning a JSON value or its text. */ +export type JsonGetOp = "->" | "#>"; +export type JsonTextOp = "->>" | "#>>"; diff --git a/packages/core/src/postgres/engine/parser/ddl.ts b/packages/core/src/postgres/engine/parser/ddl.ts index e4eca832..9b56fa2c 100644 --- a/packages/core/src/postgres/engine/parser/ddl.ts +++ b/packages/core/src/postgres/engine/parser/ddl.ts @@ -1,11 +1,6 @@ -import type { - OpTok, - StrTok, - Tok, - Toks, - WordTok, -} from "../../../engine/tokens.js"; -import type { SerialTypeName } from "../../schema/normalize.js"; +import type { Next } from "../../../engine/next.js"; +import type { Tok, Toks } from "../../../engine/tokens.js"; +import type { SerialAliases } from "../../schema/type-facts.js"; import type { AlterAction, ColumnDef, @@ -14,6 +9,8 @@ import type { TableRef, Tag, } from "../ast.js"; +import type { OpTok, StrTok, WordTok } from "../lexer/tokens.js"; +import type { CompareOp } from "../ops.js"; import type { LastOf, ParseExpr, ParseExprList } from "./expr.js"; import type { ParseSelectStmt, RefOf } from "./select.js"; import type { ParseTypeName } from "./typename.js"; @@ -25,7 +22,6 @@ import type { Expected, Fail, Ident, - Nx, Nx2, Nx3, Ok, @@ -41,11 +37,11 @@ import type { export type ParseDdl = T[I] extends WordTok ? W extends "create" - ? ParseCreate, false> + ? ParseCreate, false> : W extends "alter" - ? ParseAlter> + ? ParseAlter> : W extends "drop" - ? ParseDrop> + ? ParseDrop> : Noop : SyntaxErrorAt; @@ -63,17 +59,17 @@ type SkipUntil< T[I] extends OpTok ? D extends readonly [unknown, ...infer D2] ? O extends "(" - ? SkipUntil, Stop, [...D, 0]> + ? SkipUntil, Stop, [...D, 0]> : O extends ")" - ? SkipUntil, Stop, D2> - : SkipUntil, Stop, D> + ? SkipUntil, Stop, D2> + : SkipUntil, Stop, D> : O extends Stop ? I : O extends "(" - ? SkipUntil, Stop, [0]> - : SkipUntil, Stop, D> + ? SkipUntil, Stop, [0]> + : SkipUntil, Stop, D> : T[I] extends Tok - ? SkipUntil, Stop, D> + ? SkipUntil, Stop, D> : I; type SkipToStmtEnd = SkipUntil; @@ -92,15 +88,15 @@ type SkipRoutine = T[I] extends OpTok<";"> ? I : T[I] extends WordTok<"begin"> - ? T[Nx] extends WordTok<"atomic"> + ? T[Next] extends WordTok<"atomic"> ? SkipAtomic, []> - : SkipRoutine> + : SkipRoutine> : T[I] extends OpTok<"("> - ? SkipToCloseParen> extends infer J extends number + ? SkipToCloseParen> extends infer J extends number ? SkipRoutine - : SkipToCloseParen> + : SkipToCloseParen> : T[I] extends Tok - ? SkipRoutine> + ? SkipRoutine> : I; type SkipAtomic< T extends Toks, @@ -108,13 +104,13 @@ type SkipAtomic< D extends readonly unknown[], > = T[I] extends WordTok<"case"> - ? SkipAtomic, [...D, 0]> + ? SkipAtomic, [...D, 0]> : T[I] extends WordTok<"end"> ? D extends readonly [unknown, ...infer D2] - ? SkipAtomic, D2> - : Nx + ? SkipAtomic, D2> + : Next : T[I] extends Tok - ? SkipAtomic, D> + ? SkipAtomic, D> : SyntaxErrorAt; type RoutineEnd = @@ -123,7 +119,7 @@ type RoutineEnd = : SkipRoutine; type SkipParenOpt = - T[I] extends OpTok<"("> ? SkipToCloseParen> : I; + T[I] extends OpTok<"("> ? SkipToCloseParen> : I; type IfExists = EatWords extends never ? [false, I] : [true, Nx2]; @@ -143,14 +139,14 @@ type ParseNameList< ] ? EatOp extends never ? Ok<[...Acc, RefOf], J> - : ParseNameList, [...Acc, RefOf]> + : ParseNameList, [...Acc, RefOf]> : Expected<"name", T, I>; type Cascade = T[I] extends WordTok<"cascade"> - ? [true, Nx] + ? [true, Next] : T[I] extends WordTok<"restrict"> - ? [false, Nx] + ? [false, Next] : [false, I]; type NoopObject = @@ -192,18 +188,18 @@ type NoopCreateObject = type ParseCreate = T[I] extends WordTok ? W extends "or" - ? EatWord, "replace"> extends never - ? Expected<"REPLACE", T, Nx> + ? EatWord, "replace"> extends never + ? Expected<"REPLACE", T, Next> : ParseCreate, true> : W extends "temp" | "temporary" | "unlogged" | "global" | "local" - ? ParseCreate, OrReplace> + ? ParseCreate, OrReplace> : W extends "table" - ? ParseCreateTable> + ? ParseCreateTable> : W extends "view" - ? ParseCreateView, OrReplace, false, false> + ? ParseCreateView, OrReplace, false, false> : W extends "materialized" - ? EatWord, "view"> extends never - ? Expected<"VIEW", T, Nx> + ? EatWord, "view"> extends never + ? Expected<"VIEW", T, Next> : OrReplace extends true ? SyntaxErrorAt : IfNotExists> extends [ @@ -213,19 +209,19 @@ type ParseCreate = ? ParseCreateView : never : W extends "recursive" - ? EatWord, "view"> extends never - ? Expected<"VIEW", T, Nx> + ? EatWord, "view"> extends never + ? Expected<"VIEW", T, Next> : ParseCreateView, OrReplace, false, false, true> : W extends "type" - ? ParseCreateType> + ? ParseCreateType> : W extends "schema" - ? ParseCreateSchema> + ? ParseCreateSchema> : W extends "function" - ? ParseCreateFunction> + ? ParseCreateFunction> : W extends "procedure" ? RoutineEnd< T, - Nx, + Next, { t: "noop"; what: `create ${W}` } > : W extends NoopCreateObject @@ -243,7 +239,7 @@ type ParseCreateFunction = infer J extends number, ] ? T[J] extends OpTok<"("> - ? SkipToCloseParen> extends infer J1 extends number + ? SkipToCloseParen> extends infer J1 extends number ? FnReturns extends [ infer Ret, infer Srf, @@ -260,16 +256,16 @@ type ParseCreateFunction = } > : FnReturns - : SkipToCloseParen> + : SkipToCloseParen> : Expected<'"("', T, J> : Expected<"function name", T, I>; type FnReturns = T[I] extends WordTok<"returns"> - ? T[Nx] extends WordTok<"table"> + ? T[Next] extends WordTok<"table"> ? FnTableReturn> - : T[Nx] extends WordTok<"setof"> + : T[Next] extends WordTok<"setof"> ? FnRetType, true> - : FnRetType, false> + : FnRetType, false> : ["unknown", false, I]; type FnRetType = QualName extends [ @@ -277,9 +273,9 @@ type FnRetType = infer J extends number, ] ? T[J] extends OpTok<"%"> - ? T[Nx] extends WordTok<"type"> + ? T[Next] extends WordTok<"type"> ? [`${LastOf}%type`, Srf, Nx2] - : SyntaxErrorAt> + : SyntaxErrorAt> : FnPlainRetType : FnPlainRetType; type FnPlainRetType = @@ -289,13 +285,13 @@ type FnPlainRetType = type FnTableReturn = T[I] extends OpTok<"("> - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? FnRetType extends [infer Ty, true, infer K extends number] ? T[K] extends OpTok<")"> - ? [Ty, true, Nx] + ? [Ty, true, Next] : ["record", true, I] : FnRetType - : Expected<"column name", T, Nx> + : Expected<"column name", T, Next> : Expected<'"("', T, I>; type CreateTableNode = { @@ -323,9 +319,9 @@ type ParseCreateTable = infer J extends number, ] ? T[J] extends OpTok<"("> - ? IsCtasColList> extends true + ? IsCtasColList> extends true ? ParseCtasCols> - : ParseTableBody, RefOf["name"]> extends Ok< + : ParseTableBody, RefOf["name"]> extends Ok< [infer Cols, infer Cons, infer Like], infer J2 > @@ -343,9 +339,9 @@ type ParseCreateTable = J3 > : ParseTableTail - : ParseTableBody, RefOf["name"]> + : ParseTableBody, RefOf["name"]> : T[J] extends WordTok<"of"> - ? ParseTypedTable, Ine, RefOf> + ? ParseTypedTable, Ine, RefOf> : EatWords extends never ? ParseCtasAfterCols, null> : ParsePartitionOf, Ine, RefOf> @@ -364,7 +360,7 @@ type ParseTypedTable< ] ? ( T[J] extends OpTok<"("> - ? ParseTypedElements, Ref["name"]> + ? ParseTypedElements, Ref["name"]> : J ) extends infer J2 ? J2 extends number @@ -389,9 +385,9 @@ type ParseTypedElements = ( ) extends infer R ? R extends Ok ? T[J] extends OpTok<","> - ? ParseTypedElements, TN> + ? ParseTypedElements, TN> : T[J] extends OpTok<")"> - ? Nx + ? Next : Expected<'"," or ")"', T, J> : R : never; @@ -426,11 +422,11 @@ type ParseCtasAfterCols< SkipStorage extends infer I1 extends number ? EatWord extends never ? Expected<'"(", AS, OF or PARTITION OF', T, I1> - : T[Nx] extends WordTok<"execute"> + : T[Next] extends WordTok<"execute"> ? ParseCtasExecute, Ine, Ref, Names> - : ParseSelectStmt> extends Ok + : ParseSelectStmt> extends Ok ? Ok, SkipWithData> - : ParseSelectStmt> + : ParseSelectStmt> : SkipStorage; type ParseCtasExecute< @@ -443,7 +439,7 @@ type ParseCtasExecute< Ident extends [infer N, infer J extends number] ? ( T[J] extends OpTok<"("> - ? ParseExprList, []> + ? ParseExprList, []> : Ok<[], J> ) extends infer R ? R extends Ok @@ -451,7 +447,7 @@ type ParseCtasExecute< T[J] extends OpTok<"("> ? EatOp extends never ? Expected<'")"', T, J2> - : Nx + : Next : J2 ) extends infer J3 ? J3 extends number @@ -491,37 +487,37 @@ type ParsePartitionOf< type SkipPartitionBound = T[I] extends WordTok<"default"> - ? Nx + ? Next : EatWords extends never ? Expected<"FOR VALUES or DEFAULT", T, I> : Nx2 extends infer J extends number ? T[J] extends WordTok<"in" | "with"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? T[Nx2] extends OpTok<")"> ? SyntaxErrorAt> : SkipToCloseParen> - : Expected<'"("', T, Nx> + : Expected<'"("', T, Next> : T[J] extends WordTok<"from"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? SkipToCloseParen> extends infer K extends number ? EatWord extends never ? Expected<"TO", T, K> - : T[Nx] extends OpTok<"("> + : T[Next] extends OpTok<"("> ? SkipToCloseParen> - : Expected<'"("', T, Nx> + : Expected<'"("', T, Next> : SkipToCloseParen> - : Expected<'"("', T, Nx> + : Expected<'"("', T, Next> : Expected<"IN, FROM or WITH", T, J> : never; type ParseTableTail = T[I] extends WordTok<"inherits"> - ? EatOp, "("> extends never - ? Expected<'"("', T, Nx> + ? EatOp, "("> extends never + ? Expected<'"("', T, Next> : ParseNameList, []> extends Ok ? EatOp extends never ? Expected<'")"', T, J> - : WithInherits>> + : WithInherits>> : ParseNameList, []> : WithInherits<[], PartitionTail>; type WithInherits = J extends number ? [Ps, J] : J; @@ -532,9 +528,9 @@ type PartitionTail = : Nx2 extends infer J extends number ? Ident extends [string, infer J1 extends number] ? T[J1] extends OpTok<"("> - ? SkipToCloseParen> extends infer J2 extends number + ? SkipToCloseParen> extends infer J2 extends number ? SkipStorage - : SkipToCloseParen> + : SkipToCloseParen> : Expected<'"("', T, J1> : Expected<"RANGE, LIST or HASH", T, J> : never; @@ -555,9 +551,9 @@ type SkipStorage = : never; type SkipReloptions = T[I] extends WordTok<"with"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? SkipToCloseParen> - : SyntaxErrorAt> + : SyntaxErrorAt> : Mat extends true ? I : SkipWords; @@ -567,10 +563,10 @@ type SkipOnCommit = : OnCommitAction>; type OnCommitAction = T[I] extends WordTok<"drop"> - ? Nx + ? Next : T[I] extends WordTok<"delete" | "preserve"> - ? EatWord, "rows"> extends never - ? Expected<"ROWS", T, Nx> + ? EatWord, "rows"> extends never + ? Expected<"ROWS", T, Next> : Nx2 : Expected<"DROP, DELETE ROWS or PRESERVE ROWS", T, I>; type SkipTablespace = @@ -578,7 +574,7 @@ type SkipTablespace = type ParseTableBody = T[I] extends OpTok<")"> - ? Ok<[[], [], []], Nx> + ? Ok<[[], [], []], Next> : ParseTableElements; type ParseTableElements< @@ -597,9 +593,9 @@ type ParseTableElements< ? ElementNext : ParseTableConstraint : T[I] extends WordTok<"like"> - ? ParseLike> extends Ok + ? ParseLike> extends Ok ? ElementNext - : ParseLike> + : ParseLike> : ParseColumnDef extends Ok ? ElementNext : ParseColumnDef; @@ -615,7 +611,7 @@ type IsConstraintStart = T[I] extends WordTok ? true : T[I] extends WordTok<"exclude"> - ? T[Nx] extends WordTok<"using"> | OpTok<"("> + ? T[Next] extends WordTok<"using"> | OpTok<"("> ? true : false : false; @@ -629,24 +625,24 @@ type ElementNext< Like extends readonly LikeClause[], > = T[I] extends OpTok<","> - ? ParseTableElements, TN, Cols, Cons, Like> + ? ParseTableElements, TN, Cols, Cons, Like> : T[I] extends OpTok<")"> - ? Ok<[Cols, Cons, Like], Nx> + ? Ok<[Cols, Cons, Like], Next> : Expected<'"," or ")"', T, I>; type ParseTableConstraint = T[I] extends WordTok<"constraint"> - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? ParseTableConstraintBody - : Expected<"constraint name", T, Nx> + : Expected<"constraint name", T, Next> : ParseTableConstraintBody; type ParseTableConstraintBody = EatWords extends never ? T[I] extends WordTok<"not"> - ? ParseNotNullConstraint> + ? ParseNotNullConstraint> : T[I] extends WordTok<"unique" | "check" | "foreign" | "exclude"> - ? Ok<{ t: "other" }, SkipToElementEnd>> + ? Ok<{ t: "other" }, SkipToElementEnd>> : Expected<"table constraint", T, I> : T[Nx2] extends OpTok<"("> ? KeyColumns, []> extends Ok @@ -661,7 +657,7 @@ type KeyColumns< > = Ident extends [infer N extends string, infer J extends number] ? T[J] extends OpTok<","> - ? KeyColumns, [...Acc, N]> + ? KeyColumns, [...Acc, N]> : ( EatWords extends never ? J @@ -669,14 +665,14 @@ type KeyColumns< ) extends infer K extends number ? EatOp extends never ? Expected<'"," or ")"', T, K> - : Ok<[...Acc, N], Nx> + : Ok<[...Acc, N], Next> : never : Expected<"column name", T, I>; type ParseNotNullConstraint = EatWord extends never ? Expected<"NULL", T, I> - : Ident> extends [infer C, infer J extends number] + : Ident> extends [infer C, infer J extends number] ? ConstraintAttrs extends [ infer NotValid, infer J2 extends number, @@ -686,7 +682,7 @@ type ParseNotNullConstraint = J2 > : ConstraintAttrs - : Expected<"column name", T, Nx>; + : Expected<"column name", T, Next>; type ConstraintAttrs< T extends Toks, @@ -700,21 +696,21 @@ type ConstraintAttrs< : W extends "enforced" ? NotMarked<"ENFORCED", At> : W extends "initially" - ? T[Nx] extends WordTok<"immediate"> + ? T[Next] extends WordTok<"immediate"> ? ConstraintAttrs, NotValid, At> - : T[Nx] extends WordTok<"deferred"> + : T[Next] extends WordTok<"deferred"> ? NotMarked<"DEFERRABLE", At> : [NotValid, I] : W extends "no" - ? T[Nx] extends WordTok<"inherit"> + ? T[Next] extends WordTok<"inherit"> ? ConstraintAttrs, NotValid, At> : [NotValid, I] : W extends "not" - ? T[Nx] extends WordTok<"deferrable"> + ? T[Next] extends WordTok<"deferrable"> ? ConstraintAttrs, NotValid, At> - : T[Nx] extends WordTok<"enforced"> + : T[Next] extends WordTok<"enforced"> ? NotMarked<"NOT ENFORCED", At> - : T[Nx] extends WordTok<"valid"> + : T[Next] extends WordTok<"valid"> ? ConstraintAttrs, true, At> : [NotValid, I] : [NotValid, I] @@ -752,7 +748,7 @@ type ParseLikeOptions< Copies extends boolean, > = T[I] extends WordTok<"including"> - ? T[Nx] extends WordTok + ? T[Next] extends WordTok ? ParseLikeOptions< T, Nx2, @@ -761,16 +757,16 @@ type ParseLikeOptions< : Def | Extract, O extends DefaultLikeOption | "all" ? true : Copies > - : Expected<"LIKE option", T, Nx> + : Expected<"LIKE option", T, Next> : T[I] extends WordTok<"excluding"> - ? T[Nx] extends WordTok + ? T[Next] extends WordTok ? ParseLikeOptions< T, Nx2, O extends "all" ? never : Exclude, O extends DefaultLikeOption | "all" ? false : Copies > - : Expected<"LIKE option", T, Nx> + : Expected<"LIKE option", T, Next> : [Def, I, Copies]; type ParseColumnDef = @@ -793,7 +789,7 @@ type SkipCompression = T[I] extends WordTok<"compression"> ? Nx2 : I; type IsSerial = - T[I] extends WordTok ? true : false; + T[I] extends WordTok ? true : false; type ColCtx = { readonly tn: string; @@ -813,11 +809,11 @@ type ParseColConstraints< > = T[I] extends WordTok ? W extends "constraint" - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? ParseColConstraints - : Expected<"constraint name", T, Nx> + : Expected<"constraint name", T, Next> : W extends "not" - ? T[Nx] extends WordTok<"null"> + ? T[Next] extends WordTok<"null"> ? "nul" extends F ? NullConflict : ParseColConstraints< @@ -826,20 +822,20 @@ type ParseColConstraints< C, F | "nn" > - : T[Nx] extends WordTok<"deferrable" | "enforced"> + : T[Next] extends WordTok<"deferrable" | "enforced"> ? ParseColConstraints, C, F> - : Expected<"NULL", T, Nx> + : Expected<"NULL", T, Next> : W extends "null" ? Extract extends never - ? ParseColConstraints, C, F | "nul"> + ? ParseColConstraints, C, F | "nul"> : NullConflict : W extends "default" - ? ParseBExpr> extends Ok + ? ParseBExpr> extends Ok ? ParseColConstraints - : ParseBExpr> + : ParseBExpr> : W extends "primary" - ? EatWord, "key"> extends never - ? Expected<"KEY", T, Nx> + ? EatWord, "key"> extends never + ? Expected<"KEY", T, Next> : "nul" extends F ? NullConflict : SkipIndexParams> extends infer J extends number @@ -850,7 +846,7 @@ type ParseColConstraints< T, SkipFirst< T, - Nx, + Next, [["nulls", "distinct"], ["nulls", "not", "distinct"]] > > extends infer J @@ -859,7 +855,7 @@ type ParseColConstraints< : J : never : W extends "check" - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? SkipToCloseParen> extends infer J extends number ? ParseColConstraints< T, @@ -868,9 +864,9 @@ type ParseColConstraints< F > : SkipToCloseParen> - : Expected<'"("', T, Nx> + : Expected<'"("', T, Next> : W extends "references" - ? QualName> extends [ + ? QualName> extends [ unknown, infer J extends number, ] @@ -879,9 +875,9 @@ type ParseColConstraints< ? ParseColConstraints : SkipRefActions : SkipParenOpt - : Expected<"referenced table", T, Nx> + : Expected<"referenced table", T, Next> : W extends "generated" - ? ParseGenerated> extends infer G + ? ParseGenerated> extends infer G ? G extends [true, infer J extends number] ? "nul" extends F ? NullConflict @@ -891,16 +887,16 @@ type ParseColConstraints< : G : never : W extends "deferrable" | "enforced" - ? ParseColConstraints, C, F> + ? ParseColConstraints, C, F> : W extends "initially" ? ParseColConstraints, C, F> : W extends "collate" - ? QualName> extends [ + ? QualName> extends [ unknown, infer J extends number, ] ? ParseColConstraints - : Expected<"collation name", T, Nx> + : Expected<"collation name", T, Next> : Expected<"column constraint", T, I> : Ok< { @@ -916,13 +912,13 @@ type ParseColConstraints< type ParseBExpr = ParseExpr extends infer R ? R extends Ok - ? T[J] extends OpTok<"=" | "<" | ">" | "<=" | ">=" | "<>" | "!="> - ? ParseExpr, 7> + ? T[J] extends OpTok + ? ParseExpr, 7> : T[J] extends WordTok<"is"> - ? EatWords, ["distinct", "from"]> extends never - ? EatWords, ["not", "distinct", "from"]> extends never + ? EatWords, ["distinct", "from"]> extends never + ? EatWords, ["not", "distinct", "from"]> extends never ? R - : ParseExpr>, 7> + : ParseExpr>, 7> : ParseExpr, 7> : R : R @@ -930,7 +926,7 @@ type ParseBExpr = type SkipIndexParams = T[I] extends WordTok<"with"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? SkipToCloseParen> extends infer J extends number ? SkipIndexTablespace : SkipToCloseParen> @@ -945,7 +941,7 @@ type SkipRefActions = T[I] extends WordTok<"match"> ? SkipRefActions> : T[I] extends WordTok<"on"> - ? T[Nx] extends WordTok<"delete" | "update"> + ? T[Next] extends WordTok<"delete" | "update"> ? T[Nx2] extends WordTok<"no"> ? SkipRefActions>> : T[Nx2] extends WordTok<"set"> @@ -967,9 +963,9 @@ type ParseGenerated = ? [J] extends [never] ? Expected<"ALWAYS or BY DEFAULT", T, I> : T[J] extends OpTok<"("> - ? SkipToCloseParen> extends infer K extends number + ? SkipToCloseParen> extends infer K extends number ? [false, SkipWord] - : SkipToCloseParen> + : SkipToCloseParen> : Expected<'"(" or IDENTITY', T, J> : never : IdentityNext< @@ -1013,9 +1009,9 @@ type SkipViewStorage< > = Mat extends true ? SkipStorage : T[I] extends WordTok<"with"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? SkipToCloseParen> - : SyntaxErrorAt> + : SyntaxErrorAt> : I; type ParseViewAs< @@ -1030,7 +1026,7 @@ type ParseViewAs< > = EatWord extends never ? Expected<"AS", T, I> - : ParseSelectStmt> extends Ok + : ParseSelectStmt> extends Ok ? Rec extends true ? SkipCheckOption extends J ? ViewOk< @@ -1052,7 +1048,7 @@ type ParseViewAs< Q, Mat extends true ? SkipWithData : SkipCheckOption > - : ParseSelectStmt>; + : ParseSelectStmt>; type ViewOk = Ok< { t: "create_view"; @@ -1105,7 +1101,7 @@ type SkipCheckOption = ? CheckOptionEnd< EatWords< T, - SkipWord, "cascaded" | "local">, + SkipWord, "cascaded" | "local">, ["check", "option"] >, I @@ -1130,24 +1126,24 @@ type ParseCreateType = : Expected<"type name", T, I>; type ParseEnumValues = - T[I] extends OpTok<")"> ? Ok<[], Nx> : EnumLabels; + T[I] extends OpTok<")"> ? Ok<[], Next> : EnumLabels; type EnumLabels< T extends Toks, I extends number, Acc extends readonly string[], > = T[I] extends StrTok - ? T[Nx] extends OpTok<","> + ? T[Next] extends OpTok<","> ? EnumLabels, [...Acc, V]> - : T[Nx] extends OpTok<")"> + : T[Next] extends OpTok<")"> ? Ok<[...Acc, V], Nx2> - : Expected<'"," or ")"', T, Nx> + : Expected<'"," or ")"', T, Next> : Expected<"enum label", T, I>; type ParseCreateSchema = IfNotExists extends [infer Ine extends boolean, infer I1 extends number] ? T[I1] extends WordTok<"authorization"> - ? AnyLabel> extends [ + ? AnyLabel> extends [ infer Role extends string, infer J extends number, ] @@ -1156,7 +1152,7 @@ type ParseCreateSchema = J, Ine, Role, - T[Nx] extends WordTok< + T[Next] extends WordTok< "current_role" | "current_user" | "session_user" > ? false @@ -1164,12 +1160,12 @@ type ParseCreateSchema = [], [] > - : Expected<"role name", T, Nx> + : Expected<"role name", T, Next> : Ident extends [infer Name extends string, infer J extends number] ? T[J] extends WordTok<"authorization"> - ? AnyLabel> extends [string, infer J2 extends number] + ? AnyLabel> extends [string, infer J2 extends number] ? SchemaElements - : Expected<"role name", T, Nx> + : Expected<"role name", T, Next> : SchemaElements : Expected<"schema name", T, I1> : never; @@ -1189,14 +1185,14 @@ type SchemaElements< : T[I] extends WordTok<"grant"> ? SchemaElements< T, - SkipElement, I>, + SkipElement, I>, Ine, S, Known, Tbls, Views > - : SchemaElement, S, Known, false, false> extends infer R + : SchemaElement, S, Known, false, false> extends infer R ? R extends Ok ? E extends Tag<"create_table"> ? SchemaElements @@ -1225,13 +1221,13 @@ type SchemaElement< > = T[I] extends WordTok ? W extends "or" - ? EatWord, "replace"> extends never - ? Expected<"REPLACE", T, Nx> + ? EatWord, "replace"> extends never + ? Expected<"REPLACE", T, Next> : SchemaElement, S, Known, true, Temp> : W extends "temp" | "temporary" | "local" | "global" | "unlogged" ? SchemaElement< T, - Nx, + Next, S, Known, OrR, @@ -1245,11 +1241,11 @@ type SchemaElement< > : ( W extends "table" - ? ParseCreateTable> + ? ParseCreateTable> : W extends "view" - ? ParseCreateView, OrR, false, false> - : EatWord, "view"> extends never - ? Expected<"VIEW", T, Nx> + ? ParseCreateView, OrR, false, false> + : EatWord, "view"> extends never + ? Expected<"VIEW", T, Next> : ParseCreateView, OrR, false, false, true> ) extends infer R ? R extends Ok @@ -1265,7 +1261,7 @@ type SchemaElement< Known, Ref, Requalify, - IfNotExists>[1] + IfNotExists>[1] > : E extends { readonly t: "create_view"; @@ -1277,13 +1273,13 @@ type SchemaElement< Known, Ref, Requalify, - W extends "view" ? Nx : Nx2 + W extends "view" ? Next : Nx2 > : R : R : never : W extends "sequence" - ? QualName>[1]> extends [ + ? QualName>[1]> extends [ infer P extends readonly string[], infer J extends number, ] @@ -1291,18 +1287,18 @@ type SchemaElement< T, J, InSchema< - SkipElement, I>, + SkipElement, I>, S, Known, RefOf

, null, - IfNotExists>[1] + IfNotExists>[1] > > - : Expected<"sequence name", T, Nx> + : Expected<"sequence name", T, Next> : W extends "index" | "unique" | "trigger" | "constraint" - ? SkipElement, I> extends infer J extends number - ? ElementOn, J> extends infer K extends number + ? SkipElement, I> extends infer J extends number + ? ElementOn, J> extends infer K extends number ? QualName> extends [ infer P extends readonly string[], infer K2 extends number, @@ -1320,7 +1316,7 @@ type SchemaElement< > > : SyntaxErrorAt> - : ElementOn, J> + : ElementOn, J> : never : Expected<"TABLE, VIEW, INDEX, SEQUENCE or TRIGGER", T, I> : Expected<"TABLE, VIEW, INDEX, SEQUENCE or TRIGGER", T, I>; @@ -1334,7 +1330,7 @@ type Requalify = { }; type NoDanglingDot = - T[J] extends OpTok<"."> ? SyntaxErrorAt> : Then; + T[J] extends OpTok<"."> ? SyntaxErrorAt> : Then; type InSchema< J extends number, @@ -1361,20 +1357,20 @@ type ElementOn< > = I extends End ? SyntaxErrorAt : T[I] extends WordTok<"on"> - ? Nx + ? Next : T[I] extends OpTok<"("> ? SyntaxErrorAt - : ElementOn, End>; + : ElementOn, End>; type SkipElement = T[I] extends OpTok<";"> ? I : T[I] extends WordTok<"create"> ? T[P] extends WordTok<"grant"> | OpTok<","> - ? SkipElement, I> + ? SkipElement, I> : I : T[I] extends Tok - ? SkipElement, I> + ? SkipElement, I> : I; type NoopAlterObject = @@ -1389,15 +1385,15 @@ type NoopAlterObject = type ParseAlter = T[I] extends WordTok ? W extends "table" | "view" - ? ParseAlterTable> + ? ParseAlterTable> : W extends "materialized" - ? EatWord, "view"> extends never - ? Expected<"VIEW", T, Nx> + ? EatWord, "view"> extends never + ? Expected<"VIEW", T, Next> : ParseAlterTable> : W extends "type" - ? ParseAlterType> + ? ParseAlterType> : W extends "schema" - ? ParseAlterSchema> + ? ParseAlterSchema> : W extends NoopAlterObject ? Noop : Expected<"TABLE, VIEW, TYPE, SCHEMA, ... after ALTER", T, I> @@ -1414,8 +1410,8 @@ type ParseAlterSchema = type ParseAlterTable = T[I] extends WordTok<"all"> - ? EatWords, ["in", "tablespace"]> extends never - ? Expected<"IN TABLESPACE", T, Nx> + ? EatWords, ["in", "tablespace"]> extends never + ? Expected<"IN TABLESPACE", T, Next> : Noop : IfExists extends [ infer Ife extends boolean, @@ -1425,12 +1421,15 @@ type ParseAlterTable = infer Parts extends readonly string[], infer J extends number, ] - ? (EatOp extends never ? J : Nx) extends infer J1 extends - number + ? ( + EatOp extends never + ? J + : Next + ) extends infer J1 extends number ? IsStandalone extends true ? ( T[J1] extends WordTok<"rename"> - ? ParseAlterRename> + ? ParseAlterRename> : ParseSetSchema> ) extends infer R ? R extends Ok @@ -1454,7 +1453,7 @@ type IsStandalone = T[I] extends WordTok<"rename"> ? true : T[I] extends WordTok<"set"> - ? T[Nx] extends WordTok<"schema"> + ? T[Next] extends WordTok<"schema"> ? true : false : false; @@ -1478,7 +1477,7 @@ type ParseAlterActions< infer J > ? T[J] extends OpTok<","> - ? ParseAlterActions, Ref, Ife, [...Acc, A]> + ? ParseAlterActions, Ref, Ife, [...Acc, A]> : Ok< { t: "alter_table"; @@ -1498,14 +1497,14 @@ type NoopAction = Ok< type ParseAlterAction = T[I] extends WordTok ? W extends "add" - ? ParseAlterAdd, TN> + ? ParseAlterAdd, TN> : W extends "drop" - ? ParseAlterDrop> + ? ParseAlterDrop> : W extends "alter" - ? ParseAlterColumn> + ? ParseAlterColumn> : W extends "set" - ? T[Nx] extends WordTok<"with"> - ? SyntaxErrorAt> + ? T[Next] extends WordTok<"with"> + ? SyntaxErrorAt> : NoopAction : W extends | "owner" @@ -1529,9 +1528,9 @@ type ParseAlterAction = type ParseAlterAdd = T[I] extends WordTok<"constraint"> - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? ParseAddConstraintBody - : Expected<"constraint name", T, Nx> + : Expected<"constraint name", T, Next> : T[I] extends WordTok<"primary" | "not"> ? ParseAddConstraintBody : T[I] extends WordTok<"unique" | "check" | "foreign" | "exclude"> @@ -1547,17 +1546,17 @@ type ParseAlterAdd = type ParseAddConstraintBody = T[I] extends WordTok<"not"> - ? ParseNotNullConstraint> extends Ok + ? ParseNotNullConstraint> extends Ok ? Ok< C extends { readonly col: infer N } ? { t: "set_not_null"; name: N } : { t: "noop" }, J > - : ParseNotNullConstraint> + : ParseNotNullConstraint> : T[I] extends WordTok<"primary"> - ? EatWord, "key"> extends never - ? Expected<"KEY", T, Nx> + ? EatWord, "key"> extends never + ? Expected<"KEY", T, Next> : T[Nx2] extends OpTok<"("> ? KeyColumns, []> extends Ok ? Ok<{ t: "add_pk"; cols: Cols }, SkipToActionEnd> @@ -1579,26 +1578,26 @@ type ParseAlterDrop = type ParseAlterRename = T[I] extends WordTok<"to"> - ? Ident> extends [infer To, infer J extends number] + ? Ident> extends [infer To, infer J extends number] ? Ok<{ t: "rename_table"; to: To }, J> - : Expected<"new name", T, Nx> + : Expected<"new name", T, Next> : T[I] extends WordTok<"constraint"> - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? EatWord extends never ? Expected<"TO", T, J> - : Ident> extends [string, infer J2 extends number] + : Ident> extends [string, infer J2 extends number] ? Ok<{ t: "noop" }, J2> - : Expected<"new constraint name", T, Nx> - : Expected<"constraint name", T, Nx> + : Expected<"new constraint name", T, Next> + : Expected<"constraint name", T, Next> : Ident> extends [ infer From, infer J extends number, ] ? EatWord extends never ? Expected<"TO", T, J> - : Ident> extends [infer To, infer J2 extends number] + : Ident> extends [infer To, infer J2 extends number] ? Ok<{ t: "rename_column"; from: From; to: To }, J2> - : Expected<"new column name", T, Nx> + : Expected<"new column name", T, Next> : Expected<"column name", T, SkipWord>; type ParseAlterColumn = @@ -1610,29 +1609,29 @@ type ParseAlterColumn = ] ? T[J] extends WordTok ? W extends "type" - ? ParseAlterColType, C> + ? ParseAlterColType, C> : W extends "set" - ? T[Nx] extends WordTok<"data"> + ? T[Next] extends WordTok<"data"> ? EatWord, "type"> extends never ? Expected<"TYPE", T, Nx2> : ParseAlterColType, C> - : T[Nx] extends WordTok<"not"> + : T[Next] extends WordTok<"not"> ? EatWord, "null"> extends never ? Expected<"NULL", T, Nx2> : Ok<{ t: "set_not_null"; name: C }, Nx3> - : T[Nx] extends WordTok<"default"> + : T[Next] extends WordTok<"default"> ? ParseExpr, 0> extends Ok ? Ok<{ t: "set_default"; name: C }, J2> : ParseExpr, 0> : NoopAction : W extends "drop" - ? T[Nx] extends WordTok<"not"> + ? T[Next] extends WordTok<"not"> ? EatWord, "null"> extends never ? Expected<"NULL", T, Nx2> : Ok<{ t: "drop_not_null"; name: C }, Nx3> - : T[Nx] extends WordTok<"default"> + : T[Next] extends WordTok<"default"> ? Ok<{ t: "drop_default"; name: C }, Nx2> - : T[Nx] extends WordTok<"identity" | "expression"> + : T[Next] extends WordTok<"identity" | "expression"> ? Ok< { t: "drop_default"; name: C }, SkipWords, ["if", "exists"]> @@ -1640,11 +1639,11 @@ type ParseAlterColumn = : Expected< "NOT NULL, DEFAULT, IDENTITY or EXPRESSION", T, - Nx + Next > : W extends "add" - ? EatWord, "generated"> extends never - ? Expected<"GENERATED", T, Nx> + ? EatWord, "generated"> extends never + ? Expected<"GENERATED", T, Next> : ParseGenerated> extends infer G ? G extends [true, infer J2 extends number] ? Ok<{ t: "add_identity"; name: C }, J2> @@ -1663,16 +1662,16 @@ type ParseAlterColType = ? SkipCollate extends infer J2 extends number ? EatWord extends never ? Ok<{ t: "alter_type"; name: C; type: Ty }, J2> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok<{ t: "alter_type"; name: C; type: Ty }, J3> - : ParseExpr, 0> + : ParseExpr, 0> : never : ParseTypeName; type SkipCollate = T[I] extends WordTok<"collate"> - ? QualName> extends [unknown, infer J] + ? QualName> extends [unknown, infer J] ? J - : Nx + : Next : I; type ParseAlterType = @@ -1681,7 +1680,7 @@ type ParseAlterType = infer J extends number, ] ? T[J] extends WordTok<"add"> - ? EatWord, "value"> extends never + ? EatWord, "value"> extends never ? Ok< { t: "alter_enum"; name: RefOf; action: { t: "noop" } }, SkipToStmtEnd @@ -1694,12 +1693,12 @@ type ParseAlterType = name: RefOf; action: { t: "add_value"; value: V; ifNotExists: Ine }; }, - SkipBeforeAfter> + SkipBeforeAfter> > : Expected<"enum label", T, I1> : never : T[J] extends WordTok<"rename"> - ? T[Nx] extends WordTok<"value"> + ? T[Next] extends WordTok<"value"> ? T[Nx2] extends StrTok ? EatWord, "to"> extends never ? Expected<"TO", T, Nx3> @@ -1714,7 +1713,7 @@ type ParseAlterType = > : Expected<"enum label", T, Nx2>> : Expected<"enum label", T, Nx2> - : T[Nx] extends WordTok<"to"> + : T[Next] extends WordTok<"to"> ? Ident> extends [infer To, infer J2 extends number] ? Ok< { @@ -1738,7 +1737,7 @@ type ParseAlterType = : Expected<"type name", T, I>; type SkipBeforeAfter = T[I] extends WordTok<"before" | "after"> - ? T[Nx] extends StrTok + ? T[Next] extends StrTok ? Nx2 : I : I; @@ -1756,17 +1755,17 @@ type NoopDropObject = type ParseDrop = T[I] extends WordTok ? W extends "table" - ? ParseDropList, "drop_table"> + ? ParseDropList, "drop_table"> : W extends "view" - ? ParseDropList, "drop_view"> + ? ParseDropList, "drop_view"> : W extends "materialized" - ? EatWord, "view"> extends never - ? Expected<"VIEW", T, Nx> + ? EatWord, "view"> extends never + ? Expected<"VIEW", T, Next> : ParseDropList, "drop_matview"> : W extends "type" - ? ParseDropList, "drop_type"> + ? ParseDropList, "drop_type"> : W extends "schema" - ? ParseDropList, "drop_schema"> + ? ParseDropList, "drop_schema"> : W extends NoopDropObject ? Noop : Expected< diff --git a/packages/core/src/postgres/engine/parser/dml.ts b/packages/core/src/postgres/engine/parser/dml.ts index 9c796eeb..72cd199b 100644 --- a/packages/core/src/postgres/engine/parser/dml.ts +++ b/packages/core/src/postgres/engine/parser/dml.ts @@ -1,4 +1,5 @@ -import type { OpTok, Toks, WordTok } from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { Expr, ReturningOpt, @@ -6,6 +7,7 @@ import type { TargetInd, WithClause, } from "../ast.js"; +import type { OpTok, WordTok } from "../lexer/tokens.js"; import type { ParseBracket, ParseExpr } from "./expr.js"; import type { IsSelectStart, @@ -25,7 +27,6 @@ import type { Expected, Fail, Ident, - Nx, Nx2, Nx3, Ok, @@ -40,7 +41,7 @@ export type ParseInsert< > = EatWord extends never ? Expected<"INTO", T, I> - : ParseDmlTarget, "values" | "overriding", false> extends Ok< + : ParseDmlTarget, "values" | "overriding", false> extends Ok< [infer Ref, infer Alias], infer I1 > @@ -78,11 +79,11 @@ export type ParseInsert< : never : ParseOverriding : ParseInsertColumns - : ParseDmlTarget, "values" | "overriding", false>; + : ParseDmlTarget, "values" | "overriding", false>; type ParseInsertColumns = T[I] extends OpTok<"("> - ? IsSelectStart> extends true + ? IsSelectStart> extends true ? Ok<[null, null], I> : ParenTargetList : Ok<[null, null], I>; @@ -90,8 +91,8 @@ type ParseInsertColumns = export type ParseOverriding = EatWord extends never ? Ok - : EatWord, "system" | "user"> extends never - ? Expected<"SYSTEM or USER", T, Nx> + : EatWord, "system" | "user"> extends never + ? Expected<"SYSTEM or USER", T, Next> : EatWord, "value"> extends never ? Expected<"VALUE", T, Nx2> : Ok>; @@ -102,15 +103,15 @@ type ParseInsertSource< HasCols extends boolean, > = T[I] extends WordTok<"values"> - ? ParseValues> + ? ParseValues> : T[I] extends WordTok<"default"> ? HasCols extends true ? Fail< "syntax error at or near default: DEFAULT VALUES cannot be combined with a column list", I > - : EatWord, "values"> extends never - ? Expected<"VALUES", T, Nx> + : EatWord, "values"> extends never + ? Expected<"VALUES", T, Next> : Ok<{ t: "default" }, Nx2> : IsSelectStart extends true ? ParseSelectStmt extends Ok @@ -127,13 +128,13 @@ type ParseOnConflictOpt = type ParseConflictTarget = T[I] extends OpTok<"("> - ? ParseIndexParams> extends Ok + ? ParseIndexParams> extends Ok ? EatWord extends never ? Ok - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok - : ParseExpr, 0> - : ParseIndexParams> + : ParseExpr, 0> + : ParseIndexParams> : EatWords extends never ? Ok : Ident> extends [string, infer J extends number] @@ -150,8 +151,8 @@ type ParseIndexParams = : EatOp extends never ? EatOp extends never ? Expected<'"," or ")"', T, J2> - : Ok> - : ParseIndexParams> + : Ok> + : ParseIndexParams> : never : ParseExpr; type SkipOpClass = @@ -164,9 +165,9 @@ type SkipOpClass = type ParseConflictAction = EatWord extends never ? Expected<"DO", T, I> - : EatWord, "nothing"> extends never - ? EatWords, ["update", "set"]> extends never - ? Expected<"NOTHING or UPDATE SET", T, Nx> + : EatWord, "nothing"> extends never + ? EatWords, ["update", "set"]> extends never + ? Expected<"NOTHING or UPDATE SET", T, Next> : ParseSetList, []> extends infer X ? X extends Ok ? ParseWhereOpt extends Ok @@ -187,7 +188,7 @@ export type ParseUpdate< > ? EatWord extends never ? Expected<"SET", T, I1> - : ParseSetList, []> extends Ok + : ParseSetList, []> extends Ok ? ParseFromOpt extends Ok ? ParseDmlWhere extends Ok ? ParseReturningOpt extends Ok< @@ -211,7 +212,7 @@ export type ParseUpdate< : ParseReturningOpt : ParseDmlWhere : ParseFromOpt - : ParseSetList, []> + : ParseSetList, []> : ParseDmlTarget; export type ParseSetList< @@ -222,7 +223,7 @@ export type ParseSetList< ParseSetItem extends Ok ? EatOp extends never ? Ok<[...Acc, Item], J> - : ParseSetList, [...Acc, Item]> + : ParseSetList, [...Acc, Item]> : ParseSetItem; type ParseSetItem = @@ -230,21 +231,21 @@ type ParseSetItem = ? ParenTargetList extends Ok<[infer Cols, infer Ind], infer J> ? EatOp - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok<{ t: "setmulti"; cols: Cols; ind: Ind; e: E }, J2> - : ParseExpr, 0> + : ParseExpr, 0> : ParenTargetList : ParseAssignTarget extends Ok<[infer Col, infer Ind], infer J> ? EatOp - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok< Ind extends Expr ? { t: "setind"; col: Col; target: Ind; e: E } : { t: "set"; col: Col; e: E }, J2 > - : ParseExpr, 0> + : ParseExpr, 0> : ParseAssignTarget; type ParseAssignTarget = @@ -261,20 +262,20 @@ type ParseIndirection< E, > = T[I] extends OpTok<"["> - ? ParseBracket> extends Ok + ? ParseBracket> extends Ok ? ParseIndirection - : ParseBracket> + : ParseBracket> : T[I] extends OpTok<"."> - ? T[Nx] extends OpTok<"*"> + ? T[Next] extends OpTok<"*"> ? Fail<'row expansion via "*" is not supported here', Tg[1]> - : AnyLabel> extends [infer N, infer J extends number] + : AnyLabel> extends [infer N, infer J extends number] ? ParseIndirection - : Expected<"field name", T, Nx> + : Expected<"field name", T, Next> : Ok<[Tg[0], E], I>; export type ParenTargetList = TargetListLoop< T, - Nx, + Next, [], [], false @@ -302,11 +303,11 @@ type TargetListLoop< ? null : [...Inds, Ind], ], - Nx + Next > : TargetListLoop< T, - Nx, + Next, [...Cols, N], [...Inds, Ind], Ind extends null ? Any : true @@ -320,7 +321,7 @@ export type ParseDelete< > = EatWord extends never ? Expected<"FROM", T, I> - : ParseDmlTarget, never, true> extends Ok< + : ParseDmlTarget, never, true> extends Ok< [infer Ref, infer Alias], infer I1 > @@ -346,12 +347,12 @@ export type ParseDelete< : ParseReturningOpt : ParseDmlWhere : ParseUsingOpt - : ParseDmlTarget, never, true>; + : ParseDmlTarget, never, true>; type ParseUsingOpt = EatWord extends never ? Ok<[], I> - : ParseFromList, []>; + : ParseFromList, []>; export type ParseDmlTarget< T extends Toks, @@ -365,7 +366,7 @@ export type ParseDmlTarget< ] ? ParseDmlAlias< T, - Star extends true ? (T[J] extends OpTok<"*"> ? Nx : J) : J, + Star extends true ? (T[J] extends OpTok<"*"> ? Next : J) : J, Stop > extends infer A ? A extends Ok @@ -376,9 +377,9 @@ export type ParseDmlTarget< type ParseDmlAlias = T[I] extends WordTok<"as"> - ? Ident> extends [infer A, infer J extends number] + ? Ident> extends [infer A, infer J extends number] ? Ok - : Expected<"alias", T, Nx> + : Expected<"alias", T, Next> : Ident extends [infer A, infer J extends number] ? A extends Stop ? Ok @@ -395,8 +396,8 @@ type ParseDmlWhere = export type ParseReturningOpt = EatWord extends never ? Ok<[null, []], I> - : EatWord, "with"> extends never - ? ReturningTargets, []> + : EatWord, "with"> extends never + ? ReturningTargets, []> : EatOp, "("> extends never ? Expected<'"("', T, Nx2> : ParseReturningOpts, []>; @@ -416,8 +417,8 @@ type ParseReturningOpts< Acc extends readonly ReturningOpt[], > = T[I] extends WordTok - ? EatWord, "as"> extends never - ? Expected<"AS", T, Nx> + ? EatWord, "as"> extends never + ? Expected<"AS", T, Next> : Ident> extends [ infer A extends string, infer J extends number, @@ -425,7 +426,7 @@ type ParseReturningOpts< ? EatOp extends never ? EatOp extends never ? Expected<'"," or ")"', T, J> - : ReturningTargets, [...Acc, { kind: K; alias: A }]> - : ParseReturningOpts, [...Acc, { kind: K; alias: A }]> + : ReturningTargets, [...Acc, { kind: K; alias: A }]> + : ParseReturningOpts, [...Acc, { kind: K; alias: A }]> : Expected<"alias", T, Nx2> : Expected<"OLD or NEW", T, I>; diff --git a/packages/core/src/postgres/engine/parser/expr.ts b/packages/core/src/postgres/engine/parser/expr.ts index eb63c8fe..6de165c7 100644 --- a/packages/core/src/postgres/engine/parser/expr.ts +++ b/packages/core/src/postgres/engine/parser/expr.ts @@ -1,14 +1,16 @@ +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; +import type { NormalizeTypeName } from "../../schema/normalize.js"; +import type { IsTest, Tag } from "../ast.js"; import type { IdTok, NumTok, OpTok, ParamTok, StrTok, - Toks, WordTok, -} from "../../../engine/tokens.js"; -import type { NormalizeTypeName } from "../../schema/normalize.js"; -import type { IsTest, Tag } from "../ast.js"; +} from "../lexer/tokens.js"; +import type { CompareOp } from "../ops.js"; import type { JsonFuncWord, ParseJsonFunc, SkipUniqueKeys } from "./json.js"; import type { NotBareLabel, @@ -37,7 +39,6 @@ import type { Expected, Fail, FuncName, - Nx, Nx2, Nx3, Ok, @@ -135,15 +136,15 @@ export type ParseExpr< Min extends MinPrec = 0, > = T[I] extends ParamTok - ? T[Nx] extends OpTok | undefined - ? Ok, Nx> - : ParseInfix, Nx, Min> + ? T[Next] extends OpTok | undefined + ? Ok, Next> + : ParseInfix, Next, Min> : T[I] extends WordTok | IdTok ? T[I] extends WordTok ? ParseExprSlow - : ColumnPrefix], T[Nx2], T[Nx3]> extends infer P + : ColumnPrefix], T[Nx2], T[Nx3]> extends infer P ? P extends [infer L, infer N] - ? ParseInfix : Nx3, Min> + ? ParseInfix : Nx3, Min> : ParseExprSlow : never : ParseExprSlow; @@ -194,7 +195,6 @@ type NextPrec = { 13: 13; }; -type ComparisonOp = "=" | "<" | ">" | "<=" | ">=" | "<>" | "!="; type NotPrefixOp = | "/" | "%" @@ -224,9 +224,9 @@ type ParseInfix = ? T[I] extends WordTok ? W extends NotBareLabel ? ParseInfixAt - : T[Nx] extends OpTok<","> + : T[Next] extends OpTok<","> ? Ok - : EndsTargetList> extends true + : EndsTargetList> extends true ? Ok : ParseInfixAt : ParseInfixAt @@ -244,7 +244,7 @@ type InfixAction = ? "subscript" : O extends "." ? "field" - : O extends ComparisonOp + : O extends CompareOp ? Binds<5, Min> extends true ? "comparison" : "done" @@ -295,37 +295,39 @@ type InfixOps< O extends string, > = { done: Ok; - cast: ParseCastSuffix, Min>; + cast: ParseCastSuffix, Min>; subscript: L extends Indirectable | Tag<"col" | "param"> - ? ParseSubscript, Min> + ? ParseSubscript, Min> + : Ok; + field: L extends Indirectable + ? ParseFieldAccess, Min> : Ok; - field: L extends Indirectable ? ParseFieldAccess, Min> : Ok; - comparison: ParseComparisonRhs, Min>; - add: BinRhs, Min>; - multiply: BinRhs, Min>; - power: BinRhs, Min>; - binary: BinRhs, Min>; - and: ParseChain, Min>; - or: ParseChain, Min>; - is: ParseIs, Min>; - isnull: ParseInfix, Nx, Min>; - notnull: ParseInfix, Nx, Min>; - not: ParseNegated, Min, I>; - in: ParseIn, Min, false>; - like: ParseLike, Min, "like", false>; - ilike: ParseLike, Min, "ilike", false>; - similar: EatWord, "to"> extends never + comparison: ParseComparisonRhs, Min>; + add: BinRhs, Min>; + multiply: BinRhs, Min>; + power: BinRhs, Min>; + binary: BinRhs, Min>; + and: ParseChain, Min>; + or: ParseChain, Min>; + is: ParseIs, Min>; + isnull: ParseInfix, Next, Min>; + notnull: ParseInfix, Next, Min>; + not: ParseNegated, Min, I>; + in: ParseIn, Min, false>; + like: ParseLike, Min, "like", false>; + ilike: ParseLike, Min, "ilike", false>; + similar: EatWord, "to"> extends never ? Ok : ParseLike, Min, "similar", false>; - between: ParseBetween, Min, false>; - at: ParseAtTimeZone, Min>; - collate: ParseCollate, Min>; - operator: ParseOperatorSyntax, Min, I>; + between: ParseBetween, Min, false>; + at: ParseAtTimeZone, Min>; + collate: ParseCollate, Min>; + operator: ParseOperatorSyntax, Min, I>; overlaps: L extends { readonly t: "row"; readonly items: infer Ls extends readonly unknown[]; } - ? ParseOverlaps, Min> + ? ParseOverlaps, Min> : Ok; }; @@ -339,7 +341,7 @@ type NonAssoc< P extends 4 | 5 | 6, > = PrecAt extends P ? SyntaxErrorAt : ParseInfix; type PrecAt = - T[J] extends OpTok + T[J] extends OpTok ? 5 : T[J] extends WordTok ? W extends "is" | "isnull" | "notnull" @@ -347,7 +349,7 @@ type PrecAt = : W extends Prec6Word ? 6 : W extends "not" - ? T[Nx] extends WordTok + ? T[Next] extends WordTok ? 6 : 0 : 0 @@ -372,12 +374,12 @@ type ParseChain< ParseExpr extends Ok ? T[J] extends WordTok ? Min extends "target" - ? T[Nx] extends OpTok<","> + ? T[Next] extends OpTok<","> ? Ok, J> - : EndsTargetList> extends true + : EndsTargetList> extends true ? Ok, J> - : ParseChain, Min> - : ParseChain, Min> + : ParseChain, Min> + : ParseChain, Min> : ParseInfix, J, Min> : ParseExpr; type Balance< @@ -405,9 +407,9 @@ type BinRhs< > = Binds extends true ? T[I] extends WordTok - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseQuantified, Min> - : SyntaxErrorAt> + : SyntaxErrorAt> : ParseExpr extends Ok ? ParseInfix, J, Min> : ParseExpr @@ -421,9 +423,9 @@ type ParseComparisonRhs< Min extends MinPrec, > = T[I] extends WordTok - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseQuantified, Min> - : SyntaxErrorAt> + : SyntaxErrorAt> : ParseExpr extends Ok ? NonAssoc, J, Min, 5> : ParseExpr; @@ -439,7 +441,7 @@ type ParseQuantified< ParseSelectStmt extends Ok ? EatOp extends never ? ParseQuantifiedExpr - : ParseInfix, Nx, Min> + : ParseInfix, Next, Min> : ParseQuantifiedExpr; type ParseQuantifiedExpr< T extends Toks, @@ -452,7 +454,7 @@ type ParseQuantifiedExpr< ParseExpr extends Ok ? EatOp extends never ? Expected<'")"', T, J> - : ParseInfix, Nx, Min> + : ParseInfix, Next, Min> : ParseExpr; type ParseCastSuffix = @@ -470,16 +472,16 @@ export type ParseBracket = ? EatOp extends never ? EatOp extends never ? Expected<'"]"', T, J> - : ParseSliceHi> - : Ok, Nx> + : ParseSliceHi> + : Ok, Next> : ParseExpr - : ParseSliceHi>; + : ParseSliceHi>; type ParseSliceHi = EatOp extends never ? ParseExpr extends Ok ? Close, "]"> : ParseExpr - : Ok, Nx>; + : Ok, Next>; type ParseFieldAccess< T extends Toks, @@ -488,7 +490,7 @@ type ParseFieldAccess< Min extends MinPrec, > = T[I] extends OpTok<"*"> - ? ParseInfix, Nx, Min> + ? ParseInfix, Next, Min> : AnyLabel extends [infer N, infer J extends number] ? ParseInfix, J, Min> : Expected<"field name", T, I>; @@ -496,7 +498,7 @@ type ParseFieldAccess< type ParseIs = EatWord extends never ? ParseIsBody - : ParseIsBody, Min, true>; + : ParseIsBody, Min, true>; type ParseIsBody< T extends Toks, L, @@ -506,16 +508,16 @@ type ParseIsBody< > = T[I] extends WordTok ? W extends "null" | "true" | "false" | "unknown" - ? ParseInfix, Nx, Min> + ? ParseInfix, Next, Min> : W extends "document" - ? ParseInfix, Nx, Min> + ? ParseInfix, Next, Min> : W extends "json" ? ParseInfix< T, IsJsonNode, SkipUniqueKeys< T, - SkipWord, "value" | "scalar" | "array" | "object"> + SkipWord, "value" | "scalar" | "array" | "object"> >, Min > @@ -523,11 +525,11 @@ type ParseIsBody< ? ParseInfix< T, NotIf, Not>, - Nx, + Next, Min > : W extends "nfc" | "nfd" | "nfkc" | "nfkd" - ? T[Nx] extends WordTok<"normalized"> + ? T[Next] extends WordTok<"normalized"> ? ParseInfix< T, NotIf< @@ -537,10 +539,10 @@ type ParseIsBody< Nx2, Min > - : Expected<"NORMALIZED", T, Nx> + : Expected<"NORMALIZED", T, Next> : W extends "distinct" - ? EatWord, "from"> extends never - ? Expected<"FROM", T, Nx> + ? EatWord, "from"> extends never + ? Expected<"FROM", T, Next> : ParseExpr, 5> extends Ok ? NonAssoc, J, Min, 4> : ParseExpr, 5> @@ -561,15 +563,15 @@ type ParseNegated< > = T[I] extends WordTok ? W extends "in" - ? ParseIn, Min, true> + ? ParseIn, Min, true> : W extends "like" | "ilike" - ? ParseLike, Min, W, true> + ? ParseLike, Min, W, true> : W extends "similar" - ? EatWord, "to"> extends never - ? Expected<"TO", T, Nx> + ? EatWord, "to"> extends never + ? Expected<"TO", T, Next> : ParseLike, Min, "similar", true> : W extends "between" - ? ParseBetween, Min, true> + ? ParseBetween, Min, true> : Ok : Ok; @@ -582,15 +584,15 @@ type ParseIn< > = EatOp extends never ? Expected<'"("', T, I> - : IsSelectStart> extends true - ? ParseSelectStmt> extends Ok + : IsSelectStart> extends true + ? ParseSelectStmt> extends Ok ? EatOp extends never - ? T[Nx] extends OpTok<"("> - ? ParseInList, Min, Not> + ? T[Next] extends OpTok<"("> + ? ParseInList, Min, Not> : Expected<'")"', T, J> - : ParseInfix, Nx, Min> - : ParseSelectStmt> - : ParseInList, Min, Not>; + : ParseInfix, Next, Min> + : ParseSelectStmt> + : ParseInList, Min, Not>; type ParseInList< T extends Toks, L, @@ -601,7 +603,7 @@ type ParseInList< ParseExprList extends Ok ? EatOp extends never ? Expected<'")"', T, J> - : ParseInfix, Nx, Min> + : ParseInfix, Next, Min> : ParseExprList; type ParseLike< @@ -615,7 +617,7 @@ type ParseLike< T[I] extends WordTok ? Kind extends "similar" ? SyntaxErrorAt - : T[Nx] extends OpTok<"("> + : T[Next] extends OpTok<"("> ? ParseQuantified< T, L, @@ -624,13 +626,13 @@ type ParseLike< Nx2, Min > - : SyntaxErrorAt> + : SyntaxErrorAt> : ParseExpr extends Ok ? EatWord extends never ? NonAssoc, J, Min, 6> - : ParseExpr, 7> extends Ok + : ParseExpr, 7> extends Ok ? NonAssoc, J2, Min, 6> - : ParseExpr, 7> + : ParseExpr, 7> : ParseExpr; type LikeOp = Kind extends "ilike" @@ -654,14 +656,14 @@ type ParseBetween< > ? EatWord extends never ? Expected<"AND", T, J> - : ParseExpr, 7> extends Ok + : ParseExpr, 7> extends Ok ? NonAssoc, J2, Min, 6> - : ParseExpr, 7> + : ParseExpr, 7> : ParseExpr, 7>; type ParseAtTimeZone = T[I] extends WordTok<"local"> - ? ParseInfix, Nx, Min> + ? ParseInfix, Next, Min> : EatWords extends never ? Expected<"TIME ZONE or LOCAL", T, I> : ParseExpr, 11> extends Ok @@ -714,18 +716,21 @@ type ParseOperatorSyntax< I0 extends number, > = T[I] extends OpTok<"("> - ? QualOp> extends [infer O extends string, infer J extends number] + ? QualOp> extends [ + infer O extends string, + infer J extends number, + ] ? BinRhs - : SyntaxErrorAt>, number>> + : SyntaxErrorAt>, number>> : Ok; type QualOp = T[I] extends OpTok - ? T[Nx] extends OpTok<")"> + ? T[Next] extends OpTok<")"> ? [O, Nx2] - : Nx - : T[Nx] extends OpTok<"."> + : Next + : T[Next] extends OpTok<"."> ? QualOp> - : Nx; + : Next; type ParseQualPrefix = QualOp extends [infer O, infer J extends number] ? ParseExpr extends Ok @@ -758,23 +763,23 @@ type PrefixKeyword = type ParsePrefix = T[I] extends NumTok - ? Ok, Nx> + ? Ok, Next> : T[I] extends StrTok ? T[I] extends { readonly bits: true } - ? Ok<{ t: "typed"; type: "bit"; v: V; bits: true }, Nx> - : Ok, Nx> + ? Ok<{ t: "typed"; type: "bit"; v: V; bits: true }, Next> + : Ok, Next> : T[I] extends ParamTok - ? Ok, Nx> + ? Ok, Next> : T[I] extends OpTok ? O extends "(" - ? ParseParen> + ? ParseParen> : O extends "*" - ? Ok, Nx> + ? Ok, Next> : O extends "[" - ? ParseArrayLiteral> + ? ParseArrayLiteral> : O extends NotPrefixOp ? SyntaxErrorAt - : ParseUnary> + : ParseUnary> : T[I] extends IdTok ? ParseNameExpr : T[I] extends WordTok @@ -786,29 +791,29 @@ type ParseKeywordPrefix< I extends number, W extends string, > = W extends ParenFormWord - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParenForms>[W] : ParseNameExpr : W extends keyof PrefixWords - ? Ok> + ? Ok> : W extends "not" - ? ParseUnary> + ? ParseUnary> : W extends "case" - ? ParseCase> + ? ParseCase> : W extends "cast" - ? ParseCast> + ? ParseCast> : W extends "array" - ? ParseArray> + ? ParseArray> : W extends SpecialValueFunction ? ParseSpecialValue : W extends XmlFuncKeyword - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseXmlFunc> : ParseNameExpr : W extends ConstTypeWord ? ParseConstTypeLit : W extends "collation" - ? T[Nx] extends WordTok<"for"> + ? T[Next] extends WordTok<"for"> ? ParseCollationFor> : ParseNameExpr : W extends ReservedKeyword @@ -854,38 +859,38 @@ type ParseConstTypeLit = ? ParseNameExpr : Ty extends "interval" ? ParseIntervalLit - : Ok, Nx> + : Ok, Next> : ParseNameExpr : ParseNameExpr; type ParseIntervalLit = - T[Nx] extends OpTok<"("> - ? Ok, Nx> - : Nx extends J - ? IntervalFields> extends infer K extends number + T[Next] extends OpTok<"("> + ? Ok, Next> + : Next extends J + ? IntervalFields> extends infer K extends number ? Ok, K> - : IntervalFields> - : SyntaxErrorAt>; + : IntervalFields> + : SyntaxErrorAt>; type ParseCollationFor = EatOp extends never ? Expected<'"("', T, I> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0>; + : ParseExpr, 0>; type ParseNormalize = ParseExpr extends Ok ? T[J] extends OpTok<")"> - ? Ok, Nx> + ? Ok, Next> : T[J] extends OpTok<","> - ? T[Nx] extends WordTok< + ? T[Next] extends WordTok< infer F extends "nfc" | "nfd" | "nfkc" | "nfkd" > ? T[Nx2] extends OpTok<")"> ? Ok>]>, Nx3> : Expected<'")"', T, Nx2> - : Expected<"NFC, NFD, NFKC or NFKD", T, Nx> + : Expected<"NFC, NFD, NFKC or NFKD", T, Next> : Expected<'")"', T, J> : ParseExpr; @@ -910,15 +915,15 @@ type PrecisionValueFunction = | "localtimestamp"; type ParseSpecialValue = - T[Nx] extends OpTok<"("> + T[Next] extends OpTok<"("> ? W extends "current_schema" ? ParseNameExpr : W extends PrecisionValueFunction ? ParsePrecision> extends infer J extends number ? SpecialValue : ParsePrecision> - : SyntaxErrorAt> - : SpecialValue>; + : SyntaxErrorAt> + : SpecialValue>; type SpecialValue = Ok< CallNode, @@ -944,7 +949,7 @@ type ParseParen = T[I] extends OpTok<"("> ? ParseParenExpr : Expected<'")"', T, J> - : Ok, Nx> + : Ok, Next> : ParseSelectStmt : ParseParenExpr; type ParseParenExpr = @@ -952,18 +957,18 @@ type ParseParenExpr = ? EatOp extends never ? EatOp extends never ? Expected<'")"', T, J> - : ParseExprList, [E]> extends Ok + : ParseExprList, [E]> extends Ok ? Close> - : ParseExprList, [E]> - : T[Nx] extends OpTok<"."> + : ParseExprList, [E]> + : T[Next] extends OpTok<"."> ? ParseParenField> - : T[Nx] extends OpTok<"["> + : T[Next] extends OpTok<"["> ? ParseSubscript, 13> - : Ok> + : Ok> : ParseExpr; type ParseParenField = T[I] extends OpTok<"*"> - ? Ok, Nx> + ? Ok, Next> : AnyLabel extends [infer N, infer J extends number] ? Ok, J> : Expected<"field name", T, I>; @@ -983,9 +988,9 @@ type ColumnPrefix = H extends IdTok | WordTok : null; type ParseNameExpr = - ColumnPrefix], T[Nx2], T[Nx3]> extends infer R + ColumnPrefix], T[Nx2], T[Nx3]> extends infer R ? R extends [infer E, infer N] - ? Ok : Nx3> + ? Ok : Nx3> : ParseNameExprSlow : never; @@ -996,20 +1001,20 @@ type ParseNameExprSlow = ] ? T[J] extends OpTok<"("> ? T[I] extends WordTok - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseJsonFunc - : ParseCall, Nx> - : ParseCall, Nx> + : ParseCall, Next> + : ParseCall, Next> : T[J] extends OpTok<"."> - ? T[Nx] extends OpTok<"*"> + ? T[Next] extends OpTok<"*"> ? Ok>, Nx2> - : SyntaxErrorAt> + : SyntaxErrorAt> : T[J] extends StrTok ? Parts["length"] extends 1 | 2 - ? Ok>, V>, Nx> + ? Ok>, V>, Next> : SyntaxErrorAt : Ok, J> - : SyntaxErrorAt>; + : SyntaxErrorAt>; export type LastOf = Parts extends readonly [ ...string[], @@ -1022,14 +1027,14 @@ type ColRefOf = export type ParseCall = T[I] extends OpTok<")"> - ? ParseCallSuffix> + ? ParseCallSuffix> : T[I] extends OpTok<"*"> - ? T[Nx] extends OpTok<")"> + ? T[Next] extends OpTok<")"> ? ParseCallSuffix], false, Nx2> - : SyntaxErrorAt> + : SyntaxErrorAt> : EatWord extends never ? ParseCallArgs, false> - : ParseCallArgs, true>; + : ParseCallArgs, true>; type ParseCallArgs< T extends Toks, @@ -1041,7 +1046,7 @@ type ParseCallArgs< ? SkipAggOrderBy extends infer J2 extends number ? EatOp extends never ? Expected<'")"', T, J2> - : ParseCallSuffix> + : ParseCallSuffix> : SkipAggOrderBy : ParseArgList; @@ -1052,15 +1057,15 @@ type ParseArgList< Names extends string, > = T[I] extends WordTok<"variadic"> - ? ParseArg, Names> extends Ok + ? ParseArg, Names> extends Ok ? Ok<[...Acc, VariadicNode], J> - : ParseArg, Names> + : ParseArg, Names> : ParseArg extends Ok ? EatOp extends never ? Ok<[...Acc, E], J> : ParseArgList< T, - Nx, + Next, [...Acc, E], | Names | (E extends { @@ -1072,7 +1077,7 @@ type ParseArgList< > : ParseArg; type ParseArg = - T[Nx] extends OpTok<"=>" | ":="> + T[Next] extends OpTok<"=>" | ":="> ? ArgName extends infer N extends string ? N extends Names ? Fail<`argument name "${N}" used more than once`, I> @@ -1081,7 +1086,7 @@ type ParseArg = : ParseExpr, 0> : T[I] extends WordTok ? SyntaxErrorAt - : SyntaxErrorAt> + : SyntaxErrorAt> : [Names] extends [never] ? ParseExpr : ParseExpr extends Ok @@ -1156,8 +1161,8 @@ type ParseWithinGroup = export type ParseFilter = EatWord extends never ? Ok - : EatOp, "("> extends never - ? Expected<'"("', T, Nx> + : EatOp, "("> extends never + ? Expected<'"("', T, Next> : EatWord, "where"> extends never ? Expected<"WHERE", T, Nx2> : ParseExpr, 0> extends Ok @@ -1167,11 +1172,11 @@ export type ParseFilter = export type ParseOver = EatWord extends never ? Ok - : T[Nx] extends OpTok<"("> + : T[Next] extends OpTok<"("> ? ParseWindowSpec> - : T[Nx] extends WordTok | IdTok + : T[Next] extends WordTok | IdTok ? Ok<{ name: W; partition: []; order: [] }, Nx2> - : Expected<"window specification", T, Nx>; + : Expected<"window specification", T, Next>; export type ParseWindowSpec = WindowBaseName extends [infer Name, infer I1 extends number] @@ -1201,11 +1206,11 @@ export type ParseWindowSpec = : never; type WindowBaseName = T[I] extends IdTok - ? [V, Nx] + ? [V, Next] : T[I] extends WordTok ? W extends "partition" | "order" | "rows" | "range" | "groups" ? [null, I] - : [W, Nx] + : [W, Next] : [null, I]; type ParseWindowPartition = EatWords extends never @@ -1223,8 +1228,8 @@ type FrameInfo = { readonly kinds: unknown; readonly empty: boolean }; type ParseFrame = EatWord extends never ? Ok<{ kinds: never; empty: false }, I> - : EatWord, "between"> extends never - ? ParseFrameBound> extends readonly [ + : EatWord, "between"> extends never + ? ParseFrameBound> extends readonly [ infer S, infer J extends number, ] @@ -1236,14 +1241,14 @@ type ParseFrame = I > : FrameExclusion - : ParseFrameBound> + : ParseFrameBound> : ParseFrameBound> extends readonly [ infer S, infer J extends number, ] ? EatWord extends never ? Expected<"AND", T, J> - : ParseFrameBound> extends readonly [ + : ParseFrameBound> extends readonly [ infer E, infer J2 extends number, ] @@ -1259,7 +1264,7 @@ type ParseFrame = : false, J2 > - : ParseFrameBound> + : ParseFrameBound> : ParseFrameBound>; type FrameExclusion< T extends Toks, @@ -1275,8 +1280,8 @@ type FrameExclusion< ? true : EatWord extends never ? false - : EatWords, ["current", "row"]> extends never - ? EatWord, "group"> extends never + : EatWords, ["current", "row"]> extends never + ? EatWord, "group"> extends never ? false : true : true; @@ -1315,7 +1320,7 @@ type ParseFrameBound = ? EatWords extends never ? ParseExpr extends Ok ? T[J] extends WordTok - ? [D, Nx] + ? [D, Next] : Expected<"PRECEDING or FOLLOWING", T, J> : ParseExpr : ["current row", Nx2] @@ -1324,10 +1329,10 @@ type ParseFrameBound = type SkipExclude = EatWord extends never ? I - : EatWords, ["current", "row"]> extends never - ? EatWords, ["no", "others"]> extends never - ? EatWord, "group" | "ties"> extends never - ? Expected<"CURRENT ROW, GROUP, TIES or NO OTHERS", T, Nx> + : EatWords, ["current", "row"]> extends never + ? EatWords, ["no", "others"]> extends never + ? EatWord, "group" | "ties"> extends never + ? Expected<"CURRENT ROW, GROUP, TIES or NO OTHERS", T, Next> : Nx2 : Nx3 : Nx3; @@ -1348,30 +1353,30 @@ type ParseCaseWhens< ? EatWord extends never ? EatWord extends never ? Expected<"WHEN, ELSE or END", T, I> - : Ok, Nx> - : ParseExpr, 0> extends Ok + : Ok, Next> + : ParseExpr, 0> extends Ok ? EatWord extends never ? Expected<"END", T, J> - : Ok, Nx> - : ParseExpr, 0> - : ParseExpr, 0> extends Ok + : Ok, Next> + : ParseExpr, 0> + : ParseExpr, 0> extends Ok ? EatWord extends never ? Expected<"THEN", T, J> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? ParseCaseWhens - : ParseExpr, 0> - : ParseExpr, 0>; + : ParseExpr, 0> + : ParseExpr, 0>; type ParseCast = EatOp extends never ? Expected<'"("', T, I> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? EatWord extends never ? Expected<"AS", T, J> - : ParseTypeName> extends Ok + : ParseTypeName> extends Ok ? Close> - : ParseTypeName> - : ParseExpr, 0>; + : ParseTypeName> + : ParseExpr, 0>; type ParseExists = ParseSelectStmt extends Ok @@ -1382,29 +1387,29 @@ type ParseArray = EatOp extends never ? EatOp extends never ? Expected<'"[" or "("', T, I> - : ParseSelectStmt> extends Ok + : ParseSelectStmt> extends Ok ? Close> - : ParseSelectStmt> - : ParseArrayLiteral>; + : ParseSelectStmt> + : ParseArrayLiteral>; type ParseArrayLiteral = EatOp extends never ? ParseExprList extends Ok ? Close, "]"> : ParseExprList - : Ok, Nx>; + : Ok, Next>; type ParseRow = EatOp extends never ? ParseExprList extends Ok ? Close> : ParseExprList - : Ok, Nx>; + : Ok, Next>; type ParseExtract = T[I] extends | WordTok | StrTok - ? EatWord, "from"> extends never - ? Expected<"FROM", T, Nx> + ? EatWord, "from"> extends never + ? Expected<"FROM", T, Next> : ParseExpr, 0> extends Ok ? Close> : ParseExpr, 0> @@ -1414,16 +1419,16 @@ type ParsePosition = ParseExpr extends Ok ? EatWord extends never ? Expected<"IN", T, J> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0> + : ParseExpr, 0> : ParseExpr; type ParseSubstring = ParseExpr extends Ok ? EatOp extends never ? ParseSubstringFromFor - : ParseExprList, []> extends Ok< + : ParseExprList, []> extends Ok< readonly [infer From, ...infer More], infer J2 > @@ -1434,40 +1439,40 @@ type ParseSubstring = More extends readonly [infer For, ...unknown[]] ? For : null, J2 > - : ParseExprList, []> + : ParseExprList, []> : ParseExpr; type ParseSubstringFromFor = EatWord extends never ? EatWord extends never ? EatWord extends never ? CloseSubstring - : SubstringOperands, "escape"> extends Ok< + : SubstringOperands, "escape"> extends Ok< { a: infer P; b: infer C }, infer J > ? C extends null ? Expected<"ESCAPE", T, J> : CloseSubstring - : SubstringOperands, "escape"> - : SubstringOperands, "from"> extends Ok< + : SubstringOperands, "escape"> + : SubstringOperands, "from"> extends Ok< { a: infer For; b: infer From }, infer J > ? CloseSubstring - : SubstringOperands, "from"> - : SubstringOperands, "for"> extends Ok< + : SubstringOperands, "from"> + : SubstringOperands, "for"> extends Ok< { a: infer From; b: infer For }, infer J > ? CloseSubstring - : SubstringOperands, "for">; + : SubstringOperands, "for">; type SubstringOperands = ParseExpr extends Ok ? EatWord extends never ? Ok<{ a: A; b: null }, J> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok<{ a: A; b: B }, J2> - : ParseExpr, 0> + : ParseExpr, 0> : ParseExpr; type CloseSubstring = Close< T, @@ -1477,7 +1482,7 @@ type CloseSubstring = Close< type ParseTrim = T[I] extends WordTok - ? ParseTrimBody, Side> + ? ParseTrimBody, Side> : ParseTrimBody; type ParseTrimBody = EatWord extends never @@ -1485,32 +1490,32 @@ type ParseTrimBody = ? EatWord extends never ? EatOp extends never ? Close> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> + : ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0> + : ParseExpr, 0> : ParseExpr - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0>; + : ParseExpr, 0>; type ParseOverlay = ParseExpr extends Ok ? EatWord extends never ? Expected<"PLACING", T, J> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? EatWord extends never ? Expected<"FROM", T, J2> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? EatWord extends never ? CloseOverlay - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? CloseOverlay - : ParseExpr, 0> - : ParseExpr, 0> - : ParseExpr, 0> + : ParseExpr, 0> + : ParseExpr, 0> + : ParseExpr, 0> : ParseExpr; type CloseOverlay = Close< T, @@ -1526,7 +1531,7 @@ export type ParseExprList< ParseExpr extends Ok ? EatOp extends never ? Ok<[...Acc, E], J> - : ParseExprList, [...Acc, E]> + : ParseExprList, [...Acc, E]> : ParseExpr; type ParseOrderList< @@ -1537,16 +1542,16 @@ type ParseOrderList< ParseExpr extends Ok ? EatOp, ","> extends never ? Ok<[...Acc, E], SkipOrderTail> - : ParseOrderList>, [...Acc, E]> + : ParseOrderList>, [...Acc, E]> : ParseExpr; type SkipOrderTail = SkipFirst< T, EatWord extends never ? T[I] extends WordTok<"using"> - ? T[Nx] extends OpTok + ? T[Next] extends OpTok ? Nx2 : I : I - : Nx, + : Next, [["nulls", "first"], ["nulls", "last"]] >; diff --git a/packages/core/src/postgres/engine/parser/json.ts b/packages/core/src/postgres/engine/parser/json.ts index 81d889b9..e0fbff21 100644 --- a/packages/core/src/postgres/engine/parser/json.ts +++ b/packages/core/src/postgres/engine/parser/json.ts @@ -1,12 +1,8 @@ -import type { - IdTok, - OpTok, - StrTok, - Toks, - WordTok, -} from "../../../engine/tokens.js"; -import type { PgScalarName } from "../../schema/pg-types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; +import type { JsonT, KnownType } from "../../schema/type-facts.js"; import type { Tag } from "../ast.js"; +import type { IdTok, OpTok, StrTok, WordTok } from "../lexer/tokens.js"; import type { CallNode, ParseCall, @@ -26,7 +22,6 @@ import type { Expected, Fail, Ident, - Nx, Nx2, Nx3, Ok, @@ -63,7 +58,7 @@ export type ParseJsonFunc< ? ParseArrayAgg> : W extends "json" | "json_scalar" | "json_serialize" ? ParseUnaryFn> - : SyntaxErrorAt>; + : SyntaxErrorAt>; type Fn< F extends string, @@ -92,7 +87,7 @@ type EatFormat = SkipEncoding< >; type SkipEncoding = T[I] extends WordTok<"encoding"> - ? T[Nx] extends WordTok | IdTok + ? T[Next] extends WordTok | IdTok ? Nx2 : I : I; @@ -111,13 +106,13 @@ type ParseJsonValue = type ParseReturning = EatWord extends never ? Ok - : ParseTypeName> extends Ok + : ParseTypeName> extends Ok ? Ok> - : ParseTypeName>; + : ParseTypeName>; export type SkipUniqueKeys = T[I] extends WordTok<"with" | "without"> - ? T[Nx] extends WordTok<"unique"> + ? T[Next] extends WordTok<"unique"> ? SkipWord, "keys"> : I : I; @@ -133,17 +128,17 @@ type SkipWrapper = T[I] extends WordTok<"with" | "without"> ? SkipWord< T, - SkipWord, "conditional" | "unconditional">, + SkipWord, "conditional" | "unconditional">, "array" > extends infer J extends number ? T[J] extends WordTok<"wrapper"> - ? Nx + ? Next : I : never : I; type SkipQuotes = T[I] extends WordTok<"keep" | "omit"> - ? T[Nx] extends WordTok<"quotes"> + ? T[Next] extends WordTok<"quotes"> ? EatWords, ["on", "scalar", "string"]> extends never ? Nx2 : Nx3> @@ -153,7 +148,7 @@ type SkipQuotes = type ParsePassing = EatWord extends never ? Ok<[], I> - : ParsePassingList, []>; + : ParsePassingList, []>; type ParsePassingList< T extends Toks, I extends number, @@ -162,25 +157,25 @@ type ParsePassingList< ParseJsonValue extends Ok ? EatWord extends never ? Expected<"AS", T, J> - : AnyLabel> extends [string, infer J2 extends number] + : AnyLabel> extends [string, infer J2 extends number] ? EatOp extends never ? Ok<[...Acc, E], J2> - : ParsePassingList, [...Acc, E]> - : Expected<"name", T, Nx> + : ParsePassingList, [...Acc, E]> + : Expected<"name", T, Next> : ParseJsonValue; type ParseBehavior = T[I] extends WordTok ? W extends "default" - ? ParseExpr, 0> extends Ok + ? ParseExpr, 0> extends Ok ? Ok<{ k: "default"; e: E; at: I }, J> - : ParseExpr, 0> + : ParseExpr, 0> : W extends "error" | "null" | "true" | "false" | "unknown" - ? Ok<{ k: W; at: I }, Nx> + ? Ok<{ k: W; at: I }, Next> : W extends "empty" - ? T[Nx] extends WordTok<"object"> + ? T[Next] extends WordTok<"object"> ? Ok<{ k: "emptyobj"; at: I }, Nx2> - : Ok<{ k: "empty"; at: I }, SkipWord, "array">> + : Ok<{ k: "empty"; at: I }, SkipWord, "array">> : null : null; @@ -206,7 +201,7 @@ type ParseBehaviors< J > : Empty extends false - ? SyntaxErrorAt> + ? SyntaxErrorAt> : ParseOnError, B1, Allowed, Of> : CheckBehaviors<[null, B1], Nx2, Allowed, Of> : B extends null @@ -269,7 +264,7 @@ type CheckDefault = B extends { } ? Fail< "can only specify a constant, non-aggregate function, or operator expression for DEFAULT", - Nx + Next > : R; @@ -297,12 +292,12 @@ type ParseUnaryFn< type ParseObject = T[I] extends OpTok<")"> - ? Ok, Nx> + ? Ok, Next> : T[I] extends WordTok<"returning"> ? CtorOptions : ParseExpr extends Ok ? T[J] extends OpTok<":"> | WordTok<"value"> - ? ParsePairValue> + ? ParsePairValue> : ParseCall : ParseExpr; type ParsePairs< @@ -312,7 +307,7 @@ type ParsePairs< > = ParseExpr extends Ok ? T[J] extends OpTok<":"> | WordTok<"value"> - ? ParsePairValue> + ? ParsePairValue> : Expected<'":" or VALUE', T, J> : ParseExpr; type ParsePairValue< @@ -323,7 +318,7 @@ type ParsePairValue< ParseJsonValue extends Ok ? EatOp extends never ? CtorOptions - : ParsePairs> + : ParsePairs> : ParseJsonValue; type CtorOptions< T extends Toks, @@ -345,7 +340,7 @@ type CtorOptions< type ParseArrayCtor = T[I] extends OpTok<")"> - ? Ok, Nx> + ? Ok, Next> : T[I] extends WordTok<"returning"> ? CtorOptions : T[I] extends WordTok<"select" | "with" | "values" | "table"> @@ -363,14 +358,14 @@ type ParseArrayValues< ParseJsonValue extends Ok ? EatOp extends never ? CtorOptions - : ParseArrayValues, [...Acc, V]> + : ParseArrayValues, [...Acc, V]> : ParseJsonValue; type ParseObjectAgg = ParseExpr extends Ok ? T[J] extends OpTok<":"> | WordTok<"value"> - ? ParseJsonValue> extends Ok + ? ParseJsonValue> extends Ok ? AggTail - : ParseJsonValue> + : ParseJsonValue> : Expected<'":" or VALUE', T, J> : ParseExpr; @@ -402,7 +397,7 @@ type AggTail< `${AggBase}_${W extends "json_objectagg" ? "object_agg" : "agg"}${Absent extends true ? "_strict" : ""}`, Args, Ret, - Nx + Next > : ParseReturning : J> : never; @@ -458,9 +453,9 @@ type ParseCtxPath = ParseJsonValue extends Ok ? EatOp extends never ? Expected<'","', T, J> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Ok<[Cx, Path], J2> - : ParseExpr, 0> + : ParseExpr, 0> : ParseJsonValue; type QueryTail< T extends Toks, @@ -549,7 +544,7 @@ export type ParseJsonTable = : ParseCtxPath; type PathName = T[I] extends WordTok<"as"> - ? Ident> extends [infer N, infer J] + ? Ident> extends [infer N, infer J] ? [[N], J] : [[], I] : [[], I]; @@ -580,13 +575,13 @@ type AddCol< type ParseColumnsClause = EatWord extends never ? Expected<"COLUMNS", T, I> - : EatOp, "("> extends never - ? Expected<'"("', T, Nx> + : EatOp, "("> extends never + ? Expected<'"("', T, Next> : ParseColumns, S>; type ParseColumns = T[I] extends WordTok<"nested"> - ? T[Nx] extends WordTok<"path"> | StrTok - ? ParseNested, "path">, S> + ? T[Next] extends WordTok<"path"> | StrTok + ? ParseNested, "path">, S> : ParseColumn : ParseColumn; type ColumnsNext = @@ -599,16 +594,16 @@ type ColumnsNext = names: S["names"]; defs: S["defs"]; }, - Nx + Next > - : ParseColumns, S>; + : ParseColumns, S>; type ParseColumn = Ident extends [infer N extends string, infer J extends number] ? EatWords extends never ? ParseTypeName extends Ok ? T[J2] extends WordTok<"exists"> - ? ParsePathOpt> extends infer P extends number + ? ParsePathOpt> extends infer P extends number ? ParseBehaviors< T, P, @@ -620,7 +615,7 @@ type ParseColumn = ? ColumnsNext, N>> : B : never - : ParsePathOpt> + : ParsePathOpt> : ParsePathOpt> extends infer P extends number ? ParseBehaviors< T, @@ -668,31 +663,32 @@ type Col = { n: N; t: { type: Ty; nullable: Nullable }; }; +/** + * A column of a built-in scalar type works as JSON_VALUE; a FORMAT JSON one, + * or one of a json, array or record type, as JSON_QUERY. So does one of a type + * the engine doesn't know, which may be a domain over one of those. + */ type ColumnBehavior< Ty extends string, Formatted extends boolean, > = Formatted extends true ? QueryBehavior - : Ty extends ScalarType + : Ty extends Exclude ? ValueBehavior : QueryBehavior; -type ScalarType = Exclude< - PgScalarName, - "json" | "jsonb" | "record" | "void" | "unknown" | `any${string}` ->; type ParsePathOpt = EatWord extends never ? I - : T[Nx] extends StrTok + : T[Next] extends StrTok ? Nx2 - : Expected<"path string", T, Nx>; + : Expected<"path string", T, Next>; type ParseNested< T extends Toks, I extends number, S extends ColsState, > = T[I] extends StrTok - ? PathName> extends [ + ? PathName> extends [ infer PN extends readonly string[], infer J extends number, ] diff --git a/packages/core/src/postgres/engine/parser/keywords.ts b/packages/core/src/postgres/engine/parser/keywords.ts index c4249fae..e9df71a2 100644 --- a/packages/core/src/postgres/engine/parser/keywords.ts +++ b/packages/core/src/postgres/engine/parser/keywords.ts @@ -1,82 +1,84 @@ -export type ReservedKeyword = - | "all" - | "analyse" - | "analyze" - | "and" - | "any" - | "array" - | "as" - | "asc" - | "asymmetric" - | "both" - | "case" - | "cast" - | "check" - | "collate" - | "column" - | "constraint" - | "create" - | "current_catalog" - | "current_date" - | "current_role" - | "current_time" - | "current_timestamp" - | "current_user" - | "default" - | "deferrable" - | "desc" - | "distinct" - | "do" - | "else" - | "end" - | "except" - | "false" - | "fetch" - | "for" - | "foreign" - | "from" - | "grant" - | "group" - | "having" - | "in" - | "initially" - | "intersect" - | "into" - | "lateral" - | "leading" - | "limit" - | "localtime" - | "localtimestamp" - | "not" - | "null" - | "offset" - | "on" - | "only" - | "or" - | "order" - | "placing" - | "primary" - | "references" - | "returning" - | "select" - | "session_user" - | "some" - | "symmetric" - | "system_user" - | "table" - | "then" - | "to" - | "trailing" - | "true" - | "union" - | "unique" - | "user" - | "using" - | "variadic" - | "when" - | "where" - | "window" - | "with"; +export const RESERVED_KEYWORDS = [ + "all", + "analyse", + "analyze", + "and", + "any", + "array", + "as", + "asc", + "asymmetric", + "both", + "case", + "cast", + "check", + "collate", + "column", + "constraint", + "create", + "current_catalog", + "current_date", + "current_role", + "current_time", + "current_timestamp", + "current_user", + "default", + "deferrable", + "desc", + "distinct", + "do", + "else", + "end", + "except", + "false", + "fetch", + "for", + "foreign", + "from", + "grant", + "group", + "having", + "in", + "initially", + "intersect", + "into", + "lateral", + "leading", + "limit", + "localtime", + "localtimestamp", + "not", + "null", + "offset", + "on", + "only", + "or", + "order", + "placing", + "primary", + "references", + "returning", + "select", + "session_user", + "some", + "symmetric", + "system_user", + "table", + "then", + "to", + "trailing", + "true", + "union", + "unique", + "user", + "using", + "variadic", + "when", + "where", + "window", + "with", +] as const; +export type ReservedKeyword = (typeof RESERVED_KEYWORDS)[number]; type ColNameKeyword = | "between" @@ -143,30 +145,32 @@ type ColNameKeyword = | "xmlserialize" | "xmltable"; -export type TypeFuncNameKeyword = - | "authorization" - | "binary" - | "collation" - | "concurrently" - | "cross" - | "current_schema" - | "freeze" - | "full" - | "ilike" - | "inner" - | "is" - | "isnull" - | "join" - | "left" - | "like" - | "natural" - | "notnull" - | "outer" - | "overlaps" - | "right" - | "similar" - | "tablesample" - | "verbose"; +export const TYPE_FUNC_NAME_KEYWORDS = [ + "authorization", + "binary", + "collation", + "concurrently", + "cross", + "current_schema", + "freeze", + "full", + "ilike", + "inner", + "is", + "isnull", + "join", + "left", + "like", + "natural", + "notnull", + "outer", + "overlaps", + "right", + "similar", + "tablesample", + "verbose", +] as const; +export type TypeFuncNameKeyword = (typeof TYPE_FUNC_NAME_KEYWORDS)[number]; export type NotBareLabel = | "array" diff --git a/packages/core/src/postgres/engine/parser/merge.ts b/packages/core/src/postgres/engine/parser/merge.ts index 592b69a8..19404579 100644 --- a/packages/core/src/postgres/engine/parser/merge.ts +++ b/packages/core/src/postgres/engine/parser/merge.ts @@ -1,5 +1,7 @@ -import type { OpTok, Toks, WordTok } from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { Expr, MergeWhen, WithClause } from "../ast.js"; +import type { OpTok, WordTok } from "../lexer/tokens.js"; import type { ParenTargetList, ParseDmlTarget, @@ -15,7 +17,6 @@ import type { EatWord, EatWords, Expected, - Nx, Nx2, Ok, SkipWords, @@ -29,16 +30,16 @@ export type ParseMerge< > = EatWord extends never ? Expected<"INTO", T, I> - : ParseDmlTarget, never, true> extends Ok< + : ParseDmlTarget, never, true> extends Ok< [infer Ref, infer Alias], infer I1 > ? EatWord extends never ? Expected<"USING", T, I1> - : ParseFromItem> extends Ok + : ParseFromItem> extends Ok ? EatWord extends never ? Expected<"ON", T, I2> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? T[I3] extends WordTok<"when"> ? ParseWhens extends Ok ? ParseReturningOpt extends Ok< @@ -62,9 +63,9 @@ export type ParseMerge< : ParseReturningOpt : ParseWhens : Expected<"WHEN", T, I3> - : ParseExpr, 0> - : ParseFromItem> - : ParseDmlTarget, never, true>; + : ParseExpr, 0> + : ParseFromItem> + : ParseDmlTarget, never, true>; type ParseWhens< T extends Toks, @@ -72,9 +73,9 @@ type ParseWhens< Acc extends readonly MergeWhen[], > = T[I] extends WordTok<"when"> - ? ParseWhen> extends Ok + ? ParseWhen> extends Ok ? ParseWhens - : ParseWhen> + : ParseWhen> : Ok; type ParseWhen = @@ -84,7 +85,7 @@ type ParseWhen = : EatWords, ["by", "source"]> extends never ? ParseWhenCond, ["by", "target"]>, "target"> : ParseWhenCond>, "source"> - : ParseWhenCond, "matched">; + : ParseWhenCond, "matched">; type ParseWhenCond< T extends Toks, I extends number, @@ -92,9 +93,9 @@ type ParseWhenCond< > = EatWord extends never ? ParseThen - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? ParseThen - : ParseExpr, 0>; + : ParseExpr, 0>; type ParseThen< T extends Toks, @@ -104,9 +105,9 @@ type ParseThen< > = EatWord extends never ? Expected<"THEN", T, I> - : ParseAction, M> extends Ok + : ParseAction, M> extends Ok ? Ok<{ match: M; cond: Cond; action: A }, J> - : ParseAction, M>; + : ParseAction, M>; type ParseAction< T extends Toks, @@ -117,20 +118,20 @@ type ParseAction< ? M extends "target" ? EatWord extends never ? T[I] extends WordTok<"do"> - ? SyntaxErrorAt> + ? SyntaxErrorAt> : Expected<"INSERT or DO NOTHING", T, I> - : ParseMergeInsert> + : ParseMergeInsert> : EatWord extends never ? EatWords extends never ? T[I] extends WordTok<"update" | "do"> - ? SyntaxErrorAt> + ? SyntaxErrorAt> : Expected<"UPDATE, DELETE or DO NOTHING", T, I> : ParseSetList, []> extends infer X ? X extends Ok ? Ok<{ t: "update"; set: Set }, J> : X : never - : Ok<{ t: "delete" }, Nx> + : Ok<{ t: "delete" }, Next> : Ok<{ t: "nothing" }, Nx2>; type ParseMergeInsert = @@ -144,8 +145,8 @@ type ParseMergeInsert = ? ParseOverriding extends Ok ? EatWord extends never ? Expected<"VALUES", T, J2> - : EatOp, "("> extends never - ? Expected<'"("', T, Nx> + : EatOp, "("> extends never + ? Expected<'"("', T, Next> : ParseExprList, []> extends Ok ? Close< T, diff --git a/packages/core/src/postgres/engine/parser/select.ts b/packages/core/src/postgres/engine/parser/select.ts index ec6c45ac..373b7102 100644 --- a/packages/core/src/postgres/engine/parser/select.ts +++ b/packages/core/src/postgres/engine/parser/select.ts @@ -1,11 +1,7 @@ -import type { - IdTok, - OpTok, - StrTok, - Toks, - WordTok, -} from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { Expr, JoinKind, Tag } from "../ast.js"; +import type { IdTok, OpTok, StrTok, WordTok } from "../lexer/tokens.js"; import type { ColNode, ParseExpr, @@ -28,7 +24,6 @@ import type { Fail, FuncName, Ident, - Nx, Nx2, Nx3, Ok, @@ -128,15 +123,15 @@ export type IsSelectStart = T[I] extends WordTok<"select" | "with" | "values" | "table"> ? true : T[I] extends OpTok<"("> - ? IsSelectStart> + ? IsSelectStart> : false; export type ParseSelectStmt = EatWord extends never ? ParseSelectAfterWith - : ParseWith> extends Ok + : ParseWith> extends Ok ? ParseSelectAfterWith - : ParseWith>; + : ParseWith>; export type ParseSelectAfterWith = ParseSelectBody extends infer B @@ -237,7 +232,7 @@ type ParseLimitOffset< T[I] extends WordTok<"offset"> ? "offset" extends Seen ? SyntaxErrorAt - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? ParseLimitOffset< T, SkipWord, @@ -246,13 +241,13 @@ type ParseLimitOffset< Ties, Seen | "offset" > - : ParseExpr, 0> + : ParseExpr, 0> : T[I] extends WordTok<"limit" | "fetch"> ? "limit" extends Seen ? SyntaxErrorAt : ( T[I] extends WordTok<"fetch"> - ? ParseFetch> + ? ParseFetch> : ParseLimit ) extends infer X ? X extends Ok<[infer E, infer Ti], infer J> @@ -261,8 +256,8 @@ type ParseLimitOffset< : never : Ok<[Lim, Off, Ties], I>; type ParseLimit = ( - EatWord, "all"> extends never - ? ParseExpr, 0> + EatWord, "all"> extends never + ? ParseExpr, 0> : Ok> ) extends infer X ? X extends Ok @@ -275,18 +270,18 @@ type ParseFetch = EatWord extends never ? Expected<"FIRST or NEXT", T, I> : ( - EatWord, "row" | "rows"> extends never - ? ParseExpr, 0> - : Ok<{ t: "num"; v: "1" }, Nx> + EatWord, "row" | "rows"> extends never + ? ParseExpr, 0> + : Ok<{ t: "num"; v: "1" }, Next> ) extends infer X ? X extends Ok ? EatWord extends never ? SyntaxErrorAt - : T[Nx] extends WordTok<"only"> + : T[Next] extends WordTok<"only"> ? Ok<[E, null], Nx2> - : EatWords, ["with", "ties"]> extends never - ? SyntaxErrorAt> - : Ok<[E, Nx], Nx3> + : EatWords, ["with", "ties"]> extends never + ? SyntaxErrorAt> + : Ok<[E, Next], Nx3> : X : never; @@ -299,23 +294,24 @@ type LockingClause = type SkipLocking = EatWord extends never ? I - : SkipLockStrength> extends Nx + : SkipLockStrength> extends Next ? I - : SkipLockOf>> extends infer J extends number + : SkipLockOf>> extends infer J extends + number ? SkipLocking> - : SkipLockOf>>; + : SkipLockOf>>; type SkipLockStrength = SkipFirst< T, I, [["update"], ["share"], ["no", "key", "update"], ["key", "share"]] >; type SkipLockOf = - EatWord extends never ? I : SkipNameList>; + EatWord extends never ? I : SkipNameList>; type SkipNameList = QualName extends [unknown, infer J extends number] ? EatOp extends never ? J - : SkipNameList> + : SkipNameList> : SyntaxErrorAt; type ParseSelectBody = ParseIntersectChain extends infer X @@ -328,14 +324,14 @@ type ParseUnionLoop = T[I] extends WordTok ? ParseIntersectChain< T, - SkipWord, "all" | "distinct"> + SkipWord, "all" | "distinct"> > extends infer X ? X extends Ok ? ParseUnionLoop< T, SetNode< Op, - EatWord, "all"> extends never ? false : true, + EatWord, "all"> extends never ? false : true, L, R >, @@ -354,7 +350,7 @@ type ParseIntersectChain = type ParseIntersectLoop = T[I] extends WordTok<"intersect"> - ? ParseSetOperand, "all" | "distinct">> extends Ok< + ? ParseSetOperand, "all" | "distinct">> extends Ok< infer R, infer J > @@ -362,26 +358,26 @@ type ParseIntersectLoop = T, SetNode< "intersect", - EatWord, "all"> extends never ? false : true, + EatWord, "all"> extends never ? false : true, L, R >, J > - : ParseSetOperand, "all" | "distinct">> + : ParseSetOperand, "all" | "distinct">> : Ok; type ParseSetOperand = T[I] extends OpTok<"("> - ? ParseSelectStmt> extends Ok + ? ParseSelectStmt> extends Ok ? Close> - : ParseSelectStmt> + : ParseSelectStmt> : T[I] extends WordTok<"select"> - ? ParseSimpleSelect> + ? ParseSimpleSelect> : T[I] extends WordTok<"values"> - ? ParseValues> + ? ParseValues> : T[I] extends WordTok<"table"> - ? ParseTableShorthand> + ? ParseTableShorthand> : Expected<"SELECT, VALUES, TABLE or (", T, I>; type ParseTableShorthand = @@ -405,7 +401,7 @@ type ParseTableShorthand = : TableRelation; type TableRelation = T[I] extends WordTok<"only"> - ? OnlyRelation> + ? OnlyRelation> : QualName extends [infer Parts, infer J extends number] ? [Parts, SkipInheritanceStar] : Expected<"table name", T, I>; @@ -417,13 +413,13 @@ export type ParseValues< > = EatOp extends never ? Expected<'"("', T, I> - : ParseExprList, []> extends Ok + : ParseExprList, []> extends Ok ? EatOp extends never ? Expected<'")"', T, J> - : EatOp, ","> extends never - ? Ok, Nx> + : EatOp, ","> extends never + ? Ok, Next> : ParseValues, [...Acc, Row]> - : ParseExprList, []>; + : ParseExprList, []>; type ParseSimpleSelect = ParseDistinct extends Ok @@ -515,7 +511,7 @@ type FromAfterInto = : ParseFromOpt; type IntoEnd = - SkipWord>, "table"> extends infer K extends + SkipWord>, "table"> extends infer K extends number ? QualName extends [unknown, infer J] ? J @@ -523,11 +519,11 @@ type IntoEnd = : never; export type IntoName = SkipWord< T, - SkipIntoPersistence>, + SkipIntoPersistence>, "table" >; type IntoRef = - QualName>, "table">> extends [ + QualName>, "table">> extends [ infer Parts extends readonly string[], number, ] @@ -535,15 +531,15 @@ type IntoRef = : null; type SkipIntoPersistence = T[I] extends WordTok<"local" | "global"> - ? T[Nx] extends WordTok<"temporary" | "temp"> + ? T[Next] extends WordTok<"temporary" | "temp"> ? Nx2 : I : T[I] extends WordTok<"temporary" | "temp" | "unlogged"> - ? T[Nx] extends WordTok<"table"> - ? Nx - : QualName> extends null + ? T[Next] extends WordTok<"table"> + ? Next + : QualName> extends null ? I - : Nx + : Next : I; type ParseDistinct = @@ -551,13 +547,13 @@ type ParseDistinct = ? EatWord extends never ? Ok : EatWords extends never - ? Ok> + ? Ok> : EatOp, "("> extends never ? Expected<'"("', T, Nx2> : ParseExprList, []> extends Ok ? Close : ParseExprList, []> - : Ok>; + : Ok>; type TargetEndWord = | "from" @@ -596,9 +592,9 @@ export type ParseTargetList< type TryColumnTarget = Ident extends [infer Name, infer J extends number] ? T[J] extends OpTok<"."> - ? AnyLabel> extends [infer Col, infer K extends number] + ? AnyLabel> extends [infer Col, infer K extends number] ? TryTargetTail, K> - : T[Nx] extends OpTok<"*"> + : T[Next] extends OpTok<"*"> ? TryTargetTail, Nx2> : null : T[J] extends OpTok<"("> | StrTok @@ -629,7 +625,7 @@ type ContinueTargets< > = EatOp extends never ? Ok<[...Acc, E], I> - : ParseTargetList, [...Acc, E]>; + : ParseTargetList, [...Acc, E]>; type ParseTargetSlow< T extends Toks, @@ -646,19 +642,21 @@ type ParseTargetSlow< type ParseTargetAlias = T[I] extends WordTok<"as"> - ? AnyLabel> extends [infer V, infer J] + ? AnyLabel> extends [infer V, infer J] ? [V, J] - : SyntaxErrorAt> + : SyntaxErrorAt> : T[I] extends IdTok - ? [V, Nx] + ? [V, Next] : T[I] extends WordTok ? V extends NotBareLabel ? [null, I] - : [V, Nx] + : [V, Next] : [null, I]; export type ParseFromOpt = - EatWord extends never ? Ok<[], I> : ParseFromList, []>; + EatWord extends never + ? Ok<[], I> + : ParseFromList, []>; export type ParseFromList< T extends Toks, @@ -669,7 +667,7 @@ export type ParseFromList< ? X extends Ok ? EatOp extends never ? Ok<[...Acc, F], J> - : ParseFromList, [...Acc, F]> + : ParseFromList, [...Acc, F]> : X : never; @@ -682,9 +680,9 @@ export type ParseFromItem = type ParseFromPrimary = T[I] extends WordTok<"lateral"> - ? ParseFromPrimaryLateral, true> + ? ParseFromPrimaryLateral, true> : T[I] extends WordTok<"only"> - ? ParseOnlyItem> + ? ParseOnlyItem> : ParseFromPrimaryLateral; type ParseFromPrimaryLateral< @@ -693,31 +691,31 @@ type ParseFromPrimaryLateral< Lateral extends boolean, > = T[I] extends OpTok<"("> - ? IsSelectStart> extends true - ? ParseSelectStmt> extends infer X + ? IsSelectStart> extends true + ? ParseSelectStmt> extends infer X ? X extends Ok ? EatOp extends never ? ParenJoinOr> - : ParseAliasChecked> extends [ + : ParseAliasChecked> extends [ infer A, infer CA, infer J2 extends number, ] ? Ok, J2> - : ParseAliasChecked> + : ParseAliasChecked> : ParenJoinOr : never : Lateral extends true - ? SyntaxErrorAt> + ? SyntaxErrorAt> : ParseParenJoin : T[I] extends WordTok ? FromSpecialFunc : T[I] extends WordTok<"json_table"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseJsonTableItem> : ParseNamedFromItem : T[I] extends WordTok<"rows"> - ? T[Nx] extends WordTok<"from"> + ? T[Next] extends WordTok<"from"> ? ParseRowsFrom, Lateral> : ParseNamedFromItem : ParseNamedFromItem; @@ -744,7 +742,7 @@ type ParseNamedFromItem< ] ? T[J] extends OpTok<"("> ? T[I] extends WordTok<"xmltable"> - ? ParseXmlTable> + ? ParseXmlTable> : ParseFromCall extends Ok ? ParseFuncAlias : ParseFromCall @@ -760,7 +758,7 @@ type ParseTableItem< > = ParseAliasChecked extends [infer A, infer CA, infer J extends number] ? T[J] extends WordTok<"tablesample"> - ? ParseTableSample, A, CA>, Nx> + ? ParseTableSample, A, CA>, Next> : Ok, A, CA>, J> : ParseAliasChecked; @@ -773,11 +771,11 @@ type ParseOnlyItem = : OnlyRelation; type OnlyRelation = T[I] extends OpTok<"("> - ? QualName> extends [infer Parts, infer J extends number] + ? QualName> extends [infer Parts, infer J extends number] ? EatOp extends never ? Expected<'")"', T, J> - : [Parts, Nx] - : Expected<"table name", T, Nx> + : [Parts, Next] + : Expected<"table name", T, Next> : QualName extends [unknown, number] ? QualName : Expected<"table name", T, I>; @@ -803,7 +801,7 @@ type FromSpecialFunc< Lateral extends boolean, > = T[I] extends WordTok<"collation"> - ? T[Nx] extends WordTok<"for"> + ? T[Next] extends WordTok<"for"> ? FromFuncExpr : ParseNamedFromItem : FromFuncExpr; @@ -819,17 +817,17 @@ type ParenJoinOr< E, > = Lateral extends true ? E - : T[Nx] extends OpTok<"("> + : T[Next] extends OpTok<"("> ? ParseParenJoin : E; type ParseParenJoin = - ParseFromItem> extends Ok + ParseFromItem> extends Ok ? EatOp extends never ? Expected<'")"', T, J> : F extends Tag<"join"> - ? ParseParenJoinAlias> + ? ParseParenJoinAlias> : SyntaxErrorAt - : ParseFromItem>; + : ParseFromItem>; type ParseFromCall = ParseExpr extends infer X @@ -838,24 +836,24 @@ type ParseFromCall = | { readonly over: true } | { readonly filter: Expr } | { readonly withinGroup: Expr } - ? WindowlessError>, number>> + ? WindowlessError>, number>> : X : X : never; -type WindowlessError = T[Nx] extends +type WindowlessError = T[Next] extends | OpTok<"("> | WordTok<"group"> ? SyntaxErrorAt> - : SyntaxErrorAt>; + : SyntaxErrorAt>; type ParseTableSample = FuncName extends [infer M, infer J extends number] ? EatOp extends never ? Expected<'"("', T, J> - : ParseExprList, []> extends Ok + : ParseExprList, []> extends Ok ? EatOp extends never ? Expected<'")"', T, J2> - : ParseRepeatable> extends Ok + : ParseRepeatable> extends Ok ? Ok< { t: "sample"; @@ -866,14 +864,14 @@ type ParseTableSample = }, J3 > - : ParseRepeatable> - : ParseExprList, []> + : ParseRepeatable> + : ParseExprList, []> : Expected<"tablesample method", T, I>; type ParseRepeatable = EatWord extends never ? Ok - : EatOp, "("> extends never - ? Expected<'"("', T, Nx> + : EatOp, "("> extends never + ? Expected<'"("', T, Next> : ParseExpr, 0> extends Ok ? Close : ParseExpr, 0>; @@ -881,7 +879,7 @@ type ParseRepeatable = type ParseRowsFrom = EatOp extends never ? Expected<'"("', T, I> - : ParseRowsFromItems, []> extends Ok + : ParseRowsFromItems, []> extends Ok ? ParseFuncAlias extends Ok< { readonly alias: infer A; @@ -893,7 +891,7 @@ type ParseRowsFrom = > ? Ok, J2> : ParseFuncAlias - : ParseRowsFromItems, []>; + : ParseRowsFromItems, []>; type ParseRowsFromItems< T extends Toks, I extends number, @@ -906,17 +904,17 @@ type ParseRowsFromItems< ? EatOp extends never ? EatOp extends never ? Expected<'"," or ")"', T, J3> - : Ok<[...Acc, FromRowNode], Nx> - : ParseRowsFromItems, [...Acc, FromRowNode]> + : Ok<[...Acc, FromRowNode], Next> + : ParseRowsFromItems, [...Acc, FromRowNode]> : ParseItemColDefs : ParseFromCall : Expected<'"("', T, J> : Expected<"function call", T, I>; type ParseItemColDefs = T[I] extends WordTok<"as"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseColDefs, []> - : Expected<'"("', T, Nx> + : Expected<'"("', T, Next> : Ok; type ParseParenJoinAlias = @@ -952,7 +950,7 @@ type ParseParenJoinAlias = : never; type SkipInheritanceStar = - T[I] extends OpTok<"*"> ? Nx : I; + T[I] extends OpTok<"*"> ? Next : I; export type RefOf = Parts extends readonly [ infer N, @@ -975,11 +973,11 @@ type ParseFuncAlias< infer I1 extends number, ] ? T[I1] extends WordTok<"as"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? Ord extends true ? Fail< "WITH ORDINALITY cannot be used with a column definition list", - Nx + Next > : ParseColDefs, []> extends Ok ? Ok, J> @@ -1005,9 +1003,9 @@ type ParseFuncAliasNamed< "WITH ORDINALITY cannot be used with a column definition list", J > - : ParseColDefs, []> extends Ok + : ParseColDefs, []> extends Ok ? Ok, J2> - : ParseColDefs, []> + : ParseColDefs, []> : Ok, J> : ParseAliasChecked; type SkipOrdinality = @@ -1025,8 +1023,8 @@ type ParseColDefs< ? EatOp extends never ? EatOp extends never ? Expected<'"," or ")"', T, J2> - : Ok<[...Acc, ColDefNode], Nx> - : ParseColDefs, [...Acc, ColDefNode]> + : Ok<[...Acc, ColDefNode], Next> + : ParseColDefs, [...Acc, ColDefNode]> : ParseTypeName : Expected<"column definition", T, I>; @@ -1036,9 +1034,9 @@ export type ParseAliasChecked< Fn extends boolean = false, > = T[I] extends WordTok<"as"> - ? Ident> extends [infer A extends string, infer J extends number] + ? Ident> extends [infer A extends string, infer J extends number] ? ParseColAliases - : SyntaxErrorAt> + : SyntaxErrorAt> : Ident extends [infer A extends string, infer J extends number] ? ParseColAliases : [null, null, I]; @@ -1050,7 +1048,7 @@ type ParseColAliases< > = T[I] extends OpTok<"("> ? Fn extends true - ? Ident> extends [string, infer J extends number] + ? Ident> extends [string, infer J extends number] ? T[J] extends OpTok<"," | ")"> ? ParenAliases : [A, null, I] @@ -1064,15 +1062,15 @@ type ParenAliases = type ParseJoins = T[I] extends WordTok<"cross"> - ? T[Nx] extends WordTok<"join"> + ? T[Next] extends WordTok<"join"> ? ParseJoinRhs> - : Expected<"JOIN", T, Nx> + : Expected<"JOIN", T, Next> : T[I] extends WordTok<"natural"> - ? ParseJoinKind> extends infer R + ? ParseJoinKind> extends infer R ? R extends [infer K extends JoinKind, infer J extends number] ? ParseJoinRhs : R extends null - ? Expected<"JOIN", T, Nx> + ? Expected<"JOIN", T, Next> : R : never : ParseJoinKind extends infer R @@ -1085,14 +1083,14 @@ type ParseJoins = type ParseJoinKind = T[I] extends WordTok<"join"> - ? ["inner", Nx] + ? ["inner", Next] : T[I] extends WordTok<"inner"> - ? JoinWord> + ? JoinWord> : T[I] extends WordTok - ? JoinWord, "outer">> + ? JoinWord, "outer">> : null; type JoinWord = - T[I] extends WordTok<"join"> ? [K, Nx] : Expected<"JOIN", T, I>; + T[I] extends WordTok<"join"> ? [K, Next] : Expected<"JOIN", T, I>; type ParseJoinRhs< T extends Toks, @@ -1128,26 +1126,26 @@ type ParseJoinQual< EatWord extends never ? EatWord extends never ? Expected<"ON or USING", T, J> - : ParenIdentList> extends Ok + : ParenIdentList> extends Ok ? SkipUsingAlias extends infer J3 extends number ? ParseJoins, J3> : SkipUsingAlias - : ParenIdentList> - : ParseExpr, 0> extends Ok + : ParenIdentList> + : ParseExpr, 0> extends Ok ? ParseJoins, J2> - : ParseExpr, 0>; + : ParseExpr, 0>; type SkipUsingAlias = T[I] extends WordTok<"as"> - ? Ident> extends [string, infer J] + ? Ident> extends [string, infer J] ? J - : SyntaxErrorAt> + : SyntaxErrorAt> : I; export type ParseWhereOpt< T extends Toks, I extends number, W extends string = "where", -> = EatWord extends never ? Ok : ParseExpr, 0>; +> = EatWord extends never ? Ok : ParseExpr, 0>; type ParseGroupByOpt = EatWords extends never @@ -1161,7 +1159,7 @@ type ParseGroupItems< Sets extends GroupSets, > = T[I] extends WordTok<"rollup" | "cube"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseExprList, []> extends Ok< infer Items extends readonly unknown[], infer J @@ -1170,7 +1168,7 @@ type ParseGroupItems< ? Expected<'")"', T, J> : GroupNext< T, - Nx, + Next, [...Acc, ...Items], Sets extends true ? true @@ -1179,7 +1177,7 @@ type ParseGroupItems< : ParseExprList, []> : ParseGroupExpr : T[I] extends WordTok<"grouping"> - ? T[Nx] extends WordTok<"sets"> + ? T[Next] extends WordTok<"sets"> ? T[Nx2] extends OpTok<"("> ? SkipToCloseParen> extends infer J extends number ? GroupNext @@ -1187,7 +1185,7 @@ type ParseGroupItems< : Expected<'"("', T, Nx2> : ParseGroupExpr : T[I] extends OpTok<"("> - ? T[Nx] extends OpTok<")"> + ? T[Next] extends OpTok<")"> ? GroupNext, Acc, true> : ParseGroupExpr : ParseGroupExpr; @@ -1209,10 +1207,10 @@ type GroupNext< > = EatOp extends never ? Ok<[Acc, Sets], I> - : ParseGroupItems, Acc, Sets>; + : ParseGroupItems, Acc, Sets>; type ParseWindowClause = - T[I] extends WordTok<"window"> ? ParseWindowDefs, []> : Ok<[], I>; + T[I] extends WordTok<"window"> ? ParseWindowDefs, []> : Ok<[], I>; type ParseWindowDefs< T extends Toks, I extends number, @@ -1221,19 +1219,19 @@ type ParseWindowDefs< Ident extends [infer Name, infer J extends number] ? EatWord extends never ? Expected<"AS", T, J> - : EatOp, "("> extends never - ? Expected<'"("', T, Nx> + : EatOp, "("> extends never + ? Expected<'"("', T, Next> : ParseWindowSpec> extends Ok ? EatOp extends never ? Ok<[...Acc, WindowNode], J2> - : ParseWindowDefs, [...Acc, WindowNode]> + : ParseWindowDefs, [...Acc, WindowNode]> : ParseWindowSpec> : Expected<"window name", T, I>; export type ParseWith = EatWord extends never ? ParseCtes - : ParseCtes, [], true>; + : ParseCtes, [], true>; type ParseCtes< T extends Toks, @@ -1245,17 +1243,17 @@ type ParseCtes< ? ParseCteCols extends Ok ? EatWord extends never ? Expected<"AS", T, J2> - : EatOp>, "("> extends never - ? Expected<'"("', T, SkipMaterialized>> + : EatOp>, "("> extends never + ? Expected<'"("', T, SkipMaterialized>> : ParseStatement< T, - Nx>>, + Next>>, false > extends infer X ? X extends Ok ? EatOp extends never ? Expected<'")"', T, J3> - : ParseSearchCycle> extends Ok< + : ParseSearchCycle> extends Ok< [infer Search, infer Cycle], infer J4 > @@ -1265,7 +1263,7 @@ type ParseCtes< [...Acc, CteNode], Rec > - : ParseSearchCycle> + : ParseSearchCycle> : X : never : ParseCteCols @@ -1278,7 +1276,7 @@ type CteNext< > = EatOp extends never ? Ok, I> - : ParseCtes, Acc, Rec>; + : ParseCtes, Acc, Rec>; type ParseCteCols = T[I] extends OpTok<"("> ? ParenIdentList : Ok; type SkipMaterialized = @@ -1294,7 +1292,7 @@ type ParseSearchCycle = : ParseCteSearch; type ParseCteSearch = T[I] extends WordTok<"search"> - ? T[Nx] extends WordTok + ? T[Next] extends WordTok ? EatWords, ["first", "by"]> extends never ? Expected<"FIRST BY", T, Nx2> : ParseColIdList>, []> extends Ok @@ -1309,30 +1307,30 @@ type ParseCteSearch = > : ParseSetCol : ParseColIdList>, []> - : Expected<"DEPTH or BREADTH", T, Nx> + : Expected<"DEPTH or BREADTH", T, Next> : Ok; type ParseCteCycle = T[I] extends WordTok<"cycle"> - ? ParseColIdList, []> extends Ok + ? ParseColIdList, []> extends Ok ? ParseSetCol extends Ok ? T[J2] extends WordTok<"to"> - ? ParseCycleConst> extends Ok + ? ParseCycleConst> extends Ok ? EatWord extends never ? Expected<"DEFAULT", T, J3> - : ParseCycleConst> extends Ok + : ParseCycleConst> extends Ok ? ParseCycleUsing - : ParseCycleConst> - : ParseCycleConst> + : ParseCycleConst> + : ParseCycleConst> : ParseCycleUsing : ParseSetCol - : ParseColIdList, []> + : ParseColIdList, []> : Ok; type ParseCycleUsing = EatWord extends never ? Expected<"USING", T, I> - : Ident> extends [infer P, infer J extends number] + : Ident> extends [infer P, infer J extends number] ? Ok<{ cols: Cols; set: Mark; to: To; default: Def; using: P }, J> - : Expected<"column name", T, Nx>; + : Expected<"column name", T, Next>; type ParseCycleConst = T[I] extends OpTok<"("> ? SyntaxErrorAt @@ -1348,9 +1346,9 @@ type ParseCycleConst = type ParseSetCol = EatWord extends never ? Expected<"SET", T, I> - : Ident> extends [infer N, infer J extends number] + : Ident> extends [infer N, infer J extends number] ? Ok - : Expected<"column name", T, Nx>; + : Expected<"column name", T, Next>; type ParseColIdList< T extends Toks, I extends number, @@ -1359,5 +1357,5 @@ type ParseColIdList< Ident extends [infer N extends string, infer J extends number] ? EatOp extends never ? Ok<[...Acc, N], J> - : ParseColIdList, [...Acc, N]> + : ParseColIdList, [...Acc, N]> : Expected<"column name", T, I>; diff --git a/packages/core/src/postgres/engine/parser/stmt.ts b/packages/core/src/postgres/engine/parser/stmt.ts index 374f7fc1..ede7c767 100644 --- a/packages/core/src/postgres/engine/parser/stmt.ts +++ b/packages/core/src/postgres/engine/parser/stmt.ts @@ -1,10 +1,12 @@ -import type { OpTok, Toks, WordTok } from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { WithClause } from "../ast.js"; +import type { OpTok, WordTok } from "../lexer/tokens.js"; import type { ParseDdl } from "./ddl.js"; import type { ParseDelete, ParseInsert, ParseUpdate } from "./dml.js"; import type { ParseMerge } from "./merge.js"; import type { ParseSelectAfterWith, ParseWith } from "./select.js"; -import type { AtEnd, EatOp, Expected, Fail, Near, Nx, Ok } from "./util.js"; +import type { AtEnd, EatOp, Expected, Fail, Near, Ok } from "./util.js"; import type { ParseCursorStmt } from "./utility.js"; export type ParseProgram = @@ -14,11 +16,11 @@ export type ParseProgram = ? S : EatOp extends never ? Fail<`syntax error at or near ${Near}`, J> - : AtEnd> extends true + : AtEnd> extends true ? S : Fail< - `syntax error at or near ${Near>}: only one statement is allowed`, - Nx + `syntax error at or near ${Near>}: only one statement is allowed`, + Next > : X : never; @@ -65,32 +67,50 @@ type DdlKeyword = | "abort" | "reassign"; +/** + * The statement at I, after the WITH clause `W` if any: a query or DML + * statement, or (without WITH) DDL and cursor statements. + */ export type ParseStatement< T extends Toks, I extends number, Into extends boolean = true, + W extends WithClause | null = null, > = - T[I] extends WordTok - ? W extends "select" | "values" | "table" - ? SelectInto, Into> - : W extends "with" - ? ParseWithStatement, Into> - : W extends "insert" - ? ParseInsert, null> - : W extends "update" - ? ParseUpdate, null> - : W extends "delete" - ? ParseDelete, null> - : W extends "merge" - ? ParseMerge, null> - : W extends DdlKeyword - ? ParseDdl - : W extends "declare" | "fetch" | "move" | "close" - ? ParseCursorStmt - : Fail<`syntax error at or near ${Near}`, I> + T[I] extends WordTok + ? K extends "select" | "values" | "table" + ? SelectInto, Into> + : K extends "insert" + ? ParseInsert, W> + : K extends "update" + ? ParseUpdate, W> + : K extends "delete" + ? ParseDelete, W> + : K extends "merge" + ? ParseMerge, W> + : W extends null + ? K extends "with" + ? ParseWith> extends infer X + ? X extends Ok + ? ParseStatement + : X + : never + : K extends DdlKeyword + ? ParseDdl + : K extends "declare" | "fetch" | "move" | "close" + ? ParseCursorStmt + : Fail<`syntax error at or near ${Near}`, I> + : AfterWith : T[I] extends OpTok<"("> - ? SelectInto, Into> - : Expected<"a SQL statement", T, I>; + ? SelectInto, Into> + : W extends null + ? Expected<"a SQL statement", T, I> + : AfterWith; +type AfterWith = Expected< + "SELECT, INSERT, UPDATE, DELETE or MERGE after WITH", + T, + I +>; type SelectInto = Into extends false ? R @@ -111,32 +131,3 @@ type SelectInto = Into extends false J > : R; - -type ParseWithStatement< - T extends Toks, - I extends number, - Into extends boolean, -> = - ParseWith extends infer W - ? W extends Ok - ? T[J] extends WordTok - ? K extends "select" | "values" | "table" - ? SelectInto, Into> - : K extends "insert" - ? ParseInsert, Wc> - : K extends "update" - ? ParseUpdate, Wc> - : K extends "delete" - ? ParseDelete, Wc> - : K extends "merge" - ? ParseMerge, Wc> - : Expected< - "SELECT, INSERT, UPDATE, DELETE or MERGE after WITH", - T, - J - > - : T[J] extends OpTok<"("> - ? SelectInto, Into> - : Expected<"SELECT, INSERT, UPDATE, DELETE or MERGE after WITH", T, J> - : W - : never; diff --git a/packages/core/src/postgres/engine/parser/typename.ts b/packages/core/src/postgres/engine/parser/typename.ts index cab45a81..e913661b 100644 --- a/packages/core/src/postgres/engine/parser/typename.ts +++ b/packages/core/src/postgres/engine/parser/typename.ts @@ -1,12 +1,8 @@ import type { Digit } from "../../../engine/chars.js"; -import type { - IdTok, - NumTok, - OpTok, - Toks, - WordTok, -} from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { NormalizeTypeName } from "../../schema/normalize.js"; +import type { IdTok, NumTok, OpTok, WordTok } from "../lexer/tokens.js"; import type { ParseExprList } from "./expr.js"; import type { NotTypeFuncName } from "./keywords.js"; import type { @@ -16,7 +12,6 @@ import type { EatWords, Expected, Fail, - Nx, Nx2, Nx3, Ok, @@ -25,64 +20,60 @@ import type { export type ParseTypeName = ParseBaseType extends Ok - ? ParseArraySuffix< - T, - J, - Base extends "char" ? Base : NormalizeTypeName - > + ? ParseArraySuffix> : ParseBaseType; type ParseBaseType = T[I] extends IdTok - ? TypeModifiers>> + ? TypeModifiers>> : T[I] extends WordTok ? W extends "double" - ? EatWord, "precision"> extends never - ? Expected<"PRECISION", T, Nx> + ? EatWord, "precision"> extends never + ? Expected<"PRECISION", T, Next> : Ok<"float8", Nx2> : W extends "national" - ? EatWord, "character" | "char"> extends never - ? Expected<"CHARACTER", T, Nx> - : ParseBaseType> + ? EatWord, "character" | "char"> extends never + ? Expected<"CHARACTER", T, Next> + : ParseBaseType> : W extends "character" | "char" | "nchar" - ? EatWord, "varying"> extends never - ? OkAt<"bpchar", OptPrecision>> + ? EatWord, "varying"> extends never + ? OkAt<"bpchar", OptPrecision>> : OkAt<"varchar", OptPrecision>> : W extends "bit" - ? EatWord, "varying"> extends never - ? TypeModifiers>> + ? EatWord, "varying"> extends never + ? TypeModifiers>> : TypeModifiers>> : W extends "timestamp" | "time" - ? OptPrecision> extends infer J extends number + ? OptPrecision> extends infer J extends number ? WithTz - : OptPrecision> + : OptPrecision> : W extends "interval" ? OkAt< "interval", - T[Nx] extends OpTok<"("> + T[Next] extends OpTok<"("> ? ParsePrecision> - : IntervalFields> + : IntervalFields> > : W extends "float" - ? FloatPrecision> + ? FloatPrecision> : W extends "setof" - ? ParseBaseType> + ? ParseBaseType> : W extends NotTypeFuncName ? W extends "dec" | "decimal" | "numeric" - ? TypeModifiers>> + ? TypeModifiers>> : W extends "varchar" - ? OkAt>> + ? OkAt>> : W extends ColNameTypeWord - ? Ok> + ? Ok> : Expected<"type name", T, I> - : TypeModifiers>> + : TypeModifiers>> : Expected<"type name", T, I>; type OkAt = J extends number ? Ok : J; type FloatPrecision = T[I] extends OpTok<"("> - ? T[Nx] extends NumTok + ? T[Next] extends NumTok ? T[Nx2] extends OpTok<")"> ? P extends SmallFloat ? Ok<"float4", Nx3> @@ -92,10 +83,10 @@ type FloatPrecision = P extends "0" ? "precision for type float must be at least 1 bit" : "precision for type float must be less than 54 bits", - Nx + Next > : Expected<'")"', T, Nx2> - : Expected<"precision", T, Nx> + : Expected<"precision", T, Next> : Ok<"float8", I>; type SmallFloat = | Exclude @@ -118,9 +109,9 @@ type ColNameTypeWord = type QualifiedTail = EatOp extends never ? Ok - : AnyLabel> extends [infer N, infer J extends number] + : AnyLabel> extends [infer N, infer J extends number] ? Ok - : Expected<"type name", T, Nx>; + : Expected<"type name", T, Next>; type WithTz = EatWords extends never @@ -132,11 +123,11 @@ type WithTz = type TypeModifiers = R extends Ok ? T[J] extends OpTok<"("> - ? ParseExprList, []> extends Ok + ? ParseExprList, []> extends Ok ? T[K] extends OpTok<")"> - ? Ok> + ? Ok> : Expected<'")"', T, K> - : ParseExprList, []> + : ParseExprList, []> : R : R; @@ -144,12 +135,12 @@ export type ParsePrecision< T extends Toks, I extends number, > = T[I] extends NumTok - ? T[Nx] extends OpTok<")"> + ? T[Next] extends OpTok<")"> ? Nx2 - : SyntaxErrorAt> + : SyntaxErrorAt> : SyntaxErrorAt; type OptPrecision = - T[I] extends OpTok<"("> ? ParsePrecision> : I; + T[I] extends OpTok<"("> ? ParsePrecision> : I; type IntervalField = "year" | "month" | "day" | "hour" | "minute" | "second"; type IntervalTo = { @@ -161,30 +152,30 @@ type IntervalTo = { export type IntervalFields = T[I] extends WordTok ? F extends "second" - ? OptPrecision> - : T[Nx] extends WordTok<"to"> + ? OptPrecision> + : T[Next] extends WordTok<"to"> ? F extends keyof IntervalTo ? T[Nx2] extends WordTok ? G extends "second" ? OptPrecision> : Nx3 : SyntaxErrorAt> - : SyntaxErrorAt> - : Nx + : SyntaxErrorAt> + : Next : I; type ParseArraySuffix = T[I] extends WordTok<"array"> - ? T[Nx] extends OpTok<"["> + ? T[Next] extends OpTok<"["> ? OkAt, NumThen, "]">> - : Ok, Nx> + : Ok, Next> : ArrayBounds; type ArrayBounds = T[I] extends OpTok<"["> ? ( - T[Nx] extends OpTok<"]"> + T[Next] extends OpTok<"]"> ? Nx2 - : NumThen, "]"> + : NumThen, "]"> ) extends infer J ? J extends number ? ArrayBounds> @@ -196,8 +187,8 @@ type NumThen< I extends number, C extends string, > = T[I] extends NumTok - ? T[Nx] extends OpTok + ? T[Next] extends OpTok ? Nx2 - : SyntaxErrorAt> + : SyntaxErrorAt> : SyntaxErrorAt; type Arr = Base extends `${string}[]` ? Base : `${Base}[]`; diff --git a/packages/core/src/postgres/engine/parser/util.ts b/packages/core/src/postgres/engine/parser/util.ts index 41d0478b..4630be07 100644 --- a/packages/core/src/postgres/engine/parser/util.ts +++ b/packages/core/src/postgres/engine/parser/util.ts @@ -1,16 +1,18 @@ +import type { Next } from "../../../engine/next.js"; +import type { Ok, Tok, Toks } from "../../../engine/tokens.js"; +import type { SqlError } from "../../../errors.js"; import type { - AnyLabel, - EatOp, - EatWords, - Expected, - Nx, - Ok, -} from "../../../engine/parse.js"; -import type { IdTok, OpTok, Toks, WordTok } from "../../../engine/tokens.js"; + IdTok, + NumTok, + OpTok, + ParamTok, + StrTok, + WordTok, +} from "../lexer/tokens.js"; import type { NotColId, TypeFuncNameKeyword } from "./keywords.js"; export type Ident = - IdentName extends infer V extends string ? [V, Nx] : null; + IdentName extends infer V extends string ? [V, Next] : null; type IdentName = H extends IdTok @@ -32,7 +34,7 @@ type QualParts< > = EatOp extends never ? [Acc, I] - : AnyLabel> extends [ + : AnyLabel> extends [ infer B extends string, infer J extends number, ] @@ -43,8 +45,8 @@ type QualParts< export type FuncName = FuncKeyword extends infer V extends string - ? T[Nx] extends OpTok<"("> - ? [[V], Nx] + ? T[Next] extends OpTok<"("> + ? [[V], Next] : null : QualName; type FuncKeyword = @@ -64,12 +66,12 @@ export type SkipFirst< : I; export type Close = - EatOp extends never ? Expected<`"${C}"`, T, I> : Ok>; + EatOp extends never ? Expected<`"${C}"`, T, I> : Ok>; export type ParenIdentList = EatOp extends never ? Expected<'"("', T, I> - : IdentListLoop, []>; + : IdentListLoop, []>; type IdentListLoop< T extends Toks, I extends number, @@ -79,25 +81,91 @@ type IdentListLoop< ? EatOp extends never ? EatOp extends never ? Expected<'"," or ")"', T, J> - : Ok<[...Acc, N], Nx> - : IdentListLoop, [...Acc, N]> + : Ok<[...Acc, N], Next> + : IdentListLoop, [...Acc, N]> : Expected<"identifier", T, I>; -export type { - AnyLabel, - AtEnd, - EatOp, - EatWord, - EatWords, - Expected, - Fail, - Near, - Nx, - Nx2, - Nx3, - Ok, - SkipToCloseParen, - SkipWord, - SkipWords, - SyntaxErrorAt, -} from "../../../engine/parse.js"; +export type { AtEnd, Ok } from "../../../engine/tokens.js"; + +export type Nx2 = Next>; +export type Nx3 = Next>; + +export type Fail = SqlError< + Message, + I +>; + +export type Near = T[I] extends infer H + ? H extends WordTok + ? V + : H extends IdTok + ? `"${V}"` + : H extends OpTok + ? V + : H extends NumTok + ? V + : H extends StrTok + ? `'${V}'` + : H extends ParamTok + ? `$${V}` + : "end of input" + : never; + +export type SyntaxErrorAt = Fail< + `syntax error at or near ${Near}`, + I +>; +export type Expected< + What extends string, + T extends Toks, + I extends number, +> = Fail<`syntax error: expected ${What} at or near ${Near}`, I>; + +export type EatWord< + T extends Toks, + I extends number, + W extends string, +> = T[I] extends { readonly k: "w"; readonly v: W } ? Next : never; +export type EatOp< + T extends Toks, + I extends number, + O extends string, +> = T[I] extends { readonly k: "o"; readonly v: O } ? Next : never; +export type EatWords< + T extends Toks, + I extends number, + Ws extends readonly string[], +> = Ws extends readonly [ + infer W extends string, + ...infer R extends readonly string[], +] + ? EatWord extends never + ? never + : EatWords, R> + : I; +export type SkipWord = + EatWord extends never ? I : EatWord; +export type SkipWords< + T extends Toks, + I extends number, + Ws extends readonly string[], +> = EatWords extends never ? I : EatWords; + +export type AnyLabel = + LabelName extends infer V extends string ? [V, Next] : null; +type LabelName = H extends IdTok | WordTok ? V : null; + +export type SkipToCloseParen< + T extends Toks, + I extends number, + Depth extends readonly unknown[] = [], +> = + T[I] extends OpTok<"("> + ? SkipToCloseParen, [...Depth, 0]> + : T[I] extends OpTok<")"> + ? Depth extends readonly [unknown, ...infer D2] + ? SkipToCloseParen, D2> + : Next + : T[I] extends Tok + ? SkipToCloseParen, Depth> + : SyntaxErrorAt; diff --git a/packages/core/src/postgres/engine/parser/utility.ts b/packages/core/src/postgres/engine/parser/utility.ts index aab79a5a..aebbca41 100644 --- a/packages/core/src/postgres/engine/parser/utility.ts +++ b/packages/core/src/postgres/engine/parser/utility.ts @@ -1,15 +1,17 @@ -import type { NumTok, OpTok, Toks, WordTok } from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; import type { SqlError } from "../../../errors.js"; +import type { NumTok, OpTok, WordTok } from "../lexer/tokens.js"; import type { ParseSelectStmt } from "./select.js"; -import type { AtEnd, EatWord, Expected, Ident, Nx, Nx2, Ok } from "./util.js"; +import type { AtEnd, EatWord, Expected, Ident, Nx2, Ok } from "./util.js"; export type ParseCursorStmt = T[I] extends WordTok ? W extends "declare" - ? ParseDeclare> + ? ParseDeclare> : W extends "fetch" | "move" - ? ParseFetch, W> - : ParseClose> + ? ParseFetch, W> + : ParseClose> : never; type ParseDeclare = @@ -17,13 +19,13 @@ type ParseDeclare = ? CursorOptions extends infer K extends number ? EatWord extends never ? Expected<"CURSOR", T, K> - : SkipHold> extends infer L extends number + : SkipHold> extends infer L extends number ? EatWord extends never ? Expected<"FOR", T, L> - : ParseSelectStmt> extends Ok + : ParseSelectStmt> extends Ok ? Ok<{ t: "declare_cursor"; name: N; q: Q }, J2> - : ParseSelectStmt> - : SkipHold> + : ParseSelectStmt> + : SkipHold> : CursorOptions : Expected<"cursor name", T, I>; @@ -31,11 +33,11 @@ type CursorOptions = T[I] extends WordTok< infer W extends "binary" | "asensitive" | "insensitive" | "scroll" > - ? CursorOptions, Seen | W> + ? CursorOptions, Seen | W> : T[I] extends WordTok<"no"> - ? T[Nx] extends WordTok<"scroll"> + ? T[Next] extends WordTok<"scroll"> ? CursorOptions, Seen | "no scroll"> - : Expected<"SCROLL", T, Nx> + : Expected<"SCROLL", T, Next> : "asensitive" | "insensitive" extends Seen ? SqlError<"cannot specify both ASENSITIVE and INSENSITIVE", null> : "scroll" | "no scroll" extends Seen @@ -44,9 +46,9 @@ type CursorOptions = type SkipHold = T[I] extends WordTok<"with" | "without"> - ? T[Nx] extends WordTok<"hold"> + ? T[Next] extends WordTok<"hold"> ? Nx2 - : Expected<"HOLD", T, Nx> + : Expected<"HOLD", T, Next> : I; type ParseFetch = @@ -59,7 +61,7 @@ type ParseFetch = : FetchFrom, K>; type FetchFrom = D extends number - ? (T[D] extends WordTok<"from" | "in"> ? Nx : D) extends infer J extends + ? (T[D] extends WordTok<"from" | "in"> ? Next : D) extends infer J extends number ? Ident extends [infer N, infer J2 extends number] ? Ok<{ t: K; cursor: N }, J2> @@ -70,31 +72,31 @@ type FetchFrom = D extends number type FetchDirection = T[I] extends WordTok ? W extends "next" | "prior" | "first" | "last" | "all" - ? Nx + ? Next : W extends "absolute" | "relative" - ? SignedInt> extends infer J - ? J extends Nx + ? SignedInt> extends infer J + ? J extends Next ? Expected<"integer", T, J> : J : never : W extends "forward" | "backward" - ? T[Nx] extends WordTok<"all"> + ? T[Next] extends WordTok<"all"> ? Nx2 - : SignedInt> + : SignedInt> : I : SignedInt; type SignedInt = T[I] extends NumTok - ? Nx + ? Next : T[I] extends OpTok<"+" | "-"> - ? T[Nx] extends NumTok + ? T[Next] extends NumTok ? Nx2 - : Expected<"integer", T, Nx> + : Expected<"integer", T, Next> : I; type ParseClose = T[I] extends WordTok<"all"> - ? Ok<{ t: "close_cursor"; cursor: null }, Nx> + ? Ok<{ t: "close_cursor"; cursor: null }, Next> : Ident extends [infer N, infer J extends number] ? Ok<{ t: "close_cursor"; cursor: N }, J> : Expected<"cursor name or ALL", T, I>; diff --git a/packages/core/src/postgres/engine/parser/xml.ts b/packages/core/src/postgres/engine/parser/xml.ts index d769395e..9df04ddc 100644 --- a/packages/core/src/postgres/engine/parser/xml.ts +++ b/packages/core/src/postgres/engine/parser/xml.ts @@ -1,4 +1,6 @@ -import type { IdTok, OpTok, Toks, WordTok } from "../../../engine/tokens.js"; +import type { Next } from "../../../engine/next.js"; +import type { Toks } from "../../../engine/tokens.js"; +import type { IdTok, OpTok, WordTok } from "../lexer/tokens.js"; import type { CallOf, ParseExpr, ParseExprList } from "./expr.js"; import type { ReservedKeyword } from "./keywords.js"; import type { ParseAliasChecked } from "./select.js"; @@ -12,7 +14,6 @@ import type { Expected, Fail, Ident, - Nx, Nx2, Ok, SkipFirst, @@ -69,24 +70,24 @@ export type ParseXmlFunc< type XmlName = EatWord extends never ? Expected<"NAME", T, I> - : AnyLabel> extends [infer N, infer J] + : AnyLabel> extends [infer N, infer J] ? [N, J] - : Expected<"name", T, Nx>; + : Expected<"name", T, Next>; type ParseXmlElement = XmlName extends [string, infer J extends number] ? T[J] extends OpTok<")"> - ? Ok, Nx> + ? Ok, Next> : T[J] extends OpTok<","> - ? ParseElementBody> + ? ParseElementBody> : Expected<'"," or ")"', T, J> : XmlName; type ParseElementBody = T[I] extends WordTok<"xmlattributes"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseNamedList, true, [], []> extends Ok ? T[J] extends OpTok<","> - ? ParseElementContent, Named> + ? ParseElementContent, Named> : Close> : ParseNamedList, true, [], []> : ParseElementContent @@ -106,9 +107,9 @@ type ParseNamedList< ParseExpr extends Ok ? ( T[J] extends WordTok<"as"> - ? AnyLabel> extends [infer N, infer J2] + ? AnyLabel> extends [infer N, infer J2] ? [N, J2] - : Expected<"name", T, Nx> + : Expected<"name", T, Next> : E extends { readonly t: "col"; readonly name: infer N; @@ -138,19 +139,19 @@ type NamedNext< Names extends readonly string[], > = T[J] extends OpTok<","> - ? ParseNamedList, Attr, Vals, Names> + ? ParseNamedList, Attr, Vals, Names> : T[J] extends OpTok<")"> - ? Ok> + ? Ok> : Expected<'"," or ")"', T, J>; type ParseXmlPi = XmlName extends [string, infer J extends number] ? T[J] extends OpTok<")"> - ? Ok, Nx> + ? Ok, Next> : T[J] extends OpTok<","> - ? ParseExpr, 0> extends Ok + ? ParseExpr, 0> extends Ok ? Close> - : ParseExpr, 0> + : ParseExpr, 0> : Expected<'"," or ")"', T, J> : XmlName; @@ -158,8 +159,8 @@ type ParseXmlRoot = ParseExpr extends Ok ? EatOp extends never ? Expected<'","', T, J> - : EatWord, "version"> extends never - ? Expected<"VERSION", T, Nx> + : EatWord, "version"> extends never + ? Expected<"VERSION", T, Next> : RootVersion> extends Ok ? RootStandalone extends infer S extends number ? Close< @@ -177,47 +178,47 @@ type RootVersion = type RootStandalone = EatOp extends never ? I - : EatWord, "standalone"> extends never - ? Expected<"STANDALONE", T, Nx> + : EatWord, "standalone"> extends never + ? Expected<"STANDALONE", T, Next> : StandaloneValue>; type StandaloneValue = EatWords extends never ? EatWord extends never ? Expected<"YES, NO or NO VALUE", T, I> - : Nx + : Next : Nx2; type ParseXmlParse = EatWord extends never ? Expected<"DOCUMENT or CONTENT", T, I> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? Close< T, SkipWords, XmlNode<"parse", [E], [], null> > - : ParseExpr, 0>; + : ParseExpr, 0>; type ParseXmlSerialize = EatWord extends never ? Expected<"DOCUMENT or CONTENT", T, I> - : ParseExpr, 0> extends Ok + : ParseExpr, 0> extends Ok ? EatWord extends never ? Expected<"AS", T, J> - : ParseTypeName> extends Ok + : ParseTypeName> extends Ok ? Close< T, SkipFirst, XmlNode<"serialize", [E], [], Ty> > - : ParseTypeName> - : ParseExpr, 0>; + : ParseTypeName> + : ParseExpr, 0>; type ParseXmlExists = ParseExpr extends Ok ? EatWord extends never ? Expected<"PASSING", T, J> - : ParseExpr>, 13> extends Ok + : ParseExpr>, 13> extends Ok ? Close, CallOf<"xmlexists", [Path, Doc]>> - : ParseExpr>, 13> + : ParseExpr>, 13> : ParseExpr; type SkipMech = SkipWords< T, @@ -227,7 +228,7 @@ type SkipMech = SkipWords< export type ParseXmlTable = ( T[I] extends WordTok<"xmlnamespaces"> - ? T[Nx] extends OpTok<"("> + ? T[Next] extends OpTok<"("> ? ParseNamespaces, []> : Ok<[[], false], I> : Ok<[[], false], I> @@ -236,14 +237,17 @@ export type ParseXmlTable = ( ? ParseExpr extends Ok ? EatWord extends never ? Expected<"PASSING", T, I2> - : ParseExpr>, 13> extends Ok + : ParseExpr>, 13> extends Ok< + infer Doc, + infer I3 + > ? EatWord, "columns"> extends never ? Expected<"COLUMNS", T, SkipMech> - : ParseXmlCols>, []> extends infer CS + : ParseXmlCols>, []> extends infer CS ? CS extends Ok ? EatOp extends never ? Expected<'")"', T, I4> - : ParseAliasChecked> extends [ + : ParseAliasChecked> extends [ infer A, infer CA, infer J extends number, @@ -261,10 +265,10 @@ export type ParseXmlTable = ( }, J > - : ParseAliasChecked> + : ParseAliasChecked> : CS : never - : ParseExpr>, 13> + : ParseExpr>, 13> : ParseExpr : NS : never; @@ -276,15 +280,15 @@ type ParseNamespaces< Dflt extends boolean = false, > = T[I] extends WordTok<"default"> - ? ParseExpr, 5> extends Ok + ? ParseExpr, 5> extends Ok ? NamespacesNext - : ParseExpr, 5> + : ParseExpr, 5> : ParseExpr extends Ok ? EatWord extends never ? Expected<"AS", T, J> - : AnyLabel> extends [string, infer J2 extends number] + : AnyLabel> extends [string, infer J2 extends number] ? NamespacesNext - : Expected<"name", T, Nx> + : Expected<"name", T, Next> : ParseExpr; type NamespacesNext< T extends Toks, @@ -293,10 +297,10 @@ type NamespacesNext< Dflt extends boolean, > = T[J] extends OpTok<","> - ? ParseNamespaces, Acc, Dflt> + ? ParseNamespaces, Acc, Dflt> : T[J] extends OpTok<")"> - ? EatOp, ","> extends never - ? Expected<'","', T, Nx> + ? EatOp, ","> extends never + ? Expected<'","', T, Next> : Ok<[Acc, Dflt], Nx2> : Expected<'"," or ")"', T, J>; @@ -320,7 +324,7 @@ type XmlColsNext< T extends Toks, I extends number, Acc extends readonly unknown[], -> = T[I] extends OpTok<","> ? ParseXmlCols, Acc> : Ok; +> = T[I] extends OpTok<","> ? ParseXmlCols, Acc> : Ok; type ParseColOptions< T extends Toks, @@ -335,25 +339,25 @@ type ParseColOptions< T[I] extends WordTok ? W extends "path" ? Path extends null - ? ParseExpr, 5> extends Ok + ? ParseExpr, 5> extends Ok ? ParseColOptions - : ParseExpr, 5> + : ParseExpr, 5> : Fail<"only one PATH value per column is allowed", I> : W extends "default" ? Def extends null - ? ParseExpr, 5> extends Ok + ? ParseExpr, 5> extends Ok ? ParseColOptions - : ParseExpr, 5> + : ParseExpr, 5> : Fail<"only one DEFAULT value is allowed", I> : W extends "not" - ? T[Nx] extends WordTok<"null"> + ? T[Next] extends WordTok<"null"> ? NN extends null ? ParseColOptions, N, Ty, Path, Def, true, Acc> : NullConflict - : SyntaxErrorAt> + : SyntaxErrorAt> : W extends "null" ? NN extends null - ? ParseColOptions, N, Ty, Path, Def, false, Acc> + ? ParseColOptions, N, Ty, Path, Def, false, Acc> : NullConflict : W extends ReservedKeyword ? ColDone @@ -388,11 +392,11 @@ type NullConflict = Fail< I >; type UnknownColOption = - ParseExpr, 5> extends Ok + ParseExpr, 5> extends Ok ? Fail< W extends "__pg__is_not_null" ? `option name "${W}" cannot be used in XMLTABLE` : `unrecognized column option "${W}"`, I > - : ParseExpr, 5>; + : ParseExpr, 5>; diff --git a/packages/core/src/postgres/engine/project/dml.ts b/packages/core/src/postgres/engine/project/dml.ts index ee2d2efc..b57868c8 100644 --- a/packages/core/src/postgres/engine/project/dml.ts +++ b/packages/core/src/postgres/engine/project/dml.ts @@ -1,14 +1,11 @@ +import type { Next } from "../../../engine/next.js"; +import type { Members, PickOne } from "../../../engine/object.js"; import type { ParamEntry } from "../../../engine/params.js"; import type { ColInfo, ColMap, Cols, - CteMap, - Ctx, Info, - MkCtx, - NullableMap, - QueryContext, ScopeItem, ScopeLevel, } from "../../../engine/types.js"; @@ -37,7 +34,7 @@ import type { import type { HasStar } from "../binder/clauses.js"; import type { BindFrom } from "../binder/from.js"; import type { ResolveWritableTable } from "../binder/resolve.js"; -import type { Nx } from "../parser/util.js"; +import type { NullableMap } from "../shape.js"; import type { ParamOf, ParamsOfExpr, @@ -47,6 +44,8 @@ import type { ParamsOfTargets, } from "../typer/params.js"; import type { TypeOf } from "../typer/typeof.js"; +import type { CteMap, Ctx, MkCtx, QueryContext } from "../types.js"; +import type { WidthCheck } from "../util.js"; import type { ParamsOfMerge, ProjectMerge } from "./merge.js"; import type { BindWith, ProjectSelect, ProjectTargets } from "./select.js"; @@ -146,50 +145,6 @@ type AssignInfo< : never : ColInfoOf; -export type NoSubscript = - | "bool" - | "int2" - | "int4" - | "int8" - | "float4" - | "float8" - | "numeric" - | "money" - | "text" - | "varchar" - | "bpchar" - | "char" - | "bytea" - | "date" - | "time" - | "timetz" - | "timestamp" - | "timestamptz" - | "interval" - | "uuid" - | "json" - | "xml" - | "inet" - | "cidr" - | "macaddr" - | "macaddr8" - | "bit" - | "varbit" - | "oid" - | "regclass" - | "regtype" - | "regproc" - | "path" - | "polygon" - | "circle" - | "tsvector" - | "tsquery" - | "int4range" - | "int8range" - | "numrange" - | "tsrange" - | "tstzrange" - | "daterange"; type AssignParamInfo< Tbl extends Table, Col extends string, @@ -197,7 +152,7 @@ type AssignParamInfo< C extends Ctx, > = Extract, ColInfo>; -export type AssignTargets< +type AssignTargets< Cs extends readonly string[], Ind extends readonly TargetInd[] | null, > = { @@ -251,51 +206,70 @@ type ProjectInsert = S["alias"], "insert into" > extends Target_ - ? InsertTargetCols< + ? CheckInsert< S["columns"], S["colInd"], S["source"], Tbl, S["table"]["name"], - Env - > extends infer Cs - ? Cs extends readonly string[] | null - ? CheckInsertSource< - S["source"], - Cs extends readonly string[] ? Cs : [], - S["colInd"], - Tbl, - S["table"]["name"], - MkCtx - > extends infer Chk - ? Chk extends AnyErr - ? Chk - : CheckRequired extends AnyErr - ? CheckRequired - : CheckOnConflict< - S["onConflict"], - Tbl, - Item, - S["table"]["name"], - Env - > extends infer Oc - ? Oc extends AnyErr - ? Oc - : ProjectReturning< - S["returning"], - S["retOpts"], - [Item], - Tbl, - true, - false, - Env - > - : never + Env, + MkCtx + > extends infer Chk + ? Chk extends AnyErr + ? Chk + : CheckOnConflict< + S["onConflict"], + Tbl, + Item, + S["table"]["name"], + Env + > extends infer Oc + ? Oc extends AnyErr + ? Oc + : ProjectReturning< + S["returning"], + S["retOpts"], + [Item], + Tbl, + true, + false, + Env + > : never - : Cs : never : ResolveTarget; +/** + * The checks of an INSERT, also of MERGE's INSERT action: the target columns, + * the rows' width and values, and the NOT NULL columns left without a value. + * `C` is where the values are typed. + */ +export type CheckInsert< + Columns extends readonly string[] | null, + Ind extends readonly TargetInd[] | null, + Source extends InsertSource, + Tbl extends Table, + Name extends string, + Env extends QueryContext, + C extends Ctx, +> = + InsertTargetCols extends infer Cs + ? Cs extends readonly string[] | null + ? CheckInsertSource< + Source, + Cs extends readonly string[] ? Cs : [], + Ind, + Tbl, + Name, + C + > extends infer Chk + ? Chk extends AnyErr + ? Chk + : CheckRequired + : never + : Cs + : never; + type InsertTargetCols< Columns extends readonly string[] | null, Ind extends readonly TargetInd[] | null, @@ -324,16 +298,13 @@ type InsertTargetCols< : W extends readonly [] ? [] : W extends readonly unknown[] - ? WidthCheck> extends "more" - ? SqlError< - "INSERT has more expressions than target columns", - null - > + ? WidthCheck> extends "more" + ? WidthResult<"more", never> : null : null : never; -export type WidthResult = R extends "eq" +type WidthResult = R extends "eq" ? Ok_ : R extends "more" ? SqlError<"INSERT has more expressions than target columns", null> @@ -375,34 +346,7 @@ type SameLength< : false : true; -// Equal finite widths need no suffix tuples. Keep the recursive comparison -// for unequal widths and arrays whose lengths are not fixed. -export type WidthCheck< - A extends readonly unknown[], - B extends readonly unknown[], -> = A extends unknown - ? B extends unknown - ? number extends A["length"] | B["length"] - ? WidthCheckWalk - : A["length"] extends B["length"] - ? B["length"] extends A["length"] - ? "eq" - : WidthCheckWalk - : WidthCheckWalk - : never - : never; -type WidthCheckWalk< - A extends readonly unknown[], - B extends readonly unknown[], -> = A extends readonly [unknown, ...infer AR] - ? B extends readonly [unknown, ...infer BR] - ? WidthCheckWalk - : "more" - : B extends readonly [unknown, ...unknown[]] - ? "fewer" - : "eq"; - -export type CheckAssignedCols< +type CheckAssignedCols< Cs extends readonly string[], Tgs extends readonly TargetInd[], Tbl extends Table, @@ -416,10 +360,10 @@ export type CheckAssignedCols< ? Tgs[I] extends Expr ? C extends Whole ? AssignedTwice - : CheckAssignedCols> + : CheckAssignedCols> : C extends Whole | Part ? AssignedTwice - : CheckAssignedCols> + : CheckAssignedCols> : SqlError<`column "${C}" of relation "${Name}" does not exist`, null> : true; type AssignedTwice = Kind extends "insert" @@ -444,7 +388,7 @@ type CheckInsertSource< : CheckRows, Tbl, Name, C> : CheckTargets, [], Tbl, Name, C>; -export type CheckRows< +type CheckRows< Rows extends readonly (readonly Expr[])[], Cs extends readonly string[], Tgs extends readonly TargetInd[], @@ -462,7 +406,7 @@ export type CheckRows< : CheckValueItems : true; -export type CheckValueItems = +type CheckValueItems = Es[number] extends Tag<"param" | "default"> ? true : CheckValueItemsWalk; @@ -495,19 +439,19 @@ type CheckTargets< ? DefaultIntoInd : AssignInfo extends AnyErr ? AssignInfo - : CheckTargets> + : CheckTargets> : Es[I] extends Tag<"null"> ? Tbl[Col]["nullable"] extends false ? NullViolation - : CheckTargets> + : CheckTargets> : Es[I] extends Tag<"default"> ? Tbl[Col] extends { readonly nullable: false; readonly hasDefault: false; } ? NullViolation - : CheckTargets> - : CheckTargets> + : CheckTargets> + : CheckTargets> : SqlError<`column "${Col}" of relation "${Name}" does not exist`, null> : true; @@ -524,7 +468,7 @@ type DefaultIntoInd = Ind extends { readonly e: infer In } : DefaultIntoInd : never; -export type CheckRequired< +type CheckRequired< Tbl extends Table, Cs extends readonly string[] | null, Name extends string, @@ -532,7 +476,7 @@ export type CheckRequired< ? [MissingRequired] extends [never] ? true : SqlError< - `null value in column "${FirstOf> & string}" of relation "${Name}" violates not-null constraint`, + `null value in column "${PickOne> & string}" of relation "${Name}" violates not-null constraint`, null > : true; @@ -629,7 +573,7 @@ type CheckPlainSetAt< Set, Tbl, Name, - Nx, + Next, Seen | Col, [Duplicate] extends [never] ? Col extends Seen @@ -885,25 +829,6 @@ type RowItem = { readonly skip: string[]; }; -export type UnionToIntersection = ( - U extends unknown - ? (u: U) => void - : never -) extends (u: infer I) => void - ? I - : never; -type FirstOf = - UnionToIntersection void : never> extends ( - x: infer L, - ) => void - ? L - : never; -export type UnionToTuple = [U] extends [ - never, -] - ? Acc - : UnionToTuple>, [FirstOf, ...Acc]>; - export type ParamsOfDml = BindWith> extends infer C2 extends CteMap @@ -1024,8 +949,8 @@ export type ParamsOfRowInfos< ? { [K in keyof ET]: K extends `${number}` ? K extends keyof Infos - ? ParamsOfOpt - : ParamsOfExpr + ? ParamsOfOpt, C, Infos[K]> + : ParamsOfExpr, C> : never; }[number] : never; diff --git a/packages/core/src/postgres/engine/project/merge.ts b/packages/core/src/postgres/engine/project/merge.ts index 99c278f4..836ea247 100644 --- a/packages/core/src/postgres/engine/project/merge.ts +++ b/packages/core/src/postgres/engine/project/merge.ts @@ -1,12 +1,4 @@ -import type { - CteMap, - Ctx, - Info, - MkCtx, - QueryContext, - ScopeItem, - ScopeLevel, -} from "../../../engine/types.js"; +import type { Info, ScopeItem, ScopeLevel } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table } from "../../../schema.js"; import type { @@ -18,17 +10,14 @@ import type { TargetInd, } from "../ast.js"; import type { BindFrom } from "../binder/from.js"; -import type { NullableMap } from "../shape.js"; +import type { NullableLevel } from "../shape.js"; import type { ParamsOfFromItems, ParamsOfOpt } from "../typer/params.js"; import type { TypeOf } from "../typer/typeof.js"; +import type { CteMap, Ctx, MkCtx, QueryContext } from "../types.js"; import type { - AssignTargets, - CheckAssignedCols, + CheckInsert, CheckOptExpr, - CheckRequired, - CheckRows, CheckSet, - CheckValueItems, DmlCtx, ParamsOfInds, ParamsOfReturning, @@ -38,9 +27,6 @@ import type { ResolveTarget, Target_, TargetInfos, - UnionToTuple, - WidthCheck, - WidthResult, } from "./dml.js"; export type ProjectMerge = @@ -93,14 +79,6 @@ type ReturningSource = [ ? L : NullableLevel; -type NullableLevel = { - readonly [I in keyof L]: { - readonly alias: L[I]["alias"]; - readonly cols: NullableMap; - readonly skip: L[I]["skip"]; - }; -}; - type HasAction = [ Extract, ] extends [never] @@ -198,37 +176,17 @@ type CheckMergeInsert< Tbl extends Table, Name extends string, C extends Ctx, -> = Vals extends readonly Expr[] - ? Cols extends readonly string[] - ? CheckAssignedCols< - Cols, - AssignTargets, - Tbl, - Name, - "insert" - > extends infer E - ? E extends AnyErr - ? E - : WidthResult< - WidthCheck, - CheckRows< - [Vals], - Cols, - AssignTargets, - Tbl, - Name, - C - > extends infer V - ? V extends AnyErr - ? V - : CheckRequired - : never - > - : never - : WidthCheck> extends "more" - ? SqlError<"INSERT has more expressions than target columns", null> - : CheckValueItems - : CheckRequired; +> = CheckInsert< + Cols, + Ind, + Vals extends readonly Expr[] + ? { readonly t: "values"; readonly rows: [Vals] } + : { readonly t: "default" }, + Tbl, + Name, + C, + C +>; export type ParamsOfMerge = ResolveTarget extends Target_< diff --git a/packages/core/src/postgres/engine/project/naming.ts b/packages/core/src/postgres/engine/project/naming.ts index a70bcb14..910c3a95 100644 --- a/packages/core/src/postgres/engine/project/naming.ts +++ b/packages/core/src/postgres/engine/project/naming.ts @@ -1,5 +1,5 @@ -import type { Ctx } from "../../../engine/types.js"; import type { Expr, SelectStmt, Tag } from "../ast.js"; +import type { Ctx } from "../types.js"; import type { ProjectSelect } from "./select.js"; export type ColumnName< diff --git a/packages/core/src/postgres/engine/project/select.ts b/packages/core/src/postgres/engine/project/select.ts index dcafd1ed..faea70ef 100644 --- a/packages/core/src/postgres/engine/project/select.ts +++ b/packages/core/src/postgres/engine/project/select.ts @@ -1,17 +1,15 @@ import type { NarrowKeys, NarrowLevel } from "../../../engine/narrow.js"; +import type { Next } from "../../../engine/next.js"; import type { ColInfo, ColMap, Cols, ColsToMap, - CteMap, - Ctx, Info, - MkCtx, - QueryContext, ScopeLevel, } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; +import type { TypeFact } from "../../schema/type-facts.js"; import type { Cte, CteCycle, @@ -35,24 +33,26 @@ import type { CheckOutputOrder, CheckWindowClause, CheckWindowRefs, + Grouped, HasStar, ResolveSortKeys, + SetsLevel, TargetExprs, - TypeDisplay, WindowDefs, } from "../binder/clauses.js"; import type { BindFrom, RenameCols } from "../binder/from.js"; import type { ExpandStar } from "../binder/resolve.js"; -import type { Nx } from "../parser/util.js"; import type { ArrInfo, HasNullElems, + LitToText, NullableMap, ObjShape, Shaped, } from "../shape.js"; -import type { CommonLit, LitToText } from "../typer/operators.js"; +import type { SelectCommon, UnmatchedErr } from "../typer/operators.js"; import type { TypeOf } from "../typer/typeof.js"; +import type { CteMap, Ctx, MkCtx, QueryContext } from "../types.js"; import type { ColumnName } from "./naming.js"; import type { ProjectStmt } from "./stmt.js"; @@ -106,7 +106,7 @@ type BindCtes< AddCte >, Seen | Cs[I]["name"], - Nx + Next > : BindCte; @@ -259,15 +259,10 @@ type CycleMarkType = [To, Def] extends [ ] ? ConstType extends infer A extends string ? ConstType extends infer B extends string - ? CommonLit extends infer T extends string + ? SelectCommon extends infer T extends string ? T extends "unknown" - ? B extends "unknown" - ? "text" - : SqlError< - `CYCLE types ${TypeDisplay} and ${TypeDisplay} cannot be matched`, - null - > - : LitToText extends NoEquality + ? "text" + : TypeFact, "eq", true> extends false ? SqlError< `could not identify an equality operator for type ${LitToText}`, null @@ -278,24 +273,10 @@ type CycleMarkType = [To, Def] extends [ null > : LitToText - : never + : UnmatchedErr<"CYCLE", SelectCommon> : never : never : "bool"; -type NoEquality = - | "box" - | "circle" - | "json" - | "jsonpath" - | "line" - | "lseg" - | "path" - | "pg_snapshot" - | "point" - | "polygon" - | "refcursor" - | "txid_snapshot" - | "xml"; type ConstType = E extends Tag<"str"> ? "lit" @@ -400,13 +381,17 @@ type WithExtra = M & X extends infer All ? { readonly [K in keyof All]: All[K] } : never; type FixStep = { - readonly [K in keyof M0]: { - readonly type: M0[K]["type"]; - readonly nullable: Full[K & string]["nullable"]; - }; + readonly [K in keyof M0]: ArrInfo< + M0[K]["type"], + Full[K & string]["nullable"], + HasNullElems extends true ? true : HasNullElems + >; }; type Changed = { - [K in keyof A]: [A[K]["nullable"]] extends [B[K & string]["nullable"]] + [K in keyof A]: [A[K]["nullable"], HasNullElems] extends [ + B[K & string]["nullable"], + HasNullElems, + ] ? never : K; }[keyof A]; @@ -551,20 +536,15 @@ type AlignCols< readonly t: infer LT extends ColInfo; } ? R[I] extends { readonly t: infer RT extends ColInfo } - ? CommonLit extends infer T extends string + ? SelectCommon extends infer T extends string ? T extends "unknown" - ? RT["type"] extends "unknown" - ? AlignSetOp< - L, - R, - Op, - [...Acc, { readonly n: N; readonly t: Info<"unknown", true> }], - Nx - > - : SqlError< - `${Op} types ${TypeDisplay} and ${TypeDisplay} cannot be matched`, - null - > + ? AlignSetOp< + L, + R, + Op, + [...Acc, { readonly n: N; readonly t: Info<"unknown", true> }], + Next + > : AlignSetOp< L, R, @@ -580,9 +560,9 @@ type AlignCols< >; }, ], - Nx + Next > - : never + : UnmatchedErr> : R[I] extends undefined ? CountErr : RestFrom @@ -595,7 +575,7 @@ type RestFrom< C extends Cols, I extends number, Acc extends Cols, -> = C[I] extends undefined ? Acc : RestFrom, [...Acc, C[I]]>; +> = C[I] extends undefined ? Acc : RestFrom, [...Acc, C[I]]>; type ProjectValues< Rows extends readonly (readonly Expr[])[], @@ -667,7 +647,7 @@ type OperandRow< : TypeOf ) extends infer T ? T extends ColInfo - ? OperandRow> + ? OperandRow> : T : never; @@ -680,7 +660,7 @@ type MergeRows< ? OperandRow extends infer Us extends readonly ColInfo[] ? Us["length"] extends Acc["length"] ? ZipCommon extends infer Z extends readonly ColInfo[] - ? MergeRows> + ? MergeRows> : ZipCommon : SqlError<"VALUES lists must all be the same length", null> : OperandRow @@ -692,24 +672,23 @@ type ZipCommon< Acc extends readonly ColInfo[], I extends number, > = A[I] extends ColInfo - ? CommonLit extends infer T extends string + ? SelectCommon extends infer T extends string ? T extends "unknown" - ? B[I]["type"] extends "unknown" - ? ZipCommon], Nx> - : SqlError< - `VALUES types ${TypeDisplay} and ${TypeDisplay} cannot be matched`, - null - > + ? ZipCommon], Next> : ZipCommon< A, B, [ ...Acc, - Info, + ArrInfo< + T, + A[I]["nullable"] extends true ? true : B[I]["nullable"], + HasNullElems extends true ? true : HasNullElems + >, ], - Nx + Next > - : never + : UnmatchedErr<"VALUES", SelectCommon> : Acc; type ValuesCols< @@ -722,12 +701,16 @@ type ValuesCols< ] ? ValuesCols< R, - Nx, + Next, [ ...Acc, { readonly n: `column${I}`; - readonly t: Info, T["nullable"]>; + readonly t: ArrInfo< + LitToText, + T["nullable"], + HasNullElems + >; }, ] > @@ -805,40 +788,6 @@ type ProjectSimple< : never : BindFrom; -type Grouped = Sets extends false - ? GroupBy extends null - ? false - : true - : false; - -type SetsLevel = Sets extends false - ? L - : NullLevel : string>; -type LeftOut< - L extends ScopeLevel, - Is extends readonly unknown[], -> = Is extends readonly [infer H, ...infer R] - ? H extends { readonly t: "col"; readonly name: infer N } - ? N extends LevelNames - ? N | LeftOut - : string - : string - : never; -type LevelNames = { - [I in keyof L]: keyof L[I]["cols"]; -}[number]; -type NullLevel = { - readonly [I in keyof L]: { - readonly alias: L[I]["alias"]; - readonly cols: { - readonly [K in keyof L[I]["cols"]]: K extends Names - ? { readonly type: L[I]["cols"][K]["type"]; readonly nullable: true } - : L[I]["cols"][K]; - }; - readonly skip: L[I]["skip"]; - }; -}; - type CheckSimple< Distinct, Ts extends readonly Target[], @@ -921,7 +870,7 @@ export type ProjectTargets< > = Ts[I] extends infer Tg extends Target ? ProjectStarTarget extends infer R ? R extends { readonly star: ColMap } - ? ProjectTargets, Lit> + ? ProjectTargets, Lit> : R extends "typed" ? ( Lit extends true @@ -936,7 +885,7 @@ export type ProjectTargets< Ts, C, [...Acc, { readonly n: N; readonly t: Ty }], - Nx, + Next, Lit > : ColumnName @@ -953,7 +902,7 @@ type ProjectStarTarget = E extends { ? { readonly star: M } : ExpandStar : E extends { readonly t: "field"; readonly name: "*"; readonly e: infer F } - ? FieldStar> extends infer M extends ColMap + ? FieldStar, C>> extends infer M extends ColMap ? { readonly star: M } - : FieldStar> + : FieldStar, C>> : "typed"; diff --git a/packages/core/src/postgres/engine/project/stmt.ts b/packages/core/src/postgres/engine/project/stmt.ts index 64cc4617..02b2826c 100644 --- a/packages/core/src/postgres/engine/project/stmt.ts +++ b/packages/core/src/postgres/engine/project/stmt.ts @@ -1,6 +1,6 @@ -import type { QueryContext } from "../../../engine/types.js"; import type { AnyErr } from "../../../errors.js"; import type { DmlStmt, SelectStmt, Stmt, Tag } from "../ast.js"; +import type { QueryContext } from "../types.js"; import type { ProjectDml } from "./dml.js"; import type { ProjectSelect } from "./select.js"; diff --git a/packages/core/src/postgres/engine/shape.ts b/packages/core/src/postgres/engine/shape.ts index 528f21df..712f0032 100644 --- a/packages/core/src/postgres/engine/shape.ts +++ b/packages/core/src/postgres/engine/shape.ts @@ -1,10 +1,13 @@ -import type { ColInfo, ColMap, Info } from "../../engine/types.js"; -import type { Shaped } from "../../schema.js"; -import type { MergeShapes } from "./json.js"; +import type { ColInfo, ColMap, Info, ScopeLevel } from "../../engine/types.js"; +import type { Shaped, TsShape } from "../../schema.js"; +import type { JsonT, TypeFact } from "../schema/type-facts.js"; export type { Shaped }; export type AnyShaped = { readonly shape: unknown }; +/** An untyped string literal resolves to text. */ +export type LitToText = T extends "lit" ? "text" : T; + export type ObjShape = { readonly k: "obj"; readonly m: M }; export type BaseOf = T extends "jsonb" @@ -23,7 +26,6 @@ export type BaseOf = T extends "jsonb" */ export type NullElems = { readonly nullElems: true }; export type HasNullElems = A extends NullElems ? true : false; -/** `Info`, marked when `T` is an array and `E` says its elements can be NULL. */ export type ArrInfo< T extends string, N extends boolean, @@ -33,27 +35,51 @@ export type ArrInfo< ? { readonly type: T; readonly nullable: N; readonly nullElems: true } : Info : Info; -/** The columns of the outer side of a join, keeping their element nullability. */ -export type NullableMap = { - readonly [K in keyof M]: M[K] extends NullElems - ? { - readonly type: M[K]["type"]; - readonly nullable: true; - readonly nullElems: true; - } - : { readonly type: M[K]["type"]; readonly nullable: true }; +/** + * The columns made nullable (the outer side of a join, those a grouping set + * leaves out), keeping their element nullability. + */ +export type NullableMap = { + readonly [K in keyof M]: K extends Names ? NullableCol : M[K]; +}; +type NullableCol = C extends NullElems + ? { + readonly type: C["type"]; + readonly nullable: true; + readonly nullElems: true; + } + : Info; +/** + * A scope level with its columns, or those named `Names`, made nullable. + * `orig` keeps the columns as the relation has them, which a condition + * rejecting NULL in one of them restores (see NarrowLevel). + */ +export type NullableLevel = { + readonly [I in keyof L]: { + readonly alias: L[I]["alias"]; + readonly cols: NullableMap; + readonly skip: L[I]["skip"]; + readonly orig: L[I] extends { readonly orig: infer O } ? O : L[I]["cols"]; + }; }; -/** Whether `A` as an element can be NULL; an array passes on its own elements. */ export type ElemNull = A["type"] extends `${string}[]` ? HasNullElems : A["nullable"]; +/** A type as TypeSQL's own messages show it: its catalog name. */ export type StripShape = T extends AnyShaped ? BaseOf : T extends `${infer E}[]` ? `${BaseOf}[]` : T; +/** A type as PostgreSQL's messages show it (format_type). */ +export type TypeDisplay = T extends AnyShaped + ? BaseOf + : T extends `${infer E}[]` + ? `${TypeDisplay}[]` + : TypeFact; + export type RowInfo = { readonly type: Shaped<"record", ObjShape>; readonly nullable: AllNullable; @@ -71,3 +97,25 @@ export type ShapedCommon = [ : BaseOf : BaseOf : "unknown"; + +export type ArrShape = { readonly k: "arr"; readonly e: E }; + +export type MergeShapes = [B] extends [TsShape<[]>] + ? ArrayLike extends true + ? A + : A | B + : [A] extends [TsShape<[]>] + ? ArrayLike extends true + ? B + : A | B + : A | B; +type ArrayLike = [S] extends [ArrShape] + ? true + : [S] extends [TsShape] + ? [] extends T + ? true + : false + : false; + +export type ShapeOfType = + T extends Shaped ? S : null; diff --git a/packages/core/src/postgres/engine/suggest.ts b/packages/core/src/postgres/engine/suggest.ts index 121b8cca..e3548267 100644 --- a/packages/core/src/postgres/engine/suggest.ts +++ b/packages/core/src/postgres/engine/suggest.ts @@ -1,7 +1,9 @@ -import type { Best, IsRelation, RelCols } from "../../engine/suggest.js"; +import type { Next } from "../../engine/next.js"; +import type { Best } from "../../engine/suggest.js"; import type { Toks } from "../../engine/tokens.js"; -import type { Nx } from "./parser/util.js"; -import type { FunctionCatalog } from "./typer/catalog.js"; +import type { IsRelation, RelCols } from "../../schema.js"; +import type { MetaKey } from "./db.js"; +import type { FunctionName } from "./typer/catalog.js"; export type WithHint< M extends string, @@ -24,15 +26,14 @@ export type WithHint< : M extends `relation "${infer Name}" does not exist` ? Hinted>, "relation"> : M extends `function ${infer Name}(${infer Args}) does not exist. Declare it ${infer Rest}` - ? Best>> extends infer S extends - string + ? Best< + Name, + SameStart> + > extends infer S extends string ? `function ${Name}(${Args}) does not exist. Perhaps you meant the function "${S}"? Otherwise declare it ${Rest}` : M : M; -type FnNames = - | (keyof FunctionCatalog & string) - | (DB extends { readonly $functions: infer F } ? keyof F & string : never); type SameStart = Name extends `${infer C}${string}` ? Extract : never; @@ -44,7 +45,12 @@ type Hinted = S extends string type MentionedRels = T[I] extends { readonly k: string; } - ? MentionedRels>, Acc | RelAt, DB> | RelAt> + ? MentionedRels< + T, + DB, + Next>, + Acc | RelAt, DB> | RelAt + > : Acc; type RelAt = T[I] extends { readonly k: "w" | "id"; @@ -56,7 +62,7 @@ type RelAt = T[I] extends { : never; type RelNames = { - [K in keyof DB & string]: K extends "$types" | "$functions" | "$nullParams" + [K in keyof DB & string]: K extends MetaKey ? never : IsRelation extends true ? K diff --git a/packages/core/src/postgres/engine/typer/catalog.ts b/packages/core/src/postgres/engine/typer/catalog.ts index d79eaf3e..8f9a785b 100644 --- a/packages/core/src/postgres/engine/typer/catalog.ts +++ b/packages/core/src/postgres/engine/typer/catalog.ts @@ -1,53 +1,54 @@ import type { ColInfo, Info } from "../../../engine/types.js"; import type { SqlError } from "../../../errors.js"; -import type { TypeDisplay } from "../binder/clauses.js"; -import type { JsonRet } from "../json.js"; -import type { ArrInfo, ElemNull, HasNullElems } from "../shape.js"; +import type { DeclaredSig, Sig, SigFlags } from "../../schema/functions.js"; import type { - CommonLit, DateTimeT, - IntT, JsonT, - LitToText, NumT, + PgTypeFacts, RangeT, StrT, - Unmatchable, -} from "./operators.js"; +} from "../../schema/type-facts.js"; +import type { SchemaFunctions } from "../db.js"; +import type { JsonRet } from "../json.js"; +import type { + ArrInfo, + ElemNull, + HasNullElems, + LitToText, + TypeDisplay, +} from "../shape.js"; +import type { SelectCommon, UnmatchedErr } from "./operators.js"; import type { PgFunctions } from "./pg-functions.js"; -export type Sig< - Args extends readonly string[], - Ret extends string, - Flags extends SigFlags = {}, -> = { - readonly args: Args; - readonly ret: Ret; -} & Flags; -export type SigFlags = { - readonly strict?: boolean | readonly number[]; - readonly nullable?: boolean; - readonly agg?: boolean; - readonly window?: boolean; - readonly srf?: boolean; +/** Behaviour only built-in functions declare. */ +type CatalogFlags = { + /** + * The result array's elements can be NULL, always or when one of these + * arguments (1-based) can be NULL or is an array whose elements can be. + */ + readonly elems?: true | readonly number[]; + /** The first argument is anyelement: an untyped literal or NULL can't fill it. */ + readonly poly?: true; + /** A hypothetical-set aggregate, which needs WITHIN GROUP. */ + readonly hypothetical?: true; + /** NULL for an argument of type unknown (COLLATION FOR). */ + readonly untypedNull?: true; + /** Builds an object whose keys are the literal odd arguments. */ + readonly object?: true; + /** A set-returning function's columns in FROM. */ + readonly cols?: readonly { readonly n: string; readonly t: ColInfo }[]; + /** In FROM, its one column is named this rather than after the function. */ + readonly column?: string; + /** Returns record: FROM needs a column definition list. */ + readonly coldefs?: true; }; -/** - * Catalog only: the result array's elements can be NULL, always or when one of - * these arguments (1-based) can be NULL or is an array whose elements can be. - */ -type ElemsFlag = { readonly elems?: true | readonly number[] }; -/** `Sig` with `ElemsFlag`. */ type ESig< Args extends readonly string[], Ret extends string, - Flags extends SigFlags & ElemsFlag, + Flags extends SigFlags & CatalogFlags, > = { readonly args: Args; readonly ret: Ret } & Flags; -export type AnySig = { - readonly args: readonly string[]; - readonly ret: string; -} & SigFlags & - ElemsFlag; -export type Overloads = AnySig | readonly AnySig[]; +export type AnySig = DeclaredSig & CatalogFlags; type Agg = { agg: true }; /** array_agg: the elements are the argument's values. */ @@ -57,21 +58,27 @@ type AggNN = { agg: true; nullable: false }; /** NULL even over non-null input, e.g. for a single row or zero variance. */ type AggNullable = { agg: true; nullable: true }; type Win = { window: true; nullable: false }; -type WinNullable = { window: true; nullable: true }; +/** A window function over anyelement. */ +type WinPoly = { window: true; nullable: true; poly: true }; +type Hypothetical = { agg: true; nullable: false; hypothetical: true }; type NN = { nullable: false }; type Nullable = { nullable: true }; type Srf = { srf: true }; /** regexp_match(es): a group that takes no part in the match is NULL. */ type NullableElems = { nullable: true; elems: true }; type SrfElems = { srf: true; elems: true }; -type SrfNullable = { srf: true; nullable: true }; +/** json_each's columns. */ +type KeyValue = [ + { readonly n: "key"; readonly t: Info<"text"> }, + { readonly n: "value"; readonly t: Info }, +]; /** Ordered overloads for each supported number of trailing optional arguments. */ type OptionalArgs< Args extends readonly string[], Optional extends readonly string[], Ret extends string, - Flags extends SigFlags & ElemsFlag = {}, + Flags extends SigFlags & CatalogFlags = {}, > = [ ESig, ...(Optional extends readonly [ @@ -105,7 +112,7 @@ export interface FunctionCatalog { ]; min: Sig<["any"], "$1", Agg>; max: Sig<["any"], "$1", Agg>; - any_value: Sig<["any"], "$1", Agg>; + any_value: ESig<["any"], "$1", { agg: true; poly: true }>; array_agg: ESig<["any"], "$arr1", AggElems>; string_agg: [ Sig<["bytea", "bytea"], "bytea", Agg>, @@ -169,23 +176,32 @@ export interface FunctionCatalog { xmlagg: Sig<["xml"], "xml", Agg>; row_number: Sig<[], "int8", Win>; - rank: [Sig<[], "int8", Win>, Sig<["any", "..."], "int8", AggNN>]; - dense_rank: [Sig<[], "int8", Win>, Sig<["any", "..."], "int8", AggNN>]; - percent_rank: [Sig<[], "float8", Win>, Sig<["any", "..."], "float8", AggNN>]; - cume_dist: [Sig<[], "float8", Win>, Sig<["any", "..."], "float8", AggNN>]; + rank: [Sig<[], "int8", Win>, ESig<["any", "..."], "int8", Hypothetical>]; + dense_rank: [ + Sig<[], "int8", Win>, + ESig<["any", "..."], "int8", Hypothetical>, + ]; + percent_rank: [ + Sig<[], "float8", Win>, + ESig<["any", "..."], "float8", Hypothetical>, + ]; + cume_dist: [ + Sig<[], "float8", Win>, + ESig<["any", "..."], "float8", Hypothetical>, + ]; ntile: Sig<["int4"], "int4", { window: true }>; lag: [ - Sig<["any"], "$1", WinNullable>, - Sig<["any", "int4"], "$1", WinNullable>, + ESig<["any"], "$1", WinPoly>, + ESig<["any", "int4"], "$1", WinPoly>, Sig<["any", "int4", "any"], "$1", { window: true }>, ]; lead: FunctionCatalog["lag"]; - first_value: Sig<["any"], "$1", WinNullable>; - last_value: Sig<["any"], "$1", WinNullable>; - nth_value: Sig<["any", "int4"], "$1", WinNullable>; + first_value: ESig<["any"], "$1", WinPoly>; + last_value: ESig<["any"], "$1", WinPoly>; + nth_value: ESig<["any", "int4"], "$1", WinPoly>; coalesce: Sig<["..."], "$common", { strict: false }>; - nullif: Sig<["any", "any"], "$common", Nullable>; + nullif: Sig<["any", "any"], "$eq", Nullable>; greatest: Sig<["any", "..."], "$common", { strict: false }>; least: Sig<["any", "..."], "$common", { strict: false }>; grouping: Sig<["..."], "int4", NN>; @@ -334,7 +350,11 @@ export interface FunctionCatalog { unistr: Sig<["anystring"], "text">; normalize: OptionalArgs<["anystring"], ["anystring"], "text">; is_normalized: OptionalArgs<["anystring"], ["anystring"], "bool">; - pg_collation_for: Sig<["any"], "text", NN>; + pg_collation_for: ESig< + ["any"], + "text", + { nullable: false; untypedNull: true } + >; convert_from: Sig<["bytea", "anystring"], "text">; convert_to: Sig<["anystring", "anystring"], "bytea">; convert: Sig<["bytea", "anystring", "anystring"], "bytea">; @@ -660,8 +680,8 @@ export interface FunctionCatalog { to_json: Sig<["any"], "$jval:json">; to_jsonb: Sig<["any"], "$jval:jsonb">; - json_build_object: Sig<["..."], "json", NN>; - jsonb_build_object: Sig<["..."], "jsonb", NN>; + json_build_object: ESig<["..."], "json", { nullable: false; object: true }>; + jsonb_build_object: ESig<["..."], "jsonb", { nullable: false; object: true }>; json_build_array: Sig<["..."], "$jtup:json", NN>; jsonb_build_array: Sig<["..."], "$jtup:jsonb", NN>; json_object: OptionalArgs<["text[]"], ["text[]"], "json">; @@ -713,10 +733,38 @@ export interface FunctionCatalog { jsonb_path_query_tz: FunctionCatalog["jsonb_path_query"]; jsonb_path_query_array_tz: FunctionCatalog["jsonb_path_query_array"]; jsonb_path_query_first_tz: FunctionCatalog["jsonb_path_query_first"]; - json_array_elements: Sig<["json"], "$jelem:json", Srf>; - jsonb_array_elements: Sig<["jsonb"], "$jelem:jsonb", Srf>; - json_array_elements_text: Sig<["json"], "text", SrfNullable>; - jsonb_array_elements_text: Sig<["jsonb"], "text", SrfNullable>; + json_array_elements: ESig< + ["json"], + "$jelem:json", + { srf: true; column: "value" } + >; + jsonb_array_elements: ESig< + ["jsonb"], + "$jelem:jsonb", + { srf: true; column: "value" } + >; + json_array_elements_text: ESig< + ["json"], + "text", + { srf: true; nullable: true; column: "value" } + >; + jsonb_array_elements_text: ESig< + ["jsonb"], + "text", + { srf: true; nullable: true; column: "value" } + >; + json_each: ESig<["json"], "record", { srf: true; cols: KeyValue<"json"> }>; + jsonb_each: ESig<["jsonb"], "record", { srf: true; cols: KeyValue<"jsonb"> }>; + json_each_text: ESig< + ["json"], + "record", + { srf: true; cols: KeyValue<"text", true> } + >; + jsonb_each_text: ESig< + ["jsonb"], + "record", + { srf: true; cols: KeyValue<"text", true> } + >; json_object_keys: Sig<["json"], "text", Srf>; jsonb_object_keys: Sig<["jsonb"], "text", Srf>; row_to_json: OptionalArgs<["any"], ["bool"], "$jval:json">; @@ -724,10 +772,10 @@ export interface FunctionCatalog { jsonb_populate_record: Sig<["any", "jsonb"], "$1">; json_populate_recordset: OptionalArgs<["any", "json"], ["bool"], "$1", Srf>; jsonb_populate_recordset: Sig<["any", "jsonb"], "$1", Srf>; - json_to_record: Sig<["json"], "record">; - jsonb_to_record: Sig<["jsonb"], "record">; - json_to_recordset: Sig<["json"], "record", Srf>; - jsonb_to_recordset: Sig<["jsonb"], "record", Srf>; + json_to_record: ESig<["json"], "record", { coldefs: true }>; + jsonb_to_record: ESig<["jsonb"], "record", { coldefs: true }>; + json_to_recordset: ESig<["json"], "record", { srf: true; coldefs: true }>; + jsonb_to_recordset: ESig<["jsonb"], "record", { srf: true; coldefs: true }>; jsonb_exists: Sig<["jsonb", "anystring"], "bool">; jsonb_exists_any: Sig<["jsonb", "text[]"], "bool">; jsonb_exists_all: Sig<["jsonb", "text[]"], "bool">; @@ -738,6 +786,7 @@ export interface FunctionCatalog { Sig<["jsonb", "anystring"], "jsonb">, Sig<["jsonb", "int4"], "jsonb">, Sig<["jsonb", "text[]"], "jsonb">, + Sig<["jsonb", "anystring", "..."], "jsonb">, ]; jsonb_delete_path: Sig<["jsonb", "text[]"], "jsonb">; json_scalar: Sig<["any"], "json">; @@ -900,8 +949,8 @@ export type MatchKind

= A extends : "c" : P extends "any" | "..." ? "c" - : P extends keyof PolyCat - ? A extends PolyCat[P] + : P extends keyof PolyAccepts + ? A extends PolyAccepts[P] ? "x" : false : A extends P @@ -914,40 +963,27 @@ export type MatchKind

= A extends ? false : "c" : false - : P extends keyof ImplicitFrom - ? A extends ImplicitFrom[P] - ? "c" + : P extends keyof PgTypeFacts + ? PgTypeFacts[P] extends { readonly from: infer F } + ? A extends F + ? "c" + : false : false : false; -type PolyCat = { - anynumeric: NumT; - anystring: StrT; - anyarray: `${string}[]`; - anyjson: JsonT; - anydatetime: DateTimeT; - anyrange: RangeT; -}; -type ImplicitFrom = { - int4: "int2"; - int8: "int2" | "int4"; - numeric: IntT; - float4: IntT | "numeric"; - float8: IntT | "numeric" | "float4"; - text: StrT; - varchar: StrT; - bpchar: StrT; - timestamptz: "date" | "timestamp"; - timestamp: "date"; - timetz: "time"; - interval: "time"; - inet: "cidr"; - oid: IntT; - regclass: IntT; - regtype: IntT; - regproc: IntT; - regconfig: IntT; +/** + * The catalog's argument categories: the types each accepts, and the type a + * parameter passed for it gets (`unknown` when no one type fits; anyarray's + * follows the other arguments). + */ +type PolyAccepts = { [P in keyof PolyArgs]: PolyArgs[P]["accepts"] }; +export type PolyArgs = { + anynumeric: { accepts: NumT; param: "float8" }; + anystring: { accepts: StrT; param: "text" }; + anyarray: { accepts: `${string}[]`; param: "unknown" }; + anyjson: { accepts: JsonT; param: "unknown" }; + anydatetime: { accepts: DateTimeT; param: "timestamptz" }; + anyrange: { accepts: RangeT; param: "unknown" }; }; - type ArgsMatch< Ps extends readonly string[], As extends readonly ColInfo[], @@ -1033,14 +1069,17 @@ type TypeAt< As extends readonly ColInfo[], I extends number, > = As[I] extends ColInfo ? LitToText : "unknown"; +/** The common type of the arguments, or `Unmatched` for the first two that have none. */ export type CommonOf< As extends readonly ColInfo[], - Acc extends string, + Acc extends string = "unknown", > = As extends readonly [ infer A extends ColInfo, ...infer R extends readonly ColInfo[], ] - ? CommonOf> + ? SelectCommon extends infer T extends string + ? CommonOf + : SelectCommon : LitToText; type ElemOf = T extends `${infer E}[]` @@ -1101,8 +1140,10 @@ type Ret< ? ElemOf> : R extends "$arr1" ? ArrOf> - : R extends "$common" - ? CommonOf + : R extends "$common" | "$eq" + ? CommonOf extends infer T extends string + ? T + : "unknown" : R extends "$rangeelem1" ? RangeElem> : R extends "$multirange1" @@ -1140,15 +1181,15 @@ export type AllNullable = As extends readonly [ ? AllNullable : false : true; -type Prev = { 1: 0; 2: 1; 3: 2; 4: 3; 5: 4; 6: 5; 7: 6; 8: 7 }; +type ArgAt = [never, ...As][I]; type AnyNullableAt< As extends readonly ColInfo[], Idx extends readonly number[], > = Idx extends readonly [ - infer I extends keyof Prev, + infer I extends number, ...infer R extends readonly number[], ] - ? As[Prev[I]] extends { readonly nullable: true } + ? ArgAt extends { readonly nullable: true } ? true : AnyNullableAt : false; @@ -1182,16 +1223,14 @@ type CallNullable< : AnyNullable; type CallInfo< - Name extends string, S extends AnySig, As extends readonly ColInfo[], C extends CallCtx, > = ArrInfo< Ret, - NullableCall extends true ? true : ElemRetNull, + NullableCall extends true ? true : ElemRetNull, RetElemsNull >; -/** Whether the elements of the array a call returns can be NULL. */ type RetElemsNull< S extends AnySig, As extends readonly ColInfo[], @@ -1204,10 +1243,10 @@ type ElemsNullable< E extends true | readonly number[], As extends readonly ColInfo[], > = E extends readonly [ - infer I extends keyof Prev, + infer I extends number, ...infer R extends readonly number[], ] - ? As[Prev[I]] extends infer A extends ColInfo + ? ArgAt extends infer A extends ColInfo ? ElemNull extends true ? true : ElemsNullable @@ -1223,7 +1262,7 @@ type PassedElems< ? HasNullElems : R extends keyof RetArg ? HasNullElems - : R extends "$common" + : R extends "$common" | "$eq" ? HasNullElems extends false ? false : true @@ -1234,19 +1273,30 @@ type ElemRetNull< As extends readonly ColInfo[], > = S["ret"] extends "$elem1" ? HasNullElems : false; -/** COLLATION FOR returns NULL for an argument of type unknown. */ type NullableCall< - Name extends string, S extends AnySig, As extends readonly ColInfo[], C extends CallCtx, -> = Name extends "pg_collation_for" +> = S["untypedNull"] extends true ? As extends readonly [{ readonly type: "lit" | "null" | "unknown" }] ? true : CallNullable : CallNullable; -type SchemaFunctions = DB extends { readonly $functions: infer F } ? F : {}; +/** The functions whose odd arguments are object keys (json_build_object). */ +export type ObjectBuilders = { + [K in keyof FunctionCatalog]: FunctionCatalog[K] extends { + readonly object: true; + } + ? K + : never; +}[keyof FunctionCatalog]; + +/** The names LookupFn finds, which an unknown name's hint picks from. */ +export type FunctionName = + | keyof FunctionCatalog + | keyof SchemaFunctions + | keyof PgFunctions; export type LookupFn< DB, @@ -1292,54 +1342,20 @@ export type CallType< : PickSig, As> extends infer S extends AnySig ? CallMisuse extends infer M extends string ? SqlError - : UnknownPolymorphic extends true + : [C["untypedFirst"], S["poly"]] extends [true, true] ? SqlError< "could not determine polymorphic type because input has type unknown", null > : S["ret"] extends "$common" - ? CommonMismatch extends infer M extends string - ? SqlError - : CallInfo - : CallInfo - : Info<"unknown", true>; - -/** An anyelement argument that only an untyped literal or NULL fills. */ -type UnknownPolymorphic< - Name extends string, - As extends readonly ColInfo[], - C extends CallCtx, -> = C["untypedFirst"] extends true ? PolymorphicArity : false; -type PolymorphicArity< - Name extends string, - As extends readonly ColInfo[], -> = Name extends - | "first_value" - | "last_value" - | "nth_value" - | "any_value" - | "lag" - | "lead" - ? As["length"] extends 1 | 2 - ? true - : false - : false; - -/** PostgreSQL's select_common_type error for coalesce, greatest and least. */ -type CommonMismatch< - Name extends string, - As extends readonly ColInfo[], - Acc extends string, -> = Name extends "coalesce" | "greatest" | "least" - ? As extends readonly [ - infer A extends ColInfo, - ...infer R extends readonly ColInfo[], - ] - ? Unmatchable extends true - ? `${Uppercase} types ${TypeDisplay} and ${TypeDisplay} cannot be matched` - : CommonMismatch> - : null - : null; + ? CommonOf extends string + ? CallInfo + : UnmatchedErr, CommonOf> + : CallInfo + : SqlError< + `function ${Name}(${ArgList}) does not exist`, + null + >; type CallMisuse< Name extends string, @@ -1351,7 +1367,7 @@ type CallMisuse< ? AggMisuse : S["ret"] extends "$wg" | "$wgarr" | "$pct" | "$pctarr" ? `function ${Name}(${ArgList}) does not exist` - : Name extends "rank" | "dense_rank" | "percent_rank" | "cume_dist" + : S["hypothetical"] extends true ? `WITHIN GROUP is required for ordered-set aggregate ${Name}` : AggMisuse : S["window"] extends true diff --git a/packages/core/src/postgres/engine/typer/operators.ts b/packages/core/src/postgres/engine/typer/operators.ts index 8a21868e..1b0e4504 100644 --- a/packages/core/src/postgres/engine/typer/operators.ts +++ b/packages/core/src/postgres/engine/typer/operators.ts @@ -1,53 +1,47 @@ +import type { Or } from "../../../engine/object.js"; import type { ColInfo, Info } from "../../../engine/types.js"; +import type { SqlError } from "../../../errors.js"; +import type { + BitT, + FloatT, + IntT, + JsonT, + NumT, + RangeT, + StrT, + TimeT, + TsT, + TypeFact, +} from "../../schema/type-facts.js"; import type { LitCommon } from "../json.js"; -import type { ArrInfo, BaseOf, ElemNull, ShapedCommon } from "../shape.js"; +import type { + ArithOp, + CompareOp, + JsonGetOp, + JsonTextOp, + MatchOp, +} from "../ops.js"; +import type { + ArrInfo, + BaseOf, + ElemNull, + LitToText, + ShapedCommon, + TypeDisplay, +} from "../shape.js"; -export type IntT = "int2" | "int4" | "int8"; -type FloatT = "float4" | "float8"; -export type NumT = IntT | FloatT | "numeric"; -export type StrT = "text" | "varchar" | "bpchar" | "char" | "name" | "citext"; -export type JsonT = "json" | "jsonb"; -type TsT = "timestamp" | "timestamptz"; -type TimeT = "time" | "timetz"; -export type DateTimeT = "date" | TimeT | TsT; -type BitT = "bit" | "varbit"; -export type RangeT = `${string}range`; type UnknownT = "unknown" | "null"; -/** - * Categories that never share a common type with another category, so CASE, - * COALESCE, GREATEST and LEAST over two of them fail in PostgreSQL. Other types - * (oid and reg*, domains, enums, arrays, …) may convert, so they never count. - */ -type MatchCategory = T extends NumT - ? "numeric" - : T extends StrT - ? "string" - : T extends "bool" - ? "boolean" - : T extends DateTimeT - ? "datetime" - : T extends "interval" - ? "timespan" - : T extends "bytea" | "uuid" - ? T - : null; +/** Two types of different categories, which no operator compares. */ export type Unmatchable = - MatchCategory extends infer CA extends string - ? MatchCategory extends infer CB extends string + TypeFact extends infer CA extends string + ? TypeFact extends infer CB extends string ? CA extends CB ? false : true : false : false; type LitT = "lit"; -export type LitToText = T extends LitT ? "text" : T; - -export type Or = A extends true - ? true - : B extends true - ? true - : false; type NumPromote = "float8" extends L | R ? "float8" @@ -65,16 +59,6 @@ type NumPromote = "float8" extends L | R ? "int4" : "int2"; -type NumCommon = "float8" extends L | R - ? "float8" - : "float4" extends L | R - ? "float4" - : "numeric" extends L | R - ? "numeric" - : "int8" extends L | R - ? "int8" - : "int4"; - type Norm = A extends "null" | LitT ? "unknown" : A; export type BinOpType< @@ -112,7 +96,7 @@ type BinOpNullable< Op extends string, L extends ColInfo, R extends ColInfo, -> = Op extends "->" | "->>" | "#>" | "#>>" +> = Op extends JsonGetOp | JsonTextOp ? true : Op extends "@@" ? L["type"] extends "jsonb" @@ -124,21 +108,21 @@ type BinOpResult< Op extends string, L extends string, R extends string, -> = Op extends "=" | "<" | ">" | "<=" | ">=" | "<>" | "!=" +> = Op extends CompareOp ? "bool" : Op extends "and" | "or" ? "bool" - : Op extends "~" | "~*" | "!~" | "!~*" | "~~" | "~~*" | "!~~" | "!~~*" + : Op extends MatchOp ? "bool" - : Op extends "+" | "-" | "*" | "/" | "%" + : Op extends ArithOp ? Arith : Op extends "||" ? Concat - : Op extends "->" | "#>" + : Op extends JsonGetOp ? L extends JsonT ? BaseOf : "unknown" - : Op extends "->>" | "#>>" + : Op extends JsonTextOp ? "text" : Op extends "#-" ? "jsonb" @@ -531,7 +515,12 @@ export type UnOpType = Op extends "not" ? Info<"bool", E["nullable"]> : Info<"unknown", true>; -export type CommonLit = A extends B +/** + * PostgreSQL's select_common_type for two types: a literal or NULL takes the + * other's type; otherwise the first type is kept when the second converts to + * it, and the second taken when only the first converts (TypeFact "from"). + */ +type CommonLit = A extends B ? ShapedCommon : A extends UnknownT ? B @@ -541,227 +530,47 @@ export type CommonLit = A extends B ? LitCommon : B extends LitT ? LitCommon - : A extends NumT - ? B extends NumT - ? NumCommon - : "unknown" - : A extends StrT - ? B extends StrT - ? StrCommon - : "unknown" - : A extends BitT - ? B extends BitT - ? A + : B extends TypeFact + ? A + : A extends TypeFact + ? B + : A extends `${infer AE}[]` + ? B extends `${infer BE}[]` + ? CommonLit extends infer E extends string + ? E extends "unknown" + ? "unknown" + : `${LitToText}[]` + : never : "unknown" - : A extends "timestamptz" - ? B extends TsT | "date" - ? "timestamptz" - : "unknown" - : A extends "timestamp" - ? B extends "timestamptz" - ? "timestamptz" - : B extends "date" - ? "timestamp" - : "unknown" - : A extends "date" - ? B extends TsT - ? B - : "unknown" - : A extends "time" - ? B extends "timetz" - ? "timetz" - : "unknown" - : A extends "timetz" - ? B extends "time" - ? "timetz" - : "unknown" - : A extends "cidr" - ? B extends "inet" - ? "inet" - : "unknown" - : A extends "inet" - ? B extends "cidr" - ? "inet" - : "unknown" - : A extends `${infer AE}[]` - ? B extends `${infer BE}[]` - ? CommonLit extends infer E extends - string - ? E extends "unknown" - ? "unknown" - : `${LitToText}[]` - : never - : "unknown" - : ShapedCommon; + : ShapedCommon; -type StrCommon = A extends "char" - ? "unknown" - : B extends "char" - ? "unknown" - : A extends "citext" - ? B extends "text" | "varchar" - ? B - : "unknown" - : B extends "citext" - ? A extends "text" | "varchar" - ? A - : "unknown" - : B extends "name" - ? A extends "text" - ? "text" - : "name" - : A; +/** + * PostgreSQL's select_common_type for CASE, COALESCE, GREATEST, LEAST, ARRAY[], + * VALUES, UNION/INTERSECT/EXCEPT and CYCLE: the type both values convert to, + * or `Unmatched` when there is none. + */ +export type SelectCommon = + CommonLit extends infer T extends string + ? T extends "unknown" + ? A extends UnknownT + ? T + : B extends UnknownT + ? T + : Unmatched + : T + : never; +type Unmatched = { + readonly unmatched: readonly [A, B]; +}; +/** PostgreSQL's error for `Unmatched` types in a `What` construct. */ +export type UnmatchedErr = + U extends Unmatched + ? SqlError< + `${What} types ${TypeDisplay} and ${TypeDisplay} cannot be matched`, + null + > + : never; export type CommonType = LitToText< CommonLit >; - -export type NumLitType = V extends `-${infer D}` - ? UnsignedLitType, 1> - : UnsignedLitType, 0>; -type NoUnderscores = V extends `${infer A}_${infer B}` - ? NoUnderscores<`${A}${B}`> - : V; - -type UnsignedLitType = D extends - | `0x${infer H}` - | `0X${infer H}` - ? IntLitType, IntBounds["hex"][Sign]> - : D extends `0o${infer O}` | `0O${infer O}` - ? IntLitType - : D extends `0b${infer B}` | `0B${infer B}` - ? IntLitType - : D extends - | `${string}.${string}` - | `${string}e${string}` - | `${string}E${string}` - ? "numeric" - : IntLitType; - -type IntBounds = { - dec: [ - ["2147483647", "9223372036854775807"], - ["2147483648", "9223372036854775808"], - ]; - hex: [["7fffffff", "7fffffffffffffff"], ["80000000", "8000000000000000"]]; - oct: [ - ["17777777777", "777777777777777777777"], - ["20000000000", "1000000000000000000000"], - ]; - bin: [ - [ - "1111111111111111111111111111111", - "111111111111111111111111111111111111111111111111111111111111111", - ], - [ - "10000000000000000000000000000000", - "1000000000000000000000000000000000000000000000000000000000000000", - ], - ]; -}; - -type IntLitType = - ValueCmp, Max[0]> extends "gt" - ? ValueCmp, Max[1]> extends "gt" - ? "numeric" - : "int8" - : "int4"; -type NoZeros = D extends `0${infer R}` ? NoZeros : D; -type ValueCmp = - LengthCmp extends "eq" ? DigitsCmp : LengthCmp; -type LengthCmp< - A extends string, - B extends string, -> = A extends `${string}${infer AR}` - ? B extends `${string}${infer BR}` - ? LengthCmp - : "gt" - : B extends "" - ? "eq" - : "lt"; -type DigitsCmp = [A, B] extends [ - `${infer AC}${infer AR}`, - `${infer BC}${infer BR}`, -] - ? AC extends BC - ? DigitsCmp - : DigitRank[BC & keyof DigitRank] extends [ - ...DigitRank[AC & keyof DigitRank], - unknown, - ...unknown[], - ] - ? "lt" - : "gt" - : "eq"; -type DigitRank = { - "0": []; - "1": [0]; - "2": [0, 0]; - "3": [0, 0, 0]; - "4": [0, 0, 0, 0]; - "5": [0, 0, 0, 0, 0]; - "6": [0, 0, 0, 0, 0, 0]; - "7": [0, 0, 0, 0, 0, 0, 0]; - "8": [0, 0, 0, 0, 0, 0, 0, 0]; - "9": [0, 0, 0, 0, 0, 0, 0, 0, 0]; - a: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - b: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - c: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - d: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - e: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - f: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; -}; - -export type IntLitValue = - Lowercase< - NoUnderscores - > extends `0${infer P extends keyof Radix}${infer Ds}` - ? FromRadix - : FromRadix>, Ten, Digit, "0">; -type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; -type Ten = DigitRank["a"]; -type Radix = { - x: [...DigitRank["f"], 0]; - o: DigitRank["8"]; - b: DigitRank["2"]; -}; -type FromRadix< - Ds extends string, - R extends unknown[], - Dg extends keyof DigitRank, - Rev extends string, -> = Ds extends `${infer C extends Dg}${infer Rest}` - ? FromRadix> - : Ds extends "" - ? Reverse - : null; -type MulAdd< - Rev extends string, - R extends unknown[], - Carry extends unknown[], - Out extends string, -> = Rev extends `${infer D extends Digit}${infer Rest}` - ? DivMod10<[...Times, ...Carry], []> extends [ - infer Q extends unknown[], - infer M extends number, - ] - ? MulAdd - : never - : Carry extends [] - ? Out - : MulAdd<"0", R, Carry, Out>; -type Times< - N extends unknown[], - R extends unknown[], - Acc extends unknown[], -> = N extends [unknown, ...infer NR] ? Times : Acc; -type DivMod10 = T extends [ - ...Ten, - ...infer Rest, -] - ? DivMod10 - : [Q, T["length"]]; -type Reverse< - S extends string, - Acc extends string, -> = S extends `${infer C}${infer R}` ? Reverse : Acc; diff --git a/packages/core/src/postgres/engine/typer/params.ts b/packages/core/src/postgres/engine/typer/params.ts index 3ecb13f0..6b358f74 100644 --- a/packages/core/src/postgres/engine/typer/params.ts +++ b/packages/core/src/postgres/engine/typer/params.ts @@ -1,13 +1,6 @@ import type { NullTest, ParamEntry } from "../../../engine/params.js"; -import type { - ColInfo, - CteMap, - Ctx, - Info, - MkCtx, - QueryContext, - ScopeLevel, -} from "../../../engine/types.js"; +import type { ColInfo, Info, ScopeLevel } from "../../../engine/types.js"; +import type { DateTimeT } from "../../schema/type-facts.js"; import type { Call, Expr, @@ -21,11 +14,14 @@ import type { WindowSpec, XmlExpr, } from "../ast.js"; -import type { BindFrom } from "../binder/from.js"; +import type { Grouped, SetsLevel } from "../binder/clauses.js"; +import type { BindFrom, SampleMethods } from "../binder/from.js"; +import type { CompareOp, MatchOp } from "../ops.js"; import type { ParamsOfWith } from "../project/dml.js"; import type { BindWith } from "../project/select.js"; -import type { AnySig, LookupFn, PickSig } from "./catalog.js"; -import type { CommonType, DateTimeT } from "./operators.js"; +import type { CteMap, Ctx, MkCtx, QueryContext } from "../types.js"; +import type { AnySig, LookupFn, PickSig, PolyArgs } from "./catalog.js"; +import type { CommonType } from "./operators.js"; import type { ParamsOfJsonTable, ParamsOfSqlJson, @@ -41,24 +37,6 @@ type NullTested = ? ParamEntry : never; -type IsComparison = Op extends - | "=" - | "<" - | ">" - | "<=" - | ">=" - | "<>" - | "!=" - | "~" - | "~*" - | "!~" - | "!~*" - | "~~" - | "~~*" - | "!~~" - | "!~~*" - ? true - : false; type NullableOf = T extends ColInfo ? Mk : Mk<"unknown", true>; @@ -319,7 +297,7 @@ type BinParamType = Other extends { readonly type: infer OT; readonly nullable: boolean; } - ? IsComparison extends true + ? Op extends CompareOp | MatchOp ? Mk : Op extends "and" | "or" ? Info<"bool", true> @@ -579,15 +557,11 @@ type PatternType

= P extends "any" | "..." : "unknown", true > - : P extends "anystring" | "jsonpath" - ? Info<"text", true> - : P extends "anynumeric" - ? Info<"float8", true> - : P extends "anydatetime" - ? Info<"timestamptz", true> - : P extends "anyrange" - ? Info<"unknown", true> - : Mk; + : P extends keyof PolyArgs + ? Info + : P extends "jsonpath" + ? Info<"text", true> + : Mk; export type ParamsOfTargets< Ts extends readonly Target[], @@ -617,11 +591,9 @@ type ParamsOfFromItem = F["args"], C, Info< - F["method"] extends "system_rows" - ? "int8" - : F["method"] extends "system_time" - ? "float8" - : "float4" + F["method"] extends keyof SampleMethods + ? SampleMethods[F["method"]] + : "float4" > > | ParamsOfOpt> @@ -673,7 +645,13 @@ type ParamsOfBody< Env["db"], [L, ...Env["scope"]], Env["ctes"], - B["groupBy"] extends null ? false : true + Grouped + >, + MkCtx< + Env["db"], + [SetsLevel, ...Env["scope"]], + Env["ctes"], + Grouped >, SkipTargets, Order @@ -681,6 +659,7 @@ type ParamsOfBody< : ParamsOfSimple< Extract, MkCtx, + MkCtx, SkipTargets, Order > @@ -705,8 +684,10 @@ type ParamsOfRows< Rows extends readonly (readonly Expr[])[], C extends Ctx, > = ParamsOfOpt; +/** `C0` is the scope of FROM and WHERE, `C` the grouped rows' of the rest. */ type ParamsOfSimple< B extends SimpleSelect, + C0 extends Ctx, C extends Ctx, SkipTargets extends boolean = false, Order extends readonly Expr[] = [], @@ -714,11 +695,12 @@ type ParamsOfSimple< | ParamsOfOrder | ParamsOfOrder | ParamsOfWins - | ParamsOfFromItems + | ParamsOfFromItems | (SkipTargets extends true ? never : ParamsOfTargets) - | ParamsOfOpt> + | ParamsOfOpt> + | ParamsOfOpt> | ParamsOfList< B["groupBy"] extends readonly Expr[] ? B["groupBy"] : [], - C, + C0, null >; diff --git a/packages/core/src/postgres/engine/typer/pg-functions.ts b/packages/core/src/postgres/engine/typer/pg-functions.ts index 3a8d0d67..0733b615 100644 --- a/packages/core/src/postgres/engine/typer/pg-functions.ts +++ b/packages/core/src/postgres/engine/typer/pg-functions.ts @@ -1,4 +1,4 @@ -import type { Sig } from "./catalog.js"; +import type { Sig } from "../../schema/functions.js"; export interface PgFunctions { acldefault: Sig<["..."], "aclitem[]">; @@ -70,11 +70,7 @@ export interface PgFunctions { isparallel: Sig<["..."], "bool">; isperp: Sig<["..."], "bool">; isvertical: Sig<["..."], "bool">; - json_each: Sig<["..."], "record", { srf: true }>; - json_each_text: Sig<["..."], "record", { srf: true }>; json_to_tsvector: Sig<["..."], "tsvector">; - jsonb_each: Sig<["..."], "record", { srf: true }>; - jsonb_each_text: Sig<["..."], "record", { srf: true }>; jsonb_populate_record_valid: Sig<["..."], "bool", { nullable: true }>; jsonb_to_tsvector: Sig<["..."], "tsvector">; lgamma: Sig<["..."], "float8">; diff --git a/packages/core/src/postgres/engine/typer/sqljson.ts b/packages/core/src/postgres/engine/typer/sqljson.ts index 032d329b..15878c83 100644 --- a/packages/core/src/postgres/engine/typer/sqljson.ts +++ b/packages/core/src/postgres/engine/typer/sqljson.ts @@ -1,12 +1,8 @@ -import type { - ColInfo, - Ctx, - Info, - MkCtx, - QueryContext, - ScopeLevel, -} from "../../../engine/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Or } from "../../../engine/object.js"; +import type { ColInfo, Info } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; +import type { DateTimeT, JsonT, NumT } from "../../schema/type-facts.js"; import type { Expr, FromJsonTable, @@ -17,12 +13,9 @@ import type { SelectStmt, Tag, } from "../ast.js"; -import type { TypeDisplay } from "../binder/clauses.js"; -import type { CheckAlias, RenamedLevel } from "../binder/from.js"; import type { BuildObject, JsonLitCast, JsonRet } from "../json.js"; -import type { Nx } from "../parser/util.js"; -import type { BaseOf } from "../shape.js"; -import type { DateTimeT, NumT, Or } from "./operators.js"; +import type { BaseOf, TypeDisplay } from "../shape.js"; +import type { Ctx } from "../types.js"; import type { ParamsOfList, ParamsOfOpt, ParamsOfSelect } from "./params.js"; import type { FirstColOf, TypeList, TypeOf } from "./typeof.js"; @@ -50,7 +43,7 @@ type NonString = | `${string}range`; type TypeFormat = A extends ColInfo - ? BaseOf extends "json" | "jsonb" + ? BaseOf extends JsonT ? A : A["type"] extends NonString ? SqlError< @@ -71,7 +64,7 @@ type TypeIsJson = A extends ColInfo type AggResult = A extends ColInfo ? Ret extends string - ? Ret extends "json" | "jsonb" + ? Ret extends JsonT ? A : Info : A @@ -142,9 +135,9 @@ type CheckKeys< As extends readonly ColInfo[], I extends number, > = As[I] extends ColInfo - ? As[I]["type"] extends "json" | "jsonb" | "record" | `${string}[]` + ? As[I]["type"] extends JsonT | "record" | `${string}[]` ? SqlError<"key value must be scalar, not array, composite, or json", null> - : CheckKeys>> + : CheckKeys>> : true; type NullableValue< As extends readonly ColInfo[], @@ -152,7 +145,7 @@ type NullableValue< > = As[I] extends ColInfo ? As[I]["nullable"] extends true ? true - : NullableValue>> + : NullableValue>> : false; type TypeObject< @@ -169,7 +162,7 @@ type TypeObject< As > extends infer O ? O extends ColInfo - ? R extends "json" | "jsonb" + ? R extends JsonT ? Absent extends true ? NullableValue extends true ? Info @@ -185,7 +178,7 @@ type TypeArray< R extends string, As extends readonly ColInfo[], Absent, -> = R extends "json" | "jsonb" +> = R extends JsonT ? Absent extends true ? AnyNull extends true ? Info @@ -194,7 +187,7 @@ type TypeArray< : Info; type TypeArrayQuery = A extends ColInfo - ? Info : R, true> + ? Info : R, true> : A; export type ParamsOfSqlJson = @@ -214,45 +207,6 @@ type ParamsOfJsonFn = Info<"text", true> >; -export type BindJsonTable< - J extends FromJsonTable["jt"], - Alias extends string | null, - CA extends readonly string[] | null, - Env extends QueryContext, - Sofar extends ScopeLevel, -> = - J["path"] extends Tag<"str"> - ? DuplicateName extends infer D extends string - ? SqlError<`duplicate JSON_TABLE column or path name: ${D}`, null> - : TypeList< - [J["ctx"], ...J["passing"], ...J["defs"]], - MkCtx, - [] - > extends infer Ts - ? Ts extends readonly ColInfo[] - ? CheckAlias< - Alias extends string ? Alias : "json_table", - Sofar - > extends infer A - ? A extends string - ? RenamedLevel - : A - : never - : Ts - : never - : SqlError< - "only string constants are supported in JSON_TABLE path specification", - null - >; -type DuplicateName = Ns extends readonly [ - infer N, - ...infer R extends readonly string[], -] - ? N extends Seen - ? N - : DuplicateName - : null; - export type ParamsOfJsonTable< J extends FromJsonTable["jt"], C extends Ctx, diff --git a/packages/core/src/postgres/engine/typer/typeof.ts b/packages/core/src/postgres/engine/typer/typeof.ts index 2da1a5d6..eb95641b 100644 --- a/packages/core/src/postgres/engine/typer/typeof.ts +++ b/packages/core/src/postgres/engine/typer/typeof.ts @@ -1,15 +1,21 @@ import type { NarrowKeys, NarrowLevel } from "../../../engine/narrow.js"; +import type { Next } from "../../../engine/next.js"; +import type { IsUnion, Or } from "../../../engine/object.js"; import type { ColInfo, ColMap, Cols, - Ctx, Info, - MkCtx, Scope, ScopeLevel, } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; +import type { + JsonT, + KnownType, + StrT, + TypeFact, +} from "../../schema/type-facts.js"; import type { Call, CaseWhen, @@ -20,33 +26,31 @@ import type { VariadicArg, XmlExpr, } from "../ast.js"; -import type { HasStar, TypeDisplay } from "../binder/clauses.js"; +import type { HasStar } from "../binder/clauses.js"; import type { ResolveColumn, WholeRow } from "../binder/resolve.js"; +import type { SchemaTypes } from "../db.js"; import type { BuildObject, JsonArrow, JsonLitCast, LitType } from "../json.js"; -import type { Nx } from "../parser/util.js"; -import type { - NoSubscript, - UnionToIntersection, - WidthCheck, -} from "../project/dml.js"; +import type { NumLitType } from "../literals.js"; +import type { CompareOp, JsonGetOp, NonBoolOp } from "../ops.js"; import type { ProjectSelect } from "../project/select.js"; import type { ArrInfo, BaseOf, ElemNull, HasNullElems, + LitToText, ObjShape, Shaped, + TypeDisplay, } from "../shape.js"; +import type { Ctx, MkCtx } from "../types.js"; +import type { WidthCheck } from "../util.js"; import type { AnyNullable, CallType, CommonOf } from "./catalog.js"; import type { BinOpType, - CommonLit, - LitToText, - NumLitType, - Or, - StrT, + SelectCommon, Unmatchable, + UnmatchedErr, UnOpType, } from "./operators.js"; import type { SqlJsonExpr, TypeSqlJson } from "./sqljson.js"; @@ -80,7 +84,7 @@ export type TypeOf = readonly t: "param"; readonly n: infer N; } - ? Info> + ? ArrInfo, true> : E["e"] extends { readonly t: "str"; readonly v: infer V extends string; @@ -259,7 +263,7 @@ type TypeQuant< > = A extends ColInfo ? B extends ColInfo ? R extends "expr" - ? B["type"] extends ScalarType + ? B["type"] extends KnownType ? SqlError<"op ANY/ALL (array) requires array on right side", null> : QuantOp : QuantOp @@ -271,29 +275,7 @@ type QuantOp< B extends ColInfo, > = Op extends NonBoolOp ? SqlError<"op ANY/ALL (array) requires operator to yield boolean", null> - : Info<"bool", Or>; -type ScalarType = - | "int2" - | "int4" - | "int8" - | "numeric" - | "float4" - | "float8" - | "bool" - | "text" - | "varchar" - | "bpchar" - | "date" - | "time" - | "timetz" - | "timestamp" - | "timestamptz" - | "interval" - | "uuid" - | "bytea" - | "json" - | "jsonb"; -type NonBoolOp = "+" | "-" | "*" | "/" | "%" | "^" | "||"; + : Info<"bool", Or, HasNullElems>>; type StringResult = A extends ColInfo ? B extends ColInfo | null @@ -397,7 +379,7 @@ type PairwiseOp< ? NoCompare extends infer Err extends AnyErr ? Err : BinOpType["type"] extends "bool" | "unknown" - ? PairwiseOp> + ? PairwiseOp> : SqlError< `row comparison operator must yield type boolean, not type ${TypeDisplay["type"]>}`, null @@ -411,7 +393,6 @@ type PairwiseOp< `could not determine interpretation of row comparison operator ${Op}`, null >; -type CompareOp = "=" | "<>" | "!=" | "<" | ">" | "<=" | ">="; /** * PostgreSQL has no comparison operator between two of the categories that * never share a common type, and casts neither side to the other implicitly. @@ -457,7 +438,7 @@ type TypeBinOperands< ? TypeOperand extends infer RT extends ColInfo ? NoCompare extends infer Err extends AnyErr ? Err - : Op extends "->" | "#>" + : Op extends JsonGetOp ? JsonArrow extends infer J extends ColInfo ? J : BinOpType @@ -504,8 +485,8 @@ type CastItemsNull = Items extends readonly Expr[] type TypeOfCast = TypeOf extends infer T extends ColInfo ? ArrInfo< - To extends "json" | "jsonb" - ? T["type"] extends Shaped<"json" | "jsonb", infer S> + To extends JsonT + ? T["type"] extends Shaped ? Shaped : To : To, @@ -513,7 +494,7 @@ type TypeOfCast = HasNullElems > : TypeOf; -type LitCast = To extends "json" | "jsonb" +type LitCast = To extends JsonT ? JsonLitCast extends infer J extends string ? J : To @@ -545,7 +526,9 @@ type TypeArrayCtor< > = Items extends readonly [] ? EmptyArrayErr : TypeArrayItems; type TypeArrayItems = TypeList extends infer Ls extends readonly ColInfo[] - ? ArrInfo>, false, AnyElemNull> + ? CommonOf extends infer T extends string + ? ArrInfo, false, AnyElemNull> + : UnmatchedErr<"ARRAY", CommonOf> : TypeList; /** A NULL item is a NULL element; a NULL sub-array adds no elements. */ type AnyElemNull = Ls extends readonly [ @@ -571,16 +554,13 @@ type TypeIdx = ? ArrInfo<`${El}[]`, Or, HasNullElems> : never : Info - : BaseOf extends NoSubscript | EnumName - ? NoSubscriptErr - : JsonArrow<"->", A, I, B> extends infer J extends ColInfo - ? J - : Info< - BaseOf extends keyof SubscriptElem - ? SubscriptElem[BaseOf] - : "unknown", - true - > + : SubscriptOf extends infer E + ? E extends AnyErr + ? E + : JsonArrow<"->", A, I, B> extends infer J extends ColInfo + ? J + : Info + : never : TypeOf : TypeOf; type ChainOverArray = @@ -610,25 +590,25 @@ type ChainNullable = E extends { : TypeOf extends infer T ? NullableOpt : never; -type EnumName = DB extends { readonly $types: infer T } ? keyof T : never; type ChainSliced = E extends Tag<"slice"> ? true : E extends { readonly t: "idx"; readonly e: infer X } ? ChainSliced : false; -type SubscriptElem = { - jsonb: "jsonb"; - point: "float8"; - line: "float8"; - lseg: "point"; - box: "point"; - name: "char"; -}; -type NoSubscriptErr = SqlError< - `cannot subscript type ${TypeDisplay} because it does not support subscripting`, - null ->; +/** + * What a subscript of type T gives (its `sub` fact, null when the table has + * none), or PostgreSQL's error for a type without subscripting, enums included. + */ +type SubscriptOf = + TypeFact, "sub", null> extends infer E + ? [E, BaseOf] extends [false, string] | [unknown, keyof SchemaTypes] + ? SqlError< + `cannot subscript type ${TypeDisplay} because it does not support subscripting`, + null + > + : E + : never; type TypeSlice< E extends Expr, Lo extends Expr | null, @@ -646,19 +626,21 @@ type TypeSlice< HasNullElems > : never - : BaseOf extends NoSubscript | EnumName - ? NoSubscriptErr - : BaseOf extends "jsonb" - ? SqlError<"jsonb subscript does not support slices", null> - : BaseOf extends keyof SubscriptElem - ? SqlError< - "slices of fixed-length arrays not implemented", - null - > - : Info< - A["type"], - Or, NullableOpt>> - > + : SubscriptOf extends infer E + ? E extends AnyErr + ? E + : BaseOf extends "jsonb" + ? SqlError<"jsonb subscript does not support slices", null> + : E extends string + ? SqlError< + "slices of fixed-length arrays not implemented", + null + > + : Info< + A["type"], + Or, NullableOpt>> + > + : never : TypeOpt : TypeOpt : TypeOf; @@ -742,7 +724,6 @@ type StarCol = [keyof M] extends [ : IsUnion extends true ? SqlError : M[keyof M]; -type IsUnion = [U] extends [UnionToIntersection] ? false : true; type TypeCase< Operand extends Expr | null, @@ -788,18 +769,9 @@ type TypeWhens< ] ? TypeOf extends ColInfo ? TypeOperand extends infer TT extends ColInfo - ? Unmatchable extends true - ? SqlError< - `CASE types ${TypeDisplay} and ${TypeDisplay} cannot be matched`, - null - > - : TypeWhens< - R, - C, - CommonLit, - Or, - Or> - > + ? SelectCommon extends infer T2 extends string + ? TypeWhens, Or>> + : UnmatchedErr<"CASE", SelectCommon> : TypeOperand : TypeOf : readonly [T, N, NE]; diff --git a/packages/core/src/postgres/engine/typer/xml.ts b/packages/core/src/postgres/engine/typer/xml.ts index c2ee2557..036d60e1 100644 --- a/packages/core/src/postgres/engine/typer/xml.ts +++ b/packages/core/src/postgres/engine/typer/xml.ts @@ -1,7 +1,8 @@ -import type { ColInfo, Cols, Ctx, Info } from "../../../engine/types.js"; +import type { ColInfo, Cols, Info } from "../../../engine/types.js"; import type { SqlError } from "../../../errors.js"; import type { Expr, Tag, XmlExpr, XmlTableCol } from "../ast.js"; -import type { TypeDisplay } from "../binder/clauses.js"; +import type { TypeDisplay } from "../shape.js"; +import type { Ctx } from "../types.js"; import type { AllNullable } from "./catalog.js"; import type { ParamsOfList, ParamsOfOpt } from "./params.js"; import type { TypeList, TypeOf } from "./typeof.js"; diff --git a/packages/core/src/postgres/engine/types.ts b/packages/core/src/postgres/engine/types.ts new file mode 100644 index 00000000..2ef8b3f6 --- /dev/null +++ b/packages/core/src/postgres/engine/types.ts @@ -0,0 +1,34 @@ +// The scope model of the PostgreSQL engine. +import type { ColMap, Scope } from "../../engine/types.js"; + +export type CteMap = { readonly [name: string]: ColMap }; + +export type Ctx = { + readonly db: unknown; + readonly scope: Scope; + readonly ctes: CteMap; + readonly grouped: boolean; +}; + +export type MkCtx< + DB, + S extends Scope, + Ctes extends CteMap, + G extends boolean, +> = { + readonly db: DB; + readonly scope: S; + readonly ctes: Ctes; + readonly grouped: G; +}; + +/** The environment of query binding, projection and parameter inference. */ +export type QueryContext< + DB = unknown, + S extends Scope = Scope, + Ctes = CteMap, +> = { + readonly db: DB; + readonly scope: S; + readonly ctes: Ctes; +}; diff --git a/packages/core/src/postgres/engine/util.ts b/packages/core/src/postgres/engine/util.ts new file mode 100644 index 00000000..6bed53cb --- /dev/null +++ b/packages/core/src/postgres/engine/util.ts @@ -0,0 +1,27 @@ +// Generic type helpers the PostgreSQL engine shares across its layers. + +/** "eq", "more" or "fewer": whether A has as many items as B. */ +export type WidthCheck< + A extends readonly unknown[], + B extends readonly unknown[], +> = A extends unknown + ? B extends unknown + ? number extends A["length"] | B["length"] + ? WidthCheckWalk + : A["length"] extends B["length"] + ? B["length"] extends A["length"] + ? "eq" + : WidthCheckWalk + : WidthCheckWalk + : never + : never; +type WidthCheckWalk< + A extends readonly unknown[], + B extends readonly unknown[], +> = A extends readonly [unknown, ...infer AR] + ? B extends readonly [unknown, ...infer BR] + ? WidthCheckWalk + : "more" + : B extends readonly [unknown, ...unknown[]] + ? "fewer" + : "eq"; diff --git a/packages/core/src/postgres/engine/write.ts b/packages/core/src/postgres/engine/write.ts index 2876bbe0..68b33d79 100644 --- a/packages/core/src/postgres/engine/write.ts +++ b/packages/core/src/postgres/engine/write.ts @@ -1,11 +1,11 @@ -import type * as Shared from "../../engine/write.js"; +import type * as Shared from "../../sql/write.js"; import type { PgEngine } from "./engine.js"; export type { RelationName, TableName, TableOf, -} from "../../engine/write.js"; +} from "../../schema.js"; export type SetInput = Shared.SetInput< PgEngine, diff --git a/packages/core/src/postgres/index.ts b/packages/core/src/postgres/index.ts index 2bb657fc..53be90f5 100644 --- a/packages/core/src/postgres/index.ts +++ b/packages/core/src/postgres/index.ts @@ -2,10 +2,10 @@ export { type Postgres, type PostgresDialect, postgres, - type TransactionalSql, + type ValidateDdl, } from "./dialect.js"; export type { ApplyDdl, ParamsOf, RowOf, Validate } from "./engine/infer.js"; -export type { FunctionCatalog, Sig } from "./engine/typer/catalog.js"; +export type { FunctionCatalog } from "./engine/typer/catalog.js"; export type { InsertInput, RelationName, @@ -13,18 +13,19 @@ export type { TableName, TableOf, } from "./engine/write.js"; -export type { ValidateDdl } from "./migrations.js"; export type { Col, Enum, Json, PgCatalog, + Sig, View, } from "./schema/index.js"; -export type { Sql } from "./sql/types.js"; +export type { Sql, TransactionalSql } from "./sql/types.js"; export type { DefaultParamTypeMap, DefaultTypeMap, + NodePostgresTypeMap, ParamTypeMapOf, TsParamTypeOf, TypeMap, diff --git a/packages/core/src/postgres/migrations.ts b/packages/core/src/postgres/migrations.ts deleted file mode 100644 index 2d934f19..00000000 --- a/packages/core/src/postgres/migrations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AnyErr, SqlError } from "../errors.js"; -import type { ApplyDdl } from "./engine/infer.js"; - -export type ValidateDdl = string extends Q - ? SqlError< - "migrations need a string literal; use .raw() for SQL the type checker cannot see", - null - > - : ApplyDdl extends infer R - ? R extends AnyErr - ? R - : Q - : never; diff --git a/packages/core/src/postgres/schema/column.ts b/packages/core/src/postgres/schema/column.ts index 9a6e01e0..b980ef0d 100644 --- a/packages/core/src/postgres/schema/column.ts +++ b/packages/core/src/postgres/schema/column.ts @@ -2,21 +2,18 @@ import type { Col, ColDesc, Namespace, - Shaped, Table, - TsShape, + Typed, TypeRegistry, View, } from "../../schema.js"; -import type { Overloads } from "../engine/typer/catalog.js"; +import type { FunctionRegistry } from "./functions.js"; export type Json< T, Flags extends Partial = {}, Base extends "json" | "jsonb" = "jsonb", -> = Col>, Flags>; - -export type FunctionRegistry = { readonly [name: string]: Overloads }; +> = Col, Flags>; export type Schema = { readonly [key: string]: diff --git a/packages/core/src/postgres/schema/functions.ts b/packages/core/src/postgres/schema/functions.ts new file mode 100644 index 00000000..ad29b811 --- /dev/null +++ b/packages/core/src/postgres/schema/functions.ts @@ -0,0 +1,28 @@ +/** Behaviour a declared function can state besides its signature. */ +export type SigFlags = { + readonly strict?: boolean | readonly number[]; + readonly nullable?: boolean; + readonly agg?: boolean; + readonly window?: boolean; + readonly srf?: boolean; +}; + +export type Sig< + Args extends readonly string[], + Ret extends string, + Flags extends SigFlags = {}, +> = { + readonly args: Args; + readonly ret: Ret; +} & Flags; + +/** Any signature a schema can declare. */ +export type DeclaredSig = { + readonly args: readonly string[]; + readonly ret: string; +} & SigFlags; + +/** The functions a schema declares under `$functions`: a signature or overloads. */ +export type FunctionRegistry = { + readonly [name: string]: DeclaredSig | readonly DeclaredSig[]; +}; diff --git a/packages/core/src/postgres/schema/index.ts b/packages/core/src/postgres/schema/index.ts index f408b1f0..7a545390 100644 --- a/packages/core/src/postgres/schema/index.ts +++ b/packages/core/src/postgres/schema/index.ts @@ -7,7 +7,8 @@ export type { TypeRegistry, View, } from "../../schema.js"; -export type { FunctionRegistry, Json, Schema } from "./column.js"; +export type { Json, Schema } from "./column.js"; +export type { FunctionRegistry, Sig, SigFlags } from "./functions.js"; export type { NormalizeTypeName } from "./normalize.js"; export type { InformationSchemaNamespace, diff --git a/packages/core/src/postgres/schema/normalize.ts b/packages/core/src/postgres/schema/normalize.ts index 2a69b353..bd1db7be 100644 --- a/packages/core/src/postgres/schema/normalize.ts +++ b/packages/core/src/postgres/schema/normalize.ts @@ -1,30 +1,5 @@ -type TypeAliases = { - int: "int4"; - integer: "int4"; - serial: "int4"; - serial4: "int4"; - smallint: "int2"; - smallserial: "int2"; - serial2: "int2"; - bigint: "int8"; - bigserial: "int8"; - serial8: "int8"; - real: "float4"; - "double precision": "float8"; - float: "float8"; - decimal: "numeric"; - dec: "numeric"; - boolean: "bool"; - citext: "text"; - "character varying": "varchar"; - character: "bpchar"; - char: "bpchar"; - "time without time zone": "time"; - "time with time zone": "timetz"; - "timestamp without time zone": "timestamp"; - "timestamp with time zone": "timestamptz"; - "bit varying": "varbit"; -}; +import type { IsRelation } from "../../schema.js"; +import type { TypeAliases } from "./type-facts.js"; export type NormalizeTypeName = S extends `${infer E}[]` ? `${NormalizeTypeName}[]` @@ -32,14 +7,6 @@ export type NormalizeTypeName = S extends `${infer E}[]` ? TypeAliases[S] : S; -export type SerialTypeName = - | "serial" - | "serial4" - | "smallserial" - | "serial2" - | "bigserial" - | "serial8"; - /** Type spellings that differ from the canonical name (`integer`, `bool`, …). */ type AliasSpelling = keyof TypeAliases; @@ -53,14 +20,6 @@ type IsAlias = T extends string type SkippedKey = `$${string}` | "pg_catalog" | "information_schema"; -type IsRelation = R extends { readonly $view: unknown } - ? true - : [keyof R] extends [never] - ? true - : R[keyof R] extends { readonly type: unknown } - ? true - : false; - type ColsHaveAlias = true extends { [C in keyof R]: R[C] extends { readonly type: infer T } ? IsAlias : false; }[keyof R] diff --git a/packages/core/src/postgres/schema/type-facts.ts b/packages/core/src/postgres/schema/type-facts.ts new file mode 100644 index 00000000..8e896c17 --- /dev/null +++ b/packages/core/src/postgres/schema/type-facts.ts @@ -0,0 +1,227 @@ +export type IntT = "int2" | "int4" | "int8"; +export type FloatT = "float4" | "float8"; +export type NumT = IntT | FloatT | "numeric"; +export type StrT = "text" | "varchar" | "bpchar" | "char" | "name"; +export type JsonT = "json" | "jsonb"; +export type TsT = "timestamp" | "timestamptz"; +export type TimeT = "time" | "timetz"; +export type DateTimeT = "date" | TimeT | TsT; +export type BitT = "bit" | "varbit"; +export type RangeT = `${string}range`; +type RegT = "regclass" | "regtype" | "regproc" | "regconfig"; + +/** + * What PostgreSQL's catalog says about each type the engine knows, read by + * indexed access (`TypeFact`): + * - `name`: how format_type spells it in PostgreSQL's messages; + * - `from`: the types of its category (pg_type.typcategory) that convert to + * it implicitly (pg_cast), which is how a common type is found; + * - `cat`: a category no other category converts to, so two values of + * different categories have no comparison operator; + * - `sub`: what a subscript returns, or false where PostgreSQL rejects one; + * - `eq: false`: no equality operator for hashing or sorting (CYCLE marks); + * - `json`: the JSON value to_json gives for it. + * `lit` and `null` are the engine's untyped literal and NULL. + */ +export interface PgTypeFacts { + bool: { + cat: "boolean"; + name: "boolean"; + sub: false; + json: "boolean"; + }; + int2: { + cat: "numeric"; + name: "smallint"; + sub: false; + json: "number"; + }; + int4: { + cat: "numeric"; + name: "integer"; + from: "int2"; + sub: false; + json: "number"; + }; + int8: { + cat: "numeric"; + name: "bigint"; + from: "int2" | "int4"; + sub: false; + json: "number"; + }; + float4: { + cat: "numeric"; + name: "real"; + from: IntT | "numeric"; + sub: false; + json: "number"; + }; + float8: { + cat: "numeric"; + name: "double precision"; + from: IntT | "numeric" | "float4"; + sub: false; + json: "number"; + }; + numeric: { + cat: "numeric"; + from: IntT; + sub: false; + json: "number"; + }; + money: { sub: false; json: "string" }; + oid: { from: IntT | RegT; sub: false; json: "string" }; + regclass: { from: IntT | "oid"; sub: false; json: "string" }; + regtype: { from: IntT | "oid"; sub: false; json: "string" }; + regproc: { from: IntT | "oid"; sub: false; json: "string" }; + regconfig: { from: IntT | "oid"; json: "string" }; + text: { + cat: "string"; + from: "varchar" | "bpchar" | "name"; + sub: false; + json: "string"; + }; + varchar: { + cat: "string"; + name: "character varying"; + from: "text" | "bpchar"; + sub: false; + json: "string"; + }; + bpchar: { + cat: "string"; + name: "character"; + from: "text" | "varchar"; + sub: false; + json: "string"; + }; + char: { cat: "string"; name: '"char"'; sub: false; json: "string" }; + name: { + cat: "string"; + from: "text" | "varchar" | "bpchar"; + sub: "char"; + json: "string"; + }; + bytea: { cat: "bytea"; sub: false; json: "string" }; + date: { cat: "datetime"; sub: false; json: "string" }; + time: { + cat: "datetime"; + name: "time without time zone"; + sub: false; + json: "string"; + }; + timetz: { + cat: "datetime"; + name: "time with time zone"; + from: "time"; + sub: false; + json: "string"; + }; + timestamp: { + cat: "datetime"; + name: "timestamp without time zone"; + from: "date"; + sub: false; + json: "string"; + }; + timestamptz: { + cat: "datetime"; + name: "timestamp with time zone"; + from: "date" | "timestamp"; + sub: false; + json: "string"; + }; + interval: { cat: "timespan"; sub: false; json: "string" }; + uuid: { cat: "uuid"; sub: false; json: "string" }; + json: { sub: false; eq: false }; + jsonb: { sub: "jsonb" }; + jsonpath: { eq: false; json: "string" }; + xml: { sub: false; eq: false; json: "string" }; + inet: { from: "cidr"; sub: false; json: "string" }; + cidr: { sub: false; json: "string" }; + macaddr: { from: "macaddr8"; sub: false; json: "string" }; + macaddr8: { from: "macaddr"; sub: false; json: "string" }; + bit: { from: "varbit"; sub: false; json: "string" }; + varbit: { + name: "bit varying"; + from: "bit"; + sub: false; + json: "string"; + }; + point: { sub: "float8"; eq: false; json: "string" }; + line: { sub: "float8"; eq: false; json: "string" }; + lseg: { sub: "point"; eq: false; json: "string" }; + box: { sub: "point"; eq: false; json: "string" }; + path: { sub: false; eq: false; json: "string" }; + polygon: { sub: false; eq: false; json: "string" }; + circle: { sub: false; eq: false; json: "string" }; + tsvector: { sub: false; json: "string" }; + tsquery: { sub: false; json: "string" }; + int4range: { sub: false }; + int8range: { sub: false }; + numrange: { sub: false }; + tsrange: { sub: false }; + tstzrange: { sub: false }; + daterange: { sub: false }; + pg_snapshot: { eq: false }; + txid_snapshot: { eq: false }; + refcursor: { eq: false }; + lit: { name: "text" }; + null: { name: "unknown" }; +} + +type Facts = { + readonly name?: string; + readonly cat?: string; + readonly from?: string; + readonly sub?: string | false; + readonly eq?: false; + readonly json?: string; +}; + +/** A fact about type T, or `D` when the table has none. */ +export type TypeFact< + T extends string, + K extends keyof Facts, + D = null, +> = T extends keyof PgTypeFacts + ? PgTypeFacts[T] extends { readonly [P in K]: infer V } + ? V + : D + : D; + +/** The serial pseudo-types: a column of the integer type with a sequence default. */ +export interface SerialAliases { + serial: "int4"; + serial4: "int4"; + smallserial: "int2"; + serial2: "int2"; + bigserial: "int8"; + serial8: "int8"; +} + +/** Other spellings of types in SQL and schemas, with their canonical names. */ +export interface TypeAliases extends SerialAliases { + int: "int4"; + integer: "int4"; + smallint: "int2"; + bigint: "int8"; + real: "float4"; + "double precision": "float8"; + float: "float8"; + decimal: "numeric"; + dec: "numeric"; + boolean: "bool"; + citext: "text"; + "character varying": "varchar"; + character: "bpchar"; + "time without time zone": "time"; + "time with time zone": "timetz"; + "timestamp without time zone": "timestamp"; + "timestamp with time zone": "timestamptz"; + "bit varying": "varbit"; +} + +/** The types in the table, as opposed to arrays, records and unknown types. */ +export type KnownType = Exclude; diff --git a/packages/core/src/postgres/sql/types.ts b/packages/core/src/postgres/sql/types.ts index 270cfe20..ab014a0b 100644 --- a/packages/core/src/postgres/sql/types.ts +++ b/packages/core/src/postgres/sql/types.ts @@ -1,6 +1,7 @@ import type { DIALECT, DialectBrand, StrictSql } from "../../dialect.js"; import type { SqlPlugin } from "../../plugin.js"; import type { SqlApi } from "../../sql/api.js"; +import type { Transactional } from "../../transaction.js"; import type { PgEngine } from "../engine/engine.js"; import type { NormalizeSchema } from "../schema/normalize.js"; @@ -17,4 +18,21 @@ export interface Sql< readonly [DIALECT]?: DialectBrand<"postgres", DB, TM, Plugins>; /** The database this `sql` runs on: `"postgres"`. */ readonly dialect: "postgres"; + /** This `sql` with `plugin` after its plugins. */ + withPlugin( + plugin: P, + ): Sql; +} + +/** An `sql` with `transaction(fn)`, as adapters create it. */ +export interface TransactionalSql< + in out DB, + in out TM, + in out Plugins extends readonly SqlPlugin[] = [], +> extends Sql, + Transactional> { + readonly [DIALECT]?: DialectBrand<"postgres", DB, TM, Plugins>; + withPlugin( + plugin: P, + ): TransactionalSql; } diff --git a/packages/core/src/postgres/type-map/index.ts b/packages/core/src/postgres/type-map/index.ts index 88e27447..00769c67 100644 --- a/packages/core/src/postgres/type-map/index.ts +++ b/packages/core/src/postgres/type-map/index.ts @@ -1,6 +1,14 @@ -import type { TypeRegistry } from "../../schema.js"; -import type { JsonTs } from "../engine/json.js"; -import type { AnyShaped, BaseOf, NullElems, Shaped } from "../engine/shape.js"; +import type { ColInfo } from "../../engine/types.js"; +import type { JsonAs, NullableTs, ParamMapOf, TsShape } from "../../schema.js"; +import type { SchemaTypes } from "../engine/db.js"; +import type { + AnyShaped, + BaseOf, + NullElems, + Shaped, + ShapeOfType, +} from "../engine/shape.js"; +import type { RangeT, TypeFact } from "../schema/type-facts.js"; export interface DefaultTypeMap { bool: boolean; @@ -34,6 +42,7 @@ export interface DefaultTypeMap { uuid: string; json: unknown; jsonb: unknown; + jsonpath: string; xml: string; inet: string; cidr: string; @@ -45,6 +54,7 @@ export interface DefaultTypeMap { regclass: string; regtype: string; regproc: string; + regconfig: string; point: { x: number; y: number }; line: string; lseg: string; @@ -63,6 +73,9 @@ export interface DefaultTypeMap { xid: string; xid8: string; pg_lsn: string; + pg_snapshot: string; + txid_snapshot: string; + refcursor: string; aclitem: string; int2vector: string; oidvector: string; @@ -72,31 +85,49 @@ export interface DefaultTypeMap { unknown: unknown; } +/** + * What node-postgres returns with its default parsers (pg-types), as the + * migrator's `pg` client and `@typesql/pg` read rows. `bytea` is a Node + * `Buffer`, typed here as the `Uint8Array` it extends. + */ +export interface NodePostgresTypeMap + extends Omit { + interval: { + years?: number; + months?: number; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; + toPostgres(): string; + toISO(): string; + toISOString(): string; + }; + record: string; + void: string; + // Array types without a registered parser come back as their text. + "aclitem[]": string; + "char[]": string; +} + // biome-ignore lint/plugin: a type map maps any SQL type name to any TypeScript type. export type TypeMap = { readonly [pgType: string]: unknown }; -export type EnumsOf = DB extends { - readonly $types: infer R extends TypeRegistry; -} - ? R - : {}; - export type TsTypeOf = T extends AnyShaped ? ShapedTs : T extends keyof TM ? TM[T] : T extends `${infer E}[]` ? TsTypeOf[] - : T extends keyof EnumsOf - ? EnumsOf[T] + : T extends keyof SchemaTypes + ? SchemaTypes[T] : unknown; type ShapedTs = T extends Shaped<"json" | "jsonb", infer S> ? BaseOf extends infer B extends keyof TM - ? [TM[B]] extends [string] - ? TM[B] - : JsonTs + ? JsonAs> : JsonTs : TsTypeOf>; @@ -104,11 +135,7 @@ export type TsOf< TM, DB, T extends { type: string; nullable: boolean }, -> = T["type"] extends "null" - ? null - : T["nullable"] extends true - ? ValueTs | null - : ValueTs; +> = NullableTs>; type ValueTs< TM, DB, @@ -132,9 +159,7 @@ export interface DefaultParamTypeMap { bytea: Uint8Array; } -export type ParamTypeMapOf = TM extends { readonly $params: infer P } - ? P - : DefaultParamTypeMap; +export type ParamTypeMapOf = ParamMapOf; export type TsParamTypeOf = ParamTsOf< TM, @@ -148,6 +173,61 @@ type ParamTsOf = T extends AnyShaped ? TM[T] | (T extends keyof PM ? PM[T] : never) : T extends `${infer E}[]` ? readonly (ParamTsOf | null)[] - : T extends keyof EnumsOf - ? EnumsOf[T] + : T extends keyof SchemaTypes + ? SchemaTypes[T] : unknown; + +/** The TypeScript type of a JSON value of shape S. */ +type JsonTs = S extends { readonly k: "obj"; readonly m: infer M } + ? [keyof M] extends [never] + ? Record + : { + -readonly [K in keyof M]: M[K] extends ColInfo + ? JsonValueTs + : never; + } + : S extends { readonly k: "arr"; readonly e: infer E extends ColInfo } + ? JsonValueTs[] + : S extends { + readonly k: "tup"; + readonly es: infer Es extends readonly ColInfo[]; + } + ? { -readonly [I in keyof Es]: JsonValueTs } + : S extends { readonly k: "rec"; readonly v: infer V extends ColInfo } + ? { [key: string]: JsonValueTs } + : S extends { readonly k: "val"; readonly c: infer C extends ColInfo } + ? JsonValueTs + : S extends TsShape + ? T + : unknown; + +type JsonValueTs = C extends ColInfo + ? C["nullable"] extends true + ? JsonOfInfo | null + : JsonOfInfo + : never; +type JsonOfInfo = C extends NullElems + ? C["type"] extends `${infer E}[]` + ? (JsonOfType | null)[] + : JsonOfType + : JsonOfType; + +type JsonForm = { number: number; boolean: boolean; string: string }; + +type JsonOfType = T extends AnyShaped + ? ShapeOfType extends infer S + ? S extends null + ? unknown + : JsonTs + : never + : TypeFact extends infer F extends keyof JsonForm + ? JsonForm[F] + : T extends "null" + ? null + : T extends `${infer E}[]` + ? JsonOfType[] + : T extends RangeT | `${string}multirange` + ? string + : T extends keyof SchemaTypes + ? SchemaTypes[T] + : unknown; diff --git a/packages/core/src/reserved.ts b/packages/core/src/reserved.ts new file mode 100644 index 00000000..1e86bea8 --- /dev/null +++ b/packages/core/src/reserved.ts @@ -0,0 +1,19 @@ +// Runtime word lists for editor tooling such as @typesql/language-server. The +// parsers read the same lists as types, so a keyword added there is quoted in +// completions too. +import * as mysql from "./mysql/engine/parser/keywords.js"; +import * as postgres from "./postgres/engine/parser/keywords.js"; +import * as sqlite from "./sqlite/engine/parser/keywords.js"; + +export { META_KEYS } from "./schema.js"; + +/** Per dialect, the words a table, column or alias name must be quoted to be. */ +export const RESERVED_WORDS = { + postgres: [ + ...postgres.RESERVED_KEYWORDS, + ...postgres.TYPE_FUNC_NAME_KEYWORDS, + ], + sqlite: [...sqlite.RESERVED, ...sqlite.JOIN_KEYWORDS], + // Either server's, so a name is quoted wherever one of them needs it. + mysql: [...mysql.RESERVED, ...mysql.SINGLE_SERVER_RESERVED], +} as const; diff --git a/packages/core/src/runtime/compile.ts b/packages/core/src/runtime/compile.ts index 5400043e..9952250c 100644 --- a/packages/core/src/runtime/compile.ts +++ b/packages/core/src/runtime/compile.ts @@ -127,18 +127,29 @@ export function bind( } /** - * The tokens compile() reads past or rewrites: quoted strings, identifiers - * and comments (kept as they are), then `$n`, `::`, `:name` and markers. + * The lexis (see `Dialect`) of a dialect that leaves it out: standard SQL's + * strings, quoted names and comments. */ -const TOKEN = - /(? string, + lexis: string, ): string { - const pattern = new RegExp(TOKEN); + const pattern = new RegExp(lexis, "g"); let out = ""; let last = 0; for (let m = pattern.exec(text); m; m = pattern.exec(text)) { @@ -158,9 +169,63 @@ function scan( return out + text.slice(last); } -const renumber = (text: string, offset: number): string => +/** A statement split at its `$n` parameters, and the value each one takes. */ +export interface SplitStatement { + readonly parts: readonly string[]; + readonly values: readonly unknown[]; +} + +/** + * `text` split at the `$n` parameters that `lexis` reads, for drivers that + * bind `?`: each parameter's value, mapped by `map`, once for each use. + */ +export function splitAt( + text: string, + values: readonly V[], + map: (value: V | undefined) => R, + lexis: string, +): SplitStatement { + const parts: string[] = []; + const out: R[] = []; + let last = 0; + scan( + text, + (m) => { + const n = m[1]; + if (n) { + parts.push(text.slice(last, m.index)); + out.push(map(values[+n - 1])); + last = m.index + m[0].length; + } + return ""; + }, + lexis, + ); + parts.push(text.slice(last)); + return { parts, values: out }; +} + +/** SQL text with `?` placeholders, and the value for each. */ +export interface PositionalStatement { + readonly text: string; + readonly values: unknown[]; +} + +/** `text` with `?` for each `$n` parameter that `lexis` reads (see splitAt). */ +export function positionalAt( + text: string, + values: readonly V[], + map: (value: V | undefined) => R, + lexis: string, +): PositionalStatement { + if (!text.includes("$")) return { text, values: [] }; + const split = splitAt(text, values, map, lexis); + return { text: split.parts.join("?"), values: [...split.values] }; +} + +const renumber = (text: string, offset: number, lexis: string): string => offset && text.includes("$") - ? scan(text, (m) => (m[2] ? `$${+m[2] + offset}` : m[0])) + ? scan(text, ([t, n]) => (n ? `$${+n + offset}` : t), lexis) : text; /** @@ -168,40 +233,54 @@ const renumber = (text: string, offset: number): string => * (positional or, with `:name` parameters, one object) first, then each * inline sql.param() in order, then each interpolated fragment's. Without * `args` the statement is prepared: its own parameters stay for later. + * `lexis` is the dialect's (see `Dialect`). */ -export function compile(text: string, args?: readonly unknown[]): Compiled { +export function compile( + lexis: string, + text: string, + args?: readonly unknown[], +): Compiled { + const tokens = TYPESQL + lexis; const names: string[] = []; let count = 0; let inline = 0; - scan(text, ([, , n, name, id]) => { - if (n) count = Math.max(count, +n); - else if (name) names.includes(name) || names.push(name); - else if (id && id[0] !== "~" && pending.get(id) !== empty) inline++; - return ""; - }); + scan( + text, + ([, name, id, n]) => { + if (n) count = Math.max(count, +n); + else if (name) names.includes(name) || names.push(name); + else if (id && id[0] !== "~" && pending.get(id) !== empty) inline++; + return ""; + }, + tokens, + ); const values = args ? bind(names, args) : []; count = Math.max(count, args ? values.length : names.length); const carried: unknown[] = []; let k = count; let offset = count + inline; - const out = scan(text, ([t, , n, name, id]) => { - if (name) return `$${names.indexOf(name) + 1}`; - if (!id) - return n || !t.includes(MARK) - ? t - : fail("sql.param() or fragment inside a string or comment"); - const value = use(id); - if (id[0] !== "~") { - if (value === empty) return "DEFAULT"; - values.push(value); - return `$${++k}`; - } - // biome-ignore lint/nursery/noUnsafeTypeAssertion: only toString() above marks a "~" id, with a fragment. - const fragment = value as Spliceable; - carried.push(...fragment.values); - offset += fragment.values.length; - return renumber(fragment.text, offset - fragment.values.length); - }); + const out = scan( + text, + ([t, name, id, n]) => { + if (name) return `$${names.indexOf(name) + 1}`; + if (!id) + return n || !t.includes(MARK) + ? t + : fail("sql.param() or fragment inside a string or comment"); + const value = use(id); + if (id[0] !== "~") { + if (value === empty) return "DEFAULT"; + values.push(value); + return `$${++k}`; + } + // biome-ignore lint/nursery/noUnsafeTypeAssertion: only toString() above marks a "~" id, with a fragment. + const fragment = value as Spliceable; + carried.push(...fragment.values); + offset += fragment.values.length; + return renumber(fragment.text, offset - fragment.values.length, lexis); + }, + tokens, + ); if (out.includes("\0")) fail("NUL in SQL text"); return { text: out, values: [...values, ...carried], names, count }; } @@ -216,11 +295,14 @@ export const ident = (quote: string, parts: readonly string[]): string => export function join( items: readonly Spliceable[], separator: string, + lexis: string, ): SqlFragment { const values: unknown[] = []; return new SqlFragment( items - .map((f) => renumber(f.text, values.push(...f.values) - f.values.length)) + .map((f) => + renumber(f.text, values.push(...f.values) - f.values.length, lexis), + ) .join(separator), values, ); diff --git a/packages/core/src/runtime/json.ts b/packages/core/src/runtime/json.ts index 9c5be019..34759246 100644 --- a/packages/core/src/runtime/json.ts +++ b/packages/core/src/runtime/json.ts @@ -19,6 +19,10 @@ export class JsonParam { } } +/** A value as a driver that takes JSON as text binds it: `sql.json()` values as JSON text. */ +export const jsonText = (value: V): V | string => + value instanceof JsonParam ? value.toPostgres() : value; + export function json(value: T): JsonMarker { // The marker would be stored as text, and with it its secret prefix. if (holdsMarkers(value)) fail(HOLDS); diff --git a/packages/core/src/runtime/mysql-params.ts b/packages/core/src/runtime/mysql-params.ts deleted file mode 100644 index f6d52a92..00000000 --- a/packages/core/src/runtime/mysql-params.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { JsonParam } from "./json.js"; -import type { PositionalStatement } from "./sqlite-params.js"; - -/** - * Strings (with backslash escapes), quoted identifiers and comments, which - * can't hold parameters, and `$n`. - */ -const PARAMS = - /'(?:[^'\\]|\\[\s\S]|'')*'?|"(?:[^"\\]|\\[\s\S]|"")*"?|`(?:[^`]|``)*`?|--(?=[\s]|$)[^\n]*|#[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|(?(value: V): V | string { - if (value instanceof JsonParam) return JSON.stringify(value.value); - return value; -} - -/** - * Rewrites TypeSQL's `$1`, `$2`, … to MySQL's positional `?`, repeating a - * value for each use of its parameter. `map` converts each value for the - * driver; mysqlValue by default. - */ -export function mysqlPositional( - text: string, - values: readonly V[], - map?: (value: V | undefined) => R, -): PositionalStatement { - if (!text.includes("$")) return { text, values: [] }; - const out: unknown[] = []; - const rewritten = text.replace(PARAMS, (m: string, n?: string) => { - if (n === undefined) return m; - const value = values[Number(n) - 1]; - out.push(map ? map(value) : mysqlValue(value)); - return "?"; - }); - return { text: rewritten, values: out }; -} diff --git a/packages/core/src/runtime/sqlite-params.ts b/packages/core/src/runtime/sqlite-params.ts deleted file mode 100644 index 89adc3ce..00000000 --- a/packages/core/src/runtime/sqlite-params.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { JsonParam } from "./json.js"; - -const PARAMS = - /'(?:[^']|'')*'|"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[[^\]]*\]|--[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|\$(\d+)/g; - -export function paramNumbers(text: string): number[] { - const seen = new Set(); - for (const m of text.matchAll(PARAMS)) - if (m[1] !== undefined) seen.add(Number(m[1])); - return [...seen]; -} - -export function sqliteValue(value: V): V | string | 0 | 1 { - if (value instanceof JsonParam) return JSON.stringify(value.value); - if (typeof value === "boolean") return value ? 1 : 0; - if (value instanceof Date) - throw new TypeError( - "typesql: SQLite has no date type; pass an ISO string (date.toISOString()) or a number", - ); - return value; -} - -export interface SplitStatement { - readonly parts: readonly string[]; - readonly values: readonly unknown[]; -} - -/** `map` converts each value for the driver; sqliteValue by default. */ -export function splitParams( - text: string, - values: readonly V[], - map?: (value: V | undefined) => R, -): SplitStatement { - const parts: string[] = []; - const out: unknown[] = []; - let last = 0; - for (const m of text.matchAll(PARAMS)) { - if (m[1] === undefined) continue; - parts.push(text.slice(last, m.index)); - const value = values[Number(m[1]) - 1]; - out.push(map ? map(value) : sqliteValue(value)); - last = m.index + m[0].length; - } - parts.push(text.slice(last)); - return { parts, values: out }; -} - -/** SQL text with `?` placeholders, and the value for each. */ -export interface PositionalStatement { - readonly text: string; - readonly values: unknown[]; -} - -export function positional( - text: string, - values: readonly V[], - map?: (value: V | undefined) => R, -): PositionalStatement { - const split = splitParams(text, values, map); - return { text: split.parts.join("?"), values: [...split.values] }; -} diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 925ffd08..9a5fdc54 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -20,7 +20,7 @@ export type ColDesc = { * bivariant, and `Col<"int8", { nullable: true }>` was assignable to * `Col<"int8">` because `{ nullable: true }` is assignable to `{}`. */ -export type ColOptions = { readonly [K in keyof Flags]-?: Flags[K] }; +type ColOptions = { readonly [K in keyof Flags]-?: Flags[K] }; export type Col = {}> = { readonly type: T; @@ -32,13 +32,89 @@ export type Col = {}> = { : false; }; +type IsSet = + ColOptions extends { + readonly [P in K]: true; + } + ? true + : false; +// An unset flag is an optional `never` property, so a column that has the +// flag is not assignable to one that does not. +type Flag = + IsSet extends true + ? { readonly [P in K]: true } + : { readonly [P in K]?: never }; + +/** + * A MySQL or SQLite column: `Col` with its position and `generated` and + * `hidden` flags, and the dialect's own flag `Own` (`autoIncrement`, + * `rowid`), which implies a default where `OwnDefault` is true. A generated + * column has a default too. + */ +export type FlaggedCol< + T extends TypeName, + Flags, + Own extends string, + OwnDefault extends boolean, +> = { + readonly type: T; + readonly nullable: IsSet; + readonly hasDefault: IsSet extends true + ? true + : IsSet extends true + ? true + : OwnDefault extends true + ? IsSet + : false; +} & (ColOptions extends { readonly position: infer P extends number } + ? { readonly i: P } + : { readonly i?: never }) & + Flag & + Flag & + Flag; + export type Enum< Name extends string, Flags extends Partial = {}, > = Col; export type Shaped = B & { readonly shape: S }; +/** The shape of a value the schema gives a TypeScript type: `Typed`, `Json`. */ export type TsShape = { readonly k: "ts"; readonly v: T }; +/** A type whose values are typed `T`. */ +export type TsShaped = { readonly shape: TsShape }; +/** A column of type `Base` whose values are typed `T`. */ +export type Typed = Shaped>; + +/** A type as TypeSQL's messages show it: `json` for `Json`'s. */ +export type ShapeBase = T extends { + readonly shape: infer S; +} + ? // Matching the shape exactly leaves the base for B; matching any shape + // would infer B as all of T. + T extends Shaped + ? B + : T + : T; + +/** + * A JSON value of TypeScript type `T`, read or bound as the type map has a + * plain JSON value (`Plain`): as text where that is a string, else as `T`. + */ +export type JsonAs = [Plain] extends [string] ? Plain : T; + +/** The parameter type map of type map `TM`: its `$params`, or `Default`. */ +export type ParamMapOf = TM extends { + readonly $params: infer P; +} + ? P + : Default; + +/** A column's TypeScript type, given `V` for its non-null values. */ +export type NullableTs< + T extends { readonly type: string; readonly nullable: boolean }, + V, +> = T["type"] extends "null" ? null : T["nullable"] extends true ? V | null : V; export type Table = { readonly [column: string]: ColDesc }; @@ -47,3 +123,67 @@ export type View = { readonly $view: Cols }; export type TypeRegistry = { readonly [name: string]: string }; export type Namespace = { readonly [table: string]: Table | View

}; + +/** + * Keys of a schema that declare types (PostgreSQL's `$types`) and functions + * (`$functions`, in every dialect), not relations. + */ +export const DECLARED_KEYS = ["$types", "$functions"] as const; +/** + * Keys of a schema type that name no relation: the declared ones, the + * null-parameter marker inference adds, and MariaDB's flag. + */ +export const META_KEYS = [...DECLARED_KEYS, "$nullParams", "$flavor"] as const; +export type MetaKey = (typeof META_KEYS)[number]; + +/** Whether a schema entry is a relation (a table or a view) rather than a namespace or a `$` key. */ +export type IsRelation = X extends { readonly $view: unknown } + ? true + : X extends object + ? X[keyof X] extends { readonly type: string } + ? true + : false + : false; + +export type RelCols = X extends { readonly $view: infer C } + ? keyof C & string + : keyof X & string; + +type IsTable = X extends Table ? true : false; + +export type TableName = + | { + [K in keyof DB & string]: IsTable extends true ? K : never; + }[keyof DB & string] + | { + [NS in keyof DB & string]: IsTable extends true + ? never + : DB[NS] extends View
+ ? never + : NsTables; + }[keyof DB & string]; +type NsTables = { + [K in keyof N & string]: IsTable extends true ? `${NS}.${K}` : never; +}[keyof N & string]; + +export type RelationName = { + [K in keyof DB & string]: DB[K] extends View
+ ? K + : IsTable extends true + ? K + : never; +}[keyof DB & string]; + +export type TableOf = T extends keyof DB + ? DB[T] extends Table + ? DB[T] + : never + : T extends `${infer NS}.${infer N}` + ? NS extends keyof DB + ? N extends keyof DB[NS] + ? DB[NS][N] extends Table + ? DB[NS][N] + : never + : never + : never + : never; diff --git a/packages/core/src/sql/api.ts b/packages/core/src/sql/api.ts index 0750b437..8f780db1 100644 --- a/packages/core/src/sql/api.ts +++ b/packages/core/src/sql/api.ts @@ -1,21 +1,10 @@ -import type { - Empty, - EmptyFragment, - Optional, - OptionalInput, - Truthy, -} from "../empty.js"; -import type { EngineTypes } from "../engine/contract.js"; -import type { - InsertInput, - RelationName, - SetInput, - TableName, -} from "../engine/write.js"; +import type { Empty } from "../empty.js"; import type { SqlError } from "../errors.js"; import type { PluginEncoded, PluginInput, SqlPlugin } from "../plugin.js"; -import type { Fragment, Query } from "../query.js"; +import type { Fragment, InlineFragment, Query } from "../query.js"; +import type { RelationName, TableName } from "../schema.js"; import type { BoxCallArgs, CallRet, GateCallCheck } from "./call.js"; +import type { EngineTypes } from "./contract.js"; import type { BoolOf, BoxFragArgs, @@ -33,7 +22,52 @@ import type { } from "./inline.js"; import type { Slottable } from "./slots.js"; import type { QuotedIdent } from "./splice.js"; -import type { NoExtraKeys, SentRows, SetRep, ValuesRep } from "./write.js"; +import type { + InsertInput, + NoExtraKeys, + SameColumns, + SentRows, + SetInput, + SetRep, + ValuesRep, +} from "./write.js"; + +/** + * The omission of an optional fragment or query, which splices nothing as + * `${…}`. Only it carries the string brand a template keeps: a branded + * omission next to a `string` value (`x ?? omission`, `flag ? omission : x`) + * would be reduced away with the DEFAULT it stands for. + */ +export type EmptyFragment = Empty & InlineFragment; + +/** + * `undefined` becomes the omission and `null` stays SQL `NULL`, as in + * sql.set, sql.values and sql.json. + */ +export type Optional = + | Exclude + | ([Extract] extends [never] + ? never + : [Exclude] extends [never] + ? Empty + : [Exclude] extends [Slottable] + ? EmptyFragment + : Empty); + +/** A `null` fragment would splice the text `null`, so it is an error. */ +type OptionalInput = [Extract] extends [never] + ? unknown + : [Exclude] extends [never] + ? unknown + : [Exclude] extends [Slottable] + ? SqlError< + "use undefined, not null, to omit a fragment or query with sql.optional()", + null + > + : unknown; + +/** What `sql.if(value, then)` hands `then`: `value` without its falsy members. */ +type Truthy = Exclude; type Parameters = Extract< PluginInput< @@ -71,28 +105,24 @@ type WriteEncoded< // The write helpers' results are written as Fragment<…> so that declaration // emit can name them from @typesql/core. -type SetText = SetRep>["text"]; -type SetCarried

= SetRep

["carried"]; -type ValuesText = ValuesRep, EmptyRowOf>["text"]; -type ValuesCarried = ValuesRep["carried"]; -/** The identifier quote of a dialect: `"`, or a backtick for MySQL. */ -type QuoteOf = E extends { readonly quote: infer Q extends string } - ? Q - : '"'; -/** How a dialect inserts a row of defaults. */ -type EmptyRowOf = E extends { readonly emptyRow: infer R extends string } - ? R - : "DEFAULT VALUES"; - +type SetText = SetRep["text"]; +type SetCarried = SetRep["carried"]; +type ValuesText = ValuesRep< + R, + E["quote"], + E["emptyRow"] +>["text"]; +type ValuesCarried = ValuesRep< + R, + E["quote"], + E["emptyRow"] +>["carried"]; export interface SqlApi< E extends EngineTypes, DB, TM, Plugins extends readonly SqlPlugin[] = [], > { - withPlugin( - plugin: P, - ): SqlApi; ( query: GateCallCheck & Q, ...params: Parameters, Plugins> @@ -139,16 +169,16 @@ export interface SqlApi< json(value: T & JsonInput): JsonMarker; fragment( - text: GateFragCheck & Text, - ...params: A & Parameters, Plugins> - ): PlainFragment>; + text: GateFragCheck & Text, + ...params: A & Parameters, Plugins> + ): PlainFragment, E["lexis"]>; id( ...parts: Parts & LiteralParts - ): Fragment>, [], []>; + ): Fragment, [], []>; id( name: RelationName, - ): Fragment], QuoteOf>, [], []>; + ): Fragment], E["quote"]>, [], []>; id(...parts: readonly [string, ...string[]]): Fragment; combine< @@ -157,15 +187,15 @@ export interface SqlApi< >( items: Items, options?: { readonly separator?: Sep }, - ): CombineOf; + ): CombineOf; and( items: Items, - ): BoolOf; + ): BoolOf; or( items: Items, - ): BoolOf; + ): BoolOf; set, const P>( table: T, @@ -176,7 +206,7 @@ export interface SqlApi< ): Fragment< SetText, E>, readonly unknown[], - SetCarried> + SetCarried, E> >; /** Checked against the table of the `UPDATE` it is interpolated into. */ set( @@ -184,41 +214,22 @@ export interface SqlApi< ): Fragment< SetText, E>, readonly unknown[], - SetCarried> + SetCarried, E> >; - values, const R extends readonly unknown[]>( - table: T, - rows: R & { - readonly [I in keyof R]: R[I] extends WriteInput< - InsertInput, - Plugins, - "values", - T - > - ? NoExtraKeys< - R[I], - WriteInput, Plugins, "values", T> - > - : never; - }, - ): Fragment< - ValuesText, E>, - readonly unknown[], - ValuesCarried> - >; /** * Checked against the table of the `INSERT` it is interpolated into: once * with every column, and once with only the columns every row always * sends, so a required column a row can leave out is an error there. + * Declared first: a call that fails is reported, and typed, as this one. */ values( - rows: R, + rows: R & SameColumns, ): | Fragment< ValuesText, E>, readonly unknown[], - ValuesCarried> + ValuesCarried, E> > | Fragment< ValuesText< @@ -227,9 +238,31 @@ export interface SqlApi< >, readonly unknown[], ValuesCarried< - SentRows> + SentRows>, + E > >; + values, const R extends readonly unknown[]>( + table: T, + rows: R & + SameColumns & { + readonly [I in keyof R]: R[I] extends WriteInput< + InsertInput, + Plugins, + "values", + T + > + ? NoExtraKeys< + R[I], + WriteInput, Plugins, "values", T> + > + : never; + }, + ): Fragment< + ValuesText, E>, + readonly unknown[], + ValuesCarried, E> + >; raw(text: string): Fragment; } diff --git a/packages/core/src/sql/call.ts b/packages/core/src/sql/call.ts index 0e9bd2c1..393b8366 100644 --- a/packages/core/src/sql/call.ts +++ b/packages/core/src/sql/call.ts @@ -1,12 +1,18 @@ -import type { CheckCarried } from "../engine/carried.js"; -import type { EngineTypes } from "../engine/contract.js"; -import type { ArgsOf, ParamsOf, RowOf, Validate } from "../engine/infer.js"; import type { AnyErr, SqlError } from "../errors.js"; import type { PluginInput, PluginOutput, SqlPlugin } from "../plugin.js"; import type { Query } from "../query.js"; +import type { CheckCarried } from "./carried.js"; +import type { EngineTypes } from "./contract.js"; +import type { ArgsOf, ParamsOf, RowOf, Validate } from "./infer.js"; import type { HasDefaultable, Skip, WithDefaults } from "./inline.js"; -import type { Inlined, InlinedSlots, InlinedText } from "./inline-fragment.js"; -import type { NamedParams } from "./named.js"; +import type { + Inlined, + InlinedSlots, + InlinedText, + QuotedSlot, +} from "./inline-fragment.js"; +import type { Lexis } from "./lexis.js"; +import type { NamedArgs } from "./named.js"; import type { SlotCarried, SlotTexts, SlotVariants } from "./slots.js"; import type { Splice, SpliceCarried, Take } from "./splice.js"; import type { @@ -32,7 +38,7 @@ type ArgsFor< : [S, Encs] extends [null, readonly []] ? ArgsOf : Take, M> - : [params: NamedParams, Names["length"]>>]; + : NamedArgs>; type Carried< E extends EngineTypes, @@ -45,9 +51,12 @@ type Carried< type DefaultsCheck = HasDefaultable extends false ? true - : PText> extends infer X extends string - ? (S extends null ? X : Splice>) extends infer X2 extends - string + : PText, E["lexis"]> extends infer X extends string + ? ( + S extends null + ? X + : Splice, E["lexis"]> + ) extends infer X2 extends string ? Extract, AnyErr> extends infer Err ? [Err] extends [never] ? true @@ -70,10 +79,10 @@ type DefaultErr = * spliced together, a union fragment's parameters would leave the offset of * the next fragment's parameters a union, and mix their numbers. */ -export type SplicedText = +export type SplicedText = SlotVariants> extends infer V ? V extends unknown - ? Splice>, SlotTexts> + ? Splice, L>, SlotTexts, L> : never : never; @@ -81,14 +90,14 @@ export type SplicedText = type SpliceCheck = IsLiteral extends false ? SqlError - : Prepared extends infer P + : Prepared extends infer P ? P extends [ infer T2 extends string, string[], infer Encs extends readonly string[], infer M extends number, ] - ? Spliced extends infer X + ? Spliced extends infer X ? X extends string ? Validate extends AnyErr ? Validate @@ -102,7 +111,11 @@ type SpliceCheck = ...Encs, ...([S] extends [null] ? [] - : SpliceCarried>), + : SpliceCarried< + T2, + SlotCarried, + E["lexis"] + >), ] > extends infer K ? K extends AnyErr @@ -114,9 +127,9 @@ type SpliceCheck = : P : never; -type Spliced = [S] extends [null] +type Spliced = [S] extends [null] ? T2 - : Splice> extends infer X + : Splice, L> extends infer X ? X extends string ? string extends X ? SqlError< @@ -134,13 +147,13 @@ type Spliced = [S] extends [null] type SpliceArgs = IsLiteral extends false ? readonly unknown[] - : Prepared extends [ + : Prepared extends [ infer T2 extends string, infer Names extends string[], infer Encs extends readonly string[], infer M extends number, ] - ? Spliced extends infer X extends string + ? Spliced extends infer X extends string ? Validate extends AnyErr ? readonly unknown[] : ArgsFor @@ -156,13 +169,17 @@ export type GateCallCheck = [ : I extends AnyErr ? I : I extends readonly [infer T2 extends string, infer S2] - ? Surface< - SlotVariants extends infer V - ? V extends unknown - ? SpliceCheck - : never - : never - > + ? [QuotedSlot] extends [infer QE] + ? [QE] extends [never] + ? Surface< + SlotVariants extends infer V + ? V extends unknown + ? SpliceCheck + : never + : never + > + : QE + : never : never : never : never; @@ -192,14 +209,16 @@ export type BoxCallArgs = [ : never; /** The text sql() types: interpolated fragments spliced in. */ -type CallText = [Q] extends [infer Q2 extends string] +type CallText = [Q] extends [ + infer Q2 extends string, +] ? Inlined extends null - ? PText - : SplicedOr + ? PText + : SplicedOr : never; -type SplicedOr = - SplicedText extends infer X extends string ? X : string; +type SplicedOr = + SplicedText extends infer X extends string ? X : string; /** * Resolved to `Query`, so declaration emit names only the @@ -212,7 +231,7 @@ export type CallRet< TM, Q extends string, Plugins extends readonly SqlPlugin[] = [], -> = [CallText] extends [infer X extends string] +> = [CallText] extends [infer X extends string] ? Query< PluginOutput, Plugins>, Extract< diff --git a/packages/core/src/engine/carried.ts b/packages/core/src/sql/carried.ts similarity index 77% rename from packages/core/src/engine/carried.ts rename to packages/core/src/sql/carried.ts index b80c297b..4c9e0dab 100644 --- a/packages/core/src/engine/carried.ts +++ b/packages/core/src/sql/carried.ts @@ -1,6 +1,9 @@ +import type { Next } from "../engine/next.js"; +import type { PickOne } from "../engine/object.js"; +import type { NullTest } from "../engine/params.js"; +import type { Toks } from "../engine/tokens.js"; import type { SqlError } from "../errors.js"; -import type { CheckEnc, DescribeEnc, Enc } from "../sql/inline.js"; -import type { Next } from "../sql/next.js"; +import type { TsShape } from "../schema.js"; import type { AnalysisOf, EngineTypes, @@ -8,10 +11,7 @@ import type { TypeDisplayOf, } from "./contract.js"; import type { ParamTs } from "./infer.js"; -import type { PickOne } from "./object.js"; -import type { NullTest } from "./params.js"; -import type { Nx } from "./parse.js"; -import type { Toks } from "./tokens.js"; +import type { CheckEnc, DescribeEnc, Enc } from "./inline.js"; export type CheckCarried< E extends EngineTypes, @@ -44,7 +44,7 @@ type CarriedLoop< > = I extends K["length"] ? true : K[I] extends infer V extends string - ? CarriedMessage & keyof P]> extends infer M extends + ? CarriedMessage & keyof P]> extends infer M extends string ? [M] extends [never] ? CarriedLoop> @@ -83,10 +83,23 @@ type CheckValue< : CheckEnc extends true ? V extends `${string}[0${string}` ? IsJsonParam> extends true - ? `an array for ${Expected} must be passed as sql.json(value) (node-postgres sends a JS array as a Postgres array)` + ? `an array for ${Expected} must be passed as sql.json(value)${E["arrayNote"]}` : true : true - : `a ${DescribeEnc} value cannot be used as ${Expected}${Hint}`; + : AsJson extends true + ? `a ${DescribeEnc} value for ${Expected} must be passed as sql.json(value)` + : `a ${DescribeEnc} value cannot be used as ${Expected}${Hint}`; + +// A value a JSON parameter takes only as sql.json(): one its declared shape +// takes, or any, without a shape, where the parameter binds JSON text. +type AsJson = + IsJsonParam> extends true + ? [JsonShapes] extends [never] + ? [Exclude] extends [string] + ? true + : false + : CheckEnc> + : false; type JsonError< E extends EngineTypes, @@ -99,7 +112,7 @@ type JsonError< // Inline JSON still carries the original value, so check every declared shape. type JsonShapes = U extends { readonly type: E["json"] & { - readonly shape: { readonly k: "ts"; readonly v: infer V }; + readonly shape: TsShape; }; } ? (value: V) => void diff --git a/packages/core/src/engine/contract.ts b/packages/core/src/sql/contract.ts similarity index 66% rename from packages/core/src/engine/contract.ts rename to packages/core/src/sql/contract.ts index d7812baa..1eb257df 100644 --- a/packages/core/src/engine/contract.ts +++ b/packages/core/src/sql/contract.ts @@ -1,3 +1,5 @@ +import type { Lexis } from "./lexis.js"; + export interface AnalysisFn { readonly db: unknown; readonly q: string; @@ -31,6 +33,16 @@ export interface EngineTypes { readonly param: ParamFn; readonly display: DisplayFn; readonly json: string; + /** Appended to the error for a JS array passed for a JSON parameter: why the driver can't bind it. */ + readonly arrayNote: string; + /** The identifier quote `sql.id`, `sql.set` and `sql.values` write, as the runtime dialect's `quote`. */ + readonly quote: string; + /** What `sql.values` writes for a row of defaults, as the runtime dialect's `emptyRow`. */ + readonly emptyRow: string; + /** Whether `sql.values` may write `DEFAULT` for a column a row leaves out, as the runtime dialect's `valuesDefault`. */ + readonly valuesDefault: boolean; + /** Its text rules, as the runtime dialect's `lexis`. */ + readonly lexis: Lexis; } export type AnalysisOf< diff --git a/packages/core/src/sql/fragment.ts b/packages/core/src/sql/fragment.ts index ee39c33c..cf8bdfdf 100644 --- a/packages/core/src/sql/fragment.ts +++ b/packages/core/src/sql/fragment.ts @@ -1,8 +1,16 @@ import type { Empty } from "../empty.js"; +import type { Tup } from "../engine/num.js"; +import type { Members } from "../engine/object.js"; import type { AnyErr, SqlError } from "../errors.js"; import type { Fragment, TextType } from "../query.js"; import type { HasDefaultable, ValueEnc } from "./inline.js"; -import type { Inlined, InlinedSlots, InlinedText } from "./inline-fragment.js"; +import type { + Inlined, + InlinedSlots, + InlinedText, + QuotedSlot, +} from "./inline-fragment.js"; +import type { Lexis } from "./lexis.js"; import type { CarriedOf, SlotCarried, @@ -10,13 +18,7 @@ import type { Slottable, SlotVariants, } from "./slots.js"; -import type { - JoinedText, - Splice, - SpliceCarried, - UnionList, - UnknownTuple, -} from "./splice.js"; +import type { JoinedText, Splice, SpliceCarried } from "./splice.js"; import type { IsLiteral, NotLiteralMessage, @@ -33,10 +35,10 @@ export type LiteralParts = { : Parts[I]; }; -type FragCheck = +type FragCheck = IsLiteral extends false ? SqlError - : Prepared extends infer P + : Prepared extends infer P ? P extends AnyErr ? P : HasDefaultable extends true @@ -47,31 +49,35 @@ type FragCheck = : true : never; -type FragArgs = - Prepared extends [ +type FragArgs = + Prepared extends [ string, infer Names extends string[], readonly unknown[], infer M extends number, ] ? Names extends [] - ? UnknownTuple + ? Tup : // biome-ignore lint/plugin: named fragment parameters accept any value; the SQL decides their types. [params: { readonly [K in Names[number]]: unknown }] : readonly unknown[]; -type FragCount = - Prepared extends [ +type FragCount = + Prepared extends [ string, string[], infer Encs extends readonly unknown[], infer M extends number, ] - ? [...UnknownTuple, ...Encs]["length"] + ? [...Tup, ...Encs]["length"] : 0; -type FragCarried = - Prepared extends [ +type FragCarried< + Text extends string, + A extends readonly unknown[], + L extends Lexis, +> = + Prepared extends [ string, infer Names extends string[], infer Encs extends readonly string[], @@ -92,21 +98,25 @@ type EncNamed = { type JoinedCarried< Items extends readonly unknown[], + L extends Lexis, Acc extends readonly unknown[] = [], > = Items extends readonly [infer H, ...infer R] ? JoinedCarried< R, + L, [ ...Acc, - ...(CarriedOf extends infer C extends readonly unknown[] ? C : []), + ...(CarriedOf extends infer C extends readonly unknown[] + ? C + : []), ] > : Acc; -type ElemPairs = F extends unknown +type ElemPairs = F extends unknown ? TextType extends infer T extends string ? T extends unknown - ? readonly [Wrap extends true ? Parened : T, CarriedOf] + ? readonly [Wrap extends true ? Parened : T, CarriedOf] : never : never : never; @@ -149,16 +159,19 @@ type PairTexts = { }; }; -export type ListRep = [ +export type ListRep< E, -] extends [never] + Sep extends string, + L extends Lexis, + Wrap extends boolean = false, +> = [E] extends [never] ? { readonly text: string; readonly carried: []; readonly count: number } - : ElemPairs extends infer P + : ElemPairs extends infer P ? AnyDynamic

extends true ? { readonly text: string; readonly carried: []; readonly count: number } - : UnionList

extends infer Ps extends readonly unknown[] + : Members

> extends Ok< + ? ParseExprPrec, Next

> extends Ok< infer E extends Expr, infer J extends number > @@ -552,12 +559,10 @@ type InfixOp< }, Min > - : ParseExprPrec, Next1

> + : ParseExprPrec, Next

> : Ok : Ok; -type Next1

= [...Tup

, 0]["length"] & number; - type LikeOp = "like" | "glob" | "match" | "regexp"; type InfixWord< @@ -568,39 +573,39 @@ type InfixWord< W extends string, > = W extends "or" | "and" ? GE extends true - ? ParseExprPrec, W extends "or" ? 2 : 3> extends Ok< + ? ParseExprPrec, W extends "or" ? 2 : 3> extends Ok< infer E extends Expr, infer J extends number > ? ParseInfix - : ParseExprPrec, W extends "or" ? 2 : 3> + : ParseExprPrec, W extends "or" ? 2 : 3> : Ok : W extends "collate" ? GE extends true - ? Ident> extends infer N extends Name + ? Ident> extends infer N extends Name ? ParseInfix< T, - Nx>, + Next>, { t: "collate"; e: L; collation: N["v"] }, Min > - : T[Nx] extends { + : T[Next] extends { readonly k: "s"; readonly v: infer V extends string; } ? ParseInfix< T, - Nx>, + Next>, { t: "collate"; e: L; collation: Lowercase }, Min > - : SyntaxErr> + : SyntaxErr> : Ok : GE extends true ? W extends "isnull" | "notnull" ? ParseInfix< T, - Nx, + Next, { t: "is"; e: L; @@ -610,25 +615,25 @@ type InfixWord< Min > : W extends "is" - ? ParseIs, L, Min> + ? ParseIs, L, Min> : W extends "not" - ? T[Nx] extends { + ? T[Next] extends { readonly k: "w"; readonly v: infer W2 extends string; } ? W2 extends "null" ? ParseInfix< T, - Nx>, + Next>, { t: "is"; e: L; what: "null"; not: true }, Min > : W2 extends "in" | "between" | LikeOp - ? ParsePredicate>, L, Min, W2, true> + ? ParsePredicate>, L, Min, W2, true> : Ok : Ok : W extends "in" | "between" | LikeOp - ? ParsePredicate, L, Min, W, false> + ? ParsePredicate, L, Min, W, false> : Ok : Ok; @@ -637,13 +642,13 @@ type ParseIs< I extends number, L extends Expr, Min extends number, -> = (IsWord extends true ? [true, Nx] : [false, I]) extends [ +> = (IsWord extends true ? [true, Next] : [false, I]) extends [ infer Not extends boolean, infer J extends number, ] ? IsWord extends true - ? IsWord, "from"> extends true - ? ParseExprPrec>, Prec["cmp"]> extends Ok< + ? IsWord, "from"> extends true + ? ParseExprPrec>, Prec["cmp"]> extends Ok< infer E extends Expr, infer J2 extends number > @@ -658,8 +663,8 @@ type ParseIs< }, Min > - : ParseExprPrec>, Prec["cmp"]> - : SyntaxErr> + : ParseExprPrec>, Prec["cmp"]> + : SyntaxErr> : ParseExprPrec extends Ok< infer E extends Expr, infer J2 extends number @@ -693,7 +698,7 @@ type ParsePredicate< infer J extends number > ? IsWord extends true - ? ParseExprPrec, Prec["cmp"]> extends Ok< + ? ParseExprPrec, Prec["cmp"]> extends Ok< infer HiE extends Expr, infer J2 extends number > @@ -703,7 +708,7 @@ type ParsePredicate< { t: "between"; e: L; lo: LoE; hi: HiE; not: Not }, Min > - : ParseExprPrec, Prec["cmp"]> + : ParseExprPrec, Prec["cmp"]> : SyntaxErr : ParseExprPrec : W extends "in" @@ -714,7 +719,7 @@ type ParsePredicate< infer J extends number > ? IsWord extends true - ? ParseExprPrec, Prec["cmp"]> extends Ok< + ? ParseExprPrec, Prec["cmp"]> extends Ok< infer EsE extends Expr, infer J2 extends number > @@ -724,7 +729,7 @@ type ParsePredicate< { t: "like"; e: L; pat: Pat; esc: EsE; kind: W; not: Not }, Min > - : ParseExprPrec, Prec["cmp"]> + : ParseExprPrec, Prec["cmp"]> : ParseInfix< T, J, @@ -742,19 +747,19 @@ type ParseIn< Not extends boolean, > = IsOp extends true - ? IsWord, "select" | "with" | "values"> extends true - ? ParenSubquery> extends Ok< + ? IsWord, "select" | "with" | "values"> extends true + ? ParenSubquery> extends Ok< infer Q extends SelectStmt, infer J extends number > ? ParseInfix - : ParenSubquery> - : ParenArgs> extends Ok< + : ParenSubquery> + : ParenArgs> extends Ok< infer List extends readonly Expr[], infer J extends number > ? ParseInfix - : ParenArgs> + : ParenArgs> : TableRefAt extends Ok< infer R extends TableRef, infer J extends number @@ -769,7 +774,11 @@ type InTable< Not extends boolean, R extends TableRef, > = - (IsOp extends true ? ParenArgs> : Ok) extends Ok< + ( + IsOp extends true + ? ParenArgs> + : Ok + ) extends Ok< infer Args extends readonly Expr[] | null, infer J extends number > @@ -786,4 +795,4 @@ type InTable< }, Min > - : ParenArgs>; + : ParenArgs>; diff --git a/packages/core/src/sqlite/engine/parser/keywords.ts b/packages/core/src/sqlite/engine/parser/keywords.ts index 29ad026f..5301c18e 100644 --- a/packages/core/src/sqlite/engine/parser/keywords.ts +++ b/packages/core/src/sqlite/engine/parser/keywords.ts @@ -1,71 +1,75 @@ -export type Reserved = - | "add" - | "all" - | "alter" - | "and" - | "as" - | "autoincrement" - | "between" - | "case" - | "check" - | "collate" - | "commit" - | "constraint" - | "create" - | "default" - | "deferrable" - | "delete" - | "distinct" - | "drop" - | "else" - | "escape" - | "except" - | "exists" - | "foreign" - | "from" - | "group" - | "having" - | "in" - | "index" - | "insert" - | "intersect" - | "into" - | "is" - | "isnull" - | "join" - | "limit" - | "not" - | "nothing" - | "notnull" - | "null" - | "on" - | "or" - | "order" - | "primary" - | "references" - | "returning" - | "select" - | "set" - | "table" - | "then" - | "to" - | "transaction" - | "union" - | "unique" - | "update" - | "using" - | "values" - | "when" - | "where"; +export const RESERVED = [ + "add", + "all", + "alter", + "and", + "as", + "autoincrement", + "between", + "case", + "check", + "collate", + "commit", + "constraint", + "create", + "default", + "deferrable", + "delete", + "distinct", + "drop", + "else", + "escape", + "except", + "exists", + "foreign", + "from", + "group", + "having", + "in", + "index", + "insert", + "intersect", + "into", + "is", + "isnull", + "join", + "limit", + "not", + "nothing", + "notnull", + "null", + "on", + "or", + "order", + "primary", + "references", + "returning", + "select", + "set", + "table", + "then", + "to", + "transaction", + "union", + "unique", + "update", + "using", + "values", + "when", + "where", +] as const; +export type Reserved = (typeof RESERVED)[number]; -type JoinKeyword = - | "cross" - | "full" - | "inner" - | "left" - | "natural" - | "outer" - | "right"; +export const JOIN_KEYWORDS = [ + "cross", + "full", + "inner", + "left", + "natural", + "outer", + "right", +] as const; +type JoinKeyword = (typeof JOIN_KEYWORDS)[number]; export type NotBareAlias = | Reserved diff --git a/packages/core/src/sqlite/engine/parser/select.ts b/packages/core/src/sqlite/engine/parser/select.ts index 9f23d72c..10a60758 100644 --- a/packages/core/src/sqlite/engine/parser/select.ts +++ b/packages/core/src/sqlite/engine/parser/select.ts @@ -1,3 +1,16 @@ +import type { + AliasAt, + CloseOk, + Expect, + IsOp, + IsWord, + Name, + NamesV, + SpanText, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { SqlError } from "../../../errors.js"; import type { Cte, @@ -21,40 +34,28 @@ import type { } from "./expr.js"; import type { Reserved } from "./keywords.js"; import type { - AliasAt, BareAlias, - CloseOk, - Expect, Ident, IdentList, - IsOp, - IsWord, - Name, - NamesV, - Nx, - Ok, OptCols, SkipIndexed, - SpanText, - SyntaxErr, TableAlias, TableRefAt, - Toks, } from "./util.js"; export type ParseSubquery = IsWord extends true - ? ParseWith> extends Ok< + ? ParseWith> extends Ok< infer Wc extends WithClause, infer J extends number > ? ParseSelectAfterWith - : ParseWith> + : ParseWith> : ParseSelectAfterWith; export type ParseWith = IsWord extends true - ? CteLoop, true, []> + ? CteLoop, true, []> : CteLoop; type CteLoop< @@ -64,9 +65,9 @@ type CteLoop< Acc extends readonly Cte[], > = Ident extends infer N extends Name - ? OptCols> extends Ok + ? OptCols> extends Ok ? IsWord extends true - ? CteOpen> extends infer J1 extends number + ? CteOpen> extends infer J1 extends number ? ParenSubquery extends Ok< infer S extends SelectStmt, infer J2 extends number @@ -81,21 +82,21 @@ type CteLoop< }, ] extends infer Acc2 extends readonly Cte[] ? IsOp extends true - ? CteLoop, Rec, Acc2> + ? CteLoop, Rec, Acc2> : Ok<{ recursive: Rec; ctes: Acc2 }, J2> : never : ParenSubquery - : CteOpen> + : CteOpen> : SyntaxErr - : OptCols> + : OptCols> : SyntaxErr; type CteOpen = IsWord extends true - ? Expect, "("> + ? Expect, "("> : IsWord extends true - ? IsWord, "materialized"> extends true - ? Expect>, "("> - : SyntaxErr> + ? IsWord, "materialized"> extends true + ? Expect>, "("> + : SyntaxErr> : Expect; export type ParenSubquery = @@ -120,7 +121,7 @@ export type ParseSelectAfterWith = readonly v: infer Op extends "union" | "intersect" | "except"; } ? SqlError< - `${S["orderBy"] extends readonly [] ? "LIMIT" : "ORDER BY"} clause should come after ${Uppercase}${IsWord, "all"> extends true ? " ALL" : ""} not before`, + `${S["orderBy"] extends readonly [] ? "LIMIT" : "ORDER BY"} clause should come after ${Uppercase}${IsWord, "all"> extends true ? " ALL" : ""} not before`, J2 > : ParseTail @@ -133,26 +134,26 @@ type ParseTail = infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Lim extends Expr, infer J2 extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Off extends Expr, infer J3 extends number > ? Ok, J3> - : ParseExpr> + : ParseExpr> : IsOp extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer Cnt extends Expr, infer J3 extends number > ? Ok, J3> - : ParseExpr> + : ParseExpr> : Ok, J2> - : ParseExpr> + : ParseExpr> : Ok, J> : OptBy; type MkSelect = { @@ -181,10 +182,10 @@ type CompoundLoop< } ? ( Op extends "union" - ? IsWord, "all"> extends true - ? [true, Nx>] - : [false, Nx] - : [false, Nx] + ? IsWord, "all"> extends true + ? [true, Next>] + : [false, Next] + : [false, Next] ) extends [infer All extends boolean, infer J extends number] ? ParseCore extends Ok< infer R extends SelectBody, @@ -197,9 +198,9 @@ type CompoundLoop< type ParseCore = IsWord extends true - ? ParseSimple> + ? ParseSimple> : IsWord extends true - ? ParseValues, []> + ? ParseValues, []> : SyntaxErr; export type ParseValues< @@ -214,7 +215,7 @@ export type ParseValues< > ? Expect extends infer J3 extends number ? IsOp extends true - ? ParseValues, [...Acc, Row]> + ? ParseValues, [...Acc, Row]> : Acc extends readonly [] ? Ok<{ t: "values"; rows: [Row] }, J3> : Row["length"] extends Acc[0]["length"] @@ -226,9 +227,9 @@ export type ParseValues< type ParseSimple = ( IsWord extends true - ? [true, Nx] + ? [true, Next] : IsWord extends true - ? [false, Nx] + ? [false, Next] : [false, I] ) extends [infer D extends boolean, infer J extends number] ? ParseTargets extends Ok< @@ -263,7 +264,7 @@ type ParseSimple = ( : never; export type OptFrom = - IsWord extends true ? ParseFrom> : Ok; + IsWord extends true ? ParseFrom> : Ok; type ParseGroup = IsWord extends true @@ -281,13 +282,13 @@ type ParseGroup = : OptHaving : OptBy; type OptHaving = - IsWord extends true ? ParseExpr> : Ok; + IsWord extends true ? ParseExpr> : Ok; type ParseWindows = IsWord extends true - ? Ident> extends Name - ? IsWord>, "as"> extends true - ? WindowLoop, []> + ? Ident> extends Name + ? IsWord>, "as"> extends true + ? WindowLoop, []> : Ok<[], I> : Ok<[], I> : Ok<[], I>; @@ -297,18 +298,18 @@ type WindowLoop< Acc extends readonly NamedWindow[], > = Ident extends infer N extends Name - ? IsWord, "as"> extends true - ? Expect>, "("> extends infer J extends number + ? IsWord, "as"> extends true + ? Expect>, "("> extends infer J extends number ? ParseWindowSpec extends Ok< infer Spec extends WindowSpec, infer J2 extends number > ? IsOp extends true - ? WindowLoop, [...Acc, { name: N["v"]; spec: Spec }]> + ? WindowLoop, [...Acc, { name: N["v"]; spec: Spec }]> : Ok<[...Acc, { name: N["v"]; spec: Spec }], J2> : ParseWindowSpec - : Expect>, "("> - : SyntaxErr> + : Expect>, "("> + : SyntaxErr> : SyntaxErr; export type ParseTargets< @@ -318,17 +319,17 @@ export type ParseTargets< > = ParseTarget extends Ok ? IsOp extends true - ? ParseTargets, [...Acc, Tg]> + ? ParseTargets, [...Acc, Tg]> : Ok<[...Acc, Tg], J> : ParseTarget; type ParseTarget = IsOp extends true - ? Ok<{ t: "star"; tbl: null }, Nx> - : IsOp, "."> extends true - ? IsOp>, "*"> extends true + ? Ok<{ t: "star"; tbl: null }, Next> + : IsOp, "."> extends true + ? IsOp>, "*"> extends true ? Ident extends infer N extends Name - ? Ok<{ t: "star"; tbl: N["v"] }, Nx>>> + ? Ok<{ t: "star"; tbl: N["v"] }, Next>>> : SyntaxErr : ParseExprTarget : ParseExprTarget; @@ -336,11 +337,11 @@ type ParseTarget = type ParseExprTarget = ParseExpr extends Ok ? IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: "" }, Nx>> - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: "" }, Next>> + : SyntaxErr> : BareAlias extends infer A extends Name - ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: "" }, Nx> + ? Ok<{ t: "expr"; e: E; alias: A["n"]; span: "" }, Next> : Ok< { t: "expr"; @@ -362,34 +363,34 @@ type ParseFrom = type JoinOp = IsOp extends true - ? ["cross", false, Nx] + ? ["cross", false, Next] : ( IsWord extends true - ? [true, Nx] + ? [true, Next] : [false, I] ) extends [infer Nat extends boolean, infer J extends number] ? IsWord extends true - ? ["inner", Nat, Nx] + ? ["inner", Nat, Next] : T[J] extends { readonly k: "w"; readonly v: infer K extends "left" | "right" | "full"; } ? ( - IsWord, "outer"> extends true - ? Nx> - : Nx + IsWord, "outer"> extends true + ? Next> + : Next ) extends infer J2 extends number ? IsWord extends true - ? [K, Nat, Nx] + ? [K, Nat, Next] : SyntaxErr : never : T[J] extends { readonly k: "w"; readonly v: infer K extends "inner" | "cross"; } - ? IsWord, "join"> extends true - ? [K extends "cross" ? "cross" : "inner", Nat, Nx>] - : SyntaxErr> + ? IsWord, "join"> extends true + ? [K extends "cross" ? "cross" : "inner", Nat, Next>] + : SyntaxErr> : Nat extends true ? SyntaxErr : null @@ -428,31 +429,31 @@ type JoinLoop = type ParseJoinConstraint = IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer E extends Expr, infer J extends number > ? Ok<[E, null], J> - : ParseExpr> + : ParseExpr> : IsWord extends true - ? IdentList> extends Ok< + ? IdentList> extends Ok< infer Cols extends readonly Name[], infer J extends number > ? Ok<[null, NamesV], J> - : IdentList> + : IdentList> : Ok<[null, null], I>; type ParseTableOrSub = IsOp extends true - ? IsWord, "select" | "with" | "values"> extends true - ? ParenSubquery> extends Ok< + ? IsWord, "select" | "with" | "values"> extends true + ? ParenSubquery> extends Ok< infer Q extends SelectStmt, infer J extends number > ? WithAlias - : ParenSubquery> - : ParseFrom> extends Ok< + : ParenSubquery> + : ParseFrom> extends Ok< infer F extends FromItem, infer J extends number > @@ -464,7 +465,7 @@ type ParseTableOrSub = ? WithAlias : Ok : Expect - : ParseFrom> + : ParseFrom> : TableRefAt extends Ok< infer R extends TableRef, infer J extends number @@ -474,12 +475,12 @@ type ParseTableOrSub = type TableTail = IsOp extends true - ? ParenArgs> extends Ok< + ? ParenArgs> extends Ok< infer Args extends readonly Expr[], infer J extends number > ? WithAlias - : ParenArgs> + : ParenArgs> : WithAlias extends Ok< infer F, infer J extends number @@ -491,13 +492,13 @@ type TableTail = type AliasOf = IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? [A["v"], Nx>] - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? [A["v"], Next>] + : SyntaxErr> : TableAlias extends infer A extends Name ? IsWindowClause extends true ? [null, I] - : [A["v"], Nx] + : [A["v"], Next] : [null, I]; type WithAlias = @@ -518,7 +519,7 @@ type Aliased = F extends { readonly t: "sub"; readonly q: infer Q } type IsWindowClause = IsWord extends true - ? Ident> extends Name - ? IsWord>, "as"> + ? Ident> extends Name + ? IsWord>, "as"> : false : false; diff --git a/packages/core/src/sqlite/engine/parser/stmt.ts b/packages/core/src/sqlite/engine/parser/stmt.ts index 577a9041..9b4c9393 100644 --- a/packages/core/src/sqlite/engine/parser/stmt.ts +++ b/packages/core/src/sqlite/engine/parser/stmt.ts @@ -1,37 +1,42 @@ -import type { SqlError } from "../../../errors.js"; -import type { Stmt, WithClause } from "../ast.js"; +import type { Unlocated } from "../../../engine/family/diagnostics.js"; import type { - ParseAlter, - ParseCreate, - ParseDrop, + IsOp, + IsWord, + SkipSemis, SkipStatement, -} from "./ddl.js"; + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { AtEnd, Ok, Toks } from "../../../engine/tokens.js"; +import type { SqlError } from "../../../errors.js"; +import type { Stmt, WithClause } from "../ast.js"; +import type { ParseAlter, ParseCreate, ParseDrop } from "./ddl.js"; import type { ParseDelete, ParseInsert, ParseUpdate } from "./dml.js"; import type { ParseSelectAfterWith, ParseWith } from "./select.js"; -import type { AtEnd, IsOp, IsWord, Nx, Ok, SyntaxErr, Toks } from "./util.js"; +import type { SyntaxWords } from "./util.js"; -export type ParseStatement = T[I] extends { +type StatementAt = T[I] extends { readonly k: "w"; readonly v: infer W extends string; } ? W extends "select" | "values" ? ParseSelectAfterWith : W extends "with" - ? ParseWithStatement> + ? ParseWithStatement> : W extends "insert" | "replace" ? ParseInsert : W extends "update" - ? ParseUpdate, null> + ? ParseUpdate, null> : W extends "delete" - ? ParseDelete, null> + ? ParseDelete, null> : W extends "create" - ? ParseCreate> + ? ParseCreate> : W extends "alter" - ? ParseAlter> + ? ParseAlter> : W extends "drop" - ? ParseDrop> + ? ParseDrop> : W extends NoopKeyword - ? SkipStatement, W> + ? SkipStatement, W> : SyntaxErr : SyntaxErr; @@ -60,20 +65,17 @@ type ParseWithStatement = : IsWord extends true ? ParseInsert : IsWord extends true - ? ParseUpdate, Wc> + ? ParseUpdate, Wc> : IsWord extends true - ? ParseDelete, Wc> + ? ParseDelete, Wc> : SyntaxErr : ParseWith; -type SkipSemis = - IsOp extends true ? SkipSemis> : I; - -export type ParseProgram = +export type ParseProgramAt = SkipSemis extends infer I0 extends number ? AtEnd extends true ? SqlError<"the SQL text contains no statements", 0> - : ParseStatement extends Ok< + : StatementAt extends Ok< infer S extends Stmt, infer J extends number > @@ -86,7 +88,7 @@ export type ParseProgram = : SqlError<"only one statement is allowed", J2> : never : SyntaxErr - : ParseStatement + : StatementAt : never; export type ParseScript< @@ -97,7 +99,7 @@ export type ParseScript< SkipSemis extends infer I0 extends number ? AtEnd extends true ? Acc - : ParseStatement extends Ok< + : StatementAt extends Ok< infer S extends Stmt, infer J extends number > @@ -106,5 +108,17 @@ export type ParseScript< : IsOp extends true ? ParseScript : SyntaxErr - : ParseStatement + : StatementAt : never; + +/** The statement at token `I`, as `Ok` or the error. */ +export type ParseStatement = Unlocated< + StatementAt, + SyntaxWords +>; + +/** The one statement of `T`. */ +export type ParseProgram = Unlocated< + ParseProgramAt, + SyntaxWords +>; diff --git a/packages/core/src/sqlite/engine/parser/util.ts b/packages/core/src/sqlite/engine/parser/util.ts index 781c154e..4a384b01 100644 --- a/packages/core/src/sqlite/engine/parser/util.ts +++ b/packages/core/src/sqlite/engine/parser/util.ts @@ -1,45 +1,25 @@ -import type { AtEnd, Nx, Ok } from "../../../engine/parse.js"; +import type { + AliasAt, + IdentAt, + IdentList as IdentListOf, + IsWord, + Name, + OptCols as OptColsOf, + SyntaxErr, + TableRefAt as TableRefAtOf, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; import type { Toks } from "../../../engine/tokens.js"; -import type { SqlError } from "../../../errors.js"; -import type { NameOf, RawOf } from "../tokens.js"; import type { NotBareAlias, NotTableAlias, Reserved } from "./keywords.js"; -export type { AtEnd, Nx, Ok } from "../../../engine/parse.js"; - -export type { Toks }; - -export type SyntaxErr = SqlError< - AtEnd extends true - ? "incomplete input" - : T[I] extends { readonly k: string } - ? `near "${RawOf}": syntax error` - : "incomplete input", - I ->; - -export type Name = { readonly v: string; readonly n: string }; - -type IdentAt = T[I] extends { - readonly k: "id"; -} - ? Name & { readonly v: T[I]["v" & keyof T[I]]; readonly n: NameOf } - : T[I] extends { readonly k: "w"; readonly v: infer V } - ? V extends Not - ? null - : { readonly v: V; readonly n: NameOf } - : null; +/** How the server words a syntax error. */ +export type SyntaxWords = { + readonly near: ['near "', '": syntax error']; + readonly end: "incomplete input"; +}; export type Ident = IdentAt; -export type AliasAt = T[I] extends { - readonly k: "s"; - readonly v: infer V extends string; -} - ? T[I] extends { readonly blob: true } - ? null - : { readonly v: Lowercase; readonly n: V } - : IdentAt; - export type BareAlias = AliasAt< T, I, @@ -51,84 +31,33 @@ export type TableAlias = AliasAt< NotTableAlias >; -export type IsOp< - T extends Toks, - I extends number, - O extends string, -> = T[I] extends { readonly k: "o"; readonly v: O } ? true : false; -export type IsWord< - T extends Toks, - I extends number, - W extends string, -> = T[I] extends { readonly k: "w"; readonly v: W } ? true : false; - -export type Expect< - T extends Toks, - I extends number, - O extends string, -> = T[I] extends { readonly k: "o"; readonly v: O } ? Nx : SyntaxErr; - -export type CloseOk = - IsOp extends true ? Ok> : SyntaxErr; - -export type SpanText = Trim< - SpanLoop, J, RawOf> +export type IdentList = IdentListOf< + T, + I, + Reserved >; -type SpanLoop< - T extends Toks, - I extends number, - J extends number, - Acc extends string, -> = I extends J - ? T[I] extends { readonly g: infer G extends string } - ? `${Acc}${G}` - : Acc - : T[I] extends { readonly g: infer G extends string } - ? SpanLoop, J, `${Acc}${G}${RawOf}`> - : Acc; -type Space = " " | "\n" | "\t" | "\r" | "\f" | "\v"; -type Trim = S extends `${infer R}${Space}` ? Trim : S; - -export type IdentList = - Expect extends infer J extends number - ? IdentListLoop - : Expect; -type IdentListLoop< - T extends Toks, - I extends number, - Acc extends readonly Name[], -> = - Ident extends infer N extends Name - ? T[Nx] extends { readonly k: "o"; readonly v: "," } - ? IdentListLoop>, [...Acc, N]> - : CloseOk, [...Acc, N]> - : SyntaxErr; - -export type OptCols = - IsOp extends true ? IdentList : Ok; -export type NamesV = { - [K in keyof N]: N[K] extends Name ? N[K]["v"] : never; -}; +export type OptCols = OptColsOf< + T, + I, + Reserved +>; -export type TableRefAt = - Ident extends infer A extends Name - ? IsOp, "."> extends true - ? Ident>> extends infer B extends Name - ? Ok<{ schema: A["v"]; name: B["v"] }, Nx>>> - : SyntaxErr>> - : Ok<{ schema: null; name: A["v"] }, Nx> - : SyntaxErr; +export type TableRefAt = TableRefAtOf< + T, + I, + Reserved +>; export type SkipIndexed = IsWord extends true - ? IsWord, "by"> extends true - ? Ident>> extends Name - ? Nx>> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "by"> extends true + ? Ident>> extends Name + ? Next>> + : SyntaxErr>> + : SyntaxErr> : IsWord extends true - ? IsWord, "indexed"> extends true - ? Nx> - : SyntaxErr> + ? IsWord, "indexed"> extends true + ? Next> + : SyntaxErr> : I; diff --git a/packages/core/src/sqlite/engine/project/dml.ts b/packages/core/src/sqlite/engine/project/dml.ts index 19322747..5a3f013f 100644 --- a/packages/core/src/sqlite/engine/project/dml.ts +++ b/packages/core/src/sqlite/engine/project/dml.ts @@ -1,5 +1,13 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { QueryContext } from "../../../engine/types.js"; +import type { + LastPart, + NameAt, + NamedAt, +} from "../../../engine/family/diagnostics.js"; +import type { RefDisplay } from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Members } from "../../../engine/object.js"; +import type { Info } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; import type { @@ -16,9 +24,9 @@ import type { } from "../ast.js"; import type { BindFrom, + InsertKeys, InsertOrder, LookupRelation, - RefDisplay, RelationItem, } from "../binder/scope.js"; import type { @@ -31,7 +39,7 @@ import type { ParamsOfTargets, Slot, } from "../typer/params.js"; -import type { Info, TypeList, TypeOf } from "../typer/typeof.js"; +import type { TypeList, TypeOf } from "../typer/typeof.js"; import type { Cell, MkCtx, @@ -53,17 +61,23 @@ type Target_< Ctes extends SqCteMap, > = Ref["schema"] extends null ? Ref["name"] extends keyof Ctes - ? SqlError<`no such table: ${Ref["name"]}`, null> + ? SqlError<`no such table: ${Ref["name"]}`, NamedAt> : Writable : Writable; type Writable = LookupRelation extends [infer Rel, infer K extends string] ? Rel extends View

extends infer Ps extends readonly unknown[] ? { - readonly text: JoinedText, Sep>; + readonly text: JoinedText, Sep, L>; readonly carried: PairCarried; readonly count: Ps["length"]; } @@ -189,33 +202,37 @@ type PresentItems< export type CombineOf< Items extends readonly (Slottable | Empty)[], Sep extends string, + L extends Lexis, > = CombinePresent< number extends Items["length"] ? readonly Exclude[] : PresentItems, - Sep + Sep, + L >; type CombinePresent< Items extends readonly Slottable[], Sep extends string, + L extends Lexis, > = string extends Sep ? Fragment : number extends Items["length"] ? Fragment< - ListRep["text"], + ListRep["text"], readonly unknown[], - ListRep["carried"] + ListRep["carried"] > : Fragment< JoinedText< SlotTexts extends readonly { readonly text: string }[] ? SlotTexts : never, - Sep + Sep, + L >, readonly unknown[], - JoinedCarried + JoinedCarried >; export type CombineSeparator = ", " | (string & {}); @@ -225,18 +242,21 @@ export type CombineSeparator = ", " | (string & {}); export type BoolOf< Items extends readonly (Slottable | Empty)[], Op extends "AND" | "OR", + L extends Lexis, > = BoolPresent< number extends Items["length"] ? readonly Exclude[] : PresentItems, - Op + Op, + L >; type BoolPresent< Items extends readonly Slottable[], Op extends "AND" | "OR", + L extends Lexis, > = number extends Items["length"] - ? ListRep extends { + ? ListRep extends { readonly text: infer T extends string; readonly carried: infer C extends readonly unknown[]; readonly count: infer N; @@ -246,9 +266,9 @@ type BoolPresent< : Items extends readonly [] ? Fragment & Grouped : Fragment< - Group, ` ${Op} `>, Items["length"]>, + Group, ` ${Op} `, L>, Items["length"]>, readonly unknown[], - JoinedCarried + JoinedCarried > & Grouped; @@ -266,36 +286,62 @@ type Group = string extends Text ? Text : `(${Text})`; -export type GateFragCheck = - Inlined extends infer I extends AnyErr - ? I - : [InlinedText] extends [infer T2 extends string] - ? Surface> - : never; +export type GateFragCheck = + Inlined extends infer I + ? I extends AnyErr + ? I + : [ + I extends readonly [infer T2 extends string, unknown] + ? QuotedSlot + : never, + ] extends [infer QE] + ? [QE] extends [never] + ? [InlinedText] extends [infer T2 extends string] + ? Surface> + : never + : QE + : never + : never; -export type BoxFragArgs = [InlinedText] extends [ - infer T2 extends string, -] - ? Extract<{ [K in T2 as `${K}`]: FragArgs }[`${T2}`], readonly unknown[]> +export type BoxFragArgs = [ + InlinedText, +] extends [infer T2 extends string] + ? Extract< + { [K in T2 as `${K}`]: FragArgs }[`${T2}`], + readonly unknown[] + > : never; /** `sql.fragment(text, ...params)`; its `${fragment}` interpolations are slots. */ -export type PlainFragment = +export type PlainFragment< + Text extends string, + A extends readonly unknown[], + L extends Lexis, +> = Inlined extends null - ? Fragment, UnknownTuple>, FragCarried> - : InlinedFragment; + ? Fragment< + PText, + Tup, unknown>, + FragCarried + > + : InlinedFragment; /** One fragment for each combination of union fragments, so each text keeps its own values. */ -type InlinedFragment = [ - InlinedText, -] extends [infer T2 extends string] +type InlinedFragment< + Text extends string, + A extends readonly unknown[], + L extends Lexis, +> = [InlinedText] extends [infer T2 extends string] ? SlotVariants> extends infer V ? V extends unknown - ? Splice, SlotTexts> extends infer X extends string + ? Splice, SlotTexts, L> extends infer X extends string ? Fragment< X, readonly unknown[], - [...FragCarried, ...SpliceCarried, SlotCarried>] + [ + ...FragCarried, + ...SpliceCarried, SlotCarried, L>, + ] > : never : never diff --git a/packages/core/src/engine/infer.ts b/packages/core/src/sql/infer.ts similarity index 88% rename from packages/core/src/engine/infer.ts rename to packages/core/src/sql/infer.ts index 54e280ef..2f56a696 100644 --- a/packages/core/src/engine/infer.ts +++ b/packages/core/src/sql/infer.ts @@ -1,16 +1,16 @@ +import type { Next } from "../engine/next.js"; +import type { Tup } from "../engine/num.js"; +import type { NullTest } from "../engine/params.js"; +import type { Cols, ColsToMap } from "../engine/types.js"; import type { AnyErr } from "../errors.js"; -import type { JsonValue } from "../sql/inline.js"; -import type { NamedParams } from "../sql/named.js"; -import type { UnknownTuple } from "../sql/splice.js"; import type { AnalysisOf, EngineTypes, RowTypeOf, TsParamTypeOf, } from "./contract.js"; -import type { NullTest } from "./params.js"; -import type { Nx } from "./parse.js"; -import type { Cols, ColsToMap } from "./types.js"; +import type { JsonValue } from "./inline.js"; +import type { NamedArgs } from "./named.js"; export type RowOf = AnalysisOf< E, @@ -39,10 +39,10 @@ type ParamDataOf = > : []; type BuildParamData = - UnknownTuple extends infer T extends unknown[] + Tup extends infer T extends unknown[] ? { [K in keyof T]: K extends `${infer I extends number}` - ? DataFor & keyof P], TM, DB> + ? DataFor & keyof P], TM, DB> : never; } : never; @@ -122,7 +122,7 @@ export type ParamsArgs = NamesOf extends infer N extends readonly string[] ? N extends readonly [] ? ArgsOf - : [params: NamedParams>] + : NamedArgs> : never; export type Validate = diff --git a/packages/core/src/sql/inline-fragment.ts b/packages/core/src/sql/inline-fragment.ts index 8a364d43..682f4a20 100644 --- a/packages/core/src/sql/inline-fragment.ts +++ b/packages/core/src/sql/inline-fragment.ts @@ -1,11 +1,10 @@ import type { Empty } from "../empty.js"; import type { AnyErr, SqlError } from "../errors.js"; import type { Fragment, InlineFragment } from "../query.js"; -import type { SlotNames } from "./splice.js"; +import type { Lexis } from "./lexis.js"; +import type { InlineName, SlotNames, SlotNamesIn } from "./splice.js"; import type { NotLiteralMessage } from "./text.js"; -type InlineName = `__typesql_inline${N}`; - /** * A statement with `${fragment}` / `${query}` interpolations as * `[text, slots]`: each becomes an internal `{name}` slot, and an omitted @@ -68,20 +67,15 @@ type Finish = R extends [ infer A extends string, infer F extends readonly unknown[], ] - ? Quoted extends true - ? SqlError< - "a fragment cannot be interpolated inside a quoted string, quoted identifier, dollar-quoted string or comment; remove the surrounding quotes or comment", - null - > - : [ - A, - { - readonly [I in keyof F & - `${number}` as `__typesql_inline${I}`]: F[I] extends Empty - ? Fragment<"", [], []> - : F[I]; - }, - ] + ? [ + A, + { + readonly [I in keyof F & + `${number}` as InlineName]: F[I] extends Empty + ? Fragment<"", [], []> + : F[I]; + }, + ] : R; type LiftChars< @@ -102,12 +96,18 @@ type LiftChars< ? [Acc, Fs] : SqlError; -/** Whether a fragment's slot landed where `{name}` is not a slot. */ -type Quoted = [ - Exclude>, SlotNames>, -] extends [never] - ? false - : true; +/** + * The error for a statement `T` (Inlined text) with a fragment's slot where + * `{name}` is not a slot, which lexis `L` reads as quoted text or a comment. + */ +export type QuotedSlot = T extends unknown + ? [Exclude, SlotNames>] extends [never] + ? never + : SqlError< + "a fragment cannot be interpolated inside a quoted string, quoted identifier, dollar-quoted string or comment; remove the surrounding quotes or comment", + null + > + : never; /** The members of a union of statements (`${flag ? a : b}`) as one statement with union slots. */ type Merge = [Extract] extends [never] diff --git a/packages/core/src/sql/inline.ts b/packages/core/src/sql/inline.ts index 5b35f197..cf67e2cf 100644 --- a/packages/core/src/sql/inline.ts +++ b/packages/core/src/sql/inline.ts @@ -1,8 +1,9 @@ import type { Empty } from "../empty.js"; +import type { Next } from "../engine/next.js"; import type { Members } from "../engine/object.js"; import type { SqlError } from "../errors.js"; import type { InlineFragment } from "../query.js"; -import type { Next } from "./next.js"; +import type { Lexis, Opaque, OpaqueKind, SplitFirst } from "./lexis.js"; export type ParamInput = unknown extends T ? unknown @@ -61,12 +62,16 @@ export type JsonMarker = unknown extends T ? `\u0000PJ${Enc}\u0000` : [Extract] extends [never] ? `\u0000PJ${Enc}\u0000` - : `\u0000DuJ${Enc>}\u0000`; + : `${DefaultMark}J${Enc>}\u0000`; export type JsonValue = `\u0000PJ${string}\u0000`; /** A `sql.json()` value that can be DEFAULT, which `sql.set` and `sql.values` leave out. */ -export type JsonDefaultValue = `\u0000DuJ${string}\u0000`; +export type JsonDefaultValue = `${DefaultMark}J${string}\u0000`; -export type DefaultMarker = `\u0000Du${Enc>}\u0000`; +/** How a DEFAULT marker starts: `sql.param()` of a value that can be the omission. */ +type DefaultMark = "\u0000Du"; +export type DefaultMarker = `${DefaultMark}${Enc>}\u0000`; +/** Any DEFAULT marker, which `sql.set` and `sql.values` leave out. */ +export type AnyDefaultMarker = `${DefaultMark}${string}\u0000`; /** * A value's type, written into the marker text so `sql()` can check it @@ -451,118 +456,57 @@ type JoinBar = Parts extends [infer A] ? `${A} | ${JoinBar}` : "never"; -type Openers = '"' | "--" | "/*"; - -export type SplitFirst = T extends `${infer A}'${infer B}` - ? A extends `${string}${Openers}${string}` - ? Earlier - : [A, "'", B] - : T extends `${string}${Openers}${string}` - ? T extends `${infer A}"${infer B}` - ? Earlier - : T extends `${infer A}--${infer B}` - ? Earlier - : T extends `${infer A}/*${infer B}` - ? Earlier - : [T, "", ""] - : [T, "", ""]; -type Earlier< - A extends string, - O extends string, - B extends string, -> = A extends `${infer A2}"${infer M}` - ? Earlier - : A extends `${infer A2}--${infer M}` - ? Earlier - : A extends `${infer A2}/*${infer M}` - ? Earlier - : A extends `${infer A2}'${infer M}` - ? Earlier - : [A, O, B]; - -export type Opaque = O extends "'" | '"' - ? CloseQuote - : O extends "--" - ? S extends `${infer C}\n${infer R}` - ? [`${C}\n`, R] - : [S, ""] - : O extends "/*" - ? CloseBlock - : ["", S]; - -type CloseBlock< - S extends string, - Depth extends unknown[], - Acc extends string, -> = S extends `${infer Pre}/${infer R}` - ? Pre extends `${string}*` - ? Depth extends [unknown, ...infer D] - ? D extends [] - ? [`${Acc}${Pre}/`, R] - : CloseBlock - : never - : R extends `*${infer Rest}` - ? CloseBlock - : CloseBlock - : [`${Acc}${S}`, ""]; - -type CloseQuote< - S extends string, - Q extends string, - Acc extends string, -> = S extends `${infer In}${Q}${infer R}` - ? R extends `${Q}${infer R2}` - ? CloseQuote - : [`${Acc}${In}${Q}`, R] - : [`${Acc}${S}`, ""]; - type HasMarker = S extends `${string}\u0000${string}` ? true : false; -export type RewriteInlineStr = - HasMarker extends false ? [T, []] : InlineLoop>; +export type RewriteInlineStr< + T extends string, + Start extends number, + L extends Lexis, +> = + HasMarker extends false ? [T, []] : InlineLoop, L>; +/** + * Each marker becomes parameter `$K` on: the text before a marker is read + * up to its first opener, whose quoted text or comment must end before the + * marker. Marker text, whose arrays are `[…]`, is never read for openers, + * nor is the text after the last marker. + */ type InlineLoop< T extends string, Acc extends string, Encs extends string[], K extends number, -> = - SplitFirst extends [ - infer Code extends string, - infer O extends string, - infer Rest extends string, - ] - ? InlineCode extends [ - infer C extends string, - infer E2 extends string[], - infer K2 extends number, - ] - ? O extends "" - ? [`${Acc}${C}`, E2] - : Opaque extends [ - infer Seg extends string, - infer After extends string, - ] - ? HasMarker extends true - ? SqlError< - `sql.param() cannot be used inside ${O extends "'" ? "a quoted string" : O extends '"' ? "a quoted identifier" : "a comment"}; remove the surrounding ${O extends "'" | '"' ? "quotes" : "comment"}`, - null - > - : InlineLoop - : never - : never - : never; + L extends Lexis, +> = T extends `${infer Pre}\u0000${infer Body}\u0000${infer Rest}` + ? SplitFirst extends [ + infer Code extends string, + infer O extends string, + infer PRest extends string, + ] + ? O extends "" + ? InlineLoop< + Rest, + `${Acc}${Pre}$${K}`, + [...Encs, MarkerEnc], + Next, + L + > + : Opaque extends [ + infer Seg extends string, + infer After extends string, + ] + ? HasMarker extends true + ? SqlError< + `sql.param() cannot be used inside ${OpaqueKind}; remove the surrounding ${OpaqueKind extends "a comment" ? "comment" : "quotes"}`, + null + > + : InlineLoop + : never + : never + : [`${Acc}${T}`, Encs]; -type InlineCode< - S extends string, - Acc extends string, - Encs extends string[], - K extends number, -> = S extends `${infer Pre}\u0000${infer Body}\u0000${infer Rest}` - ? InlineCode], Next> - : [`${Acc}${S}`, Encs, K]; type MarkerEnc = B extends `P${infer E}` ? E : B extends `Du${infer E}` @@ -582,12 +526,12 @@ export type ValueEnc = 0 extends 1 & V type MarkerEncs = M extends `\u0000${infer B}\u0000` ? MarkerEnc : never; export type HasDefaultable = - S extends `${string}\u0000Du${string}` ? true : false; + S extends `${string}${DefaultMark}${string}` ? true : false; export type WithDefaults< S extends string, Acc extends string = "", -> = S extends `${infer Pre}\u0000Du${string}\u0000${infer Rest}` +> = S extends `${infer Pre}${DefaultMark}${string}\u0000${infer Rest}` ? WithDefaults : `${Acc}${S}`; diff --git a/packages/core/src/sql/lexis.ts b/packages/core/src/sql/lexis.ts new file mode 100644 index 00000000..f97f003f --- /dev/null +++ b/packages/core/src/sql/lexis.ts @@ -0,0 +1,282 @@ +import type { Digit, IdStart, Ws } from "../engine/chars.js"; + +/** Standard SQL's openers besides `'`: quoted names and comments. */ +type Standard = '"' | "--" | "/*"; +/** The openers some dialects add. */ +type Extra = "`" | "[" | "#"; + +/** + * A dialect's text rules at type level, as its runtime dialect's `lexis`: + * which text holds no parameters, markers or slots. Standard SQL's strings, + * quoted names and comments (`'`, `"`, `--`, `/*`) open such text in every + * dialect. + */ +export interface Lexis { + /** The openers it adds: quotes and comment starts. */ + readonly extra: Extra; + /** The quotes inside which a backslash escapes the next character. */ + readonly backslash: "'" | '"'; + /** Whether `--` starts a comment only before whitespace. */ + readonly spacedDash: boolean; + /** Whether block comments nest. */ + readonly nest: boolean; + /** Whether `$tag$` quotes a string and `E'…'` strings take backslash escapes, as in PostgreSQL. */ + readonly dollar: boolean; + /** The quotes of names, for messages; the others quote strings. */ + readonly names: '"' | "`" | "["; +} + +/** + * Whether lexis `L` reads all of `T` as code: no string, quoted name, comment + * or dollar quote. The pre-passes then read `T` in one piece. The first test, + * which most statements pass, involves no lexis. + */ +export type AllCode< + T extends string, + L extends Lexis, +> = T extends `${string}${"'" | Standard | Extra}${string}` + ? T extends `${string}${"'" | Standard | L["extra"]}${string}` + ? false + : NoDollarQuote + : NoDollarQuote; +type NoDollarQuote< + T extends string, + L extends Lexis, +> = T extends `${string}$${string}` + ? DollarOpen extends null + ? true + : false + : true; + +/** + * Splits `T` at its first opener: `[code before it, the opener, text after + * it]`, or `[T, "", ""]` when it has none. + */ +export type SplitFirst< + T extends string, + L extends Lexis, +> = T extends `${infer A}'${infer B}` + ? A extends `${string}${Standard | Extra}${string}` + ? Earlier + : [A, "'", B] + : T extends `${string}${Standard | Extra}${string}` + ? T extends `${infer A}"${infer B}` + ? A extends `${string}${Standard | Extra}${string}` + ? Earlier + : [A, '"', B] + : T extends `${infer A}--${infer B}` + ? Earlier + : T extends `${infer A}/*${infer B}` + ? Earlier + : Added + : [T, "", ""]; +/** `A` split at its first opener, if it has one, before opener `O` and text `B`. */ +type Earlier< + A extends string, + O extends string, + B extends string, + L extends Lexis, +> = A extends `${infer A2}"${infer M}` + ? Earlier + : A extends `${infer A2}--${infer M}` + ? Earlier + : A extends `${infer A2}/*${infer M}` + ? Earlier + : A extends `${infer A2}'${infer M}` + ? Earlier + : A extends `${string}${Extra}${string}` + ? Added + : [A, O, B]; +/** Earlier, for the openers lexis `L` adds, when `A` has no standard one. */ +type Added< + A extends string, + O extends string, + B extends string, + L extends Lexis, +> = A extends `${infer A2}${"`" & L["extra"]}${infer M}` + ? Added + : A extends `${infer A2}${"[" & L["extra"]}${infer M}` + ? Added + : A extends `${infer A2}${"#" & L["extra"]}${infer M}` + ? Added + : [A, O, B]; + +/** + * The text an opener `O` opens, up to and including what closes it, and the + * text after that. An empty text when `O` doesn't open one here. + */ +export type Opaque< + O extends string, + S extends string, + L extends Lexis, +> = O extends "'" | '"' | "`" + ? S extends `${string}\\${string}` + ? O extends L["backslash"] + ? CloseEscaped + : CloseQuote + : CloseQuote + : O extends "/*" + ? L["nest"] extends true + ? CloseBlock + : S extends `${infer C}*/${infer R}` + ? [`${C}*/`, R] + : [S, ""] + : O extends "--" + ? S extends "" | `${Ws}${string}` + ? CloseLine + : L["spacedDash"] extends true + ? ["", S] + : CloseLine + : O extends "#" + ? CloseLine + : O extends "[" + ? S extends `${infer C}]${infer R}` + ? [`${C}]`, R] + : [S, ""] + : ["", S]; + +/** What a marker or slot inside the text an opener `O` opens is inside. */ +export type OpaqueKind = O extends L["names"] + ? "a quoted identifier" + : O extends "'" | '"' | "`" + ? "a quoted string" + : "a comment"; + +type CloseLine = S extends `${infer C}\n${infer R}` + ? [`${C}\n`, R] + : [S, ""]; + +type CloseBlock< + S extends string, + Depth extends unknown[], + Acc extends string, +> = S extends `${infer Pre}/${infer R}` + ? Pre extends `${string}*` + ? Depth extends [unknown, ...infer D] + ? D extends [] + ? [`${Acc}${Pre}/`, R] + : CloseBlock + : never + : R extends `*${infer Rest}` + ? CloseBlock + : CloseBlock + : [`${Acc}${S}`, ""]; + +type CloseQuote< + S extends string, + Q extends string, + Acc extends string, +> = S extends `${infer In}${Q}${infer R}` + ? R extends `${Q}${infer R2}` + ? CloseQuote + : [`${Acc}${In}${Q}`, R] + : [`${Acc}${S}`, ""]; + +/** The text of quote `Q`, which a backslash escapes as well as a doubled `Q`. */ +type CloseEscaped< + S extends string, + Q extends string, + Acc extends string, +> = S extends `${infer In}${Q}${infer R}` + ? In extends `${infer A}\\${infer B}` + ? B extends `${infer C}${infer B2}` + ? CloseEscaped<`${B2}${Q}${R}`, Q, `${Acc}${A}\\${C}`> + : CloseEscaped + : R extends `${Q}${infer R2}` + ? CloseEscaped + : [`${Acc}${In}${Q}`, R] + : [`${Acc}${S}`, ""]; + +/** + * Splits `T` at its first text that holds no parameters (string, quoted + * name, comment or dollar quote): `[code before it, that whole text, text + * after it]`. The text is `""` when `T` is all code. + */ +export type NextOpaque = + SplitFirst extends [ + infer Code extends string, + infer O extends string, + infer Rest extends string, + ] + ? Code extends `${string}$${string}` + ? DollarOpen extends [ + infer Pre extends string, + infer Tag extends string, + infer InCode extends string, + ] + ? DollarBody<`${InCode}${O}${Rest}`, Tag> extends [ + infer Seg extends string, + infer After extends string, + ] + ? [Pre, `${Tag}${Seg}`, After] + : never + : Quoted + : Quoted + : never; +/** `[Code, the text opener O opens, the text after it]`. */ +type Quoted< + Code extends string, + O extends string, + Rest extends string, + L extends Lexis, +> = O extends "" + ? [Code, "", ""] + : ( + O extends "'" + ? IsEString extends true + ? L["dollar"] extends true + ? CloseEscaped + : Opaque + : Opaque + : Opaque + ) extends [infer Seg extends string, infer After extends string] + ? [Code, `${O}${Seg}`, After] + : never; + +/** `E'…'` strings escape quotes with backslashes. */ +type IsEString = Code extends `${infer C0}${"E" | "e"}` + ? C0 extends `${string}${NameChar}` + ? false + : true + : false; + +/** + * What a name holds after its first character: `$` too, so `a$b$` is a + * name. A `$` after code that ends in one continues the name. + */ +type NameChar = NChar | "$"; + +/** The first `$$` or `$tag$` in `S`, and the code before and after it. */ +type DollarOpen< + S extends string, + Acc extends string, + L extends Lexis, +> = S extends `${infer A}$${infer B}` + ? L["dollar"] extends true + ? B extends `$${infer R}` + ? `${Acc}${A}` extends `${string}${NameChar}` + ? DollarOpen + : [`${Acc}${A}`, "$$", R] + : B extends `${infer C extends IdStart}${infer R}` + ? TakeName extends [infer Tag extends string, `$${infer R2}`] + ? `${Acc}${A}` extends `${string}${NameChar}` + ? DollarOpen + : [`${Acc}${A}`, `$${Tag}$`, R2] + : DollarOpen + : DollarOpen + : null + : null; +type DollarBody< + S extends string, + Tag extends string, +> = S extends `${infer Body}${Tag}${infer R}` ? [`${Body}${Tag}`, R] : [S, ""]; + +type NChar = IdStart | Digit; + +/** A name's characters after its first, `Acc`, and the text after them. */ +export type TakeName< + S extends string, + Acc extends string, +> = S extends `${infer C extends NChar}${infer R}` + ? TakeName + : [Acc, S]; diff --git a/packages/core/src/sql/named.ts b/packages/core/src/sql/named.ts index eefdda68..57c9c185 100644 --- a/packages/core/src/sql/named.ts +++ b/packages/core/src/sql/named.ts @@ -1,17 +1,27 @@ -import type { Digit, IdStart } from "../engine/chars.js"; +import type { IdStart } from "../engine/chars.js"; import type { SqlError } from "../errors.js"; -import type { Opaque, SplitFirst } from "./inline.js"; -import type { MaxParamStr } from "./splice.js"; +import type { AllCode, Lexis, NextOpaque, TakeName } from "./lexis.js"; +import type { MaxParamStr, Take } from "./splice.js"; -export type RewriteNamed = - RewriteNamedStr extends [infer T, infer Names] +/** `Q` with its `:name` parameters numbered, and their names; an error if it also has `$n`. */ +export type RewriteNamed = + RewriteNamedStr extends [infer T, infer Names] ? Names extends [] ? [T, []] - : MaxParamStr extends 0 + : MaxParamStr extends 0 ? [T, Names] - : SqlError<"cannot mix named (:name) and positional ($n) parameters", 0> + : SqlError< + "cannot mix named (:name) and positional ($n) parameters", + null + > : never; +/** The one object argument of a statement with named parameters, from its positional arguments. */ +export type NamedArgs< + Names extends readonly string[], + Args extends readonly unknown[], +> = [params: NamedParams>]; + type IndexOf< Names extends string[], Name extends string, @@ -32,8 +42,12 @@ export type NamedParams< ? { [K in keyof O]: O[K] } : never; -export type RewriteNamedStr = - HasSingleColon extends true ? NamedLoop : [Text, []]; +export type RewriteNamedStr = + HasSingleColon extends true + ? AllCode extends true + ? RewriteCode + : NamedLoop + : [Text, []]; type HasSingleColon< S extends string, @@ -46,8 +60,13 @@ type HasSingleColon< : true : false; -type NamedLoop = - NextOpaque extends [ +type NamedLoop< + T extends string, + Acc extends string, + Names extends string[], + L extends Lexis, +> = + NextOpaque extends [ infer Code extends string, infer Seg extends string, infer After extends string, @@ -58,83 +77,10 @@ type NamedLoop = ] ? Seg extends "" ? [`${Acc}${C}`, N2] - : NamedLoop + : NamedLoop : never : never; -/** - * Splits `T` at its first string, E-string, quoted identifier, comment or - * dollar quote: `[code before it, the whole quoted segment, text after it]`. - * The segment is `""` when `T` is all code. - */ -export type NextOpaque = - SplitFirst extends [ - infer Code extends string, - infer O extends string, - infer Rest extends string, - ] - ? DollarOpen extends [ - infer Pre extends string, - infer Tag extends string, - infer InCode extends string, - ] - ? DollarBody<`${InCode}${O}${Rest}`, Tag> extends [ - infer Seg extends string, - infer After extends string, - ] - ? [Pre, `${Tag}${Seg}`, After] - : never - : O extends "" - ? [Code, "", ""] - : ( - O extends "'" - ? IsEString extends true - ? EClose - : Opaque - : Opaque - ) extends [infer Seg extends string, infer After extends string] - ? [Code, `${O}${Seg}`, After] - : never - : never; - -/** `E'…'` strings escape quotes with backslashes. */ -type IsEString = Code extends `${infer C0}${"E" | "e"}` - ? C0 extends `${string}${NChar}` - ? false - : true - : false; -type EClose< - S extends string, - Acc extends string, -> = S extends `${infer In}'${infer R}` - ? In extends `${infer A}\\${infer B}` - ? B extends `${infer C}${infer B2}` - ? EClose<`${B2}'${R}`, `${Acc}${A}\\${C}`> - : EClose - : R extends `'${infer R2}` - ? EClose - : [`${Acc}${In}'`, R] - : [`${Acc}${S}`, ""]; - -type DollarOpen< - S extends string, - Acc extends string, -> = S extends `${infer A}$${infer B}` - ? B extends `$${infer R}` - ? [`${Acc}${A}`, "$$", R] - : B extends `${infer C extends IdStart}${infer R}` - ? TakeName extends [infer Tag extends string, `$${infer R2}`] - ? [`${Acc}${A}`, `$${Tag}$`, R2] - : DollarOpen - : DollarOpen - : null; -type DollarBody< - S extends string, - Tag extends string, -> = S extends `${infer Body}${Tag}${infer R}` ? [`${Body}${Tag}`, R] : [S, ""]; - -type NChar = IdStart | Digit; - type RewriteCode< S extends string, Acc extends string, @@ -155,9 +101,3 @@ type RewriteCode< : never : RewriteCode : [`${Acc}${S}`, Names]; -type TakeName< - S extends string, - Acc extends string, -> = S extends `${infer C extends NChar}${infer R}` - ? TakeName - : [Acc, S]; diff --git a/packages/core/src/sql/slots.ts b/packages/core/src/sql/slots.ts index ca7bc046..2d9143ff 100644 --- a/packages/core/src/sql/slots.ts +++ b/packages/core/src/sql/slots.ts @@ -1,6 +1,8 @@ +import type { IsUnion, Members } from "../engine/object.js"; import type { CARRIED, Fragment, Query, TextType } from "../query.js"; import type { Skip } from "./inline.js"; -import type { MaxParamStr, UnionList } from "./splice.js"; +import type { Lexis } from "./lexis.js"; +import type { MaxParamStr } from "./splice.js"; /** A value that can be interpolated into SQL text, and an item of sql.combine/and/or. */ export type Slottable = @@ -11,21 +13,20 @@ export type SlotTexts = { readonly [K in keyof S]: { readonly text: TextType }; }; -export type CarriedOf = F extends { readonly [CARRIED]?: infer C } +export type CarriedOf = F extends { + readonly [CARRIED]?: infer C; +} ? C extends readonly unknown[] ? number extends C["length"] - ? Skip>> + ? Skip, L>> : C - : Skip>> - : Skip>>; + : Skip, L>> + : Skip, L>>; -export type SlotCarried = { readonly [K in keyof S]: CarriedOf }; +export type SlotCarried = { + readonly [K in keyof S]: CarriedOf; +}; -type IsUnion = T extends unknown - ? [U] extends [T] - ? false - : true - : never; type UnionSlots = { [K in keyof S]: true extends IsUnion ? K : never; }[keyof S]; @@ -46,4 +47,4 @@ type Expand = Ks extends readonly [ */ export type SlotVariants = [UnionSlots] extends [never] ? S - : Expand>>; + : Expand>>; diff --git a/packages/core/src/sql/splice.ts b/packages/core/src/sql/splice.ts index 18d7396b..0a9931d3 100644 --- a/packages/core/src/sql/splice.ts +++ b/packages/core/src/sql/splice.ts @@ -1,28 +1,37 @@ -import type { Digit } from "../engine/chars.js"; -import type { Lt, Members, Tup } from "../engine/object.js"; +import type { Digit, TakeDigits } from "../engine/chars.js"; +import type { Add, Lt, ToNum, Tup } from "../engine/num.js"; import type { SqlError } from "../errors.js"; -import type { NextOpaque } from "./named.js"; +import type { AllCode, Lexis, NextOpaque } from "./lexis.js"; -export type Splice = string extends Q +export type Splice = string extends Q ? string - : SpliceLoop, "">; + : Q extends `${string}{${string}}${string}` + ? AllCode extends true + ? SpliceCode, "", L> extends infer R + ? R extends [infer T extends string, number] + ? T + : R + : never + : SpliceLoop, "", L> + : Q; type SpliceLoop< Q extends string, S, Off extends number, Acc extends string, + L extends Lexis, > = Q extends `${string}{${string}}${string}` - ? NextOpaque extends [ + ? NextOpaque extends [ infer Code extends string, infer Seg extends string, infer After extends string, ] - ? SpliceCode extends infer R + ? SpliceCode extends infer R ? R extends [infer T extends string, infer NewOff extends number] ? Seg extends "" ? `${Acc}${T}` - : SpliceLoop + : SpliceLoop : R : never : never @@ -33,40 +42,54 @@ type SpliceCode< S, Off extends number, Acc extends string, + L extends Lexis, > = Q extends `${infer Pre}{${infer Name}}${infer Post}` ? IsSlotName extends true ? Name extends keyof S ? S[Name] extends { readonly text: infer FT extends string } ? string extends FT ? string - : Renumber extends [ + : Renumber extends [ infer NT extends string, infer NewOff extends number, ] - ? SpliceCode + ? SpliceCode : never : string : SqlError<`unknown fragment slot {${Name}}`, null> - : SpliceCode + : SpliceCode : [`${Acc}${Q}`, Off]; -/** The SQL outside strings, quoted identifiers, comments and dollar quotes. */ +/** The SQL outside strings, quoted identifiers, comments and dollar quotes, where it has slots. */ +type CodeOf< + Q extends string, + L extends Lexis, +> = Q extends `${string}{${string}}${string}` + ? AllCode extends true + ? Q + : CodeOnly + : ""; type CodeOnly< Q extends string, + L extends Lexis, Acc extends string = "", > = Q extends `${string}{${string}}${string}` - ? NextOpaque extends [ + ? NextOpaque extends [ infer Code extends string, infer Seg extends string, infer After extends string, ] ? Seg extends "" ? `${Acc}${Code}` - : CodeOnly + : CodeOnly : never : Acc; -export type SpliceCarried = CarriedIn, C, []>; +export type SpliceCarried = CarriedIn< + CodeOf, + C, + [] +>; type CarriedIn< Q extends string, C, @@ -81,13 +104,16 @@ type CarriedIn< : CarriedIn : Acc; +export type InlineName = `__typesql_inline${N}`; + /** Only the internal slots that interpolated fragments become; any other `{…}` is SQL text. */ -type IsSlotName = N extends `__typesql_inline${number}` - ? true - : false; +type IsSlotName = N extends InlineName ? true : false; -export type SlotNames = SlotNamesIn>; -type SlotNamesIn = +export type SlotNames = SlotNamesIn< + CodeOf +>; +/** The internal slots in `Q`, quoted or not. */ +export type SlotNamesIn = Q extends `${string}{${infer Name}}${infer Post}` ? IsSlotName extends true ? Name | SlotNamesIn @@ -98,20 +124,31 @@ type Renumber< T extends string, Off extends number, Acc extends string, + L extends Lexis, > = T extends `${string}$${string}` ? Off extends 0 ? T extends `${string}$0${string}` - ? RenumberLoop - : [`${Acc}${T}`, MaxParamStr] - : RenumberLoop + ? RenumberAll + : [`${Acc}${T}`, MaxParamStr] + : RenumberAll : [`${Acc}${T}`, Off]; +type RenumberAll< + T extends string, + Off extends number, + Acc extends string, + L extends Lexis, +> = + AllCode extends true + ? RenumberCode + : RenumberLoop; type RenumberLoop< T extends string, Off extends number, Max extends number, Acc extends string, + L extends Lexis, > = - NextOpaque extends [ + NextOpaque extends [ infer Code extends string, infer Seg extends string, infer After extends string, @@ -122,7 +159,7 @@ type RenumberLoop< ] ? Seg extends "" ? [`${Acc}${C}`, M] - : RenumberLoop + : RenumberLoop : never : never; type RenumberCode< @@ -145,22 +182,16 @@ type RenumberCode< : RenumberCode : [`${Acc}${T}`, Max]; -type TakeDigits< - S extends string, - Acc extends string, -> = S extends `${infer C extends Digit}${infer R}` - ? TakeDigits - : [Acc, S]; -type ToNum = S extends `${infer N extends number}` - ? N - : never; - export type MaxParamStr< T extends string, - Max extends number = 0, -> = T extends `${string}$${string}` ? MaxLoop : Max; -type MaxLoop = - NextOpaque extends [ + L extends Lexis, +> = T extends `${string}$${string}` + ? AllCode extends true + ? MaxIn + : MaxLoop + : 0; +type MaxLoop = + NextOpaque extends [ infer Code extends string, infer Seg extends string, infer After extends string, @@ -168,7 +199,7 @@ type MaxLoop = ? MaxIn extends infer M extends number ? Seg extends "" ? M - : MaxLoop + : MaxLoop : never : never; type MaxIn< @@ -182,16 +213,7 @@ type MaxIn< : MaxIn : Max; -type Add = [ - ...Tup, - ...Tup, -]["length"] & - number; - -export type UnknownTuple< - N extends number, - Acc extends unknown[] = [], -> = Acc["length"] extends N ? Acc : UnknownTuple; +export type UnknownTuple = Tup; export type Take< T extends readonly unknown[], @@ -205,7 +227,7 @@ export type Take< export type QuotedIdent< Parts extends readonly string[], - Q extends string = '"', + Q extends string, > = Parts extends readonly [infer P extends string] ? `${Q}${Dq}${Q}` : Parts extends readonly [ @@ -219,11 +241,10 @@ type Dq< Q extends string, > = S extends `${infer A}${Q}${infer B}` ? `${A}${Q}${Q}${Dq}` : S; -export type UnionList = Members; - export type JoinedText< Items extends readonly { readonly text: string }[], Sep extends string, + L extends Lexis, Off extends number = 0, Acc extends string = "", First extends boolean = true, @@ -234,13 +255,14 @@ export type JoinedText< ? H extends { readonly text: infer FT extends string } ? string extends FT ? string - : Renumber extends [ + : Renumber extends [ infer NT extends string, infer NewOff extends number, ] ? JoinedText< R, Sep, + L, NewOff, First extends true ? NT : `${Acc}${Sep}${NT}`, false diff --git a/packages/core/src/sql/text.ts b/packages/core/src/sql/text.ts index 13521aee..aa2bec7e 100644 --- a/packages/core/src/sql/text.ts +++ b/packages/core/src/sql/text.ts @@ -1,6 +1,7 @@ -import type { AnyErr, SqlError } from "../errors.js"; +import type { AnyErr } from "../errors.js"; import type { RewriteInlineStr } from "./inline.js"; -import type { RewriteNamedStr } from "./named.js"; +import type { Lexis } from "./lexis.js"; +import type { RewriteNamed, RewriteNamedStr } from "./named.js"; import type { MaxParamStr } from "./splice.js"; export type IsLiteral = string extends Q @@ -12,30 +13,32 @@ export type IsLiteral = string extends Q export type NotLiteralMessage = "the SQL text is not a string literal: pass values as parameters ($1, :name or ${sql.param(value)}), identifiers with sql.id(), reusable pieces as ${fragment}, or use sql.as() for dynamic SQL"; -export type Prepared = - RewriteNamedStr extends [ +export type Prepared = + RewriteNamedStr extends [ infer T1 extends string, infer Names extends string[], ] ? Names extends [] - ? WithInline> - : MaxParamStr extends 0 - ? WithInline - : SqlError< - "cannot mix named (:name) and positional ($n) parameters", - null - > + ? WithInline, L> + : MaxParamStr extends 0 + ? WithInline + : RewriteNamed : never; -type WithInline = - RewriteInlineStr extends infer R +type WithInline< + T1 extends string, + Names extends string[], + M extends number, + L extends Lexis, +> = + RewriteInlineStr extends infer R ? R extends [infer T2 extends string, infer Encs extends string[]] ? [T2, Names, Encs, M] : R : never; -export type PText = - Prepared extends [infer X extends string, ...unknown[]] ? X : Q; +export type PText = + Prepared extends [infer X extends string, ...unknown[]] ? X : Q; export type Surface = [Extract] extends [never] ? [R] extends [never] diff --git a/packages/core/src/sql/write.ts b/packages/core/src/sql/write.ts index fa1bd8e4..58060ecf 100644 --- a/packages/core/src/sql/write.ts +++ b/packages/core/src/sql/write.ts @@ -1,6 +1,14 @@ -import type { ValueEnc } from "./inline.js"; -import type { Next } from "./next.js"; -import type { QuotedIdent, UnionList } from "./splice.js"; +import type { Next } from "../engine/next.js"; +import type { Members } from "../engine/object.js"; +import type { ColDesc, Table, TableOf } from "../schema.js"; +import type { EngineTypes, TsParamTypeOf } from "./contract.js"; +import type { + AnyDefaultMarker, + JsonDefaultValue, + JsonValue, + ValueEnc, +} from "./inline.js"; +import type { QuotedIdent } from "./splice.js"; export type NoExtraKeys = Allowed extends unknown ? P & { @@ -61,20 +69,16 @@ type JoinPlaceholders< ? JoinPlaceholders, Acc extends "" ? `$${N}` : `${Acc}, $${N}`> : Acc; -export type SetRep = - UnionList> extends infer Ks extends readonly unknown[] +export type SetRep = + Members> extends infer Ks extends readonly unknown[] ? { readonly text: JoinAssignments; readonly carried: CarriedOfKeys, Ks>; } : never; -export type ValuesRep< - R, - Q extends string = '"', - Empty extends string = "DEFAULT VALUES", -> = - UnionList> extends infer Ks extends readonly unknown[] +export type ValuesRep = + Members> extends infer Ks extends readonly unknown[] ? Ks extends readonly [] ? { readonly text: Empty; readonly carried: [] } : { @@ -84,7 +88,7 @@ export type ValuesRep< : never; /** A value `sql.values` leaves out of its row: `undefined`, or a DEFAULT marker. */ -type MayOmit = undefined | `\u0000Du${string}\u0000`; +type MayOmit = undefined | AnyDefaultMarker; /** Keys every row has, with a value that is always sent. */ type AlwaysSent = { [K in KeysOf]: [R] extends [{ readonly [P in K]: ValueAt }] @@ -98,3 +102,94 @@ type PickEach = R extends unknown : never; /** The rows with only the columns `sql.values` always sends. */ export type SentRows = PickEach>; + +type SendsAt = R extends unknown + ? K extends keyof R + ? [Exclude] extends [never] + ? false + : true + : false + : never; +type OmitsAt = R extends unknown + ? K extends keyof R + ? Pick extends Required> + ? [Extract] extends [never] + ? false + : true + : true + : true + : never; +/** The columns one row sends and another row leaves out, or may. */ +type Varying = { + [K in KeysOf]: true extends SendsAt + ? true extends OmitsAt + ? K + : never + : never; +}[KeysOf]; +/** + * Without DEFAULT in a VALUES row (SQLite), the rows of `sql.values` send the + * same columns: a column one row sends, every row of several must send. + */ +export type SameColumns< + E extends EngineTypes, + R extends readonly unknown[], +> = E["valuesDefault"] extends true + ? unknown + : R extends readonly [unknown] + ? unknown + : { + // biome-ignore lint/plugin: any value but undefined is sent, whatever the column. + readonly [I in keyof R]: { + readonly [K in Varying]-?: NonNullable | null; + }; + }; + +type ColIn< + E extends EngineTypes, + TM, + DB, + D extends ColDesc, +> = D["type"] extends "null" + ? null + : + | (D["nullable"] extends true + ? TsParamTypeOf | null + : TsParamTypeOf) + | (D["type"] extends E["json"] ? JsonValue | JsonDefaultValue : never); + +export type SetInput = + TableOf extends infer Tbl extends Table + ? { + readonly [K in Writable]?: ColIn | undefined; + } + : never; + +type Writable = { + [K in keyof Tbl]: Tbl[K] extends + | { readonly generated: true } + | { readonly hidden: true } + ? never + : K; +}[keyof Tbl]; + +type Required_ = { + [K in keyof Tbl]: Tbl[K] extends { + readonly nullable: false; + readonly hasDefault: false; + } + ? K + : never; +}[keyof Tbl]; + +export type InsertInput = + TableOf extends infer Tbl extends Table + ? Spread< + { readonly [K in Required_]: ColIn } & { + readonly [K in Exclude, Required_>]?: + | ColIn + | undefined; + } + > + : never; +type Spread = { [K in keyof O]: O[K] }; diff --git a/packages/core/src/sqlite/dialect.ts b/packages/core/src/sqlite/dialect.ts index da90cbb1..3ea5ff59 100644 --- a/packages/core/src/sqlite/dialect.ts +++ b/packages/core/src/sqlite/dialect.ts @@ -1,45 +1,23 @@ -import type { - DIALECT, - Dialect, - DialectBrand, - DialectTypes, -} from "../dialect.js"; -import type { SqlPlugin } from "../plugin.js"; +import type { DdlCheck, Dialect, DialectTypes, PluginsOf } from "../dialect.js"; import type { Executor } from "../query.js"; -import type { Transactional } from "../transaction.js"; import type { ApplyDdl } from "./engine/infer.js"; -import type { ValidateDdl } from "./migrations.js"; -import type { Sql } from "./sql/types.js"; +import type { Sql, TransactionalSql } from "./sql/types.js"; import type { DefaultTypeMap } from "./type-map/index.js"; export interface Sqlite extends DialectTypes { readonly typeMap: DefaultTypeMap; - readonly sql: Sql< - this["db"], - this["tm"], - this["plugins"] extends readonly SqlPlugin[] ? this["plugins"] : [] - >; + readonly sql: Sql>; readonly transactionalSql: TransactionalSql< this["db"], this["tm"], - this["plugins"] extends readonly SqlPlugin[] ? this["plugins"] : [] + PluginsOf >; - readonly validateDdl: ValidateDdl; + readonly validateDdl: DdlCheck; readonly applyDdl: ApplyDdl; } -// Invariant parameters, as on Sql. -export interface TransactionalSql< - in out DB, - in out TM, - in out Plugins extends readonly SqlPlugin[] = [], -> extends Sql, - Transactional> { - readonly [DIALECT]?: DialectBrand<"sqlite", DB, TM, Plugins>; - withPlugin( - plugin: P, - ): TransactionalSql; -} +/** DDL script `Q` checked against schema `DB`: the script, or its error. */ +export type ValidateDdl = DdlCheck; export interface SqliteDialect extends Dialect { readonly name: "sqlite"; @@ -55,7 +33,19 @@ declare module "../create-sql.js" { } } +/** + * SQLite's lexis (see `Dialect`): names are quoted with "", `` and [], + * strings take no backslash escapes, and block comments don't nest. `$` + * belongs to a name after its first character, so `x$1` is a name. + * `SqliteLexis` states these rules for the types. + */ +export const sqliteLexis = + /(?(); + scan( + text, + ([t, n]) => { + if (n) seen.add(+n); + return t; + }, + sqliteLexis, + ); + return [...seen]; +} + +export function sqliteValue(value: V): V | string | 0 | 1 { + if (typeof value === "boolean") return value ? 1 : 0; + if (value instanceof Date) + throw new TypeError( + "typesql: SQLite has no date type; pass an ISO string (date.toISOString()) or a number", + ); + return jsonText(value); +} + +/** What SQLite drivers bind: node:sqlite, better-sqlite3 and bun:sqlite. */ +export type SqliteInput = null | number | bigint | string | ArrayBufferView; + +/** + * The named parameters of a statement that uses the parameters `numbers`, + * converted with sqliteValue: `{ $1: … }`, or `{ 1: … }` for + * better-sqlite3 (`bare`), which names them without their prefix. + */ +export function sqliteBindings( + numbers: readonly number[], + values: readonly unknown[], + bare = false, +) { + return Object.fromEntries( + numbers.map((n) => { + const input = sqliteValue(values[n - 1]); + if (!isInput(input)) + throw new TypeError( + `typesql: SQLite cannot bind ${typeof input} to parameter ${n}`, + ); + return [bare ? n : `$${n}`, input]; + }), + ); +} + +const isInput = (value: V): value is V & SqliteInput => + value === null || + typeof value === "number" || + typeof value === "bigint" || + typeof value === "string" || + ArrayBuffer.isView(value); + +/** `map` converts each value for the driver; sqliteValue by default. */ +export function splitParams( + text: string, + values: readonly V[], + map?: (value: V | undefined) => R, +): SplitStatement { + return splitAt(text, values, map ?? sqliteValue, sqliteLexis); +} + +export function positional( + text: string, + values: readonly V[], + map?: (value: V | undefined) => R, +): PositionalStatement { + return positionalAt( + text, + values, + map ?? sqliteValue, + sqliteLexis, + ); +} + +/** How long a statement waits for another connection's lock. */ +const BUSY_LIMIT = 30_000; + +/** + * Run a statement, waiting while another connection holds the database's + * lock. busy_timeout would wait inside SQLite and block the event loop, so a + * connection of the same process holding the lock could never release it. + */ +async function whenFree(step: () => T): Promise { + const start = Date.now(); + for (let delay = 5; ; delay = Math.min(delay * 2, 100)) { + try { + return step(); + } catch (error) { + // SQLITE_BUSY and its extended codes. + const busy = + typeof error === "object" && + error !== null && + "errcode" in error && + typeof error.errcode === "number" && + (error.errcode & 0xff) === 5; + if (!busy || Date.now() - start >= BUSY_LIMIT) throw error; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } +} + +/** A `node:sqlite` database, as the connection hands it out. */ +export interface SqliteClient { + prepare(sql: string): { + columns(): readonly unknown[]; + all(...params: unknown[]): unknown[]; + run(...params: unknown[]): { readonly changes: number | bigint }; + }; + exec(sql: string): void; + close(): void; +} + +/** + * One `node:sqlite` database for a file. It runs one statement per call: + * the migrator sends a script's statements one by one, as for any SQLite + * executor. + */ +export async function connectSqlite( + file: string, + options: { readonly readOnly?: boolean } = {}, +): Promise> { + const { DatabaseSync } = process.getBuiltinModule("node:sqlite"); + const db: SqliteClient = new DatabaseSync(file, { + readOnly: options.readOnly === true, + }); + return { + client: db, + execute: async (sql, values) => { + const statement = await whenFree(() => db.prepare(sql)); + const bindings = sqliteBindings(paramNumbers(sql), values); + if (statement.columns().length) { + const rows = await whenFree(() => statement.all(bindings)); + return { rows, rowCount: rows.length }; + } + const result = await whenFree(() => statement.run(bindings)); + return { rows: [], rowCount: Number(result.changes) }; + }, + close: async () => db.close(), + }; +} diff --git a/packages/core/src/sqlite/engine/ast.ts b/packages/core/src/sqlite/engine/ast.ts index 48382048..f0b8b8b3 100644 --- a/packages/core/src/sqlite/engine/ast.ts +++ b/packages/core/src/sqlite/engine/ast.ts @@ -4,6 +4,8 @@ export type ColRef = { readonly tbl: string | null; readonly name: string; readonly disp: string; + /** A double-quoted bare name, which SQLite would take as a string if no column has it. */ + readonly quoted?: true; }; export type NumLit = { readonly t: "num"; readonly v: string }; diff --git a/packages/core/src/sqlite/engine/binder/scope.ts b/packages/core/src/sqlite/engine/binder/scope.ts index 2a487cd2..aabcf2ff 100644 --- a/packages/core/src/sqlite/engine/binder/scope.ts +++ b/packages/core/src/sqlite/engine/binder/scope.ts @@ -1,8 +1,28 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { QueryContext, Scope } from "../../../engine/types.js"; +import type { + ColumnOrder, + KnownOrder, + VisibleKeys, +} from "../../../engine/family/ddl.js"; +import type { HintKind, NamedAt } from "../../../engine/family/diagnostics.js"; +import type { + AllCells, + BareIn, + CellsMap, + FindKey, + ItemCells, + LevelCol, + LevelHas, + NaturalCols, + NullLevel, + RefDisplay, + SkipIn, +} from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { IsUnion } from "../../../engine/object.js"; +import type { Scope } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; -import type { UnionList } from "../../../sql/splice.js"; import type { Expr, FromItem, FromJoin, TableRef } from "../ast.js"; import type { ProjectSelect } from "../project/select.js"; import type { CheckBool, TypeList } from "../typer/typeof.js"; @@ -16,16 +36,6 @@ import type { SqLevel, } from "../types.js"; -export type FindKey = Lc extends keyof O - ? Lc - : { - [K in keyof O & string]: K extends `$${string}` - ? never - : Lowercase extends Lc - ? K - : never; - }[keyof O & string]; - type MainSchema = "main" | "temp"; export type LookupRelation< @@ -48,10 +58,6 @@ export type LookupRelation< : null : null; -export type RefDisplay = Ref["schema"] extends string - ? `${Ref["schema"]}.${Ref["name"]}` - : Ref["name"]; - type TableCols = { readonly [K in keyof Tbl & string as Lowercase]: SqColOf< Tbl[K] extends { readonly type: infer T } ? T : "any", @@ -60,61 +66,16 @@ type TableCols = { >; }; -type TableOrder = - VisibleKeys extends infer V extends string - ? [V] extends [never] - ? [] - : Tbl[V & keyof Tbl] extends { readonly i: number } - ? ByIndex, 0, []> - : LowerAll> - : never; -type VisibleKeys = { - [K in keyof Tbl & string]: Tbl[K] extends { readonly hidden: true } +/** The columns `INSERT INTO t VALUES (…)` without a column list fills, in order: all but hidden and generated ones. */ +export type InsertOrder = KnownOrder>; +export type InsertKeys = { + [K in keyof Tbl & string]: Tbl[K] extends + | { readonly hidden: true } + | { readonly generated: true } ? never : K; }[keyof Tbl & string]; -export type InsertOrder = - TableOrder extends infer O extends readonly string[] - ? WithoutGenerated> - : []; -type GeneratedKeys = { - [K in keyof Tbl & string]: Tbl[K] extends { readonly generated: true } - ? Lowercase - : never; -}[keyof Tbl & string]; -type WithoutGenerated = O extends readonly [ - infer H extends string, - ...infer R, -] - ? H extends G - ? WithoutGenerated - : [H, ...WithoutGenerated] - : []; -// Each column's name by its position, built once: walking it is one lookup -// per column, where scanning every column for each position is quadratic. -// A dropped column leaves a gap; the walk goes on while a position is left. -type PositionMap = { - [K in keyof Tbl & string as Tbl[K] extends { - readonly i: infer I extends number; - } - ? I - : never]: Lowercase; -}; -type ByIndex< - M, - N extends number, - Acc extends readonly string[], - Seen extends number = never, -> = N extends keyof M - ? ByIndex, [...Acc, M[N] & string], Seen | N> - : [Exclude] extends [never] - ? Acc - : ByIndex, Acc, Seen>; -export type LowerAll = { - [I in keyof L]: Lowercase; -}; - type RowidName = { [K in keyof Tbl & string]: Tbl[K] extends { readonly rowid: true } ? K @@ -135,7 +96,7 @@ type TableItem = { readonly alias: Alias; readonly cols: TableCols; readonly skip: []; - readonly order: TableOrder; + readonly order: ColumnOrder>; readonly rowid: Rowid; }; @@ -157,8 +118,14 @@ type BindDbTable = Alias extends string ? Alias : Ref["name"] > extends infer It extends SqItem ? [It] - : SqlError<`no such table: ${RefDisplay}`, null> - : SqlError<`no such table: ${RefDisplay}`, null>; + : SqlError< + `no such table: ${RefDisplay}`, + NamedAt, "table"> + > + : SqlError< + `no such table: ${RefDisplay}`, + NamedAt, "table"> + >; type EntryItem< E extends { readonly cols: unknown; readonly order: unknown }, @@ -171,14 +138,6 @@ type EntryItem< readonly rowid: null; }; -export type CellsMap = { - readonly [E in C[number] as Lowercase]: SqColOf< - E["t"]["type"], - E["t"]["nullable"], - E["n"] - >; -}; - export type UniqueCells< C extends readonly Cell[], Seen extends string = never, @@ -209,7 +168,7 @@ type DerivedName = C extends { ? D : C["n"]; type Renamed = - Lowercase<`${N}:${K}`> extends Seen ? Renamed> : `${N}:${K}`; + Lowercase<`${N}:${K}`> extends Seen ? Renamed> : `${N}:${K}`; export type CteEntry = UniqueCells extends infer U extends readonly Cell[] @@ -271,7 +230,7 @@ type BindFunc< }, ] : SqlError<`too many arguments on ${Name}() - max 2`, null> - : SqlError<`no such table: ${Name}`, null>; + : SqlError<`no such table: ${Name}`, NamedAt>; export type BindFrom< F extends FromItem | null, @@ -327,16 +286,6 @@ type JoinUsing< ? CheckUsing : null; -type LevelHas = true extends { - [I in keyof L]: C extends keyof L[I]["cols"] - ? C extends L[I]["skip"][number] - ? false - : true - : false; -}[number] - ? true - : false; - type CheckUsing< U extends readonly string[], L extends SqLevel, @@ -344,74 +293,13 @@ type CheckUsing< I extends number = 0, > = U[I] extends infer C extends string ? [LevelHas, LevelHas] extends [true, true] - ? CheckUsing> + ? CheckUsing> : SqlError< `cannot join using column ${C} - column not present in both tables`, null > : U; -type NaturalCols = - LevelNames extends infer RN extends string - ? FilterNames, RN> - : never; -type LevelNames = { - [I in keyof L]: Exclude; -}[number]; -type LevelOrder< - L extends SqLevel, - I extends number = 0, - Acc extends readonly string[] = [], -> = L[I] extends SqItem - ? LevelOrder< - L, - Nx, - [...Acc, ...WithoutSkip] - > - : Acc; -type WithoutSkip = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends S - ? WithoutSkip - : [H, ...WithoutSkip] - : []; -type FilterNames = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends Keep - ? [H, ...FilterNames] - : FilterNames - : []; - -type NullItem = { - readonly alias: It["alias"]; - readonly cols: { - readonly [K in keyof It["cols"]]: SqColOf< - It["cols"][K]["type"], - true, - It["cols"][K]["n"] - >; - }; - readonly skip: It["skip"]; - readonly order: It["order"]; - readonly rowid: It["rowid"]; - readonly nullRowid: true; -}; -export type NullLevel = { - readonly [I in keyof L]: NullItem; -}; - -type SkipIn = { - readonly [I in keyof L]: { - readonly [P in keyof L[I]]: P extends "skip" - ? [...L[I]["skip"], ...Extract[]] - : L[I][P]; - }; -}; - type SubstIn = { readonly [I in keyof L]: L[I] & { readonly subst: M }; }; @@ -468,9 +356,6 @@ type MergedUsing< readonly order: []; readonly rowid: null; }; -type LevelCol = { - [I in keyof L]: C extends keyof L[I]["cols"] ? L[I]["cols"][C] : never; -}[number]; export type ResolveColumn< S extends Scope, @@ -482,36 +367,39 @@ export type ResolveColumn< Disp extends string, Aliases extends SqColMap = {}, > = Ref["tbl"] extends string - ? ResolveQualified - : ResolveBare; + ? ResolveQualified + : Ref extends { readonly quoted: true } + ? ResolveBare< + S, + Ref["name"], + `"${Disp}" - should this be a string literal in single-quotes?`, + Aliases, + Disp, + null + > + : ResolveBare; type ResolveBare< S extends Scope, Name extends string, Disp extends string, Aliases extends SqColMap, + /** The name as written, which errors point at. */ + Raw extends string, + H extends HintKind = "column", > = S extends readonly [infer L extends SqLevel, ...infer Rest extends Scope] ? BareIn extends infer M ? [M] extends [never] ? Name extends RowidAlias - ? RowidIn - : ResolveBare + ? RowidIn + : ResolveBare : IsUnion extends true - ? SqlError<`ambiguous column name: ${Disp}`, null> + ? SqlError<`ambiguous column name: ${Disp}`, NamedAt> : M : never : Name extends keyof Aliases ? Aliases[Name] - : SqlError<`no such column: ${Disp}`, null>; - -type BareIn = { - [I in keyof L]: Name extends keyof L[I]["cols"] - ? Name extends L[I]["skip"][number] - ? never - : Tag - : never; -}[number]; -type Tag = C & { readonly at: I }; + : SqlError<`no such column: ${Disp}`, NamedAt>; type RowidAlias = "rowid" | "oid" | "_rowid_"; type RowidIn< @@ -520,20 +408,23 @@ type RowidIn< Rest extends Scope, Aliases extends SqColMap, Name extends string, + /** The name as written, which errors point at. */ + Raw extends string, + H extends HintKind, > = { [I in keyof L]: L[I]["rowid"] extends string ? RowidCol & { readonly at: I } : never; }[number] extends infer M ? [M] extends [never] - ? ResolveBare + ? ResolveBare : IsUnion extends true - ? SqlError<`ambiguous column name: ${Disp}`, null> + ? SqlError<`ambiguous column name: ${Disp}`, NamedAt> : M : never; type RowidCol = { readonly type: "integer"; - readonly nullable: It extends { readonly nullRowid: true } ? true : false; + readonly nullable: It extends { readonly orig: unknown } ? true : false; readonly n: It["rowid"] & string; }; @@ -543,31 +434,27 @@ type ResolveQualified< Tbl extends string, Name extends string, Disp extends string, + /** The name as written, which errors point at. */ + Raw extends string, > = S extends readonly [infer L extends SqLevel, ...infer Rest extends Scope] ? { [I in keyof L]: L[I]["alias"] extends Tbl ? L[I] : never; }[number] extends infer It ? [It] extends [never] - ? ResolveQualified + ? ResolveQualified : IsUnion extends true - ? SqlError<`ambiguous column name: ${Disp}`, null> + ? SqlError<`ambiguous column name: ${Disp}`, NamedAt> : It extends SqItem ? Name extends keyof It["cols"] ? It["cols"][Name] : Name extends RowidAlias ? It["rowid"] extends string ? RowidCol - : SqlError<`no such column: ${Disp}`, null> - : SqlError<`no such column: ${Disp}`, null> + : SqlError<`no such column: ${Disp}`, NamedAt> + : SqlError<`no such column: ${Disp}`, NamedAt> : never : never - : SqlError<`no such column: ${Disp}`, null>; - -type IsUnion = T extends unknown - ? [U] extends [T] - ? false - : true - : never; + : SqlError<`no such column: ${Disp}`, NamedAt>; export type StarCells< L extends SqLevel, @@ -577,7 +464,7 @@ export type StarCells< [I in keyof L]: L[I]["alias"] extends Tbl ? L[I] : never; }[number] extends infer It ? [It] extends [never] - ? SqlError<`no such table: ${Tbl}`, null> + ? SqlError<`no such table: ${Tbl}`, NamedAt> : IsUnion extends true ? AmbiguousStar : It extends SqItem @@ -597,76 +484,15 @@ type DupAlias = L[I] extends SqItem ? L[I]["alias"] extends string ? L[I]["alias"] extends Seen ? L[I]["alias"] - : DupAlias, Seen | L[I]["alias"]> - : DupAlias, Seen> + : DupAlias, Seen | L[I]["alias"]> + : DupAlias, Seen> : null; -type AmbiguousStar = SqlError< - `ambiguous column name: main.${Tbl}.${It extends SqItem +type AmbiguousStar = ( + It extends SqItem ? It["order"][0] extends infer C extends keyof It["cols"] ? It["cols"][C]["n"] : "" - : ""}`, - null ->; - -type AllCells< - L extends SqLevel, - I extends number, - Acc extends readonly Cell[], -> = L[I] extends SqItem - ? AllCells< - L, - Nx, - [...Acc, ...ItemCells] - > - : Acc; -type ItemCells< - It extends SqItem, - O extends readonly string[], - Skip, - Acc extends readonly Cell[] = [], -> = O extends readonly [ - infer H extends string, - ...infer R extends readonly string[], -] - ? H extends Skip - ? ( - It extends { readonly subst: infer S } - ? H extends keyof S - ? S[H] - : null - : null - ) extends { - readonly type: infer T extends string; - readonly nullable: infer N extends boolean; - readonly n: infer D extends string; - } - ? ItemCells< - It, - R, - Skip, - [ - ...Acc, - { - readonly n: D; - readonly t: { readonly type: T; readonly nullable: N }; - }, - ] - > - : ItemCells - : ItemCells< - It, - R, - Skip, - [ - ...Acc, - { - readonly n: It["cols"][H]["n"]; - readonly t: { - readonly type: It["cols"][H]["type"]; - readonly nullable: It["cols"][H]["nullable"]; - }; - }, - ] - > - : Acc; + : "" +) extends infer C extends string + ? SqlError<`ambiguous column name: main.${Tbl}.${C}`, NamedAt> + : never; diff --git a/packages/core/src/sqlite/engine/ddl/apply.ts b/packages/core/src/sqlite/engine/ddl/apply.ts index df43c350..28db666b 100644 --- a/packages/core/src/sqlite/engine/ddl/apply.ts +++ b/packages/core/src/sqlite/engine/ddl/apply.ts @@ -1,12 +1,21 @@ +import type { + AppendIndex, + ColumnPks, + DupName, + Indexed, + LowerAll, +} from "../../../engine/family/ddl.js"; +import type { NamedAt, Unlocated } from "../../../engine/family/diagnostics.js"; +import type { FindKey, RenameCells } from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; import type { DelKey, Rebuild, RenameKey, SetKey, } from "../../../engine/object.js"; -import type { Nx } from "../../../engine/parse.js"; import type { Toks } from "../../../engine/tokens.js"; -import type { QueryContext } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Table, View } from "../../../schema.js"; import type { @@ -22,19 +31,19 @@ import type { Stmt, TableRef, } from "../ast.js"; -import type { - FindKey, - LookupRelation, - LowerAll, - UniqueCells, -} from "../binder/scope.js"; +import type { LookupRelation, UniqueCells } from "../binder/scope.js"; import type { Lex } from "../lexer.js"; import type { ParseScript } from "../parser/stmt.js"; +import type { SyntaxWords } from "../parser/util.js"; import type { ProjectDml } from "../project/dml.js"; -import type { ProjectSelect, RenameCells } from "../project/select.js"; +import type { ProjectSelect } from "../project/select.js"; import type { Cell, NormalizeType } from "../types.js"; -export type ApplyDdlAll = +export type ApplyDdlAll = Unlocated< + ApplyScript, + SyntaxWords +>; +type ApplyScript = Lex extends infer T extends Toks ? ParseScript extends infer S extends readonly Stmt[] ? ApplyLoop @@ -48,7 +57,7 @@ type ApplyLoop< > = S[I] extends infer St extends Stmt ? ApplyOne extends AnyErr ? ApplyOne - : ApplyLoop, S, Nx> + : ApplyLoop, S, Next> : DB; export type ApplyOne = S extends { @@ -105,31 +114,6 @@ type DisplayName = S extends { ? D : S["table"]["name"]; -type ColumnPks< - Cols extends readonly ColumnDef[], - Acc extends readonly string[] = [], -> = Cols extends readonly [ - infer H extends ColumnDef, - ...infer R extends readonly ColumnDef[], -] - ? ColumnPks< - R, - H["primaryKey"] extends false ? Acc : [...Acc, Lowercase] - > - : Acc; - -type DupName< - Cols extends readonly ColumnDef[], - Seen extends string = never, -> = Cols extends readonly [ - infer H extends ColumnDef, - ...infer R extends readonly ColumnDef[], -] - ? Lowercase extends Seen - ? H["name"] - : DupName> - : null; - type StrictTypes = "int" | "integer" | "real" | "text" | "blob" | "any"; type StrictCheck< @@ -170,13 +154,6 @@ type TableFromDefs = > : never; -type Indexed = { - [I in keyof L]: { - readonly c: L[I]; - readonly i: I extends `${infer N extends number}` ? N : 0; - }; -}; - type BuildTable = Rebuild<{ [D in Indexed[number] as D["c"]["name"]]: ColDesc< D["c"], @@ -288,7 +265,7 @@ type CtasLoop< ? CtasLoop< Ts, C, - Nx, + Next, [ ...Acc, E extends { readonly t: "col" } @@ -367,14 +344,14 @@ type AlterEffect = : AlterColumns extends AnyErr ? AlterColumns : SetKey> - : SqlError<`no such table: ${S["table"]["name"]}`, null> - : SqlError<`no such table: ${S["table"]["name"]}`, null>; - -type MaxIndex = { - [P in keyof Rel]: Rel[P] extends { readonly i: infer N extends number } - ? N - : never; -}[keyof Rel]; + : SqlError< + `no such table: ${S["table"]["name"]}`, + NamedAt + > + : SqlError< + `no such table: ${S["table"]["name"]}`, + NamedAt + >; type AlterColumns = A extends { readonly t: "rename_column"; @@ -383,7 +360,7 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string ? [Old] extends [never] - ? SqlError<`no such column: "${F}"`, null> + ? SqlError<`no such column: "${F}"`, NamedAt<`"${F}"`, "column">> : [FindKey>] extends [never] ? RenameKey : SqlError<`duplicate column name: ${To}`, null> @@ -403,7 +380,7 @@ type AlterColumns = A extends { } ? FindKey extends infer Old extends string ? [Old] extends [never] - ? SqlError<`no such column: "${N}"`, null> + ? SqlError<`no such column: "${N}"`, NamedAt<`"${N}"`, "column">> : Rel[Old] extends { readonly rowid: true } ? SqlError<`cannot drop PRIMARY KEY column: "${Old}"`, null> : [Exclude] extends [never] @@ -419,26 +396,9 @@ type AddedCol = MkCol< NormalizeType, C["notNull"] extends true ? false : true, C["hasDefault"], - NextIndex>, + AppendIndex, C["generated"] >; -// The position after the last column. Positions are 0…n-1 unless a column -// was dropped: count up while each one exists (one membership check per -// column), and look for the largest one only after a gap. -type NextIndex< - U extends number, - A extends unknown[] = [], - Seen extends number = never, -> = A["length"] extends U - ? NextIndex - : [Exclude] extends [never] - ? A["length"] - : Nx>; -type MaxOf = [U] extends [never] - ? 0 - : [Exclude] extends [never] - ? A["length"] - : MaxOf, [...A, 0]>; type DropEffect = S["kind"] extends "table" | "view" ? LookupRelation extends [infer Rel, infer K extends string] @@ -451,7 +411,10 @@ type DropEffect = S["kind"] extends "table" | "view" : SqlError<`use DROP TABLE to delete table ${K}`, null> : S["ifExists"] extends true ? DB - : SqlError<`no such ${S["kind"]}: ${S["name"]["name"]}`, null> + : SqlError< + `no such ${S["kind"]}: ${S["name"]["name"]}`, + S["kind"] extends "table" ? NamedAt : null + > : DB; type IndexEffect = S extends { @@ -459,7 +422,10 @@ type IndexEffect = S extends { } ? Exists extends true ? DB - : SqlError<`no such table: main.${Ref["name"]}`, null> + : SqlError< + `no such table: main.${Ref["name"]}`, + NamedAt<`main.${Ref["name"]}`, "table"> + > : DB; type VirtualEffect = diff --git a/packages/core/src/sqlite/engine/diagnostics.ts b/packages/core/src/sqlite/engine/diagnostics.ts index 05d4a14b..6452f9e3 100644 --- a/packages/core/src/sqlite/engine/diagnostics.ts +++ b/packages/core/src/sqlite/engine/diagnostics.ts @@ -1,115 +1,15 @@ -import type { Nx } from "../../engine/parse.js"; -import type { Best, IsRelation, RelCols } from "../../engine/suggest.js"; +import type { Located } from "../../engine/family/diagnostics.js"; import type { Toks } from "../../engine/tokens.js"; -import type { SqlError } from "../../errors.js"; +import type { SyntaxWords } from "./parser/util.js"; import type { FunctionCatalog, Functions } from "./typer/functions.js"; -export type Reposition = - E extends SqlError - ? At extends null - ? SqlError, Locate> - : SqlError - : E; +/** The error with its position in the statement `T`, and a hint for unknown names. */ +export type Reposition = Located< + E, + T, + DB, + Builtins, + SyntaxWords +>; -type WithHint = M extends - | `no such column: ${infer Name}` - | `table ${string} has no column named ${infer Name}` - ? Name extends `${string} - should this be${string}` - ? M - : Hinted< - M, - Best< - LastPart>, - ColumnsOf>, - true - >, - "column" - > - : M extends `no such table: ${infer Name}` - ? Hinted>, RelNames, true>, "table"> - : M extends `no such function: ${infer Name}` - ? Hinted, FnNames, true>, "function"> - : M; - -type Hinted = S extends string - ? `${M}. Perhaps you meant the ${What} "${S}"?` - : M; - -type RelNames = { - [K in keyof DB & string]: K extends `$${string}` - ? never - : IsRelation extends true - ? K - : never; -}[keyof DB & string]; - -type Mentioned = T[I] extends { - readonly k: string; -} - ? Mentioned, Acc | RelAt> - : Acc; -type RelAt = Tok extends { - readonly k: "w" | "id"; - readonly v: infer V; -} - ? { - [K in RelNames]: Lowercase extends V ? K : never; - }[RelNames] - : never; - -type ColumnsOf = { - [K in RelNames]: K extends Rels ? RelCols : never; -}[RelNames]; - -type FnNames = - | (keyof Functions & string) - | (keyof FunctionCatalog & string) - | (DB extends { readonly $functions: infer F } ? keyof F & string : never); - -type Locate = M extends - | `no such column: ${infer Name}` - | `ambiguous column name: ${infer Name}` - ? FindName>, 0> - : M extends - | `no such table: ${infer Name}` - | `no such function: ${infer Name}` - | `cannot modify ${infer Name} because it is a view` - | `table ${infer Name} has no column named ${string}` - | `no such collation sequence: ${infer Name}` - ? FindName>, 0> - : M extends - | `wrong number of arguments to function ${infer Name}()` - | `misuse of aggregate function ${infer Name}()` - | `misuse of window function ${infer Name}()` - | `${infer Name}() may not be used as a window function` - ? FindName, 0> - : M extends `${string}ORDER BY${string}` - ? FindName - : M extends `${string}GROUP BY${string}` - ? FindName - : M extends - | `${string} values for ${string}` - | `table ${string} has ${string} columns but ${string}` - ? FindName - : M extends `${string}RETURNING${string}` - ? FindName - : 0; - -type Unhint = N extends `"${infer X}" - ${string}` - ? Lowercase - : Lowercase; -type LastPart = N extends `${string}.${infer R}` - ? LastPart - : N; - -type FindName< - T extends Toks, - Name extends string, - I extends number, -> = T[I] extends { - readonly k: string; -} - ? T[I] extends { readonly k: "w" | "id"; readonly v: Name } - ? I - : FindName> - : 0; +type Builtins = keyof Functions | keyof FunctionCatalog; diff --git a/packages/core/src/sqlite/engine/engine.ts b/packages/core/src/sqlite/engine/engine.ts index 3b20f4b0..db001ad5 100644 --- a/packages/core/src/sqlite/engine/engine.ts +++ b/packages/core/src/sqlite/engine/engine.ts @@ -1,3 +1,4 @@ +import type { ShapeBase } from "../../schema.js"; import type { AnalysisFn, DisplayFn, @@ -5,7 +6,8 @@ import type { ParamFn, RowFn, TokensFn, -} from "../../engine/contract.js"; +} from "../../sql/contract.js"; +import type { Lexis } from "../../sql/lexis.js"; import type { TsParamTypeOf } from "../type-map/index.js"; import type { Analysis, RowFrom } from "./infer.js"; import type { Lex } from "./lexer.js"; @@ -17,6 +19,24 @@ export interface SqliteEngine extends EngineTypes { readonly param: SqliteParam; readonly display: SqliteDisplay; readonly json: "json"; + readonly arrayNote: ""; + readonly quote: '"'; + readonly emptyRow: "DEFAULT VALUES"; + readonly valuesDefault: false; + readonly lexis: SqliteLexis; +} + +/** + * SQLite's text rules, as `sqliteLexis`: names are quoted with "", `` and [], + * strings take no backslash escapes, and block comments don't nest. + */ +export interface SqliteLexis extends Lexis { + readonly extra: "`" | "["; + readonly backslash: never; + readonly spacedDash: false; + readonly nest: false; + readonly dollar: false; + readonly names: '"' | "`" | "["; } interface SqliteAnalysis extends AnalysisFn { @@ -32,5 +52,5 @@ interface SqliteParam extends ParamFn { readonly out: TsParamTypeOf; } interface SqliteDisplay extends DisplayFn { - readonly out: this["t"]; + readonly out: ShapeBase; } diff --git a/packages/core/src/sqlite/engine/index.ts b/packages/core/src/sqlite/engine/index.ts index ab906be4..dc5da25a 100644 --- a/packages/core/src/sqlite/engine/index.ts +++ b/packages/core/src/sqlite/engine/index.ts @@ -1,9 +1,8 @@ export type * from "../../index.js"; -export type { SplicedText } from "../../sql/call.js"; export type * from "../index.js"; export type { Col } from "../schema/index.js"; export type * as Ast from "./ast.js"; -export type { SqliteEngine } from "./engine.js"; +export type { SqliteEngine, SqliteLexis } from "./engine.js"; export type { Analysis, Analyze, @@ -12,6 +11,7 @@ export type { KindOf, NamesOf, ParamsArgs, + SplicedText, } from "./infer.js"; export type { Lex } from "./lexer.js"; export type { ParseProgram, ParseStatement } from "./parser/stmt.js"; diff --git a/packages/core/src/sqlite/engine/infer.ts b/packages/core/src/sqlite/engine/infer.ts index 459d4a00..6b904a64 100644 --- a/packages/core/src/sqlite/engine/infer.ts +++ b/packages/core/src/sqlite/engine/infer.ts @@ -1,8 +1,11 @@ -import type * as Shared from "../../engine/infer.js"; +import type { QueryContext } from "../../engine/family/types.js"; +import type { Simplify } from "../../engine/object.js"; import type { MaxParam, ParamsToMap } from "../../engine/params.js"; import type { Toks } from "../../engine/tokens.js"; -import type { Cols, QueryContext, Simplify } from "../../engine/types.js"; +import type { Cols } from "../../engine/types.js"; import type { AnyErr } from "../../errors.js"; +import type * as Call from "../../sql/call.js"; +import type * as Shared from "../../sql/infer.js"; import type { RewriteNamed } from "../../sql/named.js"; import type { TsOf } from "../type-map/index.js"; import type { DmlStmt, SelectStmt, Stmt } from "./ast.js"; @@ -10,17 +13,17 @@ import type { ApplyDdlAll, ApplyOne } from "./ddl/apply.js"; import type { Reposition } from "./diagnostics.js"; import type { SqliteEngine } from "./engine.js"; import type { Lex } from "./lexer.js"; -import type { ParseProgram } from "./parser/stmt.js"; +import type { ParseProgramAt } from "./parser/stmt.js"; import type { ParamsOfDml, ProjectDml } from "./project/dml.js"; import type { ProjectSelect } from "./project/select.js"; import type { ParamsOfSelect } from "./typer/params.js"; export type Analyze = - RewriteNamed extends infer RN + RewriteNamed extends infer RN ? RN extends [infer Q1 extends string, infer Names] ? Lex extends infer T ? T extends Toks - ? ParseProgram extends infer S + ? ParseProgramAt extends infer S ? S extends Stmt ? ProjectStmt extends infer C ? C extends Cols | null @@ -114,3 +117,8 @@ export type Validate = Shared.Validate< export type KindOf = Shared.KindOf; export type ApplyDdl = ApplyDdlAll; + +export type SplicedText = Call.SplicedText< + Q, + SqliteEngine["lexis"] +>; diff --git a/packages/core/src/sqlite/engine/lexer.ts b/packages/core/src/sqlite/engine/lexer.ts index b6f84016..e4cf49a6 100644 --- a/packages/core/src/sqlite/engine/lexer.ts +++ b/packages/core/src/sqlite/engine/lexer.ts @@ -1,28 +1,33 @@ import type { Digit, HexDigit, + IdRun, IdStart, NonIdAscii, Ws, } from "../../engine/chars.js"; -import type { SqlError } from "../../errors.js"; -import type { Next } from "../../sql/next.js"; import type { - SqBlob, - SqIdent, - SqNum, - SqOp, - SqParam, - SqStr, - SqWord, -} from "./tokens.js"; - -type Nx = Next; + BadParam, + Blob, + ChunkSize, + DollarParam, + Escape, + Ident, + IsHex, + LexErr, + Num, + Op, + Param, + Str, + TakeQuoted, + TakeWord, + Word, +} from "../../engine/family/lex.js"; +import type { Next } from "../../engine/next.js"; +import type { SqlError } from "../../errors.js"; export type Lex = LexAll; -type ChunkSize = 200; - type LexAll< S extends string, Acc extends readonly unknown[], @@ -47,7 +52,6 @@ type LexAll< : never : never; -type Err = ["err", M, Acc]; type Unrecognized = `unrecognized token: "${T}"`; type LexChunk< @@ -59,7 +63,7 @@ type LexChunk< ? ["done", Acc] : B extends ChunkSize ? ["more", Acc, S, G] - : LexStep, G>; + : LexStep, G>; type Punct = "(" | ")" | "," | ";" | "~" | "&" | "+" | "*" | "%"; @@ -80,7 +84,7 @@ type LexStep< : C extends Digit ? LexNumber : C extends Punct - ? LexChunk], B, ""> + ? LexChunk], B, ""> : C extends "'" ? LexString : C extends '"' | "`" @@ -90,7 +94,7 @@ type LexStep< : C extends "." ? R extends `${Digit}${string}` ? LexNumber - : LexChunk], B, ""> + : LexChunk], B, ""> : LexSymbol : never; @@ -106,45 +110,42 @@ type LexSymbol< ? LexChunk : LexChunk<"", Acc, B, `${G}--${R1}`> : R extends `>>${infer R1}` - ? LexChunk>", G>], B, ""> + ? LexChunk>", G>], B, ""> : R extends `>${infer R1}` - ? LexChunk", G>], B, ""> - : LexChunk], B, ""> + ? LexChunk", G>], B, ""> + : LexChunk], B, ""> : C extends "/" ? R extends `*${infer R1}` ? R1 extends `${infer Body}*/${infer R2}` ? LexChunk : LexChunk<"", Acc, B, `${G}/*${R1}`> - : LexChunk], B, ""> + : LexChunk], B, ""> : C extends "|" ? R extends `|${infer R1}` - ? LexChunk], B, ""> - : LexChunk], B, ""> + ? LexChunk], B, ""> + : LexChunk], B, ""> : C extends "<" ? R extends `${infer O extends "=" | ">" | "<"}${infer R1}` - ? LexChunk], B, ""> - : LexChunk], B, ""> + ? LexChunk], B, ""> + : LexChunk], B, ""> : C extends ">" ? R extends `${infer O extends "=" | ">"}${infer R1}` - ? LexChunk${O}`, G>], B, ""> - : LexChunk", G>], B, ""> + ? LexChunk${O}`, G>], B, ""> + : LexChunk", G>], B, ""> : C extends "=" ? R extends `=${infer R1}` - ? LexChunk], B, ""> - : LexChunk + ? LexChunk], B, ""> + : LexChunk : C extends "!" ? R extends `=${infer R1}` - ? LexChunk - : Err, Acc> + ? LexChunk + : LexErr, Acc> : C extends "$" ? LexDollar : C extends "?" | "@" | ":" - ? Err< - `TypeSQL parameters are written $1 or :name, not "${C}${IdRun}"`, - Acc - > + ? LexErr}`>, Acc> : C extends NonIdAscii - ? Err, Acc> + ? LexErr, Acc> : LexWord; type LexDollar< @@ -152,16 +153,10 @@ type LexDollar< Acc extends readonly unknown[], B extends number, G extends string, -> = R extends `${infer D extends Digit}${infer R1}` - ? TakeDigits extends [infer V extends string, infer R2 extends string] - ? IdRun extends "" - ? LexChunk, G>], B, ""> - : Err< - `TypeSQL parameters are written $1 or :name, not "$${V}${IdRun}"`, - Acc - > - : never - : Err<`TypeSQL parameters are written $1 or :name, not "$${IdRun}"`, Acc>; +> = + DollarParam extends [infer N extends number, infer R2 extends string] + ? LexChunk], B, ""> + : LexErr & string, Acc>; type LexWord< R extends string, @@ -171,7 +166,7 @@ type LexWord< G extends string, > = TakeWord extends [infer W extends string, infer R2 extends string] - ? LexChunk, W, G>], B, ""> + ? LexChunk, W, G>], B, ""> : never; type LexBlob< @@ -183,18 +178,10 @@ type LexBlob< > = R1 extends `${infer Hex}'${infer R2}` ? IsHex extends true ? IdRun extends "" - ? LexChunk], B, ""> - : Err}`>, Acc> - : Err, Acc> - : Err, Acc>; -type IsHex< - S extends string, - Even extends boolean, -> = S extends `${infer C}${infer R}` - ? C extends HexDigit - ? IsHex - : false - : Even; + ? LexChunk], B, ""> + : LexErr}`>, Acc> + : LexErr, Acc> + : LexErr, Acc>; type LexString< R extends string, @@ -206,8 +193,8 @@ type LexString< infer V extends string, infer R2 extends string, ] - ? LexChunk}'`, G>], B, ""> - : Err, Acc>; + ? LexChunk}'`, G>], B, ""> + : LexErr, Acc>; type LexQuoted< R extends string, @@ -227,7 +214,7 @@ type LexQuoted< R2, [ ...Acc, - SqIdent< + Ident< Lowercase, V, `${Open}${Close extends "]" ? V : Escape}${Close}`, @@ -237,25 +224,7 @@ type LexQuoted< B, "" > - : Err, Acc>; - -type TakeQuoted< - S extends string, - Qc extends string, - Acc extends string, -> = S extends `${infer Body}${Qc}${infer R}` - ? R extends `${Qc}${infer R2}` - ? TakeQuoted - : [`${Acc}${Body}`, R] - : null; - -type Escape< - S extends string, - Q extends string, - Acc extends string = "", -> = S extends `${infer A}${Q}${infer B}` - ? Escape - : `${Acc}${S}`; + : LexErr, Acc>; type LexNumber< S extends string, @@ -265,10 +234,10 @@ type LexNumber< > = TakeNumber extends [infer V extends string, infer R2 extends string] ? IdRun extends "" - ? LexChunk], B, ""> - : Err}`>, Acc> + ? LexChunk], B, ""> + : LexErr}`>, Acc> : TakeNumber extends infer Bad extends string - ? Err, Acc> + ? LexErr, Acc> : never; type TakeNumber = @@ -326,42 +295,3 @@ type TakeExponent< : [`${V}${E}${X}`, R3] : never : [V, S]; - -type TakeDigits< - S extends string, - Acc extends string, -> = S extends `${infer C extends Digit}${infer R}` - ? TakeDigits - : [Acc, S]; - -type IdRun< - R extends string, - Acc extends string = "", -> = R extends `${infer C}${infer R1}` - ? C extends NonIdAscii - ? Acc - : IdRun - : Acc; - -type TakeWord< - S extends string, - Acc extends string, -> = S extends `${infer A}${infer B}${infer C}${infer D}${infer R}` - ? A extends NonIdAscii - ? [Acc, S] - : B extends NonIdAscii - ? [`${Acc}${A}`, `${B}${C}${D}${R}`] - : C extends NonIdAscii - ? [`${Acc}${A}${B}`, `${C}${D}${R}`] - : D extends NonIdAscii - ? [`${Acc}${A}${B}${C}`, `${D}${R}`] - : TakeWord - : S extends `${infer C}${infer R}` - ? C extends NonIdAscii - ? [Acc, S] - : TakeWord - : [Acc, S]; - -type ToNum = S extends `${infer N extends number}` - ? N - : never; diff --git a/packages/core/src/sqlite/engine/parser/ddl.ts b/packages/core/src/sqlite/engine/parser/ddl.ts index ad37b6e6..d2f4cac1 100644 --- a/packages/core/src/sqlite/engine/parser/ddl.ts +++ b/packages/core/src/sqlite/engine/parser/ddl.ts @@ -1,3 +1,16 @@ +import type { + CloseOk, + Expect, + IfExists, + IfNotExists, + IsOp, + IsWord, + Name, + SkipColumn, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { SqlError } from "../../../errors.js"; import type { ColumnDef, Expr, SelectStmt } from "../ast.js"; import type { @@ -7,66 +20,37 @@ import type { ParseTypeName, } from "./expr.js"; import type { ParseSubquery } from "./select.js"; -import type { - CloseOk, - Expect, - Ident, - IdentList, - IsOp, - IsWord, - Name, - Nx, - Ok, - OptCols, - SyntaxErr, - TableRefAt, - Toks, -} from "./util.js"; +import type { Ident, IdentList, OptCols, TableRefAt } from "./util.js"; export type ParseCreate = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? IsWord extends true - ? ParseCreateTable> + ? ParseCreateTable> : IsWord extends true - ? ParseCreateView> + ? ParseCreateView> : IsWord extends true - ? SkipTrigger> + ? SkipTrigger> : J extends I ? IsWord extends true - ? IsWord, "index"> extends true - ? ParseCreateIndex>> - : SyntaxErr> + ? IsWord, "index"> extends true + ? ParseCreateIndex>> + : SyntaxErr> : IsWord extends true - ? ParseCreateIndex> + ? ParseCreateIndex> : IsWord extends true - ? ParseVirtual> + ? ParseVirtual> : SyntaxErr : SyntaxErr : never; -type IfNotExists = - IsWord extends true - ? IsWord, "not"> extends true - ? IsWord>, "exists"> extends true - ? [true, Nx>>] - : SyntaxErr>> - : SyntaxErr> - : [false, I]; -type IfExists = - IsWord extends true - ? IsWord, "exists"> extends true - ? [true, Nx>] - : SyntaxErr> - : [false, I]; - type ParseCreateTable = IfNotExists extends [infer Ine extends boolean, infer J extends number] ? TableRefAt extends Ok ? IsWord extends true - ? ParseSubquery> extends Ok< + ? ParseSubquery> extends Ok< infer S extends SelectStmt, infer J3 extends number > @@ -79,7 +63,7 @@ type ParseCreateTable = }, J3 > - : ParseSubquery> + : ParseSubquery> : Expect extends infer J3 extends number ? ColumnDefs extends Ok< infer Cols extends readonly ColumnDef[], @@ -124,18 +108,18 @@ type TableOptions< Strict extends boolean, > = IsWord extends true - ? IsWord, "rowid"> extends true - ? IsOp>, ","> extends true - ? TableOptions>>, true, Strict> - : Ok<[true, Strict], Nx>> + ? IsWord, "rowid"> extends true + ? IsOp>, ","> extends true + ? TableOptions>>, true, Strict> + : Ok<[true, Strict], Next>> : SqlError< - `unknown table option: ${T[Nx] extends { readonly v: infer V extends string } ? V : ""}`, - Nx + `unknown table option: ${T[Next] extends { readonly v: infer V extends string } ? V : ""}`, + Next > : IsWord extends true - ? IsOp, ","> extends true - ? TableOptions>, NoRowid, true> - : Ok<[NoRowid, true], Nx> + ? IsOp, ","> extends true + ? TableOptions>, NoRowid, true> + : Ok<[NoRowid, true], Next> : Ok<[NoRowid, Strict], I>; type ColumnDefs< @@ -150,17 +134,17 @@ type ColumnDefs< ? IsOp extends true ? IsWord< T, - Nx, + Next, "constraint" | "primary" | "unique" | "check" | "foreign" > extends true - ? Ok<[...Acc, Col], Nx> - : ColumnDefs, [...Acc, Col]> + ? Ok<[...Acc, Col], Next> + : ColumnDefs, [...Acc, Col]> : Ok<[...Acc, Col], J> : ParseColumnDef; type ParseColumnDef = Ident extends infer N extends Name - ? ParseTypeName> extends Ok< + ? ParseTypeName> extends Ok< infer Ty extends string, infer J extends number > @@ -190,7 +174,7 @@ type ParseColumnDef = J2 > : ColConstraints - : ParseTypeName> + : ParseTypeName> : SyntaxErr; type NoFlags = { @@ -214,30 +198,30 @@ type Set_ = { type ConflictClause = IsWord extends true - ? IsWord, "conflict"> extends true + ? IsWord, "conflict"> extends true ? IsWord< T, - Nx>, + Next>, "rollback" | "abort" | "fail" | "ignore" | "replace" > extends true - ? Nx>> - : SyntaxErr>> - : SyntaxErr> + ? Next>> + : SyntaxErr>> + : SyntaxErr> : I; type ColConstraints = IsWord extends true - ? Ident> extends Name - ? ColConstraints>, F> - : SyntaxErr> + ? Ident> extends Name + ? ColConstraints>, F> + : SyntaxErr> : IsWord extends true - ? IsWord, "key"> extends true + ? IsWord, "key"> extends true ? ( - IsWord>, "desc"> extends true - ? [true, Nx>>] - : IsWord>, "asc"> extends true - ? [false, Nx>>] - : [false, Nx>] + IsWord>, "desc"> extends true + ? [true, Next>>] + : IsWord>, "asc"> extends true + ? [false, Next>>] + : [false, Next>] ) extends [infer D extends boolean, infer J extends number] ? ColNext< T, @@ -245,19 +229,19 @@ type ColConstraints = D extends true ? Set_, "desc"> : Set_ > : never - : SyntaxErr> + : SyntaxErr> : IsWord extends true - ? IsWord, "null"> extends true - ? ColNext>>, Set_> - : SyntaxErr> + ? IsWord, "null"> extends true + ? ColNext>>, Set_> + : SyntaxErr> : IsWord extends true - ? ColNext>, F> + ? ColNext>, F> : IsWord extends true - ? ColNext>, F> + ? ColNext>, F> : IsWord extends true - ? ColNext>, Set_> + ? ColNext>, Set_> : IsWord extends true - ? ColNext>, F> + ? ColNext>, F> : IsWord extends true ? ColNext< T, @@ -270,17 +254,17 @@ type ColNext = J extends number : J; type SkipWordAt = J extends number ? IsWord extends true - ? Nx + ? Next : J : J; type GeneratedAs = IsWord extends true - ? SkipWordAt>, "stored" | "virtual"> - : IsWord, "always"> extends true - ? IsWord>, "as"> extends true - ? SkipWordAt>>>, "stored" | "virtual"> - : SyntaxErr>> - : SyntaxErr>; + ? SkipWordAt>, "stored" | "virtual"> + : IsWord, "always"> extends true + ? IsWord>, "as"> extends true + ? SkipWordAt>>>, "stored" | "virtual"> + : SyntaxErr>> + : SyntaxErr>; type ParenExpr = Expect extends infer J extends number @@ -293,60 +277,60 @@ type DefaultValue = IsOp extends true ? ParenExpr : IsOp extends true - ? T[Nx] extends { readonly k: "n" } - ? Nx> - : SyntaxErr> + ? T[Next] extends { readonly k: "n" } + ? Next> + : SyntaxErr> : T[I] extends { readonly k: "n" | "s" | "id" } - ? Nx + ? Next : T[I] extends { readonly k: "w" } ? IsWord< T, I, "null" | "current_time" | "current_date" | "current_timestamp" > extends true - ? Nx + ? Next : Ident extends Name - ? Nx + ? Next : SyntaxErr : SyntaxErr; type ForeignKey = Ident extends Name - ? OptCols> extends Ok + ? OptCols> extends Ok ? FkActions - : OptCols> + : OptCols> : SyntaxErr; type FkActions = IsWord extends true - ? IsWord, "delete" | "update"> extends true - ? IsWord>, "set"> extends true - ? IsWord>>, "null" | "default"> extends true - ? FkActions>>>> - : SyntaxErr>>> - : IsWord>, "cascade" | "restrict"> extends true - ? FkActions>>> - : IsWord>, "no"> extends true - ? IsWord>>, "action"> extends true - ? FkActions>>>> - : SyntaxErr>>> - : SyntaxErr>> - : SyntaxErr> + ? IsWord, "delete" | "update"> extends true + ? IsWord>, "set"> extends true + ? IsWord>>, "null" | "default"> extends true + ? FkActions>>>> + : SyntaxErr>>> + : IsWord>, "cascade" | "restrict"> extends true + ? FkActions>>> + : IsWord>, "no"> extends true + ? IsWord>>, "action"> extends true + ? FkActions>>>> + : SyntaxErr>>> + : SyntaxErr>> + : SyntaxErr> : IsWord extends true - ? Ident> extends Name - ? FkActions>> - : SyntaxErr> + ? Ident> extends Name + ? FkActions>> + : SyntaxErr> : IsWord extends true - ? IsWord, "deferrable"> extends true - ? Deferrable>> + ? IsWord, "deferrable"> extends true + ? Deferrable>> : I : IsWord extends true - ? Deferrable> + ? Deferrable> : I; type Deferrable = IsWord extends true - ? IsWord, "deferred" | "immediate"> extends true - ? Nx> - : SyntaxErr> + ? IsWord, "deferred" | "immediate"> extends true + ? Next> + : SyntaxErr> : I; type TableConstraints< @@ -355,35 +339,35 @@ type TableConstraints< Pk extends readonly string[], > = IsWord extends true - ? Ident> extends Name - ? TableConstraints>, Pk> - : SyntaxErr> + ? Ident> extends Name + ? TableConstraints>, Pk> + : SyntaxErr> : IsWord extends true - ? IsWord, "key"> extends true - ? ParenOrderList>> extends Ok< + ? IsWord, "key"> extends true + ? ParenOrderList>> extends Ok< infer Cols extends readonly Expr[], infer J extends number > ? NextConstraint, [...Pk, ...PkNames]> - : ParenOrderList>> - : SyntaxErr> + : ParenOrderList>> + : SyntaxErr> : IsWord extends true - ? ParenOrderList> extends Ok + ? ParenOrderList> extends Ok ? NextConstraint, Pk> - : ParenOrderList> + : ParenOrderList> : IsWord extends true - ? NextConstraint>, Pk> + ? NextConstraint>, Pk> : IsWord extends true - ? IsWord, "key"> extends true - ? IdentList>> extends Ok< + ? IsWord, "key"> extends true + ? IdentList>> extends Ok< unknown, infer J extends number > ? IsWord extends true - ? NextConstraint>, Pk> + ? NextConstraint>, Pk> : SyntaxErr - : IdentList>> - : SyntaxErr> + : IdentList>> + : SyntaxErr> : Ok; type NextConstraint< T extends Toks, @@ -391,7 +375,7 @@ type NextConstraint< Pk extends readonly string[], > = J extends number ? IsOp extends true - ? TableConstraints, Pk> + ? TableConstraints, Pk> : Ok : J; type ParenOrderList = @@ -425,7 +409,7 @@ type ParseCreateView = ? TableRefAt extends Ok ? OptCols extends Ok ? IsWord extends true - ? ParseSubquery> extends Ok< + ? ParseSubquery> extends Ok< infer S extends SelectStmt, infer J4 extends number > @@ -439,7 +423,7 @@ type ParseCreateView = }, J4 > - : ParseSubquery> + : ParseSubquery> : SyntaxErr : OptCols : TableRefAt @@ -449,8 +433,8 @@ type ParseCreateIndex = IfNotExists extends [boolean, infer J extends number] ? TableRefAt extends Ok ? IsWord extends true - ? Ident> extends infer N extends Name - ? ParenOrderList>> extends Ok< + ? Ident> extends infer N extends Name + ? ParenOrderList>> extends Ok< infer Cols extends readonly Expr[], infer J3 extends number > @@ -465,8 +449,8 @@ type ParseCreateIndex = J4 > : OptWhere - : ParenOrderList>> - : SyntaxErr> + : ParenOrderList>> + : SyntaxErr> : SyntaxErr : TableRefAt : IfNotExists; @@ -479,9 +463,9 @@ type SkipTrigger = : IfNotExists; type SkipToBegin = IsWord extends true - ? SkipBody, []> + ? SkipBody, []> : T[I] extends { readonly k: string } - ? SkipToBegin> + ? SkipToBegin> : SyntaxErr; type SkipBody< T extends Toks, @@ -489,23 +473,23 @@ type SkipBody< Depth extends readonly unknown[], > = IsWord extends true - ? SkipBody, [...Depth, 0]> + ? SkipBody, [...Depth, 0]> : IsWord extends true ? Depth extends readonly [unknown, ...infer D] - ? SkipBody, D> - : Ok<{ t: "noop"; what: "trigger" }, Nx> + ? SkipBody, D> + : Ok<{ t: "noop"; what: "trigger" }, Next> : T[I] extends { readonly k: string } - ? SkipBody, Depth> + ? SkipBody, Depth> : SyntaxErr; type ParseVirtual = IsWord extends true - ? IfNotExists> extends [boolean, infer J extends number] + ? IfNotExists> extends [boolean, infer J extends number] ? TableRefAt extends Ok ? IsWord extends true - ? Ident> extends infer M extends Name - ? IsOp>, "("> extends true - ? VirtualArgs>>, [], []> extends Ok< + ? Ident> extends infer M extends Name + ? IsOp>, "("> extends true + ? VirtualArgs>>, [], []> extends Ok< infer Cols extends readonly string[], infer J3 extends number > @@ -518,15 +502,15 @@ type ParseVirtual = }, J3 > - : VirtualArgs>>, [], []> + : VirtualArgs>>, [], []> : Ok< { t: "virtual"; module: M["v"]; table: Table; cols: [] }, - Nx> + Next> > - : SyntaxErr> + : SyntaxErr> : SyntaxErr : TableRefAt - : IfNotExists> + : IfNotExists> : SyntaxErr; type VirtualArgs< T extends Toks, @@ -535,10 +519,10 @@ type VirtualArgs< Depth extends readonly unknown[], > = Depth extends readonly [] ? IsOp extends true - ? Ok> + ? Ok> : ( Ident extends infer N extends Name - ? IsOp, "," | ")"> extends true + ? IsOp, "," | ")"> extends true ? N["n"] extends `${string}=${string}` ? Acc : [...Acc, N["n"]] @@ -547,7 +531,7 @@ type VirtualArgs< ) extends infer Acc2 extends readonly string[] ? SkipArg extends infer J extends number ? IsOp extends true - ? VirtualArgs, Acc2, []> + ? VirtualArgs, Acc2, []> : VirtualArgs : SkipArg : never @@ -560,37 +544,41 @@ type SkipArg< ? IsOp extends true ? I : IsOp extends true - ? SkipArg, [0]> + ? SkipArg, [0]> : T[I] extends { readonly k: string } - ? SkipArg, Depth> + ? SkipArg, Depth> : SyntaxErr : IsOp extends true - ? SkipArg, [...Depth, 0]> + ? SkipArg, [...Depth, 0]> : IsOp extends true - ? SkipArg, Depth extends readonly [unknown, ...infer D] ? D : []> + ? SkipArg< + T, + Next, + Depth extends readonly [unknown, ...infer D] ? D : [] + > : T[I] extends { readonly k: string } - ? SkipArg, Depth> + ? SkipArg, Depth> : SyntaxErr; export type ParseAlter = IsWord extends true - ? TableRefAt> extends Ok + ? TableRefAt> extends Ok ? IsWord extends true - ? IsWord, "to"> extends true - ? Ident>> extends infer N extends Name + ? IsWord, "to"> extends true + ? Ident>> extends infer N extends Name ? Ok< { t: "alter_table"; table: Table; action: { t: "rename_table"; to: N["n"] }; }, - Nx>> + Next>> > - : SyntaxErr>> - : SkipColumn> extends infer J2 extends number + : SyntaxErr>> + : SkipColumn> extends infer J2 extends number ? Ident extends infer A extends Name - ? IsWord, "to"> extends true - ? Ident>> extends infer B extends Name + ? IsWord, "to"> extends true + ? Ident>> extends infer B extends Name ? Ok< { t: "alter_table"; @@ -601,14 +589,14 @@ export type ParseAlter = to: B["n"]; }; }, - Nx>> + Next>> > - : SyntaxErr>> - : SyntaxErr> + : SyntaxErr>> + : SyntaxErr> : SyntaxErr : never : IsWord extends true - ? ParseColumnDef>> extends Ok< + ? ParseColumnDef>> extends Ok< infer Col, infer J2 extends number > @@ -620,9 +608,9 @@ export type ParseAlter = }, J2 > - : ParseColumnDef>> + : ParseColumnDef>> : IsWord extends true - ? SkipColumn> extends infer J2 extends number + ? SkipColumn> extends infer J2 extends number ? Ident extends infer N extends Name ? Ok< { @@ -630,37 +618,24 @@ export type ParseAlter = table: Table; action: { t: "drop_column"; name: N["v"] }; }, - Nx + Next > : SyntaxErr : never : SyntaxErr - : TableRefAt> + : TableRefAt> : SyntaxErr; -type SkipColumn = - IsWord extends true ? Nx : I; - export type ParseDrop = T[I] extends { readonly k: "w"; readonly v: infer K extends "table" | "view" | "index" | "trigger"; } - ? IfExists> extends [ + ? IfExists> extends [ infer Ie extends boolean, infer J extends number, ] ? TableRefAt extends Ok ? Ok<{ t: "drop"; kind: K; ifExists: Ie; name: Name_ }, J2> : TableRefAt - : IfExists> + : IfExists> : SyntaxErr; - -export type SkipStatement< - T extends Toks, - I extends number, - What extends string, -> = T[I] extends { readonly k: "o"; readonly v: ";" } - ? Ok<{ t: "noop"; what: What }, I> - : T[I] extends { readonly k: string } - ? SkipStatement, What> - : Ok<{ t: "noop"; what: What }, I>; diff --git a/packages/core/src/sqlite/engine/parser/dml.ts b/packages/core/src/sqlite/engine/parser/dml.ts index d616c659..2220d1f6 100644 --- a/packages/core/src/sqlite/engine/parser/dml.ts +++ b/packages/core/src/sqlite/engine/parser/dml.ts @@ -1,3 +1,14 @@ +import type { + AliasAt, + Expect, + IsOp, + IsWord, + Name, + NamesV, + SyntaxErr, +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; import type { Expr, SelectStmt, SetItem, Upsert } from "../ast.js"; import type { OptWhere, ParseExpr, ParseExprList } from "./expr.js"; import type { Reserved } from "./keywords.js"; @@ -8,37 +19,27 @@ import type { ParseValues, } from "./select.js"; import type { - AliasAt, - Expect, Ident, IdentList, - IsOp, - IsWord, - Name, - NamesV, - Nx, - Ok, OptCols, SkipIndexed, - SyntaxErr, TableRefAt, - Toks, } from "./util.js"; type Conflict = "abort" | "fail" | "ignore" | "replace" | "rollback"; type SkipOr = IsWord extends true - ? IsWord, Conflict> extends true - ? Nx> - : SyntaxErr> + ? IsWord, Conflict> extends true + ? Next> + : SyntaxErr> : I; type AsAlias = IsWord extends true - ? AliasAt, Reserved> extends infer A extends Name - ? Ok>> - : SyntaxErr> + ? AliasAt, Reserved> extends infer A extends Name + ? Ok>> + : SyntaxErr> : Ok; type TargetTable = @@ -57,11 +58,11 @@ type IndexedTarget = export type ParseInsert = ( IsWord extends true - ? Nx - : SkipOr> + ? Next + : SkipOr> ) extends infer J extends number ? IsWord extends true - ? TargetTable> extends Ok< + ? TargetTable> extends Ok< [infer Table, infer Alias], infer J2 extends number > @@ -89,17 +90,17 @@ export type ParseInsert = ( : UpsertLoop : InsertSource : OptCols - : TargetTable> + : TargetTable> : SyntaxErr - : SkipOr>; + : SkipOr>; type InsertSource = IsWord extends true - ? IsWord, "values"> extends true - ? Ok<{ t: "default" }, Nx>> - : SyntaxErr> + ? IsWord, "values"> extends true + ? Ok<{ t: "default" }, Next>> + : SyntaxErr> : IsWord extends true - ? ParseValues, []> extends Ok< + ? ParseValues, []> extends Ok< { readonly rows: infer Rows extends readonly (readonly Expr[])[] }, infer J extends number > @@ -110,7 +111,7 @@ type InsertSource = > extends true ? SelectSource : Ok<{ t: "values"; rows: Rows }, J> - : ParseValues, []> + : ParseValues, []> : IsWord extends true ? SelectSource : SyntaxErr; @@ -128,8 +129,8 @@ type UpsertLoop< Acc extends readonly unknown[], > = IsWord extends true - ? IsWord, "conflict"> extends true - ? ConflictTarget>> extends Ok< + ? IsWord, "conflict"> extends true + ? ConflictTarget>> extends Ok< [infer Target, infer TWhere], infer J extends number > @@ -146,38 +147,38 @@ type UpsertLoop< ] > : UpsertAction - : ConflictTarget>> - : SyntaxErr> + : ConflictTarget>> + : SyntaxErr> : Ok; type ConflictTarget = IsOp extends true - ? ParseExprList, []> extends Ok< + ? ParseExprList, []> extends Ok< infer Target extends readonly Expr[], infer J extends number > ? IsOp extends true - ? OptWhere> extends Ok + ? OptWhere> extends Ok ? Ok<[Target, W], J2> - : OptWhere> + : OptWhere> : SyntaxErr - : ParseExprList, []> + : ParseExprList, []> : Ok<[null, null], I>; type UpsertAction = IsWord extends true - ? IsWord, "nothing"> extends true - ? Ok<[null, null], Nx>> - : IsWord, "update"> extends true - ? IsWord>, "set"> extends true - ? ParseSet>>, []> extends Ok< + ? IsWord, "nothing"> extends true + ? Ok<[null, null], Next>> + : IsWord, "update"> extends true + ? IsWord>, "set"> extends true + ? ParseSet>>, []> extends Ok< infer Set extends readonly SetItem[], infer J extends number > ? OptWhere extends Ok ? Ok<[Set, SW], J2> : OptWhere - : ParseSet>>, []> - : SyntaxErr>> - : SyntaxErr> + : ParseSet>>, []> + : SyntaxErr>> + : SyntaxErr> : SyntaxErr; type ParseSet< @@ -197,7 +198,7 @@ type ParseSet< ? [...Acc, { cols: NamesV; e: E }] extends infer Acc2 extends readonly unknown[] ? IsOp extends true - ? ParseSet, Acc2> + ? ParseSet, Acc2> : Ok : never : ParseExpr @@ -207,12 +208,12 @@ type SetCols = IsOp extends true ? IdentList : Ident extends infer N extends Name - ? Ok<[N], Nx> + ? Ok<[N], Next> : SyntaxErr; type Returning = IsWord extends true - ? ParseTargets, []> + ? ParseTargets, []> : Ok; export type ParseUpdate = @@ -222,7 +223,7 @@ export type ParseUpdate = infer J2 extends number > ? IsWord extends true - ? ParseSet, []> extends Ok< + ? ParseSet, []> extends Ok< infer Set extends readonly SetItem[], infer J3 extends number > @@ -245,14 +246,14 @@ export type ParseUpdate = : Returning : OptWhere : OptFrom - : ParseSet, []> + : ParseSet, []> : SyntaxErr : IndexedTarget : SkipOr; export type ParseDelete = IsWord extends true - ? IndexedTarget> extends Ok< + ? IndexedTarget> extends Ok< [infer Table, infer Alias], infer J extends number > @@ -271,5 +272,5 @@ export type ParseDelete = > : Returning : OptWhere - : IndexedTarget> + : IndexedTarget> : SyntaxErr; diff --git a/packages/core/src/sqlite/engine/parser/expr.ts b/packages/core/src/sqlite/engine/parser/expr.ts index c3d90df2..d71fcc65 100644 --- a/packages/core/src/sqlite/engine/parser/expr.ts +++ b/packages/core/src/sqlite/engine/parser/expr.ts @@ -1,22 +1,21 @@ -import type { SqlError } from "../../../errors.js"; -import type { Expr, SelectStmt, TableRef, WindowSpec } from "../ast.js"; -import type { NameOf } from "../tokens.js"; -import type { GE, Tup } from "../types.js"; -import type { Reserved } from "./keywords.js"; -import type { ParenSubquery } from "./select.js"; +import type { NameOf } from "../../../engine/family/lex.js"; import type { CloseOk, Expect, - Ident, IsOp, IsWord, Name, - Nx, - Ok, + Negate, SyntaxErr, - TableRefAt, - Toks, -} from "./util.js"; +} from "../../../engine/family/parse.js"; +import type { Next } from "../../../engine/next.js"; +import type { GE } from "../../../engine/num.js"; +import type { Ok, Toks } from "../../../engine/tokens.js"; +import type { SqlError } from "../../../errors.js"; +import type { Expr, SelectStmt, TableRef, WindowSpec } from "../ast.js"; +import type { Reserved } from "./keywords.js"; +import type { ParenSubquery } from "./select.js"; +import type { Ident, TableRefAt } from "./util.js"; type Prec = { readonly or: 1; @@ -48,29 +47,33 @@ type ParsePrefix = T[I] extends { readonly v: infer O; } ? O extends "-" | "+" | "~" - ? ParseExprPrec, Prec["unary"]> extends Ok< + ? ParseExprPrec, Prec["unary"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok, J> - : ParseExprPrec, Prec["unary"]> + : ParseExprPrec, Prec["unary"]> : O extends "(" - ? ParseParen> + ? ParseParen> : SyntaxErr : T[I] extends { readonly k: "w"; readonly v: "not" } - ? ParseExprPrec, Prec["not"]> extends Ok< + ? ParseExprPrec, Prec["not"]> extends Ok< infer E extends Expr, infer J extends number > ? Ok<{ t: "un"; op: "not"; e: E }, J> - : ParseExprPrec, Prec["not"]> + : ParseExprPrec, Prec["not"]> : ParsePrimary; +// Unary plus is no-op the typer would see through everywhere; it stays only on a +// column, which SQLite then names by the expression's text instead of the column's. type Unary = O extends "-" - ? E extends { readonly t: "num"; readonly v: infer V extends string } - ? { t: "num"; v: `-${V}` } - : { t: "un"; op: "-"; e: E } - : { t: "un"; op: O; e: E }; + ? Negate + : O extends "+" + ? E extends { readonly t: "col" } + ? { t: "un"; op: O; e: E } + : E + : { t: "un"; op: O; e: E }; type ParseParen = T[I] extends { readonly k: "w"; @@ -102,7 +105,7 @@ export type ParseExprList< > = ParseExpr extends Ok ? IsOp extends true - ? ParseExprList, [...Acc, E]> + ? ParseExprList, [...Acc, E]> : Ok<[...Acc, E], J> : ParseExpr; @@ -110,13 +113,13 @@ type ParsePrimary = T[I] extends { readonly k: "n"; readonly v: infer V extends string; } - ? Ok<{ t: "num"; v: V }, Nx> + ? Ok<{ t: "num"; v: V }, Next> : T[I] extends { readonly k: "s"; readonly v: infer V extends string } ? T[I] extends { readonly blob: true } - ? Ok<{ t: "blob"; v: V }, Nx> - : Ok<{ t: "str"; v: V }, Nx> + ? Ok<{ t: "blob"; v: V }, Next> + : Ok<{ t: "str"; v: V }, Next> : T[I] extends { readonly k: "$"; readonly v: infer N extends number } - ? Ok<{ t: "param"; n: N }, Nx> + ? Ok<{ t: "param"; n: N }, Next> : T[I] extends { readonly k: "w"; readonly v: infer W extends string } ? ParseWordPrimary : T[I] extends { readonly k: "id" } @@ -128,40 +131,43 @@ type ParseWordPrimary< I extends number, W extends string, > = W extends "null" - ? Ok<{ t: "null" }, Nx> + ? Ok<{ t: "null" }, Next> : W extends "current_time" | "current_date" | "current_timestamp" - ? Ok<{ t: "ctime"; fn: W }, Nx> + ? Ok<{ t: "ctime"; fn: W }, Next> : W extends "cast" - ? ParseCast> + ? ParseCast> : W extends "case" - ? ParseCase> + ? ParseCase> : W extends "exists" - ? Expect, "("> extends infer J extends number + ? Expect, "("> extends infer J extends number ? ParenSubquery extends Ok< infer Q extends SelectStmt, infer J2 extends number > ? Ok<{ t: "exists"; q: Q }, J2> : ParenSubquery - : Expect, "("> + : Expect, "("> : W extends "raise" ? SqlError<"RAISE() may only be used within a trigger-program", I> - : IsOp, "("> extends true + : IsOp, "("> extends true ? W extends Reserved ? SyntaxErr : ParseCall & string]> : W extends "true" | "false" - ? IsOp, "."> extends true + ? IsOp, "."> extends true ? ParseColumnRef - : Ok<{ t: "bool"; v: W extends "true" ? true : false }, Nx> + : Ok< + { t: "bool"; v: W extends "true" ? true : false }, + Next + > : ParseColumnRef; type ParseColumnRef = Ident extends infer A extends Name - ? IsOp, "."> extends true - ? Ident>> extends infer B extends Name - ? IsOp>>, "."> extends true - ? Ident>>>> extends infer C extends Name + ? IsOp, "."> extends true + ? Ident>> extends infer B extends Name + ? IsOp>>, "."> extends true + ? Ident>>>> extends infer C extends Name ? Ok< { t: "col"; @@ -170,9 +176,9 @@ type ParseColumnRef = name: C["v"]; disp: `${A["n"]}.${B["n"]}.${C["n"]}`; }, - Nx>>>> + Next>>>> > - : SyntaxErr>>>> + : SyntaxErr>>>> : Ok< { t: "col"; @@ -181,20 +187,21 @@ type ParseColumnRef = name: B["v"]; disp: `${A["n"]}.${B["n"]}`; }, - Nx>> + Next>> > - : SyntaxErr>> + : SyntaxErr>> : Ok< - { - t: "col"; - schema: null; - tbl: null; - name: A["v"]; - disp: T[I] extends { readonly r: `"${string}` } - ? `"${A["n"]}" - should this be a string literal in single-quotes?` - : A["n"]; - }, - Nx + T[I] extends { readonly r: `"${string}` } + ? { + t: "col"; + schema: null; + tbl: null; + name: A["v"]; + disp: A["n"]; + quoted: true; + } + : { t: "col"; schema: null; tbl: null; name: A["v"]; disp: A["n"] }, + Next > : SyntaxErr; @@ -211,10 +218,10 @@ type TypeWords< ? T[I] extends { readonly k: "w"; readonly v: TypeStop } ? TypeArgs : NameOf extends infer V extends string - ? TypeWords, Acc extends "" ? V : `${Acc} ${V}`> + ? TypeWords, Acc extends "" ? V : `${Acc} ${V}`> : never : T[I] extends { readonly k: "s"; readonly v: infer V extends string } - ? TypeWords, Acc extends "" ? V : `${Acc} ${V}`> + ? TypeWords, Acc extends "" ? V : `${Acc} ${V}`> : TypeArgs; type TypeStop = | "constraint" @@ -232,37 +239,37 @@ type TypeArgs = IsOp extends true ? Acc extends "" ? SyntaxErr - : TypeArg> extends infer J extends number + : TypeArg> extends infer J extends number ? IsOp extends true - ? TypeArg> extends infer J2 extends number + ? TypeArg> extends infer J2 extends number ? CloseOk - : TypeArg> + : TypeArg> : CloseOk - : TypeArg> + : TypeArg> : Ok; type TypeArg = T[I] extends { readonly k: "o"; readonly v: "+" | "-"; } - ? T[Nx] extends { readonly k: "n" } - ? Nx> - : SyntaxErr> + ? T[Next] extends { readonly k: "n" } + ? Next> + : SyntaxErr> : T[I] extends { readonly k: "n" } - ? Nx + ? Next : T[I] extends { readonly k: "w" | "id" } - ? Nx + ? Next : SyntaxErr; type ParseCast = Expect extends infer J extends number ? ParseExpr extends Ok ? IsWord extends true - ? ParseTypeName> extends Ok< + ? ParseTypeName> extends Ok< infer Ty extends string, infer J3 extends number > ? CloseOk - : ParseTypeName> + : ParseTypeName> : SyntaxErr : ParseExpr : Expect; @@ -282,48 +289,48 @@ type ParseWhens< Acc extends readonly unknown[], > = IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer C extends Expr, infer J extends number > ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer V extends Expr, infer J2 extends number > ? ParseWhens - : ParseExpr> + : ParseExpr> : SyntaxErr - : ParseExpr> + : ParseExpr> : Acc extends readonly [] ? SyntaxErr : IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer E extends Expr, infer J extends number > ? IsWord extends true - ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: E }, Nx> + ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: E }, Next> : SyntaxErr - : ParseExpr> + : ParseExpr> : IsWord extends true - ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: null }, Nx> + ? Ok<{ t: "case"; operand: Operand; whens: Acc; else: null }, Next> : SyntaxErr; type ParseCall = - IsOp>, ")"> extends true - ? CallTail>>, Name, [], false, false, []> - : IsOp>, "*"> extends true - ? Expect>>, ")"> extends infer J extends number + IsOp>, ")"> extends true + ? CallTail>>, Name, [], false, false, []> + : IsOp>, "*"> extends true + ? Expect>>, ")"> extends infer J extends number ? CallTail - : Expect>>, ")"> + : Expect>>, ")"> : CallArgs< T, - IsWord>, "distinct" | "all"> extends true - ? Nx>> - : Nx>, + IsWord>, "distinct" | "all"> extends true + ? Next>> + : Next>, Name, - IsWord>, "distinct"> + IsWord>, "distinct"> >; type CallArgs< T extends Toks, @@ -336,16 +343,16 @@ type CallArgs< infer J extends number > ? IsWord extends true - ? IsWord, "by"> extends true - ? ParseOrderList>, []> extends Ok< + ? IsWord, "by"> extends true + ? ParseOrderList>, []> extends Ok< infer Ord extends readonly Expr[], infer J2 extends number > ? Expect extends infer J3 extends number ? CallTail : Expect - : ParseOrderList>, []> - : SyntaxErr> + : ParseOrderList>, []> + : SyntaxErr> : Expect extends infer J3 extends number ? CallTail : Expect @@ -361,18 +368,18 @@ type CallTail< Ord extends readonly Expr[], > = IsWord extends true - ? Expect, "("> extends infer J extends number + ? Expect, "("> extends infer J extends number ? IsWord extends true - ? ParseExpr> extends Ok< + ? ParseExpr> extends Ok< infer F extends Expr, infer J2 extends number > ? Expect extends infer J3 extends number ? CallOver : Expect - : ParseExpr> + : ParseExpr> : SyntaxErr - : Expect, "("> + : Expect, "("> : CallOver; type CallOver< @@ -386,12 +393,12 @@ type CallOver< Filter, > = IsWord extends true - ? ParseOver> extends Ok< + ? ParseOver> extends Ok< infer Spec extends WindowSpec, infer J extends number > ? Ok, J> - : ParseOver> + : ParseOver> : Ok, I>; type MkCall = { t: "call"; @@ -407,9 +414,9 @@ type MkCall = { type ParseOver = Ident extends infer N extends Name - ? Ok<{ name: N["v"]; partition: []; order: [] }, Nx> + ? Ok<{ name: N["v"]; partition: []; order: [] }, Next> : IsOp extends true - ? ParseWindowSpec> + ? ParseWindowSpec> : SyntaxErr; export type ParseWindowSpec = ( @@ -420,7 +427,7 @@ export type ParseWindowSpec = ( "partition" | "order" | "rows" | "range" | "groups" > extends true ? [null, I] - : [N["v"], Nx] + : [N["v"], Next] : [null, I] ) extends [infer Base, infer I1 extends number] ? OptBy extends Ok< @@ -445,19 +452,19 @@ export type OptBy< Absent = [], > = IsWord extends true - ? IsWord, "by"> extends true + ? IsWord, "by"> extends true ? W extends "order" - ? ParseOrderList>, []> - : ParseExprList>, []> - : SyntaxErr> + ? ParseOrderList>, []> + : ParseExprList>, []> + : SyntaxErr> : Ok; export type OptWhere = - IsWord extends true ? ParseExpr> : Ok; + IsWord extends true ? ParseExpr> : Ok; export type ParenArgs = IsOp extends true - ? Ok<[], Nx> + ? Ok<[], Next> : ParseExprList extends Ok< infer A extends readonly Expr[], infer J extends number @@ -470,13 +477,13 @@ type SkipFrame< I extends number, Depth extends readonly unknown[], > = T[I] extends { readonly k: "o"; readonly v: "(" } - ? SkipFrame, [...Depth, 0]> + ? SkipFrame, [...Depth, 0]> : T[I] extends { readonly k: "o"; readonly v: ")" } ? Depth extends readonly [unknown, ...infer D] - ? SkipFrame, D> - : Nx + ? SkipFrame, D> + : Next : T[I] extends { readonly k: string } - ? SkipFrame, Depth> + ? SkipFrame, Depth> : SyntaxErr; export type ParseOrderList< @@ -487,19 +494,19 @@ export type ParseOrderList< ParseExpr extends Ok ? OrderSuffix extends infer J2 extends number ? IsOp extends true - ? ParseOrderList, [...Acc, E]> + ? ParseOrderList, [...Acc, E]> : Ok<[...Acc, E], J2> : OrderSuffix : ParseExpr; type OrderSuffix = ( IsWord extends true - ? Nx + ? Next : I ) extends infer J extends number ? IsWord extends true - ? IsWord, "first" | "last"> extends true - ? Nx> - : SyntaxErr> + ? IsWord, "first" | "last"> extends true + ? Next> + : SyntaxErr> : J : never; @@ -537,7 +544,7 @@ type InfixOp< > = BinPrec extends infer P extends number ? GE extends true - ? ParseExprPrec, Next1

- ? SqlError<`cannot modify ${K} because it is a view`, null> + ? SqlError<`cannot modify ${K} because it is a view`, NamedAt> : Rel extends Table ? [Rel, K] - : SqlError<`no such table: ${RefDisplay}`, null> - : SqlError<`no such table: ${RefDisplay}`, null>; + : SqlError< + `no such table: ${RefDisplay}`, + NamedAt, "table"> + > + : SqlError< + `no such table: ${RefDisplay}`, + NamedAt, "table"> + >; type Item = RelationItem extends infer It extends SqItem ? It : never; @@ -115,17 +129,20 @@ type NameOf = { type CheckCols< Cols extends readonly string[], Rel extends Table, - Msg extends string, + K extends string, I extends number = 0, > = Cols[I] extends infer C extends string ? [ColOf] extends [never] - ? SqlError<`${Msg}${C}`, null> + ? SqlError< + `table ${K} has no column named ${C}`, + NameAt>, "column", LastPart>> + > : ColOf extends { readonly generated: true } ? SqlError< `cannot INSERT into generated column "${NameOf}"`, null > - : CheckCols> + : CheckCols> : true; type Width< @@ -148,7 +165,11 @@ type Width< : null; type TableWidth = - InsertOrder extends infer O extends readonly unknown[] ? O["length"] : 0; + InsertOrder extends infer O extends readonly unknown[] + ? O["length"] + : Members> extends infer L extends readonly unknown[] + ? L["length"] + : 0; type CheckInsert< S extends InsertStmt, @@ -158,7 +179,7 @@ type CheckInsert< Ctes extends SqCteMap, > = ( S["columns"] extends readonly string[] - ? CheckCols + ? CheckCols : true ) extends infer E1 extends AnyErr ? E1 @@ -171,13 +192,13 @@ type CheckInsert< ? InsertRest : SqlError< `${W} values for ${S["columns"]["length"]} columns`, - null + NameAt<"values"> > : W extends TableWidth ? InsertRest : SqlError< `table ${K} has ${TableWidth} columns but ${W} values were supplied`, - null + NameAt<"values"> > : InsertRest : never; @@ -215,7 +236,7 @@ type ValuesTyped< > = Rows[I] extends infer R extends readonly Expr[] ? TypeList extends AnyErr ? TypeList - : ValuesTyped> + : ValuesTyped> : true; type MissingRequired< @@ -294,7 +315,7 @@ type UpsertLoop< ) extends infer Y ? Y extends AnyErr ? Y - : UpsertLoop> + : UpsertLoop> : never : never : never @@ -312,7 +333,7 @@ type CheckSet< : S["cols"]["length"] extends 1 ? TypeOf extends AnyErr ? TypeOf - : CheckSet> + : CheckSet> : S["e"] extends { readonly t: "row"; readonly items: infer Items extends readonly Expr[]; @@ -320,7 +341,7 @@ type CheckSet< ? Items["length"] extends S["cols"]["length"] ? TypeList extends AnyErr ? TypeList - : CheckSet> + : CheckSet> : SqlError< `${S["cols"]["length"]} columns assigned ${Items["length"]} values`, null @@ -331,7 +352,7 @@ type CheckSet< } ? ProjectSelect extends infer X extends readonly Cell[] ? X["length"] extends S["cols"]["length"] - ? CheckSet> + ? CheckSet> : SqlError< `${S["cols"]["length"]} columns assigned ${X["length"]} values`, null @@ -358,8 +379,8 @@ type MissingCol< ? C extends keyof It["cols"] ? GenCol extends true ? SqlError<`cannot UPDATE generated column "${NameOf}"`, null> - : MissingCol> - : SqlError<`no such column: ${C}`, null> + : MissingCol> + : SqlError<`no such column: ${C}`, NamedAt> : true; type CheckUpdate< @@ -457,7 +478,10 @@ type InsertParams< > = ( S["columns"] extends readonly string[] ? S["columns"] - : InsertOrder + : // Without the table's column order, the values are typed `unknown`. + InsertOrder extends infer O extends readonly string[] + ? O + : [] ) extends infer Cols extends readonly string[] ? | (S["source"] extends { diff --git a/packages/core/src/sqlite/engine/project/select.ts b/packages/core/src/sqlite/engine/project/select.ts index edcb1556..50b2f222 100644 --- a/packages/core/src/sqlite/engine/project/select.ts +++ b/packages/core/src/sqlite/engine/project/select.ts @@ -1,6 +1,15 @@ +import type { Digit } from "../../../engine/chars.js"; +import type { NameAt } from "../../../engine/family/diagnostics.js"; +import type { + CellsMap, + NullLevel, + RenameCells, +} from "../../../engine/family/scope.js"; +import type { QueryContext } from "../../../engine/family/types.js"; import type { NarrowKeys, NarrowLevel } from "../../../engine/narrow.js"; -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, QueryContext, Scope } from "../../../engine/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { CappedLen, DigitsLE, GE, ToNum } from "../../../engine/num.js"; +import type { ColInfo, Info, Scope } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Cte, @@ -12,19 +21,12 @@ import type { Target, WithClause, } from "../ast.js"; -import type { - BindFrom, - CellsMap, - CteEntry, - NullLevel, - StarCells, -} from "../binder/scope.js"; +import type { BindFrom, CteEntry, StarCells } from "../binder/scope.js"; import type { IsAggCall } from "../typer/functions.js"; -import type { Common, Info, TypeList, TypeOf } from "../typer/typeof.js"; +import type { Common, TypeList, TypeOf } from "../typer/typeof.js"; import type { AggMode, Cell, - GE, MkCtx, SqColMap, SqColOf, @@ -82,7 +84,7 @@ type CteLoop< Env["scope"], Env["ctes"] & { readonly [K in C["name"]]: CteEntry } >, - Nx + Next > : CteCells : Env["ctes"]; @@ -132,22 +134,10 @@ type Renamed< ? RenameCells : SqlError< `table ${C["display"]} has ${R["length"]} values for ${C["cols"]["length"]} columns`, - null + NameAt<"values"> > : R; -export type RenameCells< - C extends readonly Cell[], - N extends readonly { readonly n: string }[], -> = { - [I in keyof C]: { - readonly n: N[I & keyof N] extends { readonly n: infer X extends string } - ? X - : C[I]["n"]; - readonly t: C[I]["t"]; - }; -}; - type ProjectBody< B extends SelectBody, Env extends QueryContext, @@ -230,15 +220,15 @@ type CompoundOrder< } ? Lowercase extends infer Names ? N extends Names - ? CompoundOrder> + ? CompoundOrder> : SqlError< - `${Ordinal>} ORDER BY term does not match any column in the result set`, - null + `${Ordinal>} ORDER BY term does not match any column in the result set`, + NameAt<"order"> > : never : SqlError< - `${Ordinal>} ORDER BY term does not match any column in the result set`, - null + `${Ordinal>} ORDER BY term does not match any column in the result set`, + NameAt<"order"> > : true; @@ -258,18 +248,16 @@ type IntTerm = E extends { readonly not: infer Not; } ? { readonly neg: false; readonly v: Not extends true ? 1 : 0 } - : E extends { readonly t: "un"; readonly op: "+"; readonly e: infer X } - ? IntTerm - : E extends { readonly t: "un"; readonly op: "-"; readonly e: infer X } - ? IntTerm extends infer R - ? R extends IntVal - ? { - readonly neg: R["neg"] extends true ? false : true; - readonly v: R["v"]; - } - : null + : E extends { readonly t: "un"; readonly op: "-"; readonly e: infer X } + ? IntTerm extends infer R + ? R extends IntVal + ? { + readonly neg: R["neg"] extends true ? false : true; + readonly v: R["v"]; + } : null - : null; + : null + : null; type IntLiteral = V extends `-${infer R}` ? IntLiteral @@ -293,62 +281,27 @@ type Digits = S extends "" ? Digits : false : false; -type Dec = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; -type Hex = Dec | "a" | "b" | "c" | "d" | "e" | "f"; -type Len< - S extends string, - A extends unknown[] = [], -> = S extends `${string}${infer R}` - ? A["length"] extends 20 - ? 20 - : Len - : A["length"]; +type Hex = Digit | "a" | "b" | "c" | "d" | "e" | "f"; type DecimalValue = - Digits extends true + Digits extends true ? D extends "" ? 0 - : Len extends 1 | 2 | 3 + : CappedLen extends 1 | 2 | 3 ? D extends `${infer K extends number}` ? K : null - : Len extends 4 | 5 | 6 | 7 | 8 | 9 + : CappedLen extends 4 | 5 | 6 | 7 | 8 | 9 ? "big" - : Len extends 10 + : CappedLen extends 10 ? DigitsLE extends true ? "big" : null : null : null; -type DigitsLE< - A extends string, - B extends string, -> = A extends `${infer X extends Dec}${infer AR}` - ? B extends `${infer Y extends Dec}${infer BR}` - ? X extends Y - ? DigitsLE - : DecIndex extends infer XI extends number - ? DecIndex extends infer YI extends number - ? GE - : false - : false - : false - : true; -type DecIndex = { - "0": 0; - "1": 1; - "2": 2; - "3": 3; - "4": 4; - "5": 5; - "6": 6; - "7": 7; - "8": 8; - "9": 9; -}[D]; type HexIndex = - Lowercase extends Dec - ? DecIndex & Dec> + Lowercase extends Digit + ? ToNum> : { a: 10; b: 11; c: 12; d: 13; e: 14; f: 15 }[Lowercase & ("a" | "b" | "c" | "d" | "e" | "f")]; type HexValue = @@ -356,11 +309,11 @@ type HexValue = ? H extends "" ? 0 : H extends `${infer C}` - ? Len extends 1 + ? CappedLen extends 1 ? HexIndex - : Len extends 2 | 3 | 4 | 5 | 6 | 7 + : CappedLen extends 2 | 3 | 4 | 5 | 6 | 7 ? "big" - : Len extends 8 + : CappedLen extends 8 ? H extends `${"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7"}${string}` ? "big" : null @@ -387,8 +340,8 @@ type OutOfRange< N extends number, W extends string, > = SqlError< - `${Ordinal>} ${W} BY term out of range - should be between 1 and ${N}`, - null + `${Ordinal>} ${W} BY term out of range - should be between 1 and ${N}`, + NameAt> >; type Ordinal = N extends 1 ? "1st" @@ -420,7 +373,7 @@ type ValuesCells< R extends readonly (readonly ColInfo[])[], > = { [I in keyof R0]: { - readonly n: `column${Nx}`; + readonly n: `column${Next}`; readonly t: Info< Common>, true extends Column[number]["nullable"] ? true : false @@ -437,7 +390,7 @@ type ValuesRows< Acc extends readonly (readonly ColInfo[])[], > = Rows[I] extends infer Row extends readonly Expr[] ? TypeList extends infer X extends readonly ColInfo[] - ? ValuesRows, [...Acc, X]> + ? ValuesRows, [...Acc, X]> : TypeList : Acc; @@ -474,8 +427,8 @@ type HasAggList< ? true : HasAgg extends true ? true - : HasAggList> - : HasAggList>; + : HasAggList> + : HasAggList>; type AggModeOf< S extends SimpleSelect, @@ -533,7 +486,7 @@ type MissingWindow = : null : null; export type FirstName = - import("../../../sql/splice.js").UnionList extends readonly [ + import("../../../engine/object.js").Members extends readonly [ infer F extends string, ...unknown[], ] @@ -687,7 +640,7 @@ export type ProjectTargets< : never ) extends infer S ? S extends readonly Cell[] - ? ProjectTargets, [...Acc, ...S], Ret> + ? ProjectTargets, [...Acc, ...S], Ret> : S : never : T extends { @@ -701,7 +654,7 @@ export type ProjectTargets< Ts, L, C, - Nx, + Next, [ ...Acc, { @@ -749,7 +702,7 @@ type DerivedName< ? { readonly d: N } : {} : Lowercase extends "true" | "false" - ? { readonly d: `column${Nx}` } + ? { readonly d: `column${Next}` } : {} : {}; @@ -796,13 +749,13 @@ type ClauseTerms< ? IntTerm extends infer V extends IntVal ? PositionOk extends AnyErr ? PositionOk - : ClauseTerms> + : ClauseTerms> : (W extends "GROUP" ? HasAgg : false) extends true ? SqlError< "aggregate functions are not allowed in the GROUP BY clause", - null + NameAt<"group"> > : TypeOf extends AnyErr ? TypeOf - : ClauseTerms> + : ClauseTerms> : true; diff --git a/packages/core/src/sqlite/engine/tokens.ts b/packages/core/src/sqlite/engine/tokens.ts deleted file mode 100644 index d3035b9c..00000000 --- a/packages/core/src/sqlite/engine/tokens.ts +++ /dev/null @@ -1,62 +0,0 @@ -export type SqWord = { - readonly k: "w"; - readonly v: V; - readonly r: R; - readonly g: G; -}; -export type SqIdent< - V extends string, - N extends string, - R extends string, - G extends string, -> = { - readonly k: "id"; - readonly v: V; - readonly n: N; - readonly r: R; - readonly g: G; -}; -export type SqNum = { - readonly k: "n"; - readonly v: V; - readonly g: G; -}; -export type SqStr = { - readonly k: "s"; - readonly v: V; - readonly r: R; - readonly g: G; -}; -export type SqBlob = { - readonly k: "s"; - readonly v: V; - readonly r: R; - readonly g: G; - readonly blob: true; -}; -export type SqParam = { - readonly k: "$"; - readonly v: N; - readonly g: G; -}; -export type SqOp = { - readonly k: "o"; - readonly v: V; - readonly g: G; -}; - -export type RawOf = T extends { readonly r: infer R extends string } - ? R - : T extends { readonly k: "$"; readonly v: infer N extends number } - ? `$${N}` - : T extends { readonly v: infer V extends string } - ? V - : ""; - -export type NameOf = T extends { readonly k: "id"; readonly n: infer N } - ? N - : T extends { readonly k: "w"; readonly r: infer R } - ? R - : T extends { readonly k: "s"; readonly v: infer V } - ? V - : never; diff --git a/packages/core/src/sqlite/engine/typer/functions.ts b/packages/core/src/sqlite/engine/typer/functions.ts index fe1702b2..9cd6cf2d 100644 --- a/packages/core/src/sqlite/engine/typer/functions.ts +++ b/packages/core/src/sqlite/engine/typer/functions.ts @@ -1,4 +1,11 @@ -import type { GE, Tup } from "../types.js"; +import type { + DeclaredFn, + Many, + MaxArgs, + MinArgs, + PickSig, +} from "../../../engine/family/functions.js"; +import type { Info } from "../../../engine/types.js"; type F< Min extends number, @@ -6,14 +13,37 @@ type F< Ret extends string, Null extends NullRule, Kind extends "scalar" | "agg" | "win" = "scalar", + Args = readonly Info<"unknown", true>[], > = { readonly min: Min; readonly max: Max; readonly ret: Ret; readonly null: Null; readonly kind: Kind; + readonly args: Args; }; +/** + * The type a parameter takes in each argument position, as in `upper($1)`; + * `unknown` without a rule. `same`: the first typed argument's type, for all; + * `if`: a condition, then the type both results share. + */ +type Text = readonly Info<"text", true>[]; +type Num = readonly Info<"numeric", true>[]; +type JsonArgs = [Info<"json", true>, ...Info<"text", true>[]]; +type Same = "same"; +type IfArgs = "if"; +type Substr = [ + Info<"text", true>, + Info<"integer", true>, + Info<"integer", true>, +]; +type Offset = [ + Info<"unknown", true>, + Info<"integer", false>, + Info<"unknown", true>, +]; + type NullRule = | "never" | "args" @@ -28,10 +58,8 @@ type NullRule = | "substr" | "agg"; -type Many = 1000; - export interface Functions { - abs: F<1, 1, "abs", "args">; + abs: F<1, 1, "abs", "args", "scalar", Num>; // FTS5 auxiliary functions; the first argument is the FTS5 table. bm25: F<1, Many, "real", "never">; highlight: F<4, 4, "text", "always">; @@ -39,49 +67,49 @@ export interface Functions { unistr: F<1, 1, "text", "args">; changes: F<0, 0, "integer", "never">; char: F<0, Many, "text", "never">; - coalesce: F<2, Many, "common", "all">; - concat: F<1, Many, "text", "never">; - concat_ws: F<2, Many, "text", "arg0">; - format: F<1, Many, "text", "format">; - printf: F<1, Many, "text", "format">; - glob: F<2, 2, "integer", "args">; + coalesce: F<2, Many, "common", "all", "scalar", Same>; + concat: F<1, Many, "text", "never", "scalar", Text>; + concat_ws: F<2, Many, "text", "arg0", "scalar", Text>; + format: F<1, Many, "text", "format", "scalar", Text>; + printf: F<1, Many, "text", "format", "scalar", Text>; + glob: F<2, 2, "integer", "args", "scalar", Text>; hex: F<1, 1, "text", "never">; - ifnull: F<2, 2, "common", "all">; - iif: F<2, Many, "iif", "iif">; - if: F<2, Many, "iif", "iif">; - instr: F<2, 2, "integer", "args">; + ifnull: F<2, 2, "common", "all", "scalar", Same>; + iif: F<2, Many, "iif", "iif", "scalar", IfArgs>; + if: F<2, Many, "iif", "iif", "scalar", IfArgs>; + instr: F<2, 2, "integer", "args", "scalar", Text>; last_insert_rowid: F<0, 0, "integer", "never">; length: F<1, 1, "integer", "args">; - like: F<2, 3, "integer", "args">; + like: F<2, 3, "integer", "args", "scalar", Text>; likelihood: F<2, 2, "arg0", "arg0">; likely: F<1, 1, "arg0", "arg0">; - lower: F<1, 1, "text", "args">; - ltrim: F<1, 2, "text", "args">; - max: F<1, Many, "common", "args">; - min: F<1, Many, "common", "args">; - nullif: F<2, 2, "arg0", "always">; + lower: F<1, 1, "text", "args", "scalar", Text>; + ltrim: F<1, 2, "text", "args", "scalar", Text>; + max: F<1, Many, "common", "args", "scalar", Same>; + min: F<1, Many, "common", "args", "scalar", Same>; + nullif: F<2, 2, "arg0", "always", "scalar", Same>; octet_length: F<1, 1, "integer", "args">; quote: F<1, 1, "text", "never">; random: F<0, 0, "integer", "never">; - randomblob: F<1, 1, "blob", "never">; - replace: F<3, 3, "text", "args">; - round: F<1, 2, "real", "args">; - rtrim: F<1, 2, "text", "args">; - sign: F<1, 1, "integer", "sign">; + randomblob: F<1, 1, "blob", "never", "scalar", Num>; + replace: F<3, 3, "text", "args", "scalar", Text>; + round: F<1, 2, "real", "args", "scalar", Num>; + rtrim: F<1, 2, "text", "args", "scalar", Text>; + sign: F<1, 1, "integer", "sign", "scalar", Num>; sqlite_compileoption_get: F<1, 1, "text", "always">; sqlite_compileoption_used: F<1, 1, "integer", "args">; sqlite_source_id: F<0, 0, "text", "never">; sqlite_version: F<0, 0, "text", "never">; - substr: F<2, 3, "substr", "substr">; - substring: F<2, 3, "substr", "substr">; + substr: F<2, 3, "substr", "substr", "scalar", Substr>; + substring: F<2, 3, "substr", "substr", "scalar", Substr>; total_changes: F<0, 0, "integer", "never">; - trim: F<1, 2, "text", "args">; + trim: F<1, 2, "text", "args", "scalar", Text>; typeof: F<1, 1, "text", "never">; - unhex: F<1, 2, "blob", "always">; - unicode: F<1, 1, "integer", "always">; + unhex: F<1, 2, "blob", "always", "scalar", Text>; + unicode: F<1, 1, "integer", "always", "scalar", Text>; unlikely: F<1, 1, "arg0", "arg0">; - upper: F<1, 1, "text", "args">; - zeroblob: F<1, 1, "blob", "never">; + upper: F<1, 1, "text", "args", "scalar", Text>; + zeroblob: F<1, 1, "blob", "never", "scalar", Num>; date: F<0, Many, "text", "noargs">; time: F<0, Many, "text", "noargs">; @@ -91,72 +119,72 @@ export interface Functions { strftime: F<1, Many, "text", "always">; timediff: F<2, 2, "text", "always">; - acos: F<1, 1, "real", "always">; - acosh: F<1, 1, "real", "always">; - asin: F<1, 1, "real", "always">; - asinh: F<1, 1, "real", "numargs">; - atan: F<1, 1, "real", "numargs">; - atan2: F<2, 2, "real", "numargs">; - atanh: F<1, 1, "real", "always">; - ceil: F<1, 1, "ceil", "sign">; - ceiling: F<1, 1, "ceil", "sign">; - cos: F<1, 1, "real", "numargs">; - cosh: F<1, 1, "real", "numargs">; - degrees: F<1, 1, "real", "numargs">; - exp: F<1, 1, "real", "numargs">; - floor: F<1, 1, "ceil", "sign">; - ln: F<1, 1, "real", "always">; - log: F<1, 2, "real", "always">; - log10: F<1, 1, "real", "always">; - log2: F<1, 1, "real", "always">; - mod: F<2, 2, "real", "always">; + acos: F<1, 1, "real", "always", "scalar", Num>; + acosh: F<1, 1, "real", "always", "scalar", Num>; + asin: F<1, 1, "real", "always", "scalar", Num>; + asinh: F<1, 1, "real", "numargs", "scalar", Num>; + atan: F<1, 1, "real", "numargs", "scalar", Num>; + atan2: F<2, 2, "real", "numargs", "scalar", Num>; + atanh: F<1, 1, "real", "always", "scalar", Num>; + ceil: F<1, 1, "ceil", "sign", "scalar", Num>; + ceiling: F<1, 1, "ceil", "sign", "scalar", Num>; + cos: F<1, 1, "real", "numargs", "scalar", Num>; + cosh: F<1, 1, "real", "numargs", "scalar", Num>; + degrees: F<1, 1, "real", "numargs", "scalar", Num>; + exp: F<1, 1, "real", "numargs", "scalar", Num>; + floor: F<1, 1, "ceil", "sign", "scalar", Num>; + ln: F<1, 1, "real", "always", "scalar", Num>; + log: F<1, 2, "real", "always", "scalar", Num>; + log10: F<1, 1, "real", "always", "scalar", Num>; + log2: F<1, 1, "real", "always", "scalar", Num>; + mod: F<2, 2, "real", "always", "scalar", Num>; pi: F<0, 0, "real", "never">; - pow: F<2, 2, "real", "always">; - power: F<2, 2, "real", "always">; - radians: F<1, 1, "real", "numargs">; - sin: F<1, 1, "real", "numargs">; - sinh: F<1, 1, "real", "numargs">; - sqrt: F<1, 1, "real", "always">; - tan: F<1, 1, "real", "numargs">; - tanh: F<1, 1, "real", "numargs">; - trunc: F<1, 1, "ceil", "sign">; + pow: F<2, 2, "real", "always", "scalar", Num>; + power: F<2, 2, "real", "always", "scalar", Num>; + radians: F<1, 1, "real", "numargs", "scalar", Num>; + sin: F<1, 1, "real", "numargs", "scalar", Num>; + sinh: F<1, 1, "real", "numargs", "scalar", Num>; + sqrt: F<1, 1, "real", "always", "scalar", Num>; + tan: F<1, 1, "real", "numargs", "scalar", Num>; + tanh: F<1, 1, "real", "numargs", "scalar", Num>; + trunc: F<1, 1, "ceil", "sign", "scalar", Num>; - json: F<1, 1, "text", "args">; - jsonb: F<1, 1, "blob", "args">; + json: F<1, 1, "text", "args", "scalar", JsonArgs>; + jsonb: F<1, 1, "blob", "args", "scalar", JsonArgs>; json_array: F<0, Many, "text", "never">; jsonb_array: F<0, Many, "blob", "never">; - json_array_length: F<1, 2, "integer", "always">; + json_array_length: F<1, 2, "integer", "always", "scalar", JsonArgs>; json_error_position: F<1, 1, "integer", "args">; - json_extract: F<2, Many, "jsonext", "always">; - jsonb_extract: F<2, Many, "any", "always">; + json_extract: F<2, Many, "text", "always", "scalar", JsonArgs>; + jsonb_extract: F<2, Many, "any", "always", "scalar", JsonArgs>; json_insert: F<1, Many, "text", "arg0">; json_array_insert: F<1, Many, "text", "arg0">; jsonb_insert: F<1, Many, "blob", "arg0">; json_object: F<0, Many, "text", "never">; jsonb_object: F<0, Many, "blob", "never">; - json_patch: F<2, 2, "text", "args">; - jsonb_patch: F<2, 2, "blob", "args">; - json_pretty: F<1, 2, "text", "arg0">; + json_patch: F<2, 2, "text", "args", "scalar", JsonArgs>; + jsonb_patch: F<2, 2, "blob", "args", "scalar", JsonArgs>; + json_pretty: F<1, 2, "text", "arg0", "scalar", JsonArgs>; json_quote: F<1, 1, "text", "never">; - json_remove: F<1, Many, "text", "arg0">; - jsonb_remove: F<1, Many, "blob", "arg0">; + json_remove: F<1, Many, "text", "arg0", "scalar", JsonArgs>; + jsonb_remove: F<1, Many, "blob", "arg0", "scalar", JsonArgs>; json_replace: F<1, Many, "text", "arg0">; jsonb_replace: F<1, Many, "blob", "arg0">; json_set: F<1, Many, "text", "arg0">; jsonb_set: F<1, Many, "blob", "arg0">; - json_type: F<1, 2, "text", "always">; - json_valid: F<1, 2, "integer", "arg0">; + json_type: F<1, 2, "text", "always", "scalar", JsonArgs>; + json_valid: F<1, 2, "integer", "arg0", "scalar", JsonArgs>; - avg: F<1, 1, "real", "agg", "agg">; + avg: F<1, 1, "real", "agg", "agg", Num>; median: F<1, 1, "real", "agg", "agg">; percentile: F<2, 2, "real", "agg", "agg">; percentile_cont: F<2, 2, "real", "agg", "agg">; percentile_disc: F<2, 2, "real", "agg", "agg">; count: F<0, 1, "integer", "never", "agg">; - group_concat: F<1, 2, "text", "agg", "agg">; - string_agg: F<2, 2, "text", "agg", "agg">; - sum: F<1, 1, "sum", "agg", "agg">; - total: F<1, 1, "real", "never", "agg">; + group_concat: F<1, 2, "text", "agg", "agg", Text>; + string_agg: F<2, 2, "text", "agg", "agg", Text>; + sum: F<1, 1, "sum", "agg", "agg", Num>; + total: F<1, 1, "real", "never", "agg", Num>; json_group_array: F<1, 1, "text", "never", "agg">; jsonb_group_array: F<1, 1, "blob", "never", "agg">; json_group_object: F<2, 2, "text", "never", "agg">; @@ -167,12 +195,12 @@ export interface Functions { dense_rank: F<0, 0, "integer", "never", "win">; percent_rank: F<0, 0, "real", "never", "win">; cume_dist: F<0, 0, "real", "never", "win">; - ntile: F<1, 1, "integer", "never", "win">; - lag: F<1, 3, "arg0", "always", "win">; - lead: F<1, 3, "arg0", "always", "win">; + ntile: F<1, 1, "integer", "never", "win", Num>; + lag: F<1, 3, "arg0", "always", "win", Offset>; + lead: F<1, 3, "arg0", "always", "win", Offset>; first_value: F<1, 1, "arg0", "arg0", "win">; last_value: F<1, 1, "arg0", "arg0", "win">; - nth_value: F<2, 2, "arg0", "always", "win">; + nth_value: F<2, 2, "arg0", "always", "win", Offset>; } export type Fn = { @@ -189,7 +217,7 @@ export type IsAggCall = E extends { readonly over: null; readonly args: readonly unknown[]; } - ? DeclaredFn extends infer D + ? DeclaredFn extends infer D ? D extends null ? N extends "min" | "max" ? E["args"]["length"] extends 1 @@ -232,58 +260,10 @@ export type FunctionRegistry = { readonly [name: string]: AnySig | readonly AnySig[]; }; -type MatchName = { - [K in keyof O & string]: Lowercase extends N ? O[K] : never; -}[keyof O & string]; - -export type DeclaredFn = ( - DB extends { readonly $functions: infer F } - ? MatchName - : never -) extends infer D - ? [D] extends [never] - ? MatchName extends infer C - ? [C] extends [never] - ? null - : C - : never - : D - : never; - -export type PickSig = D extends readonly unknown[] - ? PickLoop - : D; -type PickLoop< - D extends readonly unknown[], - Argc extends number, - I extends number, -> = D[I] extends infer S extends AnySig - ? Accepts extends true - ? S - : PickLoop, 0]["length"] & number> - : D[0]; - -type Required_ = A extends readonly [ - ...infer H extends readonly string[], - "...", -] - ? H["length"] - : A["length"]; -type Accepts< - A extends readonly string[], - N extends number, -> = A extends readonly [...unknown[], "..."] - ? GE> - : N extends A["length"] - ? true - : false; - export type SigFn = S extends AnySig ? { - readonly min: Required_; - readonly max: S["args"] extends readonly [...unknown[], "..."] - ? Many - : S["args"]["length"]; + readonly min: MinArgs; + readonly max: MaxArgs; readonly ret: S["ret"]; readonly null: S["nullable"] extends true ? "always" @@ -299,15 +279,3 @@ export type SigFn = S extends AnySig : "scalar"; } : never; - -export type SigArgType = S extends AnySig - ? S["args"][I] extends infer T extends string - ? T extends "..." - ? S["args"] extends readonly [...unknown[], infer P extends string, "..."] - ? P - : "unknown" - : T - : S["args"] extends readonly [...unknown[], infer P extends string, "..."] - ? P - : "unknown" - : "unknown"; diff --git a/packages/core/src/sqlite/engine/typer/json.ts b/packages/core/src/sqlite/engine/typer/json.ts index 42c9b5e6..9b6466ba 100644 --- a/packages/core/src/sqlite/engine/typer/json.ts +++ b/packages/core/src/sqlite/engine/typer/json.ts @@ -1,80 +1,21 @@ +import type { + JsonStep, + JsonSteps, + JsonWalk, +} from "../../../engine/family/json.js"; import type { Shaped, TsShape } from "../../../schema.js"; -export type JsonShapeOf = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? [V] - : null; - -export type JsonStep = { readonly key: string } | { readonly index: true }; -type Step = JsonStep; - export type JsonPath = E extends { readonly t: "str"; readonly v: infer P extends string; } ? P extends `$${infer R}` - ? Steps + ? JsonSteps : [{ readonly key: P }] : E extends { readonly t: "num"; readonly v: string } ? [{ readonly index: true }] : null; -type Steps = S extends "" - ? Acc - : S extends `.${infer R}` - ? R extends `"${infer K}"${infer R2}` - ? Steps - : KeyRun extends [infer K extends string, infer R2 extends string] - ? Steps - : null - : S extends `[${string}]${infer R}` - ? Steps - : null; -type KeyRun< - S extends string, - Acc extends string, -> = S extends `${infer C}${infer R}` - ? C extends "." | "[" - ? [Acc, S] - : KeyRun - : [Acc, S]; - -type Walk< - V, - P extends readonly Step[], - Missing extends boolean, -> = P extends readonly [infer H, ...infer R extends readonly Step[]] - ? [Exclude] extends [never] - ? [null, true] - : H extends { readonly key: infer K extends string } - ? Exclude extends infer O - ? O extends readonly unknown[] - ? [unknown, true] - : O extends object - ? K extends keyof O - ? Walk< - O[K], - R, - Absent ? true : false> - > - : string extends keyof O - ? Walk - : [never, true] - : [unknown, true] - : never - : Exclude extends readonly (infer El)[] - ? Walk - : [unknown, true] - : [V, Absent]; -type Absent = Missing extends true - ? true - : null extends V - ? true - : undefined extends V - ? true - : Else; - type SqlValue = V extends boolean ? 0 | 1 : V extends string | number | bigint @@ -83,8 +24,12 @@ type SqlValue = V extends boolean ? never : string; -export type JsonGet" | "->>"> = - Walk extends [infer X, infer N extends boolean] +export type JsonGet< + V, + P extends readonly JsonStep[], + Kind extends "->" | "->>", +> = + JsonWalk extends [infer X, infer N extends boolean] ? unknown extends X ? { readonly type: Kind extends "->" ? "text" : "any"; diff --git a/packages/core/src/sqlite/engine/typer/params.ts b/packages/core/src/sqlite/engine/typer/params.ts index 2d6af766..9877fa48 100644 --- a/packages/core/src/sqlite/engine/typer/params.ts +++ b/packages/core/src/sqlite/engine/typer/params.ts @@ -1,6 +1,12 @@ +import type { + DeclaredFn, + PickSig, + SigSlots, +} from "../../../engine/family/functions.js"; +import type { QueryContext } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; import type { NullTest, ParamEntry } from "../../../engine/params.js"; -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, QueryContext, Scope } from "../../../engine/types.js"; +import type { ColInfo, Info, Scope } from "../../../engine/types.js"; import type { Call, CaseExpr, @@ -14,8 +20,8 @@ import type { import type { BindFrom } from "../binder/scope.js"; import type { BindWith } from "../project/select.js"; import type { Affinity, MkCtx, SqCteMap, SqCtx, SqLevel } from "../types.js"; -import type { DeclaredFn, PickSig, SigArgType } from "./functions.js"; -import type { Common, Info, SubqueryCol, TypeOf } from "./typeof.js"; +import type { FunctionCatalog, Functions } from "./functions.js"; +import type { Common, SubqueryCol, TypeOf } from "./typeof.js"; type Ctx = MkCtx< DB, @@ -29,17 +35,9 @@ type ParamN = E extends { readonly n: infer N extends number; } ? N - : E extends { - readonly t: "collate" | "un"; - readonly op?: "+"; - readonly e: infer X; - } + : E extends { readonly t: "collate"; readonly e: infer X } ? X extends { readonly t: "param"; readonly n: infer N extends number } - ? E extends { readonly t: "un"; readonly op: infer O } - ? O extends "+" - ? N - : never - : N + ? N : never : never; @@ -164,7 +162,7 @@ type RowPairs< I extends number, > = L[I] extends infer A extends Expr ? R[I] extends infer B extends Expr - ? RowPairs, Nx> + ? RowPairs, Next> : M : M; @@ -219,9 +217,9 @@ type FirstSib< ? Sib extends infer S ? S extends ColInfo ? S - : FirstSib> + : FirstSib> : never - : FirstSib> + : FirstSib> : null; export type ParamsOfList = FnArgs< L, @@ -280,7 +278,7 @@ type CaseValues< Ws, Else, C, - Nx, + Next, Sib extends infer S extends ColInfo ? [...Acc, S] : Acc > : Else extends Expr @@ -302,65 +300,6 @@ type CaseLoop = Branch extends { >) : never; -type TextFns = - | "lower" - | "upper" - | "ltrim" - | "rtrim" - | "trim" - | "replace" - | "instr" - | "like" - | "glob" - | "unicode" - | "unhex" - | "soundex" - | "concat" - | "concat_ws" - | "format" - | "printf" - | "group_concat" - | "string_agg"; -type NumFns = - | "abs" - | "round" - | "sign" - | "ceil" - | "ceiling" - | "floor" - | "trunc" - | "acos" - | "acosh" - | "asin" - | "asinh" - | "atan" - | "atan2" - | "atanh" - | "cos" - | "cosh" - | "degrees" - | "exp" - | "ln" - | "log" - | "log10" - | "log2" - | "mod" - | "pow" - | "power" - | "radians" - | "sin" - | "sinh" - | "sqrt" - | "tan" - | "tanh" - | "zeroblob" - | "randomblob" - | "ntile" - | "sum" - | "total" - | "avg"; -type SameFns = "coalesce" | "ifnull" | "nullif" | "max" | "min"; - type CallParams = | ParamsOfExpr | FnArgs> @@ -372,52 +311,22 @@ type CallParams = ? ParamsOfList<[...P, ...O], C> : never); type CallSlots = - DeclaredFn extends infer D extends {} + DeclaredFn extends infer D extends {} ? SigSlots, E["args"]> - : E["name"] extends SameFns - ? readonly Info< - FirstSib extends infer S extends ColInfo - ? S["type"] - : "unknown", - true - >[] - : E["name"] extends "iif" | "if" - ? IifSlots, true>> - : E["name"] extends "substr" | "substring" - ? [Info<"text", true>, Info<"integer", true>, Info<"integer", true>] - : E["name"] extends "lag" | "lead" | "nth_value" - ? [ - Info<"unknown", true>, - Info<"integer", false>, - Info<"unknown", true>, - ] - : E["name"] extends - | "json_extract" - | "jsonb_extract" - | "json_type" - | "json_array_length" - | "json_remove" - | "jsonb_remove" - | "json_valid" - | "json" - | "jsonb" - | "json_patch" - | "jsonb_patch" - | "json_pretty" - ? [Info<"json", true>, ...Info<"text", true>[]] - : E["name"] extends TextFns - ? readonly Info<"text", true>[] - : E["name"] extends NumFns - ? readonly Info<"numeric", true>[] - : readonly Info<"unknown", true>[]; - -type SigSlots< - S, - L extends readonly unknown[], - Acc extends readonly unknown[] = [], -> = Acc["length"] extends L["length"] - ? Acc - : SigSlots, true>]>; + : E["name"] extends keyof Functions + ? Functions[E["name"]] extends { readonly args: infer A } + ? A extends readonly unknown[] + ? A + : A extends "same" + ? readonly Info< + FirstSib extends infer S extends ColInfo + ? S["type"] + : "unknown", + true + >[] + : IifSlots, true>> + : readonly Info<"unknown", true>[] + : readonly Info<"unknown", true>[]; export type FnArgs< L extends readonly Expr[], @@ -431,7 +340,7 @@ export type FnArgs< C, M | Slot>, Ts, - Nx + Next > : M; @@ -444,7 +353,7 @@ type IifValueType< ? IifValueType< L, C, - Nx>, + Next>, Sib extends infer S extends ColInfo ? [...Acc, S] : Acc > : Common; @@ -469,7 +378,7 @@ type IsCond = I extends | 6 | 8 | 10 - ? L[Nx] extends Expr + ? L[Next] extends Expr ? true : false : false; diff --git a/packages/core/src/sqlite/engine/typer/typeof.ts b/packages/core/src/sqlite/engine/typer/typeof.ts index e83dc2d8..0ffa61c3 100644 --- a/packages/core/src/sqlite/engine/typer/typeof.ts +++ b/packages/core/src/sqlite/engine/typer/typeof.ts @@ -1,5 +1,23 @@ -import type { Nx } from "../../../engine/parse.js"; -import type { ColInfo, Scope } from "../../../engine/types.js"; +import type { + LastPart, + NameAt, + NamedAt, +} from "../../../engine/family/diagnostics.js"; +import type { + ArityOk, + DeclaredFn, + PickSig, +} from "../../../engine/family/functions.js"; +import type { JsonShapeOf, JsonStep } from "../../../engine/family/json.js"; +import type { + AllNull, + AnyNull, + NonZeroDigits, +} from "../../../engine/family/typer.js"; +import type { AggCtx } from "../../../engine/family/types.js"; +import type { Next } from "../../../engine/next.js"; +import type { Or } from "../../../engine/object.js"; +import type { ColInfo, Info } from "../../../engine/types.js"; import type { AnyErr, SqlError } from "../../../errors.js"; import type { Call, @@ -16,22 +34,9 @@ import type { ResolveColumn, } from "../binder/scope.js"; import type { ProjectSelect } from "../project/select.js"; -import type { - Affinity, - Cell, - GE, - MkCtx, - NumClass, - SqCtx, - SqItem, -} from "../types.js"; -import type { DeclaredFn, Fn, Functions, PickSig, SigFn } from "./functions.js"; -import type { JsonGet, JsonPath, JsonShapeOf, JsonStep } from "./json.js"; - -export type Info = { - readonly type: T; - readonly nullable: N; -}; +import type { Affinity, Cell, NumClass, SqCtx, SqItem } from "../types.js"; +import type { Fn, FunctionCatalog, Functions, SigFn } from "./functions.js"; +import type { JsonGet, JsonPath } from "./json.js"; export type TypeOf = E extends { readonly t: "col"; @@ -138,13 +143,6 @@ export type TypeList< : TypeOf : Acc; -type AnyNull = true extends L[number]["nullable"] - ? true - : false; -type AllNull = false extends L[number]["nullable"] - ? false - : true; - type Pred< L extends readonly Expr[], C extends SqCtx, @@ -155,8 +153,8 @@ type Pred< : TypeList; type TypeLike = E["kind"] extends "regexp" - ? DeclaredFn extends null - ? SqlError<"no such function: REGEXP", null> + ? DeclaredFn extends null + ? SqlError<"no such function: REGEXP", NameAt<"regexp", "function">> : Pred<[E["e"], E["pat"]], C, true> : Pred< E["esc"] extends Expr ? [E["e"], E["pat"], E["esc"]] : [E["e"], E["pat"]], @@ -195,7 +193,7 @@ type TypeInTable = ? E["args"] extends readonly Expr[] ? E["table"] extends "json_each" | "json_tree" ? InWidth<8> - : SqlError<`no such table: ${E["table"]}`, null> + : SqlError<`no such table: ${E["table"]}`, NamedAt> : E["table"] extends keyof C["ctes"] ? InWidth : LookupRelation< @@ -204,8 +202,14 @@ type TypeInTable = > extends [infer Rel, string] ? RelationItem extends infer It extends SqItem ? InWidth - : SqlError<`no such table: ${E["table"]}`, null> - : SqlError<`no such table: ${E["table"]}`, null> + : SqlError< + `no such table: ${E["table"]}`, + NamedAt + > + : SqlError< + `no such table: ${E["table"]}`, + NamedAt + > : TypeOf; type CastType = From extends "null" @@ -256,13 +260,6 @@ type Unsigned = V extends `${"-" | "+"}${infer R}` type IntPartNonZero = U extends `${infer I}.${string}` ? NonZeroDigits : NonZeroDigits; -type NonZeroDigits = V extends `${infer H}${infer R}` - ? H extends "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" - ? true - : H extends "e" | "E" - ? false - : NonZeroDigits - : false; type TypeBin = Op extends | "=" @@ -293,8 +290,6 @@ type TypeBin = Op extends : TypeOf : TypeOf; -type Or = A extends true ? true : B extends true ? true : false; - type JsonOp" | "->>", A extends ColInfo, R> = JsonShapeOf extends [infer V] ? JsonPath extends infer P extends readonly JsonStep[] @@ -404,7 +399,7 @@ type CaseLoop< ? TypeOf extends AnyErr ? TypeOf : TypeOf extends infer Y extends ColInfo - ? CaseLoop> + ? CaseLoop> : TypeOf : Acc; @@ -430,7 +425,7 @@ type AggOf = E["name"] extends "min" | "max" : Functions[E["name"] & keyof Functions]; type FnOf = - DeclaredFn extends infer D + DeclaredFn extends infer D ? D extends null ? E["name"] extends keyof Functions ? AggOf @@ -445,10 +440,10 @@ type TypeCall = [F["kind"], E["over"]] extends ["agg", null] ? [C["inAgg"], C["agg"]] extends [false, "group" | "whole"] ? CallShape - : `misuse of aggregate function ${E["disp"]}()` + : FnErr<`misuse of aggregate function ${E["disp"]}()`, E> : CallShape - ) extends infer M extends string - ? SqlError + ) extends infer M extends AnyErr + ? M : ( [F["kind"], E["over"]] extends ["agg", null] ? AggCtx @@ -465,51 +460,58 @@ type TypeCall = : Info, NullOf> : TypeList : never - : SqlError<`no such function: ${E["disp"]}`, null> + : SqlError< + `no such function: ${E["disp"]}`, + NameAt< + LastPart>, + "function", + Lowercase + > + > : never; -type AggCtx = ( - C extends { readonly aggScope: infer S extends Scope } - ? S - : C["scope"] -) extends infer S extends Scope - ? MkCtx - : never; - type CallShape = E["star"] extends true ? E["name"] extends "count" ? E["distinct"] extends true - ? "DISTINCT aggregates must have exactly one argument" + ? SqlError<"DISTINCT aggregates must have exactly one argument", null> : null - : `wrong number of arguments to function ${E["disp"]}()` + : FnErr<`wrong number of arguments to function ${E["disp"]}()`, E> : ArityOk extends false - ? `wrong number of arguments to function ${E["disp"]}()` + ? FnErr<`wrong number of arguments to function ${E["disp"]}()`, E> : F["kind"] extends "agg" ? E["over"] extends null ? AggPlacement : E["distinct"] extends true - ? "DISTINCT is not supported for window functions" + ? SqlError<"DISTINCT is not supported for window functions", null> : null : F["kind"] extends "win" ? E["over"] extends null - ? `misuse of window function ${E["disp"]}()` + ? FnErr<`misuse of window function ${E["disp"]}()`, E> : null : E["over"] extends null ? E["filter"] extends null ? E["distinct"] extends true - ? `DISTINCT aggregates must have exactly one argument` + ? SqlError< + `DISTINCT aggregates must have exactly one argument`, + null + > : null - : `FILTER may not be used with non-aggregate ${E["disp"]}()` - : `${E["disp"]}() may not be used as a window function`; + : SqlError< + `FILTER may not be used with non-aggregate ${E["disp"]}()`, + null + > + : FnErr<`${E["disp"]}() may not be used as a window function`, E>; +/** An error about call `E`, which points at the function's name. */ +type FnErr = SqlError< + M, + NameAt> +>; type AggPlacement = E["distinct"] extends true ? E["args"]["length"] extends 1 ? null - : "DISTINCT aggregates must have exactly one argument" + : SqlError<"DISTINCT aggregates must have exactly one argument", null> : null; -type ArityOk = - GE extends true ? (Max extends 1000 ? true : GE) : false; - type CheckExtras = ( E["filter"] extends Expr ? TypeOf @@ -539,11 +541,7 @@ type RetType = R extends "arg0" ? Affinity extends "blob" ? "blob" : "text" - : R extends "jsonext" - ? A["length"] extends 2 - ? "any" - : "text" - : R; + : R; type AbsType = [Aff] extends ["integer"] ? "integer" : [Aff] extends ["real"] @@ -564,8 +562,8 @@ type IifValues< A extends readonly ColInfo[], I extends number, Acc extends readonly ColInfo[], -> = A[Nx] extends ColInfo - ? IifValues>, [...Acc, A[Nx]]> +> = A[Next] extends ColInfo + ? IifValues>, [...Acc, A[Next]]> : A[I] extends ColInfo ? [...Acc, A[I]] : Acc; diff --git a/packages/core/src/sqlite/engine/types.ts b/packages/core/src/sqlite/engine/types.ts index 73dce60c..514f227c 100644 --- a/packages/core/src/sqlite/engine/types.ts +++ b/packages/core/src/sqlite/engine/types.ts @@ -1,58 +1,20 @@ -import type { ColInfo, Scope } from "../../engine/types.js"; - -export type SqCol = ColInfo & { readonly n: string }; -export type SqColOf = { - readonly type: T; - readonly nullable: N; - readonly n: Name; -}; -export type SqColMap = { readonly [lc: string]: SqCol }; - -export type SqItem = { - readonly alias: string | null; - readonly cols: SqColMap; - readonly skip: readonly string[]; - readonly order: readonly string[]; - readonly rowid: string | null; -}; +import type { Item } from "../../engine/family/types.js"; + +export type { + AggMode, + Cell, + Col as SqCol, + ColMap as SqColMap, + ColOf as SqColOf, + CteMap as SqCteMap, + Ctx as SqCtx, + MkCtx, +} from "../../engine/family/types.js"; + +/** A relation in scope; `rowid` names its rowid column, if it has one. */ +export type SqItem = Item & { readonly rowid: string | null }; export type SqLevel = readonly SqItem[]; -export type SqCteMap = { - readonly [lc: string]: { - readonly cols: SqColMap; - readonly order: readonly string[]; - }; -}; - -export type AggMode = "none" | "group" | "whole"; - -export type SqCtx = { - readonly db: unknown; - readonly scope: Scope; - readonly ctes: SqCteMap; - readonly agg: AggMode; - readonly inAgg: boolean; - readonly aliases: SqColMap; -}; - -export type MkCtx< - DB, - S extends Scope, - Ctes extends SqCteMap, - Agg extends AggMode = "none", - Aliases extends SqColMap = {}, - AggScope extends Scope = S, - InAgg extends boolean = false, -> = { - readonly db: DB; - readonly scope: S; - readonly aggScope: AggScope; - readonly ctes: Ctes; - readonly agg: Agg; - readonly inAgg: InAgg; - readonly aliases: Aliases; -}; - export type Affinity = T extends "null" | "unknown" | "any" ? T : T extends `${string}int${string}` @@ -81,16 +43,3 @@ export type NumClass = export type NormalizeType = T extends "" ? "any" : Lowercase; - -export type Cell = { - readonly n: string; - readonly t: ColInfo; - readonly d?: string; -}; - -export type Tup< - N extends number, - A extends unknown[] = [], -> = A["length"] extends N ? A : Tup; -export type GE = - Tup extends [...Tup, ...unknown[]] ? true : false; diff --git a/packages/core/src/sqlite/engine/write.ts b/packages/core/src/sqlite/engine/write.ts index 93e5ff83..6e8cb241 100644 --- a/packages/core/src/sqlite/engine/write.ts +++ b/packages/core/src/sqlite/engine/write.ts @@ -1,11 +1,11 @@ -import type * as Shared from "../../engine/write.js"; +import type * as Shared from "../../sql/write.js"; import type { SqliteEngine } from "./engine.js"; export type { RelationName, TableName, TableOf, -} from "../../engine/write.js"; +} from "../../schema.js"; export type SetInput = Shared.SetInput< SqliteEngine, diff --git a/packages/core/src/sqlite/index.ts b/packages/core/src/sqlite/index.ts index 52dd0909..30223bba 100644 --- a/packages/core/src/sqlite/index.ts +++ b/packages/core/src/sqlite/index.ts @@ -2,7 +2,7 @@ export { type Sqlite, type SqliteDialect, sqlite, - type TransactionalSql, + type ValidateDdl, } from "./dialect.js"; export type { ApplyDdl, @@ -23,7 +23,6 @@ export type { TableName, TableOf, } from "./engine/write.js"; -export type { ValidateDdl } from "./migrations.js"; export type { Col, ColFlags, @@ -32,7 +31,7 @@ export type { Typed, View, } from "./schema/index.js"; -export type { Sql } from "./sql/types.js"; +export type { Sql, TransactionalSql } from "./sql/types.js"; export type { BigIntTypeMap, Bindable, diff --git a/packages/core/src/sqlite/migrations.ts b/packages/core/src/sqlite/migrations.ts deleted file mode 100644 index 2d934f19..00000000 --- a/packages/core/src/sqlite/migrations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AnyErr, SqlError } from "../errors.js"; -import type { ApplyDdl } from "./engine/infer.js"; - -export type ValidateDdl = string extends Q - ? SqlError< - "migrations need a string literal; use .raw() for SQL the type checker cannot see", - null - > - : ApplyDdl extends infer R - ? R extends AnyErr - ? R - : Q - : never; diff --git a/packages/core/src/sqlite/schema/index.ts b/packages/core/src/sqlite/schema/index.ts index 9498a1a5..4531525c 100644 --- a/packages/core/src/sqlite/schema/index.ts +++ b/packages/core/src/sqlite/schema/index.ts @@ -1,6 +1,6 @@ -import type { ColOptions, Shaped, TsShape, TypeName } from "../../schema.js"; +import type { FlaggedCol, Typed, TypeName } from "../../schema.js"; -export type { View } from "../../schema.js"; +export type { Typed, View } from "../../schema.js"; export type SqliteTypeName = | "integer" @@ -21,31 +21,12 @@ export type ColFlags = { readonly hidden?: boolean; }; -// Options are read through ColOptions (see there) so columns with different -// options compare by these properties. An unset rowid, generated, hidden or -// position is an optional `never` property, so a column that has it is not -// assignable to one that does not. -export type Col = { - readonly type: T; - readonly nullable: ColOptions extends { nullable: true } ? true : false; - readonly hasDefault: ColOptions extends { hasDefault: true } - ? true - : ColOptions extends { generated: true } - ? true - : false; -} & (ColOptions extends { readonly position: infer P extends number } - ? { readonly i: P } - : { readonly i?: never }) & - (ColOptions extends { readonly rowid: true } - ? { readonly rowid: true } - : { readonly rowid?: never }) & - (ColOptions extends { readonly generated: true } - ? { readonly generated: true } - : { readonly generated?: never }) & - (ColOptions extends { readonly hidden: true } - ? { readonly hidden: true } - : { readonly hidden?: never }); - -export type Typed = Shaped>; +export type Col = FlaggedCol< + T, + F, + "rowid", + false +>; +/** A JSON column holding documents of type `T`. */ export type Json = Col, F>; diff --git a/packages/core/src/sqlite/sql/types.ts b/packages/core/src/sqlite/sql/types.ts index 4038e0d6..76042147 100644 --- a/packages/core/src/sqlite/sql/types.ts +++ b/packages/core/src/sqlite/sql/types.ts @@ -1,6 +1,7 @@ import type { DIALECT, DialectBrand, StrictSql } from "../../dialect.js"; import type { SqlPlugin } from "../../plugin.js"; import type { SqlApi } from "../../sql/api.js"; +import type { Transactional } from "../../transaction.js"; import type { SqliteEngine } from "../engine/engine.js"; // Invariant parameters and the [DIALECT] brand: see the PostgreSQL Sql. @@ -12,4 +13,21 @@ export interface Sql< readonly [DIALECT]?: DialectBrand<"sqlite", DB, TM, Plugins>; /** The database this `sql` runs on: `"sqlite"`. */ readonly dialect: "sqlite"; + /** This `sql` with `plugin` after its plugins. */ + withPlugin( + plugin: P, + ): Sql; +} + +/** An `sql` with `transaction(fn)`, as adapters create it. */ +export interface TransactionalSql< + in out DB, + in out TM, + in out Plugins extends readonly SqlPlugin[] = [], +> extends Sql, + Transactional> { + readonly [DIALECT]?: DialectBrand<"sqlite", DB, TM, Plugins>; + withPlugin( + plugin: P, + ): TransactionalSql; } diff --git a/packages/core/src/sqlite/type-map/index.ts b/packages/core/src/sqlite/type-map/index.ts index b2bc92c9..a1683d94 100644 --- a/packages/core/src/sqlite/type-map/index.ts +++ b/packages/core/src/sqlite/type-map/index.ts @@ -1,3 +1,4 @@ +import type { JsonAs, NullableTs, ParamMapOf, TsShaped } from "../../schema.js"; import type { Affinity } from "../engine/types.js"; export interface DefaultTypeMap { @@ -37,47 +38,39 @@ export interface DefaultParamTypeMap { unknown: Bindable; } -export type ParamTypeMapOf = TM extends { readonly $params: infer P } - ? P - : DefaultParamTypeMap; +export type ParamTypeMapOf = ParamMapOf; -type TsTypeOf = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? T extends "json" - ? TsTypeOf - : V - : T extends keyof TM - ? TM[T] - : Affinity extends infer A - ? A extends keyof TM - ? TM[A] - : unknown - : never; +type TsTypeOf = + T extends TsShaped + ? T extends "json" + ? JsonAs, V> + : V + : T extends keyof TM + ? TM[T] + : Affinity extends infer A + ? A extends keyof TM + ? TM[A] + : unknown + : never; export type TsOf< TM, _DB, T extends { type: string; nullable: boolean }, -> = T["type"] extends "null" - ? null - : T["nullable"] extends true - ? TsTypeOf | null - : TsTypeOf; +> = NullableTs>; export type TsParamTypeOf = ParamTs< TM, ParamTypeMapOf, T >; -type ParamTs = T extends { - readonly shape: { readonly k: "ts"; readonly v: infer V }; -} - ? T extends "json" - ? ParamTs - : V - : T extends keyof TM - ? TM[T] | (T extends keyof PM ? PM[T] : never) - : Affinity extends infer A extends string - ? ParamTs - : never; +type ParamTs = + T extends TsShaped + ? T extends "json" + ? JsonAs, V> + : V + : T extends keyof TM + ? TM[T] | (T extends keyof PM ? PM[T] : never) + : Affinity extends infer A extends string + ? ParamTs + : never; diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 473a73d4..b79b32ab 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -1,5 +1,6 @@ import { createSqlApi } from "./create-sql.js"; import type { Dialect, DialectTypes, TransactionalSqlOf } from "./dialect.js"; +import { type DialectFamily, dialectFamily } from "./family.js"; import type { SqlPlugin } from "./plugin.js"; import type { Executor } from "./query.js"; @@ -9,11 +10,22 @@ export interface Transactional { export interface SqlDriver { executor(client: C): Executor; + /** + * Run `fn` in a transaction on `client`. `depth` is how many transactions + * enclose the `sql`. `savepoint` names the savepoint to make instead of a + * transaction of its own, or is `undefined` when none encloses `client`. + */ transaction( client: C, depth: number, fn: (tx: C) => Promise, + savepoint?: string | undefined, ): Promise; + /** + * Whether `client` is a transaction already, such as an ORM's that the + * caller passed: `sql.transaction()` on it makes a savepoint too. + */ + inTransaction?(client: C): boolean; } export function createTransactionalSql( @@ -22,9 +34,33 @@ export function createTransactionalSql( driver: SqlDriver, depth = 0, ): TransactionalSqlOf { - return createWithPlugins(dialect, client, driver, depth, []); + const sql = createWithPlugins(dialect, client, driver, depth, []); + // The dialect's transactional `sql` type is computed from its type + // parameters; the runtime API cannot be checked against it. + // biome-ignore lint/nursery/noUnsafeTypeAssertion: see above. + return sql as TransactionalSqlOf; } +/** + * The transactional `sql` of `dialect`, which the caller picks at runtime, + * on `client` with `driverOf` its family's driver. Its type is the + * caller's overloads'. + */ +export const createFamilySql = ( + dialect: Dialect, + client: C | undefined, + driverOf: (family: DialectFamily) => SqlDriver, +): RuntimeSql => + createWithPlugins( + dialect, + client, + driverOf(dialectFamily(dialect.name)), + 0, + [], + ); + +const savepointName = (depth: number): string => `typesql_${depth}`; + const ENDED = "typesql: this transaction has ended; run later queries on the sql that started it"; @@ -51,25 +87,30 @@ function whileOpen(executor: Executor, open: () => boolean): Executor { }); } -function createWithPlugins( - dialect: Dialect, +/** A transactional `sql` as it is at runtime; its type is its dialect's. */ +type RuntimeSql = ReturnType & { + withPlugin(plugin: SqlPlugin): RuntimeSql; + transaction(fn: (tx: RuntimeSql) => Promise): Promise; +}; + +/** `depth` is how many transactions enclose the `sql`: 0 for the client's own. */ +function createWithPlugins( + dialect: Dialect, client: C | undefined, driver: SqlDriver, - depth = 0, - plugins: readonly SqlPlugin[] = [], + depth: number, + plugins: readonly SqlPlugin[], open?: () => boolean, -): TransactionalSqlOf { +): RuntimeSql { const executor = client === undefined ? undefined : driver.executor(client); const sql = createSqlApi( dialect, executor && open ? whileOpen(executor, open) : executor, plugins, ); - // The dialect's transactional `sql` type is computed from its type - // parameters; the runtime API cannot be checked against it. return Object.assign(sql, { withPlugin: (plugin: SqlPlugin) => - createWithPlugins( + createWithPlugins( dialect, client, driver, @@ -77,9 +118,7 @@ function createWithPlugins( [...plugins, plugin], open, ), - transaction: async ( - fn: (tx: TransactionalSqlOf) => Promise, - ): Promise => { + transaction: async (fn: (tx: RuntimeSql) => Promise): Promise => { if (client === undefined) throw new Error( "typesql: no client: pass one to createSql(client) to use sql.transaction()", @@ -87,32 +126,38 @@ function createWithPlugins( if (open && !open()) throw new Error(ENDED); let ended = false; const txOpen = () => !ended && (open?.() ?? true); - return driver.transaction(client, depth, async (tx) => { - try { - return await fn( - createWithPlugins( - dialect, - tx, - driver, - depth + 1, - plugins, - txOpen, - ), - ); - } finally { - ended = true; - } - }); + return driver.transaction( + client, + depth, + async (tx) => { + try { + return await fn( + createWithPlugins( + dialect, + tx, + driver, + depth + 1, + plugins, + txOpen, + ), + ); + } finally { + ended = true; + } + }, + depth > 0 || driver.inTransaction?.(client) + ? savepointName(depth) + : undefined, + ); }, - // biome-ignore lint/nursery/noUnsafeTypeAssertion: the transactional `sql` type is computed from the dialect's type parameters. - }) as TransactionalSqlOf; + }); } export async function runTransaction( run: (statement: string) => PromiseLike, fn: () => Promise, options: { - readonly savepoint?: string; + readonly savepoint?: string | undefined; readonly onRollbackError?: (cause: unknown) => void; } = {}, ): Promise { @@ -131,3 +176,53 @@ export async function runTransaction( throw err; } } + +/** + * Run `fn` in a transaction on `conn`, a connection checked out of a pool, + * and give it back with `release`: `broken` is set when ROLLBACK failed, and + * the connection may still be in the transaction, so the next caller must + * not get it. + */ +export async function pooledTransaction( + conn: C, + run: (statement: string) => PromiseLike, + release: (broken: Error | undefined) => void | Promise, + fn: (conn: C) => Promise, +): Promise { + let broken: Error | undefined; + try { + return await runTransaction(run, () => fn(conn), { + onRollbackError: (cause) => { + broken = cause instanceof Error ? cause : new Error(String(cause)); + }, + }); + } finally { + await release(broken); + } +} + +const queues = new WeakMap>(); + +/** + * Run `fn` in a transaction with `run`'s statements, one top-level + * transaction at a time on `client`: for drivers whose connection is one + * object that every caller shares, such as SQLite's. Below depth 0 it makes + * a savepoint in the transaction under way. + */ +export function serialTransaction( + client: WeakKey, + depth: number, + run: (statement: string) => Promise, + fn: () => Promise, +): Promise { + if (depth > 0) + return runTransaction(run, fn, { savepoint: savepointName(depth) }); + const current = (queues.get(client) ?? Promise.resolve()).then(() => + runTransaction(run, fn), + ); + queues.set( + client, + current.catch(() => undefined), + ); + return current; +} diff --git a/packages/docs/.vitepress/sql-highlighting.ts b/packages/docs/.vitepress/sql-highlighting.ts index 73cc06cc..1d2a38cd 100644 --- a/packages/docs/.vitepress/sql-highlighting.ts +++ b/packages/docs/.vitepress/sql-highlighting.ts @@ -32,7 +32,7 @@ export const sqlHighlighting: MarkdownOptions = { ...grammar, name: "typesql-inline", embeddedLangs: ["sql"], - injectTo: ["source.ts", "source.tsx", "source.js", "source.js.jsx"], + injectTo: manifest.contributes.grammars[0].injectTo, }); for (const [name, rules] of Object.entries(themes)) { const theme = shiki.getTheme(name); diff --git a/packages/docs/adapters/mysql.md b/packages/docs/adapters/mysql.md index f57dea2c..78d67c57 100644 --- a/packages/docs/adapters/mysql.md +++ b/packages/docs/adapters/mysql.md @@ -90,7 +90,7 @@ The names are `tinyint`, `smallint`, `mediumint`, `int`, `bigint`, `decimal`, `f | Flag | Effect | |---|---| | `nullable`, `hasDefault` | `NULL` allowed; optional in `INSERT` | -| `position` | column order, used by `SELECT *` and `INSERT … VALUES` without a column list | +| `position` | column order, used by `SELECT *` and `INSERT … VALUES` without a column list once every column has one | | `autoIncrement` | an `AUTO_INCREMENT` column; implies `hasDefault` | | `generated` | a `GENERATED ALWAYS AS` column: readable, but rejected in `INSERT` and `UPDATE`; implies `hasDefault` | | `hidden` | an `INVISIBLE` column: usable by name, but not in `SELECT *` or `INSERT … VALUES` without a column list | @@ -149,7 +149,7 @@ The dialect is `mysql` for a `mysql://` URL and `mariadb` for a `mariadb://` URL | `--database ` | the database to read; default the one in the URL | | `--out ` | output file; default stdout | | `--exclude ` | relation to leave out, where `*` matches anything; repeat it or comma-separate | -| `--type-name ` | name of the interface; default `DB` | +| `--type-name ` | name of the interface, a TypeScript identifier; default `DB` | | `--import-from ` | where `Col`, `Enum` and `View` are imported from; default `@typesql/core/mysql` | | `--no-comments` | leave out the JSDoc (comments, generated and invisible columns) | @@ -224,7 +224,7 @@ Row keys are the column names the server reports: Use `$1`, `:name` or `${sql.param()}`; `?` is rejected with an error that says so, because fragments and inline values are numbered with `$n`. The adapters rewrite `$n` to `?` before the statement reaches the driver. -Parameters take their type from where they are used. Integer parameters accept `number | bigint` (`BIGINT` also a `string`), `DECIMAL` accepts `number | bigint | string`, date types accept `Date | string`, binary types accept `Uint8Array | string`, and a parameter without context accepts any value the drivers bind. `sql.json(value)` binds the value as JSON text. +Parameters take their type from where they are used. Integer parameters accept `number | bigint` (`BIGINT` also a `string`), `DECIMAL` accepts `number | bigint | string`, date types accept `Date | string`, binary types accept `Uint8Array | string`, and a parameter without context accepts any value the drivers bind. `sql.json(value)` binds the value as JSON text; a JSON parameter takes it or JSON text, not a plain object, which the drivers can't bind. ## Statements @@ -298,7 +298,7 @@ The schema a migration leaves follows MySQL's rules: ## Limits -- Without `position` flags, `SELECT *` order is unspecified and `INSERT INTO t VALUES (…)` without a column list may type its values against the wrong columns, so name your columns or generate the schema. +- Without `position` flags, `SELECT *` order is unspecified, `INSERT INTO t VALUES (…)` without a column list checks only the number of values and types its parameters as any value MySQL can bind, and `FIRST` / `AFTER` in `ALTER TABLE` leave the table without positions, so name your columns or generate the schema. - `sql()` on mysql2 and the mariadb connector, and every query on the HTTP drivers, write the values into the statement before the server sees it, so an unaliased expression with a parameter is named after its value (`n + 7`), not `n + ?`. Give such expressions an alias. - A string literal that contains `\'` confuses `:name` rewriting and fragment interpolation. Write `''` instead. - `/*! … */` executable comments are treated as comments. diff --git a/packages/docs/adapters/sqlite.md b/packages/docs/adapters/sqlite.md index 2228303c..d5944b29 100644 --- a/packages/docs/adapters/sqlite.md +++ b/packages/docs/adapters/sqlite.md @@ -49,7 +49,7 @@ interface DB { | Flag | Effect | |---|---| | `nullable`, `hasDefault` | `NULL` allowed; optional in `INSERT` | -| `position` | column order, used by `SELECT *` and `INSERT … VALUES` without a column list | +| `position` | column order, used by `SELECT *` and `INSERT … VALUES` without a column list once every column has one | | `rowid` | the `INTEGER PRIMARY KEY`, so `rowid` resolves to it | | `generated` | readable, but rejected in `INSERT` and `UPDATE` | | `hidden` | usable by name, but not in `SELECT *` | @@ -135,6 +135,6 @@ await createMigrator({ ## Limits -- Without `position` flags, `SELECT *` order is unspecified, so name your columns or generate the schema. +- Without `position` flags, `SELECT *` order is unspecified and `INSERT INTO t VALUES (…)` without a column list checks only the number of values and types its parameters as `unknown`, so name your columns or generate the schema. - `sql.values()` rows must all have the same keys, because SQLite has no `DEFAULT` inside `VALUES`. - `ON CONFLICT` targets aren't checked against unique constraints. diff --git a/packages/docs/guide/introspect.md b/packages/docs/guide/introspect.md index 3ba3afb5..71c5c5ef 100644 --- a/packages/docs/guide/introspect.md +++ b/packages/docs/guide/introspect.md @@ -72,7 +72,7 @@ The file has the real Postgres types: `timestamptz`, `date` and `timestamp` stay | `--out ` | output file; default stdout | | `--schema ` | schema to read; repeat it or comma-separate (`--schema public,auth`). Default: every schema except `pg_catalog`, `information_schema` and `pg_*` | | `--exclude ` | relation to leave out: a name or `schema.name`, where `*` matches anything (`--exclude 'kysely_migration*'`); repeat it or comma-separate | -| `--type-name ` | name of the interface; default `DB` | +| `--type-name ` | name of the interface, a TypeScript identifier; default `DB` | | `--import-from ` | where `Col`, `View`, … are imported from; default `@typesql/core/postgres` | | `--config ` | a module whose default export holds these options and the ones below | | `--no-comments` | leave out the JSDoc (comments, domains, generated columns) | diff --git a/packages/docs/guide/limits.md b/packages/docs/guide/limits.md index 4e73463d..7103d70d 100644 --- a/packages/docs/guide/limits.md +++ b/packages/docs/guide/limits.md @@ -30,6 +30,7 @@ With `transaction: false` (Cloudflare D1, libsql's client), there is no transact ## SQL text +- In PostgreSQL, the `ORDER BY` of an aggregate call, window frame bounds (`ROWS n PRECEDING`) and the sets inside `GROUPING SETS` are parsed past, not checked: an unknown column there is only rejected by the database, and a parameter there is typed `unknown`. - Only literal SQL is analysed: a string literal, or a template literal whose substitutions are compile-time constants. Anything else is an error; see [Parameters](./using-sql#parameters). - Tagged templates (`` sql`…` ``) are not supported, because TypeScript does not expose their text to the type system. - Mixing `:name` and `$n` parameters in one statement is an error. @@ -58,7 +59,7 @@ The schema does not tell identity `GENERATED ALWAYS` columns apart from other co - Inline values (`sql.param()`, `sql.json()`) and values in fragments, `sql.set` and `sql.values` are checked against the parameter's type, with some leniency. A plain `string`, `number` or `boolean` is checked by kind only, so any `string` is accepted for an enum column. An object with an index signature is not checked for missing keys. An array's elements are checked against the union of the parameter's element types, so tuples are checked loosely. Some values are checked by kind only (a union of more than 64 literals, an object with more than 48 keys, a union of more than 8 object types, a function) or not at all (anything nested more than six levels deep). - A plain `jsonb` parameter (typed `unknown`) refuses objects with a `length` key. Wrap those in `sql.json()`. - Inside JSON, `int8` and `numeric` are numbers. `JSON.parse` rounds integers beyond 2^53, and a `float8` / `numeric` that is `NaN` or `Infinity` is serialized as a string. -- Elements of array columns, and of arrays that columns and functions return, are typed non-null, since PostgreSQL doesn't record whether an array holds NULLs. Arrays built from nullable values have nullable elements: `array_agg(x)` over a nullable `x`, `ARRAY[…]` and `ARRAY(SELECT …)` with a nullable item, `array_append` and the other array functions given a nullable element, and `regexp_match`. Where the engine loses track of an array's elements (a view column created by DDL, a chained slice such as `x[1][1:2]`), they are typed non-null. +- Elements of array columns, and of arrays that columns and functions return, are typed non-null, since PostgreSQL doesn't record whether an array holds NULLs. Arrays built from nullable values have nullable elements: `array_agg(x)` over a nullable `x`, `ARRAY[…]` and `ARRAY(SELECT …)` with a nullable item, `array_append` and the other array functions given a nullable element, a cast array parameter such as `$1::text[]`, and `regexp_match`. Where the engine loses track of an array's elements (a view column created by DDL, a chained slice such as `x[1][1:2]`), they are typed non-null. - Arrays are typed one-dimensional. PostgreSQL doesn't record dimensions in a type (`int[][]` is the same type as `int[]`), so a multi-dimensional value such as `ARRAY[[1, 2], [3, 4]]` is typed `number[]` but comes back nested (`[[1, 2], [3, 4]]`). Assert the row type with `sql.as()` where you store such values. - In `JSON_OBJECT`, write pairs as `k VALUE v` or `k: v`. In `k:v`, the `:v` is read as a [named parameter](./using-sql#parameters) when `v` is a name. diff --git a/packages/docs/guide/migrations.md b/packages/docs/guide/migrations.md index 802b0f01..3a019668 100644 --- a/packages/docs/guide/migrations.md +++ b/packages/docs/guide/migrations.md @@ -420,7 +420,7 @@ const { applied } = await migrator.latest(); | Option | Meaning | |---|---| | `dialect` | `"postgres"`, `"sqlite"`, `"mysql"` or `"mariadb"` | -| `url` | PostgreSQL, MySQL and MariaDB: a connection URL (`mysql://user:password@host:3306/db` names the database). The migrator connects with `pg` or `mysql2`, optional peer dependencies of `@typesql/core`: install the one you use | +| `url` | PostgreSQL, MySQL and MariaDB: a connection URL (`mysql://user:password@host:3306/db` names the database). The migrator connects with `pg` or `mysql2`, optional peer dependencies of `@typesql/core`: install the one you use. A migration's `db` types rows as these return them ([type maps](../reference/type-mapping)) | | `db` | SQLite: a database file, opened with `node:sqlite` | | `execute` | your own connection: an [executor](../adapters/custom-executor#write-an-executor), used for every call and never closed | | `migrationsDir` | a directory of migration files | diff --git a/packages/docs/guide/orms.md b/packages/docs/guide/orms.md index 0fa0c6c8..3fa08194 100644 --- a/packages/docs/guide/orms.md +++ b/packages/docs/guide/orms.md @@ -409,7 +409,7 @@ Only database defaults (`autoincrement()`, `now()`, `dbgenerated()`, literals) m | Generator option | Default | | |---|---|---| | `output` | required | a `.ts` file, or a directory (writes `index.ts`) | -| `typeName` | `"DB"` | name of the exported interface | +| `typeName` | `"DB"` | name of the exported interface, a TypeScript identifier | | `importFrom` | `"@typesql/core/postgres"` for PostgreSQL, `"@typesql/core/sqlite"` for SQLite, `"@typesql/core/mysql"` for MySQL | module `Col`, `View` and `Typed` are imported from | ### Running queries @@ -477,7 +477,7 @@ Statements are sent through the ORM as for PostgreSQL. Since some SQLite drivers | | | `Json` | `jsonb` | | `unknown` | `customType` | | `any` (Kysely's `unknown`: `Typed<"json", {}>`) | -- **Kysely**: `number` can't say whether a column is `INTEGER` or `REAL`, so it is `numeric`; rows are `number` either way. Pin a column with an override (`{ posts: { id: "integer" } }`), as for PostgreSQL. `KyselySqliteSql` and `FromKyselySqlite` are the SQLite counterparts of `KyselySql` and `FromKysely`. +- **Kysely**: `number` can't say whether a column is `INTEGER` or `REAL`, so it is `numeric`; rows are `number` either way. Pin a column with an override (`{ posts: { id: "integer" } }`), as for PostgreSQL. A `"schema.table"` key is a table of an attached database, `schema.table`, and a `"main.table"` key a table of the main one. `KyselySqliteSql` and `FromKyselySqlite` are the SQLite counterparts of `KyselySql` and `FromKysely`. - **Drizzle**: rows are what the driver returns, so an `integer({ mode: "boolean" })` column is `0` or `1` and a `timestamp` column is a number. `generatedAlwaysAs()` columns can't be written; `$defaultFn()` columns have no database default. `createSql(sqlite, db)` works with every Drizzle SQLite driver; on better-sqlite3 and bun:sqlite, `sql.transaction(fn)` runs `BEGIN` / `COMMIT` itself, since their Drizzle transactions can't await, and on the async drivers it uses `db.transaction(fn)`. With `drizzle-orm/sqlite-proxy`, return rows as objects for `all`, since TypeSQL reads columns by name. Pin a type with `sqliteType(builder, "text")`. - **Prisma**: the `typesql-prisma` generator writes a SQLite schema when the datasource provider is `sqlite`, importing from `@typesql/core/sqlite`. Rows are typed with `PrismaSqliteTypeMap`, which follows what Prisma's SQLite adapters return from `$queryRaw`: `DATETIME` columns are `Date`, `BIGINT` is `bigint`, `DECIMAL` is `Decimal`, `BOOLEAN` is `boolean` and `Json` columns are parsed. A computed column has no declared type, so Prisma returns its raw value: `max(created_at)` is a string, not a `Date`, and computed integers (`count(*)`, `1 + 1`, `length(email)`, `max(id)`) are `bigint`, although they are typed `number`. Cast them in SQL (`CAST(count(*) AS REAL)`) or convert them with `Number()`. `FromPrismaSqlite` converts `Prisma.TypeMap` without the generator. @@ -512,6 +512,6 @@ Statements are sent through the ORM's driver, with `$1`, `$2`, … rewritten to | other objects | `json().$type()` | `Json` | `Typed<"json", T>`, with typed JSON paths; Prisma: `json` | | `unknown` | `customType` | | `json` (Kysely), `unknown` (Drizzle) | -- **Kysely**: `MysqlDialect` runs on mysql2, so rows are typed with mysql2's `DefaultTypeMap`. `number` can't say which integer type a column is, so it is `int`; pin a column with an override (`{ posts: { price: "decimal" } }`), as for PostgreSQL. `KyselyMysqlSql`, `KyselyMariaDbSql` and `FromKyselyMysql` are the MySQL counterparts of `KyselySql` and `FromKysely`. +- **Kysely**: `MysqlDialect` runs on mysql2, so rows are typed with mysql2's `DefaultTypeMap`. `number` can't say which integer type a column is, so it is `int`; pin a column with an override (`{ posts: { price: "decimal" } }`), as for PostgreSQL. A `"db.table"` key is a table of another database, `db.table`. `KyselyMysqlSql`, `KyselyMariaDbSql` and `FromKyselyMysql` are the MySQL counterparts of `KyselySql` and `FromKysely`. - **Drizzle**: `createSql(mysql, db)` takes a `drizzle-orm/mysql2` database and runs queries on the driver client under it: the mysql2 pool or connection, or the transaction's connection inside `db.transaction(fn)`; the PlanetScale and TiDB Cloud serverless databases work too, with `ServerlessTypeMap` from `@typesql/core/mysql` as the second type argument. Drizzle's types don't record `unsigned` or the text size (`mediumtext`, `longtext`), so pin those columns with `mysqlType`: `mysqlType(int("n", { unsigned: true }), "int unsigned")`. `generatedAlwaysAs()` columns can't be written, `$defaultFn()` columns have no database default, and tables of a `mysqlSchema()` are under the database name (`db.table`). `DrizzleMysqlSql`, `DrizzleMariaDbSql` and `FromDrizzleMysql` are the MySQL types. - **Prisma**: the `typesql-prisma` generator writes a MySQL schema when the datasource provider is `mysql`, importing from `@typesql/core/mysql`, with native types (`@db.UnsignedInt` is `int unsigned`, `@db.Text` is `text`) and `autoincrement()` columns marked `autoIncrement`. Pass `mysql`, or `mariadb` for a MariaDB server; Prisma Client connects through `@prisma/adapter-mariadb`. Rows are typed with `PrismaMysqlTypeMap`, which follows what Prisma's MySQL adapter returns from `$queryRaw`: `BIGINT` and `INT UNSIGNED` are `bigint` (so `count(*)` is `bigint`), `DECIMAL` is `Decimal`, `TIME` is a `Date` on 1970-01-01, `BIT` and spatial values are bytes, and `Boolean` columns are numbers (`TINYINT`). `FromPrismaMysql` converts `Prisma.TypeMap` without the generator. diff --git a/packages/docs/guide/schema.md b/packages/docs/guide/schema.md index 4e9f3365..519aeae1 100644 --- a/packages/docs/guide/schema.md +++ b/packages/docs/guide/schema.md @@ -132,7 +132,7 @@ In SQLite, a declared type name sets the column's affinity, as in `CREATE TABLE` In MySQL and MariaDB, columns use MySQL's type names in lower case without lengths, with ` unsigned` where it applies: `int`, `bigint unsigned`, `varchar`, `datetime`. Integer literals are `bigint` on MySQL and `int` on MariaDB, and `/` is a `decimal`. See [MySQL and MariaDB](../adapters/mysql#schema). -In PostgreSQL, common spellings mean the same thing: `integer`, `int` and `int4`; `bigint` and `int8`; `boolean` and `bool`; `character varying` and `varchar`; `double precision` and `float8`; `timestamp with time zone` and `timestamptz`. A name TypeSQL doesn't know, such as an extension type or a domain, is kept as it is. It maps to `unknown` unless the type map or `$types` gives it a type. +In PostgreSQL, common spellings mean the same thing: `integer`, `int` and `int4`; `bigint` and `int8`; `boolean` and `bool`; `character varying` and `varchar`; `double precision` and `float8`; `timestamp with time zone` and `timestamptz`; `character` and `bpchar` (`char(n)`). `char` is PostgreSQL's one-byte `"char"` type, as in `pg_type` and introspected schemas. A name TypeSQL doesn't know, such as an extension type or a domain, is kept as it is. It maps to `unknown` unless the type map or `$types` gives it a type. The driver decides which TypeScript type each database type becomes (node-postgres returns `int8` as a `string`, for example). This is the driver's **type map**, and you pick it by importing `createSql` from the driver's package. See [Type mapping](../reference/type-mapping). @@ -188,7 +188,7 @@ const sql = createSql(mysql); `Json` takes the same flags as `Col` as its second argument (`{ nullable: true }`, `{ hasDefault: true }`). On PostgreSQL, pass `"json"` as the third argument for a `json` column. -On PostgreSQL, rows read the column as `T`, and `->`, `#>` and subscripts look inside it (see [JSON in queries](./using-sql#json)); on MySQL and MariaDB, rows read it as `T` and `->`, `->>` and `JSON_EXTRACT` with a constant path look inside it. On SQLite, rows read the column as JSON text, and `->`, `->>` and `json_extract` with a constant path take their types from `T`: +On PostgreSQL, rows read the column as `T`, and `->`, `#>` and subscripts look inside it (see [JSON in queries](./using-sql#json)); on MySQL and MariaDB, rows read it as `T` and `->`, `->>` and `JSON_EXTRACT` with a constant path look inside it. Where the type map reads JSON as a `string`, as SQLite's maps and a MySQL map for mysql2's `jsonStrings` do, rows read the column as that text. So on SQLite, rows read the column as JSON text, and `->`, `->>` and `json_extract` with a constant path take their types from `T`: ::: code-group diff --git a/packages/docs/guide/using-sql.md b/packages/docs/guide/using-sql.md index 2eb6be4d..d882ea13 100644 --- a/packages/docs/guide/using-sql.md +++ b/packages/docs/guide/using-sql.md @@ -236,7 +236,7 @@ const patched = sql( ### Bulk inserts: `sql.values` -`sql.values(table, rows)` builds the column list and `VALUES` rows from an array of objects. On Postgres, MySQL and MariaDB, a column some rows leave out is sent as `DEFAULT` (a row with no values is `() VALUES ()` on MySQL and MariaDB); on SQLite, every row needs the same keys. +`sql.values(table, rows)` builds the column list and `VALUES` rows from an array of objects. On Postgres, MySQL and MariaDB, a column some rows leave out is sent as `DEFAULT` (a row with no values is `() VALUES ()` on MySQL and MariaDB); on SQLite, every row needs the same keys, each with a value that is sent, which the types check. ```ts declare const imported: { email: string; display_name?: string | null }[]; diff --git a/packages/docs/guide/vscode.md b/packages/docs/guide/vscode.md index a05de5f2..5e3ffc74 100644 --- a/packages/docs/guide/vscode.md +++ b/packages/docs/guide/vscode.md @@ -61,7 +61,7 @@ To turn suggestions off, set `"typesql.completions.enable": false`. - Aliases follow scope: a subquery sees its own FROM items first, then the outer query's. - Subqueries and CTEs offer the names of their output columns. - Names that the database would fold to lower case or read as a keyword are inserted in double quotes (`"UserAccounts"`, `"order"`). In MySQL and MariaDB statements, names that need quoting get backticks (`` `order` ``). -- Suggestions work in `sql()`, in `tx()` inside `sql.transaction()`, in `sql.fragment()`, `sql.prepare()` and `sql.as()`, in template literals across lines, and between `${sql.param()}` values. +- Suggestions work in `sql()`, in `tx()` inside `sql.transaction()`, in `sql.fragment()`, `sql.prepare()`, `sql.raw()` and `sql.as()`, in a migration's `db()`, `.ddl()`, `.add()` and `.raw()`, in template literals across lines, and between `${sql.param()}` values. - There are none inside string literals, comments and quoted identifiers. ## Syntax highlighting diff --git a/packages/docs/internals/releasing.md b/packages/docs/internals/releasing.md index 9484b82e..aca28022 100644 --- a/packages/docs/internals/releasing.md +++ b/packages/docs/internals/releasing.md @@ -22,12 +22,14 @@ pnpm check:intents - The summary is the changelog entry: what users now see, in as many sentences as it takes, identifiers in backticks. - Name only the packages you changed; dependents and the fixed group follow automatically. + The extension bundles `packages/language-server`, so a change to its `src` also names `typesql`. - `patch` for fixes, `major` for anything that could break users, `none` only for internal changes. - One change, one intent file in `.changeset/`. Edit the file to fix its summary before release. -- The VS Code extension is not checked by `check:intents`: add `pnpm change typesql …` with every - user-facing change to `packages/vscode`, or to `packages/language-server` when VS Code users - see it. Prerelease versions are rejected by its registries. +- `check:intents` covers the VS Code extension (`typesql`) too: its packaged files are the + `.vscodeignore` entries, `src` and the `src` of the workspace packages it bundles. Use + `--bump none` for a language-server change that VS Code users don't see. Prerelease versions + are rejected by its registries. ## Cutting a release @@ -52,7 +54,7 @@ retried with **Re-run failed jobs**. Check that every version is live: ```sh -for p in core pg postgres kysely drizzle prisma language-server sqlite mysql cli better-auth; do pnpm view @typesql/$p version; done +for p in $(node tools/packages.ts npm name); do pnpm view "$p" version; done ``` ## If publishing fails diff --git a/packages/docs/reference/api.md b/packages/docs/reference/api.md index d70b0d36..00068368 100644 --- a/packages/docs/reference/api.md +++ b/packages/docs/reference/api.md @@ -4,7 +4,7 @@ description: "API reference for @typesql/core, its PostgreSQL, SQLite, MySQL, ad # API -The public APIs of `@typesql/core`, `@typesql/core/postgres`, `@typesql/core/sqlite`, `@typesql/core/mysql`, `@typesql/core/adapter`, `@typesql/core/migration`, `@typesql/core/plugins` and the extra exports of the adapter and ORM packages. Each entry links to the guide page that explains it. +The public APIs of `@typesql/core`, `@typesql/core/postgres`, `@typesql/core/sqlite`, `@typesql/core/mysql`, `@typesql/core/adapter`, `@typesql/core/migration`, `@typesql/core/plugins`, `@typesql/core/reserved` and the extra exports of the adapter and ORM packages. Each entry links to the guide page that explains it. `@typesql/core` holds what every SQL dialect shares: `createSql`, queries, fragments, executors, plugins, transactions, the schema markers and the [dialect contract](#dialects). Migrations are in `@typesql/core/migration`. Everything PostgreSQL-specific is in `@typesql/core/postgres`: the `postgres` dialect, the type engine, `Json` and `PgCatalog`, the type maps and the function catalog. `@typesql/core/sqlite` and `@typesql/core/mysql` hold the same for SQLite and for MySQL and MariaDB; see [Dialects](#dialects). @@ -126,7 +126,7 @@ See [Dynamic SQL](../guide/using-sql#dynamic-sql). | `sql.set(table, values)`, `sql.set(values)` | `"a" = $1, "b" = $2` for the defined values; throws when none is defined | `SetInput` | | `sql.values(table, rows)`, `sql.values(rows)` | `("a", "b") VALUES ($1, $2), ($3, DEFAULT)`; throws on an empty array. On MySQL and MariaDB, names get backticks and a row with no values is `() VALUES ()` | `InsertInput` | -`table` is a `TableName`: a top-level table, or `"schema.table"`. It is used only for compile-time type checking. Without it, the fragment is checked against the target of the `UPDATE` or `INSERT` it is interpolated into: `sql.set` with every key present, `sql.values` once with every key and once with only the keys every row always sends, so a required column a row can leave out is still an error. Errors are then reported on the statement. At runtime, every enumerable key on `values` or row objects becomes a column assignment or insert value. When handling untrusted inputs (such as `req.body`), pick allowed fields explicitly to prevent mass assignment. JSON columns take `sql.json(value)` or a raw non-array value. See [Inserting and updating](../guide/using-sql#inserting-and-updating). +`table` is a `TableName`: a top-level table, or `"schema.table"`. It is used only for compile-time type checking. Without it, the fragment is checked against the target of the `UPDATE` or `INSERT` it is interpolated into: `sql.set` with every key present, `sql.values` once with every key and once with only the keys every row always sends, so a required column a row can leave out is still an error. Errors are then reported on the statement. At runtime, every enumerable key on `values` or row objects becomes a column assignment or insert value. When handling untrusted inputs (such as `req.body`), pick allowed fields explicitly to prevent mass assignment. JSON columns take `sql.json(value)`, or a raw non-array value on PostgreSQL and JSON text on SQLite, MySQL and MariaDB. On SQLite, which has no `DEFAULT` in a `VALUES` row, the rows of `sql.values` must give the same columns. See [Inserting and updating](../guide/using-sql#inserting-and-updating). ## `sql.as(text, ...params)` @@ -191,9 +191,14 @@ What adapters and custom executors build on. | `mapRows(q, rows)` | applies the query's output plugins to driver rows, for code that runs `q.text` itself | | `pluginsOf(q)` | returns the query's immutable plugin list | | `createTransactionalSql(dialect, client, driver, depth?)` | the dialect's `sql` bound to a driver's client, with `sql.transaction(fn)`; the PostgreSQL adapters pass `postgres` from `@typesql/core/postgres` | -| `SqlDriver` | how a driver runs statements and transactions on its client `C` | +| `createFamilySql(dialect, client, driverOf)` | the same for a dialect picked at runtime, with `driverOf(dialectFamily(dialect.name))` as its driver | +| `dialectFamily(name)`, `DialectFamily` | `"postgres"`, `"sqlite"` or `"mysql"`: the dialects whose statements a driver writes alike (MariaDB's are MySQL's) | +| `familyStatement[family](text, values, map?)` | a statement as a driver of `family` takes it: PostgreSQL's keeps its `$n` parameters, SQLite's and MySQL's have `?` with the values in order | +| `SqlDriver` | how a driver runs statements and transactions on its client `C`: `transaction(client, depth, fn, savepoint)` gets how many transactions enclose the `sql`, and the name of the savepoint to make instead of a transaction of its own (`undefined` when none encloses the client), which a driver can leave to its database library; the optional `inTransaction(client)` tells that a client is a transaction already, such as an ORM's, so `sql.transaction()` makes a savepoint on it | | `runTransaction(run, fn, options?)` | `BEGIN` / `COMMIT` / `ROLLBACK` (or a savepoint) around `fn`, for drivers without a transaction API | +| `pooledTransaction(conn, run, release, fn)` | `runTransaction` on a connection checked out of a pool, then `release(broken)`, where `broken` is the error of a failed `ROLLBACK` | | `JsonParam` | the runtime value `sql.json()` binds: `{ value }` with `toPostgres()`, `toJSON()`, `toString()` | +| `jsonText(value)` | `sql.json()` values as JSON text, for drivers that take JSON as text | | `positional(text, values, map?)` | rewrites `$n` parameters (outside strings, quoted names and comments) to `?` and returns the values in order, mapped by `map` (default `sqliteValue`), for SQLite drivers that bind positional parameters | | `splitParams(text, values, map?)` | the statement split at its `$n` parameters: `{ parts, values }` | | `paramNumbers(text)` | the parameter numbers a statement uses, in order of first use | @@ -202,12 +207,35 @@ What adapters and custom executors build on. | `parsePgArray(text, parse)` | parses a PostgreSQL array literal, including nested arrays and null elements, using an element parser | | `parsePgUserArrays(owner, lookup, fields, rows)` | decodes arrays of user-defined PostgreSQL types using result fields and catalog lookups | | `PgField`, `PgLookup` | field metadata and the catalog lookup contract for PostgreSQL array decoding | -| `propertyKey(name)`, `colSource(type, nullable, hasDefault)`, `schemaMembers(relations, schemaOf, member)` | helpers for emitting TypeScript schema source | +| `propertyKey(name)`, `colSource(type, nullable, hasDefault)` (deprecated: PostgreSQL's `columnSource`), `schemaMembers(relations, schemaOf, member)` | helpers for emitting TypeScript schema source | +| `schemaWriter(dialect)`, `SchemaModule` | writes one schema module of a dialect: `col(type, flags)`, `enum(values, flags)`, `typed(base, ts, flags)` (PostgreSQL's `Json<…>`, else `Col>`) and `view(body)`, then `module({ header, members, typeName?, importFrom?, imports? })`, which imports the helpers they used and rejects a `typeName` that is not an identifier | +| `columnSource(type, flags, dialect, helper?)`, `colFlags(flags, dialect)`, `ColumnFlags` | a column's `Col<…>` (or `Enum<…>`) source with every flag; a flag the dialect's `Col` implies `hasDefault` from leaves `hasDefault` out | +| `doc(lines, indent)`, `helperImport(used, from)`, `RESERVED_KEYS` | a JSDoc comment, the import of the column helpers a schema uses, and the schema keys that are not relations (`$types`, `$functions`) | | `globRegExp(pattern)` | an anchored name-matching expression where `*` matches any text | -| `introspectArgs(io, usage, parse)`, `CommandIo` | shared introspection argument validation and command output callbacks | +| `introspectArgs(io, usage, parse)`, `CommandIo`, `commandIo(io?)`, `CommandEnv`, `IntrospectOutput` | shared introspection argument validation, command output callbacks with the process's as defaults, and what a command reads and writes | +| `introspectCommand(usage, parse, run)` | a dialect's `typesql introspect`, `(argv, io?) => exit code`: `io` (`stdout`, `stderr`, `cwd`, `env`) defaults to the process's | +| `errorMessage(error)`, `listOption(values)`, `databaseUrl(url, io)`, `UsageError` | the commands' error text, comma-separated options and `--url` / `DATABASE_URL` | +| `connectPostgres(url)`, `connectMysql(url)`, `connectSqlite(file, options?)` | one connection: `{ client, execute, close }`, with `pg`, `mysql2` (or the `mariadb` connector) and `node:sqlite`; `PgClient`, `MySqlClient`, `SqliteClient` are their clients | +| `pgResult(result)`, `mysqlResult(result)` | a driver's result as an executor's: a script of several statements is its last statement's result | | `sqliteValue(value)` | a value as SQLite binds it: `sql.json()` values as JSON text, booleans as `1` / `0`; throws for a `Date` | | `mysqlPositional(text, values, map?)` | rewrites `$n` parameters (outside strings, quoted names and comments, with MySQL's backslash escapes) to `?` and returns the values in order, a value once for each use, mapped by `map` (default `mysqlValue`) | | `mysqlValue(value)` | a value as the MySQL drivers bind it: `sql.json()` values as JSON text | +| `mysqlInline(text, values)` | MySQL text with its `$n` parameters written in as escaped literals, for drivers that bind nothing on the server | +| `MySqlResult`, `MySqlValue` | a MySQL driver's raw result, and what `mysqlBind` returns | +| `mysqlBind(value)` | a value as mysql2 and the mariadb connector bind it: also `undefined` as `NULL` and a `Uint8Array` as a `Buffer`; throws for any other object | +| `mysqlSplit(text, values, map)` | MySQL text split at its `$n` parameters: `{ parts, values }` | +| `sqliteBindings(numbers, values, bare?)` | a SQLite statement's named parameters, `{ $1: … }` (`{ 1: … }` for better-sqlite3) | +| `SqliteInput` | what SQLite binds | +| `SchemaDialect` | the dialects `schemaWriter` and `columnSource` write for | + +### `@typesql/core/reserved` + +Word lists for editor tooling, with nothing of the type engines at runtime. + +| Export | Does | +|---|---| +| `RESERVED_WORDS` | per dialect, the words a table, column or alias name must be quoted to be | +| `META_KEYS` | the schema keys that are not relations | ## Public types @@ -281,6 +309,7 @@ type WithCatalog = DB & PgCatalog; |---|---| | `DefaultTypeMap` | node-postgres defaults; what core's `createSql` uses with `postgres` | | `DefaultParamTypeMap` | what parameters accept when a map has no `$params` | +| `NodePostgresTypeMap` | node-postgres's parsers with `bytea` as `Uint8Array`: what a PostgreSQL migration's `db` reads | | `PgTypeMap`, `PgParamTypeMap` | `@typesql/pg` | | `PostgresJsTypeMap`, `PostgresJsBigIntTypeMap`, `PostgresJsParamTypeMap` | `@typesql/postgres` | | `PrismaTypeMap`, `PrismaSqliteTypeMap`, `PrismaMysqlTypeMap` | `@typesql/prisma`, for PostgreSQL, SQLite and MySQL | @@ -346,6 +375,7 @@ An entry is one `Sig` or a tuple of overloads. | `MigrationError` | what `latest()`, `up()` and `down()` reject with when a migration fails: `{ migration, applied, rolledBack, schemaFile?, schemaError?, cause }` | | `MigrationStatus` | an entry of `status()`: `{ name, applied, appliedAt: Date \| null }` | | `isMigrator(value)` | whether `value` is a migrator, from any copy of `@typesql/core` | +| `loadMigrator({ cwd, env, config?, overrides })` | the migrator `typesql migrate` runs: the config file's (default `typesql.config.ts`), its paths beside it, with `overrides`; `missingCore(directory)` tells why a new migration file can't import `@typesql/core` | The config options and the method results are described in [The migrator](../guide/migrations#the-migrator). `ApplyDdl` and `ValidateDdl` come from the dialect modules ([Statement analysis](#statement-analysis)); see [What is supported](../guide/migrations#what-is-supported). @@ -357,7 +387,7 @@ A dialect pairs the runtime that builds its `sql` function with that function's | Export of `@typesql/core` | Meaning | |---|---| -| `Dialect` | the runtime side, what `createSql` takes: `{ name, quote?, emptyRow?, valuesDefault? }`. Statements are built with `$n` parameters and sent as written. `quote` is the identifier quote `sql.id()`, `sql.set()` and `sql.values()` use (`"` by default, `` ` `` for MySQL and MariaDB), `emptyRow` what `sql.values()` writes for a row with no values (`() VALUES ()` for MySQL and MariaDB), and `valuesDefault: false` makes `sql.values()` throw instead of writing `DEFAULT` for a missing column | +| `Dialect` | the runtime side, what `createSql` takes: `{ name, quote?, emptyRow?, valuesDefault?, lexis? }`. Statements are built with `$n` parameters and sent as written. `quote` is the identifier quote `sql.id()`, `sql.set()` and `sql.values()` use (`"` by default, `` ` `` for MySQL and MariaDB), `emptyRow` what `sql.values()` writes for a row with no values (`() VALUES ()` for MySQL and MariaDB), `valuesDefault: false` makes `sql.values()` throw instead of writing `DEFAULT` for a missing column, and `lexis` is the regular expression source of the text that holds no parameters (strings, quoted names and comments, then `$n` as its first capture group), by which `:name` parameters, inline values and `$n` are read (standard SQL's by default; each dialect has its own) | | `DialectTypes` | the type side: its default `typeMap`, and given `db`, `tm`, `plugins` and `ddl`, the `sql`, `transactionalSql`, `validateDdl` and `applyDdl` types | | `SqlOf`, `TransactionalSqlOf` | the `sql` function of dialect `T`, without or with `transaction(fn)` | | `ValidateDdlOf`, `ApplyDdlOf` | DDL checking and the schema after `Q`, in dialect `T` | diff --git a/packages/docs/reference/type-mapping.md b/packages/docs/reference/type-mapping.md index 683ad340..32807e81 100644 --- a/packages/docs/reference/type-mapping.md +++ b/packages/docs/reference/type-mapping.md @@ -6,7 +6,7 @@ description: "How TypeSQL maps PostgreSQL, SQLite, MySQL and MariaDB types to Ty Your schema uses the database's type names (in PostgreSQL, `int8`, `timestamptz`). A **type map** says which TypeScript type each one becomes. This page lists the maps for each driver, what parameters accept, and how to write your own map. -Each adapter's `createSql` picks the map that matches its driver. `createSql` from `@typesql/core` with the `postgres` dialect uses `DefaultTypeMap`, which matches node-postgres. +Each adapter's `createSql` picks the map that matches its driver. `createSql` from `@typesql/core` with the `postgres` dialect uses `DefaultTypeMap`, which matches node-postgres. A migration's `db` reads rows as the migrator's own connection returns them: `NodePostgresTypeMap` (node-postgres, with `bytea` as `Uint8Array`), mysql2's `DefaultTypeMap` and `Mysql2MariaDbTypeMap`, and SQLite's `DefaultTypeMap`. ## At a glance @@ -32,7 +32,7 @@ The common types under each driver's default parsers: Kysely and Drizzle run on one of these two drivers. With Kysely's `PostgresDialect` (node-postgres), pass `PgTypeMap` for the exact `pg` types. With `drizzle-orm/postgres-js`, pass `PostgresJsTypeMap` (`interval` is a string there); `@typesql/drizzle` parses dates back to `Date` on that driver. See [From Kysely, Drizzle or Prisma](../guide/orms). -A `json` / `jsonb` value with a known shape ([`Json` columns](../guide/schema#typed-json-columns), JSON built in SQL) is typed from that shape, unless the map reads the type as a `string`. +A `json` / `jsonb` value with a known shape ([`Json` columns](../guide/schema#typed-json-columns), JSON built in SQL) is typed from that shape, unless the map reads the type as a `string`: then it is that text. This holds on every database: SQLite's maps read JSON as text, and so does a MySQL map with `json: string` for mysql2's `jsonStrings`. A parameter for it takes the same type, where the parameter map allows: on MySQL and MariaDB, whose drivers bind no objects, a JSON parameter takes JSON text or `sql.json(value)`. ## node-postgres @@ -57,7 +57,7 @@ A `json` / `jsonb` value with a known shape ([`Json` columns](../guide/schema Arrays are mapped element by element (`T[]` becomes the element's type `[]`), which is right for every array type `pg-types` registers. Two cases differ at runtime under the default map; exact array entries in a custom map can describe them: - `numeric[]` is parsed with `parseFloat`, so it is `number[]` at runtime but typed `string[]`. -- Array types without a registered parser come back as the raw `{…}` string: `name[]`, `"char"[]`, `xml[]`, `bit[]`, `varbit[]`, `tsvector[]`, `tsquery[]`, `circle[]`, `line[]`, `lseg[]`, `box[]`, `path[]`, `polygon[]`, `int4range[]`, `int8range[]`, `tsrange[]`, `tstzrange[]`, `daterange[]`, `macaddr8[]`, `regclass[]`, `record[]`. +- Array types without a registered parser come back as the raw `{…}` string: `name[]`, `"char"[]`, `xml[]`, `bit[]`, `varbit[]`, `tsvector[]`, `tsquery[]`, `circle[]`, `line[]`, `lseg[]`, `box[]`, `path[]`, `polygon[]`, `int4range[]`, `int8range[]`, `tsrange[]`, `tstzrange[]`, `daterange[]`, `macaddr8[]`, `regclass[]`, `regconfig[]`, `jsonpath[]`, `refcursor[]`, `pg_snapshot[]`, `txid_snapshot[]`, `record[]`. Arrays of user-defined and extension types (enums, domains, `citext`) have no parser in `pg-types` either. `@typesql/pg`, and `@typesql/drizzle` on node-postgres, parse them: the first time a result has such a column, they look up in `pg_type` which of its types are arrays (once per pool or client), then parse the `{…}` text with the element's base type (numbers for `int2`/`int4`/`oid`/`float4`/`float8` domains, booleans for `bool`, strings otherwise). Kysely and Prisma on node-postgres don't expose the result's column types, so there these arrays stay raw `{…}` strings, although they are typed as arrays; cast them in SQL (`moods::text[]`) to get an array. diff --git a/packages/drizzle/src/client.ts b/packages/drizzle/src/client.ts index c11d0deb..7f212574 100644 --- a/packages/drizzle/src/client.ts +++ b/packages/drizzle/src/client.ts @@ -3,6 +3,7 @@ import { type PgField, parsePgArray, parsePgUserArrays, + pgResult, type SqlDriver, } from "@typesql/core/adapter"; import type { DrizzleMysqlDatabase } from "./mysql-client.js"; @@ -118,7 +119,6 @@ type Literal = const isList = (value: Literal): value is readonly Literal[] => Array.isArray(value); -// Array elements written as an array literal, at any depth. const isLiteral = (value: unknown): value is Literal => value === null || typeof value === "string" || @@ -190,7 +190,9 @@ export function executor( return { rows: postgresJsRows(result), rowCount: result.count ?? null }; } const query = client.query!.bind(client); - const result = await query(text, Array.from(values)); + const result = pgResult(await query(text, Array.from(values))) ?? { + rows: [], + }; const rows = await parsePgUserArrays( client, (text, values) => query(text, values), @@ -203,5 +205,6 @@ export function executor( export const driver: SqlDriver = { executor, + // Drizzle nests transactions with savepoints itself. transaction: (db, _depth, fn) => db.transaction(fn), }; diff --git a/packages/drizzle/src/column.ts b/packages/drizzle/src/column.ts new file mode 100644 index 00000000..9ada1ff4 --- /dev/null +++ b/packages/drizzle/src/column.ts @@ -0,0 +1,67 @@ +// What the conversions read of Drizzle's types, in every dialect. + +import type { ToCamelCase, ToSnakeCase } from "./casing.js"; + +export interface FromDrizzleOptions { + readonly casing?: "snake_case" | "camelCase" | undefined; + readonly timestamp?: "timestamp" | "timestamptz" | undefined; + readonly time?: "time" | "timetz" | undefined; +} + +export type ColumnLike = { readonly _: ColumnConfig }; +export type ColumnConfig = { + readonly brand: "Column"; + readonly name: string; + readonly columnType: string; + readonly notNull: boolean; + readonly hasDefault: boolean; + readonly hasRuntimeDefault: boolean; + readonly enumValues: readonly string[] | undefined; + readonly data: unknown; + readonly baseColumn: unknown; +}; + +/** Whether a column has a database default: `$defaultFn()` runs in JavaScript. */ +export type DbDefault = D["hasDefault"] extends true + ? D["hasRuntimeDefault"] extends true + ? false + : true + : false; + +/** Whether a column is generated: it can't be written. */ +export type IsGenerated = D extends { + readonly generated: { readonly type: string }; +} + ? true + : false; + +export type IsAutoIncrement = D extends { readonly isAutoincrement: true } + ? true + : false; + +export type Nullable = C["notNull"] extends true + ? false + : true; + +type Cased = Opts extends { + readonly casing: "snake_case"; +} + ? ToSnakeCase + : Opts extends { readonly casing: "camelCase" } + ? ToCamelCase + : N; + +// Drizzle types an unnamed column's name as its key. Its casing setting renames +// only those; an explicit name that differs from the key is kept as written. +export type ColumnKey = C[K] extends ColumnLike + ? C[K]["_"]["name"] extends K + ? Cased + : C[K]["_"]["name"] + : never; + +type Aliased = { readonly _: { readonly brand: "SQL.Aliased" } }; +export type ViewKey = F[K] extends ColumnLike + ? Cased + : F[K] extends Aliased + ? K + : never; diff --git a/packages/drizzle/src/from-drizzle-mysql.ts b/packages/drizzle/src/from-drizzle-mysql.ts index 1de6e02a..fff3b72a 100644 --- a/packages/drizzle/src/from-drizzle-mysql.ts +++ b/packages/drizzle/src/from-drizzle-mysql.ts @@ -4,12 +4,11 @@ import type { ColumnKey, ColumnLike, DbDefault, - Flat, - FromDrizzleOptions, - TableLike, + IsAutoIncrement, + IsGenerated, + Nullable, ViewKey, - ViewLike, -} from "./from-drizzle.js"; +} from "./column.js"; // Drizzle's types don't record `unsigned` or the text size (`tinytext`, // `mediumtext`, `longtext`); pin those columns with `mysqlType`. @@ -74,75 +73,22 @@ type MysqlTypeOf = D extends { ? Typed<"json", D["data"]> : "unknown"; -type IsGenerated = D extends { - readonly generated: { readonly type: string }; -} - ? true - : false; - -type IsAutoIncrement = D extends { readonly isAutoincrement: true } - ? true - : false; - -type ColOf = Col< - MysqlTypeOf, - { - nullable: D["notNull"] extends true ? false : true; - hasDefault: DbDefault; - autoIncrement: IsAutoIncrement; - generated: IsGenerated; - } ->; - -type Columns = { +export type MysqlColumns = { -readonly [K in keyof C as ColumnKey]: C[K] extends ColumnLike - ? ColOf + ? Col< + MysqlTypeOf, + { + nullable: Nullable; + hasDefault: DbDefault; + autoIncrement: IsAutoIncrement; + generated: IsGenerated; + } + > : never; }; -type ViewColumns = { +export type MysqlViewColumns = { -readonly [K in keyof F as ViewKey]: F[K] extends ColumnLike - ? Col< - MysqlTypeOf, - { nullable: F[K]["_"]["notNull"] extends true ? false : true } - > + ? Col, { nullable: Nullable }> : Col<"unknown", { nullable: true }>; }; - -type Relation = X extends TableLike - ? Columns - : X extends ViewLike - ? { readonly $view: ViewColumns } - : never; - -// Tables of a `mysqlSchema()` live in another database: `db.table`. -type SchemaOf = X extends TableLike - ? Exclude - : never; -type NameIn = X extends TableLike - ? X["_"]["schema"] extends N - ? X["_"]["name"] - : never - : never; -type TopName = X extends TableLike - ? [SchemaOf] extends [never] - ? X["_"]["name"] - : never - : X extends ViewLike - ? X["_"]["name"] - : never; - -type Build = { - -readonly [K in keyof S as TopName]: Relation; -} & { - [N in { [K in keyof S]: SchemaOf }[keyof S]]: { - -readonly [K in keyof S as NameIn]: Relation; - }; -}; - -export type FromDrizzleMysql< - Schema, - Opts extends FromDrizzleOptions = {}, -> = Schema extends TableLike | ViewLike - ? Flat> - : Flat>; diff --git a/packages/drizzle/src/from-drizzle-pg.ts b/packages/drizzle/src/from-drizzle-pg.ts new file mode 100644 index 00000000..e744d9e7 --- /dev/null +++ b/packages/drizzle/src/from-drizzle-pg.ts @@ -0,0 +1,139 @@ +import type { Col } from "@typesql/core/postgres"; +import type { + ColumnConfig, + ColumnKey, + ColumnLike, + DbDefault, + FromDrizzleOptions, + Nullable, + ViewKey, +} from "./column.js"; + +export interface DrizzleColumnTypes { + PgSmallInt: "int2"; + PgInteger: "int4"; + PgBigInt53: "int8"; + PgBigInt64: "int8"; + PgSmallSerial: "int2"; + PgSerial: "int4"; + PgBigSerial53: "int8"; + PgBigSerial64: "int8"; + PgNumeric: "numeric"; + PgNumericNumber: "numeric"; + PgNumericBigInt: "numeric"; + PgReal: "float4"; + PgDoublePrecision: "float8"; + PgBoolean: "bool"; + PgText: "text"; + PgVarchar: "varchar"; + PgChar: "bpchar"; + PgUUID: "uuid"; + PgDate: "date"; + PgDateString: "date"; + PgInterval: "interval"; + PgJson: "json"; + PgJsonb: "jsonb"; + PgInet: "inet"; + PgCidr: "cidr"; + PgMacaddr: "macaddr"; + PgMacaddr8: "macaddr8"; + PgPointTuple: "point"; + PgPointObject: "point"; + PgLine: "line"; + PgLineABC: "line"; + PgGeometry: "geometry"; + PgGeometryObject: "geometry"; + PgVector: "vector"; + PgHalfVector: "halfvec"; + PgSparseVector: "sparsevec"; + PgBinaryVector: "bit"; + PgCustomColumn: "unknown"; +} + +type EnumLike = { + readonly enumName: string; + readonly enumValues: readonly string[]; +}; + +type EnumValues = X extends { + readonly enumValues: readonly [string, ...string[]]; +} + ? X["enumValues"][number] + : X extends () => { readonly _: { readonly data: infer D extends string } } + ? D + : never; + +/** The `pgEnum`s of a schema module, by export name. */ +export type Enums = { + -readonly [K in keyof S as S[K] extends EnumLike ? K : never]: EnumValues< + S[K] + >; +}; + +type LastOf = (U extends unknown ? (x: () => U) => void : never) extends ( + x: infer F, +) => void + ? F extends () => infer L + ? L + : never + : never; + +type EnumName = { + [K in keyof E]: [E[K]] extends [V] ? ([V] extends [E[K]] ? K : never) : never; +}[keyof E] extends infer N + ? [N] extends [never] + ? "text" + : LastOf & string + : never; + +type Opt = Opts extends { + readonly [P in K]: infer V extends string; +} + ? V + : D; + +/** A Drizzle `pg-core` column's PostgreSQL type, with the schema's enums `E`. */ +type PgTypeOf = D extends { + readonly typesqlType: infer T extends string; +} + ? T + : D["columnType"] extends keyof DrizzleColumnTypes + ? DrizzleColumnTypes[D["columnType"]] + : D["columnType"] extends "PgArray" + ? D["baseColumn"] extends ColumnLike + ? `${PgTypeOf}[]` + : "unknown[]" + : D["columnType"] extends "PgTimestamp" | "PgTimestampString" + ? Opt + : D["columnType"] extends "PgTime" + ? Opt + : D["columnType"] extends "PgEnumColumn" + ? EnumName<(D["enumValues"] & readonly string[])[number], E> + : D["columnType"] extends "PgEnumObjectColumn" + ? EnumName + : "unknown"; + +// One of four flag objects, so that columns of a type share one `Col`. +type PgCol = N extends true + ? H extends true + ? Col + : Col + : H extends true + ? Col + : Col; + +export type PgColumns = { + -readonly [K in keyof C as ColumnKey]: C[K] extends ColumnLike + ? PgCol< + PgTypeOf, + Nullable, + DbDefault + > + : never; +}; + +export type PgViewColumns = { + -readonly [K in keyof F as ViewKey]: F[K] extends ColumnLike + ? PgCol, Nullable, false> + : Col<"unknown", { nullable: true }>; +}; diff --git a/packages/drizzle/src/from-drizzle-sqlite.ts b/packages/drizzle/src/from-drizzle-sqlite.ts index afc6c08f..5d2bdf9f 100644 --- a/packages/drizzle/src/from-drizzle-sqlite.ts +++ b/packages/drizzle/src/from-drizzle-sqlite.ts @@ -4,12 +4,10 @@ import type { ColumnKey, ColumnLike, DbDefault, - Flat, - FromDrizzleOptions, - TableLike, + IsGenerated, + Nullable, ViewKey, - ViewLike, -} from "./from-drizzle.js"; +} from "./column.js"; export interface DrizzleSqliteColumnTypes { SQLiteInteger: "integer"; @@ -41,52 +39,21 @@ type SqliteTypeOf = D extends { ? Typed<"json", D["data"]> : "any"; -type IsGenerated = D extends { - readonly generated: { readonly type: string }; -} - ? true - : false; - -type ColOf = Col< - SqliteTypeOf, - { - nullable: D["notNull"] extends true ? false : true; - hasDefault: DbDefault; - generated: IsGenerated; - } ->; - -type Columns = { +export type SqliteColumns = { -readonly [K in keyof C as ColumnKey]: C[K] extends ColumnLike - ? ColOf + ? Col< + SqliteTypeOf, + { + nullable: Nullable; + hasDefault: DbDefault; + generated: IsGenerated; + } + > : never; }; -type ViewColumns = { +export type SqliteViewColumns = { -readonly [K in keyof F as ViewKey]: F[K] extends ColumnLike - ? Col< - SqliteTypeOf, - { nullable: F[K]["_"]["notNull"] extends true ? false : true } - > + ? Col, { nullable: Nullable }> : Col<"any", { nullable: true }>; }; - -type Relation = X extends TableLike - ? Columns - : X extends ViewLike - ? { readonly $view: ViewColumns } - : never; - -type NameOf = X extends TableLike | ViewLike ? X["_"]["name"] : never; - -export type FromDrizzleSqlite< - Schema, - Opts extends FromDrizzleOptions = {}, -> = Schema extends TableLike | ViewLike - ? Flat<{ -readonly [N in NameOf]: Relation }> - : Flat<{ - -readonly [K in keyof Schema as NameOf]: Relation< - Schema[K], - Opts - >; - }>; diff --git a/packages/drizzle/src/from-drizzle.ts b/packages/drizzle/src/from-drizzle.ts index f3900fe5..b58d673f 100644 --- a/packages/drizzle/src/from-drizzle.ts +++ b/packages/drizzle/src/from-drizzle.ts @@ -1,54 +1,29 @@ -import type { Col } from "@typesql/core/postgres"; -import type { ToCamelCase, ToSnakeCase } from "./casing.js"; +import type { FromDrizzleOptions } from "./column.js"; +import type { MysqlColumns, MysqlViewColumns } from "./from-drizzle-mysql.js"; +import type { Enums, PgColumns, PgViewColumns } from "./from-drizzle-pg.js"; +import type { + SqliteColumns, + SqliteViewColumns, +} from "./from-drizzle-sqlite.js"; -export interface FromDrizzleOptions { - readonly casing?: "snake_case" | "camelCase" | undefined; - readonly timestamp?: "timestamp" | "timestamptz" | undefined; - readonly time?: "time" | "timetz" | undefined; -} +export type FromDrizzle< + Schema, + Opts extends FromDrizzleOptions = {}, +> = FromDrizzleOf; -export interface DrizzleColumnTypes { - PgSmallInt: "int2"; - PgInteger: "int4"; - PgBigInt53: "int8"; - PgBigInt64: "int8"; - PgSmallSerial: "int2"; - PgSerial: "int4"; - PgBigSerial53: "int8"; - PgBigSerial64: "int8"; - PgNumeric: "numeric"; - PgNumericNumber: "numeric"; - PgNumericBigInt: "numeric"; - PgReal: "float4"; - PgDoublePrecision: "float8"; - PgBoolean: "bool"; - PgText: "text"; - PgVarchar: "varchar"; - PgChar: "bpchar"; - PgUUID: "uuid"; - PgDate: "date"; - PgDateString: "date"; - PgInterval: "interval"; - PgJson: "json"; - PgJsonb: "jsonb"; - PgInet: "inet"; - PgCidr: "cidr"; - PgMacaddr: "macaddr"; - PgMacaddr8: "macaddr8"; - PgPointTuple: "point"; - PgPointObject: "point"; - PgLine: "line"; - PgLineABC: "line"; - PgGeometry: "geometry"; - PgGeometryObject: "geometry"; - PgVector: "vector"; - PgHalfVector: "halfvec"; - PgSparseVector: "sparsevec"; - PgBinaryVector: "bit"; - PgCustomColumn: "unknown"; -} +export type FromDrizzleMysql< + Schema, + Opts extends FromDrizzleOptions = {}, +> = FromDrizzleOf; -export type TableLike = { +export type FromDrizzleSqlite< + Schema, + Opts extends FromDrizzleOptions = {}, +> = FromDrizzleOf; + +type Dialect = "postgres" | "mysql" | "sqlite"; + +type TableLike = { readonly _: { readonly brand: "Table"; readonly name: string; @@ -56,180 +31,84 @@ export type TableLike = { readonly columns: object; }; }; -export type ViewLike = { +type ViewLike = { readonly _: { readonly brand: "View"; readonly name: string; readonly selectedFields: object; }; }; -type EnumLike = { - readonly enumName: string; - readonly enumValues: readonly string[]; -}; -export type ColumnLike = { readonly _: ColumnConfig }; -export type ColumnConfig = { - readonly brand: "Column"; - readonly name: string; - readonly columnType: string; - readonly notNull: boolean; - readonly hasDefault: boolean; - readonly hasRuntimeDefault: boolean; - readonly enumValues: readonly string[] | undefined; - readonly data: unknown; - readonly baseColumn: unknown; -}; - -type EnumValues = X extends { - readonly enumValues: readonly [string, ...string[]]; -} - ? X["enumValues"][number] - : X extends () => { readonly _: { readonly data: infer D extends string } } - ? D - : never; - -type Enums = { - -readonly [K in keyof S as S[K] extends EnumLike ? K : never]: EnumValues< - S[K] - >; -}; - -type LastOf = (U extends unknown ? (x: () => U) => void : never) extends ( - x: infer F, -) => void - ? F extends () => infer L - ? L - : never - : never; - -type EnumName = { - [K in keyof E]: [E[K]] extends [V] ? ([V] extends [E[K]] ? K : never) : never; -}[keyof E] extends infer N - ? [N] extends [never] - ? "text" - : LastOf & string - : never; - -type Opt = Opts extends { - readonly [P in K]: infer V extends string; -} - ? V - : D; - -type PgTypeOf = D extends { - readonly typesqlType: infer T extends string; -} - ? T - : D["columnType"] extends keyof DrizzleColumnTypes - ? DrizzleColumnTypes[D["columnType"]] - : D["columnType"] extends "PgArray" - ? D["baseColumn"] extends ColumnLike - ? `${PgTypeOf}[]` - : "unknown[]" - : D["columnType"] extends "PgTimestamp" | "PgTimestampString" - ? Opt - : D["columnType"] extends "PgTime" - ? Opt - : D["columnType"] extends "PgEnumColumn" - ? EnumName<(D["enumValues"] & readonly string[])[number], E> - : D["columnType"] extends "PgEnumObjectColumn" - ? EnumName - : "unknown"; - -type Desc = N extends true - ? H extends true - ? Col - : Col - : H extends true - ? Col - : Col; - -export type DbDefault = D["hasDefault"] extends true - ? D["hasRuntimeDefault"] extends true - ? false - : true - : false; - -type ColOf = Desc< - PgTypeOf, - D["notNull"] extends true ? false : true, - DbDefault ->; - -type Cased = Opts extends { - readonly casing: "snake_case"; -} - ? ToSnakeCase - : Opts extends { readonly casing: "camelCase" } - ? ToCamelCase - : N; - -// Drizzle types an unnamed column's name as its key. Its casing setting renames -// only those; an explicit name that differs from the key is kept as written. -export type ColumnKey = C[K] extends ColumnLike - ? C[K]["_"]["name"] extends K - ? Cased - : C[K]["_"]["name"] - : never; - -type Columns = { - -readonly [K in keyof C as ColumnKey]: C[K] extends ColumnLike - ? ColOf - : never; -}; - -type Aliased = { readonly _: { readonly brand: "SQL.Aliased" } }; -export type ViewKey = F[K] extends ColumnLike - ? Cased - : F[K] extends Aliased - ? K - : never; - -type ViewColumns = { - -readonly [K in keyof F as ViewKey]: F[K] extends ColumnLike - ? Desc< - PgTypeOf, - F[K]["_"]["notNull"] extends true ? false : true, - false - > - : Col<"unknown", { nullable: true }>; -}; -type Relation = X extends TableLike - ? Columns +// Each dialect's columns are a mapped type of their own: one whose column +// type depends on the dialect costs more to check. +type Columns = D extends "mysql" + ? MysqlColumns + : D extends "sqlite" + ? SqliteColumns + : PgColumns; + +type ViewColumns = D extends "mysql" + ? MysqlViewColumns + : D extends "sqlite" + ? SqliteViewColumns + : PgViewColumns; + +type Relation = X extends TableLike + ? Columns : X extends ViewLike - ? { readonly $view: ViewColumns } + ? { readonly $view: ViewColumns } : never; -type SchemaOf = X extends TableLike - ? Exclude +// PostgreSQL leaves out the `public` schema. Tables of a `mysqlSchema()` live +// in another database, `db.table`: MySQL has no default schema to leave out. +// SQLite tables have no schema. +type DefaultSchema = D extends "postgres" ? "public" : never; + +/** + * The schema of a table that isn't in the default one: its relations are a + * namespace's, `db.schema.table`. + */ +type SchemaOf = X extends TableLike + ? Exclude : never; type NameIn = X extends TableLike ? X["_"]["schema"] extends N ? X["_"]["name"] : never : never; -type TopName = X extends TableLike - ? [SchemaOf] extends [never] +/** The name of a relation outside every namespace. */ +type TopName = X extends TableLike + ? [SchemaOf] extends [never] ? X["_"]["name"] : never : X extends ViewLike ? X["_"]["name"] : never; -type Build> = { - -readonly [K in keyof S as TopName]: Relation; +type Build = { + -readonly [K in keyof S as TopName>]: Relation< + S[K], + Opts, + D, + E + >; } & { - [N in { [K in keyof S]: SchemaOf }[keyof S]]: { - -readonly [K in keyof S as NameIn]: Relation; + [N in { [K in keyof S]: SchemaOf> }[keyof S]]: { + -readonly [K in keyof S as NameIn]: Relation; }; } & (keyof E extends never ? {} : { $types: E }); -export type Flat = { [K in keyof T]: T[K] } & {}; +type Flat = { [K in keyof T]: T[K] } & {}; -export type FromDrizzle< +type BuildOf = Flat< + Build : Record> +>; + +/** The schema of a Drizzle schema module, or of one table or view, in dialect `D`. */ +type FromDrizzleOf< Schema, - Opts extends FromDrizzleOptions = {}, + Opts extends FromDrizzleOptions, + D extends Dialect, > = Schema extends TableLike | ViewLike - ? Flat> - : Flat>; + ? BuildOf<{ [K in Schema["_"]["name"]]: Schema }, Opts, D> + : BuildOf; diff --git a/packages/drizzle/src/index.ts b/packages/drizzle/src/index.ts index e726fa02..333e9dd8 100644 --- a/packages/drizzle/src/index.ts +++ b/packages/drizzle/src/index.ts @@ -1,4 +1,4 @@ -import { createTransactionalSql } from "@typesql/core/adapter"; +import { createFamilySql, dialectFamily } from "@typesql/core/adapter"; import type { MariaDbDialect, MariaDbTransactionalSql, @@ -24,26 +24,25 @@ import { isMysqlDatabase, isSqliteDatabase, } from "./client.js"; -import type { FromDrizzle, FromDrizzleOptions } from "./from-drizzle.js"; -import type { FromDrizzleMysql } from "./from-drizzle-mysql.js"; -import type { FromDrizzleSqlite } from "./from-drizzle-sqlite.js"; +import type { FromDrizzleOptions } from "./column.js"; +import type { + FromDrizzle, + FromDrizzleMysql, + FromDrizzleSqlite, +} from "./from-drizzle.js"; import { type DrizzleMysqlDatabase, mysqlDriver } from "./mysql-client.js"; import { type DrizzleSqliteDatabase, sqliteDriver } from "./sqlite-client.js"; export { type DrizzleDatabase, executor } from "./client.js"; +export type { FromDrizzleOptions } from "./column.js"; export type { - DrizzleColumnTypes, FromDrizzle, - FromDrizzleOptions, -} from "./from-drizzle.js"; -export type { - DrizzleMysqlColumnTypes, FromDrizzleMysql, -} from "./from-drizzle-mysql.js"; -export type { - DrizzleSqliteColumnTypes, FromDrizzleSqlite, -} from "./from-drizzle-sqlite.js"; +} from "./from-drizzle.js"; +export type { DrizzleMysqlColumnTypes } from "./from-drizzle-mysql.js"; +export type { DrizzleColumnTypes } from "./from-drizzle-pg.js"; +export type { DrizzleSqliteColumnTypes } from "./from-drizzle-sqlite.js"; export { type DrizzleMysqlDatabase, mysqlExecutor } from "./mysql-client.js"; export { mysqlType, pgType, sqliteType } from "./pg-type.js"; export { type DrizzleSqliteDatabase, sqliteExecutor } from "./sqlite-client.js"; @@ -79,11 +78,6 @@ export type DrizzleSchema = D extends { ? S : never; -const notMysql = (dialect: string): Error => - new Error( - `typesql: createSql(${dialect}, db) needs a Drizzle MySQL database (with a session)`, - ); - export function createSql< D extends DrizzleMysqlDatabase & { readonly _: { readonly fullSchema: Record }; @@ -137,24 +131,22 @@ export function createSql( dialect: PostgresDialect | SqliteDialect | MySqlDialect | MariaDbDialect, db?: DrizzleDatabase | DrizzleSqliteDatabase | DrizzleMysqlDatabase, ): object { - switch (dialect.name) { + switch (dialectFamily(dialect.name)) { case "sqlite": if (db === undefined || isSqliteDatabase(db)) - return createTransactionalSql(dialect, db, sqliteDriver); + return createFamilySql(dialect, db, () => sqliteDriver); throw new Error( "typesql: createSql(sqlite, db) needs a Drizzle SQLite database (with all() and run())", ); case "mysql": if (db === undefined || isMysqlDatabase(db)) - return createTransactionalSql(dialect, db, mysqlDriver); - throw notMysql(dialect.name); - case "mariadb": - if (db === undefined || isMysqlDatabase(db)) - return createTransactionalSql(dialect, db, mysqlDriver); - throw notMysql(dialect.name); + return createFamilySql(dialect, db, () => mysqlDriver); + throw new Error( + `typesql: createSql(${dialect.name}, db) needs a Drizzle MySQL database (with a session)`, + ); default: if (db === undefined || isDrizzleDatabase(db)) - return createTransactionalSql(dialect, db, driver); + return createFamilySql(dialect, db, () => driver); throw new Error( "typesql: createSql(postgres, db) needs a Drizzle database (with a session)", ); diff --git a/packages/drizzle/src/mysql-client.ts b/packages/drizzle/src/mysql-client.ts index b79dced0..e7711568 100644 --- a/packages/drizzle/src/mysql-client.ts +++ b/packages/drizzle/src/mysql-client.ts @@ -1,7 +1,9 @@ import type { Executor } from "@typesql/core"; import { - JsonParam, + mysqlBind, + mysqlInline, mysqlPositional, + mysqlResult, type SqlDriver, } from "@typesql/core/adapter"; @@ -31,7 +33,7 @@ interface DriverClient { /** PlanetScale's and TiDB Cloud's serverless clients. */ execute?( text: string, - values: unknown[], + values: null, options: { readonly fullResult: true }, ): Promise; } @@ -54,28 +56,18 @@ function clientOf(db: DrizzleMysqlDatabase): DriverClient { ); } -// `sql.json(…)` becomes JSON text. mysql2 writes a Buffer as a binary -// literal and a plain Uint8Array as an object. -function param(value: V): V | string | Buffer { - if (value instanceof JsonParam) return JSON.stringify(value.value); - return value instanceof Uint8Array && !Buffer.isBuffer(value) - ? Buffer.from(value.buffer, value.byteOffset, value.byteLength) - : value; -} - // Queries go to the driver itself: Drizzle's mysql2 session reads dates as // strings for its own mapping, and the type maps describe the driver's values. export function mysqlExecutor(db: DrizzleMysqlDatabase): Executor { return async (text, values) => { const client = clientOf(db); - const q = mysqlPositional(text, values, param); if (client.query) { + const q = mysqlPositional(text, values, mysqlBind); const [result] = await client.query(q.text, q.values); - return Array.isArray(result) - ? { rows: result, rowCount: result.length } - : { rows: [], rowCount: result.affectedRows ?? null }; + return mysqlResult(result); } - const result = await client.execute!(q.text, q.values, { + // Their own `?` substitution also replaces a `?` inside a string literal. + const result = await client.execute!(mysqlInline(text, values), null, { fullResult: true, }); const rows = result.rows ?? []; diff --git a/packages/kysely/src/client.ts b/packages/kysely/src/client.ts index 302c8dc6..696fee0e 100644 --- a/packages/kysely/src/client.ts +++ b/packages/kysely/src/client.ts @@ -1,8 +1,10 @@ import type { Executor } from "@typesql/core"; import { - JsonParam, - mysqlPositional, - positional, + type DialectFamily, + dialectFamily, + familyStatement, + mysqlBind, + type PositionalStatement, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; @@ -26,30 +28,23 @@ export interface KyselyExecutor { type Compile = ( text: string, values: readonly unknown[], -) => { readonly text: string; readonly values: unknown[] }; - -const numbered: Compile = (text, values) => ({ - text, - values: Array.from(values), -}); - -// `sql.json(…)` becomes JSON text. mysql2 writes a Buffer as a binary -// literal and a plain Uint8Array as an object. -function mysqlParam(value: V): V | string | Buffer { - if (value instanceof JsonParam) return JSON.stringify(value.value); - return value instanceof Uint8Array && !Buffer.isBuffer(value) - ? Buffer.from(value.buffer, value.byteOffset, value.byteLength) - : value; -} - -const mysql: Compile = (text, values) => - mysqlPositional(text, values, mysqlParam); +) => PositionalStatement; + +const compileFor = + (family: DialectFamily): Compile => + (text, values) => + // Kysely's MysqlDialect runs on mysql2, which binds a Buffer, not a Uint8Array. + familyStatement[family]( + text, + values, + family === "mysql" ? mysqlBind : undefined, + ); function run( db: KyselyExecutor, text: string, values: readonly unknown[], - compile: Compile = numbered, + compile: Compile = compileFor("postgres"), ): Promise> { const q = compile(text, values); return db @@ -63,8 +58,6 @@ function rowCount(result: QueryResult): number { : Number(result.numAffectedRows); } -type DialectName = "postgres" | "sqlite" | "mysql" | "mariadb"; - /** * Whether `db`'s dialect adapter is `Adapter`, from this copy of Kysely or * by name from another. @@ -91,8 +84,7 @@ const hasExecutor = ( getExecutor(): { readonly adapter?: unknown }; } => "getExecutor" in db && typeof db.getExecutor === "function"; -/** The dialect `db` runs on, by its adapter: SQLite's, MySQL's or another (PostgreSQL's). */ -const dialectOf = (db: KyselyExecutor): DialectName => +const dialectOf = (db: KyselyExecutor): DialectFamily => hasAdapter(db, SqliteAdapter) ? "sqlite" : hasAdapter(db, MysqlAdapter) @@ -106,45 +98,37 @@ const dialectOf = (db: KyselyExecutor): DialectName => */ export function executor( db: KyselyExecutor, - options: { readonly dialect?: DialectName } = {}, + options: { readonly dialect?: DialectFamily | "mariadb" } = {}, ): Executor { - const dialect = options.dialect ?? dialectOf(db); - const compile = - dialect === "sqlite" - ? positional - : dialect === "mysql" || dialect === "mariadb" - ? mysql - : numbered; + const compile = compileFor( + options.dialect ? dialectFamily(options.dialect) : dialectOf(db), + ); return async (text, values) => { const result = await run(db, text, values, compile); return { rows: result.rows, rowCount: rowCount(result) }; }; } +// A Kysely transaction (`trx`, or one the user passed) gets a savepoint. function transaction( db: KyselyExecutor, - depth: number, + _depth: number, fn: (trx: KyselyExecutor) => Promise, + savepoint: string | undefined, ): Promise { - if (!db.isTransaction) return db.transaction().execute(fn); + if (savepoint === undefined) return db.transaction().execute(fn); return runTransaction( (statement) => run(db, statement, []), () => fn(db), - { savepoint: `typesql_${depth + 1}` }, + { savepoint }, ); } -export const driver: SqlDriver = { - executor: (db) => executor(db), - transaction, -}; - -export const sqliteDriver: SqlDriver = { - executor: (db) => executor(db, { dialect: "sqlite" }), +/** The driver for a `sql` of the dialect family `dialect`. */ +export const driverOf = ( + dialect: DialectFamily, +): SqlDriver => ({ + executor: (db) => executor(db, { dialect }), transaction, -}; - -export const mysqlDriver: SqlDriver = { - executor: (db) => executor(db, { dialect: "mysql" }), - transaction, -}; + inTransaction: (db) => db.isTransaction, +}); diff --git a/packages/kysely/src/from-kysely-mysql.ts b/packages/kysely/src/from-kysely-mysql.ts index 3d561ce4..25bccb71 100644 --- a/packages/kysely/src/from-kysely-mysql.ts +++ b/packages/kysely/src/from-kysely-mysql.ts @@ -1,74 +1,35 @@ -import type { Typed, View } from "@typesql/core/mysql"; import type { - ColumnOverrideOf, - HasDefaultOf, - Hint, + FromKyselyOf, + KyselyDialect, KyselyOptions, KyselyOverrides, - LiteralUnion, - NullableOf, - OverrideType, - Sel, - Sn, - TableOverrides, } from "./from-kysely.js"; +/** + * MySQL's reading of a kysely-codegen schema, which types ENUM columns as + * unions of their values, BIGINT and DECIMAL as strings that insert from + * numbers, and JSON as `unknown`. A `"db.table"` key is a table of another + * database: MySQL has no default schema. + */ +interface MysqlDialect extends KyselyDialect { + readonly name: "mysql"; + readonly defaultSchema: never; + readonly registry: never; + readonly unknown: "json"; + readonly never: "varchar"; + readonly string: "varchar"; + readonly numberString: "decimal"; + readonly bigintString: "bigint"; + readonly literals: "enum"; + readonly number: "int"; + readonly bigint: "bigint"; + readonly boolean: "tinyint"; + readonly date: "datetime"; + readonly bytes: "blob"; +} + export type FromKyselyMysql< DB, Overrides extends KyselyOverrides = {}, Options extends KyselyOptions = {}, -> = { - -readonly [K in keyof DB & string as Sn]-?: TableOverrides< - Overrides, - K - > extends { readonly $view: true } - ? View, Options>> - : Columns, Options>; -}; - -type Columns = { - -readonly [C in keyof Tbl & string as Sn]-?: Column< - Tbl[C], - ColumnOverrideOf - >; -}; - -type Column = { - readonly type: [OverrideType] extends [never] - ? MysqlTypeOf>, Exclude, null | undefined>> - : OverrideType; - readonly nullable: NullableOf; - readonly hasDefault: HasDefaultOf; -}; - -// kysely-codegen types ENUM columns as unions of their values, BIGINT and -// DECIMAL as strings that insert from numbers, and JSON as `unknown`. -type MysqlTypeOf = unknown extends S - ? "json" - : [S] extends [never] - ? "varchar" - : [S] extends [string] - ? string extends S - ? StringKind - : Typed<"enum", Literals & string>> - : [S] extends [number] - ? "int" - : [S] extends [bigint] - ? "bigint" - : [S] extends [boolean] - ? "tinyint" - : [S] extends [Date] - ? "datetime" - : [S] extends [Uint8Array] - ? "blob" - : Typed<"json", S>; - -type Literals = { [K in S]: K }[S]; - -type StringKind = [H] extends [never] - ? "varchar" - : bigint extends H - ? "bigint" - : number extends H - ? "decimal" - : "varchar"; +> = FromKyselyOf; diff --git a/packages/kysely/src/from-kysely-sqlite.ts b/packages/kysely/src/from-kysely-sqlite.ts index bc8cc857..7d4e076c 100644 --- a/packages/kysely/src/from-kysely-sqlite.ts +++ b/packages/kysely/src/from-kysely-sqlite.ts @@ -1,72 +1,33 @@ -import type { Typed, View } from "@typesql/core/sqlite"; import type { - ColumnOverrideOf, - HasDefaultOf, - Hint, + FromKyselyOf, + KyselyDialect, KyselyOptions, KyselyOverrides, - LiteralUnion, - NullableOf, - OverrideType, - Sel, - Sn, - TableOverrides, } from "./from-kysely.js"; +/** + * SQLite's reading of a kysely-codegen schema. A `"schema.table"` key is a + * table of an attached database, except for `main` and `temp`. + */ +interface SqliteDialect extends KyselyDialect { + readonly name: "sqlite"; + readonly defaultSchema: "main" | "temp"; + readonly registry: never; + readonly unknown: "any"; + readonly never: "text"; + readonly string: "text"; + readonly numberString: "numeric"; + readonly bigintString: "integer"; + readonly literals: "text"; + readonly number: "numeric"; + readonly bigint: "integer"; + readonly boolean: "boolean"; + readonly date: "datetime"; + readonly bytes: "blob"; +} + export type FromKyselySqlite< DB, Overrides extends KyselyOverrides = {}, Options extends KyselyOptions = {}, -> = { - -readonly [K in keyof DB & string as Sn]-?: TableOverrides< - Overrides, - K - > extends { readonly $view: true } - ? View, Options>> - : Columns, Options>; -}; - -type Columns = { - -readonly [C in keyof Tbl & string as Sn]-?: Column< - Tbl[C], - ColumnOverrideOf - >; -}; - -type Column = { - readonly type: [OverrideType] extends [never] - ? SqliteTypeOf>, Exclude, null | undefined>> - : OverrideType; - readonly nullable: NullableOf; - readonly hasDefault: HasDefaultOf; -}; - -type SqliteTypeOf = unknown extends S - ? "any" - : [S] extends [never] - ? "text" - : [S] extends [string] - ? string extends S - ? StringKind - : Typed<"text", Literals & string>> - : [S] extends [number] - ? "numeric" - : [S] extends [bigint] - ? "integer" - : [S] extends [boolean] - ? "boolean" - : [S] extends [Date] - ? "datetime" - : [S] extends [Uint8Array] - ? "blob" - : Typed<"json", S>; - -type Literals = { [K in S]: K }[S]; - -type StringKind = [H] extends [never] - ? "text" - : bigint extends H - ? "integer" - : number extends H - ? "numeric" - : "text"; +> = FromKyselyOf; diff --git a/packages/kysely/src/from-kysely.ts b/packages/kysely/src/from-kysely.ts index bd58d3f5..bb924352 100644 --- a/packages/kysely/src/from-kysely.ts +++ b/packages/kysely/src/from-kysely.ts @@ -1,5 +1,8 @@ -import type { View } from "@typesql/core/postgres"; +import type { Col as MysqlCol } from "@typesql/core/mysql"; +import type { Col, View } from "@typesql/core/postgres"; import type { ColDesc, PgScalarName } from "@typesql/core/postgres/engine"; +// MySQL's `Typed` is the same type. +import type { Col as SqliteCol, Typed } from "@typesql/core/sqlite"; export type ColumnOverride = string | Partial; @@ -15,18 +18,7 @@ export type FromKysely< DB, Overrides extends KyselyOverrides = {}, Options extends KyselyOptions = {}, -> = { - -readonly [K in (keyof DB & string) | "$types" as OuterKey< - K, - Options - >]-?: K extends "$types" - ? EnumRegistry - : K extends `public.${string}` - ? TableOf - : K extends `${infer S}.${string}` - ? NamespaceOf - : TableOf; -}; +> = FromKyselyOf; export type SnakeCase = S extends Lowercase @@ -35,17 +27,108 @@ export type SnakeCase = ? SnakeRest, Lowercase> : S; -export type InferColumn = { - readonly type: PgTypeOf< - NonNullable>, - Exclude, null | undefined>, - "enum" - >; - readonly nullable: null extends Sel ? true : false; - readonly hasDefault: HasDefault; +export type InferColumn = Column; + +/** + * How a dialect reads a kysely-codegen schema: the column types it gives + * the TypeScript types kysely-codegen writes, and its schemas. + */ +export interface KyselyDialect { + readonly name: "postgres" | "sqlite" | "mysql"; + /** The schemas whose `"schema.table"` keys name a top-level table. */ + readonly defaultSchema: string; + /** `"$types"` where enum columns name types of the schema. */ + readonly registry: string; + /** `unknown`, which kysely-codegen writes for JSON. */ + readonly unknown: string; + readonly never: string; + readonly string: string; + /** A string that inserts from a number: DECIMAL. */ + readonly numberString: string; + /** A string that inserts from a bigint: BIGINT. */ + readonly bigintString: string; + /** What a union of string literals is `Typed` on: an ENUM. */ + readonly literals: string; + readonly number: string; + readonly bigint: string; + readonly boolean: string; + readonly date: string; + readonly bytes: string; +} + +interface PgDialect extends KyselyDialect { + readonly name: "postgres"; + readonly defaultSchema: "public"; + readonly registry: "$types"; + readonly unknown: "jsonb"; + readonly never: "text"; + readonly string: "text"; + readonly numberString: "numeric"; + readonly bigintString: "int8"; + readonly literals: "text"; + readonly number: "int4"; + readonly bigint: "int8"; + readonly boolean: "bool"; + readonly date: "timestamptz"; + readonly bytes: "bytea"; +} + +/** + * The schema of a kysely-codegen `DB` in dialect `L`: a `"schema.table"` + * key names a table of a namespace, `db.schema.table`, unless the schema is + * the dialect's default one. + */ +export type FromKyselyOf< + DB, + Overrides extends KyselyOverrides, + Options extends KyselyOptions, + L extends KyselyDialect, +> = { + -readonly [K in (keyof DB & string) | L["registry"] as OuterKey< + K, + Options, + L + >]-?: K extends "$types" + ? EnumRegistry + : K extends `${L["defaultSchema"]}.${string}` + ? TableOf + : K extends `${infer S}.${string}` + ? NamespaceOf + : TableOf; }; -export type Sn = Opt extends { readonly camelCase: true } +type FlatTypeOf = unknown extends S + ? L["unknown"] + : [S] extends [never] + ? L["never"] + : [S] extends [string] + ? string extends S + ? StringKind + : Typed & string>> + : [S] extends [number] + ? L["number"] + : [S] extends [bigint] + ? L["bigint"] + : [S] extends [boolean] + ? L["boolean"] + : [S] extends [Date] + ? L["date"] + : [S] extends [Uint8Array] + ? L["bytes"] + : Typed<"json", S>; + +type Literals = { [K in S]: K }[S]; + +/** A string column's type, from what it inserts from. */ +type StringKind = [H] extends [never] + ? L["string"] + : bigint extends H + ? L["bigintString"] + : number extends H + ? L["numberString"] + : L["string"]; + +type Sn = Opt extends { readonly camelCase: true } ? SnakeCase : S; @@ -62,50 +145,79 @@ type SnakeRest< > : Acc; -type OuterKey = K extends "$types" +type OuterKey< + K extends string, + Opt, + L extends KyselyDialect, +> = K extends "$types" ? K - : K extends `public.${infer T}` + : K extends `${L["defaultSchema"]}.${infer T}` ? Sn : K extends `${infer S}.${string}` ? Sn : Sn; -type SqlTableName = K extends `public.${infer T}` - ? Sn - : Sn; +type SqlTableName< + K extends string, + Opt, + L extends KyselyDialect, +> = K extends `${L["defaultSchema"]}.${infer T}` ? Sn : Sn; -type NamespaceOf = { +type NamespaceOf = { -readonly [K in keyof DB & `${S}.${string}` as K extends `${S}.${infer T}` ? Sn - : never]-?: TableOf; + : never]-?: TableOf; }; -export type TableOverrides = K extends keyof O ? O[K] : undefined; -export type ColumnOverrideOf = C extends keyof TO ? TO[C] : undefined; +type TableOverrides = K extends keyof O ? O[K] : undefined; +type ColumnOverrideOf = C extends keyof TO ? TO[C] : undefined; type Registry = O extends { readonly $types: infer R } ? R : {}; -type TableOf = K extends keyof DB +type TableOf< + DB, + K extends string, + O, + Opt, + L extends KyselyDialect, +> = K extends keyof DB ? TableOverrides extends { readonly $view: true } ? View< Columns< DB[K], TableOverrides, - SqlTableName, + SqlTableName, Registry, - Opt + Opt, + L > > : Columns< DB[K], TableOverrides, - SqlTableName, + SqlTableName, Registry, - Opt + Opt, + L > : never; -type Columns = { +// Each dialect's columns are a mapped type of their own: one whose column +// type depends on the dialect costs more to check. +type Columns< + Tbl, + TO, + TName extends string, + Reg, + Opt, + L extends KyselyDialect, +> = L["name"] extends "mysql" + ? MysqlColumns + : L["name"] extends "sqlite" + ? SqliteColumns + : PgColumns; + +type PgColumns = { -readonly [C in keyof Tbl & string as Sn]-?: Column< Tbl[C], ColumnOverrideOf, @@ -114,11 +226,29 @@ type Columns = { >; }; -export type Sel = T extends { readonly __select__: infer S } - ? InnerSel - : T; +type MysqlColumns = { + -readonly [C in keyof Tbl & string as Sn]-?: MysqlCol< + FlatTypeName, L>, + Flags< + NullableOf>, + HasDefaultOf> + > + >; +}; + +type SqliteColumns = { + -readonly [C in keyof Tbl & string as Sn]-?: SqliteCol< + FlatTypeName, L>, + Flags< + NullableOf>, + HasDefaultOf> + > + >; +}; + +type Sel = T extends { readonly __select__: infer S } ? InnerSel : T; type InnerSel = S extends { readonly __select__: infer S2 } ? S2 : S; -export type Hint = T extends { +type Hint = T extends { readonly __select__: infer S; readonly __insert__: infer I; } @@ -137,7 +267,7 @@ type HasDefault = [Extract] extends [ ? true : false; -export type LiteralUnion = [S] extends [string] +type LiteralUnion = [S] extends [string] ? string extends S ? never : S @@ -171,32 +301,24 @@ type ScalarOf = unknown extends S ? "jsonb" : [S] extends [string] ? string extends S - ? StringKind + ? StringKind : E : [S] extends [number] - ? "int4" + ? PgDialect["number"] : [S] extends [bigint] - ? "int8" + ? PgDialect["bigint"] : [S] extends [boolean] - ? "bool" + ? PgDialect["boolean"] : [S] extends [Date] - ? "timestamptz" + ? PgDialect["date"] : [S] extends [Uint8Array] - ? "bytea" + ? PgDialect["bytes"] : [S] extends [readonly unknown[]] ? "jsonb" : [S] extends [object] ? ObjectKind : "jsonb"; -type StringKind = [H] extends [never] - ? "text" - : bigint extends H - ? "int8" - : number extends H - ? "numeric" - : "text"; - type ObjectKind = string extends keyof S ? "jsonb" : "days" extends keyof S @@ -228,7 +350,7 @@ type IsSingle = N extends unknown : false : never; -export type OverrideType = V extends string +type OverrideType = V extends string ? V : V extends { readonly type: infer X extends string } ? X @@ -244,13 +366,30 @@ type ColumnTypeName = [ > : OverrideType; -type Column = { - readonly type: ColumnTypeName; - readonly nullable: NullableOf; - readonly hasDefault: HasDefaultOf; -}; +type FlatTypeName = [OverrideType] extends [ + never, +] + ? FlatTypeOf>, Exclude, null | undefined>, L> + : OverrideType; + +type Column = PgCol< + ColumnTypeName, + NullableOf, + HasDefaultOf +>; + +type PgCol = Col>; + +// One of four flag objects, so that columns of a type share one `Col`. +type Flags = N extends true + ? H extends true + ? { nullable: true; hasDefault: true } + : { nullable: true } + : H extends true + ? { hasDefault: true } + : Record; -export type NullableOf = [V] extends [ +type NullableOf = [V] extends [ { readonly nullable: infer B extends boolean }, ] ? B @@ -258,7 +397,7 @@ export type NullableOf = [V] extends [ ? true : false; -export type HasDefaultOf = [V] extends [ +type HasDefaultOf = [V] extends [ { readonly hasDefault: infer B extends boolean }, ] ? B @@ -293,7 +432,7 @@ type EnumEntries = [K in keyof DB & string]-?: TableEnums< DB[K], TableOverrides, - SqlTableName, + SqlTableName, Registry, Opt >; diff --git a/packages/kysely/src/index.ts b/packages/kysely/src/index.ts index dc3d9f55..478deb46 100644 --- a/packages/kysely/src/index.ts +++ b/packages/kysely/src/index.ts @@ -1,5 +1,5 @@ import type { SqlPlugin } from "@typesql/core"; -import { createTransactionalSql } from "@typesql/core/adapter"; +import { createFamilySql } from "@typesql/core/adapter"; import type { MariaDbDialect, MariaDbTransactionalSql, @@ -19,12 +19,7 @@ import type { DefaultTypeMap as SqliteTypeMap, } from "@typesql/core/sqlite"; import type { ControlledTransaction, Kysely, Transaction } from "kysely"; -import { - driver, - type KyselyExecutor, - mysqlDriver, - sqliteDriver, -} from "./client.js"; +import { driverOf, type KyselyExecutor } from "./client.js"; import type { FromKysely, KyselyOptions, @@ -152,14 +147,5 @@ export function createSql( dialect: PostgresDialect | SqliteDialect | MySqlDialect | MariaDbDialect, db?: KyselyExecutor, ): object { - switch (dialect.name) { - case "sqlite": - return createTransactionalSql(dialect, db, sqliteDriver); - case "mysql": - return createTransactionalSql(dialect, db, mysqlDriver); - case "mariadb": - return createTransactionalSql(dialect, db, mysqlDriver); - default: - return createTransactionalSql(dialect, db, driver); - } + return createFamilySql(dialect, db, driverOf); } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 5c2be208..adf21649 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -14,6 +14,14 @@ }, "author": "İsmail Karslı", "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, "bin": { "typesql-language-server": "./dist/bin.js" }, @@ -30,6 +38,17 @@ "build": "tsrs -p tsconfig.build.json", "typecheck": "tsrs -p tsconfig.json --noEmit" }, + "dependencies": { + "@typesql/core": "workspace:*" + }, + "peerDependencies": { + "typescript": ">=7.1.0-0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, "devDependencies": { "@types/node": "catalog:", "tsrs": "catalog:", diff --git a/packages/language-server/src/call.ts b/packages/language-server/src/call.ts new file mode 100644 index 00000000..b0506310 --- /dev/null +++ b/packages/language-server/src/call.ts @@ -0,0 +1,227 @@ +import type { + CallExpression, + Identifier, + Node, + PropertyAccessExpression, + SourceFile, + TemplateExpression, +} from "typescript/ast"; +import type { Checker, Type } from "typescript/async"; +import { DIALECTS, type Dialect } from "./sql/dialect.js"; + +export interface EditorKinds { + readonly Identifier: number; + readonly CallExpression: number; + readonly PropertyAccessExpression: number; + readonly StringLiteral: number; + readonly NoSubstitutionTemplateLiteral: number; + readonly TemplateExpression: number; +} + +export function editorKinds( + syntaxKind: Readonly>, +): EditorKinds { + const k = (name: keyof EditorKinds): number => { + const v = syntaxKind[name]; + if (typeof v !== "number") + throw new Error(`typesql: SyntaxKind.${name} is missing`); + return v; + }; + return { + Identifier: k("Identifier"), + CallExpression: k("CallExpression"), + PropertyAccessExpression: k("PropertyAccessExpression"), + StringLiteral: k("StringLiteral"), + NoSubstitutionTemplateLiteral: k("NoSubstitutionTemplateLiteral"), + TemplateExpression: k("TemplateExpression"), + }; +} + +interface SqlType { + /** The index of the schema among the type's type arguments. */ + readonly db: number; + /** Whether the first type argument is the engine. */ + readonly generic: boolean; + /** Methods whose first argument is SQL. */ + readonly methods: readonly string[]; +} + +const SQL_METHODS = ["as", "fragment", "prepare", "raw"]; +const sql: SqlType = { db: 0, generic: false, methods: SQL_METHODS }; + +/** The core types whose call takes SQL. */ +const SQL_TYPES: ReadonlyMap = new Map([ + ["Sql", sql], + ["TransactionalSql", sql], + ["MariaDbSql", sql], + ["MariaDbTransactionalSql", sql], + // In core 7.0, createSql(dialect).withPlugin(…) returns SqlApi. + ["SqlApi", { db: 1, generic: true, methods: SQL_METHODS }], + [ + "MigrationContext", + { db: 2, generic: true, methods: [...SQL_METHODS, "ddl", "add"] }, + ], +]); + +const METHODS: ReadonlySet = new Set( + [...SQL_TYPES.values()].flatMap((t) => t.methods), +); + +/** + * The node whose type decides whether `call` takes SQL: the receiver of a + * method that takes SQL, else the callee. + */ +export function calleeOf( + call: CallExpression, + kinds: EditorKinds, +): { readonly node: Node; readonly method: string | null } { + const callee = call.expression; + return isPropertyAccess(callee, kinds) && METHODS.has(callee.name.text) + ? { node: callee.expression, method: callee.name.text } + : { node: callee, method: null }; +} + +/** The core type `t` names, when a call of it, or of its `method`, takes SQL. */ +export async function sqlTypeOf( + t: Type, + method: string | null, + isCorePath: (path: string) => boolean, +): Promise { + if (!t.isTypeReference()) return null; + const sym = await t.getSymbol(); + const entry = sym ? SQL_TYPES.get(sym.name) : undefined; + if ( + !entry || + (method !== null && !entry.methods.includes(method)) || + !sym!.declarations.some((d) => isCorePath(d.path)) + ) + return null; + return entry; +} + +/** The schema and dialect of a type that takes SQL. */ +export async function targetOf( + checker: Checker, + t: Type, + entry: SqlType, +): Promise<{ db: Type; dialect: Dialect } | null> { + if (!t.isTypeReference()) return null; + const db = (await checker.getTypeArguments(t))[entry.db]; + return db ? { db, dialect: await dialectOf(checker, t, entry) } : null; +} + +/** The dialect of each engine, the first type argument of the generic types. */ +const ENGINES = new Map([ + ["PgEngine", "postgres"], + ["SqliteEngine", "sqlite"], + ["MySqlEngine", "mysql"], + ["MariaDbEngine", "mariadb"], +]); + +const isDialect = (name: string): name is Dialect => + Object.hasOwn(DIALECTS, name); + +/** + * The dialect of a core type that takes SQL: its `dialect` property, which + * each dialect's Sql has, or the engine type argument of the generic types + * (SqlApi, MigrationContext). + */ +export async function dialectOf( + checker: Checker, + t: Type, + entry: SqlType, +): Promise { + const own = await checker.getTypeOfPropertyOfType(t, "dialect"); + if (own?.isStringLiteralType() && isDialect(own.value)) return own.value; + if (!entry.generic || !t.isTypeReference()) return "postgres"; + const [engine] = await checker.getTypeArguments(t); + const name = engine ? (await engine.getSymbol())?.name : undefined; + return (name && ENGINES.get(name)) || "postgres"; +} + +export const isCall = (n: Node, kinds: EditorKinds): n is CallExpression => + n.kind === kinds.CallExpression; + +export const isPropertyAccess = ( + n: Node, + kinds: EditorKinds, +): n is PropertyAccessExpression => n.kind === kinds.PropertyAccessExpression; + +export const isIdentifier = (n: Node, kinds: EditorKinds): n is Identifier => + n.kind === kinds.Identifier; + +const isTemplate = (n: Node, kinds: EditorKinds): n is TemplateExpression => + n.kind === kinds.TemplateExpression; + +export const isSqlLiteral = ( + n: Node | undefined, + kinds: EditorKinds, +): n is Node => + n !== undefined && + (n.kind === kinds.StringLiteral || + n.kind === kinds.NoSubstitutionTemplateLiteral || + n.kind === kinds.TemplateExpression); + +/** A SQL literal's source span and text, with each `${…}` hole as `[start, end)`. */ +export interface LiteralText { + readonly start: number; + readonly end: number; + readonly text: string; + readonly holes: [number, number][]; +} + +/** + * The SQL of a string literal, at the same offsets as its source: each + * escape keeps its length, and each `${…}` hole reads as `$0`. + */ +export function textOf( + file: SourceFile, + literal: Node, + kinds: EditorKinds, +): LiteralText { + const src = file.text; + const open = literal.getStart(file); + const quote = src[open]; + const start = open + 1; + let end = literal.end; + if (end - 1 > open && src[end - 1] === quote && !escaped(src, end - 1)) end--; + const holes: [number, number][] = []; + if (isTemplate(literal, kinds)) { + let prev: Node = literal.head; + for (const span of literal.templateSpans) { + holes.push([prev.end - 2, span.literal.getStart(file) + 1]); + prev = span.literal; + } + } + let text = ""; + let i = start; + for (const [s, e] of [...holes, [end, end] satisfies [number, number]]) { + text += escapes(src.slice(i, s)); + if (e > s) text += "$0".padEnd(e - s, " "); + i = e; + } + return { start, end, text, holes }; +} + +function escaped(s: string, i: number): boolean { + let n = 0; + while (s[i - n - 1] === "\\") n++; + return n % 2 === 1; +} + +// Replace each escape with two characters to keep offsets. An escaped quote +// keeps its quote and gets a space outside the SQL quote it opens or closes, +// so `\"Users\"` reads as ` "Users" `, not ` "Users "`. A run of n escaped +// backslashes becomes n spaces and then the n backslashes, so the last +// escapes what follows it in a string that takes backslash escapes. +const escapes = (s: string): string => { + const open = new Set(); + return s.replace(/(?:\\\\)+|\\(.)/gs, (m, c: string | undefined) => { + if (c === undefined) + return " ".repeat(m.length / 2) + "\\".repeat(m.length / 2); + if (c !== '"' && c !== "'" && c !== "`") return " "; + if (open.delete(c)) return `${c} `; + open.add(c); + return ` ${c}`; + }); +}; diff --git a/packages/language-server/src/complete/call.ts b/packages/language-server/src/complete/call.ts deleted file mode 100644 index 9030552a..00000000 --- a/packages/language-server/src/complete/call.ts +++ /dev/null @@ -1,278 +0,0 @@ -import type { - CallExpression, - Identifier, - LineAndCharacter, - Node, - PropertyAccessExpression, - SourceFile, - TemplateExpression, -} from "typescript/ast"; -import type { - Checker, - DocumentIdentifier, - Program, - Type, -} from "typescript/async"; - -export interface EditorKinds { - readonly Identifier: number; - readonly CallExpression: number; - readonly PropertyAccessExpression: number; - readonly StringLiteral: number; - readonly NoSubstitutionTemplateLiteral: number; - readonly TemplateExpression: number; -} - -export interface SqlCall { - readonly file: SourceFile; - readonly offset: number; - readonly start: number; - readonly end: number; - readonly text: string; - /** The TypeScript literal's delimiter: `"`, `'` or a backtick. */ - readonly quote: string; - readonly db: Type; - readonly dialect: Dialect; -} - -export type Dialect = "postgres" | "sqlite" | "mysql" | "mariadb"; - -/** MySQL and MariaDB: backtick identifiers, `#` comments, backslash escapes. */ -export const isMySql = (d: Dialect): boolean => - d === "mysql" || d === "mariadb"; - -const METHODS = new Set(["fragment", "as"]); - -export async function sqlCallAt( - program: Program, - checker: Checker, - kinds: EditorKinds, - document: DocumentIdentifier, - at: number | LineAndCharacter, - isCorePath: (path: string) => boolean, -): Promise { - const file = await program.getSourceFile(document); - if (!file) return null; - const offset = - typeof at === "number" - ? at - : file.getPositionOfLineAndCharacter(at.line, at.character); - const call = callAt(file, offset, kinds); - if (!call) return null; - const literal = call.arguments[0]!; - let parts = textOf(file, literal, kinds); - if ( - offset < parts.start || - offset > parts.end || - parts.holes.some(([s, e]) => offset > s && offset < e) - ) - return null; - - const callee = call.expression; - const sqlType = await checker.getTypeAtLocation( - isPropertyAccess(callee, kinds) && METHODS.has(callee.name.text) - ? callee.expression - : callee, - ); - const target = await sqlTarget(checker, sqlType, isCorePath); - if (!target) return null; - if (isMySql(target.dialect)) - parts = textOf(file, literal, kinds, target.dialect); - return { - file, - offset, - start: parts.start, - end: parts.end, - text: parts.text, - quote: file.text[literal.getStart(file)]!, - ...target, - }; -} - -async function sqlTarget( - checker: Checker, - t: Type, - isCorePath: (path: string) => boolean, -): Promise<{ db: Type; dialect: Dialect } | null> { - if (!t.isTypeReference()) return null; - // core's createSql(dialect).withPlugin(…) returns SqlApi. - const index = (await isCoreRef(t, SQL_TYPES, isCorePath)) - ? 0 - : (await isCoreRef(t, ["SqlApi"], isCorePath)) - ? 1 - : -1; - if (index < 0) return null; - const db = (await checker.getTypeArguments(t))[index]; - return db ? { db, dialect: await dialectOf(checker, t) } : null; -} - -const SQL_TYPES = [ - "Sql", - "TransactionalSql", - "MariaDbSql", - "MariaDbTransactionalSql", -]; - -const DIALECTS: ReadonlySet = new Set([ - "postgres", - "sqlite", - "mysql", - "mariadb", -]); - -const isDialect = (name: string): name is Dialect => DIALECTS.has(name); - -// Shared generic types whose first type argument is the engine or dialect. -const GENERIC = new Set(["SqlApi", "MigrationContext", "Migrations"]); - -const ENGINES = new Map([ - ["PgEngine", "postgres"], - ["SqliteEngine", "sqlite"], - ["MySqlEngine", "mysql"], - ["MariaDbEngine", "mariadb"], - ["Postgres", "postgres"], - ["Sqlite", "sqlite"], - ["MySql", "mysql"], - ["MariaDb", "mariadb"], -]); - -/** - * The dialect of a core type that takes SQL: by its `dialect` property when - * the type names one (each dialect's Sql does), else by its name for MariaDB's - * own types, by its engine or dialect type argument for the shared generic - * types (SqlApi, MigrationContext, Migrations), else by the dialect folder - * that declares it. - */ -export async function dialectOf(checker: Checker, t: Type): Promise { - const own = await checker.getTypeOfPropertyOfType(t, "dialect"); - if (own?.isStringLiteralType() && isDialect(own.value)) return own.value; - const sym = await t.getSymbol(); - if (!sym) return "postgres"; - if (sym.name.startsWith("MariaDb")) return "mariadb"; - if (GENERIC.has(sym.name) && t.isTypeReference()) { - const [first] = await checker.getTypeArguments(t); - const name = first ? (await first.getSymbol())?.name : undefined; - return (name && ENGINES.get(name)) || "postgres"; - } - const path = sym.declarations.map((d) => d.path).join("\n"); - return /[\\/](?:src|dist)[\\/]mysql[\\/]/.test(path) - ? "mysql" - : /[\\/](?:src|dist)[\\/]sqlite[\\/]/.test(path) - ? "sqlite" - : "postgres"; -} - -function callAt( - file: SourceFile, - offset: number, - kinds: EditorKinds, -): CallExpression | null { - let found: CallExpression | null = null; - const visit = (n: Node): void => { - if (offset < n.pos || offset > n.end) return; - if (isCall(n, kinds) && isSqlLiteral(n.arguments[0], kinds)) found = n; - n.forEachChild(visit); - }; - visit(file); - return found; -} - -export const isCall = (n: Node, kinds: EditorKinds): n is CallExpression => - n.kind === kinds.CallExpression; - -export const isPropertyAccess = ( - n: Node, - kinds: EditorKinds, -): n is PropertyAccessExpression => n.kind === kinds.PropertyAccessExpression; - -export const isIdentifier = (n: Node, kinds: EditorKinds): n is Identifier => - n.kind === kinds.Identifier; - -const isTemplate = (n: Node, kinds: EditorKinds): n is TemplateExpression => - n.kind === kinds.TemplateExpression; - -export const isSqlLiteral = ( - n: Node | undefined, - kinds: EditorKinds, -): n is Node => - n !== undefined && - (n.kind === kinds.StringLiteral || - n.kind === kinds.NoSubstitutionTemplateLiteral || - n.kind === kinds.TemplateExpression); - -/** A SQL literal's source span and text, with each `${…}` hole as `[start, end)`. */ -export interface LiteralText { - readonly start: number; - readonly end: number; - readonly text: string; - readonly holes: [number, number][]; -} - -export function textOf( - file: SourceFile, - literal: Node, - kinds: EditorKinds, - dialect: Dialect = "postgres", -): LiteralText { - const src = file.text; - const open = literal.getStart(file); - const quote = src[open]; - const start = open + 1; - let end = literal.end; - if (end - 1 > open && src[end - 1] === quote && !escaped(src, end - 1)) end--; - const holes: [number, number][] = []; - if (isTemplate(literal, kinds)) { - let prev: Node = literal.head; - for (const span of literal.templateSpans) { - holes.push([prev.end - 2, span.literal.getStart(file) + 1]); - prev = span.literal; - } - } - let text = ""; - let i = start; - for (const [s, e] of [...holes, [end, end] satisfies [number, number]]) { - text += escapes(src.slice(i, s), isMySql(dialect)); - if (e > s) text += "$0".padEnd(e - s, " "); - i = e; - } - return { start, end, text, holes }; -} - -function escaped(s: string, i: number): boolean { - let n = 0; - while (s[i - n - 1] === "\\") n++; - return n % 2 === 1; -} - -// Replace each escape with two characters to keep offsets. An escaped quote -// keeps its quote and gets a space outside the SQL quote it opens or closes, -// so `\"Users\"` reads as ` "Users" `, not ` "Users "`. For MySQL, whose -// strings take backslash escapes, a run of n escaped backslashes becomes n -// spaces and then the n backslashes, so the last escapes what follows it. -const escapes = (s: string, backslashes = false): string => { - const open = new Set(); - return s.replace(/(?:\\\\)+|\\(.)/gs, (m, c: string | undefined) => { - if (c === undefined) - return backslashes - ? " ".repeat(m.length / 2) + "\\".repeat(m.length / 2) - : " ".repeat(m.length); - if (c !== '"' && c !== "'" && c !== "`") return " "; - if (open.delete(c)) return `${c} `; - open.add(c); - return ` ${c}`; - }); -}; - -export async function isCoreRef( - type: Type, - names: readonly string[], - isCorePath: (path: string) => boolean, -): Promise { - if (!type.isTypeReference()) return false; - const sym = await type.getSymbol(); - return ( - !!sym && - names.includes(sym.name) && - sym.declarations.some((d) => isCorePath(d.path)) - ); -} diff --git a/packages/language-server/src/complete/complete.ts b/packages/language-server/src/complete/complete.ts index 16bb69a8..07fca52c 100644 --- a/packages/language-server/src/complete/complete.ts +++ b/packages/language-server/src/complete/complete.ts @@ -1,6 +1,15 @@ -import type { LineAndCharacter } from "typescript/ast"; +import type { CallExpression, LineAndCharacter, Node } from "typescript/ast"; import type { Checker, DocumentIdentifier, Program } from "typescript/async"; -import { type Dialect, type EditorKinds, isMySql, sqlCallAt } from "./call.js"; +import { + calleeOf, + type EditorKinds, + isCall, + isSqlLiteral, + sqlTypeOf, + targetOf, + textOf, +} from "../call.js"; +import { DIALECTS, type DialectFacts, quoteName } from "../sql/dialect.js"; import { type Context, contextAt, type RelRef } from "./scan.js"; import { type Schema, schemaOf } from "./schema.js"; @@ -17,10 +26,10 @@ const MAX_ITEMS = 1000; async function completions( schema: Schema, ctx: Context, - dialect: Dialect = "postgres", + d: DialectFacts, ): Promise { const same = (name: string): string => - dialect === "postgres" ? name : name.toLowerCase(); + d.foldsCase ? name.toLowerCase() : name; const e = ctx.expect; const items: CompletionItem[] = []; const add = ( @@ -34,7 +43,7 @@ async function completions( label, kind, detail, - insertText: quoteIdent(label, dialect), + insertText: quoteName(label, d), sortText: sort, }); }; @@ -47,7 +56,9 @@ async function completions( .filter( (r) => (r.schema ?? null) === e.schema || - (e.schema === "public" && r.schema === null), + (r.schema === null && + e.schema !== null && + d.defaultSchemas.includes(e.schema)), ) .map((r) => same(r.name)), ); @@ -99,17 +110,17 @@ async function completions( return items; } -interface Range { - readonly start: T; - readonly end: T; +interface Range { + readonly start: LineAndCharacter; + readonly end: LineAndCharacter; } export interface Completions { readonly items: readonly CompletionItem[]; - readonly replace: Range; - readonly replaceRange: Range; - readonly literal: Range; - readonly literalRange: Range; + /** The text the items replace: the name typed so far. */ + readonly replaceRange: Range; + /** The SQL literal's text, which TypeScript's own items replace. */ + readonly literalRange: Range; } export async function completionsAt( @@ -120,45 +131,72 @@ export async function completionsAt( at: number | LineAndCharacter, isCorePath: (path: string) => boolean, ): Promise { - const call = await sqlCallAt( - program, - checker, - kinds, - document, - at, - isCorePath, - ); + const file = await program.getSourceFile(document); + if (!file) return null; + const offset = + typeof at === "number" + ? at + : file.getPositionOfLineAndCharacter(at.line, at.character); + const call = callAt(file, offset, kinds); if (!call) return null; - const ctx = contextAt(call.text, call.offset - call.start, call.dialect); + const literal = call.arguments[0]!; + const sql = textOf(file, literal, kinds); + if ( + offset < sql.start || + offset > sql.end || + sql.holes.some(([s, e]) => offset > s && offset < e) + ) + return null; + const callee = calleeOf(call, kinds); + const t = await checker.getTypeAtLocation(callee.node); + const entry = await sqlTypeOf(t, callee.method, isCorePath); + const target = entry && (await targetOf(checker, t, entry)); + if (!target) return null; + const d = DIALECTS[target.dialect]; + + const ctx = contextAt(sql.text, offset - sql.start, d); const lc = (o: number): LineAndCharacter => - call.file.getLineAndCharacterOfPosition(o); - let from = call.start + ctx.from; + file.getLineAndCharacterOfPosition(o); + let from = sql.start + ctx.from; // A quoted name typed in a "…" string, or a `…` name in a template literal, // starts with its escaping backslash. - if (from > call.start && call.file.text[from - 1] === "\\") from--; - const replace = { start: from, end: call.offset }; - const literal = { start: call.start, end: call.end }; + if (from > sql.start && file.text[from - 1] === "\\") from--; const ranges = { - replace, - replaceRange: { start: lc(replace.start), end: lc(replace.end) }, - literal, - literalRange: { start: lc(literal.start), end: lc(literal.end) }, + replaceRange: { start: lc(from), end: lc(offset) }, + literalRange: { start: lc(sql.start), end: lc(sql.end) }, }; if (ctx.expect.kind === "none") return { items: [], ...ranges }; const items = await completions( - await schemaOf(checker, call.db, call.dialect), + await schemaOf(checker, target.db, d), ctx, - call.dialect, + d, ); + const quote = file.text[literal.getStart(file)]!; return { items: items.map((i) => ({ ...i, - insertText: escapeFor(call.quote, i.insertText), + insertText: escapeFor(quote, i.insertText), })), ...ranges, }; } +/** The innermost call around `offset` whose first argument is a string. */ +function callAt( + file: Node, + offset: number, + kinds: EditorKinds, +): CallExpression | null { + let found: CallExpression | null = null; + const visit = (n: Node): void => { + if (offset < n.pos || offset > n.end) return; + if (isCall(n, kinds) && isSqlLiteral(n.arguments[0], kinds)) found = n; + n.forEachChild(visit); + }; + visit(file); + return found; +} + /** Escape inserted SQL for the TypeScript string literal it goes into. */ function escapeFor(quote: string, text: string): string { const s = text.replaceAll("\\", "\\\\"); @@ -174,134 +212,3 @@ function dedupe(items: CompletionItem[]): CompletionItem[] { const seen = new Set(); return items.filter((i) => !seen.has(i.label) && !!seen.add(i.label)); } - -const RESERVED = new Set([ - "all", - "analyse", - "analyze", - "and", - "any", - "array", - "as", - "asc", - "asymmetric", - "authorization", - "binary", - "both", - "case", - "cast", - "check", - "collate", - "collation", - "column", - "concurrently", - "constraint", - "create", - "cross", - "current_catalog", - "current_date", - "current_role", - "current_schema", - "current_time", - "current_timestamp", - "current_user", - "default", - "deferrable", - "desc", - "distinct", - "do", - "else", - "end", - "except", - "false", - "fetch", - "for", - "foreign", - "freeze", - "from", - "full", - "grant", - "group", - "having", - "ilike", - "in", - "initially", - "inner", - "intersect", - "into", - "is", - "isnull", - "join", - "lateral", - "leading", - "left", - "like", - "limit", - "localtime", - "localtimestamp", - "natural", - "not", - "notnull", - "null", - "offset", - "on", - "only", - "or", - "order", - "outer", - "overlaps", - "placing", - "primary", - "references", - "returning", - "right", - "select", - "session_user", - "similar", - "some", - "symmetric", - "system_user", - "table", - "tablesample", - "then", - "to", - "trailing", - "true", - "union", - "unique", - "user", - "using", - "variadic", - "verbose", - "when", - "where", - "window", - "with", -]); - -function quoteIdent(name: string, dialect: Dialect = "postgres"): string { - if (isMySql(dialect)) - return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name) && - !MYSQL_RESERVED.has(name.toLowerCase()) - ? name - : `\`${name.replaceAll("`", "``")}\``; - const bare = - dialect === "sqlite" - ? /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name) && - !SQLITE_RESERVED.has(name.toLowerCase()) - : /^[a-z_][a-z0-9_$]*$/.test(name) && !RESERVED.has(name); - return bare ? name : `"${name.replaceAll('"', '""')}"`; -} - -const SQLITE_RESERVED = new Set( - "add all alter and as autoincrement between case check collate commit constraint create default deferrable delete distinct drop else escape except exists foreign from group having in index insert intersect into is isnull join limit not nothing notnull null on or order primary references returning select set table then to transaction union unique update using values when where cross full inner left natural outer right".split( - " ", - ), -); - -// MySQL 8.4's reserved words, and MariaDB's that MySQL lacks. -const MYSQL_RESERVED = new Set( - "accessible add all alter analyze and as asc asensitive before between bigint binary blob both by call cascade case change char character check collate column condition constraint continue convert create cross cube cume_dist current_date current_role current_time current_timestamp current_user cursor database databases day_hour day_microsecond day_minute day_second dec decimal declare default delayed delete delete_domain_id dense_rank desc describe deterministic distinct distinctrow div do_domain_ids double drop dual each else elseif empty enclosed escaped except exists exit explain false fetch first_value float float4 float8 for force foreign from fulltext function general generated get grant group grouping groups having high_priority hour_microsecond hour_minute hour_second if ignore ignore_domain_ids ignore_server_ids in index infile inner inout insensitive insert int int1 int2 int3 int4 int8 integer intersect interval into io_after_gtids io_before_gtids is iterate join json_table key keys kill lag last_value lateral lead leading leave left like limit linear lines load localtime localtimestamp lock long longblob longtext loop low_priority manual master_bind master_heartbeat_period master_ssl_verify_server_cert match maxvalue mediumblob mediumint mediumtext middleint minute_microsecond minute_second mod modifies natural not no_write_to_binlog nth_value ntile null numeric of offset on optimize optimizer_costs option optionally or order out outer outfile over page_checksum parallel parse_vcol_expr partition percent_rank position precision primary procedure purge qualify range rank read read_write reads real recursive ref_system_id references regexp release rename repeat replace require resignal restrict return returning revoke right rlike row row_number rows schema schemas second_microsecond select sensitive separator set show signal slow smallint spatial specific sql sql_big_result sql_calc_found_rows sql_small_result sqlexception sqlstate sqlwarning ssl starting stats_auto_recalc stats_persistent stats_sample_pages stored straight_join system table tablesample terminated then tinyblob tinyint tinytext to trailing trigger true undo union unique unlock unsigned update usage use using utc_date utc_time utc_timestamp values varbinary varchar varcharacter varying virtual when where while window with write xor year_month zerofill".split( - " ", - ), -); diff --git a/packages/language-server/src/complete/index.ts b/packages/language-server/src/complete/index.ts deleted file mode 100644 index 4f4c766d..00000000 --- a/packages/language-server/src/complete/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { EditorKinds } from "./call.js"; - -export type { EditorKinds } from "./call.js"; -export { - type CompletionItem, - type Completions, - completionsAt, -} from "./complete.js"; - -export function editorKinds( - syntaxKind: Readonly>, -): EditorKinds { - const k = (name: string): number => { - const v = syntaxKind[name]; - if (typeof v !== "number") - throw new Error(`typesql: SyntaxKind.${name} is missing`); - return v; - }; - return { - Identifier: k("Identifier"), - CallExpression: k("CallExpression"), - PropertyAccessExpression: k("PropertyAccessExpression"), - StringLiteral: k("StringLiteral"), - NoSubstitutionTemplateLiteral: k("NoSubstitutionTemplateLiteral"), - TemplateExpression: k("TemplateExpression"), - }; -} diff --git a/packages/language-server/src/complete/scan.ts b/packages/language-server/src/complete/scan.ts index 5e8defd2..294a5470 100644 --- a/packages/language-server/src/complete/scan.ts +++ b/packages/language-server/src/complete/scan.ts @@ -1,5 +1,5 @@ -import { blockComment } from "../highlight/lex.js"; -import { type Dialect, isMySql } from "./call.js"; +import type { DialectFacts } from "../sql/dialect.js"; +import { lex } from "../sql/lex.js"; export interface RelRef { readonly schema: string | null; @@ -38,6 +38,7 @@ export interface Context { const NONE: Context["expect"] = { kind: "none" }; +/** A word, a quoted name, a value, a `${…}` hole, or an operator or punctuation. */ interface Tok { readonly k: "w" | "q" | "v" | "x" | "o"; readonly v: string; @@ -45,122 +46,42 @@ interface Tok { readonly e: number; } -/** The tokens before the cursor, and whether the cursor is inside one. */ +/** The tokens, and whether the cursor is inside a comment. */ interface Lexed { readonly toks: Tok[]; readonly inside: boolean; } -const WORD = /[A-Za-z_\u0080-￿][A-Za-z0-9_$\u0080-￿]*/y; -const LINE_COMMENT = /--[^\n\r]*/y; -const STRING = /(?:([eE])|[bBxXnN]|[uU]&)?'/y; -const STRING_REST = /[^']*'?/y; -const ESCAPE_STRING_REST = /(?:[^'\\]|\\.|'')*'?/sy; -const QUOTED = /(?:[uU]&)?"((?:[^"]|"")*)"?/y; -const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-￿][A-Za-z0-9_\u0080-￿]*)?\$/y; -const NUMBER = - /(?:[0-9]|[.$][0-9])[0-9._]*(?:[eE][-+])?[A-Za-z0-9_$\u0080-￿]*/y; -const NAMED_PARAM = /:[A-Za-z_][A-Za-z0-9_$\u0080-￿]*/y; -/** A `${…}` hole, which the call text pads as `$0`: a value or an interpolated fragment. */ -const HOLE = /\$0(?![0-9])/y; -const PUNCTUATION = /::|./sy; -const OPERATOR = /(?:(?!--|\/\*)[+\-*/<>=~!@#%^&|`?])+/y; - -// MySQL and MariaDB: `-- ` comments need a space after the dashes, `#` starts -// a comment, block comments don't nest, strings take '…' or "…" with backslash -// escapes (after an n, x, b or _charset prefix), names are quoted with -// backticks, and @name and @@name are variables. -const MY_LINE_COMMENT = /(?:#|--(?=\s|$))[^\n\r]*/y; -const MY_STRING = /(?:[nNxXbB]|_[A-Za-z0-9_]+)?(['"])/y; -const MY_SINGLE_REST = /(?:[^'\\]|\\.|'')*'?/sy; -const MY_DOUBLE_REST = /(?:[^"\\]|\\.|"")*"?/sy; -const MY_QUOTED = /`((?:[^`]|``)*)`?/y; -const MY_VARIABLE = /@@?(?:[A-Za-z0-9_$.\u0080-￿]+|`(?:[^`]|``)*`?)?/y; -const MY_OPERATOR = /(?:(?!--\s|--$|\/\*)[+\-*/<>=~!%^&|?])+/y; - const fold = (s: string): string => s.replace(/[A-Z]/g, (m) => m.toLowerCase()); -function lex(text: string, offset: number, my: boolean): Lexed { +function tokens(text: string, offset: number, d: DialectFacts): Lexed { const toks: Tok[] = []; let inside = false; - const n = text.length; - let i = 0; - const take = (re: RegExp): RegExpExecArray | null => { - re.lastIndex = i; - const m = re.exec(text); - if (m) i = re.lastIndex; - return m; - }; - const push = (k: Tok["k"], s: number, v = text.slice(s, i)): void => { - toks.push({ k, v, s, e: i }); - }; - const value = (): boolean => { - if (my) { - const string = take(MY_STRING); - if (string) take(string[1] === "'" ? MY_SINGLE_REST : MY_DOUBLE_REST); - return !!( - string || - take(MY_VARIABLE) || - take(NUMBER) || - take(NAMED_PARAM) - ); - } - const string = take(STRING); - if (string) { - take(string[1] ? ESCAPE_STRING_REST : STRING_REST); - return true; - } - const tag = take(DOLLAR_TAG)?.[0]; - if (tag) { - const end = text.indexOf(tag, i); - i = end === -1 ? n : end + tag.length; - return true; - } - return !!(take(NUMBER) || take(NAMED_PARAM)); - }; - const comment = (s: number, open: boolean): void => { - if (s < offset && (offset < i || (open && offset === i))) inside = true; - }; - while (i < n) { - const s = i; - if (/\s/.test(text[i]!)) { - i++; - continue; - } - if (take(my ? MY_LINE_COMMENT : LINE_COMMENT)) { - comment(s, true); - continue; - } - if (my && text.startsWith("/*", i)) { - const end = text.indexOf("*/", i + 2); - i = end === -1 ? n : end + 2; - comment(s, end === -1); - continue; - } - if (text.startsWith("/*", i)) { - const [end, closed] = blockComment(text, i); - i = end; - comment(s, !closed); - continue; - } - if (take(HOLE)) { - push("x", s); - continue; - } - if (value()) { - push("v", s); - continue; - } - const quoted = take(my ? MY_QUOTED : QUOTED); - if (quoted) { - const q = my ? "`" : '"'; - push("q", s, quoted[1]!.replaceAll(q + q, q)); - continue; - } - if (take(WORD)) push("w", s, fold(text.slice(s, i))); - else { - if (!take(my ? MY_OPERATOR : OPERATOR)) take(PUNCTUATION); - push("o", s); + for (const t of lex(text, d.lexis)) { + const { start: s, end: e } = t; + const v = text.slice(s, e); + switch (t.kind) { + case "comment": + if (s < offset && (offset < e || (t.open && offset === e))) + inside = true; + break; + case "word": + toks.push({ k: "w", v: fold(v), s, e }); + break; + case "quoted": + toks.push({ k: "q", v: t.name ?? v, s, e }); + break; + case "hole": + toks.push({ k: "x", v, s, e }); + break; + case "string": + case "number": + case "parameter": + case "variable": + toks.push({ k: "v", v, s, e }); + break; + default: + toks.push({ k: "o", v, s, e }); } } return { toks, inside }; @@ -220,17 +141,6 @@ const NOT_ALIAS = new Set([ "only", "table", ]); -// MySQL words after a table that aren't its alias: index hints, PARTITION, -// STRAIGHT_JOIN, and the modifiers of UPDATE before its table. -const MY_NOT_ALIAS = new Set([ - "use", - "force", - "ignore", - "partition", - "straight_join", - "low_priority", -]); -const MY_BEFORE_TABLE = new Set(["ignore", "low_priority"]); const BEFORE_EXPR = new Set([ "select", "where", @@ -293,20 +203,32 @@ interface LevelEnd { } /** A token that can be a name: quoted, or a word that isn't a keyword after a table. */ -const named = (t: Tok | undefined, my = false): boolean => - !!t && - (t.k === "q" || - (t.k === "w" && !NOT_ALIAS.has(t.v) && !(my && MY_NOT_ALIAS.has(t.v)))); +const named = (t: Tok | undefined, notAlias: (w: string) => boolean): boolean => + !!t && (t.k === "q" || (t.k === "w" && !notAlias(t.v))); + +const commonNotAlias = (w: string): boolean => NOT_ALIAS.has(w); function* levels(level: Level | null): Generator { for (let l = level; l; l = l.parent) yield l; } -function structure(toks: readonly Tok[], my: boolean): Structure { +/** Whether the UPDATE at `i` sets the columns of the INSERT's table. */ +function setsTarget(toks: readonly Tok[], i: number, d: DialectFacts): boolean { + const before = d.updateSetsTarget; + return ( + before.length > 0 && + before.every((w, k) => { + const t = toks[i - before.length + k]; + return t?.k === "w" && t.v === w; + }) + ); +} + +function structure(toks: readonly Tok[], d: DialectFacts): Structure { const notAlias = (w: string): boolean => - NOT_ALIAS.has(w) || (my && MY_NOT_ALIAS.has(w)); + NOT_ALIAS.has(w) || d.notAlias.has(w); const beforeTable = (w: string | null): boolean => - w === "only" || w === "lateral" || (my && MY_BEFORE_TABLE.has(w ?? "")); + w === "only" || w === "lateral" || d.beforeTable.has(w ?? ""); const at: At[] = []; const used: RelRef[] = []; const word = (i: number): string | null => @@ -424,7 +346,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { if (after) { if (w === "as") continue; - if (!aliased && named(t, my) && !is(i + 1, ".")) { + if (!aliased && named(t, notAlias) && !is(i + 1, ".")) { after.name = t.v; aliased = true; continue; @@ -492,7 +414,8 @@ function structure(toks: readonly Tok[], my: boolean): Structure { continue; } } - switch (w) { + const keyword = w !== null && d.joins.has(w) ? "join" : w; + switch (keyword) { case "with": if (clause === "") { clause = "with"; @@ -501,7 +424,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { break; case "select": case "returning": - clause = w; + clause = keyword; lv.outputs = []; out = []; break; @@ -511,12 +434,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { clause = "from"; here.relNext = expectRel = true; break; - case "straight_join": case "join": - if (w === "straight_join" && !my) { - if (out) out.push(t); - break; - } clause = "from"; here.relNext = expectRel = true; break; @@ -527,8 +445,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { here.relNext = expectRel = true; break; case "update": - // MySQL's INSERT … ON DUPLICATE KEY UPDATE sets the target's columns. - if (word(i - 1) === "key" && word(i - 2) === "duplicate") { + if (setsTarget(toks, i, d)) { clause = "set"; break; } @@ -550,7 +467,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { } break; case "conflict": - clause = w; + clause = keyword; if (lv.target?.rel) lv.items.push({ name: "excluded", @@ -586,7 +503,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { case "insert": case "merge": endOutput(); - clause = w; + clause = keyword; break; default: if (out) out.push(t); @@ -603,7 +520,7 @@ function structure(toks: readonly Tok[], my: boolean): Structure { function outputName(toks: readonly Tok[]): string | null { const last = toks.at(-1); if (!last) return null; - if (named(last)) { + if (named(last, commonNotAlias)) { const before = toks.at(-2); if (before?.k !== "o" || before.v === ")" || before.v === ".") return last.v; @@ -616,10 +533,9 @@ function outputName(toks: readonly Tok[]): string | null { export function contextAt( text: string, offset: number, - dialect: Dialect = "postgres", + d: DialectFacts, ): Context { - const my = isMySql(dialect); - const { toks, inside } = lex(text, offset, my); + const { toks, inside } = tokens(text, offset, d); let from = offset; let j = toks.length - 1; while (j >= 0 && toks[j]!.s >= offset) j--; @@ -629,8 +545,8 @@ export function contextAt( j--; } else if (cur && cur.e > offset) return { expect: NONE, from }; if (inside || j < 0) return { expect: NONE, from }; - const { at, used } = structure(toks, my); - return { expect: expectAfter(toks, at, used, j), from }; + const { at, used } = structure(toks, d); + return { expect: expectAfter(toks, at, used, j, d), from }; } function expectAfter( @@ -638,6 +554,7 @@ function expectAfter( at: readonly At[], used: readonly RelRef[], j: number, + d: DialectFacts, ): Expect { const t = toks[j]!; if (t.v === ")") return NONE; @@ -696,12 +613,7 @@ function expectAfter( : NONE; } if (t.k === "w") { - if ( - t.v === "set" || - (t.v === "update" && - toks[j - 1]?.v === "key" && - toks[j - 2]?.v === "duplicate") - ) + if (t.v === "set" || (t.v === "update" && setsTarget(toks, j, d))) return target(); return BEFORE_EXPR.has(t.v) ? scope() : NONE; } diff --git a/packages/language-server/src/complete/schema.ts b/packages/language-server/src/complete/schema.ts index c9c2ca32..763ade24 100644 --- a/packages/language-server/src/complete/schema.ts +++ b/packages/language-server/src/complete/schema.ts @@ -1,5 +1,6 @@ +import { META_KEYS } from "@typesql/core/reserved"; import type { Checker, Type } from "typescript/async"; -import { type Dialect, isMySql } from "./call.js"; +import type { DialectFacts } from "../sql/dialect.js"; import type { RelRef } from "./scan.js"; interface Relation { @@ -20,7 +21,7 @@ export interface Schema { columns(ref: RelRef): Promise; } -const NOT_RELATIONS = new Set(["$types", "$functions", "$flavor"]); +const NOT_RELATIONS: ReadonlySet = new Set(META_KEYS); interface Entry { readonly name: string; @@ -30,14 +31,16 @@ interface Entry { export async function schemaOf( checker: Checker, db: Type, - dialect: Dialect, + d: DialectFacts, ): Promise { - // SQLite and MySQL find names in any case. MySQL reads a qualifier that - // names no schema of DB as the current database, so `app.users` is `users`. - const ci = dialect !== "postgres"; - const mysql = isMySql(dialect); - const defaults = ci ? ["main", "temp"] : ["public"]; - const norm = (name: string): string => (ci ? name.toLowerCase() : name); + const norm = (name: string): string => + d.foldsCase ? name.toLowerCase() : name; + // Whether `schema` names DB's top level. MySQL reads a qualifier that names + // no schema of DB as the current database, so `app.users` is `users`. + const isTopLevel = (schema: string): boolean => + d.qualifierMayBeDatabase + ? !namespaces.has(norm(schema)) + : d.defaultSchemas.includes(norm(schema)); const entries = async (t: Type): Promise => { const props = (await checker.getPropertiesOfType(t)).filter( (p) => !NOT_RELATIONS.has(p.name), @@ -114,30 +117,21 @@ export async function schemaOf( return { namespaces: [...namespaces.keys()], async relations(schema) { - if ( - schema === null || - (!ci && schema === "public") || - (mysql && !namespaces.has(norm(schema))) - ) - return relations; - return (await namespace(norm(schema))).relations; + return schema === null || isTopLevel(schema) + ? relations + : (await namespace(norm(schema))).relations; }, async columns(ref) { const name = norm(ref.name); - if ( - ref.schema === null || - (mysql - ? !namespaces.has(norm(ref.schema)) - : defaults.includes(norm(ref.schema))) - ) { - const t = - relTypes.get(name) ?? - (ref.schema === null && !ci - ? (await namespace("pg_catalog")).types.get(name) - : undefined); + if (ref.schema !== null && !isTopLevel(ref.schema)) { + const t = (await namespace(norm(ref.schema))).types.get(name); return t ? columnsOf(t) : null; } - const t = (await namespace(norm(ref.schema))).types.get(name); + const t = + relTypes.get(name) ?? + (ref.schema === null && d.catalog !== null + ? (await namespace(d.catalog)).types.get(name) + : undefined); return t ? columnsOf(t) : null; }, }; diff --git a/packages/language-server/src/highlight/grammar.ts b/packages/language-server/src/highlight/grammar.ts new file mode 100644 index 00000000..76f415fc --- /dev/null +++ b/packages/language-server/src/highlight/grammar.ts @@ -0,0 +1,70 @@ +import { GRAMMAR_LEXIS } from "../sql/lex.js"; +import { CONSTANTS, KEYWORDS, TYPES } from "../sql/words.js"; + +const NAMES = ["sql", "tx"]; +const SUFFIXES = ["Sql", "SQL", "_sql"]; + +/** + * What the VS Code grammar colors without types: calls of sql, tx and names + * ending in Sql, SQL or _sql, their .fragment() and .as(), and .ddl(), .add() + * and .raw() of a string that starts like a statement, and the words and + * tokens it colors. The extension's build writes these into its grammar, so + * the two can't drift. + */ +export const GRAMMAR = { + /** A regular expression source for the names. */ + name: `(?:${NAMES.join("|")}|[A-Za-z_$][\\w$]*(?:${SUFFIXES.join("|")}))`, + nameMethods: ["fragment", "as"], + statementMethods: ["ddl", "add", "raw"], + statementWords: [ + "alter", + "analyze", + "attach", + "begin", + "call", + "cluster", + "comment", + "commit", + "copy", + "create", + "delete", + "describe", + "detach", + "do", + "drop", + "explain", + "grant", + "import", + "insert", + "listen", + "lock", + "merge", + "notify", + "pragma", + "reassign", + "refresh", + "reindex", + "release", + "rename", + "replace", + "reset", + "revoke", + "rollback", + "savepoint", + "security", + "select", + "set", + "show", + "table", + "truncate", + "unlisten", + "update", + "vacuum", + "values", + "with", + ], + types: TYPES, + keywords: KEYWORDS, + constants: CONSTANTS, + lexis: GRAMMAR_LEXIS, +} as const; diff --git a/packages/language-server/src/highlight/index.ts b/packages/language-server/src/highlight/index.ts index fd83be00..61f8b551 100644 --- a/packages/language-server/src/highlight/index.ts +++ b/packages/language-server/src/highlight/index.ts @@ -6,20 +6,33 @@ import type { Type, } from "typescript/async"; import { - type Dialect, + calleeOf, dialectOf, type EditorKinds, isCall, - isCoreRef, isIdentifier, - isMySql, isPropertyAccess, isSqlLiteral, + sqlTypeOf, textOf, -} from "../complete/call.js"; -import { lex, type TokenKind } from "./lex.js"; +} from "../call.js"; +import { DIALECTS, type Dialect, type DialectFacts } from "../sql/dialect.js"; +import { lex } from "../sql/lex.js"; +import { GRAMMAR } from "./grammar.js"; -export type { TokenKind } from "./lex.js"; +export type TokenKind = + | "keyword" + | "constant" + | "type" + | "function" + | "number" + | "string" + | "escape" + | "comment" + | "operator" + | "parameter" + | "variable" + | "punctuation"; /** A run of one kind on one line, in UTF-16 code units. */ export interface Highlight { @@ -29,31 +42,8 @@ export interface Highlight { readonly kind: TokenKind; } -// Types whose call takes SQL, and methods that take SQL on them. -const CALLABLE = [ - "Sql", - "TransactionalSql", - "MariaDbSql", - "MariaDbTransactionalSql", - "SqlApi", - "MigrationContext", -]; -const METHODS = new Map([ - ["as", CALLABLE], - ["fragment", CALLABLE], - ["prepare", CALLABLE], - ["raw", CALLABLE], - ["ddl", ["MigrationContext"]], - ["add", ["MigrationContext", "Migrations"]], -]); - -// What the VS Code grammar colors without types, so its colors are kept for -// TypeSQL calls and undone for others: calls of sql, tx and names ending in -// Sql, SQL or _sql, their .fragment() and .as(), and .ddl(), .add() and .raw() -// of a string that starts like a statement. -const NAME = /^(?:sql|tx|[A-Za-z_$][\w$]*(?:Sql|SQL|_sql))$/; -const STATEMENT_WORDS = - "alter|analyze|attach|begin|call|cluster|comment|commit|copy|create|delete|describe|detach|do|drop|explain|grant|import|insert|listen|lock|merge|notify|pragma|reassign|refresh|reindex|release|rename|replace|reset|revoke|rollback|savepoint|security|select|set|show|table|truncate|unlisten|update|vacuum|values|with"; +const NAME = new RegExp(`^${GRAMMAR.name}$`); +const STATEMENT_WORDS = GRAMMAR.statementWords.join("|"); const STATEMENT = new RegExp( `^\\s*(?:--|/\\*|\\(|(?:${STATEMENT_WORDS})(?=[\\s;(]|$))`, "i", @@ -64,22 +54,25 @@ const STATEMENT_ANY = new RegExp( "i", ); const MARKER = /\/\*\s*sql\s*\*\//i; +const NAME_METHODS: ReadonlySet = new Set(GRAMMAR.nameMethods); +const STATEMENT_METHODS: ReadonlySet = new Set( + GRAMMAR.statementMethods, +); interface Candidate { readonly literal: Node; - /** The node whose type decides, and the type names that make it SQL. */ + /** The node whose type decides, and the method called on it. */ readonly target: Node; - readonly types: readonly string[]; + readonly method: string | null; readonly grammar: boolean; } /** * Highlights for the first-argument strings of calls in `document`: SQL tokens - * where a TypeSQL call's string isn't colored by the grammar, and `string` - * where the grammar colors another function's string as SQL. MySQL and MariaDB - * strings always get SQL tokens, since the grammar reads PostgreSQL: there - * "…" is a string, not a name, and # starts a comment. Strings after a - * `/* sql *\/` marker are left to the grammar. + * where a TypeSQL call's string isn't colored by the grammar or the grammar + * reads its dialect wrong, and `string` where the grammar colors another + * function's string as SQL. Strings after a `/* sql *\/` marker are left to + * the grammar. */ export async function highlightsIn( program: Program, @@ -95,51 +88,105 @@ export async function highlightsIn( const types = await checker.getTypeAtLocation( candidates.map((c) => c.target), ); - const verdicts = new Map>(); - const isSql = (t: Type, names: readonly string[]): Promise => { - const key = `${t.id} ${names.join()}`; - const known = verdicts.get(key); - if (known) return known; - const v = isCoreRef(t, names, isCorePath); - verdicts.set(key, v); - return v; + const dialects = new Map>(); + const dialectFor = (t: Type, method: string | null) => { + const key = `${t.id} ${method}`; + let d = dialects.get(key); + if (!d) { + d = sqlTypeOf(t, method, isCorePath).then((entry) => + entry ? dialectOf(checker, t, entry) : null, + ); + dialects.set(key, d); + } + return d; }; - const sql = await Promise.all( - candidates.map((c, i) => isSql(types[i]!, c.types)), - ); - const dialectsOf = new Map>(); - const dialects = await Promise.all( - candidates.map((_, i): Promise | Dialect => { - const t = types[i]!; - if (!sql[i]) return "postgres"; - let d = dialectsOf.get(t.id); - if (!d) { - d = dialectOf(checker, t); - dialectsOf.set(t.id, d); - } - return d; - }), + const found = await Promise.all( + candidates.map((c, i) => dialectFor(types[i]!, c.method)), ); const lines = lineStarts(file.text); const out: Highlight[] = []; candidates.forEach((c, i) => { - const dialect = dialects[i]!; - if (sql[i] === c.grammar && !(sql[i] && isMySql(dialect))) return; - const parts = textOf(file, c.literal, kinds, dialect); - const spans: [number, number, TokenKind][] = sql[i] - ? lex(parts.text, dialect).map((t) => [ - parts.start + t.start, - parts.start + t.end, - t.kind, + const dialect = found[i]; + const d = dialect ? DIALECTS[dialect] : null; + if (d ? c.grammar && d.grammarColors : !c.grammar) return; + const sql = textOf(file, c.literal, kinds); + const spans: [number, number, TokenKind][] = d + ? tokensOf(sql.text, d).map(([s, e, kind]) => [ + sql.start + s, + sql.start + e, + kind, ]) - : [[parts.start, parts.end, "string"]]; + : [[sql.start, sql.end, "string"]]; for (const [start, end, kind] of spans) - for (const [s, e] of outside(start, end, parts.holes)) + for (const [s, e] of outside(start, end, sql.holes)) split(file.text, lines, s, e, kind, out); }); return out.sort((a, b) => a.line - b.line || a.character - b.character); } +/** The highlight of each token, with adjacent punctuation joined. */ +function tokensOf( + text: string, + d: DialectFacts, +): [number, number, TokenKind][] { + const out: [number, number, TokenKind][] = []; + const push = (start: number, end: number, kind: TokenKind): void => { + if (end <= start) return; + const last = out.at(-1); + if (kind === "punctuation" && last?.[2] === kind && last[1] === start) + last[1] = end; + else out.push([start, end, kind]); + }; + let afterCast = false; + for (const t of lex(text, d.lexis)) { + const cast = afterCast; + afterCast = t.kind === "cast"; + switch (t.kind) { + case "word": { + const word = text.slice(t.start, t.end).toLowerCase(); + push( + t.start, + t.end, + cast || d.types.has(word) + ? "type" + : CONSTANT.has(word) + ? "constant" + : d.keywords.has(word) + ? "keyword" + : text[t.end] === "(" + ? "function" + : "variable", + ); + break; + } + case "string": { + let from = t.start; + for (const [s, e] of t.escapes ?? []) { + push(from, s, "string"); + push(s, e, "escape"); + from = e; + } + push(from, t.end, "string"); + break; + } + case "quoted": + push(t.start, t.end, "variable"); + break; + case "hole": + push(t.start, t.end, "parameter"); + break; + case "cast": + push(t.start, t.end, "operator"); + break; + default: + push(t.start, t.end, t.kind); + } + } + return out; +} + +const CONSTANT: ReadonlySet = new Set(GRAMMAR.constants); + function collect(file: SourceFile, kinds: EditorKinds): Candidate[] { const out: Candidate[] = []; const visit = (n: Node): void => { @@ -167,7 +214,6 @@ function candidate( const firstLine = body.slice(0, body.search(/\n|$/)); const callee = call.expression; const member = isPropertyAccess(callee, kinds) ? callee : null; - const methodTypes = member ? METHODS.get(member.name.text) : undefined; const nameOf = (n: Node): string | null => isIdentifier(n, kinds) ? n.text @@ -178,30 +224,18 @@ function candidate( const grammar = (name !== null && NAME.test(name)) || (!!member && - (member.name.text === "fragment" || member.name.text === "as") && + NAME_METHODS.has(member.name.text) && NAME.test(nameOf(member.expression) ?? "")) || (!!member && - (member.name.text === "ddl" || - member.name.text === "add" || - member.name.text === "raw") && + STATEMENT_METHODS.has(member.name.text) && (STATEMENT.test(firstLine) || (src[open] === "`" && /^\s*$/.test(firstLine)))); - // A call that the grammar leaves alone needs a type only if it could be a - // TypeSQL call: a method that takes SQL, or a string that starts like a - // statement, past any blank lines. - if (!grammar && !methodTypes && !STATEMENT_ANY.test(body.trimStart())) + const { node, method } = calleeOf(call, kinds); + if (!grammar && method === null && !STATEMENT_ANY.test(body.trimStart())) return null; - return member && methodTypes - ? { - literal, - target: member.expression, - types: methodTypes, - grammar, - } - : { literal, target: callee, types: CALLABLE, grammar }; + return { literal, target: node, method, grammar }; } -// The parts of [start, end) outside the template literal's `${…}` holes. function outside( start: number, end: number, diff --git a/packages/language-server/src/highlight/lex.ts b/packages/language-server/src/highlight/lex.ts deleted file mode 100644 index f805ca6e..00000000 --- a/packages/language-server/src/highlight/lex.ts +++ /dev/null @@ -1,664 +0,0 @@ -import { type Dialect, isMySql } from "../complete/call.js"; - -export type TokenKind = - | "keyword" - | "constant" - | "type" - | "function" - | "number" - | "string" - | "escape" - | "comment" - | "operator" - | "parameter" - | "variable" - | "punctuation"; - -export interface Token { - readonly start: number; - readonly end: number; - readonly kind: TokenKind; -} - -// The word lists and token rules follow the VS Code extension's grammar -// (packages/vscode/syntaxes/sql-injection.tmLanguage.json), so a string colors -// the same whichever of the two colors it. -const TYPES = new Set([ - "bigint", - "bigserial", - "bit", - "bool", - "boolean", - "box", - "bytea", - "char", - "character", - "cidr", - "circle", - "date", - "daterange", - "decimal", - "double", - "float", - "float4", - "float8", - "inet", - "int", - "int2", - "int4", - "int4range", - "int8", - "int8range", - "integer", - "interval", - "json", - "jsonb", - "jsonpath", - "line", - "lseg", - "macaddr", - "macaddr8", - "money", - "numeric", - "numrange", - "path", - "point", - "polygon", - "precision", - "real", - "record", - "regclass", - "regtype", - "serial", - "serial2", - "serial4", - "serial8", - "smallint", - "smallserial", - "text", - "time", - "timestamp", - "timestamptz", - "timetz", - "tsquery", - "tsrange", - "tstzrange", - "tsvector", - "uuid", - "varbit", - "varchar", - "varying", - "void", - "xml", -]); - -const KEYWORDS = new Set([ - "add", - "after", - "all", - "alter", - "always", - "analyze", - "and", - "any", - "array", - "as", - "asc", - "asymmetric", - "at", - "authorization", - "before", - "begin", - "between", - "both", - "by", - "cascade", - "case", - "cast", - "check", - "collate", - "column", - "commit", - "concurrently", - "conflict", - "constraint", - "create", - "cross", - "cube", - "current_catalog", - "current_date", - "current_role", - "current_schema", - "current_time", - "current_timestamp", - "current_user", - "database", - "declare", - "default", - "deferrable", - "deferred", - "delete", - "desc", - "distinct", - "do", - "domain", - "drop", - "each", - "else", - "end", - "enum", - "escape", - "except", - "exclude", - "execute", - "exists", - "explain", - "extension", - "fetch", - "filter", - "first", - "following", - "for", - "foreign", - "from", - "full", - "function", - "generated", - "grant", - "group", - "grouping", - "having", - "identity", - "if", - "ilike", - "immediate", - "in", - "include", - "index", - "initially", - "inner", - "insert", - "intersect", - "into", - "is", - "isnull", - "join", - "key", - "language", - "last", - "lateral", - "leading", - "left", - "like", - "limit", - "localtime", - "localtimestamp", - "lock", - "locked", - "materialized", - "matched", - "merge", - "natural", - "next", - "no", - "not", - "nothing", - "notnull", - "nowait", - "nulls", - "of", - "offset", - "on", - "only", - "or", - "order", - "ordinality", - "outer", - "over", - "overlaps", - "overriding", - "partition", - "placing", - "preceding", - "primary", - "procedure", - "range", - "recursive", - "references", - "release", - "rename", - "replace", - "restrict", - "returning", - "returns", - "revoke", - "right", - "rollback", - "rollup", - "row", - "rows", - "savepoint", - "schema", - "select", - "sequence", - "session_user", - "set", - "sets", - "share", - "similar", - "skip", - "some", - "stored", - "symmetric", - "system_user", - "table", - "tablesample", - "temp", - "temporary", - "then", - "ties", - "to", - "trailing", - "transaction", - "trigger", - "truncate", - "unbounded", - "union", - "unique", - "unlogged", - "update", - "user", - "using", - "values", - "variadic", - "verbose", - "view", - "when", - "where", - "window", - "with", - "within", - "without", - "zone", -]); - -const CONSTANTS = new Set(["true", "false", "null"]); - -// MySQL and MariaDB words on top of the lists above. -const MY_TYPES = new Set([ - "binary", - "blob", - "datetime", - "dec", - "enum", - "fixed", - "geometry", - "geometrycollection", - "inet4", - "inet6", - "linestring", - "longblob", - "longtext", - "mediumblob", - "mediumint", - "mediumtext", - "multilinestring", - "multipoint", - "multipolygon", - "nchar", - "nvarchar", - "signed", - "tinyblob", - "tinyint", - "tinytext", - "unsigned", - "varbinary", - "vector", - "year", - "zerofill", -]); - -const MY_KEYWORDS = new Set([ - "algorithm", - "auto_increment", - "change", - "charset", - "columns", - "comment", - "delayed", - "describe", - "distinctrow", - "div", - "dual", - "duplicate", - "engine", - "force", - "fulltext", - "high_priority", - "ignore", - "invisible", - "keys", - "low_priority", - "member", - "mod", - "mode", - "modify", - "nested", - "quick", - "regexp", - "rlike", - "separator", - "show", - "sounds", - "spatial", - "sql_big_result", - "sql_buffer_result", - "sql_calc_found_rows", - "sql_no_cache", - "sql_small_result", - "straight_join", - "tables", - "unlock", - "use", - "virtual", - "visible", - "xor", -]); - -const WORD = /[A-Za-z_][\w$]*/y; -const NUMBER = - /(?:0[xX][0-9A-Fa-f_]+|0[oO][0-7_]+|0[bB][01_]+|(?:\d[\d_]*(?:\.[\d_]*)?|\.\d[\d_]*)(?:[eE][+-]?\d+)?)(?![\w$])/y; -const OPERATOR = /[-+*/<>=~!@#%^&|?]+/y; -const DOLLAR_TAG = /\$([A-Za-z_]\w*)?\$/y; -const PARAM = /\$\d+/y; -const NAMED = /:[A-Za-z_][\w$]*/y; -const STRING_PREFIX = /(?:[eEbBxXnN]|[uU]&)'/y; -const MY_STRING_PREFIX = /(?:[bBxXnN]|_[A-Za-z0-9_]+)['"]/y; -const MY_VARIABLE = /@@?(?:[A-Za-z0-9_$.]+|`(?:[^`]|``)*`?)?/y; - -const at = (re: RegExp, s: string, i: number): string | null => { - re.lastIndex = i; - return re.exec(s)?.[0] ?? null; -}; - -/** The end of the nested block comment at `i`, and whether it was closed. */ -export function blockComment(s: string, i: number): [number, boolean] { - let depth = 0; - for (let j = i; j < s.length; ) { - if (s.startsWith("/*", j)) { - depth++; - j += 2; - } else if (s.startsWith("*/", j)) { - j += 2; - if (--depth === 0) return [j, true]; - } else j++; - } - return [s.length, false]; -} - -/** Splits SQL text into tokens; whitespace is left out. */ -export function lex(s: string, dialect: Dialect = "postgres"): Token[] { - if (isMySql(dialect)) return lexMySql(s); - const out: { start: number; end: number; kind: TokenKind }[] = []; - const push = (start: number, end: number, kind: TokenKind): void => { - if (end <= start) return; - const last = out[out.length - 1]; - if (kind === "punctuation" && last?.kind === kind && last.end === start) - last.end = end; - else out.push({ start, end, kind }); - }; - let i = 0; - let afterCast = false; - while (i < s.length) { - const c = s[i]!; - if (c === " " || c === "\t" || c === "\n" || c === "\r") { - i++; - continue; - } - const cast = afterCast; - afterCast = false; - if (s.startsWith("--", i) || hashComment(s, i)) { - const nl = s.indexOf("\n", i); - const end = nl < 0 ? s.length : nl; - push(i, end, "comment"); - i = end; - continue; - } - if (s.startsWith("/*", i)) { - const [end] = blockComment(s, i); - push(i, end, "comment"); - i = end; - continue; - } - const prefix = at(STRING_PREFIX, s, i); - if (c === "'" || prefix) { - const end = quoted(s, i + (prefix ? prefix.length : 1), "'"); - // A quote before the end is the first of a doubled quote. - let from = i; - for (let j = i + (prefix ? prefix.length : 1); j < end - 1; j++) - if (s[j] === "'") { - push(from, j, "string"); - push(j, j + 2, "escape"); - from = ++j + 1; - } - push(from, end, "string"); - i = end; - continue; - } - if (c === '"' || c === "`") { - const end = quoted(s, i + 1, c); - push(i, end, "variable"); - i = end; - continue; - } - if (c === "$") { - const param = at(PARAM, s, i); - if (param) { - push(i, i + param.length, "parameter"); - i += param.length; - continue; - } - const tag = at(DOLLAR_TAG, s, i); - if (tag) { - const close = s.indexOf(tag, i + tag.length); - const end = close < 0 ? s.length : close + tag.length; - push(i, end, "string"); - i = end; - continue; - } - } - if (s.startsWith("::", i)) { - push(i, i + 2, "operator"); - i += 2; - afterCast = true; - continue; - } - if (c === ":" && !/[:\w$]/.test(s[i - 1] ?? "")) { - const named = at(NAMED, s, i); - if (named) { - push(i, i + named.length, "parameter"); - i += named.length; - continue; - } - } - if (!/[\w$]/.test(s[i - 1] ?? "")) { - const n = at(NUMBER, s, i); - if (n) { - push(i, i + n.length, "number"); - i += n.length; - continue; - } - } - const word = at(WORD, s, i); - if (word) { - const lower = word.toLowerCase(); - const end = i + word.length; - push( - i, - end, - cast || TYPES.has(lower) - ? "type" - : CONSTANTS.has(lower) - ? "constant" - : KEYWORDS.has(lower) - ? "keyword" - : s[end] === "(" - ? "function" - : "variable", - ); - i = end; - continue; - } - const op = at(OPERATOR, s, i); - if (op) { - push(i, i + op.length, "operator"); - i += op.length; - continue; - } - push(i, i + 1, "punctuation"); - i++; - } - return out; -} - -// A # that starts a line, as the grammar reads MySQL's comments, but not -// PostgreSQL's #>, #>>, #- and ## operators. -const hashComment = (s: string, i: number): boolean => { - if (s[i] !== "#" || /[#>-]/.test(s[i + 1] ?? "")) return false; - const nl = s.lastIndexOf("\n", i - 1); - return nl >= 0 && /^\s*$/.test(s.slice(nl + 1, i)); -}; - -// The end of a quoted run opened before `from`; a doubled quote stays inside. -function quoted(s: string, from: number, q: string): number { - let j = from; - for (;;) { - const k = s.indexOf(q, j); - if (k < 0) return s.length; - if (s[k + 1] !== q) return k + 1; - j = k + 2; - } -} - -/** - * MySQL and MariaDB: `#` and `-- ` comments, block comments that don't nest, - * '…' and "…" strings with backslash escapes, `…` names, @ and @@ variables. - */ -function lexMySql(s: string): Token[] { - const out: { start: number; end: number; kind: TokenKind }[] = []; - const push = (start: number, end: number, kind: TokenKind): void => { - if (end <= start) return; - const last = out[out.length - 1]; - if (kind === "punctuation" && last?.kind === kind && last.end === start) - last.end = end; - else out.push({ start, end, kind }); - }; - let i = 0; - while (i < s.length) { - const c = s[i]!; - if (c === " " || c === "\t" || c === "\n" || c === "\r") { - i++; - continue; - } - if (c === "#" || (s.startsWith("--", i) && /^\s?$/.test(s[i + 2] ?? ""))) { - const nl = s.indexOf("\n", i); - const end = nl < 0 ? s.length : nl; - push(i, end, "comment"); - i = end; - continue; - } - if (s.startsWith("/*", i)) { - const close = s.indexOf("*/", i + 2); - const end = close < 0 ? s.length : close + 2; - push(i, end, "comment"); - i = end; - continue; - } - const prefix = at(MY_STRING_PREFIX, s, i); - if (c === "'" || c === '"' || prefix) { - const open = prefix ? i + prefix.length - 1 : i; - const q = s[open]!; - let from = i; - let j = open + 1; - for (; j < s.length; j++) { - const d = s[j]!; - if (d === "\\" || (d === q && s[j + 1] === q)) { - push(from, j, "string"); - push(j, Math.min(j + 2, s.length), "escape"); - from = ++j + 1; - } else if (d === q) break; - } - const end = Math.min(j + 1, s.length); - push(from, end, "string"); - i = end; - continue; - } - if (c === "`") { - const end = quoted(s, i + 1, "`"); - push(i, end, "variable"); - i = end; - continue; - } - if (c === "@") { - const v = at(MY_VARIABLE, s, i)!; - push(i, i + v.length, "variable"); - i += v.length; - continue; - } - if (c === "$") { - const param = at(PARAM, s, i); - if (param) { - push(i, i + param.length, "parameter"); - i += param.length; - continue; - } - } - if (c === ":" && !/[:\w$]/.test(s[i - 1] ?? "")) { - const named = at(NAMED, s, i); - if (named) { - push(i, i + named.length, "parameter"); - i += named.length; - continue; - } - } - if (!/[\w$]/.test(s[i - 1] ?? "")) { - const n = at(NUMBER, s, i); - if (n) { - push(i, i + n.length, "number"); - i += n.length; - continue; - } - } - const word = at(WORD, s, i); - if (word) { - const lower = word.toLowerCase(); - const end = i + word.length; - push( - i, - end, - TYPES.has(lower) || MY_TYPES.has(lower) - ? "type" - : CONSTANTS.has(lower) - ? "constant" - : KEYWORDS.has(lower) || MY_KEYWORDS.has(lower) - ? "keyword" - : s[end] === "(" - ? "function" - : "variable", - ); - i = end; - continue; - } - const op = at(MY_OPERATOR, s, i); - if (op) { - push(i, i + op.length, "operator"); - i += op.length; - continue; - } - push(i, i + 1, "punctuation"); - i++; - } - return out; -} - -const MY_OPERATOR = /(?:(?!--\s|--$|\/\*)[-+*/<>=~!%^&|?:])+/y; diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index c4649901..60775368 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,14 +1,11 @@ -export type { - CompletionItem, - Completions, -} from "./complete/index.js"; -export { mayBeInSqlLiteral, offsetOf } from "./literal.js"; -export { merge } from "./merge.js"; +export { GRAMMAR } from "./highlight/grammar.js"; +export type { TokenKind } from "./highlight/index.js"; +export { lspItems, merge } from "./merge.js"; export { type Connector, Session, type TypeScriptModules, typescriptModules, - withBudget, + typescriptModulesFrom, } from "./session.js"; export { mergeTokens, type TokenLegend, type TokenNames } from "./tokens.js"; diff --git a/packages/language-server/src/literal.ts b/packages/language-server/src/literal.ts index 79237a95..043fc868 100644 --- a/packages/language-server/src/literal.ts +++ b/packages/language-server/src/literal.ts @@ -1,4 +1,7 @@ -import type { Position } from "./session.js"; +export interface Position { + readonly line: number; + readonly character: number; +} // A call whose first argument is a string, optionally after comments such as // the `/* sql */` marker. diff --git a/packages/language-server/src/merge.ts b/packages/language-server/src/merge.ts index fc782556..d24ccfa2 100644 --- a/packages/language-server/src/merge.ts +++ b/packages/language-server/src/merge.ts @@ -7,7 +7,7 @@ import type { import type { Completions, CompletionItem as SqlItem, -} from "./complete/index.js"; +} from "./complete/complete.js"; type Result = CompletionItem[] | CompletionList | null; @@ -20,6 +20,23 @@ const KINDS = { alias: 6, } satisfies { readonly [K in SqlItem["kind"]]: CompletionItemKind }; +/** The items of `c` as LSP completion items, each replacing the name typed so far. */ +export function lspItems(c: Completions): CompletionItem[] { + const { start, end } = c.replaceRange; + return c.items.map((i) => ({ + label: i.label, + kind: KINDS[i.kind], + detail: i.detail, + sortText: i.sortText, + filterText: i.label, + textEdit: { range: { start, end }, newText: i.insertText }, + })); +} + +/** + * Adds the items of `c` to a completion response, dropping the response's + * items that would replace the SQL literal from its start. + */ export function merge(result: Result, c: Completions | null): Result { if (!c) return result; const server = @@ -38,20 +55,7 @@ export function merge(result: Result, c: Completions | null): Result { range.start.character === start.character ); }); - const ours = c.items.map( - (i): CompletionItem => ({ - label: i.label, - kind: KINDS[i.kind], - detail: i.detail, - sortText: i.sortText, - filterText: i.label, - textEdit: { - range: { start: c.replaceRange.start, end: c.replaceRange.end }, - newText: i.insertText, - }, - }), - ); - const items = [...kept, ...ours]; + const items = [...kept, ...lspItems(c)]; if (result === null) return items.length ? items : null; return Array.isArray(result) ? items : { ...result, items }; } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 27cf42e3..7fda31ae 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -1,16 +1,13 @@ import type { Readable, Writable } from "node:stream"; -import { mayBeInSqlLiteral, offsetOf } from "./literal.js"; +import type { Position } from "./literal.js"; import { merge } from "./merge.js"; import { type Connector, - type Position, + type Log, Session, - typescriptModules, - withBudget, + typescriptModulesFrom, } from "./session.js"; -const BUDGET_MS = 150; - interface Peer { readonly readable: Readable; readonly writable: Writable; @@ -35,50 +32,24 @@ export function start(pipe: string): void { { readable: process.stdin, writable: process.stdout }, { pipe: async () => pipe, - modules: (fileName) => [() => typescriptModules(fileName)], + modules: (fileName) => [() => typescriptModulesFrom(fileName)], }, (m) => process.stderr.write(`typesql: ${m}\n`), ); } -function serve( - client: Peer, - connector: Connector, - log: (message: string) => void, - budget = BUDGET_MS, -): void { +function serve(client: Peer, connector: Connector, log: Log): void { const session = new Session(connector, log); - const answer = async ({ - uri, - position, - text, - // Lua clients drop a nil `result`; a missing one is TypeScript's null. - result = null, - }: CompletionParams): Promise => { - if (!mayBeInSqlLiteral(text, offsetOf(text, position))) return result; - try { - const started = Date.now(); - const ours = await withBudget( - session.complete(uri, position, text), - budget, - ); - if (ours === "late") { - log(`completions took longer than ${budget} ms; skipped`); - return result; - } - if (ours) log(`${ours.items.length} items in ${Date.now() - started} ms`); - return merge(result, ours); - } catch (e) { - log(e instanceof Error ? (e.stack ?? e.message) : String(e)); - return result; - } - }; read(client.readable, (m) => { const id = m.id; if (id === undefined) return; - void answer(m.params).then((result) => - client.writable.write(frame({ id, result })), - ); + // Lua clients drop a nil `result`; a missing one is TypeScript's null. + const { uri, position, text, result = null } = m.params; + void session + .complete(uri, position, text) + .then((ours) => + client.writable.write(frame({ id, result: merge(result, ours) })), + ); }); client.readable.on("end", () => { void session.close(); diff --git a/packages/language-server/src/session.ts b/packages/language-server/src/session.ts index e749fdb0..f9f19751 100644 --- a/packages/language-server/src/session.ts +++ b/packages/language-server/src/session.ts @@ -3,14 +3,12 @@ import { createRequire } from "node:module"; import { dirname } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { API, Snapshot } from "typescript/async"; -import type { EditorKinds } from "./complete/call.js"; -import { - type Completions, - completionsAt, - editorKinds, -} from "./complete/index.js"; +import type { TypeScriptModuleLoader } from "typescript/vscode"; +import { type EditorKinds, editorKinds } from "./call.js"; +import { type Completions, completionsAt } from "./complete/complete.js"; import { CorePaths } from "./core.js"; import { type Highlight, highlightsIn } from "./highlight/index.js"; +import { mayBeInSqlLiteral, offsetOf, type Position } from "./literal.js"; export interface TypeScriptModules { readonly API: typeof API; @@ -19,71 +17,230 @@ export interface TypeScriptModules { readonly version: string; } -export interface Connector { - pipe(): Promise; - modules(fileName: string): readonly (() => Promise)[]; - readonly upgrade?: string; -} - +/** The API modules `loader` imports, from the TypeScript package at `dir`. */ export async function typescriptModules( - from: string, + loader: TypeScriptModuleLoader, + dir: string, + version: string, ): Promise { - const req = createRequire(from); - const manifest = req.resolve("typescript/package.json"); - const load = async (spec: string) => - import(pathToFileURL(req.resolve(spec)).href); - const pkg: { version?: string } = req(manifest); - let api: typeof import("typescript/async"); - let ast: typeof import("typescript/ast"); try { - [api, ast] = await Promise.all([ - load("typescript/async"), - load("typescript/ast"), + const [api, ast] = await Promise.all([ + loader.importModule("typescript/async"), + loader.importModule("typescript/ast"), ]); + return { API: api.API, SyntaxKind: ast.SyntaxKind, dir, version }; } catch (e) { throw new Error( - `typescript ${pkg.version ?? "unknown"} at ${realpathSync(dirname(manifest))} has no API client (completions need TypeScript 7.1)`, + `typescript ${version} at ${dir} has no API client (completions need TypeScript 7.1)`, { cause: e }, ); } - return { - API: api.API, - SyntaxKind: ast.SyntaxKind, - dir: realpathSync(dirname(manifest)), - version: pkg.version ?? "unknown", - }; } -export interface Position { - readonly line: number; - readonly character: number; +/** The API modules of the TypeScript package that `from` resolves. */ +export async function typescriptModulesFrom( + from: string, +): Promise { + const req = createRequire(from); + const manifest = req.resolve("typescript/package.json"); + const pkg: { version?: string } = req(manifest); + return typescriptModules( + { importModule: (spec) => import(pathToFileURL(req.resolve(spec)).href) }, + realpathSync(dirname(manifest)), + pkg.version ?? "unknown", + ); +} + +export interface Connector { + pipe(): Promise; + modules(fileName: string): readonly (() => Promise)[]; + readonly upgrade?: string; } +export type Log = ( + message: string, + level?: "trace" | "info" | "warn" | "error", +) => void; + interface Connection { readonly api: API; readonly kinds: EditorKinds; } +/** What one request needs: the connection, and which files are @typesql/core's. */ +interface Request extends Connection { + readonly isCorePath: (path: string) => boolean; +} + +const COMPLETION_BUDGET_MS = 150; +const HIGHLIGHT_BUDGET_MS = 500; + const IS_7_0 = /unknown API method "getCurrentLanguageServerSnapshot"/; export class Session { private connection: Promise | undefined; private readonly cores: CorePaths; private readonly connector: Connector; - private readonly log: (message: string) => void; + private readonly log: Log; - constructor(connector: Connector, log: (message: string) => void) { + constructor(connector: Connector, log: Log) { this.connector = connector; this.log = log; this.cores = new CorePaths(log); } - private connect(uri: string, fileName: string): Promise { + /** + * Tables and columns at `position` of `uri`, whose text is `text` when the + * editor has it. Null when there are none, or when they fail or take longer + * than the budget, which is logged. + */ + complete( + uri: string, + position: Position, + text?: string, + ): Promise { + if ( + text !== undefined && + !mayBeInSqlLiteral(text, offsetOf(text, position)) + ) + return Promise.resolve(null); + return this.within( + "completions", + COMPLETION_BUDGET_MS, + async () => { + const r = await this.request(uri); + return r && this.completeIn(r, uri, position, text); + }, + (c) => `${c.items.length} items`, + ); + } + + /** + * SQL highlights for `uri` as the language server sees it. Null when the + * file can't reach @typesql/core or has no project, or as `complete`. + */ + highlight(uri: string): Promise { + return this.within( + "SQL highlighting", + HIGHLIGHT_BUDGET_MS, + async () => { + const r = await this.request(uri); + if (!r) return null; + return this.withSnapshot(r.api, async (s) => { + const project = await s.getDefaultProjectForFile({ uri }); + if (!project) return null; + return highlightsIn( + project.program, + project.checker, + r.kinds, + { uri }, + r.isCorePath, + ); + }); + }, + (h) => `${h.length} SQL highlights`, + ); + } + + async close(): Promise { + const c = this.connection; + this.connection = undefined; + const connection = await c?.catch(() => undefined); + await connection?.api.close().catch(() => {}); + } + + private async within( + what: string, + ms: number, + run: () => Promise, + count: (result: T) => string, + ): Promise { + let timer: NodeJS.Timeout | undefined; + const late = new Promise<"late">((resolve) => { + timer = setTimeout(() => resolve("late"), ms); + }); + try { + const started = Date.now(); + const result = await Promise.race([run(), late]); + if (result === "late") { + this.log(`${what} took longer than ${ms} ms; skipped`, "warn"); + return null; + } + if (result !== null) + this.log(`${count(result)} in ${Date.now() - started} ms`, "trace"); + return result; + } catch (e) { + this.log( + e instanceof Error ? (e.stack ?? e.message) : String(e), + "error", + ); + return null; + } finally { + clearTimeout(timer); + } + } + + private async request(uri: string): Promise { + if (!uri.startsWith("file:")) return null; + const fileName = fileURLToPath(uri); + const isCorePath = this.cores.for(fileName); + if (!isCorePath) return null; this.connection ??= this.openOrForget(uri, fileName); - return this.connection; + const connection = await this.connection; + return connection && { ...connection, isCorePath }; + } + + private async completeIn( + r: Request, + uri: string, + position: Position, + text: string | undefined, + ): Promise { + const run = async (s: Snapshot): Promise => { + const project = await s.getDefaultProjectForFile({ uri }); + if (!project) return null; + return completionsAt( + project.program, + project.checker, + r.kinds, + { uri }, + position, + r.isCorePath, + ); + }; + return this.withSnapshot(r.api, async (snapshot) => { + if (text !== undefined) { + const project = await snapshot.getDefaultProjectForFile({ uri }); + const current = await project?.program.getSourceFile({ uri }); + if (current && current.text !== text) { + let result: Completions | null = null; + await r.api.runWithTemporaryFileUpdate( + snapshot, + { uri }, + text, + async (s) => { + result = await run(s); + }, + ); + return result; + } + } + return run(snapshot); + }); + } + + private async withSnapshot( + api: API, + run: (s: Snapshot) => Promise, + ): Promise { + const snapshot = await api.getCurrentLanguageServerSnapshot(); + try { + return await run(snapshot); + } finally { + void snapshot.dispose(); + } } - /** Opens a connection; a failed attempt is forgotten so the next call retries. */ private async openOrForget( uri: string, fileName: string, @@ -118,17 +275,14 @@ export class Session { pipe: await this.connector.pipe(), }); try { - const snapshot = await api.getCurrentLanguageServerSnapshot(); - try { + await this.withSnapshot(api, async (snapshot) => { const project = await snapshot.getDefaultProjectForFile({ uri }); if (project) await Promise.all([ project.program.getSourceFile({ uri }), project.checker.getStringType(), ]); - } finally { - void snapshot.dispose(); - } + }); this.log( `API session open (client: typescript ${ts.version} at ${ts.dir})`, ); @@ -151,94 +305,4 @@ export class Session { ); return null; } - - async complete( - uri: string, - position: Position, - text?: string, - ): Promise { - if (!uri.startsWith("file:")) return null; - const fileName = fileURLToPath(uri); - const isCorePath = this.cores.for(fileName); - if (!isCorePath) return null; - const connection = await this.connect(uri, fileName); - if (!connection) return null; - const { api, kinds } = connection; - const run = async (s: Snapshot): Promise => { - const project = await s.getDefaultProjectForFile({ uri }); - if (!project) return null; - return completionsAt( - project.program, - project.checker, - kinds, - { uri }, - position, - isCorePath, - ); - }; - const snapshot = await api.getCurrentLanguageServerSnapshot(); - try { - if (text !== undefined) { - const project = await snapshot.getDefaultProjectForFile({ uri }); - const current = await project?.program.getSourceFile({ uri }); - if (current && current.text !== text) { - let result: Completions | null = null; - await api.runWithTemporaryFileUpdate( - snapshot, - { uri }, - text, - async (s) => { - result = await run(s); - }, - ); - return result; - } - } - return await run(snapshot); - } finally { - void snapshot.dispose(); - } - } - - /** - * SQL highlights for `uri` as the language server sees it, or null when the - * file can't reach @typesql/core or has no project. - */ - async highlight(uri: string): Promise { - if (!uri.startsWith("file:")) return null; - const fileName = fileURLToPath(uri); - const isCorePath = this.cores.for(fileName); - if (!isCorePath) return null; - const connection = await this.connect(uri, fileName); - if (!connection) return null; - const snapshot = await connection.api.getCurrentLanguageServerSnapshot(); - try { - const project = await snapshot.getDefaultProjectForFile({ uri }); - if (!project) return null; - return await highlightsIn( - project.program, - project.checker, - connection.kinds, - { uri }, - isCorePath, - ); - } finally { - void snapshot.dispose(); - } - } - - async close(): Promise { - const c = this.connection; - this.connection = undefined; - const connection = await c?.catch(() => undefined); - await connection?.api.close().catch(() => {}); - } -} - -export function withBudget(p: Promise, ms: number): Promise { - let timer: NodeJS.Timeout | undefined; - const late = new Promise<"late">( - (resolve) => (timer = setTimeout(() => resolve("late"), ms)), - ); - return Promise.race([p, late]).finally(() => clearTimeout(timer)); } diff --git a/packages/language-server/src/sql/dialect.ts b/packages/language-server/src/sql/dialect.ts new file mode 100644 index 00000000..2998e495 --- /dev/null +++ b/packages/language-server/src/sql/dialect.ts @@ -0,0 +1,111 @@ +import { RESERVED_WORDS } from "@typesql/core/reserved"; +import { LEXIS, type Lexis } from "./lex.js"; +import { KEYWORDS, MY_KEYWORDS, MY_TYPES, TYPES } from "./words.js"; + +export type Dialect = "postgres" | "sqlite" | "mysql" | "mariadb"; + +/** What the editor tooling needs to know of a dialect. */ +export interface DialectFacts { + readonly lexis: Lexis; + /** Whether names match in any case. */ + readonly foldsCase: boolean; + /** The quote of a name, and the names that need none. */ + readonly quote: string; + readonly bare: RegExp; + readonly reserved: ReadonlySet; + /** Schemas that name DB's top level. */ + readonly defaultSchemas: readonly string[]; + /** Whether a qualifier that names no schema of DB names the current database. */ + readonly qualifierMayBeDatabase: boolean; + /** The schema that unqualified names fall back to, after DB's top level. */ + readonly catalog: string | null; + /** Words that read as types and keywords. */ + readonly types: ReadonlySet; + readonly keywords: ReadonlySet; + /** Whether the VS Code grammar, which reads PostgreSQL, colors its strings right. */ + readonly grammarColors: boolean; + /** Words after a table that aren't its alias, beyond the common ones. */ + readonly notAlias: ReadonlySet; + /** Words between FROM, JOIN or UPDATE and the table, beyond ONLY and LATERAL. */ + readonly beforeTable: ReadonlySet; + /** Words that join a table, beyond JOIN. */ + readonly joins: ReadonlySet; + /** The words before an UPDATE that sets the INSERT's columns: MySQL's ON DUPLICATE KEY UPDATE. */ + readonly updateSetsTarget: readonly string[]; +} + +const NONE: ReadonlySet = new Set(); + +const postgres: DialectFacts = { + lexis: LEXIS.postgres, + foldsCase: false, + quote: '"', + bare: /^[a-z_][a-z0-9_$]*$/, + reserved: new Set(RESERVED_WORDS.postgres), + defaultSchemas: ["public"], + qualifierMayBeDatabase: false, + catalog: "pg_catalog", + types: new Set(TYPES), + keywords: new Set(KEYWORDS), + grammarColors: true, + notAlias: NONE, + beforeTable: NONE, + joins: NONE, + updateSetsTarget: [], +}; + +// SQLite: the grammar reads [name] and `name` as PostgreSQL would not, and +// misses ?, @name and $name parameters. +const sqlite: DialectFacts = { + ...postgres, + lexis: LEXIS.sqlite, + grammarColors: false, + foldsCase: true, + bare: /^[A-Za-z_][A-Za-z0-9_$]*$/, + reserved: new Set(RESERVED_WORDS.sqlite), + defaultSchemas: ["main", "temp"], + catalog: null, +}; + +// MySQL and MariaDB: there "…" is a string, not a name, and # starts a +// comment, so the grammar's colors are wrong. +const mysql: DialectFacts = { + lexis: LEXIS.mysql, + foldsCase: true, + quote: "`", + bare: /^[A-Za-z_][A-Za-z0-9_$]*$/, + reserved: new Set(RESERVED_WORDS.mysql), + defaultSchemas: [], + qualifierMayBeDatabase: true, + catalog: null, + types: new Set([...TYPES, ...MY_TYPES]), + keywords: new Set([...KEYWORDS, ...MY_KEYWORDS]), + grammarColors: false, + // Index hints, PARTITION, STRAIGHT_JOIN, and UPDATE's modifiers. + notAlias: new Set([ + "use", + "force", + "ignore", + "partition", + "straight_join", + "low_priority", + ]), + beforeTable: new Set(["ignore", "low_priority"]), + joins: new Set(["straight_join"]), + updateSetsTarget: ["duplicate", "key"], +}; + +export const DIALECTS: Readonly> = { + postgres, + sqlite, + mysql, + mariadb: mysql, +}; + +/** `name` as the dialect writes it: bare when it can be, else quoted. */ +export function quoteName(name: string, d: DialectFacts): string { + const word = d.foldsCase ? name.toLowerCase() : name; + return d.bare.test(name) && !d.reserved.has(word) + ? name + : d.quote + name.replaceAll(d.quote, d.quote + d.quote) + d.quote; +} diff --git a/packages/language-server/src/sql/lex.ts b/packages/language-server/src/sql/lex.ts new file mode 100644 index 00000000..e15f1fcc --- /dev/null +++ b/packages/language-server/src/sql/lex.ts @@ -0,0 +1,278 @@ +/** How a dialect splits SQL text into tokens. */ +export interface Lexis { + readonly lineComment: RegExp; + readonly nestedComments: boolean; + /** A string's opening quote and any prefix; group 1 is the quote. */ + readonly string: RegExp; + /** Whether a backslash escapes the next character in the string `prefix` opens. */ + readonly backslashes: (prefix: string) => boolean; + /** A quoted name's opening, and the character that closes each opening quote. */ + readonly quotedName: RegExp; + readonly closes: Readonly>; + /** PostgreSQL's $tag$…$tag$ strings. */ + readonly dollarStrings: boolean; + /** PostgreSQL's `::` cast. */ + readonly cast: boolean; + /** Parameters besides `:name`. */ + readonly parameter: RegExp; + readonly variable: RegExp | null; + readonly operator: RegExp; +} + +type Lexeme = + | "word" + | "quoted" + | "string" + | "comment" + | "number" + | "parameter" + | "variable" + /** A `${…}` hole, which the literal's text pads as `$0`. */ + | "hole" + | "cast" + | "operator" + | "punctuation"; + +export interface Token { + readonly kind: Lexeme; + readonly start: number; + readonly end: number; + /** A comment, string or quoted name with no closing delimiter, so its end is still inside it. */ + readonly open: boolean; + /** A string's escapes, a doubled quote or a backslash and what it escapes, as [start, end). */ + readonly escapes?: readonly (readonly [number, number])[]; + /** A quoted name's name. */ + readonly name?: string; +} + +const NON_ASCII = "\\u0080-\\uffff"; +const NAME_START = `A-Za-z_${NON_ASCII}`; +const NAME_PART = `\\w$${NON_ASCII}`; +const NAME = `[${NAME_START}][${NAME_PART}]*`; +const DIGITS = + "0[xX][0-9A-Fa-f_]+|0[oO][0-7_]+|0[bB][01_]+|(?:\\d[\\d_]*(?:\\.[\\d_]*)?|\\.\\d[\\d_]*)(?:[eE][+-]?\\d+)?"; +// The name of a $tag$, which has no $. +const TAG = `[${NAME_START}][\\w${NON_ASCII}]*`; +const PG_STRING_PREFIX = "(?:[eEbBxXnN]|[uU]&)?"; +// PostgreSQL's operator characters, but the backtick. +const PG_OPERATOR = "+\\-*/<>=~!@#%^&|?"; + +const WORD = new RegExp(NAME, "y"); +const NUMBER = new RegExp(`(?:${DIGITS})[${NAME_PART}]*`, "y"); +const NAMED = new RegExp(`(? prefix === "e" || prefix === "E", + quotedName: /(?:[uU]&)?(")/y, + closes: { '"': '"' }, + dollarStrings: true, + cast: true, + parameter: /\$\d+/y, + variable: null, + operator: new RegExp(`(?:(?!--|/\\*)[${PG_OPERATOR}\`])+`, "y"), +}; + +/** + * PostgreSQL's tokens as regular expression sources, for the VS Code + * grammar, which reads every dialect as PostgreSQL. Its operators leave out + * the backtick, which it reads as MySQL's and SQLite's name quote. + */ +export const GRAMMAR_LEXIS = { + name: NAME, + number: `(? false, + quotedName: /(["`[])/y, + closes: { '"': '"', "`": "`", "[": "]" }, + dollarStrings: false, + cast: false, + parameter: /\?\d*|[@$][\w$\u0080-￿]+/y, + variable: null, + operator: /(?:(?!--|\/\*)[-+*/%<>=!~&|])+/y, +}; + +// MySQL and MariaDB: `-- ` needs a space after the dashes, `#` starts a +// comment, '…' and "…" are strings with backslash escapes (after an n, x, b +// or _charset prefix), and @name and @@name are variables. +const mysql: Lexis = { + lineComment: /(?:#|--(?=\s|$))[^\n\r]*/y, + nestedComments: false, + string: /(?:[nNxXbB]|_[A-Za-z0-9_]+)?(['"])/y, + backslashes: () => true, + quotedName: /(`)/y, + closes: { "`": "`" }, + dollarStrings: false, + cast: false, + parameter: /\$\d+/y, + variable: /@@?(?:[A-Za-z0-9_$.\u0080-￿]+|`(?:[^`]|``)*`?)?/y, + operator: /(?:(?!--\s|--$|\/\*)[-+*/<>=~!%^&|?:])+/y, +}; + +export const LEXIS = { postgres, sqlite, mysql } as const; + +type Rule = (text: string, i: number, lexis: Lexis) => Token | null; + +const endOf = (re: RegExp | null, text: string, i: number): number => { + if (!re) return -1; + re.lastIndex = i; + return re.test(text) ? re.lastIndex : -1; +}; + +const rule = + (kind: Lexeme, re: (lexis: Lexis) => RegExp | null, open = false): Rule => + (text, i, lexis) => { + const end = endOf(re(lexis), text, i); + return end < 0 ? null : { kind, start: i, end, open }; + }; + +const RULES: readonly Rule[] = [ + rule("comment", (l) => l.lineComment, true), + (text, i, lexis) => { + if (!text.startsWith("/*", i)) return null; + const [end, closed] = lexis.nestedComments + ? nestedComment(text, i) + : flatComment(text, i); + return { kind: "comment", start: i, end, open: !closed }; + }, + rule("hole", () => HOLE), + (text, i, lexis) => { + lexis.string.lastIndex = i; + const m = lexis.string.exec(text); + if (!m) return null; + const backslashes = lexis.backslashes(m[0].slice(0, -1)); + return stringAt(text, i, lexis.string.lastIndex, m[1]!, backslashes); + }, + (text, i, lexis) => { + const body = lexis.dollarStrings ? endOf(DOLLAR_TAG, text, i) : -1; + if (body < 0) return null; + const tag = text.slice(i, body); + const close = text.indexOf(tag, body); + return close < 0 + ? { kind: "string", start: i, end: text.length, open: true } + : { kind: "string", start: i, end: close + tag.length, open: false }; + }, + (text, i, lexis) => { + lexis.quotedName.lastIndex = i; + const m = lexis.quotedName.exec(text); + if (!m) return null; + return quotedAt(text, i, lexis.quotedName.lastIndex, lexis.closes[m[1]!]!); + }, + rule("parameter", (l) => l.parameter), + // `:name`, but not the second colon of `::` or a colon right after a name. + rule("parameter", () => NAMED), + rule("variable", (l) => l.variable), + rule("cast", (l) => (l.cast ? CAST : null)), + rule("number", () => NUMBER), + rule("word", () => WORD), + rule("operator", (l) => l.operator), +]; + +/** Splits SQL text into tokens; whitespace is left out. */ +export function lex(text: string, lexis: Lexis): Token[] { + const out: Token[] = []; + for (let i = 0; i < text.length; ) { + if (/\s/.test(text[i]!)) { + i++; + continue; + } + let token: Token = { + kind: "punctuation", + start: i, + end: i + 1, + open: false, + }; + for (const r of RULES) { + const t = r(text, i, lexis); + if (t) { + token = t; + break; + } + } + out.push(token); + i = token.end; + } + return out; +} + +/** + * The string from `start` whose body starts at `from`: a doubled quote, or + * with `backslashes` a backslash, escapes the character after it. + */ +function stringAt( + text: string, + start: number, + from: number, + quote: string, + backslashes: boolean, +): Token { + const escapes: [number, number][] = []; + for (let i = from; i < text.length; ) { + const c = text[i]; + if ((backslashes && c === "\\") || (c === quote && text[i + 1] === quote)) { + escapes.push([i, Math.min(i + 2, text.length)]); + i += 2; + } else if (c === quote) + return { kind: "string", start, end: i + 1, open: false, escapes }; + else i++; + } + return { kind: "string", start, end: text.length, open: true, escapes }; +} + +/** The quoted name from `start` whose name starts at `from`; a doubled quote other than `]` is one. */ +function quotedAt( + text: string, + start: number, + from: number, + close: string, +): Token { + for (let i = from; i < text.length; ) { + if (text[i] !== close) i++; + else if (close !== "]" && text[i + 1] === close) i += 2; + else { + const name = text.slice(from, i).replaceAll(close + close, close); + return { kind: "quoted", start, end: i + 1, open: false, name }; + } + } + const name = text.slice(from).replaceAll(close + close, close); + return { kind: "quoted", start, end: text.length, open: true, name }; +} + +/** The end of the nested block comment at `i`, and whether it was closed. */ +function nestedComment(s: string, i: number): [number, boolean] { + let depth = 0; + for (let j = i; j < s.length; ) { + if (s.startsWith("/*", j)) { + depth++; + j += 2; + } else if (s.startsWith("*/", j)) { + j += 2; + if (--depth === 0) return [j, true]; + } else j++; + } + return [s.length, false]; +} + +function flatComment(s: string, i: number): [number, boolean] { + const end = s.indexOf("*/", i + 2); + return end < 0 ? [s.length, false] : [end + 2, true]; +} diff --git a/packages/language-server/src/sql/words.ts b/packages/language-server/src/sql/words.ts new file mode 100644 index 00000000..9c2bd8ef --- /dev/null +++ b/packages/language-server/src/sql/words.ts @@ -0,0 +1,348 @@ +// The words the highlighter colors. The VS Code extension's grammar +// (packages/vscode/syntaxes/sql-injection.tmLanguage.json) is generated with +// the base lists, so a string colors the same whichever of the two colors it. + +/** Type names, in every dialect. */ +export const TYPES = [ + "bigint", + "bigserial", + "bit", + "bool", + "boolean", + "box", + "bytea", + "char", + "character", + "cidr", + "circle", + "date", + "daterange", + "decimal", + "double", + "float", + "float4", + "float8", + "inet", + "int", + "int2", + "int4", + "int4range", + "int8", + "int8range", + "integer", + "interval", + "json", + "jsonb", + "jsonpath", + "line", + "lseg", + "macaddr", + "macaddr8", + "money", + "numeric", + "numrange", + "path", + "point", + "polygon", + "precision", + "real", + "record", + "regclass", + "regtype", + "serial", + "serial2", + "serial4", + "serial8", + "smallint", + "smallserial", + "text", + "time", + "timestamp", + "timestamptz", + "timetz", + "tsquery", + "tsrange", + "tstzrange", + "tsvector", + "uuid", + "varbit", + "varchar", + "varying", + "void", + "xml", +] as const; + +/** Keywords, in every dialect. */ +export const KEYWORDS = [ + "add", + "after", + "all", + "alter", + "always", + "analyze", + "and", + "any", + "array", + "as", + "asc", + "asymmetric", + "at", + "authorization", + "before", + "begin", + "between", + "both", + "by", + "cascade", + "case", + "cast", + "check", + "collate", + "column", + "commit", + "concurrently", + "conflict", + "constraint", + "create", + "cross", + "cube", + "current_catalog", + "current_date", + "current_role", + "current_schema", + "current_time", + "current_timestamp", + "current_user", + "database", + "declare", + "default", + "deferrable", + "deferred", + "delete", + "desc", + "distinct", + "do", + "domain", + "drop", + "each", + "else", + "end", + "enum", + "escape", + "except", + "exclude", + "execute", + "exists", + "explain", + "extension", + "fetch", + "filter", + "first", + "following", + "for", + "foreign", + "from", + "full", + "function", + "generated", + "grant", + "group", + "grouping", + "having", + "identity", + "if", + "ilike", + "immediate", + "in", + "include", + "index", + "initially", + "inner", + "insert", + "intersect", + "into", + "is", + "isnull", + "join", + "key", + "language", + "last", + "lateral", + "leading", + "left", + "like", + "limit", + "localtime", + "localtimestamp", + "lock", + "locked", + "materialized", + "matched", + "merge", + "natural", + "next", + "no", + "not", + "nothing", + "notnull", + "nowait", + "nulls", + "of", + "offset", + "on", + "only", + "or", + "order", + "ordinality", + "outer", + "over", + "overlaps", + "overriding", + "partition", + "placing", + "preceding", + "primary", + "procedure", + "range", + "recursive", + "references", + "release", + "rename", + "replace", + "restrict", + "returning", + "returns", + "revoke", + "right", + "rollback", + "rollup", + "row", + "rows", + "savepoint", + "schema", + "select", + "sequence", + "session_user", + "set", + "sets", + "share", + "similar", + "skip", + "some", + "stored", + "symmetric", + "system_user", + "table", + "tablesample", + "temp", + "temporary", + "then", + "ties", + "to", + "trailing", + "transaction", + "trigger", + "truncate", + "unbounded", + "union", + "unique", + "unlogged", + "update", + "user", + "using", + "values", + "variadic", + "verbose", + "view", + "when", + "where", + "window", + "with", + "within", + "without", + "zone", +] as const; + +export const CONSTANTS = ["true", "false", "null"] as const; + +/** MySQL and MariaDB type names on top of TYPES. */ +export const MY_TYPES = [ + "binary", + "blob", + "datetime", + "dec", + "enum", + "fixed", + "geometry", + "geometrycollection", + "inet4", + "inet6", + "linestring", + "longblob", + "longtext", + "mediumblob", + "mediumint", + "mediumtext", + "multilinestring", + "multipoint", + "multipolygon", + "nchar", + "nvarchar", + "signed", + "tinyblob", + "tinyint", + "tinytext", + "unsigned", + "varbinary", + "vector", + "year", + "zerofill", +] as const; + +/** MySQL and MariaDB keywords on top of KEYWORDS. */ +export const MY_KEYWORDS = [ + "algorithm", + "auto_increment", + "change", + "charset", + "columns", + "comment", + "delayed", + "describe", + "distinctrow", + "div", + "dual", + "duplicate", + "engine", + "force", + "fulltext", + "high_priority", + "ignore", + "invisible", + "keys", + "low_priority", + "member", + "mod", + "mode", + "modify", + "nested", + "quick", + "regexp", + "rlike", + "separator", + "show", + "sounds", + "spatial", + "sql_big_result", + "sql_buffer_result", + "sql_calc_found_rows", + "sql_no_cache", + "sql_small_result", + "straight_join", + "tables", + "unlock", + "use", + "virtual", + "visible", + "xor", +] as const; diff --git a/packages/language-server/tsconfig.build.json b/packages/language-server/tsconfig.build.json index b721e7ef..eaad7e05 100644 --- a/packages/language-server/tsconfig.build.json +++ b/packages/language-server/tsconfig.build.json @@ -7,8 +7,6 @@ "rootDir": "./src", "outDir": "./dist", "noEmit": false, - "declaration": false, - "declarationMap": false, "types": ["node"] }, "include": ["src"] diff --git a/packages/mysql/package.json b/packages/mysql/package.json index 36a0b671..24cef1f0 100644 --- a/packages/mysql/package.json +++ b/packages/mysql/package.json @@ -71,7 +71,6 @@ } }, "devDependencies": { - "mariadb": "catalog:", "mysql2": "catalog:", "tsrs": "catalog:", "typescript": "catalog:" diff --git a/packages/mysql/src/introspect/cli.ts b/packages/mysql/src/introspect/cli.ts index 9b3a5e23..060d3fb1 100644 --- a/packages/mysql/src/introspect/cli.ts +++ b/packages/mysql/src/introspect/cli.ts @@ -1,13 +1,13 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; import { parseArgs } from "node:util"; -import { type CommandIo, introspectArgs } from "@typesql/core/adapter"; -import { type GenerateOptions, generateSource } from "./emit.js"; import { - type IntrospectOptions, - introspect, - type MySqlQueryable, -} from "./introspect.js"; + type CommandIo, + connectMysql, + databaseUrl, + introspectCommand, + listOption, +} from "@typesql/core/adapter"; +import { type GenerateOptions, generateSource } from "./emit.js"; +import { type IntrospectOptions, introspect } from "./introspect.js"; const USAGE = `Usage: typesql introspect --dialect mysql|mariadb [options] @@ -25,58 +25,9 @@ Options: -h, --help show this help `; -type Connection = MySqlQueryable & { end(): Promise }; - -interface Io extends CommandIo { - readonly env: Readonly>; - readonly cwd: string; - readonly connect?: (url: string) => Promise; -} - -const defaultIo = (): Io => ({ - stdout: (s) => process.stdout.write(s), - stderr: (s) => process.stderr.write(s), - env: process.env, - cwd: process.cwd(), -}); - -// A refused `localhost` connection (IPv4 and IPv6) rejects with an -// AggregateError whose own message is empty. -const message = (cause: unknown): string => { - if (!(cause instanceof Error)) return String(cause); - if (cause.message) return cause.message; - if (cause instanceof AggregateError && cause.errors.length > 0) - return Array.from(new Set(cause.errors.map(message))).join("; "); - const code = "code" in cause ? cause.code : undefined; - return code === undefined || code === null ? cause.name : String(code); -}; - -/** mysql2 if it is installed, else the mariadb connector. */ -async function connect(url: string): Promise { - if (!URL.canParse(url) || !/^(mysql|mariadb):$/.test(new URL(url).protocol)) - throw new Error( - `invalid connection URL ${JSON.stringify(url)} (expected mysql://user:password@host:port/database)`, - ); - const rest = url.slice(url.indexOf(":")); - let mysql2: typeof import("mysql2/promise") | undefined; - try { - mysql2 = (await import("mysql2/promise")).default; - } catch {} - if (mysql2) return mysql2.createConnection(`mysql${rest}`); - let mariadb: typeof import("mariadb"); - try { - mariadb = await import("mariadb"); - } catch { - throw new Error( - 'neither "mysql2" nor "mariadb" is installed: npm install mysql2', - ); - } - return mariadb.createConnection(`mariadb${rest}`); -} - -const parse = (argv: readonly string[]) => +const parse = (args: string[]) => parseArgs({ - args: [...argv], + args, allowPositionals: true, options: { url: { type: "string" }, @@ -90,50 +41,37 @@ const parse = (argv: readonly string[]) => }, }); -export async function main( +/** + * `typesql introspect --dialect mysql` (or `mariadb`), with mysql2, or the + * mariadb connector when mysql2 is not installed. `io` replaces the + * process's output, working directory and environment. + */ +export const main: ( argv: readonly string[], - io: Io = defaultIo(), -): Promise { - const values = introspectArgs(io, USAGE, () => parse(argv)); - if (typeof values === "number") return values; - const url = values.url ?? io.env.DATABASE_URL; - if (!url) { - io.stderr("typesql introspect: pass --url or set DATABASE_URL\n"); - return 2; - } + io?: CommandIo, +) => Promise = introspectCommand(USAGE, parse, async (values, io) => { + const url = databaseUrl(values.url, io); + const options: IntrospectOptions & GenerateOptions = { + ...(values.database !== undefined && { database: values.database }), + exclude: listOption(values.exclude), + ...(values["type-name"] !== undefined && { + typeName: values["type-name"], + }), + ...(values["import-from"] !== undefined && { + importFrom: values["import-from"], + }), + ...(values["no-comments"] && { comments: false }), + }; + const connection = await connectMysql(url); try { - const options: IntrospectOptions & GenerateOptions = { - ...(values.database !== undefined && { database: values.database }), - exclude: (values.exclude ?? []).flatMap((e) => - e - .split(",") - .map((x) => x.trim()) - .filter(Boolean), + return { + source: generateSource( + await introspect(connection.client, options), + options, ), - ...(values["type-name"] !== undefined && { - typeName: values["type-name"], - }), - ...(values["import-from"] !== undefined && { - importFrom: values["import-from"], - }), - ...(values["no-comments"] && { comments: false }), + out: values.out, }; - const client = await (io.connect ?? connect)(url); - let source: string; - try { - source = generateSource(await introspect(client, options), options); - } finally { - await client.end(); - } - if (values.out) { - const out = resolve(io.cwd, values.out); - await mkdir(dirname(out), { recursive: true }); - await writeFile(out, source); - io.stderr(`typesql introspect: wrote ${values.out}\n`); - } else io.stdout(source); - return 0; - } catch (err) { - io.stderr(`typesql introspect: ${message(err)}\n`); - return 1; + } finally { + await connection.close(); } -} +}); diff --git a/packages/mysql/src/introspect/emit.ts b/packages/mysql/src/introspect/emit.ts index b4182fa6..60f0ec1b 100644 --- a/packages/mysql/src/introspect/emit.ts +++ b/packages/mysql/src/introspect/emit.ts @@ -1,4 +1,4 @@ -import { propertyKey as key } from "@typesql/core/adapter"; +import { doc, propertyKey as key, schemaWriter } from "@typesql/core/adapter"; import type { IntrospectedColumn, IntrospectedDatabase, @@ -13,27 +13,6 @@ export interface GenerateOptions { const str = (s: string): string => JSON.stringify(s); -function doc(lines: readonly string[], indent: string): string { - const text = lines - .flatMap((l) => l.split("\n")) - .map((l) => l.replace(/\*\//g, "*\\/").trimEnd()); - if (text.length === 0) return ""; - if (text.length === 1) return `${indent}/** ${text[0]} */\n`; - return `${indent}/**\n${text.map((l) => `${indent} *${l ? ` ${l}` : ""}`).join("\n")}\n${indent} */\n`; -} - -// `autoIncrement` and `generated` imply `hasDefault`. -const flagsOf = (c: IntrospectedColumn, view: boolean): string[] => [ - ...(c.nullable ? ["nullable: true"] : []), - ...(c.hasDefault && !c.autoIncrement && !c.generated - ? ["hasDefault: true"] - : []), - `position: ${c.position}`, - ...(!view && c.autoIncrement ? ["autoIncrement: true"] : []), - ...(!view && c.generated ? ["generated: true"] : []), - ...(c.hidden ? ["hidden: true"] : []), -]; - function columnNotes(c: IntrospectedColumn): string[] { const notes: string[] = []; if (c.comment) notes.push(c.comment); @@ -53,29 +32,28 @@ export function generateSource( db: IntrospectedDatabase, options: GenerateOptions = {}, ): string { - const typeName = options.typeName ?? "DB"; const comments = options.comments ?? true; - const used = new Set(); + const write = schemaWriter("mysql"); const relation = (rel: IntrospectedRelation): string => { const view = rel.kind === "view"; const cols = rel.columns.map((c) => { - const flags = flagsOf(c, view).join("; "); - let descriptor: string; - if (c.enumValues) { - used.add("Enum"); - const values = c.enumValues.map(str).join(" | ") || "never"; - descriptor = `Enum<${values}, { ${flags} }>`; - } else { - used.add("Col"); - descriptor = `Col<${str(c.type)}, { ${flags} }>`; - } + const flags = { + nullable: c.nullable, + hasDefault: c.hasDefault, + position: c.position, + // A view's column is read, not written. + autoIncrement: !view && c.autoIncrement, + generated: !view && c.generated !== null, + hidden: c.hidden, + }; + const descriptor = c.enumValues + ? write.enum(c.enumValues.map(str).join(" | ") || "never", flags) + : write.col(c.type, flags); return `${comments ? doc(columnNotes(c), " ") : ""} ${key(c.name)}: ${descriptor};`; }); const body = `{\n${cols.join("\n")}\n }`; - if (!view) return body; - used.add("View"); - return `View<${body}>`; + return view ? write.view(body) : body; }; const lines = db.relations.map((rel) => { @@ -90,18 +68,10 @@ export function generateSource( : ""; return `${notes} ${key(rel.name)}: ${relation(rel)};`; }); - const helpers = ["Col", "Enum", "View"].filter((h) => used.has(h)); - return [ - `// Generated by \`typesql introspect\` from ${serverName(db)}. Do not edit.`, - ...(helpers.length > 0 - ? [ - `import type { ${helpers.join(", ")} } from ${str(options.importFrom ?? "@typesql/core/mysql")};`, - ] - : []), - "", - `export interface ${key(typeName)} {`, - ...lines, - "}", - "", - ].join("\n"); + return write.module({ + header: `Generated by \`typesql introspect\` from ${serverName(db)}. Do not edit.`, + members: lines, + typeName: options.typeName, + importFrom: options.importFrom, + }); } diff --git a/packages/mysql/src/literal.ts b/packages/mysql/src/literal.ts deleted file mode 100644 index a48ac590..00000000 --- a/packages/mysql/src/literal.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { mysqlValue } from "@typesql/core/adapter"; - -/** - * Strings (with backslash escapes), quoted identifiers and comments, which - * can't hold parameters, and `$n`: the scanner of `mysqlPositional`. - */ -const PARAMS = - /'(?:[^'\\]|\\[\s\S]|'')*'?|"(?:[^"\\]|\\[\s\S]|"")*"?|`(?:[^`]|``)*`?|--(?=[\s]|$)[^\n]*|#[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|(? = new Map([ - ["\0", "\\0"], - ["\b", "\\b"], - ["\t", "\\t"], - ["\n", "\\n"], - ["\r", "\\r"], - ["\x1a", "\\Z"], - ["\\", "\\\\"], - ["'", "\\'"], - ['"', '\\"'], -]); - -function quote(s: string): string { - let out = "'"; - for (const c of s) out += ESCAPES.get(c) ?? c; - return `${out}'`; -} - -const pad = (n: number, width = 2): string => String(n).padStart(width, "0"); - -/** A value as a MySQL literal. Dates are written in UTC, the serverless platforms' time zone. */ -function literal(value: V): string { - const v = mysqlValue(value); - if (v === null || v === undefined) return "NULL"; - switch (typeof v) { - case "string": - return quote(v); - case "boolean": - return v ? "TRUE" : "FALSE"; - case "bigint": - return v < 0n ? `(${v})` : String(v); - case "number": - if (!Number.isFinite(v)) - throw new TypeError(`typesql: MySQL has no ${v} value`); - // `5-$1` must not become the comment-free but odd `5--1`. - return v < 0 ? `(${v})` : String(v); - } - if (v instanceof Date) { - if (Number.isNaN(v.getTime())) - throw new TypeError("typesql: cannot bind an invalid Date"); - return `'${pad(v.getUTCFullYear(), 4)}-${pad(v.getUTCMonth() + 1)}-${pad(v.getUTCDate())} ${pad(v.getUTCHours())}:${pad(v.getUTCMinutes())}:${pad(v.getUTCSeconds())}.${pad(v.getUTCMilliseconds(), 3)}'`; - } - if (v instanceof Uint8Array) { - let hex = ""; - for (const b of v) hex += b.toString(16).padStart(2, "0"); - return `X'${hex}'`; - } - throw new TypeError( - `typesql: cannot bind ${Object.prototype.toString.call(v)} as a MySQL value; use sql.json() for JSON`, - ); -} - -/** - * The HTTP drivers bind nothing on the server, so `$1`, `$2`, … become - * escaped literals in the statement text. - */ -export function inline(text: string, values: readonly unknown[]): string { - if (!text.includes("$")) return text; - return text.replace(PARAMS, (m: string, n?: string) => - n === undefined ? m : literal(values[Number(n) - 1]), - ); -} diff --git a/packages/mysql/src/mariadb.ts b/packages/mysql/src/mariadb.ts index 3f8bf8fc..be539e28 100644 --- a/packages/mysql/src/mariadb.ts +++ b/packages/mysql/src/mariadb.ts @@ -1,5 +1,11 @@ import type { Executor, SqlPlugin } from "@typesql/core"; -import { createTransactionalSql, type SqlDriver } from "@typesql/core/adapter"; +import { + createTransactionalSql, + type MySqlResult, + type MySqlValue, + mysqlResult, + type SqlDriver, +} from "@typesql/core/adapter"; import { type MariaDb, type MariaDbTransactionalSql, @@ -10,25 +16,22 @@ import { type TransactionalSql, } from "@typesql/core/mysql"; import { - type DriverResult, executor as executorOf, - type MySqlValue, driver as poolDriver, type Runner, - resultOf, } from "./pool.js"; /** A `mariadb` Pool, PoolConnection or Connection (the promise API). */ export interface MariaDbQueryable { - query(sql: string, values?: MySqlValue[]): Promise; - execute(sql: string, values?: MySqlValue[]): Promise; + query(sql: string, values?: MySqlValue[]): Promise; + execute(sql: string, values?: MySqlValue[]): Promise; } const runner: Runner = { query: async (client, text, values) => - resultOf(await client.query(text, values)), + mysqlResult(await client.query(text, values)), execute: async (client, text, values) => - resultOf(await client.execute(text, values)), + mysqlResult(await client.execute(text, values)), }; const driver: SqlDriver = poolDriver(runner, "mariadb"); diff --git a/packages/mysql/src/mysql2.ts b/packages/mysql/src/mysql2.ts index aee39780..01bb9f58 100644 --- a/packages/mysql/src/mysql2.ts +++ b/packages/mysql/src/mysql2.ts @@ -1,12 +1,14 @@ import type { Executor } from "@typesql/core"; -import type { SqlDriver } from "@typesql/core/adapter"; import { - type DriverResult, - executor as executorOf, + type MySqlResult, type MySqlValue, + mysqlResult, + type SqlDriver, +} from "@typesql/core/adapter"; +import { + executor as executorOf, driver as poolDriver, type Runner, - resultOf, } from "./pool.js"; /** A `mysql2/promise` Pool, PoolConnection or Connection. */ @@ -14,18 +16,18 @@ export interface Mysql2Queryable { query( sql: string, values?: MySqlValue[], - ): Promise; + ): Promise; execute( sql: string, values?: MySqlValue[], - ): Promise; + ): Promise; } const runner: Runner = { query: async (client, text, values) => - resultOf((await client.query(text, values))[0]), + mysqlResult((await client.query(text, values))[0]), execute: async (client, text, values) => - resultOf((await client.execute(text, values))[0]), + mysqlResult((await client.execute(text, values))[0]), }; export const driver: SqlDriver = poolDriver(runner, "mysql2"); diff --git a/packages/mysql/src/planetscale.ts b/packages/mysql/src/planetscale.ts index bac33a34..2ad1ae88 100644 --- a/packages/mysql/src/planetscale.ts +++ b/packages/mysql/src/planetscale.ts @@ -1,6 +1,7 @@ import type { Executor, ExecutorResult, SqlPlugin } from "@typesql/core"; import { createTransactionalSql, + mysqlInline, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; @@ -10,7 +11,6 @@ import { type ServerlessTypeMap, type TransactionalSql, } from "@typesql/core/mysql"; -import { inline } from "./literal.js"; export interface PlanetScaleResult { readonly rows: readonly unknown[]; @@ -42,7 +42,7 @@ async function run( } export function executor(client: PlanetScaleQueryable): Executor { - return (text, values) => run(client, inline(text, values)); + return (text, values) => run(client, mysqlInline(text, values)); } const isClient = (c: PlanetScaleQueryable): c is PlanetScaleClient => @@ -50,14 +50,15 @@ const isClient = (c: PlanetScaleQueryable): c is PlanetScaleClient => function transaction( client: PlanetScaleQueryable, - depth: number, + _depth: number, fn: (tx: PlanetScaleQueryable) => Promise, + savepoint: string | undefined, ): Promise { - if (depth > 0) + if (savepoint !== undefined) return runTransaction( (statement) => client.execute(statement), () => fn(client), - { savepoint: `typesql_${depth}` }, + { savepoint }, ); if (!isClient(client)) return Promise.reject( diff --git a/packages/mysql/src/pool.ts b/packages/mysql/src/pool.ts index 7cdea0c6..97054388 100644 --- a/packages/mysql/src/pool.ts +++ b/packages/mysql/src/pool.ts @@ -1,28 +1,13 @@ import type { Executor, ExecutorResult } from "@typesql/core"; import { + type MySqlValue, + mysqlBind, mysqlPositional, + pooledTransaction, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; -/** - * What mysql2 and the mariadb connector resolve a statement to: its rows, or - * the OK packet of a statement without rows. - */ -export type DriverResult = - | readonly unknown[] - | { readonly affectedRows?: number | bigint }; - -/** A value as mysql2 and the mariadb connector bind it. */ -export type MySqlValue = - | string - | number - | bigint - | boolean - | Date - | Buffer - | null; - /** How mysql2 or the mariadb connector runs one statement on a client. */ export interface Runner { /** Text protocol: the driver escapes `values` into the statement. */ @@ -44,38 +29,6 @@ interface Checkout { destroy?(): void | Promise; } -/** - * The values of a statement as the drivers bind them: `sql.json(…)` is JSON - * text already, `undefined` is `NULL`, and a plain `Uint8Array` becomes a - * `Buffer`, which both drivers send as binary. - */ -function bind(values: readonly unknown[]): MySqlValue[] { - return values.map((v): MySqlValue => { - if (v === undefined || v === null) return null; - if (Buffer.isBuffer(v)) return v; - if (v instanceof Uint8Array) - return Buffer.from(v.buffer, v.byteOffset, v.byteLength); - if (v instanceof Date) return v; - switch (typeof v) { - case "string": - case "number": - case "bigint": - case "boolean": - return v; - } - throw new TypeError( - `typesql: cannot bind ${Object.prototype.toString.call(v)} as a MySQL value; use sql.json() for JSON`, - ); - }); -} - -/** Rows of a statement that returns them, else the affected row count. */ -export function resultOf(result: DriverResult): ExecutorResult { - if (Array.isArray(result)) return { rows: result, rowCount: result.length }; - const affected = "affectedRows" in result ? result.affectedRows : undefined; - return { rows: [], rowCount: affected === undefined ? 0 : Number(affected) }; -} - // Statements go through the text protocol, where the driver escapes each value // into the SQL: every statement works (DDL, `LIMIT $1`, statements MySQL cannot // prepare) and nothing stays allocated on the server. sql.prepare() uses the @@ -84,20 +37,15 @@ export function resultOf(result: DriverResult): ExecutorResult { // a DOUBLE, which MySQL 8.0.22+ rejects in `LIMIT ?` ("Incorrect arguments to // mysqld_stmt_execute"); 3.24 types integers from the statement's parameters. export function executor(runner: Runner, client: C): Executor { - return Object.assign( - async (text: string, values: readonly unknown[]) => { - const p = mysqlPositional(text, values); - return runner.query(client, p.text, bind(p.values)); - }, - { - prepare: (text: string): Executor => { - return async (_text, values) => { - const p = mysqlPositional(text, values); - return runner.execute(client, p.text, bind(p.values)); - }; - }, - }, - ); + const run = + (send: Runner["query"], text?: string) => + async (given: string, values: readonly unknown[]) => { + const p = mysqlPositional(text ?? given, values); + return send(client, p.text, p.values.map(mysqlBind)); + }; + return Object.assign(run(runner.query), { + prepare: (text: string): Executor => run(runner.execute, text), + }); } const isPool = (client: C): client is C & PoolLike => @@ -134,36 +82,30 @@ export function driver(runner: Runner, name: string): SqlDriver { ); // A connection whose ROLLBACK failed may still be in the transaction: // close it instead of handing it to the next caller. - let broken = false; - try { - return await runTransaction(on(conn), () => fn(conn), { - onRollbackError: () => { - broken = true; - }, - }); - } finally { - if (broken && conn.destroy) await conn.destroy(); - else await conn.release(); - } + return pooledTransaction( + conn, + on(conn), + (broken) => + broken !== undefined && conn.destroy ? conn.destroy() : conn.release(), + fn, + ); } function transaction( client: C, - depth: number, + _depth: number, fn: (tx: C) => Promise, + savepoint: string | undefined, ): Promise { - if (depth > 0) - return runTransaction(on(client), () => fn(client), { - savepoint: `typesql_${depth}`, - }); - if (isPool(client)) return inPoolTransaction(client, fn); - if (!isConnection(client)) + if (savepoint === undefined && isPool(client)) + return inPoolTransaction(client, fn); + if (savepoint === undefined && !isConnection(client)) return Promise.reject( new Error( `typesql: sql.transaction() needs a ${name} Pool, PoolConnection or Connection, so that BEGIN and COMMIT reach one connection`, ), ); - return runTransaction(on(client), () => fn(client)); + return runTransaction(on(client), () => fn(client), { savepoint }); } return { executor: (client) => executor(runner, client), transaction }; diff --git a/packages/mysql/src/tidb.ts b/packages/mysql/src/tidb.ts index 8a6a49b8..0caf991c 100644 --- a/packages/mysql/src/tidb.ts +++ b/packages/mysql/src/tidb.ts @@ -1,6 +1,7 @@ import type { Executor, ExecutorResult, SqlPlugin } from "@typesql/core"; import { createTransactionalSql, + mysqlInline, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; @@ -10,7 +11,6 @@ import { type ServerlessTypeMap, type TransactionalSql, } from "@typesql/core/mysql"; -import { inline } from "./literal.js"; interface FullResult { readonly rows: readonly unknown[] | null; @@ -55,7 +55,7 @@ async function run( } export function executor(client: TiDbQueryable): Executor { - return (text, values) => run(client, inline(text, values)); + return (text, values) => run(client, mysqlInline(text, values)); } const isConnection = (c: TiDbQueryable): c is TiDbConnection => @@ -63,14 +63,15 @@ const isConnection = (c: TiDbQueryable): c is TiDbConnection => async function transaction( client: TiDbQueryable, - depth: number, + _depth: number, fn: (tx: TiDbQueryable) => Promise, + savepoint: string | undefined, ): Promise { - if (depth > 0) + if (savepoint !== undefined) return runTransaction( (statement) => client.execute(statement, null, OPTIONS), () => fn(client), - { savepoint: `typesql_${depth}` }, + { savepoint }, ); if (!isConnection(client)) throw new Error( @@ -83,9 +84,8 @@ async function transaction( await tx.commit(); return result; } catch (err) { - try { - await tx.rollback(); - } catch {} + // The error that ended the transaction is the one to report. + await tx.rollback().catch(() => {}); throw err; } } diff --git a/packages/pg/src/client.ts b/packages/pg/src/client.ts index d9bb4dbd..62f06678 100644 --- a/packages/pg/src/client.ts +++ b/packages/pg/src/client.ts @@ -3,6 +3,8 @@ import type { Executor } from "@typesql/core"; import { type PgField, parsePgUserArrays, + pgResult, + pooledTransaction, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; @@ -51,10 +53,15 @@ export interface PgPoolLike< // A pool's checked-out clients share the pool's cache of user-defined array types. const pools = new WeakMap(); +/** A result's rows, with arrays of user-defined types parsed; a script's last statement stands for it. */ function rowsOf( client: PgQueryable, - { rows, rowCount, fields }: PgResult, + result: PgResult, ): Promise<{ rows: unknown[]; rowCount: number | null }> { + const { rows, rowCount, fields }: PgResult = pgResult(result) ?? { + rows: [], + rowCount: 0, + }; return parsePgUserArrays( pools.get(client) ?? client, (text, values) => client.query(text, values), @@ -99,46 +106,41 @@ async function inPoolTransaction( fn: (conn: C) => Promise, ): Promise { const conn = await pool.connect(); - let releaseWith: Error | undefined; - const toError = (cause: unknown) => - cause instanceof Error ? cause : new Error(String(cause)); + let lost: Error | undefined; // A checked-out pg client has no pool listener, so an unhandled 'error' // (a dropped connection) would crash the process. The pending query rejects. const onError: ErrorListener = (err) => { - releaseWith ??= toError(err); + lost ??= err; }; if (canOn(conn)) conn.on("error", onError); pools.set(conn, pool); - try { - return await runTransaction(on(conn), () => fn(conn), { - onRollbackError: (err) => { - releaseWith = toError(err); - }, - }); - } finally { - if (canOff(conn)) conn.off("error", onError); - conn.release(releaseWith); - } + return pooledTransaction( + conn, + on(conn), + (broken) => { + if (canOff(conn)) conn.off("error", onError); + conn.release(broken ?? lost); + }, + fn, + ); } function transaction( client: PgQueryable, - depth: number, + _depth: number, fn: (tx: PgQueryable) => Promise, + savepoint: string | undefined, ): Promise { - if (depth > 0) - return runTransaction(on(client), () => fn(client), { - savepoint: `typesql_${depth}`, - }); - if (isPool(client)) return inPoolTransaction(client, fn); - if (!isConnection(client)) { + if (savepoint === undefined && isPool(client)) + return inPoolTransaction(client, fn); + if (savepoint === undefined && !isConnection(client)) { return Promise.reject( new Error( "typesql: sql.transaction() needs a pg Pool, Client or PoolClient, so that BEGIN and COMMIT reach one connection", ), ); } - return runTransaction(on(client), () => fn(client)); + return runTransaction(on(client), () => fn(client), { savepoint }); } export const driver: SqlDriver = { executor, transaction }; diff --git a/packages/pg/src/introspect/cli.ts b/packages/pg/src/introspect/cli.ts index 9973b320..54dfbc99 100644 --- a/packages/pg/src/introspect/cli.ts +++ b/packages/pg/src/introspect/cli.ts @@ -1,9 +1,13 @@ -import { mkdir, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; -import { type CommandIo, introspectArgs } from "@typesql/core/adapter"; -import type { PgQueryable } from "../client.js"; +import { + type CommandIo, + connectPostgres, + databaseUrl, + introspectCommand, + listOption, +} from "@typesql/core/adapter"; import { type GenerateOptions, generateSource } from "./emit.js"; import { type IntrospectOptions, introspect } from "./introspect.js"; @@ -29,100 +33,44 @@ Options: -h, --help show this help `; -interface Io extends CommandIo { - readonly env: Readonly>; - readonly cwd: string; - readonly connect?: ( - url: string, - ) => Promise }>; -} - -const defaultIo = (): Io => ({ - stdout: (s) => process.stdout.write(s), - stderr: (s) => process.stderr.write(s), - env: process.env, - cwd: process.cwd(), -}); +type Option = IntrospectConfig[keyof IntrospectConfig]; -// pg rejects a refused `localhost` connection (IPv4 and IPv6) with an -// AggregateError whose own message is empty. -const message = (cause: unknown): string => { - if (!(cause instanceof Error)) return String(cause); - if (cause.message) return cause.message; - if (cause instanceof AggregateError && cause.errors.length > 0) - return Array.from(new Set(cause.errors.map(message))).join("; "); - const code = "code" in cause ? cause.code : undefined; - return code === undefined || code === null ? cause.name : String(code); +const isString = (option: unknown): option is string => + typeof option === "string"; +const isStrings = (option: unknown): option is readonly string[] => + Array.isArray(option) && option.every(isString); +const isFunction = (option: unknown): option is Option => + typeof option === "function"; +const isColumnTypes = (option: unknown): option is Option => + typeof option === "function" || + (typeof option === "object" && + option !== null && + !Array.isArray(option) && + Object.values(option).every(isString)); +const isBoolean = (option: unknown): option is boolean => + typeof option === "boolean"; + +const OPTIONS: Readonly< + Record option is Option> +> = { + url: isString, + out: isString, + schemas: isStrings, + exclude: isStrings, + viewNotNull: isFunction, + typeName: isString, + importFrom: isString, + imports: isStrings, + columnTypes: isColumnTypes, + column: isFunction, + comments: isBoolean, }; -const CONFIG_KEYS = new Set([ - "url", - "out", - "schemas", - "exclude", - "viewNotNull", - "typeName", - "importFrom", - "imports", - "columnTypes", - "column", - "comments", -]); - -async function pgConnect( - url: string, -): Promise }> { - if (!URL.canParse(url) && !url.startsWith("/")) - throw new Error( - `invalid connection URL ${JSON.stringify(url)} (expected postgres://user:password@host:port/database)`, - ); - let Client: typeof import("pg").Client; - try { - ({ Client } = (await import("pg")).default); - } catch { - throw new Error('the "pg" package is not installed: npm install pg'); - } - const client = new Client({ connectionString: url }); - await client.connect(); - return client; -} - -const isStrings = (option: unknown): option is readonly string[] => - Array.isArray(option) && option.every((s) => typeof s === "string"); +const isKey = (key: string): key is keyof IntrospectConfig => + Object.hasOwn(OPTIONS, key); -/** Whether `option` is a valid value of the configuration key `key`. */ -function isOption( - key: string, - option: unknown, -): option is IntrospectConfig[keyof IntrospectConfig] { - if (option === undefined) return true; - switch (key) { - case "url": - case "out": - case "typeName": - case "importFrom": - return typeof option === "string"; - case "schemas": - case "exclude": - case "imports": - return isStrings(option); - case "viewNotNull": - case "column": - return typeof option === "function"; - case "columnTypes": - return ( - typeof option === "function" || - (typeof option === "object" && - option !== null && - !Array.isArray(option) && - Object.values(option).every((type) => typeof type === "string")) - ); - case "comments": - return typeof option === "boolean"; - default: - return false; - } -} +const isOption = (key: string, option: unknown): option is Option => + option === undefined || (isKey(key) && OPTIONS[key](option)); const isIntrospectConfig = (config: unknown): config is IntrospectConfig => typeof config === "object" && @@ -130,7 +78,6 @@ const isIntrospectConfig = (config: unknown): config is IntrospectConfig => !Array.isArray(config) && Object.entries(config).every(([key, option]) => isOption(key, option)); -/** Imports the configuration module `file`, given on the command line as `name`. */ async function loadConfig( file: string, name: string, @@ -142,7 +89,7 @@ async function loadConfig( : undefined; if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) throw new Error(`${name} must default-export a configuration object`); - const unknown = Object.keys(loaded).filter((key) => !CONFIG_KEYS.has(key)); + const unknown = Object.keys(loaded).filter((key) => !isKey(key)); if (unknown.length > 0) throw new Error( `${name}: unknown option${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}`, @@ -156,9 +103,9 @@ async function loadConfig( ); } -const parse = (argv: readonly string[]) => +const parse = (args: string[]) => parseArgs({ - args: [...argv], + args, allowPositionals: true, options: { url: { type: "string" }, @@ -173,65 +120,47 @@ const parse = (argv: readonly string[]) => }, }); -export async function main( +/** + * `typesql introspect --dialect postgres`. `io` replaces the process's + * output, working directory and environment. + */ +export const main: ( argv: readonly string[], - io: Io = defaultIo(), -): Promise { - const values = introspectArgs(io, USAGE, () => parse(argv)); - if (typeof values === "number") return values; - + io?: CommandIo, +) => Promise = introspectCommand(USAGE, parse, async (values, io) => { + let config: IntrospectConfig = {}; + let configDir = io.cwd; + if (values.config) { + const file = resolve(io.cwd, values.config); + config = await loadConfig(file, values.config); + configDir = dirname(file); + } + const url = databaseUrl(values.url ?? config.url, io); + const options: IntrospectConfig = { + ...config, + ...(values.schema && { schemas: listOption(values.schema) }), + ...(values.exclude && { exclude: listOption(values.exclude) }), + ...(values["type-name"] !== undefined && { + typeName: values["type-name"], + }), + ...(values["import-from"] !== undefined && { + importFrom: values["import-from"], + }), + ...(values["no-comments"] && { comments: false }), + }; + const connection = await connectPostgres(url); try { - let config: IntrospectConfig = {}; - let configDir = io.cwd; - if (values.config) { - const file = resolve(io.cwd, values.config); - config = await loadConfig(file, values.config); - configDir = dirname(file); - } - const url = values.url ?? config.url ?? io.env.DATABASE_URL; - if (!url) { - io.stderr("typesql introspect: pass --url or set DATABASE_URL\n"); - return 2; - } - const list = (v: string[]) => - v.flatMap((s) => - s - .split(",") - .map((x) => x.trim()) - .filter(Boolean), - ); - const options: IntrospectConfig = { - ...config, - ...(values.schema && { schemas: list(values.schema) }), - ...(values.exclude && { exclude: list(values.exclude) }), - ...(values["type-name"] !== undefined && { - typeName: values["type-name"], - }), - ...(values["import-from"] !== undefined && { - importFrom: values["import-from"], - }), - ...(values["no-comments"] && { comments: false }), + return { + source: generateSource( + await introspect(connection.client, options), + options, + ), + // A config file's `out` is relative to the config file, like + // migrate's paths. + out: values.out ?? config.out, + base: values.out === undefined ? configDir : io.cwd, }; - const client = await (io.connect ?? pgConnect)(url); - let source: string; - try { - source = generateSource(await introspect(client, options), options); - } finally { - await client.end(); - } - // A config file's `out` is relative to the config file, like migrate's paths. - const out = values.out ?? config.out; - if (out === undefined) { - io.stdout(source); - } else { - const file = resolve(values.out === undefined ? configDir : io.cwd, out); - await mkdir(dirname(file), { recursive: true }); - await writeFile(file, source); - io.stderr(`typesql introspect: wrote ${out}\n`); - } - return 0; - } catch (err) { - io.stderr(`typesql introspect: ${message(err)}\n`); - return 1; + } finally { + await connection.close(); } -} +}); diff --git a/packages/pg/src/introspect/emit.ts b/packages/pg/src/introspect/emit.ts index 1a9fd5b1..8c14195e 100644 --- a/packages/pg/src/introspect/emit.ts +++ b/packages/pg/src/introspect/emit.ts @@ -1,7 +1,9 @@ import { - colSource, + doc, propertyKey as key, + RESERVED_KEYS, schemaMembers, + schemaWriter, } from "@typesql/core/adapter"; import type { IntrospectedColumn, @@ -28,20 +30,6 @@ export interface GenerateOptions { const str = (s: string): string => JSON.stringify(s); -function doc(lines: readonly string[], indent: string): string { - const text = lines - .flatMap((l) => l.split("\n")) - .map((l) => l.replace(/\*\//g, "*\\/").trimEnd()); - if (text.length === 0) return ""; - if (text.length === 1) return `${indent}/** ${text[0]} */\n`; - return `${indent}/**\n${text.map((l) => `${indent} *${l ? ` ${l}` : ""}`).join("\n")}\n${indent} */\n`; -} - -const flagsOf = (c: IntrospectedColumn): string[] => [ - ...(c.nullable ? ["nullable: true"] : []), - ...(c.hasDefault ? ["hasDefault: true"] : []), -]; - function columnNotes(c: IntrospectedColumn): string[] { const notes: string[] = []; if (c.comment) notes.push(c.comment); @@ -63,10 +51,9 @@ export function generateSource( db: IntrospectedDatabase, options: GenerateOptions = {}, ): string { - const typeName = options.typeName ?? "DB"; const comments = options.comments ?? true; const columnTypes = options.columnTypes ?? {}; - const used = new Set(); + const write = schemaWriter("postgres"); const usedKeys = new Set(); const customType = ( @@ -93,32 +80,22 @@ export function generateSource( return ts; }; - const jsonDescriptor = (col: IntrospectedColumn, ts: string): string => { - const flags = flagsOf(col); - const flagSource = flags.length > 0 ? `{ ${flags.join("; ")} }` : "{}"; - used.add("Json"); - if (col.type === "json") return `Json<${ts}, ${flagSource}, "json">`; - return flags.length > 0 ? `Json<${ts}, ${flagSource}>` : `Json<${ts}>`; - }; - const relation = (rel: IntrospectedRelation, indent: string): string => { const isView = rel.kind === "view" || rel.kind === "materialized view"; const cols = rel.columns.map((c) => { let descriptor = options.column?.(c, rel); if (descriptor === undefined) { + const flags = { nullable: c.nullable, hasDefault: c.hasDefault }; const custom = customType(rel, c); - if (custom !== undefined) descriptor = jsonDescriptor(c, custom); - else { - used.add("Col"); - descriptor = colSource(str(c.type), c.nullable, c.hasDefault); - } + descriptor = + custom === undefined + ? write.col(c.type, flags) + : write.typed(c.type, custom, flags); } return `${comments ? doc(columnNotes(c), `${indent} `) : ""}${indent} ${key(c.name)}: ${descriptor};`; }); const body = `{\n${cols.join("\n")}\n${indent}}`; - if (!isView) return body; - used.add("View"); - return `View<${body}>`; + return isView ? write.view(body) : body; }; const relDoc = (rel: IntrospectedRelation, indent: string): string => { @@ -133,7 +110,7 @@ export function generateSource( const topLevel = new Set( db.relations.filter((r) => r.schema === "public").map((r) => r.name), ); - for (const reserved of ["$types", "$functions"]) + for (const reserved of RESERVED_KEYS) if ( topLevel.has(reserved) || db.relations.some((r) => r.schema === reserved) @@ -182,26 +159,18 @@ export function generateSource( lines.push(` $types: {\n${entries.join("\n")}\n };`); } - const helpers = ["Col", "Json", "View"].filter((h) => used.has(h)); - const out = [ - `// Generated by \`typesql introspect\` from PostgreSQL ${db.version.split(/[.\s]/)[0]}. Do not edit.`, - ...(helpers.length > 0 - ? [ - `import type { ${helpers.join(", ")} } from ${str(options.importFrom ?? "@typesql/core/postgres")};`, - ] - : []), - ...(options.imports ?? []), - "", - `export interface ${key(typeName)} {`, - ...lines, - "}", - "", - ]; + const source = write.module({ + header: `Generated by \`typesql introspect\` from PostgreSQL ${db.version.split(/[.\s]/)[0]}. Do not edit.`, + members: lines, + typeName: options.typeName, + importFrom: options.importFrom, + imports: options.imports, + }); const unknownKeys = Object.keys(columnTypes).filter((k) => !usedKeys.has(k)); if (unknownKeys.length > 0 && options.column === undefined) { throw new Error( `columnTypes: no column ${unknownKeys.map((k) => JSON.stringify(k)).join(", ")} (keys are "table.column" for public tables, else "schema.table.column")`, ); } - return out.join("\n"); + return source; } diff --git a/packages/pg/src/type-map.ts b/packages/pg/src/type-map.ts index 726ee320..0f89a724 100644 --- a/packages/pg/src/type-map.ts +++ b/packages/pg/src/type-map.ts @@ -1,30 +1,14 @@ import type { DefaultParamTypeMap, - DefaultTypeMap, + NodePostgresTypeMap, } from "@typesql/core/postgres"; export type PgInterval = PgTypeMap["interval"]; -export interface PgTypeMap - extends Omit { +/** node-postgres's defaults, with `bytea` as the `Buffer` it is. */ +export interface PgTypeMap extends Omit { $params: PgParamTypeMap; bytea: Buffer; - interval: { - years?: number; - months?: number; - days?: number; - hours?: number; - minutes?: number; - seconds?: number; - milliseconds?: number; - toPostgres(): string; - toISO(): string; - toISOString(): string; - }; - record: string; - void: string; - "aclitem[]": string; - "char[]": string; } export interface PgParamTypeMap extends DefaultParamTypeMap {} diff --git a/packages/postgres/src/client.ts b/packages/postgres/src/client.ts index d302de4d..f4fc4cf0 100644 --- a/packages/postgres/src/client.ts +++ b/packages/postgres/src/client.ts @@ -61,8 +61,9 @@ export function executor(sql: PostgresJsQueryable): Executor { async function transaction( sql: PostgresJsQueryable, - depth: number, + _depth: number, fn: (tx: PostgresJsQueryable) => Promise, + savepoint: string | undefined, ): Promise { if (canSavepoint(sql)) return sql.savepoint(fn); if (canBegin(sql)) return sql.begin(fn); @@ -71,7 +72,7 @@ async function transaction( return runTransaction( (statement) => sql.unsafe(statement), () => fn(sql), - depth > 0 ? { savepoint: `typesql_${depth}` } : {}, + { savepoint }, ); throw new Error( "typesql: sql.transaction() needs a postgres.js Sql, ReservedSql or TransactionSql (with begin, savepoint or release)", diff --git a/packages/prisma/src/client.ts b/packages/prisma/src/client.ts index ef2e2f47..603b9634 100644 --- a/packages/prisma/src/client.ts +++ b/packages/prisma/src/client.ts @@ -1,12 +1,14 @@ import type { PrismaPromise } from "@prisma/client/runtime/client"; import type { Executor } from "@typesql/core"; import { - JsonParam, - mysqlPositional, - positional, + type DialectFamily, + dialectFamily, + familyStatement, + jsonText, runTransaction, type SqlDriver, } from "@typesql/core/adapter"; +import { providerDialect } from "./provider.js"; export interface PrismaRawClient { $queryRawUnsafe( @@ -17,19 +19,26 @@ export interface PrismaRawClient { $transaction?(fn: (tx: PrismaRawClient) => Promise): Promise; } -// Prisma sends a JS array as a Postgres array and a string as text, so send -// every sql.json() value as its JSON.stringify string. -const jsonText = (value: V): V | string => - value instanceof JsonParam ? JSON.stringify(value.value) : value; - -type DialectName = "postgres" | "sqlite" | "mysql" | "mariadb"; +const providerOf = (prisma: PrismaRawClient): DialectFamily => + providerDialect( + "_activeProvider" in prisma && typeof prisma._activeProvider === "string" + ? prisma._activeProvider + : undefined, + ); -/** The dialect of the client's active provider; MariaDB is Prisma's `mysql`. */ -const providerOf = (prisma: PrismaRawClient): DialectName => { - const provider = - "_activeProvider" in prisma ? prisma._activeProvider : undefined; - return provider === "sqlite" || provider === "mysql" ? provider : "postgres"; -}; +// Prisma sends a JS array as a Postgres array and a string as text, so +// sql.json() values are sent as JSON text; its MySQL adapter takes them as +// mysqlValue converts them. +const compile = ( + family: DialectFamily, + text: string, + values: readonly unknown[], +) => + familyStatement[family]( + text, + values, + family === "mysql" ? undefined : jsonText, + ); /** * An executor on `prisma`. SQLite's placeholders are used when `dialect` is @@ -38,27 +47,19 @@ const providerOf = (prisma: PrismaRawClient): DialectName => { */ export function executor( prisma: PrismaRawClient, - options: { readonly dialect?: DialectName } = {}, + options: { readonly dialect?: DialectFamily | "mariadb" } = {}, ): Executor { - const dialect = options.dialect ?? providerOf(prisma); - const compile = - dialect === "sqlite" - ? (text: string, values: readonly unknown[]) => - positional(text, values, jsonText) - : dialect === "mysql" || dialect === "mariadb" - ? mysqlPositional - : (text: string, values: readonly unknown[]) => ({ - text, - values: values.map(jsonText), - }); + const family = options.dialect + ? dialectFamily(options.dialect) + : providerOf(prisma); return Object.assign( (text: string, values: readonly unknown[]) => { - const q = compile(text, values); + const q = compile(family, text, values); return prisma.$queryRawUnsafe(q.text, ...q.values); }, { count: (text: string, values: readonly unknown[]) => { - const q = compile(text, values); + const q = compile(family, text, values); return prisma.$executeRawUnsafe(q.text, ...q.values); }, }, @@ -67,28 +68,25 @@ export function executor( function transaction( prisma: PrismaRawClient, - depth: number, + _depth: number, fn: (tx: PrismaRawClient) => Promise, + savepoint: string | undefined, ): Promise { - if (prisma.$transaction) return prisma.$transaction(fn); + if (savepoint === undefined && prisma.$transaction) + return prisma.$transaction(fn); return runTransaction( (statement) => prisma.$executeRawUnsafe(statement), () => fn(prisma), - { savepoint: `typesql_${depth + 1}` }, + { savepoint }, ); } -export const driver: SqlDriver = { - executor: (prisma) => executor(prisma), - transaction, -}; - -export const sqliteDriver: SqlDriver = { - executor: (prisma) => executor(prisma, { dialect: "sqlite" }), - transaction, -}; - -export const mysqlDriver: SqlDriver = { - executor: (prisma) => executor(prisma, { dialect: "mysql" }), +/** The driver for a `sql` of the dialect family `dialect`. */ +export const driverOf = ( + dialect: DialectFamily, +): SqlDriver => ({ + executor: (prisma) => executor(prisma, { dialect }), transaction, -}; + // An interactive transaction's client has no $transaction. + inTransaction: (prisma) => !prisma.$transaction, +}); diff --git a/packages/prisma/src/from-prisma-mysql.ts b/packages/prisma/src/from-prisma-mysql.ts index 56eb48ba..4497335c 100644 --- a/packages/prisma/src/from-prisma-mysql.ts +++ b/packages/prisma/src/from-prisma-mysql.ts @@ -1,43 +1,25 @@ -import type { Typed } from "@typesql/core/mysql"; +import type { Col, Typed } from "@typesql/core/mysql"; import type { - Column, + ColumnFlags, ColumnName, Fields, Get, - Models, NullableKeys, OptionalCreateKeys, - PrismaOverrides, - PrismaTypeMapLike, + OverrideType, Scalars, - TableName, TypeName, -} from "./from-prisma.js"; +} from "./model.js"; +import type { PRISMA_SCALARS } from "./scalars.js"; -/** Prisma's default MySQL column types; native types (`@db.Text`, …) need an override. */ -export interface PrismaScalarToMysql { - Int: "int"; - BigInt: "bigint"; - Float: "double"; - Decimal: "decimal"; - String: "varchar"; - Boolean: "tinyint"; - DateTime: "datetime"; - Json: "json"; - Bytes: "longblob"; -} +export type PrismaScalarToMysql = (typeof PRISMA_SCALARS)["mysql"]; +// An enum field is an ENUM column of its values. type MysqlOf = T extends keyof PrismaScalarToMysql ? PrismaScalarToMysql[T] : Typed<"enum", Exclude>; -type ColumnType = O extends string - ? O - : O extends { readonly type: infer X extends string } - ? X - : MysqlOf; - -type ModelColumns< +export type MysqlColumns< M, MO, FS = Fields, @@ -45,24 +27,12 @@ type ModelColumns< Null = NullableKeys, Opt = OptionalCreateKeys, > = { - -readonly [F in keyof FS as ColumnName>]: Column< - ColumnType, Get, Get>, - F extends Null ? true : false, - F extends Opt ? true : false, - Get - >; -}; - -type TableOf = M extends { operations: { createMany: unknown } } - ? ModelColumns - : { readonly $view: ModelColumns }; - -export type FromPrismaMysql< - TM extends PrismaTypeMapLike, - Overrides extends PrismaOverrides = {}, -> = { - -readonly [N in keyof Models as TableName>]: TableOf< - Models[N], - Get + -readonly [F in keyof FS as ColumnName>]: Col< + OverrideType, MysqlOf, Get>>, + ColumnFlags< + F extends Null ? true : false, + F extends Opt ? true : false, + Get + > >; }; diff --git a/packages/prisma/src/from-prisma-pg.ts b/packages/prisma/src/from-prisma-pg.ts new file mode 100644 index 00000000..c63f9a43 --- /dev/null +++ b/packages/prisma/src/from-prisma-pg.ts @@ -0,0 +1,82 @@ +import type { Col } from "@typesql/core/postgres"; +import type { + ColumnFlags, + ColumnName, + Fields, + Get, + Models, + NullableKeys, + OptionalCreateKeys, + OverrideType, + PrismaTypeMapLike, + Scalars, + TypeName, +} from "./model.js"; +import type { PRISMA_SCALARS } from "./scalars.js"; + +export type PrismaScalarToPg = (typeof PRISMA_SCALARS)["postgres"]; + +// An enum field's type is its enum's name. +type PgOf = T extends keyof PrismaScalarToPg + ? PrismaScalarToPg[T] + : T; +type ElemName = T extends `${infer E}[]` ? E : T; +type IsList = T extends `${string}[]` ? true : false; + +type ListOf = + IsList extends true ? `${E}[]` : E; + +export type PgColumns< + M, + MO, + FS = Fields, + Null = NullableKeys, + Opt = OptionalCreateKeys, +> = { + -readonly [F in keyof FS as ColumnName>]: Col< + ListOf< + TypeName, + OverrideType, PgOf>>> + >, + ColumnFlags< + IsList> extends true + ? true + : F extends Null + ? true + : false, + F extends Opt ? true : false, + Get + > + >; +}; + +type EnumEntries = { + [N in keyof Models]: ModelEnums< + Fields[N]>, + Scalars[N]> + >; +}[keyof Models]; +type ModelEnums = { + [F in keyof FS]: ElemName> extends infer E extends string + ? E extends keyof PrismaScalarToPg + ? never + : { + name: E; + values: Exclude, null> extends infer V + ? V extends readonly (infer X)[] + ? X + : V + : never; + } + : never; +}[keyof FS]; + +/** The enums of a PostgreSQL schema, with the values `$types` overrides. */ +export type Enums = { + -readonly [E in EnumEntries as E["name"]]: E["name"] extends keyof Get< + O, + "$types" + > + ? Get[E["name"]] + : E["values"]; +}; diff --git a/packages/prisma/src/from-prisma-sqlite.ts b/packages/prisma/src/from-prisma-sqlite.ts index 64879d09..a3d9ed96 100644 --- a/packages/prisma/src/from-prisma-sqlite.ts +++ b/packages/prisma/src/from-prisma-sqlite.ts @@ -1,42 +1,25 @@ -import type { Typed } from "@typesql/core/sqlite"; +import type { Col, Typed } from "@typesql/core/sqlite"; import type { - Column, + ColumnFlags, ColumnName, Fields, Get, - Models, NullableKeys, OptionalCreateKeys, - PrismaOverrides, - PrismaTypeMapLike, + OverrideType, Scalars, - TableName, TypeName, -} from "./from-prisma.js"; +} from "./model.js"; +import type { PRISMA_SCALARS } from "./scalars.js"; -export interface PrismaScalarToSqlite { - Int: "integer"; - BigInt: "bigint"; - Float: "real"; - Decimal: "decimal"; - String: "text"; - Boolean: "boolean"; - DateTime: "datetime"; - Json: "jsonb"; - Bytes: "blob"; -} +export type PrismaScalarToSqlite = (typeof PRISMA_SCALARS)["sqlite"]; +// SQLite has no enums: an enum field is text of its values. type SqliteOf = T extends keyof PrismaScalarToSqlite ? PrismaScalarToSqlite[T] : Typed<"text", Exclude>; -type ColumnType = O extends string - ? O - : O extends { readonly type: infer X extends string } - ? X - : SqliteOf; - -type ModelColumns< +export type SqliteColumns< M, MO, FS = Fields, @@ -44,24 +27,12 @@ type ModelColumns< Null = NullableKeys, Opt = OptionalCreateKeys, > = { - -readonly [F in keyof FS as ColumnName>]: Column< - ColumnType, Get, Get>, - F extends Null ? true : false, - F extends Opt ? true : false, - Get - >; -}; - -type TableOf = M extends { operations: { createMany: unknown } } - ? ModelColumns - : { readonly $view: ModelColumns }; - -export type FromPrismaSqlite< - TM extends PrismaTypeMapLike, - Overrides extends PrismaOverrides = {}, -> = { - -readonly [N in keyof Models as TableName>]: TableOf< - Models[N], - Get + -readonly [F in keyof FS as ColumnName>]: Col< + OverrideType, SqliteOf, Get>>, + ColumnFlags< + F extends Null ? true : false, + F extends Opt ? true : false, + Get + > >; }; diff --git a/packages/prisma/src/from-prisma.ts b/packages/prisma/src/from-prisma.ts index 1e6ee3e6..7f63415f 100644 --- a/packages/prisma/src/from-prisma.ts +++ b/packages/prisma/src/from-prisma.ts @@ -1,151 +1,62 @@ -export interface PrismaScalarToPg { - Int: "int4"; - BigInt: "int8"; - Float: "float8"; - Decimal: "numeric"; - String: "text"; - Boolean: "bool"; - DateTime: "timestamp"; - Json: "jsonb"; - Bytes: "bytea"; -} - -export interface PrismaTypeMapLike { - readonly model: { - readonly [model: string]: { - readonly payload: { readonly scalars: object }; - readonly fields: object; - readonly operations: object; - }; - }; -} - -export type FieldOverride = - | string - | { - readonly column?: string; - readonly type?: string; - readonly nullable?: boolean; - readonly default?: boolean; - }; - -export type ModelOverride = { - readonly $table?: string; - readonly $schema?: string; -} & { readonly [F in keyof Fields]?: FieldOverride }; - -type KnownKeys = { - readonly [K in Exclude]: never; -}; - -export type PrismaOverrides< - TM extends PrismaTypeMapLike = PrismaTypeMapLike, - O = {}, -> = { - readonly [M in keyof TM["model"]]?: ModelOverride & - KnownKeys, keyof TM["model"][M]["fields"] | "$table" | "$schema">; -} & { readonly $types?: { readonly [name: string]: string } } & KnownKeys< - O, - keyof TM["model"] | "$types" - >; - -export type Models = TM["model"]; -export type Scalars = M extends { payload: { scalars: infer S } } - ? S - : never; -export type Fields = M extends { fields: infer F } ? F : never; -export type TypeName = R extends { - readonly typeName: infer T extends string; -} - ? T - : never; -export type Get = K extends keyof O ? O[K] : undefined; - -export type OptionalCreateKeys = M extends { - operations: { createMany: { args: { data: infer D } } }; -} - ? Exclude extends infer I - ? { [K in keyof I]-?: {} extends Pick ? K : never }[keyof I] - : never +import type { MysqlColumns } from "./from-prisma-mysql.js"; +import type { Enums, PgColumns } from "./from-prisma-pg.js"; +import type { SqliteColumns } from "./from-prisma-sqlite.js"; +import type { + Get, + Models, + PrismaOverrides, + PrismaTypeMapLike, + TableName, +} from "./model.js"; + +/** + * The PostgreSQL schema of a Prisma client's `Prisma.TypeMap`: a model with + * a `$schema` override is a table of that namespace, and enums are types. + */ +export type FromPrisma< + TM extends PrismaTypeMapLike, + Overrides extends PrismaOverrides = {}, +> = TopLevel & + Namespaced & { $types: Enums } extends infer DB + ? { [K in keyof DB]: DB[K] } : never; -export type NullableKeys = M extends { - operations: { findMany: { args: { orderBy?: infer O } } }; -} - ? Exclude extends infer I - ? { [K in keyof I]-?: { sort: "asc" } extends I[K] ? K : never }[keyof I] - : never - : never; +export type FromPrismaMysql< + TM extends PrismaTypeMapLike, + Overrides extends PrismaOverrides = {}, +> = Tables; -type PgOf = T extends keyof PrismaScalarToPg - ? PrismaScalarToPg[T] - : T; -type ElemName = T extends `${infer E}[]` ? E : T; -type IsList = T extends `${string}[]` ? true : false; +export type FromPrismaSqlite< + TM extends PrismaTypeMapLike, + Overrides extends PrismaOverrides = {}, +> = Tables; -export type ColumnName = O extends { - readonly column: infer C extends string; -} - ? C - : F; -type ListOf = - IsList extends true ? `${E}[]` : E; -type ColumnType = ListOf< - T, - O extends string - ? O - : O extends { readonly type: infer X extends string } - ? X - : PgOf> ->; +type Dialect = "postgres" | "mysql" | "sqlite"; -export type Column< - Type, - Nullable extends boolean, - Optional extends boolean, - O, -> = ( - O extends { readonly nullable: infer N extends boolean } - ? N - : Nullable -) extends infer N extends boolean - ? { - readonly type: Type; - readonly nullable: N; - readonly hasDefault: O extends { - readonly default: infer D extends boolean; - } - ? D - : N extends true - ? false - : Optional; - } - : never; +// Each dialect's columns are a mapped type of their own: one whose column +// type depends on the dialect costs more to check. +type Columns = D extends "mysql" + ? MysqlColumns + : D extends "sqlite" + ? SqliteColumns + : PgColumns; -type ModelColumns< - M, - MO, - FS = Fields, - Null = NullableKeys, - Opt = OptionalCreateKeys, -> = { - -readonly [F in keyof FS as ColumnName>]: Column< - ColumnType, Get>, - IsList> extends true ? true : F extends Null ? true : false, - F extends Opt ? true : false, - Get +// A model Prisma can't create rows of is a view. +type TableOf = M extends { + operations: { createMany: unknown }; +} + ? Columns + : { readonly $view: Columns }; + +/** Every model as a top-level table. */ +type Tables = { + -readonly [N in keyof Models as TableName>]: TableOf< + Models[N], + Get, + D >; }; -type TableOf = M extends { operations: { createMany: unknown } } - ? ModelColumns - : { readonly $view: ModelColumns }; - -export type TableName = MO extends { - readonly $table: infer T extends string; -} - ? T - : N & string; type SchemaName = MO extends { readonly $schema: infer S extends string } ? S extends "public" ? never @@ -155,7 +66,7 @@ type SchemaName = MO extends { readonly $schema: infer S extends string } type TopLevel = { -readonly [N in keyof Models as [SchemaName>] extends [never] ? TableName> - : never]: TableOf[N], Get>; + : never]: TableOf[N], Get, "postgres">; }; type Namespaced = { -readonly [S in { @@ -163,43 +74,6 @@ type Namespaced = { }[keyof Models]]: { -readonly [N in keyof Models as SchemaName> extends S ? TableName> - : never]: TableOf[N], Get>; + : never]: TableOf[N], Get, "postgres">; }; }; - -type EnumEntries = { - [N in keyof Models]: ModelEnums< - Fields[N]>, - Scalars[N]> - >; -}[keyof Models]; -type ModelEnums = { - [F in keyof FS]: ElemName> extends infer E extends string - ? E extends keyof PrismaScalarToPg - ? never - : { - name: E; - values: Exclude, null> extends infer V - ? V extends readonly (infer X)[] - ? X - : V - : never; - } - : never; -}[keyof FS]; -type Enums = { - -readonly [E in EnumEntries as E["name"]]: E["name"] extends keyof Get< - O, - "$types" - > - ? Get[E["name"]] - : E["values"]; -}; - -export type FromPrisma< - TM extends PrismaTypeMapLike, - Overrides extends PrismaOverrides = {}, -> = TopLevel & - Namespaced & { $types: Enums } extends infer DB - ? { [K in keyof DB]: DB[K] } - : never; diff --git a/packages/prisma/src/generator/emit.ts b/packages/prisma/src/generator/emit.ts index 620f4fb8..2bfe921d 100644 --- a/packages/prisma/src/generator/emit.ts +++ b/packages/prisma/src/generator/emit.ts @@ -1,8 +1,10 @@ import { - colSource, + doc, propertyKey as key, schemaMembers, + schemaWriter, } from "@typesql/core/adapter"; +import { PRISMA_SCALARS } from "../scalars.js"; import type { DmmfDatamodel, DmmfEnum, DmmfField, DmmfModel } from "./dmmf.js"; export interface EmitOptions { @@ -25,41 +27,11 @@ export interface SchemaField { }; } -const SCALAR_TYPES: ReadonlyMap = new Map([ - ["Int", "int4"], - ["BigInt", "int8"], - ["Float", "float8"], - ["Decimal", "numeric"], - ["String", "text"], - ["Boolean", "bool"], - ["DateTime", "timestamp"], - ["Json", "jsonb"], - ["Bytes", "bytea"], -]); - -const SQLITE_SCALAR_TYPES: ReadonlyMap = new Map([ - ["Int", "integer"], - ["BigInt", "bigint"], - ["Float", "real"], - ["Decimal", "decimal"], - ["String", "text"], - ["Boolean", "boolean"], - ["DateTime", "datetime"], - ["Json", "jsonb"], - ["Bytes", "blob"], -]); - -const MYSQL_SCALAR_TYPES: ReadonlyMap = new Map([ - ["Int", "int"], - ["BigInt", "bigint"], - ["Float", "double"], - ["Decimal", "decimal"], - ["String", "varchar"], - ["Boolean", "tinyint"], - ["DateTime", "datetime"], - ["Json", "json"], - ["Bytes", "longblob"], -]); +const scalarOf = ( + dialect: keyof typeof PRISMA_SCALARS, + type: string, +): string | undefined => + Object.entries(PRISMA_SCALARS[dialect]).find(([name]) => name === type)?.[1]; const MYSQL_NATIVE_TYPES: ReadonlyMap = new Map([ ["TinyInt", "tinyint"], @@ -176,7 +148,7 @@ function pgTypeOf( const native = field.nativeType?.[0]; if (native !== undefined) return NATIVE_TYPES.get(native) ?? native.toLowerCase(); - return SCALAR_TYPES.get(field.type) ?? field.type.toLowerCase(); + return scalarOf("postgres", field.type) ?? field.type.toLowerCase(); } function hasDbDefault(field: DmmfField): boolean { @@ -188,49 +160,23 @@ function hasDbDefault(field: DmmfField): boolean { } const str = (s: string): string => JSON.stringify(s); -const docText = (s: string): string => s.replace(/\*\//g, "*\\/"); - -function doc(lines: readonly string[], indent: string): string { - if (lines.length === 0) return ""; - if (lines.length === 1) return `${indent}/** ${docText(lines[0]!)} */\n`; - return `${indent}/**\n${lines.map((l) => `${indent} * ${docText(l)}`).join("\n")}\n${indent} */\n`; -} -/** `colSource`, with MySQL's `autoIncrement` flag. */ -function column( - type: string, - nullable: boolean, - hasDefault: boolean, - autoIncrement: boolean, -): string { - if (!autoIncrement) return colSource(type, nullable, hasDefault); - const flags = [ - ...(nullable ? ["nullable: true"] : []), - "hasDefault: true", - "autoIncrement: true", - ]; - return `Col<${type}, { ${flags.join("; ")} }>`; +/** The union of an enum's database values. */ +function enumValues(field: DmmfField, enums: readonly DmmfEnum[]): string { + const e = enums.find((x) => x.name === field.type); + const values = (e?.values ?? []).map((v) => str(v.dbName ?? v.name)); + return values.join(" | ") || "never"; } -function sqliteTypeOf(field: DmmfField, enums: readonly DmmfEnum[]): string { - if (field.kind === "enum") { - const e = enums.find((x) => x.name === field.type); - const values = (e?.values ?? []).map((v) => str(v.dbName ?? v.name)); - return `Typed<"text", ${values.join(" | ") || "never"}>`; - } - return str(SQLITE_SCALAR_TYPES.get(field.type) ?? field.type.toLowerCase()); +function sqliteTypeOf(field: DmmfField): string { + return scalarOf("sqlite", field.type) ?? field.type.toLowerCase(); } -function mysqlTypeOf(field: DmmfField, enums: readonly DmmfEnum[]): string { - if (field.kind === "enum") { - const e = enums.find((x) => x.name === field.type); - const values = (e?.values ?? []).map((v) => str(v.dbName ?? v.name)); - return `Typed<"enum", ${values.join(" | ") || "never"}>`; - } +function mysqlTypeOf(field: DmmfField): string { const native = field.nativeType?.[0]; if (native !== undefined) - return str(MYSQL_NATIVE_TYPES.get(native) ?? native.toLowerCase()); - return str(MYSQL_SCALAR_TYPES.get(field.type) ?? field.type.toLowerCase()); + return MYSQL_NATIVE_TYPES.get(native) ?? native.toLowerCase(); + return scalarOf("mysql", field.type) ?? field.type.toLowerCase(); } const isAutoIncrement = (field: DmmfField): boolean => { @@ -248,14 +194,14 @@ export function generateSource( datamodel: DmmfDatamodel, options: EmitOptions = {}, ): string { - const typeName = options.typeName ?? "DB"; const views = new Set(options.views); const enums = new Map( datamodel.enums.map((e) => [e.name, e.dbName ?? e.name]), ); - const used = new Set(); - const sqlite = options.dialect === "sqlite"; - const mysql = options.dialect === "mysql"; + const dialect = options.dialect ?? "postgres"; + const write = schemaWriter(dialect); + const sqlite = dialect === "sqlite"; + const mysql = dialect === "mysql"; const table = (model: DmmfModel, indent: string): string => { const isView = views.has(model.name); @@ -273,22 +219,33 @@ export function generateSource( if (!("kind" in f)) { const u = f.unsupported; if (u && !sqlite) { - used.add("Col"); cols.push( - `${doc(u.dbName ? [`Prisma field \`${f.name}\`: Unsupported.`] : ["Prisma `Unsupported` field."], `${indent} `)}${indent} ${key(u.dbName ?? f.name)}: ${colSource(str(u.type), u.nullable, !isView && u.hasDefault)};`, + `${doc(u.dbName ? [`Prisma field \`${f.name}\`: Unsupported.`] : ["Prisma `Unsupported` field."], `${indent} `)}${indent} ${key(u.dbName ?? f.name)}: ${write.col(u.type, { nullable: u.nullable, hasDefault: !isView && u.hasDefault })};`, ); } continue; } if (f.kind !== "scalar" && f.kind !== "enum") continue; - const nullable = f.isList || !f.isRequired; - used.add("Col"); - const type = sqlite - ? sqliteTypeOf(f, datamodel.enums) - : mysql - ? mysqlTypeOf(f, datamodel.enums) - : str(pgTypeOf(f, enums) + (f.isList ? "[]" : "")); - if ((sqlite || mysql) && f.kind === "enum") used.add("Typed"); + const flags = { + nullable: f.isList || !f.isRequired, + hasDefault: !isView && hasDbDefault(f), + autoIncrement: mysql && !isView && isAutoIncrement(f), + }; + const column = + (sqlite || mysql) && f.kind === "enum" + ? write.typed( + sqlite ? "text" : "enum", + enumValues(f, datamodel.enums), + flags, + ) + : write.col( + sqlite + ? sqliteTypeOf(f) + : mysql + ? mysqlTypeOf(f) + : pgTypeOf(f, enums) + (f.isList ? "[]" : ""), + flags, + ); const notes = [ ...(f.dbName && f.dbName !== f.name ? [`Prisma field \`${f.name}\`.`] @@ -299,13 +256,11 @@ export function generateSource( ...(f.documentation ? f.documentation.split("\n") : []), ]; cols.push( - `${doc(notes, `${indent} `)}${indent} ${key(f.dbName ?? f.name)}: ${column(type, nullable, !isView && hasDbDefault(f), mysql && !isView && isAutoIncrement(f))};`, + `${doc(notes, `${indent} `)}${indent} ${key(f.dbName ?? f.name)}: ${column};`, ); } const body = `{\n${cols.join("\n")}\n${indent}}`; - if (!isView) return body; - used.add("View"); - return `View<${body}>`; + return isView ? write.view(body) : body; }; const lines = schemaMembers( @@ -330,23 +285,10 @@ export function generateSource( lines.push(` $types: {\n${entries.join("\n")}\n };`); } - const imports = ["Col", "Typed", "View"].filter((h) => used.has(h)); - const from = - options.importFrom ?? - (sqlite - ? "@typesql/core/sqlite" - : mysql - ? "@typesql/core/mysql" - : "@typesql/core/postgres"); - return [ - "// Generated by @typesql/prisma from the Prisma schema. Do not edit.", - ...(imports.length > 0 - ? [`import type { ${imports.join(", ")} } from ${str(from)};`] - : []), - "", - `export interface ${typeName} {`, - ...lines, - "}", - "", - ].join("\n"); + return write.module({ + header: "Generated by @typesql/prisma from the Prisma schema. Do not edit.", + members: lines, + typeName: options.typeName, + importFrom: options.importFrom, + }); } diff --git a/packages/prisma/src/generator/index.ts b/packages/prisma/src/generator/index.ts index d7bd8dbe..8774f0fc 100644 --- a/packages/prisma/src/generator/index.ts +++ b/packages/prisma/src/generator/index.ts @@ -1,5 +1,6 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, extname, join } from "node:path"; +import { providerDialect } from "../provider.js"; import { generateSource, schemaFields, viewNames } from "./emit.js"; import type { GeneratorHandler, GeneratorOptions } from "./protocol.js"; @@ -14,23 +15,12 @@ function outputFile(output: string): string { } function sourceFor(options: GeneratorOptions): string { - const provider = - options.datasources[0]?.activeProvider ?? options.datasources[0]?.provider; - if ( - provider !== undefined && - provider !== "postgresql" && - provider !== "postgres" && - provider !== "sqlite" && - provider !== "mysql" - ) { - throw new Error( - `typesql-prisma: only the postgresql, sqlite and mysql providers are supported (got "${provider}")`, - ); - } + const dialect = providerDialect( + options.datasources[0]?.activeProvider ?? options.datasources[0]?.provider, + ); const { importFrom, typeName } = options.generator.config; return generateSource(options.dmmf.datamodel, { - dialect: - provider === "sqlite" || provider === "mysql" ? provider : "postgres", + dialect, importFrom: configString(importFrom), typeName: configString(typeName), views: viewNames(options.datamodel), diff --git a/packages/prisma/src/index.ts b/packages/prisma/src/index.ts index 8f5d68fc..5d747dc8 100644 --- a/packages/prisma/src/index.ts +++ b/packages/prisma/src/index.ts @@ -1,4 +1,4 @@ -import { createTransactionalSql } from "@typesql/core/adapter"; +import { createFamilySql } from "@typesql/core/adapter"; import type { MariaDbDialect, MariaDbTransactionalSql, @@ -10,15 +10,13 @@ import type { SqliteDialect, TransactionalSql as SqliteTransactionalSql, } from "@typesql/core/sqlite"; -import { - driver, - mysqlDriver, - type PrismaRawClient, - sqliteDriver, -} from "./client.js"; -import type { FromPrisma, PrismaTypeMapLike } from "./from-prisma.js"; -import type { FromPrismaMysql } from "./from-prisma-mysql.js"; -import type { FromPrismaSqlite } from "./from-prisma-sqlite.js"; +import { driverOf, type PrismaRawClient } from "./client.js"; +import type { + FromPrisma, + FromPrismaMysql, + FromPrismaSqlite, +} from "./from-prisma.js"; +import type { PrismaTypeMapLike } from "./model.js"; import type { PrismaMariaDbTypeMap, PrismaMysqlTypeMap, @@ -28,21 +26,19 @@ import type { PrismaTypeMap } from "./type-map.js"; export { executor, type PrismaRawClient } from "./client.js"; export type { - FieldOverride, FromPrisma, + FromPrismaMysql, + FromPrismaSqlite, +} from "./from-prisma.js"; +export type { PrismaScalarToMysql } from "./from-prisma-mysql.js"; +export type { PrismaScalarToPg } from "./from-prisma-pg.js"; +export type { PrismaScalarToSqlite } from "./from-prisma-sqlite.js"; +export type { + FieldOverride, ModelOverride, PrismaOverrides, - PrismaScalarToPg, PrismaTypeMapLike, -} from "./from-prisma.js"; -export type { - FromPrismaMysql, - PrismaScalarToMysql, -} from "./from-prisma-mysql.js"; -export type { - FromPrismaSqlite, - PrismaScalarToSqlite, -} from "./from-prisma-sqlite.js"; +} from "./model.js"; export type { PrismaMariaDbTypeMap, PrismaMysqlTypeMap, @@ -135,14 +131,5 @@ export function createSql( dialect: PostgresDialect | SqliteDialect | MySqlDialect | MariaDbDialect, prisma?: PrismaRawClient, ): object { - switch (dialect.name) { - case "sqlite": - return createTransactionalSql(dialect, prisma, sqliteDriver); - case "mysql": - return createTransactionalSql(dialect, prisma, mysqlDriver); - case "mariadb": - return createTransactionalSql(dialect, prisma, mysqlDriver); - default: - return createTransactionalSql(dialect, prisma, driver); - } + return createFamilySql(dialect, prisma, driverOf); } diff --git a/packages/prisma/src/model.ts b/packages/prisma/src/model.ts new file mode 100644 index 00000000..e1ef39f2 --- /dev/null +++ b/packages/prisma/src/model.ts @@ -0,0 +1,118 @@ +// What the conversions read of Prisma Client's types, in every dialect. + +export interface PrismaTypeMapLike { + readonly model: { + readonly [model: string]: { + readonly payload: { readonly scalars: object }; + readonly fields: object; + readonly operations: object; + }; + }; +} + +export type FieldOverride = + | string + | { + readonly column?: string; + readonly type?: string; + readonly nullable?: boolean; + readonly default?: boolean; + }; + +export type ModelOverride = { + readonly $table?: string; + readonly $schema?: string; +} & { readonly [F in keyof Fields]?: FieldOverride }; + +type KnownKeys = { + readonly [K in Exclude]: never; +}; + +export type PrismaOverrides< + TM extends PrismaTypeMapLike = PrismaTypeMapLike, + O = {}, +> = { + readonly [M in keyof TM["model"]]?: ModelOverride & + KnownKeys, keyof TM["model"][M]["fields"] | "$table" | "$schema">; +} & { readonly $types?: { readonly [name: string]: string } } & KnownKeys< + O, + keyof TM["model"] | "$types" + >; + +export type Models = TM["model"]; +export type Scalars = M extends { payload: { scalars: infer S } } + ? S + : never; +export type Fields = M extends { fields: infer F } ? F : never; +export type TypeName = R extends { + readonly typeName: infer T extends string; +} + ? T + : never; +export type Get = K extends keyof O ? O[K] : undefined; + +export type OptionalCreateKeys = M extends { + operations: { createMany: { args: { data: infer D } } }; +} + ? Exclude extends infer I + ? { [K in keyof I]-?: {} extends Pick ? K : never }[keyof I] + : never + : never; + +export type NullableKeys = M extends { + operations: { findMany: { args: { orderBy?: infer O } } }; +} + ? Exclude extends infer I + ? { [K in keyof I]-?: { sort: "asc" } extends I[K] ? K : never }[keyof I] + : never + : never; + +export type ColumnName = O extends { + readonly column: infer C extends string; +} + ? C + : F; + +export type OverrideType = O extends string + ? O + : O extends { readonly type: infer X extends string } + ? X + : Default; + +/** + * The options of a field's `Col`: nullable when it is, else with a default + * when it is optional in a create, unless its override says otherwise. + */ +export type ColumnFlags< + Nullable extends boolean, + Optional extends boolean, + O, +> = ( + O extends { readonly nullable: infer N extends boolean } + ? N + : Nullable +) extends infer N extends boolean + ? Flags< + N, + O extends { readonly default: infer D extends boolean } + ? D + : N extends true + ? false + : Optional + > + : never; + +// One of four flag objects, so that columns of a type share one `Col`. +type Flags = N extends true + ? H extends true + ? { nullable: true; hasDefault: true } + : { nullable: true } + : H extends true + ? { hasDefault: true } + : Record; + +export type TableName = MO extends { + readonly $table: infer T extends string; +} + ? T + : N & string; diff --git a/packages/prisma/src/provider.ts b/packages/prisma/src/provider.ts new file mode 100644 index 00000000..1f0e1843 --- /dev/null +++ b/packages/prisma/src/provider.ts @@ -0,0 +1,22 @@ +import type { DialectFamily } from "@typesql/core/adapter"; + +/** + * The dialect family of a Prisma datasource provider, PostgreSQL's when it + * isn't known. The postgresql, sqlite and mysql providers are supported + * (MariaDB is Prisma's mysql); any other throws. + */ +export function providerDialect(provider: string | undefined): DialectFamily { + switch (provider) { + case undefined: + case "postgresql": + case "postgres": + return "postgres"; + case "sqlite": + case "mysql": + return provider; + default: + throw new Error( + `@typesql/prisma: only the postgresql, sqlite and mysql providers are supported (got "${provider}")`, + ); + } +} diff --git a/packages/prisma/src/scalars.ts b/packages/prisma/src/scalars.ts new file mode 100644 index 00000000..70e50d02 --- /dev/null +++ b/packages/prisma/src/scalars.ts @@ -0,0 +1,41 @@ +// The column type of each Prisma scalar with no native type, per database: +// what the generator writes and what FromPrisma types a model with. + +export const PRISMA_SCALARS = { + postgres: { + Int: "int4", + BigInt: "int8", + Float: "float8", + Decimal: "numeric", + String: "text", + Boolean: "bool", + DateTime: "timestamp", + Json: "jsonb", + Bytes: "bytea", + }, + sqlite: { + Int: "integer", + BigInt: "bigint", + Float: "real", + Decimal: "decimal", + String: "text", + Boolean: "boolean", + DateTime: "datetime", + Json: "jsonb", + Bytes: "blob", + }, + // Native types (`@db.Text`, …) need an override. + mysql: { + Int: "int", + BigInt: "bigint", + Float: "double", + Decimal: "decimal", + String: "varchar", + Boolean: "tinyint", + DateTime: "datetime", + Json: "json", + Bytes: "longblob", + }, +} as const satisfies Readonly< + Record<"postgres" | "sqlite" | "mysql", Readonly>> +>; diff --git a/packages/sqlite/src/client.ts b/packages/sqlite/src/client.ts index fbd55696..8e1d41a9 100644 --- a/packages/sqlite/src/client.ts +++ b/packages/sqlite/src/client.ts @@ -3,7 +3,7 @@ import { paramNumbers, type SqlDriver, serialTransaction, - sqliteValue, + sqliteBindings, } from "@typesql/core/adapter"; export interface SqliteStatement { @@ -76,14 +76,7 @@ function prepared(db: SqliteDatabase, text: string, bigint: boolean): Prepared { } function bind(p: Prepared, values: readonly unknown[]): unknown[] { - if (p.names.length === 0) return []; - const params = Object.fromEntries( - p.names.map((n) => [ - p.bare ? String(n) : `$${n}`, - sqliteValue(values[n - 1]), - ]), - ); - return [params]; + return p.names.length === 0 ? [] : [sqliteBindings(p.names, values, p.bare)]; } export interface ExecutorOptions { @@ -99,7 +92,6 @@ export function executor( async (text: string, values: readonly unknown[]) => run(prepared(db, text, bigint), values), { - // sql.prepare(): prepare the statement once and reuse it. prepare: (text: string): Executor => { let p: Prepared | undefined; return async (_text, values) => { diff --git a/packages/sqlite/src/introspect/cli.ts b/packages/sqlite/src/introspect/cli.ts index 46ab6b56..c4859d5a 100644 --- a/packages/sqlite/src/introspect/cli.ts +++ b/packages/sqlite/src/introspect/cli.ts @@ -1,9 +1,13 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; +import { resolve } from "node:path"; import { parseArgs } from "node:util"; -import { type CommandIo, introspectArgs } from "@typesql/core/adapter"; -import type { SqliteDatabase } from "../client.js"; -import { type GenerateOptions, generateSource } from "./emit.js"; +import { + type CommandIo, + connectSqlite, + introspectCommand, + listOption, + UsageError, +} from "@typesql/core/adapter"; +import { generateSource } from "./emit.js"; import { introspect } from "./introspect.js"; const USAGE = `Usage: typesql introspect --dialect sqlite --db [options] @@ -19,27 +23,9 @@ Options: -h, --help show this help `; -interface Io extends CommandIo { - readonly cwd: string; - readonly open?: (file: string) => Promise; -} - -const defaultIo = (): Io => ({ - stdout: (s) => process.stdout.write(s), - stderr: (s) => process.stderr.write(s), - cwd: process.cwd(), -}); - -async function openReadOnly( - file: string, -): Promise { - const { DatabaseSync } = await import("node:sqlite"); - return new DatabaseSync(file, { readOnly: true }); -} - -const parse = (argv: readonly string[]) => +const parse = (args: string[]) => parseArgs({ - args: [...argv], + args, allowPositionals: true, options: { db: { type: "string" }, @@ -51,44 +37,32 @@ const parse = (argv: readonly string[]) => }, }); -export async function main( +/** + * `typesql introspect --dialect sqlite`. `io` replaces the process's + * output and working directory. + */ +export const main: ( argv: readonly string[], - io: Io = defaultIo(), -): Promise { - const values = introspectArgs(io, USAGE, () => parse(argv)); - if (typeof values === "number") return values; - if (!values.db) { - io.stderr(`typesql introspect: pass --db \n`); - return 2; - } + io?: CommandIo, +) => Promise = introspectCommand(USAGE, parse, async (values, io) => { + if (!values.db) throw new UsageError("pass --db "); + const connection = await connectSqlite(resolve(io.cwd, values.db), { + readOnly: true, + }); try { - const db = await (io.open ?? openReadOnly)(resolve(io.cwd, values.db)); - let source: string; - try { - const options: GenerateOptions = { - ...(values["type-name"] && { typeName: values["type-name"] }), - ...(values["import-from"] && { importFrom: values["import-from"] }), - }; - source = generateSource( - introspect(db, { - exclude: (values.exclude ?? []).flatMap((e) => e.split(",")), + return { + source: generateSource( + introspect(connection.client, { + exclude: listOption(values.exclude), }), - options, - ); - } finally { - db.close(); - } - if (values.out) { - const out = resolve(io.cwd, values.out); - await mkdir(dirname(out), { recursive: true }); - await writeFile(out, source); - io.stderr(`typesql introspect: wrote ${values.out}\n`); - } else io.stdout(source); - return 0; - } catch (err) { - io.stderr( - `typesql introspect: ${err instanceof Error ? err.message : String(err)}\n`, - ); - return 1; + { + ...(values["type-name"] && { typeName: values["type-name"] }), + ...(values["import-from"] && { importFrom: values["import-from"] }), + }, + ), + out: values.out || undefined, + }; + } finally { + await connection.close(); } -} +}); diff --git a/packages/sqlite/src/introspect/emit.ts b/packages/sqlite/src/introspect/emit.ts index cb3780e3..b19ac491 100644 --- a/packages/sqlite/src/introspect/emit.ts +++ b/packages/sqlite/src/introspect/emit.ts @@ -1,4 +1,4 @@ -import { propertyKey as key } from "@typesql/core/adapter"; +import { propertyKey as key, schemaWriter } from "@typesql/core/adapter"; import type { IntrospectedDatabase } from "./introspect.js"; export interface GenerateOptions { @@ -10,32 +10,30 @@ export function generateSource( db: IntrospectedDatabase, opts: GenerateOptions = {}, ): string { - const typeName = opts.typeName ?? "DB"; - const importFrom = opts.importFrom ?? "@typesql/core/sqlite"; - const views = db.relations.some((r) => r.kind === "view"); - const lines = [ - `// Generated by \`typesql introspect\` from SQLite ${db.version}. Do not edit.`, - `import type { Col${views ? ", View" : ""} } from ${JSON.stringify(importFrom)};`, - "", - `export interface ${typeName} {`, - ]; + const write = schemaWriter("sqlite"); + const lines: string[] = []; for (const r of db.relations) { - const cols = r.columns.map((c) => { - const flags = [ - c.nullable ? "nullable: true" : "", - c.hasDefault && !c.generated && !c.hidden ? "hasDefault: true" : "", - c.hidden ? "hidden: true" : `position: ${c.position}`, - c.rowid ? "rowid: true" : "", - c.generated ? "generated: true" : "", - ].filter(Boolean); - return `\t\t${key(c.name)}: Col<${JSON.stringify(c.type)}, { ${flags.join("; ")} }>;`; - }); - if (r.kind === "view") { - lines.push(`\t${key(r.name)}: View<{`, ...cols, "\t}>;"); - } else { - lines.push(`\t${key(r.name)}: {`, ...cols, "\t};"); - } + const cols = r.columns.map( + (c) => + `\t\t${key(c.name)}: ${write.col(c.type, { + nullable: c.nullable, + hasDefault: c.hasDefault, + // A hidden column has no place in `SELECT *`. + position: c.hidden ? undefined : c.position, + rowid: c.rowid, + generated: c.generated, + hidden: c.hidden, + })};`, + ); + const body = `{\n${cols.join("\n")}\n\t}`; + lines.push( + `\t${key(r.name)}: ${r.kind === "view" ? write.view(body) : body};`, + ); } - lines.push("}", ""); - return lines.join("\n"); + return write.module({ + header: `Generated by \`typesql introspect\` from SQLite ${db.version}. Do not edit.`, + members: lines, + typeName: opts.typeName, + importFrom: opts.importFrom, + }); } diff --git a/packages/sqlite/src/introspect/introspect.ts b/packages/sqlite/src/introspect/introspect.ts index ef8575d4..f8d329f0 100644 --- a/packages/sqlite/src/introspect/introspect.ts +++ b/packages/sqlite/src/introspect/introspect.ts @@ -105,18 +105,14 @@ export function introspect( const rows = parseRows( db .prepare( - "SELECT name, type, sql FROM sqlite_schema WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name", + "SELECT name, type, sql FROM sqlite_schema WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND name NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow') ORDER BY name", ) .all(), isSchemaRow, ); - const virtualTables = rows - .filter((r) => /^\s*create\s+virtual\s+table\b/i.test(r.sql ?? "")) - .map((r) => r.name); const relations: IntrospectedRelation[] = []; for (const row of rows) { if (excluded.some((re) => re.test(row.name.toLowerCase()))) continue; - if (virtualTables.some((v) => row.name.startsWith(`${v}_`))) continue; const tail = options(row.sql); const strict = row.type === "table" && /\bstrict\b/.test(tail); const withoutRowid = /\bwithout\s+rowid\b/.test(tail); diff --git a/packages/vscode/package.json b/packages/vscode/package.json index b0b85f84..98297a05 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -335,7 +335,8 @@ } }, "scripts": { - "build": "esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --sourcemap --outfile=dist/extension.js", + "build": "node scripts/contributes.mts --check && esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --sourcemap --outfile=dist/extension.js", + "contributes": "node scripts/contributes.mts", "typecheck": "tsrs -p tsconfig.json --noEmit", "package": "pnpm run build && vsce package --no-dependencies --out typesql.vsix" }, diff --git a/packages/vscode/scripts/contributes.mts b/packages/vscode/scripts/contributes.mts new file mode 100644 index 00000000..448a395c --- /dev/null +++ b/packages/vscode/scripts/contributes.mts @@ -0,0 +1,230 @@ +// Writes the language server's word lists, token patterns and call names into +// the grammar, and the language and token tables of src/contributes.mts into +// package.json, so none of them can drift. With --check, as the build runs +// it, it only fails on a difference. +import { readFileSync, writeFileSync } from "node:fs"; +import { GRAMMAR } from "@typesql/language-server"; +import { LANGUAGES, SQL_TOKENS } from "../src/contributes.mts"; + +type Json = string | number | boolean | null | Json[] | JsonObject; +type JsonObject = { [key: string]: Json }; + +/** A generated value, and the object and key it goes to. */ +interface Field { + readonly path: string; + readonly owner: JsonObject | undefined; + readonly key: string; + readonly value: Json; +} + +interface Change { + readonly before: Json | undefined; + readonly after: Json; +} + +const check = process.argv.includes("--check"); +const stale: string[] = []; + +function update( + name: string, + fields: (json: JsonObject) => readonly Field[], + serialize: (text: string, json: JsonObject, changes: Change[]) => string, +): void { + const file = new URL(`../${name}`, import.meta.url); + const text = readFileSync(file, "utf8"); + const json: JsonObject = JSON.parse(text); + const changes: Change[] = []; + for (const { path, owner, key, value } of fields(json)) { + if (!owner) throw new Error(`${name} has no ${path}`); + if (JSON.stringify(owner[key]) === JSON.stringify(value)) continue; + stale.push(`${name}: ${path}`); + changes.push({ before: owner[key], after: value }); + owner[key] = value; + } + if (changes.length && !check) + writeFileSync(file, serialize(text, json, changes)); +} + +const object = (v: Json | undefined): JsonObject | undefined => + typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined; + +const words = (list: readonly string[]): string => + `(?i)(?))?(?=\\s*\\(\\s*(?:[`\"'/]|$))"; +const name = `(${GRAMMAR.name}(?![\\w$]))`; +const statement = `(?:--|/\\*|\\(|(?i:${GRAMMAR.statementWords.join("|")})(?=[\\s;(]|$))`; + +update( + "syntaxes/sql-injection.tmLanguage.json", + (g) => { + const rule = (key: string) => object(object(g.repository)?.[key]); + const patterns = rule("tokens")?.patterns; + const tokenWhere = (test: (p: JsonObject) => boolean) => + Array.isArray(patterns) + ? patterns.map(object).find((p) => p !== undefined && test(p)) + : undefined; + const token = (scope: string) => tokenWhere((p) => p.name === scope); + const lists = [ + ["storage.type.sql", GRAMMAR.types], + ["keyword.other.sql", GRAMMAR.keywords], + ["constant.language.sql", GRAMMAR.constants], + ] as const; + const lexis = GRAMMAR.lexis; + const matches = [ + ["variable.parameter.positional.sql", lexis.parameter], + ["variable.parameter.named.sql", lexis.named], + ["constant.numeric.sql", lexis.number], + ["entity.name.function.sql", `${lexis.name}(?=\\()`], + ["keyword.operator.sql", lexis.operator], + ] as const; + // A single-quoted literal writes the SQL quote as \'. + const quotes = { double: "'", single: "\\\\'", template: "'" }; + return [ + { + path: "injectionSelector", + owner: g, + key: "injectionSelector", + value: Object.values(LANGUAGES) + .map((scope) => `L:${scope} -comment -string`) + .join(", "), + }, + { + path: "repository.call.begin", + owner: rule("call"), + key: "begin", + value: `${receiver}|(? ({ + path: `repository.tokens ${scope}`, + owner: token(scope), + key: "match", + value: words(list), + }), + ), + ...matches.map( + ([scope, value]): Field => ({ + path: `repository.tokens ${scope}`, + owner: token(scope), + key: "match", + value, + }), + ), + { + path: "repository.tokens cast", + owner: tokenWhere( + (p) => + object(object(p.captures)?.["1"])?.name === + "keyword.operator.cast.sql", + ), + key: "match", + value: lexis.cast, + }, + { + path: "repository.tokens name", + owner: tokenWhere((p) => Object.keys(p).join() === "match"), + key: "match", + value: lexis.name, + }, + ...Object.entries(quotes).flatMap(([kind, quote]): Field[] => [ + { + path: `repository.string-${kind}.begin`, + owner: rule(`string-${kind}`), + key: "begin", + value: `${lexis.stringPrefix}${quote}`, + }, + { + path: `repository.dollar-string-${kind}.begin`, + owner: rule(`dollar-string-${kind}`), + key: "begin", + value: lexis.dollarTag, + }, + ]), + ]; + }, + // Replace each generated value in place, to keep the file's layout: a + // value found more than once is set alike in each place. + (text, _, changes) => { + const after = new Map(); + const count = new Map(); + for (const change of changes) { + const from = JSON.stringify(change.before); + if (typeof change.before !== "string") + throw new Error(`${from} in the grammar is not a string`); + if (after.has(from) && after.get(from) !== change.after) + throw new Error(`${from} in the grammar has two new values`); + after.set(from, change.after); + count.set(from, (count.get(from) ?? 0) + 1); + } + let out = text; + for (const [from, value] of after) { + if (out.split(from).length - 1 !== count.get(from)) + throw new Error( + `${from} is in the grammar more often than it is generated`, + ); + out = out.replaceAll(from, () => JSON.stringify(value)); + } + return out; + }, +); + +const scopes: JsonObject = Object.fromEntries( + Object.values(SQL_TOKENS).flatMap((t) => (t ? [[t.token, [t.scope]]] : [])), +); +update( + "package.json", + (pkg) => { + const contributes = object(pkg.contributes); + const grammars = contributes?.grammars; + return [ + { + path: "activationEvents", + owner: pkg, + key: "activationEvents", + value: Object.keys(LANGUAGES).map((id) => `onLanguage:${id}`), + }, + { + path: "contributes.grammars[0].injectTo", + owner: Array.isArray(grammars) ? object(grammars[0]) : undefined, + key: "injectTo", + value: Object.values(LANGUAGES), + }, + { + path: "contributes.semanticTokenScopes", + owner: contributes, + key: "semanticTokenScopes", + value: Object.keys(LANGUAGES).map((language) => ({ + language, + scopes, + })), + }, + ]; + }, + (_, pkg) => `${JSON.stringify(pkg, null, "\t")}\n`, +); + +if (stale.length) { + const list = stale.map((s) => `\n ${s}`).join(""); + if (check) { + console.error( + `Out of date with @typesql/language-server or src/contributes.mts:${list}\nRun: pnpm --filter typesql contributes`, + ); + process.exit(1); + } + console.log(`Updated:${list}`); +} diff --git a/packages/vscode/src/contributes.mts b/packages/vscode/src/contributes.mts new file mode 100644 index 00000000..969f3b40 --- /dev/null +++ b/packages/vscode/src/contributes.mts @@ -0,0 +1,43 @@ +// The tables package.json's contributions and the grammar are generated from +// (scripts/contributes.mts), and that the extension reads at run time. +import type { TokenKind } from "@typesql/language-server"; + +/** The languages whose strings hold SQL, and their TextMate scopes. */ +export const LANGUAGES = { + typescript: "source.ts", + typescriptreact: "source.tsx", + javascript: "source.js", + javascriptreact: "source.js.jsx", +} as const; + +/** + * Each SQL token kind as a semantic token, and the grammar scope whose color + * it takes. The tokens carry the definition modifier, which TypeScript never + * sets, and package.json's semanticTokenScopes map each to its scope. Strings + * and comments stay plain, as in the grammar. + */ +export const SQL_TOKENS = { + keyword: { token: "keyword.definition", scope: "keyword.other.sql" }, + constant: { token: "enumMember.definition", scope: "constant.language.sql" }, + type: { token: "type.definition", scope: "storage.type.sql" }, + function: { + token: "function.definition", + scope: "entity.name.function.sql", + }, + number: { token: "number.definition", scope: "constant.numeric.sql" }, + escape: { + token: "regexp.definition", + scope: "constant.character.escape.sql", + }, + operator: { token: "operator.definition", scope: "keyword.operator.sql" }, + parameter: { + token: "parameter.definition", + scope: "variable.parameter.positional.sql", + }, + variable: { token: "variable.definition", scope: "meta.embedded.line.sql" }, + punctuation: { token: "string.definition", scope: "meta.embedded.line.sql" }, + string: null, + comment: null, +} as const satisfies { + readonly [K in TokenKind]: { token: string; scope: string } | null; +}; diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 6429a8a0..142946f2 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -1,55 +1,35 @@ import * as path from "node:path"; import { - type Completions, type Connector, - mayBeInSqlLiteral, + lspItems, merge, mergeTokens, - offsetOf, Session, - type CompletionItem as SqlItem, type TokenLegend, type TokenNames, type TypeScriptModules, typescriptModules, - withBudget, + typescriptModulesFrom, } from "@typesql/language-server"; import type { ExtensionAPI, + CompletionItem as LspItem, Range, SemanticTokens, TypeScriptSDK, } from "typescript/vscode"; import * as vscode from "vscode"; +import { LANGUAGES, SQL_TOKENS } from "./contributes.mjs"; const TYPESCRIPT_EXTENSION = "TypeScriptTeam.native-preview"; const NIGHTLY_EXTENSION = "TypeScriptTeam.vscode-typescript-nightly"; const UPGRADE = 'Install the "TypeScript 7 Nightly" extension, or set "js/ts.tsdk.path" to a TypeScript 7.1 package and allow it.'; -const BUDGET_MS = 150; -const HIGHLIGHT_BUDGET_MS = 500; -const LANGUAGES = [ - "typescript", - "typescriptreact", - "javascript", - "javascriptreact", -]; -// SQL tokens carry the definition modifier, which TypeScript never sets, and -// package.json's semanticTokenScopes map each of these to the grammar's scope, -// so they take the same colors as the grammar's SQL. Strings and comments stay -// plain, as in the grammar. -const SQL_TOKENS: TokenNames = { - keyword: "keyword.definition", - constant: "enumMember.definition", - type: "type.definition", - function: "function.definition", - number: "number.definition", - escape: "regexp.definition", - operator: "operator.definition", - parameter: "parameter.definition", - variable: "variable.definition", - punctuation: "string.definition", -}; +const TOKEN_NAMES: TokenNames = Object.fromEntries( + Object.entries(SQL_TOKENS).flatMap(([kind, t]) => + t ? [[kind, t.token]] : [], + ), +); interface LegacyAPI { onLanguageServerInitialized(listener: () => void): vscode.Disposable; @@ -97,8 +77,9 @@ export async function activate( let session: Session | undefined; const restart = (connector: Connector): void => { const old = session; - session = new Session({ ...connector, upgrade: UPGRADE }, (m) => - log.info(m), + session = new Session( + { ...connector, upgrade: UPGRADE }, + (m, level = "info") => log[level](m), ); void old?.close(); }; @@ -108,29 +89,7 @@ export async function activate( uri: string, position: { line: number; character: number }, text: string | undefined, - ): Promise => { - const s = session; - if (!s || !enabled()) return null; - if ( - text !== undefined && - !mayBeInSqlLiteral(text, offsetOf(text, position)) - ) - return null; - try { - const started = Date.now(); - const ours = await withBudget(s.complete(uri, position, text), BUDGET_MS); - if (ours === "late") { - log.warn(`completions took longer than ${BUDGET_MS} ms; skipped`); - return null; - } - if (ours) - log.trace(`${ours.items.length} items in ${Date.now() - started} ms`); - return ours; - } catch (e) { - log.error(e instanceof Error ? e : String(e)); - return null; - } - }; + ) => (session && enabled() ? session.complete(uri, position, text) : null); // TypeScript's token legend, read from VS Code once per server start; a // failed read is retried on the next request. @@ -157,25 +116,10 @@ export async function activate( ): Promise => { const s = session; if (!s || !highlighting()) return result; - try { - const started = Date.now(); - const [ours, l] = await Promise.all([ - withBudget(s.highlight(uri), HIGHLIGHT_BUDGET_MS), - legendFor(uri), - ]); - if (ours === "late") { - log.warn( - `SQL highlighting took longer than ${HIGHLIGHT_BUDGET_MS} ms; skipped`, - ); - return result; - } - if (!ours || !l) return result; - log.trace(`${ours.length} SQL highlights in ${Date.now() - started} ms`); - return mergeTokens(result, ours, l, range, SQL_TOKENS); - } catch (e) { - log.error(e instanceof Error ? e : String(e)); - return result; - } + const [ours, l] = await Promise.all([s.highlight(uri), legendFor(uri)]); + return ours && l + ? mergeTokens(result, ours, l, range, TOKEN_NAMES) + : result; }; if (hasMiddleware) { @@ -187,21 +131,14 @@ export async function activate( restart({ pipe: () => sdk.initializeAPIConnection(), modules: () => [ - async (): Promise => { - const [async, ast] = await Promise.all([ - sdk.importModule("typescript/async"), - sdk.importModule("typescript/ast"), - ]); - const dir = sdk.packageJsonPath - ? path.dirname(sdk.packageJsonPath) - : "(TypeScript 7 extension)"; - return { - API: async.API, - SyntaxKind: ast.SyntaxKind, - dir, - version: sdk.version, - }; - }, + () => + typescriptModules( + sdk, + sdk.packageJsonPath + ? path.dirname(sdk.packageJsonPath) + : "(TypeScript 7 extension)", + sdk.version, + ), ], }); }), @@ -245,7 +182,7 @@ export async function activate( }); }), vscode.languages.registerCompletionItemProvider( - LANGUAGES.map((language) => ({ language, scheme: "file" })), + Object.keys(LANGUAGES).map((language) => ({ language, scheme: "file" })), { async provideCompletionItems(document, position) { const ours = await complete( @@ -253,7 +190,7 @@ export async function activate( { line: position.line, character: position.character }, document.getText(), ); - return ours ? ours.items.map((i) => toItem(i, ours)) : undefined; + return ours ? lspItems(ours).map(toItem) : undefined; }, }, " ", @@ -291,38 +228,38 @@ function legacyCandidates( vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? path.dirname(fileName); candidates.push(() => - typescriptModules(path.join(path.resolve(root, tsdk), "package.json")), + typescriptModulesFrom( + path.join(path.resolve(root, tsdk), "package.json"), + ), ); } - candidates.push(() => typescriptModules(fileName)); + candidates.push(() => typescriptModulesFrom(fileName)); if (nightlyPath) candidates.push(() => - typescriptModules(path.join(nightlyPath, "package.json")), + typescriptModulesFrom(path.join(nightlyPath, "package.json")), ); return candidates; } -const KINDS = { - table: vscode.CompletionItemKind.Struct, - view: vscode.CompletionItemKind.Interface, - cte: vscode.CompletionItemKind.Struct, - namespace: vscode.CompletionItemKind.Module, - column: vscode.CompletionItemKind.Field, - alias: vscode.CompletionItemKind.Variable, -} satisfies { readonly [K in SqlItem["kind"]]: vscode.CompletionItemKind }; - -function toItem(i: SqlItem, c: Completions): vscode.CompletionItem { - const item = new vscode.CompletionItem(i.label, KINDS[i.kind]); - item.detail = i.detail; - item.sortText = i.sortText; - item.filterText = i.label; - item.insertText = i.insertText; - const r = c.replaceRange; - item.range = new vscode.Range( - r.start.line, - r.start.character, - r.end.line, - r.end.character, +// An LSP item as VS Code's: its CompletionItemKind is LSP's less one. +function toItem(i: LspItem): vscode.CompletionItem { + const item = new vscode.CompletionItem( + i.label, + i.kind === undefined ? undefined : i.kind - 1, ); + if (i.detail !== undefined) item.detail = i.detail; + if (i.sortText !== undefined) item.sortText = i.sortText; + if (i.filterText !== undefined) item.filterText = i.filterText; + const edit = i.textEdit; + if (edit && "range" in edit) { + const r = edit.range; + item.insertText = edit.newText; + item.range = new vscode.Range( + r.start.line, + r.start.character, + r.end.line, + r.end.character, + ); + } return item; } diff --git a/packages/vscode/syntaxes/sql-injection.tmLanguage.json b/packages/vscode/syntaxes/sql-injection.tmLanguage.json index 929afcb0..681f2a51 100644 --- a/packages/vscode/syntaxes/sql-injection.tmLanguage.json +++ b/packages/vscode/syntaxes/sql-injection.tmLanguage.json @@ -1,5 +1,5 @@ { - "$comment": "SQL highlighting inside sql(), tx(), sql.fragment(), sql.as(), a migration's .ddl(), .add() and .raw() and /* sql */ string literals. Injected into TypeScript and JavaScript. Every rule inside a literal stops at the literal's closing quote, so an unterminated SQL string or comment never runs past it, and takes escapes first, so an escaped quote never closes it. In \"…\" and '…', a line ends a rule unless it ends in a backslash (a line continuation).", + "$comment": "SQL highlighting inside sql(), tx(), sql.fragment(), sql.as(), a migration's .ddl(), .add() and .raw() and /* sql */ string literals. Injected into TypeScript and JavaScript. Every rule inside a literal stops at the literal's closing quote, so an unterminated SQL string or comment never runs past it, and takes escapes first, so an escaped quote never closes it. In \"…\" and '…', a line ends a rule unless it ends in a backslash (a line continuation). The injection selector, call names, statement words and word lists are written by scripts/contributes.mts from @typesql/language-server (pnpm --filter typesql contributes); the build fails when they differ.", "scopeName": "inline.typesql", "injectionSelector": "L:source.ts -comment -string, L:source.tsx -comment -string, L:source.js -comment -string, L:source.js.jsx -comment -string", "patterns": [ @@ -353,21 +353,21 @@ }, "dollar-string-double": { - "begin": "\\$([A-Za-z_][\\w]*|)\\$", + "begin": "\\$([A-Za-z_\\u0080-\\uffff][\\w\\u0080-\\uffff]*|)\\$", "end": "\\$\\1\\$|(?=\")|(?=~!@#%^&|?]+", "name": "keyword.operator.sql" }, + { + "match": "(?:(?!--|/\\*)[+\\-*/<>=~!@#%^&|?])+", + "name": "keyword.operator.sql" + }, { "match": ",", "name": "punctuation.separator.comma.sql" }, { "match": ";", "name": "punctuation.terminator.statement.sql" }, { "match": "\\.", "name": "punctuation.accessor.sql" }, - { "match": "[A-Za-z_][\\w$]*" } + { "match": "[A-Za-z_\\u0080-\\uffff][\\w$\\u0080-\\uffff]*" } ] } } diff --git a/packages/vscode/tsconfig.json b/packages/vscode/tsconfig.json index 0a9a732a..54d771b6 100644 --- a/packages/vscode/tsconfig.json +++ b/packages/vscode/tsconfig.json @@ -5,10 +5,8 @@ "moduleResolution": "nodenext", "verbatimModuleSyntax": false, "noEmit": true, - "types": ["node", "vscode"], - "paths": { - "@typesql/language-server": ["../language-server/src/index.ts"] - } + "allowImportingTsExtensions": true, + "types": ["node", "vscode"] }, - "include": ["src"] + "include": ["src", "scripts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c684353b..dc23248a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,9 @@ catalogs: '@prisma/client': specifier: 7.10.1 version: 7.10.1 + '@tidbcloud/serverless': + specifier: 0.3.0 + version: 0.3.0 '@types/better-sqlite3': specifier: 9.6.0 version: 9.6.0 @@ -286,9 +289,21 @@ importers: '@types/node': specifier: 'catalog:' version: 26.6.6 + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:packages/core + '@typesql/pg': + specifier: workspace:* + version: link:packages/pg knip: specifier: 'catalog:' version: 6.44.1 + pg: + specifier: 'catalog:' + version: 8.23.1 tsrs: specifier: 'catalog:' version: 0.21.0-ts7.1.0-beta @@ -436,6 +451,10 @@ importers: version: 7.1.0-beta packages/language-server: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../core devDependencies: '@types/node': specifier: 'catalog:' @@ -453,9 +472,6 @@ importers: specifier: workspace:* version: link:../core devDependencies: - mariadb: - specifier: 'catalog:' - version: 3.5.5 mysql2: specifier: 'catalog:' version: 3.24.6 @@ -763,6 +779,28 @@ importers: specifier: 'catalog:' version: 7.1.0-beta + tests/scenarios/cli-introspect-database-url: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + tests/scenarios/cli-introspect-mariadb: dependencies: '@typesql/cli': @@ -851,11 +889,62 @@ importers: specifier: 'catalog:' version: 7.1.0-beta + tests/scenarios/cli-introspect-sqlite-exclude: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/cli-introspect-sqlite-virtual: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/cli-introspect-type-name: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + tests/scenarios/cli-invalid-command: dependencies: '@typesql/cli': specifier: workspace:* version: link:../../../packages/cli + + tests/scenarios/cli-main-io: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli devDependencies: '@types/node': specifier: 'catalog:' @@ -1004,11 +1093,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/compiler-undeclared-dependency: + tests/scenarios/core-workflows: dependencies: '@types/pg': specifier: 'catalog:' version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core '@typesql/pg': specifier: workspace:* version: link:../../../packages/pg @@ -1023,7 +1115,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/core-workflows: + tests/scenarios/dollar-identifiers: dependencies: '@types/pg': specifier: 'catalog:' @@ -1034,6 +1126,9 @@ importers: '@typesql/pg': specifier: workspace:* version: link:../../../packages/pg + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite pg: specifier: 'catalog:' version: 8.23.1 @@ -1067,6 +1162,28 @@ importers: specifier: 'catalog:' version: 7.1.0-beta + tests/scenarios/drizzle-mysql-serverless: + dependencies: + '@tidbcloud/serverless': + specifier: 'catalog:' + version: 0.3.0 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/drizzle': + specifier: workspace:* + version: link:../../../packages/drizzle + drizzle-orm: + specifier: 'catalog:' + version: 0.45.4(@tidbcloud/serverless@0.3.0) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + tests/scenarios/drizzle-pg: dependencies: '@types/pg': @@ -1161,6 +1278,28 @@ importers: specifier: 'catalog:' version: 7.1.0-beta + tests/scenarios/kysely-mysql-databases: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/kysely': + specifier: workspace:* + version: link:../../../packages/kysely + kysely: + specifier: 'catalog:' + version: 0.29.8 + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + tests/scenarios/kysely-postgres: dependencies: '@types/pg': @@ -1211,6 +1350,31 @@ importers: specifier: 'catalog:' version: 7.1.0-beta + tests/scenarios/kysely-sqlite-attached: + dependencies: + '@types/better-sqlite3': + specifier: 'catalog:' + version: 9.6.0 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/kysely': + specifier: workspace:* + version: link:../../../packages/kysely + better-sqlite3: + specifier: 'catalog:' + version: 13.0.3 + kysely: + specifier: 'catalog:' + version: 0.29.8 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + tests/scenarios/mariadb-connector: dependencies: '@typesql/core': @@ -1281,17 +1445,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/migrations-postgres: + tests/scenarios/migrations-mysql-keyword-names: dependencies: - '@types/pg': - specifier: 'catalog:' - version: 8.23.1 '@typesql/core': specifier: workspace:* version: link:../../../packages/core - pg: + mysql2: specifier: 'catalog:' - version: 8.23.1 + version: 3.24.6(@types/node@26.6.6) devDependencies: '@types/node': specifier: 'catalog:' @@ -1300,11 +1461,17 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/migrations-sqlite: + tests/scenarios/migrations-mysql-values: dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) devDependencies: '@types/node': specifier: 'catalog:' @@ -1313,29 +1480,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/module-entry-points: + tests/scenarios/migrations-postgres: dependencies: - '@prisma/client': - specifier: 'catalog:' - version: 7.10.1(typescript@7.1.0-beta) '@types/pg': specifier: 'catalog:' version: 8.23.1 - '@typesql/cli': - specifier: workspace:* - version: link:../../../packages/cli '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/pg': - specifier: workspace:* - version: link:../../../packages/pg - '@typesql/prisma': - specifier: workspace:* - version: link:../../../packages/prisma - '@typesql/sqlite': - specifier: workspace:* - version: link:../../../packages/sqlite pg: specifier: 'catalog:' version: 8.23.1 @@ -1347,11 +1499,17 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/mysql-dialect-rejection: + tests/scenarios/migrations-postgres-atomic-functions: dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 '@typesql/core': specifier: workspace:* version: link:../../../packages/core + pg: + specifier: 'catalog:' + version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -1360,17 +1518,23 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/mysql-json: + tests/scenarios/migrations-postgres-json-generate: dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/mysql': + '@typesql/pg': specifier: workspace:* - version: link:../../../packages/mysql - mysql2: + version: link:../../../packages/pg + pg: specifier: 'catalog:' - version: 3.24.6(@types/node@26.6.6) + version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -1379,17 +1543,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/mysql-quick-start: + tests/scenarios/migrations-postgres-values: dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/mysql': - specifier: workspace:* - version: link:../../../packages/mysql - mysql2: + pg: specifier: 'catalog:' - version: 3.24.6(@types/node@26.6.6) + version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -1398,14 +1559,11 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/node-sqlite: + tests/scenarios/migrations-sqlite: dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/sqlite': - specifier: workspace:* - version: link:../../../packages/sqlite devDependencies: '@types/node': specifier: 'catalog:' @@ -1414,29 +1572,11 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/observation-shapes: - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 26.6.6 - typescript: - specifier: 'catalog:' - version: 7.1.0-beta - - tests/scenarios/pg-declaration-emit: + tests/scenarios/migrations-sqlite-keyword-names: dependencies: - '@types/pg': - specifier: 'catalog:' - version: 8.23.1 '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/pg': - specifier: workspace:* - version: link:../../../packages/pg - pg: - specifier: 'catalog:' - version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -1445,7 +1585,382 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-driver-values: + tests/scenarios/module-entry-points: + dependencies: + '@better-fetch/fetch': + specifier: 'catalog:' + version: 1.3.3 + '@prisma/client': + specifier: 'catalog:' + version: 7.10.1(typescript@7.1.0-beta) + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/better-auth': + specifier: workspace:* + version: link:../../../packages/better-auth + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/drizzle': + specifier: workspace:* + version: link:../../../packages/drizzle + '@typesql/kysely': + specifier: workspace:* + version: link:../../../packages/kysely + '@typesql/language-server': + specifier: workspace:* + version: link:../../../packages/language-server + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + '@typesql/postgres': + specifier: workspace:* + version: link:../../../packages/postgres + '@typesql/prisma': + specifier: workspace:* + version: link:../../../packages/prisma + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite + better-auth: + specifier: 'catalog:' + version: 1.7.17(@prisma/client@7.10.1(typescript@7.1.0-beta))(drizzle-orm@0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0))(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1) + better-call: + specifier: 'catalog:' + version: 1.4.5(zod@4.6.7) + drizzle-orm: + specifier: 'catalog:' + version: 0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0) + jose: + specifier: 'catalog:' + version: 6.2.15 + kysely: + specifier: 'catalog:' + version: 0.29.8 + mariadb: + specifier: 'catalog:' + version: 3.5.5 + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + nanostores: + specifier: 'catalog:' + version: 1.5.5 + pg: + specifier: 'catalog:' + version: 8.23.1 + postgres: + specifier: 'catalog:' + version: 4.0.0 + zod: + specifier: 'catalog:' + version: 4.6.7 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-dialect-rejection: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-insert-column-order: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-json: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-json-array-rejection: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-json-object-rejection: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-json-strings: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-multiple-statements: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-quick-start: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-quoted-text: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-serverless-literals: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/mysql-where-narrowing: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/mysql': + specifier: workspace:* + version: link:../../../packages/mysql + mysql2: + specifier: 'catalog:' + version: 3.24.6(@types/node@26.6.6) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/node-sqlite: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-cast-parameters: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-char-type: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-common-type-rejection: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-declaration-emit: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-declared-function-calls: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-driver-values: dependencies: '@types/pg': specifier: 'catalog:' @@ -1481,9 +1996,176 @@ importers: pg: specifier: 'catalog:' version: 8.23.1 - postgres: - specifier: 'catalog:' - version: 4.0.0 + postgres: + specifier: 'catalog:' + version: 4.0.0 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-dynamic-sql: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-ended-transaction: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-engine-rejection: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-function-call-parameters: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-function-overloads: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-grouping-sets-parameters: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-json: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-json-write-rejection: + dependencies: + '@types/pg': + specifier: 'catalog:' + version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: + specifier: 'catalog:' + version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -1492,7 +2174,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-dynamic-sql: + tests/scenarios/pg-json-writes: dependencies: '@types/pg': specifier: 'catalog:' @@ -1514,7 +2196,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-ended-transaction: + tests/scenarios/pg-left-join: dependencies: '@types/pg': specifier: 'catalog:' @@ -1536,7 +2218,20 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-function-overloads: + tests/scenarios/pg-meta-key-relations: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/pg-multiple-statements: dependencies: '@types/pg': specifier: 'catalog:' @@ -1558,20 +2253,23 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-json: + tests/scenarios/pg-nested-comments: dependencies: '@types/pg': specifier: 'catalog:' version: 8.23.1 - '@typesql/core': - specifier: workspace:* - version: link:../../../packages/core '@typesql/pg': specifier: workspace:* version: link:../../../packages/pg + '@typesql/postgres': + specifier: workspace:* + version: link:../../../packages/postgres pg: specifier: 'catalog:' version: 8.23.1 + postgres: + specifier: 'catalog:' + version: 4.0.0 devDependencies: '@types/node': specifier: 'catalog:' @@ -1580,7 +2278,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-json-write-rejection: + tests/scenarios/pg-no-result: dependencies: '@types/pg': specifier: 'catalog:' @@ -1602,7 +2300,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-json-writes: + tests/scenarios/pg-null-array-elements: dependencies: '@types/pg': specifier: 'catalog:' @@ -1624,7 +2322,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-left-join: + tests/scenarios/pg-parameter-inference: dependencies: '@types/pg': specifier: 'catalog:' @@ -1646,23 +2344,20 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-nested-comments: + tests/scenarios/pg-prepared: dependencies: '@types/pg': specifier: 'catalog:' version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core '@typesql/pg': specifier: workspace:* version: link:../../../packages/pg - '@typesql/postgres': - specifier: workspace:* - version: link:../../../packages/postgres pg: specifier: 'catalog:' version: 8.23.1 - postgres: - specifier: 'catalog:' - version: 4.0.0 devDependencies: '@types/node': specifier: 'catalog:' @@ -1671,23 +2366,20 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-nested-comments-runtime: + tests/scenarios/pg-quantified-null-elements: dependencies: '@types/pg': specifier: 'catalog:' version: 8.23.1 + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core '@typesql/pg': specifier: workspace:* version: link:../../../packages/pg - '@typesql/postgres': - specifier: workspace:* - version: link:../../../packages/postgres pg: specifier: 'catalog:' version: 8.23.1 - postgres: - specifier: 'catalog:' - version: 4.0.0 devDependencies: '@types/node': specifier: 'catalog:' @@ -1696,7 +2388,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-no-result: + tests/scenarios/pg-quick-start: dependencies: '@types/pg': specifier: 'catalog:' @@ -1718,7 +2410,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-parameter-inference: + tests/scenarios/pg-text-types: dependencies: '@types/pg': specifier: 'catalog:' @@ -1740,7 +2432,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-prepared: + tests/scenarios/pg-transactions: dependencies: '@types/pg': specifier: 'catalog:' @@ -1762,7 +2454,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-quick-start: + tests/scenarios/pg-values-defaults: dependencies: '@types/pg': specifier: 'catalog:' @@ -1784,7 +2476,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/pg-transactions: + tests/scenarios/pg-where-narrowing: dependencies: '@types/pg': specifier: 'catalog:' @@ -1847,7 +2539,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-client-mysql: + tests/scenarios/prisma-generation-mysql: dependencies: '@prisma/adapter-mariadb': specifier: 'catalog:' @@ -1872,7 +2564,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-client-postgres: + tests/scenarios/prisma-generation-postgres: dependencies: '@prisma/adapter-pg': specifier: 'catalog:' @@ -1903,7 +2595,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-client-sqlite: + tests/scenarios/prisma-generation-sqlite: dependencies: '@prisma/adapter-better-sqlite3': specifier: 'catalog:' @@ -1934,23 +2626,39 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-generation-mysql: + tests/scenarios/runner-observation-shapes: + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/runner-undeclared-dependency: dependencies: - '@prisma/adapter-mariadb': + '@types/pg': specifier: 'catalog:' - version: 7.10.1 - '@prisma/client': + version: 8.23.1 + '@typesql/pg': + specifier: workspace:* + version: link:../../../packages/pg + pg: specifier: 'catalog:' - version: 7.10.1(prisma@7.10.1(@types/node@26.6.6)(typescript@7.1.0-beta))(typescript@7.1.0-beta) + version: 8.23.1 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/runtime-inline-reuse: + dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/prisma': - specifier: workspace:* - version: link:../../../packages/prisma - prisma: - specifier: 'catalog:' - version: 7.10.1(@types/node@26.6.6)(typescript@7.1.0-beta) devDependencies: '@types/node': specifier: 'catalog:' @@ -1959,29 +2667,36 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-generation-postgres: + tests/scenarios/runtime-no-executor: dependencies: - '@prisma/adapter-pg': + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': specifier: 'catalog:' - version: 7.10.1 - '@prisma/client': + version: 26.6.6 + typescript: specifier: 'catalog:' - version: 7.10.1(prisma@7.10.1(@types/node@26.6.6)(typescript@7.1.0-beta))(typescript@7.1.0-beta) + version: 7.1.0-beta + + tests/scenarios/sql-dialect: + dependencies: '@types/pg': specifier: 'catalog:' version: 8.23.1 '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/prisma': + '@typesql/pg': specifier: workspace:* - version: link:../../../packages/prisma + version: link:../../../packages/pg + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite pg: specifier: 'catalog:' version: 8.23.1 - prisma: - specifier: 'catalog:' - version: 7.10.1(@types/node@26.6.6)(typescript@7.1.0-beta) devDependencies: '@types/node': specifier: 'catalog:' @@ -1990,29 +2705,49 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/prisma-generation-sqlite: + tests/scenarios/sqlite-aggregate-traversal: dependencies: - '@prisma/adapter-better-sqlite3': - specifier: 'catalog:' - version: 7.10.1 - '@prisma/client': + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite + devDependencies: + '@types/node': specifier: 'catalog:' - version: 7.10.1(prisma@7.10.1(@types/node@26.6.6)(better-sqlite3@13.0.3)(typescript@7.1.0-beta))(typescript@7.1.0-beta) - '@types/better-sqlite3': + version: 26.6.6 + typescript: specifier: 'catalog:' - version: 9.6.0 + version: 7.1.0-beta + + tests/scenarios/sqlite-declaration-emit: + dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/prisma': + '@typesql/sqlite': specifier: workspace:* - version: link:../../../packages/prisma - better-sqlite3: + version: link:../../../packages/sqlite + devDependencies: + '@types/node': specifier: 'catalog:' - version: 13.0.3 - prisma: + version: 26.6.6 + typescript: specifier: 'catalog:' - version: 7.10.1(@types/node@26.6.6)(better-sqlite3@13.0.3)(typescript@7.1.0-beta) + version: 7.1.0-beta + + tests/scenarios/sqlite-hidden-columns: + dependencies: + '@typesql/cli': + specifier: workspace:* + version: link:../../../packages/cli + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite devDependencies: '@types/node': specifier: 'catalog:' @@ -2021,11 +2756,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/runtime-inline-reuse: + tests/scenarios/sqlite-insert-column-order: dependencies: '@typesql/core': specifier: workspace:* version: link:../../../packages/core + '@typesql/sqlite': + specifier: workspace:* + version: link:../../../packages/sqlite devDependencies: '@types/node': specifier: 'catalog:' @@ -2034,7 +2772,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/runtime-no-executor: + tests/scenarios/sqlite-json-type-map: dependencies: '@typesql/core': specifier: workspace:* @@ -2047,23 +2785,14 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sql-dialect: + tests/scenarios/sqlite-json-write-rejection: dependencies: - '@types/pg': - specifier: 'catalog:' - version: 8.23.1 '@typesql/core': specifier: workspace:* version: link:../../../packages/core - '@typesql/pg': - specifier: workspace:* - version: link:../../../packages/pg '@typesql/sqlite': specifier: workspace:* version: link:../../../packages/sqlite - pg: - specifier: 'catalog:' - version: 8.23.1 devDependencies: '@types/node': specifier: 'catalog:' @@ -2072,7 +2801,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sqlite-aggregate-traversal: + tests/scenarios/sqlite-json-writes: dependencies: '@typesql/core': specifier: workspace:* @@ -2088,7 +2817,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sqlite-declaration-emit: + tests/scenarios/sqlite-negated-zero: dependencies: '@typesql/core': specifier: workspace:* @@ -2104,7 +2833,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sqlite-json-write-rejection: + tests/scenarios/sqlite-parameter-inference: dependencies: '@typesql/core': specifier: workspace:* @@ -2120,7 +2849,7 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sqlite-json-writes: + tests/scenarios/sqlite-quoted-text: dependencies: '@typesql/core': specifier: workspace:* @@ -2136,7 +2865,20 @@ importers: specifier: 'catalog:' version: 7.1.0-beta - tests/scenarios/sqlite-parameter-inference: + tests/scenarios/sqlite-values-columns: + dependencies: + '@typesql/core': + specifier: workspace:* + version: link:../../../packages/core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 26.6.6 + typescript: + specifier: 'catalog:' + version: 7.1.0-beta + + tests/scenarios/sqlite-where-narrowing: dependencies: '@typesql/core': specifier: workspace:* @@ -4072,6 +4814,10 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tidbcloud/serverless@0.3.0': + resolution: {integrity: sha512-SrTqKNPuskV8GTm574d9otox8GAKXrsoF5NfQ5fiDirR+ao+eE6O2pfWr5zLhmFLQRbeuT8SKaUD+JOK0Z7gyQ==} + engines: {node: '>=16'} + '@tsrs/darwin-arm64@0.21.0-ts7.1.0-beta': resolution: {integrity: sha512-qBHxa+LmrNu/XYZjqLXV4qQPsHzxSI+dQTISxXuxyBaxtJVDwCwGAq9rVHKgaUX3U0bsyb8FyC1JIZFu4+EqZQ==} engines: {node: '>=16.20.0'} @@ -6647,6 +7393,13 @@ snapshots: '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) '@better-auth/utils': 0.5.1 + '@better-auth/drizzle-adapter@1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0))': + dependencies: + '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) + '@better-auth/utils': 0.5.1 + optionalDependencies: + drizzle-orm: 0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0) + '@better-auth/kysely-adapter@1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(kysely@0.29.8)': dependencies: '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) @@ -6669,6 +7422,13 @@ snapshots: '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) '@better-auth/utils': 0.5.1 + '@better-auth/prisma-adapter@1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(@prisma/client@7.10.1(typescript@7.1.0-beta))': + dependencies: + '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) + '@better-auth/utils': 0.5.1 + optionalDependencies: + '@prisma/client': 7.10.1(typescript@7.1.0-beta) + '@better-auth/telemetry@1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)': dependencies: '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) @@ -7845,6 +8605,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tidbcloud/serverless@0.3.0': {} + '@tsrs/darwin-arm64@0.21.0-ts7.1.0-beta': optional: true @@ -8357,6 +9119,33 @@ snapshots: transitivePeerDependencies: - '@opentelemetry/api' + better-auth@1.7.17(@prisma/client@7.10.1(typescript@7.1.0-beta))(drizzle-orm@0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0))(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1): + dependencies: + '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) + '@better-auth/drizzle-adapter': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0)) + '@better-auth/kysely-adapter': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(kysely@0.29.8) + '@better-auth/memory-adapter': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1) + '@better-auth/mongo-adapter': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1) + '@better-auth/prisma-adapter': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(@prisma/client@7.10.1(typescript@7.1.0-beta)) + '@better-auth/telemetry': 1.7.17(@better-auth/core@1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3) + '@better-auth/utils': 0.5.1 + '@better-fetch/fetch': 1.3.3 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.5(zod@4.6.7) + defu: 6.1.7 + jose: 6.2.15 + kysely: 0.29.8 + nanostores: 1.5.5 + zod: 4.6.7 + optionalDependencies: + '@prisma/client': 7.10.1(typescript@7.1.0-beta) + drizzle-orm: 0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0) + mysql2: 3.24.6(@types/node@26.6.6) + pg: 8.23.1 + transitivePeerDependencies: + - '@opentelemetry/api' + better-auth@1.7.17(pg@8.23.1): dependencies: '@better-auth/core': 1.7.17(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(better-call@1.4.5(zod@4.6.7))(jose@6.2.15)(kysely@0.29.8)(nanostores@1.5.5) @@ -8642,6 +9431,19 @@ snapshots: dotenv@17.4.2: {} + drizzle-orm@0.45.4(@prisma/client@7.10.1(typescript@7.1.0-beta))(@types/pg@8.23.1)(kysely@0.29.8)(mysql2@3.24.6(@types/node@26.6.6))(pg@8.23.1)(postgres@4.0.0): + optionalDependencies: + '@prisma/client': 7.10.1(typescript@7.1.0-beta) + '@types/pg': 8.23.1 + kysely: 0.29.8 + mysql2: 3.24.6(@types/node@26.6.6) + pg: 8.23.1 + postgres: 4.0.0 + + drizzle-orm@0.45.4(@tidbcloud/serverless@0.3.0): + optionalDependencies: + '@tidbcloud/serverless': 0.3.0 + drizzle-orm@0.45.4(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3): optionalDependencies: '@types/better-sqlite3': 9.6.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b29755af..06265ed1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,6 +44,7 @@ catalog: postgres: 4.0.0 kysely: 0.29.8 drizzle-orm: 0.45.4 + "@tidbcloud/serverless": 0.3.0 prisma: 7.10.1 "@prisma/client": 7.10.1 "@prisma/adapter-pg": 7.10.1 diff --git a/tests/README.md b/tests/README.md index b4d598d5..67aa16ed 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,6 @@ # Scenario tests -Requires Linux, Node.js 26 (CI pins 26.8.1) and the repository's pnpm version. pnpm installs Bun for the scenarios that run it, at the version in the root catalog. Start PostgreSQL, MySQL and MariaDB yourself with `docker compose -f tests/compose.yaml up -d --wait`, then run from the repository root: +Requires Linux, the Node.js version in `.node-version` and the repository's pnpm version. pnpm installs Bun for the scenarios that run it, at the version in the root catalog. Start PostgreSQL, MySQL and MariaDB yourself with `docker compose -f tests/compose.yaml up -d --wait`, then run from the repository root: ```sh pnpm install --frozen-lockfile @@ -9,14 +9,16 @@ pnpm test pg-left-join # one scenario pnpm test pg-left-join --update # update snapshots; review the diff ``` -Tests use PostgreSQL at `127.0.0.1:55432`, MySQL 8.4 at `127.0.0.1:53306` and MariaDB 11.8 at `127.0.0.1:53307`; set `TYPESQL_DATABASE_URL`, `TYPESQL_MYSQL_URL` or `TYPESQL_MARIADB_URL` to use other instances. A scenario's `scenario.database` (`postgres`, `mysql`, `mariadb` or `sqlite`) selects the server; each run gets a database of its own in `DATABASE_URL` (with a `mysql://` or `mariadb://` scheme for those servers) or a file in `SQLITE_PATH`. +`tests/compose.yaml` pins the server versions and ports, and the runner checks a server's version before the first scenario that uses it; set `TYPESQL_DATABASE_URL`, `TYPESQL_MYSQL_URL` or `TYPESQL_MARIADB_URL` to use other instances. A scenario's `scenario.database` (`postgres`, `mysql`, `mariadb` or `sqlite`) selects the server; each run gets a database of its own in `DATABASE_URL` (with a `mysql://` or `mariadb://` scheme for those servers) or a file in `SQLITE_PATH`. -Scenarios live in `tests/scenarios/`. Copy an existing one, use `workspace:*` for TypeSQL and `catalog:` for external dependencies, and import public APIs. The runner builds the libraries and runs temporary copies with the installed workspace dependencies, checking types, compiler errors, output, and generated files against `expected.snap`. Commands reuse the workspace-selected pnpm executable through `npm_execpath`, including in temporary copies outside the workspace. +Scenarios live in `tests/scenarios/`. Copy an existing one, use `workspace:*` for TypeSQL and `catalog:` for external dependencies, and import public APIs. The runner builds the TypeSQL packages the scenarios depend on and runs temporary copies with the installed workspace dependencies, checking types, compiler errors, output, and generated files against `expected.snap`. Commands reuse the workspace-selected pnpm executable through `npm_execpath`, including in temporary copies outside the workspace. -A normal scenario has `package.json`, `app.ts` and `expected.snap`. Use ordinary `setup`, `generate`, `compile` and `start` scripts for its public workflow. Export values or type aliases whose inferred types should be observed; `scenario.artifacts` lists generated files to snapshot. An intentional compiler or runtime rejection declares `scenario.failure` with its `stage` and `exitCode`; an application contains no assertions, suppressed expected errors or type-equality helpers. +A normal scenario has `package.json`, `app.ts` and `expected.snap`. Use ordinary `setup`, `generate` and `start` scripts for its public workflow. The runner compiles it with `tsc` and `tests/scenarios/tsconfig.base.json`, which a scenario's own `tsconfig.json` can override (without `extends` or path aliases); add a `compile` script only to compile differently. Export values or type aliases whose inferred types should be observed; `scenario.artifacts` lists generated files to snapshot. An intentional compiler or runtime rejection declares `scenario.failure` with its `stage` and `exitCode`; an application contains no assertions, suppressed expected errors or type-equality helpers. + +A snapshot records each fact once: a failed compilation's diagnostics without the compiler's text output, and an uncaught exception without Node's report and stack trace. Redirect a tool's chatter to a log file in the scenario rather than into the snapshot. For bug fixes, define the expected public types, diagnostics or database behavior first, reproduce the failure against the unfixed checkout, keep the scenario, and run the full suite after fixing it. Snapshot updates require `--update` and review; CI refuses updates. Include nullable and non-nullable cases when changing nullability. Report a failure with the scenario, stage, command and expected-versus-observed difference. Missing tools or database connections are infrastructure failures, separate from intentional application rejections. -Failure logs and retained applications are in `tests/.artifacts/`. +Failure logs and retained applications of the latest run are in `tests/.artifacts/`. diff --git a/tests/package.json b/tests/package.json index 5cc5ba77..8ec3887a 100644 --- a/tests/package.json +++ b/tests/package.json @@ -4,8 +4,7 @@ "type": "module", "packageManager": "pnpm@12.16.1", "scripts": { - "test": "vitest run", - "typecheck": "tsc -p tsconfig.json" + "test": "vitest run" }, "devDependencies": { "vitest": "catalog:", diff --git a/tests/run.ts b/tests/run.ts deleted file mode 100644 index b702d4eb..00000000 --- a/tests/run.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { spawn } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import { resolve } from "node:path"; -import { parseArgs } from "node:util"; - -const args = process.argv.slice(2); -const { tokens } = parseArgs({ - args, - options: { update: { type: "boolean", short: "u" } }, - strict: false, - allowPositionals: true, - tokens: true, -}); -if ( - process.env.CI && - tokens.some((t) => t.kind === "option" && t.name === "update") -) - throw new Error("CI must never update scenario snapshots"); -const running = spawn("pnpm", ["exec", "vitest", "run", ...args], { - cwd: import.meta.dirname, - stdio: "inherit", - env: { - ...process.env, - SCENARIO_ARTIFACTS: - process.env.SCENARIO_ARTIFACTS ?? - resolve(import.meta.dirname, ".artifacts", randomUUID()), - }, -}); -for (const signal of ["SIGINT", "SIGTERM"] as const) - process.on(signal, () => running.kill(signal)); -process.exitCode = await new Promise((done, fail) => { - running.once("error", fail); - running.once("close", (code) => done(code ?? 1)); -}); diff --git a/tests/scenario.test.ts b/tests/scenario.test.ts deleted file mode 100644 index e3adec09..00000000 --- a/tests/scenario.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - access, - cp, - mkdir, - mkdtemp, - readFile, - rm, - symlink, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { basename, join, resolve } from "node:path"; -import { expect, test } from "vitest"; -import { - command, - required, - unlessMissing, - withCommands, -} from "./support/command.ts"; -import { allocate } from "./support/database.ts"; -import type { - ApplicationException, - CompilerObservation, - Observation, - Scenario, -} from "./support/types.ts"; - -const workspace = import.meta.dirname; -const repository = resolve(workspace, ".."); - -// Generated discovery files only select a scenario. Assertions remain Vitest's. -export function scenario(name: string) { - test(name, async ({ signal }) => - withCommands(signal, async () => { - let directory: string | undefined; - let database: Awaited> | undefined; - let stage = "prepare"; - const observed: Observation = {}; - try { - directory = await mkdtemp(join(tmpdir(), `typesql-${name}-`)); - const manifest = await prepare(name, directory); - if ( - manifest.scenario?.failure && - (!manifest.scripts[manifest.scenario.failure.stage] || - manifest.scenario.failure.exitCode <= 0) - ) - throw new Error("Invalid expected failure contract"); - database = await allocate(manifest.scenario?.database, directory); - const env = { - ...database.env, - SCENARIO_ERROR_FILE: join(directory, ".scenario", "exception.json"), - NODE_OPTIONS: `--import=${join(workspace, "support", "runtime.ts")}`, - }; - const normalize = (text: string) => { - let value = text - .replaceAll(directory!, "") - .replaceAll(repository, ""); - if (database?.env.DATABASE_URL) - value = value.replaceAll( - database.env.DATABASE_URL, - "", - ); - return value.replace(/\(node:\d+\)/g, "(node:)"); - }; - const script = async ( - step: "setup" | "generate" | "compile" | "start", - ) => { - stage = step; - const result = await command( - directory!, - ["pnpm", "--silent", "run", step], - env, - ); - observed[step] = { - status: result.status, - stdout: normalize(result.stdout), - stderr: normalize(result.stderr), - }; - const failure = manifest.scenario?.failure; - if ( - result.status !== 0 && - (failure?.stage !== step || result.status !== failure.exitCode) - ) - throw new Error( - `Unexpected ${step} failure (${result.status})\n${normalize(result.stdout + result.stderr)}`, - ); - if (failure?.stage === step && result.status === 0) - throw new Error( - `Expected ${step} to fail with ${failure.exitCode}, but it succeeded`, - ); - return result; - }; - for (const step of ["setup", "generate"] as const) { - if (manifest.scripts[step]) { - const result = await script(step); - if (result.status !== 0) { - expect( - JSON.parse(normalize(JSON.stringify(observed))), - ).toMatchSnapshot("scenario"); - return; - } - } - } - if (manifest.scripts.generate) { - stage = "generated artifacts"; - for (const file of manifest.scenario?.artifacts ?? []) - await access(artifact(directory, file)); - } - if (!manifest.scripts.compile) - throw new Error("Scenario requires a compile script"); - const compilation = await script("compile"); - stage = "types and diagnostics"; - await required(directory, [ - "node", - join(workspace, "support", "observe.ts"), - ]); - const compiler: CompilerObservation = JSON.parse( - await readFile(join(directory, ".scenario", "compiler.json"), "utf8"), - ); - observed.compiler = compiler; - if ((compilation.status === 0) !== (compiler.diagnostics.length === 0)) - throw new Error( - "Compiler command and observer disagree about diagnostics", - ); - if (compilation.status === 0 && manifest.scripts.start) { - await rm(env.SCENARIO_ERROR_FILE, { force: true }); - const runtime = await script("start"); - if (runtime.status !== 0) { - let exception: ApplicationException; - try { - exception = JSON.parse( - await readFile(env.SCENARIO_ERROR_FILE, "utf8"), - ); - } catch { - throw new Error( - "Infrastructure: expected application exception was not observed", - ); - } - observed.exception = exception; - } - } - stage = "artifacts"; - const artifacts: Record = {}; - for (const file of manifest.scenario?.artifacts ?? []) - artifacts[file] = normalize( - await readFile(artifact(directory, file), "utf8"), - ); - if (Object.keys(artifacts).length) observed.artifacts = artifacts; - stage = "snapshot"; - await writeFile( - join(directory, ".scenario", "observed.json"), - JSON.stringify(observed, null, 2), - ); - expect(JSON.parse(normalize(JSON.stringify(observed)))).toMatchSnapshot( - "scenario", - ); - } catch (cause) { - if (directory) { - const retained = resolve( - process.env.SCENARIO_ARTIFACTS ?? join(workspace, ".artifacts"), - name, - ); - await rm(retained, { recursive: true, force: true }); - await mkdir(retained, { recursive: true }); - await mkdir(join(directory, ".scenario"), { recursive: true }); - await writeFile( - join(directory, ".scenario", "failure.json"), - JSON.stringify( - { name, stage, observed, error: String(cause) }, - null, - 2, - ), - ); - await unlessMissing( - cp( - join(workspace, "scenarios", name, "expected.snap"), - join(directory, ".scenario", "expected.snap"), - ), - ); - await cp(directory, retained, { recursive: true }); - throw new Error( - `${name}: failed at ${stage}. Scenario and logs retained at ${retained}`, - { cause }, - ); - } - throw new Error(`${name}: failed at ${stage}`, { cause }); - } finally { - await database?.cleanup(); - if (directory) await rm(directory, { recursive: true, force: true }); - } - }), - ); -} - -function artifact(directory: string, file: string) { - const path = resolve(directory, file); - if (!path.startsWith(`${directory}/`)) - throw new Error(`Artifact escapes scenario: ${file}`); - return path; -} - -async function prepare(name: string, directory: string) { - const source = join(workspace, "scenarios", name); - await cp(source, directory, { - recursive: true, - filter: (path) => - !["node_modules", "dist", ".scenario", "expected.snap"].includes( - basename(path), - ), - }); - await symlink( - join(source, "node_modules"), - join(directory, "node_modules"), - "dir", - ); - await mkdir(join(directory, ".scenario")); - const config = JSON.parse( - await readFile(join(workspace, "tsconfig.json"), "utf8"), - ); - delete config.compilerOptions.noEmit; - delete config.compilerOptions.allowImportingTsExtensions; - config.include = ["**/*.ts"]; - const configPath = join(directory, "tsconfig.json"); - const text = await unlessMissing(readFile(configPath, "utf8")); - if (text !== undefined) { - const overrides = JSON.parse(text); - if ( - overrides.extends || - overrides.compilerOptions?.paths || - overrides.compilerOptions?.baseUrl - ) - throw new Error( - "Scenario compiler options must use public package imports", - ); - Object.assign(config, { - ...overrides, - compilerOptions: { - ...config.compilerOptions, - ...overrides.compilerOptions, - }, - }); - } - await writeFile(configPath, JSON.stringify(config, null, 2)); - const manifest: Scenario = JSON.parse( - await readFile(join(directory, "package.json"), "utf8"), - ); - return manifest; -} diff --git a/tests/scenarios/better-auth-postgres/package.json b/tests/scenarios/better-auth-postgres/package.json index bb287f2a..51174f33 100644 --- a/tests/scenarios/better-auth-postgres/package.json +++ b/tests/scenarios/better-auth-postgres/package.json @@ -5,7 +5,6 @@ "scripts": { "setup": "node setup.ts", "generate": "typesql introspect --out schema.ts", - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/better-auth-rejection/expected.snap b/tests/scenarios/better-auth-rejection/expected.snap index c0b65677..13031138 100644 --- a/tests/scenarios/better-auth-rejection/expected.snap +++ b/tests/scenarios/better-auth-rejection/expected.snap @@ -5,16 +5,6 @@ exports[`better-auth-rejection > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(20,42): error TS2345: Argument of type 'TransactionalSql' is not assignable to parameter of type 'TypesqlAdapterSql>'. - Type 'TransactionalSql' is not assignable to type '{ readonly [DIALECT]?: Brand | undefined; }'. - The types of '[DIALECT].inputs' are incompatible between these types. - Type '(inputs: [DB, PgTypeMap, [CasePlugin]]) => [DB, PgTypeMap, [CasePlugin]]' is not assignable to type '(inputs: [never, never, []]) => [unknown, unknown, []]'. - Types of parameters 'inputs' and 'inputs' are incompatible. - Type '[never, never, []]' is not assignable to type '[DB, PgTypeMap, [CasePlugin]]'. - Type at position 2 in source is not compatible with type at position 2 in target. - Type '[]' is not assignable to type '[CasePlugin]'. - Source has 0 element(s) but target requires 1. -", }, "compiler": { "diagnostics": [ @@ -26,87 +16,55 @@ exports[`better-auth-rejection > scenario 1`] = ` { "category": 1, "code": 2322, - "column": 42, "details": [ { "category": 1, "code": 2200, - "column": 42, "details": [ { "category": 1, "code": 2322, - "column": 42, "details": [ { "category": 1, "code": 2328, - "column": 42, "details": [ { "category": 1, "code": 2322, - "column": 42, "details": [ { "category": 1, "code": 2626, - "column": 42, "details": [ { "category": 1, "code": 2322, - "column": 42, "details": [ { "category": 1, "code": 2618, - "column": 42, - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Source has 0 element(s) but target requires 1.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Type '[]' is not assignable to type '[CasePlugin]'.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Type at position 2 in source is not compatible with type at position 2 in target.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Type '[never, never, []]' is not assignable to type '[DB, PgTypeMap, [CasePlugin]]'.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Types of parameters 'inputs' and 'inputs' are incompatible.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Type '(inputs: [DB, PgTypeMap, [CasePlugin]]) => [DB, PgTypeMap, [CasePlugin]]' is not assignable to type '(inputs: [never, never, []]) => [unknown, unknown, []]'.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "The types of '[DIALECT].inputs' are incompatible between these types.", }, ], - "expression": "pg.withPlugin(new CasePlugin())", - "file": "app.ts", - "line": 20, "message": "Type 'TransactionalSql' is not assignable to type '{ readonly [DIALECT]?: Brand | undefined; }'.", }, ], diff --git a/tests/scenarios/better-auth-rejection/package.json b/tests/scenarios/better-auth-rejection/package.json index 68b6363e..eda8a8bc 100644 --- a/tests/scenarios/better-auth-rejection/package.json +++ b/tests/scenarios/better-auth-rejection/package.json @@ -2,9 +2,6 @@ "name": "better-auth-rejection", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*", "@typesql/better-auth": "workspace:*", diff --git a/tests/scenarios/better-auth-sqlite/package.json b/tests/scenarios/better-auth-sqlite/package.json index fe9950f1..1f566c0a 100644 --- a/tests/scenarios/better-auth-sqlite/package.json +++ b/tests/scenarios/better-auth-sqlite/package.json @@ -5,7 +5,6 @@ "scripts": { "setup": "node setup.ts", "generate": "typesql introspect --dialect sqlite --db \"$SQLITE_PATH\" --out schema.ts", - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/better-sqlite3/package.json b/tests/scenarios/better-sqlite3/package.json index 7a565bb9..a622673e 100644 --- a/tests/scenarios/better-sqlite3/package.json +++ b/tests/scenarios/better-sqlite3/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/bun-sqlite/package.json b/tests/scenarios/bun-sqlite/package.json index 683e3c89..adfaf13e 100644 --- a/tests/scenarios/bun-sqlite/package.json +++ b/tests/scenarios/bun-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "bun dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/cli-introspect-database-url/app.ts b/tests/scenarios/cli-introspect-database-url/app.ts new file mode 100644 index 00000000..bcdd8caf --- /dev/null +++ b/tests/scenarios/cli-introspect-database-url/app.ts @@ -0,0 +1,12 @@ +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +// `typesql introspect` without --dialect or --url reads DATABASE_URL, and +// its mysql:// scheme selects MySQL. +import type { DB } from "./schema.js"; + +const pool = createPool(String(process.env.DATABASE_URL)); +const sql = createSql(pool); +export const rows = await sql("SELECT id, email, bio FROM users").execute(); +console.log(rows); +await pool.end(); diff --git a/tests/scenarios/cli-introspect-database-url/expected.snap b/tests/scenarios/cli-introspect-database-url/expected.snap new file mode 100644 index 00000000..52ded5aa --- /dev/null +++ b/tests/scenarios/cli-introspect-database-url/expected.snap @@ -0,0 +1,47 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli-introspect-database-url > scenario 1`] = ` +{ + "artifacts": { + "schema.ts": "// Generated by \`typesql introspect\` from MySQL 8.4. Do not edit. +import type { Col } from "@typesql/core/mysql"; + +export interface DB { + users: { + id: Col<"int unsigned", { position: 0; autoIncrement: true }>; + email: Col<"varchar", { position: 1 }>; + bio: Col<"text", { nullable: true; position: 2 }>; + }; +} +", + }, + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "rows": "({ bio: null | string; email: string; id: number })[]", + }, + }, + "generate": { + "status": 0, + "stderr": "typesql introspect: wrote schema.ts +", + "stdout": "", + }, + "setup": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ { id: 1, email: 'ada@example.com', bio: null } ] +", + }, +} +`; diff --git a/tests/scenarios/cli-introspect-database-url/package.json b/tests/scenarios/cli-introspect-database-url/package.json new file mode 100644 index 00000000..9b3bea93 --- /dev/null +++ b/tests/scenarios/cli-introspect-database-url/package.json @@ -0,0 +1,26 @@ +{ + "name": "cli-introspect-database-url", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js", + "setup": "node setup.ts", + "generate": "typesql introspect --out schema.ts" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/cli": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql", + "artifacts": [ + "schema.ts" + ] + } +} diff --git a/tests/scenarios/cli-introspect-database-url/setup.ts b/tests/scenarios/cli-introspect-database-url/setup.ts new file mode 100644 index 00000000..f7c774f3 --- /dev/null +++ b/tests/scenarios/cli-introspect-database-url/setup.ts @@ -0,0 +1,8 @@ +import { createConnection } from "mysql2/promise"; + +const db = await createConnection(String(process.env.DATABASE_URL)); +await db.query( + "CREATE TABLE users (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL, bio TEXT)", +); +await db.query("INSERT INTO users (email) VALUES ('ada@example.com')"); +await db.end(); diff --git a/tests/scenarios/cli-introspect-mariadb/package.json b/tests/scenarios/cli-introspect-mariadb/package.json index e5f285b7..a83c72fe 100644 --- a/tests/scenarios/cli-introspect-mariadb/package.json +++ b/tests/scenarios/cli-introspect-mariadb/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "setup": "node setup.ts", "generate": "typesql introspect --url \"$DATABASE_URL\" --out schema.ts" diff --git a/tests/scenarios/cli-introspect-mysql/package.json b/tests/scenarios/cli-introspect-mysql/package.json index 9e75a812..870f3bab 100644 --- a/tests/scenarios/cli-introspect-mysql/package.json +++ b/tests/scenarios/cli-introspect-mysql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "setup": "node setup.ts", "generate": "typesql introspect --url \"$DATABASE_URL\" --out schema.ts" diff --git a/tests/scenarios/cli-introspect-postgres/package.json b/tests/scenarios/cli-introspect-postgres/package.json index 2ffbf952..72771a73 100644 --- a/tests/scenarios/cli-introspect-postgres/package.json +++ b/tests/scenarios/cli-introspect-postgres/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "setup": "node setup.ts", "generate": "typesql introspect --out schema.ts" diff --git a/tests/scenarios/cli-introspect-sqlite-exclude/app.ts b/tests/scenarios/cli-introspect-sqlite-exclude/app.ts new file mode 100644 index 00000000..79ca4550 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-exclude/app.ts @@ -0,0 +1,4 @@ +import type { DB } from "./schema.js"; + +// `--exclude "drafts, Logs_*"` leaves out both patterns, spaces trimmed. +export type Tables = keyof DB; diff --git a/tests/scenarios/cli-introspect-sqlite-exclude/expected.snap b/tests/scenarios/cli-introspect-sqlite-exclude/expected.snap new file mode 100644 index 00000000..46aae899 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-exclude/expected.snap @@ -0,0 +1,40 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli-introspect-sqlite-exclude > scenario 1`] = ` +{ + "artifacts": { + "schema.ts": "// Generated by \`typesql introspect\` from SQLite 3.53.4. Do not edit. +import type { Col } from "@typesql/core/sqlite"; + +export interface DB { + users: { + id: Col<"integer", { hasDefault: true; position: 0; rowid: true }>; + name: Col<"text", { position: 1 }>; + }; +} +", + }, + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "Tables": ""users"", + }, + }, + "generate": { + "status": 0, + "stderr": "typesql introspect: wrote schema.ts +", + "stdout": "", + }, + "setup": { + "status": 0, + "stderr": "", + "stdout": "", + }, +} +`; diff --git a/tests/scenarios/cli-introspect-sqlite-exclude/package.json b/tests/scenarios/cli-introspect-sqlite-exclude/package.json new file mode 100644 index 00000000..0e924178 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-exclude/package.json @@ -0,0 +1,23 @@ +{ + "name": "cli-introspect-sqlite-exclude", + "private": true, + "type": "module", + "scripts": { + "setup": "node setup.ts", + "generate": "typesql introspect --dialect sqlite --db \"$SQLITE_PATH\" --exclude \"drafts, Logs_*\" --out schema.ts" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/cli": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite", + "artifacts": [ + "schema.ts" + ] + } +} diff --git a/tests/scenarios/cli-introspect-sqlite-exclude/setup.ts b/tests/scenarios/cli-introspect-sqlite-exclude/setup.ts new file mode 100644 index 00000000..c9d3e3b7 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-exclude/setup.ts @@ -0,0 +1,7 @@ +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +db.exec(`CREATE TABLE users (id integer PRIMARY KEY, name text NOT NULL); +CREATE TABLE drafts (id integer PRIMARY KEY); +CREATE TABLE logs_2026 (id integer PRIMARY KEY);`); +db.close(); diff --git a/tests/scenarios/cli-introspect-sqlite-virtual/app.ts b/tests/scenarios/cli-introspect-sqlite-virtual/app.ts new file mode 100644 index 00000000..b0d6a017 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-virtual/app.ts @@ -0,0 +1,11 @@ +import { DatabaseSync } from "node:sqlite"; +import { createSql } from "@typesql/sqlite"; +import type { DB } from "./schema.js"; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +const sql = createSql(db); +export const archived = await sql( + "SELECT id, title FROM docs_archive ORDER BY id", +).execute(); +console.log(archived); +db.close(); diff --git a/tests/scenarios/cli-introspect-sqlite-virtual/expected.snap b/tests/scenarios/cli-introspect-sqlite-virtual/expected.snap new file mode 100644 index 00000000..18b7b3ac --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-virtual/expected.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli-introspect-sqlite-virtual > scenario 1`] = ` +{ + "artifacts": { + "schema.ts": "// Generated by \`typesql introspect\` from SQLite 3.53.4. Do not edit. +import type { Col } from "@typesql/core/sqlite"; + +export interface DB { + docs: { + title: Col<"any", { nullable: true; position: 0 }>; + docs: Col<"any", { nullable: true; hasDefault: true; hidden: true }>; + rank: Col<"any", { nullable: true; hasDefault: true; hidden: true }>; + }; + docs_archive: { + id: Col<"integer", { hasDefault: true; position: 0; rowid: true }>; + title: Col<"text", { position: 1 }>; + }; +} +", + }, + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "archived": "({ id: number; title: string })[]", + }, + }, + "generate": { + "status": 0, + "stderr": "typesql introspect: wrote schema.ts +", + "stdout": "", + }, + "setup": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ [Object: null prototype] { id: 1, title: 'Old' } ] +", + }, +} +`; diff --git a/tests/scenarios/cli-introspect-sqlite-virtual/package.json b/tests/scenarios/cli-introspect-sqlite-virtual/package.json new file mode 100644 index 00000000..491a6019 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-virtual/package.json @@ -0,0 +1,25 @@ +{ + "name": "cli-introspect-sqlite-virtual", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js", + "setup": "node setup.ts", + "generate": "typesql introspect --dialect sqlite --db \"$SQLITE_PATH\" --out schema.ts" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/cli": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite", + "artifacts": [ + "schema.ts" + ] + } +} diff --git a/tests/scenarios/cli-introspect-sqlite-virtual/setup.ts b/tests/scenarios/cli-introspect-sqlite-virtual/setup.ts new file mode 100644 index 00000000..28b20d62 --- /dev/null +++ b/tests/scenarios/cli-introspect-sqlite-virtual/setup.ts @@ -0,0 +1,11 @@ +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +// `docs_archive` is an ordinary table whose name starts like the shadow tables +// of the virtual table `docs` (docs_data, docs_idx, docs_content, …). +db.exec(` + CREATE VIRTUAL TABLE docs USING fts5(title); + CREATE TABLE docs_archive (id integer PRIMARY KEY, title text NOT NULL); + INSERT INTO docs_archive VALUES (1, 'Old'); +`); +db.close(); diff --git a/tests/scenarios/cli-introspect-sqlite/package.json b/tests/scenarios/cli-introspect-sqlite/package.json index 3a07216e..1424ff00 100644 --- a/tests/scenarios/cli-introspect-sqlite/package.json +++ b/tests/scenarios/cli-introspect-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "setup": "node setup.ts", "generate": "typesql introspect --dialect sqlite --db \"$SQLITE_PATH\" --out schema.ts" diff --git a/tests/scenarios/cli-introspect-type-name/expected.snap b/tests/scenarios/cli-introspect-type-name/expected.snap new file mode 100644 index 00000000..c316d66e --- /dev/null +++ b/tests/scenarios/cli-introspect-type-name/expected.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli-introspect-type-name > scenario 1`] = ` +{ + "generate": { + "status": 2, + "stderr": "typesql introspect: the schema's type name "my-db" is not a TypeScript identifier +", + "stdout": "", + }, + "setup": { + "status": 0, + "stderr": "", + "stdout": "", + }, +} +`; diff --git a/tests/scenarios/cli-introspect-type-name/package.json b/tests/scenarios/cli-introspect-type-name/package.json new file mode 100644 index 00000000..47a55498 --- /dev/null +++ b/tests/scenarios/cli-introspect-type-name/package.json @@ -0,0 +1,22 @@ +{ + "name": "cli-introspect-type-name", + "private": true, + "type": "module", + "scripts": { + "setup": "node setup.ts", + "generate": "typesql introspect --db \"$SQLITE_PATH\" --type-name my-db --out schema.ts" + }, + "dependencies": { + "@typesql/cli": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite", + "failure": { + "stage": "generate", + "exitCode": 2 + } + } +} diff --git a/tests/scenarios/cli-introspect-type-name/setup.ts b/tests/scenarios/cli-introspect-type-name/setup.ts new file mode 100644 index 00000000..b6f0753f --- /dev/null +++ b/tests/scenarios/cli-introspect-type-name/setup.ts @@ -0,0 +1,6 @@ +import { DatabaseSync } from "node:sqlite"; + +// --type-name names the schema interface, so it must be an identifier. +const db = new DatabaseSync(String(process.env.SQLITE_PATH)); +db.exec("CREATE TABLE users (id integer PRIMARY KEY, name text NOT NULL)"); +db.close(); diff --git a/tests/scenarios/cli-invalid-command/app.ts b/tests/scenarios/cli-invalid-command/app.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/tests/scenarios/cli-invalid-command/app.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/tests/scenarios/cli-invalid-command/package.json b/tests/scenarios/cli-invalid-command/package.json index 85baa249..92c6c19a 100644 --- a/tests/scenarios/cli-invalid-command/package.json +++ b/tests/scenarios/cli-invalid-command/package.json @@ -3,16 +3,11 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "generate": "typesql migrate invalid-command" }, "dependencies": { "@typesql/cli": "workspace:*" }, - "devDependencies": { - "typescript": "catalog:", - "@types/node": "catalog:" - }, "scenario": { "failure": { "stage": "generate", diff --git a/tests/scenarios/cli-main-io/app.ts b/tests/scenarios/cli-main-io/app.ts new file mode 100644 index 00000000..0a816dc9 --- /dev/null +++ b/tests/scenarios/cli-main-io/app.ts @@ -0,0 +1,15 @@ +import { main } from "@typesql/cli"; + +// `main` writes through the io it is given, for every command. +const stdout: string[] = []; +const stderr: string[] = []; +const io = { + stdout: (text: string) => void stdout.push(text), + stderr: (text: string) => void stderr.push(text), +}; +export const introspected = await main( + ["introspect", "--db", process.env.SQLITE_PATH!], + io, +); +export const usage = await main(["introspect", "--dialect", "sqlite"], io); +console.log(JSON.stringify({ introspected, usage, stdout, stderr }, null, 2)); diff --git a/tests/scenarios/cli-main-io/expected.snap b/tests/scenarios/cli-main-io/expected.snap new file mode 100644 index 00000000..ba6a6dcd --- /dev/null +++ b/tests/scenarios/cli-main-io/expected.snap @@ -0,0 +1,38 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli-main-io > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "introspected": "number", + "usage": "number", + }, + }, + "setup": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + "introspected": 0, + "usage": 2, + "stdout": [ + "// Generated by \`typesql introspect\` from SQLite 3.53.4. Do not edit.\\nimport type { Col } from \\"@typesql/core/sqlite\\";\\n\\nexport interface DB {\\n\\tusers: {\\n\\t\\tid: Col<\\"integer\\", { hasDefault: true; position: 0; rowid: true }>;\\n\\t\\tname: Col<\\"text\\", { position: 1 }>;\\n\\t};\\n}\\n" + ], + "stderr": [ + "typesql introspect: pass --db \\n" + ] +} +", + }, +} +`; diff --git a/tests/scenarios/cli-main-io/package.json b/tests/scenarios/cli-main-io/package.json new file mode 100644 index 00000000..b9b3f417 --- /dev/null +++ b/tests/scenarios/cli-main-io/package.json @@ -0,0 +1,19 @@ +{ + "name": "cli-main-io", + "private": true, + "type": "module", + "scripts": { + "setup": "node setup.ts", + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/cli": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/cli-main-io/setup.ts b/tests/scenarios/cli-main-io/setup.ts new file mode 100644 index 00000000..d0a7e06b --- /dev/null +++ b/tests/scenarios/cli-main-io/setup.ts @@ -0,0 +1,5 @@ +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +db.exec("CREATE TABLE users (id integer PRIMARY KEY, name text NOT NULL)"); +db.close(); diff --git a/tests/scenarios/cli-make/package.json b/tests/scenarios/cli-make/package.json index 352ede1d..0f171ff6 100644 --- a/tests/scenarios/cli-make/package.json +++ b/tests/scenarios/cli-make/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "generate": "NODE_OPTIONS=--import=./fixed-time.ts typesql migrate make \"create users\"" }, diff --git a/tests/scenarios/cli-migrations-mysql/package.json b/tests/scenarios/cli-migrations-mysql/package.json index 6ecb61d3..93a07ae9 100644 --- a/tests/scenarios/cli-migrations-mysql/package.json +++ b/tests/scenarios/cli-migrations-mysql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "generate": "typesql migrate status && typesql migrate generate && typesql migrate up && typesql migrate down && typesql migrate latest" }, diff --git a/tests/scenarios/cli-migrations-postgres/package.json b/tests/scenarios/cli-migrations-postgres/package.json index a38ccbe1..3c0c0dc1 100644 --- a/tests/scenarios/cli-migrations-postgres/package.json +++ b/tests/scenarios/cli-migrations-postgres/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "generate": "typesql migrate status && typesql migrate generate && typesql migrate up && typesql migrate down && typesql migrate latest" }, diff --git a/tests/scenarios/cli-migrations-sqlite/package.json b/tests/scenarios/cli-migrations-sqlite/package.json index 075e9c1b..1973451e 100644 --- a/tests/scenarios/cli-migrations-sqlite/package.json +++ b/tests/scenarios/cli-migrations-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", "generate": "typesql migrate status && typesql migrate generate && typesql migrate up && typesql migrate down && typesql migrate latest" }, diff --git a/tests/scenarios/compiler-nullability/expected.snap b/tests/scenarios/compiler-nullability/expected.snap index 62fb373f..2bb4ef86 100644 --- a/tests/scenarios/compiler-nullability/expected.snap +++ b/tests/scenarios/compiler-nullability/expected.snap @@ -5,14 +5,6 @@ exports[`compiler-nullability > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(12,56): error TS2345: Argument of type 'null' is not assignable to parameter of type 'string'. -app.ts(13,5): error TS2345: Argument of type '"INSERT INTO users (id) VALUES ($1)"' is not assignable to parameter of type 'SqlError<"null value in column \\"name\\" of relation \\"users\\" violates not-null constraint", 2> & "INSERT INTO users (id) VALUES ($1)"'. - Type 'string' is not assignable to type 'SqlError<"null value in column \\"name\\" of relation \\"users\\" violates not-null constraint", 2>'. -app.ts(14,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a null value cannot be used as text NOT NULL", 5> & "UPDATE users SET name = \\0P0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a null value cannot be used as text NOT NULL", 5>'. -app.ts(15,5): error TS2345: Argument of type '"SELECT slugify(name) FROM users"' is not assignable to parameter of type 'SqlError<"function slugify(text) does not exist. Declare it in the schema's $functions or in FunctionCatalog", 1> & "SELECT slugify(name) FROM users"'. - Type 'string' is not assignable to type 'SqlError<"function slugify(text) does not exist. Declare it in the schema's $functions or in FunctionCatalog", 1>'. -", }, "compiler": { "diagnostics": [ @@ -33,10 +25,6 @@ app.ts(15,5): error TS2345: Argument of type '"SELECT slugify(name) FROM users"' { "category": 1, "code": 2322, - "column": 5, - "expression": ""INSERT INTO users (id) VALUES ($1)"", - "file": "app.ts", - "line": 13, "message": "Type 'string' is not assignable to type 'SqlError<"null value in column \\"name\\" of relation \\"users\\" violates not-null constraint", 2>'.", }, ], @@ -53,10 +41,6 @@ app.ts(15,5): error TS2345: Argument of type '"SELECT slugify(name) FROM users"' { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE users SET name = \${sql.param(null)}\`", - "file": "app.ts", - "line": 14, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a null value cannot be used as text NOT NULL", 5>'.", }, ], @@ -73,10 +57,6 @@ app.ts(15,5): error TS2345: Argument of type '"SELECT slugify(name) FROM users"' { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT slugify(name) FROM users"", - "file": "app.ts", - "line": 15, "message": "Type 'string' is not assignable to type 'SqlError<"function slugify(text) does not exist. Declare it in the schema's $functions or in FunctionCatalog", 1>'.", }, ], diff --git a/tests/scenarios/compiler-nullability/package.json b/tests/scenarios/compiler-nullability/package.json index 149ce792..9205a0dc 100644 --- a/tests/scenarios/compiler-nullability/package.json +++ b/tests/scenarios/compiler-nullability/package.json @@ -2,9 +2,6 @@ "name": "compiler-nullability", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*" }, diff --git a/tests/scenarios/compiler-rejection/expected.snap b/tests/scenarios/compiler-rejection/expected.snap index 34af4f71..efa53ce3 100644 --- a/tests/scenarios/compiler-rejection/expected.snap +++ b/tests/scenarios/compiler-rejection/expected.snap @@ -5,18 +5,6 @@ exports[`compiler-rejection > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(8,43): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. -app.ts(9,5): error TS2345: Argument of type '"SELECT missing FROM users"' is not assignable to parameter of type 'SqlError<"column \\"missing\\" does not exist", 1> & "SELECT missing FROM users"'. - Type 'string' is not assignable to type 'SqlError<"column \\"missing\\" does not exist", 1>'. -app.ts(11,5): error TS2345: Argument of type '"SELECT id FROM users WHERE sum(id) > 0"' is not assignable to parameter of type 'SqlError<"aggregate functions are not allowed in WHERE", 0> & "SELECT id FROM users WHERE sum(id) > 0"'. - Type 'string' is not assignable to type 'SqlError<"aggregate functions are not allowed in WHERE", 0>'. -app.ts(12,5): error TS2345: Argument of type '"SELECT id FROM users WHERE row_number() OVER () > 0"' is not assignable to parameter of type 'SqlError<"window functions are not allowed in WHERE", 0> & "SELECT id FROM users WHERE row_number() OVER () > 0"'. - Type 'string' is not assignable to type 'SqlError<"window functions are not allowed in WHERE", 0>'. -app.ts(13,5): error TS2345: Argument of type '"SELECT sum(sum(id)) FROM users"' is not assignable to parameter of type 'SqlError<"aggregate function calls cannot be nested", 0> & "SELECT sum(sum(id)) FROM users"'. - Type 'string' is not assignable to type 'SqlError<"aggregate function calls cannot be nested", 0>'. -app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) FROM users"' is not assignable to parameter of type 'SqlError<"aggregate function calls cannot contain window function calls", 0> & "SELECT sum(row_number() OVER ()) FROM users"'. - Type 'string' is not assignable to type 'SqlError<"aggregate function calls cannot contain window function calls", 0>'. -", }, "compiler": { "diagnostics": [ @@ -37,10 +25,6 @@ app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT missing FROM users"", - "file": "app.ts", - "line": 9, "message": "Type 'string' is not assignable to type 'SqlError<"column \\"missing\\" does not exist", 1>'.", }, ], @@ -57,10 +41,6 @@ app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT id FROM users WHERE sum(id) > 0"", - "file": "app.ts", - "line": 11, "message": "Type 'string' is not assignable to type 'SqlError<"aggregate functions are not allowed in WHERE", 0>'.", }, ], @@ -77,10 +57,6 @@ app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT id FROM users WHERE row_number() OVER () > 0"", - "file": "app.ts", - "line": 12, "message": "Type 'string' is not assignable to type 'SqlError<"window functions are not allowed in WHERE", 0>'.", }, ], @@ -97,10 +73,6 @@ app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT sum(sum(id)) FROM users"", - "file": "app.ts", - "line": 13, "message": "Type 'string' is not assignable to type 'SqlError<"aggregate function calls cannot be nested", 0>'.", }, ], @@ -117,10 +89,6 @@ app.ts(14,5): error TS2345: Argument of type '"SELECT sum(row_number() OVER ()) { "category": 1, "code": 2322, - "column": 5, - "expression": ""SELECT sum(row_number() OVER ()) FROM users"", - "file": "app.ts", - "line": 14, "message": "Type 'string' is not assignable to type 'SqlError<"aggregate function calls cannot contain window function calls", 0>'.", }, ], diff --git a/tests/scenarios/compiler-rejection/package.json b/tests/scenarios/compiler-rejection/package.json index 64b65053..7356c304 100644 --- a/tests/scenarios/compiler-rejection/package.json +++ b/tests/scenarios/compiler-rejection/package.json @@ -2,9 +2,6 @@ "name": "compiler-rejection", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*" }, diff --git a/tests/scenarios/compiler-strict-null-checks/expected.snap b/tests/scenarios/compiler-strict-null-checks/expected.snap index 1337f16f..77bafd81 100644 --- a/tests/scenarios/compiler-strict-null-checks/expected.snap +++ b/tests/scenarios/compiler-strict-null-checks/expected.snap @@ -5,8 +5,6 @@ exports[`compiler-strict-null-checks > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(5,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"TypeSQL requires strictNullChecks: set strict or strictNullChecks to true in tsconfig.json", null>'. -", }, "compiler": { "diagnostics": [ diff --git a/tests/scenarios/compiler-strict-null-checks/package.json b/tests/scenarios/compiler-strict-null-checks/package.json index 3c4e0f6d..0edf62af 100644 --- a/tests/scenarios/compiler-strict-null-checks/package.json +++ b/tests/scenarios/compiler-strict-null-checks/package.json @@ -2,9 +2,6 @@ "name": "compiler-strict-null-checks", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*" }, diff --git a/tests/scenarios/core-workflows/package.json b/tests/scenarios/core-workflows/package.json index bd0fae45..b5c76fc7 100644 --- a/tests/scenarios/core-workflows/package.json +++ b/tests/scenarios/core-workflows/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/dollar-identifiers/app.ts b/tests/scenarios/dollar-identifiers/app.ts new file mode 100644 index 00000000..7f6975eb --- /dev/null +++ b/tests/scenarios/dollar-identifiers/app.ts @@ -0,0 +1,48 @@ +import { DatabaseSync } from "node:sqlite"; +import { createSql as createCoreSql } from "@typesql/core"; +import { mysql } from "@typesql/core/mysql"; +import { createSql } from "@typesql/pg"; +import { createSql as createSqliteSql } from "@typesql/sqlite"; +import { Pool } from "pg"; + +type DB = Record; + +// A name may contain `$` after its first character: `a$b$` and `x$1` are +// names, not a dollar quote or a parameter. +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); + +const named = sql("SELECT 1 AS a$b$, :y::int AS y", { y: 2 }); +console.log(named.text, named.values); +export const namedRows = await named.execute(); + +// A dollar quote after an operator or a space is still one. +const quoted = sql( + "SELECT $q$:z $1$q$ AS q, 5 =$$5$$::int AS eq, :w::int AS w", + { w: 6 }, +); +console.log(quoted.text, quoted.values); +export const quotedRows = await quoted.execute(); + +const inline = sql.as<{ x$1: number; p: number }>( + `SELECT 3 AS x$1, ${sql.param(4)}::int AS p`, +); +console.log(inline.text, inline.values); +export const inlineRows = await inline.execute(); + +console.log({ namedRows, quotedRows, inlineRows }); +await pool.end(); + +// SQLite and MySQL names take `$` too. +const sqlite = createSqliteSql(new DatabaseSync(":memory:")); +const sqliteQuery = sqlite.as<{ x$1: number; p: number }>( + `SELECT 3 AS x$1, ${sqlite.param(4)} AS p`, +); +console.log(sqliteQuery.text, sqliteQuery.values); +console.log(await sqliteQuery.executeTakeFirstOrThrow()); + +const my = createCoreSql(mysql); +const myQuery = my.as<{ x$1: number; p: number }>( + `SELECT 3 AS x$1, ${my.param(4)} AS p`, +); +console.log(myQuery.text, myQuery.values); diff --git a/tests/scenarios/dollar-identifiers/expected.snap b/tests/scenarios/dollar-identifiers/expected.snap new file mode 100644 index 00000000..c158e178 --- /dev/null +++ b/tests/scenarios/dollar-identifiers/expected.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`dollar-identifiers > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "inlineRows": "({ x$1: number; p: number })[]", + "namedRows": "({ a$b$: number; y: number })[]", + "quotedRows": "({ eq: false | true; q: string; w: number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "SELECT 1 AS a$b$, $1::int AS y [ 2 ] +SELECT $q$:z $1$q$ AS q, 5 =$$5$$::int AS eq, $1::int AS w [ 6 ] +SELECT 3 AS x$1, $1::int AS p [ 4 ] +{ + namedRows: [ { 'a$b$': 1, y: 2 } ], + quotedRows: [ { q: ':z $1', eq: true, w: 6 } ], + inlineRows: [ { 'x$1': 3, p: 4 } ] +} +SELECT 3 AS x$1, $1 AS p [ 4 ] +[Object: null prototype] { 'x$1': 3, p: 4 } +SELECT 3 AS x$1, $1 AS p [ 4 ] +", + }, +} +`; diff --git a/tests/scenarios/pg-nested-comments-runtime/package.json b/tests/scenarios/dollar-identifiers/package.json similarity index 61% rename from tests/scenarios/pg-nested-comments-runtime/package.json rename to tests/scenarios/dollar-identifiers/package.json index 877814bb..440189fb 100644 --- a/tests/scenarios/pg-nested-comments-runtime/package.json +++ b/tests/scenarios/dollar-identifiers/package.json @@ -1,17 +1,16 @@ { - "name": "pg-nested-comments-runtime", + "name": "dollar-identifiers", "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { + "@typesql/core": "workspace:*", "@typesql/pg": "workspace:*", - "@typesql/postgres": "workspace:*", + "@typesql/sqlite": "workspace:*", "pg": "catalog:", - "@types/pg": "catalog:", - "postgres": "catalog:" + "@types/pg": "catalog:" }, "devDependencies": { "typescript": "catalog:", diff --git a/tests/scenarios/drizzle-mysql-serverless/app.ts b/tests/scenarios/drizzle-mysql-serverless/app.ts new file mode 100644 index 00000000..e13aee2c --- /dev/null +++ b/tests/scenarios/drizzle-mysql-serverless/app.ts @@ -0,0 +1,24 @@ +import { connect } from "@tidbcloud/serverless"; +import { mysql } from "@typesql/core/mysql"; +import { createSql } from "@typesql/drizzle"; +import { drizzle } from "drizzle-orm/tidb-serverless"; + +// The real client over a fake transport: what reaches TiDB is what it was sent. +const sent: unknown[] = []; +const client = connect({ + url: "mysql://user:secret@tidb.invalid/app", + fetch: async (_url, init) => { + sent.push(JSON.parse(String(init?.body)).query); + return new Response( + JSON.stringify({ types: [], rows: [], rowsAffected: 0 }), + { headers: { "TiDB-Session": "session" } }, + ); + }, +}); +const sql = createSql(mysql, drizzle({ client })); + +// A `?` in a string is text, whichever side of a parameter it is on. +await sql("SELECT 'why?' AS note, $1 AS id", 7).execute(); +await sql("SELECT $1 AS quoted, 'why?' AS note", "it's").execute(); +await sql("SELECT 1 AS plain").execute(); +console.dir(sent, { depth: null }); diff --git a/tests/scenarios/drizzle-mysql-serverless/expected.snap b/tests/scenarios/drizzle-mysql-serverless/expected.snap new file mode 100644 index 00000000..db39a1b8 --- /dev/null +++ b/tests/scenarios/drizzle-mysql-serverless/expected.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`drizzle-mysql-serverless > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": {}, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ + "SELECT 'why?' AS note, 7 AS id", + "SELECT 'it''s' AS quoted, 'why?' AS note", + 'SELECT 1 AS plain' +] +", + }, +} +`; diff --git a/tests/scenarios/drizzle-mysql-serverless/package.json b/tests/scenarios/drizzle-mysql-serverless/package.json new file mode 100644 index 00000000..0f767ae6 --- /dev/null +++ b/tests/scenarios/drizzle-mysql-serverless/package.json @@ -0,0 +1,18 @@ +{ + "name": "drizzle-mysql-serverless", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@tidbcloud/serverless": "catalog:", + "@typesql/core": "workspace:*", + "@typesql/drizzle": "workspace:*", + "drizzle-orm": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + } +} diff --git a/tests/scenarios/drizzle-mysql/package.json b/tests/scenarios/drizzle-mysql/package.json index a1ee6490..f43fff6c 100644 --- a/tests/scenarios/drizzle-mysql/package.json +++ b/tests/scenarios/drizzle-mysql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/drizzle-pg/package.json b/tests/scenarios/drizzle-pg/package.json index 295b2e4f..09e645af 100644 --- a/tests/scenarios/drizzle-pg/package.json +++ b/tests/scenarios/drizzle-pg/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/drizzle-postgres/package.json b/tests/scenarios/drizzle-postgres/package.json index 8d4646cb..b4b32302 100644 --- a/tests/scenarios/drizzle-postgres/package.json +++ b/tests/scenarios/drizzle-postgres/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/drizzle-sqlite/package.json b/tests/scenarios/drizzle-sqlite/package.json index 63e505f4..85ceda45 100644 --- a/tests/scenarios/drizzle-sqlite/package.json +++ b/tests/scenarios/drizzle-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/kysely-mysql-databases/app.ts b/tests/scenarios/kysely-mysql-databases/app.ts new file mode 100644 index 00000000..a711fc89 --- /dev/null +++ b/tests/scenarios/kysely-mysql-databases/app.ts @@ -0,0 +1,42 @@ +import { mysql } from "@typesql/core/mysql"; +import { createSql } from "@typesql/kysely"; +import { type Generated, Kysely, sql as kysql, MysqlDialect } from "kysely"; +import { createPool } from "mysql2"; + +// A Kysely table key "database.table" names a table of another database. +interface DB { + users: { id: Generated; name: string }; + "typesql_archive.events": { + id: Generated; + user_id: number; + note: string | null; + }; +} +const pool = createPool(String(process.env.DATABASE_URL)); +const db = new Kysely({ dialect: new MysqlDialect({ pool }) }); +await kysql`DROP DATABASE IF EXISTS typesql_archive`.execute(db); +await kysql`CREATE DATABASE typesql_archive`.execute(db); +await kysql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL)`.execute( + db, +); +await kysql`CREATE TABLE typesql_archive.events (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, note VARCHAR(100))`.execute( + db, +); +await db.insertInto("users").values({ name: "Ada" }).execute(); +await db + .insertInto("typesql_archive.events") + .values({ user_id: 1, note: null }) + .execute(); + +const sql = createSql(mysql, db); +export const inserted = await sql( + "INSERT INTO typesql_archive.events (user_id, note) VALUES ($1, $2)", + 1, + "login", +).executeCount(); +export const rows = await sql( + "SELECT e.id, e.note, u.name FROM typesql_archive.events e JOIN users u ON u.id = e.user_id ORDER BY e.id", +).execute(); +console.log({ inserted, rows }); +await kysql`DROP DATABASE typesql_archive`.execute(db); +await db.destroy(); diff --git a/tests/scenarios/kysely-mysql-databases/expected.snap b/tests/scenarios/kysely-mysql-databases/expected.snap new file mode 100644 index 00000000..7f573cc7 --- /dev/null +++ b/tests/scenarios/kysely-mysql-databases/expected.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`kysely-mysql-databases > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "inserted": "number", + "rows": "({ id: number; name: string; note: null | string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + inserted: 1, + rows: [ + { id: 1, note: null, name: 'Ada' }, + { id: 2, note: 'login', name: 'Ada' } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/kysely-mysql-databases/package.json b/tests/scenarios/kysely-mysql-databases/package.json new file mode 100644 index 00000000..35d2a3f1 --- /dev/null +++ b/tests/scenarios/kysely-mysql-databases/package.json @@ -0,0 +1,21 @@ +{ + "name": "kysely-mysql-databases", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/kysely": "workspace:*", + "kysely": "catalog:", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/kysely-mysql/package.json b/tests/scenarios/kysely-mysql/package.json index 63ee676b..5a9257ac 100644 --- a/tests/scenarios/kysely-mysql/package.json +++ b/tests/scenarios/kysely-mysql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/kysely-postgres/package.json b/tests/scenarios/kysely-postgres/package.json index c380d76b..4650c8d4 100644 --- a/tests/scenarios/kysely-postgres/package.json +++ b/tests/scenarios/kysely-postgres/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/kysely-sqlite-attached/app.ts b/tests/scenarios/kysely-sqlite-attached/app.ts new file mode 100644 index 00000000..e6741d6c --- /dev/null +++ b/tests/scenarios/kysely-sqlite-attached/app.ts @@ -0,0 +1,41 @@ +import { sqlite } from "@typesql/core/sqlite"; +import { createSql } from "@typesql/kysely"; +import Database from "better-sqlite3"; +import { type Generated, Kysely, SqliteDialect } from "kysely"; + +const connection = new Database(process.env.SQLITE_PATH); +connection.exec(` + ATTACH DATABASE ':memory:' AS archive; + CREATE TABLE users (id integer PRIMARY KEY, name text NOT NULL); + CREATE TABLE archive.events (id integer PRIMARY KEY, user_id integer NOT NULL, note text); +`); +// A Kysely table key "schema.table" names a table of an attached database; +// "main.table" one of the main database. +interface DB { + "main.users": { id: Generated; name: string }; + "archive.events": { + id: Generated; + user_id: number; + note: string | null; + }; +} +const db = new Kysely({ + dialect: new SqliteDialect({ database: connection }), +}); +await db.insertInto("main.users").values({ name: "Ada" }).execute(); +await db + .insertInto("archive.events") + .values({ user_id: 1, note: null }) + .execute(); + +const sql = createSql(sqlite, db); +export const inserted = await sql( + "INSERT INTO archive.events (user_id, note) VALUES ($1, $2)", + 1, + "login", +).executeCount(); +export const rows = await sql( + "SELECT e.id, e.note, u.name FROM archive.events e JOIN users u ON u.id = e.user_id ORDER BY e.id", +).execute(); +console.log({ inserted, rows }); +await db.destroy(); diff --git a/tests/scenarios/kysely-sqlite-attached/expected.snap b/tests/scenarios/kysely-sqlite-attached/expected.snap new file mode 100644 index 00000000..b2e119e6 --- /dev/null +++ b/tests/scenarios/kysely-sqlite-attached/expected.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`kysely-sqlite-attached > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "inserted": "number", + "rows": "({ id: number; name: string; note: null | string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + inserted: 1, + rows: [ + { id: 1, note: null, name: 'Ada' }, + { id: 2, note: 'login', name: 'Ada' } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/kysely-sqlite-attached/package.json b/tests/scenarios/kysely-sqlite-attached/package.json new file mode 100644 index 00000000..f1962fce --- /dev/null +++ b/tests/scenarios/kysely-sqlite-attached/package.json @@ -0,0 +1,22 @@ +{ + "name": "kysely-sqlite-attached", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/kysely": "workspace:*", + "kysely": "catalog:", + "better-sqlite3": "catalog:", + "@types/better-sqlite3": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/kysely-sqlite/package.json b/tests/scenarios/kysely-sqlite/package.json index e78e48ec..302f0984 100644 --- a/tests/scenarios/kysely-sqlite/package.json +++ b/tests/scenarios/kysely-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/mariadb-connector/package.json b/tests/scenarios/mariadb-connector/package.json index fd15edce..f3fe3020 100644 --- a/tests/scenarios/mariadb-connector/package.json +++ b/tests/scenarios/mariadb-connector/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/mariadb-mysql2/package.json b/tests/scenarios/mariadb-mysql2/package.json index 55d6b44b..49668e2e 100644 --- a/tests/scenarios/mariadb-mysql2/package.json +++ b/tests/scenarios/mariadb-mysql2/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/migrations-mariadb/package.json b/tests/scenarios/migrations-mariadb/package.json index 06d6b3e7..4bb5dbe0 100644 --- a/tests/scenarios/migrations-mariadb/package.json +++ b/tests/scenarios/migrations-mariadb/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/migrations-mysql-keyword-names/app.ts b/tests/scenarios/migrations-mysql-keyword-names/app.ts new file mode 100644 index 00000000..75fbba6a --- /dev/null +++ b/tests/scenarios/migrations-mysql-keyword-names/app.ts @@ -0,0 +1,60 @@ +import { createMigrator, migration } from "@typesql/core/migration"; + +const down = async (): Promise => {}; +const tables = migration("mysql", { + name: "001-spans", + down, + up: async (db) => { + await db.raw("CREATE TABLE spans (id INT, `end` INT, `begin` INT)"); + return db; + }, +}); +// `NEW.end` and `AS end` are names: the statements after them are in the body. +const body = migration(tables, { + name: "002-trigger", + down, + up: async (db) => { + await db.raw(` + CREATE TRIGGER spans_end BEFORE INSERT ON spans FOR EACH ROW + BEGIN + SET NEW.end = NEW.id + 1; + SET @last = (SELECT 1 AS end); + END; + `); + return db; + }, +}); +// `begin` opens no body in these, so the COMMIT is its own statement. +const commits = [ + "CREATE PROCEDURE a(begin INT) SELECT 1; COMMIT", + "CREATE PROCEDURE begin() SELECT 1; COMMIT", + "CREATE PROCEDURE app.begin() SELECT 1; COMMIT", + // CASE … END FOR UPDATE ends the CASE, and BEGIN's END the body. + "CREATE PROCEDURE b() BEGIN SELECT id FROM spans ORDER BY CASE WHEN id > 0 THEN 1 END FOR UPDATE; END; COMMIT", +]; + +const url = process.env.DATABASE_URL; +const latest = async (next: typeof body): Promise => { + try { + await createMigrator({ + dialect: "mysql", + url, + migrations: [tables, next], + }).latest(); + console.log("applied", next.name); + } catch (error) { + console.log(error instanceof Error ? error.message : error); + } +}; +for (const [i, script] of commits.entries()) + await latest( + migration(tables, { + name: `002-commit-${i}`, + down, + up: async (db) => { + await db.raw(script); + return db; + }, + }), + ); +await latest(body); diff --git a/tests/scenarios/migrations-mysql-keyword-names/expected.snap b/tests/scenarios/migrations-mysql-keyword-names/expected.snap new file mode 100644 index 00000000..59c96a1e --- /dev/null +++ b/tests/scenarios/migrations-mysql-keyword-names/expected.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-mysql-keyword-names > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": {}, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "typesql: migration 2 (002-commit-0) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-1) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-2) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-3) failed: migrations cannot control transactions; each migration runs in its own transaction +applied 002-trigger +", + }, +} +`; diff --git a/tests/scenarios/migrations-mysql-keyword-names/package.json b/tests/scenarios/migrations-mysql-keyword-names/package.json new file mode 100644 index 00000000..98aae910 --- /dev/null +++ b/tests/scenarios/migrations-mysql-keyword-names/package.json @@ -0,0 +1,19 @@ +{ + "name": "migrations-mysql-keyword-names", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/migrations-mysql-values/app.ts b/tests/scenarios/migrations-mysql-values/app.ts new file mode 100644 index 00000000..e623a681 --- /dev/null +++ b/tests/scenarios/migrations-mysql-values/app.ts @@ -0,0 +1,55 @@ +import { createMigrator, migration } from "@typesql/core/migration"; +import type { Col } from "@typesql/core/mysql"; +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +// The migrator binds a value as @typesql/mysql does: a plain object is not a +// MySQL value (sql.json() writes JSON), and never becomes SQL text. Typed +// statements reject it; `.as()` statements are not checked. +const settings = { theme: "dark" }; + +const first = migration("mysql", { + name: "001-settings", + up: async (db) => { + const next = await db.ddl( + "CREATE TABLE settings (id INT PRIMARY KEY, data JSON NOT NULL)", + ); + await next( + "INSERT INTO settings VALUES (1, $1)", + db.json(settings), + ).execute(); + await next + .as("UPDATE settings SET data = $1 WHERE id = 1", settings) + .execute(); + return next; + }, + down: async (db) => { + await db.ddl("DROP TABLE settings"); + }, +}); +const migrator = createMigrator({ + dialect: "mysql", + url: process.env.DATABASE_URL, + migrations: [first], +}); +try { + await migrator.latest(); +} catch (error) { + console.log("migrator:", error instanceof Error ? error.message : error); +} + +interface DB { + settings: { id: Col<"int">; data: Col<"json"> }; +} +const pool = createPool(process.env.DATABASE_URL!); +const sql = createSql(pool); +try { + await sql + .as("UPDATE settings SET data = $1 WHERE id = 1", settings) + .execute(); +} catch (error) { + console.log("adapter:", error instanceof Error ? error.message : error); +} +export const rows = await sql("SELECT id, data FROM settings").execute(); +console.log(rows); +await pool.end(); diff --git a/tests/scenarios/migrations-mysql-values/expected.snap b/tests/scenarios/migrations-mysql-values/expected.snap new file mode 100644 index 00000000..6a8e1b3d --- /dev/null +++ b/tests/scenarios/migrations-mysql-values/expected.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-mysql-values > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "rows": "({ data: unknown; id: number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "migrator: typesql: migration 1 (001-settings) failed: cannot bind [object Object] as a MySQL value; use sql.json() for JSON; MySQL commits implicitly before and after each DDL statement, so its statements up to its last DDL statement stay applied and only the ones after it were rolled back +adapter: typesql: cannot bind [object Object] as a MySQL value; use sql.json() for JSON +[] +", + }, +} +`; diff --git a/tests/scenarios/migrations-mysql-values/package.json b/tests/scenarios/migrations-mysql-values/package.json new file mode 100644 index 00000000..b4640f5d --- /dev/null +++ b/tests/scenarios/migrations-mysql-values/package.json @@ -0,0 +1,20 @@ +{ + "name": "migrations-mysql-values", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/migrations-mysql/package.json b/tests/scenarios/migrations-mysql/package.json index 208e1e54..d586ad86 100644 --- a/tests/scenarios/migrations-mysql/package.json +++ b/tests/scenarios/migrations-mysql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/migrations-postgres-atomic-functions/app.ts b/tests/scenarios/migrations-postgres-atomic-functions/app.ts new file mode 100644 index 00000000..5a5f5815 --- /dev/null +++ b/tests/scenarios/migrations-postgres-atomic-functions/app.ts @@ -0,0 +1,145 @@ +import { createMigrator, migration } from "@typesql/core/migration"; +import { Pool } from "pg"; + +// SQL-standard routine bodies: their BEGIN and END are no transaction control. +const routines = migration("postgres", { + name: "001-routines", + up: async (db) => { + await db.raw(` + CREATE FUNCTION add_one(n integer) RETURNS integer + BEGIN ATOMIC + SELECT CASE WHEN n IS NULL THEN 0 ELSE n + 1 END; + END; + CREATE OR REPLACE PROCEDURE noop() + BEGIN ATOMIC + SELECT 1; + END; + CREATE TABLE after_routines (id integer); + `); + return db; + }, + down: async (db) => { + await db.raw("DROP FUNCTION add_one; DROP PROCEDURE noop"); + }, +}); +// A statement after a routine, or after a dollar-quoted body, is still checked. +const down = async (): Promise => {}; +const afterAtomic = migration(routines, { + name: "002-commit-after-atomic", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION two() RETURNS integer BEGIN ATOMIC SELECT 2; END; COMMIT", + ); + return db; + }, +}); +const afterQuoted = migration(routines, { + name: "002-commit-after-quoted", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION three() RETURNS integer LANGUAGE plpgsql AS $$ BEGIN RETURN 3; END $$; COMMIT", + ); + return db; + }, +}); +const bare = migration(routines, { + name: "002-bare-begin", + down, + up: async (db) => { + await db.raw("BEGIN"); + return db; + }, +}); + +// `begin` names a parameter, a function and a column here; it opens no body. +const afterParameter = migration(routines, { + name: "002-commit-after-parameter", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION overlap(begin date, finish date) RETURNS boolean LANGUAGE sql AS $$ SELECT begin < finish $$; COMMIT", + ); + return db; + }, +}); +const afterName = migration(routines, { + name: "002-commit-after-name", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION begin() RETURNS integer LANGUAGE sql AS $$ SELECT 1 $$; COMMIT", + ); + return db; + }, +}); +const afterAlias = migration(routines, { + name: "002-commit-after-alias", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION four() RETURNS integer BEGIN ATOMIC SELECT 4 AS begin; END; COMMIT", + ); + return db; + }, +}); +// `CASE … END FOR UPDATE` ends the CASE; FOR here starts a locking clause. +const afterLocking = migration(routines, { + name: "002-commit-after-locking", + down, + up: async (db) => { + await db.raw( + "CREATE FUNCTION five() RETURNS integer BEGIN ATOMIC SELECT 5 FROM after_routines ORDER BY CASE WHEN id > 0 THEN 1 END FOR UPDATE; END; COMMIT", + ); + return db; + }, +}); +// `end` as a name closes nothing: both statements are in the body. +const names = migration(routines, { + name: "003-end-as-a-name", + down, + up: async (db) => { + await db.raw(` + CREATE TABLE spans ("end" integer); + CREATE FUNCTION last_end() RETURNS integer + BEGIN ATOMIC + SELECT s.end FROM spans s; + SELECT 1 AS end; + END; + `); + return db; + }, +}); + +const url = process.env.DATABASE_URL; +await createMigrator({ + dialect: "postgres", + url, + migrations: [routines], +}).latest(); +const pool = new Pool({ connectionString: url }); +const { rows } = await pool.query("SELECT add_one(1) AS two"); +console.log(rows); +for (const next of [ + afterAtomic, + afterQuoted, + bare, + afterParameter, + afterName, + afterAlias, + afterLocking, + names, +]) { + try { + await createMigrator({ + dialect: "postgres", + url, + migrations: [routines, next], + }).latest(); + console.log("applied"); + } catch (error) { + console.log(error instanceof Error ? error.message : error); + } +} +await pool.end(); diff --git a/tests/scenarios/migrations-postgres-atomic-functions/expected.snap b/tests/scenarios/migrations-postgres-atomic-functions/expected.snap new file mode 100644 index 00000000..e88ebe16 --- /dev/null +++ b/tests/scenarios/migrations-postgres-atomic-functions/expected.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-postgres-atomic-functions > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": {}, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ { two: 2 } ] +typesql: migration 2 (002-commit-after-atomic) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-after-quoted) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-bare-begin) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-after-parameter) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-after-name) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-after-alias) failed: migrations cannot control transactions; each migration runs in its own transaction +typesql: migration 2 (002-commit-after-locking) failed: migrations cannot control transactions; each migration runs in its own transaction +applied +", + }, +} +`; diff --git a/tests/scenarios/migrations-postgres-atomic-functions/package.json b/tests/scenarios/migrations-postgres-atomic-functions/package.json new file mode 100644 index 00000000..d0245c06 --- /dev/null +++ b/tests/scenarios/migrations-postgres-atomic-functions/package.json @@ -0,0 +1,20 @@ +{ + "name": "migrations-postgres-atomic-functions", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/migrations-postgres-json-generate/app.ts b/tests/scenarios/migrations-postgres-json-generate/app.ts new file mode 100644 index 00000000..b1d99ec6 --- /dev/null +++ b/tests/scenarios/migrations-postgres-json-generate/app.ts @@ -0,0 +1,14 @@ +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; +import type { DB } from "./db.js"; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); + +export const rows = await sql( + "INSERT INTO settings (id, value) VALUES ($1, $2) RETURNING value, backup", + 1, + sql.json({ theme: "dark" }), +).execute(); +console.log(rows); +await pool.end(); diff --git a/tests/scenarios/migrations-postgres-json-generate/expected.snap b/tests/scenarios/migrations-postgres-json-generate/expected.snap new file mode 100644 index 00000000..e49fbbf9 --- /dev/null +++ b/tests/scenarios/migrations-postgres-json-generate/expected.snap @@ -0,0 +1,44 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-postgres-json-generate > scenario 1`] = ` +{ + "artifacts": { + "db.ts": "// Generated by \`typesql migrate generate\` from 001-settings.ts. Do not edit. +import type { Col, Json } from "@typesql/core/postgres"; + +export interface DB { + settings: { + id: Col<"int4">; + value: Json<{ theme: "dark" | "light"; }>; + backup: Json<{ theme: "dark" | "light"; }, { nullable: true }, "json">; + }; +} +", + }, + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "rows": "({ backup: null | { theme: "dark" | "light" }; value: { theme: "dark" | "light" } })[]", + }, + }, + "generate": { + "status": 0, + "stderr": "", + "stdout": "typesql migrate: applied 001-settings.ts +typesql migrate: generated /db.ts +typesql migrate: generated /db.ts +", + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ { value: { theme: 'dark' }, backup: null } ] +", + }, +} +`; diff --git a/tests/scenarios/migrations-postgres-json-generate/migrations/001-settings.ts b/tests/scenarios/migrations-postgres-json-generate/migrations/001-settings.ts new file mode 100644 index 00000000..d0379108 --- /dev/null +++ b/tests/scenarios/migrations-postgres-json-generate/migrations/001-settings.ts @@ -0,0 +1,20 @@ +import { migration } from "@typesql/core/migration"; +import type { Col, Json } from "@typesql/core/postgres"; + +// A schema stated with .raw<…>() keeps its column helpers in the generated +// schema: Json for a JSON column with a TypeScript type. +export default migration("postgres", { + up: async (db) => + db.raw<{ + settings: { + id: Col<"int4">; + value: Json<{ theme: "light" | "dark" }>; + backup: Json<{ theme: "light" | "dark" }, { nullable: true }, "json">; + }; + }>( + "CREATE TABLE settings (id integer PRIMARY KEY, value jsonb NOT NULL, backup json)", + ), + down: async (db) => { + await db.ddl("DROP TABLE settings"); + }, +}); diff --git a/tests/scenarios/migrations-postgres-json-generate/package.json b/tests/scenarios/migrations-postgres-json-generate/package.json new file mode 100644 index 00000000..fe44eda7 --- /dev/null +++ b/tests/scenarios/migrations-postgres-json-generate/package.json @@ -0,0 +1,26 @@ +{ + "name": "migrations-postgres-json-generate", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js", + "generate": "typesql migrate latest && typesql migrate generate" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/cli": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres", + "artifacts": [ + "db.ts" + ] + } +} diff --git a/tests/scenarios/migrations-postgres-json-generate/typesql.config.ts b/tests/scenarios/migrations-postgres-json-generate/typesql.config.ts new file mode 100644 index 00000000..5899ea26 --- /dev/null +++ b/tests/scenarios/migrations-postgres-json-generate/typesql.config.ts @@ -0,0 +1,6 @@ +import { createMigrator } from "@typesql/core/migration"; +export default createMigrator({ + dialect: "postgres", + url: process.env.DATABASE_URL, + migrationsDir: "./migrations", +}); diff --git a/tests/scenarios/migrations-postgres-values/app.ts b/tests/scenarios/migrations-postgres-values/app.ts new file mode 100644 index 00000000..68d7406f --- /dev/null +++ b/tests/scenarios/migrations-postgres-values/app.ts @@ -0,0 +1,34 @@ +import { + createMigrator, + migration, + type PostgresMigration, +} from "@typesql/core/migration"; + +// The migrator runs migrations on a `pg` client, so their rows are typed as +// node-postgres returns them. +const read = (db: PostgresMigration) => + db( + `SELECT '\\x01'::bytea AS bytes, '{a,b}'::"char"[] AS chars, ROW(1, 'a') AS pair, pg_sleep(0) AS slept, '1 day'::interval AS span`, + ).executeTakeFirstOrThrow(); +export type Read = Awaited>; + +const first = migration("postgres", { + name: "001-read", + up: async (db) => { + const row = await read(db); + console.log({ + bytes: Buffer.isBuffer(row.bytes), + chars: row.chars, + pair: row.pair, + slept: row.slept, + span: row.span.toPostgres(), + }); + return db; + }, + down: async () => {}, +}); +await createMigrator({ + dialect: "postgres", + url: process.env.DATABASE_URL, + migrations: [first], +}).latest(); diff --git a/tests/scenarios/migrations-postgres-values/expected.snap b/tests/scenarios/migrations-postgres-values/expected.snap new file mode 100644 index 00000000..a09f095c --- /dev/null +++ b/tests/scenarios/migrations-postgres-values/expected.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-postgres-values > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "Read": "{ bytes: Uint8Array; chars: string; pair: string; slept: string; span: { years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number; toPostgres: () => string; toISO: () => string; toISOString: () => string } }", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + bytes: true, + chars: '{a,b}', + pair: '(1,a)', + slept: '', + span: '1 days' +} +", + }, +} +`; diff --git a/tests/scenarios/migrations-postgres-values/package.json b/tests/scenarios/migrations-postgres-values/package.json new file mode 100644 index 00000000..d8e78e55 --- /dev/null +++ b/tests/scenarios/migrations-postgres-values/package.json @@ -0,0 +1,19 @@ +{ + "name": "migrations-postgres-values", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/migrations-postgres/app.ts b/tests/scenarios/migrations-postgres/app.ts index 45cb9222..fd7bb1c0 100644 --- a/tests/scenarios/migrations-postgres/app.ts +++ b/tests/scenarios/migrations-postgres/app.ts @@ -9,6 +9,7 @@ const first = migration("postgres", { up: async (db) => db.ddl("CREATE TABLE users (id integer PRIMARY KEY, name text NOT NULL)"), down: async (db) => { + // add() is the deprecated name of ddl(); it keeps working. await db.add("DROP TABLE users"); }, }); diff --git a/tests/scenarios/migrations-postgres/package.json b/tests/scenarios/migrations-postgres/package.json index e788af1c..6af5f010 100644 --- a/tests/scenarios/migrations-postgres/package.json +++ b/tests/scenarios/migrations-postgres/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/migrations-sqlite-keyword-names/app.ts b/tests/scenarios/migrations-sqlite-keyword-names/app.ts new file mode 100644 index 00000000..28e56c0f --- /dev/null +++ b/tests/scenarios/migrations-sqlite-keyword-names/app.ts @@ -0,0 +1,51 @@ +import { createMigrator, migration } from "@typesql/core/migration"; + +const down = async (): Promise => {}; +const tables = migration("sqlite", { + name: "001-spans", + down, + up: async (db) => { + await db.raw('CREATE TABLE spans (id integer, "end" integer)'); + return db; + }, +}); +// `NEW.end` is a name: the trigger is one statement, with all of its body. +const trigger = migration(tables, { + name: "002-trigger", + down, + up: async (db) => { + await db.raw(` + CREATE TRIGGER spans_end AFTER INSERT ON spans + BEGIN + UPDATE spans SET id = NEW.end WHERE id IS NULL; + SELECT 1; + END; + `); + return db; + }, +}); +// `begin` names the trigger, so the COMMIT is its own statement. +const commit = migration(tables, { + name: "002-commit", + down, + up: async (db) => { + await db.raw( + "CREATE TRIGGER begin AFTER INSERT ON spans BEGIN SELECT 1; END; COMMIT", + ); + return db; + }, +}); + +const db = process.env.SQLITE_PATH; +for (const next of [commit, trigger]) { + try { + await createMigrator({ + dialect: "sqlite", + db, + migrations: [tables, next], + }).latest(); + console.log("applied", next.name); + } catch (error) { + console.log(error instanceof Error ? error.message : error); + } +} diff --git a/tests/scenarios/migrations-sqlite-keyword-names/expected.snap b/tests/scenarios/migrations-sqlite-keyword-names/expected.snap new file mode 100644 index 00000000..04cbf725 --- /dev/null +++ b/tests/scenarios/migrations-sqlite-keyword-names/expected.snap @@ -0,0 +1,22 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migrations-sqlite-keyword-names > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": {}, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "typesql: migration 2 (002-commit) failed: migrations cannot control transactions; each migration runs in its own transaction +applied 002-trigger +", + }, +} +`; diff --git a/tests/scenarios/migrations-sqlite-keyword-names/package.json b/tests/scenarios/migrations-sqlite-keyword-names/package.json new file mode 100644 index 00000000..ff6dc476 --- /dev/null +++ b/tests/scenarios/migrations-sqlite-keyword-names/package.json @@ -0,0 +1,18 @@ +{ + "name": "migrations-sqlite-keyword-names", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/migrations-sqlite/package.json b/tests/scenarios/migrations-sqlite/package.json index d0c47c04..2d60fcd0 100644 --- a/tests/scenarios/migrations-sqlite/package.json +++ b/tests/scenarios/migrations-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/module-entry-points/app.ts b/tests/scenarios/module-entry-points/app.ts index 0b206c0b..cd0aaed5 100644 --- a/tests/scenarios/module-entry-points/app.ts +++ b/tests/scenarios/module-entry-points/app.ts @@ -1,30 +1,116 @@ +import { readdirSync, readFileSync, realpathSync } from "node:fs"; import { createRequire } from "node:module"; -import { main } from "@typesql/cli"; +import { dirname, join } from "node:path"; +import * as betterAuth from "@typesql/better-auth"; +import * as cli from "@typesql/cli"; import * as core from "@typesql/core"; -import * as adapter from "@typesql/core/adapter"; -import * as migrations from "@typesql/core/migration"; -import * as plugins from "@typesql/core/plugins"; -import * as postgres from "@typesql/core/postgres"; -import * as pgEngine from "@typesql/core/postgres/engine"; -import * as sqlite from "@typesql/core/sqlite"; -import * as sqliteEngine from "@typesql/core/sqlite/engine"; +import * as coreAdapter from "@typesql/core/adapter"; +import * as coreMigration from "@typesql/core/migration"; +import * as coreMysql from "@typesql/core/mysql"; +import * as coreMysqlEngine from "@typesql/core/mysql/engine"; +import * as corePlugins from "@typesql/core/plugins"; +import * as corePostgres from "@typesql/core/postgres"; +import * as corePostgresEngine from "@typesql/core/postgres/engine"; +import * as coreReserved from "@typesql/core/reserved"; +import * as coreSqlite from "@typesql/core/sqlite"; +import * as coreSqliteEngine from "@typesql/core/sqlite/engine"; +import * as drizzle from "@typesql/drizzle"; +import * as kysely from "@typesql/kysely"; +import * as languageServer from "@typesql/language-server"; +import * as mysql from "@typesql/mysql"; +import * as mysqlIntrospect from "@typesql/mysql/introspect"; +import * as mysqlMariadb from "@typesql/mysql/mariadb"; +import * as mysqlPlanetscale from "@typesql/mysql/planetscale"; +import * as mysqlTidb from "@typesql/mysql/tidb"; +import * as pg from "@typesql/pg"; import * as pgIntrospect from "@typesql/pg/introspect"; -import * as generator from "@typesql/prisma/generator"; +import * as postgres from "@typesql/postgres"; +import * as prisma from "@typesql/prisma"; +import * as prismaGenerator from "@typesql/prisma/generator"; +import * as sqlite from "@typesql/sqlite"; import * as sqliteIntrospect from "@typesql/sqlite/introspect"; const require = createRequire(import.meta.url); + +// Every public entry point, with the kind of each value it exports. +const entryPoints = { + "@typesql/better-auth": betterAuth, + "@typesql/cli": cli, + "@typesql/core": core, + "@typesql/core/adapter": coreAdapter, + "@typesql/core/migration": coreMigration, + "@typesql/core/mysql": coreMysql, + "@typesql/core/mysql/engine": coreMysqlEngine, + "@typesql/core/plugins": corePlugins, + "@typesql/core/postgres": corePostgres, + "@typesql/core/postgres/engine": corePostgresEngine, + "@typesql/core/reserved": coreReserved, + "@typesql/core/sqlite": coreSqlite, + "@typesql/core/sqlite/engine": coreSqliteEngine, + "@typesql/drizzle": drizzle, + "@typesql/kysely": kysely, + "@typesql/language-server": languageServer, + "@typesql/mysql": mysql, + "@typesql/mysql/introspect": mysqlIntrospect, + "@typesql/mysql/mariadb": mysqlMariadb, + "@typesql/mysql/planetscale": mysqlPlanetscale, + "@typesql/mysql/tidb": mysqlTidb, + "@typesql/pg": pg, + "@typesql/pg/introspect": pgIntrospect, + "@typesql/postgres": postgres, + "@typesql/prisma": prisma, + "@typesql/prisma/generator": prismaGenerator, + "@typesql/sqlite": sqlite, + "@typesql/sqlite/introspect": sqliteIntrospect, +}; +console.log( + JSON.stringify( + Object.fromEntries( + Object.entries(entryPoints).map(([specifier, module]) => [ + specifier, + Object.fromEntries( + Object.entries(module).map(([name, value]) => [name, typeof value]), + ), + ]), + ), + null, + 2, + ), +); +// Entry points the installed TypeSQL packages declare but this list misses. +interface Manifest { + name: string; + private?: boolean; + main?: string; + dependencies?: Record; + exports?: Record; +} +const manifest = (path: string): Manifest => + JSON.parse(readFileSync(path, "utf8")); +const dependencies = Object.keys( + manifest("package.json").dependencies ?? {}, +).filter((name) => name.startsWith("@typesql/")); +const declared = dependencies.flatMap((name) => { + const { exports, main } = manifest(`node_modules/${name}/package.json`); + return Object.keys(exports ?? (main ? { ".": main } : {})).map( + (path) => `${name}${path.slice(1)}`, + ); +}); +console.log({ unlisted: declared.filter((entry) => !(entry in entryPoints)) }); +// Published TypeSQL packages of the workspace the dependencies are installed +// from that this application doesn't depend on. +const workspace = dirname(realpathSync("node_modules/@typesql/core")); +const published = readdirSync(workspace).flatMap((dir) => { + const path = join(workspace, dir, "package.json"); + try { + const { name, private: unpublished } = manifest(path); + return unpublished ? [] : [name]; + } catch { + return []; + } +}); console.log({ - createSql: typeof core.createSql, - positional: typeof adapter.positional, - postgres: postgres.postgres.name, - sqlite: sqlite.sqlite.name, - pgEngine: Object.keys(pgEngine), - sqliteEngine: Object.keys(sqliteEngine), - migrations: typeof migrations.createMigrator, - CasePlugin: typeof plugins.CasePlugin, - pgIntrospect: typeof pgIntrospect.introspect, - sqliteIntrospect: typeof sqliteIntrospect.introspect, - generateSource: typeof generator.generateSource, - cli: typeof main, - requireEsm: typeof require("@typesql/core").createSql, + undeclared: published.filter((name) => !dependencies.includes(name)), }); +// CommonJS code can require the ES modules. +console.log({ requireEsm: typeof require("@typesql/core").createSql }); diff --git a/tests/scenarios/module-entry-points/expected.snap b/tests/scenarios/module-entry-points/expected.snap index 91cc64e7..4b93c201 100644 --- a/tests/scenarios/module-entry-points/expected.snap +++ b/tests/scenarios/module-entry-points/expected.snap @@ -15,20 +15,172 @@ exports[`module-entry-points > scenario 1`] = ` "status": 0, "stderr": "", "stdout": "{ - createSql: 'function', - positional: 'function', - postgres: 'postgres', - sqlite: 'sqlite', - pgEngine: [], - sqliteEngine: [], - migrations: 'function', - CasePlugin: 'function', - pgIntrospect: 'function', - sqliteIntrospect: 'function', - generateSource: 'function', - cli: 'function', - requireEsm: 'function' + "@typesql/better-auth": { + "typesqlAdapter": "function" + }, + "@typesql/cli": { + "main": "function" + }, + "@typesql/core": { + "NoResultError": "function", + "createSql": "function", + "mapRows": "function", + "pluginsOf": "function" + }, + "@typesql/core/adapter": { + "JsonParam": "function", + "RESERVED_KEYS": "object", + "UsageError": "function", + "colFlags": "function", + "colSource": "function", + "columnSource": "function", + "commandIo": "function", + "connectMysql": "function", + "connectPostgres": "function", + "connectSqlite": "function", + "createFamilySql": "function", + "createTransactionalSql": "function", + "databaseUrl": "function", + "dialectFamily": "function", + "doc": "function", + "errorMessage": "function", + "familyStatement": "object", + "globRegExp": "function", + "helperImport": "function", + "introspectArgs": "function", + "introspectCommand": "function", + "jsonText": "function", + "listOption": "function", + "mapRows": "function", + "mysqlBind": "function", + "mysqlInline": "function", + "mysqlPositional": "function", + "mysqlResult": "function", + "mysqlSplit": "function", + "mysqlValue": "function", + "paramNumbers": "function", + "parsePgArray": "function", + "parsePgUserArrays": "function", + "pgResult": "function", + "pluginsOf": "function", + "pooledTransaction": "function", + "positional": "function", + "propertyKey": "function", + "runTransaction": "function", + "schemaMembers": "function", + "schemaWriter": "function", + "serialTransaction": "function", + "splitParams": "function", + "sqliteBindings": "function", + "sqliteValue": "function" + }, + "@typesql/core/migration": { + "MigrationError": "function", + "createMigrator": "function", + "isMigrator": "function", + "loadMigrator": "function", + "migration": "function", + "missingCore": "function" + }, + "@typesql/core/mysql": { + "mariadb": "object", + "mysql": "object" + }, + "@typesql/core/mysql/engine": {}, + "@typesql/core/plugins": { + "CasePlugin": "function" + }, + "@typesql/core/postgres": { + "postgres": "object" + }, + "@typesql/core/postgres/engine": {}, + "@typesql/core/reserved": { + "META_KEYS": "object", + "RESERVED_WORDS": "object" + }, + "@typesql/core/sqlite": { + "sqlite": "object" + }, + "@typesql/core/sqlite/engine": {}, + "@typesql/drizzle": { + "createSql": "function", + "executor": "function", + "mysqlExecutor": "function", + "mysqlType": "function", + "pgType": "function", + "sqliteExecutor": "function", + "sqliteType": "function" + }, + "@typesql/kysely": { + "createSql": "function", + "executor": "function" + }, + "@typesql/language-server": { + "GRAMMAR": "object", + "Session": "function", + "lspItems": "function", + "merge": "function", + "mergeTokens": "function", + "typescriptModules": "function", + "typescriptModulesFrom": "function" + }, + "@typesql/mysql": { + "createSql": "function", + "executor": "function" + }, + "@typesql/mysql/introspect": { + "enumValues": "function", + "generateSource": "function", + "introspect": "function", + "main": "function", + "normalizeType": "function" + }, + "@typesql/mysql/mariadb": { + "createSql": "function", + "executor": "function" + }, + "@typesql/mysql/planetscale": { + "createSql": "function", + "executor": "function" + }, + "@typesql/mysql/tidb": { + "createSql": "function", + "executor": "function" + }, + "@typesql/pg": { + "createSql": "function", + "executor": "function" + }, + "@typesql/pg/introspect": { + "generateSource": "function", + "introspect": "function", + "main": "function" + }, + "@typesql/postgres": { + "createSql": "function", + "executor": "function" + }, + "@typesql/prisma": { + "createSql": "function", + "executor": "function" + }, + "@typesql/prisma/generator": { + "generateSource": "function" + }, + "@typesql/sqlite": { + "createSql": "function", + "executor": "function" + }, + "@typesql/sqlite/introspect": { + "generateSource": "function", + "introspect": "function", + "main": "function", + "normalizeType": "function" + } } +{ unlisted: [] } +{ undeclared: [] } +{ requireEsm: 'function' } ", }, } diff --git a/tests/scenarios/module-entry-points/package.json b/tests/scenarios/module-entry-points/package.json index 479a83d9..5ee7ac4b 100644 --- a/tests/scenarios/module-entry-points/package.json +++ b/tests/scenarios/module-entry-points/package.json @@ -3,18 +3,34 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { - "@typesql/core": "workspace:*", + "@typesql/better-auth": "workspace:*", "@typesql/cli": "workspace:*", + "@typesql/core": "workspace:*", + "@typesql/drizzle": "workspace:*", + "@typesql/kysely": "workspace:*", + "@typesql/language-server": "workspace:*", + "@typesql/mysql": "workspace:*", "@typesql/pg": "workspace:*", - "@typesql/sqlite": "workspace:*", + "@typesql/postgres": "workspace:*", "@typesql/prisma": "workspace:*", + "@typesql/sqlite": "workspace:*", + "@better-fetch/fetch": "catalog:", "@prisma/client": "catalog:", + "@types/pg": "catalog:", + "better-auth": "catalog:", + "better-call": "catalog:", + "drizzle-orm": "catalog:", + "jose": "catalog:", + "kysely": "catalog:", + "mariadb": "catalog:", + "mysql2": "catalog:", + "nanostores": "catalog:", "pg": "catalog:", - "@types/pg": "catalog:" + "postgres": "catalog:", + "zod": "catalog:" }, "devDependencies": { "typescript": "catalog:", diff --git a/tests/scenarios/mysql-dialect-rejection/expected.snap b/tests/scenarios/mysql-dialect-rejection/expected.snap index b9f923f3..7bff71b9 100644 --- a/tests/scenarios/mysql-dialect-rejection/expected.snap +++ b/tests/scenarios/mysql-dialect-rejection/expected.snap @@ -5,20 +5,6 @@ exports[`mysql-dialect-rejection > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(14,9): error TS2345: Argument of type '"SELECT emial FROM users"' is not assignable to parameter of type 'SqlError<"Unknown column 'emial'. Perhaps you meant the column \\"email\\"?", 1> & "SELECT emial FROM users"'. - Type 'string' is not assignable to type 'SqlError<"Unknown column 'emial'. Perhaps you meant the column \\"email\\"?", 1>'. -app.ts(15,9): error TS2345: Argument of type '"SELECT id FROM users WHERE id = ?"' is not assignable to parameter of type 'SqlError<"TypeSQL parameters are written $1 or :name, not \\"?\\"", 7> & "SELECT id FROM users WHERE id = ?"'. - Type 'string' is not assignable to type 'SqlError<"TypeSQL parameters are written $1 or :name, not \\"?\\"", 7>'. -app.ts(16,9): error TS2345: Argument of type '"INSERT INTO users (id, email) VALUES ($1, $2) RETURNING id"' is not assignable to parameter of type 'SqlError<"You have an error in your SQL syntax near 'RETURNING'", 14> & "INSERT INTO users (id, email) VALUES ($1, $2) RETURNING id"'. - Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near 'RETURNING'", 14>'. -app.ts(17,9): error TS2345: Argument of type '"SELECT email, COUNT(*) FROM users"' is not assignable to parameter of type 'SqlError<"In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'email'; this is incompatible with sql_mode=only_full_group_by", 0> & "SELECT email, COUNT(*) FROM users"'. - Type 'string' is not assignable to type 'SqlError<"In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'email'; this is incompatible with sql_mode=only_full_group_by", 0>'. -app.ts(18,47): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number | bigint'. -app.ts(20,11): error TS2345: Argument of type '"SELECT settings -> '$.theme' AS theme FROM users"' is not assignable to parameter of type 'SqlError<"You have an error in your SQL syntax near '->'", 0> & "SELECT settings -> '$.theme' AS theme FROM users"'. - Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near '->'", 0>'. -app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL"' is not assignable to parameter of type 'SqlError<"You have an error in your SQL syntax near 'LATERAL'", 6> & "SELECT * FROM users AS u LATERAL"'. - Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near 'LATERAL'", 6>'. -", }, "compiler": { "diagnostics": [ @@ -30,10 +16,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 9, - "expression": ""SELECT emial FROM users"", - "file": "app.ts", - "line": 14, "message": "Type 'string' is not assignable to type 'SqlError<"Unknown column 'emial'. Perhaps you meant the column \\"email\\"?", 1>'.", }, ], @@ -50,10 +32,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 9, - "expression": ""SELECT id FROM users WHERE id = ?"", - "file": "app.ts", - "line": 15, "message": "Type 'string' is not assignable to type 'SqlError<"TypeSQL parameters are written $1 or :name, not \\"?\\"", 7>'.", }, ], @@ -70,10 +48,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 9, - "expression": ""INSERT INTO users (id, email) VALUES ($1, $2) RETURNING id"", - "file": "app.ts", - "line": 16, "message": "Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near 'RETURNING'", 14>'.", }, ], @@ -90,10 +64,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 9, - "expression": ""SELECT email, COUNT(*) FROM users"", - "file": "app.ts", - "line": 17, "message": "Type 'string' is not assignable to type 'SqlError<"In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'email'; this is incompatible with sql_mode=only_full_group_by", 0>'.", }, ], @@ -119,10 +89,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 11, - "expression": ""SELECT settings -> '$.theme' AS theme FROM users"", - "file": "app.ts", - "line": 20, "message": "Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near '->'", 0>'.", }, ], @@ -139,10 +105,6 @@ app.ts(21,11): error TS2345: Argument of type '"SELECT * FROM users AS u LATERAL { "category": 1, "code": 2322, - "column": 11, - "expression": ""SELECT * FROM users AS u LATERAL"", - "file": "app.ts", - "line": 21, "message": "Type 'string' is not assignable to type 'SqlError<"You have an error in your SQL syntax near 'LATERAL'", 6>'.", }, ], diff --git a/tests/scenarios/mysql-dialect-rejection/package.json b/tests/scenarios/mysql-dialect-rejection/package.json index f3bbf392..683d6dd6 100644 --- a/tests/scenarios/mysql-dialect-rejection/package.json +++ b/tests/scenarios/mysql-dialect-rejection/package.json @@ -2,9 +2,6 @@ "name": "mysql-dialect-rejection", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*" }, diff --git a/tests/scenarios/mysql-insert-column-order/app.ts b/tests/scenarios/mysql-insert-column-order/app.ts new file mode 100644 index 00000000..0f2da7c2 --- /dev/null +++ b/tests/scenarios/mysql-insert-column-order/app.ts @@ -0,0 +1,86 @@ +import { + type MySqlMigration, + migration, + type SchemaOf, +} from "@typesql/core/migration"; +import type { Col } from "@typesql/core/mysql"; +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +// `items` is written by hand without positions, so its column order is +// unknown; `placed` has the positions `typesql introspect` writes. +interface DB { + items: { + sku: Col<"int">; + name: Col<"varchar">; + note: Col<"varchar", { nullable: true }>; + }; + placed: { + sku: Col<"int", { position: 0 }>; + name: Col<"varchar", { position: 1 }>; + note: Col<"varchar", { nullable: true; position: 2 }>; + }; +} +const uri = process.env.DATABASE_URL; +if (!uri) throw new Error("Missing DATABASE_URL"); +const pool = createPool({ uri, multipleStatements: true }); +await pool.query(`CREATE TABLE items (sku INT PRIMARY KEY, name VARCHAR(20) NOT NULL, note VARCHAR(20)); + CREATE TABLE placed (sku INT PRIMARY KEY, name VARCHAR(20) NOT NULL, note VARCHAR(20));`); +const sql = createSql(pool); + +const unordered = sql.prepare("INSERT INTO items VALUES (1, $1, $2)"); +export type UnorderedParams = Parameters; +await unordered("Ada", null).execute(); + +const rows = sql.prepare("INSERT INTO items VALUES (2, $1, $2), (3, $3, $4)"); +export type RowsParams = Parameters; +await rows("Grace", "SQL", "Lin", null).execute(); + +const listed = sql.prepare( + "INSERT INTO items (note, sku, name) VALUES ($1, $2, $3)", +); +export type ListedParams = Parameters; +await listed(null, 4, "Ken").execute(); + +const positioned = sql.prepare("INSERT INTO placed VALUES ($1, $2, $3)"); +export type PlacedParams = Parameters; +await positioned(1, "Ada", null).execute(); + +const upsert = sql.prepare( + "INSERT INTO placed VALUES ($1, $2, $3) AS new(a, b, c) ON DUPLICATE KEY UPDATE note = COALESCE(new.c, $4)", +); +export type UpsertParams = Parameters; +await upsert(1, "Ada", null, "kept").execute(); + +const baseline = { + up: async (db: MySqlMigration) => db, + down: async () => {}, +}; +const tagged = migration(baseline, { + up: async (db) => + db.ddl( + "ALTER TABLE items ADD COLUMN tag VARCHAR(10) FIRST; ALTER TABLE placed ADD COLUMN tag VARCHAR(10) AFTER sku", + ), + down: async () => {}, +}); +await pool.query( + "ALTER TABLE items ADD COLUMN tag VARCHAR(10) FIRST; ALTER TABLE placed ADD COLUMN tag VARCHAR(10) AFTER sku", +); +const altered = createSql>(pool); + +const first = altered.prepare("INSERT INTO items VALUES ($1, 5, $2, $3)"); +export type FirstParams = Parameters; +await first("new", "Edsger", null).execute(); + +const after = altered.prepare("INSERT INTO placed VALUES ($1, $2, $3, $4)"); +export type AfterParams = Parameters; +await after(2, "new", "Grace", "SQL").execute(); + +export const items = await altered( + "SELECT tag, sku, name, note FROM items ORDER BY sku", +).execute(); +export const placed = await altered( + "SELECT sku, tag, name, note FROM placed ORDER BY sku", +).execute(); +console.log({ items, placed }); +await pool.end(); diff --git a/tests/scenarios/mysql-insert-column-order/expected.snap b/tests/scenarios/mysql-insert-column-order/expected.snap new file mode 100644 index 00000000..096e084f --- /dev/null +++ b/tests/scenarios/mysql-insert-column-order/expected.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-insert-column-order > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "AfterParams": "[number | bigint, null | string, string, null | string]", + "FirstParams": "[null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array]", + "ListedParams": "[null | string, number | bigint, string]", + "PlacedParams": "[number | bigint, string, null | string]", + "RowsParams": "[null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array]", + "UnorderedParams": "[null | string | number | bigint | false | true | Date | Uint8Array, null | string | number | bigint | false | true | Date | Uint8Array]", + "UpsertParams": "[number | bigint, string, null | string, null | string]", + "items": "({ name: string; note: null | string; sku: number; tag: null | string })[]", + "placed": "({ name: string; note: null | string; sku: number; tag: null | string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + items: [ + { tag: null, sku: 1, name: 'Ada', note: null }, + { tag: null, sku: 2, name: 'Grace', note: 'SQL' }, + { tag: null, sku: 3, name: 'Lin', note: null }, + { tag: null, sku: 4, name: 'Ken', note: null }, + { tag: 'new', sku: 5, name: 'Edsger', note: null } + ], + placed: [ + { sku: 1, tag: null, name: 'Ada', note: 'kept' }, + { sku: 2, tag: 'new', name: 'Grace', note: 'SQL' } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/mysql-insert-column-order/package.json b/tests/scenarios/mysql-insert-column-order/package.json new file mode 100644 index 00000000..b524973c --- /dev/null +++ b/tests/scenarios/mysql-insert-column-order/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-insert-column-order", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/mysql-json-array-rejection/app.ts b/tests/scenarios/mysql-json-array-rejection/app.ts new file mode 100644 index 00000000..b0ebd0b2 --- /dev/null +++ b/tests/scenarios/mysql-json-array-rejection/app.ts @@ -0,0 +1,10 @@ +import { createSql } from "@typesql/core"; +import { type Json, mysql } from "@typesql/core/mysql"; + +interface DB { + posts: { tags: Json }; +} +const sql = createSql(mysql); +const tags = ["sql", "types"]; + +sql(`UPDATE posts SET tags = ${sql.param(tags)}`); diff --git a/tests/scenarios/mysql-json-array-rejection/expected.snap b/tests/scenarios/mysql-json-array-rejection/expected.snap new file mode 100644 index 00000000..6a27b8a6 --- /dev/null +++ b/tests/scenarios/mysql-json-array-rejection/expected.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-json-array-rejection > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a string[] value for json NOT NULL must be passed as sql.json(value)", 5>'.", + }, + ], + "expression": "\`UPDATE posts SET tags = \${sql.param(tags)}\`", + "file": "app.ts", + "line": 10, + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a string[] value for json NOT NULL must be passed as sql.json(value)", 5> & "UPDATE posts SET tags = \\0P[0s]0\\0"'.", + }, + ], + "types": {}, + }, +} +`; diff --git a/tests/scenarios/mysql-json-array-rejection/package.json b/tests/scenarios/mysql-json-array-rejection/package.json new file mode 100644 index 00000000..93ee381c --- /dev/null +++ b/tests/scenarios/mysql-json-array-rejection/package.json @@ -0,0 +1,18 @@ +{ + "name": "mysql-json-array-rejection", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/mysql-json-object-rejection/app.ts b/tests/scenarios/mysql-json-object-rejection/app.ts new file mode 100644 index 00000000..75396059 --- /dev/null +++ b/tests/scenarios/mysql-json-object-rejection/app.ts @@ -0,0 +1,31 @@ +import { createSql } from "@typesql/core"; +import { type Col, type Json, mysql } from "@typesql/core/mysql"; + +interface DB { + posts: { + id: Col<"int">; + data: Col<"json">; + meta: Json<{ theme: string }, { nullable: true }>; + }; +} +const sql = createSql(mysql); +const meta = { theme: "dark" }; + +// MySQL drivers bind JSON as text: sql.json() writes it, a plain object +// is no MySQL value. +sql( + "UPDATE posts SET data = $1, meta = $2 WHERE id = $3", + sql.json(meta), + sql.json(meta), + 1, +); +sql( + "UPDATE posts SET data = $1, meta = $2 WHERE id = $3", + '{"theme":"dark"}', + null, + 1, +); +sql("UPDATE posts SET data = $1 WHERE id = 1", meta); +sql("UPDATE posts SET meta = $1 WHERE id = 1", meta); +sql("UPDATE posts SET meta = :meta WHERE id = 1", { meta }); +sql(`UPDATE posts SET ${sql.set("posts", { meta })} WHERE id = 1`); diff --git a/tests/scenarios/mysql-json-object-rejection/expected.snap b/tests/scenarios/mysql-json-object-rejection/expected.snap new file mode 100644 index 00000000..095291a3 --- /dev/null +++ b/tests/scenarios/mysql-json-object-rejection/expected.snap @@ -0,0 +1,80 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-json-object-rejection > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 48, + "expression": "meta", + "file": "app.ts", + "line": 28, + "message": "Argument of type '{ theme: string; }' is not assignable to parameter of type 'NonArrayOrSqlJson'.", + }, + { + "category": 1, + "code": 2345, + "column": 48, + "expression": "meta", + "file": "app.ts", + "line": 29, + "message": "Argument of type '{ theme: string; }' is not assignable to parameter of type 'NonArrayOrSqlJson'.", + }, + { + "category": 1, + "code": 2322, + "column": 53, + "expression": "meta", + "file": "app.ts", + "line": 30, + "message": "Type '{ theme: string; }' is not assignable to type 'NonArrayOrSqlJson'.", + "related": [ + { + "category": 3, + "code": 6500, + "column": 21, + "expression": "{ + [K in keyof O]: O[K]; +}", + "file": "/packages/core/dist/sql/named.d.ts", + "line": 12, + "message": "The expected type comes from property 'meta' which is declared here on type '{ meta: NonArrayOrSqlJson; }'", + }, + ], + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a { theme: string } value for json must be passed as sql.json(value)", 5>'.", + }, + ], + "expression": "\`UPDATE posts SET \${sql.set("posts", { meta })} WHERE id = 1\`", + "file": "app.ts", + "line": 31, + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a { theme: string } value for json must be passed as sql.json(value)", 5> & \`UPDATE posts SET \${Fragment<"\`meta\` = $1", readonly unknown[], ["<0theme=s>0"]>} WHERE id = 1\`'.", + }, + { + "category": 1, + "code": 2345, + "column": 42, + "expression": "{ meta }", + "file": "app.ts", + "line": 31, + "message": "Argument of type '{ meta: { theme: string; }; }' is not assignable to parameter of type 'never'.", + }, + ], + "types": {}, + }, +} +`; diff --git a/tests/scenarios/mysql-json-object-rejection/package.json b/tests/scenarios/mysql-json-object-rejection/package.json new file mode 100644 index 00000000..63df7b66 --- /dev/null +++ b/tests/scenarios/mysql-json-object-rejection/package.json @@ -0,0 +1,18 @@ +{ + "name": "mysql-json-object-rejection", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/mysql-json-strings/app.ts b/tests/scenarios/mysql-json-strings/app.ts new file mode 100644 index 00000000..cc9b0992 --- /dev/null +++ b/tests/scenarios/mysql-json-strings/app.ts @@ -0,0 +1,34 @@ +import type { Col, DefaultTypeMap, Json } from "@typesql/core/mysql"; +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +interface DB { + posts: { + id: Col<"int">; + meta: Json<{ theme: string }>; + extra: Json<{ tags: string[] }, { nullable: true }>; + }; +} +// With jsonStrings, mysql2 returns JSON documents as their text. +interface JsonStringsTypeMap extends Omit { + json: string; +} +const pool = createPool({ + uri: process.env.DATABASE_URL ?? "", + jsonStrings: true, +}); +await pool.query( + "CREATE TABLE posts (id INT PRIMARY KEY, meta JSON NOT NULL, extra JSON)", +); +const sql = createSql(pool); +await sql( + "INSERT INTO posts (id, meta, extra) VALUES (1, $1, $2), (2, $3, NULL)", + sql.json({ theme: "dark" }), + sql.json({ tags: ["a"] }), + sql.json({ theme: "light" }), +).execute(); +export const rows = await sql( + "SELECT id, meta, extra, meta ->> '$.theme' AS theme FROM posts ORDER BY id", +).execute(); +console.log(rows); +await pool.end(); diff --git a/tests/scenarios/mysql-json-strings/expected.snap b/tests/scenarios/mysql-json-strings/expected.snap new file mode 100644 index 00000000..82d13c53 --- /dev/null +++ b/tests/scenarios/mysql-json-strings/expected.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-json-strings > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "rows": "({ extra: null | string; id: number; meta: string; theme: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ + { + id: 1, + meta: '{"theme": "dark"}', + extra: '{"tags": ["a"]}', + theme: 'dark' + }, + { id: 2, meta: '{"theme": "light"}', extra: null, theme: 'light' } +] +", + }, +} +`; diff --git a/tests/scenarios/mysql-json-strings/package.json b/tests/scenarios/mysql-json-strings/package.json new file mode 100644 index 00000000..6fc0276c --- /dev/null +++ b/tests/scenarios/mysql-json-strings/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-json-strings", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/mysql-json/package.json b/tests/scenarios/mysql-json/package.json index 23570258..d27313ce 100644 --- a/tests/scenarios/mysql-json/package.json +++ b/tests/scenarios/mysql-json/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/mysql-multiple-statements/app.ts b/tests/scenarios/mysql-multiple-statements/app.ts new file mode 100644 index 00000000..2529d1ed --- /dev/null +++ b/tests/scenarios/mysql-multiple-statements/app.ts @@ -0,0 +1,24 @@ +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +const pool = createPool({ + uri: process.env.DATABASE_URL!, + multipleStatements: true, +}); +const sql = createSql<{}>(pool); + +// A script of several statements, which mysql2 runs with +// multipleStatements: its last statement's result stands for it. +const script: string = [ + "CREATE TABLE notes (id INT PRIMARY KEY, body VARCHAR(10) NOT NULL)", + "INSERT INTO notes VALUES (1, 'one'), (2, 'two')", + "SELECT id, body FROM notes ORDER BY id", +].join("; "); +export const rows = await sql + .as<{ id: number; body: string }>(script) + .execute(); +export const changed = await sql + .as("UPDATE notes SET body = UPPER(body); DELETE FROM notes WHERE id = 1") + .executeCount(); +console.log({ rows, changed }); +await pool.end(); diff --git a/tests/scenarios/mysql-multiple-statements/expected.snap b/tests/scenarios/mysql-multiple-statements/expected.snap new file mode 100644 index 00000000..63826431 --- /dev/null +++ b/tests/scenarios/mysql-multiple-statements/expected.snap @@ -0,0 +1,27 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-multiple-statements > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "changed": "number", + "rows": "({ id: number; body: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + rows: [ { id: 1, body: 'one' }, { id: 2, body: 'two' } ], + changed: 1 +} +", + }, +} +`; diff --git a/tests/scenarios/mysql-multiple-statements/package.json b/tests/scenarios/mysql-multiple-statements/package.json new file mode 100644 index 00000000..cbc8d4ed --- /dev/null +++ b/tests/scenarios/mysql-multiple-statements/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-multiple-statements", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/mysql-quick-start/package.json b/tests/scenarios/mysql-quick-start/package.json index fcdb31ff..f77953fe 100644 --- a/tests/scenarios/mysql-quick-start/package.json +++ b/tests/scenarios/mysql-quick-start/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/mysql-quoted-text/app.ts b/tests/scenarios/mysql-quoted-text/app.ts new file mode 100644 index 00000000..d423a8d5 --- /dev/null +++ b/tests/scenarios/mysql-quoted-text/app.ts @@ -0,0 +1,65 @@ +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +const pool = createPool({ uri: process.env.DATABASE_URL! }); +const sql = createSql<{}>(pool); + +// MySQL text: a backtick-quoted name, a string with a backslash escape and a +// `#` comment hold no parameters, and a block comment ends at the first +// `*/`, as MySQL doesn't nest them. +const columns = sql.raw( + "1 AS `a:b`, 'it\\'s :x' AS s, '\\\\' AS backslash, 2 AS `$1` # not :y\n", +); +export const row = await sql + .as<{ + first: number; + "a:b": number; + s: string; + backslash: string; + $1: number; + last: number; + }>(`SELECT $1 AS first, ${columns} /* /* */, ${sql.param(20)} AS last`, 10) + .executeTakeFirstOrThrow(); +console.log(row); + +// sql() reads its text, and that of its fragments, by the same rules. +export const quotedName = await sql("SELECT 1 AS `a:b`, :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +export const escapedQuote = await sql( + 'SELECT \'it\\\'s :x\' AS s, "say \\"hi:x\\"" AS d, :y + 0 AS y', + { y: 2 }, +).executeTakeFirstOrThrow(); +export const hashComment = await sql("SELECT 1 AS a # not :x\n, :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +export const doubleMinus = await sql("SELECT 5--1 AS dash, :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +export const blockComment = await sql("SELECT /* /* */ :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +const dollar = sql.fragment("1 AS `$2`, $1 + 0 AS a", 10); +export const quotedDollar = await sql( + `SELECT ${dollar}`, +).executeTakeFirstOrThrow(); +const name = sql.fragment("1 AS `a:b`, :y + 0 AS y", { y: 2 }); +export const fragment = await sql( + `SELECT ${name}, ${sql.param(3)} + 0 AS z`, +).executeTakeFirstOrThrow(); +const pair = sql.combine([ + sql.fragment("$1 + 0 AS `$1 a`", 1), + sql.fragment("$1 + 0 AS `$1 b`", 2), +]); +export const combined = await sql(`SELECT ${pair}`).executeTakeFirstOrThrow(); +console.log({ + quotedName, + escapedQuote, + hashComment, + doubleMinus, + blockComment, + quotedDollar, + fragment, + combined, +}); +await pool.end(); diff --git a/tests/scenarios/mysql-quoted-text/expected.snap b/tests/scenarios/mysql-quoted-text/expected.snap new file mode 100644 index 00000000..f64f8c38 --- /dev/null +++ b/tests/scenarios/mysql-quoted-text/expected.snap @@ -0,0 +1,48 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-quoted-text > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "blockComment": "{ y: null | number }", + "combined": "{ "$1 a": null | number; "$1 b": null | number }", + "doubleMinus": "{ dash: number; y: null | number }", + "escapedQuote": "{ d: string; s: string; y: null | number }", + "fragment": "{ "a:b": number; y: null | number; z: null | number }", + "hashComment": "{ a: number; y: null | number }", + "quotedDollar": "{ $2: number; a: null | number }", + "quotedName": "{ "a:b": number; y: null | number }", + "row": "{ first: number; "a:b": number; s: string; backslash: string; $1: number; last: number }", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + first: 10, + 'a:b': 1, + s: "it's :x", + backslash: '\\\\', + '$1': 2, + last: 20 +} +{ + quotedName: { 'a:b': 1, y: 2 }, + escapedQuote: { s: "it's :x", d: 'say "hi:x"', y: 2 }, + hashComment: { a: 1, y: 2 }, + doubleMinus: { dash: 6, y: 2 }, + blockComment: { y: 2 }, + quotedDollar: { '$2': 1, a: 10 }, + fragment: { 'a:b': 1, y: 2, z: 3 }, + combined: { '$1 a': 1, '$1 b': 2 } +} +", + }, +} +`; diff --git a/tests/scenarios/mysql-quoted-text/package.json b/tests/scenarios/mysql-quoted-text/package.json new file mode 100644 index 00000000..154e4f54 --- /dev/null +++ b/tests/scenarios/mysql-quoted-text/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-quoted-text", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/mysql-serverless-literals/app.ts b/tests/scenarios/mysql-serverless-literals/app.ts new file mode 100644 index 00000000..be769062 --- /dev/null +++ b/tests/scenarios/mysql-serverless-literals/app.ts @@ -0,0 +1,40 @@ +import { createSql } from "@typesql/mysql/tidb"; +import { createConnection, type RowDataPacket } from "mysql2/promise"; + +interface DB { + notes: { + k: import("@typesql/core/mysql").Col<"varchar">; + }; +} +const connection = await createConnection(process.env.DATABASE_URL!); +await connection.query("CREATE TABLE notes (k varchar(100) NOT NULL)"); +await connection.query("INSERT INTO notes VALUES ('a'), ('b')"); + +// The real server behind the client's shape: the values reach it as literals. +const sql = createSql({ + execute: async (query) => { + const [rows] = await connection.query(query); + return { rows, rowsAffected: rows.length }; + }, +}); +const attack = "x' OR 1=1 -- "; +const lookup = () => sql("SELECT k FROM notes WHERE k = $1", attack).execute(); +const echo = (value: string) => + sql("SELECT $1 AS v", value) + .execute() + .then(([row]) => row?.v === value); + +// A value can't end its string, whichever way the server reads backslashes. +console.log("default", await lookup()); +for (const value of [ + `it's`, + `say "hi"`, + "back\\slash", + "\\'", + "tail\\", + "a\nb", +]) + console.log(JSON.stringify(value), await echo(value)); +await connection.query("SET sql_mode = 'NO_BACKSLASH_ESCAPES'"); +console.log("NO_BACKSLASH_ESCAPES", await lookup()); +await connection.end(); diff --git a/tests/scenarios/mysql-serverless-literals/expected.snap b/tests/scenarios/mysql-serverless-literals/expected.snap new file mode 100644 index 00000000..ca6b55dd --- /dev/null +++ b/tests/scenarios/mysql-serverless-literals/expected.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-serverless-literals > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": {}, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "default [] +"it's" true +"say \\"hi\\"" true +"back\\\\slash" true +"\\\\'" true +"tail\\\\" true +"a\\nb" true +NO_BACKSLASH_ESCAPES [] +", + }, +} +`; diff --git a/tests/scenarios/mysql-serverless-literals/package.json b/tests/scenarios/mysql-serverless-literals/package.json new file mode 100644 index 00000000..21ec8972 --- /dev/null +++ b/tests/scenarios/mysql-serverless-literals/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-serverless-literals", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/mysql-where-narrowing/app.ts b/tests/scenarios/mysql-where-narrowing/app.ts new file mode 100644 index 00000000..d390f90d --- /dev/null +++ b/tests/scenarios/mysql-where-narrowing/app.ts @@ -0,0 +1,51 @@ +import type { Col } from "@typesql/core/mysql"; +import { createSql } from "@typesql/mysql"; +import { createPool } from "mysql2/promise"; + +interface DB { + users: { + id: Col<"int">; + name: Col<"varchar">; + bio: Col<"varchar", { nullable: true }>; + }; + posts: { + id: Col<"int">; + author_id: Col<"int">; + title: Col<"varchar">; + body: Col<"varchar", { nullable: true }>; + }; +} +const pool = createPool({ + uri: process.env.DATABASE_URL!, + multipleStatements: true, +}); +await pool.query(`CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, bio VARCHAR(100)); + CREATE TABLE posts (id INT PRIMARY KEY, author_id INT NOT NULL, title VARCHAR(100) NOT NULL, body VARCHAR(100)); + INSERT INTO users VALUES (1, 'Ada', NULL), (2, 'Grace', 'SQL'), (3, 'Lin', 'Lisp'); + INSERT INTO posts VALUES (1, 1, 'SQL', NULL);`); +const sql = createSql(pool); +export const any = await sql( + "SELECT id, bio FROM users WHERE bio = ANY (SELECT title FROM posts) ORDER BY id", +).execute(); +export const all = await sql( + "SELECT id, bio FROM users WHERE bio <> ALL (SELECT title FROM posts WHERE id > 1) ORDER BY id", +).execute(); +export const some = await sql( + "SELECT id, bio FROM users WHERE bio <> SOME (SELECT title FROM posts) ORDER BY id", +).execute(); +export const notIn = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN (SELECT title FROM posts WHERE id > 1) ORDER BY id", +).execute(); +export const notInList = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN ('SQL') ORDER BY id", +).execute(); +export const joined = + await sql(`SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id ORDER BY u.id`).execute(); +export const matched = + await sql(`SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id WHERE p.title = 'SQL' ORDER BY u.id`).execute(); +export const right = await sql(`SELECT u.id, u.name, p.title FROM posts p + RIGHT JOIN users u ON p.author_id = u.id WHERE p.id IS NOT NULL`).execute(); +console.log({ any, some, all, notIn, notInList, joined, matched, right }); +await pool.end(); diff --git a/tests/scenarios/mysql-where-narrowing/expected.snap b/tests/scenarios/mysql-where-narrowing/expected.snap new file mode 100644 index 00000000..b0ae9387 --- /dev/null +++ b/tests/scenarios/mysql-where-narrowing/expected.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mysql-where-narrowing > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "all": "({ bio: null | string; id: number })[]", + "any": "({ bio: string; id: number })[]", + "joined": "({ body: null | string; id: null | number; name: string; title: null | string })[]", + "matched": "({ body: null | string; id: number; name: string; title: string })[]", + "notIn": "({ bio: null | string; id: number })[]", + "notInList": "({ bio: string; id: number })[]", + "right": "({ id: number; name: string; title: string })[]", + "some": "({ bio: string; id: number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + any: [ { id: 2, bio: 'SQL' } ], + some: [ { id: 3, bio: 'Lisp' } ], + all: [ + { id: 1, bio: null }, + { id: 2, bio: 'SQL' }, + { id: 3, bio: 'Lisp' } + ], + notIn: [ + { id: 1, bio: null }, + { id: 2, bio: 'SQL' }, + { id: 3, bio: 'Lisp' } + ], + notInList: [ { id: 3, bio: 'Lisp' } ], + joined: [ + { name: 'Ada', id: 1, title: 'SQL', body: null }, + { name: 'Grace', id: null, title: null, body: null }, + { name: 'Lin', id: null, title: null, body: null } + ], + matched: [ { name: 'Ada', id: 1, title: 'SQL', body: null } ], + right: [ { id: 1, name: 'Ada', title: 'SQL' } ] +} +", + }, +} +`; diff --git a/tests/scenarios/mysql-where-narrowing/package.json b/tests/scenarios/mysql-where-narrowing/package.json new file mode 100644 index 00000000..3cca09da --- /dev/null +++ b/tests/scenarios/mysql-where-narrowing/package.json @@ -0,0 +1,20 @@ +{ + "name": "mysql-where-narrowing", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/mysql": "workspace:*", + "mysql2": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "mysql" + } +} diff --git a/tests/scenarios/node-sqlite/package.json b/tests/scenarios/node-sqlite/package.json index 5e9cafc5..f677ef06 100644 --- a/tests/scenarios/node-sqlite/package.json +++ b/tests/scenarios/node-sqlite/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-cast-parameters/app.ts b/tests/scenarios/pg-cast-parameters/app.ts new file mode 100644 index 00000000..0fe3b61a --- /dev/null +++ b/tests/scenarios/pg-cast-parameters/app.ts @@ -0,0 +1,80 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + items: { + id: Col<"int4">; + tags: Col<"text[]">; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); +await pool.query(` + CREATE TABLE items (id integer NOT NULL, tags text[] NOT NULL); + INSERT INTO items VALUES (1, ARRAY['a']); +`); + +// A cast parameter that accepts NULL is nullable, whatever the select list holds. +export const nested = await sql( + "SELECT x FROM (SELECT ARRAY[$1::int4] AS x) s", + null, +).execute(); +export const nestedCommented = await sql( + "SELECT x -- comment\n FROM (SELECT ARRAY[$1::int4] AS x) s", + null, +).execute(); +export const unnested = await sql( + "SELECT x FROM unnest(ARRAY[$1::int4]) AS x", + null, +).execute(); +export const castCall = await sql( + "SELECT x FROM unnest(ARRAY[CAST($1 AS int4)]) AS x", + null, +).execute(); + +// A cast array parameter accepts NULL elements, and returns them. +export const arrayCast = await sql("SELECT $1::text[] AS x", [ + "a", + null, +]).execute(); +export const arrayRows = await sql("SELECT x FROM unnest($1::text[]) AS x", [ + "a", + null, +]).execute(); + +// Not nullable: a column, a default, a comparison and a bulk insert. +export const column = await sql("SELECT tags FROM items").execute(); +export const defaulted = await sql( + "SELECT COALESCE($1::int4, 0) AS x", + null, +).execute(); +export const defaultedArray = await sql( + "SELECT COALESCE($1::text[], ARRAY['b']) AS x", + null, +).execute(); +export const compared = await sql( + "SELECT x FROM (SELECT id AS x FROM items WHERE id = $1::int4) s", + 1, +).execute(); +export const inserted = await sql( + "INSERT INTO items (id, tags) SELECT x, ARRAY['c'] FROM unnest($1::int4[]) AS x RETURNING id", + [2], +).execute(); +console.dir( + { + nested, + nestedCommented, + unnested, + castCall, + arrayCast, + arrayRows, + column, + defaulted, + defaultedArray, + compared, + inserted, + }, + { depth: null }, +); +await pool.end(); diff --git a/tests/scenarios/pg-cast-parameters/expected.snap b/tests/scenarios/pg-cast-parameters/expected.snap new file mode 100644 index 00000000..bee7eb96 --- /dev/null +++ b/tests/scenarios/pg-cast-parameters/expected.snap @@ -0,0 +1,45 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-cast-parameters > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "arrayCast": "({ x: (null | string)[] })[]", + "arrayRows": "({ x: null | string })[]", + "castCall": "({ x: null | number })[]", + "column": "({ tags: (string)[] })[]", + "compared": "({ x: number })[]", + "defaulted": "({ x: number })[]", + "defaultedArray": "({ x: (null | string)[] })[]", + "inserted": "({ id: number })[]", + "nested": "({ x: (null | number)[] })[]", + "nestedCommented": "({ x: (null | number)[] })[]", + "unnested": "({ x: null | number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + nested: [ { x: [ null ] } ], + nestedCommented: [ { x: [ null ] } ], + unnested: [ { x: null } ], + castCall: [ { x: null } ], + arrayCast: [ { x: [ 'a', null ] } ], + arrayRows: [ { x: 'a' }, { x: null } ], + column: [ { tags: [ 'a' ] } ], + defaulted: [ { x: 0 } ], + defaultedArray: [ { x: [ 'b' ] } ], + compared: [ { x: 1 } ], + inserted: [ { id: 2 } ] +} +", + }, +} +`; diff --git a/tests/scenarios/pg-cast-parameters/package.json b/tests/scenarios/pg-cast-parameters/package.json new file mode 100644 index 00000000..918f813b --- /dev/null +++ b/tests/scenarios/pg-cast-parameters/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-cast-parameters", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-char-type/app.ts b/tests/scenarios/pg-char-type/app.ts new file mode 100644 index 00000000..c36a88b9 --- /dev/null +++ b/tests/scenarios/pg-char-type/app.ts @@ -0,0 +1,29 @@ +import { createSql, type RowType } from "@typesql/core"; +import { type ApplyDdl, type Col, postgres } from "@typesql/core/postgres"; + +// "char" is PostgreSQL's one-byte type, as introspection writes it; bpchar is +// char(n), which SQL spells char or character. +interface Introspected { + t: { flag: Col<"char">; code: Col<"bpchar"> }; +} +type Created = ApplyDdl< + Record, + `CREATE TABLE t (flag "char" NOT NULL, code char(2) NOT NULL)` +>; + +const introspected = createSql(postgres); +const created = createSql(postgres); + +const rows = introspected("SELECT flag, code FROM t"); +export type Rows = RowType; +const createdRows = created("SELECT flag, code FROM t"); +export type CreatedRows = RowType; + +// A call no overload takes names the argument types in its message. +introspected("SELECT upper(flag, 1) FROM t"); +introspected("SELECT upper(code, 1) FROM t"); +created("SELECT upper(flag, 1) FROM t"); +created("SELECT upper(code, 1) FROM t"); +introspected(`SELECT upper("char" 'a', 1)`); +introspected("SELECT upper(char 'a', 1)"); +introspected(`SELECT upper('a'::"char", 1)`); diff --git a/tests/scenarios/pg-char-type/expected.snap b/tests/scenarios/pg-char-type/expected.snap new file mode 100644 index 00000000..9e38a4f2 --- /dev/null +++ b/tests/scenarios/pg-char-type/expected.snap @@ -0,0 +1,130 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-char-type > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 14, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(flag, 1) FROM t"", + "file": "app.ts", + "line": 23, + "message": "Argument of type '"SELECT upper(flag, 1) FROM t"' is not assignable to parameter of type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1> & "SELECT upper(flag, 1) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 14, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(character, integer) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(code, 1) FROM t"", + "file": "app.ts", + "line": 24, + "message": "Argument of type '"SELECT upper(code, 1) FROM t"' is not assignable to parameter of type 'SqlError<"function upper(character, integer) does not exist", 1> & "SELECT upper(code, 1) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 9, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(flag, 1) FROM t"", + "file": "app.ts", + "line": 25, + "message": "Argument of type '"SELECT upper(flag, 1) FROM t"' is not assignable to parameter of type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1> & "SELECT upper(flag, 1) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 9, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(character, integer) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(code, 1) FROM t"", + "file": "app.ts", + "line": 26, + "message": "Argument of type '"SELECT upper(code, 1) FROM t"' is not assignable to parameter of type 'SqlError<"function upper(character, integer) does not exist", 1> & "SELECT upper(code, 1) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 14, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1>'.", + }, + ], + "expression": "\`SELECT upper("char" 'a', 1)\`", + "file": "app.ts", + "line": 27, + "message": "Argument of type '"SELECT upper(\\"char\\" 'a', 1)"' is not assignable to parameter of type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1> & "SELECT upper(\\"char\\" 'a', 1)"'.", + }, + { + "category": 1, + "code": 2345, + "column": 14, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(character, integer) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(char 'a', 1)"", + "file": "app.ts", + "line": 28, + "message": "Argument of type '"SELECT upper(char 'a', 1)"' is not assignable to parameter of type 'SqlError<"function upper(character, integer) does not exist", 1> & "SELECT upper(char 'a', 1)"'.", + }, + { + "category": 1, + "code": 2345, + "column": 14, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1>'.", + }, + ], + "expression": "\`SELECT upper('a'::"char", 1)\`", + "file": "app.ts", + "line": 29, + "message": "Argument of type '"SELECT upper('a'::\\"char\\", 1)"' is not assignable to parameter of type 'SqlError<"function upper(\\"char\\", integer) does not exist", 1> & "SELECT upper('a'::\\"char\\", 1)"'.", + }, + ], + "types": { + "CreatedRows": "{ code: string; flag: string }", + "Rows": "{ code: string; flag: string }", + }, + }, +} +`; diff --git a/tests/scenarios/pg-char-type/package.json b/tests/scenarios/pg-char-type/package.json new file mode 100644 index 00000000..68383ac8 --- /dev/null +++ b/tests/scenarios/pg-char-type/package.json @@ -0,0 +1,18 @@ +{ + "name": "pg-char-type", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/pg-common-type-rejection/app.ts b/tests/scenarios/pg-common-type-rejection/app.ts new file mode 100644 index 00000000..c866fdb9 --- /dev/null +++ b/tests/scenarios/pg-common-type-rejection/app.ts @@ -0,0 +1,27 @@ +import { createSql, type RowType } from "@typesql/core"; +import { type Col, postgres } from "@typesql/core/postgres"; + +interface DB { + t: { + label: Col<"text">; + at: Col<"time">; + span: Col<"interval">; + o: Col<"oid">; + rc: Col<"regclass">; + maybe_o: Col<"oid", { nullable: true }>; + }; + u: { o: Col<"regclass"> }; +} +const sql = createSql(postgres); + +// Of two types that convert to each other, the first one is kept. +const kept = sql( + "SELECT coalesce(o, rc) AS oid_first, coalesce(rc, o) AS regclass_first, coalesce(maybe_o, rc) AS nullable_first FROM t", +); +export type Kept = RowType; +const joined = sql("SELECT o FROM t JOIN u USING (o)"); +export type Joined = RowType; + +// An implicit cast to another category gives no common type. +sql(`SELECT coalesce('a'::"char", label) FROM t`); +sql("SELECT CASE WHEN true THEN at ELSE span END FROM t"); diff --git a/tests/scenarios/pg-common-type-rejection/expected.snap b/tests/scenarios/pg-common-type-rejection/expected.snap new file mode 100644 index 00000000..e9eaf8c1 --- /dev/null +++ b/tests/scenarios/pg-common-type-rejection/expected.snap @@ -0,0 +1,50 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-common-type-rejection > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"COALESCE types \\"char\\" and text cannot be matched", 1>'.", + }, + ], + "expression": "\`SELECT coalesce('a'::"char", label) FROM t\`", + "file": "app.ts", + "line": 26, + "message": "Argument of type '"SELECT coalesce('a'::\\"char\\", label) FROM t"' is not assignable to parameter of type 'SqlError<"COALESCE types \\"char\\" and text cannot be matched", 1> & "SELECT coalesce('a'::\\"char\\", label) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"CASE types interval and time without time zone cannot be matched", 1>'.", + }, + ], + "expression": ""SELECT CASE WHEN true THEN at ELSE span END FROM t"", + "file": "app.ts", + "line": 27, + "message": "Argument of type '"SELECT CASE WHEN true THEN at ELSE span END FROM t"' is not assignable to parameter of type 'SqlError<"CASE types interval and time without time zone cannot be matched", 1> & "SELECT CASE WHEN true THEN at ELSE span END FROM t"'.", + }, + ], + "types": { + "Joined": "{ o: number }", + "Kept": "{ nullable_first: number; oid_first: number; regclass_first: string }", + }, + }, +} +`; diff --git a/tests/scenarios/pg-common-type-rejection/package.json b/tests/scenarios/pg-common-type-rejection/package.json new file mode 100644 index 00000000..0e9c1d51 --- /dev/null +++ b/tests/scenarios/pg-common-type-rejection/package.json @@ -0,0 +1,18 @@ +{ + "name": "pg-common-type-rejection", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/pg-declaration-emit/package.json b/tests/scenarios/pg-declaration-emit/package.json index de0d861b..218430ac 100644 --- a/tests/scenarios/pg-declaration-emit/package.json +++ b/tests/scenarios/pg-declaration-emit/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-declared-function-calls/app.ts b/tests/scenarios/pg-declared-function-calls/app.ts new file mode 100644 index 00000000..16198011 --- /dev/null +++ b/tests/scenarios/pg-declared-function-calls/app.ts @@ -0,0 +1,22 @@ +import { createSql, type RowType } from "@typesql/core"; +import { type Col, postgres, type Sig } from "@typesql/core/postgres"; + +type T = "text"; +interface DB { + t: { label: Col<"text">; maybe: Col<"text", { nullable: true }> }; + $functions: { + // NULL only when its ninth argument is. + pick9: Sig<[T, T, T, T, T, T, T, T, T], "text", { strict: [9] }>; + }; +} +const sql = createSql(postgres); + +const picked = sql(` + SELECT pick9(label, label, label, label, label, label, label, label, maybe) AS ninth_nullable, + pick9(maybe, maybe, maybe, maybe, maybe, maybe, maybe, maybe, label) AS ninth_set, + pick9(label, label, label, label, label, label, label, label, label) AS all_set + FROM t`); +export type Picked = RowType; + +// An unknown function's message names the closest known one. +sql("SELECT crc33(label) FROM t"); diff --git a/tests/scenarios/pg-declared-function-calls/expected.snap b/tests/scenarios/pg-declared-function-calls/expected.snap new file mode 100644 index 00000000..e802c6e7 --- /dev/null +++ b/tests/scenarios/pg-declared-function-calls/expected.snap @@ -0,0 +1,33 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-declared-function-calls > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function crc33(text) does not exist. Perhaps you meant the function \\"crc32\\"? Otherwise declare it in the schema's $functions or in FunctionCatalog", 1>'.", + }, + ], + "expression": ""SELECT crc33(label) FROM t"", + "file": "app.ts", + "line": 22, + "message": "Argument of type '"SELECT crc33(label) FROM t"' is not assignable to parameter of type 'SqlError<"function crc33(text) does not exist. Perhaps you meant the function \\"crc32\\"? Otherwise declare it in the schema's $functions or in FunctionCatalog", 1> & "SELECT crc33(label) FROM t"'.", + }, + ], + "types": { + "Picked": "{ all_set: string; ninth_nullable: null | string; ninth_set: string }", + }, + }, +} +`; diff --git a/tests/scenarios/pg-declared-function-calls/package.json b/tests/scenarios/pg-declared-function-calls/package.json new file mode 100644 index 00000000..8d79a7b2 --- /dev/null +++ b/tests/scenarios/pg-declared-function-calls/package.json @@ -0,0 +1,18 @@ +{ + "name": "pg-declared-function-calls", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/pg-driver-values/package.json b/tests/scenarios/pg-driver-values/package.json index 149919f4..4d7e4a76 100644 --- a/tests/scenarios/pg-driver-values/package.json +++ b/tests/scenarios/pg-driver-values/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-duplicate-output-names/package.json b/tests/scenarios/pg-duplicate-output-names/package.json index 7c64c9c2..b961c952 100644 --- a/tests/scenarios/pg-duplicate-output-names/package.json +++ b/tests/scenarios/pg-duplicate-output-names/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-dynamic-sql/package.json b/tests/scenarios/pg-dynamic-sql/package.json index 52a45fbf..95192644 100644 --- a/tests/scenarios/pg-dynamic-sql/package.json +++ b/tests/scenarios/pg-dynamic-sql/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-ended-transaction/expected.snap b/tests/scenarios/pg-ended-transaction/expected.snap index 2a06c298..60f834c7 100644 --- a/tests/scenarios/pg-ended-transaction/expected.snap +++ b/tests/scenarios/pg-ended-transaction/expected.snap @@ -17,19 +17,6 @@ exports[`pg-ended-transaction > scenario 1`] = ` }, "start": { "status": 1, - "stderr": "file:///packages/core/dist/transaction.js:10 - throw new Error(ENDED); - ^ - -Error: typesql: this transaction has ended; run later queries on the sql that started it - at check (file:///packages/core/dist/transaction.js:10:19) - at run (file:///packages/core/dist/transaction.js:13:9) - at SqlQuery.execute (file:///packages/core/dist/query.js:25:50) - at file:///dist/app.js:7:34 - at process.processTicksAndRejections (node:internal/process/task_queues:104:5) - -Node.js v26.8.1 -", "stdout": "", }, } diff --git a/tests/scenarios/pg-ended-transaction/package.json b/tests/scenarios/pg-ended-transaction/package.json index 9ec84fd7..9df1cfd3 100644 --- a/tests/scenarios/pg-ended-transaction/package.json +++ b/tests/scenarios/pg-ended-transaction/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-engine-rejection/app.ts b/tests/scenarios/pg-engine-rejection/app.ts new file mode 100644 index 00000000..4a54079d --- /dev/null +++ b/tests/scenarios/pg-engine-rejection/app.ts @@ -0,0 +1,42 @@ +import { createSql, type RowType } from "@typesql/core"; +import { type Col, postgres } from "@typesql/core/postgres"; + +interface DB { + t: { id: Col<"int4">; k: Col<"text"> }; +} +const sql = createSql(postgres); + +// A window reference inside the SQL-standard string syntaxes must exist. +const named = sql( + "SELECT position(k in (sum(id) OVER w)::text) AS p FROM t WINDOW w AS (ORDER BY id)", +); +export type Named = RowType; +sql("SELECT position(k in (sum(id) OVER nowin)::text) FROM t"); +sql("SELECT substring(k from (row_number() OVER nowin)::int) FROM t"); +sql("SELECT trim(both k from (count(*) OVER nowin)::text) FROM t"); +sql("SELECT overlay(k placing 'x' from (count(*) OVER nowin)::int) FROM t"); + +// A known function called with arguments no overload takes does not exist. +const called = sql("SELECT upper(k) AS u, round(id, 1) AS r FROM t"); +export type Called = RowType; +sql("SELECT upper(k, k) FROM t"); +sql("SELECT lower() FROM t"); + +// Values of categories with no common type cannot be combined. +const combined = sql( + "SELECT CASE WHEN id > 0 THEN id ELSE 1.5 END AS c, coalesce(id, 2) AS d FROM t", +); +export type Combined = RowType; +sql("SELECT CASE WHEN id > 0 THEN id ELSE point(1, 2) END FROM t"); +sql("SELECT coalesce(id, point(1, 2)) FROM t"); +sql("SELECT greatest(id, point(1, 2)) FROM t"); +sql("SELECT ARRAY[id, point(1, 2)] FROM t"); +sql("SELECT id FROM t UNION SELECT point(1, 2)"); + +// ANY and ALL take an array on their right side. +const quantified = sql("SELECT id = ANY(ARRAY[id]::oid[]) AS hit FROM t"); +export type Quantified = RowType; +sql("SELECT id = ANY(id::oid) FROM t"); + +// A field of a call's result needs a composite result. +sql("SELECT (abs(id)).* FROM t"); diff --git a/tests/scenarios/pg-engine-rejection/expected.snap b/tests/scenarios/pg-engine-rejection/expected.snap new file mode 100644 index 00000000..bd1bda06 --- /dev/null +++ b/tests/scenarios/pg-engine-rejection/expected.snap @@ -0,0 +1,228 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-engine-rejection > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"window \\"nowin\\" does not exist", 11>'.", + }, + ], + "expression": ""SELECT position(k in (sum(id) OVER nowin)::text) FROM t"", + "file": "app.ts", + "line": 14, + "message": "Argument of type '"SELECT position(k in (sum(id) OVER nowin)::text) FROM t"' is not assignable to parameter of type 'SqlError<"window \\"nowin\\" does not exist", 11> & "SELECT position(k in (sum(id) OVER nowin)::text) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"window \\"nowin\\" does not exist", 10>'.", + }, + ], + "expression": ""SELECT substring(k from (row_number() OVER nowin)::int) FROM t"", + "file": "app.ts", + "line": 15, + "message": "Argument of type '"SELECT substring(k from (row_number() OVER nowin)::int) FROM t"' is not assignable to parameter of type 'SqlError<"window \\"nowin\\" does not exist", 10> & "SELECT substring(k from (row_number() OVER nowin)::int) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"window \\"nowin\\" does not exist", 12>'.", + }, + ], + "expression": ""SELECT trim(both k from (count(*) OVER nowin)::text) FROM t"", + "file": "app.ts", + "line": 16, + "message": "Argument of type '"SELECT trim(both k from (count(*) OVER nowin)::text) FROM t"' is not assignable to parameter of type 'SqlError<"window \\"nowin\\" does not exist", 12> & "SELECT trim(both k from (count(*) OVER nowin)::text) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"window \\"nowin\\" does not exist", 13>'.", + }, + ], + "expression": ""SELECT overlay(k placing 'x' from (count(*) OVER nowin)::int) FROM t"", + "file": "app.ts", + "line": 17, + "message": "Argument of type '"SELECT overlay(k placing 'x' from (count(*) OVER nowin)::int) FROM t"' is not assignable to parameter of type 'SqlError<"window \\"nowin\\" does not exist", 13> & "SELECT overlay(k placing 'x' from (count(*) OVER nowin)::int) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function upper(text, text) does not exist", 1>'.", + }, + ], + "expression": ""SELECT upper(k, k) FROM t"", + "file": "app.ts", + "line": 22, + "message": "Argument of type '"SELECT upper(k, k) FROM t"' is not assignable to parameter of type 'SqlError<"function upper(text, text) does not exist", 1> & "SELECT upper(k, k) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"function lower() does not exist", 1>'.", + }, + ], + "expression": ""SELECT lower() FROM t"", + "file": "app.ts", + "line": 23, + "message": "Argument of type '"SELECT lower() FROM t"' is not assignable to parameter of type 'SqlError<"function lower() does not exist", 1> & "SELECT lower() FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"CASE types point and integer cannot be matched", 1>'.", + }, + ], + "expression": ""SELECT CASE WHEN id > 0 THEN id ELSE point(1, 2) END FROM t"", + "file": "app.ts", + "line": 30, + "message": "Argument of type '"SELECT CASE WHEN id > 0 THEN id ELSE point(1, 2) END FROM t"' is not assignable to parameter of type 'SqlError<"CASE types point and integer cannot be matched", 1> & "SELECT CASE WHEN id > 0 THEN id ELSE point(1, 2) END FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"COALESCE types integer and point cannot be matched", 1>'.", + }, + ], + "expression": ""SELECT coalesce(id, point(1, 2)) FROM t"", + "file": "app.ts", + "line": 31, + "message": "Argument of type '"SELECT coalesce(id, point(1, 2)) FROM t"' is not assignable to parameter of type 'SqlError<"COALESCE types integer and point cannot be matched", 1> & "SELECT coalesce(id, point(1, 2)) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"GREATEST types integer and point cannot be matched", 1>'.", + }, + ], + "expression": ""SELECT greatest(id, point(1, 2)) FROM t"", + "file": "app.ts", + "line": 32, + "message": "Argument of type '"SELECT greatest(id, point(1, 2)) FROM t"' is not assignable to parameter of type 'SqlError<"GREATEST types integer and point cannot be matched", 1> & "SELECT greatest(id, point(1, 2)) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"ARRAY types integer and point cannot be matched", 1>'.", + }, + ], + "expression": ""SELECT ARRAY[id, point(1, 2)] FROM t"", + "file": "app.ts", + "line": 33, + "message": "Argument of type '"SELECT ARRAY[id, point(1, 2)] FROM t"' is not assignable to parameter of type 'SqlError<"ARRAY types integer and point cannot be matched", 1> & "SELECT ARRAY[id, point(1, 2)] FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"UNION types integer and point cannot be matched", 4>'.", + }, + ], + "expression": ""SELECT id FROM t UNION SELECT point(1, 2)"", + "file": "app.ts", + "line": 34, + "message": "Argument of type '"SELECT id FROM t UNION SELECT point(1, 2)"' is not assignable to parameter of type 'SqlError<"UNION types integer and point cannot be matched", 4> & "SELECT id FROM t UNION SELECT point(1, 2)"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"op ANY/ALL (array) requires array on right side", 0>'.", + }, + ], + "expression": ""SELECT id = ANY(id::oid) FROM t"", + "file": "app.ts", + "line": 39, + "message": "Argument of type '"SELECT id = ANY(id::oid) FROM t"' is not assignable to parameter of type 'SqlError<"op ANY/ALL (array) requires array on right side", 0> & "SELECT id = ANY(id::oid) FROM t"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"(e).* needs a whole-row value such as (t).*", 0>'.", + }, + ], + "expression": ""SELECT (abs(id)).* FROM t"", + "file": "app.ts", + "line": 42, + "message": "Argument of type '"SELECT (abs(id)).* FROM t"' is not assignable to parameter of type 'SqlError<"(e).* needs a whole-row value such as (t).*", 0> & "SELECT (abs(id)).* FROM t"'.", + }, + ], + "types": { + "Called": "{ r: string; u: string }", + "Combined": "{ c: string; d: number }", + "Named": "{ p: number }", + "Quantified": "{ hit: false | true }", + }, + }, +} +`; diff --git a/tests/scenarios/pg-engine-rejection/package.json b/tests/scenarios/pg-engine-rejection/package.json new file mode 100644 index 00000000..5da631ec --- /dev/null +++ b/tests/scenarios/pg-engine-rejection/package.json @@ -0,0 +1,18 @@ +{ + "name": "pg-engine-rejection", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/pg-function-call-parameters/app.ts b/tests/scenarios/pg-function-call-parameters/app.ts new file mode 100644 index 00000000..794a96ac --- /dev/null +++ b/tests/scenarios/pg-function-call-parameters/app.ts @@ -0,0 +1,38 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + items: { + id: Col<"int4">; + n: Col<"int4", { nullable: true }>; + name: Col<"text", { nullable: true }>; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); +await pool.query( + "CREATE TABLE items (id integer NOT NULL, n integer, name text)", +); + +// A function call holding a parameter, in the values of INSERT and MERGE. +export const coalesced = await sql( + "INSERT INTO items (id, n) VALUES (1, COALESCE($1, 1)) RETURNING id, n", + 5, +).execute(); +export const lowered = await sql( + "INSERT INTO items (id, name) VALUES (2, lower($1)) RETURNING name", + "ADA", +).execute(); +export const casted = await sql( + "INSERT INTO items (id, n) VALUES (3, abs($1::int4)) RETURNING n", + -3, +).execute(); +export const merged = await sql( + "MERGE INTO items t USING (SELECT 4 AS id) s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (id, n) VALUES (s.id, abs($1::int4))", + -4, +).executeCount(); +// The statements after those are still checked and typed. +export const after = await sql("SELECT id, n FROM items ORDER BY id").execute(); +console.dir({ coalesced, lowered, casted, merged, after }, { depth: null }); +await pool.end(); diff --git a/tests/scenarios/pg-function-call-parameters/expected.snap b/tests/scenarios/pg-function-call-parameters/expected.snap new file mode 100644 index 00000000..8eeb5265 --- /dev/null +++ b/tests/scenarios/pg-function-call-parameters/expected.snap @@ -0,0 +1,38 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-function-call-parameters > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "after": "({ id: number; n: null | number })[]", + "casted": "({ n: null | number })[]", + "coalesced": "({ id: number; n: null | number })[]", + "lowered": "({ name: null | string })[]", + "merged": "number", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + coalesced: [ { id: 1, n: 5 } ], + lowered: [ { name: 'ada' } ], + casted: [ { n: 3 } ], + merged: 1, + after: [ + { id: 1, n: 5 }, + { id: 2, n: null }, + { id: 3, n: 3 }, + { id: 4, n: 4 } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/pg-function-call-parameters/package.json b/tests/scenarios/pg-function-call-parameters/package.json new file mode 100644 index 00000000..08da51a8 --- /dev/null +++ b/tests/scenarios/pg-function-call-parameters/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-function-call-parameters", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-function-overloads/app.ts b/tests/scenarios/pg-function-overloads/app.ts index 739e41e5..542956d7 100644 --- a/tests/scenarios/pg-function-overloads/app.ts +++ b/tests/scenarios/pg-function-overloads/app.ts @@ -41,8 +41,8 @@ export const parameterized = await sql( 1, "i", ).execute(); -// Long predicates exercise structural traversal both without calls and with -// a call nested at the bottom of the arithmetic expression. +// Long arithmetic predicates, with and without a function call at their +// innermost operand, still type-check. export const predicates = await sql(` SELECT id FROM items WHERE id + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 diff --git a/tests/scenarios/pg-function-overloads/package.json b/tests/scenarios/pg-function-overloads/package.json index cdcf1ef0..cea4af58 100644 --- a/tests/scenarios/pg-function-overloads/package.json +++ b/tests/scenarios/pg-function-overloads/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-grouping-sets-parameters/app.ts b/tests/scenarios/pg-grouping-sets-parameters/app.ts new file mode 100644 index 00000000..da7cbc32 --- /dev/null +++ b/tests/scenarios/pg-grouping-sets-parameters/app.ts @@ -0,0 +1,32 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + sales: { + region: Col<"text">; + amount: Col<"int4">; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); +await pool.query( + "CREATE TABLE sales (region text NOT NULL, amount integer NOT NULL)", +); + +// Over no rows the grand-total row of ROLLUP has a NULL sum, so the parameter +// added to it accepts null like the sum does. +const rollup = sql.prepare( + "SELECT region, max(amount) + $1 AS top FROM sales GROUP BY ROLLUP (region)", +); +export type RollupParams = Parameters; +export const rolledUp = await rollup(null).execute(); + +// With a plain GROUP BY each group has a row, so max(amount) is not NULL. +const grouped = sql.prepare( + "SELECT region, max(amount) + $1 AS top FROM sales GROUP BY region", +); +export type GroupedParams = Parameters; +export const groups = await grouped(1).execute(); +console.dir({ rolledUp, groups }, { depth: null }); +await pool.end(); diff --git a/tests/scenarios/pg-grouping-sets-parameters/expected.snap b/tests/scenarios/pg-grouping-sets-parameters/expected.snap new file mode 100644 index 00000000..03a04628 --- /dev/null +++ b/tests/scenarios/pg-grouping-sets-parameters/expected.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-grouping-sets-parameters > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "GroupedParams": "[number]", + "RollupParams": "[null | number]", + "groups": "({ region: string; top: null | number })[]", + "rolledUp": "({ region: null | string; top: null | number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ rolledUp: [ { region: null, top: null } ], groups: [] } +", + }, +} +`; diff --git a/tests/scenarios/pg-grouping-sets-parameters/package.json b/tests/scenarios/pg-grouping-sets-parameters/package.json new file mode 100644 index 00000000..d2b83a17 --- /dev/null +++ b/tests/scenarios/pg-grouping-sets-parameters/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-grouping-sets-parameters", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-json-write-rejection/expected.snap b/tests/scenarios/pg-json-write-rejection/expected.snap index 396d33f0..8cd2c9e3 100644 --- a/tests/scenarios/pg-json-write-rejection/expected.snap +++ b/tests/scenarios/pg-json-write-rejection/expected.snap @@ -5,29 +5,6 @@ exports[`pg-json-write-rejection > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(15,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ<0ok=t,0views=qwrong~>0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'. -app.ts(16,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ<0ok=t>0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'. -app.ts(18,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t,0views=qwrong~>0"]>}\`'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'. -app.ts(20,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t>0"]>}\`'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'. -app.ts(22,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t,0views=qwrong~>0", "Jqdraft~"]>}\`'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'. -app.ts(25,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t>0", "Jqdraft~"]>}\`'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'. -app.ts(28,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10> & "INSERT INTO t (doc, label) VALUES (\\0PJ<0ok=t>0\\0, \\0PJqdraft~\\0)"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'. -app.ts(30,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5> & "UPDATE t SET optional=\\0PJ<0ok=t,0views=qwrong~>0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'. -app.ts(31,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5> & "UPDATE t SET optional=\\0PJ<0ok=t>0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'. -app.ts(32,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() null value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ0\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() null value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'. -app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() \\"wrong\\" value cannot be used as jsonb NOT NULL (expected \\"draft\\" | \\"published\\")", 5> & "UPDATE t SET label=\\0PJqwrong~\\0"'. - Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() \\"wrong\\" value cannot be used as jsonb NOT NULL (expected \\"draft\\" | \\"published\\")", 5>'. -", }, "compiler": { "diagnostics": [ @@ -39,10 +16,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json({ views: "wrong", ok: true })}\`", - "file": "app.ts", - "line": 15, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], @@ -59,10 +32,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json({ ok: true })}\`", - "file": "app.ts", - "line": 16, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], @@ -79,10 +48,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ views: "wrong", ok: true }) })}\`", - "file": "app.ts", - "line": 18, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], @@ -99,10 +64,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ ok: true }) })}\`", - "file": "app.ts", - "line": 20, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], @@ -119,10 +80,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ views: "wrong", ok: true }), label: sql.json("draft") }])}\`", - "file": "app.ts", - "line": 22, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], @@ -139,10 +96,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ ok: true }), label: sql.json("draft") }])}\`", - "file": "app.ts", - "line": 25, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], @@ -159,10 +112,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t (doc, label) VALUES (\${sql.json({ ok: true })}, \${sql.json("draft")})\`", - "file": "app.ts", - "line": 28, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], @@ -179,10 +128,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET optional=\${sql.json({ views: "wrong", ok: true })}\`", - "file": "app.ts", - "line": 30, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'.", }, ], @@ -199,10 +144,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET optional=\${sql.json({ ok: true })}\`", - "file": "app.ts", - "line": 31, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'.", }, ], @@ -219,10 +160,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json(null)}\`", - "file": "app.ts", - "line": 32, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() null value cannot be used as jsonb NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], @@ -239,10 +176,6 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET label=\${sql.json("wrong")}\`", - "file": "app.ts", - "line": 33, "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() \\"wrong\\" value cannot be used as jsonb NOT NULL (expected \\"draft\\" | \\"published\\")", 5>'.", }, ], diff --git a/tests/scenarios/pg-json-write-rejection/package.json b/tests/scenarios/pg-json-write-rejection/package.json index c7ef9fe9..c1401f61 100644 --- a/tests/scenarios/pg-json-write-rejection/package.json +++ b/tests/scenarios/pg-json-write-rejection/package.json @@ -2,9 +2,6 @@ "name": "pg-json-write-rejection", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*", "@typesql/pg": "workspace:*", diff --git a/tests/scenarios/pg-json-writes/package.json b/tests/scenarios/pg-json-writes/package.json index c73f42e7..7956a4de 100644 --- a/tests/scenarios/pg-json-writes/package.json +++ b/tests/scenarios/pg-json-writes/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-json/package.json b/tests/scenarios/pg-json/package.json index 3ed6ed67..20465c51 100644 --- a/tests/scenarios/pg-json/package.json +++ b/tests/scenarios/pg-json/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-left-join/package.json b/tests/scenarios/pg-left-join/package.json index 4a388eb1..92c0adb2 100644 --- a/tests/scenarios/pg-left-join/package.json +++ b/tests/scenarios/pg-left-join/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-meta-key-relations/app.ts b/tests/scenarios/pg-meta-key-relations/app.ts new file mode 100644 index 00000000..0f17e73f --- /dev/null +++ b/tests/scenarios/pg-meta-key-relations/app.ts @@ -0,0 +1,21 @@ +import { createSql, type RowType } from "@typesql/core"; +import { type Col, postgres, type Sig } from "@typesql/core/postgres"; + +interface DB { + t: { id: Col<"int4"> }; + $types: { mood: "happy" | "sad" }; + $functions: { slugify: Sig<["text"], "text"> }; +} +const sql = createSql(postgres); + +const rows = sql("SELECT id FROM t"); +export type Rows = RowType; + +// The schema's declarations are no relations. +sql(`SELECT 1 FROM "$types"`); +sql(`SELECT 1 FROM "$functions"`); +sql(`SELECT $1::int AS n FROM "$nullParams"`); +sql(`INSERT INTO "$types" VALUES (1)`); +sql(`DELETE FROM "$functions"`); +sql(`SELECT 1 FROM "$types".mood`); +sql(`UPDATE "$types".mood SET x = 1`); diff --git a/tests/scenarios/pg-meta-key-relations/expected.snap b/tests/scenarios/pg-meta-key-relations/expected.snap new file mode 100644 index 00000000..a209351f --- /dev/null +++ b/tests/scenarios/pg-meta-key-relations/expected.snap @@ -0,0 +1,129 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-meta-key-relations > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$types\\" does not exist", 3>'.", + }, + ], + "expression": "\`SELECT 1 FROM "$types"\`", + "file": "app.ts", + "line": 15, + "message": "Argument of type '"SELECT 1 FROM \\"$types\\""' is not assignable to parameter of type 'SqlError<"relation \\"$types\\" does not exist", 3> & "SELECT 1 FROM \\"$types\\""'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$functions\\" does not exist", 3>'.", + }, + ], + "expression": "\`SELECT 1 FROM "$functions"\`", + "file": "app.ts", + "line": 16, + "message": "Argument of type '"SELECT 1 FROM \\"$functions\\""' is not assignable to parameter of type 'SqlError<"relation \\"$functions\\" does not exist", 3> & "SELECT 1 FROM \\"$functions\\""'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$nullParams\\" does not exist", 7>'.", + }, + ], + "expression": "\`SELECT $1::int AS n FROM "$nullParams"\`", + "file": "app.ts", + "line": 17, + "message": "Argument of type '"SELECT $1::int AS n FROM \\"$nullParams\\""' is not assignable to parameter of type 'SqlError<"relation \\"$nullParams\\" does not exist", 7> & "SELECT $1::int AS n FROM \\"$nullParams\\""'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$types\\" does not exist", 2>'.", + }, + ], + "expression": "\`INSERT INTO "$types" VALUES (1)\`", + "file": "app.ts", + "line": 18, + "message": "Argument of type '"INSERT INTO \\"$types\\" VALUES (1)"' is not assignable to parameter of type 'SqlError<"relation \\"$types\\" does not exist", 2> & "INSERT INTO \\"$types\\" VALUES (1)"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$functions\\" does not exist", 2>'.", + }, + ], + "expression": "\`DELETE FROM "$functions"\`", + "file": "app.ts", + "line": 19, + "message": "Argument of type '"DELETE FROM \\"$functions\\""' is not assignable to parameter of type 'SqlError<"relation \\"$functions\\" does not exist", 2> & "DELETE FROM \\"$functions\\""'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$types.mood\\" does not exist", 3>'.", + }, + ], + "expression": "\`SELECT 1 FROM "$types".mood\`", + "file": "app.ts", + "line": 20, + "message": "Argument of type '"SELECT 1 FROM \\"$types\\".mood"' is not assignable to parameter of type 'SqlError<"relation \\"$types.mood\\" does not exist", 3> & "SELECT 1 FROM \\"$types\\".mood"'.", + }, + { + "category": 1, + "code": 2345, + "column": 5, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'string' is not assignable to type 'SqlError<"relation \\"$types.mood\\" does not exist", 1>'.", + }, + ], + "expression": "\`UPDATE "$types".mood SET x = 1\`", + "file": "app.ts", + "line": 21, + "message": "Argument of type '"UPDATE \\"$types\\".mood SET x = 1"' is not assignable to parameter of type 'SqlError<"relation \\"$types.mood\\" does not exist", 1> & "UPDATE \\"$types\\".mood SET x = 1"'.", + }, + ], + "types": { + "Rows": "{ id: number }", + }, + }, +} +`; diff --git a/tests/scenarios/pg-meta-key-relations/package.json b/tests/scenarios/pg-meta-key-relations/package.json new file mode 100644 index 00000000..ca2bd345 --- /dev/null +++ b/tests/scenarios/pg-meta-key-relations/package.json @@ -0,0 +1,18 @@ +{ + "name": "pg-meta-key-relations", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/pg-multiple-statements/app.ts b/tests/scenarios/pg-multiple-statements/app.ts new file mode 100644 index 00000000..cb4728c8 --- /dev/null +++ b/tests/scenarios/pg-multiple-statements/app.ts @@ -0,0 +1,22 @@ +import { createSql } from "@typesql/pg"; +import { Client } from "pg"; + +const client = new Client({ connectionString: process.env.DATABASE_URL }); +await client.connect(); +const sql = createSql<{}>(client); + +// A script of several statements, which node-postgres runs without +// parameters: its last statement's result stands for it. +const script: string = [ + "CREATE TABLE notes (id int PRIMARY KEY, body text NOT NULL)", + "INSERT INTO notes VALUES (1, 'one'), (2, 'two')", + "SELECT id, body FROM notes ORDER BY id", +].join("; "); +export const rows = await sql + .as<{ id: number; body: string }>(script) + .execute(); +export const changed = await sql + .as("UPDATE notes SET body = upper(body); DELETE FROM notes WHERE id = 1") + .executeCount(); +console.log({ rows, changed }); +await client.end(); diff --git a/tests/scenarios/pg-multiple-statements/expected.snap b/tests/scenarios/pg-multiple-statements/expected.snap new file mode 100644 index 00000000..4b179736 --- /dev/null +++ b/tests/scenarios/pg-multiple-statements/expected.snap @@ -0,0 +1,27 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-multiple-statements > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "changed": "number", + "rows": "({ id: number; body: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + rows: [ { id: 1, body: 'one' }, { id: 2, body: 'two' } ], + changed: 1 +} +", + }, +} +`; diff --git a/tests/scenarios/pg-multiple-statements/package.json b/tests/scenarios/pg-multiple-statements/package.json new file mode 100644 index 00000000..6faa1f07 --- /dev/null +++ b/tests/scenarios/pg-multiple-statements/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-multiple-statements", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-nested-comments-runtime/app.ts b/tests/scenarios/pg-nested-comments-runtime/app.ts deleted file mode 100644 index 8a386d60..00000000 --- a/tests/scenarios/pg-nested-comments-runtime/app.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createSql as createPgSql } from "@typesql/pg"; -import { createSql as createPostgresSql } from "@typesql/postgres"; -import { Pool } from "pg"; -import postgres from "postgres"; - -const pool = new Pool({ connectionString: process.env.DATABASE_URL }); -const client = postgres(process.env.DATABASE_URL!); -const native = "SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1"; -console.log("native pg", (await pool.query(native, [1])).rows); -console.log("native postgres", Array.from(await client.unsafe(native, [1]))); -const adapters = [ - ["pg", createPgSql<{}>(pool)], - ["postgres", createPostgresSql<{}>(client)], -] as const; -for (const [adapter, sql] of adapters) { - try { - const named = sql.as<{ n: number }>( - "SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=:id", - { id: 1 }, - ); - console.log(adapter, "named", named.text, named.values); - console.log(adapter, "named rows", await named.execute()); - } catch (error) { - console.log(adapter, "named error", String(error)); - } - try { - const fragment = sql.as<{ n: number }>( - `SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 ${sql.fragment("AND 1=$1", 1)}`, - 1, - ); - console.log(adapter, "fragment", fragment.text, fragment.values); - console.log(adapter, "fragment rows", await fragment.execute()); - } catch (error) { - console.log(adapter, "fragment error", String(error)); - } -} -await pool.end(); -await client.end(); diff --git a/tests/scenarios/pg-nested-comments-runtime/expected.snap b/tests/scenarios/pg-nested-comments-runtime/expected.snap deleted file mode 100644 index 5135cead..00000000 --- a/tests/scenarios/pg-nested-comments-runtime/expected.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`pg-nested-comments-runtime > scenario 1`] = ` -{ - "compile": { - "status": 0, - "stderr": "", - "stdout": "", - }, - "compiler": { - "diagnostics": [], - "types": {}, - }, - "start": { - "status": 0, - "stderr": "", - "stdout": "native pg [ { n: 1 } ] -native postgres [ { n: 1 } ] -pg named SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1 [ 1 ] -pg named rows [ { n: 1 } ] -pg fragment SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 AND 1=$2 [ 1, 1 ] -pg fragment rows [ { n: 1 } ] -postgres named SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1 [ 1 ] -postgres named rows [ { n: 1 } ] -postgres fragment SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 AND 1=$2 [ 1, 1 ] -postgres fragment rows [ { n: 1 } ] -", - }, -} -`; diff --git a/tests/scenarios/pg-nested-comments/app.ts b/tests/scenarios/pg-nested-comments/app.ts index a4f00a9c..a066117f 100644 --- a/tests/scenarios/pg-nested-comments/app.ts +++ b/tests/scenarios/pg-nested-comments/app.ts @@ -51,5 +51,35 @@ console.log({ pgInline, postgresInline, }); +// The drivers themselves skip a nested comment, and so do queries whose types +// are given with sql.as(). +const native = "SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1"; +console.log("native pg", (await pool.query(native, [1])).rows); +console.log("native postgres", Array.from(await client.unsafe(native, [1]))); +for (const [adapter, sql] of [ + ["pg", pgSql], + ["postgres", postgresSql], +] as const) { + try { + const query = sql.as<{ n: number }>( + "SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=:id", + { id: 1 }, + ); + console.log(adapter, "named", query.text, query.values); + console.log(adapter, "named rows", await query.execute()); + } catch (error) { + console.log(adapter, "named error", String(error)); + } + try { + const query = sql.as<{ n: number }>( + `SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 ${sql.fragment("AND 1=$1", 1)}`, + 1, + ); + console.log(adapter, "fragment", query.text, query.values); + console.log(adapter, "fragment rows", await query.execute()); + } catch (error) { + console.log(adapter, "fragment error", String(error)); + } +} await pool.end(); await client.end(); diff --git a/tests/scenarios/pg-nested-comments/expected.snap b/tests/scenarios/pg-nested-comments/expected.snap index 2f9ed418..caa0a324 100644 --- a/tests/scenarios/pg-nested-comments/expected.snap +++ b/tests/scenarios/pg-nested-comments/expected.snap @@ -39,6 +39,16 @@ postgres controls [{"quoted":":ignored $99 /*","dollar":":ignored $98 /*","escap pgInline: [ { n: 1 } ], postgresInline: [ { n: 1 } ] } +native pg [ { n: 1 } ] +native postgres [ { n: 1 } ] +pg named SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1 [ 1 ] +pg named rows [ { n: 1 } ] +pg fragment SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 AND 1=$2 [ 1, 1 ] +pg fragment rows [ { n: 1 } ] +postgres named SELECT 1 AS n /* outer /* inner */ :ignored */ WHERE 1=$1 [ 1 ] +postgres named rows [ { n: 1 } ] +postgres fragment SELECT 1 AS n /* outer /* inner */ $99 */ WHERE 1=$1 AND 1=$2 [ 1, 1 ] +postgres fragment rows [ { n: 1 } ] ", }, } diff --git a/tests/scenarios/pg-nested-comments/package.json b/tests/scenarios/pg-nested-comments/package.json index 23020524..5c4fb480 100644 --- a/tests/scenarios/pg-nested-comments/package.json +++ b/tests/scenarios/pg-nested-comments/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-no-result/expected.snap b/tests/scenarios/pg-no-result/expected.snap index a2e079e2..47f8f3a4 100644 --- a/tests/scenarios/pg-no-result/expected.snap +++ b/tests/scenarios/pg-no-result/expected.snap @@ -18,19 +18,6 @@ exports[`pg-no-result > scenario 1`] = ` }, "start": { "status": 1, - "stderr": "file:///packages/core/dist/query.js:34 - throw new NoResultError(this.text); - ^ - -NoResultError: The query returned no rows - at SqlQuery.executeTakeFirstOrThrow (file:///packages/core/dist/query.js:34:19) - at process.processTicksAndRejections (node:internal/process/task_queues:104:5) - at async file:///dist/app.js:10:5 { - text: 'SELECT id FROM users WHERE id = $1' -} - -Node.js v26.8.1 -", "stdout": "", }, } diff --git a/tests/scenarios/pg-no-result/package.json b/tests/scenarios/pg-no-result/package.json index 379df562..5045a193 100644 --- a/tests/scenarios/pg-no-result/package.json +++ b/tests/scenarios/pg-no-result/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-null-array-elements/app.ts b/tests/scenarios/pg-null-array-elements/app.ts new file mode 100644 index 00000000..a5cf12b3 --- /dev/null +++ b/tests/scenarios/pg-null-array-elements/app.ts @@ -0,0 +1,48 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + items: { + id: Col<"int4">; + tags: Col<"text[]">; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); +await pool.query(` + CREATE TABLE items (id integer NOT NULL, tags text[] NOT NULL); + INSERT INTO items VALUES (1, ARRAY['a']); +`); + +// An array with a NULL element in any row keeps NULL elements in the column. +export const values = await sql( + "SELECT a FROM (VALUES (ARRAY[2]), (ARRAY[1, NULL])) AS v(a) ORDER BY a", +).execute(); +export const valuesWithoutNull = await sql( + "SELECT a FROM (VALUES (ARRAY[2]), (ARRAY[1])) AS v(a) ORDER BY a", +).execute(); +export const union = await sql( + "SELECT tags FROM items UNION ALL SELECT ARRAY['b', NULL]", +).execute(); +export const recursive = await sql(` + WITH RECURSIVE r(a, i) AS ( + SELECT ARRAY[1, NULL], 1 + UNION ALL + SELECT a, i + 1 FROM r WHERE i < 2 + ) + SELECT a, i FROM r ORDER BY i +`).execute(); +export const recursiveWithoutNull = await sql(` + WITH RECURSIVE r(a, i) AS ( + SELECT tags, 1 FROM items + UNION ALL + SELECT a, i + 1 FROM r WHERE i < 2 + ) + SELECT a, i FROM r ORDER BY i +`).execute(); +console.dir( + { values, valuesWithoutNull, union, recursive, recursiveWithoutNull }, + { depth: null }, +); +await pool.end(); diff --git a/tests/scenarios/pg-null-array-elements/expected.snap b/tests/scenarios/pg-null-array-elements/expected.snap new file mode 100644 index 00000000..873a9f21 --- /dev/null +++ b/tests/scenarios/pg-null-array-elements/expected.snap @@ -0,0 +1,33 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-null-array-elements > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "recursive": "({ a: (null | number)[]; i: number })[]", + "recursiveWithoutNull": "({ a: (string)[]; i: number })[]", + "union": "({ tags: (null | string)[] })[]", + "values": "({ a: (null | number)[] })[]", + "valuesWithoutNull": "({ a: (number)[] })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + values: [ { a: [ 1, null ] }, { a: [ 2 ] } ], + valuesWithoutNull: [ { a: [ 1 ] }, { a: [ 2 ] } ], + union: [ { tags: [ 'a' ] }, { tags: [ 'b', null ] } ], + recursive: [ { a: [ 1, null ], i: 1 }, { a: [ 1, null ], i: 2 } ], + recursiveWithoutNull: [ { a: [ 'a' ], i: 1 }, { a: [ 'a' ], i: 2 } ] +} +", + }, +} +`; diff --git a/tests/scenarios/pg-null-array-elements/package.json b/tests/scenarios/pg-null-array-elements/package.json new file mode 100644 index 00000000..5c3e6d55 --- /dev/null +++ b/tests/scenarios/pg-null-array-elements/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-null-array-elements", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-parameter-inference/package.json b/tests/scenarios/pg-parameter-inference/package.json index 90860cbc..7b12ee4e 100644 --- a/tests/scenarios/pg-parameter-inference/package.json +++ b/tests/scenarios/pg-parameter-inference/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-prepared/package.json b/tests/scenarios/pg-prepared/package.json index 822b942c..d59e87e2 100644 --- a/tests/scenarios/pg-prepared/package.json +++ b/tests/scenarios/pg-prepared/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-quantified-null-elements/app.ts b/tests/scenarios/pg-quantified-null-elements/app.ts new file mode 100644 index 00000000..43d2d0e9 --- /dev/null +++ b/tests/scenarios/pg-quantified-null-elements/app.ts @@ -0,0 +1,70 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + items: { + id: Col<"int4">; + n: Col<"int4", { nullable: true }>; + tags: Col<"text[]">; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql(pool); +await pool.query(` + CREATE TABLE items (id integer NOT NULL, n integer, tags text[] NOT NULL); + INSERT INTO items VALUES (1, NULL, ARRAY['a']); +`); + +// A NULL element makes `x = ANY(…)` NULL when no other element matches. +export const parameter = await sql( + "SELECT 2 = ANY(ARRAY[$1::int4]) AS x", + null, +).execute(); +export const castArray = await sql( + "SELECT 2 = ANY(COALESCE($1::int4[], ARRAY[1])) AS x", + [1, null], +).execute(); +export const column = await sql( + "SELECT 1 = ANY(ARRAY[n]) AS x FROM items", +).execute(); +export const literal = await sql( + "SELECT 2 = ANY(ARRAY[1, NULL]) AS x", +).execute(); +export const subquery = await sql( + "SELECT 2 = ANY(ARRAY(SELECT n FROM items)) AS x", +).execute(); +export const aggregated = await sql( + "SELECT 2 = ANY(array_agg(n)) AS x FROM items", +).execute(); +export const all = await sql("SELECT 1 = ALL(ARRAY[1, NULL]) AS x").execute(); + +// Without NULL elements it is true or false. +export const elements = await sql("SELECT 1 = ANY(ARRAY[1, 2]) AS x").execute(); +export const arrayColumn = await sql( + "SELECT 'a' = ANY(tags) AS x FROM items", +).execute(); +export const allElements = await sql( + "SELECT 1 = ALL(ARRAY[1, 2]) AS x", +).execute(); +// WHERE only keeps true rows, whatever the array holds. +export const filtered = await sql( + "SELECT id FROM items WHERE 1 = ANY(ARRAY[n, 1])", +).execute(); +console.dir( + { + parameter, + castArray, + column, + literal, + subquery, + aggregated, + all, + elements, + arrayColumn, + allElements, + filtered, + }, + { depth: null }, +); +await pool.end(); diff --git a/tests/scenarios/pg-quantified-null-elements/expected.snap b/tests/scenarios/pg-quantified-null-elements/expected.snap new file mode 100644 index 00000000..18952da3 --- /dev/null +++ b/tests/scenarios/pg-quantified-null-elements/expected.snap @@ -0,0 +1,45 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-quantified-null-elements > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "aggregated": "({ x: null | false | true })[]", + "all": "({ x: null | false | true })[]", + "allElements": "({ x: false | true })[]", + "arrayColumn": "({ x: false | true })[]", + "castArray": "({ x: null | false | true })[]", + "column": "({ x: null | false | true })[]", + "elements": "({ x: false | true })[]", + "filtered": "({ id: number })[]", + "literal": "({ x: null | false | true })[]", + "parameter": "({ x: null | false | true })[]", + "subquery": "({ x: null | false | true })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + parameter: [ { x: null } ], + castArray: [ { x: null } ], + column: [ { x: null } ], + literal: [ { x: null } ], + subquery: [ { x: null } ], + aggregated: [ { x: null } ], + all: [ { x: null } ], + elements: [ { x: true } ], + arrayColumn: [ { x: true } ], + allElements: [ { x: false } ], + filtered: [ { id: 1 } ] +} +", + }, +} +`; diff --git a/tests/scenarios/pg-quantified-null-elements/package.json b/tests/scenarios/pg-quantified-null-elements/package.json new file mode 100644 index 00000000..44390950 --- /dev/null +++ b/tests/scenarios/pg-quantified-null-elements/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-quantified-null-elements", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-quick-start/package.json b/tests/scenarios/pg-quick-start/package.json index 5f1b47ef..1b55d48f 100644 --- a/tests/scenarios/pg-quick-start/package.json +++ b/tests/scenarios/pg-quick-start/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-text-types/app.ts b/tests/scenarios/pg-text-types/app.ts new file mode 100644 index 00000000..31e27b48 --- /dev/null +++ b/tests/scenarios/pg-text-types/app.ts @@ -0,0 +1,15 @@ +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const sql = createSql>(pool); +// node-postgres has no parser for these types and returns their text. +export const row = await sql(` + SELECT '$.a'::jsonpath AS path, NULL::jsonpath AS no_path, + 'english'::regconfig AS config, NULL::regconfig AS no_config, + 'c'::refcursor AS cursor, NULL::refcursor AS no_cursor, + '10:20:10,14,15'::pg_snapshot AS snapshot, NULL::pg_snapshot AS no_snapshot, + '10:20:10,14,15'::txid_snapshot AS txid_snapshot, NULL::txid_snapshot AS no_txid_snapshot +`).executeTakeFirstOrThrow(); +console.log(row); +await pool.end(); diff --git a/tests/scenarios/pg-text-types/expected.snap b/tests/scenarios/pg-text-types/expected.snap new file mode 100644 index 00000000..53558a16 --- /dev/null +++ b/tests/scenarios/pg-text-types/expected.snap @@ -0,0 +1,34 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-text-types > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "row": "{ config: string; cursor: string; no_config: null | string; no_cursor: null | string; no_path: null | string; no_snapshot: null | string; no_txid_snapshot: null | string; path: string; snapshot: string; txid_snapshot: string }", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + path: '$."a"', + no_path: null, + config: 'english', + no_config: null, + cursor: 'c', + no_cursor: null, + snapshot: '10:20:10,14,15', + no_snapshot: null, + txid_snapshot: '10:20:10,14,15', + no_txid_snapshot: null +} +", + }, +} +`; diff --git a/tests/scenarios/pg-text-types/package.json b/tests/scenarios/pg-text-types/package.json new file mode 100644 index 00000000..866d38e3 --- /dev/null +++ b/tests/scenarios/pg-text-types/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-text-types", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-transactions/package.json b/tests/scenarios/pg-transactions/package.json index fc037980..f6338997 100644 --- a/tests/scenarios/pg-transactions/package.json +++ b/tests/scenarios/pg-transactions/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/pg-values-defaults/app.ts b/tests/scenarios/pg-values-defaults/app.ts new file mode 100644 index 00000000..5112b4dc --- /dev/null +++ b/tests/scenarios/pg-values-defaults/app.ts @@ -0,0 +1,36 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import pg from "pg"; + +interface DB { + users: { + id: Col<"int4">; + name: Col<"text", { hasDefault: true }>; + bio: Col<"text", { nullable: true }>; + }; +} +const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); +await client.connect(); +await client.query( + "CREATE TABLE users (id int4 PRIMARY KEY, name text NOT NULL DEFAULT 'anonymous', bio text)", +); +const sql = createSql(client); +const bio: string | undefined = undefined; +const rows: { id: number; name?: string }[] = [{ id: 4, name: "d" }, { id: 5 }]; + +// PostgreSQL writes DEFAULT for a column a row leaves out. +await sql( + `INSERT INTO users ${sql.values("users", [{ id: 1, name: "a" }, { id: 2 }])}`, +).execute(); +await sql( + `INSERT INTO users ${sql.values([ + { id: 3, bio }, + { id: 6, bio: "f" }, + ])}`, +).execute(); +await sql(`INSERT INTO users ${sql.values("users", rows)}`).execute(); +export const users = await sql( + "SELECT id, name, bio FROM users ORDER BY id", +).execute(); +console.log(users); +await client.end(); diff --git a/tests/scenarios/pg-values-defaults/expected.snap b/tests/scenarios/pg-values-defaults/expected.snap new file mode 100644 index 00000000..1df26bd5 --- /dev/null +++ b/tests/scenarios/pg-values-defaults/expected.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-values-defaults > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "users": "({ bio: null | string; id: number; name: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ + { id: 1, name: 'a', bio: null }, + { id: 2, name: 'anonymous', bio: null }, + { id: 3, name: 'anonymous', bio: null }, + { id: 4, name: 'd', bio: null }, + { id: 5, name: 'anonymous', bio: null }, + { id: 6, name: 'anonymous', bio: 'f' } +] +", + }, +} +`; diff --git a/tests/scenarios/pg-values-defaults/package.json b/tests/scenarios/pg-values-defaults/package.json new file mode 100644 index 00000000..b1fb8791 --- /dev/null +++ b/tests/scenarios/pg-values-defaults/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-values-defaults", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-where-narrowing/app.ts b/tests/scenarios/pg-where-narrowing/app.ts new file mode 100644 index 00000000..eaa70376 --- /dev/null +++ b/tests/scenarios/pg-where-narrowing/app.ts @@ -0,0 +1,49 @@ +import type { Col } from "@typesql/core/postgres"; +import { createSql } from "@typesql/pg"; +import { Pool } from "pg"; + +interface DB { + users: { + id: Col<"int4">; + name: Col<"text">; + bio: Col<"text", { nullable: true }>; + }; + posts: { + id: Col<"int4">; + author_id: Col<"int4">; + title: Col<"text">; + body: Col<"text", { nullable: true }>; + }; +} +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +await pool.query(`CREATE TABLE users (id int PRIMARY KEY, name text NOT NULL, bio text); + CREATE TABLE posts (id int PRIMARY KEY, author_id int NOT NULL, title text NOT NULL, body text); + INSERT INTO users VALUES (1, 'Ada', NULL), (2, 'Grace', 'SQL'), (3, 'Lin', 'Lisp'); + INSERT INTO posts VALUES (1, 1, 'SQL', NULL);`); +const sql = createSql(pool); +export const any = await sql( + "SELECT id, bio FROM users WHERE bio = ANY (SELECT title FROM posts) ORDER BY id", +).execute(); +export const all = await sql( + "SELECT id, bio FROM users WHERE bio <> ALL (SELECT title FROM posts WHERE id > 1) ORDER BY id", +).execute(); +export const allArray = await sql( + "SELECT id, bio FROM users WHERE bio <> ALL ($1::text[]) ORDER BY id", + [], +).execute(); +export const notIn = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN (SELECT title FROM posts WHERE id > 1) ORDER BY id", +).execute(); +export const notInList = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN ('SQL') ORDER BY id", +).execute(); +export const joined = + await sql(`SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id ORDER BY u.id`).execute(); +export const matched = + await sql(`SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id WHERE p.title = 'SQL' ORDER BY u.id`).execute(); +export const right = await sql(`SELECT u.id, u.name, p.title FROM posts p + RIGHT JOIN users u ON p.author_id = u.id WHERE p.id IS NOT NULL`).execute(); +console.log({ any, all, allArray, notIn, notInList, joined, matched, right }); +await pool.end(); diff --git a/tests/scenarios/pg-where-narrowing/expected.snap b/tests/scenarios/pg-where-narrowing/expected.snap new file mode 100644 index 00000000..4c5c2783 --- /dev/null +++ b/tests/scenarios/pg-where-narrowing/expected.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pg-where-narrowing > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "all": "({ bio: null | string; id: number })[]", + "allArray": "({ bio: null | string; id: number })[]", + "any": "({ bio: string; id: number })[]", + "joined": "({ body: null | string; id: null | number; name: string; title: null | string })[]", + "matched": "({ body: null | string; id: number; name: string; title: string })[]", + "notIn": "({ bio: null | string; id: number })[]", + "notInList": "({ bio: string; id: number })[]", + "right": "({ id: number; name: string; title: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + any: [ { id: 2, bio: 'SQL' } ], + all: [ + { id: 1, bio: null }, + { id: 2, bio: 'SQL' }, + { id: 3, bio: 'Lisp' } + ], + allArray: [ + { id: 1, bio: null }, + { id: 2, bio: 'SQL' }, + { id: 3, bio: 'Lisp' } + ], + notIn: [ + { id: 1, bio: null }, + { id: 2, bio: 'SQL' }, + { id: 3, bio: 'Lisp' } + ], + notInList: [ { id: 3, bio: 'Lisp' } ], + joined: [ + { name: 'Ada', id: 1, title: 'SQL', body: null }, + { name: 'Grace', id: null, title: null, body: null }, + { name: 'Lin', id: null, title: null, body: null } + ], + matched: [ { name: 'Ada', id: 1, title: 'SQL', body: null } ], + right: [ { id: 1, name: 'Ada', title: 'SQL' } ] +} +", + }, +} +`; diff --git a/tests/scenarios/pg-where-narrowing/package.json b/tests/scenarios/pg-where-narrowing/package.json new file mode 100644 index 00000000..505ec241 --- /dev/null +++ b/tests/scenarios/pg-where-narrowing/package.json @@ -0,0 +1,21 @@ +{ + "name": "pg-where-narrowing", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/pg": "workspace:*", + "pg": "catalog:", + "@types/pg": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "postgres" + } +} diff --git a/tests/scenarios/pg-writes/package.json b/tests/scenarios/pg-writes/package.json index e92500dc..deb344fe 100644 --- a/tests/scenarios/pg-writes/package.json +++ b/tests/scenarios/pg-writes/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/postgresjs-workflows/package.json b/tests/scenarios/postgresjs-workflows/package.json index 0224bb2d..b5089b3d 100644 --- a/tests/scenarios/postgresjs-workflows/package.json +++ b/tests/scenarios/postgresjs-workflows/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/prisma-client-mysql/app.ts b/tests/scenarios/prisma-client-mysql/app.ts deleted file mode 100644 index 93a5e7b0..00000000 --- a/tests/scenarios/prisma-client-mysql/app.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PrismaMariaDb } from "@prisma/adapter-mariadb"; -import { mysql } from "@typesql/core/mysql"; -import { createSql } from "@typesql/prisma"; -import { PrismaClient } from "./generated/client/client.js"; - -const url = new URL(process.env.DATABASE_URL!); -const adapter = new PrismaMariaDb({ - host: url.hostname, - port: Number(url.port), - user: url.username, - password: url.password, - database: url.pathname.slice(1), - connectionLimit: 5, -}); -const client = new PrismaClient({ adapter }); -const sql = createSql(mysql, client); -await client.user.create({ data: { name: "Ada" } }); -export const rows = await sql( - "SELECT id, name, bio FROM User ORDER BY id", -).execute(); -try { - await sql.transaction(async (tx) => { - await tx("DELETE FROM User WHERE id = $1", 1).execute(); - throw new Error("abort"); - }); -} catch (error) { - console.log(error instanceof Error ? error.message : error); -} -export const after = await sql("SELECT id FROM User ORDER BY id").execute(); -console.log({ rows, after }); -await client.$disconnect(); diff --git a/tests/scenarios/prisma-client-mysql/expected.snap b/tests/scenarios/prisma-client-mysql/expected.snap deleted file mode 100644 index 8a454985..00000000 --- a/tests/scenarios/prisma-client-mysql/expected.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`prisma-client-mysql > scenario 1`] = ` -{ - "compile": { - "status": 0, - "stderr": "", - "stdout": "", - }, - "compiler": { - "diagnostics": [], - "types": { - "after": "({ id: number })[]", - "rows": "({ bio: null | string; id: number; name: string })[]", - }, - }, - "generate": { - "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", - "stdout": "", - }, - "start": { - "status": 0, - "stderr": "", - "stdout": "abort -{ rows: [ { id: 1, name: 'Ada', bio: null } ], after: [ { id: 1 } ] } -", - }, -} -`; diff --git a/tests/scenarios/prisma-client-mysql/package.json b/tests/scenarios/prisma-client-mysql/package.json deleted file mode 100644 index f4c69972..00000000 --- a/tests/scenarios/prisma-client-mysql/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "prisma-client-mysql", - "private": true, - "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", - "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" - }, - "dependencies": { - "@typesql/core": "workspace:*", - "@typesql/prisma": "workspace:*", - "prisma": "catalog:", - "@prisma/client": "catalog:", - "@prisma/adapter-mariadb": "catalog:" - }, - "devDependencies": { - "typescript": "catalog:", - "@types/node": "catalog:" - }, - "scenario": { - "database": "mysql" - } -} diff --git a/tests/scenarios/prisma-client-mysql/prisma.config.ts b/tests/scenarios/prisma-client-mysql/prisma.config.ts deleted file mode 100644 index 69b068e4..00000000 --- a/tests/scenarios/prisma-client-mysql/prisma.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from "prisma/config"; -export default defineConfig({ - schema: "schema.prisma", - datasource: { url: process.env.DATABASE_URL! }, -}); diff --git a/tests/scenarios/prisma-client-mysql/schema.prisma b/tests/scenarios/prisma-client-mysql/schema.prisma deleted file mode 100644 index 07bb0c64..00000000 --- a/tests/scenarios/prisma-client-mysql/schema.prisma +++ /dev/null @@ -1,14 +0,0 @@ -generator client { - provider = "prisma-client" - output = "./generated/client" - moduleFormat = "esm" - importFileExtension = "js" -} -datasource db { - provider = "mysql" -} -model User { - id Int @id @default(autoincrement()) - name String - bio String? -} diff --git a/tests/scenarios/prisma-client-postgres/app.ts b/tests/scenarios/prisma-client-postgres/app.ts deleted file mode 100644 index 5de8b8ca..00000000 --- a/tests/scenarios/prisma-client-postgres/app.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PrismaPg } from "@prisma/adapter-pg"; -import { postgres } from "@typesql/core/postgres"; -import { createSql } from "@typesql/prisma"; -import { PrismaClient } from "./generated/client/client.js"; - -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); -const client = new PrismaClient({ adapter }); -const sql = createSql(postgres, client); -await client.user.create({ data: { name: "Ada" } }); -export const rows = await sql( - 'SELECT id, name, bio FROM "User" ORDER BY id', -).execute(); -try { - await sql.transaction(async (tx) => { - await tx('DELETE FROM "User" WHERE id = $1', 1).execute(); - throw new Error("abort"); - }); -} catch (error) { - console.log(error instanceof Error ? error.message : error); -} -export const after = await sql('SELECT id FROM "User" ORDER BY id').execute(); -console.log({ rows, after }); -await client.$disconnect(); diff --git a/tests/scenarios/prisma-client-postgres/expected.snap b/tests/scenarios/prisma-client-postgres/expected.snap deleted file mode 100644 index 401ed4b9..00000000 --- a/tests/scenarios/prisma-client-postgres/expected.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`prisma-client-postgres > scenario 1`] = ` -{ - "compile": { - "status": 0, - "stderr": "", - "stdout": "", - }, - "compiler": { - "diagnostics": [], - "types": { - "after": "({ id: number })[]", - "rows": "({ bio: null | string; id: number; name: string })[]", - }, - }, - "generate": { - "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", - "stdout": "", - }, - "start": { - "status": 0, - "stderr": "", - "stdout": "abort -{ rows: [ { id: 1, name: 'Ada', bio: null } ], after: [ { id: 1 } ] } -", - }, -} -`; diff --git a/tests/scenarios/prisma-client-postgres/package.json b/tests/scenarios/prisma-client-postgres/package.json deleted file mode 100644 index b47b6a63..00000000 --- a/tests/scenarios/prisma-client-postgres/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "prisma-client-postgres", - "private": true, - "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", - "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" - }, - "dependencies": { - "@typesql/core": "workspace:*", - "@typesql/prisma": "workspace:*", - "prisma": "catalog:", - "@prisma/client": "catalog:", - "@prisma/adapter-pg": "catalog:", - "pg": "catalog:", - "@types/pg": "catalog:" - }, - "devDependencies": { - "typescript": "catalog:", - "@types/node": "catalog:" - }, - "scenario": { - "database": "postgres" - } -} diff --git a/tests/scenarios/prisma-client-postgres/prisma.config.ts b/tests/scenarios/prisma-client-postgres/prisma.config.ts deleted file mode 100644 index 69b068e4..00000000 --- a/tests/scenarios/prisma-client-postgres/prisma.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from "prisma/config"; -export default defineConfig({ - schema: "schema.prisma", - datasource: { url: process.env.DATABASE_URL! }, -}); diff --git a/tests/scenarios/prisma-client-postgres/schema.prisma b/tests/scenarios/prisma-client-postgres/schema.prisma deleted file mode 100644 index 33a364c7..00000000 --- a/tests/scenarios/prisma-client-postgres/schema.prisma +++ /dev/null @@ -1,14 +0,0 @@ -generator client { - provider = "prisma-client" - output = "./generated/client" - moduleFormat = "esm" - importFileExtension = "js" -} -datasource db { - provider = "postgresql" -} -model User { - id Int @id @default(autoincrement()) - name String - bio String? -} diff --git a/tests/scenarios/prisma-client-sqlite/app.ts b/tests/scenarios/prisma-client-sqlite/app.ts deleted file mode 100644 index c89ee0f4..00000000 --- a/tests/scenarios/prisma-client-sqlite/app.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; -import { sqlite } from "@typesql/core/sqlite"; -import { createSql } from "@typesql/prisma"; -import { PrismaClient } from "./generated/client/client.js"; - -const adapter = new PrismaBetterSqlite3({ - url: `file:${process.env.SQLITE_PATH}`, -}); -const client = new PrismaClient({ adapter }); -const sql = createSql(sqlite, client); -await client.user.create({ data: { name: "Ada" } }); -export const rows = await sql( - 'SELECT id, name, bio FROM "User" ORDER BY id', -).execute(); -try { - await sql.transaction(async (tx) => { - await tx('DELETE FROM "User" WHERE id = $1', 1).execute(); - throw new Error("abort"); - }); -} catch (error) { - console.log(error instanceof Error ? error.message : error); -} -export const after = await sql('SELECT id FROM "User" ORDER BY id').execute(); -console.log({ rows, after }); -await client.$disconnect(); diff --git a/tests/scenarios/prisma-client-sqlite/expected.snap b/tests/scenarios/prisma-client-sqlite/expected.snap deleted file mode 100644 index 657418f7..00000000 --- a/tests/scenarios/prisma-client-sqlite/expected.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`prisma-client-sqlite > scenario 1`] = ` -{ - "compile": { - "status": 0, - "stderr": "", - "stdout": "", - }, - "compiler": { - "diagnostics": [], - "types": { - "after": "({ id: number })[]", - "rows": "({ bio: null | string; id: number; name: string })[]", - }, - }, - "generate": { - "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", - "stdout": "", - }, - "start": { - "status": 0, - "stderr": "", - "stdout": "abort -{ rows: [ { id: 1, name: 'Ada', bio: null } ], after: [ { id: 1 } ] } -", - }, -} -`; diff --git a/tests/scenarios/prisma-client-sqlite/package.json b/tests/scenarios/prisma-client-sqlite/package.json deleted file mode 100644 index 11311b0d..00000000 --- a/tests/scenarios/prisma-client-sqlite/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "prisma-client-sqlite", - "private": true, - "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", - "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" - }, - "dependencies": { - "@typesql/core": "workspace:*", - "@typesql/prisma": "workspace:*", - "prisma": "catalog:", - "@prisma/client": "catalog:", - "@prisma/adapter-better-sqlite3": "catalog:", - "better-sqlite3": "catalog:", - "@types/better-sqlite3": "catalog:" - }, - "devDependencies": { - "typescript": "catalog:", - "@types/node": "catalog:" - }, - "scenario": { - "database": "sqlite" - } -} diff --git a/tests/scenarios/prisma-client-sqlite/prisma.config.ts b/tests/scenarios/prisma-client-sqlite/prisma.config.ts deleted file mode 100644 index e705c97c..00000000 --- a/tests/scenarios/prisma-client-sqlite/prisma.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from "prisma/config"; -export default defineConfig({ - schema: "schema.prisma", - datasource: { url: `file:${process.env.SQLITE_PATH}` }, -}); diff --git a/tests/scenarios/prisma-client-sqlite/schema.prisma b/tests/scenarios/prisma-client-sqlite/schema.prisma deleted file mode 100644 index 177b52fe..00000000 --- a/tests/scenarios/prisma-client-sqlite/schema.prisma +++ /dev/null @@ -1,14 +0,0 @@ -generator client { - provider = "prisma-client" - output = "./generated/client" - moduleFormat = "esm" - importFileExtension = "js" -} -datasource db { - provider = "sqlite" -} -model User { - id Int @id @default(autoincrement()) - name String - bio String? -} diff --git a/tests/scenarios/prisma-generation-mysql/app.ts b/tests/scenarios/prisma-generation-mysql/app.ts index ec004d70..a890e9e1 100644 --- a/tests/scenarios/prisma-generation-mysql/app.ts +++ b/tests/scenarios/prisma-generation-mysql/app.ts @@ -17,7 +17,10 @@ const client = new PrismaClient({ adapter }); import type { DB } from "./generated/schema.js"; const sql = createSql(mysql, client); +// Without a generated schema, the Prisma client's models are the tables. +const fromClient = createSql(mysql, client); await client.user.create({ data: { name: "Ada", big: 9007199254740993n } }); +await client.author.create({ data: { name: "Ada" } }); export const rows = await sql( "SELECT id, name, display_name, role, balance, big FROM users ORDER BY id", ).execute(); @@ -31,5 +34,9 @@ try { } export const after = await sql("SELECT id FROM users ORDER BY id").execute(); const prefs = await sql("SELECT prefs FROM users ORDER BY id").execute(); +export const clientRows = await fromClient( + "SELECT id, name, bio FROM Author ORDER BY id", +).execute(); console.log({ rows, after, prefs }); +console.log({ clientRows }); await client.$disconnect(); diff --git a/tests/scenarios/prisma-generation-mysql/expected.snap b/tests/scenarios/prisma-generation-mysql/expected.snap index 8727ed83..1d25bcfe 100644 --- a/tests/scenarios/prisma-generation-mysql/expected.snap +++ b/tests/scenarios/prisma-generation-mysql/expected.snap @@ -9,7 +9,7 @@ import type { Col, Typed } from "@typesql/core/mysql"; export interface DB { /** Prisma model \`User\`. */ users: { - id: Col<"int", { hasDefault: true; autoIncrement: true }>; + id: Col<"int", { autoIncrement: true }>; name: Col<"varchar">; /** Prisma field \`bio\`. */ display_name: Col<"text", { nullable: true }>; @@ -20,6 +20,11 @@ export interface DB { /** Prisma field \`createdAt\`. */ created_at: Col<"datetime", { hasDefault: true }>; }; + Author: { + id: Col<"int", { autoIncrement: true }>; + name: Col<"varchar">; + bio: Col<"varchar", { nullable: true }>; + }; } ", }, @@ -32,18 +37,13 @@ export interface DB { "diagnostics": [], "types": { "after": "({ id: number })[]", + "clientRows": "({ bio: null | string; id: number; name: string })[]", "rows": "({ balance: Decimal; big: bigint; display_name: null | string; id: number; name: string; role: "USER" | "admin" })[]", }, }, "generate": { "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", + "stderr": "", "stdout": "", }, "start": { @@ -64,6 +64,7 @@ Prisma schema loaded from schema.prisma. after: [ { id: 1 } ], prefs: [ { prefs: null } ] } +{ clientRows: [ { id: 1, name: 'Ada', bio: null } ] } ", }, } diff --git a/tests/scenarios/prisma-generation-mysql/package.json b/tests/scenarios/prisma-generation-mysql/package.json index 505733c6..cdbc1205 100644 --- a/tests/scenarios/prisma-generation-mysql/package.json +++ b/tests/scenarios/prisma-generation-mysql/package.json @@ -3,9 +3,8 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" + "generate": "prisma generate --no-hints > prisma-generate.log 2>&1 && prisma db push > prisma-push.log 2>&1" }, "dependencies": { "@typesql/core": "workspace:*", diff --git a/tests/scenarios/prisma-generation-mysql/schema.prisma b/tests/scenarios/prisma-generation-mysql/schema.prisma index eef99e69..9a290d1e 100644 --- a/tests/scenarios/prisma-generation-mysql/schema.prisma +++ b/tests/scenarios/prisma-generation-mysql/schema.prisma @@ -26,3 +26,8 @@ model User { createdAt DateTime @default(now()) @map("created_at") @@map("users") } +model Author { + id Int @id @default(autoincrement()) + name String + bio String? +} diff --git a/tests/scenarios/prisma-generation-postgres/app.ts b/tests/scenarios/prisma-generation-postgres/app.ts index 1aed5769..b01e6ec4 100644 --- a/tests/scenarios/prisma-generation-postgres/app.ts +++ b/tests/scenarios/prisma-generation-postgres/app.ts @@ -9,7 +9,10 @@ const client = new PrismaClient({ adapter }); import type { DB } from "./generated/schema.js"; const sql = createSql(postgres, client); +// Without a generated schema, the Prisma client's models are the tables. +const fromClient = createSql(postgres, client); await client.user.create({ data: { name: "Ada" } }); +await client.author.create({ data: { name: "Ada" } }); export const rows = await sql( "SELECT id, name, display_name FROM users ORDER BY id", ).execute(); @@ -22,5 +25,9 @@ try { console.log(error instanceof Error ? error.message : error); } export const after = await sql("SELECT id FROM users ORDER BY id").execute(); +export const clientRows = await fromClient( + 'SELECT id, name, bio FROM "Author" ORDER BY id', +).execute(); console.log({ rows, after }); +console.log({ clientRows }); await client.$disconnect(); diff --git a/tests/scenarios/prisma-generation-postgres/expected.snap b/tests/scenarios/prisma-generation-postgres/expected.snap index ab61064d..a7078598 100644 --- a/tests/scenarios/prisma-generation-postgres/expected.snap +++ b/tests/scenarios/prisma-generation-postgres/expected.snap @@ -14,6 +14,11 @@ export interface DB { /** Prisma field \`bio\`. */ display_name: Col<"text", { nullable: true }>; }; + Author: { + id: Col<"int4", { hasDefault: true }>; + name: Col<"text">; + bio: Col<"text", { nullable: true }>; + }; } ", }, @@ -26,18 +31,13 @@ export interface DB { "diagnostics": [], "types": { "after": "({ id: number })[]", + "clientRows": "({ bio: null | string; id: number; name: string })[]", "rows": "({ display_name: null | string; id: number; name: string })[]", }, }, "generate": { "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", + "stderr": "", "stdout": "", }, "start": { @@ -48,6 +48,7 @@ Prisma schema loaded from schema.prisma. rows: [ { id: 1, name: 'Ada', display_name: null } ], after: [ { id: 1 } ] } +{ clientRows: [ { id: 1, name: 'Ada', bio: null } ] } ", }, } diff --git a/tests/scenarios/prisma-generation-postgres/package.json b/tests/scenarios/prisma-generation-postgres/package.json index 0ae6685d..622532e2 100644 --- a/tests/scenarios/prisma-generation-postgres/package.json +++ b/tests/scenarios/prisma-generation-postgres/package.json @@ -3,9 +3,8 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" + "generate": "prisma generate --no-hints > prisma-generate.log 2>&1 && prisma db push > prisma-push.log 2>&1" }, "dependencies": { "@typesql/core": "workspace:*", diff --git a/tests/scenarios/prisma-generation-postgres/schema.prisma b/tests/scenarios/prisma-generation-postgres/schema.prisma index 7f993afd..a81a0284 100644 --- a/tests/scenarios/prisma-generation-postgres/schema.prisma +++ b/tests/scenarios/prisma-generation-postgres/schema.prisma @@ -17,3 +17,8 @@ model User { bio String? @map("display_name") @@map("users") } +model Author { + id Int @id @default(autoincrement()) + name String + bio String? +} diff --git a/tests/scenarios/prisma-generation-sqlite/app.ts b/tests/scenarios/prisma-generation-sqlite/app.ts index a5511fdf..afce5af9 100644 --- a/tests/scenarios/prisma-generation-sqlite/app.ts +++ b/tests/scenarios/prisma-generation-sqlite/app.ts @@ -11,7 +11,10 @@ const client = new PrismaClient({ adapter }); import type { DB } from "./generated/schema.js"; const sql = createSql(sqlite, client); +// Without a generated schema, the Prisma client's models are the tables. +const fromClient = createSql(sqlite, client); await client.user.create({ data: { name: "Ada" } }); +await client.author.create({ data: { name: "Ada" } }); export const rows = await sql( "SELECT id, name, display_name FROM users ORDER BY id", ).execute(); @@ -24,5 +27,9 @@ try { console.log(error instanceof Error ? error.message : error); } export const after = await sql("SELECT id FROM users ORDER BY id").execute(); +export const clientRows = await fromClient( + 'SELECT id, name, bio FROM "Author" ORDER BY id', +).execute(); console.log({ rows, after }); +console.log({ clientRows }); await client.$disconnect(); diff --git a/tests/scenarios/prisma-generation-sqlite/expected.snap b/tests/scenarios/prisma-generation-sqlite/expected.snap index 537eb7bf..849b71a2 100644 --- a/tests/scenarios/prisma-generation-sqlite/expected.snap +++ b/tests/scenarios/prisma-generation-sqlite/expected.snap @@ -14,6 +14,11 @@ export interface DB { /** Prisma field \`bio\`. */ display_name: Col<"text", { nullable: true }>; }; + Author: { + id: Col<"integer", { hasDefault: true }>; + name: Col<"text">; + bio: Col<"text", { nullable: true }>; + }; } ", }, @@ -26,18 +31,13 @@ export interface DB { "diagnostics": [], "types": { "after": "({ id: number })[]", + "clientRows": "({ bio: null | string; id: number; name: string })[]", "rows": "({ display_name: null | string; id: number; name: string })[]", }, }, "generate": { "status": 0, - "stderr": "Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -Loaded Prisma config from prisma.config.ts. - -Prisma schema loaded from schema.prisma. -", + "stderr": "", "stdout": "", }, "start": { @@ -48,6 +48,7 @@ Prisma schema loaded from schema.prisma. rows: [ { id: 1, name: 'Ada', display_name: null } ], after: [ { id: 1 } ] } +{ clientRows: [ { id: 1, name: 'Ada', bio: null } ] } ", }, } diff --git a/tests/scenarios/prisma-generation-sqlite/package.json b/tests/scenarios/prisma-generation-sqlite/package.json index b2234927..2760d9c3 100644 --- a/tests/scenarios/prisma-generation-sqlite/package.json +++ b/tests/scenarios/prisma-generation-sqlite/package.json @@ -3,9 +3,8 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js", - "generate": "prisma generate --no-hints > prisma-generate.log && prisma db push > prisma-push.log" + "generate": "prisma generate --no-hints > prisma-generate.log 2>&1 && prisma db push > prisma-push.log 2>&1" }, "dependencies": { "@typesql/core": "workspace:*", diff --git a/tests/scenarios/prisma-generation-sqlite/schema.prisma b/tests/scenarios/prisma-generation-sqlite/schema.prisma index 11838e27..8d5787a5 100644 --- a/tests/scenarios/prisma-generation-sqlite/schema.prisma +++ b/tests/scenarios/prisma-generation-sqlite/schema.prisma @@ -17,3 +17,8 @@ model User { bio String? @map("display_name") @@map("users") } +model Author { + id Int @id @default(autoincrement()) + name String + bio String? +} diff --git a/tests/scenarios/observation-shapes/app.ts b/tests/scenarios/runner-observation-shapes/app.ts similarity index 100% rename from tests/scenarios/observation-shapes/app.ts rename to tests/scenarios/runner-observation-shapes/app.ts diff --git a/tests/scenarios/observation-shapes/expected.snap b/tests/scenarios/runner-observation-shapes/expected.snap similarity index 92% rename from tests/scenarios/observation-shapes/expected.snap rename to tests/scenarios/runner-observation-shapes/expected.snap index 9bcd8878..18b3a36f 100644 --- a/tests/scenarios/observation-shapes/expected.snap +++ b/tests/scenarios/runner-observation-shapes/expected.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`observation-shapes > scenario 1`] = ` +exports[`runner-observation-shapes > scenario 1`] = ` { "compile": { "status": 0, diff --git a/tests/scenarios/observation-shapes/package.json b/tests/scenarios/runner-observation-shapes/package.json similarity index 58% rename from tests/scenarios/observation-shapes/package.json rename to tests/scenarios/runner-observation-shapes/package.json index 9799878d..f7d187b9 100644 --- a/tests/scenarios/observation-shapes/package.json +++ b/tests/scenarios/runner-observation-shapes/package.json @@ -1,10 +1,7 @@ { - "name": "observation-shapes", + "name": "runner-observation-shapes", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": {}, "devDependencies": { "typescript": "catalog:", diff --git a/tests/scenarios/compiler-undeclared-dependency/app.ts b/tests/scenarios/runner-undeclared-dependency/app.ts similarity index 100% rename from tests/scenarios/compiler-undeclared-dependency/app.ts rename to tests/scenarios/runner-undeclared-dependency/app.ts diff --git a/tests/scenarios/compiler-undeclared-dependency/expected.snap b/tests/scenarios/runner-undeclared-dependency/expected.snap similarity index 71% rename from tests/scenarios/compiler-undeclared-dependency/expected.snap rename to tests/scenarios/runner-undeclared-dependency/expected.snap index b059fee4..054074a3 100644 --- a/tests/scenarios/compiler-undeclared-dependency/expected.snap +++ b/tests/scenarios/runner-undeclared-dependency/expected.snap @@ -1,12 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`compiler-undeclared-dependency > scenario 1`] = ` +exports[`runner-undeclared-dependency > scenario 1`] = ` { "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(1,27): error TS2307: Cannot find module '@typesql/core' or its corresponding type declarations. -", }, "compiler": { "diagnostics": [ diff --git a/tests/scenarios/compiler-undeclared-dependency/package.json b/tests/scenarios/runner-undeclared-dependency/package.json similarity index 72% rename from tests/scenarios/compiler-undeclared-dependency/package.json rename to tests/scenarios/runner-undeclared-dependency/package.json index 460777eb..ee5f471e 100644 --- a/tests/scenarios/compiler-undeclared-dependency/package.json +++ b/tests/scenarios/runner-undeclared-dependency/package.json @@ -1,10 +1,7 @@ { - "name": "compiler-undeclared-dependency", + "name": "runner-undeclared-dependency", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/pg": "workspace:*", "pg": "catalog:", diff --git a/tests/scenarios/runtime-inline-reuse/expected.snap b/tests/scenarios/runtime-inline-reuse/expected.snap index 91852510..09f3623d 100644 --- a/tests/scenarios/runtime-inline-reuse/expected.snap +++ b/tests/scenarios/runtime-inline-reuse/expected.snap @@ -17,24 +17,6 @@ exports[`runtime-inline-reuse > scenario 1`] = ` }, "start": { "status": 1, - "stderr": "file:///packages/core/dist/runtime/compile.js:3 - throw new Error(\`typesql: \${message}\`); - ^ - -Error: typesql: this sql.param() value was already used; call sql.param() once per statement - at fail (file:///packages/core/dist/runtime/compile.js:3:11) - at use (file:///packages/core/dist/runtime/compile.js:36:9) - at file:///packages/core/dist/runtime/compile.js:146:23 - at scan (file:///packages/core/dist/runtime/compile.js:107:44) - at compile (file:///packages/core/dist/runtime/compile.js:139:17) - at call (file:///packages/core/dist/create-sql.js:55:34) - at file:///dist/app.js:6:1 - at ModuleJob.run (node:internal/modules/esm/module_job:569:25) - at async node:internal/modules/esm/loader:650:26 - at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) - -Node.js v26.8.1 -", "stdout": "", }, } diff --git a/tests/scenarios/runtime-inline-reuse/package.json b/tests/scenarios/runtime-inline-reuse/package.json index 8986b894..f301e6a2 100644 --- a/tests/scenarios/runtime-inline-reuse/package.json +++ b/tests/scenarios/runtime-inline-reuse/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/runtime-no-executor/expected.snap b/tests/scenarios/runtime-no-executor/expected.snap index 21f2cc01..7e9e829c 100644 --- a/tests/scenarios/runtime-no-executor/expected.snap +++ b/tests/scenarios/runtime-no-executor/expected.snap @@ -17,21 +17,6 @@ exports[`runtime-no-executor > scenario 1`] = ` }, "start": { "status": 1, - "stderr": "file:///packages/core/dist/runtime/compile.js:3 - throw new Error(\`typesql: \${message}\`); - ^ - -Error: typesql: no executor; pass one to createSql() or execute() - at fail (file:///packages/core/dist/runtime/compile.js:3:11) - at #pick (file:///packages/core/dist/query.js:22:29) - at SqlQuery.execute (file:///packages/core/dist/query.js:25:40) - at file:///dist/app.js:4:28 - at ModuleJob.run (node:internal/modules/esm/module_job:569:25) - at async node:internal/modules/esm/loader:650:26 - at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) - -Node.js v26.8.1 -", "stdout": "", }, } diff --git a/tests/scenarios/runtime-no-executor/package.json b/tests/scenarios/runtime-no-executor/package.json index 93fe5a33..48b540f4 100644 --- a/tests/scenarios/runtime-no-executor/package.json +++ b/tests/scenarios/runtime-no-executor/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sql-dialect/app.ts b/tests/scenarios/sql-dialect/app.ts index 5d3e67b7..2ab22b17 100644 --- a/tests/scenarios/sql-dialect/app.ts +++ b/tests/scenarios/sql-dialect/app.ts @@ -1,7 +1,13 @@ import { DatabaseSync } from "node:sqlite"; import { createSql } from "@typesql/core"; +import { mariadb, mysql } from "@typesql/core/mysql"; import { CasePlugin } from "@typesql/core/plugins"; -import { postgres } from "@typesql/core/postgres"; +import { + type DefaultTypeMap, + postgres, + type Sql, +} from "@typesql/core/postgres"; +import { sqlite as sqliteDialectObject } from "@typesql/core/sqlite"; import { createSql as createPgSql } from "@typesql/pg"; import { createSql as createSqliteSql } from "@typesql/sqlite"; @@ -15,5 +21,30 @@ export const sqliteDialect = sqlite.dialect; export const withPlugin = sqlite.withPlugin(new CasePlugin()).dialect; export const inTransaction = await sqlite.transaction(async (tx) => tx.dialect); -console.log({ core, pg, sqliteDialect, withPlugin, inTransaction }); +// An `sql` of createSql() keeps its dialect, and its type, with a plugin. +const camel: Sql = createSql( + postgres, +).withPlugin(new CasePlugin()); +export const corePostgresWithPlugin = camel.dialect; +export const coreSqliteWithPlugin = createSql( + sqliteDialectObject, +).withPlugin(new CasePlugin()).dialect; +export const coreMysqlWithPlugin = createSql(mysql).withPlugin( + new CasePlugin(), +).dialect; +export const coreMariadbWithPlugin = createSql(mariadb).withPlugin( + new CasePlugin(), +).dialect; + +console.log({ + core, + pg, + sqliteDialect, + withPlugin, + inTransaction, + corePostgresWithPlugin, + coreSqliteWithPlugin, + coreMysqlWithPlugin, + coreMariadbWithPlugin, +}); console.log(Object.getOwnPropertyDescriptor(sqlite, "dialect")); diff --git a/tests/scenarios/sql-dialect/expected.snap b/tests/scenarios/sql-dialect/expected.snap index eadec3c3..b25e5253 100644 --- a/tests/scenarios/sql-dialect/expected.snap +++ b/tests/scenarios/sql-dialect/expected.snap @@ -11,6 +11,10 @@ exports[`sql-dialect > scenario 1`] = ` "diagnostics": [], "types": { "core": ""postgres"", + "coreMariadbWithPlugin": ""mariadb"", + "coreMysqlWithPlugin": ""mysql"", + "corePostgresWithPlugin": ""postgres"", + "coreSqliteWithPlugin": ""sqlite"", "inTransaction": ""sqlite"", "pg": ""postgres"", "sqliteDialect": ""sqlite"", @@ -25,7 +29,11 @@ exports[`sql-dialect > scenario 1`] = ` pg: 'postgres', sqliteDialect: 'sqlite', withPlugin: 'sqlite', - inTransaction: 'sqlite' + inTransaction: 'sqlite', + corePostgresWithPlugin: 'postgres', + coreSqliteWithPlugin: 'sqlite', + coreMysqlWithPlugin: 'mysql', + coreMariadbWithPlugin: 'mariadb' } { value: 'sqlite', diff --git a/tests/scenarios/sql-dialect/package.json b/tests/scenarios/sql-dialect/package.json index d37031a3..06812566 100644 --- a/tests/scenarios/sql-dialect/package.json +++ b/tests/scenarios/sql-dialect/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sqlite-aggregate-traversal/package.json b/tests/scenarios/sqlite-aggregate-traversal/package.json index d0b0fd77..1e8a8b0f 100644 --- a/tests/scenarios/sqlite-aggregate-traversal/package.json +++ b/tests/scenarios/sqlite-aggregate-traversal/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sqlite-declaration-emit/app.ts b/tests/scenarios/sqlite-declaration-emit/app.ts index 3b2304c8..713b5589 100644 --- a/tests/scenarios/sqlite-declaration-emit/app.ts +++ b/tests/scenarios/sqlite-declaration-emit/app.ts @@ -10,4 +10,13 @@ db.exec(`CREATE TABLE subscriptions (id integer PRIMARY KEY, user_id text NOT NU INSERT INTO subscriptions VALUES (1, 'ada', 'pro', NULL), (2, 'ada', 'free', '2026-01-01');`); export const active = await activeSubscriptionQuery("ada").execute(); console.log(active, Object.isFrozen(active[0])); +// A transaction inside another reaches the driver with its depth. +await sql.transaction(async (tx) => { + await tx.transaction(async (inner) => { + await inner("UPDATE subscriptions SET plan_name = 'team'").execute(); + }); +}); +console.log( + await sql("SELECT plan_name FROM subscriptions ORDER BY id").execute(), +); db.close(); diff --git a/tests/scenarios/sqlite-declaration-emit/expected.snap b/tests/scenarios/sqlite-declaration-emit/expected.snap index 4be170df..9e0a9cc1 100644 --- a/tests/scenarios/sqlite-declaration-emit/expected.snap +++ b/tests/scenarios/sqlite-declaration-emit/expected.snap @@ -18,6 +18,7 @@ exports[`sqlite-declaration-emit > scenario 1`] = ` "status": 0, "stderr": "", "stdout": "[ { id: 1n, planName: 'pro', endedAt: null } ] true +[ { planName: 'team' }, { planName: 'team' } ] ", }, } diff --git a/tests/scenarios/sqlite-declaration-emit/package.json b/tests/scenarios/sqlite-declaration-emit/package.json index 7e01aee9..8fa885f7 100644 --- a/tests/scenarios/sqlite-declaration-emit/package.json +++ b/tests/scenarios/sqlite-declaration-emit/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sqlite-declaration-emit/sql.ts b/tests/scenarios/sqlite-declaration-emit/sql.ts index 61d773fd..61349aff 100644 --- a/tests/scenarios/sqlite-declaration-emit/sql.ts +++ b/tests/scenarios/sqlite-declaration-emit/sql.ts @@ -2,6 +2,7 @@ import { DatabaseSync } from "node:sqlite"; import type { IdentityPluginTypes, SqlPlugin } from "@typesql/core"; import { createTransactionalSql, + type SqlDriver, serialTransaction, } from "@typesql/core/adapter"; import { CasePlugin } from "@typesql/core/plugins"; @@ -47,20 +48,26 @@ class FrozenRows implements SqlPlugin { export const db = new DatabaseSync(process.env.SQLITE_PATH!); +const driver: SqlDriver = { + executor: (client) => executor(client, { bigint: true }), + transaction: (client, depth, fn) => + serialTransaction( + client, + depth, + async (statement) => { + client.exec(statement); + }, + () => fn(client), + ), +}; + +// A driver may pass its calls on, as written before `savepoint` came. export const sql = createTransactionalSql( sqlite, db, { - executor: (client) => executor(client, { bigint: true }), - transaction: (client, depth, fn) => - serialTransaction( - client, - depth, - async (statement) => { - client.exec(statement); - }, - () => fn(client), - ), + executor: driver.executor, + transaction: (client, depth, fn) => driver.transaction(client, depth, fn), }, ) .withPlugin(new CasePlugin()) diff --git a/tests/scenarios/sqlite-hidden-columns/app.ts b/tests/scenarios/sqlite-hidden-columns/app.ts new file mode 100644 index 00000000..cbcaa705 --- /dev/null +++ b/tests/scenarios/sqlite-hidden-columns/app.ts @@ -0,0 +1,26 @@ +import { DatabaseSync } from "node:sqlite"; +import type { SchemaOf } from "@typesql/core"; +import { createSql } from "@typesql/sqlite"; +import type { DB } from "./db.js"; +import type { DB as Introspected } from "./introspected.js"; +import type docs from "./migrations/001-docs.js"; + +// An FTS5 table's hidden columns (`docs`, `rank`) are left out of an INSERT +// without a column list, so every schema source gives them a default. +export type Migrated = SchemaOf["docs"]["docs"]["hasDefault"]; +export type Generated = DB["docs"]["docs"]["hasDefault"]; +export type FromIntrospection = Introspected["docs"]["docs"]["hasDefault"]; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +const sql = createSql(db); +await sql( + "INSERT INTO docs (title, body) VALUES ($1, $2)", + "SQL", + "typed queries", +).execute(); +export const found = await sql( + "SELECT title FROM docs WHERE docs MATCH $1", + "typed", +).execute(); +console.log(found); +db.close(); diff --git a/tests/scenarios/sqlite-hidden-columns/expected.snap b/tests/scenarios/sqlite-hidden-columns/expected.snap new file mode 100644 index 00000000..bc83c344 --- /dev/null +++ b/tests/scenarios/sqlite-hidden-columns/expected.snap @@ -0,0 +1,65 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-hidden-columns > scenario 1`] = ` +{ + "artifacts": { + "db.ts": "// Generated by \`typesql migrate generate\` from 001-docs.ts. Do not edit. +import type { Col } from "@typesql/core/sqlite"; + +export interface DB { + docs: { + body: Col<"any", { nullable: true; position: 1 }>; + docs: Col<"any", { nullable: true; hasDefault: true; hidden: true }>; + rank: Col<"real", { nullable: true; hasDefault: true; hidden: true }>; + title: Col<"any", { nullable: true; position: 0 }>; + }; +} +", + "introspected.ts": "// Generated by \`typesql introspect\` from SQLite 3.53.4. Do not edit. +import type { Col } from "@typesql/core/sqlite"; + +export interface DB { + docs: { + title: Col<"any", { nullable: true; position: 0 }>; + body: Col<"any", { nullable: true; position: 1 }>; + docs: Col<"any", { nullable: true; hasDefault: true; hidden: true }>; + rank: Col<"any", { nullable: true; hasDefault: true; hidden: true }>; + }; + schema_migrations: { + name: Col<"text", { position: 0 }>; + hash: Col<"text", { position: 1 }>; + applied_at: Col<"text", { hasDefault: true; position: 2 }>; + }; +} +", + }, + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "FromIntrospection": "true", + "Generated": "true", + "Migrated": "true", + "found": "({ title: unknown })[]", + }, + }, + "generate": { + "status": 0, + "stderr": "typesql introspect: wrote introspected.ts +", + "stdout": "typesql migrate: applied 001-docs.ts +typesql migrate: generated /db.ts +", + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[ [Object: null prototype] { title: 'SQL' } ] +", + }, +} +`; diff --git a/tests/scenarios/sqlite-hidden-columns/migrations/001-docs.ts b/tests/scenarios/sqlite-hidden-columns/migrations/001-docs.ts new file mode 100644 index 00000000..62e96212 --- /dev/null +++ b/tests/scenarios/sqlite-hidden-columns/migrations/001-docs.ts @@ -0,0 +1,7 @@ +import { migration } from "@typesql/core/migration"; +export default migration("sqlite", { + up: async (db) => db.ddl("CREATE VIRTUAL TABLE docs USING fts5(title, body)"), + down: async (db) => { + await db.ddl("DROP TABLE docs"); + }, +}); diff --git a/tests/scenarios/sqlite-hidden-columns/package.json b/tests/scenarios/sqlite-hidden-columns/package.json new file mode 100644 index 00000000..b4b08146 --- /dev/null +++ b/tests/scenarios/sqlite-hidden-columns/package.json @@ -0,0 +1,25 @@ +{ + "name": "sqlite-hidden-columns", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js", + "generate": "typesql migrate latest && typesql introspect --dialect sqlite --db \"$SQLITE_PATH\" --out introspected.ts" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/cli": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite", + "artifacts": [ + "db.ts", + "introspected.ts" + ] + } +} diff --git a/tests/scenarios/sqlite-hidden-columns/typesql.config.ts b/tests/scenarios/sqlite-hidden-columns/typesql.config.ts new file mode 100644 index 00000000..57f6b6f2 --- /dev/null +++ b/tests/scenarios/sqlite-hidden-columns/typesql.config.ts @@ -0,0 +1,6 @@ +import { createMigrator } from "@typesql/core/migration"; +export default createMigrator({ + dialect: "sqlite", + db: process.env.SQLITE_PATH, + migrationsDir: "./migrations", +}); diff --git a/tests/scenarios/sqlite-insert-column-order/app.ts b/tests/scenarios/sqlite-insert-column-order/app.ts new file mode 100644 index 00000000..7a4c600d --- /dev/null +++ b/tests/scenarios/sqlite-insert-column-order/app.ts @@ -0,0 +1,80 @@ +import { DatabaseSync } from "node:sqlite"; +import { + migration, + type SchemaOf, + type SqliteMigration, +} from "@typesql/core/migration"; +import type { Col } from "@typesql/core/sqlite"; +import { createSql } from "@typesql/sqlite"; + +// `items` is written by hand without positions, so its column order is +// unknown; `placed` has the positions `typesql introspect` writes. +interface DB { + items: { + sku: Col<"integer">; + name: Col<"text">; + note: Col<"text", { nullable: true }>; + }; + placed: { + sku: Col<"integer", { position: 0 }>; + name: Col<"text", { position: 1 }>; + note: Col<"text", { nullable: true; position: 2 }>; + }; +} +const databasePath = process.env.SQLITE_PATH; +if (!databasePath) throw new Error("Missing SQLITE_PATH"); +const db = new DatabaseSync(databasePath); +db.exec(`CREATE TABLE items (sku integer PRIMARY KEY, name text NOT NULL, note text); + CREATE TABLE placed (sku integer PRIMARY KEY, name text NOT NULL, note text);`); +const sql = createSql(db); + +const unordered = sql.prepare("INSERT INTO items VALUES (1, $1, $2)"); +export type UnorderedParams = Parameters; +await unordered("Ada", null).execute(); + +const rows = sql.prepare("INSERT INTO items VALUES (2, $1, $2), (3, $3, $4)"); +export type RowsParams = Parameters; +await rows("Grace", "SQL", "Lin", null).execute(); + +const listed = sql.prepare( + "INSERT INTO items (note, sku, name) VALUES ($1, $2, $3)", +); +export type ListedParams = Parameters; +await listed(null, 4, "Ken").execute(); + +const positioned = sql.prepare("INSERT INTO placed VALUES ($1, $2, $3)"); +export type PlacedParams = Parameters; +await positioned(1, "Ada", null).execute(); + +const baseline = { + up: async (db: SqliteMigration) => db, + down: async () => {}, +}; +const tagged = migration(baseline, { + up: async (db) => + db.ddl( + "ALTER TABLE items ADD COLUMN tag text; ALTER TABLE placed ADD COLUMN tag text", + ), + down: async () => {}, +}); +db.exec( + "ALTER TABLE items ADD COLUMN tag text; ALTER TABLE placed ADD COLUMN tag text", +); +const altered = createSql>(db); + +const added = altered.prepare("INSERT INTO items VALUES (5, $1, $2, $3)"); +export type AddedParams = Parameters; +await added("Edsger", null, "new").execute(); + +const appended = altered.prepare("INSERT INTO placed VALUES ($1, $2, $3, $4)"); +export type AppendedParams = Parameters; +await appended(2, "Grace", "SQL", "new").execute(); + +export const items = await altered( + "SELECT sku, name, note, tag FROM items ORDER BY sku", +).execute(); +export const placed = await altered( + "SELECT sku, name, note, tag FROM placed ORDER BY sku", +).execute(); +console.log({ items, placed }); +db.close(); diff --git a/tests/scenarios/sqlite-insert-column-order/expected.snap b/tests/scenarios/sqlite-insert-column-order/expected.snap new file mode 100644 index 00000000..d4b1b71c --- /dev/null +++ b/tests/scenarios/sqlite-insert-column-order/expected.snap @@ -0,0 +1,77 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-insert-column-order > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "AddedParams": "[unknown, unknown, unknown]", + "AppendedParams": "[number | bigint, string, null | string, null | string]", + "ListedParams": "[null | string, number | bigint, string]", + "PlacedParams": "[number | bigint, string, null | string]", + "RowsParams": "[unknown, unknown, unknown, unknown]", + "UnorderedParams": "[unknown, unknown]", + "items": "({ name: string; note: null | string; sku: number; tag: null | string })[]", + "placed": "({ name: string; note: null | string; sku: number; tag: null | string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + items: [ + [Object: null prototype] { + sku: 1, + name: 'Ada', + note: null, + tag: null + }, + [Object: null prototype] { + sku: 2, + name: 'Grace', + note: 'SQL', + tag: null + }, + [Object: null prototype] { + sku: 3, + name: 'Lin', + note: null, + tag: null + }, + [Object: null prototype] { + sku: 4, + name: 'Ken', + note: null, + tag: null + }, + [Object: null prototype] { + sku: 5, + name: 'Edsger', + note: null, + tag: 'new' + } + ], + placed: [ + [Object: null prototype] { + sku: 1, + name: 'Ada', + note: null, + tag: null + }, + [Object: null prototype] { + sku: 2, + name: 'Grace', + note: 'SQL', + tag: 'new' + } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/sqlite-insert-column-order/package.json b/tests/scenarios/sqlite-insert-column-order/package.json new file mode 100644 index 00000000..868e4381 --- /dev/null +++ b/tests/scenarios/sqlite-insert-column-order/package.json @@ -0,0 +1,19 @@ +{ + "name": "sqlite-insert-column-order", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/sqlite-json-type-map/app.ts b/tests/scenarios/sqlite-json-type-map/app.ts new file mode 100644 index 00000000..a6320121 --- /dev/null +++ b/tests/scenarios/sqlite-json-type-map/app.ts @@ -0,0 +1,22 @@ +import { createSql } from "@typesql/core"; +import { type DefaultTypeMap, type Json, sqlite } from "@typesql/core/sqlite"; + +interface DB { + posts: { + meta: Json<{ theme: string }>; + extra: Json<{ tags: string[] }, { nullable: true }>; + }; +} +// SQLite drivers return JSON as text, which the default map says. +const text = createSql(sqlite)("SELECT meta, extra FROM posts"); +export type Text = Awaited>; + +// An executor that returns JSON parsed says so in its map; a typed JSON +// column then reads as its type. +interface ParsedJsonTypeMap extends Omit { + json: unknown; +} +const parsed = createSql(sqlite)( + "SELECT meta, extra FROM posts", +); +export type Parsed = Awaited>; diff --git a/tests/scenarios/sqlite-json-type-map/expected.snap b/tests/scenarios/sqlite-json-type-map/expected.snap new file mode 100644 index 00000000..43a09123 --- /dev/null +++ b/tests/scenarios/sqlite-json-type-map/expected.snap @@ -0,0 +1,18 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-json-type-map > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "Parsed": "({ extra: null | { tags: (string)[] }; meta: { theme: string } })[]", + "Text": "({ extra: null | string; meta: string })[]", + }, + }, +} +`; diff --git a/tests/scenarios/sqlite-json-type-map/package.json b/tests/scenarios/sqlite-json-type-map/package.json new file mode 100644 index 00000000..243fddf9 --- /dev/null +++ b/tests/scenarios/sqlite-json-type-map/package.json @@ -0,0 +1,12 @@ +{ + "name": "sqlite-json-type-map", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + } +} diff --git a/tests/scenarios/sqlite-json-write-rejection/expected.snap b/tests/scenarios/sqlite-json-write-rejection/expected.snap index 773b56d6..84565a7e 100644 --- a/tests/scenarios/sqlite-json-write-rejection/expected.snap +++ b/tests/scenarios/sqlite-json-write-rejection/expected.snap @@ -5,29 +5,6 @@ exports[`sqlite-json-write-rejection > scenario 1`] = ` "compile": { "status": 1, "stderr": "", - "stdout": "app.ts(15,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ<0ok=t,0views=qwrong~>0\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'. -app.ts(16,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ<0ok=t>0\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'. -app.ts(18,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t,0views=qwrong~>0"]>}\`'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'. -app.ts(20,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t>0"]>}\`'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'. -app.ts(22,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t,0views=qwrong~>0", "Jqdraft~"]>}\`'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'. -app.ts(25,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t>0", "Jqdraft~"]>}\`'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'. -app.ts(28,2): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & "INSERT INTO t (doc, label) VALUES (\\0PJ<0ok=t>0\\0, \\0PJqdraft~\\0)"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'. -app.ts(30,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5> & "UPDATE t SET optional=\\0PJ<0ok=t,0views=qwrong~>0\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5>'. -app.ts(31,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5> & "UPDATE t SET optional=\\0PJ<0ok=t>0\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5>'. -app.ts(32,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() null value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ0\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() null value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'. -app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() "wrong" value cannot be used as \${"json" & { readonly shape: TsShape<"draft" | "published">; }} NOT NULL (expected "draft" | "published")\`, 5> & "UPDATE t SET label=\\0PJqwrong~\\0"'. - Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() "wrong" value cannot be used as \${"json" & { readonly shape: TsShape<"draft" | "published">; }} NOT NULL (expected "draft" | "published")\`, 5>'. -", }, "compiler": { "diagnostics": [ @@ -39,17 +16,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json({ views: "wrong", ok: true })}\`", - "file": "app.ts", - "line": 15, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], "expression": "\`UPDATE t SET doc=\${sql.json({ views: "wrong", ok: true })}\`", "file": "app.ts", "line": 15, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ<0ok=t,0views=qwrong~>0\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ<0ok=t,0views=qwrong~>0\\0"'.", }, { "category": 1, @@ -59,17 +32,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json({ ok: true })}\`", - "file": "app.ts", - "line": 16, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], "expression": "\`UPDATE t SET doc=\${sql.json({ ok: true })}\`", "file": "app.ts", "line": 16, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ<0ok=t>0\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ<0ok=t>0\\0"'.", }, { "category": 1, @@ -79,17 +48,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ views: "wrong", ok: true }) })}\`", - "file": "app.ts", - "line": 18, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ views: "wrong", ok: true }) })}\`", "file": "app.ts", "line": 18, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t,0views=qwrong~>0"]>}\`'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t,0views=qwrong~>0"]>}\`'.", }, { "category": 1, @@ -99,17 +64,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ ok: true }) })}\`", - "file": "app.ts", - "line": 20, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], "expression": "\`UPDATE t SET \${sql.set("t", { doc: sql.json({ ok: true }) })}\`", "file": "app.ts", "line": 20, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t>0"]>}\`'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5> & \`UPDATE t SET \${Fragment<"\\"doc\\" = $1", readonly unknown[], ["J<0ok=t>0"]>}\`'.", }, { "category": 1, @@ -119,17 +80,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ views: "wrong", ok: true }), label: sql.json("draft") }])}\`", - "file": "app.ts", - "line": 22, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ views: "wrong", ok: true }), label: sql.json("draft") }])}\`", "file": "app.ts", "line": 22, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t,0views=qwrong~>0", "Jqdraft~"]>}\`'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t,0views=qwrong~>0", "Jqdraft~"]>}\`'.", }, { "category": 1, @@ -139,17 +96,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ ok: true }), label: sql.json("draft") }])}\`", - "file": "app.ts", - "line": 25, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], "expression": "\`INSERT INTO t \${sql.values("t", [{ doc: sql.json({ ok: true }), label: sql.json("draft") }])}\`", "file": "app.ts", "line": 25, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t>0", "Jqdraft~"]>}\`'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10> & \`INSERT INTO t \${Fragment<"(\\"doc\\", \\"label\\") VALUES ($1, $2)", readonly unknown[], ["J<0ok=t>0", "Jqdraft~"]>}\`'.", }, { "category": 1, @@ -159,17 +112,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 2, - "expression": "\`INSERT INTO t (doc, label) VALUES (\${sql.json({ ok: true })}, \${sql.json("draft")})\`", - "file": "app.ts", - "line": 28, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10>'.", }, ], "expression": "\`INSERT INTO t (doc, label) VALUES (\${sql.json({ ok: true })}, \${sql.json("draft")})\`", "file": "app.ts", "line": 28, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 10> & "INSERT INTO t (doc, label) VALUES (\\0PJ<0ok=t>0\\0, \\0PJqdraft~\\0)"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 10> & "INSERT INTO t (doc, label) VALUES (\\0PJ<0ok=t>0\\0, \\0PJqdraft~\\0)"'.", }, { "category": 1, @@ -179,17 +128,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET optional=\${sql.json({ views: "wrong", ok: true })}\`", - "file": "app.ts", - "line": 30, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'.", }, ], "expression": "\`UPDATE t SET optional=\${sql.json({ views: "wrong", ok: true })}\`", "file": "app.ts", "line": 30, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true; views: "wrong" } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5> & "UPDATE t SET optional=\\0PJ<0ok=t,0views=qwrong~>0\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true; views: \\"wrong\\" } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5> & "UPDATE t SET optional=\\0PJ<0ok=t,0views=qwrong~>0\\0"'.", }, { "category": 1, @@ -199,17 +144,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET optional=\${sql.json({ ok: true })}\`", - "file": "app.ts", - "line": 31, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5>'.", }, ], "expression": "\`UPDATE t SET optional=\${sql.json({ ok: true })}\`", "file": "app.ts", "line": 31, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() { ok: true } value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} (expected { ok: boolean; views: number } | null)\`, 5> & "UPDATE t SET optional=\\0PJ<0ok=t>0\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() { ok: true } value cannot be used as json (expected { ok: boolean; views: number } | null)", 5> & "UPDATE t SET optional=\\0PJ<0ok=t>0\\0"'.", }, { "category": 1, @@ -219,17 +160,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET doc=\${sql.json(null)}\`", - "file": "app.ts", - "line": 32, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() null value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() null value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5>'.", }, ], "expression": "\`UPDATE t SET doc=\${sql.json(null)}\`", "file": "app.ts", "line": 32, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() null value cannot be used as \${"json" & { readonly shape: TsShape<{ views: number; ok: boolean; }>; }} NOT NULL (expected { ok: boolean; views: number })\`, 5> & "UPDATE t SET doc=\\0PJ0\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() null value cannot be used as json NOT NULL (expected { ok: boolean; views: number })", 5> & "UPDATE t SET doc=\\0PJ0\\0"'.", }, { "category": 1, @@ -239,17 +176,13 @@ app.ts(33,5): error TS2345: Argument of type 'string' is not assignable to param { "category": 1, "code": 2322, - "column": 5, - "expression": "\`UPDATE t SET label=\${sql.json("wrong")}\`", - "file": "app.ts", - "line": 33, - "message": "Type 'string' is not assignable to type 'SqlError<\`parameter $1: a sql.json() "wrong" value cannot be used as \${"json" & { readonly shape: TsShape<"draft" | "published">; }} NOT NULL (expected "draft" | "published")\`, 5>'.", + "message": "Type 'string' is not assignable to type 'SqlError<"parameter $1: a sql.json() \\"wrong\\" value cannot be used as json NOT NULL (expected \\"draft\\" | \\"published\\")", 5>'.", }, ], "expression": "\`UPDATE t SET label=\${sql.json("wrong")}\`", "file": "app.ts", "line": 33, - "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<\`parameter $1: a sql.json() "wrong" value cannot be used as \${"json" & { readonly shape: TsShape<"draft" | "published">; }} NOT NULL (expected "draft" | "published")\`, 5> & "UPDATE t SET label=\\0PJqwrong~\\0"'.", + "message": "Argument of type 'string' is not assignable to parameter of type 'SqlError<"parameter $1: a sql.json() \\"wrong\\" value cannot be used as json NOT NULL (expected \\"draft\\" | \\"published\\")", 5> & "UPDATE t SET label=\\0PJqwrong~\\0"'.", }, ], "types": {}, diff --git a/tests/scenarios/sqlite-json-write-rejection/package.json b/tests/scenarios/sqlite-json-write-rejection/package.json index 6cf432ce..2890c650 100644 --- a/tests/scenarios/sqlite-json-write-rejection/package.json +++ b/tests/scenarios/sqlite-json-write-rejection/package.json @@ -2,9 +2,6 @@ "name": "sqlite-json-write-rejection", "private": true, "type": "module", - "scripts": { - "compile": "tsc -p tsconfig.json --pretty false" - }, "dependencies": { "@typesql/core": "workspace:*", "@typesql/sqlite": "workspace:*" diff --git a/tests/scenarios/sqlite-json-writes/package.json b/tests/scenarios/sqlite-json-writes/package.json index 16b1cd98..aafcb6f0 100644 --- a/tests/scenarios/sqlite-json-writes/package.json +++ b/tests/scenarios/sqlite-json-writes/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sqlite-negated-zero/app.ts b/tests/scenarios/sqlite-negated-zero/app.ts new file mode 100644 index 00000000..dd601670 --- /dev/null +++ b/tests/scenarios/sqlite-negated-zero/app.ts @@ -0,0 +1,21 @@ +import { DatabaseSync } from "node:sqlite"; +import type { Col } from "@typesql/core/sqlite"; +import { createSql } from "@typesql/sqlite"; + +interface DB { + items: { id: Col<"integer"> }; +} + +const db = new DatabaseSync(process.env.SQLITE_PATH ?? ":memory:"); +db.exec( + "CREATE TABLE items (id integer NOT NULL); INSERT INTO items VALUES (6)", +); +const sql = createSql(db); +export const divided = await sql(` + SELECT id / - -0 AS negatedZero, id % - -0 AS negatedZeroRemainder, + id / -0 AS negativeZero, id / - -2 AS negatedTwo, id / +2 AS plusTwo, + id / +0 AS plusZero + FROM items +`).execute(); +console.dir({ divided }, { depth: null }); +db.close(); diff --git a/tests/scenarios/sqlite-negated-zero/expected.snap b/tests/scenarios/sqlite-negated-zero/expected.snap new file mode 100644 index 00000000..557fdcab --- /dev/null +++ b/tests/scenarios/sqlite-negated-zero/expected.snap @@ -0,0 +1,34 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-negated-zero > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "divided": "({ negatedTwo: number; negatedZero: null | number; negatedZeroRemainder: null | number; negativeZero: null | number; plusTwo: number; plusZero: null | number })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + divided: [ + [Object: null prototype] { + negatedZero: null, + negatedZeroRemainder: null, + negativeZero: null, + negatedTwo: 3, + plusTwo: 3, + plusZero: null + } + ] +} +", + }, +} +`; diff --git a/tests/scenarios/sqlite-negated-zero/package.json b/tests/scenarios/sqlite-negated-zero/package.json new file mode 100644 index 00000000..9aa64c17 --- /dev/null +++ b/tests/scenarios/sqlite-negated-zero/package.json @@ -0,0 +1,19 @@ +{ + "name": "sqlite-negated-zero", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/sqlite-parameter-inference/package.json b/tests/scenarios/sqlite-parameter-inference/package.json index fca6acf9..2a2f30c5 100644 --- a/tests/scenarios/sqlite-parameter-inference/package.json +++ b/tests/scenarios/sqlite-parameter-inference/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "scripts": { - "compile": "tsc -p tsconfig.json --pretty false", "start": "node dist/app.js" }, "dependencies": { diff --git a/tests/scenarios/sqlite-quoted-text/app.ts b/tests/scenarios/sqlite-quoted-text/app.ts new file mode 100644 index 00000000..78425c1f --- /dev/null +++ b/tests/scenarios/sqlite-quoted-text/app.ts @@ -0,0 +1,56 @@ +import { DatabaseSync } from "node:sqlite"; +import { createSql } from "@typesql/sqlite"; + +const db = new DatabaseSync(process.env.SQLITE_PATH!); +const sql = createSql<{}>(db); + +// SQLite text: names quoted with backticks or brackets hold no parameters, +// and a block comment ends at the first `*/`, as SQLite doesn't nest them. +const columns = sql.raw("1 AS `a:b`, 2 AS [c:d], 3 AS [$1]"); +export const row = await sql + .as<{ + first: number; + "a:b": number; + "c:d": number; + $1: number; + last: number; + }>(`SELECT $1 AS first, ${columns} /* /* */, ${sql.param(20)} AS last`, 10) + .executeTakeFirstOrThrow(); +console.log(row); + +// sql() reads its text, and that of its fragments, by the same rules. +export const backticks = await sql("SELECT 1 AS `a:b`, :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +export const brackets = await sql( + "SELECT 2 AS [c:d], 3 AS [it's], :y + 0 AS y", + { + y: 2, + }, +).executeTakeFirstOrThrow(); +export const blockComment = await sql("SELECT /* /* */ :y + 0 AS y", { + y: 2, +}).executeTakeFirstOrThrow(); +const dollar = sql.fragment("1 AS [$2], $1 + 0 AS a", 10); +export const quotedDollar = await sql( + `SELECT ${dollar}`, +).executeTakeFirstOrThrow(); +const pair = sql.combine([ + sql.fragment("$1 + 0 AS [$1 a]", 1), + sql.fragment("$1 + 0 AS [$1 b]", 2), +]); +export const combined = await sql(`SELECT ${pair}`).executeTakeFirstOrThrow(); +// A JSON array's parameter, whose type TypeSQL writes with brackets, is no +// bracket-quoted name. +export const jsonArray = await sql( + `SELECT json_array_length(${sql.json([1, 2])}) AS n, 3 AS [c:d]`, +).executeTakeFirstOrThrow(); +console.log({ + backticks, + brackets, + blockComment, + quotedDollar, + combined, + jsonArray, +}); +db.close(); diff --git a/tests/scenarios/sqlite-quoted-text/expected.snap b/tests/scenarios/sqlite-quoted-text/expected.snap new file mode 100644 index 00000000..0ee3af41 --- /dev/null +++ b/tests/scenarios/sqlite-quoted-text/expected.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-quoted-text > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "backticks": "{ "a:b": number; y: null | number }", + "blockComment": "{ y: null | number }", + "brackets": "{ "c:d": number; "it's": number; y: null | number }", + "combined": "{ "$1 a": null | number; "$1 b": null | number }", + "jsonArray": "{ "c:d": number; n: null | number }", + "quotedDollar": "{ $2: number; a: null | number }", + "row": "{ first: number; "a:b": number; "c:d": number; $1: number; last: number }", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "[Object: null prototype] { + first: 10, + 'a:b': 1, + 'c:d': 2, + '$1': 3, + last: 20 +} +{ + backticks: [Object: null prototype] { 'a:b': 1, y: 2 }, + brackets: [Object: null prototype] { 'c:d': 2, "it's": 3, y: 2 }, + blockComment: [Object: null prototype] { y: 2 }, + quotedDollar: [Object: null prototype] { '$2': 1, a: 10 }, + combined: [Object: null prototype] { '$1 a': 1, '$1 b': 2 }, + jsonArray: [Object: null prototype] { n: 2, 'c:d': 3 } +} +", + }, +} +`; diff --git a/tests/scenarios/sqlite-quoted-text/package.json b/tests/scenarios/sqlite-quoted-text/package.json new file mode 100644 index 00000000..07765fc2 --- /dev/null +++ b/tests/scenarios/sqlite-quoted-text/package.json @@ -0,0 +1,19 @@ +{ + "name": "sqlite-quoted-text", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/scenarios/sqlite-values-columns/app.ts b/tests/scenarios/sqlite-values-columns/app.ts new file mode 100644 index 00000000..b2a7a2cb --- /dev/null +++ b/tests/scenarios/sqlite-values-columns/app.ts @@ -0,0 +1,38 @@ +import { createSql } from "@typesql/core"; +import { type Col, sqlite } from "@typesql/core/sqlite"; + +interface DB { + users: { + id: Col<"integer">; + name: Col<"text", { hasDefault: true }>; + bio: Col<"text", { nullable: true }>; + }; +} +const sql = createSql(sqlite); +declare const bio: string | undefined; +declare const rows: { id: number; name?: string }[]; + +// SQLite has no DEFAULT in a VALUES row, so every row gives the same columns. +sql( + `INSERT INTO users ${sql.values("users", [ + { id: 1, name: "a" }, + { id: 2, name: "b" }, + ])}`, +); +sql(`INSERT INTO users ${sql.values("users", [{ id: 1, bio }])}`); +sql( + `INSERT INTO users ${sql.values("users", [ + { id: 1, bio: undefined }, + { id: 2, bio: undefined }, + ])}`, +); +sql( + `INSERT INTO users ${sql.values("users", [{ id: 1, name: "a" }, { id: 2 }])}`, +); +sql( + `INSERT INTO users ${sql.values([ + { id: 1, bio }, + { id: 2, bio: "b" }, + ])}`, +); +sql(`INSERT INTO users ${sql.values("users", rows)}`); diff --git a/tests/scenarios/sqlite-values-columns/expected.snap b/tests/scenarios/sqlite-values-columns/expected.snap new file mode 100644 index 00000000..0e60c5be --- /dev/null +++ b/tests/scenarios/sqlite-values-columns/expected.snap @@ -0,0 +1,88 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-values-columns > scenario 1`] = ` +{ + "compile": { + "status": 1, + "stderr": "", + }, + "compiler": { + "diagnostics": [ + { + "category": 1, + "code": 2322, + "column": 66, + "details": [ + { + "category": 1, + "code": 2741, + "message": "Property 'name' is missing in type '{ id: 2; }' but required in type '{ readonly name: {} | null; }'.", + }, + ], + "expression": "{ id: 2 }", + "file": "app.ts", + "line": 30, + "message": "Type '{ id: 2; }' is not assignable to type '{ readonly id: 2; } & { readonly name: {} | null; } & {}'.", + }, + { + "category": 1, + "code": 2322, + "column": 12, + "details": [ + { + "category": 1, + "code": 2322, + "message": "Type 'undefined' is not assignable to type 'string'.", + }, + ], + "expression": "bio", + "file": "app.ts", + "line": 34, + "message": "Type 'string | undefined' is not assignable to type 'string'.", + "related": [ + { + "category": 3, + "code": 6500, + "column": 12, + "expression": "bio", + "file": "app.ts", + "line": 34, + "message": "The expected type comes from property 'bio' which is declared here on type '{ readonly id: 1; readonly bio: string | undefined; } & { readonly bio: {} | null; }'", + }, + ], + }, + { + "category": 1, + "code": 2345, + "column": 46, + "details": [ + { + "category": 1, + "code": 2322, + "details": [ + { + "category": 1, + "code": 2322, + "details": [ + { + "category": 1, + "code": 2327, + "message": "Property 'name' is optional in type '{ id: number; name?: string; }' but required in type '{ readonly name: {} | null; }'.", + }, + ], + "message": "Type '{ id: number; name?: string; }' is not assignable to type '{ readonly name: {} | null; }'.", + }, + ], + "message": "Type '{ id: number; name?: string; }[]' is not assignable to type 'readonly { readonly name: {} | null; }[]'.", + }, + ], + "expression": "rows", + "file": "app.ts", + "line": 38, + "message": "Argument of type '{ id: number; name?: string; }[]' is not assignable to parameter of type '{ id: number; name?: string; }[] & readonly { readonly name: {} | null; }[] & readonly ({ id: number; name?: string; } & {})[]'.", + }, + ], + "types": {}, + }, +} +`; diff --git a/tests/scenarios/sqlite-values-columns/package.json b/tests/scenarios/sqlite-values-columns/package.json new file mode 100644 index 00000000..7e69668b --- /dev/null +++ b/tests/scenarios/sqlite-values-columns/package.json @@ -0,0 +1,18 @@ +{ + "name": "sqlite-values-columns", + "private": true, + "type": "module", + "dependencies": { + "@typesql/core": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "failure": { + "stage": "compile", + "exitCode": 1 + } + } +} diff --git a/tests/scenarios/sqlite-where-narrowing/app.ts b/tests/scenarios/sqlite-where-narrowing/app.ts new file mode 100644 index 00000000..6d5c3b90 --- /dev/null +++ b/tests/scenarios/sqlite-where-narrowing/app.ts @@ -0,0 +1,50 @@ +import { DatabaseSync } from "node:sqlite"; +import type { Col } from "@typesql/core/sqlite"; +import { createSql } from "@typesql/sqlite"; + +interface DB { + users: { + id: Col<"integer">; + name: Col<"text">; + bio: Col<"text", { nullable: true }>; + }; + posts: { + id: Col<"integer">; + author_id: Col<"integer">; + title: Col<"text">; + body: Col<"text", { nullable: true }>; + }; +} + +const db = new DatabaseSync(process.env.SQLITE_PATH ?? ":memory:"); +db.exec(`CREATE TABLE users (id integer NOT NULL, name text NOT NULL, bio text); + CREATE TABLE posts (id integer NOT NULL, author_id integer NOT NULL, title text NOT NULL, body text); + INSERT INTO users VALUES (1, 'Ada', NULL), (2, 'Grace', 'SQL'), (3, 'Lin', 'Lisp'); + INSERT INTO posts VALUES (1, 1, 'SQL', NULL)`); +const sql = createSql(db); +export const notIn = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN (SELECT title FROM posts WHERE id > 1) ORDER BY id", +).execute(); +export const notInEmpty = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN () ORDER BY id", +).execute(); +export const notInList = await sql( + "SELECT id, bio FROM users WHERE bio NOT IN ('SQL') ORDER BY id", +).execute(); +export const joined = await sql(` + SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id ORDER BY u.id +`).execute(); +export const matched = await sql(` + SELECT u.name, p.id, p.title, p.body FROM users u + LEFT JOIN posts p ON p.author_id = u.id WHERE p.title = 'SQL' ORDER BY u.id +`).execute(); +export const right = await sql(` + SELECT u.id, u.name, p.title FROM posts p + RIGHT JOIN users u ON p.author_id = u.id WHERE p.id IS NOT NULL +`).execute(); +console.dir( + { notIn, notInEmpty, notInList, joined, matched, right }, + { depth: null }, +); +db.close(); diff --git a/tests/scenarios/sqlite-where-narrowing/expected.snap b/tests/scenarios/sqlite-where-narrowing/expected.snap new file mode 100644 index 00000000..817c49be --- /dev/null +++ b/tests/scenarios/sqlite-where-narrowing/expected.snap @@ -0,0 +1,69 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sqlite-where-narrowing > scenario 1`] = ` +{ + "compile": { + "status": 0, + "stderr": "", + "stdout": "", + }, + "compiler": { + "diagnostics": [], + "types": { + "joined": "({ body: null | string; id: null | number; name: string; title: null | string })[]", + "matched": "({ body: null | string; id: number; name: string; title: string })[]", + "notIn": "({ bio: null | string; id: number })[]", + "notInEmpty": "({ bio: null | string; id: number })[]", + "notInList": "({ bio: string; id: number })[]", + "right": "({ id: number; name: string; title: string })[]", + }, + }, + "start": { + "status": 0, + "stderr": "", + "stdout": "{ + notIn: [ + [Object: null prototype] { id: 1, bio: null }, + [Object: null prototype] { id: 2, bio: 'SQL' }, + [Object: null prototype] { id: 3, bio: 'Lisp' } + ], + notInEmpty: [ + [Object: null prototype] { id: 1, bio: null }, + [Object: null prototype] { id: 2, bio: 'SQL' }, + [Object: null prototype] { id: 3, bio: 'Lisp' } + ], + notInList: [ [Object: null prototype] { id: 3, bio: 'Lisp' } ], + joined: [ + [Object: null prototype] { + name: 'Ada', + id: 1, + title: 'SQL', + body: null + }, + [Object: null prototype] { + name: 'Grace', + id: null, + title: null, + body: null + }, + [Object: null prototype] { + name: 'Lin', + id: null, + title: null, + body: null + } + ], + matched: [ + [Object: null prototype] { + name: 'Ada', + id: 1, + title: 'SQL', + body: null + } + ], + right: [ [Object: null prototype] { id: 1, name: 'Ada', title: 'SQL' } ] +} +", + }, +} +`; diff --git a/tests/scenarios/sqlite-where-narrowing/package.json b/tests/scenarios/sqlite-where-narrowing/package.json new file mode 100644 index 00000000..17ecda44 --- /dev/null +++ b/tests/scenarios/sqlite-where-narrowing/package.json @@ -0,0 +1,19 @@ +{ + "name": "sqlite-where-narrowing", + "private": true, + "type": "module", + "scripts": { + "start": "node dist/app.js" + }, + "dependencies": { + "@typesql/core": "workspace:*", + "@typesql/sqlite": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + }, + "scenario": { + "database": "sqlite" + } +} diff --git a/tests/tsconfig.json b/tests/scenarios/tsconfig.base.json similarity index 72% rename from tests/tsconfig.json rename to tests/scenarios/tsconfig.base.json index fc3c21f1..5c21f74f 100644 --- a/tests/tsconfig.json +++ b/tests/scenarios/tsconfig.base.json @@ -9,17 +9,10 @@ "skipLibCheck": true, "noErrorTruncation": true, "noEmitOnError": true, - "noEmit": true, - "allowImportingTsExtensions": true, "outDir": "dist", "rootDir": ".", "types": ["node"] }, - "include": [ - "support/**/*.ts", - "scenario.test.ts", - "vitest.config.ts", - "run.ts" - ], + "include": ["**/*.ts"], "exclude": ["node_modules", "dist", ".scenario"] } diff --git a/tests/support/command.ts b/tests/support/command.ts index 9bbf6f1d..e16a9368 100644 --- a/tests/support/command.ts +++ b/tests/support/command.ts @@ -26,11 +26,17 @@ export interface Result { stdout: string; stderr: string; } +export interface Options { + env?: NodeJS.ProcessEnv; + /** Kills the command after this many milliseconds; a scenario's test timeout aborts it otherwise. */ + timeout?: number; + /** Directory to write the command's result to. */ + logs?: string; +} export async function command( cwd: string, argv: string[], - env: NodeJS.ProcessEnv = {}, - timeout = 120_000, + { env = {}, timeout, logs }: Options = {}, ): Promise { const signal = cancellation.getStore(); signal?.throwIfAborted(); @@ -70,7 +76,10 @@ export async function command( }; const abort = () => kill("test aborted"); signal?.addEventListener("abort", abort, { once: true }); - const timer = setTimeout(() => kill(`timeout after ${timeout}ms`), timeout); + const timer = + timeout === undefined + ? undefined + : setTimeout(() => kill(`timeout after ${timeout}ms`), timeout); child.stdout.on("data", (chunk) => { stdout += chunk; if (stdout.length > 10_000_000) kill("output exceeded 10MB"); @@ -91,19 +100,19 @@ export async function command( signal?.removeEventListener("abort", abort); const result = { command: argv, status: status ?? -1, stdout, stderr }; try { - await mkdir(join(cwd, ".scenario", "logs"), { recursive: true }); - await writeFile( - join( - cwd, - ".scenario", - "logs", - `${argv - .map((x) => x.replace(/[^a-z0-9-]/gi, "_")) - .join("-") - .slice(0, 140)}.json`, - ), - JSON.stringify(result, null, 2), - ); + if (logs) { + await mkdir(logs, { recursive: true }); + await writeFile( + join( + logs, + `${argv + .map((x) => x.replace(/[^a-z0-9-]/gi, "_")) + .join("-") + .slice(0, 140)}.json`, + ), + JSON.stringify(result, null, 2), + ); + } if (fault || exitSignal) reject( fault ?? @@ -116,13 +125,8 @@ export async function command( }); }); } -export async function required( - cwd: string, - argv: string[], - env: NodeJS.ProcessEnv = {}, - timeout?: number, -) { - const result = await command(cwd, argv, env, timeout); +export async function required(cwd: string, argv: string[], options?: Options) { + const result = await command(cwd, argv, options); if (result.status !== 0) throw new Error( `Infrastructure/prerequisite: ${argv.join(" ")} failed (${result.status})\n${result.stdout}${result.stderr}`, diff --git a/tests/support/database.ts b/tests/support/database.ts index 830590f0..7b9f72e0 100644 --- a/tests/support/database.ts +++ b/tests/support/database.ts @@ -1,147 +1,117 @@ import { randomUUID } from "node:crypto"; +import { join } from "node:path"; import mysql from "mysql2/promise"; import pg from "pg"; +import { SERVERS, type Server, serverUrl } from "../../tools/servers.ts"; +import type { Database } from "./types.ts"; -export type Database = "postgres" | "sqlite" | "mysql" | "mariadb"; - -const adminUrl = () => - process.env.TYPESQL_DATABASE_URL ?? - "postgres://postgres:typesql@127.0.0.1:55432/postgres"; -async function admin(fn: (client: pg.Client) => Promise) { - const client = new pg.Client({ - connectionString: adminUrl(), - connectionTimeoutMillis: 5000, - query_timeout: 10000, - }); - await client.connect(); - try { - return await fn(client); - } finally { - await client.end(); +/** Runs statements on the server's admin connection, returning the last one's rows. */ +async function admin(server: Server, statements: string[]): Promise { + let rows: unknown[] = []; + if (server === "postgres") { + const client = new pg.Client({ + connectionString: serverUrl(server), + connectionTimeoutMillis: 5000, + query_timeout: 10000, + }); + await client.connect(); + try { + for (const text of statements) rows = (await client.query(text)).rows; + } finally { + await client.end(); + } + return rows; } -} - -/** The MySQL-protocol servers: mysql2 connects to both with a `mysql://` URL. */ -const SERVERS = { - mysql: { - variable: "TYPESQL_MYSQL_URL", - url: "mysql://root:typesql@127.0.0.1:53306/mysql", - scheme: "mysql", - version: /^8\.4\./, - expected: "MySQL 8.4", - }, - mariadb: { - variable: "TYPESQL_MARIADB_URL", - url: "mysql://root:typesql@127.0.0.1:53307/mysql", - scheme: "mariadb", - version: /^11\.8\.\d+-MariaDB/, - expected: "MariaDB 11.8", - }, -} as const; -type Server = keyof typeof SERVERS; - -const serverUrl = (kind: Server) => - process.env[SERVERS[kind].variable] ?? SERVERS[kind].url; -async function mysqlAdmin( - kind: Server, - fn: (connection: mysql.Connection) => Promise, -) { const connection = await mysql.createConnection({ - uri: serverUrl(kind), + uri: serverUrl(server), connectTimeout: 5000, }); try { - return await fn(connection); + for (const text of statements) + [rows] = await connection.query(text); } finally { await connection.end(); } + return rows; } -export async function ready() { - try { - await admin(async (client) => { - const { rows } = await client.query("SHOW server_version"); - if (!rows[0].server_version.startsWith("18.6")) - throw new Error(`Expected PostgreSQL 18.6: ${rows[0].server_version}`); - }); - } catch (cause) { - throw new Error("Infrastructure: pinned PostgreSQL is not ready", { - cause, - }); - } - for (const kind of ["mysql", "mariadb"] as const) { - const server = SERVERS[kind]; - try { - await mysqlAdmin(kind, async (connection) => { - const [rows] = await connection.query( - "SELECT VERSION() AS version", +const checked = new Map>(); +/** Checks a server once per worker, when the first scenario using it runs. */ +function ready(server: Server) { + let check = checked.get(server); + if (!check) { + const { version: expected } = SERVERS[server]; + check = admin(server, ["SELECT version() AS version"]) + .then(([row]) => { + const version = String( + typeof row === "object" && row && "version" in row && row.version, + ); + // PostgreSQL prefixes its version with its name; MySQL and MariaDB do not. + const number = version.replace(/^PostgreSQL /, ""); + if ( + !new RegExp(`^${expected.replaceAll(".", "\\.")}(?!\\d)`).test(number) + ) + throw new Error(`Expected ${server} ${expected}: ${version}`); + }) + .catch((cause) => { + throw new Error( + `Infrastructure: pinned ${server} ${expected} is not ready`, + { cause }, ); - const version = String(rows[0]?.version); - if (!server.version.test(version)) - throw new Error(`Expected ${server.expected}: ${version}`); }); - } catch (cause) { - throw new Error( - `Infrastructure: pinned ${server.expected} is not ready`, - { - cause, - }, - ); - } + checked.set(server, check); } + return check; } -export async function allocate(kind: Database | undefined, directory: string) { - if (kind === "mysql" || kind === "mariadb") - return allocateMysql(kind, directory); - if (kind !== "postgres") + +export interface Allocation { + env: Record; + cleanup(): Promise; +} + +/** A database of the scenario's own: a fresh one on the server, or a SQLite file. */ +export async function allocate( + kind: Database | undefined, + directory: string, +): Promise { + if (kind === undefined) return { env: {}, cleanup: async () => {} }; + if (kind === "sqlite") return { - env: { SQLITE_PATH: joinSqlite(directory) }, + env: { SQLITE_PATH: join(directory, "app.db") }, cleanup: async () => {}, }; + await ready(kind); const name = `scenario_${randomUUID().replaceAll("-", "")}`; - await admin(async (client) => { - for (const statement of [ - `CREATE DATABASE "${name}" TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'`, - `ALTER DATABASE "${name}" SET timezone TO 'UTC'`, - `ALTER DATABASE "${name}" SET datestyle TO 'ISO, YMD'`, - ]) - await client.query(statement); - }); - const url = new URL(adminUrl()); - url.pathname = `/${name}`; - return { - env: { DATABASE_URL: url.toString(), SQLITE_PATH: joinSqlite(directory) }, - cleanup: () => - admin((client) => - client.query(`DROP DATABASE "${name}" WITH (FORCE)`).then(() => {}), - ), - }; -} -/** - * A database of its own on the MySQL or MariaDB server. Its URL carries the - * server's scheme (`mysql://` or `mariadb://`), which the drivers and the CLI - * accept. - */ -async function allocateMysql(kind: Server, directory: string) { - const name = `scenario_${randomUUID().replaceAll("-", "")}`; - await mysqlAdmin(kind, async (connection) => { - await connection.query( - `CREATE DATABASE \`${name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin`, - ); - }); + const statements = + kind === "postgres" + ? { + create: [ + `CREATE DATABASE "${name}" TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'`, + `ALTER DATABASE "${name}" SET timezone TO 'UTC'`, + `ALTER DATABASE "${name}" SET datestyle TO 'ISO, YMD'`, + ], + drop: `DROP DATABASE "${name}" WITH (FORCE)`, + } + : { + create: [ + `CREATE DATABASE \`${name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin`, + ], + drop: `DROP DATABASE \`${name}\``, + }; + await admin(kind, statements.create); const url = new URL(serverUrl(kind)); url.pathname = `/${name}`; return { + // A MySQL-protocol URL carries the server's scheme (`mysql://` or + // `mariadb://`), which the drivers and the CLI accept. env: { - DATABASE_URL: url - .toString() - .replace(/^mysql:/, `${SERVERS[kind].scheme}:`), - SQLITE_PATH: joinSqlite(directory), + DATABASE_URL: + kind === "postgres" + ? url.toString() + : url.toString().replace(/^mysql:/, `${kind}:`), + }, + cleanup: async () => { + await admin(kind, [statements.drop]); }, - cleanup: () => - mysqlAdmin(kind, async (connection) => { - await connection.query(`DROP DATABASE \`${name}\``); - }), }; } -const joinSqlite = (directory: string) => `${directory}/app.db`; diff --git a/tests/support/observe.ts b/tests/support/observe.ts index 969ee874..e333ce56 100644 --- a/tests/support/observe.ts +++ b/tests/support/observe.ts @@ -1,10 +1,13 @@ // Runs with the scenario's working directory and the workspace's pinned compiler. +// usage: node observe.ts import { readFileSync, writeFileSync } from "node:fs"; import { relative, resolve } from "node:path"; import * as ts from "typescript/sync"; -import type { Diagnostic } from "./types.ts"; +import type { CompilerObservation, Diagnostic } from "./types.ts"; +const output = process.argv[2]; +if (!output) throw new Error("usage: node observe.ts "); const cwd = process.cwd(); const app = resolve("app.ts"); const api = new ts.API({ cwd }); @@ -99,27 +102,33 @@ try { return [s.name, render(type)]; }), ); - function diagnostic(d: ts.Diagnostic): Diagnostic { + type Location = Pick; + function diagnostic(d: ts.Diagnostic, parent?: Location): Diagnostic { const observation: Diagnostic = { code: d.code, category: d.category, message: d.text, }; + let location: Location | undefined; if (d.fileName) { if (!d.startPosition) throw new Error("File diagnostic has no start position"); - const source = readFileSync(d.fileName, "utf8"); - Object.assign(observation, { - file: relative(cwd, d.fileName), + location = { + // A path out of the scenario stays absolute: its relative form depends on $TMPDIR. + file: d.fileName.startsWith(`${cwd}/`) + ? relative(cwd, d.fileName) + : d.fileName, line: d.startPosition.line + 1, column: d.startPosition.character + 1, - expression: source.slice(d.pos, d.end), - }); + expression: readFileSync(d.fileName, "utf8").slice(d.pos, d.end), + }; + if (JSON.stringify(location) !== JSON.stringify(parent)) + Object.assign(observation, location); } if (d.messageChain?.length) - observation.details = d.messageChain.map(diagnostic); + observation.details = d.messageChain.map((x) => diagnostic(x, location)); if (d.relatedInformation?.length) - observation.related = d.relatedInformation.map(diagnostic); + observation.related = d.relatedInformation.map((x) => diagnostic(x)); return observation; } const diagnostics = [ @@ -129,14 +138,14 @@ try { ...program.getSyntacticDiagnostics(), ...program.getBindDiagnostics(), ...program.getSemanticDiagnostics(), - ].map(diagnostic); - const unique = [ - ...new Map(diagnostics.map((d) => [JSON.stringify(d), d])).values(), - ]; - writeFileSync( - ".scenario/compiler.json", - JSON.stringify({ types, diagnostics: unique }, null, 2), - ); + ].map((d) => diagnostic(d)); + const observation: CompilerObservation = { + types, + diagnostics: [ + ...new Map(diagnostics.map((d) => [JSON.stringify(d), d])).values(), + ], + }; + writeFileSync(output, JSON.stringify(observation, null, 2)); } finally { api.close(); } diff --git a/tests/support/paths.ts b/tests/support/paths.ts new file mode 100644 index 00000000..fd0ee036 --- /dev/null +++ b/tests/support/paths.ts @@ -0,0 +1,7 @@ +import { join, resolve } from "node:path"; + +const tests = resolve(import.meta.dirname, ".."); +export const repository = resolve(tests, ".."); +export const scenarios = join(tests, "scenarios"); +/** Failing scenarios and setup logs of the latest run; cleared when a run starts. */ +export const artifacts = join(tests, ".artifacts"); diff --git a/tests/support/scenario.ts b/tests/support/scenario.ts new file mode 100644 index 00000000..ebda45c1 --- /dev/null +++ b/tests/support/scenario.ts @@ -0,0 +1,218 @@ +import { + access, + cp, + mkdir, + mkdtemp, + readFile, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { expect, test } from "vitest"; +import { command, required, unlessMissing, withCommands } from "./command.ts"; +import { type Allocation, allocate } from "./database.ts"; +import { artifacts, repository, scenarios } from "./paths.ts"; +import type { CompilerObservation, Observation, Scenario } from "./types.ts"; + +type Step = "setup" | "generate" | "compile" | "start"; + +// Generated discovery files only select a scenario. Assertions remain Vitest's. +export function scenario(name: string) { + test(name, ({ signal }) => withCommands(signal, () => run(name))); +} + +async function run(name: string) { + const source = join(scenarios, name); + const directory = await mkdtemp(join(tmpdir(), `typesql-${name}-`)); + const internal = join(directory, ".scenario"); + const observed: Observation = {}; + let stage = "prepare"; + let database: Allocation | undefined; + // Paths and URLs of this run, replaced in the snapshot and in failure logs. + const normalized = () => { + let text = JSON.stringify(observed) + .replaceAll(directory, "") + .replaceAll(repository, ""); + if (database?.env.DATABASE_URL) + text = text.replaceAll(database.env.DATABASE_URL, ""); + return JSON.parse(text.replace(/\(node:\d+\)/g, "(node:)")); + }; + try { + const manifest = await prepare(source, directory); + const failure = manifest.scenario?.failure; + if ( + failure && + ((failure.stage !== "compile" && !manifest.scripts?.[failure.stage]) || + failure.exitCode <= 0) + ) + throw new Error("Invalid expected failure contract"); + database = await allocate(manifest.scenario?.database, directory); + const { env } = database; + + const script = async (step: Step, extra: NodeJS.ProcessEnv = {}) => { + stage = step; + const argv = manifest.scripts?.[step] + ? ["pnpm", "--silent", "run", step] + : [ + join(directory, "node_modules", ".bin", "tsc"), + "-p", + "tsconfig.json", + "--pretty", + "false", + ]; + const result = await command(directory, argv, { + env: { ...env, ...extra }, + logs: join(internal, "logs"), + }); + observed[step] = { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + }; + if ( + result.status !== 0 && + (failure?.stage !== step || result.status !== failure.exitCode) + ) + throw new Error( + `Unexpected ${step} failure (${result.status})\n${result.stdout}${result.stderr}`, + ); + if (failure?.stage === step && result.status === 0) + throw new Error( + `Expected ${step} to fail with ${failure.exitCode}, but it succeeded`, + ); + return result.status === 0; + }; + + const start = async () => { + const errorFile = join(internal, "exception.json"); + const started = await script("start", { + SCENARIO_ERROR_FILE: errorFile, + NODE_OPTIONS: `--import=${join(import.meta.dirname, "runtime.ts")}`, + }); + if (started) return; + try { + observed.exception = JSON.parse(await readFile(errorFile, "utf8")); + } catch { + throw new Error( + "Infrastructure: expected application exception was not observed", + ); + } + // Node's report of the exception repeats it with a stack trace. + delete observed.start?.stderr; + }; + + const stages = async () => { + for (const step of ["setup", "generate"] as const) + if (manifest.scripts?.[step] && !(await script(step))) return; + if (manifest.scripts?.generate) { + stage = "generated artifacts"; + for (const file of manifest.scenario?.artifacts ?? []) + await access(artifact(directory, file)); + } + const compiled = await script("compile"); + stage = "types and diagnostics"; + const output = join(internal, "compiler.json"); + await required(directory, [ + "node", + join(import.meta.dirname, "observe.ts"), + output, + ]); + const compiler: CompilerObservation = JSON.parse( + await readFile(output, "utf8"), + ); + observed.compiler = compiler; + if (compiled !== (compiler.diagnostics.length === 0)) + throw new Error( + "Compiler command and observer disagree about diagnostics", + ); + // The compiler's output repeats the observed diagnostics. + if (!compiled) delete observed.compile?.stdout; + if (compiled && manifest.scripts?.start) await start(); + stage = "artifacts"; + const files: Record = {}; + for (const file of manifest.scenario?.artifacts ?? []) + files[file] = await readFile(artifact(directory, file), "utf8"); + if (Object.keys(files).length) observed.artifacts = files; + }; + + await stages(); + stage = "snapshot"; + expect(normalized()).toMatchSnapshot("scenario"); + } catch (cause) { + const retained = join(artifacts, name); + await rm(retained, { recursive: true, force: true }); + await mkdir(internal, { recursive: true }); + await writeFile( + join(internal, "failure.json"), + JSON.stringify( + { name, stage, observed: normalized(), error: String(cause) }, + null, + 2, + ), + ); + await unlessMissing( + cp(join(source, "expected.snap"), join(internal, "expected.snap")), + ); + await cp(directory, retained, { recursive: true }); + throw new Error( + `${name}: failed at ${stage}. Scenario and logs retained at ${retained}`, + { cause }, + ); + } finally { + await database?.cleanup(); + await rm(directory, { recursive: true, force: true }); + } +} + +function artifact(directory: string, file: string) { + const path = resolve(directory, file); + if (!path.startsWith(`${directory}/`)) + throw new Error(`Artifact escapes scenario: ${file}`); + return path; +} + +async function prepare(source: string, directory: string) { + await cp(source, directory, { + recursive: true, + filter: (path) => + !["node_modules", "dist", ".scenario", "expected.snap"].includes( + basename(path), + ), + }); + await symlink( + join(source, "node_modules"), + join(directory, "node_modules"), + "dir", + ); + await mkdir(join(directory, ".scenario")); + const config = JSON.parse( + await readFile(join(scenarios, "tsconfig.base.json"), "utf8"), + ); + const configPath = join(directory, "tsconfig.json"); + const text = await unlessMissing(readFile(configPath, "utf8")); + if (text !== undefined) { + const overrides = JSON.parse(text); + if ( + overrides.extends || + overrides.compilerOptions?.paths || + overrides.compilerOptions?.baseUrl + ) + throw new Error( + "Scenario compiler options must use public package imports", + ); + Object.assign(config, { + ...overrides, + compilerOptions: { + ...config.compilerOptions, + ...overrides.compilerOptions, + }, + }); + } + await writeFile(configPath, JSON.stringify(config, null, 2)); + const manifest: Scenario = JSON.parse( + await readFile(join(directory, "package.json"), "utf8"), + ); + return manifest; +} diff --git a/tests/support/setup.ts b/tests/support/setup.ts index 88095e13..d27a1909 100644 --- a/tests/support/setup.ts +++ b/tests/support/setup.ts @@ -1,19 +1,14 @@ -import { cp, mkdir, writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; -import { required, unlessMissing } from "./command.ts"; -import { ready } from "./database.ts"; - -const workspace = resolve(import.meta.dirname, ".."); -const repository = resolve(workspace, ".."); +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { required } from "./command.ts"; +import { artifacts, repository } from "./paths.ts"; export default async function setup() { + await rm(artifacts, { recursive: true, force: true }); try { - return await environment(); + await environment(); } catch (cause) { - const retained = join( - process.env.SCENARIO_ARTIFACTS ?? join(workspace, ".artifacts"), - "setup", - ); + const retained = join(artifacts, "setup"); await mkdir(retained, { recursive: true }); await writeFile( join(retained, "failure.json"), @@ -29,14 +24,6 @@ export default async function setup() { 2, ), ); - for (const [name, cwd] of [ - ["root", repository], - ["tests", workspace], - ] as const) { - await unlessMissing( - cp(join(cwd, ".scenario"), join(retained, name), { recursive: true }), - ); - } throw new Error( `Infrastructure: scenario prerequisites failed; logs retained at ${retained}`, { cause }, @@ -45,33 +32,19 @@ export default async function setup() { } async function environment() { + const node = ( + await readFile(join(repository, ".node-version"), "utf8") + ).trim(); if ( - process.version !== "v26.8.1" || + process.version !== `v${node}` || process.platform !== "linux" || process.arch !== "x64" ) - throw new Error("Infrastructure: expected Node 26.8.1 on linux/x64"); - await ready(); + throw new Error(`Infrastructure: expected Node ${node} on linux/x64`); + // Builds the TypeSQL packages the scenarios depend on. await required( repository, - [ - "pnpm", - ...[ - "core", - "pg", - "postgres", - "sqlite", - "mysql", - "kysely", - "drizzle", - "prisma", - "cli", - "better-auth", - ].flatMap((name) => ["--filter", `@typesql/${name}`]), - "run", - "build", - ], - {}, - 300_000, + ["pnpm", "--filter", "{./tests/scenarios/*}^...", "run", "build"], + { timeout: 300_000 }, ); } diff --git a/tests/support/types.ts b/tests/support/types.ts index 4fc98a83..060211bb 100644 --- a/tests/support/types.ts +++ b/tests/support/types.ts @@ -1,7 +1,8 @@ +export type Database = "postgres" | "sqlite" | "mysql" | "mariadb"; export interface Scenario { - scripts: Record; + scripts?: Record; scenario?: { - database?: "postgres" | "sqlite" | "mysql" | "mariadb"; + database?: Database; failure?: { stage: "generate" | "compile" | "start"; exitCode: number }; artifacts?: string[]; }; @@ -30,8 +31,10 @@ export interface ApplicationException { } interface CommandObservation { status: number; - stdout: string; - stderr: string; + /** Left out of a failed compilation, whose output repeats `compiler.diagnostics`. */ + stdout?: string; + /** Left out of a failed start, whose output repeats `exception` with a stack trace. */ + stderr?: string; } export interface Observation { setup?: CommandObservation; diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts index a8f8a28f..758f83f3 100644 --- a/tests/vitest.config.ts +++ b/tests/vitest.config.ts @@ -1,16 +1,33 @@ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; +import { parseArgs } from "node:util"; import { defineConfig } from "vitest/config"; +import { scenarios } from "./support/paths.ts"; +const { tokens } = parseArgs({ + args: process.argv.slice(2), + options: { update: { type: "boolean", short: "u" } }, + strict: false, + allowPositionals: true, + tokens: true, +}); +if ( + process.env.CI && + tokens.some((t) => t.kind === "option" && t.name === "update") +) + throw new Error("CI must never update scenario snapshots"); +// Without --update, a missing snapshot fails instead of being written. process.env.UPDATE_SNAPSHOT = "none"; + const root = import.meta.dirname; const discovery = join(root, ".vitest"); rmSync(discovery, { recursive: true, force: true }); mkdirSync(discovery, { recursive: true }); -for (const name of readdirSync(join(root, "scenarios")).sort()) { +for (const entry of readdirSync(scenarios, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; writeFileSync( - join(discovery, `${name}.test.ts`), - `import { scenario } from '../scenario.test.ts';\nscenario(${JSON.stringify(name)});\n`, + join(discovery, `${entry.name}.test.ts`), + `import { scenario } from '../support/scenario.ts';\nscenario(${JSON.stringify(entry.name)});\n`, ); } export default defineConfig({ @@ -26,6 +43,6 @@ export default defineConfig({ isolate: false, snapshotFormat: { escapeString: false }, resolveSnapshotPath: (path: string) => - join(root, "scenarios", basename(path, ".test.ts"), "expected.snap"), + join(scenarios, basename(path, ".test.ts"), "expected.snap"), }, }); diff --git a/tools/biome/build.ts b/tools/biome/build.ts new file mode 100644 index 00000000..26aa0bd5 --- /dev/null +++ b/tools/biome/build.ts @@ -0,0 +1,66 @@ +// Writes the Biome GritQL plugins tools/biome/*.grit from their sources in src/. +// usage: node tools/biome/build.ts [--check] +// +// Biome's GritQL has no imports and no bounded recursion, so a source may use: +// #include the definition in shared/.grit +// #repeat .. the following lines up to #end once for each n from a to b, +// ... with {{n}}, {{n+1}}, {{n-1}}, ... replaced by n and its neighbours +// #end +// --check fails when a plugin differs from its source or biome.json does not load it. +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const here = import.meta.dirname; +const check = process.argv.includes("--check"); + +function expand(source: string): string { + return source + .replace( + /^#repeat (\d+)\.\.(\d+)\n([\s\S]*?)^#end\n/gm, + (_, from: string, to: string, body: string) => { + const copies: string[] = []; + for (let n = Number(from); n <= Number(to); n++) + copies.push( + body.replace(/\{\{n(?:([+-])(\d+))?\}\}/g, (_, sign, offset) => + String( + sign === "-" ? n - Number(offset) : n + Number(offset ?? 0), + ), + ), + ); + return copies.join("\n"); + }, + ) + .replace(/^#include (\S+)\n/gm, (_, name: string) => + readFileSync(join(here, "shared", `${name}.grit`), "utf8"), + ); +} + +const plugins = readdirSync(join(here, "src")).filter((f) => + f.endsWith(".grit"), +); +let stale = false; +for (const file of plugins) { + const output = join(here, file); + const plugin = expand(readFileSync(join(here, "src", file), "utf8")); + if (!check) writeFileSync(output, plugin); + else if (readFileSync(output, "utf8") !== plugin) { + console.error(`tools/biome/${file} differs from src/${file}`); + stale = true; + } +} + +const biome: { plugins: string[] } = JSON.parse( + readFileSync(join(here, "../../biome.json"), "utf8"), +); +const loaded = new Set(biome.plugins); +for (const file of plugins) + if (!loaded.has(`./tools/biome/${file}`)) { + console.error(`biome.json does not load ./tools/biome/${file}`); + stale = true; + } +if (stale) { + console.error( + "run node tools/biome/build.ts and add new plugins to biome.json", + ); + process.exitCode = 1; +} diff --git a/tools/biome/no-reduce-accumulator-copy.grit b/tools/biome/no-reduce-accumulator-copy.grit index 9dc370f1..c12b4134 100644 --- a/tools/biome/no-reduce-accumulator-copy.grit +++ b/tools/biome/no-reduce-accumulator-copy.grit @@ -152,16 +152,16 @@ pattern unwrapped_identifier($target) { pattern function_like() { or { JsFunctionDeclaration(), - JsFunctionExportDefaultDeclaration(), JsFunctionExpression(), JsArrowFunctionExpression(), + JsFunctionExportDefaultDeclaration(), JsMethodClassMember(), JsMethodObjectMember(), - JsConstructorClassMember(), JsGetterClassMember(), - JsSetterClassMember(), JsGetterObjectMember(), - JsSetterObjectMember() + JsSetterClassMember(), + JsSetterObjectMember(), + JsConstructorClassMember() } } diff --git a/tools/biome/shared/function_like.grit b/tools/biome/shared/function_like.grit new file mode 100644 index 00000000..519587c7 --- /dev/null +++ b/tools/biome/shared/function_like.grit @@ -0,0 +1,15 @@ +pattern function_like() { + or { + JsFunctionDeclaration(), + JsFunctionExpression(), + JsArrowFunctionExpression(), + JsFunctionExportDefaultDeclaration(), + JsMethodClassMember(), + JsMethodObjectMember(), + JsGetterClassMember(), + JsGetterObjectMember(), + JsSetterClassMember(), + JsSetterObjectMember(), + JsConstructorClassMember() + } +} diff --git a/tools/biome/shared/unparenthesized.grit b/tools/biome/shared/unparenthesized.grit new file mode 100644 index 00000000..4403a21e --- /dev/null +++ b/tools/biome/shared/unparenthesized.grit @@ -0,0 +1,8 @@ +pattern unparenthesized($inner) { + or { + $inner, + JsParenthesizedExpression(expression=$inner), + JsParenthesizedExpression(expression=JsParenthesizedExpression(expression=$inner)), + JsParenthesizedExpression(expression=JsParenthesizedExpression(expression=JsParenthesizedExpression(expression=$inner))) + } +} diff --git a/tools/biome/shared/wrapped.grit b/tools/biome/shared/wrapped.grit new file mode 100644 index 00000000..38d6cfab --- /dev/null +++ b/tools/biome/shared/wrapped.grit @@ -0,0 +1,10 @@ +pattern wrapped($leaf, $self) { + or { + $leaf, + JsParenthesizedExpression(expression=$self), + TsAsExpression(expression=$self), + TsTypeAssertionExpression(expression=$self), + TsNonNullAssertionExpression(expression=$self), + TsSatisfiesExpression(expression=$self) + } +} diff --git a/tools/biome/src/no-builtin-instanceof.grit b/tools/biome/src/no-builtin-instanceof.grit new file mode 100644 index 00000000..0f5b9cf7 --- /dev/null +++ b/tools/biome/src/no-builtin-instanceof.grit @@ -0,0 +1,15 @@ +language js(typescript) + +`$value instanceof $constructor` as $check where { + $constructor <: or { + `Object`, + `Array`, + `Function`, + `String`, + `Number`, + `Boolean`, + `Symbol`, + `BigInt` + }, + register_diagnostic(span=$check, message="An `instanceof` check against a built-in constructor probes representation and breaks across realms. Parse input at its I/O boundary, then branch on the domain value.", severity="error") +} diff --git a/tools/biome/src/no-chained-type-assertions.grit b/tools/biome/src/no-chained-type-assertions.grit new file mode 100644 index 00000000..7a52c9aa --- /dev/null +++ b/tools/biome/src/no-chained-type-assertions.grit @@ -0,0 +1,45 @@ +engine biome(1.0) +language js(typescript) + +pattern assertion($type, $inner) { + or { + TsAsExpression(ty=$type, expression=$inner), + TsTypeAssertionExpression(ty=$type, expression=$inner) + } +} + +pattern unparenthesized_assertion() { + or { + TsAsExpression(), + TsTypeAssertionExpression(), + JsParenthesizedExpression(expression=unparenthesized_assertion()) + } +} + +pattern non_const_chain() { + or { + JsParenthesizedExpression(expression=non_const_chain()), + TsAsExpression(ty=$type, expression=$inner) where { + or { $type <: not "const", $inner <: non_const_chain() } + }, + TsTypeAssertionExpression(ty=$type, expression=$inner) where { + or { $type <: not "const", $inner <: non_const_chain() } + } + } +} + +or { + TsAsExpression(expression=$inner), + TsTypeAssertionExpression(expression=$inner) +} as $outer where { + $inner <: unparenthesized_assertion(), + $outer <: non_const_chain(), + $outer <: not within assertion($_, $wrapped) where { + $wrapped <: or { + $outer, + JsParenthesizedExpression(expression=$outer), + JsParenthesizedExpression(expression=JsParenthesizedExpression(expression=$outer)) + } + }, + register_diagnostic(span=$outer, message="This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.", severity="error") +} diff --git a/tools/biome/src/no-conditional-empty-object-spread.grit b/tools/biome/src/no-conditional-empty-object-spread.grit new file mode 100644 index 00000000..2f540bed --- /dev/null +++ b/tools/biome/src/no-conditional-empty-object-spread.grit @@ -0,0 +1,18 @@ +language js(typescript) + +#include unparenthesized + +pattern empty_object() { unparenthesized(JsObjectExpression(members=[])) } + +JsSpread(argument=unparenthesized(or { + JsConditionalExpression(consequent=empty_object()), + JsConditionalExpression(alternate=empty_object()) +})) as $spread where { + $spread <: within or { + JsObjectExpression(), + JsArrayExpression(), + JsCallArguments() + } as $owner, + $owner <: JsObjectExpression(), + register_diagnostic(span=$spread, message="This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.", severity="error") +} diff --git a/tools/biome/src/no-hidden-unknown-and-object-types.grit b/tools/biome/src/no-hidden-unknown-and-object-types.grit new file mode 100644 index 00000000..4b5aa1b8 --- /dev/null +++ b/tools/biome/src/no-hidden-unknown-and-object-types.grit @@ -0,0 +1,328 @@ +engine biome(1.0) +language js(typescript) + +pattern scope_has($decl) { + or { + JsModule(items=some or { + $decl, + JsExport(export_clause=$decl) + }), + JsScript(statements=some $decl), + JsFunctionBody(statements=some $decl), + JsBlockStatement(statements=some $decl), + TsModuleBlock(items=some or { + $decl, + JsExport(export_clause=$decl) + }), + JsStaticInitializationBlockClassMember(statements=some $decl), + JsCaseClause(consequent=some $decl), + JsDefaultClause(consequent=some $decl) + } +} + +pattern type_parameters_bind($name) { + or { + JsFunctionDeclaration(type_parameters=$tp), + JsFunctionExpression(type_parameters=$tp), + JsArrowFunctionExpression(type_parameters=$tp), + TsDeclareFunctionDeclaration(type_parameters=$tp), + JsMethodClassMember(type_parameters=$tp), + JsMethodObjectMember(type_parameters=$tp), + TsMethodSignatureClassMember(type_parameters=$tp), + TsMethodSignatureTypeMember(type_parameters=$tp), + TsCallSignatureTypeMember(type_parameters=$tp), + TsConstructSignatureTypeMember(type_parameters=$tp), + TsFunctionType(type_parameters=$tp), + TsConstructorType(type_parameters=$tp), + TsTypeAliasDeclaration(type_parameters=$tp), + TsInterfaceDeclaration(type_parameters=$tp), + JsClassDeclaration(type_parameters=$tp), + JsClassExpression(type_parameters=$tp) + } where { + $tp <: TsTypeParameters(items=some TsTypeParameter(name=$p) where { + $p <: $name + }) + } +} + +pattern type_parameter_scope($name, $use) { + or { + type_parameters_bind($name), + TsMappedType(property_name=$p) where { $p <: $name }, + TsConditionalType(extends_type=$e, true_type=$t) where { + $e <: contains TsInferType(name=$p) where { $p <: $name }, + $t <: contains $use + } + } +} + +pattern named_type_binding($name) { + or { + TsTypeAliasDeclaration(binding_identifier=$b), + TsInterfaceDeclaration(id=$b), + TsEnumDeclaration(id=$b), + JsClassDeclaration(id=$b), + JsImport() as $i where { $i <: contains JsIdentifierBinding() as $b } + } where { $b <: $name } +} + +pattern alias_to($target) { + TsReferenceType(name=$n, type_arguments=.) as $ref where { + $ref <: not within type_parameter_scope($n, $ref), + $ref <: within scope_has( TsTypeAliasDeclaration(binding_identifier=$b, ty=$aliased) where { + $b <: $n, + $aliased <: $target + }), + $ref <: not within scope_has( named_type_binding($n) as $other where { + $other <: not TsTypeAliasDeclaration(ty=$target) + }) + } +} + +// `$wrappers` names references such as `Promise` whose first type argument is looked through. +// It is matched under double negation so that one wrapper name never binds for nested references. +pattern wrapped_by($wrappers, $inner) { + TsReferenceType(name=$wrapper) as $reference where { + $wrapper <: not not $wrappers, + $reference <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $inner, ... + ])) + } +} + +pattern passes_direct_2($p) { + TsReferenceType(name=$x, type_arguments=.) where { $x <: $p } +} + +pattern passes_direct_1($p, $wrappers) { + or { + passes_direct_2($p), + TsParenthesizedType(ty=passes_direct_2($p)), + TsUnionType(types=some passes_direct_2($p)), + wrapped_by($wrappers, passes_direct_2($p)) + } +} + +pattern passes_direct($p, $wrappers) { + or { + passes_direct_2($p), + TsParenthesizedType(ty=passes_direct_1($p, $wrappers)), + TsUnionType(types=some passes_direct_1($p, $wrappers)), + wrapped_by($wrappers, passes_direct_1($p, $wrappers)) + } +} + +pattern generic_alias_inner($ref, $name, $target, $wrappers) { + $ref where { + $ref <: not within type_parameter_scope($name, $ref), + $ref <: within scope_has( TsTypeAliasDeclaration(binding_identifier=$b, type_parameters=$tps, ty=$body) where { + $b <: $name, + or { + and { + $tps <: TsTypeParameters(items=[TsTypeParameter(name=$q0), ...]), + $body <: passes_direct($q0, $wrappers), + $ref <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $a0, ... + ])) where { $a0 <: $target } + }, + and { + $tps <: TsTypeParameters(items=[$_, TsTypeParameter(name=$q1), ...]), + $body <: passes_direct($q1, $wrappers), + $ref <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $a1, ... + ])) where { $a1 <: $target } + }, + and { + $tps <: TsTypeParameters(items=[ + $_, $_, TsTypeParameter(name=$q2), ... + ]), + $body <: passes_direct($q2, $wrappers), + $ref <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $_, $a2, ... + ])) where { $a2 <: $target } + } + } + }), + $ref <: not within scope_has( named_type_binding($name) as $other where { + $other <: not TsTypeAliasDeclaration(type_parameters=TsTypeParameters()) + }) + } +} + +pattern passes_nested_1($p, $wrappers) { + or { + passes_direct_2($p), + TsParenthesizedType(ty=passes_direct_1($p, $wrappers)), + TsUnionType(types=some passes_direct_1($p, $wrappers)), + wrapped_by($wrappers, passes_direct_1($p, $wrappers)), + TsReferenceType(name=$m) as $r where { + $r <: generic_alias_inner($r, $m, passes_direct_1($p, $wrappers), $wrappers) + } + } +} + +pattern passes_nested($p, $wrappers) { + or { + passes_direct_2($p), + TsParenthesizedType(ty=passes_nested_1($p, $wrappers)), + TsUnionType(types=some passes_nested_1($p, $wrappers)), + wrapped_by($wrappers, passes_nested_1($p, $wrappers)), + TsReferenceType(name=$m) as $r where { + $r <: generic_alias_inner($r, $m, passes_nested_1($p, $wrappers), $wrappers) + } + } +} + +pattern generic_alias($ref, $name, $target, $wrappers) { + $ref where { + $ref <: not within type_parameter_scope($name, $ref), + $ref <: within scope_has( TsTypeAliasDeclaration(binding_identifier=$b, type_parameters=$tps, ty=$body) where { + $b <: $name, + or { + and { + $tps <: TsTypeParameters(items=[ + TsTypeParameter(name=$q0, default=$d0), ... + ]), + $body <: passes_nested($q0, $wrappers), + $ref <: or { + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $a0, ... + ])) where { $a0 <: $target }, + or { TsReferenceType(type_arguments=.) } where { + $d0 <: TsDefaultTypeClause(ty=$target) + } + } + }, + and { + $tps <: TsTypeParameters(items=[ + $_, TsTypeParameter(name=$q1, default=$d1), ... + ]), + $body <: passes_nested($q1, $wrappers), + $ref <: or { + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $a1, ... + ])) where { $a1 <: $target }, + or { + TsReferenceType(type_arguments=.), + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_ + ])) + } where { $d1 <: TsDefaultTypeClause(ty=$target) } + } + }, + and { + $tps <: TsTypeParameters(items=[ + $_, $_, TsTypeParameter(name=$q2, default=$d2), ... + ]), + $body <: passes_nested($q2, $wrappers), + $ref <: or { + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $_, $a2, ... + ])) where { $a2 <: $target }, + or { + TsReferenceType(type_arguments=.), + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_ + ])), + TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $_ + ])) + } where { $d2 <: TsDefaultTypeClause(ty=$target) } + } + } + } + }), + $ref <: not within scope_has( named_type_binding($name) as $other where { + $other <: not TsTypeAliasDeclaration(type_parameters=TsTypeParameters()) + }) + } +} + +// One alias level: `$leaf` itself, an alias resolving to `$next`, or `$self` inside +// parentheses, unions and `$wrappers`. +pattern resolves($leaf, $next, $self, $wrappers) { + or { + $leaf, + alias_to($next), + TsReferenceType(name=$n) as $ref where { + $ref <: generic_alias($ref, $n, $next, $wrappers) + }, + TsParenthesizedType(ty=$self), + TsUnionType(types=some $self), + wrapped_by($wrappers, $self) + } +} + +// The last level stops resolving aliases. +pattern resolves_last($leaf, $self, $wrappers) { + or { + $leaf, + TsParenthesizedType(ty=$self), + TsUnionType(types=some $self), + wrapped_by($wrappers, $self) + } +} + +#repeat 1..5 +pattern resolves_unknown_{{n}}() { + resolves(TsUnknownType(), resolves_unknown_{{n+1}}(), resolves_unknown_{{n}}(), or {}) +} +#end + +pattern resolves_unknown_6() { + resolves_last(TsUnknownType(), resolves_unknown_6(), or {}) +} + +#repeat 1..5 +pattern resolves_object_{{n}}() { + resolves(TsNonPrimitiveType(), resolves_object_{{n+1}}(), resolves_object_{{n}}(), or {}) +} +#end + +pattern resolves_object_6() { + resolves_last(TsNonPrimitiveType(), resolves_object_6(), or {}) +} + +#repeat 1..5 +pattern resolves_returned_unknown_{{n}}() { + resolves(TsUnknownType(), resolves_returned_unknown_{{n+1}}(), resolves_returned_unknown_{{n}}(), or { + "Promise", + "PromiseLike" + }) +} +#end + +pattern resolves_returned_unknown_6() { + resolves_last(TsUnknownType(), resolves_returned_unknown_6(), or { + "Promise", + "PromiseLike" + }) +} + +or { + TsTypeAliasDeclaration(binding_identifier=$alias, ty=$type) where { + $type <: resolves_unknown_1(), + register_diagnostic(span=$alias, message="This type alias hides `unknown`. Keep `unknown` explicit at the parsing boundary or use the parsed owner type.", severity="error") + }, + or { + JsFormalParameter(type_annotation=TsTypeAnnotation(ty=$type)), + JsRestParameter(type_annotation=TsTypeAnnotation(ty=$type)), + TsThisParameter(type_annotation=TsTypeAnnotation(ty=$type)) + } where { + $type <: resolves_object_1(), + register_diagnostic(span=$type, message="This parameter uses the broad `object` type. Accept a named owner type and parse external input at its boundary before calling this function.", severity="error") + }, + or { + TsReturnTypeAnnotation(ty=$type), + TsFunctionType(return_type=$type), + TsConstructorType(return_type=$type), + TsConstructSignatureTypeMember(type_annotation=TsTypeAnnotation(ty=$type)), + JsGetterClassMember(return_type=TsTypeAnnotation(ty=$type)), + JsGetterObjectMember(return_type=TsTypeAnnotation(ty=$type)), + TsGetterSignatureClassMember(return_type=TsTypeAnnotation(ty=$type)), + TsGetterSignatureTypeMember(type_annotation=TsTypeAnnotation(ty=$type)) + } where { + $type <: resolves_returned_unknown_1(), + register_diagnostic(span=$type, message="This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.", severity="error") + } +} diff --git a/tools/biome/src/no-known-value-widening.grit b/tools/biome/src/no-known-value-widening.grit new file mode 100644 index 00000000..e892036d --- /dev/null +++ b/tools/biome/src/no-known-value-widening.grit @@ -0,0 +1,1020 @@ +language js(typescript) + +pattern transparent($inner) { + or { + $inner, + TsParenthesizedType(ty=$inner), + TsTypeOperatorType(ty=$inner) as $operator where { + $operator <: r"(?s)readonly\s.*" + }, + TsParenthesizedType(ty=TsParenthesizedType(ty=$inner)), + TsParenthesizedType(ty= TsTypeOperatorType(ty=$inner) as $operator where { + $operator <: r"(?s)readonly\s.*" + }), + TsTypeOperatorType(ty=TsParenthesizedType(ty=$inner)) as $operator where { + $operator <: r"(?s)readonly\s.*" + }, + TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=$inner))), + TsParenthesizedType(ty=TsParenthesizedType(ty= TsTypeOperatorType(ty=$inner) as $operator where { + $operator <: r"(?s)readonly\s.*" + })), + TsParenthesizedType(ty= TsTypeOperatorType(ty=TsParenthesizedType(ty=$inner)) as $operator where { + $operator <: r"(?s)readonly\s.*" + }), + TsTypeOperatorType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=$inner))) as $operator where { + $operator <: r"(?s)readonly\s.*" + }, + TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=$inner)))), + TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=$inner))))), + TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=TsParenthesizedType(ty=$inner)))))) + } +} + +pattern known_terminal() { + or { + JsObjectExpression(), + JsArrayExpression(), + JsArrowFunctionExpression(), + JsClassExpression(), + JsFunctionExpression(), + JsNewExpression(), + JsStringLiteralExpression(), + JsNumberLiteralExpression(), + JsBigintLiteralExpression(), + JsBooleanLiteralExpression(), + JsNullLiteralExpression(), + JsRegexLiteralExpression(), + JsTemplateExpression(tag=.), + JsUnaryExpression() + } +} + +#include wrapped + +pattern known_value() { wrapped(known_terminal(), known_value()) } + +pattern empty_object() { + wrapped(JsObjectExpression(members=[]), empty_object()) +} + +pattern identifier_value() { + wrapped(JsIdentifierExpression(), identifier_value()) +} + +pattern function_value() { + wrapped(or { + JsArrowFunctionExpression(), + JsFunctionExpression() + }, function_value()) +} + +pattern declared($declaration) { + or { + $declaration, + JsVariableStatement(declaration=$declaration), + JsExport(export_clause=or { + $declaration, + JsVariableDeclarationClause(declaration=$declaration), + TsExportDeclareClause(declaration=or { + $declaration, + JsVariableDeclarationClause(declaration=$declaration) + }), + JsExportDefaultDeclarationClause(declaration=$declaration) + }), + TsDeclareStatement(declaration=or { + $declaration, + JsVariableDeclarationClause(declaration=$declaration) + }) + } +} + +pattern binding_named($name) { + or { + JsIdentifierBinding() as $id where { $id <: $name }, + or { + JsObjectBindingPattern(), + JsArrayBindingPattern() + } as $x where { + $x <: contains JsIdentifierBinding() as $id where { + $id <: $name + } until JsInitializerClause() + } + } +} + +pattern value_entry($name) { + or { + declared(or { + JsVariableDeclaration(declarators=some JsVariableDeclarator(id=binding_named($name))), + JsFunctionDeclaration(id= $id where { $id <: $name }), + TsDeclareFunctionDeclaration(id= $id where { $id <: $name }), + JsClassDeclaration(id= $id where { $id <: $name }), + TsEnumDeclaration(id= $id where { $id <: $name }), + TsModuleDeclaration(name= $id where { $id <: $name }), + TsImportEqualsDeclaration(id= $id where { $id <: $name }), + JsFunctionExportDefaultDeclaration(id= $id where { $id <: $name }), + JsClassExportDefaultDeclaration(id= $id where { $id <: $name }) + }), + JsImport() as $x where { + $x <: contains JsIdentifierBinding() as $id where { $id <: $name } + }, + JsFormalParameter(binding=binding_named($name)), + JsRestParameter(binding=binding_named($name)), + TsPropertyParameter(formal_parameter=JsFormalParameter(binding=binding_named($name))), + JsIdentifierBinding() as $id where { $id <: $name }, + JsForVariableDeclaration(declarator=JsVariableDeclarator(id=binding_named($name))), + JsCatchDeclaration(binding=binding_named($name)) + } +} + +pattern hoisted_var($name) { + JsFunctionBody() as $x where { + $x <: contains JsVariableDeclaration(declarators=some JsVariableDeclarator(id=binding_named($name))) as $y where { + $y <: r"(?s)var\s.*" + } until or { + function_like(), + JsClassExpression(), + JsClassDeclaration() + } + } +} + +pattern type_scope($entry) { + or { + JsModule(items=some $entry), + JsScript(statements=some $entry), + TsModuleBlock(items=some $entry), + JsFunctionBody(statements=some $entry), + JsBlockStatement(statements=some $entry), + JsStaticInitializationBlockClassMember(statements=some $entry), + JsSwitchStatement(cases=some or { + JsCaseClause(consequent=some $entry), + JsDefaultClause(consequent=some $entry) + }) + } +} + +pattern value_scope($entry) { + or { + type_scope($entry), + JsFunctionDeclaration(parameters=JsParameters(items=some $entry)), + JsFunctionExpression(parameters=JsParameters(items=some $entry)), + JsFunctionExportDefaultDeclaration(parameters=JsParameters(items=some $entry)), + JsArrowFunctionExpression(parameters=or { + JsParameters(items=some $entry), + $entry + }), + JsMethodClassMember(parameters=JsParameters(items=some $entry)), + JsMethodObjectMember(parameters=JsParameters(items=some $entry)), + JsConstructorClassMember(parameters=JsConstructorParameters(parameters=some $entry)), + JsSetterClassMember(parameter=$entry), + JsSetterObjectMember(parameter=$entry), + JsForStatement(initializer=$entry), + JsForOfStatement(initializer=$entry), + JsForInStatement(initializer=$entry), + JsCatchClause(declaration=$entry) + } +} + +pattern value_scope_twice($first, $second) { + or { + JsModule(items=[..., $first, ..., $second, ...]), + JsScript(statements=[..., $first, ..., $second, ...]), + TsModuleBlock(items=[..., $first, ..., $second, ...]), + JsFunctionBody(statements=[..., $first, ..., $second, ...]), + JsBlockStatement(statements=[..., $first, ..., $second, ...]), + JsStaticInitializationBlockClassMember(statements=[ + ..., $first, ..., $second, ... + ]), + JsSwitchStatement(cases=some or { + JsCaseClause(consequent=[..., $first, ..., $second, ...]), + JsDefaultClause(consequent=[..., $first, ..., $second, ...]) + }) + } +} + +predicate scope_of($node, $name, $scope) { + $node <: within value_scope(value_entry($name)) as $scope +} + +predicate not_hoisted($node, $name, $scope) { + $node <: not within JsFunctionBody() as $x where { + $scope <: not within $x, + $x <: hoisted_var($name) + } +} + +predicate const_initializer($node, $name, $initializer) { + scope_of($node, $name, $x), + $x <: value_scope(declared( JsVariableDeclaration(declarators=some JsVariableDeclarator(id=binding_named($name), initializer=JsInitializerClause(expression=$initializer))) as $y where { + $y <: r"(?s)const\s.*" + })), + not_hoisted($node, $name, $x) +} + +predicate annotated_binding($node, $name, $annotation) { + scope_of($node, $name, $x), + $x <: value_scope(or { + declared(JsVariableDeclaration(declarators=some JsVariableDeclarator(id= JsIdentifierBinding() as $y where { + $y <: $name + }, variable_annotation=TsTypeAnnotation(ty=$annotation)))), + JsFormalParameter(binding= JsIdentifierBinding() as $y where { + $y <: $name + }, type_annotation=TsTypeAnnotation(ty=$annotation)), + JsRestParameter(binding= JsIdentifierBinding() as $y where { + $y <: $name + }, type_annotation=TsTypeAnnotation(ty=$annotation)), + TsPropertyParameter(formal_parameter=JsFormalParameter(binding= JsIdentifierBinding() as $y where { + $y <: $name + }, type_annotation=TsTypeAnnotation(ty=$annotation))) + }), + not_hoisted($node, $name, $x) +} + +pattern function_named($name, $parameters, $returns) { + or { + JsFunctionDeclaration(id= $id where { + $id <: $name + }, return_type_annotation=$returns, parameters=JsParameters(items=$parameters)), + TsDeclareFunctionDeclaration(id= $id where { + $id <: $name + }, return_type_annotation=$returns, parameters=JsParameters(items=$parameters)), + JsVariableDeclaration(declarators=some JsVariableDeclarator(id= JsIdentifierBinding() as $id where { + $id <: $name + }, initializer=JsInitializerClause(expression=function_signature($parameters, $returns)))) + } +} + +pattern function_signature($parameters, $returns) { + function_value() as $x where { + $x <: contains or { + JsArrowFunctionExpression(return_type_annotation=$returns, parameters=JsParameters(items=$parameters)), + JsFunctionExpression(return_type_annotation=$returns, parameters=JsParameters(items=$parameters)) + } until or { + JsArrowFunctionExpression(), + JsFunctionExpression() + } + } +} + +predicate local_function($callee, $parameters, $returns) { + or { + $callee <: function_signature($parameters, $returns), + and { + $callee <: identifier_value(), + $callee <: contains JsIdentifierExpression(name=$n) as $r, + scope_of($r, $n, $x), + $x <: value_scope(declared(function_named($n, $parameters, $returns))), + $x <: not value_scope_twice(declared(or { + JsFunctionDeclaration(id= $y where { $y <: $n }), + TsDeclareFunctionDeclaration(id= $y where { $y <: $n }) + }), declared(or { + JsFunctionDeclaration(id= $z where { $z <: $n }), + TsDeclareFunctionDeclaration(id= $z where { $z <: $n }) + })), + not_hoisted($r, $n, $x) + } + } +} + +pattern declares_type_parameter($name) { + TsTypeParameters(items=some TsTypeParameter(name= $x where { $x <: $name })) +} + +predicate type_parameter_shadowed($node, $name) { + $node <: within or { + JsFunctionDeclaration(type_parameters=declares_type_parameter($name)), + JsFunctionExpression(type_parameters=declares_type_parameter($name)), + JsArrowFunctionExpression(type_parameters=declares_type_parameter($name)), + JsFunctionExportDefaultDeclaration(type_parameters=declares_type_parameter($name)), + TsDeclareFunctionDeclaration(type_parameters=declares_type_parameter($name)), + TsDeclareFunctionExportDefaultDeclaration(type_parameters=declares_type_parameter($name)), + JsClassDeclaration(type_parameters=declares_type_parameter($name)), + JsClassExpression(type_parameters=declares_type_parameter($name)), + JsClassExportDefaultDeclaration(type_parameters=declares_type_parameter($name)), + TsInterfaceDeclaration(type_parameters=declares_type_parameter($name)), + TsTypeAliasDeclaration(type_parameters=declares_type_parameter($name)), + JsMethodClassMember(type_parameters=declares_type_parameter($name)), + JsMethodObjectMember(type_parameters=declares_type_parameter($name)), + TsMethodSignatureClassMember(type_parameters=declares_type_parameter($name)), + TsMethodSignatureTypeMember(type_parameters=declares_type_parameter($name)), + TsCallSignatureTypeMember(type_parameters=declares_type_parameter($name)), + TsConstructSignatureTypeMember(type_parameters=declares_type_parameter($name)), + TsFunctionType(type_parameters=declares_type_parameter($name)), + TsConstructorType(type_parameters=declares_type_parameter($name)), + TsMappedType(property_name=$x, keys_type=$y) where { + $x <: $name, + $node <: not within $y + }, + TsConditionalType(extends_type=$y, true_type=$z) where { + $y <: contains TsInferType(name= $x where { $x <: $name }), + $node <: within $z + } + } +} + +pattern type_entry($name) { + or { + declared(or { + TsTypeAliasDeclaration(binding_identifier= $id where { $id <: $name }), + TsInterfaceDeclaration(id= $id where { $id <: $name }), + TsEnumDeclaration(id= $id where { $id <: $name }), + JsClassDeclaration(id= $id where { $id <: $name }), + JsClassExportDefaultDeclaration(id= $id where { $id <: $name }) + }), + JsImport() as $x where { + $x <: contains JsIdentifierBinding() as $id where { $id <: $name } + } + } +} + +predicate find_alias($node, $name, $parameters, $body) { + not type_parameter_shadowed($node, $name), + $node <: within type_scope(type_entry($name)) as $x, + $x <: type_scope(declared(TsTypeAliasDeclaration(binding_identifier= $id where { + $id <: $name + }, type_parameters=$parameters, ty=$body))) +} + +predicate builtin($node, $name) { + not type_parameter_shadowed($node, $name), + $node <: not within type_scope(type_entry($name)) +} + +pattern utility_of($inner) { + TsReferenceType(name=$n) as $r where { + $n <: r"Readonly|Partial|Required|NonNullable", + builtin($r, $n), + $r <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $inner, ... + ])) + } +} + +pattern alias_reference($name, $parameters, $body) { + TsReferenceType(name=$name) as $r where { + or { + $name <: not r"Record|Readonly|Partial|Required|NonNullable|PropertyKey", + not builtin($r, $name) + }, + find_alias($r, $name, $parameters, $body) + } +} + +pattern broad_key_terminal() { + or { + TsStringType(), + TsNumberType(), + TsSymbolType(), + TsReferenceType(name=$n, type_arguments=.) as $r where { + $n <: r"PropertyKey", + builtin($r, $n) + } + } +} + +pattern broad_key_0() { transparent(broad_key_terminal()) } + +pattern broad_key_via($previous) { + transparent(or { + broad_key_terminal(), + TsUnionType(types=some $previous), + alias_reference($_, ., $b) where { $b <: $previous } + }) +} + +pattern broad_key_1() { broad_key_via(broad_key_0()) } + +pattern broad_key_2() { broad_key_via(broad_key_1()) } + +pattern broad_key_3() { broad_key_via(broad_key_2()) } + +pattern broad_key_4() { broad_key_via(broad_key_3()) } + +pattern defaulted($argument) { + TsTypeParameter(default=TsDefaultTypeClause(ty=$argument)) +} + +predicate parameter_argument($parameters, $arguments, $name, $argument) { + or { + and { + $parameters <: [TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$argument, ...], + and { + $arguments <: not [$_, ...], + $parameters <: [defaulted($argument), ...] + } + } + }, + and { + $parameters <: [$_, TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$_, $argument, ...], + and { + $arguments <: not [$_, $_, ...], + $parameters <: [$_, defaulted($argument), ...] + } + } + }, + and { + $parameters <: [$_, $_, TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$_, $_, $argument, ...], + and { + $arguments <: not [$_, $_, $_, ...], + $parameters <: [$_, $_, defaulted($argument), ...] + } + } + }, + and { + $parameters <: [$_, $_, $_, TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$_, $_, $_, $argument, ...], + and { + $arguments <: not [$_, $_, $_, $_, ...], + $parameters <: [$_, $_, $_, defaulted($argument), ...] + } + } + }, + and { + $parameters <: [$_, $_, $_, $_, TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$_, $_, $_, $_, $argument, ...], + and { + $arguments <: not [$_, $_, $_, $_, $_, ...], + $parameters <: [$_, $_, $_, $_, defaulted($argument), ...] + } + } + }, + and { + $parameters <: [$_, $_, $_, $_, $_, TsTypeParameter(name=$x), ...], + $x <: $name, + or { + $arguments <: [$_, $_, $_, $_, $_, $argument, ...], + and { + $arguments <: not [$_, $_, $_, $_, $_, $_, ...], + $parameters <: [$_, $_, $_, $_, $_, defaulted($argument), ...] + } + } + } + } +} + +pattern parameter_reference($items) { + TsReferenceType(name=$n, type_arguments=.) where { + $items <: some TsTypeParameter(name= $x where { $x <: $n }) + } +} + +pattern substituted_parameter($items, $arguments) { + parameter_reference($items) as $r where { + $r <: TsReferenceType(name=$n), + parameter_argument($items, $arguments, $n, broad_key_4()) + } +} + +pattern broad_key_or($parameter) { + transparent(or { + $parameter, + broad_key_4(), + TsUnionType(types=some or { + $parameter, + broad_key_4() + }) + }) +} + +pattern record_keyed($key) { + TsReferenceType(name=$n) as $r where { + $n <: r"Record", + builtin($r, $n), + $r <: TsReferenceType(type_arguments=or { + ., + TsTypeArguments(ts_type_argument_list=[$key, ...]) + }) + } +} + +pattern open_with($key) { + transparent(or { + TsObjectType(members=some TsIndexSignatureTypeMember()), + TsMappedType(keys_type=$key), + record_keyed($key) + }) +} + +pattern substituted_open($items, $arguments) { + open_with(broad_key_or(substituted_parameter($items, $arguments))) +} + +predicate chained_argument($inner_items, $inner_arguments, $items, $arguments, $name, $result) { + parameter_argument($inner_items, $inner_arguments, $name, $x), + or { + and { + $x <: parameter_reference($items), + $x <: TsReferenceType(name=$y), + parameter_argument($items, $arguments, $y, $result) + }, + and { $x <: not parameter_reference($items), $x <: $result } + } +} + +pattern chained_open($inner_items, $inner_arguments, $items, $arguments) { + open_with(broad_key_or( parameter_reference($inner_items) as $r where { + $r <: TsReferenceType(name=$n), + chained_argument($inner_items, $inner_arguments, $items, $arguments, $n, broad_key_4()) + })) +} + +predicate applied_arguments($parameters, $type_arguments, $items, $arguments) { + $parameters <: TsTypeParameters(items=$items), + or { + $type_arguments <: TsTypeArguments(ts_type_argument_list=$arguments), + and { + $type_arguments <: ., + $items <: every TsTypeParameter(default=TsDefaultTypeClause()), + $arguments = [] + } + } +} + +predicate generic_simple($parameters, $type_arguments, $body) { + applied_arguments($parameters, $type_arguments, $i, $a), + $body <: or { + substituted_open($i, $a), + transparent(utility_of(substituted_open($i, $a))) + } +} + +pattern alias_broad_terminal($kind) { + or { + TsUnknownType() where { $kind = "strict" }, + TsNonPrimitiveType() where { $kind = "strict" }, + TsObjectType(members=some TsIndexSignatureTypeMember()) where { + $kind = "dict" + }, + TsMappedType(keys_type=broad_key_4()) where { $kind = "dict" }, + record_keyed(broad_key_4()) where { $kind = "dict" } + } +} + +predicate generic_open($parameters, $type_arguments, $body) { + applied_arguments($parameters, $type_arguments, $items, $arguments), + $body <: or { + substituted_open($items, $arguments), + transparent(utility_of(substituted_open($items, $arguments))), + transparent( parameter_reference($items) as $r where { + $r <: TsReferenceType(name=$n), + parameter_argument($items, $arguments, $n, alias_broad_4($kind)), + $kind <: "dict" + }), + transparent( alias_reference($_, ., $b) where { + $b <: alias_broad_4($kind), + $kind <: "dict" + }), + transparent( alias_reference($_, TsTypeParameters(items=$inner_items), $b) as $r where { + $r <: TsReferenceType(type_arguments=$t), + or { + $t <: TsTypeArguments(ts_type_argument_list=$inner_arguments), + and { $t <: ., $inner_arguments = [] } + }, + $b <: or { + transparent( parameter_reference($inner_items) as $s where { + $s <: TsReferenceType(name=$n), + chained_argument($inner_items, $inner_arguments, $items, $arguments, $n, alias_broad_4($kind)), + $kind <: "dict" + }), + chained_open($inner_items, $inner_arguments, $items, $arguments), + // `$r` is still bound to the enclosing alias reference, unlike in `utility_of`. + transparent( TsReferenceType(name=$n) as $r where { + $n <: r"Readonly|Partial|Required|NonNullable", + builtin($r, $n), + $r <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + chained_open($inner_items, $inner_arguments, $items, $arguments), + + ... + ])) + }) + } + }) + } +} + +predicate generic_pass($parameters, $type_arguments, $body, $kind, $broad) { + applied_arguments($parameters, $type_arguments, $i, $a), + $body <: transparent( parameter_reference($i) as $r where { + $r <: TsReferenceType(name=$n), + parameter_argument($i, $a, $n, $broad), + $kind <: "dict" + }) +} + +pattern alias_broad_0($kind) { + transparent(or { + alias_broad_terminal($kind), + utility_of(transparent(alias_broad_terminal($kind))) + }) +} + +pattern alias_broad_via($kind, $previous, $pass_kind, $pass_previous) { + transparent(or { + alias_broad_terminal($kind), + utility_of($previous), + alias_reference($_, $p, $b) as $r where { + or { + and { $p <: ., $b <: $previous }, + and { + $r <: TsReferenceType(type_arguments=$t), + or { + generic_simple($p, $t, $b), + generic_pass($p, $t, $b, $pass_kind, $pass_previous) + }, + $kind = "dict" + } + } + } + }) +} + +#repeat 1..4 +pattern alias_broad_{{n}}($kind) { + alias_broad_via($kind, alias_broad_{{n-1}}($kind), $k, alias_broad_{{n-1}}($k)) +} +#end + +pattern direct_target($kind) { + or { + TsUnknownType() where { $kind = "strict" }, + TsNonPrimitiveType() where { $kind = "strict" }, + TsObjectType(members=$t) where { + or { + and { $t <: some TsIndexSignatureTypeMember(), $kind = "dict" }, + and { $t <: [$_, ...], $kind = "strict" } + } + }, + TsMappedType() where { $kind = "dict" }, + record_keyed(broad_key_4()) where { $kind = "dict" }, + alias_reference($_, $p, $b) as $r where { + or { + and { $p <: ., $b <: alias_broad_4($kind) }, + and { + $r <: TsReferenceType(type_arguments=$t), + generic_open($p, $t, $b), + $kind = "dict" + } + } + } + } +} + +pattern target_0($kind) { transparent(direct_target($kind)) } + +pattern target_1($kind) { + transparent(or { + direct_target($kind), + utility_of(target_0($kind)) + }) +} + +#repeat 2..4 +pattern target_{{n}}($kind) { + transparent(or { + direct_target($kind), + utility_of(target_{{n-1}}($kind)) + }) +} +#end + +pattern known_via($previous) { + or { + known_value(), + identifier_value() as $v where { + $v <: contains JsIdentifierExpression(name=$n), + const_initializer($v, $n, $i), + $i <: $previous + } + } +} + +pattern known_1() { known_via(known_value()) } + +pattern known_2() { known_via(known_1()) } + +pattern known_3() { known_via(known_2()) } + +pattern known_4() { known_via(known_3()) } + +pattern known_5() { known_via(known_4()) } + +predicate widens($type, $value) { + $value <: or { + known_value(), + identifier_value() + }, + $type <: target_4($kind), + or { $kind <: "strict", $value <: not empty_object() }, + $value <: known_5() +} + +pattern never_member() { + TsPropertySignatureTypeMember(type_annotation=TsTypeAnnotation(ty=transparent(TsNeverType()))) as $member where { + $member <: r"(?s)[^:]*\?\s*:.*" + } +} + +pattern unsafe_0() { + transparent(or { + TsUnknownType(), + TsAnyType(), + TsNonPrimitiveType(), + TsObjectType(members=every never_member()) + }) +} + +predicate module_interface($node, $name, $declaration) { + $node <: within or { + JsModule(), + JsScript() + } as $x, + $x <: or { + JsModule(items=some declared(TsInterfaceDeclaration(id= $id where { + $id <: $name + }) as $declaration)), + JsScript(statements=some declared(TsInterfaceDeclaration(id= $id where { + $id <: $name + }) as $declaration)) + } +} + +pattern interface_named($name, $id) { + declared(TsInterfaceDeclaration(id= $id where { $id <: $name })) +} + +predicate empty_interface($node, $name) { + module_interface($node, $name, $x), + $x <: TsInterfaceDeclaration(extends_clause=., members=every never_member()), + $node <: within or { + JsModule(), + JsScript() + } as $y, + $y <: not or { + JsModule(items=[ + ..., interface_named($name, $z), ..., interface_named($name, $id), ... + ]), + JsScript(statements=[ + ..., interface_named($name, $z), ..., interface_named($name, $id), ... + ]) + } +} + +pattern unsafe_via($previous) { + transparent(or { + unsafe_0(), + TsUnionType(types=some $previous), + TsIntersectionType(types=$t) where { + or { $t <: some transparent(TsAnyType()), $t <: every not not $previous } + }, + utility_of($previous), + TsReferenceType(name=$n) as $r where { + or { + $n <: not r"Readonly|Partial|Required|NonNullable", + not builtin($r, $n) + }, + or { + empty_interface($r, $n), + and { + not module_interface($r, $n, $_), + find_alias($r, $n, ., $t), + $t <: $previous + } + } + } + }) +} + +pattern unsafe_1() { unsafe_via(unsafe_0()) } + +pattern unsafe_2() { unsafe_via(unsafe_1()) } + +pattern unsafe_3() { unsafe_via(unsafe_2()) } + +pattern informative() { not unsafe_3() } + +pattern argument_evidence($self, $identifier) { + or { + JsParenthesizedExpression(expression=$self), + TsNonNullAssertionExpression(expression=$self), + TsSatisfiesExpression(expression=$self), + TsAsExpression(ty=informative()), + TsTypeAssertionExpression(ty=informative()), + JsCallExpression(callee=$r) where { + local_function($r, $_, TsReturnTypeAnnotation(ty=informative())) + }, + known_terminal(), + $identifier + } +} + +pattern identifier_evidence($previous) { + JsIdentifierExpression(name=$n) as $r where { + or { + and { annotated_binding($r, $n, $a), $a <: informative() }, + and { + not annotated_binding($r, $n, $_), + const_initializer($r, $n, $a), + $a <: $previous + } + } + } +} + +pattern argument_evidence_0() { + argument_evidence(argument_evidence_0(), or {}) +} + +#repeat 1..5 +pattern argument_evidence_{{n}}() { + argument_evidence(argument_evidence_{{n}}(), identifier_evidence(argument_evidence_{{n-1}}())) +} +#end + +pattern contains_unknown() { + or { + TsUnknownType(), + TsParenthesizedType(ty=contains_unknown()), + TsUnionType(types=some contains_unknown()) + } +} + +pattern unknown_parameter($binding) { + JsFormalParameter(binding=$binding, type_annotation=TsTypeAnnotation(ty=contains_unknown())) +} + +predicate predicate_argument($parameters, $arguments, $subject, $argument) { + or { + and { + $parameters <: [unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$argument, ...] + }, + and { + $parameters <: [$_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $argument, ...] + }, + and { + $parameters <: [$_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $argument, ...] + }, + and { + $parameters <: [$_, $_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $_, $argument, ...] + }, + and { + $parameters <: [$_, $_, $_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $_, $_, $argument, ...] + }, + and { + $parameters <: [$_, $_, $_, $_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $_, $_, $_, $argument, ...] + }, + and { + $parameters <: [$_, $_, $_, $_, $_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $_, $_, $_, $_, $argument, ...] + }, + and { + $parameters <: [$_, $_, $_, $_, $_, $_, $_, unknown_parameter($x), ...], + $x <: $subject, + $arguments <: [$_, $_, $_, $_, $_, $_, $_, $argument, ...] + } + } +} + +pattern predicate_shape() { + function_named($_, some JsFormalParameter(type_annotation=TsTypeAnnotation(ty=contains_unknown())), TsReturnTypeAnnotation(ty=or { + TsPredicateReturnType(), + TsAssertsReturnType() + })) +} + +pattern assertion_in_parentheses() { + or { + TsAsExpression(), + TsTypeAssertionExpression(), + JsParenthesizedExpression(expression=assertion_in_parentheses()) + } +} + +#include function_like + +pattern return_type($type) { + or { + JsFunctionDeclaration(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsFunctionExpression(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsArrowFunctionExpression(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsFunctionExportDefaultDeclaration(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsMethodClassMember(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsMethodObjectMember(return_type_annotation=TsReturnTypeAnnotation(ty=$type)), + JsGetterClassMember(return_type=TsTypeAnnotation(ty=$type)), + JsGetterObjectMember(return_type=TsTypeAnnotation(ty=$type)) + } +} + +or { + JsVariableDeclarator(id=JsIdentifierBinding(), variable_annotation=TsTypeAnnotation(ty=$type), initializer=JsInitializerClause(expression=$value)) where { + widens($type, $value) + }, + JsPropertyClassMember(property_annotation=or { + TsTypeAnnotation(ty=$type), + TsOptionalPropertyAnnotation(type_annotation=TsTypeAnnotation(ty=$type)) + }, value=JsInitializerClause(expression=$value)) where { + widens($type, $value) + }, + JsAssignmentExpression(left=JsIdentifierAssignment() as $x, right=$value) as $node where { + $node <: r"(?s)[\w$]+\s*=[^=].*", + $value <: or { + known_value(), + identifier_value() + }, + scope_of($node, $x, $y), + $y <: value_scope(declared(JsVariableDeclaration(declarators=some JsVariableDeclarator(id= JsIdentifierBinding() as $z where { + $z <: $x + }, variable_annotation=TsTypeAnnotation(ty=$type))))), + not_hoisted($node, $x, $y), + widens($type, $value) + }, + JsReturnStatement(argument=$value) as $node where { + $value <: or { + known_value(), + identifier_value() + }, + $node <: within function_like() as $x, + $x <: return_type($type), + widens($type, $value) + }, + JsArrowFunctionExpression(return_type_annotation=TsReturnTypeAnnotation(ty=$type), body=$value) where { + $value <: not JsFunctionBody(), + widens($type, $value) + }, + or { + TsAsExpression(expression=$value, ty=$type), + TsTypeAssertionExpression(expression=$value, ty=$type) + } as $node where { + $value <: or { + known_value(), + identifier_value() + }, + or { + $node <: not within or { + TsAsExpression(), + TsTypeAssertionExpression() + } as $x where { $x <: not $node }, + and { + $node <: within or { + TsAsExpression(), + TsTypeAssertionExpression() + } as $x where { $x <: not $node }, + $x <: not or { + TsAsExpression(expression=assertion_in_parentheses()), + TsTypeAssertionExpression(expression=assertion_in_parentheses()) + } + } + }, + widens($type, $value) + }, + JsCallExpression(callee=$x, arguments=JsCallArguments(args=$y)) as $node where { + $y <: [$_, ...], + or { + $x <: function_value(), + and { + $x <: identifier_value(), + $node <: within or { + JsModule(), + JsScript() + } as $w, + $w <: r"(?s).*\bunknown\b.*\)\s*:\s*(?:asserts\s+[A-Za-z_$][\w$]*|[A-Za-z_$][\w$]*\s+is)\b.*", + $node <: within type_scope(declared(predicate_shape())) + } + }, + local_function($x, $z, TsReturnTypeAnnotation(ty=or { + TsPredicateReturnType(parameter_name=$type), + TsAssertsReturnType(parameter_name=$type) + })), + predicate_argument($z, $y, $type, $value), + $value <: not JsSpread(), + $value <: argument_evidence_5() + } +} where { + register_diagnostic(span=$value, message="This explicit broad type can discard known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.", severity="error") +} diff --git a/tools/biome/src/no-reduce-accumulator-copy.grit b/tools/biome/src/no-reduce-accumulator-copy.grit new file mode 100644 index 00000000..136b6b78 --- /dev/null +++ b/tools/biome/src/no-reduce-accumulator-copy.grit @@ -0,0 +1,429 @@ +engine biome(1.0) +language js(typescript) + +#include wrapped + +pattern wrapped_call() { wrapped(JsCallExpression(), wrapped_call()) } + +pattern wrapped_member() { + wrapped(or { + JsStaticMemberExpression(), + JsComputedMemberExpression() + }, wrapped_member()) +} + +pattern member_call($object, $name) { + JsCallExpression(callee=$callee) where { + $callee <: wrapped_member(), + $callee <: contains or { + JsStaticMemberExpression(), + JsComputedMemberExpression() + } as $member, + $member <: or { + JsStaticMemberExpression(object=$object, member=$name), + JsComputedMemberExpression(object=$object, member=JsStringLiteralExpression() as $literal) where { + $literal <: r"^[\"'](.*)[\"']$"($name) + } + } + } +} + +pattern var_boundary() { + or { + JsParameters(), + JsConstructorParameters(), + JsFunctionBody(), + JsFunctionExpression(), + JsArrowFunctionExpression(), + JsSetterClassMember(), + JsSetterObjectMember(), + JsStaticInitializationBlockClassMember() + } +} + +pattern lexical_boundary() { + or { + var_boundary(), + JsBlockStatement(), + JsSwitchStatement(), + JsForStatement(), + JsForInStatement(), + JsForOfStatement(), + JsCatchClause(), + JsClassExpression() + } +} + +pattern lexical_has($target) { contains $target until lexical_boundary() } + +pattern hoisted_has($target) { + or { + lexical_has($target), + contains or { + JsVariableDeclaration(), + JsForVariableDeclaration() + } as $declaration where { + $declaration <: r"(?s)var\b.*", + $declaration <: lexical_has($target) + } until var_boundary() + } +} + +pattern scope_has($target) { + or { + or { + JsModule(items=$region), + JsScript(statements=$region), + JsStaticInitializationBlockClassMember(statements=$region) + } where { $region <: hoisted_has($target) }, + or { + JsBlockStatement(statements=$region), + JsSwitchStatement(cases=$region), + JsForStatement(initializer=$region), + JsForInStatement(initializer=$region), + JsForOfStatement(initializer=$region), + JsCatchClause(declaration=$region) + } where { $region <: lexical_has($target) }, + or { + JsFunctionExpression(id=$region), + JsClassExpression(id=$region) + } where { $region <: $target }, + or { + JsFunctionDeclaration(parameters=JsParameters(items=$parameters), body=JsFunctionBody(statements=$statements)), + JsFunctionExportDefaultDeclaration(parameters=JsParameters(items=$parameters), body=JsFunctionBody(statements=$statements)), + JsFunctionExpression(parameters=JsParameters(items=$parameters), body=JsFunctionBody(statements=$statements)), + JsArrowFunctionExpression(parameters=or { + JsParameters(items=$parameters), + $parameters + }, body=or { + JsFunctionBody(statements=$statements), + $statements + }), + JsMethodClassMember(parameters=JsParameters(items=$parameters), body=JsFunctionBody(statements=$statements)), + JsMethodObjectMember(parameters=JsParameters(items=$parameters), body=JsFunctionBody(statements=$statements)), + JsConstructorClassMember(parameters=JsConstructorParameters(parameters=$parameters), body=JsFunctionBody(statements=$statements)), + JsSetterClassMember(parameter=$parameters, body=JsFunctionBody(statements=$statements)), + JsSetterObjectMember(parameter=$parameters, body=JsFunctionBody(statements=$statements)) + } where { + or { + $parameters <: lexical_has($target), + $statements <: hoisted_has($target) + } + }, + or { + JsGetterClassMember(body=JsFunctionBody(statements=$statements)), + JsGetterObjectMember(body=JsFunctionBody(statements=$statements)) + } where { $statements <: hoisted_has($target) } + } +} + +pattern named($name) { + JsIdentifierBinding() as $binding where { $binding <: $name } +} + +pattern write_to($name, $scope) { + JsIdentifierAssignment() as $write where { + $write <: $name, + $write <: within scope_has(named($name)) as $owner, + $owner <: $scope + } +} + +pattern wrapped_identifier() { + wrapped(JsIdentifierExpression(), wrapped_identifier()) +} + +pattern unwrapped_identifier($target) { + wrapped_identifier() as $expression where { + $expression <: contains JsIdentifierExpression() as $identifier, + $identifier <: $target + } +} + +#include function_like + +pattern wrapped_object() { wrapped(JsObjectExpression(), wrapped_object()) } + +pattern wrapped_function() { + wrapped(or { + JsArrowFunctionExpression(), + JsFunctionExpression() + }, wrapped_function()) +} + +pattern global($global) { + JsIdentifierExpression(name=$name) as $reference where { + $name <: $global, + $reference <: not within scope_has(named($name)) + } +} + +pattern accumulator_0($callback, $accumulator, $seen) { + JsIdentifierExpression(name=$name) as $reference where { + $name <: $accumulator, + $reference <: within scope_has(named($name)) as $scope, + $scope <: $callback + } +} + +pattern accumulator_alias($seen, $declarator, $init) { + JsIdentifierExpression(name=$name) as $reference where { + $reference <: within scope_has(named($name)) as $scope, + $scope <: scope_has( JsVariableDeclarator(id=named($name)) as $declarator where { + $declarator <: not $seen, + $declarator <: within JsVariableDeclaration() as $statement, + $statement <: r"(?s)const\b.*", + $declarator <: JsVariableDeclarator(initializer=JsInitializerClause(expression=$init)) + }), + $scope <: not contains write_to($name, $scope) + } +} + +pattern accumulator_via($callback, $accumulator, $seen, $declarator, $previous) { + or { + accumulator_0($callback, $accumulator, $seen), + accumulator_alias($seen, $declarator, unwrapped_identifier($previous)) + } +} + +#repeat 1..16 +pattern accumulator_{{n}}($callback, $accumulator, $seen) { + accumulator_via($callback, $accumulator, $seen, $declarator, accumulator_{{n-1}}($callback, $accumulator, or { + $seen, + $declarator + })) +} +#end + +pattern copy_method_name() { + or { + "assign", + "from", + "concat", + "slice", + "toSpliced", + "toSorted", + "toReversed", + "with" + } +} + +pattern array_method_name() { + or { + "map", + "filter", + "flatMap", + "slice", + "concat", + "toSorted", + "toReversed", + "toSpliced" + } +} + +pattern array_annotation() { + or { + TsArrayType(), + TsTupleType(), + TsParenthesizedType(ty=array_annotation()), + TsTypeOperatorType(ty=array_annotation()) as $operator where { + $operator <: r"(?s)readonly\b.*" + }, + TsReferenceType(name=or { + "Array", + "ReadonlyArray" + }) + } +} + +pattern annotated($name, $type) { + or { + JsFormalParameter(binding=named($name), type_annotation=TsTypeAnnotation(ty=$type)), + JsVariableDeclarator(id=named($name), variable_annotation=or { + TsTypeAnnotation(ty=$type), + TsDefiniteVariableAnnotation(type_annotation=TsTypeAnnotation(ty=$type)) + }), + JsCatchDeclaration(binding=named($name), type_annotation=TsTypeAnnotation(ty=$type)) + } +} + +pattern array_method_callee() { + wrapped(or { + JsStaticMemberExpression(object=array_chain(), member=array_method_name()), + JsComputedMemberExpression(object=array_chain(), member=JsStringLiteralExpression() as $literal) where { + $literal <: r"^[\"'](.*)[\"']$"($text), + $text <: array_method_name() + } + }, array_method_callee()) +} + +pattern array_chain() { + wrapped(or { + JsArrayExpression(), + JsIdentifierExpression(), + JsCallExpression(callee=array_method_callee()) + }, array_chain()) +} + +pattern array_identifier($seen, $declarator, $init) { + JsIdentifierExpression(name=$name) as $reference where { + $reference <: within scope_has(named($name)) as $scope, + or { + and { + $scope <: scope_has(annotated($name, array_annotation())), + $scope <: not scope_has(annotated($name, not array_annotation())) + }, + and { + $scope <: not scope_has(annotated($name, $_)), + $scope <: scope_has( JsVariableDeclarator(id=named($name)) as $declarator where { + $declarator <: not $seen, + $declarator <: within JsVariableDeclaration() as $statement, + $statement <: r"(?s)const\b.*", + $declarator <: JsVariableDeclarator(initializer=JsInitializerClause(expression=$init)) + }) + } + }, + $scope <: not contains write_to($name, $scope) + } +} + +pattern known_array_0($seen) { + array_chain() as $expression where { + $expression <: contains or { + JsArrayExpression(), + JsIdentifierExpression() + } as $base, + $base <: JsArrayExpression() + } +} + +pattern known_array_via($seen, $declarator, $previous) { + array_chain() as $expression where { + $expression <: contains or { + JsArrayExpression(), + JsIdentifierExpression() + } as $base, + $base <: or { + JsArrayExpression(), + array_identifier($seen, $declarator, $previous) + } + } +} + +#repeat 1..15 +pattern known_array_{{n}}($seen) { + known_array_via($seen, $declarator, known_array_{{n-1}}(or { + $seen, + $declarator + })) +} +#end + +pattern known_array_16($seen) { + known_array_via($seen, $declarator, known_array_15(or { + $seen, + $declarator + })) +} + +or { + JsCallExpression(callee=or { + JsStaticMemberExpression(member=or { + "map", + "filter" + }), + JsComputedMemberExpression(), + JsParenthesizedExpression(), + TsAsExpression(), + TsTypeAssertionExpression(), + TsNonNullAssertionExpression(), + TsSatisfiesExpression() + }) as $chain where { + $chain <: member_call($inner, $outer), + $outer <: or { + "map", + "filter" + }, + $inner <: wrapped_call(), + $inner <: contains JsCallExpression() as $call, + $call <: member_call($receiver, $first), + or { + and { $outer <: "map", $first <: "filter" }, + and { $outer <: "filter", $first <: "map" } + }, + $receiver <: known_array_16(JsDebuggerStatement()), + register_diagnostic(span=$chain, message="Avoid consecutive array filter/map passes. Use flatMap or a loop, preserving callback order, indexes, and filtering semantics. Explicit lazy iterator pipelines are allowed.", severity="error") + }, + JsCallExpression(callee=or { + JsStaticMemberExpression(member=copy_method_name()), + JsComputedMemberExpression(), + JsParenthesizedExpression(), + TsAsExpression(), + TsTypeAssertionExpression(), + TsNonNullAssertionExpression(), + TsSatisfiesExpression() + }) as $copy where { + $copy <: member_call($owner, $method), + $method <: copy_method_name(), + $copy <: within function_like() as $callback, + $callback <: or { + JsArrowFunctionExpression(parameters=JsParameters(items=[ + JsFormalParameter(binding=JsIdentifierBinding() as $accumulator), + + ... + ])), + JsArrowFunctionExpression(parameters=JsIdentifierBinding() as $accumulator), + JsFunctionExpression(parameters=JsParameters(items=[ + JsFormalParameter(binding=JsIdentifierBinding() as $accumulator), + + ... + ])) + }, + $callback <: within JsCallExpression() as $reducer, + $reducer <: member_call($_, or { + "reduce", + "reduceRight" + }), + $reducer <: JsCallExpression(arguments=JsCallArguments(args=or { + [$first], + [$first, $initial] + })), + $first <: wrapped_function(), + $first <: contains or { + JsArrowFunctionExpression(), + JsFunctionExpression() + } as $function, + $function <: $callback, + $copy <: JsCallExpression(arguments=JsCallArguments(args=$arguments)), + or { + and { + $method <: "assign", + $owner <: unwrapped_identifier(global("Object")), + $arguments <: [wrapped_object(), ...], + $arguments <: some unwrapped_identifier(accumulator_16($callback, $accumulator, JsDebuggerStatement())) + }, + and { + $method <: "from", + $owner <: unwrapped_identifier(global("Array")), + $arguments <: [ + unwrapped_identifier(accumulator_16($callback, $accumulator, JsDebuggerStatement())), + + ... + ] + }, + and { + $method <: or { + "concat", + "slice", + "toSpliced", + "toSorted", + "toReversed", + "with" + }, + $owner <: unwrapped_identifier(accumulator_16($callback, $accumulator, JsDebuggerStatement())), + $initial <: known_array_16(JsDebuggerStatement()) + } + }, + register_diagnostic(span=$copy, message="Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline or flatMap.", severity="error") + } +} diff --git a/tools/biome/src/no-reflection-and-module-mocking.grit b/tools/biome/src/no-reflection-and-module-mocking.grit new file mode 100644 index 00000000..d5cd2eb6 --- /dev/null +++ b/tools/biome/src/no-reflection-and-module-mocking.grit @@ -0,0 +1,138 @@ +engine biome(1.0) +language js(typescript) + +pattern value_declares($name) { + or { + JsFunctionDeclaration(id=$b) where { $b <: $name }, + JsClassDeclaration(id=$b) where { $b <: $name }, + JsExport(export_clause=or { + JsFunctionDeclaration(id=$b), + JsClassDeclaration(id=$b) + }) where { $b <: $name }, + $statement where { + $statement <: not or { + JsFunctionDeclaration(), + JsClassDeclaration() + }, + $statement <: contains JsIdentifierBinding() as $b where { + $b <: $name + } until or { + JsFunctionDeclaration(), + JsFunctionExpression(), + JsArrowFunctionExpression(), + JsClassDeclaration(), + JsClassExpression(), + JsObjectExpression(), + JsBlockStatement(), + JsFunctionBody() + } + } + } +} + +pattern binds_value($name) { + or { + JsModule(items=some value_declares($name)), + JsScript(statements=some value_declares($name)), + JsFunctionBody(statements=some value_declares($name)), + JsBlockStatement(statements=some value_declares($name)), + TsModuleBlock(items=some value_declares($name)), + JsStaticInitializationBlockClassMember(statements=some value_declares($name)), + JsCaseClause(consequent=some value_declares($name)), + JsDefaultClause(consequent=some value_declares($name)), + or { + JsFunctionDeclaration(parameters=$scope), + JsFunctionExpression(parameters=$scope), + JsArrowFunctionExpression(parameters=$scope), + JsMethodClassMember(parameters=$scope), + JsMethodObjectMember(parameters=$scope), + JsConstructorClassMember(parameters=$scope), + JsSetterClassMember(parameter=$scope), + JsSetterObjectMember(parameter=$scope), + JsCatchClause(declaration=$scope), + JsForStatement(initializer=$scope), + JsForInStatement(initializer=$scope), + JsForOfStatement(initializer=$scope) + } where { + $scope <: or { + JsIdentifierBinding() as $b where { $b <: $name }, + contains JsIdentifierBinding() as $b where { $b <: $name } until or { + JsFunctionExpression(), + JsArrowFunctionExpression(), + JsObjectExpression() + } + } + } + } +} + +#include unparenthesized + +// Regex literals are wrapped in `or` because Grit rejects them as bare pattern arguments. +pattern imports_from($local, $module, $export) { + and { contains JsModuleSource() as $source where { $source <: $module }, + contains or { + JsShorthandNamedImportSpecifier(local_name=$binding) where { + $binding <: $export + }, + JsNamedImportSpecifier(name=$imported, local_name=$binding) where { + $imported <: $export + } + } where { $binding <: $local } } +} + +pattern framework_import($local) { + JsImport() as $import where { + $import <: or { + imports_from($local, or { r"[\"']vitest[\"']" }, "vi"), + imports_from($local, or { r"[\"']@jest/globals[\"']" }, "jest") + } + } +} + +pattern member_call($object, $member_name, $quoted_member) { + JsCallExpression(callee=unparenthesized(or { + JsStaticMemberExpression(object=unparenthesized(JsIdentifierExpression(name=$object)), member=$member) where { + $member <: $member_name + }, + JsComputedMemberExpression(object=unparenthesized(JsIdentifierExpression(name=$object)), member=$member) where { + $member <: $quoted_member + } + })) +} + +or { + member_call($name, "apply", or { r"[\"']apply[\"']" }) as $call where { + $name <: "Reflect", + $call <: not within binds_value($name), + register_diagnostic(span=$call, message="Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.", severity="error") + }, + member_call($name, "get", or { r"[\"']get[\"']" }) as $call where { + $name <: "Reflect", + $call <: not within binds_value($name), + register_diagnostic(span=$call, message="Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.", severity="error") + }, + member_call($name, or { + "doMock", + "mock", + "unstable_mockModule" + }, or { r"[\"'](?:doMock|mock|unstable_mockModule)[\"']" }) as $call where { + or { + and { + $name <: or { + "vi", + "jest" + }, + $call <: not within binds_value($name) + }, + and { + $call <: within JsModule(items=some framework_import($name)), + $call <: not within $scope where { + $scope <: binds_value($name), + $scope <: not JsModule() + } + } + }, + register_diagnostic(span=$call, message="Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.", severity="error") + } +} diff --git a/tools/biome/src/no-spread-new-set.grit b/tools/biome/src/no-spread-new-set.grit new file mode 100644 index 00000000..64b72a0f --- /dev/null +++ b/tools/biome/src/no-spread-new-set.grit @@ -0,0 +1,8 @@ +language js(typescript) + +or { + `[...new Set($args)]`, + `[...new Set<$type>($args)]` +} as $array where { + register_diagnostic(span=$array, message="Use `Array.from(new Set(...))` instead of spreading a Set into an array literal.", severity="error") +} diff --git a/tools/biome/src/no-unknown-parameters.grit b/tools/biome/src/no-unknown-parameters.grit new file mode 100644 index 00000000..8665aea9 --- /dev/null +++ b/tools/biome/src/no-unknown-parameters.grit @@ -0,0 +1,74 @@ +engine biome(1.0) +language js(typescript) + +pattern unknown_type() { + or { + TsUnknownType(), + TsParenthesizedType(ty=unknown_type()), + TsUnionType(types=some unknown_type()) + } +} + +pattern guards($name) { + or { + TsPredicateReturnType(parameter_name=$subject), + TsAssertsReturnType(parameter_name=$subject) + } where { $subject <: $name } +} + +pattern guard_return($name) { + or { + TsReturnTypeAnnotation(ty=guards($name)), + guards($name) + } +} + +pattern parameter_owner() { + or { + JsFunctionDeclaration(), + JsFunctionExpression(), + JsArrowFunctionExpression(), + JsFunctionExportDefaultDeclaration(), + TsDeclareFunctionDeclaration(), + TsDeclareFunctionExportDefaultDeclaration(), + JsMethodClassMember(), + JsMethodObjectMember(), + JsConstructorClassMember(), + JsSetterClassMember(), + JsSetterObjectMember(), + TsMethodSignatureClassMember(), + TsMethodSignatureTypeMember(), + TsCallSignatureTypeMember(), + TsConstructSignatureTypeMember(), + TsConstructorSignatureClassMember(), + TsSetterSignatureClassMember(), + TsSetterSignatureTypeMember(), + TsFunctionType(), + TsConstructorType() + } +} + +or { + JsFormalParameter(binding=$binding, type_annotation=TsTypeAnnotation(ty=$type)), + JsRestParameter(binding=$binding, type_annotation=TsTypeAnnotation(ty=$type)), + TsThisParameter(type_annotation=TsTypeAnnotation(ty=$type)) as $binding +} as $parameter where { + $type <: unknown_type(), + $binding <: not "cause", + $parameter <: within parameter_owner() as $owner, + $owner <: not or { + JsFunctionDeclaration(return_type_annotation=guard_return($binding)), + JsFunctionExpression(return_type_annotation=guard_return($binding)), + JsArrowFunctionExpression(return_type_annotation=guard_return($binding)), + JsFunctionExportDefaultDeclaration(return_type_annotation=guard_return($binding)), + TsDeclareFunctionDeclaration(return_type_annotation=guard_return($binding)), + TsDeclareFunctionExportDefaultDeclaration(return_type_annotation=guard_return($binding)), + JsMethodClassMember(return_type_annotation=guard_return($binding)), + JsMethodObjectMember(return_type_annotation=guard_return($binding)), + TsMethodSignatureClassMember(return_type_annotation=guard_return($binding)), + TsMethodSignatureTypeMember(return_type_annotation=guard_return($binding)), + TsCallSignatureTypeMember(return_type_annotation=guard_return($binding)), + TsFunctionType(return_type=guard_return($binding)) + }, + register_diagnostic(span=$type, message="This parameter leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.", severity="error") +} diff --git a/tools/biome/src/no-unsafe-dictionary-type.grit b/tools/biome/src/no-unsafe-dictionary-type.grit new file mode 100644 index 00000000..a0b055ab --- /dev/null +++ b/tools/biome/src/no-unsafe-dictionary-type.grit @@ -0,0 +1,476 @@ +language js(typescript) + +pattern scope_body($body) { + or { + JsModule(items=$body), + JsFunctionBody(statements=$body), + JsBlockStatement(statements=$body), + TsModuleBlock(items=$body), + JsStaticInitializationBlockClassMember(statements=$body) + } +} + +pattern declaration($declaration) { + or { + $declaration, + JsExport(export_clause=$declaration), + JsExport(export_clause=JsExportDefaultDeclarationClause(declaration=$declaration)) + } +} + +pattern binding_named($name) { + or { + declaration(or { + TsTypeAliasDeclaration(binding_identifier=$name), + TsInterfaceDeclaration(id=$name), + TsEnumDeclaration(id=$name), + JsClassDeclaration(id=$name) + }), + JsImport(import_clause=contains or { + JsNamedImportSpecifier(local_name=$name), + JsShorthandNamedImportSpecifier(local_name=$name), + JsDefaultImportSpecifier(local_name=$name), + JsNamespaceImportSpecifier(local_name=$name) + }) + } +} + +pattern type_parameter_owner($name) { + or { + TsTypeAliasDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsInterfaceDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsClassDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsClassExpression(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsClassExportDefaultDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsFunctionDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsFunctionExpression(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsFunctionExportDefaultDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsArrowFunctionExpression(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsDeclareFunctionDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsDeclareFunctionExportDefaultDeclaration(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsMethodClassMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + JsMethodObjectMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsMethodSignatureClassMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsMethodSignatureTypeMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsCallSignatureTypeMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsConstructSignatureTypeMember(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsFunctionType(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))), + TsConstructorType(type_parameters=TsTypeParameters(items=some TsTypeParameter(name=$name))) + } +} + +pattern scope_binding($name) { + or { + JsFunctionBody(statements=some binding_named($name)), + JsBlockStatement(statements=some binding_named($name)), + TsModuleBlock(items=some binding_named($name)), + JsStaticInitializationBlockClassMember(statements=some binding_named($name)) + } +} + +pattern type_parameter_bound($name) { + or { + within type_parameter_owner($name), + within TsMappedType(property_name=$name) + } +} + +pattern shadowed($name) { + or { + within JsModule(items=some binding_named($name)), + within scope_binding($name), + type_parameter_bound($name) + } +} + +pattern alias_shadow_without_infer($name) { + or { + scope_binding($name), + type_parameter_owner($name), + TsMappedType(property_name=$name) + } +} + +pattern alias_shadow($name) { + or { + alias_shadow_without_infer($name), + TsConditionalType(extends_type=contains TsInferType(name=$name)) + } +} + +pattern builtin($names) { + TsReferenceType(name=JsReferenceIdentifier() as $name) as $reference where { + $name <: $names, + $reference <: not shadowed($name) + } +} + +pattern visible_alias($name, $parameters, $aliased) { + $reference where { + $reference <: within scope_body($body) where { + $body <: some declaration( TsTypeAliasDeclaration(binding_identifier=$binding, type_parameters=$parameters, ty=$aliased) where { + $binding <: $name + }) + }, + $reference <: not type_parameter_bound($name) + } +} + +pattern optional_never_member() { + TsPropertySignatureTypeMember(type_annotation=TsTypeAnnotation(ty=TsNeverType())) as $member where { + $member <: r"(?s)[^:]*\?\s*:.*" + } +} + +pattern empty_object() { + TsObjectType(members=$members) where { + $members <: every optional_never_member() + } +} + +pattern empty_interface($name) { + within JsModule(items=$items) where { + $items <: some declaration( TsInterfaceDeclaration(id=$binding, extends_clause=., members=$members) as $interface where { + $binding <: $name, + $members <: every optional_never_member() + }), + $items <: not some declaration( TsInterfaceDeclaration(id=$other) as $second where { + $other <: $name, + $second <: not $interface + }) + } +} + +pattern wrapper_names() { + or { + `Readonly`, + `Partial`, + `Required`, + `NonNullable` + } +} + +pattern readonly_operator($inner) { + TsTypeOperatorType(ty=$inner) as $operator where { + $operator <: r"(?s)readonly\s.*" + } +} + +pattern wrapper_of($inner) { + builtin(wrapper_names()) as $wrapper where { + $wrapper <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $inner, ... + ])) + } +} + +pattern reference_to($parameter) { + TsReferenceType(name=$name, type_arguments=.) where { $name <: $parameter } +} + +pattern value_slot($parameter) { + or { + reference_to($parameter), + TsParenthesizedType(ty=reference_to($parameter)), + wrapper_of(or { + reference_to($parameter), + TsParenthesizedType(ty=reference_to($parameter)) + }) + } +} + +pattern forwards_parameter($parameter) { + or { + reference_to($parameter), + TsReferenceType(name=$identity, type_arguments=TsTypeArguments(ts_type_argument_list=[ + $identity_argument + ])) as $identity_reference where { + $identity_reference <: visible_alias($identity, TsTypeParameters(items=[ + TsTypeParameter(name=$identity_parameter) + ]), $identity_body), + $identity_body <: value_slot($identity_parameter), + $identity_argument <: reference_to($parameter) + } + } +} + +pattern record_of($value) { + builtin(`Record`) as $record where { + $record <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $_, $value + ])) + } +} + +pattern record_slot($parameter) { record_of(value_slot($parameter)) } + +pattern dictionary_slot($parameter) { + or { + record_slot($parameter), + TsParenthesizedType(ty=record_slot($parameter)), + readonly_operator(record_slot($parameter)), + wrapper_of(or { + record_slot($parameter), + TsParenthesizedType(ty=record_slot($parameter)) + }) + } +} + +// Applies an alias whose body is `$position`, or forwards an argument into a dictionary +// slot holding `$value` or a value slot holding `$position`. Each is matched at most once. +pattern applied($arguments, $parameters, $aliased, $value, $position) { + $_ where { + or { + $aliased <: $position, + and { + $parameters <: TsTypeParameters(items=[ + TsTypeParameter(name=$q1, default=$d1), ... + ]), + or { + $arguments <: TsTypeArguments(ts_type_argument_list=[$c1, ...]), + $d1 <: TsDefaultTypeClause(ty=$c1) + }, + or { + or { + and { $aliased <: dictionary_slot($q1), $c1 <: $value }, + and { $aliased <: value_slot($q1), $c1 <: $position } + }, + and { + $parameters <: TsTypeParameters(items=[ + $_, TsTypeParameter(name=$q2, default=$d2), ... + ]), + or { + $arguments <: TsTypeArguments(ts_type_argument_list=[ + $_, $c2, ... + ]), + and { + $d2 <: TsDefaultTypeClause(ty=$x2), + or { + and { $x2 <: forwards_parameter($q1), $c2 = $c1 }, + $c2 = $x2 + } + } + }, + or { + and { $aliased <: dictionary_slot($q2), $c2 <: $value }, + and { $aliased <: value_slot($q2), $c2 <: $position } + } + } + } + } + } + } +} + +pattern aliased($value, $position) { + TsReferenceType(name=JsReferenceIdentifier() as $name, type_arguments=$arguments) as $reference where { + $reference <: visible_alias($name, $parameters, $aliased), + $reference <: applied($arguments, $parameters, $aliased, $value, $position) + } +} + +// `$self` recurses within one level; `every` matches it under double negation so that +// intersection members are not compared with the first member's binding. +pattern unsafe_value($self, $aliased_value) { + or { + TsUnknownType(), + TsAnyType(), + TsNonPrimitiveType(), + empty_object(), + TsParenthesizedType(ty=$self), + readonly_operator($self), + TsUnionType(types=some $self), + TsIntersectionType(types=$members) where { + or { + $members <: some or { + TsAnyType(), + TsParenthesizedType(ty=TsAnyType()) + }, + $members <: every not not $self + } + }, + wrapper_of($self), + TsReferenceType(name=$name, type_arguments=.) where { + $name <: not wrapper_names(), + $name <: empty_interface($name) + }, + $aliased_value + } +} + +pattern unsafe_dictionary($value, $self, $position) { + or { + TsObjectType(members=some TsIndexSignatureTypeMember(type_annotation=TsTypeAnnotation(ty=$value))), + TsMappedType(mapped_type=TsTypeAnnotation(ty=$value)), + TsParenthesizedType(ty=$self), + readonly_operator($self), + wrapper_of($position), + record_of($value), + builtin(or { + `Pick`, + `Omit` + }) as $picked where { + $picked <: TsReferenceType(type_arguments=TsTypeArguments(ts_type_argument_list=[ + $source, ... + ])), + $source <: $position + } + } +} + +pattern dictionary_position($dictionary, $self, $aliased) { + or { + $dictionary, + TsParenthesizedType(ty=$self), + $aliased + } +} + +#repeat 0..2 +pattern unsafe_value_{{n}}() { + unsafe_value(unsafe_value_{{n}}(), aliased(or {}, unsafe_value_{{n+1}}())) +} + +pattern unsafe_dictionary_{{n}}() { + unsafe_dictionary(unsafe_value_{{n}}(), unsafe_dictionary_{{n}}(), dictionary_position_{{n}}()) +} + +pattern dictionary_position_{{n}}() { + dictionary_position(unsafe_dictionary_{{n}}(), dictionary_position_{{n}}(), aliased(unsafe_value_{{n+1}}(), dictionary_position_{{n+1}}())) +} +#end + +pattern unsafe_value_3() { unsafe_value(unsafe_value_3(), or {}) } + +pattern unsafe_dictionary_3() { + unsafe_dictionary(unsafe_value_3(), unsafe_dictionary_3(), dictionary_position_3()) +} + +pattern dictionary_position_3() { + dictionary_position(unsafe_dictionary_3(), dictionary_position_3(), or {}) +} + +pattern outermost() { + $dictionary where { + $dictionary <: not within TsTypeConstraintClause(), + $dictionary <: not within or { + TsReferenceType() as $outer where { + $outer <: not $dictionary, + $outer <: dictionary_position_0() + }, + or { + TsObjectType(), + TsMappedType() + } as $outer where { + $outer <: unsafe_dictionary_0(), + $outer <: not $dictionary + } + } + } +} + +pattern alias_reference() { + bubble TsReferenceType(type_arguments=$arguments) as $dictionary where { + or { + $arguments <: TsTypeArguments(), + $dictionary <: within TsTypeAliasDeclaration() + }, + $dictionary <: dictionary_position_0(), + $dictionary <: outermost(), + register_diagnostic(span=$dictionary, message="This dictionary value type gives callers no concrete contract. Use an owner/schema-derived value type and parse external payloads before insertion.", severity="error") + } +} + +pattern resolved_alias_reference() { + bubble TsReferenceType(name=$name, type_arguments=$arguments) as $dictionary where { + or { + $arguments <: TsTypeArguments(), + $dictionary <: within TsTypeAliasDeclaration() + }, + $dictionary <: not type_parameter_bound($name), + $dictionary <: outermost(), + register_diagnostic(span=$dictionary, message="This dictionary value type gives callers no concrete contract. Use an owner/schema-derived value type and parse external payloads before insertion.", severity="error") + } +} + +pattern alias_may_be_dictionary($parameters, $aliased) { + $_ where { + or { + $aliased <: dictionary_position_1(), + and { + $parameters <: TsTypeParameters(items=[TsTypeParameter(name=$q1), ...]), + $aliased <: or { + dictionary_slot($q1), + value_slot($q1) + } + }, + and { + $parameters <: TsTypeParameters(items=[ + $_, TsTypeParameter(name=$q2), ... + ]), + $aliased <: or { + dictionary_slot($q2), + value_slot($q2) + } + } + } + } +} + +or { + or { + TsReferenceType(name=$candidate_name) where { + $candidate_name <: r"Record|Pick|Omit|Readonly|Partial|Required|NonNullable" + }, + TsObjectType(), + TsMappedType() + } as $dictionary where { + $dictionary <: unsafe_dictionary_0(), + $dictionary <: outermost(), + register_diagnostic(span=$dictionary, message="This dictionary value type gives callers no concrete contract. Use an owner/schema-derived value type and parse external payloads before insertion.", severity="error") + }, + TsTypeAliasDeclaration(binding_identifier=$alias, type_parameters=$parameters, ty=$aliased) as $declaration where { + $aliased <: or { + TsReferenceType(), + TsObjectType(), + TsMappedType(), + TsParenthesizedType(), + TsTypeOperatorType() + }, + or { + and { + $parameters <: ., + $aliased <: dictionary_position_1(), + $declaration <: within scope_body($items), + maybe $items <: contains and { TsReferenceType(name=$alias), + resolved_alias_reference() } until alias_shadow($alias) + }, + and { + $parameters <: TsTypeParameters(), + $declaration <: alias_may_be_dictionary($parameters, $aliased), + $declaration <: within scope_body($items), + maybe $items <: contains and { TsReferenceType(name=$alias), + alias_reference() } until alias_shadow($alias) + } + }, + maybe and { + $items <: contains TsInferType(name=$alias), + $items <: contains and { TsConditionalType(extends_type=contains TsInferType(name=$alias)), + bubble TsConditionalType(check_type=$checked, extends_type=contains TsInferType(name=$inferred), false_type=$otherwise) where { + maybe $checked <: contains and { TsReferenceType(name=$inferred), + alias_reference() }, + maybe $otherwise <: contains and { TsReferenceType(name=$inferred), + alias_reference() } + } } until alias_shadow_without_infer($alias) + } + }, + or { + TsIndexSignatureTypeMember(type_annotation=TsTypeAnnotation(ty=$value)) as $dictionary where { + $dictionary <: not within TsObjectType() + }, + TsIndexSignatureClassMember(type_annotation=TsTypeAnnotation(ty=$value)) as $dictionary + } where { + $value <: unsafe_value_0(), + register_diagnostic(span=$dictionary, message="This dictionary value type gives callers no concrete contract. Use an owner/schema-derived value type and parse external payloads before insertion.", severity="error") + } +} diff --git a/tools/check-intents.ts b/tools/check-intents.ts index fc892a77..26f8f077 100644 --- a/tools/check-intents.ts +++ b/tools/check-intents.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; -import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, readFileSync } from "node:fs"; +import { type Package, releasedPackages } from "./packages.ts"; const git = (...args: string[]): string => execFileSync("git", args, { encoding: "utf8" }); @@ -24,28 +24,7 @@ if ( } const base = git("merge-base", arg, "HEAD").trim(); -interface Manifest { - name: string; - private?: boolean; - files?: string[]; -} -interface Package { - dir: string; - manifestPath: string; - manifest: Manifest; -} - -const packages: Package[] = readdirSync("packages") - .map((dir) => ({ - dir: `packages/${dir}`, - manifestPath: join("packages", dir, "package.json"), - })) - .filter((p) => existsSync(p.manifestPath)) - .map((p) => { - const manifest: Manifest = JSON.parse(readFileSync(p.manifestPath, "utf8")); - return { ...p, manifest }; - }) - .filter((p) => !p.manifest.private); +const packages = releasedPackages(); const changed = new Set( [ @@ -64,9 +43,7 @@ const withoutVersion = (text: string): string => { function shippedChange(pkg: Package): string | undefined { for (const file of changed) { - if (!file.startsWith(`${pkg.dir}/`)) continue; - const rel = file.slice(pkg.dir.length + 1); - if (rel === "package.json") { + if (file === `${pkg.dir}/package.json`) { const before = tryGit("show", `${base}:${file}`); const after = existsSync(file) ? readFileSync(file, "utf8") : undefined; if ( @@ -76,8 +53,8 @@ function shippedChange(pkg: Package): string | undefined { ) return file; } else if ( - (pkg.manifest.files ?? []).some( - (entry) => rel === entry || rel.startsWith(`${entry}/`), + pkg.shipped.some( + (entry) => file === entry || file.startsWith(`${entry}/`), ) ) { return file; @@ -120,7 +97,7 @@ const covered = new Set(intentTexts.flatMap(intentTargets)); const missing = packages.flatMap((pkg) => { const file = shippedChange(pkg); - return file && !covered.has(pkg.manifest.name) && !covered.has(pkg.dir) + return file && !covered.has(pkg.name) && !covered.has(pkg.dir) ? [{ pkg, file }] : []; }); @@ -131,11 +108,10 @@ if (missing.length === 0) { } console.error( - `check-intents: published packages changed since ${base.slice(0, 7)} without a change intent:\n`, + `check-intents: released packages changed since ${base.slice(0, 7)} without a change intent:\n`, ); -for (const { pkg, file } of missing) - console.error(` ${pkg.manifest.name} (${file})`); -const names = missing.map(({ pkg }) => pkg.manifest.name).join(" "); +for (const { pkg, file } of missing) console.error(` ${pkg.name} (${file})`); +const names = missing.map(({ pkg }) => pkg.name).join(" "); console.error(` Record one and commit it with the change: diff --git a/tools/gen-pg-functions.ts b/tools/gen-pg-functions.ts index 6062e6a3..083456d9 100644 --- a/tools/gen-pg-functions.ts +++ b/tools/gen-pg-functions.ts @@ -1,47 +1,56 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import { createRequire } from "node:module"; +// Writes PgFunctions, the pg_catalog functions FunctionCatalog does not type, from a +// PostgreSQL server. usage: node tools/gen-pg-functions.ts [--check] +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { + isInterfaceDeclaration, + isPropertySignatureDeclaration, +} from "typescript/ast"; +import { API } from "typescript/sync"; +import { emit, withPostgres } from "./generated.ts"; -const require = createRequire( - new URL("../packages/core/package.json", import.meta.url), +const catalog = new URL( + "../packages/core/src/postgres/engine/typer/catalog.ts", + import.meta.url, +); +const OUTPUT = new URL( + "../packages/core/src/postgres/engine/typer/pg-functions.ts", + import.meta.url, ); -interface PgClient { - connect(): Promise; - query(text: string): Promise<{ rows: R }>; - end(): Promise; -} -const { - Client, -}: { Client: new (config: { connectionString: string }) => PgClient } = - require("pg"); -const url = process.env.TYPESQL_PG_URL; -if (!url) { - console.error( - "set TYPESQL_PG_URL (e.g. docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=x postgres:18)", +/** The functions FunctionCatalog types by hand. */ +function curatedFunctions(): Set { + const api = new API(); + const lease = api.createSourceFile( + fileURLToPath(catalog), + readFileSync(catalog, "utf8"), ); - process.exit(2); + try { + const declaration = lease.sourceFile.statements.find( + (s) => isInterfaceDeclaration(s) && s.name.text === "FunctionCatalog", + ); + if (!declaration || !isInterfaceDeclaration(declaration)) + throw new Error("catalog.ts declares no FunctionCatalog interface"); + return new Set( + declaration.members.flatMap((member) => + isPropertySignatureDeclaration(member) ? [member.name.getText()] : [], + ), + ); + } finally { + lease.dispose(); + api.close(); + } } -const catalogSrc = readFileSync( - new URL( - "../packages/core/src/postgres/engine/typer/catalog.ts", - import.meta.url, - ), - "utf8", -); -const start = catalogSrc.indexOf("export interface FunctionCatalog"); -const body = catalogSrc.slice(start, catalogSrc.indexOf("\n}\n", start)); -const curated = new Set( - [...body.matchAll(/(?:^|; )\s*([a-z_][a-z0-9_]*): (?:\[\s*)?E?Sig m[1]!, - ), -); - -const client = new Client({ connectionString: url }); -await client.connect(); -const { rows } = await client.query< - { name: string; srf: boolean; kind: string; strict: boolean; ret: string }[] ->(` +const curated = curatedFunctions(); +const rows = await withPostgres(async (client) => { + const result = await client.query<{ + name: string; + srf: boolean; + kind: string; + strict: boolean; + ret: string; + }>(` WITH bad AS ( SELECT oid FROM pg_type WHERE typname IN ('internal', 'cstring', 'trigger', 'event_trigger', 'language_handler', 'fdw_handler', @@ -67,7 +76,8 @@ const { rows } = await client.query< AND NOT EXISTS (SELECT 1 FROM pg_conversion cv WHERE cv.conproc = p.oid) AND p.proname !~ '^(_|binary_upgrade_)' ORDER BY p.proname, p.oid`); -await client.end(); + return result.rows; +}); const fns = new Map< string, @@ -96,7 +106,7 @@ for (const r of rows) { fns.set(r.name, f); } -let out = `import type { Sig } from "./catalog.js"; +let out = `import type { Sig } from "../../schema/functions.js"; export interface PgFunctions { `; @@ -111,13 +121,8 @@ for (const [name, f] of fns) { out += ` ${name}: Sig<["..."], ${JSON.stringify(ret)}${flags.length ? `, { ${flags.join("; ")} }` : ""}>;\n`; } out += "}\n"; -writeFileSync( - new URL( - "../packages/core/src/postgres/engine/typer/pg-functions.ts", - import.meta.url, - ), +emit( + OUTPUT, out, -); -console.log( - `wrote ${fns.size} functions (${curated.size} curated in FunctionCatalog)`, + `${fns.size} functions, ${curated.size} curated in FunctionCatalog`, ); diff --git a/tools/generated.ts b/tools/generated.ts new file mode 100644 index 00000000..05411049 --- /dev/null +++ b/tools/generated.ts @@ -0,0 +1,30 @@ +// Shared by the generators of PostgreSQL catalog sources in @typesql/core. +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import pg from "pg"; +import { serverUrl } from "./servers.ts"; + +/** Connects to `TYPESQL_DATABASE_URL`, or to the PostgreSQL server of tests/compose.yaml. */ +export async function withPostgres( + fn: (client: pg.Client) => Promise, +): Promise { + const client = new pg.Client({ connectionString: serverUrl("postgres") }); + await client.connect(); + try { + return await fn(client); + } finally { + await client.end(); + } +} + +/** Writes a generated source, or with `--check` fails when the committed one differs. */ +export function emit(output: URL, source: string, summary: string) { + const path = fileURLToPath(output); + if (!process.argv.includes("--check")) { + writeFileSync(output, source); + console.log(`wrote ${path} (${summary})`); + } else if (readFileSync(output, "utf8") !== source) { + console.error(`${path} is out of date; run without --check to regenerate`); + process.exitCode = 1; + } +} diff --git a/tools/github-releases.ts b/tools/github-releases.ts index d6d76a81..c03f9fb2 100644 --- a/tools/github-releases.ts +++ b/tools/github-releases.ts @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { readFileSync } from "node:fs"; +import { releasedPackages } from "./packages.ts"; const dryRun = process.argv.includes("--dry-run"); const currentOnly = process.argv.includes("--current"); @@ -103,20 +104,10 @@ for (const { path, commit, time } of changedFiles("A", [ const changelogs = new Map(); const currentVersions = new Set(); -for (const dir of readdirSync("packages")) { - const manifest = `packages/${dir}/package.json`; - if (!existsSync(manifest)) continue; - const { - name, - version, - private: isPrivate, - }: { name: string; version: string; private?: boolean } = JSON.parse( - readFileSync(manifest, "utf8"), - ); - if (!isPrivate) { - changelogs.set(`packages/${dir}/CHANGELOG.md`, name); - currentVersions.add(`${name}@${version}`); - } +for (const { name, tag, dir, registry } of releasedPackages()) { + if (registry !== "npm") continue; + changelogs.set(`${dir}/CHANGELOG.md`, name); + currentVersions.add(tag); } for (const { path, commit, time } of changedFiles("AM", [ ...changelogs.keys(), diff --git a/tools/packages.ts b/tools/packages.ts new file mode 100644 index 00000000..404f16c0 --- /dev/null +++ b/tools/packages.ts @@ -0,0 +1,110 @@ +// The packages a release publishes, read from their manifests. Runs without an install. +// usage: node tools/packages.ts [name|version|tag|dir ...] +// prints the given fields (default: tag, i.e. name@version) of each package, one per line + +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +interface Manifest { + name: string; + version: string; + private?: boolean; + files?: string[]; + engines?: { vscode?: string }; + devDependencies?: Record; +} + +export interface Package { + name: string; + version: string; + /** `name@version`, the package's git tag and npm specifier. */ + tag: string; + /** Relative to the repository root, like the paths git prints. */ + dir: string; + /** npm packages are public; a VS Code extension is private to npm and has `engines.vscode`. */ + registry: "npm" | "vscode"; + /** Files and directories, relative to the repository root, that end up in the published package. */ + shipped: string[]; +} + +const root = fileURLToPath(new URL("..", import.meta.url)); + +export function releasedPackages(): Package[] { + const manifests = readdirSync(join(root, "packages")).flatMap((entry) => { + const dir = `packages/${entry}`; + const path = join(root, dir, "package.json"); + if (!existsSync(path)) return []; + const manifest: Manifest = JSON.parse(readFileSync(path, "utf8")); + return [{ dir, manifest }]; + }); + const dirs = new Map(manifests.map((m) => [m.manifest.name, m.dir])); + return manifests.flatMap(({ dir, manifest }): Package[] => { + const { name, version } = manifest; + const common = { name, version, tag: `${name}@${version}`, dir }; + if (manifest.engines?.vscode) + return [ + { + ...common, + registry: "vscode", + shipped: extensionFiles(dir, manifest, dirs), + }, + ]; + if (manifest.private) return []; + const shipped = (manifest.files ?? []).map((file) => `${dir}/${file}`); + return [{ ...common, registry: "npm", shipped }]; + }); +} + +/** + * What `vsce package` includes: the `!` entries of .vscodeignore, and what the build bundles + * into `dist`: `src` and the sources of its workspace dependencies. Those packages' own + * dependencies don't count: the bundle takes little of core, and every core change would + * ask for an intent. The release writes CHANGELOG.md, so it is no change of its own. + */ +function extensionFiles( + dir: string, + manifest: Manifest, + dirs: ReadonlyMap, +): string[] { + const ignore = readFileSync(join(root, dir, ".vscodeignore"), "utf8"); + const bundled = Object.entries(manifest.devDependencies ?? {}).flatMap( + ([name, version]) => { + const source = dirs.get(name); + return version.startsWith("workspace:") && source + ? [`${source}/src`] + : []; + }, + ); + return [ + `${dir}/src`, + ...bundled, + ...ignore + .split("\n") + .filter((line) => line.startsWith("!")) + .map((line) => `${dir}/${line.slice(1).replace(/\/\*\*$/, "")}`) + .filter((file) => file !== `${dir}/CHANGELOG.md`), + ]; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const [registry, ...fields] = process.argv.slice(2); + const isField = (field: string) => + field === "name" || + field === "version" || + field === "tag" || + field === "dir"; + const selected = (fields.length ? fields : ["tag"]).filter(isField); + if ( + (registry !== "npm" && registry !== "vscode") || + selected.length !== Math.max(fields.length, 1) + ) { + console.error( + "usage: node tools/packages.ts [name|version|tag|dir ...]", + ); + process.exit(2); + } + for (const pkg of releasedPackages()) + if (pkg.registry === registry) + console.log(selected.map((field) => pkg[field]).join(" ")); +} diff --git a/tools/pg-catalog-from-pg.ts b/tools/pg-catalog-from-pg.ts index c4db7362..59022df6 100644 --- a/tools/pg-catalog-from-pg.ts +++ b/tools/pg-catalog-from-pg.ts @@ -1,16 +1,11 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { fileURLToPath } from "node:url"; -import { ViewNullability } from "../packages/pg/src/introspect/nullability.ts"; +// Writes PgCatalog, the pg_catalog and information_schema relations a query can read, +// from a PostgreSQL server. usage: node tools/pg-catalog-from-pg.ts [--check] +import { columnSource, propertyKey as key } from "@typesql/core/adapter"; +import { introspect } from "@typesql/pg/introspect"; +import type pg from "pg"; +import { emit, withPostgres } from "./generated.ts"; -export interface PgClient { - query( - text: string, - values?: unknown[], - ): Promise<{ rows: R }>; -} - -export const RELATIONS: readonly (readonly [schema: string, name: string])[] = [ +const RELATIONS: readonly (readonly [schema: string, name: string])[] = [ ["pg_catalog", "pg_am"], ["pg_catalog", "pg_attrdef"], ["pg_catalog", "pg_attribute"], @@ -62,7 +57,8 @@ export const RELATIONS: readonly (readonly [schema: string, name: string])[] = [ ["information_schema", "views"], ]; -export const FUNCTION_COLUMNS_NOT_NULL = new Set([ +/** View columns from set-returning functions that never return NULL. */ +const FUNCTION_COLUMNS_NOT_NULL = new Set([ "pg_stat_activity.pid", "pg_stat_replication.pid", "pg_locks.locktype", @@ -86,12 +82,15 @@ export const FUNCTION_COLUMNS_NOT_NULL = new Set([ ]); async function columnsOf( - client: PgClient, + client: pg.Client, relid: string, ): Promise<{ name: string; type: string; notnull: boolean }[]> { - const { rows } = await client.query< - { name: string; notnull: boolean; typname: string; elem: string | null }[] - >( + const { rows } = await client.query<{ + name: string; + notnull: boolean; + typname: string; + elem: string | null; + }>( `SELECT a.attname AS name, a.attnotnull AS notnull, CASE WHEN t.typtype = 'd' THEN bt.typname ELSE t.typname END AS typname, CASE WHEN t.typtype = 'd' THEN bte.typname ELSE te.typname END AS elem @@ -112,27 +111,22 @@ async function columnsOf( })); } -const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/; -const key = (name: string): string => - IDENT.test(name) ? name : JSON.stringify(name); - -export async function generatePgCatalog(client: PgClient): Promise { - const views = new ViewNullability( - (text, values) => client.query(text, values), - { - notNull: (view, column) => - FUNCTION_COLUMNS_NOT_NULL.has(`${view}.${column}`), - }, - ); - const { - rows: [{ version }], - } = await client.query<[{ version: string }]>( - `SELECT current_setting('server_version') AS version`, +async function generatePgCatalog(client: pg.Client): Promise { + // View columns are as nullable as @typesql/pg/introspect infers from their queries. + const { version, relations } = await introspect(client, { + schemas: Array.from(new Set(RELATIONS.map(([schema]) => schema))), + viewNotNull: (view, column) => + FUNCTION_COLUMNS_NOT_NULL.has(`${view}.${column}`), + }); + const viewNotNull = new Map( + relations + .filter((r) => r.kind === "view") + .map((r) => [`${r.schema}.${r.name}`, r.columns.map((c) => !c.nullable)]), ); const namespaces = new Map(); for (const [schema, name] of RELATIONS) { - const { rows } = await client.query<{ oid: string; relkind: string }[]>( + const { rows } = await client.query<{ oid: string; relkind: string }>( `SELECT c.oid::int8::text AS oid, c.relkind FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = $1 AND c.relname = $2`, [schema, name], @@ -144,10 +138,12 @@ export async function generatePgCatalog(client: PgClient): Promise { const { oid, relkind } = rows[0]!; const cols = await columnsOf(client, oid); const notnull = - relkind === "v" ? await views.notNull(oid) : cols.map((c) => c.notnull); + relkind === "v" + ? viewNotNull.get(`${schema}.${name}`)! + : cols.map((c) => c.notnull); const body = cols.map( (c, i) => - ` ${key(c.name)}: Col<${JSON.stringify(c.type)}${notnull[i] ? "" : ", { nullable: true }"}>;`, + ` ${key(c.name)}: ${columnSource(JSON.stringify(c.type), { nullable: !notnull[i] }, "postgres")};`, ); const lines = [` ${key(name)}: View<{`, ...body, " }>;"]; @@ -172,46 +168,13 @@ export async function generatePgCatalog(client: PgClient): Promise { ].join("\n"); } -export const OUTPUT = new URL( +const OUTPUT = new URL( "../packages/core/src/postgres/schema/pg-catalog.ts", import.meta.url, ); -if (process.argv[1] === fileURLToPath(import.meta.url)) { - const require = createRequire( - new URL("../packages/core/package.json", import.meta.url), - ); - const { - Client, - }: { - Client: new (config: { - connectionString: string; - }) => PgClient & { connect(): Promise; end(): Promise }; - } = require("pg"); - const url = process.env.TYPESQL_PG_URL; - if (!url) { - console.error( - "set TYPESQL_PG_URL (e.g. docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=x postgres:18)", - ); - process.exit(2); - } - const client = new Client({ connectionString: url }); - await client.connect(); - try { - const source = await generatePgCatalog(client); - if (process.argv.includes("--check")) { - const current = readFileSync(OUTPUT, "utf8"); - if (current !== source) { - console.error( - `${fileURLToPath(OUTPUT)} is out of date; run without --check to regenerate`, - ); - process.exitCode = 1; - } - } else { - writeFileSync(OUTPUT, source); - console.log(`wrote ${fileURLToPath(OUTPUT)}`); - } - } finally { - await client.end(); - } -} +emit( + OUTPUT, + await withPostgres(generatePgCatalog), + `${RELATIONS.length} relations`, +); diff --git a/tools/servers.ts b/tools/servers.ts new file mode 100644 index 00000000..0245c6e9 --- /dev/null +++ b/tools/servers.ts @@ -0,0 +1,56 @@ +// The database servers of the scenario tests, which the generators of +// PostgreSQL catalog sources read too. +import { readFileSync } from "node:fs"; + +export type Server = "postgres" | "mysql" | "mariadb"; + +/** The database servers compose.yaml starts: its image tags and ports pin their versions and default URLs. */ +export const SERVERS: Record< + Server, + { variable: string; url: string; version: string } +> = (() => { + const compose = readFileSync( + new URL("../tests/compose.yaml", import.meta.url), + "utf8", + ); + const service = (name: Server) => { + const body = new RegExp(`^ ${name}:\\n((?: {4}.*\\n)+)`, "m").exec( + compose, + )?.[1]; + const field = (pattern: RegExp) => { + const value = body && pattern.exec(body)?.[1]; + if (!value) throw new Error(`compose.yaml: ${name} has no ${pattern}`); + return value; + }; + return { + version: field(/^ {4}image: [^:\s]+:([^@\s]+)/m), + port: field(/^ {6}- "127\.0\.0\.1:(\d+):\d+"/m), + password: field(/^ {6}\w+_PASSWORD: (\S+)/m), + }; + }; + const pg = service("postgres"); + const mysql = service("mysql"); + const mariadb = service("mariadb"); + return { + postgres: { + variable: "TYPESQL_DATABASE_URL", + url: `postgres://postgres:${pg.password}@127.0.0.1:${pg.port}/postgres`, + version: pg.version, + }, + // mysql2 connects to both MySQL-protocol servers with a `mysql://` URL. + mysql: { + variable: "TYPESQL_MYSQL_URL", + url: `mysql://root:${mysql.password}@127.0.0.1:${mysql.port}/mysql`, + version: mysql.version, + }, + mariadb: { + variable: "TYPESQL_MARIADB_URL", + url: `mysql://root:${mariadb.password}@127.0.0.1:${mariadb.port}/mysql`, + version: mariadb.version, + }, + }; +})(); + +/** The server's admin URL: its environment variable, or the compose.yaml server. */ +export const serverUrl = (server: Server) => + process.env[SERVERS[server].variable] ?? SERVERS[server].url; diff --git a/tsconfig.json b/tsconfig.json index d9ce82fa..7d96a024 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,5 +5,5 @@ "allowImportingTsExtensions": true, "types": ["node"] }, - "include": ["*.ts", "tools"] + "include": ["knip.ts", "tools", "tests/*.ts", "tests/support"] }