Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion src/env.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ handle: it will be closed when the wrapper is garbage-collected, unless
mutable struct Environment
handle::Ptr{MDB_env}
path::String
# Live transactions, tracked weakly so they remain GC-able. `close`
# walks this list to abort any still-open txn before calling
# `mdb_env_close`; otherwise LMDB corrupts state shared across envs.
txns::Vector{WeakRef}
function Environment(h::Ptr{MDB_env} = C_NULL)
e = new(h, "")
e = new(h, "", WeakRef[])
finalizer(close, e)
return e
end
Expand Down Expand Up @@ -105,6 +109,14 @@ finalizers safe to run after an explicit close.
"""
function close(env::Environment)
env.handle == C_NULL && return zero(Cint)
# LMDB requires all transactions to be closed before `mdb_env_close`;
# otherwise it leaves shared lockfile/heap state corrupted and the
# next env-open in the process can crash inside `mdb_txn_renew0`.
for wr in env.txns
t = wr.value
t isa Transaction && t.handle != C_NULL && abort(t)
end
empty!(env.txns)
mdb_env_close(env)
env.handle = C_NULL
env.path = ""
Expand Down
5 changes: 5 additions & 0 deletions src/txn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mutable struct Transaction
env::Environment
function Transaction(env::Environment, h::Ptr{MDB_txn})
t = new(h, env)
# Track on the env so `close(env)` can abort us if the caller forgot.
push!(env.txns, WeakRef(t))
finalizer(_finalize_txn, t)
return t
end
Expand Down Expand Up @@ -56,6 +58,9 @@ Idempotent: safe to call after a previous `commit`/`abort` or on a never-opened
"""
function abort(txn::Transaction)
txn.handle == C_NULL && return
# If env was closed first, `mdb_env_close` already freed the txn memory,
# so the handle is dangling. Same defensive check as `close(::Cursor)`.
isopen(txn.env) || (txn.handle = C_NULL; return)
mdb_txn_abort(txn)
txn.handle = C_NULL
return
Expand Down
14 changes: 9 additions & 5 deletions test/common.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using LMDB
using Test

@test LMDB.version()[1] >= v"0.9.15"
@testset "common" begin

# LMDBError
ex = LMDBError(Cint(0))
@test_throws LMDBError throw(ex)
@test ex.code == 0
@test LMDB.version()[1] >= v"0.9.15"

# LMDBError
ex = LMDBError(Cint(0))
@test_throws LMDBError throw(ex)
@test ex.code == 0

end # @testset "common"
218 changes: 108 additions & 110 deletions test/cur.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
module LMDB_CUR
using LMDB
using Test
using LMDB
using Test

const dbname = "testdb"
key = 10
val = "key value is "
@testset "Cursor" begin

# Create dir
mkdir(dbname)
key = 10
val = "key value is "

# Procedural style
# Procedural style + block style smoke test, exercising cursor put!/walk
# round-trip and the parent accessors.
mktempdir() do dbname
env = create()
try
open(env, dbname)
Expand Down Expand Up @@ -52,122 +51,121 @@ module LMDB_CUR
end
end
end
end

# Cursor positioning + walk primitives.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.put!(txn, dbi, "a", "1")
LMDB.put!(txn, dbi, "b", "2")
LMDB.put!(txn, dbi, "c", "3")

LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, String) == "a"
@test LMDB.value(cur, String) == "1"
@test LMDB.key(cur, String) == "a"
@test LMDB.item(cur, String, String) == ("a" => "1")

@test LMDB.next!(cur, String) == "b"
@test LMDB.value(cur, String) == "2"

@test LMDB.seek_last!(cur, String) == "c"
@test LMDB.prev!(cur, String) == "b"

@test LMDB.seek!(cur, "a", String) == "a"
@test LMDB.seek!(cur, "missing", String) === nothing

@test LMDB.seek_range!(cur, "ab", String) == "b"
@test LMDB.seek_range!(cur, "z", String) === nothing

# walk over everything
ks = String[]
LMDB.walk(cur) do k_ref, _
push!(ks, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test ks == ["a", "b", "c"]

# walk from a starting key
ks2 = String[]
LMDB.walk(cur; from="b") do k_ref, _
push!(ks2, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test ks2 == ["b", "c"]

# Remove db dir
rm(dbname, recursive=true)

# Cursor positioning + walk primitives.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.put!(txn, dbi, "a", "1")
LMDB.put!(txn, dbi, "b", "2")
LMDB.put!(txn, dbi, "c", "3")

LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, String) == "a"
@test LMDB.value(cur, String) == "1"
@test LMDB.key(cur, String) == "a"
@test LMDB.item(cur, String, String) == ("a" => "1")

@test LMDB.next!(cur, String) == "b"
@test LMDB.value(cur, String) == "2"

@test LMDB.seek_last!(cur, String) == "c"
@test LMDB.prev!(cur, String) == "b"

@test LMDB.seek!(cur, "a", String) == "a"
@test LMDB.seek!(cur, "missing", String) === nothing

@test LMDB.seek_range!(cur, "ab", String) == "b"
@test LMDB.seek_range!(cur, "z", String) === nothing

# walk over everything
ks = String[]
LMDB.walk(cur) do k_ref, _
push!(ks, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test ks == ["a", "b", "c"]

# walk from a starting key
ks2 = String[]
LMDB.walk(cur; from="b") do k_ref, _
push!(ks2, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test ks2 == ["b", "c"]

# walk from a key past the last entry — no callbacks.
ks3 = String[]
LMDB.walk(cur; from="z") do k_ref, _
push!(ks3, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test isempty(ks3)

# typed walk: each ref decoded via read(MDBValueIO, K)
# / read(MDBValueIO, V).
kv = Pair{String, String}[]
LMDB.walk(cur, String, String) do k, v
push!(kv, k => v)
end
@test kv == ["a" => "1", "b" => "2", "c" => "3"]

# typed walk respects the false-stops contract.
seen = Pair{String, String}[]
LMDB.walk(cur, String, String) do k, v
push!(seen, k => v)
k == "b" ? false : nothing
end
@test seen == ["a" => "1", "b" => "2"]
# walk from a key past the last entry — no callbacks.
ks3 = String[]
LMDB.walk(cur; from="z") do k_ref, _
push!(ks3, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test isempty(ks3)

# typed walk: each ref decoded via read(MDBValueIO, K)
# / read(MDBValueIO, V).
kv = Pair{String, String}[]
LMDB.walk(cur, String, String) do k, v
push!(kv, k => v)
end
@test kv == ["a" => "1", "b" => "2", "c" => "3"]

# typed walk respects the false-stops contract.
seen = Pair{String, String}[]
LMDB.walk(cur, String, String) do k, v
push!(seen, k => v)
k == "b" ? false : nothing
end
@test seen == ["a" => "1", "b" => "2"]
end
end
end
end
end

# seek!/next! on an empty database returns nothing.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, String) === nothing
@test LMDB.seek_last!(cur, String) === nothing
@test LMDB.seek!(cur, "x", String) === nothing
@test LMDB.seek_range!(cur, "x", String) === nothing

ks = String[]
LMDB.walk(cur) do k_ref, _
push!(ks, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test isempty(ks)
# seek!/next! on an empty database returns nothing.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, String) === nothing
@test LMDB.seek_last!(cur, String) === nothing
@test LMDB.seek!(cur, "x", String) === nothing
@test LMDB.seek_range!(cur, "x", String) === nothing

ks = String[]
LMDB.walk(cur) do k_ref, _
push!(ks, read(LMDB.MDBValueIO(k_ref[]), String))
end
@test isempty(ks)
end
end
end
end
end

# Cursor.delete!: removes the entry the cursor is on; LMDB advances to
# the next entry. Throws on an unpositioned cursor (EINVAL), unlike the
# txn-based `delete!(txn, dbi, key)` which is Bool-returning on
# MDB_NOTFOUND.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.put!(txn, dbi, "a", "1")
LMDB.put!(txn, dbi, "b", "2")

LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, "a", String) == "a"
LMDB.delete!(cur) # removes "a", cursor now on "b"
LMDB.delete!(cur) # removes "b"
@test_throws LMDBError LMDB.delete!(cur) # no live entry
end
@test LMDB.tryget(txn, dbi, "a", String) === nothing
@test LMDB.tryget(txn, dbi, "b", String) === nothing
# Cursor.delete!: removes the entry the cursor is on; LMDB advances to
# the next entry. Throws on an unpositioned cursor (EINVAL), unlike the
# txn-based `delete!(txn, dbi, key)` which is Bool-returning on
# MDB_NOTFOUND.
mktempdir() do dir
environment(dir) do env
start(env) do txn
open(txn) do dbi
LMDB.put!(txn, dbi, "a", "1")
LMDB.put!(txn, dbi, "b", "2")

LMDB.open(txn, dbi) do cur
@test LMDB.seek!(cur, "a", String) == "a"
LMDB.delete!(cur) # removes "a", cursor now on "b"
LMDB.delete!(cur) # removes "b"
@test_throws LMDBError LMDB.delete!(cur) # no live entry
end
@test LMDB.tryget(txn, dbi, "a", String) === nothing
@test LMDB.tryget(txn, dbi, "b", String) === nothing
end
end
end
end

end # @testset "Cursor"
Loading
Loading