Skip to main content

lib/ecto/adapters/ex_sql.ex

defmodule Ecto.Adapters.ExSQL do
  @moduledoc """
  Ecto SQL adapter for ExSQL.

  This module starts as a thin Ecto adapter shell over the pure-Elixir ExSQL
  DBConnection driver. Raw SQL queries are supported first; query generation,
  migration DDL, and SQLite-compatible type codecs are ported in later phases.
  """

  use Ecto.Adapters.SQL, driver: :exsql

  @behaviour Ecto.Adapter.Storage

  @impl Ecto.Adapter
  def loaders(:boolean, type), do: [&bool_decode/1, type]
  def loaders(:date, type), do: [&date_decode/1, type]
  def loaders(:time, type), do: [&time_decode/1, type]
  def loaders(:time_usec, type), do: [&time_decode/1, type]
  def loaders(:naive_datetime, type), do: [&naive_datetime_decode/1, type]
  def loaders(:naive_datetime_usec, type), do: [&naive_datetime_decode/1, type]
  def loaders(:utc_datetime, type), do: [&utc_datetime_decode/1, type]
  def loaders(:utc_datetime_usec, type), do: [&utc_datetime_decode/1, type]
  def loaders({:array, _}, type), do: [&json_decode/1, type]
  def loaders({:map, _}, type), do: [&json_decode/1, &Ecto.Type.embedded_load(type, &1, :json)]
  def loaders(:map, type), do: [&json_decode/1, type]
  def loaders(:binary_id, type), do: [Ecto.UUID, type]
  def loaders(_, type), do: [type]

  @impl Ecto.Adapter
  def dumpers(:boolean, type), do: [type, &bool_encode/1]
  def dumpers({:array, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
  def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)]
  def dumpers(:binary_id, type), do: [type, Ecto.UUID]
  def dumpers(_, type), do: [type]

  @impl Ecto.Adapter.Storage
  def storage_down(options) do
    db_path = Keyword.fetch!(options, :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 = Keyword.fetch!(options, :database)

    if File.exists?(db_path), do: :up, else: :down
  end

  @impl Ecto.Adapter.Storage
  def storage_up(options) do
    database = Keyword.get(options, :database)

    cond do
      is_nil(database) ->
        raise ArgumentError, """
        No ExSQL database path specified. Please configure your Repo with:

            config :my_app, MyApp.Repo,
              adapter: Ecto.Adapters.ExSQL,
              database: "/path/to/sqlite/database"
        """

      database in [:memory, ":memory:"] ->
        :ok

      File.exists?(database) ->
        {:error, :already_up}

      true ->
        database |> Path.dirname() |> File.mkdir_p!()
        db = ExSQL.Database.new()

        case ExSQL.FileFormat.write(db, database, journal_mode: :memory) do
          {:ok, _path} -> :ok
          {:error, reason} -> {:error, reason}
        end
    end
  end

  defp bool_decode(0), do: {:ok, false}
  defp bool_decode(1), do: {:ok, true}
  defp bool_decode(false), do: {:ok, false}
  defp bool_decode(true), do: {:ok, true}
  defp bool_decode(value), do: {:ok, value}

  defp bool_encode(false), do: {:ok, 0}
  defp bool_encode(true), do: {:ok, 1}
  defp bool_encode(value), do: {:ok, value}

  defp date_decode(value) when is_binary(value), do: Date.from_iso8601(value)
  defp date_decode(value), do: {:ok, value}

  defp time_decode(value) when is_binary(value), do: Time.from_iso8601(value)
  defp time_decode(value), do: {:ok, value}

  defp naive_datetime_decode(value) when is_binary(value), do: NaiveDateTime.from_iso8601(value)
  defp naive_datetime_decode(value), do: {:ok, value}

  defp utc_datetime_decode(value) when is_binary(value) do
    case DateTime.from_iso8601(value) do
      {:ok, datetime, _offset} -> {:ok, datetime}
      {:error, _reason} -> naive_datetime_decode(value)
    end
  end

  defp utc_datetime_decode(value), do: {:ok, value}

  defp json_decode(value) when is_binary(value), do: Jason.decode(value)
  defp json_decode(value), do: {:ok, value}

  @impl Ecto.Adapter.Migration
  def supports_ddl_transaction?, do: true

  @impl Ecto.Adapter.Migration
  def lock_for_migrations(_meta, _options, fun), do: fun.()
end