@travetto/model-sqlite 7.0.0-rc.1 → 7.0.0-rc.3

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
@@ -37,8 +37,8 @@ import { SqliteDialect } from '@travetto/model-sqlite';
37
37
 
38
38
  export class Init {
39
39
  @InjectableFactory({ primary: true })
40
- static getModelService(ctx: AsyncContext, conf: SQLModelConfig) {
41
- return new SQLModelService(ctx, conf, new SqliteDialect(ctx, conf));
40
+ static getModelService(ctx: AsyncContext, config: SQLModelConfig) {
41
+ return new SQLModelService(ctx, config, new SqliteDialect(ctx, config));
42
42
  }
43
43
  }
44
44
  ```
@@ -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.1",
3
+ "version": "7.0.0-rc.3",
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.1",
30
- "@travetto/context": "^7.0.0-rc.1",
31
- "@travetto/model": "^7.0.0-rc.1",
32
- "@travetto/model-query": "^7.0.0-rc.1",
33
- "@travetto/model-sql": "^7.0.0-rc.1",
29
+ "@travetto/config": "^7.0.0-rc.3",
30
+ "@travetto/context": "^7.0.0-rc.3",
31
+ "@travetto/model": "^7.0.0-rc.3",
32
+ "@travetto/model-query": "^7.0.0-rc.3",
33
+ "@travetto/model-sql": "^7.0.0-rc.3",
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
  */
@@ -27,20 +32,20 @@ export class SqliteConnection extends Connection<Database> {
27
32
  this.#config = config;
28
33
  }
29
34
 
30
- async #withRetries<T>(op: () => Promise<T>, retries = 10, delay = 250): Promise<T> {
31
- for (; ;) {
35
+ async #withRetries<T>(operation: () => Promise<T>, retries = 10, delay = 250): Promise<T> {
36
+ for (; retries > 1; retries -= 1) {
32
37
  try {
33
- return await op();
34
- } catch (err) {
35
- if (err instanceof Error && retries > 1 && err.message.includes('database is locked')) {
38
+ return await operation();
39
+ } catch (error) {
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
- throw err;
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> {
@@ -72,23 +77,29 @@ export class SqliteConnection extends Connection<Database> {
72
77
  ShutdownManager.onGracefulShutdown(() => this.#pool.clear());
73
78
  }
74
79
 
75
- async execute<T = unknown>(conn: Database, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
80
+ async execute<T = unknown>(connection: Database, query: string, values?: unknown[]): Promise<{ count: number, records: T[] }> {
76
81
  return this.#withRetries(async () => {
77
82
  console.debug('Executing query', { query });
78
83
  try {
79
- const out = await conn.prepare<unknown[], T>(query)[query.trim().startsWith('SELECT') ? 'all' : 'run'](...values ?? []);
84
+ const out = await connection.prepare<unknown[], T>(query)[query.trim().startsWith('SELECT') ? 'all' : 'run'](...values ?? []);
80
85
  if (Array.isArray(out)) {
81
- const records: T[] = out.map(v => ({ ...v }));
86
+ const records: T[] = out.map(item => ({ ...item }));
82
87
  return { count: out.length, records };
83
88
  } else {
84
89
  return { count: out.changes, records: [] };
85
90
  }
86
- } catch (err) {
87
- if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
88
- throw new ExistsError('query', query);
89
- } else {
90
- throw err;
91
+ } catch (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
 
@@ -14,12 +14,12 @@ import { SqliteConnection } from './connection.ts';
14
14
  @Injectable()
15
15
  export class SqliteDialect extends SQLDialect {
16
16
 
17
- conn: SqliteConnection;
17
+ connection: SqliteConnection;
18
18
  config: SQLModelConfig;
19
19
 
20
20
  constructor(context: AsyncContext, config: SQLModelConfig) {
21
21
  super(config.namespace);
22
- this.conn = new SqliteConnection(context, config);
22
+ this.connection = new SqliteConnection(context, config);
23
23
  this.config = config;
24
24
 
25
25
  // Special operators
@@ -46,14 +46,64 @@ 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
  */
52
102
  getModifyColumnSQL(stack: VisitStack[]): string {
53
103
  const field: SchemaFieldConfig = castTo(stack.at(-1));
54
104
  const type = this.getColumnType(field);
55
- const ident = this.ident(field.name.toString());
56
- return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${ident} TYPE ${type} USING (${ident}::${type});`;
105
+ const identifier = this.identifier(field.name);
106
+ return `ALTER TABLE ${this.parentTable(stack)} ALTER COLUMN ${identifier} TYPE ${type} USING (${identifier}::${type});`;
57
107
  }
58
108
 
59
109
  /**