-
Notifications
You must be signed in to change notification settings - Fork 8
Declarative
A very reasonable question is:
How are rules different from SQL?
They are both declarative...
The question is both reasonable... and important. Rules are the heart of Logic Bank value. In this document, we'll look into the key concepts behind rules.
Let's briefly review Logic Bank. It is designed to automate transaction logic for databases, based on Python and SQLAlchemy. Such backend logic is a significant element of database systems, often nearly half.
Logic Bank can reduce 95% of your backend code by 40X,
using a combination of Rules and (Python) code.
Find out more, here.
We’ve all seen how a clear specification - just a few lines - balloons into hundreds of lines of code. This leads to the key design objective for Logic Bank:
Introduce Spreadsheet-like Rules to
Automate the "Cocktail Napkin Spec"
Below is the implementation of our cocktail napkin spec to check credit:
In the diagram above, the rules are declared on lines 34-43. These 5 rules shown in the screen shot replace several hundred lines of code (40X), as shown here.
While rules are powerful, they cannot automate everything. That leads to the second key design objective:
Rules must be complemented by code for extensibility,
and manageability (debugging, source control, etc).
Python code is straightforward: your event handler is
passed a LogicRow
, which includes
-
Row
- an instance of a SQLAlchemy mapped class -
OldRow
- prior contents
Python extensibility is shown on line 51, invoking the Python event-handler code on line 32.
So, Logic = Rules + Code
. Code is familiar,
rules are less familiar but key to unlocking value.
This page provides important conceptual background on rules, so you can use them effectively.
Returning to our original question:
How are rules different from SQL?
They are both declarative...
From our rule example above, consider the
Balance
declarative rule (line 43):
Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal,
where=lambda row: row.ShippedDate is None) # *not* a sql select sum...
This rule looks much the same as this SQLAlchemy query
embedded in your procedural Python code:
qry = session.query(Order.CustomerId, func.sum(Order.AmountTotal))\
.filter(Order.CustomerId == "ALFKI", Order.ShippedDate == None)
They both seem to be syntactic variations on this sql query:
select sum("Order".AmountTotal) from "Order"
where CustomerId = "ALFKI" and ShippedDate is null
And, it's not so simple as saying "rules are declarative": SQL is declarative too! Yet, these are profoundly different:
-
while sql query is a declarative command, it's embedded in procedural Python code
-
the rule is declarative, completely (not embedded)
This table summarizes the key differences, further discussed below:
Characteristic | Procedural | Declarative | Why It Matters |
---|---|---|---|
Reuse | Not Automatic | Automatic - all Use Cases | 40X Code Reduction |
Invocation | Passive - only if called | Active - call not required | Quality |
Ordering | Manual | Automatic | Agile Maintenance |
Optimizations | Manual | Automatic | Agile Design |
SQL / SQLAlchemy statements embedded in a procedural language (like Python) return an answer, but maintain no ongoing "obligation". If the order amount changes, your balance variable is not affected. One and done.
By contrast, rules are forever - they define end-conditions that must be satisfied by the end of every transaction, for all future data access, for all rows. These definitions apply regardless of what updates are made. In other words, rules are automatically re-used over Use Cases.
In fact, the rule above could be accurately stated as as:
"""
For all Customers and all Transactions,
Update the Customers' balance (iff required)
so that it is the sum of the unshipped order totals.
"""
Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal,
where=lambda row: row.ShippedDate is None) # *not* a sql select sum...
As described in the overview, the logic engine operates as a listener for SQLAlchemy events. For each rule, it
- watches for changes in referenced values
- reacts by obtaining the value and assigning it to the derived attribute
- chains, if the derived attribute is referenced by still other derivations
So, in the balance
example, the watch
logic
is watching
- the
AmoutTotal
- the
ShippedDate
- insert, updates and deletes of
Order
- the foreign key from
Order
toCustomer
And, of course, these operate in combinations. Consider
the test upd_order_reuse.py
,
which illustrates re-use with a number of changes:
- reassign the order to a different customer
- change an OrderDetail (eg, "I'll buy 1 WidgetSet, not 5 Widgets")
- A different Product
- A different Quantity
The 5 rules above dictate the following system behavior:
- reduce the old customer balance by the old Amount,
- compute the new order Amount (per the new Quantity and Product Price)
- adjust the new customer balance by the new Amount
Dependency Management - checking to see what has changed - is another term for the "watch" logic. In you study the no-rules "legacy" code shown here, with a walk-through here), that's where all the work is.
By moving this to the logic engine, you get massive code savings. It's the key reason why rules are 40X more concise than code. To visualize:
Procedural code only runs when it is called, either directly or by an event dispatcher. So, if the code is enforcing rules, the burden is on developers to call it. This can introduce errors: "my code was there, you just didn't call it!"
By contrast, you declare rules, but you never call them. That is the job of the "watch / react" logic engine.
Automatic Invocation eliminates an entire class
of "corner-case" bugs, so improves quality.
A favorite programmer joke I tell is:
How about we reorder the code of your last program?
Think it will still run?
Ha ha, of course not.
But rules do! As the system watches what was changed, its react logic is ordered by dependencies. These dependencies are discovered - by the system - automatically.
Automatic Invocation pays off during maintenance.
You just change rules, or add them.
The system will discover them, ensure they are invoked, and
in a proper order that reflects their dependencies.
As developers, we are responsible not only for writing correct code, but also performant code. A big part of that is minimizing and optimizing SQL calls.
Not only is this time consuming, it's brittle. If we discover performance issues that require changing the database design, we must review / revise all our existing code.
For example, Denormalization describes a common pattern where
we might denormalize to store sums like the customer balance.
This enables an optimization where you can adjust
the balance when a new order is added with a 1-row update
(rather than run an expensive select sum
query).
The problem is that our optimization assumptions - or the lack of them -
are hard-coded into our programs. If we add the Customer.Balance
column,
all the existing code is oblivious, merrily issuing select sum
queries.
The optimization is enabled, but not automated.
The beauty of declarative is that you declare "what", not "how". That is, you state the end result, not how to achieve it. This is eloquently described in Chris Dates' book, What Not How.
In our example, the customer balance rule leaves it up to the system how to do it. Not only does that enable the system to automatically optimize (like a SQL optimizer), but it also enables it to adapt: re-optimize for a new database design, transparently to existing code.
Automatic reoptimization enables your team to be agile -
to change the design without recoding.
So, if we begin with a normalized database, then introduce the
Customer.Balance
column, the system will stop using expensive
select sum
queries, replacing them with 1 row adjustments.
We often use the spreadsheet metaphor to describe rule operation. Logic Bank seeks to provide the same value for database backends as spreadsheets do for financial analysis:
So, back to our original question:
How are rules different from SQL?
They are both declarative...
We can now answer. Rules are a different way of programming, where you focus on what not how:
SQL is a *single* declarative command, significant, called from procedural code...
Procedural code is manually invoked, ordered and optimized.
Rules are declarative backend - a *set* of declarative commands...
Rules are automatically invoked, ordered and optimized.
The result is 40X more concise than procedural code, higher quality, and more agile.
User Project Operations
Logic Bank Internals