Skip to content

Commit 0c385ac

Browse files
authored
Adds similar interface as Postgrex offers (#203)
* Adds missing typespecs * Adds `:table` protocol implementation
1 parent b45c25c commit 0c385ac

9 files changed

Lines changed: 217 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

3-
## [Unreleased](https://github.com/elixir-sqlite/exqlite/compare/v0.10.2...HEAD)
3+
## [Unreleased](https://github.com/elixir-sqlite/exqlite/compare/v0.11.0...HEAD)
4+
5+
## [0.11.0] - 2022-05-05
6+
### Added
7+
- Added top level interface for `Exqlite` similar to `Postgrex`'s interface.
8+
- Adds optional table protocol support for results.
49

510
## [0.10.3] - 2022-04-10
611
### Fixed
@@ -215,6 +220,7 @@
215220

216221

217222
[ecto_sqlite3]: <https://github.com/elixir-sqlite/ecto_sqlite3>
223+
[0.11.0]: https://github.com/elixir-sqlite/exqlite/compare/v0.11.0...v0.10.2
218224
[0.10.2]: https://github.com/elixir-sqlite/exqlite/compare/v0.10.1...v0.10.2
219225
[0.10.1]: https://github.com/elixir-sqlite/exqlite/compare/v0.10.0...v0.10.1
220226
[0.10.0]: https://github.com/elixir-sqlite/exqlite/compare/v0.9.3...v0.10.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Package: https://hex.pm/packages/exqlite
3434
```elixir
3535
defp deps do
3636
[
37-
{:exqlite, "~> 0.10.3"}
37+
{:exqlite, "~> 0.11.0"}
3838
]
3939
end
4040
```

lib/exqlite.ex

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,104 @@
11
defmodule Exqlite do
2-
@moduledoc false
2+
@moduledoc """
3+
SQLite3 driver for Elixir.
4+
"""
5+
6+
alias Exqlite.Connection
7+
alias Exqlite.Error
8+
alias Exqlite.Query
9+
alias Exqlite.Result
10+
11+
@doc "See `Exqlite.Connection.connect/1`"
12+
@spec start_link([Connection.connection_opt()]) :: {:ok, pid()} | {:error, Error.t()}
13+
def start_link(opts) do
14+
DBConnection.start_link(Connection, opts)
15+
end
16+
17+
@spec query(DBConnection.conn(), iodata(), list(), list()) ::
18+
{:ok, Result.t()} | {:error, Exception.t()}
19+
def query(conn, statement, params \\ [], opts \\ []) do
20+
query = %Query{name: "", statement: IO.iodata_to_binary(statement)}
21+
22+
case DBConnection.prepare_execute(conn, query, params, opts) do
23+
{:ok, _query, result} ->
24+
{:ok, result}
25+
26+
otherwise ->
27+
otherwise
28+
end
29+
end
30+
31+
@spec query!(DBConnection.conn(), iodata(), list(), list()) :: Result.t()
32+
def query!(conn, statement, params \\ [], opts \\ []) do
33+
case query(conn, statement, params, opts) do
34+
{:ok, result} -> result
35+
{:error, err} -> raise err
36+
end
37+
end
38+
39+
@spec prepare(DBConnection.conn(), iodata(), list()) ::
40+
{:ok, Query.t()} | {:error, Exception.t()}
41+
def prepare(conn, name, statement, opts \\ []) do
42+
query = %Query{name: name, statement: statement}
43+
DBConnection.prepare(conn, query, opts)
44+
end
45+
46+
@spec prepare!(DBConnection.conn(), iodata(), iodata(), list()) :: Query.t()
47+
def prepare!(conn, name, statement, opts \\ []) do
48+
query = %Query{name: name, statement: statement}
49+
DBConnection.prepare!(conn, query, opts)
50+
end
51+
52+
@spec prepare_execute(DBConnection.conn(), iodata(), iodata(), list(), list()) ::
53+
{:ok, Query.t(), Result.t()} | {:error, Error.t()}
54+
def prepare_execute(conn, name, statement, params, opts \\ []) do
55+
query = %Query{name: name, statement: statement}
56+
DBConnection.prepare_execute(conn, query, params, opts)
57+
end
58+
59+
@spec prepare_execute!(DBConnection.conn(), iodata(), iodata(), list(), list()) ::
60+
{Query.t(), Result.t()}
61+
def prepare_execute!(conn, name, statement, params, opts \\ []) do
62+
query = %Query{name: name, statement: statement}
63+
DBConnection.prepare_execute!(conn, query, params, opts)
64+
end
65+
66+
@spec execute(DBConnection.conn(), Query.t(), list(), list()) ::
67+
{:ok, Result.t()} | {:error, Error.t()}
68+
def execute(conn, query, params, opts \\ []) do
69+
DBConnection.execute(conn, query, params, opts)
70+
end
71+
72+
@spec execute!(DBConnection.conn(), Query.t(), list(), list()) :: Result.t()
73+
def execute!(conn, query, params, opts \\ []) do
74+
DBConnection.execute!(conn, query, params, opts)
75+
end
76+
77+
@spec close(DBConnection.conn(), Query.t(), list()) :: :ok | {:error, Exception.t()}
78+
def close(conn, query, opts \\ []) do
79+
with {:ok, _} <- DBConnection.close(conn, query, opts) do
80+
:ok
81+
end
82+
end
83+
84+
@spec close!(DBConnection.conn(), Query.t(), list()) :: :ok
85+
def close!(conn, query, opts \\ []) do
86+
DBConnection.close!(conn, query, opts)
87+
:ok
88+
end
89+
90+
@spec transaction(DBConnection.conn(), (DBConnection.t() -> result), list()) ::
91+
{:ok, result} | {:error, any}
92+
when result: var
93+
def transaction(conn, fun, opts \\ []) do
94+
DBConnection.transaction(conn, fun, opts)
95+
end
96+
97+
@spec rollback(DBConnection.t(), term()) :: no_return()
98+
def rollback(conn, reason), do: DBConnection.rollback(conn, reason)
99+
100+
@spec child_spec([Connection.connection_opt()]) :: :supervisor.child_spec()
101+
def child_spec(opts) do
102+
DBConnection.child_spec(Connection, opts)
103+
end
3104
end

lib/exqlite/connection.ex

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ defmodule Exqlite.Connection do
4444
status: :idle | :busy
4545
}
4646

47+
@type journal_mode() :: :delete | :truncate | :persist | :memory | :wal | :off
48+
@type temp_store() :: :default | :file | :memory
49+
@type synchronous() :: :extra | :full | :normal | :off
50+
@type auto_vacuum() :: :none | :full | :incremental
51+
@type locking_mode() :: :normal | :exclusive
52+
53+
@type connection_opt() ::
54+
{:database, String.t()}
55+
| {:journal_mode, journal_mode()}
56+
| {:temp_store, temp_store()}
57+
| {:synchronous, synchronous()}
58+
| {:foreign_keys, :on | :off}
59+
| {:cache_size, integer()}
60+
| {:cache_spill, :on | :off}
61+
| {:case_sensitive_like, boolean()}
62+
| {:auto_vacuum, auto_vacuum()}
63+
| {:locking_mode, locking_mode()}
64+
| {:secure_delete, :on | :off}
65+
| {:wal_auto_check_point, integer()}
66+
| {:busy_timeout, integer()}
67+
| {:chunk_size, integer()}
68+
| {:journal_size_limit, integer()}
69+
| {:soft_heap_limit, integer()}
70+
| {:hard_heap_limit, integer()}
71+
| {:key, String.t()}
72+
4773
@impl true
4874
@doc """
4975
Initializes the Ecto Exqlite adapter.
@@ -100,6 +126,7 @@ defmodule Exqlite.Connection do
100126
101127
[1]: https://www.sqlite.org/pragma.html
102128
"""
129+
@spec connect([connection_opt()]) :: {:ok, t()} | {:error, Exception.t()}
103130
def connect(options) do
104131
database = Keyword.get(options, :database)
105132

lib/exqlite/error.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ defmodule Exqlite.Error do
22
@moduledoc false
33
defexception [:message, :statement]
44

5+
@type t() :: %__MODULE__{
6+
message: String.t(),
7+
statement: String.t()
8+
}
9+
510
@impl true
611
def message(%__MODULE__{message: message, statement: nil}), do: message
712

lib/exqlite/result.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ defmodule Exqlite.Result do
1919
}
2020
end
2121
end
22+
23+
if Code.ensure_loaded?(Table.Reader) do
24+
defimpl Table.Reader, for: Exqlite.Result do
25+
def init(%{columns: columns}) when columns in [nil, []] do
26+
{:rows, %{columns: []}, []}
27+
end
28+
29+
def init(result) do
30+
{:rows, %{columns: result.columns}, result.rows}
31+
end
32+
end
33+
end

mix.exs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Exqlite.MixProject do
22
use Mix.Project
33

4-
@version "0.10.3"
4+
@version "0.11.0"
55

66
def project do
77
[
@@ -12,11 +12,13 @@ defmodule Exqlite.MixProject do
1212
make_targets: ["all"],
1313
make_clean: ["clean"],
1414
start_permanent: Mix.env() == :prod,
15+
aliases: aliases(),
1516
deps: deps(),
1617
package: package(),
1718
description: description(),
1819
test_paths: test_paths(System.get_env("EXQLITE_INTEGRATION")),
1920
elixirc_paths: elixirc_paths(Mix.env()),
21+
dialyzer: dialyzer(),
2022

2123
# Docs
2224
name: "Exqlite",
@@ -41,7 +43,15 @@ defmodule Exqlite.MixProject do
4143
{:elixir_make, "~> 0.6", runtime: false},
4244
{:ex_doc, "~> 0.27", only: :dev, runtime: false},
4345
{:temp, "~> 0.4", only: [:dev, :test]},
44-
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}
46+
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
47+
{:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false},
48+
{:table, "~> 0.1.0", optional: true}
49+
]
50+
end
51+
52+
defp aliases do
53+
[
54+
lint: ["format --check-formatted", "credo --all", "dialyzer"]
4555
]
4656
end
4757

@@ -92,4 +102,11 @@ defmodule Exqlite.MixProject do
92102

93103
defp test_paths(nil), do: ["test"]
94104
defp test_paths(_any), do: ["integration_test/exqlite"]
105+
106+
defp dialyzer do
107+
[
108+
plt_add_deps: :apps_direct,
109+
plt_add_apps: ~w(table)a
110+
]
111+
end
95112
end

mix.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
44
"credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"},
55
"db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
6+
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
67
"earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"},
78
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
9+
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
810
"ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"},
911
"ex_sqlean": {:hex, :ex_sqlean, "0.8.8", "be03d0aa4ee2955b59b386743ccaccbf0cc56f9635f701f185fe2d962b2ab214", [:mix], [], "hexpm", "de3644787ee736880597886decdf86f104c1778398401615c37d1ec4563146fa"},
1012
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
@@ -13,6 +15,7 @@
1315
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
1416
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
1517
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
18+
"table": {:hex, :table, "0.1.1", "e8d3c75bb14b8dac227e17d85a626e695d96c271569e94e4a2d15494a7cb0b49", [:mix], [], "hexpm", "6a73280b2f5ad70474594feeb8c034ad490b97dbd5adaeb678f71c250cbc928c"},
1619
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
1720
"temp": {:hex, :temp, "0.4.7", "2c78482cc2294020a4bc0c95950b907ff386523367d4e63308a252feffbea9f2", [:mix], [], "hexpm", "6af19e7d6a85a427478be1021574d1ae2a1e1b90882586f06bde76c63cd03e0d"},
1821
}

test/exqlite/query_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule Exqlite.QueryTest do
2+
use ExUnit.Case, async: true
3+
4+
setup :create_conn!
5+
6+
test "table reader integration", %{conn: conn} do
7+
assert {:ok, _} = Exqlite.query(conn, "CREATE TABLE tab(x integer, y text);", [])
8+
9+
assert {:ok, _} =
10+
Exqlite.query(
11+
conn,
12+
"INSERT INTO tab(x, y) VALUES (1, 'a'), (2, 'b'), (3, 'c');",
13+
[]
14+
)
15+
16+
assert {:ok, res} =
17+
Exqlite.query(
18+
conn,
19+
"SELECT * FROM tab;",
20+
[]
21+
)
22+
23+
assert res |> Table.to_rows() |> Enum.to_list() == [
24+
%{"x" => 1, "y" => "a"},
25+
%{"x" => 2, "y" => "b"},
26+
%{"x" => 3, "y" => "c"}
27+
]
28+
29+
columns = Table.to_columns(res)
30+
assert Enum.to_list(columns["x"]) == [1, 2, 3]
31+
assert Enum.to_list(columns["y"]) == ["a", "b", "c"]
32+
end
33+
34+
defp create_conn!(_) do
35+
opts = [database: "#{Temp.path!()}.db"]
36+
37+
{:ok, pid} = start_supervised(Exqlite.child_spec(opts))
38+
39+
[conn: pid]
40+
end
41+
end

0 commit comments

Comments
 (0)