@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.
Files changed (54) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +2 -2
  3. package/AbstractSqlDriver.d.ts +19 -1
  4. package/AbstractSqlDriver.js +215 -16
  5. package/AbstractSqlPlatform.d.ts +15 -3
  6. package/AbstractSqlPlatform.js +25 -7
  7. package/PivotCollectionPersister.js +13 -2
  8. package/README.md +2 -1
  9. package/SqlEntityManager.d.ts +5 -1
  10. package/SqlEntityManager.js +36 -1
  11. package/SqlMikroORM.d.ts +23 -0
  12. package/SqlMikroORM.js +23 -0
  13. package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
  14. package/dialects/mysql/BaseMySqlPlatform.js +3 -0
  15. package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
  16. package/dialects/mysql/MySqlSchemaHelper.js +145 -21
  17. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  18. package/dialects/oracledb/OracleDialect.js +2 -1
  19. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  20. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  21. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +9 -0
  22. package/dialects/postgresql/BasePostgreSqlPlatform.js +72 -6
  23. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
  24. package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
  25. package/dialects/postgresql/index.d.ts +2 -0
  26. package/dialects/postgresql/index.js +2 -0
  27. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  28. package/dialects/postgresql/typeOverrides.js +12 -0
  29. package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
  30. package/dialects/sqlite/SqlitePlatform.js +4 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
  32. package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
  33. package/index.d.ts +2 -0
  34. package/index.js +2 -0
  35. package/package.json +4 -4
  36. package/plugin/transformer.d.ts +11 -3
  37. package/plugin/transformer.js +138 -29
  38. package/query/CriteriaNode.d.ts +1 -1
  39. package/query/CriteriaNode.js +2 -2
  40. package/query/ObjectCriteriaNode.js +1 -1
  41. package/query/QueryBuilder.d.ts +36 -0
  42. package/query/QueryBuilder.js +63 -1
  43. package/schema/DatabaseSchema.js +26 -4
  44. package/schema/DatabaseTable.d.ts +20 -1
  45. package/schema/DatabaseTable.js +182 -31
  46. package/schema/SchemaComparator.d.ts +10 -0
  47. package/schema/SchemaComparator.js +104 -1
  48. package/schema/SchemaHelper.d.ts +63 -1
  49. package/schema/SchemaHelper.js +235 -6
  50. package/schema/SqlSchemaGenerator.d.ts +2 -2
  51. package/schema/SqlSchemaGenerator.js +16 -9
  52. package/schema/partitioning.d.ts +13 -0
  53. package/schema/partitioning.js +326 -0
  54. 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 = await this.getEnumDefinitions(connection, table.name, table.schema, ctx);
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
- async getEnumDefinitions(connection, tableName, schemaName, ctx) {
325
- const prefix = this.getSchemaPrefix(schemaName);
326
- const sql = `select sql from ${prefix}sqlite_master where type = ? and name = ?`;
327
- const tableDefinition = await connection.execute(sql, ['table', tableName], 'get', ctx);
328
- const checkConstraints = [...(tableDefinition.sql.match(/[`["'][^`\]"']+[`\]"'] text check \(.*?\)/gi) ?? [])];
329
- return checkConstraints.reduce((o, item) => {
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
- return o;
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",
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.28.16"
50
+ "kysely": "0.29.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.0.11"
53
+ "@mikro-orm/core": "^7.0.15"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.3"
56
+ "@mikro-orm/core": "7.1.0-dev.31"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -1,5 +1,5 @@
1
- import { type EntityMetadata, type EntityProperty } from '@mikro-orm/core';
2
- import { type CommonTableExpressionNameNode, type DeleteQueryNode, type IdentifierNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, OperationNodeTransformer, TableNode } from 'kysely';
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.
@@ -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
- return super.transformSelectQuery(node, queryId);
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 shouldConvert = this.shouldConvertValues();
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 converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
380
- if (converted === valueNode.value) {
381
- return valueNode;
392
+ const newNode = this.processInputValueNode(columnProps[idx], fieldNames[idx], valueNode);
393
+ if (newNode !== valueNode) {
394
+ changed = true;
382
395
  }
383
- changed = true;
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
- if (row.values.length !== columnProps.length) {
390
- return row;
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, shouldConvert);
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 converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
426
- if (converted === updateNode.value.value) {
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
- const newValueNode = updateNode.value.immediate
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;
@@ -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;
@@ -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);
@@ -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
  */