piccolo 1.10.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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.10.0"
1
+ __VERSION__ = "1.11.0"
@@ -86,47 +86,38 @@ class ConcatDelegate:
86
86
 
87
87
  def get_querystring(
88
88
  self,
89
- column_name: str,
90
- value: t.Union[str, Varchar, Text],
89
+ column: Column,
90
+ value: t.Union[str, Column, QueryString],
91
91
  reverse: bool = False,
92
92
  ) -> QueryString:
93
- if isinstance(value, (Varchar, Text)):
94
- column: Column = value
93
+ """
94
+ :param reverse:
95
+ By default the value is appended to the column's value. If
96
+ ``reverse=True`` then the value is prepended to the column's
97
+ value instead.
98
+
99
+ """
100
+ if isinstance(value, Column):
95
101
  if len(column._meta.call_chain) > 0:
96
102
  raise ValueError(
97
103
  "Adding values across joins isn't currently supported."
98
104
  )
99
- other_column_name = column._meta.db_column_name
100
- if reverse:
101
- return QueryString(
102
- Concat.template.format(
103
- value_1=other_column_name, value_2=column_name
104
- )
105
- )
106
- else:
107
- return QueryString(
108
- Concat.template.format(
109
- value_1=column_name, value_2=other_column_name
110
- )
111
- )
112
105
  elif isinstance(value, str):
113
- if reverse:
114
- value_1 = QueryString("CAST({} AS text)", value)
115
- return QueryString(
116
- Concat.template.format(value_1="{}", value_2=column_name),
117
- value_1,
118
- )
119
- else:
120
- value_2 = QueryString("CAST({} AS text)", value)
121
- return QueryString(
122
- Concat.template.format(value_1=column_name, value_2="{}"),
123
- value_2,
124
- )
125
- else:
106
+ value = QueryString("CAST({} AS TEXT)", value)
107
+ elif not isinstance(value, QueryString):
126
108
  raise ValueError(
127
- "Only str, Varchar columns, and Text columns can be added."
109
+ "Only str, Column and QueryString values can be added."
128
110
  )
129
111
 
112
+ args = [value, column] if reverse else [column, value]
113
+
114
+ # We use the concat operator instead of the concat function, because
115
+ # this is what we historically used, and they treat null values
116
+ # differently.
117
+ return QueryString(
118
+ Concat.template.format(value_1="{}", value_2="{}"), *args
119
+ )
120
+
130
121
 
131
122
  class MathDelegate:
132
123
  """
@@ -340,12 +331,13 @@ class Varchar(Column):
340
331
 
341
332
  def __add__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
342
333
  return self.concat_delegate.get_querystring(
343
- column_name=self._meta.db_column_name, value=value
334
+ column=self,
335
+ value=value,
344
336
  )
345
337
 
346
338
  def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
347
339
  return self.concat_delegate.get_querystring(
348
- column_name=self._meta.db_column_name,
340
+ column=self,
349
341
  value=value,
350
342
  reverse=True,
351
343
  )
@@ -442,12 +434,13 @@ class Text(Column):
442
434
 
443
435
  def __add__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
444
436
  return self.concat_delegate.get_querystring(
445
- column_name=self._meta.db_column_name, value=value
437
+ column=self,
438
+ value=value,
446
439
  )
447
440
 
448
441
  def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
449
442
  return self.concat_delegate.get_querystring(
450
- column_name=self._meta.db_column_name,
443
+ column=self,
451
444
  value=value,
452
445
  reverse=True,
453
446
  )
@@ -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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.10.0
3
+ Version: 1.11.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=PUFn6Nk_mmFhCKTk02g04j8puY3N7KoIIwdxhzTnVRU,23
1
+ piccolo/__init__.py,sha256=-4wiKExnJa5IxSz9dOFdTv99ibuBl-uX3D-HM6f-_V8,23
2
2
  piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
3
3
  piccolo/main.py,sha256=1VsFV67FWTUikPTysp64Fmgd9QBVa_9wcwKfwj2UCEA,5117
4
4
  piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -117,7 +117,7 @@ piccolo/apps/user/piccolo_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
117
117
  piccolo/columns/__init__.py,sha256=OYhO_n9anMiU9nL-K6ATq9FhAtm8RyMpqYQ7fTVbhxI,1120
118
118
  piccolo/columns/base.py,sha256=sgMiBvq-xLW6_W86g6XZTMc_3cskyeoMF6yIvIlnXsA,32487
119
119
  piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,723
120
- piccolo/columns/column_types.py,sha256=YmltgnWhGLpmuMRimPPVQFmzF5hRDB7K1LaAjcI4Lmc,82364
120
+ piccolo/columns/column_types.py,sha256=gwd93EWIULo5pGcuo7wHZdwvFEfFwtBy660pDBVqixw,81921
121
121
  piccolo/columns/combination.py,sha256=vMXC2dfY7pvnCFhsT71XFVyb4gdQzfRsCMaiduu04Ss,6900
122
122
  piccolo/columns/indexes.py,sha256=NfNok3v_791jgDlN28KmhP9ZCjl6031BXmjxV3ovXJk,372
123
123
  piccolo/columns/m2m.py,sha256=17NY0wU7ta2rUTHYUkeA2HQhTDlJ_lyv9FxqvJiiUbY,14602
@@ -149,11 +149,12 @@ piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
149
149
  piccolo/query/base.py,sha256=G8Mwz0GcHY4Xs5Co9ubCNMI-3orfOsDdRDOnFRws7TU,15212
150
150
  piccolo/query/mixins.py,sha256=EFEFb9It4y1mR6_JXLn139h5M9KgeP750STYy5M4MLs,21951
151
151
  piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
152
- piccolo/query/functions/__init__.py,sha256=e-BEHlGR3JhE2efWG_rmXdURKL4Fa8tjdGmPsvH4kWo,403
152
+ piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
153
153
  piccolo/query/functions/aggregate.py,sha256=OdjDjr_zyD4S9UbrZ2C3V5mz4OT2sIfAFAdTGr4WL54,4248
154
154
  piccolo/query/functions/base.py,sha256=Go2bg2r7GaVoyyX-wTb80WEQmtiU4OFYWQlq9eQ6Zcc,478
155
+ piccolo/query/functions/datetime.py,sha256=6YSpc_MiZK_019KUhCo01Ss_1AjXJ31M61R9-zKmoZs,6251
155
156
  piccolo/query/functions/math.py,sha256=2Wapq0lpXZh77z0uzXUhnOfmUkbkM0xjQ4tiyuCsbiE,661
156
- piccolo/query/functions/string.py,sha256=srxsQJFS6L4gPvFjvuAFQj7QtnCF7X6YoJNKARR2XP0,1236
157
+ piccolo/query/functions/string.py,sha256=X3g_4qomJJCkYOcKcK-zZEqC6qJBrS4VTogPp9Xw4Cs,2506
157
158
  piccolo/query/functions/type_conversion.py,sha256=OYbZc6TEk6b5yTwCMw2rmZ-UiQiUiWZOyxwMLzUjXwE,2583
158
159
  piccolo/query/methods/__init__.py,sha256=tm4gLeV_obDqpgnouVjFbGubbaoJcqm_cbNd4LPo48Q,622
159
160
  piccolo/query/methods/alter.py,sha256=AI9YkJeip2EitrWJN_TDExXhA8HGAG3XuDz1NR-KirQ,16728
@@ -303,9 +304,10 @@ tests/query/test_querystring.py,sha256=QrqyjwUlFlf5LrsJ7DgjCruq811I0UvrDFPud6rfZ
303
304
  tests/query/test_slots.py,sha256=I9ZjAYqAJNSFAWg9UyAqy7bm-Z52KiyQ2C_yHk2qqqI,1010
304
305
  tests/query/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
306
  tests/query/functions/base.py,sha256=RLCzLT7iN_Z5DtIFZqVESTJGES2JKb8VDU25sv5OtN4,811
307
+ tests/query/functions/test_datetime.py,sha256=8GG5ERLq6GM8NqA3J6mycNPfCUMOEICGccyZifiwEqw,2987
306
308
  tests/query/functions/test_functions.py,sha256=510fqRrOrAZ9NyFoZtlF6lIdiiLriWhZ7vvveWZ8rsc,1984
307
309
  tests/query/functions/test_math.py,sha256=Qw2MXqgY_y7vGd0bLtPhWW7HB3tJkot1o-Rh9nCmmBk,1273
308
- tests/query/functions/test_string.py,sha256=7yNkpWNBaIowzXTP_qbmQg-mJZLWrTk0lx2mgY1NIfA,825
310
+ tests/query/functions/test_string.py,sha256=RMojkBUzw1Ikrb3nTa7VjJ4FsKfrjpuHUyxQDA-F5Cs,1800
309
311
  tests/query/functions/test_type_conversion.py,sha256=WeYR9UfJnbidle07-akQ1g9hFCd93qT8xUhDF3c58n4,3235
310
312
  tests/query/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
311
313
  tests/query/mixins/test_columns_delegate.py,sha256=Zw9uaqOEb7kpPQzzO9yz0jhQEeCfoPSjsy-BCLg_8XU,2032
@@ -363,9 +365,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
363
365
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
364
366
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
365
367
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
366
- piccolo-1.10.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
367
- piccolo-1.10.0.dist-info/METADATA,sha256=iOzLZbrVstdxKNx_ifn_M7voMrX0KkQsBcPB39vw0VE,5178
368
- piccolo-1.10.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
369
- piccolo-1.10.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
370
- piccolo-1.10.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
371
- piccolo-1.10.0.dist-info/RECORD,,
368
+ piccolo-1.11.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
369
+ piccolo-1.11.0.dist-info/METADATA,sha256=th4damYz6EsRNzUVjj7Rkn5Eu3jv1LDiCHAHV-N_piE,5178
370
+ piccolo-1.11.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
371
+ piccolo-1.11.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
372
+ piccolo-1.11.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
373
+ piccolo-1.11.0.dist-info/RECORD,,
@@ -0,0 +1,114 @@
1
+ import datetime
2
+
3
+ from piccolo.columns import Timestamp
4
+ from piccolo.query.functions.datetime import (
5
+ Day,
6
+ Extract,
7
+ Hour,
8
+ Minute,
9
+ Month,
10
+ Second,
11
+ Strftime,
12
+ Year,
13
+ )
14
+ from piccolo.table import Table
15
+ from tests.base import engines_only, sqlite_only
16
+
17
+ from .base import FunctionTest
18
+
19
+
20
+ class Concert(Table):
21
+ starts = Timestamp()
22
+
23
+
24
+ class DatetimeTest(FunctionTest):
25
+ tables = [Concert]
26
+
27
+ def setUp(self) -> None:
28
+ super().setUp()
29
+ self.concert = Concert(
30
+ {
31
+ Concert.starts: datetime.datetime(
32
+ year=2024, month=6, day=14, hour=23, minute=46, second=10
33
+ )
34
+ }
35
+ )
36
+ self.concert.save().run_sync()
37
+
38
+
39
+ @engines_only("postgres", "cockroach")
40
+ class TestExtract(DatetimeTest):
41
+ def test_extract(self):
42
+ self.assertEqual(
43
+ Concert.select(
44
+ Extract(Concert.starts, "year", alias="starts_year")
45
+ ).run_sync(),
46
+ [{"starts_year": self.concert.starts.year}],
47
+ )
48
+
49
+ def test_invalid_format(self):
50
+ with self.assertRaises(ValueError):
51
+ Extract(
52
+ Concert.starts,
53
+ "abc123", # type: ignore
54
+ alias="starts_year",
55
+ )
56
+
57
+
58
+ @sqlite_only
59
+ class TestStrftime(DatetimeTest):
60
+ def test_strftime(self):
61
+ self.assertEqual(
62
+ Concert.select(
63
+ Strftime(Concert.starts, "%Y", alias="starts_year")
64
+ ).run_sync(),
65
+ [{"starts_year": str(self.concert.starts.year)}],
66
+ )
67
+
68
+
69
+ class TestDatabaseAgnostic(DatetimeTest):
70
+ def test_year(self):
71
+ self.assertEqual(
72
+ Concert.select(
73
+ Year(Concert.starts, alias="starts_year")
74
+ ).run_sync(),
75
+ [{"starts_year": self.concert.starts.year}],
76
+ )
77
+
78
+ def test_month(self):
79
+ self.assertEqual(
80
+ Concert.select(
81
+ Month(Concert.starts, alias="starts_month")
82
+ ).run_sync(),
83
+ [{"starts_month": self.concert.starts.month}],
84
+ )
85
+
86
+ def test_day(self):
87
+ self.assertEqual(
88
+ Concert.select(Day(Concert.starts, alias="starts_day")).run_sync(),
89
+ [{"starts_day": self.concert.starts.day}],
90
+ )
91
+
92
+ def test_hour(self):
93
+ self.assertEqual(
94
+ Concert.select(
95
+ Hour(Concert.starts, alias="starts_hour")
96
+ ).run_sync(),
97
+ [{"starts_hour": self.concert.starts.hour}],
98
+ )
99
+
100
+ def test_minute(self):
101
+ self.assertEqual(
102
+ Concert.select(
103
+ Minute(Concert.starts, alias="starts_minute")
104
+ ).run_sync(),
105
+ [{"starts_minute": self.concert.starts.minute}],
106
+ )
107
+
108
+ def test_second(self):
109
+ self.assertEqual(
110
+ Concert.select(
111
+ Second(Concert.starts, alias="starts_second")
112
+ ).run_sync(),
113
+ [{"starts_second": self.concert.starts.second}],
114
+ )
@@ -1,10 +1,13 @@
1
- from piccolo.query.functions.string import Upper
1
+ import pytest
2
+
3
+ from piccolo.query.functions.string import Concat, Upper
4
+ from tests.base import engine_version_lt, is_running_sqlite
2
5
  from tests.example_apps.music.tables import Band
3
6
 
4
7
  from .base import BandTest
5
8
 
6
9
 
7
- class TestUpperFunction(BandTest):
10
+ class TestUpper(BandTest):
8
11
 
9
12
  def test_column(self):
10
13
  """
@@ -23,3 +26,32 @@ class TestUpperFunction(BandTest):
23
26
  """
24
27
  response = Band.select(Upper(Band.manager._.name)).run_sync()
25
28
  self.assertListEqual(response, [{"upper": "GUIDO"}])
29
+
30
+
31
+ @pytest.mark.skipif(
32
+ is_running_sqlite() and engine_version_lt(3.44),
33
+ reason="SQLite version not supported",
34
+ )
35
+ class TestConcat(BandTest):
36
+
37
+ def test_column_and_string(self):
38
+ response = Band.select(
39
+ Concat(Band.name, "!!!", alias="name")
40
+ ).run_sync()
41
+ self.assertListEqual(response, [{"name": "Pythonistas!!!"}])
42
+
43
+ def test_column_and_column(self):
44
+ response = Band.select(
45
+ Concat(Band.name, Band.popularity, alias="name")
46
+ ).run_sync()
47
+ self.assertListEqual(response, [{"name": "Pythonistas1000"}])
48
+
49
+ def test_join(self):
50
+ response = Band.select(
51
+ Concat(Band.name, "-", Band.manager._.name, alias="name")
52
+ ).run_sync()
53
+ self.assertListEqual(response, [{"name": "Pythonistas-Guido"}])
54
+
55
+ def test_min_args(self):
56
+ with self.assertRaises(ValueError):
57
+ Concat()