piccolo 1.27.0__py3-none-any.whl → 1.28.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.
Files changed (122) hide show
  1. piccolo/__init__.py +1 -1
  2. piccolo/apps/app/commands/new.py +3 -3
  3. piccolo/apps/asgi/commands/new.py +1 -2
  4. piccolo/apps/fixtures/commands/dump.py +8 -8
  5. piccolo/apps/fixtures/commands/load.py +5 -5
  6. piccolo/apps/fixtures/commands/shared.py +9 -9
  7. piccolo/apps/migrations/auto/diffable_table.py +12 -12
  8. piccolo/apps/migrations/auto/migration_manager.py +59 -66
  9. piccolo/apps/migrations/auto/operations.py +14 -14
  10. piccolo/apps/migrations/auto/schema_differ.py +35 -34
  11. piccolo/apps/migrations/auto/schema_snapshot.py +3 -4
  12. piccolo/apps/migrations/auto/serialisation.py +27 -24
  13. piccolo/apps/migrations/auto/serialisation_legacy.py +2 -2
  14. piccolo/apps/migrations/commands/backwards.py +1 -2
  15. piccolo/apps/migrations/commands/base.py +12 -12
  16. piccolo/apps/migrations/commands/check.py +2 -3
  17. piccolo/apps/migrations/commands/clean.py +3 -3
  18. piccolo/apps/migrations/commands/forwards.py +1 -2
  19. piccolo/apps/migrations/commands/new.py +6 -6
  20. piccolo/apps/migrations/tables.py +3 -3
  21. piccolo/apps/playground/commands/run.py +29 -13
  22. piccolo/apps/schema/commands/generate.py +49 -49
  23. piccolo/apps/schema/commands/graph.py +5 -5
  24. piccolo/apps/shell/commands/run.py +1 -2
  25. piccolo/apps/sql_shell/commands/run.py +4 -4
  26. piccolo/apps/tester/commands/run.py +3 -3
  27. piccolo/apps/user/commands/change_permissions.py +6 -6
  28. piccolo/apps/user/commands/create.py +7 -7
  29. piccolo/apps/user/commands/list.py +2 -2
  30. piccolo/apps/user/tables.py +8 -8
  31. piccolo/columns/base.py +84 -52
  32. piccolo/columns/choices.py +2 -2
  33. piccolo/columns/column_types.py +297 -175
  34. piccolo/columns/combination.py +15 -12
  35. piccolo/columns/defaults/base.py +4 -4
  36. piccolo/columns/defaults/date.py +4 -3
  37. piccolo/columns/defaults/interval.py +4 -3
  38. piccolo/columns/defaults/time.py +4 -3
  39. piccolo/columns/defaults/timestamp.py +4 -3
  40. piccolo/columns/defaults/timestamptz.py +4 -3
  41. piccolo/columns/defaults/uuid.py +3 -2
  42. piccolo/columns/m2m.py +28 -35
  43. piccolo/columns/readable.py +4 -3
  44. piccolo/columns/reference.py +9 -9
  45. piccolo/conf/apps.py +53 -54
  46. piccolo/custom_types.py +28 -6
  47. piccolo/engine/base.py +14 -14
  48. piccolo/engine/cockroach.py +5 -4
  49. piccolo/engine/finder.py +2 -2
  50. piccolo/engine/postgres.py +20 -19
  51. piccolo/engine/sqlite.py +23 -22
  52. piccolo/query/base.py +30 -29
  53. piccolo/query/functions/__init__.py +12 -0
  54. piccolo/query/functions/aggregate.py +4 -3
  55. piccolo/query/functions/array.py +151 -0
  56. piccolo/query/functions/base.py +3 -3
  57. piccolo/query/functions/datetime.py +22 -22
  58. piccolo/query/functions/string.py +4 -4
  59. piccolo/query/functions/type_conversion.py +30 -15
  60. piccolo/query/methods/alter.py +47 -46
  61. piccolo/query/methods/count.py +11 -10
  62. piccolo/query/methods/create.py +6 -5
  63. piccolo/query/methods/create_index.py +9 -8
  64. piccolo/query/methods/delete.py +7 -6
  65. piccolo/query/methods/drop_index.py +7 -6
  66. piccolo/query/methods/exists.py +6 -5
  67. piccolo/query/methods/indexes.py +4 -4
  68. piccolo/query/methods/insert.py +21 -14
  69. piccolo/query/methods/objects.py +60 -50
  70. piccolo/query/methods/raw.py +7 -6
  71. piccolo/query/methods/refresh.py +8 -7
  72. piccolo/query/methods/select.py +56 -49
  73. piccolo/query/methods/table_exists.py +5 -5
  74. piccolo/query/methods/update.py +8 -7
  75. piccolo/query/mixins.py +56 -61
  76. piccolo/query/operators/json.py +11 -11
  77. piccolo/query/proxy.py +8 -9
  78. piccolo/querystring.py +14 -15
  79. piccolo/schema.py +10 -10
  80. piccolo/table.py +93 -94
  81. piccolo/table_reflection.py +9 -9
  82. piccolo/testing/model_builder.py +12 -11
  83. piccolo/testing/random_builder.py +2 -2
  84. piccolo/testing/test_case.py +4 -4
  85. piccolo/utils/dictionary.py +3 -3
  86. piccolo/utils/encoding.py +5 -5
  87. piccolo/utils/lazy_loader.py +3 -3
  88. piccolo/utils/list.py +7 -8
  89. piccolo/utils/objects.py +4 -6
  90. piccolo/utils/pydantic.py +21 -24
  91. piccolo/utils/sql_values.py +3 -3
  92. piccolo/utils/sync.py +4 -3
  93. piccolo/utils/warnings.py +1 -2
  94. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/METADATA +1 -1
  95. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/RECORD +122 -121
  96. tests/apps/fixtures/commands/test_dump_load.py +1 -2
  97. tests/apps/migrations/auto/integration/test_migrations.py +32 -7
  98. tests/apps/migrations/auto/test_migration_manager.py +2 -2
  99. tests/apps/migrations/auto/test_schema_differ.py +22 -23
  100. tests/apps/migrations/commands/test_forwards_backwards.py +3 -3
  101. tests/columns/m2m/base.py +2 -2
  102. tests/columns/test_array.py +176 -10
  103. tests/columns/test_boolean.py +2 -4
  104. tests/columns/test_combination.py +29 -1
  105. tests/columns/test_db_column_name.py +2 -2
  106. tests/engine/test_extra_nodes.py +2 -2
  107. tests/engine/test_pool.py +3 -3
  108. tests/engine/test_transaction.py +4 -4
  109. tests/query/test_freeze.py +4 -4
  110. tests/table/instance/test_get_related.py +2 -2
  111. tests/table/test_alter.py +4 -4
  112. tests/table/test_indexes.py +1 -2
  113. tests/table/test_refresh.py +2 -2
  114. tests/table/test_select.py +58 -0
  115. tests/table/test_update.py +3 -3
  116. tests/testing/test_model_builder.py +1 -2
  117. tests/utils/test_pydantic.py +36 -36
  118. tests/utils/test_table_reflection.py +1 -2
  119. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/WHEEL +0 -0
  120. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/entry_points.txt +0 -0
  121. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/licenses/LICENSE +0 -0
  122. {piccolo-1.27.0.dist-info → piccolo-1.28.0.dist-info}/top_level.txt +0 -0
piccolo/engine/sqlite.py CHANGED
@@ -5,11 +5,12 @@ import datetime
5
5
  import enum
6
6
  import os
7
7
  import sqlite3
8
- import typing as t
9
8
  import uuid
9
+ from collections.abc import Callable
10
10
  from dataclasses import dataclass
11
11
  from decimal import Decimal
12
12
  from functools import partial, wraps
13
+ from typing import TYPE_CHECKING, Any, Optional, Union
13
14
 
14
15
  from typing_extensions import Self
15
16
 
@@ -30,7 +31,7 @@ from piccolo.utils.sync import run_sync
30
31
  aiosqlite = LazyLoader("aiosqlite", globals(), "aiosqlite")
31
32
 
32
33
 
33
- if t.TYPE_CHECKING: # pragma: no cover
34
+ if TYPE_CHECKING: # pragma: no cover
34
35
  from aiosqlite import Connection, Cursor # type: ignore
35
36
 
36
37
  from piccolo.table import Table
@@ -125,7 +126,7 @@ def convert_array_in(value: list) -> str:
125
126
 
126
127
  # Register adapters
127
128
 
128
- ADAPTERS: t.Dict[t.Type, t.Callable[[t.Any], t.Any]] = {
129
+ ADAPTERS: dict[type, Callable[[Any], Any]] = {
129
130
  Decimal: convert_numeric_in,
130
131
  uuid.UUID: convert_uuid_in,
131
132
  datetime.time: convert_time_in,
@@ -143,7 +144,7 @@ for value_type, adapter in ADAPTERS.items():
143
144
  # Out
144
145
 
145
146
 
146
- def decode_to_string(converter: t.Callable[[str], t.Any]):
147
+ def decode_to_string(converter: Callable[[str], Any]):
147
148
  """
148
149
  This means we can use our converters with string and bytes. They are
149
150
  passed bytes when used directly via SQLite, and are passed strings when
@@ -151,7 +152,7 @@ def decode_to_string(converter: t.Callable[[str], t.Any]):
151
152
  """
152
153
 
153
154
  @wraps(converter)
154
- def wrapper(value: t.Union[str, bytes]) -> t.Any:
155
+ def wrapper(value: Union[str, bytes]) -> Any:
155
156
  if isinstance(value, bytes):
156
157
  return converter(value.decode("utf8"))
157
158
  elif isinstance(value, str):
@@ -247,7 +248,7 @@ def convert_timestamptz_out(value: str) -> datetime.datetime:
247
248
 
248
249
 
249
250
  @decode_to_string
250
- def convert_array_out(value: str) -> t.List:
251
+ def convert_array_out(value: str) -> list:
251
252
  """
252
253
  If the value if from an array column, deserialise the string back into a
253
254
  list.
@@ -255,7 +256,7 @@ def convert_array_out(value: str) -> t.List:
255
256
  return load_json(value)
256
257
 
257
258
 
258
- def convert_complex_array_out(value: bytes, converter: t.Callable):
259
+ def convert_complex_array_out(value: bytes, converter: Callable):
259
260
  """
260
261
  This is used to handle arrays of things like timestamps, which we can't
261
262
  just load from JSON without doing additional work to convert the elements
@@ -263,7 +264,7 @@ def convert_complex_array_out(value: bytes, converter: t.Callable):
263
264
  """
264
265
  parsed = load_json(value.decode("utf8"))
265
266
 
266
- def convert_list(list_value: t.List):
267
+ def convert_list(list_value: list):
267
268
  output = []
268
269
 
269
270
  for value in list_value:
@@ -284,7 +285,7 @@ def convert_complex_array_out(value: bytes, converter: t.Callable):
284
285
 
285
286
 
286
287
  @decode_to_string
287
- def convert_M2M_out(value: str) -> t.List:
288
+ def convert_M2M_out(value: str) -> list:
288
289
  return value.split(",")
289
290
 
290
291
 
@@ -335,7 +336,7 @@ class AsyncBatch(BaseBatch):
335
336
  batch_size: int
336
337
 
337
338
  # Set internally
338
- _cursor: t.Optional[Cursor] = None
339
+ _cursor: Optional[Cursor] = None
339
340
 
340
341
  @property
341
342
  def cursor(self) -> Cursor:
@@ -343,14 +344,14 @@ class AsyncBatch(BaseBatch):
343
344
  raise ValueError("_cursor not set")
344
345
  return self._cursor
345
346
 
346
- async def next(self) -> t.List[t.Dict]:
347
+ async def next(self) -> list[dict]:
347
348
  data = await self.cursor.fetchmany(self.batch_size)
348
349
  return await self.query._process_results(data)
349
350
 
350
351
  def __aiter__(self: Self) -> Self:
351
352
  return self
352
353
 
353
- async def __anext__(self) -> t.List[t.Dict]:
354
+ async def __anext__(self) -> list[dict]:
354
355
  response = await self.next()
355
356
  if response == []:
356
357
  raise StopAsyncIteration()
@@ -404,9 +405,9 @@ class Atomic(BaseAtomic):
404
405
  ):
405
406
  self.engine = engine
406
407
  self.transaction_type = transaction_type
407
- self.queries: t.List[t.Union[Query, DDL]] = []
408
+ self.queries: list[Union[Query, DDL]] = []
408
409
 
409
- def add(self, *query: t.Union[Query, DDL]):
410
+ def add(self, *query: Union[Query, DDL]):
410
411
  self.queries += list(query)
411
412
 
412
413
  async def run(self):
@@ -546,7 +547,7 @@ class SQLiteTransaction(BaseTransaction):
546
547
  self._savepoint_id += 1
547
548
  return self._savepoint_id
548
549
 
549
- async def savepoint(self, name: t.Optional[str] = None) -> Savepoint:
550
+ async def savepoint(self, name: Optional[str] = None) -> Savepoint:
550
551
  name = name or f"savepoint_{self.get_savepoint_id()}"
551
552
  validate_savepoint_name(name)
552
553
  await self.connection.execute(f"SAVEPOINT {name}")
@@ -576,7 +577,7 @@ class SQLiteTransaction(BaseTransaction):
576
577
  ###############################################################################
577
578
 
578
579
 
579
- def dict_factory(cursor, row) -> t.Dict:
580
+ def dict_factory(cursor, row) -> dict:
580
581
  return {col[0]: row[idx] for idx, col in enumerate(cursor.description)}
581
582
 
582
583
 
@@ -672,7 +673,7 @@ class SQLiteEngine(Engine[SQLiteTransaction]):
672
673
  ###########################################################################
673
674
 
674
675
  async def batch(
675
- self, query: Query, batch_size: int = 100, node: t.Optional[str] = None
676
+ self, query: Query, batch_size: int = 100, node: Optional[str] = None
676
677
  ) -> AsyncBatch:
677
678
  """
678
679
  :param query:
@@ -698,7 +699,7 @@ class SQLiteEngine(Engine[SQLiteTransaction]):
698
699
 
699
700
  ###########################################################################
700
701
 
701
- async def _get_inserted_pk(self, cursor, table: t.Type[Table]) -> t.Any:
702
+ async def _get_inserted_pk(self, cursor, table: type[Table]) -> Any:
702
703
  """
703
704
  If the `pk` column is a non-integer then `ROWID` and `pk` will return
704
705
  different types. Need to query by `lastrowid` to get `pk`s in SQLite
@@ -714,9 +715,9 @@ class SQLiteEngine(Engine[SQLiteTransaction]):
714
715
  async def _run_in_new_connection(
715
716
  self,
716
717
  query: str,
717
- args: t.Optional[t.List[t.Any]] = None,
718
+ args: Optional[list[Any]] = None,
718
719
  query_type: str = "generic",
719
- table: t.Optional[t.Type[Table]] = None,
720
+ table: Optional[type[Table]] = None,
720
721
  ):
721
722
  if args is None:
722
723
  args = []
@@ -740,9 +741,9 @@ class SQLiteEngine(Engine[SQLiteTransaction]):
740
741
  self,
741
742
  connection,
742
743
  query: str,
743
- args: t.Optional[t.List[t.Any]] = None,
744
+ args: Optional[list[Any]] = None,
744
745
  query_type: str = "generic",
745
- table: t.Optional[t.Type[Table]] = None,
746
+ table: Optional[type[Table]] = None,
746
747
  ):
747
748
  """
748
749
  This is used when a transaction is currently active.
piccolo/query/base.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing as t
3
+ from collections.abc import Generator, Sequence
4
4
  from time import time
5
+ from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast
5
6
 
6
7
  from piccolo.columns.column_types import JSON, JSONB
7
8
  from piccolo.custom_types import QueryResponseType, TableInstance
@@ -12,7 +13,7 @@ from piccolo.utils.encoding import load_json
12
13
  from piccolo.utils.objects import make_nested_object
13
14
  from piccolo.utils.sync import run_sync
14
15
 
15
- if t.TYPE_CHECKING: # pragma: no cover
16
+ if TYPE_CHECKING: # pragma: no cover
16
17
  from piccolo.query.mixins import OutputDelegate
17
18
  from piccolo.table import Table # noqa
18
19
 
@@ -26,13 +27,13 @@ class Timer:
26
27
  print(f"Duration: {self.end - self.start}s")
27
28
 
28
29
 
29
- class Query(t.Generic[TableInstance, QueryResponseType]):
30
+ class Query(Generic[TableInstance, QueryResponseType]):
30
31
  __slots__ = ("table", "_frozen_querystrings")
31
32
 
32
33
  def __init__(
33
34
  self,
34
- table: t.Type[TableInstance],
35
- frozen_querystrings: t.Optional[t.Sequence[QueryString]] = None,
35
+ table: type[TableInstance],
36
+ frozen_querystrings: Optional[Sequence[QueryString]] = None,
36
37
  ):
37
38
  self.table = table
38
39
  self._frozen_querystrings = frozen_querystrings
@@ -55,21 +56,21 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
55
56
  if hasattr(self, "_raw_response_callback"):
56
57
  self._raw_response_callback(raw)
57
58
 
58
- output: t.Optional[OutputDelegate] = getattr(
59
+ output: Optional[OutputDelegate] = getattr(
59
60
  self, "output_delegate", None
60
61
  )
61
62
 
62
63
  #######################################################################
63
64
 
64
65
  if output and output._output.load_json:
65
- columns_delegate: t.Optional[ColumnsDelegate] = getattr(
66
+ columns_delegate: Optional[ColumnsDelegate] = getattr(
66
67
  self, "columns_delegate", None
67
68
  )
68
69
 
69
- json_column_names: t.List[str] = []
70
+ json_column_names: list[str] = []
70
71
 
71
72
  if columns_delegate is not None:
72
- json_columns: t.List[t.Union[JSON, JSONB]] = []
73
+ json_columns: list[Union[JSON, JSONB]] = []
73
74
 
74
75
  for column in columns_delegate.selected_columns:
75
76
  if isinstance(column, (JSON, JSONB)):
@@ -107,12 +108,12 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
107
108
  if output:
108
109
  if output._output.as_objects:
109
110
  if output._output.nested:
110
- return t.cast(
111
+ return cast(
111
112
  QueryResponseType,
112
113
  [make_nested_object(row, self.table) for row in raw],
113
114
  )
114
115
  else:
115
- return t.cast(
116
+ return cast(
116
117
  QueryResponseType,
117
118
  [
118
119
  self.table(**columns, _exists_in_db=True)
@@ -120,7 +121,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
120
121
  ],
121
122
  )
122
123
 
123
- return t.cast(QueryResponseType, raw)
124
+ return cast(QueryResponseType, raw)
124
125
 
125
126
  def _validate(self):
126
127
  """
@@ -130,7 +131,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
130
131
  """
131
132
  pass
132
133
 
133
- def __await__(self) -> t.Generator[None, None, QueryResponseType]:
134
+ def __await__(self) -> Generator[None, None, QueryResponseType]:
134
135
  """
135
136
  If the user doesn't explicity call .run(), proxy to it as a
136
137
  convenience.
@@ -138,7 +139,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
138
139
  return self.run().__await__()
139
140
 
140
141
  async def _run(
141
- self, node: t.Optional[str] = None, in_pool: bool = True
142
+ self, node: Optional[str] = None, in_pool: bool = True
142
143
  ) -> QueryResponseType:
143
144
  """
144
145
  Run the query on the database.
@@ -184,16 +185,16 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
184
185
  processed_results = await self._process_results(results)
185
186
 
186
187
  responses.append(processed_results)
187
- return t.cast(QueryResponseType, responses)
188
+ return cast(QueryResponseType, responses)
188
189
 
189
190
  async def run(
190
- self, node: t.Optional[str] = None, in_pool: bool = True
191
+ self, node: Optional[str] = None, in_pool: bool = True
191
192
  ) -> QueryResponseType:
192
193
  return await self._run(node=node, in_pool=in_pool)
193
194
 
194
195
  def run_sync(
195
196
  self,
196
- node: t.Optional[str] = None,
197
+ node: Optional[str] = None,
197
198
  timed: bool = False,
198
199
  in_pool: bool = False,
199
200
  ) -> QueryResponseType:
@@ -217,7 +218,7 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
217
218
  with Timer():
218
219
  return run_sync(coroutine)
219
220
 
220
- async def response_handler(self, response: t.List) -> t.Any:
221
+ async def response_handler(self, response: list) -> Any:
221
222
  """
222
223
  Subclasses can override this to modify the raw response returned by
223
224
  the database driver.
@@ -227,23 +228,23 @@ class Query(t.Generic[TableInstance, QueryResponseType]):
227
228
  ###########################################################################
228
229
 
229
230
  @property
230
- def sqlite_querystrings(self) -> t.Sequence[QueryString]:
231
+ def sqlite_querystrings(self) -> Sequence[QueryString]:
231
232
  raise NotImplementedError
232
233
 
233
234
  @property
234
- def postgres_querystrings(self) -> t.Sequence[QueryString]:
235
+ def postgres_querystrings(self) -> Sequence[QueryString]:
235
236
  raise NotImplementedError
236
237
 
237
238
  @property
238
- def cockroach_querystrings(self) -> t.Sequence[QueryString]:
239
+ def cockroach_querystrings(self) -> Sequence[QueryString]:
239
240
  raise NotImplementedError
240
241
 
241
242
  @property
242
- def default_querystrings(self) -> t.Sequence[QueryString]:
243
+ def default_querystrings(self) -> Sequence[QueryString]:
243
244
  raise NotImplementedError
244
245
 
245
246
  @property
246
- def querystrings(self) -> t.Sequence[QueryString]:
247
+ def querystrings(self) -> Sequence[QueryString]:
247
248
  """
248
249
  Calls the correct underlying method, depending on the current engine.
249
250
  """
@@ -367,7 +368,7 @@ class FrozenQuery:
367
368
  class DDL:
368
369
  __slots__ = ("table",)
369
370
 
370
- def __init__(self, table: t.Type[Table], **kwargs):
371
+ def __init__(self, table: type[Table], **kwargs):
371
372
  self.table = table
372
373
 
373
374
  @property
@@ -379,23 +380,23 @@ class DDL:
379
380
  raise ValueError("Engine isn't defined.")
380
381
 
381
382
  @property
382
- def sqlite_ddl(self) -> t.Sequence[str]:
383
+ def sqlite_ddl(self) -> Sequence[str]:
383
384
  raise NotImplementedError
384
385
 
385
386
  @property
386
- def postgres_ddl(self) -> t.Sequence[str]:
387
+ def postgres_ddl(self) -> Sequence[str]:
387
388
  raise NotImplementedError
388
389
 
389
390
  @property
390
- def cockroach_ddl(self) -> t.Sequence[str]:
391
+ def cockroach_ddl(self) -> Sequence[str]:
391
392
  raise NotImplementedError
392
393
 
393
394
  @property
394
- def default_ddl(self) -> t.Sequence[str]:
395
+ def default_ddl(self) -> Sequence[str]:
395
396
  raise NotImplementedError
396
397
 
397
398
  @property
398
- def ddl(self) -> t.Sequence[str]:
399
+ def ddl(self) -> Sequence[str]:
399
400
  """
400
401
  Calls the correct underlying method, depending on the current engine.
401
402
  """
@@ -1,4 +1,11 @@
1
1
  from .aggregate import Avg, Count, Max, Min, Sum
2
+ from .array import (
3
+ ArrayAppend,
4
+ ArrayCat,
5
+ ArrayPrepend,
6
+ ArrayRemove,
7
+ ArrayReplace,
8
+ )
2
9
  from .datetime import Day, Extract, Hour, Month, Second, Strftime, Year
3
10
  from .math import Abs, Ceil, Floor, Round
4
11
  from .string import Concat, Length, Lower, Ltrim, Reverse, Rtrim, Upper
@@ -30,4 +37,9 @@ __all__ = (
30
37
  "Sum",
31
38
  "Upper",
32
39
  "Year",
40
+ "ArrayAppend",
41
+ "ArrayCat",
42
+ "ArrayPrepend",
43
+ "ArrayRemove",
44
+ "ArrayReplace",
33
45
  )
@@ -1,4 +1,5 @@
1
- import typing as t
1
+ from collections.abc import Sequence
2
+ from typing import Optional
2
3
 
3
4
  from piccolo.columns.base import Column
4
5
  from piccolo.querystring import QueryString
@@ -51,8 +52,8 @@ class Count(QueryString):
51
52
 
52
53
  def __init__(
53
54
  self,
54
- column: t.Optional[Column] = None,
55
- distinct: t.Optional[t.Sequence[Column]] = None,
55
+ column: Optional[Column] = None,
56
+ distinct: Optional[Sequence[Column]] = None,
56
57
  alias: str = "count",
57
58
  ):
58
59
  """
@@ -0,0 +1,151 @@
1
+ from typing import Union
2
+
3
+ from typing_extensions import TypeAlias
4
+
5
+ from piccolo.columns.base import Column
6
+ from piccolo.querystring import QueryString
7
+
8
+ ArrayType: TypeAlias = Union[Column, QueryString, list[object]]
9
+ ArrayItemType: TypeAlias = Union[Column, QueryString, object]
10
+
11
+
12
+ class ArrayQueryString(QueryString):
13
+ def __add__(self, array: ArrayType):
14
+ """
15
+ QueryString will use the ``+`` operator by default for addition, but
16
+ for arrays we want to concatenate them instead.
17
+ """
18
+ return ArrayCat(array_1=self, array_2=array)
19
+
20
+ def __radd__(self, array: ArrayType):
21
+ return ArrayCat(array_1=array, array_2=self)
22
+
23
+
24
+ class ArrayCat(ArrayQueryString):
25
+ def __init__(
26
+ self,
27
+ array_1: ArrayType,
28
+ array_2: ArrayType,
29
+ ):
30
+ """
31
+ Concatenate two arrays.
32
+
33
+ :param array_1:
34
+ These values will be at the start of the new array.
35
+ :param array_2:
36
+ These values will be at the end of the new array.
37
+
38
+ """
39
+ for value in (array_1, array_2):
40
+ if isinstance(value, Column):
41
+ engine_type = value._meta.engine_type
42
+ if engine_type not in ("postgres", "cockroach"):
43
+ raise ValueError(
44
+ "Only Postgres and Cockroach support array "
45
+ "concatenation."
46
+ )
47
+
48
+ super().__init__("array_cat({}, {})", array_1, array_2)
49
+
50
+
51
+ class ArrayAppend(ArrayQueryString):
52
+ def __init__(self, array: ArrayType, value: ArrayItemType):
53
+ """
54
+ Append an element to the end of an array.
55
+
56
+ :param column:
57
+ Identifies the column.
58
+ :param value:
59
+ The value to append.
60
+
61
+ """
62
+ if isinstance(array, Column):
63
+ engine_type = array._meta.engine_type
64
+ if engine_type not in ("postgres", "cockroach"):
65
+ raise ValueError(
66
+ "Only Postgres and Cockroach support array appending."
67
+ )
68
+
69
+ super().__init__("array_append({}, {})", array, value)
70
+
71
+
72
+ class ArrayPrepend(ArrayQueryString):
73
+ def __init__(self, array: ArrayType, value: ArrayItemType):
74
+ """
75
+ Append an element to the beginning of an array.
76
+
77
+ :param value:
78
+ The value to prepend.
79
+ :param column:
80
+ Identifies the column.
81
+
82
+ """
83
+ if isinstance(array, Column):
84
+ engine_type = array._meta.engine_type
85
+ if engine_type not in ("postgres", "cockroach"):
86
+ raise ValueError(
87
+ "Only Postgres and Cockroach support array prepending."
88
+ )
89
+
90
+ super().__init__("array_prepend({}, {})", value, array)
91
+
92
+
93
+ class ArrayReplace(ArrayQueryString):
94
+ def __init__(
95
+ self,
96
+ array: ArrayType,
97
+ old_value: ArrayItemType,
98
+ new_value: ArrayItemType,
99
+ ):
100
+ """
101
+ Replace each array element equal to the given value with a new value.
102
+
103
+ :param column:
104
+ Identifies the column.
105
+ :param old_value:
106
+ The old value to be replaced.
107
+ :param new_value:
108
+ The new value we are replacing with.
109
+
110
+ """
111
+ if isinstance(array, Column):
112
+ engine_type = array._meta.engine_type
113
+ if engine_type not in ("postgres", "cockroach"):
114
+ raise ValueError(
115
+ "Only Postgres and Cockroach support array substitution."
116
+ )
117
+
118
+ super().__init__(
119
+ "array_replace({}, {}, {})", array, old_value, new_value
120
+ )
121
+
122
+
123
+ class ArrayRemove(ArrayQueryString):
124
+ def __init__(self, array: ArrayType, value: ArrayItemType):
125
+ """
126
+ Remove all elements equal to the given value
127
+ from the array (array must be one-dimensional).
128
+
129
+ :param column:
130
+ Identifies the column.
131
+ :param value:
132
+ The value to remove.
133
+
134
+ """
135
+ if isinstance(array, Column):
136
+ engine_type = array._meta.engine_type
137
+ if engine_type not in ("postgres", "cockroach"):
138
+ raise ValueError(
139
+ "Only Postgres and Cockroach support array removing."
140
+ )
141
+
142
+ super().__init__("array_remove({}, {})", array, value)
143
+
144
+
145
+ __all__ = (
146
+ "ArrayCat",
147
+ "ArrayAppend",
148
+ "ArrayPrepend",
149
+ "ArrayReplace",
150
+ "ArrayRemove",
151
+ )
@@ -1,4 +1,4 @@
1
- import typing as t
1
+ from typing import Optional, Union
2
2
 
3
3
  from piccolo.columns.base import Column
4
4
  from piccolo.querystring import QueryString
@@ -9,8 +9,8 @@ class Function(QueryString):
9
9
 
10
10
  def __init__(
11
11
  self,
12
- identifier: t.Union[Column, QueryString, str],
13
- alias: t.Optional[str] = None,
12
+ identifier: Union[Column, QueryString, str],
13
+ alias: Optional[str] = None,
14
14
  ):
15
15
  alias = alias or self.__class__.__name__.lower()
16
16