python-sql 1.6.0__tar.gz → 1.8.0__tar.gz

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 (56) hide show
  1. python_sql-1.8.0/.gitignore +1 -0
  2. python_sql-1.8.0/.hgignore +7 -0
  3. {python_sql-1.6.0 → python_sql-1.8.0}/COPYRIGHT +3 -3
  4. {python_sql-1.6.0 → python_sql-1.8.0}/PKG-INFO +28 -48
  5. {python_sql-1.6.0 → python_sql-1.8.0}/README.rst +14 -14
  6. python_sql-1.8.0/pyproject.toml +38 -0
  7. {python_sql-1.6.0 → python_sql-1.8.0}/sql/__init__.py +24 -2
  8. {python_sql-1.6.0 → python_sql-1.8.0}/sql/aggregate.py +16 -6
  9. {python_sql-1.6.0 → python_sql-1.8.0}/sql/functions.py +63 -3
  10. {python_sql-1.6.0 → python_sql-1.8.0}/sql/operators.py +36 -18
  11. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_aggregate.py +6 -6
  12. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_combining_query.py +1 -1
  13. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_conditionals.py +3 -3
  14. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_delete.py +4 -4
  15. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_functions.py +36 -4
  16. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_insert.py +4 -4
  17. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_join.py +2 -2
  18. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_lateral.py +1 -1
  19. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_merge.py +12 -12
  20. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_operators.py +84 -67
  21. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_order.py +3 -3
  22. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_select.py +62 -31
  23. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_update.py +6 -6
  24. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_with.py +3 -3
  25. python_sql-1.6.0/.flake8 +0 -2
  26. python_sql-1.6.0/.gitlab-ci.yml +0 -66
  27. python_sql-1.6.0/.hgtags +0 -23
  28. python_sql-1.6.0/.isort.cfg +0 -2
  29. python_sql-1.6.0/CHANGELOG +0 -125
  30. python_sql-1.6.0/MANIFEST.in +0 -3
  31. python_sql-1.6.0/python_sql.egg-info/PKG-INFO +0 -246
  32. python_sql-1.6.0/python_sql.egg-info/SOURCES.txt +0 -51
  33. python_sql-1.6.0/python_sql.egg-info/dependency_links.txt +0 -1
  34. python_sql-1.6.0/python_sql.egg-info/top_level.txt +0 -1
  35. python_sql-1.6.0/setup.cfg +0 -4
  36. python_sql-1.6.0/setup.py +0 -56
  37. python_sql-1.6.0/tox.ini +0 -19
  38. {python_sql-1.6.0 → python_sql-1.8.0}/sql/conditionals.py +0 -0
  39. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/__init__.py +0 -0
  40. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_alias.py +0 -0
  41. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_as.py +0 -0
  42. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_cast.py +0 -0
  43. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_collate.py +0 -0
  44. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_column.py +0 -0
  45. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_excluded.py +0 -0
  46. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_expression.py +0 -0
  47. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_flavor.py +0 -0
  48. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_for.py +0 -0
  49. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_from.py +0 -0
  50. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_from_item.py +0 -0
  51. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_grouping.py +0 -0
  52. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_literal.py +0 -0
  53. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_rollup.py +0 -0
  54. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_table.py +0 -0
  55. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_values.py +0 -0
  56. {python_sql-1.6.0 → python_sql-1.8.0}/sql/tests/test_window.py +0 -0
@@ -0,0 +1 @@
1
+ Sync/dotfiles/git/dot-gitignore
@@ -0,0 +1,7 @@
1
+ syntax: glob
2
+ *.py[cdo]
3
+ *.egg-info
4
+ dist/
5
+ build/
6
+ .tox/
7
+ .coverage
@@ -1,6 +1,6 @@
1
- Copyright (c) 2011-2025, Cédric Krier
2
- Copyright (c) 2013-2025, Nicolas Évrard
3
- Copyright (c) 2011-2025, B2CK
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
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
@@ -1,44 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sql
3
- Version: 1.6.0
3
+ Version: 1.8.0
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
18
  Classifier: Topic :: Database
29
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
- Requires-Python: >=3.5
31
- Dynamic: author
32
- Dynamic: author-email
33
- Dynamic: classifier
34
- Dynamic: description
35
- Dynamic: download-url
36
- Dynamic: home-page
37
- Dynamic: keywords
38
- Dynamic: license
39
- Dynamic: project-url
40
- Dynamic: requires-python
41
- Dynamic: summary
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/x-rst
42
22
 
43
23
  python-sql
44
24
  ==========
@@ -81,14 +61,14 @@ Select with where condition::
81
61
 
82
62
  >>> select.where = user.name == 'foo'
83
63
  >>> tuple(select)
84
- ('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',))
85
65
 
86
66
  >>> select.where = (user.name == 'foo') & (user.active == True)
87
67
  >>> tuple(select)
88
- ('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))
89
69
  >>> select.where = user.name == user.login
90
70
  >>> tuple(select)
91
- ('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"', ())
92
72
 
93
73
  Select with join::
94
74
 
@@ -96,7 +76,7 @@ Select with join::
96
76
  >>> join.condition = join.right.user == user.id
97
77
  >>> select = join.select(user.name, join.right.group)
98
78
  >>> tuple(select)
99
- ('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"', ())
100
80
 
101
81
  Select with multiple joins::
102
82
 
@@ -135,9 +115,9 @@ Select with sub-select::
135
115
  ... where=user_group.active == True)
136
116
  >>> user = Table('user')
137
117
  >>> tuple(user.select(user.id, where=user.id.in_(subselect)))
138
- ('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,))
139
119
  >>> tuple(subselect.select(subselect.user))
140
- ('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,))
141
121
 
142
122
  Select on other schema::
143
123
 
@@ -171,20 +151,20 @@ Update query with values::
171
151
  >>> tuple(user.update(columns=[user.active], values=[True]))
172
152
  ('UPDATE "user" AS "a" SET "active" = %s', (True,))
173
153
  >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
174
- ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ())
154
+ ('UPDATE "invoice" AS "a" SET "total" = "a"."amount" + "a"."tax"', ())
175
155
 
176
156
  Update query with where condition::
177
157
 
178
158
  >>> tuple(user.update(columns=[user.active], values=[True],
179
159
  ... where=user.active == False))
180
- ('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))
181
161
 
182
162
  Update query with from list::
183
163
 
184
164
  >>> group = Table('user_group')
185
165
  >>> tuple(user.update(columns=[user.active], values=[group.active],
186
166
  ... from_=[group], where=user.id == group.user))
187
- ('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"', ())
188
168
 
189
169
  Delete query::
190
170
 
@@ -194,13 +174,13 @@ Delete query::
194
174
  Delete query with where condition::
195
175
 
196
176
  >>> tuple(user.delete(where=user.name == 'foo'))
197
- ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',))
177
+ ('DELETE FROM "user" WHERE "name" = %s', ('foo',))
198
178
 
199
179
  Delete query with sub-query::
200
180
 
201
181
  >>> tuple(user.delete(
202
182
  ... where=user.id.in_(user_group.select(user_group.user))))
203
- ('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")', ())
204
184
 
205
185
  Flavors::
206
186
 
@@ -227,7 +207,7 @@ Limit style::
227
207
  ('SELECT * FROM "user" AS "a" OFFSET (%s) ROWS FETCH FIRST (%s) ROWS ONLY', (20, 10))
228
208
  >>> Flavor.set(Flavor(limitstyle='rownum'))
229
209
  >>> tuple(select)
230
- ('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))
231
211
 
232
212
  qmark style::
233
213
 
@@ -235,7 +215,7 @@ qmark style::
235
215
  >>> select = user.select()
236
216
  >>> select.where = user.name == 'foo'
237
217
  >>> tuple(select)
238
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
218
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = ?', ('foo',))
239
219
 
240
220
  numeric style::
241
221
 
@@ -243,4 +223,4 @@ numeric style::
243
223
  >>> select = user.select()
244
224
  >>> select.where = user.name == 'foo'
245
225
  >>> format2numeric(*select)
246
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',))
226
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = :0', ('foo',))
@@ -39,14 +39,14 @@ Select with where condition::
39
39
 
40
40
  >>> select.where = user.name == 'foo'
41
41
  >>> tuple(select)
42
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s)', ('foo',))
42
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE "a"."name" = %s', ('foo',))
43
43
 
44
44
  >>> select.where = (user.name == 'foo') & (user.active == True)
45
45
  >>> tuple(select)
46
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE (("a"."name" = %s) AND ("a"."active" = %s))', ('foo', True))
46
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s) AND ("a"."active" = %s)', ('foo', True))
47
47
  >>> select.where = user.name == user.login
48
48
  >>> tuple(select)
49
- ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = "a"."login")', ())
49
+ ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE "a"."name" = "a"."login"', ())
50
50
 
51
51
  Select with join::
52
52
 
@@ -54,7 +54,7 @@ Select with join::
54
54
  >>> join.condition = join.right.user == user.id
55
55
  >>> select = join.select(user.name, join.right.group)
56
56
  >>> tuple(select)
57
- ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON ("b"."user" = "a"."id")', ())
57
+ ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON "b"."user" = "a"."id"', ())
58
58
 
59
59
  Select with multiple joins::
60
60
 
@@ -93,9 +93,9 @@ Select with sub-select::
93
93
  ... where=user_group.active == True)
94
94
  >>> user = Table('user')
95
95
  >>> tuple(user.select(user.id, where=user.id.in_(subselect)))
96
- ('SELECT "a"."id" FROM "user" AS "a" WHERE ("a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)))', (True,))
96
+ ('SELECT "a"."id" FROM "user" AS "a" WHERE "a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE "b"."active" = %s)', (True,))
97
97
  >>> tuple(subselect.select(subselect.user))
98
- ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)) AS "a"', (True,))
98
+ ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE "b"."active" = %s) AS "a"', (True,))
99
99
 
100
100
  Select on other schema::
101
101
 
@@ -129,20 +129,20 @@ Update query with values::
129
129
  >>> tuple(user.update(columns=[user.active], values=[True]))
130
130
  ('UPDATE "user" AS "a" SET "active" = %s', (True,))
131
131
  >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
132
- ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ())
132
+ ('UPDATE "invoice" AS "a" SET "total" = "a"."amount" + "a"."tax"', ())
133
133
 
134
134
  Update query with where condition::
135
135
 
136
136
  >>> tuple(user.update(columns=[user.active], values=[True],
137
137
  ... where=user.active == False))
138
- ('UPDATE "user" AS "a" SET "active" = %s WHERE ("a"."active" = %s)', (True, False))
138
+ ('UPDATE "user" AS "a" SET "active" = %s WHERE "a"."active" = %s', (True, False))
139
139
 
140
140
  Update query with from list::
141
141
 
142
142
  >>> group = Table('user_group')
143
143
  >>> tuple(user.update(columns=[user.active], values=[group.active],
144
144
  ... from_=[group], where=user.id == group.user))
145
- ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE ("b"."id" = "a"."user")', ())
145
+ ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE "b"."id" = "a"."user"', ())
146
146
 
147
147
  Delete query::
148
148
 
@@ -152,13 +152,13 @@ Delete query::
152
152
  Delete query with where condition::
153
153
 
154
154
  >>> tuple(user.delete(where=user.name == 'foo'))
155
- ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',))
155
+ ('DELETE FROM "user" WHERE "name" = %s', ('foo',))
156
156
 
157
157
  Delete query with sub-query::
158
158
 
159
159
  >>> tuple(user.delete(
160
160
  ... where=user.id.in_(user_group.select(user_group.user))))
161
- ('DELETE FROM "user" WHERE ("id" IN (SELECT "a"."user" FROM "user_group" AS "a"))', ())
161
+ ('DELETE FROM "user" WHERE "id" IN (SELECT "a"."user" FROM "user_group" AS "a")', ())
162
162
 
163
163
  Flavors::
164
164
 
@@ -185,7 +185,7 @@ Limit style::
185
185
  ('SELECT * FROM "user" AS "a" OFFSET (%s) ROWS FETCH FIRST (%s) ROWS ONLY', (20, 10))
186
186
  >>> Flavor.set(Flavor(limitstyle='rownum'))
187
187
  >>> tuple(select)
188
- ('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))
188
+ ('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))
189
189
 
190
190
  qmark style::
191
191
 
@@ -193,7 +193,7 @@ qmark style::
193
193
  >>> select = user.select()
194
194
  >>> select.where = user.name == 'foo'
195
195
  >>> tuple(select)
196
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
196
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = ?', ('foo',))
197
197
 
198
198
  numeric style::
199
199
 
@@ -201,4 +201,4 @@ numeric style::
201
201
  >>> select = user.select()
202
202
  >>> select.where = user.name == 'foo'
203
203
  >>> format2numeric(*select)
204
- ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',))
204
+ ('SELECT * FROM "user" AS "a" WHERE "a"."name" = :0', ('foo',))
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ['hatchling >= 1', 'hatch-tryton']
3
+ build-backend = 'hatchling.build'
4
+
5
+ [project]
6
+ name = 'python-sql'
7
+ dynamic = ['version', 'authors']
8
+ requires-python = '>=3.9'
9
+ maintainers = [
10
+ {name = "Tryton", email = "foundation@tryton.org"},
11
+ ]
12
+ description = "Library to write SQL queries"
13
+ readme = 'README.rst'
14
+ license = 'BSD-3-Clause'
15
+ license-files = ['COPYRIGHT']
16
+ keywords = ["SQL", "database", "query"]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: Developers",
20
+ "Topic :: Database",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [project.urls]
25
+ homepage = "https://www.tryton.org/"
26
+ changelog = "https://code.tryton.org/python-sql/-/blob/branch/default/CHANGELOG"
27
+ forum = "https://discuss.tryton.org/tags/python-sql"
28
+ issues = "https://bugs.tryton.org/python-sql"
29
+ repository = "https://code.tryton.org/python-sql"
30
+
31
+ [tool.hatch.version]
32
+ path = 'sql/__init__.py'
33
+
34
+ [tool.hatch.build]
35
+ packages = ['sql']
36
+
37
+ [tool.hatch.metadata.hooks.tryton]
38
+ copyright = 'COPYRIGHT'
@@ -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.6.0'
10
+ __version__ = '1.8.0'
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)
@@ -4,7 +4,6 @@ from sql import Expression, Flavor, Literal, Window
4
4
 
5
5
  __all__ = ['Avg', 'BitAnd', 'BitOr', 'BoolAnd', 'BoolOr', 'Count', 'Every',
6
6
  'Max', 'Min', 'Stddev', 'Sum', 'Variance']
7
- _sentinel = object()
8
7
 
9
8
 
10
9
  class Aggregate(Expression):
@@ -169,20 +168,31 @@ class BoolOr(Aggregate):
169
168
  _sql = 'BOOL_OR'
170
169
 
171
170
 
171
+ class _Star(Expression):
172
+ __slots__ = ()
173
+
174
+ def __str__(self):
175
+ return '*'
176
+
177
+ @property
178
+ def params(self):
179
+ return ()
180
+
181
+
172
182
  class Count(Aggregate):
173
183
  __slots__ = ()
174
184
  _sql = 'COUNT'
175
185
 
176
- def __init__(self, expression=_sentinel, **kwargs):
177
- if expression is _sentinel:
178
- expression = Literal('*')
186
+ def __init__(self, expression=_Star(), **kwargs):
179
187
  super().__init__(expression, **kwargs)
180
188
 
181
189
  @property
182
190
  def _case_expression(self):
183
191
  expression = super(Count, self)._case_expression
184
- if (isinstance(self.expression, Literal)
185
- and expression.value == '*'):
192
+ if (isinstance(self.expression, _Star)
193
+ # Keep testing Literal('*') for backward compatibility
194
+ or (isinstance(self.expression, Literal)
195
+ and expression.value == '*')):
186
196
  expression = Literal(1)
187
197
  return expression
188
198
 
@@ -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):
@@ -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