Controlling tenancy when joining resources in queries

I have been working on a user-facing query builder in my application. I have a resource Atlas.Tracking.Entity that I configured the multitenancy like so:
multitenancy do
strategy :context
global? true
end
multitenancy do
strategy :context
global? true
end
This is so that I can have global entities and tenant-scoped entities. The actual query parsing is a bit complex, but boils down to using ref to set up fields and then assigning criteria. Where it starts to fall apart is that these entities have a parent relationship as well as a children relationship. If I define a field in the query like ref([:parent], :label) it assumes I'm talking about the global entities table and not the entities table on the same tenant as the query I'm building for. This results in an SQL query like the following:
SELECT DISTINCT ON (e0."id")
-- ...
FROM
"client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e0
LEFT OUTER JOIN "public"."entities" AS e1 ON e0."id" = e1."parent_id"
LEFT OUTER JOIN "public"."entities" AS e2 ON e0."parent_id" = e2."id"
WHERE
-- ...
SELECT DISTINCT ON (e0."id")
-- ...
FROM
"client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e0
LEFT OUTER JOIN "public"."entities" AS e1 ON e0."id" = e1."parent_id"
LEFT OUTER JOIN "public"."entities" AS e2 ON e0."parent_id" = e2."id"
WHERE
-- ...
Whereas I'd expect something like:
SELECT DISTINCT ON (e0."id")
-- ...
FROM
"client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e0
LEFT OUTER JOIN "client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e1 ON e0."id" = e1."parent_id"
LEFT OUTER JOIN "client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e2 ON e0."parent_id" = e2."id"
WHERE
-- ...
SELECT DISTINCT ON (e0."id")
-- ...
FROM
"client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e0
LEFT OUTER JOIN "client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e1 ON e0."id" = e1."parent_id"
LEFT OUTER JOIN "client_01972299-b456-75f1-a634-7de10674c28b"."entities" AS e2 ON e0."parent_id" = e2."id"
WHERE
-- ...
I realize this is probably a bit non-standard, but I was wondering if there's a way that I could assign the tenancy of the joined relationship in the Ash query builder. I'll keep searching the docs in the meantime. Thanks!
Solution:
ok so it looks like I goofed. it's mostly working as described. I might have changed multiple things:
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!()
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!()
...
Jump to solution
11 Replies
ZachDaniel
ZachDaniel•5mo ago
It should happen automatically Oh, I see though šŸ¤”
Mylan Connolly
Mylan ConnollyOP•5mo ago
yeah I think since tenancy is optional it's probably unusual / difficult for Ash to automatically figure out what I want. things like loads work properly, though so I think I assumed queries would too lol
ZachDaniel
ZachDaniel•5mo ago
šŸ¤” so we've talked about adding ways to control this, but in filters it gets kind of complicated because it all stems from the tenant of the parent query
Mylan Connolly
Mylan ConnollyOP•5mo ago
so roughly how my logic works is I have something like:
Atlas.Tracking.Entity
|> Ash.Query.for_read(:read)
|> Atlas.QueryParser.add_to_query(json_query)
|> Ash.read()
Atlas.Tracking.Entity
|> Ash.Query.for_read(:read)
|> Atlas.QueryParser.add_to_query(json_query)
|> Ash.read()
so I think ideally what I'd want in my case is that if I apply tenancy to the Ash query, then I'd like the entity joins to be tenanted, as well. otherwise not. I did debate with myself for a while before making it whether I should make two entity resources (one tenanted and one not). maybe I made the wrong decision lol
ZachDaniel
ZachDaniel•5mo ago
šŸ¤” what you just described is how it works
Mylan Connolly
Mylan ConnollyOP•5mo ago
hmmm
ZachDaniel
ZachDaniel•5mo ago
If you set a tenant on the main query, its set for all joins
Mylan Connolly
Mylan ConnollyOP•5mo ago
let me recompile everything and start the test over, maybe I'm in a weird state (can't wait for 1.19 compile speed improvements)
Solution
Mylan Connolly
Mylan Connolly•5mo ago
ok so it looks like I goofed. it's mostly working as described. I might have changed multiple things:
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!()
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!()
results in no tenancy being applied
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!(tenant: client)
Atlas.Tracking.Entity |> Ash.Query.for_read(:read, %{}, tenant: client) |> Atlas.QueryParser.add_to_query(query) |> Ash.read!(tenant: client)
sorry for the support questions where mostly in the end there seems to be nothing wrong with the library after all
ZachDaniel
ZachDaniel•5mo ago
šŸ˜† its all good, there are still plenty of controls we can add to this stuff, like conditionally tenanting joins etc.
Mylan Connolly
Mylan ConnollyOP•5mo ago
that'd be cool; right now it looks like the data_layer_query also tenants the resulting Ecto query as well, so that's good too (was wanting to implement that for some CSV exports, graphs, etc. since most of the fields in the resource are custom fields in a JSONB column I think I'd need to drop down to Ecto for some of that functionality when it gets built) Ash continues to impress!

Did you find this page helpful?