:ticket:`1068`
+Columns can reliably get their type from a column referred to via ForeignKey
+----------------------------------------------------------------------------
+
+There's a long standing behavior which says that a :class:`.Column` can be
+declared without a type, as long as that :class:`.Column` is referred to
+by a :class:`.ForeignKeyConstraint`, and the type from the referenced column
+will be copied into this one. The problem has been that this feature never
+worked very well and wasn't maintained. The core issue was that the
+:class:`.ForeignKey` object doesn't know what target :class:`.Column` it
+refers to until it is asked, typically the first time the foreign key is used
+to construct a :class:`.Join`. So until that time, the parent :class:`.Column`
+would not have a type, or more specifically, it would have a default type
+of :class:`.NullType`.
+
+While it's taken a long time, the work to reorganize the initialization of
+:class:`.ForeignKey` objects has been completed such that this feature can
+finally work acceptably. At the core of the change is that the :attr:`.ForeignKey.column`
+attribute no longer lazily initializes the location of the target :class:`.Column`;
+the issue with this system was that the owning :class:`.Column` would be stuck
+with :class:`.NullType` as its type until the :class:`.ForeignKey` happened to
+be used.
+
+In the new version, the :class:`.ForeignKey` coordinates with the eventual
+:class:`.Column` it will refer to using internal attachment events, so that the
+moment the referencing :class:`.Column` is associated with the
+:class:`.MetaData`, all :class:`.ForeignKey` objects that
+refer to it will be sent a message that they need to initialize their parent
+column. This system is more complicated but works more solidly; as a bonus,
+there are now tests in place for a wide variety of :class:`.Column` /
+:class:`.ForeignKey` configuration scenarios and error messages have been
+improved to be very specific to no less than seven different error conditions.
+
+Scenarios which now work correctly include:
+
+1. The type on a :class:`.Column` is immediately present as soon as the
+ target :class:`.Column` becomes associated with the same :class:`.MetaData`;
+ this works no matter which side is configured first::
+
+ >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
+ >>> metadata = MetaData()
+ >>> t2 = Table('t2', metadata, Column('t1id', ForeignKey('t1.id')))
+ >>> t2.c.t1id.type
+ NullType()
+ >>> t1 = Table('t1', metadata, Column('id', Integer, primary_key=True))
+ >>> t2.c.t1id.type
+ Integer()
+
+2. The system now works with :class:`.ForeignKeyConstraint` as well::
+
+ >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKeyConstraint
+ >>> metadata = MetaData()
+ >>> t2 = Table('t2', metadata,
+ ... Column('t1a'), Column('t1b'),
+ ... ForeignKeyConstraint(['t1a', 't1b'], ['t1.a', 't1.b']))
+ >>> t2.c.t1a.type
+ NullType()
+ >>> t2.c.t1b.type
+ NullType()
+ >>> t1 = Table('t1', metadata,
+ ... Column('a', Integer, primary_key=True),
+ ... Column('b', Integer, primary_key=True))
+ >>> t2.c.t1a.type
+ Integer()
+ >>> t2.c.t1b.type
+ Integer()
+
+3. It even works for "multiple hops" - that is, a :class:`.ForeignKey` that refers to a
+ :class:`.Column` that refers to another :class:`.Column`::
+
+ >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
+ >>> metadata = MetaData()
+ >>> t2 = Table('t2', metadata, Column('t1id', ForeignKey('t1.id')))
+ >>> t3 = Table('t3', metadata, Column('t2t1id', ForeignKey('t2.t1id')))
+ >>> t2.c.t1id.type
+ NullType()
+ >>> t3.c.t2t1id.type
+ NullType()
+ >>> t1 = Table('t1', metadata, Column('id', Integer, primary_key=True))
+ >>> t2.c.t1id.type
+ Integer()
+ >>> t3.c.t2t1id.type
+ Integer()
+
+:ticket:`1765`
+
Dialect Changes
===============
.. versionadded:: 0.7.2
+ .. seealso:: :meth:`.TypeEngine.with_variant` for an example of use.
+
"""
def __init__(self, base, mapping):
class NullType(TypeEngine):
"""An unknown type.
- NullTypes will stand in if :class:`~sqlalchemy.Table` reflection
- encounters a column data type unknown to SQLAlchemy. The
- resulting columns are nearly fully usable: the DB-API adapter will
- handle all translation to and from the database data type.
-
- NullType does not have sufficient information to particpate in a
- ``CREATE TABLE`` statement and will raise an exception if
- encountered during a :meth:`~sqlalchemy.Table.create` operation.
+ :class:`.NullType` is used as a default type for those cases where
+ a type cannot be determined, including:
+
+ * During table reflection, when the type of a column is not recognized
+ by the :class:`.Dialect`
+ * When constructing SQL expressions using plain Python objects of
+ unknown types (e.g. ``somecolumn == my_special_object``)
+ * When a new :class:`.Column` is created, and the given type is passed
+ as ``None`` or is not passed at all.
+
+ The :class:`.NullType` can be used within SQL expression invocation
+ without issue, it just has no behavior either at the expression construction
+ level or at the bind-parameter/result processing level. :class:`.NullType`
+ will result in a :class:`.CompileException` if the compiler is asked to render
+ the type itself, such as if it is used in a :func:`.cast` operation
+ or within a schema creation operation such as that invoked by
+ :meth:`.MetaData.create_all` or the :class:`.CreateTable` construct.
"""
__visit_name__ = 'null'