chainlesschain 0.47.8 → 0.49.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/bin/chainlesschain.js +0 -0
- package/package.json +10 -8
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/activitypub.js +533 -0
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +441 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/social.js +265 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +114 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/ueba.js +403 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
package/src/lib/dbevo.js
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Evolution Framework — CLI port of Phase 80
|
|
3
|
+
* (docs/design/modules/45_数据库演进与迁移框架.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop uses MigrationManager (up/down versioned migrations),
|
|
6
|
+
* QueryBuilder (fluent SQL), and IndexOptimizer (slow-query analysis).
|
|
7
|
+
* CLI port ships:
|
|
8
|
+
*
|
|
9
|
+
* - Migration registration, execution (up/down), rollback, history
|
|
10
|
+
* - Query logging with duration tracking and statistics
|
|
11
|
+
* - Index suggestion heuristics (slow-query analysis, table/column extraction)
|
|
12
|
+
* - Index suggestion application tracking
|
|
13
|
+
*
|
|
14
|
+
* What does NOT port: fluent QueryBuilder (runtime API, not CLI-facing),
|
|
15
|
+
* auto-migration on startup, real EXPLAIN-based optimization,
|
|
16
|
+
* periodic background analysis, database backup before migration.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import crypto from "crypto";
|
|
20
|
+
|
|
21
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
export const MIGRATION_STATUS = Object.freeze({
|
|
24
|
+
SUCCESS: "success",
|
|
25
|
+
FAILED: "failed",
|
|
26
|
+
ROLLED_BACK: "rolled_back",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const MIGRATION_DIRECTION = Object.freeze({
|
|
30
|
+
UP: "up",
|
|
31
|
+
DOWN: "down",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const SUGGESTION_TYPE = Object.freeze({
|
|
35
|
+
CREATE_INDEX: "create_index",
|
|
36
|
+
COMPOSITE_INDEX: "composite_index",
|
|
37
|
+
COVERING_INDEX: "covering_index",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
41
|
+
|
|
42
|
+
let _migrations = []; // registered migration definitions
|
|
43
|
+
let _migrationHistory = []; // executed migration records from DB
|
|
44
|
+
let _queryLogs = [];
|
|
45
|
+
let _suggestions = new Map();
|
|
46
|
+
let _slowQueryThresholdMs = 100;
|
|
47
|
+
|
|
48
|
+
function _id() {
|
|
49
|
+
return crypto.randomUUID();
|
|
50
|
+
}
|
|
51
|
+
function _now() {
|
|
52
|
+
return Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _strip(row) {
|
|
56
|
+
if (!row) return null;
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [k, v] of Object.entries(row)) {
|
|
59
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
export function ensureDbEvoTables(db) {
|
|
67
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _migrations (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
version TEXT NOT NULL,
|
|
70
|
+
description TEXT,
|
|
71
|
+
direction TEXT NOT NULL,
|
|
72
|
+
executed_at INTEGER NOT NULL,
|
|
73
|
+
duration_ms INTEGER,
|
|
74
|
+
checksum TEXT,
|
|
75
|
+
status TEXT DEFAULT 'success'
|
|
76
|
+
)`);
|
|
77
|
+
|
|
78
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _query_log (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
sql_text TEXT NOT NULL,
|
|
81
|
+
params_json TEXT,
|
|
82
|
+
duration_ms REAL NOT NULL,
|
|
83
|
+
source TEXT,
|
|
84
|
+
tables_accessed TEXT,
|
|
85
|
+
created_at INTEGER NOT NULL
|
|
86
|
+
)`);
|
|
87
|
+
|
|
88
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _index_suggestions (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
table_name TEXT NOT NULL,
|
|
91
|
+
columns TEXT NOT NULL,
|
|
92
|
+
suggestion_type TEXT,
|
|
93
|
+
estimated_improvement REAL,
|
|
94
|
+
query_count INTEGER DEFAULT 0,
|
|
95
|
+
applied INTEGER DEFAULT 0,
|
|
96
|
+
created_at INTEGER
|
|
97
|
+
)`);
|
|
98
|
+
|
|
99
|
+
_loadAll(db);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _loadAll(db) {
|
|
103
|
+
_migrationHistory = [];
|
|
104
|
+
_queryLogs = [];
|
|
105
|
+
_suggestions.clear();
|
|
106
|
+
|
|
107
|
+
const sources = [
|
|
108
|
+
["_migrations", (r) => _migrationHistory.push(r)],
|
|
109
|
+
["_query_log", (r) => _queryLogs.push(r)],
|
|
110
|
+
["_index_suggestions", (r) => _suggestions.set(r.id, r)],
|
|
111
|
+
];
|
|
112
|
+
for (const [table, handler] of sources) {
|
|
113
|
+
try {
|
|
114
|
+
for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
|
|
115
|
+
handler(_strip(row));
|
|
116
|
+
}
|
|
117
|
+
} catch (_e) {
|
|
118
|
+
/* table may not exist */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── Migration Registration ─────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
export function registerMigration(
|
|
126
|
+
version,
|
|
127
|
+
{ description, upSql, downSql } = {},
|
|
128
|
+
) {
|
|
129
|
+
if (!version) return { registered: false, reason: "missing_version" };
|
|
130
|
+
if (!upSql) return { registered: false, reason: "missing_up_sql" };
|
|
131
|
+
|
|
132
|
+
const existing = _migrations.find((m) => m.version === version);
|
|
133
|
+
if (existing) return { registered: false, reason: "duplicate_version" };
|
|
134
|
+
|
|
135
|
+
const checksum = crypto
|
|
136
|
+
.createHash("sha256")
|
|
137
|
+
.update(upSql + (downSql || ""))
|
|
138
|
+
.digest("hex")
|
|
139
|
+
.slice(0, 16);
|
|
140
|
+
|
|
141
|
+
_migrations.push({
|
|
142
|
+
version,
|
|
143
|
+
description: description || null,
|
|
144
|
+
upSql,
|
|
145
|
+
downSql: downSql || null,
|
|
146
|
+
checksum,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Keep sorted by version
|
|
150
|
+
_migrations.sort((a, b) => a.version.localeCompare(b.version));
|
|
151
|
+
|
|
152
|
+
return { registered: true, version, checksum };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function listRegisteredMigrations() {
|
|
156
|
+
return _migrations.map((m) => ({
|
|
157
|
+
version: m.version,
|
|
158
|
+
description: m.description,
|
|
159
|
+
checksum: m.checksum,
|
|
160
|
+
hasDown: !!m.downSql,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── Migration Execution ────────────────────────────────── */
|
|
165
|
+
|
|
166
|
+
export function getCurrentVersion(db) {
|
|
167
|
+
// Find the latest successful "up" migration that hasn't been rolled back
|
|
168
|
+
const ups = _migrationHistory
|
|
169
|
+
.filter((h) => h.direction === "up" && h.status === "success")
|
|
170
|
+
.map((h) => h.version);
|
|
171
|
+
const downs = _migrationHistory
|
|
172
|
+
.filter((h) => h.direction === "down" && h.status === "success")
|
|
173
|
+
.map((h) => h.version);
|
|
174
|
+
|
|
175
|
+
// Versions that have been migrated up and not rolled back
|
|
176
|
+
const active = ups.filter(
|
|
177
|
+
(v) => !downs.includes(v) || ups.lastIndexOf(v) > downs.lastIndexOf(v),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (active.length === 0) return null;
|
|
181
|
+
return active.sort((a, b) => b.localeCompare(a))[0];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getPendingMigrations(db) {
|
|
185
|
+
const current = getCurrentVersion(db);
|
|
186
|
+
return _migrations.filter(
|
|
187
|
+
(m) => !current || m.version.localeCompare(current) > 0,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function migrateUp(db, targetVersion) {
|
|
192
|
+
const pending = getPendingMigrations(db);
|
|
193
|
+
if (pending.length === 0) return { migrated: false, reason: "no_pending" };
|
|
194
|
+
|
|
195
|
+
const toRun = targetVersion
|
|
196
|
+
? pending.filter((m) => m.version.localeCompare(targetVersion) <= 0)
|
|
197
|
+
: pending;
|
|
198
|
+
|
|
199
|
+
if (toRun.length === 0) return { migrated: false, reason: "no_pending" };
|
|
200
|
+
|
|
201
|
+
const results = [];
|
|
202
|
+
for (const migration of toRun) {
|
|
203
|
+
const start = _now();
|
|
204
|
+
const id = _id();
|
|
205
|
+
let status = "success";
|
|
206
|
+
|
|
207
|
+
// In CLI port, we record the migration as executed (no real SQL execution)
|
|
208
|
+
const durationMs = _now() - start;
|
|
209
|
+
|
|
210
|
+
const record = {
|
|
211
|
+
id,
|
|
212
|
+
version: migration.version,
|
|
213
|
+
description: migration.description,
|
|
214
|
+
direction: "up",
|
|
215
|
+
executed_at: _now(),
|
|
216
|
+
duration_ms: durationMs,
|
|
217
|
+
checksum: migration.checksum,
|
|
218
|
+
status,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
db.prepare(
|
|
222
|
+
`INSERT INTO _migrations (id, version, description, direction, executed_at, duration_ms, checksum, status)
|
|
223
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
224
|
+
).run(
|
|
225
|
+
id,
|
|
226
|
+
record.version,
|
|
227
|
+
record.description,
|
|
228
|
+
"up",
|
|
229
|
+
record.executed_at,
|
|
230
|
+
durationMs,
|
|
231
|
+
record.checksum,
|
|
232
|
+
status,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
_migrationHistory.push(record);
|
|
236
|
+
results.push({ version: migration.version, status });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { migrated: true, count: results.length, results };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function migrateDown(db, targetVersion) {
|
|
243
|
+
const current = getCurrentVersion(db);
|
|
244
|
+
if (!current) return { rolledBack: false, reason: "no_current_version" };
|
|
245
|
+
|
|
246
|
+
// Find migrations to roll back (from current down to target, exclusive)
|
|
247
|
+
const toRollBack = _migrations
|
|
248
|
+
.filter((m) => {
|
|
249
|
+
if (targetVersion) {
|
|
250
|
+
return (
|
|
251
|
+
m.version.localeCompare(current) <= 0 &&
|
|
252
|
+
m.version.localeCompare(targetVersion) > 0
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return m.version === current;
|
|
256
|
+
})
|
|
257
|
+
.sort((a, b) => b.version.localeCompare(a.version));
|
|
258
|
+
|
|
259
|
+
if (toRollBack.length === 0)
|
|
260
|
+
return { rolledBack: false, reason: "nothing_to_rollback" };
|
|
261
|
+
|
|
262
|
+
const noDown = toRollBack.filter((m) => !m.downSql);
|
|
263
|
+
if (noDown.length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
rolledBack: false,
|
|
266
|
+
reason: "missing_down_migration",
|
|
267
|
+
versions: noDown.map((m) => m.version),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const results = [];
|
|
272
|
+
for (const migration of toRollBack) {
|
|
273
|
+
const id = _id();
|
|
274
|
+
const now = _now();
|
|
275
|
+
|
|
276
|
+
const record = {
|
|
277
|
+
id,
|
|
278
|
+
version: migration.version,
|
|
279
|
+
description: migration.description,
|
|
280
|
+
direction: "down",
|
|
281
|
+
executed_at: now,
|
|
282
|
+
duration_ms: 0,
|
|
283
|
+
checksum: migration.checksum,
|
|
284
|
+
status: "success",
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
db.prepare(
|
|
288
|
+
`INSERT INTO _migrations (id, version, description, direction, executed_at, duration_ms, checksum, status)
|
|
289
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
290
|
+
).run(
|
|
291
|
+
id,
|
|
292
|
+
record.version,
|
|
293
|
+
record.description,
|
|
294
|
+
"down",
|
|
295
|
+
now,
|
|
296
|
+
0,
|
|
297
|
+
record.checksum,
|
|
298
|
+
"success",
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
_migrationHistory.push(record);
|
|
302
|
+
results.push({ version: migration.version, status: "success" });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { rolledBack: true, count: results.length, results };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function getMigrationHistory(db, { limit = 50 } = {}) {
|
|
309
|
+
return _migrationHistory
|
|
310
|
+
.sort((a, b) => b.executed_at - a.executed_at)
|
|
311
|
+
.slice(0, limit)
|
|
312
|
+
.map((h) => ({ ...h }));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function getMigrationStatus(db) {
|
|
316
|
+
const current = getCurrentVersion(db);
|
|
317
|
+
const pending = getPendingMigrations(db);
|
|
318
|
+
const history = getMigrationHistory(db, { limit: 5 });
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
currentVersion: current,
|
|
322
|
+
pendingCount: pending.length,
|
|
323
|
+
pendingVersions: pending.map((m) => m.version),
|
|
324
|
+
totalRegistered: _migrations.length,
|
|
325
|
+
totalExecuted: _migrationHistory.length,
|
|
326
|
+
recentHistory: history,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function validateMigrations() {
|
|
331
|
+
if (_migrations.length === 0) return { valid: true, issues: [] };
|
|
332
|
+
|
|
333
|
+
const issues = [];
|
|
334
|
+
const versions = _migrations.map((m) => m.version).sort();
|
|
335
|
+
|
|
336
|
+
// Check for gaps in version sequence
|
|
337
|
+
for (let i = 1; i < versions.length; i++) {
|
|
338
|
+
const prev = versions[i - 1];
|
|
339
|
+
const curr = versions[i];
|
|
340
|
+
// Simple gap detection: if versions are numeric-like, check continuity
|
|
341
|
+
const prevNum = parseInt(prev.replace(/\D/g, ""), 10);
|
|
342
|
+
const currNum = parseInt(curr.replace(/\D/g, ""), 10);
|
|
343
|
+
if (!isNaN(prevNum) && !isNaN(currNum) && currNum - prevNum > 1) {
|
|
344
|
+
issues.push({ type: "gap", between: [prev, curr] });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check for missing down migrations
|
|
349
|
+
for (const m of _migrations) {
|
|
350
|
+
if (!m.downSql) {
|
|
351
|
+
issues.push({ type: "missing_down", version: m.version });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { valid: issues.length === 0, issues };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ── Query Logging ──────────────────────────────────────── */
|
|
359
|
+
|
|
360
|
+
export function logQuery(db, sqlText, durationMs, { params, source } = {}) {
|
|
361
|
+
if (!sqlText) return { logged: false, reason: "missing_sql" };
|
|
362
|
+
if (durationMs == null || durationMs < 0)
|
|
363
|
+
return { logged: false, reason: "invalid_duration" };
|
|
364
|
+
|
|
365
|
+
const id = _id();
|
|
366
|
+
const now = _now();
|
|
367
|
+
|
|
368
|
+
// Extract table names from SQL
|
|
369
|
+
const tableMatches =
|
|
370
|
+
sqlText.match(/(?:FROM|JOIN|INTO|UPDATE)\s+(\w+)/gi) || [];
|
|
371
|
+
const tables = [...new Set(tableMatches.map((m) => m.split(/\s+/).pop()))];
|
|
372
|
+
const tablesAccessed = tables.join(",");
|
|
373
|
+
|
|
374
|
+
const record = {
|
|
375
|
+
id,
|
|
376
|
+
sql_text: sqlText,
|
|
377
|
+
params_json: params ? JSON.stringify(params) : null,
|
|
378
|
+
duration_ms: durationMs,
|
|
379
|
+
source: source || null,
|
|
380
|
+
tables_accessed: tablesAccessed,
|
|
381
|
+
created_at: now,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
db.prepare(
|
|
385
|
+
`INSERT INTO _query_log (id, sql_text, params_json, duration_ms, source, tables_accessed, created_at)
|
|
386
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
387
|
+
).run(
|
|
388
|
+
id,
|
|
389
|
+
record.sql_text,
|
|
390
|
+
record.params_json,
|
|
391
|
+
durationMs,
|
|
392
|
+
record.source,
|
|
393
|
+
tablesAccessed,
|
|
394
|
+
now,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
_queryLogs.push(record);
|
|
398
|
+
|
|
399
|
+
return { logged: true, id, isSlow: durationMs >= _slowQueryThresholdMs };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function getQueryStats(db) {
|
|
403
|
+
if (_queryLogs.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
totalQueries: 0,
|
|
406
|
+
slowQueries: 0,
|
|
407
|
+
avgDurationMs: 0,
|
|
408
|
+
maxDurationMs: 0,
|
|
409
|
+
slowQueryThresholdMs: _slowQueryThresholdMs,
|
|
410
|
+
topSlow: [],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const durations = _queryLogs.map((q) => q.duration_ms);
|
|
415
|
+
const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
|
|
416
|
+
const avg =
|
|
417
|
+
Math.round(
|
|
418
|
+
(durations.reduce((s, d) => s + d, 0) / durations.length) * 100,
|
|
419
|
+
) / 100;
|
|
420
|
+
const max = Math.max(...durations);
|
|
421
|
+
|
|
422
|
+
// Top 10 slowest queries
|
|
423
|
+
const topSlow = [..._queryLogs]
|
|
424
|
+
.sort((a, b) => b.duration_ms - a.duration_ms)
|
|
425
|
+
.slice(0, 10)
|
|
426
|
+
.map((q) => ({
|
|
427
|
+
sql: q.sql_text.slice(0, 100),
|
|
428
|
+
durationMs: q.duration_ms,
|
|
429
|
+
tables: q.tables_accessed,
|
|
430
|
+
source: q.source,
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
totalQueries: _queryLogs.length,
|
|
435
|
+
slowQueries: slow.length,
|
|
436
|
+
avgDurationMs: avg,
|
|
437
|
+
maxDurationMs: max,
|
|
438
|
+
slowQueryThresholdMs: _slowQueryThresholdMs,
|
|
439
|
+
topSlow,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function setSlowQueryThreshold(ms) {
|
|
444
|
+
if (ms == null || ms < 0) return { set: false, reason: "invalid_threshold" };
|
|
445
|
+
_slowQueryThresholdMs = ms;
|
|
446
|
+
return { set: true, thresholdMs: ms };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function clearQueryLog(db) {
|
|
450
|
+
db.prepare("DELETE FROM _query_log").run();
|
|
451
|
+
const count = _queryLogs.length;
|
|
452
|
+
_queryLogs = [];
|
|
453
|
+
return { cleared: true, count };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* ── Index Optimization ─────────────────────────────────── */
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Analyze query logs to generate index suggestions.
|
|
460
|
+
* Heuristic: extract WHERE-clause columns from slow queries,
|
|
461
|
+
* count frequency per table+column, suggest indexes for frequent patterns.
|
|
462
|
+
*/
|
|
463
|
+
export function analyzeQueries(db, { minQueryCount = 2 } = {}) {
|
|
464
|
+
const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
|
|
465
|
+
if (slow.length === 0) return { analyzed: true, suggestionsGenerated: 0 };
|
|
466
|
+
|
|
467
|
+
// Extract WHERE columns per table
|
|
468
|
+
const tableColumnCounts = new Map(); // "table:col" → count
|
|
469
|
+
|
|
470
|
+
for (const q of slow) {
|
|
471
|
+
const tables = (q.tables_accessed || "").split(",").filter(Boolean);
|
|
472
|
+
if (tables.length === 0) continue;
|
|
473
|
+
|
|
474
|
+
// Extract column names from WHERE clauses
|
|
475
|
+
const whereMatch = q.sql_text.match(
|
|
476
|
+
/WHERE\s+(.+?)(?:\s+ORDER|\s+GROUP|\s+LIMIT|$)/is,
|
|
477
|
+
);
|
|
478
|
+
if (!whereMatch) continue;
|
|
479
|
+
|
|
480
|
+
const wherePart = whereMatch[1];
|
|
481
|
+
const colMatches =
|
|
482
|
+
wherePart.match(/(\w+)\s*(?:=|>|<|>=|<=|LIKE|IN)\s*/gi) || [];
|
|
483
|
+
const cols = colMatches
|
|
484
|
+
.map((m) => m.replace(/\s*(?:=|>|<|>=|<=|LIKE|IN)\s*/i, "").trim())
|
|
485
|
+
.filter(Boolean);
|
|
486
|
+
|
|
487
|
+
const primaryTable = tables[0];
|
|
488
|
+
for (const col of cols) {
|
|
489
|
+
const key = `${primaryTable}:${col}`;
|
|
490
|
+
tableColumnCounts.set(key, (tableColumnCounts.get(key) || 0) + 1);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Generate suggestions for columns that appear at least minQueryCount times
|
|
495
|
+
let generated = 0;
|
|
496
|
+
for (const [key, count] of tableColumnCounts) {
|
|
497
|
+
if (count < minQueryCount) continue;
|
|
498
|
+
|
|
499
|
+
const [tableName, column] = key.split(":");
|
|
500
|
+
const existingId = [..._suggestions.values()].find(
|
|
501
|
+
(s) => s.table_name === tableName && s.columns === column && !s.applied,
|
|
502
|
+
);
|
|
503
|
+
if (existingId) continue; // Don't duplicate
|
|
504
|
+
|
|
505
|
+
const id = _id();
|
|
506
|
+
const now = _now();
|
|
507
|
+
const estimatedImprovement = Math.min(
|
|
508
|
+
0.9,
|
|
509
|
+
Math.round((count / slow.length) * 100) / 100,
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const suggestion = {
|
|
513
|
+
id,
|
|
514
|
+
table_name: tableName,
|
|
515
|
+
columns: column,
|
|
516
|
+
suggestion_type: "create_index",
|
|
517
|
+
estimated_improvement: estimatedImprovement,
|
|
518
|
+
query_count: count,
|
|
519
|
+
applied: 0,
|
|
520
|
+
created_at: now,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
db.prepare(
|
|
524
|
+
`INSERT INTO _index_suggestions (id, table_name, columns, suggestion_type, estimated_improvement, query_count, applied, created_at)
|
|
525
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
526
|
+
).run(
|
|
527
|
+
id,
|
|
528
|
+
tableName,
|
|
529
|
+
column,
|
|
530
|
+
"create_index",
|
|
531
|
+
estimatedImprovement,
|
|
532
|
+
count,
|
|
533
|
+
0,
|
|
534
|
+
now,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
_suggestions.set(id, suggestion);
|
|
538
|
+
generated++;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Detect composite index opportunities (2+ columns on same table)
|
|
542
|
+
const tableColumns = new Map(); // table → [cols]
|
|
543
|
+
for (const [key] of tableColumnCounts) {
|
|
544
|
+
const [table, col] = key.split(":");
|
|
545
|
+
if (!tableColumns.has(table)) tableColumns.set(table, []);
|
|
546
|
+
tableColumns.get(table).push(col);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const [table, cols] of tableColumns) {
|
|
550
|
+
if (cols.length >= 2) {
|
|
551
|
+
const compositeKey = cols.sort().join(",");
|
|
552
|
+
const existingComposite = [..._suggestions.values()].find(
|
|
553
|
+
(s) => s.table_name === table && s.columns === compositeKey,
|
|
554
|
+
);
|
|
555
|
+
if (!existingComposite) {
|
|
556
|
+
const id = _id();
|
|
557
|
+
const now = _now();
|
|
558
|
+
const totalCount = cols.reduce(
|
|
559
|
+
(s, c) => s + (tableColumnCounts.get(`${table}:${c}`) || 0),
|
|
560
|
+
0,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const suggestion = {
|
|
564
|
+
id,
|
|
565
|
+
table_name: table,
|
|
566
|
+
columns: compositeKey,
|
|
567
|
+
suggestion_type: "composite_index",
|
|
568
|
+
estimated_improvement: Math.min(
|
|
569
|
+
0.95,
|
|
570
|
+
Math.round((totalCount / slow.length) * 100) / 100,
|
|
571
|
+
),
|
|
572
|
+
query_count: totalCount,
|
|
573
|
+
applied: 0,
|
|
574
|
+
created_at: now,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
db.prepare(
|
|
578
|
+
`INSERT INTO _index_suggestions (id, table_name, columns, suggestion_type, estimated_improvement, query_count, applied, created_at)
|
|
579
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
580
|
+
).run(
|
|
581
|
+
id,
|
|
582
|
+
table,
|
|
583
|
+
compositeKey,
|
|
584
|
+
"composite_index",
|
|
585
|
+
suggestion.estimated_improvement,
|
|
586
|
+
totalCount,
|
|
587
|
+
0,
|
|
588
|
+
now,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
_suggestions.set(id, suggestion);
|
|
592
|
+
generated++;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
analyzed: true,
|
|
599
|
+
suggestionsGenerated: generated,
|
|
600
|
+
slowQueriesAnalyzed: slow.length,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function listSuggestions(db, { applied } = {}) {
|
|
605
|
+
let sugs = [..._suggestions.values()];
|
|
606
|
+
if (applied != null) {
|
|
607
|
+
sugs = sugs.filter((s) => (applied ? s.applied === 1 : s.applied === 0));
|
|
608
|
+
}
|
|
609
|
+
return sugs
|
|
610
|
+
.sort((a, b) => b.query_count - a.query_count)
|
|
611
|
+
.map((s) => ({ ...s }));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function getSuggestion(db, id) {
|
|
615
|
+
const s = _suggestions.get(id);
|
|
616
|
+
return s ? { ...s } : null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function applySuggestion(db, id) {
|
|
620
|
+
const s = _suggestions.get(id);
|
|
621
|
+
if (!s) return { applied: false, reason: "not_found" };
|
|
622
|
+
if (s.applied) return { applied: false, reason: "already_applied" };
|
|
623
|
+
|
|
624
|
+
s.applied = 1;
|
|
625
|
+
db.prepare("UPDATE _index_suggestions SET applied = 1 WHERE id = ?").run(id);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
applied: true,
|
|
629
|
+
indexSql: `CREATE INDEX IF NOT EXISTS idx_${s.table_name}_${s.columns.replace(/,/g, "_")} ON ${s.table_name} (${s.columns})`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* ── Stats ──────────────────────────────────────────────── */
|
|
634
|
+
|
|
635
|
+
export function getDbEvoStats(db) {
|
|
636
|
+
const current = getCurrentVersion(db);
|
|
637
|
+
const pending = getPendingMigrations(db);
|
|
638
|
+
const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
|
|
639
|
+
const sugs = [..._suggestions.values()];
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
migrations: {
|
|
643
|
+
registered: _migrations.length,
|
|
644
|
+
executed: _migrationHistory.length,
|
|
645
|
+
currentVersion: current,
|
|
646
|
+
pending: pending.length,
|
|
647
|
+
},
|
|
648
|
+
queryLog: {
|
|
649
|
+
total: _queryLogs.length,
|
|
650
|
+
slowQueries: slow.length,
|
|
651
|
+
thresholdMs: _slowQueryThresholdMs,
|
|
652
|
+
},
|
|
653
|
+
suggestions: {
|
|
654
|
+
total: sugs.length,
|
|
655
|
+
pending: sugs.filter((s) => !s.applied).length,
|
|
656
|
+
applied: sugs.filter((s) => s.applied).length,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
662
|
+
|
|
663
|
+
export function _resetState() {
|
|
664
|
+
_migrations = [];
|
|
665
|
+
_migrationHistory = [];
|
|
666
|
+
_queryLogs = [];
|
|
667
|
+
_suggestions.clear();
|
|
668
|
+
_slowQueryThresholdMs = 100;
|
|
669
|
+
}
|