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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -1
  3. package/dist/audit/jsonl-writer.d.ts +47 -0
  4. package/dist/audit/jsonl-writer.js +124 -0
  5. package/dist/auth/sql-profile-loader/env-source.js +10 -0
  6. package/dist/auth/sql-profile-loader/parsing.js +10 -1
  7. package/dist/auth/sql-profile-loader/runtime-override.d.ts +1 -0
  8. package/dist/auth/sql-profile-loader/runtime-override.js +19 -1
  9. package/dist/auth/sql-profile-loader/types.d.ts +7 -0
  10. package/dist/capability/probe.js +3 -1
  11. package/dist/cloud/auth.d.ts +1 -0
  12. package/dist/cloud/auth.js +37 -0
  13. package/dist/config/env.js +14 -0
  14. package/dist/config/schema.d.ts +68 -0
  15. package/dist/config/schema.js +17 -0
  16. package/dist/context/datasource-resolver.js +7 -0
  17. package/dist/context/session-context.d.ts +7 -0
  18. package/dist/diagnostics/replication-lag.d.ts +4 -0
  19. package/dist/diagnostics/replication-lag.js +138 -0
  20. package/dist/diagnostics/types.d.ts +5 -1
  21. package/dist/diagnostics/types.js +7 -7
  22. package/dist/engine/runtime.d.ts +2 -2
  23. package/dist/engine/runtime.js +8 -14
  24. package/dist/engine/types.d.ts +3 -2
  25. package/dist/engine.d.ts +4 -3
  26. package/dist/engine.js +34 -1
  27. package/dist/executor/bounded-sql.d.ts +1 -0
  28. package/dist/executor/bounded-sql.js +10 -0
  29. package/dist/executor/concurrency-limiter.d.ts +15 -0
  30. package/dist/executor/concurrency-limiter.js +67 -0
  31. package/dist/executor/connection-pool.d.ts +3 -2
  32. package/dist/executor/connection-pool.js +36 -24
  33. package/dist/executor/sql-executor.d.ts +3 -0
  34. package/dist/executor/sql-executor.js +52 -9
  35. package/dist/executor/types.d.ts +5 -1
  36. package/dist/index.d.ts +6 -3
  37. package/dist/index.js +4 -1
  38. package/dist/safety/confirmation-store.d.ts +30 -7
  39. package/dist/safety/confirmation-store.js +154 -30
  40. package/dist/safety/guardrail.d.ts +3 -0
  41. package/dist/safety/guardrail.js +16 -6
  42. package/dist/safety/redaction.d.ts +6 -0
  43. package/dist/safety/redaction.js +47 -6
  44. package/dist/safety/sql-classifier.d.ts +1 -0
  45. package/dist/safety/sql-classifier.js +1 -0
  46. package/dist/safety/sql-validator.d.ts +1 -0
  47. package/dist/safety/sql-validator.js +24 -0
  48. package/dist/schema/adapters/mysql.js +3 -1
  49. package/dist/taurus/flashback.js +4 -10
  50. package/dist/taurus/recycle-bin.js +2 -8
  51. package/dist/utils/formatter.d.ts +6 -2
  52. package/dist/utils/formatter.js +7 -3
  53. package/dist/utils/logger.js +8 -0
  54. package/dist/utils/mysql-identifier.d.ts +1 -0
  55. package/dist/utils/mysql-identifier.js +9 -0
  56. 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}_pending`,
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
- "The diagnostic contract is available, but evidence collectors have not been wired yet.",
22
- "No live control-plane or data-plane evidence was collected in this run.",
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: "diagnostics_scaffold",
28
- title: "Diagnostic scaffold active",
29
- summary: "Typed inputs, outputs, and engine entrypoints are in place, but collectors and analyzers are pending.",
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
- "This diagnostic currently returns a scaffolded result instead of live evidence-backed analysis.",
36
+ "The configured datasource, permissions, time window, or cloud metric sources did not provide sufficient evidence.",
37
37
  ],
38
38
  };
39
39
  }
@@ -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 { ConfirmationToken, ConfirmationValidationResult } from "../safety/confirmation-store.js";
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<ConfirmationToken>;
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>;
@@ -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 ${quoteIdentifier("updated_at")} FROM ${quoteIdentifier(database)}.${quoteIdentifier(input.table)} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
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 token = await engine.confirmationStore.issue({
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: "token_issued",
378
- token: token.token,
379
- issuedAt: token.issuedAt,
380
- expiresAt: token.expiresAt,
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) {
@@ -31,8 +31,9 @@ export type IssueConfirmationInput = {
31
31
  export type ConfirmationOutcome = {
32
32
  status: "confirmed";
33
33
  } | {
34
- status: "token_issued";
35
- token: string;
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 ConfirmationToken, type ConfirmationValidationResult } from "./safety/confirmation-store.js";
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<ConfirmationToken>;
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
- const confirmationStore = options.confirmationStore ?? createConfirmationStore();
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
- allowReadonlyFallbackForMutations?: boolean;
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
- allowReadonlyFallbackForMutations?: boolean;
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 `${datasource}:${mode}`;
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.enabled,
21
- rejectUnauthorized: tls.rejectUnauthorized,
22
- servername: tls.servername,
27
+ enabled: tls?.enabled ?? true,
28
+ rejectUnauthorized: tls?.rejectUnauthorized ?? true,
29
+ servername: tls?.servername ?? (isIP(host) === 0 ? host : undefined),
23
30
  };
24
- if (tls.ca) {
31
+ if (tls?.ca) {
25
32
  resolved.ca = await secretResolver.resolve(tls.ca);
26
33
  }
27
- if (tls.cert) {
34
+ if (tls?.cert) {
28
35
  resolved.cert = await secretResolver.resolve(tls.cert);
29
36
  }
30
- if (tls.key) {
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, _opts = {}) {
36
- void mode;
37
- if (!profile.user) {
38
- throw new ConnectionPoolError(`Datasource "${profile.name}" does not define SQL credentials. Configure a database username and password reference before executing SQL.`);
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 profile.user;
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 entry = await this.getOrCreatePool(profile, mode, opts);
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, opts = {}) {
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, opts)
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, opts = {}) {
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, opts);
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: profile.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
  };