diff --git a/docker-compose.yml b/docker-compose.yml index c240d86..65a382c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,10 @@ -version: "3" services: dev-db: container_name: dev-db - image: gvenzl/oracle-free:23-slim-faststart + image: container-registry-mumbai.oracle.com/database/free volumes: - - ./db:/container-entrypoint-startdb.d + - ./init-scripts:/opt/oracle/scripts/startup environment: - ORACLE_DATABASE: devdb - ORACLE_PASSWORD: password - APP_USER: username - APP_USER_PASSWORD: password + ORACLE_PWD: password ports: - 1521:1521 diff --git a/init-scripts/init.sql b/init-scripts/init.sql new file mode 100644 index 0000000..dc62f2a --- /dev/null +++ b/init-scripts/init.sql @@ -0,0 +1,4 @@ +alter session set "_oracle_script"=true; +CREATE USER username IDENTIFIED BY password; +GRANT ALL PRIVILEGES TO username; +GRANT EXECUTE ON DBMS_AQADM TO username; \ No newline at end of file diff --git a/oracle-simple.cabal b/oracle-simple.cabal index 366a0bc..c983f02 100644 --- a/oracle-simple.cabal +++ b/oracle-simple.cabal @@ -39,6 +39,7 @@ library Database.Oracle.Simple.ToField Database.Oracle.Simple.ToRow Database.Oracle.Simple.Transaction + Database.Oracle.Simple.Queue hs-source-dirs: src c-sources: @@ -92,6 +93,7 @@ executable tests , hspec-hedgehog , oracle-simple , time + , bytestring source-repository head type: git diff --git a/src/Database/Oracle/Simple.hs b/src/Database/Oracle/Simple.hs index 19d19f5..b058cad 100644 --- a/src/Database/Oracle/Simple.hs +++ b/src/Database/Oracle/Simple.hs @@ -10,3 +10,4 @@ import Database.Oracle.Simple.Query as Export import Database.Oracle.Simple.ToField as Export import Database.Oracle.Simple.ToRow as Export import Database.Oracle.Simple.Transaction as Export +import Database.Oracle.Simple.Queue as Export diff --git a/src/Database/Oracle/Simple/Internal.hs b/src/Database/Oracle/Simple/Internal.hs index 7756a15..bd44eae 100644 --- a/src/Database/Oracle/Simple/Internal.hs +++ b/src/Database/Oracle/Simple/Internal.hs @@ -44,6 +44,8 @@ module Database.Oracle.Simple.Internal OracleError (..), ErrorInfo (..), VersionInfo (..), + DPIJson (..), + genJSON, renderErrorInfo, ping, fetch, @@ -119,6 +121,10 @@ newtype DPIShardingKeyColumn = DPIShardingKeyColumn (Ptr DPIShardingKeyColumn) deriving (Show, Eq) deriving newtype (Storable) +newtype DPIJson = DPIJson (Ptr DPIJson) + deriving (Show, Eq) + deriving newtype (Storable) + data AdditionalConnectionParams = AdditionalConnectionParams { minSessions :: Natural , maxSessions :: Natural @@ -207,7 +213,7 @@ foreign import ccall unsafe "dpiConn_create" Ptr DPICommonCreateParams -> -- | const dpiConnCreateParams *createParams Ptr ConnectionCreateParams -> - -- | dpi * conn + -- | dpiConn ** conn Ptr DPIConn -> IO CInt @@ -1752,3 +1758,18 @@ Structurally equivalent to 'Data.Functor.Identity.Identity'. newtype Only a = Only {fromOnly :: a} deriving stock (Eq, Ord, Read, Show, Generic) deriving newtype (Enum) + +genJSON :: Connection -> IO DPIJson +genJSON (Connection fptr) = do + withForeignPtr fptr $ \conn -> do + alloca $ \jsonPtr -> do + throwOracleError =<< dpiConn_newJson conn jsonPtr + peek jsonPtr + +foreign import ccall unsafe "dpiConn_newJson" + dpiConn_newJson :: + -- | dpiConn * + Ptr DPIConn -> + -- | dpiJSON ** + Ptr DPIJson -> + IO CInt diff --git a/src/Database/Oracle/Simple/Queue.hs b/src/Database/Oracle/Simple/Queue.hs new file mode 100644 index 0000000..d65c423 --- /dev/null +++ b/src/Database/Oracle/Simple/Queue.hs @@ -0,0 +1,326 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ViewPatterns #-} + +module Database.Oracle.Simple.Queue ( + DPIQueue (..) + , DPIMsgProps (..) + , DPIDeqOptions (..) + , DPIEnqOptions (..) + , DPIObjectType (..) + , ObjectType (..) + , deqMany + , deqOne + , enqMany + , enqOne + , getDeqOptions + , getEnqOptions + , queueRelease + , genJSONQueue + , genMsgProps + , genQueue + , getMsgPropsNumOfAttempts + , getMsgPropsDelay + , getMsgPropsPayLoadBytes + , getMsgPropsPayLoadJson + , setMsgPropsPayLoadBytes + , setMsgPropsPayLoadJSON +) where + +import Foreign (alloca, withArray, withForeignPtr, nullPtr) +import Foreign.Storable.Generic (Storable (..)) +import Foreign.C.Types (CInt (..), CUInt (..)) +import Foreign.Ptr (Ptr) +import Foreign.C.String +import Database.Oracle.Simple.Internal +import qualified Data.ByteString.Char8 as BSC + +newtype DPIQueue = DPIQueue (Ptr DPIQueue) + deriving (Show, Eq) + deriving newtype (Storable) + +newtype DPIMsgProps = DPIMsgProps (Ptr DPIMsgProps) + deriving (Show, Eq) + deriving newtype (Storable) + +newtype DPIDeqOptions = DPIDeqOptions (Ptr DPIDeqOptions) + deriving (Show, Eq) + deriving newtype (Storable) + +newtype DPIEnqOptions = DPIEnqOptions (Ptr DPIEnqOptions) + deriving (Show, Eq) + deriving newtype (Storable) + +newtype DPIObjectType = DPIObjectType (Ptr DPIObjectType) + deriving (Show, Eq) + deriving newtype (Storable) + +data ObjectType = JSON | Raw + deriving (Show, Eq) + +deqMany :: DPIQueue -> Int -> IO DPIMsgProps +deqMany dpiQueue numProps = do + alloca $ \dpiMsgPropsPtr -> do + alloca $ \numPropsPtr -> do + poke numPropsPtr (fromIntegral numProps) + throwOracleError =<< + dpiQueue_deqMany dpiQueue numPropsPtr dpiMsgPropsPtr + peek dpiMsgPropsPtr + +foreign import ccall unsafe "dpiQueue_deqMany" + dpiQueue_deqMany :: + -- | dpiQueue * + DPIQueue -> + -- | numProps * + Ptr CUInt -> + -- | props ** + Ptr DPIMsgProps -> + IO CInt + +deqOne :: DPIQueue -> IO DPIMsgProps +deqOne dpiQueue = do + alloca $ \dpiMsgPropsPtr -> do + throwOracleError =<< + dpiQueue_deqOne dpiQueue dpiMsgPropsPtr + peek dpiMsgPropsPtr + +foreign import ccall unsafe "dpiQueue_deqOne" + dpiQueue_deqOne :: + -- | dpiQueue * + DPIQueue -> + -- | props ** + Ptr DPIMsgProps -> + IO CInt + +{- | +Warning: calling this function in parallel on different connections acquired from +the same pool may fail due to Oracle bug 29928074. Ensure that this function is not +run in parallel, use standalone connections or connections from different pools, or +make multiple calls to dpiQueue_enqOne() instead. The function dpiQueue_deqMany() call is not affected. +-} +enqMany :: DPIQueue -> [DPIMsgProps] -> IO () +enqMany dpiQueue dpiMsgPropss = do + let numOfProps = length dpiMsgPropss + withArray dpiMsgPropss $ \dpiMsgPropsPtr -> do + alloca $ \numPropsPtr -> do + poke numPropsPtr (fromIntegral numOfProps) + throwOracleError =<< + dpiQueue_enqMany dpiQueue numPropsPtr dpiMsgPropsPtr + +foreign import ccall unsafe "dpiQueue_enqMany" + dpiQueue_enqMany :: + -- | dpiQueue * + DPIQueue -> + -- | numProps * + Ptr CUInt -> + -- | props ** + Ptr DPIMsgProps -> + IO CInt + +enqOne :: DPIQueue -> DPIMsgProps -> IO () +enqOne dpiQueue dpiMsgProps = + throwOracleError =<< + dpiQueue_enqOne dpiQueue dpiMsgProps + +foreign import ccall unsafe "dpiQueue_enqOne" + dpiQueue_enqOne :: + -- | dpiQueue * + DPIQueue -> + -- | props * + DPIMsgProps -> + IO CInt + +getDeqOptions :: DPIQueue -> IO DPIDeqOptions +getDeqOptions dpiQueue = do + alloca $ \dpiDeqOptionsPtr -> do + throwOracleError =<< dpiQueue_getDeqOptions dpiQueue dpiDeqOptionsPtr + peek dpiDeqOptionsPtr + +foreign import ccall unsafe "dpiQueue_getDeqOptions" + dpiQueue_getDeqOptions :: + -- | dpiQueue * + DPIQueue -> + -- | options ** + Ptr DPIDeqOptions -> + IO CInt + +getEnqOptions :: DPIQueue -> IO DPIEnqOptions +getEnqOptions dpiQueue = do + alloca $ \dpiEnqOptionsPtr -> do + throwOracleError =<< dpiQueue_getEnqOptions dpiQueue dpiEnqOptionsPtr + peek dpiEnqOptionsPtr + +foreign import ccall unsafe "dpiQueue_getEnqOptions" + dpiQueue_getEnqOptions :: + -- | dpiQueue * + DPIQueue -> + -- | options ** + Ptr DPIEnqOptions -> + IO CInt + +queueRelease :: DPIQueue -> IO () +queueRelease dpiQueue = throwOracleError =<< dpiQueue_release dpiQueue + +foreign import ccall unsafe "dpiQueue_release" + dpiQueue_release :: + -- | dpiQueue * + DPIQueue -> + IO CInt + +genJSONQueue :: Connection -> String -> IO DPIQueue +genJSONQueue (Connection fptr) queueName = do + withForeignPtr fptr $ \conn -> do + alloca $ \dpiQueuePtr -> do + withCStringLen queueName $ \(queueNameC , fromIntegral -> queueNameLen) -> do + throwOracleError =<< dpiConn_newJsonQueue conn queueNameC queueNameLen dpiQueuePtr + peek dpiQueuePtr + +foreign import ccall unsafe "dpiConn_newJsonQueue" + dpiConn_newJsonQueue :: + -- | dpiConn * + Ptr DPIConn -> + -- | char* name + CString -> + -- | name Length + CUInt -> + -- | dpiQueue ** + Ptr DPIQueue -> + IO CInt + +genMsgProps :: Connection -> IO DPIMsgProps +genMsgProps (Connection fptr) = do + withForeignPtr fptr $ \conn -> do + alloca $ \dpiMsgPropsPtr -> do + throwOracleError =<< dpiConn_newMsgProps conn dpiMsgPropsPtr + peek dpiMsgPropsPtr + +foreign import ccall unsafe "dpiConn_newMsgProps" + dpiConn_newMsgProps :: + -- | dpiConn * + Ptr DPIConn -> + -- | dpiMsgProps ** + Ptr DPIMsgProps -> + IO CInt + +-- For now, Passing objectType will not work :( use setPayLoadType. +genQueue :: Connection -> String -> IO DPIQueue +genQueue (Connection fptr) queueName = do + withForeignPtr fptr $ \conn -> do + alloca $ \dpiQueuePtr -> do + withCStringLen queueName $ \(queueNameC , fromIntegral -> queueNameLen) -> do + throwOracleError =<< dpiConn_newQueue conn queueNameC queueNameLen nullPtr dpiQueuePtr + -- TODO: Accomodate ObjectType + peek dpiQueuePtr + +foreign import ccall unsafe "dpiConn_newQueue" + dpiConn_newQueue :: + -- | dpiConn * + Ptr DPIConn -> + -- | char* name + CString -> + -- | name Length + CUInt -> + -- | dpiObjectType * + Ptr () -> + -- | dpiQueue ** + Ptr DPIQueue -> + IO CInt + +-----x DPI MsgProps related functions x----- + +getMsgPropsNumOfAttempts :: DPIMsgProps -> IO Int +getMsgPropsNumOfAttempts dpiMsgProps = do + alloca $ \numPtr -> do + throwOracleError =<< dpiMsgProps_getNumAttempts dpiMsgProps numPtr + fromIntegral <$> peek numPtr + +foreign import ccall unsafe "dpiMsgProps_getNumAttempts" + dpiMsgProps_getNumAttempts :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | Number of Attempts that will be read. + Ptr CUInt -> + IO CInt + +getMsgPropsDelay :: DPIMsgProps -> IO Int +getMsgPropsDelay dpiMsgProps = do + alloca $ \numPtr -> do + throwOracleError =<< dpiMsgProps_getDelay dpiMsgProps numPtr + fromIntegral <$> peek numPtr + +foreign import ccall unsafe "dpiMsgProps_getDelay" + dpiMsgProps_getDelay :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | Number of delayed seconds from given Message prop. + Ptr CUInt -> + IO CInt + + +{- +This function internally calls getPayLoad which either returns payLoad in either Object or in bytes. +Hence, the result might be null. +-} +getMsgPropsPayLoadBytes :: DPIMsgProps -> IO (Maybe BSC.ByteString) +getMsgPropsPayLoadBytes dpiMsgProps = do + alloca $ \dpiObjectPtr -> do + alloca $ \cStringPtr -> do + alloca $ \cStringLengthptr -> do + throwOracleError =<< dpiMsgProps_getPayload dpiMsgProps dpiObjectPtr cStringPtr cStringLengthptr + cStr <- peek cStringPtr + if cStr == nullPtr + then return Nothing + else Just . BSC.pack <$> peekCString cStr + +foreign import ccall unsafe "dpiMsgProps_getPayload" + dpiMsgProps_getPayload :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | dpiObject ** + Ptr DPIObjectType -> + -- | const char ** value + Ptr CString -> + -- | valueLength + Ptr CUInt -> + IO CInt + +getMsgPropsPayLoadJson :: DPIMsgProps -> IO DPIJson +getMsgPropsPayLoadJson dpiMsgProps = do + alloca $ \dpiJsonPtr -> do + throwOracleError =<< dpiMsgProps_getPayloadJson dpiMsgProps dpiJsonPtr + peek dpiJsonPtr + +foreign import ccall unsafe "dpiMsgProps_getPayloadJson" + dpiMsgProps_getPayloadJson :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | dpiJson ** + Ptr DPIJson -> + IO CInt + +setMsgPropsPayLoadBytes :: DPIMsgProps -> BSC.ByteString -> IO () +setMsgPropsPayLoadBytes dpiMsgProps payLoad = do + withCStringLen (BSC.unpack payLoad) $ \(payLoadC , fromIntegral -> payLoadLen) -> do + throwOracleError =<< dpiMsgProps_setPayloadBytes dpiMsgProps payLoadC payLoadLen + +foreign import ccall unsafe "dpiMsgProps_setPayloadBytes" + dpiMsgProps_setPayloadBytes :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | const char * value + CString -> + -- | uint32 valueLength + CUInt -> + IO CInt + +setMsgPropsPayLoadJSON :: DPIMsgProps -> DPIJson -> IO () +setMsgPropsPayLoadJSON dpiMsgProps payLoadJson = do + throwOracleError =<< dpiMsgProps_setPayloadJson dpiMsgProps payLoadJson + +foreign import ccall unsafe "dpiMsgProps_setPayloadJson" + dpiMsgProps_setPayloadJson :: + -- | dpiMsgProps * + DPIMsgProps -> + -- | dpiJson * + DPIJson -> + IO CInt diff --git a/test/Main.hs b/test/Main.hs index dd9f374..69a363c 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -4,6 +4,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE OverloadedStrings #-} module Main ( main, @@ -25,6 +26,7 @@ import Test.Hspec.Hedgehog (hedgehog) import Foreign (peek, Storable, with, nullFunPtr, nullPtr) import Foreign.C.Types (CLong(..), CUInt(..), CInt(..)) import Foreign.C.String (newCString) +import qualified Data.ByteString.Char8 as BSC import Database.Oracle.Simple @@ -56,7 +58,7 @@ main :: IO () main = withPool params $ hspec . spec params :: ConnectionParams -params = ConnectionParams "username" "password" "localhost:1521/devdb" Nothing +params = ConnectionParams "username" "password" "localhost:1521/free" Nothing genDPITimestamp :: HH.Gen DPITimestamp genDPITimestamp = do @@ -388,6 +390,39 @@ spec pool = do } result <- roundTripStorable dPIJsonNode result `shouldBe` dPIJsonNode + describe "Advanced Queuing" $ do + it "should create and release a raw queue successfully" $ \conn -> do + queue <- genQueue conn "test_queue" + queueRelease queue + -- No exception implies success + it "should set and get a msgProp payload" $ \conn -> do + msgProps <- genMsgProps conn + setMsgPropsPayLoadBytes msgProps (BSC.pack "Hello from Haskell!") + payload <- getMsgPropsPayLoadBytes msgProps + payload `shouldBe` Just "Hello from Haskell!" + it "should enque and deque msg prop from queue" $ \conn -> do + void $ execute_ conn "\ + \BEGIN\ + \ DBMS_AQADM.CREATE_QUEUE_TABLE(\ + \ queue_table => 'TEST_QUEUE_TABLE',\ + \ queue_payload_type => 'RAW'\ + \ );\ + \ DBMS_AQADM.CREATE_QUEUE(\ + \ queue_name => 'TEST_QUEUE',\ + \ queue_table => 'TEST_QUEUE_TABLE'\ + \ );\ + \ DBMS_AQADM.START_QUEUE(\ + \ queue_name => 'TEST_QUEUE'\ + \);\ + \END;" + msgProps <- genMsgProps conn + setMsgPropsPayLoadBytes msgProps (BSC.pack "Hello from Haskell!") + queue <- genQueue conn "TEST_QUEUE" + void $ enqOne queue msgProps + newMsgProps <- deqOne queue + payload <- getMsgPropsPayLoadBytes newMsgProps + payload `shouldBe` Just "Hello from Haskell!" + queueRelease queue where handleOracleError action = Exc.try @OracleError action >>= either (\_ -> pure ()) (\_ -> pure ())