Skip to content

Commit cec51fa

Browse files
authored
Merge pull request #3 from dmitriid/db-connection-continue
Db Connection implementation: handle_execute + prepare + bind
2 parents 83ef224 + 1242c1e commit cec51fa

File tree

3 files changed

+129
-14
lines changed

3 files changed

+129
-14
lines changed

lib/exqlite/connection.ex

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
defmodule Exqlite.Connection do
2-
3-
@doc """
2+
@moduledoc """
43
This module imlements connection details as defined in DBProtocol.
54
65
Notes:
@@ -13,6 +12,7 @@ defmodule Exqlite.Connection do
1312
alias Exqlite.Sqlite3
1413
alias Exqlite.Error
1514
alias Exqlite.Result
15+
alias Exqlite.Query
1616

1717
defstruct [
1818
:db
@@ -53,17 +53,21 @@ defmodule Exqlite.Connection do
5353
{:ok, state}
5454
end
5555

56-
###----------------------------------
56+
### ----------------------------------
5757
# handle_* implementations
58-
###----------------------------------
58+
### ----------------------------------
5959

6060
@impl true
61-
def handle_prepare(query, _opts, state) do
61+
def handle_prepare(%Query{} = query, _opts, state) do
6262
# TODO: we may want to cache prepared queries like Myxql does
6363
# for now we just invoke sqlite directly
64-
case Sqlite3.prepare(state.db, query) do
65-
{:ok, statement} -> {:ok, statement, state}
66-
{:error, reason} -> %Error{message: reason}
64+
prepare(query, state)
65+
end
66+
67+
@impl true
68+
def handle_execute(%Query{} = query, params, _opts, state) do
69+
with {:ok, query, state} <- maybe_prepare(query, state) do
70+
execute(query, params, state)
6771
end
6872
end
6973

@@ -82,9 +86,14 @@ defmodule Exqlite.Connection do
8286

8387
# TODO: handle/track SAVEPOINT
8488
case Keyword.get(opts, :mode, :deferred) do
85-
:immediate -> handle_transaction(:begin, "BEGIN IMMEDIATE TRANSACTION", state)
86-
:exclusive -> handle_transaction(:begin, "BEGIN EXCLUSIVE TRANSACTION", state)
87-
mode when mode in [nil, :deferred] -> handle_transaction(:begin, "BEGIN TRANSACTION", state)
89+
:immediate ->
90+
handle_transaction(:begin, "BEGIN IMMEDIATE TRANSACTION", state)
91+
92+
:exclusive ->
93+
handle_transaction(:begin, "BEGIN EXCLUSIVE TRANSACTION", state)
94+
95+
mode when mode in [nil, :deferred] ->
96+
handle_transaction(:begin, "BEGIN TRANSACTION", state)
8897
end
8998
end
9099

@@ -105,9 +114,9 @@ defmodule Exqlite.Connection do
105114
handle_transaction(:rollback, "ROLLBACK", state)
106115
end
107116

108-
###----------------------------------
117+
### ----------------------------------
109118
# Internal functions and helpers
110-
###----------------------------------
119+
### ----------------------------------
111120

112121
defp do_connect(path) do
113122
case Sqlite3.open(path) do
@@ -123,6 +132,57 @@ defmodule Exqlite.Connection do
123132
end
124133
end
125134

135+
defp maybe_prepare(%Query{ref: ref} = query, state) when ref != nil, do: {:ok, query, state}
136+
defp maybe_prepare(%Query{} = query, state), do: prepare(query, state)
137+
138+
defp prepare(%Query{statement: statement} = query, state) do
139+
case Sqlite3.prepare(state.db, statement) do
140+
{:ok, ref} -> {:ok, %{query | ref: ref}, state}
141+
{:error, reason} -> {:error, %Error{message: reason}}
142+
end
143+
end
144+
145+
defp execute(%Query{} = query, params, state) do
146+
with {:ok, query} <- bind_params(query, params, state) do
147+
do_execute(query, state, %Result{})
148+
end
149+
end
150+
151+
defp do_execute(%Query{ref: ref} = query, state, %Result{} = result) do
152+
case Sqlite3.step(state.db, query.ref) do
153+
:done ->
154+
# TODO: this query may fail, we need to properly propagate this
155+
{:ok, columns} = Sqlite3.columns(state.db, ref)
156+
157+
# TODO: this may fail, we need to properly propagate this
158+
Sqlite3.close(ref)
159+
{:ok, %{result | columns: columns}}
160+
161+
{:row, row} ->
162+
# TODO: we need something better than simply appending rows
163+
do_execute(query, state, %{result | rows: result.rows ++ [row]})
164+
165+
:busy ->
166+
{:error, %Error{message: "Database busy"}}
167+
end
168+
end
169+
170+
defp bind_params(%Query{ref: ref} = query, params, state) do
171+
# TODO:
172+
# - Add parameter translation to sqlite types. See e.g.
173+
# https://github.com/elixir-sqlite/sqlitex/blob/master/lib/sqlitex/statement.ex#L274
174+
# - Do we do anything special to distinguish the different types of
175+
# parameters? See https://www.sqlite.org/lang_expr.html#varparam and
176+
# https://www.sqlite.org/c3ref/bind_blob.html E.g. we can accept a map of params
177+
# that binds values to named params. We can look up their indices via
178+
# https://www.sqlite.org/c3ref/bind_parameter_index.html
179+
case Sqlite3.bind(state.db, ref, params) do
180+
:ok -> {:ok, query}
181+
{:error, {code, reason}} -> {:error, %Error{message: "#{reason}. Code: #{code}"}}
182+
{:error, reason} -> {:error, %Error{message: reason}}
183+
end
184+
end
185+
126186
defp handle_transaction(call, statement, state) do
127187
case Sqlite3.execute(state.db, statement) do
128188
:ok ->

lib/exqlite/query.ex

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defmodule Exqlite.Query do
2+
@moduledoc """
3+
Query struct returned from a successfully prepared query.
4+
"""
5+
@type t :: %__MODULE__{
6+
statement: iodata(),
7+
ref: reference() | nil
8+
}
9+
10+
defstruct statement: nil,
11+
ref: nil
12+
13+
defimpl DBConnection.Query do
14+
def parse(query, _opts) do
15+
query
16+
end
17+
18+
def describe(query, _opts) do
19+
query
20+
end
21+
22+
def encode(%{ref: nil} = query, _params, _opts) do
23+
raise ArgumentError, "query #{inspect(query)} has not been prepared"
24+
end
25+
26+
def encode(%{num_params: nil} = query, _params, _opts) do
27+
raise ArgumentError, "query #{inspect(query)} has not been prepared"
28+
end
29+
30+
def encode(%{num_params: num_params} = query, params, _opts)
31+
when num_params != length(params) do
32+
message =
33+
"expected params count: #{inspect(num_params)}, got values: #{inspect(params)}" <>
34+
" for query: #{inspect(query)}"
35+
36+
raise ArgumentError, message
37+
end
38+
39+
def encode(_query, params, _opts) do
40+
# TODO. See also Connection.bind/3
41+
params
42+
#Protocol.encode_params(params)
43+
end
44+
45+
def decode(_query, result, _opts) do
46+
result
47+
end
48+
end
49+
50+
defimpl String.Chars do
51+
def to_string(%{statement: statement}) do
52+
IO.iodata_to_binary(statement)
53+
end
54+
end
55+
end

lib/exqlite/result.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ defmodule Exqlite.Result do
66
num_rows: integer
77
}
88

9-
defstruct command: nil, columns: nil, rows: nil, num_rows: nil
9+
defstruct command: nil, columns: [], rows: [], num_rows: 0
1010
end

0 commit comments

Comments
 (0)