Database-Backed Apps

Two recipes for database-connected KonsolScript apps - local SQLite for zero-config storage and MySQL for shared team databases.

Back to Kookbook


Notes App (SQLite)

Modules: sqlite plugin · JSON · String · Konsol

A persistent note-taking CLI app backed by a local notes.db file. Demonstrates the full SQLite workflow: open/create, DDL, INSERT, SELECT, and DELETE. Data survives between runs - run the script twice and your notes are still there.

Key patterns:

Usage

minks sqlite_notes.ks

Sample session

=== Notes (SQLite) ===

1) List notes
2) Add note
3) View note
4) Delete note
q) Quit
Choice: 2
Title: Buy groceries
Body:  Milk, eggs, bread
Saved as note #1.

Choice: 1
--- Your notes ---
  [1] Buy groceries

Choice: 3
Note ID: 1
--- Buy groceries ---
Milk, eggs, bread

Choice: q
Goodbye!

Script

// sqlite_notes.ks - local note-taking app backed by SQLite
// Modules: sqlite plugin, JSON, String, Konsol
// Usage:  minks sqlite_notes.ks
//
// Demonstrates the full SQLite workflow: open (or create) a database,
// create a table, insert rows, query rows, and delete - all persisted to
// a local "notes.db" file that survives between runs.

#include "sqlite"

Konsol:Print("=== Notes (SQLite) ===");

// ── Step 1: Open (or create) the database ────────────────────────────────────
// SQLite:Open creates the file if it does not exist yet.
Var:Number db;
SQLite:Open("notes.db", db);

// ── Step 2: Create the notes table on first run ───────────────────────────────
// IF NOT EXISTS makes this safe to call every time - no error if the table
// already exists from a previous run.
Var:Boolean ok;
SQLite:Exec(db, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)", ok);

// ── Step 3: Main menu loop ────────────────────────────────────────────────────
Var:Boolean running = true;
Var:String choice;

while (running) {
    Konsol:Print("");
    Konsol:Print("1) List notes");
    Konsol:Print("2) Add note");
    Konsol:Print("3) View note");
    Konsol:Print("4) Delete note");
    Konsol:Print("q) Quit");
    Konsol:Input("Choice: ", choice);

    if (choice == "1") {
        // ── List all notes ────────────────────────────────────────────────────
        // SQLite:Query returns a JSON array string, e.g. [{"id":1,"title":"..."},...].
        // Parse it, then access each row by zero-based index path.
        Var:String rows;
        SQLite:Query(db, "SELECT id, title FROM notes ORDER BY id", rows);

        JSON:Parse(rows, listDoc);
        Var:Number rowCount;
        JSON:Length("", listDoc, rowCount);

        if (rowCount == 0) {
            Konsol:Print("No notes yet.");
        } else {
            Konsol:Print("--- Your notes ---");
            for (Number i = 0; i < rowCount; i++) {
                Var:String noteId;
                Var:String noteTitle;
                JSON:Get("${i}.id", listDoc, noteId);
                JSON:Get("${i}.title", listDoc, noteTitle);
                Konsol:Print("  [${noteId}] ${noteTitle}");
            }
        }

    } else if (choice == "2") {
        // ── Add a note ────────────────────────────────────────────────────────
        Var:String title;
        Var:String body;
        Konsol:Input("Title: ", title);
        Konsol:Input("Body:  ", body);

        // Escape single quotes by doubling them - standard SQL injection defence
        // for values coming from interactive input.
        Var:String safeTitle;
        Var:String safeBody;
        String:Replace(title, "'", "''", safeTitle);
        String:Replace(body, "'", "''", safeBody);

        SQLite:Exec(db, "INSERT INTO notes (title, body) VALUES ('${safeTitle}', '${safeBody}')", ok);

        Var:Number newId;
        SQLite:LastInsertId(db, newId);
        Konsol:Print("Saved as note #${newId}.");

    } else if (choice == "3") {
        // ── View a note ───────────────────────────────────────────────────────
        // SQLite:QueryOne returns a single JSON object string, or "" if not found.
        Var:String idStr;
        Konsol:Input("Note ID: ", idStr);

        Var:String row;
        SQLite:QueryOne(db, "SELECT title, body FROM notes WHERE id = ${idStr}", row);

        if (row == "" || row == "null") {
            Konsol:Print("Note not found.");
        } else {
            JSON:Parse(row, noteDoc);
            Var:String noteTitle;
            Var:String noteBody;
            JSON:Get("title", noteDoc, noteTitle);
            JSON:Get("body", noteDoc, noteBody);
            Konsol:Print("--- ${noteTitle} ---");
            Konsol:Print("${noteBody}");
        }

    } else if (choice == "4") {
        // ── Delete a note ─────────────────────────────────────────────────────
        Var:String delId;
        Konsol:Input("Note ID to delete: ", delId);

        SQLite:Exec(db, "DELETE FROM notes WHERE id = ${delId}", ok);

        Var:Number affected;
        SQLite:RowsAffected(db, affected);
        if (affected > 0) {
            Konsol:Print("Note #${delId} deleted.");
        } else {
            Konsol:Print("Note not found.");
        }

    } else if (choice == "q") {
        running = false;
    } else {
        Konsol:Print("Unknown choice - enter 1-4 or q.");
    }
}

// ── Step 4: Close the connection ─────────────────────────────────────────────
// Always close when done so SQLite can flush any pending writes.
SQLite:Close(db);
Konsol:Print("Goodbye!");


Sales Reporter (MySQL)

Modules: mysql plugin · JSON · File · Konsol

Connects to a MySQL database, aggregates sales by category with GROUP BY, prints a top-5 leaderboard, and writes the full summary to report.csv.

Expects a sales table:

CREATE TABLE sales (
    id       INT PRIMARY KEY AUTO_INCREMENT,
    product  VARCHAR(100),
    category VARCHAR(50),
    amount   DECIMAL(10,2),
    sold_at  DATE
);

Key patterns:

Usage

minks mysql_reporter.ks <host> <user> <pass> <database>

# example
minks mysql_reporter.ks localhost root secret salesdb

Sample output

=== MySQL Sales Reporter ===
Connecting to localhost/salesdb...
Connected.
3 categories found.
Report saved to report.csv

--- Category Summary ---
  Electronics: 4280.50 (12 sales)
  Clothing: 1950.00 (8 sales)
  Books: 340.75 (15 sales)
  Grand total: 6571.25

--- Top 5 Sales ---
  1. Laptop Pro - 1299.99 on 2025-11-03
  2. Gaming Chair - 849.00 on 2025-10-21
  ...
Done.

Script

// mysql_reporter.ks - query MySQL and generate a CSV sales report
// Modules: mysql plugin, JSON, File, Konsol
// Usage:  minks mysql_reporter.ks <host> <user> <pass> <database>
//
// Expects a table in the target database:
//   sales (id INT, product VARCHAR, category VARCHAR, amount DECIMAL, sold_at DATE)
//
// Outputs:
//   - report.csv  - per-category sales totals
//   - Console summary with top 5 individual sales

#include "mysql"

Konsol:Print("=== MySQL Sales Reporter ===");

// ── Step 1: Read connection args ──────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 4) {
    Konsol:Print("Usage: minks mysql_reporter.ks <host> <user> <pass> <database>");
    Konsol:Exit(1);
}

Var:String host;
Var:String user;
Var:String pass;
Var:String dbName;
List:Get(0, args, host);
List:Get(1, args, user);
List:Get(2, args, pass);
List:Get(3, args, dbName);

// ── Step 2: Connect ───────────────────────────────────────────────────────────
// MySQL:Connect throws MySQLException on failure - wrap in try/catch for
// real apps so you can show a clean error message instead of a crash.
Konsol:Print("Connecting to ${host}/${dbName}...");

Var:Number db;
MySQL:Connect(host, user, pass, dbName, db);
Konsol:Print("Connected.");

// ── Step 3: Query per-category totals ────────────────────────────────────────
// MySQL:Query returns a JSON array string: [{"category":"...","total":"..."},...].
// Parse it and iterate by zero-based index paths.
Var:String catRows;
MySQL:Query(db, "SELECT category, SUM(amount) AS total, COUNT(*) AS cnt FROM sales GROUP BY category ORDER BY total DESC", catRows);

JSON:Parse(catRows, catDoc);
Var:Number catCount;
JSON:Length("", catDoc, catCount);

Konsol:Print("${catCount} categories found.");

// ── Step 4: Build CSV report ──────────────────────────────────────────────────
// Accumulate the report as a plain string then write it in one shot.
Var:String csvOut = "Category,Total Sales,Transactions\n";
Var:Number grandTotal = 0;

for (Number i = 0; i < catCount; i++) {
    Var:String cat;
    Var:Number total;
    Var:Number cnt;
    JSON:Get("${i}.category", catDoc, cat);
    JSON:Get("${i}.total", catDoc, total);
    JSON:Get("${i}.cnt", catDoc, cnt);

    grandTotal = grandTotal + total;
    csvOut = csvOut + "${cat},${total},${cnt}\n";
}

// Append a grand-total footer row.
csvOut = csvOut + "TOTAL,${grandTotal},\n";

// ── Step 5: Save to report.csv ────────────────────────────────────────────────
Var:Number wh;
File:Open("report.csv", "w", wh);
File:Write(csvOut, wh);
File:Close(wh);
Konsol:Print("Report saved to report.csv");

// ── Step 6: Print summary to the console ─────────────────────────────────────
Konsol:Print("");
Konsol:Print("--- Category Summary ---");

for (Number i = 0; i < catCount; i++) {
    Var:String cat;
    Var:Number total;
    Var:Number cnt;
    JSON:Get("${i}.category", catDoc, cat);
    JSON:Get("${i}.total", catDoc, total);
    JSON:Get("${i}.cnt", catDoc, cnt);
    Konsol:Print("  ${cat}: ${total} (${cnt} sales)");
}

Konsol:Print("  Grand total: ${grandTotal}");

// ── Step 7: Top 5 individual sales ───────────────────────────────────────────
Konsol:Print("");
Konsol:Print("--- Top 5 Sales ---");

Var:String topRows;
MySQL:Query(db, "SELECT product, amount, sold_at FROM sales ORDER BY amount DESC LIMIT 5", topRows);

JSON:Parse(topRows, topDoc);
Var:Number topCount;
JSON:Length("", topDoc, topCount);

for (Number j = 0; j < topCount; j++) {
    Var:String product;
    Var:Number amount;
    Var:String soldAt;
    Var:Number rank = j + 1;
    JSON:Get("${j}.product", topDoc, product);
    JSON:Get("${j}.amount", topDoc, amount);
    JSON:Get("${j}.sold_at", topDoc, soldAt);
    Konsol:Print("  ${rank}. ${product} - ${amount} on ${soldAt}");
}

// ── Step 8: Close the connection ─────────────────────────────────────────────
MySQL:Close(db);
Konsol:Print("Done.");


Back to Kookbook