squee.scm 13 KB

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