Files
email-tracker/external/duckdb/tools/juliapkg/test/test_sqlite.jl
2025-10-24 19:21:19 -05:00

328 lines
12 KiB
Julia

# test_sqlite.jl
# tests adopted from SQLite.jl
using Tables
function setup_clean_test_db(f::Function, args...)
tables = [
"album",
"artist",
"customer",
"employee",
"genre",
"invoice",
"invoiceline",
"mediatype",
"playlist",
"playlisttrack",
"track"
]
con = DBInterface.connect(DuckDB.DB)
datadir = joinpath(@__DIR__, "../data")
for table in tables
DBInterface.execute(con, "CREATE TABLE $table AS SELECT * FROM '$datadir/$table.parquet'")
end
try
f(con)
finally
close(con)
end
end
@testset "DB Connection" begin
con = DBInterface.connect(DuckDB.DB)
@test con isa DuckDB.DB
DBInterface.close!(con)
end
@testset "Issue #207: 32 bit integers" begin
setup_clean_test_db() do db
ds = DBInterface.execute(db, "SELECT 42::INT64 a FROM Track LIMIT 1") |> columntable
@test ds.a[1] isa Int64
end
end
@testset "Regular DuckDB Tests" begin
setup_clean_test_db() do db
@test_throws DuckDB.QueryException DBInterface.execute(db, "just some syntax error")
# syntax correct, table missing
@test_throws DuckDB.QueryException DBInterface.execute(
db,
"SELECT name FROM sqlite_nomaster WHERE type='table';"
)
end
end
@testset "close!(query)" begin
setup_clean_test_db() do db
qry = DBInterface.execute(db, "SELECT name FROM sqlite_master WHERE type='table';")
DBInterface.close!(qry)
return DBInterface.close!(qry) # test it doesn't throw on double-close
end
end
@testset "Query tables" begin
setup_clean_test_db() do db
ds = DBInterface.execute(db, "SELECT name FROM sqlite_master WHERE type='table';") |> columntable
@test length(ds) == 1
@test keys(ds) == (:name,)
@test length(ds.name) == 11
end
end
@testset "DBInterface.execute([f])" begin
setup_clean_test_db() do db
# pipe approach
results = DBInterface.execute(db, "SELECT * FROM Employee;") |> columntable
@test length(results) == 15
@test length(results[1]) == 8
# callable approach
@test isequal(DBInterface.execute(columntable, db, "SELECT * FROM Employee"), results)
employees_stmt = DBInterface.prepare(db, "SELECT * FROM Employee")
@test isequal(columntable(DBInterface.execute(employees_stmt)), results)
@test isequal(DBInterface.execute(columntable, employees_stmt), results)
@testset "throwing from f()" begin
f(::DuckDB.QueryResult) = error("I'm throwing!")
@test_throws ErrorException DBInterface.execute(f, employees_stmt)
@test_throws ErrorException DBInterface.execute(f, db, "SELECT * FROM Employee")
end
return DBInterface.close!(employees_stmt)
end
end
@testset "isempty(::Query)" begin
setup_clean_test_db() do db
@test !DBInterface.execute(isempty, db, "SELECT * FROM Employee")
@test DBInterface.execute(isempty, db, "SELECT * FROM Employee WHERE FirstName='Joanne'")
end
end
@testset "empty query has correct schema and return type" begin
setup_clean_test_db() do db
empty_scheme = DBInterface.execute(Tables.schema, db, "SELECT * FROM Employee WHERE FirstName='Joanne'")
all_scheme = DBInterface.execute(Tables.schema, db, "SELECT * FROM Employee WHERE FirstName='Joanne'")
@test empty_scheme.names == all_scheme.names
@test all(ea -> ea[1] <: ea[2], zip(empty_scheme.types, all_scheme.types))
empty_tbl = DBInterface.execute(columntable, db, "SELECT * FROM Employee WHERE FirstName='Joanne'")
all_tbl = DBInterface.execute(columntable, db, "SELECT * FROM Employee")
@test propertynames(empty_tbl) == propertynames(all_tbl)
end
end
@testset "Create table, run commit/rollback tests" begin
setup_clean_test_db() do db
DBInterface.execute(db, "create table temp as select * from album")
DBInterface.execute(db, "alter table temp add column colyear int")
DBInterface.execute(db, "update temp set colyear = 2014")
r = DBInterface.execute(db, "select * from temp limit 10") |> columntable
@test length(r) == 4 && length(r[1]) == 10
@test all(==(2014), r[4])
@test_throws DuckDB.QueryException DuckDB.rollback(db)
@test_throws DuckDB.QueryException DuckDB.commit(db)
DuckDB.transaction(db)
DBInterface.execute(db, "update temp set colyear = 2015")
DuckDB.rollback(db)
r = DBInterface.execute(db, "select * from temp limit 10") |> columntable
@test all(==(2014), r[4])
DuckDB.transaction(db)
DBInterface.execute(db, "update temp set colyear = 2015")
DuckDB.commit(db)
r = DBInterface.execute(db, "select * from temp limit 10") |> columntable
@test all(==(2015), r[4])
end
end
@testset "Dates" begin
setup_clean_test_db() do db
DBInterface.execute(db, "create table temp as select * from album")
DBInterface.execute(db, "alter table temp add column dates date")
stmt = DBInterface.prepare(db, "update temp set dates = ?")
DBInterface.execute(stmt, (Date(2014, 1, 1),))
r = DBInterface.execute(db, "select * from temp limit 10") |> columntable
@test length(r) == 4 && length(r[1]) == 10
@test isa(r[4][1], Date)
@test all(Bool[x == Date(2014, 1, 1) for x in r[4]])
return DBInterface.execute(db, "drop table temp")
end
end
@testset "Prepared Statements" begin
setup_clean_test_db() do db
DBInterface.execute(db, "CREATE TABLE temp AS SELECT * FROM Album")
r = DBInterface.execute(db, "SELECT * FROM temp LIMIT ?", [3]) |> columntable
@test length(r) == 3 && length(r[1]) == 3
r = DBInterface.execute(db, "SELECT * FROM temp WHERE Title ILIKE ?", ["%time%"]) |> columntable
@test r[1] == [76, 111, 187]
DBInterface.execute(db, "INSERT INTO temp VALUES (?1, ?3, ?2)", [0, 0, "Test Album"])
r = DBInterface.execute(db, "SELECT * FROM temp WHERE AlbumId = 0") |> columntable
@test r[1][1] == 0
@test r[2][1] == "Test Album"
@test r[3][1] == 0
DuckDB.drop!(db, "temp")
DBInterface.execute(db, "CREATE TABLE temp AS SELECT * FROM Album")
# FIXME Does it make sense to use named parameters here?
r = DBInterface.execute(db, "SELECT * FROM temp LIMIT ?", (a = 3,)) |> columntable
@test length(r) == 3 && length(r[1]) == 3
r = DBInterface.execute(db, "SELECT * FROM temp LIMIT ?", a = 3) |> columntable
@test length(r) == 3 && length(r[1]) == 3
r = DBInterface.execute(db, "SELECT * FROM temp WHERE Title ILIKE ?", (word = "%time%",)) |> columntable
@test r[1] == [76, 111, 187]
# FIXME: these are supposed to be named parameter tests, but we don't support that yet
DBInterface.execute(db, "INSERT INTO temp VALUES (?, ?, ?)", (lid = 0, title = "Test Album", rid = 1))
DBInterface.execute(db, "INSERT INTO temp VALUES (?, ?, ?)", lid = 400, title = "Test2 Album", rid = 3)
r = DBInterface.execute(db, "SELECT * FROM temp WHERE AlbumId IN (0, 400)") |> columntable
@test r[1] == [0, 400]
@test r[2] == ["Test Album", "Test2 Album"]
@test r[3] == [1, 3]
return DuckDB.drop!(db, "temp")
end
end
@testset "DuckDB to Julia type conversion" begin
binddb = DBInterface.connect(DuckDB.DB)
DBInterface.execute(
binddb,
"CREATE TABLE temp (n INTEGER, i1 INT, i2 integer,
f1 REAL, f2 FLOAT, f3 DOUBLE,
s1 TEXT, s2 CHAR(10), s3 VARCHAR(15), s4 NVARCHAR(5),
d1 DATETIME, ts1 TIMESTAMP)"
)
DBInterface.execute(
binddb,
"INSERT INTO temp VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
missing,
Int64(6),
Int64(4),
6.4,
6.3,
Int64(7),
"some long text",
"short text",
"another text",
"short",
"2021-02-21",
"2021-02-12 12:01:32"
]
)
rr = DBInterface.execute(rowtable, binddb, "SELECT * FROM temp")
@test length(rr) == 1
r = first(rr)
@test typeof.(Tuple(r)) ==
(Missing, Int32, Int32, Float32, Float32, Float64, String, String, String, String, DateTime, DateTime)
# Issue #4809: Concrete `String` types.
# Want to test exactly the types `execute` returns, so check the schema directly and
# avoid calling `Tuple` or anything else that would narrow the types in the result.
schema = Tables.schema(rr)
@test nonmissingtype.(schema.types) ==
(Int32, Int32, Int32, Float32, Float32, Float64, String, String, String, String, DateTime, DateTime)
end
@testset "Issue #158: Missing DB File" begin
@test_throws DuckDB.ConnectionException DuckDB.DB("nonexistentdir/not_there.db")
end
@testset "Issue #180, Query" begin
param = "Hello!"
query = DBInterface.execute(DuckDB.DB(), "SELECT ?1 UNION ALL SELECT ?1", [param])
param = "x"
for row in query
@test row[1] == "Hello!"
GC.gc() # this must NOT garbage collect the "Hello!" bound value
end
db = DBInterface.connect(DuckDB.DB)
DBInterface.execute(db, "CREATE TABLE T (a TEXT, PRIMARY KEY (a))")
q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)")
DBInterface.execute(q, ["a"])
@test_throws DuckDB.QueryException DBInterface.execute(q, [1, "a"])
end
@testset "show(DB)" begin
io = IOBuffer()
db = DuckDB.DB()
show(io, db)
@test String(take!(io)) == "DuckDB.DB(\":memory:\")"
DBInterface.close!(db)
end
@testset "DuckDB.execute()" begin
db = DBInterface.connect(DuckDB.DB)
DBInterface.execute(db, "CREATE TABLE T (x INT UNIQUE)")
q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)")
DuckDB.execute(q, (1,))
r = DBInterface.execute(db, "SELECT * FROM T") |> columntable
@test r[1] == [1]
DuckDB.execute(q, [2])
r = DBInterface.execute(db, "SELECT * FROM T") |> columntable
@test r[1] == [1, 2]
q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)")
DuckDB.execute(q, [3])
r = DBInterface.execute(columntable, db, "SELECT * FROM T")
@test r[1] == [1, 2, 3]
DuckDB.execute(q, [4])
r = DBInterface.execute(columntable, db, "SELECT * FROM T")
@test r[1] == [1, 2, 3, 4]
DuckDB.execute(db, "INSERT INTO T VALUES(?)", [5])
r = DBInterface.execute(columntable, db, "SELECT * FROM T")
@test r[1] == [1, 2, 3, 4, 5]
r = DBInterface.execute(db, strip(" SELECT * FROM T ")) |> columntable
@test r[1] == [1, 2, 3, 4, 5]
r = DBInterface.execute(db, "SELECT * FROM T")
@test Tables.istable(r)
@test Tables.rowaccess(r)
@test Tables.rows(r) === r
@test Base.IteratorSize(typeof(r)) == Base.SizeUnknown()
row = first(r)
end
@testset "last_insert_rowid unsupported" begin
db = DBInterface.connect(DuckDB.DB)
@test_throws DuckDB.NotImplementedException DBInterface.lastrowid(db)
@test DuckDB.esc_id(["1", "2", "3"]) == "\"1\",\"2\",\"3\""
end
@testset "Escaping" begin
@test DuckDB.esc_id(["1", "2", "3"]) == "\"1\",\"2\",\"3\""
end
@testset "Issue #253: Ensure query column names are unique by default" begin
db = DuckDB.DB()
res = DBInterface.execute(db, "select 1 as x2, 2 as x2, 3 as x2, 4 as x2_2") |> columntable
@test res == (x2 = [1], x2_1 = [2], x2_2 = [3], x2_2_1 = [4])
end
@testset "drop!() table name escaping" begin
db = DuckDB.DB()
DBInterface.execute(db, "CREATE TABLE \"escape 10.0%\"(i INTEGER)")
# table exists
DBInterface.execute(db, "SELECT * FROM \"escape 10.0%\"")
# drop the table
DuckDB.drop!(db, "escape 10.0%")
# it should no longer exist
@test_throws DuckDB.QueryException DBInterface.execute(db, "SELECT * FROM \"escape 10.0%\"")
end