kioku-core-0.1.0.0: src/Kioku/Recall/Capability.hs
module Kioku.Recall.Capability
( VectorCapability (..),
detectVectorCapability,
classifyProbe,
CapabilityProbe (..),
)
where
import Data.Int (Int32)
import Effectful (Eff, (:>))
import Hasql.Decoders qualified as D
import Hasql.Encoders qualified as E
import Hasql.Statement (Statement, preparable)
import Hasql.Transaction qualified as Tx
import Kioku.Prelude
import Kiroku.Store.Effect (Store)
import Kiroku.Store.Transaction (runTransaction)
data VectorCapability
= VectorAvailable
| VectorExtensionUnavailable
| VectorColumnsUnavailable ![Text]
| -- | @KIOKU_EMBEDDING_DIMENSIONS@ disagrees with the declared width of the
-- @kioku_memories.embedding@ column: configured first, actual second. Every embedding
-- write would fail on the @::vector@ cast, one event at a time, forever.
VectorDimensionMismatch !Int !Int
deriving stock (Generic, Eq, Show)
data CapabilityProbe = CapabilityProbe
{ hasVectorType :: !Bool,
hasEmbedding :: !Bool,
hasEmbeddingModel :: !Bool,
hasDimensions :: !Bool,
hasContentHash :: !Bool,
embeddingTypmod :: !(Maybe Int32)
}
deriving stock (Generic, Eq, Show)
-- | Probe what the vector path can actually do, given the dimension count the process is
-- configured with (@EmbeddingConfig.dimensions@).
detectVectorCapability ::
(Store :> es) =>
Int ->
Eff es VectorCapability
detectVectorCapability configuredDimensions =
classifyProbe configuredDimensions <$> runTransaction (Tx.statement () detectVectorCapabilityStmt)
classifyProbe :: Int -> CapabilityProbe -> VectorCapability
classifyProbe configuredDimensions probe
| not probe.hasVectorType = VectorExtensionUnavailable
| not (null missing) = VectorColumnsUnavailable missing
| Just actual <- declaredDimensions,
actual /= configuredDimensions =
VectorDimensionMismatch configuredDimensions actual
| otherwise = VectorAvailable
where
missing =
missingIf (not probe.hasEmbedding) "embedding"
<> missingIf (not probe.hasEmbeddingModel) "embedding_model"
<> missingIf (not probe.hasDimensions) "dimensions"
<> missingIf (not probe.hasContentHash) "content_hash"
-- For pgvector, a column's atttypmod *is* its declared dimension count. A column
-- declared without one reports -1, which constrains nothing, so there is nothing to
-- disagree with.
declaredDimensions =
case probe.embeddingTypmod of
Just typmod | typmod > 0 -> Just (fromIntegral typmod)
_ -> Nothing
missingIf :: Bool -> Text -> [Text]
missingIf True columnName = [columnName]
missingIf False _ = []
-- | The extension check asks @to_regtype@, not @pg_extension@, and the difference matters.
-- @pg_extension@ answers "is pgvector installed /somewhere/ in this database" — but recall
-- casts with a bare @$1::vector@, and the store connects with
-- @search_path = kiroku, pg_catalog@. An extension installed into @public@ (the usual
-- operator default) satisfies @pg_extension@ and still cannot be named, so every vector
-- query would fail with @42704@ while capability detection reported everything healthy.
-- @to_regtype@ resolves against the live @search_path@, which is exactly the question the
-- query asks.
detectVectorCapabilityStmt :: Statement () CapabilityProbe
detectVectorCapabilityStmt =
preparable
"""
SELECT
to_regtype('vector') IS NOT NULL AS has_vector_type,
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'kiroku' AND table_name = 'kioku_memories' AND column_name = 'embedding'
) AS has_embedding,
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'kiroku' AND table_name = 'kioku_memories' AND column_name = 'embedding_model'
) AS has_embedding_model,
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'kiroku' AND table_name = 'kioku_memories' AND column_name = 'dimensions'
) AS has_dimensions,
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'kiroku' AND table_name = 'kioku_memories' AND column_name = 'content_hash'
) AS has_content_hash,
(
SELECT a.atttypmod
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'kiroku'
AND c.relname = 'kioku_memories'
AND a.attname = 'embedding'
AND NOT a.attisdropped
) AS embedding_typmod
"""
E.noParams
( D.singleRow $
CapabilityProbe
<$> D.column (D.nonNullable D.bool)
<*> D.column (D.nonNullable D.bool)
<*> D.column (D.nonNullable D.bool)
<*> D.column (D.nonNullable D.bool)
<*> D.column (D.nonNullable D.bool)
<*> D.column (D.nullable D.int4)
)