#lang racket/base
(require racket/class)
(provide connection<%>
dbsystem<%>
prepared-statement<%>
(struct-out simple-result)
(struct-out recordset)
prop:statement
prop:statement?
prop:statement-ref
(struct-out statement-binding)
init-private
define-type-table
no-cache-prepare<%>
connector<%>
locking%
transactions%
isolation-symbol->string
hex-string->bytes
make-handler
guess-socket-path/paths
dblogger
dbdebug
(struct-out exn:fail:sql)
raise-sql-error)
(define connection<%>
(interface ()
connected? disconnect get-dbsystem query prepare
start-transaction end-transaction transaction-status
free-statement))
(define no-cache-prepare<%>
(interface ()))
(define dbsystem<%>
(interface ()
get-short-name
get-parameter-handlers field-dvecs->typeids
get-known-types describe-typeids))
(define prepared-statement<%>
(interface ()
get-handle set-handle
after-exec
get-param-count get-param-typeids
get-result-dvecs get-result-count get-result-typeids
check-owner bind
finalize register-finalizer
get-param-types get-result-types ))
(define-values (prop:statement prop:statement? prop:statement-ref)
(make-struct-type-property 'prop:statement))
(struct statement-binding (pst meta params))
(struct simple-result (info) #:transparent)
(struct recordset (headers rows) #:transparent)
(define-syntax-rule (init-private iid ...)
(begin (init-private1 iid) ...))
(define-syntax-rule (init-private1 iid)
(begin (init ([private-iid iid]))
(define iid private-iid)))
(define-syntax-rule (define-type-table (supported-types
type-alias->type
typeid->type
type->typeid
describe-typeid)
(typeid type (alias ...) supported?) ...)
(begin
(define all-types '((type supported?) ...))
(define supported-types
(sort (map car (filter cadr all-types))
string<?
#:key symbol->string
#:cache-keys? #t))
(define (type-alias->type x)
(case x
((alias ...) 'type) ...
(else x)))
(define (typeid->type x)
(case x
((typeid) 'type) ...
(else #f)))
(define (type->typeid x)
(case x
((type) 'typeid) ...
(else #f)))
(define (describe-typeid x)
(let ([t (typeid->type x)]
[ok? (case x ((typeid) supported?) ... (else #f))])
(list ok? t x)))))
(define connector<%>
(interface ()
attach-to-ports start-connection-protocol ))
(define (make-handler out header)
(if (procedure? out)
out
(lambda (code message)
(fprintf (case out
((output) (current-output-port))
((error) (current-error-port))
(else out))
"~a: ~a (SQLSTATE ~a)\n" header message code))))
(define (guess-socket-path/paths function paths)
(or (for/or ([path (in-list paths)])
(and (file-exists? path) path))
(error function
"could not find socket path")))
(define USE-LOCK-HOLDER? #f)
(define locking%
(class object%
(define lock (make-semaphore 1))
(define lock-holder never-evt)
(define delayed-async-calls null)
(define/public (call-with-lock who proc)
(call-with-lock* who proc #f #t))
(define/public-final (call-with-lock* who proc hopeless require-connected?)
(let* ([me (thread-dead-evt (current-thread))]
[result (sync lock lock-holder)])
(cond [(eq? result lock)
(when USE-LOCK-HOLDER? (set! lock-holder me))
(when (and require-connected? (not (connected?)))
(semaphore-post lock)
(error/not-connected who))
(with-handlers ([values (lambda (e) (unlock) (raise e))])
(begin0 (proc) (unlock)))]
[(eq? result lock-holder)
(if hopeless
(hopeless)
(error/hopeless who))]
[else
(call-with-lock* who proc hopeless require-connected?)])))
(define/private (unlock)
(let ([async-calls (reverse delayed-async-calls)])
(set! delayed-async-calls null)
(when USE-LOCK-HOLDER? (set! lock-holder never-evt))
(semaphore-post lock)
(for-each call-with-continuation-barrier async-calls)))
(define/public (connected?) #f)
(define/public-final (add-delayed-call! proc)
(set! delayed-async-calls (cons proc delayed-async-calls)))
(super-new)))
(define transactions%
(class locking%
(field [tx-status #f])
(define/public (check-valid-tx-status fsym)
(when (eq? tx-status 'invalid)
(uerror fsym "current transaction is invalid and must be explicitly rolled back")))
(super-new)))
(define (isolation-symbol->string isolation)
(case isolation
((serializable) "SERIALIZABLE")
((repeatable-read) "REPEATABLE READ")
((read-committed) "READ COMMITTED")
((read-uncommitted) "READ UNCOMMITTED")
(else #f)))
(define (hex-string->bytes s)
(define (hex-digit->int c)
(let ([c (char->integer c)])
(cond [(<= (char->integer #\0) c (char->integer #\9))
(- c (char->integer #\0))]
[(<= (char->integer #\a) c (char->integer #\f))
(- c (char->integer #\a))]
[(<= (char->integer #\A) c (char->integer #\F))
(- c (char->integer #\A))])))
(unless (and (string? s) (even? (string-length s))
(regexp-match? #rx"[0-9a-zA-Z]*" s))
(raise-type-error 'hex-string->bytes
"string containing an even number of hexadecimal digits" s))
(let* ([c (quotient (string-length s) 2)]
[b (make-bytes c)])
(for ([i (in-range c)])
(let ([high (hex-digit->int (string-ref s (+ i i)))]
[low (hex-digit->int (string-ref s (+ i i 1)))])
(bytes-set! b i (+ (arithmetic-shift high 4) low))))
b))
(define dblogger (make-logger 'db (current-logger)))
(define (dbdebug fmt . args)
(log-message dblogger 'debug (apply format fmt args) #f))
(define-struct (exn:fail:sql exn:fail) (sqlstate info))
(define (raise-sql-error who sqlstate message info)
(raise
(make-exn:fail:sql (format "~a: ~a (SQLSTATE ~a)" who message sqlstate)
(current-continuation-marks)
sqlstate
info)))
(provide uerror
error/internal
error/not-connected
error/need-password
error/comm
error/hopeless
error/unsupported-type
error/already-in-tx
error/no-convert)
(define uerror error)
(define (error/internal fsym fmt . args)
(apply error fsym (string-append "internal error: " fmt) args))
(define (error/not-connected fsym)
(uerror fsym "not connected"))
(define (error/need-password fsym)
(uerror fsym "password needed but not supplied"))
(define (error/comm fsym [when-occurred #f])
(if when-occurred
(error/internal fsym "communication problem ~a" when-occurred)
(error/internal fsym "communication problem")))
(define (error/hopeless fsym)
(uerror fsym "connection is permanently locked due to a terminated thread"))
(define (error/unsupported-type fsym typeid [type #f])
(if type
(uerror fsym "unsupported type: ~a (typeid ~a)" type typeid)
(uerror fsym "unsupported type: (typeid ~a)" typeid)))
(define (error/already-in-tx fsym)
(uerror fsym "already in transaction"))
(define (error/no-convert fsym sys type param [note #f])
(uerror fsym "cannot convert to ~a ~a type~a~a: ~e"
sys type (if note " " "") (or note "") param))