-
Notifications
You must be signed in to change notification settings - Fork 2
/
connections.lisp
289 lines (247 loc) · 11.9 KB
/
connections.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
(in-package :clsql-helper)
(defclass connection-database ()
((names->spec :accessor names->spec :initarg :names->spec :initform nil
:documentation "A mapping of names to connection specs")
(names->conn :accessor names->conn :initarg :names->conn :initform nil
:documentation "A mapping of names to active connections")))
(defmethod print-object ((db connection-database) stream)
(print-unreadable-object (db stream)
(format stream "CONNECTION-DATABASE ~a:specs ~a:conns"
(ignore-errors (length (names->spec db)))
(ignore-errors (length (names->conn db))))))
(defvar *connection-database* (make-instance 'connection-database)
"A variable that when bound to a connection-database object will look up ")
(defun new-connection-database (&key (db *connection-database*)
name new-connection)
(make-instance 'connection-database
:names->spec (when db (names->spec db))
:names->conn (when db
(if new-connection
(cons (cons name new-connection)
(names->conn db))
(names->conn db)))))
(defun add-connection-spec (name spec &key (db *connection-database*))
(setf (access:access (names->spec db) name :type :alist)
spec))
(defun remove-connection-spec (name &key (db *connection-database*))
(setf (names->spec db)
(remove name (names->spec db) :key #'car)))
(defun get-connection-spec (name-or-spec &key (db *connection-database*))
(etypecase name-or-spec
(null nil)
(list name-or-spec)
(t (access:access (names->spec db) name-or-spec :type :alist))))
(defun find-connection (name &key ((:db *connection-database*)
*connection-database*))
(etypecase name
;; find by connection spec
(list
(iter (for (conn-name . conn) in (names->conn *connection-database*))
(when (same-database-connection? name conn)
(return conn))))
;; find by name
(symbol (access:access (names->conn *connection-database*) name :type :alist))
(clsql-sys:database name)))
(defun find-any-connection (names &key ((:db *connection-database*)
*connection-database*))
(some #'find-connection names))
(defun find-active-connection-name (name &key ((:db *connection-database*)
*connection-database*))
(etypecase name
;; find by connection spec
((or list clsql-sys:database)
(iter (for (conn-name . conn) in (names->conn *connection-database*))
(when (same-database-connection? name conn)
(return conn-name))))
;; find by name
(symbol (and (access:access (names->conn *connection-database*) name :type :alist)
name))))
(defun find-any-active-connection-name (names &key ((:db *connection-database*)
*connection-database*))
(some #'find-active-connection-name names))
(defun maybe-call (it)
(etypecase it
(null nil)
(symbol (funcall it))
(list (ecase (first it)
((quote function) (funcall (second it)))))
(function (funcall it))))
(defun %call-perhaps-logged (fn log &optional (database clsql-sys:*default-database*))
(alexandria:if-let ((log-fn (%log-fn-perhaps log)))
(log-database-command-fn fn :log-fn log-fn :database database)
(funcall fn)))
(defun coerce-connection-spec (c)
(etypecase c
(null nil)
(symbol c)
(clsql-sys:database (clsql-sys:connection-spec c))
(list (cond ((listp (first c))
(first c))
(t c)))))
(defun same-database-connection? (c1 c2)
"determines whether or not two connections are the same
by comparing their connection spec (eg '(server db user pass))
handles connection-settings, connection-specs and clsql:database"
(or
(eql c1 c2)
(equalp (coerce-connection-spec c1) (coerce-connection-spec c2))))
(defun with-database-function (fn connect-settings &key post-connect-fn log)
"Alias of with-database-context for backwards compatability
Creates a context inside which *default-database* is bound and runs the fn
inside it"
(warn "clsql-helper:with-database-function is deprecated, please call clsql-helper:with-database-context")
(with-database-context fn :connection-settings connect-settings
:post-connect-fn post-connect-fn :log log))
(defun with-database-context
(body-fn
&key
(connection-settings *connection-settings*)
(db *connection-database*)
post-connect-fn log
&aux
(*connection-settings* (or connection-settings *connection-settings*))
(*connection-database* (or db *connection-database*)))
"opens a database connection with the given settings, and runs the function.
connect-settings: a plist of connection info for clsql, also supports :post-connect-fn, a function to run after opening the connection
post-connect-fn: a function of no arguments to run after opening the connection
"
(declare (type function body-fn) (dynamic-extent body-fn))
(let* ((name *connection-settings*)
(full-spec (copy-list (get-connection-spec *connection-settings*)))
spec settings)
(unless full-spec (error "No Database connection information"))
(cond
((listp (first full-spec))
(setf spec (first full-spec)
settings (rest full-spec)))
(t (setf spec full-spec)))
(let ((settings-post-connect (getf settings :post-connect-fn)))
(setf (getf settings :make-default) nil)
(remf settings :post-connect-fn)
;; only ever disconnect the database we connect
(let ((new-db (apply #'clsql-sys:connect spec settings)))
(unwind-protect
(let ((clsql-sys:*default-database* new-db)
(*connection-database*
(new-connection-database
:name name
:new-connection new-db)))
;; call post-connect if needed
(maybe-call post-connect-fn)
(maybe-call settings-post-connect)
(return-from with-database-context
(%call-perhaps-logged body-fn log)))
(clsql-sys:disconnect :database new-db))))))
(defun with-a-database-context
(body-fn &key
(connection-settings *connection-settings*)
(db *connection-database*)
post-connect-fn log
&aux existing-connection
(*connection-settings* (or connection-settings *connection-settings*))
(*connection-database* (or db *connection-database*)))
"If a database connection exists and it matches the passed in settings or
the passed in settings are null, use it!, otherwise aquire a new database
connection"
(declare (dynamic-extent body-fn))
;; handle logging here so that whether or not we get a new db connection
;; we get a logger if needed
(flet ((logged-with-a-database-context-body () (%call-perhaps-logged body-fn log)))
(declare (dynamic-extent #'logged-with-a-database-context-body))
;; if we've got an open connection with the same spec, reuse it
(cond ((same-database-connection? *connection-settings* clsql-sys:*default-database*)
(logged-with-a-database-context-body))
((setf existing-connection
(find-connection *connection-settings*))
(let ((clsql-sys:*default-database* existing-connection))
(logged-with-a-database-context-body)))
((and (null *connection-settings*) clsql-sys:*default-database*)
(logged-with-a-database-context-body))
((and (null *connection-settings*) (null clsql-sys:*default-database*))
(error "No database connection available, please provide a clsql::*default-database*"))
(t (with-database-context
#'logged-with-a-database-context-body
:post-connect-fn post-connect-fn )))))
(defmacro with-database ((&optional (connection-settings *connection-settings*)
&key post-connect-fn log)
&body body)
"opens a database connection and executes the body
connect-settings: a plist of connection info for clsql, also supports :post-connect-fn, a function to run after opening the connection
post-connect-fn: a function of no arguments to run after opening the connection "
`(flet ((with-database-body () ,@body))
(declare (dynamic-extent #'with-database-body))
(with-database-context
#'with-database-body
:connection-settings ,connection-settings
:post-connect-fn ,post-connect-fn
:log ,log)))
(defmacro with-a-database ((&optional (connection-settings '*connection-settings*)
&key post-connect-fn log)
&body body)
"If a database connection exists and it matches the passed in settings or
the passed in settings are null, use it!, otherwise aquire a new database
connection"
`(flet ((with-a-database-body () ,@body))
(declare (dynamic-extent #'with-a-database-body))
(with-a-database-context
#'with-a-database-body
:connection-settings ,connection-settings
:post-connect-fn ,post-connect-fn
:log ,log)))
(defvar *thread-local-transaction-catch-tag* nil
"Variable to hold the gensymed catch tag this thread's with-a-transaction is using.")
(defvar *inner-transaction-error* nil
"Variable to hold conditions from internal errors, just makes for a bit cleaner code I think.")
(define-condition rollback ()
())
(define-condition commit ()
())
(defun with-transaction-context (body-fn database)
"Establish a context for enlisting in transactions and run the body in a new transaction"
;(declare (type function body-fn) (dynamic-extent body-fn))
(let ((*thread-local-transaction-catch-tag* (gensym "w/tran-ct-"))
*inner-transaction-error*
rtn)
(catch *thread-local-transaction-catch-tag*
(setf rtn
(multiple-value-list
(clsql::with-transaction (:database database) (funcall body-fn))))
;; we made it through the transaction without aborting to the catch tag
(signal 'commit))
(if *inner-transaction-error*
(error *inner-transaction-error*)
(apply #'values rtn))))
(defun with-a-transaction-context (body-fn database)
"Either establish a new transaction context (with-transaction-context) or
run the body in the extisting transaction context "
;(declare (type function body-fn) (dynamic-extent body-fn))
;; abort-database-transaction causes this to be false, even if its the same
;; connection we aborted the transaction on, thus the catch tag hoop jumping
(if (clsql-sys::in-transaction-p :database database)
;; any error in a nested with-a-transaction, should abort the
;; entire transaction, we used to accomplish this by throwing up
;; the stack and swallowing the error which was a decidedly bad
;; approach
(handler-bind
((error
(lambda (c)
(setf *inner-transaction-error* c) ;; convenience
;; in debugging is very much helpful to have the orignal
;; error context
(if *debugger-hook*
(invoke-debugger c)
(throw *thread-local-transaction-catch-tag* c)
))))
(funcall body-fn))
;; dont have a transaction so lets create one
(with-transaction-context body-fn database)))
(defmacro with-a-transaction ((&key (database 'clsql-sys:*default-database*)) &body body)
"Wrapper around clsql:with-transaction, when a rollback is issued the code
escapes (throw) to the outermost with-a-transaction.
without the catch, its possible for an error handler in an intermediate function
"
`(labels ((with-a-transaction-body-fn () ,@body))
(declare (dynamic-extent #'with-a-transaction-body-fn))
(with-a-transaction-context
#'with-a-transaction-body-fn
,database)))