@travetto/model-sqlite 8.0.0-alpha.3 → 8.0.0-alpha.5

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.
Files changed (2) hide show
  1. package/package.json +6 -8
  2. package/src/connection.ts +35 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-sqlite",
3
- "version": "8.0.0-alpha.3",
3
+ "version": "8.0.0-alpha.5",
4
4
  "type": "module",
5
5
  "description": "SQLite backing for the travetto model module, with real-time modeling support for SQL schemas.",
6
6
  "keywords": [
@@ -27,13 +27,11 @@
27
27
  "directory": "module/model-sqlite"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/config": "^8.0.0-alpha.3",
31
- "@travetto/context": "^8.0.0-alpha.3",
32
- "@travetto/model": "^8.0.0-alpha.3",
33
- "@travetto/model-query": "^8.0.0-alpha.3",
34
- "@travetto/model-sql": "^8.0.0-alpha.3",
35
- "@types/better-sqlite3": "^7.6.13",
36
- "better-sqlite3": "^12.6.2"
30
+ "@travetto/config": "^8.0.0-alpha.5",
31
+ "@travetto/context": "^8.0.0-alpha.5",
32
+ "@travetto/model": "^8.0.0-alpha.5",
33
+ "@travetto/model-query": "^8.0.0-alpha.5",
34
+ "@travetto/model-sql": "^8.0.0-alpha.5"
37
35
  },
38
36
  "travetto": {
39
37
  "displayName": "SQLite Model Service",
package/src/connection.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { DatabaseSync, type DatabaseSyncOptions, type SQLInputValue, } from 'node:sqlite';
3
4
 
4
- import sqlDb, { type Database, type Options } from 'better-sqlite3';
5
5
  import { type Pool, createPool } from 'generic-pool';
6
6
 
7
7
  import { ShutdownManager, Util, Runtime, RuntimeError, castTo } from '@travetto/runtime';
@@ -17,16 +17,16 @@ const isRecoverableError = (error: unknown): error is Error =>
17
17
  /**
18
18
  * Connection support for Sqlite
19
19
  */
20
- export class SqliteConnection extends Connection<Database> {
20
+ export class SqliteConnection extends Connection<DatabaseSync> {
21
21
 
22
22
  isolatedTransactions = false;
23
23
 
24
- #config: SQLModelConfig<Options & { file?: string }>;
25
- #pool: Pool<Database>;
24
+ #config: SQLModelConfig<DatabaseSyncOptions & { file?: string }>;
25
+ #pool: Pool<DatabaseSync>;
26
26
 
27
27
  constructor(
28
28
  context: AsyncContext,
29
- config: SQLModelConfig<Options & { file?: string }>
29
+ config: SQLModelConfig<DatabaseSyncOptions & { file?: string }>
30
30
  ) {
31
31
  super(context);
32
32
  this.#config = config;
@@ -38,7 +38,6 @@ export class SqliteConnection extends Connection<Database> {
38
38
  return await operation();
39
39
  } catch (error) {
40
40
  if (isRecoverableError(error)) {
41
- console.error('Failed, and waiting', retries);
42
41
  await Util.blockingTimeout(delay);
43
42
  } else {
44
43
  throw error;
@@ -48,13 +47,17 @@ export class SqliteConnection extends Connection<Database> {
48
47
  throw new RuntimeError('Max retries exceeded');
49
48
  }
50
49
 
51
- async #create(): Promise<Database> {
50
+ async #create(): Promise<DatabaseSync> {
52
51
  const file = path.resolve(this.#config.options.file ?? Runtime.toolPath('@', 'sqlite_db'));
53
52
  await fs.mkdir(path.dirname(file), { recursive: true });
54
- const db = new sqlDb(file, this.#config.options);
55
- await db.pragma('foreign_keys = ON');
56
- await db.pragma('journal_mode = WAL');
57
- await db.pragma('synchronous = NORMAL');
53
+ const db = new DatabaseSync(file, this.#config.options);
54
+ for (const q of [
55
+ 'PRAGMA foreign_keys = ON',
56
+ 'PRAGMA journal_mode = WAL',
57
+ 'PRAGMA synchronous = NORMAL',
58
+ ]) {
59
+ await this.#withRetries(async () => db.exec(q));
60
+ }
58
61
  db.function('regexp', (a, b) => new RegExp(`${a}`).test(`${b}`) ? 1 : 0);
59
62
  return db;
60
63
  }
@@ -68,7 +71,7 @@ export class SqliteConnection extends Connection<Database> {
68
71
 
69
72
  await this.#create();
70
73
 
71
- this.#pool = createPool<Database>({
74
+ this.#pool = createPool<DatabaseSync>({
72
75
  create: () => this.#withRetries(() => this.#create()),
73
76
  destroy: async db => { db.close(); }
74
77
  }, { max: 1 });
@@ -77,29 +80,37 @@ export class SqliteConnection extends Connection<Database> {
77
80
  ShutdownManager.signal.addEventListener('abort', () => this.#pool.clear());
78
81
  }
79
82
 
80
- async execute<T = unknown>(connection: Database, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
83
+ async execute<T = unknown>(connection: DatabaseSync, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
84
+ const isSelect = query.trim().startsWith('SELECT');
81
85
  return this.#withRetries(async () => {
82
86
  console.debug('Executing query', { query });
83
87
 
84
88
  try {
85
- const prepared = connection.prepare<unknown[], T>(query).safeIntegers(true);
86
- if (query.trim().startsWith('SELECT')) {
87
- const out = prepared.all(...values ?? []);
88
- const records: T[] = out.map(item => ({ ...item }));
89
+ const prepared = connection.prepare(query);
90
+ prepared.setReadBigInts(true);
91
+ if (isSelect) {
92
+ const out = prepared.all(...castTo<SQLInputValue[]>(values ?? []));
93
+ const records: T[] = out.map(item => ({ ...castTo<T>(item) }));
89
94
  return { count: out.length, records };
90
95
  } else {
91
- const out = prepared.run(...values ?? []);
92
- return { count: out.changes, records: [] };
96
+ const out = prepared.run(...castTo<SQLInputValue[]>(values ?? []));
97
+ return { count: typeof out.changes === 'number' ? out.changes : +out.changes.toString(), records: [] };
93
98
  }
94
99
  } catch (error) {
95
100
  const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
101
+ const message = Error.isError(error) ? error.message : undefined;
96
102
  switch (code) {
103
+ case 'ERR_SQLITE_ERROR': {
104
+ if (message?.startsWith('UNIQUE')) {
105
+ throw new ExistsError('query', query);
106
+ }
107
+ break;
108
+ }
97
109
  case 'SQLITE_CONSTRAINT_PRIMARYKEY':
98
110
  case 'SQLITE_CONSTRAINT_UNIQUE':
99
111
  case 'SQLITE_CONSTRAINT_INDEX': throw new ExistsError('query', query);
100
112
  };
101
- const message = error instanceof Error ? error.message : '';
102
- if (/index.*?already exists/.test(message)) {
113
+ if (/index.*?already exists/.test(message ?? '')) {
103
114
  throw new ExistsError('index', query);
104
115
  }
105
116
  throw error;
@@ -107,18 +118,18 @@ export class SqliteConnection extends Connection<Database> {
107
118
  });
108
119
  }
109
120
 
110
- async acquire(): Promise<sqlDb.Database> {
121
+ async acquire(): Promise<DatabaseSync> {
111
122
  return await this.#pool.acquire();
112
123
  }
113
124
 
114
- async release(db: Database): Promise<void> {
125
+ async release(db: DatabaseSync): Promise<void> {
115
126
  return this.#pool.release(db);
116
127
  }
117
128
 
118
129
  async pragma<T>(query: string): Promise<T> {
119
130
  const db = await this.acquire();
120
131
  try {
121
- const result = db.pragma(query, { simple: false });
132
+ const result = this.#withRetries(async () => db.prepare(`PRAGMA ${query}`).get());
122
133
  return castTo<T>(result);
123
134
  } finally {
124
135
  await this.release(db);