Merge lp:~jplacerda/zeitgeist/660307 into lp:zeitgeist/0.1

Proposed by J.P. Lacerda
Status: Merged
Merged at revision: 1760
Proposed branch: lp:~jplacerda/zeitgeist/660307
Merge into: lp:zeitgeist/0.1
Diff against target: 217 lines (+104/-27)
3 files modified
_zeitgeist/engine/__init__.py (+2/-0)
_zeitgeist/engine/sql.py (+54/-25)
test/sql-test.py (+48/-2)
To merge this branch: bzr merge lp:~jplacerda/zeitgeist/660307
Reviewer Review Type Date Requested Status
Siegfried Gevatter Needs Fixing
Markus Korn Pending
Review via email: mp+61268@code.launchpad.net
To post a comment you must log in.
lp:~jplacerda/zeitgeist/660307 updated
1763. By J.P. Lacerda

remove unecessary code block and add tests

Revision history for this message
Siegfried Gevatter (rainct) wrote :

First of all, good work and thank you for working on this! :)

Some comments though:

1)
> for i in xrange(old_version, new_version):
> _set_schema_version(cursor, schema_name, i+1)
> _do_schema_backup()

The _do_schema_backup() call should be before the loop, otherwise the first upgrade isn't protected at all (also, in case of going through several version upgrades, you're needlesly backing up the database several times, the backup scripts don't create any new non-recoverable data, so just having the initial backup is enough).

2)

> # FIXME: This can return True, we just need a decent testing
> # framework first
> return True, cursor

As I said in bug #784011, this isn't true, since upgrade scripts rely on the code being run to do stuff such as updating the event view, creating new indices, etc. I also don't think we ought to change this, since copying the event view, indices, etc. into all upgrade scripts is a pain.

3)

Maybe I'm missing something but it looks like this will go into an endless loop if upgrading the schema fails (for some reason other than Zeitgeist being killed).

review: Needs Fixing
lp:~jplacerda/zeitgeist/660307 updated
1764. By J.P. Lacerda

optimize backup policy

1765. By J.P. Lacerda

reflect backup policy changes on schema upgrade and tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '_zeitgeist/engine/__init__.py'
2--- _zeitgeist/engine/__init__.py 2011-02-09 15:07:17 +0000
3+++ _zeitgeist/engine/__init__.py 2011-05-17 18:40:17 +0000
4@@ -48,6 +48,8 @@
5 BaseDirectory.save_data_path("zeitgeist"))
6 DATABASE_FILE = os.environ.get("ZEITGEIST_DATABASE_PATH",
7 os.path.join(DATA_PATH, "activity.sqlite"))
8+ DATABASE_FILE_BACKUP = os.environ.get("ZEITGEIST_DATABASE_PATH",
9+ os.path.join(DATA_PATH, "activity.sqlite.bck"))
10 DEFAULT_LOG_PATH = os.path.join(BaseDirectory.xdg_cache_home,
11 "zeitgeist", "daemon.log")
12
13
14=== modified file '_zeitgeist/engine/sql.py'
15--- _zeitgeist/engine/sql.py 2011-04-27 18:25:02 +0000
16+++ _zeitgeist/engine/sql.py 2011-05-17 18:40:17 +0000
17@@ -6,6 +6,7 @@
18 # Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
19 # Copyright © 2009-2011 Markus Korn <thekorn@gmx.net>
20 # Copyright © 2009 Seif Lotfy <seif@lotfy.com>
21+# Copyright © 2011 J.P. Lacerda <jpaflacerda@gmail.com>
22 # Copyright © 2011 Collabora Ltd.
23 # By Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com>
24 #
25@@ -26,6 +27,7 @@
26 import logging
27 import time
28 import os
29+import shutil
30
31 from _zeitgeist.engine import constants
32
33@@ -112,8 +114,10 @@
34 named '_zeitgeist.engine.upgrades.$schema_name_$(i)_$(i+1)' and executing
35 the run(cursor) method of those modules until new_version is reached
36 """
37+ _do_schema_backup()
38 for i in xrange(old_version, new_version):
39- # Fire of the right upgrade module
40+ _set_schema_version(cursor, schema_name, -1)
41+ # Fire off the right upgrade module
42 log.info("Upgrading database '%s' from version %s to %s. This may take a while" %
43 (schema_name, i, i+1))
44 upgrader_name = "%s_%s_%s" % (schema_name, i, i+1)
45@@ -122,41 +126,65 @@
46
47 # Update the schema version
48 _set_schema_version(cursor, schema_name, i+1)
49+
50 log.info("Upgrade succesful")
51
52 def _check_core_schema_upgrade (cursor):
53 """Return True if the schema is good and no setup needs to be run"""
54 # See if we have the right schema version, and try an upgrade if needed
55 core_schema_version = _get_schema_version(cursor, constants.CORE_SCHEMA)
56- if core_schema_version is not None:
57- if core_schema_version >= constants.CORE_SCHEMA_VERSION:
58- return True
59- else:
60- try:
61- _do_schema_upgrade (cursor,
62- constants.CORE_SCHEMA,
63- core_schema_version,
64- constants.CORE_SCHEMA_VERSION)
65- # Don't return here. The upgrade process might depend on the
66- # tables, indexes, and views being set up (to avoid code dup)
67- log.info("Running post upgrade setup")
68- return False
69- except Exception, e:
70- log.exception("Failed to upgrade database '%s' from version %s to %s: %s" %
71- (constants.CORE_SCHEMA, core_schema_version, constants.CORE_SCHEMA_VERSION, e))
72- raise SystemExit(27)
73+ if core_schema_version >= constants.CORE_SCHEMA_VERSION:
74+ return True, cursor
75 else:
76- return False
77-
78+ try:
79+ if core_schema_version <= -1:
80+ cursor.connection.commit()
81+ cursor.connection.close()
82+ _do_schema_restore()
83+ cursor = _connect_to_db(constants.DATABASE_FILE)
84+ core_schema_version = _get_schema_version(cursor, constants.CORE_SCHEMA)
85+ log.exception("Database corrupted at upgrade -- upgrading from version %s" % core_schema_version)
86+
87+ _do_schema_upgrade (cursor,
88+ constants.CORE_SCHEMA,
89+ core_schema_version,
90+ constants.CORE_SCHEMA_VERSION)
91+
92+ # Don't return here. The upgrade process might depend on the
93+ # tables, indexes, and views being set up (to avoid code dup)
94+ log.info("Running post upgrade setup")
95+ return False, cursor
96+ except sqlite3.OperationalError:
97+ # Something went wrong while applying the upgrade -- this is
98+ # probably due to a non existing table (this occurs when
99+ # applying core_3_4, for example). We just need to fall through
100+ # the rest of create_db to fix this...
101+ log.exception("Database corrupted -- proceeding")
102+ return False, cursor
103+ except Exception, e:
104+ print e
105+ log.exception("Failed to upgrade database '%s' from version %s to %s: %s" %
106+ (constants.CORE_SCHEMA, core_schema_version, constants.CORE_SCHEMA_VERSION, e))
107+ raise SystemExit(27)
108+
109+def _do_schema_backup ():
110+ shutil.copyfile(constants.DATABASE_FILE, constants.DATABASE_FILE_BACKUP)
111+
112+def _do_schema_restore ():
113+ shutil.move(constants.DATABASE_FILE_BACKUP, constants.DATABASE_FILE)
114+
115+def _connect_to_db(file_path):
116+ conn = sqlite3.connect(file_path)
117+ conn.row_factory = sqlite3.Row
118+ cursor = conn.cursor(UnicodeCursor)
119+ return cursor
120
121 def create_db(file_path):
122 """Create the database and return a default cursor for it"""
123 start = time.time()
124 log.info("Using database: %s" % file_path)
125 new_database = not os.path.exists(file_path)
126- conn = sqlite3.connect(file_path)
127- conn.row_factory = sqlite3.Row
128- cursor = conn.cursor(UnicodeCursor)
129+ cursor = _connect_to_db(file_path)
130
131 # Seif: as result of the optimization story (LP: #639737) we are setting
132 # journal_mode to WAL if possible, this change is irreversible but
133@@ -179,8 +207,9 @@
134 cursor.execute("CREATE TEMP TABLE _fix_cache (table_name VARCHAR, id INTEGER)")
135
136 # Always assume that temporary memory backed DBs have good schemas
137- if constants.DATABASE_FILE != ":memory:":
138- if not new_database and _check_core_schema_upgrade(cursor):
139+ if constants.DATABASE_FILE != ":memory:" and not new_database:
140+ do_upgrade, cursor = _check_core_schema_upgrade(cursor)
141+ if do_upgrade:
142 _time = (time.time() - start)*1000
143 log.debug("Core schema is good. DB loaded in %sms" % _time)
144 return cursor
145
146=== modified file 'test/sql-test.py'
147--- test/sql-test.py 2011-04-27 21:53:04 +0000
148+++ test/sql-test.py 2011-05-17 18:40:17 +0000
149@@ -19,12 +19,14 @@
150 # along with this program. If not, see <http://www.gnu.org/licenses/>.
151 #
152
153-import sys, os
154+import sys, os, shutil
155 sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
156
157 import unittest
158 WhereClause = None
159
160+from _zeitgeist.engine import constants, sql
161+
162 class SQLTest (unittest.TestCase):
163
164 def setUp(self):
165@@ -109,7 +111,51 @@
166 self.assertEquals(where.sql.replace("?", "%s") % tuple(where.arguments),
167 "(actor NOT IN (SELECT id FROM actor " \
168 "WHERE (value >= bar AND value < bas)) OR actor IS NULL)")
169-
170+
171+ def testUpgradeCorruption(self):
172+ # Set up the testing environment:
173+ DATABASE_TEST_FILE = os.environ.get("ZEITGEIST_DATABASE_PATH",
174+ os.path.join(constants.DATA_PATH, "activity.test.sqlite"))
175+ DATABASE_TEST_FILE_BACKUP = os.environ.get("ZEITGEIST_DATABASE_PATH",
176+ os.path.join(constants.DATA_PATH, "activity.test.sqlite.bck"))
177+ if os.path.exists(constants.DATABASE_FILE):
178+ shutil.move(constants.DATABASE_FILE, DATABASE_TEST_FILE)
179+ if os.path.exists(constants.DATABASE_FILE_BACKUP):
180+ shutil.move(constants.DATABASE_FILE_BACKUP, DATABASE_TEST_FILE_BACKUP)
181+
182+ # Ensure we are at version 0:
183+ cursor = sql._connect_to_db(constants.DATABASE_FILE)
184+ self.assertEqual(0, sql._get_schema_version(cursor, constants.CORE_SCHEMA))
185+
186+ # Run through a successful upgrade (core_0_1):
187+ sql._do_schema_upgrade(cursor, constants.CORE_SCHEMA, 0, 1)
188+ self.assertEquals(1, sql._get_schema_version(cursor, constants.CORE_SCHEMA))
189+ sql._set_schema_version(cursor, constants.CORE_SCHEMA, 1)
190+ sql._do_schema_backup()
191+ self.assertTrue(os.path.exists(constants.DATABASE_FILE_BACKUP))
192+
193+ # Simulate a failed upgrade:
194+ sql._set_schema_version(cursor, constants.CORE_SCHEMA, -1)
195+
196+ # ... and then try to fix it:
197+ do_upgrade, cursor = sql._check_core_schema_upgrade(cursor)
198+
199+ # ... and fail again, as a table is missing inbetween core_2_3 and core_3_4:
200+ self.assertFalse(do_upgrade)
201+ self.assertEqual(-1, sql._get_schema_version(cursor, constants.CORE_SCHEMA))
202+
203+ # ... we then let the database structure run through create_db:
204+ cursor = sql.create_db(constants.DATABASE_FILE)
205+
206+ # ... it is now well-structured:
207+ self.assertEquals(constants.CORE_SCHEMA_VERSION,
208+ sql._get_schema_version(cursor, constants.CORE_SCHEMA))
209+
210+ # Clean-up after ourselves:
211+ if os.path.exists(DATABASE_TEST_FILE):
212+ shutil.move(DATABASE_TEST_FILE, constants.DATABASE_FILE)
213+ if os.path.exists(DATABASE_TEST_FILE_BACKUP):
214+ shutil.move(DATABASE_TEST_FILE_BACKUP, constants.DATABASE_FILE_BACKUP)
215
216 if __name__ == "__main__":
217 unittest.main()