@mikro-orm/sql 7.0.0-rc.2 → 7.0.0

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 (66) hide show
  1. package/AbstractSqlConnection.d.ts +5 -4
  2. package/AbstractSqlConnection.js +20 -6
  3. package/AbstractSqlDriver.d.ts +19 -13
  4. package/AbstractSqlDriver.js +225 -47
  5. package/AbstractSqlPlatform.d.ts +35 -0
  6. package/AbstractSqlPlatform.js +51 -5
  7. package/PivotCollectionPersister.d.ts +2 -11
  8. package/PivotCollectionPersister.js +59 -59
  9. package/README.md +5 -4
  10. package/SqlEntityManager.d.ts +2 -2
  11. package/SqlEntityManager.js +5 -5
  12. package/dialects/index.d.ts +1 -0
  13. package/dialects/index.js +1 -0
  14. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  15. package/dialects/mssql/MsSqlNativeQueryBuilder.js +8 -4
  16. package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
  17. package/dialects/mysql/BaseMySqlPlatform.js +18 -2
  18. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
  19. package/dialects/mysql/MySqlSchemaHelper.js +25 -14
  20. package/dialects/oracledb/OracleDialect.d.ts +78 -0
  21. package/dialects/oracledb/OracleDialect.js +166 -0
  22. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +19 -0
  23. package/dialects/oracledb/OracleNativeQueryBuilder.js +249 -0
  24. package/dialects/oracledb/index.d.ts +2 -0
  25. package/dialects/oracledb/index.js +2 -0
  26. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
  27. package/dialects/postgresql/BasePostgreSqlPlatform.js +49 -37
  28. package/dialects/postgresql/PostgreSqlSchemaHelper.js +75 -59
  29. package/dialects/sqlite/BaseSqliteConnection.js +2 -2
  30. package/dialects/sqlite/NodeSqliteDialect.js +3 -1
  31. package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
  32. package/dialects/sqlite/SqlitePlatform.js +7 -1
  33. package/dialects/sqlite/SqliteSchemaHelper.js +23 -17
  34. package/index.d.ts +1 -1
  35. package/index.js +0 -1
  36. package/package.json +30 -30
  37. package/plugin/index.d.ts +1 -14
  38. package/plugin/index.js +13 -13
  39. package/plugin/transformer.d.ts +6 -22
  40. package/plugin/transformer.js +91 -82
  41. package/query/ArrayCriteriaNode.d.ts +1 -1
  42. package/query/CriteriaNode.js +28 -10
  43. package/query/CriteriaNodeFactory.js +20 -4
  44. package/query/NativeQueryBuilder.d.ts +28 -3
  45. package/query/NativeQueryBuilder.js +65 -3
  46. package/query/ObjectCriteriaNode.js +75 -31
  47. package/query/QueryBuilder.d.ts +199 -100
  48. package/query/QueryBuilder.js +544 -358
  49. package/query/QueryBuilderHelper.d.ts +18 -14
  50. package/query/QueryBuilderHelper.js +364 -147
  51. package/query/ScalarCriteriaNode.js +17 -8
  52. package/query/enums.d.ts +2 -0
  53. package/query/enums.js +2 -0
  54. package/query/raw.js +1 -1
  55. package/schema/DatabaseSchema.d.ts +7 -5
  56. package/schema/DatabaseSchema.js +68 -45
  57. package/schema/DatabaseTable.d.ts +8 -6
  58. package/schema/DatabaseTable.js +191 -107
  59. package/schema/SchemaComparator.d.ts +1 -3
  60. package/schema/SchemaComparator.js +76 -50
  61. package/schema/SchemaHelper.d.ts +2 -13
  62. package/schema/SchemaHelper.js +30 -9
  63. package/schema/SqlSchemaGenerator.d.ts +4 -14
  64. package/schema/SqlSchemaGenerator.js +26 -12
  65. package/typings.d.ts +10 -5
  66. package/tsconfig.build.tsbuildinfo +0 -1
@@ -3,6 +3,7 @@ import { SqlEntityRepository } from './SqlEntityRepository.js';
3
3
  import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
4
4
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
5
5
  export class AbstractSqlPlatform extends Platform {
6
+ static #JSON_PROPERTY_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6
7
  schemaHelper;
7
8
  usesPivotTable() {
8
9
  return true;
@@ -64,15 +65,21 @@ export class AbstractSqlPlatform extends Platform {
64
65
  }
65
66
  getSearchJsonPropertyKey(path, type, aliased, value) {
66
67
  const [a, ...b] = path;
67
- const quoteKey = (key) => key.match(/^[a-z]\w*$/i) ? key : `"${key}"`;
68
68
  if (aliased) {
69
- return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(quoteKey).join('.')}')`);
69
+ return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
70
70
  }
71
- return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(quoteKey).join('.')}')`);
71
+ return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
72
+ }
73
+ /**
74
+ * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
75
+ * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
76
+ * @internal
77
+ */
78
+ quoteJsonKey(key) {
79
+ return /^[a-z]\w*$/i.exec(key) ? key : `"${key}"`;
72
80
  }
73
81
  getJsonIndexDefinition(index) {
74
- return index.columnNames
75
- .map(column => {
82
+ return index.columnNames.map(column => {
76
83
  if (!column.includes('.')) {
77
84
  return column;
78
85
  }
@@ -80,6 +87,9 @@ export class AbstractSqlPlatform extends Platform {
80
87
  return `(json_extract(${root}, '$.${path.join('.')}'))`;
81
88
  });
82
89
  }
90
+ supportsUnionWhere() {
91
+ return true;
92
+ }
83
93
  supportsSchemas() {
84
94
  return false;
85
95
  }
@@ -114,4 +124,40 @@ export class AbstractSqlPlatform extends Platform {
114
124
  throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
115
125
  }
116
126
  }
127
+ /** @internal */
128
+ validateJsonPropertyName(name) {
129
+ if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
130
+ throw new Error(`Invalid JSON property name: '${name}'. JSON property names must contain only alphanumeric characters and underscores.`);
131
+ }
132
+ }
133
+ /**
134
+ * Returns FROM clause for JSON array iteration.
135
+ * @internal
136
+ */
137
+ getJsonArrayFromSQL(column, alias, _properties) {
138
+ return `json_each(${column}) as ${this.quoteIdentifier(alias)}`;
139
+ }
140
+ /**
141
+ * Returns SQL expression to access an element's property within a JSON array iteration.
142
+ * @internal
143
+ */
144
+ getJsonArrayElementPropertySQL(alias, property, _type) {
145
+ return `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(property)}`;
146
+ }
147
+ /**
148
+ * Wraps JSON array FROM clause and WHERE condition into a full EXISTS condition.
149
+ * MySQL overrides this because `json_table` doesn't support correlated subqueries.
150
+ * @internal
151
+ */
152
+ getJsonArrayExistsSQL(from, where) {
153
+ return `exists (select 1 from ${from} where ${where})`;
154
+ }
155
+ /**
156
+ * Maps a runtime type name (e.g. 'string', 'number') to a driver-specific bind type constant.
157
+ * Used by NativeQueryBuilder for output bindings.
158
+ * @internal
159
+ */
160
+ mapToBindType(type) {
161
+ return type;
162
+ }
117
163
  }
@@ -1,17 +1,8 @@
1
1
  import { type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
2
2
  import { type AbstractSqlDriver } from './AbstractSqlDriver.js';
3
3
  export declare class PivotCollectionPersister<Entity extends object> {
4
- private readonly meta;
5
- private readonly driver;
6
- private readonly ctx?;
7
- private readonly schema?;
8
- private readonly loggerContext?;
9
- private readonly inserts;
10
- private readonly upserts;
11
- private readonly deletes;
12
- private readonly batchSize;
13
- private order;
14
- constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction | undefined, schema?: string | undefined, loggerContext?: Dictionary | undefined);
4
+ #private;
5
+ constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction, schema?: string, loggerContext?: Dictionary);
15
6
  enqueueUpdate(prop: EntityProperty<Entity>, insertDiff: Primary<Entity>[][], deleteDiff: Primary<Entity>[][] | boolean, pks: Primary<Entity>[], isInitialized?: boolean): void;
16
7
  private enqueueInsert;
17
8
  private enqueueUpsert;
@@ -1,55 +1,55 @@
1
1
  class InsertStatement {
2
- keys;
3
- data;
4
2
  order;
3
+ #keys;
4
+ #data;
5
5
  constructor(keys, data, order) {
6
- this.keys = keys;
7
- this.data = data;
8
6
  this.order = order;
7
+ this.#keys = keys;
8
+ this.#data = data;
9
9
  }
10
10
  getHash() {
11
- return JSON.stringify(this.data);
11
+ return JSON.stringify(this.#data);
12
12
  }
13
13
  getData() {
14
14
  const data = {};
15
- this.keys.forEach((key, idx) => data[key] = this.data[idx]);
15
+ this.#keys.forEach((key, idx) => (data[key] = this.#data[idx]));
16
16
  return data;
17
17
  }
18
18
  }
19
19
  class DeleteStatement {
20
- keys;
21
- cond;
20
+ #keys;
21
+ #cond;
22
22
  constructor(keys, cond) {
23
- this.keys = keys;
24
- this.cond = cond;
23
+ this.#keys = keys;
24
+ this.#cond = cond;
25
25
  }
26
26
  getHash() {
27
- return JSON.stringify(this.cond);
27
+ return JSON.stringify(this.#cond);
28
28
  }
29
29
  getCondition() {
30
30
  const cond = {};
31
- this.keys.forEach((key, idx) => cond[key] = this.cond[idx]);
31
+ this.#keys.forEach((key, idx) => (cond[key] = this.#cond[idx]));
32
32
  return cond;
33
33
  }
34
34
  }
35
35
  export class PivotCollectionPersister {
36
- meta;
37
- driver;
38
- ctx;
39
- schema;
40
- loggerContext;
41
- inserts = new Map();
42
- upserts = new Map();
43
- deletes = new Map();
44
- batchSize;
45
- order = 0;
36
+ #inserts = new Map();
37
+ #upserts = new Map();
38
+ #deletes = new Map();
39
+ #batchSize;
40
+ #order = 0;
41
+ #meta;
42
+ #driver;
43
+ #ctx;
44
+ #schema;
45
+ #loggerContext;
46
46
  constructor(meta, driver, ctx, schema, loggerContext) {
47
- this.meta = meta;
48
- this.driver = driver;
49
- this.ctx = ctx;
50
- this.schema = schema;
51
- this.loggerContext = loggerContext;
52
- this.batchSize = this.driver.config.get('batchSize');
47
+ this.#meta = meta;
48
+ this.#driver = driver;
49
+ this.#ctx = ctx;
50
+ this.#schema = schema;
51
+ this.#loggerContext = loggerContext;
52
+ this.#batchSize = this.#driver.config.get('batchSize');
53
53
  }
54
54
  enqueueUpdate(prop, insertDiff, deleteDiff, pks, isInitialized = true) {
55
55
  if (insertDiff.length) {
@@ -68,8 +68,8 @@ export class PivotCollectionPersister {
68
68
  for (const fks of insertDiff) {
69
69
  const statement = this.createInsertStatement(prop, fks, pks);
70
70
  const hash = statement.getHash();
71
- if (prop.owner || !this.inserts.has(hash)) {
72
- this.inserts.set(hash, statement);
71
+ if (prop.owner || !this.#inserts.has(hash)) {
72
+ this.#inserts.set(hash, statement);
73
73
  }
74
74
  }
75
75
  }
@@ -77,26 +77,26 @@ export class PivotCollectionPersister {
77
77
  for (const fks of insertDiff) {
78
78
  const statement = this.createInsertStatement(prop, fks, pks);
79
79
  const hash = statement.getHash();
80
- if (prop.owner || !this.upserts.has(hash)) {
81
- this.upserts.set(hash, statement);
80
+ if (prop.owner || !this.#upserts.has(hash)) {
81
+ this.#upserts.set(hash, statement);
82
82
  }
83
83
  }
84
84
  }
85
85
  createInsertStatement(prop, fks, pks) {
86
86
  const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
87
- return new InsertStatement(keys, data, this.order++);
87
+ return new InsertStatement(keys, data, this.#order++);
88
88
  }
89
89
  enqueueDelete(prop, deleteDiff, pks) {
90
90
  if (deleteDiff === true) {
91
91
  const { data, keys } = this.buildPivotKeysAndData(prop, [], pks, true);
92
92
  const statement = new DeleteStatement(keys, data);
93
- this.deletes.set(statement.getHash(), statement);
93
+ this.#deletes.set(statement.getHash(), statement);
94
94
  return;
95
95
  }
96
96
  for (const fks of deleteDiff) {
97
97
  const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
98
98
  const statement = new DeleteStatement(keys, data);
99
- this.deletes.set(statement.getHash(), statement);
99
+ this.#deletes.set(statement.getHash(), statement);
100
100
  }
101
101
  }
102
102
  /**
@@ -130,46 +130,46 @@ export class PivotCollectionPersister {
130
130
  return items.filter(Boolean);
131
131
  }
132
132
  async execute() {
133
- if (this.deletes.size > 0) {
134
- const deletes = [...this.deletes.values()];
135
- for (let i = 0; i < deletes.length; i += this.batchSize) {
136
- const chunk = deletes.slice(i, i + this.batchSize);
133
+ if (this.#deletes.size > 0) {
134
+ const deletes = [...this.#deletes.values()];
135
+ for (let i = 0; i < deletes.length; i += this.#batchSize) {
136
+ const chunk = deletes.slice(i, i + this.#batchSize);
137
137
  const cond = { $or: [] };
138
138
  for (const item of chunk) {
139
139
  cond.$or.push(item.getCondition());
140
140
  }
141
- await this.driver.nativeDelete(this.meta.class, cond, {
142
- ctx: this.ctx,
143
- schema: this.schema,
144
- loggerContext: this.loggerContext,
141
+ await this.#driver.nativeDelete(this.#meta.class, cond, {
142
+ ctx: this.#ctx,
143
+ schema: this.#schema,
144
+ loggerContext: this.#loggerContext,
145
145
  });
146
146
  }
147
147
  }
148
- if (this.inserts.size > 0) {
149
- const filtered = this.collectStatements(this.inserts);
150
- for (let i = 0; i < filtered.length; i += this.batchSize) {
151
- const chunk = filtered.slice(i, i + this.batchSize);
152
- await this.driver.nativeInsertMany(this.meta.class, chunk, {
153
- ctx: this.ctx,
154
- schema: this.schema,
148
+ if (this.#inserts.size > 0) {
149
+ const filtered = this.collectStatements(this.#inserts);
150
+ for (let i = 0; i < filtered.length; i += this.#batchSize) {
151
+ const chunk = filtered.slice(i, i + this.#batchSize);
152
+ await this.#driver.nativeInsertMany(this.#meta.class, chunk, {
153
+ ctx: this.#ctx,
154
+ schema: this.#schema,
155
155
  convertCustomTypes: false,
156
156
  processCollections: false,
157
- loggerContext: this.loggerContext,
157
+ loggerContext: this.#loggerContext,
158
158
  });
159
159
  }
160
160
  }
161
- if (this.upserts.size > 0) {
162
- const filtered = this.collectStatements(this.upserts);
163
- for (let i = 0; i < filtered.length; i += this.batchSize) {
164
- const chunk = filtered.slice(i, i + this.batchSize);
165
- await this.driver.nativeUpdateMany(this.meta.class, [], chunk, {
166
- ctx: this.ctx,
167
- schema: this.schema,
161
+ if (this.#upserts.size > 0) {
162
+ const filtered = this.collectStatements(this.#upserts);
163
+ for (let i = 0; i < filtered.length; i += this.#batchSize) {
164
+ const chunk = filtered.slice(i, i + this.#batchSize);
165
+ await this.#driver.nativeUpdateMany(this.#meta.class, [], chunk, {
166
+ ctx: this.#ctx,
167
+ schema: this.#schema,
168
168
  convertCustomTypes: false,
169
169
  processCollections: false,
170
170
  upsert: true,
171
171
  onConflictAction: 'ignore',
172
- loggerContext: this.loggerContext,
172
+ loggerContext: this.#loggerContext,
173
173
  });
174
174
  }
175
175
  }
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite (including libSQL) databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
9
- [![NPM version](https://img.shields.io/npm/v/@mikro-orm/core.svg)](https://www.npmjs.com/package/@mikro-orm/core)
10
- [![NPM dev version](https://img.shields.io/npm/v/@mikro-orm/core/next.svg)](https://www.npmjs.com/package/@mikro-orm/core)
9
+ [![NPM version](https://img.shields.io/npm/v/@mikro-orm/core.svg)](https://npmx.dev/package/@mikro-orm/core)
10
+ [![NPM dev version](https://img.shields.io/npm/v/@mikro-orm/core/next.svg)](https://npmx.dev/package/@mikro-orm/core)
11
11
  [![Chat on discord](https://img.shields.io/discord/1214904142443839538?label=discord&color=blue)](https://discord.gg/w8bjxFHS7X)
12
- [![Downloads](https://img.shields.io/npm/dm/@mikro-orm/core.svg)](https://www.npmjs.com/package/@mikro-orm/core)
12
+ [![Downloads](https://img.shields.io/npm/dm/@mikro-orm/core.svg)](https://npmx.dev/package/@mikro-orm/core)
13
13
  [![Coverage Status](https://img.shields.io/coveralls/mikro-orm/mikro-orm.svg)](https://coveralls.io/r/mikro-orm/mikro-orm?branch=master)
14
14
  [![Build Status](https://github.com/mikro-orm/mikro-orm/workflows/tests/badge.svg?branch=master)](https://github.com/mikro-orm/mikro-orm/actions?workflow=tests)
15
15
 
@@ -181,6 +181,7 @@ yarn add @mikro-orm/core @mikro-orm/mysql # for mysql/mariadb
181
181
  yarn add @mikro-orm/core @mikro-orm/mariadb # for mysql/mariadb
182
182
  yarn add @mikro-orm/core @mikro-orm/postgresql # for postgresql
183
183
  yarn add @mikro-orm/core @mikro-orm/mssql # for mssql
184
+ yarn add @mikro-orm/core @mikro-orm/oracledb # for oracle
184
185
  yarn add @mikro-orm/core @mikro-orm/sqlite # for sqlite
185
186
  yarn add @mikro-orm/core @mikro-orm/libsql # for libsql
186
187
  ```
@@ -20,7 +20,7 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
20
20
  /**
21
21
  * Shortcut for `createQueryBuilder()`
22
22
  */
23
- qb<Entity extends object, RootAlias extends string = never>(entityName: EntityName<Entity>, alias?: RootAlias, type?: ConnectionType, loggerContext?: LoggingOptions): QueryBuilder<Entity, RootAlias, never, never, never, "*">;
23
+ qb<Entity extends object, RootAlias extends string = never>(entityName: EntityName<Entity>, alias?: RootAlias, type?: ConnectionType, loggerContext?: LoggingOptions): QueryBuilder<Entity, RootAlias>;
24
24
  /**
25
25
  * Returns configured Kysely instance.
26
26
  */
@@ -29,6 +29,6 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
29
29
  getRepository<T extends object, U extends EntityRepository<T> = SqlEntityRepository<T>>(entityName: EntityName<T>): GetRepository<T, U>;
30
30
  protected applyDiscriminatorCondition<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>): FilterQuery<Entity>;
31
31
  }
32
- type EntitiesFromManager<TEntityManager extends EntityManager<any>> = NonNullable<TEntityManager['~entities']> extends any[] ? (Extract<NonNullable<TEntityManager['~entities']>[number], EntitySchemaWithMeta>) : never;
32
+ type EntitiesFromManager<TEntityManager extends EntityManager<any>> = NonNullable<TEntityManager['~entities']> extends any[] ? Extract<NonNullable<TEntityManager['~entities']>[number], EntitySchemaWithMeta> : never;
33
33
  type AllEntitiesFromManager<TEntityManager extends EntityManager<any>> = NonNullable<TEntityManager['~entities']> extends any[] ? NonNullable<TEntityManager['~entities']>[number] : never;
34
34
  export {};
@@ -22,11 +22,11 @@ export class SqlEntityManager extends EntityManager {
22
22
  */
23
23
  getKysely(options = {}) {
24
24
  let kysely = this.getConnection(options.type).getClient();
25
- if (options.columnNamingStrategy != null
26
- || options.tableNamingStrategy != null
27
- || options.processOnCreateHooks != null
28
- || options.processOnUpdateHooks != null
29
- || options.convertValues != null) {
25
+ if (options.columnNamingStrategy != null ||
26
+ options.tableNamingStrategy != null ||
27
+ options.processOnCreateHooks != null ||
28
+ options.processOnUpdateHooks != null ||
29
+ options.convertValues != null) {
30
30
  kysely = kysely.withPlugin(new MikroKyselyPlugin(this, options));
31
31
  }
32
32
  return kysely;
@@ -2,3 +2,4 @@ export * from './mssql/index.js';
2
2
  export * from './mysql/index.js';
3
3
  export * from './postgresql/index.js';
4
4
  export * from './sqlite/index.js';
5
+ export * from './oracledb/index.js';
package/dialects/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './mssql/index.js';
2
2
  export * from './mysql/index.js';
3
3
  export * from './postgresql/index.js';
4
4
  export * from './sqlite/index.js';
5
+ export * from './oracledb/index.js';
@@ -11,4 +11,6 @@ export declare class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
11
11
  protected compileSelect(): void;
12
12
  protected addLockClause(): void;
13
13
  protected compileTruncate(): void;
14
+ /** MSSQL has no RECURSIVE keyword — CTEs are implicitly recursive. */
15
+ protected getCteKeyword(_hasRecursive: boolean): string;
14
16
  }
@@ -19,6 +19,7 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
19
19
  if (this.options.comment) {
20
20
  this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`));
21
21
  }
22
+ this.compileCtes();
22
23
  if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) {
23
24
  this.compileUpsert();
24
25
  }
@@ -77,9 +78,7 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
77
78
  return { prefix: '', suffix: '' };
78
79
  }
79
80
  const returningFields = this.options.returning;
80
- const selections = returningFields
81
- .map(field => `[t].${this.platform.quoteIdentifier(field)}`)
82
- .join(',');
81
+ const selections = returningFields.map(field => `[t].${this.platform.quoteIdentifier(field)}`).join(',');
83
82
  return {
84
83
  prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`,
85
84
  suffix: `select ${selections} from #out as t; drop table #out`,
@@ -181,7 +180,8 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
181
180
  }
182
181
  }
183
182
  addLockClause() {
184
- if (!this.options.lockMode || ![LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(this.options.lockMode)) {
183
+ if (!this.options.lockMode ||
184
+ ![LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(this.options.lockMode)) {
185
185
  return;
186
186
  }
187
187
  const map = {
@@ -197,4 +197,8 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
197
197
  const sql = `delete from ${tableName}; declare @count int = case @@rowcount when 0 then 1 else 0 end; dbcc checkident ('${tableName.replace(/[[\]]/g, '')}', reseed, @count)`;
198
198
  this.parts.push(sql);
199
199
  }
200
+ /** MSSQL has no RECURSIVE keyword — CTEs are implicitly recursive. */
201
+ getCteKeyword(_hasRecursive) {
202
+ return 'with';
203
+ }
200
204
  }
@@ -5,6 +5,7 @@ import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
5
5
  import type { IndexDef } from '../../typings.js';
6
6
  import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js';
7
7
  export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
8
+ #private;
8
9
  protected readonly schemaHelper: MySqlSchemaHelper;
9
10
  protected readonly exceptionConverter: MySqlExceptionConverter;
10
11
  protected readonly ORDER_BY_NULLS_TRANSLATE: {
@@ -42,5 +43,10 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
42
43
  getFullTextWhereClause(): string;
43
44
  getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string;
44
45
  getOrderByExpression(column: string, direction: string, collation?: string): string[];
46
+ getJsonArrayFromSQL(column: string, alias: string, properties: {
47
+ name: string;
48
+ type: string;
49
+ }[]): string;
50
+ getJsonArrayExistsSQL(from: string, where: string): string;
45
51
  getDefaultClientUrl(): string;
46
52
  }
@@ -6,6 +6,12 @@ import { MySqlNativeQueryBuilder } from './MySqlNativeQueryBuilder.js';
6
6
  export class BaseMySqlPlatform extends AbstractSqlPlatform {
7
7
  schemaHelper = new MySqlSchemaHelper(this);
8
8
  exceptionConverter = new MySqlExceptionConverter();
9
+ #jsonTypeCasts = {
10
+ string: 'text',
11
+ number: 'double',
12
+ bigint: 'bigint',
13
+ boolean: 'unsigned',
14
+ };
9
15
  ORDER_BY_NULLS_TRANSLATE = {
10
16
  [QueryOrder.asc_nulls_first]: 'is not null',
11
17
  [QueryOrder.asc_nulls_last]: 'is null',
@@ -44,8 +50,7 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
44
50
  return JSON.stringify(value);
45
51
  }
46
52
  getJsonIndexDefinition(index) {
47
- return index.columnNames
48
- .map(column => {
53
+ return index.columnNames.map(column => {
49
54
  if (!column.includes('.')) {
50
55
  return column;
51
56
  }
@@ -115,6 +120,17 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
115
120
  ret.push(`${col} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`);
116
121
  return ret;
117
122
  }
123
+ getJsonArrayFromSQL(column, alias, properties) {
124
+ const columns = properties
125
+ .map(p => `${this.quoteIdentifier(p.name)} ${this.#jsonTypeCasts[p.type] ?? 'text'} path '$.${this.quoteJsonKey(p.name)}'`)
126
+ .join(', ');
127
+ return `json_table(${column}, '$[*]' columns (${columns})) as ${this.quoteIdentifier(alias)}`;
128
+ }
129
+ // MySQL does not support correlated json_table inside EXISTS subqueries,
130
+ // so we use a semi-join via the comma-join pattern instead.
131
+ getJsonArrayExistsSQL(from, where) {
132
+ return `(select 1 from ${from} where ${where} limit 1) is not null`;
133
+ }
118
134
  getDefaultClientUrl() {
119
135
  return 'mysql://root@127.0.0.1:3306';
120
136
  }
@@ -5,7 +5,7 @@ import { SchemaHelper } from '../../schema/SchemaHelper.js';
5
5
  import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
6
6
  import type { DatabaseTable } from '../../schema/DatabaseTable.js';
7
7
  export declare class MySqlSchemaHelper extends SchemaHelper {
8
- private readonly _cache;
8
+ #private;
9
9
  static readonly DEFAULT_VALUES: {
10
10
  'now()': string[];
11
11
  'current_timestamp(?)': string[];
@@ -1,7 +1,7 @@
1
1
  import { EnumType, StringType, TextType } from '@mikro-orm/core';
2
2
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
3
3
  export class MySqlSchemaHelper extends SchemaHelper {
4
- _cache = {};
4
+ #cache = {};
5
5
  static DEFAULT_VALUES = {
6
6
  'now()': ['now()', 'current_timestamp'],
7
7
  'current_timestamp(?)': ['current_timestamp(?)'],
@@ -46,7 +46,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
46
46
  const createView = await connection.execute(`show create view \`${view.view_name}\``);
47
47
  if (createView[0]?.['Create View']) {
48
48
  // Extract SELECT statement from CREATE VIEW ... AS SELECT ...
49
- const match = createView[0]['Create View'].match(/\bAS\s+(.+)$/is);
49
+ const match = /\bAS\s+(.+)$/is.exec(createView[0]['Create View']);
50
50
  definition = match?.[1]?.trim();
51
51
  }
52
52
  }
@@ -89,11 +89,13 @@ export class MySqlSchemaHelper extends SchemaHelper {
89
89
  };
90
90
  // Capture column options (prefix length, sort order)
91
91
  if (index.sub_part != null || index.sort_order === 'D') {
92
- indexDef.columns = [{
92
+ indexDef.columns = [
93
+ {
93
94
  name: index.column_name,
94
95
  ...(index.sub_part != null && { length: index.sub_part }),
95
96
  ...(index.sort_order === 'D' && { sort: 'DESC' }),
96
- }];
97
+ },
98
+ ];
97
99
  }
98
100
  // Capture index type for fulltext and spatial indexes
99
101
  if (index.index_type === 'FULLTEXT') {
@@ -149,7 +151,8 @@ export class MySqlSchemaHelper extends SchemaHelper {
149
151
  */
150
152
  getIndexColumns(index) {
151
153
  if (index.columns?.length) {
152
- return index.columns.map(col => {
154
+ return index.columns
155
+ .map(col => {
153
156
  const quotedName = this.quote(col.name);
154
157
  // MySQL supports collation via expression: (column_name COLLATE collation_name)
155
158
  // When collation is specified, wrap in parentheses as an expression
@@ -173,7 +176,8 @@ export class MySqlSchemaHelper extends SchemaHelper {
173
176
  colDef += ` ${col.sort}`;
174
177
  }
175
178
  return colDef;
176
- }).join(', ');
179
+ })
180
+ .join(', ');
177
181
  }
178
182
  return index.columnNames.map(c => this.quote(c)).join(', ');
179
183
  }
@@ -205,20 +209,24 @@ export class MySqlSchemaHelper extends SchemaHelper {
205
209
  from information_schema.columns where table_schema = database() and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))})
206
210
  order by ordinal_position`;
207
211
  const allColumns = await connection.execute(sql);
208
- const str = (val) => val != null ? '' + val : val;
212
+ const str = (val) => (val != null ? '' + val : val);
209
213
  const extra = (val) => val.replace(/auto_increment|default_generated|(stored|virtual) generated/i, '').trim() || undefined;
210
214
  const ret = {};
211
215
  for (const col of allColumns) {
212
216
  const mappedType = this.platform.getMappedType(col.column_type);
213
- const defaultValue = str(this.normalizeDefaultValue((mappedType.compareAsType() === 'boolean' && ['0', '1'].includes(col.column_default))
217
+ const defaultValue = str(this.normalizeDefaultValue(mappedType.compareAsType() === 'boolean' && ['0', '1'].includes(col.column_default)
214
218
  ? ['false', 'true'][+col.column_default]
215
219
  : col.column_default, col.length));
216
220
  const key = this.getTableKey(col);
217
- const generated = col.generation_expression ? `(${col.generation_expression.replaceAll(`\\'`, `'`)}) ${col.extra.match(/stored generated/i) ? 'stored' : 'virtual'}` : undefined;
221
+ const generated = col.generation_expression
222
+ ? `(${col.generation_expression.replaceAll(`\\'`, `'`)}) ${col.extra.match(/stored generated/i) ? 'stored' : 'virtual'}`
223
+ : undefined;
218
224
  ret[key] ??= [];
219
225
  ret[key].push({
220
226
  name: col.column_name,
221
- type: this.platform.isNumericColumn(mappedType) ? col.column_type.replace(/ unsigned$/, '').replace(/\(\d+\)$/, '') : col.column_type,
227
+ type: this.platform.isNumericColumn(mappedType)
228
+ ? col.column_type.replace(/ unsigned$/, '').replace(/\(\d+\)$/, '')
229
+ : col.column_type,
222
230
  mappedType,
223
231
  unsigned: col.column_type.endsWith(' unsigned'),
224
232
  length: col.length,
@@ -328,17 +336,20 @@ export class MySqlSchemaHelper extends SchemaHelper {
328
336
  const enums = await connection.execute(sql);
329
337
  return enums.reduce((o, item) => {
330
338
  o[item.table_name] ??= {};
331
- o[item.table_name][item.column_name] = item.column_type.match(/enum\((.*)\)/)[1].split(',').map((item) => item.match(/'(.*)'/)[1]);
339
+ o[item.table_name][item.column_name] = item.column_type
340
+ .match(/enum\((.*)\)/)[1]
341
+ .split(',')
342
+ .map((item) => /'(.*)'/.exec(item)[1]);
332
343
  return o;
333
344
  }, {});
334
345
  }
335
346
  async supportsCheckConstraints(connection) {
336
- if (this._cache.supportsCheckConstraints != null) {
337
- return this._cache.supportsCheckConstraints;
347
+ if (this.#cache.supportsCheckConstraints != null) {
348
+ return this.#cache.supportsCheckConstraints;
338
349
  }
339
350
  const sql = `select 1 from information_schema.tables where table_name = 'CHECK_CONSTRAINTS' and table_schema = 'information_schema'`;
340
351
  const res = await connection.execute(sql);
341
- return this._cache.supportsCheckConstraints = res.length > 0;
352
+ return (this.#cache.supportsCheckConstraints = res.length > 0);
342
353
  }
343
354
  getChecksSQL(tables) {
344
355
  return `select cc.constraint_schema as table_schema, tc.table_name as table_name, cc.constraint_name as name, cc.check_clause as expression