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 carrygroup,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_configwith_meta={'subbuilder': 'x_config:grammar'}mounts it below one table- asqueel stores and validates the new arguments; it does not execute them