Course / Writing data / Lesson 26

Lesson 26 · Writing data

recordToUpdate and batchUpdate

Read rows with a lock and write them back through the write cycle with two table helpers.

about 4 minutes · screenplay estimate7 scenesPrerequisites: lessons 19, 20, 21

Development course · Reading edition · API compatibility

Scene 01

The pattern the helpers replace

Lesson 20 showed that update never reads the row. When you need the old values, you write three steps. You read the row with for update. You keep a copy as the old record. You change another copy and pass both to update. These three steps are the safe way to change a stored row. Two table helpers do them for you. Record to update works on one record. Batch update works on the rows a query selects.

Example & screen notes

code from lesson 20, with the three steps numbered in the margin.

row = ticket.query(columns='$num,$subject', where='$num=1', for_update=True).fetchRecord()  # 1
old = dict(row)                                                                            # 2
ticket.update(dict(old, subject='first, edited'), old)                                     # 3

Bullets: 1. read the row with a lock 2. keep a copy as old_record 3. change another copy and pass both to update

Scene 02

recordToUpdate: one record

Record to update is used in a with statement. On entry it reads the record with for update, and it keeps a copy as the old record. The with statement gives you the record. You change it in the block. On exit, the helper calls update with the record and the old record. So every hook, field trigger and relation action of lesson 21 runs, and each of them receives the old record.

Example & screen notes

code.

with invoice.recordToUpdate(invoice_id) as record:
    record['total'] = 120

Diagram below, three boxes left to right: "enter: read with for_update, copy as old_record" → "your block changes record" → "exit: update(record, old_record)".

Scene 03

Delete, and errors in the block

The same helper can delete. Set the key of the record to False inside the block. On exit, the helper then calls delete with the old record, and the delete cycle runs. When the block raises an exception, the helper writes nothing. The exception reaches the caller. The plan also gives record to update a branch that inserts a missing record. That branch creates a new record from the table defaults. This course does not cover it.

Example & screen notes

code, then two bullets.

with invoice.recordToUpdate(invoice_id) as record:
    record['id'] = False
  • the key set to False → delete(old_record) on exit
  • an exception in the block → nothing is written

Scene 04

batchUpdate: the selected rows

Batch update runs a query with the parameters you pass, such as where and its values. The rows are read with for update. For each row, it builds a copy and applies the updater. The updater is a dictionary merged into the row, or a callable that changes the row in place. When the callable returns False, the row is skipped. Then batch update calls update with the new row and the old row. So each row runs the full update cycle. The result is the list of the keys that were updated.

Example & screen notes

two calls.

invoice.batchUpdate({'vat_total': 0}, where='$customer_id = :c', c=customer_id)
def apply_vat(row):
    if row['total'] is None:
        return False
    row['vat_total'] = row['total'] * Decimal('0.20')
    row['gross_total'] = row['total'] + row['vat_total']

keys = invoice.batchUpdate(apply_vat, where='$date >= :d', d=date(2026, 1, 1))

Bullets: - updater: a dict merged into each row, or a callable that changes the row - a callable that returns False skips the row - returns the keys of the updated rows

Scene 05

The variant with the sql_* commands

The plan gives both helpers a variant that ends with the sql commands of lesson 24, instead of the full cycle. The read with a lock stays the same. The write then runs no hooks. The plan does not fix the name of the argument that selects the variant, so this lesson does not show it.

Example & screen notes

slide with two rows.

helper default variant
recordToUpdate update, delete sql_update, sql_delete
batchUpdate update sql_update

Scene 06

Logical deletion is an update

asqueel has no command for logical deletion. A row is logically deleted when its deletion column holds a timestamp. With system fields, that column is underscore underscore del TS. To delete rows logically, update that column, for example with batch update. The update cycle runs for each row, so the hooks run and the modification timestamp changes. Queries then leave those rows out, as lesson 18 explained. To restore a row, set the column back to None. The delete command always removes the row from the table.

Example & screen notes

code.

now = datetime.now(timezone.utc)
invoice.batchUpdate({'__del_ts': now}, where='$customer_id = :c', c=customer_id)

Bullets: - __del_ts is the logicalDeletionField of sys_fields - the update cycle runs: hooks, __mod_ts, __mod_user - queries then exclude the rows; delete() always removes the row

Scene 07

Recap

Record to update reads one record with a lock and writes it back at the end of the with block. Batch update does the same for every row of a query and returns the keys. Logical deletion is an update of the deletion column. The next chapter covers transactions and connections.

Example & screen notes

three bullets. - recordToUpdate: read with a lock, change in a with block, update (or delete) on exit - batchUpdate(updater, where=...): the same for every selected row; returns the keys - logical deletion: an update of the deletion column

Next: "Lesson 27 — Transactions and connections".