1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
|
;;; squee --- A guile interface to postgres via the ffi
;; Copyright (C) 2015 Christopher Allan Webber <cwebber@dustycloud.org>
;; This library is free software; you can redistribute it and/or
;; modify it under the terms of the GNU Lesser General Public
;; License as published by the Free Software Foundation; either
;; version 3 of the License, or (at your option) any later version.
;;
;; This library is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; Lesser General Public License for more details.
;;
;; You should have received a copy of the GNU Lesser General Public
;; License along with this library; if not, write to the Free Software
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
(define-module (squee)
#:use-module (system foreign)
#:use-module (rnrs enums)
#:use-module (ice-9 match)
#:use-module (ice-9 format)
#:use-module (srfi srfi-26)
#:export (;; The important ones
connect-to-postgres-paramstring
exec-query
pg-conn-finish
;; enums and indexes of enums
conn-status-enum conn-status-enum-index
polling-status-enum polling-status-index
exec-status-enum exec-status-enum-index
transaction-status-enum transaction-status-enum-index
verbosity-enum verbosity-enum-index
ping-enum ping-enum-index
;; **repl and error messages only!**
enum-set-ref
;; Connection stuff
<pg-conn> pg-conn? wrap-pg-conn unwrap-pg-conn
;; @@: We don't export the result pointer though!
;; as this needs to be cleared to avoid memory
;; leaks...
;;
;; We might provide a (exec-with-result-ptr)
;; that cleans up the result pointer after calling
;; some thunk though?
;;
;; These are still useful for building your own
;; serializer though...
result-num-rows result-num-cols result-get-value
result-serializer-simple-list result-metadata))
(define libpq (dynamic-link "libpq"))
;; ---------------------
;; Enums from libpq-fe.h
;; ---------------------
(define conn-status-enum
(make-enumeration
'(connection-ok
connection-bad
connection-started connection-made
connection-awaiting-response connection-auth-ok
connection-auth-ok connection-setenv
connection-ssl-startup
connection-needed)))
(define conn-status-enum-index
(enum-set-indexer conn-status-enum))
(define polling-status-enum
(make-enumeration
'(polling-failed
polling-reading
polling-writing
polling-ok
polling-active)))
(define polling-status-enum-index
(enum-set-indexer polling-status-enum))
(define exec-status-enum
(make-enumeration
'(empty-query
command-ok tuples-ok
copy-out copy-in
bad-response
nonfatal-error fatal-error
copy-both
single-tuple)))
(define exec-status-enum-index
(enum-set-indexer exec-status-enum))
(define transaction-status-enum
(make-enumeration
'(idle active intrans inerror unknown)))
(define transaction-status-enum-index
(enum-set-indexer transaction-status-enum))
(define verbosity-enum
(make-enumeration
'(terse default verbose)))
(define verbosity-enum-index
(enum-set-indexer verbosity-enum))
(define ping-enum
(make-enumeration
'(ok reject no-response no-attempt)))
(define ping-enum-index
(enum-set-indexer ping-enum))
(define-wrapped-pointer-type <pg-conn>
pg-conn?
wrap-pg-conn unwrap-pg-conn
(lambda (pg-conn port)
(format port "#<pg-conn ~x (~a)>"
(pointer-address (unwrap-pg-conn pg-conn))
(let ((status (pg-conn-status pg-conn)))
(cond ((eq? status (conn-status-enum-index 'connection-ok))
"connected")
((eq? status (conn-status-enum-index 'connection-bad))
(let ((conn-error (pg-conn-error-message pg-conn)))
(if (equal? conn-error "")
"disconnected"
(format #f "disconnected, error: ~s" conn-error))))
(#t
(symbol->string
(pg-conn-status-symbol pg-conn))))))))
;; This one should NOT be exposed to the outside world! We have our
;; own result structure...
(define-wrapped-pointer-type <result-ptr>
result-ptr?
wrap-result-ptr unwrap-result-ptr
(lambda (result-ptr port)
(format port "#<result-ptr ~x>"
(pointer-address (unwrap-result-ptr result-ptr)))))
(define (enum-set-ref enum-set k)
"Take an ENUM-SET and get the item at position K
This is O(n) but theoretically we don't use it much.
Again, REPL only!"
(list-ref (enum-set->list enum-set) k))
(define-syntax-rule (define-foreign-libpq name return_type func_name arg_types)
(define name
(pointer->procedure return_type
(dynamic-func func_name libpq)
arg_types)))
(define-foreign-libpq %PQconnectdb '* "PQconnectdb" (list '*))
(define-foreign-libpq %PQstatus int "PQstatus" (list '*))
(define-foreign-libpq %PQerrorMessage '* "PQerrorMessage" (list '*))
(define-foreign-libpq %PQfinish void "PQfinish" (list '*))
(define-foreign-libpq %PQntuples int "PQntuples" (list '*))
(define-foreign-libpq %PQnfields int "PQnfields" (list '*))
(define-foreign-libpq %PQexec '* "PQexec" (list '* '*))
(define-foreign-libpq %PQexecParams
'* ;; Returns a PGresult
"PQexecParams"
(list '* ;; connection
'* ;; command, a string
int ;; number of parameters
'* ;; paramTypes, ok to leave NULL
'* ;; paramValues, here goes your actual parameters!
'* ;; paramLengths, ok to leave NULL
'* ;; paramFormats, ok to leave NULL
int)) ;; resultFormat... probably 0!
(define-foreign-libpq %PQresultStatus int "PQresultStatus" (list '*))
(define-foreign-libpq %PQresStatus '* "PQresStatus" (list int))
(define-foreign-libpq %PQresultErrorMessage '* "PQresultErrorMessage" (list '*))
(define-foreign-libpq %PQclear void "PQclear" (list '*))
(define-foreign-libpq %PQcmdtuples '* "PQcmdTuples" (list '*))
(define-foreign-libpq %PQntuples int "PQntuples" (list '*))
(define-foreign-libpq %PQnfields int "PQnfields" (list '*))
(define-foreign-libpq %PQgetisnull int "PQgetisnull" (list '* int int))
(define-foreign-libpq %PQgetvalue '* "PQgetvalue" (list '* int int))
;; Via mark_weaver. Thanks Mark!
;;
;; So, apparently we can use a struct of strings just like an array
;; of strings. Because magic, and because Mark thinks the C standard
;; allows it enough!
(define (string-pointer-list->string-array ls)
"Take a list of strings, generate a C-compatible list of free strings"
(make-c-struct
(make-list (+ 1 (length ls)) '*)
(append ls (list %null-pointer))))
(define (pg-conn-status pg-conn)
"Get the connection status from a postgres connection"
(%PQstatus (unwrap-pg-conn pg-conn)))
(define (pg-conn-status-symbol pg-conn)
"Human readable version of the pg-conn status.
Inefficient... don't use this in normal code... it's just for you and
the REPL! (Well, we do use it for errors, because those are
comparatively \"rare\" so this is okay.) Compare against the enum
value of the symbol instead."
(let ((status (pg-conn-status pg-conn)))
(if (< status (length (enum-set->list conn-status-enum)))
(enum-set-ref conn-status-enum
(pg-conn-status pg-conn))
;; Weird, this is bigger than our enum of statuses
(string->symbol
(format #f "unknown-status-~a" status)))))
(define (pg-conn-error-message pg-conn)
"Get an error message for this connection"
(pointer->string (%PQerrorMessage (unwrap-pg-conn pg-conn))))
(define (pg-conn-finish pg-conn)
"Close out a database connection.
If the connection is already closed, this simply returns #f."
(if (eq? (pg-conn-status pg-conn)
(conn-status-enum-index 'connection-ok))
(begin
(%PQfinish (unwrap-pg-conn pg-conn))
#t)
#f))
(define (connect-to-postgres-paramstring paramstring)
"Open a connection to the database via a parameter string"
(let* ((conn-pointer (%PQconnectdb (string->pointer paramstring)))
(pg-conn (wrap-pg-conn conn-pointer)))
(if (eq? conn-pointer %null-pointer)
(throw 'psql-connect-error
#f "Unable to establish connection"))
(let ((status (pg-conn-status pg-conn)))
(if (eq? status (conn-status-enum-index 'connection-ok))
pg-conn
(throw 'psql-connect-error
(enum-set-ref conn-status-enum status)
(pg-conn-error-message pg-conn))))))
(define (result-num-rows result-ptr)
(%PQntuples (unwrap-result-ptr result-ptr)))
(define (result-num-cols result-ptr)
(%PQnfields (unwrap-result-ptr result-ptr)))
(define (result-get-value result-ptr row col)
(let ((res (unwrap-result-ptr result-ptr)))
(and (eqv? (%PQgetisnull res row col) 0)
(pointer->string
(%PQgetvalue res row col)))))
;; @@: We ought to also have a vector version...
;; and other serializations...
(define (result-serializer-simple-list result-ptr)
"Get a simple list of lists representing the result of the query"
(let ((rows-range (iota (result-num-rows result-ptr)))
(cols-range (iota (result-num-cols result-ptr))))
(map
(lambda (row-i)
(map
(lambda (col-i)
(result-get-value result-ptr row-i col-i))
cols-range))
rows-range)))
;; TODO
(define (result-metadata result-ptr)
#f)
(define (result-ptr-clear result-ptr)
(%PQclear (unwrap-result-ptr result-ptr)))
(define (result-error-message result-ptr)
(%PQresultErrorMessage (unwrap-result-ptr result-ptr)))
(define* (exec-query pg-conn command #:optional (params '())
#:key (serializer result-serializer-simple-list))
(let* ((param-pointers
(map (lambda (param)
(if param
(string->pointer param)
%null-pointer))
params))
(command-pointer
(string->pointer command))
(param-array-pointer
(string-pointer-list->string-array param-pointers))
(result-ptr
(wrap-result-ptr
(if (null? params)
(%PQexec
(unwrap-pg-conn pg-conn)
command-pointer)
(%PQexecParams
(unwrap-pg-conn pg-conn)
command-pointer
(length params)
%null-pointer
param-array-pointer
%null-pointer %null-pointer 0)))))
;; Protect the pointers, and thus the memory regions they point to
;; from garbage collection, until %PQexecParams has returned
(identity param-pointers)
(identity command-pointer)
(identity param-array-pointer)
(if (eq? result-ptr %null-pointer)
;; Presumably a database connection issue...
(throw 'psql-query-error
;; See below for psql-query-error param definition
#f #f (pg-conn-error-message pg-conn)))
(let ((status (%PQresultStatus (unwrap-result-ptr result-ptr))))
(cond
;; This is the kind of query that returns tuples
((eq? status (exec-status-enum-index 'tuples-ok))
(let ((serialized-result (serializer result-ptr))
(metadata (result-metadata result-ptr)))
;; Gotta clear the result to prevent memory leaks
(result-ptr-clear result-ptr)
(values serialized-result metadata)))
;; This doesn't return tuples, eg it's a DELETE or something.
((eq? status (exec-status-enum-index 'command-ok))
(let ((metadata (result-metadata result-ptr))
(rows (%PQcmdtuples (unwrap-result-ptr result-ptr))))
;; Gotta clear the result to prevent memory leaks
(result-ptr-clear result-ptr)
;; Return the number of affected rows.
(values (string->number
(pointer->string rows)) metadata)))
;; Uhoh, anything else is an error!
(#t
(let ((status-message (pointer->string (%PQresStatus status)))
(error-message (pointer->string
(%PQresultErrorMessage (unwrap-result-ptr
result-ptr)))))
(result-ptr-clear result-ptr)
(throw 'psql-query-error
;; @@: Do we need result-status?
;; (error-symbol result-status result-error-message)
(enum-set-ref exec-status-enum status)
status-message error-message)))))))
;; (define conn (connect-to-postgres-paramstring "dbname=sandbox"))
|