Queries and search¶
Lists and search read an index of the issues and people, kept in sqlite on the machine, or, for a board kept in PostgreSQL, in that database (ADR 0002, How the data flows). This page says where it lives, how it stays fresh, what a query asks, and how a program hears that entities changed.
Where it lives¶
For a board kept in git, in Markdown files or on the forges alone, the
index is one sqlite file, <git common dir>/board/index.db. The git
common dir is what git rev-parse --git-common-dir gives, so every
worktree of a repository shares one index. The core creates the board
directory when it is missing.
- It is never the source of truth. It holds only what the refs hold. Deleting it loses nothing: the next query builds it again.
- It is local. It lives in the git directory, so git never commits, fetches nor pushes it.
- Two processes can use it at once, like git-board's server and a
command. The file is in WAL mode, so a read doesn't wait for a write, and
a write waits up to 5 seconds for another (sqlite's
busy_timeout).
For a board kept in PostgreSQL, the index is the same tables in the
schema git_board_index_v<version> of the board's database (today
git_board_index_v3), shared by every machine. Each version of the
index's tables has its own schema, so two git-board versions sharing a
database each keep their index; neither drops the other's. A schema no
git-board reads any more stays until it is dropped by hand
(DROP SCHEMA git_board_index_v1 CASCADE; git_board_index, before
versions had their own). What differs from sqlite:
- A different rebuild: an index of another version is dropped and made
again (
DROP TABLE) under an advisory lock, not deleted as a file. - A different text search: a
tsvectorwith thesimpleconfiguration, the text tokenized as sqlite'sunicode61does, so a query finds the same issues; one issue's indexed text is cut at 300,000 bytes. The title, body and comments have their own weight (A,B,C), forin. - The same order:
Labelssorts byte for byte (COLLATE "C"), whatever the database's collation. - Several writers: a refresh takes a table lock, so two machines refreshing at once write one after the other.
What it holds¶
Issues and persons. Sync entities aren't indexed.
- For each issue: its id, title, status, author,
created,updated, labels and number of comments, from its state. For search, the text of its title, body and comments. For lists, the whole state, as JSON: a listing reads it instead of every issue's packs, and decodes only the issues it doesn't hold at that head. - For each person: its id, name and accounts (
server,id,login), from its state. - For each entity: its head, the commit its ref points at.
- For each broken entity: its head and its error (see Freshness).
Freshness¶
A refresh brings the index up to the refs:
- It lists
refs/board/issue/*andrefs/board/person/*. A ref whose name isn't a valid id isn't an entity, and is ignored (see Refs). - An entity that is new, or whose head differs from the stored one, is loaded again, and its rows replaced.
- An entity whose ref is gone is dropped.
- The writes are one transaction, which skips an entity whose stored head changed since step 2 read it: another refresh wrote it meanwhile, maybe a newer head than this one loaded. A head left behind is loaded again by the next refresh.
Every query refreshes first. So it answers from the refs as they are, whoever moved them: this process, another one, or a fetch.
An entity that fails to load is broken. The refresh drops its rows, so no query or lookup answers it, and still keeps the rest of its changes.
- The index records it, with its head and its error.
Broken()lists the broken entities, each with its kind, id and error, after a refresh like every query. - It is loaded again when its head moves, or at a rebuild. An entity whose ref is gone is dropped, broken or not.
- A query never fails because of one broken entity. It answers without it. A refresh that can't list the refs, or can't write the index, still fails the query.
Rebuild¶
The index holds its schema version in PRAGMA user_version. Opening it,
the core deletes the file and builds it again from the refs when:
- the file is missing;
- sqlite can't read it as a database;
- its version isn't the code's.
Its -wal and -shm files go with it.
Queries¶
A query asks for issues. Each field is optional: an empty one filters nothing.
| Field | Matches |
|---|---|
status |
open, closed, or empty for any |
labels |
issues that have every label listed, compared byte for byte |
author |
issues whose author is that person id |
text |
issues whose text matches (see Text) |
in |
the texts text is looked for in: any of title, body, comments; empty for all three |
An issue must match every field given. The answer is ordered and cut by three more:
| Field | What it does |
|---|---|
sort |
created or updated, the default |
desc |
true sorts newest first; false, the default, oldest first |
limit |
the most issues answered; 0, the default, answers all |
- Issues with the same time go by id, in the same direction.
- Another
status,sortor text inin, or a negativelimit, is an error. - Each issue comes as its summary: what the index holds for it.
Text¶
The text is searched in each issue's title, body and comments, as their last edits left them.
- Words. The text splits on whitespace into words. An issue matches when every word matches, each anywhere in its title, body or comments.
- Prefixes. A word matches the start of a word:
crashmatchesCrashes on save. - Case and diacritics don't count:
cafematchesCafé. Words are cut as FTS5's default tokenizer,unicode61, cuts them: letters and digits make up a token, and anything else separates tokens. - A word of several tokens, like
title:crash, matches those tokens in a row, the last as a prefix. - A word with no token, like
-, is left out. A text with no word left matches every issue. - FTS5's own syntax is never read. Each word is quoted, so
",*,-,:,AND,OR,NOTand parentheses are plain text. innarrows where. Each word must then match in one of the texts it names. Without the index,search.Matchanswers the same over an issue's texts: whatgit board ls --atsearches with.- On PostgreSQL (a board kept there, ADR 0002), an issue's text is
indexed up to its first 300,000 bytes of tokens: a
tsvectorholds at most 1 MB, and one issue's text never fails a query. A word past that isn't found.
Labels¶
Labels answers every label in use: each label at least one issue has,
sorted byte for byte, each once, whatever a PostgreSQL database's
collation. A label no issue has anymore isn't one.
Prefixes¶
A prefix of an issue's or a person's id, as people type them, names the entity whose id starts with it.
- It is 1 to 40 lowercase hex digits. Anything else is an error, so no character acts as a pattern.
- One entity matches: it answers that id.
- None matches: it is an error that says none does.
- Several match: it is an error that says the prefix is ambiguous. A whole id never is.
- It reads the index, after a refresh like every query, so a broken entity matches nothing.
Accounts¶
A lookup by account, a server and an id, answers the person who owns
it, by the rule in Who owns an account. When
no person holds the account, it answers none.
Change events¶
Watch tells a program, like git-board's server or its TUI, that entities
changed.
- It polls. Each poll lists
refs/board/<kind>/*for each kind, and compares each head with the one it saw last. It ignores a ref whose name isn't a valid id. - The first poll is the baseline. It runs at once, when
Watchstarts, and reports nothing. - Each later poll runs one interval after the last, and reports what changed since. An interval of 0 or less means 1 second.
- A poll whose listing of the refs fails is skipped: it reports nothing, and the next interval tries again. A poll is skipped as a whole when listing any kind fails. The next poll compares with the last poll that succeeded, and a skipped baseline leaves the next poll as the baseline.
- A poll's changes come together: each entity that appeared, moved, or
was removed, with its kind, id and new head. A removed one has the empty
version,
"", as its head (ADR 0002: a version is opaque). They are sorted by kind, then id. A poll with no change reports nothing. - It reports heads, not writes. An entity written several times between two polls is reported once, with its head at that poll. One created and removed between two polls isn't reported.
- It sees every change, whoever made it: this process, another one, or a fetch.
- It stops only when its context ends, never on its own.