import { ALIAS_REPLACEMENT, ARRAY_OPERATORS, raw, RawQueryFragment, Type, Utils } from '@mikro-orm/core'; import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js'; import { PostgreSqlNativeQueryBuilder } from './PostgreSqlNativeQueryBuilder.js'; import { PostgreSqlSchemaHelper } from './PostgreSqlSchemaHelper.js'; import { PostgreSqlExceptionConverter } from './PostgreSqlExceptionConverter.js'; import { FullTextType } from './FullTextType.js'; export class BasePostgreSqlPlatform extends AbstractSqlPlatform { schemaHelper = new PostgreSqlSchemaHelper(this); exceptionConverter = new PostgreSqlExceptionConverter(); /** Maps JS runtime type names to PostgreSQL cast types for JSON property access. @internal */ #jsonTypeCasts = { number: 'float8', bigint: 'int8', boolean: 'bool' }; createNativeQueryBuilder() { return new PostgreSqlNativeQueryBuilder(this); } usesReturningStatement() { return true; } usesCascadeStatement() { return true; } supportsNativeEnums() { return true; } usesEnumCheckConstraints() { return true; } supportsMaterializedViews() { return true; } supportsCustomPrimaryKeyNames() { return true; } getCurrentTimestampSQL(length) { return `current_timestamp(${length})`; } getDateTimeTypeDeclarationSQL(column) { /* v8 ignore next */ return 'timestamptz' + (column.length != null ? `(${column.length})` : ''); } getDefaultDateTimeLength() { return 6; } getTimeTypeDeclarationSQL() { return 'time(0)'; } getIntegerTypeDeclarationSQL(column) { if (column.autoincrement && !column.generated) { return 'serial'; } return 'int'; } getBigIntTypeDeclarationSQL(column) { /* v8 ignore next */ if (column.autoincrement) { return `bigserial`; } return 'bigint'; } getTinyIntTypeDeclarationSQL(column) { return 'smallint'; } getUuidTypeDeclarationSQL(column) { return `uuid`; } getFullTextWhereClause(prop) { if (prop.customType instanceof FullTextType) { return `:column: @@ plainto_tsquery('${prop.customType.regconfig}', :query)`; } /* v8 ignore next */ if (prop.columnTypes[0] === 'tsvector') { return `:column: @@ plainto_tsquery('simple', :query)`; } return `to_tsvector('simple', :column:) @@ plainto_tsquery('simple', :query)`; } supportsCreatingFullTextIndex() { return true; } getFullTextIndexExpression(indexName, schemaName, tableName, columns) { /* v8 ignore next */ const quotedTableName = this.quoteIdentifier(schemaName ? `${schemaName}.${tableName}` : tableName); const quotedColumnNames = columns.map(c => this.quoteIdentifier(c.name)); const quotedIndexName = this.quoteIdentifier(indexName); if (columns.length === 1 && columns[0].type === 'tsvector') { return `create index ${quotedIndexName} on ${quotedTableName} using gin(${quotedColumnNames[0]})`; } return `create index ${quotedIndexName} on ${quotedTableName} using gin(to_tsvector('simple', ${quotedColumnNames.join(` || ' ' || `)}))`; } normalizeColumnType(type, options) { const simpleType = this.extractSimpleType(type); if (['int', 'int4', 'integer'].includes(simpleType)) { return this.getIntegerTypeDeclarationSQL({}); } if (['bigint', 'int8'].includes(simpleType)) { return this.getBigIntTypeDeclarationSQL({}); } if (['smallint', 'int2'].includes(simpleType)) { return this.getSmallIntTypeDeclarationSQL({}); } if (['boolean', 'bool'].includes(simpleType)) { return this.getBooleanTypeDeclarationSQL(); } if (['varchar', 'character varying'].includes(simpleType)) { return this.getVarcharTypeDeclarationSQL(options); } if (['char', 'bpchar'].includes(simpleType)) { return this.getCharTypeDeclarationSQL(options); } if (['decimal', 'numeric'].includes(simpleType)) { return this.getDecimalTypeDeclarationSQL(options); } if (['interval'].includes(simpleType)) { return this.getIntervalTypeDeclarationSQL(options); } return super.normalizeColumnType(type, options); } getMappedType(type) { switch (this.extractSimpleType(type)) { case 'tsvector': return Type.getType(FullTextType); default: return super.getMappedType(type); } } getRegExpOperator(val, flags) { /* v8 ignore next */ if ((val instanceof RegExp && val.flags.includes('i')) || flags?.includes('i')) { return '~*'; } return '~'; } /* v8 ignore next */ getRegExpValue(val) { if (val.flags.includes('i')) { return { $re: val.source, $flags: val.flags }; } return { $re: val.source }; } isBigIntProperty(prop) { return super.isBigIntProperty(prop) || ['bigserial', 'int8'].includes(prop.columnTypes?.[0]); } getArrayDeclarationSQL() { return 'text[]'; } getFloatDeclarationSQL() { return 'real'; } getDoubleDeclarationSQL() { return 'double precision'; } getEnumTypeDeclarationSQL(column) { /* v8 ignore next */ if (column.nativeEnumName) { return column.nativeEnumName; } if (column.items?.every(item => typeof item === 'string')) { return 'text'; } return `smallint`; } supportsMultipleStatements() { return true; } getBeginTransactionSQL(options) { if (options?.isolationLevel || options?.readOnly) { let sql = 'start transaction'; sql += options.isolationLevel ? ` isolation level ${options.isolationLevel}` : ''; sql += options.readOnly ? ` read only` : ''; return [sql]; } return ['begin']; } marshallArray(values) { const quote = v => (v === '' || /["{},\\]/.exec(v) ? JSON.stringify(v) : v); return `{${values.map(v => quote('' + v)).join(',')}}`; } /* v8 ignore next */ unmarshallArray(value) { if (value === '{}') { return []; } return value .substring(1, value.length - 1) .split(',') .map(v => { if (v === `""`) { return ''; } if (/"(.*)"/.exec(v)) { return v.substring(1, v.length - 1).replaceAll('\\"', '"'); } return v; }); } getVarcharTypeDeclarationSQL(column) { if (column.length === -1) { return 'varchar'; } return super.getVarcharTypeDeclarationSQL(column); } getCharTypeDeclarationSQL(column) { if (column.length === -1) { return 'char'; } return super.getCharTypeDeclarationSQL(column); } getIntervalTypeDeclarationSQL(column) { return 'interval' + (column.length != null ? `(${column.length})` : ''); } getBlobDeclarationSQL() { return 'bytea'; } getJsonDeclarationSQL() { return 'jsonb'; } getSearchJsonPropertyKey(path, type, aliased, value) { const first = path.shift(); const last = path.pop(); const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${first}` : first); type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type); const cast = key => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key); let lastOperator = '->>'; // force `->` for operator payloads with array values if ( Utils.isPlainObject(value) && Object.keys(value).every(key => ARRAY_OPERATORS.includes(key) && Array.isArray(value[key])) ) { lastOperator = '->'; } if (path.length === 0) { return cast(`${root}${lastOperator}'${last}'`); } return cast(`${root}->${path.map(a => this.quoteValue(a)).join('->')}${lastOperator}'${last}'`); } getJsonIndexDefinition(index) { return index.columnNames.map(column => { if (!column.includes('.')) { return column; } const path = column.split('.'); const first = path.shift(); const last = path.pop(); if (path.length === 0) { return `(${this.quoteIdentifier(first)}->>${this.quoteValue(last)})`; } return `(${this.quoteIdentifier(first)}->${path.map(c => this.quoteValue(c)).join('->')}->>${this.quoteValue(last)})`; }); } quoteIdentifier(id, quote = '"') { if (RawQueryFragment.isKnownFragment(id)) { return super.quoteIdentifier(id); } return `${quote}${id.toString().replace('.', `${quote}.${quote}`)}${quote}`; } pad(number, digits) { return String(number).padStart(digits, '0'); } /** @internal */ formatDate(date) { if (this.timezone === 'Z') { return date.toISOString(); } let offset = -date.getTimezoneOffset(); let year = date.getFullYear(); const isBCYear = year < 1; /* v8 ignore next */ if (isBCYear) { year = Math.abs(year) + 1; } const datePart = `${this.pad(year, 4)}-${this.pad(date.getMonth() + 1, 2)}-${this.pad(date.getDate(), 2)}`; const timePart = `${this.pad(date.getHours(), 2)}:${this.pad(date.getMinutes(), 2)}:${this.pad(date.getSeconds(), 2)}.${this.pad(date.getMilliseconds(), 3)}`; let ret = `${datePart}T${timePart}`; /* v8 ignore next */ if (offset < 0) { ret += '-'; offset *= -1; } else { ret += '+'; } ret += this.pad(Math.floor(offset / 60), 2) + ':' + this.pad(offset % 60, 2); /* v8 ignore next */ if (isBCYear) { ret += ' BC'; } return ret; } indexForeignKeys() { return false; } getDefaultMappedType(type) { const normalizedType = this.extractSimpleType(type); const map = { int2: 'smallint', smallserial: 'smallint', int: 'integer', int4: 'integer', serial: 'integer', serial4: 'integer', int8: 'bigint', bigserial: 'bigint', serial8: 'bigint', numeric: 'decimal', bool: 'boolean', real: 'float', float4: 'float', float8: 'double', timestamp: 'datetime', timestamptz: 'datetime', bytea: 'blob', jsonb: 'json', 'character varying': 'varchar', bpchar: 'character', }; return super.getDefaultMappedType(map[normalizedType] ?? type); } supportsSchemas() { return true; } getDefaultSchemaName() { return 'public'; } /** * Returns the default name of index for the given columns * cannot go past 63 character length for identifiers in MySQL */ getIndexName(tableName, columns, type) { const indexName = super.getIndexName(tableName, columns, type); if (indexName.length > 63) { const suffix = type === 'primary' ? 'pkey' : type; return `${indexName.substring(0, 55 - type.length)}_${Utils.hash(indexName, 5)}_${suffix}`; } return indexName; } getDefaultPrimaryName(tableName, columns) { const indexName = `${tableName}_pkey`; if (indexName.length > 63) { return `${indexName.substring(0, 55 - 'pkey'.length)}_${Utils.hash(indexName, 5)}_pkey`; } return indexName; } /** * @inheritDoc */ castColumn(prop) { switch (prop?.columnTypes?.[0]) { case this.getUuidTypeDeclarationSQL({}): return '::text'; case this.getBooleanTypeDeclarationSQL(): return '::int'; default: return ''; } } getJsonArrayFromSQL(column, alias, _properties) { return `jsonb_array_elements(${column}) as ${this.quoteIdentifier(alias)}`; } getJsonArrayElementPropertySQL(alias, property, type) { const expr = `${this.quoteIdentifier(alias)}->>${this.quoteValue(property)}`; return type in this.#jsonTypeCasts ? `(${expr})::${this.#jsonTypeCasts[type]}` : expr; } getDefaultClientUrl() { return 'postgresql://postgres@127.0.0.1:5432'; } }