#lang scheme
(require "sqlite-ffi.ss"
(only-in (lib "foreign.ss")
register-finalizer))
(define-struct (exn:sqlite exn) ())
(define-struct db (handle) #:mutable)
(define-struct statement (handle) #:mutable)
(define sqlite-datum/c
(or/c integer? number? string? bytes? false/c))
(provide/contract
[exn:sqlite? (any/c . -> . boolean?)]
[statement? (any/c . -> . boolean?)]
[db? (any/c . -> . boolean?)]
[sqlite-datum/c contract?]
[open ((or/c path? (symbols ':memory: ':temp:)) . -> . db?)]
[close (db? . -> . void?)]
[errmsg (db? . -> . string?)]
[last-insert-rowid (db? . -> . integer?)]
[changes-count (db? . -> . integer?)]
[total-changes-count (db? . -> . integer?)]
[prepare (db? string? . -> . statement?)]
[load-params ((statement?) () #:rest (listof sqlite-datum/c) . ->* . void?)]
[step (statement? . -> . (or/c (vectorof sqlite-datum/c) false/c))]
[step* (statement? . -> . (listof (vectorof sqlite-datum/c)))]
[run ((statement?) () #:rest (listof sqlite-datum/c) . ->* . void?)] [reset (statement? . -> . void?)]
[finalize (statement? . -> . void?)]
[exec ((db? string? ((vectorof string?) (vectorof sqlite-datum/c) . -> . integer?)) () #:rest (listof sqlite-datum/c) . ->* . void?)]
[exec/ignore ((db? string?) () #:rest (listof sqlite-datum/c) . ->* . void?)]
[insert ((db? string?) () #:rest (listof sqlite-datum/c) . ->* . integer?)]
[select ((db? string?) () #:rest (listof sqlite-datum/c) . ->* . (listof (vectorof sqlite-datum/c)))]
[with-transaction* (db? (symbols 'none 'deferred 'immediate 'exclusive) (any/c . -> . any/c) . -> . any/c)])
(provide with-transaction/lock
with-transaction)
(define (wrap-finalizer o f)
(register-finalizer o f)
o)
(define (sqlite-error fmt . args)
(raise (make-exn:sqlite
(string->immutable-string
(format "SQLite Error: ~a"
(apply format fmt args)))
(current-continuation-marks))))
(define (handle-status db s)
(if (or (= s SQLITE_OK)
(= s SQLITE_ROW)
(= s SQLITE_DONE))
s
(sqlite-error "~a" (lookup-status-message db s))))
(define (lookup-status-message db s)
(if (and (eq? s SQLITE_ERROR) db)
(errmsg db)
(cdr (assoc s
`([,SQLITE_ERROR . "Generic error, perhaps call errmsg?"]
[,SQLITE_INTERNAL . "An internal logic error in SQLite"]
[,SQLITE_PERM . "Access permission denied"]
[,SQLITE_ABORT . "Callback routine requested an abort"]
[,SQLITE_BUSY . "The database file is locked"]
[,SQLITE_LOCKED . "table in the database is locked"]
[,SQLITE_NOMEM . "A malloc() failed"]
[,SQLITE_READONLY . "Attempt to write a readonly database"]
[,SQLITE_INTERRUPT . "Operation terminated by sqlite3_interrupt()"]
[,SQLITE_IOERR . "Some kind of disk I/O error occurred"]
[,SQLITE_CORRUPT . "The database disk image is malformed"]
[,SQLITE_NOTFOUND . "(Internal Only) Table or record not found"]
[,SQLITE_FULL . "Insertion failed because database is full"]
[,SQLITE_CANTOPEN . "Unable to open the database file"]
[,SQLITE_PROTOCOL . "Database lock protocol error"]
[,SQLITE_EMPTY . "Database is empty"]
[,SQLITE_SCHEMA . "The database schema changed"]
[,SQLITE_TOOBIG . "Too much data for one row of a table"]
[,SQLITE_CONSTRAINT . "Abort due to contraint violation"]
[,SQLITE_MISMATCH . "Data type mismatch"]
[,SQLITE_MISUSE . "Library used incorrectly"]
[,SQLITE_NOLFS . "Uses OS features not supported on host"]
[,SQLITE_AUTH . "Authorization denied"]
[,SQLITE_FORMAT . "Auxiliary database format error"]
[,SQLITE_RANGE . "2nd parameter to sqlite3_bind out of range"]
[,SQLITE_NOTADB . "File opened that is not a database file"])))))
(define (open db-path)
(let*-values
([(db-path)
(cond
[(symbol? db-path)
(case db-path
[(:memory:) #":memory:"]
[(:temp:) #""])]
[(relative-path? db-path)
(path->bytes (build-path (current-directory) db-path))]
[else
(path->bytes db-path)])]
[(db-ptr open-status) (sqlite3_open db-path)]
[(the-db)
(wrap-finalizer
(make-db db-ptr)
close)])
(when (handle-status the-db open-status)
the-db)))
(define (close db)
(let ([o-handle (db-handle db)])
(set-db-handle! db #f)
(when o-handle
(handle-status db (sqlite3_close o-handle)))
(void)))
(define (prepare db sql)
(let*-values
([(stmt prep-status tail)
(sqlite3_prepare_v2 (db-handle db) sql)]
[(the-stmt) (wrap-finalizer (make-statement stmt)
finalize)])
(when (not (zero? (string-length tail)))
(sqlite-error "You should only prepare one statement at a time! ~s" tail))
(when (handle-status db prep-status)
(if stmt
the-stmt
(sqlite-error "sqlite3_prepare_v2 returned a NULL pointer")))))
(define (load-params stmt . params)
(let* ((handle (statement-handle stmt))
(parameter-count (sqlite3_bind_parameter_count handle)))
(when (not (= (length params) parameter-count))
(raise-mismatch-error
'load-params
(format "Given ~a params when statement requires ~a params" (length params) parameter-count)
params))
(begin
(reset stmt)
(let loop ((i 1) (params params))
(if (null? params)
(void)
(begin
(let* ([param (car params)])
(handle-status
#f
(cond
[(integer? param)
(sqlite3_bind_int64 handle i param)]
[(number? param)
(sqlite3_bind_double handle i param)]
[(string? param)
(sqlite3_bind_text handle i param)]
[(bytes? param)
(sqlite3_bind_blob handle i param)]
[else
(sqlite3_bind_null handle i)])))
(loop (add1 i) (cdr params))))))))
(define (step stmt)
(let ([s (handle-status
#f
(sqlite3_step (statement-handle stmt)))]
[string-up
(lambda (s)
(if s (string-upcase s) #f))])
(cond
[(= s SQLITE_ROW)
(let* ((cols (sqlite3_column_count (statement-handle stmt)))
(vec (make-vector cols))
(handle (statement-handle stmt)))
(let loop ((i 0))
(if (= i cols)
vec
(begin
(vector-set!
vec
i
(match (string-up (sqlite3_column_decltype handle i))
["NULL" #f]
["INTEGER" (sqlite3_column_int64 handle i)]
["FLOAT" (sqlite3_column_double handle i)]
[(or #f "STRING" "TEXT")
(sqlite3_column_text handle i)]
["BLOB" (sqlite3_column_blob handle i)]))
(loop (add1 i))))))]
[(= s SQLITE_DONE)
#f])))
(define (reset stmt)
(handle-status #f (sqlite3_reset (statement-handle stmt)))
(void))
(define (finalize stmt)
(let ([o-handle (statement-handle stmt)])
(set-statement-handle! stmt #f)
(when o-handle
(handle-status #f (sqlite3_finalize o-handle)))
(void)))
(define (lock-type->string lock-type)
(case lock-type
[(none) ""]
[(deferred) "DEFERRED"]
[(immediate) "IMMEDIATE"]
[(exclusive) "EXCLUSIVE"]))
(define (with-transaction* db lock-type body-f)
(let ((end #f))
(dynamic-wind
(lambda ()
(set! end (lambda ()
(exec/ignore db "ROLLBACK TRANSACTION")))
(exec/ignore db
(format "BEGIN ~a TRANSACTION"
(lock-type->string lock-type))))
(lambda ()
(let/ec fail
(begin0
(body-f fail)
(set! end
(lambda ()
(exec/ignore db "COMMIT TRANSACTION"))))))
(lambda ()
(end)))))
(define-syntax with-transaction/lock
(syntax-rules ()
[(_ (db lock-type fail) body ...)
(with-transaction* db 'lock-type (lambda (fail) body ...))]))
(define-syntax with-transaction
(syntax-rules ()
[(_ (db fail) body ...)
(with-transaction/lock (db none fail) body ...)]))
(define (errmsg db)
(sqlite3_errmsg (db-handle db)))
(define (changes-count db)
(sqlite3_changes (db-handle db)))
(define (total-changes-count db)
(sqlite3_total_changes (db-handle db)))
(define (last-insert-rowid db)
(sqlite3_last_insert_rowid (db-handle db)))
(define (step* stmt)
(let loop ([r (list)])
(let ([c (step stmt)])
(if (not c)
(reverse r)
(loop (cons c r))))))
(define (insert db sql . params)
(apply exec/ignore db sql params)
(last-insert-rowid db))
(define (select db sql . params)
(let ([results empty]
[names #f])
(apply exec db sql
(lambda (ns row)
(set! names ns)
(set! results (list* row results))
0)
params)
(if names
(list* names (reverse results))
empty)))
(define (statement-names stmt)
(define handle (statement-handle stmt))
(list->vector
(map (lambda (i) (sqlite3_column_name handle i))
(build-list (sqlite3_column_count handle) values))))
(define (run* statement callback . params)
(when (not (empty? params))
(apply load-params statement params))
(let ([names (statement-names statement)])
(let loop ([row (step statement)])
(when row
(if (zero? (callback names row))
(loop (step statement))
(sqlite-error "Callback routine requested an abort")))))
(void))
(define (run statement . params)
(apply run* statement (lambda (names row) (void)) params))
(define (exec db sql callback . params)
(define statement (prepare db sql))
(apply run* statement callback params)
(finalize statement))
(define (exec/ignore db sql . params)
(apply exec db sql (lambda (names row) 0) params))