diff options
26 files changed, 372 insertions, 30 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 352f00c8d..5f9521c47 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -16,7 +16,87 @@ :start-line: 5 .. changelog:: + :version: 1.0.14 + + .. change:: + :tags: bug, engine, postgresql + :tickets: 3716 + + Fixed bug in cross-schema foreign key reflection in conjunction + with the :paramref:`.MetaData.schema` argument, where a referenced + table that is present in the "default" schema would fail since there + would be no way to indicate a :class:`.Table` that has "blank" for + a schema. The special symbol :attr:`.schema.BLANK_SCHEMA` has been + added as an available value for :paramref:`.Table.schema` and + :paramref:`.Sequence.schema`, indicating that the schema name + should be forced to be ``None`` even if :paramref:`.MetaData.schema` + is specified. + + .. change:: + :tags: bug, examples + :tickets: 3704 + + Fixed a regression that occurred in the + examples/vertical/dictlike-polymorphic.py example which prevented it + from running. + +.. changelog:: :version: 1.0.13 + :released: May 16, 2016 + + .. change:: + :tags: bug, orm + :tickets: 3700 + + Fixed bug in "evaluate" strategy of :meth:`.Query.update` and + :meth:`.Query.delete` which would fail to accommodate a bound + parameter with a "callable" value, as which occurs when filtering + by a many-to-one equality expression along a relationship. + + .. change:: + :tags: bug, postgresql + :tickets: 3715 + + Added disconnect detection support for the error string + "SSL error: decryption failed or bad record mac". Pull + request courtesy Iuri de Silvio. + + .. change:: + :tags: bug, mssql + :tickets: 3711 + + Fixed bug where by ROW_NUMBER OVER clause applied for OFFSET + selects in SQL Server would inappropriately substitute a plain column + from the local statement that overlaps with a label name used by + the ORDER BY criteria of the statement. + + .. change:: + :tags: bug, orm + :tickets: 3710 + + Fixed bug whereby the event listeners used for backrefs could + be inadvertently applied multiple times, when using a deep class + inheritance hierarchy in conjunction with mutiple mapper configuration + steps. + + .. change:: + :tags: bug, orm + :tickets: 3706 + + Fixed bug whereby passing a :func:`.text` construct to the + :meth:`.Query.group_by` method would raise an error, instead + of intepreting the object as a SQL fragment. + + .. change:: + :tags: bug, oracle + :tickets: 3705 + + Fixed a bug in the cx_Oracle connect process that caused a TypeError + when the either the user, password or dsn was empty. This prevented + external authentication to Oracle databases, and prevented connecting + to the default dsn. The connect string oracle:// now logs into the + default dsn using the Operating System username, equivalent to + connecting using '/' with sqlplus. .. change:: :tags: bug, oracle diff --git a/doc/build/core/metadata.rst b/doc/build/core/metadata.rst index 5052e0e7f..b37d579f1 100644 --- a/doc/build/core/metadata.rst +++ b/doc/build/core/metadata.rst @@ -303,6 +303,23 @@ described in the individual documentation sections for each dialect. Column, Table, MetaData API --------------------------- +.. attribute:: sqlalchemy.schema.BLANK_SCHEMA + + Symbol indicating that a :class:`.Table` or :class:`.Sequence` + should have 'None' for its schema, even if the parent + :class:`.MetaData` has specified a schema. + + .. seealso:: + + :paramref:`.MetaData.schema` + + :paramref:`.Table.schema` + + :paramref:`.Sequence.schema` + + .. versionadded:: 1.0.14 + + .. autoclass:: Column :members: :inherited-members: diff --git a/doc/build/core/pooling.rst b/doc/build/core/pooling.rst index 2855d1a95..65b5ca9cd 100644 --- a/doc/build/core/pooling.rst +++ b/doc/build/core/pooling.rst @@ -253,6 +253,11 @@ best way to do this is to make use of the # we don't want to bother pinging on these. return + # turn off "close with result". This flag is only used with + # "connectionless" execution, otherwise will be False in any case + save_should_close_with_result = connection.should_close_with_result + connection.should_close_with_result = False + try: # run a SELECT 1. use a core select() so that # the SELECT of a scalar value without a table is @@ -272,6 +277,9 @@ best way to do this is to make use of the connection.scalar(select([1])) else: raise + finally: + # restore "close with result" + connection.should_close_with_result = save_should_close_with_result The above recipe has the advantage that we are making use of SQLAlchemy's facilities for detecting those DBAPI exceptions that are known to indicate diff --git a/doc/build/glossary.rst b/doc/build/glossary.rst index 1f7af02c4..02cdd14a8 100644 --- a/doc/build/glossary.rst +++ b/doc/build/glossary.rst @@ -103,7 +103,7 @@ Glossary Instrumentation refers to the process of augmenting the functionality and attribute set of a particular class. Ideally, the behavior of the class should remain close to a regular - class, except that additional behviors and features are + class, except that additional behaviors and features are made available. The SQLAlchemy :term:`mapping` process, among other things, adds database-enabled :term:`descriptors` to a mapped @@ -246,7 +246,7 @@ Glossary transactional resources", to indicate more explicitly that what we are actually "releasing" is any transactional state which as accumulated upon the connection. In most - situations, the proces of selecting from tables, emitting + situations, the process of selecting from tables, emitting updates, etc. acquires :term:`isolated` state upon that connection as well as potential row or table locks. This state is all local to a particular transaction @@ -360,7 +360,7 @@ Glossary comprises the WHERE clause of the ``SELECT``. FROM clause - The portion of the ``SELECT`` statement which incicates the initial + The portion of the ``SELECT`` statement which indicates the initial source of rows. A simple ``SELECT`` will feature one or more table names in its diff --git a/examples/vertical/dictlike-polymorphic.py b/examples/vertical/dictlike-polymorphic.py index e3d5ba578..7147ac40b 100644 --- a/examples/vertical/dictlike-polymorphic.py +++ b/examples/vertical/dictlike-polymorphic.py @@ -134,7 +134,7 @@ if __name__ == '__main__': char_value = Column(UnicodeText, info={'type': (str, 'string')}) boolean_value = Column(Boolean, info={'type': (bool, 'boolean')}) - class Animal(ProxiedDictMixin._base_class(Base)): + class Animal(ProxiedDictMixin, Base): """an Animal""" __tablename__ = 'animal' diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 1193a1b0b..b1d240edf 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -120,6 +120,7 @@ from .schema import ( ThreadLocalMetaData, UniqueConstraint, DDL, + BLANK_SCHEMA ) diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 051efa719..966700420 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -1155,7 +1155,11 @@ class MSSQLCompiler(compiler.SQLCompiler): 'using an OFFSET or a non-simple ' 'LIMIT clause') - _order_by_clauses = select._order_by_clause.clauses + _order_by_clauses = [ + sql_util.unwrap_label_reference(elem) + for elem in select._order_by_clause.clauses + ] + limit_clause = select._limit_clause offset_clause = select._offset_clause kwargs['select_wraps_for'] = select diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 0c93ced97..cfd942d85 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -914,13 +914,17 @@ class OracleDialect_cx_oracle(OracleDialect): dsn = url.host opts = dict( - user=url.username, - password=url.password, - dsn=dsn, threaded=self.threaded, twophase=self.allow_twophase, ) + if dsn is not None: + opts['dsn'] = dsn + if url.password is not None: + opts['password'] = url.password + if url.username is not None: + opts['user'] = url.username + if util.py2k: if self._cx_oracle_with_unicode: for k, v in opts.items(): diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index fe245b21d..417b7654d 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -718,6 +718,7 @@ class PGDialect_psycopg2(PGDialect): 'connection has been closed unexpectedly', 'SSL SYSCALL error: Bad file descriptor', 'SSL SYSCALL error: EOF detected', + 'SSL error: decryption failed or bad record mac', ]: idx = str_e.find(msg) if idx >= 0 and '"' not in str_e[:idx]: diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index eaa5e2e48..2d524978d 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -693,6 +693,7 @@ class Inspector(object): else: sa_schema.Table(referred_table, table.metadata, autoload=True, autoload_with=self.bind, + schema=sa_schema.BLANK_SCHEMA, **reflection_options ) for column in referred_columns: diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index c9eb53eb1..7fe09b2c7 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -829,7 +829,7 @@ class ResultProxy(object): """Close this ResultProxy. This closes out the underlying DBAPI cursor corresonding - to the statement execution, if one is stil present. Note that the + to the statement execution, if one is still present. Note that the DBAPI cursor is automatically released when the :class:`.ResultProxy` exhausts all available rows. :meth:`.ResultProxy.close` is generally an optional method except in the case when discarding a diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py index 7947cd7d7..8d86fb24e 100644 --- a/lib/sqlalchemy/orm/base.py +++ b/lib/sqlalchemy/orm/base.py @@ -344,7 +344,7 @@ def _attr_as_key(attr): def _orm_columns(entity): insp = inspection.inspect(entity, False) - if hasattr(insp, 'selectable'): + if hasattr(insp, 'selectable') and hasattr(insp.selectable, 'c'): return [c for c in insp.selectable.c] else: return [entity] diff --git a/lib/sqlalchemy/orm/evaluator.py b/lib/sqlalchemy/orm/evaluator.py index 534e7fa8f..6b5da12d9 100644 --- a/lib/sqlalchemy/orm/evaluator.py +++ b/lib/sqlalchemy/orm/evaluator.py @@ -130,5 +130,8 @@ class EvaluatorCompiler(object): (type(clause).__name__, clause.operator)) def visit_bindparam(self, clause): - val = clause.value + if clause.callable: + val = clause.callable() + else: + val = clause.value return lambda obj: val diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 61be59622..826073215 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -71,8 +71,20 @@ def _register_attribute( ) ) + # a single MapperProperty is shared down a class inheritance + # hierarchy, so we set up attribute instrumentation and backref event + # for each mapper down the hierarchy. + + # typically, "mapper" is the same as prop.parent, due to the way + # the configure_mappers() process runs, however this is not strongly + # enforced, and in the case of a second configure_mappers() run the + # mapper here might not be prop.parent; also, a subclass mapper may + # be called here before a superclass mapper. That is, can't depend + # on mappers not already being set up so we have to check each one. + for m in mapper.self_and_descendants: - if prop is m._props.get(prop.key): + if prop is m._props.get(prop.key) and \ + not m.class_manager._attr_has_impl(prop.key): desc = attributes.register_attribute_impl( m.class_, @@ -83,8 +95,8 @@ def _register_attribute( useobject=useobject, extension=attribute_ext, trackparent=useobject and ( - prop.single_parent - or prop.direction is interfaces.ONETOMANY), + prop.single_parent or + prop.direction is interfaces.ONETOMANY), typecallable=typecallable, callable_=callable_, active_history=active_history, diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 5b703f7b6..bd0cbe54e 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -15,6 +15,7 @@ from .sql.base import ( from .sql.schema import ( + BLANK_SCHEMA, CheckConstraint, Column, ColumnDefault, diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 00c2c37ba..e0367f967 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -1206,6 +1206,8 @@ class TextClause(Executable, ClauseElement): @property def selectable(self): + # allows text() to be considered by + # _interpret_as_from return self _hide_froms = [] diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 5e709b1e3..64692644c 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -46,6 +46,17 @@ from . import ddl RETAIN_SCHEMA = util.symbol('retain_schema') +BLANK_SCHEMA = util.symbol( + 'blank_schema', + """Symbol indicating that a :class:`.Table` or :class:`.Sequence` + should have 'None' for its schema, even if the parent + :class:`.MetaData` has specified a schema. + + .. versionadded:: 1.0.14 + + """ +) + def _get_table_key(name, schema): if schema is None: @@ -340,6 +351,17 @@ class Table(DialectKWArgs, SchemaItem, TableClause): the table resides in a schema other than the default selected schema for the engine's database connection. Defaults to ``None``. + If the owning :class:`.MetaData` of this :class:`.Table` specifies + its own :paramref:`.MetaData.schema` parameter, then that schema + name will be applied to this :class:`.Table` if the schema parameter + here is set to ``None``. To set a blank schema name on a :class:`.Table` + that would otherwise use the schema set on the owning :class:`.MetaData`, + specify the special symbol :attr:`.BLANK_SCHEMA`. + + .. versionadded:: 1.0.14 Added the :attr:`.BLANK_SCHEMA` symbol to + allow a :class:`.Table` to have a blank schema name even when the + parent :class:`.MetaData` specifies :paramref:`.MetaData.schema`. + The quoting rules for the schema name are the same as those for the ``name`` parameter, in that quoting is applied for reserved words or case-sensitive names; to enable unconditional quoting for the @@ -371,6 +393,8 @@ class Table(DialectKWArgs, SchemaItem, TableClause): schema = kw.get('schema', None) if schema is None: schema = metadata.schema + elif schema is BLANK_SCHEMA: + schema = None keep_existing = kw.pop('keep_existing', False) extend_existing = kw.pop('extend_existing', False) if 'useexisting' in kw: @@ -442,6 +466,8 @@ class Table(DialectKWArgs, SchemaItem, TableClause): self.schema = kwargs.pop('schema', None) if self.schema is None: self.schema = metadata.schema + elif self.schema is BLANK_SCHEMA: + self.schema = None else: quote_schema = kwargs.pop('quote_schema', None) self.schema = quoted_name(self.schema, quote_schema) @@ -2120,7 +2146,10 @@ class Sequence(DefaultGenerator): .. versionadded:: 1.0.7 :param schema: Optional schema name for the sequence, if located - in a schema other than the default. + in a schema other than the default. The rules for selecting the + schema name when a :class:`.MetaData` is also present are the same + as that of :paramref:`.Table.schema`. + :param optional: boolean value, when ``True``, indicates that this :class:`.Sequence` object only needs to be explicitly generated on backends that don't provide another way to generate primary @@ -2169,7 +2198,9 @@ class Sequence(DefaultGenerator): self.nomaxvalue = nomaxvalue self.cycle = cycle self.optional = optional - if metadata is not None and schema is None and metadata.schema: + if schema is BLANK_SCHEMA: + self.schema = schema = None + elif metadata is not None and schema is None and metadata.schema: self.schema = schema = metadata.schema else: self.schema = quoted_name(schema, quote_schema) @@ -3372,8 +3403,21 @@ class MetaData(SchemaItem): :param schema: The default schema to use for the :class:`.Table`, - :class:`.Sequence`, and other objects associated with this - :class:`.MetaData`. Defaults to ``None``. + :class:`.Sequence`, and potentially other objects associated with + this :class:`.MetaData`. Defaults to ``None``. + + When this value is set, any :class:`.Table` or :class:`.Sequence` + which specifies ``None`` for the schema parameter will instead + have this schema name defined. To build a :class:`.Table` + or :class:`.Sequence` that still has ``None`` for the schema + even when this parameter is present, use the :attr:`.BLANK_SCHEMA` + symbol. + + .. seealso:: + + :paramref:`.Table.schema` + + :paramref:`.Sequence.schema` :param quote_schema: Sets the ``quote_schema`` flag for those :class:`.Table`, diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 5f180646c..24c6f5441 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -176,6 +176,16 @@ def unwrap_order_by(clause): return result +def unwrap_label_reference(element): + def replace(elem): + if isinstance(elem, (_label_reference, _textual_label_reference)): + return elem.element + + return visitors.replacement_traverse( + element, {}, replace + ) + + def expand_column_list_from_order_by(collist, order_by): """Given the columns clause and ORDER BY of a selectable, return a list of column expressions that can be added to the collist diff --git a/test/dialect/mssql/test_compiler.py b/test/dialect/mssql/test_compiler.py index b59ca4fd1..599820492 100644 --- a/test/dialect/mssql/test_compiler.py +++ b/test/dialect/mssql/test_compiler.py @@ -571,6 +571,31 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL): assert t1.c.x in set(c._create_result_map()['x'][1]) assert t1.c.y in set(c._create_result_map()['y'][1]) + def test_offset_dont_misapply_labelreference(self): + m = MetaData() + + t = Table('t', m, Column('x', Integer)) + + expr1 = func.foo(t.c.x).label('x') + expr2 = func.foo(t.c.x).label('y') + + stmt1 = select([expr1]).order_by(expr1.desc()).offset(1) + stmt2 = select([expr2]).order_by(expr2.desc()).offset(1) + + self.assert_compile( + stmt1, + "SELECT anon_1.x FROM (SELECT foo(t.x) AS x, " + "ROW_NUMBER() OVER (ORDER BY foo(t.x) DESC) AS mssql_rn FROM t) " + "AS anon_1 WHERE mssql_rn > :param_1" + ) + + self.assert_compile( + stmt2, + "SELECT anon_1.y FROM (SELECT foo(t.x) AS y, " + "ROW_NUMBER() OVER (ORDER BY foo(t.x) DESC) AS mssql_rn FROM t) " + "AS anon_1 WHERE mssql_rn > :param_1" + ) + def test_limit_zero_offset_using_window(self): t = table('t', column('x', Integer), column('y', Integer)) diff --git a/test/dialect/postgresql/test_reflection.py b/test/dialect/postgresql/test_reflection.py index 8da18108f..4897c4a7e 100644 --- a/test/dialect/postgresql/test_reflection.py +++ b/test/dialect/postgresql/test_reflection.py @@ -582,6 +582,29 @@ class ReflectionTest(fixtures.TestBase): ['test_schema_2.some_other_table', 'test_schema.some_table'])) @testing.provide_metadata + def test_cross_schema_reflection_metadata_uses_schema(self): + # test [ticket:3716] + + metadata = self.metadata + + Table('some_table', metadata, + Column('id', Integer, primary_key=True), + Column('sid', Integer, ForeignKey('some_other_table.id')), + schema='test_schema' + ) + Table('some_other_table', metadata, + Column('id', Integer, primary_key=True), + schema=None + ) + metadata.create_all() + with testing.db.connect() as conn: + meta2 = MetaData(conn, schema="test_schema") + meta2.reflect() + + eq_(set(meta2.tables), set( + ['some_other_table', 'test_schema.some_table'])) + + @testing.provide_metadata def test_uppercase_lowercase_table(self): metadata = self.metadata diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 1f4b2a51c..1dc65d7d0 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -1304,6 +1304,31 @@ class SchemaTest(fixtures.TestBase): 'sa_fake_schema_123'), False) @testing.requires.schemas + @testing.requires.cross_schema_fk_reflection + @testing.provide_metadata + def test_blank_schema_arg(self): + metadata = self.metadata + + Table('some_table', metadata, + Column('id', Integer, primary_key=True), + Column('sid', Integer, sa.ForeignKey('some_other_table.id')), + schema=testing.config.test_schema + ) + Table('some_other_table', metadata, + Column('id', Integer, primary_key=True), + schema=None + ) + metadata.create_all() + with testing.db.connect() as conn: + meta2 = MetaData(conn, schema=testing.config.test_schema) + meta2.reflect() + + eq_(set(meta2.tables), set( + [ + 'some_other_table', + '%s.some_table' % testing.config.test_schema])) + + @testing.requires.schemas @testing.fails_on('sqlite', 'FIXME: unknown') @testing.fails_on('sybase', 'FIXME: unknown') def test_explicit_default_schema(self): diff --git a/test/ext/test_compiler.py b/test/ext/test_compiler.py index 5ed50442f..f381ca185 100644 --- a/test/ext/test_compiler.py +++ b/test/ext/test_compiler.py @@ -127,7 +127,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL): class MyThingy(ColumnClause): pass - @compiles(MyThingy, "psotgresql") + @compiles(MyThingy, 'postgresql') def visit_thingy(thingy, compiler, **kw): return "mythingy" diff --git a/test/orm/test_evaluator.py b/test/orm/test_evaluator.py index 2570f7650..9aae8dd34 100644 --- a/test/orm/test_evaluator.py +++ b/test/orm/test_evaluator.py @@ -1,26 +1,30 @@ -"""Evluating SQL expressions on ORM objects""" -import sqlalchemy as sa -from sqlalchemy import testing -from sqlalchemy import String, Integer, select +"""Evaluating SQL expressions on ORM objects""" + +from sqlalchemy import String, Integer, bindparam from sqlalchemy.testing.schema import Table from sqlalchemy.testing.schema import Column -from sqlalchemy.orm import mapper, create_session -from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy import and_, or_, not_ from sqlalchemy.orm import evaluator +from sqlalchemy.orm import mapper compiler = evaluator.EvaluatorCompiler() + + def eval_eq(clause, testcases=None): evaluator = compiler.process(clause) + def testeval(obj=None, expected_result=None): - assert evaluator(obj) == expected_result, "%s != %r for %s with %r" % (evaluator(obj), expected_result, clause, obj) + assert evaluator(obj) == expected_result, \ + "%s != %r for %s with %r" % ( + evaluator(obj), expected_result, clause, obj) if testcases: - for an_obj,result in testcases: + for an_obj, result in testcases: testeval(an_obj, result) return testeval + class EvaluateTest(fixtures.MappedTest): @classmethod def define_tables(cls, metadata): @@ -54,6 +58,18 @@ class EvaluateTest(fixtures.MappedTest): (User(id=None), None), ]) + def test_compare_to_callable_bind(self): + User = self.classes.User + + eval_eq( + User.name == bindparam('x', callable_=lambda: 'foo'), + testcases=[ + (User(name='foo'), True), + (User(name='bar'), False), + (User(name=None), None), + ] + ) + def test_compare_to_none(self): User = self.classes.User @@ -65,14 +81,16 @@ class EvaluateTest(fixtures.MappedTest): def test_true_false(self): User = self.classes.User - eval_eq(User.name == False, testcases=[ + eval_eq( + User.name == False, testcases=[ (User(name='foo'), False), (User(name=True), False), (User(name=False), True), ] ) - eval_eq(User.name == True, testcases=[ + eval_eq( + User.name == True, testcases=[ (User(name='foo'), False), (User(name=True), True), (User(name=False), False), diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 69a039681..e357a7e25 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -373,6 +373,47 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): }) assert getattr(Foo().__class__, 'name').impl is not None + def test_class_hier_only_instrument_once_multiple_configure(self): + users, addresses = (self.tables.users, self.tables.addresses) + + class A(object): + pass + + class ASub(A): + pass + + class ASubSub(ASub): + pass + + class B(object): + pass + + from sqlalchemy.testing import mock + from sqlalchemy.orm.attributes import register_attribute_impl + + with mock.patch( + "sqlalchemy.orm.attributes.register_attribute_impl", + side_effect=register_attribute_impl + ) as some_mock: + + mapper(A, users, properties={ + 'bs': relationship(B) + }) + mapper(B, addresses) + + configure_mappers() + + mapper(ASub, inherits=A) + mapper(ASubSub, inherits=ASub) + + configure_mappers() + + b_calls = [ + c for c in some_mock.mock_calls if c[1][1] == 'bs' + ] + eq_(len(b_calls), 3) + + def test_check_descriptor_as_method(self): User, users = self.classes.User, self.tables.users diff --git a/test/orm/test_query.py b/test/orm/test_query.py index d79de1d96..34343d78d 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -3248,6 +3248,25 @@ class TextTest(QueryTest, AssertsCompiledSQL): [User(id=7), User(id=8), User(id=9), User(id=10)] ) + def test_group_by_accepts_text(self): + User = self.classes.User + s = create_session() + + q = s.query(User).group_by(text("name")) + self.assert_compile( + q, + "SELECT users.id AS users_id, users.name AS users_name " + "FROM users GROUP BY name" + ) + + def test_orm_columns_accepts_text(self): + from sqlalchemy.orm.base import _orm_columns + t = text("x") + eq_( + _orm_columns(t), + [t] + ) + def test_order_by_w_eager_one(self): User = self.classes.User s = create_session() diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 050929d3d..449956fcd 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -7,7 +7,8 @@ from sqlalchemy import Integer, String, UniqueConstraint, \ CheckConstraint, ForeignKey, MetaData, Sequence, \ ForeignKeyConstraint, PrimaryKeyConstraint, ColumnDefault, Index, event,\ events, Unicode, types as sqltypes, bindparam, \ - Table, Column, Boolean, Enum, func, text, TypeDecorator + Table, Column, Boolean, Enum, func, text, TypeDecorator, \ + BLANK_SCHEMA from sqlalchemy import schema, exc from sqlalchemy.engine import default from sqlalchemy.sql import elements, naming @@ -446,6 +447,7 @@ class MetaDataTest(fixtures.TestBase, ComparesTables): ('t2', m1, 'sch2', None, 'sch2', None), ('t3', m1, 'sch2', True, 'sch2', True), ('t4', m1, 'sch1', None, 'sch1', None), + ('t5', m1, BLANK_SCHEMA, None, None, None), ('t1', m2, None, None, 'sch1', True), ('t2', m2, 'sch2', None, 'sch2', None), ('t3', m2, 'sch2', True, 'sch2', True), @@ -458,6 +460,7 @@ class MetaDataTest(fixtures.TestBase, ComparesTables): ('t2', m4, 'sch2', None, 'sch2', None), ('t3', m4, 'sch2', True, 'sch2', True), ('t4', m4, 'sch1', None, 'sch1', None), + ('t5', m4, BLANK_SCHEMA, None, None, None), ]): kw = {} if schema is not None: |
