Skip to content

Commit 3eb1366

Browse files
Refactor updateCell to use updateCellBatch in SQLite engine
Refactored `updateCell` in `src/core/sqlite-db.ts` to delegate to `updateCellBatch`, eliminating duplicated logic for cell updates and JSON patching. Also updated `undoModification` and `redoModification` to use `updateCellBatch` for batch operations, removing explicit transaction loops and preventing nested transaction errors. Enhanced `updateCellBatch` with row ID validation and improved JSON object detection. Added unit tests in `tests/unit/cell_update_refactor.test.ts`. Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com>
1 parent e9c4c24 commit 3eb1366

2 files changed

Lines changed: 163 additions & 57 deletions

File tree

src/core/sqlite-db.ts

Lines changed: 26 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,12 @@ class WasmDatabaseEngine implements DatabaseOperations {
122122
case 'cell_update':
123123
if (affectedCells) {
124124
// Batch undo
125-
await this.executeQuery('BEGIN TRANSACTION');
126-
try {
127-
for (const cell of affectedCells) {
128-
await this.updateCell(targetTable, cell.rowId, cell.columnName, cell.priorValue ?? null);
129-
}
130-
await this.executeQuery('COMMIT');
131-
} catch (e) {
132-
await this.executeQuery('ROLLBACK');
133-
throw e;
134-
}
125+
const updates: CellUpdate[] = affectedCells.map(cell => ({
126+
rowId: cell.rowId,
127+
column: cell.columnName,
128+
value: cell.priorValue ?? null
129+
}));
130+
await this.updateCellBatch(targetTable, updates);
135131
} else if (targetRowId !== undefined && targetColumn) {
136132
// Single cell undo
137133
await this.updateCell(targetTable, targetRowId, targetColumn, priorValue ?? null);
@@ -214,16 +210,12 @@ class WasmDatabaseEngine implements DatabaseOperations {
214210
case 'cell_update':
215211
if (affectedCells) {
216212
// Batch redo
217-
await this.executeQuery('BEGIN TRANSACTION');
218-
try {
219-
for (const cell of affectedCells) {
220-
await this.updateCell(targetTable, cell.rowId, cell.columnName, cell.newValue ?? null);
221-
}
222-
await this.executeQuery('COMMIT');
223-
} catch (e) {
224-
await this.executeQuery('ROLLBACK');
225-
throw e;
226-
}
213+
const updates: CellUpdate[] = affectedCells.map(cell => ({
214+
rowId: cell.rowId,
215+
column: cell.columnName,
216+
value: cell.newValue ?? null
217+
}));
218+
await this.updateCellBatch(targetTable, updates);
227219
} else if (targetRowId !== undefined && targetColumn) {
228220
await this.updateCell(targetTable, targetRowId, targetColumn, newValue ?? null);
229221
}
@@ -297,43 +289,13 @@ class WasmDatabaseEngine implements DatabaseOperations {
297289
* Update a single cell value.
298290
*/
299291
async updateCell(table: string, rowId: RecordId, column: string, value: CellValue, patch?: string): Promise<void> {
300-
// Validate rowId is a number
301-
const rowIdNum = Number(rowId);
302-
if (!Number.isFinite(rowIdNum)) {
303-
throw new Error(`Invalid rowid: ${rowId}`);
304-
}
305-
306-
let sql: string;
307-
let params: CellValue[];
308-
309-
if (patch) {
310-
// Fallback to JS implementation of json_patch
311-
// Fetch current value
312-
const currentResult = await this.executeQuery(`SELECT ${escapeIdentifier(column)} FROM ${escapeIdentifier(table)} WHERE rowid = ?`, [rowIdNum]);
313-
let currentValue = currentResult[0]?.rows[0]?.[0];
314-
315-
// Parse current JSON
316-
let currentObj = {};
317-
if (typeof currentValue === 'string') {
318-
try { currentObj = JSON.parse(currentValue); } catch {}
319-
} else if (typeof currentValue === 'object' && currentValue !== null && !(currentValue instanceof Uint8Array)) {
320-
// Already an object? (unlikely from SQLite unless using some extension, usually string)
321-
currentObj = currentValue;
322-
}
323-
324-
// Apply patch
325-
const patchObj = typeof patch === 'string' ? JSON.parse(patch) : patch;
326-
const newValueObj = applyMergePatch(currentObj, patchObj);
327-
const newValueStr = JSON.stringify(newValueObj);
328-
329-
sql = `UPDATE ${escapeIdentifier(table)} SET ${escapeIdentifier(column)} = ? WHERE rowid = ?`;
330-
params = [newValueStr, rowIdNum];
331-
} else {
332-
sql = `UPDATE ${escapeIdentifier(table)} SET ${escapeIdentifier(column)} = ? WHERE rowid = ?`;
333-
params = [value, rowIdNum];
334-
}
335-
336-
await this.executeQuery(sql, params);
292+
const update: CellUpdate = {
293+
rowId,
294+
column,
295+
value: patch !== undefined ? patch : value,
296+
operation: patch !== undefined ? 'json_patch' : 'set'
297+
};
298+
return this.updateCellBatch(table, [update]);
337299
}
338300

339301
/**
@@ -460,6 +422,10 @@ class WasmDatabaseEngine implements DatabaseOperations {
460422
for (const update of columnUpdates) {
461423
const rowIdNum = Number(update.rowId);
462424

425+
if (!Number.isFinite(rowIdNum)) {
426+
throw new Error(`Invalid rowid: ${update.rowId}`);
427+
}
428+
463429
if (op === 'json_patch') {
464430
// Read using prepared statement
465431
// selectStmt.get([rowIdNum]) returns [val] or undefined
@@ -479,6 +445,9 @@ class WasmDatabaseEngine implements DatabaseOperations {
479445
let currentObj = {};
480446
if (typeof currentValue === 'string') {
481447
try { currentObj = JSON.parse(currentValue); } catch {}
448+
} else if (typeof currentValue === 'object' && currentValue !== null && !(currentValue instanceof Uint8Array)) {
449+
// Already an object? (unlikely from SQLite unless using some extension, usually string)
450+
currentObj = currentValue;
482451
}
483452

484453
const patchObj = typeof update.value === 'string' ? JSON.parse(update.value as string) : update.value;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
2+
import { describe, it, before, after } from 'node:test';
3+
import assert from 'node:assert';
4+
import { createDatabaseEngine } from '../../src/core/sqlite-db';
5+
import { CellUpdate, ModificationEntry } from '../../src/core/types';
6+
7+
describe('SQLite Engine Cell Update Refactoring', () => {
8+
let engine: any;
9+
10+
before(async () => {
11+
// Initialize with empty DB
12+
const result = await createDatabaseEngine({
13+
content: null,
14+
maxSize: 0,
15+
readOnlyMode: false
16+
});
17+
engine = result.operations;
18+
19+
// Setup table
20+
await engine.executeQuery("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, data TEXT)");
21+
await engine.insertRow('users', { id: 1, name: 'Alice', data: '{"score": 10}' });
22+
await engine.insertRow('users', { id: 2, name: 'Bob', data: '{"score": 20}' });
23+
});
24+
25+
it('should update single cell', async () => {
26+
await engine.updateCell('users', 1, 'name', 'AliceUpdated');
27+
const result = await engine.executeQuery("SELECT name FROM users WHERE id = 1");
28+
assert.strictEqual(result[0].rows[0][0], 'AliceUpdated');
29+
});
30+
31+
it('should update single cell with JSON patch', async () => {
32+
// Apply patch to data column
33+
await engine.updateCell('users', 1, 'data', null, '{"score": 15}');
34+
const result = await engine.executeQuery("SELECT data FROM users WHERE id = 1");
35+
const data = JSON.parse(result[0].rows[0][0] as string);
36+
assert.strictEqual(data.score, 15);
37+
});
38+
39+
it('should update multiple cells in batch', async () => {
40+
const updates: CellUpdate[] = [
41+
{ rowId: 1, column: 'name', value: 'AliceBatch' },
42+
{ rowId: 2, column: 'name', value: 'BobBatch' }
43+
];
44+
await engine.updateCellBatch('users', updates);
45+
46+
const result = await engine.executeQuery("SELECT name FROM users ORDER BY id");
47+
assert.strictEqual(result[0].rows[0][0], 'AliceBatch');
48+
assert.strictEqual(result[0].rows[1][0], 'BobBatch');
49+
});
50+
51+
it('should update multiple cells with JSON patch in batch', async () => {
52+
const updates: CellUpdate[] = [
53+
{ rowId: 1, column: 'data', value: '{"score": 25}', operation: 'json_patch' },
54+
{ rowId: 2, column: 'data', value: '{"score": 30}', operation: 'json_patch' }
55+
];
56+
await engine.updateCellBatch('users', updates);
57+
58+
const result = await engine.executeQuery("SELECT data FROM users ORDER BY id");
59+
const data1 = JSON.parse(result[0].rows[0][0] as string);
60+
const data2 = JSON.parse(result[0].rows[1][0] as string);
61+
assert.strictEqual(data1.score, 25);
62+
assert.strictEqual(data2.score, 30);
63+
});
64+
65+
it('should undo batch cell update', async () => {
66+
// Setup initial state
67+
await engine.updateCell('users', 1, 'name', 'AlicePreUndo');
68+
await engine.updateCell('users', 2, 'name', 'BobPreUndo');
69+
70+
// Apply batch update (to be undone)
71+
const updates: CellUpdate[] = [
72+
{ rowId: 1, column: 'name', value: 'AliceNew' },
73+
{ rowId: 2, column: 'name', value: 'BobNew' }
74+
];
75+
await engine.updateCellBatch('users', updates);
76+
77+
// Verify update
78+
let result = await engine.executeQuery("SELECT name FROM users ORDER BY id");
79+
assert.strictEqual(result[0].rows[0][0], 'AliceNew');
80+
assert.strictEqual(result[0].rows[1][0], 'BobNew');
81+
82+
// Undo
83+
const mod: ModificationEntry = {
84+
modificationType: 'cell_update',
85+
targetTable: 'users',
86+
description: 'Batch update',
87+
affectedCells: [
88+
{ rowId: 1, columnName: 'name', priorValue: 'AlicePreUndo', newValue: 'AliceNew' },
89+
{ rowId: 2, columnName: 'name', priorValue: 'BobPreUndo', newValue: 'BobNew' }
90+
]
91+
};
92+
await engine.undoModification(mod);
93+
94+
// Verify undo
95+
result = await engine.executeQuery("SELECT name FROM users ORDER BY id");
96+
assert.strictEqual(result[0].rows[0][0], 'AlicePreUndo');
97+
assert.strictEqual(result[0].rows[1][0], 'BobPreUndo');
98+
});
99+
100+
it('should fail transaction on error', async () => {
101+
const updates: CellUpdate[] = [
102+
{ rowId: 1, column: 'name', value: 'AliceTrans' },
103+
// Invalid column to force error
104+
{ rowId: 2, column: 'non_existent_column', value: 'BobTrans' }
105+
];
106+
107+
try {
108+
await engine.updateCellBatch('users', updates);
109+
assert.fail('Should have thrown');
110+
} catch (e) {
111+
// Check that first update was rolled back
112+
const result = await engine.executeQuery("SELECT name FROM users WHERE id = 1");
113+
assert.strictEqual(result[0].rows[0][0], 'AlicePreUndo'); // Value from previous test
114+
}
115+
});
116+
117+
it('should validate rowId in updateCell', async () => {
118+
try {
119+
await engine.updateCell('users', 'invalid', 'name', 'test');
120+
assert.fail('Should have thrown invalid rowid');
121+
} catch (e: any) {
122+
assert.match(e.message, /Invalid rowid/);
123+
}
124+
});
125+
126+
it('should validate rowId in updateCellBatch', async () => {
127+
const updates: CellUpdate[] = [
128+
{ rowId: 'invalid', column: 'name', value: 'test' }
129+
];
130+
try {
131+
await engine.updateCellBatch('users', updates);
132+
assert.fail('Should have thrown invalid rowid');
133+
} catch (e: any) {
134+
assert.match(e.message, /Invalid rowid/);
135+
}
136+
});
137+
});

0 commit comments

Comments
 (0)