From: Mike Bayer Date: Sun, 23 Jun 2013 23:32:54 +0000 (-0400) Subject: docs X-Git-Tag: rel_0_9_0b1~230 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2f747e0ad2e53efec4dc6d050fbeef7f0544f632;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git docs --- diff --git a/doc/build/changelog/migration_09.rst b/doc/build/changelog/migration_09.rst index a5eebb60da..03c8563a95 100644 --- a/doc/build/changelog/migration_09.rst +++ b/doc/build/changelog/migration_09.rst @@ -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 =============== diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 5cfda7cf33..ebcc9a7ed3 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -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 diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index 2fefc348f6..017c8dd047 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -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. - - 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'