summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/__init__.py2
-rw-r--r--lib/sqlalchemy/dialects/mssql/pyodbc.py22
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py62
-rw-r--r--lib/sqlalchemy/dialects/postgresql/psycopg2cffi.py2
-rw-r--r--lib/sqlalchemy/engine/base.py32
-rw-r--r--lib/sqlalchemy/engine/interfaces.py6
-rw-r--r--lib/sqlalchemy/engine/result.py23
-rw-r--r--lib/sqlalchemy/ext/automap.py9
-rw-r--r--lib/sqlalchemy/ext/declarative/api.py13
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py1
-rw-r--r--lib/sqlalchemy/ext/hybrid.py2
-rw-r--r--lib/sqlalchemy/orm/interfaces.py2
-rw-r--r--lib/sqlalchemy/orm/loading.py11
-rw-r--r--lib/sqlalchemy/orm/mapper.py33
-rw-r--r--lib/sqlalchemy/orm/persistence.py25
-rw-r--r--lib/sqlalchemy/orm/properties.py2
-rw-r--r--lib/sqlalchemy/orm/util.py9
-rw-r--r--lib/sqlalchemy/sql/compiler.py13
-rw-r--r--lib/sqlalchemy/sql/dml.py10
-rw-r--r--lib/sqlalchemy/sql/elements.py16
-rw-r--r--lib/sqlalchemy/sql/operators.py8
-rw-r--r--lib/sqlalchemy/sql/schema.py57
-rw-r--r--lib/sqlalchemy/testing/__init__.py3
-rw-r--r--lib/sqlalchemy/testing/assertions.py14
-rw-r--r--lib/sqlalchemy/testing/engines.py11
-rw-r--r--lib/sqlalchemy/testing/plugin/plugin_base.py7
-rw-r--r--lib/sqlalchemy/testing/provision.py3
27 files changed, 328 insertions, 70 deletions
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py
index afddd5941..093e90bbf 100644
--- a/lib/sqlalchemy/__init__.py
+++ b/lib/sqlalchemy/__init__.py
@@ -120,7 +120,7 @@ from .schema import (
from .inspection import inspect
from .engine import create_engine, engine_from_config
-__version__ = '1.0.6'
+__version__ = '1.0.7'
def __go(lcls):
diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py
index ad1e7ae37..7ec8cbaa7 100644
--- a/lib/sqlalchemy/dialects/mssql/pyodbc.py
+++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py
@@ -95,7 +95,7 @@ for unix + PyODBC.
"""
-from .base import MSExecutionContext, MSDialect
+from .base import MSExecutionContext, MSDialect, VARBINARY
from ...connectors.pyodbc import PyODBCConnector
from ... import types as sqltypes, util
import decimal
@@ -174,6 +174,22 @@ class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float):
pass
+class _VARBINARY_pyodbc(VARBINARY):
+ def bind_processor(self, dialect):
+ if dialect.dbapi is None:
+ return None
+
+ DBAPIBinary = dialect.dbapi.Binary
+
+ def process(value):
+ if value is not None:
+ return DBAPIBinary(value)
+ else:
+ # pyodbc-specific
+ return dialect.dbapi.BinaryNull
+ return process
+
+
class MSExecutionContext_pyodbc(MSExecutionContext):
_embedded_scope_identity = False
@@ -230,7 +246,9 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
MSDialect.colspecs,
{
sqltypes.Numeric: _MSNumeric_pyodbc,
- sqltypes.Float: _MSFloat_pyodbc
+ sqltypes.Float: _MSFloat_pyodbc,
+ VARBINARY: _VARBINARY_pyodbc,
+ sqltypes.LargeBinary: _VARBINARY_pyodbc,
}
)
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py
index bc1c3614e..dc7987d74 100644
--- a/lib/sqlalchemy/dialects/postgresql/base.py
+++ b/lib/sqlalchemy/dialects/postgresql/base.py
@@ -401,6 +401,19 @@ The value passed to the keyword argument will be simply passed through to the
underlying CREATE INDEX command, so it *must* be a valid index type for your
version of PostgreSQL.
+.. _postgresql_index_storage:
+
+Index Storage Parameters
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+PostgreSQL allows storage parameters to be set on indexes. The storage
+parameters available depend on the index method used by the index. Storage
+parameters can be specified on :class:`.Index` using the ``postgresql_with``
+keyword argument::
+
+ Index('my_index', my_table.c.data, postgresql_with={"fillfactor": 50})
+
+.. versionadded:: 1.0.6
.. _postgresql_index_concurrently:
@@ -870,6 +883,16 @@ class ARRAY(sqltypes.Concatenable, sqltypes.TypeEngine):
mytable.c.data[2:7]: [1, 2, 3]
})
+ .. note::
+
+ Multi-dimensional support for the ``[]`` operator is not supported
+ in SQLAlchemy 1.0. Please use the :func:`.type_coerce` function
+ to cast an intermediary expression to ARRAY again as a workaround::
+
+ expr = type_coerce(my_array_column[5], ARRAY(Integer))[6]
+
+ Multi-dimensional support will be provided in a future release.
+
:class:`.ARRAY` provides special methods for containment operations,
e.g.::
@@ -1592,6 +1615,13 @@ class PGDDLCompiler(compiler.DDLCompiler):
])
)
+ withclause = index.dialect_options['postgresql']['with']
+
+ if withclause:
+ text += " WITH (%s)" % (', '.join(
+ ['%s = %s' % storage_parameter
+ for storage_parameter in withclause.items()]))
+
whereclause = index.dialect_options["postgresql"]["where"]
if whereclause is not None:
@@ -1921,6 +1951,7 @@ class PGDialect(default.DefaultDialect):
"where": None,
"ops": {},
"concurrently": False,
+ "with": {}
}),
(schema.Table, {
"ignore_search_path": False,
@@ -2609,7 +2640,8 @@ class PGDialect(default.DefaultDialect):
SELECT
i.relname as relname,
ix.indisunique, ix.indexprs, ix.indpred,
- a.attname, a.attnum, NULL, ix.indkey%s
+ a.attname, a.attnum, NULL, ix.indkey%s,
+ i.reloptions, am.amname
FROM
pg_class t
join pg_index ix on t.oid = ix.indrelid
@@ -2617,6 +2649,9 @@ class PGDialect(default.DefaultDialect):
left outer join
pg_attribute a
on t.oid = a.attrelid and %s
+ left outer join
+ pg_am am
+ on i.relam = am.oid
WHERE
t.relkind IN ('r', 'v', 'f', 'm')
and t.oid = :table_oid
@@ -2636,7 +2671,8 @@ class PGDialect(default.DefaultDialect):
SELECT
i.relname as relname,
ix.indisunique, ix.indexprs, ix.indpred,
- a.attname, a.attnum, c.conrelid, ix.indkey::varchar
+ a.attname, a.attnum, c.conrelid, ix.indkey::varchar,
+ i.reloptions, am.amname
FROM
pg_class t
join pg_index ix on t.oid = ix.indrelid
@@ -2649,6 +2685,9 @@ class PGDialect(default.DefaultDialect):
on (ix.indrelid = c.conrelid and
ix.indexrelid = c.conindid and
c.contype in ('p', 'u', 'x'))
+ left outer join
+ pg_am am
+ on i.relam = am.oid
WHERE
t.relkind IN ('r', 'v', 'f', 'm')
and t.oid = :table_oid
@@ -2665,7 +2704,8 @@ class PGDialect(default.DefaultDialect):
sv_idx_name = None
for row in c.fetchall():
- idx_name, unique, expr, prd, col, col_num, conrelid, idx_key = row
+ (idx_name, unique, expr, prd, col,
+ col_num, conrelid, idx_key, options, amname) = row
if expr:
if idx_name != sv_idx_name:
@@ -2691,6 +2731,16 @@ class PGDialect(default.DefaultDialect):
index['unique'] = unique
if conrelid is not None:
index['duplicates_constraint'] = idx_name
+ if options:
+ index['options'] = dict(
+ [option.split("=") for option in options])
+
+ # it *might* be nice to include that this is 'btree' in the
+ # reflection info. But we don't want an Index object
+ # to have a ``postgresql_using`` in it that is just the
+ # default, so for the moment leaving this out.
+ if amname and amname != 'btree':
+ index['amname'] = amname
result = []
for name, idx in indexes.items():
@@ -2701,6 +2751,12 @@ class PGDialect(default.DefaultDialect):
}
if 'duplicates_constraint' in idx:
entry['duplicates_constraint'] = idx['duplicates_constraint']
+ if 'options' in idx:
+ entry.setdefault(
+ 'dialect_options', {})["postgresql_with"] = idx['options']
+ if 'amname' in idx:
+ entry.setdefault(
+ 'dialect_options', {})["postgresql_using"] = idx['amname']
result.append(entry)
return result
diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2cffi.py b/lib/sqlalchemy/dialects/postgresql/psycopg2cffi.py
index f0fe23df3..97f241d2e 100644
--- a/lib/sqlalchemy/dialects/postgresql/psycopg2cffi.py
+++ b/lib/sqlalchemy/dialects/postgresql/psycopg2cffi.py
@@ -37,7 +37,7 @@ class PGDialect_psycopg2cffi(PGDialect_psycopg2):
FEATURE_VERSION_MAP = dict(
native_json=(2, 4, 4),
- native_jsonb=(99, 99, 99),
+ native_jsonb=(2, 7, 1),
sane_multi_rowcount=(2, 4, 4),
array_oid=(2, 4, 4),
hstore_adapter=(2, 4, 4)
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index 59754a436..eaa435d45 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -1531,9 +1531,13 @@ class Transaction(object):
def __init__(self, connection, parent):
self.connection = connection
- self._parent = parent or self
+ self._actual_parent = parent
self.is_active = True
+ @property
+ def _parent(self):
+ return self._actual_parent or self
+
def close(self):
"""Close this :class:`.Transaction`.
@@ -1811,25 +1815,23 @@ class Engine(Connectable, log.Identified):
def dispose(self):
"""Dispose of the connection pool used by this :class:`.Engine`.
+ This has the effect of fully closing all **currently checked in**
+ database connections. Connections that are still checked out
+ will **not** be closed, however they will no longer be associated
+ with this :class:`.Engine`, so when they are closed individually,
+ eventually the :class:`.Pool` which they are associated with will
+ be garbage collected and they will be closed out fully, if
+ not already closed on checkin.
+
A new connection pool is created immediately after the old one has
been disposed. This new pool, like all SQLAlchemy connection pools,
does not make any actual connections to the database until one is
- first requested.
+ first requested, so as long as the :class:`.Engine` isn't used again,
+ no new connections will be made.
- This method has two general use cases:
-
- * When a dropped connection is detected, it is assumed that all
- connections held by the pool are potentially dropped, and
- the entire pool is replaced.
-
- * An application may want to use :meth:`dispose` within a test
- suite that is creating multiple engines.
+ .. seealso::
- It is critical to note that :meth:`dispose` does **not** guarantee
- that the application will release all open database connections - only
- those connections that are checked into the pool are closed.
- Connections which remain checked out or have been detached from
- the engine are not affected.
+ :ref:`engine_disposal`
"""
self.pool.dispose()
diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py
index 73a8b4635..3bad765df 100644
--- a/lib/sqlalchemy/engine/interfaces.py
+++ b/lib/sqlalchemy/engine/interfaces.py
@@ -252,7 +252,9 @@ class Dialect(object):
sequence
a dictionary of the form
- {'name' : str, 'start' :int, 'increment': int}
+ {'name' : str, 'start' :int, 'increment': int, 'minvalue': int,
+ 'maxvalue': int, 'nominvalue': bool, 'nomaxvalue': bool,
+ 'cycle': bool}
Additional column attributes may be present.
"""
@@ -1147,4 +1149,4 @@ class ExceptionContext(object):
.. versionadded:: 1.0.3
- """ \ No newline at end of file
+ """
diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py
index b2b78dee8..74a0fce77 100644
--- a/lib/sqlalchemy/engine/result.py
+++ b/lib/sqlalchemy/engine/result.py
@@ -221,7 +221,7 @@ class ResultMetaData(object):
in enumerate(result_columns)
]
self.keys = [
- elem[1] for elem in result_columns
+ elem[0] for elem in result_columns
]
else:
# case 2 - raw string, or number of columns in result does
@@ -236,7 +236,8 @@ class ResultMetaData(object):
# that SQLAlchemy has used up through 0.9.
if num_ctx_cols:
- result_map = self._create_result_map(result_columns)
+ result_map = self._create_result_map(
+ result_columns, case_sensitive)
raw = []
self.keys = []
@@ -329,10 +330,12 @@ class ResultMetaData(object):
])
@classmethod
- def _create_result_map(cls, result_columns):
+ def _create_result_map(cls, result_columns, case_sensitive=True):
d = {}
for elem in result_columns:
key, rec = elem[0], elem[1:]
+ if not case_sensitive:
+ key = key.lower()
if key in d:
# conflicting keyname, just double up the list
# of objects. this will cause an "ambiguous name"
@@ -492,10 +495,20 @@ class ResultProxy(object):
self._init_metadata()
def _getter(self, key):
- return self._metadata._getter(key)
+ try:
+ getter = self._metadata._getter
+ except AttributeError:
+ return self._non_result(None)
+ else:
+ return getter(key)
def _has_key(self, key):
- return self._metadata._has_key(key)
+ try:
+ has_key = self._metadata._has_key
+ except AttributeError:
+ return self._non_result(None)
+ else:
+ return has_key(key)
def _init_metadata(self):
metadata = self._cursor_description()
diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py
index 1006e7326..330992e56 100644
--- a/lib/sqlalchemy/ext/automap.py
+++ b/lib/sqlalchemy/ext/automap.py
@@ -188,7 +188,7 @@ scheme for class names and a "pluralizer" for collection names using the
"'words_and_underscores' -> 'WordsAndUnderscores'"
return str(tablename[0].upper() + \\
- re.sub(r'_(\w)', lambda m: m.group(1).upper(), tablename[1:]))
+ re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tablename[1:]))
_pluralizer = inflect.engine()
def pluralize_collection(base, local_cls, referred_cls, constraint):
@@ -196,10 +196,9 @@ scheme for class names and a "pluralizer" for collection names using the
"'SomeTerm' -> 'some_terms'"
referred_name = referred_cls.__name__
- uncamelized = referred_name[0].lower() + \\
- re.sub(r'\W',
- lambda m: "_%s" % m.group(0).lower(),
- referred_name[1:])
+ uncamelized = re.sub(r'[A-Z]',
+ lambda m: "_%s" % m.group(0).lower(),
+ referred_name)[1:]
pluralized = _pluralizer.plural(uncamelized)
return pluralized
diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py
index 3d46bd4cb..dfc47ce95 100644
--- a/lib/sqlalchemy/ext/declarative/api.py
+++ b/lib/sqlalchemy/ext/declarative/api.py
@@ -7,7 +7,7 @@
"""Public API functions and helpers for declarative."""
-from ...schema import Table, MetaData
+from ...schema import Table, MetaData, Column
from ...orm import synonym as _orm_synonym, \
comparable_property,\
interfaces, properties, attributes
@@ -525,6 +525,17 @@ class AbstractConcreteBase(ConcreteBase):
mappers.append(mn)
pjoin = cls._create_polymorphic_union(mappers)
+ # For columns that were declared on the class, these
+ # are normally ignored with the "__no_table__" mapping,
+ # unless they have a different attribute key vs. col name
+ # and are in the properties argument.
+ # In that case, ensure we update the properties entry
+ # to the correct column from the pjoin target table.
+ declared_cols = set(to_map.declared_columns)
+ for k, v in list(to_map.properties.items()):
+ if v in declared_cols:
+ to_map.properties[k] = pjoin.c[v.key]
+
to_map.local_table = pjoin
m_args = to_map.mapper_args_fn or dict
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 57eb54f63..57305748c 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -463,7 +463,6 @@ class _MapperConfig(object):
def _prepare_mapper_arguments(self):
properties = self.properties
-
if self.mapper_args_fn:
mapper_args = self.mapper_args_fn()
else:
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index f94c2079e..9c6178264 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -45,7 +45,7 @@ as the class itself::
return self.end - self.start
@hybrid_method
- def contains(self,point):
+ def contains(self, point):
return (self.start <= point) & (point < self.end)
@hybrid_method
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 6cc613baa..cd4a0116d 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -338,7 +338,7 @@ class PropComparator(operators.ColumnOperators):
def __init__(self, prop, parentmapper, adapt_to_entity=None):
self.prop = self.property = prop
- self._parententity = parentmapper
+ self._parententity = adapt_to_entity or parentmapper
self._adapt_to_entity = adapt_to_entity
def __clause_element__(self):
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index 50afaf601..b81e98a58 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -17,6 +17,8 @@ from __future__ import absolute_import
from .. import util
from . import attributes, exc as orm_exc
from ..sql import util as sql_util
+from . import strategy_options
+
from .util import _none_set, state_str
from .base import _SET_DEFERRED_EXPIRED, _DEFER_FOR_STATE
from .. import exc as sa_exc
@@ -612,10 +614,17 @@ def load_scalar_attributes(mapper, state, attribute_names):
result = False
if mapper.inherits and not mapper.concrete:
+ # because we are using Core to produce a select() that we
+ # pass to the Query, we aren't calling setup() for mapped
+ # attributes; in 1.0 this means deferred attrs won't get loaded
+ # by default
statement = mapper._optimized_get_statement(state, attribute_names)
if statement is not None:
result = load_on_ident(
- session.query(mapper).from_statement(statement),
+ session.query(mapper).
+ options(
+ strategy_options.Load(mapper).undefer("*")
+ ).from_statement(statement),
None,
only_load_props=attribute_names,
refresh_state=state
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 468846d40..48fbaae32 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2038,6 +2038,17 @@ class Mapper(InspectionAttr):
returned, inclding :attr:`.synonyms`, :attr:`.column_attrs`,
:attr:`.relationships`, and :attr:`.composites`.
+ .. warning::
+
+ the :attr:`.Mapper.relationships` accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.attrs[somename]`` over
+ ``getattr(mapper.attrs, somename)`` to avoid name collisions.
+
.. seealso::
:attr:`.Mapper.all_orm_descriptors`
@@ -2073,6 +2084,17 @@ class Mapper(InspectionAttr):
referring to the collection of mapped properties via
:attr:`.Mapper.attrs`.
+ .. warning::
+
+ the :attr:`.Mapper.relationships` accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.attrs[somename]`` over
+ ``getattr(mapper.attrs, somename)`` to avoid name collisions.
+
.. versionadded:: 0.8.0
.. seealso::
@@ -2114,6 +2136,17 @@ class Mapper(InspectionAttr):
"""Return a namespace of all :class:`.RelationshipProperty`
properties maintained by this :class:`.Mapper`.
+ .. warning::
+
+ the :attr:`.Mapper.relationships` accessor namespace is an
+ instance of :class:`.OrderedProperties`. This is
+ a dictionary-like object which includes a small number of
+ named methods such as :meth:`.OrderedProperties.items`
+ and :meth:`.OrderedProperties.values`. When
+ accessing attributes dynamically, favor using the dict-access
+ scheme, e.g. ``mapper.attrs[somename]`` over
+ ``getattr(mapper.attrs, somename)`` to avoid name collisions.
+
.. seealso::
:attr:`.Mapper.attrs` - namespace of all :class:`.MapperProperty`
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index a42ed2f7c..0bfee2ece 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -455,12 +455,31 @@ def _collect_update_commands(
if isinstance(value, sql.ClauseElement):
value_params[col] = value
- elif not state.manager[propkey].impl.is_equal(
- value, state.committed_state[propkey]):
+ # guard against values that generate non-__nonzero__
+ # objects for __eq__()
+ elif state.manager[propkey].impl.is_equal(
+ value, state.committed_state[propkey]) is not True:
params[col.key] = value
if update_version_id is not None and \
mapper.version_id_col in mapper._cols_by_table[table]:
+
+ if not bulk and not (params or value_params):
+ # HACK: check for history in other tables, in case the
+ # history is only in a different table than the one
+ # where the version_id_col is. This logic was lost
+ # from 0.9 -> 1.0.0 and restored in 1.0.6.
+ for prop in mapper._columntoproperty.values():
+ history = (
+ state.manager[prop.key].impl.get_history(
+ state, state_dict,
+ attributes.PASSIVE_NO_INITIALIZE))
+ if history.added:
+ break
+ else:
+ # no net change, break
+ continue
+
col = mapper.version_id_col
params[col._label] = update_version_id
@@ -469,7 +488,7 @@ def _collect_update_commands(
val = mapper.version_id_generator(update_version_id)
params[col.key] = val
- if not (params or value_params):
+ elif not (params or value_params):
continue
if bulk:
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 5694f7255..55e02984b 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -245,6 +245,8 @@ class ColumnProperty(StrategizedProperty):
if self.adapter:
return self.adapter(self.prop.columns[0])
else:
+ # no adapter, so we aren't aliased
+ # assert self._parententity is self._parentmapper
return self.prop.columns[0]._annotate({
"parententity": self._parententity,
"parentmapper": self._parententity})
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 823b97239..6d3869679 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -530,7 +530,7 @@ class AliasedInsp(InspectionAttr):
def _adapt_element(self, elem):
return self._adapter.traverse(elem).\
_annotate({
- 'parententity': self.entity,
+ 'parententity': self,
'parentmapper': self.mapper}
)
@@ -839,9 +839,10 @@ class _ORMJoin(expression.Join):
# or implicit ON clause, augment it the same way we'd augment the
# WHERE.
single_crit = right_info.mapper._single_table_criterion
- if right_info.is_aliased_class:
- single_crit = right_info._adapter.traverse(single_crit)
- self.onclause = self.onclause & single_crit
+ if single_crit is not None:
+ if right_info.is_aliased_class:
+ single_crit = right_info._adapter.traverse(single_crit)
+ self.onclause = self.onclause & single_crit
def _splice_into_center(self, other):
"""Splice a join into the center.
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index e9c3d0efa..a036dcc42 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -1270,9 +1270,6 @@ class SQLCompiler(Compiled):
return " AS " + alias_name_text
def _add_to_result_map(self, keyname, name, objects, type_):
- if not self.dialect.case_sensitive:
- keyname = keyname.lower()
-
self._result_columns.append((keyname, name, objects, type_))
def _label_select_column(self, select, column,
@@ -2299,6 +2296,16 @@ class DDLCompiler(Compiled):
text += " INCREMENT BY %d" % create.element.increment
if create.element.start is not None:
text += " START WITH %d" % create.element.start
+ if create.element.minvalue is not None:
+ text += " MINVALUE %d" % create.element.minvalue
+ if create.element.maxvalue is not None:
+ text += " MAXVALUE %d" % create.element.maxvalue
+ if create.element.nominvalue is not None:
+ text += " NO MINVALUE"
+ if create.element.nomaxvalue is not None:
+ text += " NO MAXVALUE"
+ if create.element.cycle is not None:
+ text += " CYCLE"
return text
def visit_drop_sequence(self, drop):
diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py
index a2a564690..6756f1554 100644
--- a/lib/sqlalchemy/sql/dml.py
+++ b/lib/sqlalchemy/sql/dml.py
@@ -262,10 +262,14 @@ class ValuesBase(UpdateBase):
has the effect of using the DBAPI
`executemany() <http://www.python.org/dev/peps/pep-0249/#id18>`_
method, which provides a high-performance system of invoking
- a single-row INSERT statement many times against a series
+ a single-row INSERT or single-criteria UPDATE or DELETE statement
+ many times against a series
of parameter sets. The "executemany" style is supported by
- all database backends, as it does not depend on a special SQL
- syntax.
+ all database backends, and works equally well for INSERT,
+ UPDATE, and DELETE, as it does not depend on a special SQL
+ syntax. See :ref:`execute_multiple` for an introduction to
+ the traditional Core method of multiple parameter set invocation
+ using this system.
.. versionadded:: 0.8
Support for multiple-VALUES INSERT statements.
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 5df736ac7..4af1e4463 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -715,7 +715,14 @@ class ColumnElement(operators.ColumnOperators, ClauseElement):
@util.memoized_property
def comparator(self):
- return self.type.comparator_factory(self)
+ try:
+ comparator_factory = self.type.comparator_factory
+ except AttributeError:
+ raise TypeError(
+ "Object %r associated with '.type' attribute "
+ "is not a TypeEngine class or object" % self.type)
+ else:
+ return comparator_factory(self)
def __getattr__(self, key):
try:
@@ -1847,9 +1854,12 @@ class BooleanClauseList(ClauseList, ColumnElement):
def _construct(cls, operator, continue_on, skip_on, *clauses, **kw):
convert_clauses = []
- clauses = util.coerce_generator_arg(clauses)
+ clauses = [
+ _expression_literal_as_text(clause)
+ for clause in
+ util.coerce_generator_arg(clauses)
+ ]
for clause in clauses:
- clause = _expression_literal_as_text(clause)
if isinstance(clause, continue_on):
continue
diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py
index 51f162c98..17a9d3086 100644
--- a/lib/sqlalchemy/sql/operators.py
+++ b/lib/sqlalchemy/sql/operators.py
@@ -597,6 +597,14 @@ class ColumnOperators(Operators):
"""
return self.reverse_operate(div, other)
+ def __rmod__(self, other):
+ """Implement the ``%`` operator in reverse.
+
+ See :meth:`.ColumnOperators.__mod__`.
+
+ """
+ return self.reverse_operate(mod, other)
+
def between(self, cleft, cright, symmetric=False):
"""Produce a :func:`~.expression.between` clause against
the parent object, given the lower and upper range.
diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py
index a8989627d..137208584 100644
--- a/lib/sqlalchemy/sql/schema.py
+++ b/lib/sqlalchemy/sql/schema.py
@@ -2040,8 +2040,9 @@ class Sequence(DefaultGenerator):
is_sequence = True
- def __init__(self, name, start=None, increment=None, schema=None,
- optional=False, quote=None, metadata=None,
+ def __init__(self, name, start=None, increment=None, minvalue=None,
+ maxvalue=None, nominvalue=None, nomaxvalue=None, cycle=None,
+ schema=None, optional=False, quote=None, metadata=None,
quote_schema=None,
for_update=False):
"""Construct a :class:`.Sequence` object.
@@ -2057,6 +2058,53 @@ class Sequence(DefaultGenerator):
the database as the value of the "INCREMENT BY" clause. If ``None``,
the clause is omitted, which on most platforms indicates an
increment of 1.
+ :param minvalue: the minimum value of the sequence. This
+ value is used when the CREATE SEQUENCE command is emitted to
+ the database as the value of the "MINVALUE" clause. If ``None``,
+ the clause is omitted, which on most platforms indicates a
+ minvalue of 1 and -2^63-1 for ascending and descending sequences,
+ respectively.
+
+ .. versionadded:: 1.0.7
+
+ :param maxvalue: the maximum value of the sequence. This
+ value is used when the CREATE SEQUENCE command is emitted to
+ the database as the value of the "MAXVALUE" clause. If ``None``,
+ the clause is omitted, which on most platforms indicates a
+ maxvalue of 2^63-1 and -1 for ascending and descending sequences,
+ respectively.
+
+ .. versionadded:: 1.0.7
+
+ :param nominvalue: no minimum value of the sequence. This
+ value is used when the CREATE SEQUENCE command is emitted to
+ the database as the value of the "NO MINVALUE" clause. If ``None``,
+ the clause is omitted, which on most platforms indicates a
+ minvalue of 1 and -2^63-1 for ascending and descending sequences,
+ respectively.
+
+ .. versionadded:: 1.0.7
+
+ :param nomaxvalue: no maximum value of the sequence. This
+ value is used when the CREATE SEQUENCE command is emitted to
+ the database as the value of the "NO MAXVALUE" clause. If ``None``,
+ the clause is omitted, which on most platforms indicates a
+ maxvalue of 2^63-1 and -1 for ascending and descending sequences,
+ respectively.
+
+ .. versionadded:: 1.0.7
+
+ :param cycle: allows the sequence to wrap around when the maxvalue
+ or minvalue has been reached by an ascending or descending sequence
+ respectively. This value is used when the CREATE SEQUENCE command
+ is emitted to the database as the "CYCLE" clause. If the limit is
+ reached, the next number generated will be the minvalue or maxvalue,
+ respectively. If cycle=False (the default) any calls to nextval
+ after the sequence has reached its maximum value will return an
+ error.
+
+ .. versionadded:: 1.0.7
+
:param schema: Optional schema name for the sequence, if located
in a schema other than the default.
:param optional: boolean value, when ``True``, indicates that this
@@ -2101,6 +2149,11 @@ class Sequence(DefaultGenerator):
self.name = quoted_name(name, quote)
self.start = start
self.increment = increment
+ self.minvalue = minvalue
+ self.maxvalue = maxvalue
+ self.nominvalue = nominvalue
+ self.nomaxvalue = nomaxvalue
+ self.cycle = cycle
self.optional = optional
if metadata is not None and schema is None and metadata.schema:
self.schema = schema = metadata.schema
diff --git a/lib/sqlalchemy/testing/__init__.py b/lib/sqlalchemy/testing/__init__.py
index 7482e32a1..bd6377eb7 100644
--- a/lib/sqlalchemy/testing/__init__.py
+++ b/lib/sqlalchemy/testing/__init__.py
@@ -21,7 +21,8 @@ def against(*queries):
from .assertions import emits_warning, emits_warning_on, uses_deprecated, \
eq_, ne_, le_, is_, is_not_, startswith_, assert_raises, \
assert_raises_message, AssertsCompiledSQL, ComparesTables, \
- AssertsExecutionResults, expect_deprecated, expect_warnings
+ AssertsExecutionResults, expect_deprecated, expect_warnings, \
+ in_, not_in_
from .util import run_as_contextmanager, rowset, fail, \
provide_metadata, adict, force_drop_names, \
diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py
index e0c02c896..21dc3e71a 100644
--- a/lib/sqlalchemy/testing/assertions.py
+++ b/lib/sqlalchemy/testing/assertions.py
@@ -50,8 +50,6 @@ def expect_warnings_on(db, *messages, **kw):
if isinstance(db, util.string_types) and not spec(config._current):
yield
- elif not _is_excluded(*db):
- yield
else:
with expect_warnings(*messages, **kw):
yield
@@ -90,7 +88,7 @@ def emits_warning_on(db, *messages):
"""
@decorator
def decorate(fn, *args, **kw):
- with expect_warnings_on(db, *messages):
+ with expect_warnings_on(db, assert_=False, *messages):
return fn(*args, **kw)
return decorate
@@ -231,6 +229,16 @@ def is_not_(a, b, msg=None):
assert a is not b, msg or "%r is %r" % (a, b)
+def in_(a, b, msg=None):
+ """Assert a in b, with repr messaging on failure."""
+ assert a in b, msg or "%r not in %r" % (a, b)
+
+
+def not_in_(a, b, msg=None):
+ """Assert a in not b, with repr messaging on failure."""
+ assert a not in b, msg or "%r is in %r" % (a, b)
+
+
def startswith_(a, fragment, msg=None):
"""Assert a.startswith(fragment), with repr messaging on failure."""
assert a.startswith(fragment), msg or "%r does not start with %r" % (
diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py
index 8bd1becbf..1eaf62960 100644
--- a/lib/sqlalchemy/testing/engines.py
+++ b/lib/sqlalchemy/testing/engines.py
@@ -211,6 +211,7 @@ def testing_engine(url=None, options=None):
"""Produce an engine configured by --options with optional overrides."""
from sqlalchemy import create_engine
+ from sqlalchemy.engine.url import make_url
if not options:
use_reaper = True
@@ -218,12 +219,16 @@ def testing_engine(url=None, options=None):
use_reaper = options.pop('use_reaper', True)
url = url or config.db.url
+
+ url = make_url(url)
if options is None:
- options = config.db_opts
+ if config.db is None or url.drivername == config.db.url.drivername:
+ options = config.db_opts
+ else:
+ options = {}
engine = create_engine(url, **options)
- engine._has_events = True # enable event blocks, helps with
- # profiling
+ engine._has_events = True # enable event blocks, helps with profiling
if isinstance(engine.pool, pool.QueuePool):
engine.pool._timeout = 0
diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py
index ef304afa6..6cdec05ad 100644
--- a/lib/sqlalchemy/testing/plugin/plugin_base.py
+++ b/lib/sqlalchemy/testing/plugin/plugin_base.py
@@ -40,7 +40,6 @@ file_config = None
logging = None
-db_opts = {}
include_tags = set()
exclude_tags = set()
options = None
@@ -115,7 +114,6 @@ def memoize_important_follower_config(dict_):
"""
dict_['memoized_config'] = {
- 'db_opts': db_opts,
'include_tags': include_tags,
'exclude_tags': exclude_tags
}
@@ -127,8 +125,7 @@ def restore_important_follower_config(dict_):
This invokes in the follower process.
"""
- global db_opts, include_tags, exclude_tags
- db_opts.update(dict_['memoized_config']['db_opts'])
+ global include_tags, exclude_tags
include_tags.update(dict_['memoized_config']['include_tags'])
exclude_tags.update(dict_['memoized_config']['exclude_tags'])
@@ -268,7 +265,7 @@ def _engine_uri(options, file_config):
for db_url in db_urls:
cfg = provision.setup_config(
- db_url, db_opts, options, file_config, provision.FOLLOWER_IDENT)
+ db_url, options, file_config, provision.FOLLOWER_IDENT)
if not config._current:
cfg.set_as_current(cfg, testing)
diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py
index 8469a0658..77527571b 100644
--- a/lib/sqlalchemy/testing/provision.py
+++ b/lib/sqlalchemy/testing/provision.py
@@ -46,9 +46,10 @@ def configure_follower(follower_ident):
_configure_follower(cfg, follower_ident)
-def setup_config(db_url, db_opts, options, file_config, follower_ident):
+def setup_config(db_url, options, file_config, follower_ident):
if follower_ident:
db_url = _follower_url_from_main(db_url, follower_ident)
+ db_opts = {}
_update_db_opts(db_url, db_opts)
eng = engines.testing_engine(db_url, db_opts)
eng.connect().close()