piccolo 1.5.2__py3-none-any.whl → 1.7.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.
@@ -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
 
piccolo/query/mixins.py CHANGED
@@ -9,13 +9,14 @@ from enum import Enum, auto
9
9
 
10
10
  from piccolo.columns import And, Column, Or, Where
11
11
  from piccolo.columns.column_types import ForeignKey
12
+ from piccolo.columns.combination import WhereRaw
12
13
  from piccolo.custom_types import Combinable
13
14
  from piccolo.querystring import QueryString
14
15
  from piccolo.utils.list import flatten
15
16
  from piccolo.utils.sql_values import convert_to_sql_value
16
17
 
17
18
  if t.TYPE_CHECKING: # pragma: no cover
18
- from piccolo.columns.base import Selectable
19
+ from piccolo.querystring import Selectable
19
20
  from piccolo.table import Table # noqa
20
21
 
21
22
 
@@ -254,8 +255,10 @@ class WhereDelegate:
254
255
  elif isinstance(combinable, (And, Or)):
255
256
  self._extract_columns(combinable.first)
256
257
  self._extract_columns(combinable.second)
258
+ elif isinstance(combinable, WhereRaw):
259
+ self._where_columns.extend(combinable.querystring.columns)
257
260
 
258
- def where(self, *where: Combinable):
261
+ def where(self, *where: t.Union[Combinable, QueryString]):
259
262
  for arg in where:
260
263
  if isinstance(arg, bool):
261
264
  raise ValueError(
@@ -265,6 +268,10 @@ class WhereDelegate:
265
268
  "`.where(MyTable.some_column.is_null())`."
266
269
  )
267
270
 
271
+ if isinstance(arg, QueryString):
272
+ # If a raw QueryString is passed in.
273
+ arg = WhereRaw(arg.template, *arg.args)
274
+
268
275
  self._where = And(self._where, arg) if self._where else arg
269
276
 
270
277
 
piccolo/querystring.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typing as t
4
+ from abc import ABCMeta, abstractmethod
4
5
  from dataclasses import dataclass
5
6
  from datetime import datetime
6
7
  from importlib.util import find_spec
@@ -8,6 +9,7 @@ from string import Formatter
8
9
 
9
10
  if t.TYPE_CHECKING: # pragma: no cover
10
11
  from piccolo.table import Table
12
+ from piccolo.columns import Column
11
13
 
12
14
  from uuid import UUID
13
15
 
@@ -17,22 +19,32 @@ else:
17
19
  apgUUID = UUID
18
20
 
19
21
 
20
- @dataclass
21
- class Unquoted:
22
+ class Selectable(metaclass=ABCMeta):
22
23
  """
23
- Used when we want the value to be unquoted because it's a Postgres
24
- keyword - for example DEFAULT.
24
+ Anything which inherits from this can be used in a select query.
25
25
  """
26
26
 
27
- __slots__ = ("value",)
27
+ __slots__ = ("_alias",)
28
28
 
29
- value: str
29
+ _alias: t.Optional[str]
30
30
 
31
- def __repr__(self):
32
- return f"{self.value}"
31
+ @abstractmethod
32
+ def get_select_string(
33
+ self, engine_type: str, with_alias: bool = True
34
+ ) -> QueryString:
35
+ """
36
+ In a query, what to output after the select statement - could be a
37
+ column name, a sub query, a function etc. For a column it will be the
38
+ column name.
39
+ """
40
+ raise NotImplementedError()
33
41
 
34
- def __str__(self):
35
- return f"{self.value}"
42
+ def as_alias(self, alias: str) -> Selectable:
43
+ """
44
+ Allows column names to be changed in the result of a select.
45
+ """
46
+ self._alias = alias
47
+ return self
36
48
 
37
49
 
38
50
  @dataclass
@@ -42,7 +54,7 @@ class Fragment:
42
54
  no_arg: bool = False
43
55
 
44
56
 
45
- class QueryString:
57
+ class QueryString(Selectable):
46
58
  """
47
59
  When we're composing complex queries, we're combining QueryStrings, rather
48
60
  than concatenating strings directly. The reason for this is QueryStrings
@@ -56,6 +68,7 @@ class QueryString:
56
68
  "query_type",
57
69
  "table",
58
70
  "_frozen_compiled_strings",
71
+ "columns",
59
72
  )
60
73
 
61
74
  def __init__(
@@ -64,6 +77,7 @@ class QueryString:
64
77
  *args: t.Any,
65
78
  query_type: str = "generic",
66
79
  table: t.Optional[t.Type[Table]] = None,
80
+ alias: t.Optional[str] = None,
67
81
  ) -> None:
68
82
  """
69
83
  :param template:
@@ -83,12 +97,42 @@ class QueryString:
83
97
 
84
98
  """
85
99
  self.template = template
86
- self.args = args
87
100
  self.query_type = query_type
88
101
  self.table = table
89
102
  self._frozen_compiled_strings: t.Optional[
90
103
  t.Tuple[str, t.List[t.Any]]
91
104
  ] = None
105
+ self._alias = alias
106
+ self.args, self.columns = self.process_args(args)
107
+
108
+ def process_args(
109
+ self, args: t.Sequence[t.Any]
110
+ ) -> t.Tuple[t.Sequence[t.Any], t.Sequence[Column]]:
111
+ """
112
+ If a Column is passed in, we convert it to the name of the column
113
+ (including joins).
114
+ """
115
+ from piccolo.columns import Column
116
+
117
+ processed_args = []
118
+ columns = []
119
+
120
+ for arg in args:
121
+ if isinstance(arg, Column):
122
+ columns.append(arg)
123
+ arg = QueryString(
124
+ f"{arg._meta.get_full_name(with_alias=False)}"
125
+ )
126
+ elif isinstance(arg, QueryString):
127
+ columns.extend(arg.columns)
128
+
129
+ processed_args.append(arg)
130
+
131
+ return (processed_args, columns)
132
+
133
+ def as_alias(self, alias: str) -> QueryString:
134
+ self._alias = alias
135
+ return self
92
136
 
93
137
  def __str__(self):
94
138
  """
@@ -143,7 +187,7 @@ class QueryString:
143
187
  fragment.no_arg = True
144
188
  bundled.append(fragment)
145
189
  else:
146
- if isinstance(value, self.__class__):
190
+ if isinstance(value, QueryString):
147
191
  fragment.no_arg = True
148
192
  bundled.append(fragment)
149
193
 
@@ -195,3 +239,47 @@ class QueryString:
195
239
  self._frozen_compiled_strings = self.compile_string(
196
240
  engine_type=engine_type
197
241
  )
242
+
243
+ ###########################################################################
244
+
245
+ def get_select_string(
246
+ self, engine_type: str, with_alias: bool = True
247
+ ) -> QueryString:
248
+ if with_alias and self._alias:
249
+ return QueryString("{} AS " + self._alias, self)
250
+ else:
251
+ return self
252
+
253
+ def get_where_string(self, engine_type: str) -> QueryString:
254
+ return self.get_select_string(
255
+ engine_type=engine_type, with_alias=False
256
+ )
257
+
258
+ ###########################################################################
259
+ # Basic logic
260
+
261
+ def __eq__(self, value) -> QueryString: # type: ignore[override]
262
+ return QueryString("{} = {}", self, value)
263
+
264
+ def __ne__(self, value) -> QueryString: # type: ignore[override]
265
+ return QueryString("{} != {}", self, value)
266
+
267
+ def __add__(self, value) -> QueryString:
268
+ return QueryString("{} + {}", self, value)
269
+
270
+ def __sub__(self, value) -> QueryString:
271
+ return QueryString("{} - {}", self, value)
272
+
273
+ def is_in(self, value) -> QueryString:
274
+ return QueryString("{} IN {}", self, value)
275
+
276
+ def not_in(self, value) -> QueryString:
277
+ return QueryString("{} NOT IN {}", self, value)
278
+
279
+
280
+ class Unquoted(QueryString):
281
+ """
282
+ This is deprecated - just use QueryString directly.
283
+ """
284
+
285
+ pass
piccolo/table.py CHANGED
@@ -48,7 +48,7 @@ from piccolo.query.methods.create_index import CreateIndex
48
48
  from piccolo.query.methods.indexes import Indexes
49
49
  from piccolo.query.methods.objects import First
50
50
  from piccolo.query.methods.refresh import Refresh
51
- from piccolo.querystring import QueryString, Unquoted
51
+ from piccolo.querystring import QueryString
52
52
  from piccolo.utils import _camel_to_snake
53
53
  from piccolo.utils.graphlib import TopologicalSorter
54
54
  from piccolo.utils.sql_values import convert_to_sql_value
@@ -56,7 +56,7 @@ from piccolo.utils.sync import run_sync
56
56
  from piccolo.utils.warnings import colored_warning
57
57
 
58
58
  if t.TYPE_CHECKING: # pragma: no cover
59
- from piccolo.columns import Selectable
59
+ from piccolo.querystring import Selectable
60
60
 
61
61
  PROTECTED_TABLENAMES = ("user",)
62
62
  TABLENAME_WARNING = (
@@ -796,30 +796,14 @@ class Table(metaclass=TableMetaclass):
796
796
  """
797
797
  Used when inserting rows.
798
798
  """
799
- args_dict = {}
800
- for col in self._meta.columns:
801
- column_name = col._meta.name
802
- value = convert_to_sql_value(value=self[column_name], column=col)
803
- args_dict[column_name] = value
804
-
805
- def is_unquoted(arg):
806
- return isinstance(arg, Unquoted)
807
-
808
- # Strip out any args which are unquoted.
809
- filtered_args = [i for i in args_dict.values() if not is_unquoted(i)]
799
+ args = [
800
+ convert_to_sql_value(value=self[column._meta.name], column=column)
801
+ for column in self._meta.columns
802
+ ]
810
803
 
811
804
  # If unquoted, dump it straight into the query.
812
- query = ",".join(
813
- [
814
- (
815
- args_dict[column._meta.name].value
816
- if is_unquoted(args_dict[column._meta.name])
817
- else "{}"
818
- )
819
- for column in self._meta.columns
820
- ]
821
- )
822
- return QueryString(f"({query})", *filtered_args)
805
+ query = ",".join(["{}" for _ in args])
806
+ return QueryString(f"({query})", *args)
823
807
 
824
808
  def __str__(self) -> str:
825
809
  return self.querystring.__str__()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.5.2
3
+ Version: 1.7.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