Skip to content

Commit ad1e2a0

Browse files
authored
Tables.jl integration (#154)
* Updates for 1.0 * Refactor Source/Sink to implement Tables.jl interface * Updates for new Tables.jl interface * Add Tables to REQUIRE
1 parent bd27c0c commit ad1e2a0

File tree

10 files changed

+672
-195
lines changed

10 files changed

+672
-195
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ os:
77
- osx
88

99
julia:
10-
- 0.7
10+
- 1.0
1111
- nightly
1212

1313
notifications:

REQUIRE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ DataFrames 0.11.0
44
WeakRefStrings 0.4.0
55
LegacyStrings
66
Missings
7-
BinaryProvider
7+
BinaryProvider
8+
Tables

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
environment:
22
matrix:
3-
- julia_version: 0.7
3+
- julia_version: 1
44
- julia_version: nightly
55

66
platform:

docs/src/index.md

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,8 @@
55

66
## High-level interface
77
```@docs
8-
SQLite.query
9-
SQLite.load
10-
```
11-
12-
## Lower-level utilities
13-
```@docs
14-
SQLite.Source
15-
SQLite.Sink
8+
SQLite.Query
9+
SQLite.load!
1610
```
1711

1812
## Types/Functions
@@ -59,11 +53,21 @@ SQLite.Sink
5953
Used to execute a prepared `SQLite.Stmt`. The 2nd method is a convenience method to pass in an SQL statement as a string which gets prepared and executed in one call. This method does not check for or return any results, hence it is only useful for database manipulation methods (i.e. ALTER, CREATE, UPDATE, DROP). To return results, see `SQLite.query` below.
6054

6155

62-
* `SQLite.query(db::SQLite.DB, sql::String, values=[])`
56+
* `SQLite.Query(db::SQLite.DB, sql::String, values=[])`
57+
58+
Constructs a `SQLite.Query` object by executing the SQL query `sql` against the sqlite database `db` and querying
59+
the columns names and types of the result set, if any.
6360

64-
An SQL statement `sql` is prepared, executed in the context of `db`, and results, if any, are returned. The return value is a `Data.Table` by default from the `DataStreams.jl` package. The `Data.Table` has a field `.data` which is a `Vector{NullableVector}` which holds the columns of data returned from the `sql` statement.
61+
Will bind `values` to any parameters in `sql`.
62+
`stricttypes=false` will remove strict column typing in the result set, making each column effectively `Vector{Any}`; in sqlite, individual
63+
column values are only loosely associated with declared column types, and instead each carry their own type information. This can lead to
64+
type errors when trying to query columns when a single type is expected.
65+
`nullable` controls whether `null` (`missing` in Julia) values are expected in a column.
6566

66-
The values in `values` are used in parameter binding (see `bind!` above). If your statement uses nameless parameters `values` must be a `Vector` of the values you wish to bind to your statment. If your statement uses named parameters `values` must be a Dict where the keys are of type `Symbol`. The key must match an identifier name in the statement (the name **should not** include the ':', '@' or '$' prefix).
67+
An `SQLite.Query` object will iterate NamedTuple rows by default, and also supports the Tables.jl interface for integrating with
68+
any other Tables.jl implementation. Due note however that iterating an sqlite result set is a forward-once-only operation. If you need
69+
to iterate over an `SQLite.Query` multiple times, but can't store the iterated NamedTuples, call `SQLite.reset!(q::SQLite.Query)` to
70+
re-execute the query and position the iterator back at the begining of the result set.
6771

6872

6973
* `SQLite.drop!(db::SQLite.DB,table::String;ifexists::Bool=false)`
@@ -135,15 +139,15 @@ julia> db = SQLite.DB("Chinook_Sqlite.sqlite")
135139

136140
julia> # using SQLite's in-built syntax
137141

138-
julia> SQLite.query(db, "SELECT FirstName, LastName FROM Employee WHERE LastName REGEXP 'e(?=a)'")
142+
julia> SQLite.Query(db, "SELECT FirstName, LastName FROM Employee WHERE LastName REGEXP 'e(?=a)'") |> DataFrame
139143
1x2 ResultSet
140144
| Row | "FirstName" | "LastName" |
141145
|-----|-------------|------------|
142146
| 1 | "Jane" | "Peacock" |
143147

144148
julia> # explicitly calling the regexp() function
145149

146-
julia> SQLite.query(db, "SELECT * FROM Genre WHERE regexp('e[trs]', Name)")
150+
julia> SQLite.Query(db, "SELECT * FROM Genre WHERE regexp('e[trs]', Name)") |> DataFrame
147151
6x2 ResultSet
148152
| Row | "GenreId" | "Name" |
149153
|-----|-----------|----------------------|
@@ -156,20 +160,20 @@ julia> SQLite.query(db, "SELECT * FROM Genre WHERE regexp('e[trs]', Name)")
156160

157161
julia> # you can even do strange things like this if you really want
158162

159-
julia> SQLite.query(db, "SELECT * FROM Genre ORDER BY GenreId LIMIT 2")
163+
julia> SQLite.Query(db, "SELECT * FROM Genre ORDER BY GenreId LIMIT 2") |> DataFrame
160164
2x2 ResultSet
161165
| Row | "GenreId" | "Name" |
162166
|-----|-----------|--------|
163167
| 1 | 1 | "Rock" |
164168
| 2 | 2 | "Jazz" |
165169

166-
julia> SQLite.query(db, "INSERT INTO Genre VALUES (regexp('^word', 'this is a string'), 'My Genre')")
170+
julia> SQLite.Query(db, "INSERT INTO Genre VALUES (regexp('^word', 'this is a string'), 'My Genre')") |> DataFrame
167171
1x1 ResultSet
168172
| Row | "Rows Affected" |
169173
|-----|-----------------|
170174
| 1 | 0 |
171175

172-
julia> SQLite.query(db, "SELECT * FROM Genre ORDER BY GenreId LIMIT 2")
176+
julia> SQLite.Query(db, "SELECT * FROM Genre ORDER BY GenreId LIMIT 2") |> DataFrame
173177
2x2 ResultSet
174178
| Row | "GenreId" | "Name" |
175179
|-----|-----------|------------|
@@ -180,13 +184,13 @@ julia> SQLite.query(db, "SELECT * FROM Genre ORDER BY GenreId LIMIT 2")
180184
Due to the heavy use of escape characters you may run into problems where julia parses out some backslashes in your query, for example `"\y"` simply becomes `"y"`. For example the following two queries are identical
181185

182186
```julia
183-
julia> SQLite.query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-\d'")
187+
julia> SQLite.Query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-\d'") |> DataFrame
184188
1x1 ResultSet
185189
| Row | "Rows Affected" |
186190
|-----|-----------------|
187191
| 1 | 0 |
188192

189-
julia> SQLite.query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-d'")
193+
julia> SQLite.Query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-d'") |> DataFrame
190194
1x1 ResultSet
191195
| Row | "Rows Affected" |
192196
|-----|-----------------|
@@ -198,15 +202,15 @@ This can be avoided in two ways. You can either escape each backslash yourself o
198202
```julia
199203
julia> # manually escaping backslashes
200204

201-
julia> SQLite.query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-\\d'")
205+
julia> SQLite.Query(db, "SELECT * FROM MediaType WHERE Name REGEXP '-\\d'") |> DataFrame
202206
1x2 ResultSet
203207
| Row | "MediaTypeId" | "Name" |
204208
|-----|---------------|-------------------------------|
205209
| 1 | 3 | "Protected MPEG-4 video file" |
206210

207211
julia> # using sr"..."
208212

209-
julia> SQLite.query(db, sr"SELECT * FROM MediaType WHERE Name REGEXP '-\d'")
213+
julia> SQLite.Query(db, sr"SELECT * FROM MediaType WHERE Name REGEXP '-\d'") |> DataFrame
210214
1x2 ResultSet
211215
| Row | "MediaTypeId" | "Name" |
212216
|-----|---------------|-------------------------------|

src/SQLite.jl

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,47 @@ end
202202
#int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
203203
#int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*);
204204

205+
function juliatype(handle, col)
206+
t = SQLite.sqlite3_column_decltype(handle, col)
207+
if t != C_NULL
208+
T = juliatype(unsafe_string(t))
209+
T !== Any && return T
210+
end
211+
x = SQLite.sqlite3_column_type(handle, col)
212+
if x == SQLite.SQLITE_BLOB
213+
val = SQLite.sqlitevalue(Any, handle, col)
214+
return typeof(val)
215+
else
216+
return juliatype(x)
217+
end
218+
end
219+
220+
juliatype(x::Integer) = x == SQLITE_INTEGER ? Int : x == SQLITE_FLOAT ? Float64 : x == SQLITE_TEXT ? String : Any
221+
juliatype(x::String) = x == "INTEGER" ? Int : x in ("NUMERIC","REAL") ? Float64 : x == "TEXT" ? String : Any
222+
223+
sqlitevalue(::Type{T}, handle, col) where {T <: Union{Base.BitSigned, Base.BitUnsigned}} = convert(T, sqlite3_column_int64(handle, col))
224+
const FLOAT_TYPES = Union{Float16, Float32, Float64} # exclude BigFloat
225+
sqlitevalue(::Type{T}, handle, col) where {T <: FLOAT_TYPES} = convert(T, sqlite3_column_double(handle, col))
226+
#TODO: test returning a WeakRefString instead of calling `unsafe_string`
227+
sqlitevalue(::Type{T}, handle, col) where {T <: AbstractString} = convert(T, unsafe_string(sqlite3_column_text(handle, col)))
228+
function sqlitevalue(::Type{T}, handle, col) where {T}
229+
blob = convert(Ptr{UInt8}, sqlite3_column_blob(handle, col))
230+
b = sqlite3_column_bytes(handle, col)
231+
buf = zeros(UInt8, b) # global const?
232+
unsafe_copyto!(pointer(buf), blob, b)
233+
r = sqldeserialize(buf)::T
234+
return r
235+
end
236+
237+
sqlitetype(::Type{T}) where {T<:Integer} = "INT NOT NULL"
238+
sqlitetype(::Type{T}) where {T<:Union{Missing, Integer}} = "INT"
239+
sqlitetype(::Type{T}) where {T<:AbstractFloat} = "REAL NOT NULL"
240+
sqlitetype(::Type{T}) where {T<:Union{Missing, AbstractFloat}} = "REAL"
241+
sqlitetype(::Type{T}) where {T<:AbstractString} = "TEXT NOT NULL"
242+
sqlitetype(::Type{T}) where {T<:Union{Missing, AbstractString}} = "TEXT"
243+
sqlitetype(::Type{Missing}) = "NULL"
244+
sqlitetype(x) = "BLOB"
245+
205246
"""
206247
`SQLite.execute!(stmt::SQLite.Stmt)` => `Cvoid`
207248
@@ -221,6 +262,7 @@ function execute!(stmt::Stmt)
221262
end
222263
return r
223264
end
265+
224266
function execute!(db::DB, sql::AbstractString)
225267
stmt = Stmt(db, sql)
226268
return execute!(stmt)
@@ -238,7 +280,6 @@ function esc_id end
238280
esc_id(x::AbstractString) = "\"" * replace(x, "\""=>"\"\"") * "\""
239281
esc_id(X::AbstractVector{S}) where {S <: AbstractString} = join(map(esc_id, X), ',')
240282

241-
242283
# Transaction-based commands
243284
"""
244285
`SQLite.transaction(db, mode="DEFERRED")`
@@ -264,7 +305,8 @@ function transaction(db, mode="DEFERRED")
264305
execute!(db, "SAVEPOINT $(mode);")
265306
end
266307
end
267-
function transaction(f::Function, db)
308+
309+
@inline function transaction(f::Function, db)
268310
# generate a random name for the savepoint
269311
name = string("SQLITE", Random.randstring(10))
270312
execute!(db, "PRAGMA synchronous = OFF;")
@@ -388,5 +430,27 @@ end
388430

389431
include("Source.jl")
390432
include("Sink.jl")
433+
include("tables.jl")
434+
435+
"""
436+
`SQLite.tables(db, sink=DataFrame)`
437+
438+
returns a list of tables in `db`
439+
"""
440+
tables(db::DB, sink=DataFrame) = Query(db, "SELECT name FROM sqlite_master WHERE type='table';") |> sink
441+
442+
"""
443+
`SQLite.indices(db, sink=DataFrame)`
444+
445+
returns a list of indices in `db`
446+
"""
447+
indices(db::DB, sink=DataFrame) = Query(db, "SELECT name FROM sqlite_master WHERE type='index';") |> sink
448+
449+
"""
450+
`SQLite.columns(db, table, sink=DataFrame)`
451+
452+
returns a list of columns in `table`
453+
"""
454+
columns(db::DB,table::AbstractString, sink=DataFrame) = Query(db, "PRAGMA table_info($(esc_id(table)))") |> sink
391455

392456
end # module

src/Sink.jl

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
sqlitetype(::Type{T}) where {T<:Integer} = "INT NOT NULL"
2-
sqlitetype(::Type{T}) where {T<:Union{Missing, Integer}} = "INT"
3-
sqlitetype(::Type{T}) where {T<:AbstractFloat} = "REAL NOT NULL"
4-
sqlitetype(::Type{T}) where {T<:Union{Missing, AbstractFloat}} = "REAL"
5-
sqlitetype(::Type{T}) where {T<:AbstractString} = "TEXT NOT NULL"
6-
sqlitetype(::Type{T}) where {T<:Union{Missing, AbstractString}} = "TEXT"
7-
sqlitetype(::Type{Missing}) = "NULL"
8-
sqlitetype(x) = "BLOB"
9-
101
function createtable!(db::DB, name::AbstractString, schema::Data.Schema; temp::Bool=false, ifnotexists::Bool=true)
112
rows, cols = size(schema)
123
temp = temp ? "TEMP" : ""
@@ -25,6 +16,7 @@ can optionally provide an existing SQLite table name or new name that a created
2516
`ifnotexists=false` will throw an error if `name` already exists in `db`
2617
"""
2718
function Sink(db::DB, name::AbstractString, schema::Data.Schema=Data.Schema(); temp::Bool=false, ifnotexists::Bool=true, append::Bool=false)
19+
Base.depwarn("`SQLite.Sink(db, name)` is deprecated in favor of calling `SQLite.load!(table, db, name)` where `table` can be any Tables.jl interface implementation", nothing)
2820
cols = size(Data.schema(SQLite.query(db, "pragma table_info($name)")), 1)
2921
if cols == 0
3022
createtable!(db, name, schema)
@@ -86,13 +78,22 @@ Load a Data.Source `source` into an SQLite table that will be named `tablename`
8678
`ifnotexists=false` will throw an error if `tablename` already exists in `db`
8779
"""
8880
function load(db::SQLite.DB, name, ::Type{T}, args...; append::Bool=false, transforms::Dict=Dict{Int,Function}(), kwargs...) where {T}
81+
Base.depwarn("`SQLite.load(db, name, args...)` is deprecated in favor of calling `SQLite.load!(table, db, name)` where `table` can be any Tables.jl interface implementation", nothing)
8982
sink = Data.stream!(T(args...), SQLite.Sink, db, name; append=append, transforms=transforms, kwargs...)
9083
return Data.close!(sink)
9184
end
9285
function load(db::SQLite.DB, name, source::T; append::Bool=false, transforms::Dict=Dict{Int,Function}(), kwargs...) where {T}
86+
Base.depwarn("`SQLite.load(db, name, args...)` is deprecated in favor of calling `SQLite.load!(table, db, name)` where `table` can be any Tables.jl interface implementation", nothing)
9387
sink = Data.stream!(source, SQLite.Sink, db, name; append=append, transforms=transforms, kwargs...)
9488
return Data.close!(sink)
9589
end
9690

97-
load(sink::Sink, ::Type{T}, args...; append::Bool=false, transforms::Dict=Dict{Int,Function}()) where {T} = (sink = Data.stream!(T(args...), sink; append=append, transforms=transforms); return Data.close!(sink))
98-
load(sink::Sink, source; append::Bool=false, transforms::Dict=Dict{Int,Function}()) = (sink = Data.stream!(source, sink; append=append, transforms=transforms); return Data.close!(sink))
91+
function load(sink::Sink, ::Type{T}, args...; append::Bool=false, transforms::Dict=Dict{Int,Function}()) where {T}
92+
Base.depwarn("`SQLite.load(sink::SQLite.Sink, args...)` is deprecated in favor of calling `SQLite.load!(table, db, name)` where `table` can be any Tables.jl interface implementation", nothing)
93+
(sink = Data.stream!(T(args...), sink; append=append, transforms=transforms); return Data.close!(sink))
94+
end
95+
96+
function load(sink::Sink, source; append::Bool=false, transforms::Dict=Dict{Int,Function}())
97+
Base.depwarn("`SQLite.load(sink::SQLite.Sink, args...)` is deprecated in favor of calling `SQLite.load!(table, db, name)` where `table` can be any Tables.jl interface implementation", nothing)
98+
(sink = Data.stream!(source, sink; append=append, transforms=transforms); return Data.close!(sink))
99+
end

0 commit comments

Comments
 (0)