Skip to content

Db Connection implementation: handle_execute + prepare + bind #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 73 additions & 13 deletions lib/exqlite/connection.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule Exqlite.Connection do

@doc """
@moduledoc """
This module imlements connection details as defined in DBProtocol.

Notes:
Expand All @@ -13,6 +12,7 @@ defmodule Exqlite.Connection do
alias Exqlite.Sqlite3
alias Exqlite.Error
alias Exqlite.Result
alias Exqlite.Query

defstruct [
:db
Expand Down Expand Up @@ -53,17 +53,21 @@ defmodule Exqlite.Connection do
{:ok, state}
end

###----------------------------------
### ----------------------------------
# handle_* implementations
###----------------------------------
### ----------------------------------

@impl true
def handle_prepare(query, _opts, state) do
def handle_prepare(%Query{} = query, _opts, state) do
# TODO: we may want to cache prepared queries like Myxql does
# for now we just invoke sqlite directly
case Sqlite3.prepare(state.db, query) do
{:ok, statement} -> {:ok, statement, state}
{:error, reason} -> %Error{message: reason}
prepare(query, state)
end

@impl true
def handle_execute(%Query{} = query, params, _opts, state) do
with {:ok, query, state} <- maybe_prepare(query, state) do
execute(query, params, state)
end
end

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

# TODO: handle/track SAVEPOINT
case Keyword.get(opts, :mode, :deferred) do
:immediate -> handle_transaction(:begin, "BEGIN IMMEDIATE TRANSACTION", state)
:exclusive -> handle_transaction(:begin, "BEGIN EXCLUSIVE TRANSACTION", state)
mode when mode in [nil, :deferred] -> handle_transaction(:begin, "BEGIN TRANSACTION", state)
:immediate ->
handle_transaction(:begin, "BEGIN IMMEDIATE TRANSACTION", state)

:exclusive ->
handle_transaction(:begin, "BEGIN EXCLUSIVE TRANSACTION", state)

mode when mode in [nil, :deferred] ->
handle_transaction(:begin, "BEGIN TRANSACTION", state)
end
end

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

###----------------------------------
### ----------------------------------
# Internal functions and helpers
###----------------------------------
### ----------------------------------

defp do_connect(path) do
case Sqlite3.open(path) do
Expand All @@ -123,6 +132,57 @@ defmodule Exqlite.Connection do
end
end

defp maybe_prepare(%Query{ref: ref} = query, state) when ref != nil, do: {:ok, query, state}
defp maybe_prepare(%Query{} = query, state), do: prepare(query, state)

defp prepare(%Query{statement: statement} = query, state) do
case Sqlite3.prepare(state.db, statement) do
{:ok, ref} -> {:ok, %{query | ref: ref}, state}
{:error, reason} -> {:error, %Error{message: reason}}
end
end

defp execute(%Query{} = query, params, state) do
with {:ok, query} <- bind_params(query, params, state) do
do_execute(query, state, %Result{})
end
end

defp do_execute(%Query{ref: ref} = query, state, %Result{} = result) do
case Sqlite3.step(state.db, query.ref) do
:done ->
# TODO: this query may fail, we need to properly propagate this
{:ok, columns} = Sqlite3.columns(state.db, ref)

# TODO: this may fail, we need to properly propagate this
Sqlite3.close(ref)
{:ok, %{result | columns: columns}}

{:row, row} ->
# TODO: we need something better than simply appending rows
do_execute(query, state, %{result | rows: result.rows ++ [row]})

:busy ->
{:error, %Error{message: "Database busy"}}
end
end

defp bind_params(%Query{ref: ref} = query, params, state) do
# TODO:
# - Add parameter translation to sqlite types. See e.g.
# https://github.com/elixir-sqlite/sqlitex/blob/master/lib/sqlitex/statement.ex#L274
# - Do we do anything special to distinguish the different types of
# parameters? See https://www.sqlite.org/lang_expr.html#varparam and
# https://www.sqlite.org/c3ref/bind_blob.html E.g. we can accept a map of params
# that binds values to named params. We can look up their indices via
# https://www.sqlite.org/c3ref/bind_parameter_index.html
case Sqlite3.bind(state.db, ref, params) do
:ok -> {:ok, query}
{:error, {code, reason}} -> {:error, %Error{message: "#{reason}. Code: #{code}"}}
{:error, reason} -> {:error, %Error{message: reason}}
end
end

defp handle_transaction(call, statement, state) do
case Sqlite3.execute(state.db, statement) do
:ok ->
Expand Down
55 changes: 55 additions & 0 deletions lib/exqlite/query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule Exqlite.Query do
@moduledoc """
Query struct returned from a successfully prepared query.
"""
@type t :: %__MODULE__{
statement: iodata(),
ref: reference() | nil
}

defstruct statement: nil,
ref: nil

defimpl DBConnection.Query do
def parse(query, _opts) do
query
end

def describe(query, _opts) do
query
end

def encode(%{ref: nil} = query, _params, _opts) do
raise ArgumentError, "query #{inspect(query)} has not been prepared"
end

def encode(%{num_params: nil} = query, _params, _opts) do
raise ArgumentError, "query #{inspect(query)} has not been prepared"
end

def encode(%{num_params: num_params} = query, params, _opts)
when num_params != length(params) do
message =
"expected params count: #{inspect(num_params)}, got values: #{inspect(params)}" <>
" for query: #{inspect(query)}"

raise ArgumentError, message
end

def encode(_query, params, _opts) do
# TODO. See also Connection.bind/3
params
#Protocol.encode_params(params)
end

def decode(_query, result, _opts) do
result
end
end

defimpl String.Chars do
def to_string(%{statement: statement}) do
IO.iodata_to_binary(statement)
end
end
end
2 changes: 1 addition & 1 deletion lib/exqlite/result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ defmodule Exqlite.Result do
num_rows: integer
}

defstruct command: nil, columns: nil, rows: nil, num_rows: nil
defstruct command: nil, columns: [], rows: [], num_rows: 0
end