The SQL engine (ADR-0014) runs procedures in two languages. Both are registered with CREATE PROCEDURE, invoked with CALL, run inside the caller's transaction, and replicate under Raft. Pick SQL text for plain statements, Cobra when you need real control flow.
| Language | Body | Toolchain | Best for |
|---|---|---|---|
| SQL text | AS BEGIN … END, re-parsed per CALL | None | A sequence of SQL statements |
| Cobra | Compiled .cobrac bytecode, run by the in-server VM | cobra build at author time | Loops, branches, locals, computed results |
The zero-toolchain path — the body is a block of SQL statements, re-parsed on every CALL. Parameters are referenced by name.
CREATE PROCEDURE give_raise(pct INT) AS BEGIN
UPDATE users SET salary = salary + salary * pct / 100;
INSERT INTO audit(event) VALUES ('raise applied');
END;
CALL give_raise(5);
CREATE OR ALTER PROCEDURE give_raise(pct INT) AS BEGIN
UPDATE users SET salary = salary + salary * pct / 100;
END; -- redefine in place
SHOW PROCEDURES; -- name, params, language, definition
DROP PROCEDURE give_raise;
Every statement a procedure runs is part of the surrounding transaction, so a failure rolls the whole thing back — the procedure is atomic with the work around it.
BEGIN;
CALL give_raise(5); -- its UPDATE + INSERT join this transaction
UPDATE settings SET last_raise = NOW();
COMMIT; -- all-or-nothing
Cobra is the compiled procedure language. You author a small program, compile it to portable .cobrac bytecode, and the server executes it on a built-in Rust VM — no toolchain on the server, no cgo, no sidecar process. The program defines run(db, …params); the db handle goes through the same executor as ordinary SQL.
Cobra is a full general-purpose language with its own docs, tooling, and playground at cobralang.baltavista.com.
db.execute(sql[, params]) runs a DML statement and returns the affected-row count; db.query(sql[, params]) runs a SELECT and returns a list of row dicts. Both join the CALL's transaction. Anything printed is returned to the client as notices.
# transfer.cobra — real logic, compiled to bytecode
def run(db, from_id, to_id, amount):
if amount <= 0:
raise "amount must be positive" # aborts the CALL + its transaction
db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", [amount, from_id])
db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", [amount, to_id])
rows = db.query("SELECT balance FROM accounts WHERE id = ?", [from_id])
print("remaining:", rows[0]["balance"]) # -> a notice
return rows # list of dicts -> a result set
# compile to portable bytecode, then base64 it
cobra build --portable transfer.cobra transfer.cobrac
B64=$(base64 -i transfer.cobrac)
-- register the compiled procedure (param types declared in SQL)
CREATE PROCEDURE transfer(from_id INT, to_id INT, amount DECIMAL)
LANGUAGE COBRA AS '<base64 of transfer.cobrac>';
CALL transfer(1, 2, 100); -- both UPDATEs + the SELECT run in ONE transaction
SHOW PROCEDURES; -- the language column reads COBRA
Cobra return | Wire result |
|---|---|
| list of dicts | A table (column union, first-seen order; missing keys → NULL) |
| a single dict | One row |
| a scalar / other list | A single value column |
| nothing / null | Empty result set |
Everything printed comes back as notices attached to the result, so a procedure can report progress without polluting its return set.
Cobra procedures are deterministic by construction. At CREATE time the bytecode is validated — async, imports, and every form of I/O (network, files, clocks, randomness, channels) are rejected. Each CALL runs under a 100M-instruction fuel cap, so a runaway procedure cannot stall the server. Because a procedure can only touch the database through db and has no other side effects, it produces identical results on every Raft node — CREATE, CALL, and DROP all replicate safely.
For the rest of the SQL surface — joins, CTEs, window functions, transactions — see the SQL Reference.