X-Git-Url: http://git.kpe.io/?p=clsql.git;a=blobdiff_plain;f=db-mysql%2Fmysql-sql.lisp;h=39bee7ef0c5ed25a97c44a213bef67d625086eb1;hp=7cc6f50ba53d126b0a6af193125221fcb1e354b4;hb=44c87a075cbe29141d90dc49c501f382a6d81fbe;hpb=a4097e19c5157e87b9991549bc44f3ef598aeb90 diff --git a/db-mysql/mysql-sql.lisp b/db-mysql/mysql-sql.lisp index 7cc6f50..39bee7e 100644 --- a/db-mysql/mysql-sql.lisp +++ b/db-mysql/mysql-sql.lisp @@ -14,7 +14,7 @@ ;;;; ************************************************************************* (defpackage #:clsql-mysql - (:use #:common-lisp #:clsql-base-sys #:mysql #:clsql-uffi) + (:use #:common-lisp #:clsql-sys #:mysql #:clsql-uffi) (:export #:mysql-database) (:documentation "This is the CLSQL interface to MySQL.")) @@ -22,15 +22,26 @@ ;;; Field conversion functions +(defun result-field-names (num-fields res-ptr) + (declare (fixnum num-fields)) + (let ((names '()) + (field-vec (mysql-fetch-fields res-ptr))) + (dotimes (i num-fields) + (declare (fixnum i)) + (let* ((field (uffi:deref-array field-vec '(:array mysql-field) i)) + (name (uffi:convert-from-foreign-string + (uffi:get-slot-value field 'mysql-field 'mysql::name)))) + (push name names))) + (nreverse names))) + (defun make-type-list-for-auto (num-fields res-ptr) (declare (fixnum num-fields)) (let ((new-types '()) - #+ignore (field-vec (mysql-fetch-fields res-ptr))) + (field-vec (mysql-fetch-fields res-ptr))) (dotimes (i num-fields) (declare (fixnum i)) - (let* ( (field (mysql-fetch-field-direct res-ptr i)) - #+ignore (field (uffi:deref-array field-vec '(:array mysql-field) i)) - (type (uffi:get-slot-value field 'mysql-field 'type))) + (let* ((field (uffi:deref-array field-vec '(:array mysql-field) i)) + (type (uffi:get-slot-value field 'mysql-field 'type))) (push (case type ((#.mysql-field-types#tiny @@ -88,11 +99,11 @@ (let ((mysql-ptr (mysql-init (uffi:make-null-pointer 'mysql-mysql))) (socket nil)) (if (uffi:null-pointer-p mysql-ptr) - (error 'clsql-connect-error + (error 'sql-connection-error :database-type database-type :connection-spec connection-spec - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)) + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)) (uffi:with-cstrings ((host-native host) (user-native user) (password-native password) @@ -106,11 +117,11 @@ db-native 0 socket-native 0)) (progn (setq error-occurred t) - (error 'clsql-connect-error + (error 'sql-connection-error :database-type database-type :connection-spec connection-spec - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr))) + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr))) (make-instance 'mysql-database :name (database-name-from-spec connection-spec database-type) @@ -127,7 +138,7 @@ (defmethod database-query (query-expression (database mysql-database) - result-types) + result-types field-names) (declare (optimize (speed 3) (safety 0) (debug 0) (space 0))) (let ((mysql-ptr (database-mysql-ptr database))) (uffi:with-cstring (query-native query-expression) @@ -141,34 +152,37 @@ (setq result-types (canonicalize-types result-types num-fields res-ptr)) - (loop for row = (mysql-fetch-row res-ptr) - for lengths = (mysql-fetch-lengths res-ptr) - until (uffi:null-pointer-p row) - collect - (do* ((rlist (make-list num-fields)) - (i 0 (1+ i)) - (pos rlist (cdr pos))) - ((= i num-fields) rlist) - (declare (fixnum i)) - (setf (car pos) - (convert-raw-field - (uffi:deref-array row '(:array - (* :unsigned-char)) - i) - result-types i - (uffi:deref-array lengths '(:array :unsigned-long) - i)))))) + (values + (loop for row = (mysql-fetch-row res-ptr) + for lengths = (mysql-fetch-lengths res-ptr) + until (uffi:null-pointer-p row) + collect + (do* ((rlist (make-list num-fields)) + (i 0 (1+ i)) + (pos rlist (cdr pos))) + ((= i num-fields) rlist) + (declare (fixnum i)) + (setf (car pos) + (convert-raw-field + (uffi:deref-array row '(:array + (* :unsigned-char)) + i) + result-types i + (uffi:deref-array lengths '(:array :unsigned-long) + i))))) + (when field-names + (result-field-names num-fields res-ptr)))) (mysql-free-result res-ptr)) - (error 'clsql-sql-error + (error 'sql-database-data-error :database database :expression query-expression - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)))) - (error 'clsql-sql-error + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))) + (error 'sql-database-data-error :database database :expression query-expression - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)))))) + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))))) (defmethod database-execute-command (sql-expression (database mysql-database)) (uffi:with-cstring (sql-native sql-expression) @@ -177,11 +191,11 @@ (if (zerop (mysql-real-query mysql-ptr sql-native (length sql-expression))) t - (error 'clsql-sql-error + (error 'sql-database-data-error :database database :expression sql-expression - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)))))) + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))))) (defstruct mysql-result-set @@ -219,16 +233,16 @@ (mysql-num-rows res-ptr)) (values result-set num-fields))) - (error 'clsql-sql-error + (error 'sql-database-data-error :database database :expression query-expression - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)))) - (error 'clsql-sql-error + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))) + (error 'sql-database-data-error :database database :expression query-expression - :errno (mysql-errno mysql-ptr) - :error (mysql-error-string mysql-ptr)))))) + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))))) (defmethod database-dump-result-set (result-set (database mysql-database)) (mysql-free-result (mysql-result-set-res-ptr result-set)) @@ -261,8 +275,8 @@ (declare (ignore owner)) (remove-if #'(lambda (s) (and (>= (length s) 11) - (string= (subseq s 0 11) "_clsql_seq_"))) - (mapcar #'car (database-query "SHOW TABLES" database nil)))) + (string-equal (subseq s 0 11) "_CLSQL_SEQ_"))) + (mapcar #'car (database-query "SHOW TABLES" database nil nil)))) ;; MySQL 4.1 does not support views (defmethod database-list-views ((database mysql-database) @@ -284,7 +298,7 @@ (do ((results nil) (rows (database-query (format nil "SHOW INDEX FROM ~A" (string-upcase table)) - database nil) + database nil nil) (cdr rows))) ((null rows) (nreverse results)) (let ((col (nth 2 (car rows)))) @@ -297,32 +311,36 @@ (mapcar #'car (database-query (format nil "SHOW COLUMNS FROM ~A" table) - database nil))) + database nil nil))) (defmethod database-attribute-type (attribute (table string) (database mysql-database) &key (owner nil)) (declare (ignore owner)) - (let ((result - (mapcar #'cadr - (database-query - (format nil - "SHOW COLUMNS FROM ~A LIKE '~A'" table attribute) - database nil)))) - (let* ((str (car result)) - (end-str (position #\( str)) - (substr (subseq str 0 end-str))) - (if substr - (intern (string-upcase substr) :keyword) nil)))) + (let ((row (car (database-query + (format nil + "SHOW COLUMNS FROM ~A LIKE '~A'" table attribute) + database nil nil)))) + (let* ((raw-type (second row)) + (null (third row)) + (start-length (position #\( raw-type)) + (type (if start-length + (subseq raw-type 0 start-length) + raw-type)) + (length (when start-length + (parse-integer (subseq raw-type (1+ start-length)) + :junk-allowed t)))) + (when type + (values (ensure-keyword type) length nil (if (string-equal null "YES") 1 0)))))) ;;; Sequence functions (defun %sequence-name-to-table (sequence-name) - (concatenate 'string "_clsql_seq_" (sql-escape sequence-name))) + (concatenate 'string "_CLSQL_SEQ_" (sql-escape sequence-name))) (defun %table-name-to-sequence-name (table-name) (and (>= (length table-name) 11) - (string= (subseq table-name 0 11) "_clsql_seq_") + (string-equal (subseq table-name 0 11) "_CLSQL_SEQ_") (subseq table-name 11))) (defmethod database-create-sequence (sequence-name @@ -346,9 +364,10 @@ (defmethod database-list-sequences ((database mysql-database) &key (owner nil)) (declare (ignore owner)) - (mapcar #'(lambda (s) (%table-name-to-sequence-name (car s))) - (database-query "SHOW TABLES LIKE '%clsql_seq%'" - database nil))) + (mapcan #'(lambda (s) + (let ((sn (%table-name-to-sequence-name (car s)))) + (and sn (list sn)))) + (database-query "SHOW TABLES" database nil nil))) (defmethod database-set-sequence-position (sequence-name (position integer) @@ -373,33 +392,31 @@ (defmethod database-create (connection-spec (type (eql :mysql))) (destructuring-bind (host name user password) connection-spec (multiple-value-bind (output status) - (clsql-base-sys:command-output "mysqladmin create -u~A -p~A -h~A ~A" + (clsql-sys:command-output "mysqladmin create -u~A -p~A -h~A ~A" user password (if host host "localhost") name) (if (or (not (eql 0 status)) (and (search "failed" output) (search "error" output))) - (error 'clsql-access-error - :connection-spec connection-spec - :database-type type - :error - (format nil "database-create failed: ~A" output)) - t)))) + (error 'sql-database-error + :message + (format nil "mysql database creation failed with connection-spec ~A." + connection-spec)) + t)))) (defmethod database-destroy (connection-spec (type (eql :mysql))) (destructuring-bind (host name user password) connection-spec (multiple-value-bind (output status) - (clsql-base-sys:command-output "mysqladmin drop -f -u~A -p~A -h~A ~A" + (clsql-sys:command-output "mysqladmin drop -f -u~A -p~A -h~A ~A" user password (if host host "localhost") name) (if (or (not (eql 0 status)) (and (search "failed" output) (search "error" output))) - (error 'clsql-access-error - :connection-spec connection-spec - :database-type type - :error - (format nil "database-destroy failed: ~A" output)) + (error 'sql-database-error + :message + (format nil "mysql database deletion failed with connection-spec ~A." + connection-spec)) t)))) (defmethod database-probe (connection-spec (type (eql :mysql))) @@ -413,17 +430,290 @@ (let ((database (database-connect (list host "mysql" user password) type))) (unwind-protect (progn - (setf (slot-value database 'clsql-base-sys::state) :open) - (mapcar #'car (database-query "show databases" database :auto))) + (setf (slot-value database 'clsql-sys::state) :open) + (mapcar #'car (database-query "show databases" database :auto nil))) (progn (database-disconnect database) - (setf (slot-value database 'clsql-base-sys::state) :closed)))))) + (setf (slot-value database 'clsql-sys::state) :closed)))))) + + +;;; Prepared statements + +(defclass mysql-stmt () + ((database :initarg :database :reader database) + (stmt :initarg :stmt :accessor stmt) + (input-bind :initarg :input-bind :reader input-bind) + (output-bind :initarg :output-bind :reader output-bind) + (types :initarg :types :reader types) + (result-set :initarg :result-set :reader result-set) + (num-fields :initarg :num-fields :reader num-fields) + (field-names :initarg :field-names :accessor stmt-field-names) + (length-ptr :initarg :length-ptr :reader length-ptr) + (is-null-ptr :initarg :is-null-ptr :reader is-null-ptr) + (result-types :initarg :result-types :reader result-types))) + +(defun clsql-type->mysql-type (type) + (cond + ((in type :null) mysql-field-types#null) + ((in type :int :integer) mysql-field-types#long) + ((in type :short) mysql-field-types#short) + ((in type :bigint) mysql-field-types#longlong) + ((in type :float :double :number) mysql-field-types#double) + ((and (consp type) (in (car type) :char :string :varchar)) mysql-field-types#var-string) + ((or (eq type :blob) (and (consp type) (in (car type) :blob))) mysql-field-types#var-string) + (t + (error 'sql-user-error + :message + (format nil "Unknown clsql type ~A." type))))) + +#+mysql-client-v4.1 +(defmethod database-prepare (sql-stmt types (database mysql-database) result-types field-names) + (let* ((mysql-ptr (database-mysql-ptr database)) + (stmt (mysql-stmt-init mysql-ptr))) + (when (uffi:null-pointer-p stmt) + (error 'sql-database-error + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr))) + + (uffi:with-cstring (native-query sql-stmt) + (unless (zerop (mysql-stmt-prepare stmt native-query (length sql-stmt))) + (mysql-stmt-close stmt) + (error 'sql-database-error + :error-id (mysql-errno mysql-ptr) + :message (mysql-error-string mysql-ptr)))) + + (unless (= (mysql-stmt-param-count stmt) (length types)) + (mysql-stmt-close stmt) + (error 'sql-database-error + :message + (format nil "Mysql param count (~D) does not match number of types (~D)" + (mysql-stmt-param-count stmt) (length types)))) + + (let ((rs (mysql-stmt-result-metadata stmt))) + (when (uffi:null-pointer-p rs) + (warn "mysql_stmt_result_metadata returned NULL") + #+nil + (mysql-stmt-close stmt) + #+nil + (error 'sql-database-error + :message "mysql_stmt_result_metadata returned NULL")) + + (let ((input-bind (uffi:allocate-foreign-object 'mysql-bind (length types))) + (mysql-types (mapcar 'clsql-type->mysql-type types)) + field-vec num-fields is-null-ptr output-bind length-ptr) + + (print 'a) + (dotimes (i (length types)) + (let* ((binding (uffi:deref-array input-bind '(:array mysql-bind) i))) + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer-type) + (nth i mysql-types)) + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer-length) 0))) + + (print 'b) + (unless (uffi:null-pointer-p rs) + (setq field-vec (mysql-fetch-fields rs) + num-fields (mysql-num-fields rs) + is-null-ptr (uffi:allocate-foreign-object :byte num-fields) + output-bind (uffi:allocate-foreign-object 'mysql-bind num-fields) + length-ptr (uffi:allocate-foreign-object :unsigned-long num-fields)) + (dotimes (i num-fields) + (declare (fixnum i)) + (let* ((field (uffi:deref-array field-vec '(:array mysql-field) i)) + (type (uffi:get-slot-value field 'mysql-field 'type)) + (binding (uffi:deref-array output-bind '(:array mysql-bind) i))) + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer-type) type) + + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer-length) 0) + (setf (uffi:get-slot-value binding 'mysql-bind 'is-null) + (+ i (uffi:pointer-address is-null-ptr))) + (setf (uffi:get-slot-value binding 'mysql-bind 'length) + (+ (* i 8) (uffi:pointer-address length-ptr))) + + (case type + ((#.mysql-field-types#var-string #.mysql-field-types#string + #.mysql-field-types#tiny-blob #.mysql-field-types#blob + #.mysql-field-types#medium-blob #.mysql-field-types#long-blob) + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer-length) 1024) + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :unsigned-char 1024))) + (#.mysql-field-types#tiny + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :byte))) + (#.mysql-field-types#short + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :short))) + (#.mysql-field-types#long + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + ;; segfaults if supply :int on amd64 + (uffi:allocate-foreign-object :long))) + #+64bit + (#.mysql-field-types#longlong + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :long))) + (#.mysql-field-types#float + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :float))) + (#.mysql-field-types#double + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) + (uffi:allocate-foreign-object :double))) + ((#.mysql-field-types#time #.mysql-field-types#date + #.mysql-field-types#datetime #.mysql-field-types#timestamp) + (uffi:allocate-foreign-object 'mysql-time)) + (t + (error "mysql type ~D not supported." type))))) + + (unless (zerop (mysql-stmt-bind-result stmt output-bind)) + (mysql-stmt-close stmt) + (error 'sql-database-error + :error-id (mysql-stmt-errno stmt) + :message (uffi:convert-from-cstring + (mysql-stmt-error stmt))))) + + (make-instance 'mysql-stmt + :database database + :stmt stmt + :num-fields num-fields + :input-bind input-bind + :output-bind output-bind + :result-set rs + :result-types result-types + :length-ptr length-ptr + :is-null-ptr is-null-ptr + :types mysql-types + :field-names field-names))))) + +#+mysql-client-v4.1 +(defmethod database-bind-parameter ((stmt mysql-stmt) position value) + ;; FIXME: will need to allocate bind structure. This should probably be + ;; done in C since the API is not mature and may change + (let ((binding (uffi:deref-array (input-bind stmt) '(:array mysql-bind) (1- position))) + (type (nth (1- position) (types stmt)))) + (setf (uffi:get-slot-value binding 'mysql-bind 'length) 0) + (cond + ((null value) + (when (is-null-ptr stmt) + (setf (uffi:deref-array (is-null-ptr stmt) '(:array :byte) (1- position)) 1))) + (t + (when (is-null-ptr stmt) + (setf (uffi:deref-array (is-null-ptr stmt) '(:array :byte) (1- position)) 0)) + (case type + (#.mysql-field-types#long + (setf (uffi:get-slot-value binding 'mysql-bind 'buffer) value)) + (t + (warn "Unknown input bind type ~D." type)) + ))))) + +#+mysql-client-v4.1 +(defmethod database-run-prepared ((stmt mysql-stmt)) + (print 'a1) + (when (input-bind stmt) + (unless (zerop (mysql-stmt-bind-param (stmt stmt) (input-bind stmt))) + (error 'sql-database-error + :error-id (mysql-stmt-errno (stmt stmt)) + :message (uffi:convert-from-cstring + (mysql-stmt-error (stmt stmt)))))) + (print 'a2) + (unless (zerop (mysql-stmt-execute (stmt stmt))) + (error 'sql-database-error + :error-id (mysql-stmt-errno (stmt stmt)) + :message (uffi:convert-from-cstring + (mysql-stmt-error (stmt stmt))))) + (print 'a3) + (unless (zerop (mysql-stmt-store-result (stmt stmt))) + (error 'sql-database-error + :error-id (mysql-stmt-errno (stmt stmt)) + :message (uffi:convert-from-cstring + (mysql-stmt-error (stmt stmt))))) + (database-fetch-prepared-rows stmt)) + +#+mysql-client-v4.1 +(defun database-fetch-prepared-rows (stmt) + (do ((rc (mysql-stmt-fetch (stmt stmt)) (mysql-stmt-fetch (stmt stmt))) + (num-fields (num-fields stmt)) + (rows '())) + ((not (zerop rc)) (nreverse rows)) + (push + (loop for i from 0 below num-fields + collect + (let ((is-null + (not (zerop (uffi:ensure-char-integer + (uffi:deref-array (is-null-ptr stmt) '(:array :byte) i)))))) + (unless is-null + (let* ((bind (uffi:deref-array (output-bind stmt) '(:array mysql-bind) i)) + (type (uffi:get-slot-value bind 'mysql-bind 'buffer-type)) + (buffer (uffi:get-slot-value bind 'mysql-bind 'buffer))) + (case type + ((#.mysql-field-types#var-string #.mysql-field-types#string + #.mysql-field-types#tiny-blob #.mysql-field-types#blob + #.mysql-field-types#medium-blob #.mysql-field-types#long-blob) + (uffi:convert-from-foreign-string buffer)) + (#.mysql-field-types#tiny + (uffi:ensure-char-integer + (uffi:deref-pointer buffer :byte))) + (#.mysql-field-types#short + (uffi:deref-pointer buffer :short)) + (#.mysql-field-types#long + (uffi:deref-pointer buffer :int)) + #+64bit + (#.mysql-field-types#longlong + (uffi:deref-pointer buffer :long)) + (#.mysql-field-types#float + (uffi:deref-pointer buffer :float)) + (#.mysql-field-types#double + (uffi:deref-pointer buffer :double)) + ((#.mysql-field-types#time #.mysql-field-types#date + #.mysql-field-types#datetime #.mysql-field-types#timestamp) + (let ((year (uffi:get-slot-value buffer 'mysql-time 'mysql::year)) + (month (uffi:get-slot-value buffer 'mysql-time 'mysql::month)) + (day (uffi:get-slot-value buffer 'mysql-time 'mysql::day)) + (hour (uffi:get-slot-value buffer 'mysql-time 'mysql::hour)) + (minute (uffi:get-slot-value buffer 'mysql-time 'mysql::minute)) + (second (uffi:get-slot-value buffer 'mysql-time 'mysql::second))) + (db-timestring + (make-time :year year :month month :day day :hour hour + :minute minute :second second)))) + (t + (list type))))))) + rows))) + + + + +#+mysql-client-v4.1 +(defmethod database-free-prepared ((stmt mysql-stmt)) + (with-slots (stmt) stmt + (mysql-stmt-close stmt)) + ) + ;;; Database capabilities -(defmethod db-use-column-on-drop-index? ((database mysql-database)) +(defmethod db-type-use-column-on-drop-index? ((db-type (eql :mysql))) t) -(when (clsql-base-sys:database-type-library-loaded :mysql) - (clsql-base-sys:initialize-database-type :database-type :mysql)) +(defmethod db-type-has-views? ((db-type (eql :mysql))) + #+mysql-client-v5.1 t + #-mysql-client-v5.1 nil) + +(defmethod db-type-has-subqueries? ((db-type (eql :mysql))) + #+mysql-client-v4.1 t + #-mysql-client-v4.1 nil) + +(defmethod db-type-has-boolean-where? ((db-type (eql :mysql))) + #+mysql-client-v4.1 t + #-mysql-client-v4.1 nil) + +(defmethod db-type-has-union? ((db-type (eql :mysql))) + (not (eql (schar mysql::*mysql-client-info* 0) #\3))) + +(defmethod db-type-transaction-capable? ((db-type (eql :mysql)) database) + (let ((tuple (car (database-query "SHOW VARIABLES LIKE 'HAVE_INNODB'" database :auto nil)))) + (and tuple (string-equal "YES" (second tuple))))) + +(defmethod db-type-has-prepared-stmt? ((db-type (eql :mysql))) + #+mysql-client-v4.1 t + #-mysql-client-v4.1 nil) + +(when (clsql-sys:database-type-library-loaded :mysql) + (clsql-sys:initialize-database-type :database-type :mysql))