1{-# LANGUAGE DeriveAnyClass, DeriveGeneric, ScopedTypeVariables, BangPatterns, MonoLocalBinds #-}
3Module: ProjectM36.Client
5Client interface to local and remote Project:M36 databases. To get started, connect with 'connectProjectM36', then run some database changes with 'executeDatabaseContextExpr', and issue queries using 'executeRelationalExpr'.
7module ProjectM36.Client
17 executeRelationalExpr,
18 executeDatabaseContextExpr,
19 executeDatabaseContextIOExpr,
23 executeTransGraphRelationalExpr,
26 typeForRelationalExpr,
27 inclusionDependencies,
28 ProjectM36.Client.typeConstructorMapping,
29 ProjectM36.Client.databaseContextFunctionsAsRelation,
30 planForDatabaseContextExpr,
35 transactionGraphAsRelation,
36 relationVariablesAsRelation,
37 ProjectM36.Client.atomFunctionsAsRelation,
38 disconnectedTransactionIsDirty,
44 defaultRemoteConnectionInfo,
46 PersistenceStrategy(..),
48 RelationalExprBase(..),
49 DatabaseContextExpr(..),
50 DatabaseContextIOExpr(..),
55 createSessionAtCommit,
60 RelationCardinality(..),
61 TransactionGraphOperator(..),
62 ProjectM36.Client.autoMergeToHead,
64 disconnectedTransaction_,
65 TransGraphRelationalExpr,
66 TransactionIdLookup(..),
67 TransactionIdHeadBacktrack(..),
73 emptyNotificationCallback,
74 EvaluatedNotification(..),
77 inclusionDependencyForKey,
78 databaseContextExprForUniqueKey,
79 databaseContextExprForForeignKey,
80 createScriptedAtomFunction,
81 AttributeExprBase(..),
82 TypeConstructorBase(..),
83 TypeConstructorDef(..),
84 DataConstructorDef(..),
85 AttributeNamesBase(..),
88 InclusionDependency(..),
92 DF.AttributeOrderExpr,
95 RequestTimeoutException(..),
96 RemoteProcessDiedException(..),
101 RestrictionPredicateExprBase(..),
104import ProjectM36.Base hiding (inclusionDependencies) --defined in this module as well
105import qualified ProjectM36.Base as B
106import ProjectM36.Error
107import ProjectM36.Atomable
108import ProjectM36.AtomFunction as AF
109import ProjectM36.StaticOptimizer
111import qualified ProjectM36.DataFrame as DF
112import ProjectM36.DatabaseContextFunction as DCF
113import qualified ProjectM36.IsomorphicSchema as Schema
114import Control.Monad.State
115import Control.Monad.Trans.Reader
116import qualified ProjectM36.RelationalExpression as RE
117import ProjectM36.DatabaseContext (basicDatabaseContext)
118import qualified ProjectM36.TransactionGraph as Graph
119import ProjectM36.TransactionGraph
120import qualified ProjectM36.Transaction as Trans
121import ProjectM36.TransactionGraph.Persist
122import ProjectM36.Attribute
123import ProjectM36.TransGraphRelationalExpression (TransGraphRelationalExpr, evalTransGraphRelationalExpr)
124import ProjectM36.Persist (DiskSync(..))
125import ProjectM36.FileLock
126import ProjectM36.Notifications
127import ProjectM36.Server.RemoteCallTypes
128import qualified ProjectM36.DisconnectedTransaction as Discon
129import ProjectM36.Relation (typesAsRelation)
130import ProjectM36.ScriptSession (initScriptSession, ScriptSession)
131import qualified ProjectM36.Relation as R
132import qualified ProjectM36.DatabaseContext as DBC
133import Control.Exception.Base
136import Network.Transport (Transport(closeTransport))
138#if MIN_VERSION_network_transport_tcp(0,7,0)
139import Network.Transport.TCP (createTransport, defaultTCPParameters, defaultTCPAddr)
140import Network.Transport.TCP.Internal (encodeEndPointAddress)
141#elif MIN_VERSION_network_transport_tcp(0,6,0)
142import Network.Transport.TCP (createTransport, defaultTCPParameters)
143import Network.Transport.TCP.Internal (encodeEndPointAddress)
145import Network.Transport.TCP (encodeEndPointAddress, createTransport, defaultTCPParameters)
148import Control.Distributed.Process.Node (newLocalNode, initRemoteTable, runProcess, LocalNode, forkProcess, closeLocalNode)
149import Control.Distributed.Process.Extras.Internal.Types (whereisRemote)
150import Control.Distributed.Process.ManagedProcess.Client (call, safeCall)
151import Data.Either (isRight)
152import Data.UUID.V4 (nextRandom)
154import Control.Distributed.Process (ProcessId, Process, receiveWait, send, match, NodeId(..), reconnect)
155import Control.Exception (IOException, handle, AsyncException, throwIO, fromException, Exception)
156import Control.Concurrent.MVar
157import qualified Data.Map as M
158import Control.Distributed.Process.Serializable (Serializable)
159#if MIN_VERSION_stm_containers(1,0,0)
160import qualified StmContainers.Map as StmMap
161import qualified StmContainers.Set as StmSet
163import qualified STMContainers.Map as StmMap
164import qualified STMContainers.Set as StmSet
166import qualified ProjectM36.Session as Sess
167import ProjectM36.Session
168import ProjectM36.Sessions
169import Data.Binary (Binary)
170import GHC.Generics (Generic)
171import Control.DeepSeq (force)
173import Data.Time.Clock
175type Hostname = String
179type DatabaseName = String
181-- | The type for notifications callbacks in the client. When a registered notification fires due to a changed relational expression evaluation, the server propagates the notifications to the clients in the form of the callback.
182type NotificationCallback = NotificationName -> EvaluatedNotification -> IO ()
184-- | The empty notification callback ignores all callbacks.
185emptyNotificationCallback :: NotificationCallback
186emptyNotificationCallback _ _ = pure ()
188type GhcPkgPath = String
190data RemoteProcessDiedException = RemoteProcessDiedException
193instance Exception RemoteProcessDiedException
195data RequestTimeoutException = RequestTimeoutException
198instance Exception RequestTimeoutException
200-- | Construct a 'ConnectionInfo' to describe how to make the 'Connection'. The database can be run within the current process or running remotely via distributed-process.
201data ConnectionInfo = InProcessConnectionInfo PersistenceStrategy NotificationCallback [GhcPkgPath]|
202 RemoteProcessConnectionInfo DatabaseName NodeId NotificationCallback
204type EvaluatedNotifications = M.Map NotificationName EvaluatedNotification
206-- | Used for callbacks from the server when monitored changes have been made.
207newtype NotificationMessage = NotificationMessage EvaluatedNotifications
208 deriving (Binary, Eq, Show, Generic)
210-- | When a notification is fired, the 'reportOldExpr' is evaluated in the commit's pre-change context while the 'reportNewExpr' is evaluated in the post-change context and they are returned along with the original notification.
211data EvaluatedNotification = EvaluatedNotification {
212 notification :: Notification,
213 reportOldRelation :: Either RelationalError Relation,
214 reportNewRelation :: Either RelationalError Relation
216 deriving(Binary, Eq, Show, Generic)
219-- | Create a 'NodeId' for use in connecting to a remote server using distributed-process.
220createNodeId :: Hostname -> Port -> NodeId
221createNodeId host port = NodeId $ encodeEndPointAddress host (show port) 1
223-- | Use this for connecting to remote servers on the default port.
224defaultServerPort :: Port
225defaultServerPort = 6543
227-- | Use this for connecting to remote servers with the default database name.
228defaultDatabaseName :: DatabaseName
229defaultDatabaseName = "base"
231-- | Use this for connecting to remote servers with the default head name.
232defaultHeadName :: HeadName
233defaultHeadName = "master"
235-- | Create a connection configuration which connects to the localhost on the default server port and default server database name. The configured notification callback is set to ignore all events.
236defaultRemoteConnectionInfo :: ConnectionInfo
237defaultRemoteConnectionInfo = RemoteProcessConnectionInfo defaultDatabaseName (createNodeId "127.0.0.1" defaultServerPort) emptyNotificationCallback
239-- | The 'Connection' represents either local or remote access to a database. All operations flow through the connection.
240type ClientNodes = StmSet.Set ProcessId
242-- internal structure specific to in-process connections
243data InProcessConnectionConf = InProcessConnectionConf {
244 ipPersistenceStrategy :: PersistenceStrategy,
245 ipClientNodes :: ClientNodes,
246 ipSessions :: Sessions,
247 ipTransactionGraph :: TVar TransactionGraph,
248 ipScriptSession :: Maybe ScriptSession,
249 ipLocalNode :: LocalNode,
250 ipTransport :: Transport, -- we hold onto this so that we can close it gracefully
251 ipLocks :: Maybe (LockFile, MVar LockFileHash) -- nothing when NoPersistence
254data RemoteProcessConnectionConf = RemoteProcessConnectionConf {
255 rLocalNode :: LocalNode,
256 rProcessId :: ProcessId, --remote processId
257 rTransport :: Transport --the TCP socket transport
260data Connection = InProcessConnection InProcessConnectionConf |
261 RemoteProcessConnection RemoteProcessConnectionConf
263-- | There are several reasons why a connection can fail.
264data ConnectionError = SetupDatabaseDirectoryError PersistenceError |
265 IOExceptionError IOException |
266 NoSuchDatabaseByNameError DatabaseName |
268 deriving (Show, Eq, Generic)
270remoteDBLookupName :: DatabaseName -> String
271remoteDBLookupName = (++) "db-"
273createLocalNode :: IO (LocalNode, Transport)
275#if MIN_VERSION_network_transport_tcp(0,7,0)
276 eLocalTransport <- createTransport (defaultTCPAddr "127.0.0.1" "0") defaultTCPParameters
277#elif MIN_VERSION_network_transport_tcp(0,6,0)
278 eLocalTransport <- createTransport "127.0.0.1" "0" (\sn -> ("127.0.0.1", sn)) defaultTCPParameters
280 eLocalTransport <- createTransport "127.0.0.1" "0" defaultTCPParameters
282 case eLocalTransport of
283 Left err -> error ("failed to create transport: " ++ show err)
284 Right localTransport -> do
285 localNode <- newLocalNode localTransport initRemoteTable
286 pure (localNode, localTransport)
288notificationListener :: NotificationCallback -> Process ()
289notificationListener callback = do
291 --liftIO $ putStrLn $ "LISTENER THREAD START " ++ show pid
294 match (\(NotificationMessage eNots) ->
295 --say $ "NOTIFICATION: " ++ show eNots
296 -- when notifications are thrown, they are not adjusted for the current schema which could be problematic, but we don't have access to the current session here
297 liftIO $ mapM_ (uncurry callback) (M.toList eNots)
300 --say "LISTENER THREAD EXIT"
303startNotificationListener :: LocalNode -> NotificationCallback -> IO ProcessId
304startNotificationListener localNode callback = forkProcess localNode (notificationListener callback)
306createScriptSession :: [String] -> IO (Maybe ScriptSession)
307createScriptSession ghcPkgPaths = do
308 eScriptSession <- initScriptSession ghcPkgPaths
309 case eScriptSession of
310 Left err -> hPutStrLn stderr ("Failed to load scripting engine- scripting disabled: " ++ show err) >> pure Nothing --not a fatal error, but the scripting feature must be disabled
311 Right s -> pure (Just s)
313-- | To create a 'Connection' to a remote or local database, create a 'ConnectionInfo' and call 'connectProjectM36'.
314connectProjectM36 :: ConnectionInfo -> IO (Either ConnectionError Connection)
315--create a new in-memory database/transaction graph
316connectProjectM36 (InProcessConnectionInfo strat notificationCallback ghcPkgPaths) = do
317 freshId <- nextRandom
318 stamp <- getCurrentTime
319 let bootstrapContext = basicDatabaseContext
320 freshGraph = bootstrapTransactionGraph stamp freshId bootstrapContext
322 --create date examples graph for now- probably should be empty context in the future
324 graphTvar <- newTVarIO freshGraph
325 clientNodes <- StmSet.newIO
326 sessions <- StmMap.newIO
327 (localNode, transport) <- createLocalNode
328 notificationPid <- startNotificationListener localNode notificationCallback
329 mScriptSession <- createScriptSession ghcPkgPaths
331 let conn = InProcessConnection InProcessConnectionConf {
332 ipPersistenceStrategy = strat,
333 ipClientNodes = clientNodes,
334 ipSessions = sessions,
335 ipTransactionGraph = graphTvar,
336 ipScriptSession = mScriptSession,
337 ipLocalNode = localNode,
338 ipTransport = transport,
340 addClientNode conn notificationPid
342 MinimalPersistence dbdir -> connectPersistentProjectM36 strat NoDiskSync dbdir freshGraph notificationCallback ghcPkgPaths
343 CrashSafePersistence dbdir -> connectPersistentProjectM36 strat FsyncDiskSync dbdir freshGraph notificationCallback ghcPkgPaths
345connectProjectM36 (RemoteProcessConnectionInfo databaseName serverNodeId notificationCallback) = do
346 connStatus <- newEmptyMVar
347 (localNode, transport) <- createLocalNode
348 let dbName = remoteDBLookupName databaseName
349 --putStrLn $ "Connecting to " ++ show serverNodeId ++ " " ++ dbName
350 notificationListenerPid <- startNotificationListener localNode notificationCallback
351 runProcess localNode $ do
352 --even a failed connection retains an open socket!
353 mServerProcessId <- whereisRemote serverNodeId dbName
354 case mServerProcessId of
355 Nothing -> liftIO $ putMVar connStatus $ Left (NoSuchDatabaseByNameError databaseName)
356 Just serverProcessId -> do
357 loginConfirmation <- safeLogin (Login notificationListenerPid) serverProcessId
358 if not loginConfirmation then
359 liftIO $ putMVar connStatus (Left LoginError)
361 liftIO $ putMVar connStatus (Right $ RemoteProcessConnection RemoteProcessConnectionConf {rLocalNode = localNode, rProcessId = serverProcessId, rTransport = transport})
364connectPersistentProjectM36 :: PersistenceStrategy ->
368 NotificationCallback ->
370 IO (Either ConnectionError Connection)
371connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghcPkgPaths = do
372 err <- setupDatabaseDir sync dbdir freshGraph
374 Left err' -> return $ Left (SetupDatabaseDirectoryError err')
375 Right (lockFileH, digest) -> do
376 mScriptSession <- createScriptSession ghcPkgPaths
377 graph <- transactionGraphLoad dbdir emptyTransactionGraph mScriptSession
379 Left err' -> return $ Left (SetupDatabaseDirectoryError err')
381 tvarGraph <- newTVarIO graph'
382 sessions <- StmMap.newIO
383 clientNodes <- StmSet.newIO
384 (localNode, transport) <- createLocalNode
385 lockMVar <- newMVar digest
386 let conn = InProcessConnection InProcessConnectionConf {
387 ipPersistenceStrategy = strat,
388 ipClientNodes = clientNodes,
389 ipSessions = sessions,
390 ipTransactionGraph = tvarGraph,
391 ipScriptSession = mScriptSession,
392 ipLocalNode = localNode,
393 ipTransport = transport,
394 ipLocks = Just (lockFileH, lockMVar)
397 notificationPid <- startNotificationListener localNode notificationCallback
398 addClientNode conn notificationPid
401-- | Create a new session at the transaction id and return the session's Id.
402createSessionAtCommit :: Connection -> TransactionId -> IO (Either RelationalError SessionId)
403createSessionAtCommit conn@(InProcessConnection _) commitId = do
404 newSessionId <- nextRandom
405 atomically $ createSessionAtCommit_ commitId newSessionId conn
406createSessionAtCommit conn@(RemoteProcessConnection _) uuid = remoteCall conn (CreateSessionAtCommit uuid)
408createSessionAtCommit_ :: TransactionId -> SessionId -> Connection -> STM (Either RelationalError SessionId)
409createSessionAtCommit_ commitId newSessionId (InProcessConnection conf) = do
410 let sessions = ipSessions conf
411 graphTvar = ipTransactionGraph conf
412 graph <- readTVar graphTvar
413 case transactionForId commitId graph of
414 Left err -> pure (Left err)
415 Right transaction -> do
416 let freshDiscon = DisconnectedTransaction commitId (Trans.schemas transaction) False
417 keyDuplication <- StmMap.lookup newSessionId sessions
418 case keyDuplication of
419 Just _ -> pure $ Left (SessionIdInUseError newSessionId)
421 StmMap.insert (Session freshDiscon defaultSchemaName) newSessionId sessions
422 pure $ Right newSessionId
423createSessionAtCommit_ _ _ (RemoteProcessConnection _) = error "createSessionAtCommit_ called on remote connection"
425-- | Call 'createSessionAtHead' with a transaction graph's head's name to create a new session pinned to that head. This function returns a 'SessionId' which can be used in other function calls to reference the point in the transaction graph.
426createSessionAtHead :: Connection -> HeadName -> IO (Either RelationalError SessionId)
427createSessionAtHead conn@(InProcessConnection conf) headn = do
428 let graphTvar = ipTransactionGraph conf
429 newSessionId <- nextRandom
431 graph <- readTVar graphTvar
432 case transactionForHead headn graph of
433 Nothing -> pure $ Left (NoSuchHeadNameError headn)
434 Just trans -> createSessionAtCommit_ (transactionId trans) newSessionId conn
435createSessionAtHead conn@(RemoteProcessConnection _) headn = remoteCall conn (CreateSessionAtHead headn)
437-- | Used internally for server connections to keep track of remote nodes for the purpose of sending notifications later.
438addClientNode :: Connection -> ProcessId -> IO ()
439addClientNode (RemoteProcessConnection _) _ = error "addClientNode called on remote connection"
440addClientNode (InProcessConnection conf) newProcessId = atomically (StmSet.insert newProcessId (ipClientNodes conf))
442-- | Discards a session, eliminating any uncommitted changes present in the session.
443closeSession :: SessionId -> Connection -> IO ()
444closeSession sessionId (InProcessConnection conf) =
445 atomically $ StmMap.delete sessionId (ipSessions conf)
446closeSession sessionId conn@(RemoteProcessConnection _) = remoteCall conn (CloseSession sessionId)
448-- | 'close' cleans up the database access connection and closes any relevant sockets.
449close :: Connection -> IO ()
450close (InProcessConnection conf) = do
452 let sessions = ipSessions conf
453#if MIN_VERSION_stm_containers(1,0,0)
454 StmMap.reset sessions
456 StmMap.deleteAll sessions
459 closeLocalNode (ipLocalNode conf)
460 closeTransport (ipTransport conf)
461 let mLocks = ipLocks conf
464 Just (lockFileH, _) -> closeLockFile lockFileH
466close conn@(RemoteProcessConnection conf) = do
467 _ <- remoteCall conn Logout :: IO Bool
468 closeLocalNode (rLocalNode conf)
469 closeTransport (rTransport conf)
471--used only by the server EntryPoints
472closeRemote_ :: Connection -> IO ()
473closeRemote_ (InProcessConnection _) = error "invalid call of closeRemote_ on InProcessConnection"
474closeRemote_ (RemoteProcessConnection conf) = runProcess (rLocalNode conf) (reconnect (rProcessId conf))
476 --we need to actually close the localNode's connection to the remote
477--within the database server, we must catch and handle all exception lest they take down the database process- this handling might be different for other use-cases
478--exceptions should generally *NOT* be thrown from any Project:M36 code paths, but third-party code such as AtomFunction scripts could conceivably throw undefined, etc.
480excEither :: IO (Either RelationalError a) -> IO (Either RelationalError a)
481excEither = handle handler
483 handler exc | Just (_ :: AsyncException) <- fromException exc = throwIO exc
484 | otherwise = pure (Left (UnhandledExceptionError (show exc)))
486runProcessResult :: LocalNode -> Process a -> IO a
487runProcessResult localNode proc = do
489 runProcess localNode $ do
491 liftIO $ putMVar ret val
494safeLogin :: Login -> ProcessId -> Process Bool
495safeLogin login procId = do
496 ret <- call procId login
498 Left (_ :: ServerError) -> pure False
499 Right val -> pure val
501remoteCall :: (Serializable a, Serializable b) => Connection -> a -> IO b
502remoteCall (InProcessConnection _ ) _ = error "remoteCall called on local connection"
503remoteCall (RemoteProcessConnection conf) arg = runProcessResult localNode $ do
504 ret <- safeCall serverProcessId arg
506 Left err -> error ("server died: " ++ show err)
507 Right ret' -> case ret' of
508 Left RequestTimeoutError -> liftIO (throwIO RequestTimeoutException)
509 Left (ProcessDiedError _) -> liftIO (throwIO RemoteProcessDiedException)
510 Right val -> pure val
512 localNode = rLocalNode conf
513 serverProcessId = rProcessId conf
515sessionForSessionId :: SessionId -> Sessions -> STM (Either RelationalError Session)
516sessionForSessionId sessionId sessions =
517 maybe (Left $ NoSuchSessionError sessionId) Right <$> StmMap.lookup sessionId sessions
519schemaForSessionId :: Session -> STM (Either RelationalError Schema)
520schemaForSessionId session = do
521 let sname = schemaName session
522 if sname == defaultSchemaName then
523 pure (Right (Schema [])) -- the main schema includes no transformations (but neither do empty schemas :/ )
525 case M.lookup sname (subschemas session) of
526 Nothing -> pure (Left (SubschemaNameNotInUseError sname))
527 Just schema -> pure (Right schema)
529sessionAndSchema :: SessionId -> Sessions -> STM (Either RelationalError (Session, Schema))
530sessionAndSchema sessionId sessions = do
531 eSession <- sessionForSessionId sessionId sessions
533 Left err -> pure (Left err)
535 eSchema <- schemaForSessionId session
537 Left err -> pure (Left err)
538 Right schema -> pure (Right (session, schema))
540-- | Returns the name of the currently selected isomorphic schema.
541currentSchemaName :: SessionId -> Connection -> IO (Either RelationalError SchemaName)
542currentSchemaName sessionId (InProcessConnection conf) = atomically $ do
543 let sessions = ipSessions conf
544 eSession <- sessionForSessionId sessionId sessions
546 Left err -> pure (Left err)
547 Right session -> pure (Right (Sess.schemaName session))
548currentSchemaName sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveCurrentSchemaName sessionId)
550-- | Switch to the named isomorphic schema.
551setCurrentSchemaName :: SessionId -> Connection -> SchemaName -> IO (Either RelationalError ())
552setCurrentSchemaName sessionId (InProcessConnection conf) sname = atomically $ do
553 let sessions = ipSessions conf
554 eSession <- sessionForSessionId sessionId sessions
556 Left err -> pure (Left err)
557 Right session -> case Sess.setSchemaName sname session of
558 Left err -> pure (Left err)
559 Right newSession -> StmMap.insert newSession sessionId sessions >> pure (Right ())
560setCurrentSchemaName sessionId conn@(RemoteProcessConnection _) sname = remoteCall conn (ExecuteSetCurrentSchema sessionId sname)
562-- | Execute a relational expression in the context of the session and connection. Relational expressions are queries and therefore cannot alter the database.
563executeRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)
564executeRelationalExpr sessionId (InProcessConnection conf) expr = excEither $ atomically $ do
565 let sessions = ipSessions conf
566 eSession <- sessionAndSchema sessionId sessions
568 Left err -> pure $ Left err
569 Right (session, schema) -> do
570 let expr' = if schemaName session /= defaultSchemaName then
571 Schema.processRelationalExprInSchema schema expr
575 Left err -> pure (Left err)
576 Right expr'' -> case optimizeRelationalExpr (Sess.concreteDatabaseContext session) expr'' of
577 Left err -> pure (Left err)
579 let evald = runReader (RE.evalRelationalExpr optExpr) (RE.mkRelationalExprState (Sess.concreteDatabaseContext session))
581 Left err -> pure (Left err)
582 Right rel -> pure (force (Right rel)) -- this is necessary so that any undefined/error exceptions are spit out here
583executeRelationalExpr sessionId conn@(RemoteProcessConnection _) relExpr = remoteCall conn (ExecuteRelationalExpr sessionId relExpr)
585-- | Execute a database context expression in the context of the session and connection. Database expressions modify the current session's disconnected transaction but cannot modify the transaction graph.
586executeDatabaseContextExpr :: SessionId -> Connection -> DatabaseContextExpr -> IO (Either RelationalError ())
587executeDatabaseContextExpr sessionId (InProcessConnection conf) expr = excEither $ atomically $ do
588 let sessions = ipSessions conf
589 eSession <- sessionAndSchema sessionId sessions
591 Left err -> pure (Left err)
592 Right (session, schema) -> do
593 let expr' = if schemaName session == defaultSchemaName then
596 Schema.processDatabaseContextExprInSchema schema expr
598 Left err -> pure (Left err)
599 Right expr'' -> case runState (do
600 eOptExpr <- applyStaticDatabaseOptimization expr''
602 Left err -> pure (Left err)
604 RE.evalDatabaseContextExpr optExpr) (RE.freshDatabaseState (Sess.concreteDatabaseContext session)) of
605 (Left err,_) -> pure (Left err)
606 (Right (), (_,_,False)) -> pure (Right ()) --optimization- if nothing was dirtied, nothing to do
607 (Right (), (!context',_,True)) -> do
608 let newDiscon = DisconnectedTransaction (Sess.parentId session) newSchemas True
609 newSubschemas = Schema.processDatabaseContextExprSchemasUpdate (Sess.subschemas session) expr
610 newSchemas = Schemas context' newSubschemas
611 newSession = Session newDiscon (Sess.schemaName session)
612 StmMap.insert newSession sessionId sessions
614executeDatabaseContextExpr sessionId conn@(RemoteProcessConnection _) dbExpr = remoteCall conn (ExecuteDatabaseContextExpr sessionId dbExpr)
616-- | Similar to a git rebase, 'autoMergeToHead' atomically creates a temporary branch and merges it to the latest commit of the branch referred to by the 'HeadName' and commits the merge. This is useful to reduce incidents of 'TransactionIsNotAHeadError's but at the risk of merge errors (thus making it similar to rebasing). Alternatively, as an optimization, if a simple commit is possible (meaning that the head has not changed), then a fast-forward commit takes place instead.
617autoMergeToHead :: SessionId -> Connection -> MergeStrategy -> HeadName -> IO (Either RelationalError ())
618autoMergeToHead sessionId (InProcessConnection conf) strat headName' = do
619 let sessions = ipSessions conf
623 stamp <- getCurrentTime
624 commitLock_ sessionId conf $ \graph -> do
625 eSession <- sessionForSessionId sessionId sessions
627 Left err -> pure (Left err)
629 case Graph.transactionForHead headName' graph of
630 Nothing -> pure (Left (NoSuchHeadNameError headName'))
632 --attempt fast-forward commit, if possible
633 let graphInfo = if Sess.parentId session == transactionId headTrans then do
634 ret <- Graph.evalGraphOp stamp id1 (Sess.disconnectedTransaction session) graph Commit
637 ret <- Graph.autoMergeToHead stamp (id1, id2, id3) (Sess.disconnectedTransaction session) headName' strat graph
638 pure (ret, [id1,id2,id3])
640 Left err -> pure (Left err)
641 Right ((discon', graph'), transactionIdsAdded) ->
642 pure (Right (discon', graph', transactionIdsAdded))
643autoMergeToHead sessionId conn@(RemoteProcessConnection _) strat headName' = remoteCall conn (ExecuteAutoMergeToHead sessionId strat headName')
645-- | Execute a database context IO-monad-based expression for the given session and connection. `DatabaseContextIOExpr`s modify the DatabaseContext but cannot be purely implemented.
646--this is almost completely identical to executeDatabaseContextExpr above
647executeDatabaseContextIOExpr :: SessionId -> Connection -> DatabaseContextIOExpr -> IO (Either RelationalError ())
648executeDatabaseContextIOExpr sessionId (InProcessConnection conf) expr = excEither $ do
649 let sessions = ipSessions conf
650 scriptSession = ipScriptSession conf
651 eSession <- atomically $ sessionForSessionId sessionId sessions --potentially race condition due to interleaved IO?
653 Left err -> pure (Left err)
655 res <- RE.evalDatabaseContextIOExpr scriptSession (Sess.concreteDatabaseContext session) expr
657 Left err -> pure (Left err)
659 let newDiscon = DisconnectedTransaction (Sess.parentId session) newSchemas True
660 newSchemas = Schemas context' (Sess.subschemas session)
661 newSession = Session newDiscon (Sess.schemaName session)
662 atomically $ StmMap.insert newSession sessionId sessions
664executeDatabaseContextIOExpr sessionId conn@(RemoteProcessConnection _) dbExpr = remoteCall conn (ExecuteDatabaseContextIOExpr sessionId dbExpr)
667executeGraphExprSTM_ :: TransactionId -> SessionId -> Session -> Sessions -> TransactionGraphOperator -> TransactionGraph -> TVar TransactionGraph -> STM (Either RelationalError (TransactionGraph, DisconnectedTransaction)
668executeGraphExprSTM_ freshId sessionId session sessions graphExpr graph graphTVar= do
669 case evalGraphOp freshId (Sess.disconnectedTransaction session) graph graphExpr of
671 when updateGraphOnError (writeTVar graphTVar graph)
673 Right (discon', graph') -> do
674 writeTVar graphTVar graph'
675 let newSession = Session discon' (Sess.schemaName session)
676 STMMap.insert newSession sessionId sessions
680-- process notifications for commits
681executeCommitExprSTM_ :: DatabaseContext -> DatabaseContext -> ClientNodes -> STM (EvaluatedNotifications, ClientNodes)
682executeCommitExprSTM_ oldContext newContext nodes = do
683 let nots = notifications oldContext
684 fireNots = notificationChanges nots oldContext newContext
685 evaldNots = M.map mkEvaldNot fireNots
686 evalInContext expr ctx = runReader (RE.evalRelationalExpr expr) (RE.mkRelationalExprState ctx)
687 mkEvaldNot notif = EvaluatedNotification { notification = notif,
688 reportOldRelation = evalInContext (reportOldExpr notif) oldContext,
689 reportNewRelation = evalInContext (reportNewExpr notif) newContext}
690 pure (evaldNots, nodes)
692-- | Execute a transaction graph expression in the context of the session and connection. Transaction graph operators modify the transaction graph state.
694-- OPTIMIZATION OPPORTUNITY: no locks are required to write new transaction data, only to update the transaction graph id file
695-- if writing data is re-entrant, we may be able to use unsafeIOtoSTM
696-- perhaps keep hash of data file instead of checking if our head was updated on every write
697executeGraphExpr :: SessionId -> Connection -> TransactionGraphOperator -> IO (Either RelationalError ())
698executeGraphExpr sessionId (InProcessConnection conf) graphExpr = excEither $ do
699 let sessions = ipSessions conf
700 freshId <- nextRandom
701 stamp <- getCurrentTime
702 commitLock_ sessionId conf $ \updatedGraph -> do
703 eSession <- sessionForSessionId sessionId sessions
705 Left err -> pure (Left err)
707 let discon = Sess.disconnectedTransaction session
708 case evalGraphOp stamp freshId discon updatedGraph graphExpr of
709 Left err -> pure (Left err)
710 Right (discon', graph') -> do
711 --if freshId appears in the graph, then we need to pass it on
712 let transIds = [freshId | isRight (transactionForId freshId graph')]
713 pure (Right (discon', graph', transIds))
715executeGraphExpr sessionId (InProcessConnection conf) graphExpr = excEither $ do
716 let strat = ipPersistenceStrategy conf
717 clientNodes = ipClientNodes conf
718 sessions = ipSessions conf
719 graphTvar = ipTransactionGraph conf
720 mLockFileH = ipLocks conf
721 lockHandler body = case graphExpr of
722 Commit -> case mLockFileH of
723 Nothing -> body False
724 Just (lockFileH, lockMVar) -> let acquireLocks = do
725 lastWrittenDigest <- takeMVar lockMVar
726 lockFile lockFileH WriteLock
727 latestDigest <- readGraphTransactionIdDigest strat
728 pure (latestDigest /= lastWrittenDigest)
731 --still holding the lock- get the latest digest
732 gDigest <- readGraphTransactionIdDigest strat
734 putMVar lockMVar gDigest
735 in bracket acquireLocks releaseLocks body
737 freshId <- nextRandom
738 lockHandler $ \dbWrittenByOtherProcess -> do
739 --if the database file has been updated since we wrote it last, load it before trying to sync our version done- this can result in TransactionNotAHeadErrors
740 --read transaction data and compare to existing graph
741 --in the future, we can detect if updated transaction graph can be safely merged (such as with a transaction on a separate head) (rebase-able commits should force the user to rebase from the client to confirm that the action makes sense)
742 manip <- atomically $ do
743 eSession <- sessionForSessionId sessionId sessions
744 --handle graph update by other process
745 oldGraph <- readTVar graphTvar
747 Left err -> pure (Left err)
749 let mScriptSession = ipScriptSession conf
750 dbdir = case strat of
751 MinimalPersistence x -> x
752 CrashSafePersistence x -> x
753 _ -> error "accessing dbdir on non-persisted connection"
754 eRefreshedGraph <- if dbWrittenByOtherProcess then
755 unsafeIOToSTM (transactionGraphLoad dbdir oldGraph mScriptSession)
757 pure (Right oldGraph)
758 case eRefreshedGraph of
759 Left err -> pure (Left (DatabaseLoadError err))
760 Right refreshedGraph -> do
762 eGraph <- executeGraphExprSTM_ dbWrittenByOtherProcess freshId sessionId session sessions graphExpr refreshedGraph graphTvar
765 Left err -> pure (Left err)
768 if isCommit graphExpr then do
769 case transactionForId (Sess.parentId session) oldGraph of
770 Left err -> pure $ Left err
771 Right previousTrans -> do
772 (evaldNots, nodes) <- executeCommitExprSTM_ (Trans.concreteDatabaseContext previousTrans) (Sess.concreteDatabaseContext session) clientNodes
773 nodesToNotify <- toList (StmSet.stream nodes)
774 pure $ Right (evaldNots, nodesToNotify, newGraph)
776 pure $ Right (M.empty, [], newGraph)
778 Left err -> return $ Just err
779 Right (notsToFire, nodesToNotify, newGraph) -> do
780 --update filesystem database, if necessary
781 processTransactionGraphPersistence strat newGraph
782 sendNotifications nodesToNotify (ipLocalNode conf) notsToFire
785executeGraphExpr sessionId conn@(RemoteProcessConnection _) graphExpr = remoteCall conn (ExecuteGraphExpr sessionId graphExpr)
787-- | A trans-graph expression is a relational query executed against the entirety of a transaction graph.
788executeTransGraphRelationalExpr :: SessionId -> Connection -> TransGraphRelationalExpr -> IO (Either RelationalError Relation)
789executeTransGraphRelationalExpr _ (InProcessConnection conf) tgraphExpr = excEither . atomically $ do
790 let graphTvar = ipTransactionGraph conf
791 graph <- readTVar graphTvar
792 case evalTransGraphRelationalExpr tgraphExpr graph of
793 Left err -> pure (Left err)
794 Right relExpr -> case runReader (RE.evalRelationalExpr relExpr) (RE.mkRelationalExprState DBC.empty) of
795 Left err -> pure (Left err)
796 Right rel -> pure (force (Right rel))
797executeTransGraphRelationalExpr sessionId conn@(RemoteProcessConnection _) tgraphExpr = remoteCall conn (ExecuteTransGraphRelationalExpr sessionId tgraphExpr)
799-- | Schema expressions manipulate the isomorphic schemas for the current 'DatabaseContext'.
800executeSchemaExpr :: SessionId -> Connection -> Schema.SchemaExpr -> IO (Either RelationalError ())
801executeSchemaExpr sessionId (InProcessConnection conf) schemaExpr = atomically $ do
802 let sessions = ipSessions conf
803 eSession <- sessionAndSchema sessionId sessions
805 Left err -> pure (Left err)
806 Right (session, _) -> do
807 let subschemas' = subschemas session
808 case Schema.evalSchemaExpr schemaExpr (Sess.concreteDatabaseContext session) subschemas' of
809 Left err -> pure (Left err)
810 Right (newSubschemas, newContext) -> do
811 --hm- maybe we should start using lenses
812 let discon = Sess.disconnectedTransaction session
813 newSchemas = Schemas newContext newSubschemas
814 newSession = Session (DisconnectedTransaction (Discon.parentId discon) newSchemas True) (Sess.schemaName session)
815 StmMap.insert newSession sessionId sessions
817executeSchemaExpr sessionId conn@(RemoteProcessConnection _) schemaExpr = remoteCall conn (ExecuteSchemaExpr sessionId schemaExpr)
819-- | After modifying a 'DatabaseContext', 'commit' the transaction to the transaction graph at the head which the session is referencing. This will also trigger checks for any notifications which need to be propagated.
820commit :: SessionId -> Connection -> IO (Either RelationalError ())
821commit sessionId conn@(InProcessConnection _) = executeGraphExpr sessionId conn Commit
822commit sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteGraphExpr sessionId Commit)
824sendNotifications :: [ProcessId] -> LocalNode -> EvaluatedNotifications -> IO ()
825sendNotifications pids localNode nots = mapM_ sendNots pids
827 sendNots remoteClientPid =
828 unless (M.null nots) $ runProcess localNode $ send remoteClientPid (NotificationMessage nots)
830-- | Discard any changes made in the current 'Session' and 'DatabaseContext'. This resets the disconnected transaction to reference the original database context of the parent transaction and is a very cheap operation.
831rollback :: SessionId -> Connection -> IO (Either RelationalError ())
832rollback sessionId conn@(InProcessConnection _) = executeGraphExpr sessionId conn Rollback
833rollback sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteGraphExpr sessionId Rollback)
835-- | Write the transaction graph to disk. This function can be used to incrementally write new transactions to disk.
836processTransactionGraphPersistence :: PersistenceStrategy -> [TransactionId] -> TransactionGraph -> IO ()
837processTransactionGraphPersistence NoPersistence _ _ = pure ()
838processTransactionGraphPersistence (MinimalPersistence dbdir) transIds graph = transactionGraphPersist NoDiskSync dbdir transIds graph >> pure ()
839processTransactionGraphPersistence (CrashSafePersistence dbdir) transIds graph = transactionGraphPersist FsyncDiskSync dbdir transIds graph >> pure ()
841readGraphTransactionIdDigest :: PersistenceStrategy -> IO LockFileHash
842readGraphTransactionIdDigest NoPersistence = error "attempt to read digest from transaction log without persistence enabled"
843readGraphTransactionIdDigest (MinimalPersistence dbdir) = readGraphTransactionIdFileDigest dbdir
844readGraphTransactionIdDigest (CrashSafePersistence dbdir) = readGraphTransactionIdFileDigest dbdir
846-- | Return a relation whose type would match that of the relational expression if it were executed. This is useful for checking types and validating a relational expression's types.
847typeForRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)
848typeForRelationalExpr sessionId conn@(InProcessConnection _) relExpr = atomically $ typeForRelationalExprSTM sessionId conn relExpr
849typeForRelationalExpr sessionId conn@(RemoteProcessConnection _) relExpr = remoteCall conn (ExecuteTypeForRelationalExpr sessionId relExpr)
851typeForRelationalExprSTM :: SessionId -> Connection -> RelationalExpr -> STM (Either RelationalError Relation)
852typeForRelationalExprSTM sessionId (InProcessConnection conf) relExpr = do
853 let sessions = ipSessions conf
854 eSession <- sessionAndSchema sessionId sessions
856 Left err -> pure $ Left err
857 Right (session, schema) -> do
858 let processed = if schemaName session == defaultSchemaName then
861 Schema.processRelationalExprInSchema schema relExpr
863 Left err -> pure (Left err)
864 Right relExpr' -> pure $ runReader (RE.typeForRelationalExpr relExpr') (RE.mkRelationalExprState (Sess.concreteDatabaseContext session))
866typeForRelationalExprSTM _ _ _ = error "typeForRelationalExprSTM called on non-local connection"
868-- | Return a 'Map' of the database's constraints at the context of the session and connection.
869inclusionDependencies :: SessionId -> Connection -> IO (Either RelationalError InclusionDependencies)
870inclusionDependencies sessionId (InProcessConnection conf) = do
871 let sessions = ipSessions conf
873 eSession <- sessionAndSchema sessionId sessions
875 Left err -> pure $ Left err
876 Right (session, schema) -> do
877 let context = Sess.concreteDatabaseContext session
878 if schemaName session == defaultSchemaName then
879 pure $ Right (B.inclusionDependencies context)
881 pure (Schema.inclusionDependenciesInSchema schema (B.inclusionDependencies context))
883inclusionDependencies sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveInclusionDependencies sessionId)
885typeConstructorMapping :: SessionId -> Connection -> IO (Either RelationalError TypeConstructorMapping)
886typeConstructorMapping sessionId (InProcessConnection conf) = do
887 let sessions = ipSessions conf
889 eSession <- sessionAndSchema sessionId sessions
891 Left err -> pure $ Left err
892 Right (session, _) -> --warning, no schema support for typeconstructors
893 pure (Right (B.typeConstructorMapping (Sess.concreteDatabaseContext session)))
894typeConstructorMapping sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveTypeConstructorMapping sessionId)
896-- | Return an optimized database expression which is logically equivalent to the input database expression. This function can be used to determine which expression will actually be evaluated.
897planForDatabaseContextExpr :: SessionId -> Connection -> DatabaseContextExpr -> IO (Either RelationalError DatabaseContextExpr)
898planForDatabaseContextExpr sessionId (InProcessConnection conf) dbExpr = do
899 let sessions = ipSessions conf
901 eSession <- sessionAndSchema sessionId sessions
903 Left err -> pure $ Left err
904 Right (session, _) -> if schemaName session == defaultSchemaName then
905 pure $ evalState (applyStaticDatabaseOptimization dbExpr) (RE.freshDatabaseState (Sess.concreteDatabaseContext session))
906 else -- don't show any optimization because the current optimization infrastructure relies on access to the base context- this probably underscores the need for each schema to have its own DatabaseContext, even if it is generated on-the-fly
909planForDatabaseContextExpr sessionId conn@(RemoteProcessConnection _) dbExpr = remoteCall conn (RetrievePlanForDatabaseContextExpr sessionId dbExpr)
911-- | Return a relation which represents the current state of the global transaction graph. The attributes are
912-- * current- boolean attribute representing whether or not the current session references this transaction
913-- * head- text attribute which is a non-empty 'HeadName' iff the transaction references a head.
914-- * id- id attribute of the transaction
915-- * parents- a relation-valued attribute which contains a relation of transaction ids which are parent transaction to the transaction
916transactionGraphAsRelation :: SessionId -> Connection -> IO (Either RelationalError Relation)
917transactionGraphAsRelation sessionId (InProcessConnection conf) = do
918 let sessions = ipSessions conf
919 tvar = ipTransactionGraph conf
921 eSession <- sessionForSessionId sessionId sessions
923 Left err -> pure $ Left err
925 graphAsRelation (Sess.disconnectedTransaction session) <$> readTVar tvar
927transactionGraphAsRelation sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveTransactionGraph sessionId)
929-- | Returns the names and types of the relation variables in the current 'Session'.
930relationVariablesAsRelation :: SessionId -> Connection -> IO (Either RelationalError Relation)
931relationVariablesAsRelation sessionId (InProcessConnection conf) = do
932 let sessions = ipSessions conf
934 eSession <- sessionAndSchema sessionId sessions
936 Left err -> pure (Left err)
937 Right (session, schema) -> do
938 let context = Sess.concreteDatabaseContext session
939 if Sess.schemaName session == defaultSchemaName then
940 pure $ R.relationVariablesAsRelation (relationVariables context)
942 case Schema.relationVariablesInSchema schema context of
943 Left err -> pure (Left err)
944 Right relvars -> pure $ R.relationVariablesAsRelation relvars
946relationVariablesAsRelation sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveRelationVariableSummary sessionId)
948-- | Returns the names and types of the atom functions in the current 'Session'.
949atomFunctionsAsRelation :: SessionId -> Connection -> IO (Either RelationalError Relation)
950atomFunctionsAsRelation sessionId (InProcessConnection conf) = do
951 let sessions = ipSessions conf
953 eSession <- sessionAndSchema sessionId sessions
955 Left err -> pure (Left err)
956 Right (session, _) ->
957 pure (AF.atomFunctionsAsRelation (atomFunctions (concreteDatabaseContext session)))
959atomFunctionsAsRelation sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveAtomFunctionSummary sessionId)
961databaseContextFunctionsAsRelation :: SessionId -> Connection -> IO (Either RelationalError Relation)
962databaseContextFunctionsAsRelation sessionId (InProcessConnection conf) = do
963 let sessions = ipSessions conf
965 eSession <- sessionAndSchema sessionId sessions
967 Left err -> pure (Left err)
968 Right (session, _) ->
969 pure (DCF.databaseContextFunctionsAsRelation (dbcFunctions (concreteDatabaseContext session)))
971databaseContextFunctionsAsRelation sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveDatabaseContextFunctionSummary sessionId)
973-- | Returns the transaction id for the connection's disconnected transaction committed parent transaction.
974headTransactionId :: SessionId -> Connection -> IO (Either RelationalError TransactionId)
975headTransactionId sessionId (InProcessConnection conf) = do
976 let sessions = ipSessions conf
978 eSession <- sessionForSessionId sessionId sessions
980 Left err -> pure (Left err)
981 Right session -> pure $ Right (Sess.parentId session)
982headTransactionId sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveHeadTransactionId sessionId)
984headNameSTM_ :: SessionId -> Sessions -> TVar TransactionGraph -> STM (Either RelationalError HeadName)
985headNameSTM_ sessionId sessions graphTvar = do
986 graph <- readTVar graphTvar
987 eSession <- sessionForSessionId sessionId sessions
989 Left err -> pure (Left err)
990 Right session -> case transactionForId (Sess.parentId session) graph of
991 Left err -> pure (Left err)
992 Right parentTrans -> case headNameForTransaction parentTrans graph of
993 Nothing -> pure (Left UnknownHeadError)
994 Just headName' -> pure (Right headName')
996-- | Returns Just the name of the head of the current disconnected transaction or Nothing.
997headName :: SessionId -> Connection -> IO (Either RelationalError HeadName)
998headName sessionId (InProcessConnection conf) = do
999 let sessions = ipSessions conf
1000 graphTvar = ipTransactionGraph conf
1001 atomically (headNameSTM_ sessionId sessions graphTvar)
1002headName sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteHeadName sessionId)
1004-- | Returns a listing of all available atom types.
1005atomTypesAsRelation :: SessionId -> Connection -> IO (Either RelationalError Relation)
1006atomTypesAsRelation sessionId (InProcessConnection conf) = do
1007 let sessions = ipSessions conf
1009 eSession <- sessionForSessionId sessionId sessions
1011 Left err -> pure (Left err)
1013 case typesAsRelation (B.typeConstructorMapping (Sess.concreteDatabaseContext session)) of
1014 Left err -> pure (Left err)
1015 Right rel -> pure (Right rel)
1016atomTypesAsRelation sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveAtomTypesAsRelation sessionId)
1018disconnectedTransactionIsDirty :: SessionId -> Connection -> IO (Either RelationalError Bool)
1019disconnectedTransactionIsDirty sessionId (InProcessConnection conf) = do
1020 let sessions = ipSessions conf
1022 eSession <- sessionForSessionId sessionId sessions
1024 Left err -> pure (Left err)
1026 pure (Right (isDirty session))
1027disconnectedTransactionIsDirty sessionId conn@(RemoteProcessConnection _) = remoteCall conn (RetrieveSessionIsDirty sessionId)
1029--used only for testing- we expect this to throw an exception
1030callTestTimeout_ :: SessionId -> Connection -> IO Bool
1031callTestTimeout_ _ (InProcessConnection _) = error "bad testing call"
1032callTestTimeout_ sessionId conn@(RemoteProcessConnection _) = remoteCall conn (TestTimeout sessionId)
1035transactionGraph_ :: Connection -> IO TransactionGraph
1036transactionGraph_ (InProcessConnection conf) = readTVarIO (ipTransactionGraph conf)
1037transactionGraph_ _ = error "remote connection used"
1040disconnectedTransaction_ :: SessionId -> Connection -> IO DisconnectedTransaction
1041disconnectedTransaction_ sessionId (InProcessConnection conf) = do
1042 let sessions = ipSessions conf
1043 mSession <- atomically $ StmMap.lookup sessionId sessions
1045 Nothing -> error "No such session"
1046 Just (Sess.Session discon _) -> pure discon
1047disconnectedTransaction_ _ _= error "remote connection used"
1049-- wrap a graph evaluation in file locking
1050commitLock_ :: SessionId ->
1051 InProcessConnectionConf ->
1052 (TransactionGraph ->
1053 STM (Either RelationalError (DisconnectedTransaction, TransactionGraph, [TransactionId]))) ->
1054 IO (Either RelationalError ())
1055commitLock_ sessionId conf stmBlock = do
1056 let sessions = ipSessions conf
1057 strat = ipPersistenceStrategy conf
1058 mScriptSession = ipScriptSession conf
1059 graphTvar = ipTransactionGraph conf
1060 clientNodes = ipClientNodes conf
1061 mLockFileH = ipLocks conf
1062 lockHandler body = case mLockFileH of
1063 Nothing -> body False
1064 Just (lockFileH, lockMVar) ->
1065 let acquireLocks = do
1066 lastWrittenDigest <- takeMVar lockMVar
1067 lockFile lockFileH WriteLock
1068 latestDigest <- readGraphTransactionIdDigest strat
1069 pure (latestDigest /= lastWrittenDigest)
1071 --still holding the lock- get the latest digest
1072 gDigest <- readGraphTransactionIdDigest strat
1073 unlockFile lockFileH
1074 putMVar lockMVar gDigest
1075 in bracket acquireLocks releaseLocks body
1076 manip <- lockHandler $ \dbWrittenByOtherProcess -> atomically $ do
1077 eSession <- sessionForSessionId sessionId sessions
1078 --handle graph update by other process
1079 oldGraph <- readTVar graphTvar
1081 Left err -> pure (Left err)
1083 let dbdir = case strat of
1084 MinimalPersistence x -> x
1085 CrashSafePersistence x -> x
1086 _ -> error "accessing dbdir on non-persisted connection"
1087 --this should also happen for non-commit expressions
1088 eRefreshedGraph <- if dbWrittenByOtherProcess then
1089 unsafeIOToSTM (transactionGraphLoad dbdir oldGraph mScriptSession)
1091 pure (Right oldGraph)
1092 case eRefreshedGraph of
1093 Left err -> pure (Left (DatabaseLoadError err))
1094 Right refreshedGraph -> do
1095 eGraph <- stmBlock refreshedGraph
1097 Left err -> pure (Left err)
1098 Right (discon', graph', transactionIdsToPersist) -> do
1099 writeTVar graphTvar graph'
1100 let newSession = Session discon' (Sess.schemaName session)
1101 StmMap.insert newSession sessionId sessions
1102 case transactionForId (Sess.parentId session) oldGraph of
1103 Left err -> pure $ Left err
1104 Right previousTrans ->
1105 if not (Prelude.null transactionIdsToPersist) then do
1106 (evaldNots, nodes) <- executeCommitExprSTM_ (Trans.concreteDatabaseContext previousTrans) (Sess.concreteDatabaseContext session) clientNodes
1107 nodesToNotify <- stmSetToList nodes
1108 pure $ Right (evaldNots, nodesToNotify, graph', transactionIdsToPersist)
1109 else pure (Right (M.empty, [], graph', []))
1111 --handle notification firing
1113 Left err -> pure (Left err)
1114 Right (notsToFire, nodesToNotify, newGraph, transactionIdsToPersist) -> do
1115 --update filesystem database, if necessary
1116 processTransactionGraphPersistence strat transactionIdsToPersist newGraph
1117 sendNotifications nodesToNotify (ipLocalNode conf) notsToFire
1120writeDisconAndGraph_ :: TVar TransactionGraph -> SessionId -> Session -> Sessions -> DisconnectedTransaction -> TransactionGraph -> STM ()
1121writeDisconAndGraph_ graphTvar sessionId session sessions discon graph = do
1122 writeTVar graphTvar graph
1123 let newSession = Session discon (Sess.schemaName session)
1124 StmMap.insert newSession sessionId sessions
1127-- | Runs an IO monad, commits the result when the monad returns no errors, otherwise, rolls back the changes and the error.
1128withTransaction :: SessionId -> Connection -> IO (Either RelationalError a) -> IO (Either RelationalError ()) -> IO (Either RelationalError a)
1129withTransaction sessionId conn io successFunc = bracketOnError (pure ()) (const do_rollback) block
1131 do_rollback = rollback sessionId conn
1139 eIsDirty <- disconnectedTransactionIsDirty sessionId conn
1141 Left err -> pure (Left err)
1146 Left err -> pure (Left err)
1147 Right _ -> pure (Right val)
1148 else -- no updates executed, so don't create a commit
1151executeDataFrameExpr :: SessionId -> Connection -> DF.DataFrameExpr -> IO (Either RelationalError DF.DataFrame)
1152executeDataFrameExpr sessionId conn@(InProcessConnection _) dfExpr = do
1153 eRel <- executeRelationalExpr sessionId conn (DF.convertExpr dfExpr)
1155 Left err -> pure (Left err)
1157 let relAttrs = R.attributes rel
1158 attrName (DF.AttributeOrderExpr name _) = name
1159 order (DF.AttributeOrderExpr _ ord) = ord
1160 orders = map order (DF.orderExprs dfExpr)
1161 attributeForName' = flip attributeForName relAttrs
1162 attrNames = map attrName (DF.orderExprs dfExpr)
1163 verified = forM attrNames attributeForName'
1165 Left err -> pure (Left err)
1167 let attrOrders = zipWith
1169 -> DF.AttributeOrder (attributeName attr') order')
1172 case DF.sortDataFrameBy attrOrders . DF.toDataFrame $ rel of
1173 Left err -> pure (Left err)
1175 let dFrame' = maybe dFrame (`DF.drop'` dFrame) (DF.offset dfExpr)
1176 dFrame'' = maybe dFrame' (`DF.take'` dFrame') (DF.limit dfExpr)
1177 pure (Right dFrame'')
1178executeDataFrameExpr sessionId conn@(RemoteProcessConnection _) dfExpr = remoteCall conn (ExecuteDataFrameExpr sessionId dfExpr)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list