Commit Constraints
TL;DR - Specify expression / function that must be true after the transaction settles, else exception
Commit Constraints declare an expression (lambda) or function that must be true for a transaction to complete - like Constraints, but checked once, after the transaction's rule cascade has fully settled, instead of inline as each row is processed. You also provide a message that is returned in the exception that is raised if the expression is not true.
Use a Commit Constraint instead of a Constraint when your condition depends on a derived value (e.g., a Rule.count or Rule.sum) that isn't final until sibling rows in the same transaction have been processed - most commonly, min-cardinality rules like "Order must have at least one Item."
Why not just use Constraint?
A plain Constraint is checked inline, as part of processing each row during the transaction's rule cascade - including a row's own insert. That's a problem for min-cardinality rules: if you insert an Order and its OrderDetail rows in the same transaction, the Order's own insert-time logic can run before its OrderDetail rows have been processed, so a Constraint checking row.item_count > 0 can see a stale count of 0 - even though the Items are about to be added in that same commit.
Rule.commit_constraint avoids this: it's checked once per affected row, after the whole transaction's cascade has settled - so it always sees the final, correct value.
Provide expression as lambda
Rule.count(derive=Order.item_count, as_count_of=OrderDetail)
Rule.commit_constraint(validate=Order,
as_condition=lambda row: row.item_count > 0,
error_msg="Order {row.Id} must have at least one item")
Note the argument is the row, providing access to the attributes - same as Constraint. You can stop in the debugger and examine values when the lambda is invoked.
Provide function - old_row, verb
For more complex cases, you can provide a function instead of as_condition, using calling=. Note the arguments include old_row and logic_row. The latter provides access to the verb, so you can make your logic apply only to the desired verbs - same signature as Constraint's function form.
def check_item_count(row: Order, old_row: Order, logic_row: LogicRow):
return row.item_count > 0
Rule.commit_constraint(validate=Order, calling=check_item_count,
error_msg="Order {row.Id} must have at least one item")
When it runs
| Constraint | Commit Constraint | |
|---|---|---|
| Checked | Inline, per row, as each row is processed during the flush cascade | Once per affected row, after the transaction's cascade has fully settled |
| Sees | The row's state at the moment it's processed - may be mid-cascade | The row's final, settled state |
| Good for | Ordinary single/multi-field validations (e.g., row.Balance <= row.CreditLimit) |
Min-cardinality / "existence" rules whose derived value isn't final until sibling inserts/deletes in the same transaction have chained through (e.g., row.item_count > 0) |
See the min_cardinality example in the LogicBank repo for a complete, runnable worked example (Order/OrderDetail, with regression tests for insert-with-items, insert-without-items, and delete-last-item).