@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.
@@ -1 +1 @@
1
- de3d444a601bf4e70ddaa25987415b7c9c92124e47e5e6061825d5196b939399
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 { resolve } from "path";
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
- DatabaseError
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
- return typeof value === "boolean" ? Number(value) : value;
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 === ":memory:" ? this : resolve(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
- if (!this.connected || !this.database) {
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
- if (this.transactionScope.getStore()) {
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 this.transactionScope.run(true, callback);
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 statement = this.getDatabase().prepare(sql);
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 statement = this.getDatabase().prepare(sql);
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" ? Number(result.lastInsertRowid) : result.lastInsertRowid
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.connected || !this.database) {
141
- this.database = this.createDatabaseInstance(this.filename);
142
- this.connected = true;
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.5.5",
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.5.5"
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.5.5",
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",