Merge lp:~cjwatson/storm/restore-mysql into lp:storm
- restore-mysql
- Merge into trunk
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 568 |
Proposed branch: | lp:~cjwatson/storm/restore-mysql |
Merge into: | lp:storm |
Prerequisite: | lp:~cjwatson/storm/py27-py35-workarounds |
Diff against target: |
995 lines (+708/-46) 13 files modified
.bzrignore (+1/-0) MANIFEST.in (+1/-1) NEWS (+1/-0) README (+22/-41) dev/test (+123/-2) dev/ubuntu-deps (+3/-0) setup.py (+2/-0) storm/databases/mysql.py (+246/-0) storm/docs/tutorial.rst (+2/-2) storm/tests/databases/base.py (+2/-0) storm/tests/databases/mysql.py (+167/-0) storm/tests/store/base.py (+30/-0) storm/tests/store/mysql.py (+108/-0) |
To merge this branch: | bzr merge lp:~cjwatson/storm/restore-mysql |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Storm Developers | Pending | ||
Review via email: mp+400633@code.launchpad.net |
Commit message
Restore MySQL support.
Description of the change
We've had some requests to put this back, and it's reasonably straightforward to support. The only tricky bit was arranging for automatic test database provisioning; the only near-equivalent of postgresfixture that I could find is pytest-specific, but it's not too hard to inline the necessary setup and teardown code.
MySQL support was dropped in https:/
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' | |||
2 | --- .bzrignore 2020-05-26 10:28:24 +0000 | |||
3 | +++ .bzrignore 2021-04-06 09:31:39 +0000 | |||
4 | @@ -3,6 +3,7 @@ | |||
5 | 3 | storm.egg-info | 3 | storm.egg-info |
6 | 4 | build-stamp | 4 | build-stamp |
7 | 5 | db | 5 | db |
8 | 6 | db-mysql | ||
9 | 6 | .db.lock | 7 | .db.lock |
10 | 7 | dist | 8 | dist |
11 | 8 | debian/files | 9 | debian/files |
12 | 9 | 10 | ||
13 | === modified file 'MANIFEST.in' | |||
14 | --- MANIFEST.in 2020-05-26 10:28:24 +0000 | |||
15 | +++ MANIFEST.in 2021-04-06 09:31:39 +0000 | |||
16 | @@ -5,4 +5,4 @@ | |||
17 | 5 | include storm/docs/Makefile | 5 | include storm/docs/Makefile |
18 | 6 | prune storm/docs/_build | 6 | prune storm/docs/_build |
19 | 7 | 7 | ||
21 | 8 | prune db | 8 | prune db db-mysql |
22 | 9 | 9 | ||
23 | === modified file 'NEWS' | |||
24 | --- NEWS 2020-12-07 22:38:36 +0000 | |||
25 | +++ NEWS 2021-04-06 09:31:39 +0000 | |||
26 | @@ -7,6 +7,7 @@ | |||
27 | 7 | - Add optional case_sensitive argument to Comparable.startswith, | 7 | - Add optional case_sensitive argument to Comparable.startswith, |
28 | 8 | Comparable.endswith, and Comparable.contains_string. This is only | 8 | Comparable.endswith, and Comparable.contains_string. This is only |
29 | 9 | supported in the PostgreSQL backend. | 9 | supported in the PostgreSQL backend. |
30 | 10 | - Restore MySQL support. | ||
31 | 10 | 11 | ||
32 | 11 | 0.24 (2020-06-12) | 12 | 0.24 (2020-06-12) |
33 | 12 | ================= | 13 | ================= |
34 | 13 | 14 | ||
35 | === modified file 'README' | |||
36 | --- README 2020-05-26 10:28:24 +0000 | |||
37 | +++ README 2021-04-06 09:31:39 +0000 | |||
38 | @@ -22,7 +22,8 @@ | |||
39 | 22 | * Storm is well designed (different classes have very clear | 22 | * Storm is well designed (different classes have very clear |
40 | 23 | boundaries, with small and clean public APIs). | 23 | boundaries, with small and clean public APIs). |
41 | 24 | * Designed from day one to work both with thin relational | 24 | * Designed from day one to work both with thin relational |
43 | 25 | databases, such as SQLite, and big iron systems like PostgreSQL. | 25 | databases, such as SQLite, and big iron systems like PostgreSQL |
44 | 26 | and MySQL. | ||
45 | 26 | * Storm is easy to debug, since its code is written with a KISS | 27 | * Storm is easy to debug, since its code is written with a KISS |
46 | 27 | principle, and thus is easy to understand. | 28 | principle, and thus is easy to understand. |
47 | 28 | * Designed from day one to work both at the low end, with trivial | 29 | * Designed from day one to work both at the low end, with trivial |
48 | @@ -89,6 +90,8 @@ | |||
49 | 89 | is below. | 90 | is below. |
50 | 90 | 91 | ||
51 | 91 | $ dev/ubuntu-deps | 92 | $ dev/ubuntu-deps |
52 | 93 | $ echo "$PWD/** rwk," | sudo tee /etc/apparmor.d/local/usr.sbin.mysqld >/dev/null | ||
53 | 94 | $ sudo aa-enforce /usr/sbin/mysqld | ||
54 | 92 | $ make develop | 95 | $ make develop |
55 | 93 | $ make check | 96 | $ make check |
56 | 94 | 97 | ||
57 | @@ -103,15 +106,15 @@ | |||
58 | 103 | The following instructions assume that you're using Ubuntu. The same procedure | 106 | The following instructions assume that you're using Ubuntu. The same procedure |
59 | 104 | will probably work without changes on a Debian system and with minimal changes | 107 | will probably work without changes on a Debian system and with minimal changes |
60 | 105 | on a non-Debian-based linux distribution. In order to run the test suite, and | 108 | on a non-Debian-based linux distribution. In order to run the test suite, and |
63 | 106 | exercise all supported backends, you will need to install PostgreSQL, along | 109 | exercise all supported backends, you will need to install MySQL and |
64 | 107 | with the related Python database drivers: | 110 | PostgreSQL, along with the related Python database drivers: |
65 | 108 | 111 | ||
66 | 109 | $ sudo apt-get install \ | 112 | $ sudo apt-get install \ |
67 | 113 | mysql-server \ | ||
68 | 110 | postgresql pgbouncer \ | 114 | postgresql pgbouncer \ |
69 | 111 | build-essential | 115 | build-essential |
70 | 112 | 116 | ||
73 | 113 | These will take a few minutes to download (its a bit under 200MB all | 117 | These will take a few minutes to download. |
72 | 114 | together). | ||
74 | 115 | 118 | ||
75 | 116 | The Python dependencies for running tests can mostly be installed with | 119 | The Python dependencies for running tests can mostly be installed with |
76 | 117 | apt-get: | 120 | apt-get: |
77 | @@ -135,42 +138,20 @@ | |||
78 | 135 | This ensures that all dependencies are available, downloading from | 138 | This ensures that all dependencies are available, downloading from |
79 | 136 | PyPI as appropriate. | 139 | PyPI as appropriate. |
80 | 137 | 140 | ||
117 | 138 | Setting up database users and access security | 141 | Database setup |
118 | 139 | --------------------------------------------- | 142 | -------------- |
119 | 140 | 143 | ||
120 | 141 | PostgreSQL needs to be setup to allow TCP/IP connections from | 144 | Most database setup is done automatically by the test suite. However, |
121 | 142 | localhost. Edit /etc/postgresql/8.3/main/pg_hba.conf and make sure | 145 | Ubuntu's default MySQL packaging ships an AppArmor profile that prevents it |
122 | 143 | the following line is present: | 146 | from writing to a local data directory. To allow the test suite to do this, |
123 | 144 | 147 | you will need to grant it access, which is most easily done by adding a line | |
124 | 145 | host all all 127.0.0.1/32 trust | 148 | such as this to /etc/apparmor.d/local/usr.sbin.mysqld: |
125 | 146 | 149 | ||
126 | 147 | This will probably (with PostgresSQL 8.4) entail changing 'md5' to | 150 | /path/to/storm/** rwk, |
127 | 148 | 'trust'. | 151 | |
128 | 149 | 152 | Then reload the profile: | |
129 | 150 | In order to run the two-phase commit tests, you will also need to | 153 | |
130 | 151 | change the max_prepared_transactions value in postgres.conf to | 154 | $ sudo aa-enforce /usr/sbin/mysqld |
95 | 152 | something like | ||
96 | 153 | |||
97 | 154 | max_prepared_transactions = 200 | ||
98 | 155 | |||
99 | 156 | Now save and close, then restart the server: | ||
100 | 157 | |||
101 | 158 | $ sudo /etc/init.d/postgresql-8.4 restart | ||
102 | 159 | |||
103 | 160 | Lets create our PostgreSQL user now. As noted in the Ubuntu PostgreSQL | ||
104 | 161 | documentation, the easiest thing is to create a user with the same name as your | ||
105 | 162 | username. Run the following command to create a user for yourself (if prompted | ||
106 | 163 | for a password, leave it blank): | ||
107 | 164 | |||
108 | 165 | $ sudo -u postgres createuser --superuser $USER | ||
109 | 166 | |||
110 | 167 | Creating test databases | ||
111 | 168 | ----------------------- | ||
112 | 169 | |||
113 | 170 | The test suite needs some local databases in place to exercise PostgreSQL | ||
114 | 171 | functionality. Run: | ||
115 | 172 | |||
116 | 173 | $ createdb storm_test | ||
131 | 174 | 155 | ||
132 | 175 | Running the tests | 156 | Running the tests |
133 | 176 | ----------------- | 157 | ----------------- |
134 | 177 | 158 | ||
135 | === modified file 'dev/test' | |||
136 | --- dev/test 2019-11-21 01:57:01 +0000 | |||
137 | +++ dev/test 2021-04-06 09:31:39 +0000 | |||
138 | @@ -21,10 +21,16 @@ | |||
139 | 21 | # | 21 | # |
140 | 22 | import glob | 22 | import glob |
141 | 23 | import optparse | 23 | import optparse |
142 | 24 | import os | ||
143 | 24 | import re | 25 | import re |
144 | 26 | import shutil | ||
145 | 27 | import socket | ||
146 | 28 | import subprocess | ||
147 | 29 | import sys | ||
148 | 30 | import time | ||
149 | 25 | import unittest | 31 | import unittest |
152 | 26 | import sys | 32 | |
153 | 27 | import os | 33 | from pkg_resources import parse_version |
154 | 28 | 34 | ||
155 | 29 | 35 | ||
156 | 30 | def add_eggs_to_path(): | 36 | def add_eggs_to_path(): |
157 | @@ -111,6 +117,120 @@ | |||
158 | 111 | return wrapper | 117 | return wrapper |
159 | 112 | 118 | ||
160 | 113 | 119 | ||
161 | 120 | def with_mysql(runner_func): | ||
162 | 121 | """If possible, wrap a test runner with code to set up MySQL. | ||
163 | 122 | |||
164 | 123 | Loosely based on the approach taken by pytest-mysql, although | ||
165 | 124 | implemented separately. | ||
166 | 125 | """ | ||
167 | 126 | try: | ||
168 | 127 | import MySQLdb | ||
169 | 128 | except ImportError: | ||
170 | 129 | return runner_func | ||
171 | 130 | |||
172 | 131 | from six.moves.urllib.parse import ( | ||
173 | 132 | urlencode, | ||
174 | 133 | urlunsplit, | ||
175 | 134 | ) | ||
176 | 135 | |||
177 | 136 | def wrapper(): | ||
178 | 137 | basedir = os.path.abspath("db-mysql") | ||
179 | 138 | datadir = os.path.join(basedir, "data") | ||
180 | 139 | unix_socket = os.path.join(basedir, "mysql.sock") | ||
181 | 140 | logfile = os.path.join(basedir, "mysql.log") | ||
182 | 141 | if os.path.exists(basedir): | ||
183 | 142 | shutil.rmtree(basedir) | ||
184 | 143 | os.makedirs(basedir) | ||
185 | 144 | |||
186 | 145 | mysqld_version_output = subprocess.check_output( | ||
187 | 146 | ["mysqld", "--version"], universal_newlines=True).rstrip("\n") | ||
188 | 147 | version = re.search( | ||
189 | 148 | r"Ver ([\d.]+)", mysqld_version_output, flags=re.I).group(1) | ||
190 | 149 | if ("MariaDB" not in mysqld_version_output and | ||
191 | 150 | parse_version(version) >= parse_version("5.7.6")): | ||
192 | 151 | subprocess.check_call([ | ||
193 | 152 | "mysqld", | ||
194 | 153 | "--initialize-insecure", | ||
195 | 154 | "--datadir=%s" % datadir, | ||
196 | 155 | "--tmpdir=%s" % basedir, | ||
197 | 156 | "--log-error=%s" % logfile, | ||
198 | 157 | ]) | ||
199 | 158 | else: | ||
200 | 159 | subprocess.check_call([ | ||
201 | 160 | "mysql_install_db", | ||
202 | 161 | "--datadir=%s" % datadir, | ||
203 | 162 | "--tmpdir=%s" % basedir, | ||
204 | 163 | ]) | ||
205 | 164 | with open("/dev/null", "w") as devnull: | ||
206 | 165 | server_proc = subprocess.Popen([ | ||
207 | 166 | "mysqld_safe", | ||
208 | 167 | "--datadir=%s" % datadir, | ||
209 | 168 | "--pid-file=%s" % os.path.join(basedir, "mysql.pid"), | ||
210 | 169 | "--socket=%s" % unix_socket, | ||
211 | 170 | "--skip-networking", | ||
212 | 171 | "--log-error=%s" % logfile, | ||
213 | 172 | "--tmpdir=%s" % basedir, | ||
214 | 173 | "--skip-syslog", | ||
215 | 174 | # We don't care about durability of test data. Try to | ||
216 | 175 | # persuade MySQL to agree. | ||
217 | 176 | "--innodb-doublewrite=0", | ||
218 | 177 | "--innodb-flush-log-at-trx-commit=0", | ||
219 | 178 | "--innodb-flush-method=O_DIRECT_NO_FSYNC", | ||
220 | 179 | "--skip-innodb-file-per-table", | ||
221 | 180 | "--sync-binlog=0", | ||
222 | 181 | ], stdout=devnull) | ||
223 | 182 | |||
224 | 183 | try: | ||
225 | 184 | start_time = time.time() | ||
226 | 185 | while time.time() < start_time + 60: | ||
227 | 186 | code = server_proc.poll() | ||
228 | 187 | if code is not None and code != 0: | ||
229 | 188 | raise Exception("mysqld_base exited %d" % code) | ||
230 | 189 | |||
231 | 190 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
232 | 191 | try: | ||
233 | 192 | sock.connect(unix_socket) | ||
234 | 193 | break | ||
235 | 194 | except socket.error: | ||
236 | 195 | pass # try again | ||
237 | 196 | finally: | ||
238 | 197 | sock.close() | ||
239 | 198 | |||
240 | 199 | time.sleep(0.1) | ||
241 | 200 | |||
242 | 201 | try: | ||
243 | 202 | connection = MySQLdb.connect( | ||
244 | 203 | user="root", unix_socket=unix_socket) | ||
245 | 204 | try: | ||
246 | 205 | cursor = connection.cursor() | ||
247 | 206 | try: | ||
248 | 207 | cursor.execute( | ||
249 | 208 | "CREATE DATABASE storm_test CHARACTER SET utf8;") | ||
250 | 209 | finally: | ||
251 | 210 | cursor.close() | ||
252 | 211 | finally: | ||
253 | 212 | connection.close() | ||
254 | 213 | uri = urlunsplit( | ||
255 | 214 | ("mysql", "root@localhost", "/storm_test", | ||
256 | 215 | urlencode({"unix_socket": unix_socket}), "")) | ||
257 | 216 | os.environ["STORM_MYSQL_URI"] = uri | ||
258 | 217 | os.environ["STORM_MYSQL_HOST_URI"] = uri | ||
259 | 218 | return runner_func() | ||
260 | 219 | finally: | ||
261 | 220 | subprocess.check_call([ | ||
262 | 221 | "mysqladmin", | ||
263 | 222 | "--socket=%s" % unix_socket, | ||
264 | 223 | "--user=root", | ||
265 | 224 | "shutdown", | ||
266 | 225 | ]) | ||
267 | 226 | finally: | ||
268 | 227 | if server_proc.poll() is None: | ||
269 | 228 | server_proc.kill() | ||
270 | 229 | server_proc.wait() | ||
271 | 230 | |||
272 | 231 | return wrapper | ||
273 | 232 | |||
274 | 233 | |||
275 | 114 | if __name__ == "__main__": | 234 | if __name__ == "__main__": |
276 | 115 | runner = os.environ.get("STORM_TEST_RUNNER") | 235 | runner = os.environ.get("STORM_TEST_RUNNER") |
277 | 116 | if not runner: | 236 | if not runner: |
278 | @@ -119,6 +239,7 @@ | |||
279 | 119 | if not runner_func: | 239 | if not runner_func: |
280 | 120 | sys.exit("Test runner not found: %s" % runner) | 240 | sys.exit("Test runner not found: %s" % runner) |
281 | 121 | runner_func = with_postgresfixture(runner_func) | 241 | runner_func = with_postgresfixture(runner_func) |
282 | 242 | runner_func = with_mysql(runner_func) | ||
283 | 122 | sys.exit(runner_func()) | 243 | sys.exit(runner_func()) |
284 | 123 | 244 | ||
285 | 124 | # vim:ts=4:sw=4:et | 245 | # vim:ts=4:sw=4:et |
286 | 125 | 246 | ||
287 | === modified file 'dev/ubuntu-deps' | |||
288 | --- dev/ubuntu-deps 2019-06-05 10:53:34 +0000 | |||
289 | +++ dev/ubuntu-deps 2021-04-06 09:31:39 +0000 | |||
290 | @@ -7,11 +7,14 @@ | |||
291 | 7 | 7 | ||
292 | 8 | apt_get install --no-install-recommends \ | 8 | apt_get install --no-install-recommends \ |
293 | 9 | build-essential \ | 9 | build-essential \ |
294 | 10 | libmysqlclient-dev \ | ||
295 | 10 | libpq-dev \ | 11 | libpq-dev \ |
296 | 12 | mysql-server \ | ||
297 | 11 | pgbouncer \ | 13 | pgbouncer \ |
298 | 12 | postgresql \ | 14 | postgresql \ |
299 | 13 | python-dev \ | 15 | python-dev \ |
300 | 14 | python-fixtures \ | 16 | python-fixtures \ |
301 | 17 | python-mysqldb \ | ||
302 | 15 | python-psycopg2 \ | 18 | python-psycopg2 \ |
303 | 16 | python-setuptools \ | 19 | python-setuptools \ |
304 | 17 | python-six \ | 20 | python-six \ |
305 | 18 | 21 | ||
306 | === modified file 'setup.py' | |||
307 | --- setup.py 2021-04-06 09:31:39 +0000 | |||
308 | +++ setup.py 2021-04-06 09:31:39 +0000 | |||
309 | @@ -25,6 +25,8 @@ | |||
310 | 25 | 25 | ||
311 | 26 | tests_require = [ | 26 | tests_require = [ |
312 | 27 | "fixtures >= 1.3.0", | 27 | "fixtures >= 1.3.0", |
313 | 28 | "mysqlclient", | ||
314 | 29 | "mysqlclient < 2.0.0; python_version < '3'", | ||
315 | 28 | "pgbouncer >= 0.0.7", | 30 | "pgbouncer >= 0.0.7", |
316 | 29 | "postgresfixture", | 31 | "postgresfixture", |
317 | 30 | "psycopg2 >= 2.3.0", | 32 | "psycopg2 >= 2.3.0", |
318 | 31 | 33 | ||
319 | === added file 'storm/databases/mysql.py' | |||
320 | --- storm/databases/mysql.py 1970-01-01 00:00:00 +0000 | |||
321 | +++ storm/databases/mysql.py 2021-04-06 09:31:39 +0000 | |||
322 | @@ -0,0 +1,246 @@ | |||
323 | 1 | # | ||
324 | 2 | # Copyright (c) 2006, 2007 Canonical | ||
325 | 3 | # | ||
326 | 4 | # Written by Gustavo Niemeyer <gustavo@niemeyer.net> | ||
327 | 5 | # | ||
328 | 6 | # This file is part of Storm Object Relational Mapper. | ||
329 | 7 | # | ||
330 | 8 | # Storm is free software; you can redistribute it and/or modify | ||
331 | 9 | # it under the terms of the GNU Lesser General Public License as | ||
332 | 10 | # published by the Free Software Foundation; either version 2.1 of | ||
333 | 11 | # the License, or (at your option) any later version. | ||
334 | 12 | # | ||
335 | 13 | # Storm is distributed in the hope that it will be useful, | ||
336 | 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
337 | 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
338 | 16 | # GNU Lesser General Public License for more details. | ||
339 | 17 | # | ||
340 | 18 | # You should have received a copy of the GNU Lesser General Public License | ||
341 | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
342 | 20 | # | ||
343 | 21 | |||
344 | 22 | from __future__ import print_function | ||
345 | 23 | |||
346 | 24 | from datetime import time, timedelta | ||
347 | 25 | from array import array | ||
348 | 26 | import sys | ||
349 | 27 | |||
350 | 28 | from storm.databases import dummy | ||
351 | 29 | |||
352 | 30 | try: | ||
353 | 31 | import MySQLdb | ||
354 | 32 | import MySQLdb.converters | ||
355 | 33 | except ImportError: | ||
356 | 34 | MySQLdb = dummy | ||
357 | 35 | |||
358 | 36 | from storm.database import ( | ||
359 | 37 | Connection, | ||
360 | 38 | ConnectionWrapper, | ||
361 | 39 | Database, | ||
362 | 40 | Result, | ||
363 | 41 | ) | ||
364 | 42 | from storm.exceptions import ( | ||
365 | 43 | DatabaseModuleError, | ||
366 | 44 | OperationalError, | ||
367 | 45 | wrap_exceptions, | ||
368 | 46 | ) | ||
369 | 47 | from storm.expr import ( | ||
370 | 48 | compile, | ||
371 | 49 | compile_select, | ||
372 | 50 | Insert, | ||
373 | 51 | is_safe_token, | ||
374 | 52 | Select, | ||
375 | 53 | SQLToken, | ||
376 | 54 | Undef, | ||
377 | 55 | ) | ||
378 | 56 | from storm.variables import Variable | ||
379 | 57 | |||
380 | 58 | |||
381 | 59 | compile = compile.create_child() | ||
382 | 60 | |||
383 | 61 | @compile.when(Select) | ||
384 | 62 | def compile_select_mysql(compile, select, state): | ||
385 | 63 | if select.offset is not Undef and select.limit is Undef: | ||
386 | 64 | select.limit = sys.maxsize | ||
387 | 65 | return compile_select(compile, select, state) | ||
388 | 66 | |||
389 | 67 | @compile.when(SQLToken) | ||
390 | 68 | def compile_sql_token_mysql(compile, expr, state): | ||
391 | 69 | """MySQL uses ` as the escape character by default.""" | ||
392 | 70 | if is_safe_token(expr) and not compile.is_reserved_word(expr): | ||
393 | 71 | return expr | ||
394 | 72 | return '`%s`' % expr.replace('`', '``') | ||
395 | 73 | |||
396 | 74 | |||
397 | 75 | class MySQLResult(Result): | ||
398 | 76 | |||
399 | 77 | @staticmethod | ||
400 | 78 | def from_database(row): | ||
401 | 79 | """Convert MySQL-specific datatypes to "normal" Python types. | ||
402 | 80 | |||
403 | 81 | If there are any C{array} instances in the row, convert them | ||
404 | 82 | to strings. | ||
405 | 83 | """ | ||
406 | 84 | for value in row: | ||
407 | 85 | if isinstance(value, array): | ||
408 | 86 | yield value.tostring() | ||
409 | 87 | else: | ||
410 | 88 | yield value | ||
411 | 89 | |||
412 | 90 | |||
413 | 91 | class MySQLConnection(Connection): | ||
414 | 92 | |||
415 | 93 | result_factory = MySQLResult | ||
416 | 94 | param_mark = "%s" | ||
417 | 95 | compile = compile | ||
418 | 96 | |||
419 | 97 | def execute(self, statement, params=None, noresult=False): | ||
420 | 98 | if (isinstance(statement, Insert) and | ||
421 | 99 | statement.primary_variables is not Undef): | ||
422 | 100 | |||
423 | 101 | result = Connection.execute(self, statement, params) | ||
424 | 102 | |||
425 | 103 | # The lastrowid value will be set if: | ||
426 | 104 | # - the table had an AUTO INCREMENT column, and | ||
427 | 105 | # - the column was not set during the insert or set to 0 | ||
428 | 106 | # | ||
429 | 107 | # If these conditions are met, then lastrowid will be the | ||
430 | 108 | # value of the first such column set. We assume that it | ||
431 | 109 | # is the first undefined primary key variable. | ||
432 | 110 | if result._raw_cursor.lastrowid: | ||
433 | 111 | for variable in statement.primary_variables: | ||
434 | 112 | if not variable.is_defined(): | ||
435 | 113 | variable.set(result._raw_cursor.lastrowid, | ||
436 | 114 | from_db=True) | ||
437 | 115 | break | ||
438 | 116 | if noresult: | ||
439 | 117 | result = None | ||
440 | 118 | return result | ||
441 | 119 | return Connection.execute(self, statement, params, noresult) | ||
442 | 120 | |||
443 | 121 | def to_database(self, params): | ||
444 | 122 | for param in params: | ||
445 | 123 | if isinstance(param, Variable): | ||
446 | 124 | param = param.get(to_db=True) | ||
447 | 125 | if isinstance(param, timedelta): | ||
448 | 126 | yield str(param) | ||
449 | 127 | else: | ||
450 | 128 | yield param | ||
451 | 129 | |||
452 | 130 | def is_disconnection_error(self, exc, extra_disconnection_errors=()): | ||
453 | 131 | # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html | ||
454 | 132 | return (isinstance(exc, (OperationalError, | ||
455 | 133 | extra_disconnection_errors)) and | ||
456 | 134 | exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST) | ||
457 | 135 | |||
458 | 136 | |||
459 | 137 | class MySQL(Database): | ||
460 | 138 | |||
461 | 139 | connection_factory = MySQLConnection | ||
462 | 140 | _exception_module = MySQLdb | ||
463 | 141 | _converters = None | ||
464 | 142 | |||
465 | 143 | def __init__(self, uri): | ||
466 | 144 | super(MySQL, self).__init__(uri) | ||
467 | 145 | if MySQLdb is dummy: | ||
468 | 146 | raise DatabaseModuleError("'MySQLdb' module not found") | ||
469 | 147 | self._connect_kwargs = {} | ||
470 | 148 | if uri.database is not None: | ||
471 | 149 | self._connect_kwargs["db"] = uri.database | ||
472 | 150 | if uri.host is not None: | ||
473 | 151 | self._connect_kwargs["host"] = uri.host | ||
474 | 152 | if uri.port is not None: | ||
475 | 153 | self._connect_kwargs["port"] = uri.port | ||
476 | 154 | if uri.username is not None: | ||
477 | 155 | self._connect_kwargs["user"] = uri.username | ||
478 | 156 | if uri.password is not None: | ||
479 | 157 | self._connect_kwargs["passwd"] = uri.password | ||
480 | 158 | for option in ["unix_socket"]: | ||
481 | 159 | if option in uri.options: | ||
482 | 160 | self._connect_kwargs[option] = uri.options.get(option) | ||
483 | 161 | |||
484 | 162 | if self._converters is None: | ||
485 | 163 | # MySQLdb returns a timedelta by default on TIME fields. | ||
486 | 164 | converters = MySQLdb.converters.conversions.copy() | ||
487 | 165 | converters[MySQLdb.converters.FIELD_TYPE.TIME] = _convert_time | ||
488 | 166 | self.__class__._converters = converters | ||
489 | 167 | |||
490 | 168 | self._connect_kwargs["conv"] = self._converters | ||
491 | 169 | self._connect_kwargs["use_unicode"] = True | ||
492 | 170 | self._connect_kwargs["charset"] = uri.options.get("charset", "utf8") | ||
493 | 171 | |||
494 | 172 | def _raw_connect(self): | ||
495 | 173 | raw_connection = ConnectionWrapper( | ||
496 | 174 | MySQLdb.connect(**self._connect_kwargs), self) | ||
497 | 175 | |||
498 | 176 | # Here is another sad story about bad transactional behavior. MySQL | ||
499 | 177 | # offers a feature to automatically reconnect dropped connections. | ||
500 | 178 | # What sounds like a dream, is actually a nightmare for anyone who | ||
501 | 179 | # is dealing with transactions. When a reconnection happens, the | ||
502 | 180 | # currently running transaction is transparently rolled back, and | ||
503 | 181 | # everything that was being done is lost, without notice. Not only | ||
504 | 182 | # that, but the connection may be put back in AUTOCOMMIT mode, even | ||
505 | 183 | # when that's not the default MySQLdb behavior. The MySQL developers | ||
506 | 184 | # quickly understood that this is a terrible idea, and removed the | ||
507 | 185 | # behavior in MySQL 5.0.3. Unfortunately, Debian and Ubuntu still | ||
508 | 186 | # have a patch for the MySQLdb module which *reenables* that | ||
509 | 187 | # behavior by default even past version 5.0.3 of MySQL. | ||
510 | 188 | # | ||
511 | 189 | # Some links: | ||
512 | 190 | # http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html | ||
513 | 191 | # http://dev.mysql.com/doc/refman/5.0/en/mysql-reconnect.html | ||
514 | 192 | # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html | ||
515 | 193 | # | ||
516 | 194 | # What we do here is to explore something that is a very weird | ||
517 | 195 | # side-effect, discovered by reading the code. When we call the | ||
518 | 196 | # ping() with a False argument, the automatic reconnection is | ||
519 | 197 | # disabled in a *permanent* way for this connection. The argument | ||
520 | 198 | # to ping() is new in 1.2.2, though. | ||
521 | 199 | if MySQLdb.version_info >= (1, 2, 2): | ||
522 | 200 | raw_connection.ping(False) | ||
523 | 201 | |||
524 | 202 | return raw_connection | ||
525 | 203 | |||
526 | 204 | def raw_connect(self): | ||
527 | 205 | with wrap_exceptions(self): | ||
528 | 206 | return self._raw_connect() | ||
529 | 207 | |||
530 | 208 | |||
531 | 209 | create_from_uri = MySQL | ||
532 | 210 | |||
533 | 211 | |||
534 | 212 | def _convert_time(time_str): | ||
535 | 213 | h, m, s = time_str.split(":") | ||
536 | 214 | if "." in s: | ||
537 | 215 | f = float(s) | ||
538 | 216 | s = int(f) | ||
539 | 217 | return time(int(h), int(m), s, (f-s)*1000000) | ||
540 | 218 | return time(int(h), int(m), int(s), 0) | ||
541 | 219 | |||
542 | 220 | |||
543 | 221 | # -------------------------------------------------------------------- | ||
544 | 222 | # Reserved words, MySQL specific | ||
545 | 223 | |||
546 | 224 | # The list of reserved words here are MySQL specific. SQL92 reserved words | ||
547 | 225 | # are registered in storm.expr, near the "Reserved words, from SQL1992" | ||
548 | 226 | # comment. The reserved words here were taken from: | ||
549 | 227 | # | ||
550 | 228 | # http://dev.mysql.com/doc/refman/5.4/en/reserved-words.html | ||
551 | 229 | compile.add_reserved_words(""" | ||
552 | 230 | accessible analyze asensitive before bigint binary blob call change | ||
553 | 231 | condition current_user database databases day_hour day_microsecond | ||
554 | 232 | day_minute day_second delayed deterministic distinctrow div dual each | ||
555 | 233 | elseif enclosed escaped exit explain float4 float8 force fulltext | ||
556 | 234 | high_priority hour_microsecond hour_minute hour_second if ignore index | ||
557 | 235 | infile inout int1 int2 int3 int4 int8 iterate keys kill leave limit linear | ||
558 | 236 | lines load localtime localtimestamp lock long longblob longtext loop | ||
559 | 237 | low_priority master_ssl_verify_server_cert mediumblob mediumint mediumtext | ||
560 | 238 | middleint minute_microsecond minute_second mod modifies no_write_to_binlog | ||
561 | 239 | optimize optionally out outfile purge range read_write reads regexp | ||
562 | 240 | release rename repeat replace require return rlike schemas | ||
563 | 241 | second_microsecond sensitive separator show spatial specific | ||
564 | 242 | sql_big_result sql_calc_found_rows sql_small_result sqlexception | ||
565 | 243 | sqlwarning ssl starting straight_join terminated tinyblob tinyint tinytext | ||
566 | 244 | trigger undo unlock unsigned use utc_date utc_time utc_timestamp varbinary | ||
567 | 245 | varcharacter while xor year_month zerofill | ||
568 | 246 | """.split()) | ||
569 | 0 | 247 | ||
570 | === modified file 'storm/docs/tutorial.rst' | |||
571 | --- storm/docs/tutorial.rst 2020-05-26 10:28:24 +0000 | |||
572 | +++ storm/docs/tutorial.rst 2021-04-06 09:31:39 +0000 | |||
573 | @@ -36,7 +36,7 @@ | |||
574 | 36 | >>> database = create_database("sqlite:") | 36 | >>> database = create_database("sqlite:") |
575 | 37 | >>> store = Store(database) | 37 | >>> store = Store(database) |
576 | 38 | 38 | ||
578 | 39 | Two databases are supported at the moment: SQLite and PostgreSQL. | 39 | Three databases are supported at the moment: SQLite, MySQL, and PostgreSQL. |
579 | 40 | The parameter passed to :py:func:`~storm.database.create_database` is an | 40 | The parameter passed to :py:func:`~storm.database.create_database` is an |
580 | 41 | URI, as follows: | 41 | URI, as follows: |
581 | 42 | 42 | ||
582 | @@ -45,7 +45,7 @@ | |||
583 | 45 | # database = create_database( | 45 | # database = create_database( |
584 | 46 | # "scheme://username:password@hostname:port/database_name") | 46 | # "scheme://username:password@hostname:port/database_name") |
585 | 47 | 47 | ||
587 | 48 | The ``scheme`` may be ``sqlite`` or ``postgres``. | 48 | The ``scheme`` may be ``sqlite``, ``mysql``, or ``postgres``. |
588 | 49 | 49 | ||
589 | 50 | Now we have to create the table that will actually hold the data | 50 | Now we have to create the table that will actually hold the data |
590 | 51 | for our class. | 51 | for our class. |
591 | 52 | 52 | ||
592 | === modified file 'storm/tests/databases/base.py' | |||
593 | --- storm/tests/databases/base.py 2020-02-10 15:30:23 +0000 | |||
594 | +++ storm/tests/databases/base.py 2021-04-06 09:31:39 +0000 | |||
595 | @@ -333,6 +333,8 @@ | |||
596 | 333 | that works with data in memory, in fact) becomes a dangerous thing. | 333 | that works with data in memory, in fact) becomes a dangerous thing. |
597 | 334 | 334 | ||
598 | 335 | For PostgreSQL, isolation level must be SERIALIZABLE. | 335 | For PostgreSQL, isolation level must be SERIALIZABLE. |
599 | 336 | For MySQL, isolation level must be REPEATABLE READ (the default), | ||
600 | 337 | and the InnoDB engine must be in use. | ||
601 | 336 | For SQLite, the isolation level already is SERIALIZABLE when not | 338 | For SQLite, the isolation level already is SERIALIZABLE when not |
602 | 337 | in autocommit mode. OTOH, PySQLite is nuts regarding transactional | 339 | in autocommit mode. OTOH, PySQLite is nuts regarding transactional |
603 | 338 | behavior, and will easily offer READ COMMITTED behavior inside a | 340 | behavior, and will easily offer READ COMMITTED behavior inside a |
604 | 339 | 341 | ||
605 | === added file 'storm/tests/databases/mysql.py' | |||
606 | --- storm/tests/databases/mysql.py 1970-01-01 00:00:00 +0000 | |||
607 | +++ storm/tests/databases/mysql.py 2021-04-06 09:31:39 +0000 | |||
608 | @@ -0,0 +1,167 @@ | |||
609 | 1 | # | ||
610 | 2 | # Copyright (c) 2006, 2007 Canonical | ||
611 | 3 | # | ||
612 | 4 | # Written by Gustavo Niemeyer <gustavo@niemeyer.net> | ||
613 | 5 | # | ||
614 | 6 | # This file is part of Storm Object Relational Mapper. | ||
615 | 7 | # | ||
616 | 8 | # Storm is free software; you can redistribute it and/or modify | ||
617 | 9 | # it under the terms of the GNU Lesser General Public License as | ||
618 | 10 | # published by the Free Software Foundation; either version 2.1 of | ||
619 | 11 | # the License, or (at your option) any later version. | ||
620 | 12 | # | ||
621 | 13 | # Storm is distributed in the hope that it will be useful, | ||
622 | 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
623 | 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
624 | 16 | # GNU Lesser General Public License for more details. | ||
625 | 17 | # | ||
626 | 18 | # You should have received a copy of the GNU Lesser General Public License | ||
627 | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
628 | 20 | # | ||
629 | 21 | from __future__ import print_function | ||
630 | 22 | |||
631 | 23 | import os | ||
632 | 24 | |||
633 | 25 | from six.moves.urllib.parse import urlunsplit | ||
634 | 26 | |||
635 | 27 | from storm.databases.mysql import MySQL | ||
636 | 28 | from storm.database import create_database | ||
637 | 29 | from storm.expr import Column, Insert | ||
638 | 30 | from storm.uri import URI | ||
639 | 31 | from storm.variables import IntVariable, UnicodeVariable | ||
640 | 32 | |||
641 | 33 | from storm.tests.databases.base import ( | ||
642 | 34 | DatabaseTest, DatabaseDisconnectionTest, UnsupportedDatabaseTest) | ||
643 | 35 | from storm.tests.databases.proxy import ProxyTCPServer | ||
644 | 36 | from storm.tests.helper import TestHelper | ||
645 | 37 | |||
646 | 38 | |||
647 | 39 | def create_proxy_and_uri(uri): | ||
648 | 40 | """Create a TCP proxy to a Unix-domain database identified by `uri`.""" | ||
649 | 41 | proxy = ProxyTCPServer(uri.options["unix_socket"]) | ||
650 | 42 | proxy_host, proxy_port = proxy.server_address | ||
651 | 43 | proxy_uri = URI(urlunsplit( | ||
652 | 44 | ("mysql", "%s:%s" % (proxy_host, proxy_port), "/storm_test", | ||
653 | 45 | "", ""))) | ||
654 | 46 | return proxy, proxy_uri | ||
655 | 47 | |||
656 | 48 | |||
657 | 49 | class MySQLTest(DatabaseTest, TestHelper): | ||
658 | 50 | |||
659 | 51 | supports_microseconds = False | ||
660 | 52 | |||
661 | 53 | def is_supported(self): | ||
662 | 54 | return bool(os.environ.get("STORM_MYSQL_URI")) | ||
663 | 55 | |||
664 | 56 | def create_database(self): | ||
665 | 57 | self.database = create_database(os.environ["STORM_MYSQL_URI"]) | ||
666 | 58 | |||
667 | 59 | def create_tables(self): | ||
668 | 60 | self.connection.execute("CREATE TABLE number " | ||
669 | 61 | "(one INTEGER, two INTEGER, three INTEGER)") | ||
670 | 62 | self.connection.execute("CREATE TABLE test " | ||
671 | 63 | "(id INT AUTO_INCREMENT PRIMARY KEY," | ||
672 | 64 | " title VARCHAR(50)) ENGINE=InnoDB") | ||
673 | 65 | self.connection.execute("CREATE TABLE datetime_test " | ||
674 | 66 | "(id INT AUTO_INCREMENT PRIMARY KEY," | ||
675 | 67 | " dt TIMESTAMP, d DATE, t TIME, td TEXT) " | ||
676 | 68 | "ENGINE=InnoDB") | ||
677 | 69 | self.connection.execute("CREATE TABLE bin_test " | ||
678 | 70 | "(id INT AUTO_INCREMENT PRIMARY KEY," | ||
679 | 71 | " b BLOB) ENGINE=InnoDB") | ||
680 | 72 | |||
681 | 73 | def test_wb_create_database(self): | ||
682 | 74 | database = create_database("mysql://un:pw@ht:12/db?unix_socket=us") | ||
683 | 75 | self.assertTrue(isinstance(database, MySQL)) | ||
684 | 76 | for key, value in [("db", "db"), ("host", "ht"), ("port", 12), | ||
685 | 77 | ("user", "un"), ("passwd", "pw"), | ||
686 | 78 | ("unix_socket", "us")]: | ||
687 | 79 | self.assertEqual(database._connect_kwargs.get(key), value) | ||
688 | 80 | |||
689 | 81 | def test_charset_defaults_to_utf8(self): | ||
690 | 82 | result = self.connection.execute("SELECT @@character_set_client") | ||
691 | 83 | self.assertEqual(result.get_one(), ("utf8",)) | ||
692 | 84 | |||
693 | 85 | def test_charset_option(self): | ||
694 | 86 | uri = URI(os.environ["STORM_MYSQL_URI"]) | ||
695 | 87 | uri.options["charset"] = "ascii" | ||
696 | 88 | database = create_database(uri) | ||
697 | 89 | connection = database.connect() | ||
698 | 90 | result = connection.execute("SELECT @@character_set_client") | ||
699 | 91 | self.assertEqual(result.get_one(), ("ascii",)) | ||
700 | 92 | |||
701 | 93 | def test_get_insert_identity(self): | ||
702 | 94 | # Primary keys are filled in during execute() for MySQL | ||
703 | 95 | pass | ||
704 | 96 | |||
705 | 97 | def test_get_insert_identity_composed(self): | ||
706 | 98 | # Primary keys are filled in during execute() for MySQL | ||
707 | 99 | pass | ||
708 | 100 | |||
709 | 101 | def test_execute_insert_auto_increment_primary_key(self): | ||
710 | 102 | id_column = Column("id", "test") | ||
711 | 103 | id_variable = IntVariable() | ||
712 | 104 | title_column = Column("title", "test") | ||
713 | 105 | title_variable = UnicodeVariable(u"testing") | ||
714 | 106 | |||
715 | 107 | # This is not part of the table. It is just used to show that | ||
716 | 108 | # only one primary key variable is set from the insert ID. | ||
717 | 109 | dummy_column = Column("dummy", "test") | ||
718 | 110 | dummy_variable = IntVariable() | ||
719 | 111 | |||
720 | 112 | insert = Insert({title_column: title_variable}, | ||
721 | 113 | primary_columns=(id_column, dummy_column), | ||
722 | 114 | primary_variables=(id_variable, dummy_variable)) | ||
723 | 115 | self.connection.execute(insert) | ||
724 | 116 | self.assertTrue(id_variable.is_defined()) | ||
725 | 117 | self.assertFalse(dummy_variable.is_defined()) | ||
726 | 118 | |||
727 | 119 | # The newly inserted row should have the maximum id value for | ||
728 | 120 | # the table. | ||
729 | 121 | result = self.connection.execute("SELECT MAX(id) FROM test") | ||
730 | 122 | self.assertEqual(result.get_one()[0], id_variable.get()) | ||
731 | 123 | |||
732 | 124 | def test_mysql_specific_reserved_words(self): | ||
733 | 125 | reserved_words = """ | ||
734 | 126 | accessible analyze asensitive before bigint binary blob call | ||
735 | 127 | change condition current_user database databases day_hour | ||
736 | 128 | day_microsecond day_minute day_second delayed deterministic | ||
737 | 129 | distinctrow div dual each elseif enclosed escaped exit explain | ||
738 | 130 | float4 float8 force fulltext high_priority hour_microsecond | ||
739 | 131 | hour_minute hour_second if ignore index infile inout int1 int2 | ||
740 | 132 | int3 int4 int8 iterate keys kill leave limit linear lines load | ||
741 | 133 | localtime localtimestamp lock long longblob longtext loop | ||
742 | 134 | low_priority master_ssl_verify_server_cert mediumblob mediumint | ||
743 | 135 | mediumtext middleint minute_microsecond minute_second mod modifies | ||
744 | 136 | no_write_to_binlog optimize optionally out outfile purge range | ||
745 | 137 | read_write reads regexp release rename repeat replace require | ||
746 | 138 | return rlike schemas second_microsecond sensitive separator show | ||
747 | 139 | spatial specific sql_big_result sql_calc_found_rows | ||
748 | 140 | sql_small_result sqlexception sqlwarning ssl starting | ||
749 | 141 | straight_join terminated tinyblob tinyint tinytext trigger undo | ||
750 | 142 | unlock unsigned use utc_date utc_time utc_timestamp varbinary | ||
751 | 143 | varcharacter while xor year_month zerofill | ||
752 | 144 | """.split() | ||
753 | 145 | for word in reserved_words: | ||
754 | 146 | self.assertTrue(self.connection.compile.is_reserved_word(word), | ||
755 | 147 | "Word missing: %s" % (word,)) | ||
756 | 148 | |||
757 | 149 | |||
758 | 150 | class MySQLUnsupportedTest(UnsupportedDatabaseTest, TestHelper): | ||
759 | 151 | |||
760 | 152 | dbapi_module_names = ["MySQLdb"] | ||
761 | 153 | db_module_name = "mysql" | ||
762 | 154 | |||
763 | 155 | |||
764 | 156 | class MySQLDisconnectionTest(DatabaseDisconnectionTest, TestHelper): | ||
765 | 157 | |||
766 | 158 | environment_variable = "STORM_MYSQL_URI" | ||
767 | 159 | host_environment_variable = "STORM_MYSQL_HOST_URI" | ||
768 | 160 | default_port = 3306 | ||
769 | 161 | |||
770 | 162 | def create_proxy(self, uri): | ||
771 | 163 | """See `DatabaseDisconnectionMixin.create_proxy`.""" | ||
772 | 164 | if "unix_socket" in uri.options: | ||
773 | 165 | return create_proxy_and_uri(uri)[0] | ||
774 | 166 | else: | ||
775 | 167 | return super(MySQLDisconnectionTest, self).create_proxy(uri) | ||
776 | 0 | 168 | ||
777 | === modified file 'storm/tests/store/base.py' | |||
778 | --- storm/tests/store/base.py 2020-03-18 16:50:12 +0000 | |||
779 | +++ storm/tests/store/base.py 2021-04-06 09:31:39 +0000 | |||
780 | @@ -901,6 +901,8 @@ | |||
781 | 901 | result2 = self.store.find(Foo, Foo.id != 10) | 901 | result2 = self.store.find(Foo, Foo.id != 10) |
782 | 902 | self.assertEqual(foo in result1.union(result2), True) | 902 | self.assertEqual(foo in result1.union(result2), True) |
783 | 903 | 903 | ||
784 | 904 | if self.__class__.__name__.startswith("MySQL"): | ||
785 | 905 | return | ||
786 | 904 | self.assertEqual(foo in result1.intersection(result2), False) | 906 | self.assertEqual(foo in result1.intersection(result2), False) |
787 | 905 | self.assertEqual(foo in result1.intersection(result1), True) | 907 | self.assertEqual(foo in result1.intersection(result1), True) |
788 | 906 | self.assertEqual(foo in result1.difference(result2), True) | 908 | self.assertEqual(foo in result1.difference(result2), True) |
789 | @@ -1352,6 +1354,8 @@ | |||
790 | 1352 | result2 = self.store.find((Foo, Bar), Bar.foo_id == Foo.id) | 1354 | result2 = self.store.find((Foo, Bar), Bar.foo_id == Foo.id) |
791 | 1353 | self.assertEqual((foo, bar) in result1.union(result2), True) | 1355 | self.assertEqual((foo, bar) in result1.union(result2), True) |
792 | 1354 | 1356 | ||
793 | 1357 | if self.__class__.__name__.startswith("MySQL"): | ||
794 | 1358 | return | ||
795 | 1355 | self.assertEqual((foo, bar) in result1.intersection(result2), True) | 1359 | self.assertEqual((foo, bar) in result1.intersection(result2), True) |
796 | 1356 | self.assertEqual((foo, bar) in result1.difference(result2), False) | 1360 | self.assertEqual((foo, bar) in result1.difference(result2), False) |
797 | 1357 | 1361 | ||
798 | @@ -1468,6 +1472,8 @@ | |||
799 | 1468 | result2 = self.store.find(Foo.title) | 1472 | result2 = self.store.find(Foo.title) |
800 | 1469 | self.assertEqual(u"Title 10" in result1.union(result2), True) | 1473 | self.assertEqual(u"Title 10" in result1.union(result2), True) |
801 | 1470 | 1474 | ||
802 | 1475 | if self.__class__.__name__.startswith("MySQL"): | ||
803 | 1476 | return | ||
804 | 1471 | self.assertEqual(u"Title 10" in result1.intersection(result2), True) | 1477 | self.assertEqual(u"Title 10" in result1.intersection(result2), True) |
805 | 1472 | self.assertEqual(u"Title 10" in result1.difference(result2), False) | 1478 | self.assertEqual(u"Title 10" in result1.difference(result2), False) |
806 | 1473 | 1479 | ||
807 | @@ -5733,6 +5739,9 @@ | |||
808 | 5733 | self.assertEqual(result3.avg(Foo.id), 10) | 5739 | self.assertEqual(result3.avg(Foo.id), 10) |
809 | 5734 | 5740 | ||
810 | 5735 | def test_result_difference(self): | 5741 | def test_result_difference(self): |
811 | 5742 | if self.__class__.__name__.startswith("MySQL"): | ||
812 | 5743 | self.skipTest("Skipping ResultSet.difference tests on MySQL") | ||
813 | 5744 | |||
814 | 5736 | result1 = self.store.find(Foo) | 5745 | result1 = self.store.find(Foo) |
815 | 5737 | result2 = self.store.find(Foo, id=20) | 5746 | result2 = self.store.find(Foo, id=20) |
816 | 5738 | result3 = result1.difference(result2) | 5747 | result3 = result1.difference(result2) |
817 | @@ -5750,6 +5759,9 @@ | |||
818 | 5750 | ]) | 5759 | ]) |
819 | 5751 | 5760 | ||
820 | 5752 | def test_result_difference_with_empty(self): | 5761 | def test_result_difference_with_empty(self): |
821 | 5762 | if self.__class__.__name__.startswith("MySQL"): | ||
822 | 5763 | self.skipTest("Skipping ResultSet.difference tests on MySQL") | ||
823 | 5764 | |||
824 | 5753 | result1 = self.store.find(Foo, id=30) | 5765 | result1 = self.store.find(Foo, id=30) |
825 | 5754 | result2 = EmptyResultSet() | 5766 | result2 = EmptyResultSet() |
826 | 5755 | 5767 | ||
827 | @@ -5760,11 +5772,17 @@ | |||
828 | 5760 | ]) | 5772 | ]) |
829 | 5761 | 5773 | ||
830 | 5762 | def test_result_difference_incompatible(self): | 5774 | def test_result_difference_incompatible(self): |
831 | 5775 | if self.__class__.__name__.startswith("MySQL"): | ||
832 | 5776 | self.skipTest("Skipping ResultSet.difference tests on MySQL") | ||
833 | 5777 | |||
834 | 5763 | result1 = self.store.find(Foo, id=10) | 5778 | result1 = self.store.find(Foo, id=10) |
835 | 5764 | result2 = self.store.find(Bar, id=100) | 5779 | result2 = self.store.find(Bar, id=100) |
836 | 5765 | self.assertRaises(FeatureError, result1.difference, result2) | 5780 | self.assertRaises(FeatureError, result1.difference, result2) |
837 | 5766 | 5781 | ||
838 | 5767 | def test_result_difference_count(self): | 5782 | def test_result_difference_count(self): |
839 | 5783 | if self.__class__.__name__.startswith("MySQL"): | ||
840 | 5784 | self.skipTest("Skipping ResultSet.difference tests on MySQL") | ||
841 | 5785 | |||
842 | 5768 | result1 = self.store.find(Foo) | 5786 | result1 = self.store.find(Foo) |
843 | 5769 | result2 = self.store.find(Foo, id=20) | 5787 | result2 = self.store.find(Foo, id=20) |
844 | 5770 | 5788 | ||
845 | @@ -5782,6 +5800,9 @@ | |||
846 | 5782 | self.assertEqual(result2.count(), 3) | 5800 | self.assertEqual(result2.count(), 3) |
847 | 5783 | 5801 | ||
848 | 5784 | def test_result_intersection(self): | 5802 | def test_result_intersection(self): |
849 | 5803 | if self.__class__.__name__.startswith("MySQL"): | ||
850 | 5804 | self.skipTest("Skipping ResultSet.intersection tests on MySQL") | ||
851 | 5805 | |||
852 | 5785 | result1 = self.store.find(Foo) | 5806 | result1 = self.store.find(Foo) |
853 | 5786 | result2 = self.store.find(Foo, Foo.id.is_in((10, 30))) | 5807 | result2 = self.store.find(Foo, Foo.id.is_in((10, 30))) |
854 | 5787 | result3 = result1.intersection(result2) | 5808 | result3 = result1.intersection(result2) |
855 | @@ -5799,6 +5820,9 @@ | |||
856 | 5799 | ]) | 5820 | ]) |
857 | 5800 | 5821 | ||
858 | 5801 | def test_result_intersection_with_empty(self): | 5822 | def test_result_intersection_with_empty(self): |
859 | 5823 | if self.__class__.__name__.startswith("MySQL"): | ||
860 | 5824 | self.skipTest("Skipping ResultSet.intersection tests on MySQL") | ||
861 | 5825 | |||
862 | 5802 | result1 = self.store.find(Foo, id=30) | 5826 | result1 = self.store.find(Foo, id=30) |
863 | 5803 | result2 = EmptyResultSet() | 5827 | result2 = EmptyResultSet() |
864 | 5804 | result3 = result1.intersection(result2) | 5828 | result3 = result1.intersection(result2) |
865 | @@ -5806,11 +5830,17 @@ | |||
866 | 5806 | self.assertEqual(len(list(result3)), 0) | 5830 | self.assertEqual(len(list(result3)), 0) |
867 | 5807 | 5831 | ||
868 | 5808 | def test_result_intersection_incompatible(self): | 5832 | def test_result_intersection_incompatible(self): |
869 | 5833 | if self.__class__.__name__.startswith("MySQL"): | ||
870 | 5834 | self.skipTest("Skipping ResultSet.intersection tests on MySQL") | ||
871 | 5835 | |||
872 | 5809 | result1 = self.store.find(Foo, id=10) | 5836 | result1 = self.store.find(Foo, id=10) |
873 | 5810 | result2 = self.store.find(Bar, id=100) | 5837 | result2 = self.store.find(Bar, id=100) |
874 | 5811 | self.assertRaises(FeatureError, result1.intersection, result2) | 5838 | self.assertRaises(FeatureError, result1.intersection, result2) |
875 | 5812 | 5839 | ||
876 | 5813 | def test_result_intersection_count(self): | 5840 | def test_result_intersection_count(self): |
877 | 5841 | if self.__class__.__name__.startswith("MySQL"): | ||
878 | 5842 | self.skipTest("Skipping ResultSet.intersection tests on MySQL") | ||
879 | 5843 | |||
880 | 5814 | result1 = self.store.find(Foo, Foo.id.is_in((10, 20))) | 5844 | result1 = self.store.find(Foo, Foo.id.is_in((10, 20))) |
881 | 5815 | result2 = self.store.find(Foo, Foo.id.is_in((10, 30))) | 5845 | result2 = self.store.find(Foo, Foo.id.is_in((10, 30))) |
882 | 5816 | result3 = result1.intersection(result2) | 5846 | result3 = result1.intersection(result2) |
883 | 5817 | 5847 | ||
884 | === added file 'storm/tests/store/mysql.py' | |||
885 | --- storm/tests/store/mysql.py 1970-01-01 00:00:00 +0000 | |||
886 | +++ storm/tests/store/mysql.py 2021-04-06 09:31:39 +0000 | |||
887 | @@ -0,0 +1,108 @@ | |||
888 | 1 | # | ||
889 | 2 | # Copyright (c) 2006, 2007 Canonical | ||
890 | 3 | # | ||
891 | 4 | # Written by Gustavo Niemeyer <gustavo@niemeyer.net> | ||
892 | 5 | # | ||
893 | 6 | # This file is part of Storm Object Relational Mapper. | ||
894 | 7 | # | ||
895 | 8 | # Storm is free software; you can redistribute it and/or modify | ||
896 | 9 | # it under the terms of the GNU Lesser General Public License as | ||
897 | 10 | # published by the Free Software Foundation; either version 2.1 of | ||
898 | 11 | # the License, or (at your option) any later version. | ||
899 | 12 | # | ||
900 | 13 | # Storm is distributed in the hope that it will be useful, | ||
901 | 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
902 | 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
903 | 16 | # GNU Lesser General Public License for more details. | ||
904 | 17 | # | ||
905 | 18 | # You should have received a copy of the GNU Lesser General Public License | ||
906 | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
907 | 20 | # | ||
908 | 21 | from __future__ import print_function | ||
909 | 22 | |||
910 | 23 | import os | ||
911 | 24 | |||
912 | 25 | from storm.database import create_database | ||
913 | 26 | |||
914 | 27 | from storm.tests.store.base import StoreTest, EmptyResultSetTest | ||
915 | 28 | from storm.tests.helper import TestHelper | ||
916 | 29 | |||
917 | 30 | |||
918 | 31 | class MySQLStoreTest(TestHelper, StoreTest): | ||
919 | 32 | |||
920 | 33 | def setUp(self): | ||
921 | 34 | TestHelper.setUp(self) | ||
922 | 35 | StoreTest.setUp(self) | ||
923 | 36 | |||
924 | 37 | def tearDown(self): | ||
925 | 38 | TestHelper.tearDown(self) | ||
926 | 39 | StoreTest.tearDown(self) | ||
927 | 40 | |||
928 | 41 | def is_supported(self): | ||
929 | 42 | return bool(os.environ.get("STORM_MYSQL_URI")) | ||
930 | 43 | |||
931 | 44 | def create_database(self): | ||
932 | 45 | self.database = create_database(os.environ["STORM_MYSQL_URI"]) | ||
933 | 46 | |||
934 | 47 | def create_tables(self): | ||
935 | 48 | connection = self.connection | ||
936 | 49 | connection.execute("CREATE TABLE foo " | ||
937 | 50 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
938 | 51 | " title VARCHAR(50) DEFAULT 'Default Title') " | ||
939 | 52 | "ENGINE=InnoDB") | ||
940 | 53 | connection.execute("CREATE TABLE bar " | ||
941 | 54 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
942 | 55 | " foo_id INTEGER, title VARCHAR(50)) " | ||
943 | 56 | "ENGINE=InnoDB") | ||
944 | 57 | connection.execute("CREATE TABLE bin " | ||
945 | 58 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
946 | 59 | " bin BLOB, foo_id INTEGER) " | ||
947 | 60 | "ENGINE=InnoDB") | ||
948 | 61 | connection.execute("CREATE TABLE link " | ||
949 | 62 | "(foo_id INTEGER, bar_id INTEGER," | ||
950 | 63 | " PRIMARY KEY (foo_id, bar_id)) " | ||
951 | 64 | "ENGINE=InnoDB") | ||
952 | 65 | connection.execute("CREATE TABLE money " | ||
953 | 66 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
954 | 67 | " value NUMERIC(6,4)) " | ||
955 | 68 | "ENGINE=InnoDB") | ||
956 | 69 | connection.execute("CREATE TABLE selfref " | ||
957 | 70 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
958 | 71 | " title VARCHAR(50)," | ||
959 | 72 | " selfref_id INTEGER," | ||
960 | 73 | " INDEX (selfref_id)," | ||
961 | 74 | " FOREIGN KEY (selfref_id) REFERENCES selfref(id)) " | ||
962 | 75 | "ENGINE=InnoDB") | ||
963 | 76 | connection.execute("CREATE TABLE foovalue " | ||
964 | 77 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
965 | 78 | " foo_id INTEGER," | ||
966 | 79 | " value1 INTEGER, value2 INTEGER) " | ||
967 | 80 | "ENGINE=InnoDB") | ||
968 | 81 | connection.execute("CREATE TABLE unique_id " | ||
969 | 82 | "(id VARCHAR(36) PRIMARY KEY) " | ||
970 | 83 | "ENGINE=InnoDB") | ||
971 | 84 | connection.commit() | ||
972 | 85 | |||
973 | 86 | |||
974 | 87 | class MySQLEmptyResultSetTest(TestHelper, EmptyResultSetTest): | ||
975 | 88 | |||
976 | 89 | def setUp(self): | ||
977 | 90 | TestHelper.setUp(self) | ||
978 | 91 | EmptyResultSetTest.setUp(self) | ||
979 | 92 | |||
980 | 93 | def tearDown(self): | ||
981 | 94 | TestHelper.tearDown(self) | ||
982 | 95 | EmptyResultSetTest.tearDown(self) | ||
983 | 96 | |||
984 | 97 | def is_supported(self): | ||
985 | 98 | return bool(os.environ.get("STORM_MYSQL_URI")) | ||
986 | 99 | |||
987 | 100 | def create_database(self): | ||
988 | 101 | self.database = create_database(os.environ["STORM_MYSQL_URI"]) | ||
989 | 102 | |||
990 | 103 | def create_tables(self): | ||
991 | 104 | self.connection.execute("CREATE TABLE foo " | ||
992 | 105 | "(id INT PRIMARY KEY AUTO_INCREMENT," | ||
993 | 106 | " title VARCHAR(50) DEFAULT 'Default Title') " | ||
994 | 107 | "ENGINE=InnoDB") | ||
995 | 108 | self.connection.commit() |
https:/ /paste. ubuntu. com/p/CpVtdGRZg m/ is a diff of the three "new" (restored) files in this branch against the revision immediately before their previous removal, which may make them easier to review. As you can see, there are a few changes for Python 3 support, test directory reorganization, and to support automatic test database provisioning, but they're relatively short.