Packages
exqlite
0.3.6
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.0
0.30.1
0.30.0
0.29.0
0.28.0
0.27.1
0.27.0
0.26.0
0.25.0
0.24.2
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.0
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
An Elixir SQLite3 library
Current section
Files
Jump to
Current section
Files
lib/ecto/adapters/exqlite.ex
defmodule Ecto.Adapters.Exqlite do
use Ecto.Adapters.SQL,
driver: :exqlite
@behaviour Ecto.Adapter.Storage
@behaviour Ecto.Adapter.Structure
@impl Ecto.Adapter.Storage
def storage_down(options) do
db_path = Keyword.fetch!(options, :database)
with :ok <- File.rm(db_path) do
File.rm(db_path <> "-shm")
File.rm(db_path <> "-wal")
:ok
else
_ -> {:error, :already_down}
end
end
@impl Ecto.Adapter.Storage
def storage_status(options) do
db_path = Keyword.fetch!(options, :database)
if File.exists?(db_path) do
:up
else
:down
end
end
@impl Ecto.Adapter.Storage
def storage_up(options) do
options
|> Keyword.get(:database)
|> storage_up_with_path()
end
@impl Ecto.Adapter.Migration
def supports_ddl_transaction?(), do: false
@impl Ecto.Adapter.Migration
def lock_for_migrations(_meta, query, _options, fun) do
fun.(query)
end
@impl Ecto.Adapter.Structure
def structure_dump(default, config) do
path = config[:dump_path] || Path.join(default, "structure.sql")
with {:ok, contents} <- dump_schema(config),
{:ok, versions} <- dump_versions(config) do
File.mkdir_p!(Path.dirname(path))
File.write!(path, contents <> versions)
{:ok, path}
else
err -> err
end
end
@impl Ecto.Adapter.Structure
def structure_load(default, config) do
path = config[:dump_path] || Path.join(default, "structure.sql")
case run_with_cmd("sqlite3", [config[:database], ".read #{path}"]) do
{_output, 0} -> {:ok, path}
{output, _} -> {:error, output}
end
end
@impl Ecto.Adapter.Schema
def insert(adapter_meta, schema_meta, params, on_conflict, returning, opts) do
%{source: source, prefix: prefix} = schema_meta
{_, query_params, _} = on_conflict
key = primary_key!(schema_meta, returning)
{fields, values} = :lists.unzip(params)
# Construct the insertion sql statement
sql = @conn.insert(prefix, source, fields, [fields], on_conflict, [], [])
# Build the query name we are going to pass on
opts = [{:command, :insert} | opts]
opts = [{:query_name, generate_cache_name(:insert, sql)} | opts]
case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do
{:ok, %{last_insert_id: rowid}} ->
{:ok, last_insert_id(key, rowid)}
{:error, err} ->
case @conn.to_constraints(err, source: source) do
[] -> raise err
constraints -> {:invalid, constraints}
end
end
end
##
## Loaders
##
@impl Ecto.Adapter
def loaders(:boolean, type) do
[&bool_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:binary_id, type) do
[Ecto.UUID, type]
end
@impl Ecto.Adapter
def loaders(:utc_datetime, type) do
[&datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:naive_datetime, type) do
[&naive_datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:datetime, type) do
[&datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:date, type) do
[&date_decode/1, type]
end
@impl Ecto.Adapter
def loaders({:embed, _} = type, _) do
[&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
end
@impl Ecto.Adapter
def loaders({:map, _}, type) do
[&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
end
@impl Ecto.Adapter
def loaders({:array, _}, type) do
[&json_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:map, type) do
[&json_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:float, type) do
[&float_decode/1, type]
end
@impl Ecto.Adapter
def loaders(_, type) do
[type]
end
defp bool_decode(0), do: {:ok, false}
defp bool_decode("0"), do: {:ok, false}
defp bool_decode("FALSE"), do: {:ok, false}
defp bool_decode(1), do: {:ok, true}
defp bool_decode("1"), do: {:ok, true}
defp bool_decode("TRUE"), do: {:ok, true}
defp bool_decode(x), do: {:ok, x}
defp json_decode(x) when is_binary(x) do
Application.get_env(:ecto, :json_library).decode(x)
end
defp json_decode(x), do: x
defp float_decode(x) when is_integer(x), do: {:ok, x / 1}
defp float_decode(x), do: {:ok, x}
defp datetime_decode(val) do
# TODO: Should we be preserving the timezone? SQLite3 stores everything
# shifted to UTC. sqlite_ecto2 used a custom field type "TEXT_DATETIME"
# to preserve the original string inserted. But I don't know if that
# is desirable or not.
#
# @warmwaffles 2021-02-28
case DateTime.from_iso8601(val) do
{:ok, dt, _offset} -> {:ok, dt}
_ -> :error
end
end
defp naive_datetime_decode(val) do
case NaiveDateTime.from_iso8601(val) do
{:ok, dt} -> {:ok, dt}
_ -> :error
end
end
defp date_decode(val) do
case Date.from_iso8601(val) do
{:ok, d} -> {:ok, d}
_ -> :error
end
end
##
## Dumpers
##
@impl Ecto.Adapter
def dumpers(:binary, type) do
[type, &blob_encode/1]
end
@impl Ecto.Adapter
def dumpers(:binary_id, type) do
[type, Ecto.UUID]
end
@impl Ecto.Adapter
def dumpers(:boolean, type) do
[type, &bool_encode/1]
end
@impl Ecto.Adapter
def dumpers({:embed, _} = type, _) do
[&Ecto.Type.embedded_dump(type, &1, :json)]
end
@impl Ecto.Adapter
def dumpers(:time, type) do
[type, &time_encode/1]
end
@impl Ecto.Adapter
def dumpers(:naive_datetime, type) do
[type, &naive_datetime_encode/1]
end
@impl Ecto.Adapter
def dumpers({:in, sub}, {:in, sub}) do
[{:array, sub}]
end
@impl Ecto.Adapter
def dumpers({:array, _}, type) do
[type, &json_encode/1]
end
@impl Ecto.Adapter
def dumpers({:map, _}, type) do
[type, &json_encode/1]
end
@impl Ecto.Adapter
def dumpers(_primitive, type) do
[type]
end
defp json_encode(value) do
Application.get_env(:ecto, :json_library).encode(value)
end
defp blob_encode(value), do: {:ok, {:blob, value}}
defp bool_encode(false), do: {:ok, 0}
defp bool_encode(true), do: {:ok, 1}
defp time_encode(value) do
{:ok, value}
end
defp naive_datetime_encode(value) do
{:ok, NaiveDateTime.to_iso8601(value)}
end
##
## HELPERS
##
defp primary_key!(%{autogenerate_id: {_, key, _type}}, [key]), do: key
defp primary_key!(_, []), do: nil
defp primary_key!(%{schema: schema}, returning) do
raise ArgumentError,
"Sqlite3 does not support :read_after_writes in schemas for non-primary keys. " <>
"The following fields in #{inspect(schema)} are tagged as such: #{
inspect(returning)
}"
end
defp last_insert_id(nil, _last_insert_id), do: []
defp last_insert_id(_key, 0), do: []
defp last_insert_id(key, last_insert_id), do: [{key, last_insert_id}]
defp generate_cache_name(operation, sql) do
digest = :crypto.hash(:sha, IO.iodata_to_binary(sql)) |> Base.encode16()
"ecto_#{operation}_#{digest}"
end
defp storage_up_with_path(nil) do
raise ArgumentError,
"""
No SQLite database path specified. Please check the configuration for your Repo.
Your config/*.exs file should have something like this in it:
config :my_app, MyApp.Repo,
adapter: Ecto.Adapters.Exqlite,
database: "/path/to/sqlite/database"
"""
end
defp storage_up_with_path(db_path) do
if File.exists?(db_path) do
{:error, :already_up}
else
db_path |> Path.dirname() |> File.mkdir_p!()
{:ok, db} = Exqlite.Sqlite3.open(db_path)
:ok = Exqlite.Sqlite3.close(db)
end
end
defp dump_versions(config) do
table = config[:migration_source] || "schema_migrations"
# `.dump` command also returns CREATE TABLE which will clash with CREATE we already run in dump_schema
# So we set mode to insert which makes every SELECT statement to issue the result
# as the INSERT statements instead of pure text data.
case run_with_cmd("sqlite3", [
config[:database],
".mode insert #{table}",
"SELECT * FROM #{table}"
]) do
{output, 0} -> {:ok, output}
{output, _} -> {:error, output}
end
end
defp dump_schema(config) do
case run_with_cmd("sqlite3", [config[:database], ".schema"]) do
{output, 0} -> {:ok, output}
{output, _} -> {:error, output}
end
end
defp run_with_cmd(cmd, args) do
unless System.find_executable(cmd) do
raise "could not find executable `#{cmd}` in path, " <>
"please guarantee it is available before running ecto commands"
end
System.cmd(cmd, args, stderr_to_stdout: true)
end
end