@prisma-next/adapter-sqlite 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @prisma-next/adapter-sqlite
2
+
3
+ SQLite adapter for Prisma Next.
4
+
5
+ ## Package Classification
6
+
7
+ - **Domain**: targets
8
+ - **Layer**: adapters
9
+ - **Plane**: multi-plane (shared, runtime)
10
+
11
+ ## Overview
12
+
13
+ The SQLite adapter implements the adapter SPI for SQLite databases. It provides AST-to-SQL lowering, capability discovery, codec definitions, and error mapping for SQLite-specific behavior. It also exports runtime-plane adapter descriptors for config wiring.
14
+
15
+ ## Purpose
16
+
17
+ Provide SQLite-specific adapter implementation, codecs, and capabilities. Enable SQLite dialect support in Prisma Next through the adapter SPI.
18
+
19
+ ## Responsibilities
20
+
21
+ - **Adapter Implementation**: Implement `Adapter` SPI for SQLite
22
+ - Lower SQL ASTs to SQLite dialect SQL
23
+ - Render `includeMany` as correlated subquery with `json_group_array(json_object(...))` for nested array includes
24
+ - Advertise SQLite capabilities (`jsonAgg`, `returning`; no `lateral`, no `enums`)
25
+ - Provide target-specific marker SQL via `readMarkerStatement()` on `AdapterProfile`
26
+ - Map SQLite errors to `RuntimeError` envelope
27
+ - **Codec Definitions**: Define SQLite codecs for type conversion
28
+ - 8 codecs: TEXT, INTEGER, REAL, BLOB, BOOLEAN (INTEGER 0/1), DATETIME (TEXT ISO8601), JSON (TEXT), BIGINT (INTEGER)
29
+ - Wire format to JavaScript type decoding
30
+ - JavaScript type to wire format encoding
31
+ - **Codec Types**: Export TypeScript types for SQLite codecs
32
+ - **Column Types**: Export column type descriptors with `as const` literal `codecId` types for contract authoring
33
+ - **Descriptors**: Provide adapter descriptors declaring capabilities and codec type imports
34
+
35
+ **Non-goals:**
36
+ - Transport/pooling management (drivers)
37
+ - Query compilation (sql-query)
38
+ - Runtime execution (runtime)
39
+ - Control-plane introspection and migration (future milestone)
40
+
41
+ ## Architecture
42
+
43
+ This package spans multiple planes:
44
+
45
+ - **Shared plane** (`src/core/**`): Core adapter implementation, codecs, and types that can be imported by both migration and runtime planes
46
+ - **Runtime plane** (`src/exports/runtime.ts`): Runtime-plane entry point that exports the runtime adapter descriptor
47
+
48
+ ```mermaid
49
+ flowchart TD
50
+ subgraph "Runtime"
51
+ RT[Runtime]
52
+ PLAN[Plan]
53
+ end
54
+
55
+ subgraph "SQLite Adapter"
56
+ ADAPTER[Adapter]
57
+ LOWERER[AST Lowering]
58
+ CODECS[Codecs]
59
+ CAPS[Capabilities]
60
+ end
61
+
62
+ subgraph "SQLite Driver"
63
+ DRIVER[Driver]
64
+ DB[(SQLite)]
65
+ end
66
+
67
+ subgraph "Descriptors"
68
+ RUNTIME_DESC[Runtime Descriptor]
69
+ CODECTYPES[Codec Types]
70
+ COLTYPES[Column Types]
71
+ end
72
+
73
+ RT --> PLAN
74
+ PLAN --> ADAPTER
75
+ ADAPTER --> LOWERER
76
+ ADAPTER --> CODECS
77
+ ADAPTER --> CAPS
78
+ ADAPTER --> DRIVER
79
+ DRIVER --> DB
80
+ RUNTIME_DESC --> RT
81
+ CODECTYPES --> RT
82
+ COLTYPES --> RT
83
+ CODECS --> CODECTYPES
84
+ ```
85
+
86
+ ## Components
87
+
88
+ ### Core (`src/core/`)
89
+
90
+ **Adapter (`adapter.ts`)**
91
+ - Main adapter implementation
92
+ - Lowers SQL ASTs to SQLite SQL with `?` positional parameters
93
+ - Renders joins (INNER, LEFT, RIGHT, FULL) with ON conditions
94
+ - Renders `includeMany` as correlated subquery with `json_group_array(json_object(...))` for nested array includes
95
+ - Renders DML operations (INSERT, UPDATE, DELETE) with RETURNING clauses
96
+ - Renders ON CONFLICT (DO NOTHING / DO UPDATE SET) for upserts
97
+ - Uses `CAST(expr AS type)` instead of Postgres `::type` syntax
98
+ - Advertises SQLite capabilities (`returning`, `jsonAgg`)
99
+
100
+ **Codecs (`codecs.ts`)**
101
+ - SQLite codec definitions
102
+ - Type conversion between wire format and JavaScript
103
+ - SQL base codecs reused: `char`, `varchar`, `int`, `float`
104
+ - SQLite-specific codecs: `text`, `integer`, `real`, `blob`, `boolean` (0/1), `datetime` (ISO8601), `json` (TEXT), `bigint`
105
+
106
+ **SQL Utilities (`sql-utils.ts`)**
107
+ - Identifier quoting with double quotes
108
+ - String literal escaping with single-quote doubling
109
+ - Null byte rejection for SQL injection prevention
110
+
111
+ ### Exports (`src/exports/`)
112
+
113
+ **Runtime Entry Point (`runtime.ts`)**
114
+ - Exports the runtime-plane adapter descriptor with codec registry
115
+
116
+ **Codec Types Export (`codec-types.ts`)**
117
+ - Exports TypeScript type definitions for SQLite codecs
118
+ - Used in `contract.d.ts` generation
119
+
120
+ **Column Types Export (`column-types.ts`)**
121
+ - Exports column descriptors for built-in types: `textColumn`, `integerColumn`, `realColumn`, `blobColumn`, `booleanColumn`, `datetimeColumn`, `jsonColumn`, `bigintColumn`
122
+ - Uses `as const` to preserve literal `codecId` types through the type system
123
+
124
+ **Types Export (`types.ts`)**
125
+ - Re-exports SQLite-specific types
126
+
127
+ ## Dependencies
128
+
129
+ - **`@prisma-next/sql-contract`**: SQL contract types
130
+ - **`@prisma-next/sql-relational-core`**: SQL AST types and codec registry
131
+ - **`@prisma-next/sql-runtime`**: Runtime adapter descriptor types
132
+ - **`@prisma-next/framework-components`**: Descriptor types
133
+
134
+ ## Related Subsystems
135
+
136
+ - **[Adapters & Targets](../../../../docs/architecture%20docs/subsystems/5.%20Adapters%20&%20Targets.md)**: Detailed adapter specification
137
+
138
+ ## Related ADRs
139
+
140
+ - [ADR 005 -- Thin Core Fat Targets](../../../../docs/architecture%20docs/adrs/ADR%20005%20-%20Thin%20Core%20Fat%20Targets.md)
141
+ - [ADR 016 -- Adapter SPI for Lowering](../../../../docs/architecture%20docs/adrs/ADR%20016%20-%20Adapter%20SPI%20for%20Lowering.md)
142
+ - [ADR 030 -- Result decoding & codecs registry](../../../../docs/architecture%20docs/adrs/ADR%20030%20-%20Result%20decoding%20&%20codecs%20registry.md)
143
+ - [ADR 065 -- Adapter capability schema & negotiation v1](../../../../docs/architecture%20docs/adrs/ADR%20065%20-%20Adapter%20capability%20schema%20&%20negotiation%20v1.md)
144
+
145
+ ## Capabilities
146
+
147
+ The adapter declares the following SQLite capabilities:
148
+
149
+ - **`sql.orderBy: true`** -- Supports ORDER BY clauses
150
+ - **`sql.limit: true`** -- Supports LIMIT clauses
151
+ - **`sql.lateral: false`** -- No LATERAL join support; `includeMany` uses correlated subquery fallback
152
+ - **`sql.jsonAgg: true`** -- Supports JSON aggregation via `json_group_array()` for `includeMany`
153
+ - **`sql.returning: true`** -- Supports RETURNING clauses for DML operations (SQLite 3.35+)
154
+ - **`sql.enums: false`** -- No native enum support
155
+
156
+ ## includeMany Support
157
+
158
+ The adapter supports `includeMany` for nested array includes using SQLite's `json_group_array()` and `json_object()`:
159
+
160
+ **Lowering Strategy:**
161
+ - Renders `includeMany` as a correlated subquery with `json_group_array(json_object(...))` to aggregate child rows into a JSON array
162
+ - Uses `COALESCE(..., '[]')` to handle empty results
163
+
164
+ **Example SQL Output:**
165
+ ```sql
166
+ SELECT "user"."id" AS "id",
167
+ COALESCE((SELECT json_group_array(json_object('id', "post"."id", 'title', "post"."title"))
168
+ FROM "post" WHERE "user"."id" = "post"."userId"), '[]') AS "posts"
169
+ FROM "user"
170
+ ```
171
+
172
+ ## DML Operations with RETURNING
173
+
174
+ The adapter supports RETURNING clauses for INSERT, UPDATE, and DELETE:
175
+
176
+ **Example SQL Output:**
177
+ ```sql
178
+ -- INSERT with RETURNING
179
+ INSERT INTO "user" ("email") VALUES (?) RETURNING "user"."id", "user"."email"
180
+
181
+ -- UPDATE with RETURNING
182
+ UPDATE "user" SET "email" = ? WHERE "user"."id" = ? RETURNING "user"."id", "user"."email"
183
+
184
+ -- DELETE with RETURNING
185
+ DELETE FROM "user" WHERE "user"."id" = ? RETURNING "user"."id", "user"."email"
186
+ ```
187
+
188
+ ## Exports
189
+
190
+ - `./codec-types`: SQLite codec types (`CodecTypes`, `JsonValue`, `dataTypes`)
191
+ - `./column-types`: Column type descriptors (`textColumn`, `integerColumn`, `realColumn`, `blobColumn`, `booleanColumn`, `datetimeColumn`, `jsonColumn`, `bigintColumn`)
192
+ - `./types`: SQLite-specific types
193
+ - `./control`: Control-plane entry point (stubbed for future migration support)
194
+ - `./runtime`: Runtime-plane entry point (runtime adapter descriptor)
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@prisma-next/adapter-sqlite",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsdown",
12
+ "test": "vitest run",
13
+ "test:coverage": "vitest run --coverage",
14
+ "typecheck": "tsc --project tsconfig.json --noEmit",
15
+ "lint": "biome check . --error-on-warnings",
16
+ "lint:fix": "biome check --write .",
17
+ "lint:fix:unsafe": "biome check --write --unsafe .",
18
+ "clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
19
+ },
20
+ "dependencies": {
21
+ "@prisma-next/cli": "workspace:*",
22
+ "@prisma-next/contract": "workspace:*",
23
+ "@prisma-next/contract-authoring": "workspace:*",
24
+ "@prisma-next/framework-components": "workspace:*",
25
+ "@prisma-next/family-sql": "workspace:*",
26
+ "@prisma-next/ids": "workspace:*",
27
+ "@prisma-next/sql-contract": "workspace:*",
28
+ "@prisma-next/sql-contract-psl": "workspace:*",
29
+ "@prisma-next/sql-contract-ts": "workspace:*",
30
+ "@prisma-next/sql-operations": "workspace:*",
31
+ "@prisma-next/sql-relational-core": "workspace:*",
32
+ "@prisma-next/sql-runtime": "workspace:*",
33
+ "@prisma-next/sql-schema-ir": "workspace:*",
34
+ "@prisma-next/utils": "workspace:*",
35
+ "arktype": "^2.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@prisma-next/test-utils": "workspace:*",
39
+ "@prisma-next/tsconfig": "workspace:*",
40
+ "@prisma-next/tsdown": "workspace:*",
41
+ "tsdown": "catalog:",
42
+ "typescript": "catalog:",
43
+ "vitest": "catalog:"
44
+ },
45
+ "exports": {
46
+ "./adapter": "./dist/adapter.mjs",
47
+ "./codec-types": "./dist/codec-types.mjs",
48
+ "./column-types": "./dist/column-types.mjs",
49
+ "./control": "./dist/control.mjs",
50
+ "./runtime": "./dist/runtime.mjs",
51
+ "./types": "./dist/types.mjs",
52
+ "./package.json": "./package.json"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/prisma/prisma-next.git",
57
+ "directory": "packages/3-targets/6-adapters/sqlite"
58
+ }
59
+ }
@@ -0,0 +1,528 @@
1
+ import {
2
+ type Adapter,
3
+ type AdapterProfile,
4
+ type AggregateExpr,
5
+ type AnyExpression,
6
+ type AnyFromSource,
7
+ type AnyQueryAst,
8
+ type BinaryExpr,
9
+ type CodecParamsDescriptor,
10
+ type ColumnRef,
11
+ createCodecRegistry,
12
+ type DeleteAst,
13
+ type InsertAst,
14
+ type InsertValue,
15
+ type JoinAst,
16
+ type JoinOnExpr,
17
+ type JsonArrayAggExpr,
18
+ type JsonObjectExpr,
19
+ type ListExpression,
20
+ type LiteralExpr,
21
+ type LowererContext,
22
+ type NullCheckExpr,
23
+ type OperationExpr,
24
+ type OrderByItem,
25
+ type ProjectionItem,
26
+ type SelectAst,
27
+ type SubqueryExpr,
28
+ type UpdateAst,
29
+ } from '@prisma-next/sql-relational-core/ast';
30
+ import { codecDefinitions } from './codecs';
31
+ import { escapeLiteral, quoteIdentifier } from './sql-utils';
32
+ import type { SqliteAdapterOptions, SqliteContract, SqliteLoweredStatement } from './types';
33
+
34
+ const defaultCapabilities = Object.freeze({
35
+ sql: {
36
+ orderBy: true,
37
+ limit: true,
38
+ lateral: false,
39
+ jsonAgg: true,
40
+ returning: true,
41
+ enums: false,
42
+ },
43
+ });
44
+
45
+ class SqliteAdapterImpl implements Adapter<AnyQueryAst, SqliteContract, SqliteLoweredStatement> {
46
+ readonly familyId = 'sql' as const;
47
+ readonly targetId = 'sqlite' as const;
48
+
49
+ readonly profile: AdapterProfile<'sqlite'>;
50
+ private readonly codecRegistry = (() => {
51
+ const registry = createCodecRegistry();
52
+ for (const definition of Object.values(codecDefinitions)) {
53
+ registry.register(definition.codec);
54
+ }
55
+ return registry;
56
+ })();
57
+
58
+ constructor(options?: SqliteAdapterOptions) {
59
+ this.profile = Object.freeze({
60
+ id: options?.profileId ?? 'sqlite/default@1',
61
+ target: 'sqlite',
62
+ capabilities: defaultCapabilities,
63
+ codecs: () => this.codecRegistry,
64
+ readMarkerStatement: () => ({
65
+ sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract_marker where id = ?',
66
+ params: [1],
67
+ }),
68
+ });
69
+ }
70
+
71
+ parameterizedCodecs(): ReadonlyArray<CodecParamsDescriptor> {
72
+ return [];
73
+ }
74
+
75
+ lower(ast: AnyQueryAst, context: LowererContext<SqliteContract>) {
76
+ const collectedParamRefs = ast.collectParamRefs();
77
+ const params: unknown[] = [];
78
+ for (const ref of collectedParamRefs) {
79
+ params.push(ref.value);
80
+ }
81
+
82
+ let sql: string;
83
+
84
+ const node = ast;
85
+ switch (node.kind) {
86
+ case 'select':
87
+ sql = renderSelect(node, context.contract);
88
+ break;
89
+ case 'insert':
90
+ sql = renderInsert(node);
91
+ break;
92
+ case 'update':
93
+ sql = renderUpdate(node, context.contract);
94
+ break;
95
+ case 'delete':
96
+ sql = renderDelete(node);
97
+ break;
98
+ default:
99
+ throw new Error(`Unsupported AST node kind: ${(node as { kind: string }).kind}`);
100
+ }
101
+
102
+ return Object.freeze({
103
+ profileId: this.profile.id,
104
+ body: Object.freeze({ sql, params }),
105
+ });
106
+ }
107
+ }
108
+
109
+ function renderSelect(ast: SelectAst, contract?: SqliteContract): string {
110
+ const distinctPrefix = ast.distinct ? 'DISTINCT ' : '';
111
+ const selectClause = `SELECT ${distinctPrefix}${renderProjection(ast.projection, contract)}`;
112
+ const fromClause = `FROM ${renderSource(ast.from, contract)}`;
113
+
114
+ const joinsClause = ast.joins?.length
115
+ ? ast.joins.map((join) => renderJoin(join, contract)).join(' ')
116
+ : '';
117
+
118
+ const whereClause = ast.where ? `WHERE ${renderExpr(ast.where, contract)}` : '';
119
+ const groupByClause = ast.groupBy?.length
120
+ ? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract)).join(', ')}`
121
+ : '';
122
+ const havingClause = ast.having ? `HAVING ${renderExpr(ast.having, contract)}` : '';
123
+ const orderClause = ast.orderBy?.length
124
+ ? `ORDER BY ${ast.orderBy
125
+ .map((order) => `${renderExpr(order.expr, contract)} ${order.dir.toUpperCase()}`)
126
+ .join(', ')}`
127
+ : '';
128
+ const limitClause = typeof ast.limit === 'number' ? `LIMIT ${ast.limit}` : '';
129
+ const offsetClause = typeof ast.offset === 'number' ? `OFFSET ${ast.offset}` : '';
130
+
131
+ return [
132
+ selectClause,
133
+ fromClause,
134
+ joinsClause,
135
+ whereClause,
136
+ groupByClause,
137
+ havingClause,
138
+ orderClause,
139
+ limitClause,
140
+ offsetClause,
141
+ ]
142
+ .filter((part) => part.length > 0)
143
+ .join(' ')
144
+ .trim();
145
+ }
146
+
147
+ function renderProjection(
148
+ projection: ReadonlyArray<ProjectionItem>,
149
+ contract?: SqliteContract,
150
+ ): string {
151
+ return projection
152
+ .map((item) => {
153
+ const alias = quoteIdentifier(item.alias);
154
+ if (item.expr.kind === 'literal') {
155
+ return `${renderLiteral(item.expr)} AS ${alias}`;
156
+ }
157
+ return `${renderExpr(item.expr, contract)} AS ${alias}`;
158
+ })
159
+ .join(', ');
160
+ }
161
+
162
+ function renderSource(source: AnyFromSource, contract?: SqliteContract): string {
163
+ const node = source;
164
+ switch (node.kind) {
165
+ case 'table-source': {
166
+ const table = quoteIdentifier(node.name);
167
+ if (!node.alias) {
168
+ return table;
169
+ }
170
+ return `${table} AS ${quoteIdentifier(node.alias)}`;
171
+ }
172
+ case 'derived-table-source':
173
+ return `(${renderSelect(node.query, contract)}) AS ${quoteIdentifier(node.alias)}`;
174
+ default:
175
+ throw new Error(`Unsupported source node kind: ${(node as { kind: string }).kind}`);
176
+ }
177
+ }
178
+
179
+ function renderExpr(expr: AnyExpression, contract?: SqliteContract): string {
180
+ const node = expr;
181
+ switch (node.kind) {
182
+ case 'column-ref':
183
+ return renderColumn(node);
184
+ case 'identifier-ref':
185
+ return quoteIdentifier(node.name);
186
+ case 'operation':
187
+ return renderOperation(node, contract);
188
+ case 'subquery':
189
+ return renderSubqueryExpr(node, contract);
190
+ case 'aggregate':
191
+ return renderAggregateExpr(node, contract);
192
+ case 'json-object':
193
+ return renderJsonObjectExpr(node, contract);
194
+ case 'json-array-agg':
195
+ return renderJsonArrayAggExpr(node, contract);
196
+ case 'binary':
197
+ return renderBinary(node, contract);
198
+ case 'and':
199
+ if (node.exprs.length === 0) {
200
+ return 'TRUE';
201
+ }
202
+ return `(${node.exprs.map((part) => renderExpr(part, contract)).join(' AND ')})`;
203
+ case 'or':
204
+ if (node.exprs.length === 0) {
205
+ return 'FALSE';
206
+ }
207
+ return `(${node.exprs.map((part) => renderExpr(part, contract)).join(' OR ')})`;
208
+ case 'exists': {
209
+ const notKeyword = node.notExists ? 'NOT ' : '';
210
+ const subquery = renderSelect(node.subquery, contract);
211
+ return `${notKeyword}EXISTS (${subquery})`;
212
+ }
213
+ case 'null-check':
214
+ return renderNullCheck(node, contract);
215
+ case 'not':
216
+ return `NOT (${renderExpr(node.expr, contract)})`;
217
+ case 'param-ref':
218
+ return '?';
219
+ case 'literal':
220
+ return renderLiteral(node);
221
+ case 'list':
222
+ return renderListLiteral(node);
223
+ default:
224
+ throw new Error(`Unsupported expression node kind: ${(node as { kind: string }).kind}`);
225
+ }
226
+ }
227
+
228
+ // `excluded` is a pseudo-table in ON CONFLICT DO UPDATE that references the
229
+ // row proposed for insertion. It is not quoted because it's a keyword.
230
+ function renderColumn(ref: ColumnRef): string {
231
+ if (ref.table === 'excluded') {
232
+ return `excluded.${quoteIdentifier(ref.column)}`;
233
+ }
234
+ return `${quoteIdentifier(ref.table)}.${quoteIdentifier(ref.column)}`;
235
+ }
236
+
237
+ function renderLiteral(expr: LiteralExpr): string {
238
+ if (typeof expr.value === 'string') {
239
+ return `'${escapeLiteral(expr.value)}'`;
240
+ }
241
+ if (typeof expr.value === 'number' || typeof expr.value === 'boolean') {
242
+ return String(expr.value);
243
+ }
244
+ if (typeof expr.value === 'bigint') {
245
+ return String(expr.value);
246
+ }
247
+ if (expr.value === null || expr.value === undefined) {
248
+ return 'NULL';
249
+ }
250
+ if (expr.value instanceof Date) {
251
+ return `'${escapeLiteral(expr.value.toISOString())}'`;
252
+ }
253
+ const json = JSON.stringify(expr.value);
254
+ if (json === undefined) {
255
+ return 'NULL';
256
+ }
257
+ return `'${escapeLiteral(json)}'`;
258
+ }
259
+
260
+ function renderOperation(expr: OperationExpr, contract?: SqliteContract): string {
261
+ const self = renderExpr(expr.self, contract);
262
+ const args = expr.args.map((arg) => renderExpr(arg, contract));
263
+
264
+ let result = expr.lowering.template;
265
+ result = result.replace(/\{\{self\}\}/g, self);
266
+ for (let i = 0; i < args.length; i++) {
267
+ result = result.replace(new RegExp(`\\{\\{arg${i}\\}\\}`, 'g'), args[i] ?? '');
268
+ }
269
+
270
+ return result;
271
+ }
272
+
273
+ function renderSubqueryExpr(expr: SubqueryExpr, contract?: SqliteContract): string {
274
+ if (expr.query.projection.length !== 1) {
275
+ throw new Error('Subquery expressions must project exactly one column');
276
+ }
277
+ return `(${renderSelect(expr.query, contract)})`;
278
+ }
279
+
280
+ function renderNullCheck(expr: NullCheckExpr, contract?: SqliteContract): string {
281
+ const rendered = renderExpr(expr.expr, contract);
282
+ const renderedExpr =
283
+ expr.expr.kind === 'operation' || expr.expr.kind === 'subquery' ? `(${rendered})` : rendered;
284
+ return expr.isNull ? `${renderedExpr} IS NULL` : `${renderedExpr} IS NOT NULL`;
285
+ }
286
+
287
+ function renderBinary(expr: BinaryExpr, contract?: SqliteContract): string {
288
+ if (expr.right.kind === 'list' && expr.right.values.length === 0) {
289
+ if (expr.op === 'in') {
290
+ return 'FALSE';
291
+ }
292
+ if (expr.op === 'notIn') {
293
+ return 'TRUE';
294
+ }
295
+ }
296
+
297
+ const leftExpr = expr.left;
298
+ const left = renderExpr(leftExpr, contract);
299
+ const leftRendered =
300
+ leftExpr.kind === 'operation' || leftExpr.kind === 'subquery' ? `(${left})` : left;
301
+
302
+ const rightNode = expr.right;
303
+ let right: string;
304
+ switch (rightNode.kind) {
305
+ case 'list':
306
+ right = renderListLiteral(rightNode);
307
+ break;
308
+ case 'literal':
309
+ right = renderLiteral(rightNode);
310
+ break;
311
+ case 'column-ref':
312
+ right = renderColumn(rightNode);
313
+ break;
314
+ case 'param-ref':
315
+ right = '?';
316
+ break;
317
+ default:
318
+ right = renderExpr(rightNode, contract);
319
+ break;
320
+ }
321
+
322
+ // ilike is not supported by SQLite — waiting for prisma/prisma-next#277
323
+ // to move ilike into an extension so it can be properly capability-gated.
324
+ if (expr.op === 'ilike') {
325
+ throw new Error('SQLite does not support ILIKE. Use LIKE for case-sensitive matching.');
326
+ }
327
+
328
+ const operatorMap: Record<BinaryExpr['op'], string> = {
329
+ eq: '=',
330
+ neq: '!=',
331
+ gt: '>',
332
+ lt: '<',
333
+ gte: '>=',
334
+ lte: '<=',
335
+ like: 'LIKE',
336
+ ilike: 'LIKE', // unreachable — guarded above; kept to satisfy Record<op, string>
337
+ in: 'IN',
338
+ notIn: 'NOT IN',
339
+ };
340
+
341
+ return `${leftRendered} ${operatorMap[expr.op]} ${right}`;
342
+ }
343
+
344
+ function renderListLiteral(expr: ListExpression): string {
345
+ if (expr.values.length === 0) {
346
+ return '(NULL)';
347
+ }
348
+ const values = expr.values
349
+ .map((v) => {
350
+ if (v.kind === 'param-ref') return '?';
351
+ if (v.kind === 'literal') return renderLiteral(v);
352
+ return renderExpr(v);
353
+ })
354
+ .join(', ');
355
+ return `(${values})`;
356
+ }
357
+
358
+ function renderAggregateExpr(expr: AggregateExpr, contract?: SqliteContract): string {
359
+ const fn = expr.fn.toUpperCase();
360
+ if (!expr.expr) {
361
+ return `${fn}(*)`;
362
+ }
363
+ return `${fn}(${renderExpr(expr.expr, contract)})`;
364
+ }
365
+
366
+ function renderJsonObjectExpr(expr: JsonObjectExpr, contract?: SqliteContract): string {
367
+ const args = expr.entries
368
+ .flatMap((entry): [string, string] => {
369
+ const key = `'${escapeLiteral(entry.key)}'`;
370
+ if (entry.value.kind === 'literal') {
371
+ return [key, renderLiteral(entry.value)];
372
+ }
373
+ return [key, renderExpr(entry.value, contract)];
374
+ })
375
+ .join(', ');
376
+ return `json_object(${args})`;
377
+ }
378
+
379
+ function renderOrderByItems(items: ReadonlyArray<OrderByItem>, contract?: SqliteContract): string {
380
+ return items
381
+ .map((item) => `${renderExpr(item.expr, contract)} ${item.dir.toUpperCase()}`)
382
+ .join(', ');
383
+ }
384
+
385
+ function renderJsonArrayAggExpr(expr: JsonArrayAggExpr, contract?: SqliteContract): string {
386
+ const aggregateOrderBy =
387
+ expr.orderBy && expr.orderBy.length > 0
388
+ ? ` ORDER BY ${renderOrderByItems(expr.orderBy, contract)}`
389
+ : '';
390
+ const aggregated = `json_group_array(${renderExpr(expr.expr, contract)}${aggregateOrderBy})`;
391
+ if (expr.onEmpty === 'emptyArray') {
392
+ return `coalesce(${aggregated}, '[]')`;
393
+ }
394
+ return aggregated;
395
+ }
396
+
397
+ function renderJoin(join: JoinAst, contract?: SqliteContract): string {
398
+ const joinType = join.joinType.toUpperCase();
399
+ const source = renderSource(join.source, contract);
400
+ const onClause = renderJoinOn(join.on, contract);
401
+ return `${joinType} JOIN ${source} ON ${onClause}`;
402
+ }
403
+
404
+ function renderJoinOn(on: JoinOnExpr, contract?: SqliteContract): string {
405
+ if (on.kind === 'eq-col-join-on') {
406
+ return `${renderColumn(on.left)} = ${renderColumn(on.right)}`;
407
+ }
408
+ return renderExpr(on, contract);
409
+ }
410
+
411
+ function renderInsertValue(value: InsertValue): string {
412
+ switch (value.kind) {
413
+ case 'param-ref':
414
+ return '?';
415
+ case 'column-ref':
416
+ return renderColumn(value);
417
+ case 'default-value':
418
+ throw new Error('SQLite does not support DEFAULT as a value in INSERT ... VALUES');
419
+ default:
420
+ throw new Error(`Unsupported value node in INSERT: ${(value as { kind: string }).kind}`);
421
+ }
422
+ }
423
+
424
+ function renderInsert(ast: InsertAst): string {
425
+ const table = quoteIdentifier(ast.table.name);
426
+ const rows = ast.rows;
427
+ if (rows.length === 0) {
428
+ throw new Error('INSERT requires at least one row');
429
+ }
430
+
431
+ const firstRow = rows[0] as Readonly<Record<string, InsertValue>>;
432
+ const columnOrder = Object.keys(firstRow);
433
+
434
+ let insertClause: string;
435
+ if (columnOrder.length === 0) {
436
+ insertClause = `INSERT INTO ${table} DEFAULT VALUES`;
437
+ } else {
438
+ const columns = columnOrder.map((column) => quoteIdentifier(column));
439
+ const values = rows
440
+ .map((row) => {
441
+ const renderedRow = columnOrder.map((column) => {
442
+ const value = row[column];
443
+ if (value === undefined) {
444
+ throw new Error(`Missing value for column "${column}" in INSERT row`);
445
+ }
446
+ return renderInsertValue(value);
447
+ });
448
+ return `(${renderedRow.join(', ')})`;
449
+ })
450
+ .join(', ');
451
+ insertClause = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${values}`;
452
+ }
453
+
454
+ let onConflictClause = '';
455
+ if (ast.onConflict) {
456
+ const conflictColumns = ast.onConflict.columns.map((col) => quoteIdentifier(col.column));
457
+ if (conflictColumns.length === 0) {
458
+ throw new Error('INSERT onConflict requires at least one conflict column');
459
+ }
460
+
461
+ const action = ast.onConflict.action;
462
+ switch (action.kind) {
463
+ case 'do-nothing':
464
+ onConflictClause = ` ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
465
+ break;
466
+ case 'do-update-set': {
467
+ const updates = Object.entries(action.set).map(([colName, value]) => {
468
+ const target = quoteIdentifier(colName);
469
+ if (value.kind === 'param-ref') {
470
+ return `${target} = ?`;
471
+ }
472
+ return `${target} = ${renderColumn(value)}`;
473
+ });
474
+ onConflictClause = ` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updates.join(', ')}`;
475
+ break;
476
+ }
477
+ default:
478
+ throw new Error(`Unsupported onConflict action: ${(action as { kind: string }).kind}`);
479
+ }
480
+ }
481
+
482
+ const returningClause = renderReturning(ast.returning);
483
+
484
+ return `${insertClause}${onConflictClause}${returningClause}`;
485
+ }
486
+
487
+ function renderUpdate(ast: UpdateAst, contract: SqliteContract): string {
488
+ const table = quoteIdentifier(ast.table.name);
489
+ const setClauses = Object.entries(ast.set).map(([col, val]) => {
490
+ const column = quoteIdentifier(col);
491
+ let value: string;
492
+ switch (val.kind) {
493
+ case 'param-ref':
494
+ value = '?';
495
+ break;
496
+ case 'column-ref':
497
+ value = renderColumn(val);
498
+ break;
499
+ default:
500
+ throw new Error(`Unsupported value node in UPDATE: ${(val as { kind: string }).kind}`);
501
+ }
502
+ return `${column} = ${value}`;
503
+ });
504
+
505
+ const whereClause = ast.where ? ` WHERE ${renderExpr(ast.where, contract)}` : '';
506
+ const returningClause = renderReturning(ast.returning);
507
+
508
+ return `UPDATE ${table} SET ${setClauses.join(', ')}${whereClause}${returningClause}`;
509
+ }
510
+
511
+ function renderDelete(ast: DeleteAst): string {
512
+ const table = quoteIdentifier(ast.table.name);
513
+ const whereClause = ast.where ? ` WHERE ${renderExpr(ast.where)}` : '';
514
+ const returningClause = renderReturning(ast.returning);
515
+
516
+ return `DELETE FROM ${table}${whereClause}${returningClause}`;
517
+ }
518
+
519
+ function renderReturning(returning: ReadonlyArray<ColumnRef> | undefined): string {
520
+ if (!returning?.length) {
521
+ return '';
522
+ }
523
+ return ` RETURNING ${returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`;
524
+ }
525
+
526
+ export function createSqliteAdapter(options?: SqliteAdapterOptions) {
527
+ return Object.freeze(new SqliteAdapterImpl(options));
528
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ SQL_CHAR_CODEC_ID,
3
+ SQL_FLOAT_CODEC_ID,
4
+ SQL_INT_CODEC_ID,
5
+ SQL_VARCHAR_CODEC_ID,
6
+ } from '@prisma-next/sql-relational-core/ast';
7
+ export const SQLITE_TEXT_CODEC_ID = 'sqlite/text@1' as const;
8
+ export const SQLITE_INTEGER_CODEC_ID = 'sqlite/integer@1' as const;
9
+ export const SQLITE_REAL_CODEC_ID = 'sqlite/real@1' as const;
10
+ export const SQLITE_BLOB_CODEC_ID = 'sqlite/blob@1' as const;
11
+ export const SQLITE_BOOLEAN_CODEC_ID = 'sqlite/boolean@1' as const;
12
+ export const SQLITE_DATETIME_CODEC_ID = 'sqlite/datetime@1' as const;
13
+ export const SQLITE_JSON_CODEC_ID = 'sqlite/json@1' as const;
14
+ export const SQLITE_BIGINT_CODEC_ID = 'sqlite/bigint@1' as const;
@@ -0,0 +1,108 @@
1
+ import { codec, defineCodecs, sqlCodecDefinitions } from '@prisma-next/sql-relational-core/ast';
2
+ import {
3
+ SQLITE_BIGINT_CODEC_ID,
4
+ SQLITE_BLOB_CODEC_ID,
5
+ SQLITE_BOOLEAN_CODEC_ID,
6
+ SQLITE_DATETIME_CODEC_ID,
7
+ SQLITE_INTEGER_CODEC_ID,
8
+ SQLITE_JSON_CODEC_ID,
9
+ SQLITE_REAL_CODEC_ID,
10
+ SQLITE_TEXT_CODEC_ID,
11
+ } from './codec-ids';
12
+
13
+ const sqlCharCodec = sqlCodecDefinitions.char.codec;
14
+ const sqlVarcharCodec = sqlCodecDefinitions.varchar.codec;
15
+ const sqlIntCodec = sqlCodecDefinitions.int.codec;
16
+ const sqlFloatCodec = sqlCodecDefinitions.float.codec;
17
+
18
+ export type JsonValue =
19
+ | string
20
+ | number
21
+ | boolean
22
+ | null
23
+ | { readonly [key: string]: JsonValue }
24
+ | readonly JsonValue[];
25
+
26
+ const sqliteTextCodec = codec({
27
+ typeId: SQLITE_TEXT_CODEC_ID,
28
+ targetTypes: ['text'],
29
+ traits: ['equality', 'order', 'textual'],
30
+ encode: (value: string): string => value,
31
+ decode: (wire: string): string => wire,
32
+ });
33
+
34
+ const sqliteIntegerCodec = codec({
35
+ typeId: SQLITE_INTEGER_CODEC_ID,
36
+ targetTypes: ['integer'],
37
+ traits: ['equality', 'order', 'numeric'],
38
+ encode: (value: number): number => value,
39
+ decode: (wire: number): number => wire,
40
+ });
41
+
42
+ const sqliteRealCodec = codec({
43
+ typeId: SQLITE_REAL_CODEC_ID,
44
+ targetTypes: ['real'],
45
+ traits: ['equality', 'order', 'numeric'],
46
+ encode: (value: number): number => value,
47
+ decode: (wire: number): number => wire,
48
+ });
49
+
50
+ const sqliteBlobCodec = codec({
51
+ typeId: SQLITE_BLOB_CODEC_ID,
52
+ targetTypes: ['blob'],
53
+ traits: ['equality'],
54
+ encode: (value: Uint8Array): Uint8Array => value,
55
+ decode: (wire: Uint8Array): Uint8Array => wire,
56
+ });
57
+
58
+ const sqliteBooleanCodec = codec({
59
+ typeId: SQLITE_BOOLEAN_CODEC_ID,
60
+ targetTypes: ['integer'],
61
+ traits: ['equality', 'boolean'],
62
+ encode: (value: boolean): number => (value ? 1 : 0),
63
+ decode: (wire: number): boolean => wire !== 0,
64
+ });
65
+
66
+ const sqliteDatetimeCodec = codec({
67
+ typeId: SQLITE_DATETIME_CODEC_ID,
68
+ targetTypes: ['text'],
69
+ traits: ['equality', 'order'],
70
+ encode: (value: Date): string => value.toISOString(),
71
+ decode: (wire: string): Date => new Date(wire),
72
+ });
73
+
74
+ const sqliteJsonCodec = codec({
75
+ typeId: SQLITE_JSON_CODEC_ID,
76
+ targetTypes: ['text'],
77
+ traits: ['equality'],
78
+ encode: (value: JsonValue): string => JSON.stringify(value),
79
+ decode: (wire: string | JsonValue): JsonValue =>
80
+ typeof wire === 'string' ? (JSON.parse(wire) as JsonValue) : wire,
81
+ });
82
+
83
+ const sqliteBigintCodec = codec({
84
+ typeId: SQLITE_BIGINT_CODEC_ID,
85
+ targetTypes: ['integer'],
86
+ traits: ['equality', 'order', 'numeric'],
87
+ encode: (value: bigint): number | bigint => value,
88
+ decode: (wire: number | bigint): bigint => BigInt(wire),
89
+ });
90
+
91
+ const codecs = defineCodecs()
92
+ .add('char', sqlCharCodec)
93
+ .add('varchar', sqlVarcharCodec)
94
+ .add('int', sqlIntCodec)
95
+ .add('float', sqlFloatCodec)
96
+ .add('text', sqliteTextCodec)
97
+ .add('integer', sqliteIntegerCodec)
98
+ .add('real', sqliteRealCodec)
99
+ .add('blob', sqliteBlobCodec)
100
+ .add('boolean', sqliteBooleanCodec)
101
+ .add('datetime', sqliteDatetimeCodec)
102
+ .add('json', sqliteJsonCodec)
103
+ .add('bigint', sqliteBigintCodec);
104
+
105
+ export const codecDefinitions = codecs.codecDefinitions;
106
+ export const dataTypes = codecs.dataTypes;
107
+
108
+ export type CodecTypes = typeof codecs.CodecTypes;
@@ -0,0 +1,50 @@
1
+ import {
2
+ SQLITE_BIGINT_CODEC_ID,
3
+ SQLITE_BLOB_CODEC_ID,
4
+ SQLITE_BOOLEAN_CODEC_ID,
5
+ SQLITE_DATETIME_CODEC_ID,
6
+ SQLITE_INTEGER_CODEC_ID,
7
+ SQLITE_JSON_CODEC_ID,
8
+ SQLITE_REAL_CODEC_ID,
9
+ SQLITE_TEXT_CODEC_ID,
10
+ } from './codec-ids';
11
+
12
+ export const textColumn = {
13
+ codecId: SQLITE_TEXT_CODEC_ID,
14
+ nativeType: 'text',
15
+ } as const;
16
+
17
+ export const integerColumn = {
18
+ codecId: SQLITE_INTEGER_CODEC_ID,
19
+ nativeType: 'integer',
20
+ } as const;
21
+
22
+ export const realColumn = {
23
+ codecId: SQLITE_REAL_CODEC_ID,
24
+ nativeType: 'real',
25
+ } as const;
26
+
27
+ export const blobColumn = {
28
+ codecId: SQLITE_BLOB_CODEC_ID,
29
+ nativeType: 'blob',
30
+ } as const;
31
+
32
+ export const booleanColumn = {
33
+ codecId: SQLITE_BOOLEAN_CODEC_ID,
34
+ nativeType: 'integer',
35
+ } as const;
36
+
37
+ export const datetimeColumn = {
38
+ codecId: SQLITE_DATETIME_CODEC_ID,
39
+ nativeType: 'text',
40
+ } as const;
41
+
42
+ export const jsonColumn = {
43
+ codecId: SQLITE_JSON_CODEC_ID,
44
+ nativeType: 'text',
45
+ } as const;
46
+
47
+ export const bigintColumn = {
48
+ codecId: SQLITE_BIGINT_CODEC_ID,
49
+ nativeType: 'integer',
50
+ } as const;
@@ -0,0 +1,18 @@
1
+ import type {
2
+ ControlAdapterDescriptor,
3
+ ControlAdapterInstance,
4
+ } from '@prisma-next/framework-components/control';
5
+ import { sqliteAdapterDescriptorMeta } from './descriptor-meta';
6
+
7
+ const sqliteControlAdapterDescriptor: ControlAdapterDescriptor<
8
+ 'sql',
9
+ 'sqlite',
10
+ ControlAdapterInstance<'sql', 'sqlite'>
11
+ > = {
12
+ ...sqliteAdapterDescriptorMeta,
13
+ create(): ControlAdapterInstance<'sql', 'sqlite'> {
14
+ return { familyId: 'sql', targetId: 'sqlite' };
15
+ },
16
+ };
17
+
18
+ export default sqliteControlAdapterDescriptor;
@@ -0,0 +1,26 @@
1
+ export const sqliteAdapterDescriptorMeta = {
2
+ kind: 'adapter',
3
+ familyId: 'sql',
4
+ targetId: 'sqlite',
5
+ id: 'sqlite',
6
+ version: '0.0.1',
7
+ capabilities: {
8
+ sql: {
9
+ orderBy: true,
10
+ limit: true,
11
+ lateral: false,
12
+ jsonAgg: true,
13
+ returning: true,
14
+ enums: false,
15
+ },
16
+ },
17
+ types: {
18
+ codecTypes: {
19
+ import: {
20
+ package: '@prisma-next/adapter-sqlite/codec-types',
21
+ named: 'CodecTypes',
22
+ alias: 'SqliteTypes',
23
+ },
24
+ },
25
+ },
26
+ } as const;
@@ -0,0 +1,33 @@
1
+ import type { RuntimeAdapterInstance } from '@prisma-next/framework-components/execution';
2
+ import type { CodecRegistry } from '@prisma-next/sql-relational-core/ast';
3
+ import { createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
4
+ import type { SqlRuntimeAdapterDescriptor } from '@prisma-next/sql-runtime';
5
+ import { createSqliteAdapter } from './adapter';
6
+ import { codecDefinitions } from './codecs';
7
+ import { sqliteAdapterDescriptorMeta } from './descriptor-meta';
8
+
9
+ export type SqliteRuntimeAdapterInstance = RuntimeAdapterInstance<'sql', 'sqlite'> &
10
+ ReturnType<typeof createSqliteAdapter>;
11
+
12
+ function createSqliteCodecRegistry(): CodecRegistry {
13
+ const registry = createCodecRegistry();
14
+ for (const definition of Object.values(codecDefinitions)) {
15
+ registry.register(definition.codec);
16
+ }
17
+ return registry;
18
+ }
19
+
20
+ const sqliteRuntimeAdapterDescriptor: SqlRuntimeAdapterDescriptor<
21
+ 'sqlite',
22
+ SqliteRuntimeAdapterInstance
23
+ > = {
24
+ ...sqliteAdapterDescriptorMeta,
25
+ codecs: createSqliteCodecRegistry,
26
+ parameterizedCodecs: () => [],
27
+ mutationDefaultGenerators: () => [],
28
+ create(): SqliteRuntimeAdapterInstance {
29
+ return createSqliteAdapter();
30
+ },
31
+ };
32
+
33
+ export default sqliteRuntimeAdapterDescriptor;
@@ -0,0 +1,35 @@
1
+ export class SqlEscapeError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly value: string,
5
+ public readonly kind: 'identifier' | 'literal',
6
+ ) {
7
+ super(message);
8
+ this.name = 'SqlEscapeError';
9
+ }
10
+ }
11
+
12
+ export function quoteIdentifier(identifier: string): string {
13
+ if (identifier.length === 0) {
14
+ throw new SqlEscapeError('Identifier cannot be empty', identifier, 'identifier');
15
+ }
16
+ if (identifier.includes('\0')) {
17
+ throw new SqlEscapeError(
18
+ 'Identifier cannot contain null bytes',
19
+ identifier.replace(/\0/g, '\\0'),
20
+ 'identifier',
21
+ );
22
+ }
23
+ return `"${identifier.replace(/"/g, '""')}"`;
24
+ }
25
+
26
+ export function escapeLiteral(value: string): string {
27
+ if (value.includes('\0')) {
28
+ throw new SqlEscapeError(
29
+ 'Literal value cannot contain null bytes',
30
+ value.replace(/\0/g, '\\0'),
31
+ 'literal',
32
+ );
33
+ }
34
+ return value.replace(/'/g, "''");
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
+ import type { LoweredStatement } from '@prisma-next/sql-relational-core/ast';
4
+
5
+ export interface SqliteAdapterOptions {
6
+ readonly profileId?: string;
7
+ }
8
+
9
+ export type SqliteContract = Contract<SqlStorage> & { readonly target: 'sqlite' };
10
+
11
+ export type SqliteLoweredStatement = LoweredStatement;
@@ -0,0 +1 @@
1
+ export { createSqliteAdapter } from '../core/adapter';
@@ -0,0 +1,6 @@
1
+ import type { CodecTypes as CoreCodecTypes, JsonValue } from '../core/codecs';
2
+
3
+ export type CodecTypes = CoreCodecTypes;
4
+
5
+ export type { JsonValue };
6
+ export { dataTypes } from '../core/codecs';
@@ -0,0 +1,10 @@
1
+ export {
2
+ bigintColumn,
3
+ blobColumn,
4
+ booleanColumn,
5
+ datetimeColumn,
6
+ integerColumn,
7
+ jsonColumn,
8
+ realColumn,
9
+ textColumn,
10
+ } from '../core/column-types';
@@ -0,0 +1 @@
1
+ export { default } from '../core/control-adapter';
@@ -0,0 +1,2 @@
1
+ export type { SqliteRuntimeAdapterInstance } from '../core/runtime-adapter';
2
+ export { default } from '../core/runtime-adapter';
@@ -0,0 +1 @@
1
+ export type { SqliteContract, SqliteLoweredStatement } from '../core/types';