diff --git a/test/runtests.jl b/test/runtests.jl index 912d4d4..bfde305 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -58,800 +58,1225 @@ function setup_clean_test_db(f::Function) end end -@testset "basics" begin - @testset "Julia to SQLite3 type conversion" begin - @test SQLite.sqlitetype(Int) == "INT NOT NULL" - @test SQLite.sqlitetype(Union{Float64,Missing}) == "REAL" - @test SQLite.sqlitetype(String) == "TEXT NOT NULL" - @test SQLite.sqlitetype(Symbol) == "BLOB NOT NULL" - @test SQLite.sqlitetype(Missing) == "NULL" - @test SQLite.sqlitetype(Nothing) == "NULL" - @test SQLite.sqlitetype(Any) == "BLOB" - end - - @testset "DB Connection" begin - dbfile = - joinpath(dirname(pathof(SQLite)), "../test/Chinook_Sqlite.sqlite") - @test SQLite.DB(dbfile) isa SQLite.DB - con = DBInterface.connect(SQLite.DB, dbfile) - @test con isa SQLite.DB - DBInterface.close!(con) - end +# UnknownSchemaTable is a minimal Tables.jl source used by the `load!` +# schema-handling tests; defined at top level so its method extensions are +# available to every testset below. +struct UnknownSchemaTable end - @testset "SQLite.tables(db)" begin - setup_clean_test_db() do db - results1 = SQLite.tables(db) +Tables.isrowtable(::Type{UnknownSchemaTable}) = true +Tables.rows(x::UnknownSchemaTable) = x +Base.length(x::UnknownSchemaTable) = 3 +function Base.iterate(::UnknownSchemaTable, st = 1) + st == 4 ? nothing : ((a = 1, b = 2 + st, c = 3 + st), st + 1) +end - @test isa(results1, SQLite.DBTables) - @test length(results1) == 11 - @test isa(results1[1], SQLite.DBTable) +@testset "SQLite" begin + @testset "type conversion" begin + @testset "Julia to SQLite3 type conversion" begin + @test SQLite.sqlitetype(Int) == "INT NOT NULL" + @test SQLite.sqlitetype(Union{Float64,Missing}) == "REAL" + @test SQLite.sqlitetype(String) == "TEXT NOT NULL" + @test SQLite.sqlitetype(Symbol) == "BLOB NOT NULL" + @test SQLite.sqlitetype(Missing) == "NULL" + @test SQLite.sqlitetype(Nothing) == "NULL" + @test SQLite.sqlitetype(Any) == "BLOB" + end - @test Tables.istable(results1) - @test Tables.rowaccess(results1) - @test Tables.rows(results1) == results1 + @testset "SQLite to Julia type conversion" begin + binddb = SQLite.DB() + DBInterface.execute( + binddb, + "CREATE TABLE temp (n NULL, i1 INT, i2 integer, + f1 REAL, f2 FLOAT, f3 NUMERIC, + s1 TEXT, s2 CHAR(10), s3 VARCHAR(15), s4 NVARCHAR(5), + d1 DATETIME, ts1 TIMESTAMP, + b BLOB, + x1 UNKNOWN1, x2 UNKNOWN2)", + ) + 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::1532", + b"bytearray", + "actually known", + 435, + ], + ) + rr = DBInterface.execute(rowtable, binddb, "SELECT * FROM temp") + @test length(rr) == 1 + r = first(rr) + @test typeof.(Tuple(r)) == ( + Missing, + Int64, + Int64, + Float64, + Float64, + Int64, + String, + String, + String, + String, + String, + String, + Base.CodeUnits{UInt8,String}, + String, + Int64, + ) + end + end - @test results1[1].name == "Album" - @test results1[1].schema == Tables.schema( - DBInterface.execute(db, "SELECT * FROM Album LIMIT 0"), + @testset "DB open/close" begin + @testset "DB Connection" begin + dbfile = joinpath( + dirname(pathof(SQLite)), + "../test/Chinook_Sqlite.sqlite", ) + @test SQLite.DB(dbfile) isa SQLite.DB + con = DBInterface.connect(SQLite.DB, dbfile) + @test con isa SQLite.DB + DBInterface.close!(con) + end - @test SQLite.DBTable("Album") == SQLite.DBTable("Album", nothing) - - @test [t.name for t in results1] == [ - "Album", - "Artist", - "Customer", - "Employee", - "Genre", - "Invoice", - "InvoiceLine", - "MediaType", - "Playlist", - "PlaylistTrack", - "Track", - ] + @testset "Issue #158: Missing DB File" begin + @test_throws SQLiteException SQLite.DB( + "nonexistentdir/not_there.db", + ) end - @testset "#209: SQLite.tables response when no tables in DB" begin + @testset "show(DB)" begin + io = IOBuffer() db = SQLite.DB() - tables_v = SQLite.tables(db) - @test String[] == [t.name for t in tables_v] + + show(io, db) + @test String(take!(io)) == "SQLite.DB(\":memory:\")" + DBInterface.close!(db) end - @testset "#322: SQLite.tables should escape table name" begin + @testset "Enable extension" begin db = SQLite.DB() - DBInterface.execute( - db, - "CREATE TABLE 'I.Js' (i INTEGER, j INTEGER)", - ) - tables_v = SQLite.tables(db) - @test ["I.Js"] == [t.name for t in tables_v] - DBInterface.close!(db) + @test SQLite.@OK SQLite.enable_load_extension(db) end - end - @testset "Issue #207: 32 bit integers" begin - setup_clean_test_db() do db - ds = - DBInterface.execute( - db, - "SELECT RANDOM() as a FROM Track LIMIT 1", - ) |> columntable - @test ds.a[1] isa Int64 + @testset "Test busy_timeout" begin + db = SQLite.DB() + @test SQLite.busy_timeout(db, 300) == 0 end - end - @testset "Regular SQLite Tests" begin - setup_clean_test_db() do db - @test_throws SQLiteException DBInterface.execute( - db, - "just some syntax error", - ) - # syntax correct, table missing - @test_throws SQLiteException DBInterface.execute( - db, - "SELECT name FROM sqlite_nomaster WHERE type='table';", + @testset "SQLite Open Flags" begin + rm("test.db"; force = true) + @test_throws SQLiteException("unable to open database file") SQLite.DB( + "file:test.db?mode=ro", ) + + db = SQLite.DB("file:test.db?mode=rwc") + @test db isa SQLite.DB + close(db) + rm("test.db"; force = true) end - end - @testset "close!(query)" begin - setup_clean_test_db() do db - qry = DBInterface.execute( - db, - "SELECT name FROM sqlite_master WHERE type='table';", + @testset "closed database errors" begin + db_a = SQLite.DB(":memory:") + db_b = SQLite.DB(":memory:") + SQLite.register(db_a, x -> 2x; name = "myfunc") + SQLite.register(db_b, x -> 10x; name = "myfunc") + # https://github.com/JuliaDatabases/SQLite.jl/issues/331 + tbl_a = + DBInterface.execute(db_a, "select myfunc(1) as x") |> + columntable + tbl_b = + DBInterface.execute(db_b, "select myfunc(1) as x") |> + columntable + @test tbl_a.x == [2] + @test tbl_b.x == [10] + + # Throw an error when interfacing with a closed database + close(db_b) + @test_throws SQLiteException("DB is closed") DBInterface.execute( + db_b, + "select myfunc(1) as x", ) - DBInterface.close!(qry) - 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 + @testset "tables interface" begin + @testset "SQLite.tables(db)" begin + setup_clean_test_db() do db + results1 = SQLite.tables(db) - results1 = SQLite.tables(db) - @test isequal(ds.name, [t.name for t in results1]) - end - end + @test isa(results1, SQLite.DBTables) + @test length(results1) == 11 + @test isa(results1[1], SQLite.DBTable) - @testset "DBInterface.execute([f])" begin - setup_clean_test_db() do db + @test Tables.istable(results1) + @test Tables.rowaccess(results1) + @test Tables.rows(results1) == results1 - # 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(::SQLite.Query) = error("I'm throwing!") - @test_throws ErrorException DBInterface.execute( - f, - employees_stmt, + @test results1[1].name == "Album" + @test results1[1].schema == Tables.schema( + DBInterface.execute(db, "SELECT * FROM Album LIMIT 0"), ) - @test_throws ErrorException DBInterface.execute( - f, + + @test SQLite.DBTable("Album") == + SQLite.DBTable("Album", nothing) + + @test [t.name for t in results1] == [ + "Album", + "Artist", + "Customer", + "Employee", + "Genre", + "Invoice", + "InvoiceLine", + "MediaType", + "Playlist", + "PlaylistTrack", + "Track", + ] + end + + @testset "#209: SQLite.tables response when no tables in DB" begin + db = SQLite.DB() + tables_v = SQLite.tables(db) + @test String[] == [t.name for t in tables_v] + DBInterface.close!(db) + end + + @testset "#322: SQLite.tables should escape table name" begin + db = SQLite.DB() + DBInterface.execute( db, - "SELECT * FROM Employee", + "CREATE TABLE 'I.Js' (i INTEGER, j INTEGER)", ) + tables_v = SQLite.tables(db) + @test ["I.Js"] == [t.name for t in tables_v] + DBInterface.close!(db) end - 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'", - ) + @testset "Escaping" begin + @test SQLite.esc_id(["1", "2", "3"]) == "\"1\",\"2\",\"3\"" 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) - @test all( - col -> - eltype(empty_tbl[col]) == Missing || - eltype(empty_tbl[col]) >: eltype(all_tbl[col]), - propertynames(all_tbl), - ) + @testset "execute / query" begin + @testset "Issue #207: 32 bit integers" begin + setup_clean_test_db() do db + ds = + DBInterface.execute( + db, + "SELECT RANDOM() as a FROM Track LIMIT 1", + ) |> columntable + @test ds.a[1] isa Int64 + end 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 SQLiteException SQLite.rollback(db) - @test_throws SQLiteException SQLite.commit(db) - SQLite.transaction(db) - DBInterface.execute(db, "update temp set colyear = 2015") - SQLite.rollback(db) - r = - DBInterface.execute(db, "select * from temp limit 10") |> - columntable - @test all(==(2014), r[4]) + @testset "Regular SQLite Tests" begin + setup_clean_test_db() do db + @test_throws SQLiteException DBInterface.execute( + db, + "just some syntax error", + ) + # syntax correct, table missing + @test_throws SQLiteException DBInterface.execute( + db, + "SELECT name FROM sqlite_nomaster WHERE type='table';", + ) + end + end - SQLite.transaction(db) - DBInterface.execute(db, "update temp set colyear = 2015") - SQLite.commit(db) - r = - DBInterface.execute(db, "select * from temp limit 10") |> - columntable - @test all(==(2015), r[4]) + @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) + DBInterface.close!(qry) # test it doesn't throw on double-close + end end - end - @testset "Issue #341: load! inside transaction" begin - db = SQLite.DB() - # Test that load! works when called inside an existing transaction - data = [(a=1, b="x"), (a=2, b="y"), (a=3, b="z")] - - # First verify load! works outside a transaction - SQLite.load!(data, db, "test_outside") - result = DBInterface.execute(db, "SELECT * FROM test_outside") |> columntable - @test result.a == [1, 2, 3] - @test result.b == ["x", "y", "z"] - - # Now test load! inside a transaction (this is what issue #341 reported as failing) - DBInterface.transaction(db) do - data2 = [(c=10, d="hello"), (c=20, d="world")] - SQLite.load!(data2, db, "test_inside") + @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 + + results1 = SQLite.tables(db) + @test isequal(ds.name, [t.name for t in results1]) + end end - result2 = DBInterface.execute(db, "SELECT * FROM test_inside") |> columntable - @test result2.c == [10, 20] - @test result2.d == ["hello", "world"] - - # Test nested transaction with rollback - DBInterface.transaction(db) do - data3 = [(e=100,)] - SQLite.load!(data3, db, "test_rollback") - # This table should exist within the transaction - @test DBInterface.execute(db, "SELECT * FROM test_rollback") |> columntable |> x -> x.e == [100] + + @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(::SQLite.Query) = error("I'm throwing!") + @test_throws ErrorException DBInterface.execute( + f, + employees_stmt, + ) + @test_throws ErrorException DBInterface.execute( + f, + db, + "SELECT * FROM Employee", + ) + end + DBInterface.close!(employees_stmt) + end end - # Table should still exist after commit - @test DBInterface.execute(db, "SELECT * FROM test_rollback") |> columntable |> x -> x.e == [100] - - # Test intransaction helper - @test SQLite.intransaction(db) == false - SQLite.transaction(db) - @test SQLite.intransaction(db) == true - SQLite.commit(db) - @test SQLite.intransaction(db) == false - 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 blob") - stmt = DBInterface.prepare(db, "update temp set dates = ?") - DBInterface.execute(stmt, (Date(2014, 1, 1),)) + @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 - 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]]) - DBInterface.execute(db, "drop table temp") + @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), + ) - rng = Dates.Date(2013):Dates.Day(1):Dates.Date(2013, 1, 5) - dt = (i = collect(rng), j = collect(rng)) - tablename = dt |> SQLite.load!(db, "temp") - r = - DBInterface.execute(db, "select * from $tablename") |> - columntable - @test length(r) == 2 && length(r[1]) == 5 - @test all([i for i in r[1]] .== collect(rng)) - @test all([isa(i, Dates.Date) for i in r[1]]) - SQLite.drop!(db, "$tablename") + 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) + @test all( + col -> + eltype(empty_tbl[col]) == Missing || + eltype(empty_tbl[col]) >: eltype(all_tbl[col]), + propertynames(all_tbl), + ) + end 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 = + @testset "Prepared Statements" begin + setup_clean_test_db() do db DBInterface.execute( db, - "SELECT * FROM temp WHERE Title LIKE ?", - ["%time%"], - ) |> columntable - @test r[1] == [76, 111, 187] - DBInterface.execute( - db, - "INSERT INTO temp VALUES (?1, ?3, ?2)", - [0, 0, "Test Album"], - ) - r = + "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 LIKE ?", + ["%time%"], + ) |> columntable + @test r[1] == [76, 111, 187] 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 - SQLite.drop!(db, "temp") + "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 + SQLite.drop!(db, "temp") - DBInterface.execute(db, "CREATE TABLE temp AS SELECT * FROM Album") - r = DBInterface.execute( db, - "SELECT * FROM temp LIMIT :a", - (a = 3,), - ) |> columntable - @test length(r) == 3 && length(r[1]) == 3 - r = - DBInterface.execute(db, "SELECT * FROM temp LIMIT :a"; a = 3) |> - columntable - @test length(r) == 3 && length(r[1]) == 3 - r = + "CREATE TABLE temp AS SELECT * FROM Album", + ) + r = + DBInterface.execute( + db, + "SELECT * FROM temp LIMIT :a", + (a = 3,), + ) |> columntable + @test length(r) == 3 && length(r[1]) == 3 + r = + DBInterface.execute( + db, + "SELECT * FROM temp LIMIT :a"; + a = 3, + ) |> columntable + @test length(r) == 3 && length(r[1]) == 3 + r = + DBInterface.execute( + db, + "SELECT * FROM temp WHERE Title LIKE @word", + (word = "%time%",), + ) |> columntable + @test r[1] == [76, 111, 187] DBInterface.execute( db, - "SELECT * FROM temp WHERE Title LIKE @word", - (word = "%time%",), - ) |> columntable - @test r[1] == [76, 111, 187] + "INSERT INTO temp VALUES (@lid, :title, \$rid)", + (rid = 1, lid = 0, title = "Test Album"), + ) + DBInterface.execute( + db, + "INSERT INTO temp VALUES (@lid, :title, \$rid)"; + rid = 3, + lid = 400, + title = "Test2 Album", + ) + 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] + SQLite.drop!(db, "temp") + end + end + + @testset "Issue #104: bind!() fails with named parameters" begin + db = SQLite.DB() #In case the order of tests is changed DBInterface.execute( db, - "INSERT INTO temp VALUES (@lid, :title, \$rid)", - (rid = 1, lid = 0, title = "Test Album"), + "CREATE TABLE IF NOT EXISTS tbl(a INTEGER);", ) - DBInterface.execute( - db, - "INSERT INTO temp VALUES (@lid, :title, \$rid)"; - rid = 3, - lid = 400, - title = "Test2 Album", + stmt = DBInterface.prepare(db, "INSERT INTO tbl (a) VALUES (@a);") + SQLite.bind!(stmt, "@a", 1) + SQLite.clear!(stmt) + end + + @testset "Issue #180, Query" begin + param = "Hello!" + query = DBInterface.execute( + SQLite.DB(), + "SELECT ?1 UNION ALL SELECT ?1", + [param], ) - 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] - SQLite.drop!(db, "temp") + param = "x" + for row in query + @test row[1] == "Hello!" + GC.gc() # this must NOT garbage collect the "Hello!" bound value + end - SQLite.register(db, SQLite.regexp; nargs = 2, name = "regexp") - r = - DBInterface.execute( - db, - @raw_str( - "SELECT LastName FROM Employee WHERE BirthDate REGEXP '^\\d{4}-08'" - ) - ) |> columntable - @test r[1][1] == "Peacock" + db = SQLite.DB() + DBInterface.execute(db, "CREATE TABLE T (a TEXT, PRIMARY KEY (a))") - SQLite.register(db, identity; nargs = 1, name = "identity") - r = - DBInterface.execute( - db, - """SELECT identity("abc") as x, "abc" == identity("abc") as cmp""", - ) |> columntable - @test first(r.x) == "abc" - @test first(r.cmp) == 1 + q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)") + DBInterface.execute(q, ["a"]) - @test_throws AssertionError SQLite.register(db, triple, nargs = 186) - SQLite.register(db, triple; nargs = 1) - r = - DBInterface.execute( - db, - "SELECT triple(Total) FROM Invoice ORDER BY InvoiceId LIMIT 5", - ) |> columntable - s = - DBInterface.execute( - db, - "SELECT Total FROM Invoice ORDER BY InvoiceId LIMIT 5", - ) |> columntable - for (i, j) in zip(r[1], s[1]) - @test abs(i - 3 * j) < 0.02 - end + SQLite.bind!(q, 1, "a") + @test_throws SQLiteException DBInterface.execute(q) end - end - @testset "Register functions" begin - setup_clean_test_db() do db - SQLite.@register db add4 + @testset "SQLite.execute()" begin + db = SQLite.DB() + DBInterface.execute(db, "CREATE TABLE T (x INT UNIQUE)") + + q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)") + SQLite.execute(q, (1,)) + r = DBInterface.execute(db, "SELECT * FROM T") |> columntable + @test r[1] == [1] + + SQLite.execute(q, [2]) + r = DBInterface.execute(db, "SELECT * FROM T") |> columntable + @test r[1] == [1, 2] + + q = DBInterface.prepare(db, "INSERT INTO T VALUES(:x)") + SQLite.execute(q, Dict(:x => 3)) + r = DBInterface.execute(columntable, db, "SELECT * FROM T") + @test r[1] == [1, 2, 3] + + SQLite.execute(q; x = 4) + r = DBInterface.execute(columntable, db, "SELECT * FROM T") + @test r[1] == [1, 2, 3, 4] + + SQLite.execute(db, "INSERT INTO T VALUES(:x)"; x = 5) + r = DBInterface.execute(columntable, db, "SELECT * FROM T") + @test r[1] == [1, 2, 3, 4, 5] + r = - DBInterface.execute(db, "SELECT add4(AlbumId) FROM Album") |> - columntable - s = - DBInterface.execute(db, "SELECT AlbumId FROM Album") |> + DBInterface.execute(db, strip(" SELECT * FROM T ")) |> columntable - @test r[1][1] == s[1][1] + 4 + @test r[1] == [1, 2, 3, 4, 5] + + SQLite.createindex!(db, "T", "x", "x_index"; unique = false) + inds = SQLite.indices(db) + @test last(inds.name) == "x" + SQLite.dropindex!(db, "x") + @test length(SQLite.indices(db).name) == 1 + + cols = SQLite.columns(db, "T") + @test cols.name == ["x"] + + @test SQLite.last_insert_rowid(db) == 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() + @test eltype(r) == SQLite.Row + row = first(r) + SQLite.reset!(r) + row2 = first(r) + @test row[:x] == row2[:x] + @test propertynames(row) == [:x] + @test DBInterface.lastrowid(r) == 5 + + r = DBInterface.execute(db, "SELECT * FROM T") |> columntable + SQLite.load!( + nothing, + Tables.rows(r), + db, + "T2", + SQLite.tableinfo(db, "T2"), + ) + r2 = DBInterface.execute(db, "SELECT * FROM T2") |> columntable + @test r == r2 + end - SQLite.@register db mult - r = + @testset "Issue #253: Ensure query column names are unique by default" begin + db = SQLite.DB() + res = DBInterface.execute( db, - "SELECT GenreId, UnitPrice FROM Track", + "select 1 as x2, 2 as x2, 3 as x2, 4 as x2_2", ) |> columntable - s = + @test res == (x2 = [1], x2_1 = [2], x2_2 = [3], x2_2_1 = [4]) + end + + @testset "executemany" begin + db = SQLite.DB() + DBInterface.execute( + db, + "create table tmp (a integer, b integer, c integer)", + ) + stmt = DBInterface.prepare(db, "INSERT INTO tmp VALUES(?, ?, ?)") + tbl = (a = [1, 1, 1], b = [3, 4, 5], c = [4, 5, 6]) + DBInterface.executemany(stmt, tbl) + tbl2 = DBInterface.execute(db, "select * from tmp") |> columntable + @test tbl == tbl2 + end + + @testset "query iteration protocol" begin + # https://github.com/JuliaDatabases/SQLite.jl/issues/251 + # A Query row is only valid while it is the current cursor position; + # advancing the cursor invalidates the previously-returned row. + # The table is recreated here (the original suite reused the table + # built by the issue #259 test above) so this testset is self-contained. + db = SQLite.DB() + SQLite.load!(UnknownSchemaTable(), db, "tbl") + + q = DBInterface.execute(db, "select * from tbl") + row, st = iterate(q) + @test row.a == 1 && row.b == 3 && row.c == 4 + row2, st = iterate(q, st) + @test_throws ArgumentError row.a + end + end + + @testset "transactions" begin + @testset "Create table, run commit/rollback tests" begin + setup_clean_test_db() do db DBInterface.execute( db, - "SELECT mult(GenreId, UnitPrice) FROM Track", - ) |> columntable - @test (r[1][1] * r[2][1]) == s[1][1] - t = + "create table temp as select * from album", + ) DBInterface.execute( db, - "SELECT mult(GenreId, UnitPrice, 3, 4) FROM Track", - ) |> columntable - @test (r[1][1] * r[2][1] * 3 * 4) == t[1][1] + "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 SQLiteException SQLite.rollback(db) + @test_throws SQLiteException SQLite.commit(db) + + SQLite.transaction(db) + DBInterface.execute(db, "update temp set colyear = 2015") + SQLite.rollback(db) + r = + DBInterface.execute(db, "select * from temp limit 10") |> + columntable + @test all(==(2014), r[4]) + + SQLite.transaction(db) + DBInterface.execute(db, "update temp set colyear = 2015") + SQLite.commit(db) + r = + DBInterface.execute(db, "select * from temp limit 10") |> + columntable + @test all(==(2015), r[4]) + end + end - SQLite.@register db sin - u = - DBInterface.execute( - db, - "select sin(milliseconds) from track limit 5", - ) |> columntable - @test all(-1 .< convert(Vector{Float64}, u[1]) .< 1) + @testset "Issue #341: load! inside transaction" begin + db = SQLite.DB() + # Test that load! works when called inside an existing transaction + data = [(a = 1, b = "x"), (a = 2, b = "y"), (a = 3, b = "z")] - SQLite.register(db, hypot; nargs = 2, name = "hypotenuse") - v = - DBInterface.execute( - db, - "select hypotenuse(Milliseconds,bytes) from track limit 5", - ) |> columntable - @test [round(Int, i) for i in v[1]] == [11175621, 5521062, 3997652, 4339106, 6301714] + # First verify load! works outside a transaction + SQLite.load!(data, db, "test_outside") + result = + DBInterface.execute(db, "SELECT * FROM test_outside") |> + columntable + @test result.a == [1, 2, 3] + @test result.b == ["x", "y", "z"] - SQLite.@register db str2arr - r = - DBInterface.execute( - db, - "SELECT str2arr(LastName) FROM Employee LIMIT 2", - ) |> columntable - @test r[1][2] == UInt8[0x45, 0x64, 0x77, 0x61, 0x72, 0x64, 0x73] + # Now test load! inside a transaction (this is what issue #341 reported as failing) + DBInterface.transaction(db) do + data2 = [(c = 10, d = "hello"), (c = 20, d = "world")] + SQLite.load!(data2, db, "test_inside") + end + result2 = + DBInterface.execute(db, "SELECT * FROM test_inside") |> + columntable + @test result2.c == [10, 20] + @test result2.d == ["hello", "world"] + + # Test nested transaction with rollback + DBInterface.transaction(db) do + data3 = [(e = 100,)] + SQLite.load!(data3, db, "test_rollback") + # This table should exist within the transaction + @test DBInterface.execute(db, "SELECT * FROM test_rollback") |> + columntable |> + x -> x.e == [100] + end + # Table should still exist after commit + @test DBInterface.execute(db, "SELECT * FROM test_rollback") |> + columntable |> + x -> x.e == [100] - SQLite.@register db big - r = DBInterface.execute(db, "SELECT big(5)") |> columntable - @test r[1][1] == big(5) - @test typeof(r[1][1]) == BigInt + # Test intransaction helper + @test SQLite.intransaction(db) == false + SQLite.transaction(db) + @test SQLite.intransaction(db) == true + SQLite.commit(db) + @test SQLite.intransaction(db) == false + end + end - SQLite.register( + @testset "load! / drop!" begin + @testset "Remove Duplicates" begin + setup_clean_test_db() do db + db = SQLite.DB() #In case the order of tests is changed + dt = ( + ints = Int64[1, 1, 2, 2, 3], + strs = ["A", "A", "B", "C", "C"], + ) + tablename = dt |> SQLite.load!(db, "temp") + SQLite.removeduplicates!(db, "temp", ["ints", "strs"]) #New format + dt3 = + DBInterface.execute(db, "Select * from temp") |> columntable + @test dt3[1][1] == 1 + @test dt3[2][1] == "A" + @test dt3[1][2] == 2 + @test dt3[2][2] == "B" + @test dt3[1][3] == 2 + @test dt3[2][3] == "C" + end + end + + @testset "Issue #193: Throw informative error on duplicate column names" begin + db = SQLite.DB() + @test_throws SQLiteException SQLite.load!( + (a = [1, 2, 3], A = [1, 2, 3]), db, - 0, - doublesum_step, - doublesum_final; - name = "doublesum", ) + end + + @testset "Issue #216: Table should map by name" begin + db = SQLite.DB() + + tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) + tbl2 = (b = [7, 8, 9], a = [4, 5, 6]) + SQLite.load!(tbl1, db, "data") + SQLite.load!(tbl2, db, "data") + + res = DBInterface.execute(db, "SELECT * FROM data") |> columntable + expected = (a = [1, 2, 3, 4, 5, 6], b = [4, 5, 6, 7, 8, 9]) + @test res == expected + end + + @testset "Issue #216: Table should error if names don't match" begin + db = SQLite.DB() + + tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) + SQLite.load!(tbl1, db, "data") + tbl3 = (c = [7, 8, 9], a = [4, 5, 6]) + @test_throws SQLiteException SQLite.load!(tbl3, db, "data") + end + + @testset "load!() / drop!() table name escaping" begin + db = SQLite.DB() + tbl = (a = [1, 2, 3], b = ["a", "b", "c"]) + SQLite.load!(tbl, db, "escape 10.0%") r = DBInterface.execute( db, - "SELECT doublesum(UnitPrice) FROM Track", + "SELECT * FROM $(SQLite.esc_id("escape 10.0%"))", ) |> columntable - s = - DBInterface.execute(db, "SELECT UnitPrice FROM Track") |> - columntable - @test abs(r[1][1] - 2 * sum(convert(Vector{Float64}, s[1]))) < 0.02 + @test r == tbl + SQLite.drop!(db, "escape 10.0%") + end - SQLite.register(db, 0, mycount) + @testset "load!() column names escaping" begin + db = SQLite.DB() + tbl = + NamedTuple{(:a, Symbol("50.0%"))}(([1, 2, 3], ["a", "b", "c"])) + SQLite.load!(tbl, db, "escape_colnames") r = - DBInterface.execute( - db, - "SELECT mycount(TrackId) FROM PlaylistTrack", - ) |> columntable - s = - DBInterface.execute( - db, - "SELECT count(TrackId) FROM PlaylistTrack", - ) |> columntable - @test r[1][1] == s[1][1] + DBInterface.execute(db, "SELECT * FROM escape_colnames") |> + columntable + @test r == tbl + SQLite.drop!(db, "escape_colnames") + end - SQLite.register(db, big(0), bigsum) + @testset "Bool column data" begin + db = SQLite.DB() + tbl = (a = [true, false, false], b = [false, missing, true]) + SQLite.load!(tbl, db, "bool_data") r = - DBInterface.execute( - db, - "SELECT bigsum(TrackId) FROM PlaylistTrack", - ) |> columntable - s = - DBInterface.execute(db, "SELECT TrackId FROM PlaylistTrack") |> + DBInterface.execute(db, "SELECT * FROM bool_data") |> columntable - @test r[1][1] == big(sum(convert(Vector{Int}, s[1]))) + @test isequal(r, (a = [1, 0, 0], b = [0, missing, 1])) + SQLite.drop!(db, "bool_data") + end + + @testset "load! with unknown schema source" begin + # https://github.com/JuliaDatabases/SQLite.jl/issues/259 + db = SQLite.DB() + SQLite.load!(UnknownSchemaTable(), db, "tbl") + tbl = DBInterface.execute(db, "select * from tbl") |> columntable + @test tbl == (a = [1, 1, 1], b = [3, 4, 5], c = [4, 5, 6]) - DBInterface.execute(db, "CREATE TABLE points (x INT, y INT, z INT)") + # https://github.com/JuliaDatabases/SQLite.jl/issues/243 + db = SQLite.DB() DBInterface.execute( db, - "INSERT INTO points VALUES (?, ?, ?)", - (1, 2, 3), + "create table tmp ( a INTEGER NOT NULL PRIMARY KEY, b INTEGER, c INTEGER )", ) - DBInterface.execute( + @test_throws SQLite.SQLiteException SQLite.load!( + UnknownSchemaTable(), db, - "INSERT INTO points VALUES (?, ?, ?)", - (4, 5, 6), + "tmp", ) + SQLite.load!(UnknownSchemaTable(), db, "tmp"; replace = true) + tbl = DBInterface.execute(db, "select * from tmp") |> columntable + @test tbl == (a = [1], b = [5], c = [6]) + end + + @testset "PR #302: load! on_conflict clauses" begin + # https://github.com/JuliaDatabases/SQLite.jl/pull/302 + db = SQLite.DB() DBInterface.execute( db, - "INSERT INTO points VALUES (?, ?, ?)", - (7, 8, 9), + "create table tmp ( a INTEGER NOT NULL PRIMARY KEY, b INTEGER, c INTEGER )", ) - - SQLite.register(db, Point3D(0, 0, 0), sumpoint) - r = - DBInterface.execute( - db, - "SELECT sumpoint(x, y, z) FROM points", - ) |> columntable - @test r[1][1] == Point3D(12, 15, 18) - SQLite.drop!(db, "points") - - db2 = DBInterface.connect(SQLite.DB) - DBInterface.execute(db2, "CREATE TABLE tab1 (r REAL, s INT)") - - @test_throws SQLiteException SQLite.drop!(db2, "nonexistant") - # should not throw anything - SQLite.drop!(db2, "nonexistant"; ifexists = true) - # should drop "tab2" - SQLite.drop!(db2, "tab2"; ifexists = true) - @test filter(x -> x.name == "tab2", SQLite.tables(db2)) |> length == - 0 - - SQLite.drop!(db, "sqlite_stat1"; ifexists = true) - tables = SQLite.tables(db) - @test length(tables) == 11 - end - end - - @testset "Remove Duplicates" begin - setup_clean_test_db() do db - db = SQLite.DB() #In case the order of tests is changed - dt = (ints = Int64[1, 1, 2, 2, 3], strs = ["A", "A", "B", "C", "C"]) - tablename = dt |> SQLite.load!(db, "temp") - SQLite.removeduplicates!(db, "temp", ["ints", "strs"]) #New format - dt3 = DBInterface.execute(db, "Select * from temp") |> columntable - @test dt3[1][1] == 1 - @test dt3[2][1] == "A" - @test dt3[1][2] == 2 - @test dt3[2][2] == "B" - @test dt3[1][3] == 2 - @test dt3[2][3] == "C" + @test_throws SQLite.SQLiteException SQLite.load!( + UnknownSchemaTable(), + db, + "tmp", + on_conflict = "ROLLBACK", + ) + tbl = DBInterface.execute(db, "select * from tmp") |> columntable + @test tbl == (a = [], b = [], c = []) + @test_throws SQLite.SQLiteException SQLite.load!( + UnknownSchemaTable(), + db, + "tmp", + on_conflict = "ABORT", + ) + @test_throws SQLite.SQLiteException SQLite.load!( + UnknownSchemaTable(), + db, + "tmp", + on_conflict = "FAIL", + ) + SQLite.load!( + UnknownSchemaTable(), + db, + "tmp"; + on_conflict = "IGNORE", + ) + tbl = DBInterface.execute(db, "select * from tmp") |> columntable + @test tbl == (a = [1], b = [3], c = [4]) + SQLite.load!( + UnknownSchemaTable(), + db, + "tmp"; + on_conflict = "REPLACE", + ) + tbl = DBInterface.execute(db, "select * from tmp") |> columntable + @test tbl == (a = [1], b = [5], c = [6]) end end - @testset "Issue #104: bind!() fails with named parameters" begin - db = SQLite.DB() #In case the order of tests is changed - DBInterface.execute(db, "CREATE TABLE IF NOT EXISTS tbl(a INTEGER);") - stmt = DBInterface.prepare(db, "INSERT INTO tbl (a) VALUES (@a);") - SQLite.bind!(stmt, "@a", 1) - SQLite.clear!(stmt) - end + @testset "strict mode" begin + @testset "PR #343: strict (and only strict) tables should error if types don't match" begin + db = SQLite.DB() - @testset "SQLite to Julia type conversion" begin - binddb = SQLite.DB() - DBInterface.execute( - binddb, - "CREATE TABLE temp (n NULL, i1 INT, i2 integer, - f1 REAL, f2 FLOAT, f3 NUMERIC, - s1 TEXT, s2 CHAR(10), s3 VARCHAR(15), s4 NVARCHAR(5), - d1 DATETIME, ts1 TIMESTAMP, - b BLOB, - x1 UNKNOWN1, x2 UNKNOWN2)", - ) - 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::1532", - b"bytearray", - "actually known", - 435, - ], - ) - rr = DBInterface.execute(rowtable, binddb, "SELECT * FROM temp") - @test length(rr) == 1 - r = first(rr) - @test typeof.(Tuple(r)) == ( - Missing, - Int64, - Int64, - Float64, - Float64, - Int64, - String, - String, - String, - String, - String, - String, - Base.CodeUnits{UInt8,String}, - String, - Int64, - ) - end + tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) + SQLite.load!(tbl1, db, "data_default") + SQLite.load!(tbl1, db, "data_strict", strict = true) - @testset "Issue #158: Missing DB File" begin - @test_throws SQLiteException SQLite.DB("nonexistentdir/not_there.db") - end + tbl2 = (a = ["a", "b", "c"], b = [7, 8, 9]) + SQLite.load!(tbl2, db, "data_default") + @test_throws SQLiteException SQLite.load!(tbl2, db, "data_strict") + end - @testset "Issue #180, Query" begin - param = "Hello!" - query = DBInterface.execute( - SQLite.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 + @testset "Symbol in TEXT column under strict mode" begin + db = SQLite.DB() + DBInterface.execute(db, "create table tmp ( x TEXT )") + DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) + DBInterface.execute(db, "insert into tmp values (?)", (:a,)) + tbl = DBInterface.execute(db, "select x from tmp") |> columntable + @test isequal(tbl.x, [missing, :a]) + + # Symbol in TEXT type doesn't work + # when strict and first row is NULL (the serialized bytes are interpreted as a string) + tbl = + DBInterface.execute( + DBInterface.prepare(db, "select x from tmp"), + (); + strict = true, + ) |> columntable + @test tbl.x[1] === missing + @test tbl.x[2] isa String # Symbol gets incorrectly deserialized as garbage string end - db = SQLite.DB() - DBInterface.execute(db, "CREATE TABLE T (a TEXT, PRIMARY KEY (a))") + @testset "Symbol in BLOB column under strict mode" begin + # Symbol in BLOB type does work strict + db = SQLite.DB() + DBInterface.execute(db, "create table tmp ( x BLOB )") + DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) + DBInterface.execute(db, "insert into tmp values (?)", (:a,)) + tbl = DBInterface.execute(db, "select x from tmp") |> columntable + @test isequal(tbl.x, [missing, :a]) - q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)") - DBInterface.execute(q, ["a"]) + tbl = + DBInterface.execute( + DBInterface.prepare(db, "select x from tmp"), + (); + strict = true, + ) |> columntable + @test isequal(tbl.x, [missing, :a]) + end - SQLite.bind!(q, 1, "a") - @test_throws SQLiteException DBInterface.execute(q) - end + @testset "Symbol in TEXT column, non-NULL first row" begin + # Symbol in TEXT always works when first row is not NULL + db = SQLite.DB() + DBInterface.execute(db, "create table tmp ( x TEXT )") + DBInterface.execute(db, "insert into tmp values (?)", (:a,)) + DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) + tbl = DBInterface.execute(db, "select x from tmp") |> columntable + @test isequal(tbl.x, [:a, missing]) - @testset "Enable extension" begin - db = SQLite.DB() - @test SQLite.@OK SQLite.enable_load_extension(db) + tbl = + DBInterface.execute( + DBInterface.prepare(db, "select x from tmp"), + (); + strict = true, + ) |> columntable + @test isequal(tbl.x, [:a, missing]) + end end - @testset "show(DB)" begin - io = IOBuffer() - db = SQLite.DB() + @testset "UDF registration" begin + @testset "Register functions via prepared statements" begin + setup_clean_test_db() do db + SQLite.register(db, SQLite.regexp; nargs = 2, name = "regexp") + r = + DBInterface.execute( + db, + @raw_str( + "SELECT LastName FROM Employee WHERE BirthDate REGEXP '^\\d{4}-08'" + ) + ) |> columntable + @test r[1][1] == "Peacock" + + SQLite.register(db, identity; nargs = 1, name = "identity") + r = + DBInterface.execute( + db, + """SELECT identity("abc") as x, "abc" == identity("abc") as cmp""", + ) |> columntable + @test first(r.x) == "abc" + @test first(r.cmp) == 1 + + @test_throws AssertionError SQLite.register( + db, + triple, + nargs = 186, + ) + SQLite.register(db, triple; nargs = 1) + r = + DBInterface.execute( + db, + "SELECT triple(Total) FROM Invoice ORDER BY InvoiceId LIMIT 5", + ) |> columntable + s = + DBInterface.execute( + db, + "SELECT Total FROM Invoice ORDER BY InvoiceId LIMIT 5", + ) |> columntable + for (i, j) in zip(r[1], s[1]) + @test abs(i - 3 * j) < 0.02 + end + end + end - show(io, db) - @test String(take!(io)) == "SQLite.DB(\":memory:\")" + @testset "Register scalar and aggregate functions" begin + setup_clean_test_db() do db + SQLite.@register db add4 + r = + DBInterface.execute( + db, + "SELECT add4(AlbumId) FROM Album", + ) |> columntable + s = + DBInterface.execute(db, "SELECT AlbumId FROM Album") |> + columntable + @test r[1][1] == s[1][1] + 4 + + SQLite.@register db mult + r = + DBInterface.execute( + db, + "SELECT GenreId, UnitPrice FROM Track", + ) |> columntable + s = + DBInterface.execute( + db, + "SELECT mult(GenreId, UnitPrice) FROM Track", + ) |> columntable + @test (r[1][1] * r[2][1]) == s[1][1] + t = + DBInterface.execute( + db, + "SELECT mult(GenreId, UnitPrice, 3, 4) FROM Track", + ) |> columntable + @test (r[1][1] * r[2][1] * 3 * 4) == t[1][1] + + SQLite.@register db sin + u = + DBInterface.execute( + db, + "select sin(milliseconds) from track limit 5", + ) |> columntable + @test all(-1 .< convert(Vector{Float64}, u[1]) .< 1) + + SQLite.register(db, hypot; nargs = 2, name = "hypotenuse") + v = + DBInterface.execute( + db, + "select hypotenuse(Milliseconds,bytes) from track limit 5", + ) |> columntable + @test [round(Int, i) for i in v[1]] == [11175621, 5521062, 3997652, 4339106, 6301714] + + SQLite.@register db str2arr + r = + DBInterface.execute( + db, + "SELECT str2arr(LastName) FROM Employee LIMIT 2", + ) |> columntable + @test r[1][2] == UInt8[0x45, 0x64, 0x77, 0x61, 0x72, 0x64, 0x73] + + SQLite.@register db big + r = DBInterface.execute(db, "SELECT big(5)") |> columntable + @test r[1][1] == big(5) + @test typeof(r[1][1]) == BigInt + + SQLite.register( + db, + 0, + doublesum_step, + doublesum_final; + name = "doublesum", + ) + r = + DBInterface.execute( + db, + "SELECT doublesum(UnitPrice) FROM Track", + ) |> columntable + s = + DBInterface.execute(db, "SELECT UnitPrice FROM Track") |> + columntable + @test abs(r[1][1] - 2 * sum(convert(Vector{Float64}, s[1]))) < + 0.02 + + SQLite.register(db, 0, mycount) + r = + DBInterface.execute( + db, + "SELECT mycount(TrackId) FROM PlaylistTrack", + ) |> columntable + s = + DBInterface.execute( + db, + "SELECT count(TrackId) FROM PlaylistTrack", + ) |> columntable + @test r[1][1] == s[1][1] + + SQLite.register(db, big(0), bigsum) + r = + DBInterface.execute( + db, + "SELECT bigsum(TrackId) FROM PlaylistTrack", + ) |> columntable + s = + DBInterface.execute( + db, + "SELECT TrackId FROM PlaylistTrack", + ) |> columntable + @test r[1][1] == big(sum(convert(Vector{Int}, s[1]))) + + DBInterface.execute( + db, + "CREATE TABLE points (x INT, y INT, z INT)", + ) + DBInterface.execute( + db, + "INSERT INTO points VALUES (?, ?, ?)", + (1, 2, 3), + ) + DBInterface.execute( + db, + "INSERT INTO points VALUES (?, ?, ?)", + (4, 5, 6), + ) + DBInterface.execute( + db, + "INSERT INTO points VALUES (?, ?, ?)", + (7, 8, 9), + ) - DBInterface.close!(db) + SQLite.register(db, Point3D(0, 0, 0), sumpoint) + r = + DBInterface.execute( + db, + "SELECT sumpoint(x, y, z) FROM points", + ) |> columntable + @test r[1][1] == Point3D(12, 15, 18) + SQLite.drop!(db, "points") + + db2 = DBInterface.connect(SQLite.DB) + DBInterface.execute(db2, "CREATE TABLE tab1 (r REAL, s INT)") + + @test_throws SQLiteException SQLite.drop!(db2, "nonexistant") + # should not throw anything + SQLite.drop!(db2, "nonexistant"; ifexists = true) + # should drop "tab2" + SQLite.drop!(db2, "tab2"; ifexists = true) + @test filter(x -> x.name == "tab2", SQLite.tables(db2)) |> + length == 0 + + SQLite.drop!(db, "sqlite_stat1"; ifexists = true) + tables = SQLite.tables(db) + @test length(tables) == 11 + end + end end - @testset "SQLite.execute()" begin + @testset "UDF value marshalling" begin + # These tests pin down SQLite.sqlvalue, which unmarshals a C + # sqlite3_value* into a Julia value on the UDF argument hot path. A + # registered `identity` UDF forces every argument through sqlvalue on + # the way in and sqlreturn on the way out, so `roundtrip(x)` exercises + # both marshalling directions for a single value and lets us assert + # exact round-trip equality. The Int64-boundary cases in particular + # cover the `Sys.WORD_SIZE == 64` branch of sqlvalue: on a 64-bit build + # the value must be read with sqlite3_value_int64; sqlite3_value_int + # would silently truncate anything outside the Int32 range. db = SQLite.DB() - DBInterface.execute(db, "CREATE TABLE T (x INT UNIQUE)") - - q = DBInterface.prepare(db, "INSERT INTO T VALUES(?)") - SQLite.execute(q, (1,)) - r = DBInterface.execute(db, "SELECT * FROM T") |> columntable - @test r[1] == [1] - - SQLite.execute(q, [2]) - r = DBInterface.execute(db, "SELECT * FROM T") |> columntable - @test r[1] == [1, 2] - - q = DBInterface.prepare(db, "INSERT INTO T VALUES(:x)") - SQLite.execute(q, Dict(:x => 3)) - r = DBInterface.execute(columntable, db, "SELECT * FROM T") - @test r[1] == [1, 2, 3] - - SQLite.execute(q; x = 4) - r = DBInterface.execute(columntable, db, "SELECT * FROM T") - @test r[1] == [1, 2, 3, 4] - - SQLite.execute(db, "INSERT INTO T VALUES(:x)"; x = 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] - - SQLite.createindex!(db, "T", "x", "x_index"; unique = false) - inds = SQLite.indices(db) - @test last(inds.name) == "x" - SQLite.dropindex!(db, "x") - @test length(SQLite.indices(db).name) == 1 - - cols = SQLite.columns(db, "T") - @test cols.name == ["x"] - - @test SQLite.last_insert_rowid(db) == 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() - @test eltype(r) == SQLite.Row - row = first(r) - SQLite.reset!(r) - row2 = first(r) - @test row[:x] == row2[:x] - @test propertynames(row) == [:x] - @test DBInterface.lastrowid(r) == 5 - - r = DBInterface.execute(db, "SELECT * FROM T") |> columntable - SQLite.load!( - nothing, - Tables.rows(r), - db, - "T2", - SQLite.tableinfo(db, "T2"), + SQLite.register(db, identity; nargs = 1, name = "roundtrip") + + roundtrip(v) = first( + DBInterface.execute(db, "SELECT roundtrip(?) AS v", (v,)) |> + columntable |> + t -> t.v, ) - r2 = DBInterface.execute(db, "SELECT * FROM T2") |> columntable - @test r == r2 - end - @testset "Escaping" begin - @test SQLite.esc_id(["1", "2", "3"]) == "\"1\",\"2\",\"3\"" - end + @testset "Int64 boundary integers" begin + for v in ( + typemax(Int64), + typemin(Int64), + Int64(2)^40, + Int64(2)^31, # first value past Int32 max + Int64(2)^31 - 1, # Int32 max itself + -(Int64(2)^31) - 1, # first value below Int32 min + 0, + 1, + -1, + 42, + -42, + ) + got = roundtrip(v) + @test got isa Int64 + @test got == v + end + end - @testset "Issue #193: Throw informative error on duplicate column names" begin - db = SQLite.DB() - @test_throws SQLiteException SQLite.load!( - (a = [1, 2, 3], A = [1, 2, 3]), - db, - ) - end + @testset "Float64 values" begin + for v in (0.0, 3.5, -2.25, 6.4, floatmax(Float64)) + got = roundtrip(v) + @test got isa Float64 + @test got == v + end + end - @testset "Issue #216: Table should map by name" begin - db = SQLite.DB() + @testset "String values" begin + # includes multi-byte UTF-8: sqlite3_value_text returns bytes, so + # codeunit/character confusion would corrupt non-ASCII round-trips + for v in ("", "abc", "a longer string with spaces", "příliš žluťoučký kůň 🐎 ∀x") + got = roundtrip(v) + @test got isa String + @test got == v + end + end - tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) - tbl2 = (b = [7, 8, 9], a = [4, 5, 6]) - SQLite.load!(tbl1, db, "data") - SQLite.load!(tbl2, db, "data") + @testset "blob values" begin + v = UInt8[0x00, 0x01, 0xfe, 0xff] + got = roundtrip(v) + @test got == v + end - res = DBInterface.execute(db, "SELECT * FROM data") |> columntable - expected = (a = [1, 2, 3, 4, 5, 6], b = [4, 5, 6, 7, 8, 9]) - @test res == expected + @testset "NULL value" begin + @test roundtrip(missing) === missing + end end - @testset "Issue #216: Table should error if names don't match" begin - db = SQLite.DB() + @testset "serialization" begin + @testset "serialization edgecases" begin + db = SQLite.DB() + r = + DBInterface.execute(db, "SELECT zeroblob(2) as b") |> + columntable + @test first(r.b) == [0, 0] + r = + DBInterface.execute(db, "SELECT zeroblob(0) as b") |> + columntable + @test first(r.b) == [] + end - tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) - SQLite.load!(tbl1, db, "data") - tbl3 = (c = [7, 8, 9], a = [4, 5, 6]) - @test_throws SQLiteException SQLite.load!(tbl3, db, "data") + @testset "ReinterpretArray" begin + binddb = SQLite.DB() + DBInterface.execute(binddb, "CREATE TABLE temp (b BLOB)") + DBInterface.execute( + binddb, + "INSERT INTO temp VALUES (?)", + [reinterpret(UInt8, [0x6f46, 0x426f, 0x7261])], + ) + rr = DBInterface.execute(rowtable, binddb, "SELECT b FROM temp") + @test length(rr) == 1 + r = first(rr) + @test r.b == codeunits("FooBar") + @test typeof.(Tuple(r)) == (Vector{UInt8},) + end end - @testset "PR #343: strict (and only strict) tables should error if types don't match" begin - db = SQLite.DB() + @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 blob") + stmt = DBInterface.prepare(db, "update temp set dates = ?") + DBInterface.execute(stmt, (Date(2014, 1, 1),)) - tbl1 = (a = [1, 2, 3], b = [4, 5, 6]) - SQLite.load!(tbl1, db, "data_default") - SQLite.load!(tbl1, db, "data_strict", strict=true) - - tbl2 = (a = ["a", "b", "c"], b=[7, 8, 9]) - SQLite.load!(tbl2, db, "data_default") - @test_throws SQLiteException SQLite.load!(tbl2, db, "data_strict") - end + 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]]) + DBInterface.execute(db, "drop table temp") - @testset "Test busy_timeout" begin - db = SQLite.DB() - @test SQLite.busy_timeout(db, 300) == 0 + rng = Dates.Date(2013):Dates.Day(1):Dates.Date(2013, 1, 5) + dt = (i = collect(rng), j = collect(rng)) + tablename = dt |> SQLite.load!(db, "temp") + r = + DBInterface.execute(db, "select * from $tablename") |> + columntable + @test length(r) == 2 && length(r[1]) == 5 + @test all([i for i in r[1]] .== collect(rng)) + @test all([isa(i, Dates.Date) for i in r[1]]) + SQLite.drop!(db, "$tablename") + end end @testset "backup" begin @@ -864,10 +1289,11 @@ end @test SQLite.backup(db, tmp_path; sleep_ms = 1) == tmp_path db2 = SQLite.DB(tmp_path) try - r = DBInterface.execute( - db2, - "SELECT x FROM backup_test ORDER BY x", - ) |> columntable + r = + DBInterface.execute( + db2, + "SELECT x FROM backup_test ORDER BY x", + ) |> columntable @test r.x == [1, 2] finally close(db2) @@ -878,57 +1304,6 @@ end end end - @testset "Issue #253: Ensure query column names are unique by default" begin - db = SQLite.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 "load!() / drop!() table name escaping" begin - db = SQLite.DB() - tbl = (a = [1, 2, 3], b = ["a", "b", "c"]) - SQLite.load!(tbl, db, "escape 10.0%") - r = - DBInterface.execute( - db, - "SELECT * FROM $(SQLite.esc_id("escape 10.0%"))", - ) |> columntable - @test r == tbl - SQLite.drop!(db, "escape 10.0%") - end - - @testset "load!() column names escaping" begin - db = SQLite.DB() - tbl = NamedTuple{(:a, Symbol("50.0%"))}(([1, 2, 3], ["a", "b", "c"])) - SQLite.load!(tbl, db, "escape_colnames") - r = - DBInterface.execute(db, "SELECT * FROM escape_colnames") |> - columntable - @test r == tbl - SQLite.drop!(db, "escape_colnames") - end - - @testset "Bool column data" begin - db = SQLite.DB() - tbl = (a = [true, false, false], b = [false, missing, true]) - SQLite.load!(tbl, db, "bool_data") - r = DBInterface.execute(db, "SELECT * FROM bool_data") |> columntable - @test isequal(r, (a = [1, 0, 0], b = [0, missing, 1])) - SQLite.drop!(db, "bool_data") - end - - @testset "serialization edgecases" begin - db = SQLite.DB() - r = DBInterface.execute(db, "SELECT zeroblob(2) as b") |> columntable - @test first(r.b) == [0, 0] - r = DBInterface.execute(db, "SELECT zeroblob(0) as b") |> columntable - @test first(r.b) == [] - end - @testset "Stmt scope" begin dbfile = joinpath(tempdir(), "test_stmt_scope.sqlite") db = SQLite.DB(dbfile) @@ -980,185 +1355,4 @@ end close(db) rm(dbfile) end - - @testset "ReinterpretArray" begin - binddb = SQLite.DB() - DBInterface.execute( - binddb, - "CREATE TABLE temp (b BLOB)", - ) - DBInterface.execute( - binddb, - "INSERT INTO temp VALUES (?)", - [reinterpret(UInt8, [0x6f46, 0x426f, 0x7261]),], - ) - rr = DBInterface.execute(rowtable, binddb, "SELECT b FROM temp") - @test length(rr) == 1 - r = first(rr) - @test r.b == codeunits("FooBar") - @test typeof.(Tuple(r)) == (Vector{UInt8},) - end -end # @testset - -struct UnknownSchemaTable end - -Tables.isrowtable(::Type{UnknownSchemaTable}) = true -Tables.rows(x::UnknownSchemaTable) = x -Base.length(x::UnknownSchemaTable) = 3 -function Base.iterate(::UnknownSchemaTable, st = 1) - st == 4 ? nothing : ((a = 1, b = 2 + st, c = 3 + st), st + 1) -end - -@testset "SQLite Open Flags" begin - rm("test.db"; force = true) - @test_throws SQLiteException("unable to open database file") SQLite.DB( - "file:test.db?mode=ro", - ) - - db = SQLite.DB("file:test.db?mode=rwc") - @test db isa SQLite.DB - close(db) - rm("test.db"; force = true) -end - -@testset "misc" begin - - # https://github.com/JuliaDatabases/SQLite.jl/issues/259 - db = SQLite.DB() - SQLite.load!(UnknownSchemaTable(), db, "tbl") - tbl = DBInterface.execute(db, "select * from tbl") |> columntable - @test tbl == (a = [1, 1, 1], b = [3, 4, 5], c = [4, 5, 6]) - - # https://github.com/JuliaDatabases/SQLite.jl/issues/251 - q = DBInterface.execute(db, "select * from tbl") - row, st = iterate(q) - @test row.a == 1 && row.b == 3 && row.c == 4 - row2, st = iterate(q, st) - @test_throws ArgumentError row.a - - # https://github.com/JuliaDatabases/SQLite.jl/issues/243 - db = SQLite.DB() - DBInterface.execute( - db, - "create table tmp ( a INTEGER NOT NULL PRIMARY KEY, b INTEGER, c INTEGER )", - ) - @test_throws SQLite.SQLiteException SQLite.load!( - UnknownSchemaTable(), - db, - "tmp", - ) - SQLite.load!(UnknownSchemaTable(), db, "tmp"; replace = true) - tbl = DBInterface.execute(db, "select * from tmp") |> columntable - @test tbl == (a = [1], b = [5], c = [6]) - - # https://github.com/JuliaDatabases/SQLite.jl/pull/302 - db = SQLite.DB() - DBInterface.execute( - db, - "create table tmp ( a INTEGER NOT NULL PRIMARY KEY, b INTEGER, c INTEGER )", - ) - @test_throws SQLite.SQLiteException SQLite.load!( - UnknownSchemaTable(), - db, - "tmp", - on_conflict = "ROLLBACK", - ) - tbl = DBInterface.execute(db, "select * from tmp") |> columntable - @test tbl == (a = [], b = [], c = []) - @test_throws SQLite.SQLiteException SQLite.load!( - UnknownSchemaTable(), - db, - "tmp", - on_conflict = "ABORT", - ) - @test_throws SQLite.SQLiteException SQLite.load!( - UnknownSchemaTable(), - db, - "tmp", - on_conflict = "FAIL", - ) - SQLite.load!(UnknownSchemaTable(), db, "tmp"; on_conflict = "IGNORE") - tbl = DBInterface.execute(db, "select * from tmp") |> columntable - @test tbl == (a = [1], b = [3], c = [4]) - SQLite.load!(UnknownSchemaTable(), db, "tmp"; on_conflict = "REPLACE") - tbl = DBInterface.execute(db, "select * from tmp") |> columntable - @test tbl == (a = [1], b = [5], c = [6]) - - db = SQLite.DB() - DBInterface.execute(db, "create table tmp ( x TEXT )") - DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) - DBInterface.execute(db, "insert into tmp values (?)", (:a,)) - tbl = DBInterface.execute(db, "select x from tmp") |> columntable - @test isequal(tbl.x, [missing, :a]) - - # Symbol in TEXT type doesn't work - # when strict and first row is NULL (the serialized bytes are interpreted as a string) - tbl = - DBInterface.execute( - DBInterface.prepare(db, "select x from tmp"), - (); - strict = true, - ) |> columntable - @test tbl.x[1] === missing - @test tbl.x[2] isa String # Symbol gets incorrectly deserialized as garbage string - - # Symbol in BLOB type does work strict - db = SQLite.DB() - DBInterface.execute(db, "create table tmp ( x BLOB )") - DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) - DBInterface.execute(db, "insert into tmp values (?)", (:a,)) - tbl = DBInterface.execute(db, "select x from tmp") |> columntable - @test isequal(tbl.x, [missing, :a]) - - tbl = - DBInterface.execute( - DBInterface.prepare(db, "select x from tmp"), - (); - strict = true, - ) |> columntable - @test isequal(tbl.x, [missing, :a]) - - # Symbol in TEXT always works when first row is not NULL - db = SQLite.DB() - DBInterface.execute(db, "create table tmp ( x TEXT )") - DBInterface.execute(db, "insert into tmp values (?)", (:a,)) - DBInterface.execute(db, "insert into tmp values (?)", (nothing,)) - tbl = DBInterface.execute(db, "select x from tmp") |> columntable - @test isequal(tbl.x, [:a, missing]) - - tbl = - DBInterface.execute( - DBInterface.prepare(db, "select x from tmp"), - (); - strict = true, - ) |> columntable - @test isequal(tbl.x, [:a, missing]) - - db = SQLite.DB() - DBInterface.execute( - db, - "create table tmp (a integer, b integer, c integer)", - ) - stmt = DBInterface.prepare(db, "INSERT INTO tmp VALUES(?, ?, ?)") - tbl = (a = [1, 1, 1], b = [3, 4, 5], c = [4, 5, 6]) - DBInterface.executemany(stmt, tbl) - tbl2 = DBInterface.execute(db, "select * from tmp") |> columntable - @test tbl == tbl2 - - # https://github.com/JuliaDatabases/SQLite.jl/issues/331 - db_a = SQLite.DB(":memory:") - db_b = SQLite.DB(":memory:") - SQLite.register(db_a, x -> 2x; name = "myfunc") - SQLite.register(db_b, x -> 10x; name = "myfunc") - tbl_a = DBInterface.execute(db_a, "select myfunc(1) as x") |> columntable - tbl_b = DBInterface.execute(db_b, "select myfunc(1) as x") |> columntable - @test tbl_a.x == [2] - @test tbl_b.x == [10] - - # Throw an error when interfacing with a closed database - close(db_b) - @test_throws SQLiteException("DB is closed") DBInterface.execute( - db_b, - "select myfunc(1) as x", - ) -end +end # @testset "SQLite"