Insert, Select, Update & Delete
All four take the table name as their first argument. The name is delimited for you, and a
dot separates schema from table: select('schema.table') produces FROM
`schema`.`table` on MySQL and FROM "schema"."table" on PostgreSQL.
Values always travel as driver parameters, never interpolated into the query text.
insert
insert(table, data) -> int | None # MySQL
insert(table, data, returning='id') -> int | None # PostgreSQL
data is a dictionary: the key is the column name, the value is the value to
insert.
uid = db.insert('users', {
'email': '[email protected]',
'active': True,
'meta': {'source': 'web'}, # dicts and lists are stored as JSON
})
It returns the generated id, or None if the table produces none.
On PostgreSQL the id comes from a RETURNING clause, so the column is your
choice:
pg.insert('users', {'email': '[email protected]'}, returning='user_id')
pg.insert('audit_log', {'message': 'x'}, returning=None) # table without an id
An empty data raises ValueError.
update
update(table, data, condition)
db.update('users', {'active': 0}, {'id': 7})
update('t', d, '') raises ValueError.
delete
delete(table, condition)
db.delete('users', {'id': 7})
db.delete('users', '1=1') # delete everything, explicitly
db.truncate('users') # or this
delete('t', '') raises ValueError, and delete('t',
300) raises TypeError. To delete every row, pass
'1=1' or use truncate().
select
select(table, condition="", fields="*", order="", *,
order_by=None, limit=None, offset=None,
group_by=None, having=None, for_update=False) -> list[dict]
Pass order= by name. Writing select('products', 'price < 100',
'ORDER BY price DESC') produces SELECT ORDER BY price DESC FROM products
WHERE price < 100.
db.select('users')
db.select('users', {'active': 1})
db.select('users', {'active': 1}, fields='id, email')
db.select('users', {'active': 1}, order='ORDER BY id DESC LIMIT 10')
pg.select('public.users', {'id': 1}) # schema.table, PostgreSQL
db.select('shop.users', {'id': 1}) # database.table, MySQL
The result is always a list of dictionaries, one per row:
[{'id': 1, 'email': '[email protected]', 'active': 1},
{'id': 2, 'email': '[email protected]', 'active': 1}]
No matches gives []. Iterate with a plain for:
for row in db.select('users'):
print(row['email'])
fields as a string and order are interpolated
raw — they are SQL, not data. Never build them from user input without
validating. The clause helpers and the keyword
clauses below cover the validated forms.
A list of fields is delimited as identifiers instead:
db.select('users', fields=['id', 'email']) # SELECT `id`, `email` FROM `users`
The ordering, grouping, paging and locking clauses are keyword arguments — see Query Builder:
db.select('products', cond, ['id', 'name'],
order_by=[('price', 'DESC'), 'name'], limit=20, offset=40)
select_one and row_exists
select_one(table, condition="", fields="*", **clauses) -> dict | None
row_exists(table, condition="") -> bool
user = db.select_one('users', {'email': '[email protected]'})
if user is None:
...
if db.row_exists('users', {'email': '[email protected]'}):
...
The limit is applied on the server. select_one() takes the same keyword clauses
as select() and sets limit=1 itself; passing the raw
order= fragment or a different limit raises
ValueError. Order with order_by=.
upsert
upsert(table, data, *, conflict_columns=None, update_columns=None,
returning=None) -> WriteResult
result = db.upsert('stock', {'sku': 'A-1', 'units': 10},
update_columns=['units'])
result.affected_rows
result.last_id
pg.upsert('stock', {'sku': 'A-1', 'units': 10},
conflict_columns=['sku'], returning='id')
| MySQL | PostgreSQL | |
|---|---|---|
| Clause | ON DUPLICATE KEY UPDATE | ON CONFLICT ... DO UPDATE |
conflict_columns | rejected — any unique key resolves the conflict | required |
affected_rows, inserted | 1 | 1 |
affected_rows, updated | 2 | 1 |
last_id | the generated id | None unless returning= is given |
update_columns defaults to every column that is not a conflict column. Naming a
conflict column there raises ValueError. On PostgreSQL,
returning= also works for non-integer keys such as a UUID.
On MySQL without update_columns, every column is updated. On a table with
more than one unique key that can raise IntegrityError. Name
update_columns for those tables.
insert_many and upsert_many
insert_many(table, rows, *, batch_size=1000) -> WriteResult
upsert_many(table, rows, *, conflict_columns=None, update_columns=None,
batch_size=1000) -> WriteResult
db.insert_many('events', rows, batch_size=1000)
db.upsert_many('stock', rows, update_columns=['units'])
rows is an iterable of dictionaries. Every row must be non-empty and carry the
same keys; key order does not matter, values are reordered to match. The whole input is
materialised and validated before the first statement is sent.
The write runs in a transaction — a savepoint when nested — and batch_size
splits it into several statements inside it. An empty input sends no SQL. Both return a
WriteResult whose affected_rows totals the batch and whose
last_id is None.
WriteResult
from easymysql import WriteResult
result.affected_rows # int
result.last_id # the id, or None
A frozen dataclass, independent of the cursor, returned by upsert(),
insert_many() and upsert_many(). insert(),
update() and delete() keep their previous return values.
Server-side expressions
from easymysql.expressions import increment, current_timestamp
db.update('counters', {'hits': increment('hits'), # hits = hits + 1
'seen': current_timestamp()},
{'id': 7})
db.insert('events', {'name': 'signup', 'created': current_timestamp()})
Values in insert(), update(), upsert() and the batch
writers can be one of these instead of a literal. increment() takes an optional
amount, which travels as a parameter, and is accepted only in an update;
insert() and upsert() raise ValueError for it.
current_timestamp() is accepted in both.
The condition: three accepted forms
| Form | Example | Safety |
|---|---|---|
dict | {'active': 1, 'role': 'admin'} | safe — values travel as parameters |
Condition | equals('active', 1) | safe — same, and it composes |
str | 'active = 1' | raw SQL, the caller's responsibility |
A dictionary joins its entries with AND, and None becomes IS
NULL:
db.select('users', {'active': 1, 'deleted_at': None})
# ... WHERE `active` = %s AND `deleted_at` IS NULL
Any other type raises TypeError.
Raw SQL
query(sql, params=None) -> list[dict] # returns rows
execute(sql, params=None) -> None # no result set
executemany(sql, params) # params: a sequence of tuples
execute_multiple(queries) # a list of strings, atomic
Both backends use %s as the placeholder:
db.query('SELECT * FROM users WHERE age > %s AND role = %s', (18, 'admin'))
db.executemany('INSERT INTO t (a, b) VALUES (%s, %s)', [(1, 'x'), (2, 'y')])
db.execute_multiple(['UPDATE a SET x=1', 'UPDATE b SET y=2']) # all or nothing
executemany() and execute_multiple() always run inside a
transaction, and take their own savepoint when nested in an outer one. An empty batch sends
no SQL. See Transactions.
Unlike insert / update / delete, there is no
implicit commit. Use it inside a transaction, or call commit() yourself.
While that work is pending, CRUD and the batch operations raise
RuntimeError.
Metadata
count() -> int # rows affected by the last successful operation
getLastId() -> int | None # id of the last successful insert
version() -> str # the easymysql version, NOT the server's
ping() -> bool
is_in_transaction() -> bool
affected_rows() -> int # alias of count()
get_last_id() # alias of getLastId()
reset_session() # alias of resetSession()
They describe the last operation that completed successfully. After a failed validation, a
failed statement, a failed fetch or an uncertain commit they are 0 and
None. Close, reset, reconnect and rollback clear them; a successful commit
keeps them.
For a batch, count() describes the last data statement of a successful batch
and getLastId() is None. executemany() keeps the
aggregate count the driver reports.
On MySQL, count() after an UPDATE counts
changed rows, not matched ones: updating with the same value returns
0. To count matches, connect with
client_flag=pymysql.constants.CLIENT.FOUND_ROWS.
count() == 0 and getLastId() is None mean the metadata is
unavailable, not that the server applied nothing.
Data types
| Python | Stored as |
|---|---|
bool | 1/0 on MySQL, boolean on PostgreSQL |
None | NULL |
int, float, Decimal | native numeric |
datetime, date | native date type |
bytes | binary |
dict, list | serialised to JSON |
float('nan'), float('inf') | ValueError |
On the way back a JSON column arrives as str: insert {'k':
1} and select gives you '{"k": 1}'.
Deserialise it yourself with json.loads.