@holo-js/db-sqlite 0.5.5 → 0.6.0
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/.holo-build-fingerprint +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.mjs +107 -30
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d2303f7566e65ea6e4b4bf463954b7c43ff37a93d0f3feebe4be02a3b57ec093
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DriverAdapter, DriverQueryResult, DriverExecutionResult, DatabaseOperationOptions, DatabaseDriverFactory } from '@holo-js/db';
|
|
1
|
+
import { DriverAdapter, holoRuntimeInternals, DriverQueryResult, DriverExecutionResult, DatabaseOperationOptions, DatabaseDriverFactory } from '@holo-js/db';
|
|
2
2
|
export { DriverAdapter, DriverExecutionResult, DriverQueryResult } from '@holo-js/db';
|
|
3
3
|
|
|
4
4
|
type SQLiteTransactionMode = 'deferred' | 'immediate' | 'exclusive';
|
|
@@ -14,11 +14,17 @@ interface SQLiteStatementLike {
|
|
|
14
14
|
changes?: number;
|
|
15
15
|
lastInsertRowid?: unknown;
|
|
16
16
|
};
|
|
17
|
+
safeIntegers?(enabled: boolean): unknown;
|
|
17
18
|
}
|
|
18
19
|
interface SQLiteDatabaseLike {
|
|
19
20
|
prepare(sql: string): SQLiteStatementLike;
|
|
20
21
|
exec(sql: string): unknown;
|
|
21
22
|
close(): unknown;
|
|
23
|
+
function?(name: string, options: {
|
|
24
|
+
deterministic: boolean;
|
|
25
|
+
safeIntegers?: boolean;
|
|
26
|
+
varargs?: boolean;
|
|
27
|
+
}, implementation: (...args: unknown[]) => unknown): unknown;
|
|
22
28
|
}
|
|
23
29
|
interface SQLiteAdapterOptions {
|
|
24
30
|
filename?: string;
|
|
@@ -27,15 +33,21 @@ interface SQLiteAdapterOptions {
|
|
|
27
33
|
}
|
|
28
34
|
declare class SQLiteAdapter implements DriverAdapter {
|
|
29
35
|
private database?;
|
|
36
|
+
private hasRegisteredFunctions;
|
|
37
|
+
private canCallFunctions;
|
|
30
38
|
private connected;
|
|
31
39
|
private readonly transactionKey;
|
|
32
|
-
private readonly transactionScope;
|
|
33
40
|
private readonly filename;
|
|
34
41
|
private readonly createDatabaseInstance;
|
|
35
42
|
constructor(options?: SQLiteAdapterOptions);
|
|
36
43
|
initialize(): Promise<void>;
|
|
44
|
+
databaseExists(): Promise<boolean>;
|
|
45
|
+
ensureDatabaseExists(): Promise<void>;
|
|
46
|
+
isDatabaseMissingError(error: unknown): boolean;
|
|
37
47
|
disconnect(): Promise<void>;
|
|
38
48
|
isConnected(): boolean;
|
|
49
|
+
get [holoRuntimeInternals.driverConnectionKey](): SQLiteAdapter;
|
|
50
|
+
get [holoRuntimeInternals.transactionScopeKey](): string | SQLiteAdapter;
|
|
39
51
|
runWithTransactionScope<T>(callback: () => Promise<T>): Promise<T>;
|
|
40
52
|
beforeMigrationTransaction(): Promise<SQLiteMigrationTransactionState>;
|
|
41
53
|
validateMigrationTransaction(): Promise<void>;
|
|
@@ -49,7 +61,9 @@ declare class SQLiteAdapter implements DriverAdapter {
|
|
|
49
61
|
createSavepoint(name: string): Promise<void>;
|
|
50
62
|
rollbackToSavepoint(name: string): Promise<void>;
|
|
51
63
|
releaseSavepoint(name: string): Promise<void>;
|
|
64
|
+
private prepareExact;
|
|
52
65
|
private getDatabase;
|
|
66
|
+
private openDatabase;
|
|
53
67
|
private normalizeSavepointName;
|
|
54
68
|
private resolveBeginStatement;
|
|
55
69
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,30 +1,67 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
4
6
|
import Database from "better-sqlite3";
|
|
5
7
|
import {
|
|
6
|
-
|
|
8
|
+
CapabilityError,
|
|
9
|
+
DatabaseError,
|
|
10
|
+
holoRuntimeInternals
|
|
7
11
|
} from "@holo-js/db";
|
|
8
12
|
var TransactionError = class extends Error {
|
|
9
13
|
};
|
|
14
|
+
var MISSING_DATABASE_ERROR_CODE = "SQLITE_DATABASE_MISSING";
|
|
15
|
+
function isInMemoryDatabase(filename) {
|
|
16
|
+
return filename === ":memory:" || filename === "";
|
|
17
|
+
}
|
|
18
|
+
function resolveSQLiteFilename(filename) {
|
|
19
|
+
if (!filename.startsWith("file:")) return filename;
|
|
20
|
+
const location = filename.slice("file:".length).replace(/#.*$/s, "");
|
|
21
|
+
const queryStart = location.indexOf("?");
|
|
22
|
+
if (queryStart !== -1) {
|
|
23
|
+
throw new Error(`SQLite URL "${filename}" has query parameters, which Holo doesn't support. Remove "${location.slice(queryStart)}" from the URL.`);
|
|
24
|
+
}
|
|
25
|
+
if (!location.startsWith("//")) return decodeURIComponent(location);
|
|
26
|
+
const pathStart = location.indexOf("/", 2);
|
|
27
|
+
const authority = pathStart === -1 ? location.slice(2) : location.slice(2, pathStart);
|
|
28
|
+
if (authority !== "" && authority !== "localhost") {
|
|
29
|
+
throw new Error(`SQLite URL "${filename}" names the host "${authority}". Use "file:" with a local path.`);
|
|
30
|
+
}
|
|
31
|
+
return decodeURIComponent(pathStart === -1 ? "" : location.slice(pathStart));
|
|
32
|
+
}
|
|
10
33
|
var transactionTails = /* @__PURE__ */ new Map();
|
|
34
|
+
var heldTransactionScopes = new AsyncLocalStorage();
|
|
11
35
|
function normalizeBinding(value) {
|
|
12
|
-
|
|
36
|
+
if (typeof value === "boolean") {
|
|
37
|
+
return BigInt(value);
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
40
|
+
return BigInt(value);
|
|
41
|
+
}
|
|
42
|
+
return value instanceof Date ? holoRuntimeInternals.formatSQLiteDateTime(value) : value;
|
|
43
|
+
}
|
|
44
|
+
function exactInteger(value) {
|
|
45
|
+
return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
46
|
+
}
|
|
47
|
+
function exactIntegerRow(row) {
|
|
48
|
+
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? exactInteger(value) : value]));
|
|
13
49
|
}
|
|
14
50
|
var SQLiteAdapter = class {
|
|
15
51
|
database;
|
|
52
|
+
hasRegisteredFunctions = false;
|
|
53
|
+
canCallFunctions = false;
|
|
16
54
|
connected;
|
|
17
55
|
transactionKey;
|
|
18
|
-
transactionScope = new AsyncLocalStorage();
|
|
19
56
|
filename;
|
|
20
57
|
createDatabaseInstance;
|
|
21
58
|
constructor(options = {}) {
|
|
22
59
|
this.database = options.database;
|
|
23
60
|
this.connected = !!options.database;
|
|
24
|
-
this.filename = options.filename ?? ":memory:";
|
|
25
|
-
this.transactionKey = options.database || this.filename
|
|
61
|
+
this.filename = resolveSQLiteFilename(options.filename ?? ":memory:");
|
|
62
|
+
this.transactionKey = options.database || isInMemoryDatabase(this.filename) ? this : resolve(this.filename);
|
|
26
63
|
this.createDatabaseInstance = options.createDatabase ?? ((filename) => {
|
|
27
|
-
const database = new Database(filename);
|
|
64
|
+
const database = new Database(filename, { fileMustExist: !isInMemoryDatabase(filename) });
|
|
28
65
|
try {
|
|
29
66
|
database.exec("PRAGMA journal_mode = WAL");
|
|
30
67
|
return database;
|
|
@@ -35,30 +72,35 @@ var SQLiteAdapter = class {
|
|
|
35
72
|
});
|
|
36
73
|
}
|
|
37
74
|
async initialize() {
|
|
38
|
-
if (this.connected) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
this.database = this.createDatabaseInstance(this.filename);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
const message = error instanceof Error && error.message ? error.message : "Unknown SQLite driver error.";
|
|
45
|
-
throw new Error(`Unable to open SQLite database "${this.filename}": ${message}`, { cause: error });
|
|
46
|
-
}
|
|
47
75
|
this.connected = true;
|
|
48
76
|
}
|
|
77
|
+
async databaseExists() {
|
|
78
|
+
return !!this.database || isInMemoryDatabase(this.filename) || existsSync(this.filename);
|
|
79
|
+
}
|
|
80
|
+
async ensureDatabaseExists() {
|
|
81
|
+
await mkdir(dirname(resolve(this.filename)), { recursive: true });
|
|
82
|
+
await appendFile(this.filename, "");
|
|
83
|
+
}
|
|
84
|
+
isDatabaseMissingError(error) {
|
|
85
|
+
return error instanceof DatabaseError && error.code === MISSING_DATABASE_ERROR_CODE;
|
|
86
|
+
}
|
|
49
87
|
async disconnect() {
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
this.database.close();
|
|
88
|
+
this.database?.close();
|
|
54
89
|
this.database = void 0;
|
|
55
90
|
this.connected = false;
|
|
56
91
|
}
|
|
57
92
|
isConnected() {
|
|
58
93
|
return this.connected;
|
|
59
94
|
}
|
|
95
|
+
get [holoRuntimeInternals.driverConnectionKey]() {
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
get [holoRuntimeInternals.transactionScopeKey]() {
|
|
99
|
+
return this.transactionKey;
|
|
100
|
+
}
|
|
60
101
|
async runWithTransactionScope(callback) {
|
|
61
|
-
|
|
102
|
+
const held = heldTransactionScopes.getStore();
|
|
103
|
+
if (held?.get(this.transactionKey)?.active) {
|
|
62
104
|
return callback();
|
|
63
105
|
}
|
|
64
106
|
const previous = transactionTails.get(this.transactionKey) ?? Promise.resolve();
|
|
@@ -68,9 +110,11 @@ var SQLiteAdapter = class {
|
|
|
68
110
|
}));
|
|
69
111
|
transactionTails.set(this.transactionKey, current);
|
|
70
112
|
await previous;
|
|
113
|
+
const scope = { active: true };
|
|
71
114
|
try {
|
|
72
|
-
return await
|
|
115
|
+
return await heldTransactionScopes.run(new Map(held).set(this.transactionKey, scope), callback);
|
|
73
116
|
} finally {
|
|
117
|
+
scope.active = false;
|
|
74
118
|
release();
|
|
75
119
|
if (transactionTails.get(this.transactionKey) === current) transactionTails.delete(this.transactionKey);
|
|
76
120
|
}
|
|
@@ -98,8 +142,7 @@ var SQLiteAdapter = class {
|
|
|
98
142
|
}
|
|
99
143
|
}
|
|
100
144
|
async query(sql, bindings = []) {
|
|
101
|
-
const
|
|
102
|
-
const rows = statement.all(...bindings.map(normalizeBinding));
|
|
145
|
+
const rows = this.prepareExact(sql).all(...bindings.map(normalizeBinding)).map((row) => exactIntegerRow(row));
|
|
103
146
|
return {
|
|
104
147
|
rows,
|
|
105
148
|
rowCount: rows.length
|
|
@@ -110,11 +153,10 @@ var SQLiteAdapter = class {
|
|
|
110
153
|
}
|
|
111
154
|
async execute(sql, bindings = []) {
|
|
112
155
|
return this.runWithTransactionScope(async () => {
|
|
113
|
-
const
|
|
114
|
-
const result = statement.run(...bindings.map(normalizeBinding));
|
|
156
|
+
const result = this.prepareExact(sql).run(...bindings.map(normalizeBinding));
|
|
115
157
|
return {
|
|
116
158
|
affectedRows: result.changes,
|
|
117
|
-
lastInsertId: typeof result.lastInsertRowid === "bigint" ?
|
|
159
|
+
lastInsertId: typeof result.lastInsertRowid === "bigint" ? exactInteger(result.lastInsertRowid) : result.lastInsertRowid
|
|
118
160
|
};
|
|
119
161
|
});
|
|
120
162
|
}
|
|
@@ -136,13 +178,48 @@ var SQLiteAdapter = class {
|
|
|
136
178
|
async releaseSavepoint(name) {
|
|
137
179
|
this.getDatabase().exec(`RELEASE SAVEPOINT ${this.normalizeSavepointName(name)}`);
|
|
138
180
|
}
|
|
181
|
+
prepareExact(sql) {
|
|
182
|
+
const database = this.getDatabase();
|
|
183
|
+
const wallClockFunction = holoRuntimeInternals.SQLITE_WALL_CLOCK_FUNCTION;
|
|
184
|
+
if (!this.canCallFunctions && sql.includes(`${wallClockFunction}(`)) {
|
|
185
|
+
throw new CapabilityError(`This query compares dates in app.timezone through the ${wallClockFunction}() SQL function, which the injected SQLite database can't register because it has no function() method. Pass a better-sqlite3 Database, or set app.timezone to UTC.`);
|
|
186
|
+
}
|
|
187
|
+
const decimalTextFunction = holoRuntimeInternals.SQLITE_DECIMAL_TEXT_FUNCTION;
|
|
188
|
+
if (!this.canCallFunctions && sql.includes(`${decimalTextFunction}(`)) {
|
|
189
|
+
throw new CapabilityError(`This query compares decimal keys with text keys through the ${decimalTextFunction}() SQL function, which the injected SQLite database can't register because it has no function() method. Pass a better-sqlite3 Database.`);
|
|
190
|
+
}
|
|
191
|
+
const statement = database.prepare(sql);
|
|
192
|
+
statement.safeIntegers?.(true);
|
|
193
|
+
return statement;
|
|
194
|
+
}
|
|
139
195
|
getDatabase() {
|
|
140
|
-
if (!this.
|
|
141
|
-
this.database = this.
|
|
142
|
-
this.
|
|
196
|
+
if (!this.database) {
|
|
197
|
+
this.database = this.openDatabase();
|
|
198
|
+
this.hasRegisteredFunctions = false;
|
|
199
|
+
}
|
|
200
|
+
if (!this.hasRegisteredFunctions) {
|
|
201
|
+
this.canCallFunctions = typeof this.database.function === "function";
|
|
202
|
+
this.database.function?.(holoRuntimeInternals.SQLITE_WALL_CLOCK_FUNCTION, { deterministic: true }, holoRuntimeInternals.formatSQLiteWallClock);
|
|
203
|
+
this.database.function?.(holoRuntimeInternals.SQLITE_DECIMAL_TEXT_FUNCTION, { deterministic: true, safeIntegers: true, varargs: true }, (value, places) => holoRuntimeInternals.readSQLiteDecimal(value, places == null ? void 0 : Number(places)));
|
|
204
|
+
this.hasRegisteredFunctions = true;
|
|
143
205
|
}
|
|
206
|
+
this.connected = true;
|
|
144
207
|
return this.database;
|
|
145
208
|
}
|
|
209
|
+
openDatabase() {
|
|
210
|
+
if (!isInMemoryDatabase(this.filename) && !existsSync(this.filename)) {
|
|
211
|
+
throw new DatabaseError(
|
|
212
|
+
`Database file at path "${this.filename}" does not exist. Run "holo migrate" to create it.`,
|
|
213
|
+
MISSING_DATABASE_ERROR_CODE
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return this.createDatabaseInstance(this.filename);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = error instanceof Error && error.message ? error.message : "Unknown SQLite driver error.";
|
|
220
|
+
throw new Error(`Unable to open SQLite database "${this.filename}": ${message}`, { cause: error });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
146
223
|
normalizeSavepointName(name) {
|
|
147
224
|
if (!/^[A-Z_]\w*$/i.test(name)) {
|
|
148
225
|
throw new TransactionError(`Invalid savepoint name "${name}".`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/db-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Holo-JS Framework - SQLite database adapter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"node": "^22.18.0 || ^24.0.0 || ^26.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@holo-js/db": "^0.
|
|
35
|
+
"@holo-js/db": "^0.6.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"better-sqlite3": "^13.0.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@holo-js/db": "^0.
|
|
41
|
+
"@holo-js/db": "^0.6.0",
|
|
42
42
|
"@types/better-sqlite3": "^9.6.0",
|
|
43
43
|
"tsup": "^9.0.0",
|
|
44
44
|
"typescript": "^6.0.3",
|