@gmod/gbz-base 0.0.1 → 1.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/README.md +3 -3
- package/bin/query.js +3 -1
- package/dist/cli.js +26 -7
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +5 -5
- package/dist/db.js +35 -14
- package/dist/db.js.map +1 -0
- package/dist/filehandle.js +1 -0
- package/dist/filehandle.js.map +1 -0
- package/dist/gbwt/bytecode.d.ts +1 -1
- package/dist/gbwt/bytecode.js +7 -3
- package/dist/gbwt/bytecode.js.map +1 -0
- package/dist/gbwt/node.js +7 -2
- package/dist/gbwt/node.js.map +1 -0
- package/dist/gbwt/record.js +8 -2
- package/dist/gbwt/record.js.map +1 -0
- package/dist/gbwt/sequence.js +8 -1
- package/dist/gbwt/sequence.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -0
- package/dist/lcs.js +32 -10
- package/dist/lcs.js.map +1 -0
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -0
- package/dist/query.js.map +1 -0
- package/dist/sqlite/btree.d.ts +1 -1
- package/dist/sqlite/btree.js +17 -5
- package/dist/sqlite/btree.js.map +1 -0
- package/dist/sqlite/database.d.ts +1 -1
- package/dist/sqlite/database.js +12 -2
- package/dist/sqlite/database.js.map +1 -0
- package/dist/sqlite/pager.d.ts +3 -3
- package/dist/sqlite/pager.js +6 -4
- package/dist/sqlite/pager.js.map +1 -0
- package/dist/sqlite/record.js +3 -1
- package/dist/sqlite/record.js.map +1 -0
- package/dist/subgraph.d.ts +2 -2
- package/dist/subgraph.js +116 -29
- package/dist/subgraph.js.map +1 -0
- package/package.json +36 -11
- package/src/cli.ts +203 -0
- package/src/db.ts +322 -0
- package/src/filehandle.ts +4 -0
- package/src/gbwt/bytecode.ts +87 -0
- package/src/gbwt/node.ts +64 -0
- package/src/gbwt/record.ts +154 -0
- package/src/gbwt/sequence.ts +50 -0
- package/src/index.ts +31 -0
- package/src/lcs.ts +282 -0
- package/src/query.ts +79 -0
- package/src/sqlite/btree.ts +255 -0
- package/src/sqlite/database.ts +117 -0
- package/src/sqlite/pager.ts +78 -0
- package/src/sqlite/record.ts +100 -0
- package/src/subgraph.ts +1066 -0
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmod/gbz-base",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Pure TypeScript reader for gbz-base pangenome databases (.gbz.db) with random access over HTTP range requests",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"packageManager": "pnpm@11.15.1",
|
|
6
7
|
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "Colin Diesh",
|
|
11
|
+
"email": "colin.diesh@gmail.com",
|
|
12
|
+
"url": "https://github.com/cmdcolin"
|
|
13
|
+
},
|
|
7
14
|
"main": "dist/index.js",
|
|
8
15
|
"types": "dist/index.d.ts",
|
|
9
16
|
"exports": {
|
|
@@ -18,21 +25,44 @@
|
|
|
18
25
|
},
|
|
19
26
|
"files": [
|
|
20
27
|
"dist",
|
|
28
|
+
"src",
|
|
21
29
|
"bin",
|
|
22
30
|
"tools/haplotype-index/Cargo.toml",
|
|
23
31
|
"tools/haplotype-index/src"
|
|
24
32
|
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"coverage": "pnpm test --coverage",
|
|
36
|
+
"clean": "rimraf dist",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
39
|
+
"lint": "eslint --report-unused-disable-directives --max-warnings 0",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.lint.json",
|
|
41
|
+
"prebuild": "pnpm clean",
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test --run && pnpm build",
|
|
44
|
+
"version": "git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add CHANGELOG.md",
|
|
45
|
+
"postversion": "git push --follow-tags"
|
|
46
|
+
},
|
|
25
47
|
"dependencies": {
|
|
26
48
|
"generic-filehandle2": "^2.0.0"
|
|
27
49
|
},
|
|
28
50
|
"devDependencies": {
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"vitest": "^
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
52
|
+
"@types/node": "^25.9.5",
|
|
53
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
54
|
+
"eslint": "^10.8.0",
|
|
55
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
56
|
+
"git-cliff": "^2.13.1",
|
|
57
|
+
"prettier": "^3.9.6",
|
|
58
|
+
"rimraf": "^6.1.3",
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"typescript-eslint": "^8.65.0",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
32
62
|
},
|
|
33
63
|
"repository": {
|
|
34
64
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/GMOD/gbz-base-js.git"
|
|
65
|
+
"url": "git+https://github.com/GMOD/gbz-base-js.git"
|
|
36
66
|
},
|
|
37
67
|
"keywords": [
|
|
38
68
|
"pangenome",
|
|
@@ -44,10 +74,5 @@
|
|
|
44
74
|
],
|
|
45
75
|
"publishConfig": {
|
|
46
76
|
"access": "public"
|
|
47
|
-
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"build": "tsc -p tsconfig.build.json",
|
|
50
|
-
"test": "vitest run",
|
|
51
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
52
77
|
}
|
|
53
|
-
}
|
|
78
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { LocalFile, RemoteFile } from 'generic-filehandle2'
|
|
2
|
+
|
|
3
|
+
import { GBZBase, formatPathName } from './db.ts'
|
|
4
|
+
import {
|
|
5
|
+
subgraphAroundNodes,
|
|
6
|
+
subgraphAtOffset,
|
|
7
|
+
subgraphInInterval,
|
|
8
|
+
} from './query.ts'
|
|
9
|
+
|
|
10
|
+
import type { HaplotypeOutput } from './subgraph.ts'
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage: gbz-base-query [options] graph.gbz.db
|
|
13
|
+
|
|
14
|
+
--sample STR sample name (default: generic path)
|
|
15
|
+
--contig STR contig name (required for --offset and --interval)
|
|
16
|
+
--haplotype INT haplotype number (default: 0)
|
|
17
|
+
-o, --offset INT sequence offset
|
|
18
|
+
-i, --interval A..B half-open sequence interval
|
|
19
|
+
-n, --node INT node identifier (may repeat)
|
|
20
|
+
--context INT context length in bp (default: 100)
|
|
21
|
+
--limit INT safety limit for the number of nodes
|
|
22
|
+
--haplotypes SEL all, distinct, reference-only or none (default: all)
|
|
23
|
+
--cigar output CIGAR strings for the haplotypes
|
|
24
|
+
--resolve name haplotypes from the HaplotypeSamples table
|
|
25
|
+
--alignments print one alignment record per haplotype fragment instead of the subgraph
|
|
26
|
+
--block-size INT bytes fetched per range request (default: 65536)
|
|
27
|
+
--stats print fetch statistics to stderr
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
interface Args {
|
|
31
|
+
file: string
|
|
32
|
+
sample?: string
|
|
33
|
+
contig?: string
|
|
34
|
+
haplotype: number
|
|
35
|
+
offset?: number
|
|
36
|
+
interval?: [number, number]
|
|
37
|
+
nodes: number[]
|
|
38
|
+
context: number
|
|
39
|
+
limit?: number
|
|
40
|
+
haplotypes: HaplotypeOutput
|
|
41
|
+
cigar: boolean
|
|
42
|
+
resolve: boolean
|
|
43
|
+
alignments: boolean
|
|
44
|
+
blockSize: number
|
|
45
|
+
stats: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv: string[]): Args {
|
|
49
|
+
const args: Args = {
|
|
50
|
+
file: '',
|
|
51
|
+
haplotype: 0,
|
|
52
|
+
nodes: [],
|
|
53
|
+
context: 100,
|
|
54
|
+
haplotypes: 'all',
|
|
55
|
+
cigar: false,
|
|
56
|
+
resolve: false,
|
|
57
|
+
alignments: false,
|
|
58
|
+
blockSize: 65536,
|
|
59
|
+
stats: false,
|
|
60
|
+
}
|
|
61
|
+
const next = (i: number) => {
|
|
62
|
+
const value = argv[i + 1]
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
throw new Error(`${argv[i]} needs a value`)
|
|
65
|
+
}
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const arg = argv[i]!
|
|
70
|
+
switch (arg) {
|
|
71
|
+
case '--sample':
|
|
72
|
+
args.sample = next(i++)
|
|
73
|
+
break
|
|
74
|
+
case '--contig':
|
|
75
|
+
args.contig = next(i++)
|
|
76
|
+
break
|
|
77
|
+
case '--haplotype':
|
|
78
|
+
args.haplotype = Number(next(i++))
|
|
79
|
+
break
|
|
80
|
+
case '-o':
|
|
81
|
+
case '--offset':
|
|
82
|
+
args.offset = Number(next(i++))
|
|
83
|
+
break
|
|
84
|
+
case '-i':
|
|
85
|
+
case '--interval': {
|
|
86
|
+
const [a, b] = next(i++).split('..')
|
|
87
|
+
args.interval = [Number(a), Number(b)]
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
case '-n':
|
|
91
|
+
case '--node':
|
|
92
|
+
args.nodes.push(Number(next(i++)))
|
|
93
|
+
break
|
|
94
|
+
case '--context':
|
|
95
|
+
args.context = Number(next(i++))
|
|
96
|
+
break
|
|
97
|
+
case '--limit':
|
|
98
|
+
args.limit = Number(next(i++))
|
|
99
|
+
break
|
|
100
|
+
case '--haplotypes':
|
|
101
|
+
args.haplotypes = next(i++) as HaplotypeOutput
|
|
102
|
+
break
|
|
103
|
+
case '--cigar':
|
|
104
|
+
args.cigar = true
|
|
105
|
+
break
|
|
106
|
+
case '--resolve':
|
|
107
|
+
args.resolve = true
|
|
108
|
+
break
|
|
109
|
+
case '--alignments':
|
|
110
|
+
args.alignments = true
|
|
111
|
+
args.resolve = true
|
|
112
|
+
break
|
|
113
|
+
case '--block-size':
|
|
114
|
+
args.blockSize = Number(next(i++))
|
|
115
|
+
break
|
|
116
|
+
case '--stats':
|
|
117
|
+
args.stats = true
|
|
118
|
+
break
|
|
119
|
+
case '--format':
|
|
120
|
+
next(i++)
|
|
121
|
+
break
|
|
122
|
+
case '-h':
|
|
123
|
+
case '--help':
|
|
124
|
+
process.stdout.write(USAGE)
|
|
125
|
+
process.exit(0)
|
|
126
|
+
break
|
|
127
|
+
default:
|
|
128
|
+
if (arg.startsWith('-')) {
|
|
129
|
+
throw new Error(`Unknown option ${arg}`)
|
|
130
|
+
}
|
|
131
|
+
args.file = arg
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!args.file) {
|
|
135
|
+
throw new Error(USAGE)
|
|
136
|
+
}
|
|
137
|
+
return args
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function main(argv: string[]) {
|
|
141
|
+
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 })
|
|
146
|
+
const opts = {
|
|
147
|
+
context: args.context,
|
|
148
|
+
haplotypes: args.haplotypes,
|
|
149
|
+
...(args.limit === undefined ? {} : { limit: args.limit }),
|
|
150
|
+
}
|
|
151
|
+
const query = {
|
|
152
|
+
contig: args.contig ?? '',
|
|
153
|
+
haplotype: args.haplotype,
|
|
154
|
+
...(args.sample === undefined ? {} : { sample: args.sample }),
|
|
155
|
+
}
|
|
156
|
+
const subgraph =
|
|
157
|
+
args.nodes.length > 0
|
|
158
|
+
? await subgraphAroundNodes(db, args.nodes, opts)
|
|
159
|
+
: args.interval
|
|
160
|
+
? await subgraphInInterval(
|
|
161
|
+
db,
|
|
162
|
+
query,
|
|
163
|
+
args.interval[0],
|
|
164
|
+
args.interval[1],
|
|
165
|
+
opts,
|
|
166
|
+
)
|
|
167
|
+
: args.offset !== undefined
|
|
168
|
+
? await subgraphAtOffset(db, query, args.offset, opts)
|
|
169
|
+
: undefined
|
|
170
|
+
if (!subgraph) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'Query type must be specified using --offset, --interval or --node',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
if (args.resolve) {
|
|
176
|
+
await subgraph.identifyPaths()
|
|
177
|
+
}
|
|
178
|
+
const output = args.alignments
|
|
179
|
+
? subgraph.alignments().map(a => ({
|
|
180
|
+
...a,
|
|
181
|
+
name:
|
|
182
|
+
a.name && a.hapStart !== undefined && a.hapEnd !== undefined
|
|
183
|
+
? formatPathName({ ...a.name, fragment: a.hapStart }, a.hapEnd)
|
|
184
|
+
: undefined,
|
|
185
|
+
start: undefined,
|
|
186
|
+
}))
|
|
187
|
+
: subgraph.toJSON(args.cigar, {
|
|
188
|
+
names: args.resolve ? 'resolved' : 'anonymous',
|
|
189
|
+
})
|
|
190
|
+
process.stdout.write(`${JSON.stringify(output)}\n`)
|
|
191
|
+
if (args.stats) {
|
|
192
|
+
const { fetches, bytesFetched } = db.sqlite.pager
|
|
193
|
+
const {
|
|
194
|
+
orderedAlignments,
|
|
195
|
+
lcsAlignments,
|
|
196
|
+
identificationSteps,
|
|
197
|
+
identificationFetches,
|
|
198
|
+
} = subgraph.stats
|
|
199
|
+
process.stderr.write(
|
|
200
|
+
`Subgraph contains ${subgraph.nodeCount} nodes and ${subgraph.pathCount} paths; ${fetches} fetches, ${bytesFetched} bytes; ${orderedAlignments} ordered + ${lcsAlignments} lcs alignments; identification ${identificationSteps} steps, ${identificationFetches} lookups\n`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { ENDMARKER, nodeId, nodeOrientation } from './gbwt/node.ts'
|
|
2
|
+
import { GbwtRecord, decompressEdges } from './gbwt/record.ts'
|
|
3
|
+
import { decodeSequence, encodedSequenceLength } from './gbwt/sequence.ts'
|
|
4
|
+
import { SqliteDatabase } from './sqlite/database.ts'
|
|
5
|
+
|
|
6
|
+
import type { ByteSource } from './filehandle.ts'
|
|
7
|
+
import type { Pos } from './gbwt/record.ts'
|
|
8
|
+
import type { PagerOptions } from './sqlite/pager.ts'
|
|
9
|
+
import type { SqlValue } from './sqlite/record.ts'
|
|
10
|
+
|
|
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
|
+
export const SCHEMA_VERSION = 'GBZ-base version 4'
|
|
20
|
+
|
|
21
|
+
export class SchemaVersionError extends Error {
|
|
22
|
+
override name = 'SchemaVersionError'
|
|
23
|
+
|
|
24
|
+
readonly found: string | undefined
|
|
25
|
+
|
|
26
|
+
constructor(found: string | undefined) {
|
|
27
|
+
super(
|
|
28
|
+
found === undefined
|
|
29
|
+
? `not a gbz-base database: its Tags table has no version`
|
|
30
|
+
: `unsupported database schema "${found}"; this reader understands "${SCHEMA_VERSION}"`,
|
|
31
|
+
)
|
|
32
|
+
this.found = found
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatPathName(name: PathName, end: number) {
|
|
37
|
+
return `${name.sample}#${name.haplotype}#${name.contig}[${name.fragment}-${end}]`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HaplotypeSample {
|
|
41
|
+
node: number
|
|
42
|
+
offset: number
|
|
43
|
+
pathHandle: number
|
|
44
|
+
orientation: 'forward' | 'reverse'
|
|
45
|
+
pathOffset: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GbzPath {
|
|
49
|
+
handle: number
|
|
50
|
+
fwStart: Pos
|
|
51
|
+
revStart: Pos
|
|
52
|
+
name: PathName
|
|
53
|
+
isIndexed: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class GbzRecord {
|
|
57
|
+
readonly sequenceLen: number
|
|
58
|
+
readonly handle: number
|
|
59
|
+
readonly edges: Pos[]
|
|
60
|
+
readonly bwt: Uint8Array
|
|
61
|
+
readonly encodedSequence: Uint8Array
|
|
62
|
+
readonly next: number | undefined
|
|
63
|
+
private decoded: string | undefined
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
handle: number,
|
|
67
|
+
edges: Pos[],
|
|
68
|
+
bwt: Uint8Array,
|
|
69
|
+
encodedSequence: Uint8Array,
|
|
70
|
+
next: number | undefined,
|
|
71
|
+
) {
|
|
72
|
+
this.handle = handle
|
|
73
|
+
this.edges = edges
|
|
74
|
+
this.bwt = bwt
|
|
75
|
+
this.encodedSequence = encodedSequence
|
|
76
|
+
this.next = next
|
|
77
|
+
this.sequenceLen = encodedSequenceLength(encodedSequence)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get id() {
|
|
81
|
+
return nodeId(this.handle)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get orientation() {
|
|
85
|
+
return nodeOrientation(this.handle)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get sequence() {
|
|
89
|
+
this.decoded ??= decodeSequence(this.encodedSequence)
|
|
90
|
+
return this.decoded
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
successors() {
|
|
94
|
+
return this.edges.filter(e => e.node !== ENDMARKER).map(e => e.node)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
gbwt() {
|
|
98
|
+
if (this.edges.length === 0) {
|
|
99
|
+
throw new Error(`GBWT record for handle ${this.handle} is empty`)
|
|
100
|
+
}
|
|
101
|
+
return new GbwtRecord(this.edges, this.bwt)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function num(value: SqlValue | undefined, what: string) {
|
|
106
|
+
if (typeof value !== 'number') {
|
|
107
|
+
throw new Error(`${what} is not a number in the database`)
|
|
108
|
+
}
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function str(value: SqlValue | undefined, what: string) {
|
|
113
|
+
if (typeof value !== 'string') {
|
|
114
|
+
throw new Error(`${what} is not text in the database`)
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function blob(value: SqlValue | undefined, what: string) {
|
|
120
|
+
if (!(value instanceof Uint8Array)) {
|
|
121
|
+
throw new Error(`${what} is not a blob in the database`)
|
|
122
|
+
}
|
|
123
|
+
return value
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function rowToPath(rowid: number, values: SqlValue[]): GbzPath {
|
|
127
|
+
return {
|
|
128
|
+
handle: rowid,
|
|
129
|
+
fwStart: {
|
|
130
|
+
node: num(values[1], 'Paths.fw_node'),
|
|
131
|
+
offset: num(values[2], 'Paths.fw_offset'),
|
|
132
|
+
},
|
|
133
|
+
revStart: {
|
|
134
|
+
node: num(values[3], 'Paths.rev_node'),
|
|
135
|
+
offset: num(values[4], 'Paths.rev_offset'),
|
|
136
|
+
},
|
|
137
|
+
name: {
|
|
138
|
+
sample: str(values[5], 'Paths.sample'),
|
|
139
|
+
contig: str(values[6], 'Paths.contig'),
|
|
140
|
+
haplotype: num(values[7], 'Paths.haplotype'),
|
|
141
|
+
fragment: num(values[8], 'Paths.fragment'),
|
|
142
|
+
},
|
|
143
|
+
isIndexed: num(values[9], 'Paths.is_indexed') !== 0,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class GBZBase {
|
|
148
|
+
private tagCache: Promise<Map<string, string>> | undefined
|
|
149
|
+
private pathCache: Promise<GbzPath[]> | undefined
|
|
150
|
+
|
|
151
|
+
readonly sqlite: SqliteDatabase
|
|
152
|
+
|
|
153
|
+
private constructor(sqlite: SqliteDatabase) {
|
|
154
|
+
this.sqlite = sqlite
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static async open(source: ByteSource, opts: PagerOptions = {}) {
|
|
158
|
+
const sqlite = await SqliteDatabase.open(source, opts)
|
|
159
|
+
for (const table of ['Tags', 'Nodes', 'Paths', 'ReferenceIndex']) {
|
|
160
|
+
sqlite.rootPage(table)
|
|
161
|
+
}
|
|
162
|
+
const db = new GBZBase(sqlite)
|
|
163
|
+
const version = await db.tag('version')
|
|
164
|
+
if (version !== SCHEMA_VERSION) {
|
|
165
|
+
throw new SchemaVersionError(version)
|
|
166
|
+
}
|
|
167
|
+
return db
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
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
|
+
})()
|
|
178
|
+
return this.tagCache
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async tag(key: string) {
|
|
182
|
+
return (await this.tags()).get(key)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getRecord(handle: number) {
|
|
186
|
+
const row = await this.sqlite.byRowid('Nodes', handle)
|
|
187
|
+
if (!row) {
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
const next = row[4]
|
|
191
|
+
return new GbzRecord(
|
|
192
|
+
handle,
|
|
193
|
+
decompressEdges(blob(row[1], 'Nodes.edges')),
|
|
194
|
+
blob(row[2], 'Nodes.bwt'),
|
|
195
|
+
blob(row[3], 'Nodes.sequence'),
|
|
196
|
+
typeof next === 'number' ? next : undefined,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
paths() {
|
|
201
|
+
this.pathCache ??= (async () => {
|
|
202
|
+
const paths: GbzPath[] = []
|
|
203
|
+
for await (const { rowid, values } of this.sqlite.scan('Paths')) {
|
|
204
|
+
paths.push(rowToPath(rowid, values))
|
|
205
|
+
}
|
|
206
|
+
return paths
|
|
207
|
+
})()
|
|
208
|
+
return this.pathCache
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getPath(handle: number) {
|
|
212
|
+
const row = await this.sqlite.byRowid('Paths', handle)
|
|
213
|
+
return row ? rowToPath(handle, row) : undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async findPath(name: PathName) {
|
|
217
|
+
const candidates = (await this.paths()).filter(
|
|
218
|
+
p =>
|
|
219
|
+
p.name.sample === name.sample &&
|
|
220
|
+
p.name.contig === name.contig &&
|
|
221
|
+
p.name.haplotype === name.haplotype &&
|
|
222
|
+
p.name.fragment <= name.fragment,
|
|
223
|
+
)
|
|
224
|
+
return candidates.sort((a, b) => b.name.fragment - a.name.fragment)[0]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async pathsForSample(sample: string) {
|
|
228
|
+
return (await this.paths()).filter(p => p.name.sample === sample)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get hasHaplotypeIndex() {
|
|
232
|
+
return (
|
|
233
|
+
this.sqlite.has('HaplotypeSamples') && this.sqlite.has('HaplotypeLengths')
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async haplotypeSampleInterval() {
|
|
238
|
+
const value = await this.tag('haplotype_index_interval')
|
|
239
|
+
return value === undefined ? undefined : Number(value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private sampleFromRow(values: SqlValue[]): HaplotypeSample {
|
|
243
|
+
return {
|
|
244
|
+
node: num(values[0], 'HaplotypeSamples.node_handle'),
|
|
245
|
+
offset: num(values[1], 'HaplotypeSamples.node_offset'),
|
|
246
|
+
pathHandle: num(values[2], 'HaplotypeSamples.path_handle'),
|
|
247
|
+
orientation:
|
|
248
|
+
num(values[3], 'HaplotypeSamples.orientation') === 0
|
|
249
|
+
? 'forward'
|
|
250
|
+
: 'reverse',
|
|
251
|
+
pathOffset: num(values[4], 'HaplotypeSamples.path_offset'),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async haplotypeSamplesInRange(minHandle: number, maxHandle: number) {
|
|
256
|
+
const samples: HaplotypeSample[] = []
|
|
257
|
+
for await (const key of this.sqlite.indexScanFrom('HaplotypeSamples', [
|
|
258
|
+
minHandle,
|
|
259
|
+
0,
|
|
260
|
+
])) {
|
|
261
|
+
const node = num(key[0], 'HaplotypeSamples.node_handle')
|
|
262
|
+
if (node > maxHandle) {
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
const row = await this.sqlite.byRowid(
|
|
266
|
+
'HaplotypeSamples',
|
|
267
|
+
num(key[2], 'HaplotypeSamples rowid'),
|
|
268
|
+
)
|
|
269
|
+
if (row) {
|
|
270
|
+
samples.push(this.sampleFromRow(row))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return samples
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async haplotypeSampleAt(node: number, offset: number) {
|
|
277
|
+
const key = await this.sqlite.indexSeekLE('HaplotypeSamples', [
|
|
278
|
+
node,
|
|
279
|
+
offset,
|
|
280
|
+
])
|
|
281
|
+
if (key?.[0] !== node || key[1] !== offset) {
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
const row = await this.sqlite.byRowid(
|
|
285
|
+
'HaplotypeSamples',
|
|
286
|
+
num(key[2], 'HaplotypeSamples rowid'),
|
|
287
|
+
)
|
|
288
|
+
return row ? this.sampleFromRow(row) : undefined
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async haplotypeLength(pathHandle: number) {
|
|
292
|
+
const row = await this.sqlite.byRowid('HaplotypeLengths', pathHandle)
|
|
293
|
+
return row ? num(row[1], 'HaplotypeLengths.length') : undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async indexedPosition(
|
|
297
|
+
pathHandle: number,
|
|
298
|
+
pathOffset: number,
|
|
299
|
+
): Promise<{ pathOffset: number; pos: Pos } | undefined> {
|
|
300
|
+
const key = await this.sqlite.indexSeekLE('ReferenceIndex', [
|
|
301
|
+
pathHandle,
|
|
302
|
+
pathOffset,
|
|
303
|
+
])
|
|
304
|
+
if (key?.[0] !== pathHandle) {
|
|
305
|
+
return undefined
|
|
306
|
+
}
|
|
307
|
+
const row = await this.sqlite.byRowid(
|
|
308
|
+
'ReferenceIndex',
|
|
309
|
+
num(key[2], 'ReferenceIndex rowid'),
|
|
310
|
+
)
|
|
311
|
+
if (!row) {
|
|
312
|
+
throw new Error('ReferenceIndex row referenced by its index is missing')
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
pathOffset: num(row[1], 'ReferenceIndex.path_offset'),
|
|
316
|
+
pos: {
|
|
317
|
+
node: num(row[2], 'ReferenceIndex.node_handle'),
|
|
318
|
+
offset: num(row[3], 'ReferenceIndex.node_offset'),
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class ByteCodeReader {
|
|
2
|
+
offset = 0
|
|
3
|
+
private bytes: Uint8Array
|
|
4
|
+
|
|
5
|
+
constructor(bytes: Uint8Array) {
|
|
6
|
+
this.bytes = bytes
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get done() {
|
|
10
|
+
return this.offset >= this.bytes.length
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
byte() {
|
|
14
|
+
const value = this.bytes[this.offset]
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
this.offset += 1
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
int() {
|
|
23
|
+
let shift = 1
|
|
24
|
+
let result = 0
|
|
25
|
+
while (this.offset < this.bytes.length) {
|
|
26
|
+
const value = this.bytes[this.offset]!
|
|
27
|
+
this.offset += 1
|
|
28
|
+
result += (value & 0x7f) * shift
|
|
29
|
+
shift *= 128
|
|
30
|
+
if ((value & 0x80) === 0) {
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Run {
|
|
39
|
+
value: number
|
|
40
|
+
len: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RLE_THRESHOLD = 255
|
|
44
|
+
const RLE_UNIVERSE = 256
|
|
45
|
+
|
|
46
|
+
export class RunReader {
|
|
47
|
+
private source: ByteCodeReader
|
|
48
|
+
private sigma: number
|
|
49
|
+
private threshold: number
|
|
50
|
+
|
|
51
|
+
constructor(bytes: Uint8Array, sigma: number) {
|
|
52
|
+
this.source = new ByteCodeReader(bytes)
|
|
53
|
+
this.sigma = sigma === 0 ? Number.MAX_SAFE_INTEGER : sigma
|
|
54
|
+
this.threshold =
|
|
55
|
+
this.sigma < RLE_THRESHOLD ? Math.floor(RLE_UNIVERSE / this.sigma) : 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
next(): Run | undefined {
|
|
59
|
+
if (this.sigma >= RLE_THRESHOLD) {
|
|
60
|
+
const value = this.source.int()
|
|
61
|
+
const len = this.source.int()
|
|
62
|
+
return value === undefined || len === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: { value, len: len + 1 }
|
|
65
|
+
}
|
|
66
|
+
const byte = this.source.byte()
|
|
67
|
+
if (byte === undefined) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
const value = byte % this.sigma
|
|
71
|
+
let len = Math.floor(byte / this.sigma) + 1
|
|
72
|
+
if (len === this.threshold) {
|
|
73
|
+
const extra = this.source.int()
|
|
74
|
+
if (extra === undefined) {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
len += extra
|
|
78
|
+
}
|
|
79
|
+
return { value, len }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
*[Symbol.iterator]() {
|
|
83
|
+
for (let run = this.next(); run !== undefined; run = this.next()) {
|
|
84
|
+
yield run
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|