#lang scheme/base
(require mzlib/etc
scheme/class
scheme/contract
scheme/match
(only-in srfi/1/list
delete-duplicates
unzip2)
srfi/19/time
srfi/26/cut
(planet untyped/unlib:3/hash)
(planet untyped/unlib:3/parameter)
(planet untyped/unlib:3/pipeline)
"../snooze.ss"
"attribute.ss"
"delta.ss"
"entity.ss"
"frame.ss"
"transaction.ss")
(define-struct audit-stage (proc)
#:property prop:procedure 0)
(define audit-trail<%>
(interface (delta-api<%> entity-cache<%> attribute-cache<%>)
current-audit-transaction
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 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-alias TXN audit-transaction)
(field [transaction-stage #f])
(field [insert-stage #f])
(field [update-stage #f])
(field [delete-stage #f])
(init [(init-snooze snooze)])
(init [(init-entity-cache entity-cache)
(new entity-cache%
[snooze init-snooze])])
(init [(init-attribute-cache attribute-cache)
(new attribute-cache%
[snooze init-snooze]
[entity-cache init-entity-cache])])
(super-new [snooze init-snooze]
[entity-cache init-entity-cache]
[attribute-cache init-attribute-cache])
(unless (is-a? snooze snooze<%>)
(raise-type-error 'snooze "snooze<%>" snooze))
(for ([entity entities])
(when (memq entity (list entity:audit-attribute
entity:audit-entity
entity:audit-delta
entity:audit-transaction))
(error (format "~a cannot be audited" entity))))
(define-snooze-interface snooze)
(define/public (init!)
(set! transaction-stage
(make-audit-stage
(lambda (continue conn . metadata-args)
(if (in-audit?)
(apply continue conn metadata-args)
(parameterize ([in-audit? #t])
(let* ([frame (new audit-frame%
[trail this]
[snooze snooze]
[entity-cache entity-cache]
[attribute-cache attribute-cache])])
(send frame on-transaction-start)
(parameterize ([current-audit-frame frame])
(begin0 (apply continue conn metadata-args)
(send frame on-transaction-end
(and (pair? metadata-args)
(send/apply this make-metadata
(send frame get-transaction)
metadata-args)))))))))))
(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 (make-metadata txn . metadata-args)
#f)
(define/public (clear!)
(parameterize ([in-audit? #t])
(for-each delete! (append (find-all (sql:select #:from ENTITY))
(find-all (sql:select #:from ATTR))
(find-all (sql:select #:from TXN))
(find-all (sql: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 (sql:select #:what DELTA
#:from (sql:inner TXN DELTA (sql:= TXN-id DELTA-transaction-id))
#:where (sql:= 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 sql:greater? (if inclusive? sql:>= sql:>))
(find-all (sql:select #:what DELTA
#:from DELTA
#:where (sql:and (sql:= DELTA-entity-id entity-id)
(sql:= DELTA-struct-id (guid-id guid))
(sql:greater? DELTA-transaction-id (struct-id txn)))
#:order (list (sql: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 sql:greater? (if inclusive? sql:>= sql:>))
(find-all (sql:select #:what TXN
#:from (sql:inner TXN DELTA (sql:= TXN-id DELTA-transaction-id))
#:where (sql:and (sql:= DELTA-entity-id entity-id)
(sql:= DELTA-struct-id (guid-id guid))
(sql:greater? TXN-id (struct-id txn)))
#:order (list (sql: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))
(define closed-guids (make-hash))
(define (close-txn! txn)
(hash-set! closed-txns txn txn))
(define (close-guid! guid txn)
(define old-txn (hash-ref closed-guids guid #f))
(cond [(not old-txn)
(hash-set! closed-guids guid txn)]
[(< (struct-id txn) (struct-id old-txn))
(hash-set! closed-guids guid txn)]
[else (void)]))
(define (filter-open elts closed)
(filter (lambda (elt)
(not (hash-ref 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-values affected)))
(define delta-groups
(let ([gen (g:find (sql:select #:what DELTA
#:from (sql:inner TXN DELTA (sql:= TXN-id DELTA-transaction-id))
#:where (sql:in TXN-id transaction-ids)
#:order (list (sql:asc (sql: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)])
(hash-for-each
affected
(lambda (guid txn)
(hash-set! 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-ref 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-set! working guid new-struct))
delta-groups))
log-values))
(inspect #f)))
(provide (all-from-out "attribute.ss")
(all-from-out "entity.ss")
(all-from-out "transaction.ss")
(all-from-out "delta.ss"))
(provide audit-trail<%>
audit-trail%)