db: added nested transactions

This commit is contained in:
Ryan Culpepper 2011-12-31 01:47:03 -07:00
parent 62e117bfe6
commit dba35c3116
20 changed files with 974 additions and 355 deletions

View File

@ -78,3 +78,30 @@ Misc
- sqlite3: sqlite3_last_insert_rowid(), use sqlite3_changes() to see if insert succeeded, - sqlite3: sqlite3_last_insert_rowid(), use sqlite3_changes() to see if insert succeeded,
but still need to tell if stmt was even insert (parse sql?) but still need to tell if stmt was even insert (parse sql?)
- odbc: ??? - odbc: ???
- add recursive locking?
- cons: - considered by experts to be bad design, sloppy
- pros: - would simplify cleanup for one-shot pstmts
- would enable simple impl of user-level 'call-with-lock' for grouping
multiple operations together
(but this could also be done by two locks: outer "ownership" lock
and inner "invariant-protecting" lock)
- audit code for break-safety, disable breaks as needed
- dialect info for ODBC
- can get some tx data from ODBC...
- on the other hand, not supposed to do tx-SQL in ODBC anyway, so low-priority
- postgresql query path optimizations
- once all types have binary readers...
- can eliminate prepare step when args given (use unnamed statement)
- then, can remember what SQL in unnamed statement, avoid re-parse
- mysql, sqlite3, odbc query path optimization
- can do something similar, but messier because no unnamed statement
- might make close-on-exec? obsolete
- add sql field to pstmt%, add sql=>pstmt hash in connection (update on pstmt finalize)
- use as stmt cache, avoid re-prepare
- sql field would be good for eventually implementing cursors, too
- PROBLEM: if schema changes, may invalidate pstmt, change types, etc

View File

@ -67,8 +67,8 @@
(get-base) (get-base)
(free-statement stmt) (free-statement stmt)
(transaction-status fsym) (transaction-status fsym)
(start-transaction fsym isolation) (start-transaction fsym isolation cwt?)
(end-transaction fsym mode) (end-transaction fsym mode cwt?)
(list-tables fsym schema)) (list-tables fsym schema))
(super-new))) (super-new)))
@ -177,8 +177,8 @@
(#f #f (connected?)) (#f #f (connected?))
(#t '_ (get-dbsystem)) (#t '_ (get-dbsystem))
(#t '_ (query fsym stmt)) (#t '_ (query fsym stmt))
(#t '_ (start-transaction fsym isolation)) (#t '_ (start-transaction fsym isolation cwt?))
(#f (void) (end-transaction fsym mode)) (#f (void) (end-transaction fsym mode cwt?))
(#f #f (transaction-status fsym)) (#f #f (transaction-status fsym))
(#t '_ (list-tables fsym schema))) (#t '_ (list-tables fsym schema)))
@ -340,8 +340,8 @@
(get-base) (get-base)
(free-statement stmt) (free-statement stmt)
(transaction-status fsym) (transaction-status fsym)
(start-transaction fsym isolation) (start-transaction fsym isolation cwt?)
(end-transaction fsym mode) (end-transaction fsym mode cwt?)
(list-tables fsym schema)) (list-tables fsym schema))
;; (define-forward define/override (connected?)) ;; (define-forward define/override (connected?))

View File

@ -253,13 +253,22 @@
;; ======================================== ;; ========================================
(define (start-transaction c #:isolation [isolation #f]) (define (start-transaction c #:isolation [isolation #f])
(send c start-transaction 'start-transaction isolation)) (send c start-transaction 'start-transaction isolation #f))
(define (commit-transaction c) (define (commit-transaction c)
(send c end-transaction 'commit-transaction 'commit)) (send c end-transaction 'commit-transaction 'commit #f))
(define (rollback-transaction c) (define (rollback-transaction c)
(send c end-transaction 'rollback-transaction 'rollback)) (send c end-transaction 'rollback-transaction 'rollback #f))
(define (call-with-transaction c proc #:isolation [isolation #f])
(send c start-transaction '|call-with-transaction (start)| isolation #t)
(with-handlers ([(lambda (e) #t)
(lambda (e)
(send c end-transaction '|call-with-transaction (rollback)| 'rollback #t)
(raise e))])
(begin0 (call-with-continuation-barrier proc)
(send c end-transaction '|call-with-transaction (commit)| 'commit #t))))
(define (in-transaction? c) (define (in-transaction? c)
(and (send c transaction-status 'in-transaction?) #t)) (and (send c transaction-status 'in-transaction?) #t))
@ -267,15 +276,6 @@
(define (needs-rollback? c) (define (needs-rollback? c)
(eq? (send c transaction-status 'needs-rollback?) 'invalid)) (eq? (send c transaction-status 'needs-rollback?) 'invalid))
(define (call-with-transaction c proc #:isolation [isolation #f])
(send c start-transaction 'call-with-transaction isolation)
(begin0 (with-handlers ([(lambda (e) #t)
(lambda (e)
(send c end-transaction 'call-with-transaction 'rollback)
(raise e))])
(proc))
(send c end-transaction 'call-with-transaction 'commit)))
;; ======================================== ;; ========================================
;; list-tables : ... -> (listof string) ;; list-tables : ... -> (listof string)

View File

@ -1,5 +1,6 @@
#lang racket/base #lang racket/base
(require racket/class (require racket/class
racket/string
ffi/unsafe/atomic) ffi/unsafe/atomic)
(provide connection<%> (provide connection<%>
dbsystem<%> dbsystem<%>
@ -18,6 +19,8 @@
transactions% transactions%
isolation-symbol->string isolation-symbol->string
make-sql-classifier
sql-skip-comments
hex-string->bytes hex-string->bytes
@ -42,8 +45,11 @@
prepare ;; symbol preparable boolean -> prepared-statement<%> prepare ;; symbol preparable boolean -> prepared-statement<%>
get-base ;; -> connection<%> or #f (#f means base isn't fixed) get-base ;; -> connection<%> or #f (#f means base isn't fixed)
list-tables ;; symbol symbol -> (listof string) list-tables ;; symbol symbol -> (listof string)
start-transaction ;; symbol (U 'serializable ...) -> void
end-transaction ;; symbol (U 'commit 'rollback) -> void ;; in start-tx and end-tx, the final boolean arg indicates whether the
;; transaction is managed manually (#f) or by call-with-tx (#t)
start-transaction ;; symbol (U 'serializable ...) boolean -> void
end-transaction ;; symbol (U 'commit 'rollback) boolean -> void
transaction-status ;; symbol -> (U boolean 'invalid) transaction-status ;; symbol -> (U boolean 'invalid)
free-statement)) ;; prepared-statement<%> -> void free-statement)) ;; prepared-statement<%> -> void
@ -279,13 +285,178 @@
(define transactions% (define transactions%
(class locking% (class locking%
(inherit call-with-lock)
#|
A transaction created via SQL is "unmanaged".
A transaction created via start-tx, call-with-tx is "managed".
FIXME: eliminate distinction, if possible.
- currently: tx-stack != null means tx-status != #f
- would also like: tx-stack = null iff tx-status = #f
|#
;; tx-status : #f, #t, 'invalid ;; tx-status : #f, #t, 'invalid
(field [tx-status #f]) (field [tx-status #f])
;; tx-stack : (list (cons string boolean) ... (cons #f boolean)
;; Represents the "managed" transaction stack.
(field [tx-stack null])
;; check-valid-tx-status : symbol -> void ;; check-valid-tx-status : symbol -> void
(define/public (check-valid-tx-status fsym) (define/public (check-valid-tx-status fsym)
(when (eq? tx-status 'invalid) (when (eq? tx-status 'invalid)
(uerror fsym "current transaction is invalid and must be explicitly rolled back"))) (uerror fsym "current transaction is invalid")))
;; ----
(define/public (transaction-status fsym)
(call-with-lock fsym (lambda () tx-status)))
;; transaction-nesting : -> (U #f 'unmanaged 'top-level 'nested)
(define/public (transaction-nesting)
(cond [(eq? tx-status #f) #f]
[(null? tx-stack) 'unmanaged]
[(null? (cdr tx-stack)) 'top-level]
[else 'nested]))
(define/public (tx-state->string)
(string-append (case (transaction-nesting)
((#f) "not in transaction")
((unmanaged) "in unmanaged transaction")
((top-level nested) "in managed transaction"))
(let ([savepoints (filter string? (map car tx-stack))])
(if (pair? savepoints)
(string-append "; savepoints: "
(string-join savepoints ", "))
""))))
;; ----
(define/public (start-transaction fsym isolation cwt?)
(call-with-lock fsym
(lambda ()
(check-valid-tx-status fsym)
(cond [(not tx-status)
(start-transaction* fsym isolation)
(set! tx-stack (list (cons #f cwt?)))]
[else ;; in transaction
(unless (eq? isolation #f)
(error fsym "invalid isolation level for nested transaction: ~e" isolation))
(let ([savepoint (start-transaction* fsym 'nested)])
(set! tx-stack (cons (cons savepoint cwt?) tx-stack)))])))
(void))
(define/public (start-transaction* fsym isolation)
;; returns string (savepoint name) if isolation = 'nested, #f otherwise
(error/internal fsym "not implemented"))
(define/public (end-transaction fsym mode cwt?)
(call-with-lock fsym
(lambda ()
(unless (eq? mode 'rollback)
;; PostgreSQL: otherwise COMMIT statement would cause silent ROLLBACK!
(check-valid-tx-status fsym))
(define tx-stack*
(cond [(and (eq? mode 'rollback) cwt?)
;; Need to rollback any open start-tx transactions within call-with-tx.
;; No need to complain, because cwt/rollback means exn already raised,
;; either by thunk or commit attempt.
(let loop ([tx-stack* tx-stack])
(cond [(pair? tx-stack*)
(if (cdar tx-stack*)
tx-stack*
(loop (cdr tx-stack*)))]
[else
(error/internal "unmatched end of call-with-transaction")]))]
[else tx-stack]))
(cond [(pair? tx-stack*)
(let ([savepoint (caar tx-stack*)]
[saved-cwt? (cdar tx-stack*)])
(unless (eq? saved-cwt? cwt?)
(case saved-cwt?
((#f) ;; saved-cwt = #f, cwt = #t
(error/unclosed-tx fsym mode #t))
((#t) ;; saved-cwt = #t, cwt = #f: possible
(error/unbalanced-tx fsym mode #t))))
(end-transaction* fsym mode savepoint)
(set! tx-stack (cdr tx-stack*)))]
[else ;; not in managed transaction
(when #f ;; DISABLED!
#|
FIXME: Unmatched {commit,rollback}-transaction should
probably be illegal outside of transaction for consistency
with requirements within call-with-tx. But that would break
backwards compatibility, so disabled.
|#
(error/unbalanced-tx fsym mode #f))
(when tx-status
;; Allow closing unmanaged transaction
(end-transaction* fsym mode #f))])
(void))))
(define/public (end-transaction* fsym mode savepoint)
(error/internal fsym "not implemented"))
;; check-statement/tx-status : symbol symbol/#f -> void
;; Used to check whether SQL command is allowed given managed tx status.
(define/public (check-statement/tx fsym stmt-type)
#|
Nested transaction safety
For simplicity, we put rules for all statement types here, including
non-standard statements. FIXME: need to decouple eventually.
if in "unmanaged" top-level transaction
- allow all SQL commands (but restrict tx functions)
- yes, even implicit-commit
if in "managed" top-level transaction (no "managed" savepoints):
- START not allowed
- COMMIT, ROLLBACK not allowed (for now!)
- SAVEPOINT allowed
- RELEASE TO, ROLLBACK TO allowed
- implicit-commit not allowed
if in nested "managed" transaction (impl as "managed" savepoint):
- START not allowed
- COMMIT, ROLLBACK not allowed
- SAVEPOINT not allowed -- because it could not be used; see next
- RELEASE TO, ROLLBACK TO not allowed -- because it may cross nesting levels
- implicit-commit now allowed
|#
(define (no! why)
(error fsym "~a not allowed~a"
(or (statement-type->string stmt-type)
(case stmt-type
((implicit-commit) "statement with implicit commit")
(else "unknown")))
(or why "")))
(case (transaction-nesting)
((#f)
(void))
((unmanaged)
(void))
((top-level)
(case stmt-type
((start)
(no! " within transaction"))
((commit rollback
implicit-commit)
(no! " within managed transaction"))
(else (void))))
((nested)
(case stmt-type
((start)
(no! " within transaction"))
((commit rollback
savepoint prepare-transaction
release-savepoint rollback-savepoint
implicit-commit)
(no! " in managed transaction"))
(else (void))))))
(super-new))) (super-new)))
@ -303,6 +474,57 @@
;; ---------------------------------------- ;; ----------------------------------------
;; Simple SQL "parsing" (just classification)
(define (make-sql-classifier table-spec
#:hash-comments? [hash-comments? #f])
(define (make-sql-regexp stmt-str)
;; eg, turns "alter table" into #px"^[[:space:]]*(?i:alter)[[:space:]](?i:table)"
;; FIXME/TODO: comments (need real tokenizer; keep regexps as fast path?)
(pregexp
(apply string-append
"^"
(for/list ([piece (in-list (regexp-split #rx" " stmt-str))])
(format "[[:space:]]*(?i:~a)(?i:[[:space:]]|$)" piece)))))
(define classifier-table
(for/list ([rule-spec (in-list table-spec)])
(cons (make-sql-regexp (car rule-spec)) (cadr rule-spec))))
(lambda (str [start 0])
(let ([start (sql-skip-comments str start #:hash-comments? hash-comments?)])
(for/first ([rule (in-list classifier-table)]
#:when (regexp-match? (car rule) str start))
(cdr rule)))))
;; sql-skip-comments : string nat -> nat
(define (sql-skip-comments str start #:hash-comments? [hash-comments? #f])
(define dash-rx #px"^[[:space:]]*-- [^\n\r]*(?:[\n\r]|$)")
(define sh-like-rx #px"^[[:space:]]*#[^\n\r]*(?:[\n\r]|$)")
(define c-like-rx #px"^[[:space:]]*/\\*(?:[^\\*]|\\*[^/])*\\*/")
(let loop ([start start])
(cond [(or (regexp-match-positions dash-rx str start)
(regexp-match-positions c-like-rx str start)
(and hash-comments?
(regexp-match-positions sh-like-rx str start)))
=> (lambda (pl) (loop (cdar pl)))]
[else start])))
;; statement-type->string : symbol -> string/#f
(define (statement-type->string stmt-type)
(case stmt-type
;; standard
((start) "START TRANSACTION")
((commit) "COMMIT")
((rollback) "ROLLBACK")
((savepoint) "SAVEPOINT")
((release-savepoint) "RELEASE SAVEPOINT")
((rollback-savepoint) "ROLLBACK TO SAVEPOINT")
;; postgresql extensions
((prepare-transaction) "PREPARE TRANSACTION")
;; unknown
(else #f)))
;; ----------------------------------------
;; Passwords ;; Passwords
#| #|
@ -382,7 +604,6 @@ producing plain old exn:fail.
error/comm error/comm
error/hopeless error/hopeless
error/unsupported-type error/unsupported-type
error/already-in-tx
error/no-convert) error/no-convert)
;;(define uerror raise-user-error) ;;(define uerror raise-user-error)
@ -410,9 +631,14 @@ producing plain old exn:fail.
(uerror fsym "unsupported type: ~a (typeid ~a)" type typeid) (uerror fsym "unsupported type: ~a (typeid ~a)" type typeid)
(uerror fsym "unsupported type: (typeid ~a)" 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]) (define (error/no-convert fsym sys type param [note #f])
(uerror fsym "cannot convert to ~a ~a type~a~a: ~e" (uerror fsym "cannot convert to ~a ~a type~a~a: ~e"
sys type (if note " " "") (or note "") param)) sys type (if note " " "") (or note "") param))
(define (error/unbalanced-tx fsym mode saved-cwt?)
(error fsym "~a-transaction without matching start-transaction~a"
mode (if saved-cwt? " (within the extent of call-with-transaction)" "")))
(define (error/unclosed-tx fsym mode saved-cwt?)
(error fsym "unclosed nested transaction~a"
(if saved-cwt? " (within extent of call-with-transaction)" "")))

View File

@ -74,10 +74,10 @@
(call 'prepare fsym stmt close-on-exec?)) (call 'prepare fsym stmt close-on-exec?))
(define/public (transaction-status fsym) (define/public (transaction-status fsym)
(call 'transaction-status fsym)) (call 'transaction-status fsym))
(define/public (start-transaction fsym iso) (define/public (start-transaction fsym iso cwt?)
(call 'start-transaction fsym iso)) (call 'start-transaction fsym iso cwt?))
(define/public (end-transaction fsym mode) (define/public (end-transaction fsym mode cwt?)
(call 'end-transaction fsym mode)) (call 'end-transaction fsym mode cwt?))
(define/public (list-tables fsym schema) (define/public (list-tables fsym schema)
(call 'list-tables fsym schema)) (call 'list-tables fsym schema))

View File

@ -109,8 +109,8 @@ server -> client: (or (list 'values result ...)
(forward-methods (connected?) (forward-methods (connected?)
(prepare w s m) (prepare w s m)
(list-tables w s) (list-tables w s)
(start-transaction w m) (start-transaction w m c)
(end-transaction w m) (end-transaction w m c)
(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))])

View File

@ -8,11 +8,12 @@
;; prepared-statement% ;; prepared-statement%
(define prepared-statement% (define prepared-statement%
(class* object% (prepared-statement<%>) (class* object% (prepared-statement<%>)
(init ([-owner owner]))
(init-field handle ;; handle, determined by database system, #f means closed (init-field handle ;; handle, determined by database system, #f means closed
close-on-exec? ;; boolean close-on-exec? ;; boolean
param-typeids ;; (listof typeid) param-typeids ;; (listof typeid)
result-dvecs) ;; (listof vector), layout depends on dbsys result-dvecs ;; (listof vector), layout depends on dbsys
(init ([-owner owner])) [stmt-type #f]) ;; usually symbol or #f (see classify-*-sql)
(define owner (make-weak-box -owner)) (define owner (make-weak-box -owner))
(define dbsystem (send -owner get-dbsystem)) (define dbsystem (send -owner get-dbsystem))
@ -39,6 +40,8 @@
(define/public (get-result-types) (define/public (get-result-types)
(send dbsystem describe-typeids result-typeids)) (send dbsystem describe-typeids result-typeids))
(define/public (get-stmt-type) stmt-type)
;; checktype is either #f, 'rows, or exact-positive-integer ;; checktype is either #f, 'rows, or exact-positive-integer
(define/public (check-results fsym checktype obj) (define/public (check-results fsym checktype obj)
(cond [(eq? checktype 'rows) (cond [(eq? checktype 'rows)

View File

@ -24,7 +24,8 @@
(inherit call-with-lock (inherit call-with-lock
call-with-lock* call-with-lock*
add-delayed-call! add-delayed-call!
check-valid-tx-status) check-valid-tx-status
check-statement/tx)
(inherit-field tx-status) (inherit-field tx-status)
(super-new) (super-new)
@ -38,12 +39,10 @@
;; == Debugging ;; == Debugging
(define DEBUG-RESPONSES #f) (define DEBUG? #f)
(define DEBUG-SENT-MESSAGES #f)
(define/public (debug incoming? [outgoing? incoming?]) (define/public (debug debug?)
(set! DEBUG-RESPONSES incoming?) (set! DEBUG? debug?))
(set! DEBUG-SENT-MESSAGES outgoing?))
;; ======================================== ;; ========================================
@ -62,7 +61,7 @@
;; buffer-message : message -> void ;; buffer-message : message -> void
(define/private (buffer-message msg) (define/private (buffer-message msg)
(when DEBUG-SENT-MESSAGES (when DEBUG?
(fprintf (current-error-port) " >> ~s\n" msg)) (fprintf (current-error-port) " >> ~s\n" msg))
(with-disconnect-on-error (with-disconnect-on-error
(write-packet outport msg next-msg-num) (write-packet outport msg next-msg-num)
@ -93,7 +92,7 @@
(error/comm fsym)) (error/comm fsym))
(let-values ([(msg-num next) (parse-packet inport expectation field-dvecs)]) (let-values ([(msg-num next) (parse-packet inport expectation field-dvecs)])
(set! next-msg-num (add1 msg-num)) (set! next-msg-num (add1 msg-num))
(when DEBUG-RESPONSES (when DEBUG?
(eprintf " << ~s\n" next)) (eprintf " << ~s\n" next))
;; Update transaction status (see Transactions below) ;; Update transaction status (see Transactions below)
(when (ok-packet? next) (when (ok-packet? next)
@ -145,7 +144,7 @@
(define/private (disconnect* lock-not-held?) (define/private (disconnect* lock-not-held?)
(define (go politely?) (define (go politely?)
(when DEBUG-SENT-MESSAGES (when DEBUG?
(eprintf " ** Disconnecting\n")) (eprintf " ** Disconnecting\n"))
(let ([outport* outport] (let ([outport* outport]
[inport* inport]) [inport* inport])
@ -258,18 +257,22 @@
;; == Query ;; == Query
;; name-counter : number
(define name-counter 0)
;; query : symbol Statement -> QueryResult ;; query : symbol Statement -> QueryResult
(define/public (query fsym stmt) (define/public (query fsym stmt)
(check-valid-tx-status fsym) (check-valid-tx-status fsym)
(let*-values ([(stmt result) (let*-values ([(stmt result)
(call-with-lock fsym (call-with-lock fsym
(lambda () (lambda ()
(let ([stmt (check-statement fsym stmt)]) (let* ([stmt (check-statement fsym stmt)]
[stmt-type
(cond [(statement-binding? stmt)
(send (statement-binding-pst stmt) get-stmt-type)]
[(string? stmt)
(classify-my-sql stmt)])])
(check-statement/tx fsym stmt-type)
(values stmt (query1 fsym stmt #t)))))]) (values stmt (query1 fsym stmt #t)))))])
;; For some reason, *really* slow: (statement:after-exec stmt) (when #f ;; DISABLED---for some reason, *really* slow
(statement:after-exec stmt))
(query1:process-result fsym result))) (query1:process-result fsym result)))
;; query1 : symbol Statement -> QueryResult ;; query1 : symbol Statement -> QueryResult
@ -373,6 +376,7 @@
(close-on-exec? close-on-exec?) (close-on-exec? close-on-exec?)
(param-typeids (map field-dvec->typeid param-dvecs)) (param-typeids (map field-dvec->typeid param-dvecs))
(result-dvecs field-dvecs) (result-dvecs field-dvecs)
(stmt-type (classify-my-sql stmt))
(owner this)))]))) (owner this)))])))
(define/private (prepare1:get-field-descriptions fsym) (define/private (prepare1:get-field-descriptions fsym)
@ -425,33 +429,41 @@
;; - transaction deadlock = 1213 (ER_LOCK_DEADLOCK) ;; - transaction deadlock = 1213 (ER_LOCK_DEADLOCK)
;; - lock wait timeout (depends on config) = 1205 (ER_LOCK_WAIT_TIMEOUT) ;; - lock wait timeout (depends on config) = 1205 (ER_LOCK_WAIT_TIMEOUT)
(define/public (transaction-status fsym) (define/override (start-transaction* fsym isolation)
(call-with-lock fsym (lambda () tx-status))) (cond [(eq? isolation 'nested)
(let ([savepoint (generate-name)])
(define/public (start-transaction fsym isolation) (query1 fsym (format "SAVEPOINT ~a" savepoint) #t)
(call-with-lock fsym savepoint)]
(lambda () [else
(when tx-status (let ([isolation-level (isolation-symbol->string isolation)])
(error/already-in-tx fsym))
;; SET TRANSACTION ISOLATION LEVEL sets mode for *next* transaction
;; so need lock around both statements
(let* ([isolation-level (isolation-symbol->string isolation)]
[set-stmt "SET TRANSACTION ISOLATION LEVEL "])
(when isolation-level (when isolation-level
(query1 fsym (string-append set-stmt isolation-level) #t))) (query1 fsym (format "SET TRANSACTION ISOLATION LEVEL ~a" isolation-level) #t))
(query1 fsym "START TRANSACTION" #t) (query1 fsym "START TRANSACTION" #t)
(void)))) #f)]))
(define/public (end-transaction fsym mode) (define/override (end-transaction* fsym mode savepoint)
(call-with-lock fsym (case mode
(lambda () ((commit)
(unless (eq? mode 'rollback) (cond [savepoint
(check-valid-tx-status fsym)) (query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #t)]
(let ([stmt (case mode [else
((commit) "COMMIT") (query1 fsym "COMMIT" #t)]))
((rollback) "ROLLBACK"))]) ((rollback)
(query1 fsym stmt #t) (cond [savepoint
(void))))) (query1 fsym (format "ROLLBACK TO SAVEPOINT ~a" savepoint) #t)
(query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #t)]
[else
(query1 fsym "ROLLBACK" #t)])))
(void))
;; name-counter : number
(define name-counter 0)
;; generate-name : -> string
(define/private (generate-name)
(let ([n name-counter])
(set! name-counter (add1 name-counter))
(format "λmz_~a" n)))
;; Reflection ;; Reflection
@ -461,11 +473,7 @@
(string-append "SELECT table_name FROM information_schema.tables " (string-append "SELECT table_name FROM information_schema.tables "
"WHERE table_schema = schema()")] "WHERE table_schema = schema()")]
[rows [rows
(vector-ref (vector-ref (call-with-lock fsym (lambda () (query1 fsym stmt #t))) 2)])
(call-with-lock fsym
(lambda ()
(query1 fsym stmt #t)))
2)])
(for/list ([row (in-list rows)]) (for/list ([row (in-list rows)])
(vector-ref row 0)))) (vector-ref row 0))))
@ -598,34 +606,8 @@ On the other hand, we want to force all rows-returning statements
through the prepared-statement path to use the binary data through the prepared-statement path to use the binary data
protocol. That would seem to be the following: protocol. That would seem to be the following:
CALL (?) and SELECT SELECT and SHOW
The following bit of heinously offensive code determines the kind of
SQL statement is contained in a string.
----
3 kinds of comments in mysql SQL:
- "#" to end of line
- "-- " to end of line
- "/*" to next "*/" (not nested), except some weird conditional-inclusion stuff
I'll ignore the third kind.
|# |#
(define (force-prepare-sql? fsym stmt) (define (force-prepare-sql? fsym stmt)
(let ([kw (get-sql-keyword stmt)]) (memq (classify-my-sql stmt) '(select show)))
(cond [(not kw)
;; better to have unpreparable stmt rejected than
;; to have SELECT return unconvered types
#t]
[(string-ci=? kw "select") #t]
[(string-ci=? kw "call") #t]
[else #f])))
(define sql-statement-rx
#rx"^(?:(?:#[^\n\r]*[\n\r])|(?:-- [^\n\r]*[\n\r])|[ \t\n\r])*([A-Za-z]+)")
(define (get-sql-keyword stmt)
(let ([m (regexp-match sql-statement-rx stmt)])
(and m (cadr m))))

View File

@ -4,7 +4,8 @@
"../generic/sql-data.rkt" "../generic/sql-data.rkt"
"../../util/private/geometry.rkt" "../../util/private/geometry.rkt"
(only-in "message.rkt" field-dvec->typeid)) (only-in "message.rkt" field-dvec->typeid))
(provide dbsystem) (provide dbsystem
classify-my-sql)
(define mysql-dbsystem% (define mysql-dbsystem%
(class* object% (dbsystem<%>) (class* object% (dbsystem<%>)
@ -55,6 +56,44 @@
;; ======================================== ;; ========================================
;; SQL "parsing"
;; We care about:
;; - determining whether commands must be prepared (to use binary data)
;; see http://dev.mysql.com/doc/refman/5.0/en/c-api-prepared-statements.html
;; - detecting commands that affect transaction status (maybe implicitly)
;; see http://dev.mysql.com/doc/refman/5.0/en/implicit-commit.html
;; classify-my-sql : string [nat] -> symbol/#f
(define classify-my-sql
(make-sql-classifier #:hash-comments? #t
'(;; Must be prepared
("SELECT" select)
("SHOW" show)
;; Explicit transaction commands
("ROLLBACK WORK TO" rollback-savepoint)
("ROLLBACK TO" rollback-savepoint)
("RELEASE SAVEPOINT" release-savepoint)
("SAVEPOINT" savepoint)
("START TRANSACTION" start)
("BEGIN" start)
("COMMIT" commit)
("ROLLBACK" rollback) ;; Note: after ROLLBACK TO, etc
("SET autocommit" set-autocommit) ;; trouble
;; Note: commit/rollback may immediately start new transaction
;; Implicit commit
("ALTER" implicit-commit)
("CREATE" implicit-commit)
("DROP" implicit-commit)
("RENAME" implicit-commit)
("TRUNCATE" implicit-commit)
("LOAD" implicit-commit)
("LOCK TABLES" implicit-commit)
("UNLOCK TABLES" implicit-commit))))
;; ========================================
(define-type-table (supported-types (define-type-table (supported-types
type-alias->type type-alias->type
typeid->type typeid->type

View File

@ -42,7 +42,8 @@
(inherit call-with-lock (inherit call-with-lock
call-with-lock* call-with-lock*
add-delayed-call! add-delayed-call!
check-valid-tx-status) check-valid-tx-status
check-statement/tx)
(inherit-field tx-status) (inherit-field tx-status)
(define/public (get-db fsym) (define/public (get-db fsym)
@ -58,12 +59,12 @@
(call-with-lock fsym (call-with-lock fsym
(lambda () (lambda ()
(check-valid-tx-status fsym) (check-valid-tx-status fsym)
(query1 fsym stmt)))]) (query1 fsym stmt #t)))])
(statement:after-exec stmt*) (statement:after-exec stmt*)
(cond [(pair? dvecs) (rows-result (map field-dvec->field-info dvecs) rows)] (cond [(pair? dvecs) (rows-result (map field-dvec->field-info dvecs) rows)]
[else (simple-result '())]))) [else (simple-result '())])))
(define/private (query1 fsym stmt) (define/private (query1 fsym stmt check-tx?)
(let* ([stmt (cond [(string? stmt) (let* ([stmt (cond [(string? stmt)
(let* ([pst (prepare1 fsym stmt #t)]) (let* ([pst (prepare1 fsym stmt #t)])
(send pst bind fsym null))] (send pst bind fsym null))]
@ -72,6 +73,7 @@
[pst (statement-binding-pst stmt)] [pst (statement-binding-pst stmt)]
[params (statement-binding-params stmt)]) [params (statement-binding-params stmt)])
(send pst check-owner fsym this stmt) (send pst check-owner fsym this stmt)
(when check-tx? (check-statement/tx fsym (send pst get-stmt-type)))
(let ([result-dvecs (send pst get-result-dvecs)]) (let ([result-dvecs (send pst get-result-dvecs)])
(for ([dvec (in-list result-dvecs)]) (for ([dvec (in-list result-dvecs)])
(let ([typeid (field-dvec->typeid dvec)]) (let ([typeid (field-dvec->typeid dvec)])
@ -409,9 +411,10 @@
(let ([pst (new prepared-statement% (let ([pst (new prepared-statement%
(handle stmt) (handle stmt)
(close-on-exec? close-on-exec?) (close-on-exec? close-on-exec?)
(owner this)
(param-typeids param-typeids) (param-typeids param-typeids)
(result-dvecs result-dvecs))]) (result-dvecs result-dvecs)
(stmt-type (classify-odbc-sql sql))
(owner this))])
(hash-set! statement-table pst #t) (hash-set! statement-table pst #t)
pst))) pst)))
@ -473,17 +476,11 @@
;; Transactions ;; Transactions
(define/public (transaction-status fsym) (define/override (start-transaction* fsym isolation)
(call-with-lock fsym (when (eq? isolation 'nested)
(lambda () (let ([db (get-db fsym)]) tx-status)))) (uerror fsym "already in transaction (nested transactions not supported for ODBC)"))
(let* ([db (get-db fsym)]
(define/public (start-transaction fsym isolation) [ok-levels
(call-with-lock fsym
(lambda ()
(let* ([db (get-db fsym)])
(when tx-status
(error/already-in-tx fsym))
(let* ([ok-levels
(let-values ([(status value) (let-values ([(status value)
(SQLGetInfo db SQL_TXN_ISOLATION_OPTION)]) (SQLGetInfo db SQL_TXN_ISOLATION_OPTION)])
(begin0 value (handle-status fsym status db)))] (begin0 value (handle-status fsym status db)))]
@ -508,13 +505,10 @@
(let ([status (SQLSetConnectAttr db SQL_ATTR_AUTOCOMMIT SQL_AUTOCOMMIT_OFF)]) (let ([status (SQLSetConnectAttr db SQL_ATTR_AUTOCOMMIT SQL_AUTOCOMMIT_OFF)])
(handle-status fsym status db) (handle-status fsym status db)
(set! tx-status #t) (set! tx-status #t)
(void)))))) (void)))
(define/public (end-transaction fsym mode) (define/override (end-transaction* fsym mode _savepoint)
(call-with-lock fsym ;; _savepoint = #f, because nested transactions not supported on ODBC
(lambda ()
(unless (eq? mode 'rollback)
(check-valid-tx-status fsym))
(let ([db (get-db fsym)] (let ([db (get-db fsym)]
[completion-type [completion-type
(case mode (case mode
@ -525,7 +519,7 @@
(handle-status fsym status db) (handle-status fsym status db)
;; commit/rollback can fail; don't change status until possible error handled ;; commit/rollback can fail; don't change status until possible error handled
(set! tx-status #f) (set! tx-status #f)
(void)))))) (void))))
;; GetTables ;; GetTables
@ -669,7 +663,7 @@ all Racket threads for a long time.
1) The postgresql, mysql, and oracle drivers don't even support async 1) The postgresql, mysql, and oracle drivers don't even support async
execution. Only DB2 (and probably SQL Server, but I didn't try it). execution. Only DB2 (and probably SQL Server, but I didn't try it).
2) Tests using the DB2 driver gave bafflind HY010 (function sequence 2) Tests using the DB2 driver gave baffling HY010 (function sequence
error). My best theory so far is that DB2 (or maybe unixodbc) requires error). My best theory so far is that DB2 (or maybe unixodbc) requires
poll call arguments to be identical to original call arguments, which poll call arguments to be identical to original call arguments, which
means that I would have to replace all uses of (_ptr o X) with means that I would have to replace all uses of (_ptr o X) with

View File

@ -4,7 +4,8 @@
"../generic/sql-data.rkt" "../generic/sql-data.rkt"
"../generic/sql-convert.rkt") "../generic/sql-convert.rkt")
(provide dbsystem (provide dbsystem
supported-typeid?) supported-typeid?
classify-odbc-sql)
(define odbc-dbsystem% (define odbc-dbsystem%
(class* object% (dbsystem<%>) (class* object% (dbsystem<%>)
@ -28,6 +29,37 @@
;; ---- ;; ----
;; SQL "parsing"
;; We just care about detecting commands that affect transaction status.
;; Since we have no idea what the actual database system is, just cover
;; standard commands and assume DDL is not transactional.
;; classify-odbc-sql : string [nat] -> symbol/#f
(define classify-odbc-sql
(make-sql-classifier #:hash-comments? #t
'(;; Explicit transaction commands
("ROLLBACK TRANSACTION TO" rollback-savepoint)
("ROLLBACK WORK TO" rollback-savepoint)
("ROLLBACK TO" rollback-savepoint)
("RELEASE" release-savepoint)
("SAVEPOINT" savepoint)
("START" start)
("BEGIN" start)
("COMMIT" commit)
("END" commit)
("ROLLBACK" rollback) ;; Note: after ROLLBACK TO, etc
;; Implicit commit
("ALTER" implicit-commit)
("CREATE" implicit-commit)
("DROP" implicit-commit)
("GRANT" implicit-commit)
("RENAME" implicit-commit)
("TRUNCATE" implicit-commit))))
;; ----
(define-syntax-rule (define-syntax-rule
(defchecks get-check [(typeid name pred ...) ...] [(*typeid *name *fun) ...]) (defchecks get-check [(typeid name pred ...) ...] [(*typeid *name *fun) ...])
(define get-check (define get-check

View File

@ -33,7 +33,10 @@
(inherit call-with-lock (inherit call-with-lock
call-with-lock* call-with-lock*
add-delayed-call! add-delayed-call!
check-valid-tx-status) check-valid-tx-status
check-statement/tx
transaction-nesting
tx-state->string)
(inherit-field tx-status) (inherit-field tx-status)
(super-new) (super-new)
@ -48,12 +51,10 @@
;; == Debugging ;; == Debugging
;; Debugging ;; Debugging
(define DEBUG-RESPONSES #f) (define DEBUG? #f)
(define DEBUG-SENT-MESSAGES #f)
(define/public (debug incoming? [outgoing? incoming?]) (define/public (debug debug?)
(set! DEBUG-RESPONSES incoming?) (set! DEBUG? debug?))
(set! DEBUG-SENT-MESSAGES outgoing?))
;; ======================================== ;; ========================================
@ -64,7 +65,7 @@
(define/private (raw-recv) (define/private (raw-recv)
(with-disconnect-on-error (with-disconnect-on-error
(let ([r (parse-server-message inport)]) (let ([r (parse-server-message inport)])
(when DEBUG-RESPONSES (when DEBUG?
(fprintf (current-error-port) " << ~s\n" r)) (fprintf (current-error-port) " << ~s\n" r))
r))) r)))
@ -88,7 +89,7 @@
;; buffer-message : message -> void ;; buffer-message : message -> void
(define/private (buffer-message msg) (define/private (buffer-message msg)
(when DEBUG-SENT-MESSAGES (when DEBUG?
(fprintf (current-error-port) " >> ~s\n" msg)) (fprintf (current-error-port) " >> ~s\n" msg))
(with-disconnect-on-error (with-disconnect-on-error
(write-message msg outport))) (write-message msg outport)))
@ -141,7 +142,7 @@
;; disconnect* : boolean -> void ;; disconnect* : boolean -> void
(define/private (disconnect* no-lock-held?) (define/private (disconnect* no-lock-held?)
(define (go politely?) (define (go politely?)
(when DEBUG-SENT-MESSAGES (when DEBUG?
(fprintf (current-error-port) " ** Disconnecting\n")) (fprintf (current-error-port) " ** Disconnecting\n"))
(let ([outport* outport] (let ([outport* outport]
[inport* inport]) [inport* inport])
@ -243,43 +244,55 @@
(call-with-lock fsym (call-with-lock fsym
(lambda () (lambda ()
(check-valid-tx-status fsym) (check-valid-tx-status fsym)
(query1 fsym stmt0)))]) (let* ([stmt (check-statement fsym stmt0)]
[stmt-type (send (statement-binding-pst stmt) get-stmt-type)])
(check-statement/tx fsym stmt-type)
(values stmt (query1 fsym stmt #f)))))])
(statement:after-exec stmt) (statement:after-exec stmt)
(query1:process-result fsym result))) (query1:process-result fsym result)))
(define/private (query1 fsym stmt) (define/private (query1 fsym stmt simple?)
(let ([stmt (check-statement fsym stmt)]) ;; if simple?: stmt must be string, no params, & results must be binary-readable
(query1:enqueue stmt) (query1:enqueue stmt)
(send-message (make-Sync)) (send-message (make-Sync))
(begin0 (values stmt (query1:collect fsym stmt)) (begin0 (query1:collect fsym simple?)
(check-ready-for-query fsym #f)))) (check-ready-for-query fsym #f)
(when DEBUG?
(fprintf (current-error-port) " ** ~a\n" (tx-state->string)))))
;; check-statement : symbol statement -> statement-binding ;; check-statement : symbol statement -> statement-binding
;; Always prepare, so we can have type information to choose result formats. ;; Convert to statement-binding; need to prepare to get type information, used to
;; choose result formats.
;; FIXME: if text format eliminated, can skip prepare
;; FIXME: can use classify-pg-sql to avoid preparing stmts with no results
(define/private (check-statement fsym stmt) (define/private (check-statement fsym stmt)
(cond [(statement-binding? stmt) (cond [(statement-binding? stmt)
(let ([pst (statement-binding-pst stmt)]) (let ([pst (statement-binding-pst stmt)])
(send pst check-owner fsym this stmt)) (send pst check-owner fsym this stmt)
stmt] stmt)]
[(string? stmt) [(string? stmt)
(let ([pst (prepare1 fsym stmt #t)]) (let ([pst (prepare1 fsym stmt #t)])
(send pst bind fsym null))])) (send pst bind fsym null))]))
;; query1:enqueue : Statement -> void ;; query1:enqueue : Statement -> void
(define/private (query1:enqueue stmt) (define/private (query1:enqueue stmt)
(cond [(statement-binding? stmt)
(let* ([pst (statement-binding-pst stmt)] (let* ([pst (statement-binding-pst stmt)]
[pst-name (send pst get-handle)] [pst-name (send pst get-handle)]
[params (statement-binding-params stmt)]) [params (statement-binding-params stmt)])
(buffer-message (make-Bind "" pst-name (buffer-message (make-Bind "" pst-name
(map typeid->format (send pst get-param-typeids)) (map typeid->format (send pst get-param-typeids))
params params
(map typeid->format (send pst get-result-typeids))))) (map typeid->format (send pst get-result-typeids)))))]
[(string? stmt)
(buffer-message (make-Parse "" stmt '()))
(buffer-message (make-Bind "" "" '() '() '(1)))])
(buffer-message (make-Describe 'portal "")) (buffer-message (make-Describe 'portal ""))
(buffer-message (make-Execute "" 0)) (buffer-message (make-Execute "" 0))
(buffer-message (make-Close 'portal ""))) (buffer-message (make-Close 'portal "")))
(define/private (query1:collect fsym stmt) (define/private (query1:collect fsym simple?)
(when (string? stmt) (when simple?
(match (recv-message fsym) (match (recv-message fsym)
[(struct ParseComplete ()) (void)] [(struct ParseComplete ()) (void)]
[other-r (query1:error fsym other-r)])) [other-r (query1:error fsym other-r)]))
@ -360,14 +373,14 @@
(let ([name (generate-name)]) (let ([name (generate-name)])
(prepare1:enqueue name stmt) (prepare1:enqueue name stmt)
(send-message (make-Sync)) (send-message (make-Sync))
(begin0 (prepare1:collect fsym name close-on-exec?) (begin0 (prepare1:collect fsym name close-on-exec? (classify-pg-sql stmt))
(check-ready-for-query fsym #f)))) (check-ready-for-query fsym #f))))
(define/private (prepare1:enqueue name stmt) (define/private (prepare1:enqueue name stmt)
(buffer-message (make-Parse name stmt null)) (buffer-message (make-Parse name stmt null))
(buffer-message (make-Describe 'statement name))) (buffer-message (make-Describe 'statement name)))
(define/private (prepare1:collect fsym name close-on-exec?) (define/private (prepare1:collect fsym name close-on-exec? stmt-type)
(match (recv-message fsym) (match (recv-message fsym)
[(struct ParseComplete ()) (void)] [(struct ParseComplete ()) (void)]
[other-r (prepare1:error fsym other-r)]) [other-r (prepare1:error fsym other-r)])
@ -378,6 +391,7 @@
(close-on-exec? close-on-exec?) (close-on-exec? close-on-exec?)
(param-typeids param-typeids) (param-typeids param-typeids)
(result-dvecs field-dvecs) (result-dvecs field-dvecs)
(stmt-type stmt-type)
(owner this)))) (owner this))))
(define/private (prepare1:describe-params fsym) (define/private (prepare1:describe-params fsym)
@ -423,57 +437,52 @@
;; == Transactions ;; == Transactions
(define/public (transaction-status fsym) (define/override (start-transaction* fsym isolation)
(call-with-lock fsym (lambda () tx-status))) (cond [(eq? isolation 'nested)
(let ([savepoint (generate-name)])
(define/public (start-transaction fsym isolation) (query1 fsym (format "SAVEPOINT ~a" savepoint) #t)
(internal-query fsym savepoint)]
(lambda () [else
(when tx-status (let* ([isolation-level (isolation-symbol->string isolation)]
(error/already-in-tx fsym))) [stmt (if isolation-level
(let ([isolation-level (isolation-symbol->string isolation)]) (string-append "BEGIN WORK ISOLATION LEVEL " isolation-level)
"BEGIN WORK")])
;; FIXME: also support
;; 'read-only => "READ ONLY" ;; 'read-only => "READ ONLY"
;; 'read-write => "READ WRITE" ;; 'read-write => "READ WRITE"
(if isolation-level (query1 fsym stmt #t)
(string-append "BEGIN WORK ISOLATION LEVEL " isolation-level) #f)]))
"BEGIN WORK")))
(void))
(define/public (end-transaction fsym mode) (define/override (end-transaction* fsym mode savepoint)
(internal-query fsym
(lambda ()
(unless (eq? mode 'rollback)
;; otherwise, COMMIT statement would cause silent ROLLBACK !!!
(check-valid-tx-status fsym)))
(case mode (case mode
((commit) "COMMIT WORK") ((commit)
((rollback) "ROLLBACK WORK"))) (cond [savepoint
(query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #t)]
[else
(query1 fsym "COMMIT WORK" #t)]))
((rollback)
(cond [savepoint
(query1 fsym (format "ROLLBACK TO SAVEPOINT ~a" savepoint) #t)
(query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #t)]
[else
(query1 fsym "ROLLBACK WORK" #t)])))
(void)) (void))
;; == Reflection ;; == Reflection
(define/public (list-tables fsym schema) (define/public (list-tables fsym schema)
(let* ([where-cond (let* ([stmt
(string-append
"SELECT table_name FROM information_schema.tables WHERE "
(case schema (case schema
((search search-or-current) ((search search-or-current)
"table_schema = SOME (current_schemas(false))") "table_schema = SOME (current_schemas(false))")
((current) ((current)
"table_schema = current_schema"))] "table_schema = current_schema")))]
[stmt [result (call-with-lock fsym (lambda () (query1 fsym stmt #t)))]
(string-append "SELECT table_name FROM information_schema.tables WHERE " [rows (vector-ref result 2)])
where-cond)]
[rows (vector-ref (internal-query fsym void stmt) 2)])
(for/list ([row (in-list rows)]) (for/list ([row (in-list rows)])
(bytes->string/utf-8 (vector-ref row 0))))) (bytes->string/utf-8 (vector-ref row 0)))))
(define/private (internal-query fsym pre-thunk stmt)
(let-values ([(stmt result)
(call-with-lock fsym
(lambda ()
(pre-thunk)
(query1 fsym stmt)))])
(statement:after-exec stmt)
result))
)) ))
;; ======================================== ;; ========================================

View File

@ -13,7 +13,8 @@
(only-in "message.rkt" field-dvec->typeid)) (only-in "message.rkt" field-dvec->typeid))
(provide dbsystem (provide dbsystem
typeid->type-reader typeid->type-reader
typeid->format) typeid->format
classify-pg-sql)
(define postgresql-dbsystem% (define postgresql-dbsystem%
(class* object% (dbsystem<%>) (class* object% (dbsystem<%>)
@ -45,6 +46,38 @@
;; ======================================== ;; ========================================
;; SQL "parsing"
;; We just care about detecting commands that affect transaction status.
;; classify-pg-sql : string [nat] -> symbol/#f
(define classify-pg-sql
;; Source: http://www.postgresql.org/docs/current/static/sql-commands.html
(make-sql-classifier
`(("ABORT" rollback)
("BEGIN" start)
;; COMMIT PREPARED itself is harmless.
("COMMIT PREPARED" #f) ;; Note: before COMMIT
("COMMIT" commit)
("DO" *do) ;; can do anything
("END" commit)
("EXECUTE" *execute) ;; can do anything
;; PREPARE TRANSACTION is like shift: it saves and aborts current transaction.
;; Perhaps all we care about is that it ends transaction, treat like commit/rollback.
("PREPARE TRANSACTION" prepare-transaction) ;; Note: before PREPARE
("RELEASE SAVEPOINT" release-savepoint)
;; For ROLLBACK variants, ordered carefully and expanded optional words
;; ROLLBACK PREPARED just deletes saved transaction
("ROLLBACK PREPARED" #f)
("ROLLBACK WORK TO" rollback-savepoint)
("ROLLBACK TRANSACTION TO" rollback-savepoint)
("ROLLBACK TO" rollback-savepoint)
("ROLLBACK" rollback)
("SAVEPOINT" savepoint)
("START TRANSACTION" start)
)))
;; ========================================
;; Derived from ;; Derived from
;; http://www.us.postgresql.org/users-lounge/docs/7.2/postgres/datatype.html ;; http://www.us.postgresql.org/users-lounge/docs/7.2/postgres/datatype.html
;; and ;; and

View File

@ -24,11 +24,12 @@
(inherit call-with-lock* (inherit call-with-lock*
add-delayed-call! add-delayed-call!
check-valid-tx-status) check-valid-tx-status
(inherit-field tx-status) ;; only #f or 'invalid for compat w/ check-valid-tx-status check-statement/tx)
(inherit-field tx-status)
(define/override (call-with-lock fsym proc) (define/override (call-with-lock fsym proc)
(call-with-lock* fsym (lambda () (set! saved-tx-status (get-tx-status)) (proc)) #f #t)) (call-with-lock* fsym (lambda () (set! saved-tx-status tx-status) (proc)) #f #t))
(define/private (get-db fsym) (define/private (get-db fsym)
(or -db (error/not-connected fsym))) (or -db (error/not-connected fsym)))
@ -41,11 +42,11 @@
(call-with-lock fsym (call-with-lock fsym
(lambda () (lambda ()
(check-valid-tx-status fsym) (check-valid-tx-status fsym)
(query1 fsym stmt)))]) (query1 fsym stmt #t)))])
(statement:after-exec stmt) (statement:after-exec stmt)
result)) result))
(define/private (query1 fsym stmt) (define/private (query1 fsym stmt check-tx?)
(let* ([stmt (cond [(string? stmt) (let* ([stmt (cond [(string? stmt)
(let* ([pst (prepare1 fsym stmt #t)]) (let* ([pst (prepare1 fsym stmt #t)])
(send pst bind fsym null))] (send pst bind fsym null))]
@ -54,6 +55,7 @@
[pst (statement-binding-pst stmt)] [pst (statement-binding-pst stmt)]
[params (statement-binding-params stmt)]) [params (statement-binding-params stmt)])
(send pst check-owner fsym this stmt) (send pst check-owner fsym this stmt)
(when check-tx? (check-statement/tx fsym (send pst get-stmt-type)))
(let ([db (get-db fsym)] (let ([db (get-db fsym)]
[stmt (send pst get-handle)]) [stmt (send pst get-handle)])
(HANDLE fsym (sqlite3_reset stmt)) (HANDLE fsym (sqlite3_reset stmt))
@ -68,18 +70,13 @@
[rows (step* fsym db stmt)]) [rows (step* fsym db stmt)])
(HANDLE fsym (sqlite3_reset stmt)) (HANDLE fsym (sqlite3_reset stmt))
(HANDLE fsym (sqlite3_clear_bindings stmt)) (HANDLE fsym (sqlite3_clear_bindings stmt))
(unless (eq? tx-status 'invalid)
(set! tx-status (get-tx-status)))
(values stmt (values stmt
(cond [(pair? info) (cond [(pair? info)
(rows-result info rows)] (rows-result info rows)]
[else [else
(let ([changes (sqlite3_changes db)]) (simple-result '())]))))))
(cond [(and (positive? changes)
#f ;; Note: currently disabled
#| FIXME: statement was INSERT stmt |#)
(simple-result
(list (cons 'last-insert-rowid
(sqlite3_last_insert_rowid db))))]
[else (simple-result '())]))]))))))
(define/private (load-param fsym db stmt i param) (define/private (load-param fsym db stmt i param)
(HANDLE fsym (HANDLE fsym
@ -155,6 +152,7 @@
(close-on-exec? close-on-exec?) (close-on-exec? close-on-exec?)
(param-typeids param-typeids) (param-typeids param-typeids)
(result-dvecs result-dvecs) (result-dvecs result-dvecs)
(stmt-type (classify-sl-sql sql))
(owner this))]) (owner this))])
(hash-set! statement-table pst #t) (hash-set! statement-table pst #t)
pst))) pst)))
@ -194,49 +192,46 @@
;; http://www.sqlite.org/lang_transaction.html ;; http://www.sqlite.org/lang_transaction.html
(define/public (transaction-status fsym) (define/private (get-tx-status)
(call-with-lock fsym (not (sqlite3_get_autocommit -db)))
(lambda ()
(let ([db (get-db fsym)])
(or tx-status (get-tx-status db))))))
(define/private (get-tx-status [db -db]) (define/override (start-transaction* fsym isolation)
(and db (not (sqlite3_get_autocommit db))))
(define/public (start-transaction fsym isolation)
;; Isolation level can be set to READ UNCOMMITTED via pragma, but ;; Isolation level can be set to READ UNCOMMITTED via pragma, but
;; ignored in all but a few cases, don't bother. ;; ignored in all but a few cases, don't bother.
;; FIXME: modes are DEFERRED | IMMEDIATE | EXCLUSIVE ;; FIXME: modes are DEFERRED | IMMEDIATE | EXCLUSIVE
(let ([stmt (cond [(eq? isolation 'nested)
(call-with-lock fsym (let ([savepoint (generate-name)])
(lambda () (query1 fsym (format "SAVEPOINT ~a" savepoint) #f)
(let ([db (get-db fsym)]) savepoint)]
(when (get-tx-status db) [else
(error/already-in-tx fsym)) (query1 fsym "BEGIN TRANSACTION" #f)
(let-values ([(stmt* _result) #f]))
(query1 fsym "BEGIN TRANSACTION")])
stmt*))))])
(statement:after-exec stmt)
(void)))
(define/public (end-transaction fsym mode) (define/override (end-transaction* fsym mode savepoint)
(let ([stmt
(call-with-lock fsym
(lambda ()
(let ([db (get-db fsym)])
(unless (eq? mode 'rollback)
(check-valid-tx-status fsym))
(when (get-tx-status db)
(let-values ([(stmt* _result)
(case mode (case mode
((commit) ((commit)
(query1 fsym "COMMIT TRANSACTION")) (cond [savepoint
(query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #f)]
[else
(query1 fsym "COMMIT TRANSACTION" #f)]))
((rollback) ((rollback)
(query1 fsym "ROLLBACK TRANSACTION")))]) (cond [savepoint
(set! tx-status #f) (query1 fsym (format "ROLLBACK TO SAVEPOINT ~a" savepoint) #f)
stmt*)))))]) (query1 fsym (format "RELEASE SAVEPOINT ~a" savepoint) #f)]
(statement:after-exec stmt) [else
(void))) (query1 fsym "ROLLBACK TRANSACTION" #f)])
;; remove 'invalid status, if necessary
(set! tx-status (get-tx-status))))
(void))
;; name-counter : number
(define name-counter 0)
;; generate-name : -> string
(define/private (generate-name)
(let ([n name-counter])
(set! name-counter (add1 name-counter))
(format "λmz_~a" n)))
;; Reflection ;; Reflection
@ -247,7 +242,7 @@
"WHERE type = 'table' or type = 'view'")]) "WHERE type = 'table' or type = 'view'")])
(let-values ([(stmt result) (let-values ([(stmt result)
(call-with-lock fsym (call-with-lock fsym
(lambda () (query1 fsym stmt)))]) (lambda () (query1 fsym stmt #f)))])
(statement:after-exec stmt) (statement:after-exec stmt)
(for/list ([row (in-list (rows-result-rows result))]) (for/list ([row (in-list (rows-result-rows result))])
(vector-ref row 0))))) (vector-ref row 0)))))
@ -272,7 +267,7 @@
;; Can't figure out how to test... ;; Can't figure out how to test...
(define/private (handle-status who s) (define/private (handle-status who s)
(when (memv s maybe-rollback-status-list) (when (memv s maybe-rollback-status-list)
(when (and saved-tx-status -db (not (get-tx-status -db))) ;; was in trans, now not (when (and saved-tx-status -db (not (get-tx-status))) ;; was in trans, now not
(set! tx-status 'invalid))) (set! tx-status 'invalid)))
(handle-status* who s -db)) (handle-status* who s -db))

View File

@ -1,7 +1,8 @@
#lang racket/base #lang racket/base
(require racket/class (require racket/class
"../generic/interfaces.rkt") "../generic/interfaces.rkt")
(provide dbsystem) (provide dbsystem
classify-sl-sql)
(define sqlite3-dbsystem% (define sqlite3-dbsystem%
(class* object% (dbsystem<%>) (class* object% (dbsystem<%>)
@ -34,3 +35,25 @@
(bytes? param)) (bytes? param))
(error/no-convert fsym "SQLite" "parameter" param)) (error/no-convert fsym "SQLite" "parameter" param))
param) param)
;; ========================================
;; SQL "parsing"
;; We just care about detecting commands that affect transaction status.
;; classify-sl-sql : string [nat] -> symbol/#f
(define classify-sl-sql
(make-sql-classifier
'(;; Explicit transaction commands
("ROLLBACK TRANSACTION TO" rollback-savepoint)
("ROLLBACK TO" rollback-savepoint)
("RELEASE" release-savepoint)
("SAVEPOINT" savepoint)
;; Note: SAVEPOINT allowed outside of transaction! (but that's okay)
("BEGIN" start)
("COMMIT" commit)
("END" commit)
("ROLLBACK" rollback) ;; Note: after ROLLBACK TO, etc
)))

View File

@ -22,15 +22,15 @@ connections}. PostgreSQL and MySQL connections are wire-based, and
SQLite and ODBC connections are FFI-based. SQLite and ODBC connections are FFI-based.
Wire-based connections communicate using @tech/reference{ports}, which Wire-based connections communicate using @tech/reference{ports}, which
do not cause other Racket threads to block. In contrast, all Racket do not cause other Racket threads to block. In contrast, an FFI call
threads are blocked during an FFI call, so FFI-based connections can causes all Racket threads to block until it completes, so FFI-based
seriously degrade the interactivity of a Racket program, particularly connections can degrade the interactivity of a Racket program,
if long-running queries are performed using the connection. This particularly if long-running queries are performed using the
problem can be avoided by creating the FFI-based connection in a connection. This problem can be avoided by creating the FFI-based
separate @tech/reference{place} using the @racket[#:use-place] connection in a separate @tech/reference{place} using the
keyword argument. Such a connection will not block all Racket threads @racket[#:use-place] keyword argument. Such a connection will not
during queries; the disadvantage is the cost of creating and block all Racket threads during queries; the disadvantage is the cost
communicating with a separate @tech/reference{place}. 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.
@ -567,14 +567,14 @@ ODBC's DSNs.
@racket[data-source], then @racket[dsn-file] is ignored. @racket[data-source], then @racket[dsn-file] is ignored.
@examples/results[ @examples/results[
[(put-dsn 'mydb [(put-dsn 'pg
(postgresql-data-source #:user "me" (postgresql-data-source #:user "me"
#:database "mydb" #:database "mydb"
#:password "icecream")) #:password "icecream"))
(void)] (void)]
[(dsn-connect 'mydb) [(dsn-connect 'pg)
(new connection%)] (new connection%)]
[(dsn-connect 'mydb #:notice-handler (lambda (code msg) ....)) [(dsn-connect 'pg #:notice-handler (lambda (code msg) ....))
(new connection%)] (new connection%)]
] ]
} }

View File

@ -492,19 +492,34 @@ closed.
The functions described in this section provide a consistent interface The functions described in this section provide a consistent interface
to transactions. to transactions.
ODBC connections should use these functions exclusively instead of A @deftech{managed transaction} is one created via either
transaction-changing SQL statements such as @tt{START TRANSACTION} and @racket[start-transaction] or @racket[call-with-transaction]. In
@tt{COMMIT}. Using transaction-changing SQL may cause these functions contrast, an @deftech{unmanaged transaction} is one created by
to behave incorrectly and may cause additional problems in the ODBC evaluating a SQL statement such as @tt{START TRANSACTION}. A
driver. @deftech{nested transaction} is a transaction created within the
extent of an existing transaction. If a nested transaction is
committed, its changes are promoted to the enclosing transaction,
which may itself be committed or rolled back. If a nested transaction
is rolled back, its changes are discarded, but the enclosing
transaction remains open. Nested transactions are implemented via SQL
@tt{SAVEPOINT}, @tt{RELEASE SAVEPOINT}, and @tt{ROLLBACK TO
SAVEPOINT}.
PostgreSQL, MySQL, and SQLite connections are discouraged from using ODBC connections must use @tech{managed transactions} exclusively;
transaction-changing SQL statements, but the consequences are less using transaction-changing SQL may cause these functions to behave
dire. The functions below will behave correctly, but the syntax and incorrectly and may cause additional problems in the ODBC driver. ODBC
behavior of the SQL statements is idiosyncratic. For example, in MySQL connections do not support @tech{nested transactions}.
@tt{START TRANSACTION} commits the current transaction, if one is
active; in PostgreSQL @tt{COMMIT} silently rolls back the current PostgreSQL, MySQL, and SQLite connections must not mix @tech[#:key
transaction if an error occurred in a previous statement. "managed transaction"]{managed} and @tech[#:key "unmanaged
transaction"]{unmanaged} transactions. For example, calling
@racket[start-transaction] and then executing a @tt{ROLLBACK}
statement is not allowed. Note that in MySQL, some SQL statements have
@hyperlink["http://dev.mysql.com/doc/refman/5.0/en/implicit-commit.html"]{implicit
transaction effects}. For example, in MySQL a @tt{CREATE TABLE}
statement implicitly commits the current transaction. These statements
also must not be used within @tech{managed transactions}. (In
contrast, PostgreSQL and SQLite both support transactional DDL.)
@bold{Errors} Query errors may affect an open transaction in one of @bold{Errors} Query errors may affect an open transaction in one of
three ways: three ways:
@ -512,8 +527,7 @@ three ways:
@item{the transaction remains open and unchanged} @item{the transaction remains open and unchanged}
@item{the transaction is automatically rolled back} @item{the transaction is automatically rolled back}
@item{the transaction becomes an @deftech{invalid transaction}; all @item{the transaction becomes an @deftech{invalid transaction}; all
subsequent queries will fail until the transaction is explicitly subsequent queries will fail until the transaction is rolled back}
rolled back}
] ]
To avoid the silent loss of information, this library attempts to To avoid the silent loss of information, this library attempts to
avoid behavior (2) completely by marking transactions as invalid avoid behavior (2) completely by marking transactions as invalid
@ -525,31 +539,37 @@ to what errors cause which behaviors:
parameter arity and type errors, leave the transaction open and parameter arity and type errors, leave the transaction open and
unchanged (1).} unchanged (1).}
@item{All errors originating from PostgreSQL cause the transaction to @item{All errors originating from PostgreSQL cause the transaction to
become invalid (3).} become @tech[#:key "invalid transaction"]{invalid} (3).}
@item{Most errors originating from MySQL leave the transaction open @item{Most errors originating from MySQL leave the transaction open
and unchanged (1), but a few cause the transaction to become invalid and unchanged (1), but a few cause the transaction to become
(3). In the latter cases, the underlying behavior @tech[#:key "invalid transaction"]{invalid} (3). In the latter
of MySQL is to roll back the transaction but @emph{leave it open} cases, the underlying behavior of MySQL is to roll back the
(see @hyperlink["http://dev.mysql.com/doc/refman/5.1/en/innodb-error-handling.html"]{the transaction but @emph{leave it open} (see
@hyperlink["http://dev.mysql.com/doc/refman/5.1/en/innodb-error-handling.html"]{the
MySQL documentation}). This library detects those cases and marks MySQL documentation}). This library detects those cases and marks
the transaction invalid instead.} the transaction @tech[#:key "invalid transaction"]{invalid}
instead.}
@item{Most errors originating from SQLite leave the transaction open @item{Most errors originating from SQLite leave the transaction open
and unchanged (1), but a few cause the transaction to become and unchanged (1), but a few cause the transaction to become
invalid (3). In the latter cases, the underlying behavior of SQLite @tech[#:key "invalid transaction"]{invalid} (3). In the latter
is to roll back the transaction (see cases, the underlying behavior of SQLite is to roll back the
transaction (see
@hyperlink["http://www.sqlite.org/lang_transaction.html"]{the SQLite @hyperlink["http://www.sqlite.org/lang_transaction.html"]{the SQLite
documentation}). This library detects those cases and marks the documentation}). This library detects those cases and marks the
transaction invalid instead.} transaction @tech[#:key "invalid transaction"]{invalid} instead.}
@item{All errors originating from an ODBC driver cause the transaction @item{All errors originating from an ODBC driver cause the transaction
to become invalid (3). The underlying behavior of ODBC drivers to become @tech[#:key "invalid transaction"]{invalid} (3). The
varies widely, and ODBC provides no mechanism to detect when an underlying behavior of ODBC drivers varies widely, and ODBC provides
existing transaction has been rolled back, so this library no mechanism to detect when an existing transaction has been rolled
intercepts all errors and marks the transaction invalid instead.} back, so this library intercepts all errors and marks the
transaction @tech[#:key "invalid transaction"]{invalid} instead.}
] ]
Future versions of this library may refine the set of errors that If a nested transaction marked @tech[#:key "invalid
invalidate a transaction (for example, by identifying innocuous ODBC transaction"]{invalid} is rolled back, the enclosing transaction is
errors by SQLSTATE) and may provide an option to automatically typically still valid.
rollback invalid transactions.
If a transaction is open when a connection is disconnected, it is
implicitly rolled back.
@defproc[(start-transaction [c connection?] @defproc[(start-transaction [c connection?]
[#:isolation isolation-level [#:isolation isolation-level
@ -566,37 +586,50 @@ rollback invalid transactions.
database-dependent; it may be a default isolation level or it may be database-dependent; it may be a default isolation level or it may be
the isolation level of the previous transaction. the isolation level of the previous transaction.
If @racket[c] is already in a transaction, an exception is raised. If @racket[c] is already in a transaction, @racket[isolation-level]
must be @racket[#f], and a @tech{nested transaction} is opened.
} }
@defproc[(commit-transaction [c connection?]) void?]{ @defproc[(commit-transaction [c connection?]) void?]{
Attempts to commit the current transaction, if one is active. If the Attempts to commit the current transaction, if one is open. If the
transaction cannot be commited, an exception is raised. transaction cannot be commited (for example, if it is @tech[#:key
"invalid transaction"]{invalid}), an exception is raised.
If no transaction is active, this function has no effect. If the current transaction is a @tech{nested transaction}, the
nested transaction is closed, its changes are incorporated into the
enclosing transaction, and the enclosing transaction is resumed.
If no transaction is open, this function has no effect.
} }
@defproc[(rollback-transaction [c connection?]) void?]{ @defproc[(rollback-transaction [c connection?]) void?]{
Rolls back the current transaction, if one is active. Rolls back the current transaction, if one is open.
If no transaction is active, this function has no effect. If the current transaction is a @tech{nested transaction}, the
nested transaction is closed, its changes are abandoned, and the
enclosing transaction is resumed.
If no transaction is open, this function has no effect.
} }
@defproc[(in-transaction? [c connection?]) @defproc[(in-transaction? [c connection?])
boolean?]{ boolean?]{
Returns @racket[#t] if @racket[c] has a transaction is active, Returns @racket[#t] if @racket[c] has an open transaction
@racket[#f] otherwise. (@tech[#:key "managed transaction"]{managed} or @tech[#:key
"unmanaged transaction"]{unmanaged}), @racket[#f] otherwise.
} }
@defproc[(needs-rollback? [c connection?]) boolean?]{ @defproc[(needs-rollback? [c connection?]) boolean?]{
Returns @racket[#t] if @racket[c] is in an @tech{invalid Returns @racket[#t] if @racket[c] is in an @tech{invalid
transaction}. All queries executed using @racket[c] will fail until transaction}. All queries executed using @racket[c] will fail until
the transaction is explicitly rolled back using the transaction is rolled back (either using
@racket[rollback-transaction]. @racket[rollback-transaction], if the transaction was created with
@racket[start-transaction], or when the procedure passed to
@racket[call-with-transaction] returns).
} }
@defproc[(call-with-transaction [c connection?] @defproc[(call-with-transaction [c connection?]
@ -613,8 +646,26 @@ rollback invalid transactions.
Calls @racket[proc] in the context of a new transaction with Calls @racket[proc] in the context of a new transaction with
isolation level @racket[isolation-level]. If @racket[proc] completes isolation level @racket[isolation-level]. If @racket[proc] completes
normally, the transaction is committed and @racket[proc]'s results normally, the transaction is committed and @racket[proc]'s results
are returned. If @racket[proc] raises an exception, the transaction are returned. If @racket[proc] raises an exception (or if the
is rolled back. implicit commit at the end raises an exception), the transaction is
rolled back and the exception is re-raised.
If @racket[call-with-transaction] is called within a transaction,
@racket[isolation-level] must be @racket[#f], and it creates a
@tech{nested transaction}. Within the extent of a call to
@racket[call-with-transaction], transactions must be properly
nested. In particular:
@itemlist[
@item{Calling either @racket[commit-transaction] or
@racket[rollback-transaction] when the open transaction was
created by @racket[call-with-transaction] causes an exception to be
raised.}
@item{If a further nested transaction is open when @racket[proc]
completes (that is, created by an unmatched
@racket[start-transaction] call), an exception is raised and the
nested transaction created by @racket[call-with-transaction] is
rolled back.}
]
} }
@section{SQL Errors} @section{SQL Errors}
@ -651,7 +702,7 @@ type.
provide SQLSTATE error codes. provide SQLSTATE error codes.
} }
@section{Database Information} @section{Database Catalog Information}
@defproc[(list-tables [c connection?] @defproc[(list-tables [c connection?]
[#:schema schema [#:schema schema

View File

@ -13,7 +13,8 @@
"db/sql-types.rkt" "db/sql-types.rkt"
"db/concurrent.rkt")) "db/concurrent.rkt"))
(prefix-in gen- (prefix-in gen-
(combine-in "gen/sql-types.rkt" (combine-in "gen/misc.rkt"
"gen/sql-types.rkt"
"gen/query.rkt"))) "gen/query.rkt")))
(provide (all-defined-out)) (provide (all-defined-out))
@ -193,7 +194,8 @@ Testing profiles are flattened, not hierarchical.
(define generic-test (define generic-test
(make-test-suite "Generic tests (no db)" (make-test-suite "Generic tests (no db)"
(list gen-sql-types:test (list gen-misc:test
gen-sql-types:test
gen-query:test))) gen-query:test)))
;; ---- ;; ----

View File

@ -266,12 +266,196 @@
(check-equal? (in-transaction? c) #t) (check-equal? (in-transaction? c) #t)
(check-pred void? (rollback-transaction c)) (check-pred void? (rollback-transaction c))
(check-equal? (in-transaction? c) #f))) (check-equal? (in-transaction? c) #f)))
(test-case "error on managed st, unmanaged end"
(with-connection c
(start-transaction c)
(check-exn #rx"ROLLBACK not allowed within managed transaction"
(lambda () (query-exec c "ROLLBACK")))
(check-equal? (in-transaction? c) #t)
;; SQLite-ODBC is unhappy with open tx on disconnect
(rollback-transaction c)))
(unless (ANYFLAGS 'odbc)
(test-case "unmanaged st, managed end ok"
(with-connection c
(query-exec c (cond [(ANYFLAGS 'ispg 'ismy) "START TRANSACTION"]
[(ANYFLAGS 'issl) "BEGIN TRANSACTION"]))
(check-equal? (in-transaction? c) #t)
(rollback-transaction c)
(check-equal? (in-transaction? c) #f))))
(test-case "error on cwt, unmanaged end"
(with-connection c
(check-exn #rx"ROLLBACK not allowed within managed transaction"
(lambda ()
(call-with-transaction c
(lambda () (query-exec c "ROLLBACK")))))
(check-equal? (in-transaction? c) #f)))
(when (and (ANYFLAGS 'ispg 'issl) (not (ANYFLAGS 'odbc)))
(test-case "transactional ddl"
(with-connection c
(start-transaction c)
(query-exec c "create table foo (n integer)")
(define exists1 (table-exists? c "foo"))
(rollback-transaction c)
(define exists2 (table-exists? c "foo"))
(when exists2 (query-exec c "drop table foo")) ;; shouldn't happen
(check-equal? exists1 #t)
(check-equal? exists2 #f))))
(when (ANYFLAGS 'ismy 'odbc)
(test-case "error on implicit-commit stmt"
(with-connection c
(start-transaction c)
(check-exn #rx"statement with implicit commit not allowed"
(lambda () (query-exec c "create table foo (n integer)")))
;; SQLite-ODBC is unhappy with open tx on disconnect
(rollback-transaction c))))
(when (ANYFLAGS 'odbc)
(test-case "error on repeated start" (test-case "error on repeated start"
(with-connection c
(start-transaction c)
(check-exn #rx"already in transaction"
(lambda () (start-transaction c))))))
(unless (ANYFLAGS 'odbc)
(test-case "start, start"
(with-connection c
(check-pred void? (start-transaction c))
(check-pred void? (start-transaction c))
(check-equal? (in-transaction? c) #t)
(check-pred void? (commit-transaction c))
(check-equal? (in-transaction? c) #t)
(check-pred void? (commit-transaction c))
(check-equal? (in-transaction? c) #f))))
(when (ANYFLAGS 'odbc)
(test-case "start, start fails"
(with-connection c (with-connection c
(start-transaction c) (start-transaction c)
(check-exn #rx"already in transaction" (check-exn #rx"already in transaction"
(lambda () (start-transaction c))))) (lambda () (start-transaction c)))))
(test-case "call-with-tx" (test-case "cwt, start fails"
(with-connection c
(start-transaction c)
(check-exn #rx"already in transaction"
(lambda () (call-with-transaction c void))))))
(test-case "commit w/o start is no-op"
(with-connection c
(check-pred void? (commit-transaction c))))
(test-case "rollback w/o start is no-op"
(with-connection c
(check-pred void? (rollback-transaction c))))
(test-case "cwt normal"
(with-connection c
(check-equal? (call-with-transaction c
(lambda () (query-value c (select-val "'abc'"))))
"abc")))
(test-case "cwt w/ error"
(with-connection c
(check-exn exn:fail?
(lambda ()
(call-with-transaction c
(lambda () (query-value c (select-val "foo"))))))
(check-equal? (in-transaction? c) #f)))
(test-case "cwt w/ caught error"
(with-connection c
(define (check-pg-exn proc)
(if (ANYFLAGS 'ispg 'odbc) (check-exn exn:fail? proc) (proc)))
(let ([ok? #f])
(check-pg-exn
(lambda ()
(call-with-transaction c
(lambda ()
(with-handlers ([exn:fail? void?])
(query-value c (select-val "foo")))
(set! ok? (in-transaction? c))))))
(check-equal? ok? #t "still in tx after caught error")
(check-equal? (in-transaction? c) #f))))
(unless (ANYFLAGS 'odbc)
(test-case "cwt w/ unclosed tx"
(with-connection c
(check-exn #rx"unclosed nested tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(start-transaction c)
(query-value c (select-val "17"))))))
(check-equal? (in-transaction? c) #f)))
(test-case "cwt w/ unbalanced commit"
(with-connection c
(check-exn #rx"commit-tr.* start-tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(commit-transaction c)))))
(check-equal? (in-transaction? c) #f)))
(test-case "cwt w/ unbalanced rollback"
(with-connection c
(check-exn #rx"rollback-tr.* start-tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(rollback-transaction c)))))
(check-equal? (in-transaction? c) #f)))
;; start-tx, then call-with-tx
(test-case "st, cwt normal"
(with-connection c
(start-transaction c)
(check-equal? (call-with-transaction c
(lambda () (query-value c (select-val "17"))))
17)
(check-equal? (in-transaction? c) #t)))
(test-case "st, cwt w/ error"
(with-connection c
(start-transaction c)
(check-exn exn:fail?
(lambda ()
(call-with-transaction c
(lambda () (query-value c (select-val "foo"))))))
(check-equal? (in-transaction? c) #t)))
(test-case "st, cwt w/ caught error"
(with-connection c
(define (check-pg-exn proc)
(if (ANYFLAGS 'ispg) (check-exn exn:fail? proc) (proc)))
(let ([ok? #f])
(start-transaction c)
(check-pg-exn
(lambda ()
(call-with-transaction c
(lambda ()
(with-handlers ([exn:fail? void?])
(query-value c (select-val "foo")))
(set! ok? (in-transaction? c))))))
(check-equal? ok? #t "still in tx after caught error")
(check-equal? (in-transaction? c) #t))))
(test-case "st, cwt w/ unclosed tx"
(with-connection c
(start-transaction c)
(check-exn #rx"unclosed nested tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(start-transaction c)
(query-value c (select-val "17"))))))
(check-equal? (in-transaction? c) #t)))
(test-case "st, cwt w/ unbalanced commit"
(with-connection c
(start-transaction c)
(check-exn #rx"commit-tr.* start-tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(commit-transaction c)))))
(check-equal? (in-transaction? c) #t)))
(test-case "cwt w/ unbalanced rollback"
(with-connection c
(start-transaction c)
(check-exn #rx"rollback-tr.* start-tr.* .within .* call-with-transaction"
(lambda ()
(call-with-transaction c
(lambda ()
(rollback-transaction c)))))
(check-equal? (in-transaction? c) #t))))
(test-case "cwt misc"
(with-connection c (with-connection c
(check-equal? (call-with-transaction c (check-equal? (call-with-transaction c
(lambda () (lambda ()

View File

@ -0,0 +1,19 @@
#lang racket/base
(require rackunit
racket/class
"../config.rkt")
(provide misc:test)
(require db/private/generic/interfaces)
(define misc:test
(test-suite "Misc internal function tests"
(test-case "sql-skip-comments"
(define (eat s [hash? #f]) (substring s (sql-skip-comments s 0 #:hash-comments? hash?)))
(check-equal? (eat "/* blah ** blah */ insert")
" insert")
(check-equal? (eat "-- blah\n -- /* \nok")
"ok")
(check-equal? (eat "#a\n# b c d\nok" #t)
"ok"))))