r10146: 09 Nov 2004 Kevin Rosenberg <kevin@rosenberg.net>
[clsql.git] / sql / database.lisp
1 ;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
2 ;;;; *************************************************************************
3 ;;;;
4 ;;;; $Id$
5 ;;;;
6 ;;;; Base database functions
7 ;;;;
8 ;;;; This file is part of CLSQL.
9 ;;;;
10 ;;;; CLSQL users are granted the rights to distribute and use this software
11 ;;;; as governed by the terms of the Lisp Lesser GNU Public License
12 ;;;; (http://opensource.franz.com/preamble.html), also known as the LLGPL.
13 ;;;; *************************************************************************
14
15 (in-package #:clsql-sys)
16
17
18 (defvar *connect-if-exists* :error
19   "Default value for the if-exists keyword argument in calls to
20 CONNECT. Meaningful values are :new, :warn-new, :error, :warn-old
21 and :old.")
22
23 (defvar *connected-databases* nil
24   "List of active database objects.")
25
26 (defun connected-databases ()
27   "Returns the list of active database objects."
28   *connected-databases*)
29
30 (defvar *default-database* nil
31   "Specifies the default database to be used.")
32
33 (defun is-database-open (database)
34   (eql (database-state database) :open))
35
36 (defun find-database (database &key (errorp t) (db-type nil))
37   "Returns the connected databases of type DB-TYPE whose names
38 match the string DATABASE. If DATABASE is a database object, it
39 is returned. If DB-TYPE is nil all databases matching the string
40 DATABASE are considered.  If no matching databases are found and
41 ERRORP is nil then nil is returned. If ERRORP is nil and one or
42 more matching databases are found, then the most recently
43 connected database is returned as a first value and the number of
44 matching databases is returned as a second value. If no, or more
45 than one, matching databases are found and ERRORP is true, an
46 error is signalled."
47   (etypecase database
48     (database
49      (values database 1))
50     (string
51      (let* ((matches (remove-if 
52                       #'(lambda (db)
53                           (not (and (string= (database-name db) database)
54                                     (if db-type
55                                         (equal (database-type db) db-type)
56                                         t))))
57                       (connected-databases)))
58             (count (length matches)))
59        (if (or (not errorp) (= count 1))
60            (values (car matches) count)
61            (cerror "Return nil."
62                    'sql-database-error
63                    :message
64                    (format nil "There exists ~A database called ~A."
65                            (if (zerop count) "no" "more than one")
66                            database)))))))
67
68
69 (defun connect (connection-spec
70                 &key (if-exists *connect-if-exists*)
71                 (make-default t)
72                 (pool nil)
73                 (database-type *default-database-type*))
74   "Connects to a database of the supplied DATABASE-TYPE which
75 defaults to *DEFAULT-DATABASE-TYPE*, using the type-specific
76 connection specification CONNECTION-SPEC. The value of IF-EXISTS,
77 which defaults to *CONNECT-IF-EXISTS*, determines what happens if
78 a connection to the database specified by CONNECTION-SPEC is
79 already established.  A value of :new means create a new
80 connection.  A value of :warn-new means warn the user and create
81 a new connect.  A value of :warn-old means warn the user and use
82 the old connection.  A value of :error means fail, notifying the
83 user.  A value of :old means return the old connection.
84 MAKE-DEFAULT is t by default which means that *DEFAULT-DATABASE*
85 is set to the new connection, otherwise *DEFAULT-DATABASE* is not
86 changed. If POOL is t the connection will be taken from the
87 general pool, if POOL is a CONN-POOL object the connection will
88 be taken from this pool."
89
90   (unless database-type
91     (error 'sql-database-error :message "Must specify a database-type."))
92   
93   (when (stringp connection-spec)
94     (setq connection-spec (string-to-list-connection-spec connection-spec)))
95   
96   (unless (member database-type *loaded-database-types*)
97     (asdf:operate 'asdf:load-op (ensure-keyword
98                                  (concatenate 'string 
99                                               (symbol-name '#:clsql-)
100                                               (symbol-name database-type)))))
101
102   (if pool
103       (acquire-from-pool connection-spec database-type pool)
104       (let* ((db-name (database-name-from-spec connection-spec database-type))
105              (old-db (unless (eq if-exists :new)
106                        (find-database db-name :db-type database-type
107                                       :errorp nil)))
108              (result nil))
109         (if old-db
110             (ecase if-exists
111               (:warn-new
112                (setq result
113                      (database-connect connection-spec database-type))
114                (warn 'sql-warning
115                      :message
116                      (format nil
117                              "Created new connection ~A to database ~A~%, although there is an existing connection (~A)."
118                              result (database-name result) old-db)))
119               (:error
120                (restart-case
121                    (error 'sql-connection-error
122                           :message
123                           (format nil "There is an existing connection ~A to database ~A."
124                           old-db
125                           (database-name old-db)))
126                  (create-new ()
127                    :report "Create a new connection."
128                    (setq result
129                          (database-connect connection-spec database-type)))
130                  (use-old ()
131                    :report "Use the existing connection."
132                    (setq result old-db))))
133               (:warn-old
134                (setq result old-db)
135                (warn 'sql-warning
136                      :message
137                      (format nil
138                              "Using existing connection ~A to database ~A."
139                              old-db
140                              (database-name old-db))))
141               (:old
142                (setq result old-db)))
143             (setq result
144                   (database-connect connection-spec database-type)))
145         (when result
146           (setf (slot-value result 'state) :open)
147           (pushnew result *connected-databases*)
148           (when make-default (setq *default-database* result))
149           result))))
150
151
152 (defun disconnect (&key (database *default-database*) (error nil))
153
154   "Closes the connection to DATABASE and resets
155 *DEFAULT-DATABASE* if that database was disconnected. If DATABASE
156 is a database instance, this object is closed. If DATABASE is a
157 string, then a connected database whose name matches DATABASE is
158 sought in the list of connected databases. If no matching
159 database is found and ERROR and DATABASE are both non-nil an
160 error is signaled, otherwise nil is returned. If the database is
161 from a pool it will be released to this pool."
162   (let ((database (find-database database :errorp (and database error))))
163     (when database
164       (if (conn-pool database)
165           (when (release-to-pool database)
166             (setf *connected-databases* (delete database *connected-databases*))
167             (when (eq database *default-database*)
168               (setf *default-database* (car *connected-databases*)))
169             t)
170           (when (database-disconnect database)
171             (setf *connected-databases* (delete database *connected-databases*))
172             (when (eq database *default-database*)
173               (setf *default-database* (car *connected-databases*)))
174             (setf (slot-value database 'state) :closed)
175             t)))))
176
177
178 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
179
180
181 (defmacro check-connection-spec (connection-spec database-type template)
182   "Check the connection specification against the provided template,
183 and signal an sql-user-error if they don't match. This function
184 is called by database backends."
185   `(handler-case
186     (destructuring-bind ,template ,connection-spec 
187       (declare (ignore ,@(remove '&optional template)))
188       t)
189     (error () 
190      (error 'sql-user-error
191       :message
192       (format nil 
193               "The connection specification ~A~%is invalid for database type ~A.~%The connection specification must conform to ~A"
194               ,connection-spec
195               ,database-type
196               (quote ,template))))))
197
198 (defun reconnect (&key (database *default-database*) (error nil) (force t))
199   "Reconnects DATABASE which defaults to *DEFAULT-DATABASE* to
200 the underlying database management system. On success, t is
201 returned and the variable *DEFAULT-DATABASE* is set to the newly
202 reconnected database. If DATABASE is a database instance, this
203 object is closed. If DATABASE is a string, then a connected
204 database whose name matches DATABASE is sought in the list of
205 connected databases. If no matching database is found and ERROR
206 and DATABASE are both non-nil an error is signaled, otherwise nil
207 is returned. When the current database connection cannot be
208 closed, if FORCE is non-nil, as it is by default, the connection
209 is closed and errors are suppressed. If force is nil and the
210 database connection cannot be closed, an error is signalled."
211   (let ((db (etypecase database
212               (database database)
213               ((or string list)
214                (let ((db (find-database database :errorp nil)))
215                  (when (null db)
216                    (if (and database error)
217                        (error 'sql-connection-error
218                               :message
219                               (format nil "Unable to find database with connection-spec ~A." database))
220                        (return-from reconnect nil)))
221                  db)))))
222                               
223     (when (is-database-open db)
224       (if force
225           (ignore-errors (disconnect :database db))
226           (disconnect :database db :error nil)))
227     
228     (connect (connection-spec db))))
229
230   
231 (defun status (&optional full)
232   "Prints information about the currently connected databases to
233 *STANDARD-OUTPUT*. The argument FULL is nil by default and a
234 value of t means that more detailed information about each
235 database is printed."
236   (flet ((get-data ()
237            (let ((data '()))
238              (dolist (db (connected-databases) data)
239                (push 
240                 (append 
241                  (list (if (equal db *default-database*) "*" "")        
242                        (database-name db)
243                        (string-downcase (string (database-type db)))
244                        (cond ((and (command-recording-stream db) 
245                                    (result-recording-stream db)) 
246                               "Both")
247                              ((command-recording-stream db) "Commands")
248                              ((result-recording-stream db) "Results")
249                              (t "nil")))
250                  (when full 
251                    (list 
252                     (if (conn-pool db) "t" "nil")
253                     (format nil "~A" (length (database-list-tables db)))
254                     (format nil "~A" (length (database-list-views db))))))
255                 data))))
256          (compute-sizes (data)
257            (mapcar #'(lambda (x) (apply #'max (mapcar #'length x)))
258                    (apply #'mapcar (cons #'list data))))
259          (print-separator (size)
260            (format t "~&~A" (make-string size :initial-element #\-))))
261     (format t "~&CLSQL STATUS: ~A~%" (iso-timestring (get-time)))
262     (let ((data (get-data)))
263       (when data
264         (let* ((titles (if full 
265                            (list "" "DATABASE" "TYPE" "RECORDING" "POOLED" 
266                                  "TABLES" "VIEWS")
267                            (list "" "DATABASE" "TYPE" "RECORDING")))
268                (sizes (compute-sizes (cons titles data)))
269                (total-size (+ (apply #'+ sizes) (* 2 (1- (length titles)))))
270                (control-string (format nil "~~&~~{~{~~~AA  ~}~~}" sizes)))
271           (print-separator total-size)
272           (format t control-string titles)
273           (print-separator total-size)
274           (dolist (d data) (format t control-string d))
275           (print-separator total-size))))
276     (values)))
277
278 (defun create-database (connection-spec &key database-type)
279   "This function creates a database in the database system specified
280 by DATABASE-TYPE."
281   (when (stringp connection-spec)
282     (setq connection-spec (string-to-list-connection-spec connection-spec)))
283   (database-create connection-spec database-type))
284
285 (defun probe-database (connection-spec &key database-type)
286   "This function tests for the existence of a database in the database
287 system specified by DATABASE-TYPE."
288   (when (stringp connection-spec)
289     (setq connection-spec (string-to-list-connection-spec connection-spec)))
290   (database-probe connection-spec database-type))
291
292 (defun destroy-database (connection-spec &key database-type)
293   "This function destroys a database in the database system specified
294 by DATABASE-TYPE."
295   (when (stringp connection-spec)
296     (setq connection-spec (string-to-list-connection-spec connection-spec)))
297   (database-destroy connection-spec database-type))
298
299 (defun list-databases (connection-spec &key database-type)
300   "This function returns a list of databases existing in the database
301 system specified by DATABASE-TYPE."
302   (when (stringp connection-spec)
303     (setq connection-spec (string-to-list-connection-spec connection-spec)))
304   (database-list connection-spec database-type))
305
306 (defmacro with-database ((db-var connection-spec &rest connect-args) &body body)
307   "Evaluate the body in an environment, where DB-VAR is bound to the
308 database connection given by CONNECTION-SPEC and CONNECT-ARGS.  The
309 connection is automatically closed or released to the pool on exit
310 from the body."
311   (let ((result (gensym "result-")))
312     (unless db-var (setf db-var '*default-database*))
313     `(let ((,db-var (connect ,connection-spec ,@connect-args))
314            (,result nil))
315       (unwind-protect
316            (let ((,db-var ,db-var))
317              (setf ,result (progn ,@body)))
318         (disconnect :database ,db-var))
319       ,result)))
320
321
322 (defmacro with-default-database ((database) &rest body)
323   "Perform BODY with DATABASE bound as *default-database*."
324   `(progv '(*default-database*)
325        (list ,database)
326      ,@body))
327