Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bpo-27645: Supporting native backup facility of SQLite #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d93391d
Expose the SQLite Online Backup API as Connection.backup()
lelit Jul 28, 2016
7b1277a
Preliminary documentation for sqlite3.Connection.backup()
lelit Jul 28, 2016
1258d47
Reduce code nesting depth by using a finally label, as other function…
lelit Mar 1, 2017
d8ea457
Wrap SQLite calls between BEGIN_ALLOW_THREADS and END_ALLOW_THREADS
lelit Mar 1, 2017
35fa0ad
Add an entry in Misc/NEWS related to issue #27645
lelit Mar 1, 2017
c1af5a0
Remove extraneous entry introduced in d4439d6c5b2a67682f3db74d395c603…
lelit Mar 1, 2017
c07d982
Attribute the patch to myself
lelit Mar 1, 2017
cdc7eda
Make pages and progress keyword-only args in sqlite3.Connection.backup()
lelit Mar 1, 2017
6d3eb69
Remove reference to SQLite docs in Connection.backup() documentation
lelit Mar 1, 2017
6fab17f
Use f-string in the sqlite3.Connection.backup() example
lelit Mar 1, 2017
6449f3d
Explicitly mark sqlite3.Connection.backup() as added in v3.7
lelit Mar 1, 2017
f84d375
Reduce chances of cluttering future “git blame”
lelit Mar 1, 2017
e748942
Parametrize the name of the database involved in the backup
lelit Mar 3, 2017
426dad4
Test propagation of exception raised in backup's progress callback
lelit Mar 3, 2017
895726b
Use a better name for variable
lelit Mar 3, 2017
77dfa39
Do not delay next iteration if the result was OK
lelit Mar 3, 2017
71726ba
Omit the backup method when underlying SQLite library is older than 3…
lelit Mar 3, 2017
c4fbc20
Assert that the non-mandatory arguments are keyword-only
lelit Mar 3, 2017
ff1609b
Pass also the current status of the ongoing backup to the progress ca…
lelit Mar 4, 2017
d688f20
Slightly different way handling backup step's error state
lelit Mar 4, 2017
4ed6c31
When an error occurs while the backup is going on, remove the target …
lelit Mar 4, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,43 @@ Connection Objects
f.write('%s\n' % line)


.. 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
the same connection.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention that backups work for on-disk and in-memory databases like the sqlite3 C api docs mention?


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 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
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
import sqlite3

def progress(status, 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)

.. note:: This is available only when the underlying SQLite library is at
version 3.6.11 or higher.

.. versionadded:: 3.7


.. _sqlite3-cursor-objects:

Cursor Objects
Expand Down
135 changes: 135 additions & 0 deletions Lib/sqlite3/test/backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import os
import sqlite3 as sqlite
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:")
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 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)
self.testBackup(bckfn.name)

def CheckProgress(self):
journal = []

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], sqlite.SQLITE_OK)
self.assertEqual(journal[1], sqlite.SQLITE_DONE)

def CheckProgressAllPagesAtOnce_0(self):
journal = []

def progress(status, remaining, total):
journal.append(remaining)

with NamedTemporaryFile(suffix='.sqlite') as bckfn:
self.cx.backup(bckfn.name, progress=progress)
self.testBackup(bckfn.name)

self.assertEqual(len(journal), 1)
self.assertEqual(journal[0], 0)

def CheckProgressAllPagesAtOnce_1(self):
journal = []

def progress(status, remaining, total):
journal.append(remaining)

with NamedTemporaryFile(suffix='.sqlite') as bckfn:
self.cx.backup(bckfn.name, pages=-1, progress=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 err:
self.cx.backup(bckfn.name, pages=1, progress='bar')
self.assertEqual(str(err.exception), 'progress argument must be a callable')

def CheckModifyingProgress(self):
journal = []

def progress(status, 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, pages=1, progress=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 CheckFailingProgress(self):
def progress(status, remaining, total):
raise SystemError('nearly out of space')

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', 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,)])
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"))

def test():
runner = unittest.TextTestRunner()
runner.run(suite())

if __name__ == "__main__":
test()
5 changes: 3 additions & 2 deletions Lib/test/test_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -18,7 +18,8 @@ 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()
3 changes: 3 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ What's New in Python 3.7.0 alpha 1?
Core and Builtins
-----------------

- 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.

Expand Down
120 changes: 120 additions & 0 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
* 3. This notice may not be removed or altered from any source distribution.
*/

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#else
extern int unlink(const char *);
#endif

#include "cache.h"
#include "module.h"
#include "structmember.h"
Expand All @@ -41,6 +47,10 @@
#endif
#endif

#if SQLITE_VERSION_NUMBER >= 3006011
#define HAVE_BACKUP_API
#endif

_Py_IDENTIFIER(cursor);

static const char * const begin_statements[] = {
Expand Down Expand Up @@ -1477,6 +1487,112 @@ 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)
{
char* filename;
int pages = -1;
PyObject* progress = Py_None;
char* name = "main";
PyObject* retval = NULL;
int rc;
int cberr = 0;
sqlite3 *bckconn;
sqlite3_backup *bckhandle;
static char *keywords[] = {"filename", "pages", "progress", "name", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|$iOs:backup", keywords,
&filename, &pages, &progress, &name)) {
goto finally;
}

if (progress != Py_None && !PyCallable_Check(progress)) {
PyErr_SetString(PyExc_TypeError, "progress argument must be a callable");
goto finally;
}

if (pages == 0) {
pages = -1;
}

Py_BEGIN_ALLOW_THREADS
rc = sqlite3_open(filename, &bckconn);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#359 could be useful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you are suggesting: should I take the same approach of conditionally using sqlite3_open_v2() instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to say that this should be merged after #359.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This don't actually have any effect. sqlite3_open(filename, db) is the same as sqlite3_open_v2(filename, db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)

Py_END_ALLOW_THREADS

if (rc != SQLITE_OK) {
goto finally;
}

Py_BEGIN_ALLOW_THREADS
bckhandle = sqlite3_backup_init(bckconn, "main", self->db, name);
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call progress only when sqlite3_backup_step return SQLITE_OK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about that: maybe a better option would be pass the result status to the progress callback, so that it can eventually signal the error and do some cleanup (think about closing a progress dialog).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the progress callback receives three arguments.

if (!PyObject_CallFunction(progress, "iii", rc,
sqlite3_backup_remaining(bckhandle),
sqlite3_backup_pagecount(bckhandle))) {
/* User's callback raised an error: interrupt the loop and
propagate it. */
cberr = 1;
rc = -1;
}
}

/* 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
}
} while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);

Py_BEGIN_ALLOW_THREADS
rc = sqlite3_backup_finish(bckhandle);
Py_END_ALLOW_THREADS
} else {
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 (cberr == 0 && rc == SQLITE_OK) {
Py_INCREF(Py_None);
retval = Py_None;
} else {
/* 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:
return retval;
}
#endif

static PyObject *
pysqlite_connection_create_collation(pysqlite_Connection* self, PyObject* args)
{
Expand Down Expand Up @@ -1649,6 +1765,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,
Expand Down
3 changes: 3 additions & 0 deletions Modules/_sqlite/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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}
};
Expand Down