db: various fixes and additions
- fix connection-pool for nested tx, fix race condition - ensure connected? always nonblocking - added and reorganized some doc sections - added grouping, contracts to in-query - added rows->dict
This commit is contained in:
parent
aa0d8aaa33
commit
00fd18bc62
|
@ -28,8 +28,9 @@
|
||||||
(but this could also be done by two locks: outer "ownership" lock
|
(but this could also be done by two locks: outer "ownership" lock
|
||||||
and inner "invariant-protecting" lock)
|
and inner "invariant-protecting" lock)
|
||||||
|
|
||||||
- audit code for break-safety, disable breaks as needed
|
|
||||||
|
|
||||||
- make implementation notes section of docs
|
- make implementation notes section of docs
|
||||||
- explain cursor impl (& rationale)
|
|
||||||
- explain nested tx impl
|
- explain nested tx impl
|
||||||
|
|
||||||
|
- invalidate statement cache on query error
|
||||||
|
|
||||||
|
- 2 call-with-transactions from separate threads can conflict
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/contract/base)
|
(require (for-syntax racket/base
|
||||||
|
syntax/parse
|
||||||
|
syntax/parse/experimental/template)
|
||||||
|
racket/dict
|
||||||
|
syntax/location
|
||||||
|
racket/contract/base)
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
|
|
||||||
|
@ -74,6 +79,56 @@
|
||||||
|
|
||||||
(require "private/generic/functions.rkt")
|
(require "private/generic/functions.rkt")
|
||||||
|
|
||||||
|
(define fetch-size/c
|
||||||
|
(or/c exact-positive-integer? +inf.0))
|
||||||
|
|
||||||
|
(define grouping-field/c (or/c string? exact-nonnegative-integer?))
|
||||||
|
(define group/c (or/c grouping-field/c (vectorof grouping-field/c)))
|
||||||
|
(define grouping/c (or/c group/c (listof group/c)))
|
||||||
|
|
||||||
|
(define group-mode/c
|
||||||
|
(listof (or/c 'list 'preserve-null)))
|
||||||
|
|
||||||
|
(define in-query/c
|
||||||
|
(->* (connection? statement?)
|
||||||
|
(#:fetch fetch-size/c
|
||||||
|
#:group grouping/c
|
||||||
|
#:group-mode group-mode/c)
|
||||||
|
#:rest list?
|
||||||
|
sequence?))
|
||||||
|
|
||||||
|
(define here-mod-path (quote-module-path))
|
||||||
|
|
||||||
|
(define-syntax contracted-in-query
|
||||||
|
(make-provide/contract-transformer
|
||||||
|
(quote-syntax in-query/c)
|
||||||
|
(quote-syntax in-query)
|
||||||
|
(quote-syntax in-query)
|
||||||
|
(quote-syntax here-mod-path)))
|
||||||
|
|
||||||
|
(define-sequence-syntax in-query*
|
||||||
|
(lambda () #'contracted-in-query)
|
||||||
|
(lambda (stx)
|
||||||
|
(syntax-parse stx
|
||||||
|
[[(var ...) (~and form
|
||||||
|
(in-query (~or (~optional (~seq #:fetch fetch-size))
|
||||||
|
(~optional (~seq #:group grouping-fields))
|
||||||
|
(~optional (~seq #:group-mode group-mode))
|
||||||
|
(~between arg:expr 2 +inf.0))
|
||||||
|
...))]
|
||||||
|
#:declare fetch-size (expr/c #'fetch-size/c #:context #'form) #:role "fetch size argument"
|
||||||
|
#:declare grouping-fields (expr/c #'grouping/c #:context #'form) #:role "grouping fields argument"
|
||||||
|
#:declare group-mode (expr/c #'group-mode/c #:context #'form) #:role "group mode argument"
|
||||||
|
#:with (c stmt q-arg ...) #'(arg ...)
|
||||||
|
#:declare c (expr/c #'connection? #:context #'form) #:role "connection argument"
|
||||||
|
#:declare stmt (expr/c #'statement? #:context #'form) #:role "statement argument"
|
||||||
|
(template
|
||||||
|
[(var ...) (in-query-helper (length '(var ...)) c.c stmt.c q-arg ...
|
||||||
|
(?? (?@ #:fetch fetch-size.c))
|
||||||
|
(?? (?@ #:group grouping-fields.c))
|
||||||
|
(?? (?@ #:group-mode group-mode.c)))])]
|
||||||
|
[_ #f])))
|
||||||
|
|
||||||
(provide (rename-out [in-query* in-query]))
|
(provide (rename-out [in-query* in-query]))
|
||||||
|
|
||||||
(provide/contract
|
(provide/contract
|
||||||
|
@ -105,7 +160,8 @@
|
||||||
(->* (connection? statement?) () #:rest list? any)]
|
(->* (connection? statement?) () #:rest list? any)]
|
||||||
[query-rows
|
[query-rows
|
||||||
(->* (connection? statement?)
|
(->* (connection? statement?)
|
||||||
(#:group (or/c (vectorof string?) (listof (vectorof string?))))
|
(#:group grouping/c
|
||||||
|
#:group-mode group-mode/c)
|
||||||
#:rest list? (listof vector?))]
|
#:rest list? (listof vector?))]
|
||||||
[query-list
|
[query-list
|
||||||
(->* (connection? statement?) () #:rest list? list?)]
|
(->* (connection? statement?) () #:rest list? list?)]
|
||||||
|
@ -165,9 +221,15 @@
|
||||||
|
|
||||||
[group-rows
|
[group-rows
|
||||||
(->* (rows-result?
|
(->* (rows-result?
|
||||||
#:group (or/c (vectorof string?) (listof (vectorof string?))))
|
#:group grouping/c)
|
||||||
(#:group-mode (listof (or/c 'preserve-null-rows 'list)))
|
(#:group-mode (listof (or/c 'list 'preserve-null #|deprecated:|# 'preserve-null-rows)))
|
||||||
rows-result?)])
|
rows-result?)]
|
||||||
|
|
||||||
|
[rows->dict
|
||||||
|
(->* (rows-result? #:key grouping/c #:value grouping/c)
|
||||||
|
(#:value-mode group-mode/c)
|
||||||
|
dict?)]
|
||||||
|
)
|
||||||
|
|
||||||
;; ============================================================
|
;; ============================================================
|
||||||
|
|
||||||
|
|
|
@ -421,12 +421,33 @@
|
||||||
dprintf)
|
dprintf)
|
||||||
(super-new)
|
(super-new)
|
||||||
|
|
||||||
(field [max-cache-size 50])
|
|
||||||
|
|
||||||
;; Statement Cache
|
;; Statement Cache
|
||||||
;; updated by prepare; potentially invalidated by query (via check/invalidate-cache)
|
;; updated by prepare; potentially invalidated by query (via check/invalidate-cache)
|
||||||
|
|
||||||
(define pst-cache '#hash())
|
(field [pst-cache '#hash()]
|
||||||
|
[cache-mode 'in-transaction]
|
||||||
|
[cache-flush-next? #f] ;; flush cache on next query
|
||||||
|
[max-cache-size 20])
|
||||||
|
|
||||||
|
(define/private (use-cache?)
|
||||||
|
(and (not cache-flush-next?)
|
||||||
|
(case cache-mode
|
||||||
|
((always) #t)
|
||||||
|
((never) #f)
|
||||||
|
((in-transaction) (eq? (get-tx-status) #t)))))
|
||||||
|
|
||||||
|
(define/public (stmt-cache-ctl who mode)
|
||||||
|
(case mode
|
||||||
|
((get) cache-mode)
|
||||||
|
((flush) (begin (set! cache-flush-next? #t) cache-mode))
|
||||||
|
(else (unless (eq? mode cache-mode)
|
||||||
|
(call-with-lock who
|
||||||
|
(lambda ()
|
||||||
|
(set! cache-mode mode)
|
||||||
|
(set! cache-flush-next? #t)
|
||||||
|
cache-mode))))))
|
||||||
|
|
||||||
|
;; --
|
||||||
|
|
||||||
(define/public (get-cached-statement stmt)
|
(define/public (get-cached-statement stmt)
|
||||||
(let ([cached-pst (hash-ref pst-cache stmt #f)])
|
(let ([cached-pst (hash-ref pst-cache stmt #f)])
|
||||||
|
@ -447,12 +468,6 @@
|
||||||
(dprintf " ** caching statement\n")
|
(dprintf " ** caching statement\n")
|
||||||
(set! pst-cache (hash-set pst-cache sql pst))))))
|
(set! pst-cache (hash-set pst-cache sql pst))))))
|
||||||
|
|
||||||
(define/private (use-cache?)
|
|
||||||
(case cache-statements
|
|
||||||
((always) #t)
|
|
||||||
((never) #f)
|
|
||||||
((in-transaction) (eq? (get-tx-status) #t))))
|
|
||||||
|
|
||||||
;; check/invalidate-cache : statement/pst -> hash/#f
|
;; check/invalidate-cache : statement/pst -> hash/#f
|
||||||
;; Returns old cache on invalidation, or #f if stmt is safe.
|
;; Returns old cache on invalidation, or #f if stmt is safe.
|
||||||
;; May also return part of old cache (excluding pst) when cache gets too big.
|
;; May also return part of old cache (excluding pst) when cache gets too big.
|
||||||
|
@ -463,7 +478,11 @@
|
||||||
unsafe, because they're usually transactional SQL.
|
unsafe, because they're usually transactional SQL.
|
||||||
|#
|
|#
|
||||||
(define (invalidate! except)
|
(define (invalidate! except)
|
||||||
(dprintf " ** invalidating statement cache~a\n" (if except " (too big)" ""))
|
;; FIXME: smarter cache ejection (LRU?)
|
||||||
|
(dprintf " ** invalidating statement cache~a\n"
|
||||||
|
(cond [except " (too big)"]
|
||||||
|
[cache-flush-next? " (mode changed)"]
|
||||||
|
[else ""]))
|
||||||
(let ([cache pst-cache])
|
(let ([cache pst-cache])
|
||||||
(set! pst-cache '#hash())
|
(set! pst-cache '#hash())
|
||||||
(cond [except
|
(cond [except
|
||||||
|
@ -471,7 +490,9 @@
|
||||||
(hash-remove cache (send except get-stmt))]
|
(hash-remove cache (send except get-stmt))]
|
||||||
[else
|
[else
|
||||||
cache])))
|
cache])))
|
||||||
(cond [(statement-binding? x)
|
(cond [cache-flush-next?
|
||||||
|
(invalidate! #f)]
|
||||||
|
[(statement-binding? x)
|
||||||
(check/invalidate-cache (statement-binding-pst x))]
|
(check/invalidate-cache (statement-binding-pst x))]
|
||||||
[(prepared-statement? x)
|
[(prepared-statement? x)
|
||||||
(let ([stmt-type (send x get-stmt-type)])
|
(let ([stmt-type (send x get-stmt-type)])
|
||||||
|
|
|
@ -13,33 +13,49 @@
|
||||||
(class object%
|
(class object%
|
||||||
;; other-evt : (-> evt)
|
;; other-evt : (-> evt)
|
||||||
;; generates other evt to sync on besides req-channel, eg timeouts
|
;; generates other evt to sync on besides req-channel, eg timeouts
|
||||||
(init-field (other-evt (lambda () never-evt)))
|
(init-field (other-evt (lambda () never-evt))
|
||||||
|
(alt-enabled? (lambda () #t)))
|
||||||
(super-new)
|
(super-new)
|
||||||
|
|
||||||
(define req-channel (make-channel))
|
(define req-channel (make-channel))
|
||||||
|
(define alt-req-channel (make-channel))
|
||||||
|
|
||||||
(define mthread
|
(define mthread
|
||||||
(thread/suspend-to-kill
|
(thread/suspend-to-kill
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let loop ()
|
(let loop ()
|
||||||
(sync (wrap-evt req-channel (lambda (p) (p)))
|
(sync (wrap-evt req-channel (lambda (p) (p)))
|
||||||
|
(if (alt-enabled?)
|
||||||
|
(wrap-evt alt-req-channel (lambda (p) (p)))
|
||||||
|
never-evt)
|
||||||
(other-evt))
|
(other-evt))
|
||||||
(loop)))))
|
(loop)))))
|
||||||
|
|
||||||
(define/public (call proc)
|
(define/public (call proc)
|
||||||
|
(call* proc req-channel #f))
|
||||||
|
(define/public (alt-call-evt proc)
|
||||||
|
(call* proc alt-req-channel #t))
|
||||||
|
|
||||||
|
(define/private (call* proc chan as-evt?)
|
||||||
(thread-resume mthread (current-thread))
|
(thread-resume mthread (current-thread))
|
||||||
(let ([result #f]
|
(let* ([result #f]
|
||||||
[sema (make-semaphore 0)])
|
[sema (make-semaphore 0)]
|
||||||
(channel-put req-channel
|
[proc (lambda ()
|
||||||
(lambda ()
|
(set! result
|
||||||
(set! result
|
(with-handlers ([(lambda (e) #t)
|
||||||
(with-handlers ([(lambda (e) #t)
|
(lambda (e) (cons 'exn e))])
|
||||||
(lambda (e) (cons 'exn e))])
|
(cons 'values (call-with-values proc list))))
|
||||||
(cons 'values (call-with-values proc list))))
|
(semaphore-post sema))]
|
||||||
(semaphore-post sema)))
|
[handler
|
||||||
(semaphore-wait sema)
|
(lambda (_evt)
|
||||||
(case (car result)
|
(semaphore-wait sema)
|
||||||
((values) (apply values (cdr result)))
|
(case (car result)
|
||||||
((exn) (raise (cdr result))))))))
|
((values) (apply values (cdr result)))
|
||||||
|
((exn) (raise (cdr result)))))])
|
||||||
|
(if as-evt?
|
||||||
|
(wrap-evt (channel-put-evt chan proc) handler)
|
||||||
|
(begin (channel-put chan proc)
|
||||||
|
(handler #f)))))))
|
||||||
|
|
||||||
;; ----
|
;; ----
|
||||||
|
|
||||||
|
@ -53,14 +69,20 @@
|
||||||
(init-private connection)
|
(init-private connection)
|
||||||
|
|
||||||
(define mgr (new manager%))
|
(define mgr (new manager%))
|
||||||
|
(define last-connected? #t)
|
||||||
|
|
||||||
(define-syntax-rule (define-forward (method arg ...) ...)
|
(define-syntax-rule (define-forward (method arg ...) ...)
|
||||||
(begin
|
(begin
|
||||||
(define/public (method arg ...)
|
(define/public (method arg ...)
|
||||||
(send mgr call (lambda () (send connection method arg ...)))) ...))
|
(send mgr call (lambda ()
|
||||||
|
(begin0
|
||||||
|
(send connection method arg ...)
|
||||||
|
(set! last-connected? (send connection connected?))))))
|
||||||
|
...))
|
||||||
|
|
||||||
|
(define/public (connected?) last-connected?)
|
||||||
|
|
||||||
(define-forward
|
(define-forward
|
||||||
(connected?)
|
|
||||||
(disconnect)
|
(disconnect)
|
||||||
(get-dbsystem)
|
(get-dbsystem)
|
||||||
(query fsym stmt cursor?)
|
(query fsym stmt cursor?)
|
||||||
|
@ -88,8 +110,7 @@
|
||||||
(define virtual-connection%
|
(define virtual-connection%
|
||||||
(class* object% (connection<%>)
|
(class* object% (connection<%>)
|
||||||
(init-private connector ;; called from client thread
|
(init-private connector ;; called from client thread
|
||||||
get-key ;; called from client thread
|
get-key) ;; called from client thread
|
||||||
timeout)
|
|
||||||
(super-new)
|
(super-new)
|
||||||
|
|
||||||
(define custodian (current-custodian))
|
(define custodian (current-custodian))
|
||||||
|
@ -99,36 +120,17 @@
|
||||||
;; key=>conn : hasheq[key => connection]
|
;; key=>conn : hasheq[key => connection]
|
||||||
(define key=>conn (make-hasheq))
|
(define key=>conn (make-hasheq))
|
||||||
|
|
||||||
;; alarms : hasheq[connection => evt] (alarm wrapped to return key)
|
(define/private (get key) ;; also called by client thread for connected?
|
||||||
(define alarms (make-hasheq))
|
(hash-ref key=>conn key #f))
|
||||||
|
|
||||||
(define/private (get key) ;; also refreshes alarm
|
|
||||||
(let ([c (hash-ref key=>conn key #f)])
|
|
||||||
(when c (hash-set! alarms c (fresh-alarm-for key)))
|
|
||||||
c))
|
|
||||||
|
|
||||||
(define/private (put! key c)
|
(define/private (put! key c)
|
||||||
(hash-set! key=>conn key c)
|
(hash-set! key=>conn key c))
|
||||||
(hash-set! alarms c (fresh-alarm-for key)))
|
|
||||||
|
|
||||||
(define/private (fresh-alarm-for key)
|
(define/private (remove! key)
|
||||||
(wrap-evt (alarm-evt (+ (current-inexact-milliseconds) timeout))
|
(let ([c (get key)])
|
||||||
(lambda (a) key)))
|
(when c
|
||||||
|
(hash-remove! key=>conn key)
|
||||||
(define/private (remove! key timeout?)
|
(send c disconnect))))
|
||||||
;; timeout? = if connection open, then wait longer
|
|
||||||
(let* ([c (hash-ref key=>conn key #f)]
|
|
||||||
[in-trans? (with-handlers ([exn:fail? (lambda (e) #f)])
|
|
||||||
(and c
|
|
||||||
(send c connected?)
|
|
||||||
(send c transaction-status 'virtual-connection)))])
|
|
||||||
(cond [(not c) (void)]
|
|
||||||
[(and timeout? in-trans?)
|
|
||||||
(hash-set! alarms c (fresh-alarm-for key))]
|
|
||||||
[else
|
|
||||||
(hash-remove! key=>conn key)
|
|
||||||
(hash-remove! alarms c)
|
|
||||||
(send c disconnect)])))
|
|
||||||
|
|
||||||
(define mgr
|
(define mgr
|
||||||
(new manager%
|
(new manager%
|
||||||
|
@ -137,16 +139,10 @@
|
||||||
(choice-evt
|
(choice-evt
|
||||||
(let ([keys (hash-map key=>conn (lambda (k v) k))])
|
(let ([keys (hash-map key=>conn (lambda (k v) k))])
|
||||||
(handle-evt (apply choice-evt keys)
|
(handle-evt (apply choice-evt keys)
|
||||||
;; Assignment to key has expired: move to idle or disconnect.
|
;; Assignment to key has expired
|
||||||
(lambda (key)
|
(lambda (key)
|
||||||
(dbdebug "virtual-connection: key expiration: ~e" key)
|
(dbdebug "virtual-connection: key expiration: ~e" key)
|
||||||
(remove! key #f))))
|
(remove! key)))))))))
|
||||||
(let ([alarm-evts (hash-map alarms (lambda (k v) v))])
|
|
||||||
(handle-evt (apply choice-evt alarm-evts)
|
|
||||||
;; Disconnect idle connection.
|
|
||||||
(lambda (key)
|
|
||||||
(dbdebug "virtual-connection: timeout")
|
|
||||||
(remove! key #t)))))))))
|
|
||||||
|
|
||||||
;; == methods called in client thread ==
|
;; == methods called in client thread ==
|
||||||
|
|
||||||
|
@ -159,12 +155,12 @@
|
||||||
(connector))])
|
(connector))])
|
||||||
(send mgr call
|
(send mgr call
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(when c (remove! key #f))
|
(when c (remove! key))
|
||||||
(put! key c*)))
|
(put! key c*)))
|
||||||
c*)]
|
c*)]
|
||||||
[else
|
[else
|
||||||
(when c ;; got a disconnected connection
|
(when c ;; got a disconnected connection
|
||||||
(send mgr call (lambda () (remove! key #f))))
|
(send mgr call (lambda () (remove! key))))
|
||||||
#f])))
|
#f])))
|
||||||
|
|
||||||
;; ----
|
;; ----
|
||||||
|
@ -178,24 +174,27 @@
|
||||||
...))
|
...))
|
||||||
|
|
||||||
(define-forward
|
(define-forward
|
||||||
(#f #f (connected?))
|
|
||||||
(#t '_ (get-dbsystem))
|
(#t '_ (get-dbsystem))
|
||||||
(#t '_ (query fsym stmt cursor?))
|
(#t '_ (query fsym stmt cursor?))
|
||||||
(#t '_ (fetch/cursor fsym stmt fetch-size))
|
(#t '_ (fetch/cursor fsym stmt fetch-size))
|
||||||
(#t '_ (start-transaction fsym isolation cwt?))
|
(#t '_ (start-transaction fsym isolation cwt?))
|
||||||
(#f (void) (end-transaction fsym mode cwt?))
|
(#f (void) (end-transaction fsym mode cwt?))
|
||||||
(#t '_ (transaction-status fsym))
|
(#f #f (transaction-status fsym))
|
||||||
(#t '_ (list-tables fsym schema)))
|
(#t '_ (list-tables fsym schema)))
|
||||||
|
|
||||||
(define/public (get-base)
|
(define/public (get-base)
|
||||||
(get-connection #t))
|
(get-connection #t))
|
||||||
|
|
||||||
|
(define/public (connected?)
|
||||||
|
(let ([c (get (get-key))])
|
||||||
|
(and c (send c connected?))))
|
||||||
|
|
||||||
(define/public (disconnect)
|
(define/public (disconnect)
|
||||||
(let ([c (get-connection #f)]
|
(let ([c (get-connection #f)]
|
||||||
[key (get-key)])
|
[key (get-key)])
|
||||||
(when c
|
(when c
|
||||||
(send c disconnect)
|
(send c disconnect)
|
||||||
(send mgr call (lambda () (remove! key #f)))))
|
(send mgr call (lambda () (remove! key)))))
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
(define/public (prepare fsym stmt close-on-exec?)
|
(define/public (prepare fsym stmt close-on-exec?)
|
||||||
|
@ -210,8 +209,7 @@
|
||||||
|
|
||||||
;; ----
|
;; ----
|
||||||
|
|
||||||
(define (virtual-connection connector
|
(define (virtual-connection connector)
|
||||||
#:timeout [timeout +inf.0])
|
|
||||||
(let ([connector
|
(let ([connector
|
||||||
(cond [(connection-pool? connector)
|
(cond [(connection-pool? connector)
|
||||||
(lambda () (connection-pool-lease connector))]
|
(lambda () (connection-pool-lease connector))]
|
||||||
|
@ -219,8 +217,7 @@
|
||||||
[get-key (lambda () (thread-dead-evt (current-thread)))])
|
[get-key (lambda () (thread-dead-evt (current-thread)))])
|
||||||
(new virtual-connection%
|
(new virtual-connection%
|
||||||
(connector connector)
|
(connector connector)
|
||||||
(get-key (lambda () (thread-dead-evt (current-thread))))
|
(get-key get-key))))
|
||||||
(timeout (* 1000 timeout)))))
|
|
||||||
|
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
|
@ -233,20 +230,15 @@
|
||||||
max-idle-connections)
|
max-idle-connections)
|
||||||
(super-new)
|
(super-new)
|
||||||
|
|
||||||
;; max-connections is either in [1, 10000] or +inf.0,
|
|
||||||
;; if leave-evt is sema, then counter = (max-connections - assigned connections)
|
|
||||||
;; ie, includes idle connections
|
|
||||||
(define lease-evt
|
|
||||||
(if (= max-connections +inf.0)
|
|
||||||
always-evt
|
|
||||||
(make-semaphore max-connections)))
|
|
||||||
|
|
||||||
(define proxy-counter 1) ;; for debugging
|
(define proxy-counter 1) ;; for debugging
|
||||||
(define actual-counter 1) ;; for debugging
|
(define actual-counter 1) ;; for debugging
|
||||||
(define actual=>number (make-weak-hasheq))
|
(define actual=>number (make-weak-hasheq))
|
||||||
|
|
||||||
;; == methods called in manager thread ==
|
;; == methods called in manager thread ==
|
||||||
|
|
||||||
|
;; assigned-connections : nat
|
||||||
|
(define assigned-connections 0)
|
||||||
|
|
||||||
;; proxy=>evt : hasheq[proxy-connection => evt]
|
;; proxy=>evt : hasheq[proxy-connection => evt]
|
||||||
(define proxy=>evt (make-hasheq))
|
(define proxy=>evt (make-hasheq))
|
||||||
|
|
||||||
|
@ -266,7 +258,8 @@
|
||||||
proxy-number
|
proxy-number
|
||||||
(if take-idle? "idle" "new")
|
(if take-idle? "idle" "new")
|
||||||
(hash-ref actual=>number raw-c "???"))
|
(hash-ref actual=>number raw-c "???"))
|
||||||
(hash-set! proxy=>evt c key)
|
(hash-set! proxy=>evt c (wrap-evt key (lambda (_e) c)))
|
||||||
|
(set! assigned-connections (add1 assigned-connections))
|
||||||
c))
|
c))
|
||||||
|
|
||||||
(define/private (release* proxy raw-c why)
|
(define/private (release* proxy raw-c why)
|
||||||
|
@ -279,11 +272,14 @@
|
||||||
(hash-remove! proxy=>evt proxy)
|
(hash-remove! proxy=>evt proxy)
|
||||||
(when raw-c
|
(when raw-c
|
||||||
(with-handlers ([exn:fail? void])
|
(with-handlers ([exn:fail? void])
|
||||||
(send raw-c end-transaction 'connection-pool 'rollback))
|
;; If in tx, just disconnect (for simplicity; else must loop for nested txs)
|
||||||
(cond [(< (length idle-list) max-idle-connections)
|
(when (send raw-c transaction-status 'connection-pool)
|
||||||
|
(send raw-c disconnect)))
|
||||||
|
(cond [(and (< (length idle-list) max-idle-connections)
|
||||||
|
(send raw-c connected?))
|
||||||
(set! idle-list (cons raw-c idle-list))]
|
(set! idle-list (cons raw-c idle-list))]
|
||||||
[else (send raw-c disconnect)])
|
[else (send raw-c disconnect)])
|
||||||
(when (semaphore? lease-evt) (semaphore-post lease-evt))))
|
(set! assigned-connections (sub1 assigned-connections))))
|
||||||
|
|
||||||
(define/private (new-connection)
|
(define/private (new-connection)
|
||||||
(let ([c (connector)]
|
(let ([c (connector)]
|
||||||
|
@ -299,19 +295,18 @@
|
||||||
(new manager%
|
(new manager%
|
||||||
(other-evt
|
(other-evt
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let ([evts (hash-map proxy=>evt (lambda (k v) (wrap-evt v (lambda (e) k))))])
|
(let ([evts (hash-values proxy=>evt)])
|
||||||
(handle-evt (apply choice-evt evts)
|
(handle-evt (apply choice-evt evts)
|
||||||
(lambda (proxy)
|
(lambda (proxy)
|
||||||
(release* proxy
|
(release* proxy
|
||||||
(send proxy release-connection)
|
(send proxy release-connection)
|
||||||
"release-evt"))))))))
|
"release-evt"))))))
|
||||||
|
(alt-enabled? (lambda () (< assigned-connections max-connections)))))
|
||||||
|
|
||||||
;; == methods called in client thread ==
|
;; == methods called in client thread ==
|
||||||
|
|
||||||
(define/public (lease key)
|
(define/public (lease-evt key)
|
||||||
(wrap-evt lease-evt
|
(send mgr alt-call-evt (lambda () (lease* key))))
|
||||||
(lambda (_e)
|
|
||||||
(send mgr call (lambda () (lease* key))))))
|
|
||||||
|
|
||||||
(define/public (release proxy)
|
(define/public (release proxy)
|
||||||
(let ([raw-c (send proxy release-connection)])
|
(let ([raw-c (send proxy release-connection)])
|
||||||
|
@ -350,8 +345,7 @@
|
||||||
(end-transaction fsym mode cwt?)
|
(end-transaction fsym mode cwt?)
|
||||||
(list-tables fsym schema))
|
(list-tables fsym schema))
|
||||||
|
|
||||||
;; (define-forward define/override (connected?))
|
(define/override (connected?) (and connection (send connection connected?)))
|
||||||
(define/override (connected?) (and connection #t))
|
|
||||||
|
|
||||||
(define/public (disconnect)
|
(define/public (disconnect)
|
||||||
(send pool release this))
|
(send pool release this))
|
||||||
|
@ -380,7 +374,7 @@
|
||||||
(cond [(thread? key) (thread-dead-evt key)]
|
(cond [(thread? key) (thread-dead-evt key)]
|
||||||
[(custodian? key) (make-custodian-box key #t)]
|
[(custodian? key) (make-custodian-box key #t)]
|
||||||
[else key])]
|
[else key])]
|
||||||
[result (sync/timeout 0.1 (send pool lease key))])
|
[result (sync/timeout 0.1 (send pool lease-evt key))])
|
||||||
(unless result
|
(unless result
|
||||||
(uerror 'connection-pool-lease
|
(uerror 'connection-pool-lease
|
||||||
"cannot obtain connection; connection pool limit reached"))
|
"cannot obtain connection; connection pool limit reached"))
|
||||||
|
|
|
@ -146,9 +146,7 @@
|
||||||
[result (query/rows c 'query-rows sql #f)]
|
[result (query/rows c 'query-rows sql #f)]
|
||||||
[result
|
[result
|
||||||
(cond [(not (null? group-fields-list))
|
(cond [(not (null? group-fields-list))
|
||||||
(group-rows-result* 'query-rows result group-fields-list
|
(group-rows-result* 'query-rows result group-fields-list group-mode)]
|
||||||
(not (memq 'preserve-null-rows group-mode))
|
|
||||||
(memq 'list group-mode))]
|
|
||||||
[else result])])
|
[else result])])
|
||||||
(rows-result-rows result)))
|
(rows-result-rows result)))
|
||||||
|
|
||||||
|
@ -204,34 +202,38 @@
|
||||||
|
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
(define (in-query c stmt #:fetch [fetch-size +inf.0] . args)
|
(define (in-query c stmt
|
||||||
(apply in-query-helper #f c stmt #:fetch fetch-size args))
|
#:fetch [fetch-size +inf.0]
|
||||||
|
#:group [grouping-fields null]
|
||||||
(define-sequence-syntax in-query*
|
#:group-mode [group-mode null]
|
||||||
(lambda () #'in-query)
|
. args)
|
||||||
(lambda (stx)
|
(apply in-query-helper #f c stmt
|
||||||
(syntax-case stx ()
|
#:fetch fetch-size
|
||||||
[[(var ...) (in-query c stmt arg ...)]
|
#:group grouping-fields
|
||||||
#'[(var ...)
|
#:group-mode group-mode
|
||||||
(in-query-helper (length '(var ...)) c stmt arg ...)]]
|
args))
|
||||||
[_ #f])))
|
|
||||||
|
|
||||||
(define (in-query-helper vars c stmt
|
(define (in-query-helper vars c stmt
|
||||||
#:fetch [fetch-size +inf.0]
|
#:fetch [fetch-size +inf.0]
|
||||||
|
#:group [grouping-fields null]
|
||||||
|
#:group-mode [group-mode null]
|
||||||
. args)
|
. args)
|
||||||
;; Not protected by contract
|
(when (and (not (null? grouping-fields))
|
||||||
(unless (connection? c)
|
(< fetch-size +inf.0))
|
||||||
(apply raise-type-error 'in-query "connection" 0 c stmt args))
|
(error 'in-query "cannot apply grouping to cursor (finite fetch-size)"))
|
||||||
(unless (statement? stmt)
|
(let* ([check
|
||||||
(apply raise-type-error 'in-query "statement" 1 c stmt args))
|
;; If grouping, can't check expected arity.
|
||||||
(unless (or (exact-positive-integer? fetch-size) (eqv? fetch-size +inf.0))
|
;; FIXME: should check header includes named fields
|
||||||
(raise-type-error 'in-query "positive integer or +inf.0" fetch-size))
|
(cond [(null? grouping-fields) (or vars 'rows)]
|
||||||
(let* ([check (or vars 'rows)]
|
[else 'rows])]
|
||||||
[stmt (compose-statement 'in-query c stmt args check)])
|
[stmt (compose-statement 'in-query c stmt args check)])
|
||||||
(cond [(eqv? fetch-size +inf.0)
|
(cond [(eqv? fetch-size +inf.0)
|
||||||
(in-list/vector->values
|
(in-list/vector->values
|
||||||
(rows-result-rows
|
(rows-result-rows
|
||||||
(query/rows c 'in-query stmt vars)))]
|
(let ([result (query/rows c 'in-query stmt vars)])
|
||||||
|
(if (null? grouping-fields)
|
||||||
|
result
|
||||||
|
(group-rows-result* 'in-query result grouping-fields group-mode)))))]
|
||||||
[else
|
[else
|
||||||
(let ([cursor (query/cursor c 'in-query stmt vars)])
|
(let ([cursor (query/cursor c 'in-query stmt vars)])
|
||||||
(in-list-generator/vector->values
|
(in-list-generator/vector->values
|
||||||
|
@ -333,62 +335,38 @@
|
||||||
|
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
|
;; FIXME: add 'assume-sorted optimization option?
|
||||||
|
|
||||||
(define (group-rows result
|
(define (group-rows result
|
||||||
#:group key-fields-list
|
#:group key-fields-list
|
||||||
#:group-mode [group-mode null])
|
#:group-mode [group-mode null])
|
||||||
(when (null? key-fields-list)
|
(when (null? key-fields-list)
|
||||||
(error 'group-rows "expected at least one grouping field set"))
|
(error 'group-rows "expected at least one grouping field set"))
|
||||||
(group-rows-result* 'group-rows
|
(group-rows-result* 'group-rows result key-fields-list group-mode))
|
||||||
result
|
|
||||||
key-fields-list
|
|
||||||
(not (memq 'preserve-null-rows group-mode))
|
|
||||||
(memq 'list group-mode)))
|
|
||||||
|
|
||||||
(define (group-rows-result* fsym result key-fields-list invert-outer? as-list?)
|
(define (group-rows-result* fsym result key-fields-list group-mode)
|
||||||
(let* ([key-fields-list
|
(let* ([invert-outer? (not (or (memq 'preserve-null group-mode)
|
||||||
(if (list? key-fields-list) key-fields-list (list key-fields-list))]
|
;; old flag, deprecated:
|
||||||
[total-fields (length (rows-result-headers result))]
|
(memq 'preserve-null-rows group-mode)))]
|
||||||
[name-map
|
[as-list? (memq 'list group-mode)]
|
||||||
(for/hash ([header (in-list (rows-result-headers result))]
|
[headers (rows-result-headers result)]
|
||||||
[i (in-naturals)]
|
[total-fields (length headers)]
|
||||||
#:when (assq 'name header))
|
[name-map (headers->name-map headers)]
|
||||||
(values (cdr (assq 'name header)) i))]
|
|
||||||
[fields-used (make-vector total-fields #f)]
|
[fields-used (make-vector total-fields #f)]
|
||||||
[key-indexes-list
|
[key-indexes-list
|
||||||
(for/list ([key-fields (in-list key-fields-list)])
|
(group-list->indexes fsym name-map total-fields fields-used key-fields-list)]
|
||||||
(for/vector ([key-field (in-vector key-fields)])
|
|
||||||
(let ([key-index
|
|
||||||
(cond [(string? key-field)
|
|
||||||
(hash-ref name-map key-field #f)]
|
|
||||||
[else key-field])])
|
|
||||||
(when (string? key-field)
|
|
||||||
(unless key-index
|
|
||||||
(error fsym "grouping field ~s not found" key-field)))
|
|
||||||
(when (exact-integer? key-field)
|
|
||||||
(unless (< key-index total-fields)
|
|
||||||
(error fsym "grouping index ~s out of range [0, ~a]"
|
|
||||||
key-index (sub1 total-fields))))
|
|
||||||
(when (vector-ref fields-used key-index)
|
|
||||||
(error fsym "grouping field ~s~a used multiple times"
|
|
||||||
key-field
|
|
||||||
(if (string? key-field)
|
|
||||||
(format " (index ~a)" key-index)
|
|
||||||
"")))
|
|
||||||
(vector-set! fields-used key-index #t)
|
|
||||||
key-index)))]
|
|
||||||
[residual-length
|
[residual-length
|
||||||
(for/sum ([x (in-vector fields-used)])
|
(for/sum ([x (in-vector fields-used)]) (if x 0 1))])
|
||||||
(if x 0 1))])
|
|
||||||
(when (= residual-length 0)
|
(when (= residual-length 0)
|
||||||
(error fsym "cannot group by all fields"))
|
(error fsym "cannot group by all fields"))
|
||||||
(when (and (> residual-length 1) as-list?)
|
(when (and (> residual-length 1) as-list?)
|
||||||
(error fsym
|
(error fsym
|
||||||
"exactly one residual field expected for #:group-mode 'list, got ~a"
|
"expected exactly one residual field for #:group-mode 'list, got ~a"
|
||||||
residual-length))
|
residual-length))
|
||||||
(let* ([initial-projection
|
(let* ([initial-projection
|
||||||
(for/vector #:length total-fields ([i (in-range total-fields)]) i)]
|
(for/vector #:length total-fields ([i (in-range total-fields)]) i)]
|
||||||
[headers
|
[headers
|
||||||
(group-headers (list->vector (rows-result-headers result))
|
(group-headers (list->vector headers)
|
||||||
initial-projection
|
initial-projection
|
||||||
key-indexes-list)]
|
key-indexes-list)]
|
||||||
[rows
|
[rows
|
||||||
|
@ -400,6 +378,46 @@
|
||||||
as-list?)])
|
as-list?)])
|
||||||
(rows-result headers rows))))
|
(rows-result headers rows))))
|
||||||
|
|
||||||
|
(define (headers->name-map headers)
|
||||||
|
(for/hash ([header (in-list headers)]
|
||||||
|
[i (in-naturals)]
|
||||||
|
#:when (assq 'name header))
|
||||||
|
(values (cdr (assq 'name header)) i)))
|
||||||
|
|
||||||
|
(define (group-list->indexes fsym name-map total-fields fields-used key-fields-list)
|
||||||
|
(let ([key-fields-list (if (list? key-fields-list) key-fields-list (list key-fields-list))])
|
||||||
|
(for/list ([key-fields (in-list key-fields-list)])
|
||||||
|
(group->indexes fsym name-map total-fields fields-used key-fields))))
|
||||||
|
|
||||||
|
(define (group->indexes fsym name-map total-fields fields-used key-fields)
|
||||||
|
(let ([key-fields (if (vector? key-fields) key-fields (vector key-fields))])
|
||||||
|
(for/vector ([key-field (in-vector key-fields)])
|
||||||
|
(grouping-field->index fsym name-map total-fields fields-used key-field))))
|
||||||
|
|
||||||
|
(define (grouping-field->index fsym name-map total-fields fields-used key-field)
|
||||||
|
(let ([key-index
|
||||||
|
(cond [(string? key-field)
|
||||||
|
(hash-ref name-map key-field #f)]
|
||||||
|
[else key-field])])
|
||||||
|
(when (string? key-field)
|
||||||
|
(unless key-index
|
||||||
|
(error fsym "expected grouping field in ~s, got: ~e"
|
||||||
|
(sort (hash-keys name-map) string<?)
|
||||||
|
key-field)))
|
||||||
|
(when (exact-integer? key-field)
|
||||||
|
(unless (< key-index total-fields)
|
||||||
|
(error fsym "grouping index ~s out of range [0, ~a]"
|
||||||
|
key-index (sub1 total-fields))))
|
||||||
|
(when fields-used
|
||||||
|
(when (vector-ref fields-used key-index)
|
||||||
|
(error fsym "grouping field ~s~a used multiple times"
|
||||||
|
key-field
|
||||||
|
(if (string? key-field)
|
||||||
|
(format " (index ~a)" key-index)
|
||||||
|
"")))
|
||||||
|
(vector-set! fields-used key-index #t))
|
||||||
|
key-index))
|
||||||
|
|
||||||
(define (group-headers headers projection key-indexes-list)
|
(define (group-headers headers projection key-indexes-list)
|
||||||
(define (get-headers vec)
|
(define (get-headers vec)
|
||||||
(for/list ([index (in-vector vec)])
|
(for/list ([index (in-vector vec)])
|
||||||
|
@ -414,7 +432,7 @@
|
||||||
[residual-headers
|
[residual-headers
|
||||||
(group-headers headers residual-projection (cdr key-indexes-list))])
|
(group-headers headers residual-projection (cdr key-indexes-list))])
|
||||||
(append (get-headers key-indexes)
|
(append (get-headers key-indexes)
|
||||||
(list `((grouped . ,residual-headers)))))]))
|
(list `((name . "grouped") (grouped . ,residual-headers)))))]))
|
||||||
|
|
||||||
(define (group-rows* fsym rows projection key-indexes-list invert-outer? as-list?)
|
(define (group-rows* fsym rows projection key-indexes-list invert-outer? as-list?)
|
||||||
;; projection is vector of indexes (actually projection and permutation)
|
;; projection is vector of indexes (actually projection and permutation)
|
||||||
|
@ -443,17 +461,14 @@
|
||||||
(define residual-projection
|
(define residual-projection
|
||||||
(vector-filter-not (lambda (index) (vector-member index key-indexes))
|
(vector-filter-not (lambda (index) (vector-member index key-indexes))
|
||||||
projection))
|
projection))
|
||||||
|
|
||||||
(define key-row-length (vector-length key-indexes))
|
(define key-row-length (vector-length key-indexes))
|
||||||
(define (row->key-row row)
|
(define (row->key-row row)
|
||||||
(for/vector #:length key-row-length
|
(for/vector #:length key-row-length
|
||||||
([i (in-vector key-indexes)])
|
([i (in-vector key-indexes)])
|
||||||
(vector-ref row i)))
|
(vector-ref row i)))
|
||||||
|
|
||||||
(define (residual-all-null? row)
|
(define (residual-all-null? row)
|
||||||
(for/and ([i (in-vector residual-projection)])
|
(for/and ([i (in-vector residual-projection)])
|
||||||
(sql-null? (vector-ref row i))))
|
(sql-null? (vector-ref row i))))
|
||||||
|
|
||||||
(let* ([key-table (make-hash)]
|
(let* ([key-table (make-hash)]
|
||||||
[r-keys
|
[r-keys
|
||||||
(for/fold ([r-keys null])
|
(for/fold ([r-keys null])
|
||||||
|
@ -476,3 +491,55 @@
|
||||||
invert-outer?
|
invert-outer?
|
||||||
as-list?)])
|
as-list?)])
|
||||||
(vector-append key (vector residuals))))))]))
|
(vector-append key (vector residuals))))))]))
|
||||||
|
|
||||||
|
;; ========================================
|
||||||
|
|
||||||
|
(define not-given (gensym 'not-given))
|
||||||
|
|
||||||
|
(define (rows->dict result
|
||||||
|
#:key key-field/s
|
||||||
|
#:value value-field/s
|
||||||
|
#:value-mode [value-mode null])
|
||||||
|
(let* ([who 'rows->dict]
|
||||||
|
[headers (rows-result-headers result)]
|
||||||
|
[total-fields (length headers)]
|
||||||
|
[name-map (headers->name-map headers)]
|
||||||
|
[preserve-null? (memq 'preserve-null value-mode)]
|
||||||
|
[value-list? (memq 'list value-mode)])
|
||||||
|
(define (make-project field/s)
|
||||||
|
(if (vector? field/s)
|
||||||
|
(let* ([indexes (group->indexes who name-map total-fields #f field/s)]
|
||||||
|
[indexes-length (vector-length indexes)])
|
||||||
|
(lambda (v)
|
||||||
|
(for/vector #:length indexes-length ([i (in-vector indexes)])
|
||||||
|
(vector-ref v i))))
|
||||||
|
(let ([index (grouping-field->index who name-map total-fields #f field/s)])
|
||||||
|
(lambda (v) (vector-ref v index)))))
|
||||||
|
(define get-key (make-project key-field/s))
|
||||||
|
(define get-value (make-project value-field/s))
|
||||||
|
(define ok-value?
|
||||||
|
(cond [preserve-null? (lambda (v) #t)]
|
||||||
|
[(vector? value-field/s)
|
||||||
|
(lambda (v) (not (for/or ([e (in-vector v)]) (sql-null? e))))]
|
||||||
|
[else (lambda (v) (not (sql-null? v)))]))
|
||||||
|
(for/fold ([table '#hash()]) ([row (in-list (if value-list?
|
||||||
|
(reverse (rows-result-rows result))
|
||||||
|
(rows-result-rows result)))])
|
||||||
|
(let* ([key (get-key row)]
|
||||||
|
[value (get-value row)]
|
||||||
|
[old-value (hash-ref table key (if value-list? '() not-given))])
|
||||||
|
(unless (or value-list?
|
||||||
|
(eq? (hash-ref table key not-given) not-given)
|
||||||
|
;; FIXME: okay to coalesce values if equal?
|
||||||
|
(equal? value old-value))
|
||||||
|
(error who "duplicate value for key: ~e; values are ~e and ~e"
|
||||||
|
key old-value value))
|
||||||
|
(if value-list?
|
||||||
|
(hash-set table key
|
||||||
|
(if (ok-value? value)
|
||||||
|
(cons value old-value)
|
||||||
|
;; If all-NULL value, still enter key => '() into dict
|
||||||
|
old-value))
|
||||||
|
(if (ok-value? value)
|
||||||
|
(hash-set table key value)
|
||||||
|
table))))))
|
||||||
|
|
|
@ -29,7 +29,9 @@
|
||||||
;; connection<%>
|
;; connection<%>
|
||||||
(define connection<%>
|
(define connection<%>
|
||||||
(interface ()
|
(interface ()
|
||||||
|
;; connected? method must return promptly (eg, without acquiring lock)
|
||||||
connected? ;; -> boolean
|
connected? ;; -> boolean
|
||||||
|
|
||||||
disconnect ;; -> void
|
disconnect ;; -> void
|
||||||
get-dbsystem ;; -> dbsystem<%>
|
get-dbsystem ;; -> dbsystem<%>
|
||||||
query ;; symbol statement -> QueryResult
|
query ;; symbol statement -> QueryResult
|
||||||
|
|
|
@ -49,18 +49,20 @@
|
||||||
(define/private (call* method-name args need-connected?)
|
(define/private (call* method-name args need-connected?)
|
||||||
(cond [channel
|
(cond [channel
|
||||||
(pchan-put channel (cons method-name args))
|
(pchan-put channel (cons method-name args))
|
||||||
(match (pchan-get channel)
|
(let* ([response (pchan-get channel)]
|
||||||
[(cons 'values vals)
|
[still-connected? (car response)])
|
||||||
(apply values (for/list ([val (in-list vals)]) (sexpr->result val)))]
|
(when (not still-connected?) (set! channel #f))
|
||||||
[(list 'error message)
|
(match (cdr response)
|
||||||
(raise (make-exn:fail message (current-continuation-marks)))])]
|
[(cons 'values vals)
|
||||||
|
(apply values (for/list ([val (in-list vals)]) (sexpr->result val)))]
|
||||||
|
[(list 'error message)
|
||||||
|
(raise (make-exn:fail message (current-continuation-marks)))]))]
|
||||||
[need-connected?
|
[need-connected?
|
||||||
(unless channel
|
(unless channel
|
||||||
(error/not-connected method-name))]
|
(error/not-connected method-name))]
|
||||||
[else (void)]))
|
[else (void)]))
|
||||||
|
|
||||||
(define/override (connected?)
|
(define/override (connected?)
|
||||||
;; FIXME: can underlying connection disconnect w/o us knowing?
|
|
||||||
(and channel #t))
|
(and channel #t))
|
||||||
|
|
||||||
(define/public (disconnect)
|
(define/public (disconnect)
|
||||||
|
|
|
@ -68,8 +68,8 @@ where <connect-spec> ::= (list 'sqlite3 path/sym mode-sym delay-num limit-num)
|
||||||
Connection methods protocol
|
Connection methods protocol
|
||||||
|
|
||||||
client -> server: (list '<method-name> arg ...)
|
client -> server: (list '<method-name> arg ...)
|
||||||
server -> client: (or (list 'values result ...)
|
server -> client: (or (list boolean 'values result ...)
|
||||||
(list 'error string))
|
(list boolean 'error string))
|
||||||
|#
|
|#
|
||||||
|
|
||||||
(define proxy-server%
|
(define proxy-server%
|
||||||
|
@ -86,10 +86,12 @@ server -> client: (or (list 'values result ...)
|
||||||
(serve1)
|
(serve1)
|
||||||
(when connection (serve)))
|
(when connection (serve)))
|
||||||
|
|
||||||
|
(define/private (still-connected?) (and connection (send connection connected?)))
|
||||||
|
|
||||||
(define/private (serve1)
|
(define/private (serve1)
|
||||||
(with-handlers ([exn?
|
(with-handlers ([exn?
|
||||||
(lambda (e)
|
(lambda (e)
|
||||||
(pchan-put channel (list 'error (exn-message e))))])
|
(pchan-put channel (list (still-connected?) 'error (exn-message e))))])
|
||||||
(call-with-values
|
(call-with-values
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(match (pchan-get channel)
|
(match (pchan-get channel)
|
||||||
|
@ -117,7 +119,7 @@ server -> client: (or (list 'values result ...)
|
||||||
(transaction-status w))]))
|
(transaction-status w))]))
|
||||||
(lambda results
|
(lambda results
|
||||||
(let ([results (for/list ([result (in-list results)]) (result->sexpr result))])
|
(let ([results (for/list ([result (in-list results)]) (result->sexpr result))])
|
||||||
(pchan-put channel (cons 'values results)))))))
|
(pchan-put channel (cons (still-connected?) (cons 'values results))))))))
|
||||||
|
|
||||||
(define/private (sexpr->statement x)
|
(define/private (sexpr->statement x)
|
||||||
(match x
|
(match x
|
||||||
|
|
|
@ -10,6 +10,12 @@
|
||||||
(define (tech/reference . pre-flows)
|
(define (tech/reference . pre-flows)
|
||||||
(apply tech #:doc '(lib "scribblings/reference/reference.scrbl") pre-flows))
|
(apply tech #:doc '(lib "scribblings/reference/reference.scrbl") pre-flows))
|
||||||
|
|
||||||
|
(define (parheading . pre-flows)
|
||||||
|
(elem (apply bold pre-flows) (hspace 1)))
|
||||||
|
|
||||||
|
(define (wplink path . pre-flows)
|
||||||
|
(apply hyperlink (string-append "http://en.wikipedia.org/wiki/" path) pre-flows))
|
||||||
|
|
||||||
;; ----
|
;; ----
|
||||||
|
|
||||||
(define the-eval (make-base-eval))
|
(define the-eval (make-base-eval))
|
||||||
|
|
|
@ -19,18 +19,8 @@ administrative functions for managing connections.
|
||||||
There are four kinds of base connection, and they are divided into two
|
There are four kinds of base connection, and they are divided into two
|
||||||
groups: @deftech{wire-based connections} and @deftech{FFI-based
|
groups: @deftech{wire-based connections} and @deftech{FFI-based
|
||||||
connections}. PostgreSQL and MySQL connections are wire-based, and
|
connections}. PostgreSQL and MySQL connections are wire-based, and
|
||||||
SQLite and ODBC connections are FFI-based.
|
SQLite and ODBC connections are FFI-based. See also
|
||||||
|
@secref["ffi-concurrency"].
|
||||||
Wire-based connections communicate using @tech/reference{ports}, which
|
|
||||||
do not cause other Racket threads to block. In contrast, an FFI call
|
|
||||||
causes all Racket threads to block until it completes, so FFI-based
|
|
||||||
connections can degrade the interactivity of a Racket program,
|
|
||||||
particularly if long-running queries are performed using the
|
|
||||||
connection. This problem can be avoided by creating the FFI-based
|
|
||||||
connection in a separate @tech/reference{place} using the
|
|
||||||
@racket[#:use-place] keyword argument. Such a connection will not
|
|
||||||
block all Racket threads during queries; the disadvantage is the cost
|
|
||||||
of creating and communicating with a separate @tech/reference{place}.
|
|
||||||
|
|
||||||
Base connections are made using the following functions.
|
Base connections are made using the following functions.
|
||||||
|
|
||||||
|
@ -240,7 +230,7 @@ Base connections are made using the following functions.
|
||||||
|
|
||||||
If @racket[use-place] is true, the actual connection is created in
|
If @racket[use-place] is true, the actual connection is created in
|
||||||
a distinct @tech/reference{place} for database connections and a
|
a distinct @tech/reference{place} for database connections and a
|
||||||
proxy is returned.
|
proxy is returned; see @secref["ffi-concurrency"].
|
||||||
|
|
||||||
If the connection cannot be made, an exception is raised.
|
If the connection cannot be made, an exception is raised.
|
||||||
|
|
||||||
|
@ -289,7 +279,7 @@ Base connections are made using the following functions.
|
||||||
|
|
||||||
If @racket[use-place] is true, the actual connection is created in
|
If @racket[use-place] is true, the actual connection is created in
|
||||||
a distinct @tech/reference{place} for database connections and a
|
a distinct @tech/reference{place} for database connections and a
|
||||||
proxy is returned.
|
proxy is returned; see @secref["ffi-concurrency"].
|
||||||
|
|
||||||
If the connection cannot be made, an exception is raised.
|
If the connection cannot be made, an exception is raised.
|
||||||
}
|
}
|
||||||
|
@ -436,14 +426,13 @@ connection associated with the current thread, one is obtained by
|
||||||
calling @racket[connect]. An actual connection is disconnected when
|
calling @racket[connect]. An actual connection is disconnected when
|
||||||
its associated thread dies.
|
its associated thread dies.
|
||||||
|
|
||||||
@;{or if @racket[timeout] seconds elapse since the actual connection was last used.}
|
|
||||||
|
|
||||||
Virtual connections are especially useful in contexts such as web
|
Virtual connections are especially useful in contexts such as web
|
||||||
servlets, where each request is handled in a fresh thread. A single
|
servlets (see @secref["intro-servlets"]), where each request is
|
||||||
global virtual connection can be defined, freeing each servlet request
|
handled in a fresh thread. A single global virtual connection can be
|
||||||
from explicitly opening and closing its own connections. In
|
defined, freeing each servlet request from explicitly opening and
|
||||||
particular, a @tech{virtual connection} backed by a @tech{connection
|
closing its own connections. In particular, a @tech{virtual
|
||||||
pool} combines convenience with efficiency:
|
connection} backed by a @tech{connection pool} combines convenience
|
||||||
|
with efficiency:
|
||||||
|
|
||||||
@examples/results[
|
@examples/results[
|
||||||
[(define the-connection
|
[(define the-connection
|
||||||
|
|
|
@ -50,7 +50,7 @@ and Doug Orleans for contributions to @tt{spgsql}, the PostgreSQL-only
|
||||||
predecessor of this library. The SQLite support is based in part on
|
predecessor of this library. The SQLite support is based in part on
|
||||||
code from Jay McCarthy's @tt{sqlite} package.
|
code from Jay McCarthy's @tt{sqlite} package.
|
||||||
|
|
||||||
@include-section["introduction.scrbl"]
|
@include-section["using-db.scrbl"]
|
||||||
@include-section["connect.scrbl"]
|
@include-section["connect.scrbl"]
|
||||||
@include-section["query.scrbl"]
|
@include-section["query.scrbl"]
|
||||||
@include-section["sql-types.scrbl"]
|
@include-section["sql-types.scrbl"]
|
||||||
|
|
|
@ -1,290 +0,0 @@
|
||||||
#lang scribble/doc
|
|
||||||
@(require scribble/manual
|
|
||||||
scribble/eval
|
|
||||||
scribble/struct
|
|
||||||
racket/sandbox
|
|
||||||
"config.rkt"
|
|
||||||
(for-label db
|
|
||||||
web-server/lang/web))
|
|
||||||
|
|
||||||
@title[#:tag "introduction"]{Introduction}
|
|
||||||
|
|
||||||
This section introduces this library's basic features and discusses
|
|
||||||
how to build a database-backed web servlet.
|
|
||||||
|
|
||||||
@section[#:tag "intro-basic"]{Basic Features}
|
|
||||||
|
|
||||||
The following annotated program demonstrates how to connect to a
|
|
||||||
database and perform simple queries. Some of the SQL syntax used below
|
|
||||||
is PostgreSQL-specific, such as the syntax of query parameters
|
|
||||||
(@litchar{$1} rather than @litchar{?}).
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(require db)
|
|
||||||
(void)]
|
|
||||||
]
|
|
||||||
|
|
||||||
First we create a connection. Replace @racket[_user], @racket[_db],
|
|
||||||
and @racket[_password] below with the appropriate values for your
|
|
||||||
configuration (see @secref{creating-connections} for other connection examples):
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(define pgc
|
|
||||||
(postgresql-connect #:user _user
|
|
||||||
#:database _db
|
|
||||||
#:password _password))
|
|
||||||
(void)]
|
|
||||||
[pgc
|
|
||||||
(new connection%)]
|
|
||||||
]
|
|
||||||
|
|
||||||
Use @racket[query-exec] method to execute a SQL statement for effect.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-exec pgc
|
|
||||||
"create temporary table the_numbers (n integer, d varchar(20))")
|
|
||||||
(void)]
|
|
||||||
[(query-exec pgc
|
|
||||||
"insert into the_numbers values (0, 'nothing')")
|
|
||||||
(void)]
|
|
||||||
[(query-exec pgc
|
|
||||||
"insert into the_numbers values (1, 'the loneliest number')")
|
|
||||||
(void)]
|
|
||||||
[(query-exec pgc
|
|
||||||
"insert into the_numbers values (2, 'company')")
|
|
||||||
(void)]
|
|
||||||
]
|
|
||||||
|
|
||||||
The @racket[query] function is a more general way to execute a
|
|
||||||
statement. It returns a structure encapsulating information about the
|
|
||||||
statement's execution. (But some of that information varies from
|
|
||||||
system to system and is subject to change.)
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query pgc "insert into the_numbers values (3, 'a crowd')")
|
|
||||||
(simple-result '((command insert 0 1)))]
|
|
||||||
[(query pgc "select n, d from the_numbers where n % 2 = 0")
|
|
||||||
(rows-result
|
|
||||||
(list
|
|
||||||
'((name . "n") (typeid . 23))
|
|
||||||
'((name . "d") (typeid . 1043)))
|
|
||||||
'(#(0 "nothing") #(2 "company")))]
|
|
||||||
]
|
|
||||||
|
|
||||||
When the query is known to return rows and when the field
|
|
||||||
descriptions are not needed, it is more convenient to use the
|
|
||||||
@racket[query-rows] function.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-rows pgc "select n, d from the_numbers where n % 2 = 0")
|
|
||||||
'(#(0 "nothing") #(2 "company"))]
|
|
||||||
]
|
|
||||||
|
|
||||||
Use @racket[query-row] for queries that are known to return exactly
|
|
||||||
one row.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-row pgc "select * from the_numbers where n = 0")
|
|
||||||
(vector 0 "nothing")]
|
|
||||||
]
|
|
||||||
|
|
||||||
Similarly, use @racket[query-list] for queries that produce rows of
|
|
||||||
exactly one column.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-list pgc "select d from the_numbers order by n")
|
|
||||||
(list "nothing" "the loneliest number" "company" "a crowd")]
|
|
||||||
]
|
|
||||||
|
|
||||||
When a query is known to return a single value (one row and one
|
|
||||||
column), use @racket[query-value].
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-value pgc "select count(*) from the_numbers")
|
|
||||||
4]
|
|
||||||
[(query-value pgc "select d from the_numbers where n = 5")
|
|
||||||
(error 'query-value
|
|
||||||
"query returned zero rows: ~s"
|
|
||||||
"select d from the_numbers where n = 5")]
|
|
||||||
]
|
|
||||||
|
|
||||||
When a query may return zero or one rows, as the last example, use
|
|
||||||
@racket[query-maybe-row] or @racket[query-maybe-value] instead.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-maybe-value pgc "select d from the_numbers where n = 5")
|
|
||||||
(values #f)]
|
|
||||||
]
|
|
||||||
|
|
||||||
The @racket[in-query] function produces a sequence that can be used
|
|
||||||
with Racket's iteration forms:
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(for ([(n d) (in-query pgc "select * from the_numbers where n < 4")])
|
|
||||||
(printf "~a is ~a\n" n d))
|
|
||||||
(for-each (lambda (n d) (printf "~a: ~a\n" n d))
|
|
||||||
'(0 1 2 3)
|
|
||||||
'("nothing" "the loneliest number" "company" "a crowd"))]
|
|
||||||
[(for/fold ([sum 0]) ([n (in-query pgc "select n from the_numbers")])
|
|
||||||
(+ sum n))
|
|
||||||
(for/fold ([sum 0]) ([n (in-list '(0 1 2 3))])
|
|
||||||
(+ sum n))]
|
|
||||||
]
|
|
||||||
|
|
||||||
Errors in queries generally do not cause the connection to disconnect.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(begin (with-handlers [(exn:fail?
|
|
||||||
(lambda (e) (printf "~a~n" (exn-message e))))]
|
|
||||||
(query-value pgc "select NoSuchField from NoSuchTable"))
|
|
||||||
(query-value pgc "select 'okay to proceed!'"))
|
|
||||||
(begin (display "query-value: relation \"nosuchtable\" does not exist (SQLSTATE 42P01)")
|
|
||||||
"okay to proceed!")]
|
|
||||||
]
|
|
||||||
|
|
||||||
Queries may contain parameters. The easiest way to execute a
|
|
||||||
parameterized query is to provide the parameters ``inline'' after the
|
|
||||||
SQL statement in the query function call.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(query-value pgc
|
|
||||||
"select d from the_numbers where n = $1" 2)
|
|
||||||
"company"]
|
|
||||||
[(query-list pgc
|
|
||||||
"select n from the_numbers where n > $1 and n < $2" 0 3)
|
|
||||||
(list 1 2)]
|
|
||||||
]
|
|
||||||
|
|
||||||
Alternatively, a parameterized query may be prepared in advance and
|
|
||||||
executed later. @tech{Prepared statements} can be executed multiple
|
|
||||||
times with different parameter values.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(define get-less-than-pst
|
|
||||||
(prepare pgc "select n from the_numbers where n < $1"))
|
|
||||||
(void)]
|
|
||||||
[(query-list pgc get-less-than-pst 1)
|
|
||||||
(list 0)]
|
|
||||||
[(query-list pgc (bind-prepared-statement get-less-than-pst '(2)))
|
|
||||||
(list 0 1)]
|
|
||||||
]
|
|
||||||
|
|
||||||
When a connection's work is done, it should be disconnected.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(disconnect pgc)
|
|
||||||
(void)]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@;{============================================================}
|
|
||||||
|
|
||||||
@section[#:tag "intro-servlets"]{Databases and Web Servlets}
|
|
||||||
|
|
||||||
Using database connections in a web servlet is more complicated than
|
|
||||||
in a standalone program. A single servlet is potentially used to serve
|
|
||||||
many requests at once, each in a separate request-handling
|
|
||||||
thread. Furthermore, the use of @racket[send/suspend],
|
|
||||||
@racket[send/suspend/dispatch], etc means that there are many places
|
|
||||||
where a servlet may start and stop executing to service a request.
|
|
||||||
|
|
||||||
Why not use a single connection to handle all of a servlet's requests?
|
|
||||||
That is, create the connection with the servlet instance and never
|
|
||||||
disconnect it. Such a servlet would look something like the following:
|
|
||||||
|
|
||||||
@racketmod[
|
|
||||||
#:file "bad-servlet.rkt"
|
|
||||||
web-server
|
|
||||||
(require db)
|
|
||||||
(define db-conn (postgresql-connect ....))
|
|
||||||
(define (serve req)
|
|
||||||
.... db-conn ....)
|
|
||||||
]
|
|
||||||
|
|
||||||
The main problem with using one connection for all requests is that
|
|
||||||
multiple threads accessing the same connection are not properly
|
|
||||||
@hyperlink["http://en.wikipedia.org/wiki/Isolation_%28database_systems%29"]{isolated}. For
|
|
||||||
example, if two threads both attempt to start a new transaction, the
|
|
||||||
second one will fail, because the first thread has already put the
|
|
||||||
connection into an ``in transaction'' state. And if one thread is
|
|
||||||
accessing the connection within a transaction and another thread
|
|
||||||
issues a query, the second thread may see invalid data or even disrupt
|
|
||||||
the work of the first thread.
|
|
||||||
|
|
||||||
A secondary problem is performance. A connection can only perform a
|
|
||||||
single query at a time, whereas most database systems are capable of
|
|
||||||
concurrent query processing.
|
|
||||||
|
|
||||||
The proper way to use database connections in a servlet is to create a
|
|
||||||
connection for each request and disconnect it when the request
|
|
||||||
is handled. But since a request thread may start and stop executing in
|
|
||||||
many places (due to @racket[send/suspend], etc), inserting the code to
|
|
||||||
connect and disconnect at the proper places can be challenging and
|
|
||||||
messy.
|
|
||||||
|
|
||||||
A better solution is to use a @tech{virtual connection}, which creates
|
|
||||||
a request-specific (that is, thread-specific) ``actual connection'' by
|
|
||||||
need and disconnects it when the request is handled (that is, when the
|
|
||||||
thread terminates). Different request-handling threads using the same
|
|
||||||
virtual connection are assigned different actual connection, so the
|
|
||||||
threads are properly isolated.
|
|
||||||
|
|
||||||
@racketmod[
|
|
||||||
#:file "better-servlet.rkt"
|
|
||||||
web-server
|
|
||||||
(require db)
|
|
||||||
(define db-conn
|
|
||||||
(virtual-connection
|
|
||||||
(lambda () (postgresql-connect ....))))
|
|
||||||
(define (serve req)
|
|
||||||
.... db-conn ....)
|
|
||||||
]
|
|
||||||
|
|
||||||
This solution preserves the simplicity of the naive solution and fixes
|
|
||||||
the isolation problem but at the cost of creating many short-lived
|
|
||||||
database connections. That cost can be eliminated by using a
|
|
||||||
@tech{connection pool}:
|
|
||||||
|
|
||||||
@racketmod[
|
|
||||||
#:file "best-servlet.rkt"
|
|
||||||
web-server
|
|
||||||
(require db)
|
|
||||||
(define db-conn
|
|
||||||
(virtual-connection
|
|
||||||
(connection-pool
|
|
||||||
(lambda () (postgresql-connect ....)))))
|
|
||||||
(define (serve req)
|
|
||||||
.... db-conn ....)
|
|
||||||
]
|
|
||||||
|
|
||||||
By using a virtual connection backed by a connection pool, a servlet
|
|
||||||
can achieve simplicity, isolation, and performance.
|
|
||||||
|
|
||||||
@;{
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
- talk about virtual statements, too
|
|
||||||
- show actual working servlet code
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
A prepared statement is tied to the connection used to create it;
|
|
||||||
attempting to use it with another connection results in an
|
|
||||||
error. Unfortunately, in some scenarios such as web servlets, the
|
|
||||||
lifetimes of connections are short or difficult to track, making
|
|
||||||
prepared statements inconvenient. In such cases, a better tool is the
|
|
||||||
@tech{virtual statement}, which prepares statements on demand and
|
|
||||||
caches them for future use with the same connection.
|
|
||||||
|
|
||||||
@my-interaction[
|
|
||||||
[(define get-less-than-pst
|
|
||||||
(virtual-statement "select n from the_numbers where n < $1"))
|
|
||||||
(void)]
|
|
||||||
[(code:line (query-list pgc1 get-less-than-pst 1) (code:comment "prepares statement for pgc1"))
|
|
||||||
(list 0)]
|
|
||||||
[(code:line (query-list pgc2 get-less-than-pst 2) (code:comment "prepares statement for pgc2"))
|
|
||||||
(list 0 1)]
|
|
||||||
[(code:line (query-list pgc1 get-less-than-pst 3) (code:comment "uses existing prep. stmt."))
|
|
||||||
(list 0 1 2)]
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -96,6 +96,21 @@ package in Red Hat.}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@section[#:tag "ffi-concurrency"]{FFI-Based Connections and Concurrency}
|
||||||
|
|
||||||
|
@tech{Wire-based connections} communicate using
|
||||||
|
@tech/reference{ports}, which do not cause other Racket threads to
|
||||||
|
block. In contrast, an FFI call causes all Racket threads to block
|
||||||
|
until it completes, so @tech{FFI-based connections} can degrade the
|
||||||
|
interactivity of a Racket program, particularly if long-running
|
||||||
|
queries are performed using the connection. This problem can be
|
||||||
|
avoided by creating the FFI-based connection in a separate
|
||||||
|
@tech/reference{place} using the @racket[#:use-place] keyword
|
||||||
|
argument. Such a connection will not block all Racket threads during
|
||||||
|
queries; the disadvantage is the cost of creating and communicating
|
||||||
|
with a separate @tech/reference{place}.
|
||||||
|
|
||||||
|
|
||||||
@section[#:tag "odbc-requirements"]{ODBC Requirements}
|
@section[#:tag "odbc-requirements"]{ODBC Requirements}
|
||||||
|
|
||||||
ODBC requires the appropriate driver manager native library as well as
|
ODBC requires the appropriate driver manager native library as well as
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
racket/sandbox
|
racket/sandbox
|
||||||
"config.rkt"
|
"config.rkt"
|
||||||
"tabbing.rkt"
|
"tabbing.rkt"
|
||||||
(for-label db db/util/geometry db/util/postgresql))
|
(for-label db db/util/geometry db/util/postgresql racket/dict))
|
||||||
|
|
||||||
@title[#:tag "query-api"]{Queries}
|
@title[#:tag "query-api"]{Queries}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ raises an exception. Different query functions impose different
|
||||||
constraints on the query results and offer different mechanisms for
|
constraints on the query results and offer different mechanisms for
|
||||||
processing the results.
|
processing the results.
|
||||||
|
|
||||||
@bold{Errors} In most cases, a query error does not cause the
|
@parheading{Errors} In most cases, a query error does not cause the
|
||||||
connection to be disconnected. Specifically, the following kinds of
|
connection to be disconnected. Specifically, the following kinds of
|
||||||
errors should never cause a connection to be disconnected:
|
errors should never cause a connection to be disconnected:
|
||||||
@itemize[
|
@itemize[
|
||||||
|
@ -43,7 +43,7 @@ disconnected:
|
||||||
See @secref["transactions"] for information on how errors can affect
|
See @secref["transactions"] for information on how errors can affect
|
||||||
the transaction status.
|
the transaction status.
|
||||||
|
|
||||||
@bold{Character encoding} This library is designed to interact with
|
@parheading{Character encoding} This library is designed to interact with
|
||||||
database systems using the UTF-8 character encoding. The connection
|
database systems using the UTF-8 character encoding. The connection
|
||||||
functions attempt to negotiate UTF-8 communication at the beginning of
|
functions attempt to negotiate UTF-8 communication at the beginning of
|
||||||
every connection, but some systems also allow the character encoding
|
every connection, but some systems also allow the character encoding
|
||||||
|
@ -53,13 +53,12 @@ and data might get corrupted in transmission. Avoid changing a
|
||||||
connection's character encoding. When possible, the connection will
|
connection's character encoding. When possible, the connection will
|
||||||
observe the change and automatically disconnect with an error.
|
observe the change and automatically disconnect with an error.
|
||||||
|
|
||||||
@bold{Synchronization} Connections are internally synchronized: it is
|
@parheading{Synchronization} Connections are internally synchronized:
|
||||||
safe to perform concurrent queries on the same connection object from
|
it is safe to use a connection from different threads
|
||||||
different threads. Connections are not kill-safe: killing a thread
|
concurrently. Most connections are not kill-safe: killing a thread
|
||||||
that is using a connection---or shutting down the connection's
|
that is using a connection may leave the connection locked, causing
|
||||||
managing custodian---may leave the connection locked, causing future
|
future operations to block indefinitely. See also
|
||||||
operations to block indefinitely. See @secref["kill-safe"] for a
|
@secref["kill-safe"].
|
||||||
way to make kill-safe connections.
|
|
||||||
|
|
||||||
|
|
||||||
@section{Statements}
|
@section{Statements}
|
||||||
|
@ -77,16 +76,14 @@ All query functions require both a connection and a
|
||||||
]
|
]
|
||||||
|
|
||||||
A SQL statement may contain parameter placeholders that stand for SQL
|
A SQL statement may contain parameter placeholders that stand for SQL
|
||||||
scalar values. The parameter values must be supplied when the
|
scalar values; such statements are called @deftech{parameterized
|
||||||
statement is executed; the parameterized statement and parameter
|
queries}. The parameter values must be supplied when the statement is
|
||||||
values are sent to the database back end, which combines them
|
executed; the parameterized statement and parameter values are sent to
|
||||||
correctly and safely.
|
the database back end, which combines them correctly and safely.
|
||||||
|
|
||||||
Use parameters instead of Racket string interpolation (eg,
|
Use parameters instead of Racket string interpolation (eg,
|
||||||
@racket[format] or @racket[string-append]) to avoid
|
@racket[format] or @racket[string-append]) to avoid
|
||||||
@hyperlink["http://xkcd.com/327/"]{SQL injection}, where a string
|
@secref["dbsec-sql-injection"].
|
||||||
intended to represent a SQL scalar value is interpreted as---possibly
|
|
||||||
malicious---SQL code instead.
|
|
||||||
|
|
||||||
The syntax of placeholders varies depending on the database
|
The syntax of placeholders varies depending on the database
|
||||||
system. For example:
|
system. For example:
|
||||||
|
@ -140,11 +137,13 @@ The types of parameters and returned fields are described in
|
||||||
@defproc[(query-rows [connection connection?]
|
@defproc[(query-rows [connection connection?]
|
||||||
[stmt statement?]
|
[stmt statement?]
|
||||||
[arg any/c] ...
|
[arg any/c] ...
|
||||||
[#:group grouping-fields
|
[#:group groupings
|
||||||
(or/c (vectorof string?) (listof (vectorof string?)))
|
(let* ([field/c (or/c string? exact-nonnegative-integer?)]
|
||||||
|
[grouping/c (or/c field/c (vectorof field/c))])
|
||||||
|
(or/c grouping/c (listof grouping/c)))
|
||||||
null]
|
null]
|
||||||
[#:group-mode group-mode
|
[#:group-mode group-mode
|
||||||
(listof (or/c 'preserve-null-rows 'list))
|
(listof (or/c 'preserve-null 'list))
|
||||||
null])
|
null])
|
||||||
(listof vector?)]{
|
(listof vector?)]{
|
||||||
|
|
||||||
|
@ -158,7 +157,7 @@ The types of parameters and returned fields are described in
|
||||||
(list (vector 17))]
|
(list (vector 17))]
|
||||||
]
|
]
|
||||||
|
|
||||||
If @racket[grouping-fields] is not empty, the result is the same as if
|
If @racket[groupings] is not empty, the result is the same as if
|
||||||
@racket[group-rows] had been called on the result rows.
|
@racket[group-rows] had been called on the result rows.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +244,15 @@ The types of parameters and returned fields are described in
|
||||||
@defproc[(in-query [connection connection?]
|
@defproc[(in-query [connection connection?]
|
||||||
[stmt statement?]
|
[stmt statement?]
|
||||||
[arg any/c] ...
|
[arg any/c] ...
|
||||||
[#:fetch fetch-size (or/c exact-positive-integer? +inf.0) +inf.0])
|
[#:fetch fetch-size (or/c exact-positive-integer? +inf.0) +inf.0]
|
||||||
|
[#:group groupings
|
||||||
|
(let* ([field/c (or/c string? exact-nonnegative-integer?)]
|
||||||
|
[grouping/c (or/c field/c (vectorof field/c))])
|
||||||
|
(or/c grouping/c (listof grouping/c)))
|
||||||
|
null]
|
||||||
|
[#:group-mode group-mode
|
||||||
|
(listof (or/c 'preserve-null 'list))
|
||||||
|
null])
|
||||||
sequence?]{
|
sequence?]{
|
||||||
|
|
||||||
Executes a SQL query, which must produce rows, and returns a
|
Executes a SQL query, which must produce rows, and returns a
|
||||||
|
@ -260,6 +267,11 @@ The types of parameters and returned fields are described in
|
||||||
open cursors; attempting to fetch more rows may fail. On PostgreSQL,
|
open cursors; attempting to fetch more rows may fail. On PostgreSQL,
|
||||||
a cursor can be opened only within a transaction.
|
a cursor can be opened only within a transaction.
|
||||||
|
|
||||||
|
If @racket[groupings] is not empty, the result is the same as
|
||||||
|
if @racket[group-rows] had been called on the result rows. If
|
||||||
|
@racket[groupings] is not empty, then @racket[fetch-size] must
|
||||||
|
be @racket[+inf.0]; otherwise, an exception is raised.
|
||||||
|
|
||||||
@examples/results[
|
@examples/results[
|
||||||
[(for/list ([n (in-query pgc "select n from the_numbers where n < 2")])
|
[(for/list ([n (in-query pgc "select n from the_numbers where n < 2")])
|
||||||
n)
|
n)
|
||||||
|
@ -329,25 +341,29 @@ future version of this library (even new minor versions).
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(group-rows [result rows-result?]
|
@defproc[(group-rows [result rows-result?]
|
||||||
[#:group grouping-fields
|
[#:group groupings
|
||||||
(or/c (vectorof string?) (listof (vectorof string?)))]
|
(let* ([field/c (or/c string? exact-nonnegative-integer?)]
|
||||||
|
[grouping/c (or/c field/c (vectorof field/c))])
|
||||||
|
(or/c grouping/c (listof grouping/c)))]
|
||||||
[#:group-mode group-mode
|
[#:group-mode group-mode
|
||||||
(listof (or/c 'preserve-null-rows 'list))
|
(listof (or/c 'preserve-null 'list))
|
||||||
null])
|
null])
|
||||||
rows-result?]{
|
rows-result?]{
|
||||||
|
|
||||||
If @racket[grouping-fields] is a vector, the elements must be names of
|
If @racket[groupings] is a vector, the elements must be names of
|
||||||
fields in @racket[result], and @racket[result]'s rows are regrouped
|
fields in @racket[result], and @racket[result]'s rows are regrouped
|
||||||
using the given fields. Each grouped row contains N+1 fields; the
|
using the given fields. Each grouped row contains N+1 fields; the
|
||||||
first N fields are the @racket[grouping-fields], and the final field
|
first N fields are the @racket[groupings], and the final field
|
||||||
is a list of ``residual rows'' over the rest of the fields. A residual
|
is a list of ``residual rows'' over the rest of the fields. A residual
|
||||||
row of all NULLs is dropped (for convenient processing of @tt{OUTER
|
row of all NULLs is dropped (for convenient processing of @tt{OUTER
|
||||||
JOIN} results) unless @racket[group-mode] includes
|
JOIN} results) unless @racket[group-mode] includes
|
||||||
@racket['preserve-null-rows]. If @racket[group-mode] contains
|
@racket['preserve-null]. If @racket[group-mode] contains
|
||||||
@racket['list], there must be exactly one residual field, and its
|
@racket['list], there must be exactly one residual field, and its
|
||||||
values are included without a vector wrapper (similar to
|
values are included without a vector wrapper (similar to
|
||||||
@racket[query-list]).
|
@racket[query-list]).
|
||||||
|
|
||||||
|
See also @secref["dbperf-n+1"].
|
||||||
|
|
||||||
@examples[#:eval the-eval
|
@examples[#:eval the-eval
|
||||||
(define vehicles-result
|
(define vehicles-result
|
||||||
(rows-result
|
(rows-result
|
||||||
|
@ -361,7 +377,9 @@ values are included without a vector wrapper (similar to
|
||||||
#:group '(#("type")))
|
#:group '(#("type")))
|
||||||
]
|
]
|
||||||
|
|
||||||
The @racket[grouping-fields] argument may also be a list of vectors;
|
The grouped final column is given the name @racket["grouped"].
|
||||||
|
|
||||||
|
The @racket[groupings] argument may also be a list of vectors;
|
||||||
in that case, the grouping process is repeated for each set of
|
in that case, the grouping process is repeated for each set of
|
||||||
grouping fields. The grouping fields must be distinct.
|
grouping fields. The grouping fields must be distinct.
|
||||||
|
|
||||||
|
@ -372,6 +390,38 @@ grouping fields. The grouping fields must be distinct.
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@defproc[(rows->dict [result rows-result?]
|
||||||
|
[#:key key-field/s
|
||||||
|
(let ([field/c (or/c string? exact-nonnegative-integer?)])
|
||||||
|
(or/c field/c (vectorof field/c)))]
|
||||||
|
[#:value value-field/s
|
||||||
|
(let ([field/c (or/c string? exact-nonnegative-integer?)])
|
||||||
|
(or/c field/c (vectorof field/c)))]
|
||||||
|
[#:value-mode value-mode
|
||||||
|
(listof (or/c 'list 'preserve-null))
|
||||||
|
null])
|
||||||
|
dict?]{
|
||||||
|
|
||||||
|
Creates a dictionary mapping @racket[key-field/s] to
|
||||||
|
@racket[value-field/s]. If @racket[key-field/s] is a single field name
|
||||||
|
or index, the keys are the field values; if @racket[key-field/s] is a
|
||||||
|
vector, the keys are vectors of the field values. Likewise for
|
||||||
|
@racket[value-field/s].
|
||||||
|
|
||||||
|
If @racket[value-mode] contains @racket['list], a list of values is
|
||||||
|
accumulated for each key; otherwise, there must be at most one value
|
||||||
|
for each key. Values consisting of all @racket[sql-null?] values are
|
||||||
|
dropped unless @racket[value-mode] contains
|
||||||
|
@racket['preserve-null].
|
||||||
|
|
||||||
|
@examples[#:eval the-eval
|
||||||
|
(rows->dict vehicles-result
|
||||||
|
#:key "model" #:value '#("type" "maker"))
|
||||||
|
(rows->dict vehicles-result
|
||||||
|
#:key "maker" #:value "model" #:value-mode '(list))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@section{Prepared Statements}
|
@section{Prepared Statements}
|
||||||
|
|
||||||
|
@ -533,7 +583,7 @@ statement implicitly commits the current transaction. These statements
|
||||||
also must not be used within @tech{managed transactions}. (In
|
also must not be used within @tech{managed transactions}. (In
|
||||||
contrast, PostgreSQL and SQLite both support transactional DDL.)
|
contrast, PostgreSQL and SQLite both support transactional DDL.)
|
||||||
|
|
||||||
@bold{Errors} Query errors may affect an open transaction in one of
|
@parheading{Errors} Query errors may affect an open transaction in one of
|
||||||
three ways:
|
three ways:
|
||||||
@itemlist[#:style 'ordered
|
@itemlist[#:style 'ordered
|
||||||
@item{the transaction remains open and unchanged}
|
@item{the transaction remains open and unchanged}
|
||||||
|
|
511
collects/db/scribblings/using-db.scrbl
Normal file
511
collects/db/scribblings/using-db.scrbl
Normal file
|
@ -0,0 +1,511 @@
|
||||||
|
#lang scribble/doc
|
||||||
|
@(require scribble/manual
|
||||||
|
scribble/eval
|
||||||
|
scribble/struct
|
||||||
|
racket/sandbox
|
||||||
|
"config.rkt"
|
||||||
|
(for-label db db/util/testing racket/dict web-server/lang/web))
|
||||||
|
|
||||||
|
@title[#:tag "using-db"]{Using Database Connections}
|
||||||
|
|
||||||
|
This section introduces this library's basic features and covers some
|
||||||
|
practical issues with database programming in general and with this
|
||||||
|
library in particular.
|
||||||
|
|
||||||
|
|
||||||
|
@section[#:tag "intro-basic"]{Introduction to Using Database Connections}
|
||||||
|
|
||||||
|
The following annotated program demonstrates how to connect to a
|
||||||
|
database and perform simple queries. Some of the SQL syntax used below
|
||||||
|
is PostgreSQL-specific, such as the syntax of query parameters
|
||||||
|
(@litchar{$1} rather than @litchar{?}).
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(require db)
|
||||||
|
(void)]
|
||||||
|
]
|
||||||
|
|
||||||
|
First we create a connection. Replace @racket[_user], @racket[_db],
|
||||||
|
and @racket[_password] below with the appropriate values for your
|
||||||
|
configuration (see @secref{creating-connections} for other connection examples):
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(define pgc
|
||||||
|
(postgresql-connect #:user _user
|
||||||
|
#:database _db
|
||||||
|
#:password _password))
|
||||||
|
(void)]
|
||||||
|
[pgc
|
||||||
|
(new connection%)]
|
||||||
|
]
|
||||||
|
|
||||||
|
Use @racket[query-exec] method to execute a SQL statement for effect.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-exec pgc
|
||||||
|
"create temporary table the_numbers (n integer, d varchar(20))")
|
||||||
|
(void)]
|
||||||
|
[(query-exec pgc
|
||||||
|
"insert into the_numbers values (0, 'nothing')")
|
||||||
|
(void)]
|
||||||
|
[(query-exec pgc
|
||||||
|
"insert into the_numbers values (1, 'the loneliest number')")
|
||||||
|
(void)]
|
||||||
|
[(query-exec pgc
|
||||||
|
"insert into the_numbers values (2, 'company')")
|
||||||
|
(void)]
|
||||||
|
]
|
||||||
|
|
||||||
|
The @racket[query] function is a more general way to execute a
|
||||||
|
statement. It returns a structure encapsulating information about the
|
||||||
|
statement's execution. (But some of that information varies from
|
||||||
|
system to system and is subject to change.)
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query pgc "insert into the_numbers values (3, 'a crowd')")
|
||||||
|
(simple-result '((command insert 0 1)))]
|
||||||
|
[(query pgc "select n, d from the_numbers where n % 2 = 0")
|
||||||
|
(rows-result
|
||||||
|
(list
|
||||||
|
'((name . "n") (typeid . 23))
|
||||||
|
'((name . "d") (typeid . 1043)))
|
||||||
|
'(#(0 "nothing") #(2 "company")))]
|
||||||
|
]
|
||||||
|
|
||||||
|
When the query is known to return rows and when the field
|
||||||
|
descriptions are not needed, it is more convenient to use the
|
||||||
|
@racket[query-rows] function.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-rows pgc "select n, d from the_numbers where n % 2 = 0")
|
||||||
|
'(#(0 "nothing") #(2 "company"))]
|
||||||
|
]
|
||||||
|
|
||||||
|
Use @racket[query-row] for queries that are known to return exactly
|
||||||
|
one row.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-row pgc "select * from the_numbers where n = 0")
|
||||||
|
(vector 0 "nothing")]
|
||||||
|
]
|
||||||
|
|
||||||
|
Similarly, use @racket[query-list] for queries that produce rows of
|
||||||
|
exactly one column.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-list pgc "select d from the_numbers order by n")
|
||||||
|
(list "nothing" "the loneliest number" "company" "a crowd")]
|
||||||
|
]
|
||||||
|
|
||||||
|
When a query is known to return a single value (one row and one
|
||||||
|
column), use @racket[query-value].
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-value pgc "select count(*) from the_numbers")
|
||||||
|
4]
|
||||||
|
[(query-value pgc "select d from the_numbers where n = 5")
|
||||||
|
(error 'query-value
|
||||||
|
"query returned zero rows: ~s"
|
||||||
|
"select d from the_numbers where n = 5")]
|
||||||
|
]
|
||||||
|
|
||||||
|
When a query may return zero or one rows, as the last example, use
|
||||||
|
@racket[query-maybe-row] or @racket[query-maybe-value] instead.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-maybe-value pgc "select d from the_numbers where n = 5")
|
||||||
|
(values #f)]
|
||||||
|
]
|
||||||
|
|
||||||
|
The @racket[in-query] function produces a sequence that can be used
|
||||||
|
with Racket's iteration forms:
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(for ([(n d) (in-query pgc "select * from the_numbers where n < 4")])
|
||||||
|
(printf "~a is ~a\n" n d))
|
||||||
|
(for-each (lambda (n d) (printf "~a: ~a\n" n d))
|
||||||
|
'(0 1 2 3)
|
||||||
|
'("nothing" "the loneliest number" "company" "a crowd"))]
|
||||||
|
[(for/fold ([sum 0]) ([n (in-query pgc "select n from the_numbers")])
|
||||||
|
(+ sum n))
|
||||||
|
(for/fold ([sum 0]) ([n (in-list '(0 1 2 3))])
|
||||||
|
(+ sum n))]
|
||||||
|
]
|
||||||
|
|
||||||
|
Errors in queries generally do not cause the connection to disconnect.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(begin (with-handlers [(exn:fail?
|
||||||
|
(lambda (e)
|
||||||
|
(printf "~a~n" (exn-message e))))]
|
||||||
|
(query-value pgc "select NoSuchField from NoSuchTable"))
|
||||||
|
(query-value pgc "select 'okay to proceed!'"))
|
||||||
|
(begin (display "query-value: relation \"nosuchtable\" does not exist (SQLSTATE 42P01)")
|
||||||
|
"okay to proceed!")]
|
||||||
|
]
|
||||||
|
|
||||||
|
Queries may contain parameters. The easiest way to execute a
|
||||||
|
parameterized query is to provide the parameters ``inline'' after the
|
||||||
|
SQL statement in the query function call.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(query-value pgc
|
||||||
|
"select d from the_numbers where n = $1" 2)
|
||||||
|
"company"]
|
||||||
|
[(query-list pgc
|
||||||
|
"select n from the_numbers where n > $1 and n < $2" 0 3)
|
||||||
|
(list 1 2)]
|
||||||
|
]
|
||||||
|
|
||||||
|
Alternatively, a parameterized query may be prepared in advance and
|
||||||
|
executed later. @tech{Prepared statements} can be executed multiple
|
||||||
|
times with different parameter values.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(define get-less-than-pst
|
||||||
|
(prepare pgc "select n from the_numbers where n < $1"))
|
||||||
|
(void)]
|
||||||
|
[(query-list pgc get-less-than-pst 1)
|
||||||
|
(list 0)]
|
||||||
|
[(query-list pgc (bind-prepared-statement get-less-than-pst '(2)))
|
||||||
|
(list 0 1)]
|
||||||
|
]
|
||||||
|
|
||||||
|
When a connection's work is done, it should be disconnected.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(disconnect pgc)
|
||||||
|
(void)]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@section[#:tag "dbsec"]{Database Security}
|
||||||
|
|
||||||
|
Database security requires both that the database back end be secured
|
||||||
|
against unauthorized use and that authorized clients are not tricked
|
||||||
|
or subverted into violating the database's security.
|
||||||
|
|
||||||
|
Securing database back ends is mostly beyond the scope of this
|
||||||
|
manual. In brief: choose sufficiently strong authentication methods
|
||||||
|
and keep credentials secure, and follow the
|
||||||
|
@wplink["Principle_of_least_privilege"]{principle of least privilege}:
|
||||||
|
create and use roles that have the minimum permissions needed.
|
||||||
|
|
||||||
|
The following is an incomplete list of security issues related to
|
||||||
|
database @emph{client} programming.
|
||||||
|
|
||||||
|
@;{Add section on db roles and ro/rw access?
|
||||||
|
eg, for servlet create two connections: one ro and one rw
|
||||||
|
(in pg/my/etc, backed by two roles; in sqlite, connection options) }
|
||||||
|
|
||||||
|
@subsection[#:tag "dbsec-sql-injection"]{SQL Injection}
|
||||||
|
|
||||||
|
@wplink["SQL_injection"]{SQL injection} happens when part of a SQL
|
||||||
|
statement that was intended as SQL literal data is instead interpreted
|
||||||
|
as SQL code---possibly @hyperlink["http://xkcd.com/327/"]{malicious}
|
||||||
|
SQL code.
|
||||||
|
|
||||||
|
Avoid dynamically creating SQL query strings by string concatenation
|
||||||
|
or interpolation (eg, with @racket[string-append] or
|
||||||
|
@racket[format]). In most cases, it is possible to use
|
||||||
|
@tech{parameterized queries} instead. For example, instead of this
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(code:comment "WRONG! DANGER!")
|
||||||
|
(query-exec c
|
||||||
|
(format "UPDATE users SET passwd='~a' WHERE user='~a'"
|
||||||
|
user new-passwd))
|
||||||
|
]
|
||||||
|
write one of the following instead (depending on SQL dialect):
|
||||||
|
@racketblock[
|
||||||
|
(code:comment "for PostgreSQL, SQLite")
|
||||||
|
(query-exec c "UPDATE users SET passwd=$1 WHERE user=$2" user new-passwd)
|
||||||
|
(code:comment "for MySQL, SQLite, ODBC")
|
||||||
|
(query-exec c "UPDATE users SET passwd=? WHERE user=?" user new-passwd)
|
||||||
|
]
|
||||||
|
|
||||||
|
The first form would choke on names like @racket["Patrick O'Connor"].
|
||||||
|
Worse, it would be susceptible to attack by malicious input like
|
||||||
|
@racket["me' OR user='root'"], which yields the following SQL
|
||||||
|
statement:
|
||||||
|
|
||||||
|
@(element 'tt "UPDATE users SET passwd='whatever' WHERE user='me' OR user='root'")
|
||||||
|
|
||||||
|
In contrast, using a @tech{parameterized query} causes the
|
||||||
|
parameterized SQL and its arguments to be submitted to the back end
|
||||||
|
separately; the back end then combines them safely.
|
||||||
|
|
||||||
|
Only SQL literal values can be replaced with parameter placeholders; a
|
||||||
|
SQL statement cannot be parameterized over a column name or a sort
|
||||||
|
order, for example. In such cases, constructing the query dynamically
|
||||||
|
may be the only feasible solution. But while the query construction
|
||||||
|
may be influenced by external input, it should never directly
|
||||||
|
incorporate external input without validation. That is, don't do the
|
||||||
|
following:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(code:comment "WRONG! DANGER!")
|
||||||
|
(query-rows c
|
||||||
|
(format "SELECT name, ~a FROM contestants" column))
|
||||||
|
(query-list c
|
||||||
|
(format "SELECT name FROM contestants ORDER BY score ~a" direction))
|
||||||
|
]
|
||||||
|
|
||||||
|
Instead, select the inserted SQL from known good alternatives:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(code:comment "BETTER")
|
||||||
|
(query-rows c
|
||||||
|
(format "SELECT name, ~a FROM contestants"
|
||||||
|
(cond [(member column '("wins" "losses")) column]
|
||||||
|
[else (error ....)])))
|
||||||
|
(query-list c
|
||||||
|
(format "SELECT name FROM contestants ORDER BY score ~a"
|
||||||
|
(if ascending? "ASC" "DESC")))
|
||||||
|
]
|
||||||
|
|
||||||
|
@;{ Discuss dynamic IN comparisons? }
|
||||||
|
|
||||||
|
|
||||||
|
@subsection[#:tag "dbsec-xss"]{Cross-site Scripting (XSS)}
|
||||||
|
|
||||||
|
@wplink["Cross-site_scripting"]{Cross-site scripting}---which should
|
||||||
|
probably be called ``HTML injection'' or ``markup injection''---is
|
||||||
|
when arbitrary text from an untrusted source is embedded without
|
||||||
|
escaping into an HTML page. The @emph{unstructured text from the
|
||||||
|
untrusted source} is reinterpreted as @emph{markup from the web
|
||||||
|
server}; if the reinterpreted markup contains embedded Javascript
|
||||||
|
code, it executes with the security privileges associated with the web
|
||||||
|
server's domain.
|
||||||
|
|
||||||
|
This issue has little to do with databases @emph{per se} except that
|
||||||
|
such text is often stored in a database. This issue is mitigated by
|
||||||
|
using structured markup representations like SXML or X-expressions
|
||||||
|
(xexprs), since they automatically escape ``markup'' characters found
|
||||||
|
in embedded text.
|
||||||
|
|
||||||
|
|
||||||
|
@;{============================================================}
|
||||||
|
|
||||||
|
@section[#:tag "dbperf"]{Database Performance}
|
||||||
|
|
||||||
|
Achieving good database performance mostly consists of good database
|
||||||
|
design and intelligent client behavior.
|
||||||
|
|
||||||
|
On the database design side, most important are wise use of indexes
|
||||||
|
and choosing appropriate data representations. As an example of the
|
||||||
|
latter, a regexp-based search using @tt{LIKE} will probably be slower
|
||||||
|
than a specialized
|
||||||
|
@hyperlink["http://www.postgresql.org/docs/9.0/static/textsearch.html"]{full-text
|
||||||
|
search} feature for large data sets. Consult your database back end's
|
||||||
|
manual for additional performance advice.
|
||||||
|
|
||||||
|
The following sections describe a few client-side aspects of
|
||||||
|
performance.
|
||||||
|
|
||||||
|
@subsection[#:tag "dbperf-n+1"]{The N+1 Selects Problem}
|
||||||
|
|
||||||
|
@;{ per comments on http://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem
|
||||||
|
Is N+1 actually a problem?
|
||||||
|
ie, Is communication overhead with db back end worse than
|
||||||
|
grouping cost? Should measure to see. }
|
||||||
|
|
||||||
|
A common mistake is to fetch a large amount of data by running a query
|
||||||
|
to get a set of initial records and then running another query inside
|
||||||
|
a loop with an iteration for each of the initial records. This is
|
||||||
|
sometimes called the ``n+1 selects problem.'' For example:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(for/list ([(name id) (in-query c "SELECT name, id FROM contestants")])
|
||||||
|
(define wins
|
||||||
|
(query-list c "SELECT contest FROM contests WHERE winner = $1" id))
|
||||||
|
(make-contestant-record name wins))
|
||||||
|
]
|
||||||
|
|
||||||
|
The same information can be retrieved in a single query by performing
|
||||||
|
a @tt{LEFT OUTER JOIN} and grouping the results:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(for/list ([(name id wins)
|
||||||
|
(in-query c
|
||||||
|
(string-append "SELECT name, id, contest "
|
||||||
|
"FROM contestants LEFT OUTER JOIN contests "
|
||||||
|
"ON contestants.id = contests.winner")
|
||||||
|
#:group '(#("name" "id"))
|
||||||
|
#:group-mode '(list))])
|
||||||
|
(make-contestant-record name wins))
|
||||||
|
]
|
||||||
|
|
||||||
|
The one-query form will perform better when database communication has
|
||||||
|
high latency. On the other hand, it may duplicate the contents of the
|
||||||
|
non-key @tt{name} column, using more bandwidth. Another approach is to
|
||||||
|
perform two queries:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(let ([id=>name
|
||||||
|
(rows->dict #:key "id" #:value "name"
|
||||||
|
(query c "SELECT id, name FROM contestants"))])
|
||||||
|
(for/list ([(id wins)
|
||||||
|
(in-query c
|
||||||
|
(string-append "SELECT id, contest "
|
||||||
|
"FROM contestants LEFT OUTER JOIN contests "
|
||||||
|
"ON contestants.id = contests.winner")
|
||||||
|
#:group '(#("id"))
|
||||||
|
#:group-mode '(list))])
|
||||||
|
(make-contestant-record (dict-ref id=>name id) wins)))
|
||||||
|
]
|
||||||
|
|
||||||
|
Compared with the one-query form, the two-query form requires
|
||||||
|
additional communication, but it avoids duplicating @tt{name} values
|
||||||
|
in the @tt{OUTER JOIN} results. If additional non-key @tt{contestant}
|
||||||
|
fields were to be retrieved, the bandwidth savings of this approach
|
||||||
|
would be even greater.
|
||||||
|
|
||||||
|
See also @secref["dbperf-testing"].
|
||||||
|
|
||||||
|
|
||||||
|
@subsection[#:tag "dbperf-update-tx"]{Updates and Transactions}
|
||||||
|
|
||||||
|
Using transactions can dramatically improve the performance of bulk
|
||||||
|
database operations, especially @tt{UPDATE} and @tt{INSERT}
|
||||||
|
statements. As an extreme example, on commodity hardware in 2012,
|
||||||
|
SQLite is capable of executing thousands of @tt{INSERT} statements per
|
||||||
|
second within a transaction, but it is capable of only dozens of
|
||||||
|
single-@tt{INSERT} transactions per second.
|
||||||
|
|
||||||
|
|
||||||
|
@subsection[#:tag "dbperf-pstcache"]{Statement Caching}
|
||||||
|
|
||||||
|
Connections cache implicitly prepared statements (that is, statements
|
||||||
|
given in string form directly to a query function). The effect of the
|
||||||
|
cache is to eliminate an extra round-trip to the server (to send the
|
||||||
|
statement and receive a prepared statement handle), leaving just a
|
||||||
|
single round-trip (to send parameters and receive results) per
|
||||||
|
execution.
|
||||||
|
|
||||||
|
Currently, prepared statements are only cached within a
|
||||||
|
transaction. The statement cache is flushed when entering or leaving a
|
||||||
|
transaction and whenever a DDL statement is executed.
|
||||||
|
|
||||||
|
@;{ virtual statements are mostly obsolete }
|
||||||
|
|
||||||
|
|
||||||
|
@subsection[#:tag "dbperf-testing"]{Testing Performance of Database-Backed Programs}
|
||||||
|
|
||||||
|
When testing the performance of database-backed programs, remember to
|
||||||
|
test them in environments with realistic latency and
|
||||||
|
bandwidth. High-latency environments may be roughly approximated with
|
||||||
|
the @racket[high-latency-connection] function, but there's no
|
||||||
|
substitute for the real thing.
|
||||||
|
|
||||||
|
|
||||||
|
@;{============================================================}
|
||||||
|
|
||||||
|
@section[#:tag "intro-servlets"]{Databases and Web Servlets}
|
||||||
|
|
||||||
|
Using database connections in a web servlet is more complicated than
|
||||||
|
in a standalone program. A single servlet potentially serves many
|
||||||
|
requests at once, each in a separate request-handling
|
||||||
|
thread. Furthermore, the use of @racket[send/suspend],
|
||||||
|
@racket[send/suspend/dispatch], etc means that there are many places
|
||||||
|
where a servlet may start and stop executing to service a request.
|
||||||
|
|
||||||
|
Why not use a single connection to handle all of a servlet's requests?
|
||||||
|
That is, create the connection with the servlet instance and never
|
||||||
|
disconnect it. Such a servlet would look something like the following:
|
||||||
|
|
||||||
|
@racketmod[
|
||||||
|
#:file "bad-servlet.rkt"
|
||||||
|
web-server
|
||||||
|
(require db)
|
||||||
|
(define db-conn (postgresql-connect ....))
|
||||||
|
(define (serve req)
|
||||||
|
.... db-conn ....)
|
||||||
|
]
|
||||||
|
|
||||||
|
The main problem with using one connection for all requests is that
|
||||||
|
multiple threads accessing the same connection are not properly
|
||||||
|
@wplink["Isolation_%28database_systems%29"]{isolated}. For example, if
|
||||||
|
one thread is accessing the connection within a transaction and
|
||||||
|
another thread issues a query, the second thread may see invalid data
|
||||||
|
or even disrupt the work of the first thread.
|
||||||
|
|
||||||
|
A secondary problem is performance. A connection can only perform a
|
||||||
|
single query at a time, whereas most database systems are capable of
|
||||||
|
concurrent query processing.
|
||||||
|
|
||||||
|
The proper way to use database connections in a servlet is to create a
|
||||||
|
connection for each request and disconnect it when the request has
|
||||||
|
been handled. But since a request thread may start and stop executing
|
||||||
|
in many places (due to @racket[send/suspend], etc), inserting the code
|
||||||
|
to connect and disconnect at the proper places can be challenging and
|
||||||
|
messy.
|
||||||
|
|
||||||
|
A better solution is to use a @tech{virtual connection}, which
|
||||||
|
automatically creates a request-specific (that is, thread-specific)
|
||||||
|
``actual connection'' by need and disconnects it when the request has
|
||||||
|
been handled (that is, when the thread terminates). Different
|
||||||
|
request-handling threads using the same virtual connection are
|
||||||
|
assigned different actual connections, so the requests are properly
|
||||||
|
isolated.
|
||||||
|
|
||||||
|
@racketmod[
|
||||||
|
#:file "better-servlet.rkt"
|
||||||
|
web-server
|
||||||
|
(require db)
|
||||||
|
(define db-conn
|
||||||
|
(virtual-connection
|
||||||
|
(lambda () (postgresql-connect ....))))
|
||||||
|
(define (serve req)
|
||||||
|
.... db-conn ....)
|
||||||
|
]
|
||||||
|
|
||||||
|
This solution preserves the simplicity of the naive solution and fixes
|
||||||
|
the isolation problem but at the cost of creating many short-lived
|
||||||
|
database connections. That cost can be eliminated by using a
|
||||||
|
@tech{connection pool}:
|
||||||
|
|
||||||
|
@racketmod[
|
||||||
|
#:file "best-servlet.rkt"
|
||||||
|
web-server
|
||||||
|
(require db)
|
||||||
|
(define db-conn
|
||||||
|
(virtual-connection
|
||||||
|
(connection-pool
|
||||||
|
(lambda () (postgresql-connect ....)))))
|
||||||
|
(define (serve req)
|
||||||
|
.... db-conn ....)
|
||||||
|
]
|
||||||
|
|
||||||
|
By using a virtual connection backed by a connection pool, a servlet
|
||||||
|
can achieve simplicity, isolation, and performance all at the same
|
||||||
|
time.
|
||||||
|
|
||||||
|
@;{
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- talk about virtual statements, too
|
||||||
|
- show actual working servlet code
|
||||||
|
|
||||||
|
--
|
||||||
|
|
||||||
|
A prepared statement is tied to the connection used to create it;
|
||||||
|
attempting to use it with another connection results in an
|
||||||
|
error. Unfortunately, in some scenarios such as web servlets, the
|
||||||
|
lifetimes of connections are short or difficult to track, making
|
||||||
|
prepared statements inconvenient. In such cases, a better tool is the
|
||||||
|
@tech{virtual statement}, which prepares statements on demand and
|
||||||
|
caches them for future use with the same connection.
|
||||||
|
|
||||||
|
@my-interaction[
|
||||||
|
[(define get-less-than-pst
|
||||||
|
(virtual-statement "select n from the_numbers where n < $1"))
|
||||||
|
(void)]
|
||||||
|
[(code:line (query-list pgc1 get-less-than-pst 1) (code:comment "prepares statement for pgc1"))
|
||||||
|
(list 0)]
|
||||||
|
[(code:line (query-list pgc2 get-less-than-pst 2) (code:comment "prepares statement for pgc2"))
|
||||||
|
(list 0 1)]
|
||||||
|
[(code:line (query-list pgc1 get-less-than-pst 3) (code:comment "uses existing prep. stmt."))
|
||||||
|
(list 0 1 2)]
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
scribble/struct
|
scribble/struct
|
||||||
racket/sandbox
|
racket/sandbox
|
||||||
"config.rkt"
|
"config.rkt"
|
||||||
(for-label db db/util/datetime db/util/geometry db/util/postgresql))
|
(for-label db db/util/datetime db/util/geometry db/util/postgresql db/util/testing))
|
||||||
|
|
||||||
@title[#:tag "util"]{Utilities}
|
@title[#:tag "util"]{Utilities}
|
||||||
|
|
||||||
|
@ -203,3 +203,33 @@ types that have no appropriate analogue in the OpenGIS model:
|
||||||
Note: PostgreSQL's built-in geometric types are distinct from those
|
Note: PostgreSQL's built-in geometric types are distinct from those
|
||||||
provided by the PostGIS extension library (see @secref["geometry"]).
|
provided by the PostGIS extension library (see @secref["geometry"]).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@;{========================================}
|
||||||
|
|
||||||
|
@section[#:tag "util-testing"]{Testing Database Programs}
|
||||||
|
|
||||||
|
@defmodule[db/util/testing]
|
||||||
|
|
||||||
|
This module provides utilities for testing programs that use database
|
||||||
|
connections.
|
||||||
|
|
||||||
|
@defproc[(high-latency-connection [connection connection?]
|
||||||
|
[latency (>=/c 0)]
|
||||||
|
[#:sleep-atomic? sleep-atomic? any/c #f])
|
||||||
|
connection?]{
|
||||||
|
|
||||||
|
Returns a proxy connection for @racket[connection] that introduces
|
||||||
|
@racket[latency] additional seconds of latency before operations that
|
||||||
|
require communicating with the database back end---@racket[prepare],
|
||||||
|
@racket[query], @racket[start-transaction], etc.
|
||||||
|
|
||||||
|
Use this function in performance testing to roughly simulate
|
||||||
|
environments with high-latency communication with a database back
|
||||||
|
end.
|
||||||
|
|
||||||
|
If @racket[sleep-atomic?] is true, then the proxy enters atomic mode
|
||||||
|
before sleeping, to better simulate the effect of a long-running FFI
|
||||||
|
call (see @secref["ffi-concurrency"]). Even so, it may not accurately
|
||||||
|
simulate an ODBC connection that internally uses cursors to fetch data
|
||||||
|
on demand, as each fetch would introduce additional latency.
|
||||||
|
}
|
||||||
|
|
58
collects/db/util/testing.rkt
Normal file
58
collects/db/util/testing.rkt
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#lang racket/base
|
||||||
|
(require racket/contract
|
||||||
|
racket/class
|
||||||
|
ffi/unsafe/atomic
|
||||||
|
"../private/generic/interfaces.rkt"
|
||||||
|
"../private/generic/common.rkt")
|
||||||
|
|
||||||
|
(define high-latency-connection%
|
||||||
|
(class* locking% (connection<%>)
|
||||||
|
(init-private connection
|
||||||
|
latency
|
||||||
|
sleep-atomic?)
|
||||||
|
(inherit call-with-lock)
|
||||||
|
(super-new)
|
||||||
|
|
||||||
|
(define-syntax-rule (define-forward defmethod (sleep-factor method arg ...) ...)
|
||||||
|
(begin
|
||||||
|
(defmethod (method arg ...)
|
||||||
|
(call-with-lock 'method
|
||||||
|
(lambda ()
|
||||||
|
(let ([c connection])
|
||||||
|
(unless c (error/not-connected 'method))
|
||||||
|
(when (positive? sleep-factor)
|
||||||
|
(if sleep-atomic?
|
||||||
|
(call-as-atomic (lambda () (sleep (* sleep-factor latency))))
|
||||||
|
(sleep (* sleep-factor latency))))
|
||||||
|
(send c method arg ...)))))
|
||||||
|
...))
|
||||||
|
|
||||||
|
(define-forward define/public
|
||||||
|
(0 get-dbsystem)
|
||||||
|
(2 query fsym stmt cursor?) ;; 2 because may require implicit prepare
|
||||||
|
(1 prepare fsym stmt close-on-exec?)
|
||||||
|
(1 fetch/cursor fsym stmt fetch-size)
|
||||||
|
(0 get-base)
|
||||||
|
(0 free-statement stmt need-lock?)
|
||||||
|
(0 transaction-status fsym)
|
||||||
|
(1 start-transaction fsym isolation cwt?)
|
||||||
|
(1 end-transaction fsym mode cwt?)
|
||||||
|
(1 list-tables fsym schema))
|
||||||
|
|
||||||
|
(define/override (connected?) (and connection (send connection connected?)))
|
||||||
|
|
||||||
|
(define/public (disconnect)
|
||||||
|
(set! connection #f))))
|
||||||
|
|
||||||
|
(define (high-latency-connection connection latency
|
||||||
|
#:sleep-atomic? [sleep-atomic? #f])
|
||||||
|
(new high-latency-connection%
|
||||||
|
[connection connection]
|
||||||
|
[latency latency]
|
||||||
|
[sleep-atomic? sleep-atomic?]))
|
||||||
|
|
||||||
|
(provide/contract
|
||||||
|
[high-latency-connection
|
||||||
|
(->* (connection? (>=/c 0))
|
||||||
|
(#:sleep-atomic? any/c)
|
||||||
|
connection?)])
|
|
@ -171,7 +171,7 @@
|
||||||
(when (ANYFLAGS 'postgresql 'mysql 'sqlite3)
|
(when (ANYFLAGS 'postgresql 'mysql 'sqlite3)
|
||||||
(check-roundtrip c (make-bytes #e1e6 (char->integer #\a)))
|
(check-roundtrip c (make-bytes #e1e6 (char->integer #\a)))
|
||||||
(check-roundtrip c (make-bytes #e1e7 (char->integer #\b)))
|
(check-roundtrip c (make-bytes #e1e7 (char->integer #\b)))
|
||||||
(check-roundtrip c (make-bytes #e1e8 (char->integer #\c))))
|
#| (check-roundtrip c (make-bytes #e1e8 (char->integer #\c))) |#)
|
||||||
(when (ANYFLAGS 'postgresql)
|
(when (ANYFLAGS 'postgresql)
|
||||||
(let ([r (query-value c "select cast(repeat('a', 10000000) as bytea)")])
|
(let ([r (query-value c "select cast(repeat('a', 10000000) as bytea)")])
|
||||||
(check-pred bytes? r)
|
(check-pred bytes? r)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user