packages feed

moonlight-core-0.1.0.0: src-term/Moonlight/Core/Term/Database/FreeJoin.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE QuantifiedConstraints #-}

module Moonlight.Core.Term.Database.FreeJoin where

import Control.Monad (foldM)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Maybe (catMaybes, isJust, isNothing, mapMaybe)
import Data.Set qualified as Set
import Moonlight.Core.DenseKey (DenseKey (..))
import Moonlight.Core.Language (Language)
import Moonlight.Core.Term.Database.Arrangement qualified as Arrangement
import Moonlight.Core.Term.Database.Atom
import Moonlight.Core.Term.Database.Index
import Moonlight.Core.Term.Database.OperatorTable
import Moonlight.Core.Term.Database.Table
import Moonlight.Core.Term.Database.Types
import Prelude

data ResolvedQueryAtom f key = ResolvedQueryAtom
  { resolvedQueryAtom :: !(QueryAtom f key),
    resolvedQueryAtomOperatorId :: !(Maybe Int)
  }

freeJoin ::
  (DenseKey key, Language f) =>
  FreeJoinPlan f key ->
  Database f key ->
  Either ArrangementValidationError ([QueryBinding key], Database f key)
freeJoin plan database =
  let resolvedAtoms =
        resolveFreeJoinAtoms database plan
      refreshedDatabase =
        ensurePlanDerivedIndexes resolvedAtoms database
   in case freeJoinStrategy plan of
        FreeJoinEmptyConjunction ->
          Right ([QueryBinding Map.empty], refreshedDatabase)
        FreeJoinExactAtomProbe ->
          exactAtomFreeJoin resolvedAtoms refreshedDatabase
        FreeJoinGenericIntersection ->
          genericFreeJoin resolvedAtoms refreshedDatabase
{-# INLINE freeJoin #-}

resolveFreeJoinAtoms ::
  Language f =>
  Database f key ->
  FreeJoinPlan f key ->
  [ResolvedQueryAtom f key]
resolveFreeJoinAtoms database =
  fmap (resolveQueryAtom database) . freeJoinAtoms
{-# INLINE resolveFreeJoinAtoms #-}

resolveQueryAtom ::
  Language f =>
  Database f key ->
  QueryAtom f key ->
  ResolvedQueryAtom f key
resolveQueryAtom database atom =
  ResolvedQueryAtom
    { resolvedQueryAtom = atom,
      resolvedQueryAtomOperatorId = operatorIdFor (atomOperator atom) database
    }
{-# INLINE resolveQueryAtom #-}

ensurePlanDerivedIndexes ::
  [ResolvedQueryAtom f key] ->
  Database f key ->
  Database f key
ensurePlanDerivedIndexes resolvedAtoms database =
  IntSet.foldl'
    (flip ensureOperatorDerivedIndexes)
    database
    (IntSet.fromList (mapMaybe resolvedQueryAtomOperatorId resolvedAtoms))
{-# INLINE ensurePlanDerivedIndexes #-}

freeJoinStrategy :: FreeJoinPlan f key -> FreeJoinStrategy
freeJoinStrategy plan =
  case freeJoinAtoms plan of
    [] ->
      FreeJoinEmptyConjunction
    [atom]
      | all termLiteralBound (atomChildren atom) ->
          FreeJoinExactAtomProbe
    _ ->
      FreeJoinGenericIntersection
{-# INLINE freeJoinStrategy #-}

termLiteralBound :: QueryTerm key -> Bool
termLiteralBound term =
  case term of
    QueryBound _key ->
      True
    QueryVariable _variable ->
      False
{-# INLINE termLiteralBound #-}

exactAtomFreeJoin ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  Database f key ->
  Either ArrangementValidationError ([QueryBinding key], Database f key)
exactAtomFreeJoin resolvedAtoms database =
  case resolvedAtoms of
    [resolvedAtom] -> do
      let emptyBinding :: QueryBinding key
          emptyBinding =
            QueryBinding Map.empty
          atom =
            resolvedQueryAtom resolvedAtom
      (candidateRows, arrangedDatabase) <-
        atomCandidateRows resolvedAtom emptyBinding database
      pure (mapMaybe (atomBindRow atom emptyBinding . snd) candidateRows, arrangedDatabase)
    _ ->
      genericFreeJoin resolvedAtoms database
{-# INLINE exactAtomFreeJoin #-}

genericFreeJoin ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  Database f key ->
  Either ArrangementValidationError ([QueryBinding key], Database f key)
genericFreeJoin resolvedAtoms database = do
  (joinedBindings, joinedDatabase) <-
    bindVariables
      resolvedAtoms
      (orderedVariables database resolvedAtoms)
      [QueryBinding Map.empty]
      database
  filterBindings resolvedAtoms joinedBindings joinedDatabase
{-# INLINE genericFreeJoin #-}

strictJoinTraverse ::
  (Database f key -> input -> Either err (Database f key, output)) ->
  Database f key ->
  [input] ->
  Either err (Database f key, [output])
strictJoinTraverse step database inputs =
  fmap
    (\(finalDatabase, reversedOutputs) -> (finalDatabase, reverse reversedOutputs))
    (foldM collect (database, []) inputs)
  where
    collect (!currentDatabase, !reversedOutputs) input = do
      (!nextDatabase, output) <- step currentDatabase input
      pure (nextDatabase, output : reversedOutputs)
{-# INLINE strictJoinTraverse #-}

bindVariables ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  [QueryVar] ->
  [QueryBinding key] ->
  Database f key ->
  Either ArrangementValidationError ([QueryBinding key], Database f key)
bindVariables resolvedAtoms variables bindings database =
  foldM bindVariable (bindings, database) variables
  where
    bindVariable (!currentBindings, !currentDatabase) variable = do
      (nextDatabase, extendedBindings) <-
        strictJoinTraverse
          (extendVariable resolvedAtoms variable)
          currentDatabase
          currentBindings
      let !nextBindings = concat extendedBindings
      pure (nextBindings, nextDatabase)
{-# INLINE bindVariables #-}

extendVariable ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  QueryVar ->
  Database f key ->
  QueryBinding key ->
  Either ArrangementValidationError (Database f key, [QueryBinding key])
extendVariable resolvedAtoms variable database binding = do
  (candidateKeys, candidateDatabase) <-
    variableCandidates resolvedAtoms variable binding database
  pure $
    case Map.lookup variable (queryBindingAssignments binding) of
      Just boundValue ->
        (candidateDatabase, [binding | IntSet.member (encodeDenseKey boundValue) candidateKeys])
      Nothing ->
        (candidateDatabase, mapMaybe (\key -> bindVariableValue variable key binding) (IntSet.toAscList candidateKeys))
{-# INLINE extendVariable #-}

variableCandidates ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  QueryVar ->
  QueryBinding key ->
  Database f key ->
  Either ArrangementValidationError (IntSet, Database f key)
variableCandidates resolvedAtoms variable binding database = do
  (arrangedDatabase, candidateSets) <-
    strictJoinTraverse
      (atomVariableCandidates variable binding)
      database
      (atomsForVariable variable resolvedAtoms)
  pure (intersectCandidateSets candidateSets, arrangedDatabase)
{-# INLINE variableCandidates #-}

atomVariableCandidates ::
  (DenseKey key, Language f) =>
  QueryVar ->
  QueryBinding key ->
  Database f key ->
  ResolvedQueryAtom f key ->
  Either ArrangementValidationError (Database f key, IntSet)
atomVariableCandidates variable binding database resolvedAtom = do
  (candidateRows, arrangedDatabase) <-
    atomCandidateRows resolvedAtom binding database
  pure
    ( arrangedDatabase,
      IntSet.fromList
        [ encodeDenseKey variableValue
          | (_rowId, row) <- candidateRows,
            Just nextBinding <- [atomBindRow (resolvedQueryAtom resolvedAtom) binding row],
            Just variableValue <- [Map.lookup variable (queryBindingAssignments nextBinding)]
        ]
    )
{-# INLINE atomVariableCandidates #-}

intersectCandidateSets :: [IntSet] -> IntSet
intersectCandidateSets candidateSets =
  case candidateSets of
    [] ->
      IntSet.empty
    firstSet : restSets ->
      foldl' IntSet.intersection firstSet restSets
{-# INLINE intersectCandidateSets #-}

filterBindings ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  [QueryBinding key] ->
  Database f key ->
  Either ArrangementValidationError ([QueryBinding key], Database f key)
filterBindings resolvedAtoms bindings database = do
  (checkedDatabase, checkedBindings) <-
    strictJoinTraverse (filterBinding resolvedAtoms) database bindings
  pure (catMaybes checkedBindings, checkedDatabase)
{-# INLINE filterBindings #-}

filterBinding ::
  (DenseKey key, Language f) =>
  [ResolvedQueryAtom f key] ->
  Database f key ->
  QueryBinding key ->
  Either ArrangementValidationError (Database f key, Maybe (QueryBinding key))
filterBinding resolvedAtoms database binding = do
  (checkedDatabase, satisfied) <-
    strictJoinTraverse
      (atomSatisfied binding)
      database
      resolvedAtoms
  pure (checkedDatabase, if and satisfied then Just binding else Nothing)
{-# INLINE filterBinding #-}

atomSatisfied ::
  (DenseKey key, Language f) =>
  QueryBinding key ->
  Database f key ->
  ResolvedQueryAtom f key ->
  Either ArrangementValidationError (Database f key, Bool)
atomSatisfied binding database resolvedAtom = do
  (candidateRows, arrangedDatabase) <-
    atomCandidateRows resolvedAtom binding database
  pure
    ( arrangedDatabase,
      any (isJust . atomBindRow (resolvedQueryAtom resolvedAtom) binding . snd) candidateRows
    )
{-# INLINE atomSatisfied #-}

orderedVariables ::
  Database f key ->
  [ResolvedQueryAtom f key] ->
  [QueryVar]
orderedVariables database resolvedAtoms =
  List.sortOn
    (variableCost database resolvedAtoms)
    (Set.toList (planVariables resolvedAtoms))
{-# INLINE orderedVariables #-}

planVariables :: [ResolvedQueryAtom f key] -> Set.Set QueryVar
planVariables =
  foldMap (atomVariables . resolvedQueryAtom)
{-# INLINE planVariables #-}

atomVariables :: QueryAtom f key -> Set.Set QueryVar
atomVariables =
  foldMap termVariables . atomTerms
{-# INLINE atomVariables #-}

termVariables :: QueryTerm key -> Set.Set QueryVar
termVariables term =
  case term of
    QueryBound _ ->
      Set.empty
    QueryVariable variable ->
      Set.singleton variable
{-# INLINE termVariables #-}

atomsForVariable :: QueryVar -> [ResolvedQueryAtom f key] -> [ResolvedQueryAtom f key]
atomsForVariable variable =
  filter (atomMentionsVariable variable . resolvedQueryAtom)
{-# INLINE atomsForVariable #-}

atomMentionsVariable :: QueryVar -> QueryAtom f key -> Bool
atomMentionsVariable variable =
  Set.member variable . atomVariables
{-# INLINE atomMentionsVariable #-}

variableCost ::
  Database f key ->
  [ResolvedQueryAtom f key] ->
  QueryVar ->
  (Int, Int, QueryVar)
variableCost database resolvedAtoms variable =
  ( minimumCandidateDistinct variableDistincts,
    negate (length variableDistincts),
    variable
  )
  where
    variableDistincts =
      [ distinct
        | resolvedAtom <- atomsForVariable variable resolvedAtoms,
          column <- atomVariableColumns variable (resolvedQueryAtom resolvedAtom),
          Just distinct <- [atomColumnDistinct database resolvedAtom column]
      ]
{-# INLINE variableCost #-}

minimumCandidateDistinct :: [Int] -> Int
minimumCandidateDistinct values =
  case values of
    [] ->
      maxBound
    firstValue : restValues ->
      foldl' min firstValue restValues
{-# INLINE minimumCandidateDistinct #-}

atomVariableColumns :: QueryVar -> QueryAtom f key -> [Column]
atomVariableColumns variable atom =
  [ column
    | (column, QueryVariable columnVariable) <- zip (atomColumns atom) (atomTerms atom),
      columnVariable == variable
  ]
{-# INLINE atomVariableColumns #-}

atomColumnDistinct :: Database f key -> ResolvedQueryAtom f key -> Column -> Maybe Int
atomColumnDistinct database resolvedAtom column =
  resolvedQueryAtomOperatorId resolvedAtom
    >>= \operatorId -> IntMap.lookup operatorId (operatorTables database)
    >>= \table -> Just (Arrangement.distinctColumnValueCount table column)
{-# INLINE atomColumnDistinct #-}

atomCandidateRows ::
  (DenseKey key, Language f) =>
  ResolvedQueryAtom f key ->
  QueryBinding key ->
  Database f key ->
  Either ArrangementValidationError ([(RowId, DatabaseRow)], Database f key)
atomCandidateRows resolvedAtom binding database =
  case resolvedQueryAtomOperatorId resolvedAtom >>= lookupOperatorTable of
    Just table
      | Just rowIds <- atomIndexedCandidateRows atom binding table ->
          Right (rowsForIds table rowIds, database)
    _ -> do
      (arrangementKey, arrangementPrefix) <-
        atomArrangementPrefix atom binding
      Arrangement.arrangementRowsForResolvedOperator
        (resolvedQueryAtomOperatorId resolvedAtom)
        (atomOperator atom)
        arrangementKey
        arrangementPrefix
        database
  where
    atom =
      resolvedQueryAtom resolvedAtom
    lookupOperatorTable operatorId =
      IntMap.lookup operatorId (operatorTables database)
{-# INLINE atomCandidateRows #-}

atomIndexedCandidateRows ::
  DenseKey key =>
  QueryAtom f key ->
  QueryBinding key ->
  OperatorTable f ->
  Maybe RowIdSet
atomIndexedCandidateRows atom binding table =
  chooseIndexedCandidateRows exactRows resultRows
  where
    exactRows =
      fmap
        (\childKeys -> lookupExactIndex childKeys (derivedExactIndex table))
        (traverse (termBoundValue binding) (atomChildren atom))

    resultRows =
      fmap
        (\resultKey -> lookupResultIndex resultKey (derivedResultIndex table))
        (termBoundValue binding (atomResult atom))
{-# INLINE atomIndexedCandidateRows #-}

chooseIndexedCandidateRows :: Maybe RowIdSet -> Maybe RowIdSet -> Maybe RowIdSet
chooseIndexedCandidateRows exactRows resultRows =
  case (exactRows, resultRows) of
    (Just leftRows, Just rightRows)
      | rowIdSetSize leftRows <= rowIdSetSize rightRows ->
          Just leftRows
      | otherwise ->
          Just rightRows
    (Just rows, Nothing) ->
      Just rows
    (Nothing, Just rows) ->
      Just rows
    (Nothing, Nothing) ->
      Nothing
{-# INLINE chooseIndexedCandidateRows #-}

atomArrangementPrefix ::
  (DenseKey key, Foldable f) =>
  QueryAtom f key ->
  QueryBinding key ->
  Either ArrangementValidationError (ArrangementKey, ArrangementPrefix)
atomArrangementPrefix atom binding = do
  arrangementKey <-
    arrangementKeyForOperator
      (atomOperator atom)
      (fmap fst boundColumns <> unboundColumns)
  arrangementPrefix <-
    arrangementPrefixForKey arrangementKey (fmap snd boundColumns)
  pure (arrangementKey, arrangementPrefix)
  where
    columnTerms =
      zip (atomColumns atom) (atomTerms atom)

    boundColumns =
      mapMaybe (boundColumn binding) columnTerms

    unboundColumns =
      fmap fst (filter (isNothing . termBoundValue binding . snd) columnTerms)
{-# INLINE atomArrangementPrefix #-}