FN-9002: model project-owned schema identities

Align project schema declarations with the existing ownership partition.

- Model eight project-owned tables with project-leading identities and defaults.
- Preserve composite foreign-key and project-scoped uniqueness metadata.
- Add PostgreSQL declaration coverage and document deferred runtime predicates.

Files changed:
 ...n-9002-project-schema-ownership-declarations.md |   7 +
 docs/storage.md                                    |   1 +
 ...roject-schema-ownership-declarations.pg.test.ts | 245 +++++++++++++++++++++
 .../core/src/async-stores/async-secrets-store.ts   |  26 +--
 packages/core/src/postgres/schema/project.ts       |  44 +++-
 5 files changed, 298 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-9002

Fusion-Task-Lineage: b6f45d1c-d7d9-4a96-b86e-6d120df3f756

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-12 07:09:55 -07:00
parent 36691e54d2
commit 0ed0e53d56
5 changed files with 298 additions and 25 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Model eight project-owned storage tables with their partition identities.
category: internal
dev: Eight declarations now match the 0006 project partition; runtime predicates land in FN-9000.

View File

@@ -38,6 +38,7 @@ See the [2026-07-14 PostgreSQL runtime cutover review](./postgres-migration-revi
- `project.agent_ratings` is project-owned with composite `(project_id, id)` identity, allowing the same rating id in separate projects without cross-project reads or deletes. The dynamic `0006_project_ownership.sql` migration reconciles the physical table; `0055_fn_8988_agent_ratings_project_partition.sql` repeats that guarantee idempotently for historical drift. Bound `addRating`, `getRatings`, and `deleteRating` apply the project ownership partition, while unbound compatibility layers retain trigger-stamped writes and unscoped reads/deletes.
- FN-8997 audited `workflow_steps`, `chat_room_members`, and `chat_room_messages`: their Drizzle declarations now model 0006's `project_id` and composite keys, and bound workflow/chat helpers scope reads and mutations on that partition. Chat isolation requires both membership/message predicates **and** the parent `chat_rooms.project_id` predicate; either leg alone can resolve a foreign row when room IDs collide. `plugins` remains an intentionally unmodeled compatibility table because it has no runtime Drizzle path. Migration `0056_fn_8997_project_ownership_declaration_drift.sql` is idempotent and adds only partition-prefixed predicate indexes; it does not rewrite healthy 0006 ownership columns or keys.
- `project.workflows` has project-local `(project_id, id)` identity. Bound definition reads, updates, deletes, companion workflow settings/prompt-override deletes, and analytics name prefetches use `projectScopeFor`; blank/unbound layers deliberately retain cross-project compatibility reads. The per-project workflow-id counter intentionally scans occupancy across every partition before allocation, because burning a colliding ID is safer than reusing a legacy or stale-counter ID held elsewhere.
- FN-9002 makes the `0006_project_ownership.sql` partition expressible for `artifacts`, `secrets`, `branch_groups`, `plugin_activations`, `chat_messages`, `run_audit_events`, `verification_cache`, and `approval_requests`: each declaration retains the database default and models its project-leading identity (plus the artifacts task FK and secrets/branch-name unique keys). Their runtime owners are respectively async attachments/archive/task mutation; async secrets; async branch groups; activation analytics/events; async chat; async audit/merge coordination/data layer; workflow definitions; and async approval requests. This is declaration-only: existing inserts keep omitting `projectId`, public record mappers keep omitting it, and all project predicates are deliberately deferred to FN-9000.
- Startup and Batch 1 self-healing expire locks when their lease elapsed or the owner task is terminal/missing. They never move a task or alter scheduler, worktree, semaphore, or verification state. Run-audit events are `symbol-lock:acquired`, `symbol-lock:acquire-conflict`, `symbol-lock:renewed`, `symbol-lock:released`, `symbol-lock:reconcile-stale`, and deduplicated `symbol-lock:reconcile-stale-no-action`; metadata uses only counts/outcomes and normalized opaque keys.
- FN-8405 adds `Task.declaredSymbols` as the durable, normalized task declaration source. `## Declared Symbols` in PROMPT.md is parsed only on create/update writes: an absent key may hydrate from the prompt, while a present `undefined`, `null` (update), or `[]` clears and suppresses hydration; a non-empty explicit array wins. Store resolution (`resolveTaskSymbols` and `resolveTaskSymbolsForWorkItem({ taskId })`) reads only the durable field, and slim projections plus archive/restore retain it. Scheduler admission remains a separate FN-8306 consumer; File Scope is never treated as a symbol source.

View File

@@ -0,0 +1,245 @@
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "vitest";
import { getTableConfig } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { AsyncSecretsStore } from "../../async-stores/async-secrets-store.js";
import { createApprovalRequest } from "../../async-stores/async-approval-request-store.js";
import { addChatMessage } from "../../async-stores/async-chat-store.js";
import { recordRunAuditEvent, recordRunAuditEventWithinTransaction } from "../../postgres/data-layer.js";
import { insertArtifactRow } from "../../task-store/async/async-comments-attachments.js";
import { createBranchGroup } from "../../task-store/async/async-branch-groups.js";
import { recordPluginActivation } from "../../task-store/async/async-events.js";
import * as schema from "../../postgres/schema/index.js";
import {
createSharedPgTaskStoreTestHarness,
pgDescribe,
type SharedPgTaskStoreHarness,
} from "../../__test-utils__/pg-test-harness.js";
const tableNames = [
"artifacts",
"secrets",
"branch_groups",
"plugin_activations",
"chat_messages",
"run_audit_events",
"verification_cache",
"approval_requests",
] as const;
const declaredTables = {
artifacts: schema.project.artifacts,
secrets: schema.project.secrets,
branch_groups: schema.project.branchGroups,
plugin_activations: schema.project.pluginActivations,
chat_messages: schema.project.chatMessages,
run_audit_events: schema.project.runAuditEvents,
verification_cache: schema.project.verificationCache,
approval_requests: schema.project.approvalRequests,
} as const;
const expectedPrimaryKeys: Record<(typeof tableNames)[number], string[]> = {
artifacts: ["project_id", "id"],
secrets: ["project_id", "id"],
branch_groups: ["project_id", "id"],
plugin_activations: ["project_id", "id"],
chat_messages: ["project_id", "id"],
run_audit_events: ["project_id", "id"],
verification_cache: ["project_id", "tree_sha", "test_command", "build_command"],
approval_requests: ["project_id", "id"],
};
pgDescribe("project schema ownership declarations", () => {
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_fn9002_schema",
projectId: "fn9002-runtime-bound",
});
beforeAll(h.beforeAll);
afterAll(h.afterAll);
beforeEach(h.beforeEach);
afterEach(h.afterEach);
it("proves migration 0006 physically partitions all eight drifted tables", async () => {
/*
FNXC:MultiProjectIsolation 2026-08-12-13:45:
Migration 0006 partitioned every project-schema base table before these eight Drizzle
declarations caught up. Assert catalog truth first so declaration work cannot guess an
ordered key, unique index, or foreign-key name from the pre-isolation snapshot.
*/
for (const tableName of tableNames) {
const columns = await h.adminSql()<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
}>>`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'project' AND table_name = ${tableName} AND column_name = 'project_id'
`;
expect(columns).toEqual([expect.objectContaining({
column_name: "project_id",
data_type: "text",
is_nullable: "NO",
column_default: expect.stringContaining("current_setting"),
})]);
const constraints = await h.adminSql()<Array<{
conname: string;
contype: string;
columns: string[];
}>>`
SELECT c.conname, c.contype, array_agg(a.attname ORDER BY k.ordinality) AS columns
FROM pg_constraint c
CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum, ordinality)
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
WHERE c.conrelid = ('project.' || ${tableName})::regclass
AND c.contype IN ('p', 'u', 'f')
GROUP BY c.conname, c.contype
ORDER BY c.contype, c.conname
`;
expect(constraints.find((constraint) => constraint.contype === "p")?.columns)
.toEqual(expectedPrimaryKeys[tableName]);
for (const constraint of constraints) {
expect(constraint.columns[0]).toBe("project_id");
}
const uniqueIndexes = await h.adminSql()<Array<{ index_name: string; columns: string[] }>>`
SELECT idx.relname AS index_name,
array_agg(a.attname ORDER BY key.ordinality) AS columns
FROM pg_index i
JOIN pg_class idx ON idx.oid = i.indexrelid
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY key(attnum, ordinality)
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = key.attnum
WHERE i.indrelid = ('project.' || ${tableName})::regclass
AND i.indisunique AND NOT i.indisprimary
GROUP BY idx.relname
`;
for (const index of uniqueIndexes) {
expect(index.columns[0]).toBe("project_id");
}
const declaration = getTableConfig(declaredTables[tableName]);
expect(declaration.columns.map((column) => column.name)).toContain("project_id");
expect(declaration.columns.find((column) => column.name === "project_id")?.hasDefault).toBe(true);
expect(declaration.primaryKeys).toHaveLength(1);
expect(declaration.primaryKeys[0]?.columns.map((column) => column.name))
.toEqual(expectedPrimaryKeys[tableName]);
}
const artifactForeignKey = getTableConfig(schema.project.artifacts).foreignKeys
.find((key) => key.getName() === "artifacts_task_id_fkey");
expect(artifactForeignKey?.reference().columns.map((column) => column.name))
.toEqual(["project_id", "task_id"]);
expect(artifactForeignKey?.reference().foreignColumns.map((column) => column.name))
.toEqual(["project_id", "id"]);
expect(getTableConfig(schema.project.secrets).uniqueConstraints.map((key) => ({
name: key.getName(), columns: key.columns.map((column) => column.name),
}))).toContainEqual({ name: "secrets_key_unique", columns: ["project_id", "key"] });
expect(getTableConfig(schema.project.branchGroups).uniqueConstraints.map((key) => ({
name: key.getName(), columns: key.columns.map((column) => column.name),
}))).toContainEqual({ name: "branch_groups_branch_name_key", columns: ["project_id", "branch_name"] });
});
it("keeps production inserts default-stamped and public mappers partition-free", async () => {
/*
FNXC:MultiProjectIsolation 2026-08-12-13:45:
These are the real async store helpers whose inserts intentionally omit projectId. Exercise
them through a GUC-bound layer so the declaration change proves Drizzle still delegates
ownership stamping to migration 0006 rather than relying only on catalog or raw-SQL tests.
*/
const layer = h.layer();
const artifact = await insertArtifactRow(layer, {
type: "document", title: "Runtime artifact", content: "content", authorId: "agent",
authorType: "agent",
}, {});
const secrets = new AsyncSecretsStore(layer, async () => Buffer.alloc(32, 7));
const secret = await secrets.createSecret({ scope: "project", key: "RUNTIME_KEY", plaintextValue: "value" });
const branchGroup = await createBranchGroup(layer.db, {
sourceType: "planning", sourceId: "runtime", branchName: "runtime-branch",
});
const activation = await recordPluginActivation(layer.db, { pluginId: "runtime-plugin", source: "test" });
const message = await addChatMessage(layer.db, {
id: "runtime-message", sessionId: "runtime-session", role: "user", content: "Runtime",
thinkingOutput: null, metadata: null, attachments: undefined, createdAt: "2026-08-12T00:00:00.000Z",
});
const audit = await recordRunAuditEvent(layer, {
agentId: "agent", runId: "runtime-run", domain: "test", mutationType: "insert", target: "runtime",
});
const approval = await createApprovalRequest(layer, {
id: "runtime-approval",
requester: { actorId: "agent", actorType: "agent", actorName: "Agent" },
targetAction: { category: "test", action: "insert", summary: "Runtime", resourceType: "task", resourceId: "runtime" },
});
await h.store().recordVerificationCachePass("runtime-tree", "test", "build", "runtime-task");
await h.store().recordVerificationCachePass("runtime-tree", "test", "build", "updated-task");
const mapped = [artifact, secret, branchGroup, activation, message, audit, approval];
for (const row of mapped) expect("projectId" in row).toBe(false);
expect(await h.store().getVerificationCacheHit("runtime-tree", "test", "build"))
.toMatchObject({ taskId: "updated-task" });
for (const tableName of tableNames) {
const rows = await h.adminSql()<Array<{ project_id: string }>>`
SELECT project_id FROM project.${h.adminSql().unsafe(tableName)}
WHERE project_id = 'fn9002-runtime-bound'
`;
expect(rows.length).toBeGreaterThan(0);
}
await layer.transactionImmediate(async (tx) => {
await tx.execute(sql`SELECT set_config('fusion.project_id', '', true)`);
await recordRunAuditEventWithinTransaction(tx, {
agentId: "agent", runId: "unbound-run", domain: "test", mutationType: "insert", target: "unbound",
});
});
expect(await h.adminSql()<Array<{ project_id: string }>>`
SELECT project_id FROM project.run_audit_events WHERE run_id = 'unbound-run'
`).toEqual([{ project_id: "__legacy_unscoped__" }]);
});
it("keeps duplicate natural identities available in every physical partition", async () => {
const insertRows = async (projectId: string): Promise<void> => {
await h.adminSql()`SELECT set_config('fusion.project_id', ${projectId}, false)`;
await h.adminSql()`INSERT INTO project.artifacts (id, type, title, author_id, created_at, updated_at)
VALUES ('shared', 'document', 'Shared', 'agent', '2026-08-12', '2026-08-12')`;
await h.adminSql()`INSERT INTO project.secrets (id, key, value_ciphertext, nonce, created_at, updated_at)
VALUES ('shared', 'SHARED_KEY', decode('00', 'hex'), decode('00', 'hex'), '2026-08-12', '2026-08-12')`;
await h.adminSql()`INSERT INTO project.branch_groups (id, source_type, source_id, branch_name, created_at, updated_at)
VALUES ('shared', 'planning', 'shared', 'shared-branch', 1, 1)`;
await h.adminSql()`INSERT INTO project.plugin_activations (plugin_id, source, activated_at)
VALUES ('shared', 'test', '2026-08-12')`;
await h.adminSql()`INSERT INTO project.chat_messages (id, session_id, role, content, created_at)
VALUES ('shared', 'shared', 'user', 'Shared', '2026-08-12')`;
await h.adminSql()`INSERT INTO project.run_audit_events (id, timestamp, agent_id, run_id, domain, mutation_type, target)
VALUES ('shared', '2026-08-12', 'agent', 'run', 'test', 'insert', 'shared')`;
await h.adminSql()`INSERT INTO project.verification_cache (tree_sha, test_command, build_command, recorded_at)
VALUES ('shared', 'test', 'build', '2026-08-12')`;
await h.adminSql()`INSERT INTO project.approval_requests (
id, status, requester_actor_id, requester_actor_type, requester_actor_name,
target_action_category, target_action_operation, target_action_summary,
target_resource_type, target_resource_id, requested_at, created_at, updated_at
) VALUES (
'shared', 'pending', 'agent', 'agent', 'Agent', 'test', 'insert', 'Shared',
'task', 'shared', '2026-08-12', '2026-08-12', '2026-08-12'
)`;
};
await insertRows("fn9002-project-a");
await insertRows("fn9002-project-b");
for (const tableName of tableNames) {
const rows = await h.adminSql()<Array<{ count: string }>>`
SELECT count(*)::text AS count FROM project.${h.adminSql().unsafe(tableName)}
WHERE project_id IN ('fn9002-project-a', 'fn9002-project-b')
`;
expect(rows).toEqual([{ count: "2" }]);
}
await h.adminSql()`SELECT set_config('fusion.project_id', '', false)`;
await h.adminSql()`INSERT INTO project.chat_messages (id, session_id, role, content, created_at)
VALUES ('unbound', 'unbound', 'user', 'Unbound', '2026-08-12')`;
expect(await h.adminSql()<Array<{ project_id: string }>>`
SELECT project_id FROM project.chat_messages WHERE id = 'unbound'
`).toEqual([{ project_id: "__legacy_unscoped__" }]);
});
});

View File

@@ -118,24 +118,22 @@ export class SecretsStoreError extends Error {
}
/**
* FNXC:SecretsStore 2026-06-24-20:12:
* The columns both secrets tables share. project.secrets and
* central.secrets_global have identical column shapes but are distinct Drizzle
* table objects (different schema/name literal). The helpers operate on the
* project.secrets table type and the global table is cast at the dispatch
* boundary since the two are structurally identical column-for-column.
* FNXC:SecretsStore 2026-08-12-13:45:
* Migration 0006 partitions project.secrets with project_id while
* central.secrets_global remains global. The shared helpers deliberately cast
* the central ref at this dispatch boundary; FN-9000 adds project-only
* predicates without leaking a project_id reference into the central leg.
*/
type ProjectSecretsTable = typeof schema.project.secrets;
/**
* Resolve the Drizzle table ref for a scope. Both tables share the same column
* shape, so the call sites are identical once the table ref is selected.
* FNXC:SecretsStore 2026-06-24-20:10:
* Under the shared PostgreSQL backend a single connection serves both schemas,
* so the dual-database injection collapses to a scope-to-table dispatch. The
* central.secrets_global table is structurally identical to project.secrets
* (same columns, same types), so it is cast to the project table type at the
* dispatch boundary; the helper bodies then compile against one table type.
* Resolve the Drizzle table ref for a scope. The deliberate shape bridge keeps
* current shared helper bodies stable until project-only scoping arrives.
* FNXC:SecretsStore 2026-08-12-13:45:
* A shared PostgreSQL connection serves both schemas. project.secrets now
* models its 0006 partition while central.secrets_global does not have that
* column, so the cast is a dispatch-only compatibility bridge, not evidence
* that their physical shapes are identical.
*/
function tableForScope(scope: SecretScope): ProjectSecretsTable {
return scope === "project"

View File

@@ -1171,8 +1171,15 @@ export const taskDocuments = projectSchema.table("task_documents", {
index("idxTaskDocumentsTaskId").on(t.taskId),
]);
/*
FNXC:MultiProjectIsolation 2026-08-12-13:45:
Migration 0006 physically partitions these declarations, but the Drizzle schema drifted and could
not express their project_id-leading identities. Keep the database current_setting default so
bound and unbound trigger-stamped inserts omit projectId; FN-9000 owns runtime predicates.
*/
export const artifacts = projectSchema.table("artifacts", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
type: text("type").notNull(),
title: text("title").notNull(),
description: text("description"),
@@ -1187,7 +1194,8 @@ export const artifacts = projectSchema.table("artifacts", {
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
}, (t) => [
foreignKey({ columns: [t.taskId], foreignColumns: [tasks.id] }).onDelete("cascade"),
primaryKey({ columns: [t.projectId, t.id] }),
foreignKey({ name: "artifacts_task_id_fkey", columns: [t.projectId, t.taskId], foreignColumns: [tasks.projectId, tasks.id] }).onDelete("cascade"),
index("idxArtifactsTaskId").on(t.taskId),
index("idxArtifactsAuthorId").on(t.authorId),
index("idxArtifactsType").on(t.type),
@@ -1391,7 +1399,8 @@ export const evalRunEvents = projectSchema.table("eval_run_events", {
// ── Secrets (project-scoped) ─────────────────────────────────────────
export const secrets = projectSchema.table("secrets", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
key: text("key").notNull(),
valueCiphertext: bytea("value_ciphertext").notNull(),
nonce: bytea("nonce").notNull(),
@@ -1404,7 +1413,8 @@ export const secrets = projectSchema.table("secrets", {
lastReadAt: text("last_read_at"),
lastReadBy: text("last_read_by"),
}, (t) => [
unique("secrets_key_unique").on(t.key),
primaryKey({ columns: [t.projectId, t.id] }),
unique("secrets_key_unique").on(t.projectId, t.key),
check("secrets_access_policy_check", sql`${t.accessPolicy} IN ('auto', 'prompt', 'deny')`),
check("secrets_env_exportable_check", sql`${t.envExportable} IN (0, 1)`),
]);
@@ -1451,10 +1461,11 @@ export const missions = projectSchema.table("missions", {
}, (t) => [primaryKey({ columns: [t.projectId, t.id] })]);
export const branchGroups = projectSchema.table("branch_groups", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
sourceType: text("source_type").notNull(),
sourceId: text("source_id").notNull(),
branchName: text("branch_name").notNull().unique(),
branchName: text("branch_name").notNull(),
worktreePath: text("worktree_path"),
autoMerge: integer("auto_merge").notNull().default(0),
prState: text("pr_state").notNull().default("none"),
@@ -1470,6 +1481,8 @@ export const branchGroups = projectSchema.table("branch_groups", {
updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
closedAt: bigint("closed_at", { mode: "number" }),
}, (t) => [
primaryKey({ columns: [t.projectId, t.id] }),
unique("branch_groups_branch_name_key").on(t.projectId, t.branchName),
check("branch_groups_source_type_check", sql`${t.sourceType} IN ('mission','planning','new-task')`),
check("branch_groups_pr_state_check", sql`${t.prState} IN ('none','open','merged','closed')`),
check("branch_groups_status_check", sql`${t.status} IN ('open','finalized','abandoned')`),
@@ -1905,12 +1918,14 @@ export const usageEvents = projectSchema.table("usage_events", {
]);
export const pluginActivations = projectSchema.table("plugin_activations", {
id: integer("id").generatedAlwaysAsIdentity().primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: integer("id").generatedAlwaysAsIdentity().notNull(),
pluginId: text("plugin_id").notNull(),
source: text("source").notNull(),
pluginVersion: text("plugin_version"),
activatedAt: text("activated_at").notNull(),
}, (t) => [
primaryKey({ columns: [t.projectId, t.id] }),
index("idxPluginActivationsActivatedAt").on(t.activatedAt),
index("idxPluginActivationsPluginId").on(t.pluginId),
]);
@@ -2178,7 +2193,8 @@ export const cliSessions = projectSchema.table("cli_sessions", {
]);
export const chatMessages = projectSchema.table("chat_messages", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
sessionId: text("session_id").notNull(),
role: text("role").notNull(),
content: text("content").notNull(),
@@ -2187,6 +2203,7 @@ export const chatMessages = projectSchema.table("chat_messages", {
createdAt: text("created_at").notNull(),
attachments: jsonb("attachments"),
}, (t) => [
primaryKey({ columns: [t.projectId, t.id] }),
index("idxChatMessagesSessionId").on(t.sessionId),
index("idxChatMessagesCreatedAt").on(t.createdAt),
]);
@@ -2218,7 +2235,8 @@ export const chatTokenUsage = projectSchema.table("chat_token_usage", {
]);
export const runAuditEvents = projectSchema.table("run_audit_events", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
timestamp: text("timestamp").notNull(),
taskId: text("task_id"),
agentId: text("agent_id").notNull(),
@@ -2228,6 +2246,7 @@ export const runAuditEvents = projectSchema.table("run_audit_events", {
target: text("target").notNull(),
metadata: jsonb("metadata"),
}, (t) => [
primaryKey({ columns: [t.projectId, t.id] }),
index("idxRunAuditEventsRunIdTimestamp").on(t.runId, t.timestamp),
index("idxRunAuditEventsTaskIdTimestamp").on(t.taskId, t.timestamp),
index("idxRunAuditEventsTimestamp").on(t.timestamp),
@@ -2340,13 +2359,14 @@ export const missionLineageStops = projectSchema.table("mission_lineage_stops",
]);
export const verificationCache = projectSchema.table("verification_cache", {
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
treeSha: text("tree_sha").notNull(),
testCommand: text("test_command").notNull().default(""),
buildCommand: text("build_command").notNull().default(""),
recordedAt: text("recorded_at").notNull(),
taskId: text("task_id"),
}, (t) => [
primaryKey({ columns: [t.treeSha, t.testCommand, t.buildCommand] }),
primaryKey({ columns: [t.projectId, t.treeSha, t.testCommand, t.buildCommand] }),
index("idxVerificationCacheRecordedAt").on(t.recordedAt),
]);
@@ -2387,7 +2407,8 @@ export const importTranslationCache = projectSchema.table("import_translation_ca
]);
export const approvalRequests = projectSchema.table("approval_requests", {
id: text("id").primaryKey(),
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
id: text("id").notNull(),
status: text("status").notNull(),
requesterActorId: text("requester_actor_id").notNull(),
requesterActorType: text("requester_actor_type").notNull(),
@@ -2406,6 +2427,7 @@ export const approvalRequests = projectSchema.table("approval_requests", {
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
}, (t) => [
primaryKey({ columns: [t.projectId, t.id] }),
index("idxApprovalRequestsStatusCreatedAt").on(t.status, t.createdAt),
index("idxApprovalRequestsRequesterCreatedAt").on(t.requesterActorId, t.createdAt),
index("idxApprovalRequestsTaskCreatedAt").on(t.taskId, t.createdAt),