Current section
Files
Jump to
Current section
Files
lib/ecto/adapters/turso.ex
if Code.ensure_loaded?(Ecto.Adapters.SQL) do
defmodule Ecto.Adapters.Turso do
@moduledoc """
Optional Ecto SQL adapter for ExTurso.
This adapter is compiled only when `ecto_sql` is available. It uses
`ExTurso.Connection` for DBConnection pooling and Turso native execution,
while rendering SQL with Turso's SQLite-compatible dialect.
Configure a repo with:
config :my_app, MyApp.Repo,
database: "my_app.db",
pool_size: 5
`:database`, `:remote_url`, and `:auth_token` are forwarded to ExTurso.
`:memory` and `":memory:"` open an in-memory database; use `pool_size: 1`
when using an in-memory repo so all operations share the same handle.
The adapter supports normal schema and query operations plus `ecto_sql`
migrations. Streams and multi-result queries are not supported by the
current native connection.
"""
use Ecto.Adapters.SQL,
driver: :ex_turso
@behaviour Ecto.Adapter.Storage
@behaviour Ecto.Adapter.Structure
alias Ecto.Adapters.Turso.Codec
@impl Ecto.Adapter.Storage
def storage_down(options) do
db_path = options |> Keyword.fetch!(:database) |> normalize_database()
case File.rm(db_path) do
:ok ->
File.rm(db_path <> "-shm")
File.rm(db_path <> "-wal")
:ok
_otherwise ->
{:error, :already_down}
end
end
@impl Ecto.Adapter.Storage
def storage_status(options) do
db_path = options |> Keyword.fetch!(:database) |> normalize_database()
if File.exists?(db_path) do
:up
else
:down
end
end
@impl Ecto.Adapter.Storage
def storage_up(options) do
database = options |> Keyword.get(:database) |> normalize_database()
options = Keyword.put(options, :database, database)
pool_size = Keyword.get(options, :pool_size)
cond do
is_nil(database) ->
raise ArgumentError,
"""
No Turso 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.Turso,
database: "/path/to/sqlite/database"
"""
File.exists?(database) ->
{:error, :already_up}
database == ":memory:" && pool_size != 1 ->
raise ArgumentError, """
In memory databases must have a pool_size of 1
"""
true ->
{:ok, state} = ExTurso.Connection.connect(options)
:ok = ExTurso.Connection.disconnect(:normal, state)
end
end
defp normalize_database(:memory), do: ":memory:"
defp normalize_database(database), do: database
@impl Ecto.Adapter.Migration
def supports_ddl_transaction?, do: true
@impl Ecto.Adapter.Migration
def lock_for_migrations(_meta, _options, fun) do
fun.()
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.Structure
def dump_cmd(args, opts \\ [], config) when is_list(config) and is_list(args) do
run_with_cmd("sqlite3", ["-init", "/dev/null", config[:database] | args], opts)
end
@impl Ecto.Adapter.Schema
def autogenerate(:id), do: nil
def autogenerate(:embed_id), do: Ecto.UUID.generate()
def autogenerate(:binary_id) do
case Application.get_env(:ex_turso, :binary_id_type, :string) do
:string -> Ecto.UUID.generate()
:binary -> Ecto.UUID.bingenerate()
end
end
##
## Loaders
##
@default_datetime_type :iso8601
@impl Ecto.Adapter
def loaders(:boolean, type) do
[&Codec.bool_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:naive_datetime_usec, type) do
[&Codec.naive_datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:time, type) do
[&Codec.time_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:utc_datetime_usec, type) do
[&Codec.utc_datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:utc_datetime, type) do
[&Codec.utc_datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:naive_datetime, type) do
[&Codec.naive_datetime_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:date, type) do
[&Codec.date_decode/1, type]
end
@impl Ecto.Adapter
def loaders({:map, _}, type) do
[&Codec.json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
end
@impl Ecto.Adapter
def loaders({:array, _}, type) do
[&Codec.json_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:map, type) do
[&Codec.json_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:float, type) do
[&Codec.float_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:decimal, type) do
[&Codec.decimal_decode/1, type]
end
@impl Ecto.Adapter
def loaders(:binary_id, type) do
case Application.get_env(:ex_turso, :binary_id_type, :string) do
:string -> [type]
:binary -> [Ecto.UUID, type]
end
end
@impl Ecto.Adapter
def loaders(:uuid, type) do
case Application.get_env(:ex_turso, :uuid_type, :string) do
:string -> []
:binary -> [type]
end
end
@impl Ecto.Adapter
def loaders(primitive_type, ecto_type) do
loader_from_extension(primitive_type, ecto_type)
end
##
## Dumpers
##
@impl Ecto.Adapter
def dumpers(:binary, type) do
[type, &Codec.blob_encode/1]
end
@impl Ecto.Adapter
def dumpers(:boolean, type) do
[type, &Codec.bool_encode/1]
end
@impl Ecto.Adapter
def dumpers(:decimal, type) do
[type, &Codec.decimal_encode/1]
end
@impl Ecto.Adapter
def dumpers(:binary_id, type) do
case Application.get_env(:ex_turso, :binary_id_type, :string) do
:string -> [type]
:binary -> [type, Ecto.UUID]
end
end
@impl Ecto.Adapter
def dumpers(:uuid, type) do
case Application.get_env(:ex_turso, :uuid_type, :string) do
:string -> []
:binary -> [type]
end
end
@impl Ecto.Adapter
def dumpers(:time, type) do
[type, &Codec.time_encode/1]
end
@impl Ecto.Adapter
def dumpers(:time_usec, type) do
[type, &Codec.time_encode/1]
end
@impl Ecto.Adapter
def dumpers(:date, type) do
[type, &Codec.date_encode/1]
end
@impl Ecto.Adapter
def dumpers(:utc_datetime, type) do
dt_type = Application.get_env(:ex_turso, :datetime_type, @default_datetime_type)
[type, &Codec.utc_datetime_encode(&1, dt_type)]
end
@impl Ecto.Adapter
def dumpers(:utc_datetime_usec, type) do
dt_type = Application.get_env(:ex_turso, :datetime_type, @default_datetime_type)
[type, &Codec.utc_datetime_encode(&1, dt_type)]
end
@impl Ecto.Adapter
def dumpers(:naive_datetime, type) do
dt_type = Application.get_env(:ex_turso, :datetime_type, @default_datetime_type)
[type, &Codec.naive_datetime_encode(&1, dt_type)]
end
@impl Ecto.Adapter
def dumpers(:naive_datetime_usec, type) do
dt_type = Application.get_env(:ex_turso, :datetime_type, @default_datetime_type)
[type, &Codec.naive_datetime_encode(&1, dt_type)]
end
@impl Ecto.Adapter
def dumpers({:array, _}, type) do
[type, &Codec.json_encode/1]
end
@impl Ecto.Adapter
def dumpers({:map, _}, type) do
[&Ecto.Type.embedded_dump(type, &1, :json), &Codec.json_encode/1]
end
@impl Ecto.Adapter
def dumpers(:map, type) do
[type, &Codec.json_encode/1]
end
@impl Ecto.Adapter
def dumpers(primitive_type, ecto_type) do
dumper_from_extension(primitive_type, ecto_type)
end
##
## HELPERS
##
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, cmd_opts \\ []) 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
cmd_opts = Keyword.put_new(cmd_opts, :stderr_to_stdout, true)
System.cmd(cmd, args, cmd_opts)
end
defp extensions do
Application.get_env(:ex_turso, :type_extensions, [])
end
defp loader_from_extension(primitive_type, ecto_type) do
loader_from_extension(extensions(), primitive_type, ecto_type)
end
defp loader_from_extension([], _primitive_type, ecto_type), do: [ecto_type]
defp loader_from_extension([extension | other_extensions], primitive_type, ecto_type) do
case extension.loaders(primitive_type, ecto_type) do
nil -> loader_from_extension(other_extensions, primitive_type, ecto_type)
loader -> loader
end
end
defp dumper_from_extension(primitive_type, ecto_type) do
dumper_from_extension(extensions(), primitive_type, ecto_type)
end
defp dumper_from_extension([], _primitive_type, ecto_type), do: [ecto_type]
defp dumper_from_extension([extension | other_extensions], primitive_type, ecto_type) do
case extension.dumpers(primitive_type, ecto_type) do
nil -> dumper_from_extension(other_extensions, primitive_type, ecto_type)
dumper -> dumper
end
end
end
end