2020-05-31 17:51:41 +12:00
|
|
|
defmodule AshPostgres.Repo do
|
2020-06-04 17:27:26 +12:00
|
|
|
@moduledoc """
|
2022-08-24 11:56:46 +12:00
|
|
|
Resources that use `AshPostgres.DataLayer` use a `Repo` to access the database.
|
2020-06-04 17:27:26 +12:00
|
|
|
|
2022-08-24 11:56:46 +12:00
|
|
|
This repo is a thin wrapper around an `Ecto.Repo`.
|
2020-07-08 12:01:01 +12:00
|
|
|
|
|
|
|
You can use `Ecto.Repo`'s `init/2` to configure your repo like normal, but
|
|
|
|
instead of returning `{:ok, config}`, use `super(config)` to pass the
|
|
|
|
configuration to the `AshPostgres.Repo` implementation.
|
2020-09-03 20:18:11 +12:00
|
|
|
|
2020-11-17 18:37:04 +13:00
|
|
|
## Installed Extensions
|
2020-09-03 20:18:11 +12:00
|
|
|
|
2020-11-17 18:37:04 +13:00
|
|
|
To configure your list of installed extensions, define `installed_extensions/0`
|
|
|
|
|
2023-11-28 05:12:22 +13:00
|
|
|
Extensions can be a string, representing a standard postgres extension, or a module that implements `AshPostgres.CustomExtension`.
|
|
|
|
That custom extension will be called to generate migrations that serve a specific purpose.
|
|
|
|
|
2020-11-17 18:37:04 +13:00
|
|
|
Extensions that are relevant to ash_postgres:
|
|
|
|
|
2022-07-21 06:25:47 +12:00
|
|
|
* "ash-functions" - This isn't really an extension, but it expresses that certain functions
|
|
|
|
should be added when generating migrations, to support the `||` and `&&` operators in expressions.
|
2022-11-21 20:42:26 +13:00
|
|
|
* `"uuid-ossp"` - Sets UUID primary keys defaults in the migration generator
|
2023-09-12 14:34:51 +12:00
|
|
|
* `"pg_trgm"` - Makes the `AshPostgres.Functions.TrigramSimilarity` function available
|
2021-03-20 11:41:16 +13:00
|
|
|
* "citext" - Allows case insensitive fields to be used
|
2023-09-12 14:34:51 +12:00
|
|
|
* `"vector"` - Makes the `AshPostgres.Functions.VectorCosineDistance` function available. See `AshPostgres.Extensions.Vector` for more setup instructions.
|
2020-09-03 20:18:11 +12:00
|
|
|
|
|
|
|
```
|
|
|
|
def installed_extensions() do
|
2023-11-28 05:12:22 +13:00
|
|
|
["pg_trgm", "uuid-ossp", "vector", YourCustomExtension]
|
2020-09-03 20:18:11 +12:00
|
|
|
end
|
|
|
|
```
|
2022-12-01 14:52:36 +13:00
|
|
|
|
|
|
|
## Transaction Hooks
|
|
|
|
|
|
|
|
You can define `on_transaction_begin/1`, which will be invoked whenever a transaction is started for Ash.
|
|
|
|
|
|
|
|
This will be invoked with a map containing a `type` key and metadata.
|
|
|
|
|
|
|
|
```elixir
|
|
|
|
%{type: :create, %{resource: YourApp.YourResource, action: :action}}
|
|
|
|
```
|
2020-06-04 17:27:26 +12:00
|
|
|
"""
|
2020-07-08 12:01:01 +12:00
|
|
|
|
|
|
|
@doc "Use this to inform the data layer about what extensions are installed"
|
2024-02-06 10:52:09 +13:00
|
|
|
@callback installed_extensions() :: [String.t() | module()]
|
2022-11-21 18:31:14 +13:00
|
|
|
|
2024-03-28 09:52:28 +13:00
|
|
|
@doc "Configure the version of postgres that is being used."
|
2024-03-28 12:20:49 +13:00
|
|
|
@callback min_pg_version() :: Version.t()
|
2024-03-28 09:52:28 +13:00
|
|
|
|
2022-11-21 18:31:14 +13:00
|
|
|
@doc """
|
|
|
|
Use this to inform the data layer about the oldest potential postgres version it will be run on.
|
|
|
|
|
|
|
|
Must be an integer greater than or equal to 13.
|
2023-10-14 15:47:11 +13:00
|
|
|
|
|
|
|
## Combining with other tools
|
|
|
|
|
|
|
|
For things like `Fly.Repo`, where you might need to have more fine grained control over the repo module,
|
|
|
|
you can use the `define_ecto_repo?: false` option to `use AshPostgres.Repo`.
|
2022-11-21 18:31:14 +13:00
|
|
|
"""
|
|
|
|
|
2022-12-01 13:06:51 +13:00
|
|
|
@callback on_transaction_begin(reason :: Ash.DataLayer.transaction_reason()) :: term
|
|
|
|
|
2020-10-29 16:53:28 +13:00
|
|
|
@doc "Return a list of all schema names (only relevant for a multitenant implementation)"
|
|
|
|
@callback all_tenants() :: [String.t()]
|
|
|
|
@doc "The path where your tenant migrations are stored (only relevant for a multitenant implementation)"
|
2023-02-06 06:46:44 +13:00
|
|
|
@callback tenant_migrations_path() :: String.t() | nil
|
2021-01-27 13:16:29 +13:00
|
|
|
@doc "The path where your migrations are stored"
|
2023-02-06 06:46:44 +13:00
|
|
|
@callback migrations_path() :: String.t() | nil
|
2021-07-12 18:43:39 +12:00
|
|
|
@doc "The default prefix(postgres schema) to use when building queries"
|
|
|
|
@callback default_prefix() :: String.t()
|
2024-03-28 09:52:28 +13:00
|
|
|
|
2021-11-10 22:18:36 +13:00
|
|
|
@doc "Allows overriding a given migration type for *all* fields, for example if you wanted to always use :timestamptz for :utc_datetime fields"
|
|
|
|
@callback override_migration_type(atom) :: atom
|
2024-03-28 10:05:14 +13:00
|
|
|
@doc "Should the repo should be created by `mix ash_postgres.create`?"
|
|
|
|
@callback create?() :: boolean
|
|
|
|
@doc "Should the repo should be dropped by `mix ash_postgres.drop`?"
|
|
|
|
@callback drop?() :: boolean
|
2020-05-31 17:51:41 +12:00
|
|
|
|
|
|
|
defmacro __using__(opts) do
|
|
|
|
quote bind_quoted: [opts: opts] do
|
2023-10-14 15:47:11 +13:00
|
|
|
if Keyword.get(opts, :define_ecto_repo?, true) do
|
|
|
|
otp_app = opts[:otp_app] || raise("Must configure OTP app")
|
2020-05-31 17:51:41 +12:00
|
|
|
|
2023-10-14 15:47:11 +13:00
|
|
|
use Ecto.Repo,
|
2024-04-22 03:11:00 +12:00
|
|
|
adapter: opts[:adapter] || Ecto.Adapters.Postgres,
|
2023-10-14 15:47:11 +13:00
|
|
|
otp_app: otp_app
|
|
|
|
end
|
2020-05-31 17:51:41 +12:00
|
|
|
|
2024-03-28 12:20:49 +13:00
|
|
|
@agent __MODULE__.AshPgVersion
|
2023-02-06 06:46:44 +13:00
|
|
|
@behaviour AshPostgres.Repo
|
2024-04-23 04:04:16 +12:00
|
|
|
@warn_on_missing_ash_functions Keyword.get(opts, :warn_on_missing_ash_functions?, true)
|
|
|
|
@after_compile __MODULE__
|
|
|
|
require Logger
|
2023-02-06 06:46:44 +13:00
|
|
|
|
2023-01-30 14:27:43 +13:00
|
|
|
defoverridable insert: 2, insert: 1, insert!: 2, insert!: 1
|
|
|
|
|
2020-10-29 16:53:28 +13:00
|
|
|
def installed_extensions, do: []
|
|
|
|
def tenant_migrations_path, do: nil
|
2021-01-27 13:16:29 +13:00
|
|
|
def migrations_path, do: nil
|
2021-07-12 18:43:39 +12:00
|
|
|
def default_prefix, do: "public"
|
2021-11-10 22:18:36 +13:00
|
|
|
def override_migration_type(type), do: type
|
2024-03-28 10:05:14 +13:00
|
|
|
def create?, do: true
|
|
|
|
def drop?, do: true
|
|
|
|
|
2023-12-30 15:49:34 +13:00
|
|
|
def transaction!(fun) do
|
|
|
|
case fun.() do
|
|
|
|
{:ok, value} -> value
|
|
|
|
{:error, error} -> raise Ash.Error.to_error_class(error)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-03 03:24:42 +13:00
|
|
|
def all_tenants do
|
|
|
|
raise """
|
|
|
|
`#{inspect(__MODULE__)}.all_tenants/0` was called, but was not defined. In order to migrate tenants, you must define this function.
|
|
|
|
For example, you might say:
|
|
|
|
|
|
|
|
def all_tenants do
|
|
|
|
for org <- MyApp.Accounts.all_organizations!() do
|
|
|
|
org.schema
|
|
|
|
end
|
|
|
|
end
|
|
|
|
"""
|
|
|
|
end
|
2020-05-31 17:51:41 +12:00
|
|
|
|
2024-03-28 12:20:49 +13:00
|
|
|
def init(type, config) do
|
|
|
|
if type == :supervisor do
|
|
|
|
try do
|
|
|
|
Agent.stop(@agent)
|
|
|
|
rescue
|
|
|
|
_ ->
|
|
|
|
:ok
|
|
|
|
catch
|
|
|
|
_, _ ->
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-29 16:53:28 +13:00
|
|
|
new_config =
|
|
|
|
config
|
|
|
|
|> Keyword.put(:installed_extensions, installed_extensions())
|
|
|
|
|> Keyword.put(:tenant_migrations_path, tenant_migrations_path())
|
2021-01-27 13:16:29 +13:00
|
|
|
|> Keyword.put(:migrations_path, migrations_path())
|
2021-07-12 18:43:39 +12:00
|
|
|
|> Keyword.put(:default_prefix, default_prefix())
|
2020-05-31 17:51:41 +12:00
|
|
|
|
|
|
|
{:ok, new_config}
|
|
|
|
end
|
|
|
|
|
2022-12-01 13:06:51 +13:00
|
|
|
def on_transaction_begin(_reason), do: :ok
|
|
|
|
|
2023-01-30 14:27:43 +13:00
|
|
|
def insert(struct_or_changeset, opts \\ []) do
|
|
|
|
struct_or_changeset
|
|
|
|
|> to_ecto()
|
|
|
|
|> then(fn value ->
|
|
|
|
repo = get_dynamic_repo()
|
|
|
|
|
|
|
|
Ecto.Repo.Schema.insert(
|
|
|
|
__MODULE__,
|
|
|
|
repo,
|
|
|
|
value,
|
|
|
|
Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:insert, opts))
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
|> from_ecto()
|
|
|
|
end
|
|
|
|
|
|
|
|
def insert!(struct_or_changeset, opts \\ []) do
|
|
|
|
struct_or_changeset
|
|
|
|
|> to_ecto()
|
|
|
|
|> then(fn value ->
|
|
|
|
repo = get_dynamic_repo()
|
|
|
|
|
|
|
|
Ecto.Repo.Schema.insert!(
|
|
|
|
__MODULE__,
|
|
|
|
repo,
|
|
|
|
value,
|
|
|
|
Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:insert, opts))
|
|
|
|
)
|
|
|
|
end)
|
|
|
|
|> from_ecto()
|
|
|
|
end
|
|
|
|
|
|
|
|
def from_ecto({:ok, result}), do: {:ok, from_ecto(result)}
|
|
|
|
def from_ecto({:error, _} = other), do: other
|
|
|
|
|
|
|
|
def from_ecto(nil), do: nil
|
|
|
|
|
|
|
|
def from_ecto(value) when is_list(value) do
|
|
|
|
Enum.map(value, &from_ecto/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def from_ecto(%resource{} = record) do
|
|
|
|
if Spark.Dsl.is?(resource, Ash.Resource) do
|
|
|
|
empty = struct(resource)
|
|
|
|
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.Info.relationships()
|
|
|
|
|> Enum.reduce(record, fn relationship, record ->
|
|
|
|
case Map.get(record, relationship.name) do
|
|
|
|
%Ecto.Association.NotLoaded{} ->
|
|
|
|
Map.put(record, relationship.name, Map.get(empty, relationship.name))
|
|
|
|
|
|
|
|
value ->
|
|
|
|
Map.put(record, relationship.name, from_ecto(value))
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
else
|
|
|
|
record
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-03-28 12:20:49 +13:00
|
|
|
def min_pg_version do
|
|
|
|
if version = cached_version() do
|
|
|
|
version
|
|
|
|
else
|
|
|
|
lookup_version()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp cached_version do
|
2024-04-06 07:46:38 +13:00
|
|
|
if config()[:pool] == Ecto.Adapters.SQL.Sandbox do
|
|
|
|
Agent.start_link(
|
|
|
|
fn ->
|
|
|
|
nil
|
|
|
|
end,
|
|
|
|
name: @agent
|
|
|
|
)
|
|
|
|
|
|
|
|
case Agent.get(@agent, fn state -> state end) do
|
|
|
|
nil ->
|
|
|
|
version = lookup_version()
|
|
|
|
|
|
|
|
Agent.update(@agent, fn _ ->
|
|
|
|
version
|
|
|
|
end)
|
|
|
|
|
|
|
|
version
|
|
|
|
|
|
|
|
version ->
|
|
|
|
version
|
|
|
|
end
|
|
|
|
else
|
|
|
|
Agent.start_link(
|
|
|
|
fn ->
|
|
|
|
lookup_version()
|
|
|
|
end,
|
|
|
|
name: @agent
|
|
|
|
)
|
|
|
|
|
|
|
|
Agent.get(@agent, fn state -> state end)
|
|
|
|
end
|
2024-03-28 12:20:49 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp lookup_version do
|
|
|
|
version_string =
|
|
|
|
try do
|
|
|
|
query!("SELECT version()").rows |> Enum.at(0) |> Enum.at(0)
|
|
|
|
rescue
|
|
|
|
error ->
|
|
|
|
reraise """
|
|
|
|
Got an error while trying to read postgres version
|
|
|
|
|
|
|
|
Error:
|
|
|
|
|
|
|
|
#{inspect(error)}
|
|
|
|
""",
|
|
|
|
__STACKTRACE__
|
|
|
|
end
|
|
|
|
|
|
|
|
try do
|
|
|
|
version_string
|
|
|
|
|> String.split(" ")
|
|
|
|
|> Enum.at(1)
|
|
|
|
|> String.split(".")
|
|
|
|
|> case do
|
|
|
|
[major] ->
|
|
|
|
"#{major}.0.0"
|
|
|
|
|
|
|
|
[major, minor] ->
|
|
|
|
"#{major}.#{minor}.0"
|
|
|
|
|
|
|
|
other ->
|
|
|
|
Enum.join(other, ".")
|
|
|
|
end
|
|
|
|
|> Version.parse!()
|
|
|
|
rescue
|
|
|
|
error ->
|
|
|
|
reraise(
|
|
|
|
"""
|
|
|
|
Could not parse postgres version from version string: "#{version_string}"
|
|
|
|
|
|
|
|
You may need to define the `min_version/0` callback yourself.
|
|
|
|
|
|
|
|
Error:
|
|
|
|
|
|
|
|
#{inspect(error)}
|
|
|
|
""",
|
|
|
|
__STACKTRACE__
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-30 14:27:43 +13:00
|
|
|
def from_ecto(other), do: other
|
|
|
|
|
|
|
|
def to_ecto(nil), do: nil
|
|
|
|
|
|
|
|
def to_ecto(value) when is_list(value) do
|
|
|
|
Enum.map(value, &to_ecto/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_ecto(%resource{} = record) do
|
|
|
|
if Spark.Dsl.is?(resource, Ash.Resource) do
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.Info.relationships()
|
|
|
|
|> Enum.reduce(record, fn relationship, record ->
|
|
|
|
value =
|
|
|
|
case Map.get(record, relationship.name) do
|
|
|
|
%Ash.NotLoaded{} ->
|
|
|
|
%Ecto.Association.NotLoaded{
|
2023-03-21 09:11:05 +13:00
|
|
|
__field__: relationship.name,
|
2023-01-30 14:27:43 +13:00
|
|
|
__cardinality__: relationship.cardinality
|
|
|
|
}
|
|
|
|
|
|
|
|
value ->
|
|
|
|
to_ecto(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
Map.put(record, relationship.name, value)
|
|
|
|
end)
|
|
|
|
else
|
|
|
|
record
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_ecto(other), do: other
|
|
|
|
|
2021-07-12 18:43:39 +12:00
|
|
|
defoverridable init: 2,
|
2022-12-01 13:06:51 +13:00
|
|
|
on_transaction_begin: 1,
|
2021-07-12 18:43:39 +12:00
|
|
|
installed_extensions: 0,
|
2024-03-28 12:20:49 +13:00
|
|
|
min_pg_version: 0,
|
2021-07-12 18:43:39 +12:00
|
|
|
all_tenants: 0,
|
|
|
|
tenant_migrations_path: 0,
|
2021-11-10 22:18:36 +13:00
|
|
|
default_prefix: 0,
|
2024-03-28 10:05:14 +13:00
|
|
|
override_migration_type: 1,
|
|
|
|
create?: 0,
|
|
|
|
drop?: 0
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
# We do this switch because `!@warn_on_missing_ash_functions` in the function body triggers
|
|
|
|
# a dialyzer error
|
|
|
|
if @warn_on_missing_ash_functions do
|
|
|
|
def __after_compile__(_, _) do
|
|
|
|
if "ash-functions" in installed_extensions() do
|
|
|
|
:ok
|
|
|
|
else
|
|
|
|
IO.warn("""
|
|
|
|
AshPostgres: You have not installed the `ash-functions` extension.
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
The following features will not be available:
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
- atomics (using the `raise_ash_error` function)
|
|
|
|
- `string_trim` (using the `ash_trim_whitespace` function)
|
|
|
|
- the `||` and `&&` operators (using the `ash_elixir_and` and `ash_elixir_or` functions)
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
To address this warning, do one of two things:
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
1. add the `"ash-functions"` extension to your `installed_extensions/0` function, and then generate migrations.
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
def installed_extensions do
|
|
|
|
["ash-functions"]
|
|
|
|
end
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
If you are *not* using the migration generator, but would like to leverage these features, follow the above instructions,
|
|
|
|
and then visit the source for `ash_postgres` and copy the latest version of those functions into your own migrations:
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
2. disable this warning, by adding the following to your `use` statement:
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
use AshPostgres.Repo,
|
|
|
|
..
|
|
|
|
warn_on_missing_ash_functions?: false
|
2024-04-23 04:04:16 +12:00
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
Keep in mind that if you disable this warning, you will not be able to use the features mentioned above.
|
|
|
|
If you are in an environment where you cannot define functions, you will have to use the second option.
|
2024-04-23 04:04:16 +12:00
|
|
|
|
|
|
|
|
2024-04-24 06:15:25 +12:00
|
|
|
https://github.com/ash-project/ash_postgres/blob/main/lib/migration_generator/ash_functions.ex
|
|
|
|
""")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
def __after_compile__(_, _) do
|
|
|
|
:ok
|
2024-04-23 04:04:16 +12:00
|
|
|
end
|
|
|
|
end
|
2020-05-31 17:51:41 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|