packages feed

cozo-hs-0.1.0.0: src/Database/Cozo/Internal.hs

{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-typed-holes #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# OPTIONS_HADDOCK hide #-}

{-# HLINT ignore "Use newtype instead of data" #-}

module Database.Cozo.Internal (
  open',
  close',
  runQuery',
  importRelations',
  exportRelations',
  backup',
  restore',
  importFromBackup',
  Connection,
  InternalCozoError (..),
  CozoNullResultPtrException (..),
) where

import Control.Exception (Exception)
import Control.Monad ((<=<))
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.Coerce (coerce)
import Database.Cozo.Internal.Bindings (
  cozoBackup,
  cozoCloseDB,
  cozoExportRelations,
  cozoFreeStr,
  cozoImportFromBackup,
  cozoImportRelations,
  cozoOpenDB,
  cozoRestore,
  cozoRunQuery,
 )
import Foreign (
  Ptr,
  Storable (peek),
  fromBool,
  maybePeek,
  new,
  toBool,
 )
import Foreign.C.Types (CChar, CInt)
import GHC.Generics (Generic)

newtype Connection = Connection (Ptr CInt)

{- |
Wrapper around primitive failure states.
-}
newtype InternalCozoError
  = InternalCozoError ByteString
  deriving (Show, Eq, Generic)

instance Exception InternalCozoError

data CozoNullResultPtrException = CozoNullResultPtrException deriving (Show, Eq, Generic)

instance Exception CozoNullResultPtrException

{- |
Open a connection to a cozo database

- engine: \"mem\", \"sqlite\" or \"rocksdb\"
- path: utf8 encoded filepath
- options: engine-specific options. \"{}\" is an acceptable empty value.
-}
open' ::
  -- | engine: \"mem\", \"sqlite\" or \"rocksdb\"
  ByteString ->
  -- | path: utf8 encoded filepath
  ByteString ->
  -- | options: engine-specific options. \"{}\" is an acceptable emtpy value.
  ByteString ->
  IO (Either InternalCozoError Connection)
open' engineBs pathBs optionBs =
  B.useAsCString engineBs $ \engine ->
    B.useAsCString pathBs $ \path ->
      B.useAsCString optionBs $ \options -> do
        intPtr <- new @CInt 0
        openMessagePtr <- cozoOpenDB engine path options intPtr
        !mOpenMessage <- maybePeek B.packCString openMessagePtr
        maybe
          (pure . Right . Connection $ intPtr)
          (\errStr -> (Left . InternalCozoError $ errStr) <$ cozoFreeStr openMessagePtr)
          mOpenMessage

{- |
True if the database was closed and False if it was already closed or if it
does not exist.
-}
close' :: Connection -> IO Bool
close' = fmap toBool . cozoCloseDB <=< peek . coerce

{- |
Run a query.

The `CozoError` that might arise from this function is a `CozoNullResultPtr`.
If there are any errors internal to cozo, those will be returned as part of the
JSON string returned in the Right value. Simple returning a Right value does not
mean the query was successful.

- script: utf8 encoded script to execute
- params_raw: a utf8 encoded, JSON formatted map of parameters for use in the script.
-}
runQuery' ::
  Connection ->
  ByteString ->
  ByteString ->
  IO (Either CozoNullResultPtrException ByteString)
runQuery' c q p =
  B.useAsCString q $ \q' ->
    B.useAsCString p $ \p' ->
      cozoCharPtrFn (\i -> cozoRunQuery i q' p' (fromBool False)) c

{- |
Import data in relations.

Triggers are not run for relations, if you wish to activate triggers, use a query
  with parameters.

The given bytestring is a utf8, JSON formatted payload of relations.
In the same form as that given by `exportRelations'`
-}
importRelations' ::
  Connection ->
  ByteString ->
  IO (Either CozoNullResultPtrException ByteString)
importRelations' c payloadBs =
  B.useAsCString payloadBs $ \payload ->
    cozoCharPtrFn (`cozoImportRelations` payload) c

{- |
Export relations into JSON

The given bytestring must be a utf8 encoded JSON payload. See the manual for expected
fields.
-}
exportRelations' ::
  Connection ->
  ByteString ->
  IO (Either CozoNullResultPtrException ByteString)
exportRelations' c payloadBs =
  B.useAsCString payloadBs $ \payload ->
    cozoCharPtrFn (`cozoExportRelations` payload) c

{- |
Backup a database.

Accepts the path of the output file.
-}
backup' :: Connection -> ByteString -> IO (Either CozoNullResultPtrException ByteString)
backup' c pathBs =
  B.useAsCString pathBs $ \path ->
    cozoCharPtrFn (`cozoBackup` path) c

{- |
Restore a database from a backup.
-}
restore' :: Connection -> ByteString -> IO (Either CozoNullResultPtrException ByteString)
restore' c pathBs =
  B.useAsCString pathBs $ \path ->
    cozoCharPtrFn (`cozoRestore` path) c

{- |
Import relations from a backup.

Note that triggers are not run for the relations.
To run triggers, use a query with parameters.

- payload: @"{'path': ..., 'relations': [...]}"@
-}
importFromBackup' ::
  Connection ->
  ByteString ->
  IO (Either CozoNullResultPtrException ByteString)
importFromBackup' c payloadBs =
  B.useAsCString payloadBs $ \payload ->
    cozoCharPtrFn (`cozoImportFromBackup` payload) c

{- |
Helper function for using cozo bindings that do an action and return a
string that needs to be freed.
-}
cozoCharPtrFn ::
  (CInt -> IO (Ptr CChar)) ->
  Connection ->
  IO (Either CozoNullResultPtrException ByteString)
cozoCharPtrFn a (Connection intPtr) = do
  dbId <- peek intPtr
  rPtr <- a dbId
  !mR <- maybePeek B.packCString rPtr
  case mR of
    Nothing -> pure . Left $ CozoNullResultPtrException
    Just r -> Right r <$ cozoFreeStr rPtr