Dataflow Patterns
Create
The create
pattern creates a new instance of a record or an entity. The syntax of a create pattern is,
{:RecordName
{:Attr1 value1
:Attr2 value2
...
:AttrN valueN}
:-> [relationships]
:as alias}
The values assigned to attributes could be a,
- literal string, number, boolean, map
- list of any of (1)
- path to a local binding (an instance or an attribute)
- function call expression
A function call expression takes the form of a quoted-list, as in
'(fn-name arg1 arg2 ... argN)
The arguments could be,
- a literal or,
- a list of literals or,
- a path to a local binding (an instance, an attribute or an alias)
Example
(dataflow :Acme.Inventory.Sales/PlaceOrder
{:Acme.Inventory.Sales/Order
{:Product :Acme.Inventory.Sales/PlaceOrder.Product
:Date '(fractl.lang.datetime/now)
:Customer :Acme.Inventory.Sales/PlaceOrder.Customer
:TaxRate 0.1
:TotalPrice '(compute-price :Acme.Inventory.Sales/PlaceOrder.ItemPrice :TaxRate)}})
(defn compute-price [item-price sales-tax-rate]
(+ item-price (* item-price sales-tax-rate)))
The dataflow attached to the :Acme.Inventory.Sales/PlaceOrder
event has a single create-pattern
for creating a new instance of the entity :Acme.Inventory.Sales/Order
.
Notice the arguments passed to the compute-price
function call. The first path is a fully-qualified
reference to the :ItemPrice
attribute of the :PlaceOrder
event. The next argument is a reference
to a local attribute of :Order
, so it need not be fully-qualified.
This dataflow can be triggered by a pattern that creates an instance of the :Acme.Inventory.Sales/PlaceOrder
event,
{:Acme.Inventory.Sales/PlaceOrder
{:Product "p001"
:Customer "ABC"
:ItemPrice 789.22}}
If a relationship is created by the optional :->
key, it must have the following syntax:
[[relationship-create-pattern other-node] ...]
relationship-create-pattern
is pattern that creates a new instance of the relationship.
For a :contains
relationship this will simply be the name of the relationship. For a :between
,
this will a complete pattern of the new relationship instance.
other-node
must be one of,
- a query pattern
- a create or update pattern
- a path or alias to a local instance
Example
{:Department
{:No? "101"} :as [:D]}
{:Employee
{:EmpNo "001"
:Name "KK"}
:-> [[:WorksFor :D]]}
Quote and Unquote
The quote
pattern - [:q# ...]
- can be used to turn-off evaluation on certain patterns and treat them as pure data.
The unquote
pattern - [:uq# ...]
- turns-off quoting temporarily within a quoted-pattern.
For example, consider the following pattern that assigns an instance of :SomeEvent
to the attribute :ARecord.A
-
(dataflow :ThisEvent
{:ARecord
{:A {:SomeEvent {:X :ThisEvent.X}}}})
When the instance of :SomeEvent
is created, any dataflow attached to this event will be triggered. You want to prevent
this from happening and likes to treat the {:SomeEvent {:X ....}}
pattern as a pure map. This can be achieved by quoting the
pattern as shown below:
(dataflow :ThisEvent
{:ARecord
{:A [:q# {:SomeEvent {:X [:uq# :ThisEvent.X]}}]}})
Note that if :ThisEvent.X
is not unquoted, the resulting map will contain the keyword literal :ThisEvent.X
instead of the
value bound to the :X
attribute of :ThisEvent
.
Query
A query
pattern is also expressed as a map, but is used to lookup existing instances of entities from
the persistent storage. The basic syntax of the query pattern is:
{:EntityName
{:Attr1? query1
:Attr2? query2
...
:Attr3? query3}
:-> [relationship-queries]
:as alias}
The query value (query1, query2 etc) could be either a:
- literal
- function call expression
- logical or comparison expression of the form
[:operator arg1 arg2 ... argN]
The logical operators supported are :and
and :or
. A comparison operation must be one of
:=
, :>
, :<
, :>=
, :<=
and :like
. The comparison operators (except :like
) maybe applied
to both numeric and string values, :like
works with only strings.
Example
;; Find customer by email
{:Customer
{:Email? "abc@acme.com"}}
;; Find employees belonging a department 101
;; and whose salaries are between 2000 and 3500
{:Employee
{:Department? 101
:Salary? [:and [:> 2000] [:< 3500]]}}
;; Find all customer whose first-name starts with `"Sa"`:
{:Customer
{:FirstName? [:like "Sa%"]}}
It's possible to do query and update in a single pattern. For example, the following pattern gives a salary-raise to all employees in department 101:
{:Employee
{:Department? 101
:Salary '(+ :Salary (* 0.2 :Salary))}}
If the query has to happen in the context of a relationship, a relationship-query must be provided via the :->
keyword.
This query must have the form:
[relationship-query-pattern other-node-query-pattern]
An example is shown below:
;; Load all employees whose salary is greater-than 1000 and who
;; are in a `:WorksFor` relationship with the department 101.
{:Employee
{:Salary? [:> 1000]}
:-> [[:WorksFor? {:Department {:No? 101}}]]}
The shorthand-query :EntityName?
will return all instances of an entity.
The shorthand maybe extended to a map that include more complex data-query patterns attached to the
:where
keyword. For example, the following query will return all employees from department 101
ordered by their salary:
{:Employee?
{:where [:= :Department 101]
:order-by [:Salary]}}
Here's another query that will return the number of employees in the department:
{:Employee?
{:where [:= :Department 101]
:count :EmpNo}}
The aggregate operators supported by this form of query are :count
, :sum
, :avg
(average),
:min
, and :max
.
Also see the documentation on Destructuring.
Delete
The delete
expression deletes one or more entity-instances based on a simple lookup criteria.
Example
[:delete :Department {No: "101"} :as :DeletedDepts]
The above expression will delete all employees whose salary is 1500. The deleted instances are returned by the command.
Any child-instances that falls under the deleted entity-instance via a :contains
relationship will also be deleted.
This behavior can be prevented by turning-off the :cascade-on-delete
flag in the relationship.
Example
(relationship :WorksFor
{:meta {:contains [:Department :Employee] :cascade-on-delete false}})
If :cascade-on-delete
is false
, the :Department
can only be deleted after all contained :Employee
s are deleted.
Match
Conditional pattern evaluation is made possible by the :match
expression, which has the syntax,
[:match value
case1 pattern1
case2 pattern2
...
caseN patternN
default-pattern
:as alias]
The match value
must be either a,
- literal
- path to a local record, attribute or alias
- any valid dataflow pattern
- sequence of 1-3 enclosed in
[]
The value
is compared to each of the case
for equality. The pattern
attached to the first
matching case will be evaluated and the result will become the value of the match
expression.
If none of the cases match, the default-pattern
will be evaluated. If the default-pattern
is
not provided, match
will return false
.
Example
[:match :Employee.Email
"abc@acme.com" {:SalaryIncrement {:Percentage 0.5} ...}
"xyz@acme.com" {:SalaryDecrement {:Percentage 0.2} ...}
{:SalaryIncrement {:Percentage 0.3} ...}]
There's a second form or :match
that can act as an if-else
construct in traditional languages.
The syntax is,
[:match
condition1 pattern1
condition2 pattern2
...
conditionN patternN
default-pattern
:as alias]
The condition
should be a logical expression of the form,
[:operator arg1 arg2 ... argN]
The :operator
could be one of :=
, :>
, :<
, :>=
, :<=
, :like
, :between
, :in
.
Comparison expressions could be combined using the logical operators :not
, :and
and :or
.
Example
{:Employee {:EmpNo "001"} :as :E}
[:match
[:< :E.Salary 1000] {:SalaryIncrement {:Percentage 0.6 ...}}
[:between 2000 3000 :E.Salary] {:SalaryIncrement {:Percentage 0.1 ...}}
[:in [2450 1145 2293] :E.Salary] {:SalaryIncrement {:Percentage 0.2 ...}}
[:like :E.Email "abc%"] {:SalaryIncrement {:Percentage 0.3 ...}} ; email starts-with "abc"?
{:SalaryIncrement {:Percentage 0.5 ...}}]
for-each
A sequence of values can be processed using the :for-each
expression. The format of :for-each
is,
[:for-each source-pattern body ... :as alias]
source-pattern
should return a sequence of values - usually this will be a query-pattern or an alias
bound to a query-pattern. body
is one or more patterns evaluated for each element in the source. If the elements
consists of entity or record instances, body
can refer to the current element by the name of the record or
by using the special alias :%
.
Example
;; Give salary increment to all employees
[:for-each :Employee?
{:SalaryIncrement
{:Percentage 0.2
:BaseSalary :Employee.Salary
:EmpNo :Employee.EmpNo}}]
;; using the :% alias
[:for-each :Employee?
{:SalaryIncrement
{:Percentage 0.2
:BaseSalary :%.Salary
:EmpNo :%.EmpNo}}]
The result of the :for-each
will be the value returned by the last pattern evaluated in its body.
try
The :try
expression allows the evaluation of the dataflow to continue even after an error condition.
By default, if any pattern returns a non-success result will terminate the dataflow. :try
provides a way to
handle non-success results and allow the dataflow to take corrective action.
The syntax of :try
is,
[:try
pattern
:result-tag1 handler-pattern1
:result-tag2 handler-pattern2
...
:as alias]
pattern
is evaluated and result-tag
is set to one of,
:ok
- pattern evaluation succeeded:not-found
- a query failed to find result:declined
- the evaluator refused to run the pattern:error
- the evaluation resulted in an error
The handler-pattern
attached to the tag is evaluated to produce the final result.
If the handler for a tag is not specified, the result of pattern
is returned, which
if not an :ok
result, will cause the termination of the dataflow.
Example
[:try
{:Employee {:Empno? "001"}}
:not-found {:Result {:Message "employee not found"}}]
A single handler maybe attached to a single tag as shown below:
[:try
{:Employee {:Empno? "001"}}
[:error :not-found] {:Result {:Message "employee not found or there was an error"}}]
eval
The :eval
expression can be used to invoke a Clojure function and bind the result to an alias.
The syntax of this expression is,
[:eval '(f arg1 arg2 ... argN) :check type-or-predicate :as alias]
The arguments passed to the function should be one of,
- a literal or a sequence of literals
- a path to a local instance, attribute or alias
type-or-predicate
must be either the name of a Fractl type or a single-argument function.
If it's a type-name, the value returned by f
should be an instance of the same type. If it's
a function, it should return true
for the value returned by f
. Otherwise, :eval
will terminate the
dataflow, by returning an error.
Example
[:eval '(compute-tax :Employee.Salary) :check double? :as :Tax]
[:eval '(update-employee-salary :Employee) :check :Employee :as :UpdatedEmp]
Note that both :check
and :as
are optional and the function may be invoked just for its side-effects.