diff --git a/priv/resource_snapshots/test_repo/posts/20250521105654.json b/priv/resource_snapshots/test_repo/posts/20250521105654.json new file mode 100644 index 00000000..ade3092c --- /dev/null +++ b/priv/resource_snapshots/test_repo/posts/20250521105654.json @@ -0,0 +1,600 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "1", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "version", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "title_column", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "not_selected_by_default", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "datetime", + "type": "timestamptz(6)" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "score", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "limited_score", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "public", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "category", + "type": "citext" + }, + { + "allow_nil?": true, + "default": "\"sponsored\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "price", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "\"0\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "decimal", + "type": "decimal" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "status", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "status_enum", + "type": "status" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "metadata", + "type": "map" + }, + { + "allow_nil?": false, + "default": "2", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "constrained_int", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "point", + "type": [ + "array", + "float" + ] + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "composite_point", + "type": "custom_point" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "string_point", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "person_detail", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "stuff", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "list_of_stuff", + "type": [ + "array", + "map" + ] + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_one", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_two", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_custom_one", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_custom_two", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_on_upper", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "uniq_if_contains_foo", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "model", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "list_containing_nils", + "type": [ + "array", + "text" + ] + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "ltree_unescaped", + "type": "ltree" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "ltree_escaped", + "type": "ltree" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "timestamptz(6)" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "posts_organization_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "orgs" + }, + "size": null, + "source": "organization_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "posts_parent_post_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "posts" + }, + "size": null, + "source": "parent_post_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "posts_author_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "authors" + }, + "size": null, + "source": "author_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "posts_db_point_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "points" + }, + "size": null, + "source": "db_point_id", + "type": [ + "array", + "float" + ] + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "posts_db_string_point_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "string_points" + }, + "size": null, + "source": "db_string_point_id", + "type": "text" + } + ], + "base_filter": "type = 'sponsored'", + "check_constraints": [ + { + "attribute": [ + "price" + ], + "base_filter": "type = 'sponsored'", + "check": "price > 0", + "name": "price_must_be_positive" + } + ], + "custom_indexes": [ + { + "all_tenants?": false, + "concurrently": true, + "error_fields": [ + "uniq_custom_one", + "uniq_custom_two" + ], + "fields": [ + { + "type": "atom", + "value": "uniq_custom_one" + }, + { + "type": "atom", + "value": "uniq_custom_two" + } + ], + "include": null, + "message": "dude what the heck", + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "5925C6C41F5781689EA91BF2237C6F4E321FD0C969B2D7751265E448BF3236B7", + "identities": [ + { + "all_tenants?": false, + "base_filter": "type = 'sponsored'", + "index_name": "posts_uniq_if_contains_foo_index", + "keys": [ + { + "type": "atom", + "value": "uniq_if_contains_foo" + } + ], + "name": "uniq_if_contains_foo", + "nils_distinct?": true, + "where": "(uniq_if_contains_foo LIKE '%foo%')" + }, + { + "all_tenants?": false, + "base_filter": "type = 'sponsored'", + "index_name": "posts_uniq_on_upper_index", + "keys": [ + { + "type": "string", + "value": "(UPPER(uniq_on_upper))" + } + ], + "name": "uniq_on_upper", + "nils_distinct?": true, + "where": null + }, + { + "all_tenants?": false, + "base_filter": "type = 'sponsored'", + "index_name": "posts_uniq_one_and_two_index", + "keys": [ + { + "type": "atom", + "value": "uniq_one" + }, + { + "type": "atom", + "value": "uniq_two" + } + ], + "name": "uniq_one_and_two", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "posts" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20250521105654_add_model_tuple_to_post.exs b/priv/test_repo/migrations/20250521105654_add_model_tuple_to_post.exs new file mode 100644 index 00000000..bde744b0 --- /dev/null +++ b/priv/test_repo/migrations/20250521105654_add_model_tuple_to_post.exs @@ -0,0 +1,21 @@ +defmodule AshPostgres.TestRepo.Migrations.AddModelTupleToPost 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(:model, :map, null: false) + end + end + + def down do + alter table(:posts) do + remove(:model) + end + end +end diff --git a/test/support/domain.ex b/test/support/domain.ex index eb1f047c..805a1dce 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -4,7 +4,9 @@ defmodule AshPostgres.Test.Domain do resources do resource(AshPostgres.Test.CoAuthorPost) - resource(AshPostgres.Test.Post) + resource(AshPostgres.Test.Post) do + define :review, action: :review + end resource(AshPostgres.Test.Comedian) resource(AshPostgres.Test.Comment) resource(AshPostgres.Test.CommentLink) diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index d2119d2a..ca293390 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -326,6 +326,9 @@ defmodule AshPostgres.Test.Post do require_atomic?(false) end + update :atomic_update do + end + update :update_if_author do require_atomic?(false) end @@ -413,6 +416,19 @@ defmodule AshPostgres.Test.Post do change(atomic_update(:score, expr((score || 0) + ^arg(:amount)))) end + update :review do + change(after_action(fn changeset, record, _context -> + new_model = {1.0, 2.0, 3.0} + + record + |> Ash.Changeset.for_update(:atomic_update) + |> Ash.Changeset.force_change_attribute(:model, new_model) + |> Ash.update() + end)) + + require_atomic?(false) + end + update :requires_initial_data do argument(:amount, :integer, default: 1) change(atomic_update(:score, expr((score || 0) + ^arg(:amount)))) @@ -508,6 +524,18 @@ defmodule AshPostgres.Test.Post do attribute(:uniq_on_upper, :string, public?: true) attribute(:uniq_if_contains_foo, :string, public?: true) + attribute :model, :tuple do + constraints [ + fields: [ + alpha: [type: :float, description: "The alpha field"], + beta: [type: :float, description: "The beta field"], + t: [type: :float, description: "The t field"] + ] + ] + allow_nil? false + default fn -> {3.0, 3.0, 1.0} end + end + attribute :list_containing_nils, {:array, :string} do public?(true) constraints(nil_items?: true) diff --git a/test/tuple_test.exs b/test/tuple_test.exs index 83b412f6..1233ac28 100644 --- a/test/tuple_test.exs +++ b/test/tuple_test.exs @@ -18,6 +18,20 @@ defmodule AshPostgres.Test.TupleTest do assert elem(post.person_detail, 1) == "Doe" end + test "tuple type with force_attribute_change can be updated" do + post = + Post + |> Ash.Changeset.for_create(:create, %{ + title: "Tuple Test", + score: 1 + }) + |> Ash.create!() + + post = AshPostgres.Test.Domain.review!(post) + + assert post.model == {1.0, 2.0, 3.0} + end + test "tuple type can be filtered by exact match" do # Create first post Post