/** * Shared database helper functions for SQLite store modules. * * Consolidates upsert and dependency patterns used across * sqlite-data-accessor.ts, tasks-sqlite.ts, and session-store.ts. * * @epic T4454 */ import type { ArchiveReasonValue, Session, Task, TaskInsertIdentity } from '@cleocode/contracts'; import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import { CleoError } from '../errors.js'; import type { NewTaskRow } from './tasks-schema.js'; /** Drizzle database instance type. */ type DrizzleDb = NodeSQLiteDatabase; /** Archive-specific fields for task upsert. */ export interface ArchiveFields { archivedAt?: string; /** * T11578 · AC1: typed to the canonical {@link ArchiveReasonValue} enum so * writes conform to the consolidated `tasks_tasks.archive_reason` CHECK * constraint (the bare legacy `tasks` table had no CHECK, masking * out-of-enum values such as the historical `'completed'` literal). */ archiveReason?: ArchiveReasonValue; cycleTimeDays?: number | null; } /** * Upsert a single task row into the tasks table. * Handles both active task upsert and archived task upsert via optional archiveFields. * * When `allowOrphanParent` is true (bulk/migration mode, T5034): silently nulls out * parentId if the referenced parent does not exist, preventing FK violations. * When false (normal single-task writes, default): logs a warning but still proceeds * so that FK enforcement at the DB level provides the final safety net. * * Callers that perform bulk imports or archive restoration should pass * `allowOrphanParent: true` to enable the lenient behavior. */ export declare function upsertTask(db: DrizzleDb, row: NewTaskRow, archiveFields?: ArchiveFields, allowOrphanParent?: boolean): Promise; /** * Insert a NEW task row. Never overwrites: when a row with the same id is * already stored, it throws `ExitCode.ID_COLLISION` and writes nothing. * * Every path that creates a task under a freshly allocated or computed id * (`cleo add` and `add-batch`, the task imports, snapshot restore of a * missing task) uses this instead of {@link upsertTask}. An id collision (an * older build, or any writer that took the same id between allocation and * insert) must fail loudly, never silently replace a different task. The * existence check gives the typed error; the plain INSERT (no ON CONFLICT) * is the backstop, since the primary key rejects a duplicate the check missed. * * @param db - The tasks Drizzle handle, inside the caller's transaction. * @param row - The new task row. * @throws CleoError `ID_COLLISION` when `row.id` is already stored. * @task T12724 */ export declare function insertNewTask(db: DrizzleDb, row: NewTaskRow, identity?: TaskInsertIdentity): Promise; /** * An import or restore named a row this store already holds: same uid AND * same birth fingerprint (possibly under another display id after a re-mint, * re-keyed, or held in the identity quarantine). Writing it again would * duplicate the work, so the caller skips it and reports the conflict * (T12806 review). */ export declare class SameRowPresentError extends CleoError { /** Where the row already is: its display id, or `held` when quarantined. */ readonly presentAs: string; constructor(taskId: string, presentAs: string); } /** * Parse a `labels_json` text column into a deduplicated string-array. * * Invalid / non-array JSON yields an empty list — the junction is then emptied * for that task, matching the "no labels" state. * * @param labelsJson - The serialized JSON array from `tasks.labels_json`. * @returns Deduplicated, non-empty label strings. */ export declare function parseLabels(labelsJson: string | null | undefined): string[]; /** * Make the {@link schema.taskLabels} junction rows for one task exactly mirror * its label set (T11356). * * A diff write (T12341): only labels that left the set are deleted and only * new ones inserted, so a save that keeps the set writes nothing. Deleting and * re-inserting a kept label would replicate as a tombstone plus a re-insert of * the same row uid, which lets a stale save resurrect a label another device * removed. Callers run inside the task transaction, so the read and the writes * see one state. Called from {@link upsertTask} and from raw-SQL proposal * inserters that bypass it. * * @param db - Drizzle tasks.db handle. * @param taskId - The owning task id. * @param labels - The full label set the junction should reflect. */ export declare function updateTaskLabels(db: DrizzleDb, taskId: string, labels: string[]): Promise; /** * Upsert a single session row into the sessions table. */ export declare function upsertSession(db: DrizzleDb, session: Session): Promise; /** * Append-able session id-list / notes columns (T11357). * * These three columns are JSON arrays that grow on session events. The * append-in-SQL helper below targets exactly this set. */ export type AppendableSessionColumn = 'notesJson' | 'tasksCompletedJson' | 'tasksCreatedJson'; /** * Append one element to a session's JSON-array column **in SQL** via * `json_insert(col, '$[#]', ?)` — no app-side read-modify-write of the whole * array (T11357 · AC4). * * ## Why `json_insert` (TEXT) and not `jsonb_insert` (BLOB) * * `sessions.{notes,tasks_completed,tasks_created}_json` are read WHOLE by * `rowToSession` (`safeParseJsonArray(row.notesJson)`) and by backup/export * paths. Storing them as a JSONB BLOB would force every one of those readers * onto `json(col)` and break the plain-column reads. `json_insert` performs the * same `$[#]` end-of-array append the audit calls for while keeping the column * canonical TEXT, so existing whole-value readers stay correct. The `$[#]` * path is the SQLite idiom for "append to the end of the array". * * The column is coalesced to `'[]'` first so an append onto a NULL/empty column * yields a single-element array rather than NULL. * * @param db - Drizzle sessions.db handle (tasks.db schema). * @param sessionId - Target session id. * @param column - Which appendable array column to grow. * @param value - The string element to append. */ export declare function appendSessionListItem(db: DrizzleDb, sessionId: string, column: AppendableSessionColumn, value: string): Promise; /** * Make a task's dependency rows equal `depends` (optionally filtered by a set * of valid IDs) with a diff write (T12341): only removed edges are deleted and * only new ones inserted, so an unchanged set writes nothing and a kept edge is * never deleted and re-inserted (see {@link updateTaskLabels}). */ export declare function updateDependencies(db: DrizzleDb, taskId: string, depends: string[], validIds?: Set): Promise; /** * Batch-update dependencies for multiple tasks with one read and bulk writes. * A diff write (T12341): one SELECT of the current edges of these tasks, then * one DELETE per task for the edges that left its set, and one INSERT for all * new edges. Edges kept in the set are never touched. * * Callers are responsible for wrapping this in a transaction if needed. */ export declare function batchUpdateDependencies(db: DrizzleDb, tasks: Array<{ taskId: string; deps: string[]; }>, validIds?: Set): Promise; /** * Batch-load persisted hard dependencies for selected tasks in-place. * * @remarks * Missing targets remain explicit dependency identifiers so readiness and * integrity checks can diagnose them. A caller's known population cannot prove * an omitted target is satisfied. This reader does not repair or delete edges; * database failures propagate to the caller. Soft relations are read separately. * * @param db - Canonical project database handle. * @param tasks - Selected task records to enrich with their stored hard edges. * @param _validationIds - Legacy argument retained for compatibility; no longer filters evidence. * @returns Resolves after dependencies have been read successfully. * * @example * ```ts * await loadDependenciesForTasks(db, selectedTasks); * // A missing target remains in depends for explicit readiness diagnostics. * ``` */ export declare function loadDependenciesForTasks(db: DrizzleDb, tasks: Task[], _validationIds?: Set): Promise; /** * Batch-load relations for a list of tasks and apply them in-place. * Mirrors loadDependenciesForTasks pattern for task_relations table (T5168). */ export declare function loadRelationsForTasks(db: DrizzleDb, tasks: Task[]): Promise; export {}; //# sourceMappingURL=db-helpers.d.ts.map