This commit is contained in:
Mike Bayer
2013-06-23 19:32:54 -04:00
parent dff5e56e95
commit 2f747e0ad2
3 changed files with 104 additions and 8 deletions
+85
View File
@@ -423,6 +423,91 @@ that will disable the feature based on database version detection.
: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
===============
+1 -1
View File
@@ -730,7 +730,7 @@ class Column(SchemaItem, expression.ColumnClause):
The ``type`` argument may be the second positional argument
or specified by keyword.
If the ``type`` is ``None``, it will first default to the special
If the ``type`` is ``None`` or is omitted, it will first default to the special
type :class:`.NullType`. If and when this :class:`.Column` is
made to refer to another column using :class:`.ForeignKey`
and/or :class:`.ForeignKeyConstraint`, the type of the remote-referenced
+18 -7
View File
@@ -909,6 +909,8 @@ class Variant(TypeDecorator):
.. versionadded:: 0.7.2
.. seealso:: :meth:`.TypeEngine.with_variant` for an example of use.
"""
def __init__(self, base, mapping):
@@ -985,14 +987,23 @@ def adapt_type(typeobj, colspecs):
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.
:class:`.NullType` is used as a default type for those cases where
a type cannot be determined, including:
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.
* 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'