Course / The model / Lesson 12

Lesson 12 · The model

Extending the grammar

Give one table a column grammar with extra validated attributes, mounted through `x_config`.

about 4 minutes · screenplay estimate7 scenesPrerequisites: lessons 03 and 11

Development course · Reading edition · API compatibility

Scene 01

Layers and grammars

Lesson 11 composed layers. A layer gives values to paths of the model. It cannot add an argument that the grammar does not declare. A mounted grammar does something else. It changes the vocabulary below one node: which elements exist there, which arguments they accept, and their defaults. This lesson mounts a column grammar on one table. The new arguments describe how an external export tool treats each column. asqueel stores and validates them. Lesson 13 reads them back.

Example & screen notes

Slide title "Two different mechanisms". Table:

Mechanism Changes
Layer (parents=[...]) values at matching paths
Mounted grammar (x_config) which elements and arguments are valid below one node

Scene 02

A column grammar

An element is a method decorated with element. Its signature is the list of accepted arguments, with their types and defaults. The body is empty. This class redeclares column. parent_tags says that the element lives in a columns collection. sub_tags allows a relation below it. The meta flag projects_column tells asqueel that this element is a physical column. The flag projects_relation lets its relation become a foreign key. Without these flags, asqueel would not treat the element as a column. The signature keeps a few standard arguments and adds two. export_name is an optional string. export_mode accepts three values, and its default is plain.

Example & screen notes

Slide title "ExportColumns". Code:

from typing import Literal
from genro_builders.builder import element
from asqueel import SqlDatabaseConfig


class ExportColumns(SqlDatabaseConfig):
    @element(parent_tags='columns', sub_tags='relation',
             _meta={'projects_column': True, 'projects_relation': True})
    def column(self, name: str, dtype: str | None = None, size: str | None = None,
               notnull: bool = False, unique: bool = False, indexed: bool = False,
               name_long: str | None = None,
               export_name: str | None = None,
               export_mode: Literal['plain', 'masked', 'omit'] = 'plain'):
        """A physical column with hints for an external export tool."""
        ...

Highlight _meta, then export_name, then export_mode.

Scene 03

The mount point

The grammar is carried by a small class, ExportTable, in its attribute grammar. The recipe redeclares table. The meta entry subbuilder, x_config colon grammar, means: when a table receives x_config, read its grammar attribute and use that grammar below the table. ExportInvoicing derives from the course recipe. Its main method calls the course main, then declares one more table. The redeclared table replaces the standard signature for every table of this recipe. It must list every argument those tables use. The course tables use name, pkey, name_long and caption_field.

Example & screen notes

Slide title "x_config and grammar". Code:

class ExportTable:
    grammar = ExportColumns


class ExportInvoicing(Invoicing):
    @element(parent_tags='tables', _meta={'subbuilder': 'x_config:grammar'})
    def table(self, name: str, pkey: str | None = None, name_long: str | None = None,
              caption_field: str | None = None, x_config: type | None = None):
        """The tables of this recipe; x_config mounts a table grammar."""
        ...

    def main(self, root):
        super().main(root)
        contact(root.get_node('db.packages.invc'))

Scene 04

A table that uses the grammar

The contact table passes x_config equal to ExportTable. Its columns accept export_name and export_mode. The export arguments do not reach the database. The DDL has the usual columns, the primary key, the index and the foreign key of the relation. Queries work as usual. at customer_id dot account_name reads the customer. From the customer, at contacts dot aggr counts its contacts. The other tables of the course model have no x_config. They keep the standard column grammar.

Example & screen notes

Slide title "The contact table". Code:

def contact(pkg):
    tbl = pkg.table('contact', pkey='id', name_long='Contact', x_config=ExportTable)
    tbl.column('id', size='22')
    tbl.column('customer_id', size='22', export_name='customer').relation(
        'customer.id', relation_name='contacts', onDelete='cascade')
    tbl.column('full_name', export_name='name')
    tbl.column('email', export_mode='masked')
    tbl.column('phone')
    tbl.column('internal_note', export_mode='omit')

DDL from migration_plan():

CREATE TABLE "invc"."invc_contact"(
 "id" character(22) NOT NULL,
 "customer_id" character(22),
 "full_name" text,
 "email" text,
 "phone" text,
 "internal_note" text,
 PRIMARY KEY ("id")
);
CREATE INDEX "idx_2bce0070" ON "invc"."invc_contact" USING btree ("customer_id");
ALTER TABLE "invc"."invc_contact"
 ADD CONSTRAINT "fk_0946d7e4" FOREIGN KEY ("customer_id") REFERENCES "invc"."invc_customer" ("id") ON UPDATE CASCADE;

Scene 05

Validation

The mounted grammar is closed, like the standard one. A value outside the Literal raises. An argument that the signature does not declare raises. The contact columns have no sqlname, because ExportColumns does not declare it. The narrowed signature is a choice of this example. On customer, the standard grammar applies, so export_mode raises there. Every error appears when AsqueelDb builds the model.

Example & screen notes

Slide title "What the mounted grammar refuses". Table:

Declaration Error
column('email', export_mode='hidden') on contact ValueError: Validation failed: 'export_mode': expected typing.Literal['plain', 'masked', 'omit'], got str
column('email', export_label='x') on contact ValueError: Element 'column' does not accept 'export_label'. Declared attributes: dtype, export_mode, export_name, ...
column('email', sqlname='mail') on contact ValueError: Element 'column' does not accept 'sqlname'. ...
column('email', export_mode='masked') on customer ValueError: Element 'column' does not accept 'export_mode'. ...

Scene 06

Limits

asqueel does not give meaning to a new argument. The compiler and migration ignore export_name and export_mode. A new element is stored and validated, and nothing executes it. The narrowed column also has consequences. sys_fields generates columns with arguments like group and read_only. ExportColumns does not declare them, so sys_fields on the contact table raises. That is why contact declares its id column itself. Formula columns still work under the mounted grammar, because ExportColumns derives from the full recipe grammar. Last, declare the overriding elements in the class you pass to AsqueelDb. With genro-builders 0.29, a further subclass of ExportInvoicing loses the redeclared table.

Example & screen notes

Slide title "Limits". Four bullets:

  • New elements and arguments are stored and validated; the compiler and migration do not execute them
  • options().sys_fields() on a table with this grammar raises: the generated columns carry group, read_only, x_generation
  • Under the mounted grammar, a formula column is accepted: tbl.formulaColumn(...)
  • Declare the overriding elements in the class passed to AsqueelDb

Scene 07

Recap

A grammar class redeclares an element and its accepted arguments. The subbuilder meta mounts it below the tables that pass x_config. asqueel validates and stores the new arguments, and the application decides what they mean. The next lesson reads them back from the model.

Example & screen notes

Three bullets:

  • A grammar class redeclares an element with @element(parent_tags=..., _meta={'projects_column': True})
  • x_config with _meta={'subbuilder': 'x_config:grammar'} mounts it below one table
  • asqueel stores and validates the new arguments; it does not execute them