content: {-# LANGUAGE StandaloneDeriving #-}
  
  -- | Queries related to sync and temp entities.
  module Share.Postgres.Sync.Queries
    ( expectEntity,
      entityLocations,
      saveTempEntityInMain,
      saveTempEntities,
      Share.EntityLocation (..),
      elaborateHashes,
      getEntitiesReadyToFlush,
      filterForFlushableHashes,
      tryPopFlushableTempEntity,
    )
  where
  
  import Control.Lens hiding (from)
  import Data.ByteString.Lazy.Char8 qualified as BL
  import Data.Foldable qualified as Foldable
  import Data.Set qualified as Set
  import Data.Set.NonEmpty (NESet)
  import Data.Set.NonEmpty qualified as NESet
  import Data.Vector qualified as Vector
  import Servant (ServerError (..), err500)
  import Share.Codebase.Types (CodebaseEnv (..))
  import Share.Codebase.Types qualified as Codebase
  import Share.IDs
  import Share.Postgres
  import Share.Postgres.Causal.Queries qualified as CausalQ
  import Share.Postgres.Definitions.Queries qualified as Defn
  import Share.Postgres.Definitions.Queries qualified as DefnQ
  import Share.Postgres.Definitions.Types (TermComponentElementBytes (TermComponentElementBytes))
  import Share.Postgres.Hashes.Queries qualified as HashQ
  import Share.Postgres.IDs
  import Share.Postgres.Patches.Queries qualified as PatchQ
  import Share.Postgres.Serialization qualified as S
  import Share.Postgres.Sync.Types (TypedTempEntity (..))
  import Share.Prelude
  import Share.Utils.Logging qualified as Logging
  import Share.Web.Errors (InternalServerError (..), ToServerError (..), Unimplemented (Unimplemented))
  import Share.Web.UCM.Sync.Types
  import U.Codebase.Sqlite.Branch.Format (LocalBranchBytes)
  import U.Codebase.Sqlite.Branch.Format qualified as BranchFormat
  import U.Codebase.Sqlite.Branch.Full qualified as BranchFull
  import U.Codebase.Sqlite.Causal qualified as SqliteCausal
  import U.Codebase.Sqlite.Decl.Format qualified as DeclFormat
  import U.Codebase.Sqlite.Decode qualified as Decoders
  import U.Codebase.Sqlite.Entity qualified as Entity
  import U.Codebase.Sqlite.LocalIds qualified as LocalIds
  import U.Codebase.Sqlite.Patch.Format qualified as PatchFormat
  import U.Codebase.Sqlite.Patch.Full qualified as PatchFull
  import U.Codebase.Sqlite.Queries qualified as Share
  import U.Codebase.Sqlite.TempEntity (TempEntity)
  import U.Codebase.Sqlite.TempEntityType (TempEntityType)
  import U.Codebase.Sqlite.Term.Format qualified as TermFormat
  import Unison.Hash32
  import Unison.Hash32 qualified as Hash32
  import Unison.Sync.Common qualified as Share
  import Unison.Sync.Types qualified as Share
  import Unison.SyncV2.Types (EntityKind (..))
  
  data SyncQError
    = InvalidNamespaceBytes
    | InvalidPatchBytes
    | InvalidTermComponentBytes Decoders.DecodeError
    | InvalidDeclComponentBytes Decoders.DecodeError
    | ExpectedHashNotFound Hash32
    deriving (Show)
  
  instance ToServerError SyncQError where
    toServerError = \case
      InvalidNamespaceBytes {} -> ("namespace-format-invalid", err500)
      InvalidPatchBytes {} -> ("patch-format-invalid", err500)
      InvalidTermComponentBytes {} -> ("term-format-invalid", err500)
      InvalidDeclComponentBytes {} -> ("decl-format-invalid", err500)
      ExpectedHashNotFound h -> ("expected-hash-not-found", err500 {errBody = BL.pack $ "Expected hash not found: " <> show h})
  
  instance Logging.Loggable SyncQError where
    toLog = Logging.withSeverity Logging.Error . Logging.showLog
  
  -- | Read an entity out of the database that we know is in main storage.
  expectEntity :: (HasCallStack, QueryM m) => Codebase.CodebaseEnv -> Hash32 -> m (Share.Entity Text Hash32 Hash32)
  expectEntity codebase hash = do
    expectEntityKindForHash hash >>= \case
      CausalEntity -> do
        causalId <- CausalQ.expectCausalIdByHash codebase (fromHash32 @CausalHash hash)
        Share.C <$> CausalQ.expectCausalEntity causalId
      NamespaceEntity -> do
        bhId <- HashQ.expectBranchHashId codebase (fromHash32 @BranchHash $ hash)
        Share.N <$> CausalQ.expectNamespaceEntity bhId
      TermEntity -> do
        componentHashId <- HashQ.expectComponentHashId (fromHash32 @ComponentHash hash)
        Share.TC <$> DefnQ.expectShareTermComponent codebase componentHashId
      TypeEntity -> do
        componentHashId <- HashQ.expectComponentHashId (fromHash32 @ComponentHash hash)
        Share.DC <$> DefnQ.expectShareTypeComponentsOf codebase id componentHashId
      PatchEntity -> do
        patchId <- HashQ.expectPatchIdsOf codebase id (fromHash32 @PatchHash hash)
        Share.P <$> PatchQ.expectPatchEntity patchId
  
  -- | Determine the kind of an arbitrary hash.
  expectEntityKindForHash :: (QueryM m, HasCallStack) => Hash32 -> m EntityKind
  expectEntityKindForHash h =
    do
      queryExpect1Row
        [sql|
        SELECT
          EXISTS(SELECT FROM causals WHERE hash = #{h}),
          EXISTS(SELECT FROM branch_hashes WHERE branch_hashes.base32 = #{h}),
          EXISTS(SELECT FROM terms JOIN component_hashes ON component_hashes.id = terms.component_hash_id WHERE component_hashes.base32 = #{h}),
          EXISTS(SELECT FROM types JOIN component_hashes ON component_hashes.id = types.component_hash_id WHERE component_hashes.base32 = #{h}),
          EXISTS(SELECT FROM patches WHERE hash = #{h})
      |]
      >>= \case
        (True, _, _, _, _) -> pure CausalEntity
        (_, True, _, _, _) -> pure NamespaceEntity
        (_, _, True, _, _) -> pure TermEntity
        (_, _, _, True, _) -> pure TypeEntity
        (_, _, _, _, True) -> pure PatchEntity
        _ -> unrecoverableError $ InternalServerError "expected-hash-not-found" (ExpectedHashNotFound h)
  
  -- | Efficiently determine the location of a batch of entities.
  entityLocations :: forall m. (QueryM m) => CodebaseEnv -> EntityBunch Hash32 -> m (EntityBunch (Hash32, Maybe Share.EntityLocation))
  entityLocations (CodebaseEnv {codebaseOwner}) sortedEntities = do
    causalLocations <- getCausalLocations codebaseOwner (causals sortedEntities)
    namespaceLocations <- getNamespaceLocations codebaseOwner (namespaces sortedEntities)
    termLocations <- getTermLocations codebaseOwner (terms sortedEntities)
    typeLocations <- getTypeLocations codebaseOwner (types sortedEntities)
    patchLocations <- getPatchLocations codebaseOwner (patches sortedEntities)
    pure
      EntityBunch
        { causals = causalLocations,
          namespaces = namespaceLocations,
          terms = termLocations,
          types = typeLocations,
          patches = patchLocations
        }
    where
      toEntityLocation :: UserId -> (Hash32, Bool, Bool) -> (Hash32, Maybe Share.EntityLocation)
      toEntityLocation _codebaseUserId = \(hash, inMain, inTemp) ->
        case (inMain, inTemp) of
          (True, True) ->
            -- TODO: This should never happen, but it does. Figure out why.
            -- error $ "entityLocationsOf: Entity in both main and temp storage: " <> show (hash, codebaseUserId)
            (hash, Just Share.EntityInMainStorage)
          (True, False) -> (hash, Just Share.EntityInMainStorage)
          (False, True) -> (hash, Just Share.EntityInTempStorage)
          (False, False) -> (hash, Nothing)
  
      getCausalLocations :: UserId -> [Hash32] -> m [(Hash32, Maybe Share.EntityLocation)]
      getCausalLocations codebaseUserId causalHashes = do
        queryListRows @(Hash32, Bool, Bool)
          [sql|
            WITH entities(hash) AS (
              SELECT * FROM ^{singleColumnTable causalHashes}
            )
          SELECT entities.hash,
            EXISTS(
            SELECT FROM causals causal
              JOIN causal_ownership co ON co.causal_id = causal.id
              WHERE causal.hash = entities.hash AND co.user_id = #{codebaseUserId}
            ),
            EXISTS(
              SELECT FROM temp_entity
                WHERE temp_entity.hash = entities.hash
                  AND temp_entity.user_id = #{codebaseUserId}
              )
            FROM entities
          |]
          <&> fmap (toEntityLocation codebaseUserId)
      getNamespaceLocations :: UserId -> [Hash32] -> m [(Hash32, Maybe Share.EntityLocation)]
      getNamespaceLocations codebaseUserId namespaceHashes = do
        queryListRows @(Hash32, Bool, Bool)
          [sql|
            WITH entities(hash) AS (
              SELECT * FROM ^{singleColumnTable namespaceHashes}
            )
          SELECT entities.hash,
            EXISTS(
            SELECT FROM namespaces
              JOIN branch_hashes ON branch_hashes.id = namespaces.namespace_hash_id
              JOIN namespace_ownership ON namespace_ownership.namespace_hash_id = namespaces.namespace_hash_id
              WHERE branch_hashes.base32 = entities.hash AND namespace_ownership.user_id = #{codebaseUserId}
            ),
            EXISTS(
              SELECT FROM temp_entity
                WHERE temp_entity.hash = entities.hash
                  AND temp_entity.user_id = #{codebaseUserId}
              )
            FROM entities
          |]
          <&> fmap (toEntityLocation codebaseUserId)
      getTermLocations :: UserId -> [Hash32] -> m [(Hash32, Maybe Share.EntityLocation)]
      getTermLocations codebaseUserId termHashes = do
        queryListRows @(Hash32, Bool, Bool)
          [sql|
            WITH entities(hash) AS (
              SELECT * FROM ^{singleColumnTable termHashes}
            )
          SELECT entities.hash,
            EXISTS(
            SELECT FROM terms
              JOIN sandboxed_terms ON sandboxed_terms.term_id = terms.id
              JOIN component_hashes ON component_hashes.id = terms.component_hash_id
              WHERE component_hashes.base32 = entities.hash
                AND sandboxed_terms.user_id = #{codebaseUserId}
            ),
            EXISTS(
              SELECT FROM temp_entity
                WHERE temp_entity.hash = entities.hash
                  AND temp_entity.user_id = #{codebaseUserId}
              )
            FROM entities
          |]
          <&> fmap (toEntityLocation codebaseUserId)
      getTypeLocations :: UserId -> [Hash32] -> m [(Hash32, Maybe Share.EntityLocation)]
      getTypeLocations codebaseUserId typeHashes = do
        queryListRows @(Hash32, Bool, Bool)
          [sql|
            WITH entities(hash) AS (
              SELECT * FROM ^{singleColumnTable typeHashes}
            )
          SELECT entities.hash,
            EXISTS(
            SELECT FROM types
              JOIN component_hashes ON component_hashes.id = types.component_hash_id
              JOIN sandboxed_types ON sandboxed_types.type_id = types.id
              WHERE component_hashes.base32 = entities.hash
                AND sandboxed_types.user_id = #{codebaseUserId}
            ),
            EXISTS(
              SELECT FROM temp_entity
                WHERE temp_entity.hash = entities.hash
                  AND temp_entity.user_id = #{codebaseUserId}
              )
            FROM entities
          |]
          <&> fmap (toEntityLocation codebaseUserId)
      getPatchLocations :: UserId -> [Hash32] -> m [(Hash32, Maybe Share.EntityLocation)]
      getPatchLocations codebaseUserId patchHashes = do
        queryListRows @(Hash32, Bool, Bool)
          [sql|
            WITH entities(hash) AS (
              SELECT * FROM ^{singleColumnTable patchHashes}
            )
          SELECT entities.hash,
            EXISTS(
            SELECT FROM patches
              JOIN patch_ownership ON patch_ownership.patch_id = patches.id
              WHERE patches.hash = entities.hash AND patch_ownership.user_id = #{codebaseUserId}
            ),
            EXISTS(
              SELECT FROM temp_entity
                WHERE temp_entity.hash = entities.hash
                  AND temp_entity.user_id = #{codebaseUserId}
              )
            FROM entities
          |]
          <&> fmap (toEntityLocation codebaseUserId)
  
  -- | Save a temp entity to the temp entities table, also tracking its missing dependencies.
  -- You can pass ALL the dependencies of the temp entity, the query will determine which ones
  -- are missing.
  saveTempEntities :: (QueryM m, Foldable f) => CodebaseEnv -> f (Hash32, Share.Entity Text Hash32 Hash32) -> m ()
  saveTempEntities (CodebaseEnv {codebaseOwner}) entities = do
    let tempEntities =
          Foldable.toList entities <&> \(hash, entity) ->
            let te = Share.entityToTempEntity id entity
             in (hash, te, Entity.entityType te, Share.entityDependencies entity)
    let tempEntitiesTable :: [(Hash32, TempEntity, TempEntityType)]
        tempEntitiesTable = tempEntities <&> \(hash, te, entityType, _) -> (hash, te, entityType)
    let dependenciesTable :: [(Hash32, Hash32)]
        dependenciesTable = tempEntities & foldMap (\(hash, _, _, deps) -> (hash,) <$> Set.toList deps)
    execute_
      [sql|
        WITH temp_entities (hash, bytes, kind) AS (
          SELECT * FROM ^{toTable tempEntitiesTable}
        )
        INSERT INTO temp_entity (user_id, hash, bytes, kind)
          SELECT #{codebaseOwner}, hash, bytes, kind::entity_kind FROM temp_entities
        ON CONFLICT DO NOTHING
      |]
    execute_
      [sql|
          WITH dependencies (dependent, dependency) AS (
            SELECT * FROM ^{toTable dependenciesTable}
          )
          INSERT INTO temp_entity_missing_dependency (user_id, dependent, dependency)
          SELECT #{codebaseOwner}, dependencies.dependent, dependencies.dependency
            FROM dependencies
            -- Filter out any dependencies that we already have in main storage.
            WHERE NOT have_hash_in_codebase(#{codebaseOwner}, dependencies.dependency)
            ON CONFLICT DO NOTHING
      |]
  
  -- | Filter down the given hashes to ones which are ready to flush.
  filterForFlushableHashes :: (QueryM m) => CodebaseEnv -> Set Hash32 -> m (Set Hash32)
  filterForFlushableHashes (CodebaseEnv {codebaseOwner}) hashesToCheck = do
    queryListCol
      [sql|
      WITH hashes_to_check(hash) AS (
        SELECT * FROM ^{singleColumnTable (Set.toList hashesToCheck)}
      )
        SELECT hash FROM hashes_to_check
          WHERE NOT EXISTS(SELECT FROM temp_entity_missing_dependency missing_dep
                            WHERE missing_dep.dependent = hashes_to_check.hash
                              AND missing_dep.user_id = #{codebaseOwner}
                          )
    |]
      <&> Set.fromList
  
  -- | Remove a single entity that's ready to flush from the temp entities table,
  -- and return it. If the provided hash isn't ready to flush, don't return anything.
  tryPopFlushableTempEntity :: (QueryA m) => CodebaseEnv -> Hash32 -> m (Maybe (Hash32, TempEntity))
  tryPopFlushableTempEntity (CodebaseEnv {codebaseOwner}) hashToCheck = do
    query1Row
      [sql|
      DELETE FROM temp_entity
        WHERE temp_entity.user_id = #{codebaseOwner}
          AND temp_entity.hash = #{hashToCheck}
          AND NOT EXISTS(SELECT FROM temp_entity_missing_dependency missing_dep
                            WHERE missing_dep.dependent = #{hashToCheck}
                              AND missing_dep.user_id = #{codebaseOwner}
                          )
        RETURNING hash, kind, bytes
    |]
      <&> fmap \((Only hash) :. TypedTempEntity te) -> (hash, te)
  
  -- | Delete missing-dependency rows for an entity which has been
  -- saved to main storage, returning the set of dependents.
  clearTempDependencies :: (QueryA m) => CodebaseEnv -> Hash32 -> m (Set Hash32)
  clearTempDependencies (CodebaseEnv {codebaseOwner}) hash = do
    queryListCol @Hash32
      [sql|
      DELETE FROM temp_entity_missing_dependency dep
        WHERE dep.user_id = #{codebaseOwner}
          AND dep.dependency = #{hash}
        RETURNING dependent
    |]
      <&> Set.fromList
  
  -- | Save a temp entity to main storage, and clear any missing dependency rows for it, and
  -- return the set of hashes which dependended on it and _might_ now be ready to flush.
  saveTempEntityInMain :: forall m. (HasCallStack, QueryM m) => Codebase.CodebaseEnv -> Hash32 -> TempEntity -> m (Set Hash32)
  saveTempEntityInMain codebase hash entity = do
    saveEntity entity
    clearTempDependencies codebase hash
    where
      saveEntity :: TempEntity -> m ()
      saveEntity = \case
        Entity.TC (TermFormat.SyncTerm syncTermComponent) -> do
          doTermComponent entity syncTermComponent
        Entity.DC (DeclFormat.SyncDecl syncDecl) -> do
          doDeclComponent entity syncDecl
        Entity.N BranchFormat.SyncDiff {} ->
          unrecoverableError $ InternalServerError "namespace-diffs-not-supported" Unimplemented
        Entity.N (BranchFormat.SyncFull localIds bytes) -> doNamespace entity localIds bytes
        Entity.P PatchFormat.SyncDiff {} -> unrecoverableError $ InternalServerError "patch-diffs-not-supported" Unimplemented
        Entity.P (PatchFormat.SyncFull localIds patchBytes) -> doPatch entity localIds patchBytes
        Entity.C (SqliteCausal.SyncCausalFormat {valueHash, parents}) -> doCausal entity valueHash parents
      doTermComponent ::
        TempEntity ->
        TermFormat.SyncLocallyIndexedComponent' Text Hash32 ->
        m ()
      doTermComponent entity syncTermComponent = do
        let (TermFormat.SyncLocallyIndexedComponent termComponentElements) = syncTermComponent
        elementsWithDecodedType <-
          for termComponentElements \(localIds, termElementBytes) ->
            case S.decodeTermComponentElementType termElementBytes of
              Left decodeErr -> unrecoverableError $ InternalServerError "term-format-invalid" (InvalidTermComponentBytes $ from decodeErr)
              Right typ -> pure (localIds, TermComponentElementBytes termElementBytes, typ)
        componentWithIds <-
          ( Defn.ensureTextIdsOf (traversed . _1 . LocalIds.t_) elementsWithDecodedType
              >>= HashQ.ensureComponentHashIdsOf (traversed . _1 . LocalIds.h_ . hash32AsComponentHash_)
            )
            <&> Vector.toList
        Defn.saveEncodedTermComponent codebase componentHash (Just entity) componentWithIds
      doDeclComponent ::
        TempEntity ->
        DeclFormat.SyncLocallyIndexedComponent' Text Hash32 ->
        m ()
      doDeclComponent entity syncDecl = do
        declComponentElements <- case Decoders.unsyncDeclComponent syncDecl of
          Left decodeErr -> unrecoverableError $ InternalServerError "term-format-invalid" (InvalidTermComponentBytes decodeErr)
          Right (DeclFormat.LocallyIndexedComponent elements) -> pure elements
        componentWithIds <-
          ( Defn.ensureTextIdsOf (traversed . _1 . LocalIds.t_) declComponentElements
              >>= HashQ.ensureComponentHashIdsOf (traversed . _1 . LocalIds.h_ . hash32AsComponentHash_)
            )
            <&> Vector.toList
        Defn.saveTypeComponent codebase componentHash (Just entity) componentWithIds
  
      doNamespace ::
        TempEntity ->
        BranchFormat.BranchLocalIds' Text Hash32 Hash32 (Hash32, Hash32) ->
        LocalBranchBytes ->
        m ()
      doNamespace entity localIds bytes = do
        pgNamespace <-
          (expandNamespaceFormat localIds bytes)
            >>= Defn.ensureTextIdsOf BranchFull.t_
            >>= HashQ.expectComponentHashIdsOf (BranchFull.h_ . hash32AsComponentHash_)
            >>= HashQ.expectPatchIdsOf codebase (BranchFull.p_ . hash32AsPatchHash_)
            >>= HashQ.expectCausalIdsOf codebase (BranchFull.c_ . childHashT)
        void $ CausalQ.savePgNamespace codebase (Just entity) (Just (BranchHash . Hash32.toHash $ hash)) pgNamespace
  
      doPatch ::
        TempEntity ->
        PatchFormat.PatchLocalIds' Text Hash32 Hash32 ->
        ByteString ->
        m ()
      doPatch entity localIds patchBytes = do
        pgPatch <-
          expandPatchFormat localIds patchBytes
            >>= Defn.ensureTextIdsOf PatchFull.patchT_
            -- The LHS patch component refs need to be 'ensured', not 'expected', because they
            -- aren't technically dependencies of the patch and there's no guarantee they were
            -- synced. Whereas for all other hashes there's a bug in dependency resolution
            -- if for some reason they don't already exist.
            >>= HashQ.ensureComponentHashIdsOf (PatchFull.patchH_ . hash32AsComponentHash_)
            >>= HashQ.expectComponentHashIdsOf (PatchFull.patchO_ . hash32AsComponentHash_)
        void $ PatchQ.savePatch codebase (Just entity) (PatchHash . Hash32.toHash $ hash) pgPatch
  
      doCausal ::
        TempEntity ->
        Hash32 ->
        Vector.Vector Hash32 ->
        m ()
      doCausal serialized valueHash parents = do
        bhId <- HashQ.expectBranchHashId codebase $ BranchHash (Hash32.toHash valueHash)
        parentIds <-
          HashQ.expectCausalIdsOf codebase (traversed . hash32AsCausalHash_) parents
            <&> (Vector.toList >>> fmap snd >>> Set.fromList)
        void $ CausalQ.saveCausal codebase (Just serialized) (Just . CausalHash . Hash32.toHash $ hash) bhId parentIds
  
      componentHash :: ComponentHash
      componentHash = ComponentHash . Hash32.toHash $ hash
      -- This traversal takes a (branchHash, causalHash) pair, discards the branch hash, then
      -- takes the ids expanded by the action and returns a (branchHashId, causalId) pair.
      childHashT :: Traversal (Hash32, Hash32) (BranchHashId, CausalId) CausalHash (BranchHashId, CausalId)
      childHashT f (_branchHash, causalHash) = f (CausalHash . Hash32.toHash $ causalHash)
      expandNamespaceFormat ::
        (Ord text, Ord defn) =>
        (BranchFormat.BranchLocalIds' text defn patch child) ->
        BranchFormat.LocalBranchBytes ->
        m (BranchFull.Branch' text defn patch child)
      expandNamespaceFormat localIds (BranchFormat.LocalBranchBytes bytes) = do
        case S.decodeNamespace bytes of
          Left {} -> unrecoverableError $ InternalServerError "namespace-format-invalid" InvalidNamespaceBytes
          Right localNamespace -> do
            pure $ BranchFormat.localToBranch localIds localNamespace
      expandPatchFormat :: (Ord text, Ord hash, Ord defn) => PatchFormat.PatchLocalIds' text hash defn -> ByteString -> m (PatchFull.Patch' text hash defn)
      expandPatchFormat patchLocalIds patchBytes = do
        case S.decodePatch patchBytes of
          Left {} -> unrecoverableError $ InternalServerError "patch-format-invalid" InvalidPatchBytes
          Right localPatch -> do
            pure $ PatchFormat.localPatchToPatch' patchLocalIds localPatch
  
  -- | "Elaborate" a set of hashes that we are considering asking to client to upload to Unison Share.
  --
  -- For each hash, we transitively follow its dependencies in temp_entity_missing_dependency
  -- until we find the tips of the dependency graph. We then return the set of those
  -- dependencies which still need to be uploaded by the client, and the set which are ready to
  -- be flushed to main storage.
  elaborateHashes :: (QueryM m) => CodebaseEnv -> NESet Hash32 -> m (Maybe (NESet Hash32 {- needed from client -}), Maybe (NESet Hash32 {- ready to flush -}))
  elaborateHashes (CodebaseEnv {codebaseOwner}) hashSet = do
    rows <-
      queryListRows @(Hash32, Bool, Bool)
        [sql|
              WITH RECURSIVE unelaborated_dependency(hash) AS (
                SELECT * FROM ^{singleColumnTable (Foldable.toList hashSet)}
              ),
              elaborated_dependency (hash) AS (
                SELECT unelaborated_dependency.hash FROM unelaborated_dependency
                UNION
                (SELECT temp_entity_missing_dependency.dependency
                FROM temp_entity_missing_dependency
                  JOIN elaborated_dependency
                    ON temp_entity_missing_dependency.dependent = elaborated_dependency.hash
                WHERE
                  temp_entity_missing_dependency.user_id = #{codebaseOwner}
                )
              ), tagged_hashes (hash, in_temp, has_dependencies) AS (
                SELECT elaborated_dependency.hash,
                       EXISTS(SELECT FROM temp_entity
                              WHERE elaborated_dependency.hash = temp_entity.hash
                                AND temp_entity.user_id = #{codebaseOwner}
                             ) AS in_temp,
                       EXISTS(SELECT FROM temp_entity_missing_dependency missing_dep
                                  WHERE missing_dep.dependent = elaborated_dependency.hash
                                    AND missing_dep.user_id = #{codebaseOwner}
                                 ) AS has_dependencies
                  FROM elaborated_dependency
              ) SELECT tagged_hashes.hash, tagged_hashes.in_temp, tagged_hashes.has_dependencies
                FROM tagged_hashes
                -- If something is already in_temp but still has dependencies, then we don't care
                -- about it.
                  WHERE NOT (tagged_hashes.in_temp AND tagged_hashes.has_dependencies)
            |]
    let (neededHashes, readyToFlushHashes) =
          rows & foldMap \case
            (hash, inTemp, hasDependencies)
              | not inTemp -> (Set.singleton hash, Set.empty)
              | inTemp && not hasDependencies -> (Set.empty, Set.singleton hash)
              | otherwise ->
                  -- These should already be filtered out by the SQL query, we don't care about them.
                  mempty
    pure $ (NESet.nonEmptySet neededHashes, NESet.nonEmptySet readyToFlushHashes)
  
  getEntitiesReadyToFlush :: (QueryA m) => CodebaseEnv -> m [Hash32]
  getEntitiesReadyToFlush (CodebaseEnv {codebaseOwner}) = do
    queryListCol
      [sql|
        SELECT hash FROM temp_entity
          WHERE user_id = #{codebaseOwner}
            AND NOT EXISTS(SELECT FROM temp_entity_missing_dependency missing_dep
                            WHERE missing_dep.dependent = temp_entity.hash
                              AND missing_dep.user_id = #{codebaseOwner}
                          )
      |]
  