From b68934e51a1337747f221ca256d55a1d299bcc84 Mon Sep 17 00:00:00 2001 From: khaira777 <777gurkirat@gmail.com> Date: Sun, 18 Oct 2026 14:25:42 -0400 Subject: [PATCH 1/2] fix(db): repair sequence counters and rebuild search index on partial import (#1215) --- main/routes/database.ts | 12 +++++ tests/database-tools-api.test.ts | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/main/routes/database.ts b/main/routes/database.ts index 953bc3870..e1bedc2a5 100644 --- a/main/routes/database.ts +++ b/main/routes/database.ts @@ -11,6 +11,7 @@ import { getDbPath, createBackup, createBackupUnlocked, + executeSequenceRepair, getCurrentSchemaVersion, getForeignKeyViolationKeys, getInventoryMovementRows, @@ -21,6 +22,7 @@ import { mergeUserSecurityState, mergeUserStationSecurityState, now, + rebuildSearchIndex, restoreBackup, throwIfDatabaseMaintenanceAborted, validateInventoryLedgerDatabase, @@ -597,6 +599,16 @@ router.post( } throwIfDatabaseMaintenanceAborted(signal); clearGoogleDriveRestoreBinding(db); + executeSequenceRepair(db); + db.prepare(` + INSERT INTO settings (key, value, updated_at) VALUES ('sequence_repair_completed_at', ?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at + `).run(now(), now()); + db.prepare(` + INSERT INTO settings (key, value, updated_at) VALUES ('sequence_repair_pending', '0', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at + `).run(now()); + rebuildSearchIndex(db); db.exec('COMMIT'); const cleanup = googleDrive.completeDatabaseRestore(); try { diff --git a/tests/database-tools-api.test.ts b/tests/database-tools-api.test.ts index 8b9a7046b..c3f1a388a 100644 --- a/tests/database-tools-api.test.ts +++ b/tests/database-tools-api.test.ts @@ -47,6 +47,9 @@ const { MIGRATIONS, now, validateInventoryLedgerDatabase, + generateOrderNumber, + generateBillNumber, + repairSequences, } = require('../main/db'); const { getJWTSecret } = require('../main/routes/auth'); const { authRoutes } = require('../main/routes/auth'); @@ -1752,6 +1755,95 @@ async function runTests() { ); } + // ── Test 3d: sequence repair on partial import without sequences table ── + console.log('\nTest 3d: POST /db/import repairs sequences when importing orders/bills without sequences'); + { + const insertSetting = (key: string, value: string) => + db.prepare('INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)').run(key, value, now()); + insertSetting('country', 'IN'); + insertSetting('currency', 'INR'); + insertSetting('timezone', 'Asia/Kolkata'); + + const initialOrderNum = generateOrderNumber(); + const initialBillNum = generateBillNumber(); + const ordResult = db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(initialOrderNum, now()); + const initialOrderId = ordResult.lastInsertRowid; + db.prepare("INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (?, ?, 100, 0, 100, 100, 0, ?)").run(initialOrderId, initialBillNum, now()); + + const orderDate = initialOrderNum.split('-')[1]; + const billDate = initialBillNum.split('-')[1]; + + const partialImport = await request(app) + .post('/api/db/import') + .set('Authorization', `Bearer ${ownerToken}`) + .send({ + overwrite: false, + data: { + schema_version: String(getCurrentSchemaVersion()), + data: { + settings: [], + categories: [], + products: [], + users: [], + orders: [ + { id: 102, order_number: `ORD-${orderDate}-0002`, created_at: now() }, + { id: 103, order_number: `ORD-${orderDate}-0003`, created_at: now() }, + { id: 104, order_number: `ORD-${orderDate}-0004`, created_at: now() }, + { id: 105, order_number: `ORD-${orderDate}-0005`, created_at: now() }, + ], + bills: [ + { id: 102, order_id: 102, bill_number: `INV-${billDate}-0002`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, + { id: 103, order_id: 103, bill_number: `INV-${billDate}-0003`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, + { id: 104, order_id: 104, bill_number: `INV-${billDate}-0004`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, + { id: 105, order_id: 105, bill_number: `INV-${billDate}-0005`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, + ], + }, + }, + }); + + assert(partialImport.status === 200, `partial import succeeds (got ${partialImport.status})`); + + const orderSeq = db.prepare("SELECT current_value FROM sequences WHERE name = 'orders' AND date = ?").get(orderDate) as { current_value: number } | undefined; + assertEqual(orderSeq?.current_value, 5, 'orders sequence counter advanced to 5 after import'); + + const billSeq = db.prepare("SELECT current_value FROM sequences WHERE name = 'bills' AND date = ?").get(billDate) as { current_value: number } | undefined; + assertEqual(billSeq?.current_value, 5, 'bills sequence counter advanced to 5 after import'); + + const nextOrderNum = generateOrderNumber(); + assertEqual(nextOrderNum, `ORD-${orderDate}-0006`, 'next generated order number is 0006'); + let orderInsertThrew = false; + try { + db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(nextOrderNum, now()); + } catch { + orderInsertThrew = true; + } + assert(!orderInsertThrew, 'creating next order succeeds without UNIQUE constraint failure'); + + const nextBillNum = generateBillNumber(); + assertEqual(nextBillNum, `INV-${billDate}-0006`, 'next generated bill number is 0006'); + let billInsertThrew = false; + try { + db.prepare("INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (105, ?, 10, 0, 10, 10, 0, ?)").run(nextBillNum, now()); + } catch { + billInsertThrew = true; + } + assert(!billInsertThrew, 'creating next bill succeeds without UNIQUE constraint failure'); + + // Simulate reopen fast-path + const bootRepair = repairSequences(false); + assertEqual(bootRepair.repaired, false, 'reopen fast-path skips historical scan'); + + const nextOrderNumAfterReopen = generateOrderNumber(); + assertEqual(nextOrderNumAfterReopen, `ORD-${orderDate}-0007`, 'next order number after reopen is 0007'); + let orderReopenThrew = false; + try { + db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(nextOrderNumAfterReopen, now()); + } catch { + orderReopenThrew = true; + } + assert(!orderReopenThrew, 'creating order after reopen succeeds'); + } + // ── Test 4: POST /db-tools/initialize ──────────────────────────────────── console.log('\nTest 4: POST /db-tools/initialize'); { From d300ea56fe250403bbaea554d4f109a00bdde29a Mon Sep 17 00:00:00 2001 From: khaira777 <777gurkirat@gmail.com> Date: Sun, 18 Oct 2026 14:42:06 -0400 Subject: [PATCH 2/2] no-mistakes(document): document import sequence repair and format test additions --- docs/reference/api.md | 2 +- tests/database-tools-api.test.ts | 92 +++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/docs/reference/api.md b/docs/reference/api.md index 68bb57f59..125dc5e48 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -785,7 +785,7 @@ Router: `main/routes/database.ts`. Full path: `/api/db`. | Method | Path | Authorization | Parameters | Response | | --- | --- | --- | --- | --- | | `GET` | `/export` | `ROLE_ACCESS.owner` | none | Full database snapshot as a downloadable JSON attachment. | -| `POST` | `/import` | `ROLE_ACCESS.owner` + Master PIN when `overwrite` or a schema-version mismatch | body: `data`, `overwrite` | Body `data` plus `overwrite`. Master PIN is required when `overwrite` is set or the snapshot's `schema_version` differs from the local one. | +| `POST` | `/import` | `ROLE_ACCESS.owner` + Master PIN when `overwrite` or a schema-version mismatch | body: `data`, `overwrite` | Body `data` plus `overwrite`. Master PIN is required when `overwrite` is set or the snapshot's `schema_version` differs from the local one. Re-aligns sequence counters, marks sequence repair complete, and rebuilds search indexes before commit. | | `POST` | `/backup` | `ROLE_ACCESS.owner` + Master PIN | none | - | | `GET` | `/download` | `ROLE_ACCESS.owner` + Master PIN | none | - | | `GET` | `/tables` | `ROLE_ACCESS.owner` | none | `{ tables: [ ... ] }`, every non-internal SQLite table name. | diff --git a/tests/database-tools-api.test.ts b/tests/database-tools-api.test.ts index c3f1a388a..276442be9 100644 --- a/tests/database-tools-api.test.ts +++ b/tests/database-tools-api.test.ts @@ -1756,19 +1756,27 @@ async function runTests() { } // ── Test 3d: sequence repair on partial import without sequences table ── - console.log('\nTest 3d: POST /db/import repairs sequences when importing orders/bills without sequences'); + console.log( + '\nTest 3d: POST /db/import repairs sequences when importing orders/bills without sequences', + ); { const insertSetting = (key: string, value: string) => - db.prepare('INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)').run(key, value, now()); + db + .prepare('INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)') + .run(key, value, now()); insertSetting('country', 'IN'); insertSetting('currency', 'INR'); insertSetting('timezone', 'Asia/Kolkata'); const initialOrderNum = generateOrderNumber(); const initialBillNum = generateBillNumber(); - const ordResult = db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(initialOrderNum, now()); + const ordResult = db + .prepare('INSERT INTO orders (order_number, created_at) VALUES (?, ?)') + .run(initialOrderNum, now()); const initialOrderId = ordResult.lastInsertRowid; - db.prepare("INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (?, ?, 100, 0, 100, 100, 0, ?)").run(initialOrderId, initialBillNum, now()); + db.prepare( + 'INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (?, ?, 100, 0, 100, 100, 0, ?)', + ).run(initialOrderId, initialBillNum, now()); const orderDate = initialOrderNum.split('-')[1]; const billDate = initialBillNum.split('-')[1]; @@ -1792,10 +1800,50 @@ async function runTests() { { id: 105, order_number: `ORD-${orderDate}-0005`, created_at: now() }, ], bills: [ - { id: 102, order_id: 102, bill_number: `INV-${billDate}-0002`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, - { id: 103, order_id: 103, bill_number: `INV-${billDate}-0003`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, - { id: 104, order_id: 104, bill_number: `INV-${billDate}-0004`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, - { id: 105, order_id: 105, bill_number: `INV-${billDate}-0005`, subtotal: 10, tax_amount: 0, total: 10, paid_amount: 10, balance: 0, created_at: now() }, + { + id: 102, + order_id: 102, + bill_number: `INV-${billDate}-0002`, + subtotal: 10, + tax_amount: 0, + total: 10, + paid_amount: 10, + balance: 0, + created_at: now(), + }, + { + id: 103, + order_id: 103, + bill_number: `INV-${billDate}-0003`, + subtotal: 10, + tax_amount: 0, + total: 10, + paid_amount: 10, + balance: 0, + created_at: now(), + }, + { + id: 104, + order_id: 104, + bill_number: `INV-${billDate}-0004`, + subtotal: 10, + tax_amount: 0, + total: 10, + paid_amount: 10, + balance: 0, + created_at: now(), + }, + { + id: 105, + order_id: 105, + bill_number: `INV-${billDate}-0005`, + subtotal: 10, + tax_amount: 0, + total: 10, + paid_amount: 10, + balance: 0, + created_at: now(), + }, ], }, }, @@ -1803,17 +1851,24 @@ async function runTests() { assert(partialImport.status === 200, `partial import succeeds (got ${partialImport.status})`); - const orderSeq = db.prepare("SELECT current_value FROM sequences WHERE name = 'orders' AND date = ?").get(orderDate) as { current_value: number } | undefined; + const orderSeq = db + .prepare("SELECT current_value FROM sequences WHERE name = 'orders' AND date = ?") + .get(orderDate) as { current_value: number } | undefined; assertEqual(orderSeq?.current_value, 5, 'orders sequence counter advanced to 5 after import'); - const billSeq = db.prepare("SELECT current_value FROM sequences WHERE name = 'bills' AND date = ?").get(billDate) as { current_value: number } | undefined; + const billSeq = db + .prepare("SELECT current_value FROM sequences WHERE name = 'bills' AND date = ?") + .get(billDate) as { current_value: number } | undefined; assertEqual(billSeq?.current_value, 5, 'bills sequence counter advanced to 5 after import'); const nextOrderNum = generateOrderNumber(); assertEqual(nextOrderNum, `ORD-${orderDate}-0006`, 'next generated order number is 0006'); let orderInsertThrew = false; try { - db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(nextOrderNum, now()); + db.prepare('INSERT INTO orders (order_number, created_at) VALUES (?, ?)').run( + nextOrderNum, + now(), + ); } catch { orderInsertThrew = true; } @@ -1823,7 +1878,9 @@ async function runTests() { assertEqual(nextBillNum, `INV-${billDate}-0006`, 'next generated bill number is 0006'); let billInsertThrew = false; try { - db.prepare("INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (105, ?, 10, 0, 10, 10, 0, ?)").run(nextBillNum, now()); + db.prepare( + 'INSERT INTO bills (order_id, bill_number, subtotal, tax_amount, total, paid_amount, balance, created_at) VALUES (105, ?, 10, 0, 10, 10, 0, ?)', + ).run(nextBillNum, now()); } catch { billInsertThrew = true; } @@ -1834,10 +1891,17 @@ async function runTests() { assertEqual(bootRepair.repaired, false, 'reopen fast-path skips historical scan'); const nextOrderNumAfterReopen = generateOrderNumber(); - assertEqual(nextOrderNumAfterReopen, `ORD-${orderDate}-0007`, 'next order number after reopen is 0007'); + assertEqual( + nextOrderNumAfterReopen, + `ORD-${orderDate}-0007`, + 'next order number after reopen is 0007', + ); let orderReopenThrew = false; try { - db.prepare("INSERT INTO orders (order_number, created_at) VALUES (?, ?)").run(nextOrderNumAfterReopen, now()); + db.prepare('INSERT INTO orders (order_number, created_at) VALUES (?, ?)').run( + nextOrderNumAfterReopen, + now(), + ); } catch { orderReopenThrew = true; }