Skip to content

Commit f95b36e

Browse files
authored
Adds multi_step/3 (#128)
1 parent 3f074a2 commit f95b36e

6 files changed

Lines changed: 183 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
# Changelog
22

3-
## [Unreleased]
3+
4+
## [0.5.2] - 2021-03-23
45

56
### Added
6-
- Guide for Windows users
7+
- Guide for Windows users.
8+
- `Exqlite.Sqlite3.multi_step/3` to step through results chunks at a time.
9+
- `default_chunk_size` configuration.
710

811
## [0.5.1] - 2021-03-19
912

1013
### Changed
1114
- Bumped SQLite3 amalgamation to version 3.35.2
1215
- Replaced old references of [github.com/warmwaffles](http://github.com/warmwaffles)
1316

17+
1418
## [0.5.0] - 2021-03-17
1519

1620
### Removed
17-
- Removed `Ecto.Adapters.Exqlite`
21+
- Removed `Ecto.Adapters.Exqlite`
1822
Replaced with [Ecto Sqlite3][ecto_sqlite3] library.
1923

2024

2125
[ecto_sqlite3]: <https://github.com/elixir-sqlite/ecto_sqlite3>
2226

2327
[Unreleased]: https://github.com/elixir-sqlite/exqlite/compare/v0.5.1...main
28+
[0.5.2]: https://github.com/elixir-sqlite/exqlite/compare/v0.5.1...v0.5.2
2429
[0.5.1]: https://github.com/elixir-sqlite/exqlite/compare/v0.5.0...v0.5.1
2530
[0.5.0]: https://github.com/elixir-sqlite/exqlite/compare/v0.4.9...v0.5.0

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ end
3636
```
3737

3838

39+
## Configuration
40+
41+
```elixir
42+
config :exqlite, default_chunk_size: 100
43+
```
44+
45+
* `default_chunk_size` - The chunk size that is used when multi-stepping when
46+
not specifying the chunk size explicitly.
47+
48+
3949
## Usage
4050

4151
The `Exqlite.Sqlite3` module usage is fairly straight forward.

c_src/sqlite3_nif.c

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,63 @@ make_row(ErlNifEnv* env, sqlite3_stmt* statement)
445445

446446
enif_free(columns);
447447

448-
return enif_make_tuple2(env, make_atom(env, "row"), row);
448+
return row;
449+
}
450+
451+
static ERL_NIF_TERM
452+
exqlite_multi_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
453+
{
454+
assert(env);
455+
456+
statement_t* statement = NULL;
457+
connection_t* conn = NULL;
458+
int chunk_size;
459+
460+
if (argc != 3) {
461+
return enif_make_badarg(env);
462+
}
463+
464+
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
465+
return make_error_tuple(env, "invalid_connection");
466+
}
467+
468+
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
469+
return make_error_tuple(env, "invalid_statement");
470+
}
471+
472+
if (!enif_get_int(env, argv[2], &chunk_size)) {
473+
return make_error_tuple(env, "invalid_chunk_size");
474+
}
475+
476+
if (chunk_size < 1) {
477+
return make_error_tuple(env, "invalid_chunk_size");
478+
}
479+
480+
ERL_NIF_TERM rows = enif_make_list_from_array(env, NULL, 0);
481+
for (int i = 0; i < chunk_size; i++) {
482+
ERL_NIF_TERM row;
483+
484+
int rc = sqlite3_step(statement->statement);
485+
switch (rc) {
486+
case SQLITE_BUSY:
487+
sqlite3_reset(statement->statement);
488+
return make_atom(env, "busy");
489+
490+
case SQLITE_DONE:
491+
return enif_make_tuple2(env, make_atom(env, "done"), rows);
492+
493+
case SQLITE_ROW:
494+
row = make_row(env, statement->statement);
495+
rows = enif_make_list_cell(env, row, rows);
496+
break;
497+
498+
default:
499+
sqlite3_reset(statement->statement);
500+
return make_sqlite3_error_tuple(env, rc, conn->db);
501+
}
502+
}
503+
504+
return enif_make_tuple2(env, make_atom(env, "rows"), rows);
449505
}
450506

451507
static ERL_NIF_TERM
@@ -471,7 +527,11 @@ exqlite_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
471527
int rc = sqlite3_step(statement->statement);
472528
switch (rc) {
473529
case SQLITE_ROW:
474-
return make_row(env, statement->statement);
530+
return enif_make_tuple2(
531+
env,
532+
make_atom(env, "row"),
533+
make_row(env, statement->statement)
534+
);
475535
case SQLITE_BUSY:
476536
return make_atom(env, "busy");
477537
case SQLITE_DONE:
@@ -642,6 +702,7 @@ static ErlNifFunc nif_funcs[] = {
642702
{"prepare", 2, exqlite_prepare, ERL_NIF_DIRTY_JOB_IO_BOUND},
643703
{"bind", 3, exqlite_bind, ERL_NIF_DIRTY_JOB_IO_BOUND},
644704
{"step", 2, exqlite_step, ERL_NIF_DIRTY_JOB_IO_BOUND},
705+
{"multi_step", 3, exqlite_multi_step, ERL_NIF_DIRTY_JOB_IO_BOUND},
645706
{"columns", 2, exqlite_columns, ERL_NIF_DIRTY_JOB_IO_BOUND},
646707
{"last_insert_rowid", 1, exqlite_last_insert_rowid, ERL_NIF_DIRTY_JOB_IO_BOUND},
647708
{"transaction_status", 1, exqlite_transaction_status, ERL_NIF_DIRTY_JOB_IO_BOUND},

lib/exqlite/sqlite3.ex

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ defmodule Exqlite.Sqlite3 do
1919
@type db() :: reference()
2020
@type statement() :: reference()
2121
@type reason() :: atom() | String.t()
22+
@type row() :: []
2223

2324
@doc """
2425
Opens a new sqlite database at the Path provided.
@@ -78,6 +79,30 @@ defmodule Exqlite.Sqlite3 do
7879
@spec step(db(), statement()) :: :done | :busy | {:row, []}
7980
def step(conn, statement), do: Sqlite3NIF.step(conn, statement)
8081

82+
@spec multi_step(db(), statement()) :: :busy | {:rows, [row()]} | {:done, [row()]}
83+
def multi_step(conn, statement) do
84+
chunk_size = Application.get_env(:exqlite, :default_chunk_size, 50)
85+
multi_step(conn, statement, chunk_size)
86+
end
87+
88+
@spec multi_step(db(), statement(), integer()) ::
89+
:busy | {:rows, [row()]} | {:done, [row()]}
90+
def multi_step(conn, statement, chunk_size) do
91+
case Sqlite3NIF.multi_step(conn, statement, chunk_size) do
92+
:busy ->
93+
{:error, "Database busy"}
94+
95+
{:error, reason} ->
96+
{:error, reason}
97+
98+
{:rows, rows} ->
99+
{:rows, Enum.reverse(rows)}
100+
101+
{:done, rows} ->
102+
{:done, Enum.reverse(rows)}
103+
end
104+
end
105+
81106
@spec last_insert_rowid(db()) :: {:ok, integer()}
82107
def last_insert_rowid(conn), do: Sqlite3NIF.last_insert_rowid(conn)
83108

@@ -93,22 +118,29 @@ defmodule Exqlite.Sqlite3 do
93118
Sqlite3NIF.execute(conn, String.to_charlist("PRAGMA shrink_memory"))
94119
end
95120

96-
@spec fetch_all(db(), statement()) :: {:ok, []} | {:error, reason()}
97-
def fetch_all(conn, statement) do
121+
@spec fetch_all(db(), statement()) :: {:ok, [row()]} | {:error, reason()}
122+
def fetch_all(conn, statement, chunk_size \\ 50) do
98123
# TODO: Should this be done in the NIF? It can be _much_ faster to build a
99124
# list there, but at the expense that it could block other dirty nifs from
100125
# getting work done.
101126
#
102127
# For now this just works
103-
fetch_all(conn, statement, [])
128+
fetch_all(conn, statement, chunk_size, [])
104129
end
105130

106-
defp fetch_all(conn, statement, result) do
107-
case step(conn, statement) do
108-
:busy -> {:error, "Database busy"}
109-
{:error, reason} -> {:error, reason}
110-
:done -> {:ok, result}
111-
{:row, row} -> fetch_all(conn, statement, result ++ [row])
131+
defp fetch_all(conn, statement, chunk_size, accum) do
132+
case multi_step(conn, statement, chunk_size) do
133+
{:done, rows} ->
134+
{:ok, accum ++ rows}
135+
136+
{:rows, rows} ->
137+
fetch_all(conn, statement, accum ++ rows)
138+
139+
{:error, reason} ->
140+
{:error, reason}
141+
142+
:busy ->
143+
{:error, "Database busy"}
112144
end
113145
end
114146

lib/exqlite/sqlite3_nif.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ defmodule Exqlite.Sqlite3NIF do
3737
@spec step(db(), statement()) :: :done | :busy | {:row, []}
3838
def step(_conn, _statement), do: :erlang.nif_error(:not_loaded)
3939

40+
@spec multi_step(db(), statement(), integer()) ::
41+
:busy | {:rows, [[]]} | {:done, [[]]}
42+
def multi_step(_conn, _statement, _chunk_size), do: :erlang.nif_error(:not_loaded)
43+
4044
@spec columns(db(), statement()) :: {:ok, []} | {:error, reason()}
4145
def columns(_conn, _statement), do: :erlang.nif_error(:not_loaded)
4246

test/exqlite/sqlite3_test.exs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ defmodule Exqlite.Sqlite3Test do
165165
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('Another test')")
166166
{:ok, 2} = Sqlite3.last_insert_rowid(conn)
167167

168-
{:ok, statement} = Sqlite3.prepare(conn, "select id, stuff from test")
168+
{:ok, statement} =
169+
Sqlite3.prepare(conn, "select id, stuff from test order by id asc")
169170

170171
{:row, columns} = Sqlite3.step(conn, statement)
171172
assert [1, "This is a test"] == columns
@@ -202,6 +203,61 @@ defmodule Exqlite.Sqlite3Test do
202203
end
203204
end
204205

206+
describe ".multi_step/3" do
207+
test "returns results" do
208+
{:ok, conn} = Sqlite3.open(":memory:")
209+
210+
:ok =
211+
Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
212+
213+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('one')")
214+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('two')")
215+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('three')")
216+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('four')")
217+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('five')")
218+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('six')")
219+
220+
{:ok, statement} =
221+
Sqlite3.prepare(conn, "select id, stuff from test order by id asc")
222+
223+
{:rows, rows} = Sqlite3.multi_step(conn, statement, 4)
224+
assert rows == [[1, "one"], [2, "two"], [3, "three"], [4, "four"]]
225+
226+
{:done, rows} = Sqlite3.multi_step(conn, statement, 4)
227+
assert rows == [[5, "five"], [6, "six"]]
228+
end
229+
end
230+
231+
describe ".multi_step/2" do
232+
test "returns results" do
233+
{:ok, conn} = Sqlite3.open(":memory:")
234+
235+
:ok =
236+
Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
237+
238+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('one')")
239+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('two')")
240+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('three')")
241+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('four')")
242+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('five')")
243+
:ok = Sqlite3.execute(conn, "insert into test (stuff) values ('six')")
244+
245+
{:ok, statement} =
246+
Sqlite3.prepare(conn, "select id, stuff from test order by id asc")
247+
248+
{:done, rows} = Sqlite3.multi_step(conn, statement)
249+
250+
assert rows == [
251+
[1, "one"],
252+
[2, "two"],
253+
[3, "three"],
254+
[4, "four"],
255+
[5, "five"],
256+
[6, "six"]
257+
]
258+
end
259+
end
260+
205261
describe "working with prepared statements after close" do
206262
test "returns proper error" do
207263
{:ok, conn} = Sqlite3.open(":memory:")

0 commit comments

Comments
 (0)