Skip to content

Conversation

@shahryarjb
Copy link
Contributor

Based on #2372, it covers the aggregates for know, I will try for calculations

I covered these test cases
  • Action WITH bypass + Aggregate WITH bypass
  • Action WITHOUT bypass + Aggregates WITH and WITHOUT bypass
  • Mixed aggregates (bypass and non-bypass) with regular action
  • Action WITH bypass + Aggregate WITHOUT bypass
  • Without tenant in opts (return error This is the limit of bypass actions; it should still error)

Please review carefully; I might have missed something. Many parts were involved, but I tried to keep codebase changes minimal. All Ash tests passed successfully. In my own project, cases where I previously had to use raw queries in calculations are now easily handled with aggregates. Also, many of my previous duplications are now completely removed.

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Refactors and extends the multitenancy aggregate tests to cover all aggregate types (count, exists, first, sum, list, max, min, avg) with and without multitenancy bypass. Consolidates resource module definitions, adds more comprehensive test data, and verifies correct aggregate behavior across tenants and aggregate/action combinations, including error handling for missing tenant context.
Aggregates are now grouped by their multitenancy setting and executed in separate queries for bypass and tenant-specific aggregates. This ensures correct handling of multitenancy when running aggregate queries.
@zachdaniel
Copy link
Contributor

This does look generally good, but I think we're going to have to test this in ash_postgres using context based multi-tenancy as well for resources that support global as well 😓 That is a bit harder, but it will have to affect the way that those join.

Eliminated the `multitenancy: :bypass_all` key from the context map in Ash.Query.Aggregate, as it is now only set under the `shared` key. This simplifies the context structure.
@shahryarjb
Copy link
Contributor Author

shahryarjb commented Nov 8, 2025

@zachdaniel

This does look generally good, but I think we're going to have to test this in ash_postgres using context based multi-tenancy as well for resources that support global as well 😓 That is a bit harder, but it will have to affect the way that those join.

Sure 🙏🏻 please tell me what must do, i will try it
By the way, I don’t have a context-based model in my codebase.
My CMS project has around 4k tests, most of which use global: false, as well as attributes.
I tested it there together with AshJson.
Could you please clarify exactly what I need to do and where I should do it?

I did some changes you told me please re-check it 🙏🏻♥️; By the way i tested the ash_postgres without adding any new tests with new Ash options i added and it has just 2 errors in mix test, but i think these are not about this update! 🤔

For example

  1) test aggregate with parent filter and limited select FAILS when combining select() + limit() with aggregate using parent() in filter (AshSql.AggregateTest)
     test/aggregate_test.exs:1865
     ** (Ash.Error.Unknown)
     Bread Crumbs:
       > Error returned from: AshPostgres.Test.Chat.read

     Unknown Error

     * ** (Postgrex.Error) ERROR 42703 (undefined_column) column s0.last_read_message_id does not exist

And

2) test calculations that refer to aggregates can be authorized (AshPostgres.CalculationTest)
    test/calculation_test.exs:471
    match (=) failed
    code:  assert %{has_future_comment: true} =
             Post
             |> Ash.Query.load([:has_future_comment, :latest_comment_created_at])
             |> Ash.Query.for_read(:allow_any, %{})
             |> Ash.read_one!(authorize?: false)
    left:  %{has_future_comment: true}
    right: %AshPostgres.Test.Post{
             has_future_comment: false,

Thanks!

@zachdaniel
Copy link
Contributor

@shahryarjb there wouldn't be any failing tests yet, because ash_postgres tests wouldn't be using this option. A new test would need to be added to verify that this option works (I don't think it will).

@shahryarjb
Copy link
Contributor Author

Hi dear @zachdaniel i think my PR in the Ash Postgres needs so much attention, its required-database knowledge is more than me 🥲

PR: ash-project/ash_postgres#649

Thank you in advance

@zachdaniel
Copy link
Contributor

@shahryarjb that PR makes me think that we should just make it so that we only allow this for attribute multitenancy, and that is a change we can make here. WDYT?

@shahryarjb
Copy link
Contributor Author

Hi dear @zachdaniel i put a comment in the ash postgres, I agree with you as well. ♥️🙏🏻
ash-project/ash_postgres#649 (comment)

@zachdaniel
Copy link
Contributor

Excellent. So with that in mind, what we need to do then is validate when building an aggregate that all resources along the relationship path either have no multitenancy, or have attribute multitenancy. If there is context multitenancy anywhere then we can just raise an error.

@shahryarjb
Copy link
Contributor Author

Excellent. So with that in mind, what we need to do then is validate when building an aggregate that all resources along the relationship path either have no multitenancy, or have attribute multitenancy. If there is context multitenancy anywhere then we can just raise an error.

@zachdaniel this condtion should be in this pr or should be ash postgres? Hamm it is an easy condition or some think you have in your mind?

@zachdaniel
Copy link
Contributor

@shahryarjb yeah it should be here in this PR 😄 can go somewhere around here: https://github.com/ash-project/ash/pull/2427/files#diff-b7b7a5528d3683c2b41b27b34431108ab641369372ec60ac1c908fa81beb04ffR2792

You should be able to look at each resource along the aggregate.relationship_path and check its multitenancy to ensure its not set to :context.

Adds validation to prevent use of `multitenancy: :bypass` aggregates on resources or relationship paths that use the `:context` multitenancy strategy, as bypass is only supported with the `:attribute` strategy. Includes comprehensive tests to ensure errors are raised in invalid scenarios and that valid configurations continue to work as expected.
@shahryarjb
Copy link
Contributor Author

shahryarjb commented Nov 16, 2025

Hi dear @zachdaniel , could you please re-check my last 2 updates, if is there any consideration i should care please let me know to update!
By the way i decided to have raise Ash.Error.Query.InvalidQuery and force user not to use bypass for context even not using ! in Ash function
Thank you in advance

shared: %{multitenancy: :bypass_all}
})
else
Ash.Query.set_tenant(aggregate.query, aggregate.query.tenant || query.tenant)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should set the tenant in both cases, but just set the shared context only in the case we want to bypass multitenancy for.

Also, lets use shared: %{private: %{multitenancy: :bypass_all}} perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zachdaniel
Old behavior: When bypassing multitenancy, the tenant value was lost completely.

New behavior: The tenant is always preserved. Bypassing just tells the system "don't filter by tenant" but still
keeps the tenant value available for logging, auditing, or other uses.

Based on this: da6907d

Hope it is as you told me

if Ash.Resource.Info.multitenancy_strategy(resource) == :context do
location = relationship_name && " in relationship `#{relationship_name}`"

raise Ash.Error.Query.InvalidQuery,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to raise, lets just add this error to the query.

Copy link
Contributor Author

@shahryarjb shahryarjb Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zachdaniel I refactored some lines of the code to support error tupple based on cf19f84
And i tested and all tests were okey


By the way I wish there were some coding-style guidelines, like how I should push code to the repo. There are many coding styles I prefer, but I think they might not be well-received. I'm not talking about performance, just coding style. but i tried to follow the repo style of coding

  Old behavior: When bypassing multitenancy, the tenant value was lost completely.

  New behavior: The tenant is always preserved. Bypassing just tells the system "don't filter by tenant" but still
  keeps the tenant value available for logging, auditing, or other uses.
Ash.Tracer.set_metadata(opts[:tracer], :action, metadata)

# Preserve the original tenant from opts before handle_multitenancy might clear it
original_tenant = opts[:tenant] || query.tenant
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we updated Ash.Actions.Read.handle_multitenancy to not clear the tenant, so we shouldn't need to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-checked and as you said it works, but it did not work before and force me to keep original and use it inside the code
By the way thank you
Done ✅

# Validate context multitenancy and its relationships are not used with bypass
# Ref: https://github.com/ash-project/ash_postgres/pull/649#issuecomment-3536654583
defp handle_aggregate_multitenancy(query) do
validate_context_strategy = fn resource, relationship_name, aggregate_name ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be a private function, no need to create an anonymous function here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

aggregate.query
|> Ash.Query.set_tenant(aggregate.query.tenant || query.tenant)
|> then(
&if(aggregate.multitenancy == :bypass,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of then(&if, I think we should clean this up by defining a private function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

)
)

with :ok <- validation,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets put the logic for this in a private fun, so its with :ok <- validate_multitenancy(....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

build_opts -> build_query(target_resource, resource, build_opts)
end

# Set bypass context IMMEDIATELY if multitenancy bypass is requested
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the comment. I'd look at each comment to see if its truly necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

Copy link
Contributor

@zachdaniel zachdaniel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comments. I think we're almost there 🥳

Extracted aggregate multitenancy validation into separate helper functions for improved readability and maintainability. The new functions `validate_aggregate_multitenancy/1` and `validate_context_multitenancy_strategy/3` replace the previous inline validation logic in `handle_aggregate_multitenancy/1`.
Extracted aggregate query preparation logic into a new private function `prepare_aggregate_query/3` to improve readability and maintainability. This change centralizes the logic for setting tenant and context based on the aggregate's multitenancy strategy.
@shahryarjb
Copy link
Contributor Author

shahryarjb commented Nov 27, 2025

Hi dear @zachdaniel, I did all changes you told me! and hope they will be okey 🧙‍♂️
Thank you in advance for re-checking the PR and last changes ♥️🙏🏻
It passed my tests (in my project) and the Ash tests ☃️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants