From 6455b0187185d47316897667e546020e4f5c02b0 Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Wed, 2 Sep 2026 14:27:09 +0200 Subject: [PATCH] test: cover multi-hop exists aggregates and exists/2 over relationship paths A multi-hop exists with a ref-less predicate (unfiltered exists aggregate, exists(path, true)) compiled to a subquery that only checked the first relationship in the path, wrongly returning true with zero related rows. Fixed in ash_sql by anchoring every hop of the path. Adds TwoHopExistsTest resources (Entry -> Bucket -> Item) and regression tests: zero-row multi-hop exists, three-hop chains broken at each level, a no_attributes? hop after a broken middle hop, and predicate scoping to the last resource in the path. Co-Authored-By: Claude Fable 5 --- test/aggregate_test.exs | 188 ++++++++++++++++++ test/support/domain.ex | 3 + test/support/two_hop_exists/bucket.ex | 29 +++ test/support/two_hop_exists/entry.ex | 58 ++++++ test/support/two_hop_exists/item.ex | 44 ++++ .../two_hop_buckets/20260901133822.json | 44 ++++ .../20260901133822.json.license | 3 + .../two_hop_entries/20260901133820.json | 77 +++++++ .../20260901133820.json.license | 3 + .../two_hop_items/20260901133821.json | 110 ++++++++++ .../two_hop_items/20260901133821.json.license | 3 + .../two_hop_buckets/20260901133823.json | 44 ++++ .../20260901133823.json.license | 3 + .../two_hop_entries/20260901133821.json | 77 +++++++ .../20260901133821.json.license | 3 + .../two_hop_items/20260901133822.json | 110 ++++++++++ .../two_hop_items/20260901133822.json.license | 3 + .../20260901133819_migrate_resources74.exs | 95 +++++++++ .../20260901133820_migrate_resources74.exs | 95 +++++++++ 19 files changed, 992 insertions(+) create mode 100644 test/support/two_hop_exists/bucket.ex create mode 100644 test/support/two_hop_exists/entry.ex create mode 100644 test/support/two_hop_exists/item.ex create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json.license create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json.license create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json create mode 100644 test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json.license create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json.license create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json.license create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json create mode 100644 test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json.license create mode 100644 test_priv/test_repo/migrations/20260901133819_migrate_resources74.exs create mode 100644 test_priv/test_repo_pg18/migrations/20260901133820_migrate_resources74.exs diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs index 7ecc7c87..1f80fa23 100644 --- a/test/aggregate_test.exs +++ b/test/aggregate_test.exs @@ -6,6 +6,7 @@ defmodule AshSql.AggregateTest do use AshPostgres.RepoCase, async: false import ExUnit.CaptureIO alias AshPostgres.Test.{Author, Chat, Comment, Organization, Post, Rating, User} + alias AshPostgres.Test.TwoHopExistsTest require Ash.Query require Ash.Sort @@ -678,6 +679,193 @@ defmodule AshSql.AggregateTest do refute Post |> Ash.exists?(query: [filter: [title: "non-match"]]) end + + # TwoHopExistsTest: Entry belongs_to :bucket, Bucket has_many :items. + # Regression: exists over a multi-hop path dropped every hop after the + # first when the predicate had no refs (e.g. exists(path, true)). + + test "multi-hop exists aggregate and exists/2 are false with zero related rows" do + bucket = + TwoHopExistsTest.Bucket + |> Ash.Changeset.for_create(:create, %{name: "empty bucket"}) + |> Ash.create!() + + entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "entry", bucket_id: bucket.id}) + |> Ash.create!() + + fields = [:item_count, :has_item_agg?, :has_item_true?, :has_item_id?] + + # the tail hop of the path must be part of the generated query + {sql, _params} = + TwoHopExistsTest.Entry + |> Ash.Query.load(fields) + |> Ash.Query.for_read(:read) + |> Ash.data_layer_query!() + |> Map.get(:query) + |> then(&AshPostgres.TestRepo.to_sql(:all, &1)) + + assert sql =~ "two_hop_items" + + loaded = Ash.load!(entry, fields) + + assert loaded.item_count == 0 + assert loaded.has_item_agg? == false + assert loaded.has_item_true? == false + assert loaded.has_item_id? == false + end + + test "multi-hop exists aggregate and exists/2 are true with one related row" do + bucket = + TwoHopExistsTest.Bucket + |> Ash.Changeset.for_create(:create, %{name: "bucket with item"}) + |> Ash.create!() + + TwoHopExistsTest.Item + |> Ash.Changeset.for_create(:create, %{name: "item", bucket_id: bucket.id}) + |> Ash.create!() + + entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "entry", bucket_id: bucket.id}) + |> Ash.create!() + + loaded = + Ash.load!(entry, [:item_count, :has_item_agg?, :has_item_true?, :has_item_id?]) + + assert loaded.item_count == 1 + assert loaded.has_item_agg? == true + assert loaded.has_item_true? == true + assert loaded.has_item_id? == true + end + + test "three-hop exists aggregate and exists/2 only count fully reachable rows" do + bucket = + TwoHopExistsTest.Bucket + |> Ash.Changeset.for_create(:create, %{name: "bucket"}) + |> Ash.create!() + + entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "entry", bucket_id: bucket.id}) + |> Ash.create!() + + fields = [:has_item_entry_agg?, :has_item_entry_true?] + + # no items at all -> chain broken at hop 2 + loaded = Ash.load!(entry, fields) + assert loaded.has_item_entry_agg? == false + assert loaded.has_item_entry_true? == false + + # item exists but has no entry -> chain broken at hop 3 + item = + TwoHopExistsTest.Item + |> Ash.Changeset.for_create(:create, %{name: "item", bucket_id: bucket.id}) + |> Ash.create!() + + loaded = Ash.load!(entry, fields) + assert loaded.has_item_entry_agg? == false + assert loaded.has_item_entry_true? == false + + # full chain reachable + item + |> Ash.Changeset.for_update(:update, %{entry_id: entry.id}) + |> Ash.update!() + + loaded = Ash.load!(entry, fields) + assert loaded.has_item_entry_agg? == true + assert loaded.has_item_entry_true? == true + end + + test "a broken middle hop is not masked by a no_attributes? hop later in the path" do + # the no_attributes? (on: true) join would find this row if the broken + # middle hop were not anchored + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "other entry"}) + |> Ash.create!() + + bucket = + TwoHopExistsTest.Bucket + |> Ash.Changeset.for_create(:create, %{name: "bucket without items"}) + |> Ash.create!() + + entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "entry", bucket_id: bucket.id}) + |> Ash.create!() + + fields = [:has_item_any_entry_agg?, :has_item_any_entry_true?] + + # bucket has no items -> chain broken at hop 2, despite entries existing + loaded = Ash.load!(entry, fields) + assert loaded.has_item_any_entry_agg? == false + assert loaded.has_item_any_entry_true? == false + + # once an item exists, the no_attributes? hop is reachable + TwoHopExistsTest.Item + |> Ash.Changeset.for_create(:create, %{name: "item", bucket_id: bucket.id}) + |> Ash.create!() + + loaded = Ash.load!(entry, fields) + assert loaded.has_item_any_entry_agg? == true + assert loaded.has_item_any_entry_true? == true + end + + test "single-hop exists aggregate and exists/2 are false with zero related rows" do + entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "entry without items"}) + |> Ash.create!() + + loaded = Ash.load!(entry, [:has_direct_item_agg?, :has_direct_item_true?]) + + assert loaded.has_direct_item_agg? == false + assert loaded.has_direct_item_true? == false + end + + test "multi-hop exists predicate is scoped to the last resource in the path" do + # every table in the chain has a `name` column, and every row except + # the leaf is named "target" - so this only passes if the predicate + # binds to the leaf and not to an earlier hop (or the outer row) + bucket = + TwoHopExistsTest.Bucket + |> Ash.Changeset.for_create(:create, %{name: "target"}) + |> Ash.create!() + + leaf_entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "leaf"}) + |> Ash.create!() + + TwoHopExistsTest.Item + |> Ash.Changeset.for_create(:create, %{ + name: "target", + bucket_id: bucket.id, + entry_id: leaf_entry.id + }) + |> Ash.create!() + + outer_entry = + TwoHopExistsTest.Entry + |> Ash.Changeset.for_create(:create, %{name: "target", bucket_id: bucket.id}) + |> Ash.create!() + + refute TwoHopExistsTest.Entry + |> Ash.Query.filter(exists(bucket.items.entry, name == "target")) + |> Ash.read_one!() + + leaf_entry + |> Ash.Changeset.for_update(:update, %{name: "target"}) + |> Ash.update!() + + assert %{id: outer_id} = + TwoHopExistsTest.Entry + |> Ash.Query.filter(exists(bucket.items.entry, name == "target")) + |> Ash.read_one!() + + assert outer_id == outer_entry.id + end end describe "list" do diff --git a/test/support/domain.ex b/test/support/domain.ex index 0d58c33a..22a380c9 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -80,6 +80,9 @@ defmodule AshPostgres.Test.Domain do resource(AshPostgres.Test.LabelledContent) resource(AshPostgres.Test.BulkUpsertParent) resource(AshPostgres.Test.BulkUpsertChild) + resource(AshPostgres.Test.TwoHopExistsTest.Entry) + resource(AshPostgres.Test.TwoHopExistsTest.Bucket) + resource(AshPostgres.Test.TwoHopExistsTest.Item) end authorization do diff --git a/test/support/two_hop_exists/bucket.ex b/test/support/two_hop_exists/bucket.ex new file mode 100644 index 00000000..e5bfa8f1 --- /dev/null +++ b/test/support/two_hop_exists/bucket.ex @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.Test.TwoHopExistsTest.Bucket do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.Test.Domain, + data_layer: AshPostgres.DataLayer + + postgres do + table("two_hop_buckets") + repo(AshPostgres.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + relationships do + has_many(:items, AshPostgres.Test.TwoHopExistsTest.Item, public?: true) + end +end diff --git a/test/support/two_hop_exists/entry.ex b/test/support/two_hop_exists/entry.ex new file mode 100644 index 00000000..106cae70 --- /dev/null +++ b/test/support/two_hop_exists/entry.ex @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.Test.TwoHopExistsTest.Entry do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.Test.Domain, + data_layer: AshPostgres.DataLayer + + postgres do + table("two_hop_entries") + repo(AshPostgres.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + relationships do + belongs_to(:bucket, AshPostgres.Test.TwoHopExistsTest.Bucket, + public?: true, + attribute_writable?: true + ) + + has_many(:direct_items, AshPostgres.Test.TwoHopExistsTest.Item, + public?: true, + destination_attribute: :entry_id + ) + end + + aggregates do + count(:item_count, [:bucket, :items]) + exists(:has_item_agg?, [:bucket, :items]) + exists(:has_direct_item_agg?, [:direct_items]) + exists(:has_item_entry_agg?, [:bucket, :items, :entry]) + exists(:has_item_any_entry_agg?, [:bucket, :items, :any_entries]) + end + + calculations do + calculate(:has_item_true?, :boolean, expr(exists(bucket.items, true))) + calculate(:has_item_id?, :boolean, expr(exists(bucket.items, not is_nil(id)))) + calculate(:has_direct_item_true?, :boolean, expr(exists(direct_items, true))) + calculate(:has_item_entry_true?, :boolean, expr(exists(bucket.items.entry, true))) + + calculate( + :has_item_any_entry_true?, + :boolean, + expr(exists(bucket.items.any_entries, true)) + ) + end +end diff --git a/test/support/two_hop_exists/item.ex b/test/support/two_hop_exists/item.ex new file mode 100644 index 00000000..6b16a1cd --- /dev/null +++ b/test/support/two_hop_exists/item.ex @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.Test.TwoHopExistsTest.Item do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.Test.Domain, + data_layer: AshPostgres.DataLayer + + postgres do + table("two_hop_items") + repo(AshPostgres.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + relationships do + belongs_to(:bucket, AshPostgres.Test.TwoHopExistsTest.Bucket, + public?: true, + attribute_writable?: true + ) + + # Used for the single-hop comparison aggregates on Entry + belongs_to(:entry, AshPostgres.Test.TwoHopExistsTest.Entry, + public?: true, + attribute_writable?: true + ) + + # on: true join — exercises exists paths whose hops are not FK-joined + has_many :any_entries, AshPostgres.Test.TwoHopExistsTest.Entry do + public?(true) + no_attributes?(true) + end + end +end diff --git a/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json b/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json new file mode 100644 index 00000000..de00ceef --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json @@ -0,0 +1,44 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "8702A26443EEB6064F107B0FFC83D5A6F3D084447308616B3253ECBC76CC3320", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_buckets" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json.license b/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_buckets/20260901133822.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json b/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json new file mode 100644 index 00000000..98d8252f --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json @@ -0,0 +1,77 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_entries_bucket_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_buckets" + }, + "scale": null, + "size": null, + "source": "bucket_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "A9F45FCF929ADCFAD3AADBC394B52B22CBE03E113097C6ABF5D141240AC1DF01", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_entries" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json.license b/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_entries/20260901133820.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json b/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json new file mode 100644 index 00000000..12e955a1 --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json @@ -0,0 +1,110 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_items_bucket_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_buckets" + }, + "scale": null, + "size": null, + "source": "bucket_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_items_entry_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_entries" + }, + "scale": null, + "size": null, + "source": "entry_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "88487AEF75EE84E45C9F6B6F27784C3A4429AC8BFDFC8C049DED6F492D244790", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_items" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json.license b/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots/test_repo/two_hop_items/20260901133821.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json b/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json new file mode 100644 index 00000000..de00ceef --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json @@ -0,0 +1,44 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "8702A26443EEB6064F107B0FFC83D5A6F3D084447308616B3253ECBC76CC3320", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_buckets" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json.license b/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_buckets/20260901133823.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json b/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json new file mode 100644 index 00000000..98d8252f --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json @@ -0,0 +1,77 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_entries_bucket_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_buckets" + }, + "scale": null, + "size": null, + "source": "bucket_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "A9F45FCF929ADCFAD3AADBC394B52B22CBE03E113097C6ABF5D141240AC1DF01", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_entries" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json.license b/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_entries/20260901133821.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json b/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json new file mode 100644 index 00000000..12e955a1 --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json @@ -0,0 +1,110 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_items_bucket_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_buckets" + }, + "scale": null, + "size": null, + "source": "bucket_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "index_where": null, + "match_tenant?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "two_hop_items_entry_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "two_hop_entries" + }, + "scale": null, + "size": null, + "source": "entry_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "create_table_options": null, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "88487AEF75EE84E45C9F6B6F27784C3A4429AC8BFDFC8C049DED6F492D244790", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "two_hop_items" +} \ No newline at end of file diff --git a/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json.license b/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json.license new file mode 100644 index 00000000..c5bd9989 --- /dev/null +++ b/test_priv/resource_snapshots_pg18/test_repo/two_hop_items/20260901133822.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/test_priv/test_repo/migrations/20260901133819_migrate_resources74.exs b/test_priv/test_repo/migrations/20260901133819_migrate_resources74.exs new file mode 100644 index 00000000..ce833573 --- /dev/null +++ b/test_priv/test_repo/migrations/20260901133819_migrate_resources74.exs @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.TestRepo.Migrations.MigrateResources74 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 + create table(:two_hop_entries, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:name, :text) + add(:bucket_id, :uuid) + end + + create table(:two_hop_items, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:name, :text) + add(:bucket_id, :uuid) + + add( + :entry_id, + references(:two_hop_entries, + column: :id, + name: "two_hop_items_entry_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + create table(:two_hop_buckets, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + end + + alter table(:two_hop_entries) do + modify( + :bucket_id, + references(:two_hop_buckets, + column: :id, + name: "two_hop_entries_bucket_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + alter table(:two_hop_items) do + modify( + :bucket_id, + references(:two_hop_buckets, + column: :id, + name: "two_hop_items_bucket_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + alter table(:two_hop_buckets) do + add(:name, :text) + end + end + + def down do + alter table(:two_hop_buckets) do + remove(:name) + end + + drop(constraint(:two_hop_items, "two_hop_items_entry_id_fkey")) + + drop(constraint(:two_hop_items, "two_hop_items_bucket_id_fkey")) + + alter table(:two_hop_items) do + modify(:bucket_id, :uuid) + end + + drop(constraint(:two_hop_entries, "two_hop_entries_bucket_id_fkey")) + + alter table(:two_hop_entries) do + modify(:bucket_id, :uuid) + end + + drop(table(:two_hop_buckets)) + + drop(table(:two_hop_items)) + + drop(table(:two_hop_entries)) + end +end diff --git a/test_priv/test_repo_pg18/migrations/20260901133820_migrate_resources74.exs b/test_priv/test_repo_pg18/migrations/20260901133820_migrate_resources74.exs new file mode 100644 index 00000000..ce833573 --- /dev/null +++ b/test_priv/test_repo_pg18/migrations/20260901133820_migrate_resources74.exs @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.TestRepo.Migrations.MigrateResources74 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 + create table(:two_hop_entries, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:name, :text) + add(:bucket_id, :uuid) + end + + create table(:two_hop_items, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:name, :text) + add(:bucket_id, :uuid) + + add( + :entry_id, + references(:two_hop_entries, + column: :id, + name: "two_hop_items_entry_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + create table(:two_hop_buckets, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + end + + alter table(:two_hop_entries) do + modify( + :bucket_id, + references(:two_hop_buckets, + column: :id, + name: "two_hop_entries_bucket_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + alter table(:two_hop_items) do + modify( + :bucket_id, + references(:two_hop_buckets, + column: :id, + name: "two_hop_items_bucket_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + + alter table(:two_hop_buckets) do + add(:name, :text) + end + end + + def down do + alter table(:two_hop_buckets) do + remove(:name) + end + + drop(constraint(:two_hop_items, "two_hop_items_entry_id_fkey")) + + drop(constraint(:two_hop_items, "two_hop_items_bucket_id_fkey")) + + alter table(:two_hop_items) do + modify(:bucket_id, :uuid) + end + + drop(constraint(:two_hop_entries, "two_hop_entries_bucket_id_fkey")) + + alter table(:two_hop_entries) do + modify(:bucket_id, :uuid) + end + + drop(table(:two_hop_buckets)) + + drop(table(:two_hop_items)) + + drop(table(:two_hop_entries)) + end +end