#lang mzscheme
(require mzlib/etc
(only mzlib/list sort)
mzlib/struct
scheme/class
scheme/contract
(only scheme/private/list
filter
foldl)
scheme/match
(only srfi/1/list
delete-duplicates
unzip2)
srfi/19/time
srfi/26/cut
(planet untyped/unlib:3/hash-table)
(planet untyped/unlib:3/parameter)
(planet untyped/unlib:3/pipeline)
"../snooze-mzscheme.ss"
"attribute.ss"
"delta.ss"
"entity.ss"
"frame.ss"
"transaction.ss")
(define-struct/properties audit-stage (proc)
([prop:procedure (lambda (stage . args)
(apply (audit-stage-proc stage) args))])
#f)
(define audit-trail<%>
(interface (delta-api<%> entity-cache<%> attribute-cache<%>)
init!
current-audit-transaction
format-log-values
audit-transaction-deltas
audit-deltas->guids
audit-struct-history
audit-struct-transaction-history
audit-struct-snapshot
audit-transaction-affected
audit-roll-back!))
(define audit-trail%
(class* delta-api% (audit-trail<%>)
(inherit id->entity
entity->id
id->attribute
attribute->id
audit-delta-guid
revert-delta!)
(inherit-field snooze
entity-cache
attribute-cache)
(init-field entity:audit-transaction)
(init-field entities)
(field [in-audit? (make-parameter #f)])
(field [current-audit-frame (make-parameter #f)])
(define-alias ENTITY audit-entity)
(define-alias ATTR audit-attribute)
(define-alias DELTA audit-delta)
(define-sql TXN (q:entity 'TXN entity:audit-transaction))
(define-sql TXN-id (q:attr TXN 'id))
(field [transaction-stage #f])
(field [insert-stage #f])
(field [update-stage #f])
(field [delete-stage #f])
(super-new)
(define audit-transaction?
(entity-predicate entity:audit-transaction))
(define-snooze-interface snooze)
(define/public (init!)
(set! transaction-stage
(make-audit-stage
(lambda (continue conn . log-values)
(if (in-audit?)
(apply continue conn log-values)
(parameterize ([in-audit? #t])
(let ([frame (new audit-frame%
[snooze snooze]
[entity:audit-transaction entity:audit-transaction]
[entity-cache entity-cache]
[attribute-cache attribute-cache])])
(send/apply frame init! (format-log-values log-values))
(parameterize ([current-audit-frame frame])
(begin0 (apply continue conn log-values)
(send frame clean-up!)))))))))
(set! insert-stage
(make-audit-stage
(lambda (continue conn struct)
(begin0
(continue conn struct)
(parameterize ([in-audit? #t])
(send (current-audit-frame) audit-insert! struct))))))
(set! update-stage
(make-audit-stage
(lambda (continue conn struct)
(parameterize ([in-audit? #t])
(send (current-audit-frame) audit-update! struct))
(continue conn struct))))
(set! delete-stage
(make-audit-stage
(lambda (continue conn struct)
(parameterize ([in-audit? #t])
(send (current-audit-frame) audit-delete! struct))
(continue conn struct))))
(parameterize ([in-audit? #t])
(unless (table-exists? entity:audit-entity)
(create-table entity:audit-entity))
(unless (table-exists? entity:audit-attribute)
(create-table entity:audit-attribute))
(unless (table-exists? entity:audit-transaction)
(create-table entity:audit-transaction))
(unless (table-exists? entity:audit-delta)
(create-table entity:audit-delta))
(send snooze set-transaction-pipeline! (cons transaction-stage (send snooze get-transaction-pipeline)))
(for-each (lambda (entity)
(set-entity-insert-pipeline! entity (append (entity-insert-pipeline entity) (list insert-stage)))
(set-entity-update-pipeline! entity (append (entity-update-pipeline entity) (list update-stage)))
(set-entity-delete-pipeline! entity (append (entity-delete-pipeline entity) (list delete-stage))))
entities)))
(define/public (current-audit-transaction)
(and (current-audit-frame)
(send (current-audit-frame) get-transaction)))
(define/public (clear!)
(parameterize ([in-audit? #t])
(for-each delete! (append (find-all (q:select #:from ENTITY))
(find-all (q:select #:from ATTR))
(find-all (q:select #:from TXN))
(find-all (q:select #:from DELTA))))
(send this clear-cache!)))
(define/public (drop!)
(drop-table entity:audit-entity)
(drop-table entity:audit-attribute)
(drop-table entity:audit-transaction)
(drop-table entity:audit-delta))
(define/public (format-log-values log-values)
log-values)
(define/public (audit-transaction-deltas txn)
(find-all (q:select #:what DELTA
#:from (q:inner TXN DELTA (q:= TXN-id DELTA-transaction-id))
#:where (q:= TXN-id (struct-id txn)))))
(define/public (audit-deltas->guids deltas)
(map (lambda (delta)
(define entity (id->entity (audit-delta-entity-id delta)))
(make-guid entity (audit-delta-struct-id delta)))
(sort (delete-duplicates
deltas
(lambda (a b)
(= (audit-delta-struct-id a)
(audit-delta-struct-id b))))
(lambda (a b)
(if (= (audit-delta-entity-id a) (audit-delta-entity-id b))
(< (audit-delta-struct-id a) (audit-delta-struct-id b))
(< (audit-delta-entity-id a) (audit-delta-entity-id b)))))))
(define/public audit-struct-history
(opt-lambda (guid txn [inclusive? #t])
(define entity (guid-entity guid))
(define entity-id (entity->id entity))
(define q:greater? (if inclusive? q:>= q:>))
(find-all (q:select #:what DELTA
#:from DELTA
#:where (q:and (q:= DELTA-entity-id entity-id)
(q:= DELTA-struct-id (guid-id guid))
(q:greater? DELTA-transaction-id (struct-id txn)))
#:order (list (q:desc DELTA-id))))))
(define/public audit-struct-transaction-history
(opt-lambda (guid txn [inclusive? #t])
(define entity (guid-entity guid))
(define entity-id (entity->id entity))
(define q:greater? (if inclusive? q:>= q:>))
(find-all (q:select #:what TXN
#:from (q:inner TXN DELTA (q:= TXN-id DELTA-transaction-id))
#:where (q:and (q:= DELTA-entity-id entity-id)
(q:= DELTA-struct-id (guid-id guid))
(q:greater? TXN-id (struct-id txn)))
#:order (list (q:asc DELTA-id))))))
(define/public (audit-struct-snapshot guid history)
(define struct (find-by-guid guid))
(foldl (cut revert-delta! guid <> <>)
struct
history))
(define/public (audit-transaction-affected txn0)
(define closed-txns (make-hash-table 'equal))
(define closed-guids (make-hash-table 'equal))
(define (close-txn! txn)
(hash-table-put! closed-txns txn txn))
(define (close-guid! guid txn)
(define old-txn (hash-table-get closed-guids guid #f))
(cond [(not old-txn)
(hash-table-put! closed-guids guid txn)]
[(< (struct-id txn) (struct-id old-txn))
(hash-table-put! closed-guids guid txn)]
[else (void)]))
(define (filter-open elts closed)
(filter (lambda (elt)
(not (hash-table-get closed elt #f)))
elts))
(let loop ([open (list txn0)])
(match open
[(list-rest (? audit-transaction? txn) tail)
(define deltas (audit-transaction-deltas txn))
(define guids (audit-deltas->guids deltas))
(close-txn! txn)
(loop (append tail (filter-open guids closed-guids)))]
[(list-rest (? guid? guid) tail)
(define txns (audit-struct-transaction-history guid txn0 #t))
(close-guid! guid (car txns))
(loop (append tail (filter-open txns closed-txns)))]
[(list) closed-guids])))
(define/public (audit-roll-back! affected . log-values)
(define transaction-ids
(map struct-id (hash-table-values affected)))
(define delta-groups
(let ([gen (g:find (q:select #:what DELTA
#:from (q:inner TXN DELTA (q:= TXN-id DELTA-transaction-id))
#:where (q:in TXN-id transaction-ids)
#:order (list (q:asc (q:attr DELTA 'id)))))])
(let loop ([k-group null] [k-all null])
(let ([next (gen)])
(cond [(g:end? next)
(if (null? k-group)
k-all
(cons k-group k-all))]
[(null? k-group)
(loop (cons next k-group) k-all)]
[(and (equal? (audit-delta-type next) (audit-delta-type (car k-group)))
(equal? (audit-delta-guid next) (audit-delta-guid (car k-group))))
(loop (cons next k-group) k-all)]
[else (loop (list next) (cons k-group k-all))])))))
(define working
(let ([ans (make-hash-table 'equal)])
(hash-table-for-each
affected
(lambda (guid txn)
(hash-table-put! ans guid (find-by-guid guid))))
ans))
(apply call-with-transaction
(lambda ()
(for-each (lambda (deltas)
(define guid (audit-delta-guid (car deltas)))
(define old-struct (hash-table-get working guid))
(define new-struct
(foldl (cut revert-delta! guid <> <>)
old-struct
deltas))
(case (audit-delta-type (car deltas))
[(I) (delete/id+revision! old-struct (list delete-stage))]
[(U) (update/id+revision! new-struct (list update-stage))]
[(D) (insert/id+revision! new-struct (list insert-stage))])
(hash-table-put! working guid new-struct))
delta-groups))
log-values))
(inspect #f)))
(define make-audit-trail
(case-lambda
[(snooze entity:audit-transaction entities)
(make-audit-trail audit-trail% snooze entity:audit-transaction entities)]
[(audit-trail% snooze entity:audit-transaction entities)
(define entity-cache
(new entity-cache%
[snooze snooze]))
(define attribute-cache
(new attribute-cache%
[snooze snooze]
[entity-cache entity-cache]))
(new audit-trail%
[snooze snooze]
[entity:audit-transaction entity:audit-transaction]
[entities entities]
[entity-cache entity-cache]
[attribute-cache attribute-cache])]))
(provide (all-from "attribute.ss")
(all-from "entity.ss")
(all-from "transaction.ss")
(all-from "delta.ss")
audit-trail<%>
audit-trail%)
(provide/contract
[struct audit-stage ([proc procedure?])]
[make-audit-trail (case-> (-> (is-a?/c snooze<%>)
entity?
(listof entity?)
(is-a?/c audit-trail%))
(-> (subclass?/c audit-trail%)
(is-a?/c snooze<%>)
entity?
(listof entity?)
(is-a?/c audit-trail%)))])