X-Git-Url: http://git.kpe.io/?p=clsql.git;a=blobdiff_plain;f=sql%2Fexpressions.lisp;h=df9cfc08b55da8a5195310172e23402a8ea29c43;hp=fb9f3f7ea555c18480729a0e7d678f674f725813;hb=refs%2Ftags%2Fv3.8.6;hpb=e622ee6f4bf2b9fe81af59d566e651c983a4833b diff --git a/sql/expressions.lisp b/sql/expressions.lisp index fb9f3f7..df9cfc0 100644 --- a/sql/expressions.lisp +++ b/sql/expressions.lisp @@ -1,7 +1,7 @@ ;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*- ;;;; ************************************************************************* ;;;; -;;;; $Id: +;;;; $Id$ ;;;; ;;;; Classes defining SQL expressions and methods for formatting the ;;;; appropriate SQL commands. @@ -22,12 +22,40 @@ (defvar *sql-stream* nil "stream which accumulates SQL output") -(defun sql-output (sql-expr &optional database) +(defun sql-output (sql-expr &optional (database *default-database*)) + "Top-level call for generating SQL strings. Returns an SQL + string appropriate for DATABASE which corresponds to the + supplied lisp expression SQL-EXPR." (progv '(*sql-stream*) `(,(make-string-output-stream)) (output-sql sql-expr database) (get-output-stream-string *sql-stream*))) +(defmethod output-sql (expr database) + (write-string (database-output-sql expr database) *sql-stream*) + (values)) + +(defvar *output-hash* (make-hash-table :test #'equal) + "For caching generated SQL strings.") + +(defmethod output-sql :around ((sql t) database) + (let* ((hash-key (output-sql-hash-key sql database)) + (hash-value (when hash-key (gethash hash-key *output-hash*)))) + (cond ((and hash-key hash-value) + (write-string hash-value *sql-stream*)) + (hash-key + (let ((*sql-stream* (make-string-output-stream))) + (call-next-method) + (setf hash-value (get-output-stream-string *sql-stream*)) + (setf (gethash hash-key *output-hash*) hash-value)) + (write-string hash-value *sql-stream*)) + (t + (call-next-method))))) + +(defmethod output-sql-hash-key (expr database) + (declare (ignore expr database)) + nil) + (defclass %sql-expression () ()) @@ -39,7 +67,8 @@ (defmethod print-object ((self %sql-expression) stream) (print-unreadable-object (self stream :type t) - (write-string (sql-output self) stream))) + (write-string (sql-output self) stream)) + self) ;; For straight up strings @@ -63,13 +92,15 @@ (defmethod print-object ((ident sql) stream) (format stream "#<~S \"~A\">" (type-of ident) - (sql-output ident nil))) + (sql-output ident nil)) + ident) ;; For SQL Identifiers of generic type + (defclass sql-ident (%sql-expression) ((name :initarg :name - :initform "NULL")) + :initform +null-string+)) (:documentation "An SQL identifer.")) (defmethod make-load-form ((sql sql-ident) &optional environment) @@ -78,34 +109,13 @@ sql `(make-instance 'sql-ident :name ',name))) -(defvar *output-hash* (make-hash-table :test #'equal)) - -(defmethod output-sql-hash-key (expr database) - (declare (ignore expr database)) - nil) - -#+ignore -(defmethod output-sql :around ((sql t) database) - (let* ((hash-key (output-sql-hash-key sql database)) - (hash-value (when hash-key (gethash hash-key *output-hash*)))) - (cond ((and hash-key hash-value) - (write-string hash-value *sql-stream*)) - (hash-key - (let ((*sql-stream* (make-string-output-stream))) - (call-next-method) - (setf hash-value (get-output-stream-string *sql-stream*)) - (setf (gethash hash-key *output-hash*) hash-value)) - (write-string hash-value *sql-stream*)) - (t - (call-next-method))))) - (defmethod output-sql ((expr sql-ident) database) (with-slots (name) expr (write-string - (convert-to-db-default-case + (convert-to-db-default-case (etypecase name - (string name) - (symbol (symbol-name name))) + (string name) + (symbol (symbol-name name))) database) *sql-stream*)) t) @@ -115,10 +125,10 @@ (defclass sql-ident-attribute (sql-ident) ((qualifier :initarg :qualifier - :initform "NULL") + :initform +null-string+) (type :initarg :type - :initform "NULL")) + :initform +null-string+)) (:documentation "An SQL Attribute identifier.")) (defmethod collect-table-refs (sql) @@ -142,39 +152,40 @@ (defmethod output-sql ((expr sql-ident-attribute) database) (with-slots (qualifier name type) expr (if (and (not qualifier) (not type)) - (etypecase name - ;; Honor care of name - (string - (write-string name *sql-stream*)) - (symbol - (write-string (sql-escape (convert-to-db-default-case - (symbol-name name) database)) *sql-stream*))) - - ;;; KMR: The TYPE field is used by CommonSQL for type conversion -- it + (etypecase name + ;; Honor care of name + (string + (write-string name *sql-stream*)) + (symbol + (write-string (sql-escape (convert-to-db-default-case + (symbol-name name) database)) *sql-stream*))) + + ;;; KMR: The TYPE field is used by CommonSQL for type conversion -- it ;;; should not be output in SQL statements #+ignore (format *sql-stream* "~@[~A.~]~A~@[ ~A~]" - (when qualifier - (convert-to-db-default-case (sql-escape qualifier) database)) - (sql-escape (convert-to-db-default-case name database)) - (when type - (convert-to-db-default-case (symbol-name type) database))) + (when qualifier + (convert-to-db-default-case (sql-escape qualifier) database)) + (sql-escape (convert-to-db-default-case name database)) + (when type + (convert-to-db-default-case (symbol-name type) database))) (format *sql-stream* "~@[~A.~]~A" - (when qualifier - (typecase qualifier + (when qualifier + (typecase qualifier (string (format nil "~s" qualifier)) - (t (convert-to-db-default-case (sql-escape qualifier) + (t (convert-to-db-default-case (sql-escape qualifier) database)))) - (sql-escape (convert-to-db-default-case name database)))) + (sql-escape (convert-to-db-default-case name database)))) t)) (defmethod output-sql-hash-key ((expr sql-ident-attribute) database) - (declare (ignore database)) (with-slots (qualifier name type) - expr - (list 'sql-ident-attribute qualifier name type))) + expr + (list (and database (database-underlying-type database)) + 'sql-ident-attribute qualifier name type))) ;; For SQL Identifiers for tables + (defclass sql-ident-table (sql-ident) ((alias :initarg :table-alias :initform nil)) @@ -186,37 +197,28 @@ sql `(make-instance 'sql-ident-table :name ',name :table-alias ',alias))) -(defun generate-sql (expr database) - (let ((*sql-stream* (make-string-output-stream))) - (output-sql expr database) - (get-output-stream-string *sql-stream*))) - (defmethod output-sql ((expr sql-ident-table) database) - (with-slots (name alias) - expr - (if (null alias) - (write-string (sql-escape (convert-to-db-default-case (symbol-name name) database)) *sql-stream*) - (progn - (write-string (sql-escape (convert-to-db-default-case (symbol-name name) database)) *sql-stream*) - (write-char #\Space *sql-stream*) - (format *sql-stream* "~s" alias)))) + (with-slots (name alias) expr + (let ((namestr (if (symbolp name) + (symbol-name name) + name))) + (if (null alias) + (write-string + (sql-escape (convert-to-db-default-case namestr database)) + *sql-stream*) + (progn + (write-string + (sql-escape (convert-to-db-default-case namestr database)) + *sql-stream*) + (write-char #\Space *sql-stream*) + (format *sql-stream* "~s" alias))))) t) -#| -(defmethod database-output-sql ((self duration) database) - (declare (ignore database)) - (format nil "'~a'" (duration-timestring self))) - -(defmethod database-output-sql ((self money) database) - (database-output-sql (slot-value self 'odcl::units) database)) -|# - - (defmethod output-sql-hash-key ((expr sql-ident-table) database) - (declare (ignore database)) (with-slots (name alias) - expr - (list 'sql-ident-table name alias))) + expr + (list (and database (database-underlying-type database)) + 'sql-ident-table name alias))) (defclass sql-relational-exp (%sql-expression) ((operator @@ -227,6 +229,11 @@ :initform nil)) (:documentation "An SQL relational expression.")) +(defmethod make-load-form ((self sql-relational-exp) &optional environment) + (make-load-form-saving-slots self + :slot-names '(operator sub-expressions) + :environment environment)) + (defmethod collect-table-refs ((sql sql-relational-exp)) (let ((tabs nil)) (dolist (exp (slot-value sql 'sub-expressions)) @@ -262,10 +269,7 @@ (defclass sql-upcase-like (sql-relational-exp) () (:documentation "An SQL 'like' that upcases its arguments.")) - -;; Write SQL for relational operators (like 'AND' and 'OR'). -;; should do arity checking of subexpressions - + (defmethod output-sql ((expr sql-upcase-like) database) (flet ((write-term (term) (write-string "upper(" *sql-stream*) @@ -343,14 +347,10 @@ (:documentation "An SQL typecast expression.")) (defmethod output-sql ((expr sql-typecast-exp) database) - (database-output-sql expr database)) - -(defmethod database-output-sql ((expr sql-typecast-exp) database) (with-slots (components) expr (output-sql components database))) - (defmethod collect-table-refs ((sql sql-typecast-exp)) (when (slot-value sql 'components) (collect-table-refs (slot-value sql 'components)))) @@ -367,29 +367,31 @@ (defmethod collect-table-refs ((sql sql-function-exp)) (let ((tabs nil)) - (dolist (exp (slot-value sql 'components)) + (dolist (exp (slot-value sql 'args)) (let ((refs (collect-table-refs exp))) (if refs (setf tabs (append refs tabs))))) (remove-duplicates tabs :test (lambda (tab1 tab2) (equal (slot-value tab1 'name) (slot-value tab2 'name)))))) +(defvar *in-subselect* nil) (defmethod output-sql ((expr sql-function-exp) database) (with-slots (name args) expr (output-sql name database) - (when args (output-sql args database))) + (let ((*in-subselect* nil)) ;; aboid double parens + (when args (output-sql args database)))) t) (defclass sql-between-exp (sql-function-exp) - () + () (:documentation "An SQL between expression.")) (defmethod output-sql ((expr sql-between-exp) database) - (with-slots (name args) - expr + (with-slots (args) + expr (output-sql (first args) database) (write-string " BETWEEN " *sql-stream*) (output-sql (second args) database) @@ -397,7 +399,7 @@ (output-sql (third args) database)) t) -(defclass sql-query-modifier-exp (%sql-expression) +(defclass sql-query-modifier-exp (%sql-expression) ((modifier :initarg :modifier :initform nil) (components :initarg :components :initform nil)) (:documentation "An SQL query modifier expression.")) @@ -408,11 +410,11 @@ (output-sql modifier database) (write-string " " *sql-stream*) (output-sql (car components) database) - (when components - (mapc #'(lambda (comp) - (write-string ", " *sql-stream*) - (output-sql comp database)) - (cdr components)))) + (when components + (mapc #'(lambda (comp) + (write-string ", " *sql-stream*) + (output-sql comp database)) + (cdr components)))) t) (defclass sql-set-exp (%sql-expression) @@ -542,37 +544,35 @@ uninclusive, and the args from that keyword to the end." (find-class arg nil))) target-args)))) (multiple-value-bind (selections arglist) - (query-get-selections args) - (if (select-objects selections) - (destructuring-bind (&key flatp refresh &allow-other-keys) arglist - (make-instance 'sql-object-query :objects selections - :flatp flatp :refresh refresh - :exp arglist)) - (destructuring-bind (&key all flatp set-operation distinct from where - group-by having order-by - offset limit inner-join on &allow-other-keys) - arglist - (if (null selections) - (error "No target columns supplied to select statement.")) - (if (null from) - (error "No source tables supplied to select statement.")) - (make-instance 'sql-query :selections selections - :all all :flatp flatp :set-operation set-operation - :distinct distinct :from from :where where - :limit limit :offset offset - :group-by group-by :having having :order-by order-by - :inner-join inner-join :on on)))))) - -(defvar *in-subselect* nil) + (query-get-selections args) + (if (select-objects selections) + (destructuring-bind (&key flatp refresh &allow-other-keys) arglist + (make-instance 'sql-object-query :objects selections + :flatp flatp :refresh refresh + :exp arglist)) + (destructuring-bind (&key all flatp set-operation distinct from where + group-by having order-by + offset limit inner-join on &allow-other-keys) + arglist + (if (null selections) + (error "No target columns supplied to select statement.")) + (if (null from) + (error "No source tables supplied to select statement.")) + (make-instance 'sql-query :selections selections + :all all :flatp flatp :set-operation set-operation + :distinct distinct :from from :where where + :limit limit :offset offset + :group-by group-by :having having :order-by order-by + :inner-join inner-join :on on)))))) (defmethod output-sql ((query sql-query) database) (with-slots (distinct selections from where group-by having order-by - limit offset inner-join on all set-operation) + limit offset inner-join on all set-operation) query (when *in-subselect* (write-string "(" *sql-stream*)) (write-string "SELECT " *sql-stream*) - (when all + (when all (write-string "ALL " *sql-stream*)) (when (and distinct (not all)) (write-string "DISTINCT " *sql-stream*) @@ -583,10 +583,22 @@ uninclusive, and the args from that keyword to the end." (output-sql (apply #'vector selections) database) (when from (write-string " FROM " *sql-stream*) - (typecase from - (list (output-sql (apply #'vector from) database)) - (string (write-string from *sql-stream*)) - (t (output-sql from database)))) + (flet ((ident-table-equal (a b) + (and (if (and (eql (type-of a) 'sql-ident-table) + (eql (type-of b) 'sql-ident-table)) + (string-equal (slot-value a 'alias) + (slot-value b 'alias)) + t) + (string-equal (sql-escape (slot-value a 'name)) + (sql-escape (slot-value b 'name)))))) + (typecase from + (list (output-sql (apply #'vector + (remove-duplicates from + :test #'ident-table-equal)) + database)) + (string (write-string from *sql-stream*)) + (t (let ((*in-subselect* t)) + (output-sql from database)))))) (when inner-join (write-string " INNER JOIN " *sql-stream*) (output-sql inner-join database)) @@ -599,7 +611,19 @@ uninclusive, and the args from that keyword to the end." (output-sql where database))) (when group-by (write-string " GROUP BY " *sql-stream*) - (output-sql group-by database)) + (if (listp group-by) + (do ((order group-by (cdr order))) + ((null order)) + (let ((item (car order))) + (typecase item + (cons + (output-sql (car item) database) + (format *sql-stream* " ~A" (cadr item))) + (t + (output-sql item database))) + (when (cdr order) + (write-char #\, *sql-stream*)))) + (output-sql group-by database))) (when having (write-string " HAVING " *sql-stream*) (output-sql having database)) @@ -609,11 +633,11 @@ uninclusive, and the args from that keyword to the end." (do ((order order-by (cdr order))) ((null order)) (let ((item (car order))) - (typecase item - (cons + (typecase item + (cons (output-sql (car item) database) (format *sql-stream* " ~A" (cadr item))) - (t + (t (output-sql item database))) (when (cdr order) (write-char #\, *sql-stream*)))) @@ -626,7 +650,7 @@ uninclusive, and the args from that keyword to the end." (output-sql offset database)) (when *in-subselect* (write-string ")" *sql-stream*)) - (when set-operation + (when set-operation (write-char #\Space *sql-stream*) (output-sql set-operation database))) t) @@ -661,10 +685,10 @@ uninclusive, and the args from that keyword to the end." (with-slots (into attributes values query) ins (write-string "INSERT INTO " *sql-stream*) - (output-sql + (output-sql (typecase into (string (sql-expression :attribute into)) - (t into)) + (t into)) database) (when attributes (write-char #\Space *sql-stream*) @@ -694,7 +718,7 @@ uninclusive, and the args from that keyword to the end." stmt (write-string "DELETE FROM " *sql-stream*) (typecase from - (symbol (write-string (sql-escape from) *sql-stream*)) + ((or symbol string) (write-string (sql-escape from) *sql-stream*)) (t (output-sql from database))) (when where (write-string " WHERE " *sql-stream*) @@ -771,10 +795,10 @@ uninclusive, and the args from that keyword to the end." (write-char #\Space *sql-stream*) (write-string (if (stringp db-type) db-type ; override definition - (database-get-type-specifier (car type) (cdr type) database - (database-underlying-type database))) + (database-get-type-specifier (car type) (cdr type) database + (database-underlying-type database))) *sql-stream*) - (let ((constraints (database-constraint-statement + (let ((constraints (database-constraint-statement (if (and db-type (symbolp db-type)) (cons db-type constraints) constraints) @@ -799,9 +823,9 @@ uninclusive, and the args from that keyword to the end." (write-string (car modifier) *sql-stream*))) (write-char #\) *sql-stream*) (when (and (eq :mysql (database-underlying-type database)) - transactions - (db-type-transaction-capable? :mysql database)) - (write-string " Type=InnoDB" *sql-stream*)))) + transactions + (db-type-transaction-capable? :mysql database)) + (write-string " Type=InnoDB" *sql-stream*)))) t) @@ -826,35 +850,136 @@ uninclusive, and the args from that keyword to the end." ;; -;; Column constraint types +;; DATABASE-OUTPUT-SQL ;; -(defparameter *constraint-types* - (list - (cons (symbol-name-default-case "NOT-NULL") "NOT NULL") - (cons (symbol-name-default-case "PRIMARY-KEY") "PRIMARY KEY") - (cons (symbol-name-default-case "NOT") "NOT") - (cons (symbol-name-default-case "NULL") "NULL") - (cons (symbol-name-default-case "PRIMARY") "PRIMARY") - (cons (symbol-name-default-case "KEY") "KEY"))) + +(defmethod database-output-sql ((str string) database) + (declare (optimize (speed 3) (safety 1) + #+cmu (extensions:inhibit-warnings 3))) + (let ((len (length str))) + (declare (type fixnum len)) + (cond ((zerop len) + +empty-string+) + ((and (null (position #\' str)) + (null (position #\\ str))) + (concatenate 'string "'" str "'")) + (t + (let ((buf (make-string (+ (* len 2) 2) :initial-element #\'))) + (declare (simple-string buf)) + (do* ((i 0 (incf i)) + (j 1 (incf j))) + ((= i len) (subseq buf 0 (1+ j))) + (declare (type fixnum i j)) + (let ((char (aref str i))) + (declare (character char)) + (cond ((char= char #\') + (setf (aref buf j) #\') + (incf j) + (setf (aref buf j) #\')) + ((and (char= char #\\) + ;; MTP: only escape backslash with pgsql/mysql + (member (database-underlying-type database) + '(:postgresql :mysql) + :test #'eq)) + (setf (aref buf j) #\\) + (incf j) + (setf (aref buf j) #\\)) + (t + (setf (aref buf j) char)))))))))) + +(let ((keyword-package (symbol-package :foo))) + (defmethod database-output-sql ((sym symbol) database) + (if (null sym) + +null-string+ + (convert-to-db-default-case + (if (equal (symbol-package sym) keyword-package) + (concatenate 'string "'" (string sym) "'") + (symbol-name sym)) + database)))) + +(defmethod database-output-sql ((tee (eql t)) database) + (if database + (let ((val (database-output-sql-as-type 'boolean t database (database-type database)))) + (when val + (typecase val + (string (format nil "'~A'" val)) + (integer (format nil "~A" val))))) + "'Y'")) + +#+nil(defmethod database-output-sql ((tee (eql t)) database) + (declare (ignore database)) + "'Y'") + +(defmethod database-output-sql ((num number) database) + (declare (ignore database)) + (number-to-sql-string num)) + +(defmethod database-output-sql ((arg list) database) + (if (null arg) + +null-string+ + (format nil "(~{~A~^,~})" (mapcar #'(lambda (val) + (sql-output val database)) + arg)))) + +(defmethod database-output-sql ((arg vector) database) + (format nil "~{~A~^,~}" (map 'list #'(lambda (val) + (sql-output val database)) + arg))) + +(defmethod output-sql-hash-key ((arg vector) database) + (list 'vector (map 'list (lambda (arg) + (or (output-sql-hash-key arg database) + (return-from output-sql-hash-key nil))) + arg))) + +(defmethod database-output-sql ((self wall-time) database) + (declare (ignore database)) + (db-timestring self)) + +(defmethod database-output-sql ((self date) database) + (declare (ignore database)) + (db-datestring self)) + +(defmethod database-output-sql ((self duration) database) + (declare (ignore database)) + (format nil "'~a'" (duration-timestring self))) + +#+ignore +(defmethod database-output-sql ((self money) database) + (database-output-sql (slot-value self 'odcl::units) database)) + +(defmethod database-output-sql (thing database) + (if (or (null thing) + (eq 'null thing)) + +null-string+ + (error 'sql-user-error + :message + (format nil + "No type conversion to SQL for ~A is defined for DB ~A." + (type-of thing) (type-of database))))) + ;; -;; Convert type spec to sql syntax +;; Column constraint types and conversion to SQL ;; -(defmethod database-constraint-description (constraint database) - (declare (ignore database)) - (let ((output (assoc (symbol-name constraint) *constraint-types* - :test #'equal))) - (if (null output) - (error 'sql-user-error - :message (format nil "unsupported column constraint '~A'" - constraint)) - (cdr output)))) +(defparameter *constraint-types* + (list + (cons (symbol-name-default-case "NOT-NULL") "NOT NULL") + (cons (symbol-name-default-case "PRIMARY-KEY") "PRIMARY KEY") + (cons (symbol-name-default-case "NOT") "NOT") + (cons (symbol-name-default-case "NULL") "NULL") + (cons (symbol-name-default-case "PRIMARY") "PRIMARY") + (cons (symbol-name-default-case "KEY") "KEY") + (cons (symbol-name-default-case "UNSIGNED") "UNSIGNED") + (cons (symbol-name-default-case "ZEROFILL") "ZEROFILL") + (cons (symbol-name-default-case "AUTO-INCREMENT") "AUTO_INCREMENT") + (cons (symbol-name-default-case "UNIQUE") "UNIQUE"))) (defmethod database-constraint-statement (constraint-list database) (declare (ignore database)) (make-constraints-description constraint-list)) - + (defun make-constraints-description (constraint-list) (if constraint-list (let ((string "")) @@ -866,7 +991,7 @@ uninclusive, and the args from that keyword to the end." (if (null output) (error 'sql-user-error :message (format nil "unsupported column constraint '~A'" - constraint)) + constraint)) (setq string (concatenate 'string string (cdr output)))) (if (< 1 (length constraint)) (setq string (concatenate 'string string " "))))))))