cl-2sql

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

commit e43721e0b368ad17191e2372f04dabd770cf7cc8
parent 713e9113770f7c35304e030c1111b93d3e4c8cb2
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun, 16 Feb 2014 21:54:32 +0100

reorganize, obsolete clos, change backend stuff

Diffstat:
A2sql-cl-postgres.lisp | 28++++++++++++++++++++++++++++
A2sql-cl-sqlite.lisp | 21+++++++++++++++++++++
A2sql-dbquery.lisp | 18++++++++++++++++++
A2sql.lisp | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MREADME | 6++++++
Dbackend.lisp | 67-------------------------------------------------------------------
Acl-2sql-cl-postgres.asd | 12++++++++++++
Acl-2sql-cl-sqlite.asd | 12++++++++++++
Acl-2sql-dbquery.asd | 12++++++++++++
Mcl-2sql.asd | 21+++------------------
Dcompiler.lisp | 66------------------------------------------------------------------
Mmacros.lisp | 59+++++++++++++++++++++++++++++++++++++++--------------------
Morm.lisp | 65++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Aormc.lisp | 840+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages.lisp | 56--------------------------------------------------------
Dprinter.lisp | 85-------------------------------------------------------------------------------
Mtest.lisp | 283++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
17 files changed, 1452 insertions(+), 435 deletions(-)

diff --git a/2sql-cl-postgres.lisp b/2sql-cl-postgres.lisp @@ -0,0 +1,28 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(defpackage :2sql-cl-postgres + (:use :cl) + (:export :make-backend)) + +(in-package :2sql-cl-postgres) + +(defun make-backend (db user password host port use-ssl) + (let ((x (cl-postgres:open-database db user password host port use-ssl))) + (lambda (msg &rest args) + (assert x) + (ecase msg + (:finish + (destructuring-bind () args + (cl-postgres:close-database x) + (setq x nil))) + (:query + (destructuring-bind (q) args ;; TODO query args + (cl-postgres:exec-query x q 'cl-postgres:list-row-reader))) + (:prepare + (destructuring-bind (stm q &rest args2) args + (declare (ignore args2)) ;; TODO args2 + (cl-postgres:prepare-query x stm q))) + (:execute + (destructuring-bind (stm &rest args2) args + ;; TODO return ctype . cname too + (cl-postgres:exec-prepared x stm args2 'cl-postgres:list-row-reader))))))) diff --git a/2sql-cl-sqlite.lisp b/2sql-cl-sqlite.lisp @@ -0,0 +1,21 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(defpackage :2sql-cl-sqlite + (:use :cl) + (:export :make-backend)) + +(in-package :2sql-cl-sqlite) + +(defun make-backend (database-path busy-timeout) + (let ((x (sqlite:connect database-path :busy-timeout busy-timeout))) + (lambda (msg &rest args) + (assert x) + (ecase msg + (:finish + (destructuring-bind () args + (sqlite:disconnect x) + (setq x nil))) + (:query (apply 'sqlite:execute-to-list x args)) + ;; TODO via prep stm http://common-lisp.net/project/cl-sqlite/ + #+nil(:prepare) + #+nil(:execute))))) diff --git a/2sql-dbquery.lisp b/2sql-dbquery.lisp @@ -0,0 +1,18 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(defpackage :2sql-dbquery + (:use :cl) + (:export :make-backend)) + +(in-package :2sql-dbquery) + +(defun make-backend (dbquery-server) + (lambda (msg &rest args) + (assert dbquery-server) + (ecase msg + (:finish + (apply 'dbquery:finish dbquery-server args) + (setq dbquery-server nil)) + (:query (apply 'dbquery:query dbquery-server args)) + (:prepare (apply 'dbquery:prepare dbquery-server args)) + (:execute (apply 'dbquery:execute dbquery-server args))))) diff --git a/2sql.lisp b/2sql.lisp @@ -0,0 +1,236 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(defpackage 2sql + (:use :cl) + (:export :*backend* + :*server* + ;; printer + ;;:print-string + :print-symbol + ;;:print-qvar + ;;:print-form + ;;:pure-form-to-string + ;;:to-string + ;; run-time + :with-server + ;; compiler + ;;:*compiled-query-lambda-cache* + :with-backend + ;;:execute + :query + :qlambda + :qdefun + :apply-query + :qlet + ;;:qmacroexpand + ;;:qmap + ;;:quoted-query + )) + +(in-package :2sql) + +(defparameter *backend* nil) +(defparameter *server* nil) + +;;; printer + +;;; 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). + +(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 &optional stream) + (flet ((out (s) + (when symbol + (format s "~(~{~a_~^.~}~)" + (split #\. (substitute #\_ #\- (symbol-name symbol))))))) + (if stream + (out stream) + (with-output-to-string (s) + (out s))))) + +(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 :mysql) (write-char #\? stream)) + (:oracle (format stream ":~d" n)))) + +;;(defparameter *suppress-qvar* nil) + +(defun print-form (form stream) + (let (qvars qchunks *suppress-qvar*) + (declare (special *suppress-qvar*)) + (labels ((rec (x) + (declare (special *suppress-qvar*)) + (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)) + (declare (special *suppress-qvar*)) + (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))) + +;;; run-time + +(defun make-server (backend) ;; TODO thread safety + (let ((cache (make-hash-table :test #'equal)) ;; q->stm + (n 0)) + (lambda (msg &rest args) + (ecase msg + ;;(:clear-cache (clrhash cache)) + ((:finish :query) (apply backend msg args)) + (:execute + (destructuring-bind (q &rest args2) args + (apply backend msg + (princ-to-string + (or (gethash q cache) + (let ((x (setf (gethash q cache) (incf n)))) + (apply backend :prepare (princ-to-string x) q args2) + x))) + args2))))))) + +(defun call-with-server (server finish fn) + (let ((*server* server)) + (unwind-protect (funcall fn) + (when finish + (funcall server :finish))))) + +(defmacro with-server ((server &optional (finish t)) &body body) + `(call-with-server ,server ,finish (lambda () ,@body))) + +;;; compiler + +(defparameter *compiled-query-lambda-cache* nil) ;; equal form->fn + +(defun call-with-backend (backend compiled-query-lambda-cache fn) + (let ((*backend* backend) + (*compiled-query-lambda-cache* (or compiled-query-lambda-cache + (make-hash-table :test #'equal)))) + (funcall fn))) + +(defmacro with-backend ((backend &optional compiled-query-lambda-cache) &body body) + `(call-with-backend ,backend ,compiled-query-lambda-cache (lambda () ,@body))) + +;; good for development, let *backend* in with-database, leave +;; *compiled-query-lambda-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) + +(defun execute (q &rest qvars) + (apply *server* :execute q qvars)) + +(defmacro qmacroexpand (form) + `(funcall (lambda () (macroexpand ',form)))) + +(defun process-qchunk (x) + `(pure-form-to-string (macroexpand (funcall (lambda () ,@x))))) + +(defun make-query-lambda (args form) + `(lambda ,args + , (multiple-value-bind (str qvars qchunks) (to-string (macroexpand form)) + `(execute + ,(if qchunks + `(format nil ,str ,@(mapcar 'process-qchunk qchunks)) + str) + ,@(mapcar #'car qvars) + #+nil(list ,@(mapcar #'car qvars)))))) + +;;(make-query-lambda '(a b) '(q:+ (q:qvar a) (q:qvar b))) + +(defun compiled-query-lambda (args form) + (or (when *compiled-query-lambda-cache* + (or (gethash form *compiled-query-lambda-cache*) + (setf (gethash form *compiled-query-lambda-cache*) + (compile nil (make-query-lambda args form))))) + (compile nil (make-query-lambda args form)))) + +;;(compiled-query-lambda '(a b) '(q:+ (q:qvar a) (q:qvar b))) + +(defmacro query (args &body form) + (assert (not (cdr form))) + `(funcall (compiled-query-lambda ',args ,(car form)) ,@args)) + +;;(let ((a 1) (b 2)) (query (a b) '(q:+ (q:qvar a) (q:qvar b)))) + +(defun queries (args forms) + (mapcar (lambda (form) `(query ,args ',form)) forms)) + +(defmacro qlambda (args &body body) + `(lambda ,args ,@(queries args body))) + +(defmacro qdefun (name args &body body) + `(defun ,name ,args ,@(queries args body))) + +(defun apply-query (args vals form) + (apply (compiled-query-lambda args form) vals)) + +#+nil +(defmacro qlet (bindings &body body) + `(flet ,(mapcar (lambda (x) + (print `(,(car x) ,(cadr x) ,@(queries (cadr x) (cddr x)))) + `(,(car x) ,(cadr x) ,@(queries (cadr x) (cddr x)))) + bindings) + ,@body)) + +#+nil +(defun qmap (fn q) ;; TODO optimize properly using cl-postgres, move to backend? + (mapcar (lambda (x) (apply fn x)) q)) diff --git a/README b/README @@ -1,3 +1,5 @@ +-*- org -*- + http://ondoc.logand.com/d/900/5/ http://users.encs.concordia.ca/~haarslev/publications/jvlc92/node6.html http://www.cliki.net/MOP%20design%20patterns @@ -87,6 +89,7 @@ built on top of it. - Compiler - Macros - Backends + - Full text search - Portability - Object Relational Mapping - Custom persistent slot types @@ -122,6 +125,9 @@ built on top of it. Backends -------- + Full text search + ---------------- + Portability ----------- diff --git a/backend.lisp b/backend.lisp @@ -1,67 +0,0 @@ -;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty - -(in-package :2sql-backend) - -(defparameter *database* nil) -(defparameter *query-to-name* nil) -(defparameter *name-seq* nil) - -(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:*compiled-query-lambda-cache* (make-hash-table :test #'equal))) - (unwind-protect (funcall fn) - (cl-postgres:close-database *database*)))) - -(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))) - -(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:*compiled-query-lambda-cache* (make-hash-table :test #'equal))) - (unwind-protect (funcall fn) - (sqlite:disconnect *database*)))) - -(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 - (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 - (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 -;; *compiled-query-lambda-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-cl-postgres.asd b/cl-2sql-cl-postgres.asd @@ -0,0 +1,12 @@ +;; -*- lisp; -*- +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(asdf:defsystem :cl-2sql-cl-postgres + :description "cl-2sql -- Lisp to SQL compiler for Common Lisp." + :version "" + :author "Tomas Hlavaty" + :maintainer "Tomas Hlavaty" + :licence "MIT" + :depends-on (:cl-postgres) + :serial t + :components ((:file "2sql-cl-postgres"))) diff --git a/cl-2sql-cl-sqlite.asd b/cl-2sql-cl-sqlite.asd @@ -0,0 +1,12 @@ +;; -*- lisp; -*- +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(asdf:defsystem :cl-2sql-cl-sqlite + :description "cl-2sql -- Lisp to SQL compiler for Common Lisp." + :version "" + :author "Tomas Hlavaty" + :maintainer "Tomas Hlavaty" + :licence "MIT" + :depends-on (:cl-sqlite) + :serial t + :components ((:file "2sql-cl-sqlite"))) diff --git a/cl-2sql-dbquery.asd b/cl-2sql-dbquery.asd @@ -0,0 +1,12 @@ +;; -*- lisp; -*- +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(asdf:defsystem :cl-2sql-dbquery + :description "cl-2sql -- Lisp to SQL compiler for Common Lisp." + :version "" + :author "Tomas Hlavaty" + :maintainer "Tomas Hlavaty" + :licence "MIT" + :depends-on (:dbquery) + :serial t + :components ((:file "2sql-dbquery"))) diff --git a/cl-2sql.asd b/cl-2sql.asd @@ -1,29 +1,14 @@ ;; -*- 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." :version "" :author "Tomas Hlavaty" :maintainer "Tomas Hlavaty" :licence "MIT" - :depends-on (:split-sequence - :ironclad - :babel - ;;#+postgresql :cl-postgres - ;;#+sqlite :sqlite - :cl-postgres - :sqlite - :closer-mop - ) + :depends-on (:split-sequence) ;; TODO remove :serial t - :components ((:file "packages") - (:file "printer") - (:file "compiler") + :components ((:file "2sql") (:file "macros") - ;;#+(or postgresql sqlite) (:file "backend") - (:file "backend") - (:file "orm"))) + (:file "ormc"))) diff --git a/compiler.lisp b/compiler.lisp @@ -1,66 +0,0 @@ -;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty - -;;; Redefine EXECUTE. - -(in-package :2sql) - -(defparameter *compiled-query-lambda-cache* nil) ;; equal form->fn - -(defun execute (q qvars) ;; to be redefined in user code - (values q qvars)) - -(defmacro qmacroexpand (form) - `(funcall (lambda () (macroexpand ',form)))) - -(defun process-qchunk (x) - `(pure-form-to-string (macroexpand (funcall (lambda () ,@x))))) - -(defun make-query-lambda (args form) - `(lambda ,args - , (multiple-value-bind (str qvars qchunks) (to-string (macroexpand form)) - `(execute - ,(if qchunks - `(format nil ,str ,@(mapcar 'process-qchunk qchunks)) - str) - (list ,@(mapcar #'car qvars)))))) - -;;(make-query-lambda '(a b) '(q:+ (q:qvar a) (q:qvar b))) - -(defun compiled-query-lambda (args form) - (or (when *compiled-query-lambda-cache* - (or (gethash form *compiled-query-lambda-cache*) - (setf (gethash form *compiled-query-lambda-cache*) - (compile nil (make-query-lambda args form))))) - (compile nil (make-query-lambda args form)))) - -;;(compiled-query-lambda '(a b) '(q:+ (q:qvar a) (q:qvar b))) - -(defmacro query (args &body form) - (assert (not (cdr form))) - `(funcall (compiled-query-lambda ',args ,(car form)) ,@args)) - -;;(let ((a 1) (b 2)) (query (a b) '(q:+ (q:qvar a) (q:qvar b)))) - -(defun queries (args forms) - (mapcar (lambda (form) `(query ,args ',form)) forms)) - -(defmacro qlambda (args &body body) - `(lambda ,args ,@(queries args body))) - -(defmacro qdefun (name args &body body) - `(defun ,name ,args ,@(queries args body))) - -(defun apply-query (args vals form) - (apply (compiled-query-lambda args form) vals)) - -#+nil -(defmacro qlet (bindings &body body) - `(flet ,(mapcar (lambda (x) - (print `(,(car x) ,(cadr x) ,@(queries (cadr x) (cddr x)))) - `(,(car x) ,(cadr x) ,@(queries (cadr x) (cddr x)))) - bindings) - ,@body)) - -#+nil -(defun qmap (fn q) ;; TODO optimize properly using cl-postgres, move to backend? - (mapcar (lambda (x) (apply fn x)) q)) diff --git a/macros.lisp b/macros.lisp @@ -1,5 +1,10 @@ ;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty +(defpackage :2sql-macros + (:use) + (:nicknames :q) ;; TODO really? + (:import-from :cl :&optional :&rest :&body :nil :t)) + (in-package :2sql-macros) (cl:defmacro defsyntax (name args &body body) @@ -30,7 +35,10 @@ (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:or (cl:eq t backend) + (cl:if (cl:atom backend) + (cl:eq backend 2sql:*backend*) + (cl:member 2sql:*backend* backend)))) (cl:return-from here (cl:cadr x)))) ;; cadr or cdr? (cl:when ,ecase (cl:error "Backend ~s fell through BACKEND-CASE ~s" @@ -173,9 +181,11 @@ `(drop-if-exists ,name :index ,if-exists)) (defsyntax drop-sequence (name &optional if-exists) - `(drop-if-exists ,name :sequence ,if-exists)) + `(backend-ecase + (:postgresql (drop-if-exists ,name :sequence ,if-exists)) + (:sqlite (drop-table ,name ,if-exists)))) -;;(2sql:query (drop-sequence seq t)) +;;(2sql:query () '(drop-sequence seq t)) (defsyntax create-index (name unique tab using cols properties triggers &body where) #+nil ;; -> cols @@ -232,10 +242,25 @@ (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))))) + (:postgresql ,@(cl:when cols `((prefix :returning (clist ,@cols))))))) + +(defsyntax column (name type &optional nullable default other) + `(prefix ,name ,type + (nullable ,nullable) + ,@(cl:when default `(:default ,default)) + ,@(cl:when other (cl:list (cl:macroexpand other))))) + +(defsyntax columns (&body cols) + `(clist ,@cols)) (defsyntax create-sequence (name &optional temp) - `(prefix :create ,@(cl:when temp '(:temporary)) :sequence ,name)) + `(backend-ecase + (:postgresql + (prefix :create ,@(cl:when temp '(:temporary)) :sequence ,name)) + (:sqlite + (create-table ,name + (columns (column seq (integer-type) nil nil + (prefix :primary :key #+nil :autoincrement))))))) (defsyntax curval (seq) `(function :curval ,seq)) @@ -243,7 +268,7 @@ (defsyntax nextval (seq) `(backend-case (:oracle ,(cl:intern (cl:format nil "~:@(~a.nextval~)" seq))) ;; TODO dont intern, polutes this package - (:postgresql (function :nextval ,seq)))) + (:postgresql (function :nextval ,(2sql:print-symbol seq))))) (defsyntax setval (seq val &optional current) `(function :curval ,seq ,val ,@(cl:unless current '(:false)))) @@ -263,13 +288,13 @@ (defsyntax true-value () `(backend-ecase (:oracle "Y") - (:postgresql :true) + (:postgresql t #+nil :true) (:sqlite 1))) (defsyntax false-value () `(backend-ecase (:oracle "N") - (:postgresql :false) + (:postgresql nil #+nil :false) (:sqlite 0))) (defsyntax true-exp () @@ -354,8 +379,8 @@ (defop avg :avg) (defop sum :sum)) -(defsyntax asc (exp) `(prefix :asc ,exp)) -(defsyntax desc (exp) `(prefix :desc ,exp)) +(defsyntax asc (exp) `(prefix ,exp :asc)) +(defsyntax desc (exp) `(prefix ,exp :desc)) (defsyntax as (tab alias) `(prefix ,tab ,alias)) @@ -368,6 +393,9 @@ (defsyntax alter-table (tab &body clist) `(prefix :alter :table ,tab (clist ,@clist))) +(defsyntax alter-sequence (seq &body body) + `(prefix :alter :sequence ,seq ,@body)) + (defsyntax nullable (nullable) (cl:if nullable '(prefix :null) '(prefix :not :null))) @@ -443,15 +471,6 @@ (clist ,@(cl:loop :for (k v) :in alist :collect `(lst " = " ,k ,v))) ,where)) -(defsyntax column (name type &optional nullable default other) - `(prefix ,name ,type - (nullable ,nullable) - ,@(cl:when default `(:default ,default)) - ,@(cl:when other (cl:list (cl:macroexpand other))))) - -(defsyntax columns (&body cols) - `(clist ,@cols)) - (defsyntax create-table (name cols &optional temp as using) `(prefix :create ,@(cl:when temp '(:global :temporary)) @@ -500,7 +519,7 @@ (defsyntax sqrt (exp) `(backend-ecase - (:oracle (function :sqrt ,exp)) + ((:oracle :mysql) (function :sqrt ,exp)) (:postgresql (\|/ ,exp)))) (defsyntax bitand(lexp rexp) diff --git a/orm.lisp b/orm.lisp @@ -1,5 +1,19 @@ ;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty +(defpackage 2sql-orm + (:use :cl) + (:export :textp + :text + :defptype + :defpclass + :setup-pclass + :with-instance-cache + :make-pinstance + :with-pinstance-collector-cache + :query + :instance + )) + (in-package :2sql-orm) ;;; orm types @@ -36,8 +50,8 @@ (defstruct (pdate (:constructor make-pdate (y m d))) y m d) (defstruct (ptime (:constructor make-ptime (hh mm ss ms))) hh mm ss ms) -(defstruct (ptimestamp-with-timezone - (:constructor make-ptimestamp-with-timezone (date time timezone))) +(defstruct (ptimestamp-tz + (:constructor make-ptimestamp-tz (date time timezone))) date time timezone) ;;(defstruct pinterval y m d hh mm ss ms) @@ -45,7 +59,7 @@ (with-slots (y m d) a (values (format nil "~4,'0d-~2,'0d-~2,'0d" y m d) "date"))) -(defmethod cl-postgres:to-sql-string ((a ptimestamp-with-timezone)) ;; TODO timezone +(defmethod cl-postgres:to-sql-string ((a ptimestamp-tz)) ;; TODO timezone (with-slots (date time timezone) a (with-slots (y m d) date (with-slots (hh mm ss ms) time @@ -71,6 +85,11 @@ (defgeneric ptype-macroexpand (type)) +(defun persistent-type-p (type) + (get type 'persistent-type)) + +(deftype persistent-type () '(satisfies persistent-type-p)) + (defun lisp-type-to-ptype (type) ;; TODO more types (if (atom type) (case type @@ -79,11 +98,13 @@ (string '(q:varchar-type)) (pdate '(q:date-type)) (ptime '(q:time-type)) - (ptimestamp-with-timezone '(q:timestamp-with-timezone-type)) + (ptimestamp-tz '(q:timestamp-with-timezone-type)) (octet-vector '(q:blob-type)) (t (cond ((subtypep type 'persistent-object) (lisp-type-to-ptype 'oid)) + ((subtypep type 'persistent-type) (lisp-type-to-ptype 'oid)) + ;;((persistent-type-p type) (lisp-type-to-ptype 'oid)) (t (let ((x (ptype-macroexpand type))) (assert (not (eq x type))) (lisp-type-to-ptype x)))))) @@ -103,6 +124,10 @@ ;; (lisp-type-to-ptype '(text 3)) ;; (lisp-type-to-ptype '(text 3 2)) +(defun lisp-type-nullable-p (type) + (unless (eql 'boolean type) + (typep nil type))) + (defmacro defptype (name args specifier &optional db-type db-check) `(progn (deftype ,name ,args ,specifier) @@ -112,10 +137,13 @@ (defun natural0p (a) (and (integerp a) (<= 0 a))) -(defptype natural0 () '(and integer (satisfies natural0p)) 'integer 'q:le0) -(defptype natural1 () '(and integer (satisfies plusp)) 'integer 'q:plusp) +(defmethod ptype-macroexpand ((type (eql 'list))) + 'string) -(defptype oid () 'natural1) +(defptype natural0 () '(and integer (satisfies natural0p)) 'integer 'q:le0) +(defptype natural () '(and integer (satisfies plusp)) 'integer 'q:plusp) +(defptype universal-time () 'integer 'ptimestamp-tz) +(defptype oid () 'natural) ;;; persistent-object @@ -453,12 +481,23 @@ (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))))) ;; TODO oid_seq_ via symbol printing + (:postgresql (q:\| (q:<< (q:nextval "oid_seq_") ,bit-size) ,class-id)) ;; TODO oid_seq_ via symbol printing + (:sqlite (q:\| (q:<< (q:nextval oid-seq) ,bit-size) ,class-id))))) +#+nil +(defun octets-to-integer (octets) ;; TODO + (do () + () + )) + +#+nil +(defun string-to-octets () ;; TODO + (babel:string-to-octets (symbol-name class-name) :encoding :utf-8)) + +#+nil (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))) + (ironclad:digest-sequence :crc32 (string-to-octets (symbol-name class-name)))) (expt 2 +class-id-bit-size+))) (defmacro defpclass (name superclasses slots) @@ -496,9 +535,9 @@ ((typep value 'simple-date:timestamp) (setq value (multiple-value-bind (y m d hh mm ss ms) (simple-date:decode-timestamp value) - (make-ptimestamp-with-timezone (make-pdate y m d) - (make-ptime hh mm ss ms) - nil)))) + (make-ptimestamp-tz (make-pdate y m d) + (make-ptime hh mm ss ms) + nil)))) ((subtypep type 'persistent-object) (setq value (or (gethash value *instance-cache*) (make-proxy value)))))) diff --git a/ormc.lisp b/ormc.lisp @@ -0,0 +1,840 @@ +;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty + +(defpackage 2sql-ormc + (:use :cl) + (:export :textp + :text + :defptype + :defpclass + :deftable + :setup-pclass + :with-instance-cache + :make-pinstance + :with-pinstance-collector-cache + :query + :instance + )) + +(in-package :2sql-ormc) + +;;; orm types + +;; string => string | clob +;; string 3 => char 3 +;; text 5 &optional 2 => varchar 5 (no "" allowed, due to oracle) +;; string-or-text => string | varchar +;; char 1 => boolean + +(deftype universal-time () 'integer) + +(defun textp (a max &optional (min 1)) + (and (stringp a) (<= min (length a) max))) + +;; (textp "h" 3) +;; (textp "h" 3 2) +;; (textp "hi" 3) +;; (textp "hi" 3 2) +;; (textp "hello" 3) +;; (textp "hello" 3 2) + +(deftype text (max &optional (min 1)) + (assert (plusp min)) + (assert (< min max)) + (let ((p (gensym))) + (setf (symbol-function p) #'(lambda (a) (textp a max min))) + `(and string (satisfies ,p)))) + +;; (typep "h" '(text 3)) +;; (typep "h" '(text 3 2)) +;; (typep "hi" '(text 3)) +;; (typep "hi" '(text 3 2)) +;; (typep "hello" '(text 3)) +;; (typep "hello" '(text 3 2)) + +(defstruct (pdate (:constructor make-pdate (y m d))) y m d) +(defstruct (ptime (:constructor make-ptime (hh mm ss ms))) hh mm ss ms) +(defstruct (ptimestamp-tz + (:constructor make-ptimestamp-tz (date time timezone))) + date time timezone) +;;(defstruct pinterval y m d hh mm ss ms) + +(defun persistent-type-p (type) + (get type 'persistent-type)) + +(deftype persistent-type () '(satisfies persistent-type-p)) + +(defun oid (x) (funcall x 'oid)) + +;;(defstruct (proxy (:constructor make-proxy (oid object))) oid object) + +(defun nullablep (type) + (unless (eql 'boolean type) + (typep nil type))) + +(defun persistent-type-pkey (type1 &optional specs) + (unless specs + (setq specs (get type1 'deftable-specs))) + (when type1 + (destructuring-bind (a) (cdr (assoc :pkey specs)) + a))) + +(defun persistent-type-pkey-type (type1) + (destructuring-bind (&key type &allow-other-keys) + (cdr (assoc (persistent-type-pkey type1) (get type1 'deftable-slots))) + type)) + +(defun ptype-specifier (type) + (let ((y (get type 'ptype-specifier))) + (assert y) + (assert (not (eq y type))) + (assert (not (get type 'ptype-args))) + y)) + +(defun expand-ptype-to-db (type) + (assert type) + (if (atom type) + (case type + (boolean '(q:boolean-type)) + (integer '(q:integer-type)) + (string '(q:varchar-type)) + (pdate '(q:date-type)) + (ptime '(q:time-type)) + (ptimestamp-tz '(q:timestamp-with-timezone-type)) + (universal-time '(q:timestamp-with-timezone-type)) + (octet-vector '(q:blob-type)) + (t (if (subtypep type 'persistent-type) + (expand-ptype-to-db (persistent-type-pkey-type type)) + (or (get type 'db-type) + (expand-ptype-to-db (ptype-specifier type)))))) + (ecase (car type) + (or + (destructuring-bind (a b) (cdr type) + (assert (eq 'null a)) + (expand-ptype-to-db b))) + (integer `(q:integer-type ,(cadr type))) + (string `(q:char-type ,(cadr type))) + (text `(q:varchar-type ,(cadr type)))))) + +;;(expand-ptype-to-db 'integer) +;;(expand-ptype-to-db 'string) +;;(expand-ptype-to-db '(integer 16)) +;;(expand-ptype-to-db '(string 3)) +;;(expand-ptype-to-db '(text 3)) +;;(expand-ptype-to-db '(text 3 2)) +;;(expand-ptype-to-db 'oid) +;;(expand-ptype-to-db 'natural) + +(defun format-pdate (x) + (format nil "~4,'0d-~2,'0d-~2,'0d" (pdate-y x) (pdate-m x) (pdate-d x))) + +(defun format-ptime (x) + (format nil "~2,'0d:~2,'0d:~2,'0d" (ptime-hh x) (ptime-mm x) (ptime-mm x))) + +(defun format-ptimestamp-tz (x) + (error "TODO to-db ptmestamp-tz")) + +(defun format-universal-time (x) + (multiple-value-bind (ss mm hh d m y dw st tz) + (decode-universal-time x) + (declare (ignore dw st)) + (format nil "~4,'0d-~2,'0d-~2,'0dT~2,'0d:~2,'0d:~2,'0d~a~2,'0d" + y m d hh mm ss (if (plusp tz) "+" "-") (abs tz)))) + +(defun parse-pdate (x) + (assert (= 10 (length x))) + (assert (char= #\- (char x 4))) + (assert (char= #\- (char x 7))) + (make-pdate (parse-integer (subseq x 0 4)) + (parse-integer (subseq x 5 7)) + (parse-integer (subseq x 8 10)))) + +(defun parse-ptime (x) + (assert (= 8 (length x))) + (assert (char= #\: (char x 2))) + (assert (char= #\: (char x 5))) + (make-ptime (parse-integer (subseq x 0 2)) + (parse-integer (subseq x 3 5)) + (parse-integer (subseq x 6)) + 0)) + +(defun parse-ptimestamp-tz (x) + (error "TODO parse-ptimestamp-tz")) + +(defun parse-universal-time (x) + (assert (= 22 (length x))) + (assert (char= #\- (char x 4))) + (assert (char= #\- (char x 7))) + (assert (char= #\T (char x 10))) + (assert (char= #\: (char x 13))) + (assert (char= #\: (char x 16))) + (encode-universal-time (parse-integer (subseq x 17 19)) + (parse-integer (subseq x 14 16)) + (parse-integer (subseq x 11 13)) + (parse-integer (subseq x 8 10)) + (parse-integer (subseq x 5 7)) + (parse-integer (subseq x 0 4)) + (parse-integer (subseq x 19)))) + +;;(print (parse-universal-time (print (format-universal-time (print (get-universal-time)))))) ;; TODO fix tz stuff! + +(defun to-db (x type) + (if (atom type) + (case type + (boolean (macroexpand (if x '(q:true-value) '(q:false-value)))) + (integer x) + (string x) + (pdate (format-pdate x)) + (ptime (format-ptime x)) + (ptimestamp-tz (format-ptimestamp-tz x)) + (universal-time (format-universal-time x)) + (octet-vector x) + ((t) (prin1-to-string x)) + (t (if (subtypep type 'persistent-type) + (let ((oid (oid x))) + (if (atom oid) + oid + (destructuring-bind (type2 &rest pkey) oid + (assert (eq type type2)) + (assert (not (cdr pkey))) ;; TODO multislot pkey + (car pkey)))) + (to-db x (ptype-specifier type))))) + (ecase (car type) + (or + (destructuring-bind (a b) (cdr type) + (assert (eq 'null a)) + (if x (to-db x b) :null))) + (and + (destructuring-bind (a b) (cdr type) + (assert a) + (assert (eq 'satisfies (car b))) + (assert (funcall (cadr b) x)) + (to-db x a))) + #+nil(integer `(q:integer-type ,(cadr type))) + (string + (destructuring-bind (a) (cdr type) + (assert (= a (length x))) + (to-db x 'string))) + (text + (assert (apply 'textp (cons x (cdr type)))) + (to-db x 'string))))) + +(defun from-db (x type) + (if (atom type) + (case type + (boolean (cond + ((eql x (macroexpand '(q:true-value))) t) + ((eql x (macroexpand '(q:false-value))) nil) + (t (error "Unknown boolean value ~s of type ~s" x type)))) + (integer x) + (string x) + (pdate (parse-pdate x)) + (ptime (parse-ptime x)) + (ptimestamp-tz (parse-ptimestamp-tz x)) + (universal-time (parse-universal-time x)) + (octet-vector x) + (oid x) + ((t) (read-from-string x)) + (t (if (subtypep type 'persistent-type) + (error "TODO from-db persistent-type ~s ~s" x type) + #+nil + (let ((oid (oid x))) + (if (atom oid) + oid + (destructuring-bind (type2 &rest pkey) oid + (assert (eq type type2)) + (assert (not (cdr pkey))) ;; TODO multislot pkey + (car pkey)))) + (from-db x (ptype-specifier type))) + ;;(error "TODO from-db") + #+nil + (if (subtypep type 'persistent-type) + (make-proxy x nil) + (error "TODO from-db")))) + (ecase (car type) + (or + (destructuring-bind (a b) (cdr type) + (assert (eq 'null a)) + (unless (eq :null x) (from-db x b)))) + (and + (destructuring-bind (a b) (cdr type) + (assert a) + (assert (eq 'satisfies (car b))) + (assert (funcall (cadr b) x)) + (from-db x a))) + #+nil(integer `(q:integer-type ,(cadr type))) + (string + (destructuring-bind (a) (cdr type) + (assert (= a (length x))) + (from-db x 'string))) + (text + (assert (apply 'textp (cons x (cdr type)))) + (from-db x 'string))))) + +(defun to-internal (x type) + (if (atom type) + (case type + (boolean x) + (integer x) + (string x) + (pdate x) + (ptime x) + (ptimestamp-tz x) + (universal-time x) + (octet-vector x) + ((t) x) + (t (if (subtypep type 'persistent-type) + x ;;(error "TODO to-internal") ;;(make-proxy (oid x) x) + (to-internal x (ptype-specifier type))))) + (ecase (car type) + (or + (destructuring-bind (a b) (cdr type) + (assert (eq 'null a)) + (when x (to-internal x b)))) + (and + (destructuring-bind (a b) (cdr type) + (assert a) + (assert (eq 'satisfies (car b))) + (assert (funcall (cadr b) x)) + (to-internal x a))) + #+nil(integer `(q:integer-type ,(cadr type))) + (string + (destructuring-bind (a) (cdr type) + (assert (= a (length x))) + (to-internal x 'string))) + (text + (assert (apply 'textp (cons x (cdr type)))) + (to-internal x 'string))))) + +(defun from-internal (x type) + (if (atom type) + (case type + (boolean x) + (integer x) + (string x) + (pdate x) + (ptime x) + (ptimestamp-tz x) + (universal-time x) + (octet-vector x) + (oid x) + ((t) x) + (t (if (subtypep type 'persistent-type) + (load-object type x) + (from-internal x (ptype-specifier type))))) + (ecase (car type) + (or + (assert (eq 'null (cadr type))) + (assert (not (cddddr type))) + (unless (eq :null x) + (from-internal x (caddr type)))) + (and + (destructuring-bind (a b) (cdr type) + (assert a) + (assert (eq 'satisfies (car b))) + (assert (funcall (cadr b) x)) + (from-internal x a))) + #+nil(integer `(q:integer-type ,(cadr type))) + (string + (destructuring-bind (a) (cdr type) + (assert (= a (length x))) + (from-internal x 'string))) + (text + (assert (apply 'textp (cons x (cdr type)))) + (from-internal x 'string))))) + +(defmacro defptype (name args specifier &optional #+nil internal db-type db-check) + (assert name) + `(progn + ,@(when specifier `((deftype ,name ,args ,specifier))) + (eval-when (:compile-toplevel :load-toplevel :execute) + (setf (get ',name 'ptype-args) ,args + (get ',name 'ptype-specifier) ,specifier + ;;(get ',name 'internal-type) ,internal + (get ',name 'db-type) ,db-type + (get ',name 'db-check) ,db-check)))) + +(deftype octet () '(unsigned-byte 8)) +(deftype octet-vector (&optional size) `(simple-array octet (,size))) + +(defun natural0p (a) + (and (integerp a) (<= 0 a))) + +(defptype natural0 () '(and integer (satisfies natural0p)) '(q:integer-type) 'q:le0) +(defptype natural () '(and integer (satisfies plusp)) '(q:integer-type) 'q:plusp) +(defptype oid () 'natural) +(defptype t () () '(q:varchar-type)) + +(defvar *object-cache*) + +(defmacro with-object-cache (() &body body) + `(let ((*object-cache* (make-hash-table :test #'equal))) + ,@body)) + +(defun check-cached-object (x) + (assert (eq x (gethash (oid x) *object-cache*)))) + +(defun cache-object (x) + (assert (not (gethash (oid x) *object-cache*))) + (setf (gethash (oid x) *object-cache*) x)) + +(defun insert-into (tab args vals returning-cols) + (2sql:apply-query args + vals + `(q:insert-into ,tab ,args + (q:values ,@(mapcar (lambda (x) `(q:qvar ,x)) args)) + (q:returning ,returning-cols)))) + +(defun %pslot-accessor (k) + (values (intern (format nil "$~a" k) (symbol-package k)))) + +(defun pslot-accessor (table slot) + (%pslot-accessor + (or (getf (cdr (assoc slot (list-pslots table))) :accessor) + (pslot-name (assoc slot (list-pslots table)))))) + +;;(pslot-accessor 'ex::document 'ex::nr) + +(defun build-deftable-args (slots) + (loop + for x in slots + collect (destructuring-bind (k &key initform &allow-other-keys) x + (if initform + `(,k ,initform) + `(,k '%unbound-slot))))) + +(defun build-deftable-insert (name slots) + (let ((unbound (gensym)) + (args (gensym)) + (vals (gensym)) + (ret (gensym)) + (ret1 (gensym))) + `(let (,unbound ,args ,vals) + ,@(loop + for x in slots + collect (destructuring-bind (k &key type &allow-other-keys) x + `(cond + ((eq '%unbound-slot ,k) (push ',k ,unbound)) + (t (push ',k ,args) + (push (to-db ,k ',type) ,vals))))) + (let ((,ret (insert-into ',name ,args ,vals (nreverse ,unbound)))) + (assert (not (cdr ,ret))) + (let ((,ret1 (car ,ret))) + ,@(loop + for x in slots + collect (destructuring-bind (k &key type &allow-other-keys) x + `(when (eq '%unbound-slot ,k) + (setq ,k (from-db (pop ,ret1) ',type)))))))))) + +(defun build-deftable-messages (name slots specs) + (nconc + (unless (member 'oid slots :key #'car) + `((oid + (assert (not p)) + (list ',name ,@(cdr (assoc :pkey specs)))))) + (loop + for x in slots + collect (destructuring-bind (k &key type &allow-other-keys) x + `(,k ,(if (eq 'oid k) + `(progn + (assert (not p)) + (from-internal (bound ,k ',k) ',type)) + `(if p + (setq ,k (to-internal v ',type)) + (from-internal (bound ,k ',k) ',type)))))))) + +(defun build-deftable-accessors (slots) + (loop + for x in slots + appending (destructuring-bind (k &key accessor &allow-other-keys) x + (unless accessor + (setq accessor (%pslot-accessor k))) + (unless (eq 'oid k) + `((defun ,accessor (x) (funcall x ',k)) + (defun (setf ,accessor) (v x) (funcall x ',k v))))))) + +(defun persistent-type1 (type) + (when (subtypep type 'persistent-type) + (if (atom type) + type + (ecase (car type) + (or (destructuring-bind (a b) (cdr type) + (assert (eq 'null a)) + b)))))) + +(defun build-deftable-create-table (name slots specs) + `(,@(loop + for x in specs + when (eq :sequence (car x)) + appending (destructuring-bind (seq &optional slot) (cdr x) + `((q:create-sequence ,seq) + ,@ (when slot + #+nil ;; TODO alter sequence password_seq_ owned by password.nr + (q:alter-sequence seq :owned :by name.slot))))) + (q:create-table + ,name + (q:columns + ,@(loop + for x in slots + collect (destructuring-bind + (k &key type db-initform &allow-other-keys) x + `(q:column ,k + ,(expand-ptype-to-db type) + ,(nullablep type) + ,db-initform))))) + ,@ (let ((pkey (cdr (assoc :pkey specs)))) + (when pkey + `((q:alter-table ,name + (q:add-primary-key-constraint + ,(intern (format nil "~a-PK" name) (symbol-package name)) + ,@pkey))))) + ,@(loop + for x in slots + appending (destructuring-bind + (k &key type on-delete on-update &allow-other-keys) + x + (let ((pkey (persistent-type-pkey + (persistent-type1 type)))) + (when pkey + `((q:alter-table ,name + (q:add-foreign-key-constraint + ,(intern (format nil "~a-~a-FK" name k) + (symbol-package name)) + (,k) + (,pkey) + ,type + ,on-delete + ,on-update))))))) + ,@ (loop + for x in specs + when (eq :unique (car x)) + collect (destructuring-bind (uname &rest cols) (cdr x) + `(q:alter-table + ,name (q:add-unique-constraint ,uname ,@cols)))))) + +(defun build-deftable-load (name slots specs) + (let* ((pkey (cdr (assoc :pkey specs))) + (npkey (loop + for x in slots + for k = (car x) + unless (member k pkey) + collect k)) + (package (symbol-package name)) + (load-name (intern (format nil "LOAD-~a" name) (symbol-package name))) + (%class-name (intern (format nil "MAKE-~a" name) package))) + `(defun ,load-name ,pkey + (or (gethash ,(if (find 'oid slots :key #'car) ;; or (car pkey) ? + 'oid + `(cons ',name (list ,@pkey))) + *object-cache*) + (let ((z (2sql:query ,pkey + '(q:select (q:clist ,@npkey) + (q:from ,name) + (q:where + (q:and ,@(loop + for x in pkey + collect `(q:= ,x (q:qvar ,x))))))))) + (assert z) + (assert (not (cdr z))) + (let ((zz (car z))) + (let (,@(loop + for x in npkey + collect `(,x (pop zz)))) + (cache-object + (,%class-name + ,@(loop + for x in slots + for k = (car x) + appending `(,(intern (symbol-name k) :keyword) ,k))))))))))) + +(defun build-deftable-list (name slots) + (let* ((package (symbol-package name)) + (where + (loop + for s in slots + as n = (pslot-name s) + as type = (pslot-type s) + collect `(unless (eq '%unbound-slot ,n) + (if (atom ,n) + `(q:= ,',n ,(to-internal (to-db ,n ',type) ',type)) + `(q:in ,',n + (q:par + (q:clist + ,@(loop + for x in ,n + collect (to-internal (to-db x ',type) + ',type)))))))))) + `(defun ,(intern (format nil "LIST-~a" name) package) + (project order limit offset + &key ,@(mapcar (lambda (x) `(,(pslot-name x) '%unbound-slot)) slots)) + (2sql-ormc:query () + `(q:select (,@(or project `((2sql-ormc::instance ,',name)))) + (q:from ,',name) + (q:where (q:and ,@(loop + for x in (list ,@where) + when x + collect x))) + ,@(when order `((q:order-by ,@order))) + ,@(when limit `((q:limit ,limit))) + ,@(when offset `((q:offset ,offset)))))))) + +(defun build-deftable (name body) + (let ((package (symbol-package name))) + (let ((class-name (intern (format nil "CREATE-~a" name) package)) + (%class-name (intern (format nil "MAKE-~a" name) package)) + (slots (car body)) + (specs (cdr body))) + `(progn + (defun ,%class-name (&key ,@(build-deftable-args slots)) + (lambda (msg &optional (v nil p)) + (flet ((bound (v slot) + (if (eq '%unbound-slot v) + (error "unbound slot ~s in ~s" slot ',class-name) + v))) + (ecase msg + ,@(build-deftable-messages name slots specs))))) + (defun ,class-name (&key ,@(build-deftable-args slots)) + ,@(loop + for x in slots + collect (destructuring-bind (k &key type &allow-other-keys) x + `(unless (eq '%unbound-slot ,k) + (setq ,k (to-internal ,k ',type))))) + ,(build-deftable-insert name slots) + (cache-object + (,%class-name + ,@(loop + for x in slots + for k = (car x) + appending `(,(intern (symbol-name k) :keyword) ,k))))) + ,@(build-deftable-accessors slots) + ,(build-deftable-load name slots specs) + ,(build-deftable-list name slots) + (eval-when (:compile-toplevel :load-toplevel :execute) + (deftype ,name () '(satisfies persistent-type-p)) + (setf (get ',name 'create-table) + ',(build-deftable-create-table name slots specs) + (get ',name 'make-object) + ',%class-name + #+nil(get ',name 'persistent-type) + #+nil t)))))) + +(defmacro deftable (name () &body body) + `(progn + ,(build-deftable name body) + (eval-when (:compile-toplevel :load-toplevel :execute) + (setf (get ',name 'deftable-slots) ',(car body) + (get ',name 'deftable-specs) ',(cdr body))))) + +#+nil +(2sql:query (oid value) + `(q:update ,tab + ((,pslot-name (q:qvar value))) + (q:where (q:= oid (q:qvar oid))))) + +#+nil +(defconstant +table-id-bit-size+ 16) + +;; #+nil +;; (defmacro oid-exp (table-id) +;; (let ((bit-size +table-id-bit-size+)) +;; `(q:backend-ecase +;; (:oracle (q:+ (q:* (q:nextval oid-seq) (q:power 2 ,bit-size)) ,table-id)) +;; (:postgresql ;; TODO oid_seq_ via symbol printing +;; (q:\| (q:<< (q:nextval "oid_seq_") ,bit-size) ,table-id)) +;; (:sqlite (q:\| (q:<< (q:nextval oid-seq) ,bit-size) ,table-id))))) + +#+nil +(defun setup-pclass (class-name) + (2sql:query () + `(q:create-table + ,class-name + (q:columns + (q:column oid (q:integer-type) nil + (oid-exp (q:qchunk (class-name-to-class-id ',class-name)))) + ,@(loop + for x in (list-pslots class-name) + unless (eq 'oid (pslot-name x)) + collect `(q:column ,(pslot-name x) + ,(pslot-ptype x) + ,(pslot-nullable x) + ,(pslot-initform x))))))) + +#+nil +(defun assert-type (value type) + (unless (typep value type) + (cond ;; TODO more cases + ((typep value 'string) + (setq value (coerce value 'simple-string))) + ((typep value 'simple-date:timestamp) + (setq value (multiple-value-bind (y m d hh mm ss ms) + (simple-date:decode-timestamp value) + (make-ptimestamp-tz (make-pdate y m d) + (make-ptime hh mm ss ms) + nil)))) + ((subtypep type 'persistent-object) + (setq value (or (gethash value *instance-cache*) + (make-proxy value)))))) + (assert (or (typep value type) + (when (subtypep type 'persistent-object) + (proxy-p value)))) + value) + +#+nil +(defun delete-object (x) + (let ((oid (oid x))) + (remhash oid *object-cache*) + (2sql:query (oid) + `(q:delete-from ,(type-of a) + (q:where (q:= oid (q:qvar oid))))))) + +(defparameter *instance-collector-cache* nil) ;; equal form->fn + +(defmacro with-pinstance-collector-cache (args &body body) + (declare (ignore args)) + `(let ((*instance-collector-cache* (make-hash-table :test #'equal))) + ,@body)) + +(defparameter *instance-collectors* nil) ;; list fn + +(defun %query (form rows) + (multiple-value-bind (value present) + (gethash form *instance-collector-cache*) + (cond + (present + (assert (not *instance-collectors*)) + (setq *instance-collectors* value)) + (t + (setq *instance-collectors* (nreverse *instance-collectors*)) + (setf (gethash form *instance-collector-cache*) *instance-collectors*)))) + (loop + for row in rows + for tail = row + collect (nconc (loop + for fn in *instance-collectors* + collect (multiple-value-bind (instance tail2) + (funcall fn tail) + (setq tail tail2) + instance)) + tail))) + +(defmacro query (args form) + `(let ((*instance-collectors* nil) ;; TODO that's not right is it? need to remember form->collector! for subsequent queries + #+nil(*alias-to-table* (make-hash-table))) + ;;(maphash (lambda (k v) (print (list :@@@ k v))) *alias-to-table*) + (%query ,form (2sql:query ,args ,form)))) + +#+nil +(defmacro query (args form) + `(let* (#+nil(*alias-to-table* (make-hash-table)) + (*instance-collectors* nil) ;; TODO that's not right is it? need to remember form->collector! for subsequent queries + (rows (2sql:query ,args ,form))) + ;;(maphash (lambda (k v) (print (list :@@@ k v))) *alias-to-table*) + (multiple-value-bind (value present) + (gethash ,form *instance-collector-cache*) + (cond + (present + (assert (not *instance-collectors*)) + (setq *instance-collectors* value)) + (t + (setq *instance-collectors* (nreverse *instance-collectors*)) + (setf (gethash ,form *instance-collector-cache*) *instance-collectors*)))) + (loop + for row in rows + for tail = row + collect (nconc (loop + for fn in *instance-collectors* + collect (multiple-value-bind (instance tail2) + (funcall fn tail) + (setq tail tail2) + instance)) + tail)))) + +(defun list-pslots (table) + (get table 'deftable-slots)) + +(defun pslot-name (pslot) + (car pslot)) + +(defun pslot-type (pslot) + (getf (cdr pslot) :type)) + +;;(list-pslots 'ex::document) +;;(pslot-name (car (list-pslots 'ex::document))) +;;(pslot-type (cadr (list-pslots 'ex::document))) + +(defun pslot-initarg (pslot) + (or (getf (cdr pslot) :initarg) + (values (intern (symbol-name (pslot-name pslot)) :keyword)) + #+nil(pslot-name pslot))) ;; TODO this is better + explicit keyword + +(defun make-object (table &rest args) + (apply (get table 'make-object) args)) + +;;(get 'ex::document 'make-object) + +(defmacro instance (tab &optional alias) ;; use inside 2sql queries + (let ((pslots (list-pslots tab))) + (push (lambda (row) + (values (let ((oid (car row)) + (args (loop + for x in pslots + appending (list (pslot-initarg x) + (pop row) + ;;(from-db (pop row) (pslot-type x)) + #+nil(assert-type (pop row) (pslot-type x)))))) ;; TODO resolve pclassp foreign ref (lazy foreign reference?) + ;;(print (list :@@@ (mapcar (lambda (x) (cons x (type-of x))) args))) + (or (gethash oid *object-cache*) + (cache-object (apply 'make-object tab args))) + #+nil + (unless (eq :null oid) + ;; TODO uniq oid->instance cache + (apply #'make-instance tab :oid oid args))) ;; TODO fix values from rdbms backend to lisp, e.g. :null :true :false pclasp + row)) + *instance-collectors*) + `(q:clist + ,@ (flet ((sym (name) + (if alias + (intern (format nil "~a.~a" alias name)) ;; TODO avoid intern + name))) + (mapcar (lambda (x) (sym (pslot-name x))) pslots))))) + +(defmacro with-pobject ((name &rest slots) object &body body) + (let ((_object (gensym))) + `(let ((,_object ,object)) + (symbol-macrolet + ,(mapcar (lambda (s) `(,s (,(pslot-accessor name s) ,_object))) slots) + ,@body)))) + +(defun load-object (table oid) + (or (gethash oid *object-cache*) + (let ((x (query (oid) + `(q:select ((instance ,table)) + (q:from ,table) + (q:where (q:= oid (q:qvar oid))))))) + (assert (not (cdr x))) + (assert (car x)) + (assert (not (cadr x))) + (assert (functionp (caar x))) + (caar x)))) + +;; #+nil +;; (defmacro with-pclasses (names &body body) +;; (labels ((rec (x) +;; (if x +;; `(progn +;; (c2mop:ensure-finalized (find-class ',(car x))) +;; (2sql-orm:setup-pclass ',(car x)) +;; (unwind-protect ,(rec (cdr x)) +;; (2sql:query () '(q:drop-table ,(car x) t t)))) +;; `(progn ,@body)))) +;; (rec names))) + +;; #+nil +;; (defmacro with-psequences (names &body body) +;; (labels ((rec (x) +;; (if x +;; `(progn +;; (2sql:query () '(q:create-sequence ,(car x))) +;; (unwind-protect ,(rec (cdr x)) +;; (2sql:query () '(q:drop-sequence ,(car x) t)))) +;; `(progn ,@body)))) +;; (rec names))) diff --git a/packages.lisp b/packages.lisp @@ -1,56 +0,0 @@ -;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty - -(in-package :cl-user) - -(defpackage 2sql - (:use :cl) - (:export - ;; printer - #:print-string - #:print-symbol - #:print-qvar - #:print-form - #:pure-form-to-string - #:to-string - ;; compiler - #:*backend* - #:*compiled-query-lambda-cache* - #:execute - #:query - #:qlambda - #:qdefun - #:apply-query - #:qlet - #:qmacroexpand - #:qmap - #:quoted-query - )) - -(defpackage 2sql-macros - (:use) - (:nicknames :q) ;; TODO really? - (:import-from :cl #:&optional #:&rest #:&body #:nil #:t)) - -(defpackage 2sql-backend - (:use :cl) - (:export - #:with-postgresql-connection - #:with-sqlite-connection)) - -(defpackage 2sql-orm - (:use :cl) - (:export - #:textp - #:text - #:defptype - #:defpclass - #:setup-pclass - #:with-instance-cache - #:make-pinstance - #:with-pinstance-collector-cache - #:query - #:instance - )) - -(defpackage 2sql-tests - (:use :cl :2sql)) diff --git a/printer.lisp b/printer.lisp @@ -1,85 +0,0 @@ -;;; 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 @@ -1,7 +1,68 @@ ;;; cl-2sql Copyright (c) 2011 Tomas Hlavaty +(defpackage 2sql-tests + (:use :cl :2sql)) + (in-package :2sql-tests) +;; (setq c (make-sqlite-server "./dbquery-sqlite" "sqlite.db")) +;; (time (funcall c "select 1, 2+3")) +;; (time (funcall c "select 4, 'hello'")) +;; (funcall c nil) +;; (funcall c) + +;; (setq c (make-pg-server "./dbquery-pg" "dbname='pokus' user='tomas'")) +;; (time (funcall c "select 1, 2+3")) +;; (time (funcall c "select 4, 'hello'")) +;; (funcall c nil) +;; (funcall c) + +;; (setq c (make-mysql-server "./dbquery-mysql" "localhost" "tomas" "Ri3OoL3h" "pokus")) +;; (time (funcall c "select 1, 2+3")) +;; (time (funcall c "select 4, 'hello'")) +;; (funcall c nil) +;; (funcall c) + + +(loop + for (2sql:*backend* backend) + in (list + (list :postgresql + (2sql-dbquery:make-backend + (dbquery:make-pg-server "/home/tomas/git/dbquery/dbquery-pg" + "dbname='pokus' user='tomas'"))) + #+nil + (list :postgresql + (2sql-cl-postgres:make-backend "pokus" "tomas" "test123" "localhost" 5432 :no)) + #+nil + (list :sqlite + (2sql-dbquery:make-backend + (dbquery:make-sqlite-server "/home/tomas/git/dbquery/dbquery-sqlite" + "/home/tomas/git/dbquery/sqlite.db"))) + #+nil ;; TODO dbquery-mysql + (list :mysql + (2sql-dbquery:make-backend + (dbquery:make-mysql-server "/home/tomas/git/dbquery/dbquery-mysql" + "localhost" "tomas" "Ri3OoL3h" "pokus")))) + collect (progn ;;time + (2sql:with-server ((2sql::make-server backend)) + (2sql:with-backend (2sql:*backend*) + (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) + #+nil + (q:select ((q:sqrt 2))) + #+nil + (q:select ((q:sqrt (q:qvar a #+nil(q:integer-type))))) + (q:select ((q:sqrt (q:qvar a (q:integer-type)))))))) + (progn ;;time + (list 2sql:*backend* + (funcall q a) + (funcall q a)))))))))) + + @@ -138,19 +199,18 @@ (let ((v1 "hello") (v2 314159)) - (query (q:select (:*) - (q:from x) - (q:where (q:= (q:qvar v1) (q:qvar v2)))))) + (2sql:query (v1 v2) + '(q:select (q:+ (q:qvar v1) (q:qvar v2))))) (let ((v1 "hello") (v2 314159)) - (query ;; macroexpand - (q:select (:*) - (q:from x) - (q:where (q:where (q:= (q:qvar v1) (q:qvar v2))))))) + (2sql:query (v1 v2) ;; macroexpand + '(q:select (:*) + (q:from x) + (q:where (q:where (q:= (q:qvar v1) (q:qvar v2))))))) (let ((v1 "hello") (v2 314159) @@ -188,7 +248,7 @@ ;;(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))))))) + (q:select ((q:sqrt (q:qvar a (q:integer-type)))))))) (funcall q a) (funcall q a)))) ;; reusing from *compiled-query-lambda-cache* @@ -210,30 +270,33 @@ ;; suppress-qvar -(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") +(2sql-backend: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))))))) + (2sql:query (a b) + '(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") +(2sql-backend: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)))) +#+nil +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (2sql: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)))) +#+nil (with-postgresql-connection ("pokus" "tomas" "test123" "localhost") (flet ((mul (x y) ;; TODO undefined function! capture flet? (* x y))) @@ -262,27 +325,28 @@ bytea (vector (unsigned-byte 8)) (defmacro integer-qvar (name) `(q:qvar ,name :integer)) -(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") (let ((a 2)) - (query (q:select ((q:sqrt (integer-qvar a)) - (q:+ 1 (q:qchunk (+ 2 3)))))))) + (2sql:query (a) + '(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)))) +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (2sql:query () '(q:select (q:clist 22 "Folie et déraison" #+nil 4.5 )))) -(with-postgresql-connection ("pokus" "tomas" "test123" "localhost") +(2sql-backend: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)))))))) + (2sql:query (a b c d) + '(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)))))))) @@ -330,25 +394,25 @@ bytea (vector (unsigned-byte 8)) -(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)))))) +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (2sql: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)))))) +(2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") + (2sql: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 @@ -394,12 +458,12 @@ bytea (vector (unsigned-byte 8)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; orm -(defpclass t1 () +(2sql-orm: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))) -(defpclass t2 () +(2sql-orm:defpclass t2 () ((d1 :type integer :initarg :d1 :accessor d1) (d2 :type (or null integer) :initarg :d2 :initform nil :accessor d2) (d3 :type integer :initarg :d3 :initform 271 :accessor d3))) @@ -407,30 +471,27 @@ bytea (vector (unsigned-byte 8)) (trace 2sql:execute) (2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") - (with-pinstance-collector-cache () - (2sql:query () '(q:drop-sequence oid-seq t)) - (2sql:query () '(q:create-sequence oid-seq)) - (with-tables (t1 t2) - (make-pinstance 't1 :c1 1 :c2 2 :c3 3) - (make-pinstance 't1 :c1 1 :c2 2) - (2sql:query () '(q:select :* (q:from t1))) - - #+nil - (x-query () - '(q:select ((x-instance t1 x) (q:sum x.c1)) - (q:from (q:as t1 x)))) - ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) - (x-query () - '(q:select ((x-instance t1 a) (x-instance t2 b) a.c2) - (q:from (q:left-join (q:as t1 a) (q:as t2 b) (q:= a.c1 b.d1))))) - ;; this works 2nd time only with *instance-collector-cache* - ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) - ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) - ))) - -select => populate instances -update => clear affected instances from cache -delete => clear affected instances from cache + (2sql-orm:with-pinstance-collector-cache () + (2sql-orm:with-instance-cache () + (2sql:query () '(q:drop-sequence oid-seq t)) + (2sql:query () '(q:create-sequence oid-seq)) + (2sql-orm::with-pclasses (t1 t2) + (2sql-orm:make-pinstance 't1 :c1 1 :c2 2 :c3 3) + (2sql-orm:make-pinstance 't1 :c1 1 :c2 2) + (2sql:query () '(q:select :* (q:from t1))) + #+nil + (x-query () + '(q:select ((x-instance t1 x) (q:sum x.c1)) + (q:from (q:as t1 x)))) + ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) + #+nil + (x-query () + '(q:select ((x-instance t1 a) (x-instance t2 b) a.c2) + (q:from (q:left-join (q:as t1 a) (q:as t2 b) (q:= a.c1 b.d1))))) + ;; this works 2nd time only with *instance-collector-cache* + ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) + ;;(x-query () '(q:select ((x-instance t1)) (q:from t1))) + )))) ;;; http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html ;;; toxi solution @@ -460,6 +521,7 @@ delete => clear affected instances from cache (defun build-sql-query (q) `(q:par ,(if (atom q) + ;; TODO not :b_.* but (2sql-orm:instance b) and coolect only if toplevel select `(q:select :b_.* ;; TODO b.* w/o interning (q:dotted-name b :*) (q:from (q:as bookmark b) (q:as tag t) (q:as tagmap m)) (q:where (q:and (q:= b.oid m.bookmark) @@ -472,31 +534,32 @@ delete => clear affected instances from cache (2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") (2sql-orm:with-pinstance-collector-cache () - (2sql-orm::with-psequences (oid-seq) - (2sql-orm::with-pclasses (tag bookmark tagmap) - (let ((t1 (make-tag "bookmark")) - (t2 (make-tag "webservice")) - (t3 (make-tag "semweb")) - (t4 (make-tag "lisp")) - (t5 (make-tag "sql")) - (b1 (make-bookmark "b1")) - (b2 (make-bookmark "b2"))) - (make-tagmap b1 t1) - (make-tagmap b1 t2) - (make-tagmap b1 t3) - (make-tagmap b1 t4) - (make-tagmap b2 t1) - (make-tagmap b2 t2) - (flet ((look-up (q) - (2sql-orm:query () ;; TODO cache compiled queries - `(q:select ((2sql-orm:instance bookmark x)) - (q:from (q:as ,(build-sql-query q) x)))))) - ;; Query for “bookmark+webservice+semweb” - (look-up '(and "bookmark" "webservice" "semweb")) - ;; Query for “bookmark|webservice|semweb” - (look-up '(or "bookmark" "webservice" "semweb")) - ;; Query for “bookmark+webservice-semweb” - (look-up '(not (and "bookmark" "webservice") "semweb")))))))) + (2sql-orm:with-instance-cache () + (2sql-orm::with-psequences (oid-seq) + (2sql-orm::with-pclasses (tag bookmark tagmap) + (let ((t1 (make-tag "bookmark")) + (t2 (make-tag "webservice")) + (t3 (make-tag "semweb")) + (t4 (make-tag "lisp")) + (t5 (make-tag "sql")) + (b1 (make-bookmark "b1")) + (b2 (make-bookmark "b2"))) + (make-tagmap b1 t1) + (make-tagmap b1 t2) + (make-tagmap b1 t3) + (make-tagmap b1 t4) + (make-tagmap b2 t1) + (make-tagmap b2 t2) + (flet ((look-up (q) + (2sql-orm:query () ;; TODO cache compiled queries + `(q:select ((2sql-orm:instance bookmark x)) + (q:from (q:as ,(build-sql-query q) x)))))) + ;; Query for “bookmark+webservice+semweb” + (look-up '(and "bookmark" "webservice" "semweb")) + ;; Query for “bookmark|webservice|semweb” + (look-up '(or "bookmark" "webservice" "semweb")) + ;; Query for “bookmark+webservice-semweb” + (look-up '(not (and "bookmark" "webservice") "semweb"))))))))) ;; http://pinterface.livejournal.com/34706.html ;; http://pinterface.livejournal.com/35042.html @@ -507,7 +570,7 @@ delete => clear affected instances from cache (2sql-orm:defptype title () '(2sql-orm:text 64)) (2sql-orm:defptype body () '(2sql-orm:text 128)) -(2sql-orm:defptype timestamp () '2sql-orm::ptimestamp-with-timezone) +(2sql-orm:defptype timestamp () '2sql-orm::ptimestamp-tz) (2sql-orm:defpclass blog-post () ((title :type title :initarg :title) @@ -515,24 +578,24 @@ delete => clear affected instances from cache (created :type timestamp :initarg :created))) (defun now () - (2sql-orm::make-ptimestamp-with-timezone - :date (2sql-orm::make-pdate :y 2011 :m 8 :d 13) - :time (2sql-orm::make-ptime :hh 17 :mm 4 :ss 0 :ms 0) - :timezone nil)) + (2sql-orm::make-ptimestamp-tz (2sql-orm::make-pdate 2011 8 13) + (2sql-orm::make-ptime 17 4 0 0) + nil)) (defun make-blog-post (title body) (2sql-orm:make-pinstance 'blog-post :title title :body body :created (now))) (2sql-backend:with-postgresql-connection ("pokus" "tomas" "test123" "localhost") (2sql-orm:with-pinstance-collector-cache () - (2sql-orm::with-psequences (oid-seq) - (2sql-orm::with-pclasses (blog-post) - (let ((p1 (make-blog-post "Hello blog world" "First post!")) - (p2 (make-blog-post "This is fun" "Common Lisp is easy!"))) - (2sql-orm:query () - `(q:select ((2sql-orm:instance blog-post x)) - (q:from (q:as blog-post x)) - (q:where (q:like x.title "%wor%"))))))))) + (2sql-orm:with-instance-cache () + (2sql-orm::with-psequences (oid-seq) + (2sql-orm::with-pclasses (blog-post) + (let ((p1 (make-blog-post "Hello blog world" "First post!")) + (p2 (make-blog-post "This is fun" "Common Lisp is easy!"))) + (2sql-orm:query () + `(q:select ((2sql-orm:instance blog-post x)) + (q:from (q:as blog-post x)) + (q:where (q:like x.title "%wor%")))))))))) #+nil (defun save-blog-post ()