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.
- python_sql-1.8.0/.gitignore +1 -0
- python_sql-1.8.0/.hgignore +7 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/COPYRIGHT +3 -3
- {python_sql-1.7.0 → python_sql-1.8.0}/PKG-INFO +28 -49
- {python_sql-1.7.0 → python_sql-1.8.0}/README.rst +14 -14
- python_sql-1.8.0/pyproject.toml +38 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/__init__.py +24 -2
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/functions.py +63 -3
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/operators.py +36 -18
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_aggregate.py +4 -4
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_combining_query.py +1 -1
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_conditionals.py +3 -3
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_delete.py +4 -4
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_functions.py +36 -4
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_insert.py +4 -4
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_join.py +2 -2
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_lateral.py +1 -1
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_merge.py +12 -12
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_operators.py +84 -67
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_order.py +3 -3
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_select.py +62 -31
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_update.py +6 -6
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_with.py +3 -3
- python_sql-1.7.0/.flake8 +0 -2
- python_sql-1.7.0/.gitlab-ci.yml +0 -66
- python_sql-1.7.0/.hgtags +0 -24
- python_sql-1.7.0/.isort.cfg +0 -2
- python_sql-1.7.0/CHANGELOG +0 -129
- python_sql-1.7.0/MANIFEST.in +0 -3
- python_sql-1.7.0/python_sql.egg-info/PKG-INFO +0 -247
- python_sql-1.7.0/python_sql.egg-info/SOURCES.txt +0 -51
- python_sql-1.7.0/python_sql.egg-info/dependency_links.txt +0 -1
- python_sql-1.7.0/python_sql.egg-info/top_level.txt +0 -1
- python_sql-1.7.0/setup.cfg +0 -4
- python_sql-1.7.0/setup.py +0 -57
- python_sql-1.7.0/tox.ini +0 -19
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/aggregate.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/conditionals.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/__init__.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_alias.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_as.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_cast.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_collate.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_column.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_excluded.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_expression.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_flavor.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_for.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_from.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_from_item.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_grouping.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_literal.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_rollup.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_table.py +0 -0
- {python_sql-1.7.0 → python_sql-1.8.0}/sql/tests/test_values.py +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Copyright (c) 2011-
|
|
2
|
-
Copyright (c) 2013-2025
|
|
3
|
-
Copyright (c) 2011-
|
|
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.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Library to write SQL queries
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
32
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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))))
|
|
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
|
-
|
|
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 '
|
|
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 '
|
|
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
|
|
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 '
|
|
188
|
+
return '%s IS NULL' % self.right
|
|
188
189
|
elif self.right is Null:
|
|
189
|
-
return '
|
|
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 '
|
|
200
|
+
return '%s IS NOT NULL' % self.right
|
|
200
201
|
elif self.right is Null:
|
|
201
|
-
return '
|
|
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 '
|
|
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 '
|
|
263
|
+
return '%s %s UNKNOWN' % (
|
|
263
264
|
self._format(self.left), self._operator)
|
|
264
265
|
elif self.right is True:
|
|
265
|
-
return '
|
|
266
|
+
return '%s %s TRUE' % (self._format(self.left), self._operator)
|
|
266
267
|
elif self.right is False:
|
|
267
|
-
return '
|
|
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 '
|
|
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 '
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|