Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

tr_treedb

Graph memory database with hook/fkey relationships, persisted through timeranger2. See the TreeDB crash course for link/unlink rules and g_rowid semantics.

Source code:

_treedb_create_topic_cols_desc()

The _treedb_create_topic_cols_desc() function creates and returns a JSON object describing the column schema for a TreeDB topic.

json_t *_treedb_create_topic_cols_desc(void);

Parameters

KeyTypeDescription
--This function does not take any parameters.

Returns

A JSON list describing what a user column may declare. The return is yours — every caller decrefs it (parse_schema(), treedb_open_db()).

Notes

It is DERIVED from the cols topic of treedb_system_schema, not written by hand: value is renamed back to id, and the storage-only fields of that topic (id, topics, order, _geometry) are dropped, because they say how a column is stored in __system__ and not what a column may declare. A field added there for user columns needs nothing here; a storage-only one has to be added to that skip list too.


add_jtree_path()

The add_jtree_path() function appends a child node to a parent node in a hierarchical JSON tree structure.

int add_jtree_path(
    json_t *parent,  // not owned
    json_t *child    // not owned
);

Parameters

KeyTypeDescription
parentjson_t *A pointer to the parent JSON node. This parameter is not owned by the function.
childjson_t *A pointer to the child JSON node to be added. This parameter is not owned by the function.

Returns

Returns 0 on success, or a negative error code if the operation fails.

Notes

The function does not take ownership of the parent or child nodes. This means the caller is responsible for managing their memory.


create_template_record()

create_template_record() generates a new template record based on the provided column definitions and input data.

json_t *create_template_record(
    const char *template_name, // used only for log
    json_t *cols,       // NOT owned
    json_t *kw          // Owned
);

Parameters

KeyTypeDescription
template_nameconst char *The name of the template, used only for logging purposes.
colsjson_t *A JSON object describing the column definitions. This parameter is not owned by the function.
kwjson_t *A JSON object containing the input data for the template record. This parameter is owned by the function.

Returns

Returns a JSON object representing the created template record.

Notes

The returned JSON object must be decremented (json_decref()) by the caller when no longer needed.


current_snap_tag()

Retrieves the current snapshot tag of the specified treedb_name in the given tranger instance.

int current_snap_tag(
    json_t  *tranger,
    const char  *treedb_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the tree database.
treedb_nameconst char *Name of the tree database whose current snapshot tag is to be retrieved.

Returns

Returns the current snapshot tag as an integer.

Notes

The snapshot tag is used to track versions of the tree database.


decode_child_ref()

Parses a child reference string formatted as ‘child_topic_name^child_id’ and extracts its components into separate buffers.

BOOL decode_child_ref(
    const char *pref,
    char *topic_name,    int topic_name_size,
    char *id,           int id_size
);

Parameters

KeyTypeDescription
prefconst char *The child reference string in the format ‘child_topic_name^child_id’.
topic_namechar *Buffer to store the extracted child topic name.
topic_name_sizeintSize of the ‘topic_name’ buffer.
idchar *Buffer to store the extracted child ID.
id_sizeintSize of the ‘id’ buffer.

Returns

Returns TRUE if the reference was successfully parsed, otherwise returns FALSE.

Notes

This function is used to extract child references from hierarchical tree structures in the TreeDB system.

A part that does not fit its buffer is refused, not cut: the function logs “Wrong reference: a part of it is too long” and returns FALSE (new after 7.25.4; it cut the part in silence, and the cut id named another node or none). Decode into buffers of NAME_MAX. A child reference is what a hook gives with the refs option, for example "users^alice":

char topic_name[NAME_MAX], id[NAME_MAX];
if(!decode_child_ref("users^alice",
        topic_name, sizeof(topic_name), id, sizeof(id))) {
    // malformed, or a part too long (logged)
}
// topic_name is "users", id is "alice"

The refs option lists every child of the hook, also a child whose id holds a ^ or is NAME_MAX bytes long (a topic without hooks keeps such an id): "users^a^b". decode_child_ref() refuses that reference and logs why. In 7.25.4 the reference of a long id was cut in silence, and named another node or none.


decode_parent_ref()

Parses a parent reference string into its components: topic name, ID, and hook name. The reference format is ‘parent_topic_name^parent_id^hook_name’.

BOOL decode_parent_ref(
    const char *pref,
    char *topic_name,    int topic_name_size,
    char *id,           int id_size,
    char *hook_name,    int hook_name_size
);

Parameters

KeyTypeDescription
prefconst char *The parent reference string in the format ‘parent_topic_name^parent_id^hook_name’.
topic_namechar *Buffer to store the extracted topic name.
topic_name_sizeintSize of the topic_name buffer.
idchar *Buffer to store the extracted parent ID.
id_sizeintSize of the id buffer.
hook_namechar *Buffer to store the extracted hook name.
hook_name_sizeintSize of the hook_name buffer.

Returns

Returns TRUE if the reference was successfully parsed, otherwise returns FALSE.

Notes

This function is used to extract structured information from a parent reference string, which is used in hierarchical relationships within the tree database.

A part that does not fit its buffer is refused, not cut: the function logs “Wrong reference: a part of it is too long” and returns FALSE (new after 7.25.4). Decode into buffers of NAME_MAX:

char topic_name[NAME_MAX], id[NAME_MAX], hook_name[NAME_MAX];
if(!decode_parent_ref("departments^direction^users",
        topic_name, sizeof(topic_name), id, sizeof(id), hook_name, sizeof(hook_name))) {
    // malformed, or a part too long (logged)
}

node_collapsed_view()

Generates a collapsed view of a node in the tree database, applying filtering and transformation options.

json_t *node_collapsed_view(
    json_t *tranger,    // NOT owned
    json_t *node,       // NOT owned
    json_t *jn_options  // owned, fkey, hook options
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the tree database. Not owned by the caller.
nodejson_t *Pointer to the node to be collapsed. Not owned by the caller.
jn_optionsjson_t *JSON object containing options for collapsing the node, including fkey and hook options. Owned by the caller.

Returns

A JSON object representing the collapsed view of the node. The caller must decrement the reference when done.

Notes

The function applies filtering and transformation rules based on jn_options to generate a simplified representation of the node.


parse_hooks()

parse_hooks() processes the schema to extract and validate hook definitions.

int parse_hooks(
    json_t *schema  // not owned
);

Parameters

KeyTypeDescription
schemajson_t *A JSON object representing the schema. It is not owned by the function.

Returns

Returns 0 on success or a negative number if an error occurs during parsing.

Notes

This function makes sure that hooks in the schema are correctly defined and structured.


parse_schema()

parse_schema() validates and processes a JSON schema definition. This makes sure of its structure and integrity.

int parse_schema(
    json_t *schema  // not owned
);

Parameters

KeyTypeDescription
schemajson_t *A JSON object representing the schema definition. The caller retains ownership.

Returns

Returns 0 on success, or a negative error code if the schema is invalid.

Notes

This function does not modify the input schema and does not take ownership of it.

A column flagged both hook and fkey is refused (“A column cannot be both ‘hook’ and ‘fkey’”). A node that is both child and parent carries two columns, the hook and the fkey:

'manager': {
    'header': 'Manager',
    'type': 'array',
    'flag': ['fkey']
},
'managers': {
    'header': 'Managers',
    'type': 'object',
    'flag': ['hook'],
    'hook': {
        'departments': 'manager'
    }
}

parse_schema_cols()

parse_schema_cols() validates and processes the column definitions in a schema. This makes sure of correctness and consistency.

int parse_schema_cols(
    json_t *cols_desc,  // NOT owned
    json_t *data        // owned
);

Parameters

KeyTypeDescription
cols_descjson_t *A JSON object describing the schema columns. This parameter is not owned by the function.
datajson_t *A JSON object containing the column data to be validated. This parameter is owned by the function.

Returns

Returns 0 if the schema columns are valid, or a negative number indicating the number of errors encountered.

Notes

The function makes sure that the column definitions conform to the expected schema format. If errors are found, the return value indicates the number of issues detected.


set_volatil_values()

The set_volatil_values() function assigns volatile values to a record in the TreeDB. This makes sure that non-persistent fields are set using default values if not provided.

int set_volatil_values(
    json_t *tranger,
    const char *topic_name,
    json_t *record,
    json_t *kw,
    BOOL broadcast
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the TimeRanger database instance.
topic_nameconst char *The name of the topic to which the record belongs.
recordjson_t *The record to update with volatile values. This parameter is not owned by the function.
kwjson_t *A JSON object containing the values to be set. This parameter is not owned by the function.

Returns

Returns 0 on success, or a negative error code if an issue occurs.

Notes

This function does not modify foreign key (fkey), hook, or persistent fields. It only updates non-persistent attributes.


treedb_activate_snap()

Marks a previously shot snapshot as active (active: true on the snap node in __snaps__). Use the reserved name "__clear__" to instead deactivate whichever snap is currently active — this is the deactivate-snap path.

int treedb_activate_snap(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *snap_name
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the TimeRanger instance managing the TreeDB.
treedb_nameconst char *The name of the TreeDB where the snapshot is stored.
snap_nameconst char *The name of the snapshot to activate, or "__clear__" to deactivate the current active snap.

Returns

Returns the snapshot tag (snap id = its g_rowid in __snaps__) as an integer on success, 0 for the "__clear__" deactivate path, or a negative value on error (-1 if the named snap is not found, on a replica, or when a snap node cannot be saved).

A save that fails changes nothing, in memory either: the snap that was active stays active, and the one being activated stays inactive. Until 7.25.3 the "__clear__" path answered 0 with the save failed, and the snap went on being active on disk.

An activation saves the active snap inactive FIRST, then the new one active. When the second save fails, the old snap is saved active again and the error says so (“Cannot activate snap, the one active before is active again”); until 7.25.4 it stayed inactive, and the treedb was left with no active snap. Only if that save fails too is no snap active, on disk and in memory, and the error says that instead.

if(treedb_activate_snap(tranger, "treedb_agent", "__clear__") < 0) {
    // still active: the cause is in the log (and gobj_log_last_message())
}

Only one snap can be active. When the treedb finds two or more snaps marked active (at open, and when a snap is shot or activated), it logs “Too much actives tags”, keeps the last one active, and deactivates and saves the others. A deactivation that cannot be saved puts the flag back: that snap stays active in memory, as on disk, and an ERROR says so (“Cannot deactivate a snap of too many active ones, it stays active on disk”). A replica does not repair: it uses the last one, as the master does, and leaves the repair to the master. 7.25.4 marked the snap inactive in memory whatever the save said, and the next open found it active again.

/*  __snaps__ on disk: s1 and s2 both "active": true;
 *  the files of the key of s1 are read-only                               */
treedb_open_db(tranger, "treedb_agent", jn_schema, "persistent");
/*  ERROR "Too much actives tags", then "Cannot deactivate a snap of too
 *  many active ones, it stays active on disk"; the treedb loads under s2,
 *  and s1 still says "active": true in memory                            */

Behavior

This call only toggles the active flag on the snap node. The primary index of every topic is not refreshed in memory. The new visibility takes effect on the next treedb_open_db():

Consumers that need the change visible immediately must close and reopen the resource (for example gobj_stop + gobj_start on the gclass that owns the treedb, which is what the agent’s restart_nodes does).

Working from an activated snap

An activation is a filtered load, not a restore: nothing is rewritten and nothing is undone, so the store keeps every record it had and only the answer of the primary index changes. It serves two purposes — looking at a state that was marked as good, and carrying on working from it. The second one has a rule:

Working from an activated snap ignores everything written after the shot, for every key you touch, and it ignores it without destroying it.

Reading a node under snap S gives the record S froze. Saving it appends a new record, tagged 0, whose content is the photo’s plus the change — never the content of the records written in between. That record is the newest of its key, so it becomes the primary after the deactivation and its reload; the records in between stay on disk and in the secondary indexes, and stop being read as the current state.

From inside an activated snapWhat it does to what came after
readhides it: the primary answers with the photo
update / saveignores it for that key: the new record descends from the photo
create of an id born after the shotthe same, by another door: the id is absent from the FILTERED primary index, so the create is accepted and appends over the record already there
deletedestroys it: treedb_delete_node() refuses only what a snap holds, and a node born after the shot is held by none — the key is erased and deactivating does not bring it back

It is per key, not per store: a node never touched keeps the record written after the shot as its newest, so the deactivation brings it back as it was. The activation does not put the treedb into a past state, it makes the past the thing you write from.

/*  binaries^ycommand: 7.21.0 at the shot, 7.23.0 installed after it  */
treedb_activate_snap(tranger, treedb_name, "pre-upgrade");   // + reload
json_t *node = treedb_get_node(tranger, treedb_name, "binaries", "ycommand");
/*  node says 7.21.0, not 7.23.0                                      */
treedb_update_node(tranger, node, json_pack("{s:s}", "description", "patched"), TRUE);
/*  the appended record says 7.21.0 + the new description: the 7.23.0
    record is still on disk and in the pkey2 index, and is no longer
    what the primary index answers after the deactivation.            */
treedb_activate_snap(tranger, treedb_name, "__clear__");     // + reload

The full account, with the worked walk of one key, is in Snapshots.

Notes

Make sure that the snapshot exists before calling treedb_activate_snap() (except for "__clear__", which is always valid).


treedb_autolink() automatically links a node using foreign key fields from the provided JSON object.

int treedb_autolink(
    json_t  *tranger,
    json_t  *node,   // NOT owned, pure node
    json_t  *kw,     // owned
    BOOL    save
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *Pointer to the node to be linked. This node is not owned by the function.
kwjson_t *JSON object containing foreign key fields used for auto-linking. This object is owned by the function.
saveBOOLFlag indicating whether to save the changes to the database.

Returns

Returns 0 on success, or a negative error code on failure.

Notes

The function uses the foreign key fields in kw to establish links between nodes. If save is TRUE, the changes are persisted in the database. When that save fails, or a link fails, every link of the call is taken back in memory and no event is told (see treedb_link_nodes()). The node parameter must be a valid pure node object.

The bytes of the file columns of kw are stored BEFORE any link moves. Storing them can write an asset node (a new name of an asset is an update of that node). That is a write of its own: it is on disk, and its event is told, whatever the autolink does after.

A ref is linked through the hook it names, and that hook must fill the column the ref arrives in, as treedb_replace_links() asks. A ref whose hook fills ANOTHER column is refused (“fkey reference: its hook does not link into this column”): the autolink answers -1, and nothing moves. 7.25.4 linked it through the column the hook fills, a column kw did not name.

/*  departments: 'department_id' (fkey) and 'unit_of' (fkey);
 *  the hook 'units' of departments fills 'unit_of'                        */
json_t *kw = json_pack("{s:s}", "department_id", "departments^sales^units");
treedb_autolink(tranger, admin, kw, TRUE);     // -1, nothing moved

It only ADDS links, and it stops at the first ref it cannot link. To make the links of a node equal to the ones a record names, use treedb_replace_links(): it does not touch the links that do not change, and a bad ref does not stop the others. C_NODE’s update-node with autolink uses treedb_update_node_and_links(), which replaces the links as treedb_replace_links() does, not treedb_clean_node() + treedb_autolink().

/*  alice hangs from nobody: link her to direction, and save her  */
json_t *kw = json_pack("{s:s, s:[s]}",
    "id", "alice",
    "departments", "departments^direction^users"
);
if(treedb_autolink(tranger, alice, kw, TRUE) < 0) {
    // Error already logged: alice hangs from nobody, and no event was told
}

treedb_blob_path()

Where the bytes of an asset of a file column live: <treedb dir>/.blobs/ab/cd/<id>.<ext>. The id is the lowercase sha256 of the bytes; the two fanout levels are its first four hex characters, and the extension comes from the stored content type (treedb_file_ext()), never from the name the file was given. The directory starts with a dot so no scan of the treedb directory takes it for a topic.

int treedb_blob_path(
    json_t      *tranger,
    const char  *id,
    const char  *content_type,
    char        *bf,
    size_t      bflen
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb (its directory is the root).
idconst char *The asset id: 64 lowercase hex characters.
content_typeconst char *The stored mime type; it picks the extension.
bfchar *Output buffer.
bflensize_tSize of bf (PATH_MAX).

Returns

0, or -1 with “Asset id is not a lowercase sha256” logged when id is not one, or when the path does not fit.

Example

char path[PATH_MAX];
treedb_blob_path(tranger,
    "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
    "image/png", path, sizeof(path));
/* <directory>/.blobs/ba/78/ba7816bf...15ad.png */

treedb_clean_node()

treedb_clean_node() removes all foreign key links from a given node in the tree database, effectively disconnecting it from its parent and child relationships.

int treedb_clean_node(
    json_t  *tranger,
    json_t  *node,   // NOT owned, pure node
    BOOL     save
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the TimeRanger database instance.
nodejson_t *The target node to be cleaned. This node is not owned by the function.
saveBOOLIf TRUE, the changes are saved to the database.

Returns

Returns 0 on success, or a negative error code on failure.

Notes

This function only removes foreign key links. It does not delete the node itself. If save is TRUE, the changes are persisted in the database. When that save fails, or an unlink fails, every unlink of the call is taken back in memory and no event is told (see treedb_link_nodes()).


treedb_close_db()

Closes the TreeDB instance identified by treedb_name in the given json_t * tranger. This function makes sure that all resources associated with the TreeDB instance are properly released.

int treedb_close_db(
    json_t *tranger,
    const char *treedb_name
);

Parameters

KeyTypeDescription
trangerjson_t *A pointer to the json_t * object representing the TimeRanger instance.
treedb_nameconst char *The name of the TreeDB instance to be closed.

Returns

Returns 0 on success, or a negative error code if the operation fails.

Notes

Make sure that treedb_open_db() was previously called before attempting to close the TreeDB instance.


treedb_close_topic()

Closes the specified topic in the TreeDB system. This makes sure that all associated resources are properly released.

int treedb_close_topic(
    json_t  *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB containing the topic to be closed.
topic_nameconst char *Name of the topic to be closed.

Returns

Returns 0 on success, or a negative error code if the operation fails.

Notes

Make sure that the topic is not in use before calling treedb_close_topic().


treedb_content_type_of_name()

The mime type a file NAME claims, by its extension (case-insensitive). The pairs that share a container are told apart by extension on purpose: .webm is video and .weba audio, .mp4 video and .m4a audio, .ogv video and .ogg audio. A name is only a claim: the write path checks it against the bytes (treedb_sniff_content_type()).

const char *treedb_content_type_of_name(
    const char  *name
);

Parameters

KeyTypeDescription
nameconst char *A file name or path.

Returns

A static string: one of image/jpeg, image/png, image/webp, image/gif, application/pdf, video/mp4, video/webm, video/quicktime, video/ogg, video/x-matroska, audio/mpeg, audio/mp4, audio/ogg, audio/wav, audio/webm, audio/flac. "" when the name has no extension or an unknown one.

Example

treedb_content_type_of_name("nave-1.JPG");     /* "image/jpeg" */
treedb_content_type_of_name("voice.m4a");      /* "audio/mp4" */
treedb_content_type_of_name("notes.txt");      /* "" */

treedb_create_node()

Creates a new node in the TreeDB. The node is stored in tranger under the specified treedb_name and topic_name.

json_t *treedb_create_node(
    json_t       *tranger,
    const char   *treedb_name,
    const char   *topic_name,
    json_t       *kw // owned
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
treedb_nameconst char *Name of the TreeDB where the node will be created.
topic_nameconst char *Name of the topic under which the node will be stored.
kwjson_t *JSON object containing the attributes of the new node. This parameter is owned by the function.

Returns

Returns a JSON object representing the newly created node. WARNING: The returned object is NOT owned by the caller and must not be modified or freed.

Notes

This function creates a ‘pure node’ without loading hook links. The primary key (pkey) of all topics must be ‘id’, and it must be a string.

id is mandatory, but the topic can hand it out. When the kw carries no id, the flag on the id column decides what the new one is:

Flag on idThe id the topic hands out
uuidA random UUID.
rowidOne past every id the topic ever handed out. An id is never reused, not even after its node is deleted: a snap’s id rides the records it tagged. The counter lives in the topic’s topic_var.json (last_rowid_id) and survives a topic_version change, which re-creates that file from the schema. It is raised past every numeric key of the TOPIC (tranger2’s cache, every key on disk), not past the treedb’s index: with a snap active that index holds only what the snap loaded, and a node created after the shot got its id handed out again (fixed after 7.24.1).
qualifiedThe id of the parent, a dot, and the name of the record. The name is the first secondary key of the topic (pkey2s). The parent is the one named in the fkey of the kw.

A column carries at most one of the three. With none of them, a kw with no id is an error. A qualified id is an error too when the kw carries no secondary key, when it carries no parent fkey, or when the composed id is longer than a record key: a key too long is refused, never trimmed, because a truncated id is the address of another node. In all of these the function logs the cause and returns NULL.

The id of a node of a topic with hooks must make a reference. Its children hold it in their fkeys as topic^id^hook, so such an id cannot hold a ^ (the separator), and it must be shorter than NAME_MAX (a reference is decoded into parts of NAME_MAX). Either is refused, with “Invalid ‘id’: it holds a ‘^’, the separator of a reference” or “Invalid ‘id’: too long to be part of a reference” (new after 7.25.4; they were accepted, and every reference to the node was undecodable, or cut in silence). A topic with no hooks is not named by any reference, and keeps any id a key can be:

treedb_create_node(tranger, "my_db", "departments", json_pack("{s:s}", "id", "a^b"));  // NULL
treedb_create_node(tranger, "my_db", "users", json_pack("{s:s}", "id", "a^b"));        // created: no hooks

A pkey2 value is indexed as the record holds it. When the kw does not carry the pkey2 column, or carries it as another type than a string, the create takes the value the record gets (the column’s default, a wild conversion), and looks up and fills the instance of that value. Until 7.25.4 it used the raw value of the kw: a column with a default filled the slot of "" while the record held the default, and the instance of the default was missing until a reload (new after 7.25.4).

// 'version' is a pkey2 with 'default': 'v0'
json_t *d = treedb_create_node(tranger, "my_db", "defaulted", json_pack("{s:s}", "id", "d"));
treedb_get_instance(tranger, "my_db", "defaulted", "version", "d", "v0");   // d

See the TreeDB crash course §3.3 for the flags and §3.11 for why the schema topics are keyed this way.


treedb_create_topic()

treedb_create_topic() creates a new topic in the TreeDB with the specified schema and primary key constraints.

json_t *treedb_create_topic(
    json_t       *tranger,
    const char   *treedb_name,
    const char   *topic_name,
    int          topic_version,
    const char   *topic_tkey,
    json_t       *pkey2s,      // owned, string or dict of string | [strings]
    json_t       *jn_cols,     // owned
    uint32_t     snap_tag,
    BOOL         system_topic, // TRUE: topic cannot be deleted (persisted in topic_var)
    BOOL         create_schema
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB where the topic will be created.
topic_nameconst char *Name of the topic to be created.
topic_versionintVersion number of the topic schema.
topic_tkeyconst char *Topic key used for indexing.
pkey2sjson_t *Primary key(s) for the topic, either a string or a dictionary of strings.
jn_colsjson_t *JSON object defining the schema of the topic, including field types and attributes.
snap_taguint32_tSnapshot tag associated with the topic creation.
system_topicBOOLIf TRUE, the topic is marked non-deletable: treedb_delete_topic() refuses it and force does NOT override. The flag is persisted in topic_var.json (metadata, not a data column), so it survives reload and needs no topic_version bump.
create_schemaBOOLFlag indicating whether to create the schema if it does not exist.

Returns

Returns a JSON object representing the created topic. WARNING: The returned object is not owned by the caller.

Notes

The primary key (pkey) of all topics must be id. This function does not load hook links. The returned JSON object must not be modified or freed by the caller.

The __system__ structural topics and the per-treedb __snaps__ / __graphs__ topics are created with system_topic = TRUE. See treedb_set_node_immutable() for marking individual records (rather than whole topics) non-deletable.

There is no parameter for the main topic. That mark comes only from the schema that treedb_open_db() reads.

A topic with a column flagged both hook and fkey is refused, and the function returns NULL. See parse_schema().

So is a topic whose columns would not open: no columns, no id column, or a column that fails the validation parse_schema() applies when a schema is read. The function logs “Topic refused: bad columns” with the reason, sets the last message, and creates nothing on disk. Until 7.24.1 the topic was written first and validated after, so a caller was told “Topic created!” for a topic the next open could not load.

/*  refused: no `id` column  */
treedb_create_topic(tranger, "my_db", "things", 1, "", 0,
    json_pack("{s:{s:s, s:s, s:[s]}}",    /* cols, keyed by column name */
        "name", "header", "Name", "type", "string", "flag", "persistent"),
    0, FALSE, FALSE);   /* -> NULL, and no topic on disk */

treedb_delete_instance()

treedb_delete_instance() durably deletes ONE instance of a node — one value of a secondary key (pkey2). Its slot in that pkey2 index goes, every md2 row of that (id, pkey2 value) is tombstoned on disk, and the instance leaves the hooks of its parents in memory: nothing writes it back, and a reopen does not bring it back. The primary id index is not touched: route only a NON-primary instance here. treedb_delete_node() deletes a whole key.

int treedb_delete_instance(
    json_t      *tranger,
    json_t      *node,       // pure node borrowed from the index; consumed only on success
    const char  *pkey2_name,
    json_t      *jn_options  // owned, bool "ignore_snaps"
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *The instance as the pkey2 index holds it. Borrowed: the index’s reference goes on success only.
pkey2_nameconst char *Name of the secondary key that identifies the instance.
jn_optionsjson_t *Owned. ignore_snaps skips the snapshot guard (not the immutable one). force does nothing here: a delete of an instance is never refused for its links (see below).

Returns

Returns 0 on success, or a negative value if the deletion is refused or fails.

Notes

Its links are moved in memory, never refused. An fkey names the parent’s KEY, never one of its instances, so a reload links the primary instance of a node alone, and hangs every child from the primary of its parent. A link made at run time lands on the instance it is given, and the delete puts memory where a reload would put it (new after 7.25.4):

Nothing is written: the rows of the instance are tombstoned, and a record with its fkeys cleared would bring it back. Its fkeys stay as they are. An instance that is also the primary (the same object in the primary index) stays the node in memory, with its links.

In 7.25.4 the instance stayed in the hooks of its parents. A delete of such a parent without force was refused for an instance that was gone, and a forced delete saved it back: its newest row, the primary of its key after a reopen. And its children hung from no visible parent until a reload, so a delete of the key did not see them and left their fkeys naming a parent that was gone.

/*  kids x/v1 (the primary) and x/v2; parents O and P, P with instances v1
 *  (the primary) and v2. O.kids holds x/v2; P/v2.kids holds kid a.  */
treedb_delete_instance(tranger, x2, "version", 0);   // 0: O.kids is [] (or [x/v1],
                                                     //    when x/v1 names O too)
treedb_delete_instance(tranger, p2, "version", 0);   // 0: P/v1.kids holds a
treedb_delete_node(tranger, o, json_object());       // 0: O holds nothing
treedb_delete_node(tranger, p1, json_object());      // -1: has down links (a)

It refuses, before it tombstones or drops anything, when it cannot read every row of the key: a row whose metadata cannot be read (“Cannot delete instance, cannot read every row of its key”), or whose content cannot be read (“Cannot delete instance, a row of its key cannot be read”: a row that cannot be read cannot say whose instance it is). Until 7.25.4 it tombstoned what it had read, dropped the slot, answered 0, and the instance came back at the next open.

It tombstones the rows oldest first, and the first tombstone that fails stops it: the delete answers -1 (“Cannot delete instance, a row of it cannot be tombstoned: the instance stays, its newest rows alive”, with rows and tombstoned), and the instance stays in memory. A tombstone cannot be taken back, but the newest row is still alive, so the next open loads the instance as memory has it. The older rows tombstoned before the failure are gone from its history. In 7.25.4 it tombstoned newest first, went on after a failure, answered 0 and dropped the instance, which came back at the next open from an OLDER row. (new after 7.25.4)

// Delete the release "1.2.0" of yuno "gate1" (topic `yunos`, pkey2 `yuno_release`)
json_t *inst = treedb_get_instance(tranger, "treedb_yuneta_agent", "yunos",
    "yuno_release", "gate1", "1.2.0");
if(inst && treedb_delete_instance(tranger, inst, "yuno_release", 0) < 0) {
    // refused: immutable, a snapshot holds it, a row of the key cannot be read,
    // or a row cannot be tombstoned (logged); the instance is still there
}

A record marked immutable (__md_treedb__immutable, see [treedb_set_node_immutable()](<#treedb_set_node_immutable>)) is refused and force` does NOT override it.

The function tombstones every md2 row of this (id, pkey2 value), so it refuses an instance that a snapshot holds a record of: “cannot delete instance, a snapshot still holds it”. It does not read the tag the node carries in memory — a save is untagged, so an instance updated after a shot carries 0 while the record the snap froze is still under it. It reads the records of the key instead and keeps the ones of this instance (which instance a record belongs to is a FIELD, so that walk reads the content of the record). ignore_snaps overrides this guard, and force does not (since after 7.24.1; it used to). It is the twin of the one treedb_delete_node() has for a whole key.


treedb_delete_node()

The treedb_delete_node() function deletes a node from the tree database. If the node has existing links, the deletion will fail unless the ‘force’ option is enabled.

int treedb_delete_node(
    json_t *tranger,
    json_t *node,       // NOT owned: borrowed from the index, whose reference goes on success
    json_t *jn_options  // bool "force" (unlink children), bool "ignore_snaps"
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
nodejson_t *The node to be deleted: the pure node as the index holds it. It is borrowed, never the caller’s own reference: do not decref it, before or after. On success the index’s reference is released with the key; on a refusal the node is left as it was, still indexed.
jn_optionsjson_t *A JSON object containing options for deletion. force unlinks the children and the parents first (without it a node with links is refused); ignore_snaps deletes a node a snapshot holds a record of.

Returns

Returns 0 on success, or a negative error code if the deletion fails.

Notes

If the node has existing links and ‘force’ is not enabled, treedb_delete_node() will fail.

Every instance of the key is looked at. The delete takes the key whole, every instance of it on disk, and each instance is a node of its own in memory: a link made at run time lands on the instance it is given. So the guard counts the children of every instance, and the parents of every instance that a hook holds (found by pointer). Without force a child or a parent of any of them refuses the delete (“Cannot delete node: has down links” / “has up links”). With force every child is unlinked and saved (a child that two instances hold through one hook is one link, unlinked once), and every other instance is taken out of the hooks that hold it, in memory: nothing else of it moves, its key goes. An fkey of an instance that no hook holds links nothing in memory, and does not refuse the delete. A delete that is refused puts all of it back. In 7.25.4 the delete looked at the node it was given alone: a child of another instance kept naming the deleted key (“Node not found” at the next open), and another instance stayed in the hook of its parent, where a forced delete of that parent saved it back into the deleted key, which was back after a reopen (new after 7.25.4).

/*  P/v1 is the primary; kid a hangs from P/v2 only  */
treedb_delete_node(tranger, p1, json_object());                     // -1: has down links
treedb_delete_node(tranger, p1, json_pack("{s:b}", "force", 1));   // 0: a["parent"] is ""

/*  x/v1 is the primary, hanging from nothing; x/v2 hangs from O2  */
treedb_delete_node(tranger, x1, json_object());                     // -1: has up links
treedb_delete_node(tranger, x1, json_pack("{s:b}", "force", 1));   // 0: O2.kids is []

An instance of a child that NAMES the node is a down link, held or not. An instance of a child inherits the fkeys of its primary at its create, and a hook holds one object per child id: so an instance can name the node with no hook holding it. The guard counts them, in the child topics with pkey2s (a topic without pkey2s has one node per key, which the hook holds when it names the node: nothing more is looked at). Without force they refuse the delete (“Cannot delete node: has down links”, with children and unheld_instances). With force each one stops naming the node and is saved, before the children are unlinked. None of those saves, nor the saves of the children, is a write of the child that the caller asked for: the instance that wrote the newest record of each key they touch before the delete writes it again, last, so the next reload takes the same primary as it would without the delete. A delete that is refused puts them back, and keeps the newest record of each key where it was too. In 7.25.4 the child the delete unlinked last was the primary of the next reload, over a new instance created after it; and the delete did not see the instances that no hook holds: it went without force, and they named a node that is gone (“Node not found” at the next open, once one of them was the newest record of its key); and a forced delete saved a child it unlinked through one hook with its ref of another hook still there.

/*  a/v1 moved from P to Q (a relink moves the instance it is given);
 *  a/v2 still names P, and P's hooks hold nothing  */
treedb_delete_node(tranger, P, json_object());                     // -1: has down links
treedb_delete_node(tranger, P, json_pack("{s:b}", "force", 1));   // 0: a/v2["parent"] is ""
    // after a reopen a/v1 -- it wrote the newest record of a -- is the primary, in Q

A key that only the secondary indexes hold is deleted whole, quietly. With a snap active the primary index holds what the snap tagged, and a key created after the snap has instances and no primary. Up to 7.25.4 its delete logged “delete_primary_node() FAILED”, with a stack, on a delete that went.

Every child a hook holds is a down link, whatever its id. A topic without hooks keeps any id (an id that holds ^, or one of NAME_MAX bytes), and such a child hangs from its parent like any other. A delete without force is refused (“Cannot delete node: has down links”), and a forced delete unlinks it. In 7.25.4 a dict hook did not count a child whose id holds two ^: the parent was deleted, forced or not, and the child’s fkey named a node that is gone, also after a reopen (“Node not found”).

/*  users has no hooks: "u^1" is a valid id; u^1 hangs from P1 (owners.members)  */
treedb_delete_node(tranger, p1, json_object());                     // -1: has down links
treedb_delete_node(tranger, p1, json_pack("{s:b}", "force", 1));   // 0: u^1["owner"] is ""

A node whose child topic did not load whole is not deleted, forced or not: a topic one of its hooks holds has keys that did not load (see A topic that did not load whole), a child that did not load may name the node, and memory does not know it. The delete answers -1 with “Cannot delete node: a topic its hooks hold did not load whole, a child that did not load may hang from it” (child_topic), and nothing moves. Repair or delete the key that did not load, then delete the node (new after 7.25.4; the node was deleted and that child named a parent that is gone).

/*  boxes.things hooks things.box; the key of the thing t2, linked to b1, did not load  */
treedb_delete_node(tranger, b1, json_pack("{s:b}", "force", 1));   // -1, b1 and t1 as they were

With force, a delete that is refused changes nothing. A child whose unlink cannot be saved stays linked, and the delete is refused (“Cannot delete node: still has down links”). A key that cannot be deleted refuses it too (“Cannot delete node”). Then the children that were unlinked and saved before the refusal are put back as they were (a list fkey keeps its order, and each child goes back to its place in the hooks), and saved again. The node keeps its parents in memory, and no event of the delete is told.

When the delete goes through, the events of its unlinks are told after the node has left the indexes, and before EV_TREEDB_NODE_DELETED. A subscriber that looks the node up by its id finds nothing. Until the delete returns, a save of the node is refused (“Cannot save a node that is being deleted”), from these callbacks and from the one of EV_TREEDB_NODE_DELETED: its key is gone, and a record written into it would bring the node back from the disk.

/*  gina hangs from legal; the callback of the treedb saves legal when it
 *  is told EV_TREEDB_NODE_UNLINKED                                        */
treedb_delete_node(tranger, legal, json_pack("{s:b}", "force", 1));   // 0
/*  told: EV_TREEDB_NODE_UNLINKED (gina), EV_TREEDB_NODE_UPDATED (gina),
 *  EV_TREEDB_NODE_DELETED (legal). The save in the callback answered -1,
 *  and legal is not on disk.                                              */

In 7.25.4 a child whose unlink could not be saved did not stop the delete: its record on disk kept naming the deleted node. And a key that could not be deleted left the node unlinked from its parents in memory, and every child unlinked on disk.

A child that cannot be saved again stays unlinked, in memory as on disk, and an ERROR names it: “A refused delete cannot put back a child it had unlinked: the child stays unlinked, in memory as on disk”. Its unlink stays, so its events are told: EV_TREEDB_NODE_UNLINKED (or the parent’s EV_TREEDB_NODE_UPDATED, without link events) and the EV_TREEDB_NODE_UPDATED of the child. The delete answers -1 all the same.

/*  finance hangs from board; audit, bob and carol hang from finance;
 *  the files of the key of carol are read-only                            */
treedb_delete_node(tranger, finance, json_pack("{s:b}", "force", 1));    // -1
/*  audit and bob were unlinked and saved before carol failed: both are
 *  linked to finance again, on disk too, and bob's fkey keeps its order.
 *  finance still hangs from board. No event was told.                     */

A node that a snapshot holds a record of is refused (“cannot delete node, a snapshot still holds it”) unless ignore_snaps is given. force does not override that (since 7.25.0): force means “unlink the children” and nothing else. It used to mean both, and the two callers that force every delete to get the first -- the agent’s delete-yuno and the gobj-ui topic table -- switched the snapshot guard off with it.

/*  a node with children, that no snap holds  */
treedb_delete_node(tranger, node, json_pack("{s:b}", "force", 1));

/*  a node a snap froze: deleting it breaks that snap's rollback  */
treedb_delete_node(tranger, node, json_pack("{s:b, s:b}",
    "force", 1, "ignore_snaps", 1));

A record marked immutable (__md_treedb__immutable, see [treedb_set_node_immutable()](<#treedb_set_node_immutable>)) is refused and force` does NOT override it.


treedb_delete_topic()

Deletes a topic from the TreeDB identified by treedb_name. The topic and all its associated data will be permanently removed.

int treedb_delete_topic(
    json_t  *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
treedb_nameconst char *Name of the TreeDB from which the topic will be deleted.
topic_nameconst char *Name of the topic to be deleted.

Returns

Returns 0 on success, or a negative error code if the operation fails.

Notes

Make sure that the topic does not contain critical data before calling treedb_delete_topic().

A topic created with system_topic = TRUE (see treedb_create_topic()) is refused. There is no force override.


treedb_file_ext()

The extension a blob is stored with, derived from its mime type: it is what a web server reads to set the Content-Type, so it comes from the type stored and never from the name given.

const char *treedb_file_ext(
    const char  *content_type
);

Parameters

KeyTypeDescription
content_typeconst char *A mime type.

Returns

A static string without the dot: jpg, png, webp, gif, pdf, mp4, webm, mov, ogv, mkv, mp3, m4a, ogg, wav, weba, flac; bin for an empty or unknown type.

Example

treedb_file_ext("video/quicktime");    /* "mov" */
treedb_file_ext("text/plain");         /* "bin" */

treedb_gc_files()

The garbage collector of the bytes of file columns. It takes every asset of __assets__ that no live node, no instance of one, and no snapshotted version of a node links (row and bytes), and every blob of .blobs/ that no row names (what an interrupted write leaves: the blob goes down before the index node). Never automatic: treedb_delete_node() with force UNLINKS the children instead of deleting them, so an unlinked asset is a normal intermediate state of a bulk operation. It reads the snapshots on disk, which makes the answer conservative. The command is C_NODE’s gc-assets.

json_t *treedb_gc_files(
    json_t      *tranger,
    const char  *treedb_name,
    BOOL        dry_run
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb.
treedb_nameconst char *The treedb.
dry_runBOOLTRUE: take nothing, answer what would be taken.

Returns

The list of asset ids taken (or that would be taken), yours to decref. NULL with “assets index not found” logged when the treedb has no __assets__ topic.

NULL, and nothing taken -- no asset row, and not even the blobs no row names -- when the gc is refused. It refuses:

An instance holds its asset. After a reopen only the primaries are linked, so the hooks of __assets__ do not show what the other instances of a node name: the gc reads the file columns of every node the secondary indexes hold (in the topics with pkey2s), and holds what they name. Up to 7.25.4 it took the asset of an instance that is not the primary, row and bytes; once that instance was the newest record of its key, the reload said “Node not found”.

/*  docs has pkey2 `version`: d/v1 holds photo A, d/v2 (the primary) photo B  */
json_t *taken = treedb_gc_files(tranger, "my_db", FALSE);   // []: A is d/v1's

A refusal takes nothing, not even the blobs no row names. treedb_gc_files2() sweeps those blobs on a refusal too, and says which blobs it took. What to do after a refusal: see What the operator does under treedb_open_db().

Example

json_t *would = treedb_gc_files(tranger, "treedb_yunovatioscedb", TRUE);
if(!would) {
    /* refused (logged): nothing was taken, see gobj_log_last_message() */
}
/* ["3f1c...", "9a0b..."]: look at them before running it for real */
JSON_DECREF(would)

treedb_gc_files2()

The same gc as treedb_gc_files(), answered as a report that says what a refusal still did. The blobs of .blobs/ that no row names need no link nor snapshot to be judged -- nothing can lead to them -- so they are swept (or listed, dry_run) even when the asset rows are refused, and the report names them. For a command that must answer everything it deleted (C_NODE’s gc-assets).

json_t *treedb_gc_files2(
    json_t      *tranger,
    const char  *treedb_name,
    BOOL        dry_run
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb.
treedb_nameconst char *The treedb.
dry_runBOOLTRUE: take nothing, answer what would be taken.

Returns

A dict, yours to decref:

KeyTypeDescription
dry_runboolThe dry_run of the call.
refusedstringOnly when the asset rows were refused: why (the same text treedb_gc_files() logs).
assetslistThe ids of the asset rows taken, or that would be. [] when refused.
blobslistThe ids of the blobs no row names that were taken, or would be -- on a refusal too. An id already in assets is not repeated.
blobs_refusedstringOnly when the sweep refused too: “gc: the blobs are not swept, assets did not load whole” (the bytes of a row that did not load would read as bytes no row names).

NULL only on an error, logged: “assets index not found”.

On a refusal with blobs taken it also logs, as info, “gc: the asset rows were refused; blobs no row names were taken” (dry run: “would be taken”) with their ids.

Example

json_t *report = treedb_gc_files2(tranger, "treedb_items", FALSE);
/*
 *  {"dry_run": false,
 *   "refused": "gc refused: a snap is active, the nodes in memory are its photo ...",
 *   "assets": [],
 *   "blobs": ["0123...cdef"]}
 */
const char *refused = kw_get_str(gobj, report, "refused", 0, 0);
if(refused) {
    /* answer the refusal, AND the blobs of report["blobs"] that were taken */
}
JSON_DECREF(report)

treedb_get_id_index()

treedb_get_id_index() retrieves the index of node IDs for a given topic in a TreeDB instance.

json_t *treedb_get_id_index(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB instance.
topic_nameconst char *Name of the topic whose ID index is to be retrieved.

Returns

A JSON object containing the ID index of the specified topic. WARNING: The returned object is NOT owned by the caller.

Notes

The returned JSON object must not be modified or freed by the caller.


treedb_get_instance()

treedb_get_instance() retrieves a specific node instance from a TreeDB topic using both primary and secondary keys.

json_t *treedb_get_instance(
    json_t       *tranger,
    const char   *treedb_name,
    const char   *topic_name,
    const char   *pkey2_name, // required
    const char   *id,         // primary key
    const char   *key2        // secondary key
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
treedb_nameconst char *Name of the TreeDB database.
topic_nameconst char *Name of the topic within the TreeDB.
pkey2_nameconst char *Name of the secondary key field (required).
idconst char *Primary key of the node instance.
key2const char *Secondary key of the node instance.

Returns

Returns a pointer to a json_t object representing the requested node instance. The returned object is NOT owned by the caller and must not be modified or freed.

Notes

If the specified instance does not exist, NULL is returned. Use treedb_get_node() if only the primary key is needed.

The instance of the primary IS the primary. When key2 is the pkey2 value of the primary, the pointer returned is the node that treedb_get_node() returns, with its links, after a create, a save and a reopen alike. Up to 7.25.4 a reopen built that slot from its own record: a second object with no links, until the first save of the primary re-pointed it. An update through it after a restart changed a copy the primary never saw, and the next save of the primary wrote the old values back over the new ones. That save also dropped the copy, and a caller still holding the pointer held freed memory (new after 7.25.4).

/*  after a reopen, x/v2 is the primary of x  */
json_t *x = treedb_get_node(tranger, "my_db", "kids", "x");
json_t *inst = treedb_get_instance(tranger, "my_db", "kids", "version", "x", "v2");
/*  inst == x: an update through inst is an update of x, saved once  */
treedb_update_node(tranger, inst, json_pack("{s:s}", "note", "A"), TRUE);

It holds with more than one pkey2 (a save never takes the slot the primary holds, see treedb_save_node()), and for a key that has instances and no primary -- what a snap active shows of a key created after it: the create that makes the primary takes the slot of its value, where up to 7.25.4 the slot kept the loaded object, a second one of the same instance.

/*  snap s1 active; P was created after it: P/v2 is loaded, P has no primary  */
json_t *P = treedb_create_node(tranger, "my_db", "parents",
    json_pack("{s:s, s:s}", "id", "P", "version", "v2"));
treedb_get_instance(tranger, "my_db", "parents", "version", "P", "v2");   // P

treedb_get_node()

Retrieves a node from the TreeDB using its primary key. The function returns a reference to the node stored in the database, which must not be modified directly.

json_t *treedb_get_node(
    json_t       *tranger,
    const char   *treedb_name,
    const char   *topic_name,
    const char   *id
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the TimeRanger database instance.
treedb_nameconst char *The name of the TreeDB from which to retrieve the node.
topic_nameconst char *The topic within the TreeDB that contains the node.
idconst char *The primary key of the node to retrieve.

Returns

A reference to the requested node as a json_t *. The returned node must not be modified directly.

Notes

The returned node is not owned by the caller and must not be modified or freed. Use treedb_update_node() to modify the node safely.


treedb_get_topic_hooks()

Retrieves a list of column names that are hooks in the specified topic of the treedb_name tree database.

json_t *treedb_get_topic_hooks(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
treedb_nameconst char *The name of the tree database containing the topic.
topic_nameconst char *The name of the topic whose hook columns are to be retrieved.

Returns

A JSON array containing the names of the columns that are hooks in the specified topic. The returned JSON object is NOT owned by the caller.

Notes

Hooks define relationships between nodes in the tree database. Use treedb_get_topic_links() to retrieve foreign key links instead.


treedb_get_topic_links() returns a list of column names that are foreign key links in the specified topic of a TreeDB.

json_t *treedb_get_topic_links(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the TimeRanger database instance.
treedb_nameconst char *The name of the TreeDB to query.
topic_nameconst char *The name of the topic whose foreign key links are to be retrieved.

Returns

A JSON array containing the names of the columns that are foreign key links in the specified topic. The returned JSON object is not owned by the caller.

Notes

The function provides insight into the schema of a topic by identifying its foreign key relationships. The returned list must not be modified or freed by the caller.


treedb_import_files()

The second door of file columns: a directory already on the node becomes N assets in one call, with no bytes on the wire. It creates index nodes in __assets__ and links nothing; it answers the map path -> id, so the loader can link what it imported (where a file came from is a fact of the load, not of the asset). The command is C_NODE’s import-assets.

It is confined to import_root, and an empty root means refused -- a call that reads a path is a call that reads anything on the node. The confinement is resolved, not only spelled: a source_dir with .., or one that resolves out of import_root through a symlink, is refused.

json_t *treedb_import_files(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *import_root,
    const char  *source_dir,
    BOOL        dry_run,
    const char  *uploaded_by
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb.
treedb_nameconst char *The treedb.
import_rootconst char *The only tree it may read. Empty: refused.
source_dirconst char *Directory under import_root to import, walked recursively.
dry_runBOOLTRUE: store nothing, answer what would be imported.
uploaded_byconst char *Kept in each asset node.

Returns

{"source_dir", "dry_run", "imported", "would_import", "skipped", "failed", "bytes", "files": {"<relative path>": "<id>"}}, yours. NULL with the cause in gobj_log_last_message() when refused (no root, .., not a directory, out of the root).

Example

json_t *result = treedb_import_files(
    tranger, "treedb_yunovatioscedb",
    "/yuneta/store/censo", "memorias/malaga",
    FALSE, "yuneta"
);
/* result.files: {"memorias/malaga/nave-1.jpg": "ba7816bf..."} */
JSON_DECREF(result)

treedb_is_treedbs_topic()

treedb_is_treedbs_topic() checks if a given topic belongs to the internal system topics of a TreeDB instance.

BOOL treedb_is_treedbs_topic(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB instance to check.
topic_nameconst char *Name of the topic to verify.

Returns

Returns TRUE if the topic is an internal system topic of the TreeDB, otherwise returns FALSE.

Notes

System topics include __snaps__ and __graphs__.


The treedb_link_nodes() function establishes a hierarchical relationship between a parent node and a child node using the specified hook.

int treedb_link_nodes(
    json_t      *tranger,
    const char  *hook,
    json_t      *parent_node,    // NOT owned, pure node
    json_t      *child_node      // NOT owned, pure node
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
hookconst char *The name of the hook defining the relationship between the parent and child nodes.
parent_nodejson_t *The parent node to which the child node will be linked. This parameter is not owned by the function.
child_nodejson_t *The child node that will be linked to the parent node. This parameter is not owned by the function.

Returns

Returns 0 on success, or a negative error code if the operation fails.

Notes

The function does not take ownership of parent_node or child_node. Make sure that both nodes exist and are valid before calling treedb_link_nodes().

A link writes the CHILD, and only when the child moved. The persistent half of a relationship is the child’s fkey; the parent’s hook is in memory and is rebuilt at the next load. So a link that only fills a hook writes nothing: that is the ordinary case of a second instance of a node, which inherits the fkey of the instance before it (the ref names the parent’s id, shared by both instances) while the hook of the new parent is empty. Such a link logs nothing either (up to 7.25.4 it warned “Parent ref already in child fkey”, once per create-yuno of a new release). When no other instance of the parent’s key explains the ref -- none holds the child, and the parent is not a new instance with an empty hook -- the hook LOST a child its fkey names: the link puts it back and warns “Parent hook had lost a child its fkey names: repaired”. A link asked twice, with nothing to move on either side, writes nothing and publishes nothing, and it warns (“Parent ref already in child fkey, skipping duplicate”). The link EVENT follows either side: filling a hook is a new relationship in memory even when nothing is written.

A link into a single-valued fkey (a string column) replaces the old one: the child is first unlinked from the parent its string names, which emits EV_TREEDB_NODE_UNLINKED for it. When the string the child holds is longer than a reference can be (a record of an older store, 765 bytes or more), the link is refused and nothing moves (“Cannot link, the reference the child has is too long”, with ref): for example a users.department_id loaded as a string of 800 bytes refuses treedb_link_nodes(tranger, "users", dept, user) with -1. treedb_unlink_nodes() from a parent the child does not name is refused (“Cannot unlink, the child does not hang from that parent”): nothing moves, no event, no save.

A link that would hang a node from its own descendant through the same hook is refused (“Cannot link, the link would close a cycle in the hook”), and nothing moves. A cycle through two different hooks is data, and is accepted. For example, with the departments.departments hook, linking direction as a child of administration fails when administration is already a child of direction:

treedb_link_nodes(tranger, "departments", direction, administration);   // 0
treedb_link_nodes(tranger, "departments", administration, direction);   // -1

A hook can hold the nodes of several topics, and an id names a node inside its topic only: the user x and the group x are two nodes. A list hook holds both. A dict hook is keyed by the id alone, so it cannot: the link of the second one is refused (“Cannot link, the dict hook holds a node of another topic with this id”) and nothing moves. A load that finds such a pair on disk loads the first one and says so for the second (“A dict hook holds a node of another topic with this id: this link is not loaded”). Until 7.25.4 the membership of a hook was tested by the bare id: the second link of a list hook was a duplicate that was skipped, and the second one of a dict hook took the first one’s place. An unlink of the second one from a dict hook does not delete the first one’s entry: it finds the slot taken by the other topic’s node, leaves it, and logs “Child data not found in dict parent hook: its slot holds a node of another topic”.

/*  owners.members is a list hook, owners.tagged a dict hook, both of users and groups  */
treedb_link_nodes(tranger, "members", owner, user_x);   // 0
treedb_link_nodes(tranger, "members", owner, group_x);  // 0: members holds both
treedb_link_nodes(tranger, "tagged", owner, user_x);    // 0
treedb_link_nodes(tranger, "tagged", owner, group_x);   // -1: the slot "x" is the user's

In the __system__ treedb, a name is unique among siblings. A schema is rebuilt by name, so the link of a column into a topic that already has a column of that name, and of a topic into a treedb that already has a topic of that name, is refused and nothing moves (“Topic already has a column with this name”, “Treedb already has a topic with this name”, with id and sibling_id). The child already linked there is no clash, and neither is a child keyed by its qualified id under that parent (<parent id>.<name>): the name is its own there. For example, with the treedbs treedb_x and treedb_y both holding a topic users:

treedb_link_nodes(tranger, "topics", treedb_x, treedb_y_users);    // -1
treedb_link_nodes(tranger, "topics", treedb_x, treedb_x_users);    // 0: already there

A link is refused, and nothing moves, when the reference of the parent (topic^id^hook) cannot be decoded back: a part of NAME_MAX or more, or a part that holds a ^ (the separator). It logs “Cannot build the reference of a node: a part of it is too long, or holds a ‘^’”. A create refuses such an id for a topic with hooks, but a parent loaded from an older store can have one. Until 7.25.4 the link answered 0: a long reference was saved and lost at the next open, and a reference with a ^ was refused by the save:

/*  "a^b" is a parent of an older store: its id holds the separator  */
treedb_link_nodes(tranger, "members", a_caret_b, user_u2);  // -1
/*  u2["owner"] is still "", a_caret_b hooks nothing                  */

A save never drops a whole fkey column for one wrong reference in it. The wrong reference is left out and logged (“Wrong fkey reference: must be "topic_name^id^hook_name"”), and the valid references of the column are saved. Until 7.25.4 the record was saved without the column, and all its links were lost at the next open.

A save that fails takes the link back. The link moves the child’s fkey and the parents’ hooks in memory first, then saves the child. When the save fails, the link is undone in memory (a single-valued fkey goes back to its old parent), the call answers -1, and no event is told: the events of a link are told only once the child is on disk, EV_TREEDB_NODE_UPDATED of the child last. The same holds for treedb_unlink_nodes(), treedb_autolink(), treedb_clean_node(), treedb_replace_links(), treedb_update_node_and_links() and the unlinks of a forced treedb_delete_node() (a refused forced delete changes nothing, see there):

/*  the files of the key of `admin` are read-only; admin hangs from direction  */
treedb_link_nodes(tranger, "departments", sales, admin);    // -1
/*  admin["department_id"] is still "departments^direction^departments",
 *  direction still hooks it, sales does not, no event was told            */

A forced delete whose child cannot be saved unlinked is refused. The child stays linked, in memory and on disk, and the delete logs “Cannot delete node: still has down links”:

/*  the files of the key of `admin` are read-only; admin hangs from direction  */
treedb_delete_node(tranger, direction, json_pack("{s:b}", "force", 1));    // -1
/*  direction is not deleted, admin still hangs from it                    */

When memory cannot be taken back whole (a parent hook cannot be restored), an ERROR says so: “A write that did not reach the disk could not be taken back whole in memory: the links in memory differ from the disk until the treedb is opened again”.

A ref that was never a link is not linked again when a write is taken back. A stale ref (its hook no longer exists, or the hook fills another column since a schema re-pointed it) and a ref whose parent is not in memory go back into the field alone, as a load of the disk leaves them.

/*  erin: departments ["departments^sales^users", "departments^direction^nohook"]
 *  (no hook `nohook`); the files of the key of erin are read-only         */
treedb_clean_node(tranger, erin, TRUE);     // -1
/*  erin["departments"] is the same two refs, sales hooks erin,
 *  direction does not                                                     */

A child that a take-back links again goes back to its PLACE in the hook of the parent, not to the end: every hook holds its children in the order it had before the write. That holds for a list hook and for a dict hook (a dict keeps its keys in the order they were set), and for the children a refused forced delete puts back. In 7.25.4 nothing was taken back.

/*  sales["users"] holds bob, erin, frank; the files of erin are read-only  */
treedb_clean_node(tranger, erin, TRUE);     // -1
/*  sales["users"] holds bob, erin, frank again, in that order             */

A reference names the parent by its id, so all the instances of a parent with a pkey2 can hold the same child. An unlink takes the child out of all of them, and a take-back puts it back into all of them, each one in its place:

/*  x hangs from the two instances P/v1 and P/v2 of the parent P,
 *  P/v2["items"] holds w, x; the files of the key of x are read-only   */
treedb_unlink_nodes(tranger, "items", p_v1, x);    // -1
/*  P/v1["items"] holds x, P/v2["items"] holds w, x again             */

treedb_list_instances()

treedb_list_instances() returns a list of instances from a specified topic in a tree database, optionally filtered by a given JSON filter and a custom match function.

json_t *treedb_list_instances(
    json_t *tranger,
    const char *treedb_name,
    const char *topic_name,
    const char *pkey2_name,
    json_t *jn_filter,  // owned
    BOOL (*match_fn) (
        json_t *topic_desc, // NOT owned
        json_t *node,       // NOT owned
        json_t *jn_filter   // NOT owned
    )
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
treedb_nameconst char *The name of the tree database.
topic_nameconst char *The name of the topic from which instances are retrieved.
pkey2_nameconst char *The secondary key name used to identify instances.
jn_filterjson_t *A JSON object containing filter criteria. This parameter is owned by the function.
match_fnBOOL (*)(json_t *, json_t *, json_t *)A function pointer to a custom match function that determines whether an instance matches the filter criteria.

Returns

A JSON array containing the list of matching instances. The caller must decrement the reference count when done.

Notes

The returned list must be decrefed by the caller to avoid memory leaks. Filtering is applied using both jn_filter and match_fn if provided.


treedb_list_nodes()

treedb_list_nodes() retrieves a list of nodes from a specified topic in a tree database, optionally filtering the results based on a provided filter and a custom matching function.

json_t *treedb_list_nodes(
    json_t *tranger,
    const char *treedb_name,
    const char *topic_name,
    json_t *jn_filter,  // owned
    BOOL (*match_fn) (
        json_t *topic_desc, // NOT owned
        json_t *node,       // NOT owned
        json_t *jn_filter   // NOT owned
    )
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
treedb_nameconst char *The name of the tree database to query.
topic_nameconst char *The name of the topic from which to retrieve nodes.
jn_filterjson_t *A JSON object containing filter criteria for selecting nodes. This parameter is owned by the function.
match_fnBOOL (*)(json_t *, json_t *, json_t *)A function pointer to a custom matching function that determines whether a node matches the filter criteria.

Returns

Returns a JSON array containing the list of nodes that match the filter criteria. The caller must decrement the reference count of the returned JSON object when done.

Notes

If match_fn is provided, it is used to further refine the selection of nodes based on custom logic. The returned JSON object must be properly decremented to avoid memory leaks.


treedb_list_parents()

treedb_list_parents() returns a list of parent nodes linked to the given node through a specified foreign key (fkey). The function can return either full parent nodes or collapsed views based on the collapsed_view parameter.

json_t *treedb_list_parents(
    json_t *tranger,
    const char *fkey,
    json_t *node,
    json_t *jn_options
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
fkeyconst char *The foreign key field used to identify parent nodes.
nodejson_t *The node whose parents are to be retrieved. This parameter is not owned by the function.
collapsed_viewBOOLIf TRUE, returns a collapsed view of the parent nodes. Otherwise, returns full parent nodes.
jn_optionsjson_t *Options for filtering and formatting the returned parent nodes. This parameter is owned by the function.

Returns

A JSON array containing the list of parent nodes. The caller must decrement the reference count of the returned JSON object.

Notes

The function retrieves parent nodes based on the specified fkey. If collapsed_view is TRUE, the function returns a simplified representation of the parent nodes. The jn_options parameter allows customization of the output format.


treedb_list_snaps()

treedb_list_snaps() returns a list of snapshots associated with a given TreeDB.

json_t *treedb_list_snaps(
    json_t       *tranger,
    const char   *treedb_name,
    json_t       *filter
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
treedb_nameconst char *The name of the TreeDB whose snapshots are to be listed.
filterjson_t *A JSON object containing filtering criteria for the snapshots. Owned by the caller.

Returns

A JSON array containing the list of snapshots. The caller must decrement the reference count when done.

Notes

The returned JSON array must be properly decremented using json_decref() to avoid memory leaks.


treedb_list_treedb()

treedb_list_treedb() returns a list of available TreeDB names stored in the given tranger instance.

json_t *treedb_list_treedb(
    json_t *tranger,
    json_t *kw
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger instance containing the TreeDBs.
kwjson_t *Optional filtering options (owned).

Returns

A JSON array containing the names of available TreeDBs. The caller must not modify or free the returned value.

Notes

The returned list is managed internally and must not be altered or freed by the caller.


treedb_node_children()

treedb_node_children() returns a list of child nodes linked to a given node through a specified hook, optionally applying filters and recursive traversal.

json_t *treedb_node_children(
    json_t       *tranger,
    const char   *hook,
    json_t       *node,       // NOT owned, pure node
    json_t       *jn_filter,  // filter to children tree
    json_t       *jn_options  // fkey,hook options, "recursive"
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
hookconst char *The hook name used to retrieve child nodes.
nodejson_t *The parent node from which child nodes are retrieved. This parameter is not owned.
jn_filterjson_t *Optional filter criteria to apply to the child nodes. This parameter is owned.
jn_optionsjson_t *Options for controlling the retrieval, including fkey and hook options, and whether to perform recursive traversal.

Returns

Returns a JSON array containing the child nodes that match the specified criteria. The caller must decrement the reference count when done.

Notes

If the recursive option is enabled in jn_options, treedb_node_children() will traverse the hierarchy recursively.


treedb_node_jtree()

treedb_node_jtree() constructs a hierarchical tree representation of child nodes linked through a specified hook.

json_t *treedb_node_jtree(
    json_t      *tranger,
    const char  *hook,
    const char  *rename_hook,
    json_t      *node,
    json_t      *jn_filter,
    json_t      *jn_options
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
hookconst char *Hook name used to establish parent-child relationships.
rename_hookconst char *Optional new name for the hook in the resulting tree.
nodejson_t *Pointer to the parent node from which the tree is built. Not owned.
jn_filterjson_t *Filter criteria for selecting child nodes. Not owned.
jn_optionsjson_t *Options for controlling the structure of the resulting tree, including fkey and hook options.

Returns

A JSON object representing the hierarchical tree of child nodes. The caller must decrement the reference count when done.

Notes

The function recursively traverses child nodes using the specified hook. Each node of the tree is its collapsed view, with its __path__ (the ids from the root, joined by `). The children of a node go, whole, in its hook -- or in rename_hook when given, the hook itself then left out. Each child appears ONCE: up to 7.25.22, without rename_hook, the hook kept the refs of the collapsed view and the children were appended after them, so every child was there twice.

// departments: top -> dev, ops (the self hook "departments")
json_t *top = treedb_get_node(tranger, "treedb_x", "departments", "top");
json_t *tree = treedb_node_jtree(tranger, "departments", "", top, 0, 0);
// tree: {"id": "top", ..., "__path__": "top",
//        "departments": [{"id": "dev", ..., "__path__": "top`dev", "departments": []},
//                        {"id": "ops", ..., "__path__": "top`ops", "departments": []}]}
json_t *webix = treedb_node_jtree(tranger, "departments", "data", top, 0, 0);
// webix: the same, the children in "data" and no "departments" key
JSON_DECREF(tree)
JSON_DECREF(webix)

treedb_open_db()

treedb_open_db() initializes and opens a tree database within a tranger instance, using the specified schema and options.

json_t *treedb_open_db(
    json_t      *tranger,
    const char  *treedb_name,
    json_t      *jn_schema,  // owned
    const char  *options     // "persistent"
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger instance managing the database.
treedb_nameconst char *The name of the tree database to open.
jn_schemajson_t *A JSON object defining the schema of the tree database. This parameter is owned by the function.
optionsconst char *"persistent": load the schema from its file, which wins unless jn_schema has a strictly higher schema_version. "persistent,impose": jn_schema also wins over a HIGHER version on disk (see below).

Returns

A JSON dictionary representing the opened tree database inside tranger. The returned object must not be used directly by the caller.

Notes

Make sure that tranger is already initialized before calling treedb_open_db(). The function follows a hierarchical structure where nodes are linked via parent-child relationships. If the persistent option is enabled, the schema is loaded from a file, and modifications require a version update.

The impose option. With "persistent,impose", and only on the master, jn_schema wins over a newer schema on disk too. The rule is the same at both levels, the treedb and each topic: a stored schema_version or topic_version lower than the one passed takes the new one, an equal one is kept, and a higher one is overwritten with the one passed (the log says “Imposing TreeDB schema from C over a newer one” and “Imposing topic_version from C over a newer one”). It exists to revert changes made to the schema outside the code. C_TREEDB uses it when its impose_c_schema is on, and, on the master, projects the schema into its __system__ treedb when that projection is missing or behind, so a schema in use can always be asked for. The records are not touched.

The main topic. A schema topic can carry 'main_topic': true (since 7.19.0). The mark names the topic that the tree of the treedb hangs from. Viewers use it: the treedb graph of gobj-ui opens its tree from this topic. Two rules apply:

If a mark breaks a rule, treedb_open_db() logs an error and ignores that mark. With two marks, the first one stays. The mark is not kept in the topic files of the store: the function sets it in memory on each open. tranger2_topic_desc() sends the mark to clients, together with system_topic.

A non-master open reads the mark from the persisted schema file, like the rest of the schema, so a reader that is not the master sees it too. A tool that opens the topics with timeranger2 only (tr2list) does not see it: the mark is not in topic_desc.json or topic_var.json.

The mark is a change to its topic, and it is published like one: raise the topic_version of that topic and, when the runtime must use it, the schema_version of the treedb. With the persistent option, the persisted schema file wins unless jn_schema has a strictly higher schema_version.

Example — the agent’s schema marks realms, which holds its sub-realms through the fkey parent_realm_id (treedb_schema_yuneta_agent.c):

    'schema_version': '24',                                         \n\
    'topics': [                                                     \n\
        {                                                           \n\
            'id': 'realms',                                         \n\
            'pkey': 'id',                                           \n\
            'system_flag': 'sf_string_key',                         \n\
            'topic_version': '8',                                   \n\
            'pkey2s': '',                                           \n\
            'main_topic': true,                                     \n\
            'cols': {                                               \n\
                ...
                'realms': {                                         \n\
                    'header': 'Realms',                             \n\
                    'fillspace': 10,                                \n\
                    'type': 'dict',                                 \n\
                    'flag': ['hook'],                               \n\
                    'hook': {                                       \n\
                        'realms': 'parent_realm_id'                 \n\
                    }                                               \n\
                },                                                  \n\
                'parent_realm_id': {                                \n\
                    'header': 'Realm Parent',                       \n\
                    'fillspace': 10,                                \n\
                    'type': 'string',                               \n\
                    'flag': [                                       \n\
                        'fkey'                                      \n\
                    ]                                               \n\
                },                                                  \n\

A topic that did not load whole. A topic is loaded with keyless, backward tranger2_open_list()s (one for the id index, one per pkey2). A key whose history cannot be read whole is named in the list, every other key is loaded, and the realtime feed is opened. A key fails:

A .md2 whose size is not a whole number of 32-byte rows does not fail the key. Its last row is torn: a power cut during the write of the row, an append that was never acknowledged. The master cuts the .md2 back to its whole rows at the open, with one warning, and the key loads whole:

WARNING load_first_and_last_record_md: md2 file of the key ends in a part of a row: an append that
      was never acknowledged was cut back
      topic=items key=k2 file_id=2026-09-23 path=<store>/items/keys/k2/2026-09-23.md2
      old_size=1285 new_size=1280

The cut removes fewer than 32 bytes, all after the last whole row. A replica does not cut: it reads the whole rows, and the master cuts the file when it opens the store. (Up to 7.25.4 the cache build left such a file out of the key with a CRITICAL, and nothing failed.)

The master cuts only a tail that is a torn row after good rows: a row is good when its content is one whole record of the .json (the rules are in tranger2_open_iterator()). Two other shapes also end on no row boundary. They are NOT cut, the bytes of the file do not change, and the key fails (a replica makes the same check):

When the check itself cannot run, the file is not cut, and the CRITICAL is one of three:

The open flags the file. On a master, the next append into the file counts it again, and the check runs then. On a replica, the file is counted again at the next notification of it from the master.

A .md2 of 0 bytes whose .json is NOT empty does not fail the key. It is the shape an append that was never acknowledged leaves: the content is written first and the md2 row after, and the row was never written (the md2 could not be created or written, or the yuno died between the two). The file is ignored, as until 7.25.4, with a warning that names it, and the key loads from its other files:

WARNING load_key_cache_from_disk: md2 file of the key with no rows and a content file that is not
      empty: an append that was never acknowledged, the file is ignored
      topic_directory=<store>/items key=k2 file_id=2099-01-01 content_size=41

An append whose md2 fails does not leave that shape: it answers -1 and its content is cut back, BEFORE the critical that reports the failure. With exit_on_error 2 (the default, LOG_OPT_EXIT_ZERO) that critical ends the yuno inside the log call, so the cut comes first. 7.25.4 did not cut the content back at all. A kill or a power cut between the two writes still leaves it, and the next open ignores it with the warning above.

The rows read BEFORE the failure are handed over, and backward they are the key’s NEWEST: its node is in memory when its newest record was readable (the damage is in older rows), and absent when the damage is there. treedb logs the keys:

ERROR tranger2_open_list: Cannot load the whole history of a key of the list: the records read before
      the failure were handed, the list goes on with the next key
      topic_name=items key=k2
ERROR note_keys_not_loaded: treedb topic loaded WITHOUT the whole history of keys that cannot be read:
      their node is in memory only if its newest record was read, and a create of those ids is refused
      treedb_name=treedb_items topic_name=items keys=["k2"]

Memory is what treedb answers from, so it refuses what it would answer wrong, until the topic is opened again or the key is deleted:

WhatWith a topic that did not load whole
treedb_create_node() of such an idrefused: “Cannot create node, its id has records on disk that could not be loaded”. Its record would become the newest of the key, over records nobody read. Other ids are created as usual.
__snaps__also logs “snaps loaded without some snaps: the active snap is unknown, ...”. The treedb is loaded from the live records (the active snap may be the one that did not load). treedb_shoot_snap() and treedb_activate_snap() refuse (“Cannot shoot a snap: snaps did not load whole, the active snap is unknown”, “Cannot activate a snap: snaps did not load whole, the active snap is unknown”; the command shoot-snap name=s3 answers -1: <role^name>: cannot shoot snap 's3' (see the log)); treedb_delete_node(), treedb_delete_instance() and the delete of an asset refuse too, because which snap holds a record is unknown (“cannot tell which snaps exist: snaps did not load whole”). ignore_snaps still overrides the node and instance deletes.
a topic that links assets (a file column), or __assets__, in ANY treedb of the trangertreedb_gc_files() refuses and takes nothing: “gc refused: a topic that links assets did not load whole, the live links are unknown”.
__assets__, in any treedb of the trangerthe sweep of the blobs no row names (treedb_gc_files2()) refuses too: “gc: the blobs are not swept, assets did not load whole”.
a topic a hook holds (the CHILD topic)treedb_delete_node() of a node of the PARENT topic refuses, forced or not, with or without children in memory: “Cannot delete node: a topic its hooks hold did not load whole, a child that did not load may hang from it” (child_topic names the topic). A child that did not load may name the node, and the links of a node are known only from its children in memory: deleted, the node left that child naming a parent that is gone (new after 7.25.4).

A key deleted since (tranger2_delete_key(), step 3 below) has no records on disk any more: treedb forgets it the next time a guard asks (“A key that did not load has been deleted since: it is not a key that did not load any more”), with no reopen.

Until 7.25.4 such a topic loaded without the key and nothing said so.

What the operator does when a guard fails closed (this refusal, or the snapshot guard of treedb_gc_files() that cannot read a tagged record):

  1. Read the log: the read error names the file (path, file_id, rowid), the lines above name the treedb, the topic and the keys.

  2. Look at the key’s directory, <store>/<topic>/keys/<key>/: a .md2 shorter than what was written (the rows past its end fail); a .json that was cut (a row’s content past its end fails); a file the yuno’s user cannot read; a .md2 whose size is not a multiple of 32 bytes, when the log has one of the two CRITICALs above (“...not cut, repair it by hand”). Without them, such a .md2 is not on this list: the master cuts it back itself, see above.

  3. Repair it with the yuno STOPPED (the running yuno caches the store and writes it), with the least that brings the file back, in this order:

    • a file the yuno’s user cannot read: give it back its owner and mode;

    • a .md2 whose size is not a multiple of 32 bytes, when the master logged “Cannot cut back a md2 file that ends in a part of a row: the file is damaged” (the cut itself failed, for example on a read-only file system): cut it back to whole rows by hand. The part of a row that is cut was never a readable row, and every whole row before it stays. This is the same cut the master does:

      cd <store>/items/keys/k2                  # topic, key and file: from the log
      f=2026-09-23.md2
      stat -c %s $f                             # 1285: 40 rows and 5 bytes
      truncate -s $(( $(stat -c %s $f) / 32 * 32 )) $f
      stat -c %s $f                             # 1280
    • a .md2 that 7.25.4 wrote after a torn row (“md2 file of the key ends in a whole row that is not on a row boundary: written by 7.25.4 after a torn row; not cut, repair it by hand”): remove the bytes of the torn row, not the end of the file. They are size % 32 bytes (13 in the log above) and they start on a row boundary. The script below tries each row boundary, and keeps the one where, with those bytes removed, the content of every row is whole (inside the .json, its last byte a NUL and no other NUL; only zero bytes for an instance deleted with its content zeroed), each row’s content comes after the content of the row before, and the removed bytes can be the start of the torn row: when they hold its __offset__ (17 bytes or more, not all zero), that offset is between the end of the content of the row before and the __offset__ of the row after. Without this last test, two boundaries can pass (a torn row of 31 bytes whose __size__ is a multiple of 256), and the first one is not always the torn row. The .json can have content after the last row (an append killed between its two writes): the script accepts it. It writes the result only when exactly one boundary passes. It checks each row twice at most, whatever the number of boundaries (a file of 86 400 rows takes less than one second).

      Set the topic, the key and the file from the log. The block keeps a copy of the .md2 in your home directory, with the topic, the key and the file in its name (items.k2.2026-09-23.md2.orig), so the copies of two keys do not overwrite each other. If that copy exists already (an earlier try), the block stops: move the old copy away first. The block writes the result into $f.tmp, a copy of the .md2 with its owner and mode (cp -p: run it as the owner of the store, or as root), then renames it over the .md2 (mv, one step). It runs in a subshell with set -e: when a command before the mv fails (the copy first of all, or a full disk), the block stops there and the .md2 does not change. The mv replaces the .md2 in one step, so the .md2 is always the old file or the repaired one:

      (
      set -e                                    # stop at the first command that fails
      t=items                                   # topic, key and file: from the log
      k=k2
      f=2026-09-23.md2
      cd <store>/$t/keys/$k
      b=~/$t.$k.$f.orig                         # the copy: one for each topic, key and file
      if [ -e $b ]; then                        # never overwrite an earlier copy
          echo "$b exists: stop"
          exit 1
      fi
      cp -p $f $b                               # keep the original
      rm -f $f.new $f.tmp                       # no file of an earlier try
      python3 - $f <<'EOF'
      import os, struct, sys
      md2 = sys.argv[1]
      c = open(md2[:-4] + '.json', 'rb').read()
      b = open(md2, 'rb').read()
      k = len(b) % 32                           # the bytes of the torn row
      rows = len(b) // 32                       # the whole rows, with the torn bytes removed
      def whole(tm, offset, size):              # the content an append wrote
          if size < 2 or offset + size > len(c):
              return False
          seg = c[offset:offset + size]
          if seg[-1] != 0:
              return False
          zeros = seg.count(0)                  # a NUL at the end, no other
          return zeros == 1 or (zeros == size and ((tm >> 44) & 0x400) != 0)
      def row(at):                              # its offset, its size, is its content whole
          t, tm, offset, size = struct.unpack('>QQQQ', b[at:at + 32])
          return offset, size, whole(tm, offset, size)
      left = [row(32 * j) for j in range(rows)]  # rows before the torn row: on a boundary
      right = [row(k + 32 * j) for j in range(rows)]  # rows after it: moved by k bytes
      head = [True]                             # head[m]: left[:m] are good, in order
      ends = [0]                                # ends[m]: where the content of left[m-1] ends
      for offset, size, ok in left:
          head.append(head[-1] and ok and offset >= ends[-1])
          ends.append(offset + size)
      tail = [True] * (rows + 1)                # tail[m]: right[m:] are good, in order
      for m in range(rows - 1, -1, -1):
          offset, size, ok = right[m]
          tail[m] = tail[m + 1] and ok and (m + 1 == rows or right[m + 1][0] >= offset + size)
      def fits(at):                             # the removed bytes start the torn row
          r = b[at:at + k]
          if k < 17 or r.count(0) == k:         # no byte of its __offset__, or only zeros
              return True
          n = min(k, 24) - 16                   # the bytes of its __offset__ they hold
          lo = int.from_bytes(r[16:16 + n], 'big') << (8 * (8 - n))
          hi = lo + (1 << (8 * (8 - n))) - 1
          end = 0                               # where the content of the row before ends
          if at >= 32:
              t, tm, offset, size = struct.unpack('>QQQQ', b[at - 32:at])
              end = offset + size
          t, tm, after, size = struct.unpack('>QQQQ', b[at + k:at + k + 32])
          return lo <= after and hi >= end      # its content between the two rows
      found = [32 * m for m in range(rows)      # the torn row at 32*m: left[:m], right[m:]
               if head[m] and tail[m] and right[m][0] >= ends[m] and fits(32 * m)]
      print('torn row at', found, 'of', k, 'bytes')
      if len(found) == 1:
          open(md2 + '.new', 'wb').write(b[:found[0]] + b[found[0] + k:])
      EOF
      if [ -f $f.new ]; then                    # 'torn row at [96] of 13 bytes'
          cp -p $f $f.tmp                       # the owner and mode of the .md2
          cat $f.new > $f.tmp                   # the result, into that copy
          mv $f.tmp $f                          # one step: the old .md2 or the new one
          rm $f.new
      fi
      stat -c %s $f                             # 160: 5 whole rows
      )

      The torn row is gone (its append was never acknowledged); its content stays in the .json, and no row names it. Every row that 7.25.4 acknowledged is read again. When the script prints no boundary, or more than one, it writes no .new file and the .md2 does not change. When more than one boundary passes, do not choose one by hand: put the key’s directory back from a backup copy.

    • anything else (a last whole row that is not valid, too): put the key’s directory back from a backup copy of the store.

    Only when the records of the key are lost for good and that is acceptable, remove the key, on the master, with the delete-key command of C_TRANGER (force=1 when the key still holds rows). It removes EVERY record of the key, the readable ones too -- it is the last answer, never the first:

    ycommand -c 'command-yuno id=<id> service=<tranger service> command=delete-key topic_name=items key=k2 force=1'
  4. Open the treedb again (restart the yuno). After a repair from a whole backup, the next load is whole and the keys are forgotten. After a delete-key the key is forgotten at once (a create of its id is accepted with no restart), but a node of that id that loaded from its newest rows stays in memory until the topic is opened again: restart all the same.

The warning of an append never acknowledged needs no repair: nothing fails, and the next append into that day’s file names its content with a row of its own. To quiet it, remove the pair with the yuno STOPPED. Only the two files the warning names, the .md2 of 0 bytes AND its .json, never one of them alone and never the key:

cd <topic_directory>/keys/k2          # topic_directory, key and file_id: from the warning
ls -l 2099-01-01.md2 2099-01-01.json   # the .md2 is 0 bytes
rm 2099-01-01.md2 2099-01-01.json

A .json far larger than one record is worth a look first: a .md2 cut to 0 bytes behind the yuno’s back has the same shape, and then the rows of that file are gone unless a backup has them. Put the pair back from the backup in that case, instead of removing it.


treedb_parent_refs()

Retrieves a list of parent references for a given node using a specified foreign key. The references are formatted according to the provided options.

json_t *treedb_parent_refs(
    json_t       *tranger,
    const char   *fkey,
    json_t       *node,       // NOT owned, pure node
    json_t       *jn_options  // owned, fkey options
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
fkeyconst char *The foreign key field used to retrieve parent references.
nodejson_t *The node whose parent references are to be retrieved. This parameter is not owned by the function.
jn_optionsjson_t *Options for formatting the returned references. This parameter is owned by the function.

Returns

A JSON array containing the parent references. The caller must decrement the reference count when done using the returned value.

Notes

The function supports multiple formatting options for the returned references, including full references, only IDs, and list dictionaries.


treedb_replace_links() replaces the links of a node by the ones the fkey columns of kw name, and touches only what differs. C_NODE runs the same replace for an update-node with autolink, inside treedb_update_node_and_links(), together with the fields and the save.

int treedb_replace_links(
    json_t  *tranger,
    json_t  *node,   // NOT owned, pure node
    json_t  *kw,     // owned
    BOOL    save
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *The child node whose links are replaced. Must be a pure node. Not owned.
kwjson_t *The record. Its fkey columns name the parents the node must have. Owned.
saveBOOLIf TRUE, the node is saved when a link changed.

Returns

Returns 0 when every column was replaced, or -1 when at least one column was refused. Every refusal is logged, and the other columns are processed. A save that fails answers -1 too: every link of the call is taken back in memory and no event is told (see treedb_link_nodes()).

Behavior

For each fkey column of the node’s topic, the refs of the node are compared with the refs of the same column in kw:

A column that kw does not carry is an empty column, and its links are removed (with the warning “fkey empty”). Send every fkey column, or do not use autolink for a partial update.

A column is replaced whole, or not at all (since 7.25.0). Every new ref of a column is checked BEFORE any old link of it is undone, and a column with one ref that cannot be linked keeps the links it has. A ref cannot be linked when:

Before, the old links were undone first and a refused new one failed after, so a node whose only parent was replaced by an impossible one was left with NO parent, on disk, EV_TREEDB_NODE_UNLINKED published. A refused column does not stop the other columns. The caller can still save the record, and repair the link later with treedb_link_nodes().

Example

The node users^alice is linked to departments^engineering. This record keeps that link and adds departments^research:

json_t *kw = json_pack("{s:s, s:s, s:[s, s, s]}",
    "id", "alice",
    "username", "alice_w",
    "departments",
        "departments^engineering^users",
        "departments^research^users"
);
if(treedb_replace_links(tranger, alice, kw, TRUE) < 0) {
    // Error already logged: the column kept the links it had
}

The result: one EV_TREEDB_NODE_LINKED (research), no event for engineering. Had the column also named departments^ghost^users (not found), NOTHING of it would move: no link to research, no event, one error for ghost, and alice keeps engineering.


treedb_save_node()

The treedb_save_node() function directly saves a given node to the tranger database. The record is always written with tag 0 (user_flag), whether a snap is activated or not: only treedb_shoot_snap() tags a record, so a snap holds exactly what was live when it was shot.

int treedb_save_node(
    json_t *tranger,
    json_t *node    // NOT owned, pure node.
);

Parameters

KeyTypeDescription
trangerjson_t *A pointer to the tranger database instance where the node will be saved.
nodejson_t *A pointer to the node to be saved. This node is not owned by the function.

Returns

Returns 0 on success, or a negative error code on failure.

Notes

The record is always written with tag 0, whether a snap is activated or not. A record gets a snap’s tag only once, from treedb_shoot_snap(). A save never gives a tag, so it never writes into a snap. With a snap activated, the primary index is loaded from the records that the snap tagged. An edit made during that time appears in the primary index only after the snap is deactivated.

A node that treedb_delete_node() is deleting cannot be saved: from the moment its key is deleted until the delete returns, a save of it (from a callback of the delete) answers -1 and logs “Cannot save a node that is being deleted”. A record written then would bring the node back from the disk.

A node that no index holds is not saved either: its key was deleted (treedb_delete_node()), or its instance (treedb_delete_instance()). The save answers -1 and logs “Cannot save a node that no index holds: its record would bring back what was deleted”, whatever kept the pointer (a hook of a parent, a caller). The primary index is asked first, by pointer; a node that is not the primary is looked for among the instances of its key. Until 7.25.4 such a save wrote a record into the deleted key, and the node was back at the next open (new after 7.25.4).

json_t *x2 = json_incref(treedb_get_instance(tranger, "my_db", "kids", "version", "x", "v2"));
treedb_delete_instance(tranger, x2, "version", 0);   // 0
treedb_save_node(tranger, x2);                       // -1: no index holds it
json_decref(x2);

A node whose pkey2 value was changed in place is not saved either. A pkey2 value names the instance, and the record is written under the value that the node holds now: the new value is a new instance on disk, while memory keeps the node in the slot of the old value. treedb_update_node() refuses such a change; a direct save of a node that another slot of its key holds answers -1 and logs “Cannot save a node whose pkey2 value changed in place: its record would be another instance”, with topic_name, id, pkey2_name, old_value (the slot that holds the node) and new_value. Put the value back and the node saves again. To make the new value, create the instance. Only a topic with pkey2s is asked; an empty value is not (the save does not index it). Until 7.25.4 such a save wrote the new instance to disk, and when the new value was the one of another instance, it took that instance’s slot in memory (new after 7.25.4).

json_t *x = treedb_get_node(tranger, "my_db", "kids", "x");   // x/v1, the primary
json_object_set_new(x, "version", json_string("v9"));
treedb_save_node(tranger, x);                                 // -1: v9 would be a new instance
json_object_set_new(x, "version", json_string("v1"));
treedb_save_node(tranger, x);                                 // 0
treedb_create_node(tranger, "my_db", "kids",                  // the new instance: x/v9
    json_pack("{s:s, s:s}", "id", "x", "version", "v9"));

A save never takes the slot the primary holds. A save points the slot of each pkey2 value of the node at the node, and with MORE than one pkey2 an instance shares with the primary the slots of the values they have in common. The slot the primary holds stays the primary’s: the invariant “the instance of the primary’s value is the primary” (see treedb_get_instance()) holds with any number of pkey2s. Up to 7.25.4 a save of the instance took it: a lookup of the primary’s value answered the instance, and C_NODE’s delete-node through it tombstoned the rows of the primary too, and answered 0.

/*  pkey2s ['a', 'b']: P (a=1, b=1) is the primary of x, Q (a=1, b=2) an instance  */
treedb_update_node(tranger, Q, json_pack("{s:s}", "note", "q"), TRUE);
treedb_get_instance(tranger, "my_db", "multi", "a", "x", "1");   // P, not Q
treedb_get_instance(tranger, "my_db", "multi", "b", "x", "2");   // Q

treedb_set_callback()

Sets a callback function for treedb_name in tranger. The callback is triggered on node operations such as creation, update, or deletion.

int treedb_set_callback(
    json_t *tranger,
    const char *treedb_name,
    treedb_callback_t treedb_callback,
    void *user_data,
    treedb_callback_flag_t flags
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the tree database.
treedb_nameconst char *Name of the tree database where the callback will be set.
treedb_callbacktreedb_callback_tFunction pointer to the callback that will be invoked on node operations.
user_datavoid *User-defined data that will be passed to the callback function.

Returns

Returns 0 on success, or a negative error code on failure.

Notes

The callback function must follow the treedb_callback_t signature and will receive parameters such as tranger, treedb_name, topic_name, operation, and node. The callback is triggered on events like EV_TREEDB_NODE_CREATED, EV_TREEDB_NODE_UPDATED, and EV_TREEDB_NODE_DELETED.


treedb_set_files_limits()

The ceiling of a treedb’s file columns: the largest file one write may cost this process, and the mime types the store will ever hold. A column may NARROW it with its properties.max_size / properties.content_types, never raise it. Without a call the ceiling is 128 MB and the sixteen types of treedb_content_type_of_name(). C_TREEDB forwards files_max_size / files_content_types of open-treedb here.

int treedb_set_files_limits(
    json_t      *tranger,
    const char  *treedb_name,
    json_int_t  max_size,
    json_t      *content_types  // owned
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb.
treedb_nameconst char *The treedb (must be open).
max_sizejson_int_tBytes. 0 keeps the current ceiling.
content_typesjson_t *Owned. A list of mime types; NULL keeps the current list.

Returns

0, or -1 with “TreeDB not found” or “files content_types must be a list” logged.

Example

treedb_set_files_limits(tranger, "treedb_wattyzer",
    20*1024*1024,
    json_pack("[s,s,s]", "image/jpeg", "image/png", "application/pdf")
);

treedb_set_node_immutable()

treedb_set_node_immutable() marks (or unmarks) a single node as immutable — once set, the record cannot be deleted by treedb_delete_node() or treedb_delete_instance(), and force does NOT override it.

int treedb_set_node_immutable(
    json_t *tranger,
    json_t *node,   // NOT owned, pure node.
    BOOL   set
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *The node to mark/unmark. NOT owned, must be a pure node.
setBOOLTRUE to mark the node immutable, FALSE to clear the mark.

Returns

Returns 0 on success, or a negative error code on failure.

Notes

The mark rides the md2 system_flag bit (sf_immutable_record), NOT a data column: it persists across reload and is surfaced as __md_treedb__immutable. No user-schema change and no topic_version` bump are required.

The current primary record is rewritten in place (no new record is appended). treedb_save_node() re-applies the bit on every later update, like the snap tag — so the mark is inherited across updates.

To protect a whole topic from deletion (rather than individual records), pass system_topic = TRUE to treedb_create_topic().


treedb_set_trace()

Enables or disables trace logging for the TreeDB system.

int treedb_set_trace(
    BOOL set
);

Parameters

KeyTypeDescription
setBOOLIf TRUE, enables trace logging. If FALSE, disables it.

Returns

Returns TRUE if trace logging was successfully enabled or disabled, otherwise FALSE.

Notes

This function is useful for debugging and monitoring TreeDB operations.


treedb_shoot_snap()

Captures the current primary set of every user topic by stamping each primary record’s user_flag field with the snap’s id. The snap is registered as a row in __snaps__ (assigned an integer id from its g_rowid). That same id is then written in place via tranger2_write_user_flag() on the live .md2 record of each current primary. The snap is created with active: false — use treedb_activate_snap() to switch to it.

int treedb_shoot_snap(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *snap_name,
    const char  *description
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB instance to snapshot.
snap_nameconst char *Name assigned to the snapshot. Must be unique within the treedb — the call fails if one already exists. The literal "__clear__" is reserved and cannot be used.
descriptionconst char *Optional description providing details about the snapshot.

Returns

Returns 0 on success, or a negative error code on failure (snap already exists, or the snap id will exceed the 16-bit user_flag ceiling — that is, >= 0xFFFF snaps in this treedb’s history).

Behavior

For every user topic — and for __graphs__, the one meta-topic the photo carries — the function walks the primary index and, for each current primary node, calls tranger2_write_user_flag(tranger, topic_name, key, t, i_rowid, snap_id). This modifies the underlying .md2 record byte without appending a new instance — so the chronological rowid order is preserved, and tranger2_read_user_flag() on the same (topic, key, t, rowid) immediately returns the new tag.

Because the tag rides on the existing record, the snap captures exactly the primaries that were live at shoot-time — including records originally written with user_flag = 0. The tag stays on THAT record: a later save of the node appends an untagged record (see treedb_save_node()), so the snap goes on holding what the node was when it was shot.

When the next shoot finds a primary record that already carries a tag from an earlier snap (that is, __md_treedb__.tag != 0 && != snap_id), the function appends a clone of that record via tranger2_append_record() with the new snap’s id, rather than overwriting the prior tag in place. The cloned record sits at a higher rowid and carries only the new snap’s tag. The original record keeps its earlier tag intact. This makes multiple snaps over an unchanged set of primaries co-exist: activate-snap of either snap can find its own tagged records on reload. Untagged primaries still take the cheaper in-place path — no clone cost when the record is snapped for the first time.

The clone is the newest record of the node. When another instance of its key wrote a newer record (a new instance, created after the load: the primary of the next reload), that record is written again after the clone, untagged, so a shot does not choose the primary of the next reload: up to 7.25.4 the clone was the newest record, and the new instance lost to the photo. The node in memory moves to the clone at once (g_rowid, i_rowid, t, tm and tag in __md_treedb__), and an immutable node keeps its immutable bit on the clone. The clone does not publish EV_TREEDB_NODE_UPDATED.

/*  a/v1 is tagged by s1; a/v2 is created after it (the primary of the next reload)  */
treedb_shoot_snap(tranger, "treedb_links", "s2", "second");
    // 0: a clone of a/v1 tagged s2, then the record of a/v2 again, untagged
/*  reopen: a/v2 is the primary; activate s2 and reopen: a/v1 is  */

The layout travels with the photo. __graphs__ holds how the treedb was arranged — one record per topic, written by the graph view — and a snap tags it like any other topic, so an activation reads back the arrangement of the shot and not the one in use. treedb_open_db() opens __graphs__ filtered by the activated tag for exactly that. A snap shot before anything was arranged holds no layout, so activating it leaves __graphs__ empty and the graph falls back to its automatic layout. The other two meta-topics stay out: __snaps__ cannot tag itself, and __assets__ is held another way — assets_held_by_snaps() walks the links of the records the snap froze, because its blobs are shared by every treedb of the tranger.

/*  the arrangement of `devices`, as the graph view writes it  */
treedb_create_node(tranger, treedb_name, "__graphs__",
    json_pack("{s:s, s:s, s:b, s:{s:{s:{s:i, s:i}}}}",
        "id", "devices", "topic", "devices", "active", 1,
        "properties", "nodes", "dev-a", "x", 10, "y", 20));

treedb_shoot_snap(tranger, treedb_name, "arranged", "");
/*  ... the cards are moved and saved again ...  */
treedb_activate_snap(tranger, treedb_name, "arranged");  // reload: x is 10 again

What a snap holds, and for how long. Only shoot-snap tags records, and a save is always written with tag 0. So activate-snap returns every topic to what it was when the snap was shot: rows created after it are absent, and rows updated after it show their content at the shot. This is also true for rows written WHILE the snap is activated. Two earlier rules broke this. Up to 7.22.x a save inherited the node’s tag, so the latest snap followed every later update (fixed in 7.23.0). In 7.23.x a save took the tag of the activated snap, so a binary installed during a rollback went into the photo (fixed in 7.24.0). Two guards follow the snap rather than the node’s tag in memory:

After deactivate-snap, shoot-snap waits for a reload. The refusal of a shot while a snap is active (7.25.0) also holds while the treedb is still LOADED from one: deactivate-snap clears the tag on disk but the primary index in memory is the filtered one until the treedb is opened again, and a shot of that index would be a shot of the photo. C_NODE has no reload command; the agent’s deactivate-snap reloads (restart_nodes()), any other yuno is restarted. Until then shoot-snap answers “reload it first”.

treedb_shoot_snap(tranger, "treedb_yuneta_agent", "pre-upgrade", "before 7.23");
// ... rows created, rows updated ...
treedb_activate_snap(tranger, "treedb_yuneta_agent", "pre-upgrade");   // reload: the state at the shot

Notes

Snapshots allow restoring the TreeDB to a previous state using treedb_activate_snap(). Like all snap operations, the visibility change is materialised on the next treedb_open_db(), not in memory at call time — see treedb_activate_snap() for the reload semantics.


treedb_sniff_content_type()

The mime type the BYTES say, from their first bytes (magic numbers). Containers shared by several types answer one representative (video/mp4 for the ISO-BMFF family, video/webm for EBML, audio/ogg for Ogg); the declared type then picks the member. An SVG or any HTML/XML text is recognised on purpose, as image/svg+xml / text/html, so the allowlist can REFUSE it by name: declared as image/png by a client, it would otherwise walk past the check.

const char *treedb_sniff_content_type(
    const char  *data,
    size_t      len
);

Parameters

KeyTypeDescription
dataconst char *The first bytes of the file (a few dozen are enough).
lensize_tHow many. Under 4 answers "".

Returns

A static string: a mime type, or "" when the content is not recognised.

Example

treedb_sniff_content_type("\x89PNG\r\n\x1a\n....", 12);   /* "image/png" */
treedb_sniff_content_type("  <svg xmlns=...", 16);          /* "image/svg+xml": refused */

treedb_store_files()

The write path of a record with file columns: it consumes the __files__ manifest (and the kw’s gbuffer), stores the bytes under .blobs/, creates or refreshes the __assets__ node and rewrites every file column into its full fkey reference, __assets__^<id>^as_<topic>_<column>. treedb_create_node(), treedb_update_node() and treedb_autolink() call it; a direct caller needs it only for a kw that never goes through them. Idempotent: a second pass finds full references and no manifest, and does nothing. Design: DESIGN-treedb-files.md.

int treedb_store_files(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name,
    json_t      *kw     // NOT owned, modified in place
);

Parameters

KeyTypeDescription
trangerjson_t *The tranger of the treedb.
treedb_nameconst char *The treedb.
topic_nameconst char *The topic of the record.
kwjson_t *The record as it arrived. Modified: each file column leaves holding its reference, and __files__, gbuffer and __username__ are dropped. A column with a bare id and no manifest names an asset that must already exist.

The manifest has two doors, one per transport. The second one slices the kw’s single gbuffer, which carries the bytes of every column:

"__files__": {"plano": {"content64": "...", "original_name": "plano.pdf", "content_type": "application/pdf"}}
"__files__": {"plano": {"offset": 0, "size": 51234, "original_name": "plano.pdf", "content_type": "application/pdf"}}

Returns

0, or -1 with the cause in gobj_log_last_message() (a file column not flagged fkey, bytes that do not match the declared type, a type outside the allowlist, a file over the ceiling).

Example

json_t *kw = json_pack("{s:s, s:{s:{s:s, s:s, s:s}}}",
    "id", "nave-1",
    "__files__",
        "foto", "content64", b64, "original_name", "nave-1.jpg", "content_type", "image/jpeg"
);
if(treedb_store_files(tranger, "treedb_yunovatioscedb", "places", kw) == 0) {
    /* kw.foto == "__assets__^<sha256>^as_places_foto" */
}

treedb_topic_pkey2s()

treedb_topic_pkey2s() returns a list of primary key secondary values (pkey2s) for a given topic in the tree database.

json_t *treedb_topic_pkey2s(
    json_t      *tranger,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
topic_nameconst char *The name of the topic whose pkey2s values are to be retrieved.

Returns

A JSON list containing the pkey2s values of the specified topic. The returned value is not owned by the caller.

Notes

The returned list must not be modified or freed by the caller.


treedb_topic_pkey2s_filter()

treedb_topic_pkey2s_filter() retrieves a filtered list of primary key secondary values (pkey2s) for a given topic in a TreeDB, based on the provided node and identifier.

json_t *treedb_topic_pkey2s_filter(
    json_t      *tranger,
    const char  *topic_name,
    json_t      *node,      // NOT owned
    const char  *id
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the TimeRanger instance managing the TreeDB.
topic_nameconst char *The name of the topic from which to retrieve pkey2s values.
nodejson_t *A JSON object representing the node to filter against. This parameter is not owned by the function.
idconst char *The primary key identifier used to filter the pkey2s values.

Returns

Returns a JSON array containing the filtered pkey2s values. The returned object is not owned by the caller and must not be modified or freed.

Notes

This function is useful for retrieving secondary key values associated with a primary key in a structured TreeDB topic. The filtering is based on the provided node and id parameters.


treedb_create_system_schema()

The treedb meta-schema: the schema that describes what a schema may say.

json_t *treedb_create_system_schema(void);

Returns

The meta-schema, which the caller owns.

Notes

It is what validates a treedb schema before it is opened, so a schema that declares an unknown column type or flag is refused where it is written instead of failing later, once, on the record that happens to use it.


treedb_topic_size()

treedb_topic_size() returns the number of nodes in the specified topic within the given TreeDB instance.

size_t treedb_topic_size(
    json_t      *tranger,
    const char  *treedb_name,
    const char  *topic_name
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger instance managing the TreeDB.
treedb_nameconst char *Name of the TreeDB instance containing the topic.
topic_nameconst char *Name of the topic whose node count is to be retrieved.

Returns

Returns the number of nodes in the specified topic.

Notes

If the topic does not exist, the function can return 0.


treedb_topics()

treedb_topics() retrieves a list of topic names from the specified TreeDB, optionally returning detailed information in dictionary format.

json_t *treedb_topics(
    json_t       *tranger,
    const char   *treedb_name,
    json_t       *jn_options  // "dict" return list of dicts, otherwise return list of strings
);

Parameters

KeyTypeDescription
trangerjson_t *A reference to the tranger database instance.
treedb_nameconst char *The name of the TreeDB from which to retrieve topic names.
jn_optionsjson_t *Options for the output format. If set to "dict", returns a list of dictionaries. Otherwise, returns a list of strings.

Returns

A JSON array containing the topic names or a list of dictionaries if jn_options is set to "dict". The returned value is not owned by the caller.

Notes

The returned JSON object must not be modified or freed by the caller. Use treedb_list_treedb() to retrieve available TreeDB names.


The treedb_unlink_nodes() function removes the hierarchical relationship between a parent and a child node in the tree database, identified by the specified hook.

int treedb_unlink_nodes(
    json_t      *tranger,
    const char  *hook,
    json_t      *parent_node,    // NOT owned, pure node
    json_t      *child_node      // NOT owned, pure node
);

Parameters

KeyTypeDescription
trangerjson_t *A pointer to the tranger database instance.
hookconst char *The name of the hook defining the relationship to be removed.
parent_nodejson_t *A pointer to the parent node from which the child node will be unlinked. This node is not owned by the function.
child_nodejson_t *A pointer to the child node that will be unlinked from the parent node. This node is not owned by the function.

Returns

Returns 0 on success, or a negative error code if the unlinking operation fails. A child whose fkey does not name parent_node is not linked to it, so the call is refused with “Cannot unlink, the child does not hang from that parent”: the hook and the child are left as they are, no EV_TREEDB_NODE_UNLINKED is published and the child is not saved. The check is the same for the three shapes of an fkey: the string itself, one of the strings of an array, or a key of a dict.

// ch hangs from p2
treedb_unlink_nodes(tranger, "departments", p1, ch);   // -1, refused, ch untouched
treedb_unlink_nodes(tranger, "departments", p2, ch);   // 0, UNLINKED published, ch saved

Notes

The function does not take ownership of parent_node or child_node. This means the caller is responsible for managing their memory. Make sure that the specified hook exists before calling treedb_unlink_nodes().

A save of the child that fails takes the unlink back in memory, answers -1, and tells no event (see treedb_link_nodes()).

The link undone is the child KEY’s. A child’s fkey names the parent’s key, and a new instance of the child inherits the fkeys of its primary at its create, while a hook holds one object per child id: so the OTHER instances of the child name the parent too, and no hook holds them. The unlink clears the ref in each of them and saves each one, before the child (the primary of the key last among them, the child after all: a save makes its record the newest of its key, the one a reload takes for the primary). When one of those saves fails, or the child’s does, all of them are put back as they were, saved again, the instance that wrote the newest record of the key before the unlink writes it again, last, and the unlink answers -1. Not for a file column (a parent in __assets__): an asset is what each instance holds, its own. Up to 7.25.4 the other instances kept naming the parent: a delete of it without force went, and they named a node that is gone (“Node not found” at the next open, once one of them was the newest record); or, once one of them was the newest record, the reload hung the child from the parent it was unlinked from.

/*  a/v1 hangs from P through `kids`; a/v2 was created after, and inherited
 *  a["parent"] = "parents^P^kids"  */
treedb_unlink_nodes(tranger, "kids", P, a1);   // 0: a/v1 AND a/v2 have parent ""

An instance of the child that the parent’s hook does not hold is no error of the unlink either: the hook holds another instance of the child, or, for an instance that is not the primary, nothing (it inherited the ref and no hook took it). What happens to the instance the hook holds depends on the call:

A relink of such an instance to another parent logged “Child data not found in dict parent hook”, of a list hook, though the relink went, and a dict hook dropped the other instance with it (up to 7.25.4). A primary that names the parent and that no hook holds is a hook that lost its child: “Child data not found in list parent hook” (or “... in dict parent hook”).

/*  b/v1 hangs from P through `kids`; b/v2 was created after, and inherited
 *  b["parent"] = "parents^P^kids". P's hook holds b/v1.  */
treedb_link_nodes(tranger, "kids", Q, b2);     // 0: b/v2 in Q; b/v1 stays in P, naming P
/*  or, instead of that relink:  */
treedb_unlink_nodes(tranger, "kids", P, b2);   // 0: P holds nothing; b/v1 and b/v2 have parent ""

treedb_update_node()

treedb_update_node() updates an existing node with the provided fields from kw, without modifying foreign keys (fkeys) or hook fields.

json_t *treedb_update_node(
    json_t *tranger,
    json_t *node,   // NOT owned, pure node.
    json_t *kw,     // owned
    BOOL save
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *Pointer to the existing node to be updated. This parameter is not owned by the function.
kwjson_t *JSON object containing the fields to update. This parameter is owned by the function.
saveBOOLIf TRUE, the updated node is saved to the database.

Returns

Returns a pointer to the updated node. The returned node is not owned by the caller. Returns NULL if the update is refused. A refused update does not change the node and does not save it.

A saved update (save TRUE) on a replica is refused before the node moves, with “Cannot update node, NO master”: the append would be refused anyway, and the node in memory used to take the update all the same, answered as if written, until the next reload. A memory-only update (save FALSE) is a replica’s business and goes on. And a save that fails on a master answers NULL too, where the node was answered whatever the save said.

A save that fails takes the update back. The update changes the node in memory first and saves it after. When the save fails (the files of the key cannot be written), the node in memory goes back to what the disk has: the fields the update replaced, and the links of its file columns. No event is told, EV_TREEDB_NODE_UPDATED included. In 7.25.4 the node kept the update: a read answered a value the disk never held, and a retry of the same update found nothing to write.

/*  the files of the key of `node` are read-only  */
json_t *n = treedb_update_node(tranger, node, json_pack("{s:s}", "name", "x"), TRUE);
/*  n == NULL, the CRITICALs of the write logged, node["name"] as before, no event  */
/*  tranger opened with "master": false  */
json_t *n = treedb_update_node(tranger, node, json_pack("{s:s}", "name", "x"), TRUE);
/*  n == NULL, one error logged, node unchanged  */

Notes

Foreign keys (fkeys) and hook fields are not updated by treedb_update_node(). The returned node must not be modified or freed by the caller.

A pkey2 value names an instance, so an update cannot change it. A kw that carries a pkey2 with a different value, the empty string included, is refused (“An update cannot change a pkey2 value, create the instance”). A kw that carries the same value is an ordinary update. To add an instance, create it:

/*  topic `binaries`, pkey2s: 'version'  */
json_t *node = treedb_get_node(tranger, treedb_name, "binaries", "ycommand");

/*  Refused: this would move the node to another instance  */
treedb_update_node(tranger, node, json_pack("{s:s}", "version", "7.21.0"), TRUE);

/*  Right: a second instance of the same id  */
treedb_create_node(tranger, treedb_name, "binaries",
    json_pack("{s:s, s:s}", "id", "ycommand", "version", "7.21.0"));

Only the fields the kw carries are normalized — with ONE exception, a column flagged now: the clock writes it, not the caller, so no kw ever carries one. Every write stamps it, create and update, writable or not, persistent or volatile (since after 7.24.1; 7.24.0 stamped it on an update only when it was writable). The instant a record was BORN is a time column without now: the create gives it the clock when the kw brings no value, and an update leaves it alone. A now column is an integer epoch; of any other type it is not stamped.

/*  `now`: every write stamps it -- when this record was last written  */
"updated", "id","updated", "type","integer", "flag",["persistent","time","now"]

/*  `time` alone: the create stamps it -- when this record was born  */
"t",       "id","t",       "type","integer", "flag",["persistent","time"]
/*  `time` moves to the clock although the kw says nothing about it  */
treedb_update_node(tranger, layout,
    json_pack("{s:o}", "properties", json_pack("{s:{s:i,s:i}}",
        "dev-a", "x", 30, "y", 40)),
    TRUE);

treedb_update_node_and_links() writes a record over a node as ONE write: its fields (as treedb_update_node() does), its links replaced by the ones the fkey columns of the record name (as treedb_replace_links() does), and a save. It is what C_NODE runs for an update-node with autolink.

json_t *treedb_update_node_and_links(
    json_t  *tranger,
    json_t  *node,          // NOT owned, pure node
    json_t  *kw,            // owned
    BOOL    with_fields,    // FALSE: the links alone
    BOOL    *links_refused  // optional
);

Parameters

KeyTypeDescription
trangerjson_t *Pointer to the tranger database instance.
nodejson_t *The node to write. Must be a pure node. Not owned.
kwjson_t *The record: its fields, and in its fkey columns the parents the node must have. Owned.
with_fieldsBOOLTRUE: write the fields and the links. FALSE: write the links alone, for a node just created from the same record (the create wrote its fields).
links_refusedBOOL *Optional. Set to TRUE when a link could not be made, else FALSE.

Returns

The node (NOT yours), or NULL when the update is refused (for the same causes as treedb_update_node(): a replica, a field the schema refuses, a changed pkey2 value) or the save fails. Every failure is logged, and after a NULL nothing moved.

Notes

A link that cannot be made is logged and skipped, *links_refused is set, and the record is saved all the same: a link can be repaired later, a lost record cannot. The causes of a refused link are the ones of treedb_replace_links(), and its column keeps the links it had.

The node is saved always, also when no field and no link changed.

A save that fails takes the whole write back. The fields, the links and the hooks of the parents go back in memory to what the disk has, and none of the events of the write is told. When the save goes through, the events are told in their order, EV_TREEDB_NODE_UPDATED last.

In 7.25.4 C_NODE made this write with three calls: treedb_update_node() without save, treedb_replace_links() without save, and treedb_save_node(). The first two closed as successes, so a failed save took nothing back: memory kept the fields and the links until the next load, and EV_TREEDB_NODE_LINKED had been told.

Example

/*  alice hangs from nobody; `ghost` is not a department  */
BOOL links_refused = FALSE;
json_t *n = treedb_update_node_and_links(
    tranger,
    alice,
    json_pack("{s:s, s:s, s:[s, s]}",
        "id", "alice",
        "username", "Alice",
        "departments",
            "departments^direction^users",
            "departments^ghost^users"
    ),
    TRUE,
    &links_refused
);
/*  n == alice, saved once with username "Alice"; links_refused is TRUE,
 *  the column is replaced whole or not at all, so alice still hangs from
 *  nobody, direction included ("fkey reference: parent node not found")  */
/*  the same call, with the files of the key of alice read-only  */
/*  n == NULL, the CRITICALs of the write logged, alice as the disk has
 *  her (username and links), direction does not hook her, no event        */

get_hook_list()

get_hook_list() converts hook data of various JSON types into a uniform JSON array of child node references. This normalizes the different internal representations of hook data (array, object, or dict) into a single list format for iteration.

json_t *get_hook_list(
    hgobj gobj,
    json_t *hook_data
);

Parameters

KeyTypeDescription
gobjhgobjThe GObj instance used for logging.
hook_datajson_t *Not owned. The hook field data from a node. Can be a JSON array (returned as-is with incremented refcount), a JSON object (values are collected into a new array), or a JSON string (currently unsupported, logs an error).

Returns

A new JSON array containing the child references from the hook data. The caller owns the returned array and must call json_decref() on it. Returns NULL if the hook data type is not supported.

Notes

When hook_data is a JSON array, the returned array is the same object with an incremented reference count. When it is a JSON object (dict-based hook), the object values are extracted into a new array.


topic_desc_fkey_names()

topic_desc_fkey_names() extracts the names of all foreign key (fkey) fields from a topic descriptor. It iterates over the columns in the topic descriptor and collects the id of each column whose flag contains the word "fkey".

json_t *topic_desc_fkey_names(
    json_t *topic_desc
);

Parameters

KeyTypeDescription
topic_descjson_t *Owned. A JSON array describing the topic columns (the topic descriptor). It is consumed (decremented) by this function.

Returns

A new JSON array of strings, each being the id of a column flagged as fkey. The caller owns the returned array and must call json_decref() on it.

Notes

The topic_desc parameter is consumed by this function. Do not use it after calling topic_desc_fkey_names(). See also topic_desc_hook_names() for the equivalent function for hook fields.


topic_desc_hook_names()

topic_desc_hook_names() extracts the names of all hook fields from a topic descriptor. It iterates over the columns in the topic descriptor and collects the id of each column whose flag contains the word "hook".

json_t *topic_desc_hook_names(
    json_t *topic_desc
);

Parameters

KeyTypeDescription
topic_descjson_t *Owned. A JSON array describing the topic columns (the topic descriptor). It is consumed (decremented) by this function.

Returns

A new JSON array of strings, each being the id of a column flagged as hook. The caller owns the returned array and must call json_decref() on it.

Notes

The topic_desc parameter is consumed by this function. Do not use it after calling topic_desc_hook_names(). See also topic_desc_fkey_names() for the equivalent function for fkey fields.