r10077: * multiple: Apply patch from Joerg Hoehle with multiple
[clsql.git] / db-postgresql / postgresql-sql.lisp
index 373d55ce9da2ca50dd143a7f0f8cab8da8e8c6e6..6a8c7c83290c6852deff730f229e432e4b9661d8 100644 (file)
@@ -16,7 +16,7 @@
 (in-package #:cl-user)
 
 (defpackage #:clsql-postgresql
-    (:use #:common-lisp #:clsql-base-sys #:postgresql #:clsql-uffi)
+    (:use #:common-lisp #:clsql-sys #:postgresql #:clsql-uffi)
     (:export #:postgresql-database)
     (:documentation "This is the CLSQL interface to PostgreSQL."))
 
 (uffi:def-type pgsql-result-def pgsql-result)
 
 
-(defclass postgresql-database (database)
+(defclass postgresql-database (generic-postgresql-database)
   ((conn-ptr :accessor database-conn-ptr :initarg :conn-ptr
-            :type pgsql-conn-def)))
+            :type pgsql-conn-def)
+   (lock
+    :accessor database-lock
+    :initform (make-process-lock "conn"))))
 
 (defmethod database-type ((database postgresql-database))
   :postgresql)
@@ -93,6 +96,7 @@
     (declare (ignore password options tty))
     (concatenate 'string 
       (etypecase host
+       (null "localhost")
        (pathname (namestring host))
        (string host))
       (when port 
        (declare (type pgsql-conn-def connection))
        (when (not (eq (PQstatus connection) 
                       pgsql-conn-status-type#connection-ok))
-         (error 'clsql-connect-error
+         (error 'sql-connection-error
                 :database-type database-type
                 :connection-spec connection-spec
-                :errno (PQstatus connection)
-                :error (tidy-error-message 
-                        (PQerrorMessage connection))))
+                :error-id (PQstatus connection)
+                :message (tidy-error-message 
+                          (PQerrorMessage connection))))
        (make-instance 'postgresql-database
                       :name (database-name-from-spec connection-spec
                                                      database-type)
+                      :database-type :postgresql
                       :connection-spec connection-spec
                       :conn-ptr connection)))))
 
   (setf (database-conn-ptr database) nil)
   t)
 
-(defmethod database-query (query-expression (database postgresql-database) result-types)
+(defmethod database-query (query-expression (database postgresql-database) result-types field-names)
   (let ((conn-ptr (database-conn-ptr database)))
     (declare (type pgsql-conn-def conn-ptr))
     (uffi:with-cstring (query-native query-expression)
       (let ((result (PQexec conn-ptr query-native)))
         (when (uffi:null-pointer-p result)
-          (error 'clsql-sql-error
+          (error 'sql-database-data-error
                  :database database
                  :expression query-expression
-                 :errno nil
-                 :error (tidy-error-message (PQerrorMessage conn-ptr))))
+                 :message (tidy-error-message (PQerrorMessage conn-ptr))))
         (unwind-protect
             (case (PQresultStatus result)
+             ;; User gave a command rather than a query 
+              (#.pgsql-exec-status-type#command-ok
+               nil)
               (#.pgsql-exec-status-type#empty-query
                nil)
               (#.pgsql-exec-status-type#tuples-ok
               (let ((num-fields (PQnfields result)))
-                (setq result-types
-                  (canonicalize-types result-types num-fields
-                                            result))
-                (loop for tuple-index from 0 below (PQntuples result)
-                      collect
-                      (loop for i from 0 below num-fields
-                            collect
-                            (if (zerop (PQgetisnull result tuple-index i))
-                                (convert-raw-field
-                                 (PQgetvalue result tuple-index i)
-                                 result-types i)
-                                nil)))))
+                (when result-types
+                  (setq result-types
+                    (canonicalize-types result-types num-fields
+                                        result)))
+                (let ((res (loop for tuple-index from 0 below (PQntuples result)
+                               collect
+                                 (loop for i from 0 below num-fields
+                                     collect
+                                       (if (zerop (PQgetisnull result tuple-index i))
+                                           (convert-raw-field
+                                            (PQgetvalue result tuple-index i)
+                                            result-types i)
+                                         nil)))))
+                  (if field-names
+                      (values res (result-field-names num-fields result))
+                    res))))
               (t
-               (error 'clsql-sql-error
+               (error 'sql-database-data-error
                       :database database
                       :expression query-expression
-                      :errno (PQresultStatus result)
-                      :error (tidy-error-message
-                              (PQresultErrorMessage result)))))
+                      :error-id (PQresultStatus result)
+                      :message (tidy-error-message
+                               (PQresultErrorMessage result)))))
           (PQclear result))))))
 
+(defun result-field-names (num-fields result)
+  "Return list of result field names."
+  (let ((names '()))
+    (dotimes (i num-fields (nreverse names))
+      (declare (fixnum i))
+      (push (uffi:convert-from-cstring (PQfname result i)) names))))
+
 (defmethod database-execute-command (sql-expression
                                      (database postgresql-database))
   (let ((conn-ptr (database-conn-ptr database)))
     (uffi:with-cstring (sql-native sql-expression)
       (let ((result (PQexec conn-ptr sql-native)))
         (when (uffi:null-pointer-p result)
-          (error 'clsql-sql-error
+          (error 'sql-database-data-error
                  :database database
                  :expression sql-expression
-                 :errno nil
-                 :error (tidy-error-message (PQerrorMessage conn-ptr))))
+                 :message (tidy-error-message (PQerrorMessage conn-ptr))))
         (unwind-protect
             (case (PQresultStatus result)
               (#.pgsql-exec-status-type#command-ok
                (warn "Strange result...")
                t)
               (t
-               (error 'clsql-sql-error
+               (error 'sql-database-data-error
                       :database database
                       :expression sql-expression
-                      :errno (PQresultStatus result)
-                      :error (tidy-error-message
-                              (PQresultErrorMessage result)))))
+                      :error-id (PQresultStatus result)
+                      :message (tidy-error-message
+                               (PQresultErrorMessage result)))))
           (PQclear result))))))
 
 (defstruct postgresql-result-set
     (uffi:with-cstring (query-native query-expression)
       (let ((result (PQexec conn-ptr query-native)))
         (when (uffi:null-pointer-p result)
-          (error 'clsql-sql-error
+          (error 'sql-database-data-error
                  :database database
                  :expression query-expression
-                 :errno nil
-                 :error (tidy-error-message (PQerrorMessage conn-ptr))))
+                 :message (tidy-error-message (PQerrorMessage conn-ptr))))
         (case (PQresultStatus result)
           ((#.pgsql-exec-status-type#empty-query
             #.pgsql-exec-status-type#tuples-ok)
                         (PQnfields result)))))
          (t
           (unwind-protect
-               (error 'clsql-sql-error
+               (error 'sql-database-data-error
                       :database database
                       :expression query-expression
-                      :errno (PQresultStatus result)
-                      :error (tidy-error-message
-                              (PQresultErrorMessage result)))
+                      :error-id (PQresultStatus result)
+                      :message (tidy-error-message
+                               (PQresultErrorMessage result)))
              (PQclear result))))))))
   
 (defmethod database-dump-result-set (result-set (database postgresql-database))
 
 ;;; Object listing
 
-(defmethod database-list-objects-of-type ((database postgresql-database)
-                                          type owner)
-  (let ((owner-clause
-         (cond ((stringp owner)
-                (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE (usename='~A')))" owner))
-               ((null owner)
-                (format nil " AND (NOT (relowner=1))"))
-               (t ""))))
-    (mapcar #'car
-            (database-query
-             (format nil
-                     "SELECT relname FROM pg_class WHERE (relkind = '~A')~A"
-                     type
-                     owner-clause)
-             database nil))))
-    
-(defmethod database-list-tables ((database postgresql-database)
-                                 &key (owner nil))
-  (database-list-objects-of-type database "r" owner))
-  
-(defmethod database-list-views ((database postgresql-database)
-                                &key (owner nil))
-  (database-list-objects-of-type database "v" owner))
-  
-(defmethod database-list-indexes ((database postgresql-database)
-                                  &key (owner nil))
-  (database-list-objects-of-type database "i" owner))
-  
-(defmethod database-list-attributes ((table string)
-                                    (database postgresql-database)
-                                     &key (owner nil))
-  (let* ((owner-clause
-          (cond ((stringp owner)
-                 (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
-                ((null owner) " AND (not (relowner=1))")
-                (t "")))
-         (result
-         (mapcar #'car
-                 (database-query
-                  (format nil "SELECT attname FROM pg_class,pg_attribute WHERE pg_class.oid=attrelid AND relname='~A'~A"
-                           (string-downcase table)
-                           owner-clause)
-                   database nil))))
-    (if result
-       (reverse
-         (remove-if #'(lambda (it) (member it '("cmin"
-                                                "cmax"
-                                                "xmax"
-                                                "xmin"
-                                               "oid"
-                                                "ctid"
-                                               ;; kmr -- added tableoid
-                                               "tableoid") :test #'equal)) 
-                   result)))))
-
-(defmethod database-attribute-type (attribute (table string)
-                                   (database postgresql-database)
-                                    &key (owner nil))
-  (let* ((owner-clause
-          (cond ((stringp owner)
-                 (format nil " AND (relowner=(SELECT usesysid FROM pg_user WHERE usename='~A'))" owner))
-                ((null owner) " AND (not (relowner=1))")
-                (t "")))
-         (result
-         (mapcar #'car
-                 (database-query
-                  (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"
-                          (string-downcase table)
-                           (string-downcase attribute)
-                           owner-clause)
-                  database nil))))
-    (when result
-      (intern (string-upcase (car result)) :keyword))))
-
-(defmethod database-create-sequence (sequence-name
-                                    (database postgresql-database))
-  (database-execute-command
-   (concatenate 'string "CREATE SEQUENCE " (sql-escape sequence-name))
-   database))
-
-(defmethod database-drop-sequence (sequence-name
-                                  (database postgresql-database))
-  (database-execute-command
-   (concatenate 'string "DROP SEQUENCE " (sql-escape sequence-name)) database))
-
-(defmethod database-list-sequences ((database postgresql-database)
-                                    &key (owner nil))
-  (database-list-objects-of-type database "S" owner))
-
-(defmethod database-set-sequence-position (name (position integer)
-                                                (database postgresql-database))
-  (values
-   (parse-integer
-    (caar
-     (database-query
-      (format nil "SELECT SETVAL ('~A', ~A)" name position)
-      database nil)))))
-
-(defmethod database-sequence-next (sequence-name 
-                                  (database postgresql-database))
-  (values
-   (parse-integer
-    (caar
-     (database-query
-      (concatenate 'string "SELECT NEXTVAL ('" (sql-escape sequence-name) "')")
-      database nil)))))
-
-(defmethod database-sequence-last (sequence-name (database postgresql-database))
-  (values
-   (parse-integer
-    (caar
-     (database-query
-      (concatenate 'string "SELECT LAST_VALUE ('" sequence-name "')")
-      database nil)))))
+
   
+(defmethod database-create (connection-spec (type (eql :postgresql)))
+  (destructuring-bind (host name user password) connection-spec
+    (declare (ignore user password))
+    (multiple-value-bind (output status)
+       (clsql-sys:command-output "createdb -h~A ~A"
+                                      (if host host "localhost")
+                                      name)
+      (if (or (not (zerop status))
+             (search "database creation failed: ERROR:" output))
+         (error 'sql-database-error
+                :message
+                (format nil "createdb failed for postgresql backend with connection spec ~A."
+                        connection-spec))
+       t))))
+
+(defmethod database-destroy (connection-spec (type (eql :postgresql)))
+  (destructuring-bind (host name user password) connection-spec
+    (declare (ignore user password))
+    (multiple-value-bind (output status)
+       (clsql-sys:command-output "dropdb -h~A ~A"
+                                      (if host host "localhost")
+                                      name)
+      (if (or (not (zerop status))
+             (search "database removal failed: ERROR:" output))
+         (error 'sql-database-error
+                :message
+                (format nil "dropdb failed for postgresql backend with connection spec ~A."
+                        connection-spec))
+       t))))
+
+
+(defmethod database-probe (connection-spec (type (eql :postgresql)))
+  (when (find (second connection-spec) (database-list connection-spec type)
+             :key #'car :test #'string-equal)
+    t))
 
-;; Functions depending upon high-level CommonSQL classes/functions
-#|
-(defmethod database-output-sql ((expr clsql-sys::sql-typecast-exp) 
-                               (database postgresql-database))
-  (with-slots (clsql-sys::modifier clsql-sys::components)
-    expr
-    (if clsql-sys::modifier
-        (progn
-          (clsql-sys::output-sql clsql-sys::components database)
-          (write-char #\: clsql-sys::*sql-stream*)
-          (write-char #\: clsql-sys::*sql-stream*)
-          (write-string (symbol-name clsql-sys::modifier) 
-                       clsql-sys::*sql-stream*)))))
-
-(defmethod database-output-sql-as-type ((type (eql 'integer)) val
-                                       (database postgresql-database))
-  (when val   ;; typecast it so it uses the indexes
-    (make-instance 'clsql-sys::sql-typecast-exp
-                   :modifier 'int8
-                   :components val)))
-|#
-
-(when (clsql-base-sys:database-type-library-loaded :postgresql)
-  (clsql-base-sys:initialize-database-type :database-type :postgresql))
+
+(defun %pg-database-connection (connection-spec)
+  (check-connection-spec connection-spec :postgresql
+                        (host db user password &optional port options tty))
+  (macrolet ((coerce-string (var)
+               `(unless (typep ,var 'simple-base-string)
+                 (setf ,var (coerce ,var 'simple-base-string)))))
+    (destructuring-bind (host db user password &optional port options tty)
+        connection-spec
+      (coerce-string db)
+      (coerce-string user)
+      (let ((connection (PQsetdbLogin host port options tty db user password)))
+        (declare (type postgresql::pgsql-conn-ptr connection))
+        (unless (eq (PQstatus connection)
+                   pgsql-conn-status-type#connection-ok)
+          ;; Connect failed
+          (error 'sql-connection-error
+                 :database-type :postgresql
+                 :connection-spec connection-spec
+                 :error-id (PQstatus connection)
+                 :message (PQerrorMessage connection)))
+        connection))))
+
+(defmethod database-reconnect ((database postgresql-database))
+  (let ((lock (database-lock database)))
+    (with-process-lock (lock "Reconnecting")
+      (with-slots (connection-spec conn-ptr)
+         database
+       (setf conn-ptr (%pg-database-connection connection-spec))
+       database))))
+
+;;; Database capabilities
+
+(when (clsql-sys:database-type-library-loaded :postgresql)
+  (clsql-sys:initialize-database-type :database-type :postgresql))