Skip to content
valhuber edited this page Oct 8, 2020 · 24 revisions

Use Logic Bank to govern SQLAlchemy update transaction logic - multi-table derivations, constraints, and actions such as sending mail or messages. Logic consists of:

  • Rules - 40X more concise using a spreadsheet-like paradigm, and

  • Python - control and extensibility, using standard functions and event handlers

Features

Logic Bank is:

  • Extensible: logic consists of rules (see below), plus standard Python code

  • Multi-table: rules like sum automate multi-table transactions

  • Scalable: rules are automatically pruned and optimized; for example, sums are processed as 1 row adjustment updates, rather than expensive SQL aggregate queries

  • Manageable: develop and debug your rules in IDEs, manage it in SCS systems (such as git) using existing procedures

Logic Bank can return meaningful savings in time and cost for your database oriented projects, as described below.

The Business Case for Logic Bank

Transaction Logic: half the system

For most transaction-oriented database applications, backend database logic is a substantial portion of the effort. It includes multi-table derivation and constraint logic, and actions such as sending mail or messages.

Such backend logic is typically coded in before_flush events, database triggers, and/or stored procedures. The prevailing assumption is that such domain-specific logic must surely be domain-specific code.

Problem: Code-intensive - time-consuming, error prone

The problem is that this is a lot of code. Often nearly half the effort for a transactional database-oriented systems, it is time-consuming, complex and error-prone.

Solution: Rules - 40X more concise, extensible, manageable

Now there is an alternative. The first 5 rules shown below represent the same logic as this hand-coded legacy approach.

Logic consists of rules and Python: rules are expressed in Python, and can invoke Python. This provides extensibility, and enables your logic to be edited, source-controlled and debugged using familiar tools and procedures.

Architecture

  1. Declare logic as rules and Python (see example below).

  2. Your application makes calls on sqlalchemy for inserts, updates and deletes.

    • By bundling transaction logic into SQLAlchemy data access, your logic is automatically shared, whether for hand-written code (Flask apps, APIs) or via generators such as Flask AppBuilder.
  3. The logic_bank logic engine handles SQLAlchemy before_flush events on Mapped Tables

  4. The logic engine operates much like a spreadsheet: watch for changes at the attribute level, react by running rules that referenced changed attributes, which can chain to still other attributes that refer to those changes. Note these might be in different tables, providing automation for multi-table logic.

Logic does not apply to updates outside SQLAlchemy, or to SQLAlchemy batch updates or unmapped sql updates.

Declaring Logic as Spreadsheet-like Rules

To illustrate, let's use an adaption of the Northwind database, with a few rollup columns added. For those not familiar, this is basically Customers, Orders, OrderDetails and Products, as shown in the diagrams below.

Declare rules using Python

Logic is declared as spreadsheet-like rules as shown below from nw/logic/rules_bank.py, which implements the check credit requirement:

def activate_basic_check_credit_rules():
    """
    Issues function calls to activate check credit rules, below.
        These rules are executed not now, but on commits
        Order is irrelevant - determined by system based on dependency analysis
        Their inclusion in classes is for doc / convenience, no semantics

    These rules apply to all transactions (automatic re-use), eg.
    * place order
    * change Order Detail product, quantity
    * add/delete Order Detail
    * ship / unship order
    * delete order
    * move order to new customer, etc
    """

    def units_in_stock(row: Product, old_row: Product, logic_row: LogicRow):
        result = row.UnitsInStock - (row.UnitsShipped - old_row.UnitsShipped)
        return result

    Rule.constraint(validate=Customer,
                    as_condition=lambda row: row.Balance <= row.CreditLimit,
                    error_msg="balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
    Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal,
             where=lambda row: row.ShippedDate is None)  # *not* a sql select sum...

    Rule.sum(derive=Order.AmountTotal, as_sum_of=OrderDetail.Amount)

    Rule.formula(derive=OrderDetail.Amount, as_expression=lambda row: row.UnitPrice * row.Quantity)
    Rule.copy(derive=OrderDetail.UnitPrice, from_parent=Product.UnitPrice)

Here is a good example of extensibility - activate_basic_check_credit_rules also includes:

    def congratulate_sales_rep(row: Order, old_row: Order, logic_row: LogicRow):
        if logic_row.ins_upd_dlt == "ins" or True:  # logic engine fills parents for insert
            sales_rep = row.SalesRep  # type : Employee
            if sales_rep is None:
                logic_row.log("no salesrep for this order")
            else:
                logic_row.log(f'Hi, {sales_rep.Manager.FirstName}, congratulate {sales_rep.FirstName} on their new order')

    Rule.commit_row_event(on_class=Order, calling=congratulate_sales_rep)

And finally, we add 3 more rules to manage UnitsInStock:

  • when orders are shipped, reduce the UnitsInStock for the ordered items
    Rule.formula(derive=OrderDetail.ShippedDate, as_expression=lambda row: row.OrderHeader.ShippedDate)
    
    Rule.sum(derive=Product.UnitsShipped, as_sum_of=OrderDetail.Quantity,
             where="row.ShippedDate is not None")
    Rule.formula(derive=Product.UnitsInStock, calling=units_in_stock)

That defines the rules; we activate them as follows.

Activate Rules

One of the test programs is nw/tests/add_order.py. It activates the rules using this import:

from nw.nw_logic import session  # opens db, activates logic listener <--

This executes nw/logic/__init__.py, which sets up the rule engine:

by_rules = True  # True => use rules, False => use legacy hand code (for comparison)
if by_rules:
    LogicBank.activate(session=session, activator=declare_logic)
else:
    # ... conventional after_flush listeners (to see rules/code contrast)

Logic Execution: Watch, React, Chain

The activation does not execute any logic. It simply registers some listeners, and loads the rules into memory.

The rules run when you commit altered SQLAlchemy row. No changes are required to the SQLAlchemy API's.

The engine gains control on SQLAlchemy after_flush. It operates much as you might imagine a spreadsheet:

  • Watch - for inserts, deletes, and updates at the attribute level

  • React - derivation rules referencing changes are (re)executed (forward chaining rule inference); unreferenced rules are pruned.

  • Chain - if recomputed values are referenced by still other rules, these are re-executed. Note this can be in other tables, thus automating multi-table transaction logic.

Let's see how.

The specification is fully executable, and governs around a dozen transactions. Here we look at 2 simple examples:

  • Add Order (Check Credit) - enter an order/orderdetails, and rollup to AmountTotal / Balance to check CreditLimit

  • Ship / Unship an Order (Adjust Balance) - when an Order's DateShipped is changed, adjust the Customers Balance

These representatively complex transactions illustrate common logic execution patterns, described in the following sections.

Example: Add Order - Multi-Table Adjustment, Chaining

The Add Order example illustrates chaining as OrderDetails are added:

  1. The OrderDetail.UnitPrice is referenced from the Product so it is copied

  2. OrderDetails are referenced by the Orders' AmountTotal sum rule, so AmountTotal is adjusted

  3. The AmountTotal is referenced by the Customers' Balance, so it is adjusted

  4. And the Credit Limit constraint is checked (exceptions are raised if constraints are violated)

All of the dependency management to see which attribute have changed, logic ordering, the SQL commands to read and adjust rows, and the chaining are fully automated by the engine, based solely on the rules above. This is how 5 rules represent the same logic as 200 lines of code.

Key points are discussed in the sub-sections below.

Multi-Table Logic

The sum rule that "watches" OrderDetail.AmountTotal changes is in a different table: Orders. So, the "react" logic has to perform a multi-table transaction, which means we need to be careful about performance.

Optimizations: Adjustment (vs. nested sum queries)

Note that rules declare end conditions, enabling / obligating the engine to optimize execution (like a SQL query optimizer). Consider the rule for Customer.Balance.

As in commonly the case (e.g. Rete engines, some ORM systems), you may reasonably expect this is executed as a SQL select sum.

It is not.

Instead, it is executed as an adjustment: as single row update to the Orders balance. This optimization dramatically reduces the SQL cost, often by orders of magnitude:

  • select sum queries are expensive - imagine a customer with thousands of Orders.

  • Here, it's lots worse, since it's a chained sum, so computing the balance requires not only we read all the orders, but all the OrderDetails of each order.

To see more on how watch, react and chain logic operates for multi-table transactions,
click here.

Logic Logging

Logic execution logs all rules that fire, to aid in debugging.

  • Log indention shows multi-table chaining.
  • Logged row state shows the {reason} that caused the update (e.g., client, sum adjustment), and old and new values
Logic Phase:		BEFORE COMMIT          						 - 2020-10-05 20:23:26,050 - logic_logger - DEBUG
Logic Phase:		ROW LOGIC (sqlalchemy before_flush)			 - 2020-10-05 20:23:26,050 - logic_logger - DEBUG
..Order[None] {Insert - client} AmountTotal: 0, CustomerId: ALFKI, EmployeeId: 6, Freight: 1, Id: None, OrderDate: None, RequiredDate: None, ShipAddress: None, ShipCity: Richmond, ShipCountry: None, ShipName: None, ShipPostalCode: None, ShipRegion: None, ShipVia: None, ShippedDate: None  row@: 0x102e67f40 - 2020-10-05 20:23:26,052 - logic_logger - DEBUG
....Customer[ALFKI] {Update - Adjusting Customer} Address: Obere Str. 57, Balance: 960.0000000000, City: Berlin, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Country: Germany, CreditLimit: 2000.0000000000, Fax: 030-0076545, Id: ALFKI, OrderCount:  [8-->] 9, Phone: 030-0074321, PostalCode: 12209, Region: Western Europe, UnpaidOrderCount:  [3-->] 4  row@: 0x1030cc490 - 2020-10-05 20:23:26,072 - logic_logger - DEBUG
..OrderDetail[None] {Insert - client} Amount: 0, Discount: 0, Id: None, OrderId: None, ProductId: 1, Quantity: 1, ShippedDate: None, UnitPrice: 18  row@: 0x102e67fd0 - 2020-10-05 20:23:26,074 - logic_logger - DEBUG
..OrderDetail[None] {copy_rules for role: ProductOrdered} Amount: 0, Discount: 0, Id: None, OrderId: None, ProductId: 1, Quantity: 1, ShippedDate: None, UnitPrice: 18  row@: 0x102e67fd0 - 2020-10-05 20:23:26,079 - logic_logger - DEBUG
..OrderDetail[None] {Formula Amount} Amount: 18.0000000000, Discount: 0, Id: None, OrderId: None, ProductId: 1, Quantity: 1, ShippedDate: None, UnitPrice: 18.0000000000  row@: 0x102e67fd0 - 2020-10-05 20:23:26,082 - logic_logger - DEBUG
....Order[None] {Update - Adjusting OrderHeader} AmountTotal:  [0-->] 18.0000000000, CustomerId: ALFKI, EmployeeId: 6, Freight: 1, Id: None, OrderDate: None, RequiredDate: None, ShipAddress: None, ShipCity: Richmond, ShipCountry: None, ShipName: None, ShipPostalCode: None, ShipRegion: None, ShipVia: None, ShippedDate: None  row@: 0x102e67f40 - 2020-10-05 20:23:26,085 - logic_logger - DEBUG
......Customer[ALFKI] {Update - Adjusting Customer} Address: Obere Str. 57, Balance:  [960.0000000000-->] 978.0000000000, City: Berlin, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Country: Germany, CreditLimit: 2000.0000000000, Fax: 030-0076545, Id: ALFKI, OrderCount: 9, Phone: 030-0074321, PostalCode: 12209, Region: Western Europe, UnpaidOrderCount: 4  row@: 0x1030cc490 - 2020-10-05 20:23:26,088 - logic_logger - DEBUG
..OrderDetail[None] {Insert - client} Amount: 0, Discount: 0, Id: None, OrderId: None, ProductId: 2, Quantity: 2, ShippedDate: None, UnitPrice: 18  row@: 0x102ef2040 - 2020-10-05 20:23:26,092 - logic_logger - DEBUG
..OrderDetail[None] {copy_rules for role: ProductOrdered} Amount: 0, Discount: 0, Id: None, OrderId: None, ProductId: 2, Quantity: 2, ShippedDate: None, UnitPrice: 18  row@: 0x102ef2040 - 2020-10-05 20:23:26,097 - logic_logger - DEBUG
..OrderDetail[None] {Formula Amount} Amount: 38.0000000000, Discount: 0, Id: None, OrderId: None, ProductId: 2, Quantity: 2, ShippedDate: None, UnitPrice: 19.0000000000  row@: 0x102ef2040 - 2020-10-05 20:23:26,098 - logic_logger - DEBUG
....Order[None] {Update - Adjusting OrderHeader} AmountTotal:  [18.0000000000-->] 56.0000000000, CustomerId: ALFKI, EmployeeId: 6, Freight: 1, Id: None, OrderDate: None, RequiredDate: None, ShipAddress: None, ShipCity: Richmond, ShipCountry: None, ShipName: None, ShipPostalCode: None, ShipRegion: None, ShipVia: None, ShippedDate: None  row@: 0x102e67f40 - 2020-10-05 20:23:26,103 - logic_logger - DEBUG
......Customer[ALFKI] {Update - Adjusting Customer} Address: Obere Str. 57, Balance:  [978.0000000000-->] 1016.0000000000, City: Berlin, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Country: Germany, CreditLimit: 2000.0000000000, Fax: 030-0076545, Id: ALFKI, OrderCount: 9, Phone: 030-0074321, PostalCode: 12209, Region: Western Europe, UnpaidOrderCount: 4  row@: 0x1030cc490 - 2020-10-05 20:23:26,105 - logic_logger - DEBUG
Logic Phase:		COMMIT   									 - 2020-10-05 20:23:26,106 - logic_logger - DEBUG
..Order[None] {Commit Event} AmountTotal: 56.0000000000, CustomerId: ALFKI, EmployeeId: 6, Freight: 1, Id: None, OrderDate: None, RequiredDate: None, ShipAddress: None, ShipCity: Richmond, ShipCountry: None, ShipName: None, ShipPostalCode: None, ShipRegion: None, ShipVia: None, ShippedDate: None  row@: 0x102e67f40 - 2020-10-05 20:23:26,107 - logic_logger - DEBUG
..Order[None] {Hi, Steven, congratulate Michael on their new order} AmountTotal: 56.0000000000, CustomerId: ALFKI, EmployeeId: 6, Freight: 1, Id: None, OrderDate: None, RequiredDate: None, ShipAddress: None, ShipCity: Richmond, ShipCountry: None, ShipName: None, ShipPostalCode: None, ShipRegion: None, ShipVia: None, ShippedDate: None  row@: 0x102e67f40 - 2020-10-05 20:23:26,112 - logic_logger - DEBUG
Logic Phase:		FLUSH   (sqlalchemy flush processing       	 - 2020-10-05 20:23:26,113 - logic_logger - DEBUG

Example: Ship Order - Pruning, Adjustment and Cascade


The `upd_order_shipped` example illustrates pruning and adjustment: If `DueDate` is altered, nothing is dependent on that, so the rule is **pruned** from the logic execution. The result is a 1 row transaction - zero SQL overhead from rules.

If ShippedDate is altered, 2 kinds of multi-table logic are triggered - adjustment and cascade:

  1. the logic engine adjusts the Customer.Balance with a 1 row update, as described above

    • Note that in this case, the triggering event is a change to the where condition, rather than a change to the summed value

    • The watch logic is monitoring changes to summed fields, where conditions, foreign keys, and inserts / updates / delete. This eliminates large amounts of clumsy, boring and error prone code

  2. the ShippedDate is also referenced by the OrderDetail.ShippedDate rule, so the system cascades the change to each OrderDetail to reevaluate referring rules

  3. This further chains to adjust Product.UnitsInStock, whose change recomputes Product.UnitsInStock (see below)

State Transition Logic (old values)

Logic often depends on the old vs. new state of a row. For example, here is the function used to compute Product.UnitsInStock:

def units_in_stock(row: Product, old_row: Product, logic_row: LogicRow):
    result = row.UnitsInStock - (row.UnitsShipped - old_row.UnitsShipped)
    return result

Note this logic is in Python: you can invoke Python functions, set breakpoints, etc.

Ship Order: Log

The log makes clear the chain of logic:

Logic Phase:		BEFORE COMMIT          						 - 2020-10-07 08:33:21,422 - logic_logger - DEBUG
Logic Phase:		ROW LOGIC (sqlalchemy before_flush)			 - 2020-10-07 08:33:21,423 - logic_logger - DEBUG
..Order[11011] {Update - client} AmountTotal: 960.0000000000, CustomerId: ALFKI, EmployeeId: 3, Freight: 1.2100000000, Id: 11011, OrderDate: 2014-04-09, RequiredDate: 2014-05-07, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipCountry: Germany, ShipName: Alfred's Futterkiste, ShipPostalCode: 12209, ShipRegion: Western Europe, ShipVia: 1, ShippedDate:  [None-->] 2020-10-07 08:33:21.422547  row@: 0x10b2d1a90 - 2020-10-07 08:33:21,423 - logic_logger - DEBUG
....Customer[ALFKI] {Update - Adjusting Customer} Address: Obere Str. 57, Balance:  [960.0000000000-->] 0E-10, City: Berlin, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Country: Germany, CreditLimit: 2000.0000000000, Fax: 030-0076545, Id: ALFKI, OrderCount: 8, Phone: 030-0074321, PostalCode: 12209, Region: Western Europe, UnpaidOrderCount:  [3-->] 2  row@: 0x10b3ca640 - 2020-10-07 08:33:21,425 - logic_logger - DEBUG
....OrderDetail[1972] {Update - Cascading OrderHeader.ShippedDate (,...)} Amount: 530.0000000000, Discount: 0.05, Id: 1972, OrderId: 11011, ProductId: 58, Quantity: 40, ShippedDate: None, UnitPrice: 13.2500000000  row@: 0x10b4fd070 - 2020-10-07 08:33:21,429 - logic_logger - DEBUG
....OrderDetail[1972] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Amount: 530.0000000000, Discount: 0.05, Id: 1972, OrderId: 11011, ProductId: 58, Quantity: 40, ShippedDate: None, UnitPrice: 13.2500000000  row@: 0x10b4fd070 - 2020-10-07 08:33:21,429 - logic_logger - DEBUG
....OrderDetail[1972] {Formula ShippedDate} Amount: 530.0000000000, Discount: 0.05, Id: 1972, OrderId: 11011, ProductId: 58, Quantity: 40, ShippedDate:  [None-->] 2020-10-07 08:33:21.422547, UnitPrice: 13.2500000000  row@: 0x10b4fd070 - 2020-10-07 08:33:21,429 - logic_logger - DEBUG
......Product[58] {Update - Adjusting ProductOrdered} CategoryId: 8, Discontinued: 0, Id: 58, ProductName: Escargots de Bourgogne, QuantityPerUnit: 24 pieces, ReorderLevel: 20, SupplierId: 27, UnitPrice: 13.2500000000, UnitsInStock: 102, UnitsOnOrder: 0, UnitsShipped:  [0-->] 40  row@: 0x10b5057f0 - 2020-10-07 08:33:21,432 - logic_logger - DEBUG
......Product[58] {Formula UnitsInStock} CategoryId: 8, Discontinued: 0, Id: 58, ProductName: Escargots de Bourgogne, QuantityPerUnit: 24 pieces, ReorderLevel: 20, SupplierId: 27, UnitPrice: 13.2500000000, UnitsInStock:  [102-->] 62, UnitsOnOrder: 0, UnitsShipped:  [0-->] 40  row@: 0x10b5057f0 - 2020-10-07 08:33:21,432 - logic_logger - DEBUG
....OrderDetail[1973] {Update - Cascading OrderHeader.ShippedDate (,...)} Amount: 430.0000000000, Discount: 0.0, Id: 1973, OrderId: 11011, ProductId: 71, Quantity: 20, ShippedDate: None, UnitPrice: 21.5000000000  row@: 0x10b4fd160 - 2020-10-07 08:33:21,433 - logic_logger - DEBUG
....OrderDetail[1973] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Amount: 430.0000000000, Discount: 0.0, Id: 1973, OrderId: 11011, ProductId: 71, Quantity: 20, ShippedDate: None, UnitPrice: 21.5000000000  row@: 0x10b4fd160 - 2020-10-07 08:33:21,433 - logic_logger - DEBUG
....OrderDetail[1973] {Formula ShippedDate} Amount: 430.0000000000, Discount: 0.0, Id: 1973, OrderId: 11011, ProductId: 71, Quantity: 20, ShippedDate:  [None-->] 2020-10-07 08:33:21.422547, UnitPrice: 21.5000000000  row@: 0x10b4fd160 - 2020-10-07 08:33:21,433 - logic_logger - DEBUG
......Product[71] {Update - Adjusting ProductOrdered} CategoryId: 4, Discontinued: 0, Id: 71, ProductName: Flotemysost, QuantityPerUnit: 10 - 500 g pkgs., ReorderLevel: 0, SupplierId: 15, UnitPrice: 21.5000000000, UnitsInStock: 46, UnitsOnOrder: 0, UnitsShipped:  [-20-->] 0  row@: 0x10b505460 - 2020-10-07 08:33:21,436 - logic_logger - DEBUG
......Product[71] {Formula UnitsInStock} CategoryId: 4, Discontinued: 0, Id: 71, ProductName: Flotemysost, QuantityPerUnit: 10 - 500 g pkgs., ReorderLevel: 0, SupplierId: 15, UnitPrice: 21.5000000000, UnitsInStock:  [46-->] 26, UnitsOnOrder: 0, UnitsShipped:  [-20-->] 0  row@: 0x10b505460 - 2020-10-07 08:33:21,436 - logic_logger - DEBUG
Logic Phase:		COMMIT   									 - 2020-10-07 08:33:21,437 - logic_logger - DEBUG
..Order[11011] {Commit Event} AmountTotal: 960.0000000000, CustomerId: ALFKI, EmployeeId: 3, Freight: 1.2100000000, Id: 11011, OrderDate: 2014-04-09, RequiredDate: 2014-05-07, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipCountry: Germany, ShipName: Alfred's Futterkiste, ShipPostalCode: 12209, ShipRegion: Western Europe, ShipVia: 1, ShippedDate:  [None-->] 2020-10-07 08:33:21.422547  row@: 0x10b2d1a90 - 2020-10-07 08:33:21,437 - logic_logger - DEBUG
..Order[11011] {Hi, Andrew, congratulate Janet on their new order} AmountTotal: 960.0000000000, CustomerId: ALFKI, EmployeeId: 3, Freight: 1.2100000000, Id: 11011, OrderDate: 2014-04-09, RequiredDate: 2014-05-07, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipCountry: Germany, ShipName: Alfred's Futterkiste, ShipPostalCode: 12209, ShipRegion: Western Europe, ShipVia: 1, ShippedDate:  [None-->] 2020-10-07 08:33:21.422547  row@: 0x10b2d1a90 - 2020-10-07 08:33:21,448 - logic_logger - DEBUG
Logic Phase:		FLUSH   (sqlalchemy flush processing       	 - 2020-10-07 08:33:21,448 - logic_logger - DEBUG

DB-generated Keys

DB-generated keys are often tricky (how do you insert items if you don't know the db-generated orderId?), shown here in Order and OrderDetail. These were well-handled by sqlalchemy, where adding OrderDetail rows into the Orders' collection automatically set the foreign keys.

An Agile Perspective

The core tenant of agile is working software, driving collaboration, for rapid iterations. Here's how rules can help.

Working Software Now

The examples above illustrate how just a few rules can replace pages of code.

Certainly business users are more easily able to read rules than code. But still, rules are pretty abstract.

Business users relate best to actual working pages - their interpretation of working software. The fab-quick-start project enables you to build a basic web app in minutes.

This project has already generated such an app, which you can run like this once you've finished the Installation process:

cd Logic-Bank
cd nw/basic_web_app
# windows set FLASK_APP=app
export FLASK_APP=app
flask run

Login: user = admin, password = p

You can

  1. Navigate to Order 11011 (a multi-page web app)
  2. Click Edit so you can make changes
  3. Change the Shipped Date
  4. Verify logic enforcement
    • The web app has been configured to activate the rules
    • The logic for this update is interesting - check out the console log

Iteration

Rules are self-organizing - they recognize their interdependencies, and order their execution and database access (pruning, adjustments etc) accordingly. This means:

  • order is independent - you can state the rules in any order and get the same result

  • maintenance is simple - just make changes, additions and deletions, the engine will reorganize execution order and database access, automatically

Background (Note: DB always refreshed)

The subject database is an adaption of nw, with a few rollup columns added. For those not familiar, this is basically Customers, Orders, OrderDetails and Products.

Important - the by-code and by-rules approaches both share the same init module which opens the database.

  • It also copies a fresh database, so if you alter the nw.db between runs, be careful - your changes will be overridden very time you run a test