summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES7
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py5
-rw-r--r--lib/sqlalchemy/sql/expression.py90
-rw-r--r--test/ext/declarative/test_inheritance.py24
-rw-r--r--test/sql/test_selectable.py129
5 files changed, 253 insertions, 2 deletions
diff --git a/CHANGES b/CHANGES
index 043c93585..1ff19ce4a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -231,6 +231,13 @@ underneath "0.7.xx".
or remove operation is received on the
now-detached collection. [ticket:2476]
+ - [bug] Declarative can now propagate a column
+ declared on a single-table inheritance subclass
+ up to the parent class' table, when the parent
+ class is itself mapped to a join() or select()
+ statement, directly or via joined inheritane,
+ and not just a Table. [ticket:2549]
+
- [bug] An error is emitted when uselist=False
is combined with a "dynamic" loader.
This is a warning in 0.7.9.
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 100a68678..0348da744 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -244,6 +244,7 @@ def _as_declarative(cls, classname, dict_):
elif inherits:
inherited_mapper = _declared_mapping_info(inherits)
inherited_table = inherited_mapper.local_table
+ inherited_mapped_table = inherited_mapper.mapped_table
if table is None:
# single table inheritance.
@@ -268,6 +269,9 @@ def _as_declarative(cls, classname, dict_):
(c, cls, inherited_table.c[c.name])
)
inherited_table.append_column(c)
+ if inherited_mapped_table is not None and \
+ inherited_mapped_table is not inherited_table:
+ inherited_mapped_table._refresh_for_new_column(c)
mt = _MapperConfig(mapper_cls,
cls, table,
@@ -281,6 +285,7 @@ def _as_declarative(cls, classname, dict_):
class _MapperConfig(object):
configs = util.OrderedDict()
+ mapped_table = None
def __init__(self, mapper_cls,
cls,
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 8217f0542..613705c38 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -2086,7 +2086,6 @@ class _DefaultColumnComparator(object):
return o[0](self, expr, op, other, reverse=True, *o[1:], **kwargs)
-
def _check_literal(self, expr, operator, other):
if isinstance(other, BindParameter) and \
isinstance(other.type, sqltypes.NullType):
@@ -2722,8 +2721,49 @@ class FromClause(Selectable):
self.primary_key = ColumnSet()
self.foreign_keys = set()
+ @property
+ def _cols_populated(self):
+ return '_columns' in self.__dict__
+
def _populate_column_collection(self):
- pass
+ """Called on subclasses to establish the .c collection.
+
+ Each implementation has a different way of establishing
+ this collection.
+
+ """
+
+ def _refresh_for_new_column(self, column):
+ """Given a column added to the .c collection of an underlying
+ selectable, produce the local version of that column, assuming this
+ selectable ultimately should proxy this column.
+
+ this is used to "ping" a derived selectable to add a new column
+ to its .c. collection when a Column has been added to one of the
+ Table objects it ultimtely derives from.
+
+ If the given selectable hasn't populated it's .c. collection yet,
+ it should at least pass on the message to the contained selectables,
+ but it will return None.
+
+ This method is currently used by Declarative to allow Table
+ columns to be added to a partially constructed inheritance
+ mapping that may have already produced joins. The method
+ isn't public right now, as the full span of implications
+ and/or caveats aren't yet clear.
+
+ It's also possible that this functionality could be invoked by
+ default via an event, which would require that
+ selectables maintain a weak referencing collection of all
+ derivations.
+
+ """
+ if not self._cols_populated:
+ return None
+ elif column.key in self.columns and self.columns[column.key] is column:
+ return column
+ else:
+ return None
class BindParameter(ColumnElement):
"""Represent a bind parameter.
@@ -3723,6 +3763,19 @@ class Join(FromClause):
self.foreign_keys.update(itertools.chain(
*[col.foreign_keys for col in columns]))
+ def _refresh_for_new_column(self, column):
+ col = self.left._refresh_for_new_column(column)
+ if col is None:
+ col = self.right._refresh_for_new_column(column)
+ if col is not None:
+ if self._cols_populated:
+ self._columns[col._label] = col
+ self.foreign_keys.add(col)
+ if col.primary_key:
+ self.primary_key.add(col)
+ return col
+ return None
+
def _copy_internals(self, clone=_clone, **kw):
self._reset_exported()
self.left = clone(self.left, **kw)
@@ -3863,6 +3916,16 @@ class Alias(FromClause):
for col in self.element.columns:
col._make_proxy(self)
+ def _refresh_for_new_column(self, column):
+ col = self.element._refresh_for_new_column(column)
+ if col is not None:
+ if not self._cols_populated:
+ return None
+ else:
+ return col._make_proxy(self)
+ else:
+ return None
+
def _copy_internals(self, clone=_clone, **kw):
# don't apply anything to an aliased Table
# for now. May want to drive this from
@@ -4808,6 +4871,16 @@ class CompoundSelect(SelectBase):
proxy.proxies = [c._annotate({'weight': i + 1}) for (i,
c) in enumerate(cols)]
+ def _refresh_for_new_column(self, column):
+ for s in self.selects:
+ s._refresh_for_new_column(column)
+
+ if not self._cols_populated:
+ return None
+
+ raise NotImplementedError("CompoundSelect constructs don't support "
+ "addition of columns to underlying selectables")
+
def _copy_internals(self, clone=_clone, **kw):
self._reset_exported()
self.selects = [clone(s, **kw) for s in self.selects]
@@ -5474,6 +5547,19 @@ class Select(SelectBase):
name=c._label if self.use_labels else None,
key=c._key_label if self.use_labels else None)
+ def _refresh_for_new_column(self, column):
+ for fromclause in self._froms:
+ col = fromclause._refresh_for_new_column(column)
+ if col is not None:
+ if col in self.inner_columns and self._cols_populated:
+ our_label = col._key_label if self.use_labels else col.key
+ if our_label not in self.c:
+ return col._make_proxy(self,
+ name=col._label if self.use_labels else None,
+ key=col._key_label if self.use_labels else None)
+ return None
+ return None
+
def self_group(self, against=None):
"""return a 'grouping' construct as per the ClauseElement
specification.
diff --git a/test/ext/declarative/test_inheritance.py b/test/ext/declarative/test_inheritance.py
index 5a8c8e23e..c7117fb43 100644
--- a/test/ext/declarative/test_inheritance.py
+++ b/test/ext/declarative/test_inheritance.py
@@ -577,6 +577,30 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
eq_(sess.query(Engineer).filter_by(primary_language='cobol'
).one(), Engineer(name='vlad', primary_language='cobol'))
+ def test_single_from_joined_colsonsub(self):
+ class Person(Base, fixtures.ComparableEntity):
+
+ __tablename__ = 'people'
+ id = Column(Integer, primary_key=True,
+ test_needs_autoincrement=True)
+ name = Column(String(50))
+ discriminator = Column('type', String(50))
+ __mapper_args__ = {'polymorphic_on': discriminator}
+
+ class Manager(Person):
+ __tablename__ = 'manager'
+ __mapper_args__ = {'polymorphic_identity': 'manager'}
+ id = Column(Integer, ForeignKey('people.id'), primary_key=True)
+ golf_swing = Column(String(50))
+
+ class Boss(Manager):
+ boss_name = Column(String(50))
+
+ is_(
+ Boss.__mapper__.column_attrs['boss_name'].columns[0],
+ Manager.__table__.c.boss_name
+ )
+
def test_polymorphic_on_converted_from_inst(self):
class A(Base):
__tablename__ = 'A'
diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py
index ef5f99c40..045f6695c 100644
--- a/test/sql/test_selectable.py
+++ b/test/sql/test_selectable.py
@@ -578,6 +578,135 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
Table('t1', MetaData(), c1)
eq_(c1._label, "t1_c1")
+
+class RefreshForNewColTest(fixtures.TestBase):
+ def test_join_uninit(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ j = a.join(b, a.c.x == b.c.y)
+
+ q = column('q')
+ b.append_column(q)
+ j._refresh_for_new_column(q)
+ assert j.c.b_q is q
+
+ def test_join_init(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ j = a.join(b, a.c.x == b.c.y)
+ j.c
+ q = column('q')
+ b.append_column(q)
+ j._refresh_for_new_column(q)
+ assert j.c.b_q is q
+
+
+ def test_join_samename_init(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ j = a.join(b, a.c.x == b.c.y)
+ j.c
+ q = column('x')
+ b.append_column(q)
+ j._refresh_for_new_column(q)
+ assert j.c.b_x is q
+
+ def test_select_samename_init(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ s = select([a, b]).apply_labels()
+ s.c
+ q = column('x')
+ b.append_column(q)
+ s._refresh_for_new_column(q)
+ assert q in s.c.b_x.proxy_set
+
+ def test_aliased_select_samename_uninit(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ s = select([a, b]).apply_labels().alias()
+ q = column('x')
+ b.append_column(q)
+ s._refresh_for_new_column(q)
+ assert q in s.c.b_x.proxy_set
+
+ def test_aliased_select_samename_init(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ s = select([a, b]).apply_labels().alias()
+ s.c
+ q = column('x')
+ b.append_column(q)
+ s._refresh_for_new_column(q)
+ assert q in s.c.b_x.proxy_set
+
+ def test_aliased_select_irrelevant(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ c = table('c', column('z'))
+ s = select([a, b]).apply_labels().alias()
+ s.c
+ q = column('x')
+ c.append_column(q)
+ s._refresh_for_new_column(q)
+ assert 'c_x' not in s.c
+
+ def test_aliased_select_no_cols_clause(self):
+ a = table('a', column('x'))
+ s = select([a.c.x]).apply_labels().alias()
+ s.c
+ q = column('q')
+ a.append_column(q)
+ s._refresh_for_new_column(q)
+ assert 'a_q' not in s.c
+
+ def test_union_uninit(self):
+ a = table('a', column('x'))
+ s1 = select([a])
+ s2 = select([a])
+ s3 = s1.union(s2)
+ q = column('q')
+ a.append_column(q)
+ s3._refresh_for_new_column(q)
+ assert a.c.q in s3.c.q.proxy_set
+
+ def test_union_init_raises(self):
+ a = table('a', column('x'))
+ s1 = select([a])
+ s2 = select([a])
+ s3 = s1.union(s2)
+ s3.c
+ q = column('q')
+ a.append_column(q)
+ assert_raises_message(
+ NotImplementedError,
+ "CompoundSelect constructs don't support addition of "
+ "columns to underlying selectables",
+ s3._refresh_for_new_column, q
+ )
+ def test_nested_join_uninit(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ c = table('c', column('z'))
+ j = a.join(b, a.c.x == b.c.y).join(c, b.c.y == c.c.z)
+
+ q = column('q')
+ b.append_column(q)
+ j._refresh_for_new_column(q)
+ assert j.c.b_q is q
+
+ def test_nested_join_init(self):
+ a = table('a', column('x'))
+ b = table('b', column('y'))
+ c = table('c', column('z'))
+ j = a.join(b, a.c.x == b.c.y).join(c, b.c.y == c.c.z)
+
+ j.c
+ q = column('q')
+ b.append_column(q)
+ j._refresh_for_new_column(q)
+ assert j.c.b_q is q
+
class AnonLabelTest(fixtures.TestBase):
"""Test behaviors fixed by [ticket:2168]."""