From f941a997f3459227e82881d3405bed0c5cdbfcc9 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 11:36:34 +0100 Subject: [PATCH 01/10] Support REPLACE --- lib/node/query.js | 31 ++++++++++++++++++++ lib/node/replace.js | 70 +++++++++++++++++++++++++++++++++++++++++++++ lib/table.js | 11 +++++++ 3 files changed, 112 insertions(+) create mode 100644 lib/node/replace.js diff --git a/lib/node/query.js b/lib/node/query.js index 674d07b7..973267bc 100644 --- a/lib/node/query.js +++ b/lib/node/query.js @@ -15,6 +15,7 @@ var OrderBy = require('./orderBy'); var GroupBy = require('./groupBy'); var Having = require('./having'); var Insert = require('./insert'); +var Replace = require('./replace'); var Update = require('./update'); var Delete = require('./delete'); var Returning = require('./returning'); @@ -224,6 +225,36 @@ var Query = Node.define({ }, + replace: function(o) { + var self = this; + + var args = sliced(arguments); + // object literal + if (arguments.length === 1 && !o.toNode && !o.forEach) { + args = []; + Object.keys(o).forEach(function(key) { + var col = self.table.get(key); + if(col && !col.autoGenerated) + args.push(col.value(o[key])); + }); + } else if (o.forEach) { + o.forEach(function(arg) { + return self.replace.call(self, arg); + }); + return self; + } + + if (self.replaceClause) { + self.replaceClause.add(args); + return self; + } else { + self.replaceClause = new Replace(); + self.replaceClause.add(args); + return self.add(self.replaceClause); + } + + }, + update: function(o) { var self = this; var update = new Update(); diff --git a/lib/node/replace.js b/lib/node/replace.js new file mode 100644 index 00000000..d17099d2 --- /dev/null +++ b/lib/node/replace.js @@ -0,0 +1,70 @@ +'use strict'; + +var DefaultNode = require('./default'); +var Node = require('./'); +var ParameterNode = require('./parameter'); + +var Replace = Node.define({ + type: 'REPLACE', + constructor: function () { + Node.call(this); + this.names = []; + this.columns = []; + this.valueSets = []; + } +}); + +module.exports = Replace; + +Replace.prototype.add = function (nodes) { + var hasColumns = false; + var hasValues = false; + var self = this; + var values = {}; + nodes.forEach(function (node) { + var column = node.toNode(); + var name = column.name; + var idx = self.names.indexOf(name); + if (idx < 0) { + self.names.push(name); + self.columns.push(column); + } + hasColumns = true; + hasValues = hasValues || column.value !== undefined; + values[name] = column; + }); + + // When none of the columns have a value, it's ambiguous whether the user + // intends to replace a row of default values or append a SELECT statement + // later. Resolve the ambiguity by assuming that if no columns are specified + // it is a row of default values, otherwise a SELECT will be added. + if (hasValues || !hasColumns) { + this.valueSets.push(values); + } + + return self; +}; + +/* + * Get parameters for all values to be replaced. This function + * handles handles bulk replaces, where keys may be present + * in some objects and not others. When keys are not present, + * the replace should refer to the column value as DEFAULT. + */ +Replace.prototype.getParameters = function () { + var self = this; + return this.valueSets + .map(function (nodeDict) { + var set = []; + self.names.forEach(function (name) { + var node = nodeDict[name]; + if (node) { + set.push(ParameterNode.getNodeOrParameterNode(node.value)); + } + else { + set.push(new DefaultNode()); + } + }); + return set; + }); +}; diff --git a/lib/table.js b/lib/table.js index 23e335a8..3fae2710 100644 --- a/lib/table.js +++ b/lib/table.js @@ -214,6 +214,17 @@ Table.prototype.insert = function() { return query; }; +Table.prototype.replace = function() { + var query = new Query(this); + if(!arguments[0] || (util.isArray(arguments[0]) && arguments[0].length === 0)){ + query.select.call(query, this.star()); + query.where.apply(query,["1=2"]); + } else { + query.replace.apply(query, arguments); + } + return query; +}; + Table.prototype.toNode = function() { return new TableNode(this); }; From 2ed02ec3777daaf6a888b3f1b6282ac2f11a13da Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 11:59:49 +0100 Subject: [PATCH 02/10] Add REPLACE to default Postgres dialect --- lib/dialect/postgres.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index 6e67c38e..c93de8db 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -131,6 +131,7 @@ Postgres.prototype.visit = function(node) { case 'SUBQUERY' : return this.visitSubquery(node); case 'SELECT' : return this.visitSelect(node); case 'INSERT' : return this.visitInsert(node); + case 'REPLACE' : return this.visitReplace(node); case 'UPDATE' : return this.visitUpdate(node); case 'DELETE' : return this.visitDelete(node); case 'CREATE' : return this.visitCreate(node); @@ -266,6 +267,39 @@ Postgres.prototype.visitInsert = function(insert) { return result; }; +Postgres.prototype.visitReplace = function(replace) { + var self = this; + // don't use table.column for replaces + this._visitedReplace = true; + + var result = ['REPLACE']; + result = result.concat(replace.nodes.map(this.visit.bind(this))); + result.push('INTO ' + this.visit(this._queryNode.table.toNode())); + result.push('(' + replace.columns.map(this.visit.bind(this)).join(', ') + ')'); + + var paramNodes = replace.getParameters(); + + if (paramNodes.length > 0) { + var paramText = paramNodes.map(function (paramSet) { + return paramSet.map(function (param) { + return self.visit(param); + }).join(', '); + }).map(function (param) { + return '('+param+')'; + }).join(', '); + + result.push('VALUES', paramText); + + if (result.slice(2, 5).join(' ') === '() VALUES ()') { + result.splice(2, 3, 'DEFAULT VALUES'); + } + } + + this._visitedReplace = false; + + return result; +}; + Postgres.prototype.visitUpdate = function(update) { // don't auto-generate from clause var params = []; From 2294510243e1286f8c63fead5b98abad7dc7d8ce Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 12:26:08 +0100 Subject: [PATCH 03/10] Make sure REPLACE doesn't get prepended with SELECT * --- lib/dialect/postgres.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index c93de8db..82e34004 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -658,6 +658,7 @@ Postgres.prototype.visitQuery = function(queryNode) { break; case "INDEXES": case "INSERT": + case "REPLACE": case "UPDATE": case "CREATE": case "DROP": From ed0dfef813f81df595bf9bb3c7f6a1f737f31fd8 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 12:50:11 +0100 Subject: [PATCH 04/10] Make sure REPLACE doesn't use table.column syntax --- lib/dialect/postgres.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index 82e34004..81686dbe 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -760,7 +760,7 @@ Postgres.prototype.visitTable = function(tableNode) { Postgres.prototype.visitColumn = function(columnNode) { var table = columnNode.table; - var inInsertUpdateClause = this._visitedInsert || this._visitingUpdateTargetColumn; + var inInsertUpdateClause = this._visitedInsert || this._visitedReplace || this._visitingUpdateTargetColumn; var inDdlClause = this._visitingAddColumn || this._visitingAlter || this._visitingCreate; var inSelectClause = this.visitingReturning || From ba33cace4ac9b47c4798c51e1e31a5a70af5a7ba Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 12:59:09 +0100 Subject: [PATCH 05/10] REPLACE throws on unsupported dialects --- lib/dialect/postgres.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index 81686dbe..53486970 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -268,6 +268,11 @@ Postgres.prototype.visitInsert = function(insert) { }; Postgres.prototype.visitReplace = function(replace) { + var dialect = this._queryNode.sql.dialectName; + if (['mysql', 'sqlite'].indexOf(dialect) === -1) { + throw new Error(dialect + ' does not support REPLACE.'); + } + var self = this; // don't use table.column for replaces this._visitedReplace = true; From 248c7226c4db5faf75fdf365780b4755a2fe9383 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 13:54:11 +0100 Subject: [PATCH 06/10] Move replace to dialect prototypes --- lib/dialect/mssql.js | 4 ++++ lib/dialect/mysql.js | 33 +++++++++++++++++++++++++++++++++ lib/dialect/oracle.js | 6 +++++- lib/dialect/postgres.js | 36 +----------------------------------- lib/dialect/sqlite.js | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/lib/dialect/mssql.js b/lib/dialect/mssql.js index 3606f5b6..488f7fa2 100644 --- a/lib/dialect/mssql.js +++ b/lib/dialect/mssql.js @@ -30,6 +30,10 @@ Mssql.prototype._quoteCharacter = '['; Mssql.prototype._arrayAggFunctionName = ''; +Mssql.prototype.visitReplace = function(replace) { + throw new Error('Mssql does not support REPLACE.'); +}; + Mssql.prototype._getParameterPlaceholder = function(index, value) { if (this.config.questionMarkParameterPlaceholder) return '?'; return '@' + index; diff --git a/lib/dialect/mysql.js b/lib/dialect/mysql.js index b2fc2c26..d2ee9507 100644 --- a/lib/dialect/mysql.js +++ b/lib/dialect/mysql.js @@ -20,6 +20,39 @@ Mysql.prototype._quoteCharacter = '`'; Mysql.prototype._arrayAggFunctionName = 'GROUP_CONCAT'; +Mysql.prototype.visitReplace = function(replace) { + var self = this; + // don't use table.column for replaces + this._visitedReplace = true; + + var result = ['REPLACE']; + result = result.concat(replace.nodes.map(this.visit.bind(this))); + result.push('INTO ' + this.visit(this._queryNode.table.toNode())); + result.push('(' + replace.columns.map(this.visit.bind(this)).join(', ') + ')'); + + var paramNodes = replace.getParameters(); + + if (paramNodes.length > 0) { + var paramText = paramNodes.map(function (paramSet) { + return paramSet.map(function (param) { + return self.visit(param); + }).join(', '); + }).map(function (param) { + return '('+param+')'; + }).join(', '); + + result.push('VALUES', paramText); + + if (result.slice(2, 5).join(' ') === '() VALUES ()') { + result.splice(2, 3, 'DEFAULT VALUES'); + } + } + + this._visitedReplace = false; + + return result; +}; + Mysql.prototype._getParameterPlaceholder = function() { return '?'; }; diff --git a/lib/dialect/oracle.js b/lib/dialect/oracle.js index b1f95eec..d1a62b5f 100644 --- a/lib/dialect/oracle.js +++ b/lib/dialect/oracle.js @@ -15,7 +15,11 @@ var Mssql = require('./mssql'); util.inherits(Oracle, Postgres); -Oracle.prototype._myClass = Oracle; +Oracle.prototype._myClass = Oracle + +Oracle.prototype.visitReplace = function(replace) { + throw new Error('Oracle does not support REPLACE.'); +};; Oracle.prototype._aliasText = ' '; Oracle.prototype._getParameterPlaceholder = function(index, value) { diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index 53486970..078ee9bf 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -268,41 +268,7 @@ Postgres.prototype.visitInsert = function(insert) { }; Postgres.prototype.visitReplace = function(replace) { - var dialect = this._queryNode.sql.dialectName; - if (['mysql', 'sqlite'].indexOf(dialect) === -1) { - throw new Error(dialect + ' does not support REPLACE.'); - } - - var self = this; - // don't use table.column for replaces - this._visitedReplace = true; - - var result = ['REPLACE']; - result = result.concat(replace.nodes.map(this.visit.bind(this))); - result.push('INTO ' + this.visit(this._queryNode.table.toNode())); - result.push('(' + replace.columns.map(this.visit.bind(this)).join(', ') + ')'); - - var paramNodes = replace.getParameters(); - - if (paramNodes.length > 0) { - var paramText = paramNodes.map(function (paramSet) { - return paramSet.map(function (param) { - return self.visit(param); - }).join(', '); - }).map(function (param) { - return '('+param+')'; - }).join(', '); - - result.push('VALUES', paramText); - - if (result.slice(2, 5).join(' ') === '() VALUES ()') { - result.splice(2, 3, 'DEFAULT VALUES'); - } - } - - this._visitedReplace = false; - - return result; + throw new Error('Postgres does not support REPLACE.'); }; Postgres.prototype.visitUpdate = function(update) { diff --git a/lib/dialect/sqlite.js b/lib/dialect/sqlite.js index af2c3ef4..60d46997 100644 --- a/lib/dialect/sqlite.js +++ b/lib/dialect/sqlite.js @@ -19,6 +19,39 @@ Sqlite.prototype._myClass = Sqlite; Sqlite.prototype._arrayAggFunctionName = 'GROUP_CONCAT'; +Sqlite.prototype.visitReplace = function(replace) { + var self = this; + // don't use table.column for replaces + this._visitedReplace = true; + + var result = ['REPLACE']; + result = result.concat(replace.nodes.map(this.visit.bind(this))); + result.push('INTO ' + this.visit(this._queryNode.table.toNode())); + result.push('(' + replace.columns.map(this.visit.bind(this)).join(', ') + ')'); + + var paramNodes = replace.getParameters(); + + if (paramNodes.length > 0) { + var paramText = paramNodes.map(function (paramSet) { + return paramSet.map(function (param) { + return self.visit(param); + }).join(', '); + }).map(function (param) { + return '('+param+')'; + }).join(', '); + + result.push('VALUES', paramText); + + if (result.slice(2, 5).join(' ') === '() VALUES ()') { + result.splice(2, 3, 'DEFAULT VALUES'); + } + } + + this._visitedReplace = false; + + return result; +}; + Sqlite.prototype._getParameterValue = function(value) { if (Buffer.isBuffer(value)) { value = 'x' + this._getParameterValue(value.toString('hex')); From 1207801feecec298745e2396026e1823ed5a5544 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 13:59:41 +0100 Subject: [PATCH 07/10] Copy MySQL insert syntax for repalce --- lib/dialect/mysql.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/dialect/mysql.js b/lib/dialect/mysql.js index d2ee9507..0a63f3be 100644 --- a/lib/dialect/mysql.js +++ b/lib/dialect/mysql.js @@ -50,6 +50,9 @@ Mysql.prototype.visitReplace = function(replace) { this._visitedReplace = false; + if (result[2] === 'DEFAULT VALUES') { + result[2] = '() VALUES ()'; + } return result; }; From 94ee6c0b92a7b64b58a64c309d7f9800579465d0 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 14:18:23 +0100 Subject: [PATCH 08/10] Run insert tests against replace --- test/dialects/replace-tests.js | 1000 ++++++++++++++++++++++++++++++++ 1 file changed, 1000 insertions(+) create mode 100644 test/dialects/replace-tests.js diff --git a/test/dialects/replace-tests.js b/test/dialects/replace-tests.js new file mode 100644 index 00000000..72af0e4b --- /dev/null +++ b/test/dialects/replace-tests.js @@ -0,0 +1,1000 @@ +'use strict'; + +var Harness = require('./support'); +var post = Harness.definePostTable(); +var user = Harness.defineUserTable(); +var contentTable = Harness.defineContentTable(); +var customerAliasTable = Harness.defineCustomerAliasTable(); + +var arrayTable = require('../../lib/table').define({ + name: 'arraytest', + columns: ['id', 'numbers'] +}); + +Harness.test({ + query: post.replace(post.content.value('test'), post.userId.value(1)), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (\'test\', 1)' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (\'test\', 1)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 1] +}); + +Harness.test({ + query: post.replace(post.content.value('whoah')), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content") VALUES ($1)', + string: 'REPLACE INTO "post" ("content") VALUES (\'whoah\')' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`) VALUES (?)', + string: 'REPLACE INTO `post` (`content`) VALUES (\'whoah\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['whoah'] +}); + +Harness.test({ + query: post.replace({length: 0}), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("length") VALUES ($1)', + string: 'REPLACE INTO "post" ("length") VALUES (0)' + }, + mysql: { + text : 'REPLACE INTO `post` (`length`) VALUES (?)', + string: 'REPLACE INTO `post` (`length`) VALUES (0)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [0] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (\'test\', 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2] +}); + +Harness.test({ + query: post.replace({ + content: post.sql.functions.LOWER('TEST'), + userId: 2 + }), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES (LOWER($1), $2)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (LOWER(\'TEST\'), 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['TEST', 2] +}); + +// allow bulk replace +Harness.test({ + query: post.replace([{ + content: 'whoah' + }, { + content: 'hey' + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content") VALUES ($1), ($2)', + string: 'REPLACE INTO "post" ("content") VALUES (\'whoah\'), (\'hey\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['whoah', 'hey'] +}); + +Harness.test({ + query: post.replace([{ + content: 'whoah', + userId: 1 + }, { + content: 'hey', + userId: 2 + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2), ($3, $4)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (\'whoah\', 1), (\'hey\', 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['whoah', 1, 'hey', 2] +}); + +// consistent order +Harness.test({ + query: post.replace([{ + content: 'whoah', + userId: 1 + }, { + userId: 2, + content: 'hey' + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2), ($3, $4)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (\'whoah\', 1), (\'hey\', 2)' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?), (?, ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (\'whoah\', 1), (\'hey\', 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['whoah', 1, 'hey', 2] +}); + +Harness.test({ + query: post.replace({}), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" DEFAULT VALUES', + string: 'REPLACE INTO "post" DEFAULT VALUES' + }, + mysql: { + text : 'REPLACE INTO `post` () VALUES ()', + string: 'REPLACE INTO `post` () VALUES ()' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace({}).returning('*'), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace({}).returning(post.star()), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace({}).returning(post.id), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace({}).returning(post.id, post.content), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace({}).returning([post.id, post.content]), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +// handle missing columns +Harness.test({ + query: post.replace([{ + content: 'whoah', + userId: 1 + }, { + content: 'hey' + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'Sqlite requires the same number of columns in each replace row', + throws: true + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?), (?, DEFAULT)', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (\'whoah\', 1), (\'hey\', DEFAULT)', + params: ['whoah', 1, 'hey'] + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, +}); + +Harness.test({ + query: post.replace([{ + userId: 1 + }, { + content: 'hey', + userId: 2 + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'Sqlite requires the same number of columns in each replace row', + throws: true + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`, `content`) VALUES (?, DEFAULT), (?, ?)', + string: 'REPLACE INTO `post` (`userId`, `content`) VALUES (1, DEFAULT), (2, \'hey\')', + params: [1, 2, 'hey'] + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, +}); + +Harness.test({ + query: post.replace(post.content, post.userId) + .select('\'test\'', user.id).from(user).where(user.name.like('A%')), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") SELECT \'test\', "user"."id" FROM "user" WHERE ("user"."name" LIKE $1)', + string: 'REPLACE INTO "post" ("content", "userId") SELECT \'test\', "user"."id" FROM "user" WHERE ("user"."name" LIKE \'A%\')' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) SELECT \'test\', `user`.`id` FROM `user` WHERE (`user`.`name` LIKE ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) SELECT \'test\', `user`.`id` FROM `user` WHERE (`user`.`name` LIKE \'A%\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['A%'] +}); + +Harness.test({ + query: post.replace([post.content, post.userId]) + .select('\'test\'', user.id).from(user).where(user.name.like('A%')), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") SELECT \'test\', "user"."id" FROM "user" WHERE ("user"."name" LIKE $1)', + string: 'REPLACE INTO "post" ("content", "userId") SELECT \'test\', "user"."id" FROM "user" WHERE ("user"."name" LIKE \'A%\')' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) SELECT \'test\', `user`.`id` FROM `user` WHERE (`user`.`name` LIKE ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) SELECT \'test\', `user`.`id` FROM `user` WHERE (`user`.`name` LIKE \'A%\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['A%'] +}); + +Harness.test({ + query: post.replace(post.userId) + .select(user.id).from(user).where(user.name.like('A%')), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user" WHERE ("user"."name" LIKE $1)', + string: 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user" WHERE ("user"."name" LIKE \'A%\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['A%'] +}); + +Harness.test({ + query: post.replace(post.userId) + .select(post.userId).from(user.join(post).on(user.id.equals(post.userId))).where(post.tags.like('A%')), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "post"."userId" FROM "user" INNER JOIN "post" ON ("user"."id" = "post"."userId") WHERE ("post"."tags" LIKE $1)', + string: 'REPLACE INTO "post" ("userId") SELECT "post"."userId" FROM "user" INNER JOIN "post" ON ("user"."id" = "post"."userId") WHERE ("post"."tags" LIKE \'A%\')' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT `post`.`userId` FROM `user` INNER JOIN `post` ON (`user`.`id` = `post`.`userId`) WHERE (`post`.`tags` LIKE ?)', + string: 'REPLACE INTO `post` (`userId`) SELECT `post`.`userId` FROM `user` INNER JOIN `post` ON (`user`.`id` = `post`.`userId`) WHERE (`post`.`tags` LIKE \'A%\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['A%'] +}); + +Harness.test({ + query: post.replace(post.userId).select(user.id).distinct().from(user), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT DISTINCT "user"."id" FROM "user"', + string: 'REPLACE INTO "post" ("userId") SELECT DISTINCT "user"."id" FROM "user"' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT DISTINCT `user`.`id` FROM `user`', + string: 'REPLACE INTO `post` (`userId`) SELECT DISTINCT `user`.`id` FROM `user`' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +// Binary replaces +Harness.test({ + query: post.replace(post.content.value(new Buffer('test')), post.userId.value(2)), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (x\'74657374\', 2)' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (x\'74657374\', 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [new Buffer('test'), 2] +}); + +Harness.test({ + query: post.replace({ + content: new Buffer('test'), + userId: 2 + }), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content", "userId") VALUES ($1, $2)', + string: 'REPLACE INTO "post" ("content", "userId") VALUES (x\'74657374\', 2)' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?)', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (x\'74657374\', 2)' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [new Buffer('test'), 2] +}); + +Harness.test({ + query: post.replace([{ + content: new Buffer('whoah') + }, { + content: new Buffer('hey') + } + ]), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("content") VALUES ($1), ($2)', + string: 'REPLACE INTO "post" ("content") VALUES (x\'77686f6168\'), (x\'686579\')' + }, + mysql: { + text : 'REPLACE INTO `post` (`content`) VALUES (?), (?)', + string: 'REPLACE INTO `post` (`content`) VALUES (x\'77686f6168\'), (x\'686579\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [new Buffer('whoah'), new Buffer('hey')] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onDuplicate({ + content: 'testupdate', + }), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + text : 'REPLACE INTO `post` (`content`, `userId`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `post`.`content` = ?', + string: 'REPLACE INTO `post` (`content`, `userId`) VALUES (\'test\', 2) ON DUPLICATE KEY UPDATE `post`.`content` = \'testupdate\'' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2, 'testupdate'] +}); + +Harness.test({ + query: customerAliasTable.replace({ + id : 2, + name : 'test' + }).onConflict({ + columns: ['id'], + update: ['name'] + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [2, 'test'] +}); + +Harness.test({ + query: customerAliasTable.replace({ + id : 2, + name : 'test' + }).orIgnore(), + mysql: { + throws: true + }, + sqlite: { + text : 'REPLACE OR IGNORE INTO "customer" ("id", "name") VALUES ($1, $2)', + string: 'REPLACE OR IGNORE INTO "customer" ("id", "name") VALUES (2, \'test\')' + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [2, 'test'] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + columns: ['userId'], + update: ['content'] + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + columns: ['userId','content'], + update: ['content','userId'] + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + columns: ['userId'], + update: ['content'] + }).where(post.userId.equals(2)), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2, 2] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + constraint: 'conc_userId', + update: ['content'] + }).where(post.userId.equals(2)), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2, 2] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + columns: ['userId'], + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2] +}); + +Harness.test({ + query: post.replace({ + content: 'test', + userId: 2 + }).onConflict({ + constraint: 'conc_userId', + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: ['test', 2] +}); + +Harness.test({ + query: contentTable.replace({ + contentId: 20, + text : "something" + }).onConflict({ + columns: ['contentId'], + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [20, "something"] +}); + +Harness.test({ + query: contentTable.replace({ + contentId: 20, + text : "something", + contentPosts : "another thing", + }).onConflict({ + columns: ['contentId'], + update: ['contentPosts'] + }), + mysql: { + throws: true + }, + sqlite: { + throws: true + }, + pg: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [20, "something", "another thing"] +}); + +Harness.test({ + query: post.replace([]), + + mysql: { + text : 'SELECT `post`.* FROM `post` WHERE (1=2)', + string: 'SELECT `post`.* FROM `post` WHERE (1=2)' + }, + params: [] +}); + +Harness.test({ + query: arrayTable.replace(arrayTable.id.value(1), arrayTable.numbers.value([2, 3, 4])), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "arraytest" ("id", "numbers") VALUES ($1, $2)', + string: 'REPLACE INTO "arraytest" ("id", "numbers") VALUES (1, \'[2,3,4]\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + } +}); + +Harness.test({ + query: arrayTable.replace(arrayTable.id.value(1), arrayTable.numbers.value(["one", "two", "three"])), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "arraytest" ("id", "numbers") VALUES ($1, $2)', + string: 'REPLACE INTO "arraytest" ("id", "numbers") VALUES (1, \'["one","two","three"]\')' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + } +}); + +Harness.test({ + query: post.replace(post.userId).select(user.id).from(user), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"', + string: 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`', + string: 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace(post.userId).select(user.id).from(user).onConflict({ + columns: ['userId'], + update: ['content'] + }), + pg: { + throws: true + }, + sqlite: { + throws: true + }, + mysql: { + throws: true + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace(post.userId).add(user.select(user.id)), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"', + string: 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`', + string: 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace(post.userId).add(user.select(user.id).from(user)), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"', + string: 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user"' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`', + string: 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user`' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); + +Harness.test({ + query: post.replace(post.userId).add(user.select(user.id).order(user.id)), + pg: { + throws: true + }, + sqlite: { + text : 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user" ORDER BY "user"."id"', + string: 'REPLACE INTO "post" ("userId") SELECT "user"."id" FROM "user" ORDER BY "user"."id"' + }, + mysql: { + text : 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user` ORDER BY `user`.`id`', + string: 'REPLACE INTO `post` (`userId`) SELECT `user`.`id` FROM `user` ORDER BY `user`.`id`' + }, + mssql: { + throws: true + }, + oracle: { + throws: true + }, + params: [] +}); From ca23f2c0332baddbaaaf785e916cf783003ae8bc Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 14:20:38 +0100 Subject: [PATCH 09/10] Fix misplaced semicolon --- lib/dialect/oracle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dialect/oracle.js b/lib/dialect/oracle.js index d1a62b5f..38267339 100644 --- a/lib/dialect/oracle.js +++ b/lib/dialect/oracle.js @@ -15,11 +15,11 @@ var Mssql = require('./mssql'); util.inherits(Oracle, Postgres); -Oracle.prototype._myClass = Oracle +Oracle.prototype._myClass = Oracle; Oracle.prototype.visitReplace = function(replace) { throw new Error('Oracle does not support REPLACE.'); -};; +}; Oracle.prototype._aliasText = ' '; Oracle.prototype._getParameterPlaceholder = function(index, value) { From d01428a378333a421b8e664a71e9bcdad803b661 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 31 Jul 2017 14:51:15 +0100 Subject: [PATCH 10/10] Treat subquery parenthesis the same on INSERT and REPLACE --- lib/dialect/postgres.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dialect/postgres.js b/lib/dialect/postgres.js index 078ee9bf..2816c901 100644 --- a/lib/dialect/postgres.js +++ b/lib/dialect/postgres.js @@ -1217,7 +1217,7 @@ Postgres.prototype.handleDistinct = function(actions,filters) { function dontParenthesizeSubQuery(parentQuery){ if (!parentQuery) return false; if (parentQuery.nodes.length === 0) return false; - if (parentQuery.nodes[0].type != 'INSERT') return false; + if (['INSERT', 'REPLACE'].indexOf(parentQuery.nodes[0].type) === -1) return false; return true; }