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:
M | README | | | 21 | ++++++++++++++++++++- |
A | backend.lisp | | | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | cl-2sql.asd | | | 16 | +++++++++++++--- |
A | compiler.lisp | | | 136 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | core.lisp | | | 86 | ------------------------------------------------------------------------------- |
M | macros.lisp | | | 722 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- |
A | orm.lisp | | | 141 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | packages.lisp | | | 38 | +++++++++++++++++++++++++++++--------- |
A | printer.lisp | | | 85 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | test.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"))
+ )))