Expanded support for Configurable editing.
[clinton/lisp-on-lines.git] / src / rofl.lisp
CommitLineData
6de8d300 1(in-package :lisp-on-lines)
2
b7657b86 3;;;; NB: These could really be in upstream
4
5;;;; * A PLIST reader for postmodern.
6(postmodern::def-row-reader symbol-plist-row-reader (fields)
7 (let ((symbols (map 'list (lambda (desc)
8 (postmodern::from-sql-name (postmodern::field-name desc))) fields)))
9 (loop :while (postmodern::next-row)
10 :collect (loop :for field :across fields
11 :for symbol :in symbols
12 :nconc (list symbol (postmodern::next-field field))))))
13
14(s-sql::def-sql-op :between (n start end)
15 `(,@(s-sql::sql-expand n) " BETWEEN " ,@(s-sql::sql-expand start) " AND " ,@(s-sql::sql-expand end)))
16
17(s-sql::def-sql-op :case (&rest clauses)
18 `("CASE " ,@(loop for (test expr) in clauses collect (format nil "WHEN ~A THEN ~A " (s-sql::sql-expand test) (s-sql::sql-expand expr))) "END"))
19
20
21;;;; now the rofl code itself
22(defun %query (query)
23 (cl-postgres:exec-query *database* (sql-compile query) 'symbol-plist-row-reader))
24
25(defun select (&rest query)
26 (%query (cons :select query)))
27
28(defun select-only (num &rest query)
29 (let ((results (%query `(:limit ,(cons :select query) ,num))))
30 (if (eql 1 num)
31 (first results)
32 results)))
33
34(defun insert-into (table &rest values-plist)
35 (postmodern:execute
36 (postmodern:sql-compile `(:insert-into ,table :set ,@values-plist))))
37
6de8d300 38
39(defclass db-access-slot-definition ()
b7657b86 40 ((column-name :initform nil
41 :initarg :db-name
42 :initarg :column
43 :accessor slot-definition-column-name
6de8d300 44 :documentation
45 "If non-NIL, contains the name of the column this slot is representing.")
46 (primary-key :initform nil
47 :initarg :primary-key
48 :accessor slot-definition-primary-key-p)
49 (transient :initform nil :initarg :transient :accessor slot-definition-transient-p
50 :documentation
51 "If non-NIL, this slot should be treated as transient and
52ignored in all database related operations.")
53 (not-null :initform nil :initarg :not-null :accessor slot-definition-not-null-p
54 :documentation "If non-NIL, a NON NULL database
55constrained will be introduced.")
56 (foreign-type
57 :initform nil
58 :initarg :foreign-type
59 :initarg :references
60 :accessor slot-definition-foreign-type)
61 (unique :initform nil :initarg :unique :accessor slot-definition-unique)
62
63
64 (on-delete :initform :cascade :initarg :on-delete :accessor slot-definition-on-delete
65 :documentation "Action to be performed for this slot
66when the refering row in the database ceases to exist. Possible
67values: :CASCADE, :RESTRICT, :SET-NULL, :SET-DEFAULT. If this slot is
68not a foreign key, it does nothing.")
69 (delayed-constraint :initform nil :accessor slot-definition-delayed-constraint
70 :documentation "Closures adding constraints
71that, for some reason, could not be executed. If there's a slot with
72this attribute not-NIL in a class definition, then there's something
73wrong with its SQL counterpart.")))
74
6de8d300 75
76(defclass db-access-class (standard-class)
77 ((table-name :initarg :table-name :initform nil :accessor class-table-name)
78 (indices :initarg :indices :initform () :reader class-indices)
79 (unique :initarg :unique :initform () :reader class-unique)
80 #+not!(connection-spec :initarg :connection-spec :initform nil :reader db-class-connection-spec)
81
82 (unfinished-classes :initform nil :allocation :class :accessor class-unfinished-classes
83 :documentation "A class allocated slot
84containing classes for whom not all the constraints could be
85applied.")
86 (foreign-keys :initform nil :accessor class-foreign-keys
87 :documentation "List of foreign-key slots.")
88 (unique-keys :initform nil :accessor class-unique-keys
89 :documentation "List of slots whose value should be unique."))
90 (:documentation "Metaclass for PostgreSQL aware classes. It takes
91two additional arguments in DEFTABLE: :INDICES (which slots are used
92as indices) and :CONNECTION-SPEC, which specifies how the class should
93connect to the database (its format is the same as in
94POSTMODERN:CONNECT-TOPLEVEL). If :CONNECTION-SPEC is not provided,
95SUBMARINE assumes it is a class created just for the sake of
96inheritance and does not create any tables for it."))
97
98(defmethod validate-superclass
99 ((class db-access-class)
100 (superclass standard-class))
101 t)
102
103
104(defclass db-access-direct-slot-definition (standard-direct-slot-definition
105 db-access-slot-definition)
106 ())
107
108(defmethod direct-slot-definition-class
109 ((class db-access-class) &key &allow-other-keys)
110 (find-class 'db-access-direct-slot-definition))
111
112(defclass db-access-effective-slot-definition
113 (standard-effective-slot-definition
114 db-access-slot-definition)
115 ())
116
117(defmethod effective-slot-definition-class
118 ((class db-access-class) &key &allow-other-keys)
119 (find-class 'db-access-effective-slot-definition))
120
121(defmethod compute-effective-slot-definition
122 ((class db-access-class) name direct-slot-definitions)
123 (declare (ignore name))
124 (let ((slotd (call-next-method)))
125 (setf (slot-definition-primary-key-p slotd)
126 (some #'slot-definition-primary-key-p direct-slot-definitions)
b7657b86 127 (slot-definition-column-name slotd)
128 (or (let ((slot (find-if #'slot-definition-column-name direct-slot-definitions)))
129 (when slot
130 (slot-definition-column-name slot)))
131 name)
6de8d300 132 (slot-definition-transient-p slotd)
133 (every #'slot-definition-transient-p direct-slot-definitions)
134 (slot-definition-foreign-type slotd)
135 (slot-definition-foreign-type (car direct-slot-definitions))
136 (slot-definition-not-null-p slotd)
137 (slot-definition-not-null-p (car direct-slot-definitions))
138 (slot-definition-unique slotd) (slot-definition-unique (car direct-slot-definitions))
139 (slot-definition-type slotd) (slot-definition-type (car direct-slot-definitions)))
140 slotd))
141
142(defun class-id-slot-definition (class)
143 (find-if #'slot-definition-primary-key-p
144 (class-slots class)))
145
146(defmethod class-table-name :around (class)
147 (or (call-next-method)
148 (class-name class)))
149
150(defclass standard-db-access-class (db-access-class)
151 ())
152
153(defun dao-id-column-name (class)
154 (slot-definition-column-name
155 (or (class-id-slot-definition class)
156 (error "No ID slot (primary key) for ~A" class))))
157
158(defclass described-db-access-class (standard-db-access-class described-class)
159 ())
160
161(defmethod initialize-instance :around ((class standard-db-access-class) &rest initargs &key (direct-superclasses '()))
162 (declare (dynamic-extent initargs))
163 (if (loop for direct-superclass in direct-superclasses
164 thereis (ignore-errors (subtypep direct-superclass 'standard-db-access-object)))
165 (call-next-method)
166 (apply #'call-next-method
167 class
168 :direct-superclasses
169 (append direct-superclasses
170 (list (find-class 'standard-db-access-object)))
171 initargs)))
172
173(defmethod reinitialize-instance :around ((class standard-db-access-class) &rest initargs &key (direct-superclasses '() direct-superclasses-p))
174 (declare (dynamic-extent initargs))
175 (if (or (not direct-superclasses-p)
176 (loop for direct-superclass in direct-superclasses
177 thereis (ignore-errors (subtypep direct-superclass 'standard-db-access-object))))
178 (call-next-method)
179 (apply #'call-next-method
180 class
181 :direct-superclasses
182 (append direct-superclasses
183 (list (find-class 'standard-db-access-object)))
184 initargs)))
185
186(defclass standard-db-access-object (standard-object)
187 ())
188
b7657b86 189(defun %select-objects (type select-fn query)
190 (mapcar (curry 'make-object-from-plist type)
191 (apply select-fn (intern (format nil "*"))
192 (if (string-equal (first query) :from)
193 query
194 (append `(:from ,type) query)))))
195
196(defun select-objects (type &rest query)
197 (%select-objects type #'select query))
198
199(defun select-only-n-objects (n type &rest query)
f4efa7ff 200 (let ((fields (if (eq :fields (car query))
201 (loop
202 :for cons :on (cdr query)
203 :if (not (keywordp (car cons)))
204 :collect (car cons) into fields
205 :else :do
206 (setf query cons)
207 (return (nreverse (print fields)))
208 :finally
209 (setf query cons)
210 (return (nreverse (print fields))))
211
212 (list (intern "*")))))
213 (let ((results
214 (%query
215 (print `(:limit (:select
216 ,@fields
217 ,@(if (string-equal (first query) :from)
218 (print query)
219 (append `(:from ,type) query)))
220 ,n)))))
b7657b86 221 (if (eql 1 n)
222 (make-object-from-plist type (first results))
f4efa7ff 223 (mapcar (curry 'make-object-from-plist type) results)))))
b7657b86 224
225(defun make-object-from-plist (type plist)
226 (let* ((class (find-class type))
227 (object (make-instance class))
228 (slotds (class-slots class)))
229
230 (loop
231 :for (key val) :on plist :by #'cddr
232 :do
233 (dolist (slotd (remove key slotds
234 :key #'slot-definition-column-name
235 :test-not #'string-equal))
236
237 (setf (slot-value-using-class class object slotd) val))
238 :finally (return (reinitialize-instance object)))))
239
240(defun make-object (type &rest plist)
241 (make-object-from-plist type plist))
242
6de8d300 243
244
245(defun find-dao (type id
246 &key (table (class-table-name (find-class type)))
247 id-column-name)
248
249 "Get the dao corresponding to the given primary key,
250or return nil if it does not exist."
b7657b86 251 (let ((plist
252 (select-only 1 '*
6de8d300 253 :from table
b7657b86 254 :where (list ':= id (or id-column-name
6de8d300 255 (dao-id-column-name
b7657b86 256 (find-class type)))))))
257 (make-object-from-plist type plist)))
6de8d300 258
259(defmethod shared-initialize :after ((dao standard-db-access-object)
260 slots &rest initargs)
b7657b86 261 (let ((class (class-of dao))
262 (foreign-key))
6de8d300 263 (dolist (slotd (class-slots class))
264 (with-slots (foreign-type) slotd
265 (when foreign-type
b7657b86 266 (when (consp foreign-type)
267 (setf foreign-key (cdr foreign-type)
268 foreign-type (car foreign-type)))
6de8d300 269 (if (slot-boundp-using-class class dao slotd)
270 (let ((value (slot-value-using-class class dao slotd)))
271 (unless (typep value foreign-type)
272 (if (connected-p *database*)
273 (setf (slot-value-using-class class dao slotd)
274 (find-dao foreign-type value))
275 (let ((obj (make-instance foreign-type)))
276 (setf (slot-value-using-class
277 (class-of obj)
278 obj
279 (class-id-slot-definition (class-of obj)))
280 value)))))))))))
281
282(defgeneric dao-id (dao)
283 (:method ((dao standard-db-access-object))
284 (let ((class (class-of dao)))
285
286 (slot-value-using-class class dao (class-id-slot-definition class)))))
287
288(defun make-dao-from-row (type row &key slots)
289 (let* ((class (find-class type))
290 (dao (make-instance class))
291 (slotds (class-slots class)))
292 (loop
293 :for val :in row
294 :for slotd
295 :in (or
296 (loop
297 :for slot :in slots
298 :collect (find slot slotds
299 :key #'slot-definition-name))
300 slotds)
301 :do (setf (slot-value-using-class class dao slotd) val)
302 :finally (return (reinitialize-instance dao)))))
303
304;(defgeneric make-dao (type &rest initargs)
305#+nil(defun make-dao (type initargs)
306 "Create a DAO of the given `TYPE' and initialize it according
307 to the values of the alist `INITARGS'. `Initargs' may contain
308 additional values, not used in the initialization proccess."
309 (let ((instance (make-instance type)))
310 (iter (for slot in (slots-of instance))
311 (setf (slot-value instance (slot-definition-name slot))
312 (let ((the-value (cdr (assoc (intern (symbol-name (slot-definition-name slot)) 'keyword) initargs))))
313 (if (foreign-type-p slot)
314 (make-instance (sb-pcl:slot-definition-type slot) :id the-value)
315 the-value))))
316 instance))
317
318
319
320
321