Skip to content

Commit 2d1e48b

Browse files
chschuquinnj
authored andcommitted
Add Refs to bound values to avoid premature garbage collection (#180) (#181)
* Store all bound parameter values in Stmt to avoid premature garbage collection (#180) * Determine SERIALIZATION magic bytes dynamically to make it work with (not between) julia versions * Fix AppVeyor build issues on x86 by collect()ing the Date range into an Array * Test that clear!() removes parameter references * Finalize internally created Stmt immediately to release locks
1 parent 387e591 commit 2d1e48b

File tree

3 files changed

+42
-17
lines changed

3 files changed

+42
-17
lines changed

src/SQLite.jl

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ Base.show(io::IO, db::SQLite.DB) = print(io, string("SQLite.DB(", db.file == ":m
5757
mutable struct Stmt
5858
db::DB
5959
handle::Ptr{Cvoid}
60+
params::Dict{Int, Any}
6061

6162
function Stmt(db::DB,sql::AbstractString)
6263
handle = Ref{Ptr{Cvoid}}()
6364
sqliteprepare(db, sql, handle, Ref{Ptr{Cvoid}}())
64-
stmt = new(db, handle[])
65+
stmt = new(db, handle[], Dict{Int, Any}())
6566
finalizer(_close, stmt)
6667
return stmt
6768
end
@@ -85,6 +86,7 @@ clears any bound values to a prepared SQL statement.
8586
"""
8687
function clear!(stmt::Stmt)
8788
sqlite3_clear_bindings(stmt.handle)
89+
empty!(stmt.params)
8890
return
8991
end
9092

@@ -136,14 +138,14 @@ function bind!(stmt::Stmt,name::AbstractString, val)
136138
end
137139
return bind!(stmt, i, val)
138140
end
139-
bind!(stmt::Stmt, i::Int, val::AbstractFloat) = (sqlite3_bind_double(stmt.handle, i ,Float64(val)); return nothing)
140-
bind!(stmt::Stmt, i::Int, val::Int32) = (sqlite3_bind_int(stmt.handle, i ,val); return nothing)
141-
bind!(stmt::Stmt, i::Int, val::Int64) = (sqlite3_bind_int64(stmt.handle, i ,val); return nothing)
142-
bind!(stmt::Stmt, i::Int, val::Missing) = (sqlite3_bind_null(stmt.handle, i ); return nothing)
143-
bind!(stmt::Stmt, i::Int, val::AbstractString) = (sqlite3_bind_text(stmt.handle, i ,val); return nothing)
144-
bind!(stmt::Stmt, i::Int, val::WeakRefString{UInt8}) = (sqlite3_bind_text(stmt.handle, i, val.ptr, val.len); return nothing)
145-
bind!(stmt::Stmt, i::Int, val::WeakRefString{UInt16}) = (sqlite3_bind_text16(stmt.handle, i, val.ptr, val.len*2); return nothing)
146-
bind!(stmt::Stmt, i::Int, val::Vector{UInt8}) = (sqlite3_bind_blob(stmt.handle, i, val); return nothing)
141+
bind!(stmt::Stmt, i::Int, val::AbstractFloat) = (stmt.params[i] = val; sqlite3_bind_double(stmt.handle, i ,Float64(val)); return nothing)
142+
bind!(stmt::Stmt, i::Int, val::Int32) = (stmt.params[i] = val; sqlite3_bind_int(stmt.handle, i ,val); return nothing)
143+
bind!(stmt::Stmt, i::Int, val::Int64) = (stmt.params[i] = val; sqlite3_bind_int64(stmt.handle, i ,val); return nothing)
144+
bind!(stmt::Stmt, i::Int, val::Missing) = (stmt.params[i] = val; sqlite3_bind_null(stmt.handle, i ); return nothing)
145+
bind!(stmt::Stmt, i::Int, val::AbstractString) = (stmt.params[i] = val; sqlite3_bind_text(stmt.handle, i ,val); return nothing)
146+
bind!(stmt::Stmt, i::Int, val::WeakRefString{UInt8}) = (stmt.params[i] = val; sqlite3_bind_text(stmt.handle, i, val.ptr, val.len); return nothing)
147+
bind!(stmt::Stmt, i::Int, val::WeakRefString{UInt16}) = (stmt.params[i] = val; sqlite3_bind_text16(stmt.handle, i, val.ptr, val.len*2); return nothing)
148+
bind!(stmt::Stmt, i::Int, val::Vector{UInt8}) = (stmt.params[i] = val; sqlite3_bind_blob(stmt.handle, i, val); return nothing)
147149
# Fallback is BLOB and defaults to serializing the julia value
148150

149151
# internal wrapper mutable struct to, in-effect, mark something which has been serialized
@@ -169,11 +171,12 @@ struct SerializeError <: Exception
169171
end
170172

171173
# magic bytes that indicate that a value is in fact a serialized julia value, instead of just a byte vector
172-
const SERIALIZATION = UInt8[0x37,0x4a,0x4c,0x07,0x04,0x00,0x00,0x00,0x34,0x10,0x01,0x0a,0x53,0x65,0x72,0x69,0x61,0x6c]
174+
# these bytes depend on the julia version and other things, so they are determined using an actual serialization
175+
const SERIALIZATION = sqlserialize(0)[1:18]
173176

174177
function sqldeserialize(r)
175178
ret = ccall(:memcmp, Int32, (Ptr{UInt8}, Ptr{UInt8}, UInt),
176-
SERIALIZATION, r, min(18, length(r)))
179+
SERIALIZATION, r, min(sizeof(SERIALIZATION), sizeof(r)))
177180
if ret == 0
178181
try
179182
v = Serialization.deserialize(IOBuffer(r))
@@ -252,7 +255,9 @@ end
252255

253256
function execute!(db::DB, sql::AbstractString)
254257
stmt = Stmt(db, sql)
255-
return execute!(stmt)
258+
r = execute!(stmt)
259+
finalize(stmt)
260+
return r
256261
end
257262

258263
"""

src/tables.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,7 @@ load!(db::DB, table::AbstractString="sqlitejl_"*Random.randstring(5); kwargs...)
121121
function load!(itr, db::DB, name::AbstractString="sqlitejl_"*Random.randstring(5); kwargs...)
122122
# check if table exists
123123
nm = esc_id(name)
124-
checkstmt = Stmt(db, "pragma table_info($nm)")
125-
status = execute!(checkstmt)
124+
status = execute!(db, "pragma table_info($nm)")
126125
rows = Tables.rows(itr)
127126
sch = Tables.schema(rows)
128127
return load!(sch, rows, db, nm, name, status == SQLITE_DONE; kwargs...)
@@ -175,4 +174,4 @@ function load!(::Nothing, rows, db::DB, nm::AbstractString, name, shouldcreate;
175174
end
176175
execute!(db, "ANALYZE $nm")
177176
return name
178-
end
177+
end

test/runtests.jl

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ dt = DataFrame(i=collect(rng), j=collect(rng))
8080
tablename = dt |> SQLite.load!(db, "temp")
8181
r = SQLite.Query(db, "select * from $tablename") |> DataFrame
8282
@test size(r) == (5,2)
83-
@test all([i for i in r[1]] .== rng)
83+
@test all([i for i in r[1]] .== collect(rng))
8484
@test all([typeof(i) for i in r[1]] .== Dates.Date)
8585
SQLite.drop!(db, "$tablename")
8686

@@ -253,4 +253,25 @@ end
253253
############################################
254254

255255
#test for #158
256-
@test_throws SQLite.SQLiteException SQLite.DB("nonexistentdir/not_there.db")
256+
@test_throws SQLite.SQLiteException SQLite.DB("nonexistentdir/not_there.db")
257+
258+
#test for #180 (Query)
259+
param = "Hello!"
260+
query = SQLite.Query(SQLite.DB(), "SELECT ?1 UNION ALL SELECT ?1", values = Any[param])
261+
param = "x"
262+
for row in query
263+
@test row[1] == "Hello!"
264+
GC.gc() # this must NOT garbage collect the "Hello!" bound value
265+
end
266+
267+
#test for #180 (bind! and clear!)
268+
params = tuple("string", UInt8[1, 2, 3]) # parameter types that can be finalized
269+
wkdict = WeakKeyDict{Any, Any}(param => 1 for param in params)
270+
stmt = SQLite.Stmt(SQLite.DB(), "SELECT ?, ?")
271+
SQLite.bind!(stmt, params)
272+
params = "x"
273+
GC.gc() # this MUST NOT garbage collect any of the bound values
274+
@test length(wkdict) == 2
275+
SQLite.clear!(stmt)
276+
GC.gc() # this will garbage collect the no longer bound values
277+
@test isempty(wkdict)

0 commit comments

Comments
 (0)