24

I have an upsert query in PostgreSQL like:

INSERT INTO table
  (id, name)
values
  (1, 'Gabbar')
ON CONFLICT (id) DO UPDATE SET
  name = 'Gabbar'
WHERE
  table.id = 1

I need to use knex to this upsert query. How to go about this?

myusuf
  • 11,810
  • 11
  • 35
  • 50
  • maybe the best way is 2 queries : first select if date does exists, then if not insert your data. – Godev Oct 19 '20 at 15:51

5 Answers5

15

As of knex@v0.21.10+ a new method onConflict was introduced.

Official documentation says:

Implemented for the PostgreSQL, MySQL, and SQLite databases. A modifier for insert queries that specifies alternative behaviour in the case of a conflict. A conflict occurs when a table has a PRIMARY KEY or a UNIQUE index on a column (or a composite index on a set of columns) and a row being inserted has the same value as a row which already exists in the table in those column(s). The default behaviour in case of conflict is to raise an error and abort the query. Using this method you can change this behaviour to either silently ignore the error by using .onConflict().ignore() or to update the existing row with new data (perform an "UPSERT") by using .onConflict().merge().

So in your case, the implementation would be:

knex('table')
  .insert({
    id: id,
    name: name
  })
  .onConflict('id')
  .merge()
Dorad
  • 3,413
  • 2
  • 44
  • 71
12

So I solved this using the following suggestion from Dotnil's answer on Knex Issues Page:

var data = {id: 1, name: 'Gabbar'};
var insert = knex('table').insert(data);
var dataClone = {id: 1, name: 'Gabbar'};

delete dataClone.id;

var update = knex('table').update(dataClone).whereRaw('table.id = ' + data.id);
var query = `${ insert.toString() } ON CONFLICT (id) DO UPDATE SET ${ update.toString().replace(/^update\s.*\sset\s/i, '') }`;

return knex.raw(query)
.then(function(dbRes){
  // stuff
});

Hope this helps someone.

myusuf
  • 11,810
  • 11
  • 35
  • 50
  • From @HasFiveVowels link they talk a lot about how using toString method won't help preventing sql injections. I'm wondering if the formatted string is any different? Otherwise you might consider making a better switch or updating your answer. – ArchNoob Sep 23 '17 at 17:11
  • Will this work for array of objects to be inserted? – Suyash Gulati May 31 '19 at 07:46
8

I've created a function for doing this and described it on the knex github issues page (along with some of the gotchas for dealing with composite unique indices).

const upsert = (params) => {
  const {table, object, constraint} = params;
  const insert = knex(table).insert(object);
  const update = knex.queryBuilder().update(object);
  return knex.raw(`? ON CONFLICT ${constraint} DO ? returning *`, [insert, update]).get('rows').get(0);
};

Example usage:

const objToUpsert = {a:1, b:2, c:3}

upsert({
    table: 'test',
    object: objToUpsert,
    constraint: '(a, b)',
})

A note about composite nullable indices

If you have a composite index (a,b) and b is nullable, then values (1, NULL) and (1, NULL) are considered mutually unique by Postgres (I don't get it either).

mrm
  • 5,001
  • 2
  • 32
  • 30
Tim
  • 6,265
  • 5
  • 29
  • 24
2

Yet another approach I could think of!

exports.upsert = (t, tableName, columnsToRetain, conflictOn) => {
    const insert = knex(tableName)
        .insert(t)
        .toString();
    const update = knex(tableName)
        .update(t)
        .toString();
    const keepValues = columnsToRetain.map((c) => `"${c}"=${tableName}."${c}"`).join(',');
    const conflictColumns = conflictOn.map((c) => `"${c.toString()}"`).join(',');
    let insertOrUpdateQuery = `${insert} ON CONFLICT( ${conflictColumns}) DO ${update}`;
    insertOrUpdateQuery = keepValues ? `${insertOrUpdateQuery}, ${keepValues}` : insertOrUpdateQuery;
    insertOrUpdateQuery = insertOrUpdateQuery.replace(`update "${tableName}"`, 'update');
    insertOrUpdateQuery = insertOrUpdateQuery.replace(`"${tableName}"`, tableName);
    return Promise.resolve(knex.raw(insertOrUpdateQuery));
};
2

very simple.

Adding onto Dorad's answer, you can choose specific columns to upsert using merge keyword.

knex('table')
  .insert({
    id: id,
    name: name
  })
  .onConflict('id')
  .merge(['name']); // put column names inside an array which you want to merge.