dbquery

Query RDBMS and return S-expression
git clone https://logand.com/git/dbquery.git/
Log | Files | Refs | README

commit 94bd5f470d032edc611f095d11618732d5f6bfe1
parent 0f0992bce47f0af0af0ceccbca8f75ea8eb93d08
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun,  5 May 2013 21:18:27 +0200

server protocol implemented

Diffstat:
MMakefile | 4++--
Mcommon.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++--------
Mdbquery-pg.c | 122++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mdbquery.asd | 1+
Mdbquery.lisp | 327++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
5 files changed, 381 insertions(+), 130 deletions(-)

diff --git a/Makefile b/Makefile @@ -6,8 +6,8 @@ all: .c.o: $(CC) $(CFLAGS) -c -o $@ $< -dbquery-pg: dbquery-pg.c common.o - $(CC) $(CFLAGS) -o $@ $< common.o -lpq +dbquery-pg: dbquery-pg.c common.c + $(CC) $(CFLAGS) -o $@ $< -lpq strip $@ dbquery-mysql: dbquery-mysql.c common.o diff --git a/common.c b/common.c @@ -26,24 +26,65 @@ #include <stdlib.h> #include <stdio.h> -#define BLEN 4096 +#define HLEN 102400 -void query(char *q); +enum Command {ERROR, QUERY, PREPARE, EXECUTE, DEALLOCATE}; -void die(const char *format, ...) { +static void die(const char *format, ...) { va_list argv; va_start(argv, format); vfprintf(stderr, format, argv); va_end(argv); fprintf(stderr, "\n"); fflush(stderr); + fflush(stdout); exit(-1); } -void repl() { - for(; !feof(stdin);) { - char buf[BLEN]; - if(!fgets(buf, BLEN, stdin)) break; - query(buf); +static void pnum(int x) {printf("%d\n", x);} +static void pqstr(char *x) { + putchar('"'); + for(; *x; x++) { + switch(*x) { + case '\\': putchar('\\'); putchar('\\'); break; + case '"': putchar('\\'); putchar('"'); break; + default: putchar(*x); + } } + putchar('"'); + putchar('\n'); } +static void pnil() {pqstr("NIL");} + +static int rnum(void) { // TODO limit interval + int z = 0, y = 0; + for(;;) { + int c = getchar(); + if('0' <= c && c <= '9') {z = 10 * z + c - '0'; y = 1;} + else if('\n' == c && y) goto done; + else die("expected number"); + } + done: + return z; +} + +static char heap[HLEN]; +static char *buf; + +static char *rstr(void) { + char *z = buf; + if('"' != getchar()) die("expected string"); + for(;;) { + if(heap + HLEN <= buf) die("out of memory"); + int c = getchar(); + switch(c) { + case '\\': *buf++ = getchar(); break; + case '"': if('\n' != getchar()) die("expected eol"); *buf++ = 0; goto done; + default: *buf++ = c; + } + } + done: + return z; +} + +static int eof(void) {return EOF == ungetc(getchar(), stdin);} diff --git a/dbquery-pg.c b/dbquery-pg.c @@ -25,50 +25,88 @@ #include <stdio.h> #include <libpq-fe.h> -void die(const char *format, ...); -void repl(); +#include "common.c" -static PGconn *conn; - -void query(char *q) { - PGresult *z = PQexec(conn, q); - if(!z) die("error: query failed"); - int i, j, n = PQntuples(z), m = PQnfields(z); - printf("(("); - for(j = 0; j < m; j++) { - if(0 < j) printf(" "); - printf("\"%s\"", PQfname(z, j)); - } - printf(")\n ("); - for(j = 0; j < m; j++) { - if(0 < j) printf(" "); - printf("%d", PQftype(z, j)); - } - printf(")"); - for(i = 0; i < n; i++) { - printf("\n ("); - for(j = 0; j < m; j++) { - if(0 < j) printf(" "); - if(PQgetisnull(z, i, j)) - printf("NIL"); - else - switch(PQftype(z, j)) { - case 1700: // float - case 23: printf("%s", PQgetvalue(z, i, j)); break; // int - default: - printf("\"%s\"", PQgetvalue(z, i, j)); +int main(int argc, char *argv[]) { + if(argc != 2) die("usage: %s conninfo", argv[0]); + PGconn *conn = PQconnectdb(argv[1]); + if(!conn || CONNECTION_OK != PQstatus(conn)) die("error: connection failed"); + for(;;) { + if(eof()) return 0; + buf = heap; + int i, j, cmd = rnum(), nargs = rnum(); + PGresult *z; + { + Oid atype[nargs]; + char *aval[nargs]; + int alen[nargs], afmt[nargs]; + char *q, *stm; + switch(cmd) { + case QUERY: + q = rstr(); + for(i = 0; i < nargs; i++) { + atype[i] = rnum(); + aval[i] = rstr(); + alen[i] = 0; + afmt[i] = 0; + } + z = PQexecParams(conn, q, nargs, atype, (const char * const *) aval, alen, afmt, 0); + if(PGRES_TUPLES_OK != PQresultStatus(z)) goto fail; + goto rows; + case PREPARE: + stm = rstr(); + q = rstr(); + for(i = 0; i < nargs; i++) + atype[i] = rnum(); + z = PQprepare(conn, stm, q, nargs, atype); + if(PGRES_COMMAND_OK != PQresultStatus(z)) goto fail; + pnum(cmd); + goto done; + case EXECUTE: + stm = rstr(); + for(i = 0; i < nargs; i++) { + aval[i] = rstr(); + alen[i] = 0; + afmt[i] = 0; } + z = PQexecPrepared(conn, stm, nargs, (const char * const *) aval, alen, afmt, 0); + if(PGRES_TUPLES_OK != PQresultStatus(z)) goto fail; + goto rows; + default: die("unexpected command %d", cmd); + } } - printf(")"); + rows: + pnum(cmd); + int n = PQntuples(z), m = PQnfields(z); + pnum(n); + pnum(m); + for(j = 0; j < m; j++) { + pnum(PQftype(z, j)); + pqstr(PQfname(z, j)); + } + for(i = 0; i < n; i++) { + for(j = 0; j < m; j++) { + if(PQgetisnull(z, i, j)) + pnil(); + else + pqstr(PQgetvalue(z, i, j)); + /* switch(PQftype(z, j)) { */ + /* case 1700: // float */ + /* case 23: // int */ + /* pstr(PQgetvalue(z, i, j)); */ + /* break; */ + /* default: */ + /* pqstr(PQgetvalue(z, i, j)); */ + /* } */ + } + } + goto done; + fail: + pnum(ERROR); + pqstr(PQresultErrorField(z, PG_DIAG_SQLSTATE)); + pqstr(PQerrorMessage(conn)); + done: + fflush(stdout); + PQclear(z); } - printf(")\n"); - fflush(stdout); -} - -int main(int argc, char **argv) { - if(argc < 2) die("usage: %s conninfo [query]", argv[0]); - if(!(conn = PQconnectdb(argv[1])) || (PQstatus(conn) != CONNECTION_OK)) - die("error: connection failed"); - if(argc < 3) repl(); else query(argv[2]); - return 0; } diff --git a/dbquery.asd b/dbquery.asd @@ -32,5 +32,6 @@ :author "Tomas Hlavaty <tom@logand.com>" :maintainer "Tomas Hlavaty <tom@logand.com>" :licence "MIT" + :depends-on (#+sbcl :sb-concurrency) :serial t :components ((:file "dbquery"))) diff --git a/dbquery.lisp b/dbquery.lisp @@ -22,64 +22,93 @@ (defpackage :dbquery (:use :cl) - (:export :make-mysql-server + (:export :query + :prepare + :execute + :deallocate + :finish :make-pg-server + :make-mysql-server :make-sqlite-server)) (in-package :dbquery) -(defun call-with-program-output (command fn) - (let ((x (ccl:run-program (car command) (cdr command) :output :stream))) - (funcall fn (ccl:external-process-output-stream x)) - #+nil - (unwind-protect (funcall fn (ccl:external-process-output-stream x)) - (ccl:external-process-status )))) - -(defmacro with-program-output ((stream command) &body body) - `(call-with-program-output ,command (lambda (,stream) ,@body))) - -#+nil -(time - (with-program-output (s (list "./dbquery-sqlite" - "sqlite.db" - "select 123, 1, 0, null, 12.34, 'hello'")) - (read s))) - -#+nil -(time - (with-program-output (s (list "./dbquery-pg" - "dbname='pokus' user='tomas'" - "select 123, 1, 0, null, 12.34, 'hello'")) - (read s))) - -#+nil -(time - (with-program-output (s (list "./dbquery-mysql" - "localhost" - "tomas" - "Ri3OoL3h" - "pokus" - "select 123, 1, 0, null, 12.34, 'hello'" - )) - (read s))) +;; (defun call-with-program-output (command fn) +;; #+ccl +;; (let ((x (ccl:run-program (car command) (cdr command) :output :stream))) +;; (funcall fn (ccl:external-process-output-stream x)) +;; #+nil +;; (unwind-protect (funcall fn (ccl:external-process-output-stream x)) +;; (ccl:external-process-status ))) +;; #+ecl +;; (multiple-value-bind (io status proc) +;; (ext:run-program (car command) (cdr command) +;; :input nil +;; :output :stream +;; :error nil ;;t +;; :wait nil) +;; (when proc +;; (funcall fn io) +;; #+nil +;; (unwind-protect (funcall fn io) +;; (close io) +;; (ext:external-process-wait proc)))) +;; #-(or ccl ecl) +;; (error "DBQUERY::CALL-WITH-PROGRAM-OUTPUT not implemented")) + +;; (defmacro with-program-output ((stream command) &body body) +;; `(call-with-program-output ,command (lambda (,stream) ,@body))) + +;; ;;(with-program-output (s '("sha1sum" "/etc/passwd"))) +;; ;;(with-program-output (s '("sha1sum" "/etc/passwd")) (read-line s)) + +(defmacro with-lock ((lock) &body body) + #+ccl `(ccl:with-lock-grabbed (,lock) ,@body) + #+ecl `(mp:with-lock (,lock) ,@body) + #+sbcl `(sb-concurrency::with-mutex (,lock) ,@body) + #-(or ccl ecl sbcl) (error "DBQUERY::WITH-LOCK not implemented")) + +(defun make-lock (name) + #+ccl (ccl:make-lock name) + #+ecl (mp:make-lock :name name) + #+sbcl (sb-concurrency::make-mutex :name (string name)) + #-(or ccl ecl sbcl) (error "DBQUERY::MAKE-LOCK not implemented")) + +(defun make-semaphore () + #+ccl (ccl:make-semaphore) + #+ecl (mp:make-semaphore) + #+sbcl (sb-concurrency::make-semaphore) + #-(or ccl ecl sbcl) (error "DBQUERY::MAKE-SEMAPHORE not implemented")) + +(defun signal-semaphore (x) + #+ccl (ccl:signal-semaphore x) + #+ecl (mp:signal-semaphore x) + #+sbcl (sb-concurrency::signal-semaphore x) + #-(or ccl ecl sbcl) (error "DBQUERY::SIGNAL-SEMAPHORE not implemented")) + +(defun wait-on-semaphore (x) + #+ccl (ccl:wait-on-semaphore x) + #+ecl (mp:wait-on-semaphore x) + #+sbcl (sb-concurrency::wait-on-semaphore x) + #-(or ccl ecl sbcl) (error "DBQUERY::WAIT-ON-SEMAPHORE not implemented")) (defun make-concurrent-queue () (let ((x (cons nil nil)) - (l (ccl:make-lock 'concurrent-queue-lock)) - (s (ccl:make-semaphore))) + (l (make-lock 'concurrent-queue-lock)) + (s (make-semaphore))) (setf (car x) x) (lambda (&optional (value nil valuep)) (if valuep (let ((y (cons value nil))) - (ccl:with-lock-grabbed (l) + (with-lock (l) (setf (cdar x) y (car x) y) - (ccl:signal-semaphore s)) + (signal-semaphore s)) value) (do (done z) (done z) - (ccl:wait-on-semaphore s) - (ccl:with-lock-grabbed (l) + (wait-on-semaphore s) + (with-lock (l) (unless (eq x (car x)) (setq done t z (pop (cdr x))) @@ -92,53 +121,173 @@ ;; (funcall q 3) ;; (funcall q) -(defun make-program-server (command args writer reader) +(defun make-thread (name fn) + #+ccl (ccl:process-run-function name fn) + #+ecl (mp:process-run-function name fn) + #+sbcl (sb-concurrency::make-thread fn :name (string name)) + #-(or ccl ecl sbcl) (error "DBQUERY::MAKE-THREAD not implemented")) + +(defun make-program (command args) + #+ccl (let ((p (ccl:run-program command args :input :stream :output :stream + :error nil :sharing :external :wait nil))) (assert (eq :running (ccl:external-process-status p))) - (let ((wq (make-concurrent-queue))) - (ccl:process-run-function - 'program-server-writer - (let ((s (ccl:external-process-input-stream p))) - (lambda () - (do (x) - ((not (setq x (funcall wq))) - (close s)) - (funcall writer x s))))) - (let ((l (ccl:make-lock 'program-server-lock)) - (s (ccl:external-process-output-stream p))) - (lambda (query) - (ccl:with-lock-grabbed (l) - (when wq - (cond - (query - (funcall wq query) - (funcall reader s)) - (t (funcall wq nil) - (setq wq nil) - (ccl::external-process-wait p) - (multiple-value-bind (status code) (ccl:external-process-status p) - (assert (eq :exited status)) - (assert (zerop code)))))))))))) - -(defun dbquery-writer (value stream) - (write-line value stream) - (finish-output stream)) - -(defun dbquery-reader (stream) ;; TODO raise backend errors! - (let (*read-eval*) - (prog1 (read stream nil nil) - (assert (char= #\newline (read-char stream)))))) + (lambda (msg) + (ecase msg + (status-and-code (ccl:external-process-status p)) + (input-stream (ccl:external-process-input-stream p)) + (output-stream (ccl:external-process-output-stream p)) + (wait (ccl::external-process-wait p))))) + #+ecl + (multiple-value-bind (io x p) + (ext:run-program command + args + :input :stream + :output :stream + :error nil + :wait nil) + (declare (ignore x)) + (assert (eq :running (ext:external-process-status p))) + (lambda (msg) + (ecase msg + (status-and-code (ext:external-process-status p)) + (input-stream io) + (output-stream io) + (wait (ext:external-process-wait p))))) + #+sbcl + (let ((p (sb-ext:run-program command + args + :input :stream + :output :stream + :error nil + :wait nil))) + (assert (eq :running (sb-ext:process-status p))) + (lambda (msg) + (ecase msg + (status-and-code (sb-ext:process-status p)) + (input-stream (sb-ext:process-input p)) + (output-stream (sb-ext:process-output p)) + (wait (sb-ext:process-wait p))))) + #-(or ccl ecl sbcl) + (error "DBQUERY::MAKE-PROGRAM not implemented")) -(defun make-sqlite-server (command db) - (make-program-server command (list db) 'dbquery-writer 'dbquery-reader)) +(defun make-program-server (command args writer reader) + (let ((p (make-program command args)) + (wq (make-concurrent-queue))) + (make-thread 'program-server-writer + (let ((s (funcall p 'input-stream))) + (lambda () + (do (x) + ((not (setq x (funcall wq))) + (close s)) + (funcall writer x s))))) + (let ((l (make-lock 'program-server-lock)) + (s (funcall p 'output-stream))) + (lambda (query) + (with-lock (l) + (when wq + (cond + (query + (funcall wq query) + (funcall reader s)) + (t (funcall wq nil) + (setq wq nil) + (funcall p 'wait) + (multiple-value-bind (status code) (funcall p 'status-and-code) + (assert (eq :exited status)) + (assert (zerop code))))))))))) + +(defun query (server q &rest args) + (funcall server `(:query ,q ,@args))) + +(defun prepare (server stm q &rest args) + (funcall server `(:prepare ,stm ,q ,@args))) + +(defun execute (server stm &rest args) + (funcall server `(:execute ,stm ,@args))) + +(defun deallocate (server stm) + (funcall server `(:deallocate ,stm))) + +(defun finish (server) + (funcall server nil)) + +(defun rnum () + (do (done y (z 0)) + (done z) + (let ((c (read-char))) + (cond + ((char<= #\0 c #\9) + (setq y t + z (+ (* 10 z) (char-code c) #.(- (char-code #\0))))) + ((and (char= #\newline c) y) (setq done t)) + (t (error "expected number ~d ~s" z c)))))) + +(defun rstr () + (assert (char= #\" (read-char))) + (with-output-to-string (*standard-output*) + (do (done) + (done) + (let ((c (read-char))) + (case c + (#\\ (write-char (read-char))) + (#\" (assert (char= #\newline (read-char))) + (setq done t)) + (t (write-char c))))))) + +(defun dbquery-pg-writer (value *standard-output*) + (destructuring-bind (cmd &rest rest) value + (flet ((%query (q &rest args) + (format t "1~%~d~%~s~%" (length args) q) + (dolist (a args) + (etypecase a + (integer (format t "~s~%\"~s\"~%" 23 a)) + (string (format t "~s~%~s~%" 705 a)))))) + (ecase cmd + (:query (apply #'%query rest)) + (:prepare + (destructuring-bind (stm q &rest args) rest + (format t "2~%~d~%~s~%~s~%" (length args) stm q) + (dolist (a args) + (etypecase a + (integer (format t "~s~%" 23)) + (string (format t "~s~%" 705)))))) + (:execute + (destructuring-bind (stm &rest args) rest + (format t "3~%~d~%~s~%" (length args) stm) + (dolist (a args) + (etypecase a + (integer (format t "\"~s\"~%" a)) + (string (format t "~s~%" a)))))) + (:deallocate + (destructuring-bind (stm) rest + (%query (format nil "DEALLOCATE ~s" stm))))))) + (finish-output)) + +(defun dbquery-pg-reader (*standard-input*) + (ecase (rnum) + (0 (error "Database error ~s ~s" (rstr) (rstr))) + ((1 3) + (let ((n (rnum)) + (m (rnum))) + (when (and (plusp n) (plusp m)) + (cons (loop for i from 0 below m collect (cons (rnum) (rstr))) + (loop + for i from 0 below n + appending (loop for j from 0 below m collect (rstr))))))) + (2))) (defun make-pg-server (command connection-info) - (make-program-server command (list connection-info) 'dbquery-writer 'dbquery-reader)) + (make-program-server + command (list connection-info) 'dbquery-pg-writer 'dbquery-pg-reader)) + +(defun make-sqlite-server (command db) + (make-program-server command (list db) 'dbquery-writer 'dbquery-reader)) (defun make-mysql-server (command host user password db) (make-program-server command (list host user password db) 'dbquery-writer 'dbquery-reader)) @@ -161,3 +310,25 @@ ;; (time (funcall c "select 1, 2+3")) ;; (time (funcall c "select 4, 'hello'")) ;; (funcall c nil) + +;;; (() ...) first car param types, then query, then params + +;; in: nargs query [[type param]...] +;; out: ncol nrow [[[ctyp cname]...] [row...]] + +;; 0 "select 1, 2+3" => 2 1 91 91 "1" "2+3" "1" "5" +;; 2 "select 1, 2+3, $1, $2" 0 "hi" 0 "123" => 4 1 91 91 92 93 "1" "2+3" "$1" "$2" "1" "5" "hi" "123" + +;;(dbquery-pg-writer '(:query "select $1, 1, 2+3" 123) *standard-output*) +;;(dbquery-pg-writer '(:prepare "stm1" "select $1, 1, 2+3" 1234567890) *standard-output*) +;;(dbquery-pg-writer '(:execute "stm1" 1234567890) *standard-output*) +;;(dbquery-pg-writer '(:deallocate "stm1") *standard-output*) + +;;(defparameter c (make-pg-server "/home/tomas/git/dbquery/dbquery-pg" "dbname='pokus' user='tomas'")) +;;(query c "select 1, 2+3") +;;(query c "select 1, 2+3 from hi") +;;(query c "select $1, 1, 2+3" 1234567890) +;;(query c "select $1, 1, 2+3" 1234567890123456789) +;;(prepare c "stm1" "select $1, 1, 2+3" 1234567890) +;;(execute c "stm1" 890) +;;(deallocate c "stm1") <<<< TODO crashes