srtree-db-0.1.0.0: src/Algorithm/EqSat/Storage/Query.hs
{-# LANGUAGE OverloadedStrings #-}
-- | SQL query API over a stored e-graph's @fit@ / @enode@ tables.
--
-- The queries are intentionally SQL-shaped (this is the slice of the
-- reggression functionality that runs directly in the database) and mirror the
-- in-memory counterparts in 'Algorithm.EqSat.Queries'. They are written
-- against 'Algorithm.EqSat.Storage.Backend', so the same SQL drives the
-- SQLite and PostgreSQL backends.
module Algorithm.EqSat.Storage.Query
( getOrCreateDataset
, datasetId
, firstDatasetId
, writeDatasetFit
, readDatasetFit
, topN
, pareto
, paretoBySize
, distributionCounts
, countPattern
, expressionEclass
, testedOnDataset
, versionsOf
) where
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text as T
import Algorithm.EqSat.Egraph (EClassId)
import Algorithm.EqSat.Storage.Backend
( SqlBackend, SqlValue(..), runDb, queryDb, sqlToInt, sqlToMaybeDouble, sqlToText )
import Algorithm.EqSat.Storage.Schema (createSchema)
-- | Resolve a dataset name to its @dataset@ row id, creating it if needed.
getOrCreateDataset :: SqlBackend db => db -> String -> IO Int
getOrCreateDataset db name = do
createSchema db
m <- datasetId db name
case m of
Just i -> pure i
Nothing -> do
runDb db "INSERT INTO dataset (name) VALUES (?)" [SqlText (T.pack name)]
r <- datasetId db name
pure (maybe 0 id r)
-- | Look up an existing dataset id by name.
datasetId :: SqlBackend db => db -> String -> IO (Maybe Int)
datasetId db name = do
rows <- queryDb db "SELECT id FROM dataset WHERE name = ?" [SqlText (T.pack name)]
pure $ case rows of
([i] : _) -> Just (sqlToInt i)
_ -> Nothing
-- | The id of the first dataset (used by the legacy 'loadGraph' path, which is
-- not dataset-scoped, to source per-class risk metrics from @dataset_fit@).
firstDatasetId :: SqlBackend db => db -> IO (Maybe Int)
firstDatasetId db = do
rows <- queryDb db "SELECT id FROM dataset ORDER BY id LIMIT 1" []
pure $ case rows of
([i] : _) -> Just (sqlToInt i)
_ -> Nothing
-- | Upsert a per-(dataset, e-class) fit row.
writeDatasetFit
:: SqlBackend db => db -> Int -> EClassId
-> Maybe Double -> Maybe Double -> Text -> Int -> IO ()
writeDatasetFit db ds eid fit dl theta sz = do
let (fitCol, fitVal) = case fit of
Nothing -> ("NULL", Nothing)
Just f -> ("?", Just (SqlFloat f))
(dlCol, dlVal) = case dl of
Nothing -> ("NULL", Nothing)
Just d -> ("?", Just (SqlFloat d))
runDb db
("INSERT OR REPLACE INTO dataset_fit \
\(dataset_id, eid, fitness, dl, theta, size, evaluated, fitted) \
\VALUES (?, ?, " <> fitCol <> ", " <> dlCol <> ", ?, ?, 1, 1)")
(catMaybes [ Just (SqlInteger (fromIntegral ds))
, Just (SqlInteger (fromIntegral eid))
, fitVal
, dlVal
, Just (SqlText theta)
, Just (SqlInteger (fromIntegral sz)) ])
-- | Read per-(dataset, e-class) fit rows.
readDatasetFit
:: SqlBackend db => db -> Int
-> IO [(EClassId, (Maybe Double, Maybe Double, Int, Text))]
readDatasetFit db ds = do
createSchema db
rows <- queryDb db
"SELECT eid, fitness, dl, size, theta FROM dataset_fit WHERE dataset_id = ?"
[SqlInteger (fromIntegral ds)]
pure [ (sqlToInt eid, (sqlToMaybeDouble f, sqlToMaybeDouble d, sqlToInt sz, sqlToText th))
| [eid, f, d, sz, th] <- rows ]
-- | The @n@ e-classes with the best fitness (on the dataset), descending.
topN :: SqlBackend db => db -> Int -> Int -> IO [(EClassId, Double)]
topN db ds n = do
rows <- queryDb db
"SELECT eid, fitness FROM dataset_fit \
\WHERE dataset_id = ? AND fitness IS NOT NULL \
\ORDER BY fitness DESC LIMIT ?"
[ SqlInteger (fromIntegral ds), SqlInteger (fromIntegral n) ]
pure [ (sqlToInt eid, f)
| [eid, f] <- rows
, Just f <- [sqlToMaybeDouble f] ]
-- | Non-dominated classes over (max fitness, min dl) on the dataset. Returns
-- the (eid, fitness, dl) triples that are not dominated by any other class.
pareto :: SqlBackend db => db -> Int -> IO [(EClassId, Double, Double)]
pareto db ds = do
rows <- queryDb db
"SELECT eid, fitness, dl FROM dataset_fit \
\WHERE dataset_id = ? AND fitness IS NOT NULL AND dl IS NOT NULL"
[SqlInteger (fromIntegral ds)]
let pts = [ (sqlToInt eid, f, d)
| [eid, ff, dd] <- rows
, Just f <- [sqlToMaybeDouble ff]
, Just d <- [sqlToMaybeDouble dd] ]
dominates (f1, d1) (f0, d0) = f1 >= f0 && d1 <= d0 && (f1 > f0 || d1 < d0)
nonDominated (eid_, f, d) = not (any (\(q0, qf, qd) -> dominates (qf, qd) (f, d)) pts)
pure [ p | p@(_, f, d) <- pts, nonDominated p ]
-- | Non-dominated classes over (max fitness, min size) on the dataset.
paretoBySize :: SqlBackend db => db -> Int -> IO [(EClassId, Double, Int)]
paretoBySize db ds = do
rows <- queryDb db
"SELECT eid, fitness, size FROM dataset_fit \
\WHERE dataset_id = ? AND fitness IS NOT NULL"
[SqlInteger (fromIntegral ds)]
let pts = [ (sqlToInt eid, f, s)
| [eid, ff, ss] <- rows
, Just f <- [sqlToMaybeDouble ff]
, let s = sqlToInt ss ]
dominates (f1, s1) (f0, s0) = f1 >= f0 && s1 <= s0 && (f1 > f0 || s1 < s0)
nonDominated (eid_, f, s) = not (any (\(q0, qf, qs) -> dominates (qf, qs) (f, s)) pts)
pure [ p | p@(_, f, s) <- pts, nonDominated p ]
-- | Number of evaluated e-classes per model size (up to @maxSize@) on the
-- dataset.
distributionCounts :: SqlBackend db => db -> Int -> Int -> IO [(Int, Int)]
distributionCounts db ds maxSize = do
rows <- queryDb db
"SELECT size, COUNT(*) FROM dataset_fit \
\WHERE dataset_id = ? AND fitness IS NOT NULL AND size <= ? \
\GROUP BY size ORDER BY size"
[ SqlInteger (fromIntegral ds), SqlInteger (fromIntegral maxSize) ]
pure [ (sqlToInt s, sqlToInt c) | [s, c] <- rows ]
-- | The e-class a previously-indexed expression maps to (NULL if never seen).
expressionEclass :: SqlBackend db => db -> Text -> IO (Maybe EClassId)
expressionEclass db key = do
rows <- queryDb db "SELECT eclass FROM expression_index WHERE expression_key = ?"
[SqlText key]
pure $ case rows of
([e] : _) -> Just (sqlToInt e)
_ -> Nothing
-- | Whether a class has a fitness (i.e. was evaluated/fitted) on a dataset.
testedOnDataset :: SqlBackend db => db -> Int -> EClassId -> IO Bool
testedOnDataset db ds eid = do
rows <- queryDb db
"SELECT 1 FROM dataset_fit WHERE dataset_id = ? AND eid = ? AND fitness IS NOT NULL"
[SqlInteger (fromIntegral ds), SqlInteger (fromIntegral eid)]
pure (not (null rows))
-- | The e-node content keys that make up an e-class (multiple versions of one
-- expression).
versionsOf :: SqlBackend db => db -> EClassId -> IO [Text]
versionsOf db eid = do
rows <- queryDb db "SELECT enode_key FROM eclass_node WHERE eid = ?"
[SqlInteger (fromIntegral eid)]
pure [ sqlToText k | [k] <- rows ]
-- | Number of distinct e-classes containing at least one e-node whose
-- specific operator matches (e.g. \"EAdd\", \"EMul\", \"Add\", \"LogAbs\").
countPattern :: SqlBackend db => db -> Text -> IO Int
countPattern db op = do
rows <- queryDb db
"SELECT COUNT(DISTINCT eclass_node.eid) \
\FROM eclass_node JOIN enode ON enode.key = eclass_node.enode_key \
\WHERE enode.op_detail = ?"
[ SqlText op ]
pure $ case rows of
row : _ | [n] <- row -> sqlToInt n
_ -> 0