#| access to data in a mysql DB This relies on mysql command shell from which to run it clisp ext:run-shell-command sample usage: suppose you have access to a mysql database via the command mysql --password=pass --user=username dbname Suppose further that in that database there is a table named table1 with columns (among others) x, y, z, where x is numeric and y and z are text. And suppose that this table has indices on the x and y columns (which you want to use). The following gives you access to table1 via the ap5 relation T1 (defrelation T1 :representation (mysql "table1" "mysql -N -B -e ~S --password=pass --user=username dbname" ("x" "y" "z") (number string string) :indices (("x" "y")))) You can now test whether a tuple is in the relation (?? t1 3 "a" "b") or generate a list of tuples (listof t1) or a list of tuples where x is 15 (listof (x y) s.t. (t1 15 x y)) etc. |# ;; various code below uses unexported functions ;; easier to just use ap5 pkg than to use ::'s (in-package :ap5) (defun parse-mysql-select (stream coltypes) "parse output of command mysql -N -B -e 'SELECT...' ... fields are separated by tabs coltypes should be a list of types in mysql-parse-types" ;; we currently assume strings do not contain tabs (loop with line while (setf line (read-line stream nil nil)) collect (parse-mysql-select-line line coltypes))) (defun parse-mysql-select-line (line types &aux *read-eval* pos tab) (setf tab -1) (loop for type in types do (setf pos (1+ tab) tab (position #\tab line :start pos)) collect (ecase type (string (subseq line pos tab)) (number (read-from-string line t nil :start pos :end tab))))) ;; types we accept for mysql relations (defvar mysql-parse-types '(string number)) ;; add dates/times? (defun mysql (relname defrel-args sqlname command colnames coltypes &key indices) ;; sqlname is the name of the relation in mysql ;; command is a string with a ~S in it for accessing the relation ;; the ~S is to be replaced with a SQL statement. ;; something like "mysql -N -B -e ~S --password=foo test" ;; colnames is a list of column names ;; coltypes is a corresponding list of types, limited to mysql-parse-types ;; indices is a list of lists of column names, indicating that we should ;; build a generator in which those columns are the inputs ;; ;; (any x s.t. (parameter-list rel x))) = (mysql ...) (let ((arity (length colnames))) (unless (= arity (length coltypes)) (error "mysql needs the same number of column names and column types")) (loop for type in coltypes unless (member type mysql-parse-types) do (error "type ~a not in ~a" type mysql-parse-types)) (append `(:representation individual :nonatomic t :arity ,arity :types ,coltypes :type-enforcements ,(loop for i below arity collect :none) :tester mysql-tester :generator (mysql-generators)) defrel-args))) (defun mysql-tester (rel &rest ignore) (declare (ignore ignore)) `(lambda (rel &rest tuple) (let ((params (any x s.t. (parameter-list rel x))) stream) (setf stream (ext:run-shell-command (format nil (third params) (with-output-to-string (s) (format s "SELECT * FROM ~a where " (second params)) (loop for col on (fourth params) as val in tuple do (format s "~a=~s" (car col) val) (when (cdr col) (format s " and "))))) :output :stream)) (unwind-protect (not (not (read-line stream nil nil))) (close stream))))) (defun sql-gen (pat params) ;; pat is the subset of params supplied as inputs `(lambda (rel &rest args) (declare (ignore rel)) (let ((state (let (stream) (setf stream (ext:run-shell-command (apply #'format nil ,(format nil (third params) (format nil "SELECT ~a FROM ~a~a~a" (with-output-to-string (s) (loop with done1 = nil for col in (fourth params) unless (member col pat :test 'string=) do (format s (if done1 ",~a" "~a") col) (setf done1 t))) (second params) (if pat " where " "") (with-output-to-string (s) (loop with done1 = nil for col in (fourth params) when (member col pat :test 'string=) do (when done1 (format s " and ")) (setf done1 t) (format s "~a=~~s" col))))) args) :output :stream)) (unwind-protect (parse-mysql-select stream ',(loop for p in (fifth params) as a in (fourth params) unless (member a pat :test 'string=) collect p)) (close stream))))) #'(lambda (&rest ignore) (declare (ignore ignore)) (if state #+clisp;; clisp likes the original (apply #'values nil (pop state)) #-clisp ;; most others seem to like this much better (values-list (cons nil (pop state))) t))))) #| effort per tuple generated, *** need to calibrate should be divided into overhead and incremental cost per tuple |# (defvar *mysql-effort-per-tuple* 300) (defun mysql-generators ;; originally baserelgenerator (ignore vars rel &rest args) (declare (ignore ignore vars)) ;; this test should never fail -- we believe it is TOTALLY unnecessary (unless (= (length args) (arity* rel)) (error "wrong arity ~s" (cons rel args))) ; new version: initialstate contains code which is applied to ; (rel &rest constant-args) [as before] but returns [rather than state] ; a closure which can be funcall'd to produce what pulsecode used to ; return [except new state] (values-list ;; (cons ...) #+ignore `((initialstate sql-total-gen);; originally baserelgen (template ;;,(loop for arg in args collect 'output) ,(make-list (length args) :initial-element 'output)) (effort , (iftimes (relationsize args (cons rel args)) *mysql-effort-per-tuple*))) (loop with params = (any x s.t. (parameter-list rel x)) for pat in (cons nil (cadr (member :indices params))) collect `((initialstate ,(sql-gen pat params)) (template ,(loop for arg in (fourth params) collect (if (member arg pat :test 'string=) 'input 'output))) (effort , (iftimes (relationsize (loop for a in args as arg in (fourth params) unless (member arg pat :test 'string=) collect a) (cons rel args)) *mysql-effort-per-tuple*))))))