hive-am 1.3.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +20 -5
  3. package/bin/hive-am.mjs +242 -31
  4. package/bin/run.mjs +135 -0
  5. package/package.json +1 -1
  6. package/server/dist/connections/manager.js +15 -3
  7. package/server/dist/connections/router.js +2 -2
  8. package/server/dist/connections/secrets.js +8 -0
  9. package/server/dist/db/migrations.js +68 -0
  10. package/server/dist/db.js +11 -10
  11. package/server/dist/efforts.js +105 -0
  12. package/server/dist/http/routes/agents.js +4 -3
  13. package/server/dist/http/routes/meta.js +12 -0
  14. package/server/dist/http/routes/notebook.js +10 -2
  15. package/server/dist/http/routes/resources.js +3 -0
  16. package/server/dist/http/routes/types.js +18 -1
  17. package/server/dist/http/validate.js +5 -2
  18. package/server/dist/index.js +4 -1
  19. package/server/dist/instructions.js +10 -2
  20. package/server/dist/objects/agent-db.js +258 -0
  21. package/server/dist/objects/database.js +496 -34
  22. package/server/dist/objects/index.js +20 -14
  23. package/server/dist/objects/runner.js +20 -0
  24. package/server/dist/objects/supervisor.js +2 -2
  25. package/server/dist/providers/kiro.js +7 -1
  26. package/server/dist/providers/opencode-server.js +11 -1
  27. package/server/dist/providers/opencode.js +4 -3
  28. package/server/dist/providers/spawn.js +38 -4
  29. package/server/dist/retention.js +50 -0
  30. package/server/dist/runtime.js +52 -21
  31. package/server/dist/services/agents.js +53 -1
  32. package/server/dist/services/notebook.js +7 -3
  33. package/server/dist/skills/defaults.js +35 -1
  34. package/server/dist/skills/notebook.js +57 -13
  35. package/server/mcp/dispatch.mjs +68 -0
  36. package/web/.next/BUILD_ID +1 -1
  37. package/web/.next/app-build-manifest.json +79 -70
  38. package/web/.next/app-path-routes-manifest.json +9 -8
  39. package/web/.next/build-manifest.json +2 -2
  40. package/web/.next/prerender-manifest.json +71 -47
  41. package/web/.next/routes-manifest.json +6 -0
  42. package/web/.next/server/app/_not-found/page.js +1 -1
  43. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  44. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  45. package/web/.next/server/app/_not-found.html +1 -1
  46. package/web/.next/server/app/_not-found.rsc +3 -3
  47. package/web/.next/server/app/agents/[id]/page.js +4 -4
  48. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  49. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  50. package/web/.next/server/app/agents/page.js +2 -2
  51. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  52. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  53. package/web/.next/server/app/agents.html +1 -1
  54. package/web/.next/server/app/agents.rsc +4 -4
  55. package/web/.next/server/app/chat/page.js +2 -0
  56. package/web/.next/server/app/chat/page.js.nft.json +1 -0
  57. package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -0
  58. package/web/.next/server/app/chat.html +1 -0
  59. package/web/.next/server/app/chat.meta +15 -0
  60. package/web/.next/server/app/chat.rsc +25 -0
  61. package/web/.next/server/app/connections/page.js +1 -1
  62. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  63. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  64. package/web/.next/server/app/connections.html +1 -1
  65. package/web/.next/server/app/connections.rsc +4 -4
  66. package/web/.next/server/app/index.html +1 -1
  67. package/web/.next/server/app/index.rsc +4 -4
  68. package/web/.next/server/app/page.js +1 -1
  69. package/web/.next/server/app/page.js.nft.json +1 -1
  70. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  71. package/web/.next/server/app/relations/page.js +2 -2
  72. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  73. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  74. package/web/.next/server/app/relations.html +1 -1
  75. package/web/.next/server/app/relations.rsc +4 -4
  76. package/web/.next/server/app/resources/[id]/page.js +2 -2
  77. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
  78. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
  79. package/web/.next/server/app/resources/page.js +2 -2
  80. package/web/.next/server/app/resources/page.js.nft.json +1 -1
  81. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
  82. package/web/.next/server/app/resources.html +1 -1
  83. package/web/.next/server/app/resources.rsc +4 -4
  84. package/web/.next/server/app/schedules/page.js +1 -1
  85. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  86. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  87. package/web/.next/server/app/schedules.html +1 -1
  88. package/web/.next/server/app/schedules.rsc +4 -4
  89. package/web/.next/server/app/sessions/page.js +2 -2
  90. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  91. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  92. package/web/.next/server/app/sessions.html +1 -1
  93. package/web/.next/server/app/sessions.rsc +4 -4
  94. package/web/.next/server/app/skills/page.js +1 -1
  95. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  96. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  97. package/web/.next/server/app/skills.html +1 -1
  98. package/web/.next/server/app/skills.rsc +4 -4
  99. package/web/.next/server/app/types/page.js +1 -1
  100. package/web/.next/server/app/types/page.js.nft.json +1 -1
  101. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  102. package/web/.next/server/app/types.html +1 -1
  103. package/web/.next/server/app/types.rsc +4 -4
  104. package/web/.next/server/app-paths-manifest.json +9 -8
  105. package/web/.next/server/chunks/143.js +1 -1
  106. package/web/.next/server/chunks/148.js +1 -0
  107. package/web/.next/server/chunks/213.js +6 -0
  108. package/web/.next/server/chunks/274.js +1 -1
  109. package/web/.next/server/chunks/368.js +1 -0
  110. package/web/.next/server/chunks/403.js +1 -0
  111. package/web/.next/server/chunks/{790.js → 435.js} +2 -2
  112. package/web/.next/server/chunks/505.js +1 -6
  113. package/web/.next/server/chunks/612.js +1 -1
  114. package/web/.next/server/pages/404.html +1 -1
  115. package/web/.next/server/pages/500.html +1 -1
  116. package/web/.next/server/server-reference-manifest.json +1 -1
  117. package/web/.next/static/UDA414PFPu9ZROltNmjbX/_buildManifest.js +1 -0
  118. package/web/.next/static/chunks/{316-26da9cc233ba7557.js → 107-77dd87cc2ca4f941.js} +1 -1
  119. package/web/.next/static/chunks/281-30e32d932b8b5a75.js +1 -0
  120. package/web/.next/static/chunks/391-18768499e741112e.js +1 -0
  121. package/web/.next/static/chunks/517-b3b1c9044d2ef23c.js +1 -0
  122. package/web/.next/static/chunks/587-5b511f5f93910180.js +1 -0
  123. package/web/.next/static/chunks/606-14fb5aaa4d0ff22d.js +1 -0
  124. package/web/.next/static/chunks/656-f3cc78328603241a.js +1 -0
  125. package/web/.next/static/chunks/{705-780e48e55b0346bd.js → 705-ce86175dea00e6a9.js} +1 -1
  126. package/web/.next/static/chunks/757-03361e7a97a605a2.js +1 -0
  127. package/web/.next/static/chunks/app/agents/[id]/page-638e12ba3cb813c3.js +1 -0
  128. package/web/.next/static/chunks/app/agents/page-9da499db6d22731f.js +1 -0
  129. package/web/.next/static/chunks/app/chat/page-383bc24e72dab7fa.js +1 -0
  130. package/web/.next/static/chunks/app/connections/{page-eb11cfb1ffeab3ca.js → page-06bd2d7d9495126a.js} +1 -1
  131. package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +1 -0
  132. package/web/.next/static/chunks/app/page-0062ebc30322b1ae.js +1 -0
  133. package/web/.next/static/chunks/app/relations/page-02710fe1f8a670a9.js +1 -0
  134. package/web/.next/static/chunks/app/resources/[id]/{page-bf05836f97248ca7.js → page-fc494e85b01e8cb7.js} +1 -1
  135. package/web/.next/static/chunks/app/resources/{page-800f774e5cacf4c9.js → page-61cccd8fdcc2f6c8.js} +1 -1
  136. package/web/.next/static/chunks/app/sessions/page-85638705fdc18174.js +1 -0
  137. package/web/.next/static/chunks/app/types/page-8e25f503bdcd72ad.js +1 -0
  138. package/web/.next/static/css/dfcaa19a8d46e60d.css +1 -0
  139. package/web/.next/trace +30 -29
  140. package/web/.next/types/app/chat/page.ts +84 -0
  141. package/web/.next/types/routes.d.ts +2 -1
  142. package/web/.next/types/validator.ts +9 -0
  143. package/web/.next/server/chunks/306.js +0 -1
  144. package/web/.next/server/chunks/451.js +0 -1
  145. package/web/.next/server/chunks/638.js +0 -1
  146. package/web/.next/server/chunks/802.js +0 -1
  147. package/web/.next/static/aSYzbHk1M5YeS_d3SQKTZ/_buildManifest.js +0 -1
  148. package/web/.next/static/chunks/281-d8a0dcfaa6d064e4.js +0 -1
  149. package/web/.next/static/chunks/291-23f314877fa39054.js +0 -1
  150. package/web/.next/static/chunks/304-681348099f874a30.js +0 -1
  151. package/web/.next/static/chunks/491-28f093772bd460ac.js +0 -1
  152. package/web/.next/static/chunks/517-373a80c8ba432bdc.js +0 -1
  153. package/web/.next/static/chunks/562-7c2e33d838e869bf.js +0 -1
  154. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +0 -1
  155. package/web/.next/static/chunks/844-519177e6198ca84f.js +0 -1
  156. package/web/.next/static/chunks/app/agents/[id]/page-c005161cec5527d8.js +0 -1
  157. package/web/.next/static/chunks/app/agents/page-9bb8cba69776c719.js +0 -1
  158. package/web/.next/static/chunks/app/layout-49b46c9b5aa512a8.js +0 -1
  159. package/web/.next/static/chunks/app/page-da042d146caeeab6.js +0 -1
  160. package/web/.next/static/chunks/app/relations/page-a44fd515a3b0525e.js +0 -1
  161. package/web/.next/static/chunks/app/sessions/page-a7534ebd90e982d6.js +0 -1
  162. package/web/.next/static/chunks/app/types/page-ebd37e220f77c1f3.js +0 -1
  163. package/web/.next/static/css/d42799b7e6a0e8a2.css +0 -1
  164. /package/web/.next/static/{aSYzbHk1M5YeS_d3SQKTZ → 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 stmt = db.prepare(w.sql);
273
- if (stmt.reader) {
274
- const columns = stmt.columns().map((c) => ({ name: c.name })); stmt.raw(true);
275
- const rows = []; let truncated = false;
276
- 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))); }
277
- out = { ok: true, columns, rows, truncated };
278
- } else { const r = stmt.run(); out = { ok: true, columns: [], rows: [], truncated: false, affected: r.changes }; }
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) => new Promise((ok, no) => {
293
- const child = spawn(process.execPath, ['-e', SQLITE_CHILD], { stdio: ['pipe', 'pipe', 'ignore'] });
294
- const chunks = [];
295
- let size = 0, done = false;
296
- const finish = (err, v) => { if (done)
297
- return; done = true; clearTimeout(timer); child.kill('SIGKILL'); err ? no(err) : ok(v); };
298
- const timer = setTimeout(() => finish(new Error(`The query took more than ${Math.round(o.timeoutMs / 1000)} s`)), o.timeoutMs);
299
- child.stdout.on('data', (c) => { size += c.length; if (size > SQLITE_MAX_OUT)
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 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ${lit(schema)} AND TABLE_NAME = ${lit(table)} ORDER BY ORDINAL_POSITION`))
427
- .map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', default: r[3] === null ? null : String(r[3]) }));
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
+ }