From ad82849bbe4ef329129204d02781f737c0c79fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Wed, 10 Sep 2014 11:34:33 +0300 Subject: implementation for FILTER (WHERE ...) --- lib/sqlalchemy/__init__.py | 1 + lib/sqlalchemy/sql/__init__.py | 1 + lib/sqlalchemy/sql/compiler.py | 6 ++++ lib/sqlalchemy/sql/elements.py | 65 ++++++++++++++++++++++++++++++++++++++++ lib/sqlalchemy/sql/expression.py | 4 ++- lib/sqlalchemy/sql/functions.py | 24 ++++++++++++++- 6 files changed, 99 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 853566172..1af0de3ba 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -7,6 +7,7 @@ from .sql import ( + aggregatefilter, alias, and_, asc, diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 4d013859c..8fbf1b536 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -19,6 +19,7 @@ from .expression import ( Selectable, TableClause, Update, + aggregatefilter, alias, and_, asc, diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 5149fa4fe..6ebd61e9c 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -760,6 +760,12 @@ class SQLCompiler(Compiled): ) ) + def visit_aggregatefilter(self, aggregatefilter, **kwargs): + return "%s FILTER (WHERE %s)" % ( + aggregatefilter.func._compiler_dispatch(self, **kwargs), + aggregatefilter.criterion._compiler_dispatch(self, **kwargs) + ) + def visit_extract(self, extract, **kwargs): field = self.extract_map.get(extract.field, extract.field) return "EXTRACT(%s FROM %s)" % ( diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 8ec0aa700..5562e80d7 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2888,6 +2888,71 @@ class Over(ColumnElement): )) +class AggregateFilter(ColumnElement): + """Represent an aggregate FILTER clause. + + This is a special operator against aggregate functions, + which controls which rows are passed to it. + It's supported only by certain database backends. + + """ + __visit_name__ = 'aggregatefilter' + + criterion = None + + def __init__(self, func, *criterion): + """Produce an :class:`.AggregateFilter` object against a function. + + Used against aggregate functions, + for database backends that support aggregate "FILTER" clause. + + E.g.:: + + from sqlalchemy import aggregatefilter + aggregatefilter(func.count(1), MyClass.name == 'some name') + + Would produce "COUNT(1) FILTER (WHERE myclass.name = 'some name')". + + This function is also available from the :data:`~.expression.func` + construct itself via the :meth:`.FunctionElement.filter` method. + + """ + self.func = func + self.filter(*criterion) + + def filter(self, *criterion): + for criterion in list(criterion): + criterion = _expression_literal_as_text(criterion) + + if self.criterion is not None: + self.criterion = self.criterion & criterion + else: + self.criterion = criterion + + return self + + @util.memoized_property + def type(self): + return self.func.type + + def get_children(self, **kwargs): + return [c for c in + (self.func, self.criterion) + if c is not None] + + def _copy_internals(self, clone=_clone, **kw): + self.func = clone(self.func, **kw) + if self.criterion is not None: + self.criterion = clone(self.criterion, **kw) + + @property + def _from_objects(self): + return list(itertools.chain( + *[c._from_objects for c in (self.func, self.criterion) + if c is not None] + )) + + class Label(ColumnElement): """Represents a column label (AS). diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index d96f048b9..7b22cab3e 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -36,7 +36,7 @@ from .elements import ClauseElement, ColumnElement,\ True_, False_, BinaryExpression, Tuple, TypeClause, Extract, \ Grouping, not_, \ collate, literal_column, between,\ - literal, outparam, type_coerce, ClauseList + literal, outparam, type_coerce, ClauseList, AggregateFilter from .elements import SavepointClause, RollbackToSavepointClause, \ ReleaseSavepointClause @@ -97,6 +97,8 @@ outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin") insert = public_factory(Insert, ".expression.insert") update = public_factory(Update, ".expression.update") delete = public_factory(Delete, ".expression.delete") +aggregatefilter = public_factory( + AggregateFilter, ".expression.aggregatefilter") # internal functions still being called from tests and the ORM, diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 7efb1e916..46f3e27dc 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -12,7 +12,7 @@ from . import sqltypes, schema from .base import Executable, ColumnCollection from .elements import ClauseList, Cast, Extract, _literal_as_binds, \ literal_column, _type_from_args, ColumnElement, _clone,\ - Over, BindParameter + Over, BindParameter, AggregateFilter from .selectable import FromClause, Select, Alias from . import operators @@ -116,6 +116,28 @@ class FunctionElement(Executable, ColumnElement, FromClause): """ return Over(self, partition_by=partition_by, order_by=order_by) + def filter(self, *criterion): + """Produce a FILTER clause against this function. + + Used against aggregate functions, + for database backends that support aggregate "FILTER" clause. + + The expression:: + + func.count(1).filter(True) + + is shorthand for:: + + from sqlalchemy import aggregatefilter + aggregatefilter(func.count(1), True) + + See :func:`~.expression.aggregatefilter` for a full description. + + """ + if not criterion: + return self + return AggregateFilter(self, *criterion) + @property def _from_objects(self): return self.clauses._from_objects -- cgit v1.2.1 From ab1c25266dd49f087b5fff316b6ba6fb610b1d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 11 Sep 2014 15:29:33 +0300 Subject: renamed aggregatefilter to funcfilter, since it is that --- lib/sqlalchemy/__init__.py | 2 +- lib/sqlalchemy/sql/__init__.py | 2 +- lib/sqlalchemy/sql/compiler.py | 6 +++--- lib/sqlalchemy/sql/elements.py | 18 +++++++++--------- lib/sqlalchemy/sql/expression.py | 6 +++--- lib/sqlalchemy/sql/functions.py | 14 +++++++------- 6 files changed, 24 insertions(+), 24 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 1af0de3ba..d184e1fbf 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -7,7 +7,6 @@ from .sql import ( - aggregatefilter, alias, and_, asc, @@ -26,6 +25,7 @@ from .sql import ( extract, false, func, + funcfilter, insert, intersect, intersect_all, diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 8fbf1b536..351e08d0b 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -19,7 +19,6 @@ from .expression import ( Selectable, TableClause, Update, - aggregatefilter, alias, and_, asc, @@ -39,6 +38,7 @@ from .expression import ( false, False_, func, + funcfilter, insert, intersect, intersect_all, diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 6ebd61e9c..d59012d12 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -760,10 +760,10 @@ class SQLCompiler(Compiled): ) ) - def visit_aggregatefilter(self, aggregatefilter, **kwargs): + def visit_funcfilter(self, funcfilter, **kwargs): return "%s FILTER (WHERE %s)" % ( - aggregatefilter.func._compiler_dispatch(self, **kwargs), - aggregatefilter.criterion._compiler_dispatch(self, **kwargs) + funcfilter.func._compiler_dispatch(self, **kwargs), + funcfilter.criterion._compiler_dispatch(self, **kwargs) ) def visit_extract(self, extract, **kwargs): diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 5562e80d7..5ac16ab7a 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2888,28 +2888,28 @@ class Over(ColumnElement): )) -class AggregateFilter(ColumnElement): - """Represent an aggregate FILTER clause. +class FunctionFilter(ColumnElement): + """Represent a function FILTER clause. - This is a special operator against aggregate functions, + This is a special operator against aggregate and window functions, which controls which rows are passed to it. It's supported only by certain database backends. """ - __visit_name__ = 'aggregatefilter' + __visit_name__ = 'funcfilter' criterion = None def __init__(self, func, *criterion): - """Produce an :class:`.AggregateFilter` object against a function. + """Produce an :class:`.FunctionFilter` object against a function. - Used against aggregate functions, - for database backends that support aggregate "FILTER" clause. + Used against aggregate and window functions, + for database backends that support the "FILTER" clause. E.g.:: - from sqlalchemy import aggregatefilter - aggregatefilter(func.count(1), MyClass.name == 'some name') + from sqlalchemy import funcfilter + funcfilter(func.count(1), MyClass.name == 'some name') Would produce "COUNT(1) FILTER (WHERE myclass.name = 'some name')". diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 7b22cab3e..2e10b7370 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -36,7 +36,7 @@ from .elements import ClauseElement, ColumnElement,\ True_, False_, BinaryExpression, Tuple, TypeClause, Extract, \ Grouping, not_, \ collate, literal_column, between,\ - literal, outparam, type_coerce, ClauseList, AggregateFilter + literal, outparam, type_coerce, ClauseList, FunctionFilter from .elements import SavepointClause, RollbackToSavepointClause, \ ReleaseSavepointClause @@ -97,8 +97,8 @@ outerjoin = public_factory(Join._create_outerjoin, ".expression.outerjoin") insert = public_factory(Insert, ".expression.insert") update = public_factory(Update, ".expression.update") delete = public_factory(Delete, ".expression.delete") -aggregatefilter = public_factory( - AggregateFilter, ".expression.aggregatefilter") +funcfilter = public_factory( + FunctionFilter, ".expression.funcfilter") # internal functions still being called from tests and the ORM, diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 46f3e27dc..a07eca8c6 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -12,7 +12,7 @@ from . import sqltypes, schema from .base import Executable, ColumnCollection from .elements import ClauseList, Cast, Extract, _literal_as_binds, \ literal_column, _type_from_args, ColumnElement, _clone,\ - Over, BindParameter, AggregateFilter + Over, BindParameter, FunctionFilter from .selectable import FromClause, Select, Alias from . import operators @@ -119,8 +119,8 @@ class FunctionElement(Executable, ColumnElement, FromClause): def filter(self, *criterion): """Produce a FILTER clause against this function. - Used against aggregate functions, - for database backends that support aggregate "FILTER" clause. + Used against aggregate and window functions, + for database backends that support the "FILTER" clause. The expression:: @@ -128,15 +128,15 @@ class FunctionElement(Executable, ColumnElement, FromClause): is shorthand for:: - from sqlalchemy import aggregatefilter - aggregatefilter(func.count(1), True) + from sqlalchemy import funcfilter + funcfilter(func.count(1), True) - See :func:`~.expression.aggregatefilter` for a full description. + See :func:`~.expression.funcfilter` for a full description. """ if not criterion: return self - return AggregateFilter(self, *criterion) + return FunctionFilter(self, *criterion) @property def _from_objects(self): -- cgit v1.2.1 From 52a095ba6675f5f5807a1dc655b4ae32b9999f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 11 Sep 2014 15:39:56 +0300 Subject: allow windowing filtered functions --- lib/sqlalchemy/sql/elements.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 5ac16ab7a..62fe6553a 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2931,6 +2931,26 @@ class FunctionFilter(ColumnElement): return self + def over(self, partition_by=None, order_by=None): + """Produce an OVER clause against this filtered function. + + Used against aggregate or so-called "window" functions, + for database backends that support window functions. + + The expression:: + + func.rank().filter(MyClass.y > 5).over(order_by='x') + + is shorthand for:: + + from sqlalchemy import over, funcfilter + over(funcfilter(func.rank(), MyClass.y > 5), order_by='x') + + See :func:`~.expression.over` for a full description. + + """ + return Over(self, partition_by=partition_by, order_by=order_by) + @util.memoized_property def type(self): return self.func.type -- cgit v1.2.1 From 89fc7d65b9ac12dd70d48c8d3be04bd50e696ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 11 Sep 2014 15:47:24 +0300 Subject: documentation indentation fix --- lib/sqlalchemy/sql/elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 62fe6553a..c1c4fc1e1 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2908,8 +2908,8 @@ class FunctionFilter(ColumnElement): E.g.:: - from sqlalchemy import funcfilter - funcfilter(func.count(1), MyClass.name == 'some name') + from sqlalchemy import funcfilter + funcfilter(func.count(1), MyClass.name == 'some name') Would produce "COUNT(1) FILTER (WHERE myclass.name = 'some name')". -- cgit v1.2.1 From 76c06aa65345b47af38a0a1d20638dfbc890b640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 11 Sep 2014 15:49:51 +0300 Subject: method documentation typo fix --- lib/sqlalchemy/sql/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index c1c4fc1e1..53838358d 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2901,7 +2901,7 @@ class FunctionFilter(ColumnElement): criterion = None def __init__(self, func, *criterion): - """Produce an :class:`.FunctionFilter` object against a function. + """Produce a :class:`.FunctionFilter` object against a function. Used against aggregate and window functions, for database backends that support the "FILTER" clause. -- cgit v1.2.1 From 7fa21b22989f6d53ff70a8df71fc6d210c556e07 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Wed, 10 Sep 2014 07:37:59 -0700 Subject: Reflect unique constraints when reflecting a Table object Calls to reflect a table did not create any UniqueConstraint objects. The reflection core made no calls to get_unique_constraints and as a result, the sqlite dialect would never reflect any unique constraints. MySQL transparently converts unique constraints into unique indexes, but SQLAlchemy would reflect those as an Index object and as a UniqueConstraint. The reflection core will now deduplicate the unique constraints. PostgreSQL would reflect unique constraints as an Index object and as a UniqueConstraint object. The reflection core will now deduplicate the unique indexes. --- lib/sqlalchemy/dialects/mysql/base.py | 3 ++- lib/sqlalchemy/dialects/postgresql/base.py | 18 +++++++++---- lib/sqlalchemy/engine/reflection.py | 34 +++++++++++++++++++++++++ lib/sqlalchemy/testing/suite/test_reflection.py | 13 +++++++--- 4 files changed, 58 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 7ccd59abb..2f85a3626 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2590,7 +2590,8 @@ class MySQLDialect(default.DefaultDialect): return [ { 'name': key['name'], - 'column_names': [col[0] for col in key['columns']] + 'column_names': [col[0] for col in key['columns']], + 'duplicates_index': key['name'], } for key in parsed_state.keys if key['type'] == 'UNIQUE' diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index b9a0d461b..556493b3c 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -2471,14 +2471,19 @@ class PGDialect(default.DefaultDialect): SELECT i.relname as relname, ix.indisunique, ix.indexprs, ix.indpred, - a.attname, a.attnum, ix.indkey%s + a.attname, a.attnum, c.conrelid, ix.indkey%s FROM pg_class t join pg_index ix on t.oid = ix.indrelid - join pg_class i on i.oid=ix.indexrelid + join pg_class i on i.oid = ix.indexrelid left outer join pg_attribute a - on t.oid=a.attrelid and %s + on t.oid = a.attrelid and %s + left outer join + pg_constraint c + on (ix.indrelid = c.conrelid and + ix.indexrelid = c.conindid and + c.contype in ('p', 'u', 'x')) WHERE t.relkind IN ('r', 'v', 'f', 'm') and t.oid = :table_oid @@ -2501,7 +2506,7 @@ class PGDialect(default.DefaultDialect): sv_idx_name = None for row in c.fetchall(): - idx_name, unique, expr, prd, col, col_num, idx_key = row + idx_name, unique, expr, prd, col, col_num, conrelid, idx_key = row if expr: if idx_name != sv_idx_name: @@ -2523,11 +2528,14 @@ class PGDialect(default.DefaultDialect): index['cols'][col_num] = col index['key'] = [int(k.strip()) for k in idx_key.split()] index['unique'] = unique + index['duplicates_constraint'] = (None if conrelid is None + else idx_name) return [ {'name': name, 'unique': idx['unique'], - 'column_names': [idx['cols'][i] for i in idx['key']]} + 'column_names': [idx['cols'][i] for i in idx['key']], + 'duplicates_constraint': idx['duplicates_constraint']} for name, idx in indexes.items() ] diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index c0a3240a5..330fc2b19 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -603,12 +603,15 @@ class Inspector(object): columns = index_d['column_names'] unique = index_d['unique'] flavor = index_d.get('type', 'index') + duplicates = index_d.get('duplicates_constraint') if include_columns and \ not set(columns).issubset(include_columns): util.warn( "Omitting %s key for (%s), key covers omitted columns." % (flavor, ', '.join(columns))) continue + if duplicates: + continue # look for columns by orig name in cols_by_orig_name, # but support columns that are in-Python only as fallback idx_cols = [] @@ -626,3 +629,34 @@ class Inspector(object): idx_cols.append(idx_col) sa_schema.Index(name, *idx_cols, **dict(unique=unique)) + + # Unique Constraints + constraints = self.get_unique_constraints(table_name, schema) + for const_d in constraints: + conname = const_d['name'] + columns = const_d['column_names'] + duplicates = const_d.get('duplicates_index') + if include_columns and \ + not set(columns).issubset(include_columns): + util.warn( + "Omitting unique constraint key for (%s), " + "key covers omitted columns." % + ', '.join(columns)) + continue + if duplicates: + continue + # look for columns by orig name in cols_by_orig_name, + # but support columns that are in-Python only as fallback + constrained_cols = [] + for c in columns: + try: + constrained_col = cols_by_orig_name[c] \ + if c in cols_by_orig_name else table.c[c] + except KeyError: + util.warn( + "unique constraint key '%s' was not located in " + "columns for table '%s'" % (c, table_name)) + else: + constrained_cols.append(constrained_col) + table.append_constraint( + sa_schema.UniqueConstraint(*constrained_cols, name=conname)) diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py index 690a880bb..bd0be5738 100644 --- a/lib/sqlalchemy/testing/suite/test_reflection.py +++ b/lib/sqlalchemy/testing/suite/test_reflection.py @@ -487,10 +487,12 @@ class ComponentReflectionTest(fixtures.TablesTest): @testing.requires.temp_table_reflection def test_get_temp_table_unique_constraints(self): insp = inspect(self.metadata.bind) - eq_( - insp.get_unique_constraints('user_tmp'), - [{'column_names': ['name'], 'name': 'user_tmp_uq'}] - ) + reflected = insp.get_unique_constraints('user_tmp') + for refl in reflected: + # Different dialects handle duplicate index and constraints + # differently, so ignore this flag + refl.pop('duplicates_index', None) + eq_(reflected, [{'column_names': ['name'], 'name': 'user_tmp_uq'}]) @testing.requires.temp_table_reflection def test_get_temp_table_indexes(self): @@ -544,6 +546,9 @@ class ComponentReflectionTest(fixtures.TablesTest): ) for orig, refl in zip(uniques, reflected): + # Different dialects handle duplicate index and constraints + # differently, so ignore this flag + refl.pop('duplicates_index', None) eq_(orig, refl) @testing.provide_metadata -- cgit v1.2.1 From ce52dd9e3b71f2074d7821fe62803d4e0eefe512 Mon Sep 17 00:00:00 2001 From: ndparker Date: Tue, 23 Sep 2014 23:28:11 +0200 Subject: improve exception vs. exit handling --- lib/sqlalchemy/dialects/mssql/base.py | 2 ++ lib/sqlalchemy/dialects/mysql/base.py | 4 ++++ lib/sqlalchemy/dialects/mysql/mysqlconnector.py | 2 ++ lib/sqlalchemy/engine/base.py | 2 ++ lib/sqlalchemy/orm/mapper.py | 2 ++ lib/sqlalchemy/orm/state.py | 4 ++-- lib/sqlalchemy/pool.py | 8 ++++---- lib/sqlalchemy/sql/elements.py | 2 ++ lib/sqlalchemy/sql/schema.py | 4 ++-- lib/sqlalchemy/testing/provision.py | 10 ++++++++++ lib/sqlalchemy/util/langhelpers.py | 6 ++++++ 11 files changed, 38 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index ba3050ae5..ade2d00cb 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -846,6 +846,8 @@ class MSExecutionContext(default.DefaultExecutionContext): "SET IDENTITY_INSERT %s OFF" % self.dialect.identifier_preparer. format_table( self.compiled.statement.table))) + except (SystemExit, KeyboardInterrupt): + raise except: pass diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 7ccd59abb..0994e2416 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2317,6 +2317,8 @@ class MySQLDialect(default.DefaultDialect): # basic operations via autocommit fail. try: dbapi_connection.commit() + except (SystemExit, KeyboardInterrupt): + raise except: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args @@ -2329,6 +2331,8 @@ class MySQLDialect(default.DefaultDialect): try: dbapi_connection.rollback() + except (SystemExit, KeyboardInterrupt): + raise except: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args diff --git a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py index e51e80005..afa61d85b 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -103,6 +103,8 @@ class MySQLDialect_mysqlconnector(MySQLDialect): 'client_flags', ClientFlag.get_default()) client_flags |= ClientFlag.FOUND_ROWS opts['client_flags'] = client_flags + except (SystemExit, KeyboardInterrupt): + raise except: pass return [[], opts] diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index d2cc8890f..b3460c240 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1135,6 +1135,8 @@ class Connection(Connectable): per_fn = fn(ctx) if per_fn is not None: ctx.chained_exception = newraise = per_fn + except (SystemExit, KeyboardInterrupt): + raise except Exception as _raised: # handler raises an exception - stop processing newraise = _raised diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index a59a38a5b..bd28975dd 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2649,6 +2649,8 @@ def configure_mappers(): mapper._expire_memoizations() mapper.dispatch.mapper_configured( mapper, mapper.class_) + except (SystemExit, KeyboardInterrupt): + raise except: exc = sys.exc_info()[1] if not hasattr(exc, '_configure_failed'): diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 3c12fda1a..4756f1707 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -258,8 +258,8 @@ class InstanceState(interfaces.InspectionAttr): try: return manager.original_init(*mixed[1:], **kwargs) except: - manager.dispatch.init_failure(self, args, kwargs) - raise + with util.safe_reraise(): + manager.dispatch.init_failure(self, args, kwargs) def get_history(self, key, passive): return self.manager[key].impl.get_history(self, self.dict, passive) diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index bc9affe4a..0c162e984 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -441,8 +441,8 @@ class _ConnectionRecord(object): try: dbapi_connection = rec.get_connection() except: - rec.checkin() - raise + with util.safe_reraise(): + rec.checkin() echo = pool._should_log_debug() fairy = _ConnectionFairy(dbapi_connection, rec, echo) rec.fairy_ref = weakref.ref( @@ -962,8 +962,8 @@ class QueuePool(Pool): try: return self._create_connection() except: - self._dec_overflow() - raise + with util.safe_reraise(): + self._dec_overflow() else: return self._do_get() diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 8ec0aa700..8e18a22fe 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -3491,6 +3491,8 @@ def _string_or_unprintable(element): else: try: return str(element) + except (SystemExit, KeyboardInterrupt): + raise except: return "unprintable element %r" % element diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index d9fd37f92..9afc31be8 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -412,8 +412,8 @@ class Table(DialectKWArgs, SchemaItem, TableClause): table.dispatch.after_parent_attach(table, metadata) return table except: - metadata._remove_table(name, schema) - raise + with util.safe_reraise(): + metadata._remove_table(name, schema) @property @util.deprecated('0.9', 'Use ``table.schema.quote``') diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py index 0bcdad959..64688d6b5 100644 --- a/lib/sqlalchemy/testing/provision.py +++ b/lib/sqlalchemy/testing/provision.py @@ -120,6 +120,8 @@ def _pg_create_db(cfg, eng, ident): isolation_level="AUTOCOMMIT") as conn: try: _pg_drop_db(cfg, conn, ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass currentdb = conn.scalar("select current_database()") @@ -131,6 +133,8 @@ def _mysql_create_db(cfg, eng, ident): with eng.connect() as conn: try: _mysql_drop_db(cfg, conn, ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass conn.execute("CREATE DATABASE %s" % ident) @@ -173,14 +177,20 @@ def _mysql_drop_db(cfg, eng, ident): with eng.connect() as conn: try: conn.execute("DROP DATABASE %s_test_schema" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass try: conn.execute("DROP DATABASE %s_test_schema_2" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass try: conn.execute("DROP DATABASE %s" % ident) + except (SystemExit, KeyboardInterrupt): + raise except: pass diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 76f85f605..75c6e7b46 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -490,6 +490,8 @@ def generic_repr(obj, additional_kw=(), to_inspect=None, omit_kwarg=()): val = getattr(obj, arg, missing) if val is not missing and val != defval: output.append('%s=%r' % (arg, val)) + except (SystemExit, KeyboardInterrupt): + raise except: pass @@ -499,6 +501,8 @@ def generic_repr(obj, additional_kw=(), to_inspect=None, omit_kwarg=()): val = getattr(obj, arg, missing) if val is not missing and val != defval: output.append('%s=%r' % (arg, val)) + except (SystemExit, KeyboardInterrupt): + raise except: pass @@ -1185,6 +1189,8 @@ def warn_exception(func, *args, **kwargs): """ try: return func(*args, **kwargs) + except (SystemExit, KeyboardInterrupt): + raise except: warn("%s('%s') ignored" % sys.exc_info()[0:2]) -- cgit v1.2.1 From 690532131d8ce8250c62f1d3e27405902df03e70 Mon Sep 17 00:00:00 2001 From: ndparker Date: Thu, 2 Oct 2014 22:00:31 +0200 Subject: cleanup exception handling - use new exception hierarchy (since python 2.5) --- lib/sqlalchemy/dialects/mssql/base.py | 4 +--- lib/sqlalchemy/dialects/mysql/base.py | 8 ++------ lib/sqlalchemy/dialects/mysql/mysqlconnector.py | 4 +--- lib/sqlalchemy/engine/base.py | 4 ---- lib/sqlalchemy/exc.py | 6 +++--- lib/sqlalchemy/orm/mapper.py | 4 +--- lib/sqlalchemy/pool.py | 12 ++++-------- lib/sqlalchemy/sql/elements.py | 4 +--- lib/sqlalchemy/testing/engines.py | 4 ---- lib/sqlalchemy/testing/provision.py | 20 +++++--------------- lib/sqlalchemy/util/langhelpers.py | 12 +++--------- 11 files changed, 21 insertions(+), 61 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index ade2d00cb..dad02ee0f 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -846,9 +846,7 @@ class MSExecutionContext(default.DefaultExecutionContext): "SET IDENTITY_INSERT %s OFF" % self.dialect.identifier_preparer. format_table( self.compiled.statement.table))) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass def get_result_proxy(self): diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 0994e2416..277644022 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2317,9 +2317,7 @@ class MySQLDialect(default.DefaultDialect): # basic operations via autocommit fail. try: dbapi_connection.commit() - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args if args and args[0] == 1064: @@ -2331,9 +2329,7 @@ class MySQLDialect(default.DefaultDialect): try: dbapi_connection.rollback() - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: if self.server_version_info < (3, 23, 15): args = sys.exc_info()[1].args if args and args[0] == 1064: diff --git a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py index afa61d85b..6077ce53e 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -103,9 +103,7 @@ class MySQLDialect_mysqlconnector(MySQLDialect): 'client_flags', ClientFlag.get_default()) client_flags |= ClientFlag.FOUND_ROWS opts['client_flags'] = client_flags - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass return [[], opts] diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index b3460c240..220679c12 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1055,8 +1055,6 @@ class Connection(Connectable): """ try: cursor.close() - except (SystemExit, KeyboardInterrupt): - raise except Exception: # log the error through the connection pool's logger. self.engine.pool.logger.error( @@ -1135,8 +1133,6 @@ class Connection(Connectable): per_fn = fn(ctx) if per_fn is not None: ctx.chained_exception = newraise = per_fn - except (SystemExit, KeyboardInterrupt): - raise except Exception as _raised: # handler raises an exception - stop processing newraise = _raised diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index a82bae33f..5d35dc2e7 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -280,7 +280,9 @@ class DBAPIError(StatementError): connection_invalidated=False): # Don't ever wrap these, just return them directly as if # DBAPIError didn't exist. - if isinstance(orig, (KeyboardInterrupt, SystemExit, DontWrapMixin)): + if (isinstance(orig, BaseException) and + not isinstance(orig, Exception)) or \ + isinstance(orig, DontWrapMixin): return orig if orig is not None: @@ -310,8 +312,6 @@ class DBAPIError(StatementError): def __init__(self, statement, params, orig, connection_invalidated=False): try: text = str(orig) - except (KeyboardInterrupt, SystemExit): - raise except Exception as e: text = 'Error in str() of DB-API-generated exception: ' + str(e) StatementError.__init__( diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index bd28975dd..e4c0de188 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2649,9 +2649,7 @@ def configure_mappers(): mapper._expire_memoizations() mapper.dispatch.mapper_configured( mapper, mapper.class_) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: exc = sys.exc_info()[1] if not hasattr(exc, '_configure_failed'): mapper._configure_failed = exc diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index 0c162e984..a174df784 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -248,9 +248,7 @@ class Pool(log.Identified): self.logger.debug("Closing connection %r", connection) try: self._dialect.do_close(connection) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: self.logger.error("Exception closing connection %r", connection, exc_info=True) @@ -569,12 +567,12 @@ def _finalize_fairy(connection, connection_record, # Immediately close detached instances if not connection_record: pool._close_connection(connection) - except Exception as e: + except BaseException as e: pool.logger.error( "Exception during reset or similar", exc_info=True) if connection_record: connection_record.invalidate(e=e) - if isinstance(e, (SystemExit, KeyboardInterrupt)): + if not isinstance(e, Exception): raise if connection_record: @@ -842,9 +840,7 @@ class SingletonThreadPool(Pool): for conn in self._all_conns: try: conn.close() - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: # pysqlite won't even let you close a conn from a thread # that didn't create it pass diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 8e18a22fe..4bc1683dd 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -3491,9 +3491,7 @@ def _string_or_unprintable(element): else: try: return str(element) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: return "unprintable element %r" % element diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py index 67c13231e..1284f9c2a 100644 --- a/lib/sqlalchemy/testing/engines.py +++ b/lib/sqlalchemy/testing/engines.py @@ -37,8 +37,6 @@ class ConnectionKiller(object): def _safe(self, fn): try: fn() - except (SystemExit, KeyboardInterrupt): - raise except Exception as e: warnings.warn( "testing_reaper couldn't " @@ -168,8 +166,6 @@ class ReconnectFixture(object): def _safe(self, fn): try: fn() - except (SystemExit, KeyboardInterrupt): - raise except Exception as e: warnings.warn( "ReconnectFixture couldn't " diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py index 64688d6b5..c8f7fdf30 100644 --- a/lib/sqlalchemy/testing/provision.py +++ b/lib/sqlalchemy/testing/provision.py @@ -120,9 +120,7 @@ def _pg_create_db(cfg, eng, ident): isolation_level="AUTOCOMMIT") as conn: try: _pg_drop_db(cfg, conn, ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass currentdb = conn.scalar("select current_database()") conn.execute("CREATE DATABASE %s TEMPLATE %s" % (ident, currentdb)) @@ -133,9 +131,7 @@ def _mysql_create_db(cfg, eng, ident): with eng.connect() as conn: try: _mysql_drop_db(cfg, conn, ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass conn.execute("CREATE DATABASE %s" % ident) conn.execute("CREATE DATABASE %s_test_schema" % ident) @@ -177,21 +173,15 @@ def _mysql_drop_db(cfg, eng, ident): with eng.connect() as conn: try: conn.execute("DROP DATABASE %s_test_schema" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass try: conn.execute("DROP DATABASE %s_test_schema_2" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass try: conn.execute("DROP DATABASE %s" % ident) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 75c6e7b46..ea8f30e9d 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -490,9 +490,7 @@ def generic_repr(obj, additional_kw=(), to_inspect=None, omit_kwarg=()): val = getattr(obj, arg, missing) if val is not missing and val != defval: output.append('%s=%r' % (arg, val)) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass if additional_kw: @@ -501,9 +499,7 @@ def generic_repr(obj, additional_kw=(), to_inspect=None, omit_kwarg=()): val = getattr(obj, arg, missing) if val is not missing and val != defval: output.append('%s=%r' % (arg, val)) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: pass return "%s(%s)" % (obj.__class__.__name__, ", ".join(output)) @@ -1189,9 +1185,7 @@ def warn_exception(func, *args, **kwargs): """ try: return func(*args, **kwargs) - except (SystemExit, KeyboardInterrupt): - raise - except: + except Exception: warn("%s('%s') ignored" % sys.exc_info()[0:2]) -- cgit v1.2.1 From 49e750a1d788710b89764c4dd9c0ddbf9b1f38ad Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 4 Oct 2014 12:18:20 -0400 Subject: - changelog, migration for pr github:134 --- lib/sqlalchemy/sql/elements.py | 29 +++++++++++++++++++++++++++++ lib/sqlalchemy/sql/functions.py | 9 ++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 53838358d..db14031d2 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2895,6 +2895,17 @@ class FunctionFilter(ColumnElement): which controls which rows are passed to it. It's supported only by certain database backends. + Invocation of :class:`.FunctionFilter` is via + :meth:`.FunctionElement.filter`:: + + func.count(1).filter(True) + + .. versionadded:: 1.0.0 + + .. seealso:: + + :meth:`.FunctionElement.filter` + """ __visit_name__ = 'funcfilter' @@ -2916,11 +2927,29 @@ class FunctionFilter(ColumnElement): This function is also available from the :data:`~.expression.func` construct itself via the :meth:`.FunctionElement.filter` method. + .. versionadded:: 1.0.0 + + .. seealso:: + + :meth:`.FunctionElement.filter` + + """ self.func = func self.filter(*criterion) def filter(self, *criterion): + """Produce an additional FILTER against the function. + + This method adds additional criteria to the initial criteria + set up by :meth:`.FunctionElement.filter`. + + Multiple criteria are joined together at SQL render time + via ``AND``. + + + """ + for criterion in list(criterion): criterion = _expression_literal_as_text(criterion) diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index a07eca8c6..9280c7d60 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -131,7 +131,14 @@ class FunctionElement(Executable, ColumnElement, FromClause): from sqlalchemy import funcfilter funcfilter(func.count(1), True) - See :func:`~.expression.funcfilter` for a full description. + .. versionadded:: 1.0.0 + + .. seealso:: + + :class:`.FunctionFilter` + + :func:`.funcfilter` + """ if not criterion: -- cgit v1.2.1 From b510b990947cb8e461df8877ca0f011542b7a319 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 4 Oct 2014 18:57:01 -0400 Subject: - use provide_metadata for new unique constraint / index tests - add a test for PG reflection of unique index without any unique constraint - for PG, don't include 'duplicates_constraint' in the entry if the index does not actually mirror a constraint - use a distinct method for unique constraint reflection within table - catch unique constraint not implemented condition; this may be within some dialects and also is expected to be supported by Alembic tests - migration + changelogs for #3184 - add individual doc notes as well to MySQL, Postgreql fixes #3184 --- lib/sqlalchemy/dialects/mysql/base.py | 23 +++++++++++++ lib/sqlalchemy/dialects/postgresql/base.py | 52 +++++++++++++++++++++++------- lib/sqlalchemy/engine/reflection.py | 15 ++++++++- 3 files changed, 77 insertions(+), 13 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 2f85a3626..793e6566d 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -341,6 +341,29 @@ reflection will not include foreign keys. For these tables, you may supply a :ref:`mysql_storage_engines` +.. _mysql_unique_constraints: + +MySQL Unique Constraints and Reflection +--------------------------------------- + +SQLAlchemy supports both the :class:`.Index` construct with the +flag ``unique=True``, indicating a UNIQUE index, as well as the +:class:`.UniqueConstraint` construct, representing a UNIQUE constraint. +Both objects/syntaxes are supported by MySQL when emitting DDL to create +these constraints. However, MySQL does not have a unique constraint +construct that is separate from a unique index; that is, the "UNIQUE" +constraint on MySQL is equivalent to creating a "UNIQUE INDEX". + +When reflecting these constructs, the :meth:`.Inspector.get_indexes` +and the :meth:`.Inspector.get_unique_constraints` methods will **both** +return an entry for a UNIQUE index in MySQL. However, when performing +full table reflection using ``Table(..., autoload=True)``, +the :class:`.UniqueConstraint` construct is +**not** part of the fully reflected :class:`.Table` construct under any +circumstances; this construct is always represented by a :class:`.Index` +with the ``unique=True`` setting present in the :attr:`.Table.indexes` +collection. + .. _mysql_timestamp_null: diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 556493b3c..baa640eaa 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -402,6 +402,28 @@ underlying CREATE INDEX command, so it *must* be a valid index type for your version of PostgreSQL. +.. _postgresql_index_reflection: + +Postgresql Index Reflection +--------------------------- + +The Postgresql database creates a UNIQUE INDEX implicitly whenever the +UNIQUE CONSTRAINT construct is used. When inspecting a table using +:class:`.Inspector`, the :meth:`.Inspector.get_indexes` +and the :meth:`.Inspector.get_unique_constraints` will report on these +two constructs distinctly; in the case of the index, the key +``duplicates_constraint`` will be present in the index entry if it is +detected as mirroring a constraint. When performing reflection using +``Table(..., autoload=True)``, the UNIQUE INDEX is **not** returned +in :attr:`.Table.indexes` when it is detected as mirroring a +:class:`.UniqueConstraint` in the :attr:`.Table.constraints` collection. + +.. versionchanged:: 1.0.0 - :class:`.Table` reflection now includes + :class:`.UniqueConstraint` objects present in the :attr:`.Table.constraints` + collection; the Postgresql backend will no longer include a "mirrored" + :class:`.Index` construct in :attr:`.Table.indexes` if it is detected + as corresponding to a unique constraint. + Special Reflection Options -------------------------- @@ -2523,21 +2545,27 @@ class PGDialect(default.DefaultDialect): % idx_name) sv_idx_name = idx_name + has_idx = idx_name in indexes index = indexes[idx_name] if col is not None: index['cols'][col_num] = col - index['key'] = [int(k.strip()) for k in idx_key.split()] - index['unique'] = unique - index['duplicates_constraint'] = (None if conrelid is None - else idx_name) - - return [ - {'name': name, - 'unique': idx['unique'], - 'column_names': [idx['cols'][i] for i in idx['key']], - 'duplicates_constraint': idx['duplicates_constraint']} - for name, idx in indexes.items() - ] + if not has_idx: + index['key'] = [int(k.strip()) for k in idx_key.split()] + index['unique'] = unique + if conrelid is not None: + index['duplicates_constraint'] = idx_name + + result = [] + for name, idx in indexes.items(): + entry = { + 'name': name, + 'unique': idx['unique'], + 'column_names': [idx['cols'][i] for i in idx['key']] + } + if 'duplicates_constraint' in idx: + entry['duplicates_constraint'] = idx['duplicates_constraint'] + result.append(entry) + return result @reflection.cache def get_unique_constraints(self, connection, table_name, diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 15c2dd195..2a1def86a 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -508,6 +508,10 @@ class Inspector(object): table_name, schema, table, cols_by_orig_name, include_columns, exclude_columns, reflection_options) + self._reflect_unique_constraints( + table_name, schema, table, cols_by_orig_name, + include_columns, exclude_columns, reflection_options) + def _reflect_column( self, table, col_d, include_columns, exclude_columns, cols_by_orig_name): @@ -665,8 +669,17 @@ class Inspector(object): sa_schema.Index(name, *idx_cols, **dict(unique=unique)) + def _reflect_unique_constraints( + self, table_name, schema, table, cols_by_orig_name, + include_columns, exclude_columns, reflection_options): + # Unique Constraints - constraints = self.get_unique_constraints(table_name, schema) + try: + constraints = self.get_unique_constraints(table_name, schema) + except NotImplementedError: + # optional dialect feature + return + for const_d in constraints: conname = const_d['name'] columns = const_d['column_names'] -- cgit v1.2.1 From 119674dd250d7da8be92a262ad53396c2fac4e0f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 6 Oct 2014 18:23:42 -0400 Subject: - The execution options passed to an :class:`.Engine` either via :paramref:`.create_engine.execution_options` or :meth:`.Engine.update_execution_options` are not passed to the special :class:`.Connection` used to initialize the dialect within the "first connect" event; dialects will usually perform their own queries in this phase, and none of the current available options should be applied here. In particular, the "autocommit" option was causing an attempt to autocommit within this initial connect which would fail with an AttributeError due to the non-standard state of the :class:`.Connection`. fixes #3200 --- lib/sqlalchemy/engine/strategies.py | 1 + 1 file changed, 1 insertion(+) (limited to 'lib') diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index 38206be89..49438372b 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -162,6 +162,7 @@ class DefaultEngineStrategy(EngineStrategy): def first_connect(dbapi_connection, connection_record): c = base.Connection(engine, connection=dbapi_connection, _has_events=False) + c._execution_options = {} dialect.initialize(c) event.listen(pool, 'first_connect', first_connect, once=True) -- cgit v1.2.1 From 5a10b6a455f9ad7be752469cbaa503857ae9fda2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 6 Oct 2014 18:33:05 -0400 Subject: - Fixed "'NoneType' object has no attribute 'concrete'" error when using :class:`.AbstractConcreteBase` in conjunction with a subclass that declares ``__abstract__``. fixes #3185 --- lib/sqlalchemy/ext/declarative/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index e84b21ad2..66fe05fd0 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -547,7 +547,7 @@ class AbstractConcreteBase(ConcreteBase): for scls in cls.__subclasses__(): sm = _mapper_or_none(scls) - if sm.concrete and cls in scls.__bases__: + if sm and sm.concrete and cls in scls.__bases__: sm._set_concrete_base(m) -- cgit v1.2.1 From 2885f78e4e20c2ae8552594ac7f0231b1bee4aad Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 7 Oct 2014 12:43:27 -0400 Subject: - adjustment for ref #3200 as we need an immutabledict() here so that union() can be called, in the case of a dialect that uses execution options inside of initialize() (e.g. oursql) --- lib/sqlalchemy/engine/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index 49438372b..398ef8df6 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -162,7 +162,7 @@ class DefaultEngineStrategy(EngineStrategy): def first_connect(dbapi_connection, connection_record): c = base.Connection(engine, connection=dbapi_connection, _has_events=False) - c._execution_options = {} + c._execution_options = util.immutabledict() dialect.initialize(c) event.listen(pool, 'first_connect', first_connect, once=True) -- cgit v1.2.1 From c55d10940b1953fce2129e7bcfe1728bea33cd1d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 7 Oct 2014 14:06:46 -0400 Subject: - clean up unicode docs and clarify that client_encoding at the engine level is not the same thing as at the connect args level. --- lib/sqlalchemy/dialects/postgresql/psycopg2.py | 66 +++++++++++++++++++++----- 1 file changed, 53 insertions(+), 13 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index 9dfd53e22..1a2a1ffe4 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -32,10 +32,25 @@ psycopg2-specific keyword arguments which are accepted by way of enabling this mode on a per-execution basis. * ``use_native_unicode``: Enable the usage of Psycopg2 "native unicode" mode per connection. True by default. + + .. seealso:: + + :ref:`psycopg2_disable_native_unicode` + * ``isolation_level``: This option, available for all PostgreSQL dialects, includes the ``AUTOCOMMIT`` isolation level when using the psycopg2 - dialect. See :ref:`psycopg2_isolation_level`. + dialect. + + .. seealso:: + + :ref:`psycopg2_isolation_level` + +* ``client_encoding``: sets the client encoding in a libpq-agnostic way, + using psycopg2's ``set_client_encoding()`` method. + + .. seealso:: + :ref:`psycopg2_unicode` Unix Domain Connections ------------------------ @@ -75,8 +90,10 @@ The following DBAPI-specific options are respected when used with If ``None`` or not set, the ``server_side_cursors`` option of the :class:`.Engine` is used. -Unicode -------- +.. _psycopg2_unicode: + +Unicode with Psycopg2 +---------------------- By default, the psycopg2 driver uses the ``psycopg2.extensions.UNICODE`` extension, such that the DBAPI receives and returns all strings as Python @@ -84,27 +101,51 @@ Unicode objects directly - SQLAlchemy passes these values through without change. Psycopg2 here will encode/decode string values based on the current "client encoding" setting; by default this is the value in the ``postgresql.conf`` file, which often defaults to ``SQL_ASCII``. -Typically, this can be changed to ``utf-8``, as a more useful default:: +Typically, this can be changed to ``utf8``, as a more useful default:: + + # postgresql.conf file - #client_encoding = sql_ascii # actually, defaults to database + # client_encoding = sql_ascii # actually, defaults to database # encoding client_encoding = utf8 A second way to affect the client encoding is to set it within Psycopg2 -locally. SQLAlchemy will call psycopg2's ``set_client_encoding()`` -method (see: -http://initd.org/psycopg/docs/connection.html#connection.set_client_encoding) +locally. SQLAlchemy will call psycopg2's +:meth:`psycopg2:connection.set_client_encoding` method on all new connections based on the value passed to :func:`.create_engine` using the ``client_encoding`` parameter:: + # set_client_encoding() setting; + # works for *all* Postgresql versions engine = create_engine("postgresql://user:pass@host/dbname", client_encoding='utf8') This overrides the encoding specified in the Postgresql client configuration. +When using the parameter in this way, the psycopg2 driver emits +``SET client_encoding TO 'utf8'`` on the connection explicitly, and works +in all Postgresql versions. + +Note that the ``client_encoding`` setting as passed to :func:`.create_engine` +is **not the same** as the more recently added ``client_encoding`` parameter +now supported by libpq directly. This is enabled when ``client_encoding`` +is passed directly to ``psycopg2.connect()``, and from SQLAlchemy is passed +using the :paramref:`.create_engine.connect_args` parameter:: + + # libpq direct parameter setting; + # only works for Postgresql **9.1 and above** + engine = create_engine("postgresql://user:pass@host/dbname", + connect_args={'client_encoding': 'utf8'}) + + # using the query string is equivalent + engine = create_engine("postgresql://user:pass@host/dbname?client_encoding=utf8") + +The above parameter was only added to libpq as of version 9.1 of Postgresql, +so using the previous method is better for cross-version support. + +.. _psycopg2_disable_native_unicode: -.. versionadded:: 0.7.3 - The psycopg2-specific ``client_encoding`` parameter to - :func:`.create_engine`. +Disabling Native Unicode +^^^^^^^^^^^^^^^^^^^^^^^^ SQLAlchemy can also be instructed to skip the usage of the psycopg2 ``UNICODE`` extension and to instead utilize its own unicode encode/decode @@ -116,8 +157,7 @@ in and coerce from bytes on the way back, using the value of the :func:`.create_engine` ``encoding`` parameter, which defaults to ``utf-8``. SQLAlchemy's own unicode encode/decode functionality is steadily becoming -obsolete as more DBAPIs support unicode fully along with the approach of -Python 3; in modern usage psycopg2 should be relied upon to handle unicode. +obsolete as most DBAPIs now support unicode fully. Transactions ------------ -- cgit v1.2.1 From 81d1e0455a406560be468d1aacc37aa63bb4d717 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 9 Oct 2014 17:20:30 -0400 Subject: - Fixed bug where a fair number of SQL elements within the sql package would fail to ``__repr__()`` successfully, due to a missing ``description`` attribute that would then invoke a recursion overflow when an internal AttributeError would then re-invoke ``__repr__()``. fixes #3195 --- lib/sqlalchemy/sql/elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index db14031d2..c38d83106 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -228,6 +228,7 @@ class ClauseElement(Visitable): is_selectable = False is_clause_element = True + description = None _order_by_label_element = None _is_from_container = False @@ -540,7 +541,7 @@ class ClauseElement(Visitable): __nonzero__ = __bool__ def __repr__(self): - friendly = getattr(self, 'description', None) + friendly = self.description if friendly is None: return object.__repr__(self) else: -- cgit v1.2.1 From dd6389f171736abd28d777b6fde40403cab0c13e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 9 Oct 2014 20:40:35 -0400 Subject: - The ON clause rendered when using :meth:`.Query.join`, :meth:`.Query.outerjoin`, or the standalone :func:`.orm.join` / :func:`.orm.outerjoin` functions to a single-inheritance subclass will now include the "single table criteria" in the ON clause even if the ON clause is otherwise hand-rolled; it is now added to the criteria using AND, the same way as if joining to a single-table target using relationship or similar. fixes #3222 --- lib/sqlalchemy/orm/util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'lib') diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 734f9d5e6..8d40ae21c 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -804,6 +804,16 @@ class _ORMJoin(expression.Join): expression.Join.__init__(self, left, right, onclause, isouter) + if not prop and getattr(right_info, 'mapper', None) \ + and right_info.mapper.single: + # if single inheritance target and we are using a manual + # 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 + def join(self, right, onclause=None, isouter=False, join_to_left=None): return _ORMJoin(self, right, onclause, isouter) -- cgit v1.2.1 From 3a6cdff88429e047a684c0f5d6029a30d9aaa062 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 10 Oct 2014 15:31:07 -0400 Subject: - apply patches to correct for + sign in with_hint() docs, fixes #3036 --- lib/sqlalchemy/sql/selectable.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index b4df87e54..8198a6733 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -2572,7 +2572,7 @@ class Select(HasPrefixes, GenerativeSelect): following:: select([mytable]).\\ - with_hint(mytable, "+ index(%(name)s ix_mytable)") + with_hint(mytable, "index(%(name)s ix_mytable)") Would render SQL as:: @@ -2583,8 +2583,7 @@ class Select(HasPrefixes, GenerativeSelect): and Sybase simultaneously:: select([mytable]).\\ - with_hint( - mytable, "+ index(%(name)s ix_mytable)", 'oracle').\\ + with_hint(mytable, "index(%(name)s ix_mytable)", 'oracle').\\ with_hint(mytable, "WITH INDEX ix_mytable", 'sybase') .. seealso:: -- cgit v1.2.1 From 95be42c06ff4e5f3528de42bb04dcba228ea74c2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 10 Oct 2014 17:15:19 -0400 Subject: - :meth:`.Insert.from_select` now includes Python and SQL-expression defaults if otherwise unspecified; the limitation where non- server column defaults aren't included in an INSERT FROM SELECT is now lifted and these expressions are rendered as constants into the SELECT statement. --- lib/sqlalchemy/sql/compiler.py | 2 +- lib/sqlalchemy/sql/crud.py | 97 +++++++++++++++++++++++------ lib/sqlalchemy/sql/dml.py | 26 +++++--- lib/sqlalchemy/testing/suite/test_insert.py | 37 ++++++++++- 4 files changed, 132 insertions(+), 30 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 86f00d944..a6c30b7dc 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1793,7 +1793,7 @@ class SQLCompiler(Compiled): text += " " + returning_clause if insert_stmt.select is not None: - text += " %s" % self.process(insert_stmt.select, **kw) + text += " %s" % self.process(self._insert_from_select, **kw) elif not crud_params and supports_default_values: text += " DEFAULT VALUES" elif insert_stmt._has_multi_parameters: diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index 1c1f661d2..831d05be1 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -89,18 +89,15 @@ def _get_crud_params(compiler, stmt, **kw): _col_bind_name, _getattr_col_key, values, kw) if compiler.isinsert and stmt.select_names: - # for an insert from select, we can only use names that - # are given, so only select for those names. - cols = (stmt.table.c[_column_as_key(name)] - for name in stmt.select_names) + _scan_insert_from_select_cols( + compiler, stmt, parameters, + _getattr_col_key, _column_as_key, + _col_bind_name, check_columns, values, kw) else: - # iterate through all table columns to maintain - # ordering, even for those cols that aren't included - cols = stmt.table.columns - - _scan_cols( - compiler, stmt, cols, parameters, - _getattr_col_key, _col_bind_name, check_columns, values, kw) + _scan_cols( + compiler, stmt, parameters, + _getattr_col_key, _column_as_key, + _col_bind_name, check_columns, values, kw) if parameters and stmt_parameters: check = set(parameters).intersection( @@ -118,13 +115,17 @@ def _get_crud_params(compiler, stmt, **kw): return values -def _create_bind_param(compiler, col, value, required=False, name=None): +def _create_bind_param( + compiler, col, value, process=True, required=False, name=None): if name is None: name = col.key bindparam = elements.BindParameter(name, value, type_=col.type, required=required) bindparam._is_crud = True - return bindparam._compiler_dispatch(compiler) + if process: + bindparam = bindparam._compiler_dispatch(compiler) + return bindparam + def _key_getters_for_crud_column(compiler): if compiler.isupdate and compiler.statement._extra_froms: @@ -162,14 +163,52 @@ def _key_getters_for_crud_column(compiler): return _column_as_key, _getattr_col_key, _col_bind_name +def _scan_insert_from_select_cols( + compiler, stmt, parameters, _getattr_col_key, + _column_as_key, _col_bind_name, check_columns, values, kw): + + need_pks, implicit_returning, \ + implicit_return_defaults, postfetch_lastrowid = \ + _get_returning_modifiers(compiler, stmt) + + cols = [stmt.table.c[_column_as_key(name)] + for name in stmt.select_names] + + compiler._insert_from_select = stmt.select + + add_select_cols = [] + if stmt.include_insert_from_select_defaults: + col_set = set(cols) + for col in stmt.table.columns: + if col not in col_set and col.default: + cols.append(col) + + for c in cols: + col_key = _getattr_col_key(c) + if col_key in parameters and col_key not in check_columns: + parameters.pop(col_key) + values.append((c, None)) + else: + _append_param_insert_select_hasdefault( + compiler, stmt, c, add_select_cols, kw) + + if add_select_cols: + values.extend(add_select_cols) + compiler._insert_from_select = compiler._insert_from_select._generate() + compiler._insert_from_select._raw_columns += tuple( + expr for col, expr in add_select_cols) + + def _scan_cols( - compiler, stmt, cols, parameters, _getattr_col_key, - _col_bind_name, check_columns, values, kw): + compiler, stmt, parameters, _getattr_col_key, + _column_as_key, _col_bind_name, check_columns, values, kw): need_pks, implicit_returning, \ implicit_return_defaults, postfetch_lastrowid = \ _get_returning_modifiers(compiler, stmt) + cols = stmt.table.columns + for c in cols: col_key = _getattr_col_key(c) if col_key in parameters and col_key not in check_columns: @@ -196,7 +235,8 @@ def _scan_cols( elif c.default is not None: _append_param_insert_hasdefault( - compiler, stmt, c, implicit_return_defaults, values, kw) + compiler, stmt, c, implicit_return_defaults, + values, kw) elif c.server_default is not None: if implicit_return_defaults and \ @@ -299,10 +339,8 @@ def _append_param_insert_hasdefault( elif not c.primary_key: compiler.postfetch.append(c) elif c.default.is_clause_element: - values.append( - (c, compiler.process( - c.default.arg.self_group(), **kw)) - ) + proc = compiler.process(c.default.arg.self_group(), **kw) + values.append((c, proc)) if implicit_return_defaults and \ c in implicit_return_defaults: @@ -317,6 +355,25 @@ def _append_param_insert_hasdefault( compiler.prefetch.append(c) +def _append_param_insert_select_hasdefault( + compiler, stmt, c, values, kw): + + if c.default.is_sequence: + if compiler.dialect.supports_sequences and \ + (not c.default.optional or + not compiler.dialect.sequences_optional): + proc = c.default + values.append((c, proc)) + elif c.default.is_clause_element: + proc = c.default.arg.self_group() + values.append((c, proc)) + else: + values.append( + (c, _create_bind_param(compiler, c, None, process=False)) + ) + compiler.prefetch.append(c) + + def _append_param_update( compiler, stmt, c, implicit_return_defaults, values, kw): diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 1934d0776..9f2ce7ce3 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -475,6 +475,7 @@ class Insert(ValuesBase): ValuesBase.__init__(self, table, values, prefixes) self._bind = bind self.select = self.select_names = None + self.include_insert_from_select_defaults = False self.inline = inline self._returning = returning self._validate_dialect_kwargs(dialect_kw) @@ -487,7 +488,7 @@ class Insert(ValuesBase): return () @_generative - def from_select(self, names, select): + def from_select(self, names, select, include_defaults=True): """Return a new :class:`.Insert` construct which represents an ``INSERT...FROM SELECT`` statement. @@ -506,6 +507,21 @@ class Insert(ValuesBase): is not checked before passing along to the database, the database would normally raise an exception if these column lists don't correspond. + :param include_defaults: if True, non-server default values and + SQL expressions as specified on :class:`.Column` objects + (as documented in :ref:`metadata_defaults_toplevel`) not + otherwise specified in the list of names will be rendered + into the INSERT and SELECT statements, so that these values are also + included in the data to be inserted. + + .. note:: A Python-side default that uses a Python callable function + will only be invoked **once** for the whole statement, and **not + per row**. + + .. versionadded:: 1.0.0 - :meth:`.Insert.from_select` now renders + Python-side and SQL expression column defaults into the + SELECT statement for columns otherwise not included in the + list of column names. .. versionchanged:: 1.0.0 an INSERT that uses FROM SELECT implies that the :paramref:`.insert.inline` flag is set to @@ -514,13 +530,6 @@ class Insert(ValuesBase): deals with an arbitrary number of rows, so the :attr:`.ResultProxy.inserted_primary_key` accessor does not apply. - .. note:: - - A SELECT..INSERT construct in SQL has no VALUES clause. Therefore - :class:`.Column` objects which utilize Python-side defaults - (e.g. as described at :ref:`metadata_defaults_toplevel`) - will **not** take effect when using :meth:`.Insert.from_select`. - .. versionadded:: 0.8.3 """ @@ -533,6 +542,7 @@ class Insert(ValuesBase): self.select_names = names self.inline = True + self.include_insert_from_select_defaults = include_defaults self.select = _interpret_as_select(select) def _copy_internals(self, clone=_clone, **kw): diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index 92d3d93e5..c197145c7 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -4,7 +4,7 @@ from .. import exclusions from ..assertions import eq_ from .. import engines -from sqlalchemy import Integer, String, select, util +from sqlalchemy import Integer, String, select, literal_column from ..schema import Table, Column @@ -90,6 +90,13 @@ class InsertBehaviorTest(fixtures.TablesTest): Column('id', Integer, primary_key=True, autoincrement=False), Column('data', String(50)) ) + Table('includes_defaults', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('data', String(50)), + Column('x', Integer, default=5), + Column('y', Integer, + default=literal_column("2", type_=Integer) + 2)) def test_autoclose_on_insert(self): if requirements.returning.enabled: @@ -158,6 +165,34 @@ class InsertBehaviorTest(fixtures.TablesTest): ("data3", ), ("data3", )] ) + @requirements.insert_from_select + def test_insert_from_select_with_defaults(self): + table = self.tables.includes_defaults + config.db.execute( + table.insert(), + [ + dict(id=1, data="data1"), + dict(id=2, data="data2"), + dict(id=3, data="data3"), + ] + ) + + config.db.execute( + table.insert(inline=True). + from_select(("id", "data",), + select([table.c.id + 5, table.c.data]). + where(table.c.data.in_(["data2", "data3"])) + ), + ) + + eq_( + config.db.execute( + select([table]).order_by(table.c.data) + ).fetchall(), + [(1, 'data1', 5, 4), (2, 'data2', 5, 4), + (7, 'data2', 5, 4), (3, 'data3', 5, 4), (8, 'data3', 5, 4)] + ) + class ReturningTest(fixtures.TablesTest): run_create_tables = 'each' -- cgit v1.2.1 From 3e810771a8ace7351fcd43deaf55891fd1c30a53 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 17:33:44 -0400 Subject: - change this literal so that the bound name doesn't have a numeric name, this is sort of a bug for oracle --- lib/sqlalchemy/testing/suite/test_insert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index c197145c7..2334d3049 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -4,7 +4,7 @@ from .. import exclusions from ..assertions import eq_ from .. import engines -from sqlalchemy import Integer, String, select, literal_column +from sqlalchemy import Integer, String, select, literal_column, literal from ..schema import Table, Column @@ -96,7 +96,7 @@ class InsertBehaviorTest(fixtures.TablesTest): Column('data', String(50)), Column('x', Integer, default=5), Column('y', Integer, - default=literal_column("2", type_=Integer) + 2)) + default=literal_column("2", type_=Integer) + literal(2))) def test_autoclose_on_insert(self): if requirements.returning.enabled: -- cgit v1.2.1 From cc29c4afff20dd251dbc045a490da9942f98b1bf Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 18:25:21 -0400 Subject: - Fixed long-standing bug in Oracle dialect where bound parameter names that started with numbers would not be quoted, as Oracle doesn't like numerics in bound parameter names. fixes #2138 --- lib/sqlalchemy/dialects/oracle/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 837a498fb..6df38e57e 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -813,7 +813,8 @@ class OracleDDLCompiler(compiler.DDLCompiler): class OracleIdentifierPreparer(compiler.IdentifierPreparer): reserved_words = set([x.lower() for x in RESERVED_WORDS]) - illegal_initial_characters = set(range(0, 10)).union(["_", "$"]) + illegal_initial_characters = set( + (str(dig) for dig in range(0, 10))).union(["_", "$"]) def _bindparam_requires_quotes(self, value): """Return True if the given identifier requires quoting.""" -- cgit v1.2.1 From 83e465633793e4a6d76e41b12fb92d7cc4bbddf3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 18:35:12 -0400 Subject: - embedding an existing predicate into a new one only seems to be used by test_oracle->test_coerce_to_unicode(). The predicate here should treat as a lambda based on enabled_for_config. not sure why this test is not failing on jenkins --- lib/sqlalchemy/testing/exclusions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py index 49211f805..f94724608 100644 --- a/lib/sqlalchemy/testing/exclusions.py +++ b/lib/sqlalchemy/testing/exclusions.py @@ -178,8 +178,7 @@ class Predicate(object): @classmethod def as_predicate(cls, predicate, description=None): if isinstance(predicate, compound): - return cls.as_predicate(predicate.fails.union(predicate.skips)) - + return cls.as_predicate(predicate.enabled_for_config, description) elif isinstance(predicate, Predicate): if description and predicate.description is None: predicate.description = description -- cgit v1.2.1 From 216b88894d95c17a1bd18b9d574e96530fb6f1cb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2014 19:02:32 -0400 Subject: add more order by here --- lib/sqlalchemy/testing/suite/test_insert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py index 2334d3049..38519dfb9 100644 --- a/lib/sqlalchemy/testing/suite/test_insert.py +++ b/lib/sqlalchemy/testing/suite/test_insert.py @@ -187,7 +187,7 @@ class InsertBehaviorTest(fixtures.TablesTest): eq_( config.db.execute( - select([table]).order_by(table.c.data) + select([table]).order_by(table.c.data, table.c.id) ).fetchall(), [(1, 'data1', 5, 4), (2, 'data2', 5, 4), (7, 'data2', 5, 4), (3, 'data3', 5, 4), (8, 'data3', 5, 4)] -- cgit v1.2.1 From 6d5c03001cd81d9ad5cee2459f222a6319f5b956 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 12 Oct 2014 09:54:44 -0400 Subject: - roll back part of pr 140 to get exception wrapping working again --- lib/sqlalchemy/exc.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 5d35dc2e7..8e65ef07b 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -280,9 +280,13 @@ class DBAPIError(StatementError): connection_invalidated=False): # Don't ever wrap these, just return them directly as if # DBAPIError didn't exist. - if (isinstance(orig, BaseException) and - not isinstance(orig, Exception)) or \ - isinstance(orig, DontWrapMixin): + if isinstance(orig, (KeyboardInterrupt, SystemExit, DontWrapMixin)): + + # TODO: shouldn't it work this way? see if we can get this + # to work in py3k + #if (isinstance(orig, BaseException) and + # not isinstance(orig, Exception)) or \ + # isinstance(orig, DontWrapMixin): return orig if orig is not None: -- cgit v1.2.1 From 0426d174e4a608cb09878fe18185b2ae853243ad Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 12 Oct 2014 19:37:14 -0400 Subject: - the test_except test was doing an unnecessary workaround of some kind, take that out, restore the better exception logic in exc --- lib/sqlalchemy/exc.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 8e65ef07b..5d35dc2e7 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -280,13 +280,9 @@ class DBAPIError(StatementError): connection_invalidated=False): # Don't ever wrap these, just return them directly as if # DBAPIError didn't exist. - if isinstance(orig, (KeyboardInterrupt, SystemExit, DontWrapMixin)): - - # TODO: shouldn't it work this way? see if we can get this - # to work in py3k - #if (isinstance(orig, BaseException) and - # not isinstance(orig, Exception)) or \ - # isinstance(orig, DontWrapMixin): + if (isinstance(orig, BaseException) and + not isinstance(orig, Exception)) or \ + isinstance(orig, DontWrapMixin): return orig if orig is not None: -- cgit v1.2.1 From 50d2432a9efa65c9798ef207e3f887cb5c0071e1 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 12 Oct 2014 20:14:32 -0400 Subject: - Mysqlconnector as of version 2.0, probably as a side effect of the python 3 merge, now does not expect percent signs (e.g. as used as the modulus operator and others) to be doubled, even when using the "pyformat" bound parameter format (this change is not documented by Mysqlconnector). The dialect now checks for py2k and for mysqlconnector less than version 2.0 when detecting if the modulus operator should be rendered as ``%%`` or ``%``. - Unicode SQL is now passed for MySQLconnector version 2.0 and above; for Py2k and MySQL < 2.0, strings are encoded. Note that mysqlconnector as of 2.0.1 appears to have a bug with unicode DDL on py2k, so the tests here are skipping until we observe it's fixed. - take out profiling on mysqlconnector, callcounts vary too much with its current development speed --- lib/sqlalchemy/dialects/mysql/mysqlconnector.py | 46 +++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py index 6077ce53e..417e1ad6f 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/lib/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -21,6 +21,7 @@ from .base import (MySQLDialect, MySQLExecutionContext, BIT) from ... import util +import re class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext): @@ -31,18 +32,34 @@ class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext): class MySQLCompiler_mysqlconnector(MySQLCompiler): def visit_mod_binary(self, binary, operator, **kw): - return self.process(binary.left, **kw) + " %% " + \ - self.process(binary.right, **kw) + if self.dialect._mysqlconnector_double_percents: + return self.process(binary.left, **kw) + " %% " + \ + self.process(binary.right, **kw) + else: + return self.process(binary.left, **kw) + " % " + \ + self.process(binary.right, **kw) def post_process_text(self, text): - return text.replace('%', '%%') + if self.dialect._mysqlconnector_double_percents: + return text.replace('%', '%%') + else: + return text + + def escape_literal_column(self, text): + if self.dialect._mysqlconnector_double_percents: + return text.replace('%', '%%') + else: + return text class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer): def _escape_identifier(self, value): value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace("%", "%%") + if self.dialect._mysqlconnector_double_percents: + return value.replace("%", "%%") + else: + return value class _myconnpyBIT(BIT): @@ -55,8 +72,6 @@ class _myconnpyBIT(BIT): class MySQLDialect_mysqlconnector(MySQLDialect): driver = 'mysqlconnector' - if util.py2k: - supports_unicode_statements = False supports_unicode_binds = True supports_sane_rowcount = True @@ -77,6 +92,10 @@ class MySQLDialect_mysqlconnector(MySQLDialect): } ) + @util.memoized_property + def supports_unicode_statements(self): + return util.py3k or self._mysqlconnector_version_info > (2, 0) + @classmethod def dbapi(cls): from mysql import connector @@ -107,6 +126,21 @@ class MySQLDialect_mysqlconnector(MySQLDialect): pass return [[], opts] + @util.memoized_property + def _mysqlconnector_version_info(self): + if self.dbapi and hasattr(self.dbapi, '__version__'): + m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?', + self.dbapi.__version__) + if m: + return tuple( + int(x) + for x in m.group(1, 2, 3) + if x is not None) + + @util.memoized_property + def _mysqlconnector_double_percents(self): + return not util.py3k and self._mysqlconnector_version_info < (2, 0) + def _get_server_version_info(self, connection): dbapi_con = connection.connection version = dbapi_con.get_server_version() -- cgit v1.2.1 From fb09ad7551cf348f999647347882546a1f50dcbe Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 14 Oct 2014 11:59:48 -0400 Subject: - The ``__module__`` attribute is now set for all those SQL and ORM functions that are derived as "public factory" symbols, which should assist with documentation tools being able to report on the target module. fixes #3218 --- lib/sqlalchemy/util/langhelpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index f6da9a87d..5c17bea88 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -134,7 +134,8 @@ def public_factory(target, location): fn = target.__init__ callable_ = target doc = "Construct a new :class:`.%s` object. \n\n"\ - "This constructor is mirrored as a public API function; see :func:`~%s` "\ + "This constructor is mirrored as a public API function; "\ + "see :func:`~%s` "\ "for a full usage and argument description." % ( target.__name__, location, ) else: @@ -155,6 +156,7 @@ def %(name)s(%(args)s): exec(code, env) decorated = env[location_name] decorated.__doc__ = fn.__doc__ + decorated.__module__ = "sqlalchemy" + location.rsplit(".", 1)[0] if compat.py2k or hasattr(fn, '__func__'): fn.__func__.__doc__ = doc else: -- cgit v1.2.1 From 6de1a878702b8737b2257b89b478ead79a8d78cc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 14 Oct 2014 14:04:17 -0400 Subject: - Improvements to the mechanism used by :class:`.Session` to locate "binds" (e.g. engines to use), such engines can be associated with mixin classes, concrete subclasses, as well as a wider variety of table metadata such as joined inheritance tables. fixes #3035 --- lib/sqlalchemy/orm/session.py | 96 ++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 43 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 13afcb357..db9d3a51d 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -641,14 +641,8 @@ class Session(_SessionClassMethods): SessionExtension._adapt_listener(self, ext) if binds is not None: - for mapperortable, bind in binds.items(): - insp = inspect(mapperortable) - if insp.is_selectable: - self.bind_table(mapperortable, bind) - elif insp.is_mapper: - self.bind_mapper(mapperortable, bind) - else: - assert False + for key, bind in binds.items(): + self._add_bind(key, bind) if not self.autocommit: self.begin() @@ -1026,40 +1020,47 @@ class Session(_SessionClassMethods): # TODO: + crystallize + document resolution order # vis. bind_mapper/bind_table - def bind_mapper(self, mapper, bind): - """Bind operations for a mapper to a Connectable. - - mapper - A mapper instance or mapped class + def _add_bind(self, key, bind): + try: + insp = inspect(key) + except sa_exc.NoInspectionAvailable: + if not isinstance(key, type): + raise exc.ArgumentError( + "Not acceptable bind target: %s" % + key) + else: + self.__binds[key] = bind + else: + if insp.is_selectable: + self.__binds[insp] = bind + elif insp.is_mapper: + self.__binds[insp.class_] = bind + for selectable in insp._all_tables: + self.__binds[selectable] = bind + else: + raise exc.ArgumentError( + "Not acceptable bind target: %s" % + key) - bind - Any Connectable: a :class:`.Engine` or :class:`.Connection`. + def bind_mapper(self, mapper, bind): + """Associate a :class:`.Mapper` with a "bind", e.g. a :class:`.Engine` + or :class:`.Connection`. - All subsequent operations involving this mapper will use the given - `bind`. + The given mapper is added to a lookup used by the + :meth:`.Session.get_bind` method. """ - if isinstance(mapper, type): - mapper = class_mapper(mapper) - - self.__binds[mapper.base_mapper] = bind - for t in mapper._all_tables: - self.__binds[t] = bind + self._add_bind(mapper, bind) def bind_table(self, table, bind): - """Bind operations on a Table to a Connectable. - - table - A :class:`.Table` instance + """Associate a :class:`.Table` with a "bind", e.g. a :class:`.Engine` + or :class:`.Connection`. - bind - Any Connectable: a :class:`.Engine` or :class:`.Connection`. - - All subsequent operations involving this :class:`.Table` will use the - given `bind`. + The given mapper is added to a lookup used by the + :meth:`.Session.get_bind` method. """ - self.__binds[table] = bind + self._add_bind(table, bind) def get_bind(self, mapper=None, clause=None): """Return a "bind" to which this :class:`.Session` is bound. @@ -1113,6 +1114,7 @@ class Session(_SessionClassMethods): bound :class:`.MetaData`. """ + if mapper is clause is None: if self.bind: return self.bind @@ -1122,15 +1124,23 @@ class Session(_SessionClassMethods): "Connection, and no context was provided to locate " "a binding.") - c_mapper = mapper is not None and _class_to_mapper(mapper) or None + if mapper is not None: + try: + mapper = inspect(mapper) + except sa_exc.NoInspectionAvailable: + if isinstance(mapper, type): + raise exc.UnmappedClassError(mapper) + else: + raise - # manually bound? if self.__binds: - if c_mapper: - if c_mapper.base_mapper in self.__binds: - return self.__binds[c_mapper.base_mapper] - elif c_mapper.mapped_table in self.__binds: - return self.__binds[c_mapper.mapped_table] + if mapper: + for cls in mapper.class_.__mro__: + if cls in self.__binds: + return self.__binds[cls] + if clause is None: + clause = mapper.mapped_table + if clause is not None: for t in sql_util.find_tables(clause, include_crud=True): if t in self.__binds: @@ -1142,12 +1152,12 @@ class Session(_SessionClassMethods): if isinstance(clause, sql.expression.ClauseElement) and clause.bind: return clause.bind - if c_mapper and c_mapper.mapped_table.bind: - return c_mapper.mapped_table.bind + if mapper and mapper.mapped_table.bind: + return mapper.mapped_table.bind context = [] if mapper is not None: - context.append('mapper %s' % c_mapper) + context.append('mapper %s' % mapper) if clause is not None: context.append('SQL expression') -- cgit v1.2.1 From d06e9c361fe0abd7bfdc7ecaceda931d981344db Mon Sep 17 00:00:00 2001 From: mozillazg Date: Wed, 15 Oct 2014 21:40:19 +0800 Subject: Fixed typo --- lib/sqlalchemy/sql/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index f9c65f6f7..ef5d79a48 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -1061,8 +1061,8 @@ class Column(SchemaItem, ColumnClause): conditionally rendered differently on different backends, consider custom compilation rules for :class:`.CreateColumn`. - ..versionadded:: 0.8.3 Added the ``system=True`` parameter to - :class:`.Column`. + .. versionadded:: 0.8.3 Added the ``system=True`` parameter to + :class:`.Column`. """ -- cgit v1.2.1 From 61a4a89d993eda1d3168b501ba9ed8d94ea9b5f8 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 16 Oct 2014 14:36:56 -0400 Subject: - The :meth:`.Query.update` method will now convert string key names in the given dictionary of values into mapped attribute names against the mapped class being updated. Previously, string names were taken in directly and passed to the core update statement without any means to resolve against the mapped entity. Support for synonyms and hybrid attributes as the subject attributes of :meth:`.Query.update` are also supported. fixes #3228 --- lib/sqlalchemy/orm/persistence.py | 43 ++++++++++++++++++++++++++++++++++----- lib/sqlalchemy/orm/query.py | 26 +++++++++++++++-------- 2 files changed, 56 insertions(+), 13 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 74e69e44c..114b79ea5 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -18,7 +18,7 @@ import operator from itertools import groupby from .. import sql, util, exc as sa_exc, schema from . import attributes, sync, exc as orm_exc, evaluator -from .base import state_str, _attr_as_key +from .base import state_str, _attr_as_key, _entity_descriptor from ..sql import expression from . import loading @@ -987,6 +987,7 @@ class BulkUpdate(BulkUD): super(BulkUpdate, self).__init__(query) self.query._no_select_modifiers("update") self.values = values + self.mapper = self.query._mapper_zero_or_none() @classmethod def factory(cls, query, synchronize_session, values): @@ -996,9 +997,40 @@ class BulkUpdate(BulkUD): False: BulkUpdate }, synchronize_session, query, values) + def _resolve_string_to_expr(self, key): + if self.mapper and isinstance(key, util.string_types): + attr = _entity_descriptor(self.mapper, key) + return attr.__clause_element__() + else: + return key + + def _resolve_key_to_attrname(self, key): + if self.mapper and isinstance(key, util.string_types): + attr = _entity_descriptor(self.mapper, key) + return attr.property.key + elif isinstance(key, attributes.InstrumentedAttribute): + return key.key + elif hasattr(key, '__clause_element__'): + key = key.__clause_element__() + + if self.mapper and isinstance(key, expression.ColumnElement): + try: + attr = self.mapper._columntoproperty[key] + except orm_exc.UnmappedColumnError: + return None + else: + return attr.key + else: + raise sa_exc.InvalidRequestError( + "Invalid expression type: %r" % key) + def _do_exec(self): + values = dict( + (self._resolve_string_to_expr(k), v) + for k, v in self.values.items() + ) update_stmt = sql.update(self.primary_table, - self.context.whereclause, self.values) + self.context.whereclause, values) self.result = self.query.session.execute( update_stmt, params=self.query._params) @@ -1044,9 +1076,10 @@ class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate): def _additional_evaluators(self, evaluator_compiler): self.value_evaluators = {} for key, value in self.values.items(): - key = _attr_as_key(key) - self.value_evaluators[key] = evaluator_compiler.process( - expression._literal_as_binds(value)) + key = self._resolve_key_to_attrname(key) + if key is not None: + self.value_evaluators[key] = evaluator_compiler.process( + expression._literal_as_binds(value)) def _do_post_synchronize(self): session = self.query.session diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 7b2ea7977..fce7a3665 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -2756,9 +2756,25 @@ class Query(object): Updates rows matched by this query in the database. - :param values: a dictionary with attributes names as keys and literal + E.g.:: + + sess.query(User).filter(User.age == 25).\ + update({User.age: User.age - 10}, synchronize_session='fetch') + + + sess.query(User).filter(User.age == 25).\ + update({"age": User.age - 10}, synchronize_session='evaluate') + + + :param values: a dictionary with attributes names, or alternatively + mapped attributes or SQL expressions, as keys, and literal values or sql expressions as values. + .. versionchanged:: 1.0.0 - string names in the values dictionary + are now resolved against the mapped entity; previously, these + strings were passed as literal column names with no mapper-level + translation. + :param synchronize_session: chooses the strategy to update the attributes on objects in the session. Valid values are: @@ -2796,7 +2812,7 @@ class Query(object): which normally occurs upon :meth:`.Session.commit` or can be forced by using :meth:`.Session.expire_all`. - * As of 0.8, this method will support multiple table updates, as + * The method supports multiple table updates, as detailed in :ref:`multi_table_updates`, and this behavior does extend to support updates of joined-inheritance and other multiple table mappings. However, the **join condition of an inheritance @@ -2827,12 +2843,6 @@ class Query(object): """ - # TODO: value keys need to be mapped to corresponding sql cols and - # instr.attr.s to string keys - # TODO: updates of manytoone relationships need to be converted to - # fk assignments - # TODO: cascades need handling. - update_op = persistence.BulkUpdate.factory( self, synchronize_session, values) update_op.exec_() -- cgit v1.2.1