Database
Tova servers include built-in database support. SQLite is the default driver, with PostgreSQL and MySQL also available. Configuration is declarative, and query methods are available throughout your server code.
Configuration
SQLite (Default)
server {
db { path: "./data.db" }
}SQLite requires no external service -- the database is a single file. This is the recommended default for development and many production workloads.
PostgreSQL
server {
db {
driver: "postgres"
url: "postgres://user:pass@localhost:5432/mydb"
}
}MySQL
server {
db {
driver: "mysql"
url: "mysql://user:pass@localhost:3306/mydb"
}
}TIP
Store database connection strings in environment variables rather than hardcoding them. Use the env declaration to load them at startup:
env DATABASE_URL: String = "sqlite:./data.db"
db { url: DATABASE_URL }Query Methods
Tova provides four core query methods on the db object:
db.query
Execute a SELECT statement and return all matching rows as an array:
users = db.query("SELECT * FROM users WHERE age > ?", 18)
active = db.query("SELECT * FROM users WHERE active = ? AND role = ?", true, "admin")db.get
Execute a SELECT statement and return a single row (or nil if no match):
user = db.get("SELECT * FROM users WHERE id = ?", id)db.run
Execute an INSERT, UPDATE, or DELETE statement. Returns metadata about the operation (e.g., rows affected, last insert ID):
db.run("INSERT INTO users (name, email) VALUES (?, ?)", name, email)
db.run("UPDATE users SET active = ? WHERE id = ?", false, user_id)
db.run("DELETE FROM users WHERE last_login < ?", cutoff_date)db.exec
Execute raw SQL statements, typically for DDL (schema changes). Does not support parameterized values:
db.exec("CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)")Parameter Substitution
All parameterized methods (query, get, run) use ? placeholders to prevent SQL injection:
// Safe -- parameters are escaped
db.query("SELECT * FROM users WHERE name = ?", user_input)
// NEVER do this -- vulnerable to SQL injection
db.query("SELECT * FROM users WHERE name = '{user_input}'")Transactions
Wrap multiple operations in a transaction to ensure atomicity. If any statement fails, the entire transaction rolls back:
db.transaction(fn() {
db.run("INSERT INTO orders (user_id, total) VALUES (?, ?)", user_id, total)
db.run("UPDATE inventory SET count = count - 1 WHERE id = ?", item_id)
db.run("INSERT INTO audit_log (action) VALUES (?)", "order_placed")
})If the function completes without error, the transaction commits. If it throws, the transaction rolls back automatically.
Migrations
Migrations let you evolve your database schema over time in a controlled, versioned way.
Creating Migrations
Use the CLI to generate a new migration file:
tova migrate:create add_users_tableThis creates a timestamped migration file that you can fill in with your schema changes.
Running Migrations
Apply all pending migrations:
tova migrate:up app.tovaChecking Status
See which migrations have been applied and which are pending:
tova migrate:status app.tovaAuto-Migration on Startup
Run pending migrations automatically when the server starts:
server {
db { path: "./data.db" }
db.migrate()
}This is convenient for development. For production, running migrations explicitly via the CLI gives you more control.
Race Protection
A global async mutex is available for protecting critical sections from concurrent access. Use withLock to serialize access:
server {
fn update_counter() {
withLock(fn() {
count = db.get("SELECT count FROM counters WHERE id = 1")
db.run("UPDATE counters SET count = ? WHERE id = 1", count.count + 1)
})
}
}withLock ensures that only one invocation of the wrapped function runs at a time, even under concurrent requests. This prevents read-modify-write race conditions.
Practical Tips
Use transactions for multi-step writes. Any time you perform multiple related writes, wrap them in db.transaction. This prevents partial updates if something fails midway.
Prefer parameterized queries. Always use ? placeholders instead of string interpolation. This prevents SQL injection and handles type escaping correctly.
Run migrations explicitly in production. While db.migrate() on startup is convenient for development, production deployments benefit from running tova migrate:up as a separate step so you can inspect and control the migration process.