import { isRaw, JsonProperty, Platform, raw, Utils } from '@mikro-orm/core'; import { SqlEntityRepository } from './SqlEntityRepository.js'; import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js'; import { NativeQueryBuilder } from './query/NativeQueryBuilder.js'; /** Base class for SQL database platforms, providing SQL generation and quoting utilities. */ export class AbstractSqlPlatform extends Platform { static #JSON_PROPERTY_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/; schemaHelper; usesPivotTable() { return true; } indexForeignKeys() { return true; } getRepositoryClass() { return SqlEntityRepository; } getSchemaHelper() { return this.schemaHelper; } /** @inheritDoc */ lookupExtensions(orm) { SqlSchemaGenerator.register(orm); } /* v8 ignore next: kept for type inference only */ getSchemaGenerator(driver, em) { return new SqlSchemaGenerator(em ?? driver); } /** @internal */ /* v8 ignore next */ createNativeQueryBuilder() { return new NativeQueryBuilder(this); } getBeginTransactionSQL(options) { if (options?.isolationLevel) { return [`set transaction isolation level ${options.isolationLevel}`, 'begin']; } return ['begin']; } getCommitTransactionSQL() { return 'commit'; } getRollbackTransactionSQL() { return 'rollback'; } getSavepointSQL(savepointName) { return `savepoint ${this.quoteIdentifier(savepointName)}`; } getRollbackToSavepointSQL(savepointName) { return `rollback to savepoint ${this.quoteIdentifier(savepointName)}`; } getReleaseSavepointSQL(savepointName) { return `release savepoint ${this.quoteIdentifier(savepointName)}`; } quoteValue(value) { if (isRaw(value)) { return this.formatQuery(value.sql, value.params); } if (Utils.isPlainObject(value) || value?.[JsonProperty]) { return this.escape(JSON.stringify(value)); } return this.escape(value); } getSearchJsonPropertySQL(path, type, aliased) { return this.getSearchJsonPropertyKey(path.split('->'), type, aliased); } getSearchJsonPropertyKey(path, type, aliased, value) { const [a, ...b] = path; if (aliased) { return raw( alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(this.quoteJsonKey).join('.')}')`, ); } return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(this.quoteJsonKey).join('.')}')`); } /** * Quotes a key for use inside a JSON path expression (e.g. `$.key`). * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes. * @internal */ quoteJsonKey(key) { return /^[a-z]\w*$/i.exec(key) ? key : `"${key}"`; } getJsonIndexDefinition(index) { return index.columnNames.map(column => { if (!column.includes('.')) { return column; } const [root, ...path] = column.split('.'); return `(json_extract(${root}, '$.${path.join('.')}'))`; }); } supportsUnionWhere() { return true; } supportsSchemas() { return false; } /** @inheritDoc */ generateCustomOrder(escapedColumn, values) { let ret = '(case '; values.forEach((v, i) => { ret += `when ${escapedColumn} = ${this.quoteValue(v)} then ${i} `; }); return ret + 'else null end)'; } /** * @internal */ getOrderByExpression(column, direction, collation) { if (collation) { return [`${column} collate ${this.quoteCollation(collation)} ${direction.toLowerCase()}`]; } return [`${column} ${direction.toLowerCase()}`]; } /** * Quotes a collation name for use in COLLATE clauses. * @internal */ quoteCollation(collation) { this.validateCollationName(collation); return this.quoteIdentifier(collation); } /** @internal */ validateCollationName(collation) { if (!/^[\w]+$/.test(collation)) { throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`); } } /** @internal */ validateJsonPropertyName(name) { if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) { throw new Error( `Invalid JSON property name: '${name}'. JSON property names must contain only alphanumeric characters and underscores.`, ); } } /** * Returns FROM clause for JSON array iteration. * @internal */ getJsonArrayFromSQL(column, alias, _properties) { return `json_each(${column}) as ${this.quoteIdentifier(alias)}`; } /** * Returns SQL expression to access an element's property within a JSON array iteration. * @internal */ getJsonArrayElementPropertySQL(alias, property, _type) { return `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(property)}`; } /** * Wraps JSON array FROM clause and WHERE condition into a full EXISTS condition. * MySQL overrides this because `json_table` doesn't support correlated subqueries. * @internal */ getJsonArrayExistsSQL(from, where) { return `exists (select 1 from ${from} where ${where})`; } /** * Maps a runtime type name (e.g. 'string', 'number') to a driver-specific bind type constant. * Used by NativeQueryBuilder for output bindings. * @internal */ mapToBindType(type) { return type; } }