Skip to content

Database Queries (q:query)

q:query runs SQL against a datasource and gives you the rows. Every example with an Output on this page runs in the test suite against the example database below.

Declaring a datasource

Datasources live in quantum.config.yaml. SQLite needs only a file:

yaml
datasources:
  db:
    driver: sqlite
    database: ./data/app.db

PostgreSQL and MySQL take host, port, database, username and password. Keep secrets in environment variables with ${DB_PASSWORD} — see Installation.

The example database

sql
create table users (id integer primary key, name text, email text, status text);
insert into users (name, email, status) values
  ('Ana', 'ana@example.com', 'active'),
  ('Bruno', 'bruno@example.com', 'active'),
  ('Carla', 'carla@example.com', 'inactive');
create table products (id integer primary key, name text, price real, stock integer);
insert into products (name, price, stock) values
  ('Notebook', 3500.0, 5), ('Mouse', 80.0, 40), ('Monitor', 1200.0, 0);
create table orders (id integer primary key, user_id integer, total real);

Reading rows

xml
<q:component name="ActiveUsers" xmlns:q="https://quantum.lang/ns">
  <q:query name="users" datasource="db">
    SELECT name, email FROM users
    WHERE status = :status
    ORDER BY name
    <q:param name="status" value="active" type="string" />
  </q:query>

  <q:return value="{users}" />
</q:component>

Output: [{"name": "Ana", "email": "ana@example.com"}, {"name": "Bruno", "email": "bruno@example.com"}]

  • users is the list of rows, each row a record: {users[0].name}.
  • Every :name in the SQL is bound to the q:param of the same name — the value is never pasted into the SQL, so form input cannot inject SQL. A :name without a q:param is a parse error.
  • type on q:param is string, integer, decimal or boolean.

Looping over the rows

xml
<q:component name="UserList" xmlns:q="https://quantum.lang/ns">
  <q:query name="products" datasource="db">
    SELECT name, stock FROM products ORDER BY id
  </q:query>

  <q:loop query="products">
    <q:return value="{products.name}: {products.stock}" />
  </q:loop>
</q:component>

Output: ["Notebook: 5", "Mouse: 40", "Monitor: 0"]

In a page, the same loop renders HTML: <ul><q:loop query="products"><li>{products.name}</li></q:loop></ul>. <q:loop items="{products}" var="p"> works too, with {p.name}.

One row

When the query returns exactly one row, its fields are also available directly:

xml
<q:component name="One" xmlns:q="https://quantum.lang/ns">
  <q:query name="user" datasource="db">
    SELECT name, email FROM users WHERE id = :id
    <q:param name="id" value="2" type="integer" />
  </q:query>

  <q:return value="{user.name} &lt;{user.email}&gt;" />
</q:component>

Output: "Bruno <bruno@example.com>"

Query metadata

<name>_result describes the query:

FieldMeaning
successthe query ran
recordCountrows returned
columnListcolumn names
executionTimemilliseconds
affectedRowsrows changed by INSERT / UPDATE / DELETE
lastInsertIdid of the row an INSERT created

result="meta" gives the same object another name.

xml
<q:component name="Count" xmlns:q="https://quantum.lang/ns">
  <q:query name="users" datasource="db">SELECT id, name FROM users</q:query>
  <q:return value="{users_result.recordCount} rows, columns {join(users_result.columnList)}" />
</q:component>

Output: "3 rows, columns id, name"

Writing

xml
<q:component name="Stock" xmlns:q="https://quantum.lang/ns">
  <q:query name="restock" datasource="db">
    UPDATE products SET stock = stock + :amount WHERE stock &lt; :limit
    <q:param name="amount" value="10" type="integer" />
    <q:param name="limit" value="10" type="integer" />
  </q:query>

  <q:query name="placed" datasource="db">
    INSERT INTO orders (user_id, total) VALUES (:user, :total)
    <q:param name="user" value="1" type="integer" />
    <q:param name="total" value="80.0" type="decimal" />
  </q:query>

  <q:return value="{restock_result.affectedRows} restocked, order {placed_result.lastInsertId}" />
</q:component>

Output: "2 restocked, order 1"

In SQL inside a .q file, write < as &lt; — the file is XML.

A query that fails — bad SQL, a missing table, a datasource that is not declared — stops the component with the database's message:

xml
<q:component name="Failure" xmlns:q="https://quantum.lang/ns">
  <q:query name="x" datasource="db">SELECT * FROM clients</q:query>
</q:component>

Error: no such table: clients

Pagination

xml
<q:component name="Pages" xmlns:q="https://quantum.lang/ns">
  <q:query name="page" datasource="db" paginate="true" page="2" page_size="2">
    SELECT name FROM products ORDER BY id
  </q:query>

  <q:return value="{page[0].name} (page {page_result.pagination.currentPage} of {page_result.pagination.totalPages})" />
</q:component>

Output: "Monitor (page 2 of 2)"

page_result.pagination also has totalRecords, pageSize, hasNextPage, hasPreviousPage, startRecord and endRecord. In a page, take the page number from the URL: page="{query.page}".

Query of queries

source= runs SQL in memory over the rows of an earlier query, which appears as a table with its name:

xml
<q:component name="Summary" xmlns:q="https://quantum.lang/ns">
  <q:query name="all" datasource="db">SELECT name, status FROM users</q:query>

  <q:query name="byStatus" source="all">
    SELECT status, COUNT(*) AS total FROM all GROUP BY status ORDER BY status
  </q:query>

  <q:return value="{byStatus}" />
</q:component>

Output: [{"status": "active", "total": 2}, {"status": "inactive", "total": 1}]

Transactions

Inside q:transaction, the queries use its datasource. If any of them fails, everything is undone:

xml
<q:component name="Order" xmlns:q="https://quantum.lang/ns">
  <q:transaction datasource="db">
    <q:query name="order">
      INSERT INTO orders (user_id, total) VALUES (1, 3500.0)
    </q:query>
    <q:query name="stock">
      UPDATE products SET stock = stock - 1 WHERE name = 'Notebook'
    </q:query>
  </q:transaction>

  <q:query name="left" datasource="db">SELECT stock FROM products WHERE name = 'Notebook'</q:query>
  <q:return value="{left.stock}" />
</q:component>

Output: 4

Change history

Turn it on per datasource:

yaml
datasources:
  db:
    driver: sqlite
    database: ./data/app.db
    history: true

Every write an action makes — INSERT, UPDATE, DELETE, and edits in an editable table — is recorded in a quantum_history table of the same database: when, who (the session's userName), which action, which row, and the row before and after. It is written in the same transaction as the change, so a rolled-back write leaves no trace.

Show a row's history on any page:

xml
<ui:history table="posts" key="{post.id}" datasource="db" />
WhenWhoActionChange
2026-09-24 10:02:11anarenametitle: First → First!

Page statements and migrations are not recorded — only actions change records. SQLite for now.

Not available

Earlier versions of this page described cached, reactive and batch queries, maxrows, timeout, stored procedures (q:storedproc), q:try/q:catch, several statements in one query and array parameters for IN. None of them worked. The attributes are refused by the parser since 0.11, and unknown tags are an error.

Quantum Framework - Simplicity over configuration