Add new Dataset ORM tutorial page covering CRUD operations, query operators, schema evolution, and JSON field handling. Update navigation.yaml to include the new tutorial in the tutorials section. Enhance markdown module documentation with GitHub Flavored Markdown support details including tables, task lists, and language identifier attributes for syntax highlighting. Update tutorial index page and pagination links to reflect the new tutorial order. Refactor dataset.wren module source to introduce Sql and QueryBuilder classes with soft-delete support and identifier validation.
629 lines
18 KiB
HTML
629 lines
18 KiB
HTML
{# retoor <retoor@molodetz.nl> #}
|
|
{% extends 'page.html' %}
|
|
|
|
{% set page_title = "Database Application" %}
|
|
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "Database App"}] %}
|
|
{% set prev_page = {"url": "tutorials/websocket-chat.html", "title": "WebSocket Chat"} %}
|
|
{% set next_page = {"url": "tutorials/dataset-orm.html", "title": "Dataset ORM"} %}
|
|
|
|
{% block article %}
|
|
<h1>Database Application</h1>
|
|
|
|
<p>In this tutorial, you will build a complete task management application using SQLite for persistent storage. You will learn to create tables, perform CRUD operations, and build a command-line interface.</p>
|
|
|
|
<h2>What You Will Learn</h2>
|
|
|
|
<ul>
|
|
<li>Creating and managing SQLite databases</li>
|
|
<li>Designing database schemas</li>
|
|
<li>Performing CRUD operations (Create, Read, Update, Delete)</li>
|
|
<li>Using parameterized queries to prevent SQL injection</li>
|
|
<li>Building a command-line interface</li>
|
|
</ul>
|
|
|
|
<h2>Step 1: Setting Up the Database</h2>
|
|
|
|
<p>Create a file called <code>task_app.wren</code>:</p>
|
|
|
|
<pre><code>import "sqlite" for Sqlite
|
|
|
|
var db = Sqlite.open("tasks.db")
|
|
|
|
db.execute("
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
priority INTEGER DEFAULT 1,
|
|
completed INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
due_date DATE
|
|
)
|
|
")
|
|
|
|
System.print("Database initialized!")
|
|
db.close()</code></pre>
|
|
|
|
<h2>Step 2: Creating a Task Model</h2>
|
|
|
|
<p>Let's create a class to manage task operations:</p>
|
|
|
|
<pre><code>import "sqlite" for Sqlite
|
|
import "datetime" for DateTime
|
|
|
|
class TaskManager {
|
|
construct new(dbPath) {
|
|
_db = Sqlite.open(dbPath)
|
|
initDatabase()
|
|
}
|
|
|
|
initDatabase() {
|
|
_db.execute("
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
priority INTEGER DEFAULT 1,
|
|
completed INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
due_date DATE
|
|
)
|
|
")
|
|
}
|
|
|
|
create(title, description, priority, dueDate) {
|
|
_db.execute(
|
|
"INSERT INTO tasks (title, description, priority, due_date) VALUES (?, ?, ?, ?)",
|
|
[title, description, priority, dueDate]
|
|
)
|
|
return _db.lastInsertId
|
|
}
|
|
|
|
findAll() {
|
|
return _db.execute("SELECT * FROM tasks ORDER BY priority DESC, due_date ASC")
|
|
}
|
|
|
|
findById(id) {
|
|
var results = _db.execute("SELECT * FROM tasks WHERE id = ?", [id])
|
|
return results.count > 0 ? results[0] : null
|
|
}
|
|
|
|
findPending() {
|
|
return _db.execute("SELECT * FROM tasks WHERE completed = 0 ORDER BY priority DESC")
|
|
}
|
|
|
|
findCompleted() {
|
|
return _db.execute("SELECT * FROM tasks WHERE completed = 1 ORDER BY created_at DESC")
|
|
}
|
|
|
|
update(id, title, description, priority, dueDate) {
|
|
_db.execute(
|
|
"UPDATE tasks SET title = ?, description = ?, priority = ?, due_date = ? WHERE id = ?",
|
|
[title, description, priority, dueDate, id]
|
|
)
|
|
}
|
|
|
|
complete(id) {
|
|
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
|
|
}
|
|
|
|
uncomplete(id) {
|
|
_db.execute("UPDATE tasks SET completed = 0 WHERE id = ?", [id])
|
|
}
|
|
|
|
delete(id) {
|
|
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
|
|
}
|
|
|
|
search(query) {
|
|
return _db.execute(
|
|
"SELECT * FROM tasks WHERE title LIKE ? OR description LIKE ?",
|
|
["\%%(query)\%", "\%%(query)\%"]
|
|
)
|
|
}
|
|
|
|
close() {
|
|
_db.close()
|
|
}
|
|
}
|
|
|
|
var tasks = TaskManager.new("tasks.db")
|
|
|
|
var id = tasks.create("Learn Wren-CLI", "Complete the database tutorial", 3, "2024-12-31")
|
|
System.print("Created task with ID: %(id)")
|
|
|
|
tasks.close()</code></pre>
|
|
|
|
<h2>Step 3: Building the CLI Interface</h2>
|
|
|
|
<p>Now let's create an interactive command-line interface:</p>
|
|
|
|
<pre><code>import "sqlite" for Sqlite
|
|
import "io" for Stdin
|
|
|
|
class TaskManager {
|
|
construct new(dbPath) {
|
|
_db = Sqlite.open(dbPath)
|
|
initDatabase()
|
|
}
|
|
|
|
initDatabase() {
|
|
_db.execute("
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
priority INTEGER DEFAULT 1,
|
|
completed INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
")
|
|
}
|
|
|
|
create(title, description, priority) {
|
|
_db.execute(
|
|
"INSERT INTO tasks (title, description, priority) VALUES (?, ?, ?)",
|
|
[title, description, priority]
|
|
)
|
|
return _db.lastInsertId
|
|
}
|
|
|
|
findAll() {
|
|
return _db.execute("SELECT * FROM tasks ORDER BY completed ASC, priority DESC")
|
|
}
|
|
|
|
complete(id) {
|
|
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
|
|
}
|
|
|
|
delete(id) {
|
|
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
|
|
}
|
|
|
|
close() {
|
|
_db.close()
|
|
}
|
|
}
|
|
|
|
class TaskApp {
|
|
construct new() {
|
|
_tasks = TaskManager.new("tasks.db")
|
|
_running = true
|
|
}
|
|
|
|
run() {
|
|
System.print("=== Task Manager ===\n")
|
|
showHelp()
|
|
|
|
while (_running) {
|
|
System.write("\n> ")
|
|
var input = Stdin.readLine()
|
|
|
|
if (input == null) break
|
|
|
|
var parts = input.split(" ")
|
|
var command = parts.count > 0 ? parts[0] : ""
|
|
|
|
if (command == "list") {
|
|
listTasks()
|
|
} else if (command == "add") {
|
|
addTask()
|
|
} else if (command == "done") {
|
|
if (parts.count > 1) {
|
|
completeTask(Num.fromString(parts[1]))
|
|
} else {
|
|
System.print("Usage: done <id>")
|
|
}
|
|
} else if (command == "delete") {
|
|
if (parts.count > 1) {
|
|
deleteTask(Num.fromString(parts[1]))
|
|
} else {
|
|
System.print("Usage: delete <id>")
|
|
}
|
|
} else if (command == "help") {
|
|
showHelp()
|
|
} else if (command == "quit" || command == "exit") {
|
|
_running = false
|
|
} else if (command.count > 0) {
|
|
System.print("Unknown command: %(command)")
|
|
}
|
|
}
|
|
|
|
_tasks.close()
|
|
System.print("Goodbye!")
|
|
}
|
|
|
|
showHelp() {
|
|
System.print("Commands:")
|
|
System.print(" list - Show all tasks")
|
|
System.print(" add - Add a new task")
|
|
System.print(" done <id> - Mark task as complete")
|
|
System.print(" delete <id> - Delete a task")
|
|
System.print(" help - Show this help")
|
|
System.print(" quit - Exit the application")
|
|
}
|
|
|
|
listTasks() {
|
|
var tasks = _tasks.findAll()
|
|
|
|
if (tasks.count == 0) {
|
|
System.print("No tasks found.")
|
|
return
|
|
}
|
|
|
|
System.print("\nID | Pri | Status | Title")
|
|
System.print("----|-----|--------|------")
|
|
|
|
for (task in tasks) {
|
|
var status = task["completed"] == 1 ? "[X]" : "[ ]"
|
|
var priority = ["Low", "Med", "High"][task["priority"] - 1]
|
|
System.print("%(task["id"]) | %(priority) | %(status) | %(task["title"])")
|
|
}
|
|
}
|
|
|
|
addTask() {
|
|
System.write("Title: ")
|
|
var title = Stdin.readLine()
|
|
|
|
System.write("Description (optional): ")
|
|
var description = Stdin.readLine()
|
|
|
|
System.write("Priority (1=Low, 2=Medium, 3=High): ")
|
|
var priorityStr = Stdin.readLine()
|
|
var priority = Num.fromString(priorityStr) || 1
|
|
|
|
if (priority < 1) priority = 1
|
|
if (priority > 3) priority = 3
|
|
|
|
var id = _tasks.create(title, description, priority)
|
|
System.print("Task created with ID: %(id)")
|
|
}
|
|
|
|
completeTask(id) {
|
|
_tasks.complete(id)
|
|
System.print("Task %(id) marked as complete.")
|
|
}
|
|
|
|
deleteTask(id) {
|
|
_tasks.delete(id)
|
|
System.print("Task %(id) deleted.")
|
|
}
|
|
}
|
|
|
|
var app = TaskApp.new()
|
|
app.run()</code></pre>
|
|
|
|
<h2>Step 4: Adding Advanced Features</h2>
|
|
|
|
<p>Let's enhance our application with categories and due dates:</p>
|
|
|
|
<pre><code>import "sqlite" for Sqlite
|
|
import "io" for Stdin
|
|
import "datetime" for DateTime
|
|
|
|
class TaskManager {
|
|
construct new(dbPath) {
|
|
_db = Sqlite.open(dbPath)
|
|
initDatabase()
|
|
}
|
|
|
|
initDatabase() {
|
|
_db.execute("
|
|
CREATE TABLE IF NOT EXISTS categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
color TEXT DEFAULT '#808080'
|
|
)
|
|
")
|
|
|
|
_db.execute("
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
category_id INTEGER,
|
|
priority INTEGER DEFAULT 1,
|
|
completed INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
due_date DATE,
|
|
FOREIGN KEY (category_id) REFERENCES categories(id)
|
|
)
|
|
")
|
|
|
|
var categories = _db.execute("SELECT COUNT(*) as count FROM categories")
|
|
if (categories[0]["count"] == 0) {
|
|
_db.execute("INSERT INTO categories (name, color) VALUES ('Work', '#FF6B6B')")
|
|
_db.execute("INSERT INTO categories (name, color) VALUES ('Personal', '#4ECDC4')")
|
|
_db.execute("INSERT INTO categories (name, color) VALUES ('Shopping', '#45B7D1')")
|
|
}
|
|
}
|
|
|
|
createTask(title, description, categoryId, priority, dueDate) {
|
|
_db.execute(
|
|
"INSERT INTO tasks (title, description, category_id, priority, due_date) VALUES (?, ?, ?, ?, ?)",
|
|
[title, description, categoryId, priority, dueDate]
|
|
)
|
|
return _db.lastInsertId
|
|
}
|
|
|
|
findAllTasks() {
|
|
return _db.execute("
|
|
SELECT t.*, c.name as category_name
|
|
FROM tasks t
|
|
LEFT JOIN categories c ON t.category_id = c.id
|
|
ORDER BY t.completed ASC, t.priority DESC, t.due_date ASC
|
|
")
|
|
}
|
|
|
|
findTasksByCategory(categoryId) {
|
|
return _db.execute("
|
|
SELECT t.*, c.name as category_name
|
|
FROM tasks t
|
|
LEFT JOIN categories c ON t.category_id = c.id
|
|
WHERE t.category_id = ?
|
|
ORDER BY t.completed ASC, t.priority DESC
|
|
", [categoryId])
|
|
}
|
|
|
|
findOverdueTasks() {
|
|
return _db.execute("
|
|
SELECT t.*, c.name as category_name
|
|
FROM tasks t
|
|
LEFT JOIN categories c ON t.category_id = c.id
|
|
WHERE t.completed = 0 AND t.due_date < DATE('now')
|
|
ORDER BY t.due_date ASC
|
|
")
|
|
}
|
|
|
|
findDueToday() {
|
|
return _db.execute("
|
|
SELECT t.*, c.name as category_name
|
|
FROM tasks t
|
|
LEFT JOIN categories c ON t.category_id = c.id
|
|
WHERE t.completed = 0 AND t.due_date = DATE('now')
|
|
ORDER BY t.priority DESC
|
|
")
|
|
}
|
|
|
|
findCategories() {
|
|
return _db.execute("SELECT * FROM categories ORDER BY name")
|
|
}
|
|
|
|
createCategory(name, color) {
|
|
_db.execute("INSERT INTO categories (name, color) VALUES (?, ?)", [name, color])
|
|
return _db.lastInsertId
|
|
}
|
|
|
|
completeTask(id) {
|
|
_db.execute("UPDATE tasks SET completed = 1 WHERE id = ?", [id])
|
|
}
|
|
|
|
deleteTask(id) {
|
|
_db.execute("DELETE FROM tasks WHERE id = ?", [id])
|
|
}
|
|
|
|
getStats() {
|
|
var total = _db.execute("SELECT COUNT(*) as count FROM tasks")[0]["count"]
|
|
var completed = _db.execute("SELECT COUNT(*) as count FROM tasks WHERE completed = 1")[0]["count"]
|
|
var pending = total - completed
|
|
var overdue = _db.execute("SELECT COUNT(*) as count FROM tasks WHERE completed = 0 AND due_date < DATE('now')")[0]["count"]
|
|
|
|
return {
|
|
"total": total,
|
|
"completed": completed,
|
|
"pending": pending,
|
|
"overdue": overdue
|
|
}
|
|
}
|
|
|
|
close() {
|
|
_db.close()
|
|
}
|
|
}
|
|
|
|
class TaskApp {
|
|
construct new() {
|
|
_tasks = TaskManager.new("tasks.db")
|
|
_running = true
|
|
}
|
|
|
|
run() {
|
|
System.print("=== Task Manager v2 ===\n")
|
|
|
|
while (_running) {
|
|
showMenu()
|
|
System.write("\nChoice: ")
|
|
var choice = Stdin.readLine()
|
|
|
|
if (choice == "1") {
|
|
listTasks()
|
|
} else if (choice == "2") {
|
|
addTask()
|
|
} else if (choice == "3") {
|
|
completeTask()
|
|
} else if (choice == "4") {
|
|
showOverdue()
|
|
} else if (choice == "5") {
|
|
showStats()
|
|
} else if (choice == "6") {
|
|
manageCategories()
|
|
} else if (choice == "0") {
|
|
_running = false
|
|
}
|
|
}
|
|
|
|
_tasks.close()
|
|
System.print("\nGoodbye!")
|
|
}
|
|
|
|
showMenu() {
|
|
System.print("\n--- Main Menu ---")
|
|
System.print("1. List all tasks")
|
|
System.print("2. Add new task")
|
|
System.print("3. Complete task")
|
|
System.print("4. Show overdue")
|
|
System.print("5. Statistics")
|
|
System.print("6. Categories")
|
|
System.print("0. Exit")
|
|
}
|
|
|
|
listTasks() {
|
|
var tasks = _tasks.findAllTasks()
|
|
|
|
if (tasks.count == 0) {
|
|
System.print("\nNo tasks found.")
|
|
return
|
|
}
|
|
|
|
System.print("\n--- All Tasks ---")
|
|
for (task in tasks) {
|
|
var status = task["completed"] == 1 ? "[X]" : "[ ]"
|
|
var category = task["category_name"] || "None"
|
|
var due = task["due_date"] || "No due date"
|
|
|
|
System.print("%(task["id"]). %(status) %(task["title"])")
|
|
System.print(" Category: %(category) | Due: %(due)")
|
|
}
|
|
}
|
|
|
|
addTask() {
|
|
System.print("\n--- Add Task ---")
|
|
|
|
System.write("Title: ")
|
|
var title = Stdin.readLine()
|
|
if (title.count == 0) return
|
|
|
|
System.write("Description: ")
|
|
var description = Stdin.readLine()
|
|
|
|
var categories = _tasks.findCategories()
|
|
System.print("\nCategories:")
|
|
for (cat in categories) {
|
|
System.print(" %(cat["id"]). %(cat["name"])")
|
|
}
|
|
System.write("Category ID (or 0 for none): ")
|
|
var categoryId = Num.fromString(Stdin.readLine())
|
|
if (categoryId == 0) categoryId = null
|
|
|
|
System.write("Priority (1-3): ")
|
|
var priority = Num.fromString(Stdin.readLine()) || 1
|
|
|
|
System.write("Due date (YYYY-MM-DD or empty): ")
|
|
var dueDate = Stdin.readLine()
|
|
if (dueDate.count == 0) dueDate = null
|
|
|
|
var id = _tasks.createTask(title, description, categoryId, priority, dueDate)
|
|
System.print("\nTask created with ID: %(id)")
|
|
}
|
|
|
|
completeTask() {
|
|
System.write("\nTask ID to complete: ")
|
|
var id = Num.fromString(Stdin.readLine())
|
|
if (id) {
|
|
_tasks.completeTask(id)
|
|
System.print("Task %(id) marked as complete!")
|
|
}
|
|
}
|
|
|
|
showOverdue() {
|
|
var tasks = _tasks.findOverdueTasks()
|
|
|
|
if (tasks.count == 0) {
|
|
System.print("\nNo overdue tasks!")
|
|
return
|
|
}
|
|
|
|
System.print("\n--- Overdue Tasks ---")
|
|
for (task in tasks) {
|
|
System.print("%(task["id"]). %(task["title"]) (Due: %(task["due_date"]))")
|
|
}
|
|
}
|
|
|
|
showStats() {
|
|
var stats = _tasks.getStats()
|
|
|
|
System.print("\n--- Statistics ---")
|
|
System.print("Total tasks: %(stats["total"])")
|
|
System.print("Completed: %(stats["completed"])")
|
|
System.print("Pending: %(stats["pending"])")
|
|
System.print("Overdue: %(stats["overdue"])")
|
|
|
|
if (stats["total"] > 0) {
|
|
var pct = (stats["completed"] / stats["total"] * 100).round
|
|
System.print("Completion rate: %(pct)\%")
|
|
}
|
|
}
|
|
|
|
manageCategories() {
|
|
var categories = _tasks.findCategories()
|
|
|
|
System.print("\n--- Categories ---")
|
|
for (cat in categories) {
|
|
System.print("%(cat["id"]). %(cat["name"])")
|
|
}
|
|
|
|
System.write("\nAdd new category? (y/n): ")
|
|
if (Stdin.readLine().lower == "y") {
|
|
System.write("Category name: ")
|
|
var name = Stdin.readLine()
|
|
if (name.count > 0) {
|
|
_tasks.createCategory(name, "#808080")
|
|
System.print("Category created!")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var app = TaskApp.new()
|
|
app.run()</code></pre>
|
|
|
|
<h2>Running the Application</h2>
|
|
|
|
<pre><code>$ wren_cli task_app.wren
|
|
=== Task Manager v2 ===
|
|
|
|
--- Main Menu ---
|
|
1. List all tasks
|
|
2. Add new task
|
|
3. Complete task
|
|
4. Show overdue
|
|
5. Statistics
|
|
6. Categories
|
|
0. Exit
|
|
|
|
Choice: 2
|
|
|
|
--- Add Task ---
|
|
Title: Complete database tutorial
|
|
Description: Learn SQLite with Wren-CLI
|
|
|
|
Categories:
|
|
1. Work
|
|
2. Personal
|
|
3. Shopping
|
|
Category ID (or 0 for none): 1
|
|
Priority (1-3): 3
|
|
Due date (YYYY-MM-DD or empty): 2024-12-31
|
|
|
|
Task created with ID: 1</code></pre>
|
|
|
|
<div class="admonition tip">
|
|
<div class="admonition-title">Tip</div>
|
|
<p>Always use parameterized queries with <code>?</code> placeholders instead of string concatenation. This prevents SQL injection vulnerabilities.</p>
|
|
</div>
|
|
|
|
<div class="admonition note">
|
|
<div class="admonition-title">Note</div>
|
|
<p>Use <code>":memory:"</code> as the database path for testing without creating a file on disk.</p>
|
|
</div>
|
|
|
|
<h2>Next Steps</h2>
|
|
|
|
<ul>
|
|
<li>Add data export to JSON or CSV</li>
|
|
<li>Implement task reminders with <a href="../api/timer.html">Timer</a></li>
|
|
<li>Generate HTML reports with <a href="template-rendering.html">Jinja templates</a></li>
|
|
<li>See the <a href="../api/sqlite.html">SQLite API reference</a></li>
|
|
</ul>
|
|
{% endblock %}
|