Course / Writing data / Lesson 25

Lesson 25 · Writing data

sys_fields, hierarchical, auto_counter

Declare the three table options that add columns and the field triggers that fill them.

about 5 minutes · screenplay estimate8 scenesPrerequisites: lessons 4, 21, 22

Development course · Reading edition · API compatibility

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".