summaryrefslogtreecommitdiffstats
path: root/Modules/_sqlite
diff options
context:
space:
mode:
authorEmanuele Gaifas <lelegaifax@gmail.com>2018-03-10 22:08:31 (GMT)
committerBerker Peksag <berker.peksag@gmail.com>2018-03-10 22:08:31 (GMT)
commitd7aed4102d2a40c74553240c7f03585624d27aea (patch)
treef65be332675189f8a73fbfdf543c28a23f96a795 /Modules/_sqlite
parentc10b288f345aaef66d2c844924b9a576f9ea4f8b (diff)
downloadcpython-d7aed4102d2a40c74553240c7f03585624d27aea.zip
cpython-d7aed4102d2a40c74553240c7f03585624d27aea.tar.gz
cpython-d7aed4102d2a40c74553240c7f03585624d27aea.tar.bz2
bpo-27645: Add support for native backup facility of SQLite (GH-4238)
Diffstat (limited to 'Modules/_sqlite')
-rw-r--r--Modules/_sqlite/connection.c139
-rw-r--r--Modules/_sqlite/module.c3
2 files changed, 142 insertions, 0 deletions
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index 3e83fb6..14b6a27 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[] = {
@@ -1447,6 +1451,137 @@ finally:
return retval;
}
+#ifdef HAVE_BACKUP_API
+static PyObject *
+pysqlite_connection_backup(pysqlite_Connection *self, PyObject *args, PyObject *kwds)
+{
+ PyObject *target = NULL;
+ int pages = -1;
+ PyObject *progress = Py_None;
+ const char *name = "main";
+ int rc;
+ int callback_error = 0;
+ double sleep_secs = 0.250;
+ sqlite3 *bck_conn;
+ sqlite3_backup *bck_handle;
+ static char *keywords[] = {"target", "pages", "progress", "name", "sleep", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|$iOsd:backup", keywords,
+ &pysqlite_ConnectionType, &target,
+ &pages, &progress, &name, &sleep_secs)) {
+ return NULL;
+ }
+
+ if (!pysqlite_check_connection((pysqlite_Connection *)target)) {
+ return NULL;
+ }
+
+ if ((pysqlite_Connection *)target == self) {
+ PyErr_SetString(PyExc_ValueError, "target cannot be the same connection instance");
+ return NULL;
+ }
+
+#if SQLITE_VERSION_NUMBER < 3008007
+ /* Since 3.8.7 this is already done, per commit
+ https://www.sqlite.org/src/info/169b5505498c0a7e */
+ if (!sqlite3_get_autocommit(((pysqlite_Connection *)target)->db)) {
+ PyErr_SetString(pysqlite_OperationalError, "target is in transaction");
+ return NULL;
+ }
+#endif
+
+ if (progress != Py_None && !PyCallable_Check(progress)) {
+ PyErr_SetString(PyExc_TypeError, "progress argument must be a callable");
+ return NULL;
+ }
+
+ if (pages == 0) {
+ pages = -1;
+ }
+
+ bck_conn = ((pysqlite_Connection *)target)->db;
+
+ Py_BEGIN_ALLOW_THREADS
+ bck_handle = sqlite3_backup_init(bck_conn, "main", self->db, name);
+ Py_END_ALLOW_THREADS
+
+ if (bck_handle) {
+ do {
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_backup_step(bck_handle, pages);
+ Py_END_ALLOW_THREADS
+
+ if (progress != Py_None) {
+ PyObject *res;
+
+ res = PyObject_CallFunction(progress, "iii", rc,
+ sqlite3_backup_remaining(bck_handle),
+ sqlite3_backup_pagecount(bck_handle));
+ if (res == NULL) {
+ /* User's callback raised an error: interrupt the loop and
+ propagate it. */
+ callback_error = 1;
+ rc = -1;
+ } else {
+ Py_DECREF(res);
+ }
+ }
+
+ /* Sleep for a while 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(sleep_secs * 1000.0);
+ Py_END_ALLOW_THREADS
+ }
+ } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);
+
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_backup_finish(bck_handle);
+ Py_END_ALLOW_THREADS
+ } else {
+ rc = _pysqlite_seterror(bck_conn, NULL);
+ }
+
+ if (!callback_error && 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 {
+#if SQLITE_VERSION_NUMBER > 3007015
+ PyErr_SetString(pysqlite_OperationalError, sqlite3_errstr(rc));
+#else
+ switch (rc) {
+ case SQLITE_READONLY:
+ PyErr_SetString(pysqlite_OperationalError,
+ "attempt to write a readonly database");
+ break;
+ case SQLITE_BUSY:
+ PyErr_SetString(pysqlite_OperationalError, "database is locked");
+ break;
+ case SQLITE_LOCKED:
+ PyErr_SetString(pysqlite_OperationalError,
+ "database table is locked");
+ break;
+ default:
+ PyErr_Format(pysqlite_OperationalError,
+ "unrecognized error code: %d", rc);
+ break;
+ }
+#endif
+ }
+ }
+
+ if (!callback_error && rc == SQLITE_OK) {
+ Py_RETURN_NONE;
+ } else {
+ return NULL;
+ }
+}
+#endif
+
static PyObject *
pysqlite_connection_create_collation(pysqlite_Connection* self, PyObject* args)
{
@@ -1619,6 +1754,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,
diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c
index 879c666..6befa07 100644
--- a/Modules/_sqlite/module.c
+++ b/Modules/_sqlite/module.c
@@ -323,6 +323,9 @@ static const IntConstantPair _int_constants[] = {
#if SQLITE_VERSION_NUMBER >= 3008003
{"SQLITE_RECURSIVE", SQLITE_RECURSIVE},
#endif
+#if SQLITE_VERSION_NUMBER >= 3006011
+ {"SQLITE_DONE", SQLITE_DONE},
+#endif
{(char*)NULL, 0}
};