diff options
| -rw-r--r-- | CHANGES | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/default.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/expression.py | 26 | ||||
| -rw-r--r-- | test/engine/transaction.py | 75 |
4 files changed, 108 insertions, 5 deletions
@@ -16,6 +16,14 @@ CHANGES - cast() accepts text('something') and other non-literal operands properly [ticket:962] + + - added "autocommit=True" kwarg to select() and text(), + as well as generative autocommit() method on select(); + for statements which modify the database through some + user-defined means other than the usual INSERT/UPDATE/ + DELETE etc., this flag will enable "autocommit" behavior + during execution if no transaction is in progress + [ticket:915] - The '.c.' attribute on a selectable now gets an entry for every column expression in its columns clause. diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index e78eedd5c..3b93862ae 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -164,10 +164,10 @@ class DefaultExecutionContext(base.ExecutionContext): self.isupdate = compiled.isupdate if isinstance(compiled.statement, expression._TextClause): self.returns_rows = self.returns_rows_text(self.statement) - self.should_autocommit = self.should_autocommit_text(self.statement) + self.should_autocommit = compiled.statement._autocommit or self.should_autocommit_text(self.statement) else: self.returns_rows = self.returns_rows_compiled(compiled) - self.should_autocommit = self.should_autocommit_compiled(compiled) + self.should_autocommit = getattr(compiled.statement, '_autocommit', False) or self.should_autocommit_compiled(compiled) if not parameters: self.compiled_parameters = [compiled.construct_params()] diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 6c0c4659e..2e5797b34 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -158,6 +158,11 @@ def select(columns=None, whereclause=None, from_obj=[], **kwargs): \**kwargs Additional parameters include: + autocommit + indicates this SELECT statement modifies the database, and + should be subject to autocommit behavior if no transaction + has been started. + prefixes a list of strings or ``ClauseElement`` objects to include directly after the SELECT keyword in the generated statement, @@ -727,6 +732,11 @@ def text(text, bind=None, *args, **kwargs): bind an optional connection or engine to be used for this text query. + autocommit=True + indicates this SELECT statement modifies the database, and + should be subject to autocommit behavior if no transaction + has been started. + bindparams a list of ``bindparam()`` instances which can be used to define the types and/or initial values for the bind parameters within @@ -740,6 +750,7 @@ def text(text, bind=None, *args, **kwargs): which will be used to perform post-processing on columns within the result set (for textual statements that produce result sets). + """ return _TextClause(text, bind=bind, *args, **kwargs) @@ -1821,10 +1832,11 @@ class _TextClause(ClauseElement): _bind_params_regex = re.compile(r'(?<![:\w\x5c]):(\w+)(?!:)', re.UNICODE) - def __init__(self, text = "", bind=None, bindparams=None, typemap=None): + def __init__(self, text = "", bind=None, bindparams=None, typemap=None, autocommit=False): self._bind = bind self.bindparams = {} self.typemap = typemap + self._autocommit = autocommit if typemap is not None: for key in typemap.keys(): typemap[key] = sqltypes.to_instance(typemap[key]) @@ -2711,9 +2723,10 @@ class TableClause(FromClause): class _SelectBaseMixin(object): """Base class for ``Select`` and ``CompoundSelects``.""" - def __init__(self, use_labels=False, for_update=False, limit=None, offset=None, order_by=None, group_by=None, bind=None): + def __init__(self, use_labels=False, for_update=False, limit=None, offset=None, order_by=None, group_by=None, bind=None, autocommit=False): self.use_labels = use_labels self.for_update = for_update + self._autocommit = autocommit self._limit = limit self._offset = offset self._bind = bind @@ -2734,7 +2747,7 @@ class _SelectBaseMixin(object): return _ScalarSelect(self) def apply_labels(self): - """set the 'labels' flag on this selectable. + """return a new selectable with the 'use_labels' flag set to True. This will result in column expressions being generated using labels against their table name, such as "SELECT somecolumn AS tablename_somecolumn". This allows selectables which @@ -2760,6 +2773,13 @@ class _SelectBaseMixin(object): return True + def autocommit(self): + """return a new selectable with the 'autocommit' flag set to True.""" + + s = self._generate() + s._autocommit = True + return s + def _generate(self): s = self._clone() s._clone_from_clause() diff --git a/test/engine/transaction.py b/test/engine/transaction.py index c54d67e21..686c640fd 100644 --- a/test/engine/transaction.py +++ b/test/engine/transaction.py @@ -375,6 +375,81 @@ class AutoRollbackTest(PersistTest): users.drop(conn2) conn2.close() +class ExplicitAutoCommitTest(PersistTest): + """test the 'autocommit' flag on select() and text() objects. + + Requires Postgres so that we may define a custom function which modifies the database. + """ + + __only_on__ = 'postgres' + + def setUpAll(self): + global metadata, foo + metadata = MetaData(testing.db) + foo = Table('foo', metadata, Column('id', Integer, primary_key=True), Column('data', String(100))) + metadata.create_all() + testing.db.execute("create function insert_foo(varchar) returns integer as 'insert into foo(data) values ($1);select 1;' language sql") + + def tearDown(self): + foo.delete().execute() + + def tearDownAll(self): + testing.db.execute("drop function insert_foo(varchar)") + metadata.drop_all() + + def test_control(self): + # test that not using autocommit does not commit + conn1 = testing.db.connect() + conn2 = testing.db.connect() + + conn1.execute(select([func.insert_foo('data1')])) + assert conn2.execute(select([foo.c.data])).fetchall() == [] + + conn1.execute(text("select insert_foo('moredata')")) + assert conn2.execute(select([foo.c.data])).fetchall() == [] + + trans = conn1.begin() + trans.commit() + + assert conn2.execute(select([foo.c.data])).fetchall() == [('data1',), ('moredata',)] + + conn1.close() + conn2.close() + + def test_explicit_compiled(self): + conn1 = testing.db.connect() + conn2 = testing.db.connect() + + conn1.execute(select([func.insert_foo('data1')], autocommit=True)) + assert conn2.execute(select([foo.c.data])).fetchall() == [('data1',)] + + conn1.execute(select([func.insert_foo('data2')]).autocommit()) + assert conn2.execute(select([foo.c.data])).fetchall() == [('data1',), ('data2',)] + + conn1.close() + conn2.close() + + def test_explicit_text(self): + conn1 = testing.db.connect() + conn2 = testing.db.connect() + + conn1.execute(text("select insert_foo('moredata')", autocommit=True)) + assert conn2.execute(select([foo.c.data])).fetchall() == [('moredata',)] + + conn1.close() + conn2.close() + + def test_implicit_text(self): + conn1 = testing.db.connect() + conn2 = testing.db.connect() + + conn1.execute(text("insert into foo (data) values ('implicitdata')")) + assert conn2.execute(select([foo.c.data])).fetchall() == [('implicitdata',)] + + conn1.close() + conn2.close() + + class TLTransactionTest(PersistTest): def setUpAll(self): global users, metadata, tlengine |
