piccolo 0.109.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 +2 -3
- piccolo/apps/asgi/commands/templates/app/{_starlite_app.py.jinja → _litestar_app.py.jinja} +8 -8
- piccolo/apps/asgi/commands/templates/app/app.py.jinja +2 -4
- piccolo/apps/asgi/commands/templates/app/home/{_starlite_endpoints.py.jinja → _litestar_endpoints.py.jinja} +2 -1
- piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja +2 -4
- piccolo/apps/asgi/commands/templates/app/home/templates/home.html.jinja_raw +1 -6
- piccolo/apps/playground/commands/run.py +24 -8
- piccolo/columns/column_types.py +2 -2
- piccolo/query/methods/insert.py +61 -5
- piccolo/query/methods/objects.py +2 -0
- piccolo/query/methods/select.py +23 -10
- piccolo/query/mixins.py +245 -7
- piccolo/testing/model_builder.py +9 -7
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/METADATA +2 -2
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/RECORD +22 -24
- tests/table/test_insert.py +397 -1
- tests/table/test_select.py +160 -1
- piccolo/apps/asgi/commands/templates/app/_xpresso_app.py.jinja +0 -113
- piccolo/apps/asgi/commands/templates/app/home/_xpresso_endpoints.py.jinja +0 -20
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/LICENSE +0 -0
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/WHEEL +0 -0
- {piccolo-0.109.0.dist-info → piccolo-0.111.0.dist-info}/entry_points.txt +0 -0
- {piccolo-0.109.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"
|
@@ -10,10 +10,9 @@ from jinja2 import Environment, FileSystemLoader
|
|
10
10
|
|
11
11
|
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates/app/")
|
12
12
|
SERVERS = ["uvicorn", "Hypercorn"]
|
13
|
-
ROUTERS = ["starlette", "fastapi", "blacksheep", "
|
13
|
+
ROUTERS = ["starlette", "fastapi", "blacksheep", "litestar"]
|
14
14
|
ROUTER_DEPENDENCIES = {
|
15
|
-
"
|
16
|
-
"xpresso": ["xpresso==0.43.0", "di==0.72.1"],
|
15
|
+
"litestar": ["litestar==2.0.0a3"],
|
17
16
|
}
|
18
17
|
|
19
18
|
|
@@ -2,13 +2,13 @@ import typing as t
|
|
2
2
|
|
3
3
|
from piccolo.engine import engine_finder
|
4
4
|
from piccolo_admin.endpoints import create_admin
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
5
|
+
from litestar import Litestar, asgi, delete, get, patch, post
|
6
|
+
from litestar.static_files import StaticFilesConfig
|
7
|
+
from litestar.template import TemplateConfig
|
8
|
+
from litestar.contrib.jinja import JinjaTemplateEngine
|
9
|
+
from litestar.contrib.piccolo_orm import PiccoloORMPlugin
|
10
|
+
from litestar.exceptions import NotFoundException
|
11
|
+
from litestar.types import Receive, Scope, Send
|
12
12
|
|
13
13
|
from home.endpoints import home
|
14
14
|
from home.piccolo_app import APP_CONFIG
|
@@ -71,7 +71,7 @@ async def close_database_connection_pool():
|
|
71
71
|
print("Unable to connect to the database")
|
72
72
|
|
73
73
|
|
74
|
-
app =
|
74
|
+
app = Litestar(
|
75
75
|
route_handlers=[
|
76
76
|
admin,
|
77
77
|
home,
|
@@ -4,8 +4,6 @@
|
|
4
4
|
{% include '_starlette_app.py.jinja' %}
|
5
5
|
{% elif router == 'blacksheep' %}
|
6
6
|
{% include '_blacksheep_app.py.jinja' %}
|
7
|
-
{% elif router == '
|
8
|
-
{% include '
|
9
|
-
{% elif router == 'starlite' %}
|
10
|
-
{% include '_starlite_app.py.jinja' %}
|
7
|
+
{% elif router == 'litestar' %}
|
8
|
+
{% include '_litestar_app.py.jinja' %}
|
11
9
|
{% endif %}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
import jinja2
|
4
|
-
from
|
4
|
+
from litestar import MediaType, Request, Response, get
|
5
5
|
|
6
6
|
ENVIRONMENT = jinja2.Environment(
|
7
7
|
loader=jinja2.FileSystemLoader(
|
@@ -19,3 +19,4 @@ def home(request: Request) -> Response:
|
|
19
19
|
media_type=MediaType.HTML,
|
20
20
|
status_code=200,
|
21
21
|
)
|
22
|
+
|
@@ -2,8 +2,6 @@
|
|
2
2
|
{% include '_starlette_endpoints.py.jinja' %}
|
3
3
|
{% elif router == 'blacksheep' %}
|
4
4
|
{% include '_blacksheep_endpoints.py.jinja' %}
|
5
|
-
{% elif router == '
|
6
|
-
{% include '
|
7
|
-
{% elif router == 'starlite' %}
|
8
|
-
{% include '_starlite_endpoints.py.jinja' %}
|
5
|
+
{% elif router == 'litestar' %}
|
6
|
+
{% include '_litestar_endpoints.py.jinja' %}
|
9
7
|
{% endif %}
|
@@ -51,12 +51,7 @@
|
|
51
51
|
<li><a href="/admin/">Admin</a></li>
|
52
52
|
<li><a href="/docs/">Swagger API</a></li>
|
53
53
|
</ul>
|
54
|
-
<h3>
|
55
|
-
<ul>
|
56
|
-
<li><a href="/admin/">Admin</a></li>
|
57
|
-
<li><a href="/docs/">Swagger API</a></li>
|
58
|
-
</ul>
|
59
|
-
<h3>Starlite</h3>
|
54
|
+
<h3>Litestar</h3>
|
60
55
|
<ul>
|
61
56
|
<li><a href="/admin/">Admin</a></li>
|
62
57
|
<li><a href="/schema/swagger">Swagger API</a></li>
|
@@ -136,8 +136,7 @@ def populate():
|
|
136
136
|
"""
|
137
137
|
for _table in reversed(TABLES):
|
138
138
|
try:
|
139
|
-
|
140
|
-
_table.alter().drop_table().run_sync()
|
139
|
+
_table.alter().drop_table(if_exists=True).run_sync()
|
141
140
|
except Exception as e:
|
142
141
|
print(e)
|
143
142
|
|
@@ -180,13 +179,30 @@ def populate():
|
|
180
179
|
ticket = Ticket(concert=concert.id, price=Decimal("50.0"))
|
181
180
|
ticket.save().run_sync()
|
182
181
|
|
183
|
-
|
184
|
-
|
182
|
+
DiscountCode.insert(
|
183
|
+
*[DiscountCode({DiscountCode.code: uuid.uuid4()}) for _ in range(5)]
|
184
|
+
).run_sync()
|
185
185
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
186
|
+
RecordingStudio.insert(
|
187
|
+
RecordingStudio(
|
188
|
+
{
|
189
|
+
RecordingStudio.name: "Abbey Road",
|
190
|
+
RecordingStudio.facilities: {
|
191
|
+
"restaurant": True,
|
192
|
+
"mixing_desk": True,
|
193
|
+
},
|
194
|
+
}
|
195
|
+
),
|
196
|
+
RecordingStudio(
|
197
|
+
{
|
198
|
+
RecordingStudio.name: "Electric Lady",
|
199
|
+
RecordingStudio.facilities: {
|
200
|
+
"restaurant": False,
|
201
|
+
"mixing_desk": True,
|
202
|
+
},
|
203
|
+
},
|
204
|
+
),
|
205
|
+
).run_sync()
|
190
206
|
|
191
207
|
|
192
208
|
def run(
|
piccolo/columns/column_types.py
CHANGED
@@ -1784,7 +1784,7 @@ class ForeignKey(Column):
|
|
1784
1784
|
|
1785
1785
|
:param on_update:
|
1786
1786
|
Determines what the database should do when a row has it's primary key
|
1787
|
-
updated. If set to ``
|
1787
|
+
updated. If set to ``OnUpdate.cascade``, any rows referencing the
|
1788
1788
|
updated row will have their references updated to point to the new
|
1789
1789
|
primary key.
|
1790
1790
|
|
@@ -1800,7 +1800,7 @@ class ForeignKey(Column):
|
|
1800
1800
|
|
1801
1801
|
.. code-block:: python
|
1802
1802
|
|
1803
|
-
from piccolo.columns import
|
1803
|
+
from piccolo.columns import OnUpdate
|
1804
1804
|
|
1805
1805
|
class Band(Table):
|
1806
1806
|
name = ForeignKey(
|
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
@@ -353,8 +353,13 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
353
353
|
self.columns_delegate.columns(*_columns)
|
354
354
|
return self
|
355
355
|
|
356
|
-
def distinct(
|
357
|
-
self.
|
356
|
+
def distinct(
|
357
|
+
self: Self, *, on: t.Optional[t.Sequence[Column]] = None
|
358
|
+
) -> Self:
|
359
|
+
if on is not None and self.engine_type == "sqlite":
|
360
|
+
raise NotImplementedError("SQLite doesn't support DISTINCT ON")
|
361
|
+
|
362
|
+
self.distinct_delegate.distinct(enabled=True, on=on)
|
358
363
|
return self
|
359
364
|
|
360
365
|
def group_by(self: Self, *columns: t.Union[Column, str]) -> Self:
|
@@ -367,6 +372,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
367
372
|
return self
|
368
373
|
|
369
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
|
+
|
370
378
|
self.as_of_delegate.as_of(interval)
|
371
379
|
return self
|
372
380
|
|
@@ -722,17 +730,22 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
|
|
722
730
|
|
723
731
|
#######################################################################
|
724
732
|
|
725
|
-
|
726
|
-
"SELECT DISTINCT" if self.distinct_delegate._distinct else "SELECT"
|
727
|
-
)
|
728
|
-
query = f"{select} {columns_str} FROM {self.table._meta.tablename}"
|
733
|
+
args: t.List[t.Any] = []
|
729
734
|
|
730
|
-
|
731
|
-
query += f" {join}"
|
735
|
+
query = "SELECT"
|
732
736
|
|
733
|
-
|
737
|
+
distinct = self.distinct_delegate._distinct
|
738
|
+
if distinct:
|
739
|
+
if distinct.on:
|
740
|
+
distinct.validate_on(self.order_by_delegate._order_by)
|
734
741
|
|
735
|
-
|
742
|
+
query += "{}"
|
743
|
+
args.append(distinct.querystring)
|
744
|
+
|
745
|
+
query += f" {columns_str} FROM {self.table._meta.tablename}"
|
746
|
+
|
747
|
+
for join in joins:
|
748
|
+
query += f" {join}"
|
736
749
|
|
737
750
|
if self.as_of_delegate._as_of:
|
738
751
|
query += "{}"
|
piccolo/query/mixins.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import collections.abc
|
4
5
|
import itertools
|
5
6
|
import typing as t
|
6
7
|
from dataclasses import dataclass, field
|
7
8
|
from enum import Enum, auto
|
8
9
|
|
10
|
+
from typing_extensions import Literal
|
11
|
+
|
9
12
|
from piccolo.columns import And, Column, Or, Where
|
10
13
|
from piccolo.columns.column_types import ForeignKey
|
11
14
|
from piccolo.custom_types import Combinable
|
@@ -18,6 +21,70 @@ if t.TYPE_CHECKING: # pragma: no cover
|
|
18
21
|
from piccolo.table import Table # noqa
|
19
22
|
|
20
23
|
|
24
|
+
class DistinctOnError(ValueError):
|
25
|
+
"""
|
26
|
+
Raised when ``DISTINCT ON`` queries are malformed.
|
27
|
+
"""
|
28
|
+
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class Distinct:
|
34
|
+
__slots__ = ("enabled", "on")
|
35
|
+
|
36
|
+
enabled: bool
|
37
|
+
on: t.Optional[t.Sequence[Column]]
|
38
|
+
|
39
|
+
@property
|
40
|
+
def querystring(self) -> QueryString:
|
41
|
+
if self.enabled:
|
42
|
+
if self.on:
|
43
|
+
column_names = ", ".join(
|
44
|
+
i._meta.get_full_name(with_alias=False) for i in self.on
|
45
|
+
)
|
46
|
+
return QueryString(f" DISTINCT ON ({column_names})")
|
47
|
+
else:
|
48
|
+
return QueryString(" DISTINCT")
|
49
|
+
else:
|
50
|
+
return QueryString(" ALL")
|
51
|
+
|
52
|
+
def validate_on(self, order_by: OrderBy):
|
53
|
+
"""
|
54
|
+
When using the `on` argument, the first column must match the first
|
55
|
+
order by column.
|
56
|
+
|
57
|
+
:raises DistinctOnError:
|
58
|
+
If the columns don't match.
|
59
|
+
|
60
|
+
"""
|
61
|
+
validated = True
|
62
|
+
|
63
|
+
try:
|
64
|
+
first_order_column = order_by.order_by_items[0].columns[0]
|
65
|
+
except IndexError:
|
66
|
+
validated = False
|
67
|
+
else:
|
68
|
+
if not self.on:
|
69
|
+
validated = False
|
70
|
+
elif isinstance(first_order_column, Column) and not self.on[
|
71
|
+
0
|
72
|
+
]._equals(first_order_column):
|
73
|
+
validated = False
|
74
|
+
|
75
|
+
if not validated:
|
76
|
+
raise DistinctOnError(
|
77
|
+
"The first `order_by` column must match the first column "
|
78
|
+
"passed to `on`."
|
79
|
+
)
|
80
|
+
|
81
|
+
def __str__(self) -> str:
|
82
|
+
return self.querystring.__str__()
|
83
|
+
|
84
|
+
def copy(self) -> Distinct:
|
85
|
+
return self.__class__(enabled=self.enabled, on=self.on)
|
86
|
+
|
87
|
+
|
21
88
|
@dataclass
|
22
89
|
class Limit:
|
23
90
|
__slots__ = ("number",)
|
@@ -259,10 +326,19 @@ class AsOfDelegate:
|
|
259
326
|
@dataclass
|
260
327
|
class DistinctDelegate:
|
261
328
|
|
262
|
-
_distinct:
|
329
|
+
_distinct: Distinct = field(
|
330
|
+
default_factory=lambda: Distinct(enabled=False, on=None)
|
331
|
+
)
|
332
|
+
|
333
|
+
def distinct(
|
334
|
+
self, enabled: bool, on: t.Optional[t.Sequence[Column]] = None
|
335
|
+
):
|
336
|
+
if on and not isinstance(on, collections.abc.Sequence):
|
337
|
+
# Check a sequence is passed in, otherwise the user will get some
|
338
|
+
# unuseful errors later on.
|
339
|
+
raise ValueError("`on` must be a sequence of `Column` instances")
|
263
340
|
|
264
|
-
|
265
|
-
self._distinct = True
|
341
|
+
self._distinct = Distinct(enabled=enabled, on=on)
|
266
342
|
|
267
343
|
|
268
344
|
@dataclass
|
@@ -507,9 +583,10 @@ class OffsetDelegate:
|
|
507
583
|
|
508
584
|
Typically used in conjunction with order_by and limit.
|
509
585
|
|
510
|
-
Example usage
|
586
|
+
Example usage::
|
587
|
+
|
588
|
+
.offset(100)
|
511
589
|
|
512
|
-
.offset(100)
|
513
590
|
"""
|
514
591
|
|
515
592
|
_offset: t.Optional[Offset] = None
|
@@ -539,12 +616,173 @@ class GroupBy:
|
|
539
616
|
@dataclass
|
540
617
|
class GroupByDelegate:
|
541
618
|
"""
|
542
|
-
Used to group results - needed when doing aggregation
|
619
|
+
Used to group results - needed when doing aggregation::
|
620
|
+
|
621
|
+
.group_by(Band.name)
|
543
622
|
|
544
|
-
.group_by(Band.name)
|
545
623
|
"""
|
546
624
|
|
547
625
|
_group_by: t.Optional[GroupBy] = None
|
548
626
|
|
549
627
|
def group_by(self, *columns: Column):
|
550
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
|
+
)
|