ejdb2-binding (empty) → 0.1.0.0
raw patch · 36 files changed
+2444/−0 lines, 36 filesdep +aesondep +basedep +bytestringsetup-changedbinary-added
Dependencies added: aeson, base, bytestring, directory, ejdb2-binding, tasty, tasty-hunit, unordered-containers, vector
Files
- CHANGELOG.md +0/−0
- LICENSE +21/−0
- Setup.hs +2/−0
- cbits/finalizer.c +8/−0
- cbits/finalizer.h +3/−0
- ejdb2-binding.cabal +79/−0
- src/Database/EJDB2.hs +323/−0
- src/Database/EJDB2/Bindings/EJDB2.hs +71/−0
- src/Database/EJDB2/Bindings/JBL.hs +30/−0
- src/Database/EJDB2/Bindings/JQL.hs +38/−0
- src/Database/EJDB2/Bindings/Types/EJDB.hs +5/−0
- src/Database/EJDB2/Bindings/Types/EJDBDoc.hsc +33/−0
- src/Database/EJDB2/Bindings/Types/EJDBExec.hsc +80/−0
- src/Database/EJDB2/CollectionMeta.hs +23/−0
- src/Database/EJDB2/HTTP.hsc +105/−0
- src/Database/EJDB2/IndexMeta.hs +21/−0
- src/Database/EJDB2/IndexMode.hsc +38/−0
- src/Database/EJDB2/JBL.hs +78/−0
- src/Database/EJDB2/KV.hsc +110/−0
- src/Database/EJDB2/Meta.hs +22/−0
- src/Database/EJDB2/Options.hsc +65/−0
- src/Database/EJDB2/Query.hs +156/−0
- src/Database/EJDB2/QueryConstructor.hs +14/−0
- src/Database/EJDB2/Result.hsc +172/−0
- src/Database/EJDB2/WAL.hsc +60/−0
- test/Asserts.hs +16/−0
- test/CollectionTests.hs +72/−0
- test/DeleteTests.hs +41/−0
- test/GetTests.hs +143/−0
- test/IndexTests.hs +52/−0
- test/Main.hs +35/−0
- test/OnlineBackupTests.hs +53/−0
- test/Plant.hs +25/−0
- test/PutTests.hs +144/−0
- test/QueryTests.hs +306/−0
- test/read-only-db binary
+ CHANGELOG.md view
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2020 Francesco Burelli++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cbits/finalizer.c view
@@ -0,0 +1,8 @@+#include "finalizer.h"+#include <stdlib.h>++void finalizerJQL(JQL* jql) {+ JQL* jqlCopy = jql;+ jql_destroy(jql);+ free(jqlCopy);+}
+ cbits/finalizer.h view
@@ -0,0 +1,3 @@+#include <ejdb2/ejdb2.h>++void finalizerJQL(JQL* jql);
+ ejdb2-binding.cabal view
@@ -0,0 +1,79 @@+cabal-version: 2.4+name: ejdb2-binding+category: Database+description: Binding to EJDB2 C library, an embedded JSON noSQL database. Package requires libejdb2 to build. Please see the README on GitHub at <https://github.com/cescobaz/ejdb2haskell#readme>+synopsis: Binding to EJDB2 C library, an embedded JSON noSQL database+version: 0.1.0.0+license: MIT+license-file: LICENSE+homepage: https://github.com/cescobaz/ejdb2haskell#readme+bug-reports: https://github.com/cescobaz/ejdb2haskell/issues+copyright: 2020 Francesco Burelli+author: Francesco Burelli+maintainer: francesco.burelli@protonmail.com+extra-source-files: CHANGELOG.md+ cbits/finalizer.h+ cbits/finalizer.c+ test/read-only-db++source-repository head+ type: git+ location: https://github.com/cescobaz/ejdb2haskell++library+ exposed-modules: Database.EJDB2+ , Database.EJDB2.Query+ , Database.EJDB2.Meta+ , Database.EJDB2.CollectionMeta+ , Database.EJDB2.IndexMeta+ , Database.EJDB2.Options+ , Database.EJDB2.HTTP+ , Database.EJDB2.WAL+ , Database.EJDB2.KV+ other-modules:+ Database.EJDB2.JBL+ , Database.EJDB2.Result+ , Database.EJDB2.IndexMode+ , Database.EJDB2.QueryConstructor+ , Database.EJDB2.Bindings.EJDB2+ , Database.EJDB2.Bindings.JQL+ , Database.EJDB2.Bindings.JBL+ , Database.EJDB2.Bindings.Types.EJDB+ , Database.EJDB2.Bindings.Types.EJDBExec+ , Database.EJDB2.Bindings.Types.EJDBDoc+ build-depends: base ^>=4.12.0.0+ , bytestring ^>=0.10.10.0+ , aeson ^>=1.4.6.0+ , unordered-containers ^>=0.2.10.0+ build-tool-depends: hsc2hs:hsc2hs+ extra-libraries: ejdb2+ pkgconfig-depends: libejdb2+ c-sources: cbits/finalizer.c+ install-includes: finalizer.h+ include-dirs: cbits+ hs-source-dirs: src+ default-language: Haskell2010++test-suite ejdb2haskell-test+ default-language: Haskell2010+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ other-modules:+ Asserts+ Plant+ GetTests+ QueryTests+ PutTests+ DeleteTests+ CollectionTests+ IndexTests+ OnlineBackupTests+ build-depends: base ^>=4.12.0.0+ , tasty ^>=1.2.3+ , tasty-hunit ^>=0.10.0.2+ , aeson ^>=1.4.6.0+ , unordered-containers ^>=0.2.10.0+ , vector ^>=0.12.1.2+ , directory ^>=1.3.6.0+ , ejdb2-binding
+ src/Database/EJDB2.hs view
@@ -0,0 +1,323 @@+module Database.EJDB2+ ( init+ , Database+ , KV.readonlyOpenFlags+ , KV.truncateOpenFlags+ , KV.noTrimOnCloseOpenFlags+ , minimalOptions+ , open+ , close+ , getById+ , getCount+ , getList+ , getList'+ , putNew+ , put+ , mergeOrPut+ , patch+ , delete+ , ensureCollection+ , removeCollection+ , renameCollection+ , getMeta+ , IndexMode.IndexMode+ , IndexMode.uniqueIndexMode+ , IndexMode.strIndexMode+ , IndexMode.f64IndexMode+ , IndexMode.i64IndexMode+ , ensureIndex+ , removeIndex+ , onlineBackup+ ) where++import Control.Exception+import Control.Monad++import qualified Data.Aeson as Aeson+import qualified Data.ByteString as BS+import Data.IORef+import Data.Int+import Data.Word++import Database.EJDB2.Bindings.EJDB2+import Database.EJDB2.Bindings.JBL+import Database.EJDB2.Bindings.Types.EJDB+import Database.EJDB2.Bindings.Types.EJDBDoc as EJDBDoc+import Database.EJDB2.Bindings.Types.EJDBExec as EJDBExec+import qualified Database.EJDB2.IndexMode as IndexMode+import Database.EJDB2.JBL+import qualified Database.EJDB2.KV as KV+import Database.EJDB2.Options as Options+import Database.EJDB2.Query+import Database.EJDB2.QueryConstructor+import Database.EJDB2.Result++import Foreign+import Foreign.C.String+import Foreign.C.Types+import Foreign.Marshal.Alloc+import Foreign.Ptr+import Foreign.Storable++import Prelude hiding ( init )++-- | Reference to database. You can create it by 'open'.+data Database = Database (Ptr EJDB) EJDB++-- | Create minimal 'Options' for opening a database: just path to file and opening mode.+minimalOptions :: String -- ^ Database file path + -> [KV.OpenFlags] -- ^ Open mode+ -> Options -- ^ Options to use in 'open'++minimalOptions path openFlags =+ Options.zero { kv = KV.zero { KV.path = Just path, KV.oflags = openFlags }+ }++{-|+ ejdb2 initialization routine.++ /Must be called before using any of ejdb API function./+-}+init :: IO ()+init = c_ejdb_init >>= checkRC++{-|+ Open storage file.++ Storage can be opened only by one single process at time.++ /Remember to release database by 'close' when database is no longer required./+-}+open :: Options -> IO Database+open opts = do+ ejdbPtr <- malloc+ optsB <- build opts+ with optsB $ \optsPtr -> do+ result <- decodeRC <$> c_ejdb_open optsPtr ejdbPtr+ if result == Ok+ then Database ejdbPtr <$> peek ejdbPtr+ else free ejdbPtr >> fail (show result)++{-|+ Closes storage and frees up all resources.+-}+close :: Database -> IO ()+close (Database ejdbPtr _) = do+ result <- decodeRC <$> c_ejdb_close ejdbPtr+ if result == Ok then free ejdbPtr else fail $ show result++-- | Retrieve document identified by given id from collection.+getById :: Aeson.FromJSON a+ => Database+ -> String -- ^ Collection name+ -> Int64 -- ^ Document identifier. Not zero+ -> IO (Maybe a)+getById (Database _ ejdb) collection id = alloca $ \jblPtr ->+ finally (do+ rc <- withCString collection $ \cCollection ->+ c_ejdb_get ejdb cCollection (CIntMax id) jblPtr+ let result = decodeRC rc+ case result of+ Ok -> peek jblPtr >>= decode+ ErrorNotFound -> return Nothing+ _ -> fail $ show result)+ (c_jbl_destroy jblPtr)++-- | Executes a given query and returns the number of documents.+getCount :: Database -> Query -> IO Int64+getCount (Database _ ejdb) (Query jql _ _) = alloca $+ \countPtr -> c_ejdb_count ejdb jql countPtr 0 >>= checkRC >> peek countPtr+ >>= \(CIntMax int) -> return int++{-|+ Executes a given query and builds a query result as list of tuple with id and document.+-}+getList :: Aeson.FromJSON a => Database -> Query -> IO [(Int64, Maybe a)]+getList = exec Database.EJDB2.visitor++visitor :: Aeson.FromJSON a => IORef [(Int64, Maybe a)] -> EJDBExecVisitor+visitor ref _ docPtr _ = do+ doc <- peek docPtr+ value <- decode (raw doc)+ modifyIORef' ref $ \list -> (fromIntegral $ EJDBDoc.id doc, value) : list+ return 0++{-|+ Executes a given query and builds a query result as list of documents with id injected as attribute.+-}+getList' :: Aeson.FromJSON a => Database -> Query -> IO [Maybe a]+getList' = exec Database.EJDB2.visitor'++visitor' :: Aeson.FromJSON a => IORef [Maybe a] -> EJDBExecVisitor+visitor' ref _ docPtr _ = do+ doc <- peek docPtr+ value <- decode' (raw doc) (fromIntegral $ EJDBDoc.id doc)+ modifyIORef' ref $ \list -> value : list+ return 0++exec :: (IORef [a] -> EJDBExecVisitor) -> Database -> Query -> IO [a]+exec visitor (Database _ ejdb) (Query jql _ _) = do+ ref <- newIORef []+ visitor <- mkEJDBExecVisitor (visitor ref)+ let exec = EJDBExec.zero { db = ejdb, q = jql, EJDBExec.visitor = visitor }+ finally (with exec $ \execPtr -> do+ c_ejdb_exec execPtr >>= checkRC+ reverse <$> readIORef ref)+ (freeHaskellFunPtr visitor)++{-|+ Save new document into collection under new generated identifier.+-}+putNew :: Aeson.ToJSON a+ => Database+ -> String -- ^ Collection name+ -> a -- ^ Document+ -> IO Int64 -- ^ New document identifier. Not zero++putNew (Database _ ejdb) collection obj = encode obj $+ \doc -> withCString collection $ \cCollection -> alloca $ \idPtr ->+ c_ejdb_put_new ejdb cCollection doc idPtr >>= checkRC >> peek idPtr+ >>= \(CIntMax int) -> return int++{-|+ Save a given document under specified id.+-}+put :: Aeson.ToJSON a+ => Database+ -> String -- ^ Collection name+ -> a -- ^ Document+ -> Int64 -- ^ Document identifier. Not zero+ -> IO ()+put (Database _ ejdb) collection obj id =+ encode obj $ \doc -> withCString collection $ \cCollection ->+ c_ejdb_put ejdb cCollection doc (CIntMax id) >>= checkRC++{-|+ Apply JSON merge patch (rfc7396) to the document identified by id or insert new document under specified id.++ /This is an atomic operation./+-}+mergeOrPut :: Aeson.ToJSON a+ => Database+ -> String -- ^ Collection name+ -> a -- ^ JSON merge patch conformed to rfc7396 specification+ -> Int64 -- ^ Document identifier. Not zero+ -> IO ()+mergeOrPut (Database _ ejdb) collection obj id = withCString collection $+ \cCollection -> BS.useAsCString (encodeToByteString obj) $ \jsonPatch ->+ c_ejdb_merge_or_put ejdb cCollection jsonPatch (CIntMax id) >>= checkRC++{-|+ Apply rfc6902\/rfc7396 JSON patch to the document identified by id.+-}+patch :: Aeson.ToJSON a+ => Database+ -> String -- ^ Collection name+ -> a -- ^ JSON patch conformed to rfc6902 or rfc7396 specification+ -> Int64 -- ^ Document identifier. Not zero+ -> IO ()+patch (Database _ ejdb) collection obj id = withCString collection $+ \cCollection -> BS.useAsCString (encodeToByteString obj) $ \jsonPatch ->+ c_ejdb_patch ejdb cCollection jsonPatch (CIntMax id) >>= checkRC++{-|+ Remove document identified by given id from collection coll.+-}+delete :: Database+ -> String -- ^ Collection name+ -> Int64 -- ^ Document identifier. Not zero+ -> IO ()+delete (Database _ ejdb) collection id = withCString collection $+ \cCollection -> c_ejdb_del ejdb cCollection (CIntMax id) >>= checkRC++{-|+ Create collection with given name if it has not existed before+-}+ensureCollection :: Database+ -> String -- ^ Collection name+ -> IO ()+ensureCollection (Database _ ejdb) collection =+ withCString collection (c_ejdb_ensure_collection ejdb >=> checkRC)++{-|+ Remove collection under the given name.+-}+removeCollection :: Database+ -> String -- ^ Collection name+ -> IO ()+removeCollection (Database _ ejdb) collection =+ withCString collection (c_ejdb_remove_collection ejdb >=> checkRC)++{-|+ Rename collection to new name.+-}+renameCollection :: Database+ -> String -- ^ Old collection name+ -> String -- ^ New collection name+ -> IO ()+renameCollection (Database _ ejdb) collection newCollection =+ withCString collection $ \cCollection ->+ withCString newCollection+ (c_ejdb_rename_collection ejdb cCollection >=> checkRC)++{-|+ Returns JSON document describing database structure. You can use the convenient data 'Database.EJDB2.Meta.Meta'+-}+getMeta :: Aeson.FromJSON a+ => Database+ -> IO (Maybe a) -- ^ JSON object describing ejdb storage. See data 'Database.EJDB2.Meta.Meta'++getMeta (Database _ ejdb) = alloca $ \jblPtr -> c_ejdb_get_meta ejdb jblPtr+ >>= checkRC >> finally (peek jblPtr >>= decode) (c_jbl_destroy jblPtr)++{-|+ Create index with specified parameters if it has not existed before.++ /Index path must be fully specified as rfc6901 JSON pointer and must not countain unspecified *\/** element in middle sections./++ > ensureIndex database "mycoll" "/address/street" [uniqueIndexMode | strIndexMode]+-}+ensureIndex :: Database+ -> String -- ^ Collection name+ -> String -- ^ rfc6901 JSON pointer to indexed field+ -> [IndexMode.IndexMode] -- ^ Index mode+ -> IO ()+ensureIndex (Database _ ejdb) collection path indexMode =+ withCString collection $ \cCollection -> withCString path $+ \cPath -> c_ejdb_ensure_index ejdb cCollection cPath mode >>= checkRC+ where+ mode = IndexMode.unIndexMode $ IndexMode.combineIndexMode indexMode++{-|+ Remove index if it has existed before.+-}+removeIndex :: Database+ -> String -- ^ Collection name+ -> String -- ^ rfc6901 JSON pointer to indexed field+ -> [IndexMode.IndexMode] -- ^ Index mode+ -> IO ()+removeIndex (Database _ ejdb) collection path indexMode =+ withCString collection $ \cCollection -> withCString path $+ \cPath -> c_ejdb_remove_index ejdb cCollection cPath mode >>= checkRC+ where+ mode = IndexMode.unIndexMode $ IndexMode.combineIndexMode indexMode++{-|+ Creates an online database backup image and copies it into the specified target file.+ During online backup phase read/write database operations are allowed and not+ blocked for significant amount of time. Backup finish time is placed into result+ as number of milliseconds since epoch.++ Online backup guaranties what all records before timestamp will+ be stored in backup image. Later, online backup image can be+ opened as ordinary database file.++ /In order to avoid deadlocks: close all opened database cursors before calling this method or do call in separate thread./+-}+onlineBackup :: Database+ -> String -- ^ Backup file path+ -> IO Word64 -- ^ Backup completion timestamp++onlineBackup (Database _ ejdb) filePath = withCString filePath $ \cFilePath ->+ alloca $ \timestampPtr -> c_ejdb_online_backup ejdb timestampPtr cFilePath+ >>= checkRC >> peek timestampPtr >>= \(CUIntMax t) -> return t
+ src/Database/EJDB2/Bindings/EJDB2.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE ForeignFunctionInterface #-}++module Database.EJDB2.Bindings.EJDB2 where++import Database.EJDB2.Bindings.JBL+import Database.EJDB2.Bindings.JQL+import Database.EJDB2.Bindings.Types.EJDB+import Database.EJDB2.Bindings.Types.EJDBDoc+import Database.EJDB2.Bindings.Types.EJDBExec+import Database.EJDB2.Options+import Database.EJDB2.Result++import Foreign+import Foreign.C.String+import Foreign.C.Types++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_init" c_ejdb_init :: IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_open" c_ejdb_open+ :: Ptr OptionsB -> Ptr EJDB -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_close" c_ejdb_close+ :: Ptr EJDB -> IO RC++foreign import ccall "wrapper" mkEJDBExecVisitor+ :: EJDBExecVisitor -> IO EJDB_EXEC_VISITOR++foreign import ccall "ejdb2/ejdb2.h ejdb_exec" c_ejdb_exec+ :: Ptr EJDBExec -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_get" c_ejdb_get+ :: EJDB -> CString -> CIntMax -> Ptr JBL -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_count" c_ejdb_count+ :: EJDB -> JQL -> Ptr CIntMax -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_put_new" c_ejdb_put_new+ :: EJDB -> CString -> JBL -> Ptr CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_put" c_ejdb_put+ :: EJDB -> CString -> JBL -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_merge_or_put" c_ejdb_merge_or_put+ :: EJDB -> CString -> CString -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_patch" c_ejdb_patch+ :: EJDB -> CString -> CString -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_del" c_ejdb_del+ :: EJDB -> CString -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_ensure_collection" c_ejdb_ensure_collection+ :: EJDB -> CString -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_remove_collection" c_ejdb_remove_collection+ :: EJDB -> CString -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_rename_collection" c_ejdb_rename_collection+ :: EJDB -> CString -> CString -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_ensure_index" c_ejdb_ensure_index+ :: EJDB -> CString -> CString -> CUChar -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_remove_index" c_ejdb_remove_index+ :: EJDB -> CString -> CString -> CUChar -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_get_meta" c_ejdb_get_meta+ :: EJDB -> Ptr JBL -> IO RC++foreign import ccall unsafe "ejdb2/ejdb2.h ejdb_online_backup" c_ejdb_online_backup+ :: EJDB -> Ptr CUIntMax -> CString -> IO RC
+ src/Database/EJDB2/Bindings/JBL.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.Bindings.JBL where++import Database.EJDB2.Result++import Foreign+import Foreign.C.String+import Foreign.C.Types++type JBL = Ptr ()++type JBLNode = Ptr ()++type JBLPrintFlags = CUChar++type JBLJSONPrinter = Ptr CChar -> CInt -> CChar -> CInt -> Ptr () -> IO RC++foreign import ccall "wrapper" mkJBLJSONPrinter+ :: JBLJSONPrinter -> IO (FunPtr JBLJSONPrinter)++foreign import ccall "ejdb2/jbl.h jbl_as_json" c_jbl_as_json+ :: JBL -> FunPtr JBLJSONPrinter -> Ptr () -> JBLPrintFlags -> IO RC++foreign import ccall unsafe "ejdb2/jbl.h jbl_destroy" c_jbl_destroy+ :: Ptr JBL -> IO ()++foreign import ccall "ejdb2/jbl.h jbl_from_json" c_jbl_from_json+ :: Ptr JBL -> CString -> IO RC+
+ src/Database/EJDB2/Bindings/JQL.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE ForeignFunctionInterface #-}++module Database.EJDB2.Bindings.JQL where++import Database.EJDB2.Result++import Foreign+import Foreign.C.String+import Foreign.C.Types++type JQL = Ptr ()++foreign import ccall unsafe "ejdb2/jql.h jql_create" c_jql_create+ :: Ptr JQL -> CString -> CString -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_bool" c_jql_set_bool+ :: JQL -> CString -> CInt -> CBool -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_i64" c_jql_set_i64+ :: JQL -> CString -> CInt -> CIntMax -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_f64" c_jql_set_f64+ :: JQL -> CString -> CInt -> CDouble -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_str" c_jql_set_str+ :: JQL -> CString -> CInt -> CString -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_regexp" c_jql_set_regexp+ :: JQL -> CString -> CInt -> CString -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_set_null" c_jql_set_null+ :: JQL -> CString -> CInt -> IO RC++foreign import ccall unsafe "ejdb2/jql.h jql_destroy" c_jql_destroy+ :: Ptr JQL -> IO ()++foreign import ccall "finalizer.h &finalizerJQL" p_finalizerJQL+ :: FunPtr (Ptr JQL -> IO ())
+ src/Database/EJDB2/Bindings/Types/EJDB.hs view
@@ -0,0 +1,5 @@+module Database.EJDB2.Bindings.Types.EJDB where++import Foreign++type EJDB = Ptr ()
+ src/Database/EJDB2/Bindings/Types/EJDBDoc.hsc view
@@ -0,0 +1,33 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.Bindings.Types.EJDBDoc where++import Foreign+import Foreign.C.Types++import Database.EJDB2.Bindings.JBL++#include <ejdb2/ejdb2.h>++data EJDBDoc = EJDBDoc { id :: !CIntMax+ , raw :: !JBL+ , node :: !JBLNode+ , next :: !(Ptr EJDBDoc)+ , prev :: !(Ptr EJDBDoc) }++instance Storable EJDBDoc where+ sizeOf _ = #{size struct _EJDB_DOC}+ alignment _ = #{alignment struct _EJDB_DOC}+ peek ptr = do+ id <- #{peek struct _EJDB_DOC, id} ptr+ raw <- #{peek struct _EJDB_DOC, raw} ptr+ node <- #{peek struct _EJDB_DOC, node} ptr+ next <- #{peek struct _EJDB_DOC, next} ptr+ prev <- #{peek struct _EJDB_DOC, prev} ptr+ return $ EJDBDoc id raw node next prev+ poke ptr (EJDBDoc id raw node next prev) = do+ #{poke struct _EJDB_DOC, id} ptr id+ #{poke struct _EJDB_DOC, raw} ptr raw+ #{poke struct _EJDB_DOC, node} ptr node+ #{poke struct _EJDB_DOC, next} ptr next+ #{poke struct _EJDB_DOC, prev} ptr prev
+ src/Database/EJDB2/Bindings/Types/EJDBExec.hsc view
@@ -0,0 +1,80 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.Bindings.Types.EJDBExec where+++import Prelude hiding ( log )++import Foreign+import Foreign.C.String+import Foreign.C.Types++import Database.EJDB2.Bindings.Types.EJDB+import Database.EJDB2.Bindings.JQL+import Database.EJDB2.Bindings.Types.EJDBDoc+import Database.EJDB2.Result++#include <ejdb2/ejdb2.h>++type EJDBExecVisitor = Ptr EJDBExec -> Ptr EJDBDoc -> Ptr CIntMax -> IO RC+type EJDB_EXEC_VISITOR = FunPtr EJDBExecVisitor++type IWXSTR = Ptr ()+type IWPOOL = Ptr ()++data EJDBExec = EJDBExec { db :: !EJDB+ , q :: !JQL+ , visitor :: !EJDB_EXEC_VISITOR+ , opaque :: !(Ptr ())+ , skip :: !CIntMax+ , limit :: !CIntMax+ , cnt :: !CIntMax+ , log :: !IWXSTR+ , pool :: !IWPOOL }++minimal :: EJDB -> JQL -> EJDB_EXEC_VISITOR -> EJDBExec+minimal db q visitor = EJDBExec { db = db+ , q = q+ , visitor = visitor+ , opaque = nullPtr+ , skip = 0+ , limit = 0+ , cnt = 0+ , log = nullPtr+ , pool = nullPtr }++zero :: EJDBExec+zero = EJDBExec { db = nullPtr+ , q = nullPtr+ , visitor = nullFunPtr+ , opaque = nullPtr+ , skip = 0+ , limit = 0+ , cnt = 0+ , log = nullPtr+ , pool = nullPtr }++instance Storable EJDBExec where+ sizeOf _ = #{size EJDB_EXEC}+ alignment _ = #{alignment EJDB_EXEC}+ peek ptr = do+ db <- #{peek EJDB_EXEC, db} ptr+ q <- #{peek EJDB_EXEC, q} ptr+ visitor <- #{peek EJDB_EXEC, visitor} ptr+ opaque <- #{peek EJDB_EXEC, opaque} ptr+ skip <- #{peek EJDB_EXEC, skip} ptr+ limit <- #{peek EJDB_EXEC, limit} ptr+ cnt <- #{peek EJDB_EXEC, cnt} ptr+ log <- #{peek EJDB_EXEC, log} ptr+ pool <- #{peek EJDB_EXEC, pool} ptr+ return $ EJDBExec db q visitor opaque skip limit cnt log pool+ poke ptr (EJDBExec db q visitor opaque skip limit cnt log pool) = do+ #{poke EJDB_EXEC, db} ptr db+ #{poke EJDB_EXEC, q} ptr q+ #{poke EJDB_EXEC, visitor} ptr visitor+ #{poke EJDB_EXEC, opaque} ptr opaque+ #{poke EJDB_EXEC, skip} ptr skip+ #{poke EJDB_EXEC, limit} ptr limit+ #{poke EJDB_EXEC, cnt} ptr cnt+ #{poke EJDB_EXEC, log} ptr log+ #{poke EJDB_EXEC, pool} ptr pool
+ src/Database/EJDB2/CollectionMeta.hs view
@@ -0,0 +1,23 @@++{-# LANGUAGE DeriveGeneric #-}++module Database.EJDB2.CollectionMeta where++import Data.Aeson ( FromJSON )+import Data.Int++import Database.EJDB2.IndexMeta++import GHC.Generics++-- | Metadata about collection.+data CollectionMeta =+ CollectionMeta { name :: String -- ^ Collection name+ , dbid :: Int64 -- ^ Collection database ID+ , rnum :: Int64 -- ^ Number of documents in collection+ , indexes :: [IndexMeta] -- ^ List of collections indexes+ }+ deriving ( Eq, Generic, Show )++instance FromJSON CollectionMeta+
+ src/Database/EJDB2/HTTP.hsc view
@@ -0,0 +1,105 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.HTTP+ ( Options(..)+ , zero+ , OptionsB+ , options+ , build+ ) where++import Foreign+import Foreign.C.String+import Foreign.C.Types+import Foreign.Marshal.Utils+import Data.Int++#include <ejdb2/ejdb2.h>++-- | EJDB HTTP\/Websocket Server options.+data Options = Options { enabled :: !Bool -- ^ If HTTP\/Websocket endpoint enabled. Default: false+ , port :: !Int32 -- ^ Listen port number, required+ , bind :: Maybe String -- ^ Listen IP\/host. Default: /localhost/+ , accessToken :: Maybe String -- ^ Server access token passed in /X-Access-Token/ header. Default: zero+ , blocking :: !Bool -- ^ Block 'open' thread until http service finished.+-- Otherwise HTTP servee started in background.++ , readAnon :: !Bool -- ^ Allow anonymous read-only database access+ , maxBodySize :: !Word64 -- ^ Maximum WS\/HTTP API body size. Default: 64Mb, Min: 512K+ }++-- | Create default 'Options'+zero :: Options+zero = Options { enabled = False+ , port = 0+ , bind = Nothing+ , accessToken = Nothing+ , blocking = False+ , readAnon = False+ , maxBodySize = 0+ }++-- | Storable version of Options+data OptionsB = OptionsB { options :: Options+ , bindPtr :: ForeignPtr CChar+ , accessTokenPtr :: ForeignPtr CChar+ , accessTokenLen :: CSize+ }++-- | Create Storable version of Options+build :: Options -> IO OptionsB+build options = do+ bindPtr <- maybeNew newCString (bind options)+ >>= newForeignPtr finalizerFree+ (accessTokenPtr, accessTokenLen) <- case (accessToken options) of+ Nothing -> return (nullPtr, 0)+ Just value -> newCStringLen value+ accessTokenFPtr <- newForeignPtr finalizerFree accessTokenPtr+ return OptionsB { options = options+ , bindPtr = bindPtr+ , accessTokenPtr = accessTokenFPtr+ , accessTokenLen = CSize $ fromIntegral accessTokenLen+ }+++instance Storable OptionsB where+ sizeOf _ = #{size EJDB_HTTP}+ alignment _ = #{alignment EJDB_HTTP}+ peek ptr = do+ enabled <- #{peek EJDB_HTTP, enabled} ptr :: IO CInt+ port <- #{peek EJDB_HTTP, port} ptr+ bindPtr <- #{peek EJDB_HTTP, bind} ptr+ bindFPtr <- newForeignPtr finalizerFree nullPtr+ bind <- maybePeek peekCString bindPtr+ access_token <- #{peek EJDB_HTTP, access_token} ptr+ access_token_len <- #{peek EJDB_HTTP, access_token_len} ptr+ accessTokenFPtr <- newForeignPtr finalizerFree nullPtr+ accessToken <- maybePeek (\ptr -> peekCStringLen (ptr, access_token_len)) access_token+ blocking <- #{peek EJDB_HTTP, blocking} ptr :: IO CInt+ read_anon <- #{peek EJDB_HTTP, read_anon} ptr :: IO CInt+ max_body_size <- #{peek EJDB_HTTP, max_body_size} ptr+ return $ OptionsB+ (Options+ (toBool enabled)+ port+ bind+ accessToken+ (toBool blocking)+ (toBool read_anon)+ max_body_size)+ bindFPtr accessTokenFPtr (CSize $ fromIntegral access_token_len)+ poke ptr (OptionsB+ (Options enabled port _ _ blocking read_anon max_body_size)+ bindPtr accessTokenPtr accessTokenLen) = do+ #{poke EJDB_HTTP, enabled} ptr (fromBool enabled :: CInt)+ #{poke EJDB_HTTP, port} ptr port+ withForeignPtr bindPtr $ \cBind ->+ #{poke EJDB_HTTP, bind} ptr cBind+ withForeignPtr accessTokenPtr $ \access_token ->+ #{poke EJDB_HTTP, access_token} ptr access_token+ #{poke EJDB_HTTP, access_token_len} ptr accessTokenLen+ #{poke EJDB_HTTP, blocking} ptr (fromBool blocking :: CInt)+ #{poke EJDB_HTTP, read_anon} ptr (fromBool read_anon :: CInt)+ #{poke EJDB_HTTP, max_body_size} ptr max_body_size++
+ src/Database/EJDB2/IndexMeta.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE DeriveGeneric #-}++module Database.EJDB2.IndexMeta where++import Data.Aeson ( FromJSON )+import Data.Int++import GHC.Generics++-- | Metadata abount collection index.+data IndexMeta =+ IndexMeta { ptr :: String -- ^ rfc6901 JSON pointer to indexed field+ , mode :: Int64 -- ^ Index mode+ , idbf :: Int64 -- ^ Index flags+ , dbid :: Int64 -- ^ Index database ID+ , rnum :: Int64 -- ^ Number records stored in index database+ }+ deriving ( Eq, Generic, Show )++instance FromJSON IndexMeta+
+ src/Database/EJDB2/IndexMode.hsc view
@@ -0,0 +1,38 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.IndexMode where+++import Foreign+import Foreign.C.String+import Foreign.C.Types+++#include <ejdb2/ejdb2.h>+-- | Index creation mode.+newtype IndexMode = IndexMode { unIndexMode :: CUChar }++-- | Marks index is unique, no duplicated values allowed.+uniqueIndexMode = IndexMode #{const EJDB_IDX_UNIQUE}+-- | Index values have string type.+--+-- Type conversion will be performed on atempt to save value with other type.+strIndexMode = IndexMode #{const EJDB_IDX_STR}+-- | Index values have signed integer 64 bit wide type.+--+-- Type conversion will be performed on atempt to save value with other type.+i64IndexMode = IndexMode #{const EJDB_IDX_I64}+-- | Index value have floating point type.+-- /Internally floating point numbers are converted to string with precision of 6 digits after decimal point./+f64IndexMode = IndexMode #{const EJDB_IDX_F64}++allIndexMode :: [IndexMode]+allIndexMode = [uniqueIndexMode, strIndexMode, i64IndexMode , f64IndexMode]++combineIndexMode :: [IndexMode] -> IndexMode+combineIndexMode = IndexMode . foldr ((.|.) . unIndexMode) 0++unCombineIndexMode :: IndexMode -> [IndexMode]+unCombineIndexMode (IndexMode (CUChar oflags)) = filter f allIndexMode+ where+ f = \(IndexMode (CUChar value)) -> value .&. oflags /= 0
+ src/Database/EJDB2/JBL.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}++module Database.EJDB2.JBL ( decode, decode', encode, encodeToByteString ) where++import Control.Exception++import qualified Data.Aeson as Aeson+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL+import qualified Data.HashMap.Strict as Map+import Data.IORef+import Data.Int++import Database.EJDB2.Bindings.JBL+import qualified Database.EJDB2.Result as Result++import Foreign+import Foreign.C.Types+import Foreign.Marshal.Array++decode :: Aeson.FromJSON a => JBL -> IO (Maybe a)+decode jbl = Aeson.decode <$> decodeToByteString jbl++decode' :: Aeson.FromJSON a => JBL -> Int64 -> IO (Maybe a)+decode' jbl id = parse . setId id <$> decode jbl++decodeToByteString :: JBL -> IO BSL.ByteString+decodeToByteString jbl = do+ ref <- newIORef BSL.empty+ thePrinter <- mkJBLJSONPrinter (printer ref)+ c_jbl_as_json jbl thePrinter nullPtr 0+ >>= Result.checkRCFinally (freeHaskellFunPtr thePrinter)+ BSL.reverse <$> readIORef ref++parse :: Aeson.FromJSON a => Maybe Aeson.Value -> Maybe a+parse Nothing = Nothing+parse (Just value) = case Aeson.fromJSON value of+ Aeson.Success v -> Just v+ Aeson.Error _ -> Nothing++setId :: Int64 -> Maybe Aeson.Value -> Maybe Aeson.Value+setId id (Just (Aeson.Object map)) =+ Just (Aeson.Object (Map.insert "id" (Aeson.Number $ fromIntegral id) map))+setId _ Nothing = Nothing+setId _ value = value++printer :: IORef BSL.ByteString -> JBLJSONPrinter+printer ref _ 0 (CChar ch) _ _ = do+ modifyIORef' ref $ \string -> BSL.cons word string+ return 0+ where+ word = fromIntegral ch+printer ref buffer size _ _ _+ | size > 0 = do+ array <- peekArray (fromIntegral size) buffer+ printerArray ref array+ | otherwise = do+ array <- peekArray0 (CChar 0) buffer+ printerArray ref array++printerArray :: IORef BSL.ByteString -> [CChar] -> IO Result.RC+printerArray ref array = do+ modifyIORef' ref $ \string ->+ foldl (\result (CChar ch) -> BSL.cons (fromIntegral ch) result)+ string+ array+ return 0++encode :: Aeson.ToJSON a => a -> (JBL -> IO b) -> IO b+encode obj f = do+ let byteString = encodeToByteString obj+ BS.useAsCString byteString $ \string -> alloca $ \jblPtr ->+ finally (c_jbl_from_json jblPtr string >>= Result.checkRC >> peek jblPtr+ >>= f)+ (c_jbl_destroy jblPtr)++encodeToByteString :: Aeson.ToJSON a => a -> BS.ByteString+encodeToByteString obj = BSL.toStrict $ Aeson.encode obj
+ src/Database/EJDB2/KV.hsc view
@@ -0,0 +1,110 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.KV+ ( OpenFlags+ , readonlyOpenFlags+ , truncateOpenFlags+ , noTrimOnCloseOpenFlags+ , Options(..)+ , zero+ , OptionsB+ , build+ , options+ ) where++import Foreign+import Foreign.C.String+import Foreign.C.Types++import qualified Database.EJDB2.WAL as WAL++#include <ejdb2/ejdb2.h>+-- | Database file open modes.+newtype OpenFlags = OpenFlags { unOpenFlags :: CUChar }++-- | Open storage file in read-only mode.+readonlyOpenFlags :: OpenFlags+readonlyOpenFlags = OpenFlags #{const IWKV_RDONLY}++-- | Truncate storage file on open.+truncateOpenFlags :: OpenFlags+truncateOpenFlags = OpenFlags #{const IWKV_TRUNC}++noTrimOnCloseOpenFlags :: OpenFlags+noTrimOnCloseOpenFlags = OpenFlags #{const IWKV_NO_TRIM_ON_CLOSE}++allOpenFlags :: [OpenFlags]+allOpenFlags = [readonlyOpenFlags, truncateOpenFlags, noTrimOnCloseOpenFlags]++combineOpenFlags :: [OpenFlags] -> OpenFlags+combineOpenFlags = OpenFlags . foldr ((.|.) . unOpenFlags) 0++unCombineOpenFlags :: OpenFlags -> [OpenFlags]+unCombineOpenFlags (OpenFlags (CUChar oflags)) = filter f allOpenFlags+ where+ f = \(OpenFlags (CUChar value)) -> value .&. oflags /= 0++-- | IWKV storage open options+data Options =+ Options { path :: Maybe String -- ^ Path to database file+ , randomSeed :: !Word32 -- ^ Random seed used for iwu random generator+ , fmtVersion :: !Int32 -- ^ Database storage format version. Leave it as zero for the latest supported format. Used only for newly created databases+ , oflags :: ![OpenFlags] -- ^ Database file open modes+ , fileLockFailFast :: !Bool -- ^ Do not wait and raise error if database is locked by another process+ , wal :: !WAL.Options+ }++-- | Create default Options+zero :: Options+zero = Options { path = Nothing+ , randomSeed = 0+ , fmtVersion = 0+ , oflags = []+ , fileLockFailFast = False+ , wal = WAL.zero+ }++-- | Storable version of Options+data OptionsB =+ OptionsB { options :: Options+ , pathPtr :: ForeignPtr CChar+ }++-- | Create Storable version of Options+build :: Options -> IO OptionsB+build options = do+ pathPtr <- maybeNew newCString (path options)+ pathFPtr <- newForeignPtr finalizerFree pathPtr+ return OptionsB { options = options+ , pathPtr = pathFPtr+ }++instance Storable OptionsB where+ sizeOf _ = #{size IWKV_OPTS}+ alignment _ = #{alignment IWKV_OPTS}+ peek ptr = do+ pathPtr <- #{peek IWKV_OPTS, path} ptr+ pathFPtr <- newForeignPtr finalizerFree nullPtr -- I'm just reading the pointer, I'm not responsable to free memory about this pointer.+ path <- maybePeek peekCString pathPtr+ random_seed <- #{peek IWKV_OPTS, random_seed} ptr+ fmt_version <- #{peek IWKV_OPTS, fmt_version} ptr+ oflags <- #{peek IWKV_OPTS, oflags} ptr+ file_lock_fail_fast <- #{peek IWKV_OPTS, file_lock_fail_fast} ptr+ wal <- #{peek IWKV_OPTS, wal} ptr+ let options = Options + path+ random_seed+ fmt_version+ (unCombineOpenFlags $ OpenFlags oflags)+ file_lock_fail_fast+ wal+ return $ OptionsB options pathFPtr+ poke ptr (OptionsB (Options path random_seed fmt_version oflags file_lock_fail_fast wal) pathPtr) = do+ withForeignPtr pathPtr $ \cPath ->+ #{poke IWKV_OPTS, path} ptr cPath+ #{poke IWKV_OPTS, random_seed} ptr random_seed+ #{poke IWKV_OPTS, fmt_version} ptr fmt_version+ #{poke IWKV_OPTS, oflags} ptr (unOpenFlags $ combineOpenFlags oflags)+ #{poke IWKV_OPTS, file_lock_fail_fast} ptr file_lock_fail_fast+ #{poke IWKV_OPTS, wal} ptr wal+
+ src/Database/EJDB2/Meta.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE DeriveGeneric #-}++module Database.EJDB2.Meta where++import Data.Aeson ( FromJSON )+import Data.Int++import Database.EJDB2.CollectionMeta++import GHC.Generics hiding ( Meta )++-- | Metadata about database.+data Meta =+ Meta { version :: String -- ^ EJDB engine version+ , file :: String -- ^ Path to storage file+ , size :: Int64 -- ^ Storage file size in bytes+ , collections :: [CollectionMeta] -- ^ List of collections+ }+ deriving ( Eq, Generic, Show )++instance FromJSON Meta+
+ src/Database/EJDB2/Options.hsc view
@@ -0,0 +1,65 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.Options+ ( Options(..), zero, OptionsB, options, build ) where++import Data.ByteString.Char8++import Foreign+import Foreign.C.String+import Foreign.C.Types++import qualified Database.EJDB2.KV as KV+import qualified Database.EJDB2.HTTP as HTTP++#include <ejdb2/ejdb2.h>++-- | EJDB open options+data Options = Options { kv :: !KV.Options -- ^ IWKV storage options+ , http :: !HTTP.Options -- ^ HTTP/Websocket server options+ , noWal :: !Bool -- ^ Do not use write-ahead-log. Default: false+ , sortBufferSz :: !Word32 -- ^ Max sorting buffer size. If exceeded an overflow temp file for sorted data will created. Default 16Mb, min: 1Mb+ , documentBufferSz :: !Word32 -- ^ Initial size of buffer in bytes used to process/store document during query execution. Default 64Kb, min: 16Kb+ }++-- | Create default Options+zero :: Options+zero = Options { kv = KV.zero+ , http = HTTP.zero+ , noWal = False+ , sortBufferSz = 0+ , documentBufferSz = 0+ }++-- | Storable version of 'Options'+data OptionsB = OptionsB { options :: Options+ , kvB :: !KV.OptionsB+ , httpB :: !HTTP.OptionsB+ }++-- | Create Storable version of 'Options'+build :: Options -> IO OptionsB+build options = do+ kvB <- KV.build (kv options)+ httpB <- HTTP.build (http options)+ return $ OptionsB options kvB httpB++instance Storable OptionsB where+ sizeOf _ = #{size EJDB_OPTS}+ alignment _ = #{alignment EJDB_OPTS}+ peek ptr = do+ kvB <- #{peek EJDB_OPTS, kv} ptr+ let kv = KV.options kvB+ httpB <- #{peek EJDB_OPTS, http} ptr+ let http = HTTP.options httpB+ (CBool no_wal) <- #{peek EJDB_OPTS, no_wal} ptr+ (CUInt sort_buffer_sz) <- #{peek EJDB_OPTS, sort_buffer_sz} ptr+ (CUInt document_buffer_sz) <- #{peek EJDB_OPTS, document_buffer_sz} ptr+ return $ OptionsB (Options kv http (toBool no_wal) sort_buffer_sz document_buffer_sz) kvB httpB+ poke ptr (OptionsB (Options _ http noWal sort_buffer_sz document_buffer_sz) kvB httpB) = do+ #{poke EJDB_OPTS, kv} ptr kvB+ #{poke EJDB_OPTS, http} ptr httpB+ #{poke EJDB_OPTS, no_wal} ptr (CBool $ fromBool noWal)+ #{poke EJDB_OPTS, sort_buffer_sz} ptr (CUInt sort_buffer_sz)+ #{poke EJDB_OPTS, document_buffer_sz} ptr (CUInt document_buffer_sz)+
+ src/Database/EJDB2/Query.hs view
@@ -0,0 +1,156 @@+module Database.EJDB2.Query+ ( Query+ , fromString+ , setBool+ , setBoolAtIndex+ , setI64+ , setI64AtIndex+ , setF64+ , setF64AtIndex+ , setString+ , setStringAtIndex+ , setRegex+ , setRegexAtIndex+ , setNull+ , setNullAtIndex+ ) where++import qualified Data.Bool as Bool+import Data.IORef+import Data.Int++import Database.EJDB2.Bindings.JQL+import Database.EJDB2.QueryConstructor+import Database.EJDB2.Result++import Foreign+import Foreign.C.String+import Foreign.C.Types+import Foreign.Marshal.Alloc++-- | Create query object from specified text query. Collection must be specified in query.+fromString :: String -- ^ Query text+ -> IO Query+fromString string = do+ jqlPtr <- malloc+ withCString string $ \cString -> do+ c_jql_create jqlPtr nullPtr cString >>= checkRC+ jql <- peek jqlPtr+ jqlFPtr <- newForeignPtr p_finalizerJQL jqlPtr+ ioRef <- newIORef []+ return $ Query jql jqlFPtr ioRef++-- | Bind bool to query placeholder+setBool :: Bool+ -> String -- ^ Placeholder+ -> Query+ -> IO ()+setBool bool placeholder (Query jql _ _) =+ withCString placeholder $ \cPlaceholder ->+ c_jql_set_bool jql cPlaceholder 0 (CBool (Bool.bool 0 1 bool)) >>= checkRC++-- | Bind bool to query at specified index+setBoolAtIndex :: Bool+ -> Int -- ^ Index+ -> Query+ -> IO ()+setBoolAtIndex bool index (Query jql _ _) =+ c_jql_set_bool jql+ nullPtr+ (CInt $ fromIntegral index)+ (CBool (Bool.bool 0 1 bool)) >>= checkRC++-- | Bind number to query placeholder+setI64 :: Int64+ -> String -- ^ Placeholder+ -> Query+ -> IO ()+setI64 number placeholder (Query jql _ _) = withCString placeholder $+ \cPlaceholder -> c_jql_set_i64 jql cPlaceholder 0 (CIntMax number)+ >>= checkRC++-- | Bind number to query at specified index+setI64AtIndex :: Int64+ -> Int -- ^ Index+ -> Query+ -> IO ()+setI64AtIndex number index (Query jql _ _) =+ c_jql_set_i64 jql nullPtr (CInt $ fromIntegral index) (CIntMax number)+ >>= checkRC++-- | Bind 'Double' to query placeholder+setF64 :: Double+ -> String -- ^ Placeholder+ -> Query+ -> IO ()+setF64 number placeholder (Query jql _ _) = withCString placeholder $+ \cPlaceholder -> c_jql_set_f64 jql cPlaceholder 0 (CDouble number)+ >>= checkRC++-- | Bind 'Double' to query at specified index+setF64AtIndex :: Double+ -> Int -- ^ Index+ -> Query+ -> IO ()+setF64AtIndex number index (Query jql _ _) =+ c_jql_set_f64 jql nullPtr (CInt $ fromIntegral index) (CDouble number)+ >>= checkRC++newCStringInIORef :: String -> IORef [ForeignPtr CChar] -> IO CString+newCStringInIORef string ioRef = do+ cString <- newCString string+ cStringPtr <- newForeignPtr finalizerFree cString+ modifyIORef' ioRef ((:) cStringPtr)+ return cString++-- | Bind string to query placeholder+setString :: String+ -> String -- ^ Placeholder+ -> Query+ -> IO ()+setString string placeholder (Query jql _ ioRef) = do+ cString <- newCStringInIORef string ioRef+ withCString placeholder $+ \cPlaceholder -> c_jql_set_str jql cPlaceholder 0 cString >>= checkRC++-- | Bind string to query at specified index+setStringAtIndex :: String+ -> Int -- ^ Index+ -> Query+ -> IO ()+setStringAtIndex string index (Query jql _ ioRef) = do+ cString <- newCStringInIORef string ioRef+ c_jql_set_str jql nullPtr (CInt $ fromIntegral index) cString >>= checkRC++-- | Bind regex to query placeholder+setRegex :: String -- ^ Regex+ -> String -- ^ Placeholder+ -> Query+ -> IO ()+setRegex string placeholder (Query jql _ ioRef) = do+ cString <- newCStringInIORef string ioRef+ withCString placeholder $+ \cPlaceholder -> c_jql_set_regexp jql cPlaceholder 0 cString >>= checkRC++-- | Bind regex to query at specified index+setRegexAtIndex :: String -- ^ Regex+ -> Int -- ^ Index+ -> Query+ -> IO ()+setRegexAtIndex string index (Query jql _ ioRef) = do+ cString <- newCStringInIORef string ioRef+ c_jql_set_regexp jql nullPtr (CInt $ fromIntegral index) cString >>= checkRC++-- | Bind /null/ value to query placeholder+setNull :: String -- ^ Placeholder+ -> Query+ -> IO ()+setNull placeholder (Query jql _ _) = withCString placeholder $+ \cPlaceholder -> c_jql_set_null jql cPlaceholder 0 >>= checkRC++-- | Bind /null/ value to query at specified index+setNullAtIndex :: Int -- ^ Index+ -> Query+ -> IO ()+setNullAtIndex index (Query jql _ _) =+ c_jql_set_null jql nullPtr (CInt $ fromIntegral index) >>= checkRC
+ src/Database/EJDB2/QueryConstructor.hs view
@@ -0,0 +1,14 @@+module Database.EJDB2.QueryConstructor ( Query(..) ) where++import Data.IORef++import Database.EJDB2.Bindings.JQL++import Foreign+import Foreign.C.Types++-- | Query handle+data Query = Query { jql :: JQL+ , jqlPtr :: ForeignPtr JQL+ , strings :: IORef [ForeignPtr CChar]+ }
+ src/Database/EJDB2/Result.hsc view
@@ -0,0 +1,172 @@+{-# LANGUAGE CPP #-}+module Database.EJDB2.Result where++import Foreign+import Foreign.C.Types++#include <ejdb2/ejdb2.h>++type RC = CUIntMax++checkRC :: RC -> IO ()+checkRC rc = do+ let result = decodeRC rc+ if result == Ok then return () else fail $ show result++checkRCFinally :: IO a -> RC -> IO a+checkRCFinally computation rc = do+ let result = decodeRC rc+ if result == Ok+ then computation+ else do+ computation+ fail $ show result++data Result =+ Ok -- ^ No error.+ | ErrorFail -- ^ Unspecified error.+ | ErrorErrno -- ^ Error with expected errno status set.+ | ErrorIoErrno -- ^ IO error with expected errno status set.+ | ErrorNotExists -- ^ Resource is not exists.+ | ErrorReadonly -- ^ Resource is readonly.+ | ErrorAlreadyOpened -- ^ Resource is already opened.+ | ErrorThreading -- ^ Threading error.+ | ErrorThreadingErrno -- ^ Threading error with errno status set.+ | ErrorAssertion -- ^ Generic assertion error.+ | ErrorInvalidHandle -- ^ Invalid HANDLE value.+ | ErrorOutOfBounds -- ^ Invalid bounds specified.+ | ErrorNotImplemented -- ^ Method is not implemented.+ | ErrorAlloc -- ^ Memory allocation failed.+ | ErrorInvalidState -- ^ Illegal state error.+ | ErrorNotAligned -- ^ Argument is not aligned properly.+ | ErrorFalse -- ^ Request rejection/false response.+ | ErrorInvalidArgs -- ^ Invalid function arguments.+ | ErrorOverflow -- ^ Overflow.+ | ErrorInvalidValue -- ^ Invalid value.+ | ErrorNotFound -- ^ Key not found (IWKV_ERROR_NOTFOUND)+ | ErrorKeyExists -- ^ Key already exists (IWKV_ERROR_KEY_EXISTS)+ | ErrorMaxkvsz -- ^ Size of Key+value must be not greater than 0xfffffff bytes (IWKV_ERROR_MAXKVSZ)+ | ErrorCorrupted -- ^ Database file invalid or corrupted (IWKV_ERROR_CORRUPTED)+ | ErrorDupValueSize -- ^ Value size is not compatible for insertion into sorted values array (IWKV_ERROR_DUP_VALUE_SIZE)+ | ErrorKeyNumValueSize -- ^ Given key is not compatible to storage as number (IWKV_ERROR_KEY_NUM_VALUE_SIZE)+ | ErrorIncompatibleDbMode -- ^ Incorpatible database open mode (IWKV_ERROR_INCOMPATIBLE_DB_MODE)+ | ErrorIncompatibleDbFormat -- ^ Incompatible database format version, please migrate database data (IWKV_ERROR_INCOMPATIBLE_DB_FORMAT)+ | ErrorCorruptedWalFile -- ^ Corrupted WAL file (IWKV_ERROR_CORRUPTED_WAL_FILE)+ | ErrorValueCannotBeIncremented -- ^ Stored value cannot be incremented/descremented (IWKV_ERROR_VALUE_CANNOT_BE_INCREMENTED)+ | ErrorWalModeRequired -- ^ Operation requires WAL enabled database. (IWKV_ERROR_WAL_MODE_REQUIRED)+ | ErrorBackupInProgress -- ^ Backup operation in progress. (IWKV_ERROR_BACKUP_IN_PROGRESS)+ | ErrorInvalidBuffer -- ^ Invalid JBL buffer (JBLERRORINVALIDBUFFER)+ | ErrorCreation -- ^ Cannot create JBL object (JBLERRORCREATION)+ | ErrorInvalid -- ^ Invalid JBL object (JBLERRORINVALID)+ | ErrorParseJson -- ^ Failed to parse JSON string (JBLERRORPARSEJSON)+ | ErrorParseUnquotedString -- ^ Unquoted JSON string (JBLERRORPARSEUNQUOTEDSTRING)+ | ErrorParseInvalidCodepoint -- ^ Invalid unicode codepoint/escape sequence (JBLERRORPARSEINVALIDCODEPOINT)+ | ErrorParseInvalidUtf8 -- ^ Invalid utf8 string (JBLERRORPARSEINVALIDUTF8)+ | ErrorJsonPointer -- ^ Invalid JSON pointer (rfc6901) path (JBLERRORJSONPOINTER)+ | ErrorPathNotFound -- ^ JSON object not matched the path specified (JBLERRORPATHNOTFOUND)+ | ErrorPatchInvalid -- ^ Invalid JSON patch specified (JBLERRORPATCHINVALID)+ | ErrorPatchInvalidOp -- ^ Invalid JSON patch operation specified (JBLERRORPATCHINVALIDOP)+ | ErrorPatchNovalue -- ^ No value specified in JSON patch (JBLERRORPATCHNOVALUE)+ | ErrorPatchTargetInvalid -- ^ Could not find target object to set value (JBLERRORPATCHTARGETINVALID)+ | ErrorPatchInvalidValue -- ^ Invalid value specified by patch (JBLERRORPATCHINVALIDVALUE)+ | ErrorPatchInvalidArrayIndex -- ^ Invalid array index in JSON patch path (JBLERRORPATCHINVALIDARRAYINDEX)+ | ErrorNotAnObject -- ^ JBL is not an object (JBLERRORNOTANOBJECT)+ | ErrorPatchTestFailed -- ^ JSON patch test operation failed (JBLERRORPATCHTESTFAILED)+ | ErrorInvalidCollectionName -- ^ Invalid collection name+ | ErrorInvalidCollectionMeta -- ^ Invalid collection metadata+ | ErrorInvalidCollectionIndexMeta -- ^ Invalid collection index metadata+ | ErrorInvalidIndexMode -- ^ Invalid index mode specified+ | ErrorMismatchedIndexUniquenessMode -- ^ Index exists but mismatched uniqueness constraint+ | ErrorUniqueIndexConstraintViolated -- ^ Unique index constraint violated+ | ErrorCollectionNotFound -- ^ Collection not found+ | ErrorTargetCollectionExists -- ^ Target collection exists+ | ErrorPatchJsonNotObject -- ^ Patch JSON must be an object (map)+ | ErrorQueryParse -- ^ Query parsing error (JQL_ERROR_QUERY_PARSE)+ | ErrorInvalidPlaceholder -- ^ Invalid placeholder position (JQL_ERROR_INVALID_PLACEHOLDER)+ | ErrorUnsetPlaceholder -- ^ Found unset placeholder (JQL_ERROR_UNSET_PLACEHOLDER)+ | ErrorRegexpInvalid -- ^ Invalid regular expression (JQL_ERROR_REGEXP_INVALID)+ | ErrorRegexpCharset -- ^ Invalid regular expression: expected ']' at end of character set (JQL_ERROR_REGEXP_CHARSET)+ | ErrorRegexpSubexp -- ^ Invalid regular expression: expected ')' at end of subexpression (JQL_ERROR_REGEXP_SUBEXP)+ | ErrorRegexpSubmatch -- ^ Invalid regular expression: expected '}' at end of submatch (JQL_ERROR_REGEXP_SUBMATCH)+ | ErrorRegexpEngine -- ^ Illegal instruction in compiled regular expression (please report this bug) (JQL_ERROR_REGEXP_ENGINE)+ | ErrorSkipAlreadySet -- ^ Skip clause already specified (JQL_ERROR_SKIP_ALREADY_SET)+ | ErrorLimitAlreadySet -- ^ Limit clause already specified (JQL_ERROR_SKIP_ALREADY_SET)+ | ErrorOrderbyMaxLimit -- ^ Reached max number of asc/desc order clauses: 64 (JQL_ERROR_ORDERBY_MAX_LIMIT)+ | ErrorNoCollection -- ^ No collection specified in query (JQL_ERROR_NO_COLLECTION)+ | ErrorInvalidPlaceholderValueType -- ^ Invalid type of placeholder value (JQL_ERROR_INVALID_PLACEHOLDER_VALUE_TYPE)+ deriving ( Eq, Show )++decodeRC :: RC -> Result+decodeRC rc = case rc of+ #{const IW_OK} -> Ok+ #{const IW_ERROR_FAIL} -> ErrorFail+ #{const IW_ERROR_ERRNO} -> ErrorErrno+ #{const IW_ERROR_IO_ERRNO} -> ErrorIoErrno+ #{const IW_ERROR_NOT_EXISTS} -> ErrorNotExists+ #{const IW_ERROR_READONLY} -> ErrorReadonly+ #{const IW_ERROR_ALREADY_OPENED} -> ErrorAlreadyOpened+ #{const IW_ERROR_THREADING} -> ErrorThreading+ #{const IW_ERROR_THREADING_ERRNO} -> ErrorThreadingErrno+ #{const IW_ERROR_ASSERTION} -> ErrorAssertion+ #{const IW_ERROR_INVALID_HANDLE} -> ErrorInvalidHandle+ #{const IW_ERROR_OUT_OF_BOUNDS} -> ErrorOutOfBounds+ #{const IW_ERROR_NOT_IMPLEMENTED} -> ErrorNotImplemented+ #{const IW_ERROR_ALLOC} -> ErrorAlloc+ #{const IW_ERROR_INVALID_STATE} -> ErrorInvalidState+ #{const IW_ERROR_NOT_ALIGNED} -> ErrorNotAligned+ #{const IW_ERROR_FALSE} -> ErrorFalse+ #{const IW_ERROR_INVALID_ARGS} -> ErrorInvalidArgs+ #{const IW_ERROR_OVERFLOW} -> ErrorOverflow+ #{const IW_ERROR_INVALID_VALUE} -> ErrorInvalidValue+ #{const IWKV_ERROR_NOTFOUND} -> ErrorNotFound+ #{const IWKV_ERROR_KEY_EXISTS} -> ErrorKeyExists+ #{const IWKV_ERROR_MAXKVSZ} -> ErrorMaxkvsz+ #{const IWKV_ERROR_CORRUPTED} -> ErrorCorrupted+ #{const IWKV_ERROR_DUP_VALUE_SIZE} -> ErrorDupValueSize+ #{const IWKV_ERROR_KEY_NUM_VALUE_SIZE} -> ErrorKeyNumValueSize+ #{const IWKV_ERROR_INCOMPATIBLE_DB_MODE} -> ErrorIncompatibleDbMode+ #{const IWKV_ERROR_INCOMPATIBLE_DB_FORMAT} -> ErrorIncompatibleDbFormat+ #{const IWKV_ERROR_CORRUPTED_WAL_FILE} -> ErrorCorruptedWalFile+ #{const IWKV_ERROR_VALUE_CANNOT_BE_INCREMENTED} -> ErrorValueCannotBeIncremented+ #{const IWKV_ERROR_WAL_MODE_REQUIRED} -> ErrorWalModeRequired+ #{const IWKV_ERROR_BACKUP_IN_PROGRESS} -> ErrorBackupInProgress+ #{const JBL_ERROR_INVALID_BUFFER} -> ErrorInvalidBuffer+ #{const JBL_ERROR_CREATION} -> ErrorCreation+ #{const JBL_ERROR_INVALID} -> ErrorInvalid+ #{const JBL_ERROR_PARSE_JSON} -> ErrorParseJson+ #{const JBL_ERROR_PARSE_UNQUOTED_STRING} -> ErrorParseUnquotedString+ #{const JBL_ERROR_PARSE_INVALID_CODEPOINT} -> ErrorParseInvalidCodepoint+ #{const JBL_ERROR_PARSE_INVALID_UTF8} -> ErrorParseInvalidUtf8+ #{const JBL_ERROR_JSON_POINTER} -> ErrorJsonPointer+ #{const JBL_ERROR_PATH_NOTFOUND} -> ErrorPathNotFound+ #{const JBL_ERROR_PATCH_INVALID} -> ErrorPatchInvalid+ #{const JBL_ERROR_PATCH_INVALID_OP} -> ErrorPatchInvalidOp+ #{const JBL_ERROR_PATCH_NOVALUE} -> ErrorPatchNovalue+ #{const JBL_ERROR_PATCH_TARGET_INVALID} -> ErrorPatchTargetInvalid+ #{const JBL_ERROR_PATCH_INVALID_VALUE} -> ErrorPatchInvalidValue+ #{const JBL_ERROR_PATCH_INVALID_ARRAY_INDEX} -> ErrorPatchInvalidArrayIndex+ #{const JBL_ERROR_NOT_AN_OBJECT} -> ErrorNotAnObject+ #{const JBL_ERROR_PATCH_TEST_FAILED} -> ErrorPatchTestFailed+ #{const EJDB_ERROR_INVALID_COLLECTION_NAME} -> ErrorInvalidCollectionName+ #{const EJDB_ERROR_INVALID_COLLECTION_META} -> ErrorInvalidCollectionMeta+ #{const EJDB_ERROR_INVALID_COLLECTION_INDEX_META} -> ErrorInvalidCollectionIndexMeta+ #{const EJDB_ERROR_INVALID_INDEX_MODE} -> ErrorInvalidIndexMode+ #{const EJDB_ERROR_MISMATCHED_INDEX_UNIQUENESS_MODE} -> ErrorMismatchedIndexUniquenessMode+ #{const EJDB_ERROR_UNIQUE_INDEX_CONSTRAINT_VIOLATED} -> ErrorUniqueIndexConstraintViolated+ #{const EJDB_ERROR_COLLECTION_NOT_FOUND} -> ErrorCollectionNotFound+ #{const EJDB_ERROR_TARGET_COLLECTION_EXISTS} -> ErrorTargetCollectionExists+ #{const EJDB_ERROR_PATCH_JSON_NOT_OBJECT} -> ErrorPatchJsonNotObject+ #{const JQL_ERROR_QUERY_PARSE} -> ErrorQueryParse+ #{const JQL_ERROR_INVALID_PLACEHOLDER} -> ErrorInvalidPlaceholder+ #{const JQL_ERROR_UNSET_PLACEHOLDER} -> ErrorUnsetPlaceholder+ #{const JQL_ERROR_REGEXP_INVALID} -> ErrorRegexpInvalid+ #{const JQL_ERROR_REGEXP_CHARSET} -> ErrorRegexpCharset+ #{const JQL_ERROR_REGEXP_SUBEXP} -> ErrorRegexpSubexp+ #{const JQL_ERROR_REGEXP_SUBMATCH} -> ErrorRegexpSubmatch+ #{const JQL_ERROR_REGEXP_ENGINE} -> ErrorRegexpEngine+ #{const JQL_ERROR_SKIP_ALREADY_SET} -> ErrorSkipAlreadySet+ #{const JQL_ERROR_LIMIT_ALREADY_SET} -> ErrorLimitAlreadySet+ #{const JQL_ERROR_ORDERBY_MAX_LIMIT} -> ErrorOrderbyMaxLimit+ #{const JQL_ERROR_NO_COLLECTION} -> ErrorNoCollection+ #{const JQL_ERROR_INVALID_PLACEHOLDER_VALUE_TYPE} -> ErrorInvalidPlaceholderValueType+ _ -> error $ "Database.EJDB2.Bindings.IW.decodeRC " ++ show rc
+ src/Database/EJDB2/WAL.hsc view
@@ -0,0 +1,60 @@+{-# LANGUAGE CPP #-}++module Database.EJDB2.WAL (Options(..), zero) where+++import Foreign+import Foreign.C.String+import Foreign.C.Types++import Database.EJDB2.Result++#include <ejdb2/ejdb2.h>++-- | Write ahead log (WAL) options.+data Options = Options { enabled :: !Bool -- ^ WAL enabled+ , checkCRCOnCheckpoint :: !Bool -- ^ Check CRC32 sum of data blocks during checkpoint. Default: false+ , savepointTimeoutSec :: !Word32 -- ^ Savepoint timeout seconds. Default: 10 sec+ , checkpointTimeoutSec :: !Word32 -- ^ Checkpoint timeout seconds. Default: 300 sec (5 min);+ , walBufferSz :: !Word64 -- ^ WAL file intermediate buffer size. Default: 8Mb+ , checkpointBufferSz :: !Word8 -- ^ Checkpoint buffer size in bytes. Default: 1Gb+ , walLockInterceptor :: !(FunPtr (CBool -> Ptr () -> IO RC)) -- ^ Optional function called before acquiring and after releasing.+-- exclusive database lock byAL checkpoint thread.+-- In the case of 'before loc first argument will be set to true+ , walLockInterceptorOpaque :: !(Ptr ()) -- ^ Opaque data for 'walLockInterceptor'+ }++-- | Create default Options+zero :: Options+zero = Options { enabled = False+ , checkCRCOnCheckpoint = False+ , savepointTimeoutSec = 0+ , checkpointTimeoutSec = 0+ , walBufferSz = 0+ , checkpointBufferSz = 0+ , walLockInterceptor = nullFunPtr+ , walLockInterceptorOpaque = nullPtr+ }++instance Storable Options where+ sizeOf _ = #{size IWKV_WAL_OPTS}+ alignment _ = #{alignment IWKV_WAL_OPTS}+ peek ptr = do+ enabled <- #{peek IWKV_WAL_OPTS, enabled} ptr+ check_crc_on_checkpoint <- #{peek IWKV_WAL_OPTS, check_crc_on_checkpoint} ptr+ savepoint_timeout_sec <- #{peek IWKV_WAL_OPTS, savepoint_timeout_sec} ptr+ checkpoint_timeout_sec <- #{peek IWKV_WAL_OPTS, checkpoint_timeout_sec} ptr+ wal_buffer_sz <- #{peek IWKV_WAL_OPTS, wal_buffer_sz} ptr+ checkpoint_buffer_sz <- #{peek IWKV_WAL_OPTS, checkpoint_buffer_sz} ptr+ wal_lock_interceptor <- #{peek IWKV_WAL_OPTS, wal_lock_interceptor} ptr+ wal_lock_interceptor_opaque <- #{peek IWKV_WAL_OPTS, wal_lock_interceptor_opaque} ptr+ return $ Options enabled check_crc_on_checkpoint savepoint_timeout_sec checkpoint_timeout_sec wal_buffer_sz checkpoint_buffer_sz wal_lock_interceptor wal_lock_interceptor_opaque+ poke ptr (Options enabled check_crc_on_checkpoint savepoint_timeout_sec checkpoint_timeout_sec wal_buffer_sz checkpoint_buffer_sz wal_lock_interceptor wal_lock_interceptor_opaque) = do+ #{poke IWKV_WAL_OPTS, enabled} ptr enabled+ #{poke IWKV_WAL_OPTS, check_crc_on_checkpoint} ptr check_crc_on_checkpoint+ #{poke IWKV_WAL_OPTS, savepoint_timeout_sec} ptr savepoint_timeout_sec+ #{poke IWKV_WAL_OPTS, checkpoint_timeout_sec} ptr checkpoint_timeout_sec+ #{poke IWKV_WAL_OPTS, wal_buffer_sz} ptr wal_buffer_sz+ #{poke IWKV_WAL_OPTS, checkpoint_buffer_sz} ptr checkpoint_buffer_sz+ #{poke IWKV_WAL_OPTS, wal_lock_interceptor} ptr wal_lock_interceptor+ #{poke IWKV_WAL_OPTS, wal_lock_interceptor_opaque} ptr wal_lock_interceptor_opaque
+ test/Asserts.hs view
@@ -0,0 +1,16 @@+module Asserts ( assertException ) where++import Control.Exception+import Control.Monad++import Test.Tasty.HUnit++-- https://stackoverflow.com/questions/6147435/is-there-an-assertexception-in-any-of-the-haskell-test-frameworks+assertException :: (Exception e, Eq e, Show a) => e -> IO a -> IO ()+assertException ex action = handleJust isWanted (const $ return ()) $ do+ a <- action+ assertFailure $ "Expected exception: " ++ show ex+ ++ ", but got execution success: " ++ show a+ where+ isWanted = guard . (== ex)+
+ test/CollectionTests.hs view
@@ -0,0 +1,72 @@+module CollectionTests ( tests ) where++import Asserts++import Database.EJDB2+import Database.EJDB2.Options+import qualified Database.EJDB2.Query as Query++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testDatabaseOpts) close $ \databaseIO ->+ testGroup "collection"+ [ removeTest databaseIO+ , renameTest databaseIO+ , ensureTest databaseIO+ ]++testDatabaseOpts :: Options+testDatabaseOpts = minimalOptions "./test/collection-db" [ truncateOpenFlags ]++removeTest :: IO Database -> TestTree+removeTest databaseIO = testCase "removeTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ removeCollection database "plants"+ count <- Query.fromString "@plants/*" >>= getCount database+ count @?= 0+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++renameTest :: IO Database -> TestTree+renameTest databaseIO = testCase "renameTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ renameCollection database "plants" "vegetables"+ count <- Query.fromString "@plants/*" >>= getCount database+ count @?= 0+ storedPlant <- getById database "vegetables" id+ storedPlant @?= Just plant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++ensureTest :: IO Database -> TestTree+ensureTest databaseIO = testCase "ensureTest" $ do+ database <- databaseIO+ _ <- putNew database "plants" plant+ ensureCollection database "vegetables"+ assertException (userError "ErrorTargetCollectionExists")+ (renameCollection database "plants" "vegetables")+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }
+ test/DeleteTests.hs view
@@ -0,0 +1,41 @@+module DeleteTests ( tests ) where++import Asserts++import Database.EJDB2+import Database.EJDB2.Options++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testDatabaseOpts) close $ \databaseIO ->+ testGroup "delete"+ [ deleteTest databaseIO, deleteNotExistingTest databaseIO ]++testDatabaseOpts :: Options+testDatabaseOpts = minimalOptions "./test/delete-db" [ truncateOpenFlags ]++deleteTest :: IO Database -> TestTree+deleteTest databaseIO = testCase "deleteTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ delete database "plants" id+ storedPlant <- getById database "plants" id :: IO (Maybe Plant)+ storedPlant @?= Nothing+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++deleteNotExistingTest :: IO Database -> TestTree+deleteNotExistingTest databaseIO = testCase "deleteNotExistingTest" $ do+ database <- databaseIO+ assertException (userError "ErrorNotFound") (delete database "plants" 42)
+ test/GetTests.hs view
@@ -0,0 +1,143 @@+module GetTests ( tests ) where++import Asserts++import Control.Exception+import Control.Monad++import Data.Aeson ( Value )+import Data.Int++import Database.EJDB2+import Database.EJDB2.Options+import qualified Database.EJDB2.Query as Query++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testReadOnlyDatabaseOpts) close $ \databaseIO ->+ testGroup "get"+ [ getByIdTest databaseIO+ , getByIdNotFoundTest databaseIO+ , getCountTest databaseIO+ , getListTest databaseIO+ , getListTest' databaseIO+ , getByIdFromNotExistingCollectionTest databaseIO+ , getListFromNotExistingCollectionTest databaseIO+ ]++testReadOnlyDatabaseOpts :: Options+testReadOnlyDatabaseOpts =+ minimalOptions "./test/read-only-db" [ readonlyOpenFlags ]++getByIdTest :: IO Database -> TestTree+getByIdTest databaseIO = testCase "getById" $ do+ database <- databaseIO+ plant <- getById database "plants" 1+ plant @?= Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++getByIdNotFoundTest :: IO Database -> TestTree+getByIdNotFoundTest databaseIO = testCase "getById - not found" $ do+ database <- databaseIO+ plant <- getById database "plants" 42+ plant @?= (Nothing :: Maybe Plant)++getCountTest :: IO Database -> TestTree+getCountTest databaseIO = testCase "getCount" $ do+ database <- databaseIO+ count <- Query.fromString "@plants/*" >>= getCount database+ count @?= 4++getListTestQuery :: IO Query.Query+getListTestQuery = do+ query <- Query.fromString "@plants/[isTree=:tree] | asc /name"+ Query.setBool False "tree" query+ return query++getListTest :: IO Database -> TestTree+getListTest databaseIO = testCase "getList" $ do+ database <- databaseIO+ plants <- getListTestQuery >>= getList database+ plants+ @?= [ ( 2+ , Just nothingPlant { id = Nothing+ , name = Just "gentiana brentae"+ , isTree = Just False+ , year = Just 2008+ , description = Just "violet 🌺flower"+ }+ )+ , ( 3+ , Just nothingPlant { id = Nothing+ , name = Just "leontopodium"+ , isTree = Just False+ , year = Just 1817+ , description =+ Just "tipical alpine flower"+ }+ )+ , ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListTest' :: IO Database -> TestTree+getListTest' databaseIO = testCase "getList'" $ do+ database <- databaseIO+ plants <- getListTestQuery >>= getList' database+ plants @?= [ Just nothingPlant { id = Just 2+ , name = Just "gentiana brentae"+ , isTree = Just False+ , year = Just 2008+ , description = Just "violet 🌺flower"+ }+ , Just nothingPlant { id = Just 3+ , name = Just "leontopodium"+ , isTree = Just False+ , year = Just 1817+ , description = Just "tipical alpine flower"+ }+ , Just nothingPlant { id = Just 4+ , name = Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description =+ Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ ]++getByIdFromNotExistingCollectionTest :: IO Database -> TestTree+getByIdFromNotExistingCollectionTest databaseIO =+ testCase "getByIdFromNotExistingCollection" $ do+ database <- databaseIO+ assertException (userError "ErrorNotExists") -- this happens only with readonlyOpenFlags+ (getById database "noexisting" 1 :: IO (Maybe Value))++-- on ejdb_exec there is no error if collection doesn't exists+-- https://github.com/Softmotions/ejdb/blob/40fb43a30e410b4f1bce68f79f397ce44c272c78/src/ejdb2.c#L821+getListFromNotExistingCollectionTest :: IO Database -> TestTree+getListFromNotExistingCollectionTest databaseIO =+ testCase "getListFromNotExistingCollectionTest" $ do+ database <- databaseIO+ query <- Query.fromString "@noexisting/[isTree=:tree] | asc /name"+ Query.setBool False "tree" query+ list <- getList database query :: IO [(Int64, Maybe Value)]+ list @?= []
+ test/IndexTests.hs view
@@ -0,0 +1,52 @@+module IndexTests ( tests ) where++import Asserts++import qualified Data.Aeson as Aeson++import Database.EJDB2+import qualified Database.EJDB2.CollectionMeta as CollectionMeta+import qualified Database.EJDB2.Meta as Meta+import Database.EJDB2.Options++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testDatabaseOpts) close $ \databaseIO ->+ testGroup "index"+ [ getMetaTest databaseIO, createAndRemoveIndexTest databaseIO ]++testDatabaseOpts :: Options+testDatabaseOpts = minimalOptions "./test/index-db" [ truncateOpenFlags ]++getMetaTest :: IO Database -> TestTree+getMetaTest databaseIO = testCase "getMetaTest" $ do+ database <- databaseIO+ Just meta <- getMeta database+ let (major : '.' : minor : '.' : _) = Meta.version meta+ [ major, '.', minor ] @?= "2.0"++createAndRemoveIndexTest :: IO Database -> TestTree+createAndRemoveIndexTest databaseIO = testCase "createAndRemoveIndexTest" $ do+ database <- databaseIO+ ensureCollection database "plants"+ ensureIndex database "plants" "/name" mode+ indexesCount <- getIndexesCount database+ indexesCount @?= 1+ removeIndex database "plants" "/name" mode+ indexesCountLast <- getIndexesCount database+ indexesCountLast @?= 0+ where+ mode = [ uniqueIndexMode, strIndexMode ]++getIndexesCount :: Database -> IO Int+getIndexesCount database = do+ Just meta <- getMeta database+ let indexes = CollectionMeta.indexes $ head $ Meta.collections meta+ return $ length indexes+
+ test/Main.hs view
@@ -0,0 +1,35 @@+module Main ( main ) where++import CollectionTests++import Database.EJDB2++import DeleteTests++import GetTests++import IndexTests++import OnlineBackupTests++import Prelude hiding ( init )++import PutTests++import QueryTests++import Test.Tasty++main :: IO ()+main = init >> defaultMain Main.tests++tests :: TestTree+tests = testGroup "ejdb2"+ [ GetTests.tests+ , QueryTests.tests+ , PutTests.tests+ , DeleteTests.tests+ , CollectionTests.tests+ , IndexTests.tests+ , OnlineBackupTests.tests+ ]
+ test/OnlineBackupTests.hs view
@@ -0,0 +1,53 @@+module OnlineBackupTests ( tests ) where++import Control.Exception+import Control.Monad++import Database.EJDB2+import Database.EJDB2.Options++import Plant++import Prelude hiding ( id )++import System.Directory++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testDatabaseOpts) close $+ \databaseIO -> testGroup "onlineBackup" [ onlineBackupTest databaseIO ]++testDatabaseOpts :: Options+testDatabaseOpts =+ minimalOptions "./test/onlinebackup-db" [ truncateOpenFlags ]++backupFilePath :: String+backupFilePath = "./test/onlinebackup-backup-db"++onlineBackupTest :: IO Database -> TestTree+onlineBackupTest databaseIO = testCase "onlineBackupTest" $ do+ doesFileExist backupFilePath+ >>= \exists -> when exists $ removeFile backupFilePath+ database <- databaseIO+ id <- putNew database "plants" plant+ _ <- onlineBackup database backupFilePath+ backupFileExists <- doesFileExist backupFilePath+ backupFileExists @? "backup file exists"+ databaseLast <- open $ minimalOptions backupFilePath [ readonlyOpenFlags ]+ catch (do+ backupPlant <- getById databaseLast "plants" id+ backupPlant @?= Just plant+ close databaseLast)+ (\e -> do+ close databaseLast+ throw (e :: IOException))+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }+
+ test/Plant.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE DeriveGeneric #-}++module Plant where++import Data.Aeson ( FromJSON, ToJSON )++import GHC.Generics++import Prelude hiding ( id )++data Plant = Plant { id :: Maybe Int+ , name :: Maybe String+ , isTree :: Maybe Bool+ , year :: Maybe Int+ , description :: Maybe String+ , ratio :: Maybe Double+ }+ deriving ( Eq, Generic, Show )++instance FromJSON Plant++instance ToJSON Plant++nothingPlant :: Plant+nothingPlant = Plant Nothing Nothing Nothing Nothing Nothing Nothing
+ test/PutTests.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE OverloadedStrings #-}++module PutTests ( tests ) where++import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Strict as Map+import qualified Data.Vector as Vector++import Database.EJDB2+import Database.EJDB2.Options++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testDatabaseOpts) close $ \databaseIO ->+ testGroup "put"+ [ putNewTest databaseIO+ , putOnNewIdTest databaseIO+ , putOnExistingIdTest databaseIO+ , mergeOrPutNewTest databaseIO+ , mergeOrPutExistingTest databaseIO+ , patchTest databaseIO+ ]++testDatabaseOpts :: Options+testDatabaseOpts = minimalOptions "./test/put-db" [ truncateOpenFlags ]++putNewTest :: IO Database -> TestTree+putNewTest databaseIO = testCase "putNewTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ storedPlant <- getById database "plants" id+ storedPlant @?= Just plant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++putOnNewIdTest :: IO Database -> TestTree+putOnNewIdTest databaseIO = testCase "putOnNewIdTest" $ do+ database <- databaseIO+ put database "plants" plant 42+ storedPlant <- getById database "plants" 42+ storedPlant @?= Just plant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++putOnExistingIdTest :: IO Database -> TestTree+putOnExistingIdTest databaseIO = testCase "putOnExistingIdTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ put database "plants" lastPlant id+ storedPlant <- getById database "plants" id+ storedPlant @?= Just lastPlant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++ lastPlant = plant { description = Just "a tipical christmas tree" }++mergeOrPutNewTest :: IO Database -> TestTree+mergeOrPutNewTest databaseIO = testCase "mergeOrPutNewTest" $ do+ database <- databaseIO+ mergeOrPut database "plants" plant 4242+ storedPlant <- getById database "plants" 4242+ storedPlant @?= Just plant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++mergeOrPutExistingTest :: IO Database -> TestTree+mergeOrPutExistingTest databaseIO = testCase "mergeOrPutExistingTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ mergeOrPut database "plants" jsonPatch id+ storedPlant <- getById database "plants" id+ storedPlant @?= Just lastPlant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++ jsonPatch = Aeson.Object $+ Map.fromList [ ("year", Aeson.Null)+ , ("description", "a tipical christmas tree")+ ]++ lastPlant =+ plant { year = Nothing, description = Just "a tipical christmas tree" }++patchTest :: IO Database -> TestTree+patchTest databaseIO = testCase "patchTest" $ do+ database <- databaseIO+ id <- putNew database "plants" plant+ patch database "plants" jsonPatch id+ storedPlant <- getById database "plants" id+ storedPlant @?= Just lastPlant+ where+ plant = nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }++ jsonPatch = Aeson.Array $+ Vector.fromList [ Aeson.Object $ Map.fromList [ ("op", "remove")+ , ("path", "/year")+ ]+ , Aeson.Object $+ Map.fromList [ ("op", "replace")+ , ("path", "/description")+ , ( "value"+ , "a tipical christmas tree"+ )+ ]+ ]++ lastPlant =+ plant { year = Nothing, description = Just "a tipical christmas tree" }
+ test/QueryTests.hs view
@@ -0,0 +1,306 @@+module QueryTests ( tests ) where++import Asserts++import Data.Aeson ( Value )+import Data.Int++import Database.EJDB2+import Database.EJDB2.Options+import qualified Database.EJDB2.Query as Query++import Plant++import Prelude hiding ( id )++import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = withResource (open testReadOnlyDatabaseOpts) close $ \databaseIO ->+ testGroup "get"+ [ getListWithBoolQueryTest databaseIO+ , getListWithI64QueryTest databaseIO+ , getListWithI64AtIndexQueryTest databaseIO+ , getListWithF64QueryTest databaseIO+ , getListWithF64AtIndexQueryTest databaseIO+ , getListWithStringQueryTest databaseIO+ , getListWithStringAtIndexQueryTest databaseIO+ , getListWithRegexQueryTest databaseIO+ , getListWithRegexAtIndexQueryTest databaseIO+ , getListWithNullQueryTest databaseIO+ , getListWithNullAtIndexQueryTest databaseIO+ , getListWithMixedQueryTest databaseIO+ , getListWithTwoStringsQueryTest databaseIO+ ]++testReadOnlyDatabaseOpts :: Options+testReadOnlyDatabaseOpts =+ minimalOptions "./test/read-only-db" [ readonlyOpenFlags ]++getListWithBoolQueryTest :: IO Database -> TestTree+getListWithBoolQueryTest databaseIO = testCase "getListWithBoolQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[isTree=:?] | asc /name"+ Query.setBoolAtIndex False 0 query+ plants <- getList database query+ plants+ @?= [ ( 2+ , Just nothingPlant { id = Nothing+ , name = Just "gentiana brentae"+ , isTree = Just False+ , year = Just 2008+ , description = Just "violet 🌺flower"+ }+ )+ , ( 3+ , Just nothingPlant { id = Nothing+ , name = Just "leontopodium"+ , isTree = Just False+ , year = Just 1817+ , description =+ Just "tipical alpine flower"+ }+ )+ , ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListWithI64QueryTest :: IO Database -> TestTree+getListWithI64QueryTest databaseIO = testCase "getListWithI64Query" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[year>:year] | asc /name"+ Query.setI64 1800 "year" query+ plants <- getList database query+ plants+ @?= [ ( 2+ , Just nothingPlant { id = Nothing+ , name = Just "gentiana brentae"+ , isTree = Just False+ , year = Just 2008+ , description = Just "violet 🌺flower"+ }+ )+ , ( 3+ , Just nothingPlant { id = Nothing+ , name = Just "leontopodium"+ , isTree = Just False+ , year = Just 1817+ , description =+ Just "tipical alpine flower"+ }+ )+ ]++getListWithI64AtIndexQueryTest :: IO Database -> TestTree+getListWithI64AtIndexQueryTest databaseIO = testCase "getListWithI64AtIndexQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[year>:?] | asc /name"+ Query.setI64AtIndex 1800 0 query+ plants <- getList database query+ plants+ @?= [ ( 2+ , Just nothingPlant { id = Nothing+ , name = Just "gentiana brentae"+ , isTree = Just False+ , year = Just 2008+ , description = Just "violet 🌺flower"+ }+ )+ , ( 3+ , Just nothingPlant { id = Nothing+ , name = Just "leontopodium"+ , isTree = Just False+ , year = Just 1817+ , description =+ Just "tipical alpine flower"+ }+ )+ ]++getListWithF64QueryTest :: IO Database -> TestTree+getListWithF64QueryTest databaseIO = testCase "getListWithF64Query" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[ratio > :ratio] | asc /name"+ Query.setF64 1.6 "ratio" query+ plants <- getList database query+ plants @?= [ ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListWithF64AtIndexQueryTest :: IO Database -> TestTree+getListWithF64AtIndexQueryTest databaseIO =+ testCase "getListWithF64AtIndexQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[ratio > :?] | asc /name"+ Query.setF64AtIndex 1.6 0 query+ plants <- getList database query+ plants @?= [ ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListWithStringQueryTest :: IO Database -> TestTree+getListWithStringQueryTest databaseIO = testCase "getListWithStringQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[name=:name] | asc /name"+ Query.setString "pinus" "name" query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }+ )+ ]++getListWithStringAtIndexQueryTest :: IO Database -> TestTree+getListWithStringAtIndexQueryTest databaseIO =+ testCase "getListWithStringAtIndexQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[name=:?] | asc /name"+ Query.setStringAtIndex "pinus" 0 query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }+ )+ ]++getListWithRegexQueryTest :: IO Database -> TestTree+getListWithRegexQueryTest databaseIO = testCase "getListWithRegexQuery" $ do+ database <- databaseIO+ query+ <- Query.fromString "@plants/[description re :description ] | asc /name"+ Query.setRegex ".*Italy.*" "description" query+ plants <- getList database query+ plants @?= [ ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListWithRegexAtIndexQueryTest :: IO Database -> TestTree+getListWithRegexAtIndexQueryTest databaseIO =+ testCase "getListWithRegexAtIndexQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[description re :?] | asc /name"+ Query.setRegexAtIndex "very.*Italy" 0 query+ plants <- getList database query+ plants @?= [ ( 4+ , Just nothingPlant { id = Nothing+ , name =+ Just "leucanthemum vulgare"+ , isTree = Just False+ , year = Just 1778+ , description = Just "very common flower in Italy 🍕"+ , ratio = Just 1.618+ }+ )+ ]++getListWithNullQueryTest :: IO Database -> TestTree+getListWithNullQueryTest databaseIO = testCase "getListWithNullQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[ratio=:ratio] | asc /name"+ Query.setNull "ratio" query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ , ratio = Nothing+ }+ )+ ]++getListWithNullAtIndexQueryTest :: IO Database -> TestTree+getListWithNullAtIndexQueryTest databaseIO =+ testCase "getListWithNullAtIndexQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[ratio=:?] | asc /name"+ Query.setNullAtIndex 0 query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ , ratio = Nothing+ }+ )+ ]++getListWithMixedQueryTest :: IO Database -> TestTree+getListWithMixedQueryTest databaseIO = testCase "getListWithMixedQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[year>:year] and /[isTree=:?] and /[name=:?] | asc /name"+ Query.setI64 1700 "year" query+ Query.setBoolAtIndex True 0 query+ Query.setStringAtIndex "pinus" 1 query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }+ )+ ]++getListWithTwoStringsQueryTest :: IO Database -> TestTree+getListWithTwoStringsQueryTest databaseIO =+ testCase "getListWithTwoStringsQuery" $ do+ database <- databaseIO+ query <- Query.fromString "@plants/[description re :descr] and /[name=:name] | asc /name"+ Query.setRegex ".*wow.*" "descr" query+ Query.setString "pinus" "name" query+ plants <- getList database query+ plants @?= [ ( 1+ , Just nothingPlant { id = Nothing+ , name = Just "pinus"+ , isTree = Just True+ , year = Just 1753+ , description = Just "wow 🌲"+ }+ )+ ]
+ test/read-only-db view
binary file changed (absent → 20480 bytes)