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