piccolo 1.9.0__py3-none-any.whl → 1.11.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/fixtures/commands/load.py +1 -1
- piccolo/apps/migrations/auto/__init__.py +8 -0
- piccolo/apps/migrations/auto/migration_manager.py +2 -1
- piccolo/apps/migrations/commands/backwards.py +3 -1
- piccolo/apps/migrations/commands/base.py +1 -1
- piccolo/apps/migrations/commands/check.py +1 -1
- piccolo/apps/migrations/commands/clean.py +1 -1
- piccolo/apps/migrations/commands/forwards.py +3 -1
- piccolo/apps/migrations/commands/new.py +4 -2
- piccolo/apps/schema/commands/generate.py +2 -2
- piccolo/apps/shell/commands/run.py +1 -1
- piccolo/columns/column_types.py +56 -39
- piccolo/columns/defaults/base.py +1 -1
- piccolo/columns/defaults/date.py +9 -1
- piccolo/columns/defaults/interval.py +1 -0
- piccolo/columns/defaults/time.py +9 -1
- piccolo/columns/defaults/timestamp.py +1 -0
- piccolo/columns/defaults/uuid.py +1 -1
- piccolo/columns/m2m.py +7 -7
- piccolo/columns/operators/comparison.py +4 -0
- piccolo/conf/apps.py +9 -4
- piccolo/engine/base.py +69 -20
- piccolo/engine/cockroach.py +2 -3
- piccolo/engine/postgres.py +33 -19
- piccolo/engine/sqlite.py +27 -22
- piccolo/query/functions/__init__.py +11 -1
- piccolo/query/functions/datetime.py +260 -0
- piccolo/query/functions/string.py +45 -0
- piccolo/query/methods/create_index.py +1 -1
- piccolo/query/methods/drop_index.py +1 -1
- piccolo/query/methods/objects.py +7 -7
- piccolo/query/methods/select.py +13 -7
- piccolo/query/mixins.py +3 -10
- piccolo/schema.py +18 -11
- piccolo/table.py +22 -21
- piccolo/utils/encoding.py +5 -3
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/METADATA +1 -1
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/RECORD +52 -50
- tests/apps/migrations/auto/integration/test_migrations.py +1 -1
- tests/columns/test_array.py +28 -0
- tests/conf/test_apps.py +1 -1
- tests/engine/test_nested_transaction.py +2 -0
- tests/engine/test_transaction.py +1 -2
- tests/query/functions/test_datetime.py +114 -0
- tests/query/functions/test_string.py +34 -2
- tests/table/test_indexes.py +4 -2
- tests/utils/test_pydantic.py +70 -29
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/LICENSE +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/WHEEL +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.9.0.dist-info → piccolo-1.11.0.dist-info}/top_level.txt +0 -0
piccolo/engine/postgres.py
CHANGED
@@ -4,7 +4,15 @@ import contextvars
|
|
4
4
|
import typing as t
|
5
5
|
from dataclasses import dataclass
|
6
6
|
|
7
|
-
from
|
7
|
+
from typing_extensions import Self
|
8
|
+
|
9
|
+
from piccolo.engine.base import (
|
10
|
+
BaseAtomic,
|
11
|
+
BaseBatch,
|
12
|
+
BaseTransaction,
|
13
|
+
Engine,
|
14
|
+
validate_savepoint_name,
|
15
|
+
)
|
8
16
|
from piccolo.engine.exceptions import TransactionError
|
9
17
|
from piccolo.query.base import DDL, Query
|
10
18
|
from piccolo.querystring import QueryString
|
@@ -18,16 +26,17 @@ if t.TYPE_CHECKING: # pragma: no cover
|
|
18
26
|
from asyncpg.connection import Connection
|
19
27
|
from asyncpg.cursor import Cursor
|
20
28
|
from asyncpg.pool import Pool
|
29
|
+
from asyncpg.transaction import Transaction
|
21
30
|
|
22
31
|
|
23
32
|
@dataclass
|
24
|
-
class AsyncBatch(
|
33
|
+
class AsyncBatch(BaseBatch):
|
25
34
|
connection: Connection
|
26
35
|
query: Query
|
27
36
|
batch_size: int
|
28
37
|
|
29
38
|
# Set internally
|
30
|
-
_transaction = None
|
39
|
+
_transaction: t.Optional[Transaction] = None
|
31
40
|
_cursor: t.Optional[Cursor] = None
|
32
41
|
|
33
42
|
@property
|
@@ -36,20 +45,26 @@ class AsyncBatch(Batch):
|
|
36
45
|
raise ValueError("_cursor not set")
|
37
46
|
return self._cursor
|
38
47
|
|
48
|
+
@property
|
49
|
+
def transaction(self) -> Transaction:
|
50
|
+
if not self._transaction:
|
51
|
+
raise ValueError("The transaction can't be found.")
|
52
|
+
return self._transaction
|
53
|
+
|
39
54
|
async def next(self) -> t.List[t.Dict]:
|
40
55
|
data = await self.cursor.fetch(self.batch_size)
|
41
56
|
return await self.query._process_results(data)
|
42
57
|
|
43
|
-
def __aiter__(self):
|
58
|
+
def __aiter__(self: Self) -> Self:
|
44
59
|
return self
|
45
60
|
|
46
|
-
async def __anext__(self):
|
61
|
+
async def __anext__(self) -> t.List[t.Dict]:
|
47
62
|
response = await self.next()
|
48
63
|
if response == []:
|
49
64
|
raise StopAsyncIteration()
|
50
65
|
return response
|
51
66
|
|
52
|
-
async def __aenter__(self):
|
67
|
+
async def __aenter__(self: Self) -> Self:
|
53
68
|
self._transaction = self.connection.transaction()
|
54
69
|
await self._transaction.start()
|
55
70
|
querystring = self.query.querystrings[0]
|
@@ -60,9 +75,9 @@ class AsyncBatch(Batch):
|
|
60
75
|
|
61
76
|
async def __aexit__(self, exception_type, exception, traceback):
|
62
77
|
if exception:
|
63
|
-
await self.
|
78
|
+
await self.transaction.rollback()
|
64
79
|
else:
|
65
|
-
await self.
|
80
|
+
await self.transaction.commit()
|
66
81
|
|
67
82
|
await self.connection.close()
|
68
83
|
|
@@ -72,7 +87,7 @@ class AsyncBatch(Batch):
|
|
72
87
|
###############################################################################
|
73
88
|
|
74
89
|
|
75
|
-
class Atomic:
|
90
|
+
class Atomic(BaseAtomic):
|
76
91
|
"""
|
77
92
|
This is useful if you want to build up a transaction programatically, by
|
78
93
|
adding queries to it.
|
@@ -140,7 +155,7 @@ class Savepoint:
|
|
140
155
|
)
|
141
156
|
|
142
157
|
|
143
|
-
class PostgresTransaction:
|
158
|
+
class PostgresTransaction(BaseTransaction):
|
144
159
|
"""
|
145
160
|
Used for wrapping queries in a transaction, using a context manager.
|
146
161
|
Currently it's async only.
|
@@ -243,7 +258,7 @@ class PostgresTransaction:
|
|
243
258
|
|
244
259
|
###########################################################################
|
245
260
|
|
246
|
-
async def __aexit__(self, exception_type, exception, traceback):
|
261
|
+
async def __aexit__(self, exception_type, exception, traceback) -> bool:
|
247
262
|
if self._parent:
|
248
263
|
return exception is None
|
249
264
|
|
@@ -269,7 +284,7 @@ class PostgresTransaction:
|
|
269
284
|
###############################################################################
|
270
285
|
|
271
286
|
|
272
|
-
class PostgresEngine(Engine[
|
287
|
+
class PostgresEngine(Engine[PostgresTransaction]):
|
273
288
|
"""
|
274
289
|
Used to connect to PostgreSQL.
|
275
290
|
|
@@ -331,16 +346,10 @@ class PostgresEngine(Engine[t.Optional[PostgresTransaction]]):
|
|
331
346
|
__slots__ = (
|
332
347
|
"config",
|
333
348
|
"extensions",
|
334
|
-
"log_queries",
|
335
|
-
"log_responses",
|
336
349
|
"extra_nodes",
|
337
350
|
"pool",
|
338
|
-
"current_transaction",
|
339
351
|
)
|
340
352
|
|
341
|
-
engine_type = "postgres"
|
342
|
-
min_version_number = 10
|
343
|
-
|
344
353
|
def __init__(
|
345
354
|
self,
|
346
355
|
config: t.Dict[str, t.Any],
|
@@ -362,7 +371,12 @@ class PostgresEngine(Engine[t.Optional[PostgresTransaction]]):
|
|
362
371
|
self.current_transaction = contextvars.ContextVar(
|
363
372
|
f"pg_current_transaction_{database_name}", default=None
|
364
373
|
)
|
365
|
-
super().__init__(
|
374
|
+
super().__init__(
|
375
|
+
engine_type="postgres",
|
376
|
+
log_queries=log_queries,
|
377
|
+
log_responses=log_responses,
|
378
|
+
min_version_number=10,
|
379
|
+
)
|
366
380
|
|
367
381
|
@staticmethod
|
368
382
|
def _parse_raw_version_string(version_string: str) -> float:
|
piccolo/engine/sqlite.py
CHANGED
@@ -11,7 +11,15 @@ from dataclasses import dataclass
|
|
11
11
|
from decimal import Decimal
|
12
12
|
from functools import partial, wraps
|
13
13
|
|
14
|
-
from
|
14
|
+
from typing_extensions import Self
|
15
|
+
|
16
|
+
from piccolo.engine.base import (
|
17
|
+
BaseAtomic,
|
18
|
+
BaseBatch,
|
19
|
+
BaseTransaction,
|
20
|
+
Engine,
|
21
|
+
validate_savepoint_name,
|
22
|
+
)
|
15
23
|
from piccolo.engine.exceptions import TransactionError
|
16
24
|
from piccolo.query.base import DDL, Query
|
17
25
|
from piccolo.querystring import QueryString
|
@@ -309,7 +317,7 @@ for column_name in ("TIMESTAMP", "TIMESTAMPTZ", "DATE", "TIME"):
|
|
309
317
|
|
310
318
|
|
311
319
|
@dataclass
|
312
|
-
class AsyncBatch(
|
320
|
+
class AsyncBatch(BaseBatch):
|
313
321
|
connection: Connection
|
314
322
|
query: Query
|
315
323
|
batch_size: int
|
@@ -327,16 +335,16 @@ class AsyncBatch(Batch):
|
|
327
335
|
data = await self.cursor.fetchmany(self.batch_size)
|
328
336
|
return await self.query._process_results(data)
|
329
337
|
|
330
|
-
def __aiter__(self):
|
338
|
+
def __aiter__(self: Self) -> Self:
|
331
339
|
return self
|
332
340
|
|
333
|
-
async def __anext__(self):
|
341
|
+
async def __anext__(self) -> t.List[t.Dict]:
|
334
342
|
response = await self.next()
|
335
343
|
if response == []:
|
336
344
|
raise StopAsyncIteration()
|
337
345
|
return response
|
338
346
|
|
339
|
-
async def __aenter__(self):
|
347
|
+
async def __aenter__(self: Self) -> Self:
|
340
348
|
querystring = self.query.querystrings[0]
|
341
349
|
template, template_args = querystring.compile_string()
|
342
350
|
|
@@ -344,7 +352,7 @@ class AsyncBatch(Batch):
|
|
344
352
|
return self
|
345
353
|
|
346
354
|
async def __aexit__(self, exception_type, exception, traceback):
|
347
|
-
await self.
|
355
|
+
await self.cursor.close()
|
348
356
|
await self.connection.close()
|
349
357
|
return exception is not None
|
350
358
|
|
@@ -363,7 +371,7 @@ class TransactionType(enum.Enum):
|
|
363
371
|
exclusive = "EXCLUSIVE"
|
364
372
|
|
365
373
|
|
366
|
-
class Atomic:
|
374
|
+
class Atomic(BaseAtomic):
|
367
375
|
"""
|
368
376
|
Usage:
|
369
377
|
|
@@ -384,9 +392,9 @@ class Atomic:
|
|
384
392
|
):
|
385
393
|
self.engine = engine
|
386
394
|
self.transaction_type = transaction_type
|
387
|
-
self.queries: t.List[Query] = []
|
395
|
+
self.queries: t.List[t.Union[Query, DDL]] = []
|
388
396
|
|
389
|
-
def add(self, *query: Query):
|
397
|
+
def add(self, *query: t.Union[Query, DDL]):
|
390
398
|
self.queries += list(query)
|
391
399
|
|
392
400
|
async def run(self):
|
@@ -434,7 +442,7 @@ class Savepoint:
|
|
434
442
|
)
|
435
443
|
|
436
444
|
|
437
|
-
class SQLiteTransaction:
|
445
|
+
class SQLiteTransaction(BaseTransaction):
|
438
446
|
"""
|
439
447
|
Used for wrapping queries in a transaction, using a context manager.
|
440
448
|
Currently it's async only.
|
@@ -534,7 +542,7 @@ class SQLiteTransaction:
|
|
534
542
|
|
535
543
|
###########################################################################
|
536
544
|
|
537
|
-
async def __aexit__(self, exception_type, exception, traceback):
|
545
|
+
async def __aexit__(self, exception_type, exception, traceback) -> bool:
|
538
546
|
if self._parent:
|
539
547
|
return exception is None
|
540
548
|
|
@@ -560,16 +568,8 @@ def dict_factory(cursor, row) -> t.Dict:
|
|
560
568
|
return {col[0]: row[idx] for idx, col in enumerate(cursor.description)}
|
561
569
|
|
562
570
|
|
563
|
-
class SQLiteEngine(Engine[
|
564
|
-
__slots__ = (
|
565
|
-
"connection_kwargs",
|
566
|
-
"current_transaction",
|
567
|
-
"log_queries",
|
568
|
-
"log_responses",
|
569
|
-
)
|
570
|
-
|
571
|
-
engine_type = "sqlite"
|
572
|
-
min_version_number = 3.25
|
571
|
+
class SQLiteEngine(Engine[SQLiteTransaction]):
|
572
|
+
__slots__ = ("connection_kwargs",)
|
573
573
|
|
574
574
|
def __init__(
|
575
575
|
self,
|
@@ -613,7 +613,12 @@ class SQLiteEngine(Engine[t.Optional[SQLiteTransaction]]):
|
|
613
613
|
f"sqlite_current_transaction_{path}", default=None
|
614
614
|
)
|
615
615
|
|
616
|
-
super().__init__(
|
616
|
+
super().__init__(
|
617
|
+
engine_type="sqlite",
|
618
|
+
min_version_number=3.25,
|
619
|
+
log_queries=log_queries,
|
620
|
+
log_responses=log_responses,
|
621
|
+
)
|
617
622
|
|
618
623
|
@property
|
619
624
|
def path(self):
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from .aggregate import Avg, Count, Max, Min, Sum
|
2
|
+
from .datetime import Day, Extract, Hour, Month, Second, Strftime, Year
|
2
3
|
from .math import Abs, Ceil, Floor, Round
|
3
|
-
from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
|
4
|
+
from .string import Concat, Length, Lower, Ltrim, Reverse, Rtrim, Upper
|
4
5
|
from .type_conversion import Cast
|
5
6
|
|
6
7
|
__all__ = (
|
@@ -8,16 +9,25 @@ __all__ = (
|
|
8
9
|
"Avg",
|
9
10
|
"Cast",
|
10
11
|
"Ceil",
|
12
|
+
"Concat",
|
11
13
|
"Count",
|
14
|
+
"Day",
|
15
|
+
"Extract",
|
16
|
+
"Extract",
|
12
17
|
"Floor",
|
18
|
+
"Hour",
|
13
19
|
"Length",
|
14
20
|
"Lower",
|
15
21
|
"Ltrim",
|
16
22
|
"Max",
|
17
23
|
"Min",
|
24
|
+
"Month",
|
18
25
|
"Reverse",
|
19
26
|
"Round",
|
20
27
|
"Rtrim",
|
28
|
+
"Second",
|
29
|
+
"Strftime",
|
21
30
|
"Sum",
|
22
31
|
"Upper",
|
32
|
+
"Year",
|
23
33
|
)
|
@@ -0,0 +1,260 @@
|
|
1
|
+
import typing as t
|
2
|
+
|
3
|
+
from piccolo.columns.base import Column
|
4
|
+
from piccolo.columns.column_types import (
|
5
|
+
Date,
|
6
|
+
Integer,
|
7
|
+
Time,
|
8
|
+
Timestamp,
|
9
|
+
Timestamptz,
|
10
|
+
)
|
11
|
+
from piccolo.querystring import QueryString
|
12
|
+
|
13
|
+
from .type_conversion import Cast
|
14
|
+
|
15
|
+
###############################################################################
|
16
|
+
# Postgres / Cockroach
|
17
|
+
|
18
|
+
ExtractComponent = t.Literal[
|
19
|
+
"century",
|
20
|
+
"day",
|
21
|
+
"decade",
|
22
|
+
"dow",
|
23
|
+
"doy",
|
24
|
+
"epoch",
|
25
|
+
"hour",
|
26
|
+
"isodow",
|
27
|
+
"isoyear",
|
28
|
+
"julian",
|
29
|
+
"microseconds",
|
30
|
+
"millennium",
|
31
|
+
"milliseconds",
|
32
|
+
"minute",
|
33
|
+
"month",
|
34
|
+
"quarter",
|
35
|
+
"second",
|
36
|
+
"timezone",
|
37
|
+
"timezone_hour",
|
38
|
+
"timezone_minute",
|
39
|
+
"week",
|
40
|
+
"year",
|
41
|
+
]
|
42
|
+
|
43
|
+
|
44
|
+
class Extract(QueryString):
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
|
48
|
+
datetime_component: ExtractComponent,
|
49
|
+
alias: t.Optional[str] = None,
|
50
|
+
):
|
51
|
+
"""
|
52
|
+
.. note:: This is for Postgres / Cockroach only.
|
53
|
+
|
54
|
+
Extract a date or time component from a ``Date`` / ``Time`` /
|
55
|
+
``Timestamp`` / ``Timestamptz`` column. For example, getting the month
|
56
|
+
from a timestamp:
|
57
|
+
|
58
|
+
.. code-block:: python
|
59
|
+
|
60
|
+
>>> from piccolo.query.functions import Extract
|
61
|
+
>>> await Concert.select(
|
62
|
+
... Extract(Concert.starts, "month", alias="start_month")
|
63
|
+
... )
|
64
|
+
[{"start_month": 12}]
|
65
|
+
|
66
|
+
:param identifier:
|
67
|
+
Identifies the column.
|
68
|
+
:param datetime_component:
|
69
|
+
The date or time component to extract from the column.
|
70
|
+
|
71
|
+
"""
|
72
|
+
if datetime_component.lower() not in t.get_args(ExtractComponent):
|
73
|
+
raise ValueError("The date time component isn't recognised.")
|
74
|
+
|
75
|
+
super().__init__(
|
76
|
+
f"EXTRACT({datetime_component} FROM {{}})",
|
77
|
+
identifier,
|
78
|
+
alias=alias,
|
79
|
+
)
|
80
|
+
|
81
|
+
|
82
|
+
###############################################################################
|
83
|
+
# SQLite
|
84
|
+
|
85
|
+
|
86
|
+
class Strftime(QueryString):
|
87
|
+
def __init__(
|
88
|
+
self,
|
89
|
+
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
|
90
|
+
datetime_format: str,
|
91
|
+
alias: t.Optional[str] = None,
|
92
|
+
):
|
93
|
+
"""
|
94
|
+
.. note:: This is for SQLite only.
|
95
|
+
|
96
|
+
Format a datetime value. For example:
|
97
|
+
|
98
|
+
.. code-block:: python
|
99
|
+
|
100
|
+
>>> from piccolo.query.functions import Strftime
|
101
|
+
>>> await Concert.select(
|
102
|
+
... Strftime(Concert.starts, "%Y", alias="start_year")
|
103
|
+
... )
|
104
|
+
[{"start_month": "2024"}]
|
105
|
+
|
106
|
+
:param identifier:
|
107
|
+
Identifies the column.
|
108
|
+
:param datetime_format:
|
109
|
+
A string describing the output format (see SQLite's
|
110
|
+
`documentation <https://www.sqlite.org/lang_datefunc.html>`_
|
111
|
+
for more info).
|
112
|
+
|
113
|
+
"""
|
114
|
+
super().__init__(
|
115
|
+
f"strftime('{datetime_format}', {{}})",
|
116
|
+
identifier,
|
117
|
+
alias=alias,
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
###############################################################################
|
122
|
+
# Database agnostic
|
123
|
+
|
124
|
+
|
125
|
+
def _get_engine_type(identifier: t.Union[Column, QueryString]) -> str:
|
126
|
+
if isinstance(identifier, Column):
|
127
|
+
return identifier._meta.engine_type
|
128
|
+
elif isinstance(identifier, QueryString) and (
|
129
|
+
columns := identifier.columns
|
130
|
+
):
|
131
|
+
return columns[0]._meta.engine_type
|
132
|
+
else:
|
133
|
+
raise ValueError("Unable to determine the engine type")
|
134
|
+
|
135
|
+
|
136
|
+
def _extract_component(
|
137
|
+
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
|
138
|
+
sqlite_format: str,
|
139
|
+
postgres_format: ExtractComponent,
|
140
|
+
alias: t.Optional[str],
|
141
|
+
) -> QueryString:
|
142
|
+
engine_type = _get_engine_type(identifier=identifier)
|
143
|
+
|
144
|
+
return Cast(
|
145
|
+
(
|
146
|
+
Strftime(
|
147
|
+
identifier=identifier,
|
148
|
+
datetime_format=sqlite_format,
|
149
|
+
)
|
150
|
+
if engine_type == "sqlite"
|
151
|
+
else Extract(
|
152
|
+
identifier=identifier,
|
153
|
+
datetime_component=postgres_format,
|
154
|
+
)
|
155
|
+
),
|
156
|
+
Integer(),
|
157
|
+
alias=alias,
|
158
|
+
)
|
159
|
+
|
160
|
+
|
161
|
+
def Year(
|
162
|
+
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
|
163
|
+
alias: t.Optional[str] = None,
|
164
|
+
) -> QueryString:
|
165
|
+
"""
|
166
|
+
Extract the year as an integer.
|
167
|
+
"""
|
168
|
+
return _extract_component(
|
169
|
+
identifier=identifier,
|
170
|
+
sqlite_format="%Y",
|
171
|
+
postgres_format="year",
|
172
|
+
alias=alias,
|
173
|
+
)
|
174
|
+
|
175
|
+
|
176
|
+
def Month(
|
177
|
+
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
|
178
|
+
alias: t.Optional[str] = None,
|
179
|
+
) -> QueryString:
|
180
|
+
"""
|
181
|
+
Extract the month as an integer.
|
182
|
+
"""
|
183
|
+
return _extract_component(
|
184
|
+
identifier=identifier,
|
185
|
+
sqlite_format="%m",
|
186
|
+
postgres_format="month",
|
187
|
+
alias=alias,
|
188
|
+
)
|
189
|
+
|
190
|
+
|
191
|
+
def Day(
|
192
|
+
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
|
193
|
+
alias: t.Optional[str] = None,
|
194
|
+
) -> QueryString:
|
195
|
+
"""
|
196
|
+
Extract the day as an integer.
|
197
|
+
"""
|
198
|
+
return _extract_component(
|
199
|
+
identifier=identifier,
|
200
|
+
sqlite_format="%d",
|
201
|
+
postgres_format="day",
|
202
|
+
alias=alias,
|
203
|
+
)
|
204
|
+
|
205
|
+
|
206
|
+
def Hour(
|
207
|
+
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
|
208
|
+
alias: t.Optional[str] = None,
|
209
|
+
) -> QueryString:
|
210
|
+
"""
|
211
|
+
Extract the hour as an integer.
|
212
|
+
"""
|
213
|
+
return _extract_component(
|
214
|
+
identifier=identifier,
|
215
|
+
sqlite_format="%H",
|
216
|
+
postgres_format="hour",
|
217
|
+
alias=alias,
|
218
|
+
)
|
219
|
+
|
220
|
+
|
221
|
+
def Minute(
|
222
|
+
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
|
223
|
+
alias: t.Optional[str] = None,
|
224
|
+
) -> QueryString:
|
225
|
+
"""
|
226
|
+
Extract the minute as an integer.
|
227
|
+
"""
|
228
|
+
return _extract_component(
|
229
|
+
identifier=identifier,
|
230
|
+
sqlite_format="%M",
|
231
|
+
postgres_format="minute",
|
232
|
+
alias=alias,
|
233
|
+
)
|
234
|
+
|
235
|
+
|
236
|
+
def Second(
|
237
|
+
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
|
238
|
+
alias: t.Optional[str] = None,
|
239
|
+
) -> QueryString:
|
240
|
+
"""
|
241
|
+
Extract the second as an integer.
|
242
|
+
"""
|
243
|
+
return _extract_component(
|
244
|
+
identifier=identifier,
|
245
|
+
sqlite_format="%S",
|
246
|
+
postgres_format="second",
|
247
|
+
alias=alias,
|
248
|
+
)
|
249
|
+
|
250
|
+
|
251
|
+
__all__ = (
|
252
|
+
"Extract",
|
253
|
+
"Strftime",
|
254
|
+
"Year",
|
255
|
+
"Month",
|
256
|
+
"Day",
|
257
|
+
"Hour",
|
258
|
+
"Minute",
|
259
|
+
"Second",
|
260
|
+
)
|
@@ -5,6 +5,12 @@ https://www.postgresql.org/docs/current/functions-string.html
|
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
8
|
+
import typing as t
|
9
|
+
|
10
|
+
from piccolo.columns.base import Column
|
11
|
+
from piccolo.columns.column_types import Text, Varchar
|
12
|
+
from piccolo.querystring import QueryString
|
13
|
+
|
8
14
|
from .base import Function
|
9
15
|
|
10
16
|
|
@@ -63,6 +69,44 @@ class Upper(Function):
|
|
63
69
|
function_name = "UPPER"
|
64
70
|
|
65
71
|
|
72
|
+
class Concat(QueryString):
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
*args: t.Union[Column, QueryString, str],
|
76
|
+
alias: t.Optional[str] = None,
|
77
|
+
):
|
78
|
+
"""
|
79
|
+
Concatenate multiple values into a single string.
|
80
|
+
|
81
|
+
.. note::
|
82
|
+
Null values are ignored, so ``null + '!!!'`` returns ``!!!``,
|
83
|
+
not ``null``.
|
84
|
+
|
85
|
+
.. warning::
|
86
|
+
For SQLite, this is only available in version 3.44.0 and above.
|
87
|
+
|
88
|
+
"""
|
89
|
+
if len(args) < 2:
|
90
|
+
raise ValueError("At least two values must be passed in.")
|
91
|
+
|
92
|
+
placeholders = ", ".join("{}" for _ in args)
|
93
|
+
|
94
|
+
processed_args: t.List[t.Union[QueryString, Column]] = []
|
95
|
+
|
96
|
+
for arg in args:
|
97
|
+
if isinstance(arg, str) or (
|
98
|
+
isinstance(arg, Column)
|
99
|
+
and not isinstance(arg, (Varchar, Text))
|
100
|
+
):
|
101
|
+
processed_args.append(QueryString("CAST({} AS TEXT)", arg))
|
102
|
+
else:
|
103
|
+
processed_args.append(arg)
|
104
|
+
|
105
|
+
super().__init__(
|
106
|
+
f"CONCAT({placeholders})", *processed_args, alias=alias
|
107
|
+
)
|
108
|
+
|
109
|
+
|
66
110
|
__all__ = (
|
67
111
|
"Length",
|
68
112
|
"Lower",
|
@@ -70,4 +114,5 @@ __all__ = (
|
|
70
114
|
"Reverse",
|
71
115
|
"Rtrim",
|
72
116
|
"Upper",
|
117
|
+
"Concat",
|
73
118
|
)
|
piccolo/query/methods/objects.py
CHANGED
@@ -5,7 +5,7 @@ import typing as t
|
|
5
5
|
from piccolo.columns.column_types import ForeignKey
|
6
6
|
from piccolo.columns.combination import And, Where
|
7
7
|
from piccolo.custom_types import Combinable, TableInstance
|
8
|
-
from piccolo.engine.base import
|
8
|
+
from piccolo.engine.base import BaseBatch
|
9
9
|
from piccolo.query.base import Query
|
10
10
|
from piccolo.query.methods.select import Select
|
11
11
|
from piccolo.query.mixins import (
|
@@ -268,17 +268,17 @@ class Objects(
|
|
268
268
|
|
269
269
|
###########################################################################
|
270
270
|
|
271
|
-
def first(self
|
271
|
+
def first(self) -> First[TableInstance]:
|
272
272
|
self.limit_delegate.limit(1)
|
273
273
|
return First[TableInstance](query=self)
|
274
274
|
|
275
|
-
def get(self
|
275
|
+
def get(self, where: Combinable) -> Get[TableInstance]:
|
276
276
|
self.where_delegate.where(where)
|
277
277
|
self.limit_delegate.limit(1)
|
278
278
|
return Get[TableInstance](query=First[TableInstance](query=self))
|
279
279
|
|
280
280
|
def get_or_create(
|
281
|
-
self
|
281
|
+
self,
|
282
282
|
where: Combinable,
|
283
283
|
defaults: t.Optional[t.Dict[Column, t.Any]] = None,
|
284
284
|
) -> GetOrCreate[TableInstance]:
|
@@ -288,17 +288,17 @@ class Objects(
|
|
288
288
|
query=self, table_class=self.table, where=where, defaults=defaults
|
289
289
|
)
|
290
290
|
|
291
|
-
def create(self
|
291
|
+
def create(self, **columns: t.Any) -> Create[TableInstance]:
|
292
292
|
return Create[TableInstance](table_class=self.table, columns=columns)
|
293
293
|
|
294
294
|
###########################################################################
|
295
295
|
|
296
296
|
async def batch(
|
297
|
-
self
|
297
|
+
self,
|
298
298
|
batch_size: t.Optional[int] = None,
|
299
299
|
node: t.Optional[str] = None,
|
300
300
|
**kwargs,
|
301
|
-
) ->
|
301
|
+
) -> BaseBatch:
|
302
302
|
if batch_size:
|
303
303
|
kwargs.update(batch_size=batch_size)
|
304
304
|
if node:
|