whatsxmpp/sqlite.lisp

68 lines
2.9 KiB
Common Lisp

(in-package :whatsxmpp)
(defvar *db* nil
"Connection to the database.")
(defparameter *default-database-path* "data.sqlite3"
"Default path to the SQLite database file.")
(defvar *prepared-statements* nil
"List of statements prepared by PREPARED-STATEMENT.")
(defparameter *sqlite-pragmas*
'("PRAGMA journal_mode = WAL"
"PRAGMA foreign_keys = ON"
"PRAGMA synchronous = NORMAL")
"List of SQLite pragmas to run on connection to make things bearable")
(defun run-pragmas ()
"Runs all statements in *SQLITE-PRAGMAS*."
(mapc (lambda (x) (sqlite:execute-non-query *db* x)) *sqlite-pragmas*))
(defun connect-database (&optional (path *default-database-path*))
"Establish a connection to the database."
(setf *db* (sqlite:connect path))
(run-pragmas)
(loop for sym in *prepared-statements*
do (eval `(setf ,sym nil)))
(setf *prepared-statements* nil))
(defmacro prepared-statement (statement)
"Caches the creation of a prepared statement with SQL text STATEMENT.
In other words, prepares STATEMENT once, then returns the prepared statement after that instead of doing that work again."
(let ((statement-sym (gensym "PREPARED-STATEMENT-")))
(eval `(defvar ,statement-sym nil))
`(progn
(defvar ,statement-sym nil)
(unless ,statement-sym
(setf ,statement-sym (sqlite:prepare-statement *db* ,statement))
(setf *prepared-statements* (cons ',statement-sym *prepared-statements*)))
,statement-sym)))
(defmacro with-prepared-statement ((name statement) &body forms)
"Evaluates FORMS, binding a prepared statement with SQL text STATEMENT to NAME, and ensuring it is reset when control is transferred."
`(let ((,name (prepared-statement ,statement)))
(unwind-protect
(progn ,@forms)
(ignore-errors (sqlite:reset-statement ,name)))))
(defmacro with-prepared-statements (statements &body forms)
"Like WITH-PREPARED-STATEMENT, but takes multiple statements."
(let ((let-forms (loop for (name statement) in statements
collect `(,name (prepared-statement ,statement))))
(reset-forms (loop for (name statement) in statements
collect `(ignore-errors (sqlite:reset-statement ,name)))))
`(let (,@let-forms)
(unwind-protect
(progn ,@forms))
(ignore-errors (progn ,@reset-forms)))))
(defmacro bind-parameters (statement &rest parameters)
"Binds PARAMETERS to the prepared statement STATEMENT.
PARAMETERS are either simple values (in which case they're bound to parameters 1, 2, ...),
or cons cells, where the `car` is the index to bind to and the `cdr' is the value to use."
`(progn
,@(loop for param in parameters
for idx from 1 upto (length parameters)
collect (if (listp param)
`(sqlite:bind-parameter ,statement ,(car param) ,(second param))
`(sqlite:bind-parameter ,statement ,idx ,param)))))