squee.scm 16 KB

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