defmodule Sorcery.Storage.GenserverAdapter.QueryMeta do alias Sorcery.Utils.Maps @moduledoc """ Can be used with the entire state.db under :old_db. OR if changes are being made, with the p """ defstruct [ all_table_keys: MapSet.new(), all_entities: MapSet.new(), # Format: MapSet.new([{tk, id}]) old_db: %{}, new_db: %{} ] def new(state) do %__MODULE__{new_db: state.db} end def new(src, state) do qm = %__MODULE__{} Enum.reduce(src.changes_db, qm, fn {tk, table}, acc -> acc = Enum.reduce(table, acc, fn {id, partial_entity}, acc -> entity = Map.get(state.db[tk], id, %{}) new_entity = Map.merge(entity, partial_entity) acc |> Map.update!(:all_entities, fn set -> MapSet.put(set, {tk, id}) end) |> Maps.put_in_p([:old_db, tk, id], entity) |> Maps.put_in_p([:new_db, tk, id], new_entity) end) all_table_keys = MapSet.put(acc.all_table_keys, tk) Map.put(acc, :all_table_keys, all_table_keys) end) end end defmodule Sorcery.Storage.GenserverAdapter.Query do use Norm alias Sorcery.Specs.Portals, as: PT alias Sorcery.Specs.Primative, as: T alias Sorcery.Storage.GenserverAdapter.Specs, as: AdapterT @contract solve_portal(PT.portal(), AdapterT.qmeta()) :: T.db() @doc """ Given a portal, return a db of entities satisfying it. """ def solve_portal(portal, qmeta) do tk = Map.get(portal, :tk) #old = Map.get(qmeta.old_db, tk, %{}) new = Map.get(qmeta.new_db, tk, %{}) table = new #Map.merge(old, new) |> Enum.reduce(%{}, fn {id, entity}, acc -> if portal_watching_entity?(portal, entity) do Map.put(acc, id, entity) else acc end end) if Enum.empty?(table) do %{} else %{tk => table} end end @contract solve_portals(coll_of(PT.portal()), AdapterT.qmeta()) :: T.db() @doc """ Given a list of portals, return a db of entities satisfying it. """ def solve_portals(portals, qmeta) do Enum.reduce(portals, %{}, fn portal, acc -> Map.merge(acc, solve_portal(portal, qmeta)) end) end @contract affects_portal?(PT.portal(), AdapterT.qmeta()) :: T.bool @doc """ Determine if a portal is observing any of the entities in the qmeta """ def affects_portal?(portal, qmeta) do tk = Map.get(portal, :tk) old = Map.get(qmeta.old_db, tk, %{}) new = Map.get(qmeta.new_db, tk, %{}) db = Map.merge(old, new) Enum.any?(db, fn {_, entity} -> portal_watching_entity?(portal, entity) end) end @contract affects_portals?(coll_of(PT.portal()), AdapterT.qmeta()) :: T.bool def affects_portals?(portals, qmeta) do Enum.any?(portals, fn portal -> affects_portal?(portal, qmeta) end) end @contract affected_pids(coll_of(PT.portal()), AdapterT.qmeta()) :: coll_of(T.pid()) @doc """ Given a list of ALL connected portals, return a list of pids, such that at least one of their portals is affected by the qmeta. Careful, we're potentially working with huge amounts of data. """ def affected_pids(all_portals, qmeta) do Enum.group_by(all_portals, fn %{pid: pid} -> pid end) |> Enum.reduce([], fn {pid, portals}, acc -> if affects_portals?(portals, qmeta) do [pid | acc] else acc end end) end def entity_matches_clause?(entity, {:or, guards}) do Enum.any?(guards, fn guard -> entity_matches_clause?(entity, guard) end) end def entity_matches_clause?(entity, {:and, guards}) do Enum.all?(guards, fn guard -> entity_matches_clause?(entity, guard) end) end def entity_matches_clause?(entity, {:in, attr, set}) do Map.get(entity, attr) in set end def entity_matches_clause?(entity, {fun, attr, v}) do cb = Function.capture(Kernel, fun, 2) e = Map.get(entity, attr) cb.(e, v) end def portal_watching_entity?(portal, entity) do Enum.all?(portal.resolved_guards, fn guard -> entity_matches_clause?(entity, guard) end) end @doc """ Given a list of portals, recalculate such that: 1. Every tuple of {ref, attr} in a guard will become a MapSet of values in the resolve_guard. 2. Every entity matching the portal will be found, and indexed """ def resolve_portal(portals, state) do Enum.map(portals, fn %{guards: guards} = portal -> resolved_guards = Enum.map(guards, fn {:in, attr, {ref, ref_attr}} -> reffed_portal = Enum.find(portals, fn p -> p.id == ref end) index = Map.get(reffed_portal.indices, ref_attr) {:in, attr, index} guard -> guard end) portal |> Map.put(:resolved_guards, resolved_guards) |> build_indices(state) end) end defp build_indices(%{tk: tk} = portal, state) do # Get all entities matching entities = Enum.reduce(state.db[tk], [], fn {_id, entity}, acc -> if portal_watching_entity?(portal, entity) do [entity | acc] else acc end end) indices = Enum.reduce(portal.indices, %{}, fn {attr, _}, acc -> index = Enum.map(entities, fn e -> Map.get(e, attr) end) |> MapSet.new() Map.put(acc, attr, index) end) Map.put(portal, :indices, indices) end end