diff --git a/docs/references/file/architecture.md b/docs/references/file/architecture.md index 9895eccfa8..8916d84751 100644 --- a/docs/references/file/architecture.md +++ b/docs/references/file/architecture.md @@ -161,7 +161,7 @@ Reference layer FileEntryHandle FilePathHandle ▼ FileManager.getEntry ▼ fs.stat + projection Data-shape layer FileEntry FileInfo (after resolution) { id, origin, name, ext, { path, name, ext, size, - size, trashedAt, ... } mime, type, modifiedAt, ... } + size, deletedAt, ... } mime, type, modifiedAt, ... } ``` Picking a handle variant is a **call-site choice of reference form**, not a statement about the file itself. Crucially, **the two axes are orthogonal**: @@ -192,7 +192,7 @@ Once a handle is dispatched, the handler works with either a `FileEntry` (the DB **Field overlap is inherent, not redundant**: `name`, `ext`, `type` (and `mime` / `size` on `FileInfo`) describe a file regardless of whether an entry row exists for it. What distinguishes the two types is the *surrounding* fields and the *liveness* of the shared ones: -- **`FileEntry` has identity fields** `FileInfo` lacks: `id`, `origin`, `externalPath`, `trashedAt`. +- **`FileEntry` has identity fields** `FileInfo` lacks: `id`, `origin`, `externalPath`, `deletedAt`. - **`FileInfo` has live fields** `FileEntry` lacks: `path` (derived, never stored on `FileEntry`), `modifiedAt`, and a live `size`. - **`FileEntry.size` is origin-gated**. For `origin='internal'` it is an authoritative byte count (kept in sync by atomic writes). For `origin='external'` it is **always `null`** — external files may change outside Cherry at any time, so no DB snapshot is stored. Consumers that need a live value for an external entry call File IPC `getMetadata(id)`, which runs `fs.stat` on demand. This eliminates the "is this snapshot current?" question at the type level rather than at call sites. - **`FileEntry.name` / `FileEntry.ext` never drift**. For internal they are user-editable SoT; for external they are pure projections of `externalPath` (basename / extname) and therefore stable as long as the entry itself exists. @@ -272,7 +272,7 @@ All operations that can act on any file (FileEntry or arbitrary path) **accept a |---|---| | `createInternalEntry` / `batchCreateInternalEntries` | Create a new Cherry-owned FileEntry (writes to `{userData}/Data/Files/{id}.{ext}`; each call produces an independent new entry, no conflict possible) | | `ensureExternalEntry` / `batchEnsureExternalEntries` | Pure upsert by `externalPath`—the entry point first `canonicalizeExternalPath(raw)` normalizes it (see `pathResolver.ts`); reuses the existing entry with the same path or inserts a new one. Idempotent by design—callers may safely repeat calls. No "restore" branch: external entries cannot be trashed. External rows carry no stored `size` (always `null`); live values come from `getMetadata`. | -| `trash` / `restore` | Soft delete based on trashedAt (DB only). **Internal-origin only** — external-origin entries cannot be trashed (`fe_external_no_trash` CHECK); passing an external id throws. | +| `trash` / `restore` | Soft delete based on deletedAt (DB only). **Internal-origin only** — external-origin entries cannot be trashed (`fe_external_no_delete` CHECK); passing an external id throws. | | `batchTrash` / `batchRestore` | Batch versions of `trash` / `restore` — same internal-origin-only rule. | | `batchPermanentDelete` | Batch version of `permanentDelete`. | | `withTempCopy` | Copy isolation for calling third-party libraries | @@ -299,7 +299,7 @@ All operations that can act on any file (FileEntry or arbitrary path) **accept a | User action | Physical external file | |---|---| -| Trash from Cherry | **Not applicable** — external-origin entries cannot be trashed (`fe_external_no_trash` CHECK) | +| Trash from Cherry | **Not applicable** — external-origin entries cannot be trashed (`fe_external_no_delete` CHECK) | | Restore from Cherry | **Not applicable** — external-origin entries are never trashed | | permanentDelete from Cherry (entry-level) | **Untouched** — only the DB row is deleted; the physical file remains on disk | | write / writeIfUnchanged from Cherry | **Overwritten** (atomic write) | @@ -469,7 +469,7 @@ DataApi endpoints (read-only, SQL-only, fixed-shape): > > Rule of thumb: if a handler must call anything outside the Drizzle / `@db/*` surface to answer the request, it belongs in IPC. If two callers want the same data in different shapes, the answer is **two endpoints**, not one endpoint with a flag. -**List queries for external entries**: DataApi returns the DB row directly — identity (`id`, `origin`, `externalPath`), stable projections (`name`, `ext`), timestamps, `trashedAt`. External rows carry `size: null` by design (no snapshot stored). Consumers needing **live `size` / `mtime`** call File IPC `getMetadata(id)`; those needing only **whether the file currently exists** (dangling) call File IPC `getDanglingState` / `batchGetDanglingStates`. +**List queries for external entries**: DataApi returns the DB row directly — identity (`id`, `origin`, `externalPath`), stable projections (`name`, `ext`), timestamps, `deletedAt`. External rows carry `size: null` by design (no snapshot stored). Consumers needing **live `size` / `mtime`** call File IPC `getMetadata(id)`; those needing only **whether the file currently exists** (dangling) call File IPC `getDanglingState` / `batchGetDanglingStates`. ### 4.1.1 DataApi Boundary: SQL-Only, Fixed Shape @@ -1028,7 +1028,7 @@ src/main/utils/file/ -- pure FS primitives, sole FS owner, open - **External entry is a best-effort reference**: no guarantee the file remains stable, no guarantee content matches the reference-time content. Equivalent to "the user expressed intent to reference this path at some point" semantics in tools like codex - **External entry path is globally unique**: at most one row per `externalPath` at any time, regardless of any state (SQLite global unique index on `externalPath`; internal rows have `externalPath = null` and are exempt, since SQLite treats multiple NULLs as distinct). `ensureExternalEntry` is therefore a pure upsert by path — reuse if an entry exists, otherwise insert; no "restore" branch is possible because external entries cannot be trashed. -- **External entries cannot be trashed**: enforced at the DB layer by `CHECK (origin != 'external' OR trashedAt IS NULL)` (`fe_external_no_trash`). External lifecycle is monotonic: create via `ensureExternalEntry` → update in place via `write` / `rename` → remove via `permanentDelete` (DB row only). There is no soft-delete / restore cycle for external entries. Calling `trash` / `restore` on an external id throws. +- **External entries cannot be trashed**: enforced at the DB layer by `CHECK (origin != 'external' OR deletedAt IS NULL)` (`fe_external_no_delete`). External lifecycle is monotonic: create via `ensureExternalEntry` → update in place via `write` / `rename` → remove via `permanentDelete` (DB row only). There is no soft-delete / restore cycle for external entries. Calling `trash` / `restore` on an external id throws. - **External entries allow explicit user edits**: `write` / `writeIfUnchanged` / `createWriteStream` / `rename` take effect on external (delegated to ops' atomic write / fs.rename), triggered by explicit user action. Cherry does **not** perform automatic / watcher-driven external file modifications - **`permanentDelete` on external is entry-level, not file-level**: removes only the DB row + CASCADE-cleans `file_ref`; the physical file is left untouched. Path-level deletion remains available via `remove(path)` (from `@main/utils/file/fs`, reached through a `FilePathHandle`), which is a separate explicit call not bound to any entry id. - **Cherry does not track rename/move of external files**: an external rename turns the entry dangling; the user must re-@ to establish a new reference diff --git a/docs/references/file/file-manager-architecture.md b/docs/references/file/file-manager-architecture.md index 7439bdb5e5..2f66e35971 100644 --- a/docs/references/file/file-manager-architecture.md +++ b/docs/references/file/file-manager-architecture.md @@ -33,7 +33,7 @@ FileEntry ├── ext: extension (without leading dot), nullable ├── size: bytes ├── externalPath: absolute path, non-null only when origin='external' -├── trashedAt: ms epoch | null +├── deletedAt: ms epoch | null ├── createdAt / updatedAt ``` @@ -650,17 +650,17 @@ The `atomicWriteFile` / `atomicWriteIfUnchanged` / `createAtomicWriteStream` pri ## 6. Deletion and Recycle Bin -### 6.1 trashedAt Model +### 6.1 deletedAt Model -All soft deletes are implemented via the `trashedAt` timestamp, without physically moving files: +All soft deletes are implemented via the `deletedAt` timestamp, without physically moving files: | Operation | Physical impact (internal) | Physical impact (external) | |---|---|---| -| `trash(id)` | None | **N/A** (`fe_external_no_trash` CHECK rejects; external rows cannot be trashed) | +| `trash(id)` | None | **N/A** (`fe_external_no_delete` CHECK rejects; external rows cannot be trashed) | | `restore(id)` | None | **N/A** (no trashed external rows to restore) | | `permanentDelete(id)` | DB delete + best-effort `remove(physicalPath)` (`@main/utils/file/fs`) | **DB delete only — user's file is never modified** (matches `architecture.md §3.4`) | -**trash / restore are internal-only.** External entries cannot be trashed by definition (`fe_external_no_trash` CHECK enforces this); the trash semantics make sense only for files Cherry owns. +**trash / restore are internal-only.** External entries cannot be trashed by definition (`fe_external_no_delete` CHECK enforces this); the trash semantics make sense only for files Cherry owns. **permanentDelete on internal**: DB row is removed first, then the physical file at `{userData}/Data/Files/{id}.{ext}` is best-effort unlinked. Unlink failures (ENOENT, insufficient permissions, etc.) are logged but do not block — the DB-row-gone outcome is what callers observe; any orphaned blob is later cleaned by the startup file sweep (§10). @@ -669,7 +669,7 @@ All soft deletes are implemented via the `trashedAt` timestamp, without physical ### 6.2 Auto Expiry (deferred — lands in Phase 2) > **Status**: design only. Phase 1 ships no expiry timer service, no -> Preferences key, and no `WHERE trashedAt < now() - retentionMs` query. +> Preferences key, and no `WHERE deletedAt < now() - retentionMs` query. > Trashed entries persist until the user runs an explicit > `permanentDelete` (or the startup orphan sweep collects an > already-deleted entry's residual blob). The 30-day window below is @@ -678,7 +678,7 @@ All soft deletes are implemented via the `trashedAt` timestamp, without physical By default trashed entries are cleaned up after 30 days (lifecycle service timer); the user may configure the days or disable it in Preferences. -Query: `WHERE trashedAt < now() - retentionMs` → batch permanentDelete. +Query: `WHERE deletedAt < now() - retentionMs` → batch permanentDelete. ### 6.3 Edge Cases @@ -1114,7 +1114,7 @@ The reverse index of DanglingCache (`Map>`) is built via a si ```sql SELECT id, externalPath FROM file_entry -WHERE origin = 'external' AND trashedAt IS NULL +WHERE origin = 'external' AND deletedAt IS NULL ``` **No stat performed**—the state field (`Map`) is initially empty; lazy stat on query (see §11). @@ -1291,13 +1291,13 @@ Timing for changes to `pathToEntryIds` (fully self-governed inside file_module, | Event | Action | |---|---| -| Startup `initFromDb()` | `SELECT id, externalPath FROM file_entry WHERE origin='external' AND trashedAt IS NULL` → batch add | +| Startup `initFromDb()` | `SELECT id, externalPath FROM file_entry WHERE origin='external' AND deletedAt IS NULL` → batch add | | `ensureExternalEntry` creates new | addEntry(id, path) | | `ensureExternalEntry` reuses (upsert hit) | No change (path already indexed) | | `permanentDelete(external)` | removeEntry(id, path) | | `rename(external)` (explicit user action) | removeEntry(id, oldPath) + addEntry(id, newPath) | -External entries cannot be trashed (`fe_external_no_trash` CHECK enforces this +External entries cannot be trashed (`fe_external_no_delete` CHECK enforces this at the schema level; `trash` / `restore` throw at the entry layer before reaching the reverse-index update). Earlier drafts listed `restore(external)` and `trash(external)` rows here — they were dead branches and have been @@ -1411,7 +1411,7 @@ These thresholds are heuristic starting points — tune based on real-world tele | **Content hash algorithm** | xxhash-h64 | Optimal cost-performance for non-cryptographic scenarios (~20GB/s). 64-bit collision space is sufficient for distinguishing successive versions within a single file's write history — the `xxhash-wasm` package shipped in this version exposes only h32 / h64, and h64 is the strongest variant available; revisit if a 128-bit variant becomes a dependency-cost tradeoff worth taking. | | **Does write carry version** | Split into write / writeIfUnchanged | Force the caller to explicitly choose; avoid silent degradation to blind write when version is forgotten | | **Atomic write fsync** | On by default | Correctness guarantee takes precedence over performance; Cherry is not a high-throughput scenario | -| **Trash model** | trashedAt timestamp | parentId unchanged; naturally supports expiry; no system_trash entries | +| **Trash model** | deletedAt timestamp | parentId unchanged; naturally supports expiry; no system_trash entries | | **pending_fs_ops** | Removed | After extreme simplification, orphan sweep suffices to cover crashes | | **Startup dangling probe** | Removed | Changed to lazy + Promise.all; stat only when an IPC caller explicitly requests dangling state | | **Is Watcher a lifecycle service** | No | DirectoryWatcher is a primitive; business modules `new` it via the factory; file_module doesn't actively watch | @@ -1441,7 +1441,7 @@ This checklist is the canonical addition procedure. A PR introducing a new origi | Location | Change required | |---|---| -| `src/main/data/db/schemas/file.ts` | Review every CHECK constraint naming `origin` — `fe_origin_consistency`, `fe_size_internal_only`, `fe_external_no_trash`, `fe_external_path_unique`, etc. — and decide whether the new variant honors / violates / is exempt from each | +| `src/main/data/db/schemas/file.ts` | Review every CHECK constraint naming `origin` — `fe_origin_consistency`, `fe_size_internal_only`, `fe_external_no_delete`, `fe_external_path_unique`, etc. — and decide whether the new variant honors / violates / is exempt from each | | Drizzle migration | Ship the constraint updates in the same migration as the enum expansion. Partial unique indexes on `externalPath` may need a new branch | | Existing rows | No migration should run for existing rows unless the new variant has a natural subset mapping (unlikely) | @@ -1458,7 +1458,7 @@ Every ad-hoc `if (entry.origin === 'internal')` / `=== 'external'` in the codeba | Policy | Location | |---|---| -| Trash-ability (who can soft-delete) | `trash` / `restore` in FileManager; DB CHECK `fe_external_no_trash` | +| Trash-ability (who can soft-delete) | `trash` / `restore` in FileManager; DB CHECK `fe_external_no_delete` | | Size snapshot storage | `write` / `writeIfUnchanged` internal-DB-update branch; `toFileInfo` projection | | Name / ext as SoT vs projection | `rename` mutation; `toFileInfo` projection; `FileEntrySchema` field docs | | DanglingCache participation | `DanglingCache.check` returns `'present'` for internal; consider where the new variant falls on the `present/missing/unknown` axis | diff --git a/migrations/sqlite-drizzle/0026_sturdy_aqueduct.sql b/migrations/sqlite-drizzle/0026_sturdy_aqueduct.sql new file mode 100644 index 0000000000..e5ea20bfc1 --- /dev/null +++ b/migrations/sqlite-drizzle/0026_sturdy_aqueduct.sql @@ -0,0 +1,36 @@ +-- MANUAL PATCH: workaround for drizzle-orm/drizzle-kit #3653 +-- (rebuild-table path drops the leading ALTER TABLE RENAME COLUMN statement, +-- so the INSERT ... SELECT below references "deleted_at" on the old table +-- which still has the column named "trashed_at" and would fail with +-- "no such column: deleted_at". Fix per yume-chan's patch suggestion.) +-- https://github.com/drizzle-team/drizzle-orm/issues/3653 +ALTER TABLE `file_entry` RENAME COLUMN `trashed_at` TO `deleted_at`;--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_file_entry` ( + `id` text PRIMARY KEY NOT NULL, + `origin` text NOT NULL, + `name` text NOT NULL, + `ext` text, + `size` integer, + `external_path` text, + `deleted_at` integer, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + CONSTRAINT "fe_origin_check" CHECK("__new_file_entry"."origin" IN ('internal', 'external')), + CONSTRAINT "fe_origin_consistency" CHECK(("__new_file_entry"."origin" = 'internal' AND "__new_file_entry"."external_path" IS NULL) OR ("__new_file_entry"."origin" = 'external' AND "__new_file_entry"."external_path" IS NOT NULL)), + CONSTRAINT "fe_external_no_delete" CHECK("__new_file_entry"."origin" != 'external' OR "__new_file_entry"."deleted_at" IS NULL), + CONSTRAINT "fe_size_internal_only" CHECK(("__new_file_entry"."origin" = 'internal' AND "__new_file_entry"."size" IS NOT NULL AND "__new_file_entry"."size" >= 0) OR ("__new_file_entry"."origin" = 'external' AND "__new_file_entry"."size" IS NULL)) +); +--> statement-breakpoint +INSERT INTO `__new_file_entry`("id", "origin", "name", "ext", "size", "external_path", "deleted_at", "created_at", "updated_at") SELECT "id", "origin", "name", "ext", "size", "external_path", "deleted_at", "created_at", "updated_at" FROM `file_entry`;--> statement-breakpoint +DROP TABLE `file_entry`;--> statement-breakpoint +ALTER TABLE `__new_file_entry` RENAME TO `file_entry`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `fe_deleted_at_idx` ON `file_entry` (`deleted_at`);--> statement-breakpoint +CREATE INDEX `fe_created_at_idx` ON `file_entry` (`created_at`);--> statement-breakpoint +-- Functional index on `lower(external_path)`. drizzle-kit's rebuild path +-- wraps the whole expression in backticks (`lower("external_path")`) which +-- SQLite reads as a single quoted identifier instead of a function call; +-- preserve the original 0023 syntax — bare `lower("external_path")` — by hand. +CREATE UNIQUE INDEX `fe_external_path_lower_unique_idx` ON `file_entry` (lower("external_path"));--> statement-breakpoint +CREATE INDEX `fe_external_path_idx` ON `file_entry` (`external_path`); \ No newline at end of file diff --git a/migrations/sqlite-drizzle/meta/0026_snapshot.json b/migrations/sqlite-drizzle/meta/0026_snapshot.json new file mode 100644 index 0000000000..831363681a --- /dev/null +++ b/migrations/sqlite-drizzle/meta/0026_snapshot.json @@ -0,0 +1,3665 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3693bd3e-7915-4bdd-bfe0-7cb58b5984d4", + "prevId": "eda75c54-7724-4482-bfa5-d78033f6c964", + "tables": { + "agent": { + "name": "agent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agent_name_idx": { + "name": "agent_name_idx", + "columns": ["name"], + "isUnique": false + }, + "agent_type_idx": { + "name": "agent_type_idx", + "columns": ["type"], + "isUnique": false + }, + "agent_sort_order_idx": { + "name": "agent_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_channel": { + "name": "agent_channel", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "active_chat_ids": { + "name": "active_chat_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_channel_agent_id_idx": { + "name": "agent_channel_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agent_channel_type_idx": { + "name": "agent_channel_type_idx", + "columns": ["type"], + "isUnique": false + }, + "agent_channel_session_id_idx": { + "name": "agent_channel_session_id_idx", + "columns": ["session_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_channel_agent_id_agent_id_fk": { + "name": "agent_channel_agent_id_agent_id_fk", + "tableFrom": "agent_channel", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_channel_session_id_agent_session_id_fk": { + "name": "agent_channel_session_id_agent_session_id_fk", + "tableFrom": "agent_channel", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_channel_type_check": { + "name": "agent_channel_type_check", + "value": "\"agent_channel\".\"type\" IN ('telegram', 'feishu', 'qq', 'wechat', 'discord', 'slack')" + }, + "agent_channel_permission_mode_check": { + "name": "agent_channel_permission_mode_check", + "value": "\"agent_channel\".\"permission_mode\" IS NULL OR \"agent_channel\".\"permission_mode\" IN ('default', 'acceptEdits', 'bypassPermissions', 'plan')" + } + } + }, + "agent_channel_task": { + "name": "agent_channel_task", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_channel_task_channel_id_idx": { + "name": "agent_channel_task_channel_id_idx", + "columns": ["channel_id"], + "isUnique": false + }, + "agent_channel_task_task_id_idx": { + "name": "agent_channel_task_task_id_idx", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_channel_task_channel_id_agent_channel_id_fk": { + "name": "agent_channel_task_channel_id_agent_channel_id_fk", + "tableFrom": "agent_channel_task", + "tableTo": "agent_channel", + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_channel_task_task_id_agent_task_id_fk": { + "name": "agent_channel_task_task_id_agent_task_id_fk", + "tableFrom": "agent_channel_task", + "tableTo": "agent_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_channel_task_channel_id_task_id_pk": { + "columns": ["channel_id", "task_id"], + "name": "agent_channel_task_channel_id_task_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_global_skill": { + "name": "agent_global_skill", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_name": { + "name": "folder_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_global_skill_folder_name_unique": { + "name": "agent_global_skill_folder_name_unique", + "columns": ["folder_name"], + "isUnique": true + }, + "agent_global_skill_source_idx": { + "name": "agent_global_skill_source_idx", + "columns": ["source"], + "isUnique": false + }, + "agent_global_skill_is_enabled_idx": { + "name": "agent_global_skill_is_enabled_idx", + "columns": ["is_enabled"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_session": { + "name": "agent_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "slash_commands": { + "name": "slash_commands", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_session_agent_id_idx": { + "name": "agent_session_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agent_session_model_idx": { + "name": "agent_session_model_idx", + "columns": ["model"], + "isUnique": false + }, + "agent_session_sort_order_idx": { + "name": "agent_session_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_session_agent_id_agent_id_fk": { + "name": "agent_session_agent_id_agent_id_fk", + "tableFrom": "agent_session", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_session_message": { + "name": "agent_session_message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_session_id": { + "name": "agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_session_message_session_id_idx": { + "name": "agent_session_message_session_id_idx", + "columns": ["session_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_session_message_session_id_agent_session_id_fk": { + "name": "agent_session_message_session_id_agent_session_id_fk", + "tableFrom": "agent_session_message", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_skill": { + "name": "agent_skill", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_skill_agent_id_idx": { + "name": "agent_skill_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agent_skill_skill_id_idx": { + "name": "agent_skill_skill_id_idx", + "columns": ["skill_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_skill_agent_id_agent_id_fk": { + "name": "agent_skill_agent_id_agent_id_fk", + "tableFrom": "agent_skill", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_skill_skill_id_agent_global_skill_id_fk": { + "name": "agent_skill_skill_id_agent_global_skill_id_fk", + "tableFrom": "agent_skill", + "tableTo": "agent_global_skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_skill_agent_id_skill_id_pk": { + "columns": ["agent_id", "skill_id"], + "name": "agent_skill_agent_id_skill_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_task_run_log": { + "name": "agent_task_run_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "run_at": { + "name": "run_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_task_run_log_task_id_idx": { + "name": "agent_task_run_log_task_id_idx", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_task_run_log_task_id_agent_task_id_fk": { + "name": "agent_task_run_log_task_id_agent_task_id_fk", + "tableFrom": "agent_task_run_log", + "tableTo": "agent_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_task_run_log_status_check": { + "name": "agent_task_run_log_status_check", + "value": "\"agent_task_run_log\".\"status\" IN ('running', 'success', 'error')" + } + } + }, + "agent_task": { + "name": "agent_task", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_value": { + "name": "schedule_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeout_minutes": { + "name": "timeout_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "next_run": { + "name": "next_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run": { + "name": "last_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_task_agent_id_idx": { + "name": "agent_task_agent_id_idx", + "columns": ["agent_id"], + "isUnique": false + }, + "agent_task_next_run_idx": { + "name": "agent_task_next_run_idx", + "columns": ["next_run"], + "isUnique": false + }, + "agent_task_status_idx": { + "name": "agent_task_status_idx", + "columns": ["status"], + "isUnique": false + } + }, + "foreignKeys": { + "agent_task_agent_id_agent_id_fk": { + "name": "agent_task_agent_id_agent_id_fk", + "tableFrom": "agent_task", + "tableTo": "agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "agent_task_schedule_type_check": { + "name": "agent_task_schedule_type_check", + "value": "\"agent_task\".\"schedule_type\" IN ('cron', 'interval', 'once')" + }, + "agent_task_status_check": { + "name": "agent_task_status_check", + "value": "\"agent_task\".\"status\" IN ('active', 'paused', 'completed')" + } + } + }, + "app_state": { + "name": "app_state", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant": { + "name": "assistant", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "assistant_created_at_idx": { + "name": "assistant_created_at_idx", + "columns": ["created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "assistant_model_id_user_model_id_fk": { + "name": "assistant_model_id_user_model_id_fk", + "tableFrom": "assistant", + "tableTo": "user_model", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_knowledge_base": { + "name": "assistant_knowledge_base", + "columns": { + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "assistant_knowledge_base_assistant_id_assistant_id_fk": { + "name": "assistant_knowledge_base_assistant_id_assistant_id_fk", + "tableFrom": "assistant_knowledge_base", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_knowledge_base_knowledge_base_id_knowledge_base_id_fk": { + "name": "assistant_knowledge_base_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "assistant_knowledge_base", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assistant_knowledge_base_assistant_id_knowledge_base_id_pk": { + "columns": ["assistant_id", "knowledge_base_id"], + "name": "assistant_knowledge_base_assistant_id_knowledge_base_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assistant_mcp_server": { + "name": "assistant_mcp_server", + "columns": { + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "assistant_mcp_server_assistant_id_assistant_id_fk": { + "name": "assistant_mcp_server_assistant_id_assistant_id_fk", + "tableFrom": "assistant_mcp_server", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_mcp_server_mcp_server_id_mcp_server_id_fk": { + "name": "assistant_mcp_server_mcp_server_id_mcp_server_id_fk", + "tableFrom": "assistant_mcp_server", + "tableTo": "mcp_server", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assistant_mcp_server_assistant_id_mcp_server_id_pk": { + "columns": ["assistant_id", "mcp_server_id"], + "name": "assistant_mcp_server_assistant_id_mcp_server_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "file_entry": { + "name": "file_entry", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ext": { + "name": "ext", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_path": { + "name": "external_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "fe_deleted_at_idx": { + "name": "fe_deleted_at_idx", + "columns": ["deleted_at"], + "isUnique": false + }, + "fe_created_at_idx": { + "name": "fe_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "fe_external_path_lower_unique_idx": { + "name": "fe_external_path_lower_unique_idx", + "columns": ["lower(\"external_path\")"], + "isUnique": true + }, + "fe_external_path_idx": { + "name": "fe_external_path_idx", + "columns": ["external_path"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "fe_origin_check": { + "name": "fe_origin_check", + "value": "\"file_entry\".\"origin\" IN ('internal', 'external')" + }, + "fe_origin_consistency": { + "name": "fe_origin_consistency", + "value": "(\"file_entry\".\"origin\" = 'internal' AND \"file_entry\".\"external_path\" IS NULL) OR (\"file_entry\".\"origin\" = 'external' AND \"file_entry\".\"external_path\" IS NOT NULL)" + }, + "fe_external_no_delete": { + "name": "fe_external_no_delete", + "value": "\"file_entry\".\"origin\" != 'external' OR \"file_entry\".\"deleted_at\" IS NULL" + }, + "fe_size_internal_only": { + "name": "fe_size_internal_only", + "value": "(\"file_entry\".\"origin\" = 'internal' AND \"file_entry\".\"size\" IS NOT NULL AND \"file_entry\".\"size\" >= 0) OR (\"file_entry\".\"origin\" = 'external' AND \"file_entry\".\"size\" IS NULL)" + } + } + }, + "file_ref": { + "name": "file_ref", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "file_entry_id": { + "name": "file_entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_ref_entry_id_idx": { + "name": "file_ref_entry_id_idx", + "columns": ["file_entry_id"], + "isUnique": false + }, + "file_ref_source_idx": { + "name": "file_ref_source_idx", + "columns": ["source_type", "source_id"], + "isUnique": false + }, + "file_ref_unique_idx": { + "name": "file_ref_unique_idx", + "columns": ["file_entry_id", "source_type", "source_id", "role"], + "isUnique": true + } + }, + "foreignKeys": { + "file_ref_file_entry_id_file_entry_id_fk": { + "name": "file_ref_file_entry_id_file_entry_id_fk", + "tableFrom": "file_ref", + "tableTo": "file_entry", + "columnsFrom": ["file_entry_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "group_entity_type_order_key_idx": { + "name": "group_entity_type_order_key_idx", + "columns": ["entity_type", "order_key"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "job_schedule": { + "name": "job_schedule", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_input_template": { + "name": "job_input_template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "next_run": { + "name": "next_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run": { + "name": "last_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "job_schedule_type_name_uq": { + "name": "job_schedule_type_name_uq", + "columns": ["type", "name"], + "isUnique": true + }, + "job_schedule_enabled_next_run_idx": { + "name": "job_schedule_enabled_next_run_idx", + "columns": ["enabled", "next_run"], + "isUnique": false + }, + "job_schedule_type_idx": { + "name": "job_schedule_type_idx", + "columns": ["type"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "job": { + "name": "job", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "queue": { + "name": "queue", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_requested": { + "name": "cancel_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "timeout_ms": { + "name": "timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "job_queue_status_scheduled_at_idx": { + "name": "job_queue_status_scheduled_at_idx", + "columns": ["queue", "status", "scheduled_at"], + "isUnique": false + }, + "job_status_idx": { + "name": "job_status_idx", + "columns": ["status"], + "isUnique": false + }, + "job_schedule_id_finished_at_idx": { + "name": "job_schedule_id_finished_at_idx", + "columns": ["schedule_id", "finished_at"], + "isUnique": false + }, + "job_parent_id_idx": { + "name": "job_parent_id_idx", + "columns": ["parent_id"], + "isUnique": false + }, + "job_idempotency_key_partial_uq": { + "name": "job_idempotency_key_partial_uq", + "columns": ["idempotency_key"], + "isUnique": true, + "where": "\"job\".\"idempotency_key\" IS NOT NULL AND \"job\".\"status\" NOT IN ('completed','failed','cancelled')" + } + }, + "foreignKeys": { + "job_schedule_id_job_schedule_id_fk": { + "name": "job_schedule_id_job_schedule_id_fk", + "tableFrom": "job", + "tableTo": "job_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_parent_id_job_id_fk": { + "name": "job_parent_id_job_id_fk", + "tableFrom": "job", + "tableTo": "job", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "job_status_check": { + "name": "job_status_check", + "value": "\"job\".\"status\" IN ('pending','delayed','running','completed','failed','cancelled')" + } + } + }, + "knowledge_base": { + "name": "knowledge_base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "embedding_model_id": { + "name": "embedding_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rerank_model_id": { + "name": "rerank_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_processor_id": { + "name": "file_processor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chunk_size": { + "name": "chunk_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chunk_overlap": { + "name": "chunk_overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "document_count": { + "name": "document_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_mode": { + "name": "search_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hybrid_alpha": { + "name": "hybrid_alpha", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "knowledge_base_group_id_group_id_fk": { + "name": "knowledge_base_group_id_group_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "knowledge_base_embedding_model_id_user_model_id_fk": { + "name": "knowledge_base_embedding_model_id_user_model_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user_model", + "columnsFrom": ["embedding_model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "knowledge_base_rerank_model_id_user_model_id_fk": { + "name": "knowledge_base_rerank_model_id_user_model_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user_model", + "columnsFrom": ["rerank_model_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "knowledge_base_search_mode_check": { + "name": "knowledge_base_search_mode_check", + "value": "\"knowledge_base\".\"search_mode\" IN ('default', 'bm25', 'hybrid')" + }, + "knowledge_base_status_check": { + "name": "knowledge_base_status_check", + "value": "\"knowledge_base\".\"status\" IN ('completed', 'failed')" + }, + "knowledge_base_status_error_check": { + "name": "knowledge_base_status_error_check", + "value": "\n (\n \"knowledge_base\".\"status\" = 'completed'\n AND \"knowledge_base\".\"embedding_model_id\" IS NOT NULL\n AND \"knowledge_base\".\"dimensions\" IS NOT NULL\n AND \"knowledge_base\".\"dimensions\" > 0\n AND \"knowledge_base\".\"error\" IS NULL\n )\n OR (\n \"knowledge_base\".\"status\" = 'failed'\n AND \"knowledge_base\".\"error\" IS NOT NULL\n AND length(trim(\"knowledge_base\".\"error\")) > 0\n )\n " + } + } + }, + "knowledge_item": { + "name": "knowledge_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "knowledge_item_base_type_created_idx": { + "name": "knowledge_item_base_type_created_idx", + "columns": ["base_id", "type", "created_at"], + "isUnique": false + }, + "knowledge_item_base_group_created_idx": { + "name": "knowledge_item_base_group_created_idx", + "columns": ["base_id", "group_id", "created_at"], + "isUnique": false + }, + "knowledge_item_baseId_id_unique": { + "name": "knowledge_item_baseId_id_unique", + "columns": ["base_id", "id"], + "isUnique": true + } + }, + "foreignKeys": { + "knowledge_item_base_id_knowledge_base_id_fk": { + "name": "knowledge_item_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_item", + "tableTo": "knowledge_base", + "columnsFrom": ["base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_item_base_id_group_id_knowledge_item_base_id_id_fk": { + "name": "knowledge_item_base_id_group_id_knowledge_item_base_id_id_fk", + "tableFrom": "knowledge_item", + "tableTo": "knowledge_item", + "columnsFrom": ["base_id", "group_id"], + "columnsTo": ["base_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "knowledge_item_type_check": { + "name": "knowledge_item_type_check", + "value": "\"knowledge_item\".\"type\" IN ('file', 'url', 'note', 'sitemap', 'directory')" + }, + "knowledge_item_status_check": { + "name": "knowledge_item_status_check", + "value": "\"knowledge_item\".\"status\" IN ('idle', 'processing', 'completed', 'failed')" + }, + "knowledge_item_phase_check": { + "name": "knowledge_item_phase_check", + "value": "\n \"knowledge_item\".\"phase\" IS NULL\n OR (\"knowledge_item\".\"type\" IN ('file', 'url', 'note') AND \"knowledge_item\".\"phase\" IN ('reading', 'embedding'))\n OR (\"knowledge_item\".\"type\" IN ('directory', 'sitemap') AND \"knowledge_item\".\"phase\" = 'preparing')\n " + }, + "knowledge_item_status_phase_error_check": { + "name": "knowledge_item_status_phase_error_check", + "value": "\n (\n \"knowledge_item\".\"status\" IN ('idle', 'completed')\n AND \"knowledge_item\".\"phase\" IS NULL\n AND \"knowledge_item\".\"error\" IS NULL\n )\n OR (\n -- Containers may stay processing after their own prepare phase ends\n -- while descendant leaf items continue reading/embedding.\n \"knowledge_item\".\"status\" = 'processing'\n AND \"knowledge_item\".\"error\" IS NULL\n )\n OR (\n \"knowledge_item\".\"status\" = 'failed'\n AND \"knowledge_item\".\"phase\" IS NULL\n AND \"knowledge_item\".\"error\" IS NOT NULL\n AND length(trim(\"knowledge_item\".\"error\")) > 0\n )\n " + } + } + }, + "mcp_server": { + "name": "mcp_server", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registry_url": { + "name": "registry_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_url": { + "name": "provider_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "long_running": { + "name": "long_running", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dxt_version": { + "name": "dxt_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dxt_path": { + "name": "dxt_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_key": { + "name": "search_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_sample": { + "name": "config_sample", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled_tools": { + "name": "disabled_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled_auto_approve_tools": { + "name": "disabled_auto_approve_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "should_config": { + "name": "should_config", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "install_source": { + "name": "install_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_trusted": { + "name": "is_trusted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trusted_at": { + "name": "trusted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_server_name_idx": { + "name": "mcp_server_name_idx", + "columns": ["name"], + "isUnique": false + }, + "mcp_server_is_active_idx": { + "name": "mcp_server_is_active_idx", + "columns": ["is_active"], + "isUnique": false + }, + "mcp_server_sort_order_idx": { + "name": "mcp_server_sort_order_idx", + "columns": ["sort_order"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mcp_server_type_check": { + "name": "mcp_server_type_check", + "value": "\"mcp_server\".\"type\" IS NULL OR \"mcp_server\".\"type\" IN ('stdio', 'sse', 'streamableHttp', 'inMemory')" + }, + "mcp_server_install_source_check": { + "name": "mcp_server_install_source_check", + "value": "\"mcp_server\".\"install_source\" IS NULL OR \"mcp_server\".\"install_source\" IN ('builtin', 'manual', 'protocol', 'unknown')" + } + } + }, + "message": { + "name": "message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "searchable_text": { + "name": "searchable_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "siblings_group_id": { + "name": "siblings_group_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_snapshot": { + "name": "model_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats": { + "name": "stats", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "message_parent_id_idx": { + "name": "message_parent_id_idx", + "columns": ["parent_id"], + "isUnique": false + }, + "message_topic_created_idx": { + "name": "message_topic_created_idx", + "columns": ["topic_id", "created_at"], + "isUnique": false + }, + "message_trace_id_idx": { + "name": "message_trace_id_idx", + "columns": ["trace_id"], + "isUnique": false + } + }, + "foreignKeys": { + "message_topic_id_topic_id_fk": { + "name": "message_topic_id_topic_id_fk", + "tableFrom": "message", + "tableTo": "topic", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_model_id_user_model_id_fk": { + "name": "message_model_id_user_model_id_fk", + "tableFrom": "message", + "tableTo": "user_model", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "message_parent_id_message_id_fk": { + "name": "message_parent_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "message_role_check": { + "name": "message_role_check", + "value": "\"message\".\"role\" IN ('user', 'assistant', 'system')" + }, + "message_status_check": { + "name": "message_status_check", + "value": "\"message\".\"status\" IN ('pending', 'success', 'error', 'paused')" + } + } + }, + "mini_app": { + "name": "mini_app", + "columns": { + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "preset_mini_app_id": { + "name": "preset_mini_app_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'enabled'" + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bordered": { + "name": "bordered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "background": { + "name": "background", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supported_regions": { + "name": "supported_regions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_key": { + "name": "name_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mini_app_status_order_key_idx": { + "name": "mini_app_status_order_key_idx", + "columns": ["status", "order_key"], + "isUnique": false + }, + "mini_app_preset_mini_app_id_idx": { + "name": "mini_app_preset_mini_app_id_idx", + "columns": ["preset_mini_app_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "mini_app_status_check": { + "name": "mini_app_status_check", + "value": "\"mini_app\".\"status\" IN ('enabled', 'disabled', 'pinned')" + } + } + }, + "pin": { + "name": "pin", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "pin_entity_type_entity_id_unique_idx": { + "name": "pin_entity_type_entity_id_unique_idx", + "columns": ["entity_type", "entity_id"], + "isUnique": true + }, + "pin_entity_type_order_key_idx": { + "name": "pin_entity_type_order_key_idx", + "columns": ["entity_type", "order_key"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "preference": { + "name": "preference", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "preference_scope_key_pk": { + "columns": ["scope", "key"], + "name": "preference_scope_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompt": { + "name": "prompt", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "prompt_order_key_idx": { + "name": "prompt_order_key_idx", + "columns": ["order_key"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entity_tag": { + "name": "entity_tag", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "entity_tag_tag_id_idx": { + "name": "entity_tag_tag_id_idx", + "columns": ["tag_id"], + "isUnique": false + } + }, + "foreignKeys": { + "entity_tag_tag_id_tag_id_fk": { + "name": "entity_tag_tag_id_tag_id_fk", + "tableFrom": "entity_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entity_tag_entity_type_entity_id_tag_id_pk": { + "columns": ["entity_type", "entity_id", "tag_id"], + "name": "entity_tag_entity_type_entity_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tag": { + "name": "tag", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tag_name_unique": { + "name": "tag_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "topic": { + "name": "topic", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_name_manually_edited": { + "name": "is_name_manually_edited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_node_id": { + "name": "active_node_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "topic_group_updated_idx": { + "name": "topic_group_updated_idx", + "columns": ["group_id", "updated_at"], + "isUnique": false + }, + "topic_updated_at_idx": { + "name": "topic_updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + }, + "topic_group_id_order_key_idx": { + "name": "topic_group_id_order_key_idx", + "columns": ["group_id", "order_key"], + "isUnique": false + }, + "topic_assistant_id_idx": { + "name": "topic_assistant_id_idx", + "columns": ["assistant_id"], + "isUnique": false + } + }, + "foreignKeys": { + "topic_assistant_id_assistant_id_fk": { + "name": "topic_assistant_id_assistant_id_fk", + "tableFrom": "topic", + "tableTo": "assistant", + "columnsFrom": ["assistant_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "topic_group_id_group_id_fk": { + "name": "topic_group_id_group_id_fk", + "tableFrom": "topic", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translate_history": { + "name": "translate_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_text": { + "name": "source_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_text": { + "name": "target_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_language": { + "name": "source_language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_language": { + "name": "target_language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "star": { + "name": "star", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "translate_history_created_at_idx": { + "name": "translate_history_created_at_idx", + "columns": ["created_at"], + "isUnique": false + }, + "translate_history_star_created_at_idx": { + "name": "translate_history_star_created_at_idx", + "columns": ["star", "created_at"], + "isUnique": false + } + }, + "foreignKeys": { + "translate_history_source_language_translate_language_lang_code_fk": { + "name": "translate_history_source_language_translate_language_lang_code_fk", + "tableFrom": "translate_history", + "tableTo": "translate_language", + "columnsFrom": ["source_language"], + "columnsTo": ["lang_code"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "translate_history_target_language_translate_language_lang_code_fk": { + "name": "translate_history_target_language_translate_language_lang_code_fk", + "tableFrom": "translate_history", + "tableTo": "translate_language", + "columnsFrom": ["target_language"], + "columnsTo": ["lang_code"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translate_language": { + "name": "translate_language", + "columns": { + "lang_code": { + "name": "lang_code", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_model": { + "name": "user_model", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "preset_model_id": { + "name": "preset_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_modalities": { + "name": "output_modalities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "endpoint_types": { + "name": "endpoint_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_endpoint_url": { + "name": "custom_endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_input_tokens": { + "name": "max_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supports_streaming": { + "name": "supports_streaming", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parameters": { + "name": "parameters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pricing": { + "name": "pricing", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_hidden": { + "name": "is_hidden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_deprecated": { + "name": "is_deprecated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_overrides": { + "name": "user_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_model_preset_idx": { + "name": "user_model_preset_idx", + "columns": ["preset_model_id"], + "isUnique": false + }, + "user_model_provider_enabled_idx": { + "name": "user_model_provider_enabled_idx", + "columns": ["provider_id", "is_enabled"], + "isUnique": false + }, + "user_model_provider_id_order_key_idx": { + "name": "user_model_provider_id_order_key_idx", + "columns": ["provider_id", "order_key"], + "isUnique": false + }, + "user_model_provider_model_unique": { + "name": "user_model_provider_model_unique", + "columns": ["provider_id", "model_id"], + "isUnique": true + } + }, + "foreignKeys": { + "user_model_provider_id_user_provider_provider_id_fk": { + "name": "user_model_provider_id_user_provider_provider_id_fk", + "tableFrom": "user_model", + "tableTo": "user_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["provider_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_provider": { + "name": "user_provider", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "preset_provider_id": { + "name": "preset_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint_configs": { + "name": "endpoint_configs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_chat_endpoint": { + "name": "default_chat_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_keys": { + "name": "api_keys", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "auth_config": { + "name": "auth_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_features": { + "name": "api_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_settings": { + "name": "provider_settings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_provider_preset_idx": { + "name": "user_provider_preset_idx", + "columns": ["preset_provider_id"], + "isUnique": false + }, + "user_provider_enabled_idx": { + "name": "user_provider_enabled_idx", + "columns": ["is_enabled"], + "isUnique": false + }, + "user_provider_order_key_idx": { + "name": "user_provider_order_key_idx", + "columns": ["order_key"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"file_entry\".\"trashed_at\"": "\"file_entry\".\"deleted_at\"" + } + }, + "internal": { + "indexes": { + "fe_external_path_lower_unique_idx": { + "columns": { + "lower(\"external_path\")": { + "isExpression": true + } + } + } + } + } +} diff --git a/migrations/sqlite-drizzle/meta/_journal.json b/migrations/sqlite-drizzle/meta/_journal.json index 1e829fef76..b4dc2e2d63 100644 --- a/migrations/sqlite-drizzle/meta/_journal.json +++ b/migrations/sqlite-drizzle/meta/_journal.json @@ -182,6 +182,13 @@ "when": 1779183817228, "tag": "0025_left_black_tarantula", "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1779373185863, + "tag": "0026_sturdy_aqueduct", + "breakpoints": true } ], "version": "7" diff --git a/packages/shared/data/api/schemas/__tests__/files.test.ts b/packages/shared/data/api/schemas/__tests__/files.test.ts index c76da9e287..19d1e1f498 100644 --- a/packages/shared/data/api/schemas/__tests__/files.test.ts +++ b/packages/shared/data/api/schemas/__tests__/files.test.ts @@ -21,7 +21,7 @@ describe('ListFilesQuerySchema', () => { }) it('rejects the impossible combo inTrash=true && origin=external (S6 refine)', () => { - // DB CHECK fe_external_no_trash makes this combo always return zero + // DB CHECK fe_external_no_delete makes this combo always return zero // rows; the refine surfaces the contradiction at the parse boundary // instead of silently returning empty results to callers. const result = ListFilesQuerySchema.safeParse({ inTrash: true, origin: 'external' }) diff --git a/packages/shared/data/api/schemas/files.ts b/packages/shared/data/api/schemas/files.ts index e9dc2317cc..9fb511992a 100644 --- a/packages/shared/data/api/schemas/files.ts +++ b/packages/shared/data/api/schemas/files.ts @@ -89,7 +89,7 @@ export const ListFilesQuerySchema = z }) .refine( (q) => !(q.inTrash === true && q.origin === 'external'), - 'inTrash=true is incompatible with origin=external — external entries cannot be trashed (DB CHECK fe_external_no_trash)' + 'inTrash=true is incompatible with origin=external — external entries cannot be trashed (DB CHECK fe_external_no_delete)' ) export type ListFilesQueryParams = z.input export type ListFilesQuery = z.output @@ -127,7 +127,7 @@ export type FileSchemas = { * Trash + origin caveat: the combination `inTrash=true & origin='external'` * is rejected by the schema (`ListFilesQuerySchema` `.refine` rule), * because external rows are constrained by the DB CHECK - * `fe_external_no_trash` to always have `trashedAt = NULL` and would + * `fe_external_no_delete` to always have `deletedAt = NULL` and would * otherwise return an empty result with no error signal. Modelling the * query as a discriminated union (`{ origin: 'external'; inTrash?: false } | * { origin?: 'internal'; inTrash?: boolean }`) is a follow-up worth doing diff --git a/packages/shared/data/types/__tests__/fileEntry.test.ts b/packages/shared/data/types/__tests__/fileEntry.test.ts index 8ed8626a6f..3dd0398918 100644 --- a/packages/shared/data/types/__tests__/fileEntry.test.ts +++ b/packages/shared/data/types/__tests__/fileEntry.test.ts @@ -9,7 +9,7 @@ const TS = 1700000000000 // After the BO/DB split, each variant's schema declares only its own fields // (strictObject — extra keys are rejected). Internal has no `externalPath` and -// `trashedAt` is optional; external has no `size` and no `trashedAt`. Factories +// `deletedAt` is optional; external has no `size` and no `deletedAt`. Factories // match that shape so callers only set fields that belong on the arm. function makeInternal(overrides: Record = {}) { return { @@ -179,24 +179,24 @@ describe('FileEntrySchema origin invariants', () => { // ─── Trash ─── -describe('FileEntrySchema trash (trashedAt)', () => { - // After I19 the internal arm types `trashedAt` as `optional number` — present +describe('FileEntrySchema trash (deletedAt)', () => { + // After I19 the internal arm types `deletedAt` as `optional number` — present // when trashed, absent (undefined) when live. The external arm drops the - // field entirely (the DB CHECK fe_external_no_trash already forbids it). + // field entirely (the DB CHECK fe_external_no_delete already forbids it). - it('accepts active internal entry (trashedAt absent)', () => { + it('accepts active internal entry (deletedAt absent)', () => { expect(FileEntrySchema.safeParse(makeInternal()).success).toBe(true) }) it('accepts trashed internal entry', () => { - expect(FileEntrySchema.safeParse(makeInternal({ trashedAt: TS })).success).toBe(true) + expect(FileEntrySchema.safeParse(makeInternal({ deletedAt: TS })).success).toBe(true) }) - it('rejects trashed external entry (strictObject — external has no trashedAt field)', () => { - expect(FileEntrySchema.safeParse(makeExternal({ trashedAt: TS })).success).toBe(false) + it('rejects trashed external entry (strictObject — external has no deletedAt field)', () => { + expect(FileEntrySchema.safeParse(makeExternal({ deletedAt: TS })).success).toBe(false) }) - it('accepts external entry (no trashedAt field by construction)', () => { + it('accepts external entry (no deletedAt field by construction)', () => { expect(FileEntrySchema.safeParse(makeExternal()).success).toBe(true) }) }) diff --git a/packages/shared/data/types/file/fileEntry.ts b/packages/shared/data/types/file/fileEntry.ts index a2b4425fdf..1f43c7ef6c 100644 --- a/packages/shared/data/types/file/fileEntry.ts +++ b/packages/shared/data/types/file/fileEntry.ts @@ -28,7 +28,7 @@ * | `ext` | SoT | derived from `externalPath` extname (stable) | * | `size` | SoT (bytes, ≥ 0) | **absent** — live value via `getMetadata` | * | `externalPath`| **absent** | non-null absolute path (canonical) | - * | `trashedAt` | optional (present iff trashed) | **absent** (external cannot be trashed) | + * | `deletedAt` | optional (present iff trashed) | **absent** (external cannot be trashed) | * * "Absent" means the field is not declared on that variant's schema at all — * `entry.size` is a type error on the external arm, not `null` you have to @@ -86,11 +86,11 @@ * (update in place via rename / write) * ``` * - * - Active: `trashedAt` is absent — on `InternalEntrySchema` it's `optional` + * - Active: `deletedAt` is absent — on `InternalEntrySchema` it's `optional` * so omitted means live; `ExternalEntrySchema` doesn't declare the - * field at all and the DB `fe_external_no_trash` CHECK enforces it + * field at all and the DB `fe_external_no_delete` CHECK enforces it * at the row layer - * - Trashed: `trashedAt = ` (internal-only) + * - Trashed: `deletedAt = ` (internal-only) * - permanentDelete on internal: unlink FS file + delete DB row * - permanentDelete on external: **DB row only** — the physical file is left * untouched. Entry-level deletion is decoupled from physical deletion; @@ -203,15 +203,15 @@ export type CanonicalFilePath = FilePath & CanonicalExternalPath // ## DB row vs Business Object // // The `file_entry` SQLite table is a flat row with all columns physically -// present (size / externalPath / trashedAt are all nullable on the column +// present (size / externalPath / deletedAt are all nullable on the column // level), guarded by three CHECK constraints (`fe_origin_consistency`, -// `fe_size_internal_only`, `fe_external_no_trash`) so a row can never +// `fe_size_internal_only`, `fe_external_no_delete`) so a row can never // represent an impossible combination. That is the **DB-row** layer. // // `FileEntry` is the **business object** consumers actually work with. // Discrimination on `origin` means an internal entry doesn't *have* an // `externalPath`, and an external entry doesn't *have* a `size` / -// `trashedAt` — these fields are simply absent on the BO shape, not `null`. +// `deletedAt` — these fields are simply absent on the BO shape, not `null`. // Narrowing on `origin` gives TS the right keys at the right callsite, // so renderer code never has to `if (entry.origin === 'internal') ...` // just to access `entry.size`, and never has to `as` a `null` check away. @@ -247,7 +247,7 @@ const CommonEntryFields = { /** * Internal entry — Cherry owns the content at `{userData}/Data/Files/{id}.{ext}`. * - * Variant-only fields: `size` (authoritative byte count), `trashedAt` + * Variant-only fields: `size` (authoritative byte count), `deletedAt` * (optional, present and non-null when entry is trashed). `externalPath` * is absent on this variant — there is no user-provided path. The DB row * carries `externalPath: null` to satisfy the table schema; the BO @@ -264,20 +264,20 @@ export const InternalEntrySchema = z.strictObject({ /** * Trash timestamp (ms epoch). Optional — present and non-null when the * entry is in the trash, absent when it is live. Internal entries are the - * only ones that can be trashed (`fe_external_no_trash` CHECK). + * only ones that can be trashed (`fe_external_no_delete` CHECK). */ - trashedAt: TimestampSchema.optional() + deletedAt: TimestampSchema.optional() }) /** * External entry — Cherry references a user-provided path. * * Variant-only field: `externalPath` (absolute, canonical). `size` and - * `trashedAt` are absent on this variant — external files may change + * `deletedAt` are absent on this variant — external files may change * outside Cherry at any time so no DB size snapshot is kept (live values * come from File IPC `getMetadata`), and external entries cannot be - * trashed (`fe_external_no_trash` CHECK). The DB row carries `size: null` - * and `trashedAt: null` to satisfy the table schema; the BO dispatcher + * trashed (`fe_external_no_delete` CHECK). The DB row carries `size: null` + * and `deletedAt: null` to satisfy the table schema; the BO dispatcher * drops them. */ export const ExternalEntrySchema = z.strictObject({ diff --git a/packages/shared/file/types/ipc.ts b/packages/shared/file/types/ipc.ts index 29991f1e88..2c4de6b030 100644 --- a/packages/shared/file/types/ipc.ts +++ b/packages/shared/file/types/ipc.ts @@ -274,7 +274,7 @@ export interface FileIpcApi { * Idempotent by design — callers holding an `externalPath` can invoke this * freely without pre-checking. The global unique index * `UNIQUE(externalPath)` (internal rows are `null` and exempt) enforces the - * invariant; `fe_external_no_trash` forbids trashed external rows so no + * invariant; `fe_external_no_delete` forbids trashed external rows so no * "restore" branch exists. * * @phase 2 — not yet wired @@ -398,9 +398,9 @@ export interface FileIpcApi { // Section status: all `@phase 2`. /** - * Move entry to Trash (soft delete via trashedAt). Internal-origin entries only. + * Move entry to Trash (soft delete via deletedAt). Internal-origin entries only. * Passing an external-origin entry id throws: external entries cannot be trashed - * (`fe_external_no_trash` CHECK). + * (`fe_external_no_delete` CHECK). * * @phase 2 — not yet wired */ diff --git a/src/main/data/api/handlers/__tests__/files.test.ts b/src/main/data/api/handlers/__tests__/files.test.ts index 8907683d44..721b0c098b 100644 --- a/src/main/data/api/handlers/__tests__/files.test.ts +++ b/src/main/data/api/handlers/__tests__/files.test.ts @@ -29,7 +29,7 @@ describe('fileHandlers (DataApi)', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now, ...overrides @@ -41,7 +41,7 @@ describe('fileHandlers (DataApi)', () => { await Promise.all([ seedEntry('019606a0-0000-7000-8000-000000000a01'), seedEntry('019606a0-0000-7000-8000-000000000a02'), - seedEntry('019606a0-0000-7000-8000-000000000a03', { trashedAt: Date.now() }) + seedEntry('019606a0-0000-7000-8000-000000000a03', { deletedAt: Date.now() }) ]) const result = (await fileHandlers['/files/entries'].GET({ query: {} } as never)) as { diff --git a/src/main/data/db/schemas/__tests__/file.test.ts b/src/main/data/db/schemas/__tests__/file.test.ts index 060074142b..9cd708589b 100644 --- a/src/main/data/db/schemas/__tests__/file.test.ts +++ b/src/main/data/db/schemas/__tests__/file.test.ts @@ -30,7 +30,7 @@ function baseInternal(overrides: Record = {}) { ext: 'md', size: 100, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: TS, updatedAt: TS, ...overrides @@ -45,7 +45,7 @@ function baseExternal(path: string, overrides: Record = {}) { ext: 'pdf', size: null, externalPath: path, - trashedAt: null, + deletedAt: null, createdAt: TS, updatedAt: TS, ...overrides @@ -104,17 +104,17 @@ describe('fileEntryTable — functional unique index on lower(externalPath)', () }) }) -describe('fileEntryTable — fe_external_no_trash check', () => { +describe('fileEntryTable — fe_external_no_delete check', () => { const dbh = setupTestDatabase() - it('rejects an external entry with non-null trashedAt', async () => { + it('rejects an external entry with non-null deletedAt', async () => { await expect( - dbh.db.insert(fileEntryTable).values(baseExternal('/Users/me/will-not-trash.pdf', { trashedAt: TS })) + dbh.db.insert(fileEntryTable).values(baseExternal('/Users/me/will-not-trash.pdf', { deletedAt: TS })) ).rejects.toThrow() }) it('allows internal entries to be trashed', async () => { - await expect(dbh.db.insert(fileEntryTable).values(baseInternal({ trashedAt: TS }))).resolves.not.toThrow() + await expect(dbh.db.insert(fileEntryTable).values(baseInternal({ deletedAt: TS }))).resolves.not.toThrow() }) }) diff --git a/src/main/data/db/schemas/file.ts b/src/main/data/db/schemas/file.ts index 576e620987..148181e66c 100644 --- a/src/main/data/db/schemas/file.ts +++ b/src/main/data/db/schemas/file.ts @@ -1,7 +1,12 @@ import { sql } from 'drizzle-orm' import { check, index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' -import { createUpdateTimestamps, uuidPrimaryKey, uuidPrimaryKeyOrdered } from './_columnHelpers' +import { + createUpdateDeleteTimestamps, + createUpdateTimestamps, + uuidPrimaryKey, + uuidPrimaryKeyOrdered +} from './_columnHelpers' /** * NOTE: `file_upload` (AI provider upload cache) is intentionally NOT included @@ -46,23 +51,18 @@ export const fileEntryTable = sqliteTable( /** Absolute path to the user-provided file. Non-null iff origin='external' */ externalPath: text(), - // ─── Trash ─── - /** - * Non-null = trashed (ms epoch). Internal-only. - * - * External entries cannot be trashed (enforced by `fe_external_no_trash` - * check constraint). Their lifecycle is monotonic: create via - * `ensureExternalEntry`, update in place, or remove immediately via - * `permanentDelete` (DB-only — the physical file is left untouched; - * path-level deletion is a separate, explicit unmanaged `@main/utils/file/fs.remove(path)`). - */ - trashedAt: integer(), - // ─── Timestamps ─── - ...createUpdateTimestamps + // `deletedAt` is soft-delete (NULL = not deleted). Internal-only — + // external entries cannot be soft-deleted (enforced by + // `fe_external_no_delete`); their lifecycle is monotonic: create via + // `ensureExternalEntry`, update in place, or remove immediately via + // `permanentDelete` (DB-only — the physical file is left untouched; + // path-level deletion is a separate, explicit unmanaged + // `@main/utils/file/fs.remove(path)`). + ...createUpdateDeleteTimestamps }, (t) => [ - index('fe_trashed_at_idx').on(t.trashedAt), + index('fe_deleted_at_idx').on(t.deletedAt), index('fe_created_at_idx').on(t.createdAt), // Case-insensitive uniqueness for `externalPath`. SQLite indexes // expressions verbatim, so this index covers both the uniqueness @@ -99,7 +99,7 @@ export const fileEntryTable = sqliteTable( // External entries cannot be trashed — trash/restore is internal-only. // External removal is always immediate via permanentDelete (DB-only; the // physical file is left untouched, path-level @main/utils/file/fs.remove is a separate call). - check('fe_external_no_trash', sql`${t.origin} != 'external' OR ${t.trashedAt} IS NULL`), + check('fe_external_no_delete', sql`${t.origin} != 'external' OR ${t.deletedAt} IS NULL`), // Size semantics are origin-dependent: internal rows carry an authoritative // byte count (non-null, ≥ 0); external rows must leave size NULL and read // live values from File IPC `getMetadata`. The Zod layer rejects the same diff --git a/src/main/data/services/FileEntryService.ts b/src/main/data/services/FileEntryService.ts index 6ae91c1772..81638b3f8c 100644 --- a/src/main/data/services/FileEntryService.ts +++ b/src/main/data/services/FileEntryService.ts @@ -41,7 +41,7 @@ export interface CreateFileEntryRow { readonly size: number | null /** Non-null iff `origin === 'external'`; must be pre-canonicalized. */ readonly externalPath: string | null - readonly trashedAt?: number | null + readonly deletedAt?: number | null } /** @@ -50,7 +50,7 @@ export interface CreateFileEntryRow { * byte count atomically; on external rows it must remain `null`. */ export type UpdateFileEntryRow = Partial> & { - readonly trashedAt?: number | null + readonly deletedAt?: number | null } export interface FindEntriesQuery { @@ -200,11 +200,11 @@ function rowToFileEntry(row: FileEntryRow): FileEntry { name: row.name, ext: row.ext, size: row.size, - // trashedAt is `optional` on the BO — present iff the DB column is + // deletedAt is `optional` on the BO — present iff the DB column is // non-null. Bypass `nullsToUndefined` so we don't pull in a helper // whose project-wide meaning is "every null becomes undefined"; // here only this specific column flips. - ...(row.trashedAt !== null ? { trashedAt: row.trashedAt } : {}), + ...(row.deletedAt !== null ? { deletedAt: row.deletedAt } : {}), createdAt: row.createdAt, updatedAt: row.updatedAt }) @@ -271,9 +271,9 @@ class FileEntryServiceImpl implements FileEntryService { conditions.push(eq(fileEntryTable.origin, query.origin)) } if (query.inTrash === true) { - conditions.push(isNotNull(fileEntryTable.trashedAt)) + conditions.push(isNotNull(fileEntryTable.deletedAt)) } else { - conditions.push(isNull(fileEntryTable.trashedAt)) + conditions.push(isNull(fileEntryTable.deletedAt)) } let queryBuilder = this.getDb() @@ -300,9 +300,9 @@ class FileEntryServiceImpl implements FileEntryService { conditions.push(eq(fileEntryTable.origin, query.origin)) } if (query.inTrash === true) { - conditions.push(isNotNull(fileEntryTable.trashedAt)) + conditions.push(isNotNull(fileEntryTable.deletedAt)) } else { - conditions.push(isNull(fileEntryTable.trashedAt)) + conditions.push(isNull(fileEntryTable.deletedAt)) } const where = and(...conditions) @@ -343,7 +343,7 @@ class FileEntryServiceImpl implements FileEntryService { } async findUnreferenced(query: { origin?: FileEntryOrigin } = {}): Promise { - const conditions: SQL[] = [isNull(fileEntryTable.trashedAt), isNull(fileRefTable.id)] + const conditions: SQL[] = [isNull(fileEntryTable.deletedAt), isNull(fileRefTable.id)] if (query.origin) conditions.push(eq(fileEntryTable.origin, query.origin)) const rows = await this.getDb() .select({ entry: fileEntryTable }) @@ -371,7 +371,7 @@ class FileEntryServiceImpl implements FileEntryService { ext: values.ext, size: values.size, externalPath: values.externalPath, - trashedAt: values.trashedAt ?? null, + deletedAt: values.deletedAt ?? null, createdAt: now, updatedAt: now }) @@ -392,7 +392,7 @@ class FileEntryServiceImpl implements FileEntryService { if (values.name !== undefined) updates.name = values.name if (values.ext !== undefined) updates.ext = values.ext if (values.size !== undefined) updates.size = values.size - if (values.trashedAt !== undefined) updates.trashedAt = values.trashedAt + if (values.deletedAt !== undefined) updates.deletedAt = values.deletedAt const rows = await this.getDb().update(fileEntryTable).set(updates).where(eq(fileEntryTable.id, id)).returning() if (rows.length === 0) { throw DataApiErrorFactory.notFound('FileEntry', id) diff --git a/src/main/data/services/__tests__/FileEntryService.test.ts b/src/main/data/services/__tests__/FileEntryService.test.ts index 80fc3b85f4..7ecdb91309 100644 --- a/src/main/data/services/__tests__/FileEntryService.test.ts +++ b/src/main/data/services/__tests__/FileEntryService.test.ts @@ -31,7 +31,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 11, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -74,14 +74,14 @@ describe('FileEntryService', () => { ext: 'md', size: 0, externalPath: null, - trashedAt: now, + deletedAt: now, createdAt: now, updatedAt: now }) const entry = await fileEntryService.findById(id) if (entry?.origin === 'internal') { - expect(entry.trashedAt).toBe(now) + expect(entry.deletedAt).toBe(now) } else { throw new Error('expected internal entry') } @@ -99,7 +99,7 @@ describe('FileEntryService', () => { ext: 'pdf', size: null, externalPath: '/Users/me/doc.pdf', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -124,7 +124,7 @@ describe('FileEntryService', () => { ext: 'txt', size: null, externalPath: '/Users/me/a.txt', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -149,7 +149,7 @@ describe('FileEntryService', () => { ext: 'txt', size: null, externalPath: '/Users/me/A.TXT', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -173,7 +173,7 @@ describe('FileEntryService', () => { ext: 'txt', size: null, externalPath: '/Users/me/A.TXT', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -190,7 +190,7 @@ describe('FileEntryService', () => { ext: 'txt', size: null, externalPath: '/Users/me/a.txt', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -215,7 +215,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -226,7 +226,7 @@ describe('FileEntryService', () => { ext: 'md', size: 2, externalPath: null, - trashedAt: now, + deletedAt: now, createdAt: now, updatedAt: now } @@ -247,7 +247,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -258,7 +258,7 @@ describe('FileEntryService', () => { ext: 'pdf', size: null, externalPath: '/foo/e.pdf', - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now } @@ -279,7 +279,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -290,7 +290,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: now, + deletedAt: now, createdAt: now, updatedAt: now } @@ -310,7 +310,7 @@ describe('FileEntryService', () => { ext: 'txt', size: i, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now + i, updatedAt: now + i })) @@ -331,7 +331,7 @@ describe('FileEntryService', () => { ext: 'txt', size: i + 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now + i, updatedAt: now + i })) @@ -348,7 +348,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -359,7 +359,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 2, externalPath: null, - trashedAt: now, + deletedAt: now, createdAt: now, updatedAt: now } @@ -409,7 +409,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now + 2, updatedAt: now + 2 }, @@ -420,7 +420,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -431,7 +431,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now + 1, updatedAt: now + 1 } @@ -473,7 +473,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: sharedTs, updatedAt: sharedTs })) @@ -513,7 +513,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: sharedTs, updatedAt: sharedTs })) @@ -628,16 +628,16 @@ describe('FileEntryService', () => { }) }) - it('updates trashedAt for soft delete', async () => { + it('updates deletedAt for soft delete', async () => { const id = '019606a0-0000-7000-8000-000000000b02' as FileEntryId await fileEntryService.create({ id, origin: 'internal', name: 'tmp', ext: 'txt', size: 1, externalPath: null }) - const trashedAt = Date.now() - const updated = await fileEntryService.update(id, { trashedAt }) + const deletedAt = Date.now() + const updated = await fileEntryService.update(id, { deletedAt }) if (updated.origin !== 'internal') throw new Error('expected internal entry') - expect(updated.trashedAt).toBe(trashedAt) + expect(updated.deletedAt).toBe(deletedAt) }) - it('throws when setting trashedAt on an external row (CHECK fe_external_no_trash)', async () => { + it('throws when setting deletedAt on an external row (CHECK fe_external_no_delete)', async () => { const id = '019606a0-0000-7000-8000-000000000b03' as FileEntryId await fileEntryService.create({ id, @@ -647,7 +647,7 @@ describe('FileEntryService', () => { size: null, externalPath: '/x/y.txt' }) - await expect(fileEntryService.update(id, { trashedAt: Date.now() })).rejects.toThrow() + await expect(fileEntryService.update(id, { deletedAt: Date.now() })).rejects.toThrow() }) it('rejects unsafe name BEFORE the SQL UPDATE commits', async () => { @@ -671,7 +671,7 @@ describe('FileEntryService', () => { // listAllIds backs the Phase 1b.4 startup disk scan, which decides which // on-disk UUID files are orphaned (no DB row, regardless of trashed // state). The implementation is one query — the regressions worth - // catching are misclassifying trashed rows as deleted (trashedAt filter + // catching are misclassifying trashed rows as deleted (deletedAt filter // creeping in) or returning an array shape. it('returns an empty Set on an empty table', async () => { @@ -698,7 +698,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: Date.now() + deletedAt: Date.now() }) const ids = await fileEntryService.listAllIds() @@ -944,7 +944,7 @@ describe('FileEntryService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: Date.now() + deletedAt: Date.now() }) const result = await fileEntryService.findUnreferenced() diff --git a/src/main/data/services/__tests__/FileRefService.test.ts b/src/main/data/services/__tests__/FileRefService.test.ts index d763e99d48..9136ce68ea 100644 --- a/src/main/data/services/__tests__/FileRefService.test.ts +++ b/src/main/data/services/__tests__/FileRefService.test.ts @@ -28,7 +28,7 @@ describe('FileRefService', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) diff --git a/src/main/services/file/FileManager.ts b/src/main/services/file/FileManager.ts index cc0db680e0..c7b0e02cc2 100644 --- a/src/main/services/file/FileManager.ts +++ b/src/main/services/file/FileManager.ts @@ -91,7 +91,7 @@ * **External entries cannot be trashed.** Their lifecycle is monotonic: * created by `ensureExternalEntry` (pure upsert keyed by path — see below), * updated in place via `write` / `rename`, and removed only by an explicit - * (non-UI) `permanentDelete`. The `fe_external_no_trash` CHECK constraint + * (non-UI) `permanentDelete`. The `fe_external_no_delete` CHECK constraint * enforces this at the DB level; `trash` / `restore` on an external entry id * will throw. * @@ -375,7 +375,7 @@ export interface IFileManager { * The global unique index `UNIQUE(externalPath)` (internal rows have * `externalPath = null` and are exempt — SQLite treats NULLs as distinct) * guarantees at most one row per path. External entries cannot be trashed - * (`fe_external_no_trash` CHECK), so no "restore" branch is possible. + * (`fe_external_no_delete` CHECK), so no "restore" branch is possible. * Repeated calls with the same path are safe and idempotent. */ ensureExternalEntry(params: EnsureExternalEntryParams): Promise @@ -471,17 +471,17 @@ export interface IFileManager { // ─── Trash / Delete ─── /** - * Move entry to Trash (soft delete via `trashedAt`). Internal-only. + * Move entry to Trash (soft delete via `deletedAt`). Internal-only. * * Passing an external entry id throws: external entries cannot be trashed - * (enforced by the `fe_external_no_trash` CHECK constraint). Business layers + * (enforced by the `fe_external_no_delete` CHECK constraint). Business layers * should call `permanentDelete` on external entries if the user really wants * the reference gone. */ trash(id: FileEntryId): Promise /** - * Restore entry from Trash (`trashedAt = null`). Internal-only — external + * Restore entry from Trash (`deletedAt = null`). Internal-only — external * entries are never trashed, so passing one throws (the entry is already * active by definition). */ diff --git a/src/main/services/file/__tests__/FileManager.integration.test.ts b/src/main/services/file/__tests__/FileManager.integration.test.ts index 81313b9b56..b8d760c841 100644 --- a/src/main/services/file/__tests__/FileManager.integration.test.ts +++ b/src/main/services/file/__tests__/FileManager.integration.test.ts @@ -57,7 +57,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: 'internal-payload'.length, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -93,7 +93,7 @@ describe('FileManager (integration)', () => { ext: 'pdf', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -124,7 +124,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -150,7 +150,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -235,30 +235,30 @@ describe('FileManager (integration)', () => { await fm.trash(created.id) const trashed = await fm.getById(created.id) if (trashed.origin === 'internal') { - expect(typeof trashed.trashedAt).toBe('number') + expect(typeof trashed.deletedAt).toBe('number') } const restored = await fm.restore(created.id) if (restored.origin === 'internal') { - expect(restored.trashedAt).toBeUndefined() + expect(restored.deletedAt).toBeUndefined() } await fm.permanentDelete(created.id) await expect(fm.getById(created.id)).rejects.toThrow(/not found/i) }) - it('INT-5: trash on external entry is blocked by DB CHECK fe_external_no_trash', async () => { + it('INT-5: trash on external entry is blocked by DB CHECK fe_external_no_delete', async () => { const file = path.join(tmp, 'ext.txt') await writeFile(file, 'x') const e = await fm.ensureExternalEntry({ externalPath: file as never }) await expect(fm.trash(e.id)).rejects.toThrow() - // External BO has no `trashedAt` field by construction; if the trash - // attempt had slipped through, the DB CHECK fe_external_no_trash would + // External BO has no `deletedAt` field by construction; if the trash + // attempt had slipped through, the DB CHECK fe_external_no_delete would // have rejected it, so reading the row back must still surface as - // origin='external' with no trashedAt projection. + // origin='external' with no deletedAt projection. const refreshed = await fm.getById(e.id) expect(refreshed.origin).toBe('external') - expect(refreshed).not.toHaveProperty('trashedAt') + expect(refreshed).not.toHaveProperty('deletedAt') }) it('INT-6: permanentDelete on external leaves user file untouched', async () => { @@ -283,7 +283,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: 5, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -312,7 +312,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: 0, updatedAt: 0 }) @@ -368,7 +368,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -508,7 +508,7 @@ describe('FileManager (integration)', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: 0, updatedAt: 0 }) diff --git a/src/main/services/file/__tests__/toFileInfo.test.ts b/src/main/services/file/__tests__/toFileInfo.test.ts index 02d3a6392c..b064da980d 100644 --- a/src/main/services/file/__tests__/toFileInfo.test.ts +++ b/src/main/services/file/__tests__/toFileInfo.test.ts @@ -44,7 +44,7 @@ describe('toFileInfo', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: 1000, updatedAt: 1000 } as unknown as FileEntry @@ -72,7 +72,7 @@ describe('toFileInfo', () => { ext: 'png', size: bytes.length, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: 1000, updatedAt: 1000 } as unknown as FileEntry @@ -92,7 +92,7 @@ describe('toFileInfo', () => { ext: 'txt', size: null, externalPath: path.join(tmp, 'gone.txt'), - trashedAt: null, + deletedAt: null, createdAt: 1000, updatedAt: 1000 } as unknown as FileEntry @@ -111,7 +111,7 @@ describe('toFileInfo', () => { ext: null, size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: 1000, updatedAt: 1000 } as unknown as FileEntry diff --git a/src/main/services/file/internal/__tests__/orphanSweep.test.ts b/src/main/services/file/internal/__tests__/orphanSweep.test.ts index 024d950502..a353b22f00 100644 --- a/src/main/services/file/internal/__tests__/orphanSweep.test.ts +++ b/src/main/services/file/internal/__tests__/orphanSweep.test.ts @@ -37,7 +37,7 @@ describe('OrphanRefScanner', () => { ext: 'txt', size: 1, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -501,7 +501,7 @@ describe('runStartupFileSweep (FS-level)', () => { ext: 'txt', size: 4, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -774,7 +774,7 @@ describe('runStartupFileSweep (FS-level)', () => { it('preserves trashed entries’ physical files through the sweep (listAllIds returns active + trashed)', async () => { // Regression: `FileEntryService.listAllIds` unit test verifies it returns // active + trashed ids, but no end-to-end test wired it through - // runStartupFileSweep. A regression that filtered `WHERE trashedAt IS + // runStartupFileSweep. A regression that filtered `WHERE deletedAt IS // NULL` would silently nuke every trashed file's physical blob on next // boot — data loss the user did not consent to. Pin the contract here. const trashedId = '019606a0-0000-7000-8000-0000000fa2ed' as FileEntryId @@ -789,8 +789,8 @@ describe('runStartupFileSweep (FS-level)', () => { externalPath: null }) await writeFile(trashedPath, 'x') - // 2) Move to trash via the service (sets trashedAt; row stays in DB). - await fileEntryService.update(trashedId, { trashedAt: Date.now() }) + // 2) Move to trash via the service (sets deletedAt; row stays in DB). + await fileEntryService.update(trashedId, { deletedAt: Date.now() }) const report = await runStartupFileSweep({ fileEntryService }) expect(report.outcome).toBe('completed') diff --git a/src/main/services/file/internal/content/__tests__/hash.test.ts b/src/main/services/file/internal/content/__tests__/hash.test.ts index b4c49fbb2e..84cbd412b9 100644 --- a/src/main/services/file/internal/content/__tests__/hash.test.ts +++ b/src/main/services/file/internal/content/__tests__/hash.test.ts @@ -72,7 +72,7 @@ describe('internal/content/hash', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -99,7 +99,7 @@ describe('internal/content/hash', () => { ext: 'txt', size: null, externalPath: fileA, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }, @@ -110,7 +110,7 @@ describe('internal/content/hash', () => { ext: 'txt', size: null, externalPath: fileB, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now } @@ -130,7 +130,7 @@ describe('internal/content/hash', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) diff --git a/src/main/services/file/internal/content/__tests__/read.test.ts b/src/main/services/file/internal/content/__tests__/read.test.ts index 462d4e8041..db7edb8f4d 100644 --- a/src/main/services/file/internal/content/__tests__/read.test.ts +++ b/src/main/services/file/internal/content/__tests__/read.test.ts @@ -72,7 +72,7 @@ describe('internal/content/read', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -96,7 +96,7 @@ describe('internal/content/read', () => { ext: 'png', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -119,7 +119,7 @@ describe('internal/content/read', () => { ext: 'pdf', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) @@ -144,7 +144,7 @@ describe('internal/content/read', () => { ext: 'txt', size: null, externalPath: file, - trashedAt: null, + deletedAt: null, createdAt: now, updatedAt: now }) diff --git a/src/main/services/file/internal/entry/__tests__/create.test.ts b/src/main/services/file/internal/entry/__tests__/create.test.ts index 2806a51bf9..08e9f0b8c4 100644 --- a/src/main/services/file/internal/entry/__tests__/create.test.ts +++ b/src/main/services/file/internal/entry/__tests__/create.test.ts @@ -294,7 +294,7 @@ describe('internal/entry/create.createInternal', () => { ext: 'txt', size: null, externalPath: phantomCaseDifferent, - trashedAt: null, + deletedAt: null, createdAt: Date.now(), updatedAt: Date.now() }) diff --git a/src/main/services/file/internal/entry/__tests__/lifecycle.test.ts b/src/main/services/file/internal/entry/__tests__/lifecycle.test.ts index 1dc928e8e9..6f140ca189 100644 --- a/src/main/services/file/internal/entry/__tests__/lifecycle.test.ts +++ b/src/main/services/file/internal/entry/__tests__/lifecycle.test.ts @@ -86,30 +86,30 @@ describe('internal/entry/lifecycle', () => { const id = await makeInternal() await trash(deps, id) const entry = await fileEntryService.getById(id) - // trashedAt is an `optional` ms-epoch number on internal entries — + // deletedAt is an `optional` ms-epoch number on internal entries — // present + non-zero when trashed, absent (undefined) when live. expect(entry.origin).toBe('internal') if (entry.origin === 'internal') { - expect(typeof entry.trashedAt).toBe('number') + expect(typeof entry.deletedAt).toBe('number') } }) - it('throws when called on an external entry (CHECK fe_external_no_trash)', async () => { + it('throws when called on an external entry (CHECK fe_external_no_delete)', async () => { const id = await makeExternal() await expect(trash(deps, id)).rejects.toThrow() }) }) describe('restore', () => { - it('clears trashedAt on a trashed internal entry', async () => { + it('clears deletedAt on a trashed internal entry', async () => { const id = await makeInternal() await trash(deps, id) await restore(deps, id) const entry = await fileEntryService.getById(id) - // After restore, trashedAt is absent (undefined) on the BO. + // After restore, deletedAt is absent (undefined) on the BO. expect(entry.origin).toBe('internal') if (entry.origin === 'internal') { - expect(entry.trashedAt).toBeUndefined() + expect(entry.deletedAt).toBeUndefined() } }) diff --git a/src/main/services/file/internal/entry/lifecycle.ts b/src/main/services/file/internal/entry/lifecycle.ts index 1bf2cf9151..981702da97 100644 --- a/src/main/services/file/internal/entry/lifecycle.ts +++ b/src/main/services/file/internal/entry/lifecycle.ts @@ -2,7 +2,7 @@ * Entry lifecycle — trash / restore / permanentDelete + batch variants. * * `trash` / `restore` are internal-only; passing an external id throws (the - * `fe_external_no_trash` CHECK enforces this at the DB level for `trash`, and + * `fe_external_no_delete` CHECK enforces this at the DB level for `trash`, and * `restore` uses an explicit early-throw because trashed external rows cannot * exist by definition). * @@ -26,7 +26,7 @@ import type { FileManagerDeps } from '../deps' const logger = loggerService.withContext('internal/entry/lifecycle') export async function trash(deps: FileManagerDeps, id: FileEntryId): Promise { - await deps.fileEntryService.update(id, { trashedAt: Date.now() }) + await deps.fileEntryService.update(id, { deletedAt: Date.now() }) } export async function restore(deps: FileManagerDeps, id: FileEntryId): Promise { @@ -34,7 +34,7 @@ export async function restore(deps: FileManagerDeps, id: FileEntryId): Promise { diff --git a/src/main/services/file/watcher/__tests__/watcher.test.ts b/src/main/services/file/watcher/__tests__/watcher.test.ts index 2587817ee9..003ac19e23 100644 --- a/src/main/services/file/watcher/__tests__/watcher.test.ts +++ b/src/main/services/file/watcher/__tests__/watcher.test.ts @@ -80,7 +80,7 @@ describe('createDirectoryWatcher', () => { name: 'note', ext: 'txt', size: null, - trashedAt: null, + deletedAt: null, createdAt: 0, updatedAt: 0 } as never) @@ -107,7 +107,7 @@ describe('createDirectoryWatcher', () => { name: 'gone', ext: 'txt', size: null, - trashedAt: null, + deletedAt: null, createdAt: 0, updatedAt: 0 } as never) @@ -188,7 +188,7 @@ describe('createDirectoryWatcher', () => { name: 'qué', ext: 'txt', size: null, - trashedAt: null, + deletedAt: null, createdAt: 0, updatedAt: 0 } as never) diff --git a/v2-refactor-temp/docs/file-manager/file-arch-problems-response.md b/v2-refactor-temp/docs/file-manager/file-arch-problems-response.md index 3bd56f4be5..35964a2631 100644 --- a/v2-refactor-temp/docs/file-manager/file-arch-problems-response.md +++ b/v2-refactor-temp/docs/file-manager/file-arch-problems-response.md @@ -37,7 +37,7 @@ - **`createInternalEntry` / `ensureExternalEntry` IPC** 在 main 侧一次性完成 "FS 落地 + DB 登记"(internal)或 "upsert + stat 验证"(external),成功/失败整体可见 - Internal:拷贝内容到 `{userData}/files/{id}.{ext}` 后 insert `file_entry` 行 -- External:按 `externalPath` 纯 upsert(同 path 有行就 reuse + 刷 snapshot;否则新建);external 不进入 trashed 生命周期,`fe_external_no_trash` CHECK 在 schema 层兜底 +- External:按 `externalPath` 纯 upsert(同 path 有行就 reuse + 刷 snapshot;否则新建);external 不进入 trashed 生命周期,`fe_external_no_delete` CHECK 在 schema 层兜底 - Renderer **没有**独立登记 entry 的入口——`addFile` 这类绕路被类型系统封闭 **参考**:`architecture.md §2.3` 的 `createInternalEntry` / `ensureExternalEntry`(及批量版本)、`file-manager-architecture.md §1.2`(external path unique)。 diff --git a/v2-refactor-temp/docs/file-manager/filestorage-redesign.md b/v2-refactor-temp/docs/file-manager/filestorage-redesign.md index 1009d1c375..dc35457c76 100644 --- a/v2-refactor-temp/docs/file-manager/filestorage-redesign.md +++ b/v2-refactor-temp/docs/file-manager/filestorage-redesign.md @@ -5,7 +5,7 @@ > 本文档描述的拆分目标("🔀 FileManager.createEntry")捕获的是早期设计,已被后续评审推翻: > > - `FileManager.createEntry({origin, content, ...})` → 拆分为 `createInternalEntry`(内容来源 discriminated union)+ `ensureExternalEntry`(纯 upsert by path) -> - External entry 不进入 trash 生命周期(`fe_external_no_trash` CHECK) +> - External entry 不进入 trash 生命周期(`fe_external_no_delete` CHECK) > - `permanentDelete` 对 external 只删 DB 行,物理文件不动 > > **实现准绳**:[`docs/references/file/file-manager-architecture.md`](../../../docs/references/file/file-manager-architecture.md)、[`rfc-file-manager.md`](./rfc-file-manager.md)、[`file-arch-problems-response.md`](./file-arch-problems-response.md)。 diff --git a/v2-refactor-temp/docs/file-manager/ipc-redesign.md b/v2-refactor-temp/docs/file-manager/ipc-redesign.md index f077f6aef0..d86f09c76c 100644 --- a/v2-refactor-temp/docs/file-manager/ipc-redesign.md +++ b/v2-refactor-temp/docs/file-manager/ipc-redesign.md @@ -5,7 +5,7 @@ > 本文档捕获的是早期设计,关键术语/决策已被后续评审推翻: > > - `FileManager.createEntry({origin})` 已拆分为 `createInternalEntry` + `ensureExternalEntry`(A-7) -> - External entry 不再进入 trash 生命周期(`fe_external_no_trash` CHECK) +> - External entry 不再进入 trash 生命周期(`fe_external_no_delete` CHECK) > - `externalPath` 唯一性由 partial unique 升级为 global unique > - `permanentDelete` 对 external 只删 DB 行,不触碰物理文件 > diff --git a/v2-refactor-temp/docs/file-manager/migration-plan.md b/v2-refactor-temp/docs/file-manager/migration-plan.md index e2cc26e389..c3eb32c65b 100644 --- a/v2-refactor-temp/docs/file-manager/migration-plan.md +++ b/v2-refactor-temp/docs/file-manager/migration-plan.md @@ -50,7 +50,7 @@ | 旧 `FileMetadata` 的角色 | v2 对应类型 | 说明 | | ------------------------ | ---------------- | ---------------------------------------------------------------------------- | -| DB 行 / 持久化身份 | `FileEntry` | 带 `id`、`origin`、`trashedAt`;有 lifecycle;Zod brand 强制走 sanctioned 生产路径 | +| DB 行 / 持久化身份 | `FileEntry` | 带 `id`、`origin`、`deletedAt`;有 lifecycle;Zod brand 强制走 sanctioned 生产路径 | | 磁盘描述符 / 临时传参 | `FileInfo` | 带 `path`、`modifiedAt`;live view;任意构造 | | 跨边界引用(两者通用) | `FileHandle` | tagged union;IPC 边界首选签名 | @@ -346,7 +346,7 @@ ORDER BY ref_count DESC - 删除操作原子性:业务侧只管 file_ref,不需要跨表 trigger - 抗误删:短时间内重新引用该文件不会 fail(比如 "undo" 删除一条 message 时) -- 与 internal `trashedAt` 软删的哲学一致(延迟、可逆);external 虽然没有软删状态,但延迟 orphan 清理仍然提供"重新引用不失败"的好处 +- 与 internal `deletedAt` 软删的哲学一致(延迟、可逆);external 虽然没有软删状态,但延迟 orphan 清理仍然提供"重新引用不失败"的好处 #### 2.3.11 UI 变化 @@ -1155,8 +1155,8 @@ interface FileEntry { ext: string | null; // 扩展名,**不含前导点**('pdf'),无扩展名时 null size: number; externalPath: string | null; - // trashedAt 仅 internal 可非空;external 恒为 null(fe_external_no_trash CHECK) - trashedAt: number | null; + // deletedAt 仅 internal 可非空;external 恒为 null(fe_external_no_delete CHECK) + deletedAt: number | null; createdAt: number; updatedAt: number; } @@ -1478,7 +1478,7 @@ async function migrateFileEntry(oldFile: DexieFileRow): Promise { oldFile.origin === "external" ? canonicalizeExternalPath(oldFile.path) : null, - trashedAt: null, // Dexie 没软删除字段;external 也不允许 trashed(fe_external_no_trash) + deletedAt: null, // Dexie 没软删除字段;external 也不允许 trashed(fe_external_no_delete) createdAt: toMs(oldFile.created_at), // ISO → ms updatedAt: toMs(oldFile.created_at), // Dexie 无 updatedAt,用 createdAt }; diff --git a/v2-refactor-temp/docs/file-manager/rfc-file-manager.md b/v2-refactor-temp/docs/file-manager/rfc-file-manager.md index 39b65e8fc2..382fd906ce 100644 --- a/v2-refactor-temp/docs/file-manager/rfc-file-manager.md +++ b/v2-refactor-temp/docs/file-manager/rfc-file-manager.md @@ -71,7 +71,7 @@ | `origin` 枚举 | `'internal' \| 'external'` | Cherry 拥有 vs 用户拥有;语义清晰 | | External path 唯一性 | Global unique index on `externalPath`(internal 行为 null,SQLite UNIQUE 视多个 NULL 互不冲突,天然只约束 external 行) | 同 path 全局最多一条;`ensureExternalEntry` 纯 upsert by path,无 "restore trashed" 分支 | | `size` 字段 | 必填(INTEGER NOT NULL) | 查询/排序需要;external 为最后观测的快照 | -| trash 语义 | `trashedAt` 时间戳;**仅对 internal 有效**,external 由 `fe_external_no_trash` CHECK 禁止 trashed | internal 保留软删可逆窗口;external 生命周期单向(Active → Deleted),重建成本为零所以不需要撤销 | +| trash 语义 | `deletedAt` 时间戳;**仅对 internal 有效**,external 由 `fe_external_no_delete` CHECK 禁止 trashed | internal 保留软删可逆窗口;external 生命周期单向(Active → Deleted),重建成本为零所以不需要撤销 | | external 删除语义 | `permanentDelete` 只删 DB 行;物理文件不动(path-level `ops.remove` 独立提供) | Cherry 不在 entry-level 自动 unlink 用户拥有的文件;用户有需要时走独立的 unmanaged 删除通道 | | `sourceType` / `role` | 应用层 Zod 验证 + 编译期 checker 注册 | 新增 sourceType 无需 DB migration | | `file_ref` 防重 | UNIQUE(fileEntryId, sourceType, sourceId, role) | 一个业务对象不会以同一角色重复引用同一文件 | @@ -116,14 +116,14 @@ export const fileEntryTable = sqliteTable( /** * 软删时间戳(ms epoch);null 表示未 trash。**仅 internal 可用**; - * external 恒为 null(由 `fe_external_no_trash` CHECK 强制)。 + * external 恒为 null(由 `fe_external_no_delete` CHECK 强制)。 */ - trashedAt: integer(), + deletedAt: integer(), ...createUpdateTimestamps, }, (t) => [ - index("fe_trashed_at_idx").on(t.trashedAt), + index("fe_deleted_at_idx").on(t.deletedAt), index("fe_created_at_idx").on(t.createdAt), // 同 externalPath 全局最多一条。internal 行为 null,SQLite 视多个 NULL // 互不冲突,因此天然只约束 external 行。兼任查询索引。 @@ -135,8 +135,8 @@ export const fileEntryTable = sqliteTable( ), // External 不可 trashed:trash/restore 仅对 internal,external 走 permanentDelete check( - "fe_external_no_trash", - sql`${t.origin} != 'external' OR ${t.trashedAt} IS NULL`, + "fe_external_no_delete", + sql`${t.origin} != 'external' OR ${t.deletedAt} IS NULL`, ), ], ); @@ -287,7 +287,7 @@ interface FileInfo { } ``` -**定位**:`FileInfo` 是**path 引用下的文件数据形状**——与 `FilePathHandle` 在引用层对应。它不承载身份(无 `id`、无 `origin`、无 `trashedAt`),只承载"磁盘上此刻的一份描述"。 +**定位**:`FileInfo` 是**path 引用下的文件数据形状**——与 `FilePathHandle` 在引用层对应。它不承载身份(无 `id`、无 `origin`、无 `deletedAt`),只承载"磁盘上此刻的一份描述"。 **与 `FileEntry` 的关系**: @@ -382,7 +382,7 @@ async function ensureExternalEntry( // 不含 fs.realpath(case-insensitive FS 去重由 Phase 2 视用户反馈补)。 // 是 upsert/查询的唯一 key 来源。 const canonicalPath = canonicalizeExternalPath(params.externalPath); - // External 恒非 trashed(fe_external_no_trash CHECK),所以不需要 includeTrashed。 + // External 恒非 trashed(fe_external_no_delete CHECK),所以不需要 includeTrashed。 const existing = await fileEntryService.findByExternalPath(canonicalPath); if (existing) return existing; // name/ext 来自 externalPath,不会漂移;size 不存 @@ -416,7 +416,7 @@ async function ensureExternalEntry( ### 5.3 trash / restore(软删除,仅 internal) -**纯 DB 操作,不碰 FS。仅对 internal 有效**——external 由 `fe_external_no_trash` CHECK 禁止 trashed,调用入口先校验 origin,传入 external id 直接抛错;schema 层兜底: +**纯 DB 操作,不碰 FS。仅对 internal 有效**——external 由 `fe_external_no_delete` CHECK 禁止 trashed,调用入口先校验 origin,传入 external id 直接抛错;schema 层兜底: ```typescript async function trash(id: FileEntryId): Promise { @@ -426,7 +426,7 @@ async function trash(id: FileEntryId): Promise { `Cannot trash external entry ${id}; external entries have no trashed state. Use permanentDelete.`, ); } - await fileEntryService.update(id, { trashedAt: Date.now() }); + await fileEntryService.update(id, { deletedAt: Date.now() }); } async function restore(id: FileEntryId): Promise { @@ -436,7 +436,7 @@ async function restore(id: FileEntryId): Promise { `Cannot restore external entry ${id}; external entries are never trashed.`, ); } - return fileEntryService.update(id, { trashedAt: null }); + return fileEntryService.update(id, { deletedAt: null }); } ``` @@ -700,7 +700,7 @@ export interface FileSchemas { | `getContentHash` | `FileHandle` | `string` | xxhash-128 | | `write` | `FileHandle, data` | `FileVersion` | 原子写 | | `writeIfUnchanged` | `FileHandle, data, version` | `FileVersion` | 乐观并发写 | -| `trash` | `{ id }` | `void` | 软删(DB only)。**Internal-only** — 传 external id 抛错(`fe_external_no_trash` CHECK) | +| `trash` | `{ id }` | `void` | 软删(DB only)。**Internal-only** — 传 external id 抛错(`fe_external_no_delete` CHECK) | | `restore` | `{ id }` | `FileEntry` | 从 Trash 恢复。**Internal-only** — external 恒非 trashed,传 external id 抛错 | | `permanentDelete` | `FileHandle` | `void` | 删 entry。Internal: unlink FS + 删 DB 行;External (managed): **只删 DB 行**,物理文件不动;Unmanaged path: `ops.remove(path)` 物理删除 | | `batchTrash` / `batchRestore` | 批量参数 | `BatchOperationResult` | 批量版本,internal-only | @@ -859,7 +859,7 @@ private transformFile(old: DexieFileMetadata): InsertFileEntry { ext: (old.ext ?? '').replace(/^\./, '') || null, size: old.size ?? 0, externalPath: null, - trashedAt: null, + deletedAt: null, createdAt: new Date(old.created_at).getTime(), updatedAt: new Date(old.created_at).getTime() } @@ -999,7 +999,7 @@ Phase 1a ──→ Phase 1b.1 ──→ Phase 1b.2 ──→ Phase 1b.3 ── | 类别 | 内容 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| DB Schema | `src/main/data/db/schemas/file.ts` — `fileEntryTable` + `fileRefTable`,全部 CHECK 约束(`fe_origin_consistency` / `fe_external_no_trash` / `fe_size_internal_only`)就位 | +| DB Schema | `src/main/data/db/schemas/file.ts` — `fileEntryTable` + `fileRefTable`,全部 CHECK 约束(`fe_origin_consistency` / `fe_external_no_delete` / `fe_size_internal_only`)就位 | | DB migration | `pnpm agents:generate` 生成的 SQL | | 跨进程类型 | `packages/shared/data/types/file/` DTO(FileEntry brand DU / FileRef / DanglingState 等);`packages/shared/data/api/schemas/files.ts` DataApi schema 声明 | | File 类型 | `packages/shared/file/types/ipc.ts` File IPC 契约;`packages/shared/file/types/handle.ts` `FileHandle` tagged union + factory;`packages/shared/file/types/info.ts` `FileInfo` + `toFileInfo` **declare only** | @@ -1077,7 +1077,7 @@ Phase 1a ──→ Phase 1b.1 ──→ Phase 1b.2 ──→ Phase 1b.3 ── **出口条件**: - renderer 可完整走 FileManager 做增删改 -- external entry 的 `trash` 调用被 DB CHECK 阻断(`fe_external_no_trash`) +- external entry 的 `trash` 调用被 DB CHECK 阻断(`fe_external_no_delete`) - 写失败时物理文件零残留(atomic 保证) - `writeIfUnchanged` 在同秒+同 size 场景用 content-hash 回退,不误判