@gmod/gbz-base 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { LocalFile, RemoteFile } from 'generic-filehandle2'
2
2
 
3
3
  import { GBZBase, formatPathName } from './db.ts'
4
+ import { encodeNode } from './gbwt/node.ts'
4
5
  import {
5
6
  subgraphAroundNodes,
6
7
  subgraphAtOffset,
8
+ subgraphBetween,
7
9
  subgraphInInterval,
8
10
  } from './query.ts'
9
11
 
10
- import type { HaplotypeOutput } from './subgraph.ts'
12
+ import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
11
13
 
12
14
  const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
13
15
 
@@ -17,12 +19,17 @@ const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
17
19
  -o, --offset INT sequence offset
18
20
  -i, --interval A..B half-open sequence interval
19
21
  -n, --node INT node identifier (may repeat)
22
+ -b, --between A:B subgraph between two chain boundary handles, each INT[+-]
20
23
  --context INT context length in bp (default: 100)
24
+ --snarls extend the subgraph with contained top-level snarls
25
+ --extend-snarls extend the subgraph with overlapping top-level snarls
21
26
  --limit INT safety limit for the number of nodes
22
27
  --haplotypes SEL all, distinct, reference-only or none (default: all)
23
28
  --cigar output CIGAR strings for the haplotypes
29
+ --format FMT json (default) or gfa
24
30
  --resolve name haplotypes from the HaplotypeSamples table
25
31
  --alignments print one alignment record per haplotype fragment instead of the subgraph
32
+ --haplotype-index F companion database written by gbz-haplotype-index --output
26
33
  --block-size INT bytes fetched per range request (default: 65536)
27
34
  --stats print fetch statistics to stderr
28
35
  `
@@ -35,24 +42,39 @@ interface Args {
35
42
  offset?: number
36
43
  interval?: [number, number]
37
44
  nodes: number[]
45
+ between?: [number, number]
38
46
  context: number
47
+ snarls: SnarlOutput
39
48
  limit?: number
40
49
  haplotypes: HaplotypeOutput
41
50
  cigar: boolean
51
+ format: 'json' | 'gfa'
42
52
  resolve: boolean
43
53
  alignments: boolean
44
54
  blockSize: number
55
+ haplotypeIndex?: string
45
56
  stats: boolean
46
57
  }
47
58
 
59
+ function parseHandle(text: string) {
60
+ const orientation = text.endsWith('-') ? 'reverse' : 'forward'
61
+ const digits = /[+-]$/.test(text) ? text.slice(0, -1) : text
62
+ if (!/^\d+$/.test(digits)) {
63
+ throw new Error(`Failed to parse oriented node ${text}`)
64
+ }
65
+ return encodeNode(Number(digits), orientation)
66
+ }
67
+
48
68
  function parseArgs(argv: string[]): Args {
49
69
  const args: Args = {
50
70
  file: '',
51
71
  haplotype: 0,
52
72
  nodes: [],
53
73
  context: 100,
74
+ snarls: 'none',
54
75
  haplotypes: 'all',
55
76
  cigar: false,
77
+ format: 'json',
56
78
  resolve: false,
57
79
  alignments: false,
58
80
  blockSize: 65536,
@@ -91,9 +113,25 @@ function parseArgs(argv: string[]): Args {
91
113
  case '--node':
92
114
  args.nodes.push(Number(next(i++)))
93
115
  break
116
+ case '-b':
117
+ case '--between': {
118
+ const [a, b, extra] = next(i++).split(':')
119
+ if (a === undefined || b === undefined || extra !== undefined) {
120
+ throw new Error(`--between needs two oriented nodes, like 14+:17-`)
121
+ }
122
+ args.between = [parseHandle(a), parseHandle(b)]
123
+ break
124
+ }
94
125
  case '--context':
95
126
  args.context = Number(next(i++))
96
127
  break
128
+ case '--snarls':
129
+ args.snarls =
130
+ args.snarls === 'overlapping' ? 'overlapping' : 'contained'
131
+ break
132
+ case '--extend-snarls':
133
+ args.snarls = 'overlapping'
134
+ break
97
135
  case '--limit':
98
136
  args.limit = Number(next(i++))
99
137
  break
@@ -113,12 +151,20 @@ function parseArgs(argv: string[]): Args {
113
151
  case '--block-size':
114
152
  args.blockSize = Number(next(i++))
115
153
  break
154
+ case '--haplotype-index':
155
+ args.haplotypeIndex = next(i++)
156
+ break
116
157
  case '--stats':
117
158
  args.stats = true
118
159
  break
119
- case '--format':
120
- next(i++)
160
+ case '--format': {
161
+ const format = next(i++)
162
+ if (format !== 'json' && format !== 'gfa') {
163
+ throw new Error(`Unknown output format ${format}`)
164
+ }
165
+ args.format = format
121
166
  break
167
+ }
122
168
  case '-h':
123
169
  case '--help':
124
170
  process.stdout.write(USAGE)
@@ -139,13 +185,18 @@ function parseArgs(argv: string[]): Args {
139
185
 
140
186
  export async function main(argv: string[]) {
141
187
  const args = parseArgs(argv)
142
- const source = /^https?:\/\//.test(args.file)
143
- ? new RemoteFile(args.file)
144
- : new LocalFile(args.file)
145
- const db = await GBZBase.open(source, { blockSize: args.blockSize })
188
+ const open = (file: string) =>
189
+ /^https?:\/\//.test(file) ? new RemoteFile(file) : new LocalFile(file)
190
+ const db = await GBZBase.open(open(args.file), {
191
+ blockSize: args.blockSize,
192
+ ...(args.haplotypeIndex === undefined
193
+ ? {}
194
+ : { haplotypeIndex: open(args.haplotypeIndex) }),
195
+ })
146
196
  const opts = {
147
197
  context: args.context,
148
198
  haplotypes: args.haplotypes,
199
+ snarls: args.snarls,
149
200
  ...(args.limit === undefined ? {} : { limit: args.limit }),
150
201
  }
151
202
  const query = {
@@ -153,8 +204,9 @@ export async function main(argv: string[]) {
153
204
  haplotype: args.haplotype,
154
205
  ...(args.sample === undefined ? {} : { sample: args.sample }),
155
206
  }
156
- const subgraph =
157
- args.nodes.length > 0
207
+ const subgraph = args.between
208
+ ? await subgraphBetween(db, args.between[0], args.between[1], opts)
209
+ : args.nodes.length > 0
158
210
  ? await subgraphAroundNodes(db, args.nodes, opts)
159
211
  : args.interval
160
212
  ? await subgraphInInterval(
@@ -169,12 +221,13 @@ export async function main(argv: string[]) {
169
221
  : undefined
170
222
  if (!subgraph) {
171
223
  throw new Error(
172
- 'Query type must be specified using --offset, --interval or --node',
224
+ 'Query type must be specified using --offset, --interval, --node or --between',
173
225
  )
174
226
  }
175
227
  if (args.resolve) {
176
228
  await subgraph.identifyPaths()
177
229
  }
230
+ const names = args.resolve ? 'resolved' : 'anonymous'
178
231
  const output = args.alignments
179
232
  ? subgraph.alignments().map(a => ({
180
233
  ...a,
@@ -184,10 +237,12 @@ export async function main(argv: string[]) {
184
237
  : undefined,
185
238
  start: undefined,
186
239
  }))
187
- : subgraph.toJSON(args.cigar, {
188
- names: args.resolve ? 'resolved' : 'anonymous',
189
- })
190
- process.stdout.write(`${JSON.stringify(output)}\n`)
240
+ : subgraph.toJSON(args.cigar, { names })
241
+ process.stdout.write(
242
+ args.format === 'gfa' && !args.alignments
243
+ ? await subgraph.toGFA(args.cigar, { names })
244
+ : `${JSON.stringify(output)}\n`,
245
+ )
191
246
  if (args.stats) {
192
247
  const { fetches, bytesFetched } = db.sqlite.pager
193
248
  const {
package/src/db.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
2
2
  import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
3
3
  import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
4
+ import { graphNameFromTags } from './graphName.ts'
4
5
  import { SqliteDatabase } from './sqlite/database.ts'
5
6
 
6
7
  import type { ByteSource } from './filehandle.ts'
7
8
  import type { Pos } from './gbwt/record.ts'
9
+ import type { GraphName } from './graphName.ts'
8
10
  import type { PagerOptions } from './sqlite/pager.ts'
9
11
  import type { SqlValue } from './sqlite/record.ts'
10
12
 
@@ -144,37 +146,63 @@ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
144
146
  }
145
147
  }
146
148
 
149
+ export interface OpenOptions extends PagerOptions {
150
+ haplotypeIndex?: ByteSource
151
+ }
152
+
153
+ async function readTags(sqlite: SqliteDatabase) {
154
+ const tags = new Map<string, string>()
155
+ for await (const { values } of sqlite.scan('Tags')) {
156
+ tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
157
+ }
158
+ return tags
159
+ }
160
+
147
161
  export class GBZBase {
148
162
  private tagCache: Promise<Map<string, string>> | undefined
149
163
  private pathCache: Promise<GbzPath[]> | undefined
164
+ private indexTags: Map<string, string> | undefined
150
165
 
151
166
  readonly sqlite: SqliteDatabase
167
+ private readonly index: SqliteDatabase
152
168
 
153
- private constructor(sqlite: SqliteDatabase) {
169
+ private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
154
170
  this.sqlite = sqlite
171
+ this.index = index
155
172
  }
156
173
 
157
- static async open(source: ByteSource, opts: PagerOptions = {}) {
158
- const sqlite = await SqliteDatabase.open(source, opts)
174
+ static async open(source: ByteSource, opts: OpenOptions = {}) {
175
+ const { haplotypeIndex, ...pagerOptions } = opts
176
+ const sqlite = await SqliteDatabase.open(source, pagerOptions)
159
177
  for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
160
178
  sqlite.rootPage(table)
161
179
  }
162
- const db = new GBZBase(sqlite)
180
+ const index = haplotypeIndex
181
+ ? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
182
+ : sqlite
183
+ const db = new GBZBase(sqlite, index)
163
184
  const version = await db.tag('version')
164
185
  if (version !== SCHEMA_VERSION) {
165
186
  throw new SchemaVersionError(version)
166
187
  }
188
+ if (haplotypeIndex) {
189
+ for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
190
+ index.rootPage(table)
191
+ }
192
+ db.indexTags = await readTags(index)
193
+ const indexed = db.indexTags.get('haplotype_index_paths')
194
+ const paths = await db.tag('paths')
195
+ if (indexed !== paths) {
196
+ throw new Error(
197
+ `haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
198
+ )
199
+ }
200
+ }
167
201
  return db
168
202
  }
169
203
 
170
204
  tags() {
171
- this.tagCache ??= (async () => {
172
- const tags = new Map<string, string>()
173
- for await (const { values } of this.sqlite.scan('Tags')) {
174
- tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
175
- }
176
- return tags
177
- })()
205
+ this.tagCache ??= readTags(this.sqlite)
178
206
  return this.tagCache
179
207
  }
180
208
 
@@ -228,14 +256,41 @@ export class GBZBase {
228
256
  return (await this.paths()).filter(p => p.name.sample === sample)
229
257
  }
230
258
 
259
+ async graphName() {
260
+ const gbzTags = new Map<string, string>()
261
+ for (const [key, value] of await this.tags()) {
262
+ if (key.startsWith('gbz_')) {
263
+ gbzTags.set(key.slice('gbz_'.length), value)
264
+ }
265
+ }
266
+ let name: GraphName = {
267
+ name: undefined,
268
+ subgraph: new Map(),
269
+ translation: new Map(),
270
+ }
271
+ try {
272
+ name = graphNameFromTags(gbzTags)
273
+ } catch {
274
+ // upstream falls back to an empty name when the tags do not parse
275
+ }
276
+ return name
277
+ }
278
+
279
+ async hasChainLinks() {
280
+ const links = await this.tag('chain_links')
281
+ return links !== undefined && Number(links) > 0
282
+ }
283
+
231
284
  get hasHaplotypeIndex() {
232
285
  return (
233
- this.sqlite.has('HaplotypeSamples') && this.sqlite.has('HaplotypeLengths')
286
+ this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
234
287
  )
235
288
  }
236
289
 
237
290
  async haplotypeSampleInterval() {
238
- const value = await this.tag('haplotype_index_interval')
291
+ const value = this.indexTags
292
+ ? this.indexTags.get('haplotype_index_interval')
293
+ : await this.tag('haplotype_index_interval')
239
294
  return value === undefined ? undefined : Number(value)
240
295
  }
241
296
 
@@ -254,7 +309,7 @@ export class GBZBase {
254
309
 
255
310
  async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
256
311
  const samples: HaplotypeSample[] = []
257
- for await (const key of this.sqlite.indexScanFrom('HaplotypeSamples', [
312
+ for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
258
313
  minHandle,
259
314
  0,
260
315
  ])) {
@@ -262,7 +317,7 @@ export class GBZBase {
262
317
  if (node > maxHandle) {
263
318
  break
264
319
  }
265
- const row = await this.sqlite.byRowid(
320
+ const row = await this.index.byRowid(
266
321
  'HaplotypeSamples',
267
322
  num(key[2], 'HaplotypeSamples rowid'),
268
323
  )
@@ -274,14 +329,11 @@ export class GBZBase {
274
329
  }
275
330
 
276
331
  async haplotypeSampleAt(node: number, offset: number) {
277
- const key = await this.sqlite.indexSeekLE('HaplotypeSamples', [
278
- node,
279
- offset,
280
- ])
332
+ const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
281
333
  if (key?.[0] !== node || key[1] !== offset) {
282
334
  return undefined
283
335
  }
284
- const row = await this.sqlite.byRowid(
336
+ const row = await this.index.byRowid(
285
337
  'HaplotypeSamples',
286
338
  num(key[2], 'HaplotypeSamples rowid'),
287
339
  )
@@ -289,7 +341,7 @@ export class GBZBase {
289
341
  }
290
342
 
291
343
  async haplotypeLength(pathHandle: number) {
292
- const row = await this.sqlite.byRowid('HaplotypeLengths', pathHandle)
344
+ const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
293
345
  return row ? num(row[1], 'HaplotypeLengths.length') : undefined
294
346
  }
295
347
 
@@ -139,6 +139,29 @@ export class GbwtRecord {
139
139
  return undefined
140
140
  }
141
141
 
142
+ decompressArrays() {
143
+ const offsets = this.edges.map(e => e.offset)
144
+ let total = 0
145
+ for (const run of this.runs()) {
146
+ total += run.len
147
+ }
148
+ const nodes = new Int32Array(total)
149
+ const nextOffsets = new Int32Array(total)
150
+ let i = 0
151
+ for (const run of this.runs()) {
152
+ const edge = this.edge(run.value)
153
+ let offset = offsets[run.value]!
154
+ for (let k = 0; k < run.len; k++) {
155
+ nodes[i] = edge.node
156
+ nextOffsets[i] = offset
157
+ offset += 1
158
+ i += 1
159
+ }
160
+ offsets[run.value] = offset
161
+ }
162
+ return { nodes, offsets: nextOffsets }
163
+ }
164
+
142
165
  decompress(): Pos[] {
143
166
  const offsets = this.edges.map(e => e.offset)
144
167
  const result: Pos[] = []
@@ -0,0 +1,94 @@
1
+ export interface GraphName {
2
+ name: string | undefined
3
+ subgraph: Map<string, Set<string>>
4
+ translation: Map<string, Set<string>>
5
+ }
6
+
7
+ function parseRelationships(field: string | undefined, what: string) {
8
+ const relationships = new Map<string, Set<string>>()
9
+ if (field !== undefined) {
10
+ for (const rel of field.split(';')) {
11
+ const parts = rel.split(',')
12
+ const [from, to] = parts
13
+ if (parts.length !== 2 || !from || !to) {
14
+ throw new Error(`Invalid ${what} relationship: ${rel}`)
15
+ }
16
+ let targets = relationships.get(from)
17
+ if (!targets) {
18
+ targets = new Set()
19
+ relationships.set(from, targets)
20
+ }
21
+ targets.add(to)
22
+ }
23
+ }
24
+ return relationships
25
+ }
26
+
27
+ export function graphNameFromTags(tags: Map<string, string>): GraphName {
28
+ return {
29
+ name: tags.get('pggname'),
30
+ subgraph: parseRelationships(tags.get('subgraph'), 'subgraph'),
31
+ translation: parseRelationships(tags.get('translation'), 'translation'),
32
+ }
33
+ }
34
+
35
+ function merge(into: Map<string, Set<string>>, from: Map<string, Set<string>>) {
36
+ for (const [key, values] of from) {
37
+ let targets = into.get(key)
38
+ if (!targets) {
39
+ targets = new Set()
40
+ into.set(key, targets)
41
+ }
42
+ for (const value of values) {
43
+ targets.add(value)
44
+ }
45
+ }
46
+ }
47
+
48
+ export function subgraphName(name: string, parent: GraphName): GraphName {
49
+ const result: GraphName = {
50
+ name,
51
+ subgraph: new Map(),
52
+ translation: new Map(),
53
+ }
54
+ if (parent.name !== undefined) {
55
+ result.subgraph.set(name, new Set([parent.name]))
56
+ merge(result.subgraph, parent.subgraph)
57
+ merge(result.translation, parent.translation)
58
+ }
59
+ return result
60
+ }
61
+
62
+ function sortedEntries(relationships: Map<string, Set<string>>) {
63
+ return [...relationships.entries()]
64
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
65
+ .flatMap(([from, tos]) => [...tos].sort().map(to => [from, to] as const))
66
+ }
67
+
68
+ export function gfaHeaderLines(graphName: GraphName) {
69
+ const lines: string[] = []
70
+ if (graphName.name !== undefined) {
71
+ lines.push(`H\tNM:Z:${graphName.name}`)
72
+ }
73
+ for (const [subgraph, supergraph] of sortedEntries(graphName.subgraph)) {
74
+ lines.push(`H\tSG:Z:${subgraph},${supergraph}`)
75
+ }
76
+ for (const [from, to] of sortedEntries(graphName.translation)) {
77
+ lines.push(`H\tTL:Z:${from},${to}`)
78
+ }
79
+ return lines
80
+ }
81
+
82
+ export async function sha256Hex(chunks: Uint8Array[]) {
83
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
84
+ const bytes = new Uint8Array(total)
85
+ let offset = 0
86
+ for (const chunk of chunks) {
87
+ bytes.set(chunk, offset)
88
+ offset += chunk.length
89
+ }
90
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)
91
+ return [...new Uint8Array(digest)]
92
+ .map(byte => byte.toString(16).padStart(2, '0'))
93
+ .join('')
94
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export {
6
6
  SchemaVersionError,
7
7
  formatPathName,
8
8
  } from './db.ts'
9
- export type { GbzPath, HaplotypeSample, PathName } from './db.ts'
9
+ export type { GbzPath, HaplotypeSample, OpenOptions, PathName } from './db.ts'
10
10
  export type { ByteSource } from './filehandle.ts'
11
11
  export type { Pos } from './gbwt/record.ts'
12
12
  export { Subgraph } from './subgraph.ts'
@@ -16,6 +16,7 @@ export type {
16
16
  PathIdentity,
17
17
  PathPosition,
18
18
  ReferencePath,
19
+ SnarlOutput,
19
20
  SubgraphJson,
20
21
  SubgraphPath,
21
22
  ToJsonOptions,
@@ -23,9 +24,11 @@ export type {
23
24
  export {
24
25
  subgraphAroundNodes,
25
26
  subgraphAtOffset,
27
+ subgraphBetween,
26
28
  subgraphInInterval,
27
29
  } from './query.ts'
28
30
  export type { PathQuery, QueryOptions } from './query.ts'
29
31
  export { SqliteDatabase } from './sqlite/database.ts'
32
+ export type { GraphName } from './graphName.ts'
30
33
  export { weightedLcs } from './lcs.ts'
31
34
  export * as nodes from './gbwt/node.ts'
package/src/query.ts CHANGED
@@ -2,11 +2,12 @@ import { GENERIC_SAMPLE } from './db.ts'
2
2
  import { Subgraph } from './subgraph.ts'
3
3
 
4
4
  import type { GBZBase, PathName } from './db.ts'
5
- import type { HaplotypeOutput } from './subgraph.ts'
5
+ import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
6
6
 
7
7
  export interface QueryOptions {
8
8
  context?: number
9
9
  haplotypes?: HaplotypeOutput
10
+ snarls?: SnarlOutput
10
11
  limit?: number
11
12
  }
12
13
 
@@ -39,6 +40,7 @@ export async function subgraphAtOffset(
39
40
  reference.position.nodeOffset,
40
41
  opts.context ?? 100,
41
42
  )
43
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
42
44
  subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
43
45
  return subgraph
44
46
  }
@@ -58,6 +60,7 @@ export async function subgraphInInterval(
58
60
  end - start,
59
61
  opts.context ?? 100,
60
62
  )
63
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
61
64
  subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
62
65
  return subgraph
63
66
  }
@@ -68,12 +71,36 @@ export async function subgraphAroundNodes(
68
71
  opts: QueryOptions = {},
69
72
  ) {
70
73
  const haplotypes = opts.haplotypes ?? 'all'
74
+ const snarls = opts.snarls ?? 'none'
71
75
  if (haplotypes === 'reference-only') {
72
76
  throw new Error('Cannot output a reference path in a node-based query')
73
77
  }
78
+ if (snarls === 'overlapping' && nodes.length > 1) {
79
+ throw new Error(
80
+ 'Overlapping snarls cannot be extracted for a node-based query with multiple nodes',
81
+ )
82
+ }
74
83
  const subgraph = new Subgraph(db)
75
84
  subgraph.limit = opts.limit
76
85
  await subgraph.aroundNodes(nodes, opts.context ?? 100)
86
+ await subgraph.extractSnarls(snarls)
87
+ subgraph.extractPaths(undefined, haplotypes)
88
+ return subgraph
89
+ }
90
+
91
+ export async function subgraphBetween(
92
+ db: GBZBase,
93
+ start: number,
94
+ end: number,
95
+ opts: Pick<QueryOptions, 'haplotypes' | 'limit'> = {},
96
+ ) {
97
+ const haplotypes = opts.haplotypes ?? 'all'
98
+ if (haplotypes === 'reference-only') {
99
+ throw new Error('Cannot output a reference path in a node-based query')
100
+ }
101
+ const subgraph = new Subgraph(db)
102
+ subgraph.limit = opts.limit
103
+ await subgraph.betweenNodes(start, end)
77
104
  subgraph.extractPaths(undefined, haplotypes)
78
105
  return subgraph
79
106
  }
@@ -167,10 +167,15 @@ export class BTree {
167
167
  }
168
168
  }
169
169
  } else {
170
+ const children: number[] = []
170
171
  for (let i = 0; i < header.cellCount; i++) {
171
- yield* this.tableScan(readUint32(page, cellOffset(page, header, i)))
172
+ children.push(readUint32(page, cellOffset(page, header, i)))
173
+ }
174
+ children.push(header.rightChild)
175
+ this.pager.prefetch(children)
176
+ for (const child of children) {
177
+ yield* this.tableScan(child)
172
178
  }
173
- yield* this.tableScan(header.rightChild)
174
179
  }
175
180
  }
176
181