Skip to content

Conversation

@nicoburns
Copy link
Contributor

@nicoburns nicoburns commented Mar 27, 2020

CC: #3186

^ I'll write it up there.

Documentation PR: knex/documentation#260

@sheerlox
Copy link

Hey, thanks for the PR !

Overall LGTM, but it's missing a test case for passing an array of fields into .onConflict.

I could add those if you'd like 😄

@kibertoad
Copy link
Collaborator

This also needs documentation - could you create a PR in https://github.com/knex/documentation?

@sheerlox
Copy link

@nicoburns tell me if you want help on the missing test case / the documentation, I'd be glad to help

@nicoburns
Copy link
Contributor Author

nicoburns commented Mar 28, 2020

I've put in a documentation PR here: knex/documentation#260.

Feedback on the documentation would definitely be helpful. I found it a little tricky to balance being clear and describing all possible combinations of options with keeping the explanation concise. Ideally there should probably be a separate section on upserting with a guide-level explanation that explains things like setting of the schema to make it work (similar to the section on transactions). But thats a much bigger change to the structure of the docs, and could always come later.

Help implementing extra tests would also be helpful. In addition to @SherloxTV's suggestion of a test that covers passing an array of fields into .onConflict, an integration test that covers use of merge with an array of inserted objects if probably a good idea.

Edit: these have now been added. I think the main outstanding task is updating the TypeScript definitions.

}

sql += insertData.columns
.map((column) => this.formatter.wrapString(column.split('.').pop()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why here needed .split('.').pop()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that the column specified for the insert may be of the form tableName.columnName, in which case I only wanted the column name. I guess specifying the table name might not be supported for inserts anyway?

@tsondergaard
Copy link
Contributor

I'm using knex for a project that can use either sqlite, mssql or oracle. For the time being this project would let me use onConflict() with only sqlite. My question is, what would a program look like that would like to use the onConflict() syntax when supported by the current dialect. Specifically what goes in the if-clause in place of hasOnConflict() here?

if (hasOnConflict()) {
    // Use onConfict to upsert
} else {
    // use delete followed by insert
}

// inserts using a single query statement.
insert() {
const sql = QueryCompiler.prototype.insert.call(this);
let sql = QueryCompiler.prototype.insert.call(this);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also apply review fixes from mysql driver?

@kibertoad kibertoad closed this Mar 31, 2020
@kibertoad kibertoad reopened this Mar 31, 2020
sql += ' select ' + blocks.join(' union all select ');

const { onConflict, ignore, merge } = this.single;
if (onConflict && ignore) sql += ' where true' + this._ignore(onConflict);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we setup prettier to force having braces also in one-liner if statements?

Those are pretty error prone when code is modified later on and in merging errors (here is one pretty public example of it https://blog.codecentric.de/en/2014/02/curly-braces/ ).

Also in this case where the first if doesn't have braces and the second else if does hurt my eyes 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally support the curly braces, regardless of the 1st of April post date :)

Copy link
Member

@elhigu elhigu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you 🙇 This is awesome and code looks pretty good!

I couldn't see any errors in the code, but this still needs integration tests to make sure that the generated queries does what they are expected to do also when they are ran against real databases.

Also with mysql if columns are passed to .onConflict() it should throw an error. Silent ignore will just generate an invalid query that doesn't do what user expects. So if user wants to write cross database compatible upsert he should always list all of the columns in .onConflict() and pass some additional option to signal mysql that it is fine (like was already mentioned in the original issue).

@kibertoad kibertoad closed this Apr 12, 2020
@kibertoad kibertoad reopened this Apr 12, 2020
@kibertoad
Copy link
Collaborator

@nicoburns Do you need any help finishing this?


sql += insertData.columns
.map((column) => this.formatter.wrapIdentifier(column))
.map((column) => `${column} = values(${column})`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of MySQL 8.0.20, values(column) is deprecated and they recommend using row aliases (added in 8.0.19):
INSERT INTO t1 (id,val) VALUES (1,15),(2,10) AS new ON DUPLICATE KEY UPDATE val = new.val;

@alubbe
Copy link

alubbe commented Jun 16, 2020

as someone who woud love to see this merged, what are the meaning of the two labels here? One says missing documentation, but there is a PR for it here knex/documentation#260. The other one says missing tests, but there are a few hundred lines of testing code here. What cases specifically would you like to see tested? I do see some merge conflicts against master that need resolving though.

@elhigu
Copy link
Member

elhigu commented Jun 16, 2020

What cases specifically would you like to see tested? I do see some merge conflicts against master that need resolving though.

Integration tests, which verifies that when those queries are ran against real database they behave as expected. In other words all this: #3186 (comment)

Mostly the same queries, whose generation is currently covered by query builder code generation tests.

@Alexsey

This comment has been minimized.

@tsheaff
Copy link

tsheaff commented Oct 28, 2020

Amazing @nicoburns thanks! Looks like it's super close. The ramining failing tests I'm seeing are:

  1) Integration Tests
       oracle | oracledb
         Inserts
           will silently do nothing when multiple inserts are made into a unique column and ignore is specified:
     Error: insert into "upsert_tests" ("email", "name") values (:1, :2) returning "email" into :3 - ORA-00001: unique constraint (SYSTEM.upsert_tests_email_unique) violated
  
  2) Integration Tests
       oracle | oracledb
         Inserts
           will silently do nothing when multiple inserts are made into a composite unique column and ignore is specified:
     Error: insert into "upsert_composite_key_tests" ("email", "name", "org") values (:1, :2, :3) returning "email" into :4 - ORA-00001: unique constraint (SYSTEM.pWLdp/gASIHRY5hXRVs73MBLb1E) violated
  
  3) Integration Tests
       mssql | mssql
         Inserts
           will silently do nothing when multiple inserts are made into a unique column and ignore is specified:
     RequestError: insert into [upsert_tests] ([email], [name]) output inserted.[email] values (@p0, @p1) - Cannot insert duplicate key row in object 'dbo.upsert_tests' with unique index 'upsert_tests_email_unique'. The duplicate key value is (ignoretest@example.com).

  4) Integration Tests
       mssql | mssql
         Inserts
           will silently do nothing when multiple inserts are made into a composite unique column and ignore is specified:
     RequestError: insert into [upsert_composite_key_tests] ([email], [name], [org]) output inserted.[email] values (@p0, @p1, @p2) - Cannot insert duplicate key row in object 'dbo.upsert_composite_key_tests' with unique index 'upsert_composite_key_tests_org_email_unique'. The duplicate key value is (acme-inc, ignoretest@example.com).

So it's specific to mssql and oracle. If it were an issue remaining with Postgres I'd hop on it since I'm an expert in Postgres, but I've worked with mssql and oracle zero in my career so I don't feel comfortable with such an important diff on a DB engine I'm not familiar with.

Anyone from the community who's expressed interest able to hop on and fix these last failures? cc @vazra @ARKKYN @a-tokyo or others?

@nicoburns
Copy link
Contributor Author

nicoburns commented Oct 28, 2020

I've pushed a commit ignoring those tests for oracle and mssql, as those platforms are not currently supported by this feature. Perhaps we ought to have some kind of runtime check that throws an error if someone tries to use this feature with those databases?

For those following this PR, there is also still ongoing work on the corresponding documentation taking place here: knex/documentation#260

@kibertoad
Copy link
Collaborator

Perhaps we ought to have some kind of runtime check that throws an error if someone tries to use this feature with those databases?

Sounds good!

@nicoburns
Copy link
Contributor Author

nicoburns commented Oct 28, 2020

Perhaps we ought to have some kind of runtime check that throws an error if someone tries to use this feature with those databases?

This is now implemented.

Additionally I have created a new "class" (ES5 syntax) OnConflictQueryBuilder which is returned from .onConflict() and wraps the regular query builder. It has two methods: .ignore() and .merge() which modify the main query builder and return it back to you. The end result of this is that the interface for onConflict queries is the same for valid queries, but invalid combinations of .onConflict(), .ignore(), and .merge() are impossible. I also added a .then() method to OnConflictQueryBuilder which throws, to catch the case where someone tries to await an query that ends in a .onConflict() (which is invalid without a matching .ignore() or .merge().

@edd83
Copy link

edd83 commented Oct 31, 2020

🎉 Amazing work @nicoburns, definitely want this feature to Knex, @elhigu anything else to add to this PR? 😄

@kibertoad kibertoad merged commit 8d43019 into knex:master Oct 31, 2020
@tsheaff
Copy link

tsheaff commented Oct 31, 2020

Thanks for all the hard work on this @nicoburns 👏

@kibertoad how long would you expect for it to take to get this master-merged commit into the next official release version?

@kibertoad
Copy link
Collaborator

Will be released as soon as documentation is finalized by @nicoburns

@kibertoad
Copy link
Collaborator

Released in 0.21.10

@vvo
Copy link
Contributor

vvo commented Nov 5, 2020

Thank you so much to the knex contributors for implementing this wonderful feature. Saves a lot of boilerplate code on my side. Congrats on the API too.

@KrisLau
Copy link

KrisLau commented Jun 10, 2024

@kibertoad Someone implemented it for mssql here: #6050. I tested it and it seems to work well at least on my local environment

UPDATE: except for unique composite indexes and tables with triggers

@melroyvandenberg
Copy link

melroyvandenberg commented Oct 16, 2024

It's not documented? https://knexjs.org/guide/query-builder.html#upsert Or can't we use the simple upsert method for Postgres? That would be a shame.

@Naddiseo
Copy link
Contributor

@melroyvandenberg In PG you use .onConflict().merge() https://knexjs.org/guide/query-builder.html#onconflict

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.