Repository navigation
bpo-27645: Supporting native backup facility of SQLite #4238
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
Changes from 35 commits
0c9a4e5
624849a
b17e572
3a64654
8220ed6
f7c6fc5
df607df
169369b
7ee5341
f88cd12
7ee018c
21bfc82
9574b91
09407c8
f99e65e
aaa1508
13de3a1
9b2f47a
ce55873
5a08168
960303f
4bd0b3e
7dc53f0
a333639
4ea5ca4
48fd04b
3256d52
a04a86e
a4334a6
69d8996
ac5c64b
09671e4
b5260e0
37f316f
b7fcf9e
6566166
e6f8950
5b06a74
5c20723
2155feb
d21c9cb
8e096c2
a2f15bc
66df8b3
814ef4e
8fe5c30
acc2f37
32285b1
7943561
824d5f8
6fd854a
57f49c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -521,6 +521,55 @@ Connection Objects | |
| f.write('%s\n' % line) | ||
|
|
||
|
|
||
| .. method:: backup(target, *, pages=0, progress=None, name="main", sleep=0.250) | ||
|
|
||
| This method makes a backup of a SQLite database into the mandatory argument | ||
| *target*, even while it's being accessed by other clients, or concurrently by | ||
| the same connection. The *target* can be either a string, the path to the file | ||
| where the backup will be written, or another :class:`Connection` instance. | ||
|
|
||
| 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. | ||
|
|
||
| The *sleep* argument specifies the number of seconds to sleep by between | ||
| successive attempts to backup remaining pages, can be specified either as an | ||
| integer or a floating point value. | ||
|
|
||
| Example 1, 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) | ||
|
|
||
| Example 2, copy an existing database into a transient copy:: | ||
|
|
||
| import sqlite3 | ||
|
|
||
| source = sqlite3.connect('existing_db.db') | ||
| dest = sqlite3.connect(':memory') | ||
|
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.
Contributor
Author
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. Fixed. |
||
| source.backup(dest) | ||
|
|
||
| .. 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import os | ||
| import sqlite3 as sqlite | ||
| import unittest | ||
|
|
||
| from test import support | ||
|
|
||
|
|
||
| @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() | ||
| os.mkdir(support.TESTFN) | ||
| self.addCleanup(support.rmtree, support.TESTFN) | ||
| self.temp_counter = 0 | ||
|
|
||
| @property | ||
| def temp_file_name(self): | ||
| self.temp_counter += 1 | ||
| return os.path.join(support.TESTFN, 'bcktest_%d' % self.temp_counter) | ||
|
|
||
| def tearDown(self): | ||
| self.cx.close() | ||
|
|
||
| def verify_backup(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 test_bad_target(self): | ||
| with self.assertRaises(TypeError): | ||
| self.cx.backup(None) | ||
|
|
||
| def test_keyword_only_args(self): | ||
| with self.assertRaises(TypeError): | ||
| self.cx.backup('foo', 1) | ||
|
|
||
| def test_simple(self): | ||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn) | ||
| self.verify_backup(bckfn) | ||
|
|
||
| def test_progress(self): | ||
| journal = [] | ||
|
|
||
| def progress(status, remaining, total): | ||
| journal.append(status) | ||
|
|
||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, pages=1, progress=progress) | ||
| self.verify_backup(bckfn) | ||
|
|
||
| self.assertEqual(len(journal), 2) | ||
| self.assertEqual(journal[0], sqlite.SQLITE_OK) | ||
| self.assertEqual(journal[1], sqlite.SQLITE_DONE) | ||
|
|
||
| def test_progress_all_pages_at_once_1(self): | ||
| journal = [] | ||
|
|
||
| def progress(status, remaining, total): | ||
| journal.append(remaining) | ||
|
|
||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, progress=progress) | ||
| self.verify_backup(bckfn) | ||
|
|
||
| self.assertEqual(len(journal), 1) | ||
| self.assertEqual(journal[0], 0) | ||
|
|
||
| def test_progress_all_pages_at_once_2(self): | ||
| journal = [] | ||
|
|
||
| def progress(status, remaining, total): | ||
| journal.append(remaining) | ||
|
|
||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, pages=-1, progress=progress) | ||
| self.verify_backup(bckfn) | ||
|
|
||
| self.assertEqual(len(journal), 1) | ||
| self.assertEqual(journal[0], 0) | ||
|
|
||
| def test_non_callable_progress(self): | ||
| bckfn = self.temp_file_name | ||
| with self.assertRaises(TypeError) as err: | ||
| self.cx.backup(bckfn, pages=1, progress='bar') | ||
| self.assertEqual(str(err.exception), 'progress argument must be a callable') | ||
|
|
||
| def test_modifying_progress(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) | ||
|
|
||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, pages=1, progress=progress) | ||
| self.verify_backup(bckfn) | ||
|
|
||
| cx = sqlite.connect(bckfn) | ||
| 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 test_failing_progress(self): | ||
| def progress(status, remaining, total): | ||
| raise SystemError('nearly out of space') | ||
|
|
||
| bckfn = self.temp_file_name | ||
| with self.assertRaises(SystemError) as err: | ||
| self.cx.backup(bckfn, progress=progress) | ||
| self.assertEqual(str(err.exception), 'nearly out of space') | ||
| self.assertFalse(os.path.exists(bckfn)) | ||
|
|
||
| def test_database_source_name(self): | ||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, name='main') | ||
| self.cx.backup(bckfn, name='temp') | ||
| with self.assertRaises(sqlite.OperationalError): | ||
| self.cx.backup(bckfn, name='non-existing') | ||
| self.assertFalse(os.path.exists(bckfn)) | ||
| 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() | ||
| bckfn = self.temp_file_name | ||
| self.cx.backup(bckfn, name='attached_db') | ||
| self.verify_backup(bckfn) | ||
|
|
||
| def test_backup_to_other_connection(self): | ||
| dx = sqlite.connect(':memory:') | ||
| self.cx.backup(dx) | ||
| result = dx.execute("SELECT key FROM foo ORDER BY key").fetchall() | ||
| self.assertEqual(result[0][0], 3) | ||
| self.assertEqual(result[1][0], 4) | ||
|
|
||
| def test_backup_to_other_connection_in_transaction(self): | ||
| dx = sqlite.connect(':memory:') | ||
| dx.execute('CREATE TABLE bar (key INTEGER)') | ||
| dx.executemany('INSERT INTO bar (key) VALUES (?)', [(3,), (4,)]) | ||
| try: | ||
| with self.assertRaises(sqlite.OperationalError): | ||
| self.cx.backup(dx) | ||
| finally: | ||
| dx.rollback() | ||
|
|
||
| def suite(): | ||
| return unittest.TestSuite(unittest.makeSuite(BackupTests, "test_")) | ||
|
|
||
| def test(): | ||
| runner = unittest.TextTestRunner() | ||
| runner.run(suite()) | ||
|
|
||
| if __name__ == "__main__": | ||
| test() | ||
|
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. Does not
Contributor
Author
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. Yes, now that the backup suite uses snake_like method names. Done. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| sqlite3.Connection now exposes a backup() method, if the underlying SQLite | ||
| library is at version 3.6.11 or higher. Patch by Lele Gaifax | ||
|
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. Period.
Contributor
Author
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. Done. |
||
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.
I think
*aroundpagesis not needed here.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.
You mean only the first reference to the argument should be emphasized?
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.
I think this isn't a reference to the argument, but a unit. If you want to use a reference to the argument you should write
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.
Ok.