Skip to content

Commit 8e32e0a

Browse files
committed
fix: ensure that context multitenancy is properly applied to lateral many-to-many joins
1 parent 2c31b3e commit 8e32e0a

File tree

10 files changed

+235
-13
lines changed

10 files changed

+235
-13
lines changed

.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ spark_locals_without_parens = [
1919
include: 1,
2020
index: 1,
2121
index: 2,
22+
index?: 1,
2223
match_type: 1,
2324
match_with: 1,
2425
message: 1,

documentation/dsls/DSL:-AshPostgres.DataLayer.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,11 @@ reference :post, on_delete: :delete, on_update: :update, name: "comments_to_post
301301
| [`ignore?`](#postgres-references-reference-ignore?){: #postgres-references-reference-ignore? } | `boolean` | | If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way |
302302
| [`on_delete`](#postgres-references-reference-on_delete){: #postgres-references-reference-on_delete } | `:delete \| :nilify \| :nothing \| :restrict \| {:nilify, atom \| list(atom)}` | | What should happen to records of this resource when the referenced record of the *destination* resource is deleted. |
303303
| [`on_update`](#postgres-references-reference-on_update){: #postgres-references-reference-on_update } | `:update \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. |
304-
| [`deferrable`](#postgres-references-reference-deferrable){: #postgres-references-reference-deferrable } | `false \| true \| :initially` | `false` | Wether or not the constraint is deferrable. This only affects the migration generator. |
304+
| [`deferrable`](#postgres-references-reference-deferrable){: #postgres-references-reference-deferrable } | `false \| true \| :initially` | `false` | Whether or not the constraint is deferrable. This only affects the migration generator. |
305305
| [`name`](#postgres-references-reference-name){: #postgres-references-reference-name } | `String.t` | | The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey |
306306
| [`match_with`](#postgres-references-reference-match_with){: #postgres-references-reference-match_with } | `keyword` | | Defines additional keys to the foreign key in order to build a composite foreign key. The key should be the name of the source attribute (in the current resource), the value the name of the destination attribute. |
307307
| [`match_type`](#postgres-references-reference-match_type){: #postgres-references-reference-match_type } | `:simple \| :partial \| :full` | | select if the match is `:simple`, `:partial`, or `:full` |
308+
| [`index?`](#postgres-references-reference-index?){: #postgres-references-reference-index? } | `boolean` | `false` | Whether to create or not a corresponding index |
308309

309310

310311

lib/data_layer.ex

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,11 +1270,17 @@ defmodule AshPostgres.DataLayer do
12701270
end
12711271

12721272
defp lateral_join_source_query(
1273-
%{__ash_bindings__: %{lateral_join_source_query: lateral_join_source_query}},
1274-
_
1273+
%{
1274+
__ash_bindings__: %{
1275+
lateral_join_source_query: lateral_join_source_query
1276+
}
1277+
},
1278+
source_query
12751279
)
12761280
when not is_nil(lateral_join_source_query) do
1277-
{:ok, lateral_join_source_query}
1281+
{:ok,
1282+
lateral_join_source_query
1283+
|> set_subquery_prefix(source_query, lateral_join_source_query.__ash_bindings__.resource)}
12781284
end
12791285

12801286
defp lateral_join_source_query(query, source_query) do

lib/migration_generator/migration_generator.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,8 +1777,10 @@ defmodule AshPostgres.MigrationGenerator do
17771777
attribute.source == old_attribute.source
17781778
end)
17791779

1780-
has_removed_index? = attribute && !attribute[:index?] && old_attribute[:index?]
1781-
attribute_doesnt_exist? = !attribute && old_attribute[:index?]
1780+
has_removed_index? =
1781+
attribute && !attribute[:references][:index?] && old_attribute[:references][:index?]
1782+
1783+
attribute_doesnt_exist? = !attribute && old_attribute[:references][:index?]
17821784

17831785
has_removed_index? || attribute_doesnt_exist?
17841786
end)
@@ -3173,6 +3175,7 @@ defmodule AshPostgres.MigrationGenerator do
31733175
|> Map.put_new(:on_update, nil)
31743176
|> Map.update!(:on_delete, &(&1 && load_references_on_delete(&1)))
31753177
|> Map.update!(:on_update, &(&1 && maybe_to_atom(&1)))
3178+
|> Map.put_new(:index?, false)
31763179
|> Map.put_new(:match_with, nil)
31773180
|> Map.put_new(:match_type, nil)
31783181
|> Map.update!(
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"attributes": [
3+
{
4+
"default": "nil",
5+
"size": null,
6+
"type": "uuid",
7+
"source": "source_id",
8+
"references": {
9+
"name": "friend_links_source_id_fkey",
10+
"table": "multitenant_posts",
11+
"multitenancy": {
12+
"global": false,
13+
"attribute": null,
14+
"strategy": "context"
15+
},
16+
"destination_attribute": "id",
17+
"primary_key?": true,
18+
"schema": "public",
19+
"on_delete": null,
20+
"on_update": null,
21+
"deferrable": false,
22+
"match_with": null,
23+
"match_type": null,
24+
"index?": false,
25+
"destination_attribute_default": null,
26+
"destination_attribute_generated": null
27+
},
28+
"primary_key?": true,
29+
"allow_nil?": false,
30+
"generated?": false
31+
},
32+
{
33+
"default": "nil",
34+
"size": null,
35+
"type": "uuid",
36+
"source": "dest_id",
37+
"references": {
38+
"name": "friend_links_dest_id_fkey",
39+
"table": "multitenant_posts",
40+
"multitenancy": {
41+
"global": false,
42+
"attribute": null,
43+
"strategy": "context"
44+
},
45+
"destination_attribute": "id",
46+
"primary_key?": true,
47+
"schema": "public",
48+
"on_delete": null,
49+
"on_update": null,
50+
"deferrable": false,
51+
"match_with": null,
52+
"match_type": null,
53+
"index?": false,
54+
"destination_attribute_default": null,
55+
"destination_attribute_generated": null
56+
},
57+
"primary_key?": true,
58+
"allow_nil?": false,
59+
"generated?": false
60+
}
61+
],
62+
"table": "friend_links",
63+
"hash": "880BED202EB36FA2543D5DCC25DE1373676CE38CECFA2C8B5651757FFF3817EF",
64+
"repo": "Elixir.AshPostgres.TestRepo",
65+
"multitenancy": {
66+
"global": false,
67+
"attribute": null,
68+
"strategy": "context"
69+
},
70+
"schema": null,
71+
"check_constraints": [],
72+
"identities": [],
73+
"custom_indexes": [],
74+
"base_filter": null,
75+
"custom_statements": [],
76+
"has_create_action": true
77+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule AshPostgres.TestRepo.TenantMigrations.MigrateResources3 do
2+
@moduledoc """
3+
Updates resources based on their most recent snapshots.
4+
5+
This file was autogenerated with `mix ash_postgres.generate_migrations`
6+
"""
7+
8+
use Ecto.Migration
9+
10+
def up do
11+
create table(:friend_links, primary_key: false, prefix: prefix()) do
12+
add(
13+
:source_id,
14+
references(:multitenant_posts,
15+
column: :id,
16+
name: "friend_links_source_id_fkey",
17+
type: :uuid,
18+
prefix: prefix()
19+
),
20+
primary_key: true,
21+
null: false
22+
)
23+
24+
add(
25+
:dest_id,
26+
references(:multitenant_posts,
27+
column: :id,
28+
name: "friend_links_dest_id_fkey",
29+
type: :uuid,
30+
prefix: prefix()
31+
),
32+
primary_key: true,
33+
null: false
34+
)
35+
end
36+
end
37+
38+
def down do
39+
drop(constraint(:friend_links, "friend_links_source_id_fkey"))
40+
41+
drop(constraint(:friend_links, "friend_links_dest_id_fkey"))
42+
43+
drop(table(:friend_links, prefix: prefix()))
44+
end
45+
end

test/multitenancy_test.exs

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,23 +109,74 @@ defmodule AshPostgres.Test.MultitenancyTest do
109109
|> Ash.Changeset.new()
110110
|> Ash.create!()
111111

112-
user1 =
112+
user =
113113
User
114-
|> Ash.Changeset.for_create(:create, %{name: "a"})
114+
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
115115
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
116116
|> Ash.create!()
117117

118118
user2 =
119119
User
120-
|> Ash.Changeset.for_create(:create, %{name: "b"})
120+
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
121121
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
122122
|> Ash.create!()
123123

124-
user1_id = user1.id
125-
user2_id = user2.id
124+
post =
125+
Post
126+
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
127+
authorize?: false,
128+
tenant: "org_#{org.id}"
129+
)
130+
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
131+
|> Ash.create!()
126132

127-
assert [%{id: ^user1_id}, %{id: ^user2_id}] =
128-
Ash.load!(org, users: Ash.Query.sort(User, :name)).users
133+
post_id = post.id
134+
135+
assert [%{posts: [%{id: ^post_id}]}, _] =
136+
Ash.load!([user, user2], [posts: Ash.Query.limit(Post, 2)],
137+
tenant: "org_#{org.id}",
138+
authorize?: false
139+
)
140+
end
141+
142+
test "loading context multitenant resources across a many-to-many with a limit works" do
143+
org =
144+
Org
145+
|> Ash.Changeset.new()
146+
|> Ash.create!()
147+
148+
user =
149+
User
150+
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
151+
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
152+
|> Ash.create!()
153+
154+
post =
155+
Post
156+
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
157+
authorize?: false,
158+
tenant: "org_#{org.id}"
159+
)
160+
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
161+
|> Ash.create!()
162+
163+
post2 =
164+
Post
165+
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
166+
authorize?: false,
167+
tenant: "org_#{org.id}"
168+
)
169+
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
170+
|> Ash.Changeset.manage_relationship(:linked_posts, post, type: :append_and_remove)
171+
|> Ash.create!()
172+
173+
post_id = post.id
174+
175+
assert [%{linked_posts: [%{id: ^post_id}]}, _] =
176+
Ash.load!([post2, post], [linked_posts: Ash.Query.limit(Post, 2)],
177+
tenant: "org_#{org.id}",
178+
authorize?: false
179+
)
129180
end
130181

131182
test "manage_relationship from context multitenant resource to attribute multitenant resource doesn't raise an error" do

test/support/multitenancy/domain.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ defmodule AshPostgres.MultitenancyTest.Domain do
66
resource(AshPostgres.MultitenancyTest.Org)
77
resource(AshPostgres.MultitenancyTest.User)
88
resource(AshPostgres.MultitenancyTest.Post)
9+
resource(AshPostgres.MultitenancyTest.PostLink)
910
end
1011
end

test/support/multitenancy/resources/post.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ defmodule AshPostgres.MultitenancyTest.Post do
5050
end
5151

5252
has_one(:self, __MODULE__, destination_attribute: :id, source_attribute: :id, public?: true)
53+
54+
many_to_many :linked_posts, __MODULE__ do
55+
through(AshPostgres.MultitenancyTest.PostLink)
56+
source_attribute_on_join_resource(:source_id)
57+
destination_attribute_on_join_resource(:dest_id)
58+
end
5359
end
5460

5561
calculations do
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule AshPostgres.MultitenancyTest.PostLink do
2+
@moduledoc false
3+
use Ash.Resource,
4+
domain: AshPostgres.MultitenancyTest.Domain,
5+
data_layer: AshPostgres.DataLayer
6+
7+
postgres do
8+
table "friend_links"
9+
repo AshPostgres.TestRepo
10+
end
11+
12+
multitenancy do
13+
strategy(:context)
14+
end
15+
16+
actions do
17+
defaults([:read, :destroy, create: :*, update: :*])
18+
end
19+
20+
relationships do
21+
belongs_to(:source, AshPostgres.MultitenancyTest.Post,
22+
primary_key?: true,
23+
allow_nil?: false
24+
)
25+
26+
belongs_to(:dest, AshPostgres.MultitenancyTest.Post,
27+
primary_key?: true,
28+
allow_nil?: false
29+
)
30+
end
31+
end

0 commit comments

Comments
 (0)