2026-04-13 14:24:19 +03:00
|
|
|
(define-module (model)
|
|
|
|
|
#:use-module (knots thread-pool)
|
|
|
|
|
#:use-module (sqlite3)
|
|
|
|
|
#:export (make-db
|
|
|
|
|
call-with-db
|
|
|
|
|
db-init!
|
|
|
|
|
db-list-posts
|
|
|
|
|
db-get-post
|
|
|
|
|
db-create-post!
|
|
|
|
|
db-update-post!
|
|
|
|
|
db-delete-post!))
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Connection pool
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define* (make-db database-file #:key (pool-size 4))
|
|
|
|
|
"Create a thread pool where each thread holds an open SQLite
|
|
|
|
|
connection to DATABASE-FILE."
|
|
|
|
|
(make-fixed-size-thread-pool
|
|
|
|
|
pool-size
|
|
|
|
|
#:name "sqlite"
|
|
|
|
|
#:thread-initializer
|
|
|
|
|
(lambda ()
|
|
|
|
|
(let ((db (sqlite-open database-file
|
|
|
|
|
(logior SQLITE_OPEN_READWRITE
|
|
|
|
|
SQLITE_OPEN_CREATE))))
|
|
|
|
|
(sqlite-busy-timeout db 5000)
|
|
|
|
|
(sqlite-exec db "PRAGMA journal_mode=WAL")
|
|
|
|
|
(sqlite-exec db "PRAGMA foreign_keys=ON")
|
2026-04-14 15:19:11 +03:00
|
|
|
|
|
|
|
|
(db-init! db)
|
|
|
|
|
|
2026-04-13 14:24:19 +03:00
|
|
|
(list db)))
|
|
|
|
|
#:thread-destructor
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(sqlite-close db))))
|
|
|
|
|
|
|
|
|
|
(define (call-with-db pool proc)
|
|
|
|
|
"Run (PROC db) on a thread from POOL, where DB is the thread's
|
|
|
|
|
SQLite connection. Returns whatever PROC returns."
|
|
|
|
|
(call-with-thread pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(proc db))))
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Schema
|
|
|
|
|
;;;
|
|
|
|
|
|
2026-04-14 15:19:11 +03:00
|
|
|
(define (db-init! db)
|
|
|
|
|
(sqlite-exec db "
|
|
|
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
title TEXT NOT NULL,
|
|
|
|
|
body TEXT NOT NULL,
|
|
|
|
|
image_url TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
|
|
|
)"))
|
2026-04-13 14:24:19 +03:00
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Row conversions
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define (row->post-summary row)
|
|
|
|
|
"Convert a list-view row (vector) to an alist."
|
|
|
|
|
`((id . ,(vector-ref row 0))
|
|
|
|
|
(title . ,(vector-ref row 1))
|
|
|
|
|
(created-at . ,(vector-ref row 2))))
|
|
|
|
|
|
|
|
|
|
(define (row->post row)
|
|
|
|
|
"Convert a detail-view row (vector) to an alist."
|
|
|
|
|
`((id . ,(vector-ref row 0))
|
|
|
|
|
(title . ,(vector-ref row 1))
|
|
|
|
|
(body . ,(vector-ref row 2))
|
|
|
|
|
(image-url . ,(vector-ref row 3))
|
|
|
|
|
(created-at . ,(vector-ref row 4))))
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Queries
|
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
(define (db-list-posts pool)
|
|
|
|
|
"Return all posts as a list of alists (id, title, created-at),
|
|
|
|
|
newest first."
|
|
|
|
|
(call-with-db pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(let ((stmt (sqlite-prepare db
|
|
|
|
|
"SELECT id, title, created_at FROM posts ORDER BY id DESC")))
|
|
|
|
|
(let ((rows (sqlite-map row->post-summary stmt)))
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
rows)))))
|
|
|
|
|
|
|
|
|
|
(define (db-get-post pool id)
|
|
|
|
|
"Return the post with ID as an alist, or #f if not found."
|
|
|
|
|
(call-with-db pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(let ((stmt (sqlite-prepare db
|
|
|
|
|
"SELECT id, title, body, image_url, created_at
|
|
|
|
|
FROM posts WHERE id = ?")))
|
|
|
|
|
(sqlite-bind stmt 1 id)
|
|
|
|
|
(let ((row (sqlite-step stmt)))
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(and row (row->post row)))))))
|
|
|
|
|
|
|
|
|
|
(define (db-create-post! pool title body image-url)
|
|
|
|
|
"Insert a new post and return its ID."
|
|
|
|
|
(call-with-db pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(let ((stmt (sqlite-prepare db
|
|
|
|
|
"INSERT INTO posts (title, body, image_url) VALUES (?, ?, ?)")))
|
|
|
|
|
(sqlite-bind stmt 1 title)
|
|
|
|
|
(sqlite-bind stmt 2 body)
|
|
|
|
|
(sqlite-bind stmt 3 image-url)
|
|
|
|
|
(sqlite-step stmt)
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(let ((stmt (sqlite-prepare db "SELECT last_insert_rowid()")))
|
|
|
|
|
(let ((row (sqlite-step stmt)))
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(vector-ref row 0)))))))
|
|
|
|
|
|
2026-04-14 15:19:11 +03:00
|
|
|
(define (sqlite-changes db)
|
|
|
|
|
"Return the number of rows changed by the most recent INSERT, UPDATE,
|
|
|
|
|
or DELETE statement on DB."
|
|
|
|
|
(let ((stmt (sqlite-prepare db "SELECT changes()")))
|
|
|
|
|
(let ((row (sqlite-step stmt)))
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(vector-ref row 0))))
|
|
|
|
|
|
2026-04-13 14:24:19 +03:00
|
|
|
(define (db-update-post! pool id title body image-url)
|
|
|
|
|
"Update the post with ID. Returns #t if a row was changed, #f otherwise."
|
|
|
|
|
(call-with-db pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(let ((stmt (sqlite-prepare db
|
|
|
|
|
"UPDATE posts SET title = ?, body = ?, image_url = ?
|
|
|
|
|
WHERE id = ?")))
|
|
|
|
|
(sqlite-bind stmt 1 title)
|
|
|
|
|
(sqlite-bind stmt 2 body)
|
|
|
|
|
(sqlite-bind stmt 3 image-url)
|
|
|
|
|
(sqlite-bind stmt 4 id)
|
|
|
|
|
(sqlite-step stmt)
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(> (sqlite-changes db) 0)))))
|
|
|
|
|
|
|
|
|
|
(define (db-delete-post! pool id)
|
|
|
|
|
"Delete the post with ID. Returns #t if a row was deleted, #f otherwise."
|
|
|
|
|
(call-with-db pool
|
|
|
|
|
(lambda (db)
|
|
|
|
|
(let ((stmt (sqlite-prepare db "DELETE FROM posts WHERE id = ?")))
|
|
|
|
|
(sqlite-bind stmt 1 id)
|
|
|
|
|
(sqlite-step stmt)
|
|
|
|
|
(sqlite-finalize stmt)
|
|
|
|
|
(> (sqlite-changes db) 0)))))
|