
When a tag is serializable but not `write'--`read' invariant, then it needs to be serialized and deserialized. Also, clarify and check in `tag?' that a tag should be serializable.
527 lines
19 KiB
Racket
527 lines
19 KiB
Racket
#lang racket/base
|
|
(require db
|
|
racket/format
|
|
racket/serialize
|
|
"main-doc.rkt")
|
|
|
|
(provide doc-db-available?
|
|
doc-db-clear-provides
|
|
doc-db-add-provides
|
|
doc-db-clear-dependencies
|
|
doc-db-add-dependencies
|
|
doc-db-clear-searches
|
|
doc-db-add-searches
|
|
doc-db-key->path
|
|
doc-db-check-duplicates
|
|
doc-db-check-unsatisfied
|
|
doc-db-get-dependencies
|
|
doc-db-file->connection
|
|
doc-db-disconnect
|
|
doc-db-clean-files
|
|
doc-db-init-pause
|
|
doc-db-pause
|
|
log-doc-db-info)
|
|
|
|
(define-logger doc-db)
|
|
|
|
(define (doc-db-available?)
|
|
(sqlite3-available?))
|
|
|
|
(define (doc-db-file->connection db-file)
|
|
(define exists? (file-exists? db-file))
|
|
(define db (sqlite3-connect #:database db-file
|
|
#:mode 'create
|
|
#:busy-retry-limit 0))
|
|
(unless exists?
|
|
(call-with-retry/transaction
|
|
'prepare-tables
|
|
db
|
|
#t
|
|
(lambda ()
|
|
(prepare-tables db))))
|
|
;; we don't need to survive a catastrophic failure:
|
|
(call-with-retry
|
|
'synchronous-off
|
|
db
|
|
(lambda ()
|
|
(query-exec db "pragma synchronous = off")))
|
|
db)
|
|
|
|
(define (doc-db-disconnect db)
|
|
(disconnect db))
|
|
|
|
(define select-pathid-vq
|
|
(virtual-statement "SELECT pathid FROM documented WHERE stag=$1"))
|
|
(define select-path-vq
|
|
(virtual-statement "SELECT atmain, path FROM pathids WHERE pathid=$1"))
|
|
(define select-other-path-vq
|
|
(virtual-statement "SELECT atmain, path FROM other.pathids WHERE pathid=$1"))
|
|
|
|
(define (doc-db-init-pause)
|
|
0.01)
|
|
(define (doc-db-pause who pause)
|
|
(log-doc-db-info "database locked at ~a; now waiting ~a seconds"
|
|
who
|
|
pause)
|
|
(sleep pause)
|
|
(min 2 (* 2 pause)))
|
|
|
|
(define (call-with-database who db-file proc
|
|
#:write? [write? #f]
|
|
#:setup [setup void]
|
|
#:teardown [teardown void])
|
|
(define db (if (connection? db-file)
|
|
db-file
|
|
(doc-db-file->connection db-file)))
|
|
(setup db)
|
|
(begin0
|
|
(call-with-retry/transaction
|
|
who
|
|
db
|
|
write?
|
|
(lambda () (proc db)))
|
|
(teardown db)
|
|
(unless (connection? db-file)
|
|
(disconnect db))))
|
|
|
|
(define (doc-db-key->path db-file key
|
|
#:main-doc-relative-ok? [main-doc-relative-ok? #f])
|
|
(call-with-database
|
|
'doc-db-key->path
|
|
db-file
|
|
(lambda (db)
|
|
(define rows (query-rows db select-pathid-vq
|
|
(->string key)))
|
|
(define pathid (and (pair? rows)
|
|
(vector-ref (car rows) 0)))
|
|
(and pathid
|
|
(pathid->filename db pathid #f main-doc-relative-ok?)))))
|
|
|
|
|
|
(define (add who db-file elems filename callback)
|
|
(call-with-database
|
|
who
|
|
db-file
|
|
#:write? #t
|
|
(lambda (db)
|
|
(prepare-tables db)
|
|
(define pathid (filename->pathid db filename))
|
|
(for ([p (in-list elems)])
|
|
(define stag (->string p))
|
|
(callback db stag pathid)))))
|
|
|
|
(define (clear who db-file filename statement)
|
|
(call-with-database
|
|
who
|
|
db-file
|
|
#:write? #t
|
|
(lambda (db)
|
|
(prepare-tables db)
|
|
(define pathid (filename->pathid db filename))
|
|
(query-exec db statement
|
|
pathid))))
|
|
|
|
(define (doc-db-add-provides db-file provides filename)
|
|
(add 'doc-db-add-provides
|
|
db-file provides filename
|
|
(lambda (db stag pathid)
|
|
(query-exec db "INSERT INTO documented VALUES ($1, $2)"
|
|
stag
|
|
pathid))))
|
|
|
|
|
|
(define (doc-db-clear-provides db-file filename)
|
|
(clear 'doc-db-clear-provides
|
|
db-file filename
|
|
"DELETE FROM documented WHERE pathid=$1"))
|
|
|
|
(define (doc-db-add-dependencies db-file depends filename)
|
|
(add 'doc-db-add-dependencies
|
|
db-file depends filename
|
|
(lambda (db stag pathid)
|
|
(query-exec db "INSERT INTO dependencies VALUES ($1, $2)"
|
|
pathid
|
|
stag))))
|
|
|
|
|
|
(define (doc-db-clear-dependencies db-file filename)
|
|
(clear 'doc-db-clear-dependencies
|
|
db-file filename
|
|
"DELETE FROM dependencies WHERE pathid=$1"))
|
|
|
|
(define (doc-db-add-searches db-file searches filename)
|
|
(call-with-database
|
|
'doc-db-add-searches
|
|
db-file
|
|
#:write? #t
|
|
(lambda (db)
|
|
(prepare-tables db)
|
|
(define pathid (filename->pathid db filename))
|
|
(for ([(sk s) (in-hash searches)]
|
|
[setid (in-naturals)])
|
|
(query-exec db "INSERT INTO searchSets VALUES ($1, $2, $3)"
|
|
pathid
|
|
setid
|
|
(->string sk))
|
|
(for ([k (in-hash-keys s)])
|
|
(define stag (->string k))
|
|
(query-exec db "INSERT INTO searches VALUES ($1, $2, $3)"
|
|
pathid
|
|
setid
|
|
stag))))))
|
|
|
|
(define (doc-db-clear-searches db-file filename)
|
|
(call-with-database
|
|
'doc-db-clear-searches
|
|
db-file
|
|
#:write? #t
|
|
(lambda (db)
|
|
(prepare-tables db)
|
|
(define pathid (filename->pathid db filename))
|
|
(query-exec db "DELETE FROM searchSets WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "DELETE FROM searches WHERE pathid=$1"
|
|
pathid))))
|
|
|
|
(define (maybe-attach attach-db-path)
|
|
(lambda (db)
|
|
(when attach-db-path
|
|
(attach-db db attach-db-path))))
|
|
(define (maybe-detach attach-db-path)
|
|
(lambda (db)
|
|
(when attach-db-path
|
|
(detach-db db attach-db-path))))
|
|
|
|
(define (doc-db-check-duplicates db-file
|
|
#:attach [attach-db-path #f]
|
|
#:main-doc-relative-ok? [main-doc-relative-ok? #f])
|
|
(call-with-database
|
|
'doc-db-check-duplicates
|
|
db-file
|
|
#:setup (maybe-attach attach-db-path)
|
|
#:teardown (maybe-detach attach-db-path)
|
|
(lambda (db)
|
|
(define rows
|
|
(append
|
|
(query-rows db (~a "SELECT stag"
|
|
" FROM documented"
|
|
" GROUP BY stag"
|
|
" HAVING COUNT(pathid) > 1"))
|
|
(if attach-db-path
|
|
;; duplicates across tables:
|
|
(query-rows db (~a "SELECT D.stag"
|
|
" FROM documented D, other.documented OD"
|
|
" WHERE D.stag = OD.stag"
|
|
" GROUP BY D.stag"))
|
|
null)))
|
|
(for/list ([row (in-list rows)])
|
|
(define stag (vector-ref row 0))
|
|
(define pathid-rows (query-rows db (~a "SELECT pathid"
|
|
" FROM documented"
|
|
" WHERE stag=$1")
|
|
stag))
|
|
(define other-pathid-rows
|
|
(if attach-db-path
|
|
(query-rows db (~a "SELECT pathid"
|
|
" FROM other.documented"
|
|
" WHERE stag=$1")
|
|
stag)
|
|
null))
|
|
(cons (<-string (if (bytes? stag)
|
|
(open-input-bytes stag)
|
|
(open-input-string stag)))
|
|
(append
|
|
(for/list ([pathid-row (in-list pathid-rows)])
|
|
(pathid->filename db (vector-ref pathid-row 0) #f main-doc-relative-ok?))
|
|
(for/list ([pathid-row (in-list other-pathid-rows)])
|
|
(pathid->filename db (vector-ref pathid-row 0) #t main-doc-relative-ok?))))))))
|
|
|
|
(define (doc-db-check-unsatisfied filename db-file
|
|
#:attach [attach-db-path #f])
|
|
(call-with-database
|
|
'doc-db-check-unsatisfied
|
|
db-file
|
|
#:setup (maybe-attach attach-db-path)
|
|
#:teardown (maybe-detach attach-db-path)
|
|
(lambda (db)
|
|
(define pathid (filename->pathid db filename))
|
|
;; Items with no `searches' entries:
|
|
(define rows
|
|
(query-rows db (~a "SELECT P.stag "
|
|
" FROM dependencies P"
|
|
" LEFT OUTER JOIN documented D ON D.stag = P.stag"
|
|
" LEFT OUTER JOIN searches S ON S.stag = P.stag"
|
|
(if attach-db-path
|
|
(~a " LEFT OUTER JOIN other.documented OD ON OD.stag = P.stag"
|
|
" LEFT OUTER JOIN other.searches OS ON OS.stag = P.stag")
|
|
"")
|
|
" WHERE P.pathid = $1"
|
|
" AND D.stag IS NULL"
|
|
" AND S.stag is NULL"
|
|
(if attach-db-path
|
|
(~a " AND OD.stag IS NULL"
|
|
" AND OS.stag is NULL")
|
|
""))
|
|
pathid))
|
|
;; Items with `searches' entries, but no documentation:
|
|
(define more-rows
|
|
(query-rows db (~a "SELECT SS.stag "
|
|
" FROM searchSets SS"
|
|
" WHERE SS.pathid = $1"
|
|
" AND NOT EXISTS"
|
|
" (SELECT S.stag"
|
|
" FROM documented D, searches S"
|
|
" WHERE D.stag = S.stag"
|
|
" AND S.setid = SS.setid"
|
|
" AND S.pathid = SS.pathid)"
|
|
(if attach-db-path
|
|
(~a " AND NOT EXISTS"
|
|
" (SELECT S.stag"
|
|
" FROM other.documented OD, searches S"
|
|
" WHERE OD.stag = S.stag"
|
|
" AND S.setid = SS.setid"
|
|
" AND S.pathid = SS.pathid)")
|
|
"")
|
|
" GROUP BY SS.stag")
|
|
pathid))
|
|
(map (lambda (s)
|
|
(<-string (open-input-string (vector-ref s 0))))
|
|
(append
|
|
rows
|
|
more-rows)))))
|
|
|
|
(define (attach-db db attach-db-path)
|
|
(query-exec db "ATTACH $1 AS other"
|
|
(path->bytes (cleanse-path
|
|
(path->complete-path attach-db-path)))))
|
|
(define (detach-db db attach-db-path)
|
|
(query-exec db "DETACH other"))
|
|
|
|
|
|
(define (doc-db-get-dependencies filename db-file
|
|
#:attach [attach-db-path #f]
|
|
#:main-doc-relative-ok? [main-doc-relative-ok? #f])
|
|
(call-with-database
|
|
'doc-db-get-dependencies
|
|
db-file
|
|
#:setup (maybe-attach attach-db-path)
|
|
#:teardown (maybe-detach attach-db-path)
|
|
(lambda (db)
|
|
(define pathid (filename->pathid db filename))
|
|
(define ((rows->paths in-other?) rows)
|
|
(for/list ([row (in-list rows)])
|
|
(pathid->filename db (vector-ref row 0) in-other? main-doc-relative-ok?)))
|
|
(append
|
|
((rows->paths #f)
|
|
(query-rows db (~a "SELECT D.pathid "
|
|
" FROM dependencies P, documented D"
|
|
" WHERE P.pathid = $1"
|
|
" AND D.stag = P.stag"
|
|
" GROUP BY D.pathid")
|
|
pathid)))
|
|
(if attach-db-path
|
|
((rows->paths #t)
|
|
(query-rows db (~a "SELECT D.pathid "
|
|
" FROM dependencies P, other.documented D"
|
|
" WHERE P.pathid = $1"
|
|
" AND D.stag = P.stag"
|
|
" GROUP BY D.pathid")
|
|
pathid))
|
|
null))))
|
|
|
|
(define (doc-db-clean-files db-file ok-files)
|
|
(call-with-database
|
|
'doc-db-clean-files
|
|
db-file
|
|
#:write? #t
|
|
(lambda (db)
|
|
(prepare-tables db)
|
|
(define rows (query-rows db "SELECT atmain, path, pathid FROM pathids"))
|
|
(for ([row (in-list rows)])
|
|
(define bstr (vector-ref row 1))
|
|
(define path (cond
|
|
[(equal? "y" (vector-ref row 0))
|
|
(main-doc-relative->path
|
|
(cons 'doc (or (hash-ref reader-cache bstr #f)
|
|
(let ([v (<-string (open-input-bytes bstr))])
|
|
(hash-set! reader-cache bstr v)
|
|
v))))]
|
|
[(bytes? bstr)
|
|
(bytes->path bstr)]
|
|
[else ; "placeholder"
|
|
#f]))
|
|
(unless (or (not path)
|
|
(hash-ref ok-files path #f))
|
|
(define pathid (vector-ref row 2))
|
|
(query-exec db "DELETE FROM documented WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "DELETE FROM searches WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "DELETE FROM searchSets WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "DELETE FROM dependencies WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "DELETE FROM pathids WHERE pathid=$1"
|
|
pathid)
|
|
(query-exec db "INSERT INTO pathids VALUES ($1, 'n', 'placeholder')"
|
|
pathid))))))
|
|
|
|
|
|
(define (filename->pathid db filename)
|
|
(define filename* (path->main-doc-relative filename))
|
|
(define filename-bytes (cond
|
|
[(pair? filename*)
|
|
(string->bytes/utf-8 (->string (cdr filename*)))]
|
|
[(path? filename*)
|
|
(path->bytes filename*)]
|
|
[else (path->bytes (string->path filename*))]))
|
|
(define id (query-maybe-row db (~a "SELECT pathid FROM pathids"
|
|
" WHERE atmain=$1 AND path=$2")
|
|
(if (pair? filename*) "y" "n")
|
|
filename-bytes))
|
|
(cond
|
|
[(not id)
|
|
(define num (vector-ref (query-row db "SELECT COUNT(pathid) FROM pathids") 0))
|
|
(query-exec db "INSERT INTO pathids VALUES ($1, $2, $3)"
|
|
(add1 num)
|
|
(if (pair? filename*) "y" "n")
|
|
filename-bytes)
|
|
(add1 num)]
|
|
[else (vector-ref id 0)]))
|
|
|
|
(define reader-cache (make-weak-hash))
|
|
|
|
(define (pathid->filename db pathid in-other? main-doc-relative-ok?)
|
|
(define row (query-maybe-row db
|
|
(if in-other?
|
|
select-other-path-vq
|
|
select-path-vq)
|
|
pathid))
|
|
(and row
|
|
(let ([path (vector-ref row 1)])
|
|
(if (equal? "y" (vector-ref row 0))
|
|
((if main-doc-relative-ok? values main-doc-relative->path)
|
|
(cons 'doc (or (hash-ref reader-cache path #f)
|
|
(let ([v (<-string (open-input-bytes path))])
|
|
(hash-set! reader-cache path v)
|
|
v))))
|
|
(bytes->path path)))))
|
|
|
|
(define (prepare-tables db)
|
|
(when (null?
|
|
(query-rows db (~a "SELECT name FROM sqlite_master"
|
|
" WHERE type='table' AND name='documented'")))
|
|
(query-exec db (~a "CREATE TABLE documented "
|
|
"(stag VARCHAR(256),"
|
|
" pathid SMALLINT,"
|
|
" UNIQUE (stag, pathid))"))
|
|
(query-exec db (~a "CREATE INDEX documentedStags "
|
|
"on documented (stag)"))
|
|
(query-exec db (~a "CREATE TABLE dependencies "
|
|
"(pathid SMALLINT,"
|
|
" stag VARCHAR(256))"))
|
|
(query-exec db (~a "CREATE TABLE searchSets "
|
|
"(pathid SMALLINT, "
|
|
" setid SMALLINT, "
|
|
" stag VARCHAR(256),"
|
|
" PRIMARY KEY (pathid, setid))"))
|
|
(query-exec db (~a "CREATE TABLE searches "
|
|
"(pathid SMALLINT,"
|
|
" setid SMALLINT, "
|
|
" stag VARCHAR(256))"))
|
|
(query-exec db (~a "CREATE TABLE pathids "
|
|
"(pathid SMALLINT,"
|
|
" atmain CHAR(1),"
|
|
" path VARCHAR(1024),"
|
|
" PRIMARY KEY (pathid))"))
|
|
(query-exec db (~a "CREATE INDEX dependenciesPath "
|
|
"on dependencies (pathid)"))
|
|
(query-exec db (~a "CREATE INDEX searchSetsPath "
|
|
"on searchSets (pathid)"))
|
|
(query-exec db (~a "CREATE INDEX searchesTag "
|
|
"on searches (stag)"))
|
|
(query-exec db (~a "CREATE INDEX searchesPathId "
|
|
"on searches (pathid, setid)"))))
|
|
|
|
(define (exn:fail:retry? v)
|
|
(and (exn:fail:sql? v)
|
|
(let ([s (exn:fail:sql-sqlstate v)])
|
|
(or (eq? s 'busy)
|
|
(and (string? s) (regexp-match? s #rx"^40...$"))))))
|
|
|
|
(define (call-with-lock-handler handler thunk)
|
|
(with-handlers* ([exn:fail:retry?
|
|
(lambda (exn) (handler))])
|
|
(thunk)))
|
|
|
|
(define (call-with-retry/transaction who db write? thunk)
|
|
(let loop ([pause (doc-db-init-pause)])
|
|
((let/ec esc
|
|
(define at-commit? #f)
|
|
(define success? #f)
|
|
(dynamic-wind
|
|
(lambda ()
|
|
(call-with-lock-handler
|
|
(lambda () (esc (lambda ()
|
|
(loop (doc-db-pause `(start ,who) pause)))))
|
|
(lambda () (start-transaction db #:option (if write? 'immediate #f)))))
|
|
(lambda ()
|
|
(call-with-lock-handler
|
|
(lambda () (esc (lambda ()
|
|
(loop (doc-db-pause `(,(if at-commit? 'commit 'query)
|
|
,who)
|
|
pause)))))
|
|
(lambda ()
|
|
(define l (call-with-values thunk list))
|
|
(set! at-commit? #t)
|
|
(commit-transaction db)
|
|
(set! success? #t)
|
|
(lambda () (apply values l)))))
|
|
(lambda ()
|
|
(unless success?
|
|
(rollback db 1))))))))
|
|
|
|
(define (rollback db count)
|
|
(when (in-transaction? db)
|
|
(with-handlers* ([exn:fail:retry?
|
|
(lambda (exn)
|
|
(log-doc-db-info "database locked on rollback; tried ~a times so far"
|
|
count)
|
|
(rollback db (add1 count)))])
|
|
(rollback-transaction db))))
|
|
|
|
(define (call-with-retry who db thunk)
|
|
(let loop ([pause (doc-db-init-pause)])
|
|
(call-with-lock-handler
|
|
(lambda () (loop (doc-db-pause `(query ,who) pause)))
|
|
thunk)))
|
|
|
|
(define (readable? s)
|
|
(or (string? s)
|
|
(bytes? s)
|
|
(symbol? s)
|
|
(number? s)
|
|
(boolean? s)
|
|
(and (list? s) (andmap readable? s))
|
|
(and (vector? s) (for/and ([v (in-vector s)]) (readable? s)))))
|
|
|
|
(define (->string v)
|
|
(if (and (not (box? v))
|
|
(readable? v))
|
|
(~s v)
|
|
(~s (box (serialize v)))))
|
|
|
|
(define (<-string p)
|
|
(with-handlers ([exn:fail:read?
|
|
(lambda (exn)
|
|
(error 'read
|
|
(~a "error unmarshaling from database\n"
|
|
" original error: ~e")
|
|
(exn-message exn)))])
|
|
(define v (read p))
|
|
(if (box? v)
|
|
;; A box indicates serialization:
|
|
(deserialize (unbox v))
|
|
;; Normal value:
|
|
v)))
|