174 lines
6.1 KiB
Common Lisp
174 lines
6.1 KiB
Common Lisp
;;; cl-scopes/storage/tracking.lisp
|
|
|
|
;;;; A simple generic (SQL-based) storage for tracks, messages, and other stuff.
|
|
|
|
(defpackage :scopes/storage/tracking
|
|
(:use :common-lisp)
|
|
(:local-nicknames (:shape :scopes/shape)
|
|
(:storage :scopes/storage)
|
|
(:util :scopes/util)
|
|
(:alx :alexandria)
|
|
(:jzon :com.inuoe.jzon))
|
|
(:export #:track #:trackid #:time-stamp
|
|
#:container #:insert
|
|
#:item-class #:table-name #:indexes #:storage
|
|
#:make-item
|
|
#:get-track
|
|
#:create-indexes #:create-table))
|
|
|
|
(in-package :scopes/storage/tracking)
|
|
|
|
(defvar *build-track-data* #'alx:hash-table-plist)
|
|
|
|
(defclass track (shape:record)
|
|
((trackid :accessor trackid :initform nil)
|
|
(time-stamp :accessor time-stamp :initform nil)
|
|
(container :reader container :initarg :container)))
|
|
|
|
(defun timestamp-to-sql (tr ts)
|
|
(funcall (getf (storage:params (storage (container tr))) :ts-sql) ts))
|
|
|
|
(defun default-indexes (cont)
|
|
(maplist #'identity (item-head-fields cont)))
|
|
;'((taskid username) (username)))
|
|
|
|
(defclass container ()
|
|
((item-class :reader item-class :initarg :item-class :initform 'track)
|
|
(table-name :reader table-name :initarg :table-name :initform :tracks)
|
|
(index-factory :reader index-factory :initarg :index-factory
|
|
:initform #'default-indexes)
|
|
(storage :reader storage :initarg :storage)))
|
|
|
|
(defun item-head-fields (cont)
|
|
(shape:head-fields (make-instance (item-class cont))))
|
|
|
|
(defun make-item (cont &rest head)
|
|
(make-instance (item-class cont) :head head :container cont))
|
|
|
|
(defun query-last (cont &rest head-specs)
|
|
(query1 cont (make-where head-specs)))
|
|
|
|
(defun get-track (cont trid)
|
|
(query1 cont (make-where (list (list :trackid trid)))))
|
|
;(query1 cont (list := :trackid trid)))
|
|
|
|
(defun save (cont track)
|
|
(let ((*build-track-data* #'identity)
|
|
(tr0 (query-last cont (shape:head-plist track)))
|
|
(new-data (alx:plist-hash-table (shape:data track))))
|
|
(if tr0
|
|
(if (equalp (shape:data tr0) new-data)
|
|
nil ; or insert <= insert-when == :always
|
|
(update track new-data)) ; or insert <= insert-when == :changed
|
|
(insert track new-data))))
|
|
|
|
(defun query1 (cont crit)
|
|
(let* ((tr (make-item cont))
|
|
(st (storage cont))
|
|
(table (storage:qualified-table-name st (table-name cont)))
|
|
(cols (cons :trackid (append (shape:head-fields tr) '(:timestamp :data))))
|
|
(row (storage:normalize-plist (car (storage:query st
|
|
(sxql:select cols
|
|
(sxql:from table)
|
|
(sxql:where crit)
|
|
(sxql:order-by (:desc :timestamp))
|
|
(sxql:limit 1)))))))
|
|
(setup-track tr row)))
|
|
|
|
(defun insert (track &optional data)
|
|
(ensure-timestamp track)
|
|
(let* ((cont (container track))
|
|
(st (storage cont))
|
|
(table (storage:qualified-table-name st (table-name cont)))
|
|
(res (car (storage:query st
|
|
(sxql:insert-into table
|
|
(apply #'sxql:make-clause ':set= (plist track data))
|
|
(sxql:returning :trackid))))))
|
|
(setf (trackid track) (cadr res))
|
|
track))
|
|
|
|
(defun update (track &optional data)
|
|
(ensure-timestamp track)
|
|
(let* ((cont (container track))
|
|
(st (storage cont))
|
|
(table (storage:qualified-table-name st (table-name cont))))
|
|
(sxql:update table
|
|
(apply #'sxql:make-clause ':set= (plist track data))
|
|
(where (:= :trackid (trackid track))))))
|
|
|
|
;;;; auxiliary functions for queries, ...
|
|
|
|
(defun make-where (specs)
|
|
(let (crit)
|
|
(dolist (spec specs)
|
|
(destructuring-bind (f v &optional op) spec
|
|
(when v
|
|
(when (eq f :time-stamp)
|
|
(setf f :timestamp))
|
|
(unless op
|
|
(setf op (if (eq f :timestamp) :>= :=)))
|
|
(push (list op f v) crit))))
|
|
(if (cdr crit)
|
|
(cons :and crit)
|
|
(car crit))))
|
|
|
|
(defun setup-track (tr row)
|
|
;(log:info "tr: ~s, row: ~s" tr row)
|
|
(let ((hv (mapcar #'(lambda (x) (util:to-keyword (getf row x)))
|
|
(shape:head-fields tr))))
|
|
(setf (shape:head tr) hv)
|
|
(setf (trackid tr) (getf row :trackid))
|
|
(setf (time-stamp tr) (getf row :timestamp))
|
|
(setf (shape:data tr)
|
|
(funcall *build-track-data*
|
|
(jzon:parse (getf row :data) :key-fn #'util:to-keyword)))
|
|
tr))
|
|
|
|
(defun ensure-timestamp (track)
|
|
(if (not (time-stamp track))
|
|
(setf (time-stamp track) (get-universal-time))))
|
|
|
|
(defun plist (track &optional data)
|
|
(let ((vl (shape:head-plist track))
|
|
(data (or data (shape:data track))))
|
|
(with-slots ((trid trackid) (ts time-stamp)) track
|
|
(when trid (setf (getf vl :trackid) trid))
|
|
(when ts (setf (getf vl :timestamp) (timestamp-to-sql track ts)))
|
|
(when data
|
|
(unless (hash-table-p data)
|
|
(setf data (alx:plist-hash-table data)))
|
|
(setf (getf vl :data) (jzon:stringify data))))
|
|
vl))
|
|
|
|
;;;; create table and indexes
|
|
|
|
(defun create-table (cont)
|
|
(let* ((st (storage cont))
|
|
(tn (table-name cont))
|
|
(table (storage:qualified-table-name st tn))
|
|
(params (storage:params st))
|
|
(id-type (getf params :id-type))
|
|
(json-type (getf params :json-type))
|
|
(hf-def (mapcar #'(lambda (x) (list x :type 'text :not-null t :default '|''|))
|
|
(item-head-fields cont))))
|
|
(storage:do-sql st
|
|
(sxql:make-statement :create-table table
|
|
(nconc
|
|
`((trackid :type ,id-type :primary-key t :not-null t))
|
|
hf-def
|
|
`((timestamp :type timestamptz :not-null t :default current_timestamp)
|
|
(data :type ,json-type :not-null t :default |'{}'|)))))
|
|
(create-indexes st table tn
|
|
(funcall (index-factory cont) cont))))
|
|
|
|
(defun create-indexes (st table tname ixs)
|
|
(let ((i 1)
|
|
(tn (symbol-name tname)))
|
|
(dolist (ix ixs)
|
|
(let ((ixname (intern (format nil "IDX_~a_~d" tn i))))
|
|
(incf i)
|
|
(storage:do-sql st
|
|
(sxql:create-index ixname :on (cons table ix)))))
|
|
(storage:do-sql st
|
|
(sxql:create-index (intern (format nil "IDX_~a_TS" tn))
|
|
:on (cons table '(timestamp))))))
|