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
executemanyper run of records with the same columns; noRETURNING - one record:
RETURNINGof the values the database decides, as ininsert
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".