mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-04-19 13:33:34 -04:00
248 lines
6.8 KiB
TypeScript
248 lines
6.8 KiB
TypeScript
/**
|
|
* PCD conflict test helpers — create database instances, insert ops, query state.
|
|
*/
|
|
|
|
import { Database } from '@db/sqlite';
|
|
import { log } from '$test-harness/log.ts';
|
|
|
|
// ─── Database Instance ─────────────────────────────────────────────────────────
|
|
|
|
export interface CreateDatabaseInstanceOpts {
|
|
name?: string;
|
|
uuid: string;
|
|
localPath: string;
|
|
conflictStrategy?: 'override' | 'align' | 'ask';
|
|
enabled?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Insert a database_instances row directly. Also marks default_database_linked
|
|
* so the auto-link hook won't fire on future startups.
|
|
*/
|
|
export function createDatabaseInstance(dbPath: string, opts: CreateDatabaseInstanceOpts): number {
|
|
log.setup(`Creating database instance "${opts.name ?? 'test-db'}"`);
|
|
const db = new Database(dbPath);
|
|
try {
|
|
db.exec(
|
|
`INSERT INTO database_instances (
|
|
uuid, name, repository_url, local_path,
|
|
sync_strategy, auto_pull, enabled, is_private,
|
|
local_ops_enabled, conflict_strategy
|
|
) VALUES (?, ?, '', ?, 60, 0, ?, 0, 1, ?)`,
|
|
[
|
|
opts.uuid,
|
|
opts.name ?? 'test-db',
|
|
opts.localPath,
|
|
opts.enabled !== false ? 1 : 0,
|
|
opts.conflictStrategy ?? 'ask'
|
|
]
|
|
);
|
|
const row = db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
|
|
const id = row.id;
|
|
|
|
// Prevent auto-link from firing
|
|
db.exec('UPDATE setup_state SET default_database_linked = 1 WHERE id = 1');
|
|
|
|
log.setup(`Database instance created with id=${id}`);
|
|
return id;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
// ─── PCD Ops ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface InsertOpOpts {
|
|
databaseId: number;
|
|
origin: 'base' | 'user';
|
|
state?: 'published' | 'draft' | 'superseded' | 'dropped';
|
|
source?: 'local' | 'repo' | 'import';
|
|
sql: string;
|
|
sequence?: number;
|
|
metadata?: string | null;
|
|
desiredState?: string | null;
|
|
contentHash?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Insert a pcd_ops row directly. Returns the inserted op id.
|
|
*/
|
|
export function insertOp(dbPath: string, opts: InsertOpOpts): number {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
db.exec(
|
|
`INSERT INTO pcd_ops (
|
|
database_id, origin, state, source,
|
|
sql, sequence, metadata, desired_state, content_hash
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
opts.databaseId,
|
|
opts.origin,
|
|
opts.state ?? 'published',
|
|
opts.source ?? 'local',
|
|
opts.sql,
|
|
opts.sequence ?? null,
|
|
opts.metadata ?? null,
|
|
opts.desiredState ?? null,
|
|
opts.contentHash ?? null
|
|
]
|
|
);
|
|
const row = db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
|
|
return row.id;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
// ─── PCD Repo on Disk ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Create a minimal PCD repo on disk with just the schema SQL.
|
|
* This is all that `loadAllOperations` needs from disk — base/user ops come from DB.
|
|
*/
|
|
export async function createPcdRepo(basePath: string, uuid: string): Promise<string> {
|
|
const pcdPath = `${basePath}/data/databases/${uuid}`;
|
|
const schemaDir = `${pcdPath}/deps/schema/ops`;
|
|
await Deno.mkdir(schemaDir, { recursive: true });
|
|
|
|
// Copy PCD schema from docs
|
|
const schema = await Deno.readTextFile('docs/backend/0.schema.sql');
|
|
await Deno.writeTextFile(`${schemaDir}/0.schema.sql`, schema);
|
|
|
|
log.setup(`PCD repo created at ${pcdPath}`);
|
|
return pcdPath;
|
|
}
|
|
|
|
// ─── Query Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
export interface OpRow {
|
|
id: number;
|
|
database_id: number;
|
|
origin: string;
|
|
state: string;
|
|
source: string;
|
|
sql: string;
|
|
sequence: number | null;
|
|
metadata: string | null;
|
|
desired_state: string | null;
|
|
content_hash: string | null;
|
|
superseded_by_op_id: number | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface HistoryRow {
|
|
id: number;
|
|
op_id: number;
|
|
database_id: number;
|
|
batch_id: string;
|
|
status: string;
|
|
rowcount: number | null;
|
|
conflict_reason: string | null;
|
|
error: string | null;
|
|
details: string | null;
|
|
applied_at: string;
|
|
}
|
|
|
|
/**
|
|
* Get a single pcd_ops row by id.
|
|
*/
|
|
export function queryOp(dbPath: string, opId: number): OpRow | undefined {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
return db.prepare('SELECT * FROM pcd_ops WHERE id = ?').get(opId) as OpRow | undefined;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all pcd_ops rows for a database, optionally filtered.
|
|
*/
|
|
export function queryOpsByDatabase(
|
|
dbPath: string,
|
|
databaseId: number,
|
|
filters?: { origin?: string; state?: string }
|
|
): OpRow[] {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
const clauses = ['database_id = ?'];
|
|
const params: (string | number)[] = [databaseId];
|
|
if (filters?.origin) {
|
|
clauses.push('origin = ?');
|
|
params.push(filters.origin);
|
|
}
|
|
if (filters?.state) {
|
|
clauses.push('state = ?');
|
|
params.push(filters.state);
|
|
}
|
|
return db
|
|
.prepare(`SELECT * FROM pcd_ops WHERE ${clauses.join(' AND ')} ORDER BY id`)
|
|
.all(...params) as OpRow[];
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the latest pcd_op_history entry for each op in a database,
|
|
* filtered to conflicted/conflicted_pending statuses.
|
|
*/
|
|
export function queryLatestConflicts(dbPath: string, databaseId: number): HistoryRow[] {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
return db
|
|
.prepare(
|
|
`SELECT h.* FROM pcd_op_history h
|
|
INNER JOIN (
|
|
SELECT op_id, MAX(id) as max_id
|
|
FROM pcd_op_history
|
|
WHERE database_id = ?
|
|
GROUP BY op_id
|
|
) latest ON h.id = latest.max_id
|
|
WHERE h.status IN ('conflicted', 'conflicted_pending')
|
|
ORDER BY h.id`
|
|
)
|
|
.all(databaseId) as HistoryRow[];
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all pcd_op_history entries for a specific op.
|
|
*/
|
|
export function queryAllHistory(dbPath: string, opId: number): HistoryRow[] {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
return db
|
|
.prepare('SELECT * FROM pcd_op_history WHERE op_id = ? ORDER BY id DESC')
|
|
.all(opId) as HistoryRow[];
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the latest pcd_op_history entry for each op in a database (all statuses).
|
|
*/
|
|
export function queryLatestHistory(dbPath: string, databaseId: number): HistoryRow[] {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
return db
|
|
.prepare(
|
|
`SELECT h.* FROM pcd_op_history h
|
|
INNER JOIN (
|
|
SELECT op_id, MAX(id) as max_id
|
|
FROM pcd_op_history
|
|
WHERE database_id = ?
|
|
GROUP BY op_id
|
|
) latest ON h.id = latest.max_id
|
|
ORDER BY h.id`
|
|
)
|
|
.all(databaseId) as HistoryRow[];
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|