Skip to content

Commit e5ad0ab

Browse files
committed
'with' starts a transaction even on autocommit connections
Close #941
1 parent d8e6426 commit e5ad0ab

File tree

6 files changed

+127
-12
lines changed

6 files changed

+127
-12
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ What's new in psycopg 2.9
55
-------------------------
66

77
- Dropped support for Python 2.7, 3.4, 3.5 (:tickets:`#1198, #1000, #1197`).
8+
- ``with connection`` starts a transaction on autocommit transactions too
9+
(:ticket:`#941`).
810
- Connection exceptions with sqlstate ``08XXX`` reclassified as
911
`~psycopg2.OperationalError` (a subclass of the previously used
1012
`~psycopg2.DatabaseError`) (:ticket:`#1148`).

doc/src/usage.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,9 @@ and each ``with`` block is effectively wrapped in a separate transaction::
832832
finally:
833833
conn.close()
834834

835+
.. versionchanged:: 2.9
836+
``with connection`` starts a transaction also on autocommit connections.
837+
835838

836839
.. index::
837840
pair: Server side; Cursor

psycopg/connection.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ struct connectionObject {
145145

146146
/* the pid this connection was created into */
147147
pid_t procpid;
148+
149+
/* inside a with block */
150+
int entered;
148151
};
149152

150153
/* map isolation level values into a numeric const */

psycopg/connection_type.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,22 @@ psyco_conn_tpc_recover(connectionObject *self, PyObject *dummy)
406406
static PyObject *
407407
psyco_conn_enter(connectionObject *self, PyObject *dummy)
408408
{
409+
PyObject *rv = NULL;
410+
409411
EXC_IF_CONN_CLOSED(self);
410412

413+
if (self->entered) {
414+
PyErr_SetString(ProgrammingError,
415+
"the connection cannot be re-entered recursively");
416+
goto exit;
417+
}
418+
419+
self->entered = 1;
411420
Py_INCREF(self);
412-
return (PyObject *)self;
421+
rv = (PyObject *)self;
422+
423+
exit:
424+
return rv;
413425
}
414426

415427

@@ -427,6 +439,9 @@ psyco_conn_exit(connectionObject *self, PyObject *args)
427439
goto exit;
428440
}
429441

442+
/* even if there will be an error, consider ourselves out */
443+
self->entered = 0;
444+
430445
if (type == Py_None) {
431446
if (!(tmp = PyObject_CallMethod((PyObject *)self, "commit", NULL))) {
432447
goto exit;

psycopg/pqpath.c

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -347,14 +347,19 @@ pq_begin_locked(connectionObject *conn, PyThreadState **tstate)
347347
char buf[256]; /* buf size must be same as bufsize */
348348
int result;
349349

350-
Dprintf("pq_begin_locked: pgconn = %p, autocommit = %d, status = %d",
350+
Dprintf("pq_begin_locked: pgconn = %p, %d, status = %d",
351351
conn->pgconn, conn->autocommit, conn->status);
352352

353-
if (conn->autocommit || conn->status != CONN_STATUS_READY) {
353+
if (conn->status != CONN_STATUS_READY) {
354354
Dprintf("pq_begin_locked: transaction in progress");
355355
return 0;
356356
}
357357

358+
if (conn->autocommit && !conn->entered) {
359+
Dprintf("pq_begin_locked: autocommit and no with block");
360+
return 0;
361+
}
362+
358363
if (conn->isolevel == ISOLATION_LEVEL_DEFAULT
359364
&& conn->readonly == STATE_DEFAULT
360365
&& conn->deferrable == STATE_DEFAULT) {
@@ -393,10 +398,10 @@ pq_commit(connectionObject *conn)
393398
Py_BEGIN_ALLOW_THREADS;
394399
pthread_mutex_lock(&conn->lock);
395400

396-
Dprintf("pq_commit: pgconn = %p, autocommit = %d, status = %d",
397-
conn->pgconn, conn->autocommit, conn->status);
401+
Dprintf("pq_commit: pgconn = %p, status = %d",
402+
conn->pgconn, conn->status);
398403

399-
if (conn->autocommit || conn->status != CONN_STATUS_BEGIN) {
404+
if (conn->status != CONN_STATUS_BEGIN) {
400405
Dprintf("pq_commit: no transaction to commit");
401406
retvalue = 0;
402407
}
@@ -427,10 +432,10 @@ pq_abort_locked(connectionObject *conn, PyThreadState **tstate)
427432
{
428433
int retvalue = -1;
429434

430-
Dprintf("pq_abort_locked: pgconn = %p, autocommit = %d, status = %d",
431-
conn->pgconn, conn->autocommit, conn->status);
435+
Dprintf("pq_abort_locked: pgconn = %p, status = %d",
436+
conn->pgconn, conn->status);
432437

433-
if (conn->autocommit || conn->status != CONN_STATUS_BEGIN) {
438+
if (conn->status != CONN_STATUS_BEGIN) {
434439
Dprintf("pq_abort_locked: no transaction to abort");
435440
return 0;
436441
}
@@ -488,12 +493,12 @@ pq_reset_locked(connectionObject *conn, PyThreadState **tstate)
488493
{
489494
int retvalue = -1;
490495

491-
Dprintf("pq_reset_locked: pgconn = %p, autocommit = %d, status = %d",
492-
conn->pgconn, conn->autocommit, conn->status);
496+
Dprintf("pq_reset_locked: pgconn = %p, status = %d",
497+
conn->pgconn, conn->status);
493498

494499
conn->mark += 1;
495500

496-
if (!conn->autocommit && conn->status == CONN_STATUS_BEGIN) {
501+
if (conn->status == CONN_STATUS_BEGIN) {
497502
retvalue = pq_execute_command_locked(conn, "ABORT", tstate);
498503
if (retvalue != 0) return retvalue;
499504
}

tests/test_with.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,93 @@ def rollback(self):
155155
curs.execute("select * from test_with")
156156
self.assertEqual(curs.fetchall(), [])
157157

158+
def test_cant_reenter(self):
159+
raised_ok = False
160+
with self.conn:
161+
try:
162+
with self.conn:
163+
pass
164+
except psycopg2.ProgrammingError:
165+
raised_ok = True
166+
167+
self.assert_(raised_ok)
168+
169+
# Still good
170+
with self.conn:
171+
pass
172+
173+
def test_with_autocommit(self):
174+
self.conn.autocommit = True
175+
self.assertEqual(
176+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
177+
)
178+
with self.conn:
179+
curs = self.conn.cursor()
180+
curs.execute("insert into test_with values (1)")
181+
self.assertEqual(
182+
self.conn.info.transaction_status,
183+
ext.TRANSACTION_STATUS_INTRANS,
184+
)
185+
186+
self.assertEqual(
187+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
188+
)
189+
curs.execute("select count(*) from test_with")
190+
self.assertEqual(curs.fetchone()[0], 1)
191+
self.assertEqual(
192+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
193+
)
194+
195+
def test_with_autocommit_pyerror(self):
196+
self.conn.autocommit = True
197+
raised_ok = False
198+
try:
199+
with self.conn:
200+
curs = self.conn.cursor()
201+
curs.execute("insert into test_with values (2)")
202+
self.assertEqual(
203+
self.conn.info.transaction_status,
204+
ext.TRANSACTION_STATUS_INTRANS,
205+
)
206+
1 / 0
207+
except ZeroDivisionError:
208+
raised_ok = True
209+
210+
self.assert_(raised_ok)
211+
self.assertEqual(
212+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
213+
)
214+
curs.execute("select count(*) from test_with")
215+
self.assertEqual(curs.fetchone()[0], 0)
216+
self.assertEqual(
217+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
218+
)
219+
220+
def test_with_autocommit_pgerror(self):
221+
self.conn.autocommit = True
222+
raised_ok = False
223+
try:
224+
with self.conn:
225+
curs = self.conn.cursor()
226+
curs.execute("insert into test_with values (2)")
227+
self.assertEqual(
228+
self.conn.info.transaction_status,
229+
ext.TRANSACTION_STATUS_INTRANS,
230+
)
231+
curs.execute("insert into test_with values ('x')")
232+
except psycopg2.errors.InvalidTextRepresentation:
233+
raised_ok = True
234+
235+
self.assert_(raised_ok)
236+
self.assertEqual(
237+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
238+
)
239+
curs.execute("select count(*) from test_with")
240+
self.assertEqual(curs.fetchone()[0], 0)
241+
self.assertEqual(
242+
self.conn.info.transaction_status, ext.TRANSACTION_STATUS_IDLE
243+
)
244+
158245

159246
class WithCursorTestCase(WithTestCase):
160247
def test_with_ok(self):

0 commit comments

Comments
 (0)