169 lines
5.2 KiB
JavaScript
169 lines
5.2 KiB
JavaScript
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;
|
|
}
|
|
}
|