Skip to content

Commit 30c7704

Browse files
authored
Fix #136 by implementing a MySQL.load function (#161)
1 parent 70699d4 commit 30c7704

File tree

5 files changed

+141
-4
lines changed

5 files changed

+141
-4
lines changed

.travis.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@ arch:
1515

1616
julia:
1717
- 1.0
18-
- 1.3
18+
- 1
1919
- nightly
2020

2121
env:
2222
- JULIA_PROJECT="@."
2323

24+
branches:
25+
only:
26+
- master
27+
- gh-pages # For building documentation
28+
- /^testing-.*$/ # testing branches
29+
- /^v[0-9]+\.[0-9]+\.[0-9]+$/ # version tags
30+
31+
cache:
32+
directories:
33+
- $HOME/.julia/artifacts
34+
2435
matrix:
2536
exclude:
2637
- os: osx
@@ -31,7 +42,7 @@ matrix:
3142
- julia: nightly
3243
include:
3344
- stage: "Documentation"
34-
julia: 1.3
45+
julia: 1
3546
os: linux
3647
script:
3748
- julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate(); Pkg.build("MySQL")'
@@ -51,7 +62,6 @@ before_install:
5162
5263
before_script:
5364
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then brew install mysql; brew services start mysql; fi
54-
5565

5666
notifications:
5767
email: false

src/MySQL.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ end
278278

279279
include("execute.jl")
280280
include("prepare.jl")
281+
include("load.jl")
281282

282283
"""
283284
MySQL.escape(conn::MySQL.Connection, str::String) -> String

src/api/API.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ using Dates, DecFP
44

55
if VERSION < v"1.3.0"
66

7-
# Load libmariadb from our deps.jl
7+
# Load libmariadb from our deps.jl
88
const depsjl_path = joinpath(dirname(@__FILE__), "..", "..", "deps", "deps.jl")
99
if !isfile(depsjl_path)
1010
error("MySQL not installed properly, run Pkg.build(\"MySQL\"), restart Julia and try again")

src/load.jl

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
function quoteid(str)
2+
# avoid double quoting
3+
if str[1] == '`' && str[end] == '`'
4+
return str
5+
else
6+
return string('`', str, '`')
7+
end
8+
end
9+
10+
sqltype(::Type{Union{T, Missing}}) where {T} = sqltype(T)
11+
sqltype(T) = get(SQLTYPES, T, "VARCHAR(255)")
12+
13+
const SQLTYPES = Dict{Type, String}(
14+
Int8 => "TINYINT",
15+
Int16 => "SMALLINT",
16+
Int32 => "INTEGER",
17+
Int64 => "BIGINT",
18+
UInt8 => "TINYINT UNSIGNED",
19+
UInt16 => "SMALLINT UNSIGNED",
20+
UInt32 => "INTEGER UNSIGNED",
21+
UInt64 => "BIGINT UNSIGNED",
22+
Float32 => "FLOAT",
23+
Float64 => "DOUBLE",
24+
DecFP.Dec64 => "NUMERIC(16, 6)",
25+
DecFP.Dec128 => "NUMERIC(35, 6)",
26+
Bool => "BOOL",
27+
Vector{UInt8} => "BLOB",
28+
String => "VARCHAR(255)",
29+
Date => "DATE",
30+
Time => "TIME",
31+
DateTime => "DATETIME",
32+
)
33+
34+
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")
35+
36+
function createtable(conn::Connection, nm::AbstractString, sch::Tables.Schema; debug::Bool=false, quoteidentifiers::Bool=true, createtableclause::AbstractString="CREATE TABLE", columnsuffix=Dict())
37+
names = sch.names
38+
checkdupnames(names)
39+
types = [sqltype(T) for T in sch.types]
40+
columns = (string(quoteidentifiers ? quoteid(String(names[i])) : names[i], ' ', types[i], ' ', get(columnsuffix, names[i], "")) for i = 1:length(names))
41+
debug && @info "executing create table statement: `$createtableclause $nm ($(join(columns, ", ")))`"
42+
return DBInterface.execute(conn, "$createtableclause $nm ($(join(columns, ", ")))")
43+
end
44+
45+
"""
46+
ODBC.load(table, conn, name; append=true, quoteidentifiers=true, limit=typemax(Int64), createtableclause=nothing, columnsuffix=Dict(), debug=false)
47+
table |> ODBC.load(conn, name; append=true, quoteidentifiers=true, limit=typemax(Int64), createtableclause=nothing, columnsuffix=Dict(), debug=false)
48+
49+
Attempts to take a Tables.jl source `table` and load into the database represented by `conn` with table name `name`.
50+
51+
It first detects the `Tables.Schema` of the table source and generates a `CREATE TABLE` statement
52+
with the appropriate column names and types. If no table name is provided, one will be autogenerated, like `odbcjl_xxxxx`.
53+
The `CREATE TABLE` clause can be provided manually by passing the `createtableclause` keyword argument, which
54+
would allow specifying a temporary table or `if not exists`.
55+
Column definitions can also be enhanced by providing arguments to `columnsuffix` as a `Dict` of
56+
column name (given as a `Symbol`) to a string of the enhancement that will come after name and type like
57+
`[column name] [column type] enhancements`. This allows, for example, specifying the charset of a string column
58+
by doing something like `columnsuffix=Dict(:Name => "CHARACTER SET utf8mb4")`.
59+
60+
Do note that databases vary wildly in requirements for `CREATE TABLE` and column definitions
61+
so it can be extremely difficult to load data generically. You may just need to tweak some of the provided
62+
keyword arguments, but you may also need to execute the `CREATE TABLE` and `INSERT` statements
63+
yourself. If you run into issues, you can [open an issue](https://github.com/JuliaDatabases/ODBC.jl/issues) and
64+
we can see if there's something we can do to make it easier to use this function.
65+
"""
66+
function load end
67+
68+
load(conn::Connection, table::AbstractString="mysql_"*Random.randstring(5); kw...) = x->load(x, conn, table; kw...)
69+
70+
function load(itr, conn::Connection, name::AbstractString="mysql_"*Random.randstring(5); append::Bool=true, quoteidentifiers::Bool=true, debug::Bool=false, limit::Integer=typemax(Int64), kw...)
71+
# get data
72+
rows = Tables.rows(itr)
73+
sch = Tables.schema(rows)
74+
if sch === nothing
75+
# we want to ensure we always have a schema, so materialize if needed
76+
rows = Tables.rows(columntable(rows))
77+
sch = Tables.schema(rows)
78+
end
79+
# ensure table exists
80+
if quoteidentifiers
81+
name = quoteid(name)
82+
end
83+
try
84+
createtable(conn, name, sch; quoteidentifiers=quoteidentifiers, debug=debug, kw...)
85+
catch e
86+
@warn "error creating table" (e, catch_backtrace())
87+
end
88+
if !append
89+
DBInterface.execute(conn, "DELETE FROM $name")
90+
end
91+
# start a transaction for inserting rows
92+
transaction(conn) do
93+
params = chop(repeat("?,", length(sch.names)))
94+
stmt = DBInterface.prepare(conn, "INSERT INTO $name VALUES ($params)")
95+
for (i, row) in enumerate(rows)
96+
i > limit && break
97+
debug && @info "inserting row $i; $(Tables.Row(row))"
98+
DBInterface.execute(stmt, Tables.Row(row))
99+
end
100+
end
101+
102+
return name
103+
end
104+
105+
function transaction(f::Function, conn)
106+
API.autocommit(conn.mysql, false)
107+
try
108+
f()
109+
API.commit(conn.mysql)
110+
catch
111+
API.rollback(conn.mysql)
112+
rethrow()
113+
finally
114+
API.autocommit(conn.mysql, true)
115+
end
116+
end

test/runtests.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ for i = 1:length(expected)
139139
end
140140
end
141141

142+
# MySQL.load
143+
MySQL.load(Base.structdiff(expected, NamedTuple{(:LastLogin2, :Senior,)}), conn, "Employee_copy"; limit=4, columnsuffix=Dict(:Name=>"CHARACTER SET utf8mb4"), debug=true)
144+
res = DBInterface.execute(conn, "select * from Employee_copy") |> columntable
145+
@test length(res) == 14
146+
@test length(res[1]) == 4
147+
for nm in keys(res)
148+
@test isequal(res[nm], expected[nm][1:4])
149+
end
150+
151+
142152
# now test insert/parameter binding
143153
DBInterface.execute(conn, "DELETE FROM Employee")
144154
for i = 1:length(expected)

0 commit comments

Comments
 (0)