@syncular/dialect-sqlite3 0.0.1-100
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/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/index.test.ts +66 -0
- package/src/index.ts +203 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/dialect-sqlite3 - node-sqlite3 dialect for sync
|
|
3
|
+
*
|
|
4
|
+
* Provides a Kysely dialect for the callback-based `sqlite3` npm package
|
|
5
|
+
* with SerializePlugin for automatic JSON serialization/deserialization.
|
|
6
|
+
* SQLite-compatible — use with @syncular/server-dialect-sqlite.
|
|
7
|
+
*
|
|
8
|
+
* Implements a custom Kysely Driver that wraps sqlite3's callback API
|
|
9
|
+
* into the promise-based interface Kysely expects.
|
|
10
|
+
*/
|
|
11
|
+
import { SerializePlugin } from '@syncular/core';
|
|
12
|
+
import type { DatabaseIntrospector, Dialect, DialectAdapter, Driver, QueryCompiler } from 'kysely';
|
|
13
|
+
import { Kysely } from 'kysely';
|
|
14
|
+
import sqlite3 from 'sqlite3';
|
|
15
|
+
export interface Sqlite3PathOptions {
|
|
16
|
+
/** Path to SQLite database file, or ':memory:' for in-memory */
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Sqlite3InstanceOptions {
|
|
20
|
+
/** An existing sqlite3.Database instance */
|
|
21
|
+
database: sqlite3.Database;
|
|
22
|
+
}
|
|
23
|
+
export type Sqlite3Options = Sqlite3PathOptions | Sqlite3InstanceOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Create a Kysely instance with node-sqlite3 dialect and SerializePlugin.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const db = createSqlite3Db<MyDb>({ path: './data.db' });
|
|
29
|
+
* const db = createSqlite3Db<MyDb>({ path: ':memory:' });
|
|
30
|
+
*/
|
|
31
|
+
export declare function createSqlite3Db<T>(options: Sqlite3Options): Kysely<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Create the sqlite3 dialect directly (without SerializePlugin).
|
|
34
|
+
*/
|
|
35
|
+
export declare function createSqlite3Dialect(options: Sqlite3Options): Sqlite3Dialect;
|
|
36
|
+
export declare function createSerializePlugin(): SerializePlugin;
|
|
37
|
+
declare class Sqlite3Dialect implements Dialect {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(options: Sqlite3Options);
|
|
40
|
+
createAdapter(): DialectAdapter;
|
|
41
|
+
createDriver(): Driver;
|
|
42
|
+
createQueryCompiler(): QueryCompiler;
|
|
43
|
+
createIntrospector(db: Kysely<any>): DatabaseIntrospector;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAEV,oBAAoB,EACpB,OAAO,EACP,cAAc,EACd,MAAM,EACN,aAAa,EAGd,MAAM,QAAQ,CAAC;AAChB,OAAO,EAEL,MAAM,EAIP,MAAM,QAAQ,CAAC;AAChB,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG,sBAAsB,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,CAKrE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAE5E;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAMD,cAAM,cAAe,YAAW,OAAO;;IAGrC,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,aAAa,IAAI,cAAc,CAE9B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,mBAAmB,IAAI,aAAa,CAEnC;IAED,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,oBAAoB,CAExD;CACF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/dialect-sqlite3 - node-sqlite3 dialect for sync
|
|
3
|
+
*
|
|
4
|
+
* Provides a Kysely dialect for the callback-based `sqlite3` npm package
|
|
5
|
+
* with SerializePlugin for automatic JSON serialization/deserialization.
|
|
6
|
+
* SQLite-compatible — use with @syncular/server-dialect-sqlite.
|
|
7
|
+
*
|
|
8
|
+
* Implements a custom Kysely Driver that wraps sqlite3's callback API
|
|
9
|
+
* into the promise-based interface Kysely expects.
|
|
10
|
+
*/
|
|
11
|
+
import { SerializePlugin } from '@syncular/core';
|
|
12
|
+
import { CompiledQuery, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from 'kysely';
|
|
13
|
+
import sqlite3 from 'sqlite3';
|
|
14
|
+
/**
|
|
15
|
+
* Create a Kysely instance with node-sqlite3 dialect and SerializePlugin.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const db = createSqlite3Db<MyDb>({ path: './data.db' });
|
|
19
|
+
* const db = createSqlite3Db<MyDb>({ path: ':memory:' });
|
|
20
|
+
*/
|
|
21
|
+
export function createSqlite3Db(options) {
|
|
22
|
+
return new Kysely({
|
|
23
|
+
dialect: createSqlite3Dialect(options),
|
|
24
|
+
plugins: [new SerializePlugin()],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create the sqlite3 dialect directly (without SerializePlugin).
|
|
29
|
+
*/
|
|
30
|
+
export function createSqlite3Dialect(options) {
|
|
31
|
+
return new Sqlite3Dialect(options);
|
|
32
|
+
}
|
|
33
|
+
export function createSerializePlugin() {
|
|
34
|
+
return new SerializePlugin();
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Kysely Dialect implementation for node-sqlite3
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
class Sqlite3Dialect {
|
|
40
|
+
#options;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.#options = options;
|
|
43
|
+
}
|
|
44
|
+
createAdapter() {
|
|
45
|
+
return new SqliteAdapter();
|
|
46
|
+
}
|
|
47
|
+
createDriver() {
|
|
48
|
+
return new Sqlite3Driver(this.#options);
|
|
49
|
+
}
|
|
50
|
+
createQueryCompiler() {
|
|
51
|
+
return new SqliteQueryCompiler();
|
|
52
|
+
}
|
|
53
|
+
createIntrospector(db) {
|
|
54
|
+
return new SqliteIntrospector(db);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
class Sqlite3Driver {
|
|
58
|
+
#options;
|
|
59
|
+
#db;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.#options = options;
|
|
62
|
+
}
|
|
63
|
+
async init() {
|
|
64
|
+
this.#db = await this.#resolveDatabase();
|
|
65
|
+
}
|
|
66
|
+
async acquireConnection() {
|
|
67
|
+
return new Sqlite3Connection(this.#db);
|
|
68
|
+
}
|
|
69
|
+
async beginTransaction(connection, _settings) {
|
|
70
|
+
await connection.executeQuery(CompiledQuery.raw('begin'));
|
|
71
|
+
}
|
|
72
|
+
async commitTransaction(connection) {
|
|
73
|
+
await connection.executeQuery(CompiledQuery.raw('commit'));
|
|
74
|
+
}
|
|
75
|
+
async rollbackTransaction(connection) {
|
|
76
|
+
await connection.executeQuery(CompiledQuery.raw('rollback'));
|
|
77
|
+
}
|
|
78
|
+
async releaseConnection(_connection) {
|
|
79
|
+
// Single-connection model — nothing to release.
|
|
80
|
+
}
|
|
81
|
+
async destroy() {
|
|
82
|
+
if (this.#db) {
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
this.#db.close((err) => (err ? reject(err) : resolve()));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async #resolveDatabase() {
|
|
89
|
+
if ('database' in this.#options) {
|
|
90
|
+
return this.#options.database;
|
|
91
|
+
}
|
|
92
|
+
const path = this.#options.path;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const db = new sqlite3.Database(path, (err) => {
|
|
95
|
+
if (err)
|
|
96
|
+
reject(err);
|
|
97
|
+
else
|
|
98
|
+
resolve(db);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
class Sqlite3Connection {
|
|
104
|
+
#db;
|
|
105
|
+
constructor(db) {
|
|
106
|
+
this.#db = db;
|
|
107
|
+
}
|
|
108
|
+
async executeQuery(compiledQuery) {
|
|
109
|
+
const { sql, parameters } = compiledQuery;
|
|
110
|
+
const params = [...parameters];
|
|
111
|
+
const hasReturning = /\breturning\b/i.test(sql);
|
|
112
|
+
const isSelectLike = /^\s*(select|pragma|explain|with)\b/i.test(sql);
|
|
113
|
+
const isInsert = /^\s*(insert|replace)\b/i.test(sql);
|
|
114
|
+
if (isSelectLike || hasReturning) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
this.#db.all(sql, params, (err, rows) => {
|
|
117
|
+
if (err)
|
|
118
|
+
return reject(err);
|
|
119
|
+
const normalizedRows = rows ?? [];
|
|
120
|
+
resolve({
|
|
121
|
+
rows: normalizedRows,
|
|
122
|
+
...(hasReturning
|
|
123
|
+
? { numAffectedRows: BigInt(normalizedRows.length) }
|
|
124
|
+
: {}),
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// For INSERT, UPDATE, DELETE — use run() to get lastID and changes
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
this.#db.run(sql, params, function (err) {
|
|
132
|
+
if (err)
|
|
133
|
+
return reject(err);
|
|
134
|
+
resolve({
|
|
135
|
+
rows: [],
|
|
136
|
+
numAffectedRows: BigInt(this.changes),
|
|
137
|
+
...(isInsert ? { insertId: BigInt(this.lastID) } : {}),
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
streamQuery(_compiledQuery, _chunkSize) {
|
|
143
|
+
throw new Error('sqlite3 driver does not support streaming');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncular/dialect-sqlite3",
|
|
3
|
+
"version": "0.0.1-100",
|
|
4
|
+
"description": "node-sqlite3 dialect for the Syncular client",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benjamin Kniffler",
|
|
7
|
+
"homepage": "https://syncular.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/syncular/syncular.git",
|
|
11
|
+
"directory": "packages/dialect-sqlite3"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/syncular/syncular/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sync",
|
|
18
|
+
"offline-first",
|
|
19
|
+
"realtime",
|
|
20
|
+
"database",
|
|
21
|
+
"typescript",
|
|
22
|
+
"sqlite3",
|
|
23
|
+
"node"
|
|
24
|
+
],
|
|
25
|
+
"private": false,
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"tsgo": "tsgo --noEmit",
|
|
41
|
+
"build": "tsgo",
|
|
42
|
+
"release": "bunx syncular-publish"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@syncular/core": "0.0.1",
|
|
46
|
+
"sqlite3": "^5.1.7"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"kysely": "^0.28.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@syncular/config": "0.0.0",
|
|
53
|
+
"kysely": "*"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist",
|
|
57
|
+
"src"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { Kysely, type Kysely as KyselyType } from 'kysely';
|
|
3
|
+
import { createSqlite3Db, createSqlite3Dialect } from './index';
|
|
4
|
+
|
|
5
|
+
interface TestDb {
|
|
6
|
+
tasks: {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('sqlite3 dialect RETURNING behavior', () => {
|
|
13
|
+
let db: KyselyType<TestDb>;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
db = createSqlite3Db<TestDb>({ path: ':memory:' });
|
|
17
|
+
await db.schema
|
|
18
|
+
.createTable('tasks')
|
|
19
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
20
|
+
.addColumn('title', 'text', (col) => col.notNull())
|
|
21
|
+
.execute();
|
|
22
|
+
await db
|
|
23
|
+
.insertInto('tasks')
|
|
24
|
+
.values({ id: 'task-1', title: 'before' })
|
|
25
|
+
.execute();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await db.destroy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns rows for non-select UPDATE ... RETURNING queries', async () => {
|
|
33
|
+
const updated = await db
|
|
34
|
+
.updateTable('tasks')
|
|
35
|
+
.set({ title: 'after' })
|
|
36
|
+
.where('id', '=', 'task-1')
|
|
37
|
+
.returning(['id', 'title'])
|
|
38
|
+
.executeTakeFirstOrThrow();
|
|
39
|
+
|
|
40
|
+
expect(updated).toEqual({ id: 'task-1', title: 'after' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('supports direct dialect construction via createSqlite3Dialect', async () => {
|
|
44
|
+
const directDb = new Kysely<TestDb>({
|
|
45
|
+
dialect: createSqlite3Dialect({ path: ':memory:' }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await directDb.schema
|
|
50
|
+
.createTable('tasks')
|
|
51
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
52
|
+
.addColumn('title', 'text', (col) => col.notNull())
|
|
53
|
+
.execute();
|
|
54
|
+
|
|
55
|
+
const inserted = await directDb
|
|
56
|
+
.insertInto('tasks')
|
|
57
|
+
.values({ id: 'task-2', title: 'before' })
|
|
58
|
+
.returning(['id', 'title'])
|
|
59
|
+
.executeTakeFirstOrThrow();
|
|
60
|
+
|
|
61
|
+
expect(inserted).toEqual({ id: 'task-2', title: 'before' });
|
|
62
|
+
} finally {
|
|
63
|
+
await directDb.destroy();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/dialect-sqlite3 - node-sqlite3 dialect for sync
|
|
3
|
+
*
|
|
4
|
+
* Provides a Kysely dialect for the callback-based `sqlite3` npm package
|
|
5
|
+
* with SerializePlugin for automatic JSON serialization/deserialization.
|
|
6
|
+
* SQLite-compatible — use with @syncular/server-dialect-sqlite.
|
|
7
|
+
*
|
|
8
|
+
* Implements a custom Kysely Driver that wraps sqlite3's callback API
|
|
9
|
+
* into the promise-based interface Kysely expects.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SerializePlugin } from '@syncular/core';
|
|
13
|
+
import type {
|
|
14
|
+
DatabaseConnection,
|
|
15
|
+
DatabaseIntrospector,
|
|
16
|
+
Dialect,
|
|
17
|
+
DialectAdapter,
|
|
18
|
+
Driver,
|
|
19
|
+
QueryCompiler,
|
|
20
|
+
QueryResult,
|
|
21
|
+
TransactionSettings,
|
|
22
|
+
} from 'kysely';
|
|
23
|
+
import {
|
|
24
|
+
CompiledQuery,
|
|
25
|
+
Kysely,
|
|
26
|
+
SqliteAdapter,
|
|
27
|
+
SqliteIntrospector,
|
|
28
|
+
SqliteQueryCompiler,
|
|
29
|
+
} from 'kysely';
|
|
30
|
+
import sqlite3 from 'sqlite3';
|
|
31
|
+
|
|
32
|
+
export interface Sqlite3PathOptions {
|
|
33
|
+
/** Path to SQLite database file, or ':memory:' for in-memory */
|
|
34
|
+
path: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Sqlite3InstanceOptions {
|
|
38
|
+
/** An existing sqlite3.Database instance */
|
|
39
|
+
database: sqlite3.Database;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Sqlite3Options = Sqlite3PathOptions | Sqlite3InstanceOptions;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a Kysely instance with node-sqlite3 dialect and SerializePlugin.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const db = createSqlite3Db<MyDb>({ path: './data.db' });
|
|
49
|
+
* const db = createSqlite3Db<MyDb>({ path: ':memory:' });
|
|
50
|
+
*/
|
|
51
|
+
export function createSqlite3Db<T>(options: Sqlite3Options): Kysely<T> {
|
|
52
|
+
return new Kysely<T>({
|
|
53
|
+
dialect: createSqlite3Dialect(options),
|
|
54
|
+
plugins: [new SerializePlugin()],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create the sqlite3 dialect directly (without SerializePlugin).
|
|
60
|
+
*/
|
|
61
|
+
export function createSqlite3Dialect(options: Sqlite3Options): Sqlite3Dialect {
|
|
62
|
+
return new Sqlite3Dialect(options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createSerializePlugin(): SerializePlugin {
|
|
66
|
+
return new SerializePlugin();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Kysely Dialect implementation for node-sqlite3
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
class Sqlite3Dialect implements Dialect {
|
|
74
|
+
readonly #options: Sqlite3Options;
|
|
75
|
+
|
|
76
|
+
constructor(options: Sqlite3Options) {
|
|
77
|
+
this.#options = options;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
createAdapter(): DialectAdapter {
|
|
81
|
+
return new SqliteAdapter();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
createDriver(): Driver {
|
|
85
|
+
return new Sqlite3Driver(this.#options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
createQueryCompiler(): QueryCompiler {
|
|
89
|
+
return new SqliteQueryCompiler();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
|
93
|
+
return new SqliteIntrospector(db);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class Sqlite3Driver implements Driver {
|
|
98
|
+
readonly #options: Sqlite3Options;
|
|
99
|
+
#db: sqlite3.Database | undefined;
|
|
100
|
+
|
|
101
|
+
constructor(options: Sqlite3Options) {
|
|
102
|
+
this.#options = options;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async init(): Promise<void> {
|
|
106
|
+
this.#db = await this.#resolveDatabase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async acquireConnection(): Promise<DatabaseConnection> {
|
|
110
|
+
return new Sqlite3Connection(this.#db!);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async beginTransaction(
|
|
114
|
+
connection: DatabaseConnection,
|
|
115
|
+
_settings: TransactionSettings
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
await connection.executeQuery(CompiledQuery.raw('begin'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
|
121
|
+
await connection.executeQuery(CompiledQuery.raw('commit'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
|
125
|
+
await connection.executeQuery(CompiledQuery.raw('rollback'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async releaseConnection(_connection: DatabaseConnection): Promise<void> {
|
|
129
|
+
// Single-connection model — nothing to release.
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async destroy(): Promise<void> {
|
|
133
|
+
if (this.#db) {
|
|
134
|
+
await new Promise<void>((resolve, reject) => {
|
|
135
|
+
this.#db!.close((err) => (err ? reject(err) : resolve()));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #resolveDatabase(): Promise<sqlite3.Database> {
|
|
141
|
+
if ('database' in this.#options) {
|
|
142
|
+
return this.#options.database;
|
|
143
|
+
}
|
|
144
|
+
const path = this.#options.path;
|
|
145
|
+
return new Promise<sqlite3.Database>((resolve, reject) => {
|
|
146
|
+
const db = new sqlite3.Database(path, (err) => {
|
|
147
|
+
if (err) reject(err);
|
|
148
|
+
else resolve(db);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class Sqlite3Connection implements DatabaseConnection {
|
|
155
|
+
readonly #db: sqlite3.Database;
|
|
156
|
+
|
|
157
|
+
constructor(db: sqlite3.Database) {
|
|
158
|
+
this.#db = db;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
|
|
162
|
+
const { sql, parameters } = compiledQuery;
|
|
163
|
+
const params = [...parameters];
|
|
164
|
+
|
|
165
|
+
const hasReturning = /\breturning\b/i.test(sql);
|
|
166
|
+
const isSelectLike = /^\s*(select|pragma|explain|with)\b/i.test(sql);
|
|
167
|
+
const isInsert = /^\s*(insert|replace)\b/i.test(sql);
|
|
168
|
+
|
|
169
|
+
if (isSelectLike || hasReturning) {
|
|
170
|
+
return new Promise<QueryResult<R>>((resolve, reject) => {
|
|
171
|
+
this.#db.all(sql, params, (err: Error | null, rows: R[]) => {
|
|
172
|
+
if (err) return reject(err);
|
|
173
|
+
const normalizedRows = rows ?? [];
|
|
174
|
+
resolve({
|
|
175
|
+
rows: normalizedRows,
|
|
176
|
+
...(hasReturning
|
|
177
|
+
? { numAffectedRows: BigInt(normalizedRows.length) }
|
|
178
|
+
: {}),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// For INSERT, UPDATE, DELETE — use run() to get lastID and changes
|
|
185
|
+
return new Promise<QueryResult<R>>((resolve, reject) => {
|
|
186
|
+
this.#db.run(sql, params, function (err: Error | null) {
|
|
187
|
+
if (err) return reject(err);
|
|
188
|
+
resolve({
|
|
189
|
+
rows: [],
|
|
190
|
+
numAffectedRows: BigInt(this.changes),
|
|
191
|
+
...(isInsert ? { insertId: BigInt(this.lastID) } : {}),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
streamQuery<R>(
|
|
198
|
+
_compiledQuery: CompiledQuery,
|
|
199
|
+
_chunkSize?: number
|
|
200
|
+
): AsyncIterableIterator<QueryResult<R>> {
|
|
201
|
+
throw new Error('sqlite3 driver does not support streaming');
|
|
202
|
+
}
|
|
203
|
+
}
|