diff --git a/README.md b/README.md index e3dd307..c6d78b1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,20 @@ config :my_app, MyApp.Repo, database: "path/to/my/database.db" ``` +## Type Extensions + +Type extensions allow custom data types to be stored and retrieved from an SQLite3 database. + +This is done by implementing a module with the `Ecto.Adapters.SQLite3.TypeExtension` behaviour which maps types to encoder and decoder functions. Type extensions are activated by adding them to the `ecto_sqlite3` configuration as a list of type extention modules assigned to the `type_extensions` key: + +```elixir +config :exqlite: + type_extensions: [MyApp.TypeExtension] + +config :ecto_sqlite3, + type_extensions: [MyApp.TypeExtension] +``` + ## Database Encryption As of version 0.9, `exqlite` supports loading database engines at runtime rather than compiling `sqlite3.c` itself. diff --git a/lib/ecto/adapters/sqlite3.ex b/lib/ecto/adapters/sqlite3.ex index 2e5832d..d4ed0a6 100644 --- a/lib/ecto/adapters/sqlite3.ex +++ b/lib/ecto/adapters/sqlite3.ex @@ -444,8 +444,8 @@ defmodule Ecto.Adapters.SQLite3 do end @impl Ecto.Adapter - def loaders(_, type) do - [type] + def loaders(primitive_type, ecto_type) do + loader_from_extension(primitive_type, ecto_type) end ## @@ -528,8 +528,8 @@ defmodule Ecto.Adapters.SQLite3 do end @impl Ecto.Adapter - def dumpers(_primitive, type) do - [type] + def dumpers(primitive_type, ecto_type) do + dumper_from_extension(primitive_type, ecto_type) end ## @@ -569,4 +569,34 @@ defmodule Ecto.Adapters.SQLite3 do System.cmd(cmd, args, cmd_opts) end + + defp extensions do + Application.get_env(:ecto_sqlite3, :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 diff --git a/lib/ecto/adapters/sqlite3/type_extension.ex b/lib/ecto/adapters/sqlite3/type_extension.ex new file mode 100644 index 0000000..4448710 --- /dev/null +++ b/lib/ecto/adapters/sqlite3/type_extension.ex @@ -0,0 +1,31 @@ +defmodule Ecto.Adapters.SQLite3.TypeExtension do + @moduledoc """ + A behaviour that defines the API for extensions providing custom data loaders and dumpers + for Ecto schemas. + """ + + @type primitive_type :: atom | {:map, type :: atom} + @type ecto_type :: atom + + @doc """ + Takes a primitive type and, if it knows how to decode it into an + appropriate Elixir data structure, reutrns a two-element list in + the form `[(db_data :: any -> term), elixir_type :: atom]`. + + The function that is the first element will be called whenever + the `primitive_type` appears in a schema and data is fetched from + the database for it. + """ + @callback loaders(primitive_type, ecto_type) :: list | nil + + @doc """ + Takes a primitive type and, if it knows how to encode it for storage + in the database, returns a two-element list in + the form `[elixir_type :: atom, (term -> db_data :: any)]`. + + The function that is the second element will be called whenever + the `primitive_type` appears in a schema and data is about to be + sent to the database for storage. + """ + @callback dumpers(ecto_type, primitive_type) :: list | nil +end