taurusdb-core 0.4.0 → 0.5.0-rc.1
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 +10 -0
- package/dist/auth/sql-profile-loader/parsing.js +10 -1
- 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 +7 -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 +14 -0
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.js +17 -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 +4 -3
- package/dist/engine.js +34 -1
- 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 +36 -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 +6 -3
- package/dist/index.js +4 -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 +24 -0
- 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 +6 -2
- package/dist/utils/formatter.js +7 -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
|
@@ -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
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 DiagnosticResult, type ServiceLatencyResult } from "./diagnostics/types.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>;
|
|
@@ -70,7 +71,7 @@ export declare class TaurusDBEngine {
|
|
|
70
71
|
restoreRecycleBinTable(input: RestoreRecycleBinTableInput, ctx: SessionContext, opts?: MutationOptions): Promise<MutationResult>;
|
|
71
72
|
getQueryStatus(queryId: string): Promise<QueryStatus>;
|
|
72
73
|
cancelQuery(queryId: string): Promise<CancelResult>;
|
|
73
|
-
issueConfirmation(input: IssueConfirmationInput): Promise<
|
|
74
|
+
issueConfirmation(input: IssueConfirmationInput): Promise<ConfirmationRequest>;
|
|
74
75
|
validateConfirmation(token: string, sql: string, ctx: SessionContext): Promise<ConfirmationValidationResult>;
|
|
75
76
|
handleConfirmation(decision: GuardrailDecision, ctx: SessionContext): Promise<ConfirmationOutcome>;
|
|
76
77
|
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
21
|
import { cancelQuery, close, executeMutation, 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,23 @@ 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
|
+
if (config.security.mutationsEnabled && !approvalSecret && !options.confirmationStore) {
|
|
118
|
+
throw new Error("Mutations require TAURUSDB_MUTATION_APPROVAL_SECRET_FILE with at least 32 bytes.");
|
|
119
|
+
}
|
|
120
|
+
const confirmationStore = options.confirmationStore ??
|
|
121
|
+
createConfirmationStore({
|
|
122
|
+
approvalSecret,
|
|
123
|
+
ttlSeconds: config.security.approvalTtlSeconds,
|
|
124
|
+
});
|
|
95
125
|
const capabilityProbe = options.capabilityProbe ??
|
|
96
126
|
createCapabilityProbe({
|
|
97
127
|
connectionPool,
|
|
@@ -155,6 +185,9 @@ export class TaurusDBEngine {
|
|
|
155
185
|
async showLockWaits(input, ctx) {
|
|
156
186
|
return showLockWaits(this, input, ctx);
|
|
157
187
|
}
|
|
188
|
+
async diagnoseReplicationLag(input, ctx) {
|
|
189
|
+
return diagnoseReplicationLag(this.executor, input, ctx);
|
|
190
|
+
}
|
|
158
191
|
async findMetadataLockWaits(input, ctx) {
|
|
159
192
|
return findMetadataLockWaits(this, input, ctx);
|
|
160
193
|
}
|
|
@@ -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>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
1
2
|
import { ulid } from "ulid";
|
|
2
3
|
export class ConnectionPoolError extends Error {
|
|
3
4
|
code = "CONNECTION_FAILED";
|
|
@@ -9,35 +10,42 @@ export class ConnectionPoolError extends Error {
|
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
function poolKey(datasource, mode) {
|
|
13
|
-
return
|
|
13
|
+
function poolKey(datasource, mode, database) {
|
|
14
|
+
return JSON.stringify([datasource, mode, database ?? null]);
|
|
14
15
|
}
|
|
15
|
-
async function resolveTls(tls, secretResolver) {
|
|
16
|
-
if (!tls) {
|
|
16
|
+
async function resolveTls(tls, secretResolver, host, requireTls) {
|
|
17
|
+
if (!tls && !requireTls) {
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
20
|
+
if (requireTls && tls?.enabled === false) {
|
|
21
|
+
throw new ConnectionPoolError("TLS is required by server policy and cannot be disabled for this datasource.");
|
|
22
|
+
}
|
|
23
|
+
if (requireTls && tls?.rejectUnauthorized === false) {
|
|
24
|
+
throw new ConnectionPoolError("TLS certificate verification is required by server policy.");
|
|
25
|
+
}
|
|
19
26
|
const resolved = {
|
|
20
|
-
enabled: tls
|
|
21
|
-
rejectUnauthorized: tls
|
|
22
|
-
servername: tls
|
|
27
|
+
enabled: tls?.enabled ?? true,
|
|
28
|
+
rejectUnauthorized: tls?.rejectUnauthorized ?? true,
|
|
29
|
+
servername: tls?.servername ?? (isIP(host) === 0 ? host : undefined),
|
|
23
30
|
};
|
|
24
|
-
if (tls
|
|
31
|
+
if (tls?.ca) {
|
|
25
32
|
resolved.ca = await secretResolver.resolve(tls.ca);
|
|
26
33
|
}
|
|
27
|
-
if (tls
|
|
34
|
+
if (tls?.cert) {
|
|
28
35
|
resolved.cert = await secretResolver.resolve(tls.cert);
|
|
29
36
|
}
|
|
30
|
-
if (tls
|
|
37
|
+
if (tls?.key) {
|
|
31
38
|
resolved.key = await secretResolver.resolve(tls.key);
|
|
32
39
|
}
|
|
33
40
|
return resolved;
|
|
34
41
|
}
|
|
35
|
-
function selectCredential(profile, mode
|
|
36
|
-
|
|
37
|
-
if (!
|
|
38
|
-
|
|
42
|
+
function selectCredential(profile, mode) {
|
|
43
|
+
const credential = mode === "rw" ? profile.mutationUser : profile.user;
|
|
44
|
+
if (!credential) {
|
|
45
|
+
const credentialKind = mode === "rw" ? "mutation" : "read-only";
|
|
46
|
+
throw new ConnectionPoolError(`Datasource "${profile.name}" does not define ${credentialKind} SQL credentials. Configure a dedicated ${credentialKind} database username and password reference.`);
|
|
39
47
|
}
|
|
40
|
-
return
|
|
48
|
+
return credential;
|
|
41
49
|
}
|
|
42
50
|
async function resolveCredentialValue(ref, secretResolver, context) {
|
|
43
51
|
try {
|
|
@@ -65,7 +73,8 @@ export class ConnectionPoolManager {
|
|
|
65
73
|
if (!profile) {
|
|
66
74
|
throw new ConnectionPoolError(`Datasource profile not found: "${datasource}".`);
|
|
67
75
|
}
|
|
68
|
-
const
|
|
76
|
+
const database = opts.database ?? profile.database;
|
|
77
|
+
const entry = await this.getOrCreatePool(profile, mode, database);
|
|
69
78
|
let driverSession;
|
|
70
79
|
try {
|
|
71
80
|
driverSession = await entry.pool.acquire();
|
|
@@ -79,6 +88,7 @@ export class ConnectionPoolManager {
|
|
|
79
88
|
entryKey: entry.key,
|
|
80
89
|
driverSession,
|
|
81
90
|
datasource,
|
|
91
|
+
database,
|
|
82
92
|
mode,
|
|
83
93
|
};
|
|
84
94
|
this.activeSessions.set(sessionId, active);
|
|
@@ -86,6 +96,7 @@ export class ConnectionPoolManager {
|
|
|
86
96
|
return {
|
|
87
97
|
id: sessionId,
|
|
88
98
|
datasource,
|
|
99
|
+
database,
|
|
89
100
|
mode,
|
|
90
101
|
async execute(sql, options) {
|
|
91
102
|
return active.driverSession.execute(sql, options);
|
|
@@ -151,13 +162,13 @@ export class ConnectionPoolManager {
|
|
|
151
162
|
};
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
|
-
async getOrCreatePool(profile, mode,
|
|
155
|
-
const key = poolKey(profile.name, mode);
|
|
165
|
+
async getOrCreatePool(profile, mode, database) {
|
|
166
|
+
const key = poolKey(profile.name, mode, database);
|
|
156
167
|
const existing = this.pools.get(key);
|
|
157
168
|
if (existing) {
|
|
158
169
|
return existing instanceof Promise ? existing : existing;
|
|
159
170
|
}
|
|
160
|
-
const pending = this.createPool(profile, mode,
|
|
171
|
+
const pending = this.createPool(profile, mode, database)
|
|
161
172
|
.then((entry) => {
|
|
162
173
|
this.pools.set(key, entry);
|
|
163
174
|
return entry;
|
|
@@ -169,7 +180,7 @@ export class ConnectionPoolManager {
|
|
|
169
180
|
this.pools.set(key, pending);
|
|
170
181
|
return pending;
|
|
171
182
|
}
|
|
172
|
-
async createPool(profile, mode,
|
|
183
|
+
async createPool(profile, mode, database) {
|
|
173
184
|
const adapter = this.adapters[profile.engine];
|
|
174
185
|
if (!adapter) {
|
|
175
186
|
throw new ConnectionPoolError(`No driver adapter registered for engine "${profile.engine}".`);
|
|
@@ -177,9 +188,9 @@ export class ConnectionPoolManager {
|
|
|
177
188
|
if (!profile.host) {
|
|
178
189
|
throw new ConnectionPoolError(`Datasource "${profile.name}" does not define a host. Select a cloud instance or configure host in the datasource profile.`);
|
|
179
190
|
}
|
|
180
|
-
const credential = selectCredential(profile, mode
|
|
191
|
+
const credential = selectCredential(profile, mode);
|
|
181
192
|
const password = await resolveCredentialValue(credential.password, this.secretResolver, `${profile.name}.${mode}.password`);
|
|
182
|
-
const tls = await resolveTls(profile.tls, this.secretResolver);
|
|
193
|
+
const tls = await resolveTls(profile.tls, this.secretResolver, profile.host, this.config.security.requireTls);
|
|
183
194
|
let pool;
|
|
184
195
|
try {
|
|
185
196
|
pool = await adapter.createPool({
|
|
@@ -188,7 +199,7 @@ export class ConnectionPoolManager {
|
|
|
188
199
|
engine: profile.engine,
|
|
189
200
|
host: profile.host,
|
|
190
201
|
port: profile.port,
|
|
191
|
-
database
|
|
202
|
+
database,
|
|
192
203
|
username: credential.username,
|
|
193
204
|
password,
|
|
194
205
|
poolSize: profile.poolSize,
|
|
@@ -199,8 +210,9 @@ export class ConnectionPoolManager {
|
|
|
199
210
|
throw new ConnectionPoolError(`Failed to create ${mode === "ro" ? "readonly" : "mutation"} pool for datasource "${profile.name}".`, error);
|
|
200
211
|
}
|
|
201
212
|
return {
|
|
202
|
-
key: poolKey(profile.name, mode),
|
|
213
|
+
key: poolKey(profile.name, mode, database),
|
|
203
214
|
datasource: profile.name,
|
|
215
|
+
database,
|
|
204
216
|
mode,
|
|
205
217
|
pool,
|
|
206
218
|
};
|