pax_global_header00006660000000000000000000000064126007363440014517gustar00rootroot0000000000000052 comment=4210920eadbe67b01238cbd8eb77c126eff56a3d postmodern-20151031-git/000077500000000000000000000000001260073634400147305ustar00rootroot00000000000000postmodern-20151031-git/.gitignore000066400000000000000000000000071260073634400167150ustar00rootroot00000000000000*.fasl postmodern-20151031-git/LICENSE000066400000000000000000000015521260073634400157400ustar00rootroot00000000000000Copyright (c) Marijn Haverbeke, marijnh@gmail.com This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. postmodern-20151031-git/cl-postgres.asd000066400000000000000000000042611260073634400176660ustar00rootroot00000000000000(defpackage :cl-postgres-system (:use :common-lisp :asdf)) (in-package :cl-postgres-system) ;; Change this to enable/disable unicode manually (mind that it won't ;; work unless your implementation supports it). (defparameter *unicode* #+(or sb-unicode unicode ics openmcl-unicode-strings) t #-(or sb-unicode unicode ics openmcl-unicode-strings) nil) (defparameter *string-file* (if *unicode* "strings-utf-8" "strings-ascii")) (defsystem :cl-postgres :description "Low-level client library for PostgreSQL" :depends-on (:md5 #-(or sbcl allegro ccl) :usocket #+sbcl :sb-bsd-sockets) :components ((:module :cl-postgres :components ((:file "trivial-utf-8") (:file "ieee-floats") (:file "package") (:file "errors" :depends-on ("package")) (:file "sql-string" :depends-on ("package")) (:file #.*string-file* :depends-on ("package" "trivial-utf-8")) (:file "communicate" :depends-on (#.*string-file* "sql-string")) (:file "messages" :depends-on ("communicate")) (:file "interpret" :depends-on ("communicate" "ieee-floats")) (:file "protocol" :depends-on ("interpret" "messages" "errors")) (:file "public" :depends-on ("protocol")) (:file "bulk-copy" :depends-on ("public")))))) (defsystem :cl-postgres-tests :depends-on (:cl-postgres :eos :simple-date) :components ((:module :cl-postgres :components ((:file "tests"))))) (defmethod perform ((op asdf:test-op) (system (eql (find-system :cl-postgres)))) (asdf:oos 'asdf:load-op :cl-postgres-tests) (funcall (intern (string :prompt-connection) (string :cl-postgres-tests))) (funcall (intern (string :run!) (string :Eos)) :cl-postgres)) (defmethod perform :after ((op asdf:load-op) (system (eql (find-system :cl-postgres)))) (when (and (find-package :simple-date) (not (find-symbol (symbol-name '#:+postgres-day-offset+) :simple-date))) (asdf:oos 'asdf:load-op :simple-date-postgres-glue))) postmodern-20151031-git/cl-postgres/000077500000000000000000000000001260073634400171725ustar00rootroot00000000000000postmodern-20151031-git/cl-postgres/bulk-copy.lisp000066400000000000000000000114251260073634400217730ustar00rootroot00000000000000 (in-package :cl-postgres) (defclass bulk-copier () ((own-connection :initarg :own-connection :reader bulk-copier-own-connection) (database :initarg :database :reader copier-database) (table :initarg :table :reader copier-table) (columns :initarg :columns :reader copier-columns) (count :initform 0 :accessor copier-count))) (defmethod print-object ((self bulk-copier) stream) (print-unreadable-object (self stream :type t :identity t) (format stream "~a ~a" (copier-table self) (copier-columns self)))) (defun open-db-writer (db-spec table columns) (let* ((own-connection (listp db-spec)) (copier (make-instance 'bulk-copier :own-connection own-connection :database (if own-connection (apply 'open-database db-spec) db-spec) :table table :columns columns))) (initialize-copier copier) copier)) (defun close-db-writer (self &key (abort nil)) (unwind-protect (let* ((connection (copier-database self)) (socket (connection-socket connection))) (with-reconnect-restart connection (using-connection connection (send-copy-done socket)))) (when (or abort (bulk-copier-own-connection self)) (close-database (copier-database self)))) (copier-count self)) (defun db-write-row (self row &optional (data (prepare-row self row))) (let* ((connection (copier-database self)) (socket (connection-socket connection))) (with-reconnect-restart connection (using-connection connection (with-syncing (copy-data-message socket data))))) (incf (copier-count self))) (defun copy-query (self) (format nil "~%copy ~a ~@[(~{~a~^,~})~] ~a ~a" (copier-table self) (copier-columns self) "FROM" "STDIN")) (defun send-copy-start (socket query) (with-syncing (query-message socket query) (flush-message socket) (force-output socket) (message-case socket ;; Ignore the field formats because we're only supporting plain ;; text for now (#\G (read-uint1 socket) (skip-bytes socket (* 2 (read-uint2 socket))))))) (defun initialize-copier (self) (let* ((query (copy-query self)) (connection (copier-database self)) (socket (connection-socket connection))) (with-reconnect-restart connection (using-connection connection (send-copy-start socket query))))) (defun copier-write-value (s val) (typecase val (string (let ((pg-string (with-output-to-string (str) (loop for byte across (cl-postgres-trivial-utf-8:string-to-utf-8-bytes val) do (case (code-char byte) (#\Space (princ " " str)) ((#\Newline #\Tab) (format str "\\~a" (code-char byte))) (#\\ (progn (princ #\\ str) (princ #\\ str))) (otherwise (if (and (< 32 byte) (> 127 byte)) (write-char (code-char byte) str) (princ (format nil "\\~o" byte) str)))))))) #+nil(print `(:loading ,pg-string)) (princ pg-string s))) (number (princ val s)) (null (princ "false" s)) (symbol (case val (:null (princ "\\N" s)) ((t) (princ "true" s)) (otherwise (error "copier-write-val: Symbols shouldn't be getting this far ~a" val)))))) (defun copier-write-sequence (s vector) (write-char #\{ s) (loop for (item . more-p) on (coerce vector 'list) do (cond ((null item) (copier-write-value s :null)) ((atom item) (copier-write-value s item)) (t (copier-write-sequence s item))) when more-p do (write-char #\, s)) (write-char #\} s)) (defmethod prepare-row (self row) (declare (ignore self)) (with-output-to-string (s) (loop for (val . more-p) on row do (progn (if (typep val '(or string (not vector))) (copier-write-value s val) (copier-write-sequence s val))) if more-p do (write-char #\Tab s) finally (write-char #\Newline s)))) (defun send-copy-done (socket) (with-syncing (setf sync-sent t) (copy-done-message socket) (force-output socket) (message-case socket (#\C (let* ((command-tag (read-str socket)) (space (position #\Space command-tag :from-end t))) (when space (parse-integer command-tag :junk-allowed t :start (1+ space)))))) (block find-ready (loop (message-case socket (#\Z (read-uint1 socket) (return-from find-ready)) (t :skip)))))) postmodern-20151031-git/cl-postgres/communicate.lisp000066400000000000000000000136141260073634400223740ustar00rootroot00000000000000(in-package :cl-postgres) ;; These are used to synthesize reader and writer names for integer ;; reading/writing functions when the amount of bytes and the ;; signedness is known. Both the macro that creates the functions and ;; some macros that use them create names this way. (eval-when (:compile-toplevel :load-toplevel :execute) (defun integer-reader-name (bytes signed) (intern (with-standard-io-syntax (format nil "~a~a~a~a" '#:read- (if signed "" '#:u) '#:int bytes)))) (defun integer-writer-name (bytes signed) (intern (with-standard-io-syntax (format nil "~a~a~a~a" '#:write- (if signed "" '#:u) '#:int bytes))))) (defmacro integer-reader (bytes) "Create a function to read integers from a binary stream." (let ((bits (* bytes 8))) (labels ((return-form (signed) (if signed `(if (logbitp ,(1- bits) result) (dpb result (byte ,(1- bits) 0) -1) result) `result)) (generate-reader (signed) `(defun ,(integer-reader-name bytes signed) (socket) (declare (type stream socket) #.*optimize*) ,(if (= bytes 1) `(let ((result (the (unsigned-byte 8) (read-byte socket)))) (declare (type (unsigned-byte 8) result)) ,(return-form signed)) `(let ((result 0)) (declare (type (unsigned-byte ,bits) result)) ,@(loop :for byte :from (1- bytes) :downto 0 :collect `(setf (ldb (byte 8 ,(* 8 byte)) result) (the (unsigned-byte 8) (read-byte socket)))) ,(return-form signed)))))) `(progn ;; This causes weird errors on SBCL in some circumstances. Disabled for now. ;; (declaim (inline ,(integer-reader-name bytes t) ;; ,(integer-reader-name bytes nil))) (declaim (ftype (function (t) (signed-byte ,bits)) ,(integer-reader-name bytes t))) ,(generate-reader t) (declaim (ftype (function (t) (unsigned-byte ,bits)) ,(integer-reader-name bytes nil))) ,(generate-reader nil))))) (defmacro integer-writer (bytes) "Create a function to write integers to a binary stream." (let ((bits (* 8 bytes))) `(progn (declaim (inline ,(integer-writer-name bytes t) ,(integer-writer-name bytes nil))) (defun ,(integer-writer-name bytes nil) (socket value) (declare (type stream socket) (type (unsigned-byte ,bits) value) #.*optimize*) ,@(if (= bytes 1) `((write-byte value socket)) (loop :for byte :from (1- bytes) :downto 0 :collect `(write-byte (ldb (byte 8 ,(* byte 8)) value) socket))) (values)) (defun ,(integer-writer-name bytes t) (socket value) (declare (type stream socket) (type (signed-byte ,bits) value) #.*optimize*) ,@(if (= bytes 1) `((write-byte (ldb (byte 8 0) value) socket)) (loop :for byte :from (1- bytes) :downto 0 :collect `(write-byte (ldb (byte 8 ,(* byte 8)) value) socket))) (values))))) ;; All the instances of the above that we need. (integer-reader 1) (integer-reader 2) (integer-reader 4) (integer-reader 8) (integer-writer 1) (integer-writer 2) (integer-writer 4) (defun write-bytes (socket bytes) "Write a byte-array to a stream." (declare (type stream socket) (type (simple-array (unsigned-byte 8)) bytes) #.*optimize*) (write-sequence bytes socket)) (defun write-str (socket string) "Write a null-terminated string to a stream \(encoding it when UTF-8 support is enabled.)." (declare (type stream socket) (type string string) #.*optimize*) (enc-write-string string socket) (write-uint1 socket 0)) (declaim (ftype (function (t unsigned-byte) (simple-array (unsigned-byte 8) (*))) read-bytes)) (defun read-bytes (socket length) "Read a byte array of the given length from a stream." (declare (type stream socket) (type fixnum length) #.*optimize*) (let ((result (make-array length :element-type '(unsigned-byte 8)))) (read-sequence result socket) result)) (declaim (ftype (function (t) string) read-str)) (defun read-str (socket) "Read a null-terminated string from a stream. Takes care of encoding when UTF-8 support is enabled." (declare (type stream socket) #.*optimize*) (enc-read-string socket :null-terminated t)) (declaim (ftype (function (t) string) read-simple-str)) (defun read-simple-str (socket) "Read a null-terminated string from a stream. Interprets it as ASCII." (declare (type stream socket) #.*optimize*) (with-output-to-string (out) (loop :for b := (read-byte socket nil 0) :do (cond ((eq b 0) (return)) ((< b 128) (write-char (code-char b) out)))))) (defun skip-bytes (socket length) "Skip a given number of bytes in a binary stream." (declare (type stream socket) (type (unsigned-byte 32) length) #.*optimize*) (dotimes (i length) (read-byte socket))) (defun skip-str (socket) "Skip a null-terminated string." (declare (type stream socket) #.*optimize*) (loop :for char :of-type fixnum = (read-byte socket) :until (zerop char))) (defun ensure-socket-is-closed (socket &key abort) (when (open-stream-p socket) (handler-case (close socket :abort abort) (error (error) (warn "Ignoring the error which happened while trying to close PostgreSQL socket: ~A" error))))) postmodern-20151031-git/cl-postgres/errors.lisp000066400000000000000000000162461260073634400214100ustar00rootroot00000000000000(in-package :cl-postgres) (defparameter *current-query* nil) (defparameter *query-log* nil) (defparameter *query-callback* 'log-query) (defun log-query (query time-units) (when *query-log* (format *query-log* "CL-POSTGRES query (~ams): ~a~%" (round (/ (* 1000 time-units) internal-time-units-per-second)) query))) (defmacro with-query ((query) &body body) (let ((time-name (gensym))) `(let ((*current-query* ,query) (,time-name (if *query-callback* (get-internal-real-time) 0))) (multiple-value-prog1 (progn ,@body) (when *query-callback* (funcall *query-callback* *current-query* (- (get-internal-real-time) ,time-name))))))) ;; ;; See http://www.postgresql.org/docs/9.3/static/protocol-error-fields.html ;; for details, including documentation strings. ;; (define-condition database-error (error) ((error-code :initarg :code :initform nil :reader database-error-code :documentation "Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present.") (message :initarg :message :accessor database-error-message :documentation "Message: the primary human-readable error message. This should be accurate but terse (typically one line). Always present.") (detail :initarg :detail :initform nil :reader database-error-detail :documentation "Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines.") (hint :initarg :hint :initform nil :reader database-error-hint :documentation "Hint: an optional suggestion what to do about the problem.") (context :initarg :context :initform nil :reader database-error-context :documentation "Where: an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first." ) (query :initform *current-query* :reader database-error-query :documentation "Query that led to the error, if any.") (position :initarg :position :initform nil :reader database-error-position :documentation "Position: the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes.") (cause :initarg :cause :initform nil :reader database-error-cause)) (:report (lambda (err stream) (format stream "Database error~@[ ~A~]: ~A~@[~&DETAIL: ~A~]~@[~&HINT: ~A~]~@[~&CONTEXT: ~A~]~@[~&QUERY: ~A~]~@[~VT^~]" (database-error-code err) (database-error-message err) (database-error-detail err) (database-error-hint err) (database-error-context err) (database-error-query err) (database-error-position err)))) (:documentation "This is the condition type that will be used to signal virtually all database-related errors \(though in some cases socket errors may be raised when a connection fails on the IP level).")) (defun database-error-constraint-name (err) "Given a database-error for an integrity violation, will attempt to extract the constraint name." (labels ((extract-quoted-part (string n) "Extracts the Nth quoted substring from STRING." (let* ((start-quote-inst (* 2 n)) (start-quote-pos (position-nth #\" string start-quote-inst)) (end-quote-pos (position #\" string :start (1+ start-quote-pos)))) (subseq string (1+ start-quote-pos) end-quote-pos))) (position-nth (item seq n) "Finds the position of the zero-indexed Nth ITEM in SEQ." (loop :with pos = -1 :repeat (1+ n) :do (setf pos (position item seq :start (1+ pos))) :finally (return pos)))) (let ((message (database-error-message err))) (typecase err (cl-postgres-error:not-null-violation (extract-quoted-part message 0)) (cl-postgres-error:unique-violation (extract-quoted-part message 0)) (cl-postgres-error:foreign-key-violation (extract-quoted-part message 1)) (cl-postgres-error:check-violation (extract-quoted-part message 1)))))) (define-condition database-connection-error (database-error) () (:documentation "Conditions of this type are signalled when an error occurs that breaks the connection socket. They offer a :reconnect restart.")) (define-condition database-connection-lost (database-connection-error) () (:documentation "Raised when a query is initiated on a disconnected connection object.")) (define-condition database-socket-error (database-connection-error) () (:documentation "Used to wrap stream-errors and socket-errors, giving them a database-connection-error superclass.")) (defun wrap-socket-error (err) (make-instance 'database-socket-error :message (princ-to-string err) :cause err)) (in-package :cl-postgres-error) (defparameter *error-table* (make-hash-table :test 'equal)) (defmacro deferror (code typename &optional (superclass 'database-error)) `(progn (define-condition ,typename (,superclass) ()) (setf (gethash ,code *error-table*) ',typename))) (deferror "0A" feature-not-supported) (deferror "22" data-exception) (deferror "22012" db-division-by-zero data-exception) (deferror "22007" invalid-datetime-format data-exception) (deferror "22003" numeric-value-out-of-range data-exception) (deferror "22P01" floating-point-exception data-exception) (deferror "23" integrity-violation) (deferror "23001" restrict-violation integrity-violation) (deferror "23502" not-null-violation integrity-violation) (deferror "23503" foreign-key-violation integrity-violation) (deferror "23505" unique-violation integrity-violation) (deferror "23514" check-violation integrity-violation) (deferror "42" syntax-error-or-access-violation) (deferror "42501" insufficient-privilege syntax-error-or-access-violation) (deferror "40" transaction-rollback) (deferror "40001" serialization-failure transaction-rollback) (deferror "40002" transaction-integrity-constraint-violation transaction-rollback) (deferror "40003" statement-completion-unknown transaction-rollback) (deferror "40P01" deadlock-detected transaction-rollback) (deferror "53" insufficient-resources) (deferror "54" program-limit-exceeded) (deferror "55" object-state-error) (deferror "55006" object-in-use object-state-error) (deferror "55P03" lock-not-available object-state-error) (deferror "57" operator-intervention) (deferror "57014" query-canceled operator-intervention) (define-condition server-shutdown (operator-intervention database-connection-error) ()) (deferror "57P01" admin-shutdown server-shutdown) (deferror "57P02" crash-shutdown server-shutdown) (deferror "57P03" cannot-connect-now operator-intervention) (deferror "58" system-error) (deferror "XX" internal-error) (defun get-error-type (code) (or (gethash code *error-table*) (and code (gethash (subseq code 0 2) *error-table*)) 'database-error)) postmodern-20151031-git/cl-postgres/ieee-floats.lisp000066400000000000000000000142301260073634400222600ustar00rootroot00000000000000;;; Functions for converting floating point numbers represented in ;;; IEEE 754 style to lisp numbers. ;;; ;;; See http://common-lisp.net/project/ieee-floats/ (defpackage :cl-postgres-ieee-floats (:use :common-lisp) (:export :make-float-converters :encode-float32 :decode-float32 :encode-float64 :decode-float64)) (in-package :cl-postgres-ieee-floats) ;; The following macro may look a bit overcomplicated to the casual ;; reader. The main culprit is the fact that NaN and infinity can be ;; optionally included, which adds a bunch of conditional parts. ;; ;; Assuming you already know more or less how floating point numbers ;; are typically represented, I'll try to elaborate a bit on the more ;; confusing parts, as marked by letters: ;; ;; (A) Exponents in IEEE floats are offset by half their range, for ;; example with 8 exponent bits a number with exponent 2 has 129 ;; stored in its exponent field. ;; ;; (B) The maximum possible exponent is reserved for special cases ;; (NaN, infinity). ;; ;; (C) If the exponent fits in the exponent-bits, we have to adjust ;; the significand for the hidden bit. Because decode-float will ;; return a significand between 0 and 1, and we want one between 1 ;; and 2 to be able to hide the hidden bit, we double it and then ;; subtract one (the hidden bit) before converting it to integer ;; representation (to adjust for this, 1 is subtracted from the ;; exponent earlier). When the exponent is too small, we set it to ;; zero (meaning no hidden bit, exponent of 1), and adjust the ;; significand downward to compensate for this. ;; ;; (D) Here the hidden bit is added. When the exponent is 0, there is ;; no hidden bit, and the exponent is interpreted as 1. ;; ;; (E) Here the exponent offset is subtracted, but also an extra ;; factor to account for the fact that the bits stored in the ;; significand are supposed to come after the 'decimal dot'. (defmacro make-float-converters (encoder-name decoder-name exponent-bits significand-bits support-nan-and-infinity-p) "Writes an encoder and decoder function for floating point numbers with the given amount of exponent and significand bits (plus an extra sign bit). If support-nan-and-infinity-p is true, the decoders will also understand these special cases. NaN is represented as :not-a-number, and the infinities as :positive-infinity and :negative-infinity. Note that this means that the in- or output of these functions is not just floating point numbers anymore, but also keywords." (let* ((total-bits (+ 1 exponent-bits significand-bits)) (exponent-offset (1- (expt 2 (1- exponent-bits)))) ; (A) (sign-part `(ldb (byte 1 ,(1- total-bits)) bits)) (exponent-part `(ldb (byte ,exponent-bits ,significand-bits) bits)) (significand-part `(ldb (byte ,significand-bits 0) bits)) (nan support-nan-and-infinity-p) (max-exponent (1- (expt 2 exponent-bits)))) ; (B) `(progn (defun ,encoder-name (float) ,@(unless nan `((declare (type float float)))) (multiple-value-bind (sign significand exponent) (cond ,@(when nan `(((eq float :not-a-number) (values 0 1 ,max-exponent)) ((eq float :positive-infinity) (values 0 0 ,max-exponent)) ((eq float :negative-infinity) (values 1 0 ,max-exponent)))) ((zerop float) (values 0 0 0)) (t (multiple-value-bind (significand exponent sign) (decode-float float) (let ((exponent (+ (1- exponent) ,exponent-offset)) (sign (if (= sign 1.0) 0 1))) (unless (< exponent ,(expt 2 exponent-bits)) (error "Floating point overflow when encoding ~A." float)) (if (< exponent 0) ; (C) (values sign (ash (round (* ,(expt 2 significand-bits) significand)) exponent) 0) (values sign (round (* ,(expt 2 significand-bits) (1- (* significand 2)))) exponent)))))) (let ((bits 0)) (declare (type (unsigned-byte ,total-bits) bits)) (setf ,sign-part sign ,exponent-part exponent ,significand-part significand) bits))) (defun ,decoder-name (bits) (declare (type (unsigned-byte ,total-bits) bits)) (let* ((sign ,sign-part) (exponent ,exponent-part) (significand ,significand-part)) ,@(when nan `((when (= exponent ,max-exponent) (return-from ,decoder-name (cond ((not (zerop significand)) :not-a-number) ((zerop sign) :positive-infinity) (t :negative-infinity)))))) (if (zerop exponent) ; (D) (setf exponent 1) (setf (ldb (byte 1 ,significand-bits) significand) 1)) (unless (zerop sign) (setf significand (- significand))) (scale-float (float significand ,(if (> total-bits 32) 1.0d0 1.0)) (- exponent ,(+ exponent-offset significand-bits)))))))) ; (E) ;; And instances of the above for the common forms of floats. (make-float-converters encode-float32 decode-float32 8 23 nil) (make-float-converters encode-float64 decode-float64 11 52 nil) ;;; Copyright (c) 2006 Marijn Haverbeke ;;; ;;; This software is provided 'as-is', without any express or implied ;;; warranty. In no event will the authors be held liable for any ;;; damages arising from the use of this software. ;;; ;;; Permission is granted to anyone to use this software for any ;;; purpose, including commercial applications, and to alter it and ;;; redistribute it freely, subject to the following restrictions: ;;; ;;; 1. The origin of this software must not be misrepresented; you must ;;; not claim that you wrote the original software. If you use this ;;; software in a product, an acknowledgment in the product ;;; documentation would be appreciated but is not required. ;;; ;;; 2. Altered source versions must be plainly marked as such, and must ;;; not be misrepresented as being the original software. ;;; ;;; 3. This notice may not be removed or altered from any source ;;; distribution. postmodern-20151031-git/cl-postgres/interpret.lisp000066400000000000000000000243321260073634400221030ustar00rootroot00000000000000(in-package :cl-postgres) (defparameter *timestamp-format* :unbound "This is used to communicate the format \(integer or float) used for timestamps and intervals in the current connection, so that the interpreters for those types know how to parse them.") (defparameter *sql-readtable* (make-hash-table) "The exported special var holding the current read table, a hash mapping OIDs to (binary-p . interpreter-function) pairs.") (defun interpret-as-text (stream size) "This interpreter is used for types that we have no specific interpreter for -- it just reads the value as a string. \(Values of unknown types are passed in text form.)" (enc-read-string stream :byte-length size)) (let ((default-interpreter (cons nil #'interpret-as-text))) (defun type-interpreter (oid) "Returns a pair representing the interpretation rules for this type. The car is a boolean indicating whether the type should be fetched as binary, and the cdr is a function that will read the value from the socket and build a Lisp value from it." (gethash oid *sql-readtable* default-interpreter))) (defun set-sql-reader (oid function &key (table *sql-readtable*) binary-p) "Add an sql reader to a readtable. When the reader is not binary, it is wrapped by a function that will read the string from the socket." (setf (gethash oid table) (if binary-p (cons t function) (cons nil (lambda (stream size) (funcall function (enc-read-string stream :byte-length size)))))) table) (defmacro binary-reader (fields &body value) "A slightly convoluted macro for defining interpreter functions. It allows two forms. The first is to pass a single type identifier, in which case a value of this type will be read and returned directly. The second is to pass a list of lists containing names and types, and then a body. In this case the names will be bound to values read from the socket and interpreted as the given types, and then the body will be run in the resulting environment. If the last field is of type bytes, string, or uint2s, all remaining data will be read and interpreted as an array of the given type." (let ((stream-name (gensym)) (size-name (gensym)) (length-used 0)) (flet ((read-type (type &optional modifier) (ecase type (bytes `(read-bytes ,stream-name (- ,size-name ,length-used))) (string `(enc-read-string ,stream-name :byte-length (- ,size-name ,length-used))) (uint2s `(let* ((size (/ (- ,size-name ,length-used) 2)) (result (make-array size :element-type '(unsigned-byte 16)))) (dotimes (i size) (setf (elt result i) (read-uint2 ,stream-name))) result)) (int (assert (integerp modifier)) (incf length-used modifier) `(,(integer-reader-name modifier t) ,stream-name)) (uint (assert (integerp modifier)) (incf length-used modifier) `(,(integer-reader-name modifier nil) ,stream-name))))) `(lambda (,stream-name ,size-name) (declare (type stream ,stream-name) (type integer ,size-name) (ignorable ,size-name)) ,(if (consp fields) `(let ,(loop :for field :in fields :collect `(,(first field) ,(apply #'read-type (cdr field)))) ,@value) (read-type fields (car value))))))) (defmacro define-interpreter (oid name fields &body value) "Shorthand for defining binary readers." (declare (ignore name)) ;; Names are there just for clarity `(set-sql-reader ,oid (binary-reader ,fields ,@value) :binary-p t)) (define-interpreter 18 "char" int 1) (define-interpreter 21 "int2" int 2) (define-interpreter 23 "int4" int 4) (define-interpreter 20 "int8" int 8) (define-interpreter 26 "oid" uint 4) (define-interpreter 16 "bool" ((value int 1)) (if (zerop value) nil t)) (define-interpreter 17 "bytea" bytes) (define-interpreter 25 "text" string) (define-interpreter 1042 "bpchar" string) (define-interpreter 1043 "varchar" string) (define-interpreter 700 "float4" ((bits uint 4)) (cl-postgres-ieee-floats:decode-float32 bits)) (define-interpreter 701 "float8" ((bits uint 8)) (cl-postgres-ieee-floats:decode-float64 bits)) ;; Numeric types are rather involved. I got some clues on their ;; structure from http://archives.postgresql.org/pgsql-interfaces/2004-08/msg00000.php (define-interpreter 1700 "numeric" ((length uint 2) (weight int 2) (sign int 2) (dscale int 2) (digits uint2s)) (declare (ignore dscale)) (let ((total (loop :for i :from (1- length) :downto 0 :for scale = 1 :then (* scale #.(expt 10 4)) :summing (* scale (elt digits i)))) (scale (- length weight 1))) (unless (zerop sign) (setf total (- total))) (/ total (expt 10000 scale)))) ;; Since date and time types are the most likely to require custom ;; readers, there is a hook for easily adding binary readers for them. (defun set-date-reader (f table) (set-sql-reader 1082 (binary-reader ((days int 4)) (funcall f days)) :table table :binary-p t)) (defun interpret-usec-bits (bits) "Decode a 64 bit time-related value based on the timestamp format used. Correct for sign bit when using integer format." (ecase *timestamp-format* (:float (round (* (cl-postgres-ieee-floats:decode-float64 bits) 1000000))) (:integer (if (logbitp 63 bits) (dpb bits (byte 63 0) -1) bits)))) (defun set-interval-reader (f table) (set-sql-reader 1186 (binary-reader ((usec-bits uint 8) (days int 4) (months int 4)) (funcall f months days (interpret-usec-bits usec-bits))) :table table :binary-p t)) (defun set-usec-reader (oid f table) (set-sql-reader oid (binary-reader ((usec-bits uint 8)) (funcall f (interpret-usec-bits usec-bits))) :table table :binary-p t)) ;; Public interface for adding date/time readers (defun set-sql-datetime-readers (&key date timestamp timestamp-with-timezone interval time (table *sql-readtable*)) (when date (set-date-reader date table)) (when timestamp (set-usec-reader 1114 timestamp table)) (when timestamp-with-timezone (set-usec-reader 1184 timestamp-with-timezone table)) (when interval (set-interval-reader interval table)) (when time (set-usec-reader 1083 time table)) table) ;; Provide meaningful defaults for the date/time readers. (defconstant +start-of-2000+ (encode-universal-time 0 0 0 1 1 2000 0)) (defconstant +seconds-in-day+ (* 60 60 24)) (set-sql-datetime-readers :date (lambda (days-since-2000) (+ +start-of-2000+ (* days-since-2000 +seconds-in-day+))) :timestamp (lambda (useconds-since-2000) (+ +start-of-2000+ (floor useconds-since-2000 1000000))) :interval (lambda (months days useconds) (multiple-value-bind (sec us) (floor useconds 1000000) `((:months ,months) (:days ,days) (:seconds ,sec) (:useconds ,us))))) ;; Readers for a few of the array types (defun read-array-value (transform) (declare #.*optimize*) (lambda (value) (declare (type string value)) (let ((pos 0)) (declare (type fixnum pos)) (labels ((readelt () (case (char value pos) (#\" (interpret (with-output-to-string (out) (loop :with escaped := nil :for ch := (char value (incf pos)) :do (when (and (char= ch #\") (not escaped)) (return)) (setf escaped (and (not escaped) (char= ch #\\))) (unless escaped (write-char ch out))) (incf pos)))) (#\{ (incf pos) (unless (char= (char value pos) #\}) (loop :for val := (readelt) :collect val :into vals :do (let ((next (char value pos))) (incf pos) (ecase next (#\,) (#\} (return vals))))))) (t (let ((start pos)) (loop :for ch := (char value pos) :do (when (or (char= ch #\,) (char= ch #\})) (return (interpret (subseq value start pos)))) (incf pos)))))) (interpret (word) (if (string= word "NULL") :null (funcall transform word)))) (let* ((arr (readelt)) (dim (if arr (loop :for x := arr :then (car x) :while (consp x) :collect (length x)) '(0)))) (make-array dim :initial-contents arr)))))) ;; Integral array types (let ((read-integral (read-array-value #'parse-integer))) (dolist (oid '(1561 1005 1007 1016 1028)) (set-sql-reader oid read-integral))) ;; String array types (let ((read-strings (read-array-value #'identity))) (dolist (oid '(1014 1002 1009 1015)) (set-sql-reader oid read-strings))) ;; Floating point arrays (set-sql-reader 1231 (read-array-value 'read-from-string)) (set-sql-reader 1021 (read-array-value (lambda (x) (float (read-from-string x))))) ;; Bit of a hack, really. CL needs a proper float parser. (flet ((read-as-double (str) (loop :for ch :across str :for i :from 0 :do (when (char= ch #\e) (setf (char str i) #\d))) (coerce (read-from-string str) 'double-float))) (set-sql-reader 1022 (read-array-value #'read-as-double))) ;; Boolean arrays (flet ((read-as-bool (str) (equal str "t"))) (set-sql-reader 1000 (read-array-value #'read-as-bool))) ;; Working with tables. (defun copy-sql-readtable (&optional (table *sql-readtable*)) (let ((new-table (make-hash-table))) (maphash (lambda (oid interpreter) (setf (gethash oid new-table) interpreter)) table) new-table)) (defparameter *default-sql-readtable* (copy-sql-readtable *sql-readtable*) "A copy of the default readtable that client code can fall back on.") (defun default-sql-readtable () *default-sql-readtable*) postmodern-20151031-git/cl-postgres/messages.lisp000066400000000000000000000224651260073634400217030ustar00rootroot00000000000000(in-package :cl-postgres) ;; For more information about the PostgreSQL scocket protocol, see ;; http://www.postgresql.org/docs/current/interactive/protocol.html (defmacro define-message (name id (&rest arglist) &body parts) "This macro synthesizes a function to send messages of a specific type. It takes care of the plumbing -- calling writer functions on a stream, keeping track of the length of the message -- so that the message definitions themselves stay readable." (let ((writers nil) (socket (gensym)) (strings ()) (base-length 4) (extra-length ())) (setf writers (mapcar (lambda (part) (let ((name (gensym))) (ecase (first part) (uint (incf base-length (second part)) `(,(integer-writer-name (second part) nil) ,socket ,(third part))) (string (push `(,name ,(second part)) strings) (incf base-length 1) ;; The null terminator (push `(enc-byte-length ,name) extra-length) `(write-str ,socket ,name)) (bytes (push `(,name ,(second part)) strings) (push `(length ,name) extra-length) `(write-bytes ,socket ,name))))) parts)) (push `(write-uint4 ,socket (+ ,base-length ,@extra-length)) writers) (when id (push `(write-uint1 ,socket ,(char-code id)) writers)) `(defun ,name ,(cons socket arglist) (declare (type stream ,socket) #.*optimize*) (let ,strings ,@writers)))) ;; Try to enable SSL for a connection. (define-message ssl-request-message nil () (uint 4 80877103)) ;; Sends the initial message and sets a few parameters. (define-message startup-message nil (user database) (uint 4 196608) ;; Identifies protocol 3.0 (string "user") (string user) (string "database") (string database) (string "client_encoding") (string *client-encoding*) (uint 1 0)) ;; Terminates the parameter list ;; Identify a user with a plain-text password. (define-message plain-password-message #\p (password) (string password)) (defun bytes-to-hex-string (bytes) "Convert an array of 0-255 numbers into the corresponding string of \(lowercase) hex codes." (declare (type (vector (unsigned-byte 8)) bytes) #.*optimize*) (let ((digits #.(coerce "0123456789abcdef" 'simple-base-string)) (result (make-string (* (length bytes) 2) :element-type 'base-char))) (loop :for byte :across bytes :for pos :from 0 :by 2 :do (setf (char result pos) (aref digits (ldb (byte 4 4) byte)) (char result (1+ pos)) (aref digits (ldb (byte 4 0) byte)))) result)) (defun md5-password (password user salt) "Apply the hashing that PostgreSQL expects to a password." (declare (type string user password) (type (vector (unsigned-byte 8)) salt) #.*optimize*) (flet ((md5-and-hex (sequence) (bytes-to-hex-string (md5:md5sum-sequence sequence)))) (let* ((pass1 (md5-and-hex (enc-string-bytes (concatenate 'string password user)))) (pass2 (md5-and-hex (concatenate '(vector (unsigned-byte 8) *) (enc-string-bytes pass1) salt)))) (concatenate 'string "md5" pass2)))) ;; Identify a user with an MD5-hashed password. (define-message md5-password-message #\p (password user salt) (string (md5-password password user salt))) (define-message gss-auth-buffer-message #\p (buf) (bytes buf)) ;; Send a query, the simple way. (define-message query-message #\Q (query) (string query)) ;; Parse a query (define-message simple-parse-message #\P (query) (uint 1 0) ;; Name of the prepared statement (string query) (uint 2 0)) ;; Parameter types ;; Parse a query, giving it a name. (define-message parse-message #\P (name query) (string name) (string query) (uint 2 0)) ;; Close a named parsed query, freeing the name. (define-message close-prepared-message #\C (name) (uint 1 #.(char-code #\S)) ;; Prepared statement (string name)) (defun formats-to-bytes (formats) "Formats have to be passed as arrays of 2-byte integers, with 1 indicating binary and 0 indicating plain text." (declare (type vector formats) #.*optimize*) (let* ((result (make-array (* 2 (length formats)) :element-type '(unsigned-byte 8) :initial-element 0))) (loop :for format :across formats :for pos :from 1 :by 2 :do (when format (setf (elt result pos) 1))) result)) ;; Bind the unnamed prepared query, asking for the given result ;; formats. (define-message simple-bind-message #\B (formats) (uint 1 0) ;; Name of the portal (uint 1 0) ;; Name of the prepared statement (uint 2 0) ;; Number of parameter format specs (uint 2 0) ;; Number of parameter specifications (uint 2 (length formats)) ;; Number of result format specifications (bytes (formats-to-bytes formats))) ;; Result format ;; This one was a bit too complex to put into define-message format, ;; so it does everything by hand. (defun bind-message (socket name result-formats parameters) "Bind a prepared statement, ask for the given formats, and pass the given parameters, that can be either string or byte vector. \(vector \(unsigned-byte 8)) parameters will be sent as binary data, useful for binding data for binary long object columns." (declare (type stream socket) (type string name) (type vector result-formats) (type list parameters) #.*optimize*) (let* ((n-params (length parameters)) (param-formats (make-array n-params :element-type 'fixnum)) (param-sizes (make-array n-params :element-type 'fixnum)) (param-values (make-array n-params)) (n-result-formats (length result-formats))) (declare (type (unsigned-byte 16) n-params n-result-formats)) (loop :for param :in parameters :for i :from 0 :do (flet ((set-param (format size value) (setf (aref param-formats i) format (aref param-sizes i) size (aref param-values i) value))) (declare (inline set-param)) (cond ((eq param :null) (set-param 0 0 nil)) ((typep param '(vector (unsigned-byte 8))) (set-param 1 (length param) param)) (t (unless (typep param 'string) (setf param (serialize-for-postgres param))) (etypecase param (string (set-param 0 (enc-byte-length param) param)) ((vector (unsigned-byte 8)) (set-param 1 (length param) param))))))) (write-uint1 socket #.(char-code #\B)) (write-uint4 socket (+ 12 (enc-byte-length name) (* 6 n-params) ;; Input formats and sizes (* 2 n-result-formats) (loop :for size :of-type fixnum :across param-sizes :sum size))) (write-uint1 socket 0) ;; Name of the portal (write-str socket name) ;; Name of the prepared statement (write-uint2 socket n-params) ;; Number of parameter format specs (loop :for format :across param-formats ;; Param formats (text/binary) :do (write-uint2 socket format)) (write-uint2 socket n-params) ;; Number of parameter specifications (loop :for param :across param-values :for size :across param-sizes :do (write-int4 socket (if param size -1)) :do (when param (if (typep param '(vector (unsigned-byte 8))) (write-sequence param socket) (enc-write-string param socket)))) (write-uint2 socket n-result-formats) ;; Number of result formats (loop :for format :across result-formats ;; Result formats (text/binary) :do (write-uint2 socket (if format 1 0))))) ;; Describe the anonymous portal, so we can find out what kind of ;; result types will be passed. (define-message simple-describe-message #\D () (uint 1 #.(char-code #\S)) ;; This is a statement describe (uint 1 0)) ;; Name of the portal ;; Describe a named portal. (define-message describe-prepared-message #\D (name) (uint 1 #.(char-code #\S)) ;; This is a statement describe (string name)) ;; Execute a bound statement. (define-message simple-execute-message #\E () (uint 1 0) ;; Name of the portal (uint 4 0)) ;; Max amount of rows (0 = all rows) ;; Flush the sent messages, force server to start responding. (define-message flush-message #\H ()) ;; For re-synchronizing a socket. (define-message sync-message #\S ()) ;; Tell the server we are about to close the connection. (define-message terminate-message #\X ()) ;; To get out of the copy-in protocol. (define-message copy-done-message #\c ()) (defun copy-data-message (socket data) (declare (type string data) #.*optimize*) (write-uint1 socket 100) (write-uint4 socket (+ 4 (length data))) (enc-write-string data socket)) (define-message copy-fail-message #\f (reason) (string reason)) postmodern-20151031-git/cl-postgres/package.lisp000066400000000000000000000062261260073634400214640ustar00rootroot00000000000000(defpackage :cl-postgres (:use :common-lisp) (:export #:database-error #:database-connection-lost #:database-error-message #:database-error-code #:database-error-detail #:database-error-query #:database-error-cause #:database-error-constraint-name #:database-connection #:database-connection-error #:database-socket-error #:connection-meta #:connection-parameters #:open-database #:reopen-database #:database-open-p #:close-database #:wait-for-notification #:exec-query #:prepare-query #:unprepare-query #:exec-prepared #:field-name #:field-type #:row-reader #:def-row-reader #:next-row #:next-field #:list-row-reader #:log-query #:vector-row-reader #:alist-row-reader #:postgresql-notification #:postgresql-notification-channel #:postgresql-notification-payload #:postgresql-notification-pid #:postgresql-warning #:ignore-row-reader #:*sql-readtable* #:copy-sql-readtable #:default-sql-readtable #:set-sql-reader #:set-sql-datetime-readers #:serialize-for-postgres #:to-sql-string #:*silently-truncate-rationals* #:*query-callback* #:*query-log* #:open-db-writer #:db-write-row #:close-db-writer #:*ssl-certificate-file* #:*ssl-key-file* #+(and sbcl unix) #:*unix-socket-dir*)) (defpackage :cl-postgres-error (:use :common-lisp :cl-postgres) (:export #:admin-shutdown #:cannot-connect-now #:check-violation #:crash-shutdown #:data-exception #:db-division-by-zero #:feature-not-supported #:floating-point-exception #:foreign-key-violation #:insufficient-resources #:insufficient-privilege #:transaction-rollback #:serialization-failure #:transaction-integrity-constraint-violation #:statement-completion-unknown #:deadlock-detected #:integrity-violation #:internal-error #:invalid-datetime-format #:lock-not-available #:not-null-violation #:numeric-value-out-of-range #:object-in-use #:object-state-error #:operator-intervention #:program-limit-exceeded #:query-canceled #:restrict-violation #:server-shutdown #:syntax-error-or-access-violation #:system-error #:unique-violation)) (in-package :cl-postgres) (eval-when (:compile-toplevel :load-toplevel :execute) ;; Optimization settings (only used by functions that need it). (defparameter *optimize* '(optimize (speed 3) #-ecl(safety 0) #+ecl(safety 1) (space 1) (debug 1) (compilation-speed 0)))) postmodern-20151031-git/cl-postgres/protocol.lisp000066400000000000000000000514221260073634400217300ustar00rootroot00000000000000(in-package :cl-postgres) ;; For more information about the PostgreSQL scocket protocol, see ;; http://www.postgresql.org/docs/current/interactive/protocol.html (define-condition protocol-error (error) ((message :initarg :message)) (:report (lambda (err stream) (format stream "PostgreSQL protocol error: ~A" (slot-value err 'message)))) (:documentation "This is raised if something really unexpected happens in the communcation with the server. Should only happen in case of a bug or a connection to something that is not a \(supported) PostgreSQL server at all.")) (defmacro message-case (socket &body clauses) "Helper macro for reading messages from the server. A list of cases \(characters that identify the message) can be given, each with a body that handles the message, or the keyword :skip to skip the message. Cases for error and warning messages are always added. The body may contain an initial parameter of the form :LENGTH-SYM SYMBOL where SYMBOL is a symbol to which the remaining length of the packet is bound. This value indicates the number of bytes that have to be read from the socket." (let ((socket-name (gensym)) (size-name (gensym)) (char-name (gensym)) (iter-name (gensym)) (t-found nil) (size-sym (and (eq (car clauses) :length-sym) (progn (pop clauses) (pop clauses))))) (flet ((expand-characters (chars) (cond ((eq chars t) (setf t-found t) t) ((consp chars) (mapcar #'char-code chars)) (t (char-code chars))))) `(let* ((,socket-name ,socket)) (declare (type stream ,socket-name)) (labels ((,iter-name () (let ((,char-name (read-uint1 ,socket-name)) (,size-name (read-uint4 ,socket-name))) (declare (type (unsigned-byte 8) ,char-name) (type (unsigned-byte 32) ,size-name) (ignorable ,size-name)) (case ,char-name (#.(char-code #\A) (get-notification ,socket-name) (,iter-name)) (#.(char-code #\E) (get-error ,socket-name)) (#.(char-code #\S) ;; ParameterStatus: read and continue (update-parameter ,socket-name) (,iter-name)) (#.(char-code #\N) ;; A warning (get-warning ,socket-name) (,iter-name)) ,@(mapcar (lambda (clause) `(,(expand-characters (first clause)) ,(if (eq (second clause) :skip) `(skip-bytes ,socket-name (- ,size-name 4)) (if size-sym `(let ((,size-sym (- ,size-name 4))) ,@(cdr clause)) `(progn ,@(cdr clause)))))) clauses) ,@(unless t-found `((t (ensure-socket-is-closed ,socket-name) (error 'protocol-error :message (format nil "Unexpected message received: ~A" (code-char ,char-name)))))))))) (,iter-name)))))) (defparameter *connection-params* nil "Bound to the current connection's parameter table when executing a query.") (defun update-parameter (socket) (let ((name (read-str socket)) (value (read-str socket))) (setf (gethash name *connection-params*) value))) (defun read-byte-delimited (socket) "Read the fields of a null-terminated list of byte + string values and put them in an alist." (loop :for type = (read-uint1 socket) :until (zerop type) :collect (cons (code-char type) (read-simple-str socket)))) (define-condition postgresql-notification (simple-warning) ((pid :initarg :pid :accessor postgresql-notification-pid) (channel :initarg :channel :accessor postgresql-notification-channel) (payload :initarg :payload :accessor postgresql-notification-payload))) (defun get-notification (socket) "Read an asynchronous notification message from the socket and signal a condition for it." (let ((pid (read-int4 socket)) (channel (read-str socket)) (payload (read-str socket))) (warn 'postgresql-notification :pid pid :channel channel :payload payload :format-control "Asynchronous notification ~S~@[ (payload: ~S)~] received from ~ server process with PID ~D." :format-arguments (list channel payload pid)))) (defun get-error (socket) "Read an error message from the socket and raise the corresponding database-error condition." (let ((data (read-byte-delimited socket))) (flet ((get-field (char) (cdr (assoc char data)))) (let ((code (get-field #\C))) ;; These are the errors "ADMIN SHUTDOWN" and "CRASH SHUTDOWN", ;; in which case the server will close the connection right ;; away. (when (or (string= code "57P01") (string= code "57P02")) (ensure-socket-is-closed socket)) (error (cl-postgres-error::get-error-type code) :code code :message (get-field #\M) :detail (get-field #\D) :hint (get-field #\H) :context (get-field #\W) :position (let ((position (get-field #\p))) (when position (parse-integer position)))))))) (define-condition postgresql-warning (simple-warning) ()) (defun get-warning (socket) "Read a warning from the socket and emit it." (let ((data (read-byte-delimited socket))) (flet ((get-field (char) (cdr (assoc char data)))) (warn 'postgresql-warning :format-control "PostgreSQL warning: ~A~@[~%~A~]" :format-arguments (list (get-field #\M) (or (get-field #\D) (get-field #\H))))))) (defparameter *ssl-certificate-file* nil "When set to a filename, this file will be used as client certificate for SSL connections.") (defparameter *ssl-key-file* nil "When set to a filename, this file will be used as client key for SSL connections.") ;; The let is used to remember that we have found the ;; cl+ssl:make-ssl-client-stream function before. (let ((make-ssl-stream nil)) (defun initiate-ssl (socket required) "Initiate SSL handshake with the PostgreSQL server, and wrap the socket in an SSL stream. When require is true, an error will be raised when the server does not support SSL." (unless make-ssl-stream (unless (find-package :cl+ssl) (error 'database-error :message "CL+SSL is not loaded. Load it to enable SSL.")) (setf make-ssl-stream (intern (string '#:make-ssl-client-stream) :cl+ssl))) (ssl-request-message socket) (force-output socket) (ecase (read-byte socket) (#.(char-code #\S) (setf socket (funcall make-ssl-stream socket :key *ssl-key-file* :certificate *ssl-certificate-file*))) (#.(char-code #\N) (when required (error 'database-error :message "Server does not support SSL encryption.")))))) (defun authenticate (socket conn) "Try to initiate a connection. Caller should close the socket if this raises a condition." (let ((gss-context nil) (gss-init-function nil) (user (connection-user conn)) (password (connection-password conn)) (database (connection-db conn)) (use-ssl (connection-use-ssl conn))) (unless (eq use-ssl :no) (setf socket (initiate-ssl socket (eq use-ssl :yes)))) (startup-message socket user database) (force-output socket) (labels ((init-gss-msg (in-buffer) (when (null gss-init-function) (when (null (find-package "CL-GSS")) (error 'database-error :message "To use GSS authentication, make sure the CL-GSS package is loaded.")) (setq gss-init-function (find-symbol "INIT-SEC" "CL-GSS")) (unless gss-init-function (error 'database-error :message "INIT-SEC not found in CL-GSS package"))) (multiple-value-bind (continue-needed context buffer flags) (funcall gss-init-function (format nil "~a@~a" (connection-service conn) (connection-host conn)) :flags '(:mutual) :context gss-context :input-token in-buffer) (declare (ignore flags)) (setq gss-context context) (when buffer (gss-auth-buffer-message socket buffer)) (force-output socket) continue-needed))) (loop (message-case socket :length-sym size ;; Authentication message (#\R (let ((type (read-uint4 socket))) (ecase type (0 (return)) (2 (error 'database-error :message "Unsupported Kerberos authentication requested.")) (3 (unless password (error "Server requested plain-password authentication, but no password was given.")) (plain-password-message socket password) (force-output socket)) (4 (error 'database-error :message "Unsupported crypt authentication requested.")) (5 (unless password (error "Server requested md5-password authentication, but no password was given.")) (md5-password-message socket password user (read-bytes socket 4)) (force-output socket)) (6 (error 'database-error :message "Unsupported SCM authentication requested.")) (7 (when gss-context (error 'database-error :message "Got GSS init message when a context was already established")) (init-gss-msg nil)) (8 (unless gss-context (error 'database-error :message "Got GSS continuation message without a context")) (init-gss-msg (read-bytes socket (- size 4))))))))))) (loop (message-case socket ;; BackendKeyData - ignore (#\K :skip) ;; ReadyForQuery (#\Z (read-uint1 socket) (return)))) socket) (defclass field-description () ((name :initarg :name :accessor field-name) (type-id :initarg :type-id :accessor field-type) (interpreter :initarg :interpreter :accessor field-interpreter) (receive-binary-p :initarg :receive-binary-p :accessor field-binary-p)) (:documentation "Description of a field in a query result.")) (defun read-field-descriptions (socket) "Read the field descriptions for a query result and put them into an array of field-description objects." (declare (type stream socket) #.*optimize*) (let* ((number (read-uint2 socket)) (descriptions (make-array number))) (declare (type fixnum number) (type (simple-array field-description) descriptions)) (dotimes (i number) (let* ((name (read-str socket)) (table-oid (read-uint4 socket)) (column (read-uint2 socket)) (type-id (read-uint4 socket)) (size (read-uint2 socket)) (type-modifier (read-uint4 socket)) (format (read-uint2 socket)) (interpreter (type-interpreter type-id))) (declare (ignore table-oid column size type-modifier format) (type string name) (type (unsigned-byte 32) type-id)) (setf (elt descriptions i) (make-instance 'field-description :name name :type-id type-id :interpreter (cdr interpreter) :receive-binary-p (car interpreter))))) descriptions)) (defun terminate-connection (socket) "Close a connection, notifying the server." (terminate-message socket) (close socket)) ;; This is a hacky way to communicate the amount of effected rows up ;; from look-for-row to the send-execute or send-query that (directly ;; or indirectly) called it. (defparameter *effected-rows* nil) (defun look-for-row (socket) "Read server messages until either a new row can be read, or there are no more results. Return a boolean indicating whether any more results are available, and, if available, stores the amount of effected rows in *effected-rows*. Also handle getting out of copy-in/copy-out states \(which are not supported)." (declare (type stream socket) #.*optimize*) (loop (message-case socket ;; CommandComplete (#\C (let* ((command-tag (read-str socket)) (space (position #\Space command-tag :from-end t))) (when space (setf *effected-rows* (parse-integer command-tag :junk-allowed t :start (1+ space)))) (return-from look-for-row nil))) ;; CopyInResponse (#\G (read-uint1 socket) (skip-bytes socket (* 2 (read-uint2 socket))) ;; The field formats (copy-done-message socket) (error 'database-error :message "Copy-in not supported.")) ;; CopyOutResponse (#\H (read-uint1 socket) (skip-bytes socket (* 2 (read-uint2 socket))) ;; The field formats (error 'database-error :message "Copy-out not supported.")) ;; DataRow (#\D (skip-bytes socket 2) (return-from look-for-row t)) ;; EmptyQueryResponse (#\I (warn "Empty query sent.") (return-from look-for-row nil))))) (defun try-to-sync (socket sync-sent) "Try to re-synchronize a connection by sending a sync message if it hasn't already been sent, and then looking for a ReadyForQuery message." (when (open-stream-p socket) (let ((ok nil)) (unwind-protect (progn (unless sync-sent (sync-message socket) (force-output socket)) ;; TODO initiate timeout on the socket read, signal timeout error (loop :while (and (not ok) (open-stream-p socket)) :do (message-case socket (#\Z (read-uint1 socket) (setf ok t)) (t :skip)))) (unless ok ;; if we can't sync, make sure the socket is shot ;; (e.g. a timeout, or aborting execution with a restart from sldb) (ensure-socket-is-closed socket :abort t)))))) (defmacro with-syncing (&body body) "Macro to wrap a block in a handler that will try to re-sync the connection if something in the block raises a condition. Not hygienic at all, only used right below here." `(let ((sync-sent nil) (ok nil)) (handler-case (unwind-protect (multiple-value-prog1 (progn ,@body) (setf ok t)) (unless ok (try-to-sync socket sync-sent))) (end-of-file (c) (ensure-socket-is-closed socket :abort t) (error c))))) (defmacro returning-effected-rows (value &body body) "Computes a value, then runs a body, then returns, as multiple values, that value and the amount of effected rows, if any (see *effected rows*)." (let ((value-name (gensym))) `(let* ((*effected-rows* nil) (,value-name ,value)) ,@body (if *effected-rows* (values ,value-name *effected-rows*) ,value-name)))) (defun send-query (socket query row-reader) "Send a query to the server, and apply the given row-reader to the results." (declare (type stream socket) (type string query) #.*optimize*) (with-syncing (with-query (query) (let ((row-description nil)) (simple-parse-message socket query) (simple-describe-message socket) (flush-message socket) (force-output socket) (message-case socket ;; ParseComplete (#\1)) (message-case socket ;; ParameterDescription (#\t :skip)) (message-case socket ;; RowDescription (#\T (setf row-description (read-field-descriptions socket))) ;; NoData (#\n)) (simple-bind-message socket (map 'vector 'field-binary-p row-description)) (simple-execute-message socket) (sync-message socket) (setf sync-sent t) (force-output socket) (message-case socket ;; BindComplete (#\2)) (returning-effected-rows (if row-description (funcall row-reader socket row-description) (look-for-row socket)) (message-case socket ;; ReadyForQuery, skipping transaction status (#\Z (read-uint1 socket)))))))) (defun send-parse (socket name query) "Send a parse command to the server, giving it a name." (declare (type stream socket) (type string name query) #.*optimize*) (with-syncing (with-query (query) (parse-message socket name query) (flush-message socket) (force-output socket) (message-case socket ;; ParseComplete (#\1))))) (defun send-close (socket name) "Send a close command to the server, giving it a name." (declare (type stream socket) (type string name) #.*optimize*) (with-syncing (close-prepared-message socket name) (flush-message socket) (force-output socket) (message-case socket ;; CloseComplete (#\3)))) (defun send-execute (socket name parameters row-reader) "Execute a previously parsed query, and apply the given row-reader to the result." (declare (type stream socket) (type string name) (type list parameters) #.*optimize*) (with-syncing (let ((row-description nil) (n-parameters 0)) (declare (type (unsigned-byte 16) n-parameters)) (describe-prepared-message socket name) (flush-message socket) (force-output socket) (message-case socket ;; ParameterDescription (#\t (setf n-parameters (read-uint2 socket)) (skip-bytes socket (* 4 n-parameters)))) (message-case socket ;; RowDescription (#\T (setf row-description (read-field-descriptions socket))) ;; NoData (#\n)) (unless (= (length parameters) n-parameters) (error 'database-error :message (format nil "Incorrect number of parameters given for prepared statement ~A." name))) (bind-message socket name (map 'vector 'field-binary-p row-description) parameters) (simple-execute-message socket) (sync-message socket) (setf sync-sent t) (force-output socket) (message-case socket ;; BindComplete (#\2)) (returning-effected-rows (if row-description (funcall row-reader socket row-description) (look-for-row socket)) (message-case socket ;; CommandComplete (#\C (read-str socket) (message-case socket (#\Z (read-uint1 socket)))) ;; ReadyForQuery, skipping transaction status (#\Z (read-uint1 socket))))))) (defun build-row-reader (function-form fields body) "Helper for the following two macros." (let ((socket (gensym))) `(,@function-form (,socket ,fields) (declare (type stream ,socket) (type (simple-array field-description) ,fields)) (flet ((next-row () (look-for-row ,socket)) (next-field (field) (declare (type field-description field)) (let ((size (read-int4 ,socket))) (declare (type (signed-byte 32) size)) (if (eq size -1) :null (funcall (field-interpreter field) ,socket size))))) ,@body)))) (defmacro row-reader ((fields) &body body) "Create a row-reader, using the given name for the fields argument and the given body for reading the rows. A row reader is a function that is used to do something with the results of a query. It has two local functions: next-row and next-field, the first should be called once per row and will return a boolean indicating whether there are any more rows, the second should be called once for every element in the fields vector, with that field as argument, to read a single value in a row. See list-row-reader in public.lisp for an example." (build-row-reader '(lambda) fields body)) (defmacro def-row-reader (name (fields) &body body) "Create a row reader, as in the row-reader macro, and assign a name to it." (build-row-reader `(defun ,name) fields body)) postmodern-20151031-git/cl-postgres/public.lisp000066400000000000000000000315511260073634400213460ustar00rootroot00000000000000(in-package :cl-postgres) (defclass database-connection () ((host :initarg :host :reader connection-host) (port :initarg :port :reader connection-port) (database :initarg :db :reader connection-db) (user :initarg :user :reader connection-user) (password :initarg :password :reader connection-password) (use-ssl :initarg :ssl :reader connection-use-ssl) (service :initarg :service :accessor connection-service) (socket :initarg :socket :accessor connection-socket) (meta :initform nil) (available :initform t :accessor connection-available) (parameters :accessor connection-parameters) (timestamp-format :accessor connection-timestamp-format)) (:documentation "Representation of a database connection. Contains login information in order to be able to automatically re-establish a connection when it is somehow closed.")) (defun connection-meta (connection) "Retrieves the meta field of a connection, the primary purpose of which is to store information about the prepared statements that exists for it." (or (slot-value connection 'meta) (let ((meta-data (make-hash-table))) (setf (slot-value connection 'meta) meta-data) meta-data))) (defun database-open-p (connection) "Returns a boolean indicating whether the given connection is currently connected." (and (connection-socket connection) (open-stream-p (connection-socket connection)))) (defun open-database (database user password host &optional (port 5432) (use-ssl :no) (service "postgres")) "Create and connect a database object. use-ssl may be :no, :yes, or :try." (check-type database string) (check-type user string) (check-type password (or null string)) (check-type host (or string (eql :unix)) "a string or :unix") (check-type port (integer 1 65535) "an integer from 1 to 65535") (check-type use-ssl (member :no :yes :try) ":no, :yes, or :try") (let ((conn (make-instance 'database-connection :host host :port port :user user :password password :socket nil :db database :ssl use-ssl :service service))) (initiate-connection conn) conn)) #+(and (or sbcl ccl allegro) unix) (progn (defparameter *unix-socket-dir* #-(or freebsd darwin) "/var/run/postgresql/" #+(or darwin freebsd) "/tmp/" "Directory where the Unix domain socket for PostgreSQL be found.") (defun unix-socket-path (base-dir port) (unless (char= #\/ (aref base-dir (1- (length base-dir)))) (setf base-dir (concatenate 'string base-dir "/"))) (format nil "~a.s.PGSQL.~a" base-dir port)) #+sbcl (defun unix-socket-connect (path) (let ((sock (make-instance 'sb-bsd-sockets:local-socket :type :stream))) (sb-bsd-sockets:socket-connect sock path) (sb-bsd-sockets:socket-make-stream sock :input t :output t :element-type '(unsigned-byte 8)))) #+ccl (defun unix-socket-connect (path) (ccl:make-socket :type :stream :address-family :file :format :binary :remote-filename path)) #+allegro (defun unix-socket-connect (path) (socket:make-socket :type :stream :address-family :file :format :binary :remote-filename path))) #+sbcl (defun inet-socket-connect (host port) (let ((sock (make-instance 'sb-bsd-sockets:inet-socket :type :stream :protocol :tcp)) (host (sb-bsd-sockets:host-ent-address (sb-bsd-sockets:get-host-by-name host)))) (sb-bsd-sockets:socket-connect sock host port) (sb-bsd-sockets:socket-make-stream sock :input t :output t :buffering :full :element-type '(unsigned-byte 8)))) #+ccl (defun inet-socket-connect (host port) (ccl:make-socket :format :binary :remote-host host :remote-port port)) #+allegro (defun inet-socket-connect (host port) (socket:make-socket :remote-host host :remote-port port :format :binary :type :stream)) (defun initiate-connection (conn) "Check whether a connection object is connected, try to connect it if it isn't." (flet ((add-restart (err) (restart-case (error (wrap-socket-error err)) (:reconnect () :report "Try again." (initiate-connection conn)))) (assert-unix () #+unix t #-unix (error "Unix sockets only available on Unix (really)"))) (handler-case (let ((socket #-(or allegro sbcl ccl) (usocket:socket-stream (usocket:socket-connect (connection-host conn) (connection-port conn) :element-type '(unsigned-byte 8))) #+(or allegro sbcl ccl) (cond ((equal (connection-host conn) :unix) (assert-unix) (unix-socket-connect (unix-socket-path *unix-socket-dir* (connection-port conn)))) ((and (stringp (connection-host conn)) (char= #\/ (aref (connection-host conn) 0))) (assert-unix) (unix-socket-connect (unix-socket-path (connection-host conn) (connection-port conn)))) ((and (pathnamep (connection-host conn)) (eql :absolute (pathname-directory (connection-host conn)))) (assert-unix) (unix-socket-connect (unix-socket-path (namestring (connection-host conn)) (connection-port conn)))) (t (inet-socket-connect (connection-host conn) (connection-port conn))))) (finished nil) (*connection-params* (make-hash-table :test 'equal))) (setf (slot-value conn 'meta) nil (connection-parameters conn) *connection-params*) (unwind-protect (setf socket (authenticate socket conn) (connection-timestamp-format conn) (if (string= (gethash "integer_datetimes" (connection-parameters conn)) "on") :integer :float) (connection-socket conn) socket finished t) (unless finished (ensure-socket-is-closed socket)))) #-(or allegro sbcl ccl)(usocket:socket-error (e) (add-restart e)) #+ccl (ccl:socket-error (e) (add-restart e)) #+allegro(excl:socket-error (e) (add-restart e)) #+sbcl(sb-bsd-sockets:socket-error (e) (add-restart e)) (stream-error (e) (add-restart e)))) (values)) (defun reopen-database (conn) "Reconnect a disconnected database connection." (unless (database-open-p conn) (initiate-connection conn))) (defun ensure-connection (conn) "Used to make sure a connection object is connected before doing anything with it." (unless conn (error "No database connection selected.")) (unless (database-open-p conn) (restart-case (error 'database-connection-lost :message "Connection to database server lost.") (:reconnect () :report "Try to reconnect." (initiate-connection conn))))) (defun close-database (connection) "Gracefully disconnect a database connection." (when (database-open-p connection) (terminate-connection (connection-socket connection))) (values)) (defmacro using-connection (connection &body body) "This is used to prevent a row-reader from recursively calling some query function. Because the connection is still returning results from the previous query when a row-reading is being executed, starting another query will not work as expected \(or at all, in general). This might also raise an error when you are using a single database connection from multiple threads, but you should not do that at all. Also binds *timestamp-format* and *connection-params*, which might be needed by the code interpreting the query results." (let ((connection-name (gensym))) `(let* ((,connection-name ,connection) (*timestamp-format* (connection-timestamp-format ,connection-name)) (*connection-params* (connection-parameters ,connection-name))) (when (not (connection-available ,connection-name)) (error 'database-error :message "This connection is still processing another query.")) (setf (connection-available ,connection-name) nil) (unwind-protect (progn ,@body) (setf (connection-available ,connection-name) t))))) (defmacro with-reconnect-restart (connection &body body) "When, inside the body, an error occurs that breaks the connection socket, a condition of type database-connection-error is raised, offering a :reconnect restart." (let ((connection-name (gensym)) (body-name (gensym)) (retry-name (gensym))) `(let ((,connection-name ,connection)) (ensure-connection ,connection-name) (labels ((,body-name () (handler-case (progn ,@body) (stream-error (e) (cond ((eq (connection-socket ,connection-name) (stream-error-stream e)) (ensure-socket-is-closed (connection-socket ,connection-name) :abort t) (,retry-name (wrap-socket-error e))) (t (error e)))) (cl-postgres-error:server-shutdown (e) (ensure-socket-is-closed (connection-socket ,connection-name) :abort t) (,retry-name e)))) (,retry-name (err) (restart-case (error err) (:reconnect () :report "Try to reconnect" (reopen-database ,connection-name) (,body-name))))) (,body-name))))) (defun wait-for-notification (connection) "Perform a blocking wait for asynchronous notification. Return the channel string, the payload and notifying pid as multiple values." (block nil (with-reconnect-restart connection (handler-bind ((postgresql-notification (lambda (c) (return (values (postgresql-notification-channel c) (postgresql-notification-payload c) (postgresql-notification-pid c)))))) (message-case (connection-socket connection)))))) (defun exec-query (connection query &optional (row-reader 'ignore-row-reader)) "Execute a query string and apply the given row-reader to the result." (check-type query string) (with-reconnect-restart connection (using-connection connection (send-query (connection-socket connection) query row-reader)))) (defun prepare-query (connection name query) "Prepare a query string and store it under the given name." (check-type query string) (check-type name string) (with-reconnect-restart connection (using-connection connection (send-parse (connection-socket connection) name query) (values)))) (defun unprepare-query (connection name) "Close the prepared query given by name." (check-type name string) (with-reconnect-restart connection (using-connection connection (send-close (connection-socket connection) name) (values)))) (defun exec-prepared (connection name parameters &optional (row-reader 'ignore-row-reader)) "Execute a previously prepared query with the given parameters, apply a row-reader to the result." (check-type name string) (check-type parameters list) (with-reconnect-restart connection (using-connection connection (send-execute (connection-socket connection) name parameters row-reader)))) ;; A row-reader that returns a list of (field-name . field-value) ;; alist for the returned rows. (def-row-reader alist-row-reader (fields) (loop :while (next-row) :collect (loop :for field :across fields :collect (cons (field-name field) (next-field field))))) ;; Row-reader that returns a list of lists. (def-row-reader list-row-reader (fields) (loop :while (next-row) :collect (loop :for field :across fields :collect (next-field field)))) ;; Row-reader that returns a vector of vectors. (def-row-reader vector-row-reader (fields) (let ((rows (make-array 1 :adjustable t :fill-pointer 0))) (loop :for row = (make-array (length fields)) :while (next-row) :do (progn (loop :for field :across fields :for idx :upfrom 0 :do (setf (aref row idx) (next-field field))) (vector-push-extend row rows))) rows)) ;; Row-reader that discards the query results. (def-row-reader ignore-row-reader (fields) (loop :while (next-row) :do (loop :for field :across fields :do (next-field field))) (values)) postmodern-20151031-git/cl-postgres/sql-string.lisp000066400000000000000000000117541260073634400221760ustar00rootroot00000000000000(in-package :cl-postgres) (defun escape-bytes (bytes) "Escape an array of octets in PostgreSQL's horribly inefficient textual format for binary data." (let ((*print-pretty* nil)) (with-output-to-string (out) (loop :for byte :of-type fixnum :across bytes :do (if (or (< byte 32) (> byte 126) (= byte 39) (= byte 92)) (progn (princ #\\ out) (princ (digit-char (ldb (byte 3 6) byte) 8) out) (princ (digit-char (ldb (byte 3 3) byte) 8) out) (princ (digit-char (ldb (byte 3 0) byte) 8) out)) (princ (code-char byte) out)))))) (defparameter *silently-truncate-rationals* t) (defun write-rational-as-floating-point (number stream digit-length-limit) (declare #.*optimize*) (flet ((fail () (if *silently-truncate-rationals* (return-from write-rational-as-floating-point) (error 'database-error :message (format nil "Can not write the rational ~a with only ~a digits" number digit-length-limit))))) (multiple-value-bind (quotient remainder) (truncate (if (< number 0) (prog1 (- number) (write-char #\- stream)) number)) (let* ((quotient-part (princ-to-string quotient)) (decimal-length-limit (- digit-length-limit (length quotient-part)))) (write-string quotient-part stream) (when (<= decimal-length-limit 0) (fail)) (unless (zerop remainder) (write-char #\. stream)) (loop :for decimal-digits :upfrom 1 :until (zerop remainder) :do (when (> decimal-digits decimal-length-limit) (fail)) :do (multiple-value-bind (quotient rem) (floor (* remainder 10)) (princ quotient stream) (setf remainder rem))))))) (defun write-quoted (string out) (write-char #\" out) (loop :for ch :across string :do (when (member ch '(#\" #\\)) (write-char #\\ out)) (write-char ch out)) (write-char #\" out)) (defgeneric to-sql-string (arg) (:documentation "Turn a lisp value into a string containing its SQL representation. Returns an optional second value that indicates whether the string should be escaped before being put into a query.") (:method ((arg string)) (values arg t)) (:method ((arg vector)) (if (typep arg '(vector (unsigned-byte 8))) (values (escape-bytes arg) t) (values (with-output-to-string (out) (write-char #\{ out) (loop :for sep := "" :then #\, :for x :across arg :do (princ sep out) (multiple-value-bind (string escape) (to-sql-string x) (if escape (write-quoted string out) (write-string string out)))) (write-char #\} out)) t))) (:method ((arg array)) (values (with-output-to-string (out) (labels ((recur (dims off) (write-char #\{ out) (if (cdr dims) (let ((factor (reduce #'* (cdr dims)))) (loop :for i :below (car dims) :for sep := "" :then #\, :do (princ sep out) (recur (cdr dims) (+ off (* factor i))))) (loop :for sep := "" :then #\, :for i :from off :below (+ off (car dims)) :do (princ sep out) (multiple-value-bind (string escape) (to-sql-string (row-major-aref arg i)) (if escape (write-quoted string out) (write-string string out))))) (write-char #\} out))) (recur (array-dimensions arg) 0))) t)) (:method ((arg integer)) (princ-to-string arg)) (:method ((arg float)) (format nil "~f" arg)) #-clisp (:method ((arg double-float)) ;; CLISP doesn't allow methods on double-float (format nil "~,,,,,,'EE" arg)) (:method ((arg ratio)) ;; Possible optimization: we could probably build up the same binary structure postgres ;; sends us instead of sending it as a string. See the "numeric" interpreter for more details... (with-output-to-string (result) ;; PostgreSQL happily handles 200+ decimal digits, but the SQL standard only requires ;; 38 digits from the NUMERIC type, and Oracle also doesn't handle more. For practical ;; reasons we also draw the line there. If someone needs full rational numbers then ;; 200 wouldn't help them much more than 38... (write-rational-as-floating-point arg result 38))) (:method ((arg (eql t))) "true") (:method ((arg (eql nil))) "false") (:method ((arg (eql :null))) "NULL") (:method ((arg t)) (error "Value ~S can not be converted to an SQL literal." arg))) (defgeneric serialize-for-postgres (arg) (:documentation "Conversion function used to turn a lisp value into a value that PostgreSQL understands when sent through its socket connection. May return a string or a (vector (unsigned-byte 8)).") (:method (arg) (to-sql-string arg))) postmodern-20151031-git/cl-postgres/strings-ascii.lisp000066400000000000000000000031311260073634400226400ustar00rootroot00000000000000(in-package :cl-postgres) (defparameter *client-encoding* "SQL_ASCII") (declaim (inline enc-byte-length)) (defun enc-byte-length (sequence) (length sequence)) (declaim (ftype (function (t &key (:null-terminated t) (:byte-length unsigned-byte)) string) enc-read-string)) (defun enc-read-string (stream &key null-terminated byte-length) "Read an ascii-string from a byte stream, until either a null byte is reached or the given amount of bytes have been read." (declare (type stream stream) (type (or null fixnum) byte-length) #.*optimize*) (let ((bytes-read 0) (string (make-array 64 :element-type 'character :adjustable t :fill-pointer 0))) (loop (when (and byte-length (>= bytes-read byte-length)) (return)) (let ((next-char (read-byte stream))) (incf bytes-read) (when (and null-terminated (eq next-char 0)) (return)) (vector-push-extend (code-char next-char) string))) string)) (declaim (ftype (function (string) (simple-array (unsigned-byte 8) (*))) enc-string-bytes)) (defun enc-string-bytes (string) "Convert an ascii string to an array of octets." (map '(simple-array (unsigned-byte 8) (*)) 'char-code string)) (defun enc-write-string (string stream) "Write an ascii string to a stream." (declare (type stream stream) (type string string) #.*optimize*) (loop :for char :of-type character :across string :do (write-byte (char-code char) stream))) postmodern-20151031-git/cl-postgres/strings-utf-8.lisp000066400000000000000000000020041260073634400225110ustar00rootroot00000000000000(in-package :cl-postgres) (defparameter *client-encoding* "UNICODE") (declaim (inline enc-byte-length)) (defun enc-byte-length (sequence) (cl-postgres-trivial-utf-8:utf-8-byte-length sequence)) (declaim (inline enc-write-string)) (defun enc-write-string (string output &key null-terminate) (cl-postgres-trivial-utf-8:write-utf-8-bytes string output :null-terminate null-terminate)) (declaim (inline enc-read-string)) (declaim (ftype (function (t &key (:null-terminated t) (:byte-length unsigned-byte)) string) enc-read-string)) (defun enc-read-string (input &key null-terminated (byte-length -1)) (cl-postgres-trivial-utf-8:read-utf-8-string input :null-terminated null-terminated :byte-length byte-length)) (declaim (inline enc-string-bytes)) (defun enc-string-bytes (string &key null-terminate) (cl-postgres-trivial-utf-8:string-to-utf-8-bytes string :null-terminate null-terminate)) postmodern-20151031-git/cl-postgres/tests.lisp000066400000000000000000000135511260073634400212320ustar00rootroot00000000000000(defpackage :cl-postgres-tests (:use :common-lisp :Eos :simple-date :cl-postgres :cl-postgres-error) (:export #:prompt-connection)) (in-package :cl-postgres-tests) (defparameter *test-connection* '("test" "test" "" "localhost")) (defun prompt-connection (&optional (list *test-connection*)) (flet ((ask (name pos) (format *query-io* "~a (enter to keep '~a'): " name (nth pos list)) (finish-output *query-io*) (let ((answer (read-line *query-io*))) (unless (string= answer "") (setf (nth pos list) answer))))) (format *query-io* "~%To run this test, you must configure a database connection.~%") (ask "Database name" 0) (ask "User" 1) (ask "Password" 2) (ask "Hostname" 3))) ;; Adjust the above to some db/user/pass/host/[port] combination that ;; refers to a valid postgresql database, then after loading the file, ;; run the tests with (Eos:run! :cl-postgres) (def-suite :cl-postgres) (in-suite :cl-postgres) (defmacro with-test-connection (&body body) `(let ((connection (apply 'open-database *test-connection*))) (unwind-protect (progn ,@body) (close-database connection)))) (test connect-sanity (with-test-connection (is (database-open-p connection)))) (test simple-query (with-test-connection (destructuring-bind ((a b c d e)) (exec-query connection "select 22::integer, 44.5::double precision, 'abcde'::varchar, true::boolean, 4.5::numeric(5,2)" 'list-row-reader) (is (eql a 22)) (is (eql b 44.5d0)) (is (string= c "abcde")) (is (eql d t)) (is (eql e 9/2))))) (test sql-strings (is (string= (to-sql-string :null) "NULL")) (is (string= (to-sql-string t) "true")) (is (string= (to-sql-string 400) "400")) (is (string= (to-sql-string "foo") "foo")) (is (eq t (nth-value 1 (to-sql-string "bar")))) (is (eq nil (nth-value 1 (to-sql-string 10))))) (test date-query (with-test-connection (destructuring-bind ((a b c)) (exec-query connection "select '1980-02-01'::date, '2010-04-05 14:42:21.500'::timestamp, '2 years -4 days'::interval" 'list-row-reader) (is (time= a (encode-date 1980 2 1))) (is (time= b (encode-timestamp 2010 4 5 14 42 21 500))) (is (time= c (encode-interval :year 2 :day -4)))))) (test alist-row-reader (with-test-connection (is (equal (exec-query connection "select 42 as foo, 99 as bar" 'alist-row-reader) '((("foo" . 42) ("bar" . 99))))))) (test prepared-statement (with-test-connection (prepare-query connection "test" "select $1::integer, $2::boolean, $3::text") (is (equal (exec-prepared connection "test" '(42 nil "foo") 'list-row-reader) '((42 nil "foo")))))) (test unprepare-statement (with-test-connection (prepare-query connection "test" "select true") (unprepare-query connection "test") (prepare-query connection "test" "select false") (is (equal (exec-prepared connection "test" '() 'list-row-reader) '((nil)))))) (test prepared-array-param (with-test-connection (prepare-query connection "test" "select ($1::int[])[2]") (is (equal (exec-prepared connection "test" '(#(1 2 3)) 'list-row-reader) '((2)))) (prepare-query connection "test2" "select ($1::text[])[2]") (is (equal (exec-prepared connection "test2" '(#("A" "B" "C")) 'list-row-reader) '(("B")))))) (test blob (with-test-connection (let* ((str "foobar42") (bytes (coerce #(102 111 111 98 97 114 52 50) '(vector (unsigned-byte 8))))) (prepare-query connection "test" "select $1::varchar, $2::bytea") (is (equalp (exec-prepared connection "test" (list str bytes) 'list-row-reader) (list (list str bytes))))))) (test recover-error (with-test-connection (signals cl-postgres-error:syntax-error-or-access-violation (exec-query connection "gubble gubble gabble goo")) (is (equal (exec-query connection "select false" 'list-row-reader) '((nil)))))) (test unique-violation-error (with-test-connection (exec-query connection "create table test (id int not null primary key, name text)") (exec-query connection "insert into test values (1, 'bert')") (signals unique-violation (exec-query connection "insert into test values (1, 'harry')")) (exec-query connection "drop table test"))) (test sql-reader (with-test-connection (let ((*sql-readtable* (copy-sql-readtable))) (set-sql-reader 2249 (lambda (text) (with-input-from-string (*standard-input* text) (read-char) ;; opening paren (let ((x (read))) (read-char) ;; comma (cons x (read)))))) (is (equal (exec-query connection "select (10,20)" 'list-row-reader) '(((10 . 20)))))) (is (equal (exec-query connection "select (30,40)" 'list-row-reader) '(("(30,40)")))))) (test bulk-writer (with-test-connection (exec-query connection "create table test (a int, b text, c date, d timestamp, e int[])") (let ((stream (open-db-writer *test-connection* 'test '(a b c d e)))) ;; test a variety of types (int, text, date, timstamp, int array) (loop for row in '((1 "one" "2012-01-01" "2012-01-01 00:00" #(1 2 3 42)) (2 "two" "2012-01-01" "2012-01-01 00:00" #(3 2 1 42)) ;; make sure utf-8 gets through ok (3 "κόσμε" "2012-01-01" "2012-01-01 00:00" #(1)) ;; make sure tabs get through ok (4 "one two three" "2012-01-01" "2012-01-01 00:00" #(1))) do (db-write-row stream row)) (close-db-writer stream)) (print (exec-query connection "select * from test")) (exec-query connection "drop table test"))) postmodern-20151031-git/cl-postgres/trivial-utf-8.lisp000066400000000000000000000215061260073634400225020ustar00rootroot00000000000000;;; Minimal utf-8 decoding and encoding library. ;;; ;;; See http://common-lisp.net/project/trivial-utf-8/ (defpackage :cl-postgres-trivial-utf-8 (:use :common-lisp) (:export #:utf-8-byte-length #:string-to-utf-8-bytes #:write-utf-8-bytes #:utf-8-group-size #:utf-8-bytes-to-string #:read-utf-8-string #:utf-8-decoding-error)) (in-package :cl-postgres-trivial-utf-8) (eval-when (:compile-toplevel :load-toplevel :execute) (defparameter *optimize* '(optimize (speed 3) #-ecl(safety 0) #+ecl(safety 1) (space 0) (debug 1) (compilation-speed 0)))) (defun utf-8-byte-length (string) "Calculate the amount of bytes needed to encode a string." (declare (type string string) #'*optimize*) (let ((length (length string)) (string (coerce string 'simple-string))) (loop :for char :across string :do (let ((code (char-code char))) (when (> code 127) (incf length (cond ((< code 2048) 1) ((< code 65536) 2) (t 3)))))) length)) (defmacro as-utf-8-bytes (char writer) "Given a character, calls the writer function for every byte in the encoded form of that character." (let ((char-code (gensym))) `(let ((,char-code (char-code ,char))) (declare (type fixnum ,char-code)) (cond ((< ,char-code 128) (,writer ,char-code)) ((< ,char-code 2048) (,writer (logior #b11000000 (ldb (byte 5 6) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 0) ,char-code)))) ((< ,char-code 65536) (,writer (logior #b11100000 (ldb (byte 4 12) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 6) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 0) ,char-code)))) (t (,writer (logior #b11110000 (ldb (byte 3 18) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 12) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 6) ,char-code))) (,writer (logior #b10000000 (ldb (byte 6 0) ,char-code)))))))) (defun string-to-utf-8-bytes (string &key null-terminate) "Convert a string into an array of unsigned bytes containing its utf-8 representation." (declare (type string string) #.*optimize*) (let ((buffer (make-array (+ (the fixnum (utf-8-byte-length string)) (if null-terminate 1 0)) :element-type '(unsigned-byte 8))) (position 0) (string (coerce string 'simple-string))) (declare (type (array (unsigned-byte 8)) buffer) (type fixnum position)) (macrolet ((add-byte (byte) `(progn (setf (aref buffer position) ,byte) (incf position)))) (loop :for char :across string :do (as-utf-8-bytes char add-byte))) (when null-terminate (setf (elt buffer (1- (length buffer))) 0)) buffer)) (defun write-utf-8-bytes (string output &key null-terminate) "Write a string to a byte-stream, encoding it as utf-8." (declare (type string string) (type stream output) #.*optimize*) (macrolet ((byte-out (byte) `(write-byte ,byte output))) (let ((string (coerce string 'simple-string))) (loop :for char :across string :do (as-utf-8-bytes char byte-out)))) (when null-terminate (write-byte 0 output))) (define-condition utf-8-decoding-error (simple-error) ((message :initarg :message) (byte :initarg :byte :initform nil)) (:report (lambda (err stream) (format stream (slot-value err 'message) (slot-value err 'byte))))) (declaim (inline utf-8-group-size)) (defun utf-8-group-size (byte) "Determine the amount of bytes that are part of the character starting with a given byte." (declare (type fixnum byte) #.*optimize*) (cond ((zerop (logand byte #b10000000)) 1) ((= (logand byte #b11100000) #b11000000) 2) ((= (logand byte #b11110000) #b11100000) 3) ((= (logand byte #b11111000) #b11110000) 4) (t (error 'utf-8-decoding-error :byte byte :message "Invalid byte at start of character: 0x~X")))) (defun utf-8-string-length (bytes &key (start 0) (end (length bytes))) "Calculate the length of the string encoded by the given bytes." (declare (type (simple-array (unsigned-byte 8) (*)) bytes) (type fixnum start end) #.*optimize*) (loop :with i :of-type fixnum = start :with string-length = 0 :while (< i end) :do (progn (incf (the fixnum string-length) 1) (incf i (utf-8-group-size (elt bytes i)))) :finally (return string-length))) (defun get-utf-8-character (bytes group-size &optional (start 0)) "Given an array of bytes and the amount of bytes to use, extract the character starting at the given start position." (declare (type (simple-array (unsigned-byte 8) (*)) bytes) (type fixnum group-size start) #.*optimize*) (labels ((next-byte () (prog1 (elt bytes start) (incf start))) (six-bits (byte) (unless (= (logand byte #b11000000) #b10000000) (error 'utf-8-decoding-error :byte byte :message "Invalid byte 0x~X inside a character.")) (ldb (byte 6 0) byte))) (case group-size (1 (next-byte)) (2 (logior (ash (ldb (byte 5 0) (next-byte)) 6) (six-bits (next-byte)))) (3 (logior (ash (ldb (byte 4 0) (next-byte)) 12) (ash (six-bits (next-byte)) 6) (six-bits (next-byte)))) (4 (logior (ash (ldb (byte 3 0) (next-byte)) 18) (ash (six-bits (next-byte)) 12) (ash (six-bits (next-byte)) 6) (six-bits (next-byte))))))) (defun utf-8-bytes-to-string (bytes-in &key (start 0) (end (length bytes-in))) "Convert a byte array containing utf-8 encoded characters into the string it encodes." (declare (type vector bytes-in) (type fixnum start end) #.*optimize*) (loop :with bytes = (coerce bytes-in '(simple-array (unsigned-byte 8) (*))) :with buffer = (make-string (utf-8-string-length bytes :start start :end end) :element-type 'character) :with array-position :of-type fixnum = start :with string-position :of-type fixnum = 0 :while (< array-position end) :do (let* ((char (elt bytes array-position)) (current-group (utf-8-group-size char))) (when (> (+ current-group array-position) end) (error 'utf-8-decoding-error :message "Unfinished character at end of byte array.")) (setf (char buffer string-position) (code-char (get-utf-8-character bytes current-group array-position))) (incf string-position 1) (incf array-position current-group)) :finally (return buffer))) (defun read-utf-8-string (input &key null-terminated stop-at-eof (char-length -1) (byte-length -1)) "Read utf-8 encoded data from a byte stream and construct a string with the characters found. When null-terminated is given it will stop reading at a null character, stop-at-eof tells it to stop at the end of file without raising an error, and the char-length and byte-length parameters can be used to specify the max amount of characters or bytes to read." (declare (type stream input) (type fixnum byte-length char-length) #.*optimize*) (let ((buffer (make-array 4 :element-type '(unsigned-byte 8))) (bytes-read 0) (string (make-array 64 :element-type 'character :adjustable t :fill-pointer 0))) (declare (type fixnum bytes-read)) (loop (when (or (and (/= -1 byte-length) (>= bytes-read byte-length)) (and (/= -1 char-length) (= char-length (length string)))) (return)) (let ((next-char (read-byte input (not stop-at-eof) :eof))) (when (or (eq next-char :eof) (and null-terminated (eq next-char 0))) (return)) (let ((current-group (utf-8-group-size next-char))) (incf bytes-read current-group) (cond ((= current-group 1) (vector-push-extend (code-char next-char) string)) (t (setf (elt buffer 0) next-char) (loop :for i :from 1 :below current-group :for next-char = (read-byte input nil :eof) :do (when (eq next-char :eof) (error 'utf-8-decoding-error :message "Unfinished character at end of input.")) :do (setf (elt buffer i) next-char)) (vector-push-extend (code-char (get-utf-8-character buffer current-group)) string)))))) string)) postmodern-20151031-git/doc/000077500000000000000000000000001260073634400154755ustar00rootroot00000000000000postmodern-20151031-git/doc/cl-postgres.html000066400000000000000000000710001260073634400206230ustar00rootroot00000000000000 CL-postgres reference manual

CL-postgres reference manual

The CL-postgres module implements a rather low-level interface for communicating with a PostgreSQL database server. It is part of the Postmodern library, but can be used separately.

Contents

  1. Connecting
  2. Querying
  3. Reading values
  4. Row readers
  5. Bulk Copying
  6. Conditions
  7. Symbol-index

Connecting

class database-connection

Objects of this type represent database connections.

function open-database (database user password host &optional (port 5432) (use-ssl :no))
→ database-connection

Create and open a connection for the specified server, database, and user. use-ssl may be :no, :yes, or :try, where :try means 'if the server supports it'. When it is anything but :no, you must have the CL+SSL package loaded to initiate the connection.

On SBCL and Clozure CL, the value :unix may be passed for host, in order to connect using a Unix domain socket instead of a TCP socket.

function close-database (database-connection)

Close a database connection. It is advisable to call this on connections when you are done with them. Otherwise the open socket will stick around until it is garbage collected, and no one will tell the database server that we are done with it.

function reopen-database (database-connection)

Re-establish a database connection for a previously closed connection object. (Calling this on a connection that is still open is harmless.)

function database-open-p (database-connection)
→ boolean

Test whether a database connection is still open.

method connection-meta (database-connection)
→ hash-table

This method provides access to a hash table that is associated with the current database connection. When the connection is closed and re-opened this hash table is reset. The most obvious use for this is for storing information about the prepared statements that have been parsed for this connection.

method connection-parameters (database-connection)
→ hash-table

This method returns a mapping (string to string) containing all the configuration parameters for the connection.

variable *unix-socket-dir*

On SBCL, when using the :unix keyword as host argument when creating a connection, this variable determines the directory in which CL-Postgres will look for the socket file.

variable *ssl-certificate-file*
variable *ssl-key-file*

When using SSL (see open-database), these can be used to provide client key and certificate files. They can be either NIL, for no file, or a pathname.

function wait-for-notification (database-connection)

This function blocks until a notification is received on the connection. The PostgreSQL LISTEN command must be used to enable listening for notifications.

Querying

function exec-query (database-connection query &optional (row-reader 'ignore-row-reader))
→ result

Sends the given query to the given connection, and interprets the results (if there are any) with the given row-reader. If the database returns information about the amount of rows affected, this is returned as a second value.

function prepare-query (database-connection name query)

Parse and plan the given query, and store it under the given name. Note that prepared statements are per-connection, so they can only be executed through the same connection that prepared them.

function exec-prepared (database-connection name parameters &optional (row-reader 'ignore-row-reader))
→ result

Execute the prepared statement by the given name. Parameters should be given as a list. Each value in this list should be of a type that to-sql-string has been specialised on. (Byte arrays will be passed in their binary form, without being put through to-sql-string.) The result of the executing the statement, if any, is interpreted by the given row reader, and returned. Again, the number or affected rows is optionally returned as a second value.

function unprepare-query (database-connection name)

Close the prepared statement by the given name. This will free resources and allow the name to be associated with a new prepared query.

method to-sql-string (value)
→ (values string needs-escaping)

Convert a Lisp value to its textual unescaped SQL representation. Returns a second value indicating whether this value should be escaped if it is to be put directly into a query.

You can define to-sql-string methods for your own datatypes if you want to be able to pass them to exec-prepared. When a non-NIL second value is returned, this may be T to indicate that the first value should simply be escaped as a string, or a second string providing a type prefix for the value. (This is used by S-SQL.)

variable *silently-truncate-rationals*

When a rational number is passed into a query (as per to-sql-string), but it can not be expressed within 38 decimal digits (for example 1/3), it will be truncated, and lose some precision. Set this variable to nil to suppress that behaviour and raise an error instead.

variable *query-log*

When debugging, it can be helpful to inspect the queries that are being sent to the database. Set this variable to an output stream value (*standard-output*, for example) to have CL-postgres log every query it makes.

variable *query-callback*

When profiling or debugging, the *query-log* may not give enough information, or reparsing its output may not be feasible. This variable may be set to a designator of function taking two arguments. This function will be then called after every query, and receive query string and internal time units (as in (CL:GET-INTERNAL-REAL-TIME)) spent in query as its arguments.

Default value of this variable is 'LOG-QUERY, which takes care of *QUERY-LOG* processing. If you provide custom query callback and wish to keep *QUERY-LOG* functionality, you will have to call LOG-QUERY from your callback function

function log-query (query internal-time)

This function is default value of *QUERY-CALLBACK* and logs queries to *QUERY-LOG* if it is not NIL.

Reading values

CL-postgres knows how to convert commonly used PostgreSQL data types to Lisp values. This table shows the mapping:

PostgreSQLLisp
smallintinteger
integerinteger
bigintinteger
numericratio
realfloat
double precisiondouble-float
booleanboolean
varcharstring
textstring
bytea(vector (unsigned-byte 8))

The mapping from PostgreSQL types (identified by OID numbers) to the functions that interpret them is kept in so-called SQL readtables. All types for which no reader is defined will be returned as string values containing their PostgreSQL representation.

variable *sql-readtable*

This variable is used to choose the current readtable. For simple use, you will not have to touch this, but it is possible that code within a Lisp image requires different readers in different situations, in which case you can create separate read tables.

function copy-sql-readtable (table)
→ readtable

Copies a given readtable.

function default-sql-readtable ()
→ readtable

Returns the default readtable, containing only the readers defined by CL-postgres itself.

function set-sql-reader (oid function &key table binary-p)

Define a new reader for a given type. table defaults to *sql-readtable*. The reader function should take a single argument, a string, and transform that into some kind of equivalent Lisp value. When binary-p is true, the reader function is supposed to directly read the binary representation of the value. In most cases this is not recommended, but if you want to use it: provide a function that takes a binary input stream and an integer (the size of the value, in bytes), and reads the value from that stream. Note that reading less or more bytes than the given size will horribly break your connection.

function set-sql-datetime-readers (&key date timestamp timestamp-with-timezone time interval table)

Since there is no widely recognised standard way of representing dates and times in Common Lisp, and reading these from string representation is clunky and slow, this function provides a way to easily plug in binary readers for the date, time, timestamp, and interval types. It should be given functions with the following signatures:

:date (days)
Where days is the amount of days since January 1st, 2000.
:timestamp (useconds)
Timestamps have a microsecond resolution. Again, the zero point is the start of the year 2000, UTC.
:timestamp-with-timezone
Like :timestamp, but for values of the 'timestamp with time zone' type (which PostgreSQL internally stores exactly the same as regular timestamps).
:time (useconds)
Refers to a time of day, counting from midnight.
:interval (months days useconds)
An interval is represented as several separate components. The reason that days and microseconds are separated is that you might want to take leap seconds into account.

Row readers

Row readers are a way to read and group the results of queries. Roughly, they are functions that perform the iteration over the rows and cells in the result, and do something with the returned values.

macro row-reader ((fields) &body body)
→ function

Creates a row-reader, using the given name for the variable. Inside the body this variable refers to a vector of field descriptions. On top of that, two local functions are bound, next-row and next-field. The first will start reading the next row in the result, and returns a boolean indicating whether there is another row. The second will read and return one field, and should be passed the corresponding field description from the fields argument as a parameter.

A row reader should take care to iterate over all the rows in a result, and within each row iterate over all the fields. This means it should contain an outer loop that calls next-row, and every time next-row returns T it should iterate over the fields vector and call next-field for every field.

The definition of list-row-reader should give you an idea what a row reader looks like:

(row-reader (fields)
  (loop :while (next-row)
        :collect (loop :for field :across fields
                       :collect (next-field field))))

Obviously, row readers should not do things with the database connection like, say, close it or start a new query, since it still reading out the results from the current query.

macro def-row-reader (name (fields) &body body)

The defun-like variant of row-reader: creates a row reader and gives it a top-level function name.

method field-name (field)
→ string

This can be used to get information about the fields read by a row reader. Given a field description, it returns the name the database associated with this column.

method field-type (field)
→ oid

This extracts the PostgreSQL OID associated with this column. You can, if you really want to, query the pg_types table to find out more about the types denoted by OIDs.

function list-row-reader (socket fields)
→ list

A row reader that builds a list of lists from the query results.

function alist-row-reader (socket fields)
→ alist

A row reader that returns a list of alists, which associate column names with values.

function ignore-row-reader (socket fields)

A row reader that completely ignores the result of a query.

Bulk Copying

When loading large amounts of data into PostgreSQL, it can be done significantly faster using the bulk copying feature. The drawback to this approach is that you don't find out about data integrity errors until the entire batch is completed but sometimes the speed is worth it

function open-db-writer (db table &optional columns)

Opens a table stream into which rows can be written one at a time using db-write-row. db is either a connection object or a list of arguments that could be passed to open-database. table is the name of an existing table into which this writer will write rows. If you don't have data for all columns, use columns to indicate those that you do.

function close-db-writer (writer &key abort)

Closes a bulk writer opened by open-db-writer. Will close the associated database connection when it was created for this copier, or abort is true.

function db-write-row (writer row-data)

Writes row-data into the table and columns referenced by the writer. row-data is a list of Lisp objects, one for each column included when opening the writer. Arrays (the elements of which must all be the same type) will be serialized into their PostgreSQL representation before being written into the DB.

Conditions

Opening or querying a database may raise errors. CL-postgres will wrap the errors that the server returns in a lisp condition, and raise conditions of the same type when it detects some problem itself. Socket errors are let through as they are.

condition database-error

The type of database-related conditions. For errors that you may want to catch by type, the cl-postgres-error package defines a bucket of subtypes used for specific errors. See the cl-postgres/package.lisp file for a list.

method database-error-message (database-error)
→ string

A short message associated with this error.

method database-error-detail (database-error)
→ string

A longer description of the problem, or NIL if none is available.

method database-error-code (database-error)
→ string

The error code PostgreSQL associated with this error, if any. See the PostgreSQL manual for their meaning.

method database-error-query (database-error)
→ string

The query that led to this error, or NIL if no query was involved.

method database-error-cause (database-error)
→ condition

The condition that caused this error, or NIL when it was not caused by another condition.

function database-error-constraint-name (database-error)
→ string

For integrity-violation errors, returns the name of the constraint that was violated (or nil if no constraint was found.)

condition database-connection-error

Subtype of database-error. An error of this type (or one of its subclasses) is signaled when a query is attempted with a connection object that is no longer connected, or a database connection becomes invalid during a query. Always provides a :reconnect restart, which will cause the library to make an attempt to restore the connection and re-try the query.

The following shows an example use of this feature, a way to ensure that the first connection error causes a reconnect attempt, while others pass through as normal. A variation on this theme could continue trying to reconnect, with successively longer pauses.

(defun call-with-single-reconnect (fun)
  (let ((reconnected nil))
    (handler-bind
        ((database-connection-error
          (lambda (err)
            (when (not reconnected)
              (setf reconnected t)
              (invoke-restart :reconnect)))))
      (funcall fun))))

condition postgresql-notification

The condition that is signalled when a notification message is received from the PostgreSQL server. This is a WARNING condition which is caught by the WAIT-FOR-NOTIFICATION function that implements synchronous waiting for notifications.

method postgresql-notification-channel (postgresql-notification)
→ string

The channel string of this notification.

method postgresql-notification-payload (postgresql-notification)
→ string

The payload of this notification.

method postgresql-notification-pid (postgresql-notification)
→ integer

The process ID of the process that sent the notification.

Symbol-index

postmodern-20151031-git/doc/index.html000066400000000000000000000550411260073634400174770ustar00rootroot00000000000000 Postmodern

Postmodern

Postmodern is a Common Lisp library for interacting with PostgreSQL databases. Features are:

The biggest differences between this library and CLSQL/CommonSQL are that Postmodern has no intention of being portable across different SQL implementations (it embraces non-standard PostgreSQL features), and approaches extensions like lispy SQL and database access objects in a quite different way. This library was written because the CLSQL approach did not really work for me, your mileage may vary.

Contents

  1. News
  2. Dependencies
  3. License
  4. Download and installation
  5. Support and mailing lists
  6. Quickstart
  7. Reference
  8. Caveats and to-dos
  9. Resources

News

28-11-2012: Version 1.19: Fix ECL incompatibilities, add upsert-dao function, add support for notifications and bulk copying, and make unix sockets work on CCL.

19-10-2011: Version 1.18: Improve support for array literals, add array slicing. Switch test suite over to Eos. Added make-dao, :dao query selectors, and define-dao-finalization. Support PostGIS operators, locking, and constraint deferring syntax in S-SQL. Add a !unique operator for defining table constraints. Add a database-error-constraint-name condition object accessor.

02-02-2011: Version 1.17: Another modest maintenance release. Fixes day-of-week in simple-date. Makes the :plist query format actually work. Adds sql-escape as an exported symbol for client code that needs to escape stuff. Adds support for multi-dimensional arrays. Adds the *ignore-unknown-columns* configuration variable.

02-04-2010: Version 1.16: Introduces a save-dao/transaction, because the old semantics were broken (originally inside of transactions, after fixing that outside of them). Add support for passing vectors as argument to prepared queries, and reading them from query results. Add :on-update and :on-delete arguments to !foreign. Add :plist and :plists query result formats. Guarantee that deftable definitions are executed in the order they were defined in. Moves the ieee-floats and trivial-utf-8 dependencies into the repository, so they don't have to separately fetched.

02-04-2010: We are moving from common-lisp.net to marijnhaverbeke.nl, and from darcs to git. New project home: http://marijnhaverbeke.nl/postmodern.

01-03-2009: Version 1.15: Almost a year since the last release. Postmodern is still alive—it is just so perfect it hardly needs updates anymore. In this release: Stop depending on the usocket library in Allegro CL and SBCL, just use the built-in socket bindings instead. Allow connecting over a Unix socket in SBCL. Support natural joins in :select. Add :if-exists argument to :drop-... operators, add support for :add-column form to :alter-table. Add :between-symmetric operator. Introduce "ghost slot" hack to DAO system to support fetching OIDs. Extend doquery to also handle parameterised queries.

07-03-2009: Version 1.14: Some more syntax supported in S-SQL, add *silently-truncate-rationals* and *query-callback*, export dao-keys, and fix some small bugs.

28-08-2008: Version 1.13: A few small fixes and optimisations, and there is once again a deftable ― though it has a different role than it used to have.

30-04-2008: Version 1.12: Restores the save-dao function (though you should still be careful with it), adds support for SSL connections, makes some error messages clearer, adds some S-SQL operators, and fixes a few bugs.

09-04-2008: Version 1.11: Fixes bugs, most importantly one that caused CL-postgres to blow up whenever a connection parameter was changed for an open connection. Also adds automatic escaping of reserved words to S-SQL, a *max-pool-size* parameter and a coalesce function to Postmodern

19-03-2008: Version 1.10: Note that this release is not entirely backwards-compatible. It introduces a new, more flexible approach to DAO objects, a more pluggable handling of custom data types, and a bunch of small clean-ups, optimizations, and enhancements. See the migration guide for details.

Dependencies

The library depends on usocket (except on SBCL and ACL, where the built-in socket library is used), md5, closer-mop, bordeaux-threads if you want thread-safe connection pools, and CL+SSL when SSL connections are needed.

Postmodern itself is split into four different packages, some of which can be used independently. Simple-date is a very basic implementation of date and time objects, used to support storing and retrieving time-related SQL types. CL-postgres is the low-level library used for interfacing with a PostgreSQL server over a socket. S-SQL is used to compile s-expressions to strings of SQL code, escaping any Lisp values inside, and doing as much as possible of the work at compile time. Finally, Postmodern itself is the library that tries to put all these things together into a convenient programming interface.

License

Postmodern is released under a zlib-style license. Which approximately means you can use the code in whatever way you like, except for passing it off as your own or releasing a modified version without indication that it is not the original.

Download and installation

The latest release of Postmodern can be downloaded from http://marijnhaverbeke.nl/postmodern/postmodern.tgz, or installed with asdf-install.

A git repository with the most recent changes can be checked out with:

> git clone http://marijnhaverbeke.nl/git/postmodern

You can also view the repository on github.

The file http://marijnhaverbeke.nl/postmodern/postmodern-latest.tgz always contains a snapshot of the current repository head.

Support and mailing lists

The postmodern-devel can be used for questions, discussion, bug-reports, patches, or anything else relating to this library. To subscribe, send a message to postmodern-devel+subscribe@common-lisp.net. Or mail the author/maintainer directly: Marijn Haverbeke.

Quickstart

This quickstart is intended to give you a feel of the way coding with Postmodern works. Further details about the workings of the library can be found in the reference manual.

Assuming you have already installed it, first load and use the system:

(asdf:oos 'asdf:load-op :postmodern)
(use-package :postmodern)

If you have a PostgreSQL server running on localhost, with a database called 'testdb' on it, which is accessible for user 'foucault' with password 'surveiller', you can connect like this:

(connect-toplevel "testdb" "foucault" "surveiller" "localhost")

Which will establish a connection to be used by all code, except for that wrapped in a with-connection form, which takes the same arguments but only establishes the connection locally.

Now for a basic sanity test:

(query "select 22, 'Folie et déraison', 4.5")
;; => ((22 "Folie et déraison" 9/2))

That should work. query is the basic way to send queries to the database. The same query can be expressed like this:

(query (:select 22 "Folie et déraison" 4.5))
;; => ((22 "Folie et déraison" 9/2))

In many contexts, query strings and lists starting with keywords can be used interchangeably. The lists will be compiled to SQL. The S-SQL manual describes the syntax used by these expressions. Lisp values occurring in them are automatically escaped. In the above query, only constant values are used, but it is possible to transparently use run-time values as well:

(defun database-powered-addition (a b)
  (query (:select (:+ a b)) :single))
(database-powered-addition 1030 204)
;; => 1234

That last argument, :single, indicates that we want the result not as a list of lists (for the result rows), but as a single value, since we know that we are only selecting one value. Some other options are :rows, :row, :column, :alists, and :none. Their precise effect is documented in the reference manual.

You do not have to pull in the whole result of a query at once, you can also iterate over it with the doquery macro:

(doquery (:select 'x 'y :from 'some-imaginary-table) (x y)
  (format t "On this row, x = ~A and y = ~A.~%" x y))

This is what a database-access class looks like:

(defclass country ()
  ((name :col-type string :initarg :name
         :reader country-name)
   (inhabitants :col-type integer :initarg :inhabitants
                :accessor country-inhabitants)
   (sovereign :col-type (or db-null string) :initarg :sovereign
              :accessor country-sovereign))
  (:metaclass dao-class)
  (:keys name))

The above defines a class that can be used to handle records in a table with three columns: name, inhabitants, and sovereign. In simple cases, the information above is enough to define the table as well:

(dao-table-definition 'country)
;; => "CREATE TABLE country (
;;      name TEXT NOT NULL,
;;      inhabitants INTEGER NOT NULL,
;;      sovereign TEXT,
;;      PRIMARY KEY (name))"
(execute (dao-table-definition 'country))

This defines our table in the database. execute works like query, but does not expect any results back.

Let us add a few countries:

(insert-dao (make-instance 'country :name "The Netherlands"
                                    :inhabitants 16800000
                                    :sovereign "Willem-Alexander"))
(insert-dao (make-instance 'country :name "Croatia"
                                    :inhabitants 4400000))

Then, to update Croatia's population, we could do this:

(let ((croatia (get-dao 'country "Croatia")))
  (setf (country-inhabitants croatia) 4500000)
  (update-dao croatia))
(query (:select '* :from 'country))
;; => (("The Netherlands" 16800000 "Willem-Alexander")
;;     ("Croatia" 4500000 :NULL))

Next, to demonstrate a bit more of the S-SQL syntax, here is the query the utility function list-tables uses to get a list of the tables in a database:

(sql (:select 'relname :from 'pg-catalog.pg-class
      :inner-join 'pg-catalog.pg-namespace :on (:= 'relnamespace 'pg-namespace.oid)
      :where (:and (:= 'relkind "r")
                   (:not-in 'nspname (:set "pg_catalog" "pg_toast"))
                   (:pg-catalog.pg-table-is-visible 'pg-class.oid))))
;; => "(SELECT relname FROM pg_catalog.pg_class
;;      INNER JOIN pg_catalog.pg_namespace ON (relnamespace = pg_namespace.oid)
;;      WHERE ((relkind = 'r') and (nspname NOT IN ('pg_catalog', 'pg_toast'))
;;             and pg_catalog.pg_table_is_visible(pg_class.oid)))"

sql is a macro that will simply compile a query, it can be useful for seeing how your queries are expanded or if you want to do something unexpected with them.

As you can see, lists starting with keywords are used to express SQL commands and operators (lists starting with something else will be evaluated and then inserted into the query). Quoted symbols name columns or tables (keywords can also be used but might introduce ambiguities). The syntax supports subqueries, multiple joins, stored procedures, etc. See the S-SQL reference manual for a complete treatment.

Finally, here is an example of the use of prepared statements:

(defprepared sovereign-of
  (:select 'sovereign :from 'country :where (:= 'name '$1))
  :single!)
(sovereign-of "The Netherlands")
;; => "Willem-Alexander"

The defprepared macro creates a function that takes the same amount of arguments as there are $X placeholders in the given query. The query will only be parsed and planned once (per database connection), which can be faster, especially for complex queries.

(disconnect-toplevel)

Reference

The reference manuals for the different components of Postmodern are kept in separate files. For using the library in the most straightforward way, you only really need to read the Postmodern reference and glance over the S-SQL reference. The simple-date reference explains the time-related data types included in Postmodern, and the CL-postgres reference might be useful if you just want a low-level library for talking to a PostgreSQL server.

Caveats and to-dos

Timezones

Simple-date has no concept of time zones. This means that using it is rather error-prone, and if you really need your time-keeping to be reliable and/or universal you should either not use the types it provides or think really hard about the way you handle time zones.

Recently, a lot of work has been done on local-time, which solves the same problem as simple-date, but does understand time zones. The 1.0 repository currently has code for integration with CL-postgres, though this might not be stable yet.

Portability

The Lisp code in Postmodern is theoretically portable across implementations, and seems to work on all major ones. I am not actively testing against new releases or obscure implementations, but if you run into problems you are welcome to contact me through the mailing list, and we can try to solve them. Implementations that do not have meta-object protocol support will not have DAOs, but all other parts of the library should work (all widely used implementations do support this).

The library will definitely not work for PostgreSQL versions older than 7.4 (it uses a client/server protocol that was introduced in that version). On versions prior to 8.1, retrieving date and time objects is broken, because their binary representation was changed. Part of the functionality of insert-dao (automatic defaulting of unbound slots) only works in PostgreSQL 8.2 and up.

Things that should be implemented

It would be a nice feature if Postmodern could help you with defining your database schemas and, more importantly, updating your databases when your code changes. It would theoretically not be hard to build a function that compares a schema on the Lisp side with the state of the database, and helps you to interactively update your database. PostgreSQL has a quite complete introspection system. Unfortunately it would be a lot of work to implement this, since databases can contain so many different types of entities (tables, views, indices, procedures, constraints, sequences, etc.) which are all created, changed, and dropped in different ways.

Resources

postmodern-20151031-git/doc/migrating-to-1.10.html000066400000000000000000000164351260073634400213520ustar00rootroot00000000000000 Migrating to Postmodern 1.10

Migrating to Postmodern 1.10

A number of backwards-incompatible changes are introduced in this version of the library ― there are a few small cleanups, and the database-access object system has been completely overhauled.

with-connection

The old with-connection form has been replaced by what used to be called with-connection*. This means that if you had code like this...

(with-connection ("my-db" "harry" "****" "localhost") ...)

... you should change it to ...

(with-connection (list "my-db" "harry" "****" "localhost") ...)

... since the whole list is now evaluated. Similarly, if you were using with-connection*, you should remove the asterisk.

Integrating custom data types

CL-postgres now exports ways to manipulate the way it reads values from query results. The old s-sql:sql-ize generic has been moved to cl-posgres:to-sql-string, and can be used to control the way values are written out when passed as arguments to prepared statements or inserted in S-SQL query forms.

simple-date is no longer depended on by CL-postgres and S-SQL, but uses the above interface to integrate itself. Load it after loading CL-postgres, and suitable readers and writers for its types will be registered. Integrating other date/time libraries is trivial.

Errors and reconnecting

In previous versions, only the database-connection-lost conditions offered a :reconnect restart. There are now various conditions offering this restart, all subtypes of database-connection-error, and the library tries its very best to wrap all hard-to-prevent errors with such a restart (socket errors, database shutdowns). The goal is that you can use this feature to cleanly and simply add functionality for recovering from connectivity problems and server restarts. If you still have issues here, please discuss them on the mailing list (universal error recovery is rather hard to test).

There is now also a large set of condition types exported from the cl-postgres-error package, which can make writing handler-case forms around database code a lot more pleasant. See cl-postgres/error.lisp for the list (or just cause the error you are interested in to be raised, and look at its type).

The DAO system

This is where upgrading might be somewhat painful. The old deftable macro has been dropped completely, in favour of the dao-class metaclass. The focus of this part of the library has shifted from defining tables to defining access objects. You can still generate simple CREATE TABLE statements using the dao-table-definition function, but this is intended to just be a shortcut. Table definition is now the responsibility of the library user, not the library.

So why this regression in functionality? It turned out that coupling access objects and table definitions like this was not such a good idea. You might want to create access objects for views, or for tables with all kinds of complicated constraints. Adding support for this to deftable would have turned it into an even bigger behemoth than it already was, and not fundamentally solve the problem.

So now we have a nice, clean DAO interface, and no schema-definition interface at all (create-template and friends were also dropped). The most notable change is probably that the :auto-id option is gone. This was very convenient but horribly 'magical'. If you had something like this:

(deftable product ()
  ((name :type string :initarg :name :accessor product-name)
   (weight :type float :initarg :weight :accessor product-weight))
  (:class-name product)
  (:auto-id t)
  (:indices (:unique name)))

(defun create-tables ()
  ; ...
  (create-table 'product))

The equivalent could look like this:

(defclass product ()
  ((id :col-type serial :initarg :id :accessor product-id)
   (name :col-type string :initarg :name :accessor product-name)
   (weight :col-type float :initarg :weight :accessor product-weight))
  (:keys id)
  (:metaclass dao-class))

(defun create-tables ()
  ; ...
  (execute (dao-table-definition 'product))
  (execute (:create-unique-index 'product-name :on 'product :fields 'name)))

Or you could explicitly create the id sequence and give the id field a :col-default of (:nextval "product_ids"), to have more control over the id generation.

The above example should give you a basic idea of the new interface: DAO classes are now created by regular class definitions. Instead of :type options, column slots should get :column or :col-type options. The semantics of creating and inserting DAOs have been slightly adjusted: There is no magic happening when you create a DAO instance (it used to fetch id values), except when you give make-instance a :fetch-defaults keyword argument, in which case it will query the database for the rows' default values, and put them in the instance. Usually, it is cleaner to not use this, since it generates extra queries and does not work for stuff like serial fields anyway, where no proper :col-default can be given. When an object is inserted into the database with insert-dao, some slots may be unbound. These will then, both in the database and in the object, be assigned values based on the column defaults. For example, if you have the above product class:

(defvar *p* (make-instance 'product :name "brick" :weight 2))
;; The id slot is unbound
(insert-dao *p*)
(print (product-id *p*))
;; Here it will have received a new id value

Note that this works even for serial types, since the defaults are fetched by giving the INSERT statement a RETURNING clause, so the association between default values and columns is handled by the database, not the DAO class.

postmodern-20151031-git/doc/postmodern.html000066400000000000000000001427751260073634400205750ustar00rootroot00000000000000 Postmodern reference manual

Postmodern reference manual

This is the reference manual for the component named postmodern, which is part of a library of the same name.

Note that this package also exports the database-connection and database-error types from CL-postgres and a few operators from S-SQL.

query, execute, and any other function that would logically need to communicate with the database will raise a condition of the type database-error when something goes wrong. As a special case, errors that break the connection (socket errors, database shutdowns) will be raised as subtypes of database-connection-error, providing a :reconnect restart to re-try the operation that encountered to the error.

Contents

  1. Connecting
  2. Querying
  3. Inspecting the database
  4. Database access objects
  5. Table definition and creation
  6. Schemata
  7. Symbol-index

Connecting

class database-connection

Objects of this type represent database connections.

function connect (database user password host &key (port 5432) pooled-p use-ssl)
→ database-connection

Create a new database connection for the given user and the database. Port will default to 5432, which is where most PostgreSQL servers are running. If pooled-p is T, a connection will be taken from a pool of connections of this type, if one is available there, and when the connection is disconnected it will be put back into this pool instead. use-ssl can be :no, :yes, or :try, as in open-database, and defaults to the value of *default-use-ssl*.

variable *default-use-ssl*

The default for connect's use-ssl argument. This starts at :no. If you set it to anything else, be sure to also load the CL+SSL library.

method disconnect (database-connection)

Disconnects a normal database connection, or moves a pooled connection into the pool.

function connected-p (database-connection)
→ boolean

Returns a boolean indicating whether the given connection is still connected to the server.

method reconnect (database-connection)

Reconnect a disconnected database connection. This is not allowed for pooled connections ― after they are disconnected they might be in use by some other process, and should no longer be used.

variable *database*

Special variable holding the current database. Most functions and macros operating on a database assume this binds to a connected database.

macro with-connection (spec &body body)

Evaluates the body with *database* bound to a connection as specified by spec, which should be list that connect can be applied to.

macro call-with-connection (spec thunk)

The functional backend to with-connection. Binds *database* to a new connection as specified by spec, which should be a list that connect can be applied to, and runs the zero-argument function given as second argument in the new environment. When the function returns or throws, the new connection is disconnected.

function connect-toplevel (database user password host &key (port 5432))

Bind the *database* to a new connection. Use this if you only need one connection, or if you want a connection for debugging from the REPL.

function disconnect-toplevel ()

Disconnect the *database*.

function clear-connection-pool ()

Disconnect and remove all connections from the connection pools.

variable *max-pool-size*

Set the maximum amount of connections kept in a single connection pool, where a pool consists of all the stored connections with the exact same connect arguments. Defaults to NIL, which means there is no maximum.

Querying

macro query (query &rest args/format)
→ result

Execute the given query, which can be either a string or an S-SQL form (list starting with a keyword). If the query contains placeholders ($1, $2, etc) their values can be given as extra arguments. If one of these arguments is a keyword occurring in the table below, it will not be used as a query argument, but will determine the format in which the results are returned instead. Any of the following formats can be used, with the default being :rows:

:noneIgnore the result values.
:lists, :rowsReturn a list of lists, each list containing the values for a row.
:list, :rowReturn a single row as a list.
:alistsReturn a list of alists which map column names to values, with the names represented as keywords.
:alistReturn a single row as an alist.
:str-alistsLike :alists, but use the original column names.
:str-alistReturn a single row as an alist, with strings for names.
:plistsReturn a list of plists which map column names to values,with the names represented as keywords.
:plistReturn a single row as a plist.
:columnReturn a single column as a list.
:singleReturn a single value.
:single!Like :single, but raise an error when the number of selected rows is not equal to 1.
(:dao type)Return a list of DAOs of the given type. The names of the fields returned by the query must match slots in the DAO class the same way as with query-dao.
(:dao type :single)Return a single DAO of the given type.

If the database returns information about the amount rows that were affected, such as with updating or deleting queries, this is returned as a second value.

macro execute (query &rest args)

Like query called with format :none. Returns the amount of affected rows as its first returned value. (Also returns this amount as the second returned value, but use of this is deprecated.)

macro doquery (query (&rest names) &body body)

Execute the given query (a string or a list starting with a keyword), iterating over the rows in the result. The body will be executed with the values in the row bound to the symbols given in names. To iterate over a parameterised query, one can specify a list whose car is the query, and whose cdr contains the arguments. For example:

(doquery (:select 'name 'score :from 'scores) (n s)
  (incf (gethash n *scores*) s))

(doquery ((:select 'name :from 'scores :where (:> 'score '$1)) 100) (name)
  (print name))

macro prepare (query &optional (format :rows))
→ function

Creates a function that can be used as the interface to a prepared statement. The given query (either a string or an S-SQL form) may contain placeholders, which look like $1, $2, etc. The resulting function takes one argument for every placeholder in the query, executes the prepared query, and returns the result in the format specified. (Allowed formats are the same as for query.)

For queries that have to be run very often, especially when they are complex, it may help performance since the server only has to plan them once. See the PostgreSQL manual for details.

In some cases, the server will complain about not being able to deduce the type of the arguments in a statement. In that case you should add type declarations (either with the PostgreSQL's CAST SQL-conforming syntax or historical :: syntax, or with S-SQL's :type construct) to help it out.

macro defprepared (name query &optional (format :rows))

This is the defun-style variant of prepare. It will define a top-level function for the prepared statement.

macro defprepared-with-names (name (&rest args) (query &rest query-args) &optional (format :rows))

Like defprepared, but allows to specify names of the function arguments as well as arguments supplied to the query.

(defprepared-with-names user-messages (user &key (limit 10))
  ("select * from messages
    where user_id = $1
    order by date desc
    limit $2" (user-id user) limit)
  :plists)
    

macro with-transaction ((&optional name) &body body)

Execute the given body within a database transaction, committing it when the body exits normally, and aborting otherwise. An optional name can be given to the transaction, which can be used to force a commit or abort before the body unwinds.

function commit-transaction (transaction)

Commit the given transaction.

function abort-transaction (transaction)

Roll back the given transaction.

macro with-savepoint (name &body body)

Can only be used within a transaction. Establishes a savepoint with the given name at the start of body, and binds the same name to a handle for that savepoint. At the end of body, the savepoint is released, unless a condition is thrown, in which case it is rolled back.

function release-savepoint (savepoint)

Release the given savepoint.

function rollback-savepoint (savepoint)

Roll back the given savepoint.

function commit-hooks (transaction-or-savepoint), setf (commit-hooks transaction-or-savepoint)

An accessor for the transaction or savepoint's list of commit hooks, each of which should be a function with no required arguments. These functions will be executed when a transaction is committed or a savepoint released.

function abort-hooks (transaction-or-savepoint), setf (abort-hooks transaction-or-savepoint)

An accessor for the transaction or savepoint's list of abort hooks, each of which should be a function with no required arguments. These functions will be executed when a transaction is aborted or a savepoint rolled back (whether via a non-local transfer of control or explicitly by either abort-transaction or rollback-savepoint).

macro with-logical-transaction ((&optional name) &body body)

Executes body within a with-transaction form if no transaction is currently in progress, otherwise simulates a nested transaction by executing it within a with-savepoint form. The transaction or savepoint is bound to name if one is supplied.

function abort-logical-transaction (transaction-or-savepoint)

Roll back the given logical transaction, regardless of whether it is an actual transaction or a savepoint.

function commit-logical-transaction (transaction-or-savepoint)

Commit the given logical transaction, regardless of whether it is an actual transaction or a savepoint.

variable *current-logical-transaction*

This is bound to the current transaction-handle or savepoint-handle instance representing the innermost open logical transaction.

macro ensure-transaction (&body body)

Ensures that body is executed within a transaction, but does not begin a new transaction if one is already in progress.

macro with-schema ((namespace &key :strict t :if-not-exist :create :drop-after) &body body)

Sets the current schema to namespace and executes the body. Before executing body the PostgreSQL's session variable search_path is set to the given namespace. After executing body the search_path variable is restored to the original value. If the keyword :strict is set to T then the namespace is only the scheme on the search path upon the body execution. Otherwise the namespace is just first schema on the search path upon the the body execution. If :if-not-exist is NIL, an error is signaled. If :drop-after is T the namespace is dropped from the database after the body execution.

function sequence-next (sequence)
→ integer

Get the next value from a sequence. The sequence identifier can be either a string or a symbol, in the latter case it will be converted to a string according to S-SQL rules.

function coalesce (&rest arguments)
→ value

Returns the first non-NIL, non-NULL (as in :null) argument, or NIL if none are present. Useful for providing a fall-back value for the result of a query, or, when given only one argument, for transforming :nulls to NIL.

Inspecting the database

function list-tables (&optional strings-p)
→ list

Returns a list of the tables in the current database. When strings-p is T, the names will be given as strings, otherwise as keywords.

function table-exists-p (name)
→ boolean

Tests whether a table with the given name exists. The name can be either a string or a symbol.

function table-description (name &optional schema-name)
→ list

Returns a list of the fields in the named table. Each field is represented by a list of three elements: the field name, the type, and a boolean indicating whether the field may be NULL. Optionally, schema-name can be specified to restrict the result to fields from the named schema. Without it, all fields in the table are returned, regardless of their schema.

function list-sequences (&optional strings-p)
→ list

Returns a list of the sequences in the current database. When strings-p is T, the names will be given as strings, otherwise as keywords.

function sequence-exists-p (name)
→ boolean

Tests whether a sequence with the given name exists. The name can be either a string or a symbol.

function list-views (&optional strings-p)
→ list

Returns list of the user defined views in the current database. When strings-p is T, the names will be returned as strings, otherwise as keywords.

function view-exists-p (name)
→ boolean

Tests whether a view with the given name exists. The name can be either a string or a symbol.

function list-schemata ()
→ list

Returns list of the user defined schemata (as strings) and the quantity of existing schemata.

function schema-exist-p (schema)
→ boolean

Tests the existence of a given schema. Returns T if the schema exists or NIL otherwise.

Database access objects

Postmodern contains a simple system for defining CLOS classes that represent rows in the database. This is not intended as a full-fledged object-relational magic system ― while serious ORM systems have their place, they are notoriously hard to get right, and are outside of the scope of a humble SQL library like this.

metaclass dao-class

At the heart of Postmodern's DAO system is the dao-class metaclass. It allows you to define classes for your database-access objects as regular CLOS classes. Some of the slots in these classes will refer to columns in the database. To specify that a slot refers to a column, give it a :col-type option containing an S-SQL type expression (useful if you want to be able to derive a table definition from the class definition), or simply a :column option with value T. Such slots can also take a :col-default option, used to provide a database-side default value as an S-SQL expression. You can use the :col-name initarg (whose unevaluated value will be passed to to-sql-name) to specify the slot's column's name.

DAO class definitions support two extra class options: :table-name to give the name of the table that the class refers to (defaults to the class name), and :keys to provide a set of primary keys for the table. When no primary keys are defined, operations such as update-dao and get-dao will not work.

Simple example:

(defclass user ()
  ((name :col-type string :initarg :name :accessor user-name)
   (creditcard :col-type (or db-null integer) :initarg :card :col-default :null)
   (score :col-type bigint :col-default 0 :accessor user-score))
  (:metaclass dao-class)
  (:keys name))

The (or db-null integer) form is used to indicate a column can have NULL values.

When inheriting from DAO classes, a subclass' set of columns also contains all the columns of its superclasses. The primary key for such a class is the union of its own keys and all the keys from its superclasses. Classes inheriting from DAO classes should probably always use the dao-class metaclass themselves.

When a DAO is created with make-instance, the :fetch-defaults keyword argument can be passed, which, when T, will cause a query to fetch the default values for all slots that refers to columns with defaults and were not bound through initargs. In some cases, such as serial columns, which have an implicit default, this will not work. You can work around this by creating your own sequence, e.g. "my_sequence", and defining a (:nextval "my_sequence") default.

Finally, DAO class slots can have an option :ghost t to specify them as ghost slots. These are selected when retrieving instances, but not written when updating or inserting, or even included in the table definition. The only known use for this to date is for creating the table with (oids=true), and specify a slot like this:

(oid :col-type integer :ghost t :accessor get-oid)

method dao-keys (class)
→ list

Returns list of slot names that are the primary key of DAO class.

method dao-keys (dao)
→ list

Returns list of values that are the primary key of dao.

method dao-exists-p (dao)
→ boolean

Test whether a row with the same primary key as the given dao exists in the database. Will also return NIL when any of the key slots in the object are unbound.

method make-dao (type &rest args &key &allow-other-keys)
→ dao

Combines make-instance with insert-dao. Return the created dao.

macro define-dao-finalization (((dao-name class) &rest keyword-args) &body body)

Create an :around-method for make-dao. The body is executed in a lexical environment where dao-name is bound to a freshly created and inserted DAO. The representation of the DAO in the database is then updated to reflect changes that body might have introduced. Useful for processing values of slots with the type serial, which are unknown before insert-dao.

method get-dao (type &rest keys)
→ dao

Select the DAO object from the row that has the given primary key values, or NIL if no such row exists. Objects created by this function will have initialize-instance called on them (after loading in the values from the database) without any arguments ― even :default-initargs are skipped. The same goes for select-dao and query-dao.

macro select-dao (type &optional (test t) &rest sort)
→ list

Select DAO objects for the rows in the associated table for which the given test (either an S-SQL expression or a string) holds. When sorting arguments are given, which can also be S-SQL forms or strings, these are used to sort the result. (Note that, if you want to sort, you have to pass the test argument.)

(select-dao 'user (:> 'score 10000) 'name)

macro do-select-dao (((type type-var) &optional (test t) &rest sort) &body body)

Like select-dao, but iterates over the results rather than returning them. For each matching DAO, body is evaluated with type-var bound to the DAO instance.

(do-select-dao (('user user) (:> 'score 10000) 'name)
  (pushnew user high-scorers))

function query-dao (type query &rest args)
→ list

Execute the given query (which can be either a string or an S-SQL expression) and return the result as DAOs of the given type. If the query contains placeholders ($1, $2, etc) their values can be given as extra arguments. The names of the fields returned by the query must either match slots in the DAO class, or be bound through with-column-writers.

function do-query-dao (((type type-var) query &rest args) &body body)
→ list

Like query-dao, but iterates over the results rather than returning them. For each matching DAO, body is evaluated with type-var bound to the instance.

(do-query-dao (('user user) (:order-by (:select '* :from 'user :where (:> 'score 10000)) 'name))
  (pushnew user high-scorers))

variable *ignore-unknown-columns*

Normally, when get-dao, select-dao, or query-dao finds a column in the database that's not in the DAO class, it will raise an error. Setting this variable to a non-NIL will cause it to simply ignore the unknown column.

method insert-dao (dao)
→ dao

Insert the given dao into the database. Column slots of the object which are unbound implies the database defaults. Hence, if these columns has no defaults defined in the database, the the insertion of the dao will be failed. (This feature only works on PostgreSQL 8.2 and up.)

method update-dao (dao)
→ dao

Update the representation of the given dao in the database to the values in the object. This is not defined for tables that do not have any non-primary-key columns. Raises an error when no row matching the dao exists.

function save-dao (dao)
→ boolean

Tries to insert the given dao using insert-dao. If this raises a unique key violation error, it tries to update it by using update-dao instead. Be aware that there is a possible race condition here ― if some other process deletes the row at just the right moment, the update fails as well. Returns a boolean telling you whether a new row was inserted.

This function is unsafe to use inside of a transaction ― when a row with the given keys already exists, the transaction will be aborted. Use save-dao/transaction instead in such a situation.

See also: upsert-dao.

function save-dao/transaction (dao)
→ boolean

Acts exactly like save-dao, except that it protects its attempt to insert the object with a rollback point, so that a failure will not abort the transaction.

See also: upsert-dao.

method upsert-dao (dao)
→ dao

Like save-dao or save-dao/transaction but using a different method that doesn't involve a database exception. This is safe to use both in and outside a transaction, though it's advisable to always do it in a transaction to prevent a race condition. The way it works is:

  1. If the object contains unbound slots, we call insert-dao directly, thus the behavior is like save-dao.
  2. Otherwise we try to update a record with the same primary key. If the PostgreSQL returns a non-zero number of rows updated it treated as the record is already exists in the database, and we stop here.
  3. If the PostgreSQL returns a zero number of rows updated, it treated as the record does not exist and we call insert-dao.

The race condition might occur at step 3 if there's no transaction: if UPDATE returns zero number of rows updated and another thread inserts the record at that moment, the insertion implied by step 3 will fail.

Note, that triggers and rules may affect the number of inserted or updated rows returned by PostgreSQL, so zero or non-zero number of affected rows may not actually indicate the existence of record in the database.

This method returns two values: the DAO object and a boolean (T if the object was inserted, NIL if it was updated).

method delete-dao (dao)

Delete the given dao from the database.

function dao-table-name (class)
→ string

Get the name of the table associated with the given DAO class (or symbol naming such a class).

function dao-table-definition (class)
→ string

Given a DAO class, or the name of one, this will produce an SQL query string with a definition of the table. This is just the bare simple definition, so if you need any extra indices or or constraints, you'll have to write your own queries to add them.

macro with-column-writers ((&rest writers) &body body)

Provides control over the way get-dao, select-dao, and query-dao read values from the database. This is not commonly needed, but can be used to reduce the amount of queries a system makes. writers should be a list of alternating column names (strings or symbols) and writers, where writers are either symbols referring to a slot in the objects, or functions taking two arguments ― an instance and a value ― which can be used to somehow store the value in the new instance. When any DAO-fetching function is called in the body, and columns matching the given names are encountered in the result, the writers are used instead of the default behaviour (try and store the value in the slot that matches the column name).

An example of using this is to add some non-column slots to a DAO class, and use query-dao within a with-column-writers form to pull in extra information about the objects, and immediately store it in the new instances.

Table definition and creation

It can be useful to have the SQL statements needed to build an application's tables available from the source code, to do things like automatically deploying a database. The following macro and functions allow you to group sets of SQL statements under symbols, with some shortcuts for common elements in table definitions.

macro deftable (name &body definition)

Define a table. name can be either a symbol or a (symbol string) list. In the first case, the table name is derived from the symbol's name by S-SQL's rules. In the second case, the name is given explicitly. The body of definitions can contain anything that evaluates to a string, as well as S-SQL expressions. The variables *table-name* and *table-symbol* are bound to the relevant values in the body. Note that the evaluation of the definition is ordered, so you'll generally want to create your table first and then define indices on it.

function !dao-def ()

Should only be used inside deftable's body. Adds the result of calling dao-table-definition on *table-symbol* to the definition.

function !index (&rest columns), !unique-index (&rest columns)

Define an index on the table being defined. The columns can be given as symbols or strings.

function !foreign (target-table columns &optional target-columns &key on-delete on-update deferrable initially-deferred)

Add a foreign key to the table being defined. target-table is the referenced table. columns is a list of column names or single name in this table, and, if the columns have different names in the referenced table, target-columns must be another list of column names or single column name of the target-table, or :primary-key to denote the column(s) of the target-table's primary key as referenced column(s).

The on-delete and on-update arguments can be used to specify ON DELETE and ON UPDATE actions, as per the keywords allowed in create-table. In addition, the deferrable and initially-deferred arguments can be used to indicate whether constraint checking can be deferred until the current transaction completed, and whether this should be done by default. Note that none of these are really &key arguments, but rather are picked out of a &rest arg at runtime, so that they can be specified even when target-columns is not given.

function !unique (target-fields &key deferrable initially-deferred)

Constrains one or more columns to only contain unique (combinations of) values, with deferrable and initially-deferred defined as in !foreign

function create-table (symbol)

Creates the table identified by symbol by executing all forms in its definition.

function create-all-tables ()

Creates all defined tables.

function create-package-tables (package)

Creates all tables identified by symbols interned in the given package.

variables *table-name*, *table-symbol*

These variables are bound to the relevant name and symbol while the forms of a table definition are evaluated. Can be used to define shorthands like the ones below.

Schemata

Schema allow you to separate tables into differnet name spaces. In different schemata two tables with the same name are allowed to exists. The tables can be referred by fully qualified names or with the macro with-schema. You could also set the search path with set-search-path. For listing end checking there are also the functions list-schemata and schema-exist-p. The following functions allow you to create, drop schemata and to set the search path.

function create-schema (schema)

Creates a new schema. Raises an error if the schema is already exists.

function drop-schema (schema)

Removes a schema. Raises an error if the schema is not empty.

function get-search-path ()

Retrieve the current search path.

function set-search-path (path)

Sets the search path to the path. This function is used by with-schema.

Symbol-index

postmodern-20151031-git/doc/s-sql.html000066400000000000000000001163141260073634400174300ustar00rootroot00000000000000 S-SQL reference manual

S-SQL reference manual

This is the reference manual for the S-SQL component of the postmodern library.

S-SQL provides a lispy syntax for SQL queries, and knows how to convert various lisp types to their textual SQL representation. It takes care to do as much of the work as possible at compile-time, so that at runtime a string concatenation is all that is needed to produce the final SQL query.

Contents

  1. Interface
  2. SQL Types
  3. SQL Syntax
  4. Symbol-index

Interface

macro sql (form)
→ string

Convert the given form (a list starting with a keyword) to an SQL query string at compile time, according to the rules described here.

function sql-compile (form)
→ string

This is the run-time variant of the sql macro. It converts the given list to an SQL query, with the same rules except that symbols in this list do not have to be quoted to be interpreted as identifiers.

function sql-template (form)

In cases where you do need to build the query at run time, yet you do not want to re-compile it all the time, this function can be used to compile it once and store the result. It takes an S-SQL form, which may contain $$ placeholder symbols, and returns a function that takes one argument for every $$. When called, this returned function produces an SQL string in which the placeholders have been replaced by the values of the arguments.

function enable-s-sql-syntax (&optional (char #\Q))

Modifies the current readtable to add a #Q syntax that is read as (sql ...). The character to use can be overridden by passing an argument.

function sql-escape-string (string)
→ string

Escapes a string for inclusion in a PostgreSQL query.

method sql-escape (value)
→ string

A generalisation of sql-escape-string. Looks at the type of the value passed, and properly writes it out it for inclusion in an SQL query. Symbols will be converted to SQL names.

variable *standard-sql-strings*

Used to configure whether S-SQL will use standard SQL strings (just replace #\' with ''), or backslash-style escaping. Setting this to NIL is always safe, but when the server is configured to allow standard strings (compile-time parameter 'standard_conforming_strings' is 'on', which will become the default in future versions of PostgreSQL), the noise in queries can be reduced by setting this to T.

variable *escape-sql-names-p*

Determines whether double quotes are added around column, table, and function names in queries. May be T, in which case every name is escaped, NIL, in which case none is, or :auto, which causes only reserved words to be escaped.. The default value is :auto. Be careful when binding this with let and such ― since a lot of SQL compilation tends to happen at compile-time, the result might not be what you expect.

function sql-type-name (type)
→ string

Create the SQL equivalent of the given Lisp type, if one is known. See types.

function to-sql-name (name &optional (escape-p *escape-sql-names-p*))
→ string

Convert a symbol or string to a name that can be used as an SQL identifier by converting all non-alphanumeric characters to underscores. Also lowercases the name to make queries look a bit less hideous. When a second argument is given, this overrides the current value of *escape-sql-names-p*.

function from-sql-name (string)
→ keyword

Convert a string that represents an SQL identifier to a keyword by uppercasing it and converting the underscores to dashes.

macro register-sql-operators (arity &rest names)

Define simple SQL operators. Arity is one of :unary (like 'not'), :unary-postfix (the operator comes after the operand), :n-ary (like '+': the operator falls away when there is only one operand), :2+-ary (like '=', which is meaningless for one operand), or :n-or-unary (like '-', where the operator is kept in the unary case). After the arity may follow any number of operators, either just a keyword, in which case the downcased symbol name is used as the SQL operator, or a two-element list containing a keyword and a name string.

SQL Types

S-SQL knows the SQL equivalents to a number of Lisp types, and defines some extra types that can be used to denote other SQL types. The following table shows the correspondence:

Lisp typeSQL type
smallintsmallint
integerinteger
bigintbigint
(numeric X Y)numeric(X, Y)
float, realreal
double-float, double-precisiondouble-precision
string, texttext
(string X)char(X)
(varchar X)varchar(X)
booleanboolean
byteabytea
datedate
timestamptimestamp
intervalinterval

type db-null

This is a type of which only the keyword :null is a member. It is used to represent NULL values from the database.

SQL Syntax

An S-SQL form is converted to a query through the following rules:

The following operators are defined:

sql-op :+, :*, :%, :&, :|, :||, :and, :or, :=, :/, :!=, :<, :>, :<=, :>=, :^, :union, :union-all, :intersect, :intersect-all, :except, :except-all (&rest args)

These are expanded as infix operators. When meaningful, they allow more than two arguments. :- can also be used as a unary operator to negate a value. Note that the arguments to :union, :union-all, :intersect, and :except should be queries (:select forms).

Note that you'll have to escape pipe characters to enter them as keywords. S-SQL handles the empty keyword symbol (written :||) specially, and treats it like :\|\|, so that it can be written without escapes. With :\|, this doesn't work.

sql-op :~, :not (arg)

Unary operators for bitwise and logical negation.

sql-op :~, :~*, :!~, :!~* (string pattern)

Regular expression matching operators. The exclamation mark means 'does not match', the asterisk makes the match case-insensitive.

sql-op :like, :ilike (string pattern)

Simple SQL string matching operators (:ilike is case-insensitive).

sql-op :@@

Fast Text Search match operator.

sql-op :desc (column)

Used to invert the meaning of an operator in an :order-by clause.

sql-op :nulls-first, :nulls-last (column)

Used to determine where :null values appear in an :order-by clause.

sql-op :as (form name &rest fields)

Assigns a name to a column or table in a :select form. When fields are given, they are added after the name, in parentheses. For example, (:as 'table1 't1 'foo 'bar) becomes table1 AS t1(foo, bar). When you need to specify types for the fields, you can do something like (:as 'table2 't2 ('foo integer)). Note that names are quoted, types are not (when using sql-compile or sql-template, you can leave out the quotes entirely).

sql-op :exists (query)

The EXISTS operator. Takes a query as an argument, and returns true or false depending on whether that query returns any rows.

sql-op :is-null (arg)

Test whether a value is null.

sql-op :not-null (arg)

Test whether a value is not null.

sql-op :in (value set)

Test whether a value is in a set of values.

sql-op :not-in (value set)

Inverse of the above.

sql-op :set (&rest elements)

Denote a set of values. This one has two interfaces. When the elements are known at compile-time, they can be given as multiple arguments to the operator. When they are not, a single argument that evaluates to a list should be used.

sql-op :[] (form start &optional end)

Dereference an array value. If end is provided, extract a slice of the array.

sql-op :extract (unit form)

Extract a field from a date/time value. For example, (:extract :month (:now)).

sql-op :case (&rest clauses)

A conditional expression. Clauses should take the form (test value). If test is :else, an ELSE clause will be generated.

sql-op :between (n start end)

Test whether a value lies between two other values.

sql-op :between-symmetric (n start end)

Works like :between, except that the start value is not required to be less than the end value.

sql-op :dot (&rest names)

Can be used to combine multiple names into a name of the form A.B to refer to a column in a table, or a table in a schema. Note that you can also just use a symbol with a dot in it.

sql-op :type (form type)

Add a type declaration to a value, as in in "4.3::real". The second argument is not evaluated normally, but put through sql-type-name to get a type identifier.

sql-op :raw (string)

Insert a string as-is into the query. This can be useful for doing things that the syntax does not support, or to re-use parts of a query across multiple queries:

(let* ((test (sql (:and (:= 'foo 22) (:not-null 'bar))))
       (rows (query (:select '* :from 'baz :where (:raw test)))))
  (query (:delete-from 'baz :where (:raw test)))
  (do-stuff rows))

sql-op :select (&rest args)

Creates a select query. The arguments are split on the keywords found among them. The group of arguments immediately after :select is interpreted as the expressions that should be selected. After this, an optional :distinct may follow, which will cause the query to only select distinct rows, or alternatively :distinct-on followed by a group of row names. Next comes the optional keyword :from, followed by at least one table name and then any number of join statements. Join statements start with one of :left-join, :right-join, :inner-join, :outer-join or :cross-join, then a table name or subquery, then the keyword :on or :using, if applicable, and then a form. A join can be preceded by :natural (leaving off the :on clause) to use a natural join. After the joins an optional :where followed by a single form may occur. And finally :group-by and :having can optionally be specified. The first takes any number of arguments, and the second only one. An example:

(:select (:+ 'field-1 100) 'field-5
   :from (:as 'my-table 'x)
   :left-join 'your-table :on (:= 'x.field-2 'your-table.field-1)
   :where (:not-null 'a.field-3))

sql-op :limit (query amount &optional offset)

In S-SQL limit is not part of the select operator, but an extra operator that is applied to a query (this works out better when limiting the union or intersection of multiple queries, same for sorting). It limits the number of results to the amount given as the second argument, and optionally offsets the result by the amount given as the third argument.

sql-op :order-by (query &rest exprs)

Order the results of a query by the given expressions. See :desc for when you want to invert an ordering.

sql-op :over (form &rest args)

Over, partition-by and window are so-called window functions. A window function performs a calculation across a set of table rows that are somehow related to the current row.

(query (:select 'salary (:over (:sum 'salary))
                :from 'empsalary))

sql-op :partition-by (&rest args)

Args is a list of one or more columns to partition by, optionally followed by an :order-by clause.

(query (:select 'depname 'subdepname 'empno 'salary
                (:over (:avg 'salary)
                       (:partition-by 'depname 'subdepname))
                :from 'empsalary))

Note the use of :order-by without parens:

(query (:select 'depname 'empno 'salary
                (:over (:rank)
                       (:partition-by 'depname :order-by (:desc 'salary)))
                :from 'empsalary))
    

sql-op :window (form)

(query (:select (:over (:sum 'salary) 'w)
              (:over (:avg 'salary) 'w)
              :from 'empsalary :window
              (:as 'w (:partition-by 'depname :order-by (:desc 'salary)))))

sql-op :with (&rest args)

With provides a way to write auxillary statements for use in a larger query, often referred to as Common Table Expressions or CTEs.

(query (:with (:as 'upd
                 (:parens
                  (:update 'employees :set 'sales-count (:+ 'sales-count 1)
                           :where (:= 'id
                                      (:select 'sales-person
                                               :from 'accounts
                                               :where (:= 'name "Acme Corporation")))
                           :returning '*)))
            (:insert-into 'employees-log
                          (:select '* 'current-timestamp :from
                 'upd))))

sql-op :with-recursive (&rest args)

Recursive modifier to a WITH statement, allowing the query to refer to its own output.

(query (:with-recursive
      (:as (:t1 'n)
           (:union-all (:values 1)
                       (:select (:+ 'n 1)
                                :from 't1
                                :where (:< 'n 100))))
      (:select (:sum 'n) :from 't1)))

(query (:with-recursive
      (:as (:included_parts 'sub-part 'part 'quantity)
           (:union-all
            (:select 'sub-part 'part 'quantity
                     :from 'parts
                     :where (:= 'part "our-product"))
            (:select 'p.sub-part 'p.part 'p.quantity
                     :from (:as 'included-parts 'pr)
                     (:as 'parts 'p)
                     :where (:= 'p.part 'pr.sub-part) )))
      (:select 'sub-part (:as (:sum 'quantity) 'total-quantity)
               :from 'included-parts
               :group-by 'sub-part)))

(query (:with-recursive
      (:as (:search-graph 'id 'link 'data 'depth)
           (:union-all (:select 'g.id 'g.link 'g.data 1
                                :from (:as 'graph 'g))
                       (:select 'g.id 'g.link 'g.data (:+ 'sg.depth 1)
                                :from (:as 'graph 'g) (:as 'search-graph 'sg)
                                :where (:= 'g.id 'sg.link))))
      (:select '* :from 'search-graph)))

(query (:with-recursive
      (:as (:search-graph 'id 'link 'data'depth 'path 'cycle)
           (:union-all
            (:select 'g.id 'g.link 'g.data 1
                     (:[] 'g.f1 'g.f2) nil
                     :from (:as 'graph 'g))
            (:select 'g.id 'g.link 'g.data (:+ 'sg.depth 1)
                     (:|| 'path (:row 'g.f1 'g.f2))
                     (:= (:row 'g.f1 'g.f2)
                         (:any* 'path))
                     :from (:as 'graph 'g)
                     (:as 'search-graph 'sg)
                     :where (:and (:= 'g.id 'sg.link)
                                  (:not 'cycle)))))
      (:select '* :from 'search-graph)))

sql-op :for-update (query &key of nowait)

Locks the selected rows against concurrent updates. This will prevent the rows from being modified or deleted by other transactions until the current transaction ends. The :of keyword should be followed by one or more table names. If provided, PostgreSQL will lock these tables instead of the ones detected in the select statement. The :nowait keyword should be provided by itself (with no argument attached to it), after all the :of arguments . If :nowait is provided, PostgreSQL will throw an error if a table cannot be locked immediately, instead of pausing until it's possible.

(:for-update (:select :* :from 'foo 'bar 'baz) :of 'bar 'baz :nowait)

sql-op :for-share (query &key of nowait)

Similar to :for-update, except it acquires a shared lock on the table, allowing other transactions to perform :for-share selects on the locked tables.

sql-op :function (name (&rest arg-types) return-type stability body)

Create a stored procedure. The argument and return types are interpreted as type names and not evaluated. Stability should be one of :immutable, :stable, or :volatile (see the PostgreSQL documentation). For example, a function that gets foobars by id:

(:function 'get-foobar (integer) foobar :stable (:select '* :from 'foobar :where (:= 'id '$1)))

sql-op :insert-into (table &rest rest)

Insert a row into a table. When the second argument is :set, the other arguments should be alternating field names and values, otherwise it should be a :select form that will produce the values to be inserted. Example:

(:insert-into 'my-table :set 'field-1 42 'field-2 "foobar")

It is possible to add :returning, followed by a list of field names or expressions, at the end of the :insert-into form. This will cause the query to return the values of these expressions as a single row.

sql-op :update (table &rest rest)

Update values in a table. After the table name there should follow the keyword :set and any number of alternating field names and values, like for :insert-into. Next comes the optional keyword :from, followed by at least one table name and then any number of join statements, like for :select. After the joins, an optional :where keyword followed by the condition, and :returning keyword followed by a list of field names or expressions indicating values to be returned as query result.

sql-op :delete-from (table &rest rest)

Delete rows from the named table. Can be given a :where argument followed by a condition, and a :returning argument, followed by one or more expressions that should be returned for every deleted row.

sql-op :create-table (name (&rest columns) &rest options)

Create a new table. After the table name a list of column definitions follows, which are lists that start with a name, followed by one or more of the following keyword arguments:

:type
This one is required. It specifies the type of the column. Use a type like (or db-null integer) to specify a column that may have NULL values.
:default
Provides a default value for the field.
:unique
If this argument is non-nil, the values of the column must be unique.
:primary-key
When non-nil, the column is a primary key of the table.
:check
Adds a constraint to this column. The value provided for this argument must be an S-SQL expression that returns a boolean value. It can refer to other columns in the table if needed.
:references
Adds a foreign key constraint to this table. The argument provided must be a list of the form (target &optional on-delete on-update). When target is a symbol, it names the table to whose primary key this constraint refers. When it is a list, its first element is the table, and its second element the column within that table that the key refers to. on-delete and on-update can be used to specify the actions that must be taken when the row that this key refers to is deleted or changed. Allowed values are :restrict, :set-null, :set-default, :cascade, and :no-action.

After the list of columns, zero or more extra options (table constraints) can be specified. These are lists starting with one of the following keywords:

:check
Adds a constraint to the table. Takes a single S-SQL expression that produces a boolean as its argument.
:primary-key
Specifies a primary key for the table. The arguments to this option are the names of the columns that this key consists of.
:unique
Adds a unique constraint to a group of columns. Again, the arguments are a list of symbols that indicate the relevant columns.
:foreign-key
Create a foreign key. The arguments should have the form (columns target &optional on-delete on-update), where columns is a list of columns that are used by this key, while the rest of the arguments have the same meaning as they have in the :references option for columns.

Every list can start with :constraint name to create a specifically named constraint.

Note that, unlike most other operators, :create-table expects most of its arguments to be unquoted symbols. The exception to this is the value of :check constraints: These must be normal S-SQL expressions, which means that any column names they contain should be quoted. When programmatically generating table definitions, sql-compile is usually more practical than the sql macro.

Here is an example of a :create-table form:

(:create-table enemy
  ((name :type string :primary-key t)
   (age :type integer)
   (address :type (or db-null string) :references (important-addresses :cascade :cascade))
   (fatal-weakness :type text :default "None")
   (identifying-color :type (string 20) :unique t))
  (:foreign-key (identifying-color) (colors name))
  (:constraint enemy-age-check :check (:> 'age 12))

sql-op:alter-table (name action &rest args)

Alters named table. Currently changing a column's data type is not supported. The meaning of args depends on action:

:add-column
Adds column to table. args should be a column in the same form as for :create-table.
:drop-column
Drops a column from the table.
:add-constraint
Adds a named constraint to the table.
:drop-constraint
Drops constraint. First of args should name a constraint to be dropped; second, optional argument specifies behaviour regarding objects dependent on the constraint and it may equal :cascade or :restrict.
:add
Adds an unnamed constraint to table. args should be a constraint in the same form as for :create-table. (This is for backwards-compatibility, you should use named constraints.)

Here is an example using the table defined above:

(:alter-table enemy :drop-constraint enemy-age-check)
(:alter-table enemy :add-constraint enemy-age-check :check (:> 'age 21))

sql-op :drop-table (name)

Drops the named table. You may optionally pass :if-exists before the name to suppress the error message.

sql-op :create-index (name &rest args)

Create an index on a table. After the name of the index the keyword :on should follow, with the table name after it. Then the keyword :fields, followed by one or more column names. Optionally, a :where clause with a condition can be added at the end to make a partial index.

sql-op :create-unique-index (name &rest args)

Works like :create-index, except that the index created is unique.

sql-op :drop-index (name)

Drop an index. Takes an :if-exists argument like :drop-table.

sql-op :create-sequence (name &key increment min-value max-value start cache cycle)

Create a sequence with the given name. The rest of the arguments control the way the sequence selects values.

sql-op :drop-sequence (name)

Drop a sequence. You may pass :if-exists as an extra first argument.

sql-op :create-view (name query)

Create a view from an S-SQL-style query.

sql-op :drop-view (name)

Drop a view. Takes optional :if-exists argument.

sql-op :set-constraints (state &rest constraints)

Configure whether deferrable constraints should be checked when a statement is executed, or when the transaction containing that statement is completed. The provided state must be either :immediate, indicating the former, or :deferred, indicating the latter. The constraints must be either the names of the constraints to be configured, or unspecified, indicating that all deferrable constraints should be thus configured.

sql-op :listen (channel)

Tell the server to listen for notification events on channel channel, a string, on the current connection.

sql-op :unlisten (channel)

Stop listening for events on channel.

sql-op :notify (channel &optional payload)

Signal a notification event on channel channel, a string. The optional payload string can be used to send additional event information to the listeners.

Symbol-index

postmodern-20151031-git/doc/simple-date.html000066400000000000000000000205741260073634400205770ustar00rootroot00000000000000 Simple-date reference manual

Simple-date reference manual

Simple-date provides types (CLOS classes) for dates, timestamps, and intervals similar to the ones SQL databases use, in order to be able to store and read these to and from a database in a straighforward way. A few obvious operations are defined on these types.

The most glaring defect of this library is its ignorance of time zones. It pretends the whole world lives in UTC. Use with care.

When this libary is loaded after CL-postgres, it will register suitable SQL readers and writers for the associated database types.

Contents

  1. Date type
  2. Timestamp type
  3. Interval type
  4. Operations
  5. Symbol-index

Date type

class date

Represents a date, with no time-of-day information.

function encode-date (year month day)
→ date

Creates a date object.

function decode-date (date)
→ (values year month day)

Extract the elements from a date object.

function day-of-week (date)
→ integer

Determine the day of the week that the given date falls on. Value ranges from 0 to 6, with 0 being Sunday and 6 being Saturday.

Timestamp type

class timestamp

Represents an absolute timestamp, with a millisecond precision.

function encode-timestamp (year month day &optional (hour 0) (minute 0) (second 0) (millisecond 0))
→ timestamp

Create a timestamp. No negative values or values outside of an arguments normal range (i.e. 60 for minutes, 1000 for milliseconds) should be passed.

function decode-timestamp (timestamp)
→ (values year month day hour minute second millisecond)

Decode a timestamp into its components.

function timestamp-to-universal-time (timestamp)
→ universal-time

Convert a timestamp to the corresponding universal-time, rounding to seconds. Note that this will treat the timestamp as if it were in UTC.

function universal-time-to-timestamp (universal-time)
→ timestamp

Create a timestamp from a universal time. Again, the resulting timestamp should be treated as if it were in UTC.

Interval type

class interval

An interval represents a period of time. It contains both an absolute part in milliseconds (days, weeks, minutes, etc are always the same length), and a relative part for months and years ― the amount of time that a month or year represents is not always the same.

function encode-interval (&key (year 0) (month 0) (week 0) (day 0) (hour 0) (minute 0) (second 0) (millisecond 0))
→ interval

Create an interval. Arguments may be negative and of any size.

function decode-interval (interval)
→ (values year month day hour minute second millisecond)

Decompose an interval into parts. Note that these may be different from the parameters that created it ― an interval of 3600 seconds is the same as one of 1 hour.

Operations

To prevent a proliferation of different function names, generic functions are used for operations on time values. The semantics of these differ for the type of the operands.

method time-add (a b)
→ value

Adds two time-related objects. Adding an interval to a date or timestamp will return a new date or timestamp, increased by the value of the interval. Adding two intervals returns a new interval with the sum of the two arguments. Integers can be used in place of intervals, and will be interpreted as an amount of milliseconds.

method time-subtract (a b)
→ value

Subtracts time-related objects from each other. Subtracting two dates or timestamps results in an interval that represents the difference between them. Similarly, subtracting two intervals also gives their difference.

method time= (a b)
→ boolean

Compare two time-related values, returns a boolean indicating whether they denote the same time or period.

method time< (a b)
→ boolean

Compare two time-related values, returns a boolean indicating whether the first is less than the second.

method time> (a b)
→ boolean

Compare two time-related values, returns a boolean indicating whether the first is greater than the second.

function time<= (a b)
→ boolean

The inverse of time>.

function time>= (a b)
→ boolean

The inverse of time<.

Symbol-index

postmodern-20151031-git/doc/style.css000066400000000000000000000022161260073634400173500ustar00rootroot00000000000000body { margin: 0; font-family: tahoma, arial, sans-serif; padding: 50px 100px; color: black; } h1 { font-size: 250%; border-bottom: 3px solid #449977; } h2 { font-size: 140%; border-bottom: 1px solid #449977; } h3 { font-size: 110%; } code { font-size: 1.2em; } p.news { text-indent: -3em; padding-left: 3em; } pre.code { margin: 0 16px; padding: 7px; border: 1px solid #99CCBB; } p.def { margin-top: 1.5em; font-family: courier; } p.def span { color: #555555; font-weight: bold; font-family: tahoma, arial, sans-serif; font-size: .8em; } .desc { margin-left: 1em; } thead { font-weight: bold; } table { border-collapse: collapse; } tr + tr { border-top: 1px solid #88BB99; } thead tr { border-bottom: 2px solid #88BB99; } td + td, th + th { border-left: 2px solid #88BB99; } th { text-align: left; padding: 2px 5px; } td { padding: 2px 5px; vertical-align: top; } a:link { color: #3333AA; text-decoration: none; } a:visited { color: #773377; text-decoration: none; } a:hover { text-decoration: underline; } ul.symbol-index { font-family: monospace; font-size: 1.2em; } postmodern-20151031-git/postmodern.asd000066400000000000000000000040641260073634400176170ustar00rootroot00000000000000(defpackage :postmodern-system (:use :common-lisp :asdf) (:export :*threads*)) (in-package :postmodern-system) ;; Change this to manually turn threading support on or off. (eval-when (:compile-toplevel :load-toplevel :execute) #+(or allegro armedbear cmu corman (and digitool ccl-5.1) ecl lispworks openmcl sbcl) (pushnew :postmodern-thread-safe *features*) #+(or allegro clisp ecl lispworks mcl openmcl cmu sbcl) (pushnew :postmodern-use-mop *features*)) (defsystem :postmodern :description "PostgreSQL programming API" :author "Marijn Haverbeke " :license "BSD" :depends-on (:cl-postgres :s-sql #+postmodern-use-mop :closer-mop #+postmodern-thread-safe :bordeaux-threads) :components ((:module :postmodern :components ((:file "package") (:file "connect" :depends-on ("package")) (:file "query" :depends-on ("connect")) (:file "prepare" :depends-on ("query")) (:file "util" :depends-on ("query")) (:file "transaction" :depends-on ("query")) (:file "namespace" :depends-on ("query")) #+postmodern-use-mop (:file "table" :depends-on ("util" "transaction")) (:file "deftable" :depends-on ("query" #+postmodern-use-mop "table")))))) (defsystem :postmodern-tests :depends-on (:postmodern :eos :simple-date :simple-date-postgres-glue) :components ((:module :postmodern :components ((:file "tests"))))) (defmethod perform ((op asdf:test-op) (system (eql (find-system :postmodern)))) (asdf:oos 'asdf:load-op :postmodern) (asdf:oos 'asdf:load-op :cl-postgres-tests) (asdf:oos 'asdf:load-op :postmodern-tests) (funcall (intern (string :prompt-connection) (string :cl-postgres-tests)) (eval (intern (string :*test-connection*) (string :postmodern-tests)))) (funcall (intern (string :run!) (string :Eos)) :postmodern)) postmodern-20151031-git/postmodern/000077500000000000000000000000001260073634400171225ustar00rootroot00000000000000postmodern-20151031-git/postmodern/connect.lisp000066400000000000000000000113041260073634400214430ustar00rootroot00000000000000(in-package :postmodern) (defclass pooled-database-connection (database-connection) ((pool-type :initarg :pool-type :accessor connection-pool-type)) (:documentation "Type for database connections that are pooled. Stores the arguments used to create it, so different pools can be distinguished.")) (defparameter *database* nil "Special holding the current database. Most functions and macros operating on a database assume this contains a connected database.") (defparameter *default-use-ssl* :no) (defun connect (database user password host &key (port 5432) pooled-p (use-ssl *default-use-ssl*) (service "postgres")) "Create and return a database connection." (cond (pooled-p (let ((type (list database user password host port use-ssl))) (or (get-from-pool type) (let ((connection (open-database database user password host port use-ssl))) (change-class connection 'pooled-database-connection :pool-type type) connection)))) (t (open-database database user password host port use-ssl service)))) (defun connected-p (database) "Test whether a database connection is still connected." (database-open-p database)) (defun connect-toplevel (database user password host &key (port 5432) (use-ssl *default-use-ssl*)) "Set *database* to a new connection. Use this if you only need one connection, or if you want a connection for debugging from the REPL." (when (and *database* (connected-p *database*)) (restart-case (error "Top-level database already connected.") (replace () :report "Replace it with a new connection." (disconnect-toplevel)) (leave () :report "Leave it." (return-from connect-toplevel nil)))) (setf *database* (connect database user password host :port port :use-ssl use-ssl)) (values)) (defgeneric disconnect (database) (:method ((connection database-connection)) (close-database connection)) (:documentation "Close a database connection. Returns it to a pool if it is a pooled connection.")) (defgeneric reconnect (database) (:method ((database database-connection)) (reopen-database database)) (:method ((connection pooled-database-connection)) (error "Can not reconnect a pooled database.")) (:documentation "Reconnect a database connection.")) (defun disconnect-toplevel () "Disconnect *database*." (when (and *database* (connected-p *database*)) (disconnect *database*)) (setf *database* nil)) (defun call-with-connection (spec thunk) "Binds *database* to a new connection, as specified by the spec argument, which should be a list of arguments that can be passed to connect, and runs the function given as a second argument with that database." (let ((*database* (apply #'connect spec))) (unwind-protect (funcall thunk) (disconnect *database*)))) (defmacro with-connection (spec &body body) "Locally establish a database connection, and bind *database* to it." `(let ((*database* (apply #'connect ,spec))) (unwind-protect (progn ,@body) (disconnect *database*)))) (defvar *max-pool-size* nil "The maximum amount of connection that will be kept in a single pool, or NIL for no maximum.") (defvar *connection-pools* (make-hash-table :test 'equal) "Maps pool specifiers to lists of pooled connections.") #+postmodern-thread-safe (defvar *pool-lock* (bordeaux-threads:make-lock "connection-pool-lock") "A lock to prevent multiple threads from messing with the connection pool at the same time.") (defmacro with-pool-lock (&body body) "Aquire a lock for the pool when evaluating body \(if thread support is present)." #+postmodern-thread-safe `(bordeaux-threads:with-lock-held (*pool-lock*) ,@body) #-postmodern-thread-safe `(progn ,@body)) (defun get-from-pool (type) "Get a database connection from the specified pool, returns nil if no connection was available." (with-pool-lock (pop (gethash type *connection-pools*)))) (defmethod disconnect ((connection pooled-database-connection)) "Add the connection to the corresponding pool, or drop it when the pool is full." (macrolet ((the-pool () '(gethash (connection-pool-type connection) *connection-pools* ()))) (when (database-open-p connection) (with-pool-lock (if (or (not *max-pool-size*) (< (length (the-pool)) *max-pool-size*)) (push connection (the-pool)) (call-next-method)))) (values))) (defun clear-connection-pool () "Disconnect and remove all connections in the connection pool." (with-pool-lock (maphash (lambda (type connections) (declare (ignore type)) (dolist (conn connections) (close-database conn))) *connection-pools*) (setf *connection-pools* (make-hash-table :test 'equal)) (values))) postmodern-20151031-git/postmodern/deftable.lisp000066400000000000000000000131061260073634400215620ustar00rootroot00000000000000(in-package :postmodern) (defvar *table-name*) (setf (documentation '*table-name* 'variable) "Used inside deftable to find the name of the table being defined.") (defvar *table-symbol*) (setf (documentation '*table-symbol* 'variable) "Used inside deftable to find the symbol naming the table being defined.") (defvar *tables* () "Unexported ordered list containing the known table definitions.") (defun add-table-definition (symbol func) (let (last-cons) (loop :for cons :on *tables* :do (when (eq (caar cons) symbol) (setf (cdar cons) func) (return-from add-table-definition (values))) (setf last-cons cons)) (if last-cons (setf (cdr last-cons) (list (cons symbol func))) (setf *tables* (list (cons symbol func))))) (values)) (defmacro deftable (name &body definitions) "Define a table. name can be either a symbol or a (symbol string) list. In the first case, the table name is derived from the symbol by S-SQL's rules, in the second case, the name is given explicitly. The body of definitions can contain anything that evaluates to a string, as well as S-SQL expressions. In this body, the variables *table-name* and *table-symbol* are bound to the relevant values." (multiple-value-bind (symbol name) (if (consp name) (values-list name) (values name (to-sql-name name nil))) (flet ((check-s-sql (form) (if (and (consp form) (keywordp (car form))) (list 'sql form) form))) `(add-table-definition ',symbol (lambda () (let ((*table-name* ,name) (*table-symbol* ',symbol)) (dolist (stat (list ,@(mapcar #'check-s-sql definitions))) (execute stat)))))))) (defun create-table (name) "Create a defined table." (with-transaction () (funcall (or (cdr (assoc name *tables*)) (error "No table '~a' defined." name))) (values))) (defun create-all-tables () "Create all defined tables." (loop :for (nil . def) :in *tables* :do (funcall def))) (defun create-package-tables (package) "Create all tables whose identifying symbol is interned in the given package." (let ((package (find-package package))) (loop :for (sym . def) :in *tables* :do (when (eq (symbol-package sym) package) (funcall def))))) (defun flat-table-name (&optional (table *table-name*)) (when (symbolp table) (setf table (string-downcase (string table)))) (let ((dotpos (position #\. table))) (if dotpos (subseq table (1+ dotpos)) table))) (labels ((index-name (fields) (make-symbol (format nil "~a-~{~a~^-~}-index" (flat-table-name) fields))) (make-index (type fields) (sql-compile `(,type ,(index-name fields) :on ,*table-name* :fields ,@fields)))) (defun \!index (&rest fields) "Used inside a deftable form. Define an index on the defined table." (make-index :create-index fields)) (defun \!unique-index (&rest fields) "Used inside a deftable form. Define a unique index on the defined table." (make-index :create-unique-index fields))) #+postmodern-use-mop (defun \!dao-def () "Used inside a deftable form. Define this table using the corresponding DAO class' slots." (dao-table-definition *table-symbol*)) (defun \!foreign (target fields &rest target-fields/on-delete/on-update/deferrable/initially-deferred) "Used inside a deftable form. Define a foreign key on this table. Pass a table the index refers to, a list of fields or single field in *this* table, and, if the fields have different names in the table referred to, another field or list of fields for the target table, or :primary-key to indicate that the other table's primary key should be referenced." (let* ((args target-fields/on-delete/on-update/deferrable/initially-deferred) (target-fields (and args (or (not (keywordp (car args))) (eq (car args) :primary-key)) (pop args)))) (labels ((fkey-name (target fields) (to-sql-name (format nil "~a_~a_~{~a~^_~}_foreign" (flat-table-name) (flat-table-name target) fields)))) (unless (listp fields) (setf fields (list fields))) (unless (listp target-fields) (setf target-fields (list target-fields))) (let* ((target-name (to-sql-name target)) (field-names (mapcar #'to-sql-name fields)) (target-names (cond ((equal target-fields '(:primary-key)) nil) ((null target-fields) field-names) (t (mapcar #'to-sql-name target-fields))))) (format nil "ALTER TABLE ~a ADD CONSTRAINT ~a FOREIGN KEY (~{~a~^, ~}) REFERENCES ~a~@[ (~{~a~^, ~})~] ~@[ON DELETE ~a~] ~@[ON UPDATE ~a~] ~:[NOT DEFERRABLE~;DEFERRABLE INITIALLY ~:[IMMEDIATE~;DEFERRED~]~]" (to-sql-name *table-name*) (fkey-name target fields) field-names target-name target-names (s-sql::expand-foreign-on* (getf args :on-delete :restrict)) (s-sql::expand-foreign-on* (getf args :on-update :restrict)) (getf args :deferrable nil) (getf args :initially-deferred nil)))))) (defun \!unique (target-fields &key deferrable initially-deferred) (unless (listp target-fields) (setf target-fields (list target-fields))) (format nil "ALTER TABLE ~A ADD CONSTRAINT ~A UNIQUE (~{~A~^, ~}) ~:[NOT DEFERRABLE~;DEFERRABLE INITIALLY ~:[IMMEDIATE~;DEFERRED~]~]" (to-sql-name *table-name*) (to-sql-name (format nil "~A_~{~A~^_~}_unique" *table-name* target-fields)) (mapcar #'pomo::to-sql-name target-fields) deferrable initially-deferred)) postmodern-20151031-git/postmodern/namespace.lisp000066400000000000000000000053341260073634400217540ustar00rootroot00000000000000(in-package :postmodern) (defmacro with-schema ((schema &key (strict t) (if-not-exist :create) (drop-after nil)) &body form) "A macro to set the schema search path of the postgresql database to include as first entry a specified schema. calling with strict 't only the specified schema is set as current search path. All other schema are then not searched any more. calling with if-not-exist set to :create the schema is created if this schema did not exist. calling with drop-after set to 't the schema is removed after the execution of the body form. example : (with-schema (:schema-name :strict nil :drop-after nil :if-not-exist :error) (foo 1) (foo 2))" `(do-with-schema ,schema (lambda () ,@form) :strict ,strict :if-not-exist ,if-not-exist :drop-after ,drop-after)) (defun do-with-schema (schema thunk &key strict if-not-exist drop-after) (let ((old-search-path (get-search-path))) (unwind-protect (progn (unless (schema-exist-p schema) (if (eq if-not-exist :create) (create-schema schema) (error 'database-error :message (format nil "Schema '~a' does not exist." schema)))) (set-search-path (if strict (to-sql-name schema t) (concatenate 'string (to-sql-name schema t) "," old-search-path))) (funcall thunk)) (set-search-path old-search-path) (when drop-after (drop-schema schema :cascade 't))))) (defun get-search-path () (query "SHOW search_path" :single)) (defun set-search-path (path) (execute (format nil "SET search_path TO ~a" path))) (defun list-schemata () "List all existing user defined schemata. Note: The query uses the portable information_schema relations instead of pg_tables relations SELECT schema_name FROM information_schema.schemata where schema_name !~ '(pg_*)|information_schema' ORDER BY schema_name ;" (query (:select 'schema_name :from 'information_schema.schemata :where (:!~ 'schema_name "(pg_*)|information_schema")) :column)) (defun schema-exist-p (name) "Predicate for schema existence" (query (:select (:exists (:select 'schema_name :from 'information_schema.schemata :where (:= 'schema_name (to-sql-name name t))))) :single)) (defun create-schema (schema) "Creating a non existing schema. If the schema exists an error is raised." ;;(format t "creating schema: ~a" schema) (execute (format nil "CREATE SCHEMA ~a" (s-sql:to-sql-name schema t)))) (defun drop-schema (schema &key (cascade nil)) "Drops an existing database schema 'schema'" (execute (format nil "DROP SCHEMA ~a ~:[~;CASCADE~]" (s-sql:to-sql-name schema t) cascade))) postmodern-20151031-git/postmodern/package.lisp000066400000000000000000000040421260073634400214060ustar00rootroot00000000000000(defpackage :postmodern (:use #-postmodern-use-mop :common-lisp #+postmodern-use-mop :closer-common-lisp :s-sql :cl-postgres) (:nicknames :pomo) #+postmodern-use-mop (:export #:dao-class #:dao-exists-p #:dao-keys #:query-dao #:select-dao #:get-dao #:do-query-dao #:do-select-dao #:with-column-writers #:insert-dao #:update-dao #:save-dao #:save-dao/transaction #:upsert-dao #:delete-dao #:make-dao #:define-dao-finalization #:dao-table-name #:dao-table-definition #:\!dao-def #:*ignore-unknown-columns*) (:export #:connect #:disconnect #:reconnect #:call-with-connection #:with-connection #:*database* #:connected-p #:database-connection #:connect-toplevel #:disconnect-toplevel #:clear-connection-pool #:*max-pool-size* #:*default-use-ssl* #:query #:execute #:doquery #:prepare #:defprepared #:defprepared-with-names #:sequence-next #:list-sequences #:sequence-exists-p #:list-tables #:table-exists-p #:table-description #:list-views #:view-exists-p #:*current-logical-transaction* #:with-transaction #:commit-transaction #:abort-transaction #:with-savepoint #:rollback-savepoint #:release-savepoint #:with-logical-transaction #:ensure-transaction #:abort-hooks #:commit-hooks #:db-null #:coalesce #:deftable #:*table-name* #:*table-symbol* #:create-table #:create-all-tables #:create-package-tables #:\!index #:\!unique-index #:\!foreign #:\!unique #:create-schema #:drop-schema #:list-schemata #:with-schema #:schema-exist-p #:set-search-path #:get-search-path ;; Reduced S-SQL interface #:sql #:sql-compile #:smallint #:bigint #:numeric #:real #:double-precision #:bytea #:text #:varchar #:*escape-sql-names-p* #:sql-escape-string #:sql-escape #:register-sql-operators #:sql-error ;; Condition type from cl-postgres #:database-error #:database-error-message #:database-error-code #:database-error-detail #:database-error-query #:database-error-cause #:database-connection-error #:database-error-constraint-name)) (in-package :postmodern) postmodern-20151031-git/postmodern/prepare.lisp000066400000000000000000000034341260073634400214550ustar00rootroot00000000000000(in-package :postmodern) (defun ensure-prepared (connection id query) "Make sure a statement has been prepared for this connection." (let ((meta (connection-meta connection))) (unless (gethash id meta) (setf (gethash id meta) t) (prepare-query connection (symbol-name id) query)))) (let ((next-id 0)) (defun next-statement-id () "Provide unique statement names." (incf next-id) (intern (with-standard-io-syntax (format nil "STATEMENT-~A" next-id)) :keyword))) (defun generate-prepared (function-form query format) "Helper macro for the following two functions." (destructuring-bind (reader result-form) (reader-for-format format) (let ((base `(exec-prepared *database* (symbol-name statement-id) params ,reader))) `(let ((statement-id (next-statement-id)) (query ,(real-query query))) (,@function-form (&rest params) (ensure-prepared *database* statement-id query) (,result-form ,base)))))) (defmacro prepare (query &optional (format :rows)) "Wraps a query into a function that will prepare it once for a connection, and then execute it with the given parameters. The query should contain a placeholder \($1, $2, etc) for every parameter." (generate-prepared '(lambda) query format)) (defmacro defprepared (name query &optional (format :rows)) "Like perpare, but gives the function a name instead of returning it." (generate-prepared `(defun ,name) query format)) (defmacro defprepared-with-names (name (&rest args) (query &rest query-args) &optional (format :rows)) "Like defprepared, but with lambda list for statement arguments." (let ((prepared-name (gensym "STATEMENT"))) `(progn (defprepared ,prepared-name ,query ,format) (defun ,name ,args (,prepared-name ,@query-args))))) postmodern-20151031-git/postmodern/query.lisp000066400000000000000000000130221260073634400211560ustar00rootroot00000000000000(in-package :postmodern) ;; Like alist-row-reader from cl-postgres, but converts the field ;; names to keywords (with underscores converted to dashes). (def-row-reader symbol-alist-row-reader (fields) (let ((symbols (map 'list (lambda (desc) (from-sql-name (field-name desc))) fields))) (loop :while (next-row) :collect (loop :for field :across fields :for symbol :in symbols :collect (cons symbol (next-field field)))))) ;; Like symbol-alist-row-reader, but return plist (def-row-reader symbol-plist-row-reader (fields) (let ((symbols (map 'list (lambda (desc) (from-sql-name (field-name desc))) fields))) (loop :while (next-row) :collect (loop :for field :across fields :for symbol :in symbols :collect symbol :collect (next-field field))))) ;; A row-reader for reading only a single column, and returning a list ;; of single values. (def-row-reader column-row-reader (fields) (assert (= (length fields) 1)) (loop :while (next-row) :collect (next-field (elt fields 0)))) (defparameter *result-styles* '((:none ignore-row-reader all-rows) (:lists list-row-reader all-rows) (:list list-row-reader single-row) (:rows list-row-reader all-rows) (:row list-row-reader single-row) (:alists symbol-alist-row-reader all-rows) (:alist symbol-alist-row-reader single-row) (:str-alists alist-row-reader all-rows) (:str-alist alist-row-reader single-row) (:plists symbol-plist-row-reader all-rows) (:plist symbol-plist-row-reader single-row) (:column column-row-reader all-rows) (:single column-row-reader single-row) (:single! column-row-reader single-row!)) "Mapping from keywords identifying result styles to the row-reader that should be used and whether all values or only one value should be returned.") (defun dao-spec-for-format (format) (if (and (consp format) (eq :dao (car format))) (cdr format))) (defun reader-for-format (format) (let ((format-spec (cdr (assoc format *result-styles*)))) (if format-spec `(',(car format-spec) ,@(cdr format-spec)) (destructuring-bind (class &optional result) (dao-spec-for-format format) (unless class (error "~S is not a valid result style." format)) (let ((class-name (gensym))) (list `(let ((,class-name (find-class ',class))) (unless (class-finalized-p ,class-name) (finalize-inheritance ,class-name)) (dao-row-reader ,class-name)) (if (eq result :single) 'single-row 'all-rows))))))) (defmacro all-rows (form) form) (defmacro single-row (form) `(multiple-value-bind (rows affected) ,form (if affected (values (car rows) affected) (car rows)))) (defmacro single-row! (form) `(multiple-value-bind (rows affected) ,form (unless (= (length rows) 1) (error 'database-error :message (format nil "Query for a single row returned ~a rows." (length rows)))) (if affected (values (car rows) affected) (car rows)))) (defun real-query (query) "Used for supporting both plain string queries and S-SQL constructs. Looks at the argument at compile-time and wraps it in (sql ...) if it looks like an S-SQL query." (if (and (consp query) (keywordp (first query))) `(sql ,query) query)) (defmacro query (query &rest args/format) "Execute a query, optionally with arguments to put in the place of $X elements. If one of the arguments is a known result style or a class name, it specifies the format in which the results should be returned." (let* ((format :rows) (args (loop :for arg :in args/format :if (or (dao-spec-for-format arg) (assoc arg *result-styles*)) :do (setf format arg) :else :collect arg))) (destructuring-bind (reader result-form) (reader-for-format format) (let ((base (if args `(progn (prepare-query *database* "" ,(real-query query)) (exec-prepared *database* "" (list ,@args) ,reader)) `(exec-query *database* ,(real-query query) ,reader)))) `(,result-form ,base))))) (defmacro execute (query &rest args) "Execute a query, ignore the results." `(let ((rows (nth-value 1 (query ,query ,@args :none)))) (if rows (values rows rows) 0))) (defmacro doquery (query (&rest names) &body body) "Iterate over the rows in the result of a query, binding the given names to the results and executing body for every row. Query can be a string, an s-sql query, or a list starting with one of those, followed by the arguments to parameterize the query with." (let* ((fields (gensym)) (query-name (gensym)) args (reader-expr `(row-reader (,fields) (unless (= ,(length names) (length ,fields)) (error "Number of field names does not match number of selected fields in query ~A." ,query-name)) (loop :while (next-row) :do (let ,(loop :for i :from 0 :for name :in names :collect `(,name (next-field (elt ,fields ,i)))) ,@body))))) (when (and (consp query) (not (keywordp (first query)))) (setf args (cdr query) query (car query))) (if args `(let ((,query-name ,(real-query query))) (prepare-query *database* "" ,query-name) (exec-prepared *database* "" (list ,@args) ,reader-expr)) `(let ((,query-name ,(real-query query))) (exec-query *database* ,query-name ,reader-expr))))) postmodern-20151031-git/postmodern/table.lisp000066400000000000000000000440351260073634400211100ustar00rootroot00000000000000(in-package :postmodern) (defclass dao-class (standard-class) ((direct-keys :initarg :keys :initform nil :reader direct-keys) (effective-keys :reader dao-keys) (table-name) (column-map :reader dao-column-map)) (:documentation "Metaclass for database-access-object classes.")) (defmethod dao-keys :before ((class dao-class)) (unless (class-finalized-p class) (finalize-inheritance class))) (defmethod validate-superclass ((class dao-class) (super-class standard-class)) t) (defmethod dao-keys ((class-name symbol)) (dao-keys (find-class class-name))) (defmethod dao-keys (dao) (mapcar #'(lambda (slot) (slot-value dao slot)) (dao-keys (class-of dao)))) (defun dao-column-slots (class) "Enumerate the slots in a class that refer to table rows." (mapcar 'slot-column (remove-if-not (lambda (x) (typep x 'effective-column-slot)) (class-slots class)))) (defun dao-column-fields (class) (mapcar 'slot-definition-name (dao-column-slots class))) (defun dao-table-name (class) (when (symbolp class) (setf class (find-class class))) (if (slot-boundp class 'table-name) (slot-value class 'table-name) (class-name class))) (defmethod shared-initialize :before ((class dao-class) slot-names &key table-name &allow-other-keys) (declare (ignore slot-names)) (setf (slot-value class 'direct-keys) nil) (if table-name (setf (slot-value class 'table-name) (if (symbolp (car table-name)) (car table-name) (intern (car table-name)))) (slot-makunbound class 'table-name))) (defun dao-superclasses (class) "Build a list of superclasses of a given class that are DAO classes." (let ((found ())) (labels ((explore (class) (when (typep class 'dao-class) (pushnew class found)) (mapc #'explore (class-direct-superclasses class)))) (explore class) found))) (defmethod finalize-inheritance :after ((class dao-class)) "Building a row reader and a set of methods can only be done after inheritance has been finalised." ;; The effective set of keys of a class is the union of its keys and ;; the keys of all its superclasses. (setf (slot-value class 'effective-keys) (reduce 'union (mapcar 'direct-keys (dao-superclasses class)))) (unless (every (lambda (x) (member x (dao-column-fields class))) (dao-keys class)) (error "Class ~A has a key that is not also a slot." (class-name class))) (build-dao-methods class)) (defclass direct-column-slot (standard-direct-slot-definition) ((col-type :initarg :col-type :reader column-type) (col-default :initarg :col-default :reader column-default) (ghost :initform nil :initarg :ghost :reader ghost) (sql-name :reader slot-sql-name)) (:documentation "Type of slots that refer to database columns.")) (defmethod shared-initialize :after ((slot direct-column-slot) slot-names &key col-type col-default (col-name nil col-name-p) &allow-other-keys) (declare (ignore slot-names)) (setf (slot-value slot 'sql-name) (to-sql-name (if col-name-p col-name (slot-definition-name slot)))) ;; The default for nullable columns defaults to :null. (when (and (null col-default) (consp col-type) (eq (car col-type) 'or) (member 'db-null col-type) (= (length col-type) 3)) (setf (slot-value slot 'col-default) :null))) (defmethod direct-slot-definition-class ((class dao-class) &key column col-type &allow-other-keys) "Slots that have a :col-type option are column-slots." (if (or column col-type) (find-class 'direct-column-slot) (call-next-method))) (defparameter *direct-column-slot* nil "This is used to communicate the fact that a slot is a column to effective-slot-definition-class.") (defclass effective-column-slot (standard-effective-slot-definition) ((direct-slot :initform *direct-column-slot* :reader slot-column))) (defmethod compute-effective-slot-definition ((class dao-class) name direct-slot-definitions) (declare (ignore name)) (flet ((is-column (slot) (typep slot 'direct-column-slot))) (let ((*direct-column-slot* (find-if #'is-column direct-slot-definitions))) #+(or) ;; Things seem to work without this check. Removed for now. (when (and *direct-column-slot* (not (every #'is-column direct-slot-definitions))) (error "Slot ~a in class ~a is both a column slot and a regular slot." name class)) (call-next-method)))) (defmethod effective-slot-definition-class ((class dao-class) &rest initargs) (declare (ignore initargs)) (if *direct-column-slot* (find-class 'effective-column-slot) (call-next-method))) (defgeneric dao-exists-p (dao) (:documentation "Return a boolean indicating whether the given dao exists in the database.")) (defgeneric insert-dao (dao) (:documentation "Insert the given object into the database.")) (defgeneric update-dao (dao) (:documentation "Update the object's representation in the database with the values in the given instance.")) (defgeneric delete-dao (dao) (:documentation "Delete the given dao from the database.")) (defgeneric upsert-dao (dao) (:documentation "Update or insert the given dao. If its primary key is already in the database and all slots are bound, an update will occur. Otherwise it tries to insert it.")) (defgeneric get-dao (type &rest args) (:method ((class-name symbol) &rest args) (let ((class (find-class class-name))) (if (class-finalized-p class) (error "Class ~a has no key slots." (class-name class)) (finalize-inheritance class)) (apply 'get-dao class-name args))) (:documentation "Get the object corresponding to the given primary key, or return nil if it does not exist.")) (defgeneric make-dao (type &rest args &key &allow-other-keys) (:method ((class-name symbol) &rest args &key &allow-other-keys) (let ((class (find-class class-name))) (apply 'make-dao class args))) (:method ((class dao-class) &rest args &key &allow-other-keys) (unless (class-finalized-p class) (finalize-inheritance class)) (let ((instance (apply #'make-instance class args))) (insert-dao instance))) (:documentation "Make the instance of the given class and insert it into the database")) (defmacro define-dao-finalization (((dao-name class) &rest keyword-args) &body body) (let ((args-name (gensym))) `(defmethod make-dao :around ((class (eql ',class)) &rest ,args-name &key ,@keyword-args &allow-other-keys) (declare (ignorable ,args-name)) (let ((,dao-name (call-next-method))) ,@body (update-dao ,dao-name))))) (defgeneric fetch-defaults (object) (:documentation "Used to fetch the default values of an object on creation.")) (defun %eval (code) (funcall (compile nil `(lambda () ,code)))) (defun build-dao-methods (class) "Synthesise a number of methods for a newly defined DAO class. \(Done this way because some of them are not defined in every situation, and each of them needs to close over some pre-computed values.)" (setf (slot-value class 'column-map) (mapcar (lambda (s) (cons (slot-sql-name s) (slot-definition-name s))) (dao-column-slots class))) (%eval `(let* ((fields (dao-column-fields ,class)) (key-fields (dao-keys ,class)) (ghost-slots (remove-if-not 'ghost (dao-column-slots ,class))) (ghost-fields (mapcar 'slot-definition-name ghost-slots)) (value-fields (remove-if (lambda (x) (or (member x key-fields) (member x ghost-fields))) fields)) (table-name (dao-table-name ,class))) (labels ((field-sql-name (field) (make-symbol (car (find field (slot-value ,class 'column-map) :key #'cdr :test #'eql)))) (test-fields (fields) `(:and ,@(loop :for field :in fields :collect (list := (field-sql-name field) '$$)))) (set-fields (fields) (loop :for field :in fields :append (list (field-sql-name field) '$$))) (slot-values (object &rest slots) (loop :for slot :in (apply 'append slots) :collect (slot-value object slot)))) ;; When there is no primary key, a lot of methods make no sense. (when key-fields (let ((tmpl (sql-template `(:select (:exists (:select t :from ,table-name :where ,(test-fields key-fields))))))) (defmethod dao-exists-p ((object ,class)) (and (every (lambda (s) (slot-boundp object s)) key-fields) (query (apply tmpl (slot-values object key-fields)) :single)))) ;; When all values are primary keys, updating makes no sense. (when value-fields (let ((tmpl (sql-template `(:update ,table-name :set ,@(set-fields value-fields) :where ,(test-fields key-fields))))) (defmethod update-dao ((object ,class)) (when (zerop (execute (apply tmpl (slot-values object value-fields key-fields)))) (error "Updated row does not exist.")) object) (defmethod upsert-dao ((object ,class)) (handler-case (if (zerop (execute (apply tmpl (slot-values object value-fields key-fields)))) (values (insert-dao object) t) (values object nil)) (unbound-slot () (values (insert-dao object) t)))))) (let ((tmpl (sql-template `(:delete-from ,table-name :where ,(test-fields key-fields))))) (defmethod delete-dao ((object ,class)) (execute (apply tmpl (slot-values object key-fields))))) (let ((tmpl (sql-template `(:select * :from ,table-name :where ,(test-fields key-fields))))) (defmethod get-dao ((type (eql (class-name ,class))) &rest keys) (car (exec-query *database* (apply tmpl keys) (dao-row-reader ,class)))))) (defmethod insert-dao ((object ,class)) (let (bound unbound) (loop :for field :in fields :do (if (slot-boundp object field) (push field bound) (push field unbound))) (let* ((values (mapcan (lambda (x) (list (field-sql-name x) (slot-value object x))) (remove-if (lambda (x) (member x ghost-fields)) bound) )) (returned (query (sql-compile `(:insert-into ,table-name :set ,@values ,@(when unbound (cons :returning unbound)))) :row))) (when unbound (loop :for value :in returned :for field :in unbound :do (setf (slot-value object field) value))))) object) (let* ((defaulted-slots (remove-if-not (lambda (x) (slot-boundp x 'col-default)) (dao-column-slots ,class))) (defaulted-names (mapcar 'slot-definition-name defaulted-slots)) (default-values (mapcar 'column-default defaulted-slots))) (if defaulted-slots (defmethod fetch-defaults ((object ,class)) (let (names defaults) ;; Gather unbound slots and their default expressions. (loop :for slot-name :in defaulted-names :for default :in default-values :do (unless (slot-boundp object slot-name) (push slot-name names) (push default defaults))) ;; If there are any unbound, defaulted slots, fetch their content. (when names (loop :for value :in (query (sql-compile (cons :select defaults)) :list) :for slot-name :in names :do (setf (slot-value object slot-name) value))))) (defmethod fetch-defaults ((object ,class)) nil))) (defmethod shared-initialize :after ((object ,class) slot-names &key (fetch-defaults nil) &allow-other-keys) (declare (ignore slot-names)) (when fetch-defaults (fetch-defaults object))))))) (defparameter *custom-column-writers* nil "A hook for locally overriding/adding behaviour to DAO row readers. Should be an alist mapping strings (column names) to symbols or functions. Symbols are interpreted as slot names that values should be written to, functions are called with the new object and the value as arguments.") (defmacro with-column-writers ((&rest defs) &body body) `(let ((*custom-column-writers* (append (list ,@(loop :for (field writer) :on defs :by #'cddr :collect `(cons (to-sql-name ,field) ,writer))) *custom-column-writers*))) ,@body)) (defparameter *ignore-unknown-columns* nil) (defun dao-from-fields (class column-map query-fields result-next-field-generator-fn) (let ((instance (allocate-instance class))) (loop :for field :across query-fields :for writer := (cdr (assoc (field-name field) column-map :test #'string=)) :do (etypecase writer (null (if *ignore-unknown-columns* (funcall result-next-field-generator-fn field) (error "No slot named ~a in class ~a. DAO out of sync with table, or incorrect query used." (field-name field) (class-name class)))) (symbol (setf (slot-value instance writer) (funcall result-next-field-generator-fn field))) (function (funcall writer instance (funcall result-next-field-generator-fn field))))) (initialize-instance instance) instance)) (defun dao-row-reader (class) "Defines a row-reader for objects of a given class." (row-reader (query-fields) (let ((column-map (append *custom-column-writers* (dao-column-map class)))) (loop :while (next-row) :collect (dao-from-fields class column-map query-fields #'next-field))))) (defun save-dao (dao) "Try to insert the content of a DAO. If this leads to a unique key violation, update it instead." (handler-case (progn (insert-dao dao) t) (cl-postgres-error:unique-violation () (update-dao dao) nil))) (defun save-dao/transaction (dao) (handler-case (with-savepoint save-dao/transaction (insert-dao dao) t) (cl-postgres-error:unique-violation () (update-dao dao) nil))) (defun query-dao% (type query row-reader &rest args) (let ((class (find-class type))) (unless (class-finalized-p class) (finalize-inheritance class)) (if args (progn (prepare-query *database* "" query) (exec-prepared *database* "" args row-reader)) (exec-query *database* query row-reader)))) (defmacro query-dao (type query &rest args) "Execute a query and return the result as daos of the given type. The fields returned by the query must match the slots of the dao, both by type and by name." `(query-dao% ,type ,(real-query query) (dao-row-reader (find-class ,type)) ,@args)) (defmacro dao-row-reader-with-body ((type type-var) &body body) (let ((fields (gensym)) (column-map (gensym))) `(row-reader (,fields) (let ((,column-map (append *custom-column-writers* (dao-column-map (find-class ,type))))) (loop :while (next-row) :do (let ((,type-var (dao-from-fields (find-class ,type) ,column-map ,fields #'next-field))) ,@body)))))) (defmacro do-query-dao (((type type-var) query) &body body) "Like query-dao, but rather than returning a list of results, executes BODY once for each result, with TYPE-VAR bound to the DAO representing that result." (let (args) (when (and (consp query) (not (keywordp (first query)))) (setf args (cdr query) query (car query))) `(query-dao% ,type ,(real-query query) (dao-row-reader-with-body (,type ,type-var) ,@body) ,@args))) (defun generate-dao-query (type &optional (test t) ordering) (flet ((check-string (x) (if (stringp x) `(:raw ,x) x))) (let ((query `(:select '* :from (dao-table-name (find-class ,type)) :where ,(check-string test)))) (when ordering (setf query `(:order-by ,query ,@(mapcar #'check-string ordering)))) query))) (defmacro select-dao (type &optional (test t) &rest ordering) "Select daos for the rows in its table for which the given test holds, order them by the given criteria." `(query-dao% ,type (sql ,(generate-dao-query type test ordering)) (dao-row-reader (find-class ,type)))) (defmacro do-select-dao (((type type-var) &optional (test t) &rest ordering) &body body) "Like select-dao, but rather than returning a list of results, executes BODY once for each result, with TYPE-VAR bound to the DAO representing that result." `(query-dao% ,type (sql ,(generate-dao-query type test ordering)) (dao-row-reader-with-body (,type ,type-var) ,@body))) (defun dao-table-definition (table) "Generate the appropriate CREATE TABLE query for this class." (unless (typep table 'dao-class) (setf table (find-class table))) (unless (class-finalized-p table) (finalize-inheritance table)) (sql-compile `(:create-table ,(dao-table-name table) ,(loop :for slot :in (dao-column-slots table) :unless (ghost slot) :collect `(,(slot-definition-name slot) :type ,(column-type slot) ,@(when (slot-boundp slot 'col-default) `(:default ,(column-default slot))))) ,@(when (dao-keys table) `((:primary-key ,@(dao-keys table))))))) postmodern-20151031-git/postmodern/tests.lisp000066400000000000000000000265571260073634400211740ustar00rootroot00000000000000(defpackage :postmodern-tests (:use :common-lisp :Eos :postmodern :simple-date)) (in-package :postmodern-tests) (defvar *test-connection* '("test" "test" "" "localhost")) ;; Adjust the above to some db/user/pass/host combination that refers ;; to a valid postgresql database in which no table named test_data ;; currently exists. Then after loading the file, run the tests with ;; (Eos:run! :postmodern) (def-suite :postmodern) (in-suite :postmodern) (defmacro with-test-connection (&body body) `(with-connection *test-connection* ,@body)) (defmacro protect (&body body) `(unwind-protect (progn ,@(butlast body)) ,(car (last body)))) (test connect-sanely (with-test-connection (is (not (null *database*))))) (test connection-pool (let ((pooled (apply 'connect (append *test-connection* '(:pooled-p t))))) (disconnect pooled) (let ((pooled* (apply 'connect (append *test-connection* '(:pooled-p t))))) (is (eq pooled pooled*)) (disconnect pooled*)) (clear-connection-pool) (let ((pooled* (apply 'connect (append *test-connection* '(:pooled-p t))))) (is (not (eq pooled pooled*))) (disconnect pooled*)) (clear-connection-pool))) (test reconnect (with-test-connection (disconnect *database*) (is (not (connected-p *database*))) (reconnect *database*) (is (connected-p *database*)))) (test simple-query (with-test-connection (destructuring-bind (a b c d e f) (query (:select 22 (:type 44.5 double-precision) "abcde" t (:type 9/2 (numeric 5 2)) (:[] #("A" "B") 2)) :row) (is (eql a 22)) (is (eql b 44.5d0)) (is (string= c "abcde")) (is (eql d t)) (is (eql e 9/2)) (is (equal f "B"))))) (test reserved-words (with-test-connection (is (= (query (:select '* :from (:as (:select (:as 1 'as)) 'where) :where (:= 'where.as 1)) :single!) 1)))) (test time-types (with-test-connection (is (time= (query (:select (:type (encode-date 1980 2 1) date)) :single) (encode-date 1980 2 1))) (is (time= (query (:select (:type (encode-timestamp 2040 3 19 12 15 0 2) timestamp)) :single) (encode-timestamp 2040 3 19 12 15 0 2))) (is (time= (query (:select (:type (encode-interval :month -1 :hour 24) interval)) :single) (encode-interval :month -1 :hour 24))))) (test table (with-test-connection (execute (:create-table test-data ((a :type integer :primary-key t) (b :type real) (c :type (or text db-null))) (:unique c))) (protect (is (table-exists-p 'test-data)) (execute (:insert-into 'test-data :set 'a 1 'b 5.4 'c "foobar")) (execute (:insert-into 'test-data :set 'a 2 'b 88 'c :null)) (is (equal (query (:order-by (:select '* :from 'test-data) 'a)) '((1 5.4 "foobar") (2 88.0 :null)))) (execute (:drop-table 'test-data))) (is (not (table-exists-p 'test-data))))) (test sequence (with-test-connection (execute (:create-sequence 'my-seq :increment 4 :start 10)) (protect (is (sequence-exists-p 'my-seq)) (is (= (sequence-next 'my-seq) 10)) (is (= (sequence-next 'my-seq) 14)) (execute (:drop-sequence 'my-seq))) (is (not (sequence-exists-p 'my-seq))))) (test prepare (with-test-connection (let ((select-int (prepare (:select (:type '$1 integer)) :single)) (byte-arr (make-array 10 :element-type '(unsigned-byte 8) :initial-element 10)) (select-bytes (prepare (:select (:type '$1 bytea)) :single))) (is (= (funcall select-int 10) 10)) (is (= (funcall select-int -40) -40)) (is (eq (funcall select-int :null) :null)) (is (equalp (funcall select-bytes byte-arr) byte-arr))))) (test doquery (with-test-connection (doquery (:select 55 "foobar") (number string) (is (= number 55)) (is (string= string "foobar"))))) (test doquery-params (with-test-connection (doquery ("select $1::integer + 10" 20) (answer) (is (= answer 30))))) (test transaction (with-test-connection (execute (:create-table test-data ((value :type integer)))) (protect (ignore-errors (with-transaction () (execute (:insert-into 'test-data :set 'value 2)) (error "no wait"))) (is (= 0 (length (query (:select '* :from 'test-data))))) (ignore-errors (with-transaction (transaction) (execute (:insert-into 'test-data :set 'value 2)) (commit-transaction transaction) (error "no wait!!"))) (is (= 1 (length (query (:select '* :from 'test-data))))) (with-transaction (transaction) (execute (:insert-into 'test-data :set 'value 44)) (abort-transaction transaction)) (is (= 1 (length (query (:select '* :from 'test-data))))) (execute (:drop-table 'test-data))))) (test logical-transaction (with-test-connection (protect (execute (:create-table test-data ((value :type integer)))) (with-logical-transaction () (execute (:insert-into 'test-data :set 'value 1)) (ignore-errors (with-logical-transaction () (execute (:insert-into 'test-data :set 'value 2)) (error "fail here")))) (is-true (query (:select '* :from 'test-data :where (:= 'value 1)))) (is-false (query (:select '* :from 'test-data :where (:= 'value 2)))) (execute (:drop-table 'test-data))))) (test transaction-commit-hooks (with-test-connection (protect (execute (:create-table test-data ((value :type integer)))) (with-logical-transaction (transaction-1) (execute (:insert-into 'test-data :set 'value 1)) (with-logical-transaction (transaction-2) (push (lambda () (execute (:insert-into 'test-data :set 'value 3))) (commit-hooks transaction-2)) (push (lambda () (execute (:insert-into 'test-data :set 'value 4))) (commit-hooks transaction-1)) (execute (:insert-into 'test-data :set 'value 2)))) (is (= 4 (length (query (:select '* :from 'test-data))))) (execute (:drop-table 'test-data))))) (test transaction-abort-hooks (with-test-connection (protect (execute (:create-table test-data ((value :type integer)))) (with-logical-transaction (transaction-1) (execute (:insert-into 'test-data :set 'value 1)) (ignore-errors (with-logical-transaction (transaction-2) (push (lambda () (execute (:insert-into 'test-data :set 'value 3))) (abort-hooks transaction-2)) (push (lambda () (execute (:insert-into 'test-data :set 'value 4))) (abort-hooks transaction-1)) (error "no wait") (execute (:insert-into 'test-data :set 'value 2))))) (is (= 2 (length (query (:select '* :from 'test-data))))) (execute (:drop-table 'test-data))))) (test ensure-transaction (with-test-connection (with-transaction () (ensure-transaction (is (eql postmodern::*transaction-level* 1)))) (is (eql postmodern::*transaction-level* 0)) (ensure-transaction (is (eql postmodern::*transaction-level* 1))))) (defclass test-data () ((id :col-type serial :initarg :id :accessor test-id) (a :col-type (or (varchar 100) db-null) :initarg :a :accessor test-a) (b :col-type boolean :col-default nil :initarg :b :accessor test-b)) (:metaclass dao-class) (:table-name dao-test) (:keys id)) (test dao-class (with-test-connection (execute (dao-table-definition 'test-data)) (protect (is (member :dao-test (list-tables))) (is (null (get-dao 'test-data 1))) (let ((dao (make-instance 'test-data :a "quux"))) (insert-dao dao) (is (eql (test-id dao) 1)) (is (dao-exists-p dao))) (let ((dao (get-dao 'test-data 1))) (is (not (null dao))) (setf (test-b dao) t) (update-dao dao)) (let ((dao (get-dao 'test-data 1))) (is (not (null dao))) (is (string= (test-a dao) "quux")) (is (eq (test-b dao) t)) (delete-dao dao)) (is (not (select-dao 'test-data))) (execute (:drop-table 'dao-test))))) (test save-dao (with-test-connection (execute (dao-table-definition 'test-data)) (protect (let ((dao (make-instance 'test-data :a "quux"))) (is (save-dao dao)) (setf (test-a dao) "bar") (is (not (save-dao dao))) (is (equal (test-a (get-dao 'test-data (test-id dao))) "bar")) (signals database-error (with-transaction () (save-dao dao))) (with-transaction () (is (not (save-dao/transaction dao))))) (execute (:drop-table 'dao-test))))) (defclass test-oid () ((oid :col-type integer :ghost t :accessor test-oid) (a :col-type string :initarg :a :accessor test-a) (b :col-type string :initarg :b :accessor test-b)) (:metaclass dao-class) (:keys a)) (test dao-class-oid (with-test-connection (execute (concatenate 'string (dao-table-definition 'test-oid) "with (oids=true)")) (protect (let ((dao (make-instance 'test-oid :a "a" :b "b"))) (insert-dao dao) (is-true (integerp (test-oid dao))) (let ((back (get-dao 'test-oid "a"))) (is (test-oid dao) (test-oid back)) (setf (test-b back) "c") (update-dao back)) (is (test-b (get-dao 'test-oid "a")) "c")) (execute (:drop-table 'test-oid))))) (test notification (with-test-connection (execute (:listen 'foo)) (with-test-connection (execute (:notify 'foo))) (is (cl-postgres:wait-for-notification *database*) "foo"))) (defclass test-col-name () ((a :col-type string :col-name aa :initarg :a :accessor test-a) (b :col-type string :col-name bb :initarg :b :accessor test-b) (c :col-type string :initarg :c :accessor test-c)) (:metaclass dao-class) (:keys a)) (test dao-class-col-name (with-test-connection (execute "CREATE TEMPORARY TABLE test_col_name (aa text primary key, bb text not null, c text not null)") (let ((o (make-instance 'test-col-name :a "1" :b "2" :c "3"))) (save-dao o) (let ((oo (get-dao 'test-col-name "1"))) (is (string= "1" (test-a oo))) (is (string= "2" (test-b oo))) (is (string= "3" (test-c oo))))) (let ((o (get-dao 'test-col-name "1"))) (setf (test-b o) "b") (update-dao o)) (is (string= "1" (test-a (get-dao 'test-col-name "1")))) (is (string= "b" (test-b (get-dao 'test-col-name "1")))) (is (string= "3" (test-c (get-dao 'test-col-name "1")))))) ;; create two tables with the same name in two different ;; namespaces. (test namespace (with-test-connection (is (not (table-exists-p 'test-uniq))) (execute (:create-table test-uniq ((value :type integer)))) (is (table-exists-p 'test-uniq)) (is (not (schema-exist-p 'uniq))) (with-schema ('uniq :if-not-exist :create) (is (schema-exist-p 'uniq)) (is (not (table-exists-p 'test-uniq))) (execute (:create-table test-uniq ((value :type integer)))) (is (table-exists-p 'test-uniq)) (execute (:drop-table 'test-uniq))) (is (schema-exist-p 'uniq)) (drop-schema 'uniq) (is (not (schema-exist-p 'uniq))) (execute (:drop-table 'test-uniq)))) (test arrays (with-test-connection (execute (:create-table test-data ((a :type integer[])))) (protect (is (table-exists-p 'test-data)) (execute (:insert-into 'test-data :set 'a (vector 3 4 5))) (execute (:insert-into 'test-data :set 'a #())) (execute (:drop-table 'test-data))) (is (not (table-exists-p 'test-data))))) postmodern-20151031-git/postmodern/transaction.lisp000066400000000000000000000134461260073634400223500ustar00rootroot00000000000000(in-package :postmodern) (defparameter *transaction-level* 0) (defparameter *current-logical-transaction* nil) (defclass transaction-handle () ((open-p :initform t :accessor transaction-open-p) (connection :initform *database* :reader transaction-connection) (commit-hooks :initform nil :accessor commit-hooks) (abort-hooks :initform nil :accessor abort-hooks)) (:documentation "Simple box type for storing the status and the associated database connection of a transaction. When open-p is nil, the transaction has been aborted or committed. commit-hooks and abort-hooks hold lists of functions (which should require no arguments) to be executed at commit and abort time, respectively.")) (defun call-with-transaction (body) (let ((transaction (make-instance 'transaction-handle))) (execute "BEGIN") (unwind-protect (multiple-value-prog1 (let ((*transaction-level* (1+ *transaction-level*)) (*current-logical-transaction* transaction)) (funcall body transaction)) (commit-transaction transaction)) (abort-transaction transaction)))) (defmacro with-transaction ((&optional name) &body body) "Execute the body within a database transaction, committing when the body exits normally, and aborting otherwise. An optional name can be given to the transaction, which can be used to force a commit or abort before the body unwinds." (let ((transaction-name (or name (gensym "anonymous-transaction")))) `(call-with-transaction (lambda (,transaction-name) (declare (ignorable ,transaction-name)) ,@body)))) (defun abort-transaction (transaction) "Immediately abort an open transaction." (when (transaction-open-p transaction) (let ((*database* (transaction-connection transaction))) (execute "ABORT")) (setf (transaction-open-p transaction) nil) (mapc #'funcall (abort-hooks transaction)))) (defun commit-transaction (transaction) "Immediately commit an open transaction." (when (transaction-open-p transaction) (let ((*database* (transaction-connection transaction))) (execute "COMMIT")) (setf (transaction-open-p transaction) nil) (mapc #'funcall (commit-hooks transaction)))) (defclass savepoint-handle (transaction-handle) ((name :initform (error "Savepoint name is not provided.") :initarg :name :reader savepoint-name) (open-p :initform t :accessor savepoint-open-p) (connection :initform *database* :reader savepoint-connection)) (:documentation "Simple box type for storing the state and the associated database connection of a savepoint.")) (defun call-with-savepoint (name body) (let ((savepoint (make-instance 'savepoint-handle :name (to-sql-name name)))) (execute (format nil "SAVEPOINT ~A" (savepoint-name savepoint))) (unwind-protect (multiple-value-prog1 (let ((*transaction-level* (1+ *transaction-level*)) (*current-logical-transaction* savepoint)) (funcall body savepoint)) (release-savepoint savepoint)) (rollback-savepoint savepoint)))) (defmacro with-savepoint (name &body body) "Execute the body within a savepoint, releasing savepoint when the body exits normally, and rolling back otherwise. NAME is both the variable that can be used to release or rolled back before the body unwinds, and the SQL name of the savepoint." `(call-with-savepoint ',name (lambda (,name) (declare (ignorable ,name)) ,@body))) (defun rollback-savepoint (savepoint) "Immediately roll back a savepoint, aborting it results." (when (savepoint-open-p savepoint) (let ((*database* (savepoint-connection savepoint))) (execute (format nil "ROLLBACK TO SAVEPOINT ~A" (savepoint-name savepoint)))) (setf (savepoint-open-p savepoint) nil) (mapc #'funcall (abort-hooks savepoint)))) (defun release-savepoint (savepoint) "Immediately release a savepoint, commiting its results." (when (savepoint-open-p savepoint) (let ((*database* (savepoint-connection savepoint))) (execute (format nil "RELEASE SAVEPOINT ~A" (savepoint-name savepoint)))) (setf (transaction-open-p savepoint) nil) (mapc #'funcall (commit-hooks savepoint)))) (defun call-with-logical-transaction (name body) (if (zerop *transaction-level*) (call-with-transaction body) (call-with-savepoint name body))) (defmacro with-logical-transaction ((&optional (name nil name-p)) &body body) "Executes the body within a with-transaction (if no transaction is already in progress) or a with-savepoint (if one is), binding the transaction or savepoint to NAME (if supplied)" (let* ((effective-name (if name-p name (gensym))) (effective-body (if name-p `(lambda (,name) ,@body) `(lambda (,effective-name) (declare (ignore ,effective-name)) ,@body)))) `(call-with-logical-transaction ',effective-name ,effective-body))) (defmethod abort-logical-transaction ((savepoint savepoint-handle)) (rollback-savepoint savepoint)) (defmethod abort-logical-transaction ((transaction transaction-handle)) (abort-transaction transaction)) (defmethod commit-logical-transaction ((savepoint savepoint-handle)) (commit-transaction savepoint)) (defmethod commit-logical-transaction ((transaction transaction-handle)) (commit-transaction transaction)) (defun call-with-ensured-transaction (thunk) (if (zerop *transaction-level*) (with-transaction () (funcall thunk)) (funcall thunk))) (defmacro ensure-transaction (&body body) "Executes body within a with-transaction form if and only if no transaction is already in progress." `(call-with-ensured-transaction (lambda () ,@body))) postmodern-20151031-git/postmodern/util.lisp000066400000000000000000000073301260073634400207730ustar00rootroot00000000000000(in-package :postmodern) (defun to-identifier (name) "Used to allow both strings and symbols as identifier - converts symbols to string with the S-SQL rules." (if (stringp name) name (to-sql-name name))) (defun sequence-next (sequence) "Shortcut for getting the next value from a sequence." (query (:select (:nextval (to-identifier sequence))) :single)) (defmacro make-list-query (relkind) "Helper macro for the functions that list tables, sequences, and views." `(sql (:select 'relname :from 'pg-catalog.pg-class :inner-join 'pg-catalog.pg-namespace :on (:= 'relnamespace 'pg-namespace.oid) :where (:and (:= 'relkind ,relkind) (:not-in 'nspname (:set "pg_catalog" "pg_toast")) (:pg-catalog.pg-table-is-visible 'pg-class.oid))))) (defmacro make-exists-query (relkind name) "Helper macro for the functions that check whether an object exists." `(sql (:select (:exists (:select 'relname :from 'pg_catalog.pg_class :inner-join 'pg_catalog.pg_namespace :on (:= 'pg_class.relnamespace 'pg_namespace.oid) :where (:and (:= 'pg_class.relkind ,relkind) (:= 'pg_namespace.nspname (:any* (:current_schemas nil))) (:= 'pg_class.relname (to-identifier ,name)))))))) (defun list-tables (&optional strings-p) "Return a list of the tables in a database. Turn them into keywords if strings-p is not true." (let ((result (query (make-list-query "r") :column))) (if strings-p result (mapcar 'from-sql-name result)))) (defun table-exists-p (table) "Check whether a table exists. Takes either a string or a symbol for the table name." (query (make-exists-query "r" table) :single)) (defun list-sequences (&optional strings-p) "Return a list of the sequences in a database. Turn them into keywords if strings-p is not true." (let ((result (query (make-list-query "S") :column))) (if strings-p result (mapcar 'from-sql-name result)))) (defun sequence-exists-p (sequence) "Check whether a sequence exists. Takes either a string or a symbol for the sequence name." (query (make-exists-query "S" sequence) :single)) (defun list-views (&optional strings-p) "Return a list of the views in a database. Turn them into keywords if strings-p is not true." (let ((result (query (make-list-query "v") :column))) (if strings-p result (mapcar 'from-sql-name result)))) (defun view-exists-p (view) "Check whether a view exists. Takes either a string or a symbol for the view name." (query (make-exists-query "v" view) :single)) (defun table-description (table &optional schema-name) "Return a list of (name type null-allowed) lists for the fields of a table. If SCHEMA-NAME is specified, only fields from that schema are returned." (let ((schema-test (if schema-name (sql (:= 'pg-namespace.nspname schema-name)) "true"))) (mapcar #'butlast (query (:order-by (:select 'attname 'typname (:not 'attnotnull) 'attnum :distinct :from 'pg-catalog.pg-attribute :inner-join 'pg-catalog.pg-type :on (:= 'pg-type.oid 'atttypid) :inner-join 'pg-catalog.pg-class :on (:and (:= 'pg-class.oid 'attrelid) (:= 'pg-class.relname (to-identifier table))) :inner-join 'pg-catalog.pg-namespace :on (:= 'pg-namespace.oid 'pg-class.relnamespace) :where (:and (:> 'attnum 0) (:raw schema-test))) 'attnum))))) (defun coalesce (&rest args) (some (lambda (x) (if (eq x :null) nil x)) args)) postmodern-20151031-git/s-sql.asd000066400000000000000000000003111260073634400164530ustar00rootroot00000000000000(defpackage :s-sql-system (:use :common-lisp :asdf)) (in-package :s-sql-system) (defsystem :s-sql :depends-on (:cl-postgres) :components ((:module :s-sql :components ((:file "s-sql"))))) postmodern-20151031-git/s-sql/000077500000000000000000000000001260073634400157675ustar00rootroot00000000000000postmodern-20151031-git/s-sql/s-sql.lisp000066400000000000000000001146401260073634400177250ustar00rootroot00000000000000(defpackage :s-sql (:use :common-lisp) (:export #:smallint #:bigint #:numeric #:real #:double-precision #:bytea #:text #:varchar #:db-null #:sql-type-name #:*standard-sql-strings* #:*downcase-symbols* #:sql-escape-string #:sql-escape #:from-sql-name #:to-sql-name #:*escape-sql-names-p* #:sql #:sql-compile #:sql-template #:$$ #:register-sql-operators #:enable-s-sql-syntax #:sql-error)) (in-package :s-sql) ;; Utils (define-condition sql-error (simple-error) ()) (defun sql-error (control &rest args) (error 'sql-error :format-control control :format-arguments args)) (defun strcat (args) "Concatenate a list of strings into a single one." (let ((result (make-string (reduce #'+ args :initial-value 0 :key 'length)))) (loop :for pos = 0 :then (+ pos (length arg)) :for arg :in args :do (replace result arg :start1 pos)) result)) (defun implode (sep list) "Reduce a list of strings to a single string, inserting a separator between them." (strcat (loop :for element :on list :collect (car element) :if (cdr element) :collect sep))) (defun split-on-keywords% (shape list) "Helper function for split-on-keywords. Extracts the values associated with the keywords from an argument list, and checks for errors." (let ((result ())) (labels ((next-word (words values) (if words (let* ((me (intern (symbol-name (caar words)) :keyword)) (optional (member '? (car words))) (multi (member '* (car words))) (no-args (member '- (car words))) (found (position me values))) (cond (found (let ((after-me (nthcdr (1+ found) values))) (unless (or after-me no-args) (sql-error "Keyword ~A encountered at end of arguments." me)) (let ((next (next-word (cdr words) after-me))) (cond (no-args (unless (zerop next) (sql-error "Keyword ~A does not take any arguments." me))) (multi (unless (>= next 1) (sql-error "Not enough arguments to keyword ~A." me))) (t (unless (= next 1) (sql-error "Keyword ~A takes exactly one argument." me)))) (push (cons (caar words) (if no-args t (subseq after-me 0 next))) result) found))) (optional (next-word (cdr words) values)) (t (sql-error "Required keyword ~A not found." me)))) (length values)))) (unless (= (next-word shape list) 0) (sql-error "Arguments do not start with a valid keyword.")) result))) (defmacro split-on-keywords (words form &body body) "Used to handle arguments to some complex SQL operations. Arguments are divided by keywords, which are interned with the name of the non-keyword symbols in words, and bound to these symbols. After the naming symbols, a ? can be used to indicate this argument group is optional, an * to indicate it can consist of more than one element, and a - to indicate it does not take any elements." (let ((alist (gensym))) `(let* ((,alist (split-on-keywords% ',words ,form)) ,@(mapcar (lambda (word) `(,(first word) (cdr (assoc ',(first word) ,alist)))) words)) ,@body))) ;; Converting between symbols and SQL strings. (defparameter *postgres-reserved-words* (let ((words (make-hash-table :test 'equal))) (dolist (word '("all" "analyse" "analyze" "and" "any" "array" "as" "asc" "asymmetric" "authorization" "between" "binary" "both" "case" "cast" "check" "collate" "column" "constraint" "create" "cross" "default" "deferrable" "desc" "distinct" "do" "else" "end" "except" "false" "for" "foreign" "freeze" "from" "full" "grant" "group" "having" "ilike" "in" "initially" "inner" "intersect" "into" "is" "isnull" "join" "leading" "left" "like" "limit" "localtime" "localtimestamp" "natural" "new" "not" "notnull" "null" "off" "offset" "old" "on" "only" "or" "order" "outer" "overlaps" "placing" "primary" "references" "returning" "right" "select" "similar" "some" "symmetric" "table" "then" "to" "trailing" "true" "union" "unique" "user" "using" "verbose" "when" "where" "with" "for" "nowait" "share")) (setf (gethash word words) t)) words) "A set of all PostgreSQL's reserved words, for automatic escaping.") (defparameter *escape-sql-names-p* :auto "Setting this to T will make S-SQL add double quotes around identifiers in queries. Setting it :auto will turn on this behaviour only for reserved words.") (defvar *downcase-symbols* t) (defun to-sql-name (name &optional (escape-p *escape-sql-names-p*)) "Convert a symbol or string into a name that can be an sql table, column, or operation name. Add quotes when escape-p is true, or escape-p is :auto and the name contains reserved words." (declare (optimize (speed 3) (debug 0))) (let ((*print-pretty* nil) (name (string name))) (with-output-to-string (*standard-output*) (flet ((subseq-downcase (str from to) (let ((result (make-string (- to from)))) (loop :for i :from from :below to :for p :from 0 :do (setf (char result p) (if *downcase-symbols* (char-downcase (char str i)) (char str i)))) result)) (write-element (str) (declare (type string str)) (let ((escape-p (if (eq escape-p :auto) (gethash str *postgres-reserved-words*) escape-p))) (when escape-p (write-char #\")) (if (and (> (length str) 1) ;; Placeholders like $2 (char= (char str 0) #\$) (every #'digit-char-p (the string (subseq str 1)))) (princ str) (loop :for ch :of-type character :across str :do (if (or (eq ch #\*) (alphanumericp ch)) (write-char ch) (write-char #\_)))) (when escape-p (write-char #\"))))) (loop :for start := 0 :then (1+ dot) :for dot := (position #\. name) :then (position #\. name :start start) :do (write-element (subseq-downcase name start (or dot (length name)))) :if dot :do (princ #\.) :else :do (return)))))) (defun from-sql-name (str) "Convert a string to something that might have been its original lisp name \(does not work if this name contained non-alphanumeric characters other than #\-)" (intern (map 'string (lambda (x) (if (eq x #\_) #\- x)) (if (eq (readtable-case *readtable*) :upcase) (string-upcase str) str)) (find-package :keyword))) ;; Writing out SQL type identifiers. ;; Aliases for some types that can be expressed in SQL. (deftype smallint () '(signed-byte 16)) (deftype bigint () `(signed-byte 64)) (deftype numeric (&optional precision/scale scale) (declare (ignore precision/scale scale)) 'number) (deftype double-precision () 'double-float) (deftype bytea () '(array (unsigned-byte 8))) (deftype text () 'string) (deftype varchar (length) (declare (ignore length)) `string) (deftype serial () 'integer) (deftype serial8 () 'integer) (deftype db-null () "Type for representing NULL values. Use like \(or integer db-null) for declaring a type to be an integer that may be null." '(eql :null)) ;; For types integer and real, the Lisp type isn't quite the same as ;; the SQL type. Close enough though. (defgeneric sql-type-name (lisp-type &rest args) (:documentation "Transform a lisp type into a string containing something SQL understands. Default is to just use the type symbol's name.") (:method ((lisp-type symbol) &rest args) (declare (ignore args)) (map 'string (lambda (ch) (if (eq ch #\-) #\space ch)) (symbol-name lisp-type))) (:method ((lisp-type (eql 'string)) &rest args) (cond (args (format nil "CHAR(~A)" (car args))) (t "TEXT"))) (:method ((lisp-type (eql 'varchar)) &rest args) (cond (args (format nil "VARCHAR(~A)" (car args))) (t "VARCHAR"))) (:method ((lisp-type (eql 'numeric)) &rest args) (cond ((cdr args) (destructuring-bind (precision scale) args (format nil "NUMERIC(~d, ~d)" precision scale))) (args (format nil "NUMERIC(~d)" (car args))) (t "NUMERIC"))) (:method ((lisp-type (eql 'float)) &rest args) (declare (ignore args)) "REAL") (:method ((lisp-type (eql 'double-float)) &rest args) (declare (ignore args)) "DOUBLE PRECISION") (:method ((lisp-type (eql 'double-precision)) &rest args) (declare (ignore args)) "DOUBLE PRECISION") (:method ((lisp-type (eql 'serial)) &rest args) (declare (ignore args)) "SERIAL") (:method ((lisp-type (eql 'serial8)) &rest args) (declare (ignore args)) "SERIAL8") (:method ((lisp-type (eql 'array)) &rest args) (format nil "~a[]" (to-type-name (car args)))) (:method ((lisp-type (eql 'db-null)) &rest args) (declare (ignore args)) (sql-error "Bad use of ~s." 'db-null))) (defun to-type-name (type) "Turn a Lisp type expression into an SQL typename." (if (listp type) (apply 'sql-type-name type) (sql-type-name type))) ;; Turning lisp values into SQL strings. (defparameter *standard-sql-strings* nil "Indicate whether S-SQL will use standard SQL strings (just use '' for #\'), or backslash-style escaping. Setting this to NIL is always safe, but when the server is configured to allow standard strings (parameter 'standard_conforming_strings' is 'on'), the noise in queries can be reduced by setting this to T.") (defun sql-escape-string (string &optional prefix) "Escape string data so it can be used in a query." (let ((*print-pretty* nil)) (with-output-to-string (*standard-output*) (when prefix (princ prefix) (princ #\space)) (unless *standard-sql-strings* (princ #\E)) (princ #\') (if *standard-sql-strings* (loop :for char :across string :do (princ (if (char= char #\') "''" char))) (loop :for char :across string :do (princ (case char (#\' "''") (#\\ "\\\\") (otherwise char))))) (princ #\')))) (defgeneric sql-escape (arg) (:documentation "Get the representation of a Lisp value so that it can be used in a query.") (:method ((arg symbol)) (if (or (typep arg 'boolean) (eq arg :null)) (call-next-method) (to-sql-name arg))) (:method ((arg vector)) (if (or (typep arg '(vector (unsigned-byte 8))) (stringp arg)) (call-next-method) (format nil "~:['{}'~;ARRAY[~:*~{~A~^, ~}]~]" (map 'list 'sql-escape arg)))) (:method ((arg t)) (multiple-value-bind (string escape) (cl-postgres:to-sql-string arg) (if escape (sql-escape-string string (and (not (eq escape t)) escape)) string)))) (defparameter *expand-runtime* nil) (defun sql-expand (arg) "Compile-time expansion of forms into lists of stuff that evaluates to strings \(which will form an SQL query when concatenated)." (cond ((and (consp arg) (keywordp (first arg))) (expand-sql-op (car arg) (cdr arg))) ((and (consp arg) (eq (first arg) 'quote)) (list (sql-escape (second arg)))) ((and (consp arg) *expand-runtime*) (expand-sql-op (intern (symbol-name (car arg)) :keyword) (cdr arg))) ((and (eq arg '$$) *expand-runtime*) '($$)) (*expand-runtime* (list (sql-escape arg))) ((or (consp arg) (and (symbolp arg) (not (or (keywordp arg) (eq arg t) (eq arg nil))))) (list `(sql-escape ,arg))) (t (list (sql-escape arg))))) (defun sql-expand-list (elts &optional (sep ", ")) "Expand a list of elements, adding a separator in between them." (loop :for (elt . rest) :on elts :append (sql-expand elt) :if rest :collect sep)) (defun sql-expand-names (names &optional (sep ", ")) (loop :for (name . rest) :on names :if (consp name) :append (let ((*expand-runtime* t)) (sql-expand name)) :else :collect (to-sql-name name) :if rest :collect sep)) (defun reduce-strings (list) "Join adjacent strings in a list, leave other values intact." (let ((accum ()) (span "")) (dolist (part list) (cond ((stringp part) (setf span (concatenate 'string span part))) (t (when (not (string= "" span)) (push span accum) (setf span "")) (push part accum)))) (if (not (string= "" span)) (push span accum)) (nreverse accum))) (defmacro sql (form) "Compile form to an sql expression as far as possible." (let ((list (reduce-strings (sql-expand form)))) (if (= 1 (length list)) (car list) `(strcat (list ,@list))))) (defun sql-compile (form) (let ((*expand-runtime* t)) (strcat (sql-expand form)))) (defun sql-template (form) (let* ((*expand-runtime* t) (compiled (reduce-strings (sql-expand form))) (*print-pretty* nil)) (lambda (&rest args) (with-output-to-string (*standard-output*) (dolist (element compiled) (princ (if (eq element '$$) (sql-escape (pop args)) element))))))) ;; The reader syntax. (defun s-sql-reader (stream char min-args) (declare (ignore char min-args)) (list 'sql (read stream))) (defun enable-s-sql-syntax (&optional (char #\Q)) "Enable a syntactic shortcut #Q\(...) for \(sql \(...)). Optionally takes a character to use instead of #\\Q." (set-dispatch-macro-character #\# char 's-sql-reader)) ;; Definitions of sql operators (defgeneric expand-sql-op (op args) (:documentation "For overriding expansion of operators. Default is to just place operator name in front, arguments between parentheses behind it.") (:method ((op t) args) `(,(to-sql-name op) "(" ,@(sql-expand-list args) ")"))) (defmacro def-sql-op (name arglist &body body) "Macro to make defining syntax a bit more straightforward. Name should be the keyword identifying the operator, arglist a lambda list to apply to the arguments, and body something that produces a list of strings and forms that evaluate to strings." (let ((args-name (gensym))) `(defmethod expand-sql-op ((op (eql ,name)) ,args-name) (destructuring-bind ,arglist ,args-name ,@body)))) (defun make-expander (arity name) "Generates an appropriate expander function for a given operator with a given arity." (let ((with-spaces (strcat (list " " name " ")))) (flet ((check-unary (args) (when (or (not args) (cdr args)) (sql-error "SQL operator ~A is unary." name))) (expand-n-ary (args) `("(" ,@(sql-expand-list args with-spaces) ")"))) (ecase arity (:unary (lambda (args) (check-unary args) `("(" ,name " " ,@(sql-expand (car args)) ")"))) (:unary-postfix (lambda (args) (check-unary args) `("(" ,@(sql-expand (car args)) " " ,name ")"))) (:n-ary (lambda (args) (if (cdr args) (expand-n-ary args) (sql-expand (car args))))) (:2+-ary (lambda (args) (unless (cdr args) (sql-error "SQL operator ~A takes at least two arguments." name)) (expand-n-ary args))) (:n-or-unary (lambda (args) (if (cdr args) (expand-n-ary args) `("(" ,name " " ,@(sql-expand (car args)) ")")))))))) (defmacro register-sql-operators (arity &rest names) "Define simple operators. Arity is one of :unary \(like 'not'), :unary-postfix \(the operator comes after the operand), :n-ary \(like '+': the operator falls away when there is only one operand), :2+-ary (like '=', which is meaningless for one operand), or :n-or-unary (like '-', where the operator is kept in the unary case). After the arity follow any number of operators, either just a keyword, in which case the downcased symbol name is used as the operator, or a two-element list containing a keyword and a name string." (declare (type (member :unary :unary-postfix :n-ary :n-or-unary :2+-ary) arity)) (flet ((define-op (name) (let ((name (if (listp name) (second name) (string-downcase (symbol-name name)))) (symbol (if (listp name) (first name) name))) `(let ((expander (make-expander ,arity ,name))) (defmethod expand-sql-op ((op (eql ,symbol)) args) (funcall expander args)))))) `(progn ,@(mapcar #'define-op names)))) (register-sql-operators :unary :not) (register-sql-operators :n-ary :+ :* :% :& :|\|| :|\|\|| :and :or :union (:union-all "union all")) (register-sql-operators :n-or-unary :- :~) (register-sql-operators :2+-ary := :/ :!= :<> :< :> :<= :>= :^ :~* :!~ :!~* :like :ilike :intersect (:intersect-all "intersect all") :except (:except-all "except all")) ;; PostGIS operators (register-sql-operators :2+-ary :&& :&< :|&<\|| :&> :<< :|<<\|| :>> :@ :|\|&>| :|\|>>| :~= :@> :@<) ;; hstore operators (register-sql-operators :2+-ary :-> :=> :? :?& :?\| :<@ :#= :unary :%% :%#) (def-sql-op :|| (&rest args) `("(" ,@(sql-expand-list args " || ") ")")) (def-sql-op :desc (arg) `(,@(sql-expand arg) " DESC")) (def-sql-op :nulls-first (arg) `(,@(sql-expand arg) " NULLS FIRST")) (def-sql-op :nulls-last (arg) `(,@(sql-expand arg) " NULLS LAST")) (def-sql-op :as (form name &rest fields) `(,@(sql-expand form) " AS " ,@(sql-expand name) ,@(when fields `("(" ,@(loop :for field :in fields :for (name type) := (if (and (consp field) (not (eq (first field) 'quote))) field (list field nil)) :for first := t :then nil :unless first :collect ", " :append (sql-expand name) :when type :append (list " " (to-type-name type))) ")")))) (def-sql-op :@@ (op1 op2) `("(" ,@(sql-expand op1) " @@ " ,@(sql-expand op2) ")")) (def-sql-op :distinct (&rest forms) `("DISTINCT(" ,@(sql-expand-list forms) ")")) (def-sql-op :any* (query) `("ANY(" ,@(sql-expand query) ")")) (def-sql-op :any (query) `("ANY " ,@(sql-expand query))) (def-sql-op :all (query) `("ALL " ,@(sql-expand query))) (def-sql-op :exists (query) `("(EXISTS " ,@(sql-expand query) ")")) (def-sql-op :is-null (arg) `("(" ,@(sql-expand arg) " IS NULL)")) (def-sql-op :not-null (arg) `("(" ,@(sql-expand arg) " IS NOT NULL)")) (def-sql-op :in (form set) `("(" ,@(sql-expand form) " IN " ,@(sql-expand set) ")")) (def-sql-op :not-in (form set) `("(" ,@(sql-expand form) " NOT IN " ,@(sql-expand set) ")")) (def-sql-op :extract (unit form) `("EXTRACT(" ,@(sql-expand unit) " FROM " ,@(sql-expand form) ")")) (def-sql-op :count (what &optional distinct) `("COUNT(" ,@(when (eq distinct :distinct) '("DISTINCT ")) ,@(sql-expand what) ")")) (def-sql-op :between (n start end) `("(" ,@(sql-expand n) " BETWEEN " ,@(sql-expand start) " AND " ,@(sql-expand end) ")")) (def-sql-op :between-symmetric (n start end) `("(" ,@(sql-expand n) " BETWEEN SYMMETRIC " ,@(sql-expand start) " AND " ,@(sql-expand end) ")")) (def-sql-op :case (&rest clauses) `("CASE" ,@(loop :for (test expr) :in clauses :if (eql test :else) :append `(" ELSE " ,@(sql-expand expr)) :else :append `(" WHEN " ,@(sql-expand test) " THEN " ,@(sql-expand expr)) :end) " END")) (def-sql-op :[] (form start &optional end) (if end `("(" ,@(sql-expand form) ")[" ,@(sql-expand start) ":" ,@(sql-expand end) "]") `("(" ,@(sql-expand form) ")[" ,@(sql-expand start) "]"))) ;; This one has two interfaces. When the elements are known at ;; compile-time, they can be given as multiple arguments to the ;; operator. When they are not, a single argument that evaulates to a ;; list should be used. (def-sql-op :set (&rest elements) (if (not elements) '("(NULL)") (let ((expanded (sql-expand-list elements))) ;; Ugly way to check if everything was expanded (if (stringp (car expanded)) `("(" ,@expanded ")") `("(" (let ((elements ,(car elements))) (if (null elements) "NULL" (implode ", " (mapcar 'sql-escape elements)))) ")"))))) (def-sql-op :dot (&rest args) (sql-expand-list args ".")) (def-sql-op :type (value type) `(,@(sql-expand value) "::" ,(to-type-name type))) (def-sql-op :raw (sql) (list sql)) ;; Selecting and manipulating (defun expand-joins (args) "Helper for the select operator. Turns the part following :from into the proper SQL syntax for joining tables." (labels ((expand-join (natural-p) (let ((type (first args)) (table (second args)) kind param) (unless table (sql-error "Incomplete join clause in select.")) (setf args (cddr args)) (unless (or natural-p (eq type :cross-join)) (setf kind (pop args)) (unless (and (or (eq kind :on) (eq kind :using)) args) (sql-error "Incorrect join form in select.")) (setf param (pop args))) `(" " ,@(when natural-p '("NATURAL ")) ,(ecase type (:left-join "LEFT") (:right-join "RIGHT") (:inner-join "INNER") (:outer-join "FULL OUTER") (:cross-join "CROSS")) " JOIN " ,@(sql-expand table) ,@(unless (or natural-p (eq type :cross-join)) (ecase kind (:on `(" ON " . ,(sql-expand param))) (:using `(" USING (" ,@(sql-expand-list param) ")"))))))) (is-join (x) (member x '(:left-join :right-join :inner-join :outer-join :cross-join)))) (when (null args) (sql-error "Empty :from clause in select")) (loop :for first = t :then nil :while args :append (cond ((is-join (car args)) (when first (sql-error ":from clause starts with a join.")) (expand-join nil)) ((eq (car args) :natural) (when first (sql-error ":from clause starts with a join.")) (pop args) (expand-join t)) (t `(,@(if first () '(", ")) ,@(sql-expand (pop args)))))))) (def-sql-op :select (&rest args) (split-on-keywords ((vars *) (distinct - ?) (distinct-on * ?) (from * ?) (where ?) (group-by * ?) (having ?) (window ?)) (cons :vars args) `("(SELECT " ,@(if distinct '("DISTINCT ")) ,@(if distinct-on `("DISTINCT ON (" ,@(sql-expand-list distinct-on) ") ")) ,@(sql-expand-list vars) ,@(if from (cons " FROM " (expand-joins from))) ,@(if where (cons " WHERE " (sql-expand (car where)))) ,@(if group-by (cons " GROUP BY " (sql-expand-list group-by))) ,@(if having (cons " HAVING " (sql-expand (car having)))) ,@(if window (cons " WINDOW " (sql-expand-list window))) ")"))) (def-sql-op :limit (form amount &optional offset) `("(" ,@(sql-expand form) " LIMIT " ,@(if amount (sql-expand amount) (list "ALL")) ,@(if offset (cons " OFFSET " (sql-expand offset)) ()) ")")) (def-sql-op :order-by (form &rest fields) (if fields `("(" ,@(sql-expand form) " ORDER BY " ,@(sql-expand-list fields) ")") `("( ORDER BY " ,@(sql-expand form) ")"))) (def-sql-op :set-constraints (state &rest constraints) `("SET CONSTRAINTS " ,@(if constraints (sql-expand-list constraints) '("ALL")) ,(ecase state (:deferred " DEFERRED") (:immediate " IMMEDIATE")))) (defun for-update/share (share-or-update form &rest args) (let* ((of-position (position :of args)) (no-wait-position (position :nowait args)) (of-tables (when of-position (subseq args (1+ of-position) no-wait-position)))) `("(" ,@(sql-expand form) ,(format nil " FOR ~:@(~A~)" share-or-update) ,@(when of-tables (list (format nil " OF ~{~A~^, ~}" (mapcar #'sql-compile of-tables)))) ,@(when no-wait-position (list " NOWAIT")) ")"))) (def-sql-op :for-update (form &rest args) (apply #'for-update/share "UPDATE" form args)) (def-sql-op :for-share (form &rest args) (apply #'for-update/share "SHARE" form args)) (defun escape-sql-expression (expr) "Try to escape an expression at compile-time, if not possible, delay to runtime. Used to create stored procedures." (let ((expanded (append (sql-expand expr) '(";")))) (if (every 'stringp expanded) (sql-escape-string (apply 'concatenate 'string expanded)) `(sql-escape-string (concatenate 'string ,@(reduce-strings expanded)))))) (def-sql-op :function (name (&rest args) return-type stability body) (assert (member stability '(:immutable :stable :volatile))) `("CREATE OR REPLACE FUNCTION " ,@(sql-expand name) " (" ,(implode ", " (mapcar 'to-type-name args)) ") RETURNS " ,(to-type-name return-type) " LANGUAGE SQL " ,(symbol-name stability) " AS " ,(escape-sql-expression body))) (def-sql-op :insert-into (table &rest rest) (split-on-keywords ((method *) (returning ? *)) (cons :method rest) `("INSERT INTO " ,@(sql-expand table) " " ,@(cond ((eq (car method) :set) (cond ((oddp (length (cdr method))) (sql-error "Invalid amount of :set arguments passed to insert-into sql operator")) ((null (cdr method)) '("DEFAULT VALUES")) (t `("(" ,@(sql-expand-list (loop :for (field nil) :on (cdr method) :by #'cddr :collect field)) ") VALUES (" ,@(sql-expand-list (loop :for (nil value) :on (cdr method) :by #'cddr :collect value)) ")")))) ((and (not (cdr method)) (consp (car method)) (keywordp (caar method))) (sql-expand (car method))) (t (sql-error "No :set arguments or select operator passed to insert-into sql operator"))) ,@(when returning `(" RETURNING " ,@(sql-expand-list returning)))))) (def-sql-op :listen (channel) `("LISTEN " ,@(sql-expand channel))) (def-sql-op :unlisten (channel) `("UNLISTEN " ,@(sql-expand channel))) (def-sql-op :notify (channel &optional payload) `("NOTIFY " ,@(sql-expand channel) ,@(when payload (list ", " (sql-escape-string payload))))) (defun expand-rows (rows length) (unless rows (sql-error "Running :insert-rows-into without data.")) (unless length (setf length (length (car rows)))) (let ((*expand-runtime* t)) (strcat (loop :for row :in rows :for first := t :then nil :when (/= (length row) length) :do (sql-error "Found rows of unequal length in :insert-rows-into.") :append `(,@(unless first '(", ")) "(" ,@(sql-expand-list row) ")"))))) (def-sql-op :insert-rows-into (table &rest rest) (split-on-keywords ((columns ? *) (values) (returning ? *)) rest `("INSERT INTO " ,@(sql-expand table) " " ,@(when columns `("(" ,@(sql-expand-list columns) ") ")) "VALUES " ,(if *expand-runtime* (expand-rows (car values) (and columns (length columns))) `(expand-rows ,(car values) ,(and columns (length columns)))) ,@(when returning `(" RETURNING " ,@(sql-expand-list returning)))))) (def-sql-op :update (table &rest args) (split-on-keywords ((set *) (from * ?) (where ?) (returning ? *)) args (when (oddp (length set)) (sql-error "Invalid amount of :set arguments passed to update sql operator")) `("UPDATE " ,@(sql-expand table) " SET " ,@(loop :for (field value) :on set :by #'cddr :for first = t :then nil :append `(,@(if first () '(", ")) ,@(sql-expand field) " = " ,@(sql-expand value))) ,@(if from (cons " FROM " (expand-joins from))) ,@(if where (cons " WHERE " (sql-expand (car where))) ()) ,@(when returning (cons " RETURNING " (sql-expand-list returning)))))) (def-sql-op :delete-from (table &rest args) (split-on-keywords ((where ?) (returning ? *)) args `("DELETE FROM " ,@(sql-expand table) ,@(when where (cons " WHERE " (sql-expand (car where)))) ,@(when returning (cons " RETURNING " (sql-expand-list returning)))))) (def-sql-op :over (form &rest args) (if args `("(" ,@(sql-expand form) " OVER " ,@(sql-expand-list args) ")") `("(" ,@(sql-expand form) " OVER ()) "))) (def-sql-op :partition-by (&rest args) (split-on-keywords ((partition-by *) (order-by ? *)) (cons :partition-by args) `("(PARTITION BY " ,@(sql-expand-list partition-by) ,@(when order-by (cons " ORDER BY " (sql-expand-list order-by))) ")"))) (def-sql-op :parens (op) `(" (" ,@(sql-expand op) ") ")) (def-sql-op :with (&rest args) (let ((x (butlast args)) (y (last args))) `("WITH " ,@(sql-expand-list x) ,@(sql-expand (car y))))) (def-sql-op :with-recursive (form1 form2) `("WITH RECURSIVE " ,@(sql-expand form1) ,@(sql-expand form2))) (def-sql-op :window (form) `("WINDOW " ,@(sql-expand form))) ;; Data definition (defun dissect-type (type) ;; todo: better documentation "Return the type and whether it may be NULL." (if (and (consp type) (eq (car type) 'or)) (if (and (member 'db-null type) (= (length type) 3)) (if (eq (second type) 'db-null) (values (third type) t) (values (second type) t)) (sql-error "Invalid type: ~a. 'or' types must have two alternatives, one of which is ~s." type 'db-null)) (values type nil))) (defun expand-foreign-on* (action) (case action (:restrict "RESTRICT") (:set-null "SET NULL") (:set-default "SET DEFAULT") (:cascade "CASCADE") (:no-action "NO ACTION") (t (sql-error "Unsupported action for foreign key: ~A" action)))) (defun %build-foreign-reference (target on-delete on-update) `(" REFERENCES " ,@(if (consp target) `(,(to-sql-name (car target)) "(" ,@(sql-expand-names (cdr target)) ")") `(,(to-sql-name target))) " ON DELETE " ,(expand-foreign-on* on-delete) " ON UPDATE " ,(expand-foreign-on* on-update))) (defun expand-table-constraint (option args) (case option (:constraint `("CONSTRAINT " ,(to-sql-name (car args)) " " ,@(expand-table-constraint (cadr args) (cddr args)))) (:check `("CHECK " ,@(sql-expand (car args)))) (:primary-key `("PRIMARY KEY (" ,@(sql-expand-names args) ")")) (:unique `("UNIQUE (" ,@(sql-expand-names args) ")")) (:foreign-key (destructuring-bind (columns target &optional (on-delete :restrict) (on-update :restrict)) args `("FOREIGN KEY (" ,@(sql-expand-names columns) ")" ,@(%build-foreign-reference target on-delete on-update)))))) (defun expand-table-column (column-name args) `(,(to-sql-name column-name) " " ,@(let ((type (or (getf args :type) (sql-error "No type specified for column ~A." column-name)))) (multiple-value-bind (type null) (dissect-type type) `(,(to-type-name type) ,@(when (not null) '(" NOT NULL"))))) ,@(loop :for (option value) :on args :by #'cddr :append (case option (:default `(" DEFAULT " ,@(sql-expand value))) (:primary-key (when value `(" PRIMARY KEY"))) (:unique (when value `(" UNIQUE"))) (:check `(" CHECK " ,@(sql-expand value))) (:references (destructuring-bind (target &optional (on-delete :restrict) (on-update :restrict)) value (%build-foreign-reference target on-delete on-update))) (:type ()) (t (sql-error "Unknown column option: ~A." option)))))) (def-sql-op :create-table (name (&rest columns) &rest options) (when (null columns) (sql-error "No columns defined for table ~A." name)) `("CREATE TABLE " ,(to-sql-name name) " (" ,@(loop :for ((column-name . args) . rest) :on columns :append (expand-table-column column-name args) :if rest :collect ", ") ,@(loop :for ((option . args)) :on options :collect ", " :append (expand-table-constraint option args)) ")")) (def-sql-op :alter-table (name action &rest args) (flet ((drop-action (action) (case action (:restrict "RESTRICT") (:cascade "CASCADE") (t (sql-error "Unknown DROP action ~A." action))))) `("ALTER TABLE " ,(to-sql-name name) " " ,@ (case action (:add (cons "ADD " (expand-table-constraint (first args) (rest args)))) (:add-column (cons "ADD COLUMN " (expand-table-column (first args) (rest args)))) (:alter-column (cons "ALTER COLUMN " (expand-table-column (first args) (rest args)))) (:drop-column (list "DROP COLUMN " (to-sql-name (first args)))) (:add-constraint (append (list "ADD CONSTRAINT ") (list (to-sql-name (first args)) " ") (expand-table-constraint (second args) (cddr args)))) (:drop-constraint (list "DROP CONSTRAINT " (to-sql-name (first args)) (if (rest args) (drop-action (second args)) ""))) (t (sql-error "Unknown ALTER TABLE action ~A" action)))))) (defun expand-create-index (name args) (split-on-keywords ((on) (using ?) (fields *) (where ?)) args `(,@(sql-expand name) " ON " ,(to-sql-name (first on)) ,@(when using `(" USING " ,(symbol-name (first using)))) " (" ,@(sql-expand-names fields) ")" ,@(when where `(" WHERE " ,@(sql-expand (first where))))))) (def-sql-op :create-index (name &rest args) (cons "CREATE INDEX " (expand-create-index name args))) (def-sql-op :create-unique-index (name &rest args) (cons "CREATE UNIQUE INDEX " (expand-create-index name args))) (def-sql-op :cascade (op) `(,@(sql-expand op) " CASCADE")) (defmacro def-drop-op (op-name word) `(def-sql-op ,op-name (&rest args) (let ((if-exists (if (eq (car args) :if-exists) (pop args) nil))) (destructuring-bind (name) args `("DROP " ,,word " " ,@(when if-exists '("IF EXISTS ")) ,@(sql-expand name)))))) (def-drop-op :drop-table "TABLE") (def-drop-op :drop-index "INDEX") (def-drop-op :drop-sequence "SEQUENCE") (def-drop-op :drop-view "VIEW") (def-drop-op :drop-type "TYPE") (def-drop-op :drop-rule "RULE") (defun dequote (val) (if (and (consp val) (eq (car val) 'quote)) (cadr val) val)) (def-sql-op :nextval (name) `("nextval(" ,(if *expand-runtime* (sql-escape-string (to-sql-name (dequote name))) `(sql-escape-string (to-sql-name ,name))) ")")) (def-sql-op :create-sequence (name &key increment min-value max-value start cache cycle) `("CREATE SEQUENCE " ,@(sql-expand name) ,@(when increment `(" INCREMENT " ,@(sql-expand increment))) ,@(when min-value `(" MINVALUE " ,@(sql-expand min-value))) ,@(when max-value `(" MAXVALUE " ,@(sql-expand max-value))) ,@(when start `(" START " ,@(sql-expand start))) ,@(when cache `(" CACHE " ,@(sql-expand cache))) ,@(when cycle `(" CYCLE")))) (def-sql-op :create-view (name query) ;; does not allow to specify the columns of the view yet `("CREATE VIEW " ,(to-sql-name name) " AS " ,@(sql-expand query))) (def-sql-op :create-enum (name members) (let ((strings (loop :for m :in members :collect (etypecase m (symbol (string-downcase m)) (string m))))) `("CREATE TYPE " ,@(sql-expand name) " AS ENUM (" ,@(sql-expand-list strings) ")"))) ;;; http://www.postgresql.org/docs/8.3/interactive/sql-createdomain.html (def-sql-op :create-domain (name &rest args) (split-on-keywords ((type) (default ?) (constraint-name ?) (check ?)) args (multiple-value-bind (type may-be-null) (dissect-type (car type)) `("CREATE DOMAIN " ,@(sql-expand name) " AS " ,(to-type-name type) ,@(when default `(" DEFAULT " ,@(sql-expand (car default)))) ,@(when constraint-name `(" CONSTRAINT " ,@(sql-expand (car constraint-name)))) ,@(unless may-be-null '(" NOT NULL")) ,@(when check `(" CHECK" ,@(sql-expand (car check)))))))) (def-sql-op :drop-domain (name) `("DROP DOMAIN " ,@(sql-expand name))) ;http://www.postgresql.org/docs/8.3/static/sql-createrule.html (def-sql-op :create-rule (name &rest rest) (split-on-keywords ((on) (to) (where ?) (instead ? -) (do ? *)) rest (check-type (car on) (member :select :insert :update :delete)) `("CREATE RULE " ,@(sql-expand name) " AS ON " ,(symbol-name (car on)) " TO " ,@(sql-expand (car to)) ,@(when where `(" WHERE " ,@(sql-expand (car where)))) " DO" ,@(when instead '(" INSTEAD")) ,@(if (or (null do) (eq do :nothing)) '(" NOTHING") `("(" ,@(sql-expand-list do "; ") ")"))))) postmodern-20151031-git/simple-date.asd000066400000000000000000000017651260073634400176360ustar00rootroot00000000000000(defpackage :simple-date-system (:use :common-lisp :asdf)) (in-package :simple-date-system) (defsystem :simple-date :components ((:module :simple-date :components ((:file "simple-date"))))) (defsystem :simple-date-postgres-glue :depends-on (:simple-date :cl-postgres) :components ((:module :simple-date :components ((:file "cl-postgres-glue"))))) (defsystem :simple-date-tests :depends-on (:eos :simple-date) :components ((:module :simple-date :components ((:file "tests"))))) (defmethod perform ((op asdf:test-op) (system (eql (find-system :simple-date)))) (asdf:oos 'asdf:load-op :simple-date-tests) (funcall (intern (string :run!) (string :Eos)) :simple-date)) (defmethod perform :after ((op asdf:load-op) (system (eql (find-system :simple-date)))) (when (and (find-package :cl-postgres) (not (find-symbol (symbol-name '#:+postgres-day-offset+) :simple-date))) (asdf:oos 'asdf:load-op :simple-date-postgres-glue))) postmodern-20151031-git/simple-date/000077500000000000000000000000001260073634400171345ustar00rootroot00000000000000postmodern-20151031-git/simple-date/cl-postgres-glue.lisp000066400000000000000000000036321260073634400232250ustar00rootroot00000000000000(in-package :simple-date) ;; PostgreSQL days are measured from 01-01-2000, whereas simple-date ;; uses 01-03-2000. (defconstant +postgres-day-offset+ -60) (defconstant +usecs-in-one-day+ (* 1000 1000 3600 24)) (flet ((interpret-timestamp (usecs) (multiple-value-bind (days usecs) (floor usecs +usecs-in-one-day+) (make-instance 'timestamp :days (+ days +postgres-day-offset+) :ms (floor usecs 1000))))) (cl-postgres:set-sql-datetime-readers :date (lambda (days) (make-instance 'date :days (+ days +postgres-day-offset+))) :timestamp #'interpret-timestamp :timestamp-with-timezone #'interpret-timestamp :interval (lambda (months days usecs) (make-instance 'interval :months months :ms (floor (+ (* days +usecs-in-one-day+) usecs) 1000))))) (defmethod cl-postgres:to-sql-string ((arg date)) (multiple-value-bind (year month day) (decode-date arg) (values (format nil "~4,'0d-~2,'0d-~2,'0d" year month day) "date"))) (defmethod cl-postgres:to-sql-string ((arg timestamp)) (multiple-value-bind (year month day hour min sec ms) (decode-timestamp arg) (values (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d~@[.~3,'0d~]" year month day hour min sec (if (zerop ms) nil ms)) "timestamp"))) (defmethod cl-postgres:to-sql-string ((arg interval)) (multiple-value-bind (year month day hour min sec ms) (decode-interval arg) (if (= year month day hour min sec ms 0) (values "0 milliseconds" "interval") (flet ((not-zero (x) (if (zerop x) nil x))) (values (format nil "~@[~d years ~]~@[~d months ~]~@[~d days ~]~@[~d hours ~]~@[~d minutes ~]~@[~d seconds ~]~@[~d milliseconds~]" (not-zero year) (not-zero month) (not-zero day) (not-zero hour) (not-zero min) (not-zero sec) (not-zero ms)) "interval"))))) postmodern-20151031-git/simple-date/simple-date.lisp000066400000000000000000000363271260073634400222440ustar00rootroot00000000000000(defpackage :simple-date (:use :common-lisp) (:export #:date #:encode-date #:decode-date #:day-of-week #:timestamp #:encode-timestamp #:decode-timestamp #:timestamp-to-universal-time #:universal-time-to-timestamp #:interval #:encode-interval #:decode-interval #:time-add #:time-subtract #:time= #:time> #:time< #:time<= #:time>=)) (in-package :simple-date) (defun to-external-date (year month day) "Convert a internal date representation to an external one. Internally, months and days start at 0 and the 1st of March 2000 is the year 0." (let ((day (1+ day)) (month (+ month 3)) (year (+ year 2000))) (if (> month 12) (values (1+ year) (- month 12) day) (values year month day)))) (defun to-internal-date (year month day) "Convert an external date representation to an internal one." (declare (type integer year) (type (integer 1 12) month) (type (integer 1 31) day)) (let ((day (1- day)) (month (- month 3)) (year (- year 2000))) (if (< month 0) (values (1- year) (+ month 12) day) (values year month day)))) (let ((lookup #(0 31 61 92 122 153 184 214 245 275 306 337))) (defun days-before-month (month) "Find the amount of days in a year before the start of a given month. \(This works because in the internal representation march is the first month, so leap days only happen at the end of the year.)" (aref lookup month))) (defun month-at-day (days) "Find the month in which a given day of the year falls. It's a manually constructed binary search. Which is pretty ugly." (if (< days 184) (if (< days 61) (if (< days 31) 0 1) (if (< days 122) (if (< days 92) 2 3) (if (< days 153) 4 5))) (if (< days 245) (if (< days 214) 6 7) (if (< days 306) (if (< days 275) 8 9) (if (< days 337) 10 11))))) ;; These assume a typical set of years, they are used by first taking ;; care of the 4-century periods, then the centuries, etc. That way, ;; the centuries will never have to deal with a year divisible by 400, ;; the 4-year periods will never contain century-years, and the ;; one-year periods are never leap years. (defconstant +days-in-400-years+ (+ (* 400 365) 97)) (defconstant +days-in-100-years+ (+ (* 100 365) 24)) (defconstant +days-in-4-years+ (+ (* 4 365) 1)) (defconstant +days-in-year+ 365) (defconstant +millisecs-in-day+ (* 1000 3600 24)) (defun encode-days (year month day) "Get the number of days since March 1st 2000 for a given date \(in internal format.)" (incf day (days-before-month month)) (flet ((adjust (factor days) (multiple-value-bind (parts remainder) (floor year factor) (incf day (* parts days)) (setf year remainder)))) (adjust 400 +days-in-400-years+) (adjust 100 +days-in-100-years+) (adjust 4 +days-in-4-years+) (adjust 1 +days-in-year+) day)) (defun decode-days (days) "Transform a number of days since March 1st 2000 to a date \(in internal format.)" (let ((year 0) (month 0)) (flet ((adjust (factor factor-days prev) (multiple-value-bind (parts remainder) (floor days factor-days) (when (and prev (= parts prev)) (decf parts) (incf remainder factor-days)) (incf year (* factor parts)) (setf days remainder)))) (adjust 400 +days-in-400-years+ nil) (adjust 100 +days-in-100-years+ 4) (adjust 4 +days-in-4-years+ 25) (adjust 1 +days-in-year+ 4) (setf month (month-at-day days)) (values year month (- days (days-before-month month)))))) (defun encode-millisecs (hour minute second millisecond) "Get the amount of milliseconds from a number of bigger units." (+ millisecond (* 1000 (+ second (* 60 (+ minute (* 60 hour))))))) (defun decode-millisecs (millisecs) "Decompose a number of milliseconds into hours, minutes, seconds and milliseconds." (multiple-value-bind (seconds millisecs) (floor millisecs 1000) (multiple-value-bind (minutes seconds) (floor seconds 60) (multiple-value-bind (hours minutes) (floor minutes 60) (values hours minutes seconds millisecs))))) (defun leap-year-p (year) "Is this year a leap year?" (and (zerop (mod year 4)) (or (not (zerop (mod year 100))) (zerop (mod year 400))))) (defun days-in-month (month year) "Days in a certain month -- note that these months use internal encoding." (case month (11 (if (leap-year-p (1+ year)) 29 28)) ((1 3 6 8) 30) (t 31))) (defun normalize-timestamp (days millisecs) "Make sure that a number of milliseconds falls within a day, correct the amount of days if necessary." (multiple-value-bind (extra-days millisecs) (floor millisecs +millisecs-in-day+) (values (+ days extra-days) millisecs))) (defun date-add (base-days months) "Add a number of months to a date \(expressed in days)." (multiple-value-bind (year month day) (decode-days base-days) (multiple-value-bind (extra-years month) (floor (+ month months) 12) (incf year extra-years) (encode-days year month (min day (1- (days-in-month month year))))))) (defun invert-interval (interval) "Invert the components of an interval." (make-instance 'interval :ms (- (millisecs interval)) :months (- (months interval)))) (defclass date () ((days :initarg :days :accessor days)) (:documentation "This class is used to represent dates where the time of day is not important.")) (defmethod print-object ((date date) stream) (print-unreadable-object (date stream :type t) (multiple-value-bind (year month day) (decode-date date) (format stream "~2,'0d-~2,'0d-~4,'0d" day month year)))) (defun encode-date (year month day) "Create a date object." (multiple-value-bind (year month day) (to-internal-date year month day) (make-instance 'date :days (encode-days year month day)))) (defun decode-date (date) "Get the date elements from a date object." (multiple-value-bind (year month day) (decode-days (days date)) (to-external-date year month day))) (defun day-of-week (date) "Returns the weekday of the given date as a number between 0 and 6, 0 being Sunday and 6 being Saturday." (+ (mod (+ (days date) 3) 7))) (defclass timestamp (date) ((millisecs :initarg :ms :accessor millisecs)) (:documentation "A timestamp specifies a time with a precision up to milliseconds.")) (defmethod print-object ((stamp timestamp) stream) (print-unreadable-object (stamp stream :type t) (multiple-value-bind (year month day hour min sec ms) (decode-timestamp stamp) (format stream "~2,'0d-~2,'0d-~4,'0dT~2,'0d:~2,'0d:~2,'0d~@[,~3,'0d~]" day month year hour min sec (if (zerop ms) nil ms))))) (defun encode-timestamp (year month day &optional (hour 0) (minute 0) (second 0) (millisecond 0)) "Create a timestamp object." (multiple-value-bind (year month day) (to-internal-date year month day) (make-instance 'timestamp :days (encode-days year month day) :ms (encode-millisecs hour minute second millisecond)))) (defun decode-timestamp (timestamp) "Extract the date and time from a timestamp object." (multiple-value-bind (year month day) (decode-days (days timestamp)) (multiple-value-bind (year month day) (to-external-date year month day) (multiple-value-bind (hour minute second millisec) (decode-millisecs (millisecs timestamp)) (values year month day hour minute second millisec))))) (defconstant +universal-time-offset+ (encode-universal-time 0 0 0 1 3 2000 0)) (defun timestamp-to-universal-time (timestamp) "Convert a timestamp to a Lisp universal time." (+ +universal-time-offset+ (round (+ (* +millisecs-in-day+ (days timestamp)) (millisecs timestamp)) 1000))) (defun universal-time-to-timestamp (utime) "Convert a Lisp universal time to a timestamp." (multiple-value-bind (days millisecs) (floor (* 1000 (- utime +universal-time-offset+)) +millisecs-in-day+) (make-instance 'timestamp :days days :ms millisecs))) (defclass interval () ((millisecs :initarg :ms :accessor millisecs) (months :initform 0 :initarg :months :accessor months)) (:documentation "Intervals can be added to date and timestamp units to get relative times. The amount of time added for the month part of a timestamp depends on the time it is being added to.")) (defmethod print-object ((interval interval) stream) (print-unreadable-object (interval stream :type t) (multiple-value-bind (year month day hour min sec ms) (decode-interval interval) (flet ((not-zero (x) (if (zerop x) nil x))) (format stream "P~@[~dY~]~@[~dM~]~@[~dD~]~@[~dH~]~@[~dm~]~@[~d~@[,~3,'0d~]S~]" (not-zero year) (not-zero month) (not-zero day) (not-zero hour) (not-zero min) (if (and (zerop sec) (zerop ms)) nil sec) (not-zero ms)))))) (defun encode-interval (&key (year 0) (month 0) (week 0) (day 0) (hour 0) (minute 0) (second 0) (millisecond 0)) "Create an interval object. Parameters may be negative." (make-instance 'interval :ms (+ (* +millisecs-in-day+ (+ day (* 7 week))) (encode-millisecs hour minute second millisecond)) :months (+ month (* 12 year)))) (defun decode-interval (interval) (multiple-value-bind (day millisecs) (floor (millisecs interval) +millisecs-in-day+) (multiple-value-bind (hour min sec ms) (decode-millisecs millisecs) (multiple-value-bind (year month) (floor (months interval) 12) (values year month day hour min sec ms))))) (defgeneric time-add (a b) (:documentation "Generic function for combining datetime objects. Adding an interval to a date or timestamp will return a new date or timestamp, increased by the value of the interval. Adding two intervals returns a new interval with the sum of the two arguments. Integers can be used in place of intervals, and will be interpreted as an amount of milliseconds.")) (defmethod time-add ((stamp timestamp) (interval interval)) (multiple-value-bind (days millisecs) (normalize-timestamp (days stamp) (+ (millisecs stamp) (millisecs interval))) (unless (zerop (months interval)) (setf days (date-add days (months interval)))) (make-instance 'timestamp :ms millisecs :days days))) (defmethod time-add ((interval interval) (stamp timestamp)) (time-add stamp interval)) (defmethod time-add ((date date) (interval interval)) (multiple-value-bind (days remainder) (floor (millisecs interval) +millisecs-in-day+) (unless (zerop remainder) (error "Can not add an interval spanning a fractional number of days to a date.")) (make-instance 'date :days (date-add (+ (days date) days) (months interval))))) (defmethod time-add ((interval interval) (date date)) (time-add date interval)) (defmethod time-add ((a interval) (b interval)) (make-instance 'interval :ms (+ (millisecs a) (millisecs b)) :months (+ (months a) (months b)))) (defmethod time-add ((interval interval) (millisecs integer)) (make-instance 'interval :ms (+ (millisecs interval) millisecs) :months (months interval))) (defmethod time-add ((millisecs integer) (interval interval)) (time-add interval millisecs)) (defmethod time-add ((stamp timestamp) (millisecs integer)) (multiple-value-bind (days millisecs) (normalize-timestamp (days stamp) (+ (millisecs stamp) millisecs)) (make-instance 'timestamp :ms millisecs :days days))) (defmethod time-add ((millisecs integer) (stamp timestamp)) (time-add stamp millisecs)) (defmethod time-add ((a integer) (b integer)) (+ a b)) (defgeneric time-subtract (a b) (:documentation "Subtracts datetime objects from each other. Subtracting two dates or timestamps results in an interval that represents the difference between them. Similarly subtracting two intervals gives their difference.")) (defmethod time-subtract ((a date) (b date)) (make-instance 'interval :ms (* +millisecs-in-day+ (- (days a) (days b))))) (defmethod time-subtract ((a timestamp) (b date)) (make-instance 'interval :ms (+ (* +millisecs-in-day+ (- (days a) (days b))) (millisecs a)))) (defmethod time-subtract ((a timestamp) (b timestamp)) (make-instance 'interval :ms (+ (* +millisecs-in-day+ (- (days a) (days b))) (- (millisecs a) (millisecs b))))) (defmethod time-subtract ((date date) (interval interval)) (time-add date (invert-interval interval))) (defmethod time-subtract ((stamp timestamp) (millisecs integer)) (time-add stamp (- millisecs))) (defmethod time-subtract ((a interval) (b interval)) (time-add a (invert-interval b))) (defmethod time-subtract ((interval interval) (millisecs integer)) (time-add interval (- millisecs))) (defmethod time-subtract ((millisecs integer) (interval interval)) (time-add millisecs (invert-interval interval))) (defgeneric time= (a b) (:documentation "Compare two time-related values, returns a boolean indicating whether they denote the same time or period.")) (defmethod time= ((a date) (b date)) (= (days a) (days b))) (defmethod time= ((a date) (b timestamp)) (and (= (days a) (days b)) (= (millisecs b) 0))) (defmethod time= ((a timestamp) (b date)) (time= b a)) (defmethod time= ((a timestamp) (b timestamp)) (and (= (days a) (days b)) (= (millisecs a) (millisecs b)))) (defmethod time= ((a interval) (b interval)) (and (= (millisecs a) (millisecs b)) (= (months a) (months b)))) (defgeneric time< (a b) (:documentation "Compare two time-related values, returns a boolean indicating whether the first is less than the second.")) (defmethod time< ((a date) (b date)) (< (days a) (days b))) (defmethod time< ((a date) (b timestamp)) (or (< (days a) (days b)) (and (= (days a) (days b)) (> (millisecs b) 0)))) (defmethod time< ((a timestamp) (b date)) (time> b a)) (defmethod time< ((a timestamp) (b timestamp)) (or (< (days a) (days b)) (and (= (days a) (days b)) (< (millisecs a) (millisecs b))))) (defmethod time< ((a interval) (b interval)) (or (< (months a) (months b)) (and (= (months a) (months b)) (< (millisecs a) (millisecs b))))) (defgeneric time> (a b) (:documentation "Compare two time-related values, returns a boolean indicating whether the first is greater than the second.")) (defmethod time> ((a date) (b date)) (> (days a) (days b))) (defmethod time> ((a date) (b timestamp)) (> (days a) (days b))) (defmethod time> ((a timestamp) (b date)) (time< b a)) (defmethod time> ((a timestamp) (b timestamp)) (or (> (days a) (days b)) (and (= (days a) (days b)) (> (millisecs a) (millisecs b))))) (defmethod time> ((a interval) (b interval)) (or (> (months a) (months b)) (and (= (months a) (months b)) (> (millisecs a) (millisecs b))))) (defun time<= (a b) "Compare two time-related values, returns a boolean indicating whether the first is less or equal than the second." (not (time> a b))) (defun time>= (a b) "Compare two time-related values, returns a boolean indicating whether the first is greater or equal than the second." (not (time< a b))) postmodern-20151031-git/simple-date/tests.lisp000066400000000000000000000106371260073634400211760ustar00rootroot00000000000000(defpackage :simple-date-tests (:use :common-lisp :Eos :simple-date)) (in-package :simple-date-tests) ;; After loading the file, run the tests with (Eos:run! :simple-date) (def-suite :simple-date) (in-suite :simple-date) (test days-in-month ;; Note: internal date numbers, so 0 is March (is (= 31 (simple-date::days-in-month 0 2000))) (is (= 30 (simple-date::days-in-month 1 2000))) (is (= 31 (simple-date::days-in-month 2 2000))) (is (= 30 (simple-date::days-in-month 3 2000))) (is (= 31 (simple-date::days-in-month 4 2000))) (is (= 31 (simple-date::days-in-month 5 2000))) (is (= 30 (simple-date::days-in-month 6 2000))) (is (= 31 (simple-date::days-in-month 7 2000))) (is (= 30 (simple-date::days-in-month 8 2000))) (is (= 31 (simple-date::days-in-month 9 2000))) (is (= 31 (simple-date::days-in-month 10 2000))) (is (= 29 (simple-date::days-in-month 11 2000))) (is (= 28 (simple-date::days-in-month 11 2001)))) (defmacro with-random-dates (amount &body body) (let ((i (gensym))) `(dotimes (,i ,amount) (let ((year (+ 1900 (random 300))) (month (1+ (random 12))) (day (1+ (random 28))) (hour (random 24)) (min (random 60)) (sec (random 60)) (millisec (random 1000))) ,@body)))) (test encode-date (with-random-dates 100 (declare (ignore hour min sec millisec)) (multiple-value-bind (year* month* day*) (decode-date (encode-date year month day)) (is (and (= year* year) (= month* month) (= day* day)))))) (test leap-year (flet ((test-date (y m d) (multiple-value-bind (y2 m2 d2) (decode-date (encode-date y m d)) (and (= y y2) (= m m2) (= d d2))))) (is (test-date 2000 2 29)) (is (test-date 2004 2 29)) (is (test-date 2108 2 29)) (is (test-date 1992 2 29)))) (test encode-timestamp (with-random-dates 100 (multiple-value-bind (year* month* day* hour* min* sec* millisec*) (decode-timestamp (encode-timestamp year month day hour min sec millisec)) (is (and (= year* year) (= month* month) (= day* day) (= hour* hour) (= min* min) (= sec* sec) (= millisec* millisec)))))) (test timestamp-universal-times (with-random-dates 100 (declare (ignore millisec)) (let ((stamp (encode-timestamp year month day hour min sec 0)) (utime (encode-universal-time sec min hour day month year 0))) (is (= (timestamp-to-universal-time stamp) utime)) (is (time= (universal-time-to-timestamp utime) stamp))))) (test add-month (with-random-dates 100 (multiple-value-bind (year* month* day* hour* min* sec* millisec*) (decode-timestamp (time-add (encode-timestamp year month day hour min sec millisec) (encode-interval :month 1))) (is (and (or (and (= year* year) (= month* (1+ month))) (and (= year* (1+ year)) (= month* 1))) (= day* day) (= hour* hour) (= min* min) (= sec* sec) (= millisec* millisec)))))) (test subtract-month (with-random-dates 100 (multiple-value-bind (year* month* day* hour* min* sec* millisec*) (decode-timestamp (time-add (encode-timestamp year month day hour min sec millisec) (encode-interval :month -1))) (is (and (or (and (= year* year) (= month* (1- month))) (and (= year* (1- year)) (= month* 12))) (= day* day) (= hour* hour) (= min* min) (= sec* sec) (= millisec* millisec)))))) (test add-hour (with-random-dates 100 (declare (ignore millisec)) (is (= (- (timestamp-to-universal-time (time-add (encode-timestamp year month day hour min sec 0) (encode-interval :hour 1))) (encode-universal-time sec min hour day month year 0)) 3600)))) (test time< (with-random-dates 100 (is (time< (encode-date year month day) (encode-date (1+ year) month day))) (is (time< (encode-timestamp year month day hour min sec millisec) (encode-timestamp year month day hour min (1+ sec) millisec))) (is (time< (encode-interval :month month :hour hour) (encode-interval :month month :hour hour :minute 30)))))