DataDungeon::DB
Constants
Every change to an asset's tags, type or description, and every deletion, in order: what a MatchIndex or SemanticIndex in another process catches up from. Only the latest CHANGES_KEPT are kept; an index further behind rebuilds.
An assets row (alias a) as read_asset_record reads it, with its key
in its type's own index.
The indexes kept on types, and each asset's key in every index of its type, so a lookup by an index's leading traits is one range scan. A type's own index (the order it was registered with) has the empty consumer and name; the rest are consumers' (register_index), each its consumer's own: the consumer is part of its name.
Each way round: an asset's links (and linking twice changing nothing), what links to an asset, and the assets linked to a name.
One row per registered asset type. Trait lists are JSON arrays in
registration order, labels and facets JSON objects, sets a JSON array;
key_scheme records the TraitKey format the stored keys were made with.
Relations from an asset to another asset (derived-from, supersedes)
or to a name (used-by "boardwalk"). Exactly one target is set; links
go when either asset is deleted.
Each asset's type and tag values as words, for list_assets(q:):
lowercase, with dashes and underscores inside a word (red-hair) and a
word's parts listed too, so house finds west-of-house. Diacritics
are kept, so the index and a walk through the words match alike.
How many assets have each word, kept with the index so a search can tell a common word from a rare one without reading its matches. The empty word counts every indexed asset.
How many assets of a type carry each tag value, current and retired, and (under the empty tag) how many assets the type has: for browsing counts, and for planning a lookup by tags. Triggers keep them, so every write keeps them right. An empty tag name is refused for that.
Tag ranges are per type, so a lookup never walks other types' tags.
Tag filters across all types.
An asset's tracks, each replaced whole; its cues as JSON, each [start_ms, end_ms, value].
An asset's keys are in asset_keys, its type's own index among them.
0.15 dropped the conversions from before 0.12 (Thomas, 2026-10-17): a store last opened by an older DataDungeon, whose keys were still a column of its assets, is opened once by 0.14 first.
How long a write waits for another connection's write lock.
Which assets get and best matches choose among: current ones nobody
has flagged (a report takes an asset out of play; listings still show
it).
The counts as the tables have them, for checking the triggers kept them.
An animated picture's frames (ImageAnimation); a still one has no row.
The image_meta columns in table order, after asset_id, with their types.
(view_name, last, is the view's Direction#name, kept by DataDungeon
so its words can be counted: see view_counts.)
Nearly every image has these, and filters ask for ranges of them.
An animation's checkpoints after its first frame (ImageKeyframe): only the fields that change.
Image fields (see ImageMeta): what DataDungeon measures from the bytes,
with measured once it has read them, and what the application sets.
For an animation they describe its first frame.
The image fields, then the animation's, as read_image_meta_join reads them.
Each image's view in readable form (Direction), for people.
The fields an animation's checkpoints can change.
The id of a type's own index, for a query taking the type as an argument.
A 0.13 picture's prompt fields in the order they were joined, for reading what it was sent (Recipe#joined in 0.13).
Which of several candidates to prefer: the highest rating (none counts as 0), then the newest made, then the newest filed.
Image fields a listing can ask for by range.
How many rows of a key range to count when judging where a tag filter starts; past this its size is estimated.
Settings are stored once by hash, as canonical JSON (keys sorted).
A named text is one wording of an owner's name, of some kind (style,
rating, ...); its version is the start of its words' SHA-256
(Recipe::VERSION_SIZE), so it names the same words in every store. A
recipe is a row per step (0 for a single recipe), with what its
provider was sent, and a row per named wording it used. Before 0.13 an
asset's generation was free-form JSON on its row; it stays readable
in recipe_legacy until its app gives it a recipe.
The tracks apps share (Thomas, 2026-10-19: a take's word times and mouth cues go in with the speech; 2026-10-22: subtitles). Other names are an app's own.
DataDungeon's asset types, as agreed (Thomas with the apps, 2026-10-13). The list changes in a release, by agreement, never by an app. The apps moved their own types onto it in 0.15 (rename_type); since 0.16, register_type, rename_type and storing take only these.
Application types that are text to read.
The triggers' names, for recreating them.
A tag counts as its asset does, current or retired. An asset's tags go
before the asset itself, while it still says which they counted as.
A picture's view counts too, under the tag @view, once for each word
of its name (back-left, from-above). setup_schema recreates the
triggers on every read-write open, so a database has these ones.
Every wording, an asset's description or what a recipe's provider was sent, is one row, stored once (DESIGN-recipes.md).
Uses counted in memory are written once there are this many (touch_asset).
The shared vocabulary as this release agrees it. It grows by agreement, in a DataDungeon release: every store opened read-write takes it as it is here. A value's words are DataDungeon's wording of it, of the tag's kind of wording; changed words are the wording's next version, and recipes keep the one they were made with.
The owner of DataDungeon's own wordings, the ratings' words.
A wording never changes and is never deleted, so a name and version mean the same words for as long as the store lasts, in the sqlite3 shell too. New words are a new version; a new name is a new wording.
Constructors
With read_only: true the database is only read: nothing is created,
upgraded or re-keyed, every connection refuses writes (PRAGMA
query_only), and the methods that write raise ReadOnlyError. A database
an older DataDungeon wrote must be opened read-write once first.
consumer names the application, for its own indexes (register_index).
Class methods
A content type's medium, when the caller gives none: its top-level type (image, audio, video, text, model, font), with JSON, XML and YAML counted as text, and any other type (a PDF, raw bytes) "data".
Instance methods
Names wordings a stored recipe's step was sent but doesn't name, for
an app naming its words after the fact (its rating's words, on
pictures made before it named them); step is 0 for a recipe without
steps. Only the new wordings are checked, as a new recipe's are: their
words in the step's prompt or instructions, word for word, and theirs
to add. What the step names already stays as it was stored, and a
wording it names already is skipped. Returns how many were added.
Move a row from outside_store into the store: copy its file's bytes
to their content-addressed key, as store would, and point the row at
that object. The row keeps its id, type, tags and metadata, and the
original file is left where it is. A row already in the store comes
back unchanged. Raises File::NotFoundError if the file is gone.
Whether a current asset has this key in its type's own index.
Every asset type with how many current assets it has (with
retired: true, how many retired ones), by name. Registered types
are listed even with none.
Report-only reconciliation of the index against the storage backend. Walks both directions of divergence: rows whose object is missing, and stored objects with no owning row (leaked blobs — e.g. from deletes made before v0.3.4, when delete_asset didn't unlink). Changes nothing. In shared storage, other stores' objects are among those with no row here.
Who is using the store ("cattacula"), which says whose indexes
register_index creates and changes.
How many assets list_assets would list with these filters, on every
page. It reads every match, so ask only when the total is wanted.
How often each custom value of a trait occurs among assets of a type, most frequent first: candidates for becoming standard choices.
Public delete: removes the row and, once no other row shares it, the backing object. DataDungeon owns the write path, so it owns the unlink too — callers must not reach around to File.delete the blob (that breaks for non-local backends and for objects other assets still use). The object key is read before the row is removed; Storage#delete is a no-op if the object is already gone.
Forget an attached indexer; it calls this when it closes.
Both lookups at once: pool as find_pool (with custom), and group
the rest of the group, for ranking or as a fallback. A type without
group traits has an empty group.
Of the current, unflagged assets with this key in its type's own index, the preferred (PREFERRED), without get's file check or touch.
Assets of a type carrying every one of these tags (and possibly others).
The assets holding these bytes, by their SHA-256, oldest first. Identical bytes share one object, so several assets can hold it.
Every asset whose first size traits (by default, the group traits)
equal these: one key-prefix range lookup. Later traits are ignored.
Every asset whose key in its type's own index starts with this prefix,
which must end at a separator (as group_key_for's do); portrait. is
the whole type.
The current assets whose traits all equal these. Traits named in
custom may differ, leaving the
caller to rank them (e.g. semantically); every other trait matches
exactly, and a trait absent from traits must be absent on the asset.
Without custom this is one key lookup in the type's own index. With
it, the traits before the first custom one narrow the candidates by key
prefix, and the rest are
checked against each candidate's tags, so it's fastest when the custom
traits come late in the type's order.
Every current asset with this key in its type's own index. A key names its type, so the key alone is enough.
Writes the uses counted since the last write (touch_asset). With
wait: false, gives up at once if another connection holds the write
lock, keeping the counts for later. Returns whether they were written.
High-level lookup: of the current, unflagged assets with these traits, the preferred (PREFERRED: highest rating, then newest made); verifies the backing file still exists (self-heals a stale row to a miss), touches usage, and returns the record. Every current asset with the traits comes from find_pool.
get by a key in a type's own index; the key names its type.
One asset by its id, retired or flagged too.
Eager lookup: record + the media metadata appropriate to the asset's
media (ImageMeta / AudioMeta / VideoMeta / TextMeta), returned as the
polymorphic AssetMeta base, in a single query. Same self-heal + touch
semantics as get. Returns {record, meta?} or nil on miss; meta is nil
when the asset has no sidecar row. Callers narrow with .as?(ImageMeta)
when they need media-specific fields; .presentation works directly on
any visual medium without narrowing.
get_with_meta by a key in a type's own index.
The key prefix shared by assets whose first size traits (by default,
the group traits) equal these. Later traits are ignored.
Every consumer's indexes on a type (not the type's own).
A clause (alias a) for the assets with this key in their type's own
index, or with a key starting with this prefix: one range of
asset_keys, from prefix up to, not including, the prefix with its
final separator raised by one ('.' to '/').
:nodoc:
The key a registered type's own index gives these traits. Traits left out are absent (an empty segment); names that aren't the type's traits raise.
Relate an asset to another asset (derived-from) or to a name
(used-by "boardwalk.holidayland": a name nests under its user with
dots, so filters can take a user and everything under it). Relations
are lowercase words joined by '-'. data is JSON kept with the link,
such as how the asset is placed in that use; linking again replaces it
when given, and otherwise changes nothing. Links go when either asset
does. A supersedes link is made by supersede, which also retires
the old take.
An asset's links, of one relation or of all.
The links to an asset, or to names (a pattern, as name_match takes
it), of one relation or of all.
A page of current assets (with retired: true retired ones, with
nil both), newest filed first: of one type or of all, carrying every
one of tags (normalized for a registered type), matching every word
of q, linked by each relation in links to a name (or a pattern:
boardwalk.*, boardwalk*, see links_to), with no unlinked link,
with the file file_hash (to follow a recipe's reference), the
description description_hash (every asset sharing one), and using
each named text in texts (prompt field => owner:name, any wording,
or owner:name@version). Each word of q is the type or a tag value (or a part
of one), in any order, as in a search box: cattacula night finds
front.boardwalk.cattacula.night; catt* matches the start of a
word; and red-hair matches only a labelled trait spelled that way. Skip offset assets for a numbered
page (it costs a little per asset skipped: about 2 ms for 100,000), or,
to walk through them all, pass the last asset's id as before for the
next page. count_assets counts them all.
The image fields of these assets, those that have them, in one query.
The image fields best matches use (AssetType::MATCH_FIELDS) of these
assets, as text: a view or light as its unit vector x,y,z, a roll in
quarter turns, a dominant colour as rrggbb. Fields not known are left
out.
Measure the stored pictures DataDungeon hasn't read yet (stored before
it measured them, or through store_asset), or with all every one,
of one type or all. Each object is read from storage: for a bucket, a
download. A file outside the store, or an object gone, is left as it
is. Returns how many were read.
The wordings an owner has of a kind: {name, version, text}, by name, oldest first.
Trait values normalized as a registered type stores them (blank trait values dropped); other tags, and any tags of an unregistered type (one stored before 0.12 required registering), as given.
Every object key this store's rows use, retired assets' included: what a program clearing a shared bucket keeps (Storage#shared?).
Stream the record's object: the block gets an IO over its bytes, from whichever storage is configured, without loading it all into memory.
The rows that index a file outside the store: those store_asset
recorded by an absolute path the caller owns. adopt moves them in.
Resolve a record's storage key to a usable location (local path or URL).
Treat a custom value as a standard choice from now on. Keys include custom values like any other, so no key changes. Returns the number of tags updated.
Delete a type's retired assets: all, or keeping each key's (in the
type's own index) keep_last most recently retired, and only those
retired before retired_before. When to prune is the application's
call. Returns how many were deleted.
DataDungeon's words for a rating, the same for every form: what's
suitable for it, dd:pg@<version>, "Suitable for older children: …".
A picture takes them with its rules. Raises for a value that isn't a
rating.
The record's object, read through whichever storage is configured (a local file or a remote bucket). Prefer it to opening path_for yourself, which only works for local storage.
The asset's recipe, steps and all, with each wording as
owner:name@version; nil when it has none.
Read the registrations again, so a long-running process (dd-serve) sees types another process registered or changed since it opened. SemanticIndex#catch_up calls it when the change log has moved. Read under the lock, as register_type writes, so a registration made here meanwhile isn't replaced by the rows read before it.
Keep an index on a registered type for this consumer's common lookups:
traits in the order it usually asks by. Apps just ask with traits:
a lookup fixing the leading traits of any index on the type (the
type's own, or any consumer's) can start from that range, and
DataDungeon starts from the narrowest (see tag_filter). Each consumer
names and changes only its own indexes, so open the store with
consumer:. Registering an index again changes nothing; other traits
raise unless rekey: true, which re-keys the type's assets in it.
Declare an asset type, which storing requires. DataDungeon derives each
asset's key in the type's own index from its pool_traits tags, so
applications never build keys. List the traits most important first: the key is their
normalized values in that order (see TraitKey), so a lookup on leading
traits is a prefix match. group_traits names the leading traits that
make up the type's group, and labels suffixes for traits whose values
alone would read ambiguously in a key ({"skin" => "skin"} gives
tan-skin). sets names traits whose value is a set, given as
comma-separated members in any order ("sword, Lantern" is stored and
keyed as lantern,sword); a set matches only the same set. Trait tags
are stored normalized, as the key holds them. embed lists what
SemanticIndex embeds of each asset: "description" and tag names
(%w[description race hair-color]), in any order; a type with none
isn't embedded. facets names groups of tags that SemanticIndex
embeds separately, so a search can rank one part of an asset (its
hair, say) without the rest diluting it.
scales, colors and weights shape best matches (MatchIndex) and
change no keys: scales lists an ordered trait's values low to high
({"rating" => %w[g pg r x]}), so near values count as close; colors
names traits whose values are colors (CSS names or hex codes),
compared by appearance; cycles lists values that go round a circle
({"time" => %w[dawn day dusk night]}), so night's nearest are dusk
and dawn; match_fields names image fields matched on beside the
traits (AssetType::MATCH_FIELDS); weights gives traits (and match
fields) a default weight other than 1. A key is an index, not a name:
several current assets can share one, and every store adds an asset.
The registration is stored in the database. Registering a type again
with the same definition changes nothing, and one that changes only
what keys don't hold (what's embedded, facets, scales, colors, cycles,
weights, match fields) is saved as it is; a change to what's embedded
or to facets queues the type's assets to be embedded again. Other traits, a new order, group, labels or
sets raise unless rekey: true: changing them re-derives the keys of
every asset of the type, so a process holding a stale definition fails
loudly instead of re-keying every asset.
The registration of an asset type, or nil when it has none.
Make a retired asset current again. If takes superseded it, it supersedes in turn the current take they led to (directly, or down a chain of takes): that take is retired, and the links superseding this one go. Returns whether it was retired; a current asset is left as it is.
Rename a tag on every asset of a type, and in the type's registration
if it names it (a trait or a facet's tag). A registered type's keys are
derived again: they hold trait values, not names, so they normally stay
the same, but a type registered under the new name first gets its keys
right. Raises if an asset already has a tag named to. Returns the
number of assets whose tag was renamed.
Rename an asset type: its assets, their tags and its registration move
to to. If to already has assets, these join them, under to's
registration if it has one. A registered type's keys begin with its
name, so they're derived again. Returns the number of assets moved.
Once nothing of from is left, it changes nothing, so an app can keep
the call in its start-up (@pirateship).
Retire an asset with nothing replacing it: a soft, reversible delete. Returns whether it was current.
Replace an animation's checkpoints (see ImageKeyframe), in time order, each after its start and within its duration. Neighbouring views, starting from the first frame's, may be at most 90 degrees apart, or which way round it turns would be ambiguous; and a checkpoint goes wherever something turns (the top of a jump), so values move only one way between them and a filter at the checkpoints is exact. For an animated WebP, DataDungeon measures the content box at each one, keeping it where it differs from the one before.
Gives an asset its recipe, for moving an app's old records into
recipes. An asset that has a recipe keeps it: replacing one takes
replace: true. The free-form record the asset was stored with before
0.13 (generation) is kept too, unless drop_generation: true says
the recipe holds everything it said.
Keep what a tool measured of a sound or a video over time, replacing
the track of that name: words and mouth are shared
(STANDARD_TRACKS), other names an app's own. tool names the tool, its
version and any options that change its result (rhubarb 1.14.0 --dialogFile), so a better one can redo the track.
Cues go in time order; a track with none says the tool found nothing.
Tracks go when their asset does.
Store an asset end to end: apply the media compression policy, hash the
resulting bytes, write them to their content-addressed key
(<first two hex chars>/<sha256>.<ext>), then index the row. Identical
bytes share one object. Returns the stored AssetRecord.
Every store adds an asset. supersedes names a take this one replaces
(see supersede), and made_at when the content was made, if not
now. Bytes the type already holds raise AlreadyStored, unless storing
them is an exact repeat (the same traits, the asset current), which
returns that asset.
The type must be registered (register_type). Provide the source as
exactly one of source_path or bytes.
Index a file whose path the caller owns. The type must be registered:
DataDungeon normalizes the trait tags and derives the asset's keys from
them. custom names the tags whose values are not among the
application's standard choices; they are keyed like any other value
and can later be promoted.
Store the WebP of a picture DataDungeon decodes (a TIFF, with
data_dungeon/tiff, or a PNG) as a new asset of its type, linked
derived-from it with the quality in the link's data. It takes the
picture's tags with these over them ({"fmt" => "webp"}), its
description and rating, and the image fields the application sets; it
isn't permanent, since it can be made again. Storing the same WebP with
the same tags again returns it (see store).
new_id replaces old_id: the old take is retired (kept, but out of
get, listings, best match and search), new_id supersedes old_id is
linked as its history, and the old take's other links are copied to
the new one, placement data and all.
A type's tag names, each with how many of its current assets (or,
with retired, as list_assets takes it, others) carry it.
The values one tag takes across a type's current assets (or, with
retired, others), most frequent first, each with how many carry it.
Counts a use of the asset (last_used_at, use_count), as get does
unless told touch: false. The count is kept in memory, so a read
doesn't wait on a disk sync. Every USAGE_WRITE_AT uses are written in
one transaction, if the write lock is free (if not, ten uses later),
and the rest at close, when the process exits, or by flush_usage.
Until then a record shows the counts as last written, and a process
killed outright loses those not yet written: they're a hint for
eviction, not a record of plays.
Remove a link; returns whether there was one. Removing a supersedes
link leaves the old take retired: reinstate brings it back.
Drop one of this consumer's indexes, and its keys. Returns whether there was one.
Edit an asset in place, without storing its bytes again: replace its
tags, or change its description, flag, rating or permanent, or the image fields
the application sets (ImageMeta::SET_FIELDS, and an animation's frames
for a sprite sheet). Arguments left nil stay as they are. A registered
type's traits are normalized and its keys derived again. A tag stays
custom while its value is unchanged. A rating and flagged decide
which of several candidates get and best matches prefer.
A blank description clears it. Assets can share one description (the
same description_hash); a new one is this asset's alone unless
everywhere: true, which gives it to every asset in the store that
shared this one's, as when fixing a typo. The old wording stays in the
store either way: a recipe may record it as what a provider was sent.
Reconcile the index and the backend. Off by default (behaves like check_consistency); pass the flags to actually delete. Returns the report of what was found — and, per the flags, acted on.
- delete_missing_rows: drop rows whose backing object is gone.
- delete_orphan_objects: unlink stored objects with no owning row.
How many of a type's current pictures (or, with retired, others) are
seen from each named direction, most first. Each word of a view's name
counts (back-left, from-above), as the view= filter takes them.
The assets whose recipes used a named wording of a kind, retired ones
included, oldest first: with_text("style", "overlore:silver-comic")
for any of its wordings, "overlore:silver-comic@3f9a2c71b0" for one;
with_text("rating", "dd:pg") for everything given DataDungeon's
words for a rating.