#lang scheme/base
(require scheme/class
scheme/contract
mzlib/etc
mzlib/kw
srfi/26/cut
(planet untyped/unlib:3/gen)
(planet untyped/unlib:3/parameter)
(planet untyped/unlib:3/pipeline)
"base.ss"
"snooze-interface.ss"
"era/era.ss"
"generic/connection.ss"
"generic/database.ss"
"sql/sql.ss")
(define (make-snooze database #:auto-connect? [auto-connect? #f])
(new snooze% [database database] [auto-connect? auto-connect?]))
(define snooze%
(class* object% (snooze<%>)
(init-field [database #f])
(init-field [auto-connect? #f])
(super-new)
(define current-connection-cell
(make-thread-cell #f))
(define transaction-pipeline
null)
(define/public (get-database)
database)
(define/public (set-database! new-database)
(set! database new-database))
(define/public (get-transaction-pipeline)
transaction-pipeline)
(define/public (set-transaction-pipeline! pipeline)
(set! transaction-pipeline pipeline))
(define/public (call-with-connection thunk)
(dynamic-wind (cut connect)
(cut thunk)
(cut disconnect)))
(define/public (connect)
(unless (thread-cell-ref current-connection-cell)
(thread-cell-set! current-connection-cell (send database connect))))
(define/public (disconnect)
(when (thread-cell-ref current-connection-cell)
(send database disconnect (thread-cell-ref current-connection-cell))
(thread-cell-set! current-connection-cell #f)))
(define (auto-connect)
(when (and auto-connect? (not (thread-cell-ref current-connection-cell)))
(connect)))
(define/public (current-connection)
(define conn (thread-cell-ref current-connection-cell))
(if conn
conn
(raise-exn exn:fail:snooze
"No database connection: use call-with-connection to set one up.")))
(define/public (create-table entity)
(auto-connect)
(send database create-table (current-connection) entity))
(define/public (drop-table entity)
(auto-connect)
(send database drop-table (current-connection) entity))
(define/public (save! struct)
(define id (struct-id struct))
(define revision (struct-revision struct))
(define entity (struct-entity struct))
(auto-connect)
(call-with-transaction
(lambda ()
(if id
(begin (if (and revision (record-exists-with-revision? entity id revision))
(begin (set-struct-revision! struct (add1 revision))
(call-with-pipeline
(append (entity-save-pipeline entity) (entity-update-pipeline entity))
(lambda (conn struct)
(send database update-record conn struct)
struct)
(current-connection)
struct))
(raise-exn exn:fail:snooze:revision
"Structure has been revised since it was loaded from the database."
struct)))
(begin (set-struct-revision! struct 0)
(call-with-pipeline
(append (entity-save-pipeline entity) (entity-insert-pipeline entity))
(lambda (conn struct)
(set-struct-id! struct (send database insert-record conn struct))
struct)
(current-connection)
struct))))))
(define/public (delete! struct)
(define id (struct-id struct))
(define entity (struct-entity struct))
(define revision (struct-revision struct))
(auto-connect)
(if id
(call-with-transaction
(lambda ()
(if (and revision (record-exists-with-revision? entity id (struct-revision struct)))
(call-with-pipeline
(entity-delete-pipeline entity)
(lambda (conn struct)
(send database delete-record conn (struct-guid struct))
(set-struct-id! struct #f)
struct)
(current-connection)
struct)
(raise-exn exn:fail:snooze:revision
"Database has been revised since structure was loaded."
struct))))
(raise-exn exn:fail:snooze
(format "Cannot delete a struct that has not been saved to the database: ~a" struct))))
(define/public (insert/id+revision! struct [pipeline null])
(define id (struct-id struct))
(define entity (struct-entity struct))
(auto-connect)
(call-with-pipeline pipeline
(cut send database insert-record/id <> <>)
(current-connection)
struct)
struct)
(define/public (update/id+revision! struct [pipeline null])
(define id (struct-id struct))
(define entity (struct-entity struct))
(auto-connect)
(call-with-pipeline pipeline
(cut send database update-record <> <>)
(current-connection)
struct)
struct)
(define/public (delete/id+revision! struct [pipeline null])
(define id (struct-id struct))
(define entity (struct-entity struct))
(define revision (struct-revision struct))
(auto-connect)
(call-with-pipeline pipeline
(lambda (conn struct)
(send database delete-record (current-connection) (struct-guid struct)))
(current-connection)
struct)
struct)
(define/public (find-all query)
(g:collect (g:find query)))
(define/public (find-one query)
(define result ((g:find query)))
(and (not (g:end? result)) result))
(define/public (g:find select)
(auto-connect)
(send database g:find (current-connection) select))
(define/public (call-with-transaction body . metadata-args)
(define conn (current-connection))
(auto-connect)
(if (send database transaction-allowed? conn)
(call-with-transaction-frame
(cut send database call-with-transaction
conn
(lambda ()
(apply call-with-pipeline
(get-transaction-pipeline)
(lambda args (body))
conn
metadata-args))))
(body)))
(define/public (find-by-id entity id)
(cond [(not id) #f]
[(integer? id)
(let ([x (sql:entity 'x entity)])
(find-one (sql:select #:from x #:where (sql:= (sql:attr x 'id) id))))]
[else (raise-exn exn:fail:snooze
(format "Expected (U integer #f), received ~s." id))]))
(define/public (find-by-guid guid)
(find-by-id (guid-entity guid) (guid-id guid)))
(define/public (table-names)
(auto-connect)
(send database table-names (current-connection)))
(define/public (table-exists? table)
(auto-connect)
(send database table-exists? (current-connection) table))
(define/public (dump-sql query [format "~a~n"] [output-port (current-output-port)])
(send database dump-sql query output-port format))
(define (record-exists-with-revision? entity id revision)
(define x (sql:entity 'x entity))
(define x-id (sql:attr x 'id))
(define x-rev (sql:attr x 'revision))
(if (find-one (sql:select #:what x-id
#:from x
#:where (sql:and (sql:= x-id id)
(sql:= x-rev revision))))
#t
#f))
(inspect #f)))
(define snooze%/c
(object-contract
[field database (is-a?/c database<%>)]
[field auto-connect? boolean?]
[get-database (-> (is-a?/c database<%>))]
[set-database! (-> (is-a?/c database<%>) void?)]
[get-transaction-pipeline (-> (listof procedure?))]
[set-transaction-pipeline! (-> (listof procedure?) void?)]
[call-with-connection (-> procedure? any)]
[connect (-> any)]
[disconnect (-> any)]
[current-connection (-> connection?)]
[create-table (-> entity? void?)]
[drop-table (-> (or/c entity? symbol?) void?)]
[save! (-> persistent-struct? persistent-struct?)]
[delete! (-> persistent-struct? persistent-struct?)]
[insert/id+revision! (->* (persistent-struct?)
((listof procedure?))
persistent-struct?)]
[update/id+revision! (->* (persistent-struct?)
((listof procedure?))
persistent-struct?)]
[delete/id+revision! (->* (persistent-struct?)
((listof procedure?))
persistent-struct?)]
[find-all (-> query? list?)]
[find-one (-> query? any)]
[g:find (-> query? procedure?)]
[call-with-transaction (->* (procedure?) ((or/c string? false/c)) any)]
[find-by-id (-> entity? (or/c integer? false/c) (or/c persistent-struct? false/c))]
[find-by-guid (-> guid? (or/c persistent-struct? false/c))]
[table-names (-> (listof symbol?))]
[table-exists? (-> (or/c entity? symbol?) boolean?)]
[dump-sql (->* (query?)
(string? output-port?)
query?)]))
(provide/contract
[snooze% class?]
[snooze%/c contract?]
[make-snooze (->* ((is-a?/c database<%>))
(#:auto-connect? boolean?)
snooze%/c)])