Scene 01
Options bring columns and triggers
Some table options change the write cycle. Each option declares its columns, puts field triggers on them, and provides the trigger methods on the base table class. You saw field triggers in lesson 22. This lesson covers three options: system fields, hierarchical, and auto counter. You declare them under the options of a table.
Example & screen notes
slide "Table options with write behaviour". Three rows.
| option | columns | trigger methods |
|---|---|---|
sys_fields(...) |
key, timestamps, users, deletion, draft | trigger_setTSNow, trigger_setCurrentUser |
hierarchical(...) |
parent_id, path columns, counters |
trigger_hierarchical_before, trigger_hierarchical_after |
auto_counter(...) |
_row_count |
trigger_setRowCounter |
Scene 02
sys_fields: the columns
System fields adds up to seven columns. Each parameter switches one column on or off. All are on by default, except draft. The id column becomes the primary key, a text of twenty-two characters, generated by check pkey as lesson 20 showed. The region table of the course model passes id equal to False, because its key is a code. The two timestamps have the type DHZ, a timestamp with time zone. The deletion column marks a logically deleted row. The draft column marks a draft row. These two have no trigger. They set table attributes that the read filters of lesson 18 use.
Example & screen notes
the signature, then a table.
tbl.options().sys_fields(id=True, ins=True, upd=True, user_ins=True,
user_upd=True, ldel=True, draft=False)
| parameter | column | type | trigger |
|---|---|---|---|
id |
id, primary key |
A, size 22 |
key generated by checkPkey |
ins |
__ins_ts |
DHZ |
onInserting='setTSNow' |
upd |
__mod_ts |
DHZ |
onInserting, onUpdating='setTSNow' |
user_ins |
__ins_user |
T |
onInserting='setCurrentUser' |
user_upd |
__mod_user |
T |
onInserting, onUpdating='setCurrentUser' |
ldel |
__del_ts |
DHZ |
— (table attribute logicalDeletionField) |
draft |
__is_draft |
B |
— (table attribute draftField) |
Scene 03
Timestamps and the current user
Set TS now writes the current time into the column. For a DHZ column, the time is in UTC. On insert both timestamps are set. On update only the modification timestamp changes. Set current user writes the user of the environment. The environment is the one of lesson 17. Its key user is read by db dot current user. Here we set it with temp env. Without a user in the environment, the user columns stay None. These triggers run only in the full cycle. The sql commands of lesson 24 leave the columns as you pass them.
Example & screen notes
code and output.
with db.temp_env(user='ada'):
hw = product_group.insert({'code': 'HW', 'description': 'Hardware'})
'__ins_ts': datetime(2026, 10, 20, 7, 28, 54, 946276, tzinfo=utc)
'__mod_ts': datetime(2026, 10, 20, 7, 28, 54, 946545, tzinfo=utc)
'__ins_user': 'ada'
Bullets:
- trigger_setTSNow: current time, in UTC for a DHZ column
- trigger_setCurrentUser: db.currentUser, the user key of the environment
- no user in the environment → None
Scene 04
hierarchical: the declaration
Hierarchical turns a table into a tree. The table needs the system field id as primary key. The option adds a parent id column, with a relation to the same table. That relation has on delete cascade, so deleting a node deletes its subtree. For each field you list, here code and row count, the option adds a path column named hierarchical underscore field, and the same path for the parent. The pkey path is always added. Underscore h count holds a sortable position path. Two virtual columns give the number of children and the level. With hierarchical, auto counter keeps the position of a node among its siblings in the row count column.
Example & screen notes
an addition to the course recipe, then the generated columns.
def product_group(pkg):
tbl = pkg.table('product_group', pkey='id', name_long='Product group',
caption_field='hierarchical_code')
tbl.options().sys_fields(user_upd=False)
tbl.options().hierarchical('code,_row_count')
tbl.options().auto_counter()
tbl.column('code', size=':10')
tbl.column('description', size=':50')
_row_count, parent_id,
hierarchical_code, _parent_h_code,
hierarchical__row_count, _parent_h__row_count,
hierarchical_pkey, _parent_h_pkey,
_h_count, _parent_h_count
virtual: child_count, hlevel
Scene 05
Inserting nodes
We insert two roots and a chain of two children. You pass only the code, the description and the parent. The trigger on parent id reads the parent row and writes the path columns. It also gives each node the next position among its siblings. It locks the parent row while it computes the position, so two connections do not get the same number. Each position is two base-36 digits in the h count path.
Example & screen notes
code and query output.
with db.temp_env(user='ada'):
hw = pg.insert({'code': 'HW', 'description': 'Hardware'})
sw = pg.insert({'code': 'SW', 'description': 'Software'})
disk = pg.insert({'code': 'DISK', 'description': 'Disks', 'parent_id': hw['id']})
ssd = pg.insert({'code': 'SSD', 'description': 'SSD', 'parent_id': disk['id']})
code hierarchical_code _row_count _h_count hlevel child_count
HW HW 1 01 1 1
DISK HW/DISK 1 0101 2 1
SSD HW/DISK/SSD 1 010101 3 0
SW SW 2 02 1 0
Scene 06
Moving a subtree
To move a node, update its parent id. Pass the old record, read with a lock. The hierarchical triggers need it. The before-trigger rewrites the paths of the node. The after-trigger updates each child with the update command, one child at a time. Each child then updates its own children. So the whole subtree gets new paths, and every child runs the full update cycle. Moving a node under itself or under one of its descendants raises a business logic exception. An update of a hierarchical row without old record raises an exception too.
Example & screen notes
code and query output.
old = dict(pg.query(columns=allcols, where='$id=:i', i=disk['id'], for_update=True).fetchRecord())
pg.update(dict(old, parent_id=sw['id']), old)
code hierarchical_code _h_count hlevel
HW HW 01 1
SW SW 02 1
DISK SW/DISK 0201 2
SSD SW/DISK/SSD 020101 3
Below, the refusal:
BusinessLogicException: The requested operation violates the internal business logic:
A node cannot be moved under itself or one of its descendants
Scene 07
auto_counter on its own
Auto counter also works without a hierarchy. It adds a counter column, row count by default, with the trigger set row counter. The scope names the columns that group the rows. In the example, the lines of each invoice are numbered from one. Without a scope, the whole table shares one counter. When the record already has a value, the trigger keeps it. Otherwise the value is the highest counter in the scope plus one. Rows with an empty counter are ignored in that computation. The trigger takes a lock before it reads the highest value, so two connections that insert in the same scope do not get the same number.
Example & screen notes
the signature and an addition to the recipe.
tbl.options().auto_counter(scope=None, field='_row_count')
def invoice_line(pkg):
t = pkg.table('invoice_line', pkey='id')
t.options().sys_fields()
t.options().auto_counter('invoice_id')
t.column('invoice_id', size='22')
t.column('text')
Bullets:
- field: the counter column, onInserting='setRowCounter'
- scope: the columns that group the rows; None counts over the whole table
- a value already in the record is kept
- the next value is the maximum in the scope plus one; NULL counters are ignored; a lock prevents two connections from taking the same value
Scene 08
Recap
System fields gives a generated key, UTC timestamps, the user of the environment, and the deletion and draft columns. Hierarchical keeps paths and positions through triggers on parent id, and a move rewrites the subtree. Auto counter numbers rows within a scope. The next lesson shows two helpers that read with a lock and then write.
Example & screen notes
three bullets.
- sys_fields: key, UTC timestamps, user from the environment, deletion and draft columns
- hierarchical: paths and positions maintained by triggers on parent_id; a move rewrites the subtree
- auto_counter(scope, field='_row_count'): max + 1 within the scope
Next: "Lesson 26 — recordToUpdate and batchUpdate".