Skip to content

Declarative

valhuber edited this page Jan 1, 2021 · 59 revisions

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.

Logic Bank Background: Logic = Rules + Code

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.

Rules Automate the "Cocktail Napkin Spec"

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.

Python for Extensibility, Manageability

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.

Are Rules Just "Refried SQL"?

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

Automatic Reuse: "one and done" vs. "forever"

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...

Watch, React and Change - to all Updates

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 to Customer

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

Automatic "Watch" Logic: 40X Code Savings

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:

Automatic Invocation

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.

Automatic Ordering by System-Discovered Dependencies

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.

Automatic Optimizations

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.

Procedural: design choices are hard-coded

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.

Declarative: automatic optimizations are adaptable, agile

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.

Like a Spreadsheet

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:


Summary

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.

Clone this wiki locally