Archive every deletion via a bulk SQL retirement
`retireDeletedArtifacts` used to iterate `deleted.slice(0, MAX_BASH_PROJECTED_FILES)` and emit no signal for paths past the cap — a large `rm -r` would leave the 51st row onward visible in the DocumentCenter with no indication anything was missed. The cap was inherited from the registration path, where it bounds file-stat round-trips; retirement is a pure metadata update with no filesystem cost, so the cap was value-free safety theatre. New DB surface `archiveWorkProductsByExternalPaths` runs a single `UPDATE … SET status = 'archived' … WHERE external_id IN (…) AND status <> 'archived' RETURNING id, external_id` per 500-path chunk — one round-trip per chunk instead of one per path. The projector feeds the full `deleted` list in, maps the returned ids back to `retired` outcomes, and no deletion is left behind regardless of the run's cleanup fan-out size.
This commit is contained in:
1 parent
f0c4a66cc1
commit
fe808d13a2
3 files changed
+78
-18
No files matched your search
@@ -312,6 +312,10 @@ function projectChangedPaths(
|
||||
* path. `last_touched_by_run_id` carries the retiring run so the
|
||||
* provenance timeline records the deletion alongside the earlier
|
||||
* edits.
|
||||
*
|
||||
* Every detected deletion is processed — retirement is a bulk
|
||||
* `UPDATE … IN (…)` chunked by the DB layer, so a large `rm -r`
|
||||
* retires the full tail instead of leaving the overflow visible.
|
||||
*/
|
||||
function retireDeletedArtifacts(
|
||||
db: AppDatabase,
|
||||
@@ -319,24 +323,18 @@ function retireDeletedArtifacts(
|
||||
deleted: string[],
|
||||
): ProjectionOutcome[] {
|
||||
if (deleted.length === 0) return [];
|
||||
const outcomes: ProjectionOutcome[] = [];
|
||||
const budget = deleted.slice(0, MAX_BASH_PROJECTED_FILES);
|
||||
for (const rel of budget) {
|
||||
const existing = db.findWorkProductByExternalId(
|
||||
ctx.companyId,
|
||||
"filesystem",
|
||||
rel,
|
||||
ctx.workspaceId,
|
||||
);
|
||||
if (!existing) continue;
|
||||
db.updateWorkProduct(existing.id, ctx.companyId, {
|
||||
status: "archived",
|
||||
lastTouchedByRunId: ctx.runId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
});
|
||||
outcomes.push({ kind: "retired", workProductId: existing.id, relativePath: rel });
|
||||
}
|
||||
return outcomes;
|
||||
const archived = db.archiveWorkProductsByExternalPaths({
|
||||
companyId: ctx.companyId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
provider: "filesystem",
|
||||
paths: deleted,
|
||||
lastTouchedByRunId: ctx.runId,
|
||||
});
|
||||
return archived.map(({ id, externalId }) => ({
|
||||
kind: "retired",
|
||||
workProductId: id,
|
||||
relativePath: externalId,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,9 @@ export class AppDatabase extends AppDatabaseBase {
|
||||
findWorkProductByExternalId(companyId: string, provider: string, externalId: string, workspaceId: string | null) { return this.documentsMod.findWorkProductByExternalId(companyId, provider, externalId, workspaceId); }
|
||||
resolveWorkProductTarget(workProductId: string, companyId: string) { return this.documentsMod.resolveWorkProductTarget(workProductId, companyId); }
|
||||
deleteWorkProduct(id: string, companyId: string) { return this.documentsMod.deleteWorkProduct(id, companyId); }
|
||||
archiveWorkProductsByExternalPaths(input: Parameters<ReturnType<typeof import("./database/documents").buildDocumentMethods>["archiveWorkProductsByExternalPaths"]>[0]) {
|
||||
return this.documentsMod.archiveWorkProductsByExternalPaths(input);
|
||||
}
|
||||
linkDocumentToTask(taskId: string, documentId: string, companyId: string, key: string) { return this.documentsMod.linkDocumentToTask(taskId, documentId, companyId, key); }
|
||||
unlinkDocumentFromTask(taskId: string, key: string, companyId: string) { this.documentsMod.unlinkDocumentFromTask(taskId, key, companyId); }
|
||||
listDocumentsForTask(taskId: string, companyId: string) { return this.documentsMod.listDocumentsForTask(taskId, companyId); }
|
||||
|
||||
@@ -451,6 +451,65 @@ export function buildDocumentMethods(
|
||||
return Number(result.changes ?? 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive every `work_products` row whose natural identity matches
|
||||
* one of `paths` under the given `(companyId, workspaceId,
|
||||
* provider)` scope. The change detectors walk the whole workspace
|
||||
* delta, so every detected deletion has to be retired — previously
|
||||
* the projector capped the retire list at a hardcoded budget and
|
||||
* the tail stayed visible in the DocumentCenter.
|
||||
*
|
||||
* Runs as a single `UPDATE … WHERE external_id IN (…)` per chunk
|
||||
* so bulk deletions cost one SQL round-trip per 500 paths instead
|
||||
* of one per path. `RETURNING` carries the archived row ids back
|
||||
* to the projector so it can emit `retired` outcomes without a
|
||||
* follow-up SELECT.
|
||||
*/
|
||||
archiveWorkProductsByExternalPaths(input: {
|
||||
companyId: string;
|
||||
workspaceId: string | null;
|
||||
provider: string;
|
||||
paths: string[];
|
||||
lastTouchedByRunId: string | null;
|
||||
}): Array<{ id: string; externalId: string }> {
|
||||
if (input.paths.length === 0) return [];
|
||||
const ts = nowIso();
|
||||
const CHUNK = 500;
|
||||
const workspaceClause = input.workspaceId
|
||||
? "AND workspace_id = ?"
|
||||
: "AND workspace_id IS NULL";
|
||||
const results: Array<{ id: string; externalId: string }> = [];
|
||||
for (let i = 0; i < input.paths.length; i += CHUNK) {
|
||||
const chunk = input.paths.slice(i, i + CHUNK);
|
||||
const placeholders = chunk.map(() => "?").join(", ");
|
||||
const params: SQLInputValue[] = [
|
||||
input.lastTouchedByRunId ?? null,
|
||||
ts,
|
||||
input.companyId,
|
||||
input.provider,
|
||||
];
|
||||
if (input.workspaceId) params.push(input.workspaceId);
|
||||
params.push(...chunk);
|
||||
const rows = readAll<{ id: string; external_id: string }>(
|
||||
`UPDATE work_products
|
||||
SET status = 'archived',
|
||||
last_touched_by_run_id = ?,
|
||||
updated_at = ?
|
||||
WHERE company_id = ?
|
||||
AND provider = ?
|
||||
${workspaceClause}
|
||||
AND external_id IN (${placeholders})
|
||||
AND status <> 'archived'
|
||||
RETURNING id, external_id`,
|
||||
...params,
|
||||
);
|
||||
for (const row of rows) {
|
||||
results.push({ id: row.id, externalId: row.external_id });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
// ── Task-Document Links ──
|
||||
|
||||
linkDocumentToTask(taskId: string, documentId: string, companyId: string, key: string): string {
|
||||
|
||||
Reference in new issue
Block a user