summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_10.rst80
-rw-r--r--doc/build/core/metadata.rst17
-rw-r--r--doc/build/core/pooling.rst8
-rw-r--r--doc/build/glossary.rst6
-rw-r--r--examples/vertical/dictlike-polymorphic.py2
-rw-r--r--lib/sqlalchemy/__init__.py1
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py6
-rw-r--r--lib/sqlalchemy/dialects/oracle/cx_oracle.py10
-rw-r--r--lib/sqlalchemy/dialects/postgresql/psycopg2.py1
-rw-r--r--lib/sqlalchemy/engine/reflection.py1
-rw-r--r--lib/sqlalchemy/engine/result.py2
-rw-r--r--lib/sqlalchemy/orm/base.py2
-rw-r--r--lib/sqlalchemy/orm/evaluator.py5
-rw-r--r--lib/sqlalchemy/orm/strategies.py18
-rw-r--r--lib/sqlalchemy/schema.py1
-rw-r--r--lib/sqlalchemy/sql/elements.py2
-rw-r--r--lib/sqlalchemy/sql/schema.py52
-rw-r--r--lib/sqlalchemy/sql/util.py10
-rw-r--r--test/dialect/mssql/test_compiler.py25
-rw-r--r--test/dialect/postgresql/test_reflection.py23
-rw-r--r--test/engine/test_reflection.py25
-rw-r--r--test/ext/test_compiler.py2
-rw-r--r--test/orm/test_evaluator.py38
-rw-r--r--test/orm/test_mapper.py41
-rw-r--r--test/orm/test_query.py19
-rw-r--r--test/sql/test_metadata.py5
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: