Make device row moves atomic
This commit is contained in:
@@ -47,15 +47,49 @@ function createTestDatabase(): DatabaseContext {
|
||||
return context;
|
||||
}
|
||||
|
||||
function insertDeviceRow(context: DatabaseContext) {
|
||||
function insertCircuit(
|
||||
context: DatabaseContext,
|
||||
input: {
|
||||
id: string;
|
||||
equipmentIdentifier: string;
|
||||
sortOrder: number;
|
||||
isReserve?: number;
|
||||
}
|
||||
) {
|
||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
||||
context.db
|
||||
.insert(circuits)
|
||||
.values({
|
||||
id: input.id,
|
||||
circuitListId: seedCircuit.circuitListId,
|
||||
sectionId: seedCircuit.sectionId,
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.equipmentIdentifier,
|
||||
sortOrder: input.sortOrder,
|
||||
isReserve: input.isReserve ?? 1,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
function insertDeviceRow(
|
||||
context: DatabaseContext,
|
||||
input: {
|
||||
id?: string;
|
||||
circuitId?: string;
|
||||
sortOrder?: number;
|
||||
displayName?: string;
|
||||
} = {}
|
||||
) {
|
||||
const rowId = input.id ?? "row-1";
|
||||
const circuitId = input.circuitId ?? "circuit-1";
|
||||
context.db
|
||||
.insert(circuitDeviceRows)
|
||||
.values({
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
sortOrder: 10,
|
||||
id: rowId,
|
||||
circuitId,
|
||||
sortOrder: input.sortOrder ?? 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
displayName: input.displayName ?? "Leuchte",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
@@ -64,7 +98,7 @@ function insertDeviceRow(context: DatabaseContext) {
|
||||
context.db
|
||||
.update(circuits)
|
||||
.set({ isReserve: 0 })
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.where(eq(circuits.id, circuitId))
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -283,4 +317,213 @@ describe("circuit device-row transaction repository", () => {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("moves rows to an existing circuit and updates both reserve states together", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertCircuit(context, {
|
||||
id: "circuit-2",
|
||||
equipmentIdentifier: "-1F2",
|
||||
sortOrder: 20,
|
||||
});
|
||||
insertCircuit(context, {
|
||||
id: "circuit-3",
|
||||
equipmentIdentifier: "-1F3",
|
||||
sortOrder: 30,
|
||||
});
|
||||
insertDeviceRow(context);
|
||||
insertDeviceRow(context, {
|
||||
id: "row-2",
|
||||
circuitId: "circuit-2",
|
||||
sortOrder: 10,
|
||||
displayName: "Zweite Quellzeile",
|
||||
});
|
||||
insertDeviceRow(context, {
|
||||
id: "target-row",
|
||||
circuitId: "circuit-3",
|
||||
sortOrder: 10,
|
||||
displayName: "Bestehende Zielzeile",
|
||||
});
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
const result = repository.moveRows({
|
||||
rows: [
|
||||
{ id: "row-1", expectedCircuitId: "circuit-1" },
|
||||
{ id: "row-2", expectedCircuitId: "circuit-2" },
|
||||
],
|
||||
targetCircuitId: "circuit-3",
|
||||
});
|
||||
|
||||
const movedRows = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.all()
|
||||
.filter((row) => row.id === "row-1" || row.id === "row-2")
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
const targetCircuit = persistedCircuits.find(
|
||||
(circuit) => circuit.id === "circuit-3"
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
movedRowIds: ["row-1", "row-2"],
|
||||
targetCircuitId: "circuit-3",
|
||||
createdCircuitId: undefined,
|
||||
});
|
||||
assert.deepEqual(
|
||||
movedRows.map((row) => [row.id, row.circuitId, row.sortOrder]),
|
||||
[
|
||||
["row-1", "circuit-3", 20],
|
||||
["row-2", "circuit-3", 30],
|
||||
]
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
||||
1
|
||||
);
|
||||
assert.equal(targetCircuit?.isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back an existing-target move when a reserve update fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertCircuit(context, {
|
||||
id: "circuit-2",
|
||||
equipmentIdentifier: "-1F2",
|
||||
sortOrder: 20,
|
||||
});
|
||||
insertDeviceRow(context);
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_source_reserve_update
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced source reserve failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
targetCircuitId: "circuit-2",
|
||||
}),
|
||||
/forced source reserve failure/
|
||||
);
|
||||
|
||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
assert.equal(row.circuitId, "circuit-1");
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
||||
1
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates a new target circuit and moves rows into it atomically", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
const [sourceCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
const result = repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
createTargetCircuit: {
|
||||
circuitListId: sourceCircuit.circuitListId,
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Neues Ziel",
|
||||
sortOrder: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const [movedRow] = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, "row-1"))
|
||||
.all();
|
||||
const [createdCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, result.createdCircuitId!))
|
||||
.all();
|
||||
const [updatedSource] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
|
||||
assert.equal(result.targetCircuitId, result.createdCircuitId);
|
||||
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
|
||||
assert.equal(createdCircuit.isReserve, 0);
|
||||
assert.equal(movedRow.circuitId, createdCircuit.id);
|
||||
assert.equal(updatedSource.isReserve, 1);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back a newly created target circuit when the move fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
const [sourceCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_new_target_source_reserve
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced new-target move failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
createTargetCircuit: {
|
||||
circuitListId: sourceCircuit.circuitListId,
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Rollback-Ziel",
|
||||
sortOrder: 20,
|
||||
},
|
||||
}),
|
||||
/forced new-target move failure/
|
||||
);
|
||||
|
||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
assert.equal(row.circuitId, "circuit-1");
|
||||
assert.equal(persistedCircuits.length, 1);
|
||||
assert.equal(persistedCircuits[0].isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user