improvement: update ash and support new identity features

This commit is contained in:
Zach Daniel 2024-05-24 01:14:55 -04:00
parent 4e84a3f75d
commit 8ad92cc3c0
15 changed files with 971 additions and 49 deletions

View file

@ -1,6 +1,7 @@
spark_locals_without_parens = [
all_tenants?: 1,
base_filter_sql: 1,
calculations_to_sql: 1,
check: 1,
check_constraint: 2,
check_constraint: 3,
@ -13,6 +14,7 @@ spark_locals_without_parens = [
exclusion_constraint_names: 1,
foreign_key_names: 1,
identity_index_names: 1,
identity_wheres_to_sql: 1,
ignore?: 1,
include: 1,
index: 1,

View file

@ -7,46 +7,37 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
## [v2.0.4](https://github.com/ash-project/ash_postgres/compare/v2.0.3...v2.0.4) (2024-05-23)
### Bug Fixes:
* ensure update's reselect all changing values
[updates] ensure update's reselect all changing values
## [v2.0.3](https://github.com/ash-project/ash_postgres/compare/v2.0.2...v2.0.3) (2024-05-22)
### Bug Fixes:
* handle complex maps/list on update
[updates] handle complex maps/list on update
* support anonymous aggregates in sorts
[Ash.Query] support anonymous aggregates in sorts
* ensure parent_as bindings properly reference binding names
[exists] ensure parent_as bindings properly reference binding names
* add and remove custom indexes in tandem properly
[migration generator] add and remove custom indexes in tandem properly
### Improvements:
* support `on_delete: :nilify` for specific columns (#289)
[references] support `on_delete: :nilify` for specific columns (#289)
## [v2.0.2](https://github.com/ash-project/ash_postgres/compare/v2.0.1...v2.0.2) (2024-05-15)
### Bug Fixes:
* [update_query/destroy_query] rework the update and destroy query builder to support multiple kinds of joining
- [update_query/destroy_query] rework the update and destroy query builder to support multiple kinds of joining
* [mix ash_postgres.migrate] remove duplicate repo flags (#285)
- [mix ash_postgres.migrate] remove duplicate repo flags (#285)
* [Ash.Error.Changes.StaleRecord] ensure filter is included in stale record error messages we return
- [Ash.Error.Changes.StaleRecord] ensure filter is included in stale record error messages we return
* [AshPostgres.MigrationGenerator] properly parse previous version from migration generation
- [AshPostgres.MigrationGenerator] properly parse previous version from migration generation
## [v2.0.1](https://github.com/ash-project/ash_postgres/compare/v2.0.0...v2.0.1) (2024-05-12)

View file

@ -42,6 +42,8 @@ end
| [`migrate?`](#postgres-migrate?){: #postgres-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` |
| [`migration_types`](#postgres-migration_types){: #postgres-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. |
| [`migration_defaults`](#postgres-migration_defaults){: #postgres-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. |
| [`calculations_to_sql`](#postgres-calculations_to_sql){: #postgres-calculations_to_sql } | `keyword` | | A keyword list of calculations and their SQL representation. Used when creating unique indexes for identities over calculations |
| [`identity_wheres_to_sql`](#postgres-identity_wheres_to_sql){: #postgres-identity_wheres_to_sql } | `keyword` | | A keyword list of identity names and the SQL representation of their `where` clause. Used when creating unique indexes for identities over calculations |
| [`base_filter_sql`](#postgres-base_filter_sql){: #postgres-base_filter_sql } | `String.t` | | A raw sql version of the base_filter, e.g `representative = true`. Required if trying to create a unique constraint on a resource with a base_filter |
| [`simple_join_first_aggregates`](#postgres-simple_join_first_aggregates){: #postgres-simple_join_first_aggregates } | `list(atom)` | `[]` | A list of `:first` type aggregate names that can be joined to using a simple join. Use when you have a `:first` aggregate that uses a to-many relationship , but your `filter` statement ensures that there is only one result. Optimizes the generated query. |
| [`skip_unique_indexes`](#postgres-skip_unique_indexes){: #postgres-skip_unique_indexes } | `atom \| list(atom)` | `false` | Skip generating unique indexes when generating migrations |

View file

@ -300,6 +300,16 @@ defmodule AshPostgres.DataLayer do
A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\\\"now()\\\\")`, or for `nil`, use `\\\\"nil\\\\"`.
"""
],
calculations_to_sql: [
type: :keyword_list,
doc:
"A keyword list of calculations and their SQL representation. Used when creating unique indexes for identities over calculations"
],
identity_wheres_to_sql: [
type: :keyword_list,
doc:
"A keyword list of identity names and the SQL representation of their `where` clause. Used when creating unique indexes for identities over calculations"
],
base_filter_sql: [
type: :string,
doc:

View file

@ -14,6 +14,24 @@ defmodule AshPostgres.DataLayer.Info do
end
end
@doc "A keyword list of calculations to their sql representation"
def calculations_to_sql(resource) do
Extension.get_opt(resource, [:postgres], :calculations_to_sql, [])
end
def calculation_to_sql(resource, calc) do
calculations_to_sql(resource)[calc]
end
@doc "A keyword list of identity names to the sql representation of their where clauses"
def identity_wheres_to_sql(resource) do
Extension.get_opt(resource, [:postgres], :identity_wheres_to_sql, [])
end
def identity_where_to_sql(resource, identity) do
identity_wheres_to_sql(resource)[identity]
end
@doc "Checks a version requirement against the resource's repo's postgres version"
def pg_version_matches?(resource, requirement) do
resource

View file

@ -1817,7 +1817,9 @@ defmodule AshPostgres.MigrationGenerator do
old_identity.name == identity.name &&
Enum.sort(old_identity.keys) == Enum.sort(identity.keys) &&
old_identity.base_filter == identity.base_filter &&
old_identity.all_tenants? == identity.all_tenants?
old_identity.all_tenants? == identity.all_tenants? &&
old_identity.nils_distinct? == identity.nils_distinct? &&
old_identity.where == identity.where
end)
else
false
@ -1829,7 +1831,9 @@ defmodule AshPostgres.MigrationGenerator do
old_identity.name == identity.name &&
Enum.sort(old_identity.keys) == Enum.sort(identity.keys) &&
old_identity.base_filter == identity.base_filter &&
old_identity.all_tenants? == identity.all_tenants?
old_identity.all_tenants? == identity.all_tenants? &&
old_identity.nils_distinct? == identity.nils_distinct? &&
old_identity.where == identity.where
end)
end)
end
@ -2820,23 +2824,32 @@ defmodule AshPostgres.MigrationGenerator do
|> Enum.reject(fn identity ->
identity.name in AshPostgres.DataLayer.Info.skip_unique_indexes(resource)
end)
|> Enum.filter(fn identity ->
Enum.all?(identity.keys, fn key ->
Ash.Resource.Info.attribute(resource, key)
end)
end)
|> Enum.sort_by(& &1.name)
|> Enum.map(&Map.take(&1, [:name, :keys, :all_tenants?]))
|> Enum.map(fn %{keys: keys} = identity ->
%{
identity
| keys:
Enum.map(keys, fn key ->
attribute = Ash.Resource.Info.attribute(resource, key)
attribute.source || attribute.name
case Ash.Resource.Info.field(resource, key) do
%Ash.Resource.Attribute{} = attribute ->
to_string(attribute.source || attribute.name)
%Ash.Resource.Calculation{} ->
AshPostgres.DataLayer.Info.calculation_to_sql(resource, key) ||
raise "Must define an entry for :#{key} in `postgres.calculations_to_sql`, or skip this identity with `postgres.skip_unique_indexes`"
end
end)
|> Enum.sort(),
where:
if identity.where do
AshPostgres.DataLayer.Info.identity_where_to_sql(resource, identity.name) ||
raise(
"Must provide an entry for :#{identity.name} in `postgres.identity_wheres_to_sql`, or skip this identity with `postgres.skip_unique_indexes`"
)
end
}
end)
|> Enum.map(&Map.take(&1, [:name, :keys, :where, :all_tenants?, :nils_distinct?]))
|> Enum.map(fn identity ->
Map.put(
identity,
@ -3179,13 +3192,13 @@ defmodule AshPostgres.MigrationGenerator do
identity
|> Map.update!(:name, &maybe_to_atom/1)
|> Map.update!(:keys, fn keys ->
keys
|> Enum.map(&maybe_to_atom/1)
|> Enum.sort()
Enum.sort(keys)
end)
|> add_index_name(table)
|> Map.put_new(:base_filter, nil)
|> Map.put_new(:all_tenants?, false)
|> Map.put_new(:where, nil)
|> Map.put_new(:nils_distinct?, true)
end
defp add_index_name(%{name: name} = index, table) do

View file

@ -30,7 +30,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
# sobelow_skip ["DOS.StringToAtom"]
def as_atom(value), do: Macro.inspect_atom(:remote_call, String.to_atom(value))
def option(:nulls_distinct = key, value) do
def option(key, value) when key in [:nulls_distinct, "nulls_distinct"] do
if !value do
"#{as_atom(key)}: #{inspect(value)}"
end
@ -790,6 +790,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
identity: %{
name: name,
keys: keys,
nils_distinct?: nils_distinct?,
where: where,
base_filter: base_filter,
index_name: index_name,
all_tenants?: all_tenants?
@ -813,10 +815,17 @@ defmodule AshPostgres.MigrationGenerator.Operation do
index_name = index_name || "#{table}_#{name}_index"
if base_filter do
"create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], where: \"#{base_filter}\", #{join(["name: \"#{index_name}\"", option("prefix", schema)])})"
else
"create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\"", option("prefix", schema)])})"
cond do
base_filter && where ->
where = "(#{where}) AND (#{base_filter})"
"create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\"", option("prefix", schema), option("nulls_distinct", nils_distinct?), option("where", where)])})"
base_filter ->
"create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], where: \"#{base_filter}\", #{join(["name: \"#{index_name}\"", option("prefix", schema), option("nulls_distinct", nils_distinct?)])})"
true ->
"create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\"", option("prefix", schema), option("nulls_distinct", nils_distinct?), option("where", where)])})"
end
end

View file

@ -162,7 +162,7 @@ defmodule AshPostgres.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, ash_version("~> 3.0 and >= 3.0.6")},
{:ash, ash_version("~> 3.0 and >= 3.0.7")},
{:ash_sql, ash_sql_version("~> 0.1 and >= 0.1.3")},
{:ecto_sql, "~> 3.9"},
{:ecto, "~> 3.9"},

View file

@ -1,5 +1,5 @@
%{
"ash": {:hex, :ash, "3.0.6", "888a5b81a0106e7122a487ea55cd968d8acfc5ac85a22c876eef7ffea9083041", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c3d529933cfe53fb9e018f04cc7106eef74d74872edee1288d0ba75d9e97202d"},
"ash": {:hex, :ash, "3.0.7", "6c37e092f53b1b21eb89596f600a652b2a601f84378f44fd5dd1cdec72eb1cc2", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9288ddb50fe727096c6f63fd82c631de2505dcd29bdfa50b5dc13c865f0bf434"},
"ash_sql": {:hex, :ash_sql, "0.1.3", "c9acc4809b7f253aad31764024aee0cd632077a32cff6bea3b105c7b8d9015b7", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "d2d3d1044f0fa48454d0cdaeb22d55a2de3210d48a2208fd2eecf6f3007a5216"},
"benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
@ -30,7 +30,7 @@
"reactor": {:hex, :reactor, "0.8.2", "b2be82b1c3402537d06a8f85bb1849f72cb6b4be140495cb8956de7aec2fdebd", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c35eb23b77cc77ba922af108722ac93257899e35cfdd18882f0e659ad2cac9f3"},
"simple_sat": {:hex, :simple_sat, "0.1.3", "f650fc3c184a5fe741868b5ac56dc77fdbb428468f6dbf1978e14d0334497578", [:mix], [], "hexpm", "a54305066a356b7194dc81db2a89232bacdc0b3edaef68ed9aba28dcbc34887b"},
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"sourceror": {:hex, :sourceror, "1.2.0", "471232b2eb9ab930b90673d37cf005bbaec0ef02dadf5bf4c8c00c3d75a6c131", [:mix], [], "hexpm", "f01796ce1b87016573ce7b66073d6b48297c4d233982340340834269b8c95e51"},
"sourceror": {:hex, :sourceror, "1.2.1", "b415255ad8bd05f0e859bb3d7ea617f6c2a4a405f2a534a231f229bd99b89f8b", [:mix], [], "hexpm", "e4d97087e67584a7585b5fe3d5a71bf8e7332f795dd1a44983d750003d5e750c"},
"spark": {:hex, :spark, "2.1.22", "a36400eede64c51af578de5fdb5a5aaa3e0811da44bcbe7545fce059bd2a990b", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "f764611d0b15ac132e72b2326539acc11fc4e63baa3e429f541bca292b5f7064"},
"splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},

View file

@ -0,0 +1,371 @@
{
"attributes": [
{
"default": "fragment(\"gen_random_uuid()\")",
"size": null,
"type": "uuid",
"source": "id",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "title_column",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "timestamptz(6)",
"source": "datetime",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "score",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "boolean",
"source": "public",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "citext",
"source": "category",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "\"sponsored\"",
"size": null,
"type": "text",
"source": "type",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "price",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "\"0\"",
"size": null,
"type": "decimal",
"source": "decimal",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "status",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "status",
"source": "status_enum",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": [
"array",
"float"
],
"source": "point",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "custom_point",
"source": "composite_point",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "map",
"source": "stuff",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": [
"array",
"map"
],
"source": "list_of_stuff",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_one",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_two",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_custom_one",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_custom_two",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_on_upper",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": [
"array",
"text"
],
"source": "list_containing_nils",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "created_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "timestamptz(6)",
"source": "updated_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "uuid",
"source": "organization_id",
"references": {
"name": "posts_organization_id_fkey",
"table": "orgs",
"schema": "public",
"on_delete": null,
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"primary_key?": true,
"destination_attribute": "id",
"on_update": null,
"deferrable": false,
"match_type": null,
"match_with": null,
"destination_attribute_default": null,
"destination_attribute_generated": null
},
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "uuid",
"source": "author_id",
"references": {
"name": "posts_author_id_fkey",
"table": "authors",
"schema": "public",
"on_delete": null,
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"primary_key?": true,
"destination_attribute": "id",
"on_update": null,
"deferrable": false,
"match_type": null,
"match_with": null,
"destination_attribute_default": null,
"destination_attribute_generated": null
},
"allow_nil?": true,
"generated?": false,
"primary_key?": false
}
],
"table": "posts",
"hash": "A602EBEA21CE56CC203A0FC8EA66D35D5FD2DA646411088CE343EF793C9D3E9C",
"repo": "Elixir.AshPostgres.TestRepo",
"identities": [
{
"name": "uniq_one_and_two",
"keys": [
"uniq_one",
"uniq_two"
],
"base_filter": "type = 'sponsored'",
"all_tenants?": false,
"index_name": "posts_uniq_one_and_two_index"
},
{
"name": "uniq_on_upper",
"keys": [
"UPPER(uniq_on_upper)"
],
"base_filter": "type = 'sponsored'",
"all_tenants?": false,
"index_name": "posts_uniq_on_upper_index"
}
],
"schema": null,
"check_constraints": [
{
"name": "price_must_be_positive",
"check": "price > 0",
"attribute": [
"price"
],
"base_filter": "type = 'sponsored'"
}
],
"custom_indexes": [
{
"message": "dude what the heck",
"name": null,
"table": null,
"include": null,
"fields": [
{
"type": "atom",
"value": "uniq_custom_one"
},
{
"type": "atom",
"value": "uniq_custom_two"
}
],
"prefix": null,
"where": null,
"unique": true,
"all_tenants?": false,
"concurrently": true,
"error_fields": [
"uniq_custom_one",
"uniq_custom_two"
],
"nulls_distinct": true,
"using": null
}
],
"base_filter": "type = 'sponsored'",
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"custom_statements": [],
"has_create_action": true
}

View file

@ -0,0 +1,396 @@
{
"attributes": [
{
"default": "fragment(\"gen_random_uuid()\")",
"size": null,
"type": "uuid",
"source": "id",
"references": null,
"generated?": false,
"primary_key?": true,
"allow_nil?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "title_column",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "timestamptz(6)",
"source": "datetime",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "score",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "boolean",
"source": "public",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "citext",
"source": "category",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "\"sponsored\"",
"size": null,
"type": "text",
"source": "type",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "bigint",
"source": "price",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "\"0\"",
"size": null,
"type": "decimal",
"source": "decimal",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "status",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "status",
"source": "status_enum",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": [
"array",
"float"
],
"source": "point",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "custom_point",
"source": "composite_point",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "map",
"source": "stuff",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": [
"array",
"map"
],
"source": "list_of_stuff",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_one",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_two",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_custom_one",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_custom_two",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_on_upper",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uniq_if_contains_foo",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": [
"array",
"text"
],
"source": "list_containing_nils",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "created_at",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "timestamptz(6)",
"source": "updated_at",
"references": null,
"generated?": false,
"primary_key?": false,
"allow_nil?": false
},
{
"default": "nil",
"size": null,
"type": "uuid",
"source": "organization_id",
"references": {
"name": "posts_organization_id_fkey",
"table": "orgs",
"on_delete": null,
"on_update": null,
"destination_attribute_generated": null,
"destination_attribute_default": null,
"primary_key?": true,
"destination_attribute": "id",
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"match_with": null,
"match_type": null,
"schema": "public",
"deferrable": false
},
"generated?": false,
"primary_key?": false,
"allow_nil?": true
},
{
"default": "nil",
"size": null,
"type": "uuid",
"source": "author_id",
"references": {
"name": "posts_author_id_fkey",
"table": "authors",
"on_delete": null,
"on_update": null,
"destination_attribute_generated": null,
"destination_attribute_default": null,
"primary_key?": true,
"destination_attribute": "id",
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"match_with": null,
"match_type": null,
"schema": "public",
"deferrable": false
},
"generated?": false,
"primary_key?": false,
"allow_nil?": true
}
],
"table": "posts",
"hash": "1C2BE60C682696F09AC2505B2B8844DFA449834F0E05B4C69D8E8F40B8C9CA89",
"repo": "Elixir.AshPostgres.TestRepo",
"multitenancy": {
"global": null,
"attribute": null,
"strategy": null
},
"schema": null,
"base_filter": "type = 'sponsored'",
"custom_indexes": [
{
"message": "dude what the heck",
"name": null,
"table": null,
"include": null,
"where": null,
"prefix": null,
"fields": [
{
"type": "atom",
"value": "uniq_custom_one"
},
{
"type": "atom",
"value": "uniq_custom_two"
}
],
"unique": true,
"nulls_distinct": true,
"all_tenants?": false,
"concurrently": true,
"using": null,
"error_fields": [
"uniq_custom_one",
"uniq_custom_two"
]
}
],
"has_create_action": true,
"identities": [
{
"name": "uniq_on_upper",
"keys": [
"UPPER(uniq_on_upper)"
],
"where": null,
"nils_distinct?": true,
"base_filter": "type = 'sponsored'",
"index_name": "posts_uniq_on_upper_index",
"all_tenants?": false
},
{
"name": "uniq_one_and_two",
"keys": [
"uniq_one",
"uniq_two"
],
"where": null,
"nils_distinct?": true,
"base_filter": "type = 'sponsored'",
"index_name": "posts_uniq_one_and_two_index",
"all_tenants?": false
},
{
"name": "uniq_if_contains_foo",
"keys": [
"uniq_if_contains_foo"
],
"where": "(uniq_if_contains_foo LIKE '%foo%')",
"nils_distinct?": true,
"base_filter": "type = 'sponsored'",
"index_name": "posts_uniq_if_contains_foo_index",
"all_tenants?": false
}
],
"custom_statements": [],
"check_constraints": [
{
"name": "price_must_be_positive",
"check": "price > 0",
"attribute": [
"price"
],
"base_filter": "type = 'sponsored'"
}
]
}

View file

@ -0,0 +1,32 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources25 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:posts) do
add(:uniq_on_upper, :text)
end
create(
unique_index(:posts, ["UPPER(uniq_on_upper)"],
where: "type = 'sponsored'",
name: "posts_uniq_on_upper_index"
)
)
end
def down do
drop_if_exists(
unique_index(:posts, ["UPPER(uniq_on_upper)"], name: "posts_uniq_on_upper_index")
)
alter table(:posts) do
remove(:uniq_on_upper)
end
end
end

View file

@ -0,0 +1,54 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources26 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:posts) do
add(:uniq_if_contains_foo, :text)
end
drop_if_exists(
unique_index(:posts, [:"UPPER(uniq_on_upper)"], name: "posts_uniq_on_upper_index")
)
create(
unique_index(:posts, ["UPPER(uniq_on_upper)"],
where: "type = 'sponsored'",
name: "posts_uniq_on_upper_index"
)
)
create(
unique_index(:posts, [:uniq_if_contains_foo],
name: "posts_uniq_if_contains_foo_index",
where: "((uniq_if_contains_foo LIKE '%foo%')) AND (type = 'sponsored')"
)
)
end
def down do
drop_if_exists(
unique_index(:posts, [:uniq_if_contains_foo], name: "posts_uniq_if_contains_foo_index")
)
drop_if_exists(
unique_index(:posts, ["UPPER(uniq_on_upper)"], name: "posts_uniq_on_upper_index")
)
create(
unique_index(:posts, [:"UPPER(uniq_on_upper)"],
where: "type = 'sponsored'",
name: "posts_uniq_on_upper_index"
)
)
alter table(:posts) do
remove(:uniq_if_contains_foo)
end
end
end

View file

@ -155,15 +155,16 @@ defmodule AshPostgres.MigrationGeneratorTest do
assert file_contents =~ ~S[add :second_title, :varchar, size: 16]
# the migration creates unique_indexes based on the identities of the resource
assert file_contents =~ ~S{create unique_index(:posts, [:title], name: "posts_title_index")}
assert file_contents =~
~S{create unique_index(:posts, ["title"], name: "posts_title_index")}
# the migration creates unique_indexes based on the identities of the resource
assert file_contents =~
~S{create unique_index(:posts, [:title, :second_title], name: "posts_thing_index")}
~S{create unique_index(:posts, ["second_title", "title"], name: "posts_thing_index")}
# the migration creates unique_indexes using the `source` on the attributes of the identity on the resource
assert file_contents =~
~S{create unique_index(:posts, [:title, :t_w_s], name: "posts_thing_with_source_index")}
~S{create unique_index(:posts, ["t_w_s", "title"], name: "posts_thing_with_source_index")}
end
end
@ -177,11 +178,19 @@ defmodule AshPostgres.MigrationGeneratorTest do
defposts do
postgres do
migration_types(second_title: {:varchar, 16})
identity_wheres_to_sql(second_title: "(second_title like '%foo%')")
schema("example")
end
identities do
identity(:title, [:title])
identity :second_title, [:second_title] do
nils_distinct?(false)
where expr(contains(second_title, "foo"))
end
end
attributes do
@ -231,6 +240,9 @@ defmodule AshPostgres.MigrationGeneratorTest do
assert file_contents =~ ~S{create index(:posts, ["id"]}
assert file_contents =~
~S{create unique_index(:posts, ["second_title"], name: "posts_second_title_index", prefix: "example", nulls_distinct: false, where: "(second_title like '%foo%')")}
# the migration adds the id, with its default
assert file_contents =~
~S[add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true]
@ -243,7 +255,7 @@ defmodule AshPostgres.MigrationGeneratorTest do
# the migration creates unique_indexes based on the identities of the resource
assert file_contents =~
~S{create unique_index(:posts, [:title], name: "posts_title_index", prefix: "example")}
~S{create unique_index(:posts, ["title"], name: "posts_title_index", prefix: "example")}
end
end
@ -703,18 +715,18 @@ defmodule AshPostgres.MigrationGeneratorTest do
file1_content = File.read!(file1)
assert file1_content =~
"create unique_index(:posts, [:title], name: \"posts_title_index\")"
"create unique_index(:posts, [\"title\"], name: \"posts_title_index\")"
file2_content = File.read!(file2)
assert file2_content =~
"drop_if_exists unique_index(:posts, [:title], name: \"posts_title_index\")"
"drop_if_exists unique_index(:posts, [\"title\"], name: \"posts_title_index\")"
assert file2_content =~
"create unique_index(:posts, [:name], name: \"posts_unique_name_index\")"
"create unique_index(:posts, [\"name\"], name: \"posts_unique_name_index\")"
assert file2_content =~
"create unique_index(:posts, [:title], name: \"posts_unique_title_index\")"
"create unique_index(:posts, [\"title\"], name: \"posts_unique_title_index\")"
end
test "when an attribute exists only on some of the resources that use the same table, it isn't marked as null: false" do
@ -1073,7 +1085,7 @@ defmodule AshPostgres.MigrationGeneratorTest do
assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")
assert File.read!(file) =~
~S{create unique_index(:users, [:name], name: "users_unique_name_index")}
~S{create unique_index(:users, ["name"], name: "users_unique_name_index")}
end
test "when modified, the foreign key is dropped before modification" do

View file

@ -60,6 +60,9 @@ defmodule AshPostgres.Test.Post do
repo(AshPostgres.TestRepo)
base_filter_sql("type = 'sponsored'")
calculations_to_sql(upper_thing: "UPPER(uniq_on_upper)")
identity_wheres_to_sql(uniq_if_contains_foo: "(uniq_if_contains_foo LIKE '%foo%')")
check_constraints do
check_constraint(:price, "price_must_be_positive",
message: "yo, bad price",
@ -186,6 +189,11 @@ defmodule AshPostgres.Test.Post do
identities do
identity(:uniq_one_and_two, [:uniq_one, :uniq_two])
identity(:uniq_on_upper, [:upper_thing])
identity(:uniq_if_contains_foo, [:uniq_if_contains_foo]) do
where expr(contains(title, "foo"))
end
end
attributes do
@ -219,6 +227,8 @@ defmodule AshPostgres.Test.Post do
attribute(:uniq_two, :string, public?: true)
attribute(:uniq_custom_one, :string, public?: true)
attribute(:uniq_custom_two, :string, public?: true)
attribute(:uniq_on_upper, :string, public?: true)
attribute(:uniq_if_contains_foo, :string, public?: true)
attribute :list_containing_nils, {:array, :string} do
public?(true)
@ -335,6 +345,8 @@ defmodule AshPostgres.Test.Post do
end
calculations do
calculate(:upper_thing, :string, expr(fragment("UPPER(?)", uniq_on_upper)))
calculate(
:author_has_post_with_follower_named_fred,
:boolean,