-
Notifications
You must be signed in to change notification settings - Fork 91
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
On conflict clause / upserts #816
Conversation
Codecov Report
📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more @@ Coverage Diff @@
## master #816 +/- ##
===========================================
- Coverage 91.36% 78.02% -13.35%
===========================================
Files 108 108
Lines 7552 7644 +92
===========================================
- Hits 6900 5964 -936
- Misses 652 1680 +1028
|
@dantownsend What is the syntax for updating multiple values=[(Band.name, "Javas"),(Band.popularity, 1000)] but it doesn't work. How do we write test like this https://github.com/piccolo-orm/piccolo/pull/798/files#diff-fac5af1d6e85e27669c58e982989dfcf8327f312f8b29a272a1188f86ecb7f39R126-R127 or that's not possible at this time? |
@sinisaos My thinking was that the # If there's a conflict, it sets the popularity to 2000:
await Band.insert(
Band(name='Pythonistas', popularity=2000)
).on_conflict(
action='DO UPDATE',
target=[Band.name],
values=[Band.popularity]
)
# If there's a conflict, it sets the popularity to something custom:
await Band.insert(
Band(name='Pythonistas', popularity=2000)
).on_conflict(
action='DO UPDATE',
target=[Band.name],
values=[(Band.popularity, 3000)]
) I haven't had time to fully test this though - so there may be bugs. |
@dantownsend I understand it for one column but how to do it for multiple columns. With the old API we didn't have to set any columns or values because Piccolo did everything for us. For example if we have rows like this [
{'id': 1, 'name': 'Band 1', 'popularity': 1},
{'id': 2, 'name': 'Band 2', 'popularity': 2},
{'id': 3, 'name': 'Band 3', 'popularity': 3}
] we can set one or more columns to update await Band.insert(
Band(id=1, name="Band 1", popularity=111),
Band(id=2, name="Band 2", popularity=222),
Band(id=3, name="Band 3", popularity=333),
on_conflict=OnConflict.do_update,
).run()
# result
[
{'id': 1, 'name': 'Band 1', 'popularity': 111},
{'id': 2, 'name': 'Band 2', 'popularity': 222},
{'id': 3, 'name': 'Band 3', 'popularity': 333}
] or for multiple column ( await Band.insert(
Band(id=1, name="Band 1111", popularity=1111),
Band(id=2, name="Band 2222", popularity=2222),
Band(id=3, name="Band 3333", popularity=3333),
on_conflict=OnConflict.do_update,
).run()
# result
[
{'id': 1, 'name': 'Band 1111', 'popularity': 1111},
{'id': 2, 'name': 'Band 2222', 'popularity': 2222},
{'id': 3, 'name': 'Band 3333', 'popularity': 3333}
] How can we do this with the new API, and if not implemented, please do. Sorry for long comment. |
Should be something like this: await Band.insert(
Band(id=1, name="Band 1111", popularity=1111),
Band(id=2, name="Band 2222", popularity=2222),
Band(id=3, name="Band 3333", popularity=3333)
).on_conflict(
action='DO UPDATE',
target=[Band.id],
values=[Band.name, Band.popularity]
) If there are loads of columns we pass |
I'm sorry, but that doesn't work. Both |
Thanks - sounds like a bug. I'll see what's going on. |
@dantownsend Yes. Missing comma between EXCLUDED columns after UPDATE SET in querystring. This is log |
@sinisaos You're right - that's the problem. I'll refactor it later. |
Co-authored-by: sinisaos <[email protected]>
Co-authored-by: sinisaos <[email protected]>
…o into 252-on-conflict-clause
@dantownsend I'm sorry if I caused problems because I never used Github suggested changes, but it seemed to me that it was easiest to show what changes I think should be made. |
It's good - works well. Thanks for suggesting those changes 👍 |
I've added some docs. Just need to decide on the names of the arguments. It's currently: >>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... target=[Band.name],
... values=[Band.popularity],
... ) The names >>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... constraints=[Band.name],
... update=[Band.popularity],
... ) |
@sinisaos I think it's pretty much done now 🤞 The only thing I know of missing is we're letting the user specify the target using a string. Postgres lets you specify the target using the name of a column, or an https://www.postgresql.org/docs/current/sql-insert.html#index_expression I need to read a bit more about how the |
@@ -1364,12 +1364,12 @@ def test_distinct_on_sqlite(self): | |||
SQLite doesn't support ``DISTINCT ON``, so a ``ValueError`` should be |
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 think it should be NotImplementedError
instead ValueError
.
This works and looks great. Can I suggest two more tests that will cover tuple value as columns and tuple value as columns name in def test_do_update_tuple_values(self):
"""
We can use tuple values in ``values``.
"""
Band = self.Band
new_popularity = self.band.popularity + 1000
new_name = "Rustaceans"
Band.insert(
Band(
id=self.band.id,
name=new_name,
popularity=new_popularity,
)
).on_conflict(
action="DO UPDATE",
targets=[Band.id],
values=[
(Band.name, new_name),
(Band.popularity, new_popularity + 2000),
],
).run_sync()
self.assertListEqual(
Band.select().run_sync(),
[
{
"id": self.band.id,
"name": new_name,
"popularity": new_popularity + 2000,
}
],
)
def test_do_update_tuple_values_string_column_name(self):
"""
We can use string column name in tuple values.
"""
Band = self.Band
new_popularity = self.band.popularity + 1000
new_name = "Rustaceans"
Band.insert(
Band(
id=self.band.id,
name=new_name,
popularity=new_popularity,
)
).on_conflict(
action="DO UPDATE",
targets=[Band.id],
values=[
("name", new_name),
("popularity", new_popularity + 2000),
],
).run_sync()
self.assertListEqual(
Band.select().run_sync(),
[
{
"id": self.band.id,
"name": new_name,
"popularity": new_popularity + 2000,
}
],
) |
Good point - I'll add those. I realised I made a small mistake. When specifying the |
I agree with you and think you should move if self.action:
query += " {}"
values.append(self.action_string)
if self.where:
query += " WHERE {}"
values.append(self.where.querystring) because Cockroach
After that you should also update the test for query = Band.insert(
Band(name=self.band.name, popularity=new_popularity)
).on_conflict(
target=Band.name,
where=Band.popularity < new_popularity,
action="DO UPDATE",
values=[Band.popularity],
)
self.assertIn(
f'WHERE "band"."popularity" < {new_popularity}',
query.__str__(),
) I hope that makes sense. |
@sinisaos Yes, you're right - thanks. I've made those changes. Once the tests pass, I'll release this. There are some minor things missing, but I think this is fine for 99% of use cases. |
Follow up actions in the future:
|
When you release the new Piccolo version, if you want, I can open a PR to do it as we discussed. |
@sinisaos Thanks. My only concern is we might need composite unique constraints to make it work. So the M2M table has two foreign keys, and they're unique together. |
@dantownsend Ok. You're probably right and you know better about these things than I do, so I'll leave it up to you. |
@sinisaos Composite unique constraints is definitely something we need to add soon. I just haven't figured out a great API for it, because most of Piccolo works with column references instead of strings. Could be something like this (using strings): class Band(Table, constraints=[UniqueTogether('name', 'manager')]):
name = Varchar()
manager = ForeignKey() Or this: class Band(Table):
name = Varchar()
manager = ForeignKey()
constraints = [UniqueTogether('name', 'manager')]) This might work ... I'm not sure though: class Band(Table):
name = Varchar()
manager = ForeignKey()
constraints = [UniqueTogether(name, manager)]) I considered something like this, but it's hard to track for migrations: class Band(Table):
name = Varchar()
manager = ForeignKey()
@classmethod
def constraints(cls):
return [UniqueTogether(cls.name, cls.manager)] If you have any ideas / preference let me know. |
@dantownsend If I may ask, why not use this PR #582? |
@sinisaos I think that PR is good. Just considering all the options, as once it's in, the API is very hard to change. |
@dantownsend These are all valid choices and I don't know how difficult it is to implement them, but I like this example the most.
|
@sinisaos Yeah, something like that probably makes more sense than passing the args in like Constraints and schema support are the main things to add next. Will try and go through that PR soon. |
@dantownsend As usual you were right 😄. Unique constraints in class TrackToPlaylist(Table, db=DB):
tracks = ForeignKey(Track)
playlists = ForeignKey(Playlist)
async def constraints():
constraint = "CREATE UNIQUE INDEX unique_tracks_playlists ON track_to_playlist(tracks, playlists)"
await TrackToPlaylist.raw(constraint) and then I just change the insert methods to use async def _run(self):
...
if unsaved:
await rows[0].__class__.insert(*unsaved).on_conflict(
action="DO NOTHING"
).run()
...
return (
await joining_table.insert(*joining_table_rows)
.on_conflict(action="DO NOTHING")
.run()
) and everything works and there are no duplicates in the |
@sinisaos Thanks for checking that - we know it's the right approach then. Do you want to create a PR, adding |
Resolves #252
Based on #798 by @sinisaos with the following changes:
Remaining tasks: