safsaf/examples/blog-site/model.scm

145 lines
4.3 KiB
Scheme
Raw Normal View History

(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")
(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
;;;
(define (db-init! pool)
"Create the schema if it doesn't exist."
(call-with-db pool
(lambda (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'))
)"))))
;;;
;;; 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)))))))
(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)))))