diff --git a/pokedex/db/tables/__init__.py b/pokedex/db/tables/__init__.py
new file mode 100644
index 0000000..29532d9
--- /dev/null
+++ b/pokedex/db/tables/__init__.py
@@ -0,0 +1,61 @@
+# encoding: utf8
+
+u"""The Pokédex schema.
+
+Columns have a info dictionary with these keys:
+- official: True if the values appear in games or official material; False if
+  they are fan-created or fan-written. This flag is currently only set for
+  official text columns.
+- format: The format of a text column. Can be one of:
+  - plaintext: Normal Unicode text (widely used in names)
+  - markdown: Veekun's Markdown flavor (generally used in effect descriptions)
+  - gametext: Transcription of in-game text that strives to be both
+    human-readable and represent the original text exactly.
+  - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
+    for translation.
+  - latex: A formula in LaTeX syntax.
+- ripped: True for text that has been ripped from the games, and can be ripped
+  again for new versions or languages
+
+- string_getter: for translation columns, a function taking (text, session,
+  language) that is used for properties on the main table. Used for Markdown
+  text.
+
+See `pokedex.db.multilang` for how localizable text columns work.  The session
+classes in that module can be used to change the default language.
+"""
+
+from pokedex.db.tables.base import (
+    metadata, TableBase, mapped_classes, Language, create_translation_table)
+
+from pokedex.db.tables.core import (
+    Ability, AbilityChangelog, AbilityFlavorText, Berry, BerryFirmness,
+    BerryFlavor, Characteristic, ContestCombo, ContestEffect, ContestType,
+    EggGroup, Encounter, EncounterCondition, EncounterConditionValue,
+    EncounterConditionValueMap, EncounterMethod, EncounterSlot, EvolutionChain,
+    EvolutionTrigger, Experience, Gender, Generation, GrowthRate, Item,
+    ItemCategory, ItemFlag, ItemFlagMap, ItemFlavorText, ItemFlingEffect,
+    ItemGameIndex, ItemPocket, Location, LocationArea,
+    LocationAreaEncounterRate, LocationGameIndex, Machine, Move,
+    MoveBattleStyle, MoveChangelog, MoveDamageClass, MoveEffect,
+    MoveEffectChangelog, MoveFlag, MoveFlagMap, MoveFlavorText, MoveMeta,
+    MoveMetaAilment, MoveMetaCategory, MoveMetaStatChange, MoveTarget, Nature,
+    NatureBattleStylePreference, NaturePokeathlonStat, PalPark, PalParkArea,
+    PokeathlonStat, Pokedex, PokedexVersionGroup, Pokemon, PokemonAbility,
+    PokemonColor, PokemonDexNumber, PokemonEggGroup, PokemonEvolution,
+    PokemonForm, PokemonFormGeneration, PokemonFormPokeathlonStat,
+    PokemonGameIndex, PokemonHabitat, PokemonItem, PokemonMove,
+    PokemonMoveMethod, PokemonShape, PokemonSpecies, PokemonSpeciesFlavorText,
+    PokemonStat, PokemonType, Region, Stat, SuperContestCombo,
+    SuperContestEffect, Type, TypeEfficacy, TypeGameIndex, Version,
+    VersionGroup, VersionGroupPokemonMoveMethod, VersionGroupRegion)
+
+from pokedex.db.tables.conquest import (
+    ConquestEpisode, ConquestEpisodeWarrior, ConquestKingdom, ConquestMaxLink,
+    ConquestMoveData, ConquestMoveDisplacement, ConquestMoveEffect,
+    ConquestMoveRange, ConquestPokemonAbility, ConquestPokemonEvolution,
+    ConquestPokemonMove, ConquestPokemonStat, ConquestStat,
+    ConquestTransformationPokemon, ConquestTransformationWarrior,
+    ConquestWarrior, ConquestWarriorArchetype, ConquestWarriorRank,
+    ConquestWarriorRankStatMap, ConquestWarriorSkill, ConquestWarriorSpecialty,
+    ConquestWarriorStat, ConquestWarriorTransformation)
diff --git a/pokedex/db/tables/base.py b/pokedex/db/tables/base.py
new file mode 100644
index 0000000..0562031
--- /dev/null
+++ b/pokedex/db/tables/base.py
@@ -0,0 +1,80 @@
+# encoding: utf8
+
+from functools import partial
+
+from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
+from sqlalchemy import Column, MetaData
+from sqlalchemy.types import Boolean, Integer, Unicode
+
+from pokedex.db import multilang
+
+
+class TableSuperclass(object):
+    """Superclass for declarative tables, to give them some generic niceties
+    like stringification.
+    """
+    def __unicode__(self):
+        """Be as useful as possible.  Show the primary key, and an identifier
+        if we've got one.
+        """
+        typename = u'.'.join((__name__, type(self).__name__))
+
+        pk_constraint = self.__table__.primary_key
+        if not pk_constraint:
+            return u"<%s object at %x>" % (typename, id(self))
+
+        pk = u', '.join(unicode(getattr(self, column.name))
+            for column in pk_constraint.columns)
+        try:
+            return u"<%s object (%s): %s>" % (typename, pk, self.identifier)
+        except AttributeError:
+            return u"<%s object (%s)>" % (typename, pk)
+
+    def __str__(self):
+        return unicode(self).encode('utf8')
+
+    def __repr__(self):
+        return unicode(self).encode('utf8')
+
+
+mapped_classes = []
+class TableMetaclass(DeclarativeMeta):
+    def __init__(cls, name, bases, attrs):
+        super(TableMetaclass, cls).__init__(name, bases, attrs)
+        if hasattr(cls, '__tablename__'):
+            mapped_classes.append(cls)
+            cls.translation_classes = []
+
+metadata = MetaData()
+TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
+
+
+### Need Language first, to create the partial() below
+
+class Language(TableBase):
+    u"""A language the Pokémon games have been translated into."""
+    __tablename__ = 'languages'
+    __singlename__ = 'language'
+    id = Column(Integer, primary_key=True, nullable=False,
+        doc=u"A numeric ID")
+    iso639 = Column(Unicode(79), nullable=False,
+        doc=u"The two-letter code of the country where this language is spoken. Note that it is not unique.",
+        info=dict(format='identifier'))
+    iso3166 = Column(Unicode(79), nullable=False,
+        doc=u"The two-letter code of the language. Note that it is not unique.",
+        info=dict(format='identifier'))
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u"An identifier",
+        info=dict(format='identifier'))
+    official = Column(Boolean, nullable=False, index=True,
+        doc=u"True iff games are produced in the language.")
+    order = Column(Integer, nullable=True,
+        doc=u"Order for sorting in foreign name lists.")
+
+create_translation_table = partial(multilang.create_translation_table, language_class=Language)
+
+create_translation_table('language_names', Language, 'names',
+    name = Column(Unicode(79), nullable=False, index=True,
+        doc=u"The name",
+        info=dict(format='plaintext', official=True)),
+)
diff --git a/pokedex/db/tables/conquest.py b/pokedex/db/tables/conquest.py
new file mode 100644
index 0000000..a63df91
--- /dev/null
+++ b/pokedex/db/tables/conquest.py
@@ -0,0 +1,550 @@
+# encoding: utf8
+
+from sqlalchemy import Column, ForeignKey, UniqueConstraint
+from sqlalchemy.orm import backref, relationship
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.types import Boolean, Integer, Unicode, UnicodeText
+
+from pokedex.db.tables.base import TableBase, create_translation_table
+from pokedex.db.tables.core import (
+    Move, Type, Ability, PokemonSpecies, Gender, Item)
+
+from pokedex.db import markdown
+
+class ConquestEpisode(TableBase):
+    u"""An episode from Pokémon Conquest: one of a bunch of mini-stories
+    featuring a particular warrior.
+
+    The main story, "The Legend of Ransei", also counts, even though it's not
+    in the episode select menu and there's no way to replay it.
+    """
+    __tablename__ = 'conquest_episodes'
+    __singlename__ = 'episode'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this episode.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this episode.',
+        info=dict(format='identifier'))
+
+create_translation_table('conquest_episode_names', ConquestEpisode, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestEpisodeWarrior(TableBase):
+    u"""A warrior featured in an episode in Pokémon Conquest.
+
+    This needs its own table because of the player having two episodes and
+    there being two players.
+    """
+    __tablename__ = 'conquest_episode_warriors'
+    episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), primary_key=True,
+        doc=u'The ID of the episode.')
+    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True,
+        doc=u'The ID of the warrior.')
+
+class ConquestKingdom(TableBase):
+    u"""A kingdom in Pokémon Conquest."""
+    __tablename__ = 'conquest_kingdoms'
+    __singlename__ = 'kingdom'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u"An ID for this kingdom.")
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u"A readable identifier for this kingdom.",
+        info=dict(format='identifier'))
+    type_id = Column(Integer, ForeignKey('types.id'), nullable=False,
+        doc=u"The type associated with this kingdom in-game.")
+
+create_translation_table('conquest_kingdom_names', ConquestKingdom, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestMaxLink(TableBase):
+    u"""The maximum link a warrior rank can reach with a Pokémon in Pokémon Conquest."""
+    __tablename__ = 'conquest_max_links'
+    warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True,
+        doc=u"The ID of the warrior rank.")
+    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True,
+        doc=u'The ID of the Pokémon species.')
+    max_link = Column(Integer, nullable=False,
+        doc=u'The maximum link percentage this warrior rank and Pokémon can reach.')
+
+class ConquestMoveData(TableBase):
+    u"""Data about a move in Pokémon Conquest."""
+    __tablename__ = 'conquest_move_data'
+    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the move.')
+    power = Column(Integer, nullable=True,
+        doc=u"The move's power, null if it does no damage.")
+    accuracy = Column(Integer, nullable=True,
+        doc=u"The move's base accuracy, null if it is self-targeted or never misses.")
+    effect_chance = Column(Integer, nullable=True,
+        doc=u"The chance as a percentage that the move's secondary effect will trigger.")
+    effect_id = Column(Integer, ForeignKey('conquest_move_effects.id'), nullable=False,
+        doc=u"The ID of the move's effect.")
+    range_id = Column(Integer, ForeignKey('conquest_move_ranges.id'), nullable=False,
+        doc=u"The ID of the move's range.")
+    displacement_id = Column(Integer, ForeignKey('conquest_move_displacements.id'), nullable=True,
+        doc=u"The ID of the move's displacement.")
+
+    @property
+    def star_rating(self):
+        """Return the move's in-game power rating as a number of stars."""
+        if not self.power:
+            return 0
+        else:
+            stars = (self.power - 1) // 10
+            stars = min(stars, 5)  # i.e. maximum of 5 stars
+            stars = max(stars, 1)  # And minimum of 1
+            return stars
+
+class ConquestMoveDisplacement(TableBase):
+    u"""A way in which a move can cause the user or target to move to a
+    different tile.
+
+    If a move displaces its user, the move's range is relative to the user's
+    original position.
+    """
+    __tablename__ = 'conquest_move_displacements'
+    __singlename__ = 'move_displacement'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this displacement.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this displacement.',
+        info=dict(format='identifier'))
+    affects_target = Column(Boolean, nullable=False,
+        doc=u'True iff the move displaces its target(s) and not its user.')
+
+create_translation_table('conquest_move_displacement_prose', ConquestMoveDisplacement, 'prose',
+    name = Column(Unicode(79), nullable=True,
+        doc=u'A name for the displacement.',
+        info=dict(format='plaintext')),
+    short_effect = Column(UnicodeText, nullable=True,
+        doc=u"A short summary of how the displacement works, to be used in the move's short effect.",
+        info=dict(format='markdown')),
+    effect = Column(UnicodeText, nullable=True,
+        doc=u"A detailed description of how the displacement works, to be used alongside the move's long effect.",
+        info=dict(format='markdown')),
+)
+
+class ConquestMoveEffect(TableBase):
+    u"""An effect moves can have in Pokémon Conquest."""
+    __tablename__ = 'conquest_move_effects'
+    __singlename__ = 'conquest_move_effect'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this effect.')
+
+create_translation_table('conquest_move_effect_prose', ConquestMoveEffect, 'prose',
+    short_effect = Column(UnicodeText, nullable=True,
+        doc=u"A short summary of the effect",
+        info=dict(format='markdown')),
+    effect = Column(UnicodeText, nullable=True,
+        doc=u"A detailed description of the effect",
+        info=dict(format='markdown')),
+)
+
+class ConquestMoveRange(TableBase):
+    u"""A set of tiles moves can target in Pokémon Conquest."""
+    __tablename__ = 'conquest_move_ranges'
+    __singlename__ = 'conquest_move_range'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this range.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this range.',
+        info=dict(format='identifier'))
+    targets = Column(Integer, nullable=False,
+        doc=u'The number of tiles this range targets.')
+
+create_translation_table('conquest_move_range_prose', ConquestMoveRange, 'prose',
+    name = Column(Unicode(79), nullable=True,
+        doc=u"A short name briefly describing the range",
+        info=dict(format='plaintext')),
+    description = Column(UnicodeText, nullable=True,
+        doc=u"A detailed description of the range",
+        info=dict(format='plaintext')),
+)
+
+class ConquestPokemonAbility(TableBase):
+    u"""An ability a Pokémon species has in Pokémon Conquest."""
+    __tablename__ = 'conquest_pokemon_abilities'
+    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, nullable=False, autoincrement=False,
+        doc=u'The ID of the Pokémon species with this ability.')
+    slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
+        doc=u"The order abilities are listed in.  Upon evolution, if a Pokémon's abilities change, it will receive the one in the same slot.")
+    ability_id = Column(Integer, ForeignKey('abilities.id'), nullable=False,
+        doc=u'The ID of the ability.')
+
+class ConquestPokemonEvolution(TableBase):
+    u"""The conditions under which a Pokémon must successfully complete an
+    action to evolve in Pokémon Conquest.
+
+    Any condition may be null if it does not apply for a particular Pokémon.
+    """
+    __tablename__ = 'conquest_pokemon_evolution'
+    evolved_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, nullable=False,
+        doc=u"The ID of the post-evolution species.")
+    required_stat_id = Column(Integer, ForeignKey('conquest_stats.id'), nullable=True,
+        doc=u"The ID of the stat which minimum_stat applies to.")
+    minimum_stat = Column(Integer, nullable=True,
+        doc=u"The minimum value the Pokémon must have in a particular stat.")
+    minimum_link = Column(Integer, nullable=True,
+        doc=u"The minimum link percentage the Pokémon must have with its warrior.")
+    kingdom_id = Column(Integer, ForeignKey('conquest_kingdoms.id'), nullable=True,
+        doc=u"The ID of the kingdom in which this Pokémon must complete an action after meeting all other requirements.")
+    warrior_gender_id = Column(Integer, ForeignKey('genders.id'), nullable=True,
+        doc=u"The ID of the gender the Pokémon's warrior must be.")
+    item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
+        doc=u"The ID of the item the Pokémon's warrior must have equipped.")
+    recruiting_ko_required = Column(Boolean, nullable=False,
+        doc=u"If true, the Pokémon must KO a Pokémon under the right conditions to recruit that Pokémon's warrior.")
+
+class ConquestPokemonMove(TableBase):
+    u"""A Pokémon's move in Pokémon Conquest.
+
+    Yes, "move"; each Pokémon has exactly one.
+    """
+    __tablename__ = 'conquest_pokemon_moves'
+    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the Pokémon species.')
+    move_id = Column(Integer, ForeignKey('moves.id'), nullable=False,
+        doc=u'The ID of the move.')
+
+class ConquestPokemonStat(TableBase):
+    u"""A Pokémon's base stat in Pokémon Conquest.
+
+    The main four base stats in Conquest are derived from level 100 stats in
+    the main series (ignoring effort, genes, and natures).  Attack matches
+    either Attack or Special Attack, and Defense matches the average of Defense
+    and Special Defense.  HP and Speed are the same.
+    """
+    __tablename__ = 'conquest_pokemon_stats'
+    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the Pokémon species.')
+    conquest_stat_id = Column(Integer, ForeignKey('conquest_stats.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the stat.')
+    base_stat = Column(Integer, nullable=False,
+        doc=u'The base stat.')
+
+class ConquestStat(TableBase):
+    u"""A stat Pokémon have in Pokémon Conquest."""
+    __tablename__ = 'conquest_stats'
+    __singlename__ = 'conquest_stat'  # To be safe
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this stat.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this stat.',
+        info=dict(format='identifier'))
+    is_base = Column(Boolean, nullable=False,
+        doc=u'True iff this is one of the main stats, calculated for individual Pokémon.')
+
+create_translation_table('conquest_stat_names', ConquestStat, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestTransformationPokemon(TableBase):
+    u"""A Pokémon that satisfies a warrior transformation's link condition.
+
+    If a warrior has one or more Pokémon listed here, they only need to raise
+    one of them to the required link.
+    """
+    __tablename__ = 'conquest_transformation_pokemon'
+    transformation_id = Column(Integer, ForeignKey('conquest_warrior_transformation.transformed_warrior_rank_id'), primary_key=True,
+        doc=u'The ID of the corresponding transformation, in turn a warrior rank ID.')
+    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True,
+        doc=u'The ID of the Pokémon species.')
+
+class ConquestTransformationWarrior(TableBase):
+    u"""A warrior who must be present in the same nation as another warrior for
+    the latter to transform into their next rank.
+
+    If a warrior has one or more other warriors listed here, they *all* need to
+    gather in the same nation for the transformation to take place.
+    """
+    __tablename__ = 'conquest_transformation_warriors'
+    transformation_id = Column(Integer, ForeignKey('conquest_warrior_transformation.transformed_warrior_rank_id'), primary_key=True,
+        doc=u'The ID of the corresponding transformation, in turn a warrior rank ID.')
+    present_warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True,
+        doc=u'The ID of the other warrior who must be present.')
+
+class ConquestWarrior(TableBase):
+    u"""A warrior in Pokémon Conquest."""
+    __tablename__ = 'conquest_warriors'
+    __singlename__ = 'warrior'
+    id = Column(Integer, primary_key=True, nullable=False, autoincrement=True,
+        doc=u'An ID for this warrior.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this warrior.',
+        info=dict(format='identifier'))
+    gender_id = Column(Integer, ForeignKey('genders.id'), nullable=False,
+        doc=u"The ID of the warrior's gender.")
+    archetype_id = Column(Integer, ForeignKey('conquest_warrior_archetypes.id'), nullable=True,
+        doc=u"The ID of this warrior's archetype.  Null for unique warriors.")
+
+create_translation_table('conquest_warrior_names', ConquestWarrior, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestWarriorArchetype(TableBase):
+    u"""An archetype that generic warriors in Pokémon Conquest can have.  All
+    warriors of a particular archetype share sprites and dialogue.
+
+    Some of these are unused as warriors because they exist only as NPCs.  They
+    should still be kept because we have their sprites and may eventually get
+    their dialogue.
+    """
+    __tablename__ = 'conquest_warrior_archetypes'
+    __singlename__ = 'archetype'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this archetype.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier describing this archetype.',
+        info=dict(format='identifier'))
+
+class ConquestWarriorRank(TableBase):
+    u"""A warrior at a particular rank in Pokémon Conquest.
+
+    These are used for whatever changes between ranks, much like Pokémon forms.
+    Generic warriors who have only one rank are also represented here, with a
+    single row.
+
+    To clarify, each warrior's ranks are individually called "warrior ranks"
+    here; for example, "Rank 2 Nobunaga" is an example of a warrior rank, not
+    just "Rank 2".
+    """
+    __tablename__ = 'conquest_warrior_ranks'
+    __singlename__ = 'warrior_rank'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this warrior rank.')
+    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), nullable=False,
+        doc=u'The ID of the warrior.')
+    rank = Column(Integer, nullable=False,
+        doc=u'The rank number.')
+    skill_id = Column(Integer, ForeignKey('conquest_warrior_skills.id'), nullable=False,
+        doc=u"The ID of this warrior rank's warrior skill.")
+
+    __table_args__ = (
+        UniqueConstraint(warrior_id, rank),
+        {},
+    )
+
+class ConquestWarriorRankStatMap(TableBase):
+    u"""Any of a warrior rank's warrior stats in Pokémon Conquest."""
+    __tablename__ = 'conquest_warrior_rank_stat_map'
+    warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the warrior rank.')
+    warrior_stat_id = Column(Integer, ForeignKey('conquest_warrior_stats.id'), primary_key=True, autoincrement=False,
+        doc=u'The ID of the warrior stat.')
+    base_stat = Column(Integer, nullable=False,
+        doc=u'The stat.')
+
+class ConquestWarriorSkill(TableBase):
+    u"""A warrior skill in Pokémon Conquest."""
+    __tablename__ = 'conquest_warrior_skills'
+    __singlename__ = 'skill'
+    id = Column(Integer, primary_key=True, nullable=False, autoincrement=True,
+        doc=u'An ID for this skill.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this skill.',
+        info=dict(format='identifier'))
+
+create_translation_table('conquest_warrior_skill_names', ConquestWarriorSkill, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestWarriorSpecialty(TableBase):
+    u"""A warrior's specialty types in Pokémon Conquest.
+
+    These have no actual effect on gameplay; they just indicate which types of
+    Pokémon each warrior generally has strong maximum links with.
+    """
+    __tablename__ = 'conquest_warrior_specialties'
+    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True, nullable=False, autoincrement=False,
+        doc=u'The ID of the warrior.')
+    type_id = Column(Integer, ForeignKey('types.id'), primary_key=True, nullable=False, autoincrement=False,
+        doc=u'The ID of the type.')
+    slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
+        doc=u"The order in which the warrior's types are listed.")
+
+class ConquestWarriorStat(TableBase):
+    u"""A stat that warriors have in Pokémon Conquest."""
+    __tablename__ = 'conquest_warrior_stats'
+    __singlename__ = 'warrior_stat'
+    id = Column(Integer, primary_key=True, autoincrement=True,
+        doc=u'An ID for this stat.')
+    identifier = Column(Unicode(79), nullable=False,
+        doc=u'A readable identifier for this stat.',
+        info=dict(format='identifier'))
+
+create_translation_table('conquest_warrior_stat_names', ConquestWarriorStat, 'names',
+    relation_lazy='joined',
+    name=Column(Unicode(79), nullable=False, index=True,
+        doc=u'The name.',
+        info=dict(format='plaintext', official=True))
+)
+
+class ConquestWarriorTransformation(TableBase):
+    u"""The conditions under which a warrior must perform an action in order
+    to transform to the next rank.
+
+    Or most of them, anyway.  See also ConquestTransformationPokemon and
+    ConquestTransformationWarrior.
+    """
+    __tablename__ = 'conquest_warrior_transformation'
+    transformed_warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True,
+        doc=u'The ID of the post-transformation warrior rank.')
+    is_automatic = Column(Boolean, nullable=False,
+        doc=u'True iff the transformation happens automatically in the story with no further requirements.')
+    required_link = Column(Integer, nullable=True,
+        doc=u'The link percentage the warrior must reach with one of several specific Pokémon, if any.')
+    completed_episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), nullable=True,
+        doc=u'The ID of the episode the player must have completed, if any.')
+    current_episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), nullable=True,
+        doc=u'The ID of the episode the player must currently be playing, if any.')
+    distant_warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), nullable=True,
+        doc=u'The ID of another warrior who must be in the army, but not in the same kingdom or in any adjacent kingdom.')
+    female_warlord_count = Column(Integer, nullable=True,
+        doc=u'The number of female warlords who must be in the same nation.')
+    pokemon_count = Column(Integer, nullable=True,
+        doc=u'The number of Pokémon that must be registered in the gallery.')
+    collection_type_id = Column(Integer, ForeignKey('types.id'), nullable=True,
+        doc=u'The ID of a type all Pokémon of which must be registered in the gallery.')
+    warrior_count = Column(Integer, nullable=True,
+        doc=u'The number of warriors that must be registered in the gallery.')
+
+
+### Relationships down here, to avoid dependency ordering problems
+
+ConquestEpisode.warriors = relationship(ConquestWarrior,
+    secondary=ConquestEpisodeWarrior.__table__,
+    innerjoin=True,
+    backref='episodes')
+
+ConquestKingdom.type = relationship(Type,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('conquest_kingdom', uselist=False))
+
+ConquestMaxLink.pokemon = relationship(PokemonSpecies,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('conquest_max_links', lazy='dynamic',
+                    order_by=ConquestMaxLink.warrior_rank_id))
+ConquestMaxLink.warrior_rank = relationship(ConquestWarriorRank,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('max_links', lazy='dynamic'))
+ConquestMaxLink.warrior = association_proxy('warrior_rank', 'warrior')
+
+ConquestMoveData.move_displacement = relationship(ConquestMoveDisplacement,
+    uselist=False,
+    backref='move_data')
+ConquestMoveData.move = relationship(Move,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('conquest_data', uselist=False))
+ConquestMoveData.move_effect = relationship(ConquestMoveEffect,
+    innerjoin=True, lazy='joined',
+    backref='move_data')
+ConquestMoveData.range = relationship(ConquestMoveRange,
+    innerjoin=True, lazy='joined',
+    backref='move_data')
+
+ConquestMoveData.effect = markdown.MoveEffectProperty('effect')
+ConquestMoveData.effect_map = markdown.MoveEffectPropertyMap('effect_map')
+ConquestMoveData.short_effect = markdown.MoveEffectProperty('short_effect')
+ConquestMoveData.short_effect_map = markdown.MoveEffectPropertyMap('short_effect_map')
+ConquestMoveData.displacement = markdown.MoveEffectProperty('effect', relationship='move_displacement')
+
+ConquestPokemonEvolution.gender = relationship(Gender,
+    backref='conquest_evolutions')
+ConquestPokemonEvolution.item = relationship(Item,
+    backref='conquest_evolutions')
+ConquestPokemonEvolution.kingdom = relationship(ConquestKingdom,
+    backref='evolutions')
+ConquestPokemonEvolution.stat = relationship(ConquestStat,
+    backref='evolutions')
+
+ConquestPokemonStat.pokemon = relationship(PokemonSpecies,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref='conquest_stats')
+ConquestPokemonStat.stat = relationship(ConquestStat,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref='pokemon_stats')
+
+ConquestWarrior.archetype = relationship(ConquestWarriorArchetype,
+    uselist=False,
+    backref=backref('warriors'))
+ConquestWarrior.ranks = relationship(ConquestWarriorRank,
+    order_by=ConquestWarriorRank.rank,
+    innerjoin=True,
+    backref=backref('warrior', uselist=False))
+ConquestWarrior.types = relationship(Type,
+    secondary=ConquestWarriorSpecialty.__table__,
+    order_by=ConquestWarriorSpecialty.slot,
+    innerjoin=True,
+    backref='conquest_warriors')
+
+ConquestWarriorRank.skill = relationship(ConquestWarriorSkill,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('warrior_ranks', order_by=ConquestWarriorRank.id))
+ConquestWarriorRank.stats = relationship(ConquestWarriorRankStatMap,
+    innerjoin=True,
+    order_by=ConquestWarriorRankStatMap.warrior_stat_id,
+    backref=backref('warrior_rank', uselist=False, innerjoin=True, lazy='joined'))
+
+ConquestWarriorRankStatMap.stat = relationship(ConquestWarriorStat,
+    innerjoin=True, lazy='joined',
+    uselist=False,
+    backref='stat_map')
+
+ConquestWarriorTransformation.completed_episode = relationship(ConquestEpisode,
+    primaryjoin=ConquestWarriorTransformation.completed_episode_id==ConquestEpisode.id,
+    uselist=False)
+ConquestWarriorTransformation.current_episode = relationship(ConquestEpisode,
+    primaryjoin=ConquestWarriorTransformation.current_episode_id==ConquestEpisode.id,
+    uselist=False)
+ConquestWarriorTransformation.distant_warrior = relationship(ConquestWarrior,
+    uselist=False)
+ConquestWarriorTransformation.pokemon = relationship(PokemonSpecies,
+    secondary=ConquestTransformationPokemon.__table__,
+    order_by=PokemonSpecies.conquest_order)
+ConquestWarriorTransformation.present_warriors = relationship(ConquestWarrior,
+    secondary=ConquestTransformationWarrior.__table__,
+    order_by=ConquestWarrior.id)
+ConquestWarriorTransformation.type = relationship(Type,
+    uselist=False)
+ConquestWarriorTransformation.warrior_rank = relationship(ConquestWarriorRank,
+    uselist=False,
+    innerjoin=True, lazy='joined',
+    backref=backref('transformation', uselist=False, innerjoin=True))
+
+
+PokemonSpecies.conquest_abilities = relationship(Ability,
+    secondary=ConquestPokemonAbility.__table__,
+    order_by=ConquestPokemonAbility.slot,
+    backref=backref('conquest_pokemon', order_by=PokemonSpecies.conquest_order,
+                    innerjoin=True))
+PokemonSpecies.conquest_move = relationship(Move,
+    secondary=ConquestPokemonMove.__table__,
+    uselist=False,
+    backref=backref('conquest_pokemon', order_by=PokemonSpecies.conquest_order))
+PokemonSpecies.conquest_evolution = relationship(ConquestPokemonEvolution,
+    uselist=False,
+    backref=backref('evolved_species', innerjoin=True, lazy='joined', uselist=False))
diff --git a/pokedex/db/tables.py b/pokedex/db/tables/core.py
similarity index 77%
rename from pokedex/db/tables.py
rename to pokedex/db/tables/core.py
index 447b6a5..ff1b8d8 100644
--- a/pokedex/db/tables.py
+++ b/pokedex/db/tables/core.py
@@ -1,117 +1,15 @@
 # encoding: utf8
 
-u"""The Pokédex schema.
-
-Columns have a info dictionary with these keys:
-- official: True if the values appear in games or official material; False if
-  they are fan-created or fan-written. This flag is currently only set for
-  official text columns.
-- format: The format of a text column. Can be one of:
-  - plaintext: Normal Unicode text (widely used in names)
-  - markdown: Veekun's Markdown flavor (generally used in effect descriptions)
-  - gametext: Transcription of in-game text that strives to be both
-    human-readable and represent the original text exactly.
-  - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
-    for translation.
-  - latex: A formula in LaTeX syntax.
-- ripped: True for text that has been ripped from the games, and can be ripped
-  again for new versions or languages
-
-- string_getter: for translation columns, a function taking (text, session,
-  language) that is used for properties on the main table. Used for Markdown
-  text.
-
-See `pokedex.db.multilang` for how localizable text columns work.  The session
-classes in that module can be used to change the default language.
-"""
-# XXX: Check if "gametext" is set correctly everywhere
-
-import collections
-from functools import partial
-
-from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table, UniqueConstraint
-from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
+from sqlalchemy import Column, ForeignKey, PrimaryKeyConstraint, UniqueConstraint
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.hybrid import hybrid_property
 from sqlalchemy.orm import backref, relationship
-from sqlalchemy.orm.session import Session
-from sqlalchemy.orm.interfaces import AttributeExtension
-from sqlalchemy.sql import and_, or_
-from sqlalchemy.schema import ColumnDefault
+from sqlalchemy.sql import and_
 from sqlalchemy.types import Boolean, Enum, Integer, SmallInteger, Unicode, UnicodeText
 
-from pokedex.db import markdown, multilang
+from pokedex.db import markdown
+from pokedex.db.tables.base import TableBase, Language, create_translation_table
 
-class TableSuperclass(object):
-    """Superclass for declarative tables, to give them some generic niceties
-    like stringification.
-    """
-    def __unicode__(self):
-        """Be as useful as possible.  Show the primary key, and an identifier
-        if we've got one.
-        """
-        typename = u'.'.join((__name__, type(self).__name__))
-
-        pk_constraint = self.__table__.primary_key
-        if not pk_constraint:
-            return u"<%s object at %x>" % (typename, id(self))
-
-        pk = u', '.join(unicode(getattr(self, column.name))
-            for column in pk_constraint.columns)
-        try:
-            return u"<%s object (%s): %s>" % (typename, pk, self.identifier)
-        except AttributeError:
-            return u"<%s object (%s)>" % (typename, pk)
-
-    def __str__(self):
-        return unicode(self).encode('utf8')
-
-    def __repr__(self):
-        return unicode(self).encode('utf8')
-
-mapped_classes = []
-class TableMetaclass(DeclarativeMeta):
-    def __init__(cls, name, bases, attrs):
-        super(TableMetaclass, cls).__init__(name, bases, attrs)
-        if hasattr(cls, '__tablename__'):
-            mapped_classes.append(cls)
-            cls.translation_classes = []
-
-metadata = MetaData()
-TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
-
-
-### Need Language first, to create the partial() below
-
-class Language(TableBase):
-    u"""A language the Pokémon games have been translated into."""
-    __tablename__ = 'languages'
-    __singlename__ = 'language'
-    id = Column(Integer, primary_key=True, nullable=False,
-        doc=u"A numeric ID")
-    iso639 = Column(Unicode(79), nullable=False,
-        doc=u"The two-letter code of the country where this language is spoken. Note that it is not unique.",
-        info=dict(format='identifier'))
-    iso3166 = Column(Unicode(79), nullable=False,
-        doc=u"The two-letter code of the language. Note that it is not unique.",
-        info=dict(format='identifier'))
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u"An identifier",
-        info=dict(format='identifier'))
-    official = Column(Boolean, nullable=False, index=True,
-        doc=u"True iff games are produced in the language.")
-    order = Column(Integer, nullable=True,
-        doc=u"Order for sorting in foreign name lists.")
-
-create_translation_table = partial(multilang.create_translation_table, language_class=Language)
-
-create_translation_table('language_names', Language, 'names',
-    name = Column(Unicode(79), nullable=False, index=True,
-        doc=u"The name",
-        info=dict(format='plaintext', official=True)),
-)
-
-### The actual tables
 
 class Ability(TableBase):
     u"""An ability a Pokémon can have, such as Static or Pressure."""
@@ -244,420 +142,6 @@ create_translation_table('characteristic_text', Characteristic, 'text',
         info=dict(official=True, format='plaintext')),
 )
 
-class ConquestEpisode(TableBase):
-    u"""An episode from Pokémon Conquest: one of a bunch of mini-stories
-    featuring a particular warrior.
-
-    The main story, "The Legend of Ransei", also counts, even though it's not
-    in the episode select menu and there's no way to replay it.
-    """
-    __tablename__ = 'conquest_episodes'
-    __singlename__ = 'episode'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this episode.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this episode.',
-        info=dict(format='identifier'))
-
-create_translation_table('conquest_episode_names', ConquestEpisode, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestEpisodeWarrior(TableBase):
-    u"""A warrior featured in an episode in Pokémon Conquest.
-
-    This needs its own table because of the player having two episodes and
-    there being two players.
-    """
-    __tablename__ = 'conquest_episode_warriors'
-    episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), primary_key=True,
-        doc=u'The ID of the episode.')
-    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True,
-        doc=u'The ID of the warrior.')
-
-class ConquestKingdom(TableBase):
-    u"""A kingdom in Pokémon Conquest."""
-    __tablename__ = 'conquest_kingdoms'
-    __singlename__ = 'kingdom'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u"An ID for this kingdom.")
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u"A readable identifier for this kingdom.",
-        info=dict(format='identifier'))
-    type_id = Column(Integer, ForeignKey('types.id'), nullable=False,
-        doc=u"The type associated with this kingdom in-game.")
-
-create_translation_table('conquest_kingdom_names', ConquestKingdom, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestMaxLink(TableBase):
-    u"""The maximum link a warrior rank can reach with a Pokémon in Pokémon Conquest."""
-    __tablename__ = 'conquest_max_links'
-    warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True,
-        doc=u"The ID of the warrior rank.")
-    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True,
-        doc=u'The ID of the Pokémon species.')
-    max_link = Column(Integer, nullable=False,
-        doc=u'The maximum link percentage this warrior rank and Pokémon can reach.')
-
-class ConquestMoveData(TableBase):
-    u"""Data about a move in Pokémon Conquest."""
-    __tablename__ = 'conquest_move_data'
-    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the move.')
-    power = Column(Integer, nullable=True,
-        doc=u"The move's power, null if it does no damage.")
-    accuracy = Column(Integer, nullable=True,
-        doc=u"The move's base accuracy, null if it is self-targeted or never misses.")
-    effect_chance = Column(Integer, nullable=True,
-        doc=u"The chance as a percentage that the move's secondary effect will trigger.")
-    effect_id = Column(Integer, ForeignKey('conquest_move_effects.id'), nullable=False,
-        doc=u"The ID of the move's effect.")
-    range_id = Column(Integer, ForeignKey('conquest_move_ranges.id'), nullable=False,
-        doc=u"The ID of the move's range.")
-    displacement_id = Column(Integer, ForeignKey('conquest_move_displacements.id'), nullable=True,
-        doc=u"The ID of the move's displacement.")
-
-    @property
-    def star_rating(self):
-        """Return the move's in-game power rating as a number of stars."""
-        if not self.power:
-            return 0
-        else:
-            stars = (self.power - 1) // 10
-            stars = min(stars, 5)  # i.e. maximum of 5 stars
-            stars = max(stars, 1)  # And minimum of 1
-            return stars
-
-class ConquestMoveDisplacement(TableBase):
-    u"""A way in which a move can cause the user or target to move to a
-    different tile.
-
-    If a move displaces its user, the move's range is relative to the user's
-    original position.
-    """
-    __tablename__ = 'conquest_move_displacements'
-    __singlename__ = 'move_displacement'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this displacement.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this displacement.',
-        info=dict(format='identifier'))
-    affects_target = Column(Boolean, nullable=False,
-        doc=u'True iff the move displaces its target(s) and not its user.')
-
-create_translation_table('conquest_move_displacement_prose', ConquestMoveDisplacement, 'prose',
-    name = Column(Unicode(79), nullable=True,
-        doc=u'A name for the displacement.',
-        info=dict(format='plaintext')),
-    short_effect = Column(UnicodeText, nullable=True,
-        doc=u"A short summary of how the displacement works, to be used in the move's short effect.",
-        info=dict(format='markdown')),
-    effect = Column(UnicodeText, nullable=True,
-        doc=u"A detailed description of how the displacement works, to be used alongside the move's long effect.",
-        info=dict(format='markdown')),
-)
-
-class ConquestMoveEffect(TableBase):
-    u"""An effect moves can have in Pokémon Conquest."""
-    __tablename__ = 'conquest_move_effects'
-    __singlename__ = 'conquest_move_effect'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this effect.')
-
-create_translation_table('conquest_move_effect_prose', ConquestMoveEffect, 'prose',
-    short_effect = Column(UnicodeText, nullable=True,
-        doc=u"A short summary of the effect",
-        info=dict(format='markdown')),
-    effect = Column(UnicodeText, nullable=True,
-        doc=u"A detailed description of the effect",
-        info=dict(format='markdown')),
-)
-
-class ConquestMoveRange(TableBase):
-    u"""A set of tiles moves can target in Pokémon Conquest."""
-    __tablename__ = 'conquest_move_ranges'
-    __singlename__ = 'conquest_move_range'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this range.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this range.',
-        info=dict(format='identifier'))
-    targets = Column(Integer, nullable=False,
-        doc=u'The number of tiles this range targets.')
-
-create_translation_table('conquest_move_range_prose', ConquestMoveRange, 'prose',
-    name = Column(Unicode(79), nullable=True,
-        doc=u"A short name briefly describing the range",
-        info=dict(format='plaintext')),
-    description = Column(UnicodeText, nullable=True,
-        doc=u"A detailed description of the range",
-        info=dict(format='plaintext')),
-)
-
-class ConquestPokemonAbility(TableBase):
-    u"""An ability a Pokémon species has in Pokémon Conquest."""
-    __tablename__ = 'conquest_pokemon_abilities'
-    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, nullable=False, autoincrement=False,
-        doc=u'The ID of the Pokémon species with this ability.')
-    slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
-        doc=u"The order abilities are listed in.  Upon evolution, if a Pokémon's abilities change, it will receive the one in the same slot.")
-    ability_id = Column(Integer, ForeignKey('abilities.id'), nullable=False,
-        doc=u'The ID of the ability.')
-
-class ConquestPokemonEvolution(TableBase):
-    u"""The conditions under which a Pokémon must successfully complete an
-    action to evolve in Pokémon Conquest.
-
-    Any condition may be null if it does not apply for a particular Pokémon.
-    """
-    __tablename__ = 'conquest_pokemon_evolution'
-    evolved_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, nullable=False,
-        doc=u"The ID of the post-evolution species.")
-    required_stat_id = Column(Integer, ForeignKey('conquest_stats.id'), nullable=True,
-        doc=u"The ID of the stat which minimum_stat applies to.")
-    minimum_stat = Column(Integer, nullable=True,
-        doc=u"The minimum value the Pokémon must have in a particular stat.")
-    minimum_link = Column(Integer, nullable=True,
-        doc=u"The minimum link percentage the Pokémon must have with its warrior.")
-    kingdom_id = Column(Integer, ForeignKey('conquest_kingdoms.id'), nullable=True,
-        doc=u"The ID of the kingdom in which this Pokémon must complete an action after meeting all other requirements.")
-    warrior_gender_id = Column(Integer, ForeignKey('genders.id'), nullable=True,
-        doc=u"The ID of the gender the Pokémon's warrior must be.")
-    item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
-        doc=u"The ID of the item the Pokémon's warrior must have equipped.")
-    recruiting_ko_required = Column(Boolean, nullable=False,
-        doc=u"If true, the Pokémon must KO a Pokémon under the right conditions to recruit that Pokémon's warrior.")
-
-class ConquestPokemonMove(TableBase):
-    u"""A Pokémon's move in Pokémon Conquest.
-
-    Yes, "move"; each Pokémon has exactly one.
-    """
-    __tablename__ = 'conquest_pokemon_moves'
-    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the Pokémon species.')
-    move_id = Column(Integer, ForeignKey('moves.id'), nullable=False,
-        doc=u'The ID of the move.')
-
-class ConquestPokemonStat(TableBase):
-    u"""A Pokémon's base stat in Pokémon Conquest.
-
-    The main four base stats in Conquest are derived from level 100 stats in
-    the main series (ignoring effort, genes, and natures).  Attack matches
-    either Attack or Special Attack, and Defense matches the average of Defense
-    and Special Defense.  HP and Speed are the same.
-    """
-    __tablename__ = 'conquest_pokemon_stats'
-    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the Pokémon species.')
-    conquest_stat_id = Column(Integer, ForeignKey('conquest_stats.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the stat.')
-    base_stat = Column(Integer, nullable=False,
-        doc=u'The base stat.')
-
-class ConquestStat(TableBase):
-    u"""A stat Pokémon have in Pokémon Conquest."""
-    __tablename__ = 'conquest_stats'
-    __singlename__ = 'conquest_stat'  # To be safe
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this stat.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this stat.',
-        info=dict(format='identifier'))
-    is_base = Column(Boolean, nullable=False,
-        doc=u'True iff this is one of the main stats, calculated for individual Pokémon.')
-
-create_translation_table('conquest_stat_names', ConquestStat, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestTransformationPokemon(TableBase):
-    u"""A Pokémon that satisfies a warrior transformation's link condition.
-
-    If a warrior has one or more Pokémon listed here, they only need to raise
-    one of them to the required link.
-    """
-    __tablename__ = 'conquest_transformation_pokemon'
-    transformation_id = Column(Integer, ForeignKey('conquest_warrior_transformation.transformed_warrior_rank_id'), primary_key=True,
-        doc=u'The ID of the corresponding transformation, in turn a warrior rank ID.')
-    pokemon_species_id = Column(Integer, ForeignKey('pokemon_species.id'), primary_key=True,
-        doc=u'The ID of the Pokémon species.')
-
-class ConquestTransformationWarrior(TableBase):
-    u"""A warrior who must be present in the same nation as another warrior for
-    the latter to transform into their next rank.
-
-    If a warrior has one or more other warriors listed here, they *all* need to
-    gather in the same nation for the transformation to take place.
-    """
-    __tablename__ = 'conquest_transformation_warriors'
-    transformation_id = Column(Integer, ForeignKey('conquest_warrior_transformation.transformed_warrior_rank_id'), primary_key=True,
-        doc=u'The ID of the corresponding transformation, in turn a warrior rank ID.')
-    present_warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True,
-        doc=u'The ID of the other warrior who must be present.')
-
-class ConquestWarrior(TableBase):
-    u"""A warrior in Pokémon Conquest."""
-    __tablename__ = 'conquest_warriors'
-    __singlename__ = 'warrior'
-    id = Column(Integer, primary_key=True, nullable=False, autoincrement=True,
-        doc=u'An ID for this warrior.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this warrior.',
-        info=dict(format='identifier'))
-    gender_id = Column(Integer, ForeignKey('genders.id'), nullable=False,
-        doc=u"The ID of the warrior's gender.")
-    archetype_id = Column(Integer, ForeignKey('conquest_warrior_archetypes.id'), nullable=True,
-        doc=u"The ID of this warrior's archetype.  Null for unique warriors.")
-
-create_translation_table('conquest_warrior_names', ConquestWarrior, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestWarriorArchetype(TableBase):
-    u"""An archetype that generic warriors in Pokémon Conquest can have.  All
-    warriors of a particular archetype share sprites and dialogue.
-
-    Some of these are unused as warriors because they exist only as NPCs.  They
-    should still be kept because we have their sprites and may eventually get
-    their dialogue.
-    """
-    __tablename__ = 'conquest_warrior_archetypes'
-    __singlename__ = 'archetype'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this archetype.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier describing this archetype.',
-        info=dict(format='identifier'))
-
-class ConquestWarriorRank(TableBase):
-    u"""A warrior at a particular rank in Pokémon Conquest.
-
-    These are used for whatever changes between ranks, much like Pokémon forms.
-    Generic warriors who have only one rank are also represented here, with a
-    single row.
-
-    To clarify, each warrior's ranks are individually called "warrior ranks"
-    here; for example, "Rank 2 Nobunaga" is an example of a warrior rank, not
-    just "Rank 2".
-    """
-    __tablename__ = 'conquest_warrior_ranks'
-    __singlename__ = 'warrior_rank'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this warrior rank.')
-    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), nullable=False,
-        doc=u'The ID of the warrior.')
-    rank = Column(Integer, nullable=False,
-        doc=u'The rank number.')
-    skill_id = Column(Integer, ForeignKey('conquest_warrior_skills.id'), nullable=False,
-        doc=u"The ID of this warrior rank's warrior skill.")
-
-    __table_args__ = (
-        UniqueConstraint(warrior_id, rank),
-        {},
-    )
-
-class ConquestWarriorRankStatMap(TableBase):
-    u"""Any of a warrior rank's warrior stats in Pokémon Conquest."""
-    __tablename__ = 'conquest_warrior_rank_stat_map'
-    warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the warrior rank.')
-    warrior_stat_id = Column(Integer, ForeignKey('conquest_warrior_stats.id'), primary_key=True, autoincrement=False,
-        doc=u'The ID of the warrior stat.')
-    base_stat = Column(Integer, nullable=False,
-        doc=u'The stat.')
-
-class ConquestWarriorSkill(TableBase):
-    u"""A warrior skill in Pokémon Conquest."""
-    __tablename__ = 'conquest_warrior_skills'
-    __singlename__ = 'skill'
-    id = Column(Integer, primary_key=True, nullable=False, autoincrement=True,
-        doc=u'An ID for this skill.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this skill.',
-        info=dict(format='identifier'))
-
-create_translation_table('conquest_warrior_skill_names', ConquestWarriorSkill, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestWarriorSpecialty(TableBase):
-    u"""A warrior's specialty types in Pokémon Conquest.
-
-    These have no actual effect on gameplay; they just indicate which types of
-    Pokémon each warrior generally has strong maximum links with.
-    """
-    __tablename__ = 'conquest_warrior_specialties'
-    warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), primary_key=True, nullable=False, autoincrement=False,
-        doc=u'The ID of the warrior.')
-    type_id = Column(Integer, ForeignKey('types.id'), primary_key=True, nullable=False, autoincrement=False,
-        doc=u'The ID of the type.')
-    slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
-        doc=u"The order in which the warrior's types are listed.")
-
-class ConquestWarriorStat(TableBase):
-    u"""A stat that warriors have in Pokémon Conquest."""
-    __tablename__ = 'conquest_warrior_stats'
-    __singlename__ = 'warrior_stat'
-    id = Column(Integer, primary_key=True, autoincrement=True,
-        doc=u'An ID for this stat.')
-    identifier = Column(Unicode(79), nullable=False,
-        doc=u'A readable identifier for this stat.',
-        info=dict(format='identifier'))
-
-create_translation_table('conquest_warrior_stat_names', ConquestWarriorStat, 'names',
-    relation_lazy='joined',
-    name=Column(Unicode(79), nullable=False, index=True,
-        doc=u'The name.',
-        info=dict(format='plaintext', official=True))
-)
-
-class ConquestWarriorTransformation(TableBase):
-    u"""The conditions under which a warrior must perform an action in order
-    to transform to the next rank.
-
-    Or most of them, anyway.  See also ConquestTransformationPokemon and
-    ConquestTransformationWarrior.
-    """
-    __tablename__ = 'conquest_warrior_transformation'
-    transformed_warrior_rank_id = Column(Integer, ForeignKey('conquest_warrior_ranks.id'), primary_key=True,
-        doc=u'The ID of the post-transformation warrior rank.')
-    is_automatic = Column(Boolean, nullable=False,
-        doc=u'True iff the transformation happens automatically in the story with no further requirements.')
-    required_link = Column(Integer, nullable=True,
-        doc=u'The link percentage the warrior must reach with one of several specific Pokémon, if any.')
-    completed_episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), nullable=True,
-        doc=u'The ID of the episode the player must have completed, if any.')
-    current_episode_id = Column(Integer, ForeignKey('conquest_episodes.id'), nullable=True,
-        doc=u'The ID of the episode the player must currently be playing, if any.')
-    distant_warrior_id = Column(Integer, ForeignKey('conquest_warriors.id'), nullable=True,
-        doc=u'The ID of another warrior who must be in the army, but not in the same kingdom or in any adjacent kingdom.')
-    female_warlord_count = Column(Integer, nullable=True,
-        doc=u'The number of female warlords who must be in the same nation.')
-    pokemon_count = Column(Integer, nullable=True,
-        doc=u'The number of Pokémon that must be registered in the gallery.')
-    collection_type_id = Column(Integer, ForeignKey('types.id'), nullable=True,
-        doc=u'The ID of a type all Pokémon of which must be registered in the gallery.')
-    warrior_count = Column(Integer, nullable=True,
-        doc=u'The number of warriors that must be registered in the gallery.')
-
 class ContestCombo(TableBase):
     u"""Combo of two moves in a Contest."""
     __tablename__ = 'contest_combos'
@@ -2257,113 +1741,6 @@ Characteristic.stat = relationship(Stat,
     backref='characteristics')
 
 
-ConquestEpisode.warriors = relationship(ConquestWarrior,
-    secondary=ConquestEpisodeWarrior.__table__,
-    innerjoin=True,
-    backref='episodes')
-
-ConquestKingdom.type = relationship(Type,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('conquest_kingdom', uselist=False))
-
-ConquestMaxLink.pokemon = relationship(PokemonSpecies,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('conquest_max_links', lazy='dynamic',
-                    order_by=ConquestMaxLink.warrior_rank_id))
-ConquestMaxLink.warrior_rank = relationship(ConquestWarriorRank,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('max_links', lazy='dynamic'))
-ConquestMaxLink.warrior = association_proxy('warrior_rank', 'warrior')
-
-ConquestMoveData.move_displacement = relationship(ConquestMoveDisplacement,
-    uselist=False,
-    backref='move_data')
-ConquestMoveData.move = relationship(Move,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('conquest_data', uselist=False))
-ConquestMoveData.move_effect = relationship(ConquestMoveEffect,
-    innerjoin=True, lazy='joined',
-    backref='move_data')
-ConquestMoveData.range = relationship(ConquestMoveRange,
-    innerjoin=True, lazy='joined',
-    backref='move_data')
-
-ConquestMoveData.effect = markdown.MoveEffectProperty('effect')
-ConquestMoveData.effect_map = markdown.MoveEffectPropertyMap('effect_map')
-ConquestMoveData.short_effect = markdown.MoveEffectProperty('short_effect')
-ConquestMoveData.short_effect_map = markdown.MoveEffectPropertyMap('short_effect_map')
-ConquestMoveData.displacement = markdown.MoveEffectProperty('effect', relationship='move_displacement')
-
-ConquestPokemonEvolution.gender = relationship(Gender,
-    backref='conquest_evolutions')
-ConquestPokemonEvolution.item = relationship(Item,
-    backref='conquest_evolutions')
-ConquestPokemonEvolution.kingdom = relationship(ConquestKingdom,
-    backref='evolutions')
-ConquestPokemonEvolution.stat = relationship(ConquestStat,
-    backref='evolutions')
-
-ConquestPokemonStat.pokemon = relationship(PokemonSpecies,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref='conquest_stats')
-ConquestPokemonStat.stat = relationship(ConquestStat,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref='pokemon_stats')
-
-ConquestWarrior.archetype = relationship(ConquestWarriorArchetype,
-    uselist=False,
-    backref=backref('warriors'))
-ConquestWarrior.ranks = relationship(ConquestWarriorRank,
-    order_by=ConquestWarriorRank.rank,
-    innerjoin=True,
-    backref=backref('warrior', uselist=False))
-ConquestWarrior.types = relationship(Type,
-    secondary=ConquestWarriorSpecialty.__table__,
-    order_by=ConquestWarriorSpecialty.slot,
-    innerjoin=True,
-    backref='conquest_warriors')
-
-ConquestWarriorRank.skill = relationship(ConquestWarriorSkill,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('warrior_ranks', order_by=ConquestWarriorRank.id))
-ConquestWarriorRank.stats = relationship(ConquestWarriorRankStatMap,
-    innerjoin=True,
-    order_by=ConquestWarriorRankStatMap.warrior_stat_id,
-    backref=backref('warrior_rank', uselist=False, innerjoin=True, lazy='joined'))
-
-ConquestWarriorRankStatMap.stat = relationship(ConquestWarriorStat,
-    innerjoin=True, lazy='joined',
-    uselist=False,
-    backref='stat_map')
-
-ConquestWarriorTransformation.completed_episode = relationship(ConquestEpisode,
-    primaryjoin=ConquestWarriorTransformation.completed_episode_id==ConquestEpisode.id,
-    uselist=False)
-ConquestWarriorTransformation.current_episode = relationship(ConquestEpisode,
-    primaryjoin=ConquestWarriorTransformation.current_episode_id==ConquestEpisode.id,
-    uselist=False)
-ConquestWarriorTransformation.distant_warrior = relationship(ConquestWarrior,
-    uselist=False)
-ConquestWarriorTransformation.pokemon = relationship(PokemonSpecies,
-    secondary=ConquestTransformationPokemon.__table__,
-    order_by=PokemonSpecies.conquest_order)
-ConquestWarriorTransformation.present_warriors = relationship(ConquestWarrior,
-    secondary=ConquestTransformationWarrior.__table__,
-    order_by=ConquestWarrior.id)
-ConquestWarriorTransformation.type = relationship(Type,
-    uselist=False)
-ConquestWarriorTransformation.warrior_rank = relationship(ConquestWarriorRank,
-    uselist=False,
-    innerjoin=True, lazy='joined',
-    backref=backref('transformation', uselist=False, innerjoin=True))
-
 
 ContestCombo.first = relationship(Move,
     primaryjoin=ContestCombo.first_move_id==Move.id,
@@ -2805,19 +2182,6 @@ PokemonSpecies.pal_park = relationship(PalPark,
     uselist=False,
     backref='species')
 
-PokemonSpecies.conquest_abilities = relationship(Ability,
-    secondary=ConquestPokemonAbility.__table__,
-    order_by=ConquestPokemonAbility.slot,
-    backref=backref('conquest_pokemon', order_by=PokemonSpecies.conquest_order,
-                    innerjoin=True))
-PokemonSpecies.conquest_move = relationship(Move,
-    secondary=ConquestPokemonMove.__table__,
-    uselist=False,
-    backref=backref('conquest_pokemon', order_by=PokemonSpecies.conquest_order))
-PokemonSpecies.conquest_evolution = relationship(ConquestPokemonEvolution,
-    uselist=False,
-    backref=backref('evolved_species', innerjoin=True, lazy='joined', uselist=False))
-
 PokemonSpeciesFlavorText.version = relationship(Version, innerjoin=True, lazy='joined')
 PokemonSpeciesFlavorText.language = relationship(Language, innerjoin=True, lazy='joined')
 
diff --git a/pokedex/tests/test_schema.py b/pokedex/tests/test_schema.py
index 2288ac9..b14d915 100644
--- a/pokedex/tests/test_schema.py
+++ b/pokedex/tests/test_schema.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from sqlalchemy import Column, Integer, String, create_engine
+from sqlalchemy import Column, Integer, String, Unicode, create_engine
 from sqlalchemy.orm import class_mapper, joinedload, sessionmaker
 from sqlalchemy.orm.session import Session
 from sqlalchemy.ext.declarative import declarative_base
@@ -31,16 +31,6 @@ def test_variable_names_2(table):
     """We also want all of the tables exported"""
     assert getattr(tables, table.__name__) is table
 
-def test_class_order():
-    """The declarative classes should be defined in alphabetical order.
-    Except for Language which should be first.
-    """
-    class_names = [table.__name__ for table in tables.mapped_classes]
-    def key(name):
-        return name != 'Language', name
-    print [(a,b) for (a,b) in zip(class_names, sorted(class_names, key=key)) if a!=b]
-    assert class_names == sorted(class_names, key=key)
-
 def test_i18n_table_creation():
     """Creates and manipulates a magical i18n table, completely independent of
     the existing schema and data.  Makes sure that the expected behavior of the
@@ -190,7 +180,7 @@ def test_texts(cls):
                 pytest.fail('%s: official text with bad format' % column)
             text_columns.append(column)
         else:
-            if isinstance(column.type, tables.Unicode):
+            if isinstance(column.type, Unicode):
                 pytest.fail('%s: text column without format' % column)
         if column.name == 'name' and format != 'plaintext':
             pytest.fail('%s: non-plaintext name' % column)