@prisma-next/driver-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 +121 -0
- package/package.json +48 -0
- package/src/core/control-driver.ts +54 -0
- package/src/core/descriptor-meta.ts +8 -0
- package/src/core/runtime-driver.ts +19 -0
- package/src/exports/control.ts +1 -0
- package/src/exports/runtime.ts +3 -0
- package/src/normalize-error.ts +105 -0
- package/src/sqlite-driver.ts +276 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @prisma-next/driver-sqlite
|
|
2
|
+
|
|
3
|
+
SQLite driver for Prisma Next.
|
|
4
|
+
|
|
5
|
+
## Package Classification
|
|
6
|
+
|
|
7
|
+
- **Domain**: targets
|
|
8
|
+
- **Layer**: drivers
|
|
9
|
+
- **Plane**: multi-plane (migration, runtime)
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The SQLite driver provides transport and connection management for SQLite databases using Node.js built-in `node:sqlite` module (`DatabaseSync`). It implements the `SqlDriver` interface for executing SQL statements, explaining queries, and managing connections.
|
|
14
|
+
|
|
15
|
+
In Prisma Next, "driver" refers to the Prisma Next interface (not the underlying `node:sqlite` API). Drivers own connection management and transport, but contain no dialect-specific logic. All dialect behavior lives in adapters. Instantiation is separate from connection; `create()` returns an unbound driver, `connect(binding)` binds at the boundary ([ADR 159](../../../../docs/architecture%20docs/adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)).
|
|
16
|
+
|
|
17
|
+
This package spans multiple planes:
|
|
18
|
+
- **Migration plane** (`src/exports/control.ts`): Control plane entry point for driver descriptors
|
|
19
|
+
- **Runtime plane** (`src/exports/runtime.ts`): Runtime entry point for driver implementation
|
|
20
|
+
|
|
21
|
+
## Purpose
|
|
22
|
+
|
|
23
|
+
Provide SQLite transport and connection management. Execute SQL statements and manage connections without dialect-specific logic.
|
|
24
|
+
|
|
25
|
+
## Responsibilities
|
|
26
|
+
|
|
27
|
+
- **Connection Management**: Acquire and release database connections via `node:sqlite` `DatabaseSync`
|
|
28
|
+
- **Statement Execution**: Execute SQL statements with positional `?` parameters
|
|
29
|
+
- **Query Explanation**: Execute `EXPLAIN QUERY PLAN` queries for query analysis
|
|
30
|
+
- **Persistent Connection**: Top-level `execute()`/`query()`/`explain()` reuse a persistent connection opened at `connect()` time
|
|
31
|
+
- **Scoped Connections**: `acquireConnection()` opens fresh `DatabaseSync` handles for isolated scopes (transactions)
|
|
32
|
+
- **Transaction Support**: `BEGIN`/`COMMIT`/`ROLLBACK` via `SqliteTransactionImpl`
|
|
33
|
+
- **PRAGMA Configuration**: Enables `PRAGMA foreign_keys = ON` and `PRAGMA busy_timeout = 5000` on every opened connection
|
|
34
|
+
- **Error Normalization**: Maps SQLite extended error codes to SQL state codes (23505 unique, 23503 FK, 23502 NOT NULL) and distinguishes transient (BUSY/LOCKED) from permanent errors
|
|
35
|
+
|
|
36
|
+
**Non-goals:**
|
|
37
|
+
- Dialect-specific SQL lowering (adapters)
|
|
38
|
+
- Query compilation (sql-query)
|
|
39
|
+
- Runtime execution orchestration (sql-runtime)
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
flowchart TD
|
|
45
|
+
subgraph "Runtime"
|
|
46
|
+
RT[Runtime]
|
|
47
|
+
ADAPTER[Adapter]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph "SQLite Driver"
|
|
51
|
+
DRIVER[Driver]
|
|
52
|
+
CONN[Persistent Connection]
|
|
53
|
+
SCOPED[Scoped Connection]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
subgraph "SQLite"
|
|
57
|
+
DB[(SQLite File / :memory:)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
RT --> ADAPTER
|
|
61
|
+
ADAPTER --> DRIVER
|
|
62
|
+
DRIVER --> CONN
|
|
63
|
+
DRIVER --> SCOPED
|
|
64
|
+
CONN --> DB
|
|
65
|
+
SCOPED --> DB
|
|
66
|
+
DB --> DRIVER
|
|
67
|
+
DRIVER --> RT
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Components
|
|
71
|
+
|
|
72
|
+
### Driver (`sqlite-driver.ts`)
|
|
73
|
+
- `SqliteBoundDriver`: main driver implementation wrapping `DatabaseSync`
|
|
74
|
+
- `SqliteConnectionImpl`: connection wrapper with `execute()`, `query()`, `explain()`, `beginTransaction()`
|
|
75
|
+
- `SqliteTransactionImpl`: transaction wrapper adding `commit()` and `rollback()`
|
|
76
|
+
- `createBoundDriverFromBinding()`: factory that creates and immediately connects a driver
|
|
77
|
+
|
|
78
|
+
### Error Normalization (`normalize-error.ts`)
|
|
79
|
+
- Maps SQLite extended `errcode` to structured `SqlQueryError` / `SqlConnectionError`
|
|
80
|
+
- Handles constraint types individually (UNIQUE, FK, NOT NULL, CHECK)
|
|
81
|
+
|
|
82
|
+
## Dependencies
|
|
83
|
+
|
|
84
|
+
- **`@prisma-next/sql-relational-core`**: SQL contract types (`SqlDriver`, `SqlConnection`, `SqlTransaction`)
|
|
85
|
+
- **`@prisma-next/framework-components`**: Descriptor types (`RuntimeDriverDescriptor`, `ControlDriverDescriptor`)
|
|
86
|
+
- **`@prisma-next/errors`**: Structured error factories
|
|
87
|
+
|
|
88
|
+
## Related Subsystems
|
|
89
|
+
|
|
90
|
+
- **[Adapters & Targets](../../../../docs/architecture%20docs/subsystems/5.%20Adapters%20&%20Targets.md)**: Driver specification
|
|
91
|
+
|
|
92
|
+
## Related ADRs
|
|
93
|
+
|
|
94
|
+
- [ADR 159 -- Driver Terminology and Lifecycle](../../../../docs/architecture%20docs/adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)
|
|
95
|
+
- [ADR 005 -- Thin Core Fat Targets](../../../../docs/architecture%20docs/adrs/ADR%20005%20-%20Thin%20Core%20Fat%20Targets.md)
|
|
96
|
+
- [ADR 016 -- Adapter SPI for Lowering](../../../../docs/architecture%20docs/adrs/ADR%20016%20-%20Adapter%20SPI%20for%20Lowering.md)
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
Use the descriptor + connect lifecycle:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import sqliteDriver from '@prisma-next/driver-sqlite/runtime';
|
|
104
|
+
|
|
105
|
+
const driver = sqliteDriver.create();
|
|
106
|
+
await driver.connect({ kind: 'path', path: ':memory:' });
|
|
107
|
+
// driver is now bound; use acquireConnection, query, execute, etc.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Binding:
|
|
111
|
+
- `{ kind: 'path', path: ':memory:' }`: In-memory database
|
|
112
|
+
- `{ kind: 'path', path: './data.db' }`: File-based database
|
|
113
|
+
|
|
114
|
+
## Exports
|
|
115
|
+
|
|
116
|
+
- `./runtime`: Runtime entry point for driver implementation
|
|
117
|
+
- Default: `sqliteRuntimeDriverDescriptor` -- use `create()` for unbound driver, then `connect(binding)`
|
|
118
|
+
- Types: `SqliteBinding`, `SqliteRuntimeDriver`
|
|
119
|
+
- `./control`: Control plane entry point for driver descriptors
|
|
120
|
+
- Default: `ControlDriverDescriptor` for use in CLI config
|
|
121
|
+
- `SqliteControlDriver` class for direct control-plane usage
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prisma-next/driver-sqlite",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsdown",
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"test:coverage": "vitest run --coverage",
|
|
10
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
11
|
+
"lint": "biome check . --error-on-warnings",
|
|
12
|
+
"lint:fix": "biome check --write .",
|
|
13
|
+
"lint:fix:unsafe": "biome check --write --unsafe .",
|
|
14
|
+
"clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@prisma-next/contract": "workspace:*",
|
|
18
|
+
"@prisma-next/errors": "workspace:*",
|
|
19
|
+
"@prisma-next/framework-components": "workspace:*",
|
|
20
|
+
"@prisma-next/sql-contract": "workspace:*",
|
|
21
|
+
"@prisma-next/sql-errors": "workspace:*",
|
|
22
|
+
"@prisma-next/sql-operations": "workspace:*",
|
|
23
|
+
"@prisma-next/sql-relational-core": "workspace:*",
|
|
24
|
+
"@prisma-next/utils": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@prisma-next/test-utils": "workspace:*",
|
|
28
|
+
"@prisma-next/tsconfig": "workspace:*",
|
|
29
|
+
"@prisma-next/tsdown": "workspace:*",
|
|
30
|
+
"tsdown": "catalog:",
|
|
31
|
+
"typescript": "catalog:",
|
|
32
|
+
"vitest": "catalog:"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
"./control": "./dist/control.mjs",
|
|
40
|
+
"./runtime": "./dist/runtime.mjs",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/prisma/prisma-next.git",
|
|
46
|
+
"directory": "packages/3-targets/7-drivers/sqlite"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SQLInputValue } from 'node:sqlite';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { errorRuntime } from '@prisma-next/errors/execution';
|
|
4
|
+
import type {
|
|
5
|
+
ControlDriverDescriptor,
|
|
6
|
+
ControlDriverInstance,
|
|
7
|
+
} from '@prisma-next/framework-components/control';
|
|
8
|
+
import { normalizeSqliteError } from '../normalize-error';
|
|
9
|
+
import { sqliteDriverDescriptorMeta } from './descriptor-meta';
|
|
10
|
+
|
|
11
|
+
export class SqliteControlDriver implements ControlDriverInstance<'sql', 'sqlite'> {
|
|
12
|
+
readonly familyId = 'sql' as const;
|
|
13
|
+
readonly targetId = 'sqlite' as const;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly db: DatabaseSync) {}
|
|
16
|
+
|
|
17
|
+
async query<Row = Record<string, unknown>>(
|
|
18
|
+
sql: string,
|
|
19
|
+
params?: readonly unknown[],
|
|
20
|
+
): Promise<{ readonly rows: Row[] }> {
|
|
21
|
+
try {
|
|
22
|
+
const stmt = this.db.prepare(sql);
|
|
23
|
+
const rows = stmt.all(...((params ?? []) as SQLInputValue[])) as Row[];
|
|
24
|
+
return { rows };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw normalizeSqliteError(error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async close(): Promise<void> {
|
|
31
|
+
this.db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sqliteDriverDescriptor: ControlDriverDescriptor<'sql', 'sqlite', SqliteControlDriver> = {
|
|
36
|
+
...sqliteDriverDescriptorMeta,
|
|
37
|
+
async create(pathOrMemory: string): Promise<SqliteControlDriver> {
|
|
38
|
+
try {
|
|
39
|
+
const db = new DatabaseSync(pathOrMemory);
|
|
40
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
41
|
+
return new SqliteControlDriver(db);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw errorRuntime('Database connection failed', {
|
|
44
|
+
why: error instanceof Error ? error.message : String(error),
|
|
45
|
+
fix: 'Verify the database file path exists and is accessible',
|
|
46
|
+
meta: {
|
|
47
|
+
path: pathOrMemory,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default sqliteDriverDescriptor;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RuntimeDriverDescriptor } from '@prisma-next/framework-components/execution';
|
|
2
|
+
import { SqliteDriver, type SqliteRuntimeDriver } from '../sqlite-driver';
|
|
3
|
+
import { sqliteDriverDescriptorMeta } from './descriptor-meta';
|
|
4
|
+
|
|
5
|
+
export type { SqliteRuntimeDriver } from '../sqlite-driver';
|
|
6
|
+
|
|
7
|
+
const sqliteRuntimeDriverDescriptor: RuntimeDriverDescriptor<
|
|
8
|
+
'sql',
|
|
9
|
+
'sqlite',
|
|
10
|
+
void,
|
|
11
|
+
SqliteRuntimeDriver
|
|
12
|
+
> = {
|
|
13
|
+
...sqliteDriverDescriptorMeta,
|
|
14
|
+
create(): SqliteRuntimeDriver {
|
|
15
|
+
return new SqliteDriver();
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default sqliteRuntimeDriverDescriptor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, SqliteControlDriver } from '../core/control-driver';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { SqlConnectionError, SqlQueryError } from '@prisma-next/sql-errors';
|
|
2
|
+
|
|
3
|
+
interface SqliteError extends Error {
|
|
4
|
+
readonly code?: string;
|
|
5
|
+
readonly errcode?: number;
|
|
6
|
+
readonly errstr?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// SQLite extended error code ranges (base code * 256 + extended)
|
|
10
|
+
// Base code 19 = SQLITE_CONSTRAINT
|
|
11
|
+
const SQLITE_CONSTRAINT_BASE = 19;
|
|
12
|
+
const SQLITE_CONSTRAINT_UNIQUE = 2067; // 19 + 8*256
|
|
13
|
+
const SQLITE_CONSTRAINT_PRIMARYKEY = 1555; // 19 + 6*256
|
|
14
|
+
const SQLITE_CONSTRAINT_FOREIGNKEY = 787; // 19 + 3*256
|
|
15
|
+
const SQLITE_CONSTRAINT_NOTNULL = 1299; // 19 + 5*256
|
|
16
|
+
const SQLITE_CONSTRAINT_CHECK = 275; // 19 + 1*256
|
|
17
|
+
|
|
18
|
+
// Base code 5 = SQLITE_BUSY
|
|
19
|
+
const SQLITE_BUSY = 5;
|
|
20
|
+
// Base code 6 = SQLITE_LOCKED
|
|
21
|
+
const SQLITE_LOCKED = 6;
|
|
22
|
+
|
|
23
|
+
function isConstraintError(errcode: number): boolean {
|
|
24
|
+
return (errcode & 0xff) === SQLITE_CONSTRAINT_BASE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isBusyOrLocked(errcode: number): boolean {
|
|
28
|
+
const base = errcode & 0xff;
|
|
29
|
+
return base === SQLITE_BUSY || base === SQLITE_LOCKED;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isSqliteError(error: unknown): error is SqliteError {
|
|
33
|
+
if (!(error instanceof Error)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return (error as SqliteError).code === 'ERR_SQLITE_ERROR';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function constraintNameFromMessage(message: string): string | undefined {
|
|
40
|
+
// SQLite constraint messages follow patterns like:
|
|
41
|
+
// "UNIQUE constraint failed: table.column"
|
|
42
|
+
// "FOREIGN KEY constraint failed"
|
|
43
|
+
// "NOT NULL constraint failed: table.column"
|
|
44
|
+
const match = /constraint failed: (.+)/.exec(message);
|
|
45
|
+
return match?.[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mapErrCodeToSqlState(errcode: number): string {
|
|
49
|
+
switch (errcode) {
|
|
50
|
+
case SQLITE_CONSTRAINT_UNIQUE:
|
|
51
|
+
case SQLITE_CONSTRAINT_PRIMARYKEY:
|
|
52
|
+
return '23505'; // unique_violation
|
|
53
|
+
case SQLITE_CONSTRAINT_FOREIGNKEY:
|
|
54
|
+
return '23503'; // foreign_key_violation
|
|
55
|
+
case SQLITE_CONSTRAINT_NOTNULL:
|
|
56
|
+
return '23502'; // not_null_violation
|
|
57
|
+
case SQLITE_CONSTRAINT_CHECK:
|
|
58
|
+
return '23514'; // check_violation
|
|
59
|
+
default:
|
|
60
|
+
if (isConstraintError(errcode)) {
|
|
61
|
+
return '23000'; // integrity_constraint_violation
|
|
62
|
+
}
|
|
63
|
+
return 'HY000'; // general error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeSqliteError(error: unknown): SqlQueryError | SqlConnectionError | Error {
|
|
68
|
+
if (!(error instanceof Error)) {
|
|
69
|
+
return new Error(String(error));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isSqliteError(error)) {
|
|
73
|
+
const sqliteErr = error as SqliteError;
|
|
74
|
+
const errcode = sqliteErr.errcode ?? 0;
|
|
75
|
+
|
|
76
|
+
if (isBusyOrLocked(errcode)) {
|
|
77
|
+
return new SqlConnectionError(error.message, {
|
|
78
|
+
cause: error,
|
|
79
|
+
transient: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sqlState = mapErrCodeToSqlState(errcode);
|
|
84
|
+
const constraint = constraintNameFromMessage(error.message);
|
|
85
|
+
|
|
86
|
+
return new SqlQueryError(error.message, {
|
|
87
|
+
cause: error,
|
|
88
|
+
sqlState,
|
|
89
|
+
...(constraint !== undefined ? { constraint } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Connection-related Node.js errors
|
|
94
|
+
if (
|
|
95
|
+
error.message.includes('database is locked') ||
|
|
96
|
+
error.message.includes('unable to open database')
|
|
97
|
+
) {
|
|
98
|
+
return new SqlConnectionError(error.message, {
|
|
99
|
+
cause: error,
|
|
100
|
+
transient: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return error;
|
|
105
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { SQLInputValue } from 'node:sqlite';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import type { RuntimeDriverInstance } from '@prisma-next/framework-components/execution';
|
|
4
|
+
import type {
|
|
5
|
+
SqlConnection,
|
|
6
|
+
SqlDriver,
|
|
7
|
+
SqlDriverState,
|
|
8
|
+
SqlExecuteRequest,
|
|
9
|
+
SqlExplainResult,
|
|
10
|
+
SqlQueryResult,
|
|
11
|
+
SqlTransaction,
|
|
12
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
13
|
+
import { normalizeSqliteError } from './normalize-error';
|
|
14
|
+
|
|
15
|
+
export type SqliteBinding = { readonly kind: 'path'; readonly path: string };
|
|
16
|
+
|
|
17
|
+
export type SqliteRuntimeDriver = RuntimeDriverInstance<'sql', 'sqlite'> & SqlDriver<SqliteBinding>;
|
|
18
|
+
|
|
19
|
+
interface DriverRuntimeError extends Error {
|
|
20
|
+
readonly code:
|
|
21
|
+
| 'DRIVER.NOT_CONNECTED'
|
|
22
|
+
| 'DRIVER.ALREADY_CONNECTED'
|
|
23
|
+
| 'DRIVER.EXPLAIN_NOT_SUPPORTED';
|
|
24
|
+
readonly category: 'RUNTIME';
|
|
25
|
+
readonly severity: 'error';
|
|
26
|
+
readonly details?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function driverError(
|
|
30
|
+
code: DriverRuntimeError['code'],
|
|
31
|
+
message: string,
|
|
32
|
+
details?: Record<string, unknown>,
|
|
33
|
+
): DriverRuntimeError {
|
|
34
|
+
const error = new Error(message) as DriverRuntimeError;
|
|
35
|
+
Object.defineProperty(error, 'name', {
|
|
36
|
+
value: 'RuntimeError',
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
return Object.assign(error, {
|
|
40
|
+
code,
|
|
41
|
+
category: 'RUNTIME' as const,
|
|
42
|
+
severity: 'error' as const,
|
|
43
|
+
message,
|
|
44
|
+
details,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NOT_CONNECTED_MESSAGE =
|
|
49
|
+
'SQLite driver not connected. Call connect(binding) before acquireConnection or execute.';
|
|
50
|
+
const ALREADY_CONNECTED_MESSAGE =
|
|
51
|
+
'SQLite driver already connected. Call close() before reconnecting with a new binding.';
|
|
52
|
+
|
|
53
|
+
function toSqliteParams(params: readonly unknown[] | undefined): SQLInputValue[] {
|
|
54
|
+
return (params ?? []) as SQLInputValue[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function openConnection(path: string): DatabaseSync {
|
|
58
|
+
try {
|
|
59
|
+
const db = new DatabaseSync(path);
|
|
60
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
61
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
62
|
+
return db;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw normalizeSqliteError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class SqliteConnectionImpl implements SqlConnection {
|
|
69
|
+
readonly #db: DatabaseSync;
|
|
70
|
+
|
|
71
|
+
constructor(db: DatabaseSync) {
|
|
72
|
+
this.#db = db;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async *execute<Row = Record<string, unknown>>(request: SqlExecuteRequest): AsyncIterable<Row> {
|
|
76
|
+
try {
|
|
77
|
+
const stmt = this.#db.prepare(request.sql);
|
|
78
|
+
for (const row of stmt.iterate(...toSqliteParams(request.params))) {
|
|
79
|
+
yield row as Row;
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw normalizeSqliteError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async explain(request: SqlExecuteRequest): Promise<SqlExplainResult> {
|
|
87
|
+
try {
|
|
88
|
+
const stmt = this.#db.prepare(`EXPLAIN QUERY PLAN ${request.sql}`);
|
|
89
|
+
const rows = stmt.all(...toSqliteParams(request.params)) as ReadonlyArray<
|
|
90
|
+
Record<string, unknown>
|
|
91
|
+
>;
|
|
92
|
+
return { rows };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw normalizeSqliteError(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async query<Row = Record<string, unknown>>(
|
|
99
|
+
sql: string,
|
|
100
|
+
params?: readonly unknown[],
|
|
101
|
+
): Promise<SqlQueryResult<Row>> {
|
|
102
|
+
try {
|
|
103
|
+
const stmt = this.#db.prepare(sql);
|
|
104
|
+
const rows = stmt.all(...toSqliteParams(params)) as Row[];
|
|
105
|
+
return { rows };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw normalizeSqliteError(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async beginTransaction(): Promise<SqlTransaction> {
|
|
112
|
+
try {
|
|
113
|
+
this.#db.exec('BEGIN');
|
|
114
|
+
return new SqliteTransactionImpl(this.#db);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw normalizeSqliteError(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async release(): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
this.#db.close();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw normalizeSqliteError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class SqliteTransactionImpl implements SqlTransaction {
|
|
130
|
+
readonly #db: DatabaseSync;
|
|
131
|
+
|
|
132
|
+
constructor(db: DatabaseSync) {
|
|
133
|
+
this.#db = db;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async *execute<Row = Record<string, unknown>>(request: SqlExecuteRequest): AsyncIterable<Row> {
|
|
137
|
+
try {
|
|
138
|
+
const stmt = this.#db.prepare(request.sql);
|
|
139
|
+
for (const row of stmt.iterate(...toSqliteParams(request.params))) {
|
|
140
|
+
yield row as Row;
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw normalizeSqliteError(error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async explain(request: SqlExecuteRequest): Promise<SqlExplainResult> {
|
|
148
|
+
try {
|
|
149
|
+
const stmt = this.#db.prepare(`EXPLAIN QUERY PLAN ${request.sql}`);
|
|
150
|
+
const rows = stmt.all(...toSqliteParams(request.params)) as ReadonlyArray<
|
|
151
|
+
Record<string, unknown>
|
|
152
|
+
>;
|
|
153
|
+
return { rows };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw normalizeSqliteError(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async query<Row = Record<string, unknown>>(
|
|
160
|
+
sql: string,
|
|
161
|
+
params?: readonly unknown[],
|
|
162
|
+
): Promise<SqlQueryResult<Row>> {
|
|
163
|
+
try {
|
|
164
|
+
const stmt = this.#db.prepare(sql);
|
|
165
|
+
const rows = stmt.all(...toSqliteParams(params)) as Row[];
|
|
166
|
+
return { rows };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw normalizeSqliteError(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async commit(): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
this.#db.exec('COMMIT');
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw normalizeSqliteError(error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async rollback(): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
this.#db.exec('ROLLBACK');
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw normalizeSqliteError(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface ConnectedState {
|
|
190
|
+
readonly kind: 'connected';
|
|
191
|
+
readonly path: string;
|
|
192
|
+
readonly conn: SqliteConnectionImpl;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type DriverState = { readonly kind: 'unbound' } | ConnectedState | { readonly kind: 'closed' };
|
|
196
|
+
|
|
197
|
+
export class SqliteDriver implements SqliteRuntimeDriver {
|
|
198
|
+
readonly familyId = 'sql' as const;
|
|
199
|
+
readonly targetId = 'sqlite' as const;
|
|
200
|
+
|
|
201
|
+
#state: DriverState;
|
|
202
|
+
|
|
203
|
+
constructor(initialState?: ConnectedState) {
|
|
204
|
+
this.#state = initialState ?? { kind: 'unbound' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#requireConnected(): ConnectedState {
|
|
208
|
+
if (this.#state.kind !== 'connected') {
|
|
209
|
+
throw driverError('DRIVER.NOT_CONNECTED', NOT_CONNECTED_MESSAGE);
|
|
210
|
+
}
|
|
211
|
+
return this.#state;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get state(): SqlDriverState {
|
|
215
|
+
return this.#state.kind;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async connect(binding: SqliteBinding): Promise<void> {
|
|
219
|
+
if (this.#state.kind === 'connected') {
|
|
220
|
+
throw driverError('DRIVER.ALREADY_CONNECTED', ALREADY_CONNECTED_MESSAGE, {
|
|
221
|
+
bindingKind: binding.kind,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
this.#state = {
|
|
225
|
+
kind: 'connected',
|
|
226
|
+
path: binding.path,
|
|
227
|
+
conn: new SqliteConnectionImpl(openConnection(binding.path)),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async acquireConnection(): Promise<SqliteConnectionImpl> {
|
|
232
|
+
const { path } = this.#requireConnected();
|
|
233
|
+
return new SqliteConnectionImpl(openConnection(path));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async close(): Promise<void> {
|
|
237
|
+
if (this.#state.kind !== 'connected') return;
|
|
238
|
+
const { conn } = this.#state;
|
|
239
|
+
this.#state = { kind: 'closed' };
|
|
240
|
+
await conn.release();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
execute<Row = Record<string, unknown>>(request: SqlExecuteRequest): AsyncIterable<Row> {
|
|
244
|
+
if (this.#state.kind !== 'connected') {
|
|
245
|
+
return {
|
|
246
|
+
[Symbol.asyncIterator]() {
|
|
247
|
+
return {
|
|
248
|
+
async next() {
|
|
249
|
+
throw driverError('DRIVER.NOT_CONNECTED', NOT_CONNECTED_MESSAGE);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return this.#state.conn.execute<Row>(request);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async explain(request: SqlExecuteRequest): Promise<SqlExplainResult> {
|
|
259
|
+
return this.#requireConnected().conn.explain(request);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async query<Row = Record<string, unknown>>(
|
|
263
|
+
sql: string,
|
|
264
|
+
params?: readonly unknown[],
|
|
265
|
+
): Promise<SqlQueryResult<Row>> {
|
|
266
|
+
return this.#requireConnected().conn.query<Row>(sql, params);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createBoundDriverFromBinding(binding: SqliteBinding): SqliteDriver {
|
|
271
|
+
return new SqliteDriver({
|
|
272
|
+
kind: 'connected',
|
|
273
|
+
path: binding.path,
|
|
274
|
+
conn: new SqliteConnectionImpl(openConnection(binding.path)),
|
|
275
|
+
});
|
|
276
|
+
}
|