| 1 | ;;; Bindings to the PostgreSQL C library |
|---|
| 2 | ;; |
|---|
| 3 | ;; Copyright (C) 2008-2009 Peter Bex |
|---|
| 4 | ;; Copyright (C) 2004 Johannes Grødem <johs@copyleft.no> |
|---|
| 5 | ;; Redistribution and use in source and binary forms, with or without |
|---|
| 6 | ;; modification, is permitted. |
|---|
| 7 | ;; |
|---|
| 8 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS |
|---|
| 9 | ;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|---|
| 10 | ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|---|
| 11 | ;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE |
|---|
| 12 | ;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|---|
| 13 | ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT |
|---|
| 14 | ;; OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
|---|
| 15 | ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|---|
| 16 | ;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|---|
| 17 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE |
|---|
| 18 | ;; USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
|---|
| 19 | ;; DAMAGE. |
|---|
| 20 | |
|---|
| 21 | (module postgresql |
|---|
| 22 | (update-type-parsers! default-type-parsers |
|---|
| 23 | char-parser bool-parser bytea-parser numeric-parser |
|---|
| 24 | update-type-unparsers! default-type-unparsers |
|---|
| 25 | bool-unparser |
|---|
| 26 | |
|---|
| 27 | connect reset-connection disconnect connection? |
|---|
| 28 | |
|---|
| 29 | exec-simple-queries exec-query |
|---|
| 30 | |
|---|
| 31 | result? clear-result! result-row-count result-column-count |
|---|
| 32 | result-column-index result-column result-column-format |
|---|
| 33 | result-column-type result-column-type-modifier result-columns |
|---|
| 34 | result-table-oid result-table-column-index |
|---|
| 35 | result-value result-values result-alist result-affected-rows |
|---|
| 36 | result-inserted-oid invalid-oid |
|---|
| 37 | |
|---|
| 38 | escape-string escape-bytea unescape-bytea |
|---|
| 39 | |
|---|
| 40 | query-fold query-fold* query-fold-right query-fold-right* |
|---|
| 41 | query-for-each query-for-each* query-map query-map*) |
|---|
| 42 | |
|---|
| 43 | (import chicken scheme foreign) |
|---|
| 44 | |
|---|
| 45 | (require-extension srfi-1 srfi-4 srfi-13 srfi-18 srfi-69 |
|---|
| 46 | extras data-structures sql-null) |
|---|
| 47 | |
|---|
| 48 | (foreign-declare "#include <libpq-fe.h>") |
|---|
| 49 | |
|---|
| 50 | (define-foreign-type pg-polling-status (enum "PostgresPollingStatusType")) |
|---|
| 51 | (define-foreign-variable PGRES_POLLING_FAILED pg-polling-status) |
|---|
| 52 | (define-foreign-variable PGRES_POLLING_READING pg-polling-status) |
|---|
| 53 | (define-foreign-variable PGRES_POLLING_WRITING pg-polling-status) |
|---|
| 54 | (define-foreign-variable PGRES_POLLING_OK pg-polling-status) |
|---|
| 55 | |
|---|
| 56 | (define-foreign-type pg-exec-status (enum "ExecStatusType")) |
|---|
| 57 | (define-foreign-variable PGRES_EMPTY_QUERY pg-exec-status) |
|---|
| 58 | (define-foreign-variable PGRES_COMMAND_OK pg-exec-status) |
|---|
| 59 | (define-foreign-variable PGRES_TUPLES_OK pg-exec-status) |
|---|
| 60 | (define-foreign-variable PGRES_COPY_OUT pg-exec-status) |
|---|
| 61 | (define-foreign-variable PGRES_COPY_IN pg-exec-status) |
|---|
| 62 | (define-foreign-variable PGRES_BAD_RESPONSE pg-exec-status) |
|---|
| 63 | (define-foreign-variable PGRES_NONFATAL_ERROR pg-exec-status) |
|---|
| 64 | (define-foreign-variable PGRES_FATAL_ERROR pg-exec-status) |
|---|
| 65 | |
|---|
| 66 | ;(define-foreign-type pgconn* (c-pointer "PGconn")) |
|---|
| 67 | (define-foreign-type pgconn* c-pointer) |
|---|
| 68 | |
|---|
| 69 | (define PQconnectStart (foreign-lambda pgconn* PQconnectStart (const c-string))) |
|---|
| 70 | (define PQconnectPoll (foreign-lambda pg-polling-status PQconnectPoll pgconn*)) |
|---|
| 71 | (define PQresetStart (foreign-lambda bool PQresetStart pgconn*)) |
|---|
| 72 | (define PQresetPoll (foreign-lambda pg-polling-status PQresetPoll pgconn*)) |
|---|
| 73 | (define PQfinish (foreign-lambda void PQfinish pgconn*)) |
|---|
| 74 | (define PQstatus (foreign-lambda (enum "ConnStatusType") PQstatus (const pgconn*))) |
|---|
| 75 | (define PQerrorMessage (foreign-lambda c-string PQerrorMessage (const pgconn*))) |
|---|
| 76 | |
|---|
| 77 | ;(define-foreign-type oid "Oid") |
|---|
| 78 | (define-foreign-type oid unsigned-int) |
|---|
| 79 | |
|---|
| 80 | (define invalid-oid (foreign-value "InvalidOid" oid)) |
|---|
| 81 | |
|---|
| 82 | (define PQisBusy (foreign-lambda bool PQisBusy pgconn*)) |
|---|
| 83 | (define PQconsumeInput (foreign-lambda bool PQconsumeInput pgconn*)) |
|---|
| 84 | |
|---|
| 85 | (define-foreign-type pgresult* (c-pointer "PGresult")) |
|---|
| 86 | |
|---|
| 87 | (define PQgetResult (foreign-lambda pgresult* PQgetResult pgconn*)) |
|---|
| 88 | (define PQresultStatus (foreign-lambda pg-exec-status PQresultStatus (const pgresult*))) |
|---|
| 89 | (define PQresultErrorMessage (foreign-lambda c-string PQresultErrorMessage (const pgresult*))) |
|---|
| 90 | (define PQresultErrorField (foreign-lambda c-string PQresultErrorField (const pgresult*) int)) |
|---|
| 91 | |
|---|
| 92 | (define PQclear (foreign-lambda void PQclear pgresult*)) |
|---|
| 93 | (define PQntuples (foreign-lambda int PQntuples (const pgresult*))) |
|---|
| 94 | (define PQnfields (foreign-lambda int PQnfields (const pgresult*))) |
|---|
| 95 | (define PQfname (foreign-lambda c-string PQfname (const pgresult*) int)) |
|---|
| 96 | (define PQfnumber (foreign-lambda int PQfnumber (const pgresult*) (const c-string))) |
|---|
| 97 | (define PQftable (foreign-lambda oid PQftable (const pgresult*) int)) |
|---|
| 98 | (define PQftablecol (foreign-lambda int PQftablecol (const pgresult*) int)) |
|---|
| 99 | (define PQfformat (foreign-lambda int PQfformat (const pgresult*) int)) |
|---|
| 100 | (define PQftype (foreign-lambda oid PQftype (const pgresult*) int)) |
|---|
| 101 | (define PQfmod (foreign-lambda int PQfmod (const pgresult*) int)) |
|---|
| 102 | (define PQgetisnull (foreign-lambda bool PQgetisnull (const pgresult*) int int)) |
|---|
| 103 | (define PQcmdTuples (foreign-lambda nonnull-c-string PQcmdTuples pgresult*)) |
|---|
| 104 | (define PQoidValue (foreign-lambda oid PQoidValue pgresult*)) |
|---|
| 105 | |
|---|
| 106 | ;; TODO: Create a real callback system? |
|---|
| 107 | (foreign-declare "static void nullNoticeReceiver(void *arg, const PGresult *res){ }") |
|---|
| 108 | |
|---|
| 109 | (define-syntax define-foreign-int |
|---|
| 110 | (er-macro-transformer |
|---|
| 111 | (lambda (e r c) |
|---|
| 112 | ;; cannot rename define-foreign-variable; it's a really special form |
|---|
| 113 | `(define-foreign-variable ,(cadr e) int ,(conc "(int) " (cadr e)))))) |
|---|
| 114 | |
|---|
| 115 | (define-foreign-int PG_DIAG_SEVERITY) |
|---|
| 116 | (define-foreign-int PG_DIAG_SQLSTATE) |
|---|
| 117 | (define-foreign-int PG_DIAG_MESSAGE_PRIMARY) |
|---|
| 118 | (define-foreign-int PG_DIAG_MESSAGE_DETAIL) |
|---|
| 119 | (define-foreign-int PG_DIAG_MESSAGE_HINT) |
|---|
| 120 | (define-foreign-int PG_DIAG_STATEMENT_POSITION) |
|---|
| 121 | (define-foreign-int PG_DIAG_CONTEXT) |
|---|
| 122 | (define-foreign-int PG_DIAG_SOURCE_FILE) |
|---|
| 123 | (define-foreign-int PG_DIAG_SOURCE_LINE) |
|---|
| 124 | (define-foreign-int PG_DIAG_SOURCE_FUNCTION) |
|---|
| 125 | |
|---|
| 126 | (define (postgresql-error loc message . args) |
|---|
| 127 | (signal (make-pg-condition loc message args: args))) |
|---|
| 128 | |
|---|
| 129 | (define (make-pg-condition loc message #!key (args '()) severity |
|---|
| 130 | error-class error-code message-detail |
|---|
| 131 | message-hint statement-position context |
|---|
| 132 | source-file source-line |
|---|
| 133 | source-function) |
|---|
| 134 | (make-composite-condition |
|---|
| 135 | (make-property-condition |
|---|
| 136 | 'exn 'location loc 'message message 'arguments args) |
|---|
| 137 | (make-property-condition |
|---|
| 138 | 'postgresql 'severity severity 'error-class error-class |
|---|
| 139 | 'error-code error-code 'message-detail message-detail |
|---|
| 140 | 'message-hint message-hint 'statement-position statement-position |
|---|
| 141 | 'context context 'source-file source-file 'source-line source-line |
|---|
| 142 | ;; Might break not-terribly-old versions of postgresql |
|---|
| 143 | ;;'internal-position internal-position 'internal-query internal-query |
|---|
| 144 | 'source-function source-function))) |
|---|
| 145 | |
|---|
| 146 | ;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 147 | ;;;; Type parsers |
|---|
| 148 | ;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 149 | |
|---|
| 150 | (define (char-parser str) (string-ref str 0)) |
|---|
| 151 | |
|---|
| 152 | (define (bool-parser str) (string=? str "t")) |
|---|
| 153 | |
|---|
| 154 | (define (abstime-parser str) str) |
|---|
| 155 | |
|---|
| 156 | (define (reltime-parser str) str) |
|---|
| 157 | |
|---|
| 158 | (define (parse-format-string s) |
|---|
| 159 | (let-syntax ((push! (syntax-rules () |
|---|
| 160 | ((_ value place) |
|---|
| 161 | (set! place (cons value place)))))) |
|---|
| 162 | (do ([i 0 (+ i 1)] |
|---|
| 163 | [ranges (list)] |
|---|
| 164 | [cur-range (list)] |
|---|
| 165 | [len (string-length s)]) |
|---|
| 166 | ([= i len] |
|---|
| 167 | (when (not (null? cur-range)) |
|---|
| 168 | (push! (cons (- i (length cur-range)) i) |
|---|
| 169 | ranges)) |
|---|
| 170 | (reverse! ranges)) |
|---|
| 171 | (let ([char (string-ref s i)]) |
|---|
| 172 | (cond ([and (or (null? cur-range) |
|---|
| 173 | (char=? char (car cur-range))) |
|---|
| 174 | (char-alphabetic? char)] |
|---|
| 175 | (push! char cur-range)) |
|---|
| 176 | ([and (not (null? cur-range)) |
|---|
| 177 | (not (char=? char (car cur-range)))] |
|---|
| 178 | (push! (cons (- i (length cur-range)) i) |
|---|
| 179 | ranges) |
|---|
| 180 | (set! cur-range |
|---|
| 181 | (if (char-alphabetic? char) |
|---|
| 182 | (list char) |
|---|
| 183 | (list))))))))) |
|---|
| 184 | |
|---|
| 185 | (define-syntax define-time-parser |
|---|
| 186 | (syntax-rules () |
|---|
| 187 | ((_ name format-string) |
|---|
| 188 | (define name |
|---|
| 189 | (let ((format-ranges (parse-format-string format-string))) |
|---|
| 190 | (lambda (str) |
|---|
| 191 | (apply |
|---|
| 192 | vector |
|---|
| 193 | (map (lambda (range) |
|---|
| 194 | (if (> (cdr range) (string-length str)) |
|---|
| 195 | 0 |
|---|
| 196 | (string->number |
|---|
| 197 | (substring str (car range) (cdr range))))) |
|---|
| 198 | format-ranges)))))))) |
|---|
| 199 | |
|---|
| 200 | (define-time-parser date-parser "YYYY-MM-DD") |
|---|
| 201 | (define-time-parser timestamp-parser "YYYY-MM-DD hh:mm:ss.ssssss") |
|---|
| 202 | (define-time-parser timestamp/tz-parser "YYYY-MM-DD hh:mm:ss.sssssszzz") |
|---|
| 203 | (define-time-parser time-parser "hh:mm:ss.ssssss") |
|---|
| 204 | |
|---|
| 205 | (define (numeric-parser str) |
|---|
| 206 | (or (string->number str) |
|---|
| 207 | (postgresql-error 'numeric-parser "Unable to parse number" str))) |
|---|
| 208 | |
|---|
| 209 | (define (bytea-parser str) |
|---|
| 210 | (blob->u8vector/shared (string->blob (unescape-bytea str)))) |
|---|
| 211 | |
|---|
| 212 | (define default-type-parsers |
|---|
| 213 | (make-parameter |
|---|
| 214 | `(("text" . ,identity) |
|---|
| 215 | ("bytea" . ,bytea-parser) |
|---|
| 216 | ("char" . ,char-parser) |
|---|
| 217 | ("bpchar" . ,identity) |
|---|
| 218 | ("bool" . ,bool-parser) |
|---|
| 219 | ("int8" . ,numeric-parser) |
|---|
| 220 | ("int4" . ,numeric-parser) |
|---|
| 221 | ("int2" . ,numeric-parser) |
|---|
| 222 | ("float4" . ,numeric-parser) |
|---|
| 223 | ("float8" . ,numeric-parser) |
|---|
| 224 | ("abstime" . ,abstime-parser) |
|---|
| 225 | ("reltime" . ,reltime-parser) |
|---|
| 226 | ("date" . ,date-parser) |
|---|
| 227 | ("time" . ,time-parser) |
|---|
| 228 | ("timestamp" . ,timestamp-parser) |
|---|
| 229 | ("timestamptz" . ,timestamp/tz-parser) |
|---|
| 230 | ("numeric" . ,numeric-parser) |
|---|
| 231 | ("oid" . ,numeric-parser)))) |
|---|
| 232 | |
|---|
| 233 | ;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 234 | ;;;; Type unparsers |
|---|
| 235 | ;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 236 | |
|---|
| 237 | (define (bool-unparser b) |
|---|
| 238 | (if b "TRUE" "FALSE")) |
|---|
| 239 | |
|---|
| 240 | (define default-type-unparsers |
|---|
| 241 | (make-parameter |
|---|
| 242 | `((,string? . ,identity) |
|---|
| 243 | (,u8vector? . ,u8vector->blob/shared) |
|---|
| 244 | (,char? . ,string) |
|---|
| 245 | (,boolean? . ,bool-unparser) |
|---|
| 246 | (,number? . ,number->string) |
|---|
| 247 | #;(,vector? . ,vector-unparser)))) |
|---|
| 248 | |
|---|
| 249 | ;; Retrieve type-oids from PostgreSQL: |
|---|
| 250 | (define (update-type-parsers! conn #!optional new-type-parsers) |
|---|
| 251 | (let ((type-parsers (or new-type-parsers (pg-connection-type-parsers conn))) |
|---|
| 252 | (ht (make-hash-table)) |
|---|
| 253 | (result '())) |
|---|
| 254 | ;; Set the parsers now, so that we will be retrieving raw data |
|---|
| 255 | (pg-connection-oid-parsers-set! conn ht) |
|---|
| 256 | (pg-connection-type-parsers-set! conn type-parsers) |
|---|
| 257 | (unless (null? type-parsers) ; empty IN () clause is not allowed |
|---|
| 258 | (query-for-each* |
|---|
| 259 | (lambda (oid typname) |
|---|
| 260 | (and-let* ((procedure (assoc typname type-parsers))) |
|---|
| 261 | (hash-table-set! ht (string->number oid) (cdr procedure)))) |
|---|
| 262 | conn |
|---|
| 263 | (conc "SELECT oid, typname FROM pg_type WHERE typname IN " |
|---|
| 264 | "('" (string-intersperse |
|---|
| 265 | (map (lambda (p) (escape-string conn (car p))) |
|---|
| 266 | type-parsers) "', '") "')"))))) |
|---|
| 267 | |
|---|
| 268 | (define (update-type-unparsers! conn new-type-unparsers) |
|---|
| 269 | (pg-connection-type-unparsers-set! conn new-type-unparsers)) |
|---|
| 270 | |
|---|
| 271 | ;;;;;;;;;;;;;;;;;;;; |
|---|
| 272 | ;;;; Connections |
|---|
| 273 | ;;;;;;;;;;;;;;;;;;;; |
|---|
| 274 | |
|---|
| 275 | (define-record pg-connection ptr type-parsers oid-parsers type-unparsers) |
|---|
| 276 | (define connection? pg-connection?) |
|---|
| 277 | |
|---|
| 278 | (define (pgsql-connection->fd conn) |
|---|
| 279 | ((foreign-lambda int PQsocket pgconn*) (pg-connection-ptr conn))) |
|---|
| 280 | |
|---|
| 281 | ;; TODO: Add timeout code |
|---|
| 282 | (define (wait-for-connection! conn poll-function) |
|---|
| 283 | (let ((conn-fd (pgsql-connection->fd conn)) |
|---|
| 284 | (conn-ptr (pg-connection-ptr conn))) |
|---|
| 285 | (let loop ((result (poll-function conn-ptr))) |
|---|
| 286 | (cond ((= result PGRES_POLLING_OK) (void)) |
|---|
| 287 | ((= result PGRES_POLLING_FAILED) |
|---|
| 288 | (let ((error-message (PQerrorMessage conn-ptr))) |
|---|
| 289 | (disconnect conn) |
|---|
| 290 | (postgresql-error 'connect |
|---|
| 291 | (conc "Polling Postgres database failed. " |
|---|
| 292 | error-message)))) |
|---|
| 293 | ((member result (list PGRES_POLLING_WRITING PGRES_POLLING_READING)) |
|---|
| 294 | (thread-wait-for-i/o! conn-fd (if (= PGRES_POLLING_READING result) |
|---|
| 295 | #:output |
|---|
| 296 | #:input)) |
|---|
| 297 | (loop (poll-function conn-ptr))) |
|---|
| 298 | (else |
|---|
| 299 | (postgresql-error 'connect (conc "Unknown status code!"))))))) |
|---|
| 300 | |
|---|
| 301 | (define (alist->connection-spec alist) |
|---|
| 302 | (string-join |
|---|
| 303 | (map (lambda (subspec) |
|---|
| 304 | (sprintf "~A='~A'" |
|---|
| 305 | (car subspec) ;; this had better not contain [ =\'] |
|---|
| 306 | (string-translate* (->string (cdr subspec)) |
|---|
| 307 | '(("\\" . "\\\\") ("'" . "\\'"))))) |
|---|
| 308 | alist))) |
|---|
| 309 | |
|---|
| 310 | (define (connect connection-spec |
|---|
| 311 | #!optional |
|---|
| 312 | (type-parsers (default-type-parsers)) |
|---|
| 313 | (type-unparsers (default-type-unparsers))) |
|---|
| 314 | (let* ((connection-spec (if (string? connection-spec) |
|---|
| 315 | connection-spec |
|---|
| 316 | (alist->connection-spec connection-spec))) |
|---|
| 317 | (conn-ptr (PQconnectStart connection-spec))) |
|---|
| 318 | (cond |
|---|
| 319 | ((not conn-ptr) |
|---|
| 320 | (postgresql-error 'connect |
|---|
| 321 | "Unable to allocate a Postgres connection structure." |
|---|
| 322 | connection-spec)) |
|---|
| 323 | ((= (foreign-value "CONNECTION_BAD" int) (PQstatus conn-ptr)) |
|---|
| 324 | (let ((error-message (PQerrorMessage conn-ptr))) |
|---|
| 325 | (PQfinish conn-ptr) |
|---|
| 326 | (postgresql-error 'connect |
|---|
| 327 | (conc "Connection to Postgres database failed: " |
|---|
| 328 | error-message) |
|---|
| 329 | connection-spec))) |
|---|
| 330 | (else |
|---|
| 331 | (let ((conn (make-pg-connection conn-ptr type-parsers |
|---|
| 332 | (make-hash-table) type-unparsers))) |
|---|
| 333 | ;; We don't want libpq to piss in our stderr stream |
|---|
| 334 | ((foreign-lambda* void ((pgconn* conn)) |
|---|
| 335 | "PQsetNoticeReceiver(conn, nullNoticeReceiver, NULL);") conn-ptr) |
|---|
| 336 | (wait-for-connection! conn PQconnectPoll) |
|---|
| 337 | (set-finalizer! conn disconnect) |
|---|
| 338 | ;; Retrieve type-information from PostgreSQL metadata for use by |
|---|
| 339 | ;; the various value-parsers. |
|---|
| 340 | (update-type-parsers! conn) |
|---|
| 341 | conn))))) |
|---|
| 342 | |
|---|
| 343 | (define (reset-connection connection) |
|---|
| 344 | (let ((conn-ptr (pg-connection-ptr connection))) |
|---|
| 345 | (if (PQresetStart conn-ptr) ;; Update oid-parsers? |
|---|
| 346 | (wait-for-connection! connection PQresetPoll) |
|---|
| 347 | (let ((error-message (PQerrorMessage conn-ptr))) |
|---|
| 348 | (disconnect connection) |
|---|
| 349 | (postgresql-error 'reset-connection |
|---|
| 350 | (conc "Reset of connection failed " error-message) |
|---|
| 351 | connection))))) |
|---|
| 352 | |
|---|
| 353 | (define (disconnect connection) |
|---|
| 354 | (and-let* ((conn-ptr (pg-connection-ptr connection))) |
|---|
| 355 | (pg-connection-ptr-set! connection #f) |
|---|
| 356 | (pg-connection-type-parsers-set! connection #f) |
|---|
| 357 | (pg-connection-oid-parsers-set! connection #f) |
|---|
| 358 | (PQfinish conn-ptr)) |
|---|
| 359 | (void)) |
|---|
| 360 | |
|---|
| 361 | ;;;;;;;;;;;;;;; |
|---|
| 362 | ;;;; Results |
|---|
| 363 | ;;;;;;;;;;;;;;; |
|---|
| 364 | |
|---|
| 365 | (define-record pg-result ptr value-parsers) |
|---|
| 366 | (define result? pg-result?) |
|---|
| 367 | |
|---|
| 368 | (define (clear-result! result) |
|---|
| 369 | (and-let* ((result-ptr (pg-result-ptr result))) |
|---|
| 370 | (pg-result-ptr-set! result #f) |
|---|
| 371 | (PQclear result-ptr))) |
|---|
| 372 | |
|---|
| 373 | (define (result-row-count result) |
|---|
| 374 | (PQntuples (pg-result-ptr result))) |
|---|
| 375 | |
|---|
| 376 | (define (result-column-count result) |
|---|
| 377 | (PQnfields (pg-result-ptr result))) |
|---|
| 378 | |
|---|
| 379 | ;; Helper procedures for bounds checking; so we can distinguish between |
|---|
| 380 | ;; out of bounds and nonexistant columns, and signal it. |
|---|
| 381 | (define (check-result-column-index! result index location) |
|---|
| 382 | (when (>= index (result-column-count result)) |
|---|
| 383 | (postgresql-error |
|---|
| 384 | location (sprintf "Result column ~A out of bounds" index) result index))) |
|---|
| 385 | |
|---|
| 386 | (define (check-result-row-index! result index location) |
|---|
| 387 | (when (>= index (result-row-count result)) |
|---|
| 388 | (postgresql-error |
|---|
| 389 | location (sprintf "Result row ~A out of bounds" index) result index))) |
|---|
| 390 | |
|---|
| 391 | (define (result-column result index) |
|---|
| 392 | (check-result-column-index! result index 'result-column) |
|---|
| 393 | (string->symbol (PQfname (pg-result-ptr result) index))) |
|---|
| 394 | |
|---|
| 395 | (define (result-columns result) |
|---|
| 396 | (let loop ((ptr (pg-result-ptr result)) |
|---|
| 397 | (row '()) |
|---|
| 398 | (idx (result-column-count result))) |
|---|
| 399 | (if (= idx 0) |
|---|
| 400 | row |
|---|
| 401 | (loop ptr (cons (string->symbol |
|---|
| 402 | (PQfname ptr (sub1 idx))) row) (sub1 idx))))) |
|---|
| 403 | |
|---|
| 404 | (define (result-column-index result name) |
|---|
| 405 | (let ((idx (PQfnumber (pg-result-ptr result) (symbol->string name)))) |
|---|
| 406 | (and (>= idx 0) idx))) |
|---|
| 407 | |
|---|
| 408 | (define (result-table-oid result index) |
|---|
| 409 | (check-result-column-index! result index 'result-table-oid) |
|---|
| 410 | (let ((oid (PQftable (pg-result-ptr result) index))) |
|---|
| 411 | (and (not (= oid invalid-oid)) oid))) |
|---|
| 412 | |
|---|
| 413 | ;; Fixes the off-by-1 unexpectedness in libpq/the protocol to make it more |
|---|
| 414 | ;; consistent with the rest of Scheme. However, this is inconsistent with |
|---|
| 415 | ;; almost all other PostgreSQL interfaces... |
|---|
| 416 | (define (result-table-column-index result index) |
|---|
| 417 | (check-result-column-index! result index 'result-table-column-index) |
|---|
| 418 | (let ((idx (PQftablecol (pg-result-ptr result) index))) |
|---|
| 419 | (and (> idx 0) (sub1 idx)))) |
|---|
| 420 | |
|---|
| 421 | (define format-table |
|---|
| 422 | '((0 . text) (1 . binary))) |
|---|
| 423 | |
|---|
| 424 | (define (format->symbol format) |
|---|
| 425 | (or (alist-ref format format-table eq?) |
|---|
| 426 | (postgresql-error 'format->symbol "Unknown format" format))) |
|---|
| 427 | |
|---|
| 428 | (define (symbol->format symbol) |
|---|
| 429 | (or (and-let* ((res (rassoc symbol format-table eq?))) |
|---|
| 430 | (car res)) |
|---|
| 431 | (postgresql-error 'format->symbol "Unknown format" symbol))) |
|---|
| 432 | |
|---|
| 433 | (define (result-column-format result index) |
|---|
| 434 | (check-result-column-index! result index 'result-column-format) |
|---|
| 435 | (format->symbol (PQfformat (pg-result-ptr result) index))) |
|---|
| 436 | |
|---|
| 437 | (define (result-column-type result index) |
|---|
| 438 | (check-result-column-index! result index 'result-column-type) |
|---|
| 439 | (PQftype (pg-result-ptr result) index)) |
|---|
| 440 | |
|---|
| 441 | ;; This is really not super-useful as it requires intimate knowledge |
|---|
| 442 | ;; about the internal implementations of types in PostgreSQL. |
|---|
| 443 | (define (result-column-type-modifier result index) |
|---|
| 444 | (check-result-column-index! result index 'result-column-type) |
|---|
| 445 | (let ((mod (PQfmod (pg-result-ptr result) index))) |
|---|
| 446 | (and (>= mod 0) mod))) |
|---|
| 447 | |
|---|
| 448 | ;; Unchecked version, for speed |
|---|
| 449 | (define (result-value* result row column #!key raw) |
|---|
| 450 | (if (PQgetisnull (pg-result-ptr result) row column) |
|---|
| 451 | (sql-null) |
|---|
| 452 | (let ((value ((foreign-safe-lambda* |
|---|
| 453 | scheme-object ((c-pointer res) (int row) (int col)) |
|---|
| 454 | "C_word fin, *str; char *val; int len;" |
|---|
| 455 | "len = PQgetlength(res, row, col);" |
|---|
| 456 | "str = C_alloc(C_bytestowords(len + sizeof(C_header)));" |
|---|
| 457 | "val = PQgetvalue(res, row, col);" |
|---|
| 458 | "fin = C_string(&str, len, val);" |
|---|
| 459 | "if (PQfformat(res, col) == 1) /* binary? */" |
|---|
| 460 | " C_string_to_bytevector(fin);" |
|---|
| 461 | "C_return(fin);") |
|---|
| 462 | (pg-result-ptr result) row column))) |
|---|
| 463 | (if (or raw (blob? value)) |
|---|
| 464 | value |
|---|
| 465 | ((vector-ref (pg-result-value-parsers result) column) value))))) |
|---|
| 466 | |
|---|
| 467 | (define (result-value result row column #!key raw) |
|---|
| 468 | (check-result-row-index! result row 'result-value) |
|---|
| 469 | (check-result-column-index! result column 'result-value) |
|---|
| 470 | (result-value* result row column raw: raw)) |
|---|
| 471 | |
|---|
| 472 | (define (result-values result row #!key raw) |
|---|
| 473 | (check-result-row-index! result row 'result-list) |
|---|
| 474 | (let loop ((list '()) |
|---|
| 475 | (column (result-column-count result))) |
|---|
| 476 | (if (= column 0) |
|---|
| 477 | list |
|---|
| 478 | (loop (cons (result-value* result row (sub1 column) raw: raw) list) |
|---|
| 479 | (sub1 column))))) |
|---|
| 480 | |
|---|
| 481 | ;; (define (result-alist result row) |
|---|
| 482 | ;; (map cons (result-columns result row) (result-values result row))) |
|---|
| 483 | (define (result-alist result row) |
|---|
| 484 | (check-result-row-index! result row 'result-alist) |
|---|
| 485 | (let loop ((alist '()) |
|---|
| 486 | (column (result-column-count result))) |
|---|
| 487 | (if (= column 0) |
|---|
| 488 | alist |
|---|
| 489 | (loop (cons (cons (PQfname (pg-result-ptr result) column) |
|---|
| 490 | (result-value* result row (sub1 column))) alist) |
|---|
| 491 | (sub1 column))))) |
|---|
| 492 | |
|---|
| 493 | ;;; TODO: Do we want/need PQnparams and PQparamtype bindings? |
|---|
| 494 | |
|---|
| 495 | (define (result-affected-rows result) |
|---|
| 496 | (string->number (PQcmdTuples (pg-result-ptr result)))) |
|---|
| 497 | |
|---|
| 498 | (define (result-inserted-oid result) |
|---|
| 499 | (let ((oid (PQoidValue (pg-result-ptr result)))) |
|---|
| 500 | (and (not (= oid invalid-oid)) oid))) |
|---|
| 501 | |
|---|
| 502 | |
|---|
| 503 | ;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 504 | ;;;; Query procedures |
|---|
| 505 | ;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 506 | |
|---|
| 507 | ;; Buffer all available input, yielding if nothing is available: |
|---|
| 508 | (define (buffer-available-input! conn) |
|---|
| 509 | (let ((conn-ptr (pg-connection-ptr conn)) |
|---|
| 510 | (conn-fd (pgsql-connection->fd conn))) |
|---|
| 511 | (let loop () |
|---|
| 512 | (if (PQconsumeInput conn-ptr) |
|---|
| 513 | (when (PQisBusy conn-ptr) |
|---|
| 514 | (thread-wait-for-i/o! conn-fd #:input) |
|---|
| 515 | (loop)) |
|---|
| 516 | (postgresql-error 'buffer-available-input! |
|---|
| 517 | (conc "Error reading reply from server. " |
|---|
| 518 | (PQerrorMessage conn-ptr)) |
|---|
| 519 | conn-ptr))))) |
|---|
| 520 | |
|---|
| 521 | (define (make-value-parsers conn pqresult) |
|---|
| 522 | (let ((nfields (PQnfields pqresult))) |
|---|
| 523 | (do ([col 0 (+ col 1)] |
|---|
| 524 | [parsers (make-vector nfields)]) |
|---|
| 525 | ([= col nfields] parsers) |
|---|
| 526 | (vector-set! parsers col |
|---|
| 527 | (hash-table-ref (pg-connection-oid-parsers conn) |
|---|
| 528 | (PQftype pqresult col) |
|---|
| 529 | (lambda () identity)))))) |
|---|
| 530 | |
|---|
| 531 | ;; Collect the result pointers from the last query. |
|---|
| 532 | ;; |
|---|
| 533 | ;; A pgresult represents an entire resultset and is always read into memory |
|---|
| 534 | ;; all at once. |
|---|
| 535 | (define (collect-results conn) |
|---|
| 536 | (buffer-available-input! conn) |
|---|
| 537 | (let loop ((results (list))) |
|---|
| 538 | (let* ((conn-ptr (pg-connection-ptr conn)) |
|---|
| 539 | (result (PQgetResult conn-ptr))) |
|---|
| 540 | (if result |
|---|
| 541 | (cond |
|---|
| 542 | ((member (PQresultStatus result) (list PGRES_BAD_RESPONSE |
|---|
| 543 | PGRES_FATAL_ERROR)) |
|---|
| 544 | (let* ((msg (string-trim-right (PQresultErrorMessage result))) |
|---|
| 545 | (get-error-field (lambda (diag) |
|---|
| 546 | (PQresultErrorField result diag))) |
|---|
| 547 | (sqlstate (get-error-field PG_DIAG_SQLSTATE)) |
|---|
| 548 | (maybe-severity (get-error-field PG_DIAG_SEVERITY)) |
|---|
| 549 | (maybe-statement-position |
|---|
| 550 | (get-error-field PG_DIAG_STATEMENT_POSITION)) |
|---|
| 551 | (condition |
|---|
| 552 | (make-pg-condition |
|---|
| 553 | 'collect-results |
|---|
| 554 | (conc "PQgetResult: " msg) |
|---|
| 555 | args: (list conn) |
|---|
| 556 | severity: (and maybe-severity |
|---|
| 557 | (string->symbol |
|---|
| 558 | (string-downcase maybe-severity))) |
|---|
| 559 | error-class: (and sqlstate (string-take sqlstate 2)) |
|---|
| 560 | error-code: sqlstate |
|---|
| 561 | message-detail: (get-error-field PG_DIAG_MESSAGE_DETAIL) |
|---|
| 562 | message-hint: (get-error-field PG_DIAG_MESSAGE_HINT) |
|---|
| 563 | statement-position: (and maybe-statement-position |
|---|
| 564 | (string->number |
|---|
| 565 | maybe-statement-position)) |
|---|
| 566 | context: (get-error-field PG_DIAG_CONTEXT) |
|---|
| 567 | source-file: (get-error-field PG_DIAG_SOURCE_FILE) |
|---|
| 568 | source-line: (get-error-field PG_DIAG_SOURCE_LINE) |
|---|
| 569 | source-function: (get-error-field PG_DIAG_SOURCE_FUNCTION)))) |
|---|
| 570 | ;; Read out all remaining results (including the current one). |
|---|
| 571 | ;; TODO: Is this really needed? libpq does it (in pqExecFinish), |
|---|
| 572 | ;; but ostensibly only to concatenate the error messages for |
|---|
| 573 | ;; each query. OTOH, maybe we want to do that, too. |
|---|
| 574 | (let clean-results! ((result result)) |
|---|
| 575 | (when result |
|---|
| 576 | (PQclear result) |
|---|
| 577 | (clean-results! (PQgetResult (pg-connection-ptr conn))))) |
|---|
| 578 | (signal condition))) |
|---|
| 579 | (else |
|---|
| 580 | (let ((result-obj (make-pg-result result |
|---|
| 581 | (make-value-parsers conn result)))) |
|---|
| 582 | (set-finalizer! result-obj clear-result!) |
|---|
| 583 | (loop (cons result-obj results))))) |
|---|
| 584 | (reverse! results))))) |
|---|
| 585 | |
|---|
| 586 | (define (exec-simple-queries conn query) |
|---|
| 587 | (if ((foreign-lambda bool PQsendQuery pgconn* (const c-string)) |
|---|
| 588 | (pg-connection-ptr conn) query) |
|---|
| 589 | (collect-results conn) |
|---|
| 590 | (postgresql-error 'exec-simple-queries |
|---|
| 591 | (conc "Unable to send query to server. " |
|---|
| 592 | (PQerrorMessage (pg-connection-ptr conn))) |
|---|
| 593 | conn query))) |
|---|
| 594 | |
|---|
| 595 | (define (exec-query conn query #!optional (params '()) #!key (format 'text) raw) |
|---|
| 596 | (let* ((unparsers (pg-connection-type-unparsers conn)) |
|---|
| 597 | (unparse (lambda (x) |
|---|
| 598 | (cond ((find (lambda (parse?) |
|---|
| 599 | ((car parse?) x)) |
|---|
| 600 | unparsers) => (lambda (parse) |
|---|
| 601 | ((cdr parse) x))) |
|---|
| 602 | (else x)))) |
|---|
| 603 | (params ;; Check all params and ensure they are proper pairs |
|---|
| 604 | (map ;; See if this can be moved into C |
|---|
| 605 | (lambda (p) |
|---|
| 606 | (let ((obj (if raw p (unparse p)))) |
|---|
| 607 | (when (and (not (string? obj)) |
|---|
| 608 | (not (blob? obj)) |
|---|
| 609 | (not (sql-null? obj))) |
|---|
| 610 | (postgresql-error |
|---|
| 611 | 'exec-query |
|---|
| 612 | (sprintf "Param value is not a string, sql-null or blob: ~S" p) |
|---|
| 613 | conn query params format)) |
|---|
| 614 | (if (sql-null? obj) #f obj))) params)) |
|---|
| 615 | (send-query |
|---|
| 616 | (foreign-lambda* |
|---|
| 617 | bool ((pgconn* conn) (nonnull-c-string query) |
|---|
| 618 | (int num) (scheme-object params) (int resfmt)) |
|---|
| 619 | "int res = 0, i = 0, *lens = NULL;" |
|---|
| 620 | "char **vals = NULL;" |
|---|
| 621 | "int *fmts = NULL;" |
|---|
| 622 | "C_word obj, cons;" |
|---|
| 623 | "if (num > 0) {" |
|---|
| 624 | " vals = C_malloc(num * sizeof(char *));" |
|---|
| 625 | " lens = C_malloc(num * sizeof(int));" |
|---|
| 626 | " fmts = C_malloc(num * sizeof(int));" |
|---|
| 627 | "}" |
|---|
| 628 | "for (i=0,cons=params; i < num; ++i,cons=C_u_i_cdr(cons)) {" |
|---|
| 629 | " obj = C_u_i_car(cons);" |
|---|
| 630 | " if (obj == C_SCHEME_FALSE) {" |
|---|
| 631 | " fmts[i] = 0; /* don't care */" |
|---|
| 632 | " lens[i] = 0;" |
|---|
| 633 | " vals[i] = NULL;" |
|---|
| 634 | " } else if (C_header_bits(obj) == C_BYTEVECTOR_TYPE) {" |
|---|
| 635 | " fmts[i] = 1; /* binary */" |
|---|
| 636 | " lens[i] = C_header_size(obj);" |
|---|
| 637 | " vals[i] = C_c_string(obj);" |
|---|
| 638 | " } else {" |
|---|
| 639 | " /* text needs to be copied; it expects ASCIIZ */" |
|---|
| 640 | " fmts[i] = 0; /* text */" |
|---|
| 641 | " lens[i] = C_header_size(obj);" |
|---|
| 642 | " vals[i] = malloc(lens[i] + 1);" |
|---|
| 643 | " memcpy(vals[i], C_c_string(obj), lens[i]);" |
|---|
| 644 | " vals[i][lens[i]] = '\\0';" |
|---|
| 645 | " }" |
|---|
| 646 | "}" |
|---|
| 647 | "res = PQsendQueryParams(conn, query, num, NULL," |
|---|
| 648 | " vals, lens, fmts, resfmt);" |
|---|
| 649 | "for (i=0,cons=params; i < num; ++i,cons=C_u_i_cdr(cons)) {" |
|---|
| 650 | " obj = C_u_i_car(cons);" |
|---|
| 651 | " if (!C_immediatep(obj) && C_header_bits(obj) == C_STRING_TYPE)" |
|---|
| 652 | " free(vals[i]); /* Clear copied strings only */" |
|---|
| 653 | "}" |
|---|
| 654 | "if (num > 0) {" |
|---|
| 655 | " free(fmts);" |
|---|
| 656 | " free(lens);" |
|---|
| 657 | " free(vals);" |
|---|
| 658 | "}" |
|---|
| 659 | "C_return(res);"))) |
|---|
| 660 | (if (send-query (pg-connection-ptr conn) query |
|---|
| 661 | (length params) params (symbol->format format)) |
|---|
| 662 | (car (collect-results conn)) ;; assumed to always return one result... |
|---|
| 663 | (postgresql-error 'exec-query |
|---|
| 664 | (conc "Unable to send query to server. " |
|---|
| 665 | (PQerrorMessage (pg-connection-ptr conn))) |
|---|
| 666 | conn query params format)))) |
|---|
| 667 | |
|---|
| 668 | ;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 669 | ;;;; Value escaping |
|---|
| 670 | ;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 671 | |
|---|
| 672 | (define (escape-string conn str) |
|---|
| 673 | (define %escape-string-conn |
|---|
| 674 | ;; This could be more efficient by copying straight into a Scheme object. |
|---|
| 675 | ;; Now it's being copied by PQescapeStringConn, and Chicken copies it again. |
|---|
| 676 | ;; This can allocate up to twice as much memory than the string actually |
|---|
| 677 | ;; uses; in extreme cases this could be a problem. |
|---|
| 678 | (foreign-lambda* c-string* ((pointer conn) (c-string from) (int fromlen)) |
|---|
| 679 | "int err = 0; char *to;" |
|---|
| 680 | "to = malloc(sizeof(char) * (fromlen * 2 + 1));" |
|---|
| 681 | "PQescapeStringConn(conn, to, from, fromlen, &err);" |
|---|
| 682 | "if (err) {" |
|---|
| 683 | " free(to);" |
|---|
| 684 | " C_return(NULL);" |
|---|
| 685 | "}" |
|---|
| 686 | "C_return(to);" |
|---|
| 687 | )) |
|---|
| 688 | (or (%escape-string-conn conn str (string-length str)) |
|---|
| 689 | (postgresql-error 'escape-string |
|---|
| 690 | (conc "String escaping failed. " |
|---|
| 691 | (PQerrorMessage conn)) conn str))) |
|---|
| 692 | |
|---|
| 693 | (define (escape-bytea conn str) |
|---|
| 694 | (define %escape-bytea-conn |
|---|
| 695 | ;; This must copy because libpq returns a malloced ptr... |
|---|
| 696 | (foreign-safe-lambda* scheme-object ((pointer conn) |
|---|
| 697 | ;; not copied/NUL interpreted: |
|---|
| 698 | ((const unsigned-c-string*) from) |
|---|
| 699 | (int fromlen)) |
|---|
| 700 | "size_t tolen=0; C_word res, *fin; unsigned char *esc;" |
|---|
| 701 | "esc = PQescapeByteaConn(conn, from, (size_t)fromlen, &tolen);" |
|---|
| 702 | "if (esc == NULL)" |
|---|
| 703 | " C_return(C_SCHEME_FALSE);" |
|---|
| 704 | "fin = C_alloc(C_bytestowords(tolen + sizeof(C_header)));" |
|---|
| 705 | "/* tolen includes the resulting NUL byte */" |
|---|
| 706 | "res = C_string(&fin, tolen - 1, (char *)esc);" |
|---|
| 707 | "PQfreemem(esc);" |
|---|
| 708 | "C_return(res);" |
|---|
| 709 | )) |
|---|
| 710 | (or (%escape-bytea-conn conn str (string-length str)) |
|---|
| 711 | (postgresql-error 'escape-bytea |
|---|
| 712 | (conc "Byte array escaping failed. " |
|---|
| 713 | (PQerrorMessage conn)) conn str))) |
|---|
| 714 | |
|---|
| 715 | (define (unescape-bytea str) |
|---|
| 716 | (define %unescape-bytea |
|---|
| 717 | ;; This must copy because libpq returns a malloced ptr... |
|---|
| 718 | (foreign-safe-lambda* scheme-object (((const unsigned-c-string*) from)) |
|---|
| 719 | "size_t tolen=0; C_word res, *fin; unsigned char *unesc;" |
|---|
| 720 | "unesc = PQunescapeBytea(from, &tolen);" |
|---|
| 721 | "if (unesc == NULL)" |
|---|
| 722 | " C_return(C_SCHEME_FALSE);" |
|---|
| 723 | "fin = C_alloc(C_bytestowords(tolen + sizeof(C_header)));" |
|---|
| 724 | "res = C_string(&fin, tolen, (char *)unesc);" |
|---|
| 725 | "PQfreemem(unesc);" |
|---|
| 726 | "C_return(res);" |
|---|
| 727 | )) |
|---|
| 728 | (or (%unescape-bytea str) |
|---|
| 729 | (postgresql-error 'unescape-bytea |
|---|
| 730 | "Byte array unescaping failed (out of memory?)" str))) |
|---|
| 731 | |
|---|
| 732 | |
|---|
| 733 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 734 | ;;;; High-level interface |
|---|
| 735 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
|---|
| 736 | |
|---|
| 737 | (define (query-fold kons knil conn query #!optional (params '())) |
|---|
| 738 | (let* ((result (exec-query conn query params)) |
|---|
| 739 | (rows (result-row-count result))) |
|---|
| 740 | (let loop ((seed knil) |
|---|
| 741 | (row 0)) |
|---|
| 742 | (if (= row rows) |
|---|
| 743 | seed |
|---|
| 744 | (loop (kons (result-values result row) seed) (add1 row)))))) |
|---|
| 745 | |
|---|
| 746 | (define (query-fold-right kons knil conn query #!optional (params '())) |
|---|
| 747 | (let* ((result (exec-query conn query params)) |
|---|
| 748 | (rows (result-row-count result))) |
|---|
| 749 | (let loop ((seed knil) |
|---|
| 750 | (row 0)) |
|---|
| 751 | (if (= row rows) |
|---|
| 752 | seed |
|---|
| 753 | (kons (result-values result row) (loop seed (add1 row))))))) |
|---|
| 754 | |
|---|
| 755 | (define (query-fold* kons knil conn query #!optional (params '())) |
|---|
| 756 | (query-fold (lambda (values seed) (apply kons (append values (list seed)))) |
|---|
| 757 | knil conn query params)) |
|---|
| 758 | (define (query-fold-right* kons knil conn query #!optional (params '())) |
|---|
| 759 | (query-fold-right (lambda (val seed) (apply kons (append val (list seed)))) |
|---|
| 760 | knil conn query params)) |
|---|
| 761 | |
|---|
| 762 | (define (query-for-each proc conn query #!optional (params '())) |
|---|
| 763 | (query-fold (lambda (values seed) (proc values)) #f conn query params) |
|---|
| 764 | (void)) |
|---|
| 765 | |
|---|
| 766 | (define (query-for-each* proc conn query #!optional (params '())) |
|---|
| 767 | (query-fold (lambda (values seed) (apply proc values)) #f conn query params) |
|---|
| 768 | (void)) |
|---|
| 769 | |
|---|
| 770 | ;; Like regular Scheme map, the order in which the procedure is applied is |
|---|
| 771 | ;; undefined. We make good use of that by traversing the resultset from |
|---|
| 772 | ;; the end back to the beginning, thereby avoiding a reverse! on the result. |
|---|
| 773 | (define (query-map proc conn query #!optional (params '())) |
|---|
| 774 | (let ((result (exec-query conn query params))) |
|---|
| 775 | (let loop ((output '()) |
|---|
| 776 | (row (result-row-count result))) |
|---|
| 777 | (if (= row 0) |
|---|
| 778 | output |
|---|
| 779 | (loop (cons (proc (result-values result (sub1 row))) output) |
|---|
| 780 | (sub1 row)))))) |
|---|
| 781 | |
|---|
| 782 | (define (query-map* proc conn query #!optional (params '())) |
|---|
| 783 | (query-map (lambda (values seed) (apply proc values)) #f conn query params)) |
|---|
| 784 | |
|---|
| 785 | ) |
|---|