-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Implement "UPSERT" (Postgres/MySQL/Sqlite) #3763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Hey, thanks for the PR ! Overall LGTM, but it's missing a test case for passing an array of fields into I could add those if you'd like 😄 |
|
This also needs documentation - could you create a PR in https://github.com/knex/documentation? |
|
@nicoburns tell me if you want help on the missing test case / the documentation, I'd be glad to help |
|
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.
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())) |
There was a problem hiding this comment.
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()?
There was a problem hiding this comment.
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?
|
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); |
There was a problem hiding this comment.
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?
| sql += ' select ' + blocks.join(' union all select '); | ||
|
|
||
| const { onConflict, ignore, merge } = this.single; | ||
| if (onConflict && ignore) sql += ' where true' + this._ignore(onConflict); |
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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 :)
There was a problem hiding this 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).
|
@nicoburns Do you need any help finishing this? |
|
|
||
| sql += insertData.columns | ||
| .map((column) => this.formatter.wrapIdentifier(column)) | ||
| .map((column) => `${column} = values(${column})`) |
There was a problem hiding this comment.
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;
|
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 |
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. |
This comment has been minimized.
This comment has been minimized.
|
Amazing @nicoburns thanks! Looks like it's super close. The ramining failing tests I'm seeing are: 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? |
|
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 |
…ted in merge conflict)
Sounds good! |
This is now implemented. Additionally I have created a new "class" (ES5 syntax) |
|
🎉 Amazing work @nicoburns, definitely want this feature to Knex, @elhigu anything else to add to this PR? 😄 |
|
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? |
|
Will be released as soon as documentation is finalized by @nicoburns |
|
Released in 0.21.10 |
|
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. |
|
@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 |
|
It's not documented? https://knexjs.org/guide/query-builder.html#upsert Or can't we use the simple |
|
@melroyvandenberg In PG you use |
CC: #3186
^ I'll write it up there.
Documentation PR: knex/documentation#260