TinyTable Expressions example

TinyTable is a SPARKL configuration that acts like a simple database management system. You can use TinyTable to create, lookup and delete records in an Erlang map.

Figure: TinyTable Expressions - The mix


The mix comprises:
  • Four solicit/response operations implemented by the Sequencer service
  • Four request/reply operations implemented by an svc_expr service

As each solicit can trigger a new transaction, four different transactions are possible in this mix.

Each solicit operation is paired up with a request operation.

Each pair forms an island. That is, the vertices generated by the solicit/response operations are connected only with the vertices created by the corresponding request/reply operations.

There are no edges between the islands.

The solicits are used by SPARKL to kick off the transactions.

The requests use expressions to create, delete, list or find records in the database.

Tip: Get the code from the SPARKL public repository.
Table 1. TinyTable Expressions - Markup
Example Description
<folder name="TinyTable_expr">
  <field name="INSERT"/>
  <field name="first_name" 
    type="string"/>
  <field name="key" 
    type="integer"/>
  <field name="GET"/>
  <field name="error" 
    type="string"/>
  <field name="records" 
    type="term"/>
  <field name="DELETE"/>
  <field name="last_name" 
    type="string"/>
  <field name="LIST"/>
  <field name="OK"/>
  <service name="Database"
    provision="expr">
  ... 
  </service>
  <service name="Sequencer"
    provision="sequencer"/>
  <mix name="Mix">
    <folder name="Client">
      ...
    </folder>
    <folder name="Server">
    ...
    </folder>
  </mix>
</folder>
Besides the operations, this example comprises:
  • 10 fields sent between the operations
  • Database, a service provisioned using the Expressions extension
  • Two folders containing the operations

The fields are of different types:

  • FLAGs, used for representing intentions. For example, inserting or deleting a record.
  • Strings, that can carry strings. For example, names.
  • Integers, that can carry integers. For example, a unique ID.
  • Terms, that can carry Erlang terms. For example, lists[...], or maps{...}.

The folders group the operations:

  • Client contains the solicits, which a client service can fire to start transactions.
  • Server contains the requests, which send the fields needed to satisfy the responses.
<service name="Database" 
  provision="expr">
  <prop name="expr.state" 
    Map="NewMap" 
    NextKey="NewNextKey"/>
  <prop name="expr.init" 
    NextKey="2"/>
  <prop name="expr.init.Map" 
    content-type="text/x-erlang"><![CDATA[
#{
  0 => #{
    first_name => "Bill",
    last_name => "Door"},

  1 => #{
    first_name => "King",
    last_name => "Kong"}
}
  ]]></prop>
  <prop name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Error = not_found,
ExistFun =  
  fun(Value,Map) ->   
    maps:is_key(Value,Map)  
  end.
  ]]></prop>
</service>
The Database service implements the request operations.

Two state variables are defined on Database:

  • Map, which is updated by NewMap
  • NextKey, which is updated by NewNextKey
When Database is started, both state variables are initialised:
  • NextKey is set to 2
  • Map is initialised with an indexed Erlang map{...}, with two records
These state variables can be referenced and updated by the operations on the service.

Database also has static bindings comprising:

  • The variable Error, which has the value not_found
  • The variable ExistFun, which is an Erlang function used to decide if a given value is a key in Map
The operations on Database can use these variables.
<solicit name="GetName" 
  service="Sequencer" 
  fields="GET key">
  <response name="Ok" 
    fields="first_name last_name"/>
  <response name="Error" 
    fields="error"/>
</solicit>
...
<request name="Get" 
  service="Database" 
  fields="GET key">
  <prop name="expr.bind.in" 
    Key="key"/>
  <prop name="expr.bind.out" 
    FirstName="first_name" 
    LastName="last_name" 
    Error="error"/>
  <prop name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
case ExistFun(Key,Map) of
  true ->
    #{
      Key:=#{
        first_name := FirstName,
        last_name := LastName}} = Map,
    "Ok";
  _Otherwise ->
    "Error"
end.
  ]]></prop>
  <reply name="Ok" 
    fields="first_name last_name"/>
  <reply name="Error" 
    fields="error"/>
</request>
The GetName solicit starts a transaction that returns the value of a record based on the key of the record, or sends an error message.

The Get operation binds its input and output fields to variables:

  • The input field key to Key
  • The output field first_name to FirstName
  • The output field last_name to LastName
  • The output field error to Error
The expressions in Get:
  1. Use the ExistFun function, specified on the service, to decide if Key is a valid key in Map.
    • If yes, Key is used to get the record with the corresponding values
    • If not, only the Error reply is sent
  2. Depending on whether the key exists, send either:
    • The Ok reply, with the fields first_name and last_name. The fields get their content through the variables.
    • The Error reply, with the error field. This field gets its content through the Error variable, which was defined on the Database service.
Note: If you have not yet inserted new records, only the keys 0 and 1 return a record.
<solicit name="InsertName" 
  service="Sequencer" 
  fields="INSERT first_name last_name">
  <response name="Ok" 
    fields="key"/>
</solicit>
...
<request name="Insert" 
  service="Database" 
  fields="INSERT first_name last_name">
  <prop name="expr.bind.in" 
    FirstName="first_name" 
    LastName="last_name"/>
  <prop name="expr.bind.out" 
    NextKey="key"/>
  <prop name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Record =
  #{first_name => FirstName,
    last_name => LastName},
NewMap =
  Map#{
    NextKey => Record},
NewNextKey =
  NextKey + 1,
"Ok".
  ]]></prop>
  <reply name="Ok" 
    fields="key"/>
</request>
The InsertName solicit starts a transaction that creates a new record in the database and assigns it a key, which is a unique ID.

The Insert request binds its fields to variables:

  • The input field first_name to FirstName
  • The input field last_name to LastName
  • The output field key to NextKey
The expressions in Insert:
  1. Bind the variable Record to the new record being created
  2. Specify the FirstName and LastName variables as the values of this new record
  3. Insert the new record into the database by:
    • Updating the Map state variable on Database
    • Specifying the NextKey variable as the key of the new record
    • Specifying the Record variable as the new record
  4. Create a new key the next record can use by:
    • Updating the NextKey state variable on Database
    • Specifying that the value of the new NextKey, and thus the key of the new record, is the previous value of NextKey plus one
  5. Send the Ok reply with the key field. The value of this field is the value of the NextKey variable, before the update.
Note: If you have not yet inserted new records, the key of the first new record will be 2, the initial value of NextKey.
<solicit name="DeleteName" 
  service="Sequencer" 
  fields="DELETE key">
  <response name="Ok" 
    fields="OK"/>
  <response name="Error" 
    fields="error"/>
</solicit>
...
<request name="Delete" 
  service="Database" 
  fields="DELETE key">
  <prop name="expr.bind.in" 
    Key="key"/>
  <prop name="expr.bind.out" 
    Error="error"/>
  <prop name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
case ExistFun(Key,Map) of
  true ->
    NewMap =
      maps:remove(Key, Map),
    "Ok";
  _Otherwise ->
    "Error"
end.
  ]]></prop>
  <reply name="Ok" 
    fields="OK"/>
  <reply name="Error" 
    fields="error"/>
</request>
The DeleteName solicit starts a transaction that removes an existing record from the database, or sends an error message.

The Delete request binds its input and output fields:

  • The key field to Key
  • The error field to Error
The expressions in Delete:
  1. Use the ExistFun function, specified on the service, to decide if Key is a valid key in Map.
    • If yes, the record corresponding to Key is removed. This is done by updating the Map state variable with NewMap, which does not have the record.
    • If not, only the Error reply is sent.
  2. Depending on whether the key exists, send either:
    • The Ok reply, with the FLAG OK.
    • The Error reply, with the error field. This field gets its content through the Error variable, which was defined on the Database service.
Note: If you have not yet inserted new records, only the keys 0 and 1 can delete a record.
<solicit name="ListNames" 
  service="Sequencer" 
  fields="LIST">
  <response name="Ok" 
    fields="records"/>
  </solicit>
...
<request name="List" 
  service="Database" 
  fields="LIST">
  <prop name="expr.bind.out" 
    Records="records"/>
  <prop name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Records = maps:to_list(Map),
"Ok".
  ]]></prop>
  <reply name="Ok" 
    fields="records"/>
</request>
The ListNames solicit starts a transaction that retrieves all records from the database.

The List request binds its output field records to the Records variable.

The records field is of type term, which means it can hold erlang terms, like lists[...] and maps{...}.

The expressions in List:

  1. Bind the Records variable to an Erlang function, which returns all records of Map as a list.
    Tip: You can leave out the Erlang function by simply binding the output field records to the Map state variable. In this case, the records are returned as an Erlang map, not as a list.
  2. Send the Ok reply, with the field records.
Note: If you have not yet inserted new records, only the original two records are returned. If you have deleted all records, an empty records field is returned.