Merge lp:~stefanor/ibid/upgradeable-db-schema into lp:~ibid-core/ibid/old-trunk-pack-0.92
- upgradeable-db-schema
- Merge into old-trunk-pack-0.92
Status: | Merged |
---|---|
Approved by: | Stefano Rivera |
Approved revision: | 601 |
Merged at revision: | 594 |
Proposed branch: | lp:~stefanor/ibid/upgradeable-db-schema |
Merge into: | lp:~ibid-core/ibid/old-trunk-pack-0.92 |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~stefanor/ibid/upgradeable-db-schema |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Hitchcock | Approve | ||
Michael Gorven | Approve | ||
Review via email: mp+5493@code.launchpad.net |
Commit message
Description of the change
Stefano Rivera (stefanor) wrote : | # |
Stefano Rivera (stefanor) wrote : | # |
To enable schema upgrades on an existing database, you'll have to do something like:
CREATE TABLE schema (
id INTEGER NOT NULL,
"table" VARCHAR(32) NOT NULL,
version INTEGER NOT NULL,
PRIMARY KEY (id),
UNIQUE ("table")
);
INSERT INTO schema (id, "table", version) VALUES (1, "account_
INSERT INTO schema (id, "table", version) VALUES (2, "accounts", 1);
INSERT INTO schema (id, "table", version) VALUES (3, "credentials", 1);
INSERT INTO schema (id, "table", version) VALUES (4, "factoid_names", 1);
INSERT INTO schema (id, "table", version) VALUES (5, "factoid_values", 1);
INSERT INTO schema (id, "table", version) VALUES (6, "factoids", 1);
INSERT INTO schema (id, "table", version) VALUES (7, "feeds", 1);
INSERT INTO schema (id, "table", version) VALUES (8, "identities", 1);
INSERT INTO schema (id, "table", version) VALUES (9, "karma", 1);
INSERT INTO schema (id, "table", version) VALUES (10, "memos", 1);
INSERT INTO schema (id, "table", version) VALUES (11, "permissions", 1);
INSERT INTO schema (id, "table", version) VALUES (12, "seen", 1);
INSERT INTO schema (id, "table", version) VALUES (13, "schema", 1);
INSERT INTO schema (id, "table", version) VALUES (14, "urls", 1);
Also, Known issue to examine tomorrow: Doesn't work in jaunty:
Traceback (most recent call last):
File "scripts/
upgrade_
File "/home/
table.
File "/home/
eval(
File "/home/
self.
File "/home/
if session.
AttributeError: 'SQLiteDialect' object has no attribute 'name'
- 599. By Stefano Rivera
-
Support dialect detection in SQLAlchemy 0.4
Stefano Rivera (stefanor) wrote : | # |
> Also, Known issue to examine tomorrow: Doesn't work in jaunty:
Fixed in r599
- 600. By Stefano Rivera
-
Use getattr() over eval()
Stefano Rivera (stefanor) wrote : | # |
Another known issue: There are no helpers for dealing with indexes yet.
Michael Gorven (mgorven) wrote : | # |
I don't like that the MySQL engine is forced to InnoDB, but this otherwise
looks good.
review approve
- 601. By Stefano Rivera
-
Move InnoDB preference into MySQLModeListener, use DatabaseManager in ibid-setup
Stefano Rivera (stefanor) wrote : | # |
> I don't like that the MySQL engine is forced to InnoDB
That's configurable (and implemented better) in r601
Jonathan Hitchcock (vhata) : | # |
Preview Diff
1 | === modified file 'ibid/__init__.py' |
2 | --- ibid/__init__.py 2009-03-16 20:55:20 +0000 |
3 | +++ ibid/__init__.py 2009-04-13 17:36:47 +0000 |
4 | @@ -93,7 +93,4 @@ |
5 | class SourceException(IbidException): |
6 | pass |
7 | |
8 | -class ConfigException(Exception): |
9 | - pass |
10 | - |
11 | # vi: set et sta sw=4 ts=4: |
12 | |
13 | === modified file 'ibid/core.py' |
14 | --- ibid/core.py 2009-03-24 10:42:41 +0000 |
15 | +++ ibid/core.py 2009-04-13 19:09:37 +0000 |
16 | @@ -172,6 +172,8 @@ |
17 | ibid.processors.append(klass(name)) |
18 | else: |
19 | self.log.debug("Skipping Processor: %s.%s", name, klass.__name__) |
20 | + |
21 | + ibid.models.check_schema_versions(ibid.databases['ibid']) |
22 | |
23 | except Exception, e: |
24 | self.log.exception(u"Couldn't instantiate %s processor of %s plugin", classname, name) |
25 | @@ -235,13 +237,28 @@ |
26 | for database in ibid.config.databases.keys(): |
27 | self.load(database) |
28 | |
29 | + ibid.models.check_schema_versions(self['ibid']) |
30 | + |
31 | def load(self, name): |
32 | uri = ibid.config.databases[name] |
33 | if uri.startswith('sqlite:///'): |
34 | - engine = create_engine('sqlite:///', creator=sqlite_creator(join(ibid.options['base'], expanduser(uri.replace('sqlite:///', '', 1)))), encoding='utf-8', convert_unicode=True, assert_unicode=True, echo=False) |
35 | + engine = create_engine('sqlite:///', |
36 | + creator=sqlite_creator(join(ibid.options['base'], expanduser(uri.replace('sqlite:///', '', 1)))), |
37 | + encoding='utf-8', convert_unicode=True, assert_unicode=True, echo=False) |
38 | + |
39 | else: |
40 | - engine = create_engine(uri, encoding='utf-8', convert_unicode=True, assert_unicode=True) |
41 | + engine = create_engine(uri, encoding='utf-8', convert_unicode=True, assert_unicode=True, echo=False) |
42 | + |
43 | + if uri.startswith('mysql://'): |
44 | + class MySQLModeListener(object): |
45 | + def connect(self, dbapi_con, con_record): |
46 | + dbapi_con.set_sql_mode("ANSI") |
47 | + engine.pool.add_listener(MySQLModeListener()) |
48 | + |
49 | + engine.dialect.use_ansiquotes = True |
50 | + |
51 | self[name] = scoped_session(sessionmaker(bind=engine, transactional=False, autoflush=True)) |
52 | + |
53 | self.log.info(u"Loaded %s database", name) |
54 | |
55 | def __getattr__(self, name): |
56 | |
57 | === modified file 'ibid/models.py' |
58 | --- ibid/models.py 2009-02-18 19:05:10 +0000 |
59 | +++ ibid/models.py 2009-04-13 19:07:30 +0000 |
60 | @@ -1,20 +1,254 @@ |
61 | -from sqlalchemy import Column, Integer, Unicode, DateTime, ForeignKey, UniqueConstraint, MetaData, Table |
62 | +import logging |
63 | + |
64 | +from sqlalchemy import Column, Integer, Unicode, DateTime, ForeignKey, UniqueConstraint, MetaData, Table, PassiveDefault, __version__ |
65 | from sqlalchemy.orm import relation |
66 | from sqlalchemy.ext.declarative import declarative_base |
67 | from sqlalchemy.sql import func |
68 | +from sqlalchemy.sql.expression import text |
69 | +from sqlalchemy.exceptions import OperationalError, InvalidRequestError |
70 | + |
71 | +if __version__ < '0.5': |
72 | + NoResultFound = InvalidRequestError |
73 | +else: |
74 | + from sqlalchemy.orm.exc import NoResultFound |
75 | |
76 | metadata = MetaData() |
77 | Base = declarative_base(metadata=metadata) |
78 | +log = logging.getLogger('ibid.models') |
79 | + |
80 | +class VersionedSchema(object): |
81 | + """For an initial table schema, set |
82 | + table.versioned_schema = VersionedSchema(__table__, 1) |
83 | + Table creation (upgrading to version 1) is implicitly supported. |
84 | + |
85 | + When you have upgrades to the schema, instead of using VersionedSchema |
86 | + directly, derive from it and include your own upgrade_x_to_y(self) methods, |
87 | + where y = x + 1 |
88 | + |
89 | + In the upgrade methods, you can call the helper functions: |
90 | + add_column, drop_column, rename_column, alter_column |
91 | + They try to do the correct thing in most situations, including rebuilding |
92 | + tables in SQLite, which doesn't actually support dropping/altering columns. |
93 | + For column parameters, while you can point to columns in the table |
94 | + definition, it is better style to repeat the Column() specification as the |
95 | + column might be altered in a future version. |
96 | + """ |
97 | + |
98 | + def __init__(self, table, version): |
99 | + self.table = table |
100 | + self.version = version |
101 | + |
102 | + def is_up_to_date(self, session): |
103 | + "Is the table in the database up to date with the schema?" |
104 | + |
105 | + if not session.bind.has_table(self.table.name): |
106 | + return False |
107 | + |
108 | + try: |
109 | + schema = session.query(Schema).filter(Schema.table==unicode(self.table.name)).one() |
110 | + return schema.version == self.version |
111 | + except NoResultFound: |
112 | + return False |
113 | + |
114 | + def upgrade_schema(self, sessionmaker): |
115 | + "Upgrade the table's schema to the latest version." |
116 | + |
117 | + for fk in self.table.foreign_keys: |
118 | + dependancy = fk.target_fullname.split('.')[0] |
119 | + log.debug("Upgrading table %s before %s", dependancy, self.table.name) |
120 | + metadata.tables[dependancy].versioned_schema.upgrade_schema(sessionmaker) |
121 | + |
122 | + self.upgrade_session = session = sessionmaker() |
123 | + trans = session.begin() |
124 | + |
125 | + schema = session.query(Schema).filter(Schema.table==unicode(self.table.name)).first() |
126 | + |
127 | + try: |
128 | + if not schema: |
129 | + log.info(u"Creating table %s", self.table.name) |
130 | + |
131 | + # If MySQL, we prefer InnoDB: |
132 | + if 'mysql_engine' not in self.table.kwargs: |
133 | + self.table.kwargs['mysql_engine'] = 'InnoDB' |
134 | + |
135 | + self.table.create(bind=session.bind) |
136 | + |
137 | + schema = Schema(unicode(self.table.name), self.version) |
138 | + session.save_or_update(schema) |
139 | + |
140 | + elif self.version > schema.version: |
141 | + self.upgrade_reflected_model = MetaData(session.bind, reflect=True) |
142 | + for version in range(schema.version + 1, self.version + 1): |
143 | + log.info(u"Upgrading table %s to version %i", self.table.name, version) |
144 | + |
145 | + trans.commit() |
146 | + trans = session.begin() |
147 | + |
148 | + eval('self.upgrade_%i_to_%i' % (version - 1, version))() |
149 | + |
150 | + schema.version = version |
151 | + session.save_or_update(schema) |
152 | + del self.upgrade_reflected_model |
153 | + |
154 | + trans.commit() |
155 | + |
156 | + except: |
157 | + trans.rollback() |
158 | + raise |
159 | + |
160 | + session.close() |
161 | + del self.upgrade_session |
162 | + |
163 | + def get_reflected_model(self): |
164 | + "Get a reflected table from the current DB's schema" |
165 | + |
166 | + return self.upgrade_reflected_model.tables.get(self.table.name, None) |
167 | + |
168 | + def add_column(self, col): |
169 | + "Add column col to table" |
170 | + |
171 | + session = self.upgrade_session |
172 | + table = self.get_reflected_model() |
173 | + |
174 | + log.debug(u"Adding column %s to table %s", col.name, table.name) |
175 | + |
176 | + table.append_column(col) |
177 | + |
178 | + sg = session.bind.dialect.schemagenerator(session.bind.dialect, session.bind) |
179 | + description = sg.get_column_specification(col) |
180 | + |
181 | + session.execute('ALTER TABLE "%s" ADD COLUMN %s;' % (table.name, description)) |
182 | + |
183 | + def drop_column(self, col_name): |
184 | + "Drop column col_name from table" |
185 | + |
186 | + session = self.upgrade_session |
187 | + |
188 | + log.debug(u"Dropping column %s from table %s", col_name, self.table.name) |
189 | + |
190 | + if session.bind.dialect.name == 'sqlite': |
191 | + self.rebuild_sqlite({col_name: None}) |
192 | + else: |
193 | + session.execute('ALTER TABLE "%s" DROP COLUMN "%s";' % (self.table.name, col_name)) |
194 | + |
195 | + def rename_column(self, col, old_name): |
196 | + "Rename column from old_name to Column col" |
197 | + |
198 | + session = self.upgrade_session |
199 | + table = self.get_reflected_model() |
200 | + |
201 | + log.debug(u"Rename column %s to %s in table %s", old_name, col.name, table.name) |
202 | + |
203 | + if session.bind.dialect.name == 'sqlite': |
204 | + self.rebuild_sqlite({old_name: col}) |
205 | + elif session.bind.dialect.name == 'mysql': |
206 | + self.alter_column(col, old_name) |
207 | + else: |
208 | + session.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s";' % (table.name, old_name, col.name)) |
209 | + |
210 | + def alter_column(self, col, old_name=None, length_only=False): |
211 | + """Change a column (possibly renaming from old_name) to Column col. |
212 | + Specify length_only if the change is simply a change of data-type length.""" |
213 | + |
214 | + session = self.upgrade_session |
215 | + table = self.get_reflected_model() |
216 | + |
217 | + log.debug(u"Altering column %s in table %s", col.name, table.name) |
218 | + |
219 | + sg = session.bind.dialect.schemagenerator(session.bind.dialect, session.bind) |
220 | + description = sg.get_column_specification(col) |
221 | + |
222 | + if session.bind.dialect.name == 'sqlite': |
223 | + #TODO: Automatically detect length_only |
224 | + if length_only: |
225 | + # SQLite doesn't enforce value length restrictions, only type changes have a real effect |
226 | + return |
227 | + |
228 | + self.rebuild_sqlite({old_name is None and col.name or old_name: col}) |
229 | + |
230 | + elif session.bind.dialect.name == 'mysql': |
231 | + session.execute('ALTER TABLE "%s" CHANGE "%s" %s;' |
232 | + % (table.name, old_name is not None and old_name or col.name, description)) |
233 | + |
234 | + else: |
235 | + if old_name is not None: |
236 | + self.rename_column(col, old_name) |
237 | + session.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' |
238 | + % (table.name, col.name, description.split(" ", 1)[1])) |
239 | + |
240 | + def rebuild_sqlite(self, colmap): |
241 | + """SQLite doesn't support modification of table schema - must rebuild the table. |
242 | + colmap maps old column names to new Columns (or None for column deletion). |
243 | + Only modified columns need to be listed, unchaged columns are carried over automatically. |
244 | + Specify table in case name has changed in a more recent version.""" |
245 | + |
246 | + session = self.upgrade_session |
247 | + table = self.get_reflected_model() |
248 | + |
249 | + log.debug(u"Rebuilding SQLite table %s", table.name) |
250 | + |
251 | + fullcolmap = {} |
252 | + for col in table.c: |
253 | + if col.name in colmap: |
254 | + if colmap[col.name] is not None: |
255 | + fullcolmap[col.name] = colmap[col.name].name |
256 | + else: |
257 | + fullcolmap[col.name] = col.name |
258 | + |
259 | + for old, col in colmap.iteritems(): |
260 | + del table.c[old] |
261 | + if col is not None: |
262 | + table.append_column(col) |
263 | + |
264 | + session.execute('ALTER TABLE "%s" RENAME TO "%s_old";' % (table.name, table.name)) |
265 | + table.create() |
266 | + session.execute('INSERT INTO "%s" ("%s") SELECT "%s" FROM "%s_old";' |
267 | + % (table.name, '", "'.join(fullcolmap.values()), '", "'.join(fullcolmap.keys()), table.name)) |
268 | + session.execute('DROP TABLE "%s_old";' % table.name) |
269 | + |
270 | +class Schema(Base): |
271 | + __table__ = Table('schema', Base.metadata, |
272 | + Column('id', Integer, primary_key=True), |
273 | + Column('table', Unicode(32), unique=True, nullable=False), |
274 | + Column('version', Integer, nullable=False), |
275 | + useexisting=True) |
276 | + |
277 | + # Upgrades to this table are probably going to be tricky |
278 | + class SchemaSchema(VersionedSchema): |
279 | + def upgrade_schema(self, sessionmaker): |
280 | + session = sessionmaker() |
281 | + |
282 | + if not session.bind.has_table(self.table.name): |
283 | + metadata.bind = session.bind |
284 | + self.table.kwargs['mysql_engine'] = 'InnoDB' |
285 | + self.table.create() |
286 | + |
287 | + schema = Schema(unicode(self.table.name), self.version) |
288 | + session.save_or_update(schema) |
289 | + |
290 | + session.flush() |
291 | + session.close() |
292 | + |
293 | + __table__.versioned_schema = SchemaSchema(__table__, 1) |
294 | + |
295 | + def __init__(self, table, version=0): |
296 | + self.table = table |
297 | + self.version = version |
298 | + |
299 | + def __repr__(self): |
300 | + return '<Schema %s>' % self.table |
301 | |
302 | class Identity(Base): |
303 | __table__ = Table('identities', Base.metadata, |
304 | - Column('id', Integer, primary_key=True), |
305 | - Column('account_id', Integer, ForeignKey('accounts.id')), |
306 | - Column('source', Unicode(16), nullable=False), |
307 | - Column('identity', Unicode(64), nullable=False), |
308 | - Column('created', DateTime, default=func.current_timestamp()), |
309 | - UniqueConstraint('source', 'identity'), |
310 | - useexisting=True) |
311 | + Column('id', Integer, primary_key=True), |
312 | + Column('account_id', Integer, ForeignKey('accounts.id')), |
313 | + Column('source', Unicode(16), nullable=False), |
314 | + Column('identity', Unicode(64), nullable=False), |
315 | + Column('created', DateTime, default=func.current_timestamp()), |
316 | + UniqueConstraint('source', 'identity'), |
317 | + useexisting=True) |
318 | + |
319 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
320 | |
321 | def __init__(self, source, identity, account_id=None): |
322 | self.source = source |
323 | @@ -26,12 +260,14 @@ |
324 | |
325 | class Attribute(Base): |
326 | __table__ = Table('account_attributes', Base.metadata, |
327 | - Column('id', Integer, primary_key=True), |
328 | - Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
329 | - Column('name', Unicode(32), nullable=False), |
330 | - Column('value', Unicode(128), nullable=False), |
331 | - UniqueConstraint('account_id', 'name'), |
332 | - useexisting=True) |
333 | + Column('id', Integer, primary_key=True), |
334 | + Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
335 | + Column('name', Unicode(32), nullable=False), |
336 | + Column('value', Unicode(128), nullable=False), |
337 | + UniqueConstraint('account_id', 'name'), |
338 | + useexisting=True) |
339 | + |
340 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
341 | |
342 | def __init__(self, name, value): |
343 | self.name = name |
344 | @@ -42,12 +278,14 @@ |
345 | |
346 | class Credential(Base): |
347 | __table__ = Table('credentials', Base.metadata, |
348 | - Column('id', Integer, primary_key=True), |
349 | - Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
350 | - Column('source', Unicode(16)), |
351 | - Column('method', Unicode(16), nullable=False), |
352 | - Column('credential', Unicode(256), nullable=False), |
353 | - useexisting=True) |
354 | + Column('id', Integer, primary_key=True), |
355 | + Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
356 | + Column('source', Unicode(16)), |
357 | + Column('method', Unicode(16), nullable=False), |
358 | + Column('credential', Unicode(256), nullable=False), |
359 | + useexisting=True) |
360 | + |
361 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
362 | |
363 | def __init__(self, method, credential, source=None, account_id=None): |
364 | self.account_id = account_id |
365 | @@ -57,13 +295,14 @@ |
366 | |
367 | class Permission(Base): |
368 | __table__ = Table('permissions', Base.metadata, |
369 | - Column('id', Integer, primary_key=True), |
370 | - Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
371 | - Column('name', Unicode(16), nullable=False), |
372 | - Column('value', Unicode(4), nullable=False), |
373 | - UniqueConstraint('account_id', 'name'), |
374 | - useexisting=True) |
375 | + Column('id', Integer, primary_key=True), |
376 | + Column('account_id', Integer, ForeignKey('accounts.id'), nullable=False), |
377 | + Column('name', Unicode(16), nullable=False), |
378 | + Column('value', Unicode(4), nullable=False), |
379 | + UniqueConstraint('account_id', 'name'), |
380 | + useexisting=True) |
381 | |
382 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
383 | |
384 | def __init__(self, name=None, value=None, account_id=None): |
385 | self.account_id = account_id |
386 | @@ -72,9 +311,11 @@ |
387 | |
388 | class Account(Base): |
389 | __table__ = Table('accounts', Base.metadata, |
390 | - Column('id', Integer, primary_key=True), |
391 | - Column('username', Unicode(32), unique=True, nullable=False), |
392 | - useexisting=True) |
393 | + Column('id', Integer, primary_key=True), |
394 | + Column('username', Unicode(32), unique=True, nullable=False), |
395 | + useexisting=True) |
396 | + |
397 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
398 | |
399 | identities = relation(Identity, backref='account') |
400 | attributes = relation(Attribute) |
401 | @@ -87,4 +328,36 @@ |
402 | def __repr__(self): |
403 | return '<Account %s>' % self.username |
404 | |
405 | +def check_schema_versions(sessionmaker): |
406 | + """Pass through all tables, log out of date ones, |
407 | + and except if not all up to date""" |
408 | + |
409 | + session = sessionmaker() |
410 | + upgrades = [] |
411 | + for table in metadata.tables.itervalues(): |
412 | + if not hasattr(table, 'versioned_schema'): |
413 | + log.error("Table %s is not versioned.", table.name) |
414 | + continue |
415 | + |
416 | + if not table.versioned_schema.is_up_to_date(session): |
417 | + upgrades.append(table.name) |
418 | + |
419 | + if not upgrades: |
420 | + return |
421 | + |
422 | + raise Exception(u"Tables %s are out of date. Run ibid-setup" % u", ".join(upgrades)) |
423 | + |
424 | +def upgrade_schemas(sessionmaker): |
425 | + "Pass through all tables and update schemas" |
426 | + |
427 | + # Make sure schema table is created first |
428 | + metadata.tables['schema'].versioned_schema.upgrade_schema(sessionmaker) |
429 | + |
430 | + for table in metadata.tables.itervalues(): |
431 | + if not hasattr(table, 'versioned_schema'): |
432 | + log.error("Table %s is not versioned.", table.name) |
433 | + continue |
434 | + |
435 | + table.versioned_schema.upgrade_schema(sessionmaker) |
436 | + |
437 | # vi: set et sta sw=4 ts=4: |
438 | |
439 | === modified file 'ibid/plugins/factoid.py' |
440 | --- ibid/plugins/factoid.py 2009-03-17 14:40:03 +0000 |
441 | +++ ibid/plugins/factoid.py 2009-04-13 19:07:30 +0000 |
442 | @@ -10,7 +10,7 @@ |
443 | from ibid.plugins import Processor, match, handler, authorise, auth_responses, RPC |
444 | from ibid.config import Option, IntOption |
445 | from ibid.plugins.identity import get_identities |
446 | -from ibid.models import Base |
447 | +from ibid.models import Base, VersionedSchema |
448 | |
449 | help = {'factoids': u'Factoids are arbitrary pieces of information stored by a key. ' |
450 | u'Factoids beginning with a command such as "<action>" or "<reply>" will supress the "name verb value" output. ' |
451 | @@ -27,6 +27,8 @@ |
452 | Column('time', DateTime, nullable=False, default=func.current_timestamp()), |
453 | useexisting=True) |
454 | |
455 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
456 | + |
457 | def __init__(self, name, identity_id, factoid_id=None): |
458 | self.name = name |
459 | self.factoid_id = factoid_id |
460 | @@ -44,6 +46,8 @@ |
461 | Column('time', DateTime, nullable=False, default=func.current_timestamp()), |
462 | useexisting=True) |
463 | |
464 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
465 | + |
466 | def __init__(self, value, identity_id, factoid_id=None): |
467 | self.value = value |
468 | self.factoid_id = factoid_id |
469 | @@ -58,6 +62,8 @@ |
470 | Column('time', DateTime, nullable=False, default=func.current_timestamp()), |
471 | useexisting=True) |
472 | |
473 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
474 | + |
475 | names = relation(FactoidName, cascade='all,delete', backref='factoid') |
476 | values = relation(FactoidValue, cascade='all,delete', backref='factoid') |
477 | |
478 | |
479 | === modified file 'ibid/plugins/feeds.py' |
480 | --- ibid/plugins/feeds.py 2009-03-11 11:11:04 +0000 |
481 | +++ ibid/plugins/feeds.py 2009-04-13 19:07:30 +0000 |
482 | @@ -12,7 +12,7 @@ |
483 | |
484 | import ibid |
485 | from ibid.plugins import Processor, match, authorise |
486 | -from ibid.models import Base |
487 | +from ibid.models import Base, VersionedSchema |
488 | from ibid.utils import cacheable_download, get_html_parse_tree |
489 | |
490 | help = {'feeds': u'Displays articles from RSS and Atom feeds'} |
491 | @@ -27,6 +27,8 @@ |
492 | Column('identity_id', Integer, ForeignKey('identities.id'), nullable=False), |
493 | Column('time', DateTime, nullable=False), |
494 | useexisting=True) |
495 | + |
496 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
497 | |
498 | feed = None |
499 | entries = None |
500 | |
501 | === modified file 'ibid/plugins/karma.py' |
502 | --- ibid/plugins/karma.py 2009-03-16 10:43:01 +0000 |
503 | +++ ibid/plugins/karma.py 2009-04-13 19:07:30 +0000 |
504 | @@ -7,7 +7,7 @@ |
505 | import ibid |
506 | from ibid.plugins import Processor, match, handler, authorise |
507 | from ibid.config import Option, BoolOption, IntOption |
508 | -from ibid.models import Base |
509 | +from ibid.models import Base, VersionedSchema |
510 | |
511 | help = {'karma': u'Keeps track of karma for people and things.'} |
512 | |
513 | @@ -22,6 +22,8 @@ |
514 | Column('time', DateTime, nullable=False, default=func.current_timestamp()), |
515 | useexisting=True) |
516 | |
517 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
518 | + |
519 | def __init__(self, subject): |
520 | self.subject = subject |
521 | self.changes = 0 |
522 | |
523 | === modified file 'ibid/plugins/memo.py' |
524 | --- ibid/plugins/memo.py 2009-03-18 11:36:41 +0000 |
525 | +++ ibid/plugins/memo.py 2009-04-13 19:07:30 +0000 |
526 | @@ -10,7 +10,7 @@ |
527 | from ibid.config import Option |
528 | from ibid.plugins.auth import permission |
529 | from ibid.plugins.identity import get_identities |
530 | -from ibid.models import Base, Identity, Account |
531 | +from ibid.models import Base, VersionedSchema, Identity, Account |
532 | from ibid.utils import ago |
533 | |
534 | help = {'memo': u'Keeps messages for people.'} |
535 | @@ -29,6 +29,8 @@ |
536 | Column('time', DateTime, nullable=False, default=func.current_timestamp()), |
537 | useexisting=True) |
538 | |
539 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
540 | + |
541 | def __init__(self, from_id, to_id, memo, private=False): |
542 | self.from_id = from_id |
543 | self.to_id = to_id |
544 | |
545 | === modified file 'ibid/plugins/seen.py' |
546 | --- ibid/plugins/seen.py 2009-03-08 13:16:28 +0000 |
547 | +++ ibid/plugins/seen.py 2009-04-13 19:07:30 +0000 |
548 | @@ -7,7 +7,7 @@ |
549 | import ibid |
550 | from ibid.plugins import Processor, match |
551 | from ibid.config import Option |
552 | -from ibid.models import Base, Identity, Account |
553 | +from ibid.models import Base, VersionedSchema, Identity, Account, |
554 | from ibid.utils import ago |
555 | |
556 | help = {'seen': u'Records when people were last seen.'} |
557 | @@ -24,6 +24,8 @@ |
558 | UniqueConstraint('identity_id', 'type'), |
559 | useexisting=True) |
560 | |
561 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
562 | + |
563 | identity = relation('Identity') |
564 | |
565 | def __init__(self, identity_id=None, type='message', channel=None, value=None): |
566 | |
567 | === modified file 'ibid/plugins/url.py' |
568 | --- ibid/plugins/url.py 2009-03-25 14:58:06 +0000 |
569 | +++ ibid/plugins/url.py 2009-04-13 19:07:30 +0000 |
570 | @@ -9,7 +9,7 @@ |
571 | import ibid |
572 | from ibid.plugins import Processor, match, handler |
573 | from ibid.config import Option |
574 | -from ibid.models import Base |
575 | +from ibid.models import Base, VersionedSchema |
576 | from ibid.utils import get_html_parse_tree |
577 | |
578 | help = {'url': u'Captures URLs seen in channel to database and/or to delicious, and shortens and lengthens URLs'} |
579 | @@ -25,6 +25,8 @@ |
580 | Column('time', DateTime, nullable=False), |
581 | useexisting=True) |
582 | |
583 | + __table__.versioned_schema = VersionedSchema(__table__, 1) |
584 | + |
585 | def __init__(self, url, channel, identity_id): |
586 | self.url = url |
587 | self.channel = channel |
588 | |
589 | === modified file 'scripts/ibid-setup' |
590 | --- scripts/ibid-setup 2009-03-16 16:52:51 +0000 |
591 | +++ scripts/ibid-setup 2009-04-13 17:36:47 +0000 |
592 | @@ -13,7 +13,7 @@ |
593 | |
594 | from ibid.plugins.auth import hash |
595 | from ibid.config import FileConfig |
596 | -from ibid.models import Account, Identity, Permission, Credential, metadata |
597 | +from ibid.models import Account, Identity, Permission, Credential, metadata, upgrade_schemas |
598 | |
599 | for module in getModule('ibid.plugins').iterModules(): |
600 | try: |
601 | @@ -63,7 +63,8 @@ |
602 | copyfileobj(resource_stream('ibid', 'logging.ini'), open('logging.ini', 'w')) |
603 | |
604 | engine = create_engine(config.databases['ibid'], encoding='utf-8', convert_unicode=True, assert_unicode=True) |
605 | -metadata.create_all(engine) |
606 | +Session = sessionmaker(bind=engine, transactional=False) |
607 | +upgrade_schemas(Session) |
608 | |
609 | print u'Database tables created' |
610 | |
611 | @@ -77,8 +78,8 @@ |
612 | print 'Password do not match' |
613 | exit(1) |
614 | |
615 | -Session = sessionmaker(bind=engine) |
616 | session = Session() |
617 | +session.begin() |
618 | account = Account(identity) |
619 | identity = Identity(source, identity) |
620 | account.identities.append(identity) |
OK, I (finally) have something workable. To see it in action, take a look at lp:~stefanor/ibid/schema-strings
Known issues:
* Plugins with outdated schemas are simply not loaded. At startup time, this should probably abort startup
* Table upgrade is done with ibid-setup (hit ^C when it prompts you for the initial account). ibid-setup should probably output the upgrade progress (hint: logging) and not prompt for initial accounts, if it detects an upgrade.
Unknown issues:
* please provide :)