r8938: add describe-table
[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 (defmethod database-list-objects-of-type ((database postgresql-socket-database)
318                                           type owner)
319   (let ((owner-clause
320          (cond ((stringp owner)
321                 (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE (usename='~A')))" owner))
322                ((null owner)
323                 (format nil " AND (NOT (relowner=1))"))
324                (t ""))))
325     (mapcar #'car
326             (database-query
327              (format nil
328                      "SELECT relname FROM pg_class WHERE (relkind = '~A')~A"
329                      type
330                      owner-clause)
331              database nil))))
332     
333 (defmethod database-list-tables ((database postgresql-socket-database)
334                                  &key (owner nil))
335   (database-list-objects-of-type database "r" owner))
336   
337 (defmethod database-list-views ((database postgresql-socket-database)
338                                 &key (owner nil))
339   (database-list-objects-of-type database "v" owner))
340   
341 (defmethod database-list-indexes ((database postgresql-socket-database)
342                                   &key (owner nil))
343   (database-list-objects-of-type database "i" owner))
344   
345 (defmethod database-list-attributes ((table string)
346                                      (database postgresql-socket-database)
347                                      &key (owner nil))
348   (let* ((owner-clause
349           (cond ((stringp owner)
350                  (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
351                 ((null owner) " AND (not (relowner=1))")
352                 (t "")))
353          (result
354           (mapcar #'car
355                   (database-query
356                    (format nil "SELECT attname FROM pg_class,pg_attribute WHERE pg_class.oid=attrelid AND relname='~A'~A"
357                            (string-downcase table)
358                            owner-clause)
359                    database nil))))
360     (if result
361         (reverse
362          (remove-if #'(lambda (it) (member it '("cmin"
363                                                 "cmax"
364                                                 "xmax"
365                                                 "xmin"
366                                                 "oid"
367                                                 "ctid"
368                                                 ;; kmr -- added tableoid
369                                                 "tableoid") :test #'equal)) 
370                     result)))))
371
372 (defmethod database-attribute-type (attribute (table string)
373                                     (database postgresql-socket-database)
374                                     &key (owner nil))
375   (let* ((owner-clause
376           (cond ((stringp owner)
377                  (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
378                 ((null owner) " AND (not (relowner=1))")
379                 (t "")))
380          (result
381           (mapcar #'car
382                   (database-query
383                    (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"
384                            (string-downcase table)
385                            (string-downcase attribute)
386                            owner-clause)
387                    database nil))))
388     (when result
389       (intern (string-upcase (car result)) :keyword))))
390
391 (defmethod database-create-sequence (sequence-name
392                                      (database postgresql-socket-database))
393   (database-execute-command
394    (concatenate 'string "CREATE SEQUENCE " (sql-escape sequence-name))
395    database))
396
397 (defmethod database-drop-sequence (sequence-name
398                                    (database postgresql-socket-database))
399   (database-execute-command
400    (concatenate 'string "DROP SEQUENCE " (sql-escape sequence-name)) database))
401
402 (defmethod database-list-sequences ((database postgresql-socket-database)
403                                     &key (owner nil))
404   (database-list-objects-of-type database "S" owner))
405
406 (defmethod database-set-sequence-position (name (position integer)
407                                           (database postgresql-socket-database))
408   (values
409    (parse-integer
410     (caar
411      (database-query
412       (format nil "SELECT SETVAL ('~A', ~A)" name position)
413       database nil)))))
414
415 (defmethod database-sequence-next (sequence-name 
416                                    (database postgresql-socket-database))
417   (values
418    (parse-integer
419     (caar
420      (database-query
421       (concatenate 'string "SELECT NEXTVAL ('" (sql-escape sequence-name) "')")
422       database nil)))))
423
424 (defmethod database-sequence-last (sequence-name (database postgresql-socket-database))
425   (values
426    (parse-integer
427     (caar
428      (database-query
429       (concatenate 'string "SELECT LAST_VALUE ('" sequence-name "')")
430       database nil)))))
431   
432
433 (defmethod database-create (connection-spec (type (eql :postgresql-socket)))
434   (destructuring-bind (host name user password) connection-spec
435     (let ((database (database-connect (list host "template1" user password)
436                                       type)))
437       (unwind-protect
438            (execute-command (format nil "create database ~A" name))
439         (database-disconnect database)))))
440
441 (defmethod database-destroy (connection-spec (type (eql :postgresql-socket)))
442   (destructuring-bind (host name user password) connection-spec
443     (let ((database (database-connect (list host "template1" user password)
444                                       type)))
445       (unwind-protect
446           (execute-command (format nil "drop database ~A" name))
447         (database-disconnect database)))))
448
449 (defmethod database-probe (connection-spec (type (eql :postgresql-socket)))
450   (destructuring-bind (host name user password) connection-spec
451     (let ((database (database-connect (list host "template1" user password)
452                                       type)))
453       (unwind-protect
454           (when
455               (find name (database-query "select datname from pg_database" 
456                                          database :auto)
457                     :key #'car :test #'string-equal)
458             t)
459         (database-disconnect database)))))
460
461 (defmethod database-describe-table ((database postgresql-socket-database) 
462                                     table)
463   (database-query
464    (format nil "select a.attname, t.typname
465                                from pg_class c, pg_attribute a, pg_type t
466                                where c.relname = '~a'
467                                    and a.attnum > 0
468                                    and a.attrelid = c.oid
469                                    and a.atttypid = t.oid"
470            (sql-escape (string-downcase 
471                         (etypecase table
472                           (string table)
473                           (clsql-base-sys::sql-create-table
474                            (symbol-name 
475                             (slot-value table 'clsql-base-sys::name)))))))
476    database :auto))
477
478 (when (clsql-base-sys:database-type-library-loaded :postgresql-socket)
479   (clsql-base-sys:initialize-database-type :database-type :postgresql-socket))