Add SQLite persistence via Docker named volume

Tasks are now stored in SQLite (DB_PATH env var, defaults to ./tasks.db).
Pre-seeding runs only when the table is empty, so upgrades preserve data.
This is the v1.0.0 baseline for the persistence demo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
davidtio
2026-02-28 14:15:17 +08:00
parent 78a5f98f11
commit 371f2c949d
3 changed files with 123 additions and 91 deletions

View File

@@ -1,41 +1,87 @@
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import scala.jdk.CollectionConverters.*
import java.sql.{Connection, DriverManager}
case class Task(id: UUID, title: String, completed: Boolean)
object TaskStore:
private val tasks = ConcurrentHashMap[UUID, Task]()
private val dbPath = sys.env.getOrElse("DB_PATH", "./tasks.db")
private val url = s"jdbc:sqlite:$dbPath"
// Pre-seed sample tasks
Class.forName("org.sqlite.JDBC")
private def connect(): Connection = DriverManager.getConnection(url)
// Initialize schema and pre-seed if empty
locally {
val samples = List(
"Deploy Scala app to Clouderized",
"Set up CI/CD with Gitea Actions",
"Configure custom domain"
)
samples.foreach { title =>
val id = UUID.randomUUID()
tasks.put(id, Task(id, title, false))
}
val conn = connect()
try
val stmt = conn.createStatement()
stmt.execute(
"CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL, completed INTEGER NOT NULL DEFAULT 0)"
)
val rs = stmt.executeQuery("SELECT COUNT(*) FROM tasks")
val count = rs.getInt(1)
if count == 0 then
val samples = List(
"Deploy Scala app to Clouderized",
"Set up CI/CD with Gitea Actions",
"Configure custom domain"
)
val ps = conn.prepareStatement("INSERT INTO tasks (id, title, completed) VALUES (?, ?, 0)")
samples.foreach { title =>
ps.setString(1, UUID.randomUUID().toString)
ps.setString(2, title)
ps.executeUpdate()
}
finally conn.close()
}
def all(): List[Task] =
tasks.values().asScala.toList.sortBy(_.title)
val conn = connect()
try
val rs = conn.createStatement().executeQuery("SELECT id, title, completed FROM tasks ORDER BY title")
val buf = collection.mutable.ListBuffer.empty[Task]
while rs.next() do
buf += Task(UUID.fromString(rs.getString("id")), rs.getString("title"), rs.getInt("completed") == 1)
buf.toList
finally conn.close()
def add(title: String): Task =
val id = UUID.randomUUID()
val task = Task(id, title.trim, false)
tasks.put(id, task)
task
val conn = connect()
try
val ps = conn.prepareStatement("INSERT INTO tasks (id, title, completed) VALUES (?, ?, 0)")
ps.setString(1, id.toString)
ps.setString(2, title.trim)
ps.executeUpdate()
Task(id, title.trim, false)
finally conn.close()
def toggle(id: UUID): Unit =
Option(tasks.get(id)).foreach { task =>
tasks.put(id, task.copy(completed = !task.completed))
}
val conn = connect()
try
val ps = conn.prepareStatement(
"UPDATE tasks SET completed = CASE WHEN completed = 1 THEN 0 ELSE 1 END WHERE id = ?"
)
ps.setString(1, id.toString)
ps.executeUpdate()
finally conn.close()
def delete(id: UUID): Unit =
tasks.remove(id)
val conn = connect()
try
val ps = conn.prepareStatement("DELETE FROM tasks WHERE id = ?")
ps.setString(1, id.toString)
ps.executeUpdate()
finally conn.close()
def get(id: UUID): Option[Task] =
Option(tasks.get(id))
val conn = connect()
try
val ps = conn.prepareStatement("SELECT id, title, completed FROM tasks WHERE id = ?")
ps.setString(1, id.toString)
val rs = ps.executeQuery()
if rs.next() then
Some(Task(UUID.fromString(rs.getString("id")), rs.getString("title"), rs.getInt("completed") == 1))
else None
finally conn.close()