diff --git a/lib/dialects/postgres/index.js b/lib/dialects/postgres/index.js index ce44a0c7c5..5215e91d11 100644 --- a/lib/dialects/postgres/index.js +++ b/lib/dialects/postgres/index.js @@ -7,6 +7,7 @@ const Client = require('../../client'); const Transaction = require('./execution/pg-transaction'); const QueryCompiler = require('./query/pg-querycompiler'); +const QueryBuilder = require('./query/pg-querybuilder'); const ColumnCompiler = require('./schema/pg-columncompiler'); const TableCompiler = require('./schema/pg-tablecompiler'); const ViewCompiler = require('./schema/pg-viewcompiler'); @@ -30,6 +31,10 @@ class Client_PG extends Client { return new Transaction(this, ...arguments); } + queryBuilder() { + return new QueryBuilder(this); + } + queryCompiler(builder, formatter) { return new QueryCompiler(this, builder, formatter); } diff --git a/lib/dialects/postgres/query/pg-querybuilder.js b/lib/dialects/postgres/query/pg-querybuilder.js new file mode 100644 index 0000000000..2c42592873 --- /dev/null +++ b/lib/dialects/postgres/query/pg-querybuilder.js @@ -0,0 +1,8 @@ +const QueryBuilder = require('../../../query/querybuilder.js'); + +module.exports = class QueryBuilder_PostgreSQL extends QueryBuilder { + using(tables) { + this._single.using = tables; + return this; + } +}; diff --git a/lib/dialects/postgres/query/pg-querycompiler.js b/lib/dialects/postgres/query/pg-querycompiler.js index 872a538ddb..24b5bd7d8e 100644 --- a/lib/dialects/postgres/query/pg-querycompiler.js +++ b/lib/dialects/postgres/query/pg-querycompiler.js @@ -4,7 +4,10 @@ const identity = require('lodash/identity'); const reduce = require('lodash/reduce'); const QueryCompiler = require('../../../query/querycompiler'); -const { wrapString } = require('../../../formatter/wrappingFormatter'); +const { + wrapString, + wrap: wrap_, +} = require('../../../formatter/wrappingFormatter'); class QueryCompiler_PG extends QueryCompiler { constructor(client, builder, formatter) { @@ -57,9 +60,70 @@ class QueryCompiler_PG extends QueryCompiler { }; } - // Compiles an `update` query, allowing for a return value. + using() { + const usingTables = this.single.using; + if (!usingTables) return; + let sql = 'using '; + if (Array.isArray(usingTables)) { + sql += usingTables + .map((table) => { + return this.formatter.wrap(table); + }) + .join(','); + } else { + sql += this.formatter.wrap(usingTables); + } + return sql; + } + + // Compiles an `delete` query, allowing for a return value. del() { - const sql = super.del(...arguments); + // Make sure tableName is processed by the formatter first. + const { tableName } = this; + const withSQL = this.with(); + let wheres = this.where() || ''; + let using = this.using() || ''; + const joins = this.grouped.join; + + const tableJoins = []; + if (Array.isArray(joins)) { + for (const join of joins) { + tableJoins.push( + wrap_( + this._joinTable(join), + undefined, + this.builder, + this.client, + this.bindingsHolder + ) + ); + + const joinWheres = []; + for (const clause of join.clauses) { + joinWheres.push( + this.whereBasic({ + column: clause.column, + operator: '=', + value: clause.value, + asColumn: true, + }) + ); + } + if (joinWheres.length > 0) { + wheres += (wheres ? ' and ' : '') + joinWheres.join(' '); + } + } + if (tableJoins.length > 0) { + using += (using ? ',' : 'using ') + tableJoins.join(','); + } + } + + // With 'using' syntax, no tablename between DELETE and FROM. + const sql = + withSQL + + `delete from ${this.single.only ? 'only ' : ''}${tableName}` + + (using ? ` ${using}` : '') + + (wheres ? ` ${wheres}` : ''); const { returning } = this.single; return { sql: sql + this._returning(returning), diff --git a/lib/query/querybuilder.js b/lib/query/querybuilder.js index 50c76c1a82..a5ca769754 100644 --- a/lib/query/querybuilder.js +++ b/lib/query/querybuilder.js @@ -269,6 +269,12 @@ class Builder extends EventEmitter { return this; } + using(tables) { + throw new Error( + "'using' function is only available in PostgreSQL dialect with Delete statements." + ); + } + // JOIN blocks: innerJoin(...args) { return this._joinType('inner').join(...args); diff --git a/lib/query/querycompiler.js b/lib/query/querycompiler.js index 6e9084485e..0388c6dfde 100644 --- a/lib/query/querycompiler.js +++ b/lib/query/querycompiler.js @@ -333,6 +333,12 @@ class QueryCompiler { })`; } + _joinTable(join) { + return join.schema && !(join.table instanceof Raw) + ? `${join.schema}.${join.table}` + : join.table; + } + // Compiles all each of the `join` clauses on the query, // including any nested join queries. join() { @@ -342,10 +348,7 @@ class QueryCompiler { if (!joins) return ''; while (++i < joins.length) { const join = joins[i]; - const table = - join.schema && !(join.table instanceof Raw) - ? `${join.schema}.${join.table}` - : join.table; + const table = this._joinTable(join); if (i > 0) sql += ' '; if (join.joinType === 'raw') { sql += unwrapRaw_( diff --git a/test/integration/query/deletes.js b/test/integration/query/deletes.js index e24a442f53..9f76c5ddd8 100644 --- a/test/integration/query/deletes.js +++ b/test/integration/query/deletes.js @@ -2,12 +2,8 @@ const { expect } = require('chai'); const { TEST_TIMESTAMP } = require('../../util/constants'); -const { - isSQLite, - isPostgreSQL, - isOracle, - isCockroachDB, -} = require('../../util/db-helpers'); +const { isSQLite, isOracle, isCockroachDB } = require('../../util/db-helpers'); +const { isPostgreSQL } = require('../../util/db-helpers.js'); module.exports = function (knex) { describe('Deletes', function () { @@ -96,12 +92,7 @@ module.exports = function (knex) { .join('accounts', 'accounts.id', 'test_table_two.account_id') .where({ 'accounts.email': 'test3@example.com' }) .del(); - if ( - isSQLite(knex) || - isPostgreSQL(knex) || - isCockroachDB(knex) || - isOracle(knex) - ) { + if (isSQLite(knex) || isCockroachDB(knex) || isOracle(knex)) { await expect(query).to.be.rejected; return; } @@ -112,6 +103,12 @@ module.exports = function (knex) { ['test3@example.com'], 1 ); + tester( + 'pg', + 'delete from "test_table_two" using "accounts" where "accounts"."email" = ? and "accounts"."id" = "test_table_two"."account_id"', + ['test3@example.com'], + 1 + ); tester( 'mssql', 'delete [test_table_two] from [test_table_two] inner join [accounts] on [accounts].[id] = [test_table_two].[account_id] where [accounts].[email] = ?;select @@rowcount', @@ -120,6 +117,35 @@ module.exports = function (knex) { ); }); }); + + it('should handle basic delete with join and "using" syntax in PostgreSQL', async function () { + if (!isPostgreSQL(knex)) { + this.skip(); + } + await knex('test_table_two').insert({ + account_id: 4, + details: '', + status: 1, + }); + const query = knex('test_table_two') + .using('accounts') + .where({ 'accounts.email': 'test4@example.com' }) + .whereRaw('"accounts"."id" = "test_table_two"."account_id"') + .del(); + if (!isPostgreSQL(knex)) { + await expect(query).to.be.rejected; + return; + } + return query.testSql(function (tester) { + tester( + 'pg', + 'delete from "test_table_two" using "accounts" where "accounts"."email" = ? and "accounts"."id" = "test_table_two"."account_id"', + ['test4@example.com'], + 1 + ); + }); + }); + it('should handle returning', async function () { await knex('test_table_two').insert({ account_id: 4, @@ -130,12 +156,7 @@ module.exports = function (knex) { .join('accounts', 'accounts.id', 'test_table_two.account_id') .where({ 'accounts.email': 'test4@example.com' }) .del('*'); - if ( - isSQLite(knex) || - isPostgreSQL(knex) || - isCockroachDB(knex) || - isOracle(knex) - ) { + if (isSQLite(knex) || isCockroachDB(knex) || isOracle(knex)) { await expect(query).to.be.rejected; return; } @@ -146,6 +167,28 @@ module.exports = function (knex) { ['test4@example.com'], 1 ); + tester( + 'pg', + 'delete from "test_table_two" using "accounts" where "accounts"."email" = ? and "accounts"."id" = "test_table_two"."account_id" returning *', + ['test4@example.com'], + [ + { + about: 'Lorem ipsum Dolore labore incididunt enim.', + balance: 0, + id: '4', + account_id: 4, + details: '', + status: 1, + phone: null, + logins: 2, + email: 'test4@example.com', + first_name: 'Test', + last_name: 'User', + created_at: TEST_TIMESTAMP, + updated_at: TEST_TIMESTAMP, + }, + ] + ); tester( 'mssql', 'delete [test_table_two] output deleted.* from [test_table_two] inner join [accounts] on [accounts].[id] = [test_table_two].[account_id] where [accounts].[email] = ?', diff --git a/test/unit/query/builder.js b/test/unit/query/builder.js index 9c32f063df..f23d76811c 100644 --- a/test/unit/query/builder.js +++ b/test/unit/query/builder.js @@ -10160,55 +10160,152 @@ describe('QueryBuilder', () => { ); }); - it('should include join when deleting', () => { - testsql( - qb() - .del() - .from('users') - .join('photos', 'photos.id', 'users.id') - .where({ 'user.email': 'mock@example.com' }), - { - mysql: { - sql: 'delete `users` from `users` inner join `photos` on `photos`.`id` = `users`.`id` where `user`.`email` = ?', - bindings: ['mock@example.com'], - }, - mssql: { - sql: 'delete [users] from [users] inner join [photos] on [photos].[id] = [users].[id] where [user].[email] = ?;select @@rowcount', - bindings: ['mock@example.com'], - }, - oracledb: { - sql: 'delete "users" from "users" inner join "photos" on "photos"."id" = "users"."id" where "user"."email" = ?', - bindings: ['mock@example.com'], - }, - pg: { - sql: 'delete "users" from "users" inner join "photos" on "photos"."id" = "users"."id" where "user"."email" = ?', - bindings: ['mock@example.com'], - }, - 'pg-redshift': { - sql: 'delete "users" from "users" inner join "photos" on "photos"."id" = "users"."id" where "user"."email" = ?', - bindings: ['mock@example.com'], - }, - sqlite3: { - sql: 'delete `users` from `users` inner join `photos` on `photos`.`id` = `users`.`id` where `user`.`email` = ?', - bindings: ['mock@example.com'], - }, - } - ); - }); - it('should include join when deleting with mssql triggers', () => { - const triggerOptions = { includeTriggerModifications: true }; - testsql( - qb() - .del('*', triggerOptions) - .from('users') - .join('photos', 'photos.id', 'users.id') - .where({ 'user.email': 'mock@example.com' }), - { - mssql: { - sql: 'select top(0) [t].* into #out from [users] as t left join [users] on 0=1;delete [users] output deleted.* into #out from [users] inner join [photos] on [photos].[id] = [users].[id] where [user].[email] = ?; select * from #out; drop table #out;', - bindings: ['mock@example.com'], - }, - } - ); + describe('deleting with joins', () => { + it('should transform joins into "using" syntax with PostgreSQL', () => { + // Knex transform joins into "using" syntax for PostgreSQL + testsql( + qb() + .del() + .from('users') + .join('photos', 'photos.id', 'users.id') + .where({ 'user.email': 'mock@example.com' }), + { + pg: { + sql: 'delete from "users" using "photos" where "user"."email" = ? and "photos"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should transform multiple joins into multiple "using" syntax with PostgreSQL', () => { + // Knex transform joins into "using" syntax for PostgreSQL + testsql( + qb() + .del() + .from('users') + .join('photos', 'photos.id', 'users.id') + .join('docs', 'docs.id', 'users.id') + .where({ 'user.email': 'mock@example.com' }), + { + pg: { + sql: 'delete from "users" using "photos","docs" where "user"."email" = ? and "photos"."id" = "users"."id" and "docs"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should join with "using" explicit syntax with PostgreSQL', () => { + // explicit using syntax + testsql( + qb() + .del() + .from('users') + .using('photos') + .where({ 'user.email': 'mock@example.com' }) + .whereRaw('"photos"."id" = "users"."id"'), + { + pg: { + sql: 'delete from "users" using "photos" where "user"."email" = ? and "photos"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should join with multiple tables and "using" explicit syntax with PostgreSQL', () => { + // Test with multiple tables 'using' + testsql( + qb() + .del() + .from('users') + .using(['photos', 'docs']) + .where({ + 'user.email': 'mock@example.com', + }) + .whereRaw( + '"photos"."id" = "users"."id" and "docs"."id" = "users"."id"' + ), + { + pg: { + sql: 'delete from "users" using "photos","docs" where "user"."email" = ? and "photos"."id" = "users"."id" and "docs"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should joins with mixed joins and "using" explicit syntax with PostgreSQL', () => { + // you can use explicit 'using' and joins, all are merged at the end. + testsql( + qb() + .del() + .from('users') + .using('photos') + .join('docs', 'docs.id', 'users.id') + .whereRaw('"photos"."id" = "users"."id"') + .where({ 'user.email': 'mock@example.com' }), + { + pg: { + sql: 'delete from "users" using "photos","docs" where "photos"."id" = "users"."id" and "user"."email" = ? and "docs"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should include join when deleting', () => { + testsql( + qb() + .del() + .from('users') + .join('photos', 'photos.id', 'users.id') + .where({ 'user.email': 'mock@example.com' }), + { + mysql: { + sql: 'delete `users` from `users` inner join `photos` on `photos`.`id` = `users`.`id` where `user`.`email` = ?', + bindings: ['mock@example.com'], + }, + mssql: { + sql: 'delete [users] from [users] inner join [photos] on [photos].[id] = [users].[id] where [user].[email] = ?;select @@rowcount', + bindings: ['mock@example.com'], + }, + oracledb: { + sql: 'delete "users" from "users" inner join "photos" on "photos"."id" = "users"."id" where "user"."email" = ?', + bindings: ['mock@example.com'], + }, + pg: { + sql: 'delete from "users" using "photos" where "user"."email" = ? and "photos"."id" = "users"."id"', + bindings: ['mock@example.com'], + }, + 'pg-redshift': { + sql: 'delete "users" from "users" inner join "photos" on "photos"."id" = "users"."id" where "user"."email" = ?', + bindings: ['mock@example.com'], + }, + sqlite3: { + sql: 'delete `users` from `users` inner join `photos` on `photos`.`id` = `users`.`id` where `user`.`email` = ?', + bindings: ['mock@example.com'], + }, + } + ); + }); + + it('should include join when deleting with mssql triggers', () => { + const triggerOptions = { includeTriggerModifications: true }; + testsql( + qb() + .del('*', triggerOptions) + .from('users') + .join('photos', 'photos.id', 'users.id') + .where({ 'user.email': 'mock@example.com' }), + { + mssql: { + sql: 'select top(0) [t].* into #out from [users] as t left join [users] on 0=1;delete [users] output deleted.* into #out from [users] inner join [photos] on [photos].[id] = [users].[id] where [user].[email] = ?; select * from #out; drop table #out;', + bindings: ['mock@example.com'], + }, + } + ); + }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 9c77a5f9dd..4a79c7c4f4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -482,7 +482,7 @@ export declare namespace Knex { // // QueryInterface // - type ClearStatements = "with" | "select" | "columns" | "hintComments" | "where" | "union" | "join" | "group" | "order" | "having" | "limit" | "offset" | "counter" | "counters"; + type ClearStatements = "with" | "select" | "columns" | "hintComments" | "where" | "union" | "using" | "join" | "group" | "order" | "having" | "limit" | "offset" | "counter" | "counters"; interface QueryInterface { select: Select; @@ -509,6 +509,9 @@ export declare namespace Knex { fullOuterJoin: Join; crossJoin: Join; + // Using + using: Using; + // Withs with: With; withRecursive: With; @@ -1338,6 +1341,10 @@ export declare namespace Knex { >; } + interface Using { + (tables: string[]): QueryBuilder; + } + interface With extends WithRaw, WithWrapped {}