taurusdb-core 0.4.0 → 0.5.0-rc.10
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/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/audit/jsonl-writer.d.ts +47 -0
- package/dist/audit/jsonl-writer.js +124 -0
- package/dist/auth/sql-profile-loader/env-source.js +6 -10
- package/dist/auth/sql-profile-loader/parsing.js +8 -5
- package/dist/auth/sql-profile-loader/runtime-override.d.ts +1 -0
- package/dist/auth/sql-profile-loader/runtime-override.js +19 -1
- package/dist/auth/sql-profile-loader/types.d.ts +5 -0
- package/dist/capability/probe.js +3 -1
- package/dist/cloud/auth.d.ts +1 -0
- package/dist/cloud/auth.js +37 -0
- package/dist/config/env.js +16 -0
- package/dist/config/schema.d.ts +78 -0
- package/dist/config/schema.js +19 -0
- package/dist/context/datasource-resolver.js +7 -0
- package/dist/context/session-context.d.ts +7 -0
- package/dist/diagnostics/replication-lag.d.ts +4 -0
- package/dist/diagnostics/replication-lag.js +138 -0
- package/dist/diagnostics/types.d.ts +5 -1
- package/dist/diagnostics/types.js +7 -7
- package/dist/engine/runtime.d.ts +2 -2
- package/dist/engine/runtime.js +8 -14
- package/dist/engine/types.d.ts +3 -2
- package/dist/engine.d.ts +5 -5
- package/dist/engine.js +32 -5
- package/dist/executor/bounded-sql.d.ts +1 -0
- package/dist/executor/bounded-sql.js +10 -0
- package/dist/executor/concurrency-limiter.d.ts +15 -0
- package/dist/executor/concurrency-limiter.js +67 -0
- package/dist/executor/connection-pool.d.ts +3 -2
- package/dist/executor/connection-pool.js +35 -24
- package/dist/executor/sql-executor.d.ts +3 -0
- package/dist/executor/sql-executor.js +52 -9
- package/dist/executor/types.d.ts +5 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/dist/safety/confirmation-store.d.ts +30 -7
- package/dist/safety/confirmation-store.js +154 -30
- package/dist/safety/guardrail.d.ts +3 -0
- package/dist/safety/guardrail.js +16 -6
- package/dist/safety/redaction.d.ts +6 -0
- package/dist/safety/redaction.js +47 -6
- package/dist/safety/sql-classifier.d.ts +1 -0
- package/dist/safety/sql-classifier.js +1 -0
- package/dist/safety/sql-validator.d.ts +1 -0
- package/dist/safety/sql-validator.js +25 -1
- package/dist/schema/adapters/mysql.js +3 -1
- package/dist/taurus/flashback.js +4 -10
- package/dist/taurus/recycle-bin.js +2 -8
- package/dist/utils/formatter.d.ts +11 -2
- package/dist/utils/formatter.js +12 -3
- package/dist/utils/logger.js +8 -0
- package/dist/utils/mysql-identifier.d.ts +1 -0
- package/dist/utils/mysql-identifier.js +9 -0
- package/package.json +13 -2
|
@@ -5,6 +5,8 @@ export interface RuntimeLimits {
|
|
|
5
5
|
maxRows: number;
|
|
6
6
|
maxColumns: number;
|
|
7
7
|
maxFieldChars: number;
|
|
8
|
+
maxResultBytes?: number;
|
|
9
|
+
maxBlobBytes?: number;
|
|
8
10
|
}
|
|
9
11
|
export interface SessionContext {
|
|
10
12
|
task_id: string;
|
|
@@ -12,6 +14,11 @@ export interface SessionContext {
|
|
|
12
14
|
engine: DatabaseEngine;
|
|
13
15
|
database?: string;
|
|
14
16
|
schema?: string;
|
|
17
|
+
host?: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
projectId?: string;
|
|
20
|
+
instanceId?: string;
|
|
21
|
+
nodeId?: string;
|
|
15
22
|
limits: RuntimeLimits;
|
|
16
23
|
}
|
|
17
24
|
export interface DatasourceResolveInput {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SessionContext } from "../context/session-context.js";
|
|
2
|
+
import type { SqlExecutor } from "../executor/sql-executor.js";
|
|
3
|
+
import type { DiagnoseReplicationLagInput, DiagnosticResult } from "./types.js";
|
|
4
|
+
export declare function diagnoseReplicationLag(executor: SqlExecutor, input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
function rowsAsObjects(result) {
|
|
2
|
+
return result.rows.map((row) => Object.fromEntries(result.columns.map((column, index) => [column.name, row[index]])));
|
|
3
|
+
}
|
|
4
|
+
function text(row, ...names) {
|
|
5
|
+
for (const name of names) {
|
|
6
|
+
const value = row[name];
|
|
7
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
8
|
+
return String(value).trim();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function number(row, ...names) {
|
|
14
|
+
const value = text(row, ...names);
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
20
|
+
}
|
|
21
|
+
async function readReplicationStatus(executor, ctx) {
|
|
22
|
+
try {
|
|
23
|
+
return rowsAsObjects(await executor.executeReadonly("SHOW REPLICA STATUS", ctx, {
|
|
24
|
+
maxRows: 20,
|
|
25
|
+
maxColumns: 80,
|
|
26
|
+
maxFieldChars: 512,
|
|
27
|
+
maxResultBytes: 262144,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return rowsAsObjects(await executor.executeReadonly("SHOW SLAVE STATUS", ctx, {
|
|
32
|
+
maxRows: 20,
|
|
33
|
+
maxColumns: 80,
|
|
34
|
+
maxFieldChars: 512,
|
|
35
|
+
maxResultBytes: 262144,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function diagnoseReplicationLag(executor, input, ctx) {
|
|
40
|
+
let rows;
|
|
41
|
+
try {
|
|
42
|
+
rows = await readReplicationStatus(executor, ctx);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
rows = [];
|
|
46
|
+
}
|
|
47
|
+
const filtered = rows.filter((row) => {
|
|
48
|
+
const channel = text(row, "Channel_Name");
|
|
49
|
+
const replicaId = text(row, "Server_Id", "Source_Server_Id", "Master_Server_Id");
|
|
50
|
+
return ((!input.channel || channel === input.channel) &&
|
|
51
|
+
(!input.replicaId || replicaId === input.replicaId));
|
|
52
|
+
});
|
|
53
|
+
const window = {
|
|
54
|
+
from: input.timeRange?.from,
|
|
55
|
+
to: input.timeRange?.to,
|
|
56
|
+
relative: input.timeRange?.relative,
|
|
57
|
+
};
|
|
58
|
+
if (filtered.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
tool: "diagnose_replication_lag",
|
|
61
|
+
status: "not_applicable",
|
|
62
|
+
severity: "info",
|
|
63
|
+
summary: "No replication channel was visible on the selected database endpoint.",
|
|
64
|
+
diagnosisWindow: window,
|
|
65
|
+
rootCauseCandidates: [],
|
|
66
|
+
keyFindings: [
|
|
67
|
+
"SHOW REPLICA STATUS and SHOW SLAVE STATUS returned no matching channel.",
|
|
68
|
+
],
|
|
69
|
+
evidence: [],
|
|
70
|
+
recommendedActions: [
|
|
71
|
+
"Confirm that the selected endpoint is a read replica and that the read-only SQL user can inspect replication status.",
|
|
72
|
+
],
|
|
73
|
+
limitations: [
|
|
74
|
+
"A primary or standalone endpoint legitimately has no replica status.",
|
|
75
|
+
"Cloud time-series replication metrics are not included in this data-plane-only result.",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const findings = filtered.map((row) => {
|
|
80
|
+
const lag = number(row, "Seconds_Behind_Source", "Seconds_Behind_Master");
|
|
81
|
+
const io = text(row, "Replica_IO_Running", "Slave_IO_Running") ?? "unknown";
|
|
82
|
+
const sql = text(row, "Replica_SQL_Running", "Slave_SQL_Running") ?? "unknown";
|
|
83
|
+
const channel = text(row, "Channel_Name") ?? "default";
|
|
84
|
+
return { lag, io, sql, channel };
|
|
85
|
+
});
|
|
86
|
+
const maxLag = Math.max(...findings.map((item) => item.lag ?? 0));
|
|
87
|
+
const stopped = findings.some((item) => item.io.toLowerCase() === "no" || item.sql.toLowerCase() === "no");
|
|
88
|
+
const severity = stopped || maxLag >= 300 ? "high" : maxLag >= 30 ? "warning" : "info";
|
|
89
|
+
const status = stopped || maxLag >= 30 ? "inconclusive" : "ok";
|
|
90
|
+
return {
|
|
91
|
+
tool: "diagnose_replication_lag",
|
|
92
|
+
status,
|
|
93
|
+
severity,
|
|
94
|
+
summary: stopped
|
|
95
|
+
? "At least one replication worker is not running."
|
|
96
|
+
: `Observed maximum replication delay of ${maxLag} seconds.`,
|
|
97
|
+
diagnosisWindow: window,
|
|
98
|
+
rootCauseCandidates: [
|
|
99
|
+
...(stopped
|
|
100
|
+
? [{
|
|
101
|
+
code: "replication_worker_stopped",
|
|
102
|
+
title: "Replication worker stopped",
|
|
103
|
+
confidence: "high",
|
|
104
|
+
rationale: "Replica IO or SQL worker reports No.",
|
|
105
|
+
}]
|
|
106
|
+
: []),
|
|
107
|
+
...(maxLag >= 30
|
|
108
|
+
? [{
|
|
109
|
+
code: "replication_delay",
|
|
110
|
+
title: "Replica apply delay",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
rationale: `Seconds behind source reached ${maxLag}.`,
|
|
113
|
+
}]
|
|
114
|
+
: []),
|
|
115
|
+
],
|
|
116
|
+
keyFindings: findings.map((item) => `Channel ${item.channel}: lag=${item.lag ?? "unknown"}s, io=${item.io}, sql=${item.sql}.`),
|
|
117
|
+
evidence: [{
|
|
118
|
+
source: "replication_status",
|
|
119
|
+
title: "Current replication status",
|
|
120
|
+
summary: `Collected ${findings.length} matching replication channel(s).`,
|
|
121
|
+
}],
|
|
122
|
+
recommendedActions: stopped
|
|
123
|
+
? [
|
|
124
|
+
"Inspect Last_IO_Error and Last_SQL_Error directly with a privileged DBA account.",
|
|
125
|
+
"Do not skip replication errors until the failed event and data consistency impact are understood.",
|
|
126
|
+
]
|
|
127
|
+
: maxLag >= 30
|
|
128
|
+
? [
|
|
129
|
+
"Check long transactions, write bursts, DDL, and replica resource saturation.",
|
|
130
|
+
"Compare lag over time in Cloud Eye before changing replica topology.",
|
|
131
|
+
]
|
|
132
|
+
: ["Continue monitoring the replication delay trend."],
|
|
133
|
+
limitations: [
|
|
134
|
+
"This result is a point-in-time data-plane snapshot.",
|
|
135
|
+
"Error text and relay-log SQL are intentionally not returned to the MCP client.",
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_storage_pressure";
|
|
1
|
+
export type DiagnosticToolName = "diagnose_service_latency" | "diagnose_db_hotspot" | "find_top_slow_sql" | "diagnose_slow_query" | "diagnose_connection_spike" | "diagnose_lock_contention" | "diagnose_replication_lag" | "diagnose_storage_pressure";
|
|
2
2
|
export type DiagnosticStatus = "ok" | "inconclusive" | "not_applicable";
|
|
3
3
|
export type DiagnosticSeverity = "info" | "warning" | "high" | "critical";
|
|
4
4
|
export type DiagnosticEvidenceLevel = "basic" | "standard" | "full";
|
|
@@ -46,6 +46,10 @@ export interface DiagnoseStoragePressureInput extends DiagnosticBaseInput {
|
|
|
46
46
|
scope?: "instance" | "database" | "table";
|
|
47
47
|
table?: string;
|
|
48
48
|
}
|
|
49
|
+
export interface DiagnoseReplicationLagInput extends DiagnosticBaseInput {
|
|
50
|
+
replicaId?: string;
|
|
51
|
+
channel?: string;
|
|
52
|
+
}
|
|
49
53
|
export interface DiagnosticRootCauseCandidate {
|
|
50
54
|
code: string;
|
|
51
55
|
title: string;
|
|
@@ -11,29 +11,29 @@ export function createPlaceholderDiagnosticResult(tool, input, options) {
|
|
|
11
11
|
},
|
|
12
12
|
rootCauseCandidates: [
|
|
13
13
|
{
|
|
14
|
-
code: `${tool}
|
|
14
|
+
code: `${tool}_insufficient_evidence`,
|
|
15
15
|
title: options.candidateTitle,
|
|
16
16
|
confidence: "low",
|
|
17
17
|
rationale: options.candidateRationale,
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
keyFindings: options.keyFindings ?? [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"No conclusive live control-plane or data-plane evidence matched this diagnosis.",
|
|
22
|
+
"The result is intentionally inconclusive rather than inferring a root cause without evidence.",
|
|
23
23
|
],
|
|
24
24
|
suspiciousEntities: options.suspiciousEntities,
|
|
25
25
|
evidence: options.evidence ?? [
|
|
26
26
|
{
|
|
27
|
-
source: "
|
|
28
|
-
title: "
|
|
29
|
-
summary: "
|
|
27
|
+
source: "diagnostic_limitations",
|
|
28
|
+
title: "No conclusive evidence",
|
|
29
|
+
summary: "The configured collectors returned no evidence sufficient for a root-cause conclusion.",
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
32
|
recommendedActions: options.recommendedActions ?? [
|
|
33
33
|
"Implement the required collectors before relying on this tool for production diagnosis.",
|
|
34
34
|
],
|
|
35
35
|
limitations: options.limitations ?? [
|
|
36
|
-
"
|
|
36
|
+
"The configured datasource, permissions, time window, or cloud metric sources did not provide sufficient evidence.",
|
|
37
37
|
],
|
|
38
38
|
};
|
|
39
39
|
}
|
package/dist/engine/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionContext } from "../context/session-context.js";
|
|
2
2
|
import type { CancelResult, ExplainResult, MutationOptions, MutationResult, QueryResult, QueryStatus, ReadonlyOptions } from "../executor/sql-executor.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ConfirmationRequest, ConfirmationValidationResult } from "../safety/confirmation-store.js";
|
|
4
4
|
import type { GuardrailDecision } from "../safety/guardrail.js";
|
|
5
5
|
import { type FlashbackInput } from "../taurus/flashback.js";
|
|
6
6
|
import { type RestoreRecycleBinTableInput } from "../taurus/recycle-bin.js";
|
|
@@ -14,7 +14,7 @@ export declare function listRecycleBin(engine: TaurusDBEngine, ctx: SessionConte
|
|
|
14
14
|
export declare function restoreRecycleBinTable(engine: TaurusDBEngine, input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
15
15
|
export declare function getQueryStatus(engine: TaurusDBEngine, queryId: string): Promise<QueryStatus>;
|
|
16
16
|
export declare function cancelQuery(engine: TaurusDBEngine, queryId: string): Promise<CancelResult>;
|
|
17
|
-
export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<
|
|
17
|
+
export declare function issueConfirmation(engine: TaurusDBEngine, input: IssueConfirmationInput): Promise<ConfirmationRequest>;
|
|
18
18
|
export declare function validateConfirmation(engine: TaurusDBEngine, token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
|
|
19
19
|
export declare function handleConfirmation(engine: TaurusDBEngine, decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
|
|
20
20
|
export declare function close(engine: TaurusDBEngine): Promise<void>;
|
package/dist/engine/runtime.js
CHANGED
|
@@ -3,6 +3,7 @@ import { InMemoryConfirmationStore } from "../safety/confirmation-store.js";
|
|
|
3
3
|
import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, normalizeFlashbackWhereClause, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
|
|
4
4
|
import { buildListRecycleBinSql, buildRestoreRecycleBinTableSql, recycleBinMutationOptions, recycleBinReadonlyOptions } from "../taurus/recycle-bin.js";
|
|
5
5
|
import { normalizeSql, sqlHash } from "../utils/hash.js";
|
|
6
|
+
import { quoteMysqlIdentifier } from "../utils/mysql-identifier.js";
|
|
6
7
|
function resolveConfirmationSql(input) {
|
|
7
8
|
const normalized = input.normalizedSql ?? (input.sql ? normalizeSql(input.sql) : undefined);
|
|
8
9
|
const hash = input.sqlHash ?? (normalized ? sqlHash(normalized) : undefined);
|
|
@@ -26,13 +27,6 @@ function hasSqlPattern(sql, pattern) {
|
|
|
26
27
|
return pattern.test(sql);
|
|
27
28
|
}
|
|
28
29
|
const NO_FLASHBACK_VIEW_PATTERN = /No view available for provided TIMESTAMP/i;
|
|
29
|
-
const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
30
|
-
function quoteIdentifier(identifier) {
|
|
31
|
-
if (!SIMPLE_IDENTIFIER_PATTERN.test(identifier)) {
|
|
32
|
-
throw new Error(`Invalid identifier: "${identifier}".`);
|
|
33
|
-
}
|
|
34
|
-
return `\`${identifier}\``;
|
|
35
|
-
}
|
|
36
30
|
function firstRowAsObject(result) {
|
|
37
31
|
if (result.rows.length === 0) {
|
|
38
32
|
return undefined;
|
|
@@ -91,7 +85,7 @@ async function buildFlashbackNoViewError(engine, ctx, input, database, requested
|
|
|
91
85
|
if (input.where?.trim()) {
|
|
92
86
|
try {
|
|
93
87
|
const where = normalizeFlashbackWhereClause(input.where);
|
|
94
|
-
const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${
|
|
88
|
+
const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteMysqlIdentifier("updated_at")} FROM ${quoteMysqlIdentifier(database, "database")}.${quoteMysqlIdentifier(input.table, "table")} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
|
|
95
89
|
const updatedAtRow = firstRowAsObject(updatedAtResult);
|
|
96
90
|
if (typeof updatedAtRow?.updated_at === "string") {
|
|
97
91
|
details.current_row_updated_at = updatedAtRow.updated_at;
|
|
@@ -341,7 +335,6 @@ export async function restoreRecycleBinTable(engine, input, ctx, opts) {
|
|
|
341
335
|
}
|
|
342
336
|
return engine.executor.executeMutation(buildRestoreRecycleBinTableSql(input), ctx, {
|
|
343
337
|
...recycleBinMutationOptions(opts),
|
|
344
|
-
allowReadonlyFallbackForMutations: true,
|
|
345
338
|
});
|
|
346
339
|
}
|
|
347
340
|
export async function getQueryStatus(engine, queryId) {
|
|
@@ -367,17 +360,18 @@ export async function handleConfirmation(engine, decision, ctx) {
|
|
|
367
360
|
if (!decision.requiresConfirmation) {
|
|
368
361
|
return { status: "confirmed" };
|
|
369
362
|
}
|
|
370
|
-
const
|
|
363
|
+
const approval = await engine.confirmationStore.issue({
|
|
371
364
|
sqlHash: decision.sqlHash,
|
|
372
365
|
normalizedSql: decision.normalizedSql,
|
|
373
366
|
context: ctx,
|
|
374
367
|
riskLevel: decision.riskLevel,
|
|
375
368
|
});
|
|
376
369
|
return {
|
|
377
|
-
status: "
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
370
|
+
status: "approval_required",
|
|
371
|
+
request: approval.request,
|
|
372
|
+
requestId: approval.requestId,
|
|
373
|
+
issuedAt: approval.issuedAt,
|
|
374
|
+
expiresAt: approval.expiresAt,
|
|
381
375
|
};
|
|
382
376
|
}
|
|
383
377
|
export async function close(engine) {
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -31,8 +31,9 @@ export type IssueConfirmationInput = {
|
|
|
31
31
|
export type ConfirmationOutcome = {
|
|
32
32
|
status: "confirmed";
|
|
33
33
|
} | {
|
|
34
|
-
status: "
|
|
35
|
-
|
|
34
|
+
status: "approval_required";
|
|
35
|
+
request: string;
|
|
36
|
+
requestId: string;
|
|
36
37
|
issuedAt: number;
|
|
37
38
|
expiresAt: number;
|
|
38
39
|
};
|
package/dist/engine.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { type Config } from "./config/index.js";
|
|
|
6
6
|
import type { DatasourceResolveInput, DatasourceResolver, SessionContext } from "./context/session-context.js";
|
|
7
7
|
import { type ConnectionPool } from "./executor/connection-pool.js";
|
|
8
8
|
import { type CancelResult, type ExplainResult, type MutationOptions, type MutationResult, type QueryResult, type QueryStatus, type ReadonlyOptions, type SqlExecutor } from "./executor/sql-executor.js";
|
|
9
|
-
import { type ConfirmationStore, type
|
|
9
|
+
import { type ConfirmationStore, type ConfirmationRequest, type ConfirmationValidationResult } from "./safety/confirmation-store.js";
|
|
10
10
|
import { type Guardrail, type GuardrailDecision, type InspectInput } from "./safety/guardrail.js";
|
|
11
11
|
import { type DatabaseInfo, type SchemaIntrospector, type TableInfo, type TableSchema } from "./schema/introspector.js";
|
|
12
12
|
import { type FlashbackInput } from "./taurus/flashback.js";
|
|
13
|
-
import {
|
|
14
|
-
import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
|
|
13
|
+
import type { RestoreRecycleBinTableInput } from "./taurus/recycle-bin.js";
|
|
14
|
+
import { type DbHotspotResult, type DiagnoseDbHotspotInput, type DiagnoseServiceLatencyInput, type FindTopSlowSqlInput, type FindTopSlowSqlResult, type DiagnoseConnectionSpikeInput, type DiagnoseLockContentionInput, type DiagnoseSlowQueryInput, type DiagnoseStoragePressureInput, type DiagnoseReplicationLagInput, type DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.js";
|
|
15
15
|
import { type SlowSqlSource } from "./diagnostics/slow-sql-source.js";
|
|
16
16
|
import { type MetricsSource } from "./diagnostics/metrics-source.js";
|
|
17
17
|
import type { ConfirmationOutcome, DataSourceInfo, EnhancedExplainResult, IssueConfirmationInput, ShowLockWaitsInput, ShowProcesslistInput, TaurusDBEngineCreateOptions, TaurusDBEngineDeps } from "./engine/types.js";
|
|
@@ -44,6 +44,7 @@ export declare class TaurusDBEngine {
|
|
|
44
44
|
listFeatures(ctx: SessionContext): Promise<FeatureMatrix>;
|
|
45
45
|
showProcesslist(input: ShowProcesslistInput, ctx: SessionContext): Promise<QueryResult>;
|
|
46
46
|
showLockWaits(input: ShowLockWaitsInput, ctx: SessionContext): Promise<QueryResult>;
|
|
47
|
+
diagnoseReplicationLag(input: DiagnoseReplicationLagInput, ctx: SessionContext): Promise<DiagnosticResult>;
|
|
47
48
|
findMetadataLockWaits(input: DiagnoseLockContentionInput, ctx: SessionContext): Promise<MetadataLockRow[]>;
|
|
48
49
|
findLatestDeadlock(ctx: SessionContext): Promise<DeadlockSummary | undefined>;
|
|
49
50
|
findStatementDigestSample(digestText: string, ctx: SessionContext): Promise<StatementDigestRow | undefined>;
|
|
@@ -64,13 +65,12 @@ export declare class TaurusDBEngine {
|
|
|
64
65
|
explain(sql: string, ctx: SessionContext): Promise<ExplainResult>;
|
|
65
66
|
explainEnhanced(sql: string, ctx: SessionContext): Promise<EnhancedExplainResult>;
|
|
66
67
|
executeReadonly(sql: string, ctx: SessionContext, opts?: ReadonlyOptions): Promise<QueryResult>;
|
|
67
|
-
executeMutation(sql: string, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
68
68
|
flashbackQuery(input: FlashbackInput, ctx: SessionContext, opts?: ReadonlyOptions): Promise<QueryResult>;
|
|
69
69
|
listRecycleBin(ctx: SessionContext, opts?: ReadonlyOptions): Promise<QueryResult>;
|
|
70
70
|
restoreRecycleBinTable(input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
71
71
|
getQueryStatus(queryId: string): Promise<QueryStatus>;
|
|
72
72
|
cancelQuery(queryId: string): Promise<CancelResult>;
|
|
73
|
-
issueConfirmation(input: IssueConfirmationInput): Promise<
|
|
73
|
+
issueConfirmation(input: IssueConfirmationInput): Promise<ConfirmationRequest>;
|
|
74
74
|
validateConfirmation(token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
|
|
75
75
|
handleConfirmation(decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
|
|
76
76
|
close(): Promise<void>;
|
package/dist/engine.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
1
2
|
import { createSqlProfileLoader, } from "./auth/sql-profile-loader.js";
|
|
2
3
|
import { createSecretResolver, } from "./auth/secret-resolver.js";
|
|
3
4
|
import { createHuaweiKmsSecretResolver } from "./cloud/kms.js";
|
|
@@ -12,11 +13,26 @@ import { createConfirmationStore, } from "./safety/confirmation-store.js";
|
|
|
12
13
|
import { createGuardrail, } from "./safety/guardrail.js";
|
|
13
14
|
import { createSchemaIntrospector, } from "./schema/introspector.js";
|
|
14
15
|
import { createMySqlSchemaAdapter } from "./schema/adapters/mysql.js";
|
|
16
|
+
import { diagnoseReplicationLag } from "./diagnostics/replication-lag.js";
|
|
15
17
|
import { createSlowSqlSource, } from "./diagnostics/slow-sql-source.js";
|
|
16
18
|
import { createMetricsSource, } from "./diagnostics/metrics-source.js";
|
|
17
19
|
import { findLatestDeadlock, findMetadataLockWaits, findStatementDigestCandidatesForSqlHints, findStatementDigestSample, findStatementDigestSampleForSql, findStatementWaitEvents, findStorageStatementDigests, findTableStorageStats, findTopStatementDigests, isPerformanceSchemaEnabled, showLockWaits, showProcesslist, } from "./engine/data-access.js";
|
|
18
20
|
import { diagnoseConnectionSpike, diagnoseDbHotspot, diagnoseLockContention, diagnoseServiceLatency, diagnoseSlowQuery, diagnoseStoragePressure, findTopSlowSql, } from "./engine/diagnostics.js";
|
|
19
|
-
import { cancelQuery, close,
|
|
21
|
+
import { cancelQuery, close, executeReadonly, explain, explainEnhanced, flashbackQuery, getQueryStatus, handleConfirmation, issueConfirmation, listRecycleBin, restoreRecycleBinTable, validateConfirmation, } from "./engine/runtime.js";
|
|
22
|
+
async function readMutationApprovalSecret(filePath) {
|
|
23
|
+
const info = await lstat(filePath);
|
|
24
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
25
|
+
throw new Error("Mutation approval secret must be a regular, non-symlink file.");
|
|
26
|
+
}
|
|
27
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
28
|
+
throw new Error("Mutation approval secret file must not be accessible by group or other users.");
|
|
29
|
+
}
|
|
30
|
+
const secret = (await readFile(filePath, "utf8")).trim();
|
|
31
|
+
if (Buffer.byteLength(secret, "utf8") < 32) {
|
|
32
|
+
throw new Error("Mutation approval secret must contain at least 32 bytes.");
|
|
33
|
+
}
|
|
34
|
+
return secret;
|
|
35
|
+
}
|
|
20
36
|
function toDataSourceInfo(profile, defaultDatasource) {
|
|
21
37
|
return {
|
|
22
38
|
name: profile.name,
|
|
@@ -89,9 +105,20 @@ export class TaurusDBEngine {
|
|
|
89
105
|
const executor = options.executor ??
|
|
90
106
|
createSqlExecutor({
|
|
91
107
|
connectionPool,
|
|
108
|
+
maxConcurrentQueries: config.limits.maxConcurrentQueries,
|
|
109
|
+
maxQueuedQueries: config.limits.maxQueuedQueries,
|
|
110
|
+
queueTimeoutMs: config.limits.queueTimeoutMs,
|
|
92
111
|
});
|
|
93
112
|
const guardrail = options.guardrail ?? createGuardrail();
|
|
94
|
-
|
|
113
|
+
let approvalSecret;
|
|
114
|
+
if (config.security.approvalSecretPath) {
|
|
115
|
+
approvalSecret = await readMutationApprovalSecret(config.security.approvalSecretPath);
|
|
116
|
+
}
|
|
117
|
+
const confirmationStore = options.confirmationStore ??
|
|
118
|
+
createConfirmationStore({
|
|
119
|
+
approvalSecret,
|
|
120
|
+
ttlSeconds: config.security.approvalTtlSeconds,
|
|
121
|
+
});
|
|
95
122
|
const capabilityProbe = options.capabilityProbe ??
|
|
96
123
|
createCapabilityProbe({
|
|
97
124
|
connectionPool,
|
|
@@ -155,6 +182,9 @@ export class TaurusDBEngine {
|
|
|
155
182
|
async showLockWaits(input, ctx) {
|
|
156
183
|
return showLockWaits(this, input, ctx);
|
|
157
184
|
}
|
|
185
|
+
async diagnoseReplicationLag(input, ctx) {
|
|
186
|
+
return diagnoseReplicationLag(this.executor, input, ctx);
|
|
187
|
+
}
|
|
158
188
|
async findMetadataLockWaits(input, ctx) {
|
|
159
189
|
return findMetadataLockWaits(this, input, ctx);
|
|
160
190
|
}
|
|
@@ -215,9 +245,6 @@ export class TaurusDBEngine {
|
|
|
215
245
|
async executeReadonly(sql, ctx, opts) {
|
|
216
246
|
return executeReadonly(this, sql, ctx, opts);
|
|
217
247
|
}
|
|
218
|
-
async executeMutation(sql, ctx, opts) {
|
|
219
|
-
return executeMutation(this, sql, ctx, opts);
|
|
220
|
-
}
|
|
221
248
|
async flashbackQuery(input, ctx, opts) {
|
|
222
249
|
return flashbackQuery(this, input, ctx, opts);
|
|
223
250
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildServerBoundedReadonlySql(sql: string, maxRows: number): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function buildServerBoundedReadonlySql(sql, maxRows) {
|
|
2
|
+
const normalized = sql.trim().replace(/;\s*$/, "");
|
|
3
|
+
if (!/^(?:select|with)\b/i.test(normalized)) {
|
|
4
|
+
return normalized;
|
|
5
|
+
}
|
|
6
|
+
if (!Number.isInteger(maxRows) || maxRows <= 0) {
|
|
7
|
+
throw new Error("maxRows must be a positive integer.");
|
|
8
|
+
}
|
|
9
|
+
return `SELECT * FROM (${normalized}) AS __taurus_mcp_bounded LIMIT ${maxRows + 1}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class QueryConcurrencyError extends Error {
|
|
2
|
+
readonly code = "SERVER_BUSY";
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class QueryConcurrencyLimiter {
|
|
6
|
+
private readonly maxConcurrent;
|
|
7
|
+
private readonly maxQueued;
|
|
8
|
+
private readonly queueTimeoutMs;
|
|
9
|
+
private active;
|
|
10
|
+
private readonly queue;
|
|
11
|
+
constructor(maxConcurrent: number, maxQueued: number, queueTimeoutMs: number);
|
|
12
|
+
run<T>(operation: () => Promise<T>): Promise<T>;
|
|
13
|
+
private acquire;
|
|
14
|
+
private releaseFactory;
|
|
15
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class QueryConcurrencyError extends Error {
|
|
2
|
+
code = "SERVER_BUSY";
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "QueryConcurrencyError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class QueryConcurrencyLimiter {
|
|
9
|
+
maxConcurrent;
|
|
10
|
+
maxQueued;
|
|
11
|
+
queueTimeoutMs;
|
|
12
|
+
active = 0;
|
|
13
|
+
queue = [];
|
|
14
|
+
constructor(maxConcurrent, maxQueued, queueTimeoutMs) {
|
|
15
|
+
this.maxConcurrent = maxConcurrent;
|
|
16
|
+
this.maxQueued = maxQueued;
|
|
17
|
+
this.queueTimeoutMs = queueTimeoutMs;
|
|
18
|
+
}
|
|
19
|
+
async run(operation) {
|
|
20
|
+
const release = await this.acquire();
|
|
21
|
+
try {
|
|
22
|
+
return await operation();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
release();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
acquire() {
|
|
29
|
+
if (this.active < this.maxConcurrent) {
|
|
30
|
+
this.active += 1;
|
|
31
|
+
return Promise.resolve(this.releaseFactory());
|
|
32
|
+
}
|
|
33
|
+
if (this.queue.length >= this.maxQueued) {
|
|
34
|
+
return Promise.reject(new QueryConcurrencyError("Query concurrency queue is full."));
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const waiter = {
|
|
38
|
+
resolve,
|
|
39
|
+
reject,
|
|
40
|
+
timer: setTimeout(() => {
|
|
41
|
+
const index = this.queue.indexOf(waiter);
|
|
42
|
+
if (index >= 0) {
|
|
43
|
+
this.queue.splice(index, 1);
|
|
44
|
+
}
|
|
45
|
+
reject(new QueryConcurrencyError("Timed out waiting for query capacity."));
|
|
46
|
+
}, this.queueTimeoutMs),
|
|
47
|
+
};
|
|
48
|
+
this.queue.push(waiter);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
releaseFactory() {
|
|
52
|
+
let released = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (released) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
released = true;
|
|
58
|
+
const waiter = this.queue.shift();
|
|
59
|
+
if (waiter) {
|
|
60
|
+
clearTimeout(waiter.timer);
|
|
61
|
+
waiter.resolve(this.releaseFactory());
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.active -= 1;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -18,6 +18,7 @@ export interface RawResult {
|
|
|
18
18
|
export interface Session {
|
|
19
19
|
id: string;
|
|
20
20
|
datasource: string;
|
|
21
|
+
database?: string;
|
|
21
22
|
mode: PoolMode;
|
|
22
23
|
execute(sql: string, options?: ExecOptions): Promise<RawResult>;
|
|
23
24
|
cancel(): Promise<void>;
|
|
@@ -35,7 +36,7 @@ export interface PoolHealth {
|
|
|
35
36
|
}
|
|
36
37
|
export interface ConnectionPool {
|
|
37
38
|
acquire(datasource: string, mode: PoolMode, opts?: {
|
|
38
|
-
|
|
39
|
+
database?: string;
|
|
39
40
|
}): Promise<Session>;
|
|
40
41
|
release(session: Session): Promise<void>;
|
|
41
42
|
healthCheck(datasource: string): Promise<PoolHealth>;
|
|
@@ -91,7 +92,7 @@ export declare class ConnectionPoolManager implements ConnectionPool {
|
|
|
91
92
|
private readonly activeSessions;
|
|
92
93
|
constructor(options: ConnectionPoolManagerOptions);
|
|
93
94
|
acquire(datasource: string, mode: PoolMode, opts?: {
|
|
94
|
-
|
|
95
|
+
database?: string;
|
|
95
96
|
}): Promise<Session>;
|
|
96
97
|
release(session: Session): Promise<void>;
|
|
97
98
|
healthCheck(datasource: string): Promise<PoolHealth>;
|