Skip to content

Database Access (SQLite)

Frankie has built-in SQLite support via db_open() — zero external dependencies, works everywhere Python does.


Opening a Database

# In-memory database (great for testing)
db = db_open(":memory:")

# File-based database (persists to disk)
db = db_open("myapp.db")
db = db_open("/path/to/data.db")

Creating Tables

db.exec("CREATE TABLE IF NOT EXISTS users (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  name  TEXT    NOT NULL,
  email TEXT    UNIQUE,
  age   INTEGER
)")

Use triple-quoted strings for multi-line SQL:

sql = """
  CREATE TABLE IF NOT EXISTS products (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    name  TEXT    NOT NULL,
    price REAL    NOT NULL,
    stock INTEGER DEFAULT 0
  )
"""
db.exec(sql)

Inserting Data

Convenience insert with a hash

db.insert("users", {name: "Alice", email: "alice@example.com", age: 30})
db.insert("users", {name: "Bob",   email: "bob@example.com",   age: 25})

Raw SQL with?placeholders (safe from SQL injection)

db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
        ["Carol", "carol@example.com", 28])

Bulk insert

rows = [
  {name: "Dave",  email: "dave@x.com",  age: 35},
  {name: "Eve",   email: "eve@x.com",   age: 29},
  {name: "Frank", email: "frank@x.com", age: 42}
]
db.insert_many("users", rows)

Getting the new row ID

id = db.insert("users", {name: "Grace", email: "g@x.com", age: 31})
puts "New user ID: #{id}"
# or
db.insert("users", {name: "Hector", email: "h@x.com", age: 27})
puts "Last ID: #{db.last_id}"

Querying Data

Get all rows from a table

users = db.find_all("users")
users.each do |u|
  puts "#{u["name"]} (#{u["age"]})"
end

Filter by conditions (hash → AND clauses)

devs = db.find("users", {department: "Engineering"})
puts "Engineers: #{devs.length}"

Find a single row (returns nil if not found)

alice = db.find_one("users", {name: "Alice"})
if alice != nil
  puts "Found: #{alice["name"]}, age #{alice["age"]}"
end

Raw SQL queries

# Parameterised — always use ? placeholders for user input
results = db.query(
  "SELECT name, age FROM users WHERE age > ? ORDER BY age DESC",
  [25]
)

results.each do |row|
  puts "#{row["name"]}: #{row["age"]}"
end

Single-row raw query

row = db.query_one("SELECT COUNT(*) AS n, AVG(age) AS avg FROM users")
puts "Count: #{row["n"]}, Avg age: #{row["avg"]}"

Counting Rows

puts db.count("users")                          # all rows
puts db.count("users", {department: "Finance"}) # filtered

Updating Data

updated = db.update("users", {age: 31}, {name: "Alice"})
puts "Updated #{updated} row(s)"

# Raw SQL update
db.exec("UPDATE users SET age = age + 1 WHERE department = ?", ["Engineering"])

Deleting Data

deleted = db.delete("users", {name: "Bob"})
puts "Deleted #{deleted} row(s)"

# Raw SQL delete
db.exec("DELETE FROM users WHERE age > ?", [60])

Transactions

Wrap multiple operations in a transaction — all succeed or all roll back:

db.transaction do
  db.insert("accounts", {owner: "Alice", balance: 1000})
  db.insert("accounts", {owner: "Bob",   balance: 500})
end

# Transfer with rollback on error
begin
  db.transaction do
    db.exec("UPDATE accounts SET balance = balance - 100 WHERE owner = ?", ["Alice"])
    db.exec("UPDATE accounts SET balance = balance + 100 WHERE owner = ?", ["Bob"])
    # If either fails, both roll back
  end
  puts "Transfer complete"
rescue e
  puts "Transfer failed: #{e}"
end

Schema Introspection

puts db.tables           # ["users", "products", ...]

cols = db.columns("users")
cols.each do |col|
  puts "#{col["name"]} (#{col["type"]})"
end

Combining with Frankie's Stats

Query results are vectors of hashes — pipe them straight into stdlib functions:

rows     = db.query("SELECT salary FROM employees")
salaries = rows.map do |r|
  r["salary"]
end

puts mean(salaries)    # average salary
puts stdev(salaries)   # standard deviation
puts max(salaries)     # top salary

# Or filter with select
senior_salaries = rows.select do |r|
  r["salary"] > 100000
end.map do |r|
  r["salary"]
end
puts "Senior mean: #{mean(senior_salaries)}"

Closing the Connection

db.close

Always close when done. In-memory databases are destroyed on close.


Full API Reference

Method Description
db_open(path) Open/create DB. ":memory:" for in-memory.
db.exec(sql, params) Run DDL/DML statement. Returns row count.
db.query(sql, params) SELECT → vector of hashes
db.query_one(sql, params) SELECT → first hash or nil
db.insert(table, hash) Insert a row. Returns new row id.
db.insert_many(table, rows) Bulk insert vector of hashes.
db.find_all(table) All rows as vector of hashes
db.find(table, where) Filtered rows (hash → AND)
db.find_one(table, where) First matching row or nil
db.update(table, data, where) Update matching rows. Returns count.
db.delete(table, where) Delete matching rows. Returns count.
db.count(table) Total row count
db.count(table, where) Filtered row count
db.last_id Rowid of last INSERT
db.tables List of table names
db.columns(table) Column info as vector of hashes
db.transaction do...end Atomic block — rollback on error
db.begin Begin explicit transaction
db.commit Commit current transaction
db.rollback Roll back current transaction
db.close Close the connection

Notes

  • Uses Python's built-in sqlite3no external dependencies
  • All query results are vectors of hashes keyed by column name (strings)
  • Always use ? placeholders for user data — never interpolate into SQL strings
  • ":memory:" databases are perfect for testing and temporary work
  • File databases persist between program runs