Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
56ecabb
docs(projects): record the MySQL placeholder decision
wmadden-electric Oct 28, 2026
f685c01
feat(mysql): run the MySQL drivers on the mariadb package
wmadden-electric Oct 28, 2026
ee5e392
refactor(mysql): drop the integer casts the mysql2 driver needed
wmadden-electric Oct 28, 2026
cccb654
test(mysql): keep the key-retrieval test independent of MYSQL_URL
wmadden-electric Oct 28, 2026
73d9b0e
ci(mysql): let the MySQL tests fetch the server key
wmadden-electric Oct 28, 2026
595949b
docs(projects): the MySQL reads slice makes no shared changes
wmadden-electric Oct 28, 2026
785f6c4
fix(driver-mysql): report server errors and refused values with codes…
wmadden-electric Oct 28, 2026
20e7133
fix(driver-mysql): keep pooled connections after early stops and refu…
wmadden-electric Oct 28, 2026
638042f
fix(driver-mysql): name accepted URL parameters and Prisma 7 replacem…
wmadden-electric Oct 28, 2026
0ae061d
docs(driver-mysql): describe the protocol rule, value rules, early st…
wmadden-electric Oct 28, 2026
ee684d3
docs(errors): drop the MySQL meanings given to shared codes and docum…
wmadden-electric Oct 28, 2026
af93f72
docs(mysql): add allowPublicKeyRetrieval=true to the local MySQL URLs
wmadden-electric Oct 28, 2026
255c974
test(test-utils): pass MYSQL_URL's ssl parameter through when creatin…
wmadden-electric Oct 28, 2026
365d473
test(prisma-8-demo-mysql): create test databases through the public M…
wmadden-electric Oct 28, 2026
2825564
test(adapter-mysql): trim the raw integer placeholder test to the cas…
wmadden-electric Oct 28, 2026
c5ea507
refactor(driver-mysql): track the one open stream on a held connectio…
wmadden-electric Oct 28, 2026
f338ece
docs(projects): describe the MySQL driver as built on mariadb
wmadden-electric Oct 28, 2026
e3b7c11
chore(upgrade): the mariadb MySQL driver needs no upgrade steps
wmadden-electric Oct 28, 2026
3561115
fix(driver-mysql): return every row value as text, bytes or null
wmadden-electric Oct 28, 2026
eea4203
fix(target-mysql): read and write a bigint as decimal text only
wmadden-electric Oct 28, 2026
01620c3
fix(driver-mysql): queue a statement behind an open stream, as Postgr…
wmadden-electric Oct 28, 2026
9f74ee3
docs(driver-mysql): say the drivers return text, bytes or null
wmadden-electric Oct 28, 2026
da81e69
fix(target-mysql): send a bigint parameter as a bigint again
wmadden-electric Oct 28, 2026
24227b1
fix(driver-mysql): keep a malformed URL's password out of the error
wmadden-electric Oct 28, 2026
63560f1
fix(driver-mysql): cap statementCacheSize at 1,000 again
wmadden-electric Oct 28, 2026
9bdb240
fix(driver-mysql): destroy a held connection released with a stream open
wmadden-electric Oct 28, 2026
c4a0550
fix(driver-mysql): close the statement when executeStream throws
wmadden-electric Oct 28, 2026
8e8814e
test(driver-mysql): cover the early-stop fallback and values left out…
wmadden-electric Oct 28, 2026
c322b3f
refactor(driver-mysql): rename prepared-count to placeholder-count
wmadden-electric Oct 28, 2026
e661166
refactor(driver-mysql): move the parameter value rules into parameter…
wmadden-electric Oct 28, 2026
bf92778
docs(mysql-poc): describe the driver as built
wmadden-electric Oct 28, 2026
4393e72
fix(target-mysql): send a bigint parameter as decimal text, as the Po…
wmadden-electric Oct 28, 2026
e10ff98
fix(driver-mysql): read one text form per column on either protocol, …
wmadden-electric Oct 28, 2026
423991d
fix(driver-mysql): report connection failures as SqlConnectionError a…
wmadden-electric Oct 28, 2026
adbb02d
feat(target-mysql): declare the UTC time zone as the target's session…
wmadden-electric Oct 28, 2026
65ba604
fix(driver-mysql): run and configure statements as the Postgres drive…
wmadden-electric Oct 28, 2026
e5e27fb
feat(mysql): check the URL scheme and pass stream and poolOptions to …
wmadden-electric Oct 28, 2026
6e3666b
fix(driver-mysql): leave a refused bigint out of its error, and name …
wmadden-electric Oct 28, 2026
965cbd2
test(test-utils): read MYSQL_URL's TLS parameters as the MySQL driver…
wmadden-electric Oct 28, 2026
f814147
docs(mysql-poc): describe the MySQL driver as it now copies the Postg…
wmadden-electric Oct 28, 2026
7b3ff90
test(adapter-mysql): open the control connection with the target's se…
wmadden-electric Oct 28, 2026
710cc2f
fix(driver-mysql): ignore URL parameters the driver does not read, as…
wmadden-electric Oct 28, 2026
1d9a0d8
fix(driver-mysql): set the target's session settings with a prepared …
wmadden-electric Oct 28, 2026
dac2338
refactor(driver-mysql): order statements on a held connection with th…
wmadden-electric Oct 28, 2026
24a84f6
fix(driver-mysql): fail a sign-in that cannot succeed at once, and ne…
wmadden-electric Oct 28, 2026
b43333c
fix(driver-mysql): read a FLOAT as MySQL writes it over the text prot…
wmadden-electric Oct 28, 2026
cdd2f62
test(driver-mysql): read every column type prepared as well as over t…
wmadden-electric Oct 28, 2026
940bce7
fix(mysql): check the stream option when mysql() is called, as postgr…
wmadden-electric Oct 28, 2026
374044c
docs(mysql-poc): give the true reasons for the MySQL driver's differe…
wmadden-electric Oct 28, 2026
bd718ac
build(orm-mysql): list arktype, which the MySQL facade now uses
wmadden-electric Oct 28, 2026
a303897
docs(errors): say the MySQL drivers refuse charset rather than ignore it
wmadden-electric Oct 28, 2026
d970378
fix(driver-mysql): read FLOAT and DOUBLE as MySQL writes them over th…
wmadden-electric Oct 28, 2026
29cf74b
fix(driver-mysql): know a pooled connection only by its info
wmadden-electric Oct 28, 2026
77da8ee
fix(driver-mysql): classify socket errors by their Node code, as the …
wmadden-electric Oct 28, 2026
89ca2d9
fix(driver-mysql): fail statements fast only on connection failures, …
wmadden-electric Oct 28, 2026
ab5521e
docs(mysql-poc): describe FLOAT and DOUBLE text and connection failur…
wmadden-electric Oct 28, 2026
e33232b
fix(driver-mysql): read FLOAT and DOUBLE declared with decimals with …
wmadden-electric Oct 28, 2026
00fc489
fix(driver-mysql): keep no connection failure, so a statement after a…
wmadden-electric Oct 28, 2026
4a4660b
docs(mysql-poc): describe declared FLOAT and DOUBLE decimals, and con…
wmadden-electric Oct 28, 2026
56bf813
fix(driver-mysql): keep the sign of negative zero in a column declare…
wmadden-electric Oct 28, 2026
a856967
test(driver-mysql): check that a connection handed to a statement tha…
wmadden-electric Oct 28, 2026
5caff27
docs(mysql-poc): state the pool's retries with measured waits, and th…
wmadden-electric Oct 28, 2026
f1e3f7f
docs(driver-mysql): give the wait after a refused connection without …
wmadden-electric Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ jobs:
index: [1, 2, 3, 4]
env:
TEST_TIMEOUT_MULTIPLIER: 2
MYSQL_URL: mysql://root:root@127.0.0.1:3306/mysql
MYSQL_URL: mysql://root:root@127.0.0.1:3306/mysql?allowPublicKeyRetrieval=true
services:
postgres:
image: postgres:15
Expand Down Expand Up @@ -240,7 +240,7 @@ jobs:
# container, because GitHub Actions service containers can't override
# the postgres CMD to enable shared_preload_libraries=pg_stat_statements).
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: postgres://postgres:postgres@127.0.0.1:5433/prisma_8_cloudflare_worker
MYSQL_URL: mysql://root:root@127.0.0.1:3306/mysql
MYSQL_URL: mysql://root:root@127.0.0.1:3306/mysql?allowPublicKeyRetrieval=true
services:
postgres:
image: postgres:15
Expand Down
14 changes: 8 additions & 6 deletions docs/reference/error-reference.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion examples/prisma-8-demo-mysql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,20 @@ The repository's `docker-compose.yaml` starts a server on port 3307; create the
that database, never MySQL's own `mysql` system database, or the CLI creates
the example's tables inside it.

The URL sets `allowPublicKeyRetrieval=true` because the local server has no
TLS: MySQL 8.4 signs `root` in with `caching_sha2_password`, which then needs
the server's public key to send the password. Without it, connecting fails
once the server no longer has the account cached, such as after a restart.
Against a server reached over a network, use TLS (`sslmode=verify-full`, or
`ssl=true`) instead, since fetching the key without TLS lets a machine in between read the
password.

```bash
docker compose up -d mysql
docker compose exec mysql mysql -uroot -proot -e 'CREATE DATABASE prisma_demo'
pnpm install
pnpm emit # generates src/prisma/contract.json + contract.d.ts
export MYSQL_URL=mysql://root:root@127.0.0.1:3307/prisma_demo
export MYSQL_URL='mysql://root:root@127.0.0.1:3307/prisma_demo?allowPublicKeyRetrieval=true'
pnpm db:migrate # applies migrations/ to the database
pnpm db:verify # checks the database matches the contract
pnpm db:sign # records the contract the database now satisfies
Expand Down
1 change: 0 additions & 1 deletion examples/prisma-8-demo-mysql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@repo/prisma-bin": "workspace:8.0.0-rc.21",
"@repo/tsconfig": "workspace:8.0.0-rc.21",
"@types/node": "catalog:",
"mysql2": "3.15.3",
"typescript": "catalog:",
"vitest": "catalog:"
},
Expand Down
9 changes: 5 additions & 4 deletions examples/prisma-8-demo-mysql/test/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomBytes } from 'node:crypto';
import { createConnection } from 'mysql2/promise';
import mysqlControlDriver from '@prisma/orm-mysql/driver/control';

export interface TestDatabase {
readonly url: string;
Expand All @@ -23,11 +23,12 @@ export async function createTestDatabase(serverUrl: string): Promise<TestDatabas
};
}

/** Runs one statement through the MySQL control driver, which reads `serverUrl` as the CLI does, TLS settings included. */
async function runOnServer(serverUrl: string, statement: string): Promise<void> {
const connection = await createConnection(serverUrl);
const driver = await mysqlControlDriver.create(serverUrl);
try {
await connection.query(statement);
await driver.query(statement);
} finally {
await connection.end();
await driver.close();
}
}
2 changes: 1 addition & 1 deletion packages/3-targets/mysql/1-target/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Declares what MySQL is to the SQL family: its data types, codecs and PSL scalars
- **Descriptor metadata**: `targetId: 'mysql'`, `supportsNamespaces: false` and the unbound default namespace ([ADR 223](../../../../docs/architecture%20docs/adrs/ADR%20223%20-%20Target-owned%20default%20namespace.md)). Namespace blocks are refused through that flag, with the display name and reason the descriptor supplies.
- **Data types** (`src/core/data-types.ts`, [ADR 254](../../../../docs/architecture%20docs/adrs/ADR%20254%20-%20Data%20types%20and%20casts.md)): `mysql/int`, `mysql/bigint`, `mysql/varchar`, `mysql/char`, `mysql/text`, `mysql/datetime` and `mysql/enum`. PSL `String` is `varchar(191)`, so a `@unique` column can be indexed. `mysql/enum` is a native `enum('A','B')` column type whose name is the text MySQL's catalog prints, so `db verify` compares the members and their order.
- **Enum columns** (`src/core/mysql-database.ts`): the namespace factory writes each enum column's members, from the value set the column names, into its type parameters in declared order. No CHECK constraint is derived.
- **Codecs**: one per data type, plus MySQL adaptations of the family's `sql/char@1`, `sql/varchar@1` and `sql/raw@1`. Each `fromWire` reads what the driver hands over: the text MySQL sends for text, bigint, decimal, date and JSON columns, and a number for an `int`. `projectJson` writes a column into the JSON an include reads, in the form its `fromWire` reads from a row: `int`, the text types, `enum` and `datetime` pass through; `bigint` renders as `CAST(x AS CHAR)`, because JavaScript's `JSON.parse` would round a number past 2^53. A raw column is refused. The bigint codec sends a JavaScript bigint, which mysql2 sends as its decimal text; the adapter writes an `int` or `bigint` param as `CAST(?N AS SIGNED)`, so MySQL reads it as an integer. The codec refuses a value outside the signed 64-bit range.
- **Codecs**: one per data type, plus MySQL adaptations of the family's `sql/char@1`, `sql/varchar@1` and `sql/raw@1`. Each `fromWire` reads what the driver hands over: the text MySQL sends, for every column; an include's JSON carries an `int` as a number. `projectJson` writes a column into the JSON an include reads, in the form its `fromWire` reads from a row: `int`, the text types, `enum` and `datetime` pass through; `bigint` renders as `CAST(x AS CHAR)`, because JavaScript's `JSON.parse` would round a number past 2^53. A raw column is refused. As on Postgres, `toWire` returns what the database reads for each data type: the `int` codec a number and the `bigint` codec decimal text, as the Postgres `int8` codec does. MySQL reads a text parameter beside a `bigint` column exactly; where nothing gives a parameter a type, as in `?1 + ?2` in raw SQL, the author writes the cast, as on Postgres. The bigint codec refuses a value outside the signed 64-bit range.
- **Control tables**: `_prisma_marker` and `_prisma_ledger` live beside the application's tables, because MySQL has no schema inside a database. `src/contract-free/control-bootstrap.ts` declares their DDL.
- **Control target descriptor**: contract serializer, `contractToSchema`, `diffSchema`, the planner and the runner.
- **Planner** (`src/core/migrations/planner.ts`, `issue-planner.ts`): creates and drops tables, columns, indexes, unique constraints and foreign keys, with backtick-quoted DDL. Foreign keys are added after every table exists and dropped first. Dropping a foreign key also drops the index InnoDB made for it, when no other index served the key and the destination keeps no index of that name; dropping an index a foreign key of the destination uses, with no other index to take over, is refused, because MySQL refuses it. A change to a column's type, nullability or default, a primary key, a CHECK constraint, a required column with no default, a name longer than 64 characters, an autoincrement column that does not lead the primary key or a unique constraint, and every statement such as a rename are refused with a conflict that says they are not yet supported on MySQL. `migration.ts` is written as calls on `MysqlMigration` (`src/core/migrations/mysql-migration.ts`).
Expand Down
15 changes: 7 additions & 8 deletions packages/3-targets/mysql/1-target/src/core/codecs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* The MySQL target's codecs. The driver hands every column to a codec as the text MySQL sends for it (ADR 155), so each `fromWire` reads that text; `toWire` writes the value MySQL reads for a parameter.
* The MySQL target's codecs. The driver hands every column to a codec as text, or bytes for a binary column (ADR 155), so each `fromWire` reads that text; `toWire` writes the value MySQL reads for a parameter.
*/

import type { JsonValue } from '@internal/contract/types';
Expand Down Expand Up @@ -127,7 +127,7 @@ export class MysqlTextDescriptor extends MysqlCodecDescriptor<void> {

export const mysqlTextDescriptor = new MysqlTextDescriptor();

/** A MySQL `int` as a JavaScript number. MySQL sends it as decimal text. */
/** A MySQL `int` as a JavaScript number. A row carries it as decimal text, and an include's JSON as a number. */
export class MysqlIntCodec extends CodecImpl<
typeof MYSQL_INT_CODEC_ID,
readonly ['equality', 'order', 'numeric'],
Expand Down Expand Up @@ -173,25 +173,24 @@ export class MysqlIntDescriptor extends MysqlCodecDescriptor<void> {

export const mysqlIntDescriptor = new MysqlIntDescriptor();

/** A MySQL `bigint` as a JavaScript bigint. The driver reads it as its decimal text and sends a bigint as a 64-bit integer, which MySQL compares exactly. */
/** A MySQL `bigint` as a JavaScript bigint. A row, an include's JSON and a parameter carry it as decimal text, as the Postgres `int8` codec does; MySQL reads a text parameter beside a `bigint` column exactly. */
export class MysqlBigintCodec extends CodecImpl<
typeof MYSQL_BIGINT_CODEC_ID,
readonly ['equality', 'order', 'numeric'],
string | bigint,
string,
bigint
> {
async toWire(value: bigint, _ctx: CodecCallContext): Promise<bigint> {
async toWire(value: bigint, _ctx: CodecCallContext): Promise<string> {
if (typeof value !== 'bigint' || value < BIGINT_MIN || value > BIGINT_MAX) {
return encodeFailed(
MYSQL_BIGINT_CODEC_ID,
`a bigint from ${BIGINT_MIN} to ${BIGINT_MAX}`,
value,
);
}
return value;
return value.toString();
}
async fromWire(wire: string | bigint, _ctx: CodecCallContext): Promise<bigint> {
if (typeof wire === 'bigint') return wire;
async fromWire(wire: string, _ctx: CodecCallContext): Promise<bigint> {
if (!DECIMAL_INTEGER.test(wire)) {
return decodeFailed(MYSQL_BIGINT_CODEC_ID, 'the decimal text of an integer', wire);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ const mysqlTargetDescriptorMetaRuntimeBase = {
version: '0.0.1',
capabilities: {},
dataTypes: mysqlDataTypes,
sessionSettings: { time_zone: '+00:00' },
} as const;

/** The runtime-safe part of the MySQL target's metadata: it leaves out `authoring`, which only contract construction reads. */
/** The runtime-safe part of the MySQL target's metadata: it leaves out `authoring`, which only contract construction reads. Every session runs in UTC, because the datetime codec reads and writes UTC. */
export const mysqlTargetDescriptorMetaRuntime: typeof mysqlTargetDescriptorMetaRuntimeBase & {
readonly __codecTypes?: CodecTypes;
} = mysqlTargetDescriptorMetaRuntimeBase;
7 changes: 4 additions & 3 deletions packages/3-targets/mysql/1-target/test/codecs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ describe('MySQL codecs read the text MySQL sends', () => {
expect(codec.fromDataTypeValue(value(mysqlInt, 3))).toBe(3);
});

it('reads a bigint from its decimal text without losing digits', async () => {
it('reads a bigint from its decimal text and writes it as decimal text, without losing digits', async () => {
const codec = mysqlBigintDescriptor.factory()(instance);
expect(await codec.fromWire('9007199254740993', ctx)).toBe(9007199254740993n);
expect(await codec.toWire(9007199254740993n, ctx)).toBe(9007199254740993n);
expect(await codec.toWire(9007199254740993n, ctx)).toBe('9007199254740993');
await expect(codec.toWire(Object(1n), ctx)).rejects.toThrow(/must be a bigint/);
await expect(codec.fromWire('x', ctx)).rejects.toThrow(/decimal text/);
expect(codec.toDataTypeValue(12n)).toEqual(value(mysqlBigint, '12'));
expect(codec.fromDataTypeValue(value(mysqlBigint, '12'))).toBe(12n);
Expand Down Expand Up @@ -129,7 +130,7 @@ describe('the MySQL bigint codec', () => {
expect([
await codec.toWire(2n ** 63n - 1n, ctx),
await codec.toWire(-(2n ** 63n), ctx),
]).toEqual([2n ** 63n - 1n, -(2n ** 63n)]);
]).toEqual(['9223372036854775807', '-9223372036854775808']);
for (const value of [2n ** 63n, -(2n ** 63n) - 1n, 2n ** 64n - 1n]) {
await expect(codec.toWire(value, ctx)).rejects.toMatchObject({
code: 'RUNTIME.ENCODE_FAILED',
Expand Down
7 changes: 7 additions & 0 deletions packages/3-targets/mysql/1-target/test/descriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MysqlCreateTable } from '../src/core/ddl/nodes';
import { mysqlTargetDescriptorMeta } from '../src/core/descriptor-meta';
import mysqlControlTarget from '../src/core/migrations/control-target';
import { escapeLiteral, quoteIdentifier, quoteLiteral } from '../src/core/sql-utils';
import mysqlRuntimeTarget from '../src/exports/runtime';

describe('the MySQL target descriptor', () => {
it('reports no namespaces and the unbound default namespace', () => {
Expand All @@ -23,6 +24,12 @@ describe('the MySQL target descriptor', () => {
});
});

it('declares, on the control and runtime target, that every session runs in UTC, which its datetime codec reads, as Postgres declares DateStyle', () => {
expect([mysqlTargetDescriptorMeta.sessionSettings, mysqlRuntimeTarget.sessionSettings]).toEqual(
[{ time_zone: '+00:00' }, { time_zone: '+00:00' }],
);
});

it('maps String to varchar(191) and DateTime to datetime(3)', () => {
const types = mysqlTargetDescriptorMeta.authoring.type;
expect([types.String.output, types.DateTime.output, types.Int.output]).toEqual([
Expand Down
4 changes: 2 additions & 2 deletions packages/3-targets/mysql/2-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Lowers the queries the ORM and the SQL builder build into MySQL SQL, and reads a
- **Marker and ledger reads and writes**, **`withTransaction`** (`START TRANSACTION`), and the `CREATE TABLE IF NOT EXISTS` statements for the control tables. `initMarker` writes with `INSERT … ON DUPLICATE KEY UPDATE`. MySQL has no `RETURNING`, so `updateMarker` reads and locks the row with `FOR UPDATE` inside the caller's transaction and writes it only when it still holds the expected hash.
- **`holdMarkerLock`** takes the marker lock, a `GET_LOCK` MySQL holds for the session, around the work it guards and releases it with `RELEASE_LOCK` when that work ends; `db sign` creates the marker table and writes the markers inside it. `lockMarker` takes no lock, because the lock is already held around the transaction. The migration runner takes the same lock through the same helper.
- **Query lowering** (`src/core/sql-renderer.ts`), used by the runtime adapter:
- Backtick quoting. Each parameter is written as `?N`, N being its position in `collectOrderedParamRefs`, the order the runtime encodes parameters in; a parameter written twice is bound once. `src/core/placeholders.ts` then checks that every placeholder names a parameter and every parameter has one, and raises `RUNTIME.PARAM_PLACEHOLDER_MISMATCH` otherwise, as for a `?` written into raw SQL text. The driver sends the values apart from the SQL, as a prepared statement's parameters. mysql2 sends an integer value as text, so an `int` or `bigint` param, decided from its codec's data type, is written `CAST(?N AS SIGNED)`, which keeps `?1 < ?2`, `GREATEST`, `JSON_ARRAY(?1)` and arithmetic past 2^53 integer; `LIMIT` and `OFFSET`, which take only a bare placeholder, read it as an integer themselves. A literal the AST carries is written inline only when it is a number, a bigint, a boolean or NULL, which only code writes, such as the ORM's `1`; a text, date or JSON literal is refused with `RUNTIME.AST_UNSUPPORTED`, because only a caller could supply it and a caller's value belongs in a param. `JSON_OBJECT` keys, which are contract field and relation names, are written through the target's `escapeLiteral`, which doubles quotes and backslashes.
- Backtick quoting. Each parameter is written as `?N`, N being its position in `collectOrderedParamRefs`, the order the runtime encodes parameters in; a parameter written twice is bound once. `src/core/placeholders.ts` then checks that every placeholder names a parameter and every parameter has one, and raises `RUNTIME.PARAM_PLACEHOLDER_MISMATCH` otherwise, as for a `?` written into raw SQL text. The driver sends the values apart from the SQL, as a prepared statement's parameters. Every parameter, an integer one included, is a bare `?N`, as the Postgres adapter writes `$N` for every data type whose type the server infers from where the parameter stands, the integer ones among them. MySQL infers it the same way: a parameter beside a `bigint` column is read as an integer, even when the `bigint` codec sends decimal text. Where nothing gives a parameter a type, as in `?1 + ?2` in raw SQL, the author writes the cast, as on Postgres. `LIMIT ?1` takes only a bare placeholder, so the adapter could not cast there in any case. A literal the AST carries is written inline only when it is a number, a bigint, a boolean or NULL, which only code writes, such as the ORM's `1`; a text, date or JSON literal is refused with `RUNTIME.AST_UNSUPPORTED`, because only a caller could supply it and a caller's value belongs in a param. `JSON_OBJECT` keys, which are contract field and relation names, are written through the target's `escapeLiteral`, which doubles quotes and backslashes.
- `<=>` for IS NOT DISTINCT FROM, a leading `x IS NULL` sort key for NULLS FIRST and NULLS LAST, the largest row count MySQL takes for an `OFFSET` with no `LIMIT`.
- A to-one include and an unordered array use `JSON_ARRAYAGG`. A to-many include with an order uses `GROUP_CONCAT(… ORDER BY …)` cast to JSON, because `JSON_ARRAYAGG` takes no `ORDER BY`; the statement raises `group_concat_max_len` itself with a `SET_VAR` hint, so it does not depend on the driver's session. An include with no rows is `JSON_ARRAY()`. Text longer than `max_allowed_packet` makes MySQL's `CONCAT` return NULL with only a warning, so a NULL array with rows fails the statement instead of reading as empty. MySQL reports that failure as `Invalid JSON text in argument 1 to function cast_as_json`, which names neither the include nor `max_allowed_packet`: the driver cannot tell it from any other invalid JSON cast, so it passes the error on unchanged. This size limit applies only once an include has an order.
- Refused with `RUNTIME.AST_UNSUPPORTED`: FULL and LATERAL joins, function sources, row locks, DISTINCT ON, non-finite numbers, and writes.
Expand All @@ -28,7 +28,7 @@ Lowers the queries the ORM and the SQL builder build into MySQL SQL, and reads a

## Driver contract

The SQL this adapter lowers is not runnable as written: each parameter is a numbered `?N`, which MySQL does not read. A driver used with this adapter must rewrite each `?N` to MySQL's positional `?`, order the values to match, and send them as a server-side prepared statement's parameters, never inside the SQL. `@internal/driver-mysql` does this. The adapter writes an `int` or `bigint` parameter as `CAST(?N AS SIGNED)` only because mysql2 sends a JavaScript `BigInt` as text, and a comparison with no column to give that text a type, such as `?1 < ?2`, would compare it as text. A driver that sends integers as integers would not need the cast.
The SQL this adapter lowers is not runnable as written: each parameter is a numbered `?N`, which MySQL does not read. A driver used with this adapter must rewrite each `?N` to MySQL's positional `?`, order the values to match, and send them as a server-side prepared statement's parameters, never inside the SQL. `@internal/driver-mysql` does this. The adapter writes no casts on parameters. The binary protocol needs a type for each value, so the driver must send a JavaScript integer as an integer type: `LIMIT ?1` refuses one sent as a double, which is how the `mariadb` package sends any number past 2^31. The driver's rules for the type each value is sent as are in [`parameter-values.ts`](../3-driver/src/core/parameter-values.ts).

## Exports

Expand Down
Loading
Loading