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 +1 -1
- piccolo/apps/asgi/commands/new.py +1 -1
- piccolo/query/methods/insert.py +61 -5
- piccolo/query/methods/objects.py +2 -0
- piccolo/query/methods/select.py +5 -7
- piccolo/query/mixins.py +168 -4
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/METADATA +1 -1
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/RECORD +14 -14
- tests/table/test_insert.py +397 -1
- tests/table/test_select.py +2 -2
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/LICENSE +0 -0
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/WHEEL +0 -0
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/entry_points.txt +0 -0
- {piccolo-0.110.0.dist-info → piccolo-0.111.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "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
|
15
|
+
"litestar": ["litestar==2.0.0a3"],
|
16
16
|
}
|
17
17
|
|
18
18
|
|
piccolo/query/methods/insert.py
CHANGED
@@ -2,9 +2,16 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import typing as t
|
4
4
|
|
5
|
-
from
|
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
|
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
|
-
|
132
|
+
returning = self.returning_delegate._returning
|
133
|
+
if returning:
|
78
134
|
return [
|
79
135
|
QueryString(
|
80
136
|
"{}{}",
|
81
137
|
querystring,
|
82
|
-
|
138
|
+
returning.querystring,
|
83
139
|
query_type="insert",
|
84
140
|
table=self.table,
|
85
141
|
)
|
piccolo/query/methods/objects.py
CHANGED
@@ -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
|
|
piccolo/query/methods/select.py
CHANGED
@@ -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
|
360
|
-
"
|
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,4 +1,4 @@
|
|
1
|
-
piccolo/__init__.py,sha256=
|
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=
|
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=
|
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=
|
156
|
-
piccolo/query/methods/objects.py,sha256=
|
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=
|
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=
|
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=
|
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.
|
344
|
-
piccolo-0.
|
345
|
-
piccolo-0.
|
346
|
-
piccolo-0.
|
347
|
-
piccolo-0.
|
348
|
-
piccolo-0.
|
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,,
|
tests/table/test_insert.py
CHANGED
@@ -1,8 +1,22 @@
|
|
1
|
+
import sqlite3
|
2
|
+
from unittest import TestCase
|
3
|
+
|
1
4
|
import pytest
|
2
5
|
|
3
|
-
from
|
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
|
+
)
|
tests/table/test_select.py
CHANGED
@@ -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(
|
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
|
-
"
|
1372
|
+
"SQLite doesn't support DISTINCT ON",
|
1373
1373
|
)
|
1374
1374
|
|
1375
1375
|
@engines_only("postgres", "cockroach")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|