Course / Writing data / Lesson 24

Lesson 24 · Writing data

Writes without triggers: sql_*, table.lock

Write rows with the `sql_*` commands and the query terminals, which run only SQL, and lock a table.

about 4 minutes · screenplay estimate8 scenesPrerequisites: lessons 14, 20, 21

Development course · Reading edition · API compatibility

Scene 01

Only the SQL

The commands of lesson 20 run the full write cycle. Sometimes you want only the SQL statement. Data loads, repairs and bulk changes are examples. For this, asqueel has the commands with the prefix sql underscore. The table has sql insert, sql update and sql delete. A query has two terminals, sql update and sql delete, that write the rows the query selects. None of them runs hooks, field triggers or relation actions. They do not enter the stack behind current trigger.

Example & screen notes

slide "sql_* commands". Two groups.

Table commands, one record:

table.sql_insert(record_or_list)
table.sql_update(record, old_record=None, pkey=None)
table.sql_delete(record)

Query terminals, the selected rows:

table.query(...).sql_update(values)   # -> row count
table.query(...).sql_delete()         # -> row count

Bullet: "No hooks, no field triggers, no relation actions, no currentTrigger."

Scene 02

sql_insert with one record or a list

Sql insert takes one record or a list of records. It returns what it received. Before writing, it assigns missing keys with the same rules as check pkey. So every note gets an id. No field trigger runs. That is why the insert timestamp of the system fields stays empty. Use sql insert when you control every value yourself.

Example & screen notes

code and output.

notes = [{'invoice_id': inv['id'], 'note_type': 'memo', 'note_text': f'n{i}', 'priority': i}
         for i in range(3)]
notes.append({'invoice_id': inv['id'], 'note_type': 'memo', 'note_text': 'no priority'})
result = note.sql_insert(notes)
assert result is notes
[n['id'][-4:] for n in notes]  ->  ['M3zP', 'Fp1k', 'iZiU', 'vhnX']
notes[0].get('__ins_ts')       ->  None

Scene 03

Keys and statements in a list

With an integer key, the first missing key is the maximum plus one. In a list, each further missing key is the next integer after the highest key seen so far. Here ticket d gets eleven, after the explicit ten. A list is written with executemany. Consecutive records with the same columns share one statement. A list gets no RETURNING, so values decided by the database are not copied back. A single record does get RETURNING, as with insert. The ticket table of lesson 20 shows it: the opened column comes back only in the single insert.

Example & screen notes

code and output, then two bullets.

ticket.sql_insert([{'subject': 'a'}, {'subject': 'b'},
                   {'num': 10, 'subject': 'c'}, {'subject': 'd'}])
[{'subject': 'a', 'num': 1}, {'subject': 'b', 'num': 2},
 {'num': 10, 'subject': 'c'}, {'subject': 'd', 'num': 11}]
ticket.sql_insert({'subject': 'single'})
# {'subject': 'single', 'num': 12, 'opened': datetime(2026, 10, 20, 9, 28, 23)}
  • a list: one executemany per run of records with the same columns; no RETURNING
  • one record: RETURNING of the values the database decides, as in insert

Scene 04

sql_update and sql_delete for one record

Sql update writes one record, with the same key rules as update. The key comes from the old record, then from the pkey argument, then from the record. Only the columns present in the record are written. Sql delete takes a record that holds the key. Both return None. They do not check how many rows changed. Sql delete runs no on delete action. A related row is then handled only by the foreign key in the database.

Example & screen notes

code.

note.sql_update({'id': first['id'], 'note_text': 'edited'})   # -> None
note.sql_delete({'id': notes[1]['id']})                        # -> None

Bullets: - sql_update: the key comes from old_record, then pkey, then record - sql_delete: a record holding the key columns

Scene 05

Query terminals

A query can write the rows it selects. Sql update takes a dictionary of values. Sql delete takes nothing. Both build one statement. The WHERE clause is the key, inside the same SELECT the query would run, reduced to the key columns. So the filters, the order and the limit of the query decide the rows. The default filters apply too. A logically deleted row is not written, unless the query says exclude logical deleted equal to False. A useful pattern is to fetch first, look at the rows, and then call sql delete on the same query. Both terminals return the number of rows written. A query with group by or having cannot be used for writing.

Example & screen notes

code and output.

n = note.query(where="$note_type='memo'").sql_update({'priority': 9})
# n == 3

q = note.query(columns='$id,$note_text', where='$priority = :p', p=9,
               order_by='$note_text', limit=1)
q.fetch()        # [[id=Z0G7qgZSlmykWCMZbzM3zP, note_text=edited]]
q.sql_delete()   # 1

SQL shape shown under the code:

DELETE FROM invc.invc_invoice_note WHERE (id) IN (<the SELECT of q, reduced to the key>)

Scene 06

What the commands skip

This table summarises the difference. The run that produced this lesson recorded every hook call of the note table. The list stayed empty after every sql command, and current trigger stayed None. Use the sql commands when the hooks would be wrong or too slow. Remember that the rules in the hooks are then your responsibility.

Example & screen notes

two-column table.

full cycle (insert, update, delete) sql_*
checkPkey keys assigned by sql_insert only
protections, field triggers, table hooks —
relation actions —
currentTrigger stays None
RETURNING copied into the record single-record sql_insert only

Footer: "Observed: hook calls [], currentTrigger None."

Scene 07

table.lock

Table lock takes a lock on the whole table, inside the current transaction. The default mode is access exclusive. You can pass any PostgreSQL table lock mode. With nowait, the statement fails at once when another transaction holds a conflicting lock. The lock ends with the commit or the rollback. SQLite has no table locks. There, lock takes the write lock of the database file, so other connections cannot write until the transaction ends.

Example & screen notes

code and the produced statements.

note.lock()                              # LOCK invc.invc_invoice_note IN ACCESS EXCLUSIVE MODE
note.lock(mode='SHARE', nowait=True)     # LOCK invc.invc_invoice_note IN SHARE MODE NOWAIT

Bullets: - mode: one of the eight PostgreSQL lock modes; default ACCESS EXCLUSIVE - nowait=True: fail at once instead of waiting - SQLite: takes the write lock of the database file

Scene 08

Recap

The sql commands run one statement and no hooks. The query terminals write the selected rows and return their count. Table lock holds a table lock until the transaction ends. The next lesson returns to the full cycle, with three table options that bring their own triggers.

Example & screen notes

three bullets. - sql_insert, sql_update, sql_delete: one statement, no hooks - query(...).sql_update(values) and query(...).sql_delete() write the selected rows and return the count - table.lock(mode='ACCESS EXCLUSIVE', nowait=False) locks the table until the end of the transaction

Next: "Lesson 25 — sys_fields, hierarchical, auto_counter".