packages feed

hydra-0.15.0: src/main/haskell/Hydra/Demos/Genpg/Runtime.hs

module Hydra.Demos.GenPG.Runtime where

import Hydra.Kernel
import qualified Hydra.Show.Errors as ShowError
import qualified Hydra.Pg.Model as Pg
import Hydra.Dsl.Pg.Mappings
import Hydra.Lib.Literals
import qualified Hydra.Extract.Core as ExtractCore
import qualified Hydra.Show.Core as ShowCore
import qualified Hydra.Dsl.Terms as Terms
import Hydra.Sources.Kernel.Types.Core (hydraCoreGraph)

import qualified Control.Monad as CM
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Maybe as Y


type PgTransform = M.Map String ([Pg.Vertex Term], [Pg.Edge Term])


type Result a = Either Error a

evaluate :: Context -> Graph -> Term -> Result Term
evaluate cx g term = reduceTerm cx g True term

evaluateEdge :: Context -> Graph -> Pg.Edge Term -> Term -> Result (Maybe (Pg.Edge Term))
evaluateEdge cx g (Pg.Edge label idSpec outSpec inSpec propSpecs) term = do
    id <- evaluate cx g $ Terms.apply idSpec term
    mOutId <- evaluate cx g (Terms.apply outSpec term) >>= ExtractCore.maybeTerm Right g
    mInId <- evaluate cx g (Terms.apply inSpec term) >>= ExtractCore.maybeTerm Right g
    props <- evaluateProperties cx g propSpecs term
    return $ case mOutId of
      Nothing -> Nothing
      Just outId -> case mInId of
        Nothing -> Nothing
        Just inId -> Just $ Pg.Edge label id outId inId props

evaluateProperties :: Context -> Graph -> M.Map Pg.PropertyKey Term -> Term -> Result (M.Map Pg.PropertyKey Term)
evaluateProperties cx g specs record = M.fromList . Y.catMaybes <$> (CM.mapM forPair $ M.toList specs)
  where
    forPair (k, spec) = do
      value <- evaluate cx g $ Terms.apply spec record
      case deannotateTerm value of
        TermMaybe mv -> case mv of
          Nothing -> return Nothing
          Just v -> return $ Just (k, v)
        _ -> Left $ ErrorOther (OtherError $ "expected an optional value for property " ++ Pg.unPropertyKey k ++ " but got " ++ ShowCore.term value)

evaluateVertex :: Context -> Graph -> Pg.Vertex Term -> Term -> Result (Maybe (Pg.Vertex Term))
evaluateVertex cx g (Pg.Vertex label idSpec propSpecs) record = do
  mId <- evaluate cx g (Terms.apply idSpec record) >>= ExtractCore.maybeTerm Right g
  props <- evaluateProperties cx g propSpecs record
  return $ case mId of
    Nothing -> Nothing
    Just id -> Just $ Pg.Vertex label id props

findTablesInTerm :: Term -> S.Set String
findTablesInTerm = foldOverTerm TraversalOrderPre f S.empty
  where
    f names term = case term of
      TermProject (Projection tname _) -> S.insert (unName tname) names
      _ -> names

findTablesInTerms :: [Term] -> S.Set String
findTablesInTerms terms = S.unions $ fmap findTablesInTerm terms

elementSpecsByTable :: Pg.LazyGraph Term -> Either String PgTransform
elementSpecsByTable (Pg.LazyGraph vertices edges) = do
    vertexPairs <- CM.mapM vertexPair vertices
    edgePairs <- CM.mapM edgePair edges
    return $ L.foldl addEdgePair (L.foldl addVertexPair M.empty vertexPairs) edgePairs
  where
    vertexPair v = case tableForVertex v of
          Left err -> Left err
          Right table -> Right (table, v)
    edgePair e = case tableForEdge e of
          Left err -> Left err
          Right table -> Right (table, e)
    addVertexPair m (table, v) = M.insert table (v : vertices, edges) m
      where
        (vertices, edges) = Y.fromMaybe ([], []) $ M.lookup table m
    addEdgePair m (table, e) = M.insert table (vertices, e : edges) m
      where
        (vertices, edges) = Y.fromMaybe ([], []) $ M.lookup table m

tableForEdge :: Pg.Edge Term -> Either String String
tableForEdge (Pg.Edge label id outId inId props) = if S.size tables == 1
    then Right $ L.head $ S.toList tables
    else Left $ "Specification for " ++ Pg.unEdgeLabel label ++ " edges has wrong number of tables: "
      ++ show tables
  where
    tables = findTablesInTerms $ [id, outId, inId] ++ M.elems props

tableForVertex :: Pg.Vertex Term -> Either String String
tableForVertex (Pg.Vertex label id props) = if S.size tables == 1
    then Right $ L.head $ S.toList tables
    else Left $ "Specification for " ++ Pg.unVertexLabel label ++ " vertices has wrong number of tables: "
      ++ show tables
  where
    tables = findTablesInTerms $ [id] ++ M.elems props

termRowToRecord :: TableType -> DataRow Term -> Term
termRowToRecord (TableType (RelationName tname) colTypes) (DataRow cells) = TermRecord $ Record (Name tname) $
    L.zipWith toField colTypes cells
  where
    toField (ColumnType (ColumnName cname) _) mvalue = Field (Name cname) $ TermMaybe mvalue

transformRecord :: Context -> Graph -> [Pg.Vertex Term] -> [Pg.Edge Term] -> Term -> Result ([Pg.Vertex Term], [Pg.Edge Term])
transformRecord cx g vspecs especs term = do
  vertices <- CM.mapM (\s -> evaluateVertex cx g s term) vspecs
  edges <- CM.mapM (\s -> evaluateEdge cx g s term) especs
  return (Y.catMaybes vertices, Y.catMaybes edges)

transformTable :: TableType -> FilePath -> [Pg.Vertex Term] -> [Pg.Edge Term] -> IO ([Pg.Vertex Term], [Pg.Edge Term])
transformTable tableType@(TableType (RelationName tableName) _) path vspecs especs = do
    (Table _ rows) <- decodeTableIo tableType path
    let cx = emptyContext
    pairs <- case CM.mapM (transformRecord cx hydraCoreGraph vspecs especs . termRowToRecord tableType) rows of
      Left ic -> fail $ ShowError.error ic
      Right ps -> return ps
    return $ L.foldl addRow ([], []) pairs
  where
    filePath = tableName
    addRow (vertices, edges) (v, e) = (vertices ++ v, edges ++ e)

transformTables :: FilePath -> [TableType] -> Pg.LazyGraph Term -> IO (Pg.LazyGraph Term)
transformTables fileRoot tableTypes spec = do
    transform <- case (elementSpecsByTable spec) of
      Left err -> fail $ "Error in mapping specification: " ++ err
      Right t -> return t
    pairs <- CM.mapM forTable $ M.toList transform
    let (vertices, edges) = L.foldl addRow ([], []) pairs
    return $ Pg.LazyGraph vertices edges
  where
    addRow (vertices, edges) (v, e) = (vertices ++ v, edges ++ e)
    forTable (tname, (vspecs, especs)) = case M.lookup (RelationName tname) tableTypesByName of
        Nothing -> fail $ "Table specified in mapping does not exist: " ++ tname
        Just tableType -> do
          (vertices, edges) <- transformTable tableType path vspecs especs
          return (vertices, edges)
      where
        path = fileRoot ++ "/" ++ tname
    tableTypesByName = M.fromList $ fmap (\t -> (tableTypeName t, t)) tableTypes


--------------------------------------------------------------------------------
-- Table reading (consider making this into a separate module)

decodeTable :: TableType -> Table String -> Either String (Table Term)
decodeTable (TableType _ colTypes) (Table mheader rows) = do
    -- TODO: check header names against the table schema
    -- TODO: check that all rows have the same length as the header
    drows <- CM.zipWithM decodeRow rows [1..]
    return $ Table mheader $ fmap DataRow drows
  where
    decodeRow (DataRow row) lineno = CM.zipWithM decodeCell colTypes row
      where
        decodeCell (ColumnType (ColumnName cname) typ) mvalue = case mvalue of
          Nothing -> Right Nothing
          Just value -> case typ of
              TypeLiteral lt -> case lt of
                LiteralTypeBoolean -> readValue Terms.boolean readBoolean value
                LiteralTypeFloat ft -> case ft of
                   FloatTypeBigfloat -> readValue Terms.bigfloat readFloat64 value
                   FloatTypeFloat32 -> readValue Terms.float32 readFloat32 value
                   FloatTypeFloat64 -> readValue Terms.float64 readFloat64 value
                LiteralTypeInteger it -> case it of
                  IntegerTypeInt32 -> readValue Terms.int32 readInt32 value
                  IntegerTypeInt64 -> readValue Terms.int64 readInt64 value
                  _ -> unsupported
                LiteralTypeString -> readValue Terms.string Just value
              _ -> unsupported
            where
              toEither mv = case mv of
                Nothing -> Left $ "Invalid value of type " ++ ShowCore.type_ typ ++ " for column " ++ show cname
                  ++ " on line " ++ show lineno ++ ": " ++ value
                Just v -> Right v
              unsupported = Left $ "Unsupported type for column " ++ show cname ++ ": " ++ ShowCore.type_ typ
              readValue cons read value = (Just . cons) <$> (toEither $ read value)

decodeTableIo :: TableType -> FilePath -> IO (Table Term)
decodeTableIo tableType path = do
    table <- readTable True path
    case decodeTable tableType table of
      Left err -> fail err
      Right t -> return t

-- Note: LLM-generated function. Not thoroughly tested.
parseCsvLine :: String -> Either String [Maybe String]
parseCsvLine = go [] [] False False
  where
    go acc field inQuotes escape [] =
      finalize acc field inQuotes

    go acc field inQuotes escape (c:cs)
      | c == '"' =
          if inQuotes
            then case cs of
              ('"':cs') -> go acc (field ++ ['"']) True False cs'  -- Escaped quote
              _         -> go acc field False False cs             -- End quote
          else if null field
            then go acc field True False cs                       -- Start quote
          else Left "Unexpected quote inside unquoted field"

      | c == ',' && not inQuotes =
          go (acc ++ [normalize field]) [] False False cs

      | otherwise =
          go acc (field ++ [c]) inQuotes False cs

    finalize acc field inQuotes
      | inQuotes  = Left "Unclosed quoted field"
      | otherwise = Right (acc ++ [normalize field])

    normalize "" = Nothing
    normalize s  = Just s

readTable :: Bool -> FilePath -> IO (Table String)
readTable hasHeader path = do
  rawLines <- fmap lines $ readFile path
  case CM.zipWithM parseLine rawLines [1..] of
    Left err -> fail $ "CSV read error in " ++ show path ++ ": " ++ err
    Right rows0 -> do
      let rows = Y.catMaybes rows0
      if hasHeader
        then do
          let headerRow = L.head rows
          if any Y.isNothing headerRow
            then fail $ "null header column(s) in " ++ show path
            else pure $ Table (Just $ HeaderRow $ Y.catMaybes headerRow) $ fmap DataRow $ L.tail rows
        else pure $ Table Nothing $ fmap DataRow rows
  where
    parseLine line number = if L.null trimmed
        then pure Nothing
        else case parseCsvLine trimmed of
          Left err -> Left $ "CSV error in " ++ show path ++ ", line " ++ show number ++ ": " ++ err
          Right row -> pure $ Just row
      where
        trimmed = stripLeadingAndTrailingWhitespace line