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.
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 +6 -10
  6. package/dist/auth/sql-profile-loader/parsing.js +8 -5
  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 +5 -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 +16 -0
  14. package/dist/config/schema.d.ts +78 -0
  15. package/dist/config/schema.js +19 -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 +5 -5
  26. package/dist/engine.js +32 -5
  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 +35 -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 +8 -3
  37. package/dist/index.js +5 -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 +25 -1
  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 +11 -2
  52. package/dist/utils/formatter.js +12 -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
@@ -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}_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
- 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";
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<ConfirmationToken>;
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, executeMutation, executeReadonly, explain, explainEnhanced, flashbackQuery, getQueryStatus, handleConfirmation, issueConfirmation, listRecycleBin, restoreRecycleBinTable, validateConfirmation, } from "./engine/runtime.js";
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
- const confirmationStore = options.confirmationStore ?? createConfirmationStore();
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
- 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>;