Sigh! Remove support for strings as keys; use Language objects.
[zzz-pokedex.git] / pokedex / db / tables.py
index 89a0efa..20fcbea 100644 (file)
@@ -15,22 +15,35 @@ Columns have a info dictionary with these keys:
   - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
     for translation.
   - latex: A formula in LaTeX syntax.
   - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
     for translation.
   - latex: A formula in LaTeX syntax.
+
+A localizable text column is visible as two properties:
+The plural-name property (e.g. Pokemon.names) is a language-to-name dictionary:
+  bulbasaur.names['en'] == "Bulbasaur" and bulbasaur.names['de'] == "Bisasam".
+  You can use Pokemon.names['en'] to filter a query.
+The singular-name property returns the name in the default language, English.
+  For example bulbasaur.name == "Bulbasaur"
+  Setting pokedex.db.tables.default_lang changes the default language.
 """
 # XXX: Check if "gametext" is set correctly everywhere
 
 """
 # XXX: Check if "gametext" is set correctly everywhere
 
-import operator
+import collections
+from functools import partial
 
 
-from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table
+from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table, UniqueConstraint
 from sqlalchemy.ext.declarative import (
         declarative_base, declared_attr, DeclarativeMeta,
     )
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import (
 from sqlalchemy.ext.declarative import (
         declarative_base, declared_attr, DeclarativeMeta,
     )
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import (
-        backref, eagerload_all, relation, class_mapper, synonym, mapper,
+        backref, compile_mappers, eagerload_all, relation, class_mapper, synonym, mapper,
     )
     )
-from sqlalchemy.orm.session import Session
-from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.orm.session import Session, object_session
+from sqlalchemy.orm.interfaces import AttributeExtension
+from sqlalchemy.orm.collections import attribute_mapped_collection, MappedCollection, collection, collection_adapter
+from sqlalchemy.ext.associationproxy import _AssociationDict, association_proxy
 from sqlalchemy.sql import and_
 from sqlalchemy.sql import and_
+from sqlalchemy.sql.expression import ColumnOperators, bindparam
+from sqlalchemy.schema import ColumnDefault
 from sqlalchemy.types import *
 from inspect import isclass
 
 from sqlalchemy.types import *
 from inspect import isclass
 
@@ -45,30 +58,34 @@ class TableMetaclass(DeclarativeMeta):
         if hasattr(cls, '__tablename__'):
             table_classes.append(cls)
 
         if hasattr(cls, '__tablename__'):
             table_classes.append(cls)
 
-metadata = MetaData()
-TableBase = declarative_base(metadata=metadata, metaclass=TableMetaclass)
-
-### Helper classes
-class Named(object):
-    """Mixin for objects that have names"""
+class TableSuperclass(object):
+    """Superclass for declarative tables, to give them some generic niceties
+    like stringification.
+    """
     def __unicode__(self):
     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:
         try:
-            return '<%s: %s>' % (type(self).__name__, self.identifier)
+            return u"<%s object (%s): %s>" % (typename, pk, self.identifier)
         except AttributeError:
         except AttributeError:
-            return '<%s>' % type(self).__name__
+            return u"<%s object (%s)>" % (typename, pk)
 
     def __str__(self):
 
     def __str__(self):
-        return unicode(self).encode('utf-8')
-
-    def __repr__(self):
-        return str(self)
+        return unicode(self).encode('utf8')
 
 
-class OfficiallyNamed(Named):
-    """Mixin for stuff with official names"""
-
-class UnofficiallyNamed(Named):
-    """Mixin for stuff with unofficial names"""
+metadata = MetaData()
+TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
 
 
+### Helper classes
 class LanguageSpecific(object):
     """Mixin for prose and text tables"""
     @declared_attr
 class LanguageSpecific(object):
     """Mixin for prose and text tables"""
     @declared_attr
@@ -98,7 +115,7 @@ class TextColumn(LanguageSpecificColumn):
 
 ### The actual tables
 
 
 ### The actual tables
 
-class Ability(TableBase, OfficiallyNamed):
+class Ability(TableBase):
     u"""An ability a Pokémon can have, such as Static or Pressure.
     """
     __tablename__ = 'abilities'
     u"""An ability a Pokémon can have, such as Static or Pressure.
     """
     __tablename__ = 'abilities'
@@ -113,6 +130,8 @@ class Ability(TableBase, OfficiallyNamed):
         info=dict(description="A detailed description of this ability's effect", format='markdown'))
     short_effect = ProseColumn(markdown.MarkdownColumn(255), plural='short_effects', nullable=False,
         info=dict(description="A short summary of this ability's effect", format='markdown'))
         info=dict(description="A detailed description of this ability's effect", format='markdown'))
     short_effect = ProseColumn(markdown.MarkdownColumn(255), plural='short_effects', nullable=False,
         info=dict(description="A short summary of this ability's effect", format='markdown'))
+    name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class AbilityChangelog(TableBase):
     """History of changes to abilities across main game versions."""
 
 class AbilityChangelog(TableBase):
     """History of changes to abilities across main game versions."""
@@ -165,7 +184,7 @@ class Berry(TableBase):
     smoothness = Column(Integer, nullable=False,
         info=dict(description="The smoothness of this Berry, used in making Pokéblocks or Poffins"))
 
     smoothness = Column(Integer, nullable=False,
         info=dict(description="The smoothness of this Berry, used in making Pokéblocks or Poffins"))
 
-class BerryFirmness(TableBase, OfficiallyNamed):
+class BerryFirmness(TableBase):
     u"""A Berry firmness, such as "hard" or "very soft".
     """
     __tablename__ = 'berry_firmness'
     u"""A Berry firmness, such as "hard" or "very soft".
     """
     __tablename__ = 'berry_firmness'
@@ -174,6 +193,8 @@ class BerryFirmness(TableBase, OfficiallyNamed):
         info=dict(description="A unique ID for this firmness"))
     identifier = Column(Unicode(10), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for this firmness"))
     identifier = Column(Unicode(10), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(10), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class BerryFlavor(TableBase):
     u"""A Berry flavor level.
 
 class BerryFlavor(TableBase):
     u"""A Berry flavor level.
@@ -211,7 +232,7 @@ class ContestEffect(TableBase):
     effect = ProseColumn(Unicode(255), plural='effects', nullable=False,
         info=dict(description="A detailed description of the effect", format='plaintext'))
 
     effect = ProseColumn(Unicode(255), plural='effects', nullable=False,
         info=dict(description="A detailed description of the effect", format='plaintext'))
 
-class ContestType(TableBase, OfficiallyNamed):
+class ContestType(TableBase):
     u"""A Contest type, such as "cool" or "smart", and their associated Berry flavors and Pokéblock colors.
     """
     __tablename__ = 'contest_types'
     u"""A Contest type, such as "cool" or "smart", and their associated Berry flavors and Pokéblock colors.
     """
     __tablename__ = 'contest_types'
@@ -224,8 +245,10 @@ class ContestType(TableBase, OfficiallyNamed):
         info=dict(description="The name of the corresponding Berry flavor", official=True, format='plaintext'))
     color = TextColumn(Unicode(6), nullable=False, plural='colors',
         info=dict(description=u"The name of the corresponding Pokéblock color", official=True, format='plaintext'))
         info=dict(description="The name of the corresponding Berry flavor", official=True, format='plaintext'))
     color = TextColumn(Unicode(6), nullable=False, plural='colors',
         info=dict(description=u"The name of the corresponding Pokéblock color", official=True, format='plaintext'))
+    name = TextColumn(Unicode(6), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class EggGroup(TableBase, UnofficiallyNamed):
+class EggGroup(TableBase):
     u"""An Egg group. Usually, two Pokémon can breed if they share an Egg Group.
 
     (exceptions are the Ditto and No Eggs groups)
     u"""An Egg group. Usually, two Pokémon can breed if they share an Egg Group.
 
     (exceptions are the Ditto and No Eggs groups)
@@ -236,6 +259,8 @@ class EggGroup(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for this group"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier.", format='identifier'))
         info=dict(description="A unique ID for this group"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier.", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class Encounter(TableBase):
     u"""Encounters with wild Pokémon.
 
 class Encounter(TableBase):
     u"""Encounters with wild Pokémon.
@@ -277,7 +302,7 @@ class Encounter(TableBase):
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
-class EncounterCondition(TableBase, UnofficiallyNamed):
+class EncounterCondition(TableBase):
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
 
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
 
@@ -287,8 +312,10 @@ class EncounterCondition(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for this condition"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for this condition"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class EncounterConditionValue(TableBase, UnofficiallyNamed):
+class EncounterConditionValue(TableBase):
     u"""A possible state for a condition; for example, the state of 'swarm' could be 'swarm' or 'no swarm'.
     """
 
     u"""A possible state for a condition; for example, the state of 'swarm' could be 'swarm' or 'no swarm'.
     """
 
@@ -302,6 +329,8 @@ class EncounterConditionValue(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     is_default = Column(Boolean, nullable=False,
         info=dict(description='Set if this value is the default state for the condition'))
         info=dict(description="An identifier", format='identifier'))
     is_default = Column(Boolean, nullable=False,
         info=dict(description='Set if this value is the default state for the condition'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class EncounterConditionValueMap(TableBase):
     u"""Maps encounters to the specific conditions under which they occur.
 
 class EncounterConditionValueMap(TableBase):
     u"""Maps encounters to the specific conditions under which they occur.
@@ -312,7 +341,7 @@ class EncounterConditionValueMap(TableBase):
     encounter_condition_value_id = Column(Integer, ForeignKey('encounter_condition_values.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="The ID of the encounter condition value"))
 
     encounter_condition_value_id = Column(Integer, ForeignKey('encounter_condition_values.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="The ID of the encounter condition value"))
 
-class EncounterTerrain(TableBase, UnofficiallyNamed):
+class EncounterTerrain(TableBase):
     u"""A way the player can enter a wild encounter, e.g., surfing, fishing, or walking through tall grass.
     """
 
     u"""A way the player can enter a wild encounter, e.g., surfing, fishing, or walking through tall grass.
     """
 
@@ -322,6 +351,8 @@ class EncounterTerrain(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for the terrain"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for the terrain"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class EncounterSlot(TableBase):
     u"""An abstract "slot" within a terrain, associated with both some set of conditions and a rarity.
 
 class EncounterSlot(TableBase):
     u"""An abstract "slot" within a terrain, associated with both some set of conditions and a rarity.
@@ -353,7 +384,7 @@ class EvolutionChain(TableBase):
     baby_trigger_item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
         info=dict(description="Item that a parent must hold while breeding to produce a baby"))
 
     baby_trigger_item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
         info=dict(description="Item that a parent must hold while breeding to produce a baby"))
 
-class EvolutionTrigger(TableBase, UnofficiallyNamed):
+class EvolutionTrigger(TableBase):
     u"""An evolution type, such as "level" or "trade".
     """
     __tablename__ = 'evolution_triggers'
     u"""An evolution type, such as "level" or "trade".
     """
     __tablename__ = 'evolution_triggers'
@@ -362,6 +393,8 @@ class EvolutionTrigger(TableBase, UnofficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class Experience(TableBase):
     u"""EXP needed for a certain level with a certain growth rate
 
 class Experience(TableBase):
     u"""EXP needed for a certain level with a certain growth rate
@@ -374,7 +407,7 @@ class Experience(TableBase):
     experience = Column(Integer, nullable=False,
         info=dict(description="The number of EXP points needed to get to that level"))
 
     experience = Column(Integer, nullable=False,
         info=dict(description="The number of EXP points needed to get to that level"))
 
-class Generation(TableBase, OfficiallyNamed):
+class Generation(TableBase):
     u"""A Generation of the Pokémon franchise
     """
     __tablename__ = 'generations'
     u"""A Generation of the Pokémon franchise
     """
     __tablename__ = 'generations'
@@ -387,8 +420,10 @@ class Generation(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Pokédex this generation's main games use by default"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u'An identifier', format='identifier'))
         info=dict(description=u"ID of the Pokédex this generation's main games use by default"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u'An identifier', format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class GrowthRate(TableBase, UnofficiallyNamed):
+class GrowthRate(TableBase):
     u"""Growth rate of a Pokémon, i.e. the EXP → level function.
     """
     __tablename__ = 'growth_rates'
     u"""Growth rate of a Pokémon, i.e. the EXP → level function.
     """
     __tablename__ = 'growth_rates'
@@ -399,8 +434,10 @@ class GrowthRate(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     formula = Column(Unicode(500), nullable=False,
         info=dict(description="The formula", format='latex'))
         info=dict(description="An identifier", format='identifier'))
     formula = Column(Unicode(500), nullable=False,
         info=dict(description="The formula", format='latex'))
+    name = ProseColumn(Unicode(20), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Item(TableBase, OfficiallyNamed):
+class Item(TableBase):
     u"""An Item from the games, like "Poké Ball" or "Bicycle".
     """
     __tablename__ = 'items'
     u"""An Item from the games, like "Poké Ball" or "Bicycle".
     """
     __tablename__ = 'items'
@@ -421,6 +458,8 @@ class Item(TableBase, OfficiallyNamed):
         info=dict(description="A short summary of the effect", format='plaintext'))
     effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
         info=dict(description=u"Detailed description of the item's effect.", format='markdown'))
         info=dict(description="A short summary of the effect", format='plaintext'))
     effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
         info=dict(description=u"Detailed description of the item's effect.", format='markdown'))
+    name = TextColumn(Unicode(20), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def appears_underground(self):
 
     @property
     def appears_underground(self):
@@ -428,7 +467,7 @@ class Item(TableBase, OfficiallyNamed):
         """
         return any(flag.identifier == u'underground' for flag in self.flags)
 
         """
         return any(flag.identifier == u'underground' for flag in self.flags)
 
-class ItemCategory(TableBase, UnofficiallyNamed):
+class ItemCategory(TableBase):
     u"""An item category
     """
     # XXX: This is fanon, right?
     u"""An item category
     """
     # XXX: This is fanon, right?
@@ -440,8 +479,10 @@ class ItemCategory(TableBase, UnofficiallyNamed):
         info=dict(description="ID of the pocket these items go to"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID of the pocket these items go to"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class ItemFlag(TableBase, UnofficiallyNamed):
+class ItemFlag(TableBase):
     u"""An item attribute such as "consumable" or "holdable".
     """
     __tablename__ = 'item_flags'
     u"""An item attribute such as "consumable" or "holdable".
     """
     __tablename__ = 'item_flags'
@@ -452,6 +493,8 @@ class ItemFlag(TableBase, UnofficiallyNamed):
         info=dict(description="Identifier of the flag", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="Short description of the flag", format='plaintext'))
         info=dict(description="Identifier of the flag", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="Short description of the flag", format='plaintext'))
+    name = ProseColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class ItemFlagMap(TableBase):
     u"""Maps an item flag to its item.
 
 class ItemFlagMap(TableBase):
     u"""Maps an item flag to its item.
@@ -495,7 +538,7 @@ class ItemInternalID(TableBase):
     internal_id = Column(Integer, nullable=False,
         info=dict(description="Internal ID of the item in the generation"))
 
     internal_id = Column(Integer, nullable=False,
         info=dict(description="Internal ID of the item in the generation"))
 
-class ItemPocket(TableBase, OfficiallyNamed):
+class ItemPocket(TableBase):
     u"""A pocket that categorizes items
     """
     __tablename__ = 'item_pockets'
     u"""A pocket that categorizes items
     """
     __tablename__ = 'item_pockets'
@@ -504,8 +547,10 @@ class ItemPocket(TableBase, OfficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier of this pocket", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier of this pocket", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Language(TableBase, OfficiallyNamed):
+class Language(TableBase):
     u"""A language the Pokémon games have been transleted into
     """
     __tablename__ = 'languages'
     u"""A language the Pokémon games have been transleted into
     """
     __tablename__ = 'languages'
@@ -522,26 +567,10 @@ class Language(TableBase, OfficiallyNamed):
         info=dict(description=u"True iff games are produced in the language."))
     order = Column(Integer, nullable=True,
         info=dict(description=u"Order for sorting in foreign name lists."))
         info=dict(description=u"True iff games are produced in the language."))
     order = Column(Integer, nullable=True,
         info=dict(description=u"Order for sorting in foreign name lists."))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-    # Languages compare equal to its identifier, so a dictionary of
-    # translations, with a Language as the key, can be indexed by the identifier
-    def __eq__(self, other):
-        try:
-            return (
-                    self is other or
-                    self.identifier == other or
-                    self.identifier == other.identifier
-                )
-        except AttributeError:
-            return NotImplemented
-
-    def __ne__(self, other):
-        return not (self == other)
-
-    def __hash__(self):
-        return hash(self.identifier)
-
-class Location(TableBase, OfficiallyNamed):
+class Location(TableBase):
     u"""A place in the Pokémon world
     """
     __tablename__ = 'locations'
     u"""A place in the Pokémon world
     """
     __tablename__ = 'locations'
@@ -552,8 +581,10 @@ class Location(TableBase, OfficiallyNamed):
         info=dict(description="ID of the region this location is in"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID of the region this location is in"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class LocationArea(TableBase, UnofficiallyNamed):
+class LocationArea(TableBase):
     u"""A sub-area of a location
     """
     __tablename__ = 'location_areas'
     u"""A sub-area of a location
     """
     __tablename__ = 'location_areas'
@@ -566,6 +597,8 @@ class LocationArea(TableBase, UnofficiallyNamed):
         info=dict(description="ID the games ude for this area"))
     identifier = Column(Unicode(64), nullable=True,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID the games ude for this area"))
     identifier = Column(Unicode(64), nullable=True,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class LocationAreaEncounterRate(TableBase):
     # XXX: What's this exactly? Someone add the docstring & revise the descriptions
 
 class LocationAreaEncounterRate(TableBase):
     # XXX: What's this exactly? Someone add the docstring & revise the descriptions
@@ -609,7 +642,7 @@ class Machine(TableBase):
         """
         return self.machine_number >= 100
 
         """
         return self.machine_number >= 100
 
-class MoveBattleStyle(TableBase, UnofficiallyNamed):
+class MoveBattleStyle(TableBase):
     u"""A battle style of a move"""  # XXX: Explain better
     __tablename__ = 'move_battle_styles'
     __singlename__ = 'move_battle_style'
     u"""A battle style of a move"""  # XXX: Explain better
     __tablename__ = 'move_battle_styles'
     __singlename__ = 'move_battle_style'
@@ -617,8 +650,10 @@ class MoveBattleStyle(TableBase, UnofficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class MoveEffectCategory(TableBase, UnofficiallyNamed):
+class MoveEffectCategory(TableBase):
     u"""Category of a move effect
     """
     __tablename__ = 'move_effect_categories'
     u"""Category of a move effect
     """
     __tablename__ = 'move_effect_categories'
@@ -629,6 +664,8 @@ class MoveEffectCategory(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     can_affect_user = Column(Boolean, nullable=False,
         info=dict(description="Set if the user can be affected"))
         info=dict(description="An identifier", format='identifier'))
     can_affect_user = Column(Boolean, nullable=False,
         info=dict(description="Set if the user can be affected"))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveEffectCategoryMap(TableBase):
     u"""Maps a move effect category to a move effect
 
 class MoveEffectCategoryMap(TableBase):
     u"""Maps a move effect category to a move effect
@@ -641,7 +678,7 @@ class MoveEffectCategoryMap(TableBase):
     affects_user = Column(Boolean, primary_key=True, nullable=False,
         info=dict(description="Set if the user is affected"))
 
     affects_user = Column(Boolean, primary_key=True, nullable=False,
         info=dict(description="Set if the user is affected"))
 
-class MoveDamageClass(TableBase, UnofficiallyNamed):
+class MoveDamageClass(TableBase):
     u"""Any of the damage classes moves can have, i.e. physical, special, or non-damaging.
     """
     __tablename__ = 'move_damage_classes'
     u"""Any of the damage classes moves can have, i.e. physical, special, or non-damaging.
     """
     __tablename__ = 'move_damage_classes'
@@ -652,6 +689,8 @@ class MoveDamageClass(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="A description of the class", format='plaintext'))
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="A description of the class", format='plaintext'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveEffect(TableBase):
     u"""An effect of a move
 
 class MoveEffect(TableBase):
     u"""An effect of a move
@@ -671,13 +710,18 @@ class MoveEffectChangelog(TableBase):
     __singlename__ = 'move_effect_changelog'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description="A numeric ID"))
     __singlename__ = 'move_effect_changelog'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description="A numeric ID"))
-    effect_id = Column(Integer, ForeignKey('move_effects.id'), primary_key=True, nullable=False,
+    effect_id = Column(Integer, ForeignKey('move_effects.id'), nullable=False,
         info=dict(description="The ID of the effect that changed"))
         info=dict(description="The ID of the effect that changed"))
-    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False,
+    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), nullable=False,
         info=dict(description="The ID of the version group in which the effect changed"))
     effect = ProseColumn(markdown.MarkdownColumn(512), plural='effects', nullable=False,
         info=dict(description="A description of the old behavior", format='markdown'))
 
         info=dict(description="The ID of the version group in which the effect changed"))
     effect = ProseColumn(markdown.MarkdownColumn(512), plural='effects', nullable=False,
         info=dict(description="A description of the old behavior", format='markdown'))
 
+    __table_args__ = (
+        UniqueConstraint(effect_id, changed_in_version_group_id),
+        {},
+    )
+
 class MoveFlag(TableBase):
     u"""Maps a move flag to a move
     """
 class MoveFlag(TableBase):
     u"""Maps a move flag to a move
     """
@@ -689,7 +733,7 @@ class MoveFlag(TableBase):
     move_flag_type_id = Column(Integer, ForeignKey('move_flag_types.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="ID of the flag"))
 
     move_flag_type_id = Column(Integer, ForeignKey('move_flag_types.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="ID of the flag"))
 
-class MoveFlagType(TableBase, UnofficiallyNamed):
+class MoveFlagType(TableBase):
     u"""A Move attribute such as "snatchable" or "contact".
     """
     __tablename__ = 'move_flag_types'
     u"""A Move attribute such as "snatchable" or "contact".
     """
     __tablename__ = 'move_flag_types'
@@ -700,6 +744,8 @@ class MoveFlagType(TableBase, UnofficiallyNamed):
         info=dict(description="A short identifier for the flag", format='identifier'))
     description = ProseColumn(markdown.MarkdownColumn(128), plural='descriptions', nullable=False,
         info=dict(description="A short description of the flag", format='markdown'))
         info=dict(description="A short identifier for the flag", format='identifier'))
     description = ProseColumn(markdown.MarkdownColumn(128), plural='descriptions', nullable=False,
         info=dict(description="A short description of the flag", format='markdown'))
+    name = ProseColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveFlavorText(TableBase, LanguageSpecific):
     u"""In-game description of a move
 
 class MoveFlavorText(TableBase, LanguageSpecific):
     u"""In-game description of a move
@@ -742,7 +788,7 @@ class MoveMeta(TableBase):
     stat_chance = Column(Integer, nullable=False, index=True,
         info=dict(description="Chance to cause a stat change, in percent"))
 
     stat_chance = Column(Integer, nullable=False, index=True,
         info=dict(description="Chance to cause a stat change, in percent"))
 
-class MoveMetaAilment(TableBase, OfficiallyNamed):
+class MoveMetaAilment(TableBase):
     u"""Common status ailments moves can inflict on a single Pokémon, including
     major ailments like paralysis and minor ailments like trapping.
     """
     u"""Common status ailments moves can inflict on a single Pokémon, including
     major ailments like paralysis and minor ailments like trapping.
     """
@@ -752,6 +798,8 @@ class MoveMetaAilment(TableBase, OfficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(24), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(24), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class MoveMetaCategory(TableBase):
     u"""Very general categories that loosely group move effects."""
 
 class MoveMetaCategory(TableBase):
     u"""Very general categories that loosely group move effects."""
@@ -772,7 +820,7 @@ class MoveMetaStatChange(TableBase):
     change = Column(Integer, nullable=False, index=True,
         info=dict(description="Amount of increase/decrease, in stages"))
 
     change = Column(Integer, nullable=False, index=True,
         info=dict(description="Amount of increase/decrease, in stages"))
 
-class MoveTarget(TableBase, UnofficiallyNamed):
+class MoveTarget(TableBase):
     u"""Targetting or "range" of a move, e.g. "Affects all opponents" or "Affects user".
     """
     __tablename__ = 'move_targets'
     u"""Targetting or "range" of a move, e.g. "Affects all opponents" or "Affects user".
     """
     __tablename__ = 'move_targets'
@@ -783,8 +831,10 @@ class MoveTarget(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(128), plural='descriptions', nullable=False,
         info=dict(description="A description", format='plaintext'))
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(128), plural='descriptions', nullable=False,
         info=dict(description="A description", format='plaintext'))
+    name = ProseColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Move(TableBase, OfficiallyNamed):
+class Move(TableBase):
     u"""A Move: technique or attack a Pokémon can learn to use
     """
     __tablename__ = 'moves'
     u"""A Move: technique or attack a Pokémon can learn to use
     """
     __tablename__ = 'moves'
@@ -819,6 +869,8 @@ class Move(TableBase, OfficiallyNamed):
         info=dict(description="ID of the move's Contest effect"))
     super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
         info=dict(description="ID of the move's Super Contest effect"))
         info=dict(description="ID of the move's Contest effect"))
     super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
         info=dict(description="ID of the move's Super Contest effect"))
+    name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class MoveChangelog(TableBase):
     """History of changes to moves across main game versions."""
 
 class MoveChangelog(TableBase):
     """History of changes to moves across main game versions."""
@@ -841,7 +893,7 @@ class MoveChangelog(TableBase):
     effect_chance = Column(Integer, nullable=True,
         info=dict(description="Prior effect chance, or NULL if unchanged"))
 
     effect_chance = Column(Integer, nullable=True,
         info=dict(description="Prior effect chance, or NULL if unchanged"))
 
-class Nature(TableBase, OfficiallyNamed):
+class Nature(TableBase):
     u"""A nature a Pokémon can have, such as Calm or Brave
     """
     __tablename__ = 'natures'
     u"""A nature a Pokémon can have, such as Calm or Brave
     """
     __tablename__ = 'natures'
@@ -858,6 +910,8 @@ class Nature(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Berry flavor the Pokémon hates (if likes_flavor_id is the same, the effects cancel out)"))
     likes_flavor_id = Column(Integer, ForeignKey('contest_types.id'), nullable=False,
         info=dict(description=u"ID of the Berry flavor the Pokémon likes (if hates_flavor_id is the same, the effects cancel out)"))
         info=dict(description=u"ID of the Berry flavor the Pokémon hates (if likes_flavor_id is the same, the effects cancel out)"))
     likes_flavor_id = Column(Integer, ForeignKey('contest_types.id'), nullable=False,
         info=dict(description=u"ID of the Berry flavor the Pokémon likes (if hates_flavor_id is the same, the effects cancel out)"))
+    name = TextColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def is_neutral(self):
 
     @property
     def is_neutral(self):
@@ -893,7 +947,7 @@ class NaturePokeathlonStat(TableBase):
     max_change = Column(Integer, nullable=False,
         info=dict(description="Maximum change"))
 
     max_change = Column(Integer, nullable=False,
         info=dict(description="Maximum change"))
 
-class PokeathlonStat(TableBase, OfficiallyNamed):
+class PokeathlonStat(TableBase):
     u"""A Pokéathlon stat, such as "Stamina" or "Jump".
     """
     __tablename__ = 'pokeathlon_stats'
     u"""A Pokéathlon stat, such as "Stamina" or "Jump".
     """
     __tablename__ = 'pokeathlon_stats'
@@ -902,8 +956,10 @@ class PokeathlonStat(TableBase, OfficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Pokedex(TableBase, UnofficiallyNamed):
+class Pokedex(TableBase):
     u"""A collection of Pokémon species ordered in a particular way
     """
     __tablename__ = 'pokedexes'
     u"""A collection of Pokémon species ordered in a particular way
     """
     __tablename__ = 'pokedexes'
@@ -916,8 +972,10 @@ class Pokedex(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(512), plural='descriptions', nullable=False,
         info=dict(description=u"A longer description of the Pokédex", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(512), plural='descriptions', nullable=False,
         info=dict(description=u"A longer description of the Pokédex", format='plaintext'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Pokemon(TableBase, OfficiallyNamed):
+class Pokemon(TableBase):
     u"""A species of Pokémon.  The core to this whole mess.
     """
     __tablename__ = 'pokemon'
     u"""A species of Pokémon.  The core to this whole mess.
     """
     __tablename__ = 'pokemon'
@@ -959,6 +1017,8 @@ class Pokemon(TableBase, OfficiallyNamed):
         info=dict(description=u"Set iff the species exhibits enough sexual dimorphism to have separate sets of sprites in Gen IV and beyond."))
     order = Column(Integer, nullable=False, index=True,
         info=dict(description=u"Order for sorting. Almost national order, except families and forms are grouped together."))
         info=dict(description=u"Set iff the species exhibits enough sexual dimorphism to have separate sets of sprites in Gen IV and beyond."))
     order = Column(Integer, nullable=False, index=True,
         info=dict(description=u"Order for sorting. Almost national order, except families and forms are grouped together."))
+    name = TextColumn(Unicode(20), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     ### Stuff to handle alternate Pokémon forms
 
 
     ### Stuff to handle alternate Pokémon forms
 
@@ -1058,7 +1118,7 @@ class PokemonAbility(TableBase):
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The ability slot, i.e. 1 or 2 for gen. IV"))
 
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The ability slot, i.e. 1 or 2 for gen. IV"))
 
-class PokemonColor(TableBase, OfficiallyNamed):
+class PokemonColor(TableBase):
     u"""The "Pokédex color" of a Pokémon species. Usually based on the Pokémon's color.
     """
     __tablename__ = 'pokemon_colors'
     u"""The "Pokédex color" of a Pokémon species. Usually based on the Pokémon's color.
     """
     __tablename__ = 'pokemon_colors'
@@ -1067,6 +1127,8 @@ class PokemonColor(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Pokémon"))
     identifier = Column(Unicode(6), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"ID of the Pokémon"))
     identifier = Column(Unicode(6), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(6), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class PokemonDexNumber(TableBase):
     u"""The number of a Pokémon in a particular Pokédex (e.g. Jigglypuff is #138 in Hoenn's 'dex)
 
 class PokemonDexNumber(TableBase):
     u"""The number of a Pokémon in a particular Pokédex (e.g. Jigglypuff is #138 in Hoenn's 'dex)
@@ -1137,7 +1199,7 @@ class PokemonFlavorText(TableBase, LanguageSpecific):
     flavor_text = Column(Unicode(255), nullable=False,
         info=dict(description=u"ID of the version that has this flavor text", official=True, format='gametext'))
 
     flavor_text = Column(Unicode(255), nullable=False,
         info=dict(description=u"ID of the version that has this flavor text", official=True, format='gametext'))
 
-class PokemonForm(TableBase, OfficiallyNamed):
+class PokemonForm(TableBase):
     u"""An individual form of a Pokémon.
 
     Pokémon that do not have separate forms are still given a single row to
     u"""An individual form of a Pokémon.
 
     Pokémon that do not have separate forms are still given a single row to
@@ -1159,6 +1221,8 @@ class PokemonForm(TableBase, OfficiallyNamed):
         info=dict(description=u'Set for exactly one form used as the default for each species.'))
     order = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The order in which forms should be sorted.  Multiple forms may have equal order, in which case they should fall back on sorting by name.'))
         info=dict(description=u'Set for exactly one form used as the default for each species.'))
     order = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The order in which forms should be sorted.  Multiple forms may have equal order, in which case they should fall back on sorting by name.'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def pokemon(self):
 
     @property
     def pokemon(self):
@@ -1217,7 +1281,7 @@ class PokemonFormPokeathlonStat(TableBase):
     maximum_stat = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The maximum value for this stat for this Pokémon form.'))
 
     maximum_stat = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The maximum value for this stat for this Pokémon form.'))
 
-class PokemonHabitat(TableBase, OfficiallyNamed):
+class PokemonHabitat(TableBase):
     u"""The habitat of a Pokémon, as given in the FireRed/LeafGreen version Pokédex
     """
     __tablename__ = 'pokemon_habitats'
     u"""The habitat of a Pokémon, as given in the FireRed/LeafGreen version Pokédex
     """
     __tablename__ = 'pokemon_habitats'
@@ -1226,6 +1290,8 @@ class PokemonHabitat(TableBase, OfficiallyNamed):
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class PokemonInternalID(TableBase):
     u"""The number of a Pokémon a game uses internally
 
 class PokemonInternalID(TableBase):
     u"""The number of a Pokémon a game uses internally
@@ -1273,7 +1339,7 @@ class PokemonMove(TableBase):
         {},
     )
 
         {},
     )
 
-class PokemonMoveMethod(TableBase, UnofficiallyNamed):
+class PokemonMoveMethod(TableBase):
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
     __tablename__ = 'pokemon_move_methods'
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
     __tablename__ = 'pokemon_move_methods'
@@ -1284,8 +1350,10 @@ class PokemonMoveMethod(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(255), plural='descriptions', nullable=False,
         info=dict(description=u"A detailed description of how the method works", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(255), plural='descriptions', nullable=False,
         info=dict(description=u"A detailed description of how the method works", format='plaintext'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class PokemonShape(TableBase, UnofficiallyNamed):
+class PokemonShape(TableBase):
     u"""The shape of a Pokémon's body, as used in generation IV Pokédexes.
     """
     __tablename__ = 'pokemon_shapes'
     u"""The shape of a Pokémon's body, as used in generation IV Pokédexes.
     """
     __tablename__ = 'pokemon_shapes'
@@ -1296,6 +1364,8 @@ class PokemonShape(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     awesome_name = ProseColumn(Unicode(16), plural='awesome_names', nullable=False,
         info=dict(description=u"A splendiferous name of the body shape", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     awesome_name = ProseColumn(Unicode(16), plural='awesome_names', nullable=False,
         info=dict(description=u"A splendiferous name of the body shape", format='plaintext'))
+    name = ProseColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class PokemonStat(TableBase):
     u"""A stat value of a Pokémon
 
 class PokemonStat(TableBase):
     u"""A stat value of a Pokémon
@@ -1321,7 +1391,7 @@ class PokemonType(TableBase):
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The type's slot, 1 or 2, used to sort types if there are two of them"))
 
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The type's slot, 1 or 2, used to sort types if there are two of them"))
 
-class Region(TableBase, OfficiallyNamed):
+class Region(TableBase):
     u"""Major areas of the world: Kanto, Johto, etc.
     """
     __tablename__ = 'regions'
     u"""Major areas of the world: Kanto, Johto, etc.
     """
     __tablename__ = 'regions'
@@ -1330,8 +1400,10 @@ class Region(TableBase, OfficiallyNamed):
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Stat(TableBase, OfficiallyNamed):
+class Stat(TableBase):
     u"""A Stat, such as Attack or Speed
     """
     __tablename__ = 'stats'
     u"""A Stat, such as Attack or Speed
     """
     __tablename__ = 'stats'
@@ -1342,6 +1414,8 @@ class Stat(TableBase, OfficiallyNamed):
         info=dict(description=u"For offensive and defensive stats, the damage this stat relates to; otherwise None (the NULL value)"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"For offensive and defensive stats, the damage this stat relates to; otherwise None (the NULL value)"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class StatHint(TableBase):
     u"""Flavor text for genes that appears in a Pokémon's summary.  Sometimes
 
 class StatHint(TableBase):
     u"""Flavor text for genes that appears in a Pokémon's summary.  Sometimes
@@ -1355,7 +1429,7 @@ class StatHint(TableBase):
         info=dict(description=u"ID of the highest stat"))
     gene_mod_5 = Column(Integer, nullable=False, index=True,
         info=dict(description=u"Value of the highest stat modulo 5"))
         info=dict(description=u"ID of the highest stat"))
     gene_mod_5 = Column(Integer, nullable=False, index=True,
         info=dict(description=u"Value of the highest stat modulo 5"))
-    text = TextColumn(Unicode(24), plural='texts', nullable=False, index=True, unique=True,
+    message = TextColumn(Unicode(24), plural='messages', nullable=False, index=True, unique=True,
         info=dict(description=u"The text displayed", official=True, format='plaintext'))
 
 class SuperContestCombo(TableBase):
         info=dict(description=u"The text displayed", official=True, format='plaintext'))
 
 class SuperContestCombo(TableBase):
@@ -1391,18 +1465,20 @@ class TypeEfficacy(TableBase):
     damage_factor = Column(Integer, nullable=False,
         info=dict(description=u"The multiplier, as a percentage of damage inflicted."))
 
     damage_factor = Column(Integer, nullable=False,
         info=dict(description=u"The multiplier, as a percentage of damage inflicted."))
 
-class Type(TableBase, OfficiallyNamed):
+class Type(TableBase):
     u"""Any of the elemental types Pokémon and moves can have."""
     __tablename__ = 'types'
     __singlename__ = 'type'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description=u"A unique ID for this type."))
     u"""Any of the elemental types Pokémon and moves can have."""
     __tablename__ = 'types'
     __singlename__ = 'type'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description=u"A unique ID for this type."))
-    identifier = Column(Unicode(8), nullable=False,
+    identifier = Column(Unicode(12), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description=u"The ID of the generation this type first appeared in."))
     damage_class_id = Column(Integer, ForeignKey('move_damage_classes.id'), nullable=True,
         info=dict(description=u"The ID of the damage class this type's moves had before Generation IV, null if not applicable (e.g. ???)."))
         info=dict(description=u"An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description=u"The ID of the generation this type first appeared in."))
     damage_class_id = Column(Integer, ForeignKey('move_damage_classes.id'), nullable=True,
         info=dict(description=u"The ID of the damage class this type's moves had before Generation IV, null if not applicable (e.g. ???)."))
+    name = TextColumn(Unicode(12), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class VersionGroup(TableBase):
     u"""A group of versions, containing either two paired versions (such as Red
 
 class VersionGroup(TableBase):
     u"""A group of versions, containing either two paired versions (such as Red
@@ -1424,7 +1500,7 @@ class VersionGroupRegion(TableBase):
     region_id = Column(Integer, ForeignKey('regions.id'), primary_key=True, nullable=False,
         info=dict(description=u"The ID of the region."))
 
     region_id = Column(Integer, ForeignKey('regions.id'), primary_key=True, nullable=False,
         info=dict(description=u"The ID of the region."))
 
-class Version(TableBase, OfficiallyNamed):
+class Version(TableBase):
     u"""An individual main-series Pokémon game."""
     __tablename__ = 'versions'
     __singlename__ = 'version'
     u"""An individual main-series Pokémon game."""
     __tablename__ = 'versions'
     __singlename__ = 'version'
@@ -1434,6 +1510,8 @@ class Version(TableBase, OfficiallyNamed):
         info=dict(description=u"The ID of the version group this game belongs to."))
     identifier = Column(Unicode(32), nullable=False,
         info=dict(description=u'And identifier', format='identifier'))
         info=dict(description=u"The ID of the version group this game belongs to."))
     identifier = Column(Unicode(32), nullable=False,
         info=dict(description=u'And identifier', format='identifier'))
+    name = TextColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
 ### Relations down here, to avoid ordering problems
 
 
 ### Relations down here, to avoid ordering problems
@@ -1562,20 +1640,10 @@ Move.super_contest_combo_prev = association_proxy('super_contest_combo_second',
 Move.target = relation(MoveTarget, backref='moves')
 Move.type = relation(Type, back_populates='moves')
 
 Move.target = relation(MoveTarget, backref='moves')
 Move.type = relation(Type, back_populates='moves')
 
-Move.effect = markdown.MoveEffectProperty('effect')
-Move.effects = markdown.MoveEffectsProperty('effect')
-Move.short_effect = markdown.MoveEffectProperty('short_effect')
-Move.short_effects = markdown.MoveEffectsProperty('short_effect')
-
 MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
 MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
 MoveChangelog.type = relation(Type, backref='move_changelog')
 
 MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
 MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
 MoveChangelog.type = relation(Type, backref='move_changelog')
 
-MoveChangelog.effect = markdown.MoveEffectProperty('effect')
-MoveChangelog.effects = markdown.MoveEffectsProperty('effect')
-MoveChangelog.short_effect = markdown.MoveEffectProperty('short_effect')
-MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect')
-
 MoveEffect.category_map = relation(MoveEffectCategoryMap)
 MoveEffect.categories = association_proxy('category_map', 'category')
 MoveEffect.changelog = relation(MoveEffectChangelog,
 MoveEffect.category_map = relation(MoveEffectCategoryMap)
 MoveEffect.categories = association_proxy('category_map', 'category')
 MoveEffect.changelog = relation(MoveEffectChangelog,
@@ -1774,93 +1842,239 @@ VersionGroup.regions = association_proxy('version_group_regions', 'region')
 VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')
 
 
 VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')
 
 
-### Add name tables
-for table in list(table_classes):
-    if issubclass(table, OfficiallyNamed):
-        cls = TextColumn
-        info=dict(description="The name", format='plaintext', official=True)
-    elif issubclass(table, UnofficiallyNamed):
-        cls = ProseColumn
-        info=dict(description="The name", format='plaintext', official=False)
-    else:
-        continue
-    table.name = cls(Unicode(class_mapper(table).c.identifier.type.length),
-        plural='names', nullable=False, info=info)
-
 ### Add text/prose tables
 
 ### Add text/prose tables
 
-def makeTextTable(object_table, name_plural, name_singular, columns, lazy):
-    # With "Language", we'd have two language_id. So, rename one to 'lang'
-    safe_name = object_table.__singlename__
-    if safe_name == 'language':
-        safe_name = 'lang'
+default_lang = u'en'
 
 
-    tablename = object_table.__singlename__ + '_' + name_plural
-    singlename = object_table.__singlename__ + '_' + name_singular
+def create_translation_table(_table_name, foreign_class,
+    _language_class=Language, **kwargs):
+    """Creates a table that represents some kind of data attached to the given
+    foreign class, but translated across several languages.  Returns the new
+    table's mapped class.
+TODO give it a __table__ or __init__?
 
 
-    class Strings(object):
-        __tablename__ = tablename
-        __singlename__ = singlename
+    `foreign_class` must have a `__singlename__`, currently only used to create
+    the name of the foreign key column.
+TODO remove this requirement
 
 
-    for name, plural, column in columns:
-        column.name = name
+    Also supports the notion of a default language, which is attached to the
+    session.  This is English by default, for historical and practical reasons.
+
+    Usage looks like this:
 
 
-    table = Table(tablename, metadata,
-            Column(safe_name + '_id', Integer, ForeignKey(object_table.id),
+        class Foo(Base): ...
+
+        create_translation_table('foo_bars', Foo,
+            name = Column(...),
+        )
+
+        # Now you can do the following:
+        foo.name
+        foo.name_map['en']
+        foo.foo_bars['en']
+
+        foo.name_map['en'] = "new name"
+        del foo.name_map['en']
+
+        q.options(joinedload(Foo.default_translation))
+        q.options(joinedload(Foo.foo_bars))
+
+    In the above example, the following attributes are added to Foo:
+
+    - `foo_bars`, a relation to the new table.  It uses a dict-based collection
+      class, where the keys are language identifiers and the values are rows in
+      the created tables.
+    - `foo_bars_local`, a relation to the row in the new table that matches the
+      current default language.
+
+    Note that these are distinct relations.  Even though the former necessarily
+    includes the latter, SQLAlchemy doesn't treat them as linked; loading one
+    will not load the other.  Modifying both within the same transaction has
+    undefined behavior.
+
+    For each column provided, the following additional attributes are added to
+    Foo:
+
+    - `(column)_map`, an association proxy onto `foo_bars`.
+    - `(column)`, an association proxy onto `foo_bars_local`.
+
+    Pardon the naming disparity, but the grammar suffers otherwise.
+
+    Modifying these directly is not likely to be a good idea.
+    """
+    # n.b.: _language_class only exists for the sake of tests, which sometimes
+    # want to create tables entirely separate from the pokedex metadata
+
+    foreign_key_name = foreign_class.__singlename__ + '_id'
+    # A foreign key "language_id" will clash with the language_id we naturally
+    # put in every table.  Rename it something else
+    if foreign_key_name == 'language_id':
+        # TODO change language_id below instead and rename this
+        foreign_key_name = 'lang_id'
+
+    Translations = type(_table_name, (object,), {
+        '_language_identifier': association_proxy('language', 'identifier'),
+    })
+    
+    # Create the table object
+    table = Table(_table_name, foreign_class.__table__.metadata,
+        Column(foreign_key_name, Integer, ForeignKey(foreign_class.id),
+            primary_key=True, nullable=False),
+        Column('language_id', Integer, ForeignKey(_language_class.id),
+            primary_key=True, nullable=False),
+    )
+
+    # Add ye columns
+    # Column objects have a _creation_order attribute in ascending order; use
+    # this to get the (unordered) kwargs sorted correctly
+    kwitems = kwargs.items()
+    kwitems.sort(key=lambda kv: kv[1]._creation_order)
+    for name, column in kwitems:
+        column.name = name
+        table.append_column(column)
+
+    # Construct ye mapper
+    mapper(Translations, table, properties={
+        # TODO change to foreign_id
+        'object_id': synonym(foreign_key_name),
+        # TODO change this as appropriate
+        'language': relation(_language_class,
+            primaryjoin=table.c.language_id == _language_class.id,
+            lazy='joined',
+            innerjoin=True),
+        # TODO does this need to join to the original table?
+    })
+
+    # Add full-table relations to the original class
+    # Class.foo_bars
+    setattr(foreign_class, _table_name, relation(Translations,
+        primaryjoin=foreign_class.id == Translations.object_id,
+        collection_class=attribute_mapped_collection('language'),
+        # TODO
+        lazy='select',
+    ))
+    # Class.foo_bars_local
+    # This is a bit clever; it uses bindparam() to make the join clause
+    # modifiable on the fly.  db sessions know the current language identifier
+    # populates the bindparam.
+    local_relation_name = _table_name + '_local'
+    setattr(foreign_class, local_relation_name, relation(Translations,
+        primaryjoin=and_(
+            foreign_class.id == Translations.object_id,
+            Translations._language_identifier ==
+                bindparam('_default_language', required=True),
+        ),
+        uselist=False,
+        # TODO MORESO HERE
+        lazy='select',
+    ))
+
+    # Add per-column proxies to the original class
+    for name, column in kwitems:
+        # Class.(column) -- accessor for the default language's value
+        setattr(foreign_class, name,
+            association_proxy(local_relation_name, name))
+
+        # Class.(column)_map -- accessor for the language dict
+        # Need a custom creator since Translations doesn't have an init, and
+        # these are passed as *args anyway
+        def creator(language, value):
+            row = Translations()
+            row.language = language
+            setattr(row, name, value)
+            return row
+        setattr(foreign_class, name + '_map',
+            association_proxy(_table_name, name, creator=creator))
+
+    # Done
+    return Translations
+
+def makeTextTable(foreign_table_class, table_suffix_plural, table_suffix_singular, columns, lazy, Language=Language):
+    # With "Language", we'd have two language_id. So, rename one to 'lang'
+    foreign_key_name = foreign_table_class.__singlename__
+    if foreign_key_name == 'language':
+        foreign_key_name = 'lang'
+
+    table_name = foreign_table_class.__singlename__ + '_' + table_suffix_plural
+
+    class TranslatedStringsTable(object):
+        __tablename__ = table_name
+        _attrname = table_suffix_plural
+        _language_identifier = association_proxy('language', 'identifier')
+
+    for column_name, column_name_plural, column in columns:
+        column.name = column_name
+        if not column.nullable:
+            # A Python side default value, so that the strings can be set
+            # one by one without the DB complaining about missing values
+            column.default = ColumnDefault(u'')
+
+    table = Table(table_name, foreign_table_class.__table__.metadata,
+            Column(foreign_key_name + '_id', Integer, ForeignKey(foreign_table_class.id),
                     primary_key=True, nullable=False),
             Column('language_id', Integer, ForeignKey(Language.id),
                     primary_key=True, nullable=False),
             Column('language_id', Integer, ForeignKey(Language.id),
-                    primary_key=True, nullable=False),
+                    primary_key=True, index=True, nullable=False),
             *(column for name, plural, column in columns)
         )
 
             *(column for name, plural, column in columns)
         )
 
-    mapper(Strings, table,
-            properties={
-                    "object_id": synonym(safe_name + '_id'),
-                    "language": relation(
-                            Language,
-                            primaryjoin=table.c.language_id == Language.id,
-                        ),
-                },
-        )
+    mapper(TranslatedStringsTable, table,
+        properties={
+            "object_id": synonym(foreign_key_name + '_id'),
+            "language": relation(Language,
+                primaryjoin=table.c.language_id == Language.id,
+            ),
+            foreign_key_name: relation(foreign_table_class,
+                primaryjoin=(foreign_table_class.id == table.c[foreign_key_name + "_id"]),
+                backref=backref(table_suffix_plural,
+                    collection_class=attribute_mapped_collection('_language_identifier'),
+                    lazy=lazy,
+                ),
+            ),
+        },
+    )
 
     # The relation to the object
 
     # The relation to the object
-    setattr(object_table, name_plural, relation(
-            Strings,
-            primaryjoin=(object_table.id == Strings.object_id),
-            backref=safe_name,
-            collection_class=attribute_mapped_collection('language'),
-            lazy=lazy,
-        ))
-    Strings.object = getattr(Strings, safe_name)
-
-    # Link the tables themselves, so we can get to them
-    Strings.object_table = object_table
-    setattr(object_table, name_singular + '_table', Strings)
-
-    for colname, pluralname, column in columns:
-        # Provide a relation with all the names, and an English accessor
+    TranslatedStringsTable.object = getattr(TranslatedStringsTable, foreign_key_name)
+
+    # Link the tables themselves, so we can get them if needed
+    TranslatedStringsTable.foreign_table_class = foreign_table_class
+    setattr(foreign_table_class, table_suffix_singular + '_table', TranslatedStringsTable)
+
+    for column_name, column_name_plural, column in columns:
+        # Provide a property with all the names, and an English accessor
         # for backwards compatibility
         # for backwards compatibility
-        def scope(colname, pluralname, column):
-            def get_strings(self):
-                return dict(
-                        (l, getattr(t, colname))
-                        for l, t in getattr(self, name_plural).items()
-                    )
+        def text_string_creator(language_code, string):
+            row = TranslatedStringsTable()
+            row._language_identifier = language_code
+            setattr(row, column_name, string)
+            return row
 
 
-            def get_english_string(self):
-                try:
-                    return get_strings(self)['en']
-                except KeyError:
-                    raise AttributeError(colname)
+        setattr(foreign_table_class, column_name_plural,
+            association_proxy(table_suffix_plural, column_name, creator=text_string_creator))
+        setattr(foreign_table_class, column_name, DefaultLangProperty(column_name_plural))
 
 
-            setattr(object_table, pluralname, property(get_strings))
-            setattr(object_table, colname, property(get_english_string))
-        scope(colname, pluralname, column)
+        if column_name == 'name':
+            foreign_table_class.name_table = TranslatedStringsTable
 
 
-        if colname == 'name':
-            object_table.name_table = Strings
+    compile_mappers()
+    return TranslatedStringsTable
 
 
-    return Strings
+class DefaultLangProperty(object):
+    def __init__(self, column_name):
+        self.column_name = column_name
+
+    def __get__(self, instance, cls):
+        if instance:
+            return getattr(instance, self.column_name)[default_lang]
+        else:
+            # TODO I think this is kind of broken
+            return getattr(cls, self.column_name)[default_lang]
+
+    def __set__(self, instance, value):
+        getattr(instance, self.colname)[default_lang] = value
+
+    def __delete__(self, instance):
+        del getattr(instance, self.colname)[default_lang]
 
 for table in list(table_classes):
     # Find all the language-specific columns, keeping them in the order they
 
 for table in list(table_classes):
     # Find all the language-specific columns, keeping them in the order they
@@ -1870,6 +2084,7 @@ for table in list(table_classes):
         column = getattr(table, colname)
         if isinstance(column, LanguageSpecificColumn):
             all_columns.append((colname, column))
         column = getattr(table, colname)
         if isinstance(column, LanguageSpecificColumn):
             all_columns.append((colname, column))
+            delattr(table, colname)
     all_columns.sort(key=lambda pair: pair[1].order)
 
     # Break them into text and prose columns
     all_columns.sort(key=lambda pair: pair[1].order)
 
     # Break them into text and prose columns
@@ -1888,9 +2103,19 @@ for table in list(table_classes):
     if text_columns:
         string_table = makeTextTable(table, 'texts', 'text', text_columns, lazy=False)
     if prose_columns:
     if text_columns:
         string_table = makeTextTable(table, 'texts', 'text', text_columns, lazy=False)
     if prose_columns:
-        string_table = makeTextTable(table, 'prose', 'prose', prose_columns, lazy=True)
+        string_table = makeTextTable(table, 'prose', 'prose', prose_columns, lazy='select')
 
 ### Add language relations
 for table in list(table_classes):
     if issubclass(table, LanguageSpecific):
         table.language = relation(Language, primaryjoin=table.language_id == Language.id)
 
 ### Add language relations
 for table in list(table_classes):
     if issubclass(table, LanguageSpecific):
         table.language = relation(Language, primaryjoin=table.language_id == Language.id)
+
+Move.effect = DefaultLangProperty('effects')
+Move.effects = markdown.MoveEffectsProperty('effect')
+Move.short_effect = DefaultLangProperty('short_effects')
+Move.short_effects = markdown.MoveEffectsProperty('short_effect')
+
+MoveChangelog.effect = DefaultLangProperty('effects')
+MoveChangelog.effects = markdown.MoveEffectsProperty('effect')
+MoveChangelog.short_effect = DefaultLangProperty('short_effects')
+MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect')