From 7755862414eb5c2aeff1e8ed88c6c51319fea853 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Wed, 30 Sep 2026 09:49:19 -0600 Subject: [PATCH] Reject ignored statements and run all of them in executemultiple prepare/execute silently ran only the first statement of SQL like "CREATE TABLE t(a); INSERT INTO t VALUES (1)", and executemultiple returned only the first result. Now preparing SQL with more than one statement throws, and DBInterface.executemultiple runs each statement in order, preparing each after the previous one runs. Closes #303. Co-Authored-By: Claude Opus 5.5 --- docs/src/index.md | 1 + src/SQLite.jl | 63 ++++++++++++++++++++++++++++++++++++++--------- test/runtests.jl | 24 ++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 0b94bbb..6f05343 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,6 +6,7 @@ ## High-level interface ```@docs DBInterface.execute(::SQLite.Stmt, ::DBInterface.StatementParams) +DBInterface.executemultiple(::SQLite.DB, ::AbstractString, ::DBInterface.StatementParams) SQLite.load! ``` diff --git a/src/SQLite.jl b/src/SQLite.jl index d5f5c46..f27a9da 100644 --- a/src/SQLite.jl +++ b/src/SQLite.jl @@ -195,16 +195,35 @@ function Base.show(io::IO, db::DB) end # prepare given sql statement +# Prepare the first statement in `sql` at or after byte `offset`. Returns its handle, +# or C_NULL if only whitespace and comments remain, and the offset just past it. +function prepare_next(db::DB, sql::String, offset::Int) + handle = Ref{StmtHandle}(C_NULL) + tail = Ref{Ptr{Cchar}}(C_NULL) + GC.@preserve sql begin + start = pointer(sql) + offset + @CHECK db C.sqlite3_prepare_v2(db.handle, start, sizeof(sql) - offset, handle, tail) + return handle[], Int(tail[] - pointer(sql)) + end +end + function prepare_stmt_wrapper(db::DB, sql::AbstractString) - handle_ptr = Ref{StmtHandle}() - @CHECK db C.sqlite3_prepare_v2( - db.handle, - sql, - sizeof(sql), - handle_ptr, - C_NULL, - ) - return handle_ptr + sql = String(sql) + handle, offset = prepare_next(db, sql, 0) + if offset < sizeof(sql) + # Anything but whitespace and comments after the first statement would be ignored. + next = try + prepare_next(db, sql, offset)[1] + catch + nothing # a statement that can't be prepared yet, e.g. using a table the first creates + end + if next != C_NULL + next === nothing || C.sqlite3_finalize(next) + C.sqlite3_finalize(handle) + throw(ArgumentError("SQL contains multiple statements; use `DBInterface.executemultiple` to run them all")) + end + end + return StmtWrapper(handle) end """ @@ -238,8 +257,7 @@ mutable struct Stmt <: DBInterface.Statement # used for holding references to bound statement values via bind! params::Dict{Int,Any} - function Stmt(db::DB, sql::AbstractString; register::Bool = true) - stmt_wrapper = prepare_stmt_wrapper(db, sql) + function Stmt(db::DB, stmt_wrapper::StmtWrapper; register::Bool = true) if register Base.@lock db.lock db.stmt_wrappers[stmt_wrapper] = nothing end @@ -249,6 +267,9 @@ mutable struct Stmt <: DBInterface.Statement end end +Stmt(db::DB, sql::AbstractString; register::Bool = true) = + Stmt(db, prepare_stmt_wrapper(db, sql); register) + _get_stmt_handle(stmt::Stmt)::StmtHandle = stmt.stmt_wrapper[] function _set_stmt_handle(stmt::Stmt, handle) stmt.stmt_wrapper[] = handle @@ -292,6 +313,26 @@ See `DBInterface.execute`(@ref) for information on executing a prepared statemen A `SQLite.Stmt` object can be closed (resources freed) using `DBInterface.close!`(@ref). """ DBInterface.prepare(db::DB, sql::AbstractString) = Stmt(db, sql) + +""" + DBInterface.executemultiple(db::SQLite.DB, sql::AbstractString, [params]) -> Vector{SQLite.Query} + +Run each statement in `sql` in order and return their result cursors. Each statement is +prepared after the previous one runs, so it can use tables created earlier in `sql`. +`params` are bound to every statement; use named parameters when statements take different ones. +Statements that ran before an error are not rolled back unless they're in a transaction. +""" +function DBInterface.executemultiple(db::DB, sql::AbstractString, params::DBInterface.StatementParams = ()) + sql = String(sql) + results = Query[] + offset = 0 + while offset < sizeof(sql) + handle, offset = prepare_next(db, sql, offset) + handle == C_NULL && break + push!(results, DBInterface.execute(Stmt(db, StmtWrapper(handle)), params)) + end + return results +end DBInterface.getconnection(stmt::Stmt) = stmt.db DBInterface.close!(stmt::Stmt) = _close_stmt!(stmt) diff --git a/test/runtests.jl b/test/runtests.jl index ea3a568..87a2b57 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1465,3 +1465,27 @@ end rm(dbfile) end end # @testset "SQLite" + +# https://github.com/JuliaDatabases/SQLite.jl/issues/303 +@testset "multiple statements" begin + db = SQLite.DB() + @test_throws ArgumentError DBInterface.execute(db, "CREATE TABLE m(a); INSERT INTO m VALUES (1)") + @test_throws ArgumentError DBInterface.execute(db, "SELECT 1; SELECT 2") + @test_throws ArgumentError DBInterface.prepare(db, "SELECT 1; SELECT 2") + @test_throws ArgumentError SQLite.execute(db, "SELECT 1; SELECT 2") + @test isempty(SQLite.tables(db)) # nothing ran + # trailing whitespace, semicolons, and comments are fine + for sql in ("SELECT 1 AS a;", "SELECT 1 AS a; \n", "SELECT 1 AS a; -- note", "SELECT 1 AS a /* x */;;") + @test Tables.columntable(DBInterface.execute(db, sql)).a == [1] + end + rs = DBInterface.executemultiple(db, "CREATE TABLE m(a); INSERT INTO m VALUES (1), (2);\n-- c\nSELECT sum(a) AS s FROM m; SELECT 'x' AS y") + @test length(rs) == 4 + @test Tables.columntable(rs[3]).s == [3] + @test Tables.columntable(rs[4]).y == ["x"] + @test length(DBInterface.executemultiple(db, "SELECT 1")) == 1 + @test isempty(DBInterface.executemultiple(db, " -- nothing\n")) + rs = DBInterface.executemultiple(db, "INSERT INTO m VALUES (:v); SELECT count(*) AS n FROM m WHERE a = :v"; v = 7) + @test Tables.columntable(rs[2]).n == [1] + @test_throws SQLiteException DBInterface.executemultiple(db, "INSERT INTO m VALUES (9); SELEC oops") + @test Tables.columntable(DBInterface.execute(db, "SELECT count(*) AS n FROM m WHERE a = 9")).n == [1] # first ran +end