racket/collects/setup/doc-db.rkt
Matthew Flatt 6eef00a312 raco setup: fix problem with doc index database
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.
2012-12-13 15:45:48 -07:00

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)))