piccolo 0.110.0__py3-none-any.whl → 0.111.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
piccolo/__init__.py CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "0.110.0"
1
+ __VERSION__ = "0.111.0"
@@ -12,7 +12,7 @@ TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates/app/")
12
12
  SERVERS = ["uvicorn", "Hypercorn"]
13
13
  ROUTERS = ["starlette", "fastapi", "blacksheep", "litestar"]
14
14
  ROUTER_DEPENDENCIES = {
15
- "litestar": ["litestar>=2.0.0a3"],
15
+ "litestar": ["litestar==2.0.0a3"],
16
16
  }
17
17
 
18
18
 
@@ -2,9 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import typing as t
4
4
 
5
- from piccolo.custom_types import TableInstance
5
+ from typing_extensions import Literal
6
+
7
+ from piccolo.custom_types import Combinable, TableInstance
6
8
  from piccolo.query.base import Query
7
- from piccolo.query.mixins import AddDelegate, ReturningDelegate
9
+ from piccolo.query.mixins import (
10
+ AddDelegate,
11
+ OnConflictAction,
12
+ OnConflictDelegate,
13
+ ReturningDelegate,
14
+ )
8
15
  from piccolo.querystring import QueryString
9
16
 
10
17
  if t.TYPE_CHECKING: # pragma: no cover
@@ -15,7 +22,7 @@ if t.TYPE_CHECKING: # pragma: no cover
15
22
  class Insert(
16
23
  t.Generic[TableInstance], Query[TableInstance, t.List[t.Dict[str, t.Any]]]
17
24
  ):
18
- __slots__ = ("add_delegate", "returning_delegate")
25
+ __slots__ = ("add_delegate", "on_conflict_delegate", "returning_delegate")
19
26
 
20
27
  def __init__(
21
28
  self, table: t.Type[TableInstance], *instances: TableInstance, **kwargs
@@ -23,6 +30,7 @@ class Insert(
23
30
  super().__init__(table, **kwargs)
24
31
  self.add_delegate = AddDelegate()
25
32
  self.returning_delegate = ReturningDelegate()
33
+ self.on_conflict_delegate = OnConflictDelegate()
26
34
  self.add(*instances)
27
35
 
28
36
  ###########################################################################
@@ -36,6 +44,43 @@ class Insert(
36
44
  self.returning_delegate.returning(columns)
37
45
  return self
38
46
 
47
+ def on_conflict(
48
+ self: Self,
49
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None,
50
+ action: t.Union[
51
+ OnConflictAction, Literal["DO NOTHING", "DO UPDATE"]
52
+ ] = OnConflictAction.do_nothing,
53
+ values: t.Optional[
54
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
55
+ ] = None,
56
+ where: t.Optional[Combinable] = None,
57
+ ) -> Self:
58
+ if (
59
+ self.engine_type == "sqlite"
60
+ and self.table._meta.db.get_version_sync() < 3.24
61
+ ):
62
+ raise NotImplementedError(
63
+ "SQLite versions lower than 3.24 don't support ON CONFLICT"
64
+ )
65
+
66
+ if (
67
+ self.engine_type in ("postgres", "cockroach")
68
+ and len(self.on_conflict_delegate._on_conflict.on_conflict_items)
69
+ == 1
70
+ ):
71
+ raise NotImplementedError(
72
+ "Postgres and Cockroach only support a single ON CONFLICT "
73
+ "clause."
74
+ )
75
+
76
+ self.on_conflict_delegate.on_conflict(
77
+ target=target,
78
+ action=action,
79
+ values=values,
80
+ where=where,
81
+ )
82
+ return self
83
+
39
84
  ###########################################################################
40
85
 
41
86
  def _raw_response_callback(self, results):
@@ -70,16 +115,27 @@ class Insert(
70
115
 
71
116
  engine_type = self.engine_type
72
117
 
118
+ on_conflict = self.on_conflict_delegate._on_conflict
119
+ if on_conflict.on_conflict_items:
120
+ querystring = QueryString(
121
+ "{}{}",
122
+ querystring,
123
+ on_conflict.querystring,
124
+ query_type="insert",
125
+ table=self.table,
126
+ )
127
+
73
128
  if engine_type in ("postgres", "cockroach") or (
74
129
  engine_type == "sqlite"
75
130
  and self.table._meta.db.get_version_sync() >= 3.35
76
131
  ):
77
- if self.returning_delegate._returning:
132
+ returning = self.returning_delegate._returning
133
+ if returning:
78
134
  return [
79
135
  QueryString(
80
136
  "{}{}",
81
137
  querystring,
82
- self.returning_delegate._returning.querystring,
138
+ returning.querystring,
83
139
  query_type="insert",
84
140
  table=self.table,
85
141
  )
@@ -230,6 +230,8 @@ class Objects(
230
230
  return self
231
231
 
232
232
  def as_of(self, interval: str = "-1s") -> Objects:
233
+ if self.engine_type != "cockroach":
234
+ raise NotImplementedError("Only CockroachDB supports AS OF")
233
235
  self.as_of_delegate.as_of(interval)
234
236
  return self
235
237
 
@@ -356,13 +356,8 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
356
356
  def distinct(
357
357
  self: Self, *, on: t.Optional[t.Sequence[Column]] = None
358
358
  ) -> Self:
359
- if on is not None and self.engine_type not in (
360
- "postgres",
361
- "cockroach",
362
- ):
363
- raise ValueError(
364
- "Only Postgres and Cockroach supports DISTINCT ON"
365
- )
359
+ if on is not None and self.engine_type == "sqlite":
360
+ raise NotImplementedError("SQLite doesn't support DISTINCT ON")
366
361
 
367
362
  self.distinct_delegate.distinct(enabled=True, on=on)
368
363
  return self
@@ -377,6 +372,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
377
372
  return self
378
373
 
379
374
  def as_of(self: Self, interval: str = "-1s") -> Self:
375
+ if self.engine_type != "cockroach":
376
+ raise NotImplementedError("Only CockroachDB supports AS OF")
377
+
380
378
  self.as_of_delegate.as_of(interval)
381
379
  return self
382
380
 
piccolo/query/mixins.py CHANGED
@@ -7,6 +7,8 @@ import typing as t
7
7
  from dataclasses import dataclass, field
8
8
  from enum import Enum, auto
9
9
 
10
+ from typing_extensions import Literal
11
+
10
12
  from piccolo.columns import And, Column, Or, Where
11
13
  from piccolo.columns.column_types import ForeignKey
12
14
  from piccolo.custom_types import Combinable
@@ -581,9 +583,10 @@ class OffsetDelegate:
581
583
 
582
584
  Typically used in conjunction with order_by and limit.
583
585
 
584
- Example usage:
586
+ Example usage::
587
+
588
+ .offset(100)
585
589
 
586
- .offset(100)
587
590
  """
588
591
 
589
592
  _offset: t.Optional[Offset] = None
@@ -613,12 +616,173 @@ class GroupBy:
613
616
  @dataclass
614
617
  class GroupByDelegate:
615
618
  """
616
- Used to group results - needed when doing aggregation.
619
+ Used to group results - needed when doing aggregation::
620
+
621
+ .group_by(Band.name)
617
622
 
618
- .group_by(Band.name)
619
623
  """
620
624
 
621
625
  _group_by: t.Optional[GroupBy] = None
622
626
 
623
627
  def group_by(self, *columns: Column):
624
628
  self._group_by = GroupBy(columns=columns)
629
+
630
+
631
+ class OnConflictAction(str, Enum):
632
+ """
633
+ Specify which action to take on conflict.
634
+ """
635
+
636
+ do_nothing = "DO NOTHING"
637
+ do_update = "DO UPDATE"
638
+
639
+
640
+ @dataclass
641
+ class OnConflictItem:
642
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None
643
+ action: t.Optional[OnConflictAction] = None
644
+ values: t.Optional[
645
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
646
+ ] = None
647
+ where: t.Optional[Combinable] = None
648
+
649
+ @property
650
+ def target_string(self) -> str:
651
+ target = self.target
652
+ assert target
653
+
654
+ def to_string(value) -> str:
655
+ if isinstance(value, Column):
656
+ return f'"{value._meta.db_column_name}"'
657
+ else:
658
+ raise ValueError("OnConflict.target isn't a valid type")
659
+
660
+ if isinstance(target, str):
661
+ return f'ON CONSTRAINT "{target}"'
662
+ elif isinstance(target, Column):
663
+ return f"({to_string(target)})"
664
+ elif isinstance(target, tuple):
665
+ columns_str = ", ".join([to_string(i) for i in target])
666
+ return f"({columns_str})"
667
+ else:
668
+ raise ValueError("OnConflict.target isn't a valid type")
669
+
670
+ @property
671
+ def action_string(self) -> QueryString:
672
+ action = self.action
673
+ if isinstance(action, OnConflictAction):
674
+ if action == OnConflictAction.do_nothing:
675
+ return QueryString(OnConflictAction.do_nothing.value)
676
+ elif action == OnConflictAction.do_update:
677
+ values = []
678
+ query = f"{OnConflictAction.do_update.value} SET"
679
+
680
+ if not self.values:
681
+ raise ValueError("No values specified for `on conflict`")
682
+
683
+ for value in self.values:
684
+ if isinstance(value, Column):
685
+ column_name = value._meta.db_column_name
686
+ query += f' "{column_name}"=EXCLUDED."{column_name}",'
687
+ elif isinstance(value, tuple):
688
+ column = value[0]
689
+ value_ = value[1]
690
+ if isinstance(column, Column):
691
+ column_name = column._meta.db_column_name
692
+ else:
693
+ raise ValueError("Unsupported column type")
694
+
695
+ query += f' "{column_name}"={{}},'
696
+ values.append(value_)
697
+
698
+ return QueryString(query.rstrip(","), *values)
699
+
700
+ raise ValueError("OnConflict.action isn't a valid type")
701
+
702
+ @property
703
+ def querystring(self) -> QueryString:
704
+ query = " ON CONFLICT"
705
+ values = []
706
+
707
+ if self.target:
708
+ query += f" {self.target_string}"
709
+
710
+ if self.action:
711
+ query += " {}"
712
+ values.append(self.action_string)
713
+
714
+ if self.where:
715
+ query += " WHERE {}"
716
+ values.append(self.where.querystring)
717
+
718
+ return QueryString(query, *values)
719
+
720
+ def __str__(self) -> str:
721
+ return self.querystring.__str__()
722
+
723
+
724
+ @dataclass
725
+ class OnConflict:
726
+ """
727
+ Multiple `ON CONFLICT` statements are allowed - which is why we have this
728
+ parent class.
729
+ """
730
+
731
+ on_conflict_items: t.List[OnConflictItem] = field(default_factory=list)
732
+
733
+ @property
734
+ def querystring(self) -> QueryString:
735
+ query = "".join("{}" for i in self.on_conflict_items)
736
+ return QueryString(
737
+ query, *[i.querystring for i in self.on_conflict_items]
738
+ )
739
+
740
+ def __str__(self) -> str:
741
+ return self.querystring.__str__()
742
+
743
+
744
+ @dataclass
745
+ class OnConflictDelegate:
746
+ """
747
+ Used with insert queries to specify what to do when a query fails due to
748
+ a constraint::
749
+
750
+ .on_conflict(action='DO NOTHING')
751
+
752
+ .on_conflict(action='DO UPDATE', values=[Band.popularity])
753
+
754
+ .on_conflict(action='DO UPDATE', values=[(Band.popularity, 1)])
755
+
756
+ """
757
+
758
+ _on_conflict: OnConflict = field(default_factory=OnConflict)
759
+
760
+ def on_conflict(
761
+ self,
762
+ target: t.Optional[t.Union[str, Column, t.Tuple[Column, ...]]] = None,
763
+ action: t.Union[
764
+ OnConflictAction, Literal["DO NOTHING", "DO UPDATE"]
765
+ ] = OnConflictAction.do_nothing,
766
+ values: t.Optional[
767
+ t.Sequence[t.Union[Column, t.Tuple[Column, t.Any]]]
768
+ ] = None,
769
+ where: t.Optional[Combinable] = None,
770
+ ):
771
+ action_: OnConflictAction
772
+ if isinstance(action, OnConflictAction):
773
+ action_ = action
774
+ elif isinstance(action, str):
775
+ action_ = OnConflictAction(action.upper())
776
+ else:
777
+ raise ValueError("Unrecognised `on conflict` action.")
778
+
779
+ if where and action_ == OnConflictAction.do_nothing:
780
+ raise ValueError(
781
+ "The `where` option can only be used with DO NOTHING."
782
+ )
783
+
784
+ self._on_conflict.on_conflict_items.append(
785
+ OnConflictItem(
786
+ target=target, action=action_, values=values, where=where
787
+ )
788
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 0.110.0
3
+ Version: 0.111.0
4
4
  Summary: A fast, user friendly ORM and query builder which supports asyncio.
5
5
  Home-page: https://github.com/piccolo-orm/piccolo
6
6
  Author: Daniel Townsend
@@ -1,4 +1,4 @@
1
- piccolo/__init__.py,sha256=zz24Vmp-MN5KVpqbveNBFX2-Th4v1ySTLtZmXtwh1cY,24
1
+ piccolo/__init__.py,sha256=FD-yuKtGvNyh8GH2WGzOJQnF7v7XuGXevze_b_yuJ4g,24
2
2
  piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
3
3
  piccolo/main.py,sha256=2W2EXXEr-EN1PG8s8xHIWCvU7t7kT004fBChK9CZhzo,5024
4
4
  piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -16,7 +16,7 @@ piccolo/apps/app/commands/templates/tables.py.jinja,sha256=revzdrvDDwe78VedBKz0z
16
16
  piccolo/apps/asgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  piccolo/apps/asgi/piccolo_app.py,sha256=7VUvqQJbB-ScO0A62S6MiJmQL9F5DS-SdlqlDLbAblE,217
18
18
  piccolo/apps/asgi/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- piccolo/apps/asgi/commands/new.py,sha256=bcK7xnbmCsP5yapjjvrpdZDBPOc9W_Itdz8ycUc0V4E,4100
19
+ piccolo/apps/asgi/commands/new.py,sha256=P64IwftHM3uHkUObl6tVXmBfsTaFon7im6LEgDFwg10,4100
20
20
  piccolo/apps/asgi/commands/templates/app/README.md.jinja,sha256=As3gNEZt9qcRmTVkjCzNtXJ8r4-3g0fCSe7Q-P39ezI,214
21
21
  piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja,sha256=bgAGe0a9nWk0LAqK3VNDhPcKGqg0z8V-eIX2YmMoZLk,3117
22
22
  piccolo/apps/asgi/commands/templates/app/_fastapi_app.py.jinja,sha256=cCmRVAN8gw6zfHBcLI_NxapwN7LGM5QSXM7K94imDh8,2436
@@ -141,7 +141,7 @@ piccolo/engine/postgres.py,sha256=UvGihQeUgg2MniTN5mABlSMPkBgtQQSmx9QE32uv9SA,18
141
141
  piccolo/engine/sqlite.py,sha256=io3fBdXxXjOoSzLgP-HYGKgTDgHIRpmFa7mU6ifbqn8,21915
142
142
  piccolo/query/__init__.py,sha256=WkG78nTz4Ww3rE9Pu5tZI130JMVfGnwyghhu97XFk0w,617
143
143
  piccolo/query/base.py,sha256=BsrzbeuJo7k-KJn4YlRUibxITw_J7xslbgJg0eVFB8s,15124
144
- piccolo/query/mixins.py,sha256=zD13SqgKq613BAcqjSTblT0u9fMyctv_AGS03E7xR0c,16585
144
+ piccolo/query/mixins.py,sha256=N6HAN_A4kd-PC07q3OIzwrkRy3ZwGMtB2xLueYefBSM,21649
145
145
  piccolo/query/proxy.py,sha256=Hg5S6tp1EiKD899eYdDKHscFYucHdKtL3YC2GTcL2Jk,1833
146
146
  piccolo/query/methods/__init__.py,sha256=_PfGUdOd6AsKq1sqXeZUHhESHE-e1cNpwFr8Lyz7QoY,421
147
147
  piccolo/query/methods/alter.py,sha256=gyx4kVF4EiN4sSFjIqcKykxcKB808j1ioa2lKrLdP4Y,14935
@@ -152,11 +152,11 @@ piccolo/query/methods/delete.py,sha256=JkDMkee3LVKtZdAS2AQnNFkDbxcus3xq7kbDAu6Fk
152
152
  piccolo/query/methods/drop_index.py,sha256=SOX5wfm-Tbb5TrN6kaLRVHUWdEhyrmCQwF33JfWdtwE,1043
153
153
  piccolo/query/methods/exists.py,sha256=LAeWpGKEMYZJeNGEcxbucxWDxAjn84jduNz2ZjzukPc,1181
154
154
  piccolo/query/methods/indexes.py,sha256=_Io7vy_yIjPYHIw1ILGhAiWQAEOIe-Lwn3mEELw8mOc,1083
155
- piccolo/query/methods/insert.py,sha256=qmwvPysnrANjubF-F41TtBYKVdy2dHfw_lgD3C_T9Kw,2998
156
- piccolo/query/methods/objects.py,sha256=CAsZjTzOtD6elMqc2nIK_Ra1GKu7sxHBgcDRh9NAes8,11556
155
+ piccolo/query/methods/insert.py,sha256=BwIJb1g0PFqMws060Wn0uSAK1ybLbkK9mKUfOOmzroc,4716
156
+ piccolo/query/methods/objects.py,sha256=i71GHPJZJcRpgM6e69Vk7vVcCuAFokOsMnR5EXbZq1w,11673
157
157
  piccolo/query/methods/raw.py,sha256=VhYpCB52mZk4zqFTsqK5CHKTDGskUjISXTBV7UjohmA,600
158
158
  piccolo/query/methods/refresh.py,sha256=P1Eo_HYU_L7kcGM_cvDDgyLi1boCXY7Pc4tv_eDAzvc,2769
159
- piccolo/query/methods/select.py,sha256=e7BuECHITRwYJ902NvC5ZHG8tirCljeVoGWzpQYxRts,25379
159
+ piccolo/query/methods/select.py,sha256=6LVVNJEIooB0cQItI77rxwURsA88ifTOm1dj-wFlHRY,25406
160
160
  piccolo/query/methods/table_exists.py,sha256=rPY20QNdJI9TvKjGyTPVvGGEuD3bDnQim8K1ZurthmU,1211
161
161
  piccolo/query/methods/update.py,sha256=0hURc7PQU9NX7QQFJ1XgFJvw3nXYIrWUjE-D_7W5JV4,3625
162
162
  piccolo/testing/__init__.py,sha256=pRFSqRInfx95AakOq54atmvqoB-ue073q2aR8u8zR40,83
@@ -303,7 +303,7 @@ tests/table/test_exists.py,sha256=AHvhodkRof7PVd4IDdGQ2nyOj_1Cag1Rpg1H84s4jU0,28
303
303
  tests/table/test_from_dict.py,sha256=I4PMxuzgkgi3-adaw9Gr3u5tQHexc31Vrq7RSPcPcJs,840
304
304
  tests/table/test_indexes.py,sha256=GdlPfLmvM0s2fe-4-2XSqQLYYjwsBu5dzf6o7ZvlPj4,1990
305
305
  tests/table/test_inheritance.py,sha256=s5JIo8hZN7xqOPlZ9EDkkNLo5_kWirsfCJAqaXSHn88,3034
306
- tests/table/test_insert.py,sha256=8G7TvgZIKqKNDJR0-Ck5gTtzQa0c89OeFNaWf9wPpBk,2274
306
+ tests/table/test_insert.py,sha256=LeYNvApZ2T-PZ9fseINLZ6hnrY5F1Axe3QHqidwzbAQ,13668
307
307
  tests/table/test_join.py,sha256=tk2r5OUaay9-4U37aj2-qul1XybchBG3xr-k7K_IQh0,14705
308
308
  tests/table/test_join_on.py,sha256=NhJRg_7_YQ0o2ox5mF330ZaIvmtq09Xl2lfDTwKtUng,2719
309
309
  tests/table/test_metaclass.py,sha256=liJuKArpco1qb3lshSQTwRsqXXZZNgzmFoMDP9r2uHw,2637
@@ -313,7 +313,7 @@ tests/table/test_raw.py,sha256=AxT6qB0bEjVbOz6lmGQ9_IiDuEoVmh5c72gzyiatBwo,1683
313
313
  tests/table/test_ref.py,sha256=eYNRnYHzNMXuMbV3B1ca5EidpIg4500q6hr1ccuVaso,269
314
314
  tests/table/test_refresh.py,sha256=tZktBoUQth3S2_vou5NcKiwOJDFrhF6CIuC7YZ2janw,2694
315
315
  tests/table/test_repr.py,sha256=dKdM0HRygvqjmSPz-l95SJQXQ-O18MHUGrcIzzYKrsQ,411
316
- tests/table/test_select.py,sha256=0BdI_gVHIt2eyjWWojFfoLuwL9h5LzCpUAJQzvPl-Zs,39683
316
+ tests/table/test_select.py,sha256=sjSzJfHaYG8Zrg2jXbp7eNprqXilMzDHDxpM3k0BCFg,39678
317
317
  tests/table/test_str.py,sha256=eztWNULcjARR1fr9X5n4tojhDNgDfatVyNHwuYrzHAo,1731
318
318
  tests/table/test_table_exists.py,sha256=9Qqwbg6Q3OfasSH4FUqD3z99ExvJqpHSu0pYu7aa998,375
319
319
  tests/table/test_update.py,sha256=ZEkIDgQ9PHcAn58dlN4WIHRJm0RENi6AVDsmgLX9Qlw,20342
@@ -340,9 +340,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
340
340
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
341
341
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
342
342
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
343
- piccolo-0.110.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
344
- piccolo-0.110.0.dist-info/METADATA,sha256=zYj2Bfz5piDmp3d4zRoKWGva_r4JNZ7dPVLAEpGWvfw,5113
345
- piccolo-0.110.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
346
- piccolo-0.110.0.dist-info/entry_points.txt,sha256=zYhu-YNtMlh2N_8wptCS8YWKOgc81UPL3Ji5gly8ouc,47
347
- piccolo-0.110.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
348
- piccolo-0.110.0.dist-info/RECORD,,
343
+ piccolo-0.111.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
344
+ piccolo-0.111.0.dist-info/METADATA,sha256=mtWcL-RTGmGGTTQuSJT8wm_KPoQerNk1MHE_8It-4Mo,5113
345
+ piccolo-0.111.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
346
+ piccolo-0.111.0.dist-info/entry_points.txt,sha256=zYhu-YNtMlh2N_8wptCS8YWKOgc81UPL3Ji5gly8ouc,47
347
+ piccolo-0.111.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
348
+ piccolo-0.111.0.dist-info/RECORD,,
@@ -1,8 +1,22 @@
1
+ import sqlite3
2
+ from unittest import TestCase
3
+
1
4
  import pytest
2
5
 
3
- from tests.base import DBTestCase, engine_version_lt, is_running_sqlite
6
+ from piccolo.columns import Integer, Varchar
7
+ from piccolo.query.methods.insert import OnConflictAction
8
+ from piccolo.table import Table
9
+ from piccolo.utils.lazy_loader import LazyLoader
10
+ from tests.base import (
11
+ DBTestCase,
12
+ engine_version_lt,
13
+ engines_only,
14
+ is_running_sqlite,
15
+ )
4
16
  from tests.example_apps.music.tables import Band, Manager
5
17
 
18
+ asyncpg = LazyLoader("asyncpg", globals(), "asyncpg")
19
+
6
20
 
7
21
  class TestInsert(DBTestCase):
8
22
  def test_insert(self):
@@ -76,3 +90,385 @@ class TestInsert(DBTestCase):
76
90
  )
77
91
 
78
92
  self.assertListEqual(response, [{"manager_name": "Maz"}])
93
+
94
+
95
+ @pytest.mark.skipif(
96
+ is_running_sqlite() and engine_version_lt(3.24),
97
+ reason="SQLite version not supported",
98
+ )
99
+ class TestOnConflict(TestCase):
100
+ class Band(Table):
101
+ name = Varchar(unique=True)
102
+ popularity = Integer()
103
+
104
+ def setUp(self) -> None:
105
+ Band = self.Band
106
+ Band.create_table().run_sync()
107
+ self.band = Band({Band.name: "Pythonistas", Band.popularity: 1000})
108
+ self.band.save().run_sync()
109
+
110
+ def tearDown(self) -> None:
111
+ Band = self.Band
112
+ Band.alter().drop_table().run_sync()
113
+
114
+ def test_do_update(self):
115
+ """
116
+ Make sure that `DO UPDATE` works.
117
+ """
118
+ Band = self.Band
119
+
120
+ new_popularity = self.band.popularity + 1000
121
+
122
+ Band.insert(
123
+ Band(name=self.band.name, popularity=new_popularity)
124
+ ).on_conflict(
125
+ target=Band.name,
126
+ action="DO UPDATE",
127
+ values=[Band.popularity],
128
+ ).run_sync()
129
+
130
+ self.assertListEqual(
131
+ Band.select().run_sync(),
132
+ [
133
+ {
134
+ "id": self.band.id,
135
+ "name": self.band.name,
136
+ "popularity": new_popularity, # changed
137
+ }
138
+ ],
139
+ )
140
+
141
+ def test_do_update_tuple_values(self):
142
+ """
143
+ Make sure we can use tuples in ``values``.
144
+ """
145
+ Band = self.Band
146
+
147
+ new_popularity = self.band.popularity + 1000
148
+ new_name = "Rustaceans"
149
+
150
+ Band.insert(
151
+ Band(
152
+ id=self.band.id,
153
+ name=new_name,
154
+ popularity=new_popularity,
155
+ )
156
+ ).on_conflict(
157
+ action="DO UPDATE",
158
+ target=Band.id,
159
+ values=[
160
+ (Band.name, new_name),
161
+ (Band.popularity, new_popularity + 2000),
162
+ ],
163
+ ).run_sync()
164
+
165
+ self.assertListEqual(
166
+ Band.select().run_sync(),
167
+ [
168
+ {
169
+ "id": self.band.id,
170
+ "name": new_name,
171
+ "popularity": new_popularity + 2000,
172
+ }
173
+ ],
174
+ )
175
+
176
+ def test_do_update_no_values(self):
177
+ """
178
+ Make sure that `DO UPDATE` with no `values` raises an exception.
179
+ """
180
+ Band = self.Band
181
+
182
+ new_popularity = self.band.popularity + 1000
183
+
184
+ with self.assertRaises(ValueError) as manager:
185
+ Band.insert(
186
+ Band(name=self.band.name, popularity=new_popularity)
187
+ ).on_conflict(
188
+ target=Band.name,
189
+ action="DO UPDATE",
190
+ ).run_sync()
191
+
192
+ self.assertEqual(
193
+ manager.exception.__str__(),
194
+ "No values specified for `on conflict`",
195
+ )
196
+
197
+ @engines_only("postgres", "cockroach")
198
+ def test_target_tuple(self):
199
+ """
200
+ Make sure that a composite unique constraint can be used as a target.
201
+
202
+ We only run it on Postgres and Cockroach because we use ALTER TABLE
203
+ to add a contraint, which SQLite doesn't support.
204
+ """
205
+ Band = self.Band
206
+
207
+ # Add a composite unique constraint:
208
+ Band.raw(
209
+ "ALTER TABLE band ADD CONSTRAINT id_name_unique UNIQUE (id, name)"
210
+ ).run_sync()
211
+
212
+ Band.insert(
213
+ Band(
214
+ id=self.band.id,
215
+ name=self.band.name,
216
+ popularity=self.band.popularity,
217
+ )
218
+ ).on_conflict(
219
+ target=(Band.id, Band.name),
220
+ action="DO NOTHING",
221
+ ).run_sync()
222
+
223
+ @engines_only("postgres", "cockroach")
224
+ def test_target_string(self):
225
+ """
226
+ Make sure we can explicitly specify the name of target constraint using
227
+ a string.
228
+
229
+ We just test this on Postgres for now, as we have to get the constraint
230
+ name from the database.
231
+ """
232
+ Band = self.Band
233
+
234
+ constraint_name = Band.raw(
235
+ """
236
+ SELECT constraint_name
237
+ FROM information_schema.constraint_column_usage
238
+ WHERE column_name = 'name'
239
+ AND table_name = 'band';
240
+ """
241
+ ).run_sync()[0]["constraint_name"]
242
+
243
+ query = Band.insert(Band(name=self.band.name)).on_conflict(
244
+ target=constraint_name,
245
+ action="DO NOTHING",
246
+ )
247
+ self.assertIn(f'ON CONSTRAINT "{constraint_name}"', query.__str__())
248
+ query.run_sync()
249
+
250
+ def test_violate_non_target(self):
251
+ """
252
+ Make sure that if we specify a target constraint, but violate a
253
+ different constraint, then we still get the error.
254
+ """
255
+ Band = self.Band
256
+
257
+ new_popularity = self.band.popularity + 1000
258
+
259
+ with self.assertRaises(Exception) as manager:
260
+ Band.insert(
261
+ Band(name=self.band.name, popularity=new_popularity)
262
+ ).on_conflict(
263
+ target=Band.id, # Target the primary key instead.
264
+ action="DO UPDATE",
265
+ values=[Band.popularity],
266
+ ).run_sync()
267
+
268
+ if self.Band._meta.db.engine_type in ("postgres", "cockroach"):
269
+ self.assertIsInstance(
270
+ manager.exception, asyncpg.exceptions.UniqueViolationError
271
+ )
272
+ elif self.Band._meta.db.engine_type == "sqlite":
273
+ self.assertIsInstance(manager.exception, sqlite3.IntegrityError)
274
+
275
+ def test_where(self):
276
+ """
277
+ Make sure we can pass in a `where` argument.
278
+ """
279
+ Band = self.Band
280
+
281
+ new_popularity = self.band.popularity + 1000
282
+
283
+ query = Band.insert(
284
+ Band(name=self.band.name, popularity=new_popularity)
285
+ ).on_conflict(
286
+ target=Band.name,
287
+ action="DO UPDATE",
288
+ values=[Band.popularity],
289
+ where=Band.popularity < self.band.popularity,
290
+ )
291
+
292
+ self.assertIn(
293
+ f'WHERE "band"."popularity" < {self.band.popularity}',
294
+ query.__str__(),
295
+ )
296
+
297
+ query.run_sync()
298
+
299
+ def test_do_nothing_where(self):
300
+ """
301
+ Make sure an error is raised if `where` is used with `DO NOTHING`.
302
+ """
303
+ Band = self.Band
304
+
305
+ with self.assertRaises(ValueError) as manager:
306
+ Band.insert(Band()).on_conflict(
307
+ action="DO NOTHING",
308
+ where=Band.popularity < self.band.popularity,
309
+ )
310
+
311
+ self.assertEqual(
312
+ manager.exception.__str__(),
313
+ "The `where` option can only be used with DO NOTHING.",
314
+ )
315
+
316
+ def test_do_nothing(self):
317
+ """
318
+ Make sure that `DO NOTHING` works.
319
+ """
320
+ Band = self.Band
321
+
322
+ new_popularity = self.band.popularity + 1000
323
+
324
+ Band.insert(
325
+ Band(name="Pythonistas", popularity=new_popularity)
326
+ ).on_conflict(action="DO NOTHING").run_sync()
327
+
328
+ self.assertListEqual(
329
+ Band.select().run_sync(),
330
+ [
331
+ {
332
+ "id": self.band.id,
333
+ "name": self.band.name,
334
+ "popularity": self.band.popularity,
335
+ }
336
+ ],
337
+ )
338
+
339
+ @engines_only("sqlite")
340
+ def test_multiple_do_update(self):
341
+ """
342
+ Make sure multiple `ON CONFLICT` clauses work for SQLite.
343
+ """
344
+ Band = self.Band
345
+
346
+ new_popularity = self.band.popularity + 1000
347
+
348
+ # Conflicting with name - should update.
349
+ Band.insert(
350
+ Band(name="Pythonistas", popularity=new_popularity)
351
+ ).on_conflict(action="DO NOTHING", target=Band.id).on_conflict(
352
+ action="DO UPDATE", target=Band.name, values=[Band.popularity]
353
+ ).run_sync()
354
+
355
+ self.assertListEqual(
356
+ Band.select().run_sync(),
357
+ [
358
+ {
359
+ "id": self.band.id,
360
+ "name": self.band.name,
361
+ "popularity": new_popularity, # changed
362
+ }
363
+ ],
364
+ )
365
+
366
+ @engines_only("sqlite")
367
+ def test_multiple_do_nothing(self):
368
+ """
369
+ Make sure multiple `ON CONFLICT` clauses work for SQLite.
370
+ """
371
+ Band = self.Band
372
+
373
+ new_popularity = self.band.popularity + 1000
374
+
375
+ # Conflicting with ID - should be ignored.
376
+ Band.insert(
377
+ Band(
378
+ id=self.band.id,
379
+ name="Pythonistas",
380
+ popularity=new_popularity,
381
+ )
382
+ ).on_conflict(action="DO NOTHING", target=Band.id).on_conflict(
383
+ action="DO UPDATE",
384
+ target=Band.name,
385
+ values=[Band.popularity],
386
+ ).run_sync()
387
+
388
+ self.assertListEqual(
389
+ Band.select().run_sync(),
390
+ [
391
+ {
392
+ "id": self.band.id,
393
+ "name": self.band.name,
394
+ "popularity": self.band.popularity,
395
+ }
396
+ ],
397
+ )
398
+
399
+ @engines_only("postgres", "cockroach")
400
+ def test_mutiple_error(self):
401
+ """
402
+ Postgres and Cockroach don't support multiple `ON CONFLICT` clauses.
403
+ """
404
+ with self.assertRaises(NotImplementedError) as manager:
405
+ Band = self.Band
406
+
407
+ Band.insert(Band()).on_conflict(action="DO NOTHING").on_conflict(
408
+ action="DO UPDATE",
409
+ ).run_sync()
410
+
411
+ assert manager.exception.__str__() == (
412
+ "Postgres and Cockroach only support a single ON CONFLICT clause."
413
+ )
414
+
415
+ def test_all_columns(self):
416
+ """
417
+ We can use ``all_columns`` instead of specifying the ``values``
418
+ manually.
419
+ """
420
+ Band = self.Band
421
+
422
+ new_popularity = self.band.popularity + 1000
423
+ new_name = "Rustaceans"
424
+
425
+ # Conflicting with ID - should be ignored.
426
+ q = Band.insert(
427
+ Band(
428
+ id=self.band.id,
429
+ name=new_name,
430
+ popularity=new_popularity,
431
+ )
432
+ ).on_conflict(
433
+ action="DO UPDATE",
434
+ target=Band.id,
435
+ values=Band.all_columns(),
436
+ )
437
+ q.run_sync()
438
+
439
+ self.assertListEqual(
440
+ Band.select().run_sync(),
441
+ [
442
+ {
443
+ "id": self.band.id,
444
+ "name": new_name,
445
+ "popularity": new_popularity,
446
+ }
447
+ ],
448
+ )
449
+
450
+ def test_enum(self):
451
+ """
452
+ A string literal can be passed in, or an enum, to determine the action.
453
+ Make sure that the enum works.
454
+ """
455
+ Band = self.Band
456
+
457
+ Band.insert(
458
+ Band(
459
+ id=self.band.id,
460
+ name=self.band.name,
461
+ popularity=self.band.popularity,
462
+ )
463
+ ).on_conflict(action=OnConflictAction.do_nothing).run_sync()
464
+
465
+ self.assertListEqual(
466
+ Band.select().run_sync(),
467
+ [
468
+ {
469
+ "id": self.band.id,
470
+ "name": self.band.name,
471
+ "popularity": self.band.popularity,
472
+ }
473
+ ],
474
+ )
@@ -1364,12 +1364,12 @@ class TestDistinctOn(TestCase):
1364
1364
  SQLite doesn't support ``DISTINCT ON``, so a ``ValueError`` should be
1365
1365
  raised.
1366
1366
  """
1367
- with self.assertRaises(ValueError) as manager:
1367
+ with self.assertRaises(NotImplementedError) as manager:
1368
1368
  Album.select().distinct(on=[Album.band])
1369
1369
 
1370
1370
  self.assertEqual(
1371
1371
  manager.exception.__str__(),
1372
- "Only Postgres and Cockroach supports DISTINCT ON",
1372
+ "SQLite doesn't support DISTINCT ON",
1373
1373
  )
1374
1374
 
1375
1375
  @engines_only("postgres", "cockroach")