PRAGMA foreign_keys = ON; CREATE TABLE notebooks ( id TEXT PRIMARY KEY, parent_id TEXT, name TEXT NOT NULL CHECK (length(trim(name)) > 0), slug TEXT, icon TEXT, color TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), deleted_at TEXT, CHECK (parent_id IS NULL OR parent_id <> id), FOREIGN KEY (parent_id) REFERENCES notebooks(id) ON UPDATE CASCADE ON DELETE RESTRICT ); CREATE INDEX idx_notebooks_parent ON notebooks(parent_id, is_deleted, sort_order, name); CREATE INDEX idx_notebooks_deleted ON notebooks(is_deleted, updated_at); -- Keep triggers on one physical line. Remote D1 currently misparses -- multi-line CREATE TRIGGER statements even though local D1 accepts them. CREATE TRIGGER trg_notebooks_prevent_cycles BEFORE UPDATE OF parent_id ON notebooks FOR EACH ROW WHEN NEW.parent_id IS NOT NULL BEGIN WITH RECURSIVE ancestors(id, parent_id) AS (SELECT id, parent_id FROM notebooks WHERE id = NEW.parent_id UNION ALL SELECT n.id, n.parent_id FROM notebooks n INNER JOIN ancestors a ON n.id = a.parent_id WHERE a.parent_id IS NOT NULL) SELECT RAISE(ABORT, 'notebook cycle detected') WHERE EXISTS (SELECT 1 FROM ancestors WHERE id = NEW.id); END; CREATE TABLE memos ( id TEXT PRIMARY KEY, notebook_id TEXT NOT NULL, title TEXT, excerpt TEXT NOT NULL DEFAULT '', tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json)), is_pinned INTEGER NOT NULL DEFAULT 0 CHECK (is_pinned IN (0, 1)), is_archived INTEGER NOT NULL DEFAULT 0 CHECK (is_archived IN (0, 1)), is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)), source_memo_ids TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memo_ids)), merge_source_count INTEGER NOT NULL DEFAULT 0 CHECK (merge_source_count >= 0), merged_into_memo_id TEXT, merged_at TEXT, created_by TEXT NOT NULL DEFAULT 'user', updated_by TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), deleted_at TEXT, FOREIGN KEY (notebook_id) REFERENCES notebooks(id) ON UPDATE CASCADE ON DELETE RESTRICT, FOREIGN KEY (merged_into_memo_id) REFERENCES memos(id) ON UPDATE CASCADE ON DELETE SET NULL ); CREATE INDEX idx_memos_notebook_feed ON memos(notebook_id, is_deleted, updated_at DESC); CREATE INDEX idx_memos_archive_feed ON memos(is_archived, is_deleted, updated_at DESC); CREATE INDEX idx_memos_merge_target ON memos(merged_into_memo_id); CREATE TABLE memo_contents ( memo_id TEXT PRIMARY KEY, schema_version INTEGER NOT NULL DEFAULT 1, content_json TEXT NOT NULL CHECK (json_valid(content_json)), content_markdown TEXT NOT NULL DEFAULT '', content_text TEXT NOT NULL DEFAULT '', content_hash TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), FOREIGN KEY (memo_id) REFERENCES memos(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX idx_memo_contents_revision ON memo_contents(memo_id, revision); CREATE VIRTUAL TABLE memos_fts USING fts5( memo_id UNINDEXED, title, content_text, tags ); CREATE TABLE resources ( id TEXT PRIMARY KEY, memo_id TEXT NOT NULL, original_memo_id TEXT, bucket_name TEXT NOT NULL DEFAULT 'edgeever-resources', object_key TEXT NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('image', 'attachment')), mime_type TEXT, filename TEXT, byte_size INTEGER NOT NULL DEFAULT 0 CHECK (byte_size >= 0), sha256 TEXT, width INTEGER CHECK (width IS NULL OR width > 0), height INTEGER CHECK (height IS NULL OR height > 0), metadata_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(metadata_json)), is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), deleted_at TEXT, FOREIGN KEY (memo_id) REFERENCES memos(id) ON UPDATE CASCADE ON DELETE RESTRICT, FOREIGN KEY (original_memo_id) REFERENCES memos(id) ON UPDATE CASCADE ON DELETE SET NULL, UNIQUE (bucket_name, object_key) ); CREATE INDEX idx_resources_memo ON resources(memo_id, is_deleted, kind); CREATE INDEX idx_resources_original_memo ON resources(original_memo_id); CREATE INDEX idx_resources_object ON resources(bucket_name, object_key); CREATE TABLE memo_revisions ( id TEXT PRIMARY KEY, memo_id TEXT NOT NULL, revision INTEGER NOT NULL, title TEXT, content_json TEXT NOT NULL CHECK (json_valid(content_json)), content_markdown TEXT NOT NULL DEFAULT '', content_hash TEXT NOT NULL, created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), FOREIGN KEY (memo_id) REFERENCES memos(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX idx_memo_revisions_memo ON memo_revisions(memo_id, revision DESC); CREATE TABLE api_tokens ( id TEXT PRIMARY KEY, name TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, scopes_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(scopes_json)), last_used_at TEXT, expires_at TEXT, is_revoked INTEGER NOT NULL DEFAULT 0 CHECK (is_revoked IN (0, 1)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE TABLE audit_events ( id TEXT PRIMARY KEY, actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'agent', 'system')), actor_id TEXT, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, metadata_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(metadata_json)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE INDEX idx_audit_events_entity ON audit_events(entity_type, entity_id, created_at DESC); INSERT INTO notebooks (id, parent_id, name, slug, icon, color, sort_order) VALUES ('nb_inbox', NULL, '等待分类', 'inbox', 'notebook', '#0f766e', 10), ('nb_projects', NULL, '工作项目', 'work-projects', 'notebook', '#2563eb', 20), ('nb_learning', NULL, '学习资料', 'learning-resources', 'notebook', '#7c3aed', 30), ('nb_creative', NULL, '灵感创作', 'creative-ideas', 'notebook', '#db2777', 40), ('nb_personal', NULL, '生活个人', 'personal-life', 'notebook', '#ea580c', 50); INSERT INTO memos ( id, notebook_id, title, excerpt, tags_json, created_by, updated_by ) VALUES ( 'memo_welcome', 'nb_inbox', '欢迎来到 EdgeEver', '这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。', '["edgeever","welcome"]', 'system', 'system' ); INSERT INTO memo_contents ( memo_id, content_json, content_markdown, content_text, content_hash, revision ) VALUES ( 'memo_welcome', '{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"欢迎来到 EdgeEver"}]},{"type":"paragraph","content":[{"type":"text","text":"这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。"}]},{"type":"paragraph","content":[{"type":"text","text":"接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。"}]}]}', '## 欢迎来到 EdgeEver\n\n这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。\n\n接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。', '欢迎来到 EdgeEver 这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。 接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。', 'seed', 0 ); INSERT INTO memos_fts (memo_id, title, content_text, tags) VALUES ( 'memo_welcome', '欢迎来到 EdgeEver', '欢迎来到 EdgeEver 这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。 接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。', 'edgeever welcome' );