Database Guide¶
EPL v9.8.0 has two database layers:
db_*: built-in SQLite helpers. This is the most directly supported local database workflow.real_db_*: additional adapters for PostgreSQL, MySQL, and SQLite when the required drivers and runtime environment are available.
For enterprise work, treat SQL schema management as an explicit migration concern. Use raw CREATE TABLE statements through db_execute or real_db_execute, keep values parameterized, and validate behavior in CI.
Level 1: SQLite With Built-in db_*¶
db = db_open("myapp.db")
db_execute(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, active INTEGER NOT NULL DEFAULT 1)")
db_execute(db, "INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "alice@example.com"])
users = db_query(db, "SELECT id, name, email, active FROM users ORDER BY id")
Say users
db_close(db)
Why this is the recommended production pattern:
- SQL migrations stay explicit and reviewable.
- Complex constraints remain under SQLite's own SQL parser.
- Parameters are bound safely.
- The same pattern maps cleanly to
real_db_*.
Inserts And Updates¶
db = db_open("myapp.db")
db_execute(db, "UPDATE users SET active = ? WHERE email = ?", [False, "alice@example.com"])
db_execute(db, "DELETE FROM users WHERE active = ?", [False])
db_close(db)
Use db_insert only for simple table inserts where table and column names are controlled by your code:
db = db_open("myapp.db")
user_id = db_insert(db, "users", Map with name = "Bob" and email = "bob@example.com")
db_close(db)
Queries¶
db = db_open("myapp.db")
rows = db_query(db, "SELECT id, name FROM users WHERE active = ?", [True])
first_user = db_query_one(db, "SELECT id, name FROM users WHERE email = ?", ["alice@example.com"])
If first_user == Nothing Then
Say "No user"
Otherwise
Say first_user.name
End
db_close(db)
db_create_table Boundary¶
db_create_table(db, name, columns) validates table names, column names, and type components, then builds the CREATE TABLE for you. It now accepts standard SQL column constraints and parameterized types directly in the column map — INTEGER PRIMARY KEY, TEXT NOT NULL, VARCHAR(255), DECIMAL(10,2), and so on (allowed type words include DECIMAL, FLOAT, DOUBLE, CHAR, and TIMESTAMP). Identifiers are still validated and SQL injection is still rejected:
db = db_open(":memory:")
db_create_table(db, "users", Map with id = "INTEGER PRIMARY KEY" and name = "TEXT NOT NULL" and email = "VARCHAR(255)" and balance = "DECIMAL(10,2)")
Say db_tables(db)
db_close(db)
For richer schemas — foreign keys, composite or partial indexes, DEFAULT expressions, and CHECK clauses — prefer raw SQL migrations:
db_execute(db, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)")
Rationale: standard column constraints are well supported by db_create_table, but foreign keys, indexes, and defaults are still easier to review and test as explicit, versioned SQL.
Transactions¶
SQLite transactions can be expressed directly:
db = db_open("bank.db")
Try
db_execute(db, "BEGIN")
db_execute(db, "UPDATE accounts SET balance = balance - ? WHERE id = ?", [100, 1])
db_execute(db, "UPDATE accounts SET balance = balance + ? WHERE id = ?", [100, 2])
db_execute(db, "COMMIT")
Catch error
db_execute(db, "ROLLBACK")
Throw "Transfer failed: $error"
End
db_close(db)
Indexes And Migrations¶
Keep migrations in functions so they can run during app startup and test setup.
Function migrate takes db
db_execute(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL)")
db_execute(db, "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
End
db = db_open("auth.db")
migrate(db)
For larger systems, version migrations in separate .epl files and run them in order.
Level 2: Additional Database Adapters¶
Use real_db_* helpers when the target driver and deployment environment are available.
db = real_db_connect("postgresql://user:pass@localhost:5432/mydb")
rows = real_db_query(db, "SELECT id, email FROM users WHERE active = ?", [True])
real_db_close(db)
SQLite with real_db_*:
db = real_db_connect("sqlite:///myapp.db")
real_db_execute(db, "CREATE TABLE IF NOT EXISTS jobs (id INTEGER PRIMARY KEY, status TEXT NOT NULL)")
jobs = real_db_query(db, "SELECT * FROM jobs")
real_db_close(db)
Validate these adapters in CI with the same database engine and driver versions used in production.
Level 3: ORM-Style Helpers¶
ORM helpers are available for scaffolding and application convenience. Treat them as a higher-level layer over SQL and validate generated schemas before production use.
db = orm_open("myapp.db")
orm_define_model(db, "User")
orm_add_field(db, "User", "name", "TEXT NOT NULL")
orm_add_field(db, "User", "email", "TEXT UNIQUE")
orm_migrate(db)
user = orm_create(db, "User", Map with name = "Alice" and email = "alice@example.com")
users = orm_find(db, "User")
WebApp Integration¶
Create WebApp called app
db = db_open("notes.db")
db_execute(db, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL)")
Route "/api/notes" responds with
notes = db_query(db, "SELECT id, title FROM notes ORDER BY id DESC")
Send json Map with ok = True and notes = notes
End
Route "/api/notes/create" responds with
title = trim(request_data.get("title", ""))
If length(title) == 0 Then
Send json Map with ok = False and error = "Title is required"
Otherwise
db_execute(db, "INSERT INTO notes (title) VALUES (?)", [title])
Send json Map with ok = True
End
End
Security Checklist¶
- Never concatenate user input into SQL.
- Use placeholders (
?) and parameter lists for all external values. - Keep table and column names static in application code.
- Use raw SQL migrations for constraints, foreign keys, indexes, and defaults.
- Enable database backups before running migrations in production.
- Run migration tests against an empty database and a database with existing data.
- Close connections in scripts and tests.
- Treat adapter URLs and credentials as secrets.
Function Reference¶
| Function | Description |
|---|---|
db_open(path) |
Open a SQLite database and return a connection ID |
db_close(db) |
Close a SQLite connection |
db_execute(db, sql[, params]) |
Execute SQL and return affected row count |
db_query(db, sql[, params]) |
Execute query and return all rows as maps |
db_query_one(db, sql[, params]) |
Execute query and return one row or Nothing |
db_insert(db, table, map) |
Insert a simple record and return row ID |
db_create_table(db, table, columns) |
Create simple table from map of column names to type strings |
real_db_connect(url) |
Connect to PostgreSQL/MySQL/SQLite when supported |
real_db_execute(db, sql[, params]) |
Execute SQL through an additional adapter |
real_db_query(db, sql[, params]) |
Query through an additional adapter |
real_db_query_one(db, sql[, params]) |
Query one row through an additional adapter |
real_db_close(db) |
Close an additional adapter connection |