1
0

db-perf.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * DB Performance / Correctness Tests
  3. *
  4. * Regression tests for three changes:
  5. * 1. Batch `getNodesByIds` collapses graph-traversal N+1 reads.
  6. * 2. `insertNode` invalidates the LRU cache so INSERT OR REPLACE
  7. * doesn't serve a stale cached row on next `getNodeById`.
  8. * 3. `runMaintenance` runs `PRAGMA optimize` + `wal_checkpoint(PASSIVE)`
  9. * after indexAll/sync without throwing.
  10. */
  11. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  12. import * as fs from 'fs';
  13. import * as path from 'path';
  14. import * as os from 'os';
  15. import { DatabaseConnection } from '../src/db';
  16. import { QueryBuilder } from '../src/db/queries';
  17. import { Node } from '../src/types';
  18. function makeNode(id: string, name = id): Node {
  19. return {
  20. id,
  21. kind: 'function',
  22. name,
  23. qualifiedName: name,
  24. filePath: 'a.ts',
  25. language: 'typescript',
  26. startLine: 1,
  27. endLine: 1,
  28. startColumn: 0,
  29. endColumn: 0,
  30. updatedAt: Date.now(),
  31. };
  32. }
  33. describe('getNodesByIds (batch lookup)', () => {
  34. let dir: string;
  35. let db: DatabaseConnection;
  36. let q: QueryBuilder;
  37. beforeEach(() => {
  38. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-perf-batch-'));
  39. db = DatabaseConnection.initialize(path.join(dir, 'test.db'));
  40. q = new QueryBuilder(db.getDb());
  41. });
  42. afterEach(() => {
  43. db.close();
  44. if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  45. });
  46. it('returns a Map keyed by id, with one entry per existing node', () => {
  47. q.insertNodes([makeNode('n1'), makeNode('n2'), makeNode('n3')]);
  48. const out = q.getNodesByIds(['n1', 'n2', 'n3']);
  49. expect(out.size).toBe(3);
  50. expect(out.get('n1')!.name).toBe('n1');
  51. expect(out.get('n3')!.name).toBe('n3');
  52. });
  53. it('omits missing IDs from the result map (no nulls, no exceptions)', () => {
  54. q.insertNodes([makeNode('n1'), makeNode('n2')]);
  55. const out = q.getNodesByIds(['n1', 'missing', 'n2']);
  56. expect(out.size).toBe(2);
  57. expect(out.has('missing')).toBe(false);
  58. expect(out.has('n1')).toBe(true);
  59. expect(out.has('n2')).toBe(true);
  60. });
  61. it('handles an empty input array', () => {
  62. expect(q.getNodesByIds([]).size).toBe(0);
  63. });
  64. it('handles batches over the SQLite parameter limit (chunking)', () => {
  65. // Insert 1500 nodes; the helper chunks at 500 internally.
  66. const nodes = Array.from({ length: 1500 }, (_, i) => makeNode(`n${i}`));
  67. q.insertNodes(nodes);
  68. const ids = nodes.map((n) => n.id);
  69. const out = q.getNodesByIds(ids);
  70. expect(out.size).toBe(1500);
  71. // Spot-check a few from the first / middle / last chunk.
  72. expect(out.has('n0')).toBe(true);
  73. expect(out.has('n750')).toBe(true);
  74. expect(out.has('n1499')).toBe(true);
  75. });
  76. it('serves cache hits from memory and queries only the misses', () => {
  77. q.insertNodes([makeNode('n1'), makeNode('n2'), makeNode('n3')]);
  78. // Warm the cache for n1 only.
  79. q.getNodeById('n1');
  80. // Replace the underlying row to make a miss-vs-cache-hit detectable.
  81. db.getDb().prepare('UPDATE nodes SET name = ? WHERE id = ?').run('changed', 'n1');
  82. const out = q.getNodesByIds(['n1', 'n2']);
  83. // The cached n1 (still 'n1', not 'changed') must be returned.
  84. expect(out.get('n1')!.name).toBe('n1');
  85. expect(out.get('n2')!.name).toBe('n2');
  86. });
  87. });
  88. describe('insertNode cache invalidation', () => {
  89. let dir: string;
  90. let db: DatabaseConnection;
  91. let q: QueryBuilder;
  92. beforeEach(() => {
  93. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-perf-cache-'));
  94. db = DatabaseConnection.initialize(path.join(dir, 'test.db'));
  95. q = new QueryBuilder(db.getDb());
  96. });
  97. afterEach(() => {
  98. db.close();
  99. if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  100. });
  101. it('does not serve a stale cached node after INSERT OR REPLACE', () => {
  102. // Regression: insertNode (which uses INSERT OR REPLACE) used to skip
  103. // cache invalidation, so the next getNodeById returned the pre-replace
  104. // version until LRU eviction.
  105. const original = makeNode('n1', 'oldName');
  106. q.insertNode(original);
  107. const beforeReplace = q.getNodeById('n1');
  108. expect(beforeReplace!.name).toBe('oldName');
  109. // Replace via insertNode (the bug path).
  110. q.insertNode({ ...original, name: 'newName', updatedAt: Date.now() });
  111. const afterReplace = q.getNodeById('n1');
  112. expect(afterReplace!.name).toBe('newName');
  113. });
  114. });
  115. describe('runMaintenance', () => {
  116. let dir: string;
  117. let db: DatabaseConnection;
  118. beforeEach(() => {
  119. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-perf-maint-'));
  120. db = DatabaseConnection.initialize(path.join(dir, 'test.db'));
  121. });
  122. afterEach(() => {
  123. db.close();
  124. if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  125. });
  126. it('runs without throwing on a fresh database', () => {
  127. expect(() => db.runMaintenance()).not.toThrow();
  128. });
  129. it('runs without throwing after writes', () => {
  130. const q = new QueryBuilder(db.getDb());
  131. q.insertNodes([makeNode('n1'), makeNode('n2')]);
  132. expect(() => db.runMaintenance()).not.toThrow();
  133. });
  134. it('swallows failures rather than propagating (best-effort)', () => {
  135. // Close the DB so the underlying handle would normally throw on any
  136. // exec(). runMaintenance must still not propagate.
  137. db.close();
  138. expect(() => db.runMaintenance()).not.toThrow();
  139. });
  140. });