- Constraints to constrain predicate facts, e.g. with types or other restrictions (with the arrow pointing to the right:
- Rules to derive new facts from existing ones (with the arrow pointing to the left:
- Delta updates to insert, update or remove facts from the workspace.
It’s time to tell the big secret: the third type of clause is just a convenient notation for a rule and not a special thing by itself. I know — it’s crazy.
//lang:logiql +icecream_cost["Lemon Popsicle"] = 5.
Is just a more compact way of expressing:
//lang:logiql +icecream_cost["Lemon Popsicle"] = 5 <- .
This rule says: “if, ehm, always, insert the following fact into the database.”
As you may recall, in LogicBlox in 30 minutes, we used the
exec REPL command rather than the
addblock command to execute delta updates. The reason is that
exec is a one-time operation: the rules and constraints that you specify with
exec have the life time of a single transaction and when that transaction ends they disappear, although their side effects, like inserting, updating or removing facts do not, unless the transaction is rolled back. If, instead, we would
addblock this rule, effectively that would mean that there should always be a
icecream_cost fact for lemon popsicles with the value 5, which means we could never change it.
Now that we know that a delta update really is just a rule, what useful things can we do by putting something at the right side of the
Welcome to the wondrous world of delta logic.
Delta logic rules are rules that can generate new delta updates triggered by a combination of other delta updates and existing facts.
Confused? Let’s have a look at an example.
Log all the things!
Let’s say we want to extend our ice cream accounting system with logging support: we want keep track of historical costs of ice creams. So, whenever the cost of an ice cream in the system is changed, we want to create a log entry with the date and time, ice cream type and new cost.
Let’s define a predicate for this purpose:
//lang:logiql historical_icecream_cost[time, icecream] = cost -> datetime(time), icecream(icecream), int(cost).
What we could do whenever we update the price is insert an entry into
historical_icecream_cost as well:
//lang:logiql ^icecream_cost["Lemon Popsicle"] = 10. +historical_icecream_cost[datetime:now, "Lemon Popsicle"] = 10.
But we’re lazy — so instead we’re going to write some delta logic that does this for us automatically:
//lang:logiql +historical_icecream_cost[datetime:now, icecream] = cost <- +icecream_cost[icecream] = cost.
This rule says: “if, within a transaction, a new fact is inserted into the
icecream_cost predicate for
cost, insert a fact with that same data and current timestamp (
historical_icecream_cost.” Simple, right?
You may wonder why we used the
+ (insert) for
icecream_cost instead of
^ (update). The reason is we’re interested in both insertions and updates. In fact, an update is just a removal (
-) and insertion (
+) in one, so you capture both by using
Now, whenever we insert new ice cream costs, or update existing ones, time stamped entries are automatically inserted into
historical_icecream_cost. Pretty cool!
Automatically tracking creation dates
Now that we’re getting the hang of things, let’s also log creation dates for ice creams by observing the
icecream_cost predicate. This is a contrived way of doing it (usually you would do so by observing insertions into the
icecream entity predicate), but let’s stick to it for the purposes of this example.
First we define a predicate to keep track of the creation dates:
//lang:logiql icecream_creation_date[icecream] = date -> icecream(icecream), datetime(date).
We can fill this predicate with data in two ways:
We can create a regular rule that finds the minimum value of the timestamp for a given ice cream in the
historical_icecream_costpredicate — which would be the timestamp for when the ice cream was first created — as follows:
//lang:logiql icecream_creation_date[icecream] = date <- agg<<date = min(cost_date)>> historical_icecream_cost[cost_date, icecream] = _.
We create a delta rule that watches insertions into the
Let’s have a closer look at the second solution, which is more relevant to delta logic.
The rule looks as follows:
//lang:logiql +icecream_creation_date[icecream] = datetime:now <- +icecream_cost[icecream] = _, !icecream_creation_date@prev[icecream] = _.
The last line may contain two things you’re not yet familiar with:
- Negation (
- Stage tags (
Negation is pretty simple: it expresses that a rule only applies when the expression after
! does not hold. So in this case: a fact is only inserted into
icecream_creation_date when a
icecream_cost fact is inserted and there’s not already an entry for that same ice cream in the
Stage tags are used to refer to the state of a predicate during different stages of a transaction:
prev): refers to the state of a predicate before the transaction began.
init): refers to the state of a predicate after running transaction-lifetime rules (such as delta updates).
final: refers to the state of the workspace after all rules have been applied
predicatename@stage means “the state of predicate
predicatename at stage
stage“. If the stage tag is left out,
@final is assumed in the context of
@init in the context of
So, reconsidering our rule: why do we need to use
@prev in this case? The reason is we’re making changes to the
icecream_creation_date predicate during the transaction. So if we’d refer to the final state of the predicate (by leaving off
@prev) we’d ask for something that makes no sense: an
icecream related fact in
icecream_creation_date is both inserted and does not exist at the end of the transaction — this could never happen. What we mean to say is: if there is no
icecream_creation_date fact for the ice cream in question at the beginning of the transaction then insert one now.
Another question you may have is what would happen if we would leave off the whole
!icecream_creation_date@prev[icecream] = _ part. If we would do that, everything seems fine initially when a new
icecream_cost fact is inserted. However, when it is updated later on, we will get an error message, because a fact is already defined for
icecream_creation_date and that ice cream and we can’t insert another one (since it’s a functional predicate).
A way to “fix” this error is to replace
//lang:logiql +icecream_creation_date[icecream] = datetime:now
//lang:logiql ^icecream_creation_date[icecream] = datetime:now
But then we’re tracking the last updated date of an ice cream. This is possibly useful, but not what we were shooting for.
Pulse predicates are predicates whose facts have a transaction lifetime, that is: you can insert facts into them, but those facts are only available as delta updates within that same transaction — they disappear after the transaction ends.
Pulse predicates can be used as a type of eventing system between components of your application, allowing you to say: “Hey, whoever is interested: X just happened.” This can be useful to communicate between application components within a LogicBlox workspace, as well as a way to communicate with the outside world. For instance, protobuf web services are implemented using pulse predicates. Both HTTP request and response are modeled using pulse predicates. When a HTTP request comes in the LB web server starts a new transaction, “pulses” (inserts) the appropriate request predicate, and and expects a response predicate to be pulsed in response by delta logic in the workspace.
A pulse predicate is defined like any other predicate, but with an extra
//lang:logiql icecream_inserted(icecream) -> icecream(icecream). lang:pulse(`icecream_inserted).
Pulse predicates are often used as a means for LogiQL code inside of a workspace to communicate with the outside world, e.g. the LB Web server (Protobuf/JSON web services and the measure service).