@mikro-orm/sql 7.1.0-dev.3 → 7.1.0-dev.31
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/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +2 -2
- package/AbstractSqlDriver.d.ts +19 -1
- package/AbstractSqlDriver.js +215 -16
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.js +13 -2
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
- package/dialects/mysql/MySqlSchemaHelper.js +145 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +9 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +72 -6
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +4 -4
- package/plugin/transformer.d.ts +11 -3
- package/plugin/transformer.js +138 -29
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +36 -0
- package/query/QueryBuilder.js +63 -1
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +182 -31
- package/schema/SchemaComparator.d.ts +10 -0
- package/schema/SchemaComparator.js +104 -1
- package/schema/SchemaHelper.d.ts +63 -1
- package/schema/SchemaHelper.js +235 -6
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +16 -9
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +34 -2
|
@@ -15,6 +15,7 @@ const SPATIALITE_VIEWS = [
|
|
|
15
15
|
'ElementaryGeometries',
|
|
16
16
|
];
|
|
17
17
|
export class SqliteSchemaHelper extends SchemaHelper {
|
|
18
|
+
static PARTIAL_WHERE_RE = /\swhere\s+(.+?)\s*$/is;
|
|
18
19
|
disableForeignKeysSQL() {
|
|
19
20
|
return 'pragma foreign_keys = off;';
|
|
20
21
|
}
|
|
@@ -30,6 +31,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
30
31
|
getDropNamespaceSQL(name) {
|
|
31
32
|
return '';
|
|
32
33
|
}
|
|
34
|
+
async tableExists(connection, tableName, _schemaName, ctx) {
|
|
35
|
+
const rows = await connection.execute(`select name from sqlite_master where type = 'table' and name = ${this.platform.quoteValue(tableName)}`, [], 'all', ctx);
|
|
36
|
+
return rows.length > 0;
|
|
37
|
+
}
|
|
33
38
|
getListTablesSQL() {
|
|
34
39
|
return (`select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' ` +
|
|
35
40
|
`union all select name as table_name from sqlite_temp_master where type = 'table' order by name`);
|
|
@@ -100,8 +105,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
100
105
|
const checks = await this.getChecks(connection, table.name, table.schema, ctx);
|
|
101
106
|
const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
|
|
102
107
|
const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
|
|
103
|
-
const enums =
|
|
108
|
+
const enums = this.extractEnumValuesFromChecks(checks);
|
|
109
|
+
const triggers = await this.getTableTriggers(connection, table.name);
|
|
104
110
|
table.init(cols, indexes, checks, pks, fks, enums);
|
|
111
|
+
table.setTriggers(triggers);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
createTable(table, alter) {
|
|
@@ -140,6 +147,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
140
147
|
for (const index of table.getIndexes()) {
|
|
141
148
|
this.append(ret, this.createIndex(index, table));
|
|
142
149
|
}
|
|
150
|
+
for (const trigger of table.getTriggers()) {
|
|
151
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
152
|
+
}
|
|
143
153
|
return ret;
|
|
144
154
|
}
|
|
145
155
|
createTableColumn(column, table, _changedProperties) {
|
|
@@ -159,6 +169,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
159
169
|
col.push(`check (${checks[check].expression})`);
|
|
160
170
|
checks.splice(check, 1);
|
|
161
171
|
}
|
|
172
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
162
173
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
163
174
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
|
|
164
175
|
Utils.runIfNotEmpty(() => col.push('primary key'), column.primary);
|
|
@@ -211,10 +222,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
211
222
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
212
223
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
213
224
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
214
|
-
return `${sqlPrefix} (${columns.join(', ')})`;
|
|
225
|
+
return `${sqlPrefix} (${columns.join(', ')})${this.getIndexWhereClause(index)}`;
|
|
215
226
|
}
|
|
216
227
|
// Use getIndexColumns to support advanced options like sort order and collation
|
|
217
|
-
return `${sqlPrefix} (${this.getIndexColumns(index)})`;
|
|
228
|
+
return `${sqlPrefix} (${this.getIndexColumns(index)})${this.getIndexWhereClause(index)}`;
|
|
218
229
|
}
|
|
219
230
|
parseTableDefinition(sql, cols) {
|
|
220
231
|
const columns = {};
|
|
@@ -285,6 +296,17 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
285
296
|
generated = `${match[2]} ${storage}`;
|
|
286
297
|
}
|
|
287
298
|
}
|
|
299
|
+
// Strip string literals first (their contents could contain unbalanced parens), then
|
|
300
|
+
// repeatedly strip the innermost balanced `(...)` until none remain — a single pass would
|
|
301
|
+
// only remove the innermost level, leaving `collate` tokens inside nested CHECK/default
|
|
302
|
+
// expressions exposed to the column-collation regex.
|
|
303
|
+
let cleanDef = (columnDefinitions[col.name]?.definition ?? '').replace(/'[^']*'/g, '').replace(/"[^"]*"/g, '');
|
|
304
|
+
let prev;
|
|
305
|
+
do {
|
|
306
|
+
prev = cleanDef;
|
|
307
|
+
cleanDef = cleanDef.replace(/\([^()]*\)/g, '');
|
|
308
|
+
} while (cleanDef !== prev);
|
|
309
|
+
const collationMatch = /\bcollate\s+([`"']?)([\w\-.]+)\1/i.exec(cleanDef);
|
|
288
310
|
return {
|
|
289
311
|
name: col.name,
|
|
290
312
|
type: col.type,
|
|
@@ -295,6 +317,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
295
317
|
unsigned: false,
|
|
296
318
|
autoincrement: !composite && col.pk && this.platform.isNumericColumn(mappedType) && hasAutoincrement,
|
|
297
319
|
generated,
|
|
320
|
+
collation: collationMatch ? collationMatch[2] : undefined,
|
|
298
321
|
};
|
|
299
322
|
});
|
|
300
323
|
}
|
|
@@ -321,23 +344,23 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
321
344
|
// everything else is an expression that had its outer parens stripped
|
|
322
345
|
return `(${value})`;
|
|
323
346
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// check constraints are defined as (note that last closing paren is missing):
|
|
331
|
-
// `type` text check (`type` in ('local', 'global')
|
|
332
|
-
const match = /[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i.exec(item);
|
|
333
|
-
/* v8 ignore next */
|
|
334
|
-
if (match) {
|
|
335
|
-
o[match[1]] = match[2]
|
|
336
|
-
.split(/,(?=\s*'(?:[^']|'')*'(?:\s*\)|$))/)
|
|
337
|
-
.map((item) => /^\(?'((?:[^']|'')*)'/.exec(item.trim())[1].replace(/''/g, "'"));
|
|
347
|
+
/** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
|
|
348
|
+
extractEnumValuesFromChecks(checks) {
|
|
349
|
+
const result = {};
|
|
350
|
+
for (const check of checks) {
|
|
351
|
+
if (!check.columnName || typeof check.expression !== 'string') {
|
|
352
|
+
continue;
|
|
338
353
|
}
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
const inClause = /\bin\s*\(([^)]*)\)/i.exec(check.expression);
|
|
355
|
+
if (!inClause) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const items = [...inClause[1].matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1].replace(/''/g, "'"));
|
|
359
|
+
if (items.length > 0) {
|
|
360
|
+
result[check.columnName] = items;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
341
364
|
}
|
|
342
365
|
async getPrimaryKeys(connection, indexes, tableName, schemaName, ctx) {
|
|
343
366
|
const prefix = this.getSchemaPrefix(schemaName);
|
|
@@ -350,6 +373,16 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
350
373
|
const sql = `pragma ${prefix}table_info(\`${tableName}\`)`;
|
|
351
374
|
const cols = await connection.execute(sql, [], 'all', ctx);
|
|
352
375
|
const indexes = await connection.execute(`pragma ${prefix}index_list(\`${tableName}\`)`, [], 'all', ctx);
|
|
376
|
+
// sqlite_master.sql holds the original CREATE INDEX statement — the only place a partial
|
|
377
|
+
// index's WHERE predicate is preserved (PRAGMA index_* don't expose it).
|
|
378
|
+
const indexSqls = await connection.execute(`select name, sql from ${prefix}sqlite_master where type = 'index' and tbl_name = ?`, [tableName], 'all', ctx);
|
|
379
|
+
const wherePredicates = new Map();
|
|
380
|
+
for (const row of indexSqls) {
|
|
381
|
+
const match = row.sql && SqliteSchemaHelper.PARTIAL_WHERE_RE.exec(row.sql);
|
|
382
|
+
if (match) {
|
|
383
|
+
wherePredicates.set(row.name, match[1].trim());
|
|
384
|
+
}
|
|
385
|
+
}
|
|
353
386
|
const ret = [];
|
|
354
387
|
for (const col of cols.filter(c => c.pk)) {
|
|
355
388
|
ret.push({
|
|
@@ -362,12 +395,14 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
362
395
|
}
|
|
363
396
|
for (const index of indexes.filter(index => !this.isImplicitIndex(index.name))) {
|
|
364
397
|
const res = await connection.execute(`pragma ${prefix}index_info(\`${index.name}\`)`, [], 'all', ctx);
|
|
398
|
+
const where = wherePredicates.get(index.name);
|
|
365
399
|
ret.push(...res.map(row => ({
|
|
366
400
|
columnNames: [row.name],
|
|
367
401
|
keyName: index.name,
|
|
368
402
|
unique: !!index.unique,
|
|
369
403
|
constraint: !!index.unique,
|
|
370
404
|
primary: false,
|
|
405
|
+
...(where ? { where } : {}),
|
|
371
406
|
})));
|
|
372
407
|
}
|
|
373
408
|
return this.mapIndexes(ret);
|
|
@@ -501,8 +536,102 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
501
536
|
this.append(ret, this.getRenameIndexSQL(diff.name, index, oldIndexName));
|
|
502
537
|
}
|
|
503
538
|
}
|
|
539
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
540
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
541
|
+
}
|
|
542
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
543
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
544
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
545
|
+
}
|
|
546
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
547
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
548
|
+
}
|
|
504
549
|
return ret;
|
|
505
550
|
}
|
|
551
|
+
/** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
|
|
552
|
+
createTrigger(table, trigger) {
|
|
553
|
+
if (trigger.expression) {
|
|
554
|
+
return trigger.expression;
|
|
555
|
+
}
|
|
556
|
+
const timing = trigger.timing.toUpperCase();
|
|
557
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
558
|
+
const ret = [];
|
|
559
|
+
for (const event of trigger.events) {
|
|
560
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
561
|
+
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
562
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`);
|
|
563
|
+
}
|
|
564
|
+
return ret.join(';\n');
|
|
565
|
+
}
|
|
566
|
+
async getTableTriggers(connection, tableName) {
|
|
567
|
+
const rows = await connection.execute(`select name, sql from sqlite_master where type = 'trigger' and tbl_name = ?`, [tableName]);
|
|
568
|
+
// First pass: parse all triggers and collect names to detect multi-event groups
|
|
569
|
+
const parsedRows = [];
|
|
570
|
+
for (const row of rows) {
|
|
571
|
+
/* v8 ignore next 3 */
|
|
572
|
+
if (!row.sql) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const parsed = this.parseTriggerDDL(row.sql, row.name);
|
|
576
|
+
if (parsed) {
|
|
577
|
+
parsedRows.push({ name: row.name, parsed });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const allNames = parsedRows.map(r => r.name);
|
|
581
|
+
const triggers = [];
|
|
582
|
+
const triggerMap = new Map();
|
|
583
|
+
for (const { name, parsed } of parsedRows) {
|
|
584
|
+
// Only strip event suffix when another trigger with the same base exists
|
|
585
|
+
const eventLower = parsed.events[0];
|
|
586
|
+
const candidateBase = name.endsWith(`_${eventLower}`) ? name.slice(0, -eventLower.length - 1) : null;
|
|
587
|
+
const baseName = candidateBase && allNames.some(n => n !== name && n.startsWith(`${candidateBase}_`)) ? candidateBase : name;
|
|
588
|
+
if (triggerMap.has(baseName)) {
|
|
589
|
+
const existing = triggerMap.get(baseName);
|
|
590
|
+
if (!existing.events.includes(parsed.events[0])) {
|
|
591
|
+
existing.events.push(parsed.events[0]);
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const trigger = { ...parsed, name: baseName };
|
|
596
|
+
triggers.push(trigger);
|
|
597
|
+
triggerMap.set(baseName, trigger);
|
|
598
|
+
}
|
|
599
|
+
return triggers;
|
|
600
|
+
}
|
|
601
|
+
parseTriggerDDL(sql, name) {
|
|
602
|
+
// Split at the last top-level BEGIN to separate header from body,
|
|
603
|
+
// so that a WHEN clause containing the word "begin" in a string literal doesn't confuse parsing.
|
|
604
|
+
const beginIdx = sql.search(/\bbegin\b(?=[^]*$)/i);
|
|
605
|
+
/* v8 ignore next 3 */
|
|
606
|
+
if (beginIdx === -1) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
const header = sql.slice(0, beginIdx);
|
|
610
|
+
const bodyPart = sql.slice(beginIdx);
|
|
611
|
+
const headerMatch = /create\s+trigger\s+["`]?\w+["`]?\s+(before|after|instead\s+of)\s+(insert|update|delete)\s+on\s+["`]?\w+["`]?\s*(?:for\s+each\s+(row|statement))?\s*(?:when\s+([\s\S]*?))?\s*$/i.exec(header);
|
|
612
|
+
/* v8 ignore next 3 */
|
|
613
|
+
if (!headerMatch) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const bodyMatch = /^begin\s+([\s\S]*?)\s*end/i.exec(bodyPart);
|
|
617
|
+
/* v8 ignore next 3 */
|
|
618
|
+
if (!bodyMatch) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const timing = headerMatch[1].toLowerCase();
|
|
622
|
+
const event = headerMatch[2].toLowerCase();
|
|
623
|
+
const forEach = (headerMatch[3]?.toLowerCase() ?? 'row');
|
|
624
|
+
const when = headerMatch[4]?.trim() || undefined;
|
|
625
|
+
const body = bodyMatch[1].trim().replace(/;\s*$/, '');
|
|
626
|
+
return {
|
|
627
|
+
name,
|
|
628
|
+
timing,
|
|
629
|
+
events: [event],
|
|
630
|
+
forEach,
|
|
631
|
+
body,
|
|
632
|
+
when,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
506
635
|
getAlterTempTableSQL(changedTable) {
|
|
507
636
|
const tempName = `${changedTable.toTable.name}__temp_alter`;
|
|
508
637
|
const quotedName = this.quote(changedTable.toTable.name);
|
package/index.d.ts
CHANGED
|
@@ -17,3 +17,5 @@ export type * from './typings.js';
|
|
|
17
17
|
export * from './plugin/index.js';
|
|
18
18
|
export { SqlEntityManager as EntityManager } from './SqlEntityManager.js';
|
|
19
19
|
export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js';
|
|
20
|
+
export * from './SqlMikroORM.js';
|
|
21
|
+
export { SqlMikroORM as MikroORM, type SqlOptions as Options, defineSqlConfig as defineConfig } from './SqlMikroORM.js';
|
package/index.js
CHANGED
|
@@ -16,3 +16,5 @@ export * from './dialects/index.js';
|
|
|
16
16
|
export * from './plugin/index.js';
|
|
17
17
|
export { SqlEntityManager as EntityManager } from './SqlEntityManager.js';
|
|
18
18
|
export { SqlEntityRepository as EntityRepository } from './SqlEntityRepository.js';
|
|
19
|
+
export * from './SqlMikroORM.js';
|
|
20
|
+
export { SqlMikroORM as MikroORM, defineSqlConfig as defineConfig } from './SqlMikroORM.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.31",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"copy": "node ../../scripts/copy.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"kysely": "0.
|
|
50
|
+
"kysely": "0.29.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.0.
|
|
53
|
+
"@mikro-orm/core": "^7.0.15"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.31"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/plugin/transformer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type EntityMetadata, type EntityProperty } from '@mikro-orm/core';
|
|
2
|
-
import { type CommonTableExpressionNameNode, type DeleteQueryNode, type
|
|
1
|
+
import { type EntityMetadata, type EntityProperty, type Type } from '@mikro-orm/core';
|
|
2
|
+
import { type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
|
|
3
3
|
import type { MikroKyselyPluginOptions } from './index.js';
|
|
4
4
|
import type { SqlEntityManager } from '../SqlEntityManager.js';
|
|
5
5
|
export declare class MikroTransformer extends OperationNodeTransformer {
|
|
@@ -28,10 +28,18 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
28
28
|
processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
29
29
|
processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
|
|
30
30
|
processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
31
|
+
processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
|
|
32
|
+
expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
|
|
33
|
+
expandSelection(sel: SelectionNode): SelectionNode[] | null;
|
|
34
|
+
expandStar(meta: EntityMetadata | undefined, table: TableNode | undefined, tableName: string | undefined): SelectionNode[] | null;
|
|
35
|
+
wrapRead(customType: Type<any>, fieldName: string, tableName: string | undefined): SelectionNode;
|
|
36
|
+
wrapWrite(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
|
|
37
|
+
/** Resolve the customType for a specific field name (handles composite-PK FK fan-out via prop.customTypes[]). */
|
|
38
|
+
fieldType(prop: EntityProperty, fieldName: string): Type<any> | undefined;
|
|
39
|
+
findOwnerMeta(name: string | undefined): EntityMetadata | undefined;
|
|
31
40
|
mapColumnsToProperties(columns: readonly ColumnNode[], meta: EntityMetadata): (EntityProperty | undefined)[];
|
|
32
41
|
normalizeColumnName(identifier: IdentifierNode): string;
|
|
33
42
|
findProperty(meta: EntityMetadata | undefined, columnName?: string): EntityProperty | undefined;
|
|
34
|
-
shouldConvertValues(): boolean;
|
|
35
43
|
prepareInputValue(prop: EntityProperty | undefined, value: unknown, enabled: boolean): unknown;
|
|
36
44
|
/**
|
|
37
45
|
* Look up a table name/alias in the context stack.
|
package/plugin/transformer.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
2
|
-
import { AliasNode, ColumnNode, ColumnUpdateNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, SchemableIdentifierNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
|
|
2
|
+
import { AliasNode, ColumnNode, ColumnUpdateNode, IdentifierNode, OperationNodeTransformer, PrimitiveValueListNode, RawNode, ReferenceNode, SchemableIdentifierNode, SelectAllNode, SelectionNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
|
|
3
|
+
const EXPANDABLE_KINDS = new Set([
|
|
4
|
+
ReferenceKind.SCALAR,
|
|
5
|
+
ReferenceKind.EMBEDDED,
|
|
6
|
+
ReferenceKind.MANY_TO_ONE,
|
|
7
|
+
ReferenceKind.ONE_TO_ONE,
|
|
8
|
+
]);
|
|
3
9
|
export class MikroTransformer extends OperationNodeTransformer {
|
|
4
10
|
/**
|
|
5
11
|
* Context stack to support nested queries (subqueries, CTEs)
|
|
@@ -64,7 +70,14 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
64
70
|
this.processJoinNode(join, currentContext);
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
const transformed = super.transformSelectQuery(node, queryId);
|
|
74
|
+
if (this.#options.convertValues && transformed.selections?.length) {
|
|
75
|
+
const selections = this.expandSelections(transformed.selections);
|
|
76
|
+
if (selections !== transformed.selections) {
|
|
77
|
+
return { ...transformed, selections };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return transformed;
|
|
68
81
|
}
|
|
69
82
|
finally {
|
|
70
83
|
// Pop the context when exiting this query scope
|
|
@@ -365,32 +378,38 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
365
378
|
return node;
|
|
366
379
|
}
|
|
367
380
|
const columnProps = this.mapColumnsToProperties(node.columns, meta);
|
|
368
|
-
const
|
|
381
|
+
const fieldNames = node.columns.map(c => this.normalizeColumnName(c.column));
|
|
382
|
+
// hasConvertToDatabaseValueSQL is set by MetadataDiscovery only when the SQL is non-trivial
|
|
383
|
+
// (i.e. it actually wraps `?`), so a no-op cast on sqlite won't force a row upgrade.
|
|
384
|
+
const needsSqlWrap = columnProps.some(p => p?.hasConvertToDatabaseValueSQL);
|
|
369
385
|
let changed = false;
|
|
370
386
|
const convertedRows = node.values.values.map(row => {
|
|
371
|
-
if (ValueListNode.is(row)) {
|
|
372
|
-
if (row.values.length !== columnProps.length) {
|
|
373
|
-
return row;
|
|
374
|
-
}
|
|
387
|
+
if (ValueListNode.is(row) && row.values.length === columnProps.length) {
|
|
375
388
|
const values = row.values.map((valueNode, idx) => {
|
|
376
389
|
if (!ValueNode.is(valueNode)) {
|
|
377
390
|
return valueNode;
|
|
378
391
|
}
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
392
|
+
const newNode = this.processInputValueNode(columnProps[idx], fieldNames[idx], valueNode);
|
|
393
|
+
if (newNode !== valueNode) {
|
|
394
|
+
changed = true;
|
|
382
395
|
}
|
|
383
|
-
|
|
384
|
-
return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
|
|
396
|
+
return newNode;
|
|
385
397
|
});
|
|
386
398
|
return ValueListNode.create(values);
|
|
387
399
|
}
|
|
388
|
-
if (PrimitiveValueListNode.is(row)) {
|
|
389
|
-
|
|
390
|
-
|
|
400
|
+
if (PrimitiveValueListNode.is(row) && row.values.length === columnProps.length) {
|
|
401
|
+
// upgrade to ValueListNode when any column needs SQL-side wrapping, since
|
|
402
|
+
// PrimitiveValueListNode can only hold primitives
|
|
403
|
+
if (needsSqlWrap) {
|
|
404
|
+
changed = true;
|
|
405
|
+
return ValueListNode.create(row.values.map((value, idx) => {
|
|
406
|
+
const prop = columnProps[idx];
|
|
407
|
+
const converted = this.prepareInputValue(prop, value, true);
|
|
408
|
+
return this.wrapWrite(prop, fieldNames[idx], ValueNode.create(converted));
|
|
409
|
+
}));
|
|
391
410
|
}
|
|
392
411
|
const values = row.values.map((value, idx) => {
|
|
393
|
-
const converted = this.prepareInputValue(columnProps[idx], value,
|
|
412
|
+
const converted = this.prepareInputValue(columnProps[idx], value, true);
|
|
394
413
|
if (converted !== value) {
|
|
395
414
|
changed = true;
|
|
396
415
|
}
|
|
@@ -412,7 +431,6 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
412
431
|
if (!node.updates?.length) {
|
|
413
432
|
return node;
|
|
414
433
|
}
|
|
415
|
-
const shouldConvert = this.shouldConvertValues();
|
|
416
434
|
let changed = false;
|
|
417
435
|
const updates = node.updates.map(updateNode => {
|
|
418
436
|
if (!ValueNode.is(updateNode.value)) {
|
|
@@ -422,18 +440,12 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
422
440
|
? this.normalizeColumnName(updateNode.column.column)
|
|
423
441
|
: undefined;
|
|
424
442
|
const property = this.findProperty(meta, columnName);
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
443
|
+
const newValue = this.processInputValueNode(property, columnName, updateNode.value);
|
|
444
|
+
if (newValue === updateNode.value) {
|
|
427
445
|
return updateNode;
|
|
428
446
|
}
|
|
429
447
|
changed = true;
|
|
430
|
-
|
|
431
|
-
? ValueNode.createImmediate(converted)
|
|
432
|
-
: ValueNode.create(converted);
|
|
433
|
-
return {
|
|
434
|
-
...updateNode,
|
|
435
|
-
value: newValueNode,
|
|
436
|
-
};
|
|
448
|
+
return { ...updateNode, value: newValue };
|
|
437
449
|
});
|
|
438
450
|
if (!changed) {
|
|
439
451
|
return node;
|
|
@@ -443,6 +455,106 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
443
455
|
updates,
|
|
444
456
|
};
|
|
445
457
|
}
|
|
458
|
+
processInputValueNode(prop, fieldName, valueNode) {
|
|
459
|
+
const converted = this.prepareInputValue(prop, valueNode.value, true);
|
|
460
|
+
const newValueNode = converted === valueNode.value
|
|
461
|
+
? valueNode
|
|
462
|
+
: valueNode.immediate
|
|
463
|
+
? ValueNode.createImmediate(converted)
|
|
464
|
+
: ValueNode.create(converted);
|
|
465
|
+
return this.wrapWrite(prop, fieldName, newValueNode);
|
|
466
|
+
}
|
|
467
|
+
expandSelections(selections) {
|
|
468
|
+
const out = [];
|
|
469
|
+
let changed = false;
|
|
470
|
+
for (const sel of selections) {
|
|
471
|
+
const replaced = this.expandSelection(sel);
|
|
472
|
+
if (replaced) {
|
|
473
|
+
out.push(...replaced);
|
|
474
|
+
changed = true;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
out.push(sel);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return changed ? out : selections;
|
|
481
|
+
}
|
|
482
|
+
expandSelection(sel) {
|
|
483
|
+
const inner = sel.selection;
|
|
484
|
+
if (SelectAllNode.is(inner)) {
|
|
485
|
+
return this.expandStar(this.findOwnerMeta(undefined), undefined, undefined);
|
|
486
|
+
}
|
|
487
|
+
if (!ReferenceNode.is(inner)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const table = inner.table;
|
|
491
|
+
const tableName = table ? this.getTableName(table) : undefined;
|
|
492
|
+
const meta = this.findOwnerMeta(tableName);
|
|
493
|
+
if (!meta) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
if (SelectAllNode.is(inner.column)) {
|
|
497
|
+
return this.expandStar(meta, table, tableName);
|
|
498
|
+
}
|
|
499
|
+
const fieldName = inner.column.column.name;
|
|
500
|
+
const prop = this.findProperty(meta, fieldName);
|
|
501
|
+
const ct = prop && this.fieldType(prop, fieldName);
|
|
502
|
+
return ct?.convertToJSValueSQL ? [this.wrapRead(ct, fieldName, tableName)] : null;
|
|
503
|
+
}
|
|
504
|
+
expandStar(meta, table, tableName) {
|
|
505
|
+
if (!meta || !meta.props.some(p => p.hasConvertToJSValueSQL)) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const out = [];
|
|
509
|
+
for (const prop of meta.props) {
|
|
510
|
+
if (prop.persist === false || !prop.fieldNames?.length || !EXPANDABLE_KINDS.has(prop.kind)) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
for (const fieldName of prop.fieldNames) {
|
|
514
|
+
const ct = this.fieldType(prop, fieldName);
|
|
515
|
+
out.push(ct?.convertToJSValueSQL
|
|
516
|
+
? this.wrapRead(ct, fieldName, tableName)
|
|
517
|
+
: SelectionNode.create(table ? ReferenceNode.create(ColumnNode.create(fieldName), table) : ColumnNode.create(fieldName)));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
wrapRead(customType, fieldName, tableName) {
|
|
523
|
+
const key = this.#platform.quoteIdentifier(tableName ? `${tableName}.${fieldName}` : fieldName);
|
|
524
|
+
const sql = customType.convertToJSValueSQL(key, this.#platform);
|
|
525
|
+
return SelectionNode.create(AliasNode.create(RawNode.createWithSql(sql), IdentifierNode.create(fieldName)));
|
|
526
|
+
}
|
|
527
|
+
wrapWrite(prop, fieldName, valueNode) {
|
|
528
|
+
if (!prop?.hasConvertToDatabaseValueSQL || !fieldName || valueNode.value == null || isRaw(valueNode.value)) {
|
|
529
|
+
return valueNode;
|
|
530
|
+
}
|
|
531
|
+
const customType = this.fieldType(prop, fieldName);
|
|
532
|
+
if (!customType?.convertToDatabaseValueSQL) {
|
|
533
|
+
return valueNode;
|
|
534
|
+
}
|
|
535
|
+
const fragments = customType.convertToDatabaseValueSQL('?', this.#platform).split('?');
|
|
536
|
+
return RawNode.create(fragments, fragments.slice(0, -1).map(() => valueNode));
|
|
537
|
+
}
|
|
538
|
+
/** Resolve the customType for a specific field name (handles composite-PK FK fan-out via prop.customTypes[]). */
|
|
539
|
+
fieldType(prop, fieldName) {
|
|
540
|
+
return prop.customType ?? prop.customTypes?.[prop.fieldNames.indexOf(fieldName)];
|
|
541
|
+
}
|
|
542
|
+
findOwnerMeta(name) {
|
|
543
|
+
if (name) {
|
|
544
|
+
return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
|
|
545
|
+
}
|
|
546
|
+
let single;
|
|
547
|
+
for (const meta of this.#contextStack[this.#contextStack.length - 1].values()) {
|
|
548
|
+
if (!meta) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (single && single !== meta) {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
single = meta;
|
|
555
|
+
}
|
|
556
|
+
return single;
|
|
557
|
+
}
|
|
446
558
|
mapColumnsToProperties(columns, meta) {
|
|
447
559
|
return columns.map(column => {
|
|
448
560
|
const columnName = this.normalizeColumnName(column.column);
|
|
@@ -466,9 +578,6 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
466
578
|
}
|
|
467
579
|
return meta.props.find(prop => prop.fieldNames?.includes(columnName));
|
|
468
580
|
}
|
|
469
|
-
shouldConvertValues() {
|
|
470
|
-
return !!this.#options.convertValues;
|
|
471
|
-
}
|
|
472
581
|
prepareInputValue(prop, value, enabled) {
|
|
473
582
|
if (!enabled || !prop || value == null) {
|
|
474
583
|
return value;
|
package/query/CriteriaNode.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
|
|
|
21
21
|
shouldInline(payload: any): boolean;
|
|
22
22
|
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
|
|
23
23
|
shouldRename(payload: any): boolean;
|
|
24
|
-
renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string): string;
|
|
24
|
+
renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string, options?: ICriteriaNodeProcessOptions): string;
|
|
25
25
|
getPath(opts?: {
|
|
26
26
|
addIndex?: boolean;
|
|
27
27
|
parentPath?: string;
|
package/query/CriteriaNode.js
CHANGED
|
@@ -83,8 +83,8 @@ export class CriteriaNode {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
renameFieldToPK(qb, ownerAlias) {
|
|
87
|
-
const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
|
|
86
|
+
renameFieldToPK(qb, ownerAlias, options) {
|
|
87
|
+
const joinAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins: true });
|
|
88
88
|
if (!joinAlias &&
|
|
89
89
|
this.parent &&
|
|
90
90
|
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) &&
|
|
@@ -105,7 +105,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
105
105
|
this.inlineChildPayload(o, payload, field, a, childAlias);
|
|
106
106
|
}
|
|
107
107
|
else if (childNode.shouldRename(payload)) {
|
|
108
|
-
this.inlineCondition(childNode.renameFieldToPK(qb, alias), o, payload);
|
|
108
|
+
this.inlineCondition(childNode.renameFieldToPK(qb, alias, options), o, payload);
|
|
109
109
|
}
|
|
110
110
|
else if (isRawField) {
|
|
111
111
|
const rawField = RawQueryFragment.getKnownFragment(field);
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export interface ExecuteOptions {
|
|
|
11
11
|
mergeResults?: boolean;
|
|
12
12
|
}
|
|
13
13
|
export interface QBStreamOptions {
|
|
14
|
+
/**
|
|
15
|
+
* How many rows to fetch in one round-trip.
|
|
16
|
+
* Lower values will result in more queries and network bandwidth, but less memory usage.
|
|
17
|
+
* Higher values will result in fewer queries and network bandwidth, but higher memory usage.
|
|
18
|
+
* Note that the results are iterated one row at a time regardless of this value.
|
|
19
|
+
*
|
|
20
|
+
* Honored on PostgreSQL (cursor-based fetch), MSSQL (tedious stream chunk size)
|
|
21
|
+
* and Oracle (mapped to `fetchArraySize`). Ignored on MySQL, MariaDB, SQLite and
|
|
22
|
+
* libSQL, where the underlying driver already streams row-by-row with no batching
|
|
23
|
+
* knob.
|
|
24
|
+
*
|
|
25
|
+
* @default 100 on dialects that honor it.
|
|
26
|
+
*/
|
|
27
|
+
chunkSize?: number;
|
|
14
28
|
/**
|
|
15
29
|
* Results are mapped to entities, if you set `mapResults: false` you will get POJOs instead.
|
|
16
30
|
*
|
|
@@ -193,6 +207,11 @@ export interface QBState<Entity extends object> {
|
|
|
193
207
|
})[];
|
|
194
208
|
tptJoinsApplied: boolean;
|
|
195
209
|
autoJoinedPaths: string[];
|
|
210
|
+
partitionLimit?: {
|
|
211
|
+
partitionBy: string;
|
|
212
|
+
limit: number;
|
|
213
|
+
offset?: number;
|
|
214
|
+
};
|
|
196
215
|
}
|
|
197
216
|
/**
|
|
198
217
|
* SQL query builder with fluent interface.
|
|
@@ -675,6 +694,12 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
675
694
|
setFlag(flag: QueryFlag): this;
|
|
676
695
|
unsetFlag(flag: QueryFlag): this;
|
|
677
696
|
hasFlag(flag: QueryFlag): boolean;
|
|
697
|
+
/** @internal */
|
|
698
|
+
setPartitionLimit(opts: {
|
|
699
|
+
partitionBy: string;
|
|
700
|
+
limit: number;
|
|
701
|
+
offset?: number;
|
|
702
|
+
}): this;
|
|
678
703
|
cache(config?: boolean | number | [string, number]): this;
|
|
679
704
|
/**
|
|
680
705
|
* Adds index hint to the FROM clause.
|
|
@@ -952,6 +977,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
952
977
|
private processNestedJoins;
|
|
953
978
|
private hasToManyJoins;
|
|
954
979
|
protected wrapPaginateSubQuery(meta: EntityMetadata): void;
|
|
980
|
+
/**
|
|
981
|
+
* Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
|
|
982
|
+
* that filters by the __rn column to apply per-parent limiting.
|
|
983
|
+
*/
|
|
984
|
+
protected wrapPartitionLimitSubQuery(innerQb: NativeQueryBuilder): NativeQueryBuilder;
|
|
985
|
+
/**
|
|
986
|
+
* Adds ROW_NUMBER() OVER (PARTITION BY ...) to the SELECT list and prepares
|
|
987
|
+
* the query state for per-parent limiting. The actual wrapping into a subquery
|
|
988
|
+
* with __rn filtering happens in getNativeQuery().
|
|
989
|
+
*/
|
|
990
|
+
protected preparePartitionLimit(): void;
|
|
955
991
|
/**
|
|
956
992
|
* Computes the set of populate paths from the _populate hints.
|
|
957
993
|
*/
|