Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35.2k
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
d93391d7b1277a1258d47d8ea45735fa0adc1af5a0c07d982cdc7eda6d3eb696fab17f6449f3df84d375e748942426dad4895726b77dfa3971726bac4fbc20ff1609bd688f204ed6c31File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| @@ -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[] = { | ||
| @@ -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); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #359 could be useful here. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was trying to say that this should be merged after#359. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This don't actually have any effect. | ||
| 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) { | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should call ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| { | ||
| @@ -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, | ||
There was a problem hiding this comment.
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?