UPdate.
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
{# 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/template-rendering.html", "title": "Template Rendering"} %}
|
||||
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user