@travetto/model-sqlite 7.0.0-rc.2 → 7.0.0-rc.4

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 CHANGED
@@ -60,11 +60,11 @@ export class SQLModelConfig<T extends {} = {}> {
60
60
  /**
61
61
  * Username
62
62
  */
63
- user = '';
63
+ user = Runtime.production ? '' : 'travetto';
64
64
  /**
65
65
  * Password
66
66
  */
67
- password = '';
67
+ password = Runtime.production ? '' : 'travetto';
68
68
  /**
69
69
  * Table prefix
70
70
  */
@@ -74,9 +74,9 @@ export class SQLModelConfig<T extends {} = {}> {
74
74
  */
75
75
  database = 'app';
76
76
  /**
77
- * Auto schema creation
77
+ * Allow storage modification at runtime
78
78
  */
79
- autoCreate?: boolean;
79
+ modifyStorage?: boolean;
80
80
  /**
81
81
  * Db version
82
82
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-sqlite",
3
- "version": "7.0.0-rc.2",
3
+ "version": "7.0.0-rc.4",
4
4
  "description": "SQLite backing for the travetto model module, with real-time modeling support for SQL schemas.",
5
5
  "keywords": [
6
6
  "sql",
@@ -26,11 +26,11 @@
26
26
  "directory": "module/model-sqlite"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/config": "^7.0.0-rc.2",
30
- "@travetto/context": "^7.0.0-rc.2",
31
- "@travetto/model": "^7.0.0-rc.2",
32
- "@travetto/model-query": "^7.0.0-rc.2",
33
- "@travetto/model-sql": "^7.0.0-rc.2",
29
+ "@travetto/config": "^7.0.0-rc.4",
30
+ "@travetto/context": "^7.0.0-rc.4",
31
+ "@travetto/model": "^7.0.0-rc.4",
32
+ "@travetto/model-query": "^7.0.0-rc.4",
33
+ "@travetto/model-sql": "^7.0.0-rc.4",
34
34
  "@types/better-sqlite3": "^7.6.13",
35
35
  "better-sqlite3": "^12.4.6"
36
36
  },
package/src/connection.ts CHANGED
@@ -4,11 +4,16 @@ import path from 'node:path';
4
4
  import sqlDb, { type Database, Options } from 'better-sqlite3';
5
5
  import { Pool, createPool } from 'generic-pool';
6
6
 
7
- import { ShutdownManager, Util, Runtime } from '@travetto/runtime';
7
+ import { ShutdownManager, Util, Runtime, AppError, castTo } from '@travetto/runtime';
8
8
  import { AsyncContext, WithAsyncContext } from '@travetto/context';
9
9
  import { ExistsError } from '@travetto/model';
10
10
  import { SQLModelConfig, Connection } from '@travetto/model-sql';
11
11
 
12
+ const RECOVERABLE_MESSAGE = /database( table| schema)? is (locked|busy)/;
13
+
14
+ const isRecoverableError = (error: unknown): error is Error =>
15
+ error instanceof Error && RECOVERABLE_MESSAGE.test(error.message);
16
+
12
17
  /**
13
18
  * Connection support for Sqlite
14
19
  */
@@ -28,19 +33,19 @@ export class SqliteConnection extends Connection<Database> {
28
33
  }
29
34
 
30
35
  async #withRetries<T>(operation: () => Promise<T>, retries = 10, delay = 250): Promise<T> {
31
- for (; ;) {
36
+ for (; retries > 1; retries -= 1) {
32
37
  try {
33
38
  return await operation();
34
39
  } catch (error) {
35
- if (error instanceof Error && retries > 1 && error.message.includes('database is locked')) {
40
+ if (isRecoverableError(error)) {
36
41
  console.error('Failed, and waiting', retries);
37
42
  await Util.blockingTimeout(delay);
38
- retries -= 1;
39
43
  } else {
40
44
  throw error;
41
45
  }
42
46
  }
43
47
  }
48
+ throw new AppError('Max retries exceeded');
44
49
  }
45
50
 
46
51
  async #create(): Promise<Database> {
@@ -84,11 +89,17 @@ export class SqliteConnection extends Connection<Database> {
84
89
  return { count: out.changes, records: [] };
85
90
  }
86
91
  } catch (error) {
87
- if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
88
- throw new ExistsError('query', query);
89
- } else {
90
- throw error;
92
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
93
+ switch (code) {
94
+ case 'SQLITE_CONSTRAINT_PRIMARYKEY':
95
+ case 'SQLITE_CONSTRAINT_UNIQUE':
96
+ case 'SQLITE_CONSTRAINT_INDEX': throw new ExistsError('query', query);
97
+ };
98
+ const message = error instanceof Error ? error.message : '';
99
+ if (/index.*?already exists/.test(message)) {
100
+ throw new ExistsError('index', query);
91
101
  }
102
+ throw error;
92
103
  }
93
104
  });
94
105
  }
@@ -100,4 +111,14 @@ export class SqliteConnection extends Connection<Database> {
100
111
  async release(db: Database): Promise<void> {
101
112
  return this.#pool.release(db);
102
113
  }
114
+
115
+ async pragma<T>(query: string): Promise<T> {
116
+ const db = await this.acquire();
117
+ try {
118
+ const result = db.pragma(query, { simple: false });
119
+ return castTo<T>(result);
120
+ } finally {
121
+ await this.release(db);
122
+ }
123
+ }
103
124
  }
package/src/dialect.ts CHANGED
@@ -4,7 +4,7 @@ import { AsyncContext } from '@travetto/context';
4
4
  import { WhereClause } from '@travetto/model-query';
5
5
  import { castTo } from '@travetto/runtime';
6
6
 
7
- import { SQLModelConfig, SQLDialect, VisitStack } from '@travetto/model-sql';
7
+ import { SQLModelConfig, SQLDialect, VisitStack, type SQLTableDescription } from '@travetto/model-sql';
8
8
 
9
9
  import { SqliteConnection } from './connection.ts';
10
10
 
@@ -46,6 +46,56 @@ export class SqliteDialect extends SQLDialect {
46
46
  return `hex('${value}')`;
47
47
  }
48
48
 
49
+ async describeTable(table: string): Promise<SQLTableDescription | undefined> {
50
+ const IGNORE_FIELDS = [this.pathField.name, this.parentPathField.name, this.idxField.name].map(field => `'${field}'`);
51
+
52
+ const [columns, foreignKeys, indices] = await Promise.all([
53
+ this.executeSQL<{ name: string, type: string, is_notnull: 1 | 0 }>(`
54
+ SELECT
55
+ name,
56
+ type,
57
+ ${this.identifier('notnull')} <> 0 AS is_notnull
58
+ FROM PRAGMA_TABLE_INFO('${table}')
59
+ WHERE name NOT IN (${IGNORE_FIELDS.join(',')})
60
+ `),
61
+ this.executeSQL<{ name: string, to_table: string, from_column: string, to_column: string }>(`
62
+ SELECT
63
+ 'fk_' || '${table}' || '_' || ${this.identifier('from')} AS name,
64
+ ${this.identifier('from')} as from_column,
65
+ ${this.identifier('to')} as to_column,
66
+ ${this.identifier('table')} as to_table
67
+ FROM PRAGMA_FOREIGN_KEY_LIST('${table}')
68
+ `),
69
+ this.executeSQL<{ name: string, is_unique: boolean, columns: string }>(`
70
+ SELECT
71
+ il.name as name,
72
+ il.${this.identifier('unique')} = 1 as is_unique,
73
+ GROUP_CONCAT(ii.seqno || ' ' || ii.name || ' ' || ii.desc) AS columns
74
+ FROM PRAGMA_INDEX_LIST('${table}') il
75
+ JOIN PRAGMA_INDEX_XINFO(il.name) ii
76
+ WHERE il.name NOT LIKE 'sqlite_%'
77
+ GROUP BY 1, 2
78
+ `)
79
+ ]);
80
+
81
+ return {
82
+ columns: columns.records.map(col => ({
83
+ ...col,
84
+ is_notnull: !!col.is_notnull
85
+ })),
86
+ foreignKeys: foreignKeys.records,
87
+ indices: indices.records.map(idx => ({
88
+ name: idx.name,
89
+ is_unique: idx.is_unique,
90
+ columns: idx.columns.split(',')
91
+ .map(col => col.split(' '))
92
+ .map(([order, name, desc]) => [+order, { name, desc: desc === '1' }] as const)
93
+ .sort((a, b) => a[0] - b[0])
94
+ .map(([, item]) => item)
95
+ }))
96
+ };
97
+ }
98
+
49
99
  /**
50
100
  * Define column modification
51
101
  */