Skip to content
This repository was archived by the owner on Dec 17, 2018. It is now read-only.

Commit 90c4f28

Browse files
connor rigbyConnorRigby
authored andcommitted
[WIP] start implementing convient Elixir api
1 parent 40845e1 commit 90c4f28

File tree

13 files changed

+400
-9
lines changed

13 files changed

+400
-9
lines changed

coveralls.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"skip_files": [
3-
"lib/esqlite3_nif.ex",
4-
"lib/esqlite_erlang_def.ex",
5-
"erl_test/"
6-
]
2+
"skip_files": [
3+
"lib/esqlite3/esqlite3_nif.ex",
4+
"lib/esqlite3/esqlite_erlang_def.ex",
5+
"erl_test/"
6+
]
77
}

lib/sqlite.ex

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
defmodule Sqlite do
2+
@moduledoc """
3+
SQLite3 driver for Elixir.
4+
"""
5+
6+
alias Sqlite.Query
7+
8+
defmodule Connection do
9+
@moduledoc false
10+
defstruct [
11+
:pid,
12+
:ref,
13+
:database
14+
]
15+
16+
@typedoc false
17+
@type t :: %__MODULE__{
18+
pid: GenServer.server(),
19+
ref: reference,
20+
database: Esqlite3.filename()
21+
}
22+
23+
defimpl Inspect, for: __MODULE__ do
24+
def inspect(%{ref: ref}, _opts) when is_reference(ref) do
25+
String.replace(inspect(ref), "Reference", "Sqlite3")
26+
end
27+
end
28+
end
29+
30+
@typedoc "Connection identifier for your Sqlite instance."
31+
@opaque conn :: Connection.t()
32+
33+
@doc """
34+
Start the connection process and connect to sqlite.
35+
## Options
36+
* `:database` -> Databse uri.
37+
## Examples
38+
iex> {:ok, pid} = Sqlite.open(database: "sqlite.db")
39+
{:ok, #PID<0.69.0>}
40+
iex> {:ok, pid} = Sqlite.open(database: ":memory:")
41+
{:ok, #PID<0.69.0>}
42+
"""
43+
@spec open(Keyword.t(), GenServer.options()) :: {:ok, conn} | {:error, term}
44+
def open(opts, gen_server_opts \\ []) when is_list(opts) do
45+
opts = Sqlite.Utils.default_opts(opts)
46+
47+
case GenServer.start_link(Sqlite.Server, opts, gen_server_opts) do
48+
{:ok, pid} ->
49+
conn =
50+
struct(
51+
Connection,
52+
pid: pid,
53+
database: opts[:database],
54+
ref: make_ref()
55+
)
56+
57+
{:ok, conn}
58+
{:error, reason} -> {:error, reason}
59+
end
60+
end
61+
62+
@doc """
63+
Runs an (extended) query and returns the result as `{:ok, %Sqlite.Result{}}`
64+
or `{:error, %Sqlite.Error{}}` if there was a database error. Parameters can
65+
be set in the query as `$1` embedded in the query string. Parameters are given
66+
as a list of elixir values. See the README for information on how Sqlite
67+
encodes and decodes Elixir values by default. See `Sqlite.Result` for the
68+
result data.
69+
## Examples
70+
Sqlite.query(conn, "CREATE TABLE posts (id serial, title text)", [])
71+
Sqlite.query(conn, "INSERT INTO posts (title) VALUES ('my title')", [])
72+
Sqlite.query(conn, "SELECT title FROM posts", [])
73+
Sqlite.query(conn, "SELECT id FROM posts WHERE title like $1", ["%my%"])
74+
Sqlite.query(conn, "COPY posts TO STDOUT", [])
75+
"""
76+
@spec query(conn, iodata, list, Keyword.t()) ::
77+
{:ok, Sqlite.Result.t()} | {:error, Sqlite.Error.t()}
78+
def query(conn, statement, params, opts \\ []) do
79+
query = %Query{name: "", statement: statement}
80+
opts = opts |> defaults()
81+
82+
GenServer.call(conn.pid, {:query, query, params, opts}, call_timeout(opts))
83+
|> case do
84+
{:ok, %Sqlite.Result{}} = ok -> ok
85+
{:error, %Sqlite.Error{}} = ok -> ok
86+
err -> unexpected_response(err, :query)
87+
end
88+
end
89+
90+
@doc """
91+
Runs an (extended) query and returns the result or raises `Sqlite.Error` if
92+
there was an error. See `query/3`.
93+
"""
94+
@spec query!(conn, iodata, list, Keyword.t()) :: Sqlite.Result.t()
95+
def query!(conn, statement, params, opts \\ []) do
96+
case query(conn, statement, params, opts) do
97+
{:ok, result} -> result
98+
{:error, reason} -> raise Sqlite.Error, %{reason: reason}
99+
end
100+
end
101+
102+
@doc """
103+
Prepares an (extended) query and returns the result as
104+
`{:ok, %Sqlite.Query{}}` or `{:error, %Sqlite.Error{}}` if there was an
105+
error. Parameters can be set in the query as `$1` embedded in the query
106+
string. To execute the query call `execute/4`. To close the prepared query
107+
call `close/3`. See `Sqlite.Query` for the query data.
108+
109+
## Examples
110+
Sqlite.prepare(conn, "", "CREATE TABLE posts (id serial, title text)")
111+
"""
112+
@spec prepare(conn, iodata, iodata, Keyword.t()) ::
113+
{:ok, Sqlite.Query.t()} | {:error, Sqlite.Error.t()}
114+
def prepare(conn, name, statement, opts \\ []) do
115+
query = %Query{name: name, statement: statement}
116+
opts = opts |> defaults()
117+
118+
GenServer.call(conn.pid, {:prepare, query, opts}, call_timeout(opts))
119+
|> case do
120+
{:ok, %Sqlite.Query{}} = ok -> ok
121+
{:error, %Sqlite.Error{}} = ok -> ok
122+
err -> unexpected_response(err, :prepare)
123+
end
124+
end
125+
126+
@doc """
127+
Prepares an (extended) query and returns the prepared query or raises
128+
`Sqlite.Error` if there was an error. See `prepare/4`.
129+
"""
130+
@spec prepare!(conn, iodata, iodata, Keyword.t()) :: Sqlite.Query.t()
131+
def prepare!(conn, name, statement, opts \\ []) do
132+
case prepare(conn, name, statement, opts) do
133+
{:ok, result} -> result
134+
{:error, reason} -> raise Sqlite.Error, %{reason: reason}
135+
end
136+
end
137+
138+
@doc """
139+
Runs an (extended) prepared query and returns the result as
140+
`{:ok, %Sqlite.Result{}}` or `{:error, %Sqlite.Error{}}` if there was an
141+
error. Parameters are given as part of the prepared query, `%Sqlite.Query{}`.
142+
See the README for information on how Sqlite encodes and decodes Elixir
143+
values by default. See `Sqlite.Query` for the query data and
144+
`Sqlite.Result` for the result data.
145+
146+
## Examples
147+
query = Sqlite.prepare!(conn, "", "CREATE TABLE posts (id serial, title text)")
148+
Sqlite.execute(conn, query, [])
149+
query = Sqlite.prepare!(conn, "", "SELECT id FROM posts WHERE title like $1")
150+
Sqlite.execute(conn, query, ["%my%"])
151+
"""
152+
@spec execute(conn, Sqlite.Query.t(), list, Keyword.t()) ::
153+
{:ok, Sqlite.Result.t()} | {:error, Sqlite.Error.t()}
154+
def execute(conn, query, params, opts \\ []) do
155+
opts = defaults(opts)
156+
157+
GenServer.call(conn.pid, {:execute, query, params, opts}, call_timeout(opts))
158+
|> case do
159+
{:ok, %Sqlite.Result{}} = ok -> ok
160+
{:error, %Sqlite.Error{}} = ok -> ok
161+
err -> unexpected_response(err, :execute)
162+
end
163+
end
164+
165+
@doc """
166+
Runs an (extended) prepared query and returns the result or raises
167+
`Sqlite.Error` if there was an error. See `execute/4`.
168+
"""
169+
@spec execute!(conn, Sqlite.Query.t(), list, Keyword.t()) :: Sqlite.Result.t()
170+
def execute!(conn, query, params, opts \\ []) do
171+
case execute(conn, query, params, opts) do
172+
{:ok, result} -> result
173+
{:error, reason} -> raise Sqlite.Error, %{reason: reason}
174+
end
175+
end
176+
177+
@doc """
178+
Closes an (extended) prepared query and returns `:ok` or
179+
`{:error, %Sqlite.Error{}}` if there was an error. Closing a query releases
180+
any resources held by sqlite3 for a prepared query with that name. See
181+
`Sqlite.Query` for the query data.
182+
183+
## Examples
184+
query = Sqlite.prepare!(conn, "", "CREATE TABLE posts (id serial, title text)")
185+
Sqlite.close(conn, query)
186+
"""
187+
@spec close(conn, Sqlite.Query.t(), Keyword.t()) :: :ok | {:error, Sqlite.Error.t()}
188+
189+
def close(conn, query \\ nil, opts \\ [])
190+
def close(conn, query, opts) do
191+
opts = defaults(opts)
192+
193+
do_close = fn(conn, opts) ->
194+
GenServer.call(conn.pid, {:close, opts}, call_timeout(opts))
195+
|> case do
196+
:ok -> :ok
197+
{:error, %Sqlite.Error{}} = ok -> ok
198+
err -> unexpected_response(err, :close)
199+
end
200+
end
201+
202+
if query do
203+
case execute(query, [], opts) do
204+
{:error, reason} -> {:error, reason}
205+
{:ok, _result} -> do_close.(conn, opts)
206+
end
207+
else
208+
do_close.(conn, opts)
209+
end
210+
end
211+
212+
@doc """
213+
Closes an (extended) prepared query and returns `:ok` or raises
214+
`Sqlite.Error` if there was an error. See `close/3`.
215+
"""
216+
@spec close!(conn, Sqlite.Query.t(), Keyword.t()) :: :ok
217+
def close!(conn, query \\ nil, opts \\ []) do
218+
case close(conn, query, opts) do
219+
:ok -> :ok
220+
{:error, reason} -> raise Sqlite.Error, %{reason: reason}
221+
end
222+
end
223+
224+
@spec call_timeout(Keyword.t()) :: timeout
225+
defp call_timeout(opts) do
226+
case Keyword.fetch!(opts, :timeout) do
227+
number when is_integer(number) -> number + 100
228+
other -> other
229+
end
230+
end
231+
232+
@spec defaults(Keyword.t()) :: Keyword.t()
233+
defp defaults(opts) do
234+
defaults = [
235+
timeout: Application.get_env(:esqlite, :default_timeout, 5000)
236+
]
237+
238+
Keyword.merge(defaults, opts)
239+
end
240+
241+
defp unexpected_response(err, fun) do
242+
raise "Unexpected response to #{fun}: #{inspect(err)}"
243+
end
244+
end

lib/sqlite/error.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule Sqlite.Error do
2+
@moduledoc false
3+
4+
defexception [:message]
5+
6+
@typedoc "Various SQLite error."
7+
@type t :: %Sqlite.Error{}
8+
9+
def exception(%{reason: message}) do
10+
%Sqlite.Error{message: message}
11+
end
12+
13+
def message(e) do
14+
e.message
15+
end
16+
end

lib/sqlite/query.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Sqlite.Query do
2+
@moduledoc "This module handles creation of SQL queries."
3+
4+
defstruct [:name, :statement]
5+
6+
@typedoc "Sqlite Query for execution."
7+
@type t :: %__MODULE__{
8+
name: String.t(),
9+
statement: iodata
10+
}
11+
end

lib/sqlite/result.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule Sqlite.Result do
2+
@moduledoc "Results of a Query."
3+
4+
defstruct [
5+
:columns,
6+
:rows,
7+
:num_rows
8+
]
9+
10+
@type t :: %Sqlite.Result{
11+
columns: [String.t()],
12+
rows: [[term] | binary],
13+
num_rows: integer
14+
}
15+
end

lib/sqlite/server.ex

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
defmodule Sqlite.Server do
2+
@moduledoc "GenServer implementation for Sqlite."
3+
use GenServer
4+
# alias Sqlite.{Query, Error, Result}
5+
6+
defmodule State do
7+
@moduledoc false
8+
defstruct [:database, :filename]
9+
@typedoc false
10+
@type t :: %__MODULE__{
11+
database: Esqlite3.connection | :closed,
12+
filename: Esqlite3.filename,
13+
}
14+
end
15+
16+
@impl GenServer
17+
def init(opts) do
18+
filename = Keyword.fetch!(opts, :database)
19+
timeout = Keyword.fetch!(opts, :timeout)
20+
case Esqlite3.open(filename, timeout) do
21+
{:ok, db} ->
22+
{:ok, struct(State, [database: db, filename: filename])}
23+
err -> {:stop, err}
24+
end
25+
end
26+
27+
@impl GenServer
28+
def terminate(_, state) do
29+
unless state.database == :closed do
30+
:ok = Esqlite3.close(state.database)
31+
end
32+
:ok
33+
end
34+
35+
@impl GenServer
36+
def handle_call({:query, query, params, _opts}, _from, state) do
37+
Esqlite3.q(query.statement, params, state.database) |> IO.inspect
38+
{:reply, {:error, %Sqlite.Error{message: "Not implemented"}}, state}
39+
end
40+
41+
def handle_call({:prepare, query, opts}, _from, state) do
42+
Esqlite3.prepare(query.statement, state.database, opts[:timeout]) |> IO.inspect
43+
{:reply, {:error, %Sqlite.Error{message: "Not implemented"}}, state}
44+
end
45+
46+
def handle_call({:execute, query, params, opts}, _from, state) do
47+
Esqlite3.exec(query.statement, params, state.database, opts[:timeout]) |> IO.inspect
48+
{:reply, {:error, %Sqlite.Error{message: "Not implemented"}}, state}
49+
end
50+
51+
def handle_call({:close, _opts}, _from, state) do
52+
case Esqlite3.close(state.database) do
53+
:ok -> {:stop, :normal, :ok, %{state | database: :closed}}
54+
{:error, reason} -> {:stop, reason, error(reason, state), state}
55+
end
56+
end
57+
58+
defp error(reason, _state), do: %Sqlite.Error{message: reason}
59+
60+
end

lib/sqlite/utils.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule Sqlite.Utils do
2+
@moduledoc false
3+
4+
@spec default_opts(Keyword.t()) :: Keyword.t()
5+
def default_opts(opts) do
6+
Keyword.merge([timeout: Application.get_env(:esqlite, :default_timeout, 5000)], opts)
7+
end
8+
end

0 commit comments

Comments
 (0)