20e338f972b4076df0c5c1337b21e1988fd13ffc
[clinton/lisp-on-lines.git] / src / rofl.lisp
1 (in-package :lisp-on-lines)
2
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
23 (defvar *row-reader* 'symbol-plist-row-reader)
24
25 (defun %query (query)
26 (cl-postgres:exec-query *database* (sql-compile query) *row-reader*))
27
28 (defun select (&rest query)
29 (%query (cons :select query)))
30
31 (defun prepare (&rest query)
32 (cl-postgres:prepare-query *database* "test2" (sql-compile (cons :select query))))
33
34
35 (defun select-only (num &rest query)
36 (let ((results (%query `(:limit ,(cons :select query) ,num))))
37 (if (eql 1 num)
38 (first results)
39 results)))
40
41 (defun insert-into (table &rest values-plist)
42 (postmodern:execute
43 (postmodern:sql-compile `(:insert-into ,table :set ,@values-plist))))
44
45
46 (defclass db-access-slot-definition ()
47 ((column-name :initform nil
48 :initarg :db-name
49 :initarg :column
50 :accessor slot-definition-column-name
51 :documentation
52 "If non-NIL, contains the name of the column this slot is representing.")
53 (primary-key :initform nil
54 :initarg :primary-key
55 :accessor slot-definition-primary-key-p)
56 (transient :initform nil :initarg :transient :accessor slot-definition-transient-p
57 :documentation
58 "If non-NIL, this slot should be treated as transient and
59 ignored in all database related operations.")
60 (not-null :initform nil :initarg :not-null :accessor slot-definition-not-null-p
61 :documentation "If non-NIL, a NON NULL database
62 constrained will be introduced.")
63 (foreign-type
64 :initform nil
65 :initarg :foreign-type
66 :initarg :references
67 :accessor slot-definition-foreign-type)
68 (foreign-relation
69 :initform nil
70 :initarg :referenced-from
71 :initarg :referenced-by
72 :accessor slot-definition-foreign-relation)
73 (foreign-join-spec
74 :initform nil
75 :initarg :on
76 :initarg :using
77 :accessor slot-definition-foreign-join-spec)
78 (unique :initform nil :initarg :unique :accessor slot-definition-unique)
79
80
81 (on-delete :initform :cascade :initarg :on-delete :accessor slot-definition-on-delete
82 :documentation "Action to be performed for this slot
83 when the refering row in the database ceases to exist. Possible
84 values: :CASCADE, :RESTRICT, :SET-NULL, :SET-DEFAULT. If this slot is
85 not a foreign key, it does nothing.")
86 (delayed-constraint :initform nil :accessor slot-definition-delayed-constraint
87 :documentation "Closures adding constraints
88 that, for some reason, could not be executed. If there's a slot with
89 this attribute not-NIL in a class definition, then there's something
90 wrong with its SQL counterpart.")))
91
92
93 (defclass db-access-class (standard-class)
94 ((table-name :initarg :table-name :initform nil :accessor class-table-name)
95 (indices :initarg :indices :initform () :reader class-indices)
96 (unique :initarg :unique :initform () :reader class-unique)
97 #+not!(connection-spec :initarg :connection-spec :initform nil :reader db-class-connection-spec)
98
99 (unfinished-classes :initform nil :allocation :class :accessor class-unfinished-classes
100 :documentation "A class allocated slot
101 containing classes for whom not all the constraints could be
102 applied.")
103 (foreign-keys :initform nil :accessor class-foreign-keys
104 :documentation "List of foreign-key slots.")
105 (unique-keys :initform nil :accessor class-unique-keys
106 :documentation "List of slots whose value should be unique."))
107 (:documentation "Metaclass for PostgreSQL aware classes. It takes
108 two additional arguments in DEFTABLE: :INDICES (which slots are used
109 as indices) and :CONNECTION-SPEC, which specifies how the class should
110 connect to the database (its format is the same as in
111 POSTMODERN:CONNECT-TOPLEVEL). If :CONNECTION-SPEC is not provided,
112 SUBMARINE assumes it is a class created just for the sake of
113 inheritance and does not create any tables for it."))
114
115 (defmethod validate-superclass
116 ((class db-access-class)
117 (superclass standard-class))
118 t)
119
120
121 (defclass db-access-direct-slot-definition (standard-direct-slot-definition
122 db-access-slot-definition)
123 ())
124
125 (defmethod direct-slot-definition-class
126 ((class db-access-class) &key &allow-other-keys)
127 (find-class 'db-access-direct-slot-definition))
128
129 (defclass db-access-effective-slot-definition
130 (standard-effective-slot-definition
131 db-access-slot-definition)
132 ())
133
134 (defmethod effective-slot-definition-class
135 ((class db-access-class) &key &allow-other-keys)
136 (find-class 'db-access-effective-slot-definition))
137
138 (defmethod compute-effective-slot-definition
139 ((class db-access-class) name direct-slot-definitions)
140 (declare (ignore name))
141 (let ((slotd (call-next-method)))
142 (setf (slot-definition-primary-key-p slotd)
143 (some #'slot-definition-primary-key-p direct-slot-definitions)
144 (slot-definition-column-name slotd)
145 (or (let ((slot (find-if #'slot-definition-column-name direct-slot-definitions)))
146 (when slot
147 (slot-definition-column-name slot)))
148 name)
149 (slot-definition-transient-p slotd)
150 (every #'slot-definition-transient-p direct-slot-definitions)
151 (slot-definition-foreign-type slotd)
152 (slot-definition-foreign-type (car direct-slot-definitions))
153 (slot-definition-foreign-relation slotd)
154 (slot-definition-foreign-relation (car direct-slot-definitions))
155 (slot-definition-foreign-join-spec slotd)
156 (slot-definition-foreign-join-spec (car direct-slot-definitions))
157 (slot-definition-not-null-p slotd)
158 (slot-definition-not-null-p (car direct-slot-definitions))
159 (slot-definition-unique slotd) (slot-definition-unique (car direct-slot-definitions))
160 (slot-definition-type slotd) (slot-definition-type (car direct-slot-definitions)))
161 slotd))
162
163 (defun class-id-slot-definition (class)
164 (find-if #'slot-definition-primary-key-p
165 (class-slots class)))
166
167 (defmethod class-table-name :around (class)
168 (or (call-next-method)
169 (class-name class)))
170
171 (defclass standard-db-access-class (db-access-class)
172 ())
173
174 (defun find-foreign-relations (class object slotd)
175 (when (slot-boundp object (dao-id-column-name class))
176 (select-objects (slot-definition-foreign-relation slotd)
177 :where `(:= ,(or (slot-definition-foreign-join-spec slotd)
178 (dao-id-column-name class))
179 ,(slot-value object (dao-id-column-name class))))))
180
181 (defmethod slot-boundp-using-class :around
182 ((class standard-db-access-class) object slotd)
183 (let ((bound? (call-next-method)))
184 (when (and (not bound?) (slot-definition-foreign-relation slotd))
185 (setf (slot-value-using-class class object slotd)
186 (find-foreign-relations class object slotd)))
187
188 (call-next-method)))
189
190 (defmethod slot-value-using-class :around
191 ((class standard-db-access-class) object slotd)
192 (if (slot-definition-foreign-relation slotd)
193 (if (slot-boundp-using-class class object slotd)
194 (call-next-method)
195 (setf (slot-value-using-class class object slotd)
196 (find-foreign-relations class object slotd)))
197 (call-next-method)))
198
199
200 (defun dao-id-column-name (class)
201 (slot-definition-column-name
202 (or (class-id-slot-definition class)
203 (error "No ID slot (primary key) for ~A" class))))
204
205 (defun primary-key-boundp (object)
206 (slot-boundp object (dao-id-column-name (class-of object))))
207
208 (defclass described-db-access-class (described-class standard-db-access-class)
209 ())
210
211 (defmethod initialize-instance :around ((class standard-db-access-class) &rest initargs &key name (direct-superclasses '()) direct-slots)
212 (declare (dynamic-extent initargs))
213 (let ((direct-slots (loop for slot in direct-slots
214 collect (let* ((sname (getf slot :name))
215 (readers (getf slot :readers))
216 (writers (getf slot :writers)))
217 (setf (getf slot :readers)
218 (cons (intern (format nil "~A.~A"
219 name sname)) readers))
220 (setf (getf slot :writers)
221 (cons `(setf ,(intern (format nil "~A.~A"
222 name sname))) writers))
223 slot))))
224
225
226
227 (if (loop for direct-superclass in direct-superclasses
228 thereis (ignore-errors (subtypep direct-superclass 'standard-db-access-object)))
229 (call-next-method)
230 (apply #'call-next-method
231 class
232 :direct-superclasses
233 (append direct-superclasses
234 (list (find-class 'standard-db-access-object)))
235 :direct-slots direct-slots
236 initargs))))
237
238 (defmethod reinitialize-instance :around ((class standard-db-access-class)
239 &rest initargs
240 &key (name (class-name class))
241 (direct-superclasses '() direct-superclasses-p) direct-slots)
242 (declare (dynamic-extent initargs))
243 (let ((direct-slots (loop for slot in direct-slots
244 collect (let* ((sname (getf slot :name))
245 (readers (getf slot :readers))
246 (writers (getf slot :writers)))
247 (setf (getf slot :readers)
248 (cons (intern (format nil "~A.~A"
249 name sname)) readers))
250 (setf (getf slot :writers)
251 (cons `(setf ,(intern (format nil "~A.~A"
252 name sname))) writers))
253 slot))))
254
255
256
257 (if (loop for direct-superclass in direct-superclasses
258 thereis (ignore-errors (subtypep direct-superclass 'standard-db-access-object)))
259 (call-next-method)
260 (apply #'call-next-method
261 class
262 :direct-superclasses
263 (append direct-superclasses
264 (list (find-class 'standard-db-access-object)))
265 :direct-slots direct-slots
266 initargs))))
267
268 (defclass standard-db-access-object (standard-object)
269 ())
270
271 (defun %select-objects (type select-fn query)
272 (mapcar (curry 'make-object-from-plist type)
273 (apply select-fn (intern (format nil "*"))
274 (if (string-equal (first query) :from)
275 query
276 (append `(:from ,type) query)))))
277
278 (defun select-objects (type &rest query)
279 (%select-objects type #'select query))
280
281 (defun select-only-n-objects (n type &rest query)
282 (let ((fields (if (eq :fields (car query))
283 (loop
284 :for cons :on (cdr query)
285 :if (not (keywordp (car cons)))
286 :collect (car cons) into fields
287 :else :do
288 (setf query cons)
289 (return (nreverse (print fields)))
290 :finally
291 (setf query cons)
292 (return (nreverse (print fields))))
293
294 (list (intern "*")))))
295 (let ((results
296 (%query
297 (print `(:limit (:select
298 ,@fields
299 ,@(if (string-equal (first query) :from)
300 (print query)
301 (append `(:from ,type) query)))
302 ,n)))))
303 (if (eql 1 n)
304 (make-object-from-plist type (first results))
305 (mapcar (curry 'make-object-from-plist type) results)))))
306
307 (defun make-object-from-plist (type plist)
308 (let* ((class (find-class type))
309 (object (make-instance class))
310 (slotds (class-slots class)))
311
312 (loop
313 :for (key val) :on plist :by #'cddr
314 :do
315 (dolist (slotd (remove key slotds
316 :key #'slot-definition-column-name
317 :test-not #'string-equal))
318
319 (setf (slot-value-using-class class object slotd) val))
320 :finally (return (reinitialize-instance object)))))
321
322 (defun make-object (type &rest plist)
323 (make-object-from-plist type plist))
324
325 (defun insert-object (object)
326 (let ((class (class-of object))
327 insert-query)
328 (flet ((ins (slotd &optional (val (slot-value-using-class class object slotd)))
329 (push (slot-definition-column-name slotd) insert-query)
330 (push val insert-query)))
331 (loop :for slotd in (class-slots class)
332 :do (cond ((slot-boundp-using-class class object slotd)
333 (unless (or (slot-definition-foreign-relation slotd)
334 (slot-definition-foreign-type slotd))
335 (ins slotd)))
336 ((slot-definition-primary-key-p slotd)
337 (setf (slot-value-using-class class object slotd) (get-default-value (class-table-name class)
338 (slot-definition-column-name slotd)))
339 (ins slotd ))))
340 (apply #'insert-into (class-table-name class) (nreverse insert-query))))
341 object)
342
343 (defun select-using-object (object &key (combinator :and))
344 (let ((class (class-of object))
345 select-query)
346 (flet ((sel (slotd &optional (val (slot-value-using-class class object slotd)))
347 (push `(:ilike ,(slot-definition-column-name slotd) ,(if (stringp val)
348 (format nil "~A%" val) val)) select-query)))
349 (loop :for slotd in (class-slots class)
350 :do (cond ((slot-boundp-using-class class object slotd)
351 (unless (or (slot-definition-foreign-relation slotd)
352 (slot-definition-foreign-type slotd))
353 (sel slotd)))))
354 (if select-query
355 (select-objects (class-table-name class)
356 :where (print `(,combinator ,@(nreverse select-query))))
357 nil))))
358
359
360 (defun get-default-value-query (table column)
361 (format nil "select ~A "
362 (second (select-only 1 ':adsrc
363 :from 'pg_attribute 'pg_attrdef
364 :where `(:and (:= adnum attnum)
365 (:= attname ,(s-sql::to-sql-name column))
366 (:= adrelid attrelid)
367 (:= attrelid
368 (:select oid
369 :from pg_class
370 :where (:= relname ,(s-sql::to-sql-name table)))))))))
371
372 (defun get-default-value (table column)
373 (caar (query (get-default-value-query table column))))
374
375 (defun find-dao (type id
376 &key (table (class-table-name (find-class type)))
377 id-column-name)
378
379 "Get the dao corresponding to the given primary key,
380 or return nil if it does not exist."
381 (let ((plist
382 (select-only 1 '*
383 :from table
384 :where (list ':= id (or id-column-name
385 (dao-id-column-name
386 (find-class type)))))))
387 (make-object-from-plist type plist)))
388
389 (defmethod shared-initialize :after ((dao standard-db-access-object)
390 slots &rest initargs)
391 (let ((class (class-of dao))
392 (foreign-key))
393 (dolist (slotd (class-slots class))
394 (with-slots (foreign-type) slotd
395 (when foreign-type
396 (when (consp foreign-type)
397 (setf foreign-key (cdr foreign-type)
398 foreign-type (car foreign-type)))
399 (if (slot-boundp-using-class class dao slotd)
400 (let ((value (slot-value-using-class class dao slotd))) (unless (typep value foreign-type)
401 (if (connected-p *database*)
402 (setf (slot-value-using-class class dao slotd)
403 (find-dao foreign-type value))
404 (let ((obj (make-instance foreign-type)))
405 (setf (slot-value-using-class
406 (class-of obj)
407 obj
408 (class-id-slot-definition (class-of obj)))
409 value)))))))))))
410
411 (defgeneric dao-id (dao)
412 (:method ((dao standard-db-access-object))
413 (let ((class (class-of dao)))
414
415 (slot-value-using-class class dao (class-id-slot-definition class)))))
416
417 (defun make-dao-from-row (type row &key slots)
418 (let* ((class (find-class type))
419 (dao (make-instance class))
420 (slotds (class-slots class)))
421 (loop
422 :for val :in row
423 :for slotd
424 :in (or
425 (loop
426 :for slot :in slots
427 :collect (find slot slotds
428 :key #'slot-definition-name))
429 slotds)
430 :do (setf (slot-value-using-class class dao slotd) val)
431 :finally (return (reinitialize-instance dao)))))
432
433 ;(defgeneric make-dao (type &rest initargs)
434 #+nil(defun make-dao (type initargs)
435 "Create a DAO of the given `TYPE' and initialize it according
436 to the values of the alist `INITARGS'. `Initargs' may contain
437 additional values, not used in the initialization proccess."
438 (let ((instance (make-instance type)))
439 (iter (for slot in (slots-of instance))
440 (setf (slot-value instance (slot-definition-name slot))
441 (let ((the-value (cdr (assoc (intern (symbol-name (slot-definition-name slot)) 'keyword) initargs))))
442 (if (foreign-type-p slot)
443 (make-instance (sb-pcl:slot-definition-type slot) :id the-value)
444 the-value))))
445 instance))
446
447
448
449
450