diff --git a/lib/query/querycompiler.js b/lib/query/querycompiler.js index a7a4997ab3..a4a09d2723 100644 --- a/lib/query/querycompiler.js +++ b/lib/query/querycompiler.js @@ -125,16 +125,27 @@ class QueryCompiler { let sql = this.with(); let unionStatement = ''; - // Compute all statements to main query - const statements = components.map((component) => { + + const firstStatements = []; + const endStatements = []; + + components.forEach((component) => { const statement = this[component](this); // We store the 'union' statement to append it at the end. // We still need to call the component sequentially because of // order of bindings. - if (component === 'union') { - unionStatement = statement; - } else { - return statement; + switch (component) { + case 'union': + unionStatement = statement; + break; + case 'columns': + case 'join': + case 'where': + firstStatements.push(statement); + break; + default: + endStatements.push(statement); + break; } }); @@ -144,15 +155,22 @@ class QueryCompiler { const wrapMainQuery = this.grouped.union && this.grouped.union.map((u) => u.wrap).some((u) => u); - const allStatements = - (wrapMainQuery ? '(' : '') + - compact(statements).join(' ') + - (wrapMainQuery ? ')' : ''); if (this.onlyUnions()) { - sql += unionStatement + ' ' + allStatements; + const statements = compact(firstStatements.concat(endStatements)).join( + ' ' + ); + sql += unionStatement + (statements ? ' ' + statements : ''); } else { - sql += allStatements + (unionStatement ? ' ' + unionStatement : ''); + const allStatements = + (wrapMainQuery ? '(' : '') + + compact(firstStatements).join(' ') + + (wrapMainQuery ? ')' : ''); + const endStat = compact(endStatements).join(' '); + sql += + allStatements + + (unionStatement ? ' ' + unionStatement : '') + + (endStat ? ' ' + endStat : endStat); } return sql; } diff --git a/test/integration2/query/select/unions.spec.js b/test/integration2/query/select/unions.spec.js index 8a01a688c0..429c1cd335 100644 --- a/test/integration2/query/select/unions.spec.js +++ b/test/integration2/query/select/unions.spec.js @@ -7,6 +7,7 @@ const { isPgBased, isSQLite, isPostgreSQL, + isMysql, } = require('../../../util/db-helpers'); const { assertNumberArray } = require('../../../util/assertHelper'); const { @@ -22,6 +23,7 @@ const { getAllDbs, getKnexForDb, } = require('../../util/knex-instance-provider'); +const logger = require('../../../integration/logger'); describe('unions', function () { getAllDbs().forEach((db) => { @@ -30,7 +32,7 @@ describe('unions', function () { const unionCols = ['last_name', 'phone']; before(async () => { - knex = getKnexForDb(db); + knex = logger(getKnexForDb(db)); await dropTables(knex); await createUsers(knex); @@ -163,28 +165,344 @@ describe('unions', function () { ); }); - it('handles nested unions with group by and limit', async function () { + it('handles nested unions with limit', async function () { if (!isPostgreSQL(knex)) { return this.skip(); } const results = await knex('accounts') - .count('logins') .select('last_name') - .limit(1) - .groupBy('last_name') .unionAll(function () { - this.count('logins') - .select('last_name') - .from('accounts') - .limit(1) - .groupBy('last_name') - .orderBy('last_name') - .where('logins', '>', '1'); - }, true); - expect(results).to.eql([ - { count: '2', last_name: 'User2' }, - { count: '4', last_name: 'User' }, - ]); + this.select('last_name').from('accounts'); + }) + .first(); + expect(results).to.eql({ + last_name: 'User', + }); + }); + + describe('unions with wrapped queries', () => { + it('nested unions with group by in subqueries and limit and orderby', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .select('last_name') + .unionAll( + [ + knex.select('last_name').from('accounts').groupBy('last_name'), + knex.select('last_name').from('accounts').groupBy('last_name'), + ], + true + ) + .limit(5) + .orderBy('last_name') + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts" group by "last_name") union all (select "last_name" from "accounts" group by "last_name") order by "last_name" asc limit ?', + [5], + [ + { last_name: 'User' }, + { last_name: 'User' }, + { last_name: 'User2' }, + { last_name: 'User2' }, + ] + ); + tester( + 'mysql', + '(select `last_name` from `accounts` group by `last_name`) union all (select `last_name` from `accounts` group by `last_name`) order by `last_name` asc limit ?', + [5], + [ + { last_name: 'User' }, + { last_name: 'User' }, + { last_name: 'User2' }, + { last_name: 'User2' }, + ] + ); + tester( + 'mysql2', + '(select `last_name` from `accounts` group by `last_name`) union all (select `last_name` from `accounts` group by `last_name`) order by `last_name` asc limit ?', + [5], + [ + { last_name: 'User' }, + { last_name: 'User' }, + { last_name: 'User2' }, + { last_name: 'User2' }, + ] + ); + }); + }); + + it('nested unions with first', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .unionAll( + function () { + this.select('last_name').from('accounts'); + }, + function () { + this.select('last_name').from('accounts'); + }, + true + ) + .first() + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts") union all (select "last_name" from "accounts") limit ?', + [1], + { + last_name: 'User', + } + ); + tester( + 'mysql', + '(select `last_name` from `accounts`) union all (select `last_name` from `accounts`) limit ?', + [1], + { + last_name: 'User', + } + ); + tester( + 'mysql2', + '(select `last_name` from `accounts`) union all (select `last_name` from `accounts`) limit ?', + [1], + { + last_name: 'User', + } + ); + }); + }); + + it('nested unions with order by and limit', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .unionAll( + function () { + this.select('last_name').from('accounts'); + }, + function () { + this.select('last_name').from('accounts'); + }, + true + ) + .orderBy('last_name') + .limit(2) + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts") union all (select "last_name" from "accounts") order by "last_name" asc limit ?', + [2], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + tester( + 'mysql', + '(select `last_name` from `accounts`) union all (select `last_name` from `accounts`) order by `last_name` asc limit ?', + [2], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + tester( + 'mysql2', + '(select `last_name` from `accounts`) union all (select `last_name` from `accounts`) order by `last_name` asc limit ?', + [2], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + }); + }); + + it('nested unions with having and groupby in subqueries', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .unionAll( + function () { + this.select('last_name') + .from('accounts') + .having('last_name', '!=', 'User') + .groupBy('last_name'); + }, + function () { + this.select('last_name') + .from('accounts') + .having('last_name', '!=', 'User') + .groupBy('last_name'); + }, + true + ) + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts" group by "last_name" having "last_name" != ?) union all (select "last_name" from "accounts" group by "last_name" having "last_name" != ?)', + ['User', 'User'], + [{ last_name: 'User2' }, { last_name: 'User2' }] + ); + tester( + 'mysql', + '(select `last_name` from `accounts` group by `last_name` having `last_name` != ?) union all (select `last_name` from `accounts` group by `last_name` having `last_name` != ?)', + ['User', 'User'], + [{ last_name: 'User2' }, { last_name: 'User2' }] + ); + tester( + 'mysql2', + '(select `last_name` from `accounts` group by `last_name` having `last_name` != ?) union all (select `last_name` from `accounts` group by `last_name` having `last_name` != ?)', + ['User', 'User'], + [{ last_name: 'User2' }, { last_name: 'User2' }] + ); + }); + }); + + it('nested unions all with order by in subqueries and limit', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .unionAll( + function () { + this.select('last_name').from('accounts').orderBy('last_name'); + }, + function () { + this.select('last_name').from('accounts').orderBy('last_name'); + }, + true + ) + .limit(2) + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts" order by "last_name" asc) union all (select "last_name" from "accounts" order by "last_name" asc) limit ?', + [2], + [{ last_name: 'User' }, { last_name: 'User' }] + ); + tester( + 'mysql', + '(select `last_name` from `accounts` order by `last_name` asc) union all (select `last_name` from `accounts` order by `last_name` asc) limit ?', + [2], + [{ last_name: 'User' }, { last_name: 'User' }] + ); + tester( + 'mysql2', + '(select `last_name` from `accounts` order by `last_name` asc) union all (select `last_name` from `accounts` order by `last_name` asc) limit ?', + [2], + [{ last_name: 'User' }, { last_name: 'User' }] + ); + }); + }); + + it('nested unions all with limit in each subqueries', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex + .unionAll( + [ + knex.select('last_name').from('accounts').limit(1), + knex.select('last_name').from('accounts').limit(1), + ], + true + ) + .testSql(function (tester) { + tester( + 'pg', + '(select "last_name" from "accounts" limit ?) union all (select "last_name" from "accounts" limit ?)', + [1, 1], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + tester( + 'mysql', + '(select `last_name` from `accounts` limit ?) union all (select `last_name` from `accounts` limit ?)', + [1, 1], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + tester( + 'mysql2', + '(select `last_name` from `accounts` limit ?) union all (select `last_name` from `accounts` limit ?)', + [1, 1], + [ + { + last_name: 'User', + }, + { + last_name: 'User', + }, + ] + ); + }); + }); + + it('nested unions with group by and where in subqueries and limit', async function () { + if (!isPostgreSQL(knex) && !isMysql(knex)) { + return this.skip(); + } + await knex('accounts') + .count('logins') + .limit(1) + .unionAll(function () { + this.count('logins') + .from('accounts') + .groupBy('last_name') + .where('logins', '>', '1'); + }, true) + .testSql(function (tester) { + tester( + 'pg', + '(select count("logins") from "accounts") union all (select count("logins") from "accounts" where "logins" > ? group by "last_name") limit ?', + ['1', 1], + [{ count: '8' }] + ); + tester( + 'mysql', + '(select count(`logins`) from `accounts`) union all (select count(`logins`) from `accounts` where `logins` > ? group by `last_name`) limit ?', + ['1', 1], + [{ 'count(`logins`)': 8 }] + ); + tester( + 'mysql2', + '(select count(`logins`) from `accounts`) union all (select count(`logins`) from `accounts` where `logins` > ? group by `last_name`) limit ?', + ['1', 1], + [{ 'count(`logins`)': 8 }] + ); + }); + }); }); describe('intersects', function () { diff --git a/test/unit/query/builder.js b/test/unit/query/builder.js index 2986d6b920..c4a6384808 100644 --- a/test/unit/query/builder.js +++ b/test/unit/query/builder.js @@ -2296,7 +2296,7 @@ describe('QueryBuilder', () => { .first(); testsql(firstUnionAll, { mysql: { - sql: '(select * from `users` where `id` = ? group by `id` limit ?) union all (select * from `users` where `id` = ?)', + sql: '(select * from `users` where `id` = ?) union all (select * from `users` where `id` = ?) group by `id` limit ?', bindings: [1, 2, 1], }, mssql: { @@ -2304,11 +2304,11 @@ describe('QueryBuilder', () => { bindings: [1, 1, 2], }, pg: { - sql: '(select * from "users" where "id" = ? group by "id" limit ?) union all (select * from "users" where "id" = ?)', + sql: '(select * from "users" where "id" = ?) union all (select * from "users" where "id" = ?) group by "id" limit ?', bindings: [1, 2, 1], }, 'pg-redshift': { - sql: '(select * from "users" where "id" = ? group by "id" limit ?) union all (select * from "users" where "id" = ?)', + sql: '(select * from "users" where "id" = ?) union all (select * from "users" where "id" = ?) group by "id" limit ?', bindings: [1, 2, 1], }, });