hive-am 1.4.0 → 1.5.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/CHANGELOG.md +28 -1
- package/README.md +1 -5
- package/bin/hive-am.mjs +242 -31
- package/bin/run.mjs +135 -0
- package/package.json +1 -1
- package/server/dist/connections/manager.js +15 -3
- package/server/dist/connections/router.js +2 -2
- package/server/dist/connections/secrets.js +8 -0
- package/server/dist/db/migrations.js +68 -0
- package/server/dist/db.js +11 -10
- package/server/dist/efforts.js +105 -0
- package/server/dist/http/routes/agents.js +4 -3
- package/server/dist/http/routes/meta.js +12 -0
- package/server/dist/http/routes/notebook.js +10 -2
- package/server/dist/http/routes/resources.js +3 -0
- package/server/dist/http/routes/types.js +18 -1
- package/server/dist/http/validate.js +5 -2
- package/server/dist/index.js +4 -1
- package/server/dist/instructions.js +10 -2
- package/server/dist/objects/agent-db.js +258 -0
- package/server/dist/objects/database.js +496 -34
- package/server/dist/objects/index.js +16 -8
- package/server/dist/providers/kiro.js +7 -1
- package/server/dist/providers/opencode-server.js +11 -1
- package/server/dist/providers/opencode.js +4 -3
- package/server/dist/providers/spawn.js +38 -4
- package/server/dist/retention.js +50 -0
- package/server/dist/runtime.js +52 -21
- package/server/dist/services/agents.js +53 -1
- package/server/dist/services/notebook.js +7 -3
- package/server/dist/skills/defaults.js +35 -1
- package/server/dist/skills/notebook.js +57 -13
- package/server/mcp/dispatch.mjs +68 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +60 -60
- package/web/.next/app-path-routes-manifest.json +7 -7
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +58 -58
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +4 -4
- package/web/.next/server/app/chat/page.js +1 -1
- package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/chat.html +1 -1
- package/web/.next/server/app/chat.rsc +4 -4
- package/web/.next/server/app/connections/page.js +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +4 -4
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +4 -4
- package/web/.next/server/app/page.js +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +4 -4
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources.html +1 -1
- package/web/.next/server/app/resources.rsc +4 -4
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/schedules.html +1 -1
- package/web/.next/server/app/schedules.rsc +4 -4
- package/web/.next/server/app/sessions/page.js +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +4 -4
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +4 -4
- package/web/.next/server/app/types/page.js +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +4 -4
- package/web/.next/server/app-paths-manifest.json +7 -7
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/213.js +1 -1
- package/web/.next/server/chunks/274.js +1 -1
- package/web/.next/server/chunks/368.js +1 -1
- package/web/.next/server/chunks/403.js +1 -1
- package/web/.next/server/chunks/612.js +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/{316-26da9cc233ba7557.js → 107-77dd87cc2ca4f941.js} +1 -1
- package/web/.next/static/chunks/281-30e32d932b8b5a75.js +1 -0
- package/web/.next/static/chunks/517-b3b1c9044d2ef23c.js +1 -0
- package/web/.next/static/chunks/587-5b511f5f93910180.js +1 -0
- package/web/.next/static/chunks/606-14fb5aaa4d0ff22d.js +1 -0
- package/web/.next/static/chunks/656-f3cc78328603241a.js +1 -0
- package/web/.next/static/chunks/{705-f81face97b1bb1af.js → 705-ce86175dea00e6a9.js} +1 -1
- package/web/.next/static/chunks/757-03361e7a97a605a2.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-638e12ba3cb813c3.js +1 -0
- package/web/.next/static/chunks/app/agents/page-9da499db6d22731f.js +1 -0
- package/web/.next/static/chunks/app/chat/{page-5261b12b7d50f5d1.js → page-383bc24e72dab7fa.js} +1 -1
- package/web/.next/static/chunks/app/connections/{page-eb11cfb1ffeab3ca.js → page-06bd2d7d9495126a.js} +1 -1
- package/web/.next/static/chunks/app/page-0062ebc30322b1ae.js +1 -0
- package/web/.next/static/chunks/app/relations/page-02710fe1f8a670a9.js +1 -0
- package/web/.next/static/chunks/app/sessions/page-85638705fdc18174.js +1 -0
- package/web/.next/static/chunks/app/types/page-8e25f503bdcd72ad.js +1 -0
- package/web/.next/static/css/dfcaa19a8d46e60d.css +1 -0
- package/web/.next/trace +30 -30
- package/web/.next/static/chunks/281-113274e705940a55.js +0 -1
- package/web/.next/static/chunks/517-373a80c8ba432bdc.js +0 -1
- package/web/.next/static/chunks/562-7c2e33d838e869bf.js +0 -1
- package/web/.next/static/chunks/587-8e4d2be01ba7d48f.js +0 -1
- package/web/.next/static/chunks/606-c9a19918b02a98d0.js +0 -1
- package/web/.next/static/chunks/656-f537149fe28f79aa.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-6ed6edb3e74b0ebe.js +0 -1
- package/web/.next/static/chunks/app/agents/page-9bb8cba69776c719.js +0 -1
- package/web/.next/static/chunks/app/page-99e0351e6ccaa384.js +0 -1
- package/web/.next/static/chunks/app/relations/page-a44fd515a3b0525e.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-a7534ebd90e982d6.js +0 -1
- package/web/.next/static/chunks/app/types/page-ebd37e220f77c1f3.js +0 -1
- package/web/.next/static/css/582723fcea1f9887.css +0 -1
- /package/web/.next/static/{YT6HM6qmegMJaBMX5QvuP → UDA414PFPu9ZROltNmjbX}/_buildManifest.js +0 -0
- /package/web/.next/static/{YT6HM6qmegMJaBMX5QvuP → UDA414PFPu9ZROltNmjbX}/_ssgManifest.js +0 -0
|
@@ -252,6 +252,69 @@ async function openMssql(cfg, database) {
|
|
|
252
252
|
r.query(sql);
|
|
253
253
|
});
|
|
254
254
|
},
|
|
255
|
+
/**
|
|
256
|
+
* The plan of a statement, without running it. SHOWPLAN is a setting of the connection that has to be switched on by a batch of its own (the driver
|
|
257
|
+
* sends an ordinary call inside sp_executesql, where the setting does not outlast the call and the statement would RUN). It is done inside a
|
|
258
|
+
* transaction that is always rolled back, so that nothing can stay changed even if the setting did not take.
|
|
259
|
+
*/
|
|
260
|
+
async explain(statement, o) {
|
|
261
|
+
const tx = new mssql.Transaction(pool);
|
|
262
|
+
await tx.begin();
|
|
263
|
+
try {
|
|
264
|
+
const batch = (sql) => { const r = new mssql.Request(tx); r.arrayRowMode = true; return r.batch(sql); };
|
|
265
|
+
await batch('SET SHOWPLAN_TEXT ON');
|
|
266
|
+
let res;
|
|
267
|
+
try {
|
|
268
|
+
res = await Promise.race([batch(statement), new Promise((_, no) => setTimeout(() => no(new Error(`The plan took more than ${Math.round(o.timeoutMs / 1000)} s`)), o.timeoutMs).unref())]);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
await batch('SET SHOWPLAN_TEXT OFF').catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
const set = (res.recordset ?? []);
|
|
274
|
+
const cols = res.recordset?.columns ? Object.values(res.recordset.columns).sort((a, b) => a.index - b.index).map((x) => ({ name: x.name })) : [{ name: 'plan' }];
|
|
275
|
+
return { columns: cols, rows: set.slice(0, o.max), truncated: set.length > o.max };
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
try {
|
|
279
|
+
await tx.rollback();
|
|
280
|
+
}
|
|
281
|
+
catch { /* nothing left to undo */ }
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
// The statements of a script run inside one transaction of the driver (a BEGIN sent as a statement would end with the batch that carries it).
|
|
285
|
+
async script(statements, o) {
|
|
286
|
+
const tx = new mssql.Transaction(pool);
|
|
287
|
+
await tx.begin();
|
|
288
|
+
const out = [];
|
|
289
|
+
try {
|
|
290
|
+
for (let i = 0; i < statements.length; i++) {
|
|
291
|
+
try {
|
|
292
|
+
const r = new mssql.Request(tx);
|
|
293
|
+
r.arrayRowMode = true;
|
|
294
|
+
const res = await Promise.race([r.query(statements[i]), new Promise((_, no) => setTimeout(() => { try {
|
|
295
|
+
r.cancel();
|
|
296
|
+
}
|
|
297
|
+
catch { /* done */ } no(new Error(`The script took more than ${Math.round(o.timeoutMs / 1000)} s`)); }, o.timeoutMs).unref())]);
|
|
298
|
+
const set = res.recordset;
|
|
299
|
+
const cols = set ? Object.values(set.columns).sort((a, b) => a.index - b.index).map((x) => ({ name: x.name })) : [];
|
|
300
|
+
const rows = set ?? [];
|
|
301
|
+
out.push({ columns: cols, rows: rows.slice(0, o.max), truncated: rows.length > o.max, affected: cols.length === 0 ? res.rowsAffected.reduce((a, b) => a + b, 0) : undefined });
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
throw new Error(`Statement ${i + 1} failed and nothing was changed: ${e instanceof Error ? e.message : e}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
await tx.commit();
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
try {
|
|
311
|
+
await tx.rollback();
|
|
312
|
+
}
|
|
313
|
+
catch { /* already rolled back */ }
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
},
|
|
255
318
|
async close() { try {
|
|
256
319
|
await pool.close();
|
|
257
320
|
}
|
|
@@ -269,13 +332,22 @@ let db, out;
|
|
|
269
332
|
try {
|
|
270
333
|
db = new Database(w.file, { readonly: w.readOnly, fileMustExist: true, ...(w.timeout !== undefined ? { timeout: w.timeout } : {}) });
|
|
271
334
|
for (const [k, v] of Object.entries(w.pragmas)) db.pragma(k + ' = ' + v);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
335
|
+
const one = (sql) => {
|
|
336
|
+
const stmt = db.prepare(sql);
|
|
337
|
+
if (stmt.reader) {
|
|
338
|
+
const columns = stmt.columns().map((c) => ({ name: c.name })); stmt.raw(true);
|
|
339
|
+
const rows = []; let truncated = false;
|
|
340
|
+
for (const r of stmt.iterate()) { if (rows.length >= w.max) { truncated = true; break; } rows.push(r.map((v) => typeof v === 'bigint' ? v.toString() : (Buffer.isBuffer(v) ? { $blob: v.toString('base64') } : v))); }
|
|
341
|
+
return { columns, rows, truncated };
|
|
342
|
+
}
|
|
343
|
+
const r = stmt.run(); return { columns: [], rows: [], truncated: false, affected: r.changes };
|
|
344
|
+
};
|
|
345
|
+
if (w.script) {
|
|
346
|
+
// All the statements in one transaction: one failing undoes the others.
|
|
347
|
+
db.exec('BEGIN'); const results = []; let at = 0;
|
|
348
|
+
try { for (; at < w.script.length; at++) results.push(one(w.script[at])); db.exec('COMMIT'); out = { ok: true, script: results }; }
|
|
349
|
+
catch (e) { try { db.exec('ROLLBACK'); } catch {} out = { ok: false, error: String(e && e.message || e), at: at + 1 }; }
|
|
350
|
+
} else out = { ok: true, ...one(w.sql) };
|
|
279
351
|
} catch (e) { out = { ok: false, error: String(e && e.message || e) }; }
|
|
280
352
|
try { db && db.close(); } catch {}
|
|
281
353
|
process.stdout.write(JSON.stringify(out));
|
|
@@ -288,32 +360,42 @@ function openSqlite(cfg) {
|
|
|
288
360
|
// The values were checked when the object was saved (a number, true/false or one of a few words), so they are safe in a PRAGMA.
|
|
289
361
|
const { timeout, ...rest } = plain(propValues('sqlite', cfg.props));
|
|
290
362
|
const pragmas = Object.fromEntries(Object.entries(rest).map(([k, v]) => [k, typeof v === 'boolean' ? (v ? 1 : 0) : v]));
|
|
363
|
+
const blobs = (r) => r.map((v) => (v && typeof v === 'object' && '$blob' in v ? Buffer.from(v.$blob, 'base64') : v));
|
|
364
|
+
const shape = (m) => ({ columns: m.columns, rows: m.rows.map(blobs), truncated: m.truncated, affected: m.affected });
|
|
365
|
+
/** One run of the child process: the message it printed, or an error when it ended without one, took too long or printed too much. */
|
|
366
|
+
const call = (payload, o) => new Promise((ok, no) => {
|
|
367
|
+
const child = spawn(process.execPath, ['-e', SQLITE_CHILD], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
368
|
+
const chunks = [];
|
|
369
|
+
let size = 0, done = false;
|
|
370
|
+
const finish = (err, v) => { if (done)
|
|
371
|
+
return; done = true; clearTimeout(timer); child.kill('SIGKILL'); err ? no(err) : ok(v); };
|
|
372
|
+
const timer = setTimeout(() => finish(new Error(`The query took more than ${Math.round(o.timeoutMs / 1000)} s`)), o.timeoutMs);
|
|
373
|
+
child.stdout.on('data', (c) => { size += c.length; if (size > SQLITE_MAX_OUT)
|
|
374
|
+
finish(new Error('The result is too big: use fewer rows or columns'));
|
|
375
|
+
else
|
|
376
|
+
chunks.push(c); });
|
|
377
|
+
child.once('error', (e) => finish(e));
|
|
378
|
+
child.once('close', () => {
|
|
379
|
+
if (done)
|
|
380
|
+
return;
|
|
381
|
+
try {
|
|
382
|
+
finish(undefined, JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
finish(new Error('The SQLite process ended without an answer'));
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
child.stdin.end(JSON.stringify({ mod, file: cfg.file, max: o.max, readOnly: o.readOnly, timeout, pragmas, ...payload }));
|
|
389
|
+
});
|
|
291
390
|
return {
|
|
292
|
-
run: (sql, o) =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
finish(new Error('The result is too big: use fewer rows or columns'));
|
|
301
|
-
else
|
|
302
|
-
chunks.push(c); });
|
|
303
|
-
child.once('error', (e) => finish(e));
|
|
304
|
-
child.once('close', () => {
|
|
305
|
-
if (done)
|
|
306
|
-
return;
|
|
307
|
-
try {
|
|
308
|
-
const m = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
309
|
-
m.ok ? finish(undefined, { columns: m.columns, rows: m.rows.map((r) => r.map((v) => (v && typeof v === 'object' && '$blob' in v ? Buffer.from(v.$blob, 'base64') : v))), truncated: m.truncated, affected: m.affected }) : finish(new Error(m.error));
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
finish(new Error('The SQLite process ended without an answer'));
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
child.stdin.end(JSON.stringify({ mod, file: cfg.file, sql, max: o.max, readOnly: o.readOnly, timeout, pragmas }));
|
|
316
|
-
}),
|
|
391
|
+
run: async (sql, o) => { const m = await call({ sql }, o); if (!m.ok)
|
|
392
|
+
throw new Error(m.error); return shape(m); },
|
|
393
|
+
script: async (statements, o) => {
|
|
394
|
+
const m = await call({ script: statements }, o);
|
|
395
|
+
if (!m.ok)
|
|
396
|
+
throw new Error(m.at ? `Statement ${m.at} failed and nothing was changed: ${m.error}` : m.error);
|
|
397
|
+
return m.script.map(shape);
|
|
398
|
+
},
|
|
317
399
|
close: async () => undefined,
|
|
318
400
|
};
|
|
319
401
|
}
|
|
@@ -423,8 +505,11 @@ export async function dbColumns(cfg, p) {
|
|
|
423
505
|
FROM information_schema.columns c WHERE c.table_schema = ${lit(schema)} AND c.table_name = ${lit(table)} ORDER BY c.ordinal_position`))
|
|
424
506
|
.map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', key: r[3] ? String(r[3]) : undefined, default: r[4] === null ? null : String(r[4]) }));
|
|
425
507
|
}
|
|
426
|
-
return (await q(`SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
|
427
|
-
|
|
508
|
+
return (await q(`SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT,
|
|
509
|
+
(SELECT TOP 1 'PRI' FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k ON k.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND k.TABLE_SCHEMA = tc.TABLE_SCHEMA
|
|
510
|
+
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = c.TABLE_SCHEMA AND tc.TABLE_NAME = c.TABLE_NAME AND k.COLUMN_NAME = c.COLUMN_NAME)
|
|
511
|
+
FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_SCHEMA = ${lit(schema)} AND c.TABLE_NAME = ${lit(table)} ORDER BY c.ORDINAL_POSITION`))
|
|
512
|
+
.map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', key: r[4] ? String(r[4]) : undefined, default: r[3] === null ? null : String(r[3]) }));
|
|
428
513
|
});
|
|
429
514
|
}
|
|
430
515
|
// ---- state -----------------------------------------------------------------------------------------------------------------------------
|
|
@@ -449,3 +534,380 @@ export async function dbState(id, cfg) {
|
|
|
449
534
|
seen.set(id, { at: Date.now(), state });
|
|
450
535
|
return state;
|
|
451
536
|
}
|
|
537
|
+
// ---- what an agent needs on top of that ------------------------------------------------------------------------------------------------
|
|
538
|
+
/** Splits a script into its statements, the way the engine would read it: a `;` inside a quote, a comment or a dollar-quoted body does not end one. */
|
|
539
|
+
export function splitStatements(sql, engine) {
|
|
540
|
+
const out = [];
|
|
541
|
+
let cur = '', i = 0;
|
|
542
|
+
const push = () => { const t = cur.trim(); if (t)
|
|
543
|
+
out.push(t); cur = ''; };
|
|
544
|
+
while (i < sql.length) {
|
|
545
|
+
const c = sql[i], n = sql[i + 1];
|
|
546
|
+
if (c === '-' && n === '-') {
|
|
547
|
+
const e = sql.indexOf('\n', i);
|
|
548
|
+
const j = e < 0 ? sql.length : e;
|
|
549
|
+
cur += sql.slice(i, j);
|
|
550
|
+
i = j;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (c === '#' && engine === 'mysql') {
|
|
554
|
+
const e = sql.indexOf('\n', i);
|
|
555
|
+
const j = e < 0 ? sql.length : e;
|
|
556
|
+
cur += sql.slice(i, j);
|
|
557
|
+
i = j;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (c === '/' && n === '*') {
|
|
561
|
+
const e = sql.indexOf('*/', i + 2);
|
|
562
|
+
const j = e < 0 ? sql.length : e + 2;
|
|
563
|
+
cur += sql.slice(i, j);
|
|
564
|
+
i = j;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (c === '$' && engine === 'postgresql') {
|
|
568
|
+
const tag = /^\$[A-Za-z_]*\$/.exec(sql.slice(i))?.[0];
|
|
569
|
+
if (tag) {
|
|
570
|
+
const e = sql.indexOf(tag, i + tag.length);
|
|
571
|
+
const j = e < 0 ? sql.length : e + tag.length;
|
|
572
|
+
cur += sql.slice(i, j);
|
|
573
|
+
i = j;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
578
|
+
const q = c;
|
|
579
|
+
let j = i + 1;
|
|
580
|
+
while (j < sql.length) {
|
|
581
|
+
if (sql[j] === '\\' && q !== '`' && engine === 'mysql') {
|
|
582
|
+
j += 2;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (sql[j] === q) {
|
|
586
|
+
if (sql[j + 1] === q) {
|
|
587
|
+
j += 2;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
j++;
|
|
593
|
+
}
|
|
594
|
+
cur += sql.slice(i, j + 1);
|
|
595
|
+
i = j + 1;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (c === '[' && engine === 'mssql') {
|
|
599
|
+
const e = sql.indexOf(']', i);
|
|
600
|
+
const j = e < 0 ? sql.length : e + 1;
|
|
601
|
+
cur += sql.slice(i, j);
|
|
602
|
+
i = j;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (c === ';') {
|
|
606
|
+
push();
|
|
607
|
+
i++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
cur += c;
|
|
611
|
+
i++;
|
|
612
|
+
}
|
|
613
|
+
push();
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* True when the text has a statement that starts with DROP or TRUNCATE, or an `ALTER ... DROP ...`. Comments and what is inside quotes are
|
|
618
|
+
* ignored. It is a guardrail, not a sandbox: a DELETE without a WHERE, a CALL, a PostgreSQL DO block or a stored procedure are not seen.
|
|
619
|
+
*/
|
|
620
|
+
export function isDestructive(sql) {
|
|
621
|
+
return bare(sql).split(';').some((st) => {
|
|
622
|
+
const t = st.trim().toLowerCase();
|
|
623
|
+
return /^(drop|truncate)\b/.test(t) || (/^alter\b/.test(t) && /\bdrop\b/.test(t));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
/** The text with the literal of a password in it replaced by `***` (CREATE USER … IDENTIFIED BY '…', ALTER USER … PASSWORD '…', WITH PASSWORD = '…'). */
|
|
627
|
+
export function maskSecretsInSql(sql) {
|
|
628
|
+
const lit = `('(?:[^'\\\\]|\\\\.|'')*'|"(?:[^"\\\\]|\\\\.|"")*")`;
|
|
629
|
+
return String(sql)
|
|
630
|
+
.replace(new RegExp(`(\\bidentified\\s+(?:with\\s+\\w+\\s+)?by\\s+(?:password\\s+)?)${lit}`, 'gi'), "$1'***'")
|
|
631
|
+
.replace(new RegExp(`(\\b(?:encrypted\\s+|unencrypted\\s+)?password\\s*(?:=\\s*|\\(\\s*)?(?:password\\s*\\(\\s*)?)${lit}`, 'gi'), "$1'***'")
|
|
632
|
+
.replace(new RegExp(`(\\bpwdencrypt\\s*\\(\\s*)${lit}`, 'gi'), "$1'***'");
|
|
633
|
+
}
|
|
634
|
+
/** Quotes a table or schema name for the engine. */
|
|
635
|
+
export const quoteIdent = (engine, id) => engine === 'mysql' ? `\`${id.replace(/`/g, '``')}\`` : engine === 'mssql' ? `[${id.replace(/]/g, ']]')}]` : `"${id.replace(/"/g, '""')}"`;
|
|
636
|
+
const qualified = (cfg, schema, table) => `${quoteIdent(cfg.engine, schema)}.${quoteIdent(cfg.engine, table)}`;
|
|
637
|
+
const MAX_STATEMENTS = 100;
|
|
638
|
+
/**
|
|
639
|
+
* Several statements as one unit: all of them are applied or none (MySQL commits by itself on a DDL statement, so a CREATE or ALTER in the
|
|
640
|
+
* middle of a script cannot be undone there). A failure says which statement it was.
|
|
641
|
+
*/
|
|
642
|
+
export async function dbExec(cfg, p) {
|
|
643
|
+
if (typeof p.sql !== 'string' || !p.sql.trim())
|
|
644
|
+
throw new ObjectError('Write some SQL');
|
|
645
|
+
if (p.sql.length > 200_000)
|
|
646
|
+
throw new ObjectError('The script is too long');
|
|
647
|
+
if (p.database !== undefined)
|
|
648
|
+
name(p.database, 'The database');
|
|
649
|
+
const statements = splitStatements(p.sql, cfg.engine);
|
|
650
|
+
if (!statements.length)
|
|
651
|
+
throw new ObjectError('Write some SQL');
|
|
652
|
+
if (statements.length > MAX_STATEMENTS)
|
|
653
|
+
throw new ObjectError(`A script has at most ${MAX_STATEMENTS} statements`);
|
|
654
|
+
const readOnly = p.readOnly ?? cfg.readOnly !== false;
|
|
655
|
+
if (readOnly)
|
|
656
|
+
for (const st of statements)
|
|
657
|
+
assertReadOnly(st);
|
|
658
|
+
const o = optsOf(cfg, readOnly);
|
|
659
|
+
const t0 = Date.now();
|
|
660
|
+
const raw = await withConn(cfg, p.database, async (c) => {
|
|
661
|
+
if (c.script)
|
|
662
|
+
return c.script(statements, o);
|
|
663
|
+
const begin = cfg.engine === 'mssql' ? 'BEGIN TRANSACTION' : cfg.engine === 'mysql' ? 'START TRANSACTION' : 'BEGIN';
|
|
664
|
+
const results = [];
|
|
665
|
+
await c.run(begin, { ...o, readOnly: false });
|
|
666
|
+
try {
|
|
667
|
+
for (let i = 0; i < statements.length; i++) {
|
|
668
|
+
try {
|
|
669
|
+
results.push(await c.run(statements[i], { ...o, readOnly: false }));
|
|
670
|
+
}
|
|
671
|
+
catch (e) {
|
|
672
|
+
throw new Error(`Statement ${i + 1} failed and nothing was changed: ${e instanceof Error ? e.message : e}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
await c.run(cfg.engine === 'mssql' ? 'COMMIT TRANSACTION' : 'COMMIT', { ...o, readOnly: false });
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
await c.run(cfg.engine === 'mssql' ? 'ROLLBACK TRANSACTION' : 'ROLLBACK', { ...o, readOnly: false }).catch(() => undefined);
|
|
679
|
+
throw e;
|
|
680
|
+
}
|
|
681
|
+
return results;
|
|
682
|
+
});
|
|
683
|
+
return { results: raw.map((r, i) => ({ ...r, rows: r.rows.map((row) => row.map(cell)), statement: statements[i] })), ms: Date.now() - t0 };
|
|
684
|
+
}
|
|
685
|
+
/** Groups rows `[name, …, column]` that arrive ordered by name into `{ name, columns[] }`. */
|
|
686
|
+
function grouped(rows, make, col) {
|
|
687
|
+
const out = [];
|
|
688
|
+
for (const r of rows) {
|
|
689
|
+
const n = String(r[0]);
|
|
690
|
+
const cur = out.find((x) => x.name === n);
|
|
691
|
+
if (cur)
|
|
692
|
+
cur.columns.push(col(r));
|
|
693
|
+
else
|
|
694
|
+
out.push(make(r));
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
export async function dbDescribe(cfg, p) {
|
|
699
|
+
const schema = name(p.schema, 'The schema'), table = name(p.table, 'The table');
|
|
700
|
+
const columns = await dbColumns(cfg, p);
|
|
701
|
+
return withConn(cfg, p.database, async (c) => {
|
|
702
|
+
const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
|
|
703
|
+
let indexes = [], foreignKeys = [];
|
|
704
|
+
if (cfg.engine === 'sqlite') {
|
|
705
|
+
for (const r of await q(`PRAGMA index_list(${lit(table)})`)) {
|
|
706
|
+
const cols = (await q(`PRAGMA index_info(${lit(String(r[1]))})`)).map((x) => String(x[2]));
|
|
707
|
+
indexes.push({ name: String(r[1]), unique: !!r[2], columns: cols });
|
|
708
|
+
}
|
|
709
|
+
const fk = await q(`PRAGMA foreign_key_list(${lit(table)})`);
|
|
710
|
+
foreignKeys = [];
|
|
711
|
+
for (const id of [...new Set(fk.map((r) => String(r[0])))]) {
|
|
712
|
+
const part = fk.filter((r) => String(r[0]) === id);
|
|
713
|
+
foreignKeys.push({ name: `fk_${table}_${id}`, columns: part.map((r) => String(r[3])), refTable: String(part[0][2]), refColumns: part.map((r) => String(r[4])) });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else if (cfg.engine === 'mysql') {
|
|
717
|
+
indexes = grouped(await q(`SELECT index_name, non_unique, column_name FROM information_schema.statistics WHERE table_schema = ${lit(schema)} AND table_name = ${lit(table)} ORDER BY index_name, seq_in_index`), (r) => ({ name: String(r[0]), unique: !Number(r[1]), columns: [String(r[2])] }), (r) => String(r[2]));
|
|
718
|
+
foreignKeys = grouped(await q(`SELECT constraint_name, referenced_table_schema, referenced_table_name, column_name, referenced_column_name FROM information_schema.key_column_usage WHERE table_schema = ${lit(schema)} AND table_name = ${lit(table)} AND referenced_table_name IS NOT NULL ORDER BY constraint_name, ordinal_position`), (r) => ({ name: String(r[0]), columns: [String(r[3])], refSchema: String(r[1]), refTable: String(r[2]), refColumns: [String(r[4])] }), (r) => String(r[3]));
|
|
719
|
+
for (const f of foreignKeys)
|
|
720
|
+
f.refColumns = (await q(`SELECT referenced_column_name FROM information_schema.key_column_usage WHERE table_schema = ${lit(schema)} AND table_name = ${lit(table)} AND constraint_name = ${lit(f.name)} ORDER BY ordinal_position`)).map((r) => String(r[0]));
|
|
721
|
+
}
|
|
722
|
+
else if (cfg.engine === 'postgresql') {
|
|
723
|
+
indexes = grouped(await q(`SELECT i.relname, ix.indisunique, a.attname FROM pg_index ix JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
724
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
725
|
+
WHERE n.nspname = ${lit(schema)} AND t.relname = ${lit(table)} ORDER BY i.relname, k.ord`), (r) => ({ name: String(r[0]), unique: !!r[1], columns: [String(r[2])] }), (r) => String(r[2]));
|
|
726
|
+
const fk = await q(`SELECT con.conname, rn.nspname, rt.relname, a.attname, ra.attname FROM pg_constraint con JOIN pg_class t ON t.oid = con.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
727
|
+
JOIN pg_class rt ON rt.oid = con.confrelid JOIN pg_namespace rn ON rn.oid = rt.relnamespace
|
|
728
|
+
JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS u(ck, fk, ord) ON true JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = u.ck JOIN pg_attribute ra ON ra.attrelid = rt.oid AND ra.attnum = u.fk
|
|
729
|
+
WHERE con.contype = 'f' AND n.nspname = ${lit(schema)} AND t.relname = ${lit(table)} ORDER BY con.conname, u.ord`);
|
|
730
|
+
foreignKeys = [];
|
|
731
|
+
for (const r of fk) {
|
|
732
|
+
const cur = foreignKeys.find((x) => x.name === String(r[0]));
|
|
733
|
+
if (cur) {
|
|
734
|
+
cur.columns.push(String(r[3]));
|
|
735
|
+
cur.refColumns.push(String(r[4]));
|
|
736
|
+
}
|
|
737
|
+
else
|
|
738
|
+
foreignKeys.push({ name: String(r[0]), columns: [String(r[3])], refSchema: String(r[1]), refTable: String(r[2]), refColumns: [String(r[4])] });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
const obj = lit(`${quoteIdent('mssql', schema)}.${quoteIdent('mssql', table)}`);
|
|
743
|
+
indexes = grouped(await q(`SELECT i.name, i.is_unique, c.name FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
744
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = OBJECT_ID(${obj}) AND i.name IS NOT NULL ORDER BY i.name, ic.key_ordinal`), (r) => ({ name: String(r[0]), unique: !!r[1], columns: [String(r[2])] }), (r) => String(r[2]));
|
|
745
|
+
const fk = await q(`SELECT fk.name, rs.name, rt.name, pc.name, rc.name FROM sys.foreign_keys fk JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
|
|
746
|
+
JOIN sys.columns pc ON pc.object_id = fkc.parent_object_id AND pc.column_id = fkc.parent_column_id JOIN sys.tables rt ON rt.object_id = fkc.referenced_object_id
|
|
747
|
+
JOIN sys.schemas rs ON rs.schema_id = rt.schema_id JOIN sys.columns rc ON rc.object_id = fkc.referenced_object_id AND rc.column_id = fkc.referenced_column_id
|
|
748
|
+
WHERE fk.parent_object_id = OBJECT_ID(${obj}) ORDER BY fk.name, fkc.constraint_column_id`);
|
|
749
|
+
foreignKeys = [];
|
|
750
|
+
for (const r of fk) {
|
|
751
|
+
const cur = foreignKeys.find((x) => x.name === String(r[0]));
|
|
752
|
+
if (cur) {
|
|
753
|
+
cur.columns.push(String(r[3]));
|
|
754
|
+
cur.refColumns.push(String(r[4]));
|
|
755
|
+
}
|
|
756
|
+
else
|
|
757
|
+
foreignKeys.push({ name: String(r[0]), columns: [String(r[3])], refSchema: String(r[1]), refTable: String(r[2]), refColumns: [String(r[4])] });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
let rowCount = null;
|
|
761
|
+
try {
|
|
762
|
+
rowCount = Number((await q(`SELECT COUNT(*) FROM ${qualified(cfg, schema, table)}`))[0]?.[0]);
|
|
763
|
+
}
|
|
764
|
+
catch { /* a view that cannot be counted */ }
|
|
765
|
+
return { schema, table, columns, primaryKey: columns.filter((x) => x.key === 'PRI').map((x) => x.name), indexes, foreignKeys, rowCount };
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/** Tables and columns whose name contains `text`, across everything the connection can see (or the file, for SQLite). */
|
|
769
|
+
export async function dbSearch(cfg, p) {
|
|
770
|
+
const text = typeof p.query === 'string' ? p.query.trim() : '';
|
|
771
|
+
if (!text || text.length > 100)
|
|
772
|
+
throw new ObjectError('Give a name to look for (up to 100 characters)');
|
|
773
|
+
if (p.database !== undefined)
|
|
774
|
+
name(p.database, 'The database');
|
|
775
|
+
const like = lit(`%${text.replace(/[!%_]/g, '!$&')}%`) + " ESCAPE '!'";
|
|
776
|
+
const LIMIT = 200;
|
|
777
|
+
return withConn(cfg, p.database, async (c) => {
|
|
778
|
+
const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
|
|
779
|
+
if (cfg.engine === 'sqlite') {
|
|
780
|
+
const tables = await q("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
781
|
+
const out = [], needle = text.toLowerCase();
|
|
782
|
+
for (const t of tables) {
|
|
783
|
+
const tn = String(t[0]), kind = t[1] === 'view' ? 'view' : 'table';
|
|
784
|
+
if (tn.toLowerCase().includes(needle))
|
|
785
|
+
out.push({ schema: 'main', table: tn, type: kind });
|
|
786
|
+
for (const col of await q(`PRAGMA table_info(${lit(tn)})`))
|
|
787
|
+
if (String(col[1]).toLowerCase().includes(needle))
|
|
788
|
+
out.push({ schema: 'main', table: tn, column: String(col[1]), type: 'column' });
|
|
789
|
+
if (out.length >= LIMIT)
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
return out.slice(0, LIMIT);
|
|
793
|
+
}
|
|
794
|
+
const sys = cfg.engine === 'mysql' ? "('information_schema','mysql','performance_schema','sys')" : cfg.engine === 'postgresql' ? "('pg_catalog','information_schema')" : "('INFORMATION_SCHEMA','sys')";
|
|
795
|
+
const top = cfg.engine === 'mssql' ? `TOP ${LIMIT} ` : '', limit = cfg.engine === 'mssql' ? '' : ` LIMIT ${LIMIT}`;
|
|
796
|
+
const tables = await q(`SELECT ${top}table_schema, table_name, table_type FROM information_schema.tables WHERE table_schema NOT IN ${sys} AND table_name LIKE ${like} ORDER BY 1, 2${limit}`);
|
|
797
|
+
const cols = await q(`SELECT ${top}table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema NOT IN ${sys} AND column_name LIKE ${like} ORDER BY 1, 2, 3${limit}`);
|
|
798
|
+
return [
|
|
799
|
+
...tables.map((r) => ({ schema: String(r[0]), table: String(r[1]), type: /view/i.test(String(r[2])) ? 'view' : 'table' })),
|
|
800
|
+
...cols.map((r) => ({ schema: String(r[0]), table: String(r[1]), column: String(r[2]), type: 'column' })),
|
|
801
|
+
].slice(0, LIMIT);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/** The statement that creates a table. MySQL and SQLite keep it; for PostgreSQL and SQL Server it is put together from the catalog. */
|
|
805
|
+
export async function dbDdl(cfg, p) {
|
|
806
|
+
const schema = name(p.schema, 'The schema'), table = name(p.table, 'The table');
|
|
807
|
+
if (p.database !== undefined)
|
|
808
|
+
name(p.database, 'The database');
|
|
809
|
+
if (cfg.engine === 'postgresql') {
|
|
810
|
+
return withConn(cfg, p.database, async (c) => {
|
|
811
|
+
const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
|
|
812
|
+
const cols = await q(`SELECT a.attname, format_type(a.atttypid, a.atttypmod), a.attnotnull, pg_get_expr(d.adbin, d.adrelid) FROM pg_attribute a JOIN pg_class t ON t.oid = a.attrelid JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
813
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum WHERE n.nspname = ${lit(schema)} AND t.relname = ${lit(table)} AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`);
|
|
814
|
+
if (!cols.length)
|
|
815
|
+
throw new ObjectError('That table does not exist');
|
|
816
|
+
const cons = await q(`SELECT con.conname, pg_get_constraintdef(con.oid) FROM pg_constraint con JOIN pg_class t ON t.oid = con.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = ${lit(schema)} AND t.relname = ${lit(table)} ORDER BY con.contype, con.conname`);
|
|
817
|
+
const idx = await q(`SELECT indexdef FROM pg_indexes WHERE schemaname = ${lit(schema)} AND tablename = ${lit(table)} AND indexname NOT IN (SELECT conname FROM pg_constraint WHERE contype IN ('p','u'))`);
|
|
818
|
+
const lines = [...cols.map((r) => ` ${quoteIdent('postgresql', String(r[0]))} ${r[1]}${r[2] ? ' NOT NULL' : ''}${r[3] !== null ? ` DEFAULT ${r[3]}` : ''}`), ...cons.map((r) => ` CONSTRAINT ${quoteIdent('postgresql', String(r[0]))} ${r[1]}`)];
|
|
819
|
+
return [`CREATE TABLE ${qualified(cfg, schema, table)} (\n${lines.join(',\n')}\n);`, ...idx.map((r) => `${r[0]};`)].join('\n');
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (cfg.engine === 'mssql') {
|
|
823
|
+
const d = await dbDescribe(cfg, p);
|
|
824
|
+
return withConn(cfg, p.database, async (c) => {
|
|
825
|
+
const rows = (await c.run(`SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ${lit(schema)} AND TABLE_NAME = ${lit(table)} ORDER BY ORDINAL_POSITION`, internal(cfg))).rows;
|
|
826
|
+
if (!rows.length)
|
|
827
|
+
throw new ObjectError('That table does not exist');
|
|
828
|
+
const type = (r) => { const t = String(r[1]); return /char|binary/i.test(t) ? `${t}(${Number(r[2]) < 0 ? 'max' : r[2]})` : /decimal|numeric/i.test(t) ? `${t}(${r[3]},${r[4]})` : t; };
|
|
829
|
+
const lines = rows.map((r) => ` ${quoteIdent('mssql', String(r[0]))} ${type(r)}${r[5] === 'YES' ? ' NULL' : ' NOT NULL'}${r[6] !== null ? ` DEFAULT ${r[6]}` : ''}`);
|
|
830
|
+
if (d.primaryKey.length)
|
|
831
|
+
lines.push(` PRIMARY KEY (${d.primaryKey.map((x) => quoteIdent('mssql', x)).join(', ')})`);
|
|
832
|
+
for (const f of d.foreignKeys)
|
|
833
|
+
lines.push(` CONSTRAINT ${quoteIdent('mssql', f.name)} FOREIGN KEY (${f.columns.map((x) => quoteIdent('mssql', x)).join(', ')}) REFERENCES ${qualified(cfg, f.refSchema ?? schema, f.refTable)} (${f.refColumns.map((x) => quoteIdent('mssql', x)).join(', ')})`);
|
|
834
|
+
const idx = d.indexes.filter((i) => !(d.primaryKey.length && i.columns.join() === d.primaryKey.join() && i.unique)).map((i) => `CREATE ${i.unique ? 'UNIQUE ' : ''}INDEX ${quoteIdent('mssql', i.name)} ON ${qualified(cfg, schema, table)} (${i.columns.map((x) => quoteIdent('mssql', x)).join(', ')});`);
|
|
835
|
+
return [`CREATE TABLE ${qualified(cfg, schema, table)} (\n${lines.join(',\n')}\n);`, ...idx].join('\n');
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return withConn(cfg, p.database, async (c) => {
|
|
839
|
+
const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
|
|
840
|
+
if (cfg.engine === 'sqlite') {
|
|
841
|
+
const rows = await q(`SELECT sql FROM sqlite_master WHERE tbl_name = ${lit(table)} AND sql IS NOT NULL ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'view' THEN 0 ELSE 1 END, name`);
|
|
842
|
+
if (!rows.length)
|
|
843
|
+
throw new ObjectError('That table does not exist');
|
|
844
|
+
return rows.map((r) => `${String(r[0]).replace(/;\s*$/, '')};`).join('\n');
|
|
845
|
+
}
|
|
846
|
+
const r = (await q(`SHOW CREATE TABLE ${qualified(cfg, schema, table)}`))[0];
|
|
847
|
+
if (!r)
|
|
848
|
+
throw new ObjectError('That table does not exist');
|
|
849
|
+
return `${String(r[1])};`;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
/** The first rows of a table, without writing SQL. */
|
|
853
|
+
export async function dbSample(cfg, p) {
|
|
854
|
+
const schema = name(p.schema, 'The schema'), table = name(p.table, 'The table');
|
|
855
|
+
const n = Math.max(1, Math.min(Number.isFinite(Number(p.limit)) ? Math.floor(Number(p.limit)) : 10, cfg.maxRows ?? DEFAULT_ROWS));
|
|
856
|
+
const from = qualified(cfg, schema, table);
|
|
857
|
+
const sql = cfg.engine === 'mssql' ? `SELECT TOP (${n}) * FROM ${from}` : `SELECT * FROM ${from} LIMIT ${n}`;
|
|
858
|
+
return dbQuery(cfg, { sql, database: p.database, readOnly: true });
|
|
859
|
+
}
|
|
860
|
+
/** The plan the engine would follow, without running the statement. */
|
|
861
|
+
export async function dbExplain(cfg, p) {
|
|
862
|
+
if (typeof p.sql !== 'string' || !p.sql.trim())
|
|
863
|
+
throw new ObjectError('Write a query');
|
|
864
|
+
if (p.database !== undefined)
|
|
865
|
+
name(p.database, 'The database');
|
|
866
|
+
const statements = splitStatements(p.sql, cfg.engine);
|
|
867
|
+
if (statements.length !== 1)
|
|
868
|
+
throw new ObjectError('Explain takes one statement');
|
|
869
|
+
// EXPLAIN ANALYZE (and ANALYZE) run the statement to measure it: only the plain plan is offered.
|
|
870
|
+
if (/^\s*(explain|analy[sz]e|describe|desc)\b/i.test(bare(statements[0])))
|
|
871
|
+
throw new ObjectError('Give the statement itself, not an EXPLAIN or ANALYZE of it.');
|
|
872
|
+
const t0 = Date.now();
|
|
873
|
+
const r = await withConn(cfg, p.database, async (c) => {
|
|
874
|
+
const o = internal(cfg);
|
|
875
|
+
if (c.explain)
|
|
876
|
+
return c.explain(statements[0], o);
|
|
877
|
+
// MySQL refuses EXPLAIN of a write on a read-only connection although it does not run it.
|
|
878
|
+
return c.run(`${cfg.engine === 'sqlite' ? 'EXPLAIN QUERY PLAN' : 'EXPLAIN'} ${statements[0]}`, cfg.engine === 'mysql' ? { ...o, readOnly: false } : o);
|
|
879
|
+
});
|
|
880
|
+
return { ...r, rows: r.rows.map((row) => row.map(cell)), ms: Date.now() - t0 };
|
|
881
|
+
}
|
|
882
|
+
/** Who is connected and what they are running. */
|
|
883
|
+
export async function dbSessions(cfg) {
|
|
884
|
+
if (cfg.engine === 'sqlite')
|
|
885
|
+
throw new ObjectError('SQLite has no sessions: it is a file, not a server.');
|
|
886
|
+
const sql = cfg.engine === 'mysql' ? 'SHOW FULL PROCESSLIST'
|
|
887
|
+
: cfg.engine === 'postgresql' ? "SELECT pid, usename, datname, state, now() - query_start AS running, left(query, 200) AS query FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND backend_type = 'client backend' ORDER BY query_start NULLS LAST"
|
|
888
|
+
: "SELECT s.session_id, s.login_name, s.host_name, DB_NAME(s.database_id) AS db, s.status, r.command, r.total_elapsed_time / 1000 AS seconds, SUBSTRING(t.text, 1, 200) AS query FROM sys.dm_exec_sessions s LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE s.is_user_process = 1 AND s.session_id <> @@SPID";
|
|
889
|
+
const t0 = Date.now();
|
|
890
|
+
const r = await withConn(cfg, undefined, (c) => c.run(sql, internal(cfg)));
|
|
891
|
+
return { ...r, rows: r.rows.map((row) => row.map(cell)), ms: Date.now() - t0 };
|
|
892
|
+
}
|
|
893
|
+
/** Cancels what a session is running (MySQL, PostgreSQL) or ends the session (SQL Server). */
|
|
894
|
+
export async function dbKill(cfg, id) {
|
|
895
|
+
if (cfg.engine === 'sqlite')
|
|
896
|
+
throw new ObjectError('SQLite has no sessions to cancel.');
|
|
897
|
+
const n = Number(id);
|
|
898
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
899
|
+
throw new ObjectError('The id must be a session number from object_db_sessions');
|
|
900
|
+
await withConn(cfg, undefined, async (c) => {
|
|
901
|
+
const o = { ...internal(cfg), readOnly: false };
|
|
902
|
+
if (cfg.engine === 'mysql')
|
|
903
|
+
await c.run(`KILL QUERY ${n}`, o);
|
|
904
|
+
else if (cfg.engine === 'postgresql') {
|
|
905
|
+
const r = await c.run(`SELECT pg_cancel_backend(${n})`, o);
|
|
906
|
+
if (r.rows[0]?.[0] === false)
|
|
907
|
+
throw new ObjectError(`There is no running query for session ${n}`);
|
|
908
|
+
}
|
|
909
|
+
else
|
|
910
|
+
await c.run(`KILL ${n}`, o);
|
|
911
|
+
});
|
|
912
|
+
return cfg.engine === 'mssql' ? `Session ${n} was ended.` : `The query of session ${n} was cancelled.`;
|
|
913
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { DATA_DIR, agents, resolved } from '../db.js';
|
|
4
|
-
import { OBJECTS_SKILL_ID } from '../skills/defaults.js';
|
|
4
|
+
import { DATABASES_SKILL_ID, OBJECTS_SKILL_ID } from '../skills/defaults.js';
|
|
5
5
|
import { bus } from '../runtime.js';
|
|
6
6
|
import { renderTemplate, listTemplates } from './templates.js';
|
|
7
7
|
import { composeServiceAction, inspectDocker, recreateDocker, dockerLogs, dockerState, removeDocker, restartDocker, startDocker, stopDocker } from './docker.js';
|
|
@@ -361,15 +361,19 @@ export const dbSavedUpdate = (id, savedId, p) => {
|
|
|
361
361
|
export const dbSavedRemove = (id, savedId) => { dbOf(id); if (!removeSaved(id, savedId))
|
|
362
362
|
throw new ObjectError('Saved query not found'); return listSaved(id); };
|
|
363
363
|
/** Only these can be looked after by an agent: it starts, stops and restarts them. */
|
|
364
|
-
const MANAGEABLE = ['server', 'docker', 'cluster'];
|
|
365
|
-
/**
|
|
366
|
-
|
|
364
|
+
const MANAGEABLE = ['server', 'docker', 'cluster', 'database'];
|
|
365
|
+
/**
|
|
366
|
+
* An agent without the skill that goes with a resource gets it when it is made its manager: without it the tools are not offered.
|
|
367
|
+
* A database goes with "Database management"; the other kinds with "Colony resources".
|
|
368
|
+
*/
|
|
369
|
+
function ensureSkill(agentId, kind) {
|
|
367
370
|
const ag = agents.get(agentId);
|
|
368
371
|
if (!ag)
|
|
369
372
|
throw new ObjectError('That agent no longer exists');
|
|
370
|
-
|
|
373
|
+
const skill = kind === 'database' ? DATABASES_SKILL_ID : OBJECTS_SKILL_ID;
|
|
374
|
+
if (resolved(ag).skill_ids.includes(skill))
|
|
371
375
|
return false;
|
|
372
|
-
agents.setSkills(agentId, [...ag.skill_ids,
|
|
376
|
+
agents.setSkills(agentId, [...ag.skill_ids, skill]);
|
|
373
377
|
return true;
|
|
374
378
|
}
|
|
375
379
|
/** Chooses the one agent that looks after a resource (`null` leaves it to the agents of its colony). */
|
|
@@ -378,7 +382,7 @@ export function setObjectManager(id, agentId) {
|
|
|
378
382
|
if (!MANAGEABLE.includes(o.kind))
|
|
379
383
|
throw new ObjectError('This kind of resource is not looked after by agents');
|
|
380
384
|
const a = agentId ? String(agentId) : null;
|
|
381
|
-
const skillAdded = a ? ensureSkill(a) : false;
|
|
385
|
+
const skillAdded = a ? ensureSkill(a, o.kind) : false;
|
|
382
386
|
objectsStore.setManager(id, a);
|
|
383
387
|
changed();
|
|
384
388
|
return { object: viewOf(o), skillAdded };
|
|
@@ -391,12 +395,16 @@ export function setAgentObjects(agentId, objectIds) {
|
|
|
391
395
|
throw new ObjectError('Choose the resources');
|
|
392
396
|
const want = new Set(objectIds.map(String));
|
|
393
397
|
const managers = objectsStore.managers();
|
|
398
|
+
const kinds = new Set();
|
|
394
399
|
for (const id of want) {
|
|
395
400
|
const o = get(id);
|
|
396
401
|
if (!MANAGEABLE.includes(o.kind))
|
|
397
402
|
throw new ObjectError('This kind of resource is not looked after by agents');
|
|
403
|
+
kinds.add(o.kind === 'database' ? 'database' : 'other');
|
|
398
404
|
}
|
|
399
|
-
|
|
405
|
+
let skillAdded = false;
|
|
406
|
+
for (const k of kinds)
|
|
407
|
+
skillAdded = ensureSkill(agentId, k === 'database' ? 'database' : 'server') || skillAdded;
|
|
400
408
|
for (const [oid, aid] of managers)
|
|
401
409
|
if (aid === agentId && !want.has(oid))
|
|
402
410
|
objectsStore.setManager(oid, null);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { learnEfforts } from '../efforts.js';
|
|
1
2
|
import { spawnJson } from './spawn.js';
|
|
2
3
|
import { withInstructions } from './preamble.js';
|
|
3
4
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -37,7 +38,8 @@ export async function* runKiro(o) {
|
|
|
37
38
|
if (o.effort)
|
|
38
39
|
args.push('--effort', o.effort);
|
|
39
40
|
const prompt = withInstructions(o.prompt, o.instructions, !!agent.session_id, o.refreshInstructions);
|
|
40
|
-
|
|
41
|
+
// After `--` so a message that begins with a dash is the prompt and never an option; nothing may be pushed after it.
|
|
42
|
+
args.push('--', prompt);
|
|
41
43
|
let sawSession = false;
|
|
42
44
|
const open = new Set();
|
|
43
45
|
for await (const ev of spawnJson({ cmd: 'kiro-cli', args, cwd: agent.cwd, signal: o.signal })) {
|
|
@@ -46,6 +48,10 @@ export async function* runKiro(o) {
|
|
|
46
48
|
sawSession = true;
|
|
47
49
|
yield { t: 'session', sessionId: d.sessionId };
|
|
48
50
|
}
|
|
51
|
+
// The turn's metadata says which reasoning levels the model takes: remembered per model, so the agent form offers only those.
|
|
52
|
+
if (ev.type === 'metadata' && agent.model && Array.isArray(d.reasoning?.effortLevels) && d.reasoning.effortLevels.length) {
|
|
53
|
+
learnEfforts('kiro', { [agent.model]: d.reasoning.effortLevels.filter((l) => typeof l === 'string').map((l) => ({ id: l })) });
|
|
54
|
+
}
|
|
49
55
|
if (ev.type === 'metadata' && (d.meteringUsage || d.contextUsagePercentage !== undefined)) {
|
|
50
56
|
const credits = (d.meteringUsage ?? []).reduce((n, m) => n + (m.value ?? 0), 0);
|
|
51
57
|
yield { t: 'usage', durationMs: d.turnDurationMs, usage: { credits: credits || undefined, contextPct: d.contextUsagePercentage } };
|