@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 +194 -0
- package/package.json +59 -0
- package/src/core/adapter.ts +528 -0
- package/src/core/codec-ids.ts +14 -0
- package/src/core/codecs.ts +108 -0
- package/src/core/column-types.ts +50 -0
- package/src/core/control-adapter.ts +18 -0
- package/src/core/descriptor-meta.ts +26 -0
- package/src/core/runtime-adapter.ts +33 -0
- package/src/core/sql-utils.ts +35 -0
- package/src/core/types.ts +11 -0
- package/src/exports/adapter.ts +1 -0
- package/src/exports/codec-types.ts +6 -0
- package/src/exports/column-types.ts +10 -0
- package/src/exports/control.ts +1 -0
- package/src/exports/runtime.ts +2 -0
- package/src/exports/types.ts +1 -0
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 @@
|
|
|
1
|
+
export { default } from '../core/control-adapter';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { SqliteContract, SqliteLoweredStatement } from '../core/types';
|