Skip to content

Commit bee5f37

Browse files
authored
Allow query result decoding to happen later in process. (#10)
* Introduce Sqlite.Ecto.Result struct. * Allow query result decoding to happen later in process. Partial implementation of elixir-ecto/postgrex#108.
1 parent 3fc32d0 commit bee5f37

File tree

2 files changed

+98
-41
lines changed

2 files changed

+98
-41
lines changed

lib/sqlite_ecto/query.ex

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule Sqlite.Ecto.Query do
44
import Sqlite.Ecto.Transaction, only: [with_savepoint: 2]
55
import Sqlite.Ecto.Util
66

7+
alias Sqlite.Ecto.Result
8+
79
# ALTER TABLE queries:
810
def query(pid, <<"ALTER TABLE ", _ :: binary>>=sql, params, opts) do
911
sql
@@ -208,56 +210,30 @@ defmodule Sqlite.Ecto.Query do
208210
# busy error means another process is writing to the database; try again
209211
{:error, {:busy, _}} -> do_query(pid, sql, params, opts)
210212
{:error, msg} -> {:error, Sqlite.Ecto.Error.exception(msg)}
211-
{:ok, rows} when is_list(rows) -> query_result(pid, sql, rows)
213+
{:ok, rows} when is_list(rows) -> query_result(pid, sql, rows, opts)
212214
end
213215
end
214216

215217
# If this is an INSERT, UPDATE, or DELETE, then return the number of changed
216218
# rows. Otherwise (e.g. for SELECT) return the queried column values.
217-
defp query_result(pid, <<"INSERT ", _::binary>>, []), do: changes_result(pid)
218-
defp query_result(pid, <<"UPDATE ", _::binary>>, []), do: changes_result(pid)
219-
defp query_result(pid, <<"DELETE ", _::binary>>, []), do: changes_result(pid)
220-
defp query_result(_pid, _sql, rows) do
221-
rows = Enum.map(rows, fn row ->
222-
row
223-
|> cast_any_datetimes
224-
|> Keyword.values
225-
|> Enum.map(fn
226-
{:blob, binary} -> binary
227-
other -> other
228-
end)
229-
end)
230-
{:ok, %{rows: rows, num_rows: length(rows)}}
219+
defp query_result(pid, <<"INSERT ", _::binary>>, [], _opts), do: changes_result(pid)
220+
defp query_result(pid, <<"UPDATE ", _::binary>>, [], _opts), do: changes_result(pid)
221+
defp query_result(pid, <<"DELETE ", _::binary>>, [], _opts), do: changes_result(pid)
222+
defp query_result(_pid, _sql, rows, opts) do
223+
{:ok, decode(rows, Keyword.fetch(opts, :decode))}
231224
end
232225

233-
defp changes_result(pid) do
234-
{:ok, [["changes()": count]]} = Sqlitex.Server.query(pid, "SELECT changes()")
235-
{:ok, %{rows: nil, num_rows: count}}
236-
end
237-
238-
# HACK: We have to do a special conversion if the user is trying to cast to
239-
# a DATETIME type. Sqlitex cannot determine that the type of the cast is a
240-
# datetime value because datetime defaults to an integer type in SQLite.
241-
# Thus, we cast the value to a TEXT_DATETIME pseudo-type to preserve the
242-
# datetime string. Then when we get here, we convert the string to an Ecto
243-
# datetime tuple if it looks like a cast was attempted.
244-
defp cast_any_datetimes(row) do
245-
Enum.map row, fn {key, value} ->
246-
str = Atom.to_string(key)
247-
if String.contains?(str, "CAST (") && String.contains?(str, "TEXT_DATE") do
248-
{key, string_to_datetime(value)}
249-
else
250-
{key, value}
251-
end
252-
end
226+
defp decode(rows, {:ok, :manual}) do
227+
%Result{rows: rows, num_rows: length(rows), decoder: :deferred}
253228
end
254-
255-
defp string_to_datetime(<<yr::binary-size(4), "-", mo::binary-size(2), "-", da::binary-size(2)>>) do
256-
{String.to_integer(yr), String.to_integer(mo), String.to_integer(da)}
229+
defp decode(rows, _) do # not specified or :auto
230+
%Result{rows: rows, num_rows: length(rows), decoder: :deferred}
231+
|> Result.decode
257232
end
258-
defp string_to_datetime(str) do
259-
<<yr::binary-size(4), "-", mo::binary-size(2), "-", da::binary-size(2), " ", hr::binary-size(2), ":", mi::binary-size(2), ":", se::binary-size(2), ".", fr::binary-size(6)>> = str
260-
{{String.to_integer(yr), String.to_integer(mo), String.to_integer(da)},{String.to_integer(hr), String.to_integer(mi), String.to_integer(se), String.to_integer(fr)}}
233+
234+
defp changes_result(pid) do
235+
{:ok, [["changes()": count]]} = Sqlitex.Server.query(pid, "SELECT changes()")
236+
{:ok, %Result{rows: nil, num_rows: count}}
261237
end
262238

263239
# SQLite does not have a returning clause, but we append a pseudo one so

lib/sqlite_ecto/result.ex

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
defmodule Sqlite.Ecto.Result do
2+
@moduledoc """
3+
Result struct returned from any successful query. Its fields are:
4+
5+
* `command` - An atom of the query command, for example: `:select` or
6+
`:insert` (TODO: not yet implemented);
7+
* `columns` - The column names (TODO: not yet implemented);
8+
* `rows` - The result set. A list of lists, each inner list corresponding to a
9+
row, each element in the list corresponds to a column;
10+
* `num_rows` - The number of fetched or affected rows;
11+
"""
12+
13+
@type t :: %__MODULE__{
14+
# command: atom,
15+
# columns: [String.t] | nil,
16+
rows: [[term]] | nil,
17+
num_rows: integer,
18+
decoder: :deferred | :done
19+
}
20+
21+
defstruct [rows: nil, num_rows: nil, decoder: :done]
22+
23+
@doc """
24+
Decodes a result set.
25+
26+
It is a no-op if the result was already decoded.
27+
28+
A mapper function can be given to further process
29+
each row, in no specific order.
30+
"""
31+
@spec decode(t, ([term] -> term)) :: t
32+
def decode(result_set, mapper \\ fn x -> x end)
33+
34+
def decode(%__MODULE__{decoder: :done} = res, _mapper), do: res
35+
36+
def decode(res, mapper) do
37+
%__MODULE__{rows: rows} = res
38+
rows = do_decode(rows, mapper)
39+
%__MODULE__{res | rows: rows, decoder: :done}
40+
end
41+
42+
defp do_decode(nil, mapper), do: nil
43+
44+
defp do_decode(rows, mapper) do
45+
rows = Enum.map(rows, fn row ->
46+
row
47+
|> cast_any_datetimes
48+
|> Keyword.values
49+
|> Enum.map(fn
50+
{:blob, binary} -> binary
51+
other -> other
52+
end)
53+
|> Enum.map(mapper)
54+
end)
55+
end
56+
57+
# HACK: We have to do a special conversion if the user is trying to cast to
58+
# a DATETIME type. Sqlitex cannot determine that the type of the cast is a
59+
# datetime value because datetime defaults to an integer type in SQLite.
60+
# Thus, we cast the value to a TEXT_DATETIME pseudo-type to preserve the
61+
# datetime string. Then when we get here, we convert the string to an Ecto
62+
# datetime tuple if it looks like a cast was attempted.
63+
defp cast_any_datetimes(row) do
64+
Enum.map row, fn {key, value} ->
65+
str = Atom.to_string(key)
66+
if String.contains?(str, "CAST (") && String.contains?(str, "TEXT_DATE") do
67+
{key, string_to_datetime(value)}
68+
else
69+
{key, value}
70+
end
71+
end
72+
end
73+
74+
defp string_to_datetime(<<yr::binary-size(4), "-", mo::binary-size(2), "-", da::binary-size(2)>>) do
75+
{String.to_integer(yr), String.to_integer(mo), String.to_integer(da)}
76+
end
77+
defp string_to_datetime(str) do
78+
<<yr::binary-size(4), "-", mo::binary-size(2), "-", da::binary-size(2), " ", hr::binary-size(2), ":", mi::binary-size(2), ":", se::binary-size(2), ".", fr::binary-size(6)>> = str
79+
{{String.to_integer(yr), String.to_integer(mo), String.to_integer(da)},{String.to_integer(hr), String.to_integer(mi), String.to_integer(se), String.to_integer(fr)}}
80+
end
81+
end

0 commit comments

Comments
 (0)