* Batch lookup: fetch many nodes by ID in a single SQL round-trip. * * Replaces the N+1 pattern in graph traversal where every edge would * trigger its own `getNodeById` call. For a function with 50 callers * this collapses 50 point reads into one IN-list query (~10-50x * faster end-t
(ids: readonly string[])
| 794 | * the SQL query only touches the misses. |
| 795 | */ |
| 796 | getNodesByIds(ids: readonly string[]): Map<string, Node> { |
| 797 | const out = new Map<string, Node>(); |
| 798 | if (ids.length === 0) return out; |
| 799 | |
| 800 | // Serve cache hits first; build the miss list for SQL. |
| 801 | const misses: string[] = []; |
| 802 | for (const id of ids) { |
| 803 | const cached = this.nodeCache.get(id); |
| 804 | if (cached !== undefined) { |
| 805 | // LRU touch |
| 806 | this.nodeCache.delete(id); |
| 807 | this.nodeCache.set(id, cached); |
| 808 | out.set(id, cached); |
| 809 | } else { |
| 810 | misses.push(id); |
| 811 | } |
| 812 | } |
| 813 | if (misses.length === 0) return out; |
| 814 | |
| 815 | // Chunk under SQLite's parameter limit (default 999, raised to 32766 |
| 816 | // in better-sqlite3 builds — chunk at 500 for safety across both |
| 817 | // backends and to keep the query plan simple). |
| 818 | for (let i = 0; i < misses.length; i += SQLITE_PARAM_CHUNK_SIZE) { |
| 819 | const chunk = misses.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); |
| 820 | const placeholders = chunk.map(() => '?').join(','); |
| 821 | const rows = this.db |
| 822 | .prepare(`SELECT * FROM nodes WHERE id IN (${placeholders})`) |
| 823 | .all(...chunk) as NodeRow[]; |
| 824 | for (const row of rows) { |
| 825 | const node = rowToNode(row); |
| 826 | out.set(node.id, node); |
| 827 | this.cacheNode(node); |
| 828 | } |
| 829 | } |
| 830 | return out; |
| 831 | } |
| 832 | |
| 833 | private getExistingNodeIds(ids: readonly string[]): Set<string> { |
| 834 | const out = new Set<string>(); |
no test coverage detected