From df75b2452a72adfc473772c37342c75e7e731d50 Mon Sep 17 00:00:00 2001 From: Kristofer Pervin <7747148+kpervin@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:32:25 -0400 Subject: [PATCH] feat(mysql): support `order by nulls first/last` (#5021) - Added `getOrderByExpression` to AbstractSqlPlatform.ts as an overridable method to format `orderBy` queries. This uses syntax that is recognized by both SQLite and PostGreSQL - Added override of `getOrderByExpression` to MariaDbPlatform.ts and MySqlPlatform.ts that adds functionality to handle `(ASC|DESC) NULLS (FIRST|LAST)` syntax Closes #5004 --- packages/knex/src/AbstractSqlPlatform.ts | 7 +++++ packages/knex/src/query/QueryBuilderHelper.ts | 2 +- packages/mariadb/src/MariaDbPlatform.ts | 23 +++++++++++++++- packages/mysql/src/MySqlPlatform.ts | 26 ++++++++++++++++++- tests/QueryBuilder.test.ts | 4 +-- 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/knex/src/AbstractSqlPlatform.ts b/packages/knex/src/AbstractSqlPlatform.ts index c4d93eb33c3e..0090a3eea5f6 100644 --- a/packages/knex/src/AbstractSqlPlatform.ts +++ b/packages/knex/src/AbstractSqlPlatform.ts @@ -135,4 +135,11 @@ export abstract class AbstractSqlPlatform extends Platform { return ret + 'else null end)'; } + /** + * @internal + */ + getOrderByExpression(column: string, direction: string): string[] { + return [ `${column} ${direction.toLowerCase()}` ]; + } + } diff --git a/packages/knex/src/query/QueryBuilderHelper.ts b/packages/knex/src/query/QueryBuilderHelper.ts index b94692feae25..e87c764c3b9e 100644 --- a/packages/knex/src/query/QueryBuilderHelper.ts +++ b/packages/knex/src/query/QueryBuilderHelper.ts @@ -638,7 +638,7 @@ export class QueryBuilderHelper { if (Array.isArray(order)) { order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate))); } else { - ret.push(`${colPart} ${order.toLowerCase()}`); + ret.push(...this.platform.getOrderByExpression(colPart, order)); } } } diff --git a/packages/mariadb/src/MariaDbPlatform.ts b/packages/mariadb/src/MariaDbPlatform.ts index 9b4b370c9fb0..3f95f97916ad 100644 --- a/packages/mariadb/src/MariaDbPlatform.ts +++ b/packages/mariadb/src/MariaDbPlatform.ts @@ -1,4 +1,4 @@ -import { AbstractSqlPlatform } from '@mikro-orm/knex'; +import { AbstractSqlPlatform, QueryOrder } from '@mikro-orm/knex'; import { MariaDbSchemaHelper } from './MariaDbSchemaHelper'; import { MariaDbExceptionConverter } from './MariaDbExceptionConverter'; import { Utils, type SimpleColumnMeta, type Dictionary, type Type } from '@mikro-orm/core'; @@ -74,4 +74,25 @@ export class MariaDbPlatform extends AbstractSqlPlatform { return `alter table ${quotedTableName} add fulltext index ${quotedIndexName}(${quotedColumnNames.join(',')})`; } + private readonly ORDER_BY_NULLS_TRANSLATE = { + [QueryOrder.asc_nulls_first]: 'is not null', + [QueryOrder.asc_nulls_last]: 'is null', + [QueryOrder.desc_nulls_first]: 'is not null', + [QueryOrder.desc_nulls_last]: 'is null', + } as const; + + /* istanbul ignore next */ + override getOrderByExpression(column: string, direction: string): string[] { + const ret: string[] = []; + const dir = direction.toLowerCase() as keyof typeof this.ORDER_BY_NULLS_TRANSLATE; + + if (dir in this.ORDER_BY_NULLS_TRANSLATE) { + ret.push(`${column} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`); + } + + ret.push(`${column} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`); + + return ret; + } + } diff --git a/packages/mysql/src/MySqlPlatform.ts b/packages/mysql/src/MySqlPlatform.ts index 73cfeed470d6..d8bb92cbfef9 100644 --- a/packages/mysql/src/MySqlPlatform.ts +++ b/packages/mysql/src/MySqlPlatform.ts @@ -1,4 +1,8 @@ -import { AbstractSqlPlatform, type IndexDef } from '@mikro-orm/knex'; +import { + AbstractSqlPlatform, + type IndexDef, + QueryOrder, +} from '@mikro-orm/knex'; import { MySqlSchemaHelper } from './MySqlSchemaHelper'; import { MySqlExceptionConverter } from './MySqlExceptionConverter'; import { Utils, type SimpleColumnMeta, type Dictionary, type Type, type TransformContext } from '@mikro-orm/core'; @@ -88,4 +92,24 @@ export class MySqlPlatform extends AbstractSqlPlatform { return `alter table ${quotedTableName} add fulltext index ${quotedIndexName}(${quotedColumnNames.join(',')})`; } + private readonly ORDER_BY_NULLS_TRANSLATE = { + [QueryOrder.asc_nulls_first]: 'is not null', + [QueryOrder.asc_nulls_last]: 'is null', + [QueryOrder.desc_nulls_first]: 'is not null', + [QueryOrder.desc_nulls_last]: 'is null', + } as const; + + override getOrderByExpression(column: string, direction: string): string[] { + const ret: string[] = []; + const dir = direction.toLowerCase() as keyof typeof this.ORDER_BY_NULLS_TRANSLATE; + + if (dir in this.ORDER_BY_NULLS_TRANSLATE) { + ret.push(`${column} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`); + } + + ret.push(`${column} ${dir.replace(/(\s|nulls|first|last)*/gi, '')}`); + + return ret; + } + } diff --git a/tests/QueryBuilder.test.ts b/tests/QueryBuilder.test.ts index cb62453a3ba2..07ba6996076d 100644 --- a/tests/QueryBuilder.test.ts +++ b/tests/QueryBuilder.test.ts @@ -2207,13 +2207,13 @@ describe('QueryBuilder', () => { test('order by asc nulls first', async () => { const qb = orm.em.createQueryBuilder(Publisher2); qb.select('*').orderBy({ name: QueryOrder.ASC_NULLS_FIRST }); - expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` order by `e0`.`name` asc nulls first'); + expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` order by `e0`.`name` is not null, `e0`.`name` asc'); }); test('order by nulls last', async () => { const qb = orm.em.createQueryBuilder(Publisher2); qb.select('*').orderBy({ name: QueryOrder.DESC_NULLS_LAST, type: QueryOrder.ASC_NULLS_LAST }); - expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` order by `e0`.`name` desc nulls last, `e0`.`type` asc nulls last'); + expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` order by `e0`.`name` is null, `e0`.`name` desc, `e0`.`type` is null, `e0`.`type` asc'); }); test('order by custom expression', async () => {