python-sql 1.7.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.7.0 → python_sql-1.8.0}/COPYRIGHT +3 -3
  4. {python_sql-1.7.0 → python_sql-1.8.0}/PKG-INFO +28 -49
  5. {python_sql-1.7.0 → python_sql-1.8.0}/README.rst +14 -14
  6. python_sql-1.8.0/pyproject.toml +38 -0
  7. {python_sql-1.7.0 → python_sql-1.8.0}/sql/__init__.py +24 -2
  8. {python_sql-1.7.0 → python_sql-1.8.0}/sql/functions.py +63 -3
  9. {python_sql-1.7.0 → python_sql-1.8.0}/sql/operators.py +36 -18
  10. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_aggregate.py +4 -4
  11. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_combining_query.py +1 -1
  12. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_conditionals.py +3 -3
  13. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_delete.py +4 -4
  14. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_functions.py +36 -4
  15. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_insert.py +4 -4
  16. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_join.py +2 -2
  17. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_lateral.py +1 -1
  18. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_merge.py +12 -12
  19. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_operators.py +84 -67
  20. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_order.py +3 -3
  21. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_select.py +62 -31
  22. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_update.py +6 -6
  23. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_with.py +3 -3
  24. python_sql-1.7.0/.flake8 +0 -2
  25. python_sql-1.7.0/.gitlab-ci.yml +0 -66
  26. python_sql-1.7.0/.hgtags +0 -24
  27. python_sql-1.7.0/.isort.cfg +0 -2
  28. python_sql-1.7.0/CHANGELOG +0 -129
  29. python_sql-1.7.0/MANIFEST.in +0 -3
  30. python_sql-1.7.0/python_sql.egg-info/PKG-INFO +0 -247
  31. python_sql-1.7.0/python_sql.egg-info/SOURCES.txt +0 -51
  32. python_sql-1.7.0/python_sql.egg-info/dependency_links.txt +0 -1
  33. python_sql-1.7.0/python_sql.egg-info/top_level.txt +0 -1
  34. python_sql-1.7.0/setup.cfg +0 -4
  35. python_sql-1.7.0/setup.py +0 -57
  36. python_sql-1.7.0/tox.ini +0 -19
  37. {python_sql-1.7.0 → python_sql-1.8.0}/sql/aggregate.py +0 -0
  38. {python_sql-1.7.0 → python_sql-1.8.0}/sql/conditionals.py +0 -0
  39. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/__init__.py +0 -0
  40. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_alias.py +0 -0
  41. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_as.py +0 -0
  42. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_cast.py +0 -0
  43. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_collate.py +0 -0
  44. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_column.py +0 -0
  45. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_excluded.py +0 -0
  46. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_expression.py +0 -0
  47. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_flavor.py +0 -0
  48. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_for.py +0 -0
  49. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_from.py +0 -0
  50. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_from_item.py +0 -0
  51. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_grouping.py +0 -0
  52. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_literal.py +0 -0
  53. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_rollup.py +0 -0
  54. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_table.py +0 -0
  55. {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_values.py +0 -0
  56. {python_sql-1.7.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,45 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sql
3
- Version: 1.7.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
- 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',))
@@ -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.7.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)
@@ -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
 
@@ -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