-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better tracking of prepared statements & other enhancements #223
Conversation
Codecov Report
@@ Coverage Diff @@
## master #223 +/- ##
==========================================
+ Coverage 79.45% 82.52% +3.06%
==========================================
Files 5 5
Lines 555 578 +23
==========================================
+ Hits 441 477 +36
+ Misses 114 101 -13
Continue to review full report at Codecov.
|
rather than error(). Also show the name of the duplicate column.
f70ed20
to
8e36dd6
Compare
@quinnj Could you please have a look? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few questions; overall looks good! Thanks!
sqlitetype(::Type{T}) where {T<:AbstractString} = "TEXT NOT NULL" | ||
sqlitetype(::Type{T}) where {T<:Union{Missing, AbstractString}} = "TEXT" | ||
# conversion from Julia to SQLite3 types | ||
sqlitetype_(::Type{<:Integer}) = "INT" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why change the name here to sqlitetype_
? It doesnt' seem like it should conflict by leaving it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would very much prefer to avoid introducing sqlitetype_()
method too. The problem is, sqlitetype()
has to return "T NOT NULL" for non-missing types and "T" for missing ones. Which means the pattern sqlitetype(::Type{Union{T, Missing}) = f(sqlitetype(T))
will not work here. Of all solutions to this problem the one I implemented looked like the most straightforward one. But I can change it if you have better ideas.
src/SQLite.jl
Outdated
execute(stmt::Stmt, params::Union{NamedTuple, AbstractDict, AbstractVector, Tuple}) = | ||
execute(stmt.db, _stmt(stmt), params) | ||
|
||
execute(stmt::Union{Stmt, _Stmt}; kwargs...) = execute(stmt, kwargs.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to document this new form of execute
+ tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some very basic tests are already there (just mimicking what is already there for named tuple version). I will add docs for SQLite.execute()
.
The problem is, as I state in PR description, the actual API to use for row-returning statements should be DBInterface.execute()
, which have to be changed in DBInterface.jl
. Should I add keyarg version of execute
there as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the updated PR I've updated the docs for SQLite.execute
clarifying that the params could also be pass as keyargs, and I've added the corresponding tests.
src/tables.jl
Outdated
end | ||
return Query(stmt, Ref(status), header, types, Dict(x=>i for (i, x) in enumerate(header))) | ||
end | ||
|
||
DBInterface.execute(stmt::Stmt; kwargs...) = DBInterface.execute(stmt, kwargs.data) | ||
DBInterface.execute(db::DB, sql::AbstractString; kwargs...) = DBInterface.execute(DBInterface.prepare(db, sql), kwargs.data) # FIXME should be in DBInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let's not pirate here; we can do a PR to DBInterface.jl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Now the package requires DBInterface.jl
v2.3.0.
src/tables.jl
Outdated
end | ||
# table info for load!(): | ||
# returns NamedTuple with columns information, | ||
# or nothing if table does not exist | ||
function tableinfo(db::DB, name::AbstractString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the changes here? Seems breaking for not much benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though tableinfo()
is rather recent change, and TableInfo
structure was not exported, so I changed it into something that looked more generally useful to me.
Before it returned TableInfo
struct which actually contained just a vector of table column names. Now it returns the named tuple with all the information that SQLite provides, including column types etc. So the updated tableinfo()
could be used to actually get the table information.
Also, the old version was not working, because it was checking whether the result of columntable(...)
is a NamedTuple
as an indication that table exists. However, columntable()
always returns NamedTuple
-- just in case of a missing table it has only .q
element.
end | ||
|
||
checkdupnames(names) = length(unique(map(x->lowercase(String(x)), names))) == length(names) || error("duplicate case-insensitive column names detected; sqlite doesn't allow duplicate column names and treats them case insensitive") | ||
# case-insensitive check for duplicate column names | ||
function checkdupnames(names::Union{AbstractVector, Tuple}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a functional change here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still throws in the same situations. However, now it throws SQLiteException
instead of generic ErrorException
.
Plus the message indicates the duplicated column, which is quite useful :)
I've moved DBIntreface.jl changes to JuliaDatabases/DBInterface.jl#30. |
using types defined in DBInterface in 2.3.0, so the required version is updated
assert should be used only as internal logic checks
We don't need the special TableInfo structure, in particular if the table does not exist. The current version uses execute(f, db, sql) call to avoid leaving behind prepared statements. Fixes the table existence check (Query always returns NamesTuple).
this is confusing and error-prone
so that the statement is closed immediately upon completion
update the docstring and add (very) basic tests
I've updated the PR against DBInterface.jl 2.3.0. With the tighter control of prepared statements lifetime (esp. with |
src/SQLite.jl
Outdated
refcount::Int # by how many Stmt objects referenced | ||
|
||
function _Stmt(handle::StmtHandle) | ||
stmt = new(handle, Dict{Int, Any}(), 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why doesn't the refcount start at 1 here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think I see how it's used later on; the refcount is how many Stmt
s reference a _Stmt
.
Hmmm, having reviewed the whole PR, the refcount stuff feels a little fragile/fiddly to me; can you restate the benefits of going with this approach of tracking _Stmt
s apart from Stmt
and the refcounting? I just want to make sure I understand what we gain/benefit for all this work, since it just seems like a lot of effort to just ensure we close things?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you restate the benefits of going with this approach of tracking _Stmts apart from Stmt and the refcounting?
The idea is to confine the prepared statement handles within DB
object and make 1-to-1 correspondence between internal _Stmt
object and the prepared statement handle. E.g. even if the user will manage to create two Stmt
objects that reference the same statement (e.g. by copying Stmt
), there will be no double-close situation, because the handle will be closed only once (from _Stmt
object). Also, when DB
is closed, Stmt
will be "aware" that the handle is no longer valid. I'm not an expert in SQLite3, so I don't know how bad it is to call its API methods with the bogus handle, but I assume it's better if we can intercept an error before the call.
And yes, it was also handy to have all prepared statement automatically tracked at one place, so that they could all be closed at once.
One can implement that on the user side, but it's much more efficient/less error-prone to do it in the package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for refcounting itself, I'm not 100% sure about it either.
It's part of the solution to keep track of the Stmt
scope when copying is possible.
If copying is forbidden (at least copy
throws), it is not needed.
In that case double-close also should not happen, but there still needs
to be a mechanism of correctly closing statements upon DB close.
@quinnj could it be that CI actions are disabled due to 60 days of inactivity? |
Yeah, this looks pretty good now; would you mind copy/pasting this; there's one section like: - run: |
julia --project=docs -e '
using Documenter: doctest
using JSON3
doctest(JSON3)' that we can just remove. That should setup github actions to run on this PR. |
- fixes JuliaDatabases#211 - closes all prepared statements upon close!(db) - adds SQLite3.finalize_statements!(db) call - execute(db, sql): close the internal prepared statement immediately, don't wait for GC
so Union{T, Missing} is handled automatically. This also fixes "BLOB NOT NULL" case.
we don't need to create DB just to raise an exception
ci.yml taken from JSON3.jl
The bigger part of the PR is fixing problems related to DB lock is not released #211.
The PR addresses it by introducing internal prepared statement object
_Stmt
that is managed byDB
, whereasStmt
is just a reference to_Stmt
(_Stmt
keeps the count of that references).The public API is not affected by this change. As before, prepared statements should be finalized when
Stmt
object goes out of scope.But with this PR closing the DB connection automatically closes all its prepared statements.
Also there's (non-exported) new method
SQLite.finalize_statements!(db)
that explicitly closes all prepared statements without disconnecting from the DB.That is handy when DB schema needs to be updated (e.g. dropping the table), and some statement(s) hold the schema lock. An attempt to use
Stmt
objectafter its prepared statement was finalized will throw
SQLiteException
.The "non-prepared" statements (
execute(db, sql)
) now close their internally created prepared statements immediately upon finishing without waiting for GC to close the staleprepared statement.
The PR also adds support for
Bool
Julia type: before it was imported into SQLite3 as blob, now it is properly imported asINT
.Still need to update the tests, though.It also adds support for passing the statement parameters to
bind!()/execute()
as keyword arguments. Note: it also adds support for keyword args toDBInterface.execute
as well, but the proper place for this would be in DBInterface.jl package itself. I just don't have a precise idea whether such change in DBInterface would require updating other DB interface packages as well.Cleanups in
load!()
:nm
argument of some internal overloads: it is just confusing and error prone to have both escaped and non-escaped table names as argumentstableinfo()
fix/cleanup: now it returnsnothing
if the table doesn't exist, and the output ofcolumntable()
if it does, no need for special struct with a bit cryptic fieldsExporting SQLiteException and throwing it whenever makes sense: instead of
@assert
anderror()
.The PR should be ready for the review.
I still need to add a few tests for the new features (closing statements upon DB disconnect, Bool columns etc), butI would be waiting for the feedback on how to proceed further. Clearly this PR addresses several different things. I tried to make the commits atomic, so I can split the PR, if necessary -- but maintaining several independent PRs adds a bit of overhead.