Aplikasi Command Line Interface (CLI) modern sering membutuhkan persistensi data yang andal tanpa ketergantungan pada koneksi jaringan atau daemon eksternal seperti PostgreSQL atau Redis. Menggunakan SQLite embedded memungkinkan binary CLI beroperasi langsung pada filesystem lokal dengan jaminan transaksi ACID penuh.
Panduan ini membahas arsitektur offline CLI task tracker—terinspirasi sistem quest tracking dalam game RPG dunia nyata (Earth Game)—di mana setiap aktivitas harian dicatat sebagai quest terstruktur dengan reward dan log progres terverifikasi.
Arsitektur User Space dan Lokasi Database
Aplikasi CLI tidak boleh menulis file database sembarangan di direktori kerja saat ini (working directory). Lokasi file harus mengikuti standar XDG Base Directory di Linux/macOS atau AppData di Windows.
Direktori standar yang digunakan adalah $XDG_DATA_HOME/tasktracker (biasanya ~/.local/share/tasktracker) atau $HOME/.config/tasktracker. Menyiapkan path yang benar mencegah file tertimpa atau terhapus saat berpindah folder kerja.
import os
import sys
from pathlib import Path
def get_db_path() -> Path:
# Mengikuti standar XDG data directory
xdg_data = os.environ.get("XDG_DATA_HOME")
if xdg_data:
base_dir = Path(xdg_data)
else:
base_dir = Path.home() / ".local" / "share"
app_dir = base_dir / "tasktracker"
try:
app_dir.mkdir(parents=True, exist_ok=True)
except OSError as err:
sys.stderr.write(f"Error alokasi direktori user space: {err}\n")
sys.exit(1)
return app_dir / "tracker.db"
Skema Relasional Quest dan Pola Migrasi Otomatis
Struktur data memisahkan metadata tugas (quests) dari catatan pengerjaan (progress logs). SQLite embedded mendukung schema migration otomatis berbasis PRAGMA user_version tanpa memerlukan perkakas migrasi eksternal.
import sqlite3
MIGRATIONS = [
# Versi 1: Inisialisasi skema quest dan logs
"""
CREATE TABLE quests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
exp_reward INTEGER NOT NULL DEFAULT 10,
status TEXT CHECK(status IN ('OPEN', 'COMPLETED', 'FAILED')) NOT NULL DEFAULT 'OPEN',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE quest_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
quest_id INTEGER NOT NULL,
notes TEXT NOT NULL,
recorded_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (quest_id) REFERENCES quests(id) ON DELETE CASCADE
);
CREATE INDEX idx_quests_status ON quests(status);
CREATE INDEX idx_quest_logs_quest_id ON quest_logs(quest_id);
"""
]
def migrate_database(conn: sqlite3.Connection) -> None:
current_version = conn.execute("PRAGMA user_version;").fetchone()[0]
total_versions = len(MIGRATIONS)
if current_version < total_versions:
with conn:
for v in range(current_version, total_versions):
conn.executescript(MIGRATIONS[v])
conn.execute(f"PRAGMA user_version = {v + 1};")
Transaksi Atomik ACID untuk Mutasi Status
Saat menandai quest sebagai selesai, dua hal harus terjadi secara bersamaan: memperbarui status tabel quests dan menyisipkan entri ke quest_logs. Jika salah satu gagal, seluruh operasi harus dibatalkan (rollback).
Gunakan mode BEGIN IMMEDIATE untuk mencegah database lock starvation jika ada proses CLI paralel yang mengakses file yang sama.
def complete_quest(conn: sqlite3.Connection, slug: str, log_notes: str) -> bool:
try:
with conn:
# Mengaktifkan foreign keys untuk koneksi ini
conn.execute("PRAGMA foreign_keys = ON;")
# Verifikasi keberadaan quest
cursor = conn.execute(
"SELECT id, status FROM quests WHERE slug = ?;", (slug,)
)
row = cursor.fetchone()
if not row:
raise ValueError(f"Quest '{slug}' tidak ditemukan.")
quest_id, status = row
if status == "COMPLETED":
raise ValueError(f"Quest '{slug}' sudah berstatus COMPLETED.")
# Mutasi status
conn.execute(
"UPDATE quests SET status = 'COMPLETED' WHERE id = ?;",
(quest_id,)
)
# Insert catatan progres
conn.execute(
"INSERT INTO quest_logs (quest_id, notes) VALUES (?, ?);",
(quest_id, log_notes)
)
return True
except sqlite3.OperationalError as err:
# Error handling disk penuh, permission denied, atau SQLite file lock
sys.stderr.write(f"I/O Database Error: {err}\n")
return False
except ValueError as err:
sys.stderr.write(f"Business Logic Error: {err}\n")
return False
Optimasi Konfigurasi SQLite Embedded
Secara default, SQLite beroperasi dalam mode rollback journal yang memicu lock penuh pada file saat proses penulisan. Untuk CLI yang cepat dan andal, aktifkan mode WAL (Write-Ahead Logging) dan busy_timeout:
def get_connection(db_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(
db_path,
timeout=5.0, # Menunggu hingga 5 detik jika file di-lock proses lain
isolation_level=None # Mengelola transaksi manual untuk kontrol ACID ketat
)
# Optimasi performa I/O
conn.execute("PRAGMA journal_mode = WAL;")
conn.execute("PRAGMA synchronous = NORMAL;")
return conn
Error Handling dan Pemulihan I/O Lokal
Kendala umum database berbasis file pada level OS mencakup:
- SQLite Busy / Locked: Terjadi jika terdapat dua terminal yang mengeksekusi operasi penulisan serentak. Parameter
timeoutpada koneksi menangani antrean ini secara transparan. - Disk Penuh (ENOSPC): SQLite menghasilkan
sqlite3.OperationalError: database or disk is full. Pastikan transaksi dibungkus bloktry/exceptagar file.db-waltidak tertinggal dalam status korup. - Permission Denied (EACCES): Terjadi jika permission folder user space berubah. Tangani langsung sebelum koneksi SQLite dibuka melalui verifikasi
os.access().
Arsitektur SQLite embedded ini menghasilkan perkakas CLI mandiri dengan performa eksekusi sub-milidetik, tanpa dependensi server eksternal, dan aman dari inkonsistensi data lokal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!