From b465a902bdef58cafa0b0a620502b2620bd3fade Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Tue, 27 Jan 2026 17:17:15 -0700 Subject: [PATCH 1/2] Add backup API with configurable sleep --- src/SQLite.jl | 42 ++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 24 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/SQLite.jl b/src/SQLite.jl index a7f9bf6..d8f2927 100644 --- a/src/SQLite.jl +++ b/src/SQLite.jl @@ -90,6 +90,48 @@ Returns `true` if in a transaction, `false` if in autocommit mode. """ intransaction(db::DB) = C.sqlite3_get_autocommit(db.handle) == 0 +""" + SQLite.backup(db::SQLite.DB, path::AbstractString; sleep_ms::Integer=250) + +Create a backup of `db` at `path` using the SQLite backup API. +`sleep_ms` controls the delay when the backup encounters `SQLITE_BUSY` +or `SQLITE_LOCKED`. +Returns `path`. +""" +function backup(db::DB, path::AbstractString; sleep_ms::Integer = 250) + isopen(db) || throw(SQLiteException("DB is closed")) + dest = DB(path) + backup_handle = C.sqlite3_backup_init(dest.handle, "main", db.handle, "main") + if backup_handle == C_NULL + err = sqliteexception(dest) + close(dest) + throw(err) + end + rc = C.SQLITE_OK + finish_rc = C.SQLITE_OK + try + while true + rc = C.sqlite3_backup_step(backup_handle, -1) + if rc == C.SQLITE_OK + continue + elseif rc == C.SQLITE_BUSY || rc == C.SQLITE_LOCKED + C.sqlite3_sleep(Cint(sleep_ms)) + continue + end + break + end + finally + finish_rc = C.sqlite3_backup_finish(backup_handle) + end + if rc != C.SQLITE_DONE || finish_rc != C.SQLITE_OK + err = sqliteexception(dest) + close(dest) + throw(err) + end + close(dest) + return path +end + function finalize_statements!(db::DB) # close stmts for stmt_wrapper in keys(db.stmt_wrappers) diff --git a/test/runtests.jl b/test/runtests.jl index 3a86863..912d4d4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -854,6 +854,30 @@ end @test SQLite.busy_timeout(db, 300) == 0 end + @testset "backup" begin + db = SQLite.DB() + DBInterface.execute(db, "CREATE TABLE backup_test (x INT)") + DBInterface.execute(db, "INSERT INTO backup_test VALUES (1), (2)") + tmp_path, tmp_io = mktemp() + close(tmp_io) + try + @test SQLite.backup(db, tmp_path; sleep_ms = 1) == tmp_path + db2 = SQLite.DB(tmp_path) + try + r = DBInterface.execute( + db2, + "SELECT x FROM backup_test ORDER BY x", + ) |> columntable + @test r.x == [1, 2] + finally + close(db2) + end + finally + close(db) + rm(tmp_path; force = true) + end + end + @testset "Issue #253: Ensure query column names are unique by default" begin db = SQLite.DB() res = From c1a28c1c5d5f800b254f5e4fd76bffa81435b5b7 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Tue, 27 Jan 2026 17:32:28 -0700 Subject: [PATCH 2/2] chore(version): bump to 1.8.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ad23f97..b50f08e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "SQLite" uuid = "0aa819cd-b072-5ff4-a722-6bc24af294d9" -version = "1.7.1" +version = "1.8.0" authors = ["Jacob Quinn ", "JuliaData Contributors"] [deps]