packages feed

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

module Moonlight.Core.Term.Database.OperatorTable where

import Data.Foldable (toList)
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Kind (Type)
import Data.Maybe (mapMaybe)
import Data.Primitive.PrimArray (PrimArray)
import Data.Primitive.PrimArray qualified as PrimArray
import Data.Primitive.SmallArray qualified as SmallArray
import Data.Sequence qualified as Seq
import Moonlight.Core.Term.Database.Index
import Moonlight.Core.Term.Database.Types
import Prelude

operatorRowChunkCapacity :: Int
-- One sealed column occupies 4 KiB on the measured 64-bit target. The
-- unsealed suffix therefore remains strictly bounded without forcing small
-- batches through column transposition.
operatorRowChunkCapacity =
  512

emptyOperatorRowStore :: OperatorRowStore
emptyOperatorRowStore =
  OperatorRowStore
    { sealedRowChunks = Seq.empty,
      pendingStoredRows = Seq.empty,
      tombstonedRowIds = IntSet.empty
    }

emptyOperatorTable :: Foldable f => f () -> OperatorTable f
emptyOperatorTable shape =
  OperatorTable
    { opShape = shape,
      opArity = arity,
      rowStore = emptyOperatorRowStore,
      nextRowId = 0,
      derivedIndexWatermark = 0,
      resultIx = emptyResultIndex,
      childColumnIx = emptyChildColumnValueIndex arity,
      exactIx = emptyExactIndex arity,
      exactResultIx = emptyExactResultIndex arity,
      childUserIx = emptyChildUserIndex
    }
  where
    arity = length (toList shape)
{-# INLINE emptyOperatorTable #-}

operatorTableFromRows :: Foldable f => f () -> [DatabaseRow] -> OperatorTable f
operatorTableFromRows shape rows =
  OperatorTable
    { opShape = shape,
      opArity = arity,
      rowStore = operatorRowStoreFromRows arity rows,
      nextRowId = rowCount,
      derivedIndexWatermark = rowCount,
      resultIx =
        foldl'
          (\resultIndex (rowKey, row) ->
             insertResultIndex rowKey (rowResult row) resultIndex)
          emptyResultIndex
          indexedRows,
      childColumnIx =
        foldl'
          (\childColumnIndexes (rowKey, row) ->
             insertChildColumnValueIndex rowKey (rowChildrenArray row) childColumnIndexes)
          (emptyChildColumnValueIndex arity)
          indexedRows,
      exactIx =
        foldl'
          (\exactIndex (rowKey, row) ->
             insertExactIndex rowKey (rowChildrenArray row) exactIndex)
          (emptyExactIndex arity)
          indexedRows,
      exactResultIx =
        foldl'
          (\exactResultIndex (_rowKey, row) ->
             insertExactResultIndex (rowResult row) (rowChildrenArray row) exactResultIndex)
          (emptyExactResultIndex arity)
          indexedRows,
      childUserIx =
        foldl'
          (\childUsers (rowKey, row) ->
             PrimArray.foldrPrimArray
               (\childKey -> insertChildUserIndex rowKey childKey)
               childUsers
               (rowChildrenArray row))
          emptyChildUserIndex
          indexedRows
    }
  where
    arity = length (toList shape)
    rowCount = length rows
    indexedRows = zip (ascendingRowKeys rowCount) rows
{-# INLINE operatorTableFromRows #-}

operatorRowStoreFromRows :: Int -> [DatabaseRow] -> OperatorRowStore
operatorRowStoreFromRows arity =
  foldl' (\store row -> appendStoredRow arity row store) emptyOperatorRowStore

appendStoredRow :: Int -> DatabaseRow -> OperatorRowStore -> OperatorRowStore
appendStoredRow arity row store
  | Seq.length appendedPendingRows == operatorRowChunkCapacity =
      store
        { sealedRowChunks =
            sealedRowChunks store Seq.|> operatorRowChunkFromRows arity appendedPendingRows,
          pendingStoredRows = Seq.empty
        }
  | otherwise =
      store
        { pendingStoredRows = appendedPendingRows
        }
  where
    appendedPendingRows =
      pendingStoredRows store Seq.|> row
{-# INLINE appendStoredRow #-}

operatorRowChunkFromRows :: Int -> Seq.Seq DatabaseRow -> OperatorRowChunk
operatorRowChunkFromRows arity rows =
  OperatorRowChunk
    { chunkResults =
        PrimArray.primArrayFromListN rowCount (fmap rowResult rowList),
      chunkChildren =
        SmallArray.smallArrayFromList
          (fmap childColumn (ascendingRowKeys arity))
    }
  where
    rowList =
      toList rows
    rowCount =
      length rowList
    childColumn childIndex =
      PrimArray.primArrayFromListN (length childValues) childValues
      where
        childValues =
          mapMaybe (childValueAt childIndex . rowChildrenArray) rowList
{-# INLINE operatorRowChunkFromRows #-}

tableLiveRows :: OperatorTable f -> RowIdSet
tableLiveRows table =
  RowIdSet
    ( IntSet.difference
        (IntSet.fromDistinctAscList (ascendingRowKeys (nextRowId table)))
        (tombstonedRowIds (rowStore table))
    )
{-# INLINE tableLiveRows #-}

tableLiveRowCount :: OperatorTable f -> Int
tableLiveRowCount table =
  nextRowId table - IntSet.size (tombstonedRowIds (rowStore table))
{-# INLINE tableLiveRowCount #-}

applyOperatorTableRowEdit :: OperatorTableRowEdit -> OperatorTable f -> OperatorTable f
applyOperatorTableRowEdit edit table =
  case edit of
    InsertOperatorTableRow row ->
      table
        { exactResultIx =
            insertExactResultIndex (rowResult row) (rowChildrenArray row) (exactResultIx table)
        }
    DeleteOperatorTableRow rowKey row ->
      let tableWithoutEagerRow =
            table
              { exactResultIx =
                  deleteExactResultIndex (rowResult row) (rowChildrenArray row) (exactResultIx table)
              }
       in if rowKey < derivedIndexWatermark table
            then deleteRowFromDerivedIndexes rowKey row tableWithoutEagerRow
            -- Rows at or above the watermark were never derived-indexed, and
            -- refresh reads the post-delete live row store, so it cannot
            -- restore them.
            else tableWithoutEagerRow
{-# INLINE applyOperatorTableRowEdit #-}

insertRowIntoDerivedIndexes :: Int -> DatabaseRow -> OperatorTable f -> OperatorTable f
insertRowIntoDerivedIndexes rowKey row table =
  table
    { resultIx =
        insertResultIndex rowKey (rowResult row) (resultIx table),
      childColumnIx =
        insertChildColumnValueIndex rowKey (rowChildrenArray row) (childColumnIx table),
      exactIx =
        insertExactIndex rowKey (rowChildrenArray row) (exactIx table),
      childUserIx =
        PrimArray.foldrPrimArray
          (insertChildUserIndex rowKey)
          (childUserIx table)
          (rowChildrenArray row)
    }
{-# INLINE insertRowIntoDerivedIndexes #-}

deleteRowFromDerivedIndexes :: Int -> DatabaseRow -> OperatorTable f -> OperatorTable f
deleteRowFromDerivedIndexes rowKey row table =
  table
    { resultIx =
        deleteResultIndex rowKey (rowResult row) (resultIx table),
      childColumnIx =
        deleteChildColumnValueIndex rowKey (rowChildrenArray row) (childColumnIx table),
      exactIx =
        deleteExactIndex rowKey (rowChildrenArray row) (exactIx table),
      childUserIx =
        PrimArray.foldrPrimArray
          (deleteChildUserIndex rowKey)
          (childUserIx table)
          (rowChildrenArray row)
    }
{-# INLINE deleteRowFromDerivedIndexes #-}

ensureDerivedIndexes :: OperatorTable f -> OperatorTable f
ensureDerivedIndexes table
  | derivedIndexWatermark table >= nextRowId table =
      table
  | otherwise =
      ( foldUnindexedOperatorTableRowsWithId
          (\indexedTable rowKey row -> insertRowIntoDerivedIndexes rowKey row indexedTable)
          table
          table
      )
        { derivedIndexWatermark = nextRowId table
        }
{-# INLINE ensureDerivedIndexes #-}

foldOperatorTableRowsWithId ::
  (result -> Int -> DatabaseRow -> result) ->
  result ->
  OperatorTable f ->
  result
foldOperatorTableRowsWithId step initialResult table =
  foldOperatorTableRowsFromId 0 step initialResult table
{-# INLINE foldOperatorTableRowsWithId #-}

foldUnindexedOperatorTableRowsWithId ::
  (result -> Int -> DatabaseRow -> result) ->
  result ->
  OperatorTable f ->
  result
foldUnindexedOperatorTableRowsWithId step initialResult table =
  foldOperatorTableRowsFromId (derivedIndexWatermark table) step initialResult table
{-# INLINE foldUnindexedOperatorTableRowsWithId #-}

foldOperatorTableRowsFromId ::
  Int ->
  (result -> Int -> DatabaseRow -> result) ->
  result ->
  OperatorTable f ->
  result
foldOperatorTableRowsFromId firstRowKey step initialResult table =
  foldl'
    (\result (RowId rowKey, row) -> step result rowKey row)
    initialResult
    (dropWhile ((< firstRowKey) . unRowId . fst) (operatorTableRows table))
{-# INLINE foldOperatorTableRowsFromId #-}

unindexedRowIdsWhere :: (DatabaseRow -> Bool) -> OperatorTable f -> RowIdSet
unindexedRowIdsWhere predicate table =
  RowIdSet $
    foldUnindexedOperatorTableRowsWithId
      (\matchingRowIds rowKey row ->
         if predicate row
           then IntSet.insert rowKey matchingRowIds
           else matchingRowIds)
      IntSet.empty
      table
{-# INLINE unindexedRowIdsWhere #-}

insertEncodedRow :: DatabaseRow -> OperatorTable f -> OperatorTable f
insertEncodedRow row table
  | encodedRowPresent row table = table
  | otherwise = appendEncodedRow row table
{-# INLINE insertEncodedRow #-}

insertEncodedRowsWithInsertedRows :: [DatabaseRow] -> OperatorTable f -> ([(RowId, DatabaseRow)], OperatorTable f)
insertEncodedRowsWithInsertedRows rows table =
  case indexedAcceptedRows of
    [] ->
      ([], table)
    _ ->
      ( fmap (\(rowKey, row) -> (RowId rowKey, row)) indexedAcceptedRows,
        table
          { rowStore =
              foldl'
                (\store (_rowKey, row) -> appendStoredRow (opArity table) row store)
                (rowStore table)
                indexedAcceptedRows,
            nextRowId =
              acceptedNextRowId acceptance,
            exactResultIx =
              acceptedExactResultIndex acceptance
          }
      )
  where
    acceptance =
      acceptEncodedRows rows table
    indexedAcceptedRows =
      reverse (acceptedReversedRows acceptance)
{-# INLINE insertEncodedRowsWithInsertedRows #-}

type EncodedRowAcceptance :: Type
data EncodedRowAcceptance = EncodedRowAcceptance
  { acceptedReversedRows :: ![(Int, DatabaseRow)],
    acceptedNextRowId :: !Int,
    acceptedExactResultIndex :: !ExactResultIndex
  }

acceptEncodedRows :: [DatabaseRow] -> OperatorTable f -> EncodedRowAcceptance
acceptEncodedRows rows table =
  foldl'
    acceptEncodedRow
    ( EncodedRowAcceptance
        { acceptedReversedRows = [],
          acceptedNextRowId = nextRowId table,
          acceptedExactResultIndex = exactResultIx table
        }
    )
    rows
  where
    acceptEncodedRow ::
      EncodedRowAcceptance ->
      DatabaseRow ->
      EncodedRowAcceptance
    acceptEncodedRow acceptance row
      | IntSet.member
          (rowResult row)
          (lookupExactResultIndex (rowChildrenArray row) (acceptedExactResultIndex acceptance)) =
          acceptance
      | otherwise =
          acceptance
            { acceptedReversedRows =
                (rowKey, row) : acceptedReversedRows acceptance,
              acceptedNextRowId = rowKey + 1,
              acceptedExactResultIndex =
                insertExactResultIndex
                  (rowResult row)
                  (rowChildrenArray row)
                  (acceptedExactResultIndex acceptance)
            }
      where
        rowKey =
          acceptedNextRowId acceptance
{-# INLINE acceptEncodedRows #-}

appendEncodedRow :: DatabaseRow -> OperatorTable f -> OperatorTable f
appendEncodedRow row table =
  applyOperatorTableRowEdit (InsertOperatorTableRow row) $
    table
      { rowStore = appendStoredRow (opArity table) row (rowStore table),
        nextRowId = rowKey + 1
      }
  where
    rowKey = nextRowId table
{-# INLINE appendEncodedRow #-}

encodedRowPresent :: DatabaseRow -> OperatorTable f -> Bool
encodedRowPresent row table =
  IntSet.member
    (rowResult row)
    (lookupExactResultIndex (rowChildrenArray row) (exactResultIx table))
{-# INLINE encodedRowPresent #-}

operatorTableRows :: OperatorTable f -> [(RowId, DatabaseRow)]
operatorTableRows table =
  concat (toList sealedRowsByChunk) <> pendingRows
  where
    store =
      rowStore table
    tombstones =
      tombstonedRowIds store
    sealedRowsByChunk =
      Seq.mapWithIndex
        (\chunkIndex ->
           operatorRowChunkRowsAt
             (chunkIndex * operatorRowChunkCapacity)
             (opArity table)
             tombstones)
        (sealedRowChunks store)
    pendingRows =
      mapMaybe pendingRow (zip pendingRowKeys (toList (pendingStoredRows store)))
    pendingRowKeys =
      ascendingRowKeysBetween (sealedRowCount table) (nextRowId table)
    pendingRow (rowKey, row)
      | IntSet.member rowKey tombstones =
          Nothing
      | otherwise =
          Just (RowId rowKey, row)

operatorRowChunkRowsAt :: Int -> Int -> IntSet -> OperatorRowChunk -> [(RowId, DatabaseRow)]
operatorRowChunkRowsAt firstRowKey arity tombstones chunk
  | SmallArray.sizeofSmallArray childColumns /= arity =
      []
  | not (all ((== rowCount) . PrimArray.sizeofPrimArray) (smallArrayToList childColumns)) =
      []
  | otherwise =
      fmap rowAt liveRowOffsets
  where
    resultColumn =
      chunkResults chunk
    childColumns =
      chunkChildren chunk
    rowCount =
      PrimArray.sizeofPrimArray resultColumn
    liveRowOffsets =
      filter
        (\rowOffset -> not (IntSet.member (firstRowKey + rowOffset) tombstones))
        (ascendingRowKeys rowCount)
    rowAt rowOffset =
      ( RowId (firstRowKey + rowOffset),
        DatabaseRow
          { rowResult = PrimArray.indexPrimArray resultColumn rowOffset,
            rowChildrenArray =
              PrimArray.generatePrimArray arity $ \childIndex ->
                PrimArray.indexPrimArray
                  (SmallArray.indexSmallArray childColumns childIndex)
                  rowOffset
          }
      )
{-# INLINE operatorRowChunkRowsAt #-}

operatorTableRowsForResultKey :: Int -> OperatorTable f -> [(RowId, DatabaseRow)]
operatorTableRowsForResultKey resultKey table =
  rowsForIds table (operatorTableRowIdsForResultKeys (IntSet.singleton resultKey) table)

operatorTableRowIdsForResultKeys :: IntSet -> OperatorTable f -> RowIdSet
operatorTableRowIdsForResultKeys resultKeys table =
  rowIdSetUnion indexedRowIds unindexedRowIds
  where
    indexedRowIds =
      RowIdSet
        (intMapDependentsOfMany (IntSet.toAscList resultKeys) (derivedResultIndex table))
    unindexedRowIds =
      unindexedRowIdsWhere
        (\row -> IntSet.member (rowResult row) resultKeys)
        table
{-# INLINE operatorTableRowIdsForResultKeys #-}

lookupChildResultKeys :: PrimArray Int -> OperatorTable f -> IntSet
lookupChildResultKeys childKeys table =
  lookupExactResultIndex childKeys (exactResultIx table)

rowsForIds :: OperatorTable f -> RowIdSet -> [(RowId, DatabaseRow)]
rowsForIds table rowIds =
  mapMaybe rowForId (rowIdSetToAscList rowIds)
  where
    rowForId rowKey =
      fmap (\row -> (RowId rowKey, row)) (operatorTableRowAt rowKey table)

deleteEncodedRows :: DatabaseRow -> OperatorTable f -> OperatorTable f
deleteEncodedRows row table
  | encodedRowPresent row table =
      rowIdSetFoldl'
        (flip deleteEncodedRow)
        indexedTable
        ( rowIdSetIntersection
            (lookupResultIndex (rowResult row) (derivedResultIndex indexedTable))
            (lookupExactIndexArray (rowChildrenArray row) (derivedExactIndex indexedTable))
        )
  | otherwise =
      table
  where
    indexedTable =
      ensureDerivedIndexes table

deleteEncodedRow :: Int -> OperatorTable f -> OperatorTable f
deleteEncodedRow rowKey table =
  maybe table (\row -> deleteStoredRow rowKey row table) (operatorTableRowAt rowKey table)

deleteStoredRow :: Int -> DatabaseRow -> OperatorTable f -> OperatorTable f
deleteStoredRow rowKey row table =
  applyOperatorTableRowEdit (DeleteOperatorTableRow rowKey row) $
    table
      { rowStore =
          (rowStore table)
            { tombstonedRowIds =
                IntSet.insert rowKey (tombstonedRowIds (rowStore table))
            }
      }

operatorTableRowAt :: Int -> OperatorTable f -> Maybe DatabaseRow
operatorTableRowAt rowKey table
  | rowKey < 0 || rowKey >= nextRowId table =
      Nothing
  | IntSet.member rowKey (tombstonedRowIds (rowStore table)) =
      Nothing
  | rowKey < sealedRowCount table =
      sealedRowChunkAt rowKey table >>= operatorRowChunkRowAt (opArity table) (rowKey `mod` operatorRowChunkCapacity)
  | otherwise =
      Seq.lookup (rowKey - sealedRowCount table) (pendingStoredRows (rowStore table))
{-# INLINE operatorTableRowAt #-}

operatorTableResultAt :: Int -> OperatorTable f -> Maybe Int
operatorTableResultAt rowKey table
  | rowKey < 0 || rowKey >= nextRowId table =
      Nothing
  | IntSet.member rowKey (tombstonedRowIds (rowStore table)) =
      Nothing
  | rowKey < sealedRowCount table =
      sealedRowChunkAt rowKey table >>= operatorRowChunkResultAt (rowKey `mod` operatorRowChunkCapacity)
  | otherwise =
      rowResult <$> Seq.lookup (rowKey - sealedRowCount table) (pendingStoredRows (rowStore table))
{-# INLINE operatorTableResultAt #-}

sealedRowCount :: OperatorTable f -> Int
sealedRowCount table =
  Seq.length (sealedRowChunks (rowStore table)) * operatorRowChunkCapacity
{-# INLINE sealedRowCount #-}

sealedRowChunkAt :: Int -> OperatorTable f -> Maybe OperatorRowChunk
sealedRowChunkAt rowKey table =
  Seq.lookup
    (rowKey `div` operatorRowChunkCapacity)
    (sealedRowChunks (rowStore table))
{-# INLINE sealedRowChunkAt #-}

operatorRowChunkRowAt :: Int -> Int -> OperatorRowChunk -> Maybe DatabaseRow
operatorRowChunkRowAt arity rowOffset chunk
  | rowOffset < 0 || rowOffset >= rowCount =
      Nothing
  | SmallArray.sizeofSmallArray childColumns /= arity =
      Nothing
  | not (all ((== rowCount) . PrimArray.sizeofPrimArray) (smallArrayToList childColumns)) =
      Nothing
  | otherwise =
      Just
        DatabaseRow
          { rowResult = PrimArray.indexPrimArray resultColumn rowOffset,
            rowChildrenArray =
              PrimArray.generatePrimArray arity $ \childIndex ->
                PrimArray.indexPrimArray
                  (SmallArray.indexSmallArray childColumns childIndex)
                  rowOffset
          }
  where
    resultColumn =
      chunkResults chunk
    childColumns =
      chunkChildren chunk
    rowCount =
      PrimArray.sizeofPrimArray resultColumn
{-# INLINE operatorRowChunkRowAt #-}

operatorRowChunkResultAt :: Int -> OperatorRowChunk -> Maybe Int
operatorRowChunkResultAt rowOffset chunk =
  if rowOffset < 0 || rowOffset >= PrimArray.sizeofPrimArray results
    then Nothing
    else Just (PrimArray.indexPrimArray results rowOffset)
  where
    results =
      chunkResults chunk
{-# INLINE operatorRowChunkResultAt #-}

ascendingRowKeys :: Int -> [Int]
ascendingRowKeys count =
  ascendingRowKeysBetween 0 count
{-# INLINE ascendingRowKeys #-}

ascendingRowKeysBetween :: Int -> Int -> [Int]
ascendingRowKeysBetween firstRowKey endRowKey
  | firstRowKey >= endRowKey = []
  | otherwise = [max 0 firstRowKey .. endRowKey - 1]
{-# INLINE ascendingRowKeysBetween #-}

childValueAt :: Int -> PrimArray Int -> Maybe Int
childValueAt childIndex children =
  if childIndex < 0 || childIndex >= PrimArray.sizeofPrimArray children
    then Nothing
    else Just (PrimArray.indexPrimArray children childIndex)
{-# INLINE childValueAt #-}

keepNonEmptyTable :: OperatorTable f -> Maybe (OperatorTable f)
keepNonEmptyTable table
  | tableLiveRowCount table == 0 = Nothing
  | otherwise = Just table