@travetto/model-sqlite 8.0.0-alpha.26 → 8.0.0-alpha.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-sqlite",
3
- "version": "8.0.0-alpha.26",
3
+ "version": "8.0.0-alpha.28",
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,11 +27,11 @@
27
27
  "directory": "module/model-sqlite"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/config": "^8.0.0-alpha.22",
31
- "@travetto/context": "^8.0.0-alpha.20",
32
- "@travetto/model": "^8.0.0-alpha.23",
33
- "@travetto/model-query": "^8.0.0-alpha.24",
34
- "@travetto/model-sql": "^8.0.0-alpha.26"
30
+ "@travetto/config": "^8.0.0-alpha.23",
31
+ "@travetto/context": "^8.0.0-alpha.21",
32
+ "@travetto/model": "^8.0.0-alpha.24",
33
+ "@travetto/model-query": "^8.0.0-alpha.26",
34
+ "@travetto/model-sql": "^8.0.0-alpha.28"
35
35
  },
36
36
  "travetto": {
37
37
  "displayName": "SQLite Model Service"
package/src/connection.ts CHANGED
@@ -6,7 +6,7 @@ import { createPool, type Pool } from 'generic-pool';
6
6
 
7
7
  import type { AsyncContext } from '@travetto/context';
8
8
  import { Injectable } from '@travetto/di';
9
- import { ExistsError } from '@travetto/model';
9
+ import { ExistsError, UniqueError } from '@travetto/model';
10
10
  import { SQLConnection } from '@travetto/model-sql';
11
11
  import { castTo, JSONUtil, Runtime, RuntimeError, ShutdownManager, Util } from '@travetto/runtime';
12
12
 
@@ -176,13 +176,19 @@ export class SqliteConnection extends SQLConnection<DatabaseSync> {
176
176
  switch (code) {
177
177
  case 'ERR_SQLITE_ERROR': {
178
178
  if (message?.startsWith('UNIQUE')) {
179
- throw new ExistsError('query', query);
179
+ const match = message.match(/UNIQUE constraint failed: (.*)/);
180
+ const key = match ? match[1] : 'query';
181
+ throw new UniqueError('query', key, { message, query });
180
182
  }
181
183
  break;
182
184
  }
183
- case 'SQLITE_CONSTRAINT_PRIMARYKEY':
184
185
  case 'SQLITE_CONSTRAINT_UNIQUE':
185
- case 'SQLITE_CONSTRAINT_INDEX':
186
+ case 'SQLITE_CONSTRAINT_INDEX': {
187
+ const match = message?.match(/UNIQUE constraint failed: (.*)/);
188
+ const key = match ? match[1] : 'query';
189
+ throw new UniqueError('query', key, { message, query });
190
+ }
191
+ case 'SQLITE_CONSTRAINT_PRIMARYKEY':
186
192
  throw new ExistsError('query', query);
187
193
  }
188
194
  if (/index.*?already exists/.test(message ?? '')) {
package/src/dialect.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { AbstractANSI99Dialect, type TableContext, type TransactionStatements } from '@travetto/model-sql';
1
+ import { AbstractANSI99Dialect, type ResolvedPathContext, type TableContext, type TransactionStatements } from '@travetto/model-sql';
2
2
  import { type Class, castTo, JSONUtil } from '@travetto/runtime';
3
- import type { SchemaFieldConfig } from '@travetto/schema';
3
+ import { type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
4
4
 
5
5
  export class SqliteDialect extends AbstractANSI99Dialect {
6
6
  returningSupport = true;
@@ -41,41 +41,126 @@ export class SqliteDialect extends AbstractANSI99Dialect {
41
41
  return `json_extract(${columnName}, '$.${jsonPath.join('.')}')`;
42
42
  }
43
43
 
44
- compileArrayAll(sqlPath: string, identifier: string, value: unknown[]): { sql: string; formatted: unknown } {
44
+ #getSqliteArrayExpression(context: ResolvedPathContext): string {
45
+ if (!context.arrayPath || context.arrayPath.length === 0) {
46
+ return context.sqlPath;
47
+ }
48
+
49
+ const columnName = this.escapeIdentifier(context.arrayPath[0]);
50
+ return context.arrayPath.length > 1 ? `json_extract(${columnName}, '$.${context.arrayPath.slice(1).join('.')}')` : columnName;
51
+ }
52
+
53
+ #buildSubPathCondition(context: ResolvedPathContext, parentExpression: string, onLeaf: (leafExpression: string) => string): string {
54
+ if (!context.subPath || context.subPath.length === 0) {
55
+ return onLeaf(parentExpression);
56
+ }
57
+
58
+ const arraySegmentIndices: number[] = [];
59
+ let currentClass: Class | undefined = context.arrayField?.type;
60
+
61
+ for (let index = 0; index < context.subPath.length; index++) {
62
+ const segment = context.subPath[index];
63
+ if (currentClass) {
64
+ const classConfiguration = SchemaRegistryIndex.getOptional(currentClass)?.get();
65
+ const fieldConfiguration = classConfiguration?.fields[segment];
66
+ if (fieldConfiguration) {
67
+ if (fieldConfiguration.array) {
68
+ arraySegmentIndices.push(index);
69
+ }
70
+ currentClass = fieldConfiguration.type;
71
+ }
72
+ }
73
+ }
74
+
75
+ if (arraySegmentIndices.length === 0) {
76
+ const leafExpression = `json_extract(${parentExpression}, '$.${context.subPath.join('.')}')`;
77
+ return onLeaf(leafExpression);
78
+ }
79
+
80
+ const buildLevel = (levelIndex: number, currentParent: string): string => {
81
+ const startPathIndex = levelIndex === 0 ? 0 : arraySegmentIndices[levelIndex - 1] + 1;
82
+ const endPathIndex = arraySegmentIndices[levelIndex];
83
+ const arrayPath = context.subPath!.slice(startPathIndex, endPathIndex + 1).join('.');
84
+ const alias = `ing_${levelIndex}`;
85
+
86
+ const innerCondition =
87
+ levelIndex === arraySegmentIndices.length - 1
88
+ ? (() => {
89
+ const leafPath = context.subPath!.slice(endPathIndex + 1).join('.');
90
+ const leafExpression = leafPath ? `json_extract(${alias}.value, '$.${leafPath}')` : `${alias}.value`;
91
+ return onLeaf(leafExpression);
92
+ })()
93
+ : buildLevel(levelIndex + 1, `${alias}.value`);
94
+
95
+ return `
96
+ EXISTS (
97
+ SELECT 1
98
+ FROM json_each(${currentParent}, '$.${arrayPath}') AS ${alias}
99
+ WHERE ${innerCondition}
100
+ )`;
101
+ };
102
+
103
+ return buildLevel(0, parentExpression);
104
+ }
105
+
106
+ #buildArrayElementExists(context: ResolvedPathContext, onLeaf: (leafExpression: string) => string): string {
107
+ const jsonArrayExpression = this.#getSqliteArrayExpression(context);
108
+ const subPathCondition = this.#buildSubPathCondition(context, 'elem.value', onLeaf);
109
+ return `EXISTS (
110
+ SELECT 1
111
+ FROM json_each(${jsonArrayExpression}) AS elem
112
+ WHERE ${subPathCondition}
113
+ )`;
114
+ }
115
+
116
+ compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]): { sql: string; formatted: unknown } {
117
+ const elementExists = this.#buildArrayElementExists(context, leafExpression => `${leafExpression} = req.value`);
45
118
  return {
46
- sql: `NOT EXISTS (SELECT 1 FROM json_each(${identifier}) AS req WHERE req.value NOT IN (SELECT value FROM json_each(${sqlPath})))`,
119
+ sql: `NOT EXISTS (
120
+ SELECT 1
121
+ FROM json_each(${identifier}) AS req
122
+ WHERE NOT ${elementExists}
123
+ )`,
47
124
  formatted: JSONUtil.toUTF8(value)
48
125
  };
49
126
  }
50
127
 
51
- compileArrayEquals(sqlPath: string, identifier: string, values: unknown): { sql: string; formatted: unknown } {
128
+ compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown): { sql: string; formatted: unknown } {
52
129
  if (Array.isArray(values)) {
53
130
  return {
54
- sql: `NOT EXISTS (SELECT 1 FROM json_each(${identifier}) AS req WHERE req.value NOT IN (SELECT value FROM json_each(${sqlPath})))`,
131
+ sql: this.#buildArrayElementExists(context, leafExpression => `${leafExpression} IN (SELECT value FROM json_each(${identifier}))`),
55
132
  formatted: JSONUtil.toUTF8(values)
56
133
  };
57
134
  }
135
+
58
136
  if (typeof values === 'object' && values !== null) {
59
137
  return {
60
- sql: `EXISTS (SELECT 1 FROM json_each(${sqlPath}) AS elem WHERE NOT EXISTS (SELECT 1 FROM json_each(${identifier}) AS req WHERE json_extract(elem.value, '$.' || req.key) IS NOT req.value))`,
138
+ sql: this.#buildArrayElementExists(
139
+ context,
140
+ leafExpression => `
141
+ NOT EXISTS (
142
+ SELECT 1
143
+ FROM json_each(${identifier}) AS req
144
+ WHERE json_extract(${leafExpression}, '$.' || req.key) IS NOT req.value
145
+ )`
146
+ ),
61
147
  formatted: JSONUtil.toUTF8(values)
62
148
  };
63
149
  }
150
+
64
151
  return {
65
- sql: `EXISTS (SELECT 1 FROM json_each(${sqlPath}) WHERE json_each.value = ${identifier})`,
152
+ sql: this.#buildArrayElementExists(context, leafExpression => `${leafExpression} = ${identifier}`),
66
153
  formatted: values
67
154
  };
68
155
  }
69
156
 
70
- compileArrayAny(sqlPath: string, identifier: string, values: unknown[]): { sql: string; formatted: unknown } {
71
- return {
72
- sql: `EXISTS (SELECT 1 FROM json_each(${sqlPath}) AS elem WHERE elem.value IN (SELECT value FROM json_each(${identifier})))`,
73
- formatted: JSONUtil.toUTF8(values)
74
- };
157
+ compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]): { sql: string; formatted: unknown } {
158
+ return this.compileArrayEquals(context, identifier, values);
75
159
  }
76
160
 
77
- compileArrayExists(sqlPath: string): { sql: string } {
78
- return { sql: `(${sqlPath} IS NOT NULL AND json_array_length(${sqlPath}) > 0)` };
161
+ compileArrayExists(context: ResolvedPathContext, identifier?: string): { sql: string } {
162
+ const jsonArrayExpression = this.#getSqliteArrayExpression(context);
163
+ return { sql: `(${jsonArrayExpression} IS NOT NULL AND json_array_length(${jsonArrayExpression}) > 0)` };
79
164
  }
80
165
 
81
166
  getRegexOperator(caseInsensitive: boolean): string {