class

DataDungeon::DB

Inherits Reference < Object

Constants

ANIMATION_COLUMNS = ["frame_count", "duration_ms", "loop_count", "frame_w", "frame_h", "columns"] of ::String
ASSET_CHANGES_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_changes (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n asset_id INTEGER NOT NULL\n )"

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.

ASSET_COLUMNS = " a.id, a.asset_type, a.media, a.content_type, a.file_path,\n (SELECT k.key FROM asset_indexes i JOIN asset_keys k ON k.index_id = i.id AND k.asset_id = a.id\n WHERE i.asset_type = a.asset_type AND i.consumer = '' AND i.name = '') AS key,\n a.file_hash, a.created_at, a.last_used_at, a.use_count, a.permanent, a.generator,\n (SELECT generation FROM recipe_legacy WHERE asset_id = a.id) AS generation,\n (SELECT text FROM texts WHERE id = a.description_id) AS description,\n (SELECT hash FROM texts WHERE id = a.description_id) AS description_hash,\n a.generated, a.rating, a.flagged, a.made_at, a.retired_at,\n EXISTS (SELECT 1 FROM recipes WHERE asset_id = a.id) AS has_recipe"

An assets row (alias a) as read_asset_record reads it, with its key in its type's own index.

ASSET_INDEXES_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_indexes (\n id INTEGER PRIMARY KEY,\n asset_type TEXT NOT NULL,\n consumer TEXT NOT NULL,\n name TEXT NOT NULL,\n traits TEXT NOT NULL,\n UNIQUE (asset_type, consumer, name)\n )"

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.

ASSET_KEYS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_keys (\n asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,\n index_id INTEGER NOT NULL REFERENCES asset_indexes(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n PRIMARY KEY (asset_id, index_id)\n ) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS idx_asset_keys_key\n ON asset_keys(index_id, key)"
ASSET_LINKS_INDEXES = " CREATE UNIQUE INDEX IF NOT EXISTS idx_links_from\n ON asset_links(from_id, rel, ifnull(to_id, 0), ifnull(to_name, ''));\n CREATE INDEX IF NOT EXISTS idx_links_to_asset\n ON asset_links(to_id, rel);\n CREATE INDEX IF NOT EXISTS idx_links_to_name\n ON asset_links(rel, to_name, from_id)"

Each way round: an asset's links (and linking twice changing nothing), what links to an asset, and the assets linked to a name.

ASSET_LINKS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_links (\n from_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,\n rel TEXT NOT NULL,\n to_id INTEGER REFERENCES assets(id) ON DELETE CASCADE,\n to_name TEXT,\n created_at TEXT NOT NULL,\n data TEXT,\n CHECK ((to_id IS NULL) <> (to_name IS NULL))\n )"

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.

ASSET_SEARCH_SCHEMA = " CREATE VIRTUAL TABLE IF NOT EXISTS asset_search\n USING fts5(words, tokenize = \"unicode61 tokenchars '-_' remove_diacritics 0\")"

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.

ASSET_SEARCH_TERMS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_search_terms (\n term TEXT PRIMARY KEY,\n docs INTEGER NOT NULL\n ) WITHOUT ROWID"

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.

ASSET_TAG_COUNTS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_tag_counts (\n asset_type TEXT NOT NULL,\n tag_key TEXT NOT NULL,\n tag_value TEXT NOT NULL,\n current INTEGER NOT NULL DEFAULT 0,\n retired INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (asset_type, tag_key, tag_value)\n ) WITHOUT ROWID"

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.

ASSET_TAGS_INDEX = " CREATE INDEX IF NOT EXISTS idx_tags_type_kv\n ON asset_tags(asset_type, tag_key, tag_value)"

Tag ranges are per type, so a lookup never walks other types' tags.

ASSET_TAGS_KV_INDEX = " CREATE INDEX IF NOT EXISTS idx_tags_key_value\n ON asset_tags(tag_key, tag_value)"

Tag filters across all types.

ASSET_TAGS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_tags (\n asset_id INTEGER NOT NULL,\n tag_key TEXT NOT NULL,\n tag_value TEXT NOT NULL,\n custom INTEGER NOT NULL DEFAULT 0,\n asset_type TEXT NOT NULL,\n PRIMARY KEY (asset_id, tag_key),\n FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE\n )"
ASSET_TRACKS_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_tracks (\n asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n tool TEXT NOT NULL,\n cues TEXT NOT NULL,\n made_at TEXT NOT NULL,\n PRIMARY KEY (asset_id, name)\n ) WITHOUT ROWID"

An asset's tracks, each replaced whole; its cues as JSON, each [start_ms, end_ms, value].

ASSET_TYPES_SCHEMA = " CREATE TABLE IF NOT EXISTS asset_types (\n asset_type TEXT PRIMARY KEY,\n pool_traits TEXT NOT NULL,\n group_traits TEXT NOT NULL,\n labels TEXT NOT NULL DEFAULT '{}',\n sets TEXT NOT NULL DEFAULT '[]',\n facets TEXT NOT NULL DEFAULT '{}',\n key_scheme INTEGER NOT NULL,\n scales TEXT NOT NULL DEFAULT '{}',\n colors TEXT NOT NULL DEFAULT '[]',\n weights TEXT NOT NULL DEFAULT '{}',\n cycles TEXT NOT NULL DEFAULT '{}',\n match_fields TEXT NOT NULL DEFAULT '[]',\n embed TEXT NOT NULL DEFAULT '[]'\n )"
ASSETS_INDEXES = " CREATE INDEX IF NOT EXISTS idx_assets_type\n ON assets(asset_type);\n CREATE INDEX IF NOT EXISTS idx_assets_retired\n ON assets(asset_type, id) WHERE retired_at IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_assets_file_path\n ON assets(file_path);\n CREATE INDEX IF NOT EXISTS idx_assets_file_hash\n ON assets(file_hash);\n CREATE INDEX IF NOT EXISTS idx_assets_generator\n ON assets(generator)"

An asset's keys are in asset_keys, its type's own index among them.

ASSETS_SCHEMA = " CREATE TABLE IF NOT EXISTS assets (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n asset_type TEXT NOT NULL,\n media TEXT NOT NULL,\n content_type TEXT NOT NULL,\n file_path TEXT NOT NULL,\n file_hash TEXT,\n created_at TEXT NOT NULL,\n last_used_at TEXT,\n use_count INTEGER DEFAULT 0,\n permanent INTEGER DEFAULT 0,\n generator TEXT,\n description_id INTEGER REFERENCES texts(id),\n generated INTEGER NOT NULL DEFAULT 0,\n rating INTEGER,\n flagged INTEGER DEFAULT 0,\n made_at TEXT,\n retired_at TEXT\n )"
AUDIO_META_SCHEMA = " CREATE TABLE IF NOT EXISTS audio_meta (\n asset_id INTEGER PRIMARY KEY,\n duration_ms INTEGER,\n sample_rate INTEGER,\n bitrate INTEGER,\n channels INTEGER DEFAULT 1,\n transcript_id INTEGER,\n compression TEXT DEFAULT 'original',\n target_format TEXT,\n FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE,\n FOREIGN KEY (transcript_id) REFERENCES assets(id)\n )"
BEFORE_012 = "This store was last opened by a DataDungeon older than 0.12, which this one no longer converts: open it read-write once with DataDungeon 0.14, then with this one"

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.

BUSY_TIMEOUT_MS = 5000

How long a write waits for another connection's write lock.

CANDIDATE = "a.retired_at IS NULL AND ifnull(a.flagged, 0) = 0"

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

CHANGES_KEPT = 100000
CHECK_COST = 0.46
COUNT_TAGS = [" INSERT INTO asset_tag_counts (asset_type, tag_key, tag_value, current, retired)\n SELECT t.asset_type, t.tag_key, t.tag_value, SUM(a.retired_at IS NULL), SUM(a.retired_at IS NOT NULL)\n FROM asset_tags t JOIN assets a ON a.id = t.asset_id\n GROUP BY t.asset_type, t.tag_key, t.tag_value", " INSERT INTO asset_tag_counts (asset_type, tag_key, tag_value, current, retired)\n SELECT asset_type, '', '', SUM(retired_at IS NULL), SUM(retired_at IS NOT NULL)\n FROM assets GROUP BY asset_type", " INSERT INTO asset_tag_counts (asset_type, tag_key, tag_value, current, retired)\n SELECT asset_type, '@view', word, SUM(retired_at IS NULL), SUM(retired_at IS NOT NULL) FROM (\n SELECT a.asset_type, a.retired_at, CASE WHEN instr(im.view_name, ' ') > 0\n THEN substr(im.view_name, 1, instr(im.view_name, ' ') - 1) ELSE im.view_name END AS word\n FROM image_meta im JOIN assets a ON a.id = im.asset_id WHERE im.view_name IS NOT NULL\n UNION ALL\n SELECT a.asset_type, a.retired_at, substr(im.view_name, instr(im.view_name, ' ') + 1)\n FROM image_meta im JOIN assets a ON a.id = im.asset_id WHERE instr(im.view_name, ' ') > 0\n ) GROUP BY asset_type, word"]

The counts as the tables have them, for checking the triggers kept them.

IMAGE_ANIMATION_SCHEMA = " CREATE TABLE IF NOT EXISTS image_animation (\n asset_id INTEGER PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE,\n frame_count INTEGER,\n duration_ms INTEGER,\n loop_count INTEGER,\n frame_w INTEGER,\n frame_h INTEGER,\n columns INTEGER\n )"

An animated picture's frames (ImageAnimation); a still one has no row.

IMAGE_COLUMNS = {"width" => "INTEGER", "height" => "INTEGER", "usage_class" => "TEXT", "compression" => "TEXT", "quality_floor" => "INTEGER", "max_colors" => "INTEGER", "target_format" => "TEXT", "presentation" => "TEXT", "transparent" => "INTEGER", "dominant_color" => "INTEGER", "content_x" => "INTEGER", "content_y" => "INTEGER", "content_w" => "INTEGER", "content_h" => "INTEGER", "measured" => "INTEGER", "view_x" => "REAL", "view_y" => "REAL", "view_z" => "REAL", "view_roll" => "REAL", "light_x" => "REAL", "light_y" => "REAL", "light_z" => "REAL", "anchor_x" => "REAL", "anchor_y" => "REAL", "world_width" => "REAL", "world_height" => "REAL", "world_depth" => "REAL"}

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

IMAGE_INDEXED = ["width", "height", "content_w", "content_h", "view_x", "view_y", "view_z", "light_x", "light_y", "light_z", "anchor_x", "anchor_y", "world_width", "world_height", "world_depth"] of ::String

Nearly every image has these, and filters ask for ranges of them.

IMAGE_KEYFRAMES_SCHEMA = " CREATE TABLE IF NOT EXISTS image_keyframes (\n asset_id INTEGER NOT NULL REFERENCES image_animation(asset_id) ON DELETE CASCADE,\n at_ms INTEGER NOT NULL,\n held INTEGER NOT NULL DEFAULT 0,\n view_x REAL,\n view_y REAL,\n view_z REAL,\n view_roll REAL,\n light_x REAL,\n light_y REAL,\n light_z REAL,\n anchor_x REAL,\n anchor_y REAL,\n content_x INTEGER,\n content_y INTEGER,\n content_w INTEGER,\n content_h INTEGER,\n PRIMARY KEY (asset_id, at_ms)\n ) WITHOUT ROWID"

An animation's checkpoints after its first frame (ImageKeyframe): only the fields that change.

IMAGE_META_SCHEMA = " CREATE TABLE IF NOT EXISTS image_meta (\n asset_id INTEGER PRIMARY KEY,\n width INTEGER,\n height INTEGER,\n usage_class TEXT,\n compression TEXT DEFAULT 'original',\n quality_floor INTEGER,\n max_colors INTEGER,\n target_format TEXT,\n presentation TEXT,\n transparent INTEGER,\n dominant_color INTEGER,\n content_x INTEGER,\n content_y INTEGER,\n content_w INTEGER,\n content_h INTEGER,\n measured INTEGER,\n view_x REAL,\n view_y REAL,\n view_z REAL,\n view_roll REAL,\n light_x REAL,\n light_y REAL,\n light_z REAL,\n anchor_x REAL,\n anchor_y REAL,\n world_width REAL,\n world_height REAL,\n world_depth REAL,\n view_name TEXT,\n FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE\n )"

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.

IMAGE_SELECT = (((["im.asset_id"] + IMAGE_COLUMNS.keys.map do |column| "im.#{column}" end) + ["ia.asset_id"]) + ANIMATION_COLUMNS.map do |column| "ia.#{column}" end).join(", ")

The image fields, then the animation's, as read_image_meta_join reads them.

IMAGE_VIEWS = " CREATE VIEW IF NOT EXISTS image_views AS\n SELECT asset_id,\n CASE WHEN view_z >= 0 THEN 'front' ELSE 'back' END AS side,\n round(degrees(asin(max(-1.0, min(1.0, view_x / cos(asin(view_y)))))) / 90, 4) AS yaw,\n round(degrees(asin(view_y)) / 90, 4) AS pitch,\n view_roll AS roll\n FROM image_meta WHERE view_x IS NOT NULL"

Each image's view in readable form (Direction), for people.

INDEX_COST = 0.022
KEYFRAME_FIELDS = ["view_x", "view_y", "view_z", "view_roll", "light_x", "light_y", "light_z", "anchor_x", "anchor_y", "content_x", "content_y", "content_w", "content_h"] of ::String

The fields an animation's checkpoints can change.

LABEL = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
OWN_INDEX = "(SELECT id FROM asset_indexes WHERE asset_type = ? AND consumer = '' AND name = '')"

The id of a type's own index, for a query taking the type as an argument.

PICTURE_FIELDS_013 = ["preamble", "subject", "description", "staging", "theme", "style", "rules"] of ::String

A 0.13 picture's prompt fields in the order they were joined, for reading what it was sent (Recipe#joined in 0.13).

PREFERRED = "ORDER BY ifnull(a.rating, 0) DESC, a.made_at DESC, a.id DESC"

Which of several candidates to prefer: the highest rating (none counts as 0), then the newest made, then the newest filed.

RANGE_FIELDS = ["width", "height", "content_x", "content_y", "content_w", "content_h", "view_x", "view_y", "view_z", "view_roll", "light_x", "light_y", "light_z", "anchor_x", "anchor_y", "world_width", "world_height", "world_depth"] of ::String

Image fields a listing can ask for by range.

RAREST_PROBE = 1000

How many rows of a key range to count when judging where a tag filter starts; past this its size is estimated.

RECIPE_SCHEMAS = " CREATE TABLE IF NOT EXISTS recipe_settings (\n id INTEGER PRIMARY KEY,\n hash TEXT NOT NULL UNIQUE,\n settings TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS named_texts (\n id INTEGER PRIMARY KEY,\n kind TEXT NOT NULL,\n owner TEXT NOT NULL,\n name TEXT NOT NULL,\n version TEXT NOT NULL,\n text_id INTEGER NOT NULL REFERENCES texts(id),\n UNIQUE (kind, owner, name, version)\n );\n CREATE TABLE IF NOT EXISTS recipes (\n asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,\n step INTEGER NOT NULL,\n form TEXT NOT NULL,\n settings_id INTEGER REFERENCES recipe_settings(id),\n seed INTEGER,\n cost REAL,\n cost_basis TEXT,\n generated_at TEXT,\n prompt_id INTEGER REFERENCES texts(id),\n instructions_id INTEGER REFERENCES texts(id),\n refs TEXT,\n PRIMARY KEY (asset_id, step)\n ) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS idx_recipes_settings ON recipes(settings_id);\n CREATE TABLE IF NOT EXISTS recipe_wordings (\n asset_id INTEGER NOT NULL,\n step INTEGER NOT NULL,\n position INTEGER NOT NULL,\n named_id INTEGER NOT NULL REFERENCES named_texts(id),\n PRIMARY KEY (asset_id, step, position),\n FOREIGN KEY (asset_id, step) REFERENCES recipes(asset_id, step) ON DELETE CASCADE\n ) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS idx_recipe_wordings_named ON recipe_wordings(named_id);\n CREATE TABLE IF NOT EXISTS recipe_legacy (\n asset_id INTEGER PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE,\n generation TEXT NOT NULL\n )"

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.

SIN_22 = Math.sin((22.5 * Math::PI) / 180)
SIN_67 = Math.sin((67.5 * Math::PI) / 180)
STANDARD_TRACKS = [StandardTrack.new("words", "Words", "when each word is spoken, its value the word with its punctuation: as written when the tool aligns a known text, as heard when it transcribes (the tool says which)"), StandardTrack.new("mouth", "Mouth", "the mouth's shape over time, for lip sync, its values the tool's own (Rhubarb Lip Sync's A to H, and X at rest)"), StandardTrack.new("subtitles", "Subtitles", "the lines shown as subtitles, a cue for each, its value the line as shown")]

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.

STANDARD_TYPES = [StandardType.new("drop", "Drop", ["image", "video"] of ::String, "a place, the set things stand in front of"), StandardType.new("plug", "Plug", ["image", "video"] of ::String, "a framed moment, shown as it is: a comic panel, a hero shot"), StandardType.new("placard", "Placard", ["image", "video"] of ::String, "words shown on their own, between scenes: an intro, a title card, end credits"), StandardType.new("scrim", "Scrim", ["image", "video"] of ::String, "a frame's whole size, laid over a drop or plug"), StandardType.new("prop", "Prop", ["image", "video", "model"] of ::String, "an object the app places: a picture cropped close to its edges, of anything from a cup to a building; or a 3D model of a thing (a chair, a sword) that isn't a set or a figure"), StandardType.new("texture", "Texture", ["image", "video"] of ::String, "a material with no subject or edges, filling an area"), StandardType.new("icon", "Icon", ["image", "video"] of ::String, "a small indicator: a button, a rating"), StandardType.new("pass", "Pass", ["image", "video"] of ::String, "for programs, not people: a depth map, a mask, a matte"), StandardType.new("automaton", "Automaton", ["text", "data"] of ::String, "a character's moves as a graph of picture frames, which may follow which, walked afresh each play; its frames are props linked from it"), StandardType.new("speech", "Speech", ["audio"] of ::String, "one voice saying something, words or not (a laugh, a grunt)"), StandardType.new("dialogue", "Dialogue", ["audio"] of ::String, "voices in turns, a conversation"), StandardType.new("music", "Music", ["audio"] of ::String, "composed: a bed under a scene, or a cue (a fanfare, a sting)"), StandardType.new("ambience", "Ambience", ["audio"] of ::String, "a place's continuous sound (wind, a crowd, a hum)"), StandardType.new("effect", "Effect", ["audio"] of ::String, "one event's sound, trimmed close to it; interface sounds too"), StandardType.new("recording", "Recording", ["audio", "video"] of ::String, "anything captured or mixed, with no clear boundaries"), StandardType.new("prose", "Prose", ["text"] of ::String, "continuous writing in one voice: a description, a decree, a story"), StandardType.new("script", "Script", ["text"] of ::String, "lines for voices in turns"), StandardType.new("transcript", "Transcript", ["text"] of ::String, "the words of an audio or video asset as text (corrected by hand, with who speaks, with sounds that aren't words), linked transcript-of it; a words track is a tool's timing of the words, not their text"), StandardType.new("log", "Log", ["text"] of ::String, "a record of what happened, in order"), StandardType.new("document", "Document", ["data", "text"] of ::String, "made to be read: pages laid out, text and pictures (PDF, EPUB)"), StandardType.new("data", "Data", ["data", "text"] of ::String, "made for programs: structured (JSON, CSV) or binary (a story file, a saved game); also the catch-all"), StandardType.new("set", "Set", ["model"] of ::String, "a place in 3D, to be inside or look into"), StandardType.new("figure", "Figure", ["model", "data"] of ::String, "a character's body: a 3D model, usually rigged, or a 2D one on bones (Spine, Live2D), its pictures props linked from it"), StandardType.new("motion", "Motion", ["model", "data"] of ::String, "movement for a skeleton, without geometry"), StandardType.new("sequence", "Sequence", ["audio", "video"] of ::String, "parts arranged in time: shots cut together, clips put together into one file"), StandardType.new("font", "Font", ["font"] of ::String, "a typeface")]

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.

START_COST = 1.9
STRUCTURED_TEXT = ["application/json", "application/xml", "application/yaml", "application/x-yaml", "application/toml"] of ::String

Application types that are text to read.

TAG_COUNT_TRIGGER_NAMES = TAG_COUNT_TRIGGERS.map do |sql| sql[/TRIGGER IF NOT EXISTS (\w+)/, 1] end

The triggers' names, for recreating them.

TAG_COUNT_TRIGGERS = [" CREATE TRIGGER IF NOT EXISTS asset_tags_counted AFTER INSERT ON asset_tags BEGIN\n #{count_sql("new.asset_type", "new.tag_key", "new.tag_value", "ifnull((SELECT retired_at IS NULL FROM assets WHERE id = new.asset_id), 1)", "ifnull((SELECT retired_at IS NOT NULL FROM assets WHERE id = new.asset_id), 0)")}\n END", " CREATE TRIGGER IF NOT EXISTS asset_tags_uncounted AFTER DELETE ON asset_tags BEGIN\n #{count_sql("old.asset_type", "old.tag_key", "old.tag_value", "ifnull((SELECT retired_at IS NULL FROM assets WHERE id = old.asset_id), 1)", "ifnull((SELECT retired_at IS NOT NULL FROM assets WHERE id = old.asset_id), 0)", taking: true)}\n END", " CREATE TRIGGER IF NOT EXISTS asset_tags_recounted AFTER UPDATE OF asset_type, tag_key, tag_value ON asset_tags BEGIN\n #{count_sql("old.asset_type", "old.tag_key", "old.tag_value", "ifnull((SELECT retired_at IS NULL FROM assets WHERE id = old.asset_id), 1)", "ifnull((SELECT retired_at IS NOT NULL FROM assets WHERE id = old.asset_id), 0)", taking: true)}\n #{count_sql("new.asset_type", "new.tag_key", "new.tag_value", "ifnull((SELECT retired_at IS NULL FROM assets WHERE id = new.asset_id), 1)", "ifnull((SELECT retired_at IS NOT NULL FROM assets WHERE id = new.asset_id), 0)")}\n END", " CREATE TRIGGER IF NOT EXISTS assets_counted AFTER INSERT ON assets BEGIN\n #{count_sql("new.asset_type", "''", "''", "new.retired_at IS NULL", "new.retired_at IS NOT NULL")}\n END", " CREATE TRIGGER IF NOT EXISTS assets_uncounted BEFORE DELETE ON assets BEGIN\n DELETE FROM asset_tags WHERE asset_id = old.id;\n DELETE FROM image_meta WHERE asset_id = old.id;\n #{count_sql("old.asset_type", "''", "''", "old.retired_at IS NULL", "old.retired_at IS NOT NULL", taking: true)}\n END", " CREATE TRIGGER IF NOT EXISTS assets_type_recounted AFTER UPDATE OF asset_type ON assets\n WHEN old.asset_type <> new.asset_type BEGIN\n #{count_sql("old.asset_type", "''", "''", "old.retired_at IS NULL", "old.retired_at IS NOT NULL", taking: true)}\n #{count_sql("new.asset_type", "''", "''", "new.retired_at IS NULL", "new.retired_at IS NOT NULL")}\n #{view_count_sql("(SELECT view_name FROM image_meta WHERE asset_id = new.id)", "old.asset_type", "old.retired_at IS NULL", "old.retired_at IS NOT NULL", taking: true)}\n #{view_count_sql("(SELECT view_name FROM image_meta WHERE asset_id = new.id)", "new.asset_type", "new.retired_at IS NULL", "new.retired_at IS NOT NULL")}\n END", " CREATE TRIGGER IF NOT EXISTS assets_retired_recounted AFTER UPDATE OF retired_at ON assets\n WHEN (old.retired_at IS NULL) <> (new.retired_at IS NULL) BEGIN\n UPDATE asset_tag_counts\n SET current = current + iif(new.retired_at IS NULL, 1, -1),\n retired = retired - iif(new.retired_at IS NULL, 1, -1)\n FROM (SELECT asset_type, tag_key, tag_value FROM asset_tags WHERE asset_id = new.id\n UNION ALL SELECT new.asset_type, '', ''\n UNION ALL SELECT new.asset_type, '@view', word\n FROM (#{view_words("(SELECT view_name FROM image_meta WHERE asset_id = new.id)")})) AS t\n WHERE asset_tag_counts.asset_type = t.asset_type AND asset_tag_counts.tag_key = t.tag_key\n AND asset_tag_counts.tag_value = t.tag_value;\n END", " CREATE TRIGGER IF NOT EXISTS image_view_counted AFTER INSERT ON image_meta\n WHEN new.view_name IS NOT NULL BEGIN\n #{view_count_sql("new.view_name", "(SELECT asset_type FROM assets WHERE id = new.asset_id)", "(SELECT retired_at IS NULL FROM assets WHERE id = new.asset_id)", "(SELECT retired_at IS NOT NULL FROM assets WHERE id = new.asset_id)")}\n END", " CREATE TRIGGER IF NOT EXISTS image_view_recounted AFTER UPDATE OF view_name ON image_meta\n WHEN old.view_name IS NOT new.view_name BEGIN\n #{view_count_sql("old.view_name", "(SELECT asset_type FROM assets WHERE id = old.asset_id)", "(SELECT retired_at IS NULL FROM assets WHERE id = old.asset_id)", "(SELECT retired_at IS NOT NULL FROM assets WHERE id = old.asset_id)", taking: true)}\n #{view_count_sql("new.view_name", "(SELECT asset_type FROM assets WHERE id = new.asset_id)", "(SELECT retired_at IS NULL FROM assets WHERE id = new.asset_id)", "(SELECT retired_at IS NOT NULL FROM assets WHERE id = new.asset_id)")}\n END", " CREATE TRIGGER IF NOT EXISTS image_view_uncounted AFTER DELETE ON image_meta\n WHEN old.view_name IS NOT NULL BEGIN\n #{view_count_sql("old.view_name", "(SELECT asset_type FROM assets WHERE id = old.asset_id)", "(SELECT retired_at IS NULL FROM assets WHERE id = old.asset_id)", "(SELECT retired_at IS NOT NULL FROM assets WHERE id = old.asset_id)", taking: true)}\n END"]

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.

TAG_WALK_COST = 0.75
TEXT_META_SCHEMA = " CREATE TABLE IF NOT EXISTS text_meta (\n asset_id INTEGER PRIMARY KEY,\n content TEXT NOT NULL,\n language TEXT,\n FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE\n )"
TEXTS_SCHEMA = " CREATE TABLE IF NOT EXISTS texts (\n id INTEGER PRIMARY KEY,\n hash TEXT NOT NULL UNIQUE,\n text TEXT NOT NULL\n )"

Every wording, an asset's description or what a recipe's provider was sent, is one row, stored once (DESIGN-recipes.md).

TIME_FORMAT = "%F %T"
TRAIT_NAME = /\A[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*\z/
TYPE_NAME = /\A[a-z0-9]+(?:[_-][a-z0-9]+)*\z/
USAGE_WRITE_AT = 100

Uses counted in memory are written once there are this many (touch_asset).

VIDEO_META_SCHEMA = " CREATE TABLE IF NOT EXISTS video_meta (\n asset_id INTEGER PRIMARY KEY,\n width INTEGER,\n height INTEGER,\n duration_ms INTEGER,\n frame_rate REAL,\n usage_class TEXT,\n has_audio INTEGER DEFAULT 0,\n compression TEXT DEFAULT 'original',\n quality_floor INTEGER,\n target_format TEXT,\n presentation TEXT,\n FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE\n )"
VOCABULARY = [{name: "rating", title: "Rating", label: "", kind: "scale", description: "who a thing is suitable for, the same in every app", wording: "rating", values: [{value: "g", title: "G", description: "general audiences", family: "", words: "Family-friendly and wholesome, suitable for all ages."}, {value: "pg", title: "PG", description: "parental guidance", family: "", words: "Suitable for older children: mild peril and action, kept clean."}, {value: "r", title: "R", description: "restricted", family: "", words: "For mature audiences: violence, blood and risqué scenes may be shown."}, {value: "x", title: "X", description: "adults only", family: "", words: "For adults only."}]}, {name: "subtype", title: "Subtype", label: "", kind: "", description: "an app's own kind of a standard type, in its own words: a room, a door view, a background", wording: "", values: [] of ::NamedTuple(value: String, title: String, description: String, family: String, words: String)}]

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.

VOCABULARY_OWNER = "dd"

The owner of DataDungeon's own wordings, the ratings' words.

VOCABULARY_SCHEMAS = " CREATE TABLE IF NOT EXISTS vocabulary_tags (\n name TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n label TEXT NOT NULL DEFAULT '',\n kind TEXT NOT NULL DEFAULT '',\n description TEXT NOT NULL\n ) WITHOUT ROWID;\n CREATE TABLE IF NOT EXISTS vocabulary_values (\n tag TEXT NOT NULL REFERENCES vocabulary_tags(name),\n value TEXT NOT NULL,\n title TEXT NOT NULL,\n description TEXT NOT NULL,\n family TEXT NOT NULL DEFAULT '',\n wording_id INTEGER REFERENCES named_texts(id),\n position INTEGER NOT NULL,\n PRIMARY KEY (tag, value)\n ) WITHOUT ROWID"
WALK_COST = 0.44
WORDING_TRIGGERS = [" CREATE TRIGGER IF NOT EXISTS named_texts_kept_update BEFORE UPDATE ON named_texts\n BEGIN SELECT RAISE(ABORT, 'a wording never changes: new words are a new version, a new name a new wording'); END", " CREATE TRIGGER IF NOT EXISTS named_texts_kept_delete BEFORE DELETE ON named_texts\n BEGIN SELECT RAISE(ABORT, 'a wording is never deleted: recipes name it'); END"]

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

new(path : String, storage : Storage | Nil = nil, cache_root : String | Nil = nil, *, read_only : Bool = false, consumer : String | Nil = nil)

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

Source

Class methods

flush_open_usage

Writes the uses waiting in every open DB, as at exit. :nodoc:

Source
media_of(content_type : String) : String

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".

Source
standard_type?(name : String) : Bool

Whether a type's name is one of DataDungeon's (STANDARD_TYPES).

Source

Instance methods

add_wordings(id : Int64, wordings : Array(Recipe::Wording), step : Int32 = 0) : Int32

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.

Source
adopt(rec : AssetRecord) : AssetRecord

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.

Source
asset_exists?(key : String) : Bool

Whether a current asset has this key in its type's own index.

Source
asset_types(retired : Bool = false) : Hash(String, Int32)

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.

Source
attach_indexer(indexer : AssetIndexer) : Nil
Source
check_consistency

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.

Source
close

Writes the uses not yet written (touch_asset), then closes.

Source
connection

Shared connection for extensions that keep their tables in the asset DB.

Source
consumer

Who is using the store ("cattacula"), which says whose indexes register_index creates and changes.

Source
count_pool_assets(asset_type : String, filter_tags : Hash(String, String)) : Int32
Source
custom_traits(asset_id : Int64) : Array(String)

The tags of an asset whose values are custom.

Source
custom_values(asset_type : String, trait : String) : Hash(String, Int32)

How often each custom value of a trait occurs among assets of a type, most frequent first: candidates for becoming standard choices.

Source
delete_asset(id : Int64) : Nil

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.

Source
detach_indexer(indexer : AssetIndexer) : Nil

Forget an attached indexer; it calls this when it closes.

Source
eviction_candidates(limit : Int32 = 100) : Array(AssetRecord)
Source
find(asset_type : String, traits : Hash(String, String), custom : Array(String) = [] of String) : Matches

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.

Source
find_asset(key : String) : AssetRecord | Nil

Of the current, unflagged assets with this key in its type's own index, the preferred (PREFERRED), without get's file check or touch.

Source
find_asset_by_id(id : Int64) : AssetRecord | Nil
Source
find_assets_by_tags(asset_type : String, filter_tags : Hash(String, String)) : Array(AssetRecord)

Assets of a type carrying every one of these tags (and possibly others).

Source
find_by_hash(file_hash : String) : Array(AssetRecord)

The assets holding these bytes, by their SHA-256, oldest first. Identical bytes share one object, so several assets can hold it.

Source
find_group(asset_type : String, traits : Hash(String, String), size : Int32 | Nil = nil) : Array(AssetRecord)

Every asset whose first size traits (by default, the group traits) equal these: one key-prefix range lookup. Later traits are ignored.

Source
find_group(prefix : String) : Array(AssetRecord)

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.

Source
find_pool(asset_type : String, traits : Hash(String, String), custom : Array(String) = [] of String) : Array(AssetRecord)

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.

Source
find_pool(key : String) : Array(AssetRecord)

Every current asset with this key in its type's own index. A key names its type, so the key alone is enough.

Source
flush_usage(wait : Bool = true) : Bool

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.

Source
get(asset_type : String, traits : Hash(String, String), touch : Bool = true) : AssetRecord | Nil

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.

Source
get(key : String, touch : Bool = true) : AssetRecord | Nil

get by a key in a type's own index; the key names its type.

Source
get_by_id(id : Int64, touch : Bool = true) : AssetRecord | Nil

One asset by its id, retired or flagged too.

Source
get_with_meta(asset_type : String, traits : Hash(String, String), touch : Bool = true) : Tuple(AssetRecord, AssetMeta | Nil) | Nil

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.

Source
get_with_meta(key : String, touch : Bool = true) : Tuple(AssetRecord, AssetMeta | Nil) | Nil

get_with_meta by a key in a type's own index.

Source
get_with_meta_by_id(id : Int64, touch : Bool = true) : Tuple(AssetRecord, AssetMeta | Nil) | Nil
Source
group_key_for(asset_type : String, traits : Hash(String, String), size : Int32 | Nil = nil) : String

The key prefix shared by assets whose first size traits (by default, the group traits) equal these. Later traits are ignored.

Source
indexes(asset_type : String) : Array(AssetIndex)

Every consumer's indexes on a type (not the type's own).

Source
key_clause(asset_type : String, key : String, *, prefix : Bool = false) : Tuple(String, Array(::DB::Any))

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:

Source
key_for(asset_type : String, traits : Hash(String, String)) : String

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.

Source
load_audio_meta(asset_id : Int64) : AudioMeta | Nil
Source
load_image_meta(asset_id : Int64) : ImageMeta | Nil
Source
load_image_meta_batch(ids : Array(Int64)) : Hash(Int64, ImageMeta)

The image fields of these assets, those that have them, in one query.

Source
load_keyframes(id : Int64) : Array(ImageKeyframe)

An animation's checkpoints, in time order.

Source
load_tags(asset_id : Int64) : Hash(String, String)
Source
load_tags_batch(asset_ids : Array(Int64)) : Hash(Int64, Hash(String, String))
Source
load_text_meta(asset_id : Int64) : TextMeta | Nil
Source
load_video_meta(asset_id : Int64) : VideoMeta | Nil
Source
match_field_values(ids : Array(Int64)) : Hash(Int64, Hash(String, String))

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.

Source
measure_images(asset_type : String | Nil = nil, all : Bool = false) : Int32

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.

Source
named_texts(kind : String, owner : String) : Array(Tuple(String, String, String))

The wordings an owner has of a kind: {name, version, text}, by name, oldest first.

Source
normalize_tags(asset_type : String, tags : Hash(String, String)) : Hash(String, String)

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.

Source
object_keys

Every object key this store's rows use, retired assets' included: what a program clearing a shared bucket keeps (Storage#shared?).

Source
open(rec : AssetRecord, & : IO -> ) : Nil

Stream the record's object: the block gets an IO over its bytes, from whichever storage is configured, without loading it all into memory.

Source
outside_store

The rows that index a file outside the store: those store_asset recorded by an absolute path the caller owns. adopt moves them in.

Source
path

The database file.

Source
path_for(rec : AssetRecord) : String

Resolve a record's storage key to a usable location (local path or URL).

Source
promote(asset_type : String, trait : String, value : String) : Int64

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.

Source
prune(asset_type : String, keep_last : Int32 = 0, retired_before : Time | Nil = nil) : Int32

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.

Source
random_pool_text(asset_type : String, filter_tags : Hash(String, String)) : String | Nil
Source
rating_text(rating : String) : Recipe::Wording

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.

Source
read(rec : AssetRecord) : Bytes

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.

Source
read_only?
Source
recipe(id : Int64) : Recipe | Nil

The asset's recipe, steps and all, with each wording as owner:name@version; nil when it has none.

Source
refresh_registrations

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.

Source
register_index(asset_type : String, name : String, *, traits : Array(String), rekey : Bool = false) : AssetIndex

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.

Source
register_type(asset_type : String, *, pool_traits : Array(String), group_traits : Array(String) = [] of String, labels : Hash(String, String) = {} of String => String, sets : Array(String) = [] of String, facets : Hash(String, Array(String)) = {} of String => Array(String), scales : Hash(String, Array(String)) = {} of String => Array(String), colors : Array(String) = [] of String, weights : Hash(String, Float64) = {} of String => Float64, cycles : Hash(String, Array(String)) = {} of String => Array(String), match_fields : Array(String) = [] of String, embed : Array(String) = [] of String, rekey : Bool = false) : AssetType

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.

Source
registration(asset_type : String) : AssetType | Nil

The registration of an asset type, or nil when it has none.

Source
reinstate(id : Int64) : Bool

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.

Source
rename_tag(asset_type : String, from : String, to : String) : Int32

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.

Source
rename_type(from : String, to : String) : Int32

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

Source
retire(id : Int64) : Bool

Retire an asset with nothing replacing it: a soft, reversible delete. Returns whether it was current.

Source
set_keyframes(id : Int64, keyframes : Array(ImageKeyframe)) : Array(ImageKeyframe)

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.

Source
set_recipe(id : Int64, recipe : Recipe, replace : Bool = false, drop_generation : Bool = false) : Nil

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.

Source
set_track(id : Int64, name : String, cues : Array(TrackCue), *, tool : String) : Track

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.

Source
storage
Source
store(asset_type : String, tags : Hash(String, String) = {} of String => String, *, source_path : String | Nil = nil, bytes : Bytes | Nil = nil, content_type : String, media : String | Nil = nil, permanent : Bool = false, generator : String | Nil = nil, generation : String | Nil = nil, description : String | Nil = nil, generated : Bool = false, image : ImageMeta | Nil = nil, audio : AudioMeta | Nil = nil, video : VideoMeta | Nil = nil, text : TextMeta | Nil = nil, custom : Array(String) = [] of String, made_at : Time | Nil = nil, supersedes : Int64 | Nil = nil, recipe : Recipe | Nil = nil) : AssetRecord

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.

Source
store_asset(asset_type : String, media : String, content_type : String, file_path : String, tags : Hash(String, String) = {} of String => String, file_hash : String | Nil = nil, permanent : Bool = false, generator : String | Nil = nil, generation : String | Nil = nil, description : String | Nil = nil, generated : Bool = false, image : ImageMeta | Nil = nil, audio : AudioMeta | Nil = nil, video : VideoMeta | Nil = nil, text : TextMeta | Nil = nil, custom : Array(String) = [] of String, made_at : Time | Nil = nil, supersedes : Int64 | Nil = nil, recipe : Recipe | Nil = nil) : Int64

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.

Source
store_webp(id : Int64, tags : Hash(String, String) = {} of String => String, *, quality : Int32 = WebP::QUALITY.to_i) : AssetRecord

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

Source
supersede(new_id : Int64, old_id : Int64) : Nil

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.

Source
tag_keys(asset_type : String, retired : Bool | Nil = false) : Hash(String, Int32)

A type's tag names, each with how many of its current assets (or, with retired, as list_assets takes it, others) carry it.

Source
tag_values(asset_type : String, tag_key : String, limit : Int32 = 100, retired : Bool | Nil = false) : Hash(String, Int32)

The values one tag takes across a type's current assets (or, with retired, others), most frequent first, each with how many carry it.

Source
touch_asset(id : Int64) : Nil

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.

Source
track(id : Int64, name : String) : Track | Nil

One of an asset's tracks, or nil.

Source
tracks(id : Int64) : Array(Track)

An asset's tracks, by name.

Source
unregister_index(asset_type : String, name : String) : Bool

Drop one of this consumer's indexes, and its keys. Returns whether there was one.

Source
update_asset(id : Int64, *, tags : Hash(String, String) | Nil = nil, description : String | Nil = nil, everywhere : Bool = false, flagged : Bool | Nil = nil, rating : Int32 | Nil = nil, image : ImageMeta | Nil = nil, permanent : Bool | Nil = nil) : AssetRecord

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.

Source
vacuum(delete_missing_rows : Bool = false, delete_orphan_objects : Bool = false) : ConsistencyReport

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.
Source
view_counts(asset_type : String, retired : Bool | Nil = false) : Hash(String, Int32)

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.

Source
vocabulary

The shared tags by name, each with its values in the agreed order.

Source
with_text(kind : String, name : String) : Array(Int64)

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.

Source

Nested types