r9113: intial changes for list-table-indexes
[clsql.git] / db-postgresql-socket / postgresql-socket-sql.lisp
1 ;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
2 ;;;; *************************************************************************
3 ;;;; FILE IDENTIFICATION
4 ;;;;
5 ;;;; Name:          postgresql-socket-sql.sql
6 ;;;; Purpose:       High-level PostgreSQL interface using socket
7 ;;;; Programmers:   Kevin M. Rosenberg based on
8 ;;;;                Original code by Pierre R. Mai 
9 ;;;; Date Started:  Feb 2002
10 ;;;;
11 ;;;; $Id$
12 ;;;;
13 ;;;; This file, part of CLSQL, is Copyright (c) 2002 by Kevin M. Rosenberg
14 ;;;; and Copyright (c) 1999-2001 by Pierre R. Mai
15 ;;;;
16 ;;;; CLSQL users are granted the rights to distribute and use this software
17 ;;;; as governed by the terms of the Lisp Lesser GNU Public License
18 ;;;; (http://opensource.franz.com/preamble.html), also known as the LLGPL.
19 ;;;; *************************************************************************
20
21 (in-package #:cl-user)
22
23 (defpackage :clsql-postgresql-socket
24     (:use #:common-lisp #:clsql-base-sys #:postgresql-socket)
25     (:export #:postgresql-socket-database)
26     (:documentation "This is the CLSQL socket interface to PostgreSQL."))
27
28 (in-package #:clsql-postgresql-socket)
29
30 ;; interface foreign library loading routines
31
32
33 (clsql-base-sys:database-type-load-foreign :postgresql-socket)
34
35
36 ;; Field type conversion
37
38 (defun make-type-list-for-auto (cursor)
39   (let* ((fields (postgresql-cursor-fields cursor))
40          (num-fields (length fields))
41          (new-types '()))
42     (dotimes (i num-fields)
43       (declare (fixnum i))
44       (push (canonical-field-type fields i) new-types))
45     (nreverse new-types)))
46
47 (defun canonical-field-type (fields index)
48   "Extracts canonical field type from fields list"
49   (let ((oid (cadr (nth index fields))))
50     (case oid
51       ((#.pgsql-ftype#bytea
52         #.pgsql-ftype#int2
53         #.pgsql-ftype#int4)
54        :int32)
55       (#.pgsql-ftype#int8
56        :int64)
57       ((#.pgsql-ftype#float4
58         #.pgsql-ftype#float8)
59        :double)
60       (otherwise
61        t))))
62
63 (defun canonicalize-types (types cursor)
64   (if (null types)
65       nil
66       (let ((auto-list (make-type-list-for-auto cursor)))
67         (cond
68           ((listp types)
69            (canonicalize-type-list types auto-list))
70           ((eq types :auto)
71            auto-list)
72           (t
73            nil)))))
74
75 (defun canonicalize-type-list (types auto-list)
76   "Ensure a field type list meets expectations.
77 Duplicated from clsql-uffi package so that this interface
78 doesn't depend on UFFI."
79   (let ((length-types (length types))
80         (new-types '()))
81     (loop for i from 0 below (length auto-list)
82           do
83           (if (>= i length-types)
84               (push t new-types) ;; types is shorted than num-fields
85               (push
86                (case (nth i types)
87                  (:int
88                   (case (nth i auto-list)
89                     (:int32
90                      :int32)
91                     (:int64
92                      :int64)
93                     (t
94                      t)))
95                  (:double
96                   (case (nth i auto-list)
97                     (:double
98                      :double)
99                     (t
100                      t)))
101                  (t
102                   t))
103                new-types)))
104     (nreverse new-types)))
105
106
107 (defun convert-to-clsql-warning (database condition)
108   (warn 'clsql-database-warning :database database
109         :message (postgresql-condition-message condition)))
110
111 (defun convert-to-clsql-error (database expression condition)
112   (error 'clsql-sql-error :database database
113          :expression expression
114          :errno (type-of condition)
115          :error (postgresql-condition-message condition)))
116
117 (defmacro with-postgresql-handlers
118     ((database &optional expression)
119      &body body)
120   (let ((database-var (gensym))
121         (expression-var (gensym)))
122     `(let ((,database-var ,database)
123            (,expression-var ,expression))
124        (handler-bind ((postgresql-warning
125                        (lambda (c)
126                          (convert-to-clsql-warning ,database-var c)))
127                       (postgresql-error
128                        (lambda (c)
129                          (convert-to-clsql-error
130                           ,database-var ,expression-var c))))
131          ;; KMR - removed double @@
132          ,@body))))
133
134 (defmethod database-initialize-database-type ((database-type
135                                                (eql :postgresql-socket)))
136   t)
137
138 (defclass postgresql-socket-database (database)
139   ((connection :accessor database-connection :initarg :connection
140                :type postgresql-connection)))
141
142 (defmethod database-type ((database postgresql-socket-database))
143   :postgresql-socket)
144
145 (defmethod database-name-from-spec (connection-spec
146                                     (database-type (eql :postgresql-socket)))
147   (check-connection-spec connection-spec database-type
148                          (host db user password &optional port options tty))
149   (destructuring-bind (host db user password &optional port options tty)
150       connection-spec
151     (declare (ignore password options tty))
152     (concatenate 'string 
153       (etypecase host
154         (null
155          "localhost")
156         (pathname (namestring host))
157         (string host))
158       (when port 
159         (concatenate 'string
160                      ":"
161                      (etypecase port
162                        (integer (write-to-string port))
163                        (string port))))
164       "/" db "/" user)))
165
166 (defmethod database-connect (connection-spec 
167                              (database-type (eql :postgresql-socket)))
168   (check-connection-spec connection-spec database-type
169                          (host db user password &optional port options tty))
170   (destructuring-bind (host db user password &optional
171                             (port +postgresql-server-default-port+)
172                             (options "") (tty ""))
173       connection-spec
174     (handler-case
175         (handler-bind ((postgresql-warning
176                         (lambda (c)
177                           (warn 'clsql-simple-warning
178                                 :format-control "~A"
179                                 :format-arguments
180                                 (list (princ-to-string c))))))
181           (open-postgresql-connection :host host :port port
182                                       :options options :tty tty
183                                       :database db :user user
184                                       :password password))
185       (postgresql-error (c)
186         ;; Connect failed
187         (error 'clsql-connect-error
188                :database-type database-type
189                :connection-spec connection-spec
190                :errno (type-of c)
191                :error (postgresql-condition-message c)))
192       (:no-error (connection)
193                  ;; Success, make instance
194                  (make-instance 'postgresql-socket-database
195                                 :name (database-name-from-spec connection-spec
196                                                                database-type)
197                                 :database-type :postgresql-socket
198                                 :connection-spec connection-spec
199                                 :connection connection)))))
200
201 (defmethod database-disconnect ((database postgresql-socket-database))
202   (close-postgresql-connection (database-connection database))
203   t)
204
205 (defmethod database-query (expression (database postgresql-socket-database) result-types)
206   (let ((connection (database-connection database)))
207     (with-postgresql-handlers (database expression)
208       (start-query-execution connection expression)
209       (multiple-value-bind (status cursor)
210           (wait-for-query-results connection)
211         (unless (eq status :cursor)
212           (close-postgresql-connection connection)
213           (error 'clsql-sql-error
214                  :database database
215                  :expression expression
216                  :errno 'missing-result
217                  :error "Didn't receive result cursor for query."))
218         (setq result-types (canonicalize-types result-types cursor))
219         (loop for row = (read-cursor-row cursor result-types)
220               while row
221               collect row
222               finally
223               (unless (null (wait-for-query-results connection))
224                 (close-postgresql-connection connection)
225                 (error 'clsql-sql-error
226                        :database database
227                        :expression expression
228                        :errno 'multiple-results
229                        :error "Received multiple results for query.")))))))
230
231 (defmethod database-execute-command
232     (expression (database postgresql-socket-database))
233   (let ((connection (database-connection database)))
234     (with-postgresql-handlers (database expression)
235       (start-query-execution connection expression)
236       (multiple-value-bind (status result)
237           (wait-for-query-results connection)
238         (when (eq status :cursor)
239           (loop
240               (multiple-value-bind (row stuff)
241                   (skip-cursor-row result)
242                 (unless row
243                   (setq status :completed result stuff)
244                   (return)))))
245         (cond
246           ((null status)
247            t)
248           ((eq status :completed)
249            (unless (null (wait-for-query-results connection))
250              (close-postgresql-connection connection)
251              (error 'clsql-sql-error
252                     :database database
253                     :expression expression
254                     :errno 'multiple-results
255                     :error "Received multiple results for command."))
256            result)
257           (t
258            (close-postgresql-connection connection)
259            (error 'clsql-sql-error
260                   :database database
261                   :expression expression
262                   :errno 'missing-result
263                   :error "Didn't receive completion for command.")))))))
264
265 (defstruct postgresql-socket-result-set
266   (done nil)
267   (cursor nil)
268   (types nil))
269
270 (defmethod database-query-result-set ((expression string)
271                                       (database postgresql-socket-database) 
272                                       &key full-set result-types)
273   (declare (ignore full-set))
274   (let ((connection (database-connection database)))
275     (with-postgresql-handlers (database expression)
276       (start-query-execution connection expression)
277       (multiple-value-bind (status cursor)
278           (wait-for-query-results connection)
279         (unless (eq status :cursor)
280           (close-postgresql-connection connection)
281           (error 'clsql-sql-error
282                  :database database
283                  :expression expression
284                  :errno 'missing-result
285                  :error "Didn't receive result cursor for query."))
286         (values (make-postgresql-socket-result-set
287                  :done nil 
288                  :cursor cursor
289                  :types (canonicalize-types result-types cursor))
290                 (length (postgresql-cursor-fields cursor)))))))
291
292 (defmethod database-dump-result-set (result-set
293                                      (database postgresql-socket-database))
294   (if (postgresql-socket-result-set-done result-set)
295       t
296       (with-postgresql-handlers (database)
297         (loop while (skip-cursor-row 
298                      (postgresql-socket-result-set-cursor result-set))
299           finally (setf (postgresql-socket-result-set-done result-set) t)))))
300
301 (defmethod database-store-next-row (result-set
302                                     (database postgresql-socket-database)
303                                     list)
304   (let ((cursor (postgresql-socket-result-set-cursor result-set)))
305     (with-postgresql-handlers (database)
306       (if (copy-cursor-row cursor 
307                            list
308                            (postgresql-socket-result-set-types
309                             result-set))
310           t
311           (prog1 nil
312             (setf (postgresql-socket-result-set-done result-set) t)
313             (wait-for-query-results (database-connection database)))))))
314
315 ;;; Object listing
316
317 (defun owner-clause (owner)
318   (cond 
319    ((stringp owner)
320     (format
321      nil
322      " AND (relowner=(SELECT usesysid FROM pg_user WHERE (usename='~A')))" 
323      owner))
324    ((null owner)
325     (format nil " AND (NOT (relowner=1))"))
326    (t "")))
327
328 (defmethod database-list-objects-of-type ((database postgresql-socket-database)
329                                           type owner)
330   (mapcar #'car
331           (database-query
332            (format nil
333                    "SELECT relname FROM pg_class WHERE (relkind = '~A')~A"
334                    type
335                    (owner-clause owner))
336            database nil)))
337
338 (defmethod database-list-tables ((database postgresql-socket-database)
339                                  &key (owner nil))
340   (database-list-objects-of-type database "r" owner))
341   
342 (defmethod database-list-views ((database postgresql-socket-database)
343                                 &key (owner nil))
344   (database-list-objects-of-type database "v" owner))
345   
346 (defmethod database-list-indexes ((database postgresql-socket-database)
347                                   &key (owner nil))
348   (database-list-objects-of-type database "i" owner))
349
350 (defmethod database-list-table-indexes (table
351                                         (database postgresql-socket-database)
352                                         &key (owner nil))
353   (let ((indexrelids
354          (database-query
355           (format 
356            nil
357            "select indexrelid from pg_index where indrelid=(select relfilenode from pg_class where relname='~A'~A)"
358            (string-downcase table)
359            (owner-clause owner))
360           database :auto))
361         (result nil))
362     (dolist (indexrelid indexrelids (nreverse result))
363       (push 
364        (caar (database-query
365               (format nil "select relname from pg_class where relfilenode='~A'"
366                       (car indexrelid))
367               database
368               nil))
369        result))))
370
371 (defmethod database-list-attributes ((table string)
372                                      (database postgresql-socket-database)
373                                      &key (owner nil))
374   (let* ((owner-clause
375           (cond ((stringp owner)
376                  (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
377                 ((null owner) " AND (not (relowner=1))")
378                 (t "")))
379          (result
380           (mapcar #'car
381                   (database-query
382                    (format nil "SELECT attname FROM pg_class,pg_attribute WHERE pg_class.oid=attrelid AND relname='~A'~A"
383                            (string-downcase table)
384                            owner-clause)
385                    database nil))))
386     (if result
387         (reverse
388          (remove-if #'(lambda (it) (member it '("cmin"
389                                                 "cmax"
390                                                 "xmax"
391                                                 "xmin"
392                                                 "oid"
393                                                 "ctid"
394                                                 ;; kmr -- added tableoid
395                                                 "tableoid") :test #'equal)) 
396                     result)))))
397
398 (defmethod database-attribute-type (attribute (table string)
399                                     (database postgresql-socket-database)
400                                     &key (owner nil))
401   (let* ((owner-clause
402           (cond ((stringp owner)
403                  (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
404                 ((null owner) " AND (not (relowner=1))")
405                 (t "")))
406          (result
407           (mapcar #'car
408                   (database-query
409                    (format nil "SELECT pg_type.typname FROM pg_type,pg_class,pg_attribute WHERE pg_class.oid=pg_attribute.attrelid AND pg_class.relname='~A' AND pg_attribute.attname='~A' AND pg_attribute.atttypid=pg_type.oid~A"
410                            (string-downcase table)
411                            (string-downcase attribute)
412                            owner-clause)
413                    database nil))))
414     (when result
415       (intern (string-upcase (car result)) :keyword))))
416
417 (defmethod database-create-sequence (sequence-name
418                                      (database postgresql-socket-database))
419   (database-execute-command
420    (concatenate 'string "CREATE SEQUENCE " (sql-escape sequence-name))
421    database))
422
423 (defmethod database-drop-sequence (sequence-name
424                                    (database postgresql-socket-database))
425   (database-execute-command
426    (concatenate 'string "DROP SEQUENCE " (sql-escape sequence-name)) database))
427
428 (defmethod database-list-sequences ((database postgresql-socket-database)
429                                     &key (owner nil))
430   (database-list-objects-of-type database "S" owner))
431
432 (defmethod database-set-sequence-position (name (position integer)
433                                           (database postgresql-socket-database))
434   (values
435    (parse-integer
436     (caar
437      (database-query
438       (format nil "SELECT SETVAL ('~A', ~A)" name position)
439       database nil)))))
440
441 (defmethod database-sequence-next (sequence-name 
442                                    (database postgresql-socket-database))
443   (values
444    (parse-integer
445     (caar
446      (database-query
447       (concatenate 'string "SELECT NEXTVAL ('" (sql-escape sequence-name) "')")
448       database nil)))))
449
450 (defmethod database-sequence-last (sequence-name (database postgresql-socket-database))
451   (values
452    (parse-integer
453     (caar
454      (database-query
455       (concatenate 'string "SELECT LAST_VALUE ('" sequence-name "')")
456       database nil)))))
457   
458
459 (defmethod database-create (connection-spec (type (eql :postgresql-socket)))
460   (destructuring-bind (host name user password) connection-spec
461     (let ((database (database-connect (list host "template1" user password)
462                                       type)))
463       (unwind-protect
464            (execute-command (format nil "create database ~A" name))
465         (database-disconnect database)))))
466
467 (defmethod database-destroy (connection-spec (type (eql :postgresql-socket)))
468   (destructuring-bind (host name user password) connection-spec
469     (let ((database (database-connect (list host "template1" user password)
470                                       type)))
471       (unwind-protect
472           (execute-command (format nil "drop database ~A" name))
473         (database-disconnect database)))))
474
475
476 (defmethod database-probe (connection-spec (type (eql :postgresql-socket)))
477   (when (find (second connection-spec) (database-list connection-spec type)
478               :key #'car :test #'string-equal)
479     t))
480
481 (defmethod database-list (connection-spec (type (eql :postgresql-socket)))
482   (destructuring-bind (host name user password) connection-spec
483     (declare (ignore name))
484     (let ((database (database-connect (list host "template1" user password)
485                                       type)))
486       (unwind-protect
487            (progn
488              (setf (slot-value database 'clsql-base-sys::state) :open)
489              (mapcar #'car (database-query "select datname from pg_database" 
490                                            database :auto)))
491         (progn
492           (database-disconnect database)
493           (setf (slot-value database 'clsql-base-sys::state) :closed))))))
494
495 (defmethod database-describe-table ((database postgresql-socket-database) 
496                                     table)
497   (database-query
498    (format nil "select a.attname, t.typname
499                                from pg_class c, pg_attribute a, pg_type t
500                                where c.relname = '~a'
501                                    and a.attnum > 0
502                                    and a.attrelid = c.oid
503                                    and a.atttypid = t.oid"
504            (sql-escape (string-downcase table)))
505    database :auto))
506
507 (when (clsql-base-sys:database-type-library-loaded :postgresql-socket)
508   (clsql-base-sys:initialize-database-type :database-type :postgresql-socket))