@travetto/model-sqlite 7.0.0-rc.2 → 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 +2 -2
- package/package.json +6 -6
- package/src/connection.ts +29 -8
- package/src/dialect.ts +51 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-sqlite",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
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.
|
|
30
|
-
"@travetto/context": "^7.0.0-rc.
|
|
31
|
-
"@travetto/model": "^7.0.0-rc.
|
|
32
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
33
|
-
"@travetto/model-sql": "^7.0.0-rc.
|
|
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
|
*/
|
|
@@ -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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
*/
|