Skip to content

Commit 0b169f9

Browse files
committed
rm dbconnection
1 parent 9f1558d commit 0b169f9

33 files changed

+1613
-3104
lines changed

.formatter.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
inputs: [
33
"{mix,.formatter}.exs",
4-
"{config,lib,test}/**/*.{ex,exs}"
4+
"{config,bench,lib,test}/**/*.{ex,exs}"
55
],
66
line_length: 88
77
]

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- removed: `db_connection`. It's just a NIF now. `db_connection` is moved to `ecto_sqlite3`
6+
- removed: `Exqlite.Basic`
7+
38
## v0.24.1
49

510
- fixed: Pre-compile images for Apple and Windows

Makefile

+5-8
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,11 @@ CFLAGS += -DHAVE_USLEEP=1
9494
# installing the nif. Just need to have certain environment variables
9595
# enabled to support them.
9696
CFLAGS += -DALLOW_COVERING_INDEX_SCAN=1
97-
CFLAGS += -DENABLE_FTS3_PARENTHESIS=1
9897
CFLAGS += -DENABLE_LOAD_EXTENSION=1
99-
CFLAGS += -DENABLE_SOUNDEX=1
10098
CFLAGS += -DENABLE_STAT4=1
10199
CFLAGS += -DENABLE_UPDATE_DELETE_LIMIT=1
102-
CFLAGS += -DSQLITE_ENABLE_FTS3=1
103-
CFLAGS += -DSQLITE_ENABLE_FTS4=1
104-
CFLAGS += -DSQLITE_ENABLE_FTS5=1
105-
CFLAGS += -DSQLITE_ENABLE_GEOPOLY=1
106100
CFLAGS += -DSQLITE_ENABLE_MATH_FUNCTIONS=1
107101
CFLAGS += -DSQLITE_ENABLE_RBU=1
108-
CFLAGS += -DSQLITE_ENABLE_RTREE=1
109102
CFLAGS += -DSQLITE_OMIT_DEPRECATED=1
110103
CFLAGS += -DSQLITE_ENABLE_DBSTAT_VTAB=1
111104

@@ -142,7 +135,11 @@ $(ARCHIVE_NAME): $(OBJ)
142135
$(PREFIX) $(BUILD):
143136
mkdir -p $@
144137

145-
clean:
138+
rebuild:
139+
$(RM) $(LIB_NAME) $(BUILD)/sqlite3_nif.o
140+
$(MAKE) all
141+
142+
clean:
146143
$(RM) $(LIB_NAME) $(ARCHIVE_NAME) $(OBJ)
147144

148145
.PHONY: all clean

README.md

+45-71
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,11 @@ Package: https://hex.pm/packages/exqlite
1515

1616
## Caveats
1717

18-
* Prepared statements are not cached.
1918
* Prepared statements are not immutable. You must be careful when manipulating
2019
statements and binding values to statements. Do not try to manipulate the
2120
statements concurrently. Keep it isolated to one process.
22-
* Simultaneous writing is not supported by SQLite3 and will not be supported
23-
here.
24-
* All native calls are run through the Dirty NIF scheduler.
25-
* Datetimes are stored without offsets. This is due to how SQLite3 handles date
26-
and times. If you would like to store a timezone, you will need to create a
27-
second column somewhere storing the timezone name and shifting it when you
28-
get it from the database. This is more reliable than storing the offset as
29-
`+03:00` as it does not respect daylight savings time.
30-
* When storing `BLOB` values, you have to use `{:blob, the_binary}`, otherwise
31-
it will be interpreted as a string.
21+
* Some native calls are run through the Dirty NIF scheduler.
22+
Some are executed directly on current scheduler.
3223

3324
## Installation
3425

@@ -42,15 +33,6 @@ end
4233

4334

4435
## Configuration
45-
46-
### Runtime Configuration
47-
48-
```elixir
49-
config :exqlite, default_chunk_size: 100
50-
```
51-
52-
* `default_chunk_size` - The chunk size that is used when multi-stepping when
53-
not specifying the chunk size explicitly.
5436

5537
### Compile-time Configuration
5638

@@ -126,47 +108,52 @@ export EXQLITE_SYSTEM_CFLAGS=-I/usr/local/include/sqlcipher
126108
export EXQLITE_SYSTEM_LDFLAGS=-L/usr/local/lib -lsqlcipher
127109
```
128110

129-
Once you have `exqlite` configured, you can use the `:key` option in the database config to enable encryption:
111+
Once you have `exqlite` build configured, you can use the `key` pragma to enable encryption:
130112

131113
```elixir
132-
config :exqlite, key: "super-secret'
114+
{:ok, db} = Exqlite.open("sqlcipher.db")
115+
:ok = Exqlite.execute(db, "pragma key='super-secret'")
133116
```
134117

135118
## Usage
136119

137-
The `Exqlite.Sqlite3` module usage is fairly straight forward.
120+
The `Exqlite` module usage is fairly straight forward.
138121

139122
```elixir
140-
# We'll just keep it in memory right now
141-
{:ok, conn} = Exqlite.Sqlite3.open(":memory:")
123+
{:ok, db} = Exqlite.open("app.db", [:readwrite, :create])
124+
125+
:ok = Exqlite.execute(db, "pragma foreign_keys=on")
126+
:ok = Exqlite.execute(db, "pragma journal_mode=wal")
127+
:ok = Exqlite.execute(db, "pragma busy_timeout=5000")
142128

143129
# Create the table
144-
:ok = Exqlite.Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
130+
:ok = Exqlite.execute(db, "create table test (id integer primary key, stuff text)")
145131

146132
# Prepare a statement
147-
{:ok, statement} = Exqlite.Sqlite3.prepare(conn, "insert into test (stuff) values (?1)")
148-
:ok = Exqlite.Sqlite3.bind(conn, statement, ["Hello world"])
133+
{:ok, insert} = Exqlite.prepare(db, "insert into test (stuff) values (?1)")
134+
:ok = Exqlite.bind_all(db, insert, ["Hello world"])
149135

150136
# Step is used to run statements
151-
:done = Exqlite.Sqlite3.step(conn, statement)
137+
:done = Exqlite.step(db, insert)
152138

153139
# Prepare a select statement
154-
{:ok, statement} = Exqlite.Sqlite3.prepare(conn, "select id, stuff from test")
140+
{:ok, select} = Exqlite.prepare(db, "select id, stuff from test")
155141

156142
# Get the results
157-
{:row, [1, "Hello world"]} = Exqlite.Sqlite3.step(conn, statement)
143+
{:row, [1, "Hello world"]} = Exqlite.step(db, select)
158144

159145
# No more results
160-
:done = Exqlite.Sqlite3.step(conn, statement)
146+
:done = Exqlite.step(db, select)
161147

162-
# Release the statement.
148+
# Release the statements.
163149
#
164150
# It is recommended you release the statement after using it to reclaim the memory
165151
# asap, instead of letting the garbage collector eventually releasing the statement.
166152
#
167153
# If you are operating at a high load issuing thousands of statements, it would be
168154
# possible to run out of memory or cause a lot of pressure on memory.
169-
:ok = Exqlite.Sqlite3.release(conn, statement)
155+
:ok = Exqlite.finalize(insert)
156+
:ok = Exqlite.finalize(select)
170157
```
171158

172159
### Using SQLite3 native extensions
@@ -177,52 +164,39 @@ available by installing the [ExSqlean](https://github.com/mindreframer/ex_sqlean
177164
package. This package wraps [SQLean: all the missing SQLite functions](https://github.com/nalgeon/sqlean).
178165

179166
```elixir
180-
alias Exqlite.Basic
181-
{:ok, conn} = Basic.open("db.sqlite3")
182-
:ok = Basic.enable_load_extension(conn)
167+
{:ok, db} = Exqlite.open(":memory:", [:readwrite])
168+
:ok = Exqlite.enable_load_extension(db, true)
169+
170+
exec = fn db, sql, params ->
171+
with {:ok, stmt} <- Exqlite.prepare(db, sql) do
172+
try do
173+
with :ok <- Exqlite.bind_all(db, stmt, params) do
174+
Exqlite.fetch_all(db, stmt)
175+
end
176+
after
177+
Exqlite.finalize(stmt)
178+
end
179+
end
180+
end
183181

184182
# load the regexp extension - https://github.com/nalgeon/sqlean/blob/main/docs/re.md
185-
Basic.load_extension(conn, ExSqlean.path_for("re"))
183+
{:ok, _rows} = exec.(db, "select load_extension(?)", [ExSqlean.path_for("re")])
186184

187185
# run some queries to test the new `regexp_like` function
188-
{:ok, [[1]], ["value"]} = Basic.exec(conn, "select regexp_like('the year is 2021', ?) as value", ["2021"]) |> Basic.rows()
189-
{:ok, [[0]], ["value"]} = Basic.exec(conn, "select regexp_like('the year is 2021', ?) as value", ["2020"]) |> Basic.rows()
186+
{:ok, [[1]], ["value"]} = exec.(db, "select regexp_like('the year is 2021', ?) as value", ["2021"])
187+
{:ok, [[0]], ["value"]} = exec.(db, "select regexp_like('the year is 2021', ?) as value", ["2020"])
190188

191189
# prevent loading further extensions
192-
:ok = Basic.disable_load_extension(conn)
193-
{:error, %Exqlite.Error{message: "not authorized"}, _} = Basic.load_extension(conn, ExSqlean.path_for("re"))
190+
:ok = Exqlite.enable_load_extension(db, false)
194191

195-
# close connection
196-
Basic.close(conn)
197-
```
198-
199-
It is also possible to load extensions using the `Connection` configuration. For example:
192+
{:error, %Exqlite.Error{message: "not authorized"}} =
193+
exec.(db, "select load_extension(?)", [ExSqlean.path_for("stats")])
200194

201-
```elixir
202-
arch_dir =
203-
System.cmd("uname", ["-sm"])
204-
|> elem(0)
205-
|> String.trim()
206-
|> String.replace(" ", "-")
207-
|> String.downcase() # => "darwin-arm64"
208-
209-
config :myapp, arch_dir: arch_dir
210-
211-
# global
212-
config :exqlite, load_extensions: [ "./priv/sqlite/\#{arch_dir}/rotate" ]
213-
214-
# per connection in a Phoenix app
215-
config :myapp, Myapp.Repo,
216-
database: "path/to/db",
217-
load_extensions: [
218-
"./priv/sqlite/\#{arch_dir}/vector0",
219-
"./priv/sqlite/\#{arch_dir}/vss0"
220-
]
195+
# close connection
196+
Exqlite.close(db)
221197
```
222198

223-
See [Exqlite.Connection.connect/1](https://hexdocs.pm/exqlite/Exqlite.Connection.html#connect/1)
224-
for more information. When using extensions for SQLite3, they must be compiled
225-
for the environment you are targeting.
199+
When using extensions for SQLite3, they must be compiled for the environment you are targeting.
226200

227201
## Why SQLite3
228202

@@ -239,7 +213,7 @@ that would be resiliant to power outages and still maintain some state that
239213

240214
## Under The Hood
241215

242-
We are using the Dirty NIF scheduler to execute the sqlite calls. The rationale
216+
We are using the Dirty NIF scheduler to execute most of the sqlite calls. The rationale
243217
behind this is that maintaining each sqlite's connection command pool is
244218
complicated and error prone.
245219

bench/bind.exs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{:ok, db} = Exqlite.open(":memory:", [:readwrite, :nomutex])
2+
{:ok, stmt} = Exqlite.prepare(db, "select ? + 1")
3+
4+
Benchee.run(%{
5+
"bind_all" => fn -> Exqlite.bind_all(db, stmt, [1]) end,
6+
"dirty_cpu_bind_all" => fn -> Exqlite.dirty_cpu_bind_all(db, stmt, [1]) end
7+
})

bench/insert_all.exs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{:ok, db} = Exqlite.open(":memory:", [:readwrite])
2+
:ok = Exqlite.execute(db, "create table test (id integer primary key, name text)")
3+
{:ok, stmt} = Exqlite.prepare(db, "insert into test(name) values(?)")
4+
5+
Benchee.run(
6+
%{
7+
"insert_all" =>
8+
{fn rows -> Exqlite.insert_all(db, stmt, rows) end,
9+
before_scenario: fn _input -> Exqlite.execute(db, "truncate test") end}
10+
},
11+
inputs: %{
12+
"3 rows" => Enum.map(1..3, fn i -> ["name-#{i}"] end),
13+
"30 rows" => Enum.map(1..30, fn i -> ["name-#{i}"] end),
14+
"90 rows" => Enum.map(1..90, fn i -> ["name-#{i}"] end),
15+
"300 rows" => Enum.map(1..300, fn i -> ["name-#{i}"] end),
16+
"1000 rows" => Enum.map(1..1000, fn i -> ["name-#{i}"] end)
17+
}
18+
)

bench/prepare.exs

Whitespace-only changes.

bench/step.exs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
tmp_dir = Path.expand(Path.join("./tmp", "bench/step"))
2+
File.mkdir_p!(tmp_dir)
3+
4+
path = Path.join(tmp_dir, "db.sqlite")
5+
if File.exists?(path), do: File.rm!(path)
6+
7+
IO.puts("Creating DB at #{path} ...")
8+
{:ok, db} = Exqlite.open(path, [:readwrite, :nomutex, :create])
9+
10+
IO.puts("Inserting 1000 rows ...")
11+
:ok = Exqlite.execute(db, "create table test(stuff text)")
12+
{:ok, insert} = Exqlite.prepare(db, "insert into test(stuff) values(?)")
13+
:ok = Exqlite.insert_all(db, insert, Enum.map(1..1000, fn i -> ["name-#{i}"] end))
14+
:ok = Exqlite.finalize(insert)
15+
16+
select = fn limit ->
17+
{:ok, select} = Exqlite.prepare(db, "select * from test limit #{limit}")
18+
select
19+
end
20+
21+
defmodule Bench do
22+
def step_all(db, stmt) do
23+
case Exqlite.step(db, stmt) do
24+
{:row, _} -> step_all(db, stmt)
25+
:done -> :ok
26+
end
27+
end
28+
29+
def dirty_io_step_all(db, stmt) do
30+
case Exqlite.dirty_io_step(db, stmt) do
31+
{:row, _} -> dirty_io_step_all(db, stmt)
32+
:done -> :ok
33+
end
34+
end
35+
36+
def multi_step_all(db, stmt, steps) do
37+
case Exqlite.multi_step(db, stmt, steps) do
38+
{:rows, _} -> multi_step_all(db, stmt, steps)
39+
{:done, _} -> :ok
40+
end
41+
end
42+
end
43+
44+
IO.puts("Running benchmarks ...\n")
45+
46+
Benchee.run(
47+
%{
48+
"step" => fn stmt -> Bench.step_all(db, stmt) end,
49+
"dirty_io_step" => fn stmt -> Bench.dirty_io_step_all(db, stmt) end,
50+
"multi_step(100)" => fn stmt -> Bench.multi_step_all(db, stmt, _steps = 100) end
51+
},
52+
inputs: %{
53+
"10 rows" => select.(10),
54+
"100 rows" => select.(100),
55+
"500 rows" => select.(500)
56+
},
57+
memory_time: 2
58+
)

0 commit comments

Comments
 (0)