Scene 01
Three commands on the table
A table object has three commands for single-record writes. They are insert, update and delete. Each command works on one record. The record is a plain Python dictionary. Its keys are the column names of the model, written without the dollar sign. The dollar sign belongs to query expressions, not to records. Insert and update change the record you pass, in place. Then they return that same object. You do not get a new object or a result set. Delete returns nothing. None of the three commands commits. The write stays pending until you call commit on the database, as you saw in lesson 19 for reads with a lock.
Example & screen notes
slide "Three commands". Three signatures, one per line:
table.insert(record)
table.update(record, old_record=None, pkey=None)
table.delete(record_or_key)
Below, three bullets:
- record is a plain dict
- keys are column names, without $
- insert and update return the same object they received
Scene 02
insert: the record comes back filled
Here we insert a customer of the course model. The record has a key called full address. In the model, full address is a formula column. A formula column has no physical column, so insert ignores that key. The same holds for alias columns, for keys that start with an at sign, and for keys that are not columns at all. They are not an error. They are simply not written. The email key holds None. Insert leaves None values out of the INSERT statement. The column then gets its database default, or NULL when it has none. After the call, the record holds new keys. The id was generated. The timestamps and the user columns come from the system fields of the table. Lesson 25 explains them. The point here is that the record you passed is the record that was written.
Example & screen notes
code on the left, output on the right. The line result = customer.insert(record) is highlighted. The rows of state, region and customer_type were inserted before.
db = AsqueelDb(Invoicing)
customer = db.table('invc.customer')
record = {'account_name': 'Acme', 'state': 'MA', 'customer_type_code': 'B2B',
'email': None, 'full_address': 'ignored'}
result = customer.insert(record)
assert result is record
{'account_name': 'Acme', 'state': 'MA', 'customer_type_code': 'B2B',
'email': None, 'full_address': 'ignored',
'id': 'Z0G7qW9ivPGjKnhar9FYwN',
'__ins_ts': datetime(2026, 10, 20, 7, 25, 49, tzinfo=utc),
'__mod_ts': datetime(2026, 10, 20, 7, 25, 49, tzinfo=utc),
'__ins_user': None, '__mod_user': None}
Bullets under the output:
- full_address is a formulaColumn: ignored
- email is None: left out of the INSERT
- id and the __ columns: added by the write
Scene 03
Key generation
Before the INSERT, the write checks the primary key. The method is called check pkey. It acts when the key is None or an empty string. When the key column declares an SQL default, the database assigns the key and nothing is done in Python. When the key is an integer, the next key is the current maximum plus one. This value is read without a lock. Two connections that insert at the same time can compute the same key, so this strategy is not safe under concurrency. When the key is a text column of size twenty-two, or a text column with no size, the key is a new twenty-two-character id. These ids sort in creation order. To choose your own keys, define new pkey value on a table subclass. You attach the subclass to the table with the x table class attribute. In the example, the region code becomes the first two letters of the name.
Example & screen notes
slide "When the key is missing". A decision list:
- key column declares
sql_default→ the database assigns it - integer key (
L,I,R) →max(key) + 1 - text key (
T,A,C) with size22,:22or no size → 22-character id - no rule applies → no value is assigned
Footnote: pkey_columns and __syscode have rules of their own, not covered here.
Below, the override:
class Region(SqlTable):
def newPkeyValue(self, record=None):
return record['name'][:2].upper()
tbl = pkg.table('region', pkey='code', name_long='Region', x_table_class=Region)
region.insert({'name': 'south west'}) -> {'name': 'south west', 'code': 'SO', ...}
Scene 04
Values decided by the database
Some values are decided by the database, not by Python. A column default, an SQL value expression and a generated column are examples. For these columns the INSERT ends with a RETURNING clause. The returned values are written into your record. In the example, the opened column has an SQL default. We do not pass it, so the database fills it, and the record shows it after the call. The num key was generated as the maximum plus one. Update does the same for SQL value expressions and generated columns. Only those columns are read back. The other columns of the record are what you passed and what the hooks changed. RETURNING works on PostgreSQL and on SQLite. SQLite must be version 3.35 or later.
Example & screen notes
slide "RETURNING". An addition to the course recipe:
def ticket(pkg):
t = pkg.table('ticket', pkey='num')
t.column('num', dtype='L')
t.column('subject')
t.column('opened', dtype='DH', sql_default='CURRENT_TIMESTAMP')
ticket = db.table('invc.ticket')
ticket.insert({'subject': 'first'})
# {'subject': 'first', 'num': 1, 'opened': datetime(2026, 10, 20, 9, 26, 26)}
Bullets:
- RETURNING reads back only the columns the database decides
- sql_default (when the value is missing), sql_value, sql_generatedExpression
- PostgreSQL and SQLite 3.35 or later
Scene 05
update: where the key comes from
Update takes the record, an optional old record and an optional key. The key of the WHERE clause comes from the old record first. If there is no old record, it comes from the pkey argument. If there is neither, it comes from the record itself. The SET clause contains only the columns present in the record, None included. So a partial record changes only its own columns. The second call shows this. It changes the subject of ticket two and nothing else. The third call changes the key itself. The old record names the row, and the new record carries the new key value.
Example & screen notes
slide "update(record, old_record=None, pkey=None)". Three numbered rows:
old_recordgiven → key ofold_record- else
pkeygiven →pkey - else → key in
record
Code below:
rec = ticket.query(columns='$num,$subject', where='$num=1', for_update=True).fetchRecord()
old = dict(rec)
new = dict(old, subject='first, edited')
ticket.update(new, old)
ticket.update({'subject': 'renamed'}, pkey=2)
ticket.update({'num': 20, 'subject': 'moved'}, old_record={'num': 2})
Scene 06
old_record is never read for you
Update never reads the row from the database. If you pass no old record, the hooks receive None as old record. If you need the old values, read the row yourself. Read it with for update equal to True, so that no other connection changes it before your write. Lesson 19 showed this lock. Then pass a copy of the row as old record, and change another copy. Another rule follows from the absence of a read. When no row matches the key, update raises no error. The UPDATE statement changes zero rows. Lesson 26 shows two helpers that do the read and the update for you.
Example & screen notes
slide with two bullets, the first highlighted:
- update does not read the row before writing
- read it yourself with for_update=True, then pass a copy as old_record
Small box at the bottom: "No matching row: no error, nothing changed."
Scene 07
delete: a key or a record
Delete accepts a key string or a record. With a key string, delete reads the row again with a lock. The read ignores the draft, logical deletion and partition filters. When the row is missing, delete returns without an error. With a dictionary, delete does not read the row. It uses the dictionary as it is, for the hooks and for the WHERE clause. The DELETE statement must remove exactly one row. Otherwise it raises record not found error. A table declared with deletable equal to False refuses every delete with a delete exception. After an error raised inside a write, the connection accepts only a rollback. Lesson 21 explains why.
Example & screen notes
two columns.
Left, "a key string":
ticket.delete('20') # reread with a lock, then deleted
ticket.delete('999') # no row: returns None, no error
Right, "a dict":
ticket.delete({'num': 999})
# RecordNotFoundError: invc.ticket: DELETE removed 0 rows instead of 1
Below, a declaration and its error:
t3 = pkg.table('vat_history', pkey='id', deletable=False)
DeleteException: The record 1 in table invc.vat_history cannot be deleted:
The records of this table cannot be deleted
Scene 08
Composite keys and Bag values
A composite key is a JSON string. It lists the members in the order of the composite column. Dates and decimals carry a type suffix, such as colon colon D for a date. Insert writes this string into the record, under the name of the composite column. Update and delete accept the same string as key. A last rule concerns Bag values. When a record value is a Bag, the write stores its XML text. An empty Bag is stored as NULL.
Example & screen notes
an addition to the recipe and its use.
def price_list(pkg):
t = pkg.table('price_list', pkey='pk')
t.column('product_code', size=':10')
t.column('valid_from', dtype='D')
t.column('price', dtype='money')
t.compositeColumn('pk', columns='product_code,valid_from')
pl.insert({'product_code': 'P1', 'valid_from': date(2026, 1, 1), 'price': 10})
# ... 'pk': '["P1", "2026-01-01::D"]'
pl.update({'product_code': 'P1', 'valid_from': date(2026, 1, 1), 'price': 12},
pkey='["P1", "2026-01-01::D"]')
pl.delete('["P1", "2026-01-01::D"]')
Bullet: "A Bag value is written as its XML text; an empty Bag as NULL."
Scene 09
Recap
Insert and update change and return the record you pass. Update takes its key from the old record, the pkey argument, or the record, and it never reads the row. Delete by key reads the row with a lock, and delete by record must remove exactly one row. The next lesson lists the steps that run inside each of these commands, in their order.
Example & screen notes
three bullets.
- insert and update change and return the caller's record
- update takes the key from old_record, pkey, then the record, and never reads the row
- delete by key rereads with a lock; by record it must remove exactly one row
Next: "Lesson 21 — The order of the write cycle".