Merge lp:~free.ekanayaka/storm/schema-advance into lp:storm
- schema-advance
- Merge into trunk
Proposed by
Free Ekanayaka
Status: | Merged |
---|---|
Merged at revision: | 468 |
Proposed branch: | lp:~free.ekanayaka/storm/schema-advance |
Merge into: | lp:storm |
Diff against target: |
641 lines (+336/-59) 6 files modified
storm/schema/patch.py (+108/-29) storm/schema/schema.py (+89/-20) storm/zope/testing.py (+2/-1) tests/schema/patch.py (+86/-7) tests/schema/schema.py (+49/-1) tests/zope/testing.py (+2/-1) |
To merge this branch: | bzr merge lp:~free.ekanayaka/storm/schema-advance |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Björn Tillenius (community) | Approve | ||
Review via email: mp+242131@code.launchpad.net |
Commit message
Description of the change
Add a Schema.advance method that applies to the given store a certain patch version. This is useful for applying patches with the same number across a set of schemas.
To post a comment you must log in.
- 470. By Free Ekanayaka
-
Add PatchPackage class
- 471. By Free Ekanayaka
-
Add dummy patch module
- 472. By Free Ekanayaka
-
More backward compatibility
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks Bjorn, should be all good.
- 473. By Free Ekanayaka
-
Address review comments
Revision history for this message
Björn Tillenius (bjornt) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'storm/schema/patch.py' |
2 | --- storm/schema/patch.py 2010-08-18 08:16:52 +0000 |
3 | +++ storm/schema/patch.py 2014-12-18 10:58:55 +0000 |
4 | @@ -37,6 +37,7 @@ |
5 | import sys |
6 | import os |
7 | import re |
8 | +import types |
9 | |
10 | from storm.locals import StormError, Int |
11 | |
12 | @@ -78,32 +79,25 @@ |
13 | """Apply to a L{Store} the database patches from a given Python package. |
14 | |
15 | @param store: The L{Store} to apply the patches to. |
16 | - @param package: The Python package containing the patches. Each patch is |
17 | - represented by a file inside the module, whose filename must match |
18 | - the format 'patch_N.py', where N is an integer number. |
19 | + @param patch_set: The L{PatchSet} containing the patches to apply. |
20 | @param committer: Optionally an object implementing 'commit()' and |
21 | 'rollback()' methods, to be used to commit or rollback the changes |
22 | after applying a patch. If C{None} is given, the C{store} itself is |
23 | used. |
24 | """ |
25 | |
26 | - def __init__(self, store, package, committer=None): |
27 | + def __init__(self, store, patch_set, committer=None): |
28 | self._store = store |
29 | - self._package = package |
30 | + if isinstance(patch_set, types.ModuleType): |
31 | + # Up to version 0.20.0 the second positional parameter used to |
32 | + # be a raw module containing the patches. We wrap it with PatchSet |
33 | + # for keeping backward-compatibility. |
34 | + patch_set = PatchSet(patch_set) |
35 | + self._patch_set = patch_set |
36 | if committer is None: |
37 | committer = store |
38 | self._committer = committer |
39 | |
40 | - def _module(self, version): |
41 | - """Import the Python module of the patch file with the given version. |
42 | - |
43 | - @param: The version of the module patch to import. |
44 | - @return: The imported module. |
45 | - """ |
46 | - module_name = "patch_%d" % (version,) |
47 | - return __import__(self._package.__name__ + "." + module_name, |
48 | - None, None, ['']) |
49 | - |
50 | def apply(self, version): |
51 | """Execute the patch with the given version. |
52 | |
53 | @@ -116,7 +110,7 @@ |
54 | self._store.add(patch) |
55 | module = None |
56 | try: |
57 | - module = self._module(version) |
58 | + module = self._patch_set.get_patch_module(version) |
59 | module.apply(self._store) |
60 | except StormError: |
61 | self._committer.rollback() |
62 | @@ -136,10 +130,8 @@ |
63 | @raises UnknownPatchError: If the patch table has versions for which |
64 | no patch file actually exists. |
65 | """ |
66 | - unknown_patches = self.get_unknown_patch_versions() |
67 | - if unknown_patches: |
68 | - raise UnknownPatchError(self._store, unknown_patches) |
69 | - for version in self._get_unapplied_versions(): |
70 | + self.check_unknown() |
71 | + for version in self.get_unapplied_versions(): |
72 | self.apply(version) |
73 | |
74 | def mark_applied(self, version): |
75 | @@ -149,12 +141,12 @@ |
76 | |
77 | def mark_applied_all(self): |
78 | """Mark all unapplied patches as applied.""" |
79 | - for version in self._get_unapplied_versions(): |
80 | + for version in self.get_unapplied_versions(): |
81 | self.mark_applied(version) |
82 | |
83 | def has_pending_patches(self): |
84 | """Return C{True} if there are unapplied patches, C{False} if not.""" |
85 | - for version in self._get_unapplied_versions(): |
86 | + for version in self.get_unapplied_versions(): |
87 | return True |
88 | return False |
89 | |
90 | @@ -164,7 +156,7 @@ |
91 | database, but don't appear in the schema's patches module. |
92 | """ |
93 | applied = self._get_applied_patches() |
94 | - known_patches = self._get_patch_versions() |
95 | + known_patches = self._patch_set.get_patch_versions() |
96 | unknown_patches = set() |
97 | |
98 | for patch in applied: |
99 | @@ -172,10 +164,20 @@ |
100 | unknown_patches.add(patch) |
101 | return unknown_patches |
102 | |
103 | - def _get_unapplied_versions(self): |
104 | + def check_unknown(self): |
105 | + """Look for patches that we don't know about. |
106 | + |
107 | + @raises UnknownPatchError: If the store has applied patch versions |
108 | + this schema doesn't know about. |
109 | + """ |
110 | + unknown_patches = self.get_unknown_patch_versions() |
111 | + if unknown_patches: |
112 | + raise UnknownPatchError(self._store, unknown_patches) |
113 | + |
114 | + def get_unapplied_versions(self): |
115 | """Return the versions of all unapplied patches.""" |
116 | applied = self._get_applied_patches() |
117 | - for version in self._get_patch_versions(): |
118 | + for version in self._patch_set.get_patch_versions(): |
119 | if version not in applied: |
120 | yield version |
121 | |
122 | @@ -186,12 +188,89 @@ |
123 | applied.add(patch.version) |
124 | return applied |
125 | |
126 | - def _get_patch_versions(self): |
127 | + |
128 | +class PatchSet(object): |
129 | + """A collection of patch modules. |
130 | + |
131 | + Each patch module lives in a regular Python module file, contained in a |
132 | + sub-directory named against the patch version. For example, given |
133 | + a directory tree like: |
134 | + |
135 | + mypackage/ |
136 | + __init__.py |
137 | + patch_1/ |
138 | + __init__.py |
139 | + foo.py |
140 | + |
141 | + the following code will return a patch module object for foo.py: |
142 | + |
143 | + >>> import mypackage |
144 | + >>> patch_set = PackagePackage(mypackage, sub_level="foo") |
145 | + >>> patch_module = patch_set.get_patch_module(1) |
146 | + >>> print patch_module.__name__ |
147 | + 'mypackage.patch_1.foo' |
148 | + |
149 | + Different sub-levels can be used to apply different patches to different |
150 | + stores (see L{Sharding}). |
151 | + |
152 | + Alternatively if no sub-level is provided, the structure will be flat: |
153 | + |
154 | + mypackage/ |
155 | + __init__.py |
156 | + patch_1.py |
157 | + |
158 | + >>> import mypackage |
159 | + >>> patch_set = PackagePackage(mypackage) |
160 | + >>> patch_module = patch_set.get_patch_module(1) |
161 | + >>> print patch_module.__name__ |
162 | + 'mypackage.patch_1' |
163 | + |
164 | + This simpler structure can be used if you have just one store to patch |
165 | + or you don't care to co-ordinate the patches across your stores. |
166 | + """ |
167 | + |
168 | + def __init__(self, package, sub_level=None): |
169 | + self._package = package |
170 | + self._sub_level = sub_level |
171 | + |
172 | + def get_patch_versions(self): |
173 | """Return the versions of all available patches.""" |
174 | - format = re.compile(r"^patch_(\d+).py$") |
175 | + pattern = r"^patch_(\d+)" |
176 | + if not self._sub_level: |
177 | + pattern += ".py" |
178 | + pattern += "$" |
179 | + format = re.compile(pattern) |
180 | |
181 | - filenames = os.listdir(os.path.dirname(self._package.__file__)) |
182 | + patch_directory = self._get_patch_directory() |
183 | + filenames = os.listdir(patch_directory) |
184 | matches = [(format.match(fn), fn) for fn in filenames] |
185 | matches = sorted(filter(lambda x: x[0], matches), |
186 | - key=lambda x: int(x[1][6:-3])) |
187 | + key=lambda x: int(x[0].group(1))) |
188 | return [int(match.group(1)) for match, filename in matches] |
189 | + |
190 | + def get_patch_module(self, version): |
191 | + """Import the Python module of the patch file with the given version. |
192 | + |
193 | + @param: The version of the module patch to import. |
194 | + @return: The imported module. |
195 | + """ |
196 | + name = "patch_%d" % version |
197 | + levels = [self._package.__name__, name] |
198 | + if self._sub_level: |
199 | + directory = self._get_patch_directory() |
200 | + path = os.path.join(directory, name, self._sub_level + ".py") |
201 | + if not os.path.exists(path): |
202 | + return _EmptyPatchModule() |
203 | + levels.append(self._sub_level) |
204 | + return __import__(".".join(levels), None, None, ['']) |
205 | + |
206 | + def _get_patch_directory(self): |
207 | + """Get the path to the directory of the patch package.""" |
208 | + return os.path.dirname(self._package.__file__) |
209 | + |
210 | + |
211 | +class _EmptyPatchModule(object): |
212 | + """Fake module object with a no-op C{apply} function.""" |
213 | + |
214 | + def apply(self, store): |
215 | + pass |
216 | |
217 | === modified file 'storm/schema/schema.py' |
218 | --- storm/schema/schema.py 2013-09-11 08:00:51 +0000 |
219 | +++ storm/schema/schema.py 2014-12-18 10:58:55 +0000 |
220 | @@ -34,7 +34,8 @@ |
221 | >>> drops = ['DROP TABLE person'] |
222 | >>> deletes = ['DELETE FROM person'] |
223 | >>> import patch_module |
224 | ->>> schema = Schema(creates, drops, deletes, patch_module) |
225 | +>>> patch_set = PatchSet(patch_module) |
226 | +>>> schema = Schema(creates, drops, deletes, patch_set) |
227 | >>> schema.create(store) |
228 | |
229 | |
230 | @@ -42,8 +43,24 @@ |
231 | upgrade the schema over time. |
232 | """ |
233 | |
234 | +import types |
235 | + |
236 | from storm.locals import StormError |
237 | -from storm.schema.patch import PatchApplier |
238 | +from storm.schema.patch import PatchApplier, PatchSet |
239 | + |
240 | + |
241 | +class SchemaMissingError(Exception): |
242 | + """Raised when a L{Store} has no schema at all.""" |
243 | + |
244 | + |
245 | +class UnappliedPatchesError(Exception): |
246 | + """Raised when a L{Store} has unapplied schema patches. |
247 | + |
248 | + @ivar unapplied_versions: A list containing all unapplied patch versions. |
249 | + """ |
250 | + |
251 | + def __init__(self, unapplied_versions): |
252 | + self.unapplied_versions = unapplied_versions |
253 | |
254 | |
255 | class Schema(object): |
256 | @@ -52,7 +69,7 @@ |
257 | @param creates: A list of C{CREATE TABLE} statements. |
258 | @param drops: A list of C{DROP TABLE} statements. |
259 | @param deletes: A list of C{DELETE FROM} statements. |
260 | - @param patch_package: The Python package containing patch modules to apply. |
261 | + @param patch_set: The L{PatchSet} containing patch modules to apply. |
262 | @param committer: Optionally a committer to pass to the L{PatchApplier}. |
263 | |
264 | @see: L{PatchApplier}. |
265 | @@ -61,11 +78,16 @@ |
266 | _drop_patch = "DROP TABLE IF EXISTS patch" |
267 | _autocommit = True |
268 | |
269 | - def __init__(self, creates, drops, deletes, patch_package, committer=None): |
270 | + def __init__(self, creates, drops, deletes, patch_set, committer=None): |
271 | self._creates = creates |
272 | self._drops = drops |
273 | self._deletes = deletes |
274 | - self._patch_package = patch_package |
275 | + if isinstance(patch_set, types.ModuleType): |
276 | + # Up to version 0.20.0 the fourth positional parameter used to |
277 | + # be a raw module containing the patches. We wrap it with PatchSet |
278 | + # for keeping backward-compatibility. |
279 | + patch_set = PatchSet(patch_set) |
280 | + self._patch_set = patch_set |
281 | self._committer = committer |
282 | |
283 | def _execute_statements(self, store, statements): |
284 | @@ -90,10 +112,38 @@ |
285 | """ |
286 | self._autocommit = flag |
287 | |
288 | + def check(self, store): |
289 | + """Check that the given L{Store} is compliant with this L{Schema}. |
290 | + |
291 | + @param store: The L{Store} to check. |
292 | + |
293 | + @raises SchemaMissingError: If there is no schema at all. |
294 | + @raises UnappliedPatchesError: If there are unapplied schema patches. |
295 | + @raises UnknownPatchError: If the store has patches the schema doesn't. |
296 | + """ |
297 | + try: |
298 | + store.execute("SELECT * FROM patch WHERE version=0") |
299 | + except StormError: |
300 | + # No schema at all. Create it from the ground. |
301 | + store.rollback() |
302 | + raise SchemaMissingError() |
303 | + |
304 | + patch_applier = self._build_patch_applier(store) |
305 | + patch_applier.check_unknown() |
306 | + unapplied_versions = list(patch_applier.get_unapplied_versions()) |
307 | + if unapplied_versions: |
308 | + raise UnappliedPatchesError(unapplied_versions) |
309 | + |
310 | def create(self, store): |
311 | - """Run C{CREATE TABLE} SQL statements with C{store}.""" |
312 | + """Run C{CREATE TABLE} SQL statements with C{store}. |
313 | + |
314 | + @raises SchemaAlreadyCreatedError: If the schema for this store was |
315 | + already created. |
316 | + """ |
317 | self._execute_statements(store, [self._create_patch]) |
318 | self._execute_statements(store, self._creates) |
319 | + patch_applier = self._build_patch_applier(store) |
320 | + patch_applier.mark_applied_all() |
321 | |
322 | def drop(self, store): |
323 | """Run C{DROP TABLE} SQL statements with C{store}.""" |
324 | @@ -110,20 +160,39 @@ |
325 | If a schema isn't present a new one will be created. Unapplied |
326 | patches will be applied to an existing schema. |
327 | """ |
328 | - class NoopCommitter(object): |
329 | - commit = lambda _: None |
330 | - rollback = lambda _: None |
331 | - |
332 | - committer = self._committer if self._autocommit else NoopCommitter() |
333 | - patch_applier = PatchApplier(store, self._patch_package, committer) |
334 | + patch_applier = self._build_patch_applier(store) |
335 | try: |
336 | - store.execute("SELECT * FROM patch WHERE version=0") |
337 | - except StormError: |
338 | + self.check(store) |
339 | + except SchemaMissingError: |
340 | # No schema at all. Create it from the ground. |
341 | - store.rollback() |
342 | self.create(store) |
343 | - patch_applier.mark_applied_all() |
344 | - if self._autocommit: |
345 | - store.commit() |
346 | - else: |
347 | - patch_applier.apply_all() |
348 | + except UnappliedPatchesError, error: |
349 | + patch_applier.check_unknown() |
350 | + for version in error.unapplied_versions: |
351 | + self.advance(store, version) |
352 | + |
353 | + def advance(self, store, version): |
354 | + """Advance the schema of C{store} by applying the next unapplied patch. |
355 | + |
356 | + @return: The version of patch that has been applied or C{None} if |
357 | + no patch was applied (i.e. the schema is fully upgraded). |
358 | + """ |
359 | + patch_applier = self._build_patch_applier(store) |
360 | + patch_applier.apply(version) |
361 | + |
362 | + def _build_patch_applier(self, store): |
363 | + """Build a L{PatchApplier} to use for the given C{store}.""" |
364 | + committer = self._committer |
365 | + if not self._autocommit: |
366 | + committer = _NoopCommitter() |
367 | + return PatchApplier(store, self._patch_set, committer) |
368 | + |
369 | + |
370 | +class _NoopCommitter(object): |
371 | + """Dummy committer that does nothing.""" |
372 | + |
373 | + def commit(self): |
374 | + pass |
375 | + |
376 | + def rollback(self): |
377 | + pass |
378 | |
379 | === modified file 'storm/zope/testing.py' |
380 | --- storm/zope/testing.py 2012-08-27 11:57:29 +0000 |
381 | +++ storm/zope/testing.py 2014-12-18 10:58:55 +0000 |
382 | @@ -196,7 +196,8 @@ |
383 | """ |
384 | Return the modification time of the C{schema}'s patch directory. |
385 | """ |
386 | - schema_stat = os.stat(os.path.dirname(schema._patch_package.__file__)) |
387 | + patch_directory = os.path.dirname(schema._patch_set._package.__file__) |
388 | + schema_stat = os.stat(patch_directory) |
389 | return int(schema_stat.st_mtime) |
390 | |
391 | def _get_schema_stamp_mtime(self, name): |
392 | |
393 | === modified file 'tests/schema/patch.py' |
394 | --- tests/schema/patch.py 2010-08-17 23:57:51 +0000 |
395 | +++ tests/schema/patch.py 2014-12-18 10:58:55 +0000 |
396 | @@ -24,7 +24,7 @@ |
397 | |
398 | from storm.locals import StormError, Store, create_database |
399 | from storm.schema.patch import ( |
400 | - Patch, PatchApplier, UnknownPatchError, BadPatchError) |
401 | + Patch, PatchApplier, UnknownPatchError, BadPatchError, PatchSet) |
402 | from tests.mocker import MockerTestCase |
403 | |
404 | |
405 | @@ -99,10 +99,10 @@ |
406 | self.committed += 1 |
407 | |
408 | |
409 | -class PatchTest(MockerTestCase): |
410 | +class PatchApplierTest(MockerTestCase): |
411 | |
412 | def setUp(self): |
413 | - super(PatchTest, self).setUp() |
414 | + super(PatchApplierTest, self).setUp() |
415 | |
416 | self.patchdir = self.makeDir() |
417 | self.pkgdir = os.path.join(self.patchdir, "mypackage") |
418 | @@ -133,6 +133,7 @@ |
419 | |
420 | import mypackage |
421 | self.mypackage = mypackage |
422 | + self.patch_set = PatchSet(mypackage) |
423 | |
424 | # Create another connection just to keep track of the state of the |
425 | # whole transaction manager. See the assertion functions below. |
426 | @@ -152,11 +153,11 @@ |
427 | self.another_store.rollback() |
428 | |
429 | self.committer = Committer() |
430 | - self.patch_applier = PatchApplier(self.store, self.mypackage, |
431 | + self.patch_applier = PatchApplier(self.store, self.patch_set, |
432 | self.committer) |
433 | |
434 | def tearDown(self): |
435 | - super(PatchTest, self).tearDown() |
436 | + super(PatchApplierTest, self).tearDown() |
437 | self.committer.rollback() |
438 | sys.path.remove(self.patchdir) |
439 | for name in list(sys.modules): |
440 | @@ -202,6 +203,21 @@ |
441 | |
442 | self.assert_transaction_committed() |
443 | |
444 | + def test_apply_with_patch_directory(self): |
445 | + """ |
446 | + If the given L{PatchSet} uses sub-level patches, then the |
447 | + L{PatchApplier.apply} method will look at the per-patch directory and |
448 | + apply the relevant sub-level patch. |
449 | + """ |
450 | + path = os.path.join(self.pkgdir, "patch_99") |
451 | + self.makeDir(path=path) |
452 | + self.makeFile(content="", path=os.path.join(path, "__init__.py")) |
453 | + self.makeFile(content=patch_test_0, path=os.path.join(path, "foo.py")) |
454 | + self.patch_set._sub_level = "foo" |
455 | + self.add_module("patch_99/foo.py", patch_test_0) |
456 | + self.patch_applier.apply(99) |
457 | + self.assertTrue(self.store.get(Patch, (99))) |
458 | + |
459 | def test_apply_all(self): |
460 | """ |
461 | L{PatchApplier.apply_all} executes all unapplied patches. |
462 | @@ -239,10 +255,10 @@ |
463 | """ |
464 | self.add_module("patch_666.py", patch_explosion) |
465 | self.add_module("patch_667.py", patch_after_explosion) |
466 | - self.assertEquals(list(self.patch_applier._get_unapplied_versions()), |
467 | + self.assertEquals(list(self.patch_applier.get_unapplied_versions()), |
468 | [42, 380, 666, 667]) |
469 | self.assertRaises(StormError, self.patch_applier.apply_all) |
470 | - self.assertEquals(list(self.patch_applier._get_unapplied_versions()), |
471 | + self.assertEquals(list(self.patch_applier.get_unapplied_versions()), |
472 | [666, 667]) |
473 | |
474 | def test_mark_applied(self): |
475 | @@ -381,3 +397,66 @@ |
476 | self.assertTrue("# Comment" in formatted) |
477 | else: |
478 | self.fail("BadPatchError not raised") |
479 | + |
480 | + |
481 | +class PatchSetTest(MockerTestCase): |
482 | + |
483 | + def setUp(self): |
484 | + super(PatchSetTest, self).setUp() |
485 | + self.sys_dir = self.makeDir() |
486 | + self.package_dir = os.path.join(self.sys_dir, "mypackage") |
487 | + os.makedirs(self.package_dir) |
488 | + |
489 | + self.makeFile( |
490 | + content="", dirname=self.package_dir, basename="__init__.py") |
491 | + |
492 | + sys.path.append(self.sys_dir) |
493 | + import mypackage |
494 | + self.patch_package = PatchSet(mypackage, sub_level="foo") |
495 | + |
496 | + def tearDown(self): |
497 | + super(PatchSetTest, self).tearDown() |
498 | + for name in list(sys.modules): |
499 | + if name == "mypackage" or name.startswith("mypackage."): |
500 | + del sys.modules[name] |
501 | + |
502 | + def test_get_patch_versions(self): |
503 | + """ |
504 | + The C{get_patch_versions} method returns the available patch versions, |
505 | + by looking at directories named like "patch_N". |
506 | + """ |
507 | + patch_1_dir = os.path.join(self.package_dir, "patch_1") |
508 | + os.makedirs(patch_1_dir) |
509 | + self.assertEqual([1], self.patch_package.get_patch_versions()) |
510 | + |
511 | + def test_get_patch_versions_ignores_non_patch_directories(self): |
512 | + """ |
513 | + The C{get_patch_versions} method ignores files or directories not |
514 | + matching the required name pattern. |
515 | + """ |
516 | + random_dir = os.path.join(self.package_dir, "random") |
517 | + os.makedirs(random_dir) |
518 | + self.assertEqual([], self.patch_package.get_patch_versions()) |
519 | + |
520 | + def test_get_patch_module(self): |
521 | + """ |
522 | + The C{get_patch_module} method returns the Python module for the patch |
523 | + with the given version. |
524 | + """ |
525 | + patch_1_dir = os.path.join(self.package_dir, "patch_1") |
526 | + os.makedirs(patch_1_dir) |
527 | + self.makeFile(content="", dirname=patch_1_dir, basename="__init__.py") |
528 | + self.makeFile(content="", dirname=patch_1_dir, basename="foo.py") |
529 | + patch_module = self.patch_package.get_patch_module(1) |
530 | + self.assertEqual("mypackage.patch_1.foo", patch_module.__name__) |
531 | + |
532 | + def test_get_patch_module_no_sub_level(self): |
533 | + """ |
534 | + The C{get_patch_module} method returns a dummy patch module if no |
535 | + sub-level file exists in the patch directory for the given version. |
536 | + """ |
537 | + patch_1_dir = os.path.join(self.package_dir, "patch_1") |
538 | + os.makedirs(patch_1_dir) |
539 | + patch_module = self.patch_package.get_patch_module(1) |
540 | + store = object() |
541 | + self.assertIsNone(patch_module.apply(store)) |
542 | |
543 | === modified file 'tests/schema/schema.py' |
544 | --- tests/schema/schema.py 2012-09-28 12:31:28 +0000 |
545 | +++ tests/schema/schema.py 2014-12-18 10:58:55 +0000 |
546 | @@ -22,7 +22,8 @@ |
547 | import sys |
548 | |
549 | from storm.locals import StormError, Store, create_database |
550 | -from storm.schema import Schema |
551 | +from storm.schema.schema import ( |
552 | + Schema, SchemaMissingError, UnappliedPatchesError) |
553 | from tests.mocker import MockerTestCase |
554 | |
555 | |
556 | @@ -96,6 +97,29 @@ |
557 | |
558 | return Package(package_dir, name) |
559 | |
560 | + def test_check_with_missing_schema(self): |
561 | + """ |
562 | + L{Schema.check} raises an exception if the given store is |
563 | + completely pristine and no schema has been applied yet. |
564 | + """ |
565 | + rollbacks = [] |
566 | + self.store.rollback = lambda: rollbacks.append(True) |
567 | + self.assertRaises(SchemaMissingError, self.schema.check, self.store) |
568 | + self.assertEqual([True], rollbacks) |
569 | + |
570 | + def test_check_with_unapplied_patches(self): |
571 | + """ |
572 | + L{Schema.check} raises an exception if the given store has unapplied |
573 | + schema patches. |
574 | + """ |
575 | + self.schema.create(self.store) |
576 | + contents = """ |
577 | +def apply(store): |
578 | + pass |
579 | +""" |
580 | + self.package.create_module("patch_1.py", contents) |
581 | + self.assertRaises(UnappliedPatchesError, self.schema.check, self.store) |
582 | + |
583 | def test_create(self): |
584 | """ |
585 | L{Schema.create} can be used to create the tables of a L{Store}. |
586 | @@ -104,6 +128,9 @@ |
587 | self.store.execute, "SELECT * FROM person") |
588 | self.schema.create(self.store) |
589 | self.assertEquals(list(self.store.execute("SELECT * FROM person")), []) |
590 | + # By default changes are committed |
591 | + store2 = Store(self.database) |
592 | + self.assertEquals(list(store2.execute("SELECT * FROM person")), []) |
593 | |
594 | def test_create_with_autocommit_off(self): |
595 | """ |
596 | @@ -190,3 +217,24 @@ |
597 | "INSERT INTO person (id, name, phone) VALUES (1, 'Jane', '123')") |
598 | self.assertEquals(list(self.store.execute("SELECT * FROM person")), |
599 | [(1, u"Jane", u"123")]) |
600 | + |
601 | + def test_advance(self): |
602 | + """ |
603 | + L{Schema.advance} executes the given patch version. |
604 | + """ |
605 | + self.schema.create(self.store) |
606 | + contents1 = """ |
607 | +def apply(store): |
608 | + store.execute('ALTER TABLE person ADD COLUMN phone TEXT') |
609 | +""" |
610 | + contents2 = """ |
611 | +def apply(store): |
612 | + store.execute('ALTER TABLE person ADD COLUMN address TEXT') |
613 | +""" |
614 | + self.package.create_module("patch_1.py", contents1) |
615 | + self.package.create_module("patch_2.py", contents2) |
616 | + self.schema.advance(self.store, 1) |
617 | + self.store.execute( |
618 | + "INSERT INTO person (id, name, phone) VALUES (1, 'Jane', '123')") |
619 | + self.assertEquals(list(self.store.execute("SELECT * FROM person")), |
620 | + [(1, u"Jane", u"123")]) |
621 | |
622 | === modified file 'tests/zope/testing.py' |
623 | --- tests/zope/testing.py 2012-08-27 11:57:29 +0000 |
624 | +++ tests/zope/testing.py 2014-12-18 10:58:55 +0000 |
625 | @@ -27,6 +27,7 @@ |
626 | from storm.locals import create_database, Store, Unicode, Int |
627 | from storm.exceptions import IntegrityError |
628 | from storm.testing import CaptureTracer |
629 | +from storm.schema.patch import PatchSet |
630 | |
631 | if has_transaction and has_zope_component and has_testresources: |
632 | from zope.component import provideUtility, getUtility |
633 | @@ -62,7 +63,7 @@ |
634 | drop = ["DROP TABLE test"] |
635 | delete = ["DELETE FROM test"] |
636 | uri = "sqlite:///%s" % self.makeFile() |
637 | - schema = ZSchema(create, drop, delete, patch_package) |
638 | + schema = ZSchema(create, drop, delete, PatchSet(patch_package)) |
639 | self.databases = [{"name": "test", "uri": uri, "schema": schema}] |
640 | self.resource = ZStormResourceManager(self.databases) |
641 | self.store = Store(create_database(uri)) |
Thanks for the changes. The new directory structure looks like a reasonable compromise. It makes it fairly easy to write patches for a single store only and it gives a good overview of which stores get patched.
It looks good in general, but I'm putting it in Needs Fixing, since some tests need to be enabled and rewritten.