piccolo 1.5.1__py3-none-any.whl → 1.6.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 (35) hide show
  1. piccolo/__init__.py +1 -1
  2. piccolo/apps/playground/commands/run.py +70 -18
  3. piccolo/columns/base.py +31 -40
  4. piccolo/columns/column_types.py +11 -8
  5. piccolo/columns/m2m.py +16 -6
  6. piccolo/columns/readable.py +9 -7
  7. piccolo/query/__init__.py +1 -4
  8. piccolo/query/base.py +1 -5
  9. piccolo/query/functions/__init__.py +16 -0
  10. piccolo/query/functions/aggregate.py +179 -0
  11. piccolo/query/functions/base.py +21 -0
  12. piccolo/query/functions/string.py +73 -0
  13. piccolo/query/methods/__init__.py +18 -1
  14. piccolo/query/methods/count.py +3 -3
  15. piccolo/query/methods/delete.py +1 -1
  16. piccolo/query/methods/exists.py +1 -1
  17. piccolo/query/methods/objects.py +1 -1
  18. piccolo/query/methods/select.py +17 -232
  19. piccolo/query/methods/update.py +1 -1
  20. piccolo/query/mixins.py +9 -2
  21. piccolo/querystring.py +101 -13
  22. piccolo/table.py +8 -24
  23. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/METADATA +1 -1
  24. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/RECORD +35 -30
  25. tests/apps/migrations/commands/test_forwards_backwards.py +3 -0
  26. tests/apps/shell/commands/test_run.py +1 -0
  27. tests/conf/test_apps.py +6 -0
  28. tests/example_apps/music/tables.py +10 -0
  29. tests/query/test_functions.py +102 -0
  30. tests/table/test_output.py +88 -36
  31. tests/table/test_select.py +2 -9
  32. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/LICENSE +0 -0
  33. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/WHEEL +0 -0
  34. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/entry_points.txt +0 -0
  35. {piccolo-1.5.1.dist-info → piccolo-1.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,179 @@
1
+ import typing as t
2
+
3
+ from piccolo.columns.base import Column
4
+ from piccolo.querystring import QueryString
5
+
6
+ from .base import Function
7
+
8
+
9
+ class Avg(Function):
10
+ """
11
+ ``AVG()`` SQL function. Column type must be numeric to run the query.
12
+
13
+ .. code-block:: python
14
+
15
+ await Band.select(Avg(Band.popularity)).run()
16
+
17
+ # We can use an alias. These two are equivalent:
18
+
19
+ await Band.select(
20
+ Avg(Band.popularity, alias="popularity_avg")
21
+ ).run()
22
+
23
+ await Band.select(
24
+ Avg(Band.popularity).as_alias("popularity_avg")
25
+ ).run()
26
+
27
+ """
28
+
29
+ function_name = "AVG"
30
+
31
+
32
+ class Count(QueryString):
33
+ """
34
+ Used in ``Select`` queries, usually in conjunction with the ``group_by``
35
+ clause::
36
+
37
+ >>> await Band.select(
38
+ ... Band.manager.name.as_alias('manager_name'),
39
+ ... Count(alias='band_count')
40
+ ... ).group_by(Band.manager)
41
+ [{'manager_name': 'Guido', 'count': 1}, ...]
42
+
43
+ It can also be used without the ``group_by`` clause (though you may prefer
44
+ to the :meth:`Table.count <piccolo.table.Table.count>` method instead, as
45
+ it's more convenient)::
46
+
47
+ >>> await Band.select(Count())
48
+ [{'count': 3}]
49
+
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ column: t.Optional[Column] = None,
55
+ distinct: t.Optional[t.Sequence[Column]] = None,
56
+ alias: str = "count",
57
+ ):
58
+ """
59
+ :param column:
60
+ If specified, the count is for non-null values in that column.
61
+ :param distinct:
62
+ If specified, the count is for distinct values in those columns.
63
+ :param alias:
64
+ The name of the value in the response::
65
+
66
+ # These two are equivalent:
67
+
68
+ await Band.select(
69
+ Band.name, Count(alias="total")
70
+ ).group_by(Band.name)
71
+
72
+ await Band.select(
73
+ Band.name,
74
+ Count().as_alias("total")
75
+ ).group_by(Band.name)
76
+
77
+ """
78
+ if distinct and column:
79
+ raise ValueError("Only specify `column` or `distinct`")
80
+
81
+ if distinct:
82
+ engine_type = distinct[0]._meta.engine_type
83
+ if engine_type == "sqlite":
84
+ # SQLite doesn't allow us to specify multiple columns, so
85
+ # instead we concatenate the values.
86
+ column_names = " || ".join("{}" for _ in distinct)
87
+ else:
88
+ column_names = ", ".join("{}" for _ in distinct)
89
+
90
+ return super().__init__(
91
+ f"COUNT(DISTINCT({column_names}))", *distinct, alias=alias
92
+ )
93
+ else:
94
+ if column:
95
+ return super().__init__("COUNT({})", column, alias=alias)
96
+ else:
97
+ return super().__init__("COUNT(*)", alias=alias)
98
+
99
+
100
+ class Min(Function):
101
+ """
102
+ ``MIN()`` SQL function.
103
+
104
+ .. code-block:: python
105
+
106
+ await Band.select(Min(Band.popularity)).run()
107
+
108
+ # We can use an alias. These two are equivalent:
109
+
110
+ await Band.select(
111
+ Min(Band.popularity, alias="popularity_min")
112
+ ).run()
113
+
114
+ await Band.select(
115
+ Min(Band.popularity).as_alias("popularity_min")
116
+ ).run()
117
+
118
+ """
119
+
120
+ function_name = "MIN"
121
+
122
+
123
+ class Max(Function):
124
+ """
125
+ ``MAX()`` SQL function.
126
+
127
+ .. code-block:: python
128
+
129
+ await Band.select(
130
+ Max(Band.popularity)
131
+ ).run()
132
+
133
+ # We can use an alias. These two are equivalent:
134
+
135
+ await Band.select(
136
+ Max(Band.popularity, alias="popularity_max")
137
+ ).run()
138
+
139
+ await Band.select(
140
+ Max(Band.popularity).as_alias("popularity_max")
141
+ ).run()
142
+
143
+ """
144
+
145
+ function_name = "MAX"
146
+
147
+
148
+ class Sum(Function):
149
+ """
150
+ ``SUM()`` SQL function. Column type must be numeric to run the query.
151
+
152
+ .. code-block:: python
153
+
154
+ await Band.select(
155
+ Sum(Band.popularity)
156
+ ).run()
157
+
158
+ # We can use an alias. These two are equivalent:
159
+
160
+ await Band.select(
161
+ Sum(Band.popularity, alias="popularity_sum")
162
+ ).run()
163
+
164
+ await Band.select(
165
+ Sum(Band.popularity).as_alias("popularity_sum")
166
+ ).run()
167
+
168
+ """
169
+
170
+ function_name = "SUM"
171
+
172
+
173
+ __all__ = (
174
+ "Avg",
175
+ "Count",
176
+ "Min",
177
+ "Max",
178
+ "Sum",
179
+ )
@@ -0,0 +1,21 @@
1
+ import typing as t
2
+
3
+ from piccolo.columns.base import Column
4
+ from piccolo.querystring import QueryString
5
+
6
+
7
+ class Function(QueryString):
8
+ function_name: str
9
+
10
+ def __init__(
11
+ self,
12
+ identifier: t.Union[Column, QueryString, str],
13
+ alias: t.Optional[str] = None,
14
+ ):
15
+ alias = alias or self.__class__.__name__.lower()
16
+
17
+ super().__init__(
18
+ f"{self.function_name}({{}})",
19
+ identifier,
20
+ alias=alias,
21
+ )
@@ -0,0 +1,73 @@
1
+ """
2
+ These functions mirror their counterparts in the Postgresql docs:
3
+
4
+ https://www.postgresql.org/docs/current/functions-string.html
5
+
6
+ """
7
+
8
+ from .base import Function
9
+
10
+
11
+ class Length(Function):
12
+ """
13
+ Returns the number of characters in the string.
14
+ """
15
+
16
+ function_name = "LENGTH"
17
+
18
+
19
+ class Lower(Function):
20
+ """
21
+ Converts the string to all lower case, according to the rules of the
22
+ database's locale.
23
+ """
24
+
25
+ function_name = "LOWER"
26
+
27
+
28
+ class Ltrim(Function):
29
+ """
30
+ Removes the longest string containing only characters in characters (a
31
+ space by default) from the start of string.
32
+ """
33
+
34
+ function_name = "LTRIM"
35
+
36
+
37
+ class Reverse(Function):
38
+ """
39
+ Return reversed string.
40
+
41
+ Not supported in SQLite.
42
+
43
+ """
44
+
45
+ function_name = "REVERSE"
46
+
47
+
48
+ class Rtrim(Function):
49
+ """
50
+ Removes the longest string containing only characters in characters (a
51
+ space by default) from the end of string.
52
+ """
53
+
54
+ function_name = "RTRIM"
55
+
56
+
57
+ class Upper(Function):
58
+ """
59
+ Converts the string to all upper case, according to the rules of the
60
+ database's locale.
61
+ """
62
+
63
+ function_name = "UPPER"
64
+
65
+
66
+ __all__ = (
67
+ "Length",
68
+ "Lower",
69
+ "Ltrim",
70
+ "Reverse",
71
+ "Rtrim",
72
+ "Upper",
73
+ )
@@ -9,6 +9,23 @@ from .insert import Insert
9
9
  from .objects import Objects
10
10
  from .raw import Raw
11
11
  from .refresh import Refresh
12
- from .select import Avg, Max, Min, Select, Sum
12
+ from .select import Select
13
13
  from .table_exists import TableExists
14
14
  from .update import Update
15
+
16
+ __all__ = (
17
+ "Alter",
18
+ "Count",
19
+ "Create",
20
+ "CreateIndex",
21
+ "Delete",
22
+ "DropIndex",
23
+ "Exists",
24
+ "Insert",
25
+ "Objects",
26
+ "Raw",
27
+ "Refresh",
28
+ "Select",
29
+ "TableExists",
30
+ "Update",
31
+ )
@@ -4,7 +4,7 @@ import typing as t
4
4
 
5
5
  from piccolo.custom_types import Combinable
6
6
  from piccolo.query.base import Query
7
- from piccolo.query.methods.select import Count as SelectCount
7
+ from piccolo.query.functions.aggregate import Count as CountFunction
8
8
  from piccolo.query.mixins import WhereDelegate
9
9
  from piccolo.querystring import QueryString
10
10
 
@@ -32,7 +32,7 @@ class Count(Query):
32
32
  ###########################################################################
33
33
  # Clauses
34
34
 
35
- def where(self: Self, *where: Combinable) -> Self:
35
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
36
36
  self.where_delegate.where(*where)
37
37
  return self
38
38
 
@@ -50,7 +50,7 @@ class Count(Query):
50
50
  table: t.Type[Table] = self.table
51
51
 
52
52
  query = table.select(
53
- SelectCount(column=self.column, distinct=self._distinct)
53
+ CountFunction(column=self.column, distinct=self._distinct)
54
54
  )
55
55
 
56
56
  query.where_delegate._where = self.where_delegate._where
@@ -30,7 +30,7 @@ class Delete(Query):
30
30
  self.returning_delegate = ReturningDelegate()
31
31
  self.where_delegate = WhereDelegate()
32
32
 
33
- def where(self: Self, *where: Combinable) -> Self:
33
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
34
34
  self.where_delegate.where(*where)
35
35
  return self
36
36
 
@@ -16,7 +16,7 @@ class Exists(Query[TableInstance, bool]):
16
16
  super().__init__(table, **kwargs)
17
17
  self.where_delegate = WhereDelegate()
18
18
 
19
- def where(self: Self, *where: Combinable) -> Self:
19
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
20
20
  self.where_delegate.where(*where)
21
21
  return self
22
22
 
@@ -262,7 +262,7 @@ class Objects(
262
262
  self.order_by_delegate.order_by(*_columns, ascending=ascending)
263
263
  return self
264
264
 
265
- def where(self: Self, *where: Combinable) -> Self:
265
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
266
266
  self.where_delegate.where(*where)
267
267
  return self
268
268
 
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import decimal
4
3
  import itertools
5
4
  import typing as t
6
5
  from collections import OrderedDict
@@ -36,9 +35,8 @@ if t.TYPE_CHECKING: # pragma: no cover
36
35
  from piccolo.custom_types import Combinable
37
36
  from piccolo.table import Table # noqa
38
37
 
39
-
40
- def is_numeric_column(column: Column) -> bool:
41
- return column.value_type in (int, decimal.Decimal, float)
38
+ # Here to avoid breaking changes - will be removed in the future.
39
+ from piccolo.query.functions.aggregate import Count # noqa: F401
42
40
 
43
41
 
44
42
  class SelectRaw(Selectable):
@@ -59,224 +57,8 @@ class SelectRaw(Selectable):
59
57
 
60
58
  def get_select_string(
61
59
  self, engine_type: str, with_alias: bool = True
62
- ) -> str:
63
- return self.querystring.__str__()
64
-
65
-
66
- class Avg(Selectable):
67
- """
68
- ``AVG()`` SQL function. Column type must be numeric to run the query.
69
-
70
- .. code-block:: python
71
-
72
- await Band.select(Avg(Band.popularity)).run()
73
-
74
- # We can use an alias. These two are equivalent:
75
-
76
- await Band.select(
77
- Avg(Band.popularity, alias="popularity_avg")
78
- ).run()
79
-
80
- await Band.select(
81
- Avg(Band.popularity).as_alias("popularity_avg")
82
- ).run()
83
-
84
- """
85
-
86
- def __init__(self, column: Column, alias: str = "avg"):
87
- if is_numeric_column(column):
88
- self.column = column
89
- else:
90
- raise ValueError("Column type must be numeric to run the query.")
91
- self._alias = alias
92
-
93
- def get_select_string(
94
- self, engine_type: str, with_alias: bool = True
95
- ) -> str:
96
- column_name = self.column._meta.get_full_name(with_alias=False)
97
- return f'AVG({column_name}) AS "{self._alias}"'
98
-
99
-
100
- class Count(Selectable):
101
- """
102
- Used in ``Select`` queries, usually in conjunction with the ``group_by``
103
- clause::
104
-
105
- >>> await Band.select(
106
- ... Band.manager.name.as_alias('manager_name'),
107
- ... Count(alias='band_count')
108
- ... ).group_by(Band.manager)
109
- [{'manager_name': 'Guido', 'count': 1}, ...]
110
-
111
- It can also be used without the ``group_by`` clause (though you may prefer
112
- to the :meth:`Table.count <piccolo.table.Table.count>` method instead, as
113
- it's more convenient)::
114
-
115
- >>> await Band.select(Count())
116
- [{'count': 3}]
117
-
118
- """
119
-
120
- def __init__(
121
- self,
122
- column: t.Optional[Column] = None,
123
- distinct: t.Optional[t.Sequence[Column]] = None,
124
- alias: str = "count",
125
- ):
126
- """
127
- :param column:
128
- If specified, the count is for non-null values in that column.
129
- :param distinct:
130
- If specified, the count is for distinct values in those columns.
131
- :param alias:
132
- The name of the value in the response::
133
-
134
- # These two are equivalent:
135
-
136
- await Band.select(
137
- Band.name, Count(alias="total")
138
- ).group_by(Band.name)
139
-
140
- await Band.select(
141
- Band.name,
142
- Count().as_alias("total")
143
- ).group_by(Band.name)
144
-
145
- """
146
- if distinct and column:
147
- raise ValueError("Only specify `column` or `distinct`")
148
-
149
- self.column = column
150
- self.distinct = distinct
151
- self._alias = alias
152
-
153
- def get_select_string(
154
- self, engine_type: str, with_alias: bool = True
155
- ) -> str:
156
- expression: str
157
-
158
- if self.distinct:
159
- if engine_type == "sqlite":
160
- # SQLite doesn't allow us to specify multiple columns, so
161
- # instead we concatenate the values.
162
- column_names = " || ".join(
163
- i._meta.get_full_name(with_alias=False)
164
- for i in self.distinct
165
- )
166
- else:
167
- column_names = ", ".join(
168
- i._meta.get_full_name(with_alias=False)
169
- for i in self.distinct
170
- )
171
-
172
- expression = f"DISTINCT ({column_names})"
173
- else:
174
- if self.column:
175
- expression = self.column._meta.get_full_name(with_alias=False)
176
- else:
177
- expression = "*"
178
-
179
- return f'COUNT({expression}) AS "{self._alias}"'
180
-
181
-
182
- class Max(Selectable):
183
- """
184
- ``MAX()`` SQL function.
185
-
186
- .. code-block:: python
187
-
188
- await Band.select(
189
- Max(Band.popularity)
190
- ).run()
191
-
192
- # We can use an alias. These two are equivalent:
193
-
194
- await Band.select(
195
- Max(Band.popularity, alias="popularity_max")
196
- ).run()
197
-
198
- await Band.select(
199
- Max(Band.popularity).as_alias("popularity_max")
200
- ).run()
201
-
202
- """
203
-
204
- def __init__(self, column: Column, alias: str = "max"):
205
- self.column = column
206
- self._alias = alias
207
-
208
- def get_select_string(
209
- self, engine_type: str, with_alias: bool = True
210
- ) -> str:
211
- column_name = self.column._meta.get_full_name(with_alias=False)
212
- return f'MAX({column_name}) AS "{self._alias}"'
213
-
214
-
215
- class Min(Selectable):
216
- """
217
- ``MIN()`` SQL function.
218
-
219
- .. code-block:: python
220
-
221
- await Band.select(Min(Band.popularity)).run()
222
-
223
- # We can use an alias. These two are equivalent:
224
-
225
- await Band.select(
226
- Min(Band.popularity, alias="popularity_min")
227
- ).run()
228
-
229
- await Band.select(
230
- Min(Band.popularity).as_alias("popularity_min")
231
- ).run()
232
-
233
- """
234
-
235
- def __init__(self, column: Column, alias: str = "min"):
236
- self.column = column
237
- self._alias = alias
238
-
239
- def get_select_string(
240
- self, engine_type: str, with_alias: bool = True
241
- ) -> str:
242
- column_name = self.column._meta.get_full_name(with_alias=False)
243
- return f'MIN({column_name}) AS "{self._alias}"'
244
-
245
-
246
- class Sum(Selectable):
247
- """
248
- ``SUM()`` SQL function. Column type must be numeric to run the query.
249
-
250
- .. code-block:: python
251
-
252
- await Band.select(
253
- Sum(Band.popularity)
254
- ).run()
255
-
256
- # We can use an alias. These two are equivalent:
257
-
258
- await Band.select(
259
- Sum(Band.popularity, alias="popularity_sum")
260
- ).run()
261
-
262
- await Band.select(
263
- Sum(Band.popularity).as_alias("popularity_sum")
264
- ).run()
265
-
266
- """
267
-
268
- def __init__(self, column: Column, alias: str = "sum"):
269
- if is_numeric_column(column):
270
- self.column = column
271
- else:
272
- raise ValueError("Column type must be numeric to run the query.")
273
- self._alias = alias
274
-
275
- def get_select_string(
276
- self, engine_type: str, with_alias: bool = True
277
- ) -> str:
278
- column_name = self.column._meta.get_full_name(with_alias=False)
279
- return f'SUM({column_name}) AS "{self._alias}"'
60
+ ) -> QueryString:
61
+ return self.querystring
280
62
 
281
63
 
282
64
  OptionalDict = t.Optional[t.Dict[str, t.Any]]
@@ -645,7 +427,7 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
645
427
  self.callback_delegate.callback(callbacks, on=on)
646
428
  return self
647
429
 
648
- def where(self: Self, *where: Combinable) -> Self:
430
+ def where(self: Self, *where: t.Union[Combinable, QueryString]) -> Self:
649
431
  self.where_delegate.where(*where)
650
432
  return self
651
433
 
@@ -678,23 +460,25 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
678
460
  for readable in readables:
679
461
  columns += readable.columns
680
462
 
463
+ querystrings: t.List[QueryString] = [
464
+ i for i in columns if isinstance(i, QueryString)
465
+ ]
466
+ for querystring in querystrings:
467
+ if querystring_columns := getattr(querystring, "columns", []):
468
+ columns += querystring_columns
469
+
681
470
  for column in columns:
682
471
  if not isinstance(column, Column):
683
472
  continue
684
473
 
685
474
  _joins: t.List[str] = []
686
475
  for index, key in enumerate(column._meta.call_chain, 0):
687
- table_alias = "$".join(
688
- f"{_key._meta.table._meta.tablename}${_key._meta.name}"
689
- for _key in column._meta.call_chain[: index + 1]
690
- )
691
-
692
- key._meta.table_alias = table_alias
476
+ table_alias = key.table_alias
693
477
 
694
478
  if index > 0:
695
479
  left_tablename = column._meta.call_chain[
696
480
  index - 1
697
- ]._meta.table_alias
481
+ ].table_alias
698
482
  else:
699
483
  left_tablename = (
700
484
  key._meta.table._meta.get_formatted_tablename()
@@ -761,11 +545,10 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
761
545
 
762
546
  engine_type = self.table._meta.db.engine_type
763
547
 
764
- select_strings: t.List[str] = [
548
+ select_strings: t.List[QueryString] = [
765
549
  c.get_select_string(engine_type=engine_type)
766
550
  for c in self.columns_delegate.selected_columns
767
551
  ]
768
- columns_str = ", ".join(select_strings)
769
552
 
770
553
  #######################################################################
771
554
 
@@ -779,7 +562,9 @@ class Select(Query[TableInstance, t.List[t.Dict[str, t.Any]]]):
779
562
  query += "{}"
780
563
  args.append(distinct.querystring)
781
564
 
565
+ columns_str = ", ".join("{}" for i in select_strings)
782
566
  query += f" {columns_str} FROM {self.table._meta.get_formatted_tablename()}" # noqa: E501
567
+ args.extend(select_strings)
783
568
 
784
569
  for join in joins:
785
570
  query += f" {join}"
@@ -50,7 +50,7 @@ class Update(Query[TableInstance, t.List[t.Any]]):
50
50
  self.values_delegate.values(values)
51
51
  return self
52
52
 
53
- def where(self, *where: Combinable) -> Update:
53
+ def where(self, *where: t.Union[Combinable, QueryString]) -> Update:
54
54
  self.where_delegate.where(*where)
55
55
  return self
56
56