@gmod/gbz-base 1.0.0 → 2.0.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/db.ts CHANGED
@@ -1,23 +1,28 @@
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'
5
+ import { formatPathName, pathNameFor, toPathQuery } from './pathName.ts'
6
+ import { subgraphInInterval } from './query.ts'
4
7
  import { SqliteDatabase } from './sqlite/database.ts'
5
8
 
6
9
  import type { ByteSource } from './filehandle.ts'
7
10
  import type { Pos } from './gbwt/record.ts'
11
+ import type { GraphName } from './graphName.ts'
12
+ import type { PathName, PathRef } from './pathName.ts'
13
+ import type { QueryOptions } from './query.ts'
8
14
  import type { PagerOptions } from './sqlite/pager.ts'
9
15
  import type { SqlValue } from './sqlite/record.ts'
16
+ import type { HaplotypeAlignment, Subgraph } from './subgraph.ts'
10
17
 
11
- export interface PathName {
12
- sample: string
13
- contig: string
14
- haplotype: number
15
- fragment: number
16
- }
17
-
18
- export const GENERIC_SAMPLE = '_gbwt_ref'
19
18
  export const SCHEMA_VERSION = 'GBZ-base version 4'
20
19
 
20
+ export interface PathFragment {
21
+ path: GbzPath
22
+ start: number
23
+ end: number
24
+ }
25
+
21
26
  export class SchemaVersionError extends Error {
22
27
  override name = 'SchemaVersionError'
23
28
 
@@ -33,10 +38,6 @@ export class SchemaVersionError extends Error {
33
38
  }
34
39
  }
35
40
 
36
- export function formatPathName(name: PathName, end: number) {
37
- return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
38
- }
39
-
40
41
  export interface HaplotypeSample {
41
42
  node: number
42
43
  offset: number
@@ -144,37 +145,63 @@ function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
144
145
  }
145
146
  }
146
147
 
148
+ export interface OpenOptions extends PagerOptions {
149
+ haplotypeIndex?: ByteSource
150
+ }
151
+
152
+ async function readTags(sqlite: SqliteDatabase) {
153
+ const tags = new Map<string, string>()
154
+ for await (const { values } of sqlite.scan('Tags')) {
155
+ tags.set(str(values[0], 'Tags.key'), str(values[1], 'Tags.value'))
156
+ }
157
+ return tags
158
+ }
159
+
147
160
  export class GBZBase {
148
161
  private tagCache: Promise<Map<string, string>> | undefined
149
162
  private pathCache: Promise<GbzPath[]> | undefined
163
+ private indexTags: Map<string, string> | undefined
150
164
 
151
165
  readonly sqlite: SqliteDatabase
166
+ readonly index: SqliteDatabase
152
167
 
153
- private constructor(sqlite: SqliteDatabase) {
168
+ private constructor(sqlite: SqliteDatabase, index: SqliteDatabase) {
154
169
  this.sqlite = sqlite
170
+ this.index = index
155
171
  }
156
172
 
157
- static async open(source: ByteSource, opts: PagerOptions = {}) {
158
- const sqlite = await SqliteDatabase.open(source, opts)
173
+ static async open(source: ByteSource, opts: OpenOptions = {}) {
174
+ const { haplotypeIndex, ...pagerOptions } = opts
175
+ const sqlite = await SqliteDatabase.open(source, pagerOptions)
159
176
  for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
160
177
  sqlite.rootPage(table)
161
178
  }
162
- const db = new GBZBase(sqlite)
179
+ const index = haplotypeIndex
180
+ ? await SqliteDatabase.open(haplotypeIndex, pagerOptions)
181
+ : sqlite
182
+ const db = new GBZBase(sqlite, index)
163
183
  const version = await db.tag('version')
164
184
  if (version !== SCHEMA_VERSION) {
165
185
  throw new SchemaVersionError(version)
166
186
  }
187
+ if (haplotypeIndex) {
188
+ for (const table of ['Tags', 'HaplotypeSamples', 'HaplotypeLengths']) {
189
+ index.rootPage(table)
190
+ }
191
+ db.indexTags = await readTags(index)
192
+ const indexed = db.indexTags.get('haplotype_index_paths')
193
+ const paths = await db.tag('paths')
194
+ if (indexed !== paths) {
195
+ throw new Error(
196
+ `haplotype index was built for ${indexed ?? 'an unknown number of'} paths but the graph has ${paths}`,
197
+ )
198
+ }
199
+ }
167
200
  return db
168
201
  }
169
202
 
170
203
  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
- })()
204
+ this.tagCache ??= readTags(this.sqlite)
178
205
  return this.tagCache
179
206
  }
180
207
 
@@ -228,14 +255,182 @@ export class GBZBase {
228
255
  return (await this.paths()).filter(p => p.name.sample === sample)
229
256
  }
230
257
 
258
+ private async pathsNamed(ref: PathRef) {
259
+ const name = pathNameFor(toPathQuery(ref), 0)
260
+ return (await this.paths())
261
+ .filter(
262
+ p =>
263
+ p.name.sample === name.sample &&
264
+ p.name.contig === name.contig &&
265
+ p.name.haplotype === name.haplotype,
266
+ )
267
+ .sort((a, b) => a.name.fragment - b.name.fragment)
268
+ }
269
+
270
+ async hasPath(ref: PathRef) {
271
+ return (await this.pathsNamed(ref)).length > 0
272
+ }
273
+
274
+ private pathLengths = new Map<number, Promise<number>>()
275
+
276
+ pathLength(handle: number) {
277
+ let length = this.pathLengths.get(handle)
278
+ if (!length) {
279
+ length = this.walkPathLength(handle)
280
+ this.pathLengths.set(handle, length)
281
+ length.catch(() => this.pathLengths.delete(handle))
282
+ }
283
+ return length
284
+ }
285
+
286
+ private async walkPathLength(handle: number) {
287
+ const indexed = this.hasHaplotypeIndex
288
+ ? await this.haplotypeLength(handle)
289
+ : undefined
290
+ if (indexed === undefined) {
291
+ const last = await this.indexedPosition(handle, Number.MAX_SAFE_INTEGER)
292
+ if (!last) {
293
+ const path = await this.getPath(handle)
294
+ throw new Error(
295
+ `Path ${path ? formatPathName(path.name, path.name.fragment) : handle} has not been indexed for random access`,
296
+ )
297
+ }
298
+ let length = last.pathOffset
299
+ let pos = last.pos
300
+ for (;;) {
301
+ const record = await this.getRecord(pos.node)
302
+ if (!record) {
303
+ throw new Error(`Node ${pos.node} does not exist in the graph`)
304
+ }
305
+ length += record.sequenceLen
306
+ const next = record.gbwt().lf(pos.offset)
307
+ if (!next || next.node === ENDMARKER) {
308
+ return length
309
+ }
310
+ pos = next
311
+ }
312
+ }
313
+ return indexed
314
+ }
315
+
316
+ async pathFragmentsForRange(
317
+ ref: PathRef,
318
+ start: number,
319
+ end: number,
320
+ ): Promise<PathFragment[]> {
321
+ if (end <= start) {
322
+ return []
323
+ }
324
+ const ordered = await this.pathsNamed(ref)
325
+ const covering = ordered.filter(p => p.name.fragment <= start).slice(-1)
326
+ const within = ordered.filter(
327
+ p => p.name.fragment > start && p.name.fragment < end,
328
+ )
329
+ const fragments = await Promise.all(
330
+ [...covering, ...within].map(async path => ({
331
+ path,
332
+ start: path.name.fragment,
333
+ end: path.name.fragment + (await this.pathLength(path.handle)),
334
+ })),
335
+ )
336
+ return fragments.filter(f => start < f.end)
337
+ }
338
+
339
+ private async subgraphForFragment(
340
+ fragment: PathFragment,
341
+ start: number,
342
+ end: number,
343
+ opts: QueryOptions,
344
+ ) {
345
+ const lo = Math.max(start, fragment.start)
346
+ const hi = Math.min(end, fragment.end)
347
+ let subgraph: Subgraph | undefined
348
+ if (hi > lo) {
349
+ const { sample, contig, haplotype } = fragment.path.name
350
+ subgraph = await subgraphInInterval(
351
+ this,
352
+ { sample, contig, haplotype },
353
+ lo,
354
+ hi,
355
+ opts,
356
+ )
357
+ const haplotypes = opts.haplotypes ?? 'all'
358
+ const named = haplotypes === 'all' || haplotypes === 'distinct'
359
+ if (this.hasHaplotypeIndex && named) {
360
+ await subgraph.identifyPaths()
361
+ }
362
+ }
363
+ return subgraph
364
+ }
365
+
366
+ async getSubgraphForRange(
367
+ ref: PathRef,
368
+ start: number,
369
+ end: number,
370
+ opts: QueryOptions = {},
371
+ ) {
372
+ const [fragment] = await this.pathFragmentsForRange(ref, start, end)
373
+ return fragment
374
+ ? this.subgraphForFragment(fragment, start, end, opts)
375
+ : undefined
376
+ }
377
+
378
+ async getAlignmentsForRange(
379
+ ref: PathRef,
380
+ start: number,
381
+ end: number,
382
+ opts: QueryOptions = {},
383
+ ) {
384
+ const result: HaplotypeAlignment[] = []
385
+ for (const fragment of await this.pathFragmentsForRange(ref, start, end)) {
386
+ const subgraph = await this.subgraphForFragment(
387
+ fragment,
388
+ start,
389
+ end,
390
+ opts,
391
+ )
392
+ if (subgraph) {
393
+ result.push(...subgraph.alignments())
394
+ }
395
+ }
396
+ return result
397
+ }
398
+
399
+ async graphName() {
400
+ const gbzTags = new Map<string, string>()
401
+ for (const [key, value] of await this.tags()) {
402
+ if (key.startsWith('gbz_')) {
403
+ gbzTags.set(key.slice('gbz_'.length), value)
404
+ }
405
+ }
406
+ let name: GraphName = {
407
+ name: undefined,
408
+ subgraph: new Map(),
409
+ translation: new Map(),
410
+ }
411
+ try {
412
+ name = graphNameFromTags(gbzTags)
413
+ } catch {
414
+ // upstream falls back to an empty name when the tags do not parse
415
+ }
416
+ return name
417
+ }
418
+
419
+ async hasChainLinks() {
420
+ const links = await this.tag('chain_links')
421
+ return links !== undefined && Number(links) > 0
422
+ }
423
+
231
424
  get hasHaplotypeIndex() {
232
425
  return (
233
- this.sqlite.has('HaplotypeSamples') && this.sqlite.has('HaplotypeLengths')
426
+ this.index.has('HaplotypeSamples') && this.index.has('HaplotypeLengths')
234
427
  )
235
428
  }
236
429
 
237
430
  async haplotypeSampleInterval() {
238
- const value = await this.tag('haplotype_index_interval')
431
+ const value = this.indexTags
432
+ ? this.indexTags.get('haplotype_index_interval')
433
+ : await this.tag('haplotype_index_interval')
239
434
  return value === undefined ? undefined : Number(value)
240
435
  }
241
436
 
@@ -254,7 +449,7 @@ export class GBZBase {
254
449
 
255
450
  async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
256
451
  const samples: HaplotypeSample[] = []
257
- for await (const key of this.sqlite.indexScanFrom('HaplotypeSamples', [
452
+ for await (const key of this.index.indexScanFrom('HaplotypeSamples', [
258
453
  minHandle,
259
454
  0,
260
455
  ])) {
@@ -262,7 +457,7 @@ export class GBZBase {
262
457
  if (node > maxHandle) {
263
458
  break
264
459
  }
265
- const row = await this.sqlite.byRowid(
460
+ const row = await this.index.byRowid(
266
461
  'HaplotypeSamples',
267
462
  num(key[2], 'HaplotypeSamples rowid'),
268
463
  )
@@ -274,14 +469,11 @@ export class GBZBase {
274
469
  }
275
470
 
276
471
  async haplotypeSampleAt(node: number, offset: number) {
277
- const key = await this.sqlite.indexSeekLE('HaplotypeSamples', [
278
- node,
279
- offset,
280
- ])
472
+ const key = await this.index.indexSeekLE('HaplotypeSamples', [node, offset])
281
473
  if (key?.[0] !== node || key[1] !== offset) {
282
474
  return undefined
283
475
  }
284
- const row = await this.sqlite.byRowid(
476
+ const row = await this.index.byRowid(
285
477
  'HaplotypeSamples',
286
478
  num(key[2], 'HaplotypeSamples rowid'),
287
479
  )
@@ -289,7 +481,7 @@ export class GBZBase {
289
481
  }
290
482
 
291
483
  async haplotypeLength(pathHandle: number) {
292
- const row = await this.sqlite.byRowid('HaplotypeLengths', pathHandle)
484
+ const row = await this.index.byRowid('HaplotypeLengths', pathHandle)
293
485
  return row ? num(row[1], 'HaplotypeLengths.length') : undefined
294
486
  }
295
487
 
@@ -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
@@ -1,31 +1,36 @@
1
- export {
2
- GBZBase,
3
- GENERIC_SAMPLE,
4
- GbzRecord,
5
- SCHEMA_VERSION,
6
- SchemaVersionError,
7
- formatPathName,
1
+ export { GBZBase, GbzRecord, SCHEMA_VERSION, SchemaVersionError } from './db.ts'
2
+ export type {
3
+ GbzPath,
4
+ HaplotypeSample,
5
+ OpenOptions,
6
+ PathFragment,
8
7
  } from './db.ts'
9
- export type { GbzPath, HaplotypeSample, PathName } from './db.ts'
8
+ export { GENERIC_SAMPLE, formatPathName, parsePathName } from './pathName.ts'
9
+ export type { PathName, PathQuery, PathRef } from './pathName.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'
13
13
  export type {
14
+ AlignmentSpan,
14
15
  HaplotypeAlignment,
15
16
  HaplotypeOutput,
16
17
  PathIdentity,
17
18
  PathPosition,
18
19
  ReferencePath,
20
+ SnarlOutput,
19
21
  SubgraphJson,
22
+ SubgraphOptions,
23
+ SubgraphOutputOptions,
20
24
  SubgraphPath,
21
- ToJsonOptions,
22
25
  } from './subgraph.ts'
23
26
  export {
24
27
  subgraphAroundNodes,
25
28
  subgraphAtOffset,
29
+ subgraphBetween,
26
30
  subgraphInInterval,
27
31
  } from './query.ts'
28
- export type { PathQuery, QueryOptions } from './query.ts'
32
+ export type { QueryOptions } from './query.ts'
29
33
  export { SqliteDatabase } from './sqlite/database.ts'
34
+ export type { GraphName } from './graphName.ts'
30
35
  export { weightedLcs } from './lcs.ts'
31
36
  export * as nodes from './gbwt/node.ts'
@@ -0,0 +1,54 @@
1
+ export interface PathName {
2
+ sample: string
3
+ contig: string
4
+ haplotype: number
5
+ fragment: number
6
+ }
7
+
8
+ export interface PathQuery {
9
+ sample?: string
10
+ contig: string
11
+ haplotype?: number
12
+ }
13
+
14
+ export type PathRef = string | PathQuery
15
+
16
+ export const GENERIC_SAMPLE = '_gbwt_ref'
17
+
18
+ export function formatPathName(name: PathName, end: number) {
19
+ return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
20
+ }
21
+
22
+ const PAN_SN = /^([^#]+)#(\d+)#([^#]+?)(?:\[\d+-\d+\])?$/
23
+ const BARE_CONTIG = /^([^#]+?)(?:\[\d+-\d+\])?$/
24
+
25
+ export function parsePathName(name: string): PathQuery {
26
+ const panSn = PAN_SN.exec(name)
27
+ if (panSn) {
28
+ return {
29
+ sample: panSn[1]!,
30
+ haplotype: Number(panSn[2]),
31
+ contig: panSn[3]!,
32
+ }
33
+ }
34
+ const bare = BARE_CONTIG.exec(name)
35
+ if (bare) {
36
+ return { contig: bare[1]! }
37
+ }
38
+ throw new Error(
39
+ `"${name}" is not a path name; expected "sample#haplotype#contig" or a bare contig`,
40
+ )
41
+ }
42
+
43
+ export function toPathQuery(ref: PathRef) {
44
+ return typeof ref === 'string' ? parsePathName(ref) : ref
45
+ }
46
+
47
+ export function pathNameFor(query: PathQuery, fragment: number): PathName {
48
+ return {
49
+ sample: query.sample ?? GENERIC_SAMPLE,
50
+ contig: query.contig,
51
+ haplotype: query.haplotype ?? 0,
52
+ fragment,
53
+ }
54
+ }
package/src/query.ts CHANGED
@@ -1,28 +1,18 @@
1
- import { GENERIC_SAMPLE } from './db.ts'
1
+ import { pathNameFor } from './pathName.ts'
2
2
  import { Subgraph } from './subgraph.ts'
3
3
 
4
- import type { GBZBase, PathName } from './db.ts'
5
- import type { HaplotypeOutput } from './subgraph.ts'
4
+ import type { GBZBase } from './db.ts'
5
+ import type { PathQuery } from './pathName.ts'
6
+ import type { HaplotypeOutput, SnarlOutput } from './subgraph.ts'
6
7
 
7
- export interface QueryOptions {
8
- context?: number
9
- haplotypes?: HaplotypeOutput
10
- limit?: number
11
- }
8
+ export type { PathQuery } from './pathName.ts'
12
9
 
13
- export interface PathQuery {
14
- sample?: string
15
- contig: string
16
- haplotype?: number
17
- }
18
-
19
- function pathName(query: PathQuery, fragment: number): PathName {
20
- return {
21
- sample: query.sample ?? GENERIC_SAMPLE,
22
- contig: query.contig,
23
- haplotype: query.haplotype ?? 0,
24
- fragment,
25
- }
10
+ export interface QueryOptions {
11
+ context?: number | undefined
12
+ haplotypes?: HaplotypeOutput | undefined
13
+ snarls?: SnarlOutput | undefined
14
+ limit?: number | undefined
15
+ signal?: AbortSignal | undefined
26
16
  }
27
17
 
28
18
  export async function subgraphAtOffset(
@@ -31,14 +21,14 @@ export async function subgraphAtOffset(
31
21
  offset: number,
32
22
  opts: QueryOptions = {},
33
23
  ) {
34
- const subgraph = new Subgraph(db)
35
- subgraph.limit = opts.limit
36
- const reference = await subgraph.pathPosition(pathName(query, offset))
24
+ const subgraph = new Subgraph(db, opts)
25
+ const reference = await subgraph.pathPosition(pathNameFor(query, offset))
37
26
  await subgraph.aroundPosition(
38
27
  reference.position.handle,
39
28
  reference.position.nodeOffset,
40
29
  opts.context ?? 100,
41
30
  )
31
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
42
32
  subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
43
33
  return subgraph
44
34
  }
@@ -50,14 +40,14 @@ export async function subgraphInInterval(
50
40
  end: number,
51
41
  opts: QueryOptions = {},
52
42
  ) {
53
- const subgraph = new Subgraph(db)
54
- subgraph.limit = opts.limit
55
- const reference = await subgraph.pathPosition(pathName(query, start))
43
+ const subgraph = new Subgraph(db, opts)
44
+ const reference = await subgraph.pathPosition(pathNameFor(query, start))
56
45
  await subgraph.aroundInterval(
57
46
  reference.position,
58
47
  end - start,
59
48
  opts.context ?? 100,
60
49
  )
50
+ await subgraph.extractSnarls(opts.snarls ?? 'none')
61
51
  subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
62
52
  return subgraph
63
53
  }
@@ -68,12 +58,34 @@ export async function subgraphAroundNodes(
68
58
  opts: QueryOptions = {},
69
59
  ) {
70
60
  const haplotypes = opts.haplotypes ?? 'all'
61
+ const snarls = opts.snarls ?? 'none'
71
62
  if (haplotypes === 'reference-only') {
72
63
  throw new Error('Cannot output a reference path in a node-based query')
73
64
  }
74
- const subgraph = new Subgraph(db)
75
- subgraph.limit = opts.limit
65
+ if (snarls === 'overlapping' && nodes.length > 1) {
66
+ throw new Error(
67
+ 'Overlapping snarls cannot be extracted for a node-based query with multiple nodes',
68
+ )
69
+ }
70
+ const subgraph = new Subgraph(db, opts)
76
71
  await subgraph.aroundNodes(nodes, opts.context ?? 100)
72
+ await subgraph.extractSnarls(snarls)
73
+ subgraph.extractPaths(undefined, haplotypes)
74
+ return subgraph
75
+ }
76
+
77
+ export async function subgraphBetween(
78
+ db: GBZBase,
79
+ start: number,
80
+ end: number,
81
+ opts: Pick<QueryOptions, 'haplotypes' | 'limit' | 'signal'> = {},
82
+ ) {
83
+ const haplotypes = opts.haplotypes ?? 'all'
84
+ if (haplotypes === 'reference-only') {
85
+ throw new Error('Cannot output a reference path in a node-based query')
86
+ }
87
+ const subgraph = new Subgraph(db, opts)
88
+ await subgraph.betweenNodes(start, end)
77
89
  subgraph.extractPaths(undefined, haplotypes)
78
90
  return subgraph
79
91
  }