Skip to content

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

Merged
merged 52 commits into from
Mar 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
0c9a4e5
Expose the SQLite Online Backup API as Connection.backup()
lelit Jul 28, 2016
624849a
Preliminary documentation for sqlite3.Connection.backup()
lelit Jul 28, 2016
b17e572
Reduce code nesting depth by using a finally label, as other function…
lelit Mar 1, 2017
3a64654
Wrap SQLite calls between BEGIN_ALLOW_THREADS and END_ALLOW_THREADS
lelit Mar 1, 2017
8220ed6
Make pages and progress keyword-only args in sqlite3.Connection.backup()
lelit Mar 1, 2017
f7c6fc5
Remove reference to SQLite docs in Connection.backup() documentation
lelit Mar 1, 2017
df607df
Use f-string in the sqlite3.Connection.backup() example
lelit Mar 1, 2017
169369b
Explicitly mark sqlite3.Connection.backup() as added in v3.7
lelit Mar 1, 2017
7ee5341
Reduce chances of cluttering future “git blame”
lelit Mar 1, 2017
f88cd12
Parametrize the name of the database involved in the backup
lelit Mar 3, 2017
7ee018c
Test propagation of exception raised in backup's progress callback
lelit Mar 3, 2017
21bfc82
Use a better name for variable
lelit Mar 3, 2017
9574b91
Do not delay next iteration if the result was OK
lelit Mar 3, 2017
09407c8
Omit the backup method when underlying SQLite library is older than 3…
lelit Mar 3, 2017
f99e65e
Assert that the non-mandatory arguments are keyword-only
lelit Mar 3, 2017
aaa1508
Pass also the current status of the ongoing backup to the progress ca…
lelit Mar 4, 2017
13de3a1
Slightly different way handling backup step's error state
lelit Mar 4, 2017
9b2f47a
When an error occurs while the backup is going on, remove the target …
lelit Mar 4, 2017
ce55873
Add NEWS entry for issue 27645
lelit Oct 5, 2017
5a08168
New argument "sleep" to specify a different number of milliseconds
lelit Oct 24, 2017
960303f
Allow to copy the database to either an external file or to another C…
lelit Oct 24, 2017
4bd0b3e
Check that target backup DB is not in transaction
lelit Oct 24, 2017
7dc53f0
Use DeleteFileW() under MS Windows to delete incomplete backup
lelit Dec 5, 2017
a333639
Second attempt at properly calling DeleteFileW()
lelit Dec 5, 2017
4ea5ca4
Setup exception from sqlite error condition
lelit Dec 5, 2017
48fd04b
Close the backup connection in case of error
lelit Dec 5, 2017
3256d52
Attempt to chain the exception raised by incomplete backup deletion
lelit Dec 5, 2017
a04a86e
Fix double call to PyMem_Free()
lelit Dec 5, 2017
a4334a6
Remove leftover comment
lelit Dec 5, 2017
69d8996
Drop const qualifier to avoid compiler warning
lelit Dec 5, 2017
ac5c64b
Avoid using NamedTemporaryFile in test cases
lelit Dec 5, 2017
09671e4
Make the sleep parameter a double, accepting a value in seconds
lelit Dec 5, 2017
b5260e0
Replace gotos with explicit return, given that no cleanup is needed
lelit Dec 5, 2017
37f316f
Rename test methods using snake_style instead of CamelCase
lelit Dec 5, 2017
b7fcf9e
Use one single pointers declaration style in the new function
lelit Dec 6, 2017
6566166
Fix glitch in example code
lelit Dec 8, 2017
e6f8950
Rectify explanation of the pages parameter
lelit Dec 8, 2017
5b06a74
Add missing ending dot
lelit Dec 8, 2017
5c20723
Simplify code
lelit Dec 8, 2017
2155feb
Add missing const qualifier
lelit Dec 8, 2017
d21c9cb
Fix leak by DECREFing the result of progress callback call
lelit Dec 8, 2017
8e096c2
Restore the suite() function, used by outer Lib/test/test_sqlite.py
lelit Dec 8, 2017
a2f15bc
Simplify backup() implementation
lelit Dec 12, 2017
66df8b3
Remove now useless includes
lelit Dec 12, 2017
814ef4e
Recognize common error codes for old SQLite3 missing sqlite3_errstr()
lelit Dec 12, 2017
8fe5c30
Move argument type check to PyArg_ParseTupleAndKeywords()
lelit Dec 12, 2017
acc2f37
Use better names for local variables
lelit Dec 12, 2017
32285b1
Merge remote-tracking branch 'upstream/master' into pr/4238
berkerpeksag Mar 10, 2018
7943561
Cosmetic fixes
berkerpeksag Mar 10, 2018
824d5f8
Fix segfault in test_bad_target_closed_connection
berkerpeksag Mar 10, 2018
6fd854a
Relax an assert
berkerpeksag Mar 10, 2018
57f49c6
This will be included in 3.7
berkerpeksag Mar 10, 2018
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
50 changes: 50 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,56 @@ 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 even while it's being accessed
by other clients, or concurrently by the same connection. The copy will be
written into the mandatory argument *target*, that must be 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 *pages* 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 DATABASE`` 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::

import sqlite3

def progress(status, remaining, total):
print(f'Copied {total-remaining} of {total} pages...')

con = sqlite3.connect('existing_db.db')
with sqlite3.connect('backup.db') as bck:
con.backup(bck, pages=1, progress=progress)

Example 2, copy an existing database into a transient copy::

import sqlite3

source = sqlite3.connect('existing_db.db')
dest = sqlite3.connect(':memory:')
source.backup(dest)

Availability: SQLite 3.6.11 or higher

.. versionadded:: 3.7


.. _sqlite3-cursor-objects:

Cursor Objects
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,15 @@ can be set within the scope of a group.
``'^$'`` or ``(?=-)`` that matches an empty string.
(Contributed by Serhiy Storchaka in :issue:`25054`.)


sqlite3
-------

:class:`sqlite3.Connection` now exposes a :class:`~sqlite3.Connection.backup`
method, if the underlying SQLite library is at version 3.6.11 or higher.
(Contributed by Lele Gaifax in :issue:`27645`.)


ssl
---

Expand Down
162 changes: 162 additions & 0 deletions Lib/sqlite3/test/backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import sqlite3 as sqlite
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 verify_backup(self, bckcx):
result = bckcx.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_none(self):
with self.assertRaises(TypeError):
self.cx.backup(None)

def test_bad_target_filename(self):
with self.assertRaises(TypeError):
self.cx.backup('some_file_name.db')

def test_bad_target_same_connection(self):
with self.assertRaises(ValueError):
self.cx.backup(self.cx)

def test_bad_target_closed_connection(self):
bck = sqlite.connect(':memory:')
bck.close()
with self.assertRaises(sqlite.ProgrammingError):
self.cx.backup(bck)

def test_bad_target_in_transaction(self):
bck = sqlite.connect(':memory:')
bck.execute('CREATE TABLE bar (key INTEGER)')
bck.executemany('INSERT INTO bar (key) VALUES (?)', [(3,), (4,)])
with self.assertRaises(sqlite.OperationalError) as cm:
self.cx.backup(bck)
if sqlite.sqlite_version_info < (3, 8, 7):
self.assertEqual(str(cm.exception), 'target is in transaction')

def test_keyword_only_args(self):
with self.assertRaises(TypeError):
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, 1)

def test_simple(self):
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck)
self.verify_backup(bck)

def test_progress(self):
journal = []

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

with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, pages=1, progress=progress)
self.verify_backup(bck)

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)

with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, progress=progress)
self.verify_backup(bck)

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)

with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, pages=-1, progress=progress)
self.verify_backup(bck)

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

def test_non_callable_progress(self):
with self.assertRaises(TypeError) as cm:
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, pages=1, progress='bar')
self.assertEqual(str(cm.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)

with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, pages=1, progress=progress)
self.verify_backup(bck)

result = bck.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')

with self.assertRaises(SystemError) as err:
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, progress=progress)
self.assertEqual(str(err.exception), 'nearly out of space')

def test_database_source_name(self):
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, name='main')
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, name='temp')
with self.assertRaises(sqlite.OperationalError) as cm:
with sqlite.connect(':memory:') as bck:
self.cx.backup(bck, name='non-existing')
self.assertIn(
str(cm.exception),
['SQL logic error', 'SQL logic error or missing database']
)

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 sqlite.connect(':memory:') as bck:
self.cx.backup(bck, name='attached_db')
self.verify_backup(bck)


def suite():
return unittest.makeSuite(BackupTests)

if __name__ == "__main__":
unittest.main()
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:class:`sqlite3.Connection` now exposes a :class:`~sqlite3.Connection.backup`
method, if the underlying SQLite library is at version 3.6.11
or higher. Patch by Lele Gaifax.
Loading