dsh-plugin-dbgraph-store 0.0.0-stage → 0.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/lib/aliases.d.ts +34 -0
- package/lib/aliases.js +92 -0
- package/lib/connections.d.ts +42 -0
- package/lib/connections.js +172 -0
- package/lib/database.d.ts +61 -0
- package/lib/database.js +176 -0
- package/lib/index.d.ts +222 -0
- package/lib/index.js +414 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +23 -0
- package/lib/normalize.d.ts +59 -0
- package/lib/normalize.js +158 -0
- package/lib/paths.d.ts +83 -0
- package/lib/paths.js +130 -0
- package/lib/reader.d.ts +207 -0
- package/lib/reader.js +713 -0
- package/lib/resolve.d.ts +70 -0
- package/lib/resolve.js +762 -0
- package/lib/rust-engine.d.ts +108 -0
- package/lib/rust-engine.js +148 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +141 -0
- package/lib/terms.d.ts +93 -0
- package/lib/terms.js +146 -0
- package/lib/writer-js.d.ts +42 -0
- package/lib/writer-js.js +269 -0
- package/package.json +64 -3
- package/README.md +0 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small normalizations the writer and the resolver share: comment hygiene, identifier folding,
|
|
3
|
+
* declared-type classification, and a bounded edit distance for the resolver's last-tier match.
|
|
4
|
+
*
|
|
5
|
+
* Tokenization is deliberately *not* here — the one tokenizer lives in
|
|
6
|
+
* `dsh-plugin-dbgraph-service` (`tokenizeForSearch`) because the native writer reimplements it in
|
|
7
|
+
* Rust and the two must agree token for token.
|
|
8
|
+
* @module dsh-plugin-dbgraph-store/normalize
|
|
9
|
+
*/
|
|
10
|
+
/** The coarse type families the index stores in `columns.type_class`. */
|
|
11
|
+
export type TypeClass = 'integer' | 'decimal' | 'string' | 'text' | 'bool' | 'datetime' | 'json' | 'binary' | 'unknown';
|
|
12
|
+
/** Longest comment the index keeps. A comment is a hint, not a document. */
|
|
13
|
+
export declare const MAX_COMMENT_LENGTH = 2000;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a declared type onto a {@link TypeClass}. Order matters: `tinyint(1)` is MySQL's boolean
|
|
16
|
+
* spelling and is classified before the integer rule sees it.
|
|
17
|
+
* @param type - the declared type, verbatim from the server.
|
|
18
|
+
* @returns the coarse family; `unknown` for an empty or unrecognized declaration.
|
|
19
|
+
*/
|
|
20
|
+
export declare function typeClassOf(type: string): TypeClass;
|
|
21
|
+
/**
|
|
22
|
+
* Clean a comment for storage and indexing: NFKC, control characters collapsed to spaces, runs of
|
|
23
|
+
* whitespace folded, trimmed, and capped at {@link MAX_COMMENT_LENGTH}.
|
|
24
|
+
* @param comment - the comment the server reported, if any.
|
|
25
|
+
* @returns the cleaned comment, or undefined when nothing usable remains.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeComment(comment: string | undefined): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Fold an identifier for exact and fuzzy comparison: NFKC, lower-case, and every separator removed, so
|
|
30
|
+
* `user_id`, `userId` and `USER-ID` all become `userid`.
|
|
31
|
+
* @param name - the identifier (or a user's word).
|
|
32
|
+
* @returns the folded form; empty when the input holds no letter or digit.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeIdentifier(name: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a token the service tokenizer emitted is pure ASCII (an identifier part) rather than a CJK
|
|
37
|
+
* bigram. The writer weights the two differently, because a bigram is partial evidence by nature.
|
|
38
|
+
* @param token - one token from `tokenizeForSearch`.
|
|
39
|
+
* @returns true for `[a-z0-9]+`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isAsciiToken(token: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a string contains a CJK character.
|
|
44
|
+
* @param text - the text to inspect.
|
|
45
|
+
* @returns true when at least one CJK character is present.
|
|
46
|
+
*/
|
|
47
|
+
export declare function hasCjk(text: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Levenshtein distance, abandoned as soon as it is known to exceed `max`.
|
|
50
|
+
*
|
|
51
|
+
* The resolver's fuzzy tier is bounded at two edits on short identifiers, so the full quadratic table
|
|
52
|
+
* is never needed; the early exit also keeps a long column name from costing anything.
|
|
53
|
+
* @param left - first string, already folded.
|
|
54
|
+
* @param right - second string, already folded.
|
|
55
|
+
* @param max - the largest distance worth reporting.
|
|
56
|
+
* @returns the distance when it is at most `max`, else `max + 1`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function levenshteinWithin(left: string, right: string, max: number): number;
|
|
59
|
+
//# sourceMappingURL=normalize.d.ts.map
|
package/lib/normalize.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small normalizations the writer and the resolver share: comment hygiene, identifier folding,
|
|
3
|
+
* declared-type classification, and a bounded edit distance for the resolver's last-tier match.
|
|
4
|
+
*
|
|
5
|
+
* Tokenization is deliberately *not* here — the one tokenizer lives in
|
|
6
|
+
* `dsh-plugin-dbgraph-service` (`tokenizeForSearch`) because the native writer reimplements it in
|
|
7
|
+
* Rust and the two must agree token for token.
|
|
8
|
+
* @module dsh-plugin-dbgraph-store/normalize
|
|
9
|
+
*/
|
|
10
|
+
/** Longest comment the index keeps. A comment is a hint, not a document. */
|
|
11
|
+
export const MAX_COMMENT_LENGTH = 2000;
|
|
12
|
+
/** Longest identifier a fuzzy match will compare. */
|
|
13
|
+
const MAX_FUZZY_LENGTH = 64;
|
|
14
|
+
/** CJK ideographs and kana, the characters `normalizeIdentifier` keeps. */
|
|
15
|
+
const CJK = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a declared type onto a {@link TypeClass}. Order matters: `tinyint(1)` is MySQL's boolean
|
|
18
|
+
* spelling and is classified before the integer rule sees it.
|
|
19
|
+
* @param type - the declared type, verbatim from the server.
|
|
20
|
+
* @returns the coarse family; `unknown` for an empty or unrecognized declaration.
|
|
21
|
+
*/
|
|
22
|
+
export function typeClassOf(type) {
|
|
23
|
+
const normalized = type.normalize('NFKC').toLowerCase().trim();
|
|
24
|
+
if (normalized === '')
|
|
25
|
+
return 'unknown';
|
|
26
|
+
const base = normalized.replace(/\((?:[^)]*)\)/g, '').replace(/\bunsigned\b|\bzerofill\b/g, '').trim();
|
|
27
|
+
if (base === '')
|
|
28
|
+
return 'unknown';
|
|
29
|
+
if (/^tinyint\(1\)/.test(normalized) || /\bbool(ean)?\b/.test(base) || /^bit\b/.test(base))
|
|
30
|
+
return 'bool';
|
|
31
|
+
if (/^(big|small|medium|tiny)?int(eger)?\b/.test(base)
|
|
32
|
+
|| /^(u?int(2|4|8)|int[0-9]|serial|bigserial|smallserial)\b/.test(base)
|
|
33
|
+
|| /^rowid$/.test(base)
|
|
34
|
+
|| /\binteger\b/.test(base)) {
|
|
35
|
+
return 'integer';
|
|
36
|
+
}
|
|
37
|
+
if (/^(decimal|numeric|number|dec|fixed|money|smallmoney)\b/.test(base)
|
|
38
|
+
|| /^(float|double|real)\b/.test(base)
|
|
39
|
+
|| /^float[48]\b/.test(base)
|
|
40
|
+
|| /^double\b/.test(base)) {
|
|
41
|
+
return 'decimal';
|
|
42
|
+
}
|
|
43
|
+
if (/^(json|jsonb)\b/.test(base))
|
|
44
|
+
return 'json';
|
|
45
|
+
if (/^(timestamp|timestamptz|datetime|date|time|timetz|year|interval)\b/.test(base))
|
|
46
|
+
return 'datetime';
|
|
47
|
+
if (/^(blob|binary|varbinary|bytea|image|raw|bfile|long\s*blob|medium\s*blob|tiny\s*blob)\b/.test(base)) {
|
|
48
|
+
return 'binary';
|
|
49
|
+
}
|
|
50
|
+
if (/^(tiny|medium|long)?text\b|^ntext\b|^clob\b|^nclob\b/.test(base))
|
|
51
|
+
return 'text';
|
|
52
|
+
if (/^(n?char|character|varchar|nvarchar|varchar2|n?varchar2|enum|set|uuid|uniqueidentifier|citext)\b/.test(base)) {
|
|
53
|
+
return 'string';
|
|
54
|
+
}
|
|
55
|
+
if (/^(string|name|symbol)\b/.test(base))
|
|
56
|
+
return 'string';
|
|
57
|
+
if (/\bchar\b/.test(base) || /\bvarchar\b/.test(base))
|
|
58
|
+
return 'string';
|
|
59
|
+
return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Clean a comment for storage and indexing: NFKC, control characters collapsed to spaces, runs of
|
|
63
|
+
* whitespace folded, trimmed, and capped at {@link MAX_COMMENT_LENGTH}.
|
|
64
|
+
* @param comment - the comment the server reported, if any.
|
|
65
|
+
* @returns the cleaned comment, or undefined when nothing usable remains.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeComment(comment) {
|
|
68
|
+
if (comment === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
const cleaned = comment
|
|
71
|
+
.normalize('NFKC')
|
|
72
|
+
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
73
|
+
.replace(/\s+/g, ' ')
|
|
74
|
+
.trim();
|
|
75
|
+
if (cleaned === '')
|
|
76
|
+
return undefined;
|
|
77
|
+
return cleaned.length > MAX_COMMENT_LENGTH ? cleaned.slice(0, MAX_COMMENT_LENGTH) : cleaned;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fold an identifier for exact and fuzzy comparison: NFKC, lower-case, and every separator removed, so
|
|
81
|
+
* `user_id`, `userId` and `USER-ID` all become `userid`.
|
|
82
|
+
* @param name - the identifier (or a user's word).
|
|
83
|
+
* @returns the folded form; empty when the input holds no letter or digit.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeIdentifier(name) {
|
|
86
|
+
return name
|
|
87
|
+
.normalize('NFKC')
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
// Every Unicode letter and number survives, not just ASCII and CJK: folding a Cyrillic or
|
|
90
|
+
// accented identifier to '' would make it unmatchable, and `find` would then answer "no such
|
|
91
|
+
// column" about a column that exists. `\p{Alphabetic}\p{N}` is the set the Rust twin's
|
|
92
|
+
// `char::is_alphanumeric` reports, so the two writers keep folding to the same form.
|
|
93
|
+
.replace(/[^\p{Alphabetic}\p{N}]/gu, '');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether a token the service tokenizer emitted is pure ASCII (an identifier part) rather than a CJK
|
|
97
|
+
* bigram. The writer weights the two differently, because a bigram is partial evidence by nature.
|
|
98
|
+
* @param token - one token from `tokenizeForSearch`.
|
|
99
|
+
* @returns true for `[a-z0-9]+`.
|
|
100
|
+
*/
|
|
101
|
+
export function isAsciiToken(token) {
|
|
102
|
+
return /^[a-z0-9]+$/.test(token);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a string contains a CJK character.
|
|
106
|
+
* @param text - the text to inspect.
|
|
107
|
+
* @returns true when at least one CJK character is present.
|
|
108
|
+
*/
|
|
109
|
+
export function hasCjk(text) {
|
|
110
|
+
return CJK.test(text);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Levenshtein distance, abandoned as soon as it is known to exceed `max`.
|
|
114
|
+
*
|
|
115
|
+
* The resolver's fuzzy tier is bounded at two edits on short identifiers, so the full quadratic table
|
|
116
|
+
* is never needed; the early exit also keeps a long column name from costing anything.
|
|
117
|
+
* @param left - first string, already folded.
|
|
118
|
+
* @param right - second string, already folded.
|
|
119
|
+
* @param max - the largest distance worth reporting.
|
|
120
|
+
* @returns the distance when it is at most `max`, else `max + 1`.
|
|
121
|
+
*/
|
|
122
|
+
export function levenshteinWithin(left, right, max) {
|
|
123
|
+
const a = left.length > MAX_FUZZY_LENGTH ? left.slice(0, MAX_FUZZY_LENGTH) : left;
|
|
124
|
+
const b = right.length > MAX_FUZZY_LENGTH ? right.slice(0, MAX_FUZZY_LENGTH) : right;
|
|
125
|
+
if (a === b)
|
|
126
|
+
return 0;
|
|
127
|
+
if (Math.abs(a.length - b.length) > max)
|
|
128
|
+
return max + 1;
|
|
129
|
+
if (a === '')
|
|
130
|
+
return b.length <= max ? b.length : max + 1;
|
|
131
|
+
if (b === '')
|
|
132
|
+
return a.length <= max ? a.length : max + 1;
|
|
133
|
+
let previous = new Array(b.length + 1);
|
|
134
|
+
let current = new Array(b.length + 1);
|
|
135
|
+
for (let j = 0; j <= b.length; j += 1)
|
|
136
|
+
previous[j] = j;
|
|
137
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
138
|
+
current[0] = i;
|
|
139
|
+
let rowBest = i;
|
|
140
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
141
|
+
const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
142
|
+
const deletion = previous[j] + 1;
|
|
143
|
+
const insertion = current[j - 1] + 1;
|
|
144
|
+
const value = Math.min(substitution, deletion, insertion);
|
|
145
|
+
current[j] = value;
|
|
146
|
+
if (value < rowBest)
|
|
147
|
+
rowBest = value;
|
|
148
|
+
}
|
|
149
|
+
if (rowBest > max)
|
|
150
|
+
return max + 1;
|
|
151
|
+
const swap = previous;
|
|
152
|
+
previous = current;
|
|
153
|
+
current = swap;
|
|
154
|
+
}
|
|
155
|
+
const distance = previous[b.length];
|
|
156
|
+
return distance <= max ? distance : max + 1;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=normalize.js.map
|
package/lib/paths.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where dbgraph keeps its files, and the one switch that relocates them.
|
|
3
|
+
*
|
|
4
|
+
* Everything lives under a single root (`~/.dsh/dbgraph` by default):
|
|
5
|
+
*
|
|
6
|
+
* - `<root>/connections.json` — the connection list,
|
|
7
|
+
* - `<root>/<connection>/index.db` — one SQLite index per connection,
|
|
8
|
+
* - `<root>/status.json` — the prompt digest read synchronously on every prompt assembly.
|
|
9
|
+
*
|
|
10
|
+
* `DSH_DBGRAPH_ROOT` overrides the root so a test (or a deployment that keeps profile state somewhere
|
|
11
|
+
* else) never touches the real harness home. A store instance resolves the root once at construction;
|
|
12
|
+
* the free functions take an explicit root and fall back to the environment so a lone caller (a
|
|
13
|
+
* config surface, a test of `connections.json`) does not need a store first.
|
|
14
|
+
* @module dsh-plugin-dbgraph-store/paths
|
|
15
|
+
*/
|
|
16
|
+
/** Environment variable that relocates the whole index root. */
|
|
17
|
+
export declare const ROOT_ENV = "DSH_DBGRAPH_ROOT";
|
|
18
|
+
/**
|
|
19
|
+
* How long a staging file may sit before a write treats it as abandoned.
|
|
20
|
+
*
|
|
21
|
+
* A run that dies between creating its staging file and renaming it leaves that file behind forever
|
|
22
|
+
* (the path is per-run, so no later run reuses or removes it). One hour is far past any real build —
|
|
23
|
+
* the measured worst case on a 10k-column catalog is seconds — and short enough that a directory does
|
|
24
|
+
* not accumulate junk indefinitely.
|
|
25
|
+
*/
|
|
26
|
+
export declare const STALE_STAGING_MAX_AGE_MS: number;
|
|
27
|
+
/**
|
|
28
|
+
* The index root: `DSH_DBGRAPH_ROOT` when set and non-empty, else `~/.dsh/dbgraph`.
|
|
29
|
+
* @returns an absolute directory path (an override may be relative; callers treat it as-is).
|
|
30
|
+
*/
|
|
31
|
+
export declare function indexRoot(): string;
|
|
32
|
+
/**
|
|
33
|
+
* One connection's private directory.
|
|
34
|
+
* @param id - connection identity (already validated by the caller).
|
|
35
|
+
* @param root - index root; defaults to {@link indexRoot}.
|
|
36
|
+
* @returns the directory path, whether or not it exists.
|
|
37
|
+
*/
|
|
38
|
+
export declare function connectionDir(id: string, root?: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* One connection's index file.
|
|
41
|
+
* @param id - connection identity.
|
|
42
|
+
* @param root - index root.
|
|
43
|
+
* @returns the path, whether or not the file exists.
|
|
44
|
+
*/
|
|
45
|
+
export declare function indexPath(id: string, root?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* A staging path unique to one write.
|
|
48
|
+
*
|
|
49
|
+
* A plain `<index>.tmp` is not enough: two index runs for the same connection would share it, and one
|
|
50
|
+
* run could delete or rename the other's half-written file. The nonce is per call, so a run only ever
|
|
51
|
+
* cleans up and renames a file it created itself.
|
|
52
|
+
* @param id - connection identity.
|
|
53
|
+
* @param root - index root.
|
|
54
|
+
* @returns `<index>.<pid>-<nonce>.tmp`, on the same filesystem so the rename is atomic.
|
|
55
|
+
*/
|
|
56
|
+
export declare function stagingIndexPath(id: string, root?: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Remove staging files a previous run abandoned beside one index.
|
|
59
|
+
*
|
|
60
|
+
* The per-run nonce fixes the original collision (two runs cannot share a staging file) but turns
|
|
61
|
+
* each leak into a permanent one: a run killed between `open` and `rename` leaves
|
|
62
|
+
* `<index>.<pid>-<nonce>.tmp` behind and no later run would ever reuse that name. Only files older
|
|
63
|
+
* than {@link STALE_STAGING_MAX_AGE_MS} are removed, so a run that is merely slow — or a second run
|
|
64
|
+
* holding its own fresh staging file — is never touched. Removal failures are ignored: the file is
|
|
65
|
+
* junk, and a concurrent writer may remove it first.
|
|
66
|
+
* @param indexFile - the connection's live index path (the staging files share its directory).
|
|
67
|
+
* @param now - clock override, for tests.
|
|
68
|
+
* @returns the paths that were removed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sweepStaleStaging(indexFile: string, now?: number): string[];
|
|
71
|
+
/**
|
|
72
|
+
* The connection list.
|
|
73
|
+
* @param root - index root.
|
|
74
|
+
* @returns `<root>/connections.json`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function connectionsPath(root?: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* The prompt digest sidecar.
|
|
79
|
+
* @param root - index root.
|
|
80
|
+
* @returns `<root>/status.json`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function statusPath(root?: string): string;
|
|
83
|
+
//# sourceMappingURL=paths.d.ts.map
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where dbgraph keeps its files, and the one switch that relocates them.
|
|
3
|
+
*
|
|
4
|
+
* Everything lives under a single root (`~/.dsh/dbgraph` by default):
|
|
5
|
+
*
|
|
6
|
+
* - `<root>/connections.json` — the connection list,
|
|
7
|
+
* - `<root>/<connection>/index.db` — one SQLite index per connection,
|
|
8
|
+
* - `<root>/status.json` — the prompt digest read synchronously on every prompt assembly.
|
|
9
|
+
*
|
|
10
|
+
* `DSH_DBGRAPH_ROOT` overrides the root so a test (or a deployment that keeps profile state somewhere
|
|
11
|
+
* else) never touches the real harness home. A store instance resolves the root once at construction;
|
|
12
|
+
* the free functions take an explicit root and fall back to the environment so a lone caller (a
|
|
13
|
+
* config surface, a test of `connections.json`) does not need a store first.
|
|
14
|
+
* @module dsh-plugin-dbgraph-store/paths
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { basename, dirname, join } from 'node:path';
|
|
20
|
+
import { CONNECTIONS_FILE_NAME, INDEX_FILE_NAME, INDEX_ROOT_DIRNAME, STATUS_FILE_NAME, } from 'dsh-plugin-dbgraph-service';
|
|
21
|
+
/** Environment variable that relocates the whole index root. */
|
|
22
|
+
export const ROOT_ENV = 'DSH_DBGRAPH_ROOT';
|
|
23
|
+
/**
|
|
24
|
+
* How long a staging file may sit before a write treats it as abandoned.
|
|
25
|
+
*
|
|
26
|
+
* A run that dies between creating its staging file and renaming it leaves that file behind forever
|
|
27
|
+
* (the path is per-run, so no later run reuses or removes it). One hour is far past any real build —
|
|
28
|
+
* the measured worst case on a 10k-column catalog is seconds — and short enough that a directory does
|
|
29
|
+
* not accumulate junk indefinitely.
|
|
30
|
+
*/
|
|
31
|
+
export const STALE_STAGING_MAX_AGE_MS = 60 * 60 * 1000;
|
|
32
|
+
/**
|
|
33
|
+
* The index root: `DSH_DBGRAPH_ROOT` when set and non-empty, else `~/.dsh/dbgraph`.
|
|
34
|
+
* @returns an absolute directory path (an override may be relative; callers treat it as-is).
|
|
35
|
+
*/
|
|
36
|
+
export function indexRoot() {
|
|
37
|
+
const override = process.env[ROOT_ENV]?.trim();
|
|
38
|
+
if (override !== undefined && override !== '')
|
|
39
|
+
return override;
|
|
40
|
+
return join(homedir(), '.dsh', INDEX_ROOT_DIRNAME);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* One connection's private directory.
|
|
44
|
+
* @param id - connection identity (already validated by the caller).
|
|
45
|
+
* @param root - index root; defaults to {@link indexRoot}.
|
|
46
|
+
* @returns the directory path, whether or not it exists.
|
|
47
|
+
*/
|
|
48
|
+
export function connectionDir(id, root = indexRoot()) {
|
|
49
|
+
return join(root, id);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One connection's index file.
|
|
53
|
+
* @param id - connection identity.
|
|
54
|
+
* @param root - index root.
|
|
55
|
+
* @returns the path, whether or not the file exists.
|
|
56
|
+
*/
|
|
57
|
+
export function indexPath(id, root = indexRoot()) {
|
|
58
|
+
return join(connectionDir(id, root), INDEX_FILE_NAME);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A staging path unique to one write.
|
|
62
|
+
*
|
|
63
|
+
* A plain `<index>.tmp` is not enough: two index runs for the same connection would share it, and one
|
|
64
|
+
* run could delete or rename the other's half-written file. The nonce is per call, so a run only ever
|
|
65
|
+
* cleans up and renames a file it created itself.
|
|
66
|
+
* @param id - connection identity.
|
|
67
|
+
* @param root - index root.
|
|
68
|
+
* @returns `<index>.<pid>-<nonce>.tmp`, on the same filesystem so the rename is atomic.
|
|
69
|
+
*/
|
|
70
|
+
export function stagingIndexPath(id, root = indexRoot()) {
|
|
71
|
+
return `${indexPath(id, root)}.${process.pid}-${randomUUID().slice(0, 8)}.tmp`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove staging files a previous run abandoned beside one index.
|
|
75
|
+
*
|
|
76
|
+
* The per-run nonce fixes the original collision (two runs cannot share a staging file) but turns
|
|
77
|
+
* each leak into a permanent one: a run killed between `open` and `rename` leaves
|
|
78
|
+
* `<index>.<pid>-<nonce>.tmp` behind and no later run would ever reuse that name. Only files older
|
|
79
|
+
* than {@link STALE_STAGING_MAX_AGE_MS} are removed, so a run that is merely slow — or a second run
|
|
80
|
+
* holding its own fresh staging file — is never touched. Removal failures are ignored: the file is
|
|
81
|
+
* junk, and a concurrent writer may remove it first.
|
|
82
|
+
* @param indexFile - the connection's live index path (the staging files share its directory).
|
|
83
|
+
* @param now - clock override, for tests.
|
|
84
|
+
* @returns the paths that were removed.
|
|
85
|
+
*/
|
|
86
|
+
export function sweepStaleStaging(indexFile, now = Date.now()) {
|
|
87
|
+
const directory = dirname(indexFile);
|
|
88
|
+
const prefix = `${basename(indexFile)}.`;
|
|
89
|
+
let names;
|
|
90
|
+
try {
|
|
91
|
+
names = readdirSync(directory);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// No directory yet: the write that called this is about to create it.
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const removed = [];
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
if (!name.startsWith(prefix) || !name.endsWith('.tmp'))
|
|
100
|
+
continue;
|
|
101
|
+
const candidate = join(directory, name);
|
|
102
|
+
try {
|
|
103
|
+
if (now - statSync(candidate).mtimeMs <= STALE_STAGING_MAX_AGE_MS)
|
|
104
|
+
continue;
|
|
105
|
+
rmSync(candidate, { force: true });
|
|
106
|
+
removed.push(candidate);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// The file vanished, or its directory is not readable; either way there is nothing to clean.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return removed;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The connection list.
|
|
116
|
+
* @param root - index root.
|
|
117
|
+
* @returns `<root>/connections.json`.
|
|
118
|
+
*/
|
|
119
|
+
export function connectionsPath(root = indexRoot()) {
|
|
120
|
+
return join(root, CONNECTIONS_FILE_NAME);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The prompt digest sidecar.
|
|
124
|
+
* @param root - index root.
|
|
125
|
+
* @returns `<root>/status.json`.
|
|
126
|
+
*/
|
|
127
|
+
export function statusPath(root = indexRoot()) {
|
|
128
|
+
return join(root, STATUS_FILE_NAME);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=paths.js.map
|
package/lib/reader.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read path over one connection's index: `state`, `schema`, `objects`, `columns`, `impact`, `refs`,
|
|
3
|
+
* plus the summary `record()` rebuilds the prompt digest from.
|
|
4
|
+
*
|
|
5
|
+
* Every operation here is offline and read-only by construction, and one rule separates them:
|
|
6
|
+
* {@link IndexReader.state} **never throws** — a configuration surface asking "is this indexed" must
|
|
7
|
+
* get an answer even when the file is missing or unusable — while every other read fails loudly, so
|
|
8
|
+
* "no index" and "empty index" never look the same.
|
|
9
|
+
*
|
|
10
|
+
* Identifiers that reach SQL as identifiers (as opposed to bound values) are never taken from the
|
|
11
|
+
* index's own text; the only identifier interpolation is a placeholder list whose length comes from a
|
|
12
|
+
* count we control.
|
|
13
|
+
* @module dsh-plugin-dbgraph-store/reader
|
|
14
|
+
*/
|
|
15
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import type { ColumnsRequest, ColumnsResult, ImpactRequest, ImpactResult, IndexState, ObjectsRequest, ObjectsResult, RefsRequest, RefsResult, SchemaRequest, SchemaResult } from 'dsh-plugin-dbgraph-service';
|
|
17
|
+
import type { OpenedIndex } from './database.ts';
|
|
18
|
+
/** One row of `objects`, as every read operation wants it. */
|
|
19
|
+
export interface ObjectRecord {
|
|
20
|
+
/** SQLite rowid of the object. */
|
|
21
|
+
readonly id: number;
|
|
22
|
+
/** Namespace, or `''` when the engine has none. */
|
|
23
|
+
readonly schema: string;
|
|
24
|
+
/** Declared name. */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Node category. */
|
|
27
|
+
readonly kind: string;
|
|
28
|
+
/** Comment, already normalized. */
|
|
29
|
+
readonly comment: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** One row of `columns`, as the resolver and the column listing want it. */
|
|
32
|
+
export interface ColumnRecord {
|
|
33
|
+
/** SQLite rowid of the column. */
|
|
34
|
+
readonly id: number;
|
|
35
|
+
/** Declared name. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Declared type, verbatim. */
|
|
38
|
+
readonly type: string;
|
|
39
|
+
/** Whether the column admits NULL. */
|
|
40
|
+
readonly nullable: boolean;
|
|
41
|
+
/** Declared default, rendered as text. */
|
|
42
|
+
readonly defaultValue: string | undefined;
|
|
43
|
+
/** Comment, already normalized. */
|
|
44
|
+
readonly comment: string | undefined;
|
|
45
|
+
/** Whether the column participates in the primary key. */
|
|
46
|
+
readonly isPrimaryKey: boolean;
|
|
47
|
+
/** Whether a single-column unique constraint covers it. */
|
|
48
|
+
readonly isUnique: boolean;
|
|
49
|
+
/** One-based position within the object. */
|
|
50
|
+
readonly ordinal: number;
|
|
51
|
+
/** Sampled distinct literals, when the enrichment pass captured them. */
|
|
52
|
+
readonly enumValues: readonly string[] | undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Bounds the reader imposes on one call. */
|
|
55
|
+
export interface ReaderConfig {
|
|
56
|
+
/** Hours after which an L2 snapshot is reported stale. */
|
|
57
|
+
readonly staleHours: number;
|
|
58
|
+
/** Index files kept open at once. */
|
|
59
|
+
readonly maxOpenIndexes: number;
|
|
60
|
+
/** Objects one `schema` call may return. */
|
|
61
|
+
readonly maxSchemaObjects: number;
|
|
62
|
+
/** Columns one `columns` call may return. */
|
|
63
|
+
readonly maxColumns: number;
|
|
64
|
+
/** Objects one `objects` call returns when the request names no limit. */
|
|
65
|
+
readonly defaultObjectLimit: number;
|
|
66
|
+
/** Digest entries written into the prompt sidecar. */
|
|
67
|
+
readonly maxDigestObjects: number;
|
|
68
|
+
/** Foreign-key hops one `impact` walk follows. */
|
|
69
|
+
readonly impactDepth: number;
|
|
70
|
+
/** Edges one `impact` or `refs` call may return. */
|
|
71
|
+
readonly maxRelations: number;
|
|
72
|
+
}
|
|
73
|
+
/** Defaults for a store mounted without tuning. */
|
|
74
|
+
export declare const DEFAULT_READER_CONFIG: ReaderConfig;
|
|
75
|
+
/** What an object lookup found, so a caller can report ambiguity instead of guessing. */
|
|
76
|
+
export interface ObjectLookup {
|
|
77
|
+
/** The unique match, when there is exactly one. */
|
|
78
|
+
readonly record?: ObjectRecord;
|
|
79
|
+
/** Every match, when there was more than one. */
|
|
80
|
+
readonly matches: readonly ObjectRecord[];
|
|
81
|
+
/** Near misses offered when nothing matched. */
|
|
82
|
+
readonly suggestions: readonly ObjectRecord[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Render an object for a model-facing surface.
|
|
86
|
+
* @param record - the object row.
|
|
87
|
+
* @returns `schema.name` when a schema is present, else `name`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function displayName(record: ObjectRecord): string;
|
|
90
|
+
/**
|
|
91
|
+
* Object ids matched by the FTS index, best rank first.
|
|
92
|
+
*
|
|
93
|
+
* Both document kinds count: an object document carries the table's own name and comment, a column
|
|
94
|
+
* document carries one column's — and a question about a field is a question about the table holding
|
|
95
|
+
* it. `bm25()` is lower-is-better, so merging keeps the minimum rank.
|
|
96
|
+
* @param db - the open index.
|
|
97
|
+
* @param match - an FTS expression.
|
|
98
|
+
* @param limit - maximum ids to return.
|
|
99
|
+
* @returns object ids in rank order.
|
|
100
|
+
*/
|
|
101
|
+
export declare function ftsObjectIds(db: DatabaseSync, match: string, limit: number): number[];
|
|
102
|
+
/**
|
|
103
|
+
* Suggest objects when an exact name missed.
|
|
104
|
+
* @param db - the open index.
|
|
105
|
+
* @param words - what the caller typed.
|
|
106
|
+
* @returns up to five near matches, best first.
|
|
107
|
+
*/
|
|
108
|
+
export declare function suggestObjects(db: DatabaseSync, words: readonly string[]): ObjectRecord[];
|
|
109
|
+
/**
|
|
110
|
+
* Locate an object by its listed name — bare (`sys_user`) or schema-qualified (`public.sys_user`).
|
|
111
|
+
* @param db - the open index.
|
|
112
|
+
* @param name - the name the caller used.
|
|
113
|
+
* @returns the unique match, the ambiguous matches, or suggestions.
|
|
114
|
+
*/
|
|
115
|
+
export declare function lookupObject(db: DatabaseSync, name: string): ObjectLookup;
|
|
116
|
+
/**
|
|
117
|
+
* Load one object's columns in declared order.
|
|
118
|
+
* @param db - the open index.
|
|
119
|
+
* @param objectId - the object id.
|
|
120
|
+
* @param limit - maximum rows (a caller passes `cap + 1` to detect truncation).
|
|
121
|
+
* @returns column records.
|
|
122
|
+
*/
|
|
123
|
+
export declare function loadColumns(db: DatabaseSync, objectId: number, limit: number): ColumnRecord[];
|
|
124
|
+
/**
|
|
125
|
+
* Load a set of objects by id.
|
|
126
|
+
* @param db - the open index.
|
|
127
|
+
* @param ids - their ids.
|
|
128
|
+
* @returns records keyed by id.
|
|
129
|
+
*/
|
|
130
|
+
export declare function loadObjects(db: DatabaseSync, ids: readonly number[]): Map<number, ObjectRecord>;
|
|
131
|
+
/** The `reader` half of the store: one pool, one root, no network. */
|
|
132
|
+
export declare class IndexReader {
|
|
133
|
+
private readonly root;
|
|
134
|
+
private readonly config;
|
|
135
|
+
private readonly pool;
|
|
136
|
+
/**
|
|
137
|
+
* @param root - index root.
|
|
138
|
+
* @param config - the bounds this reader imposes.
|
|
139
|
+
*/
|
|
140
|
+
constructor(root: string, config?: ReaderConfig);
|
|
141
|
+
/**
|
|
142
|
+
* Open one connection's index, validating the identity first so no caller-supplied id becomes a
|
|
143
|
+
* path fragment.
|
|
144
|
+
* @param connection - connection identity.
|
|
145
|
+
* @returns the open index.
|
|
146
|
+
*/
|
|
147
|
+
open(connection: string): OpenedIndex;
|
|
148
|
+
/**
|
|
149
|
+
* State of one connection's index. Never throws: an unusable index is reported as empty and stale.
|
|
150
|
+
* @param connection - connection identity.
|
|
151
|
+
* @returns the state.
|
|
152
|
+
*/
|
|
153
|
+
state(connection: string): IndexState;
|
|
154
|
+
/**
|
|
155
|
+
* Structural overview.
|
|
156
|
+
* @param request - which connection.
|
|
157
|
+
* @returns the overview.
|
|
158
|
+
*/
|
|
159
|
+
schema(request: SchemaRequest): SchemaResult;
|
|
160
|
+
/**
|
|
161
|
+
* Ranked object listing.
|
|
162
|
+
* @param request - which connection and what to look for.
|
|
163
|
+
* @returns the listing.
|
|
164
|
+
*/
|
|
165
|
+
objects(request: ObjectsRequest): ObjectsResult;
|
|
166
|
+
/**
|
|
167
|
+
* One object's columns.
|
|
168
|
+
* @param request - which object.
|
|
169
|
+
* @returns the listing.
|
|
170
|
+
*/
|
|
171
|
+
columns(request: ColumnsRequest): ColumnsResult;
|
|
172
|
+
/**
|
|
173
|
+
* Objects a change to this one reaches: the transitive inbound reference closure, nearest first.
|
|
174
|
+
* @param request - the object to trace.
|
|
175
|
+
* @returns the reached objects.
|
|
176
|
+
*/
|
|
177
|
+
impact(request: ImpactRequest): ImpactResult;
|
|
178
|
+
/**
|
|
179
|
+
* Objects directly referencing this one.
|
|
180
|
+
* @param request - the object to trace.
|
|
181
|
+
* @returns the referencing objects.
|
|
182
|
+
*/
|
|
183
|
+
refs(request: RefsRequest): RefsResult;
|
|
184
|
+
/**
|
|
185
|
+
* Close every cached connection. Idempotent.
|
|
186
|
+
*/
|
|
187
|
+
close(): void;
|
|
188
|
+
/**
|
|
189
|
+
* State derived from an already-open index, so `schema` does not stat the file twice.
|
|
190
|
+
* @param opened - the open index.
|
|
191
|
+
* @returns the state.
|
|
192
|
+
*/
|
|
193
|
+
private stateOf;
|
|
194
|
+
/**
|
|
195
|
+
* Fraction of columns carrying a comment.
|
|
196
|
+
* @param opened - the open index.
|
|
197
|
+
* @returns 0..1, and 0 for an empty index (no evidence, not full coverage).
|
|
198
|
+
*/
|
|
199
|
+
private commentCoverageOf;
|
|
200
|
+
/**
|
|
201
|
+
* Clamp a requested object limit into the configured bounds.
|
|
202
|
+
* @param requested - the caller's value, when one was set.
|
|
203
|
+
* @returns a limit of at least 1, at most 500.
|
|
204
|
+
*/
|
|
205
|
+
private objectLimit;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=reader.d.ts.map
|