defmodule Sqlite.DbConnection.Protocol do @moduledoc false # alias Sqlite.DbConnection.Types alias Sqlite.DbConnection.Query # import Sqlite.DbConnection.Messages # import Sqlite.DbConnection.BinaryUtils require Logger @behaviour DBConnection # IMPORTANT: This is closely modeled on Postgrex's protocol.ex file. # We strive to avoid structural differences between that file and this one. # @sock_opts [packet: :raw, mode: :binary, active: false] defstruct [db: nil, path: nil, checked_out?: false] @type state :: %__MODULE__{db: pid, path: String.t, checked_out?: false} @spec connect(Keyword.t) :: {:ok, state} | {:error, Sqlite.DbConnection.Error.t} def connect(opts) do {db_path, _opts} = Keyword.pop(opts, :database) with {:ok, db} <- Sqlitex.Server.start_link(db_path), :ok <- Sqlitex.Server.exec(db, "PRAGMA foreign_keys = ON"), {:ok, [[foreign_keys: 1]]} = Sqlitex.Server.query(db, "PRAGMA foreign_keys") do {:ok, %__MODULE__{db: db, path: db_path, checked_out?: false}} end end @spec disconnect(Exception.t, state) :: :ok def disconnect(_exc, %__MODULE__{db: db} = _state) when db != nil do GenServer.stop(db) :ok end def disconnect(_exception, _state), do: :ok @spec ping(state :: any) :: {:ok, new_state :: any} | {:disconnect, Exception.t, new_state :: any} def ping(state) do # No such thing with SQLite, so this is an intentional no-op. {:ok, state} end @spec checkout(state) :: {:ok, state} | {:disconnect, Sqlite.DbConnection.Error.t, state} # def checkout(%{checked_out?: true} = s), do: # unreachable in tests; restore if hit # {:disconnect, :already_checked_out, s} # FIXME: Proper error here def checkout(%{checked_out?: false} = s), do: {:ok, %{s | checked_out?: true}} @spec checkin(state) :: {:ok, state} | {:disconnect, Sqlite.DbConnection.Error.t, state} # def checkin(%{checked_out?: false} = s), do: # unreachable in tests; restore if hit # {:disconnect, :not_checked_out, s} # FIXME: Proper error here def checkin(%{checked_out?: true} = s), do: {:ok, %{s | checked_out?: false}} @spec handle_prepare(Sqlite.DbConnection.Query.t, Keyword.t, state) :: {:ok, Sqlite.DbConnection.Query.t, state} | {:error, ArgumentError.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} # def handle_prepare(%Query{name: @reserved_prefix <> _} = query, _, s) do # reserved_error(query, s) # end def handle_prepare(%Query{statement: statement, types: nil} = query, _opts, %__MODULE__{checked_out?: true, db: db} = s) do binary_stmt = :erlang.iolist_to_binary(statement) case Sqlitex.Server.prepare(db, binary_stmt) do {:ok, prepared_info} -> updated_query = %{query | prepared: refined_info(prepared_info)} {:ok, updated_query, s} {:error, {_sqlite_errcode, _message}} = err -> sqlite_error(err, s) end end def handle_prepare(query, _opts, s) do query_error(s, "query #{inspect query} has already been prepared") end @spec handle_execute(Sqlite.DbConnection.Query.t, list, Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error, ArgumentError.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_execute(%Query{} = query, params, opts, s) do handle_execute(query, params, :sync, opts, s) end # @spec handle_execute(Sqlite.DbConnection.Parameters.t, nil, Keyword.t, state) :: # {:ok, %{binary => binary}, state} | # {:error, Sqlite.DbConnection.Errpr.t, state} # def handle_execute(%Sqlite.DbConnection.Parameters{}, nil, _, s) do # %{parameters: parameters} = s # case Sqlite.DbConnection.Parameters.fetch(parameters) do # {:ok, parameters} -> # {:ok, parameters, s} # :error -> # {:error, %Sqlite.DbConnection.Error{message: "parameters not available"}, s} # end # end @spec handle_execute_close(Sqlite.DbConnection.Query.t, list, Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error, ArgumentError.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} # def handle_execute_close(%Query{name: @reserved_prefix <> _} = query, _, _, s) do # reserved_error(query, s) # end def handle_execute_close(query, params, opts, s) do handle_execute(query, params, :sync_close, opts, s) end @spec handle_close(Sqlite.DbConnection.Query.t, Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error, ArgumentError.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_close(_query, _opts, s) do # no-op: esqlite doesn't expose statement close. # Instead it relies on statements getting garbage collected. res = %Sqlite.DbConnection.Result{command: :close} {:ok, res, s} end @spec handle_begin(Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_begin(opts, s) do sql = case Keyword.get(opts, :mode, :transaction) do :transaction -> "BEGIN" :savepoint -> "SAVEPOINT sqlite_ecto_savepoint" end handle_transaction(sql, s) end @spec handle_commit(Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_commit(opts, s) do sql = case Keyword.get(opts, :mode, :transaction) do :transaction -> "COMMIT" :savepoint -> "RELEASE SAVEPOINT sqlite_ecto_savepoint" end handle_transaction(sql, s) end @spec handle_rollback(Keyword.t, state) :: {:ok, Sqlite.DbConnection.Result.t, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_rollback(opts, s) do sql = case Keyword.get(opts, :mode, :transaction) do :transaction -> "ROLLBACK" :savepoint -> "ROLLBACK TO SAVEPOINT sqlite_ecto_savepoint" end handle_transaction(sql, s) end # @spec handle_simple(String.t, Keyword.t, state) :: # {:ok, Sqlite.DbConnection.Result.t, state} | # {:error | :disconnect, Sqlite.DbConnection.Error.t, state} # def handle_simple(statement, opts, %{buffer: buffer} = s) do # status = %{notify: notify(opts), sync: :sync} # simple_send(%{s | buffer: nil}, status, statement, buffer) # end @spec handle_info(any, state) :: {:ok, state} | {:error | :disconnect, Sqlite.DbConnection.Error.t, state} def handle_info(msg, s) do # Not expecting any messages, but DbConnection behavior interface requires # that we handle this. Logger.info(fn() -> [inspect(__MODULE__), ?\s, inspect(self()), " received unexpected message: " | inspect(msg)] end) {:ok, s} end ## prepare defp refined_info(prepared_info) do types = prepared_info.types |> Enum.map(&maybe_atom_to_lc_string/1) |> Enum.to_list prepared_info |> Map.delete(:columns) |> Map.put(:column_names, atoms_to_strings(prepared_info.columns)) |> Map.put(:types, types) end defp atoms_to_strings(nil), do: nil defp atoms_to_strings(list), do: Enum.map(list, &maybe_atom_to_string/1) defp maybe_atom_to_string(nil), do: nil defp maybe_atom_to_string(item), do: to_string(item) defp maybe_atom_to_lc_string(nil), do: nil defp maybe_atom_to_lc_string(item), do: item |> to_string |> String.downcase ## execute defp handle_execute(%Query{statement: sql}, params, _sync, _opts, s) do # Note that we rely on Sqlitex.Server to cache the prepared statement, # so we can simply refer to the original SQL statement here. case run_stmt(sql, params, s) do {:ok, result} -> {:ok, result, s} other -> other end end defp query_error(s, msg) do {:error, ArgumentError.exception(msg), s} end defp sqlite_error({:error, {sqlite_errcode, message}}, s) do {:error, %Sqlite.DbConnection.Error{sqlite: %{code: sqlite_errcode}, message: to_string(message)}, s} end defp run_stmt(query, params, s) do opts = [decode: :manual, types: true, bind: params] command = command_from_sql(query) case query_rows(s.db, to_string(query), opts) do {:ok, %{rows: raw_rows, columns: raw_column_names}} -> {rows, num_rows, column_names} = case {raw_rows, raw_column_names} do {_, []} -> {nil, get_changes_count(s.db, command), nil} _ -> {raw_rows, length(raw_rows), raw_column_names} end {:ok, %Sqlite.DbConnection.Result{rows: rows, num_rows: num_rows, columns: atoms_to_strings(column_names), command: command}} {:error, {_sqlite_errcode, _message}} = err -> sqlite_error(err, s) {:error, :args_wrong_length} -> {:error, %ArgumentError{message: "parameters must match number of placeholders in query"}, s} end end defp get_changes_count(db, command) when command in [:insert, :update, :delete] do # TODO: This statement should be cached. {:ok, %{rows: [[changes_count]]}} = Sqlitex.Server.query_rows(db, "SELECT changes()") changes_count end defp get_changes_count(_db, _command), do: 1 defp command_from_sql(sql) do sql |> :erlang.iolist_to_binary |> String.downcase |> String.split(" ", parts: 3) |> command_from_words end defp command_from_words([verb, subject, _]) when verb == "alter" or verb == "create" or verb == "drop", do: String.to_atom("#{verb}_#{subject}") defp command_from_words(words) when is_list(words), do: String.to_atom(List.first(words)) # defp reserved_error(query, s) do # err = ArgumentError.exception("query #{inspect query} uses reserved name") # {:error, err, s} # end ## transaction defp handle_transaction(stmt, s) do case query_rows(s.db, stmt, into: :raw_list) do {:ok, _rows} -> command = command_from_sql(stmt) result = %Sqlite.DbConnection.Result{rows: nil, num_rows: nil, columns: nil, command: command} {:ok, result, s} {:error, {_sqlite_errcode, _message}} = err -> sqlite_error(err, s) end end defp query_rows(db, stmt, opts) do try do Sqlitex.Server.query_rows(db, stmt, opts) catch :exit, _ -> {:raise, %Sqlite.DbConnection.Error{message: "Disconnected"}} end end ## Cursors (not supported) @type query :: any @type params :: any @type result :: any @type cursor :: any @spec handle_declare(query, params, opts :: Keyword.t, state :: any) :: {:ok, cursor, new_state :: any} | {:error | :disconnect, Exception.t, new_state :: any} def handle_declare(_query, _params, _opts, _state) do raise "Cursors not supported in SQLite" end @spec handle_first(query, cursor, opts :: Keyword.t, state :: any) :: {:ok | :deallocate, result, new_state :: any} | {:error | :disconnect, Exception.t, new_state :: any} def handle_first(_query, _cursor, _opts, _state) do raise "Cursors not supported in SQLite" end @spec handle_next(query, cursor, opts :: Keyword.t, state :: any) :: {:ok | :deallocate, result, new_state :: any} | {:error | :disconnect, Exception.t, new_state :: any} def handle_next(_query, _cursor, _opts, _state) do raise "Cursors not supported in SQLite" end @spec handle_deallocate(query, cursor, opts :: Keyword.t, state :: any) :: {:ok, result, new_state :: any} | {:error | :disconnect, Exception.t, new_state :: any} def handle_deallocate(_query, _cursor, _opts, _state) do raise "Cursors not supported in SQLite" end end