python-sql 1.7.0__py3-none-any.whl → 1.8.1__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.
@@ -1,45 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sql
3
- Version: 1.7.0
3
+ Version: 1.8.1
4
4
  Summary: Library to write SQL queries
5
- Home-page: https://pypi.org/project/python-sql/
6
- Download-URL: https://downloads.tryton.org/python-sql/
7
- Author: Tryton
8
- Author-email: foundation@tryton.org
9
- License: BSD
10
- Project-URL: Bug Tracker, https://bugs.tryton.org/python-sql
11
- Project-URL: Forum, https://discuss.tryton.org/tags/python-sql
12
- Project-URL: Source Code, https://code.tryton.org/python-sql
13
- Keywords: SQL database query
5
+ Project-URL: homepage, https://www.tryton.org/
6
+ Project-URL: changelog, https://code.tryton.org/python-sql/-/blob/branch/default/CHANGELOG
7
+ Project-URL: forum, https://discuss.tryton.org/tags/python-sql
8
+ Project-URL: issues, https://bugs.tryton.org/python-sql
9
+ Project-URL: repository, https://code.tryton.org/python-sql
10
+ Author: B2CK SRL
11
+ Author-email: Cédric Krier <cedric.krier@b2ck.com>, Nicolas Évrard <nicolas.evrard@b2ck.com>
12
+ Maintainer-email: Tryton <foundation@tryton.org>
13
+ License-Expression: BSD-3-Clause
14
+ License-File: COPYRIGHT
15
+ Keywords: SQL,database,query
14
16
  Classifier: Development Status :: 5 - Production/Stable
15
17
  Classifier: Intended Audience :: Developers
16
- Classifier: License :: OSI Approved :: BSD License
17
- Classifier: Operating System :: OS Independent
18
- Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.5
20
- Classifier: Programming Language :: Python :: 3.6
21
- Classifier: Programming Language :: Python :: 3.7
22
- Classifier: Programming Language :: Python :: 3.8
23
- Classifier: Programming Language :: Python :: 3.9
24
- Classifier: Programming Language :: Python :: 3.10
25
- Classifier: Programming Language :: Python :: 3.11
26
- Classifier: Programming Language :: Python :: 3.12
27
- Classifier: Programming Language :: Python :: 3.13
28
- Classifier: Programming Language :: Python :: 3.14
29
18
  Classifier: Topic :: Database
30
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
- Requires-Python: >=3.5
32
- Dynamic: author
33
- Dynamic: author-email
34
- Dynamic: classifier
35
- Dynamic: description
36
- Dynamic: download-url
37
- Dynamic: home-page
38
- Dynamic: keywords
39
- Dynamic: license
40
- Dynamic: project-url
41
- Dynamic: requires-python
42
- Dynamic: summary
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/x-rst
43
22
 
44
23
  python-sql
45
24
  ==========
@@ -82,14 +61,14 @@ Select with where condition::
82
61
 
83
62
  >>> select.where = user.name == 'foo'
84
63
  >>> tuple(select)
85
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s)', ('foo',))
64
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE "a"."name" = %s', ('foo',))
86
65
 
87
66
  >>> select.where = (user.name == 'foo') & (user.active == True)
88
67
  >>> tuple(select)
89
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE (("a"."name" = %s) AND ("a"."active" = %s))', ('foo', True))
68
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s) AND ("a"."active" = %s)', ('foo', True))
90
69
  >>> select.where = user.name == user.login
91
70
  >>> tuple(select)
92
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = "a"."login")', ())
71
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE "a"."name" = "a"."login"', ())
93
72
 
94
73
  Select with join::
95
74
 
@@ -97,7 +76,7 @@ Select with join::
97
76
  >>> join.condition = join.right.user == user.id
98
77
  >>> select = join.select(user.name, join.right.group)
99
78
  >>> tuple(select)
100
- ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON ("b"."user" = "a"."id")', ())
79
+ ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON "b"."user" = "a"."id"', ())
101
80
 
102
81
  Select with multiple joins::
103
82
 
@@ -136,9 +115,9 @@ Select with sub-select::
136
115
  ... where=user_group.active == True)
137
116
  >>> user = Table('user')
138
117
  >>> tuple(user.select(user.id, where=user.id.in_(subselect)))
139
- ('SELECT "a"."id" FROM "user" AS "a" WHERE ("a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)))', (True,))
118
+ ('SELECT "a"."id" FROM "user" AS "a" WHERE "a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE "b"."active" = %s)', (True,))
140
119
  >>> tuple(subselect.select(subselect.user))
141
- ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)) AS "a"', (True,))
120
+ ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE "b"."active" = %s) AS "a"', (True,))
142
121
 
143
122
  Select on other schema::
144
123
 
@@ -172,20 +151,20 @@ Update query with values::
172
151
  >>> tuple(user.update(columns=[user.active], values=[True]))
173
152
  ('UPDATE "user" AS "a" SET "active" = %s', (True,))
174
153
  >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
175
- ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ())
154
+ ('UPDATE "invoice" AS "a" SET "total" = "a"."amount" + "a"."tax"', ())
176
155
 
177
156
  Update query with where condition::
178
157
 
179
158
  >>> tuple(user.update(columns=[user.active], values=[True],
180
159
  ... where=user.active == False))
181
- ('UPDATE "user" AS "a" SET "active" = %s WHERE ("a"."active" = %s)', (True, False))
160
+ ('UPDATE "user" AS "a" SET "active" = %s WHERE "a"."active" = %s', (True, False))
182
161
 
183
162
  Update query with from list::
184
163
 
185
164
  >>> group = Table('user_group')
186
165
  >>> tuple(user.update(columns=[user.active], values=[group.active],
187
166
  ... from_=[group], where=user.id == group.user))
188
- ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE ("b"."id" = "a"."user")', ())
167
+ ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE "b"."id" = "a"."user"', ())
189
168
 
190
169
  Delete query::
191
170
 
@@ -195,13 +174,13 @@ Delete query::
195
174
  Delete query with where condition::
196
175
 
197
176
  >>> tuple(user.delete(where=user.name == 'foo'))
198
- ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',))
177
+ ('DELETE FROM "user" WHERE "name" = %s', ('foo',))
199
178
 
200
179
  Delete query with sub-query::
201
180
 
202
181
  >>> tuple(user.delete(
203
182
  ... where=user.id.in_(user_group.select(user_group.user))))
204
- ('DELETE FROM "user" WHERE ("id" IN (SELECT "a"."user" FROM "user_group" AS "a"))', ())
183
+ ('DELETE FROM "user" WHERE "id" IN (SELECT "a"."user" FROM "user_group" AS "a")', ())
205
184
 
206
185
  Flavors::
207
186
 
@@ -228,7 +207,7 @@ Limit style::
228
207
  ('SELECT * FROM "user" AS "a" OFFSET (%s) ROWS FETCH FIRST (%s) ROWS ONLY', (20, 10))
229
208
  >>> Flavor.set(Flavor(limitstyle='rownum'))
230
209
  >>> tuple(select)
231
- ('SELECT "a".* FROM (SELECT "b".*, ROWNUM AS "rnum" FROM (SELECT * FROM "user" AS "c") AS "b" WHERE (ROWNUM <= %s)) AS "a" WHERE ("rnum" > %s)', (30, 20))
210
+ ('SELECT "a".* FROM (SELECT "b".*, ROWNUM AS "rnum" FROM (SELECT * FROM "user" AS "c") AS "b" WHERE ROWNUM <= %s) AS "a" WHERE "rnum" > %s', (30, 20))
232
211
 
233
212
  qmark style::
234
213
 
@@ -236,7 +215,7 @@ qmark style::
236
215
  >>> select = user.select()
237
216
  >>> select.where = user.name == 'foo'
238
217
  >>> tuple(select)
239
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
218
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = ?', ('foo',))
240
219
 
241
220
  numeric style::
242
221
 
@@ -244,4 +223,4 @@ numeric style::
244
223
  >>> select = user.select()
245
224
  >>> select.where = user.name == 'foo'
246
225
  >>> format2numeric(*select)
247
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',))
226
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = :0', ('foo',))
@@ -0,0 +1,41 @@
1
+ sql/__init__.py,sha256=PUwBTtKhP7r6YWZEtv-7IwYF3OgICgCgHDVPFNqiVOk,65346
2
+ sql/aggregate.py,sha256=Spg5T4y8SSUN5Tyc7GUVB6I_69AhrVHVCkOgzFX482A,6008
3
+ sql/conditionals.py,sha256=xJY6ffEBeES6CiGKErXSa2dK2FXaEaR_QQl_CKILP30,2541
4
+ sql/functions.py,sha256=htradjtMpm9HfDlgSnjKi-rFWRvX5ch8odE-ngAah1I,14125
5
+ sql/operators.py,sha256=wy_11xRXjk6ZclkA7nhJ4QTW0g7bEC45-LH6nmTXuYE,11480
6
+ sql/tests/__init__.py,sha256=QA18gftL_tykRkbGriAobA3THDVu34v8Pdn-a2qJywY,679
7
+ sql/tests/test_aggregate.py,sha256=dFoqNd1fqsyYN8rNX_EGSF1rKKFtiprVMfDmdH9TyYQ,3359
8
+ sql/tests/test_alias.py,sha256=wtBwMkA8HADz3Tq6kz-MJXBG75hL0AbO2Z3LE7L2YTY,2408
9
+ sql/tests/test_as.py,sha256=OfXDu0CnJ2paRKR29dMdx7q_Q9TFY60MWlvib6MR5LY,926
10
+ sql/tests/test_cast.py,sha256=ejrqHQZwA3967UhszYR3caZYvH3waPcF6li4rnmxGi4,653
11
+ sql/tests/test_collate.py,sha256=_RJM_t-YzCoO34b6k35jBoTyQedTK7EXywYosrK1bUQ,688
12
+ sql/tests/test_column.py,sha256=_GkBkBzLW_fDoJy5yUWDtHTSyka1s5Drd4gDuvtIM_8,866
13
+ sql/tests/test_combining_query.py,sha256=TyxcakaXq3gAKqdK5lRQqWhLzpmx1n03L_MTa55ZIXI,2413
14
+ sql/tests/test_conditionals.py,sha256=Qlv10nzrHcqto-fZmEHW7mKFifQa_lmYSnrn7jBIRM0,2699
15
+ sql/tests/test_delete.py,sha256=mLnfhKJwfKhsabYuenOXrDr5CTS2Lx_wIs3VQO-u9pM,2237
16
+ sql/tests/test_excluded.py,sha256=aOdzg9oOTeaCPzQ2MOBC2F-KJl1hWI39MnM2cfaVSHs,388
17
+ sql/tests/test_expression.py,sha256=39Z4hZXxUW6vJDmWMKJGpdf1tMP5i-1bfNaYvrHv2JY,457
18
+ sql/tests/test_flavor.py,sha256=ho8wjHDbZDVB0NYN_lCPL3MdyzBhYYfCxolR1wQv3qU,1240
19
+ sql/tests/test_for.py,sha256=_qk3ovyPtcq4XBK21OzmovLPiqgSvhGc4nhpX8IYC4s,651
20
+ sql/tests/test_from.py,sha256=USwyW8WnbRO_oQBc-Fmgv9rwhARY2bOF-k5ZBxozyIw,713
21
+ sql/tests/test_from_item.py,sha256=SJosqjO8UKPNppgtYRN_HKFlu17j5Aw6OVAQ4BGSNXo,1177
22
+ sql/tests/test_functions.py,sha256=SmH9GNJNSUdEzwlWNeRgGH3kD3KPMPq3FdKwPULMU6Q,7074
23
+ sql/tests/test_grouping.py,sha256=hdUOeEhYIuKxvLrSmkFZlIW93uMq8exbtp9XqEodgEE,338
24
+ sql/tests/test_insert.py,sha256=yxQwN0GM6dHbXEoTRhle-W7ad_mAy_gHFWd6uhU-u1c,9359
25
+ sql/tests/test_join.py,sha256=xJtwC5OjCDXqG8FR-Ga6zds4TORReY70p-RHZuuv0no,2639
26
+ sql/tests/test_lateral.py,sha256=uNPIo-AvMmD7mSwAtPSB0FlHXRRUF4B43G-iKBJFizA,1045
27
+ sql/tests/test_literal.py,sha256=ht-VLhGdpUm_rbtOzpQgbX3YQup_xp7F9va0RSWY0Ts,1010
28
+ sql/tests/test_merge.py,sha256=JEwJ7hJ6Wpg_MqNcdA8uEog6_NQVtxuR-_IEQWpvAWQ,5436
29
+ sql/tests/test_operators.py,sha256=K-B5Rh8-sy88aTjuCsQqxXaK6xKBqMzar8TKSsPLiJk,16752
30
+ sql/tests/test_order.py,sha256=dcBGxPxaGW5S-zMWOZcWFZdgfVjEg0NSg60MCydVpMw,2266
31
+ sql/tests/test_rollup.py,sha256=Ae7PVTnyELICcjC00dkcAiHzr61NG8cnIcAoHagHr98,339
32
+ sql/tests/test_select.py,sha256=czr2Ss6kSPZL5LLpCbVQguBJ_QsSl7PeWdyjTx7Yz_8,23346
33
+ sql/tests/test_table.py,sha256=ZF6VzLcx54ulj69Qnt0hdQ7_XamAqpofyTKs0WiGB4E,758
34
+ sql/tests/test_update.py,sha256=09r_9047luX4WN0paJJBjchZGhFileZcavq1R1TqGV8,3772
35
+ sql/tests/test_values.py,sha256=qKvaFSDEQmVWajWhMZgqD2-rQcWJeW_nsevVMsV3jD8,1053
36
+ sql/tests/test_window.py,sha256=EpqjZX2cyQ-m7UWMubrB5MOtL_5uF8jSnNhODfE74xc,3183
37
+ sql/tests/test_with.py,sha256=Ff9LYEnXsu6062bF6C86uZFwdYlpwswuvc1cdBZs8HY,2363
38
+ python_sql-1.8.1.dist-info/METADATA,sha256=ywZsOeYln70IyzXLIkiB2VdN-VUr37qY6kiHoaSZtxc,8146
39
+ python_sql-1.8.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
40
+ python_sql-1.8.1.dist-info/licenses/COPYRIGHT,sha256=qxgK78Jt_bk7V59177UafkZEm4lS6KVUuUp0DM24lIA,1627
41
+ python_sql-1.8.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2011-2026 Cédric Krier <cedric.krier@b2ck.com>
2
+ Copyright (c) 2013-2025 Nicolas Évrard <nicolas.evrard@b2ck.com>
3
+ Copyright (c) 2011-2026 B2CK SRL
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+ * Neither the name of the <organization> nor the
14
+ names of its contributors may be used to endorse or promote products
15
+ derived from this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
21
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
sql/__init__.py CHANGED
@@ -7,7 +7,7 @@ from collections import defaultdict
7
7
  from itertools import chain
8
8
  from threading import current_thread, local
9
9
 
10
- __version__ = '1.7.0'
10
+ __version__ = '1.8.1'
11
11
  __all__ = [
12
12
  'Flavor', 'Table', 'Values', 'Literal', 'Column', 'Grouping', 'Conflict',
13
13
  'Matched', 'MatchedUpdate', 'MatchedDelete',
@@ -629,6 +629,27 @@ class Select(FromItem, SelectQuery):
629
629
  and (self.limit is not None or self.offset is not None)):
630
630
  return self._rownum(str)
631
631
 
632
+ ordinals = {}
633
+ for expression in chain(
634
+ self.group_by or [],
635
+ self.order_by or []):
636
+ if not isinstance(expression, As):
637
+ continue
638
+ for i, column in enumerate(self.columns, start=1):
639
+ if not isinstance(column, As):
640
+ continue
641
+ if column.output_name != expression.output_name:
642
+ continue
643
+ if (str(column.expression) != str(expression.expression)
644
+ or column.params != expression.params):
645
+ raise ValueError("%r != %r" % (expression, column))
646
+ ordinals[column.output_name] = i
647
+
648
+ def str_or_ordinal(expression):
649
+ if isinstance(expression, As):
650
+ expression = ordinals.get(expression.output_name, expression)
651
+ return str(expression)
652
+
632
653
  with AliasManager():
633
654
  if self.from_ is not None:
634
655
  from_ = ' FROM %s' % self.from_
@@ -657,7 +678,8 @@ class Select(FromItem, SelectQuery):
657
678
  where = ' WHERE ' + str(self.where)
658
679
  group_by = ''
659
680
  if self.group_by:
660
- group_by = ' GROUP BY ' + ', '.join(map(str, self.group_by))
681
+ group_by = ' GROUP BY ' + ', '.join(
682
+ map(str_or_ordinal, self.group_by))
661
683
  having = ''
662
684
  if self.having:
663
685
  having = ' HAVING ' + str(self.having)
sql/functions.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # This file is part of python-sql. The COPYRIGHT file at the top level of
2
2
  # this repository contains the full copyright notices and license terms.
3
+
4
+ from enum import Enum, auto
3
5
  from itertools import chain
4
6
 
5
7
  from sql import CombiningQuery, Expression, Flavor, FromItem, Select, Window
@@ -85,7 +87,7 @@ class FunctionKeyword(Function):
85
87
  return (self._function + '('
86
88
  + ' '.join(chain(*zip(
87
89
  self._keywords,
88
- map(self._format, self.args))))[1:]
90
+ map(self._format, self.args)))).strip()
89
91
  + ')')
90
92
 
91
93
 
@@ -383,9 +385,67 @@ class DateTrunc(Function):
383
385
 
384
386
 
385
387
  class Extract(FunctionKeyword):
386
- __slots__ = ()
388
+ __slots__ = ('_field',)
387
389
  _function = 'EXTRACT'
388
- _keywords = ('', 'FROM')
390
+
391
+ class Fields(str, Enum):
392
+ def _generate_next_value_(name, start, count, last_values):
393
+ return name.upper()
394
+
395
+ CENTURY = auto()
396
+ DAY = auto()
397
+ DECADE = auto()
398
+ DOW = auto()
399
+ DOY = auto()
400
+ EPOCH = auto()
401
+ HOUR = auto()
402
+ ISODOW = auto()
403
+ ISOYEAR = auto()
404
+ JULIAN = auto()
405
+ MICROSECONDS = auto()
406
+ MILLENNIUM = auto()
407
+ MILLISECONDS = auto()
408
+ MINUTE = auto()
409
+ MONTH = auto()
410
+ QUARTER = auto()
411
+ SECOND = auto()
412
+ TIMEZONE = auto()
413
+ TIMEZONE_HOUR = auto()
414
+ TIMEZONE_MINUTE = auto()
415
+ WEEK = auto()
416
+ YEAR = auto()
417
+
418
+ def __init__(self, field, *args, **kwargs):
419
+ super().__init__(*args, **kwargs)
420
+ self.field = field
421
+
422
+ @property
423
+ def field(self):
424
+ return self._field
425
+
426
+ @field.setter
427
+ def field(self, value):
428
+ value = value.upper()
429
+ if not hasattr(self.Fields, value):
430
+ raise ValueError("invalid field: %r" % value)
431
+ self._field = value
432
+
433
+ @property
434
+ def _keywords(self):
435
+ return ('%s FROM' % self.field,)
436
+
437
+ def __str__(self):
438
+ Mapping = Flavor.get().function_mapping.get(self.__class__)
439
+ if Mapping:
440
+ return str(Mapping(self.field, *self.args))
441
+ return super().__str__()
442
+
443
+ @property
444
+ def params(self):
445
+ Mapping = Flavor.get().function_mapping.get(self.__class__)
446
+ if Mapping:
447
+ return Mapping(self.field, *self.args).params
448
+ return super().params
389
449
 
390
450
 
391
451
  class Isfinite(Function):
sql/operators.py CHANGED
@@ -48,9 +48,11 @@ class Operator(Expression):
48
48
  def _format(self, operand, param=None):
49
49
  if param is None:
50
50
  param = Flavor.get().param
51
- if isinstance(operand, Expression):
51
+ if (isinstance(operand, Expression)
52
+ and (not isinstance(operand, Operator)
53
+ or isinstance(operand, UnaryOperator))):
52
54
  return str(operand)
53
- elif isinstance(operand, (Select, CombiningQuery)):
55
+ elif isinstance(operand, (Expression, Select, CombiningQuery)):
54
56
  return '(%s)' % operand
55
57
  elif isinstance(operand, (list, tuple)):
56
58
  return '(' + ', '.join(self._format(o, param)
@@ -88,7 +90,7 @@ class UnaryOperator(Operator):
88
90
  return (self.operand,)
89
91
 
90
92
  def __str__(self):
91
- return '(%s %s)' % (self._operator, self._format(self.operand))
93
+ return '%s %s' % (self._operator, self._format(self.operand))
92
94
 
93
95
 
94
96
  class BinaryOperator(Operator):
@@ -105,7 +107,7 @@ class BinaryOperator(Operator):
105
107
 
106
108
  def __str__(self):
107
109
  left, right = self._operands
108
- return '(%s %s %s)' % (self._format(left), self._operator,
110
+ return '%s %s %s' % (self._format(left), self._operator,
109
111
  self._format(right))
110
112
 
111
113
  def __invert__(self):
@@ -121,8 +123,7 @@ class NaryOperator(list, Operator):
121
123
  return self
122
124
 
123
125
  def __str__(self):
124
- return '(' + (' %s ' % self._operator).join(
125
- map(self._format, self)) + ')'
126
+ return (' %s ' % self._operator).join(map(self._format, self))
126
127
 
127
128
 
128
129
  class And(NaryOperator):
@@ -184,9 +185,9 @@ class Equal(BinaryOperator):
184
185
 
185
186
  def __str__(self):
186
187
  if self.left is Null:
187
- return '(%s IS NULL)' % self.right
188
+ return '%s IS NULL' % self.right
188
189
  elif self.right is Null:
189
- return '(%s IS NULL)' % self.left
190
+ return '%s IS NULL' % self.left
190
191
  return super(Equal, self).__str__()
191
192
 
192
193
 
@@ -196,9 +197,9 @@ class NotEqual(Equal):
196
197
 
197
198
  def __str__(self):
198
199
  if self.left is Null:
199
- return '(%s IS NOT NULL)' % self.right
200
+ return '%s IS NOT NULL' % self.right
200
201
  elif self.right is Null:
201
- return '(%s IS NOT NULL)' % self.left
202
+ return '%s IS NOT NULL' % self.left
202
203
  return super(Equal, self).__str__()
203
204
 
204
205
 
@@ -220,7 +221,7 @@ class Between(Operator):
220
221
  operator = self._operator
221
222
  if self.symmetric:
222
223
  operator += ' SYMMETRIC'
223
- return '(%s %s %s AND %s)' % (
224
+ return '%s %s %s AND %s' % (
224
225
  self._format(self.operand), operator,
225
226
  self._format(self.left), self._format(self.right))
226
227
 
@@ -259,12 +260,12 @@ class Is(BinaryOperator):
259
260
 
260
261
  def __str__(self):
261
262
  if self.right is None:
262
- return '(%s %s UNKNOWN)' % (
263
+ return '%s %s UNKNOWN' % (
263
264
  self._format(self.left), self._operator)
264
265
  elif self.right is True:
265
- return '(%s %s TRUE)' % (self._format(self.left), self._operator)
266
+ return '%s %s TRUE' % (self._format(self.left), self._operator)
266
267
  elif self.right is False:
267
- return '(%s %s FALSE)' % (self._format(self.left), self._operator)
268
+ return '%s %s FALSE' % (self._format(self.left), self._operator)
268
269
 
269
270
 
270
271
  class IsNot(Is):
@@ -395,11 +396,11 @@ class Like(BinaryOperator):
395
396
  def __str__(self):
396
397
  left, right = self._operands
397
398
  if self.escape or Flavor().get().escape_empty:
398
- return '(%s %s %s ESCAPE %s)' % (
399
+ return '%s %s %s ESCAPE %s' % (
399
400
  self._format(left), self._operator, self._format(right),
400
401
  self._format(self.escape or ''))
401
402
  else:
402
- return '(%s %s %s)' % (
403
+ return '%s %s %s' % (
403
404
  self._format(left), self._operator, self._format(right))
404
405
 
405
406
  def __invert__(self):
@@ -458,7 +459,24 @@ class Exists(UnaryOperator):
458
459
  _operator = 'EXISTS'
459
460
 
460
461
 
461
- class Any(UnaryOperator):
462
+ class _ArrayOperator(UnaryOperator):
463
+ __slots__ = ()
464
+
465
+ @property
466
+ def params(self):
467
+ if isinstance(self.operand, (list, tuple, array)):
468
+ return (list(self.operand),)
469
+ return super().params
470
+
471
+ def _format(self, operand, param=None):
472
+ if param is None:
473
+ param = Flavor.get().param
474
+ if isinstance(operand, (list, tuple, array)):
475
+ return '(%s)' % param
476
+ return super()._format(operand, param=param)
477
+
478
+
479
+ class Any(_ArrayOperator):
462
480
  __slots__ = ()
463
481
  _operator = 'ANY'
464
482
 
@@ -466,7 +484,7 @@ class Any(UnaryOperator):
466
484
  Some = Any
467
485
 
468
486
 
469
- class All(UnaryOperator):
487
+ class All(_ArrayOperator):
470
488
  __slots__ = ()
471
489
  _operator = 'ALL'
472
490
 
@@ -38,7 +38,7 @@ class TestAggregate(unittest.TestCase):
38
38
  self.assertEqual(str(avg), 'AVG("c")')
39
39
 
40
40
  avg = Avg(self.table.a + self.table.b)
41
- self.assertEqual(str(avg), 'AVG(("a" + "b"))')
41
+ self.assertEqual(str(avg), 'AVG("a" + "b")')
42
42
 
43
43
  def test_count_without_expression(self):
44
44
  count = Count()
@@ -67,7 +67,7 @@ class TestAggregate(unittest.TestCase):
67
67
  try:
68
68
  avg = Avg(self.table.a + 1, filter_=self.table.a > 0)
69
69
  self.assertEqual(
70
- str(avg), 'AVG(("a" + %s)) FILTER (WHERE ("a" > %s))')
70
+ str(avg), 'AVG("a" + %s) FILTER (WHERE "a" > %s)')
71
71
  self.assertEqual(avg.params, (1, 0))
72
72
  finally:
73
73
  Flavor.set(Flavor())
@@ -75,13 +75,13 @@ class TestAggregate(unittest.TestCase):
75
75
  def test_filter_case(self):
76
76
  avg = Avg(self.table.a + 1, filter_=self.table.a > 0)
77
77
  self.assertEqual(
78
- str(avg), 'AVG(CASE WHEN ("a" > %s) THEN ("a" + %s) END)')
78
+ str(avg), 'AVG(CASE WHEN "a" > %s THEN "a" + %s END)')
79
79
  self.assertEqual(avg.params, (0, 1))
80
80
 
81
81
  def test_filter_case_count_star(self):
82
82
  count = Count(Literal('*'), filter_=self.table.a > 0)
83
83
  self.assertEqual(
84
- str(count), 'COUNT(CASE WHEN ("a" > %s) THEN %s END)')
84
+ str(count), 'COUNT(CASE WHEN "a" > %s THEN %s END)')
85
85
  self.assertEqual(count.params, (0, 1))
86
86
 
87
87
  def test_window(self):
@@ -33,7 +33,7 @@ class TestUnion(unittest.TestCase):
33
33
 
34
34
  self.assertEqual(str(query),
35
35
  'WITH "a" AS ('
36
- 'SELECT "b"."id" FROM "t" AS "b" WHERE ("b"."id" = %s)) '
36
+ 'SELECT "b"."id" FROM "t" AS "b" WHERE "b"."id" = %s) '
37
37
  'SELECT * FROM "t1" AS "c" UNION SELECT * FROM "t2" AS "d"')
38
38
  self.assertEqual(tuple(query.params), (1,))
39
39
 
@@ -36,9 +36,9 @@ class TestConditionals(unittest.TestCase):
36
36
  where=self.table.c2 == 'foo'))
37
37
  self.assertEqual(str(case),
38
38
  'CASE WHEN '
39
- '(SELECT "a"."bool" FROM "t" AS "a" WHERE ("a"."c2" = %s)) '
39
+ '(SELECT "a"."bool" FROM "t" AS "a" WHERE "a"."c2" = %s) '
40
40
  'THEN "c1" '
41
- 'ELSE (SELECT "a"."c1" FROM "t" AS "a" WHERE ("a"."c2" = %s)) END')
41
+ 'ELSE (SELECT "a"."c1" FROM "t" AS "a" WHERE "a"."c2" = %s) END')
42
42
  self.assertEqual(case.params, ('bar', 'foo'))
43
43
 
44
44
  def test_coalesce(self):
@@ -52,7 +52,7 @@ class TestConditionals(unittest.TestCase):
52
52
  self.table.c2)
53
53
  self.assertEqual(str(coalesce),
54
54
  'COALESCE('
55
- '(SELECT "a"."c1" FROM "t" AS "a" WHERE ("a"."c2" = %s)), "c2")')
55
+ '(SELECT "a"."c1" FROM "t" AS "a" WHERE "a"."c2" = %s), "c2")')
56
56
  self.assertEqual(coalesce.params, ('bar',))
57
57
 
58
58
  def test_nullif(self):
sql/tests/test_delete.py CHANGED
@@ -15,7 +15,7 @@ class TestDelete(unittest.TestCase):
15
15
 
16
16
  def test_delete2(self):
17
17
  query = self.table.delete(where=(self.table.c == 'foo'))
18
- self.assertEqual(str(query), 'DELETE FROM "t" WHERE ("c" = %s)')
18
+ self.assertEqual(str(query), 'DELETE FROM "t" WHERE "c" = %s')
19
19
  self.assertEqual(query.params, ('foo',))
20
20
 
21
21
  def test_delete3(self):
@@ -23,8 +23,8 @@ class TestDelete(unittest.TestCase):
23
23
  t2 = Table('t2')
24
24
  query = t1.delete(where=(t1.c.in_(t2.select(t2.c))))
25
25
  self.assertEqual(str(query),
26
- 'DELETE FROM "t1" WHERE ("c" IN ('
27
- 'SELECT "a"."c" FROM "t2" AS "a"))')
26
+ 'DELETE FROM "t1" WHERE "c" IN ('
27
+ 'SELECT "a"."c" FROM "t2" AS "a")')
28
28
  self.assertEqual(query.params, ())
29
29
 
30
30
  def test_delete_invalid_table(self):
@@ -61,5 +61,5 @@ class TestDelete(unittest.TestCase):
61
61
  self.assertEqual(str(query),
62
62
  'WITH "a" AS (SELECT "b"."c1" FROM "t1" AS "b") '
63
63
  'DELETE FROM "t" WHERE '
64
- '("c2" IN (SELECT "a"."c3" FROM "a" AS "a"))')
64
+ '"c2" IN (SELECT "a"."c3" FROM "a" AS "a")')
65
65
  self.assertEqual(query.params, ())