@travetto/model-sql 8.0.0-alpha.9 → 8.0.1
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 +29 -9
- package/__index__.ts +4 -6
- package/package.json +19 -18
- package/src/connection.ts +218 -0
- package/src/dialect.ts +847 -0
- package/src/schema.ts +61 -0
- package/src/service.ts +749 -240
- package/src/types.ts +23 -9
- package/support/test/dialect.ts +125 -0
- package/support/test/query.ts +115 -72
- package/src/config.ts +0 -45
- package/src/connection/base.ts +0 -188
- package/src/connection/decorator.ts +0 -54
- package/src/dialect/base.ts +0 -1102
- package/src/internal/types.ts +0 -64
- package/src/table-manager.ts +0 -162
- package/src/util.ts +0 -331
package/src/dialect.ts
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import type { IndexConfig, ModelType } from '@travetto/model';
|
|
2
|
+
import { ModelRegistryIndex } from '@travetto/model';
|
|
3
|
+
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
4
|
+
import { isModelQueryIndex, ModelQueryUtil, type QueryIndexConfig, type SortClause, type WhereClause } from '@travetto/model-query';
|
|
5
|
+
import { type Class, castTo, JSONUtil, RuntimeError } from '@travetto/runtime';
|
|
6
|
+
import { DataUtil, type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
7
|
+
|
|
8
|
+
import { SQLModelSchemaUtil } from './schema.ts';
|
|
9
|
+
import type { JSONSqlPathMode, ResolvedPathContext, SchemaContext, TableContext } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export interface TransactionStatements {
|
|
12
|
+
begin: string;
|
|
13
|
+
beginNested: string;
|
|
14
|
+
isolate: string;
|
|
15
|
+
rollback: string;
|
|
16
|
+
rollbackNested: string;
|
|
17
|
+
commit: string;
|
|
18
|
+
commitNested: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface QueryClause {
|
|
22
|
+
sql?: string;
|
|
23
|
+
parameters?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type IdentificationPath = string;
|
|
27
|
+
|
|
28
|
+
function extractQueryIndexPathAndDirection(indexField: Record<string, unknown>): { path: string[]; sortDirection: 1 | -1 | true } {
|
|
29
|
+
const path: string[] = [];
|
|
30
|
+
let current: unknown = indexField;
|
|
31
|
+
while (typeof current === 'object' && current !== null) {
|
|
32
|
+
const keys = Object.keys(current);
|
|
33
|
+
if (keys.length === 0) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
const key = keys[0];
|
|
37
|
+
if (key.includes('.')) {
|
|
38
|
+
path.push(...key.split('.'));
|
|
39
|
+
} else {
|
|
40
|
+
path.push(key);
|
|
41
|
+
}
|
|
42
|
+
current = castTo<Record<string, unknown>>(current)[key];
|
|
43
|
+
}
|
|
44
|
+
const sortDirection = castTo<1 | -1 | true>(current ?? 1);
|
|
45
|
+
return { path, sortDirection };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Abstract ANSI SQL-99 Dialect base implementation.
|
|
50
|
+
* Pure SQL text generator and query builder (does not execute queries or hold connection state).
|
|
51
|
+
*/
|
|
52
|
+
export abstract class AbstractANSI99Dialect {
|
|
53
|
+
static TRANSACTION_STATEMENTS: TransactionStatements = {
|
|
54
|
+
begin: 'BEGIN;',
|
|
55
|
+
beginNested: 'SAVEPOINT $1;',
|
|
56
|
+
isolate: 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED;',
|
|
57
|
+
rollback: 'ROLLBACK;',
|
|
58
|
+
rollbackNested: 'ROLLBACK TO $1;',
|
|
59
|
+
commit: 'COMMIT;',
|
|
60
|
+
commitNested: 'RELEASE SAVEPOINT $1;'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
static SCHEMA_CACHE = new Map<Class, SchemaContext<unknown>>();
|
|
64
|
+
|
|
65
|
+
returningSupport = false;
|
|
66
|
+
suggestLikeOperator = 'LIKE';
|
|
67
|
+
transactionStatements: TransactionStatements = AbstractANSI99Dialect.TRANSACTION_STATEMENTS;
|
|
68
|
+
abstract getComplexColumnType(field: SchemaFieldConfig): string;
|
|
69
|
+
|
|
70
|
+
getComplexColumnValue(field: SchemaFieldConfig, value: unknown): unknown {
|
|
71
|
+
return value === null || value === undefined ? null : JSONUtil.toUTF8(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
escapeIdentifier(name: string): string {
|
|
75
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
escapeLiteral(value: string): string {
|
|
79
|
+
return value.replaceAll("'", "''");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getPlaceholder(index: number): string {
|
|
83
|
+
return '?';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
abstract getColumnType(fieldConfiguration: SchemaFieldConfig): string;
|
|
87
|
+
abstract compileJsonIndexPath(columnName: string, jsonPath: string[], mode: JSONSqlPathMode): string;
|
|
88
|
+
abstract compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]): { sql: string; formatted: unknown };
|
|
89
|
+
abstract compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown): { sql: string; formatted: unknown };
|
|
90
|
+
abstract compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]): { sql: string; formatted: unknown };
|
|
91
|
+
abstract compileArrayExists(context: ResolvedPathContext, identifier?: string): { sql: string };
|
|
92
|
+
abstract compileArrayRegex(context: ResolvedPathContext, identifier: string, value: RegExp | string): { sql: string; formatted: unknown };
|
|
93
|
+
|
|
94
|
+
abstract getRegexOperator(caseInsensitive: boolean): string;
|
|
95
|
+
abstract formatRegex(source: string, caseInsensitive: boolean): string;
|
|
96
|
+
abstract castColumn(sqlPath: string, type: Class): string;
|
|
97
|
+
|
|
98
|
+
compileJsonEquality?(sqlPath: string, identifier: string): string;
|
|
99
|
+
shiftPlaceholders?(whereSQL: string, offset: number): string;
|
|
100
|
+
|
|
101
|
+
buildSqlPath<T extends ModelType>(tableContext: TableContext<T>, path: string[], mode: JSONSqlPathMode): string {
|
|
102
|
+
const firstSegment = path[0];
|
|
103
|
+
const escapedFirst = this.escapeIdentifier(firstSegment);
|
|
104
|
+
if (tableContext.simpleFields.has(firstSegment)) {
|
|
105
|
+
if (path.length > 1) {
|
|
106
|
+
throw new RuntimeError(
|
|
107
|
+
`Cannot traverse nested properties under simple column "${firstSegment}" in table "${tableContext.tableName}"`,
|
|
108
|
+
{
|
|
109
|
+
category: 'data'
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return escapedFirst;
|
|
114
|
+
} else {
|
|
115
|
+
const nestedSegments = path.slice(1);
|
|
116
|
+
if (nestedSegments.length === 0) {
|
|
117
|
+
return escapedFirst;
|
|
118
|
+
}
|
|
119
|
+
return this.compileJsonIndexPath(escapedFirst, nestedSegments, mode);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
formatJsonPath(jsonPath: string[]): string {
|
|
124
|
+
return jsonPath.map(segment => (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment) ? segment : `"${segment.replaceAll('"', '\\"')}"`)).join('.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
compileIndexPath(context: TableContext, path: string[], mode: JSONSqlPathMode): string {
|
|
128
|
+
return this.resolvePath(context, path, mode).sqlPath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getCreateIndexSQL<T extends ModelType>(context: TableContext, indexConfig: IndexConfig<string, T> | QueryIndexConfig<T>): string {
|
|
132
|
+
const { tableName, cls: modelClass } = context;
|
|
133
|
+
const indexName = ['idx', tableName, indexConfig.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
134
|
+
|
|
135
|
+
if (isModelQueryIndex(indexConfig)) {
|
|
136
|
+
const indexFields = indexConfig.fields.map(field => {
|
|
137
|
+
const { path, sortDirection } = extractQueryIndexPathAndDirection(castTo(field));
|
|
138
|
+
const isAscending = typeof sortDirection === 'number' ? sortDirection === 1 : !sortDirection;
|
|
139
|
+
|
|
140
|
+
const expression = this.compileIndexPath(context, path, 'createIndex');
|
|
141
|
+
const formattedExpression = path.length > 1 ? `(${expression})` : expression;
|
|
142
|
+
return `${formattedExpression} ${isAscending ? 'ASC' : 'DESC'}`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return `CREATE ${indexConfig.unique ? 'UNIQUE ' : ''}INDEX ${this.escapeIdentifier(indexName)} ON ${this.escapeIdentifier(context.tableName)} (${indexFields.join(', ')});`;
|
|
146
|
+
} else if (isModelIndexedIndex(indexConfig)) {
|
|
147
|
+
const allFields = [...indexConfig.keyTemplate, ...indexConfig.sortTemplate];
|
|
148
|
+
const indexFields = allFields.map(({ path, value }) => {
|
|
149
|
+
const expression = this.compileIndexPath(context, path, 'createIndex');
|
|
150
|
+
const formattedExpression = path.length > 1 ? `(${expression})` : expression;
|
|
151
|
+
return `${formattedExpression} ${value === -1 ? 'DESC' : 'ASC'}`;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const isUnique = 'unique' in indexConfig && indexConfig.unique;
|
|
155
|
+
return `CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ${this.escapeIdentifier(indexName)} ON ${this.escapeIdentifier(context.tableName)} (${indexFields.join(', ')});`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new RuntimeError(`Unsupported index configuration for class ${modelClass.name}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getCreateTableSQL(context: TableContext): string {
|
|
162
|
+
const idType = this.getColumnType(castTo({ name: 'id', type: String }));
|
|
163
|
+
const columnDefinitions: string[] = [`${this.escapeIdentifier('id')} ${idType} PRIMARY KEY`];
|
|
164
|
+
|
|
165
|
+
for (const field of context.simpleFields.values()) {
|
|
166
|
+
if (field.name === 'id') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const columnType = this.getColumnType(field);
|
|
170
|
+
const isNotNullClause = SQLModelSchemaUtil.isColumnNotNull(context, field.name) ? ' NOT NULL' : '';
|
|
171
|
+
columnDefinitions.push(`${this.escapeIdentifier(field.name)} ${columnType}${isNotNullClause}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const field of context.complexFields.values()) {
|
|
175
|
+
const columnType = this.getComplexColumnType(field);
|
|
176
|
+
const isNotNullClause = SQLModelSchemaUtil.isColumnNotNull(context, field.name) ? ' NOT NULL' : '';
|
|
177
|
+
columnDefinitions.push(`${this.escapeIdentifier(field.name)} ${columnType}${isNotNullClause}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `
|
|
181
|
+
CREATE TABLE ${this.escapeIdentifier(context.tableName)} (
|
|
182
|
+
${columnDefinitions.join(',\n ')}
|
|
183
|
+
);
|
|
184
|
+
`.trim();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getCreateTableIndexSQLs(context: TableContext): string[] {
|
|
188
|
+
const indexes = ModelRegistryIndex.getIndices(context.cls) || [];
|
|
189
|
+
return indexes.map(indexConfig => this.getCreateIndexSQL(context, indexConfig));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getAddColumnSQL(context: TableContext, columnName: string, columnType: string): string {
|
|
193
|
+
return `ALTER TABLE ${this.escapeIdentifier(context.tableName)} ADD COLUMN ${this.escapeIdentifier(columnName)} ${columnType};`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
abstract getUpsertSQL(
|
|
197
|
+
context: TableContext,
|
|
198
|
+
columns: string[],
|
|
199
|
+
placeholders: string[],
|
|
200
|
+
conflictTarget: string[],
|
|
201
|
+
updates: string[]
|
|
202
|
+
): string;
|
|
203
|
+
|
|
204
|
+
normalizeIndexDefinition(sql: string): string {
|
|
205
|
+
return sql
|
|
206
|
+
.toLowerCase()
|
|
207
|
+
.replaceAll('"', '')
|
|
208
|
+
.replaceAll("'", '')
|
|
209
|
+
.replaceAll('`', '')
|
|
210
|
+
.replaceAll(' ', '')
|
|
211
|
+
.replaceAll('asc', '')
|
|
212
|
+
.replaceAll('desc', '')
|
|
213
|
+
.replaceAll('btree', '')
|
|
214
|
+
.replaceAll('public.', '')
|
|
215
|
+
.replaceAll('::text', '')
|
|
216
|
+
.replaceAll('(', '')
|
|
217
|
+
.replaceAll(')', '');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
abstract getTableExistsQuery(context: TableContext): { sql: string; parameters?: unknown[] };
|
|
221
|
+
abstract parseTableExistsResult(records: unknown[]): boolean;
|
|
222
|
+
abstract getExistingColumnsQuery(context: TableContext): { sql: string; parameters?: unknown[] };
|
|
223
|
+
abstract parseExistingColumns(records: unknown[]): Map<string, string>;
|
|
224
|
+
abstract getExistingIndexesQuery(context: TableContext): { sql: string; parameters?: unknown[] };
|
|
225
|
+
abstract parseExistingIndexes(records: unknown[]): Map<string, string>;
|
|
226
|
+
abstract getDropIndexSQL(context: TableContext, indexName: string): string;
|
|
227
|
+
abstract getTruncateTableSQL(context: TableContext): string;
|
|
228
|
+
|
|
229
|
+
getDropTableSQL(context: TableContext): string {
|
|
230
|
+
return `DROP TABLE IF EXISTS ${this.escapeIdentifier(context.tableName)};`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getAlterColumnTypeSQL?(context: TableContext, columnName: string, columnType: string, existingType: string): string | undefined;
|
|
234
|
+
|
|
235
|
+
// Query Compilation
|
|
236
|
+
static #combineResults(results: QueryClause[], operator: string): QueryClause {
|
|
237
|
+
const filtered = results.filter(result => !!result.sql);
|
|
238
|
+
|
|
239
|
+
if (filtered.length === 0) {
|
|
240
|
+
return {};
|
|
241
|
+
} else if (filtered.length === 1) {
|
|
242
|
+
return filtered[0];
|
|
243
|
+
} else {
|
|
244
|
+
const fullOperator = ` ${operator} `;
|
|
245
|
+
return {
|
|
246
|
+
sql: `(${filtered.map(result => result.sql).join(fullOperator)})`,
|
|
247
|
+
parameters: Object.assign({}, ...results.map(result => result.parameters))
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
compileWhere<T extends ModelType>(
|
|
253
|
+
tableContext: TableContext<T>,
|
|
254
|
+
where?: WhereClause<T>,
|
|
255
|
+
checkExpiry = true
|
|
256
|
+
): {
|
|
257
|
+
whereSQL?: string;
|
|
258
|
+
parameters?: unknown[];
|
|
259
|
+
} {
|
|
260
|
+
const resolvedWhere = ModelQueryUtil.getWhereClause(tableContext.cls, where, checkExpiry);
|
|
261
|
+
const compiled = this.#compileClause(tableContext, resolvedWhere);
|
|
262
|
+
if (Object.entries(compiled.parameters ?? {}).length) {
|
|
263
|
+
const parameters: unknown[] = [];
|
|
264
|
+
const seen = new Map<string, string>();
|
|
265
|
+
const sql = compiled
|
|
266
|
+
.sql!.replace(/%%([^%]{0,200})%%/g, key => {
|
|
267
|
+
if (!seen.has(key)) {
|
|
268
|
+
parameters.push(compiled.parameters![key]);
|
|
269
|
+
seen.set(key, this.getPlaceholder(parameters.length));
|
|
270
|
+
}
|
|
271
|
+
return seen.get(key)!;
|
|
272
|
+
})
|
|
273
|
+
.trim();
|
|
274
|
+
return { whereSQL: sql, parameters };
|
|
275
|
+
} else {
|
|
276
|
+
return { whereSQL: compiled.sql?.trim() };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
compileSort<T extends ModelType>(tableContext: TableContext<T>, sort?: SortClause<T>[]): string {
|
|
281
|
+
if (!sort || sort.length === 0) {
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
const sortClauses = sort.map(sortClause => {
|
|
285
|
+
const key = Object.keys(sortClause)[0];
|
|
286
|
+
const direction = castTo<Record<string, 1 | -1>>(sortClause)[key];
|
|
287
|
+
const path = key.split('.');
|
|
288
|
+
const { sqlPath } = this.resolvePath(tableContext, path, 'orderBy');
|
|
289
|
+
return `${sqlPath} ${direction === -1 ? 'DESC' : 'ASC'}`;
|
|
290
|
+
});
|
|
291
|
+
return sortClauses.length ? `ORDER BY ${sortClauses.join(', ')}` : '';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#resolveSchemaPath<T extends ModelType>(
|
|
295
|
+
tableContext: TableContext<T>,
|
|
296
|
+
path: string[]
|
|
297
|
+
): {
|
|
298
|
+
leafField?: SchemaFieldConfig;
|
|
299
|
+
arrayField?: SchemaFieldConfig;
|
|
300
|
+
arraySegmentIndex?: number;
|
|
301
|
+
} {
|
|
302
|
+
const firstSegment = path[0];
|
|
303
|
+
|
|
304
|
+
if (tableContext.simpleFields.has(firstSegment)) {
|
|
305
|
+
if (path.length > 1) {
|
|
306
|
+
throw new RuntimeError(
|
|
307
|
+
`Cannot traverse nested properties under simple column "${firstSegment}" in table "${tableContext.tableName}"`,
|
|
308
|
+
{
|
|
309
|
+
category: 'data'
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return { leafField: tableContext.simpleFields.get(firstSegment) };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let currentField: SchemaFieldConfig | undefined = tableContext.complexFields.get(firstSegment);
|
|
317
|
+
let arrayField: SchemaFieldConfig | undefined = currentField?.array ? currentField : undefined;
|
|
318
|
+
let arraySegmentIndex: number | undefined = currentField?.array ? 0 : undefined;
|
|
319
|
+
let currentClass = currentField?.type;
|
|
320
|
+
|
|
321
|
+
for (let pathIndex = 1; pathIndex < path.length; pathIndex += 1) {
|
|
322
|
+
const segment = path[pathIndex];
|
|
323
|
+
const subclassConfiguration = SchemaRegistryIndex.getOptional(currentClass!)?.get();
|
|
324
|
+
currentField = subclassConfiguration?.fields[segment];
|
|
325
|
+
if (currentField?.array && !arrayField) {
|
|
326
|
+
arrayField = currentField;
|
|
327
|
+
arraySegmentIndex = pathIndex;
|
|
328
|
+
}
|
|
329
|
+
currentClass = currentField?.type;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
leafField: currentField,
|
|
334
|
+
arrayField,
|
|
335
|
+
arraySegmentIndex
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getSchemaSubPathMetadata(
|
|
340
|
+
initialClass: Class | undefined,
|
|
341
|
+
subPath: string[]
|
|
342
|
+
): Array<{ segment: string; fieldConfig?: SchemaFieldConfig; isArray: boolean; fieldClass?: Class }> {
|
|
343
|
+
let currentClass = initialClass;
|
|
344
|
+
return subPath.map(segment => {
|
|
345
|
+
let fieldConfig: SchemaFieldConfig | undefined;
|
|
346
|
+
let isArray = false;
|
|
347
|
+
if (currentClass) {
|
|
348
|
+
const classConfiguration = SchemaRegistryIndex.getOptional(currentClass)?.get();
|
|
349
|
+
fieldConfig = classConfiguration?.fields[segment];
|
|
350
|
+
if (fieldConfig) {
|
|
351
|
+
isArray = !!fieldConfig.array;
|
|
352
|
+
currentClass = fieldConfig.type;
|
|
353
|
+
} else {
|
|
354
|
+
currentClass = undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { segment, fieldConfig, isArray, fieldClass: currentClass };
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
resolvePath<T extends ModelType>(tableContext: TableContext<T>, path: string[], mode: JSONSqlPathMode): ResolvedPathContext {
|
|
362
|
+
const firstSegment = path[0];
|
|
363
|
+
|
|
364
|
+
if (tableContext.simpleFields.has(firstSegment)) {
|
|
365
|
+
const { leafField } = this.#resolveSchemaPath(tableContext, path);
|
|
366
|
+
return { sqlPath: this.buildSqlPath(tableContext, path, mode), leafField };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { leafField, arrayField, arraySegmentIndex } = this.#resolveSchemaPath(tableContext, path);
|
|
370
|
+
const sqlPath = this.buildSqlPath(tableContext, path, mode);
|
|
371
|
+
|
|
372
|
+
const finalSqlPath = leafField && !leafField.array ? this.castColumn(sqlPath, leafField.type) : sqlPath;
|
|
373
|
+
|
|
374
|
+
const arrayPath = arraySegmentIndex !== undefined ? path.slice(0, arraySegmentIndex + 1) : undefined;
|
|
375
|
+
const subPath = arraySegmentIndex !== undefined ? path.slice(arraySegmentIndex + 1) : undefined;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
sqlPath: finalSqlPath,
|
|
379
|
+
leafField,
|
|
380
|
+
arrayField,
|
|
381
|
+
arrayPath,
|
|
382
|
+
subPath
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#compileClause<T extends ModelType>(
|
|
387
|
+
tableContext: TableContext<T>,
|
|
388
|
+
clause: WhereClause<T>,
|
|
389
|
+
identificationPath: IdentificationPath = ''
|
|
390
|
+
): QueryClause {
|
|
391
|
+
if (!clause) {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
if (ModelQueryUtil.has$And(clause)) {
|
|
395
|
+
const compiled = clause.$and
|
|
396
|
+
.map((item, index) => this.#compileClause(tableContext, item, `${identificationPath}_${index}`))
|
|
397
|
+
.filter(Boolean);
|
|
398
|
+
return AbstractANSI99Dialect.#combineResults(compiled, 'AND');
|
|
399
|
+
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
400
|
+
const compiled = clause.$or
|
|
401
|
+
.map((item, index) => this.#compileClause(tableContext, item, `${identificationPath}_${index}`))
|
|
402
|
+
.filter(Boolean);
|
|
403
|
+
return AbstractANSI99Dialect.#combineResults(compiled, 'OR');
|
|
404
|
+
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
405
|
+
const compiled = this.#compileClause(tableContext, clause.$not, identificationPath);
|
|
406
|
+
return compiled
|
|
407
|
+
? {
|
|
408
|
+
sql: `NOT (${compiled.sql})`,
|
|
409
|
+
parameters: compiled.parameters
|
|
410
|
+
}
|
|
411
|
+
: {};
|
|
412
|
+
} else {
|
|
413
|
+
return this.#compileSimple(tableContext, clause, [], identificationPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#compileSimple<T extends ModelType>(
|
|
418
|
+
tableContext: TableContext<T>,
|
|
419
|
+
item: Record<string, unknown>,
|
|
420
|
+
parentPath: string[] = [],
|
|
421
|
+
identificationPath: IdentificationPath = ''
|
|
422
|
+
): QueryClause {
|
|
423
|
+
if (!item) {
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
const clauses: QueryClause[] = [];
|
|
427
|
+
|
|
428
|
+
let index = 0;
|
|
429
|
+
for (const [key, value] of Object.entries(item)) {
|
|
430
|
+
index += 1;
|
|
431
|
+
const currentPath = [...parentPath, key];
|
|
432
|
+
const isPlainObject = DataUtil.isPlainObject(value);
|
|
433
|
+
const firstKey = isPlainObject ? Object.keys(value)[0] : '';
|
|
434
|
+
|
|
435
|
+
const nextIdentificationPath = `${identificationPath}__${index}`;
|
|
436
|
+
|
|
437
|
+
if (isPlainObject) {
|
|
438
|
+
if (firstKey.startsWith('$')) {
|
|
439
|
+
clauses.push(this.#compileOperator(tableContext, currentPath, value as Record<string, unknown>, nextIdentificationPath));
|
|
440
|
+
} else {
|
|
441
|
+
clauses.push(this.#compileSimple(tableContext, value as Record<string, unknown>, currentPath, nextIdentificationPath));
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
clauses.push(this.#compileOperator(tableContext, currentPath, { $eq: value }, nextIdentificationPath));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return AbstractANSI99Dialect.#combineResults(clauses, 'AND');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#compileOperator<T extends ModelType>(
|
|
452
|
+
tableContext: TableContext<T>,
|
|
453
|
+
path: string[],
|
|
454
|
+
operation: Record<string, unknown>,
|
|
455
|
+
identificationPath: IdentificationPath = ''
|
|
456
|
+
): QueryClause {
|
|
457
|
+
const resolvedContext = this.resolvePath(tableContext, path, 'read');
|
|
458
|
+
const { sqlPath, leafField, arrayField } = resolvedContext;
|
|
459
|
+
const effectiveArrayField = leafField?.array ? leafField : arrayField;
|
|
460
|
+
const clauses: QueryClause[] = [];
|
|
461
|
+
|
|
462
|
+
let index = 0;
|
|
463
|
+
for (let [operator, value] of Object.entries(operation)) {
|
|
464
|
+
index += 1;
|
|
465
|
+
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
value = value.map(valueItem => ModelQueryUtil.resolveComparator(valueItem));
|
|
468
|
+
} else {
|
|
469
|
+
value = ModelQueryUtil.resolveComparator(value);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const nestedIdentificationPath = `${identificationPath}_${index}`;
|
|
473
|
+
const identifier = `%%${nestedIdentificationPath}%%`;
|
|
474
|
+
|
|
475
|
+
let clause: QueryClause;
|
|
476
|
+
|
|
477
|
+
if (effectiveArrayField) {
|
|
478
|
+
if (operator === '$eq' || operator === '$ne') {
|
|
479
|
+
const { sql, formatted } = this.compileArrayEquals(resolvedContext, identifier, value);
|
|
480
|
+
const finalSql = operator === '$ne' ? `NOT(${sql})` : sql;
|
|
481
|
+
clause = { parameters: { [identifier]: formatted }, sql: finalSql };
|
|
482
|
+
} else if (operator === '$in' || operator === '$nin') {
|
|
483
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
484
|
+
clause = operator === '$in' ? { sql: '1=0' } : {};
|
|
485
|
+
} else {
|
|
486
|
+
const { sql, formatted } = this.compileArrayAny(resolvedContext, identifier, value);
|
|
487
|
+
const finalSql = operator === '$nin' ? `NOT(${sql})` : sql;
|
|
488
|
+
clause = { sql: finalSql, parameters: { [identifier]: formatted } };
|
|
489
|
+
}
|
|
490
|
+
} else if (operator === '$all') {
|
|
491
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
492
|
+
clause = { sql: '1=0' };
|
|
493
|
+
} else {
|
|
494
|
+
const { sql, formatted } = this.compileArrayAll(resolvedContext, identifier, value);
|
|
495
|
+
clause = { sql, parameters: { [identifier]: formatted } };
|
|
496
|
+
}
|
|
497
|
+
} else if (operator === '$exists') {
|
|
498
|
+
const { sql } = this.compileArrayExists(resolvedContext, identifier);
|
|
499
|
+
const finalSql = !value ? `NOT(${sql})` : sql;
|
|
500
|
+
clause = { sql: finalSql };
|
|
501
|
+
} else if (operator === '$regex') {
|
|
502
|
+
const { sql, formatted } = this.compileArrayRegex(resolvedContext, identifier, value as RegExp | string);
|
|
503
|
+
clause = { sql, parameters: { [identifier]: formatted } };
|
|
504
|
+
} else {
|
|
505
|
+
throw new RuntimeError(`Operator "${operator}" is not supported for arrays`, { category: 'data' });
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
if (operator === '$eq') {
|
|
509
|
+
if (value === null || value === undefined) {
|
|
510
|
+
clause = { sql: `${sqlPath} IS NULL` };
|
|
511
|
+
} else {
|
|
512
|
+
clause = {
|
|
513
|
+
sql: `${sqlPath} = ${identifier}`,
|
|
514
|
+
parameters: { [identifier]: value }
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
} else if (operator === '$ne') {
|
|
518
|
+
if (value === null || value === undefined) {
|
|
519
|
+
clause = { sql: `${sqlPath} IS NOT NULL` };
|
|
520
|
+
} else {
|
|
521
|
+
clause = {
|
|
522
|
+
sql: `${sqlPath} <> ${identifier}`,
|
|
523
|
+
parameters: { [identifier]: value }
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
} else if (operator === '$gt' || operator === '$gte' || operator === '$lt' || operator === '$lte') {
|
|
527
|
+
const sqlOperator = operator === '$gt' ? '>' : operator === '$gte' ? '>=' : operator === '$lt' ? '<' : '<=';
|
|
528
|
+
clause = {
|
|
529
|
+
sql: `${sqlPath} ${sqlOperator} ${identifier}`,
|
|
530
|
+
parameters: { [identifier]: value }
|
|
531
|
+
};
|
|
532
|
+
} else if (operator === '$in') {
|
|
533
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
534
|
+
clause = { sql: '1=0' };
|
|
535
|
+
} else {
|
|
536
|
+
const choices = value.map((valueItem, itemIndex) => {
|
|
537
|
+
const innerIdentifier = `%%${nestedIdentificationPath}_${itemIndex}%%`;
|
|
538
|
+
return [innerIdentifier, valueItem];
|
|
539
|
+
});
|
|
540
|
+
clause = {
|
|
541
|
+
sql: `${sqlPath} IN (${choices.map(choice => choice[0]).join(', ')})`,
|
|
542
|
+
parameters: Object.fromEntries(choices)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
} else if (operator === '$nin') {
|
|
546
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
547
|
+
clause = {};
|
|
548
|
+
} else {
|
|
549
|
+
const choices = value.map((valueItem, itemIndex) => {
|
|
550
|
+
const innerIdentifier = `%%${nestedIdentificationPath}_${itemIndex}%%`;
|
|
551
|
+
return [innerIdentifier, valueItem];
|
|
552
|
+
});
|
|
553
|
+
clause = {
|
|
554
|
+
sql: `${sqlPath} NOT IN (${choices.map(choice => choice[0]).join(', ')})`,
|
|
555
|
+
parameters: Object.fromEntries(choices)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
} else if (operator === '$exists') {
|
|
559
|
+
clause = { sql: value ? `${sqlPath} IS NOT NULL` : `${sqlPath} IS NULL` };
|
|
560
|
+
} else if (operator === '$regex') {
|
|
561
|
+
const regex = value instanceof RegExp ? value : new RegExp(String(value));
|
|
562
|
+
const caseInsensitive = regex.flags.includes('i');
|
|
563
|
+
const regexOp = this.getRegexOperator(caseInsensitive);
|
|
564
|
+
const regexSource = this.formatRegex(regex.source, caseInsensitive);
|
|
565
|
+
clause = {
|
|
566
|
+
parameters: { [identifier]: regexSource },
|
|
567
|
+
sql: `${sqlPath} ${regexOp} ${identifier}`
|
|
568
|
+
};
|
|
569
|
+
} else {
|
|
570
|
+
throw new RuntimeError(`Operator "${operator}" is not supported for scalar columns`, { category: 'data' });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (clause) {
|
|
575
|
+
clauses.push(clause);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return AbstractANSI99Dialect.#combineResults(clauses, 'AND');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Statement Builders
|
|
582
|
+
buildInsert<T extends ModelType>(tableContext: TableContext<T>, rawItem: Record<string, unknown>): { sql: string; values: unknown[] } {
|
|
583
|
+
return this.buildInsertAll(tableContext, [rawItem]);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
buildInsertAll<T extends ModelType>(
|
|
587
|
+
tableContext: TableContext<T>,
|
|
588
|
+
rawItems: Record<string, unknown>[]
|
|
589
|
+
): { sql: string; values: unknown[] } {
|
|
590
|
+
if (rawItems.length === 0) {
|
|
591
|
+
return { sql: '', values: [] };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const columns: string[] = [];
|
|
595
|
+
for (const field of tableContext.simpleFields.values()) {
|
|
596
|
+
columns.push(this.escapeIdentifier(field.name));
|
|
597
|
+
}
|
|
598
|
+
for (const field of tableContext.complexFields.values()) {
|
|
599
|
+
columns.push(this.escapeIdentifier(field.name));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const values: unknown[] = [];
|
|
603
|
+
const valueTuples: string[] = [];
|
|
604
|
+
|
|
605
|
+
for (const rawItem of rawItems) {
|
|
606
|
+
const tuplePlaceholders: string[] = [];
|
|
607
|
+
for (const field of tableContext.simpleFields.values()) {
|
|
608
|
+
tuplePlaceholders.push(this.getPlaceholder(values.length + 1));
|
|
609
|
+
const value = rawItem[field.name];
|
|
610
|
+
values.push(value === undefined || value === null ? null : value);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const field of tableContext.complexFields.values()) {
|
|
614
|
+
tuplePlaceholders.push(this.getPlaceholder(values.length + 1));
|
|
615
|
+
const value = rawItem[field.name];
|
|
616
|
+
values.push(this.getComplexColumnValue(field, value));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
valueTuples.push(`(${tuplePlaceholders.join(', ')})`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const sql = `INSERT INTO ${this.escapeIdentifier(tableContext.tableName)} (${columns.join(', ')}) VALUES ${valueTuples.join(', ')};`;
|
|
623
|
+
|
|
624
|
+
return { sql, values };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
buildUpdateAll<T extends ModelType>(
|
|
628
|
+
tableContext: TableContext<T>,
|
|
629
|
+
rawItems: Record<string, unknown>[]
|
|
630
|
+
): { sql: string; values: unknown[] } {
|
|
631
|
+
if (rawItems.length === 0) {
|
|
632
|
+
return { sql: '', values: [] };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (rawItems.length === 1) {
|
|
636
|
+
const { whereSQL, parameters = [] } = this.compileWhere(tableContext, castTo({ id: rawItems[0].id }));
|
|
637
|
+
return this.buildUpdate(tableContext, rawItems[0], whereSQL, parameters);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const simpleFieldsToUpdate = [...tableContext.simpleFields.values()].filter(field => field.name !== 'id');
|
|
641
|
+
const complexFieldsToUpdate = [...tableContext.complexFields.values()];
|
|
642
|
+
|
|
643
|
+
const setClauses: string[] = [];
|
|
644
|
+
const values: unknown[] = [];
|
|
645
|
+
|
|
646
|
+
const ids = rawItems.map(item => item.id);
|
|
647
|
+
|
|
648
|
+
for (const field of simpleFieldsToUpdate) {
|
|
649
|
+
const cases: string[] = [];
|
|
650
|
+
for (const rawItem of rawItems) {
|
|
651
|
+
const idPlaceholder = this.getPlaceholder(values.length + 1);
|
|
652
|
+
values.push(rawItem.id);
|
|
653
|
+
const valPlaceholder = this.getPlaceholder(values.length + 1);
|
|
654
|
+
const val = rawItem[field.name];
|
|
655
|
+
values.push(val === undefined || val === null ? null : val);
|
|
656
|
+
cases.push(`WHEN ${idPlaceholder} THEN ${valPlaceholder}`);
|
|
657
|
+
}
|
|
658
|
+
setClauses.push(`${this.escapeIdentifier(field.name)} = CASE ${this.escapeIdentifier('id')} ${cases.join(' ')} END`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const field of complexFieldsToUpdate) {
|
|
662
|
+
const cases: string[] = [];
|
|
663
|
+
for (const rawItem of rawItems) {
|
|
664
|
+
const idPlaceholder = this.getPlaceholder(values.length + 1);
|
|
665
|
+
values.push(rawItem.id);
|
|
666
|
+
const valPlaceholder = this.getPlaceholder(values.length + 1);
|
|
667
|
+
const val = rawItem[field.name];
|
|
668
|
+
values.push(this.getComplexColumnValue(field, val));
|
|
669
|
+
cases.push(`WHEN ${idPlaceholder} THEN ${valPlaceholder}`);
|
|
670
|
+
}
|
|
671
|
+
setClauses.push(`${this.escapeIdentifier(field.name)} = CASE ${this.escapeIdentifier('id')} ${cases.join(' ')} END`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (setClauses.length === 0) {
|
|
675
|
+
setClauses.push(`${this.escapeIdentifier('id')} = ${this.escapeIdentifier('id')}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const whereIdPlaceholders = ids.map(idVal => {
|
|
679
|
+
values.push(idVal);
|
|
680
|
+
return this.getPlaceholder(values.length);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const tableName = this.escapeIdentifier(tableContext.tableName);
|
|
684
|
+
const sql = `UPDATE ${tableName} SET ${setClauses.join(', ')} WHERE ${this.escapeIdentifier('id')} IN (${whereIdPlaceholders.join(', ')});`;
|
|
685
|
+
|
|
686
|
+
return { sql, values };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
buildUpdate<T extends ModelType>(
|
|
690
|
+
tableContext: TableContext<T>,
|
|
691
|
+
rawItem: Record<string, unknown>,
|
|
692
|
+
whereSQL?: string,
|
|
693
|
+
whereParameters: unknown[] = []
|
|
694
|
+
): { sql: string; values: unknown[] } {
|
|
695
|
+
const sets: string[] = [];
|
|
696
|
+
const values: unknown[] = [];
|
|
697
|
+
|
|
698
|
+
for (const field of tableContext.simpleFields.values()) {
|
|
699
|
+
if (field.name === 'id') {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
sets.push(`${this.escapeIdentifier(field.name)} = ${this.getPlaceholder(values.length + 1)}`);
|
|
703
|
+
const value = rawItem[field.name];
|
|
704
|
+
values.push(value === undefined || value === null ? null : value);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
for (const field of tableContext.complexFields.values()) {
|
|
708
|
+
sets.push(`${this.escapeIdentifier(field.name)} = ${this.getPlaceholder(values.length + 1)}`);
|
|
709
|
+
const value = rawItem[field.name];
|
|
710
|
+
values.push(this.getComplexColumnValue(field, value));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const shiftedWhereSQL = whereSQL && this.shiftPlaceholders ? this.shiftPlaceholders(whereSQL, values.length) : whereSQL;
|
|
714
|
+
if (whereSQL) {
|
|
715
|
+
values.push(...whereParameters);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const sql = `UPDATE ${this.escapeIdentifier(tableContext.tableName)} SET ${sets.join(', ')}${shiftedWhereSQL ? ` WHERE ${shiftedWhereSQL}` : ''};`;
|
|
719
|
+
return { sql, values };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#buildUpdateSets<T extends ModelType>(tableContext: TableContext<T>, preparedData: Partial<T>): { sets: string[]; values: unknown[] } {
|
|
723
|
+
const sets: string[] = [];
|
|
724
|
+
const values: unknown[] = [];
|
|
725
|
+
|
|
726
|
+
for (const [fieldName, value] of Object.entries(preparedData)) {
|
|
727
|
+
const simpleField = tableContext.simpleFields.get(fieldName);
|
|
728
|
+
if (simpleField) {
|
|
729
|
+
sets.push(`${this.escapeIdentifier(fieldName)} = ${this.getPlaceholder(values.length + 1)}`);
|
|
730
|
+
values.push(value === undefined || value === null ? null : value);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const complexField = tableContext.complexFields.get(fieldName);
|
|
735
|
+
if (complexField) {
|
|
736
|
+
sets.push(`${this.escapeIdentifier(fieldName)} = ${this.getPlaceholder(values.length + 1)}`);
|
|
737
|
+
values.push(this.getComplexColumnValue(complexField, value));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return { sets, values };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
compilePartialUpdate<T extends ModelType>(
|
|
745
|
+
tableContext: TableContext<T>,
|
|
746
|
+
preparedData: Partial<T>
|
|
747
|
+
): { sets: string[]; values: unknown[] } {
|
|
748
|
+
return this.#buildUpdateSets(tableContext, preparedData);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
buildPartialUpdate<T extends ModelType>(
|
|
752
|
+
tableContext: TableContext<T>,
|
|
753
|
+
preparedData: Partial<T>,
|
|
754
|
+
whereSQL?: string,
|
|
755
|
+
whereParameters: unknown[] = [],
|
|
756
|
+
returning = false
|
|
757
|
+
): { sql: string; values: unknown[] } {
|
|
758
|
+
const { sets, values } = this.#buildUpdateSets(tableContext, preparedData);
|
|
759
|
+
|
|
760
|
+
const shiftedWhereSQL = whereSQL && this.shiftPlaceholders ? this.shiftPlaceholders(whereSQL, values.length) : whereSQL;
|
|
761
|
+
if (whereSQL) {
|
|
762
|
+
values.push(...whereParameters);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const returningClause = returning && this.returningSupport ? ' RETURNING *' : '';
|
|
766
|
+
const sql = `UPDATE ${this.escapeIdentifier(tableContext.tableName)} SET ${sets.join(', ')}${shiftedWhereSQL ? ` WHERE ${shiftedWhereSQL}` : ''}${returningClause};`;
|
|
767
|
+
|
|
768
|
+
return { sql, values };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
buildUpsert<T extends ModelType>(
|
|
772
|
+
tableContext: TableContext<T>,
|
|
773
|
+
rawItem: Record<string, unknown>,
|
|
774
|
+
conflictTarget: string[]
|
|
775
|
+
): { sql: string; values: unknown[] } {
|
|
776
|
+
const columns: string[] = [];
|
|
777
|
+
const values: unknown[] = [];
|
|
778
|
+
const updates: string[] = [];
|
|
779
|
+
|
|
780
|
+
for (const field of tableContext.simpleFields.values()) {
|
|
781
|
+
columns.push(this.escapeIdentifier(field.name));
|
|
782
|
+
const value = rawItem[field.name];
|
|
783
|
+
values.push(value === undefined || value === null ? null : value);
|
|
784
|
+
if (field.name !== 'id') {
|
|
785
|
+
updates.push(`${this.escapeIdentifier(field.name)} = EXCLUDED.${this.escapeIdentifier(field.name)}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
for (const field of tableContext.complexFields.values()) {
|
|
790
|
+
columns.push(this.escapeIdentifier(field.name));
|
|
791
|
+
const value = rawItem[field.name];
|
|
792
|
+
values.push(this.getComplexColumnValue(field, value));
|
|
793
|
+
updates.push(`${this.escapeIdentifier(field.name)} = EXCLUDED.${this.escapeIdentifier(field.name)}`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const placeholders = columns.map((_, index) => this.getPlaceholder(index + 1));
|
|
797
|
+
const sql = this.getUpsertSQL(tableContext, columns, placeholders, conflictTarget, updates);
|
|
798
|
+
|
|
799
|
+
return { sql, values };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
buildSelect<T extends ModelType>(
|
|
803
|
+
tableContext: TableContext<T>,
|
|
804
|
+
options?: {
|
|
805
|
+
whereSQL?: string;
|
|
806
|
+
sortSQL?: string;
|
|
807
|
+
limit?: number;
|
|
808
|
+
offset?: number | string;
|
|
809
|
+
columns?: string[];
|
|
810
|
+
}
|
|
811
|
+
): string {
|
|
812
|
+
const selectedColumns = options?.columns && options.columns.length > 0 ? options.columns.join(', ') : '*';
|
|
813
|
+
const where = options?.whereSQL ? ` WHERE ${options.whereSQL}` : '';
|
|
814
|
+
const sort = options?.sortSQL ? ` ${options.sortSQL}` : '';
|
|
815
|
+
const limit = options?.limit !== undefined ? ` LIMIT ${options.limit}` : '';
|
|
816
|
+
const offset = options?.offset !== undefined ? ` OFFSET ${options.offset}` : '';
|
|
817
|
+
|
|
818
|
+
return `SELECT ${selectedColumns} FROM ${this.escapeIdentifier(tableContext.tableName)}${where}${sort}${limit}${offset};`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
buildDelete<T extends ModelType>(tableContext: TableContext<T>, whereSQL?: string): string {
|
|
822
|
+
return `DELETE FROM ${this.escapeIdentifier(tableContext.tableName)}${whereSQL ? ` WHERE ${whereSQL}` : ''};`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
buildCount<T extends ModelType>(tableContext: TableContext<T>, whereSQL?: string): string {
|
|
826
|
+
return `SELECT COUNT(*) as ${this.escapeIdentifier('total')} FROM ${this.escapeIdentifier(tableContext.tableName)}${whereSQL ? ` WHERE ${whereSQL}` : ''};`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
buildIndexSort<T extends ModelType>(
|
|
830
|
+
tableContext: TableContext<T>,
|
|
831
|
+
indexConfig: { sortTemplate: { path: string[]; value: number }[] }
|
|
832
|
+
): string {
|
|
833
|
+
const sortClauses = indexConfig.sortTemplate.map(({ path, value }) => {
|
|
834
|
+
const expression = this.compileIndexPath(tableContext, path, 'orderBy');
|
|
835
|
+
return `${expression} ${value === -1 ? 'DESC' : 'ASC'}`;
|
|
836
|
+
});
|
|
837
|
+
return sortClauses.length ? `ORDER BY ${sortClauses.join(', ')}` : '';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
buildFacet<T extends ModelType>(tableContext: TableContext<T>, sqlPath: string, whereSQL?: string): string {
|
|
841
|
+
const keySql = this.castColumn?.(sqlPath, String) ?? sqlPath;
|
|
842
|
+
const countSql = this.castColumn?.('COUNT(*)', Number) ?? 'COUNT(*)';
|
|
843
|
+
const where = whereSQL ? ` AND ${whereSQL}` : '';
|
|
844
|
+
|
|
845
|
+
return `SELECT ${keySql} AS ${this.escapeIdentifier('key')}, ${countSql} AS ${this.escapeIdentifier('count')} FROM ${this.escapeIdentifier(tableContext.tableName)} WHERE ${sqlPath} IS NOT NULL${where} GROUP BY ${sqlPath} ORDER BY ${this.escapeIdentifier('count')} DESC;`;
|
|
846
|
+
}
|
|
847
|
+
}
|