From d93391dd8ff0f99fa8c39eeb439a42a5f23e368c Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Thu, 28 Jul 2016 19:49:15 +0200 Subject: [PATCH 01/21] Expose the SQLite Online Backup API as Connection.backup() See issue #27645. --- Lib/sqlite3/test/backup.py | 104 +++++++++++++++++++++++++++++++++++ Lib/test/test_sqlite.py | 4 +- Modules/_sqlite/connection.c | 67 ++++++++++++++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 Lib/sqlite3/test/backup.py diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py new file mode 100644 index 00000000000000..f28ab4cf2d8f30 --- /dev/null +++ b/Lib/sqlite3/test/backup.py @@ -0,0 +1,104 @@ +import sqlite3 as sqlite +from tempfile import NamedTemporaryFile +import unittest + +class BackupTests(unittest.TestCase): + def setUp(self): + cx = self.cx = sqlite.connect(":memory:") + cx.execute('CREATE TABLE foo (key INTEGER)') + cx.executemany('INSERT INTO foo (key) VALUES (?)', [(3,), (4,)]) + cx.commit() + + def tearDown(self): + self.cx.close() + + def testBackup(self, bckfn): + cx = sqlite.connect(bckfn) + result = cx.execute("SELECT key FROM foo ORDER BY key").fetchall() + self.assertEqual(result[0][0], 3) + self.assertEqual(result[1][0], 4) + + def CheckSimple(self): + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name) + self.testBackup(bckfn.name) + + def CheckProgress(self): + journal = [] + + def progress(remaining, total): + journal.append(remaining) + + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, 1, progress) + self.testBackup(bckfn.name) + + self.assertEqual(len(journal), 2) + self.assertEqual(journal[0], 1) + self.assertEqual(journal[1], 0) + + def CheckProgressAllPagesAtOnce_0(self): + journal = [] + + def progress(remaining, total): + journal.append(remaining) + + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, 0, progress) + self.testBackup(bckfn.name) + + self.assertEqual(len(journal), 1) + self.assertEqual(journal[0], 0) + + def CheckProgressAllPagesAtOnce_1(self): + journal = [] + + def progress(remaining, total): + journal.append(remaining) + + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, -1, progress) + self.testBackup(bckfn.name) + + self.assertEqual(len(journal), 1) + self.assertEqual(journal[0], 0) + + def CheckNonCallableProgress(self): + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + with self.assertRaises(TypeError) as cm: + self.cx.backup(bckfn.name, 1, 'bar') + self.assertEqual(str(cm.exception), 'progress argument must be a callable') + + def CheckModifyingProgress(self): + journal = [] + + def progress(remaining, total): + if not journal: + self.cx.execute('INSERT INTO foo (key) VALUES (?)', (remaining+1000,)) + self.cx.commit() + journal.append(remaining) + + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, 1, progress) + self.testBackup(bckfn.name) + + cx = sqlite.connect(bckfn.name) + result = cx.execute("SELECT key FROM foo" + " WHERE key >= 1000" + " ORDER BY key").fetchall() + self.assertEqual(result[0][0], 1001) + + self.assertEqual(len(journal), 3) + self.assertEqual(journal[0], 1) + self.assertEqual(journal[1], 1) + self.assertEqual(journal[2], 0) + +def suite(): + return unittest.TestSuite(unittest.makeSuite(BackupTests, "Check")) + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + +if __name__ == "__main__": + test() diff --git a/Lib/test/test_sqlite.py b/Lib/test/test_sqlite.py index adfcd9994575b3..07101d24995780 100644 --- a/Lib/test/test_sqlite.py +++ b/Lib/test/test_sqlite.py @@ -7,7 +7,7 @@ import sqlite3 from sqlite3.test import (dbapi, types, userfunctions, factory, transactions, hooks, regression, - dump) + dump, backup) def load_tests(*args): if test.support.verbose: @@ -18,7 +18,7 @@ def load_tests(*args): userfunctions.suite(), factory.suite(), transactions.suite(), hooks.suite(), regression.suite(), - dump.suite()]) + dump.suite(), backup.suite()]) if __name__ == "__main__": unittest.main() diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 37b45f330b3493..fc2cbb7ef29c7f 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1477,6 +1477,71 @@ pysqlite_connection_iterdump(pysqlite_Connection* self, PyObject* args) return retval; } +static PyObject * +pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) +{ + char* filename; + int pages = -1; + PyObject* progress = Py_None; + int rc; + sqlite3 *bckconn; + sqlite3_backup *bckhandle; + + if (!PyArg_ParseTuple(args, "s|iO:backup(filename, pages, progress)", + &filename, &pages, &progress)) { + return NULL; + } + + if (progress != Py_None && !PyCallable_Check(progress)) { + PyErr_SetString(PyExc_TypeError, "progress argument must be a callable"); + return NULL; + } + + if (pages == 0) { + pages = -1; + } + + rc = sqlite3_open(filename, &bckconn); + if (rc == SQLITE_OK) { + bckhandle = sqlite3_backup_init(bckconn, "main", self->db, "main"); + if (bckhandle) { + do { + rc = sqlite3_backup_step(bckhandle, pages); + + if (progress != Py_None) { + if (!PyObject_CallFunction(progress, "ii", + sqlite3_backup_remaining(bckhandle), + sqlite3_backup_pagecount(bckhandle))) { + /* User's callback raised an error: interrupt the loop and + propagate it. */ + rc = -1; + } + } + + /* Sleep for 250ms if there are still further pages to copy */ + if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { + sqlite3_sleep(250); + } + } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); + + sqlite3_backup_finish(bckhandle); + } + + if (rc != -1) { + rc = _pysqlite_seterror(bckconn, NULL); + } + } + + sqlite3_close(bckconn); + + if (rc != 0) { + /* TODO: should the (probably incomplete/invalid) backup be removed here? */ + return NULL; + } else { + Py_RETURN_NONE; + } +} + static PyObject * pysqlite_connection_create_collation(pysqlite_Connection* self, PyObject* args) { @@ -1649,6 +1714,8 @@ static PyMethodDef connection_methods[] = { PyDoc_STR("Abort any pending database operation. Non-standard.")}, {"iterdump", (PyCFunction)pysqlite_connection_iterdump, METH_NOARGS, PyDoc_STR("Returns iterator to the dump of the database in an SQL text format. Non-standard.")}, + {"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS, + PyDoc_STR("Execute a backup of the database. Non-standard.")}, {"__enter__", (PyCFunction)pysqlite_connection_enter, METH_NOARGS, PyDoc_STR("For context manager. Non-standard.")}, {"__exit__", (PyCFunction)pysqlite_connection_exit, METH_VARARGS, From 7b1277aca09d9806c6921b7f5f152321914cac71 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Thu, 28 Jul 2016 22:14:56 +0200 Subject: [PATCH 02/21] Preliminary documentation for sqlite3.Connection.backup() --- Doc/library/sqlite3.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index d1f7a6f120620b..5af75d2452056d 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -521,6 +521,34 @@ Connection Objects f.write('%s\n' % line) + .. method:: backup(filename[, pages, progress]) + + This method exposes the `API`__ that allows to make a backup of a SQLite + database into the mandatory argument *filename*, even while it's being accessed + by other clients, or concurrently by the same connection. + + By default, or when *pages* is either ``0`` or a negative integer, the entire + database is copied in a single step; otherwise the method performs a loop + copying up to the specified *pages* at a time. + + If *progress* is specified, it must either ``None`` or a callable object that + will be executed at each iteration with two integer arguments, respectively the + *remaining* number of pages still to be copied and the *total* number of pages. + + Example:: + + # Copy an existing database into another file + import sqlite3 + + def progress(remaining, total): + print("Copied %d of %d pages..." % (total-remaining, total)) + + con = sqlite3.connect('existing_db.db') + con.backup('copy_of_existing_db.db', 1, progress) + +__ http://sqlite.org/backup.html + + .. _sqlite3-cursor-objects: Cursor Objects From 1258d47ccfab132d89e57e41f56cf287d5b64f47 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 09:28:57 +0100 Subject: [PATCH 03/21] Reduce code nesting depth by using a finally label, as other functions do --- Modules/_sqlite/connection.c | 66 ++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index fc2cbb7ef29c7f..e1189eafdc26aa 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1483,18 +1483,19 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) char* filename; int pages = -1; PyObject* progress = Py_None; + PyObject* retval = NULL; int rc; sqlite3 *bckconn; sqlite3_backup *bckhandle; if (!PyArg_ParseTuple(args, "s|iO:backup(filename, pages, progress)", &filename, &pages, &progress)) { - return NULL; + goto finally; } if (progress != Py_None && !PyCallable_Check(progress)) { PyErr_SetString(PyExc_TypeError, "progress argument must be a callable"); - return NULL; + goto finally; } if (pages == 0) { @@ -1502,44 +1503,49 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) } rc = sqlite3_open(filename, &bckconn); - if (rc == SQLITE_OK) { - bckhandle = sqlite3_backup_init(bckconn, "main", self->db, "main"); - if (bckhandle) { - do { - rc = sqlite3_backup_step(bckhandle, pages); - - if (progress != Py_None) { - if (!PyObject_CallFunction(progress, "ii", - sqlite3_backup_remaining(bckhandle), - sqlite3_backup_pagecount(bckhandle))) { - /* User's callback raised an error: interrupt the loop and - propagate it. */ - rc = -1; - } - } + if (rc != SQLITE_OK) { + goto finally; + } + + bckhandle = sqlite3_backup_init(bckconn, "main", self->db, "main"); + if (bckhandle) { + do { + rc = sqlite3_backup_step(bckhandle, pages); - /* Sleep for 250ms if there are still further pages to copy */ - if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { - sqlite3_sleep(250); + if (progress != Py_None) { + if (!PyObject_CallFunction(progress, "ii", + sqlite3_backup_remaining(bckhandle), + sqlite3_backup_pagecount(bckhandle))) { + /* User's callback raised an error: interrupt the loop and + propagate it. */ + rc = -1; } - } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); + } - sqlite3_backup_finish(bckhandle); - } + /* Sleep for 250ms if there are still further pages to copy */ + if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { + sqlite3_sleep(250); + } + } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); - if (rc != -1) { - rc = _pysqlite_seterror(bckconn, NULL); - } + sqlite3_backup_finish(bckhandle); + } + + if (rc != -1) { + rc = _pysqlite_seterror(bckconn, NULL); } sqlite3_close(bckconn); - if (rc != 0) { - /* TODO: should the (probably incomplete/invalid) backup be removed here? */ - return NULL; + if (rc == SQLITE_OK) { + Py_INCREF(Py_None); + retval = Py_None; } else { - Py_RETURN_NONE; + /* TODO: should the (probably incomplete/invalid) backup be removed here? */ } + +finally: + return retval; } static PyObject * From d8ea4574e06463e88e6b987d58a18c79ac15713c Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 09:48:37 +0100 Subject: [PATCH 04/21] Wrap SQLite calls between BEGIN_ALLOW_THREADS and END_ALLOW_THREADS Suggested by Aviv Palivoda. --- Modules/_sqlite/connection.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index e1189eafdc26aa..89bdae679838a8 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1502,15 +1502,23 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) pages = -1; } + Py_BEGIN_ALLOW_THREADS rc = sqlite3_open(filename, &bckconn); + Py_END_ALLOW_THREADS + if (rc != SQLITE_OK) { goto finally; } + Py_BEGIN_ALLOW_THREADS bckhandle = sqlite3_backup_init(bckconn, "main", self->db, "main"); + Py_END_ALLOW_THREADS + if (bckhandle) { do { + Py_BEGIN_ALLOW_THREADS rc = sqlite3_backup_step(bckhandle, pages); + Py_END_ALLOW_THREADS if (progress != Py_None) { if (!PyObject_CallFunction(progress, "ii", @@ -1524,18 +1532,24 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) /* Sleep for 250ms if there are still further pages to copy */ if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { + Py_BEGIN_ALLOW_THREADS sqlite3_sleep(250); + Py_END_ALLOW_THREADS } } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); + Py_BEGIN_ALLOW_THREADS sqlite3_backup_finish(bckhandle); + Py_END_ALLOW_THREADS } if (rc != -1) { rc = _pysqlite_seterror(bckconn, NULL); } + Py_BEGIN_ALLOW_THREADS sqlite3_close(bckconn); + Py_END_ALLOW_THREADS if (rc == SQLITE_OK) { Py_INCREF(Py_None); From 35fa0ad29bce33b33d02d31bafa2cc71811083b2 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 09:54:00 +0100 Subject: [PATCH 05/21] Add an entry in Misc/NEWS related to issue #27645 --- Misc/NEWS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Misc/NEWS b/Misc/NEWS index 4f19e75aeaa8fc..0d931d2c203cef 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,8 @@ What's New in Python 3.7.0 alpha 1? Core and Builtins ----------------- +- bpo-27645: sqlite3.Connection now exposes a backup() method. + - bpo-28598: Support __rmod__ for subclasses of str being called before str.__mod__. Patch by Martijn Pieters. @@ -34,6 +36,9 @@ Core and Builtins - bpo-29546: Improve from-import error message with location +- Issue #7063: Remove dead code from the "array" module's slice handling. + Patch by Chuck. + - Issue #29319: Prevent RunMainFromImporter overwriting sys.path[0]. - Issue #29337: Fixed possible BytesWarning when compare the code objects. From c1af5a01684fcc933b0a25be0cf38c0db6be7a04 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 10:55:18 +0100 Subject: [PATCH 06/21] Remove extraneous entry introduced in d4439d6c5b2a67682f3db74d395c603f7861361d --- Misc/NEWS | 3 --- 1 file changed, 3 deletions(-) diff --git a/Misc/NEWS b/Misc/NEWS index 0d931d2c203cef..74569e7ac26746 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -36,9 +36,6 @@ Core and Builtins - bpo-29546: Improve from-import error message with location -- Issue #7063: Remove dead code from the "array" module's slice handling. - Patch by Chuck. - - Issue #29319: Prevent RunMainFromImporter overwriting sys.path[0]. - Issue #29337: Fixed possible BytesWarning when compare the code objects. From c07d9820e16f1ffc86e4760371d473bdffba5737 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 10:56:57 +0100 Subject: [PATCH 07/21] Attribute the patch to myself --- Misc/NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/NEWS b/Misc/NEWS index 74569e7ac26746..388e8400cd1b60 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -11,6 +11,7 @@ Core and Builtins ----------------- - bpo-27645: sqlite3.Connection now exposes a backup() method. + Patch by Lele Gaifax. - bpo-28598: Support __rmod__ for subclasses of str being called before str.__mod__. Patch by Martijn Pieters. From cdc7edad9d004c000f734ccdc865025988c58a9f Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 11:43:46 +0100 Subject: [PATCH 08/21] Make pages and progress keyword-only args in sqlite3.Connection.backup() --- Doc/library/sqlite3.rst | 2 +- Lib/sqlite3/test/backup.py | 10 +++++----- Modules/_sqlite/connection.c | 11 ++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 5af75d2452056d..4e3a48f6792d9c 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -521,7 +521,7 @@ Connection Objects f.write('%s\n' % line) - .. method:: backup(filename[, pages, progress]) + .. method:: backup(filename, *, pages=0, progress=None) This method exposes the `API`__ that allows to make a backup of a SQLite database into the mandatory argument *filename*, even while it's being accessed diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index f28ab4cf2d8f30..3558ec61d94e78 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -30,7 +30,7 @@ def progress(remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: - self.cx.backup(bckfn.name, 1, progress) + self.cx.backup(bckfn.name, pages=1, progress=progress) self.testBackup(bckfn.name) self.assertEqual(len(journal), 2) @@ -44,7 +44,7 @@ def progress(remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: - self.cx.backup(bckfn.name, 0, progress) + self.cx.backup(bckfn.name, progress=progress) self.testBackup(bckfn.name) self.assertEqual(len(journal), 1) @@ -57,7 +57,7 @@ def progress(remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: - self.cx.backup(bckfn.name, -1, progress) + self.cx.backup(bckfn.name, pages=-1, progress=progress) self.testBackup(bckfn.name) self.assertEqual(len(journal), 1) @@ -66,7 +66,7 @@ def progress(remaining, total): def CheckNonCallableProgress(self): with NamedTemporaryFile(suffix='.sqlite') as bckfn: with self.assertRaises(TypeError) as cm: - self.cx.backup(bckfn.name, 1, 'bar') + self.cx.backup(bckfn.name, pages=1, progress='bar') self.assertEqual(str(cm.exception), 'progress argument must be a callable') def CheckModifyingProgress(self): @@ -79,7 +79,7 @@ def progress(remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: - self.cx.backup(bckfn.name, 1, progress) + self.cx.backup(bckfn.name, pages=1, progress=progress) self.testBackup(bckfn.name) cx = sqlite.connect(bckfn.name) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 89bdae679838a8..c61dd45674a2d3 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1478,7 +1478,7 @@ pysqlite_connection_iterdump(pysqlite_Connection* self, PyObject* args) } static PyObject * -pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) +pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* kwds) { char* filename; int pages = -1; @@ -1487,9 +1487,10 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args) int rc; sqlite3 *bckconn; sqlite3_backup *bckhandle; + static char *keywords[] = {"filename", "pages", "progress", NULL}; - if (!PyArg_ParseTuple(args, "s|iO:backup(filename, pages, progress)", - &filename, &pages, &progress)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|$iO:backup", keywords, + &filename, &pages, &progress)) { goto finally; } @@ -1734,8 +1735,8 @@ static PyMethodDef connection_methods[] = { PyDoc_STR("Abort any pending database operation. Non-standard.")}, {"iterdump", (PyCFunction)pysqlite_connection_iterdump, METH_NOARGS, PyDoc_STR("Returns iterator to the dump of the database in an SQL text format. Non-standard.")}, - {"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS, - PyDoc_STR("Execute a backup of the database. Non-standard.")}, + {"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS | METH_KEYWORDS, + PyDoc_STR("Makes a backup of the database. Non-standard.")}, {"__enter__", (PyCFunction)pysqlite_connection_enter, METH_NOARGS, PyDoc_STR("For context manager. Non-standard.")}, {"__exit__", (PyCFunction)pysqlite_connection_exit, METH_VARARGS, From 6d3eb699a7c31c0628faeac9657c19f7d1d7e3a0 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 11:45:47 +0100 Subject: [PATCH 09/21] Remove reference to SQLite docs in Connection.backup() documentation --- Doc/library/sqlite3.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 4e3a48f6792d9c..d3a07c704d86d3 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -523,9 +523,9 @@ Connection Objects .. method:: backup(filename, *, pages=0, progress=None) - This method exposes the `API`__ that allows to make a backup of a SQLite - database into the mandatory argument *filename*, even while it's being accessed - by other clients, or concurrently by the same connection. + This method makes a backup of a SQLite database into the mandatory argument + *filename*, even while it's being accessed by other clients, or concurrently by + the same connection. By default, or when *pages* is either ``0`` or a negative integer, the entire database is copied in a single step; otherwise the method performs a loop @@ -546,7 +546,6 @@ Connection Objects con = sqlite3.connect('existing_db.db') con.backup('copy_of_existing_db.db', 1, progress) -__ http://sqlite.org/backup.html .. _sqlite3-cursor-objects: From 6fab17ff713a959bbf969e7e90a65ab1e3022f8f Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 11:50:25 +0100 Subject: [PATCH 10/21] Use f-string in the sqlite3.Connection.backup() example --- Doc/library/sqlite3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index d3a07c704d86d3..08b1f45272a4ad 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -541,7 +541,7 @@ Connection Objects import sqlite3 def progress(remaining, total): - print("Copied %d of %d pages..." % (total-remaining, total)) + print(f"Copied {total-remaining} of {total} pages...") con = sqlite3.connect('existing_db.db') con.backup('copy_of_existing_db.db', 1, progress) From 6449f3d5ece3139d8b5846cca7788d18391d3b68 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 11:51:11 +0100 Subject: [PATCH 11/21] Explicitly mark sqlite3.Connection.backup() as added in v3.7 --- Doc/library/sqlite3.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 08b1f45272a4ad..c5db0a036b50d6 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -546,6 +546,7 @@ Connection Objects con = sqlite3.connect('existing_db.db') con.backup('copy_of_existing_db.db', 1, progress) + .. versionadded:: 3.7 .. _sqlite3-cursor-objects: From f84d3757694a533aecd842a7ea1300b5fcc5f567 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Wed, 1 Mar 2017 11:51:51 +0100 Subject: [PATCH 12/21] =?UTF-8?q?Reduce=20chances=20of=20cluttering=20futu?= =?UTF-8?q?re=20=E2=80=9Cgit=20blame=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/test/test_sqlite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sqlite.py b/Lib/test/test_sqlite.py index 07101d24995780..9564da35193f1f 100644 --- a/Lib/test/test_sqlite.py +++ b/Lib/test/test_sqlite.py @@ -18,7 +18,8 @@ def load_tests(*args): userfunctions.suite(), factory.suite(), transactions.suite(), hooks.suite(), regression.suite(), - dump.suite(), backup.suite()]) + dump.suite(), + backup.suite()]) if __name__ == "__main__": unittest.main() From e748942e759e8d2a456719225f52e47808404f2a Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 10:09:53 +0100 Subject: [PATCH 13/21] Parametrize the name of the database involved in the backup Suggested by Aviv Palivoda. --- Doc/library/sqlite3.rst | 7 ++++++- Lib/sqlite3/test/backup.py | 14 ++++++++++++++ Modules/_sqlite/connection.c | 9 +++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index c5db0a036b50d6..0847d413eb4d87 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -521,7 +521,7 @@ Connection Objects f.write('%s\n' % line) - .. method:: backup(filename, *, pages=0, progress=None) + .. method:: backup(filename, *, pages=0, progress=None, name="main") This method makes a backup of a SQLite database into the mandatory argument *filename*, even while it's being accessed by other clients, or concurrently by @@ -535,6 +535,11 @@ Connection Objects will be executed at each iteration with two integer arguments, respectively the *remaining* number of pages still to be copied and the *total* number of pages. + The *name* argument specifies the database name that will be copied: it must be + a string containing either ``"main"``, the default, to indicate the main + database, ``"temp"`` to indicate the temporary database or the name specified + after the ``AS`` keyword in an ``ATTACH`` statement for an attached database. + Example:: # Copy an existing database into another file diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index 3558ec61d94e78..bb3a91a70e6164 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -93,6 +93,20 @@ def progress(remaining, total): self.assertEqual(journal[1], 1) self.assertEqual(journal[2], 0) + def CheckDatabaseSourceName(self): + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, name='main') + self.cx.backup(bckfn.name, name='temp') + with self.assertRaises(sqlite.OperationalError): + self.cx.backup(bckfn.name, name='non-existing') + self.cx.execute("ATTACH DATABASE ':memory:' AS attached_db") + self.cx.execute('CREATE TABLE attached_db.foo (key INTEGER)') + self.cx.executemany('INSERT INTO attached_db.foo (key) VALUES (?)', [(3,), (4,)]) + self.cx.commit() + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + self.cx.backup(bckfn.name, name='attached_db') + self.testBackup(bckfn.name) + def suite(): return unittest.TestSuite(unittest.makeSuite(BackupTests, "Check")) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index c61dd45674a2d3..374407adef51de 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1483,14 +1483,15 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* char* filename; int pages = -1; PyObject* progress = Py_None; + char* name = "main"; PyObject* retval = NULL; int rc; sqlite3 *bckconn; sqlite3_backup *bckhandle; - static char *keywords[] = {"filename", "pages", "progress", NULL}; + static char *keywords[] = {"filename", "pages", "progress", "name", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|$iO:backup", keywords, - &filename, &pages, &progress)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|$iOs:backup", keywords, + &filename, &pages, &progress, &name)) { goto finally; } @@ -1512,7 +1513,7 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* } Py_BEGIN_ALLOW_THREADS - bckhandle = sqlite3_backup_init(bckconn, "main", self->db, "main"); + bckhandle = sqlite3_backup_init(bckconn, "main", self->db, name); Py_END_ALLOW_THREADS if (bckhandle) { From 426dad457ee1691327b4a49474b714ee7e0e64cf Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 10:10:28 +0100 Subject: [PATCH 14/21] Test propagation of exception raised in backup's progress callback --- Lib/sqlite3/test/backup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index bb3a91a70e6164..d7c2dfc75bd5ee 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -93,6 +93,15 @@ def progress(remaining, total): self.assertEqual(journal[1], 1) self.assertEqual(journal[2], 0) + def CheckFailingProgress(self): + def progress(remaining, total): + raise SystemError('nearly out of space') + + with NamedTemporaryFile(suffix='.sqlite') as bckfn: + with self.assertRaises(SystemError) as err: + self.cx.backup(bckfn.name, progress=progress) + self.assertEqual(str(err.exception), 'nearly out of space') + def CheckDatabaseSourceName(self): with NamedTemporaryFile(suffix='.sqlite') as bckfn: self.cx.backup(bckfn.name, name='main') From 895726b9fe8e2e054ef9326392afbf47ae2eac87 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 10:10:38 +0100 Subject: [PATCH 15/21] Use a better name for variable --- Lib/sqlite3/test/backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index d7c2dfc75bd5ee..7680220f70f742 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -65,9 +65,9 @@ def progress(remaining, total): def CheckNonCallableProgress(self): with NamedTemporaryFile(suffix='.sqlite') as bckfn: - with self.assertRaises(TypeError) as cm: + with self.assertRaises(TypeError) as err: self.cx.backup(bckfn.name, pages=1, progress='bar') - self.assertEqual(str(cm.exception), 'progress argument must be a callable') + self.assertEqual(str(err.exception), 'progress argument must be a callable') def CheckModifyingProgress(self): journal = [] From 77dfa3932a092cbbdacbfde26ac9719f42a201eb Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 10:18:31 +0100 Subject: [PATCH 16/21] Do not delay next iteration if the result was OK When sqlite3_backup_step() returns OK it means that it was able to do it's work but there are further pages to be copied: in such case it's better to immediately start the next iteration, without opening a window that allows other threads to go on and possibly locking the database. Suggested by Aviv Palivoda. --- Modules/_sqlite/connection.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 374407adef51de..5252897c63d448 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1532,8 +1532,9 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* } } - /* Sleep for 250ms if there are still further pages to copy */ - if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { + /* Sleep for 250ms if there are still further pages to copy and + the engine could not make any progress */ + if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { Py_BEGIN_ALLOW_THREADS sqlite3_sleep(250); Py_END_ALLOW_THREADS From 71726bae6a689be7a5991fb10c000a44c3dad036 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 14:14:48 +0100 Subject: [PATCH 17/21] Omit the backup method when underlying SQLite library is older than 3.6.11 --- Doc/library/sqlite3.rst | 3 +++ Lib/sqlite3/test/backup.py | 1 + Misc/NEWS | 4 ++-- Modules/_sqlite/connection.c | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 0847d413eb4d87..8420be103f8d52 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -551,6 +551,9 @@ Connection Objects con = sqlite3.connect('existing_db.db') con.backup('copy_of_existing_db.db', 1, progress) + .. note:: This is available only when the underlying SQLite library is at + version 3.6.11 or higher. + .. versionadded:: 3.7 diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index 7680220f70f742..7159fad84e9f5d 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -2,6 +2,7 @@ from tempfile import NamedTemporaryFile import unittest +@unittest.skipIf(sqlite.sqlite_version_info < (3, 6, 11), "Backup API not supported") class BackupTests(unittest.TestCase): def setUp(self): cx = self.cx = sqlite.connect(":memory:") diff --git a/Misc/NEWS b/Misc/NEWS index 388e8400cd1b60..9cd90f7fd99ff0 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,8 +10,8 @@ What's New in Python 3.7.0 alpha 1? Core and Builtins ----------------- -- bpo-27645: sqlite3.Connection now exposes a backup() method. - Patch by Lele Gaifax. +- bpo-27645: sqlite3.Connection now exposes a backup() method, if the underlying SQLite + library is at version 3.6.11 or higher. Patch by Lele Gaifax. - bpo-28598: Support __rmod__ for subclasses of str being called before str.__mod__. Patch by Martijn Pieters. diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 5252897c63d448..fb58b2e8d548dd 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -41,6 +41,10 @@ #endif #endif +#if SQLITE_VERSION_NUMBER >= 3006011 +#define HAVE_BACKUP_API +#endif + _Py_IDENTIFIER(cursor); static const char * const begin_statements[] = { @@ -1477,6 +1481,7 @@ pysqlite_connection_iterdump(pysqlite_Connection* self, PyObject* args) return retval; } +#ifdef HAVE_BACKUP_API static PyObject * pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* kwds) { @@ -1564,6 +1569,7 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* finally: return retval; } +#endif static PyObject * pysqlite_connection_create_collation(pysqlite_Connection* self, PyObject* args) @@ -1737,8 +1743,10 @@ static PyMethodDef connection_methods[] = { PyDoc_STR("Abort any pending database operation. Non-standard.")}, {"iterdump", (PyCFunction)pysqlite_connection_iterdump, METH_NOARGS, PyDoc_STR("Returns iterator to the dump of the database in an SQL text format. Non-standard.")}, + #ifdef HAVE_BACKUP_API {"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Makes a backup of the database. Non-standard.")}, + #endif {"__enter__", (PyCFunction)pysqlite_connection_enter, METH_NOARGS, PyDoc_STR("For context manager. Non-standard.")}, {"__exit__", (PyCFunction)pysqlite_connection_exit, METH_VARARGS, From c4fbc20334b2bea01ca0f878ba9c084d710f4c19 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 3 Mar 2017 14:26:15 +0100 Subject: [PATCH 18/21] Assert that the non-mandatory arguments are keyword-only --- Lib/sqlite3/test/backup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index 7159fad84e9f5d..5bf9b24524e7e6 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -19,6 +19,10 @@ def testBackup(self, bckfn): self.assertEqual(result[0][0], 3) self.assertEqual(result[1][0], 4) + def CheckKeywordOnlyArgs(self): + with self.assertRaises(TypeError): + self.cx.backup('foo', 1) + def CheckSimple(self): with NamedTemporaryFile(suffix='.sqlite') as bckfn: self.cx.backup(bckfn.name) From ff1609bea60a3d0f6c91959dadb8b4bd57807d87 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Sat, 4 Mar 2017 10:02:17 +0100 Subject: [PATCH 19/21] Pass also the current status of the ongoing backup to the progress callback Add new SQLITE_DONE integer constant to the module, that could be used by the callback to determine whether the backup is terminated. --- Doc/library/sqlite3.rst | 9 +++++---- Lib/sqlite3/test/backup.py | 16 ++++++++-------- Modules/_sqlite/connection.c | 2 +- Modules/_sqlite/module.c | 3 +++ 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 8420be103f8d52..410e1443ebb189 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -531,9 +531,10 @@ Connection Objects database is copied in a single step; otherwise the method performs a loop copying up to the specified *pages* at a time. - If *progress* is specified, it must either ``None`` or a callable object that - will be executed at each iteration with two integer arguments, respectively the - *remaining* number of pages still to be copied and the *total* number of pages. + If *progress* is specified, it must either be ``None`` or a callable object that + will be executed at each iteration with three integer arguments, respectively + the *status* of the last iteration, the *remaining* number of pages still to be + copied and the *total* number of pages. The *name* argument specifies the database name that will be copied: it must be a string containing either ``"main"``, the default, to indicate the main @@ -545,7 +546,7 @@ Connection Objects # Copy an existing database into another file import sqlite3 - def progress(remaining, total): + def progress(status, remaining, total): print(f"Copied {total-remaining} of {total} pages...") con = sqlite3.connect('existing_db.db') diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index 5bf9b24524e7e6..c4eb51d150a728 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -31,21 +31,21 @@ def CheckSimple(self): def CheckProgress(self): journal = [] - def progress(remaining, total): - journal.append(remaining) + def progress(status, remaining, total): + journal.append(status) with NamedTemporaryFile(suffix='.sqlite') as bckfn: self.cx.backup(bckfn.name, pages=1, progress=progress) self.testBackup(bckfn.name) self.assertEqual(len(journal), 2) - self.assertEqual(journal[0], 1) - self.assertEqual(journal[1], 0) + self.assertEqual(journal[0], sqlite.SQLITE_OK) + self.assertEqual(journal[1], sqlite.SQLITE_DONE) def CheckProgressAllPagesAtOnce_0(self): journal = [] - def progress(remaining, total): + def progress(status, remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: @@ -58,7 +58,7 @@ def progress(remaining, total): def CheckProgressAllPagesAtOnce_1(self): journal = [] - def progress(remaining, total): + def progress(status, remaining, total): journal.append(remaining) with NamedTemporaryFile(suffix='.sqlite') as bckfn: @@ -77,7 +77,7 @@ def CheckNonCallableProgress(self): def CheckModifyingProgress(self): journal = [] - def progress(remaining, total): + def progress(status, remaining, total): if not journal: self.cx.execute('INSERT INTO foo (key) VALUES (?)', (remaining+1000,)) self.cx.commit() @@ -99,7 +99,7 @@ def progress(remaining, total): self.assertEqual(journal[2], 0) def CheckFailingProgress(self): - def progress(remaining, total): + def progress(status, remaining, total): raise SystemError('nearly out of space') with NamedTemporaryFile(suffix='.sqlite') as bckfn: diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index fb58b2e8d548dd..aa9a257f09c55e 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1528,7 +1528,7 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* Py_END_ALLOW_THREADS if (progress != Py_None) { - if (!PyObject_CallFunction(progress, "ii", + if (!PyObject_CallFunction(progress, "iii", rc, sqlite3_backup_remaining(bckhandle), sqlite3_backup_pagecount(bckhandle))) { /* User's callback raised an error: interrupt the loop and diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 72c3a7f34fca0e..07eeaa2a696a10 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -315,6 +315,9 @@ static const IntConstantPair _int_constants[] = { #endif #if SQLITE_VERSION_NUMBER >= 3008003 {"SQLITE_RECURSIVE", SQLITE_RECURSIVE}, +#endif +#if SQLITE_VERSION_NUMBER >= 3006011 + {"SQLITE_DONE", SQLITE_DONE}, #endif {(char*)NULL, 0} }; From d688f20dc624e2d235bab5e8cc307e08f00e2176 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Sat, 4 Mar 2017 10:45:43 +0100 Subject: [PATCH 20/21] Slightly different way handling backup step's error state This should address Aviv's concerns about how the sqlite3_backup_step() errors are handled. --- Modules/_sqlite/connection.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index aa9a257f09c55e..23bb7091476218 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1491,6 +1491,7 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* char* name = "main"; PyObject* retval = NULL; int rc; + int cberr = 0; sqlite3 *bckconn; sqlite3_backup *bckhandle; static char *keywords[] = {"filename", "pages", "progress", "name", NULL}; @@ -1533,6 +1534,7 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* sqlite3_backup_pagecount(bckhandle))) { /* User's callback raised an error: interrupt the loop and propagate it. */ + cberr = 1; rc = -1; } } @@ -1547,19 +1549,28 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); Py_BEGIN_ALLOW_THREADS - sqlite3_backup_finish(bckhandle); + rc = sqlite3_backup_finish(bckhandle); Py_END_ALLOW_THREADS + } else { + rc = _pysqlite_seterror(bckconn, NULL); } - if (rc != -1) { - rc = _pysqlite_seterror(bckconn, NULL); + if (cberr == 0 && rc != SQLITE_OK) { + /* We cannot use _pysqlite_seterror() here because the backup APIs do + not set the error status on the connection object, but rather on + the backup handle. */ + if (rc == SQLITE_NOMEM) { + (void)PyErr_NoMemory(); + } else { + PyErr_SetString(pysqlite_OperationalError, sqlite3_errstr(rc)); + } } Py_BEGIN_ALLOW_THREADS sqlite3_close(bckconn); Py_END_ALLOW_THREADS - if (rc == SQLITE_OK) { + if (cberr == 0 && rc == SQLITE_OK) { Py_INCREF(Py_None); retval = Py_None; } else { From 4ed6c3142732222c0f959ca385e9144937f59522 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Sat, 4 Mar 2017 11:13:13 +0100 Subject: [PATCH 21/21] When an error occurs while the backup is going on, remove the target file --- Lib/sqlite3/test/backup.py | 7 +++++-- Modules/_sqlite/connection.c | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Lib/sqlite3/test/backup.py b/Lib/sqlite3/test/backup.py index c4eb51d150a728..f121b538b721c5 100644 --- a/Lib/sqlite3/test/backup.py +++ b/Lib/sqlite3/test/backup.py @@ -1,3 +1,4 @@ +import os import sqlite3 as sqlite from tempfile import NamedTemporaryFile import unittest @@ -102,17 +103,19 @@ def CheckFailingProgress(self): def progress(status, remaining, total): raise SystemError('nearly out of space') - with NamedTemporaryFile(suffix='.sqlite') as bckfn: + with NamedTemporaryFile(suffix='.sqlite', delete=False) as bckfn: with self.assertRaises(SystemError) as err: self.cx.backup(bckfn.name, progress=progress) self.assertEqual(str(err.exception), 'nearly out of space') + self.assertFalse(os.path.exists(bckfn.name)) def CheckDatabaseSourceName(self): - with NamedTemporaryFile(suffix='.sqlite') as bckfn: + with NamedTemporaryFile(suffix='.sqlite', delete=False) as bckfn: self.cx.backup(bckfn.name, name='main') self.cx.backup(bckfn.name, name='temp') with self.assertRaises(sqlite.OperationalError): self.cx.backup(bckfn.name, name='non-existing') + self.assertFalse(os.path.exists(bckfn.name)) self.cx.execute("ATTACH DATABASE ':memory:' AS attached_db") self.cx.execute('CREATE TABLE attached_db.foo (key INTEGER)') self.cx.executemany('INSERT INTO attached_db.foo (key) VALUES (?)', [(3,), (4,)]) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 23bb7091476218..c0935f715bddc3 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -21,6 +21,12 @@ * 3. This notice may not be removed or altered from any source distribution. */ +#ifdef HAVE_UNISTD_H +#include +#else +extern int unlink(const char *); +#endif + #include "cache.h" #include "module.h" #include "structmember.h" @@ -1574,7 +1580,12 @@ pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, PyObject* Py_INCREF(Py_None); retval = Py_None; } else { - /* TODO: should the (probably incomplete/invalid) backup be removed here? */ + /* Remove the probably incomplete/invalid backup */ + if (unlink(filename) < 0) { + /* FIXME: this should probably be chained to the outstanding + exception */ + return PyErr_SetFromErrno(PyExc_OSError); + } } finally: