◊(Local Yarn Code "cache.scrbl at [c8598882]")

File code-docs/cache.scrbl artifact 379418fa part of check-in c8598882


#lang scribble/manual

@; SPDX-License-Identifier: BlueOak-1.0.0
@; This file is licensed under the Blue Oak Model License 1.0.0.

@(require "scribble-helpers.rkt" scribble/example)

@(require (for-label deta
                     db
                     racket/base
                     racket/contract
                     sugar/coerce
                     pollen/template
                     "../dust.rkt"
                     "../crystalize.rkt"
                     "../cache.rkt"))

@(define example-eval (make-base-eval))
@(example-eval '(require "cache.rkt" txexpr))

@title[#:tag "cache-rkt"]{Cache}

@defmodule["cache.rkt" #:packages ()]

In this project there are several places – the blog, the footer on each page, the RSS feed, series
pages — where data from an amorphous group of Pollen documents is needed. This is what the cache is
for.

This module defines and provides the schema and database connection to the SQLite cache, and some
functions for retrieving records from the cache. Use these when you need quick access to pre-cooked
HTML.

@section{Cache database}

@defthing[cache-conn connection?]{
The database connection.
}

@defproc[(init-cache-db!) void?]{

Creates and initializes the SQLite database cache file (named @filepath{vitreous.sqlite} and located
in the project root folder) by running queries to create tables in the database if they do not
exist.

}

@defparam[current-plain-title title-plain non-empty-string? #:value "void"]{

Contains (or sets) the “plain” title (i.e., with no HTML markup) for the current article based on
analysis done by @racket[parse-and-cache-article!]. If the article did not specify a title,
a default title is supplied. If the article contained a @racket[note] that used the
@code{#:disposition} attribute, the disposition-mark may be included in the title.

This is a weird parameter, and at some point I will probably get rid of it and have
@racket[parse-and-cache-article!] supply it as an extra return value instead.

@margin-note{Note that this needs to be called @emph{after} @racket[parse-and-cache-article!] in
order to get an up-to-date value.}

}

@section{Retrieving cached data}

Some of this looks a little wacky, but it’s a case of putting a little extra complextity into the
back end to make things simple on the front end. These functions are most commonly used inside the
@emph{body} of a Pollen document (i.e., series pages). 

@filebox["series/my-series.poly.pm"
@codeblock|{
#lang pollen

◊title{My New Series}

...some other content

◊(<listing-excerpt> articles+notes #:order 'asc)
}|
]

@deftogether[(@defproc[(<listing-full> 
                                 [query-func (-> any/c query?)]
                                 [#:series series (or/c string? (listof string?) boolean?) #t]
                                 [#:limit limit integer? -1]
                                 [order stringish? 'desc]) txexpr?]
              @defproc[(<listing-excerpt>
                                 [query-func (-> any/c query?)]
                                 [#:series series (or/c string? (listof string?) boolean?) #t]
                                 [#:limit limit integer? -1]
                                 [order stringish? 'desc]) txexpr?]
              @defproc[(<listing-short>
                                 [query-func (-> any/c query?)]
                                 [#:series series (or/c string? (listof string?) boolean?) #t]
                                 [#:limit limit integer? -1]
                                 [order stringish? 'desc]) txexpr?])]{

Fetches the HTML for items from the SQLite cache, concatenates their HTML strings and returns
a @racket['style] tagged X-expression with this string as its element. The items will be ordered by
publish date according to @racket[_order] and optionally limited to the series specified in
@racket[_series].

The @racket[_query-func] should be either @racket[articles], which will create a listing of articles
only, or @racket[articles+notes], which will include notes intermingled with articles.

@margin-note{Note that the signature shown for the @racket[_query-func] argument above is
incomplete. If you choose to pass a function other than @racket[articles] or
@racket[articles+notes], you must use a function with exactly the same signature as those
functions.}

If @racket[_series] expression evaluates to @racket[#f], articles will not be filtered by series. If
it evaluates to @racket[#t] (the default), articles will be filtered by those that specify the
current output of @racket[here-output-path] in their @tt{series_pagenode} column in the SQLite
cache. If a string is supplied, articles will be filtered by those containing that exact value in
their @tt{series_pagenode} column in the SQLite cache.

The @racket[_order] expression must evaluate to either @racket["ASC"] or @racket["DESC"] and the
@racket[_limit] expressions must evaluate to a value suitable for use in the @tt{LIMIT} clause of
@ext-link["https://sqlite.org/lang_select.html"]{a SQLite @tt{SELECT} statement}. An expression that
evaluates to a negative integer (the default) is the same as having no limit.

The reason for enclosing the results in a @racket['style] txexpr is to prevent the HTML from being
escaped by @racket[->html] in the template. This tag was picked for the job because there will
generally never be a need to include any actual CSS information inside a @tt{<style>} tag in any
page, so it can be safely filtered out later. To remove the enclosing @tt{<style>} tag, see
@racket[unfence].

}

@defproc[(listing-htmls [listing-query query?]) (listof string?)]{

Returns the HTML bodies for the articles and/or notes returned by @racket[_listing-query] as a list
of strings.

}

@deftogether[(@defproc[(articles [type (or/c 'full 'excerpt 'short)]
                                 [#:series series (or/c string? (listof string?) boolean?) #t]
                                 [#:limit limit integer? -1]
                                 [order stringish? 'desc]) query?]
              @defproc[(articles+notes [type (or/c 'full 'excerpt 'short)]
                                       [#:series series (or/c string? (listof string?) boolean?) #t]
                                       [#:limit limit integer? -1]
                                       [order stringish? 'desc]) query?])]{

Create a query that fetches either articles only (@racket[articles]) or articles and notes
intermingled (@racket[articles+notes]), sorted by publish date and optionally limited to
a particular series.

Typically you will pass these functions by name to listing functions like @racket[<listing-full>]
rather than calling them directly.

@examples[#:eval example-eval
(articles 'full)]
}

@defproc[(unfence [html string?]) string?]{

Returns @racket[_html] with all occurrences of @racket["<style>"] and @racket["</style>"] removed.
The contents of the style tags are left intact.

Use this in templates with strings returned from @racket[->html] when called on docs that use the
@racket[<listing-full>] tag function or its siblings.

}

@defproc[(series-grouped-list) (listof (listof cache:series?))]{

Return a list of lists of all @racket[cache:series] in the cache database. The series are grouped so
that series using the same value in the @tt{noun-plural} column appear together.

}

@section{Deleting records}

@deftogether[(@defproc[(delete-article! [page stringish?]) void?]
              @defproc[(delete-notes!   [page stringish?]) void?])]{
              
Delete a particular article, or all notes for a particular article, respectively.

}

@section{Schema}

The cache database has four tables: @tt{articles}, @tt{notes}, @tt{index_entries} and @tt{series}. Each of these has a corresponding schema, shown below. In addition, there is a “virtual” schema, @tt{listing}, for use with queries which may or may not combine articles and notes intermingled.

The work of picking apart an article’s exported @tt{doc} and @tt{metas} into rows in these tables is done by @racket[parse-and-cache-article!].

The below are shown as @code{struct} forms but are actually defined with deta’s @racket[define-schema]. Each schema has an associated struct with the same name and a smart constructor called @tt{make-@emph{id}}. The struct’s “dumb” constructor is hidden so that invalid entities cannot be created. For every defined field there is an associated functional setter and updater named @tt{set-@emph{id}-field} and @tt{update-@emph{id}-field}, respectively.

@defstruct*[cache:article ([id                   id/f]
                           [page                 symbol/f]
                           [title-plain          string/f]
                           [title-html-flow      string/f]
                           [title-specified      boolean/f]
                           [published            string/f]
                           [updated              string/f]
                           [author               string/f]
                           [conceal              string/f]
                           [series-page          string/f]
                           [noun-singular        string/f]
                           [note-count           integer/f]
                           [doc-html             string/f]
                           [disposition          string/f]
                           [disp-html-anchor     string/f]
                           [listing-full-html    string/f]
                           [listing-excerpt-html string/f]
                           [listing-short-html   string/f])
            #:constructor-name make-cache:article]{

Table holding cached article information.

}

@defstruct*[cache:note ([id              id/f]
                        [page            symbol/f]
                        [html-anchor     string/f]
                        [title-html-flow string/f]
                        [title-plain     string/f]
                        [author          string/f]
                        [author-url      string/f]
                        [published       string/f]
                        [disposition     string/f]
                        [content-html    string/f]
                        [series-page     symbol/f]
                        [conceal         string/f]
                        [listing-full-html    string/f]
                        [listing-excerpt-html string/f]
                        [listing-short-html   string/f])
                       #:constructor-name make-cache:note]{

Table holding cached information on notes.

}

@defstruct*[cache:series ([id            id/f]
                          [page          symbol/f]
                          [title         string/f]
                          [published     string/f]
                          [noun-plural   string/f]
                          [noun-singular string/f])
                         #:constructor-name make-cache:series]{

Table holding cached information on series.

}

@defstruct*[cache:index-entry ([id id/f]
                               [entry       string/f]
                               [subentry    string/f]
                               [page        symbol/f]
                               [html-anchor string/f])
                              #:constructor-name make-cache:index-entry]{
                              
Table holding cached information about index entries found in articles.

}

@defstruct*[listing ([html        string/f]
                     [published   date/f]
                     [series-page symbol/f])
                    #:constructor-name make-listing]{

This is not a table that persists in the cache database; rather it is the schema targeted by
@racket[articles] and @racket[articles+notes] using deta’s @racket[project-onto].

}