Skip to content

Commit 4b5fc58

Browse files
authored
Ensure rows are only valid while currently being iterated (#262)
Fixes #251 amongst other issues. This PR makes `SQLite.Row` explicitly error when values are attempted to be accessed and it's not the currently iterated row. It borrows the idea from the MySQL.jl package, which as a similar "sync" of row numbers between the `Row` and `Query` objects.
1 parent 6beb466 commit 4b5fc58

File tree

2 files changed

+20
-6
lines changed

2 files changed

+20
-6
lines changed

src/tables.jl

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ struct Query
88
names::Vector{Symbol}
99
types::Vector{Type}
1010
lookup::Dict{Symbol, Int}
11+
current_rownumber::Base.RefValue{Int}
1112
end
1213

1314
# check if the query has no (more) rows
1415
Base.isempty(q::Query) = q.status[] == SQLITE_DONE
1516

1617
struct Row <: Tables.AbstractRow
1718
q::Query
19+
rownumber::Int
1820
end
1921

2022
getquery(r::Row) = getfield(r, :q)
@@ -56,7 +58,10 @@ function done(q::Query)
5658
return false
5759
end
5860

59-
function getvalue(q::Query, col::Int, ::Type{T}) where {T}
61+
@noinline wrongrow(i) = throw(ArgumentError("row $i is no longer valid; sqlite query results are forward-only iterators where each row is only valid when iterated; re-execute the query, convert rows to NamedTuples, or stream the results to a sink to save results"))
62+
63+
function getvalue(q::Query, col::Int, rownumber::Int, ::Type{T}) where {T}
64+
rownumber == q.current_rownumber[] || wrongrow(rownumber)
6065
handle = _stmt(q.stmt).handle
6166
t = sqlite3_column_type(handle, col)
6267
if t == SQLITE_NULL
@@ -67,21 +72,23 @@ function getvalue(q::Query, col::Int, ::Type{T}) where {T}
6772
end
6873
end
6974

70-
Tables.getcolumn(r::Row, ::Type{T}, i::Int, nm::Symbol) where {T} = getvalue(getquery(r), i, T)
75+
Tables.getcolumn(r::Row, ::Type{T}, i::Int, nm::Symbol) where {T} = getvalue(getquery(r), i, getfield(r, :rownumber), T)
7176

7277
Tables.getcolumn(r::Row, i::Int) = Tables.getcolumn(r, getquery(r).types[i], i, getquery(r).names[i])
7378
Tables.getcolumn(r::Row, nm::Symbol) = Tables.getcolumn(r, getquery(r).lookup[nm])
7479
Tables.columnnames(r::Row) = Tables.columnnames(getquery(r))
7580

7681
function Base.iterate(q::Query)
7782
done(q) && return nothing
78-
return Row(q), nothing
83+
q.current_rownumber[] = 1
84+
return Row(q, 1), 2
7985
end
8086

81-
function Base.iterate(q::Query, ::Nothing)
87+
function Base.iterate(q::Query, rownumber)
8288
q.status[] = sqlite3_step(_stmt(q.stmt).handle)
8389
done(q) && return nothing
84-
return Row(q), nothing
90+
q.current_rownumber[] = rownumber
91+
return Row(q, rownumber), rownumber + 1
8592
end
8693

8794
"Return the last row insert id from the executed statement"
@@ -129,7 +136,7 @@ function DBInterface.execute(stmt::Stmt, params::DBInterface.StatementParams; al
129136
header[i] = nm
130137
types[i] = Union{juliatype(_st.handle, i), Missing}
131138
end
132-
return Query(stmt, Ref(status), header, types, Dict(x=>i for (i, x) in enumerate(header)))
139+
return Query(stmt, Ref(status), header, types, Dict(x=>i for (i, x) in enumerate(header)), Ref(0))
133140
end
134141

135142
"""

test/runtests.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,4 +540,11 @@ tbl = DBInterface.execute(db, "select * from tbl") |> columntable
540540
c = [3, 3, 3]
541541
)
542542

543+
# https://github.com/JuliaDatabases/SQLite.jl/issues/251
544+
q = DBInterface.execute(db, "select * from tbl")
545+
row, st = iterate(q)
546+
@test row.a == 1 && row.b == 2 && row.c == 3
547+
row2, st = iterate(q, st)
548+
@test_throws ArgumentError row.a
549+
543550
end

0 commit comments

Comments
 (0)