piccolo 1.8.0__py3-none-any.whl → 1.9.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.8.0"
1
+ __VERSION__ = "1.9.0"
piccolo/columns/base.py CHANGED
@@ -830,7 +830,11 @@ class Column(Selectable):
830
830
  engine_type=engine_type, with_alias=False
831
831
  )
832
832
 
833
- def get_sql_value(self, value: t.Any) -> t.Any:
833
+ def get_sql_value(
834
+ self,
835
+ value: t.Any,
836
+ delimiter: str = "'",
837
+ ) -> str:
834
838
  """
835
839
  When using DDL statements, we can't parameterise the values. An example
836
840
  is when setting the default for a column. So we have to convert from
@@ -839,11 +843,18 @@ class Column(Selectable):
839
843
 
840
844
  :param value:
841
845
  The Python value to convert to a string usable in a DDL statement
842
- e.g. 1.
846
+ e.g. ``1``.
847
+ :param delimiter:
848
+ The string returned by this function is wrapped in delimiters,
849
+ ready to be added to a DDL statement. For example:
850
+ ``'hello world'``.
843
851
  :returns:
844
- The string usable in the DDL statement e.g. '1'.
852
+ The string usable in the DDL statement e.g. ``'1'``.
845
853
 
846
854
  """
855
+ from piccolo.engine.sqlite import ADAPTERS as sqlite_adapters
856
+
857
+ # Common across all DB engines
847
858
  if isinstance(value, Default):
848
859
  return getattr(value, self._meta.engine_type)
849
860
  elif value is None:
@@ -851,37 +862,52 @@ class Column(Selectable):
851
862
  elif isinstance(value, (float, decimal.Decimal)):
852
863
  return str(value)
853
864
  elif isinstance(value, str):
854
- return f"'{value}'"
865
+ return f"{delimiter}{value}{delimiter}"
855
866
  elif isinstance(value, bool):
856
867
  return str(value).lower()
857
- elif isinstance(value, datetime.datetime):
858
- return f"'{value.isoformat().replace('T', ' ')}'"
859
- elif isinstance(value, datetime.date):
860
- return f"'{value.isoformat()}'"
861
- elif isinstance(value, datetime.time):
862
- return f"'{value.isoformat()}'"
863
- elif isinstance(value, datetime.timedelta):
864
- interval = IntervalCustom.from_timedelta(value)
865
- return getattr(interval, self._meta.engine_type)
866
868
  elif isinstance(value, bytes):
867
- return f"'{value.hex()}'"
868
- elif isinstance(value, uuid.UUID):
869
- return f"'{value}'"
870
- elif isinstance(value, list):
871
- # Convert to the array syntax.
872
- return (
873
- "'{"
874
- + ", ".join(
875
- (
876
- f'"{i}"'
877
- if isinstance(i, str)
878
- else str(self.get_sql_value(i))
869
+ return f"{delimiter}{value.hex()}{delimiter}"
870
+
871
+ # SQLite specific
872
+ if self._meta.engine_type == "sqlite":
873
+ if adapter := sqlite_adapters.get(type(value)):
874
+ sqlite_value = adapter(value)
875
+ return (
876
+ f"{delimiter}{sqlite_value}{delimiter}"
877
+ if isinstance(sqlite_value, str)
878
+ else sqlite_value
879
+ )
880
+
881
+ # Postgres and Cockroach
882
+ if self._meta.engine_type in ["postgres", "cockroach"]:
883
+ if isinstance(value, datetime.datetime):
884
+ return f"{delimiter}{value.isoformat().replace('T', ' ')}{delimiter}" # noqa: E501
885
+ elif isinstance(value, datetime.date):
886
+ return f"{delimiter}{value.isoformat()}{delimiter}"
887
+ elif isinstance(value, datetime.time):
888
+ return f"{delimiter}{value.isoformat()}{delimiter}"
889
+ elif isinstance(value, datetime.timedelta):
890
+ interval = IntervalCustom.from_timedelta(value)
891
+ return getattr(interval, self._meta.engine_type)
892
+ elif isinstance(value, uuid.UUID):
893
+ return f"{delimiter}{value}{delimiter}"
894
+ elif isinstance(value, list):
895
+ # Convert to the array syntax.
896
+ return (
897
+ delimiter
898
+ + "{"
899
+ + ",".join(
900
+ self.get_sql_value(
901
+ i,
902
+ delimiter="" if isinstance(i, list) else '"',
903
+ )
904
+ for i in value
879
905
  )
880
- for i in value
906
+ + "}"
907
+ + delimiter
881
908
  )
882
- ) + "}'"
883
- else:
884
- return value
909
+
910
+ return str(value)
885
911
 
886
912
  @property
887
913
  def column_type(self):
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typing as t
4
- from abc import ABC, abstractmethod, abstractproperty
4
+ from abc import ABC, abstractmethod
5
5
 
6
6
  from piccolo.utils.repr import repr_class_instance
7
7
 
8
8
 
9
9
  class Default(ABC):
10
- @abstractproperty
10
+ @property
11
+ @abstractmethod
11
12
  def postgres(self) -> str:
12
13
  pass
13
14
 
14
- @abstractproperty
15
+ @property
16
+ @abstractmethod
15
17
  def sqlite(self) -> str:
16
18
  pass
17
19
 
@@ -1,17 +1,22 @@
1
1
  from .aggregate import Avg, Count, Max, Min, Sum
2
+ from .math import Abs, Ceil, Floor, Round
2
3
  from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
3
4
  from .type_conversion import Cast
4
5
 
5
6
  __all__ = (
7
+ "Abs",
6
8
  "Avg",
7
9
  "Cast",
10
+ "Ceil",
8
11
  "Count",
12
+ "Floor",
9
13
  "Length",
10
14
  "Lower",
11
15
  "Ltrim",
12
16
  "Max",
13
17
  "Min",
14
18
  "Reverse",
19
+ "Round",
15
20
  "Rtrim",
16
21
  "Sum",
17
22
  "Upper",
@@ -0,0 +1,48 @@
1
+ """
2
+ These functions mirror their counterparts in the Postgresql docs:
3
+
4
+ https://www.postgresql.org/docs/current/functions-math.html
5
+
6
+ """
7
+
8
+ from .base import Function
9
+
10
+
11
+ class Abs(Function):
12
+ """
13
+ Absolute value.
14
+ """
15
+
16
+ function_name = "ABS"
17
+
18
+
19
+ class Ceil(Function):
20
+ """
21
+ Nearest integer greater than or equal to argument.
22
+ """
23
+
24
+ function_name = "CEIL"
25
+
26
+
27
+ class Floor(Function):
28
+ """
29
+ Nearest integer less than or equal to argument.
30
+ """
31
+
32
+ function_name = "FLOOR"
33
+
34
+
35
+ class Round(Function):
36
+ """
37
+ Rounds to nearest integer.
38
+ """
39
+
40
+ function_name = "ROUND"
41
+
42
+
43
+ __all__ = (
44
+ "Abs",
45
+ "Ceil",
46
+ "Floor",
47
+ "Round",
48
+ )
piccolo/querystring.py CHANGED
@@ -282,12 +282,30 @@ class QueryString(Selectable):
282
282
  def __le__(self, value) -> QueryString:
283
283
  return QueryString("{} <= {}", self, value)
284
284
 
285
+ def __truediv__(self, value) -> QueryString:
286
+ return QueryString("{} / {}", self, value)
287
+
288
+ def __mul__(self, value) -> QueryString:
289
+ return QueryString("{} * {}", self, value)
290
+
291
+ def __pow__(self, value) -> QueryString:
292
+ return QueryString("{} ^ {}", self, value)
293
+
294
+ def __mod__(self, value) -> QueryString:
295
+ return QueryString("{} % {}", self, value)
296
+
285
297
  def is_in(self, value) -> QueryString:
286
298
  return QueryString("{} IN {}", self, value)
287
299
 
288
300
  def not_in(self, value) -> QueryString:
289
301
  return QueryString("{} NOT IN {}", self, value)
290
302
 
303
+ def like(self, value: str) -> QueryString:
304
+ return QueryString("{} LIKE {}", self, value)
305
+
306
+ def ilike(self, value: str) -> QueryString:
307
+ return QueryString("{} ILIKE {}", self, value)
308
+
291
309
 
292
310
  class Unquoted(QueryString):
293
311
  """
piccolo/schema.py CHANGED
@@ -13,7 +13,8 @@ class SchemaDDLBase(abc.ABC):
13
13
 
14
14
  db: Engine
15
15
 
16
- @abc.abstractproperty
16
+ @property
17
+ @abc.abstractmethod
17
18
  def ddl(self) -> str:
18
19
  pass
19
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piccolo
3
- Version: 1.8.0
3
+ Version: 1.9.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,9 +1,9 @@
1
- piccolo/__init__.py,sha256=mRuIID3n4xkvLnANZAYlJEC5QgpM7rOXi2uxp1_jzHg,22
1
+ piccolo/__init__.py,sha256=Gh6i3k_DUgCHpLwjrSRIPrDHNQum_zcZ2tZi3wiqRkU,22
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
5
- piccolo/querystring.py,sha256=Mn00hb4HGOe9MIR7WOhJ3qO1uNex21zj0m2heOeDvZk,9057
6
- piccolo/schema.py,sha256=aWPuZxEulgBRD5NTqKN-RAZchxu-PoIrn0iFrWGZuq4,7731
5
+ piccolo/querystring.py,sha256=_3enTH0oBx77LfpS9UG_5OGp5fMxmu50Dod5s1Gn9mY,9655
6
+ piccolo/schema.py,sha256=S_0dwyOVPx90wWbCOP_Y9YHWtb-2JCuAJWAoLkTbFbU,7743
7
7
  piccolo/table.py,sha256=DJT8jTgirPpzkydjSzaCgcG0DiC75XRtW_xtFqTyg80,49457
8
8
  piccolo/table_reflection.py,sha256=jrN1nHerDJ4tU09GtNN3hz7ap-7rXnSUjljFO6LB2H0,7094
9
9
  piccolo/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -115,7 +115,7 @@ piccolo/apps/user/piccolo_migrations/2020-06-11T21-38-55.py,sha256=JG_LFPrEljnSE
115
115
  piccolo/apps/user/piccolo_migrations/2021-04-30T16-14-15.py,sha256=Y_Dj4ROSxjnPsRDqcnpWeyk8UpF8c80T08_O2uq-GoA,1219
116
116
  piccolo/apps/user/piccolo_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
117
  piccolo/columns/__init__.py,sha256=OYhO_n9anMiU9nL-K6ATq9FhAtm8RyMpqYQ7fTVbhxI,1120
118
- piccolo/columns/base.py,sha256=FuMLSplFuNoK919nkas2Fn4kZjmPLMmutc4cRtmLhyY,31378
118
+ piccolo/columns/base.py,sha256=sgMiBvq-xLW6_W86g6XZTMc_3cskyeoMF6yIvIlnXsA,32487
119
119
  piccolo/columns/choices.py,sha256=-HNQuk9vMmVZIPZ5PMeXGTfr23o4nzKPSAkvcG1k0y8,723
120
120
  piccolo/columns/column_types.py,sha256=CzbNnP_VWvz6_r4aaRcMHiHZOaWHeq5IGaN8WJ7JGPA,81685
121
121
  piccolo/columns/combination.py,sha256=vMXC2dfY7pvnCFhsT71XFVyb4gdQzfRsCMaiduu04Ss,6900
@@ -124,7 +124,7 @@ piccolo/columns/m2m.py,sha256=vRJZqBcBP3TQ9Mmb7UEqTgg0QoxIIjIu6JfGLAi4X8Q,14595
124
124
  piccolo/columns/readable.py,sha256=hganxUPfIK5ZXn-qgteBxsOJfBJucgr9U0QLsLFYcuI,1562
125
125
  piccolo/columns/reference.py,sha256=FqE9rpMBMwNNkKXR3Wi4ce-fyT2Vh4KM8YpdC21s6gg,3574
126
126
  piccolo/columns/defaults/__init__.py,sha256=7hpB13baEJgc1zbZjRKDFr-5hltxM2VGj8KnKfOiS8c,145
127
- piccolo/columns/defaults/base.py,sha256=kxh5jgU9G1zpcncmqISZgwMeHnNPBgNCvuqPPQYO_zs,1854
127
+ piccolo/columns/defaults/base.py,sha256=kZ63QVI5nacQdVj2oZ_0Kr5KKtEZTkotKrFibUS6axk,1860
128
128
  piccolo/columns/defaults/date.py,sha256=7tW_tTfsnzU8LXn9Qkrtk8OPyi80SESa-dY_UMlNTp0,2455
129
129
  piccolo/columns/defaults/interval.py,sha256=QTx-iW0J5Eogv72_xXg8hWHEqbRx7jyTxrrV-eIq9HI,1947
130
130
  piccolo/columns/defaults/time.py,sha256=UEtfdMkn8YdlzyEmqO6DrVnuwZrYUrvG_gTJ-SOMmwk,2355
@@ -149,9 +149,10 @@ piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
149
149
  piccolo/query/base.py,sha256=G8Mwz0GcHY4Xs5Co9ubCNMI-3orfOsDdRDOnFRws7TU,15212
150
150
  piccolo/query/mixins.py,sha256=1RyhORDRwTZF9m_2uEgc6sOSd2uViXivBAaFN8geq5g,21982
151
151
  piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
152
- piccolo/query/functions/__init__.py,sha256=9ikQo6qPkCBaWSiuimEACWbYRk9KHoLboRsEC1VcSVw,312
152
+ piccolo/query/functions/__init__.py,sha256=e-BEHlGR3JhE2efWG_rmXdURKL4Fa8tjdGmPsvH4kWo,403
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/math.py,sha256=2Wapq0lpXZh77z0uzXUhnOfmUkbkM0xjQ4tiyuCsbiE,661
155
156
  piccolo/query/functions/string.py,sha256=srxsQJFS6L4gPvFjvuAFQj7QtnCF7X6YoJNKARR2XP0,1236
156
157
  piccolo/query/functions/type_conversion.py,sha256=OYbZc6TEk6b5yTwCMw2rmZ-UiQiUiWZOyxwMLzUjXwE,2583
157
158
  piccolo/query/methods/__init__.py,sha256=tm4gLeV_obDqpgnouVjFbGubbaoJcqm_cbNd4LPo48Q,622
@@ -242,7 +243,7 @@ tests/apps/user/commands/test_change_permissions.py,sha256=uVKEiT1EKot3VA2TDETdQ
242
243
  tests/apps/user/commands/test_create.py,sha256=iJ3Tti62rHwvdcTwNXrc5JPam6vR1qxKRdMN456vm3o,2250
243
244
  tests/apps/user/commands/test_list.py,sha256=ipPfGdW6fH7q-Jc7JcYUvlioGmH9GQU0WImZGC2m-XQ,2840
244
245
  tests/columns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
- tests/columns/test_array.py,sha256=Kd8yy3C4cXJIgutY-cv0MrE4mvgr52qqVUdkcazW_uI,9298
246
+ tests/columns/test_array.py,sha256=uElfAcJhuB0m9_O1qTgURbrwE2Php5Bc7IB9hyXoQ_Q,10772
246
247
  tests/columns/test_base.py,sha256=CTqCNcrqAJTjLXe3MCZgTczrmB3jcVRcOpU4FilpLoQ,3918
247
248
  tests/columns/test_bigint.py,sha256=a0B4y1H02ww5qaW574X2lyenbY6o29ztOhiaqybPC0c,1149
248
249
  tests/columns/test_boolean.py,sha256=kDESp6FnRtSZhuqIu0dBRwKMSpS5TFbbs3sz2MyZSs8,1720
@@ -253,6 +254,7 @@ tests/columns/test_date.py,sha256=lz3AF64CkQzulfniGs0fxuvbH2grR3pF2sxipXiyvHU,12
253
254
  tests/columns/test_db_column_name.py,sha256=v0QFOQp_atqzMB1n40simVwHeBDi5nyN1N2bSPX5k6w,7670
254
255
  tests/columns/test_defaults.py,sha256=rwlU1fXt3cCl7C51eLlZXqgWkE-K5W0pHvTrwkAKyCo,2896
255
256
  tests/columns/test_double_precision.py,sha256=CuobfnQnuwqAIuuOPoh2mKHnY9A7gZosoMIGpY-ubfE,639
257
+ tests/columns/test_get_sql_value.py,sha256=mKgsInN374jzV99y9mg_ZiG-AvnJgz36SZi89xL7RZM,1768
256
258
  tests/columns/test_interval.py,sha256=SbeRgTBWPBL5_LYQUdaP3qyN6eTNtTJtu8JXWollhQw,2993
257
259
  tests/columns/test_json.py,sha256=ErGytVqMVO86YiqGmQIcqb2zUcAUY_ya-cY9tSKKXhQ,3920
258
260
  tests/columns/test_jsonb.py,sha256=7MeH0h2SJt_uCUL6D5-6DLTBPlaz6qKSJcnyhuwvzHg,6914
@@ -296,10 +298,15 @@ tests/query/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
296
298
  tests/query/test_await.py,sha256=imGazmG0l4qilveNPwsxvYQogFJtos4YB8N9iggPEFU,412
297
299
  tests/query/test_camelcase.py,sha256=AcL2gZera1GfpVJNpuKuh5ZBosNCY_ezPWh6-duU5vU,1765
298
300
  tests/query/test_freeze.py,sha256=p3iXqHzgV39YWlqzXtZvaDa7iKZaaaelOGX3UZ8CMf0,3887
299
- tests/query/test_functions.py,sha256=B_M-giOf1xGvdCaYgJdKWLSoV8vtTiRQtEyvL4-7eCY,6401
300
301
  tests/query/test_gather.py,sha256=okWANrBoh0Ut1RomWoffiWNpFqiITF6qti-Aa3uYtRk,730
301
- tests/query/test_querystring.py,sha256=hHljfdnOTlwIMs-7Q2yP5YekYXTT2It-Q-3mP6T9e58,880
302
+ tests/query/test_querystring.py,sha256=QrqyjwUlFlf5LrsJ7DgjCruq811I0UvrDFPud6rfZNI,5019
302
303
  tests/query/test_slots.py,sha256=I9ZjAYqAJNSFAWg9UyAqy7bm-Z52KiyQ2C_yHk2qqqI,1010
304
+ tests/query/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
+ tests/query/functions/base.py,sha256=RLCzLT7iN_Z5DtIFZqVESTJGES2JKb8VDU25sv5OtN4,811
306
+ tests/query/functions/test_functions.py,sha256=510fqRrOrAZ9NyFoZtlF6lIdiiLriWhZ7vvveWZ8rsc,1984
307
+ tests/query/functions/test_math.py,sha256=Qw2MXqgY_y7vGd0bLtPhWW7HB3tJkot1o-Rh9nCmmBk,1273
308
+ tests/query/functions/test_string.py,sha256=7yNkpWNBaIowzXTP_qbmQg-mJZLWrTk0lx2mgY1NIfA,825
309
+ tests/query/functions/test_type_conversion.py,sha256=WeYR9UfJnbidle07-akQ1g9hFCd93qT8xUhDF3c58n4,3235
303
310
  tests/query/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
311
  tests/query/mixins/test_columns_delegate.py,sha256=Zw9uaqOEb7kpPQzzO9yz0jhQEeCfoPSjsy-BCLg_8XU,2032
305
312
  tests/query/mixins/test_order_by_delegate.py,sha256=mOV3Gxs0XeliONxjWSOniI1z6lbZ_xTfcGYd53JLnaY,507
@@ -356,9 +363,9 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
356
363
  tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
357
364
  tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
358
365
  tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
359
- piccolo-1.8.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
360
- piccolo-1.8.0.dist-info/METADATA,sha256=Uoe1LLvND5fOBdQKOSgFI1T047qzIlbICmwXV1zg7e4,5177
361
- piccolo-1.8.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
362
- piccolo-1.8.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
363
- piccolo-1.8.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
364
- piccolo-1.8.0.dist-info/RECORD,,
366
+ piccolo-1.9.0.dist-info/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
367
+ piccolo-1.9.0.dist-info/METADATA,sha256=DPQmRGRjgG1-6bHmApuUOwew9PIbDzX1KOEbnyxqW20,5177
368
+ piccolo-1.9.0.dist-info/WHEEL,sha256=00yskusixUoUt5ob_CiUp6LsnN5lqzTJpoqOFg_FVIc,92
369
+ piccolo-1.9.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
370
+ piccolo-1.9.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
371
+ piccolo-1.9.0.dist-info/RECORD,,
@@ -1,6 +1,8 @@
1
1
  import datetime
2
2
  from unittest import TestCase
3
3
 
4
+ import pytest
5
+
4
6
  from piccolo.columns.column_types import (
5
7
  Array,
6
8
  BigInt,
@@ -10,8 +12,9 @@ from piccolo.columns.column_types import (
10
12
  Timestamp,
11
13
  Timestamptz,
12
14
  )
15
+ from piccolo.querystring import QueryString
13
16
  from piccolo.table import Table
14
- from tests.base import engines_only, sqlite_only
17
+ from tests.base import engines_only, engines_skip, sqlite_only
15
18
 
16
19
 
17
20
  class MyTable(Table):
@@ -40,12 +43,18 @@ class TestArray(TestCase):
40
43
  def tearDown(self):
41
44
  MyTable.alter().drop_table().run_sync()
42
45
 
43
- @engines_only("postgres", "sqlite")
46
+ @pytest.mark.cockroach_array_slow
44
47
  def test_storage(self):
45
48
  """
46
49
  Make sure data can be stored and retrieved.
47
50
 
48
- 🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
51
+ In CockroachDB <= v22.2.0 we had this error:
52
+
53
+ * https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
54
+
55
+ In newer CockroachDB versions, it runs but is very slow:
56
+
57
+ * https://github.com/piccolo-orm/piccolo/issues/1005
49
58
 
50
59
  """ # noqa: E501
51
60
  MyTable(value=[1, 2, 3]).save().run_sync()
@@ -54,12 +63,19 @@ class TestArray(TestCase):
54
63
  assert row is not None
55
64
  self.assertEqual(row.value, [1, 2, 3])
56
65
 
57
- @engines_only("postgres")
66
+ @engines_skip("sqlite")
67
+ @pytest.mark.cockroach_array_slow
58
68
  def test_index(self):
59
69
  """
60
70
  Indexes should allow individual array elements to be queried.
61
71
 
62
- 🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
72
+ In CockroachDB <= v22.2.0 we had this error:
73
+
74
+ * https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
75
+
76
+ In newer CockroachDB versions, it runs but is very slow:
77
+
78
+ * https://github.com/piccolo-orm/piccolo/issues/1005
63
79
 
64
80
  """ # noqa: E501
65
81
  MyTable(value=[1, 2, 3]).save().run_sync()
@@ -68,66 +84,92 @@ class TestArray(TestCase):
68
84
  MyTable.select(MyTable.value[0]).first().run_sync(), {"value": 1}
69
85
  )
70
86
 
71
- @engines_only("postgres")
87
+ @engines_skip("sqlite")
88
+ @pytest.mark.cockroach_array_slow
72
89
  def test_all(self):
73
90
  """
74
91
  Make sure rows can be retrieved where all items in an array match a
75
92
  given value.
76
93
 
77
- 🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
94
+ In CockroachDB <= v22.2.0 we had this error:
95
+
96
+ * https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
97
+
98
+ In newer CockroachDB versions, it runs but is very slow:
99
+
100
+ * https://github.com/piccolo-orm/piccolo/issues/1005
78
101
 
79
102
  """ # noqa: E501
80
103
  MyTable(value=[1, 1, 1]).save().run_sync()
81
104
 
105
+ # We have to explicitly specify the type, so CockroachDB works.
82
106
  self.assertEqual(
83
107
  MyTable.select(MyTable.value)
84
- .where(MyTable.value.all(1))
108
+ .where(MyTable.value.all(QueryString("{}::INTEGER", 1)))
85
109
  .first()
86
110
  .run_sync(),
87
111
  {"value": [1, 1, 1]},
88
112
  )
89
113
 
114
+ # We have to explicitly specify the type, so CockroachDB works.
90
115
  self.assertEqual(
91
116
  MyTable.select(MyTable.value)
92
- .where(MyTable.value.all(0))
117
+ .where(MyTable.value.all(QueryString("{}::INTEGER", 0)))
93
118
  .first()
94
119
  .run_sync(),
95
120
  None,
96
121
  )
97
122
 
98
- @engines_only("postgres")
123
+ @engines_skip("sqlite")
124
+ @pytest.mark.cockroach_array_slow
99
125
  def test_any(self):
100
126
  """
101
127
  Make sure rows can be retrieved where any items in an array match a
102
128
  given value.
103
129
 
104
- 🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
130
+ In CockroachDB <= v22.2.0 we had this error:
131
+
132
+ * https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
133
+
134
+ In newer CockroachDB versions, it runs but is very slow:
135
+
136
+ * https://github.com/piccolo-orm/piccolo/issues/1005
105
137
 
106
138
  """ # noqa: E501
139
+
107
140
  MyTable(value=[1, 2, 3]).save().run_sync()
108
141
 
142
+ # We have to explicitly specify the type, so CockroachDB works.
109
143
  self.assertEqual(
110
144
  MyTable.select(MyTable.value)
111
- .where(MyTable.value.any(1))
145
+ .where(MyTable.value.any(QueryString("{}::INTEGER", 1)))
112
146
  .first()
113
147
  .run_sync(),
114
148
  {"value": [1, 2, 3]},
115
149
  )
116
150
 
151
+ # We have to explicitly specify the type, so CockroachDB works.
117
152
  self.assertEqual(
118
153
  MyTable.select(MyTable.value)
119
- .where(MyTable.value.any(0))
154
+ .where(MyTable.value.any(QueryString("{}::INTEGER", 0)))
120
155
  .first()
121
156
  .run_sync(),
122
157
  None,
123
158
  )
124
159
 
125
- @engines_only("postgres")
160
+ @engines_skip("sqlite")
161
+ @pytest.mark.cockroach_array_slow
126
162
  def test_cat(self):
127
163
  """
128
164
  Make sure values can be appended to an array.
129
165
 
130
- 🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
166
+ In CockroachDB <= v22.2.0 we had this error:
167
+
168
+ * https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
169
+
170
+ In newer CockroachDB versions, it runs but is very slow:
171
+
172
+ * https://github.com/piccolo-orm/piccolo/issues/1005
131
173
 
132
174
  """ # noqa: E501
133
175
  MyTable(value=[1, 1, 1]).save().run_sync()
@@ -137,7 +179,8 @@ class TestArray(TestCase):
137
179
  ).run_sync()
138
180
 
139
181
  self.assertEqual(
140
- MyTable.select().run_sync(), [{"id": 1, "value": [1, 1, 1, 2]}]
182
+ MyTable.select(MyTable.value).run_sync(),
183
+ [{"value": [1, 1, 1, 2]}],
141
184
  )
142
185
 
143
186
  # Try plus symbol
@@ -147,7 +190,8 @@ class TestArray(TestCase):
147
190
  ).run_sync()
148
191
 
149
192
  self.assertEqual(
150
- MyTable.select().run_sync(), [{"id": 1, "value": [1, 1, 1, 2, 3]}]
193
+ MyTable.select(MyTable.value).run_sync(),
194
+ [{"value": [1, 1, 1, 2, 3]}],
151
195
  )
152
196
 
153
197
  # Make sure non-list values work
@@ -157,8 +201,8 @@ class TestArray(TestCase):
157
201
  ).run_sync()
158
202
 
159
203
  self.assertEqual(
160
- MyTable.select().run_sync(),
161
- [{"id": 1, "value": [1, 1, 1, 2, 3, 4]}],
204
+ MyTable.select(MyTable.value).run_sync(),
205
+ [{"value": [1, 1, 1, 2, 3, 4]}],
162
206
  )
163
207
 
164
208
  @sqlite_only
@@ -0,0 +1,66 @@
1
+ import datetime
2
+ from unittest import TestCase
3
+
4
+ from tests.base import engines_only
5
+ from tests.example_apps.music.tables import Band
6
+
7
+
8
+ @engines_only("postgres", "cockroach")
9
+ class TestArrayPostgres(TestCase):
10
+
11
+ def test_string(self):
12
+ self.assertEqual(
13
+ Band.name.get_sql_value(["a", "b", "c"]),
14
+ '\'{"a","b","c"}\'',
15
+ )
16
+
17
+ def test_int(self):
18
+ self.assertEqual(
19
+ Band.name.get_sql_value([1, 2, 3]),
20
+ "'{1,2,3}'",
21
+ )
22
+
23
+ def test_nested(self):
24
+ self.assertEqual(
25
+ Band.name.get_sql_value([1, 2, 3, [4, 5, 6]]),
26
+ "'{1,2,3,{4,5,6}}'",
27
+ )
28
+
29
+ def test_time(self):
30
+ self.assertEqual(
31
+ Band.name.get_sql_value([datetime.time(hour=8, minute=0)]),
32
+ "'{\"08:00:00\"}'",
33
+ )
34
+
35
+
36
+ @engines_only("sqlite")
37
+ class TestArraySQLite(TestCase):
38
+ """
39
+ Note, we use ``.replace(" ", "")`` because we serialise arrays using
40
+ Python's json library, and there is inconsistency between Python versions
41
+ (some output ``["a", "b", "c"]``, and others ``["a","b","c"]``).
42
+ """
43
+
44
+ def test_string(self):
45
+ self.assertEqual(
46
+ Band.name.get_sql_value(["a", "b", "c"]).replace(" ", ""),
47
+ '\'["a","b","c"]\'',
48
+ )
49
+
50
+ def test_int(self):
51
+ self.assertEqual(
52
+ Band.name.get_sql_value([1, 2, 3]).replace(" ", ""),
53
+ "'[1,2,3]'",
54
+ )
55
+
56
+ def test_nested(self):
57
+ self.assertEqual(
58
+ Band.name.get_sql_value([1, 2, 3, [4, 5, 6]]).replace(" ", ""),
59
+ "'[1,2,3,[4,5,6]]'",
60
+ )
61
+
62
+ def test_time(self):
63
+ self.assertEqual(
64
+ Band.name.get_sql_value([datetime.time(hour=8, minute=0)]),
65
+ "'[\"08:00:00\"]'",
66
+ )
File without changes
@@ -0,0 +1,34 @@
1
+ import typing as t
2
+ from unittest import TestCase
3
+
4
+ from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
5
+ from tests.example_apps.music.tables import Band, Manager
6
+
7
+
8
+ class FunctionTest(TestCase):
9
+ tables: t.List[t.Type[Table]]
10
+
11
+ def setUp(self) -> None:
12
+ create_db_tables_sync(*self.tables)
13
+
14
+ def tearDown(self) -> None:
15
+ drop_db_tables_sync(*self.tables)
16
+
17
+
18
+ class BandTest(FunctionTest):
19
+ tables = [Band, Manager]
20
+
21
+ def setUp(self) -> None:
22
+ super().setUp()
23
+
24
+ manager = Manager({Manager.name: "Guido"})
25
+ manager.save().run_sync()
26
+
27
+ band = Band(
28
+ {
29
+ Band.name: "Pythonistas",
30
+ Band.manager: manager,
31
+ Band.popularity: 1000,
32
+ }
33
+ )
34
+ band.save().run_sync()
@@ -0,0 +1,64 @@
1
+ from piccolo.query.functions import Reverse, Upper
2
+ from piccolo.querystring import QueryString
3
+ from tests.base import engines_skip
4
+ from tests.example_apps.music.tables import Band
5
+
6
+ from .base import BandTest
7
+
8
+
9
+ @engines_skip("sqlite")
10
+ class TestNested(BandTest):
11
+ """
12
+ Skip the the test for SQLite, as it doesn't support ``Reverse``.
13
+ """
14
+
15
+ def test_nested(self):
16
+ """
17
+ Make sure we can nest functions.
18
+ """
19
+ response = Band.select(Upper(Reverse(Band.name))).run_sync()
20
+ self.assertListEqual(response, [{"upper": "SATSINOHTYP"}])
21
+
22
+ def test_nested_with_joined_column(self):
23
+ """
24
+ Make sure nested functions can be used on a column from a joined table.
25
+ """
26
+ response = Band.select(Upper(Reverse(Band.manager._.name))).run_sync()
27
+ self.assertListEqual(response, [{"upper": "ODIUG"}])
28
+
29
+ def test_nested_within_querystring(self):
30
+ """
31
+ If we wrap a function in a custom QueryString - make sure the columns
32
+ are still accessible, so joins are successful.
33
+ """
34
+ response = Band.select(
35
+ QueryString("CONCAT({}, '!')", Upper(Band.manager._.name)),
36
+ ).run_sync()
37
+
38
+ self.assertListEqual(response, [{"concat": "GUIDO!"}])
39
+
40
+
41
+ class TestWhereClause(BandTest):
42
+
43
+ def test_where(self):
44
+ """
45
+ Make sure where clauses work with functions.
46
+ """
47
+ response = (
48
+ Band.select(Band.name)
49
+ .where(Upper(Band.name) == "PYTHONISTAS")
50
+ .run_sync()
51
+ )
52
+ self.assertListEqual(response, [{"name": "Pythonistas"}])
53
+
54
+ def test_where_with_joined_column(self):
55
+ """
56
+ Make sure where clauses work with functions, when a joined column is
57
+ used.
58
+ """
59
+ response = (
60
+ Band.select(Band.name)
61
+ .where(Upper(Band.manager._.name) == "GUIDO")
62
+ .run_sync()
63
+ )
64
+ self.assertListEqual(response, [{"name": "Pythonistas"}])
@@ -0,0 +1,39 @@
1
+ import decimal
2
+
3
+ from piccolo.columns import Numeric
4
+ from piccolo.query.functions.math import Abs, Ceil, Floor, Round
5
+ from piccolo.table import Table
6
+
7
+ from .base import FunctionTest
8
+
9
+
10
+ class Ticket(Table):
11
+ price = Numeric(digits=(5, 2))
12
+
13
+
14
+ class TestMath(FunctionTest):
15
+
16
+ tables = [Ticket]
17
+
18
+ def setUp(self):
19
+ super().setUp()
20
+ self.ticket = Ticket({Ticket.price: decimal.Decimal("36.50")})
21
+ self.ticket.save().run_sync()
22
+
23
+ def test_floor(self):
24
+ response = Ticket.select(Floor(Ticket.price, alias="price")).run_sync()
25
+ self.assertListEqual(response, [{"price": decimal.Decimal("36.00")}])
26
+
27
+ def test_ceil(self):
28
+ response = Ticket.select(Ceil(Ticket.price, alias="price")).run_sync()
29
+ self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])
30
+
31
+ def test_abs(self):
32
+ self.ticket.price = decimal.Decimal("-1.50")
33
+ self.ticket.save().run_sync()
34
+ response = Ticket.select(Abs(Ticket.price, alias="price")).run_sync()
35
+ self.assertListEqual(response, [{"price": decimal.Decimal("1.50")}])
36
+
37
+ def test_round(self):
38
+ response = Ticket.select(Round(Ticket.price, alias="price")).run_sync()
39
+ self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])
@@ -0,0 +1,25 @@
1
+ from piccolo.query.functions.string import Upper
2
+ from tests.example_apps.music.tables import Band
3
+
4
+ from .base import BandTest
5
+
6
+
7
+ class TestUpperFunction(BandTest):
8
+
9
+ def test_column(self):
10
+ """
11
+ Make sure we can uppercase a column's value.
12
+ """
13
+ response = Band.select(Upper(Band.name)).run_sync()
14
+ self.assertListEqual(response, [{"upper": "PYTHONISTAS"}])
15
+
16
+ def test_alias(self):
17
+ response = Band.select(Upper(Band.name, alias="name")).run_sync()
18
+ self.assertListEqual(response, [{"name": "PYTHONISTAS"}])
19
+
20
+ def test_joined_column(self):
21
+ """
22
+ Make sure we can uppercase a column's value from a joined table.
23
+ """
24
+ response = Band.select(Upper(Band.manager._.name)).run_sync()
25
+ self.assertListEqual(response, [{"upper": "GUIDO"}])
@@ -0,0 +1,134 @@
1
+ from piccolo.columns import Integer, Text, Varchar
2
+ from piccolo.query.functions import Cast, Length
3
+ from tests.example_apps.music.tables import Band, Manager
4
+
5
+ from .base import BandTest
6
+
7
+
8
+ class TestCast(BandTest):
9
+ def test_varchar(self):
10
+ """
11
+ Make sure that casting to ``Varchar`` works.
12
+ """
13
+ response = Band.select(
14
+ Cast(
15
+ Band.popularity,
16
+ as_type=Varchar(),
17
+ )
18
+ ).run_sync()
19
+
20
+ self.assertListEqual(
21
+ response,
22
+ [{"popularity": "1000"}],
23
+ )
24
+
25
+ def test_text(self):
26
+ """
27
+ Make sure that casting to ``Text`` works.
28
+ """
29
+ response = Band.select(
30
+ Cast(
31
+ Band.popularity,
32
+ as_type=Text(),
33
+ )
34
+ ).run_sync()
35
+
36
+ self.assertListEqual(
37
+ response,
38
+ [{"popularity": "1000"}],
39
+ )
40
+
41
+ def test_integer(self):
42
+ """
43
+ Make sure that casting to ``Integer`` works.
44
+ """
45
+ Band.update({Band.name: "1111"}, force=True).run_sync()
46
+
47
+ response = Band.select(
48
+ Cast(
49
+ Band.name,
50
+ as_type=Integer(),
51
+ )
52
+ ).run_sync()
53
+
54
+ self.assertListEqual(
55
+ response,
56
+ [{"name": 1111}],
57
+ )
58
+
59
+ def test_join(self):
60
+ """
61
+ Make sure that casting works with joins.
62
+ """
63
+ Manager.update({Manager.name: "1111"}, force=True).run_sync()
64
+
65
+ response = Band.select(
66
+ Band.name,
67
+ Cast(
68
+ Band.manager.name,
69
+ as_type=Integer(),
70
+ ),
71
+ ).run_sync()
72
+
73
+ self.assertListEqual(
74
+ response,
75
+ [
76
+ {
77
+ "name": "Pythonistas",
78
+ "manager.name": 1111,
79
+ }
80
+ ],
81
+ )
82
+
83
+ def test_nested_inner(self):
84
+ """
85
+ Make sure ``Cast`` can be passed into other functions.
86
+ """
87
+ Band.update({Band.name: "1111"}, force=True).run_sync()
88
+
89
+ response = Band.select(
90
+ Length(
91
+ Cast(
92
+ Band.popularity,
93
+ as_type=Varchar(),
94
+ )
95
+ )
96
+ ).run_sync()
97
+
98
+ self.assertListEqual(
99
+ response,
100
+ [{"length": 4}],
101
+ )
102
+
103
+ def test_nested_outer(self):
104
+ """
105
+ Make sure a querystring can be passed into ``Cast`` (meaning it can be
106
+ nested).
107
+ """
108
+ response = Band.select(
109
+ Cast(
110
+ Length(Band.name),
111
+ as_type=Varchar(),
112
+ alias="length",
113
+ )
114
+ ).run_sync()
115
+
116
+ self.assertListEqual(
117
+ response,
118
+ [{"length": str(len("Pythonistas"))}],
119
+ )
120
+
121
+ def test_where_clause(self):
122
+ """
123
+ Make sure ``Cast`` works in a where clause.
124
+ """
125
+ response = (
126
+ Band.select(Band.name, Band.popularity)
127
+ .where(Cast(Band.popularity, Varchar()) == "1000")
128
+ .run_sync()
129
+ )
130
+
131
+ self.assertListEqual(
132
+ response,
133
+ [{"name": "Pythonistas", "popularity": 1000}],
134
+ )
@@ -1,6 +1,7 @@
1
1
  from unittest import TestCase
2
2
 
3
3
  from piccolo.querystring import QueryString
4
+ from tests.base import postgres_only
4
5
 
5
6
 
6
7
  # TODO - add more extensive tests (increased nesting and argument count).
@@ -28,3 +29,138 @@ class TestQueryString(TestCase):
28
29
  def test_querystring_with_no_args(self):
29
30
  qs = QueryString("SELECT name FROM band")
30
31
  self.assertEqual(qs.compile_string(), ("SELECT name FROM band", []))
32
+
33
+
34
+ @postgres_only
35
+ class TestQueryStringOperators(TestCase):
36
+ """
37
+ Make sure basic operations can be used on ``QueryString``.
38
+ """
39
+
40
+ def test_add(self):
41
+ query = QueryString("SELECT price") + 1
42
+ self.assertIsInstance(query, QueryString)
43
+ self.assertEqual(
44
+ query.compile_string(),
45
+ ("SELECT price + $1", [1]),
46
+ )
47
+
48
+ def test_multiply(self):
49
+ query = QueryString("SELECT price") * 2
50
+ self.assertIsInstance(query, QueryString)
51
+ self.assertEqual(
52
+ query.compile_string(),
53
+ ("SELECT price * $1", [2]),
54
+ )
55
+
56
+ def test_divide(self):
57
+ query = QueryString("SELECT price") / 1
58
+ self.assertIsInstance(query, QueryString)
59
+ self.assertEqual(
60
+ query.compile_string(),
61
+ ("SELECT price / $1", [1]),
62
+ )
63
+
64
+ def test_power(self):
65
+ query = QueryString("SELECT price") ** 2
66
+ self.assertIsInstance(query, QueryString)
67
+ self.assertEqual(
68
+ query.compile_string(),
69
+ ("SELECT price ^ $1", [2]),
70
+ )
71
+
72
+ def test_subtract(self):
73
+ query = QueryString("SELECT price") - 1
74
+ self.assertIsInstance(query, QueryString)
75
+ self.assertEqual(
76
+ query.compile_string(),
77
+ ("SELECT price - $1", [1]),
78
+ )
79
+
80
+ def test_modulus(self):
81
+ query = QueryString("SELECT price") % 1
82
+ self.assertIsInstance(query, QueryString)
83
+ self.assertEqual(
84
+ query.compile_string(),
85
+ ("SELECT price % $1", [1]),
86
+ )
87
+
88
+ def test_like(self):
89
+ query = QueryString("strip(name)").like("Python%")
90
+ self.assertIsInstance(query, QueryString)
91
+ self.assertEqual(
92
+ query.compile_string(),
93
+ ("strip(name) LIKE $1", ["Python%"]),
94
+ )
95
+
96
+ def test_ilike(self):
97
+ query = QueryString("strip(name)").ilike("Python%")
98
+ self.assertIsInstance(query, QueryString)
99
+ self.assertEqual(
100
+ query.compile_string(),
101
+ ("strip(name) ILIKE $1", ["Python%"]),
102
+ )
103
+
104
+ def test_greater_than(self):
105
+ query = QueryString("SELECT price") > 10
106
+ self.assertIsInstance(query, QueryString)
107
+ self.assertEqual(
108
+ query.compile_string(),
109
+ ("SELECT price > $1", [10]),
110
+ )
111
+
112
+ def test_greater_equal_than(self):
113
+ query = QueryString("SELECT price") >= 10
114
+ self.assertIsInstance(query, QueryString)
115
+ self.assertEqual(
116
+ query.compile_string(),
117
+ ("SELECT price >= $1", [10]),
118
+ )
119
+
120
+ def test_less_than(self):
121
+ query = QueryString("SELECT price") < 10
122
+ self.assertIsInstance(query, QueryString)
123
+ self.assertEqual(
124
+ query.compile_string(),
125
+ ("SELECT price < $1", [10]),
126
+ )
127
+
128
+ def test_less_equal_than(self):
129
+ query = QueryString("SELECT price") <= 10
130
+ self.assertIsInstance(query, QueryString)
131
+ self.assertEqual(
132
+ query.compile_string(),
133
+ ("SELECT price <= $1", [10]),
134
+ )
135
+
136
+ def test_equals(self):
137
+ query = QueryString("SELECT price") == 10
138
+ self.assertIsInstance(query, QueryString)
139
+ self.assertEqual(
140
+ query.compile_string(),
141
+ ("SELECT price = $1", [10]),
142
+ )
143
+
144
+ def test_not_equals(self):
145
+ query = QueryString("SELECT price") != 10
146
+ self.assertIsInstance(query, QueryString)
147
+ self.assertEqual(
148
+ query.compile_string(),
149
+ ("SELECT price != $1", [10]),
150
+ )
151
+
152
+ def test_is_in(self):
153
+ query = QueryString("SELECT price").is_in([10, 20, 30])
154
+ self.assertIsInstance(query, QueryString)
155
+ self.assertEqual(
156
+ query.compile_string(),
157
+ ("SELECT price IN $1", [[10, 20, 30]]),
158
+ )
159
+
160
+ def test_not_in(self):
161
+ query = QueryString("SELECT price").not_in([10, 20, 30])
162
+ self.assertIsInstance(query, QueryString)
163
+ self.assertEqual(
164
+ query.compile_string(),
165
+ ("SELECT price NOT IN $1", [[10, 20, 30]]),
166
+ )
@@ -1,238 +0,0 @@
1
- from unittest import TestCase
2
-
3
- from piccolo.columns import Integer, Text, Varchar
4
- from piccolo.query.functions import Cast, Length, Reverse, Upper
5
- from piccolo.querystring import QueryString
6
- from piccolo.table import create_db_tables_sync, drop_db_tables_sync
7
- from tests.base import engines_skip
8
- from tests.example_apps.music.tables import Band, Manager
9
-
10
-
11
- class FunctionTest(TestCase):
12
- tables = (Band, Manager)
13
-
14
- def setUp(self) -> None:
15
- create_db_tables_sync(*self.tables)
16
-
17
- manager = Manager({Manager.name: "Guido"})
18
- manager.save().run_sync()
19
-
20
- band = Band(
21
- {
22
- Band.name: "Pythonistas",
23
- Band.manager: manager,
24
- Band.popularity: 1000,
25
- }
26
- )
27
- band.save().run_sync()
28
-
29
- def tearDown(self) -> None:
30
- drop_db_tables_sync(*self.tables)
31
-
32
-
33
- class TestUpperFunction(FunctionTest):
34
-
35
- def test_column(self):
36
- """
37
- Make sure we can uppercase a column's value.
38
- """
39
- response = Band.select(Upper(Band.name)).run_sync()
40
- self.assertListEqual(response, [{"upper": "PYTHONISTAS"}])
41
-
42
- def test_alias(self):
43
- response = Band.select(Upper(Band.name, alias="name")).run_sync()
44
- self.assertListEqual(response, [{"name": "PYTHONISTAS"}])
45
-
46
- def test_joined_column(self):
47
- """
48
- Make sure we can uppercase a column's value from a joined table.
49
- """
50
- response = Band.select(Upper(Band.manager._.name)).run_sync()
51
- self.assertListEqual(response, [{"upper": "GUIDO"}])
52
-
53
-
54
- @engines_skip("sqlite")
55
- class TestNested(FunctionTest):
56
- """
57
- Skip the the test for SQLite, as it doesn't support ``Reverse``.
58
- """
59
-
60
- def test_nested(self):
61
- """
62
- Make sure we can nest functions.
63
- """
64
- response = Band.select(Upper(Reverse(Band.name))).run_sync()
65
- self.assertListEqual(response, [{"upper": "SATSINOHTYP"}])
66
-
67
- def test_nested_with_joined_column(self):
68
- """
69
- Make sure nested functions can be used on a column from a joined table.
70
- """
71
- response = Band.select(Upper(Reverse(Band.manager._.name))).run_sync()
72
- self.assertListEqual(response, [{"upper": "ODIUG"}])
73
-
74
- def test_nested_within_querystring(self):
75
- """
76
- If we wrap a function in a custom QueryString - make sure the columns
77
- are still accessible, so joins are successful.
78
- """
79
- response = Band.select(
80
- QueryString("CONCAT({}, '!')", Upper(Band.manager._.name)),
81
- ).run_sync()
82
-
83
- self.assertListEqual(response, [{"concat": "GUIDO!"}])
84
-
85
-
86
- class TestWhereClause(FunctionTest):
87
-
88
- def test_where(self):
89
- """
90
- Make sure where clauses work with functions.
91
- """
92
- response = (
93
- Band.select(Band.name)
94
- .where(Upper(Band.name) == "PYTHONISTAS")
95
- .run_sync()
96
- )
97
- self.assertListEqual(response, [{"name": "Pythonistas"}])
98
-
99
- def test_where_with_joined_column(self):
100
- """
101
- Make sure where clauses work with functions, when a joined column is
102
- used.
103
- """
104
- response = (
105
- Band.select(Band.name)
106
- .where(Upper(Band.manager._.name) == "GUIDO")
107
- .run_sync()
108
- )
109
- self.assertListEqual(response, [{"name": "Pythonistas"}])
110
-
111
-
112
- class TestCast(FunctionTest):
113
- def test_varchar(self):
114
- """
115
- Make sure that casting to ``Varchar`` works.
116
- """
117
- response = Band.select(
118
- Cast(
119
- Band.popularity,
120
- as_type=Varchar(),
121
- )
122
- ).run_sync()
123
-
124
- self.assertListEqual(
125
- response,
126
- [{"popularity": "1000"}],
127
- )
128
-
129
- def test_text(self):
130
- """
131
- Make sure that casting to ``Text`` works.
132
- """
133
- response = Band.select(
134
- Cast(
135
- Band.popularity,
136
- as_type=Text(),
137
- )
138
- ).run_sync()
139
-
140
- self.assertListEqual(
141
- response,
142
- [{"popularity": "1000"}],
143
- )
144
-
145
- def test_integer(self):
146
- """
147
- Make sure that casting to ``Integer`` works.
148
- """
149
- Band.update({Band.name: "1111"}, force=True).run_sync()
150
-
151
- response = Band.select(
152
- Cast(
153
- Band.name,
154
- as_type=Integer(),
155
- )
156
- ).run_sync()
157
-
158
- self.assertListEqual(
159
- response,
160
- [{"name": 1111}],
161
- )
162
-
163
- def test_join(self):
164
- """
165
- Make sure that casting works with joins.
166
- """
167
- Manager.update({Manager.name: "1111"}, force=True).run_sync()
168
-
169
- response = Band.select(
170
- Band.name,
171
- Cast(
172
- Band.manager.name,
173
- as_type=Integer(),
174
- ),
175
- ).run_sync()
176
-
177
- self.assertListEqual(
178
- response,
179
- [
180
- {
181
- "name": "Pythonistas",
182
- "manager.name": 1111,
183
- }
184
- ],
185
- )
186
-
187
- def test_nested_inner(self):
188
- """
189
- Make sure ``Cast`` can be passed into other functions.
190
- """
191
- Band.update({Band.name: "1111"}, force=True).run_sync()
192
-
193
- response = Band.select(
194
- Length(
195
- Cast(
196
- Band.popularity,
197
- as_type=Varchar(),
198
- )
199
- )
200
- ).run_sync()
201
-
202
- self.assertListEqual(
203
- response,
204
- [{"length": 4}],
205
- )
206
-
207
- def test_nested_outer(self):
208
- """
209
- Make sure a querystring can be passed into ``Cast`` (meaning it can be
210
- nested).
211
- """
212
- response = Band.select(
213
- Cast(
214
- Length(Band.name),
215
- as_type=Varchar(),
216
- alias="length",
217
- )
218
- ).run_sync()
219
-
220
- self.assertListEqual(
221
- response,
222
- [{"length": str(len("Pythonistas"))}],
223
- )
224
-
225
- def test_where_clause(self):
226
- """
227
- Make sure ``Cast`` works in a where clause.
228
- """
229
- response = (
230
- Band.select(Band.name, Band.popularity)
231
- .where(Cast(Band.popularity, Varchar()) == "1000")
232
- .run_sync()
233
- )
234
-
235
- self.assertListEqual(
236
- response,
237
- [{"name": "Pythonistas", "popularity": 1000}],
238
- )