Collections and Edges
Resources in an OmniData container are not flat. They exist in hierarchies (collections), have relationships with each other (edges), and carry labels (tags). These tables live in memory.db and provide the graph structure that turns a bag of resources into an organized knowledge base.
Database location
Collections, edges, and tags are stored in memory.db, not index.db. This separation keeps the knowledge graph independent from the resource index, allowing independent compaction, backup, and migration of each database.
Because resources live in index.db and graph structures live in memory.db, references between them are logical — not enforced by SQLite foreign keys. The resource_id columns in these tables refer to omnidata_resources.id in index.db, but no cross-database FK constraint exists. Implementations must handle referential integrity at the application layer.
Collections
The omnidata_collections table represents named groupings — folders, threads, projects, albums, or any organizational container.
CREATE TABLE omnidata_collections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
parent_id TEXT REFERENCES omnidata_collections(id),
description TEXT,
metadata TEXT DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
deleted_at TEXT
);
Collections support nesting via the parent_id self-reference. A collection with parent_id = NULL is a root collection. This allows arbitrary depth: a project contains folders, a folder contains subfolders.
Resources are placed into collections using edges (see below) with the contains relationship type.
Edges
The omnidata_edges table stores typed, directed relationships between resources and/or collections.
CREATE TABLE omnidata_edges (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
edge_type TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
deleted_at TEXT
);
The source_id and target_id columns may reference either a collection ID (within memory.db) or a resource ID (in index.db). The edge type determines the expected referent types.
Edge types
| Type | Meaning |
|---|---|
contains |
Source collection contains target resource or sub-collection |
replies_to |
Target is a reply to source (messages, comments) |
annotates |
Target is an annotation on source (highlights, notes) |
links_to |
Source references target (hyperlinks, citations) |
derived_from |
Target was produced from source (summaries, transcriptions) |
attachment_of |
Target is an attachment of source (email attachments, file uploads) |
thread_member |
Source and target belong to the same conversation thread |
Edges are directed: source_id -> target_id. The edge_type determines the semantic meaning of the direction. For example, a contains edge flows from collection to resource; a replies_to edge flows from the reply to the original message.
Cross-database queries
Since edges in memory.db reference resource IDs in index.db, querying the full graph requires opening both databases. In SQLite, use ATTACH DATABASE to join across them:
ATTACH DATABASE 'index.db' AS idx;
-- Find all resources in a collection
SELECT r.* FROM idx.omnidata_resources r
JOIN omnidata_edges e ON e.target_id = r.id
WHERE e.source_id = ? AND e.edge_type = 'contains' AND e.deleted_at IS NULL;
Or in application code, query memory.db for the edge set, then look up matching resources in index.db:
# Step 1: get resource IDs from memory.db
edge_rows = memory_conn.execute("""
SELECT target_id FROM omnidata_edges
WHERE source_id = ? AND edge_type = 'contains' AND deleted_at IS NULL
""", (collection_id,)).fetchall()
resource_ids = [row[0] for row in edge_rows]
# Step 2: fetch resources from index.db
placeholders = ",".join("?" * len(resource_ids))
resources = index_conn.execute(f"""
SELECT * FROM omnidata_resources
WHERE id IN ({placeholders}) AND deleted_at IS NULL
""", resource_ids).fetchall()
Tags
The omnidata_tags table provides flat labels for resources. Tags are simpler than collections — no hierarchy, no nesting, just a string attached to a resource.
CREATE TABLE omnidata_tags (
id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL,
tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
deleted_at TEXT
);
Note that resource_id references omnidata_resources.id in index.db. The same cross-database reference pattern applies here — no FK constraint, application-layer integrity.
Tags are useful for cross-cutting concerns: "important", "follow-up", "quarterly-review", "shared-with-team". A resource can have many tags, and the same tag string can appear on many resources.
ATTACH DATABASE 'index.db' AS idx;
-- Find all resources tagged "important"
SELECT r.* FROM idx.omnidata_resources r
JOIN omnidata_tags t ON t.resource_id = r.id
WHERE t.tag = 'important' AND t.deleted_at IS NULL AND r.deleted_at IS NULL;