Skip to content

Commit

Permalink
feat(sql): support per-field name remapping
Browse files Browse the repository at this point in the history
```typescript
class User {
   group: Group & Reference & DatabaseField<{name: 'group_id'}>
}
```
  • Loading branch information
marcj committed Mar 2, 2024
1 parent 69c56eb commit 63b8aaf
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 94 deletions.
19 changes: 13 additions & 6 deletions packages/mysql/src/mysql-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ import {
UniqueConstraintFailure,
} from '@deepkit/orm';
import { MySQLPlatform } from './mysql-platform.js';
import { Changes, getPatchSerializeFunction, getSerializeFunction, ReceiveType, ReflectionClass, resolvePath } from '@deepkit/type';
import {
Changes,
getPatchSerializeFunction,
getSerializeFunction,
ReceiveType,
ReflectionClass,
resolvePath,
} from '@deepkit/type';
import { AbstractClassType, asyncOperation, ClassType, empty, isArray } from '@deepkit/core';
import { FrameCategory, Stopwatch } from '@deepkit/stopwatch';

Expand Down Expand Up @@ -269,7 +276,7 @@ export class MySQLPersistence extends SQLPersistence {
const valuesNames: string[] = [];
const valuesSetNames: string[] = [];
for (const fieldName of prepared.changedFields) {
valuesNames.push(fieldName);
valuesNames.push(entity.fieldMap[fieldName].columnNameEscaped);
valuesSetNames.push('_changed_' + fieldName);
}

Expand All @@ -292,7 +299,7 @@ export class MySQLPersistence extends SQLPersistence {
}

for (const i of prepared.changedFields) {
const col = this.platform.quoteIdentifier(i);
const col = entity.fieldMap[i].columnNameEscaped;
const colChanged = this.platform.quoteIdentifier('_changed_' + i);
if (prepared.aggregateSelects[i]) {
const select: string[] = [];
Expand Down Expand Up @@ -404,7 +411,7 @@ export class MySQLQueryResolver<T extends OrmEntity> extends SQLQueryResolver<T>
const pkField = this.platform.quoteIdentifier(primaryKey.name);
const primaryKeyConverted = primaryKeyObjectConverter(this.classSchema, this.platform.serializer.deserializeRegistry);

const sqlBuilder = new SqlBuilder(this.platform);
const sqlBuilder = new SqlBuilder(this.adapter);
const tableName = this.platform.getTableIdentifier(this.classSchema);
const select = sqlBuilder.select(this.classSchema, model, { select: [`${tableName}.${pkField}`] });

Expand Down Expand Up @@ -509,7 +516,7 @@ export class MySQLQueryResolver<T extends OrmEntity> extends SQLQueryResolver<T>
`;
const selectVarsSQL = `SELECT ${selectVars.join(', ')};`;

const sqlBuilder = new SqlBuilder(this.platform, selectParams);
const sqlBuilder = new SqlBuilder(this.adapter, selectParams);
const selectSQL = sqlBuilder.select(this.classSchema, model, { select });

const params = selectSQL.params;
Expand Down Expand Up @@ -551,7 +558,7 @@ export class MySQLDatabaseQuery<T extends OrmEntity> extends SQLDatabaseQuery<T>
export class MySQLDatabaseQueryFactory extends SQLDatabaseQueryFactory {
createQuery<T extends OrmEntity>(type?: ReceiveType<T> | ClassType<T> | AbstractClassType<T> | ReflectionClass<T>): MySQLDatabaseQuery<T> {
return new MySQLDatabaseQuery<T>(ReflectionClass.from(type), this.databaseSession,
new MySQLQueryResolver<T>(this.connectionPool, this.platform, ReflectionClass.from(type), this.databaseSession),
new MySQLQueryResolver<T>(this.connectionPool, this.platform, ReflectionClass.from(type), this.databaseSession.adapter, this.databaseSession),
);
}
}
Expand Down
20 changes: 16 additions & 4 deletions packages/mysql/src/mysql-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@

import { Pool } from 'mariadb';
import { mySqlSerializer } from './mysql-serializer.js';
import { isDateType, isReferenceType, isUUIDType, ReflectionClass, ReflectionKind, ReflectionProperty, Serializer, Type, TypeNumberBrand } from '@deepkit/type';
import {
isDateType,
isReferenceType,
isUUIDType,
ReflectionClass,
ReflectionKind,
ReflectionProperty,
Serializer,
Type,
TypeNumberBrand,
} from '@deepkit/type';
import {
Column,
DefaultPlatform,
IndexModel,
isSet,
noopSqlTypeCaster,
PreparedAdapter,
typeResolvesToBigInt,
typeResolvesToBoolean,
typeResolvesToInteger,
Expand All @@ -32,7 +43,8 @@ export function mysqlJsonTypeCaster(placeholder: string): string {

export class MySQLPlatform extends DefaultPlatform {
protected override defaultSqlType = 'longtext';
protected override annotationId = 'mysql';
public override annotationId = 'mysql';

protected override defaultNowExpression = 'now()';
override schemaParserType = MysqlSchemaParser;

Expand Down Expand Up @@ -72,8 +84,8 @@ export class MySQLPlatform extends DefaultPlatform {
this.addBinaryType('longblob');
}

override createSqlFilterBuilder(schema: ReflectionClass<any>, tableName: string): MySQLSQLFilterBuilder {
return new MySQLSQLFilterBuilder(schema, tableName, this.serializer, new this.placeholderStrategy, this);
override createSqlFilterBuilder(adapter: PreparedAdapter, schema: ReflectionClass<any>, tableName: string): MySQLSQLFilterBuilder {
return new MySQLSQLFilterBuilder(adapter, schema, tableName, this.serializer, new this.placeholderStrategy);
}

override getSqlTypeCaster(type: Type): (placeholder: string) => string {
Expand Down
58 changes: 57 additions & 1 deletion packages/orm-integration/src/various.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { expect } from '@jest/globals';
import { AutoIncrement, BackReference, cast, DatabaseField, entity, isReferenceInstance, PrimaryKey, Reference, Unique, uuid, UUID } from '@deepkit/type';
import {
AutoIncrement,
BackReference,
cast,
DatabaseField,
entity,
isReferenceInstance,
PrimaryKey,
Reference,
Unique,
uuid,
UUID,
} from '@deepkit/type';
import { identifier, sql, SQLDatabaseAdapter } from '@deepkit/sql';
import { DatabaseFactory } from './test.js';
import { hydrateEntity, isDatabaseOf, UniqueConstraintFailure } from '@deepkit/orm';
Expand Down Expand Up @@ -902,5 +914,49 @@ export const variousTests = {
expect(items.length).toBe(1);
expect(items[0]).toMatchObject({ id: 2, doc: { name: 'Peter2' } });
}
},
async remapName(databaseFactory: DatabaseFactory) {
@entity.name('map_name_user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
group?: Group & Reference & DatabaseField<{name: 'group_id'}>;

constructor(public username: string) {}
}

@entity.name('map_name_group')
class Group {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(public name: string) {}
}

const database = await databaseFactory([User, Group]);
{
const group1 = new Group('admin');
const group2 = new Group('users');
const user = new User('peter');
user.group = group1;
await database.persist(user, group1, group2);
}

{
const user = await database.query(User).joinWith('group').filter({username: 'peter'}).findOne();
expect(user.group).toMatchObject({name: 'admin'});
const userGroup = await database.query(Group).filter({name: 'users'}).findOne();
user.group = userGroup;
await database.persist(user);
}

{
const user = await database.query(User).joinWith('group').filter({username: 'peter'}).findOne();
expect(user.group).toMatchObject({name: 'users'});
}

{
const userGroup = await database.query(Group).filter({name: 'users'}).findOne();
const user = await database.query(User).joinWith('group').filter({group: userGroup}).findOne();
expect(user).toMatchObject({username: 'peter'});
expect(user.group).toMatchObject({name: 'users'});
}
}
};
21 changes: 15 additions & 6 deletions packages/postgres/src/postgres-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ import type { Pool, PoolClient, PoolConfig } from 'pg';
import pg from 'pg';
import { AbstractClassType, asyncOperation, ClassType, empty } from '@deepkit/core';
import { FrameCategory, Stopwatch } from '@deepkit/stopwatch';
import { Changes, getPatchSerializeFunction, getSerializeFunction, ReceiveType, ReflectionClass, ReflectionKind, ReflectionProperty, resolvePath } from '@deepkit/type';
import {
Changes,
getPatchSerializeFunction,
getSerializeFunction,
ReceiveType,
ReflectionClass,
ReflectionKind,
ReflectionProperty,
resolvePath,
} from '@deepkit/type';

/**
* Converts a specific database error to a more specific error, if possible.
Expand Down Expand Up @@ -307,7 +316,7 @@ export class PostgresPersistence extends SQLPersistence {
}

for (const i of prepared.changedFields) {
const col = this.platform.quoteIdentifier(i);
const col = entity.fieldMap[i].columnNameEscaped;
const colChanged = '_changed_' + i;
if (prepared.aggregateSelects[i]) {
const select: string[] = [];
Expand All @@ -332,7 +341,7 @@ export class PostgresPersistence extends SQLPersistence {
}
}

const escapedValuesNames = valuesNames.map(v => this.platform.quoteIdentifier(v));
const escapedValuesNames = valuesNames.map(v => entity.fieldMap[v].columnNameEscaped);

const sql = `
WITH _b(${prepared.originPkField}, ${escapedValuesNames.join(', ')}) AS (
Expand Down Expand Up @@ -407,7 +416,7 @@ export class PostgresSQLQueryResolver<T extends OrmEntity> extends SQLQueryResol
const pkField = this.platform.quoteIdentifier(primaryKey.name);
const primaryKeyConverted = primaryKeyObjectConverter(this.classSchema, this.platform.serializer.deserializeRegistry);

const sqlBuilder = new SqlBuilder(this.platform);
const sqlBuilder = new SqlBuilder(this.adapter);
const tableName = this.platform.getTableIdentifier(this.classSchema);
const select = sqlBuilder.select(this.classSchema, model, { select: [`${tableName}.${pkField}`] });

Expand Down Expand Up @@ -513,7 +522,7 @@ export class PostgresSQLQueryResolver<T extends OrmEntity> extends SQLQueryResol
}
}

const sqlBuilder = new SqlBuilder(this.platform, selectParams);
const sqlBuilder = new SqlBuilder(this.adapter, selectParams);
const selectSQL = sqlBuilder.select(this.classSchema, model, { select });

const sql = `
Expand Down Expand Up @@ -556,7 +565,7 @@ export class PostgresSQLDatabaseQuery<T extends OrmEntity> extends SQLDatabaseQu
export class PostgresSQLDatabaseQueryFactory extends SQLDatabaseQueryFactory {
createQuery<T extends OrmEntity>(type?: ReceiveType<T> | ClassType<T> | AbstractClassType<T> | ReflectionClass<T>): PostgresSQLDatabaseQuery<T> {
return new PostgresSQLDatabaseQuery<T>(ReflectionClass.from(type), this.databaseSession,
new PostgresSQLQueryResolver<T>(this.connectionPool, this.platform, ReflectionClass.from(type), this.databaseSession),
new PostgresSQLQueryResolver<T>(this.connectionPool, this.platform, ReflectionClass.from(type), this.databaseSession.adapter, this.databaseSession),
);
}
}
Expand Down
20 changes: 16 additions & 4 deletions packages/postgres/src/postgres-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DefaultPlatform,
IndexModel,
isSet,
PreparedAdapter,
SqlPlaceholderStrategy,
Table,
typeResolvesToBigInt,
Expand All @@ -23,7 +24,17 @@ import {
typeResolvesToString,
} from '@deepkit/sql';
import { postgresSerializer } from './postgres-serializer.js';
import { isDateType, isReferenceType, isUUIDType, ReflectionClass, ReflectionKind, ReflectionProperty, Serializer, Type, TypeNumberBrand } from '@deepkit/type';
import {
isDateType,
isReferenceType,
isUUIDType,
ReflectionClass,
ReflectionKind,
ReflectionProperty,
Serializer,
Type,
TypeNumberBrand,
} from '@deepkit/type';
import { PostgresSchemaParser } from './postgres-schema-parser.js';
import { PostgreSQLFilterBuilder } from './sql-filter-builder.js';
import { isArray, isObject } from '@deepkit/core';
Expand Down Expand Up @@ -67,7 +78,8 @@ export class PostgresPlaceholderStrategy extends SqlPlaceholderStrategy {

export class PostgresPlatform extends DefaultPlatform {
protected override defaultSqlType = 'text';
protected override annotationId = 'postgres';
public override annotationId = 'postgres';

protected override defaultNowExpression = `now()`;
public override readonly serializer: Serializer = postgresSerializer;
override schemaParserType = PostgresSchemaParser;
Expand Down Expand Up @@ -118,8 +130,8 @@ export class PostgresPlatform extends DefaultPlatform {
return super.getAggregateSelect(tableName, property, func);
}

override createSqlFilterBuilder(schema: ReflectionClass<any>, tableName: string): PostgreSQLFilterBuilder {
return new PostgreSQLFilterBuilder(schema, tableName, this.serializer, new this.placeholderStrategy, this);
override createSqlFilterBuilder(adapter: PreparedAdapter, schema: ReflectionClass<any>, tableName: string): PostgreSQLFilterBuilder {
return new PostgreSQLFilterBuilder(adapter, schema, tableName, this.serializer, new this.placeholderStrategy);
}

override getDeepColumnAccessor(table: string, column: string, path: string) {
Expand Down
45 changes: 34 additions & 11 deletions packages/sql/src/platform/default-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,36 @@
* You should have received a copy of the MIT License along with this program.
*/

import { Column, ColumnDiff, DatabaseDiff, DatabaseModel, ForeignKey, IndexModel, Table, TableDiff } from '../schema/table.js';
import {
Column,
ColumnDiff,
DatabaseDiff,
DatabaseModel,
ForeignKey,
IndexModel,
Table,
TableDiff,
} from '../schema/table.js';
import sqlstring from 'sqlstring';
import { ClassType, isArray, isObject } from '@deepkit/core';
import { sqlSerializer } from '../serializer/sql-serializer.js';
import { parseType, SchemaParser } from '../reverse/schema-parser.js';
import { SQLFilterBuilder } from '../sql-filter-builder.js';
import { Sql } from '../sql-builder.js';
import { binaryTypes, databaseAnnotation, isCustomTypeClass, isIntegerType, ReflectionClass, ReflectionKind, ReflectionProperty, Serializer, Type } from '@deepkit/type';
import {
binaryTypes,
databaseAnnotation,
isCustomTypeClass,
isIntegerType,
ReflectionClass,
ReflectionKind,
ReflectionProperty,
Serializer,
Type,
} from '@deepkit/type';
import { DatabaseEntityRegistry, MigrateOptions } from '@deepkit/orm';
import { splitDotPath } from '../sql-adapter.js';
import { PreparedAdapter } from '../prepare.js';

export function isSet(v: any): boolean {
return v !== '' && v !== undefined && v !== null;
Expand Down Expand Up @@ -83,14 +103,16 @@ export function noopSqlTypeCaster(placeholder: string): string {
}

export interface NamingStrategy {
getColumnName(property: ReflectionProperty): string;
getColumnName(property: ReflectionProperty, databaseAdapterAnnotationId: string): string;

getTableName(reflectionClass: ReflectionClass<any>): string;
}

export class DefaultNamingStrategy implements NamingStrategy {
getColumnName(property: ReflectionProperty): string {
return property.getNameAsString();
getColumnName(property: ReflectionProperty, databaseAdapterAnnotationId: string): string {
const dbOptions = databaseAnnotation.getDatabase(property.type, databaseAdapterAnnotationId) || {};

return dbOptions?.name || property.getNameAsString();
}

getTableName(reflectionClass: ReflectionClass<any>): string {
Expand Down Expand Up @@ -128,7 +150,8 @@ export abstract class DefaultPlatform {
/**
* The ID used in annotation to get database related type information (like `type`, `default`, `defaultExpr`, ...) via databaseAnnotation.getDatabase.
*/
protected annotationId = '*';
public annotationId = '*';

protected typeMapping = new Map<ReflectionKind | TypeMappingChecker, TypeMapping>();
protected nativeTypeInformation = new Map<string, Partial<NativeTypeInformation>>();

Expand All @@ -143,8 +166,8 @@ export abstract class DefaultPlatform {
if (offset) sql.append('OFFSET ' + this.quoteValue(offset));
}

createSqlFilterBuilder(reflectionClass: ReflectionClass<any>, tableName: string): SQLFilterBuilder {
return new SQLFilterBuilder(reflectionClass, tableName, this.serializer, new this.placeholderStrategy, this);
createSqlFilterBuilder(adapter: PreparedAdapter, reflectionClass: ReflectionClass<any>, tableName: string): SQLFilterBuilder {
return new SQLFilterBuilder(adapter, reflectionClass, tableName, this.serializer, new this.placeholderStrategy);
}

getMigrationTableName() {
Expand All @@ -157,7 +180,7 @@ export abstract class DefaultPlatform {
}

getAggregateSelect(tableName: string, property: ReflectionProperty, func: string) {
return `${func}(${tableName}.${this.quoteIdentifier(property.getNameAsString())})`;
return `${func}(${tableName}.${this.quoteIdentifier(this.namingStrategy.getColumnName(property, this.annotationId))})`;
}

addBinaryType(sqlType: string, size?: number, scale?: number) {
Expand Down Expand Up @@ -360,7 +383,7 @@ export abstract class DefaultPlatform {
if (property.isBackReference()) continue;
if (property.isDatabaseMigrationSkipped(database.adapterName)) continue;

const column = table.addColumn(this.namingStrategy.getColumnName(property), property);
const column = table.addColumn(this.namingStrategy.getColumnName(property, this.annotationId), property);
const dbOptions = databaseAnnotation.getDatabase(property.type, this.annotationId) || {};

if (!property.isAutoIncrement()) {
Expand Down Expand Up @@ -398,7 +421,7 @@ export abstract class DefaultPlatform {
throw new Error(`Referenced entity ${foreignSchema.getClassName()} from ${schema.getClassName()}.${property.getNameAsString()} is not available`);
}
const foreignKey = table.addForeignKey('', foreignTable);
foreignKey.localColumns = [table.getColumn(property.getNameAsString())];
foreignKey.localColumns = [table.getColumn(this.namingStrategy.getColumnName(property, this.annotationId))];
foreignKey.foreignColumns = foreignTable.getPrimaryKeys();
if (reference.onDelete) foreignKey.onDelete = reference.onDelete;
if (reference.onUpdate) foreignKey.onUpdate = reference.onUpdate;
Expand Down
Loading

0 comments on commit 63b8aaf

Please sign in to comment.