Skip to content

Commit f02bf5a

Browse files
quinnjclaude
andcommitted
Fix nested transactions and update SQLite_jll (#341)
- Add `intransaction(db)` helper to detect if already in a transaction - Fix `transaction()` to use savepoints when nested and skip PRAGMA statements that cannot run inside transactions - Update SQLite_jll compat to 3.51 - Add test for issue #341 (load! inside transaction) Fixes #341 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 079f6c2 commit f02bf5a

3 files changed

Lines changed: 61 additions & 6 deletions

File tree

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "SQLite"
22
uuid = "0aa819cd-b072-5ff4-a722-6bc24af294d9"
3-
version = "1.7.0"
3+
version = "1.7.1"
44
authors = ["Jacob Quinn <quinn.jacobd@gmail.com>", "JuliaData Contributors"]
55

66
[deps]
@@ -13,7 +13,7 @@ WeakRefStrings = "ea10d353-3f73-51f8-a26c-33c1cb351aa5"
1313

1414
[compat]
1515
DBInterface = "2.5"
16-
SQLite_jll = "3"
16+
SQLite_jll = "3.51"
1717
Tables = "1"
1818
WeakRefStrings = "0.4,0.5,0.6,1"
1919
julia = "1.9"

src/SQLite.jl

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ Base.close(db::DB) = _close_db!(db)
8282
Base.isopen(db::DB) = isopen(db.handle)
8383
Base.isopen(handle::DBHandle) = handle != C_NULL
8484

85+
"""
86+
SQLite.intransaction(db::DB) -> Bool
87+
88+
Check if the database connection is currently in a transaction.
89+
Returns `true` if in a transaction, `false` if in autocommit mode.
90+
"""
91+
intransaction(db::DB) = C.sqlite3_get_autocommit(db.handle) == 0
92+
8593
function finalize_statements!(db::DB)
8694
# close stmts
8795
for stmt_wrapper in keys(db.stmt_wrappers)
@@ -650,9 +658,16 @@ In the second method, `func` is executed within a transaction (the transaction b
650658
function transaction end
651659

652660
function transaction(db::DB, mode = "DEFERRED")
653-
execute(db, "PRAGMA temp_store=MEMORY;")
661+
already_in_transaction = intransaction(db)
662+
# PRAGMA statements cannot be executed inside a transaction
663+
already_in_transaction || execute(db, "PRAGMA temp_store=MEMORY;")
654664
if uppercase(mode) in ["", "DEFERRED", "IMMEDIATE", "EXCLUSIVE"]
655-
execute(db, "BEGIN $(mode) TRANSACTION;")
665+
if already_in_transaction
666+
# If already in a transaction, use a savepoint instead
667+
execute(db, "SAVEPOINT $(mode);")
668+
else
669+
execute(db, "BEGIN $(mode) TRANSACTION;")
670+
end
656671
else
657672
execute(db, "SAVEPOINT $(mode);")
658673
end
@@ -663,7 +678,9 @@ DBInterface.transaction(f, db::DB) = transaction(f, db)
663678
@inline function transaction(f::Function, db::DB)
664679
# generate a random name for the savepoint
665680
name = string("SQLITE", Random.randstring(10))
666-
execute(db, "PRAGMA synchronous = OFF;")
681+
already_in_transaction = intransaction(db)
682+
# PRAGMA statements cannot be executed inside a transaction
683+
already_in_transaction || execute(db, "PRAGMA synchronous = OFF;")
667684
transaction(db, name)
668685
try
669686
f()
@@ -673,7 +690,7 @@ DBInterface.transaction(f, db::DB) = transaction(f, db)
673690
finally
674691
# savepoints are not released on rollback
675692
commit(db, name)
676-
execute(db, "PRAGMA synchronous = ON;")
693+
already_in_transaction || execute(db, "PRAGMA synchronous = ON;")
677694
end
678695
end
679696

test/runtests.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,44 @@ end
300300
end
301301
end
302302

303+
@testset "Issue #341: load! inside transaction" begin
304+
db = SQLite.DB()
305+
# Test that load! works when called inside an existing transaction
306+
data = [(a=1, b="x"), (a=2, b="y"), (a=3, b="z")]
307+
308+
# First verify load! works outside a transaction
309+
SQLite.load!(data, db, "test_outside")
310+
result = DBInterface.execute(db, "SELECT * FROM test_outside") |> columntable
311+
@test result.a == [1, 2, 3]
312+
@test result.b == ["x", "y", "z"]
313+
314+
# Now test load! inside a transaction (this is what issue #341 reported as failing)
315+
DBInterface.transaction(db) do
316+
data2 = [(c=10, d="hello"), (c=20, d="world")]
317+
SQLite.load!(data2, db, "test_inside")
318+
end
319+
result2 = DBInterface.execute(db, "SELECT * FROM test_inside") |> columntable
320+
@test result2.c == [10, 20]
321+
@test result2.d == ["hello", "world"]
322+
323+
# Test nested transaction with rollback
324+
DBInterface.transaction(db) do
325+
data3 = [(e=100,)]
326+
SQLite.load!(data3, db, "test_rollback")
327+
# This table should exist within the transaction
328+
@test DBInterface.execute(db, "SELECT * FROM test_rollback") |> columntable |> x -> x.e == [100]
329+
end
330+
# Table should still exist after commit
331+
@test DBInterface.execute(db, "SELECT * FROM test_rollback") |> columntable |> x -> x.e == [100]
332+
333+
# Test intransaction helper
334+
@test SQLite.intransaction(db) == false
335+
SQLite.transaction(db)
336+
@test SQLite.intransaction(db) == true
337+
SQLite.commit(db)
338+
@test SQLite.intransaction(db) == false
339+
end
340+
303341
@testset "Dates" begin
304342
setup_clean_test_db() do db
305343
DBInterface.execute(db, "create table temp as select * from album")

0 commit comments

Comments
 (0)