Course / The model / Lesson 11

Lesson 11 · The model

Layered recipes

Build one model from several recipes applied in order, and keep the connection in a deployment recipe.

about 4 minutes · screenplay estimate6 scenesPrerequisites: lessons 03-09

Development course · Reading edition · API compatibility

Scene 01

Functions that declare tables

A recipe is Python code. The course model declares each table in a plain function. The function receives the package and declares one table. The main method of the recipe creates the package and calls the functions in a loop. Nothing else is needed to split a model into parts. A function can live in another module. Another recipe can call the same functions on its own package. The connection uses EnvResolver. The DSN is read from the environment variable ASQUEEL_DSN when the connection settings are read.

Example & screen notes

Slide title "A recipe made of functions". Code from the course model:

def region(pkg):
    tbl = pkg.table('region', pkey='code', name_long='Region')
    tbl.options().sys_fields(id=False)
    tbl.column('code', size=':5')
    tbl.column('name', size=':50')

...

class Invoicing(SqlDatabaseConfig):
    def main(self, root):
        db = root.db('course', implementation='postgres')
        db.connection(name='main', implementation='postgres',
                      dsn=EnvResolver('ASQUEEL_DSN', default='dbname=asqueel_course'))
        invc = db.packages().package('invc', sqlschema='invc', sqlprefix=True)
        ...
        for declare in (region, state, customer_type, vat_type, customer,
                        product, invoice, invoice_row, invoice_note):
            declare(invc)

Scene 02

parents=[...]

AsqueelDb accepts a recipe and a list of parents. Each one is a layer. The parents are applied first, in list order. The recipe passed as first argument is applied last. Each layer is a complete recipe class with its own main method. All layers are composed into one source before the model is built. The model is built once, from the composed source. After that, the model objects are the only truth. The recipes are not read again.

Example & screen notes

Slide title "AsqueelDb(Recipe, parents=[...])". Code:

db = AsqueelDb(Deployment, parents=[Invoicing, SiteLayer])

Diagram: three boxes stacked from bottom to top, "Invoicing", "SiteLayer", "Deployment", with an arrow labelled "applied in this order" pointing up to a box "one model".

Scene 03

A later layer changes matching paths

A later layer reaches a node by the same path: database, package, table, column. Here the path ends at the description column of the product. The layer gives a new size. The new value replaces the old one. The other attributes of the column stay. The migration plan alters the column from eighty to one hundred twenty characters. A path that does not exist yet adds a node. The formula net_due is new, so it is added to the invoice table. The layer also attaches a table class to the invoice, with x_table_class. The layer does not repeat sqlschema, pkey or the connection. Those attributes come from the earlier layer.

Example & screen notes

Slide title "A second layer". Code:

class Invoice(SqlTable):
    def sql_formula_net_due(self, attr):
        return '$gross_total - COALESCE($vat_total, 0)'


class SiteLayer(SqlDatabaseConfig):
    def main(self, root):
        invc = root.db('course').packages().package('invc')
        product = invc.table('product')
        product.column('description', size=':120')
        invoice = invc.table('invoice', x_table_class=Invoice)
        invoice.formulaColumn('net_due', sql_formula=True, dtype='N')

migration_plan() of the composed model:

ALTER TABLE "invc"."invc_product"
ALTER COLUMN "description" TYPE character varying(120);

Scene 04

A deployment layer holds the connection

The connection is part of the recipe. A deployment layer can replace it and keep the whole model. Deployment declares only the main connection. It is applied last, so its DSN wins over the one in Invoicing. The DSN is now read from COURSE_DSN, with a default. connection_settings shows the result of the composition. The application code stays the same on every machine. Only the last layer changes.

Example & screen notes

Slide title "Deployment". Code:

class Deployment(SqlDatabaseConfig):
    def main(self, root):
        root.db('course').connection(
            name='main', implementation='postgres',
            dsn=EnvResolver('COURSE_DSN', default='host=127.0.0.1 user=postgres dbname=asqueel_course_p2'))
from asqueel.configuration import connection_settings
connection_settings(db.config)
# ('postgres', 'host=127.0.0.1 user=postgres dbname=asqueel_course_p2', {})

Scene 05

What a layer cannot do

Inside one layer, an attribute can receive one value only. Declaring the same column twice with two sizes raises ValueError. Between layers, a later value replaces an earlier one. A later layer cannot change the kind of an element. Turning the physical column total into a formula raises ValueError. Every layer is checked against the grammar. An undeclared attribute raises in a layer as it does in a single recipe.

Example & screen notes

Slide title "Errors during composition". Table:

Case Error
Same layer, same attribute, two values ValueError: ...columns.id: conflicting 'size' in contribution 'main'
Later layer turns a column into a formula ValueError: ...invoice.total: conflicting element kinds 'column' and 'formulaColumn'
Undeclared attribute in any layer ValueError: Element ... does not accept ...

Scene 06

Recap

A model can be split into functions and composed from several recipes. Each later layer changes attributes at matching paths, and a deployment layer can hold the connection. A later layer cannot change the kind of an element, and every layer is checked against the grammar. The next lesson shows how to extend the grammar itself.

Example & screen notes

Three bullets:

  • Functions split a recipe; parents=[...] composes recipes, the last one wins
  • A later layer changes attributes at matching paths and adds new nodes
  • A layer cannot change the kind of an element; every layer is checked against the grammar