@yydb/yydb 0.1.1 → 0.1.3
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 +23 -3
- package/dist/api/database.d.ts +253 -27
- package/dist/api/database.js +307 -129
- package/dist/cli/cli.js +28 -1
- package/dist/cli/info-cmd.js +30 -1
- package/dist/cli/verify-cmd.d.ts +11 -0
- package/dist/cli/verify-cmd.js +73 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/native/binding.d.ts +67 -0
- package/dist/shared/errors.d.ts +132 -0
- package/dist/shared/errors.js +154 -0
- package/package.json +9 -9
package/dist/api/database.js
CHANGED
|
@@ -1,173 +1,351 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
|
-
import net from 'node:net';
|
|
4
2
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
3
|
+
import { emitSource, } from '@voml/vos';
|
|
6
4
|
import { defineMicro as buildMicro, microFingerprint, udfWireKind, MicroSessionRegistry, } from '../shared/micro.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
5
|
+
import { loadNative } from '../native/binding.js';
|
|
6
|
+
import { YydbError } from '../shared/errors.js';
|
|
7
|
+
/**
|
|
8
|
+
* A pre-parsed, pre-lowered VOS query plan.
|
|
9
|
+
*
|
|
10
|
+
* Created by {@link Database.prepare}. The actual lowered op tree lives on
|
|
11
|
+
* the native side in a thread-local registry keyed by `statementId`; this
|
|
12
|
+
* class is a thin JS handle. Bind parameters and re-execute via
|
|
13
|
+
* {@link Statement.execute} without re-parsing or re-lowering the source.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const stmt = await db.prepare('User.all().take($limit).collect()');
|
|
17
|
+
* const rows = await stmt.execute({ limit: 100 });
|
|
18
|
+
* // re-execute with different bind
|
|
19
|
+
* const rows2 = await stmt.execute({ limit: 50 });
|
|
20
|
+
* // release when done
|
|
21
|
+
* stmt.close();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class Statement {
|
|
25
|
+
native;
|
|
26
|
+
db;
|
|
27
|
+
closed = false;
|
|
28
|
+
constructor(native, db) {
|
|
29
|
+
this.native = native;
|
|
30
|
+
this.db = db;
|
|
31
|
+
}
|
|
32
|
+
/** The fingerprint-prefixed unique id for this statement. */
|
|
33
|
+
get statementId() { return this.native.statementId(); }
|
|
34
|
+
/** Parameter descriptors — empty array when the statement takes no parameters. */
|
|
35
|
+
get parameters() {
|
|
36
|
+
return JSON.parse(this.native.parametersJson());
|
|
37
|
+
}
|
|
38
|
+
/** Full statement metadata. */
|
|
39
|
+
get info() {
|
|
40
|
+
return JSON.parse(this.native.info());
|
|
41
|
+
}
|
|
42
|
+
/** Whether this statement has been closed and can no longer execute. */
|
|
43
|
+
get isClosed() { return this.closed; }
|
|
44
|
+
/**
|
|
45
|
+
* Execute this prepared statement with optional named parameter bindings.
|
|
46
|
+
*
|
|
47
|
+
* Missing parameters, extraneous parameters or mistyped parameters raise
|
|
48
|
+
* `YydbError` with code `yydb.query.bind`.
|
|
49
|
+
*
|
|
50
|
+
* @param params Named parameter values — keys match the parameter names
|
|
51
|
+
* in the VOS source (`$name` → `{ name: value }`). Omit when the statement
|
|
52
|
+
* takes no parameters.
|
|
53
|
+
*/
|
|
54
|
+
async execute(params) {
|
|
55
|
+
if (this.closed) {
|
|
56
|
+
throw new YydbError('yydb.statement.closed', 'statement has been closed');
|
|
41
57
|
}
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
const paramsJson = params !== undefined && params !== null ? JSON.stringify(params) : null;
|
|
59
|
+
const rowsJson = this.db.queryPrepared(this.native, paramsJson);
|
|
60
|
+
return JSON.parse(rowsJson);
|
|
61
|
+
}
|
|
62
|
+
/** Release this statement's plan from the native registry. */
|
|
63
|
+
close() {
|
|
64
|
+
if (!this.closed) {
|
|
65
|
+
this.native.close();
|
|
66
|
+
this.closed = true;
|
|
44
67
|
}
|
|
45
68
|
}
|
|
46
|
-
throw new Error(`yydb engine did not accept connections on ${host}:${port}`);
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
71
|
+
* Transaction-local handle passed to {@link Database.transaction} callbacks.
|
|
72
|
+
* All CRUD methods forward to the underlying native handle while the
|
|
73
|
+
* transaction is in flight. Commit/rollback are owned by {@link Database}.
|
|
74
|
+
*/
|
|
75
|
+
export class TransactionHandle {
|
|
76
|
+
native;
|
|
77
|
+
constructor(native) {
|
|
78
|
+
this.native = native;
|
|
79
|
+
}
|
|
80
|
+
async get(key) { return this.native.get(key); }
|
|
81
|
+
async put(key, value) {
|
|
82
|
+
const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
|
|
83
|
+
this.native.put(key, bytes);
|
|
84
|
+
}
|
|
85
|
+
async delete(key) { this.native.delete(key); }
|
|
86
|
+
async scan(options = {}) {
|
|
87
|
+
const rowsJson = this.native.scanPrefix(options.prefix ?? '', options.after ?? null, options.limit ?? 1000);
|
|
88
|
+
const rows = JSON.parse(rowsJson);
|
|
89
|
+
return rows.map((r) => ({ key: r.key, value: new Uint8Array(r.value) }));
|
|
90
|
+
}
|
|
91
|
+
async batch(changes) {
|
|
92
|
+
const body = changes.map((c) => ({
|
|
93
|
+
key: c.key,
|
|
94
|
+
value: c.value === null ? null : typeof c.value === 'string' ? Array.from(new TextEncoder().encode(c.value)) : Array.from(c.value),
|
|
95
|
+
}));
|
|
96
|
+
this.native.writeBatch(JSON.stringify(body));
|
|
97
|
+
}
|
|
98
|
+
async compareExchange(key, expected, replacement) {
|
|
99
|
+
const toBytes = (v) => v === null ? null : typeof v === 'string' ? new TextEncoder().encode(v) : v;
|
|
100
|
+
return this.native.compareExchange(key, toBytes(expected), toBytes(replacement));
|
|
101
|
+
}
|
|
102
|
+
async query(source) {
|
|
103
|
+
return JSON.parse(this.native.queryVos(source));
|
|
104
|
+
}
|
|
105
|
+
async execute(source) { this.native.executeVosWrite(source); }
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Node-facing YYDB local database handle.
|
|
109
|
+
*
|
|
110
|
+
* Directly owns a native `yydb::Connection` via the Node-API binding. No
|
|
111
|
+
* child process, no loopback TCP, no wire protocol in the hot path.
|
|
51
112
|
*/
|
|
52
113
|
export class Database {
|
|
53
114
|
path;
|
|
54
|
-
|
|
55
|
-
client;
|
|
56
|
-
child;
|
|
115
|
+
native;
|
|
57
116
|
microRegistry;
|
|
58
|
-
constructor(dbPath,
|
|
117
|
+
constructor(dbPath, native) {
|
|
59
118
|
this.path = dbPath;
|
|
60
|
-
this.
|
|
61
|
-
this.client = client;
|
|
62
|
-
this.child = child;
|
|
119
|
+
this.native = native;
|
|
63
120
|
this.microRegistry = new MicroSessionRegistry();
|
|
64
|
-
this.client.setMicroHostHandler((payload) => this.microRegistry.invokeFromWire(payload));
|
|
65
121
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Open (create) a `.yydb` file. Spawns the engine on `127.0.0.1` automatically.
|
|
68
|
-
*/
|
|
122
|
+
/** Open (create when missing) the database at `dbPath`. */
|
|
69
123
|
static async open(dbPath, options = {}) {
|
|
70
124
|
const resolved = path.resolve(dbPath);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const port = options.port ?? (await freeLoopbackPort());
|
|
74
|
-
const bind = `127.0.0.1:${port}`;
|
|
75
|
-
const args = [cli, 'serve', resolved, '--bind', bind, ...(options.serveArgs ?? [])];
|
|
76
|
-
const child = spawn(process.execPath, args, {
|
|
77
|
-
stdio: 'pipe',
|
|
78
|
-
windowsHide: true,
|
|
79
|
-
});
|
|
80
|
-
let stderr = '';
|
|
81
|
-
child.stderr?.setEncoding('utf8');
|
|
82
|
-
child.stderr?.on('data', (chunk) => {
|
|
83
|
-
stderr += chunk;
|
|
84
|
-
});
|
|
85
|
-
const exitPromise = new Promise((_, reject) => {
|
|
86
|
-
child.once('exit', (code, signal) => {
|
|
87
|
-
reject(new Error(`yydb engine exited early (code=${code}, signal=${signal}): ${stderr.trim()}`));
|
|
88
|
-
});
|
|
89
|
-
child.once('error', reject);
|
|
90
|
-
});
|
|
91
|
-
try {
|
|
92
|
-
await Promise.race([waitForTcp('127.0.0.1', port, 8000), exitPromise]);
|
|
93
|
-
const client = await connect(bind);
|
|
94
|
-
child.removeAllListeners('exit');
|
|
95
|
-
child.removeAllListeners('error');
|
|
96
|
-
child.on('exit', () => {
|
|
97
|
-
/* closed via Database.close or crash */
|
|
98
|
-
});
|
|
99
|
-
return new Database(resolved, bind, client, child);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
child.kill('SIGTERM');
|
|
103
|
-
throw error;
|
|
125
|
+
if (options.mkdir !== false) {
|
|
126
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
104
127
|
}
|
|
128
|
+
const native = loadNative();
|
|
129
|
+
const db = native.Database.open(resolved);
|
|
130
|
+
return new Database(resolved, db);
|
|
105
131
|
}
|
|
106
|
-
|
|
107
|
-
|
|
132
|
+
/** Engine version string (same as `yydb --version`). */
|
|
133
|
+
serverVersion() {
|
|
134
|
+
return loadNative().version();
|
|
108
135
|
}
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
/** Basic diagnostics: path, schema, record count, journal mode. */
|
|
137
|
+
info() {
|
|
138
|
+
const lines = [
|
|
139
|
+
`path=${this.native.path() ?? '<memory>'}`,
|
|
140
|
+
`schema.version=${this.native.schemaVersion() ?? ''}`,
|
|
141
|
+
`records=${this.native.recordCount()}`,
|
|
142
|
+
`journal_mode=${this.native.journalMode()}`,
|
|
143
|
+
];
|
|
144
|
+
return lines.join('\n') + '\n';
|
|
111
145
|
}
|
|
146
|
+
/** Published schema metadata, or `null` before the first schema. */
|
|
112
147
|
async getSchema() {
|
|
113
|
-
|
|
148
|
+
const version = this.native.schemaVersion();
|
|
149
|
+
const document = this.native.schemaDocument();
|
|
150
|
+
if (version === null || document === null) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return { version, document };
|
|
114
154
|
}
|
|
115
|
-
|
|
116
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Validate and publish a VOS schema document.
|
|
157
|
+
*
|
|
158
|
+
* Accepts either a raw `.vos` source string, one `@voml/vos` descriptor
|
|
159
|
+
* (`v.table()`, `v.class()`) or a list of descriptors. When descriptors
|
|
160
|
+
* are supplied, they are rendered to source via `emitSource` first.
|
|
161
|
+
*/
|
|
162
|
+
async ensureSchema(...input) {
|
|
163
|
+
let document;
|
|
164
|
+
if (typeof input[0] === 'string') {
|
|
165
|
+
document = input[0];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
document = emitSource(input);
|
|
169
|
+
}
|
|
170
|
+
this.native.ensureSchema(document);
|
|
117
171
|
}
|
|
172
|
+
/** Read one record, or `null` when the key is absent. */
|
|
118
173
|
async get(key) {
|
|
119
|
-
return this.
|
|
174
|
+
return this.native.get(key);
|
|
120
175
|
}
|
|
176
|
+
/** Insert or replace one record. */
|
|
121
177
|
async put(key, value) {
|
|
122
|
-
|
|
178
|
+
const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
|
|
179
|
+
this.native.put(key, bytes);
|
|
180
|
+
}
|
|
181
|
+
/** Delete one record by key. Silently succeeds when the key is absent. */
|
|
182
|
+
async delete(key) {
|
|
183
|
+
this.native.delete(key);
|
|
184
|
+
}
|
|
185
|
+
/** List key/value pairs in lexical order. `prefix` defaults to `""` (all keys). */
|
|
186
|
+
async scan(options = {}) {
|
|
187
|
+
const rowsJson = this.native.scanPrefix(options.prefix ?? '', options.after ?? null, options.limit ?? 1000);
|
|
188
|
+
const rows = JSON.parse(rowsJson);
|
|
189
|
+
return rows.map((r) => ({ key: r.key, value: new Uint8Array(r.value) }));
|
|
190
|
+
}
|
|
191
|
+
/** Atomically write or delete many keys in one call. */
|
|
192
|
+
async batch(changes) {
|
|
193
|
+
const body = changes.map((c) => ({
|
|
194
|
+
key: c.key,
|
|
195
|
+
value: c.value === null
|
|
196
|
+
? null
|
|
197
|
+
: typeof c.value === 'string'
|
|
198
|
+
? Array.from(new TextEncoder().encode(c.value))
|
|
199
|
+
: Array.from(c.value),
|
|
200
|
+
}));
|
|
201
|
+
this.native.writeBatch(JSON.stringify(body));
|
|
123
202
|
}
|
|
124
203
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
204
|
+
* Compare-and-swap: replace or delete `key` only when its current value
|
|
205
|
+
* exactly equals `expected`. Both `expected` and `replacement` may be
|
|
206
|
+
* `null` (meaning absent / delete). Returns `true` when the swap applied.
|
|
127
207
|
*/
|
|
208
|
+
async compareExchange(key, expected, replacement) {
|
|
209
|
+
const toBytes = (v) => v === null ? null : typeof v === 'string' ? new TextEncoder().encode(v) : v;
|
|
210
|
+
return this.native.compareExchange(key, toBytes(expected), toBytes(replacement));
|
|
211
|
+
}
|
|
212
|
+
/** Fold the WAL into the main file and truncate the sidecar. */
|
|
213
|
+
async checkpoint() {
|
|
214
|
+
this.native.checkpoint();
|
|
215
|
+
}
|
|
216
|
+
/** Checkpoint then copy a self-contained `.yydb` main file to `dest`. */
|
|
217
|
+
async backup(dest) {
|
|
218
|
+
this.native.backup(dest);
|
|
219
|
+
}
|
|
220
|
+
/** Copy main + WAL + SHM without checkpoint; opening `dest` replays the WAL tail. */
|
|
221
|
+
async backupWithWal(dest) {
|
|
222
|
+
this.native.backupWithWal(dest);
|
|
223
|
+
}
|
|
224
|
+
/** Run read-only consistency probes against the database. */
|
|
225
|
+
async doctor() {
|
|
226
|
+
return JSON.parse(this.native.doctor());
|
|
227
|
+
}
|
|
228
|
+
/** Fold WAL + reclaim orphan blobs (`.yydx` layouts only). */
|
|
229
|
+
async compactYydx() {
|
|
230
|
+
return JSON.parse(this.native.compactYydx());
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Run `fn` inside an explicit transaction. The transaction commits on
|
|
234
|
+
* normal return and rolls back on throw. Nested calls are not supported.
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* await db.transaction(async (tx) => {
|
|
238
|
+
* await tx.put('a', '1');
|
|
239
|
+
* await tx.put('b', '2');
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
async transaction(fn) {
|
|
244
|
+
this.native.begin();
|
|
245
|
+
const tx = new TransactionHandle(this.native);
|
|
246
|
+
try {
|
|
247
|
+
const result = await fn(tx);
|
|
248
|
+
this.native.commit();
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
try {
|
|
253
|
+
this.native.rollback();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// rollback already failed, don't mask the original error
|
|
257
|
+
}
|
|
258
|
+
throw YydbError.fromDiagnosticJson(err instanceof Error ? err.message : String(err)) ?? err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Prepare a VOS read pipeline into a reusable query plan.
|
|
263
|
+
*
|
|
264
|
+
* Returns a {@link Statement} handle. The actual lowered op tree lives
|
|
265
|
+
* on the native side in a thread-local registry — you can call
|
|
266
|
+
* `statement.execute({ paramName: value })` cheaply multiple times
|
|
267
|
+
* without re-parsing or re-lowering the source.
|
|
268
|
+
*
|
|
269
|
+
* Prepare requires no schema binding; plan building is a pure parse +
|
|
270
|
+
* lower pass. Execute still needs `ensureSchema` first.
|
|
271
|
+
*
|
|
272
|
+
* ```ts
|
|
273
|
+
* const stmt = await db.prepare('User.all().take($limit).collect()');
|
|
274
|
+
* const rows = await stmt.execute({ limit: 100 });
|
|
275
|
+
* const rows2 = await stmt.execute({ limit: 50 });
|
|
276
|
+
* stmt.close();
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
async prepare(source) {
|
|
280
|
+
const nativeStmt = this.native.prepare(source);
|
|
281
|
+
return new Statement(nativeStmt, this.native);
|
|
282
|
+
}
|
|
283
|
+
/** Execute a VOS read pipeline and return typed rows. */
|
|
284
|
+
async query(source) {
|
|
285
|
+
const rowsJson = this.native.queryVos(source);
|
|
286
|
+
return JSON.parse(rowsJson);
|
|
287
|
+
}
|
|
288
|
+
/** Execute a VOS write program. */
|
|
289
|
+
async execute(source) {
|
|
290
|
+
this.native.executeVosWrite(source);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Publish a new schema version, migrating existing records when their
|
|
294
|
+
* shape no longer matches.
|
|
295
|
+
*
|
|
296
|
+
* Accepts raw `.vos` source or `@voml/vos` descriptors (same as
|
|
297
|
+
* {@link Database.ensureSchema}). Use `renames` to tell the engine which
|
|
298
|
+
* old type/field names map to the new ones — records that would otherwise
|
|
299
|
+
* look structurally unrelated get preserved.
|
|
300
|
+
*/
|
|
301
|
+
async migrateSchema(documentOrSource, ...rest) {
|
|
302
|
+
let document;
|
|
303
|
+
let options = {};
|
|
304
|
+
const all = [documentOrSource, ...rest];
|
|
305
|
+
const last = all[all.length - 1];
|
|
306
|
+
if (last &&
|
|
307
|
+
typeof last === 'object' &&
|
|
308
|
+
!('table' in last) &&
|
|
309
|
+
!('class' in last) &&
|
|
310
|
+
!('emit' in last) &&
|
|
311
|
+
('renames' in last || Object.keys(last).length === 0)) {
|
|
312
|
+
options = all.pop() ?? {};
|
|
313
|
+
}
|
|
314
|
+
if (typeof all[0] === 'string') {
|
|
315
|
+
document = all[0];
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
document = emitSource(all);
|
|
319
|
+
}
|
|
320
|
+
const renamesJson = options.renames?.length
|
|
321
|
+
? JSON.stringify(options.renames)
|
|
322
|
+
: null;
|
|
323
|
+
this.native.migrateSchema(document, renamesJson);
|
|
324
|
+
}
|
|
325
|
+
/** Build a typed session-local micro definition (without registering). */
|
|
128
326
|
defineMicro(definition) {
|
|
129
327
|
return buildMicro(definition);
|
|
130
328
|
}
|
|
131
329
|
/**
|
|
132
|
-
* Install a
|
|
133
|
-
*
|
|
134
|
-
* session-local and never written to `.yydb`.
|
|
330
|
+
* Install a session-local host micro into both the local registry and the
|
|
331
|
+
* native engine. Host micros never persist across processes.
|
|
135
332
|
*/
|
|
136
333
|
async registerMicro(definition) {
|
|
137
334
|
const handle = this.microRegistry.register(definition);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
udfVersion: definition.version,
|
|
142
|
-
name: definition.name,
|
|
143
|
-
functionId: handle.functionId,
|
|
144
|
-
args: definition.args.map(udfWireKind),
|
|
145
|
-
returns: udfWireKind(definition.returns),
|
|
146
|
-
fingerprint: microFingerprint(definition.name, definition.version),
|
|
147
|
-
});
|
|
335
|
+
const argTypes = definition.args.map((d) => udfWireKind(d));
|
|
336
|
+
const returnType = udfWireKind(definition.returns);
|
|
337
|
+
this.native.registerMicro(definition.name, definition.version, argTypes, returnType, handle.hostId, handle.functionId, handle.implementationVersion, microFingerprint(definition.name, definition.version));
|
|
148
338
|
return handle;
|
|
149
339
|
}
|
|
150
|
-
/** Invoke a registered scalar UDF on the
|
|
340
|
+
/** Invoke a registered scalar UDF on the native engine. */
|
|
151
341
|
async callScalar(name, version, args) {
|
|
152
|
-
|
|
342
|
+
const argsJson = JSON.stringify(args);
|
|
343
|
+
const resultJson = this.native.callScalar(name, version, argsJson);
|
|
344
|
+
return JSON.parse(resultJson);
|
|
153
345
|
}
|
|
154
|
-
/**
|
|
346
|
+
/** Release the native handle (best-effort; the engine also drops on GC). */
|
|
155
347
|
async close() {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.child = null;
|
|
159
|
-
if (!child || child.exitCode !== null || child.killed) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
await new Promise((resolve) => {
|
|
163
|
-
child.once('exit', () => resolve());
|
|
164
|
-
child.kill('SIGTERM');
|
|
165
|
-
setTimeout(() => {
|
|
166
|
-
if (child.exitCode === null && !child.killed) {
|
|
167
|
-
child.kill('SIGKILL');
|
|
168
|
-
}
|
|
169
|
-
resolve();
|
|
170
|
-
}, 2000);
|
|
171
|
-
});
|
|
348
|
+
// Native connections drop cleanly when the handle is garbage-collected.
|
|
349
|
+
// No explicit close() is exposed from yydb::Connection.
|
|
172
350
|
}
|
|
173
351
|
}
|
package/dist/cli/cli.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { createCli } from '@vmz/commander';
|
|
5
|
+
import { formatDiagnostic } from '@vmz/diagnostic';
|
|
6
|
+
import { YydbError } from '../shared/errors.js';
|
|
4
7
|
import { registerInfoCommand } from './info-cmd.js';
|
|
5
8
|
import { registerInitCommand } from './init-cmd.js';
|
|
6
9
|
import { registerServeCommand } from './serve-cmd.js';
|
|
10
|
+
import { registerVerifyCommand } from './verify-cmd.js';
|
|
7
11
|
import { registerVersionCommand } from './version-cmd.js';
|
|
12
|
+
const CLI_CATALOG = JSON.parse(fs.readFileSync(resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
|
|
8
13
|
function buildYydbCli() {
|
|
9
14
|
const localesRoot = join(dirname(fileURLToPath(import.meta.url)), '../../locales');
|
|
10
15
|
const cli = createCli('yydb').locales(localesRoot).intro('cli.intro');
|
|
11
16
|
registerVersionCommand(cli);
|
|
12
17
|
registerInitCommand(cli);
|
|
13
18
|
registerInfoCommand(cli);
|
|
19
|
+
registerVerifyCommand(cli);
|
|
14
20
|
registerServeCommand(cli);
|
|
15
21
|
return cli;
|
|
16
22
|
}
|
|
@@ -19,6 +25,27 @@ export async function runCli(argv) {
|
|
|
19
25
|
return await buildYydbCli().parse(argv);
|
|
20
26
|
}
|
|
21
27
|
catch (error) {
|
|
28
|
+
const parsed = error instanceof Error
|
|
29
|
+
? YydbError.fromDiagnosticJson(error.message)
|
|
30
|
+
: null;
|
|
31
|
+
if (parsed) {
|
|
32
|
+
// Structured engine error from N-API / wire / WASM envelope.
|
|
33
|
+
const severity = parsed.retryable ? 'warning' : 'error';
|
|
34
|
+
const text = formatDiagnostic({
|
|
35
|
+
path: '',
|
|
36
|
+
severity,
|
|
37
|
+
code: parsed.code,
|
|
38
|
+
args: parsed.params,
|
|
39
|
+
message: error instanceof Error ? error.message : String(error),
|
|
40
|
+
}, {
|
|
41
|
+
locale: 'en-US',
|
|
42
|
+
catalog: CLI_CATALOG,
|
|
43
|
+
style: 'compact',
|
|
44
|
+
});
|
|
45
|
+
console.error(text);
|
|
46
|
+
return parsed.retryable ? 13 : 1;
|
|
47
|
+
}
|
|
48
|
+
// Plain text fallback (commander parse failures, unknown errors).
|
|
22
49
|
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
50
|
return 1;
|
|
24
51
|
}
|
package/dist/cli/info-cmd.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatDiagnostic } from '@vmz/diagnostic';
|
|
1
4
|
import { loadNative } from '../native/binding.js';
|
|
2
5
|
import { requiredPositional } from './options.js';
|
|
6
|
+
const CLI_CATALOG = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
|
|
3
7
|
export function registerInfoCommand(cli) {
|
|
4
|
-
cli.command('info', 'cli.cmd.info')
|
|
8
|
+
cli.command('info', 'cli.cmd.info')
|
|
9
|
+
.option('--doctor', 'cli.opt.doctor')
|
|
10
|
+
.action((options) => cmdInfo(options));
|
|
5
11
|
}
|
|
6
12
|
export function cmdInfo(options) {
|
|
7
13
|
const dbPath = requiredPositional(options, 'info');
|
|
14
|
+
const wantDoctor = Boolean(options.doctor);
|
|
8
15
|
const database = loadNative().Database.open(dbPath);
|
|
9
16
|
const lines = [`path=${database.path() ?? '<memory>'}`];
|
|
10
17
|
const schemaVersion = database.schemaVersion();
|
|
@@ -25,6 +32,28 @@ export function cmdInfo(options) {
|
|
|
25
32
|
lines.push(`shm=${shm}`);
|
|
26
33
|
lines.push(`wal_frames=${database.walFrameCount()}`);
|
|
27
34
|
console.log(lines.join('\n'));
|
|
35
|
+
if (wantDoctor) {
|
|
36
|
+
const report = JSON.parse(database.doctor());
|
|
37
|
+
if (report.issues.length === 0) {
|
|
38
|
+
console.log(`doctor=clean orphan_objects=${report.orphan_object_count}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
for (const issue of report.issues) {
|
|
42
|
+
const severity = issue.severity === 'error' ? 'error' : 'warning';
|
|
43
|
+
console.error(formatDiagnostic({
|
|
44
|
+
path: dbPath,
|
|
45
|
+
severity,
|
|
46
|
+
code: issue.code,
|
|
47
|
+
message: issue.message,
|
|
48
|
+
}, {
|
|
49
|
+
locale: 'en-US',
|
|
50
|
+
catalog: CLI_CATALOG,
|
|
51
|
+
style: 'compact',
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
console.error(`doctor=issues(${report.issues.length}) orphans=${report.orphan_object_count}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
28
57
|
return 0;
|
|
29
58
|
}
|
|
30
59
|
function summarizeDocument(document) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Cli, ParsedOptions } from '@vmz/commander';
|
|
2
|
+
export declare function registerVerifyCommand(cli: Cli): void;
|
|
3
|
+
/**
|
|
4
|
+
* Run read-only consistency probes against a YYDB database file.
|
|
5
|
+
*
|
|
6
|
+
* Exit codes:
|
|
7
|
+
* 0 — clean (no issues)
|
|
8
|
+
* 1 — doctor found at least one error (database may be corrupt)
|
|
9
|
+
* 13 — only warnings present (readable but should be inspected)
|
|
10
|
+
*/
|
|
11
|
+
export declare function cmdVerify(options: ParsedOptions): number;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatDiagnostic } from '@vmz/diagnostic';
|
|
4
|
+
import { loadNative } from '../native/binding.js';
|
|
5
|
+
import { requiredPositional } from './options.js';
|
|
6
|
+
/**
|
|
7
|
+
* Inline CLI catalog — commander's `.locales()` resolution is not exposed
|
|
8
|
+
* outside the CLI builder, so we ship a static copy here. Only one locale is
|
|
9
|
+
* loaded at runtime; future locale awareness should resolve the chosen locale
|
|
10
|
+
* from commander internals once they surface it.
|
|
11
|
+
*/
|
|
12
|
+
const CLI_CATALOG = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../../locales/en-US/cli.json'), 'utf8'));
|
|
13
|
+
const DIAGNOSTIC_STYLE = 'compact';
|
|
14
|
+
const DIAGNOSTIC_LOCALE = 'en-US';
|
|
15
|
+
export function registerVerifyCommand(cli) {
|
|
16
|
+
cli.command('verify', 'cli.cmd.verify').action((options) => cmdVerify(options));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run read-only consistency probes against a YYDB database file.
|
|
20
|
+
*
|
|
21
|
+
* Exit codes:
|
|
22
|
+
* 0 — clean (no issues)
|
|
23
|
+
* 1 — doctor found at least one error (database may be corrupt)
|
|
24
|
+
* 13 — only warnings present (readable but should be inspected)
|
|
25
|
+
*/
|
|
26
|
+
export function cmdVerify(options) {
|
|
27
|
+
const dbPath = requiredPositional(options, 'verify');
|
|
28
|
+
let report;
|
|
29
|
+
try {
|
|
30
|
+
const native = loadNative();
|
|
31
|
+
const db = native.Database.open(dbPath);
|
|
32
|
+
report = JSON.parse(db.doctor());
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.error(formatDiagnostic({
|
|
37
|
+
path: dbPath,
|
|
38
|
+
severity: 'error',
|
|
39
|
+
code: 'cli.err.open_failed',
|
|
40
|
+
args: { path: dbPath, reason: message },
|
|
41
|
+
}, {
|
|
42
|
+
locale: DIAGNOSTIC_LOCALE,
|
|
43
|
+
catalog: CLI_CATALOG,
|
|
44
|
+
style: DIAGNOSTIC_STYLE,
|
|
45
|
+
}));
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
for (const issue of report.issues) {
|
|
49
|
+
const severity = issue.severity === 'error' ? 'error' : 'warning';
|
|
50
|
+
console.error(formatDiagnostic({
|
|
51
|
+
path: dbPath,
|
|
52
|
+
severity,
|
|
53
|
+
code: issue.code,
|
|
54
|
+
message: issue.message,
|
|
55
|
+
}, {
|
|
56
|
+
locale: DIAGNOSTIC_LOCALE,
|
|
57
|
+
catalog: CLI_CATALOG,
|
|
58
|
+
style: DIAGNOSTIC_STYLE,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
const errCount = report.issues.filter((i) => i.severity === 'error').length;
|
|
62
|
+
const warnCount = report.issues.length - errCount;
|
|
63
|
+
if (errCount > 0) {
|
|
64
|
+
console.log(`exit=1 errors=${errCount} warnings=${warnCount}`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
if (warnCount > 0) {
|
|
68
|
+
console.log(`exit=13 errors=0 warnings=${warnCount}`);
|
|
69
|
+
return 13;
|
|
70
|
+
}
|
|
71
|
+
console.log(`exit=0 clean last_commit=${report.last_commit_sequence}`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|