Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions test/aggregate_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down Expand Up @@ -678,6 +679,193 @@

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

Check failure on line 687 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test exists multi-hop exists aggregate and exists/2 are false with zero related rows (AshSql.AggregateTest)

Check failure on line 687 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (17) / mix test

test exists multi-hop exists aggregate and exists/2 are false with zero related rows (AshSql.AggregateTest)

Check failure on line 687 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (18) / mix test

test exists multi-hop exists aggregate and exists/2 are false with zero related rows (AshSql.AggregateTest)

Check failure on line 687 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test exists multi-hop exists aggregate and exists/2 are false with zero related rows (AshSql.AggregateTest)

Check failure on line 687 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test exists multi-hop exists aggregate and exists/2 are false with zero related rows (AshSql.AggregateTest)
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

Check failure on line 743 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test exists three-hop exists aggregate and exists/2 only count fully reachable rows (AshSql.AggregateTest)

Check failure on line 743 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (17) / mix test

test exists three-hop exists aggregate and exists/2 only count fully reachable rows (AshSql.AggregateTest)

Check failure on line 743 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (18) / mix test

test exists three-hop exists aggregate and exists/2 only count fully reachable rows (AshSql.AggregateTest)

Check failure on line 743 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test exists three-hop exists aggregate and exists/2 only count fully reachable rows (AshSql.AggregateTest)

Check failure on line 743 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test exists three-hop exists aggregate and exists/2 only count fully reachable rows (AshSql.AggregateTest)
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

Check failure on line 781 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test exists a broken middle hop is not masked by a no_attributes? hop later in the path (AshSql.AggregateTest)

Check failure on line 781 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (17) / mix test

test exists a broken middle hop is not masked by a no_attributes? hop later in the path (AshSql.AggregateTest)

Check failure on line 781 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (18) / mix test

test exists a broken middle hop is not masked by a no_attributes? hop later in the path (AshSql.AggregateTest)

Check failure on line 781 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test exists a broken middle hop is not masked by a no_attributes? hop later in the path (AshSql.AggregateTest)

Check failure on line 781 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test exists a broken middle hop is not masked by a no_attributes? hop later in the path (AshSql.AggregateTest)
# 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
Expand Down
3 changes: 3 additions & 0 deletions test/support/domain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions test/support/two_hop_exists/bucket.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs/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
58 changes: 58 additions & 0 deletions test/support/two_hop_exists/entry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs/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
44 changes: 44 additions & 0 deletions test/support/two_hop_exists/item.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs/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
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs/contributors>

SPDX-License-Identifier: MIT
Loading
Loading