1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2012, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from sqlalchemy
import (
20 Table
, Column
, MetaData
, Index
21 Integer
, Float
, Unicode
, UnicodeText
, DateTime
, Boolean
,
22 ForeignKey
, UniqueConstraint
, PickleType
)
23 from sqlalchemy
.orm
import sessionmaker
, relationship
24 from sqlalchemy
.ext
.declarative
import declarative_base
25 from sqlalchemy
.sql
import select
, insert
26 from migrate
import changeset
28 from mediagoblin
.db
.sql
.base
import GMGTableBase
31 # This one will get filled with local migrations
35 #######################################################
36 # Migration set 1: Define initial models, no migrations
37 #######################################################
39 Base1
= declarative_base(cls
=GMGTableBase
)
41 class Creature1(Base1
):
42 __tablename__
= "creature"
44 id = Column(Integer
, primary_key
=True)
45 name
= Column(Unicode
, unique
=True, nullable
=False, index
=True)
46 num_legs
= Column(Integer
, nullable
=False)
47 is_demon
= Column(Boolean
)
50 __tablename__
= "level"
52 id = Column(Unicode
, primary_key
=True)
53 name
= Column(Unicode
, unique
=True, nullable
=False, index
=True)
54 description
= Column(UnicodeText
)
55 exits
= Column(PickleType
)
57 SET1_MODELS
= [Creature1
, Level1
]
61 #######################################################
62 # Migration set 2: A few migrations and new model
63 #######################################################
65 Base2
= declarative_base(cls
=GMGTableBase
)
67 class Creature2(Base2
):
68 __tablename__
= "creature"
70 id = Column(Integer
, primary_key
=True)
71 name
= Column(Unicode
, unique
=True, nullable
=False, index
=True)
72 num_legs
= Column(Integer
, nullable
=False)
73 magical_powers
= relationship("CreaturePower2")
75 class CreaturePower2(Base2
):
76 __tablename__
= "creature_power"
78 id = Column(Integer
, primary_key
=True)
80 Integer
, ForeignKey('creature.id'), nullable
=False)
81 name
= Column(Unicode
)
82 description
= Column(Unicode
)
83 hitpower
= Column(Integer
, nullable
=False)
86 __tablename__
= "level"
88 id = Column(Unicode
, primary_key
=True)
89 name
= Column(Unicode
)
90 description
= Column(UnicodeText
)
92 class LevelExit2(Base2
):
93 __tablename__
= "level_exit"
95 id = Column(Integer
, primary_key
=True)
96 name
= Column(Unicode
)
98 Unicode
, ForeignKey('level.id'), nullable
=False)
100 Unicode
, ForeignKey('level.id'), nullable
=False)
102 SET2_MODELS
= [Creature2
, CreaturePower2
, Level2
, LevelExit2
]
105 @RegisterMigration(1, FULL_MIGRATIONS
)
106 def creature_remove_is_demon(db_conn
):
108 Remove the is_demon field from the creature model. We don't need
111 metadata
= MetaData(bind
=db_conn
.engine
)
112 creature_table
= Table(
113 'creature', metadata
,
114 autoload
=True, autoload_with
=db_conn
.engine
)
115 creature_table
.drop_column('is_demon')
118 @RegisterMigration(2, FULL_MIGRATIONS
)
119 def creature_powers_new_table(db_conn
):
121 Add a new table for creature powers. Nothing needs to go in it
122 yet though as there wasn't anything that previously held this
125 metadata
= MetaData(bind
=db_conn
.engine
)
126 creature_powers
= Table(
127 'creature_power', metadata
,
128 Column('id', Integer
, primary_key
=True),
130 Integer
, ForeignKey('creature.id'), nullable
=False),
131 Column('name', Unicode
),
132 Column('description', Unicode
),
133 Column('hitpower', Integer
, nullable
=False))
134 metadata
.create_all(db_conn
.engine
)
137 @RegisterMigration(3, FULL_MIGRATIONS
)
138 def level_exits_new_table(db_conn
):
140 Make a new table for level exits and move the previously pickled
141 stuff over to here (then drop the old unneeded table)
143 # First, create the table
144 # -----------------------
145 metadata
= MetaData(bind
=db_conn
.engine
)
147 'level_exit', metadata
,
148 Column('id', Integer
, primary_key
=True),
149 Column('name', Unicode
),
151 Integer
, ForeignKey('level.id'), nullable
=False),
153 Integer
, ForeignKey('level.id'), nullable
=False))
154 metadata
.create_all(db_conn
.engine
)
156 # And now, convert all the old exit pickles to new level exits
157 # ------------------------------------------------------------
159 # Minimal representation of level table.
160 # Not auto-introspecting here because of pickle table. I'm not
161 # sure sqlalchemy can auto-introspect pickle columns.
164 Column('id', Integer
, primary_key
=True),
165 Column('exits', PickleType
))
167 # query over and insert
168 result
= db_conn
.execute(
169 select([levels
], levels
.c
.exits
!=None))
172 this_exit
= level
['exits']
174 # Insert the level exit
176 level_exits
.insert().values(
177 name
=this_exit
['name'],
178 from_level
=this_exit
['from_level'],
179 to_level
=this_exit
['to_level']))
181 # Finally, drop the old level exits pickle table
182 # ----------------------------------------------
183 levels
.drop_column('exits')
186 # A hack! At this point we freeze-fame and get just a partial list of
189 SET2_MIGRATIONS
= copy
.copy(FULL_MIGRATIONS
)
191 #######################################################
192 # Migration set 3: Final migrations
193 #######################################################
195 Base3
= declarative_base(cls
=GMGTableBase
)
197 class Creature3(Base3
):
198 __tablename__
= "creature"
200 id = Column(Integer
, primary_key
=True)
201 name
= Column(Unicode
, unique
=True, nullable
=False, index
=True)
202 num_limbs
= Column(Integer
, nullable
=False)
204 class CreaturePower3(Base3
):
205 __tablename__
= "creature_power"
207 id = Column(Integer
, primary_key
=True)
209 Integer
, ForeignKey('creature.id'), nullable
=False, index
=True)
210 name
= Column(Unicode
)
211 description
= Column(Unicode
)
212 hitpower
= Column(Float
, nullable
=False)
213 magical_powers
= relationship("CreaturePower3")
216 __tablename__
= "level"
218 id = Column(Unicode
, primary_key
=True)
219 name
= Column(Unicode
)
220 description
= Column(UnicodeText
)
222 class LevelExit3(Base3
):
223 __tablename__
= "level_exit"
225 id = Column(Integer
, primary_key
=True)
226 name
= Column(Unicode
)
228 Unicode
, ForeignKey('level.id'), nullable
=False, index
=True)
230 Unicode
, ForeignKey('level.id'), nullable
=False, index
=True)
233 SET3_MODELS
= [Creature3
, CreaturePower3
, Level3
, LevelExit3
]
236 @RegisterMigration(4, FULL_MIGRATIONS
)
237 def creature_num_legs_to_num_limbs(db_conn
):
239 Turns out we're tracking all sorts of limbs, not "legs"
240 specifically. Humans would be 4 here, for instance. So we
243 metadata
= MetaData(bind
=db_conn
.engine
)
244 creature_table
= Table(
245 'creature', metadata
,
246 autoload
=True, autoload_with
=db_conn
.engine
)
247 creature_table
.c
.num_legs
.alter(name
="num_limbs")
250 @RegisterMigration(5, FULL_MIGRATIONS
)
251 def level_exit_index_from_and_to_level(db_conn
):
253 Index the from and to levels of the level exit table.
255 metadata
= MetaData(bind
=db_conn
.engine
)
257 'level_exit', metadata
,
258 autoload
=True, autoload_with
=db_conn
.engine
)
259 Index('ix_from_level', level_exit
.c
.from_level
).create(engine
)
260 Index('ix_to_exit', level_exit
.c
.to_exit
).create(engine
)
263 @RegisterMigration(6, FULL_MIGRATIONS
)
264 def creature_power_index_creature(db_conn
):
266 Index our foreign key relationship to the creatures
268 metadata
= MetaData(bind
=db_conn
.engine
)
269 creature_power
= Table(
270 'creature_power', metadata
,
271 autoload
=True, autoload_with
=db_conn
.engine
)
272 Index('ix_creature', creature_power
.c
.creature
).create(engine
)
275 @RegisterMigration(7, FULL_MIGRATIONS
)
276 def creature_power_hitpower_to_float(db_conn
):
278 Convert hitpower column on creature power table from integer to
281 Turns out we want super precise values of how much hitpower there
284 metadata
= MetaData(bind
=db_conn
.engine
)
285 creature_power
= Table(
286 'creature_power', metadata
,
287 autoload
=True, autoload_with
=db_conn
.engine
)
288 creature_power
.c
.hitpower
.alter(type=Float
)
291 def _insert_migration1_objects(session
):
293 Test objects to insert for the first set of things
297 [Creature1(name
='centipede',
300 Creature1(name
='wolf',
303 # don't ask me what a wizardsnake is.
304 Creature1(name
='wizardsnake',
310 [Level1(id='necroplex',
311 name
='The Necroplex',
312 description
='A complex full of pure deathzone.',
314 'deathwell': 'evilstorm',
315 'portal': 'central_park'}),
316 Level1(id='evilstorm',
318 description
='A storm full of pure evil.',
319 exits
={}), # you can't escape the evilstorm
320 Level1(id='central_park'
321 name
='Central Park, NY, NY',
322 description
="New York's friendly Central Park.",
324 'portal': 'necroplex'})])
329 def _insert_migration2_objects(session
):
331 Test objects to insert for the second set of things
344 description
="A blast of icy breath!",
348 description
="A frightening stare, for sure!",
356 description
='A rattle... of DEATH!',
360 description
="The sneakiest stare you've ever seen!"
363 name
='slithery_smoke',
364 description
="A blast of slithery, slithery smoke.",
367 name
='treacherous_tremors',
368 description
="The ground shakes beneath footed animals!",
373 [Level2(id='necroplex',
374 name
='The Necroplex',
375 description
='A complex full of pure deathzone.'),
376 Level2(id='evilstorm',
378 description
='A storm full of pure evil.',
379 exits
=[]), # you can't escape the evilstorm
380 Level2(id='central_park'
381 name
='Central Park, NY, NY',
382 description
="New York's friendly Central Park.")])
386 [LevelExit2(name
='deathwell',
387 from_level
='necroplex',
388 to_level
='evilstorm'),
389 LevelExit2(name
='portal',
390 from_level
='necroplex',
391 to_level
='central_park')])
393 # there are no evilstorm exits because there is no exit from the
398 [LevelExit2(name
='portal',
399 from_level
='central_park',
400 to_level
='necroplex')])
405 def _insert_migration3_objects(session
):
407 Test objects to insert for the third set of things
420 description
="A blast of icy breath!",
424 description
="A frightening stare, for sure!",
432 description
='A rattle... of DEATH!',
436 description
="The sneakiest stare you've ever seen!"
439 name
='slithery_smoke',
440 description
="A blast of slithery, slithery smoke.",
443 name
='treacherous_tremors',
444 description
="The ground shakes beneath footed animals!",
446 # annnnnd one more to test a floating point hitpower
453 description
='Smitten by holy wrath!',
458 [Level3(id='necroplex',
459 name
='The Necroplex',
460 description
='A complex full of pure deathzone.'),
461 Level3(id='evilstorm',
463 description
='A storm full of pure evil.',
464 exits
=[]), # you can't escape the evilstorm
465 Level3(id='central_park'
466 name
='Central Park, NY, NY',
467 description
="New York's friendly Central Park.")])
471 [LevelExit3(name
='deathwell',
472 from_level
='necroplex',
473 to_level
='evilstorm'),
474 LevelExit3(name
='portal',
475 from_level
='necroplex',
476 to_level
='central_park')])
478 # there are no evilstorm exits because there is no exit from the
483 [LevelExit3(name
='portal',
484 from_level
='central_park',
485 to_level
='necroplex')])
490 def create_test_engine():
491 from sqlalchemy
import create_engine
492 engine
= create_engine('sqlite:///:memory:', echo
=False)
496 def test_set1_to_set3():
497 # Create / connect to database
498 # Create tables by migrating on empty initial set
500 # Install the initial set
501 # Check version in database
502 # Sanity check a few things in the database
505 # Make sure version matches expected
506 # Check all things in database match expected
510 def test_set2_to_set3():
511 # Create / connect to database
512 # Create tables by migrating on empty initial set
514 # Install the initial set
515 # Check version in database
516 # Sanity check a few things in the database
519 # Make sure version matches expected
520 # Check all things in database match expected
524 def test_set1_to_set2_to_set3():
525 # Create / connect to database
526 # Create tables by migrating on empty initial set
528 # Install the initial set
529 # Check version in database
530 # Sanity check a few things in the database
533 # Make sure version matches expected
534 # Check all things in database match expected
537 # Make sure version matches expected again
538 # Check all things in database match expected again