diff --git a/lib/context.js b/lib/context.js index 3afbe1a..af1fe98 100644 --- a/lib/context.js +++ b/lib/context.js @@ -62,10 +62,11 @@ class Context { * @param {string} table * @param {Object|Array.} data * @param {Array.|Object} update + * @param {?string} [alias=new] * @return {Promise} */ - upsert(table, data, update) { - const sql = upsertStmt(table, data, update); + upsert(table, data, update, alias = 'new') { + const sql = upsertStmt(table, data, update, alias); return this.exec(sql); } diff --git a/lib/transaction.js b/lib/transaction.js index 1c4db16..fe42bac 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -67,10 +67,11 @@ class Transaction { * @param {string} table * @param {Object|Array.} data * @param {Array.|Object} update + * @param {?string} [alias=new] * @return {Promise} */ - upsert(table, data, update) { - const sql = upsertStmt(table, data, update); + upsert(table, data, update, alias = 'new') { + const sql = upsertStmt(table, data, update, alias); return this.exec(sql).then(({ affectedRows, changedRows }) => ({ affectedRows, changedRows })); } diff --git a/lib/util.js b/lib/util.js index 31ee3bb..ff0d663 100644 --- a/lib/util.js +++ b/lib/util.js @@ -133,14 +133,15 @@ function deleteStmt(table, where) { * @param {string} table * @param {Object|Array.} data * @param {Array.|Object} update + * @param {string} alias * @return {string} */ -function upsertStmt(table, data, update) { +function upsertStmt(table, data, update, alias) { if (!update || typeof update !== 'object') { throw new ImplementationError('Update parameter must be either an object or an array of strings'); } - const sql = insertStmt(table, data) + ' ON DUPLICATE KEY UPDATE '; + const sql = `${insertStmt(table, data)} AS ${escapeId(alias)} ON DUPLICATE KEY UPDATE `; if (!Array.isArray(update) && isIterable(update)) { return ( @@ -152,7 +153,7 @@ function upsertStmt(table, data, update) { } if (Array.isArray(update) && update.every((item) => typeof item === 'string')) { - return sql + update.map((column) => escapeId(column) + ' = VALUES(' + escapeId(column) + ')').join(', '); + return sql + update.map((column) => `${escapeId(column)} = ${escapeId(alias)}.${escapeId(column)}`).join(', '); } return sql; diff --git a/test/integration/transaction.spec.js b/test/integration/transaction.spec.js index 3b2ff0a..5a964f6 100644 --- a/test/integration/transaction.spec.js +++ b/test/integration/transaction.spec.js @@ -175,11 +175,15 @@ describe('transaction', () => { await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY, col1 VARCHAR(32))`); await ctx.transaction(async (trx) => { await trx.insert(table, { id: 1, col1: 'foo' }); - await trx.upsert(table, { id: 1, col1: 'bar' }, { col1: ctx.raw('MD5(col1)') }); + await trx.upsert( + table, + { id: 1, col1: 'bar' }, + { col1: ctx.raw(`CONCAT(${table}.col1, \`new\`.col1)`) } + ); }); - const hash = await ctx.queryOne(`SELECT col1 FROM ${table} WHERE id = 1`); - assert.equal(hash, 'acbd18db4cc2f85cedef654fccc4a4d8'); + const value = await ctx.queryOne(`SELECT col1 FROM ${table} WHERE id = 1`); + assert.equal(value, 'foobar'); }); it('should accept updates as an array', async () => { diff --git a/test/unit/context.spec.js b/test/unit/context.spec.js index 88dd5ed..41d9669 100644 --- a/test/unit/context.spec.js +++ b/test/unit/context.spec.js @@ -334,20 +334,34 @@ describe('context', () => { const [call] = ctx.exec.mock.calls; assert.deepEqual(call.arguments, [ - `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = VALUES(\`col1\`), \`col2\` = VALUES(\`col2\`)` + `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) AS \`new\` ON DUPLICATE KEY UPDATE \`col1\` = \`new\`.\`col1\`, \`col2\` = \`new\`.\`col2\`` ]); }); it('should accept assignment list as an object', async () => { - await ctx.upsert( - 't', - { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }, - { col1: 'foo', col2: ctx.raw(ctx.quoteIdentifier('col2') + ' + 1') } - ); + await ctx.upsert('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }, { col1: 'foo' }); const [call] = ctx.exec.mock.calls; assert.deepEqual(call.arguments, [ - `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = 'foo', \`col2\` = \`col2\` + 1` + `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) AS \`new\` ON DUPLICATE KEY UPDATE \`col1\` = 'foo'` + ]); + }); + + it('must not use alias for raw values', async () => { + await ctx.upsert('t', { id: 1, ts: '2000-01-01 00:00:00' }, { ts: ctx.raw('NOW()') }); + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ + `INSERT INTO \`t\` (\`id\`, \`ts\`) VALUES (1, '2000-01-01 00:00:00') AS \`new\` ON DUPLICATE KEY UPDATE \`ts\` = NOW()` + ]); + }); + + it('should support custom alias', async () => { + await ctx.upsert('t', { id: 1, col1: 'val1' }, ['col1'], 'funkyAlias'); + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ + `INSERT INTO \`t\` (\`id\`, \`col1\`) VALUES (1, 'val1') AS \`funkyAlias\` ON DUPLICATE KEY UPDATE \`col1\` = \`funkyAlias\`.\`col1\`` ]); }); });