Overview
Comment: | Implement basic SQLite caching (closes [ccce11c15]) |
---|---|
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
0b455f1f02d3717373f5a401b2e7c5ec |
User & Date: | joel on 2018-08-26 18:13:23 |
Other Links: | manifest | tags |
Context
2018-09-02
| ||
21:04 | Add Flammarion engraving to home.wiki check-in: 4ec64ff2 user: joel tags: trunk | |
2018-08-26
| ||
18:13 | Implement basic SQLite caching (closes [ccce11c15]) check-in: 0b455f1f user: joel tags: trunk | |
2018-08-22
| ||
02:45 | Correct unit test in dates.rkt check-in: 6034c1b0 user: joel tags: trunk | |
Changes
Added crystalize.rkt version [c9502c11].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | #lang pollen/mode racket/base ;; Copyright (c) 2018 Joel Dueck. ;; ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. ;; A copy of the License is included with this source code, in the ;; file "LICENSE.txt". ;; You may also obtain a copy of the License at ;; ;; http://www.apache.org/licenses/LICENSE-2.0 ;; ;; Unless required by applicable law or agreed to in writing, software ;; distributed under the License is distributed on an "AS IS" BASIS, ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ;; See the License for the specific language governing permissions and ;; limitations under the License. ;; ;; Author contact information: ;; joel@jdueck.net ;; https://joeldueck.com ;; ------------------------------------------------------------------------- ;; Provides functions for fast preserving and fetching of article/series data. ;; → Docs and metas go in (saved to SQLite database) ;; HTML comes out → ;; Calling sites have no notion of the database or schema. ;; The functions provided by sqlite-tools.rkt are not safe for user-provided ;; data; a maliciously crafted input could bomb the database. This is acceptable ;; since the database is merely a disposable cache, and since all the input ;; will be coming from me. (require pollen/setup pollen/core pollen/template pollen/pagetree racket/string "sqlite-tools.rkt" "template-html.rkt" "dates.rkt") ;; ~~~ Provides ~~~ (provide spell-of-summoning! crystalize-article!) ;; ~~~ Private use ~~~ (define DBFILE (build-path (current-project-root) "vitreous.sqlite")) (define table_articles-fields '(pagenode title published updated doc_html author conceal series_pagenode noun_singular note_count)) (define table_notes-fields '(pagenode note-id heading author date note_html)) (define table_series-fields '(pagenode title published noun_plural noun_singular)) (define table_articles (make-table-schema "articles" table_articles-fields)) (define table_notes (make-table-schema "notes" table_notes-fields #:primary-key-cols '(pagenode note-id))) (define table_series (make-table-schema "series" table_series-fields)) (define (optional-meta m) (or (select-from-metas m (current-metas)) "")) (define (series-noun) (define series-pagenode (->pagenode (or (select-from-metas 'series (current-metas)) ""))) (case series-pagenode ['|| ""] ; no series specified [else (or (select-from-metas 'noun-singular series-pagenode) "")])) ;; ~~~ Provided functions: Initializing; Saving posts and notes ;; Initialize the database connection, creating the database if it doesn’t ;; exist, and executing the table schema queries ;; (define (spell-of-summoning!) (init-db! DBFILE table_articles table_notes table_series)) ;; Save an article (using current-doc and current-metas) and its notes (if any) ;; to the database, and return the rendered HTML. ;; (define (crystalize-article! pagenode doc) (define header (html-article-header)) (define footer (html-article-footer)) (define body (->html (cdr doc))) ;; TK: store notes separately (define saving-query (make-insert/replace-query 'articles table_articles-fields)) (query! saving-query (symbol->string pagenode) (optional-meta 'title) (select-from-metas 'published (current-metas)) (optional-meta 'updated) (string-append header body footer) (optional-meta 'author) (optional-meta 'conceal) (optional-meta 'series) (series-noun) 0) ; note_count `(@ ,header ,body ,footer)) |
Modified pollen.rkt from [08e62a8c] to [8a4112bd].
︙ | ︙ | |||
28 29 30 31 32 33 34 | syntax/parse pollen/setup)) (require pollen/tag pollen/setup racket/function "tags-html.rkt" | | > | | > | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | syntax/parse pollen/setup)) (require pollen/tag pollen/setup racket/function "tags-html.rkt" "template-html.rkt" "crystalize.rkt") (provide (all-defined-out) (all-from-out "crystalize.rkt" "template-html.rkt")) (module setup racket/base (require syntax/modresolve) (provide (all-defined-out)) (define poly-targets '(html)) (define cache-watchlist (map resolve-module-path '("tags-html.rkt" "template-html.rkt" "dates.rkt" "crystalize.rkt")))) ;; Macro for defining tag functions that automatically branch based on the ;; current output format and the list of poly-targets in the setup module. ;; (define-syntax (poly-branch-tag stx) (syntax-parse stx [(_ TAG:id) |
︙ | ︙ |
Added sqlite-tools.rkt version [22818cf3].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | #lang racket/base ;; Copyright (c) 2018 Joel Dueck. ;; ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. ;; A copy of the License is included with this source code, in the ;; file "LICENSE.txt". ;; You may also obtain a copy of the License at ;; ;; http://www.apache.org/licenses/LICENSE-2.0 ;; ;; Unless required by applicable law or agreed to in writing, software ;; distributed under the License is distributed on an "AS IS" BASIS, ;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ;; See the License for the specific language governing permissions and ;; limitations under the License. ;; ;; Author contact information: ;; joel@jdueck.net ;; https://joeldueck.com ;; ------------------------------------------------------------------------- ;; Provides a very light set of utility functions for a SQLite database. ;; These functions are completely agnostic as to the database schema. ;; These functions are NOT SAFE for use with data provided by untrusted users! (require db/sqlite3 db/base racket/list racket/match racket/function racket/contract sugar/coerce) (module+ test (require rackunit)) (provide sqltools:dbc sqltools:log-queries?) (provide (contract-out ;; Utility functions [log-query (string? . -> . void?)] [vector->hash (vector? (listof symbolish?) . -> . hash?)] [backtick (stringish? . -> . string?)] [list->sql-fields ((listof stringish?) . -> . string?)] [list->sql-parameters ((listof any/c) . -> . string?)] ;; Simple SQL makers [make-table-schema ((string? (listof stringish?)) (#:primary-key-cols (listof stringish?)) . ->* . string?)] [make-insert/replace-query (stringish? (listof stringish?) . -> . string?)] [make-select-query (stringish? (listof stringish?) #:where string? . -> . string?)] ;; Database operations [init-db! ((pathish?) #:rest (listof string?) . ->* . void?)] [query! ((string?) #:rest (listof any/c) . ->* . void?)] [select-rows! (case-> (stringish? (listof stringish?) any/c . -> . (or/c empty? hash?)) (string? (listof stringish?) . -> . (or/c empty? hash?)))])) ;; ~~~ Private use ~~~ (define uninitialized-connection "No DB connection!") (define (weave xs ys) (for/fold [(woven null) #:result (reverse woven)] ([x (in-list xs)] [y (in-list ys)]) (cons y (cons x woven)))) ;; ~~~ Provided parameters ~~~ (define sqltools:dbc (make-parameter uninitialized-connection)) (define sqltools:log-queries? (make-parameter #f)) ;; ~~~ Provided utility functions ~~~ (define (backtick str) (format "`~a`" str)) (define (list->sql-fields fields) (apply string-append (add-between (map backtick fields) ", "))) (define (list->sql-parameters fields) (apply string-append (add-between (map (λ(x) (format "?~a" (add1 x))) (range (length fields))) ", "))) ;; TESTING: utility functions… (module+ test (check-equal? (backtick "field") "`field`") (check-equal? (list->sql-fields '("f1" "f2" "f3")) "`f1`, `f2`, `f3`") (check-equal? (list->sql-fields '(f1 f2 f3)) "`f1`, `f2`, `f3`") ; Can use symbols too (check-equal? (list->sql-parameters '("name" "rank" "serial")) "?1, ?2, ?3") (check-equal? (list->sql-parameters '(name rank serial)) "?1, ?2, ?3") (check-equal? (weave '(x y z) '(1 2 3)) '(x 1 y 2 z 3))) ;; ~~~ Public functions ~~~ ;; Prints to stdout if logging is on (define (log-query q) (unless (not (sqltools:log-queries?)) (println q))) ;; Using a list of field names, convert a vector into a hash that uses the ;; field names (in symbol form) for keys. ;; Racket’s db functions all return vectors; hashes are much easier to use. ;; If fields and v are not equal in length, the unpairable elements are omitted ;; from the hash! (define (vector->hash v fields) (cond [(zero? (vector-length v)) null] [else (let ([keys (map ->symbol fields)] [vals (vector->list v)]) (apply hash (weave keys vals)))])) ;; TESTING: vector->hash... (module+ test (let ([test-row '#("Joe" "PFC" 123)] [test-cols-SYMBOL '(name rank serial)] [test-cols-STRING '("name" "rank" "serial")] [desired-result '#hash((serial . 123) (rank . "PFC") (name . "Joe"))]) (check-equal? (vector->hash test-row test-cols-SYMBOL) desired-result) (check-equal? (vector->hash test-row test-cols-STRING) desired-result)) ;; Behavior when v and fields are unequal in length: (check-equal? (vector->hash '#("foo" "bar") '(a b c)) '#hash((a . "foo") (b . "bar"))) (check-equal? (vector->hash '#("foo" "bar" "baz") '(a b)) '#hash((a . "foo") (b . "bar")))) ;; Create a simple table schema from a list of fields, optionally specifying ;; primary key (define (make-table-schema tablename fields #:primary-key-cols [primary-cols '()]) (define primary-key (format "PRIMARY KEY (~a)" (list->sql-fields (if (empty? primary-cols) (list (first fields)) primary-cols)))) (format "CREATE TABLE IF NOT EXISTS `~a` (~a, ~a);" tablename (list->sql-fields fields) primary-key)) ;; Create a query that inserts a row if it doesn’t exist (based on the first ;; column only), or updates it if it does. The returned query is parameterized, ;; and must be used with a list of values equal in length to the number of ;; fields given. (define (make-insert/replace-query tablename fields) (format "INSERT OR REPLACE INTO `~a` (`rowid`, ~a) values ((SELECT `rowid` FROM `~a` WHERE `~a`= ?1), ~a)" tablename (list->sql-fields fields) tablename (first fields) (list->sql-parameters fields))) ;; Simple row selection (define (make-select-query table fields #:where where-clause) (format "SELECT ~a FROM `~a` WHERE ~a" (list->sql-fields fields) table where-clause)) ;; TESTING: SQL query makers... (module+ test (check-equal? (make-table-schema 'posts '(title date)) "CREATE TABLE IF NOT EXISTS `posts` (`title`, `date`, PRIMARY KEY (`title`));") (check-equal? (make-table-schema "posts" '("title" "date")) "CREATE TABLE IF NOT EXISTS `posts` (`title`, `date`, PRIMARY KEY (`title`));") (check-equal? (make-table-schema 'posts '(title date) #:primary-key-cols '(author date)) "CREATE TABLE IF NOT EXISTS `posts` (`title`, `date`, PRIMARY KEY (`author`, `date`));") (check-equal? (make-insert/replace-query 'posts '(author title)) (string-append "INSERT OR REPLACE INTO `posts` (`rowid`, `author`, `title`) " "values ((SELECT `rowid` FROM `posts` WHERE `author`= ?1), ?1, ?2)")) (check-equal? (make-select-query 'posts '(author title) #:where 1) "SELECT `author`, `title` FROM `posts` WHERE 1")) (define (good-connection?) (and (connection? (sqltools:dbc)) (connected? (sqltools:dbc)))) ;; Initialize the database connection, creating the database if it does not yet exist ;; and running any provided queries (e.g., "CREATE TABLE IF NOT EXISTS...") (define (init-db! filename . qs) (sqltools:dbc (sqlite3-connect #:database filename #:mode 'create)) (unless (empty? qs) (for ([q (in-list qs)]) (query! q)))) ;; Run a query with logging (if enabled) and return the result (define (query! q . parameters) (unless (good-connection?) (error "(query!) DB not connected")) (log-query q) (apply query-exec (sqltools:dbc) q parameters)) ;; Run a SELECT query, return a hash with field names as keys (define select-rows! (case-lambda ;; Use arbitrary query [(query fieldnames) (unless (good-connection?) (error "(select-rows!) DB not connected")) (log-query query) (define result (query-rows (sqltools:dbc) query)) (map (curryr vector->hash fieldnames) result)] ;; Use a simple SELECT FROM WHERE template [(table fields where-clause) (unless (good-connection?) (error "(select-rows!) DB not connected")) (define query (make-select-query table fields #:where where-clause)) (log-query query) (define result (query-rows (sqltools:dbc) query)) (map (curryr vector->hash fields) result)])) ;; TESTING: database connection state and queries (module+ test (define TESTDB "SQLITE-TOOLS-TEST.sqlite") ;; Check that things start out uninitialized and that queries don’t work (check-equal? (sqltools:dbc) uninitialized-connection) (check-false (file-exists? TESTDB)) (check-exn exn:fail? (lambda () (query! "-- nothing"))) (check-exn exn:fail? (lambda () (select-rows! 'posts '(title) 1))) ;; Initialize db connection, create file with no schema (test-begin (check-equal? (init-db! TESTDB) (void)) (check-true (file-exists? TESTDB)) (delete-file TESTDB)) ;; Initialize new db/connection, create file with schema, check that ;; simple queries return expected results (test-begin (check-equal? (init-db! TESTDB (make-table-schema 'posts '(title date))) (void)) (check-true (file-exists? TESTDB)) (check-equal? (select-rows! (make-select-query 'posts '(title date) #:where 1) '(title date)) null) (check-equal? (query! (make-insert/replace-query 'posts '(title date)) "Hello" "2018-08-10") (void)) (check-equal? (select-rows! 'posts '(title date) "`date`='2018-08-10'") '(#hash((title . "Hello") (date . "2018-08-10"))))) ;; Clean up (disconnect (sqltools:dbc)) (sqltools:dbc uninitialized-connection) (delete-file TESTDB)) |
Modified template-html.rkt from [45af1f57] to [c19af359].
︙ | ︙ | |||
23 24 25 26 27 28 29 | ;; Provides functions for displaying content in HTML templates. (require pollen/core "dates.rkt") (provide (all-defined-out)) | | | | | | | | | > | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | ;; Provides functions for displaying content in HTML templates. (require pollen/core "dates.rkt") (provide (all-defined-out)) (define (html-head [title #f]) ◊@{<head> <title>The Local Yarn◊when/splice[title]{: ◊title}</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="/web-extra/martin.css"> </head>}) (define (html-page-top) ◊@{<body><main> <a href="/"><header> <img src="/web-extra/logo.png" height="103" width="129" class="logo"> <h1>The Local Yarn</h1> </header></a>}) (define (html-article-header) (define title (select-from-metas 'title (current-metas))) (define published (select-from-metas 'published (current-metas))) (cond [title ◊string-append{<article class="with-title hentry"> <h1 class="entry-title">◊|title|</h1> <p class="time"><a href="#" class="rel-bookmark"> <time datetime="◊published" class="published">◊ymd->english[published]</time> </a></p> <section class="entry-content">}] [else ◊string-append{<article class="no-title hentry"> <h1><a href="#" class="rel-bookmark"> <time datetime="◊published" class="entry-title">◊ymd->english[published]</time> </a></h1> <section class="entry-content">}])) (define (html-article-footer) ◊string-append{</section> <footer class="article-info"><span class="x">(</span>Part of ‘Talking About Poetry’. Once I threw a mudball at a birdhouse. I’m not exactly proud of it, though.<span class="x">)</span></footer> </article>}) (define (html-page-bottom) ◊@{<footer>By Joel Dueck</footer> </main></body>}) |
Modified template.html.p from [be27a755] to [e6133aaa].
1 2 | <!DOCTYPE html> <html> | | | | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | <!DOCTYPE html> <html> ◊html-head[(select-from-metas 'title here)] ◊html-page-top[] ◊spell-of-summoning![] ◊crystalize-article![here doc] ◊html-page-bottom[] </html> |