How to use quma¶
quma reads sql files from the file system and makes them accessible as script objects. It passes the content of the files to a connected database management system (DBMS) when these objects are called.
Throughout this document we assume a directory with the following structure:
Scripts │ Content
───────────────────────│─────────────────────────────────────────
path/to/sql/scripts │
├─ users │
│ ├─ all.sql │ SELECT * FROM users
│ ├─ by_city.sql │ SELECT * FROM users WHERE city = :city
│ ├─ by_id.sql │ SELECT * FROM users WHERE id = :id
│ ├─ remove.sql │ DELETE FROM users WHERE id = :id
│ └─ rename.sql │ UPDATE TABLE users
│ │ SET name = :name WHERE id = :id
└─ get_admin.sql │ SELECT * FROM users WHERE admin = 1
After initialization you can run these scripts by calling members of a
Database or a Cursor instance. Using the example above the
following members are available:
# 'cur' is a cursor instance
cur.users.all(...
cur.users.by_city(...
cur.users.by_id(...
cur.users.rename(...
cur.users.remove(...
cur.get_admin(...
Read on to see how this works.
Opening a connection¶
To connect to a DBMS you need to instantiate an object of the Database class
and provide a connection string and either a single path or a list
of paths to your SQL scripts.
from quma import Database
db = Database('sqlite3:///:memory:', '/path/to/sql-scripts')
Note: Database instances are threadsafe.
For more connection examples (e. g. PostgreSQL or MySQL/MariaDB) and parameters see Connecting. quma also supports connection pooling.
From now on we assume an established connection in the examples.
Creating a cursor¶
DBAPI libs like psycopg2 or sqlite3 have the notion of a cursor, which is used to manage the context of a fetch operation. quma is similar in that way. To execute queries you need to create a cursor instance.
quma provides two ways to create a cursor object. Either by using a context manager:
with db.cursor as cur:
...
Or by calling the cursor method of the Database instance:
try:
cur = db.cursor()
finally:
cur.close()
Running queries¶
To run the query in a sql script from the path(s) you passed to the Database constructor
you call members of the Database instance or the cursor (db and cur from now on).
Scripts and directories at the root of the path are translated to direct members of db
or cur. After initialisation of our example dir above, the script /get_admin.sql is
available as Script instance db.get_admin or cur.get_admin
and the directory /users as instance of Namespace, i. e. db.users or
cur.users. Scripts in subfolders will create script objects as members of the corresponding
namespace: /users/all will be db.users.all or cur.users.all.
When you call a Script object, as in cur.user.all() where all is the mentioned object,
you get back a Query instance. The simplest use is to iterate over it (see below for
more information about the Query class):
with db.cursor as cur:
all_users = cur.users.all()
for user in all_users:
print(user['name'])
The same using the db API:
with db.cursor as cur:
all_users = db.users.all(cur)
To learn what you can do with Query objects see The Query class.
Note
As you can see cur provides a nicer API where you don’t have to pass the cursor when you call a script or a method. Then again the db API has the advantage of being around 30% faster. But this should only be noticeable if you run hundreds or thousands of queries in a row for example in a loop.
If you have cloned the quma repository
from github you can see the difference when you run the script
bin/cursor_vs_db.py.
Committing changes and rollback¶
quma does not automatically commit by default. You have to manually
commit all changes as well as rolling back if an error occurs using
the commit() and rollback() methods of the cursor.
try:
cur.users.remove(id=13).run()
cur.users.rename(id=14, name='New Name').run()
cur.commit()
except Exception:
cur.rollback()
If db is initialized with the flag contextcommit set to True
and a context manager is used, quma will automatically commit when the
context manager ends. So you don’t need to call cur.commit(). If the
block raises an exception, quma rolls back instead.
db = Database('sqlite3:///:memory:', contextcommit=True)
with db.cursor as cur:
cur.users.remove(id=13).run()
cur.users.rename(id=14, name='New Name').run()
# no need to call cur.commit()
Note: quma also rolls back when the block raises an exception that
signals success, such as a redirect raised as Pyramid’s HTTPFound.
Commit before raising it:
with db.cursor as cur:
cur.users.rename(id=14, name='New Name').run()
cur.commit()
raise HTTPFound(location='/users/14')
Note: If you are using MySQL or SQLite some statements will automatically cause a commit. See the MySQL docs and SQLite docs
Autocommit¶
If you pass autocommit=True when you initialize a cursor, each query
will be executed in its own transaction that is implicitly committed.
with db(autocommit=True).cursor as cur:
cur.users.remove(id=13).run()
try:
cur = db.cursor(autocommit=True)
cur.users.remove(id=13).run()
finally:
cur.close()
To make every cursor of a database work like that, pass autocommit=True
to Database. Statements then commit on their own, and
transaction blocks group the ones that belong together:
db = Database('psycopg2+pool://user:secret@localhost/app', sqldir,
autocommit=True)
with db.cursor as cur:
user = cur.users.by_id(id=13).one()
with cur.transaction():
cur.users.rename(id=13, name='New Name').run()
cur.users.log_rename(id=13).run()
An exception no longer undoes statements that ran outside a block, so it
is a choice for an application as a whole. It suits new applications, and
the default suits code that relies on commit().
Transaction blocks¶
The method transaction() of a cursor runs a block atomically. It
commits at the end of the block and rolls back if an exception leaves it:
with db.cursor as cur, cur.transaction():
user = cur.users.by_id(id=13).one()
cur.users.rename(id=13, name='New Name').run()
A block inside another block uses a savepoint: an exception that leaves it undoes only its own work, and the outermost block still decides about the whole transaction. So a function can use a block whether or not its caller has one:
def reserve(cur, item):
with cur.transaction():
cur.stock.reduce(item=item).run()
cur.stock.log(item=item).run()
with db.cursor as cur, cur.transaction():
cur.orders.add(item=1).run()
try:
reserve(cur, 1)
except StockError:
cur.orders.backorder(item=1).run()
Cursors on one carrier share a connection, so a block opened through one of them contains the statements of the others, and their blocks nest.
The outermost block owns its transaction, which has some consequences:
A block can’t start while a transaction is open that no block started, because it could neither commit nor undo that work on its own.
quma.APIErrortells you. Without autocommit, a statement opens a transaction: any statement on PostgreSQL, one that reads a table on MySQL and MariaDB, a write on SQLite. Start the block before the first statement, or use autocommit, where only blocks open transactions.commit()androllback()raisequma.APIErrorinside a block. Raise an exception to roll back. An exception that signals success, like Pyramid’sHTTPFound, rolls back too: raise it after the block.With
contextcommit, a block commits at its own end, and the cursor commits what ran after it. A carried cursor that ends inside another cursor’s block leaves the transaction to the block.Closing a cursor or returning its connection inside a block rolls the transaction back, and the end of the block raises
quma.APIError. Withpersist=Trueall cursors share one connection, so closing any of them does that.PostgreSQL aborts a transaction when a statement fails. If you catch that error in the block, the end of the block rolls back and raises
quma.APIErrorinstead of pretending to commit. To continue after a statement that may fail, wrap it in a nested block.MySQL and MariaDB commit on their own for some statements, like
CREATE TABLE, see the note above. That ends the transaction of a block: a nested block then fails at its end, an outermost one can’t tell.
Executing literal statements¶
Database instances provide the method execute(). You can pass
arbitrary sql strings. Each call will be automatically committed.
If there is a result it will be returned otherwise it returns None.
db.execute('CREATE TABLE users ...')
users = db.execute('SELECT * FROM users')
for user in users:
print(user.name)
If you want to execute statements in the context of a transaction
use the execute() method of the cursor:
with db.cursor as cur:
cur.execute('DELETE FROM users WHERE id = 13');
cur.commit()
Both pass the SQL to the driver unchanged, so they take the driver’s
placeholders. For ad hoc queries with :name placeholders use
cur.query() (see Passing parameters).
Accessing the DBAPI cursor and connection¶
The underlying DBAPI connection and cursor objects are available as
members of the cursor instance. The connection object as raw_conn
and the cursor as raw_cursor.cursor.
# The connection
cur.raw_conn.autocommit = True
dbapi_cursor = cur.raw_conn.cursor()
# The cursor
cur.raw_cursor.cursor.execute('SELECT * FROM users;')
users = cur.raw_cursor.cursor.fetchall()
# raw_cursor wraps the real cursor. This would work as well
cur.raw_cursor.execute('SELECT * FROM users;')
users = cur.raw_cursor.fetchall()
All members of the raw_cursor.cursor object are also available as members
of cur. Hence there should be no need to use it directly:
cur.execute('SELECT * FROM users;')
users = cur.fetchall()