#| src/db.lisp Database / connection skeleton generated by Caveman. Additional changes: WITH-DATAFLY-CONNECTION :: Same as WITH-CONNECTION, but we use DATAFLY:*CONNECTION* instead of MITO.CORE:*CONNECTION*. WITH-DATAFLY-TRANSACTION is the same. TODO: I would like to be able to specify :MITO or :DATAFLY. TODO Test this stuff DONE Caveman has a db module and dbi manager middleware - Looks useless. We already have CONNECT-CACHED in db.lisp. |# (in-package :cl-user) (defpackage #:cl-deck-builder2.db (:use #:cl) (:import-from #:cl-deck-builder2.config #:config) (:import-from #:cl-deck-builder2.toolkit #:grouped #:relative-pathname) (:import-from #:cl-dbi #:connect-cached) (:export #:connection-settings #:db #:with-connection #:with-transaction #:with-datafly-connection #:with-datafly-transaction #:with-includes #:do-grouped-insert #:count-dao #:create-dao #:delete-by-values #:delete-from #:delete-dao #:create-table #:drop-table #:recreate-table #:find-dao #:find-or-make-instance #:find-or-create-instance #:insert-dao #:max-dao #:retrieve-by-sql #:retrieve-dao #:save-dao #:select-dao #:update-dao) (:documentation "The database package. This is more than just the default generated by Caveman2 skeleton. It provides convenience functions for accessing database information and objects.")) (in-package #:cl-deck-builder2.db) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun connection-settings (&optional (db :maindb)) "Query the Caveman configuration for these database settings. DB is the name of the configuration query, e.g. :MAINDB." (cdr (assoc db (config :databases)))) (defun db (&optional (db :maindb)) "Get a handle to an open database, possibly cached with CONNECT-CACHED." (apply #'connect-cached (connection-settings db))) (defmacro with-connection (conn &body body) "Wrap BODY using LEXICAL-LET to bind MITO.CORE:*CONNECTION* to CONN." ;; TODO ;; `(if (and (listp ,conn) ;; (eq (type-of ,(car conn)) 'cl-dbi::dbi-connection)) ;; (let ((,(car conn) ,(cdr conn))) ;; ,@body)) `(let ((mito.core:*connection* ,conn)) ,@body)) (defmacro with-transaction (&body body) "Wrap BODY with CL-DBI:WITH-TRANSACTION." `(cl-dbi:with-transaction mito.core:*connection* ,@body)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; What's a better way to do this? DEFGENERIC? (defmacro with-datafly-connection (conn &body body) "Wrap BODY using LEXICAL-LET to bind DATAFLY:*CONNECTION* to CONN." `(let ((datafly:*connection* ,conn)) ;; (datafly:*trace-sql* t)) ,@body)) (defmacro with-datafly-transaction (&body body) "Wrap BODY with CL-DBI:WITH-TRANSACTION. Datafly variant." `(cl-dbi:with-transaction datafly:*connection* ,@body)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; I didn't realize that this implicitly only allows SELECT-DAO. I ;; guess in the MITO documentation he only allows SELECT-DAO to have ;; MITO:INCLUDES machinery. Where is that captured? For now, I assume ;; you know this, and that WITH-INCLUDES is only compatible with ;; SELECT-DAO anyway. (defmacro with-includes (class includes &body body) `(mito:select-dao ,class ,includes ,@body)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; What happened here? It used to work now it's returning NIL. A LOOP ;; is nicer than a DOLIST+SETF but... (defun do-grouped-insert (obj &key (n 1000) (fn #'mito:insert-dao)) "Batch INSERT-DAO to the database. Display a progress bar." (let ((pos 0) (width 50) (grouped (grouped obj n))) (with-connection (db) (loop for group in grouped collect (prog1 (with-transaction (mapcan fn group)) (format t ".") (if (>= pos (1- width)) (progn (setf pos 0) (format t "~%")) (incf pos)) (finish-output)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TODO FIND-DAO-OR-MAKE-INSTANCE? ;; TODO Maybe merge FIND-OR-MAKE-INSTANCE and FIND-OR-CREATE-INSTANCE (defun find-or-make-instance (class &rest args) "Wrap MITO:FIND-DAO and if no result is returned, issue MAKE-INSTANCE for CLASS." (with-connection (db) (with-transaction (or (apply #'mito:find-dao class args) (apply #'make-instance class args))))) (defun find-or-create-instance (class &rest args) "Wrap MITO:FIND-DAO and if no result is returned, issue CREATE-DAO for CLASS." (with-connection (db) (with-transaction (or (apply #'mito:find-dao class args) (apply #'mito:create-dao class args))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DAOs (defmacro select-dao (class &body body) "Select an object of CLASS from the database by wrapping MITO:SELECT-DAO in WITH-CONNECTION etc. Note that SELECT-DAO is a DEFMACRO form. You may additionally specify BODY parameters that are SXQL statements." `(with-connection (db) (with-transaction (mito:select-dao ,class ,@body)))) (defun delete-from (class &rest clauses) "Delete entries for CLASS by wrapping SXQL:DELETE-FROM. You may additionally specify CLAUSES parameters that are SXQL statements." (with-connection (db) (with-transaction (mito:execute-sql (apply #'sxql:make-statement :delete-from (sxql:make-sql-symbol (mito.class:table-name (find-class class))) clauses))))) (defun count-dao (class &rest fields-and-values) "Count the number of entries matching this CLASS using FIELDS-AND-VALUES. Simply wrap MITO:COUNT-DAO in WITH-CONNECTION etc." (with-connection (db) (with-transaction (apply #'mito:count-dao class fields-and-values)))) (defun create-dao (class &rest initargs) "Create an entry for this CLASS by wrapping MITO:CREATE-DAO in WITH-CONNECTION etc." (with-connection (db) (with-transaction (apply #'mito:create-dao class initargs)))) (defun max-dao (class &rest fields-and-values &key (field :id)) "Select the aggregate MAX from the SQL database and return the result, if any. You may additionally specify the FIELD to find the MAX by." (setf class (mito.util::ensure-class class)) (let ((sql (sxql:select ((:as (:ifnull (:max field) 0) :max)) (sxql:from (sxql:make-sql-symbol (mito.class:table-name class)))))) (when fields-and-values (sxql:add-child sql (mito.dao::where-and fields-and-values class))) (getf (first (retrieve-by-sql sql)) :max))) (defun delete-by-values (class &rest fields-and-values) "Delete an entry for this CLASS by wrapping MITO:DELETE-BY-VALUES. You may specify additional FIELDS-AND-VALUES to narrow your search." (with-connection (db) (with-transaction (apply #'mito:delete-by-values class fields-and-values)))) (defun delete-dao (obj) (with-connection (db) (with-transaction (mito:delete-dao obj)))) (defun find-dao (class &rest fields-and-values) "Find an entry for this CLASS, if it exists, by wrapping MITO:FIND-DAO. You may specify additional FIELDS-AND-VALUES to narrow your search." (with-connection (db) (with-transaction (apply #'mito:find-dao class fields-and-values)))) (defun insert-dao (obj) "Insert an existing object OBJ into the database by wrapping MITO:INSERT-DAO in WITH-CONNECTION etc." (with-connection (db) (with-transaction (mito:insert-dao obj)))) (defun save-dao (obj) "Save an existing object OBJ into the database by wrapping MITO:SAVE-DAO in WITH-CONNECTION etc." (with-connection (db) (with-transaction (mito:save-dao obj)))) (defun update-dao (obj) "Update an existing object OBJ into the database by wrapping MITO:UPDATE-DAO in WITH-CONNECTION etc." (with-connection (db) (with-transaction (mito:update-dao obj)))) (defun retrieve-by-sql (sql &key binds) "Retrieve a query from the database by wrapping MITO:RETRIEVE-BY-SQL in WITH-CONNECTION etc. I don't know what BINDS does. I think it has something to do with passing parameters to queries." (with-connection (db) (with-transaction (mito:retrieve-by-sql sql :binds binds)))) (defun retrieve-dao (class &rest fields-and-values) "Retrieve a row for this CLASS, if it exists, by wrapping MITO:RETRIEVE-DAO. You may specify additional FIELDS-AND-VALUES to narrow your search." (with-connection (db) (with-transaction (apply #'mito:retrieve-dao class fields-and-values)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Tables (defun create-table (&rest classes) "Create tables for CLASSES by wrapping MITO:ENSURE-TABLE-EXISTS. CLASSES may be a list of classes to create corresponding tables for." (with-connection (db) (with-transaction (mapcar (alexandria:compose #'mito:ensure-table-exists #'find-class) classes)))) (defun drop-table (&rest classes) "Drop tables for CLASSES by wrapping SXQL:DROP-TABLE. CLASSES may be a list of classes to drop corresponding tables for." (with-connection (db) (with-transaction (mapcar (lambda (class) (mito:execute-sql (sxql:drop-table (sxql:make-sql-symbol (mito.class:table-name (find-class class))) :if-exists t))) classes)))) (defun recreate-table (&rest classes) "DROP-TABLE then CREATE-TABLE for CLASSES." (mapcar #'drop-table classes) (mapcar #'create-table classes))