ash/read-action-refactor.md
Zach Daniel ad347ca38b improvement: rewrite calculation loader
Calculation loading is complex because different calculations can
depend on differently parameterized things. FOr example:

```elixir
def load(_, _, _), do: [foo: %{arg: 1}]
def load(_, _, _), do: [foo: %{arg: 2}]
```

The previous naive implementation would simply merge all of the calculation loads, which naturally would not work. Now we ensure that we load each requirement in isolation.
2023-03-28 20:34:15 -04:00

129 lines
No EOL
3.5 KiB
Markdown

# Read Actions & Data Loading Refactor
Problems:
1. Loading data - lots of expensive comparison operations being performed to check what's been loaded, etc.
- calculation metadata is recursively populated
- no simple format for declaring the data that needs to be loaded
2. Read action flow
Idea: use Ash.Flow to implement read (and in future all) actions.
Needs:
1. First class ability of steps to generate new steps.
2. (Maybe) ergonomics improvements to some conditional steps, eg: `branch` rather than `if/else`.
3. Maybe conditional dependencies?
# Actions:
1. Feature flag for switching between `read.ex` and `read_flow.ex`.
2. Iterate until all the tests pass.
# Suspicions:
1. Ash.Flow step generator.
2. Ash.Flow if/else steps.
3. Conditional dependencies?
This is too big to fail, so: let's just do calculations.
New structure for calculations:
Benefits:
simpler to manipulate and check what calculations have been loaded, and comparing what calculations have been loaded no longer *also must* compare their definitions.
Note:
we end up removing `name` and `load` from `Ash.Query.Calculation` (maybe eventually? In Ash 3.0? If we can achieve backwards compatibility then we'd leave them. Maybe won't matter, needs to be thought about)
Filters will be fine without the keys above because they don't need to call them anything to put them into an expression
```elixir
@type calculation_definition :: %{name: atom(), target: target()}
@type target :: {:calculations, atom()} | {:top_level, atom()}
# better names for this key to be workshopped
calculations_to_load: %{
score1: %{args: %{}, load_as: :}
full_name: %{arg1: 10},
full_name: %{arg1: 11},
full_name: %{arg1: 12},
some_random_shit: %{arg1: 10}
},
calculation_definitions: %{
# Supports anonymous calculations & resource calculations
%{name: :full_name, target: {:top_level, :full_name}} => %Ash.Query.Calculation{},
%{name: :full_name, target: {:calculations, :full_name_2}} => %Ash.Query.Calculation{}
}
```
Scratch
```elixir
%{
full_name: %Calculation{},
some_random_shit: %Calculation{name: :some_random_shit}
}
%Calculation{name: :full_name, load: , context: %{arg1: 10, actor: ..., ...., ...}}
calculations do
calculate :full_name, :string,
expr(first_name <> ^arg(:separator) <> last_name) do
argument :separator, :string do
allow_nil? false
default " "
end
end
end
User
|> Ash.Query.load(full_name: %{separator: "~"})
|> Ash.Query.calculate(:thing, Ash.Query.Calculation.new(...))
%Resource{
thing: thing,
calculations: %{
thing2: thing
}
}
%{name: :thing, target: :thing}
%{name: :thing, target: {:calculations, :thing2}}
calculations: %{
full_name: %{args: %{separator: "~"}}
},
calculation_definitions: %{
%{name: :full_name, load: :full_name} => %Ash.Query.Calculation{}
}
%Resource{
name: :value,
calculations: %{}
}
Ash.Resource.Info.calculation(query.resource, calc_name,)
some_read_action
|> read_them()
|> Enum.map(...)
|> calculation(....)
query
|> Ash.Query.load_calculation_as(:score, :score_1, %{arg: 1})
|> Ash.Query.load_calculation_as(:score, :score_1, %{arg: 1})
```
```elixir
# What calculate looks like now, context jumbled in with context
def calculate(records, opts, %{arg1: arg1, actor: actor}) do
end
# What it should look like < 3.0 >
def calculate(records, opts, args, %CalculationContext{actor: actor, tenant: tenant, authorize?: authorize?} = context) do
end
```