cl-2sql

Lisp to SQL compiler for Common Lisp
git clone https://logand.com/git/cl-2sql.git/
Log | Files | Refs | README | LICENSE

commit aa3ade3e5295a4464e080d5fcfaaad5a8be824b2
parent 9e15c9aa894103468b616d573394a5a2042f9142
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun,  7 Aug 2011 20:49:14 +0200

2sql-backend and 2sql-orm added

Diffstat:
MREADME | 21++++++++++++++++++++-
Abackend.lisp | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcl-2sql.asd | 16+++++++++++++---
Acompiler.lisp | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore.lisp | 86-------------------------------------------------------------------------------
Mmacros.lisp | 722+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Aorm.lisp | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages.lisp | 38+++++++++++++++++++++++++++++---------
Aprinter.lisp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest.lisp | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1309 insertions(+), 224 deletions(-)

diff --git a/README b/README @@ -11,11 +11,30 @@ - http://marijnhaverbeke.nl/postmodern/s-sql.html - completion, M-. - not class based + - classes are for state, not syntax - bloat (rdbms) + - sins: oo for syntax, format, custom readers - the above => arbitrary magic - we are defining syntax, right? - make use of lisp then (macros) -- join proper prefix +- join proper prefix (tree of tables/selects) - minimal core meta-syntax and user macro layer + +- write obscure sql rules once in one place (macro) +- ease sql portability + + + + +- qvar: query parameter + +- qchunk: code evaluated when the query is executed, pasted into the + query string directly + + - to suppress-qvar, e.g. when qvar cant be used as a sql function + parameter + + - for "unanticipated" delayed calculations, like portable full text + search diff --git a/backend.lisp b/backend.lisp @@ -0,0 +1,73 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(in-package :2sql-backend) + +(defparameter *database* nil) +(defparameter *query-to-name* nil) +(defparameter *name-seq* nil) + +#+postgresql +(defun call-with-postgresql-connection (database user password host port + use-ssl fn) + (let ((*database* (cl-postgres:open-database database user password host + port use-ssl)) + (*query-to-name* (make-hash-table :test #'equal)) + (*name-seq* 0) + (2sql:*backend* :postgresql) + (2sql:*delayed-query-cache* (make-hash-table :test #'eq))) + (unwind-protect (funcall fn) + (cl-postgres:close-database *database*)))) + +#+postgresql +(defmacro with-postgresql-connection ((database user password host + &optional (port 5432) + (use-ssl :no)) + &body body) + `(call-with-postgresql-connection + ,database ,user ,password ,host ,port ,use-ssl (lambda () ,@body))) + +#+sqlite +(defun call-with-sqlite-connection (database-path busy-timeout fn) + (let ((*database* (sqlite:connect database-path :busy-timeout busy-timeout)) + (*query-to-name* (make-hash-table :test #'equal)) + (*name-seq* 0) + (2sql:*backend* :sqlite) + (2sql:*delayed-query-cache* (make-hash-table :test #'eq))) + (unwind-protect (funcall fn) + (sqlite:disconnect *database*)))) + +#+sqlite +(defmacro with-sqlite-connection ((database-path &optional busy-timeout) &body body) + `(call-with-sqlite-connection ,database-path ,busy-timeout (lambda () ,@body))) + +(defun 2sql:execute (q qvars) + (assert *database*) + (ecase 2sql:*backend* + #+postgresql + (:postgresql + (let ((row-reader 'cl-postgres:list-row-reader)) + (if qvars + (cl-postgres:exec-prepared + *database* + (princ-to-string + (or (gethash q *query-to-name*) + (let ((x (setf (gethash q *query-to-name*) (incf *name-seq*)))) + (cl-postgres:prepare-query *database* (princ-to-string x) q) + x))) + qvars row-reader) + (cl-postgres:exec-query *database* q row-reader)))) + #+sqlite + (:sqlite + (if qvars + (apply 'sqlite:execute-to-list *database* q qvars) ;; TODO via prep stm http://common-lisp.net/project/cl-sqlite/ + (sqlite:execute-to-list *database* q))))) + +;; good for development, let *backend* in with-database, leave +;; *delayed-query-cache* nil, then queries dont get cached; then you +;; can recompile queries inside with-database, e.g. during an error +;; without closing a transaction for example + +;; (setq 2sql:*backend* :postgresql) +;; (setq 2sql:*backend* :sqlite) +;; (setq 2sql:*backend* :oracle) +;; (setq 2sql:*backend* nil) diff --git a/cl-2sql.asd b/cl-2sql.asd @@ -1,4 +1,8 @@ ;; -*- lisp; -*- +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +;;(push :postgresql *features*) +;;(push :sqlite *features*) (asdf:defsystem :cl-2sql :description "cl-2sql -- Lisp to SQL compiler for Common Lisp." @@ -6,8 +10,14 @@ :author "Tomas Hlavaty" :maintainer "Tomas Hlavaty" :licence "MIT" - :depends-on (:split-sequence) + :depends-on (:split-sequence + :ironclad + :babel + #+postgresql :cl-postgres + #+sqlite :sqlite) :serial t :components ((:file "packages") - (:file "core") - (:file "macros"))) + (:file "printer") + (:file "compiler") + (:file "macros") + #+(or postgresql sqlite) (:file "backend"))) diff --git a/compiler.lisp b/compiler.lisp @@ -0,0 +1,136 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +;;; Redefine EXECUTE. + +(in-package :2sql) + +(defparameter *delayed-query-cache* nil) + +(defun process-qchunk (x) + `(pure-form-to-string (macroexpand (funcall (lambda () ,@x))))) + +(defun execute (q qvars) + (values q qvars)) + +;; manual or automatic qvars? +#+nil +(defmacro query (form &rest qvars &environment env) ;; qvars for delayed compilation + (declare (ignorable env)) + (if *backend* + (multiple-value-bind (str qvars qchunks) (to-string (macroexpand form)) + (flet ((paste (x) + (when x + `(list ,@x)))) + `(execute + ,(if qchunks + `(format nil ,str ,@(mapcar 'process-qchunk qchunks)) + str) + (list ,@(mapcar #'car qvars))))) + ;; delay compilation until backend is known + (progn + #+sbcl ;; TODO conditional + (unless qvars + (setq qvars (remove-duplicates (lexical-variables env)))) + `(funcall + (lambda ,qvars + (assert *backend*) + ;; TODO eq cache compiled query? + (funcall (let ((fn '(lambda ,qvars (query ,form)))) + (print (list :@@@ fn)) + (or (when *delayed-query-cache* + (or (when (gethash fn *delayed-query-cache*) + (print (list :@@@-reusing (gethash fn *delayed-query-cache*))) + (gethash fn *delayed-query-cache*)) + (setf (gethash fn *delayed-query-cache*) + (compile nil fn)))) + (compile nil fn))) + ,@qvars) + #+nil(funcall (compile nil `(lambda ,',qvars (query ,',form))) ,@qvars)) + ,@qvars)))) + +#+nil +(defmacro query1 (form &rest qvars) + `(caar (query ,form ,@qvars))) + +#+nil +(let ((fn '(lambda ,qvars (query ,form)))) + (print (list :@@@ fn)) + (or (when *delayed-query-cache* + (or (when (gethash fn *delayed-query-cache*) + (print (list :@@@-reusing (gethash fn *delayed-query-cache*))) + (gethash fn *delayed-query-cache*)) + (setf (gethash fn *delayed-query-cache*) + (compile nil fn)))) + (compile nil fn))) + +(defun when-backend-known (form qvars action env) + (cond + (*backend* `(,action ,form)) + (t ;; delay compilation until backend is known + #+sbcl ;; TODO conditional + (unless qvars + (setq qvars (remove-duplicates (lexical-variables env)))) + `(funcall + (lambda ,qvars + (assert *backend*) + ;; TODO caching? + (funcall (compile nil '(lambda ,qvars + (declare (ignorable ,@qvars)) + (,action ,form))) + ,@qvars)) + ,@qvars)))) + +(defmacro execute-action (form) + (multiple-value-bind (str qvars qchunks) (to-string (macroexpand form)) + (flet ((paste (x) + (when x + `(list ,@x)))) + `(execute + ,(if qchunks + `(format nil ,str ,@(mapcar 'process-qchunk qchunks)) + str) + (list ,@(mapcar #'car qvars)))))) + +(defmacro query (form &rest qvars &environment env) + (when-backend-known form qvars 'execute-action env)) + +(defmacro qmacroexpand (form &rest qvars &environment env) + (when-backend-known form qvars 'macroexpand env)) + +;;(qmacroexpand (q:true-value)) + +(defun quoted-query (q &rest qvars) ;; TODO defmacro? + (funcall (compile nil `(lambda () (query ,q ,@qvars))))) ;; TODO cache + +(defmacro qlambda (args &body body) + `(lambda ,args + ,@(mapcar (lambda (q) `(query ,q ,@args)) body))) + +(defmacro qdefun (name args &body body) ;; rename to defqfun? + `(defun ,name ,args + ,@(mapcar (lambda (q) `(query ,q ,@args)) body))) + +(defmacro qlet (args &body body) ;; rename to query-let? + `(flet ,(mapcar (lambda (x) `(,(car x) ,(cadr x) (query ,@(cddr x)))) args) + ,@body)) + +;;; TODO lexical-variables for many lisps? what symbol-macrolet etc? + +#+sbcl ;; http://common-lisp.net/project/bese/repos/arnesi_dev/src/lexenv.lisp +(defmethod lexical-variables ((environment sb-kernel:lexenv)) + (loop + for var-spec in (sb-c::lexenv-vars environment) + when (and (atom (cdr var-spec)) + (not (and (typep (cdr var-spec) 'sb-c::lambda-var) + (sb-c::lambda-var-ignorep (cdr var-spec))))) + collect (car var-spec))) + +#+nil +(defmacro xxx (&environment env) + `(print ',(lexical-variables env))) + +;;(xxx) +;;(let (a) (let (a b) (xxx))) + +(defun qmap (fn q) ;; TODO optimize properly using cl-postgres + (mapcar (lambda (x) (apply fn x)) q)) diff --git a/core.lisp b/core.lisp @@ -1,86 +0,0 @@ -;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty - -;;; Redefine EXECUTE, PRINT-SYMBOL and PRINT-STRING if needed. - -(in-package :2sql) - -(defun print-string (string stream) - (write-char #\' stream) - (princ string stream) ;; TODO escape - (write-char #\' stream)) - -(defun split (char string) - (split-sequence:split-sequence char string)) - -(defun print-symbol (symbol stream) - (format stream "~(~{~a_~^.~}~)" (split #\. (symbol-name symbol)))) - -(defun print-form (form stream) - (let (qvars qchunks) - (labels ((rec (x) - (if (atom x) - (etypecase x - (string (print-string x stream)) - (keyword (princ x stream)) - (symbol (print-symbol x stream)) - (integer (princ x stream))) - (ecase (car x) - (:qvar - (push (cdr x) qvars) - (format stream ":~d" (length qvars))) - (:qchunk - (push (cdr x) qchunks) - (princ "~a" stream)) - (:par - (write-char #\( stream) - (rec (cons :lst (cons nil (cdr x)))) - (write-char #\) stream)) - (:lst - (let ((sep (cadr x)) - (i 0)) - (dolist (x (cddr x)) - (when (plusp i) - (princ (or sep " ") stream)) - (rec x) - (incf i)))))))) - (rec form)) - (values (nreverse qvars) (nreverse qchunks)))) - -(defun to-string (form) - (let (qvars qchunks) - (values - (with-output-to-string (s) - (multiple-value-setq (qvars qchunks) - (print-form (macroexpand form) s))) - qvars - qchunks))) - -(defun execute (q qvars) - (values q qvars)) - -(defun process-qchunk (x) - (let ((s (gensym))) - `(with-output-to-string (,s) - (multiple-value-bind (qvars qchunks) - (print-form (macroexpand (funcall (lambda () ,@x))) ,s) - (assert (not qvars)) - (assert (not qchunks)))))) - -(defmacro query (form) - (multiple-value-bind (str qvars qchunks) (to-string form) - (flet ((paste (x) - (when x - `(list ,@x)))) - `(execute - ,(if qchunks - `(format nil ,str ,@(mapcar 'process-qchunk qchunks)) - str) - ',qvars)))) - -(defmacro qlambda (args &body body) - `(lambda ,args - ,@(mapcar (lambda (q) `(query ,q)) body))) - -(defmacro define-function (name args &body body) - `(defun ,name ,args - ,@(mapcar (lambda (q) `(query ,q)) body))) diff --git a/macros.lisp b/macros.lisp @@ -12,104 +12,154 @@ (cl:lambda (x) (cl:if (cl:atom x) x (cl:macroexpand x))) body))) +(defsyntax clist (&body body) + `(lst ", " ,@body)) + (defsyntax prefix (name &body args) `(lst nil ,name ,@args)) (defsyntax infix (name &body args) `(lst ,name ,@args)) -(defsyntax clause (name &body body) - `(lst nil ,name ,@body)) - -(defsyntax not (exp) - `(prefix :not ,exp)) - -(defsyntax between (what left right) - `(lst nil ,what :between ,left :and ,right)) - -(defsyntax + (&body args) - `(infix " + " ,@args)) - -(defsyntax - (&body args) - `(infix " - " ,@args)) - -(defsyntax = (lexp rexp) - `(infix " = " ,lexp ,rexp)) - -(defsyntax and (&body args) - `(infix " AND " ,@args)) - -(defsyntax or (&body args) - `(infix " OR " ,@args)) - -(defsyntax like (text pattern) - `(infix " LIKE " ,text ,pattern)) - -(defsyntax ilike (text pattern) - `(infix " ILIKE " ,text ,pattern)) - -;; (defsyntax relike (text pattern) ;;regexp like -;; `(infix " ILIKE " ,text ,pattern)) - -(defsyntax where (exp) - `(clause :where ,exp)) - -(defsyntax from (&body exp) - `(clause :from ,@exp)) - -(defsyntax cols (cols) - `(lst ", " ,@cols)) +(defsyntax postfix (name &body args) + `(prefix ,@args ,name)) + +(cl:macrolet + ((defop (name ecase) + `(defsyntax ,name (&rest form) + (cl:block here + (cl:dolist (x form) + (cl:when (cl:let ((backend (cl:car x))) + (cl:or (cl:eq t backend) (cl:eq backend 2sql:*backend*))) + (cl:return-from here (cl:cadr x)))) ;; cadr or cdr? + (cl:when ,ecase + (cl:error "Backend ~s fell through BACKEND-CASE ~s" + 2sql:*backend* form)))))) + (defop backend-case nil) + (defop backend-ecase t)) + +(cl:macrolet ;; prefix unary operators + ((defop (name &rest rest) + `(defsyntax ,name (exp) + `(prefix ,,@rest ,exp)))) + ;;(defop not :not) + (defop @ :@) + (defop \|/ :\|/) + (defop ~ :~) + (defop ~* :~*) + (defop exists :exists) + (defop distinct :distinct)) + +(cl:macrolet ;; postfix unary operators + ((defop (name &rest rest) + `(defsyntax ,name (exp) + `(prefix ,exp ,,@rest)))) + (defop is-null :is :null) + (defop is-not-null :is :not :null)) + +(cl:macrolet ;; binary operators + ((defop (name sep) + `(defsyntax ,name (lexp rexp) + `(infix ,,sep ,lexp ,rexp)))) + (defop = " = ") + (defop < " < ") + (defop > " > ") + (defop <= " <= ") + (defop >= " >= ") + (defop <> " <> ") + (defop in " IN ") + (defop / " / ") + (defop % " % ") + (defop ^ " ^ ") + (defop ** " ** ") + (defop & " & ") + (defop \| " | ") + (defop \# " # ") + (defop << " << ") + (defop >> " >> ") + (defop like " LIKE ") + (defop ilike " ILIKE ")) + +(cl:macrolet ;; polyadic operators + ((defop (name sep) + `(defsyntax ,name (&body args) + `(infix ,,sep ,@args)))) + (defop + " + ") + (defop - " - ") + (defop * " * ") + (defop and " AND ") + (defop or " OR ") + (defop \|\| " || ")) + +(defsyntax like* (lexp rexp) + `(like (upper ,lexp) (upper ,rexp))) + +(defsyntax between (what lexp rexp) + `(prefix ,what :between ,lexp :and ,rexp)) (defsyntax select (cols &body body) - `(clause :select (cols ,cols) ,@body)) + `(prefix :select ,(cl:if (cl:atom cols) cols `(clist ,@cols)) ,@body)) + +(defsyntax from (&body body) `(prefix :from ,@body)) +(defsyntax where (exp) `(prefix :where ,exp)) +(defsyntax order-by (&body clist) `(prefix :order :by (clist ,@clist))) +(defsyntax group-by (&body clist) `(prefix :group :by (clist ,@clist))) +(defsyntax having (exp) `(prefix :having ,exp)) +(defsyntax limit (exp) `(prefix :limit ,exp)) +(defsyntax offset (exp) `(prefix :offset ,exp)) + +(defsyntax for (what &optional wait) + `(prefix :for ,@ (cl:ecase what + (:update '(:update)) + (:share '(:share))) + ,@(cl:unless wait '(:nowait)))) (defsyntax delete-from (tab &optional where) - `(clause :delete :from ,tab ,@(cl:when where (cl:list where)))) - -(defsyntax distinct (col) - `(lst nil :distinct ,col)) - -(defsyntax join (kind left right on) - `(lst nil ,left ,kind :join ,right :on ,on)) - -(defsyntax inner-join (left right on) - `(join :inner ,left ,right ,on)) - -(defsyntax natural-join (left right on) - `(join :natural ,left ,right ,on)) - -(defsyntax cross-join (left right on) - `(join :cross ,left ,right ,on)) - -(defsyntax left-join (left right on) - `(join :left ,left ,right ,on)) - -(defsyntax right-join (left right on) - `(join :right ,left ,right ,on)) - -(defsyntax full-join (left right on) - `(join (lst nil :full :outer) ,left ,right ,on)) - -(defsyntax union (&body select-forms) - `(lst " UNION " ,@select-forms)) - -(defsyntax union-all (&body select-forms) - `(lst " UNION ALL " ,@select-forms)) - -(defsyntax intersect (&body select-forms) - `(lst " INTERSECT " ,@select-forms)) - -(defsyntax intersect-all (&body select-forms) - `(lst " INTERSECT ALL " ,@select-forms)) - -(defsyntax except (&body select-forms) - `(lst " EXCEPT " ,@select-forms)) - -(defsyntax except-all (&body select-forms) - `(lst " EXCEPT " ,@select-forms)) + `(prefix :delete :from ,tab ,@(cl:when where (cl:list where)))) + +(cl:macrolet ;; join + ((defop (name &rest rest) + `(defsyntax ,name (left right on &optional using) + `(prefix ,left ,,@rest :join ,right + ,@(cl:when on `(:on ,on)) + ,@(cl:when using `(:using (par ,using))))))) + (defop inner-join :inner) + (defop natural-join :natural) + (defop cross-join :cross) + (defop left-join :left) + (defop right-join :right) + (defop full-join :full :outer)) + +(cl:macrolet ;; set operations + ((defop (name sep) + `(defsyntax ,name (order-by limit offset &body subqueries) + `(prefix (lst ,,sep ,@subqueries) + ,@(cl:when order-by `(,@order-by)) + ,@(cl:when limit `(,@limit)) + ,@(cl:when offset `(,@offset)))))) + (defop union " UNION ") + (defop union-all " UNION ALL ") + (defop intersect " INTERSECT ") + (defop intersect-all " INTERSECT ALL ") + (defop except " EXCEPT ") + (defop except-all " EXCEPT ALL ")) (defsyntax drop-if-exists (name kind if-exists &body body) - `(clause :drop ,kind ,@(cl:when if-exists '(:if :exists)) ,name ,@body)) + `(backend-ecase + (:oracle + ,(cl:if if-exists + `(prefix :declare :begin :execute :immediate + (\|\| "DROP " + , (cl:ecase kind + (:table " TABLE ") + (:view " VIEW ") + (:index " INDEX ") + (:sequence "SEQUENCE ")) + ,name #+nil(2sql:pure-form-to-string name)) + :\; :exception :when :others :then :null :\; :end :\;) + `(prefix :drop ,kind))) + (:postgresql + (prefix :drop ,kind ,@(cl:when if-exists '(:if :exists)) ,name ,@body)))) (defsyntax drop-table (name &optional if-exists cascade) `(drop-if-exists ,name :table ,if-exists ,@(cl:when cascade '(:cascade)))) @@ -121,66 +171,488 @@ `(drop-if-exists ,name :index ,if-exists)) (defsyntax drop-sequence (name &optional if-exists) - `(drop-if-exists ,name :view ,if-exists)) - -(defsyntax create-index (name unique tab &body cols) - `(clause :create ,@(cl:when unique '(:unique)) :index ,name - :on ,tab (par ,@cols))) - -(defsyntax insert-into (tab cols vals) - `(clause :insert :into ,tab (par ,@cols) :values (par ,@vals))) + `(drop-if-exists ,name :sequence ,if-exists)) + +;;(2sql:query (drop-sequence seq t)) + +(defsyntax create-index (name unique tab using cols properties triggers &body where) + #+nil ;; -> cols + (lambda (node db) + (typecase node + (sql-column (funcall 'format-sql-identifier node db)) + (t (funcall 'format-sql-syntax-node (%shorten-columns node) db)))) + ;; Oracle doesn't permit table_name.column_name in index expressions, + ;; and the table_name is redundant anyway, so let's strip it + ;; unconditionally: + #+nil + (defun %shorten-columns (node) + (etypecase node + (sql-literal) + (sql-fragment) ;; allow sexp2sql + (sql-unary-operator + (setf (expression-of node) + (%shorten-columns (expression-of node)))) + (sql-function-call + (setf (arguments-of node) + (mapcar #'%shorten-columns (arguments-of node)))) + (sql-index-operation + (setf (value-of node) (%shorten-columns (value-of node)))) + (sql-column-alias + (setf (table-of node) nil))) + node) + ;; where e.g. http://www.postgresql.org/docs/8.4/static/indexes-partial.html + ;; e.g. [USING method] for postgresql + ;; http://www.postgresql.org/docs/8.2/static/sql-createindex.html + ;; index properties for oracle + ;; http://download.oracle.com/docs/cd/B13789_01/server.101/b10759/statements_5010.htm#i2138869 + ;; triggers list of strings + `(prefix :create ,@(cl:when unique '(:unique)) :index ,name + :on ,tab ,@(cl:when using `(:using ,@using)) (par ,@cols) + ,@(cl:when where `(:where ,@where)) + ,@(cl:when properties properties))) + +;; http://developer.postgresql.org/pgdocs/postgres/indexes-opclass.html +(defsyntax operator-class (value operation) + `(prefix ,value ,operation)) + +(defsyntax values (&body values) + `(prefix :values (par (clist ,@values)))) + +(defsyntax insert-into (tab cols &body body) + ;; body (values...) | (select...) + `(prefix :insert :into ,tab (par (clist ,@cols)) ,@body)) + +(defsyntax returning (cols &optional vars) ;; oracle; better (k1 v1) (k2 v2)... + `(backend-ecase + (:oracle (prefix :returning (clist ,@cols) :into (clist ,@vars))) + (:postgresql (prefix :returning (clist ,@cols))))) (defsyntax create-sequence (name &optional temp) - `(clause :create ,@(cl:when temp '(:temporary)) :sequence ,name)) + `(prefix :create ,@(cl:when temp '(:temporary)) :sequence ,name)) (defsyntax curval (seq) `(function :curval ,seq)) (defsyntax nextval (seq) - `(function :nextval ,seq)) + `(backend-case + (:oracle ,(cl:intern (cl:format nil "~:@(~a.nextval~)" seq))) ;; TODO dont intern, polutes this package + (:postgresql (function :nextval ,seq)))) (defsyntax setval (seq val &optional current) `(function :curval ,seq ,val ,@(cl:unless current '(:false)))) -(defsyntax cond (&body cases) - `(lst nil :case - ,@(cl:loop - :for (c b) :in cases - :appending (cl:if (cl:eq cl:t c) - `(:else ,b) - `(:when ,c :then ,b))) - :end)) +;; Need to distinguish between boolean type, value and expression. +;; Oracle does not have a boolean type and value. Expressions +;; evaluate to true|false but these are not first class values so +;; manual conversion to the chosen boolean value of the chosen boolean +;; type is always necessary. + +(defsyntax boolean () + `(backend-ecase + (:oracle (function :char 1)) + (:postgresql :boolean) + (:sqlite :boolean))) + +(defsyntax true-value () + `(backend-ecase + (:oracle "Y") + (:postgresql :true) + (:sqlite 1))) + +(defsyntax false-value () + `(backend-ecase + (:oracle "N") + (:postgresql :false) + (:sqlite 0))) + +(defsyntax true-exp () + `(backend-case + (:oracle (= 1 1)) + (t (true-value)))) + +(defsyntax false-exp () + `(backend-case + (:oracle (= 1 2)) + (t (false-value)))) + +(defsyntax to-boolean (exp) + `(backend-case + (:oracle ,(cl:cond + ((cl:not exp) `(false-value)) + ((cl:eq t exp) `(true-value)) + ((cl:atom exp) (cl:error "not a boolean value ~s" exp)) + (t `(prefix :case :when ,exp (true-value) :else (false-value))))) + (t (= (true-value) ,exp)))) + +;;(cl:macroexpand '(to-boolean (= 1 2))) -(defsyntax zerop (number) - `(= 0 ,number)) +(defsyntax cond (&body cases) + `(backend-case + (:oracle + (= (true-value) + (par + (prefix :case + ,@(cl:loop + :for (c b) :in cases + :appending (cl:if (cl:eq cl:t c) `(:else ,b) `(:when ,c :then ,b))) + :end)))) + (t (prefix :case + ,@(cl:loop + :for (c b) :in cases + :appending (cl:if (cl:eq cl:t c) `(:else ,b) `(:when ,c :then ,b))) + :end)))) + +(defsyntax if (test then else) + `(cond (,test ,then) (t ,else))) + +;;(cl:macroexpand '(if 1 (false-value) (true-value))) ;; TODO forbid cl:nil and cl:t? +;;(cl:macroexpand '(if 1 2 3)) +;;(cl:macroexpand '(cond (1 1) (2 2) (t 3))) + +#+nil ;; when and unless dont make sense for sql because of return value type clash +(defsyntax when (test &body body) + `(if ,test ,body (false-value))) + +#+nil +(defsyntax unless (test &body body) + `(if ,test (false-value) ,body)) + +(defsyntax zerop (exp) `(= 0 ,exp)) +(defsyntax plusp (exp) `(< 0 ,exp)) +(defsyntax minusp (exp) `(< ,exp 0)) + +;;(plusp (+ 1 2 3)) (defsyntax par (&body body) - `(:par ,@(cl:mapcar - (cl:lambda (x) (cl:if (cl:atom x) x (cl:macroexpand x))) body))) - -(defsyntax function (name &body args) - `(lst nil ,name (par (lst ", " ,@args)))) + `(prefix :|(| ,@body :|)|)) -(defsyntax count (x) - `(function :count ,x)) +(defsyntax function (name &body clist) + `(prefix ,name (par (clist ,@clist)))) -(defsyntax asc (exp) - `(prefix :asc ,exp)) +(defsyntax count (x) `(function :count ,x)) +;;(defsyntax distinct (x) `(function :distinct ,x)) +(defsyntax upper (x) `(function :upper ,x)) -(defsyntax desc (exp) - `(prefix :desc ,exp)) +(cl:macrolet + ((defop (name &rest rest) + `(defsyntax ,name (exp) + `(function ,,@rest ,exp)))) + (defop min :min) + (defop max :max) + (defop avg :avg) + (defop sum :sum)) -(defsyntax as (tab alias) - `(lst nil ,tab ,alias)) +(defsyntax asc (exp) `(prefix :asc ,exp)) +(defsyntax desc (exp) `(prefix :desc ,exp)) -;; order-by group-by having -;; min max avg sum +(defsyntax as (tab alias) `(prefix ,tab ,alias)) (defsyntax qvar (name &optional type) - `(:qvar ,name ,@(cl:when type (cl:list type)))) - -;; (defsyntax type (&body body) -;; `(:type ,@body)) + `(:qvar ,name ,type)) (defsyntax qchunk (qchunk) `(:qchunk ,qchunk)) + +(defsyntax alter-table (tab &body clist) + `(prefix :alter :table ,tab (clist ,@clist))) + +(defsyntax nullable (nullable) + (cl:if nullable '(prefix :null) '(prefix :not :null))) + +;;; We need to distinguish between constraints which can be set up +;;; during CREATE TABLE and those which require a second ALTER TABLE +;;; step: +;;; +;;; While foreign key constraints are nominally part of the column +;;; definition, it is not possible to define both together in the case +;;; where the target table has not been defined yet, meaning that +;;; ordering matterns and circular references need to be dealt with. +;;; +;;; An similar issue would arise for table contents: We need to be able +;;; to separate out table and constraint definition for data import files, +;;; where the order must be: +;;; 1. create table +;;; 2. insert into / copy +;;; 3. alter table add foreign key +(defsyntax add-column (name type nullable default &body constraints) + `(prefix :add ,name ,type + (nullable ,nullable) + ,@(cl:when default `(:default ,default)) + #+nil + (mapc (lambda (constraint) + (unless (delay-constraint-until-alter-table-p constraint) + (format-sql-syntax-node constraint))) + constraints))) + +(defsyntax drop-column (name cascade) + `(prefix :drop :column ,name + ,@(cl:when cascade `(:cascade + ,@(backend-case (:oracle '(:constraints))))))) + +(defsyntax alter-column-type (name type nullable) ;; TODO nullable + `(backend-ecase + (:oracle (prefix :modify ,name ,type)) + (:postgresql (prefix :alter :column ,name :type ,type)))) + +(defsyntax add-unique-constraint (name &body columns) + `(prefix :add :constraint ,name :unique (par (clist ,@columns)))) + +(defsyntax add-primary-key-constraint (name &body columns) + `(prefix :add :constraint ,name :primary :key (par (clist ,@columns)))) + +(cl:defmacro foreign-key-action (action) + (cl:ecase action + (:no-action '(:no :action)) + (:restrict '(:restrict)) + (:set-null '(:set :null)) + (:set-default '(:set :default)) + (:cascade '(:cascade)))) + +(defsyntax on-delete (action) + `(prefix :on :delete (foreign-key-action ,action))) + +(defsyntax on-update (action) + `(prefix :on :update (foreign-key-action ,action))) + +(defsyntax add-foreign-key-constraint (name source-columns target-columns + target-table + on-delete on-update) + `(prefix :add :constraint ,name + :foreign :key (par (clist ,@source-columns)) + :references ,target-table (par (clist ,@target-columns)) + ,on-delete ,on-update + :deferrable :initially :immediate)) + +(defsyntax drop-constraint (name tab) + `(alter-table ,tab :drop :constraint ,name)) + +(defsyntax update (tab alist &optional where) + `(prefix :update ,tab :set + (clist ,@(cl:loop :for (k v) :in alist :collect `(lst " = " ,k ,v))) + ,@where)) + +(defsyntax column (name type &optional nullable default) + `(prefix ,name ,type + (nullable ,nullable) + ,@(cl:when default `(:default ,default)))) + +(defsyntax columns (&body cols) + `(clist ,@cols)) + +(defsyntax create-table (name cols &optional temp as using) + `(prefix :create + ,@(cl:when temp '(:global :temporary)) + ,@(cl:when using '(:virtual)) + :table ,name + ,@(cl:when using `(:using ,using)) + (par ,cols) + ,@ (cl:when (cl:and temp (cl:not (cl:eq t temp)) (cl:not as)) + `((:on :commit ,@ (cl:ecase temp + (:drop '(:drop)) + (:preserve-rows '(:preserve :rows)) + (:delete-rows '(:delete-rows)))))) + ,@(cl:when as `(,@as)))) + +(defsyntax create-view (replace name temp cols &optional as) + `(prefix :create ,@(cl:when replace '(:or :replace)) + ,@(cl:when temp '(:temporary)) :view ,name + (par ,@cols) ,@(cl:when as `(,@as)))) + +(defsyntax lock-table (tab mode wait) + `(prefix :lock :table ,tab + :in ,@ (cl:ecase mode + (:row-share '(:row :share)) + (:row-exclusive '(:row :exclusive)) + (:share-update '(:share :update)) + (:share '(:share)) + (:share-row-exclusive '(:share :row :exclusive)) + (:exclusive '(:exclusive))) + :mode + ,@(cl:unless wait '(:nowait)))) + +(defsyntax regexp-like (string pattern case-sensitive) + `(backend-ecase + (:oracle (function :regexp_like ,string ,pattern ,(cl:if case-sensitive "c" "i"))) + (:postgresql ??))) + +(defsyntax not (exp) + `(backend-ecase + (:oracle (function :not ,(cl:if (cl:atom exp) `(= (true-value) ,exp) exp))) + (:postgresql (prefix :not ,exp)))) + +(defsyntax abs (exp) + `(backend-ecase + (:oracle (function :abs ,exp)) + (:postgresql (@ ,exp)))) + +(defsyntax sqrt (exp) + `(backend-ecase + (:oracle (function :sqrt ,exp)) + (:postgresql (\|/ ,exp)))) + +(defsyntax bitand(lexp rexp) + `(backend-ecase + (:oracle (function :bitand ,lexp ,rexp)) + (:postgresql (& ,lexp ,rexp)))) + +(defsyntax suppress-qvar (exp) ;; make qchunk from qvar + `(:suppress-qvar ,(cl:macroexpand exp))) + +(defsyntax postgresql/to-tsvector (what &optional regconfig) + `(function :to_tsvector + ,@(cl:when regconfig `((suppress-qvar ,regconfig))) + ,what)) + +(defsyntax postgresql/to-tsquery (query &optional regconfig) + `(function :to_tsquery + ,@(cl:when regconfig `((suppress-qvar ,regconfig))) + ,query)) + +(defsyntax postgresql/@@ (tsvector tsquery) + `(infix " @@ " ,tsvector ,tsquery)) + +(defsyntax oracle/contains (what query &optional number) + `(function :contains ,what (suppress-qvar ,query) + ,@(cl:when number `((suppress-qvar ,number))))) + +#+nil +(define-query-macro full-text-search (class what query &optional regconfig) + `(backend-ecase + (:postgresql (full-text-search-query-outer-function + (postgresql/@@ + ,(or (let ((x (related-tsvector-accessor class (car what)))) + (when x + (cons x (cdr what)))) + `(postgresql/to-tsvector ,what ,regconfig)) + (postgresql/to-tsquery + (full-text-search-query-inner-function ,query) + ,regconfig)) + ,what + ,query)) + (:oracle (plusp (oracle/contains ,what ,query))))) + +(defsyntax empty-clob () + `(function :empty_clob)) + +(defsyntax empty-blob () + `(function :empty_blob)) + +(defsyntax boolean-type () + `(backend-ecase + (:oracle (function :char 1)) + (:postgresql :bool) + (:sqlite :boolean))) + +(defsyntax numeric-type () + `(backend-ecase + ;; NUMBER => oracle assumes NUMBER(*.0) :-{ + (:oracle ,(cl:error "use more specific type with oracle backend")) + (t (:numeric)))) + +(defsyntax tsvector-type () + '(:tsvector)) + +(defsyntax clob-type () + `(backend-ecase + (:oracle :clob) + (:postgresql (prefix :character :large :object)))) + +(defsyntax blob-type () + `(backend-ecase + (:oracle :blob) + (:postgresql (prefix :binary :large :object)))) + +(defsyntax date-type () + '(:date)) + +(defsyntax time-type () + '(:time)) + +(defsyntax timestamp-type () + '(:timestamp)) + +(defsyntax timestamp-with-timezone-type () + '(:timestamp :with :time :zone)) + +(defsyntax interval-type () + `(backend-ecase + (:oracle ,(cl:error "sql-interval-type not yet supported")) + (:postgresql '(:interval)))) + +(defsyntax char-sized-type (type &optional size) + `(backend-ecase + (:oracle (prefix ,type ,@(cl:when size `(par (prefix ,size :char))))) + (:postgresql (prefix ,type ,@(cl:when size `(#+nil :size (par ,size))))))) + +(defsyntax char-type (&optional size) + `(backend-ecase + (:oracle ,(cl:if (cl:eql 1 size) + (cl:error "CHAR(1) is reserved for booleans") + `(char-sized-type :char ,size))) + (:postgresql (char-sized-type :char ,size)))) + +(defsyntax varchar-type (&optional size) + `(backend-ecase + (:oracle (char-sized-type :varchar2 ,size)) + (t ,(cl:if size `(char-sized-type :varchar ,size) :text)))) + +(defsyntax varchar-without-size-if-possible () + `(varchar-type (backend-case + ;; max 4000 bytes? + (:oracle (varchar-type 256))))) + +(defsyntax float-type (bit-size) + (cl:progn + (cl:assert (cl:and bit-size (cl:<= 32 bit-size 64))) + (cl:cond + ((cl:<= bit-size 32) + `(backend-ecase + (:oracle :binary_float) + (:postgresql :real))) + ((cl:<= bit-size 64) + `(backend-ecase + (:oracle :binary_double) + (:postgresql '(prefix :double :precision))))))) + +(defsyntax integer-type (&optional bit-size) + (cl:cond + ((cl:null bit-size) + `(backend-ecase + (:oracle '(function :number :* 0)) + (:postgresql :numeric) + (:sqlite :numeric))) + ((cl:<= bit-size 16) + `(backend-ecase + (:oracle '(function :number 5 0)) + (:postgresql :smallint) + (:sqlite :numeric))) + ((cl:<= bit-size 32) + `(backend-ecase + (:oracle '(function :number 10 0)) + (:postgresql :int) + (:sqlite :numeric))) + ((cl:<= bit-size 64) + `(backend-ecase + (:oracle '(function :number 19 0)) + (:postgresql :bigint) + (:sqlite :numeric))) + (cl:t + `(backend-ecase + (:oracle '(function :number :* 0)) + (:postgresql :numeric) + (:sqlite :numeric))))) + +(defsyntax bit-sized-type (type &optional bit-size) + (cl:cond + ((cl:null bit-size) type) + ;; TODO why not ,bit-size + ((cl:<= bit-size 16) `(prefix ,type :bit :bit-size 16)) + ((cl:<= bit-size 32) `(prefix ,type :bit :bit-size 32)) + ((cl:<= bit-size 64) `(prefix ,type :bit :bit-size 64)) + (cl:t type))) + +(defsyntax set (&body values) + `(par (clist ,@values))) + +(defsyntax power (lexp rexp) + `(function :power ,lexp ,rexp)) diff --git a/orm.lisp b/orm.lisp @@ -0,0 +1,141 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(in-package :2sql-orm) + +(defun lisp-type-to-ptype (type) ;; TODO more types + (if (atom type) + (ecase type + (integer '(q:integer-type))) + (ecase (car type) + (or + (assert (eq 'null (cadr type))) + (assert (not (cddddr type))) + (lisp-type-to-ptype (caddr type)))))) + +(defgeneric list-pslots (class-name)) + +(defmacro defpclass (name direct-superclasses direct-slots &rest options) + `(progn + (defclass ,name ,direct-superclasses + ,(cons '(oid :type integer :initarg :oid :accessor oid) + direct-slots) + ,@options) + (defmethod list-pslots ((class-name (eql ',name))) + ',(loop + for x in direct-slots + for type = (cadr (member :type x)) + collect (list (car x) + (lisp-type-to-ptype type) + (subtypep (type-of nil) type) + (cadr (member :initarg x)) + (cadr (member :initform x))))))) + +(defpclass t1 () + ((c1 :type integer :initarg :c1 :accessor c1) + (c2 :type (or null integer) :initarg :c2 :initform nil :accessor c2) + (c3 :type integer :initarg :c3 :initform 321 :accessor c3))) + +(defconstant +class-id-bit-size+ 16) + +(defmacro oid-exp (class-id) + (let ((bit-size +class-id-bit-size+)) + `(q:backend-ecase + (:oracle (q:+ (q:* (q:nextval oid-seq) (q:power 2 ,bit-size)) ,class-id)) + (:postgresql (q:\| (q:<< (q:nextval "oid_seq") ,bit-size) ,class-id))))) + +(defun class-name-to-class-id (class-name) + (mod (ironclad:octets-to-integer + (ironclad:digest-sequence + :crc32 (babel:string-to-octets (symbol-name class-name) :encoding :utf-8))) + (expt 2 +class-id-bit-size+))) + +(defun setup-pclass (class-name) + (2sql:quoted-query + `(q:create-table t1 + (q:columns + (q:column oid (q:integer-type) nil + (oid-exp (q:qchunk (class-name-to-class-id ',class-name)))) + ,@(loop + for (name type nullable initarg default) in (list-pslots class-name) + collect `(q:column ,name ,type ,nullable ,default)))))) + +(defun make-pinstance (class-name &rest args) + (multiple-value-bind (known unknown) + (loop + for (name type nullable initarg default) in (list-pslots class-name) + for c = (member initarg args) + if c collect (list name initarg (cadr c)) into known + else collect (cons name initarg) into unknown + finally (return (values known unknown))) + (apply #'make-instance class-name + (nconc + (loop + for (name initarg value) in known + appending (list initarg value)) + (loop + for (name . initarg) in (cons (cons 'oid :oid) unknown) + for value in (car (2sql:quoted-query + `(q:insert-into t1 ,(mapcar #'car known) + (q:values ,@(mapcar #'caddr known)) + (q:returning ,(cons 'oid (mapcar #'car unknown)))))) + appending (list initarg value)))))) + +(defmacro with-tables (names &body body) + `(progn + (mapcar 'setup-pclass ',names) + (unwind-protect (progn ,@body) + ,@(mapcar (lambda (x) `(2sql:query (q:drop-table ,x t t))) names)))) + +(trace 2sql:execute) + +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (2sql:query (q:drop-sequence oid-seq t)) + (2sql:query (q:create-sequence oid-seq)) + (with-tables (t1) + (make-pinstance 't1 :c1 1 :c2 2 :c3 3) + (make-pinstance 't1 :c1 1 :c2 2) + #+nil(query (q:select :* (q:from t1))) + #+nil(x-query (q:select ((x-instance t1)) (q:from t1))))) + + + +(defparameter *x-alias-to-table* nil) + +(defmacro x-query (form &rest qvars) + ;; remember stuff, run instance reconstructor at the end + `(funcall (compile nil `(lambda () + (let (((*x-alias-to-table* (make-hash-table)))) + (query ,,form ,,@qvars))))) + #+nil + `(let* ((*x-alias-to-table* (make-hash-table)) + #+nil(x (macroexpand `(query ,,form ,,@qvars)))) + ;;x + )) + +#+nil +(defmacro x-select (cols &body body) + ;; slice in x-instance slots + `(q:select (clist ,@cols) ,@body)) + +(defmacro x-instance (x) + ;; push slot collector and instance reconstructor + `(clist + ,@(loop + for class-name = (gethash x *x-alias-to-table* x) + for (name type nullable initarg default) in (list-pslots class-name) + collect name))) + +(defmacro x-as (tab alias) + ;; remember alias -> tab + (assert (not (gethash alias *x-alias-to-table*))) + (setf (gethash alias *x-alias-to-table*) tab) + (list tab alias)) + +(x-query + (x-select ((x-instance x) (x-instance y) (q:sum y.total)) + (q:from (x-as t1 x) (x-as t2 y)) + (q:where 1))) + +select => populate instances +update => clear affected instances from cache +delete => clear affected instances from cache diff --git a/packages.lisp b/packages.lisp @@ -4,19 +4,39 @@ (defpackage 2sql (:use :cl) - (:export #:print-string - #:print-symbol - #:print-form - #:to-string - #:execute - #:query - #:qlambda - #:define-function)) + (:export + ;; printer + #:print-string + #:print-symbol + #:print-qvar + #:print-form + #:pure-form-to-string + #:to-string + ;; compiler + #:*backend* + #:*delayed-query-cache* + #:execute + #:query + #:qlambda + #:qdefun + #:qlet + #:qmacroexpand + #:qmap + #:quoted-query + )) (defpackage 2sql-macros (:use) (:nicknames :q) ;; TODO really? - (:import-from :cl #:&optional #:&body #:nil)) + (:import-from :cl #:&optional #:&rest #:&body #:nil #:t)) + +(defpackage 2sql-backend + (:use :cl) + (:export #+postgresql #:with-postgresql-connection + #+sqlite #:with-sqlite-connection)) + +(defpackage 2sql-orm + (:use :cl)) (defpackage 2sql-tests (:use :cl :2sql)) diff --git a/printer.lisp b/printer.lisp @@ -0,0 +1,85 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +;;; 1) Redefine PRINT-SYMBOL, PRINT-QVAR and PRINT-STRING if needed. +;;; +;;; 2) SETQ *backend* before using the compiler or LET it before +;;; executing queries (e.g. inside your WITH-DATABASE macro). + +(in-package :2sql) + +(defparameter *backend* nil) +(defparameter *suppress-qvar* nil) + +(defun print-string (string stream) + (write-char #\' stream) + (princ string stream) ;; TODO escape + (write-char #\' stream)) + +(defun split (char string) + (split-sequence:split-sequence char string)) + +(defun print-symbol (symbol stream) + (format stream "~(~{~a~^.~}~)" + (split #\. (substitute #\_ #\- (symbol-name symbol))))) + +(defun pure-form-to-string (form) + (let ((x (macroexpand form))) + (when x + (with-output-to-string (s) + (multiple-value-bind (qvars qchunks) (print-form x s) + (assert (not qvars)) + (assert (not qchunks))))))) + +(defun print-qvar (n type stream) + (ecase *backend* + (:postgresql + (let ((x (pure-form-to-string (car type)))) + (if x + (format stream "$~d::~a" n x) + (format stream "$~d" n)))) + (:sqlite (write-char #\? stream)) + (:oracle (format stream ":~d" n)))) + +(defun print-form (form stream) + (let (qvars qchunks) + (labels ((rec (x) + (if (atom x) + (etypecase x + (string (print-string x stream)) + (keyword (princ x stream)) + (symbol (print-symbol x stream)) + (integer (princ x stream))) + (ecase (car x) + (:suppress-qvar + (let ((*suppress-qvar* t)) + (rec (cadr x)))) + (:qvar + (cond + (*suppress-qvar* + (push (list (cadr x)) qchunks) ;; TODO pass type too? + (princ "~a" stream)) + (t + (push (cdr x) qvars) + (print-qvar (length qvars) (cddr x) stream)))) + (:qchunk + (push (cdr x) qchunks) + (princ "~a" stream)) + (:lst + (let ((sep (cadr x)) + (i 0)) + (dolist (x (cddr x)) + (when (plusp i) + (princ (or sep " ") stream)) + (rec x) + (incf i)))))))) + (rec form)) + (values (nreverse qvars) (nreverse qchunks)))) + +(defun to-string (form) + (let (qvars qchunks) + (values + (with-output-to-string (s) + (multiple-value-setq (qvars qchunks) + (print-form form #+nil(macroexpand form) s))) + qvars + qchunks))) diff --git a/test.lisp b/test.lisp @@ -2,6 +2,10 @@ (in-package :2sql-tests) + + + + ;; s-sql (to-string '(q:select ((q:+ field-1 100 @var) field-5) @@ -170,3 +174,214 @@ (q:= (q:qvar v2) (q:qchunk (+ 1 2))) (q:= (q:qvar v3) (q:qchunk "one")) (q:= (q:qvar v4) (q:qchunk '(q:desc x))))))))) + + + + + + + +;; *delayed-query-cache* test +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 2)) + ;;(EXECUTE "SELECT |/ $1::INTEGER" (LIST A)) + ;;(funcall (compile nil (lambda (a) (query (q:select ((q:sqrt (q:qvar a :integer))))))) a) + ;;(query (q:select ((q:sqrt (q:qvar a :integer))))) + (let ((q (qlambda (a) + (q:select ((q:sqrt (q:qvar a :integer))))))) + (funcall q a) + (funcall q a)))) + +;; automatic lexvars +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 2)) + ;;(EXECUTE "SELECT |/ $1::INTEGER" (LIST A)) + ;;(funcall (compile nil (lambda (a) (query (q:select ((q:sqrt (q:qvar a :integer))))))) a) + (query (q:select ((q:sqrt (q:qvar a :integer))))))) + +;; explicit lexvars +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 2)) + ;;(EXECUTE "SELECT |/ $1::INTEGER" (LIST A)) + ;;(funcall (compile nil (lambda (a) (query (q:select ((q:sqrt (q:qvar a :integer))))))) a) + (query (q:select ((q:sqrt (q:qvar a :integer)))) a))) + + + + +;; suppress-qvar + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 1) + (b 2)) + (query (q:select ((q:+ (q:qvar a :integer) + (q:suppress-qvar (q:qvar b :integer))))))) + #+nil(query (q:select (3))) + #+nil(query (q:select (4)))) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((q (qlambda (a b) + (q:select ((q:+ (q:qvar a :integer) + (q:suppress-qvar (q:qvar b :integer)))))))) + (list :first (funcall q 1 2) :second (funcall q 2 3)))) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (qlet ((q (a b) ;; TODO indenting like flet? + (q:select ((q:+ (q:qvar a :integer) + (q:suppress-qvar (q:qvar b :integer)) + (q:qchunk (* a b))))))) + (list :first (q 1 2) :second (q 2 3)))) + + + + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (flet ((mul (x y) ;; TODO undefined function! capture flet? + (* x y))) + (qlet ((q (a b) ;; TODO indenting like flet? + (q:select ((q:+ (q:qvar a :integer) + (q:suppress-qvar (q:qvar b :integer)) + (q:qchunk (mul a b))))))) + (list :first (q 1 2) :second (q 2 3))))) + +cl-postgres:to-sql-string function value->type? would be handy + +smallint integer +integer integer +bigint integer +numeric ratio +real float +double precision double-float +boolean boolean +varchar string +text string +bytea (vector (unsigned-byte 8)) + + +;; custom query macro example + +(defmacro integer-qvar (name) + `(q:qvar ,name :integer)) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 2)) + (query (q:select ((q:sqrt (integer-qvar a)) + (q:+ 1 (q:qchunk (+ 2 3)))))))) + + + + + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query (q:select (22 "Folie et déraison" #+nil 4.5)))) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (let ((a 1) + (b 2) + (c "X") + (d "hello")) + (query (q:select ((q:+ (q:qvar a :numeric) + (q:qvar b :integer)) + (q:\|\| (q:qvar c (q:char-type)) + (q:qvar d (q:varchar-type 10)))))))) + + + + +(doquery (:select 'x 'y :from 'some-imaginary-table) (x y) + (format t "On this row, x = ~A and y = ~A.~%" x y)) + + + + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query (q:create-table t1 (q:columns + (q:column c1 (q:integer-type)) + (q:column c2 (q:integer-type) 314)) + t)) + (qlet ((insert (a b) (q:insert-into t1 (c1 c2) + (q:values (q:qvar a) (q:qvar b))))) + (insert 1 2) + (insert 3 4)) + (qmap (lambda (a b) (print (list :@@@ a b))) + (query (q:select (c1 c2) (q:from t1)))) + (multiple-value-prog1 (query (q:select (c1 c2) (q:from t1))) + (query (q:drop-table t1)))) + + + + + + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query (q:create-table t1 nil ((q:column c1 (q:integer-type)) + (q:column c2 (q:integer-type) 314))))) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query (q:drop-table t1))) + + + + + + + + + + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query + (q:select (relname) + (q:from (q:inner-join pg_catalog.pg_class + pg_catalog.pg_namespace + (q:= relnamespace pg_namespace.oid))) + (q:where (q:and (q:= relkind "r") + (q:not (q:in nspname (q:set "pg_catalog" "pg_toast"))) + (q:function :pg_catalog.pg_table_is_visible pg_class.oid)))))) + +(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (query + (q:select (relname) + (q:from (q:inner-join (q:as pg_catalog.pg_class c) + (q:as pg_catalog.pg_namespace n) + (q:= relnamespace n.oid))) + (q:where (q:and (q:= relkind "r") + (q:not (q:in nspname (q:set "pg_catalog" "pg_toast"))) + (q:function :pg_catalog.pg_table_is_visible c.oid)))))) + +#+nil +(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)))" + +(with-sqlite-connection (":memory:") + (query (q:create-table t1 (q:columns + (q:column c1 (q:integer-type)) + (q:column c2 (q:integer-type) 314) + (q:column c3 (q:boolean-type)) + (q:column c4 (q:varchar-type))) + nil nil :fts3)) + (let ((tt (2sql:qmacroexpand (q:true-value))) + (ff (2sql:qmacroexpand (q:false-value)))) + (loop + for (a b c d) in `((11 12 ,tt "Ivan Ivanovic Ivanov") + (21 22 ,ff "Ivan Ovic")) + do (query (q:insert-into t1 (c1 c2 c3 c4) + (q:values (q:qvar a) (q:qvar b) (q:qvar c) (q:qvar d)))))) + ;; my sqlite version supports word and prefix search only + (query (q:select :* + (q:from t1) + ;;(q:where (q:infix " MATCH " c4 "ivan")) + (q:where (q:infix " MATCH " c4 "ov*")) + ;;(q:where (q:infix " MATCH " c4 "*ov")) ;; doesnt work + ;;(q:where (q:infix " MATCH " c4 "ivan AND ivanov")) + )))