on 52182bc help sign in
project-m36/docs/database_context_functions.markdown
167 lines 8.8 kB history raw
1# Project:M36 Database Context Functions
3## Introduction
5While "atom functions" operate on individual values and return new values (atoms), database context functions operate on pre-committed database contexts in the same context as the ```update```, ```insert```, and ```delete``` operators. The difference is that database context functions are written in Haskell and support any number of context changes.
7Other DBMS software may include a similar feature called "stored procedures". However, "stored procedures" are stored outside the transaction stream and are not versioned by the database as in Project:M36.
9In order to use compile functions at runtime in Project:M36, be sure to build your own Project:M36 library so that the optional scripting is enabled. Alternatively, precompiled database context functions can be loaded from object files.
11## Database Context Function Types
13The Haskell type of all database context functions is:
15```haskell
16[Atom] -> DatabaseContext -> Either DatabaseContextFunctionError DatabaseContext
17```
19That is, the function takes an array of ```Atom```s which represent the function's arguments and a context and returns an updated context or an error. Notice that this operation is idempotent ("pure" in Haskell parlance).
21The database context references all data which can be committed; this includes relation variables, atom functions, notifications, database context functions, and other versionable data. Database contexts become immutable once committed.
23## Scope of Database Context Functions
25Database context functions are committed alongside the transaction. Thus, previous versions of the functions can be explored or restored. Like all database context elements, database context functions are immutable once committed. However, the transaction graph can be branched in order to examine how database context functions changes might apply to future contexts.
27Remember that database context functions don't need to worry about locks or other synchronization. The database context is completely isolated from other ongoing transactions.
29Returning a ```DatabaseContextFunctionError``` from the function ensures that any changes made by the function are discarded.
31## TutorialD Demonstration
33Use ```adddatabasecontextfunction``` to compile and name a new database context function:
35```
36adddatabasecontextfunction "addperson" Int -> Text -> DatabaseContext -> Either DatabaseContextFunctionError DatabaseContext """(\(age:name:_) ctx -> let newrel = MakeRelationFromExprs Nothing [TupleExpr (fromList [("name", NakedAtomExpr name),("age", NakedAtomExpr age)])] in if isRight (executeRelationalExpr (RelationVariable "person" ()) ctx) then executeDatabaseContextExpr (Insert "person" newrel) ctx else executeDatabaseContextExpr (Assign "person" newrel) ctx) :: [Atom] -> DatabaseContext -> Either DatabaseContextFunctionError DatabaseContext"""
37```
39This function is quite dense, so let's examine its components.
41The first components are:
43```
44adddatabasecontextfunction "addperson" Int -> Text -> DatabaseContext -> Either DatabaseContextFunctionError DatabaseContext
45```
47which defines a function "addperson" which takes three arguments (an Int value, a Text value, and a DatabaseContext) and returns a DatabaseContext. All database context functions take as a final argument a database context and must return a database context.
49The meat of the function is obviously the Haskell, so let's lay it out:
51```haskell
52(\(age:name:_) ctx -> let newrel = MakeRelationFromExprs Nothing [TupleExpr (fromList [("name", NakedAtomExpr name),("age", NakedAtomExpr age)])] in
53 if isRight (executeRelationalExpr (RelationVariable "person" ()) ctx) then
54 executeDatabaseContextExpr (Insert "person" newrel) ctx
55 else
56 executeDatabaseContextExpr (Assign "person" newrel) ctx)) :: [Atom] -> DatabaseContext -> Either DatabaseContextFunctionError DatabaseContext
57```
59Line 1 sets up a new relation created from the function's arguments. In this case, we create a relation to represent a new person in our database.
61Line 2 determines if the "person" relation variable is already available in the context. If so, the function inserts the new "person" relation into the existing relation variable in line 3.
63In line 5, since the relation variable was determined not yet to be defined, we assign the new person relation directly.
65Note that there is nothing special about the Haskell above- it is indeed a full-blown Haskell environment; however, this environment operates in "Safe" mode which prohibits any non-pure function access (such as with ```unsafePerformIO```). Thus, it is safe to use as a trusted programming environment with users creating arbitrary convenience functions within the implicit sandbox.
67Users can add arbitrary (but safe and pure) calculations to these functions to capture business logic. The list of modules available within the function context can be modified at compile-time.
69Let's try the new function!
71```
72TutorialD (master/main): execute addperson(30, "Steve")
73TutorialD (master/main): :showexpr person
74┌────────┬──────────┐
75│age::Int│name::Text│
76├────────┼──────────┤
77│30 │"Steve" │
78└────────┴──────────┘
79TutorialD (master/main): execute addperson(32, "Bob")
80TutorialD (master/main): :showexpr person
81┌────────┬──────────┐
82│age::Int│name::Text│
83├────────┼──────────┤
84│32 │"Bob" │
85│30 │"Steve" │
86└────────┴──────────┘
87TutorialD (master/main):
88```
90Here we see that the first call to ```addperson``` created the relation variable while a subsequent call added another tuple to the existing relation variable.
92## Haskell access
94Database context functions come in two varieties:
96* scripted (as above)
97* pre-compiled
99If you are dealing with the Haskell interface directly and you don't anticipate changing the functions, the easiest option is to compile your Haskell database context function into the database. It can always be removed or supplanted by a scripted version later.
101To add a new, compiled database context function to the database, use:
103```haskell
104executeDatabaseContextIOExpr sessionId dbconn (AddDatabaseContextFunction funcName Nothing (\(arg1:arg2:_) ctx -> ...))
105```
106from the ```ProjectM36.Client``` module.
108The function can be executed using:
110```haskell
111executeDatabaseContextExpr sessionId dbconn (ExecuteDatabaseContextFunction funcName atomArgList)
112```
113from the same module.
115## Loading Precompiled Modules
117Database context functions compiled with GHC to object files can be loaded at runtime. Let's walk through an example:
119```haskell
120module DynamicDatabaseContextFunctions where
121import ProjectM36.Base
122import ProjectM36.Relation
123import ProjectM36.DatabaseContextFunctionError
124import qualified ProjectM36.Attribute as A
126import qualified Data.Map as M
129someDBCFunctions :: [DatabaseContextFunction]
130someDBCFunctions = [DatabaseContextFunction {
131 dbcFuncName = "addtestrel",
132 dbcFuncType = [],
133 dbcFuncBody = DatabaseContextFunctionBody Nothing addTestRel
134 }]
135 where
136 addTestRel _ ctx = do
137 let attrs = A.attributesFromList [Attribute "word" TextAtomType]
138 eRel = mkRelationFromList attrs [[TextAtom "nice"]]
139 case eRel of
140 Left err -> Left (DatabaseContextFunctionUserError (show err))
141 Right testRel ->
142 pure $ ctx { relationVariables = M.insert "testRel" testRel (relationVariables ctx) }
143```
145The function returns a list of `DatabaseContextFunction`s (just one in this case). The function adds a relation variable to the current database context.
147Compile it with GHC:
149```
150cabal exec ghc -- examples/DynamicDatabaseContextFunctions.hs -package project-m36
151```
153or invoked GHC via stack.
155Finally, connect to your database with `tutd` and load the module:
157```
158TutorialD (master/main): loaddatabasecontextfunctions "DynamicDatabaseContextFunctions" "someDBCFunctions" "examples/DynamicDatabaseContextFunctions.o"
159```
161If the load reports an "unknown symbol" error, double check that the installed "project-m36" package is identical to one used to link the Haskell module.
163Note that such functions do not survive a server restart; this will likely be improved in a future release.
165## Future Improvements
167There are likely a variety of convenience functions which would make sense in the context of the scripted function. If you have ideas for such functions, please open a GitHub issue.