summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Bex <peter@more-magic.net>2018-06-11 21:39:55 +0200
committerPeter Bex <peter@more-magic.net>2018-06-11 21:56:43 +0200
commit385f6f23fee37617e0148babdb00783775d04a70 (patch)
tree6175f6364667fba196f83bcc328a38f626ee5be0
downloadchicken-postgresql-4.0.0.tar.gz
Initial port of PostgreSQL egg to CHICKEN 54.0.0
-rw-r--r--TODO6
-rw-r--r--benchmarks/run.scm127
-rwxr-xr-xbuild-postgresql23
-rw-r--r--feature-tests/connectdb-params.c7
-rw-r--r--feature-tests/diag-query-position.c7
-rw-r--r--feature-tests/diag-schema-info.c10
-rw-r--r--feature-tests/escape-identifier.c6
-rw-r--r--postgresql.egg18
-rw-r--r--postgresql.release-info3
-rw-r--r--postgresql.scm1360
-rw-r--r--tests/run.scm949
11 files changed, 2516 insertions, 0 deletions
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..afb9160
--- /dev/null
+++ b/TODO
@@ -0,0 +1,6 @@
+- Add Batch (pipeline) API support (http://blog.2ndquadrant.com/postgresql-latency-pipelining-batching/)
+- Add LISTEN/NOTIFY support
+- Add support for range types
+- Add large object support
+- Add namespace support for types?
+
diff --git a/benchmarks/run.scm b/benchmarks/run.scm
new file mode 100644
index 0000000..2994710
--- /dev/null
+++ b/benchmarks/run.scm
@@ -0,0 +1,127 @@
+(import postgresql (chicken string) (chicken format) srfi-13)
+
+(define conn (connect '((dbname . test))))
+(define raw-conn (connect '((dbname . test)) '() '()))
+
+(print "--- Connecting/disconnecting ---")
+(print "Full connection")
+(time (do ((i 0 (add1 i)))
+ ((= i 1000))
+ (disconnect (connect '((dbname . test))))))
+
+(print "Raw connection")
+(time (do ((i 0 (add1 i)))
+ ((= i 1000))
+ (disconnect (connect '((dbname . test)) '() '()))))
+
+(let ((str (make-string 1000 #\x)))
+ (newline)
+ (print "--- Quotation ---")
+ (print "Escaping many strings")
+ (time (do ((i 0 (add1 i)))
+ ((= i 100000))
+ (escape-string conn str)))
+
+ (print "Quoting many identifiers")
+ (time (do ((i 0 (add1 i)))
+ ((= i 100000))
+ (quote-identifier conn str)))
+
+ (print "Escaping many byteas")
+ (time (do ((i 0 (add1 i)))
+ ((= i 100000))
+ (escape-bytea conn str)))
+
+ (print "Unescaping many byteas")
+ (let ((bytea (escape-bytea conn str)))
+ (time (do ((i 0 (add1 i)))
+ ((= i 100000))
+ (unescape-bytea bytea)))))
+
+(begin (newline)
+ (print "--- Reading data ---")
+ (print "Raw query with no result processing (postgres/libpq benchmark)")
+ (time (query conn "SELECT generate_series(0, 100000)"))
+
+ (newline)
+ (print "Fetching all values, no type conversion (string)")
+ (time (row-for-each (lambda (values) #f)
+ (query raw-conn "SELECT generate_series(0, 1000000)::text")))
+ (print "Fetching all values, with type conversion (string)")
+ (time (row-for-each (lambda (values) #f)
+ (query conn "SELECT generate_series(0, 1000000)::text")))
+
+ (newline)
+ (print "Fetching all values, no type conversion (integer)")
+ (time (row-for-each (lambda (values) #f)
+ (query raw-conn "SELECT generate_series(0, 1000000)")))
+ (print "Fetching all values, with type conversion (integer)")
+ (time (row-for-each (lambda (values) #f)
+ (query conn "SELECT generate_series(0, 1000000)")))
+
+ (newline)
+ (print "COPY TO")
+ (time (copy-query-for-each (lambda (values) #f)
+ raw-conn "COPY (SELECT generate_series(0, 1000000)) TO STDOUT")))
+
+(begin (newline)
+ (print "--- Inserting data ---")
+ (query raw-conn "CREATE TEMPORARY TABLE foo (bar int)")
+ (print "INSERT in a loop, no type conversion")
+ (time (do ((i 0 (add1 i)))
+ ((= i 65535))
+ (query raw-conn "INSERT INTO foo VALUES ($1)" (->string i))))
+ (query raw-conn "TRUNCATE foo")
+
+ (query conn "CREATE TEMPORARY TABLE foo (bar int)")
+ (print "INSERT in a loop, with type conversion")
+ (time (do ((i 0 (add1 i)))
+ ((= i 65535))
+ (query conn "INSERT INTO foo VALUES ($1)" i)))
+ (query conn "TRUNCATE foo")
+
+ (newline)
+
+ (print "Parameterized INSERT statement, no type conversion")
+ (time (do ((i 0 (add1 i))
+ (values '() (cons (->string i) values))
+ (values-string '() (cons (sprintf "($~A)" (add1 i)) values-string)))
+ ((= i 65535)
+ (query* raw-conn (sprintf "INSERT INTO foo VALUES ~A"
+ (string-join values-string ","))
+ values))))
+ (query raw-conn "TRUNCATE foo")
+
+ (print "Parameterized INSERT statement, with type conversion")
+ (time (do ((i 0 (add1 i))
+ (values '() (cons i values))
+ (values-string '() (cons (sprintf "($~A)" (add1 i)) values-string)))
+ ((= i 65535)
+ (query* conn (sprintf "INSERT INTO foo VALUES ~A"
+ (string-join values-string ","))
+ values))))
+ (query conn "TRUNCATE foo")
+
+ (newline)
+
+ (print "Unparameterized INSERT statement")
+ (time (do ((i 0 (add1 i))
+ (values-string '() (cons (sprintf "(~A)" i) values-string)))
+ ((= i 65535)
+ (query raw-conn (sprintf "INSERT INTO foo VALUES ~A"
+ (string-join values-string ","))))))
+ (query raw-conn "TRUNCATE foo")
+
+ (newline)
+ (print "COPY FROM")
+ (time (call-with-output-copy-query
+ (lambda (p)
+ (do ((i 0 (add1 i)))
+ ((= i 65535))
+ (display i p)
+ (newline p)))
+ raw-conn "COPY foo (bar) FROM STDIN"))
+ (query raw-conn "TRUNCATE foo"))
+
+(disconnect raw-conn)
+(disconnect conn)
diff --git a/build-postgresql b/build-postgresql
new file mode 100755
index 0000000..fb15976
--- /dev/null
+++ b/build-postgresql
@@ -0,0 +1,23 @@
+# -*- sh -*-
+
+# Determine if pkg-config exists and knows about libpq, otherwise
+# fall back on pg_config to determine compiler and linker flags.
+if pkg-config --exists libpq >/dev/null 2>/dev/null; then
+ CFLAGS="`pkg-config --cflags libpq`"
+ LDFLAGS="`pkg-config --libs libpq`"
+else
+ CFLAGS="-I`pg_config --includedir`"
+ LDFLAGS="-L`pg_config --libdir` -Wl,-rpath `pg_config --libdir` -lpq"
+fi
+
+known_features="escape-identifier connectdb-params diag-query-position diag-schema-info"
+
+feature_flags=""
+for feature in $known_features; do
+ if "$CHICKEN_CSC" -C "$CFLAGS" -L "$LDFLAGS" "feature-tests/$feature.c" >/dev/null 2>/dev/null; then
+ feature_flags="$feature_flags -Dhas-$feature"
+ fi
+ rm -f feature-tests/$feature.o feature-tests/$feature
+done
+
+"$CHICKEN_CSC" $feature_flags -C "$CFLAGS" -L "$LDFLAGS" "$@"
diff --git a/feature-tests/connectdb-params.c b/feature-tests/connectdb-params.c
new file mode 100644
index 0000000..d79a2e0
--- /dev/null
+++ b/feature-tests/connectdb-params.c
@@ -0,0 +1,7 @@
+#include <libpq-fe.h>
+int main(void)
+{
+ const char *kw[] = {"a", NULL};
+ const char *val[] = {"b", NULL};
+ return PQconnectdbParams(kw, val, 0) != NULL;
+}
diff --git a/feature-tests/diag-query-position.c b/feature-tests/diag-query-position.c
new file mode 100644
index 0000000..8feb629
--- /dev/null
+++ b/feature-tests/diag-query-position.c
@@ -0,0 +1,7 @@
+#include <libpq-fe.h>
+int main(void)
+{
+ int foo = PG_DIAG_INTERNAL_QUERY;
+ int bar = PG_DIAG_INTERNAL_POSITION;
+ return foo + bar;
+}
diff --git a/feature-tests/diag-schema-info.c b/feature-tests/diag-schema-info.c
new file mode 100644
index 0000000..e64e990
--- /dev/null
+++ b/feature-tests/diag-schema-info.c
@@ -0,0 +1,10 @@
+#include <libpq-fe.h>
+int main(void)
+{
+ int s = PG_DIAG_SCHEMA_NAME;
+ int t = PG_DIAG_TABLE_NAME;
+ int col = PG_DIAG_COLUMN_NAME;
+ int d = PG_DIAG_DATATYPE_NAME;
+ int con = PG_DIAG_CONSTRAINT_NAME;
+ return s + t + col + d + con;
+}
diff --git a/feature-tests/escape-identifier.c b/feature-tests/escape-identifier.c
new file mode 100644
index 0000000..8188cbf
--- /dev/null
+++ b/feature-tests/escape-identifier.c
@@ -0,0 +1,6 @@
+#include <libpq-fe.h>
+int main(void)
+{
+ PGconn *conn = NULL; /* Bork */
+ return PQescapeIdentifier(conn, "test", 4) != NULL;
+}
diff --git a/postgresql.egg b/postgresql.egg
new file mode 100644
index 0000000..3848b3e
--- /dev/null
+++ b/postgresql.egg
@@ -0,0 +1,18 @@
+;;; postgresql.meta -*- Scheme -*-
+
+((synopsis "Bindings for PostgreSQL's C-api")
+ (category db)
+ (version 4.0.0)
+ (author "Johannes Groedem")
+ (maintainer "Peter Bex")
+ (license "BSD")
+ (dependencies sql-null srfi-1 srfi-18 srfi-13 srfi-69)
+ (test-dependencies test)
+ (foreign-dependencies libpq) ;; Or libpq-dev? Highly OS-dependent!
+ (components (extension postgresql
+ (custom-build "build-postgresql")
+ (source-dependencies
+ "feature-tests/connectdb-params.c"
+ "feature-tests/escape-identifier.c"
+ "feature-tests/diag-query-position.c"
+ "feature-tests/diag-schema-info.c"))))
diff --git a/postgresql.release-info b/postgresql.release-info
new file mode 100644
index 0000000..57d33b4
--- /dev/null
+++ b/postgresql.release-info
@@ -0,0 +1,3 @@
+(repo git "https://code.more-magic.net/chicken-postgresql")
+(uri targz "https://code.more-magic.net/chicken-postgresql/snapshot/chicken-postgresql-{egg-release}.tar.gz")
+(release "4.0.0")
diff --git a/postgresql.scm b/postgresql.scm
new file mode 100644
index 0000000..3f1baf4
--- /dev/null
+++ b/postgresql.scm
@@ -0,0 +1,1360 @@
+;;; Bindings to the PostgreSQL C library
+;;
+;; Copyright (C) 2008-2014 Peter Bex
+;; Copyright (C) 2004 Johannes Grødem <johs@copyleft.no>
+;; Redistribution and use in source and binary forms, with or without
+;; modification, is permitted.
+;;
+;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE
+;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+;; OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+;; USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+;; DAMAGE.
+
+(module postgresql
+ (type-parsers update-type-parsers! default-type-parsers
+ char-parser bool-parser bytea-parser numeric-parser
+ make-array-parser make-composite-parser
+ scheme-value->db-value type-unparsers update-type-unparsers!
+ default-type-unparsers bool-unparser vector-unparser list-unparser
+
+ connect reset-connection disconnect connection? connected?
+
+ query query* with-transaction in-transaction?
+
+ result? clear-result! row-count column-count
+ column-index column-name column-names column-format
+ column-type column-type-modifier table-oid table-column-index
+ value-at row-values row-alist column-values affected-rows inserted-oid
+
+ invalid-oid
+
+ escape-string escape-bytea unescape-bytea quote-identifier
+
+ put-copy-data put-copy-end get-copy-data
+
+ row-fold row-fold* row-fold-right row-fold-right*
+ row-for-each row-for-each* row-map row-map*
+ column-fold column-fold* column-fold-right column-fold-right*
+ column-for-each column-for-each* column-map column-map*
+ copy-query-fold copy-query*-fold copy-query-fold-right copy-query*-fold-right
+ copy-query-for-each copy-query*-for-each copy-query-map copy-query*-map
+ call-with-output-copy-query call-with-output-copy-query*
+ with-output-to-copy-query with-output-to-copy-query*)
+
+ (import scheme
+ (chicken base)
+ (chicken foreign)
+ (chicken string)
+ (chicken port)
+ (chicken memory)
+ (chicken condition)
+ (chicken format)
+ (chicken gc)
+ (chicken blob)
+ srfi-1
+ srfi-4
+ srfi-13
+ srfi-18
+ srfi-69
+ sql-null)
+
+(import-for-syntax (chicken string))
+
+(foreign-declare "#include <libpq-fe.h>")
+
+(define-foreign-type pg-polling-status (enum "PostgresPollingStatusType"))
+(define-foreign-variable PGRES_POLLING_FAILED pg-polling-status)
+(define-foreign-variable PGRES_POLLING_READING pg-polling-status)
+(define-foreign-variable PGRES_POLLING_WRITING pg-polling-status)
+(define-foreign-variable PGRES_POLLING_OK pg-polling-status)
+
+(define-foreign-type pg-exec-status (enum "ExecStatusType"))
+(define-foreign-variable PGRES_EMPTY_QUERY pg-exec-status)
+(define-foreign-variable PGRES_COMMAND_OK pg-exec-status)
+(define-foreign-variable PGRES_TUPLES_OK pg-exec-status)
+(define-foreign-variable PGRES_COPY_OUT pg-exec-status)
+(define-foreign-variable PGRES_COPY_IN pg-exec-status)
+(define-foreign-variable PGRES_BAD_RESPONSE pg-exec-status)
+(define-foreign-variable PGRES_NONFATAL_ERROR pg-exec-status)
+(define-foreign-variable PGRES_FATAL_ERROR pg-exec-status)
+
+(define-foreign-type pgconn* (c-pointer "PGconn"))
+
+(define PQconnectStart (foreign-lambda pgconn* PQconnectStart (const c-string)))
+(define PQconnectPoll (foreign-lambda pg-polling-status PQconnectPoll pgconn*))
+(define PQresetStart (foreign-lambda bool PQresetStart pgconn*))
+(define PQresetPoll (foreign-lambda pg-polling-status PQresetPoll pgconn*))
+(define PQfinish (foreign-lambda void PQfinish pgconn*))
+(define PQstatus (foreign-lambda (enum "ConnStatusType") PQstatus (const pgconn*)))
+(define PQerrorMessage (foreign-lambda c-string PQerrorMessage (const pgconn*)))
+
+;(define-foreign-type oid "Oid")
+(define-foreign-type oid unsigned-int)
+
+(define invalid-oid (foreign-value "InvalidOid" oid))
+
+(define PQisBusy (foreign-lambda bool PQisBusy pgconn*))
+(define PQconsumeInput (foreign-lambda bool PQconsumeInput pgconn*))
+
+(define-foreign-type pgresult* (c-pointer "PGresult"))
+
+(define PQgetResult (foreign-lambda pgresult* PQgetResult pgconn*))
+(define PQresultStatus (foreign-lambda pg-exec-status PQresultStatus (const pgresult*)))
+(define PQresultErrorMessage (foreign-lambda c-string PQresultErrorMessage (const pgresult*)))
+(define PQresultErrorField (foreign-lambda c-string PQresultErrorField (const pgresult*) int))
+
+(define PQclear (foreign-lambda void PQclear pgresult*))
+(define PQnfields (foreign-lambda int PQnfields (const pgresult*)))
+(define PQntuples (foreign-lambda int PQntuples (const pgresult*)))
+(define PQfname (foreign-lambda c-string PQfname (const pgresult*) int))
+(define PQfnumber (foreign-lambda int PQfnumber (const pgresult*) (const c-string)))
+(define PQftable (foreign-lambda oid PQftable (const pgresult*) int))
+(define PQftablecol (foreign-lambda int PQftablecol (const pgresult*) int))
+(define PQfformat (foreign-lambda int PQfformat (const pgresult*) int))
+(define PQftype (foreign-lambda oid PQftype (const pgresult*) int))
+(define PQfmod (foreign-lambda int PQfmod (const pgresult*) int))
+(define PQgetisnull (foreign-lambda bool PQgetisnull (const pgresult*) int int))
+(define PQgetlength (foreign-lambda int PQgetlength (const pgresult*) int int))
+(define PQgetvalue-ptr (foreign-lambda (c-pointer char) PQgetvalue (const pgresult*) int int))
+(define PQcmdTuples (foreign-lambda nonnull-c-string PQcmdTuples pgresult*))
+(define PQoidValue (foreign-lambda oid PQoidValue pgresult*))
+
+(define PQputCopyData (foreign-lambda int PQputCopyData pgconn* scheme-pointer int))
+(define PQputCopyEnd (foreign-lambda int PQputCopyEnd pgconn* (const c-string)))
+(define PQgetCopyData (foreign-lambda int PQgetCopyData pgconn* (c-pointer (c-pointer char)) bool))
+
+(define memcpy (foreign-lambda c-pointer "C_memcpy" scheme-pointer c-pointer size_t))
+
+(define (srfi-4-vector? x) ; Copied from ##sys#srfi-4-vector? from 4.8.3
+ (and (##core#inline "C_blockp" x)
+ (##sys#generic-structure? x)
+ (memq (##sys#slot x 0)
+ '(u8vector u16vector s8vector s16vector u32vector s32vector f32vector f64vector))))
+
+;; TODO: Create a real callback system?
+(foreign-declare "static void nullNoticeReceiver(void *arg, const PGresult *res){ }")
+
+(define-syntax define-foreign-int
+ (er-macro-transformer
+ (lambda (e r c)
+ ;; cannot rename define-foreign-variable; it's a really special form
+ `(define-foreign-variable ,(cadr e) int ,(conc "(int) " (cadr e))))))
+
+(define-syntax define-pgdiag-constants
+ (syntax-rules ()
+ ((_ (?condition ?constant0 ...) ?more ...)
+ (begin (cond-expand
+ (?condition (define-foreign-int ?constant0) ...)
+ (else (define ?constant0 #f) ...))
+ (define-pgdiag-constants ?more ...)))
+ ((_ ?constant0 ?more ...)
+ (begin (define-foreign-int ?constant0)
+ (define-pgdiag-constants ?more ...)))
+ ((_) (void))))
+
+(define-pgdiag-constants
+ PG_DIAG_SEVERITY PG_DIAG_SQLSTATE PG_DIAG_CONTEXT PG_DIAG_MESSAGE_PRIMARY
+ PG_DIAG_MESSAGE_DETAIL PG_DIAG_MESSAGE_HINT PG_DIAG_STATEMENT_POSITION
+ PG_DIAG_SOURCE_FILE PG_DIAG_SOURCE_LINE PG_DIAG_SOURCE_FUNCTION
+
+ (has-diag-query-position
+ PG_DIAG_INTERNAL_QUERY PG_DIAG_INTERNAL_POSITION)
+
+ (has-diag-schema-info
+ PG_DIAG_SCHEMA_NAME PG_DIAG_TABLE_NAME PG_DIAG_COLUMN_NAME
+ PG_DIAG_DATATYPE_NAME PG_DIAG_CONSTRAINT_NAME))
+
+;; Helper procedure for lists (TODO: use ANY instead of IN with an array?)
+(define (in-list len)
+ (string-intersperse
+ (list-tabulate len (lambda (p) (conc "$" (add1 p)))) ","))
+
+(define (postgresql-error subtype loc message . args)
+ (signal (apply make-pg-condition subtype loc message args)))
+
+(define (make-pg-condition subtype loc message . args)
+ (make-composite-condition
+ (make-property-condition
+ 'exn 'location loc 'message message 'arguments args)
+ (make-property-condition 'postgresql)
+ (if (condition? subtype) subtype (make-property-condition subtype))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;
+;;;; Type parsers
+;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (char-parser str) (string-ref str 0))
+
+(define (bool-parser str) (string=? str "t"))
+
+(define (numeric-parser str)
+ (or (string->number str)
+ (postgresql-error 'parse 'numeric-parser "Unable to parse number" str)))
+
+(define (bytea-parser str)
+ (unescape-bytea str))
+
+;; Here be dragons
+(define (make-array-parser element-parser #!optional (delim #\,))
+ (define (parse str)
+ (if (string-ci=? "NULL" str)
+ (sql-null)
+ (element-parser str)))
+ (lambda (str)
+ (let loop ((chars (let ignore-bounds ((chars (string->list str)))
+ (if (char=? (car chars) #\{)
+ chars
+ (ignore-bounds (cdr chars)))))
+ (result (list)))
+ (if (null? chars)
+ (car result) ; Should contain only one vector
+ (case (car chars)
+ ((#\{) (receive (value rest-chars)
+ (loop (cdr chars) (list))
+ (loop rest-chars (cons value result))))
+ ((#\}) (values (list->vector (reverse! result)) (cdr chars)))
+ ((#\") (let consume-string ((chars (cdr chars))
+ (consumed (list)))
+ (case (car chars)
+ ((#\\) (consume-string ; Don't interpret, just add it
+ (cddr chars) (cons (cadr chars) consumed)))
+ ((#\") (loop (cdr chars)
+ (cons (element-parser
+ (reverse-list->string consumed))
+ result)))
+ (else (consume-string (cdr chars)
+ (cons (car chars) consumed))))))
+ ((#\tab #\newline #\space) (loop (cdr chars) result))
+ (else
+ (if (char=? (car chars) delim)
+ (loop (cdr chars) result)
+ (let consume-string ((chars chars)
+ (consumed (list)))
+ (cond
+ ((char=? (car chars) delim)
+ (loop (cdr chars)
+ (cons (parse (reverse-list->string consumed))
+ result)))
+ ((or (char=? (car chars) #\})
+ (char=? (car chars) #\}))
+ (loop chars
+ (cons (parse (reverse-list->string consumed))
+ result)))
+ (else (consume-string (cdr chars)
+ (cons (car chars) consumed))))))))))))
+
+(define (make-composite-parser element-parsers)
+ (define (parse str element-parser)
+ (if (string=? "" str)
+ (sql-null)
+ (element-parser str)))
+ (lambda (str)
+ (let loop ((chars (cdr (string->list (string-trim str)))) ; skip leading (
+ (maybe-null? #t)
+ (result (list))
+ (parsers element-parsers))
+ (case (car chars)
+ ((#\)) (reverse! (if maybe-null?
+ (cons (sql-null) result)
+ result)))
+ ((#\") (let consume-string ((chars (cdr chars))
+ (consumed (list)))
+ (case (car chars)
+ ((#\\) (consume-string ; Don't interpret, just add it
+ (cddr chars) (cons (cadr chars) consumed)))
+ ((#\") (if (char=? #\" (cadr chars)) ; double escapes
+ (consume-string (cddr chars)
+ (cons #\" consumed))
+ (let skip-spaces ((chars (cdr chars)))
+ (case (car chars)
+ ((#\space #\newline #\tab)
+ (skip-spaces (cdr chars)))
+ ((#\,)
+ (loop (cdr chars)
+ #t
+ (cons ((car parsers)
+ (reverse-list->string consumed))
+ result)
+ (cdr parsers)))
+ ((#\)) (loop chars
+ #f
+ (cons ((car parsers)
+ (reverse-list->string consumed))
+ result)
+ (cdr parsers)))
+ (else
+ (postgresql-error
+ 'parse 'make-composite-parser
+ "Bogus trailing characters" str))))))
+ (else (consume-string (cdr chars)
+ (cons (car chars) consumed))))))
+ (else (let consume-string ((chars chars)
+ (consumed (list)))
+ (case (car chars)
+ ((#\,) (loop (cdr chars)
+ #t
+ (cons (parse (reverse-list->string consumed)
+ (car parsers))
+ result)
+ (cdr parsers)))
+ ;; Nothing should precede this one
+ ((#\)) (loop chars
+ #f
+ (cons (parse (reverse-list->string consumed)
+ (car parsers))
+ result)
+ (cdr parsers)))
+ (else (consume-string (cdr chars)
+ (cons (car chars) consumed))))))))))
+
+;; Array parsers and composite parsers are automatically cached when such
+;; a value is requested.
+(define default-type-parsers
+ (make-parameter
+ `(("text" . ,identity)
+ ("bytea" . ,bytea-parser)
+ ("char" . ,char-parser)
+ ("bpchar" . ,identity)
+ ("bool" . ,bool-parser)
+ ("int8" . ,numeric-parser)
+ ("int4" . ,numeric-parser)
+ ("int2" . ,numeric-parser)
+ ("float4" . ,numeric-parser)
+ ("float8" . ,numeric-parser)
+ ("numeric" . ,numeric-parser)
+ ("oid" . ,numeric-parser)
+ ;; Nasty hack, or clever hack? :)
+ ("record" . ,(make-composite-parser (circular-list identity))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;
+;;;; Type unparsers
+;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (scheme-value->db-value conn value)
+ (cond ((find (lambda (parse?)
+ ((car parse?) value))
+ (pg-connection-type-unparsers conn)) =>
+ (lambda (parse)
+ ((cdr parse) conn value)))
+ (else value)))
+
+(define (bool-unparser conn b)
+ (if b "TRUE" "FALSE"))
+
+(define (vector-unparser conn v)
+ (let loop ((result (list))
+ (pos 0)
+ (len (vector-length v)))
+ (if (= pos len)
+ (string-append "{" (string-intersperse (reverse! result) ",") "}")
+ (let* ((value (vector-ref v pos))
+ (unparsed-value (scheme-value->db-value conn value))
+ (serialized (cond
+ ((sql-null? unparsed-value) "NULL")
+ ((not (string? unparsed-value))
+ (postgresql-error
+ 'unparse 'vector-unparser
+ "Param value is not string" unparsed-value))
+ ((vector? value) unparsed-value) ;; don't quote!
+ (else
+ (sprintf "\"~A\""
+ (string-translate*
+ unparsed-value
+ '(("\\" . "\\\\") ("\"" . "\\\""))))))))
+ (loop (cons serialized result) (add1 pos) len)))))
+
+(define (list-unparser conn l)
+ (let loop ((result (list))
+ (items l))
+ (if (null? items)
+ (string-append "(" (string-intersperse (reverse! result) ",") ")")
+ (let* ((unparsed-value (scheme-value->db-value conn (car items)))
+ (serialized (cond
+ ((sql-null? unparsed-value) "")
+ ((not (string? unparsed-value))
+ (postgresql-error
+ 'unparse 'list-unparser
+ "Param value is not string" unparsed-value))
+ (else
+ (sprintf "\"~A\""
+ (string-translate*
+ unparsed-value
+ '(("\\" . "\\\\") ("\"" . "\\\""))))))))
+ (loop (cons serialized result) (cdr items))))))
+
+(define default-type-unparsers
+ (make-parameter
+ `((,string? . ,(lambda (conn s) s))
+ (,u8vector? . ,(lambda (conn v) (u8vector->blob/shared v)))
+ (,char? . ,(lambda (conn c) (string c)))
+ (,boolean? . ,bool-unparser)
+ (,number? . ,(lambda (conn n) (number->string n)))
+ (,vector? . ,vector-unparser)
+ (,pair? . ,list-unparser))))
+
+;; Retrieve type-oids from PostgreSQL:
+(define (update-type-parsers! conn #!optional new-type-parsers)
+ (let ((type-parsers (or new-type-parsers (pg-connection-type-parsers conn)))
+ (ht (make-hash-table))
+ (result '()))
+ (pg-connection-oid-parsers-set! conn ht)
+ (pg-connection-type-parsers-set! conn type-parsers)
+ (unless (null? type-parsers) ; empty IN () clause is not allowed
+ (row-for-each*
+ (lambda (oid typname)
+ (and-let* ((procedure (assoc typname type-parsers)))
+ (hash-table-set! ht (string->number oid) (cdr procedure))))
+ (query* conn
+ (sprintf
+ "SELECT oid, typname FROM pg_type WHERE typname IN (~A)"
+ (in-list (length type-parsers)))
+ (map car type-parsers) raw: #t)))))
+
+(define (update-type-unparsers! conn new-type-unparsers)
+ (pg-connection-type-unparsers-set! conn new-type-unparsers))
+
+;;;;;;;;;;;;;;;;;;;;
+;;;; Connections
+;;;;;;;;;;;;;;;;;;;;
+
+(define-record pg-connection
+ ptr type-parsers oid-parsers type-unparsers transaction-depth)
+(define connection? pg-connection?)
+(define type-parsers pg-connection-type-parsers)
+(define type-unparsers pg-connection-type-unparsers)
+
+(define (connected? conn) (not (not (pg-connection-ptr conn))))
+
+(define (pgsql-connection->fd conn)
+ ((foreign-lambda int PQsocket pgconn*) (pg-connection-ptr conn)))
+
+(define (wait-for-connection! conn poll-function)
+ (let ((conn-fd (pgsql-connection->fd conn))
+ (conn-ptr (pg-connection-ptr conn)))
+ (let loop ((result (poll-function conn-ptr)))
+ (cond ((= result PGRES_POLLING_OK) (void))
+ ((= result PGRES_POLLING_FAILED)
+ (let ((message (PQerrorMessage conn-ptr)))
+ (disconnect conn)
+ (postgresql-error
+ 'connect 'connect
+ (conc "Polling Postgres database failed: " message) conn)))
+ ((member result (list PGRES_POLLING_WRITING PGRES_POLLING_READING))
+ (thread-wait-for-i/o! conn-fd (if (= PGRES_POLLING_READING result)
+ #:input
+ #:output))
+ (loop (poll-function conn-ptr)))
+ (else
+ (postgresql-error
+ 'internal 'connect
+ (conc "Internal error! Unknown status code: " result) conn))))))
+
+(cond-expand
+ ((not has-connectdb-params)
+ (define (alist->connection-spec alist)
+ (string-join
+ (map (lambda (subspec)
+ (sprintf "~A='~A'"
+ (car subspec) ;; this had better not contain [ =\']
+ (string-translate* (->string (cdr subspec))
+ '(("\\" . "\\\\") ("'" . "\\'")))))
+ alist))))
+ (else))
+
+(define (connect-start spec)
+ (if (string? spec)
+ (PQconnectStart spec)
+ (cond-expand
+ (has-connectdb-params
+ (let ((len (length spec)))
+ ((foreign-lambda* pgconn* ((scheme-object cons) (scheme-pointer keybuf)
+ (scheme-pointer valbuf) (int len))
+ "const char **key = (const char **)keybuf;"
+ "const char **val = (const char **)valbuf;"
+ "int i;"
+ "for (i=0; i < len; ++i,cons=C_u_i_cdr(cons)) {"
+ " C_word kvpair = C_u_i_car(cons);"
+ " key[i] = C_c_string(C_u_i_car(kvpair));"
+ " val[i] = C_c_string(C_u_i_cdr(kvpair));"
+ "}"
+ "key[len] = NULL;"
+ "val[len] = NULL;"
+ "C_return(PQconnectStartParams(key, val, 0));")
+ (map (lambda (x) (cons (string-append (->string (car x)) "\x00")
+ (string-append (->string (cdr x)) "\x00"))) spec)
+ (make-blob (* (add1 len) (foreign-value "sizeof(char *)" int)))
+ (make-blob (* (add1 len) (foreign-value "sizeof(char *)" int)))
+ len)))
+ (else (PQconnectStart (alist->connection-spec spec))))))
+
+(define (connect #!optional (conn-spec '())
+ (type-parsers (default-type-parsers))
+ (type-unparsers (default-type-unparsers)))
+ (let ((conn-ptr (connect-start conn-spec)))
+ (cond
+ ((not conn-ptr)
+ (postgresql-error
+ 'internal 'connect
+ "Unable to allocate a Postgres connection structure" conn-spec))
+ ((= (foreign-value "CONNECTION_BAD" int) (PQstatus conn-ptr))
+ (let ((message (PQerrorMessage conn-ptr)))
+ (PQfinish conn-ptr)
+ (postgresql-error
+ 'connect 'connect
+ (conc "Connection to Postgres database failed: " message) conn-spec)))
+ (else
+ (let ((conn (make-pg-connection conn-ptr type-parsers
+ (make-hash-table) type-unparsers 0)))
+ ;; We don't want libpq to piss in our stderr stream
+ ((foreign-lambda* void ((pgconn* conn))
+ "PQsetNoticeReceiver(conn, nullNoticeReceiver, NULL);") conn-ptr)
+ (wait-for-connection! conn PQconnectPoll)
+ (set-finalizer! conn disconnect)
+ ;; Retrieve type-information from PostgreSQL metadata for use by
+ ;; the various value-parsers.
+ (update-type-parsers! conn)
+ conn)))))
+
+(define (reset-connection connection)
+ (let ((conn-ptr (pg-connection-ptr connection)))
+ (if (PQresetStart conn-ptr) ;; Update oid-parsers?
+ (wait-for-connection! connection PQresetPoll)
+ (let ((error-message (PQerrorMessage conn-ptr)))
+ (disconnect connection)
+ (postgresql-error
+ 'connect 'reset-connection
+ (conc "Reset of connection failed: " error-message) connection)))))
+
+(define (disconnect connection)
+ (and-let* ((conn-ptr (pg-connection-ptr connection)))
+ (pg-connection-ptr-set! connection #f)
+ (pg-connection-type-parsers-set! connection #f)
+ (pg-connection-oid-parsers-set! connection #f)
+ (PQfinish conn-ptr))
+ (void))
+
+;;;;;;;;;;;;;;;
+;;;; Results
+;;;;;;;;;;;;;;;
+
+(define-record pg-result ptr value-parsers)
+(define result? pg-result?)
+
+(define (clear-result! result)
+ (and-let* ((result-ptr (pg-result-ptr result)))
+ (pg-result-ptr-set! result #f)
+ (PQclear result-ptr)))
+
+(define (row-count result)
+ (PQntuples (pg-result-ptr result)))
+
+(define (column-count result)
+ (PQnfields (pg-result-ptr result)))
+
+;; Helper procedures for bounds checking; so we can distinguish between
+;; out of bounds and nonexistant columns, and signal it.
+(define (check-column-index! result index location)
+ (when (>= index (column-count result))
+ (postgresql-error
+ 'bounds location
+ (sprintf "Result column ~A out of bounds" index) result index)))
+
+(define (check-row-index! result index location)
+ (when (>= index (row-count result))
+ (postgresql-error
+ 'bounds location
+ (sprintf "Result row ~A out of bounds" index) result index)))
+
+(define (column-name result index)
+ (check-column-index! result index 'column-name)
+ (string->symbol (PQfname (pg-result-ptr result) index)))
+
+(define (column-names result)
+ (let ((ptr (pg-result-ptr result)))
+ (let loop ((names '())
+ (column (column-count result)))
+ (if (= column 0)
+ names
+ (loop (cons (string->symbol (PQfname ptr (sub1 column))) names)
+ (sub1 column))))))
+
+(define (column-index result name)
+ (let ((idx (PQfnumber (pg-result-ptr result) (symbol->string name))))
+ (and (>= idx 0) idx)))
+
+(define (table-oid result index)
+ (check-column-index! result index 'table-oid)
+ (let ((oid (PQftable (pg-result-ptr result) index)))
+ (and (not (= oid invalid-oid)) oid)))
+
+;; Fixes the off-by-1 unexpectedness in libpq/the protocol to make it more
+;; consistent with the rest of Scheme. However, this is inconsistent with
+;; almost all other PostgreSQL interfaces...
+(define (table-column-index result index)
+ (check-column-index! result index 'table-column-index)
+ (let ((idx (PQftablecol (pg-result-ptr result) index)))
+ (and (> idx 0) (sub1 idx))))
+
+(define format-table
+ '((0 . text) (1 . binary)))
+
+(define (format->symbol format)
+ (or (alist-ref format format-table eq?)
+ (postgresql-error 'type 'format->symbol "Unknown format" format)))
+
+(define (symbol->format symbol)
+ (or (and-let* ((res (rassoc symbol format-table eq?)))
+ (car res))
+ (postgresql-error 'type 'format->symbol "Unknown format" symbol)))
+
+(define (column-format result index)
+ (check-column-index! result index 'column-format)
+ (format->symbol (PQfformat (pg-result-ptr result) index)))
+
+(define (column-type result index)
+ (check-column-index! result index 'column-type)
+ (PQftype (pg-result-ptr result) index))
+
+;; This is really not super-useful as it requires intimate knowledge
+;; about the internal implementations of types in PostgreSQL.
+(define (column-type-modifier result index)
+ (check-column-index! result index 'column-type)
+ (let ((mod (PQfmod (pg-result-ptr result) index)))
+ (and (>= mod 0) mod)))
+
+;; Unchecked version, for speed
+(define (value-at* result column row raw)
+ (let ((ptr (pg-result-ptr result)))
+ (if (PQgetisnull ptr row column)
+ (sql-null)
+ (let* ((len (PQgetlength ptr row column))
+ (fmt (PQfformat ptr column))
+ (value (case fmt
+ ((0) (make-string len))
+ ((1) (make-blob len))
+ (else (postgresql-error
+ 'internal 'value-at
+ (conc "Unknown column format type: " fmt)
+ result column row raw)))))
+ (memcpy value (PQgetvalue-ptr ptr row column) len)
+ (if (or raw (blob? value))
+ value
+ ((vector-ref (pg-result-value-parsers result) column) value))))))
+
+(define (value-at result #!optional (column 0) (row 0) #!key raw)
+ (check-row-index! result row 'value)
+ (check-column-index! result column 'value)
+ (value-at* result column row raw))
+
+(define (row-values* result row column-count raw)
+ (let loop ((list '())
+ (column column-count))
+ (if (= column 0)
+ list
+ (loop (cons (value-at* result (sub1 column) row raw) list)
+ (sub1 column)))))
+
+(define (row-values result #!optional (row 0) #!key raw)
+ (check-row-index! result row 'row)
+ (row-values* result row (column-count result) raw))
+
+(define (column-values* result column row-count raw)
+ (let loop ((list '())
+ (row row-count))
+ (if (= row 0)
+ list
+ (loop (cons (value-at* result column (sub1 row) raw) list)
+ (sub1 row)))))
+
+(define (column-values result #!optional (column 0) #!key raw)
+ (check-column-index! result column 'column)
+ (column-values* result column (row-count result) raw))
+
+;; (define (row-alist result #!optional (row 0) #!key raw)
+;; (map cons (column-names result) (row-values result row raw: raw)))
+(define (row-alist result #!optional (row 0) #!key raw)
+ (check-row-index! result row 'row-alist)
+ (let loop ((alist '())
+ (column (column-count result)))
+ (if (= column 0)
+ alist
+ (loop (cons (cons (string->symbol
+ (PQfname (pg-result-ptr result) (sub1 column)))
+ (value-at* result (sub1 column) row raw)) alist)
+ (sub1 column)))))
+
+;;; TODO: Do we want/need PQnparams and PQparamtype bindings?
+
+(define (affected-rows result)
+ (string->number (PQcmdTuples (pg-result-ptr result))))
+
+(define (inserted-oid result)
+ (let ((oid (PQoidValue (pg-result-ptr result))))
+ (and (not (= oid invalid-oid)) oid)))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;
+;;;; Query procedures
+;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Buffer all available input, yielding if nothing is available:
+(define (buffer-available-input! conn)
+ (let ((conn-ptr (pg-connection-ptr conn))
+ (conn-fd (pgsql-connection->fd conn)))
+ (let loop ()
+ (if (PQconsumeInput conn-ptr)
+ (when (PQisBusy conn-ptr)
+ (thread-wait-for-i/o! conn-fd #:input)
+ (loop))
+ (postgresql-error
+ 'i/o 'buffer-available-input!
+ (conc "Error reading reply from server: " (PQerrorMessage conn-ptr))
+ conn)))))
+
+;; Here be more dragons
+(define (resolve-unknown-types! conn oids)
+ (unless (null? oids)
+ (let* ((parsers (pg-connection-oid-parsers conn))
+ (q (conc "SELECT t.oid, t.typtype, t.typelem, t.typdelim, "
+ " t.typbasetype, t.typarray, a.attrelid, a.atttypid "
+ "FROM pg_type t "
+ " LEFT JOIN pg_attribute a "
+ " ON t.typrelid = a.attrelid AND a.attnum > 0 "
+ "WHERE t.oid IN (~A) "
+ "ORDER BY COALESCE(t.typrelid,-1) ASC, a.attnum ASC"))
+ (result (query* conn (sprintf q (in-list (length oids)))
+ (map number->string oids) raw: #t))
+ (count (row-count result)))
+ (let dissect-types ((unknown-oids (list))
+ (pos 0)
+ (domains (list))
+ (arrays (list))
+ (classes (list))
+ (last-class 0))
+ (cond
+ ((>= pos count) ; Done scanning rows?
+ ;; Keep going until all oids are resolved
+ (resolve-unknown-types! conn unknown-oids)
+ ;; Postprocessing step: resolve all nested types
+ (for-each (lambda (d)
+ (and-let* ((p (hash-table-ref/default parsers (cdr d) #f)))
+ (hash-table-set! parsers (car d) p)))
+ domains)
+ (for-each (lambda (a)
+ (and-let* ((p (hash-table-ref/default parsers (cddr a) #f)))
+ (hash-table-set! parsers (car a)
+ (make-array-parser p (cadr a)))))
+ arrays)
+ (for-each
+ (lambda (c)
+ (and-let* ((p-list
+ (fold
+ (lambda (att l)
+ (and-let* ((l)
+ (p (hash-table-ref/default parsers att #f)))
+ (cons p l)))
+ '()
+ (cdr c))))
+ (hash-table-set! parsers (car c)
+ (make-composite-parser p-list))))
+ classes))
+ ((not (string=? (value-at* result 4 pos #f) "0")) ; Domain type?
+ (let* ((basetype-oid (string->number (value-at* result 4 pos #f)))
+ (parser (hash-table-ref/default parsers basetype-oid #f))
+ (oid (string->number (value-at* result 0 pos #f))))
+ (dissect-types (if parser
+ unknown-oids
+ (cons basetype-oid unknown-oids))
+ (add1 pos) (cons (cons oid basetype-oid) domains)
+ arrays classes last-class)))
+ ((string=? (value-at* result 5 pos #f) "0") ; Array value?
+ (let* ((elem (string->number (value-at* result 2 pos #f)))
+ (delim (string-ref (value-at* result 3 pos #f) 0))
+ (parser (hash-table-ref/default parsers elem #f))
+ (oid (string->number (value-at* result 0 pos #f))))
+ (dissect-types (if parser
+ unknown-oids
+ (cons elem unknown-oids))
+ (add1 pos) domains
+ (cons (cons oid (cons delim elem)) arrays)
+ classes last-class)))
+ ((string=? (value-at* result 1 pos #f) "c") ; Class? (i.e., table or type)
+ (let* ((classid (string->number (value-at* result 6 pos #f)))
+ (attrid (string->number (value-at* result 7 pos #f)))
+ (parser (hash-table-ref/default parsers attrid #f)))
+ (dissect-types (if parser
+ unknown-oids
+ (cons attrid unknown-oids))
+ (add1 pos) domains arrays
+ ;; Keep oid at the front of the list, insert this
+ ;; attr after it, before the other attrs, if any.
+ (if (= last-class classid)
+ (cons (cons (caar classes)
+ (cons attrid (cdar classes)))
+ (cdr classes))
+ (cons (cons (string->number
+ (value-at* result 0 pos #f))
+ (list attrid)) classes))
+ classid)))
+ (else
+ (dissect-types unknown-oids (add1 pos)
+ domains arrays classes last-class)))))))
+
+(define (make-value-parsers conn pqresult #!key raw)
+ (let* ((nfields (PQnfields pqresult))
+ (parsers (make-vector nfields))
+ (ht (pg-connection-oid-parsers conn)))
+ (let loop ((col 0)
+ (unknowns (list)))
+ (if (= col nfields)
+ (begin
+ (resolve-unknown-types! conn (map cdr unknowns))
+ (for-each (lambda (unknown)
+ (let* ((col (car unknown))
+ (oid (cdr unknown))
+ (parser (hash-table-ref/default ht oid identity)))
+ (vector-set! parsers col parser)))
+ unknowns)
+ parsers)
+ (let* ((oid (PQftype pqresult col))
+ (parser (if raw identity (hash-table-ref/default ht oid #f))))
+ (vector-set! parsers col parser)
+ (loop (add1 col) (if parser
+ unknowns
+ (cons (cons col oid) unknowns))))))))
+
+;; Collect the result pointers from the last query.
+;;
+;; A pgresult represents an entire resultset and is always read into memory
+;; all at once.
+(define (get-last-result conn #!key raw)
+ (buffer-available-input! conn)
+ (let* ((conn-ptr (pg-connection-ptr conn))
+ ;; Read out all remaining results (including the current one).
+ ;; TODO: Is this really needed? libpq does it (in pqExecFinish),
+ ;; but ostensibly only to concatenate the error messages for
+ ;; each query. OTOH, maybe we want to do that, too.
+ (clean-results! (lambda (result)
+ (let loop ((result result))
+ (when result
+ (PQclear result)
+ (loop (PQgetResult conn-ptr))))))
+ (result (PQgetResult conn-ptr))
+ (status (PQresultStatus result)))
+ (cond
+ ((not result) (postgresql-error
+ 'internal 'get-last-result
+ "Internal error! No result object available from server"
+ conn))
+ ((member status (list PGRES_BAD_RESPONSE PGRES_FATAL_ERROR
+ PGRES_NONFATAL_ERROR))
+ (let* ((error-field (lambda (f) (and f (PQresultErrorField result f))))
+ (error-field/int (lambda (f)
+ (and-let* ((value (error-field f)))
+ (string->number value))))
+ (sqlstate (error-field PG_DIAG_SQLSTATE))
+ (maybe-severity (error-field PG_DIAG_SEVERITY))
+ (condition
+ (make-pg-condition
+ (make-property-condition
+ 'query
+ 'severity (and maybe-severity
+ (string->symbol
+ (string-downcase maybe-severity)))
+ 'error-class (and sqlstate (string-take sqlstate 2))
+ 'error-code sqlstate
+ 'message-primary (error-field PG_DIAG_MESSAGE_PRIMARY)
+ 'message-detail (error-field PG_DIAG_MESSAGE_DETAIL)
+ 'message-hint (error-field PG_DIAG_MESSAGE_HINT)
+ 'statement-position (error-field/int PG_DIAG_STATEMENT_POSITION)
+ 'context (error-field PG_DIAG_CONTEXT)
+ 'source-file (error-field PG_DIAG_SOURCE_FILE)
+ 'source-line (error-field/int PG_DIAG_SOURCE_LINE)
+ 'source-function (error-field PG_DIAG_SOURCE_FUNCTION)
+ 'internal-query (error-field PG_DIAG_INTERNAL_QUERY)
+ 'internal-position (error-field/int PG_DIAG_INTERNAL_POSITION)
+ 'schema-name (error-field PG_DIAG_SCHEMA_NAME)
+ 'table-name (error-field PG_DIAG_TABLE_NAME)
+ 'column-name (error-field PG_DIAG_COLUMN_NAME)
+ 'datatype-name (error-field PG_DIAG_DATATYPE_NAME)
+ 'constraint-name (error-field PG_DIAG_CONSTRAINT_NAME))
+ 'get-last-result
+ (PQresultErrorMessage result))))
+ (clean-results! result)
+ (signal condition)))
+ ((member status (list PGRES_COPY_OUT PGRES_COPY_IN))
+ ;; These are weird; A COPY puts the connection in "copy mode".
+ ;; As long as it's in "copy mode", pqgetresult will return the
+ ;; same result every time you call it, so don't try to call
+ ;; clean-results!
+ (let ((result-obj (make-pg-result result #f)))
+ (set-finalizer! result-obj clear-result!)
+ result-obj))
+ ((member status (list PGRES_EMPTY_QUERY PGRES_COMMAND_OK PGRES_TUPLES_OK))
+ (let ((result-obj (make-pg-result result #f)))
+ (set-finalizer! result-obj clear-result!)
+ (let ((trailing-result (PQgetResult conn-ptr)))
+ (when trailing-result
+ (clean-results! trailing-result)
+ (postgresql-error 'internal 'get-last-result
+ (conc "Internal error! Unexpected extra "
+ "results after first query result")
+ conn)))
+ (pg-result-value-parsers-set!
+ result-obj (make-value-parsers conn result raw: raw))
+ result-obj))
+ (else
+ (postgresql-error 'internal 'get-last-result
+ (conc "Internal error! Unknown status code: " status)
+ conn)))))
+
+(define (query conn query . params)
+ (query* conn query params))
+
+(define (query* conn query #!optional (params '()) #!key (format 'text) raw)
+ (let* ((params ;; Check all params and ensure they are proper pairs
+ (map (lambda (p)
+ (let ((obj (if raw p (scheme-value->db-value conn p))))
+ (cond ((string? obj) ; Convert to ASCIIZ
+ (let* ((len (##sys#size obj))
+ (res (string-append obj "\x00")))
+ (vector 0 len res)))
+ ((blob? obj)
+ (vector 1 (##sys#size obj) obj))
+ ((sql-null? obj) #f)
+ (else (postgresql-error
+ 'type 'query*
+ (sprintf "Param value is not string, sql-null or blob: ~S" p)
+ conn query params format)))))
+ params))
+ ;; It's a shame we need to traverse params twice (and again in C)...
+ (len (length params))
+ (send-query
+ (foreign-lambda*
+ bool ((pgconn* conn) (nonnull-c-string query)
+ (bool is_prepped) (int num) (scheme-object params)
+ (scheme-pointer valsbuf) (scheme-pointer lensbuf)
+ (scheme-pointer fmtsbuf) (int rfmt))
+ "int i = 0, *lens = (int *)lensbuf, *fmts = (int *)fmtsbuf;"
+ "const char **vals = (const char **)valsbuf;"
+ "C_word obj, cons;"
+ "for (i=0,cons=params; i < num; ++i,cons=C_u_i_cdr(cons)) {"
+ " obj = C_u_i_car(cons);"
+ " if (obj == C_SCHEME_FALSE) {"
+ " fmts[i] = lens[i] = 0;"
+ " vals[i] = NULL;"
+ " } else {"
+ " fmts[i] = C_unfix(C_block_item(obj, 0));"
+ " lens[i] = C_unfix(C_block_item(obj, 1));"
+ " vals[i] = C_c_string(C_block_item(obj, 2));"
+ " }"
+ "}"
+ "if (is_prepped)"
+ " C_return(PQsendQueryPrepared((PGconn *)conn, query, num,"
+ " vals, lens, fmts, rfmt));"
+ "else"
+ " C_return(PQsendQueryParams((PGconn *)conn, query, num, NULL,"
+ " vals, lens, fmts, rfmt));"))
+ (query-as-string (if (symbol? query) (symbol->string query) query)))
+ ;; XXX: What if we're using a newer protocol version? Then this error is
+ ;; well-meaning but completely wrong... Unfortunately the error message
+ ;; returned by the server if we exceed this limit is even more confusing.
+ (cond ((> len 65535)
+ (postgresql-error
+ 'domain 'query*
+ (sprintf "Too many bind parameters (PG protocol supports up to 65535, but got ~A). Try using the COPY support, or escaping the data and sending it as a big string." len)
+ conn query params format))
+ ((send-query (pg-connection-ptr conn) query-as-string (symbol? query)
+ len params
+ ;; We allocate here instead of in C to keep things simple
+ (make-blob (* len (foreign-value "sizeof(char *)" int)))
+ (make-blob (* len (foreign-value "sizeof(int)" int)))
+ (make-blob (* len (foreign-value "sizeof(int)" int)))
+ (symbol->format format))
+ (get-last-result conn raw: raw))
+ (else (postgresql-error 'i/o 'query*
+ (conc "Unable to send query to server: "
+ (PQerrorMessage (pg-connection-ptr conn)))
+ conn query params format)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;; Transaction management
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (with-transaction conn thunk #!key isolation access)
+ (let* ((old-depth (pg-connection-transaction-depth conn))
+ (isolation (and isolation
+ (case isolation
+ ((read-committed) "ISOLATION LEVEL READ COMMITTED")
+ ((serializable) "ISOLATION LEVEL SERIALIZABLE")
+ (else (postgresql-error
+ 'type 'with-transaction
+ "Unknown isolation level" isolation)))))
+ (access (and access
+ (case access
+ ((read/write) "READ WRITE")
+ ((read-only) "READ ONLY")
+ (else (postgresql-error
+ 'type 'with-transaction
+ "Unknown access mode" access)))))
+ (characteristics (conc (or isolation "") " " (or access "")))
+ (rollback!
+ (lambda ()
+ (if (zero? old-depth)
+ (query conn "ROLLBACK")
+ ;; We do not *need* to give savepoints unique names,
+ ;; but it aids debugging and we know the depth anyway.
+ (query conn (conc "ROLLBACK TO SAVEPOINT s_" old-depth)))))
+ (commit!
+ (lambda ()
+ (if (zero? old-depth)
+ (query conn "COMMIT")
+ (query conn (conc "RELEASE SAVEPOINT s_" old-depth))))))
+ (when (and isolation (not (zero? old-depth)))
+ (postgresql-error
+ 'domain 'with-transaction
+ "Can't set isolation level in nested transactions" isolation))
+ (if (zero? old-depth)
+ (query conn (conc "BEGIN " characteristics))
+ (begin (query conn (conc "SAVEPOINT s_" old-depth))
+ ;; XXX: This should probably be SET LOCAL instead of SET
+ ;; (which is implicitly the same as SET SESSION), but I
+ ;; can't come up with a testcase that fails with this and
+ ;; succeeds with SET LOCAL, so keep it around for now.
+ (when access
+ (query conn (conc "SET TRANSACTION " characteristics)))))
+ (pg-connection-transaction-depth-set! conn (add1 old-depth))
+ ;; TODO: Add a warning mechanism (using dynamic-wind) for when the
+ ;; user tries to jump into/out of transactions with continuations?
+ (handle-exceptions exn
+ (begin
+ (pg-connection-transaction-depth-set! conn old-depth)
+ (rollback!)
+ (raise exn))
+ (let ((res (thunk)))
+ (pg-connection-transaction-depth-set! conn old-depth)
+ (if res (commit!) (rollback!))
+ res))))
+
+(define (in-transaction? conn)
+ (> (pg-connection-transaction-depth conn) 0))
+
+;;;;;;;;;;;;;;;;;;;;
+;;;; COPY support
+;;;;;;;;;;;;;;;;;;;;
+
+(define (put-copy-data conn data)
+ (let* ((data (cond
+ ((or (string? data) (blob? data)) data)
+ ((srfi-4-vector? data) (##sys#slot data 1))
+ (else (postgresql-error
+ 'type 'put-copy-data
+ "Expected a blob, string or srfi-4 vector" conn data))))
+ (len (##sys#size data))
+ (conn-ptr (pg-connection-ptr conn))
+ (conn-fd (pgsql-connection->fd conn)))
+ (let loop ((res (PQputCopyData conn-ptr data len)))
+ (cond
+ ((= res 0)
+ (thread-wait-for-i/o! conn-fd #:output)
+ (loop (PQputCopyData conn-ptr data len)))
+ ((= res 1) (void))
+ ((= res -1)
+ (postgresql-error
+ 'i/o 'put-copy-data
+ (conc "Error putting COPY data: " (PQerrorMessage conn-ptr)) conn))
+ (else (postgresql-error
+ 'internal 'put-copy-data
+ (conc "Internal error! Unexpected return value: " res) conn))))))
+
+(define (put-copy-end conn #!optional error-message)
+ (let ((conn-ptr (pg-connection-ptr conn))
+ (conn-fd (pgsql-connection->fd conn)))
+ (let loop ((res (PQputCopyEnd conn-ptr error-message)))
+ (cond
+ ((= res 0)
+ (thread-wait-for-i/o! conn-fd #:output)
+ (loop (PQputCopyEnd conn-ptr error-message)))
+ ((= res 1) (get-last-result conn))
+ ((= res -1)
+ (postgresql-error
+ 'i/o 'put-copy-end
+ (conc "Error ending put COPY data: " (PQerrorMessage conn-ptr))
+ conn error-message))
+ (else
+ (postgresql-error
+ 'internal 'put-copy-end
+ (conc "Internal error! Unexpected return value: " res) conn))))))
+
+(define (get-copy-data conn #!key (format 'text))
+ (let ((conn-ptr (pg-connection-ptr conn)))
+ (let loop ()
+ (let-location ((buf c-pointer))
+ (let ((res (PQgetCopyData conn-ptr (location buf) #t)))
+ (cond
+ ((> res 0)
+ (let ((value (case format
+ ((text) (make-string res))
+ ((binary) (make-blob res))
+ (else (postgresql-error
+ 'internal 'get-copy-data
+ (conc "Unknown column format type: " format)
+ conn)))))
+ (memcpy value buf res)
+ (free buf)
+ value))
+ ((= res 0)
+ (buffer-available-input! conn)
+ (loop))
+ ((= res -1)
+ (get-last-result conn))
+ ((= res -2)
+ (postgresql-error
+ 'i/o 'get-copy-data
+ (conc "Error getting COPY data: " (PQerrorMessage conn-ptr)) conn))
+ (else (postgresql-error
+ 'internal 'get-copy-data
+ (conc "Internal error! Unexpected return value: " res)
+ conn))))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;; Value escaping and quotation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (escape-string conn str)
+ (define %escape-string-conn
+ (foreign-lambda size_t PQescapeStringConn
+ pgconn* scheme-pointer scheme-pointer size_t (c-pointer bool)))
+ (let-location ((err bool))
+ (let* ((strlen (string-length str))
+ (buflen (add1 (* strlen 2)))
+ (buffer (make-string buflen))
+ (conn-ptr (pg-connection-ptr conn))
+ (size (%escape-string-conn conn-ptr buffer str strlen (location err))))
+ (cond (err (postgresql-error 'internal 'escape-string
+ (conc "String escaping failed. "
+ (PQerrorMessage conn-ptr)) conn str))
+ ((= size buflen) buffer)
+ (else (##sys#substring buffer 0 size))))))
+
+(define (quote-identifier conn str)
+ (cond-expand
+ (has-escape-identifier
+ (define %escape-ident
+ (foreign-lambda c-string* PQescapeIdentifier pgconn* scheme-pointer size_t))
+ (or (%escape-ident (pg-connection-ptr conn) str (string-length str))
+ (postgresql-error 'internal 'quote-identifier
+ (conc "Identifier escaping failed: "
+ (PQerrorMessage (pg-connection-ptr conn)))
+ conn str)))
+ (else (postgresql-error 'unsupported-version 'quote-identifier
+ (conc "Please upgrade your PostgreSQL to 9.0 or later "
+ "in order to be able to use quote-identifier!")
+ conn str))))
+
+(define (escape-bytea conn obj)
+ (define %escape-bytea-conn
+ (foreign-lambda (c-pointer unsigned-char) PQescapeByteaConn
+ pgconn* scheme-pointer size_t (c-pointer size_t)))
+ (let-location ((allocated size_t))
+ (let* ((data (cond ((or (string? obj) (blob? obj)) obj)
+ ((srfi-4-vector? obj) (##sys#slot obj 1))
+ (else (postgresql-error
+ 'type 'escape-bytea
+ "Expected string, blob or srfi-4 vector" obj))))
+ (conn-ptr (pg-connection-ptr conn))
+ (buf (%escape-bytea-conn
+ conn-ptr data (##sys#size data) (location allocated))))
+ (if buf
+ (let* ((string-length (sub1 allocated))
+ (result-string (make-string string-length)))
+ (memcpy result-string buf string-length)
+ (free buf)
+ result-string)
+ (postgresql-error
+ 'internal 'escape-bytea
+ (conc "Byte array escaping failed: " (PQerrorMessage conn-ptr))
+ conn obj)))))
+
+(define (unescape-bytea str)
+ (define %unescape-bytea
+ (foreign-lambda (c-pointer unsigned-char) PQunescapeBytea c-string (c-pointer size_t)))
+ (let-location ((blob-length size_t))
+ (let ((buf (%unescape-bytea str (location blob-length))))
+ (if buf
+ (let ((result-blob (make-blob blob-length)))
+ (memcpy result-blob buf blob-length)
+ (free buf)
+ (blob->u8vector/shared result-blob))
+ (postgresql-error 'internal 'unescape-bytea
+ "Byte array unescaping failed (out of memory?)" str)))))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;; High-level interface
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (make-result-fold item-count sub-item-count extract-item)
+ (lambda (kons knil result)
+ (let ((items (item-count result))
+ (sub-items (sub-item-count result)))
+ (let loop ((seed knil)
+ (item 0))
+ (if (= item items)
+ seed
+ (loop (kons (extract-item result item sub-items #f) seed) (add1 item)))))))
+
+(define row-fold (make-result-fold row-count column-count row-values*))
+(define (row-fold* kons knil result)
+ (row-fold (lambda (values seed)
+ (apply kons (append values (list seed)))) knil result))
+
+(define column-fold (make-result-fold column-count row-count column-values*))
+(define (column-fold* kons knil result)
+ (column-fold (lambda (values seed)
+ (apply kons (append values (list seed)))) knil result))
+
+
+(define (make-result-fold-right item-count sub-item-count extract-item)
+ (lambda (kons knil result)
+ (let ((sub-items (sub-item-count result)))
+ (let loop ((seed knil)
+ (item (item-count result)))
+ (if (= item 0)
+ seed
+ (loop (kons (extract-item result (sub1 item) sub-items #f) seed) (sub1 item)))))))
+
+(define row-fold-right (make-result-fold-right row-count column-count row-values*))
+(define (row-fold-right* kons knil result)
+ (row-fold-right (lambda (values seed)
+ (apply kons (append values (list seed)))) knil result))
+
+(define column-fold-right (make-result-fold-right column-count row-count column-values*))
+(define (column-fold-right* kons knil result)
+ (column-fold-right (lambda (values seed)
+ (apply kons (append values (list seed)))) knil result))
+
+
+(define (row-for-each proc result)
+ (row-fold (lambda (values seed) (proc values)) #f result)
+ (void))
+(define (row-for-each* proc result)
+ (row-fold (lambda (values seed) (apply proc values)) #f result)
+ (void))
+
+(define (column-for-each proc result)
+ (column-fold (lambda (values seed) (proc values)) #f result)
+ (void))
+(define (column-for-each* proc result)
+ (column-fold (lambda (values seed) (apply proc values)) #f result)
+ (void))
+
+;; Like regular Scheme map, the order in which the procedure is applied is
+;; undefined. We make good use of that by traversing the resultset from
+;; the end back to the beginning, thereby avoiding a reverse! on the result.
+(define (row-map proc res)
+ (row-fold-right (lambda (row lst) (cons (proc row) lst)) '() res))
+(define (row-map* proc res)
+ (row-fold-right (lambda (row lst) (cons (apply proc row) lst)) '() res))
+(define (column-map proc res)
+ (column-fold-right (lambda (col lst) (cons (proc col) lst)) '() res))
+(define (column-map* proc res)
+ (column-fold-right (lambda (col lst) (cons (apply proc col) lst)) '() res))
+
+(define (result-format result)
+ (if (and result ((foreign-lambda bool PQbinaryTuples pgresult*)
+ (pg-result-ptr result)))
+ 'binary 'text))
+
+(define (copy-query*-fold kons knil conn query
+ #!optional (params '()) #!key (format 'text) raw)
+ (let* ((result (query* conn query params format: format raw: raw))
+ (data-format (result-format result)))
+ (handle-exceptions exn
+ (let cleanup () (if (result? (get-copy-data conn)) (raise exn) (cleanup)))
+ (let loop ((data (get-copy-data conn format: data-format))
+ (seed knil))
+ (if (result? data)
+ seed
+ ;; Explicit ordering; data could be _very_ big, allow one to be GCed
+ (let ((next (kons data seed)))
+ (loop (get-copy-data conn format: data-format) next)))))))
+
+(define (copy-query-fold kons knil conn query . params)
+ (copy-query*-fold kons knil conn query params))
+
+
+;; This is slow and memory-intensive if data is big. Provided for completeness
+(define (copy-query*-fold-right kons knil conn query
+ #!optional (params '()) #!key (format 'text) raw)
+ (let* ((result (query* conn query params format: format raw: raw))
+ (data-format (result-format result)))
+ (handle-exceptions exn
+ (let cleanup () (if (result? (get-copy-data conn)) (raise exn) (cleanup)))
+ (let loop ((data (get-copy-data conn format: data-format)))
+ (if (result? data)
+ knil
+ (kons data (loop (get-copy-data conn format: data-format))))))))
+
+(define (copy-query-fold-right kons knil conn query . params)
+ (copy-query*-fold-right kons knil conn query params))
+
+
+(define (copy-query*-map proc conn query
+ #!optional (params '()) #!key (format 'text) raw)
+ (reverse! (copy-query*-fold (lambda (data seed) (cons (proc data) seed))
+ '() conn query params format: format raw: raw)))
+
+(define (copy-query-map proc conn query . params)
+ (copy-query*-map proc conn query params))
+
+
+(define (copy-query*-for-each proc conn query
+ #!optional (params '()) #!key (format 'text) raw)
+ (copy-query*-fold (lambda (data seed) (proc data))
+ #f conn query params format: format raw: raw)
+ (void))
+
+(define (copy-query-for-each proc conn query . params)
+ (copy-query*-for-each proc conn query params))
+
+;; A bit of a weird name but consistent
+(define (call-with-output-copy-query*
+ proc conn query #!optional (params '()) #!key (format 'text) raw)
+ (query* conn query params format: format raw: raw)
+ (let* ((closed? #f)
+ (output-port (make-output-port
+ (lambda (data) (put-copy-data conn data))
+ (lambda () (put-copy-end conn) (set! closed? #t)))))
+ (handle-exceptions exn
+ (if closed?
+ (raise exn)
+ (handle-exceptions _
+ (raise exn)
+ ;; Previously written data will be discarded to guarantee atomicity
+ (put-copy-end conn "Chicken PostgreSQL egg -- forcing error")))
+ (call-with-values (lambda () (proc output-port))
+ (lambda args
+ (unless closed? (put-copy-end conn))
+ (apply values args))))))
+
+(define (call-with-output-copy-query proc conn query . params)
+ (call-with-output-copy-query* proc conn query params))
+
+(define (with-output-to-copy-query*
+ thunk conn query #!optional (params '()) #!key (format 'text) raw)
+ (call-with-output-copy-query* (lambda (x) (with-output-to-port x thunk))
+ conn query params format: format raw: raw))
+
+(define (with-output-to-copy-query thunk conn query . params)
+ (with-output-to-copy-query* thunk conn query params))
+
+)
diff --git a/tests/run.scm b/tests/run.scm
new file mode 100644
index 0000000..0469929
--- /dev/null
+++ b/tests/run.scm
@@ -0,0 +1,949 @@
+(import (chicken blob)
+ (chicken condition)
+ (chicken string)
+ test
+ postgresql
+ sql-null
+ srfi-4)
+
+(define-syntax test-error*
+ (syntax-rules ()
+ ((_ ?msg (?error-type ...) ?expr)
+ (let-syntax ((expression:
+ (syntax-rules ()
+ ((_ ?expr)
+ (condition-case (begin ?expr "<no error thrown>")
+ ((?error-type ...) '(?error-type ...))
+ (exn () (##sys#slot exn 1)))))))
+ (test ?msg '(?error-type ...) (expression: ?expr))))
+ ((_ ?msg ?error-type ?expr)
+ (test-error* ?msg (?error-type) ?expr))
+ ((_ ?error-type ?expr)
+ (test-error* (sprintf "~S" '?expr) ?error-type ?expr))))
+
+;; Perform a quick sanity check before running the actual tests
+(condition-case (disconnect (connect '((dbname . test))))
+ (exn (exn postgresql connect)
+ (print "Could not connect to server. Please ensure that the UNIX "
+ "user running these tests is allowed to connect to a database "
+ "with the name \"test\". Error from Postgres:")
+ (print ((condition-property-accessor 'exn 'message) exn))
+ (test-exit)))
+
+(test-begin "postgresql")
+
+(test-group "connection management"
+ (test "connect returns a connected connection"
+ '(#t #t)
+ (let* ((conn (connect '((dbname . test))))
+ (isconn (connection? conn))
+ (connected (connected? conn)))
+ (disconnect conn)
+ (list isconn connected)))
+ (test "disconnect causes connection to be disconnected"
+ '(#t #f)
+ (let ((conn (connect '((dbname . test)))))
+ (disconnect conn)
+ (list (connection? conn) (connected? conn))))
+ (test-error "cannot connect with invalid credentials"
+ (connect '((dbname . does-not-exist)
+ (username . nobody))))
+ (test-error "disconnect invalidates the connection"
+ (let ((conn (connect '((dbname . test)))))
+ (disconnect conn)
+ (reset-connection conn)))
+ ;; It would be nice if we could test some more error cases here but
+ ;; that's hard to do
+ )
+
+;; From now on, just keep using the same connection
+(define conn (connect '((dbname . test))))
+
+;; In the tests, we assume that the client lib is at least equal to
+;; the server's version.
+(define-values (major-version minor-version micro-version)
+ (apply values
+ (map string->number
+ (string-split
+ (value-at (query conn "SHOW server_version")) "."))))
+
+(test-group "low-level interface"
+ (test-assert "query returns result"
+ (result? (query conn "SELECT 1")))
+ (test "Correct row count"
+ 2
+ (row-count (query conn "SELECT 1 UNION SELECT 2")))
+ (test "Correct column count"
+ 4
+ (column-count (query conn "SELECT 1, 2, 3, 4")))
+ (test "Correct column name"
+ 'one
+ (column-name
+ (query conn "SELECT 1 AS one, 2 AS two") 0))
+ (test "Correct column names"
+ '(one two)
+ (column-names
+ (query conn "SELECT 1 AS one, 2 AS two")))
+ (test-error* "Condition for nonexistant column index"
+ (exn postgresql bounds)
+ (column-name
+ (query conn "SELECT 1 AS one, 2 AS two") 3))
+ (test "Not false for nameless column"
+ #f ;; Could check for ?column?, but that's a bit too specific
+ (not (column-name
+ (query conn "SELECT 1, 2") 0)))
+ ;; Maybe add a few tests here for case folding/noncase folding variants?
+ ;; Perhaps column-index-ci vs column-index? That would be
+ ;; misleading though, since column-index-ci isn't really ci,
+ ;; it will not match columns that are explicitly uppercased in the query.
+ (test "Correct column index"
+ 0
+ (column-index
+ (query conn "SELECT 1 AS one, 2 AS two") 'one))
+ (test "False column index for nonexistant column name"
+ #f
+ (column-index
+ (query conn "SELECT 1 AS one, 2 AS two") 'foo))
+ (test "False oid for virtual table"
+ #f
+ (table-oid
+ (query conn "SELECT 1 AS one, 2 AS two") 0))
+ (test-assert "Number for nonvirtual table"
+ (number?
+ (table-oid
+ (query conn "SELECT typlen FROM pg_type") 0)))
+
+ (test-error* "Condition for column index out of bounds"
+ (exn postgresql bounds)
+ (table-oid
+ (query conn "SELECT typname FROM pg_type") 1))
+ (test "Table column number for real table"
+ 0
+ (table-column-index
+ (query conn "SELECT typname FROM pg_type") 0))
+ (test "Column format is text for normal data"
+ 'text
+ (column-format
+ (query conn "SELECT 'hello'") 0))
+
+ (test "Column format is binary for forced binary data"
+ 'binary
+ (column-format
+ (query* conn "SELECT 1" '() format: 'binary) 0))
+
+ (test "Column type OID ok"
+ 23 ;; from catalog/pg_type.h
+ (column-type
+ (query conn "SELECT 1::int4") 0))
+ (test "Column modifier false"
+ #f
+ (column-type-modifier
+ (query conn "SELECT 1") 0))
+ (test "Column modifier for bit ok"
+ 2
+ (column-type-modifier
+ (query conn "SELECT '10'::bit(2)") 0))
+ (test "Result value string for strings"
+ "test"
+ (value-at (query conn "SELECT 'test'")))
+ (test "Result value string for 'name' type (with no custom parser)"
+ "test"
+ (value-at (query conn "SELECT 'test'::name")))
+ (test "Result row values"
+ '("one" "two")
+ (row-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 0))
+ (test "Result row values for second row"
+ '("three" "four")
+ (row-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 1))
+ (test "Result row alist"
+ '((a . "one") (b . "two"))
+ (row-alist
+ (query conn "SELECT 'one' AS a, 'two' AS b UNION SELECT 'three', 'four'") 0))
+ (test "Result row alist for second row"
+ '((a . "three") (b . "four"))
+ (row-alist
+ (query conn "SELECT 'one' AS a, 'two' AS b UNION SELECT 'three', 'four'") 1))
+ (test "Result column values"
+ '("one" "three")
+ (column-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 0))
+ (test "Result column values for second column"
+ '("two" "four")
+ (column-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 1))
+ (test "Result value number for numbers"
+ 1
+ (value-at (query conn "SELECT 1")))
+ (test "Result value string for raw numbers"
+ "1"
+ (value-at (query conn "SELECT 1") 0 0 raw: #t))
+ ;; We are using two levels of escaping here because the ::bytea cast
+ ;; performs another string interpretation. Yes, this is kinda confusing...
+ (test "Result value for null-terminated byte array"
+ (blob->u8vector (string->blob "h\x00ello"))
+ (value-at (query conn "SELECT E'h\\\\000ello'::bytea")))
+
+ (test "Result value for raw null-terminated byte array"
+ (if (>= major-version 9)
+ "\\x6800656c6c6f"
+ "h\\000ello")
+ (value-at (query conn "SELECT E'h\\\\000ello'::bytea") 0 0 raw: #t))
+
+ (test "Result value blob for binary string"
+ (string->blob "hello")
+ (value-at (query* conn "SELECT 'hello'" '() format: 'binary)))
+
+ (test "Result value blob for binary integer"
+ (u8vector->blob (u8vector 0 0 0 1))
+ (value-at (query* conn "SELECT 1::int4" '() format: 'binary)))
+
+ (test "Result value for binary string with NUL bytes"
+ (string->blob "h\x00ello")
+ (value-at (query* conn "SELECT E'h\\\\000ello'::bytea" '() format: 'binary)))
+
+ (test "Result value for array of integers"
+ `#(1 ,(sql-null) 3) ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array[1, null, 3]")))
+
+ (test "Result value for nested array of ints"
+ `#(#(1 ,(sql-null) 3)
+ #(4 5 ,(sql-null))) ;; Not sure if comparing sql-null is kosher
+ (value-at
+ (query conn "SELECT array[array[1, null, 3], array[4, 5, NULL]]")))
+
+ (test "Result value for array of strings"
+ `#("a" ,(sql-null) "c") ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array['a', null, 'c']")))
+
+ (test "Result value for nested array of strings"
+ `#(#("a" ,(sql-null) "c")
+ #("NULL" "e" ,(sql-null))) ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array[array['a', null, 'c'], array['NULL', 'e', NULL]]")))
+
+ (test "Result value at row 0, column 1"
+ 2
+ (value-at (query conn "SELECT 1, 2 UNION SELECT 3, 4") 1 0))
+ (test "Result value at row 1, column 0"
+ 3
+ (value-at (query conn "SELECT 1, 2 UNION SELECT 3, 4") 0 1))
+ (test-assert "Result value sql-null for NULL"
+ (sql-null? (value-at (query conn "SELECT NULL"))))
+ (test-error* "Result value error for out of bounds row"
+ (exn postgresql bounds)
+ (value-at (query conn "SELECT NULL") 0 1))
+ (test-error* "Result value error for out of bounds column"
+ (exn postgresql bounds)
+ (value-at (query conn "SELECT NULL") 1 0))
+
+ (query conn "PREPARE foo (int) AS (SELECT 1, 2, $1)")
+ (test-assert "query with prepared statement name returns result"
+ (result? (query conn 'foo 3)))
+ (test "result value ok for query with prepared statement name"
+ '(1 2 3)
+ (row-values (query conn 'foo 3)))
+ (test-error* "condition for missing parameter names"
+ (exn postgresql query)
+ (query conn 'foo))
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (test "Number of affected rows 1 with INSERT"
+ 1
+ (affected-rows
+ (query conn "INSERT INTO foo (bar) VALUES (1);")))
+ (query conn "COMMIT")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (query conn "INSERT INTO foo (bar) VALUES (100);")
+ (query conn "INSERT INTO foo (bar) VALUES (101);")
+ (test "Number of affected rows 2 with UPDATE of two rows"
+ 2
+ (affected-rows
+ (query conn "UPDATE foo SET bar=102;")))
+ (query conn "COMMIT")
+
+ (test "Inserted OID false on SELECT"
+ #f
+ (inserted-oid
+ (query conn "SELECT 1")))
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (test "Inserted OID false on OID-less table"
+ #f
+ (inserted-oid
+ (query conn "INSERT INTO foo (bar) VALUES (1);")))
+ (query conn "COMMIT")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) WITH (OIDS=true) ON COMMIT DROP")
+ (test-assert "Inserted OID number on table with OID"
+ (number?
+ (inserted-oid
+ (query conn "INSERT INTO foo (bar) VALUES (1)"))))
+ (query conn "COMMIT")
+
+ (test "regular parameters"
+ "hi"
+ (value-at (query conn "SELECT $1::text" "hi") 0 0))
+ (test-assert "NULL parameters"
+ (sql-null? (value-at
+ (query conn "SELECT $1::text" (sql-null)) 0 0)))
+ (test "blob parameters"
+ "hi"
+ (value-at (query conn "SELECT $1::text" (string->blob "hi")) 0 0))
+ (test "boolean parameters"
+ '(#t #f)
+ (row-values (query conn "SELECT $1::bool, $2::bool" #t #f)))
+ (test "integer array parameters"
+ `(#(1 2) #(,(sql-null) 4))
+ (row-values (query conn "SELECT $1::int[], $2::int[]"
+ `#(1 2) `#(,(sql-null) 4))))
+ (test "nested integer array parameters"
+ `(#(#(1 2) #(,(sql-null) 4)))
+ (row-values (query conn "SELECT $1::int[][]"
+ `#(#(1 2) #(,(sql-null) 4)))))
+ (test "string array parameters (including 'null')"
+ `(#("a" "b") #(,(sql-null) "null"))
+ (row-values (query conn "SELECT $1::text[], $2::text[]"
+ `#("a" "b") `#(,(sql-null) "null"))))
+ (test "string array parameters with meta-characters"
+ `(#("a\\b" "c\"d") #("{" "}"))
+ (row-values (query conn "SELECT $1::text[], $2::text[]"
+ '#("a\\b" "c\"d") '#("{" "}"))))
+ (test "nested string array parameters"
+ `(#(#("a" "b") #(,(sql-null) "null")))
+ (row-values (query conn "SELECT $1::text[][]"
+ `#(#("a" "b") #(,(sql-null) "null")))))
+ (test "array bounds are ignored"
+ `#(#(#(1 2 3) #(4 5 6)))
+ (value-at
+ (query conn
+ "SELECT '[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}'::int[] AS f1")))
+
+ ;; Basic domains seem to return just their base type
+ (query conn "BEGIN")
+ (query conn "CREATE DOMAIN mydom AS integer CHECK (VALUE > 2)")
+ (test "basic domains"
+ 3
+ (value-at (query conn "SELECT $1::mydom" 3)))
+ (query conn "ROLLBACK")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TYPE foo AS ( a integer, b text )")
+ (test "basic composite type"
+ `(1 "one")
+ (value-at (query conn "SELECT $1::foo" `(1 "one"))))
+ (test "composite type with meta-characters"
+ `(123 "\\backslash\"quote")
+ (value-at (query conn "SELECT $1::foo" `(123 "\\backslash\"quote"))))
+ (query conn "CREATE TYPE bar AS ( x foo, y integer )")
+ (test "Nested composite type"
+ `((2 "two") 3)
+ (value-at (query conn "SELECT $1::bar" `((2 "two") 3))))
+ (query conn "CREATE DOMAIN mydom AS integer CHECK (VALUE > 1)")
+ (query conn "CREATE TYPE qux AS ( i integer, d mydom )")
+ (test "Composite type containing domain value"
+ `(1 2)
+ (value-at (query conn "SELECT $1::qux" `(1 2))))
+ (query conn "ROLLBACK")
+
+ (test "anonymous composite type ('record')"
+ '("one" "two")
+ (value-at (query conn "SELECT ROW('one', 'two')")))
+
+ (test "anonymous composite type ('record') with NULL"
+ `("one" ,(sql-null))
+ (value-at (query conn "SELECT ROW('one', NULL)")))
+
+ ;; ROW() is indistinguishable from ROW(NULL). The latter is more likely
+ ;; to occur than the former, so let's ensure that's how it's interpreted
+ ;; See also http://archives.postgresql.org/pgsql-hackers/2004-06/msg00159.php
+ (test "anonymous composite type ('record') with just one NULL"
+ `(,(sql-null))
+ (value-at (query conn "SELECT ROW(NULL)"))))
+
+(test-group "escaping and quotation"
+ (test-group "with standards conforming strings off"
+ (query conn "SET standard_conforming_strings='off'")
+ (test "String is escaped correctly"
+ "it''s \"this\" \\\\ safe!"
+ (escape-string conn "it's \"this\" \\ safe!"))
+ (test "Small string is escaped correctly"
+ "foo"
+ (escape-string conn "foo"))
+ (test "Bytea is escaped correctly (u8vector)"
+ (if (>= major-version 9)
+ "\\\\x576800617427730a75703f"
+ "Wh\\\\000at''s\\\\012up?")
+ (escape-bytea conn (blob->u8vector (string->blob "Wh\x00at's\nup?"))))
+ (test "Bytea is unescaped correctly (blob)"
+ (blob->u8vector (string->blob "What's\nup? "))
+ ;; The extra quote is dropped here because it wouldn't be returned
+ ;; by pgsql either.
+ (unescape-bytea "What's\\012up?\\ "))
+ (test "Identifier is escaped and quoted correctly"
+ ;; Backslashes have no special meaning in identifiers,
+ ;; standards conforming strings or no
+ "\"it's \"\"this\"\" \\ safe!\""
+ (quote-identifier conn "it's \"this\" \\ safe!")))
+ (test-group "with standards conforming strings on"
+ (query conn "SET standard_conforming_strings='on'")
+ (test "String is escaped correctly"
+ "it''s \"this\" \\ safe!"
+ (escape-string conn "it's \"this\" \\ safe!"))
+ (test "Bytea is escaped correctly"
+ (if (>= major-version 9)
+ "\\x576800617427730a75703f"
+ "Wh\\000at''s\\012up?")
+ (escape-bytea conn "Wh\x00at's\nup?"))
+ (test "Bytea is unescaped correctly"
+ (blob->u8vector (string->blob "What's\nup? "))
+ ;; The extra quote is dropped here because it wouldn't be returned
+ ;; by pgsql either.
+ (unescape-bytea "What's\\012up?\\ "))
+ (test "Identifier is escaped and quoted correctly"
+ ;; Backslashes have no special meaning in identifiers,
+ ;; standards conforming strings or no
+ "\"it's \"\"this\"\" \\ safe!\""
+ (quote-identifier conn "it's \"this\" \\ safe!"))))
+
+(test-group "query error handling"
+ (test-group "basic info: position and code"
+ (handle-exceptions exn
+ (begin (test "Class of real query error is available and set to expected value"
+ "42"
+ ((condition-property-accessor 'query 'error-class) exn))
+ (test "Code of real query error is available and set to expected value"
+ "42P01" ; undefined_table
+ ((condition-property-accessor 'query 'error-code) exn))
+ ;; We can't set locale w/o superuser rights, so we'll
+ ;; have to make do with an incomplete severity check.
+ (test-assert "Severity of real query error is a symbol"
+ (symbol? ((condition-property-accessor
+ 'query 'severity) exn)))
+ (test "Statement position in real query error is correct"
+ 15
+ ((condition-property-accessor
+ 'query 'statement-position) exn)))
+ (query conn "SELECT 1 FROM nonexistant_table")
+ (error "query call didn't fail")))
+ (test-group "message and code"
+ (handle-exceptions exn
+ (begin (test "Error class is available and set to expected value"
+ "42"
+ ((condition-property-accessor 'query 'error-class) exn))
+ (test "Error code is available and set to expected value"
+ "42P16"
+ ((condition-property-accessor 'query 'error-code) exn))
+ ;; We can't set locale w/o superuser rights, so we'll
+ ;; have to make do with some neutered checks.
+ (test-assert "Severity is a symbol"
+ (symbol? ((condition-property-accessor
+ 'query 'severity) exn)))
+ (test "Message primary is set to the expected value"
+ "testing, 1, 2, 3."
+ ((condition-property-accessor 'query 'message-primary) exn))
+ (test "Detail is set to the expected value"
+ "this is a test"
+ ((condition-property-accessor 'query 'message-detail) exn))
+ (test "Hint is set to the expected value"
+ "try again"
+ ((condition-property-accessor 'query 'message-hint) exn))
+ ;; XXX These two may be too version-specific
+ (test "source function is set to the correct value"
+ "exec_stmt_raise"
+ ((condition-property-accessor 'query 'source-function) exn))
+ (test "source file is set to the the correct value"
+ "pl_exec.c"
+ ((condition-property-accessor 'query 'source-file) exn))
+ (test-assert "source line is available and a number"
+ (number? ((condition-property-accessor
+ 'query 'source-line) exn))))
+ (query conn (conc "CREATE OR REPLACE FUNCTION cause_error() "
+ "RETURNS void AS $$\n"
+ " BEGIN\n"
+ " RAISE invalid_table_definition \n"
+ " USING MESSAGE = 'testing, 1, 2, 3.', \n"
+ " DETAIL = 'this is a test', \n"
+ " HINT = 'try again';\n"
+ " END;\n"
+ "$$ LANGUAGE plpgsql"))
+ (value-at (query conn "SELECT cause_error()"))
+ (error "query call didn't fail")))
+ (when (or (> major-version 9) (and (= major-version 9) (>= minor-version 3)))
+ (test-group "schema information"
+ (query conn "BEGIN")
+ (query conn (conc "CREATE DOMAIN posint AS int"
+ " CONSTRAINT must_be_positive CHECK (VALUE > 0)"))
+ (query conn (conc "CREATE TEMP TABLE foo (bar posint) ON COMMIT DROP"))
+ (handle-exceptions exn
+ (begin (test-assert "Schema name is set" ; It is pg_temp_2
+ ((condition-property-accessor
+ 'query 'schema-name) exn))
+ (test "Data type name is correct"
+ "posint"
+ ((condition-property-accessor 'query 'datatype-name) exn))
+ (test "Constraint name is correct"
+ "must_be_positive"
+ ((condition-property-accessor 'query 'constraint-name) exn)))
+ (query conn "INSERT INTO foo (bar) VALUES (-1)")
+ (error "query call didn't fail"))
+ (query conn "ROLLBACK")
+ (query conn "BEGIN")
+ (query conn (conc "CREATE TEMP TABLE foo (bar int NOT NULL) "
+ "ON COMMIT DROP"))
+ (handle-exceptions exn
+ (begin (test-assert "Schema name is set" ; It is pg_temp_2
+ ((condition-property-accessor
+ 'query 'schema-name) exn))
+ (test "Table name is correct"
+ "foo"
+ ((condition-property-accessor 'query 'table-name) exn))
+ (test "Column name is correct"
+ "bar"
+ ((condition-property-accessor 'query 'column-name) exn)))
+ (query conn "INSERT INTO foo (bar) VALUES (NULL)")
+ (error "query call didn't fail"))
+ (query conn "ROLLBACK"))))
+
+(test-group "COPY support"
+ (query conn "CREATE TEMP TABLE copy_table ( nr integer, s text )")
+ (test-group "low-level interface"
+ (test-error* "Cannot put copy data while no COPY in progress"
+ (exn postgresql i/o)
+ (put-copy-data conn "whatever"))
+ (query conn "COPY copy_table (s, nr) FROM STDIN")
+ (test-error* "Cannot initiate new query while waiting for COPY input"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (put-copy-data conn "one\t1\n")
+ (test-error* "Cannot initiate new query while COPY data in progress"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (put-copy-data conn "two\t2")
+ (put-copy-end conn)
+ (let ((res (query conn "SELECT * FROM copy_table")))
+ (test "Simple copy from STDIN works"
+ '((1 "one")
+ (2 "two"))
+ (list (row-values res 0) (row-values res 1))))
+ (test-error* "Cannot get copy data while no COPY in progress"
+ (exn postgresql i/o)
+ (get-copy-data conn))
+ (query conn "COPY copy_table (s, nr) TO STDOUT")
+ (test-error* "Cannot initiate new query while waiting for COPY output"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (test "Simple copy to STDOUT works, first record"
+ "one\t1\n"
+ (get-copy-data conn))
+ (test-error* "Cannot initiate new query while reading COPY data"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (test "Simple copy to STDOUT works, second record"
+ "two\t2\n"
+ (get-copy-data conn))
+ (test-assert "EOF is marked by a result object"
+ (result? (get-copy-data conn))))
+ (test-group "high-level interface"
+ (test "Mapping"
+ '(("one" "1")
+ ("two" "2"))
+ (copy-query-map string-split conn "COPY copy_table (s, nr) TO STDOUT"))
+ (test "Error while mapping gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-map (lambda _ (error "blah"))
+ conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Fold"
+ '(("one" "1")
+ ("two" "2"))
+ (reverse
+ (copy-query-fold
+ (lambda (data result)
+ (cons (string-split data) result))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Error while folding gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-fold (lambda _ (error "blah"))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Fold-right"
+ '(("one" "1")
+ ("two" "2"))
+ (copy-query-fold-right
+ (lambda (data result)
+ (cons (string-split data) result))
+ '() conn "COPY copy_table (s, nr) TO STDOUT"))
+ (test "Error while folding right gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-fold-right (lambda _ (error "blah"))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "For-each"
+ '(("one" "1")
+ ("two" "2"))
+ (let ((res '()))
+ (copy-query-for-each (lambda (x)
+ (set! res (cons (string-split x) res)))
+ conn "COPY copy_table (s, nr) TO STDOUT")
+ (reverse res)))
+ (test "Error during for-each gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-for-each (lambda (x) (error "blah"))
+ conn "COPY copy_table (s, nr) TO STDOUT")))
+ (query conn "TRUNCATE copy_table")
+ (with-output-to-copy-query (lambda ()
+ (print "first\t1")
+ (print "second\t2"))
+ conn "COPY copy_table (s, nr) FROM STDIN")
+ (test "Port interface inserted data correctly"
+ '(("first" 1)
+ ("second" 2))
+ (let ((res (query conn "SELECT s, nr FROM copy_table")))
+ (list (row-values res 0) (row-values res 1))))
+ (query conn "TRUNCATE copy_table")
+ (handle-exceptions _
+ (void)
+ (with-output-to-copy-query (lambda ()
+ (print "first\t1")
+ (print "second\t2")
+ (error "blah"))
+ conn "COPY copy_table (s, nr) FROM STDIN"))
+ (test "Error inside with-output-to-copy caused abort of insert"
+ 0 (value-at (query conn "SELECT COUNT(*) FROM copy_table")))))
+
+(test-group "type parsers"
+ (test "Integer parsed correctly"
+ 1234
+ (numeric-parser "1234"))
+ (test "Float parsed correctly"
+ 123.456
+ (numeric-parser "123.456"))
+ (test-error* "Non-integer is an error"
+ (exn postgresql parse)
+ (numeric-parser "not an integer"))
+ (test "Boolean true parsed correctly"
+ #t
+ (bool-parser "t"))
+ (test "Boolean false parsed correctly"
+ #f
+ (bool-parser "f"))
+ (test "Byte array parsed correctly"
+ (blob->u8vector/shared (string->blob "abc\x01\x02\xffdef"))
+ (bytea-parser "abc\\001\\002\\377def"))
+ (test "Char parser"
+ #\x
+ (char-parser "x")))
+
+(test-group "type unparsers"
+ (test "Boolean true unparsed correctly"
+ "TRUE"
+ (bool-unparser conn #t))
+ (test "Boolean false unparsed correctly"
+ "FALSE"
+ (bool-unparser conn #f)))
+
+(test-group "high-level interface"
+ (test "row-fold"
+ '(("one" 2)
+ ("three" 4))
+ (reverse
+ (row-fold
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2))))
+ (test "column-fold"
+ '(("one" "three")
+ (2 4))
+ (reverse
+ (column-fold
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2))))
+ (test "row-fold-right"
+ '(("one" 2)
+ ("three" 4))
+ (row-fold-right
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2)))
+ (test "column-fold-right"
+ '(("one" "three")
+ (2 4))
+ (column-fold-right
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2)))
+ (test "row-for-each"
+ '(("one" 2)
+ ("three" 4))
+ (let ((res '()))
+ (row-for-each
+ (lambda (row) (set! res (cons row res)))
+ (query
+ conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))
+ (reverse res)))
+ (test "column-for-each"
+ '(("one" "three")
+ (2 4))
+ (let ((res '()))
+ (column-for-each
+ (lambda (col) (set! res (cons col res)))
+ (query
+ conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))
+ (reverse res)))
+ (test "row-map"
+ '(("one" 2)
+ ("three" 4))
+ (row-map
+ identity
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2)))
+ (test "column-map"
+ '(("one" "three")
+ (2 4))
+ (column-map
+ identity
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))))
+
+(test-group "transactions"
+ (query conn "CREATE TEMP TABLE foo ( bar integer )")
+
+ (test-group "simple transactions"
+ (test "Transaction inactive"
+ #f
+ (in-transaction? conn))
+ (test "Transaction active"
+ #t
+ (with-transaction conn
+ (lambda () (in-transaction? conn))))
+ (test "Successful transaction"
+ '(1)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Unsuccessful transaction"
+ #f
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ #f)))
+
+ (test "Empty table after unsuccessful transaction"
+ '()
+ (column-values (query conn "SELECT * FROM foo")))
+
+ (handle-exceptions exn
+ (void)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (error "oops!"))))
+
+ (test "Exception during transaction causes reset"
+ '()
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (test-group "nested transactions"
+ (test "Successful transaction"
+ '(1 2)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Unsuccessful main transaction"
+ '()
+ (and
+ (not
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))
+ #f)))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (test "Unsuccessful subtransaction"
+ '(1)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")
+ #f))
+ #t))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ ;; Test that errors do not kill the transaction. Apparently
+ ;; aborting transactions on errors is a psql(1) "feature", not a
+ ;; libpq one.
+ (test "Unsuccessful subtransaction with bad query"
+ '(1 2)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (handle-exceptions exn
+ #t
+ (with-transaction
+ conn (lambda ()
+ (query conn "INVALID QUERY"))))
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Multiple subtransactions"
+ '(1 3)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")
+ #f))
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (3)")))))
+ (column-values (query conn "SELECT * FROM foo")))))
+
+ ;; Very annoying; read-only transactions can write to temporary tables,
+ ;; so we have to create a REAL table here.
+ (query conn "CREATE TABLE chicken_pgsql_test (bar int)")
+ ;; We'd better make damn sure it gets dropped again.
+ (on-exit (lambda () (query conn "DROP TABLE chicken_pgsql_test")))
+
+ (test-group "transaction access mode"
+ (test "Writes with explicit read/write access mode work"
+ '(1)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (1)")
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test "Reads with read-only access mode work"
+ '(1)
+ (with-transaction
+ conn (lambda ()
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read-only))
+
+ (test-error* "Writes with read-only access cause errors"
+ (exn postgresql query)
+ (with-transaction
+ conn (lambda ()
+ (query conn (conc "INSERT INTO chicken_pgsql_test (bar)"
+ "VALUES (1)")))
+ access: 'read-only))
+
+ (test "Switching read-write to read-only works"
+ '(1 2)
+ (with-transaction
+ conn (lambda ()
+ (with-transaction
+ conn (lambda ()
+ (query conn "SELECT * FROM chicken_pgsql_test"))
+ access: 'read-only)
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (2)")
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test "Switching read-write to read-only works with rollback"
+ '(1 2 3)
+ (with-transaction
+ conn (lambda ()
+ (handle-exceptions exn
+ (void)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (3)"))
+ access: 'read-only))
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (3)")
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test-error* "Switching read-only to read-write causes error"
+ (exn postgresql query)
+ (with-transaction
+ conn (lambda ()
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (with-transaction
+ conn (lambda ()
+ (query conn
+ (conc "INSERT INTO chicken_pgsql_test (bar)"
+ "VALUES (2)")))
+ access: 'read/write)
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read-only))
+
+ ;; Run these "concurrent" tests with a second connection
+ (let ((conn2 (connect '((dbname . test)))))
+ (test "Read committed transaction does see concurrent changes"
+ '(1 2 3 4)
+ (with-transaction
+ conn (lambda ()
+ ;; MUST perform a query, since only after the first
+ ;; query will the isolation take effect.
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (query conn2 (conc "INSERT INTO chicken_pgsql_test (bar) "
+ "VALUES (4)"))
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ isolation: 'read-committed))
+
+ (test "Serializable transaction doesn't see concurrent changes"
+ '(1 2 3 4)
+ (with-transaction
+ conn (lambda ()
+ ;; MUST perform a query, since only after the first
+ ;; query will the isolation take effect.
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (query conn2 (conc "INSERT INTO chicken_pgsql_test (bar) "
+ "VALUES (5)"))
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ isolation: 'serializable))
+ (disconnect conn2)))
+ )
+
+(test-end)
+
+(test-exit)