squee.scm 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 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. #:use-module (squee enum)
  23. #:export (;; The important ones
  24. connect-to-postgres-paramstring
  25. exec-query
  26. ;; Connection stuff
  27. <pg-conn> pg-conn? wrap-pg-conn unwrap-pg-conn
  28. ;; @@: We don't export the result pointer though!
  29. ;; as this needs to be cleared to avoid memory
  30. ;; leaks...
  31. ;;
  32. ;; We might provide a (exec-with-result-ptr)
  33. ;; that cleans up the result pointer after calling
  34. ;; some thunk though?
  35. ;;
  36. ;; These are still useful for building your own
  37. ;; serializer though...
  38. result-num-rows result-num-cols result-get-value
  39. result-serializer-simple-list result-metadata))
  40. (define %libpq (dynamic-link "libpq"))
  41. (define-wrapped-pointer-type <pg-conn>
  42. pg-conn?
  43. wrap-pg-conn unwrap-pg-conn
  44. (lambda (pg-conn port)
  45. (format port "#<pg-conn ~x (~a)>"
  46. (pointer-address (unwrap-pg-conn pg-conn))
  47. (let ((status (pg-conn-status pg-conn)))
  48. (cond ((eq? status (conn-status-enum-index 'connection-ok))
  49. "connected")
  50. ((eq? status (conn-status-enum-index 'connection-bad))
  51. (let ((conn-error (pg-conn-error-message pg-conn)))
  52. (if (equal? conn-error "")
  53. "disconnected"
  54. (format #f "disconnected, error: ~s" conn-error))))
  55. (#t
  56. (symbol->string
  57. (pg-conn-status-symbol pg-conn))))))))
  58. ;; This one should NOT be exposed to the outside world! We have our
  59. ;; own result structure...
  60. (define-wrapped-pointer-type <result-ptr>
  61. result-ptr?
  62. wrap-result-ptr unwrap-result-ptr
  63. (lambda (result-ptr port)
  64. (format port "#<result-ptr ~x>"
  65. (pointer-address (unwrap-result-ptr result-ptr)))))
  66. (define-syntax-rule (define-foreign-libpq name return_type func_name arg_types)
  67. (define name
  68. (pointer->procedure return_type
  69. (dynamic-func func_name %libpq)
  70. arg_types)))
  71. (define-foreign-libpq %PQconnectdb '* "PQconnectdb" (list '*))
  72. (define-foreign-libpq %PQstatus int "PQstatus" (list '*))
  73. (define-foreign-libpq %PQerrorMessage '* "PQerrorMessage" (list '*))
  74. (define-foreign-libpq %PQfinish void "PQfinish" (list '*))
  75. (define-foreign-libpq %PQntuples int "PQntuples" (list '*))
  76. (define-foreign-libpq %PQnfields int "PQnfields" (list '*))
  77. (define-foreign-libpq %PQexecParams
  78. '* ;; Returns a PGresult
  79. "PQexecParams"
  80. (list '* ;; connection
  81. '* ;; command, a string
  82. int ;; number of parameters
  83. '* ;; paramTypes, ok to leave NULL
  84. '* ;; paramValues, here goes your actual parameters!
  85. '* ;; paramLengths, ok to leave NULL
  86. '* ;; paramFormats, ok to leave NULL
  87. int)) ;; resultFormat... probably 0!
  88. (define-foreign-libpq %PQresultStatus int "PQresultStatus" (list '*))
  89. (define-foreign-libpq %PQresStatus '* "PQresStatus" (list int))
  90. (define-foreign-libpq %PQresultErrorMessage '* "PQresultErrorMessage" (list '*))
  91. (define-foreign-libpq %PQclear void "PQclear" (list '*))
  92. (define-foreign-libpq %PQntuples int "PQntuples" (list '*))
  93. (define-foreign-libpq %PQnfields int "PQnfields" (list '*))
  94. (define-foreign-libpq %PQgetvalue '* "PQgetvalue" (list '* int int))
  95. ;; Via mark_weaver. Thanks Mark!
  96. ;;
  97. ;; So, apparently we can use a struct of strings just like an array
  98. ;; of strings. Because magic, and because Mark thinks the C standard
  99. ;; allows it enough!
  100. (define (string-list->string-array ls)
  101. "Take a list of strings, generate a C-compatible list of free strings"
  102. (make-c-struct
  103. (make-list (+ 1 (length ls)) '*)
  104. (append (map string->pointer ls)
  105. (list %null-pointer))))
  106. (define (pg-conn-status pg-conn)
  107. "Get the connection status from a postgres connection"
  108. (%PQstatus (unwrap-pg-conn pg-conn)))
  109. (define (pg-conn-status-symbol pg-conn)
  110. "Human readable version of the pg-conn status.
  111. Inefficient... don't use this in normal code... it's just for you and
  112. the REPL! (Well, we do use it for errors, because those are
  113. comparatively \"rare\" so this is okay.) Compare against the enum
  114. value of the symbol instead."
  115. (let ((status (pg-conn-status pg-conn)))
  116. (if (< status (length (enum-set->list conn-status-enum)))
  117. (enum-set-ref conn-status-enum
  118. (pg-conn-status pg-conn))
  119. ;; Weird, this is bigger than our enum of statuses
  120. (string->symbol
  121. (format #f "unknown-status-~a" status)))))
  122. (define (pg-conn-error-message pg-conn)
  123. "Get an error message for this connection"
  124. (pointer->string (%PQerrorMessage (unwrap-pg-conn pg-conn))))
  125. (define (pg-conn-finish pg-conn)
  126. "Close out a database connection.
  127. If the connection is already closed, this simply returns #f."
  128. (if (eq? (pg-conn-status pg-conn)
  129. (conn-status-enum-index 'connection-ok))
  130. (begin
  131. (%PQfinish (unwrap-pg-conn pg-conn))
  132. #t)
  133. #f))
  134. (define (connect-to-postgres-paramstring paramstring)
  135. "Open a connection to the database via a parameter string"
  136. (let* ((conn-pointer (%PQconnectdb (string->pointer paramstring)))
  137. (pg-conn (wrap-pg-conn conn-pointer)))
  138. (if (eq? conn-pointer %null-pointer)
  139. (throw 'psql-connect-error
  140. #f "Unable to establish connection"))
  141. (let ((status (pg-conn-status pg-conn)))
  142. (if (eq? status (conn-status-enum-index 'connection-ok))
  143. pg-conn
  144. (throw 'psql-connect-error
  145. (enum-set-ref conn-status-enum status)
  146. (pg-conn-error-message pg-conn))))))
  147. (define (result-num-rows result-ptr)
  148. (%PQntuples (unwrap-result-ptr result-ptr)))
  149. (define (result-num-cols result-ptr)
  150. (%PQnfields (unwrap-result-ptr result-ptr)))
  151. (define (result-get-value result-ptr row col)
  152. (pointer->string
  153. (%PQgetvalue (unwrap-result-ptr result-ptr) row col)))
  154. ;; @@: We ought to also have a vector version...
  155. ;; and other serializations...
  156. (define (result-serializer-simple-list result-ptr)
  157. "Get a simple list of lists representing the result of the query"
  158. (let ((rows-range (iota (result-num-rows result-ptr)))
  159. (cols-range (iota (result-num-cols result-ptr))))
  160. (map
  161. (lambda (row-i)
  162. (map
  163. (lambda (col-i)
  164. (result-get-value result-ptr row-i col-i))
  165. cols-range))
  166. rows-range)))
  167. ;; TODO
  168. (define (result-metadata result-ptr)
  169. #f)
  170. (define (result-ptr-clear result-ptr)
  171. (%PQclear (unwrap-result-ptr result-ptr)))
  172. (define (result-error-message result-ptr)
  173. (%PQresultErrorMessage (unwrap-result-ptr result-ptr)))
  174. (define* (exec-query pg-conn command #:optional (params '())
  175. #:key (serializer result-serializer-simple-list))
  176. (let ((result-ptr
  177. (wrap-result-ptr
  178. (%PQexecParams
  179. (unwrap-pg-conn pg-conn)
  180. (string->pointer command)
  181. (length params)
  182. %null-pointer
  183. (string-list->string-array params)
  184. %null-pointer %null-pointer 0))))
  185. (if (eq? result-ptr %null-pointer)
  186. ;; Presumably a database connection issue...
  187. (throw 'psql-query-error
  188. ;; See below for psql-query-error param definition
  189. #f #f (pg-conn-error-message pg-conn)))
  190. (let ((status (%PQresultStatus (unwrap-result-ptr result-ptr))))
  191. (cond
  192. ;; This is the kind of query that returns tuples
  193. ((eq? status (exec-status-enum-index 'tuples-ok))
  194. (let ((serialized-result (serializer result-ptr))
  195. (metadata (result-metadata result-ptr)))
  196. ;; Gotta clear the result to prevent memory leaks
  197. (result-ptr-clear result-ptr)
  198. (values serialized-result metadata)))
  199. ;; This doesn't return tuples, eg it's a DELETE or something.
  200. ((eq? status (exec-status-enum-index 'command-ok))
  201. (let ((metadata (result-metadata result-ptr)))
  202. ;; Gotta clear the result to prevent memory leaks
  203. (result-ptr-clear result-ptr)
  204. ;; Just return #t if there's no tuples to look at
  205. (values #t metadata)))
  206. ;; Uhoh, anything else is an error!
  207. (#t
  208. (let ((status-message (pointer->string (%PQresStatus status)))
  209. (error-message (pointer->string
  210. (%PQresultErrorMessage (unwrap-result-ptr
  211. result-ptr)))))
  212. (result-ptr-clear result-ptr)
  213. (throw 'psql-query-error
  214. ;; @@: Do we need result-status?
  215. ;; (error-symbol result-status result-error-message)
  216. (enum-set-ref exec-status-enum status)
  217. status-message error-message)))))))
  218. ;; (define conn (connect-to-postgres-paramstring "dbname=sandbox"))