#
tokens: 48365/50000 13/77 files (page 2/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 8. Use http://codebase.md/bytebase/dbhub?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .env.example
├── .github
│   ├── CODEOWNERS
│   ├── copilot-instructions.md
│   └── workflows
│       ├── docker-publish.yml
│       ├── npm-publish.yml
│       └── run-tests.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── .npmrc
├── .prettierrc.json
├── bun.lock
├── CLAUDE.md
├── Dockerfile
├── LICENSE
├── llms-full.txt
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── resources
│   ├── employee-sqlite
│   │   ├── employee.sql
│   │   ├── load_department.sql
│   │   ├── load_dept_emp.sql
│   │   ├── load_dept_manager.sql
│   │   ├── load_employee.sql
│   │   ├── load_salary1.sql
│   │   ├── load_title.sql
│   │   ├── object.sql
│   │   ├── show_elapsed.sql
│   │   └── test_employee_md5.sql
│   └── images
│       ├── claude-desktop.webp
│       ├── cursor.webp
│       ├── logo-full.svg
│       ├── logo-full.webp
│       ├── logo-icon-only.svg
│       ├── logo-text-only.svg
│       └── mcp-inspector.webp
├── scripts
│   └── setup-husky.sh
├── src
│   ├── __tests__
│   │   └── json-rpc-integration.test.ts
│   ├── config
│   │   ├── __tests__
│   │   │   ├── env.test.ts
│   │   │   └── ssh-config-integration.test.ts
│   │   ├── demo-loader.ts
│   │   └── env.ts
│   ├── connectors
│   │   ├── __tests__
│   │   │   ├── mariadb.integration.test.ts
│   │   │   ├── mysql.integration.test.ts
│   │   │   ├── postgres-ssh.integration.test.ts
│   │   │   ├── postgres.integration.test.ts
│   │   │   ├── shared
│   │   │   │   └── integration-test-base.ts
│   │   │   ├── sqlite.integration.test.ts
│   │   │   └── sqlserver.integration.test.ts
│   │   ├── interface.ts
│   │   ├── manager.ts
│   │   ├── mariadb
│   │   │   └── index.ts
│   │   ├── mysql
│   │   │   └── index.ts
│   │   ├── postgres
│   │   │   └── index.ts
│   │   ├── sqlite
│   │   │   └── index.ts
│   │   └── sqlserver
│   │       └── index.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── db-explainer.ts
│   │   ├── index.ts
│   │   └── sql-generator.ts
│   ├── resources
│   │   ├── index.ts
│   │   ├── indexes.ts
│   │   ├── procedures.ts
│   │   ├── schema.ts
│   │   ├── schemas.ts
│   │   └── tables.ts
│   ├── server.ts
│   ├── tools
│   │   ├── __tests__
│   │   │   └── execute-sql.test.ts
│   │   ├── execute-sql.ts
│   │   └── index.ts
│   ├── types
│   │   ├── sql.ts
│   │   └── ssh.ts
│   └── utils
│       ├── __tests__
│       │   ├── safe-url.test.ts
│       │   ├── ssh-config-parser.test.ts
│       │   └── ssh-tunnel.test.ts
│       ├── allowed-keywords.ts
│       ├── dsn-obfuscate.ts
│       ├── response-formatter.ts
│       ├── safe-url.ts
│       ├── sql-row-limiter.ts
│       ├── ssh-config-parser.ts
│       └── ssh-tunnel.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/resources/images/logo-text-only.svg:
--------------------------------------------------------------------------------

```
1 | <svg width="4233" height="987" viewBox="0 0 4233 987" fill="none" xmlns="http://www.w3.org/2000/svg">
2 | <path d="M229.9 982.9L45.1 966.4L85.8 210.7C66.7333 215.1 49.1333 220.233 33 226.1L0 39.1C38.8667 31.0333 86.9 22.6 144.1 13.7999C201.3 4.99998 260.333 0.599988 321.2 0.599988C368.133 0.599988 415.8 5.73332 464.2 16C512.6 25.5333 558.8 41.6666 602.8 64.4C647.533 86.4 687.133 116.1 721.6 153.5C756.8 190.9 784.3 236.733 804.1 291C824.633 345.267 834.9 409.433 834.9 483.5C834.9 587.633 818.033 676.733 784.3 750.8C751.3 824.133 702.9 880.233 639.1 919.1C576.033 957.967 499.767 977.4 410.3 977.4C350.9 977.4 292.233 966.4 234.3 944.4C232.833 959.067 231.367 971.9 229.9 982.9ZM313.5 185.4C303.233 185.4 292.6 185.4 281.6 185.4C274.267 321.8 267.667 438.033 261.8 534.1C255.933 629.433 250.8 709 246.4 772.8C268.4 780.133 292.6 786.733 319 792.6C346.133 797.733 374.367 800.3 403.7 800.3C463.1 800.3 510.767 785.267 546.7 755.2C583.367 725.133 609.767 685.533 625.9 636.4C642.033 586.533 650.1 532.633 650.1 474.7C650.1 428.5 643.133 389.267 629.2 357C615.267 324.733 596.933 297.967 574.2 276.7C552.2 255.433 528 238.933 501.6 227.2C475.2 214.733 449.167 205.567 423.5 199.7C398.567 193.833 376.2 190.167 356.4 188.7C336.6 186.5 322.3 185.4 313.5 185.4ZM971.163 971.9C970.429 963.833 970.063 955.033 970.063 945.5C970.063 935.967 970.063 924.967 970.063 912.5C970.063 880.967 970.796 843.567 972.263 800.3C973.729 756.3 975.929 709.733 978.863 660.6C981.796 610.733 984.729 560.867 987.663 511C990.596 460.4 993.529 412 996.463 365.8C1000.13 319.6 1003.43 278.167 1006.36 241.5C990.229 246.633 975.929 251.4 963.463 255.8L934.863 70.9999C1017.73 47.5333 1094.73 29.9333 1165.86 18.2C1237.73 6.46665 1302.63 0.599988 1360.56 0.599988C1478.63 0.599988 1568.83 22.9667 1631.16 67.7C1693.5 112.433 1724.66 177.333 1724.66 262.4C1724.66 310.8 1714.03 351.5 1692.76 384.5C1671.5 416.767 1641.43 443.533 1602.56 464.8C1697.9 513.2 1745.56 592.033 1745.56 701.3C1745.56 749.7 1736.4 790.4 1718.06 823.4C1700.46 855.667 1677.73 881.7 1649.86 901.5C1622 921.3 1592.3 935.967 1560.76 945.5C1529.96 955.767 1500.63 962.367 1472.76 965.3C1445.63 968.967 1424.36 970.8 1408.96 970.8C1370.83 970.8 1330.13 967.133 1286.86 959.8C1243.6 952.467 1201.43 941.467 1160.36 926.8C1160.36 941.467 1160.36 955.4 1160.36 968.6L971.163 971.9ZM1364.96 184.3C1338.56 184.3 1310.7 185.767 1281.36 188.7C1252.76 190.9 1224.16 194.567 1195.56 199.7C1191.16 253.233 1186.76 315.567 1182.36 386.7C1191.9 387.433 1201.43 387.8 1210.96 387.8C1221.23 387.8 1231.5 387.8 1241.76 387.8C1435.36 387.8 1532.16 348.2 1532.16 269C1532.16 242.6 1517.13 222.067 1487.06 207.4C1457.73 192 1417.03 184.3 1364.96 184.3ZM1267.06 561.6C1251.66 561.6 1235.9 561.6 1219.76 561.6C1204.36 561.6 1188.6 561.967 1172.46 562.7C1171 594.967 1169.53 627.233 1168.06 659.5C1167.33 691.767 1166.23 723.667 1164.76 755.2C1205.83 767.667 1246.16 776.833 1285.76 782.7C1325.36 787.833 1359.46 790.4 1388.06 790.4C1448.93 790.4 1492.56 783.067 1518.96 768.4C1545.36 753 1558.56 728.067 1558.56 693.6C1558.56 651.067 1533.26 618.433 1482.66 595.7C1432.8 572.967 1360.93 561.6 1267.06 561.6ZM1889.67 975.2C1883.07 934.133 1878.3 883.9 1875.37 824.5C1872.44 765.1 1870.97 700.933 1870.97 632C1870.97 560.867 1872.07 487.9 1874.27 413.1C1877.2 338.3 1880.5 266.433 1884.17 197.5C1888.57 127.833 1893.34 66.2333 1898.47 12.7L2089.87 16C2081.8 67.3333 2075.2 124.533 2070.07 187.6C2065.67 249.933 2062 314.833 2059.07 382.3C2111.14 374.967 2167.6 369.467 2228.47 365.8C2289.34 362.133 2348 360.3 2404.47 360.3C2431.6 360.3 2457.64 361.033 2482.57 362.5C2481.84 307.5 2480.74 256.167 2479.27 208.5C2477.8 160.833 2475.97 120.5 2473.77 87.5C2471.57 54.5 2469 31.7666 2466.07 19.2999L2655.27 7.19997C2661.87 68.8 2667 142.5 2670.67 228.3C2675.07 314.1 2677.27 405.033 2677.27 501.1C2677.27 578.833 2675.8 658.033 2672.87 738.7C2669.94 818.633 2664.8 895.633 2657.47 969.7H2473.77C2476.7 946.233 2478.9 914.333 2480.37 874C2481.84 832.933 2482.94 786.733 2483.67 735.4C2484.4 683.333 2484.77 629.067 2484.77 572.6C2484.77 562.333 2484.77 551.7 2484.77 540.7C2470.84 540.7 2456.54 540.7 2441.87 540.7C2427.94 539.967 2413.64 539.6 2398.97 539.6C2341.04 539.6 2282.37 541.8 2222.97 546.2C2164.3 549.867 2108.2 555.367 2054.67 562.7C2054.67 586.167 2054.67 609.633 2054.67 633.1C2054.67 697.633 2055.04 758.133 2055.77 814.6C2057.24 871.067 2059.44 922.4 2062.37 968.6L1889.67 975.2ZM3052.37 986.2C3005.44 986.2 2966.57 977.767 2935.77 960.9C2905.7 944.767 2881.5 922.4 2863.17 893.8C2845.57 865.2 2832.37 833.3 2823.57 798.1C2815.5 762.167 2810 725.133 2807.07 687C2804.87 648.133 2803.77 610.733 2803.77 574.8C2803.77 533.733 2804.87 491.933 2807.07 449.4C2810 406.867 2812.94 360.3 2815.87 309.7L3017.17 297.6C3014.97 313 3011.67 336.1 3007.27 366.9C3002.87 397.7 2998.84 432.9 2995.17 472.5C2991.5 512.1 2989.67 553.533 2989.67 596.8C2989.67 667.933 2995.9 720.733 3008.37 755.2C3020.84 788.933 3039.54 805.8 3064.47 805.8C3114.34 805.8 3151 769.5 3174.47 696.9C3197.94 624.3 3209.67 509.533 3209.67 352.6V302L3303.17 299.8L3416.47 297.6C3406.2 366.533 3398.5 429.967 3393.37 487.9C3388.24 545.833 3384.94 594.6 3383.47 634.2C3382 673.067 3381.27 699.1 3381.27 712.3C3381.27 746.033 3384.2 770.233 3390.07 784.9C3395.94 798.833 3406.2 805.8 3420.87 805.8C3428.2 805.8 3436.27 804.333 3445.07 801.4C3453.87 798.467 3464.14 793.333 3475.87 786L3468.17 975.2C3440.3 979.6 3416.1 981.8 3395.57 981.8C3360.37 981.8 3331.4 975.2 3308.67 962C3286.67 948.8 3269.07 931.2 3255.87 909.2C3233.14 933.4 3204.9 952.1 3171.17 965.3C3138.17 979.233 3098.57 986.2 3052.37 986.2ZM3576.47 975.2C3576.47 915.067 3577.57 850.533 3579.77 781.6C3581.97 712.667 3584.9 642.633 3588.57 571.5C3592.24 500.367 3595.9 430.7 3599.57 362.5C3603.97 294.3 3608.37 230.133 3612.77 170C3617.17 109.133 3620.84 55.2333 3623.77 8.29995L3807.47 10.4999C3803.07 52.3 3798.67 99.2333 3794.27 151.3C3790.6 203.367 3786.94 258.733 3783.27 317.4C3804.54 308.6 3827.27 302 3851.47 297.6C3876.4 292.467 3903.17 289.9 3931.77 289.9C3970.64 289.9 4007.67 296.5 4042.87 309.7C4078.8 322.167 4111.07 341.6 4139.67 368C4168.27 394.4 4190.64 428.5 4206.77 470.3C4223.64 512.1 4232.07 562.333 4232.07 621C4232.07 683.333 4223.27 737.233 4205.67 782.7C4188.07 828.167 4164.24 865.567 4134.17 894.9C4104.1 924.233 4070 945.867 4031.87 959.8C3994.47 974.467 3955.97 981.8 3916.37 981.8C3886.3 981.8 3858.07 977.767 3831.67 969.7C3806 961.633 3782.17 951 3760.17 937.8C3760.17 949.533 3760.17 961.267 3760.17 973L3576.47 975.2ZM3906.47 474.7C3851.47 474.7 3806.74 490.833 3772.27 523.1C3769.34 590.567 3766.77 656.567 3764.57 721.1C3800.5 768.767 3850.74 792.6 3915.27 792.6C3936.54 792.6 3957.8 787.833 3979.07 778.3C4001.07 768.033 4019.4 750.8 4034.07 726.6C4048.74 702.4 4056.07 668.667 4056.07 625.4C4056.07 576.267 4043.24 538.867 4017.57 513.2C3992.64 487.533 3955.6 474.7 3906.47 474.7Z" fill="black"/>
3 | </svg>
4 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4 | import express from "express";
  5 | import path from "path";
  6 | import { readFileSync } from "fs";
  7 | import { fileURLToPath } from "url";
  8 | 
  9 | import { ConnectorManager } from "./connectors/manager.js";
 10 | import { ConnectorRegistry } from "./connectors/interface.js";
 11 | import { resolveDSN, resolveTransport, resolvePort, isDemoMode, redactDSN, isReadOnlyMode, resolveId } from "./config/env.js";
 12 | import { getSqliteInMemorySetupSql } from "./config/demo-loader.js";
 13 | import { registerResources } from "./resources/index.js";
 14 | import { registerTools } from "./tools/index.js";
 15 | import { registerPrompts } from "./prompts/index.js";
 16 | 
 17 | // Create __dirname equivalent for ES modules
 18 | const __filename = fileURLToPath(import.meta.url);
 19 | const __dirname = path.dirname(__filename);
 20 | 
 21 | // Load package.json to get version
 22 | const packageJsonPath = path.join(__dirname, "..", "package.json");
 23 | const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
 24 | 
 25 | // Server info
 26 | export const SERVER_NAME = "DBHub MCP Server";
 27 | export const SERVER_VERSION = packageJson.version;
 28 | 
 29 | /**
 30 |  * Generate ASCII art banner with version information
 31 |  */
 32 | export function generateBanner(version: string, modes: string[] = []): string {
 33 |   // Create a mode string that includes all active modes
 34 |   const modeText = modes.length > 0 ? ` [${modes.join(' | ')}]` : '';
 35 | 
 36 |   return `
 37 |  _____  ____  _   _       _     
 38 | |  __ \\|  _ \\| | | |     | |    
 39 | | |  | | |_) | |_| |_   _| |__  
 40 | | |  | |  _ <|  _  | | | | '_ \\ 
 41 | | |__| | |_) | | | | |_| | |_) |
 42 | |_____/|____/|_| |_|\\__,_|_.__/ 
 43 |                                 
 44 | v${version}${modeText} - Universal Database MCP Server
 45 | `;
 46 | }
 47 | 
 48 | /**
 49 |  * Initialize and start the DBHub server
 50 |  */
 51 | export async function main(): Promise<void> {
 52 |   try {
 53 |     // Resolve ID from command line args (for Cursor multi-instance support)
 54 |     const idData = resolveId();
 55 |     const id = idData?.id;
 56 | 
 57 |     // Resolve DSN from command line args, environment variables, or .env files
 58 |     const dsnData = resolveDSN();
 59 | 
 60 |     if (!dsnData) {
 61 |       const samples = ConnectorRegistry.getAllSampleDSNs();
 62 |       const sampleFormats = Object.entries(samples)
 63 |         .map(([id, dsn]) => `  - ${id}: ${dsn}`)
 64 |         .join("\n");
 65 | 
 66 |       console.error(`
 67 | ERROR: Database connection string (DSN) is required.
 68 | Please provide the DSN in one of these ways (in order of priority):
 69 | 
 70 | 1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
 71 | 2. Command line argument: --dsn="your-connection-string"
 72 | 3. Environment variable: export DSN="your-connection-string"
 73 | 4. .env file: DSN=your-connection-string
 74 | 
 75 | Example formats:
 76 | ${sampleFormats}
 77 | 
 78 | See documentation for more details on configuring database connections.
 79 | `);
 80 |       process.exit(1);
 81 |     }
 82 | 
 83 |     // Create MCP server factory function for HTTP transport
 84 |     const createServer = () => {
 85 |       const server = new McpServer({
 86 |         name: SERVER_NAME,
 87 |         version: SERVER_VERSION,
 88 |       });
 89 | 
 90 |       // Register resources, tools, and prompts
 91 |       registerResources(server);
 92 |       registerTools(server, id);
 93 |       registerPrompts(server);
 94 | 
 95 |       return server;
 96 |     };
 97 | 
 98 |     // Create server factory function (will be used for both STDIO and HTTP transports)
 99 | 
100 |     // Create connector manager and connect to database
101 |     const connectorManager = new ConnectorManager();
102 |     console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
103 |     console.error(`DSN source: ${dsnData.source}`);
104 |     if (idData) {
105 |       console.error(`ID: ${idData.id} (from ${idData.source})`);
106 |     }
107 | 
108 |     // If in demo mode, load the employee database
109 |     if (dsnData.isDemo) {
110 |       const initScript = getSqliteInMemorySetupSql();
111 |       await connectorManager.connectWithDSN(dsnData.dsn, initScript);
112 |     } else {
113 |       await connectorManager.connectWithDSN(dsnData.dsn);
114 |     }
115 | 
116 |     // Resolve transport type
117 |     const transportData = resolveTransport();
118 |     console.error(`Using transport: ${transportData.type}`);
119 |     console.error(`Transport source: ${transportData.source}`);
120 | 
121 |     // Print ASCII art banner with version and slogan
122 |     const readonly = isReadOnlyMode();
123 |     
124 |     // Collect active modes
125 |     const activeModes: string[] = [];
126 |     const modeDescriptions: string[] = [];
127 |     
128 |     if (dsnData.isDemo) {
129 |       activeModes.push("DEMO");
130 |       modeDescriptions.push("using sample employee database");
131 |     }
132 |     
133 |     if (readonly) {
134 |       activeModes.push("READ-ONLY");
135 |       modeDescriptions.push("only read only queries allowed");
136 |     }
137 |     
138 |     // Output mode information
139 |     if (activeModes.length > 0) {
140 |       console.error(`Running in ${activeModes.join(' and ')} mode - ${modeDescriptions.join(', ')}`);
141 |     }
142 |     
143 |     console.error(generateBanner(SERVER_VERSION, activeModes));
144 | 
145 |     // Set up transport based on type
146 |     if (transportData.type === "http") {
147 |       // Set up Express server for Streamable HTTP transport
148 |       const app = express();
149 | 
150 |       // Enable JSON parsing
151 |       app.use(express.json());
152 | 
153 |       // Handle CORS and security headers
154 |       app.use((req, res, next) => {
155 |         // Validate Origin header to prevent DNS rebinding attacks
156 |         const origin = req.headers.origin;
157 |         if (origin && !origin.startsWith('http://localhost') && !origin.startsWith('https://localhost')) {
158 |           return res.status(403).json({ error: 'Forbidden origin' });
159 |         }
160 |         
161 |         res.header('Access-Control-Allow-Origin', origin || 'http://localhost');
162 |         res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
163 |         res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
164 |         res.header('Access-Control-Allow-Credentials', 'true');
165 |         
166 |         if (req.method === 'OPTIONS') {
167 |           return res.sendStatus(200);
168 |         }
169 |         next();
170 |       });
171 | 
172 |       // Health check endpoint
173 |       app.get("/healthz", (req, res) => {
174 |         res.status(200).send("OK");
175 |       });
176 | 
177 |       // Main endpoint for streamable HTTP transport
178 |       app.post("/message", async (req, res) => {
179 |         try {
180 |           // In stateless mode, create a new instance of transport and server for each request
181 |           // to ensure complete isolation. A single instance would cause request ID collisions
182 |           // when multiple clients connect concurrently.
183 |           const transport = new StreamableHTTPServerTransport({
184 |             sessionIdGenerator: undefined, // Disable session management for stateless mode
185 |             enableJsonResponse: false // Use SSE streaming
186 |           });
187 |           const server = createServer();
188 | 
189 |           await server.connect(transport);
190 |           await transport.handleRequest(req, res, req.body);
191 |         } catch (error) {
192 |           console.error("Error handling request:", error);
193 |           if (!res.headersSent) {
194 |             res.status(500).json({ error: 'Internal server error' });
195 |           }
196 |         }
197 |       });
198 | 
199 | 
200 |       // Start the HTTP server
201 |       const portData = resolvePort();
202 |       const port = portData.port;
203 |       console.error(`Port source: ${portData.source}`);
204 |       app.listen(port, '0.0.0.0', () => {
205 |         console.error(`DBHub server listening at http://0.0.0.0:${port}`);
206 |         console.error(`Connect to MCP server at http://0.0.0.0:${port}/message`);
207 |       });
208 |     } else {
209 |       // Set up STDIO transport
210 |       const server = createServer();
211 |       const transport = new StdioServerTransport();
212 |       console.error("Starting with STDIO transport");
213 |       await server.connect(transport);
214 | 
215 |       // Listen for SIGINT to gracefully shut down
216 |       process.on("SIGINT", async () => {
217 |         console.error("Shutting down...");
218 |         await transport.close();
219 |         process.exit(0);
220 |       });
221 |     }
222 |   } catch (err) {
223 |     console.error("Fatal error:", err);
224 |     process.exit(1);
225 |   }
226 | }
227 | 
```

--------------------------------------------------------------------------------
/src/prompts/sql-generator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { ConnectorManager } from "../connectors/manager.js";
  3 | import {
  4 |   formatPromptSuccessResponse,
  5 |   formatPromptErrorResponse,
  6 | } from "../utils/response-formatter.js";
  7 | import { SQLDialect } from "../types/sql.js";
  8 | 
  9 | // Schema for SQL generator prompt
 10 | export const sqlGeneratorSchema = {
 11 |   description: z.string().describe("Natural language description of the SQL query to generate"),
 12 |   schema: z.string().optional().describe("Optional database schema to use"),
 13 | };
 14 | 
 15 | /**
 16 |  * SQL Generator Prompt Handler
 17 |  * Generates SQL queries from natural language descriptions
 18 |  */
 19 | export async function sqlGeneratorPromptHandler(
 20 |   {
 21 |     description,
 22 |     schema,
 23 |   }: {
 24 |     description: string;
 25 |     schema?: string;
 26 |   },
 27 |   _extra: any
 28 | ) {
 29 |   try {
 30 |     // Get current connector to determine dialect
 31 |     const connector = ConnectorManager.getCurrentConnector();
 32 | 
 33 |     // Determine SQL dialect from connector automatically
 34 |     let sqlDialect: SQLDialect;
 35 |     switch (connector.id) {
 36 |       case "postgres":
 37 |         sqlDialect = "postgres";
 38 |         break;
 39 |       case "sqlite":
 40 |         sqlDialect = "sqlite";
 41 |         break;
 42 |       case "mysql":
 43 |         sqlDialect = "mysql";
 44 |         break;
 45 |       case "sqlserver":
 46 |         sqlDialect = "mssql";
 47 |         break;
 48 |       default:
 49 |         sqlDialect = "ansi"; // Default to standard SQL if connector type is unknown
 50 |     }
 51 | 
 52 |     // Verify schema exists if provided
 53 |     if (schema) {
 54 |       const availableSchemas = await connector.getSchemas();
 55 |       if (!availableSchemas.includes(schema)) {
 56 |         return formatPromptErrorResponse(
 57 |           `Schema '${schema}' does not exist or cannot be accessed. Available schemas: ${availableSchemas.join(", ")}`,
 58 |           "SCHEMA_NOT_FOUND"
 59 |         );
 60 |       }
 61 |     }
 62 | 
 63 |     // Get schema information to help with table/column references
 64 |     try {
 65 |       const tables = await connector.getTables(schema);
 66 | 
 67 |       if (tables.length === 0) {
 68 |         const schemaInfo = schema ? `in schema '${schema}'` : "in the database";
 69 |         return formatPromptErrorResponse(
 70 |           `No tables found ${schemaInfo}. Please check your database connection or schema name.`,
 71 |           "NO_TABLES_FOUND"
 72 |         );
 73 |       }
 74 | 
 75 |       const tableSchemas = await Promise.all(
 76 |         tables.map(async (table) => {
 77 |           try {
 78 |             const columns = await connector.getTableSchema(table, schema);
 79 |             return {
 80 |               table,
 81 |               columns: columns.map((col) => ({
 82 |                 name: col.column_name,
 83 |                 type: col.data_type,
 84 |               })),
 85 |             };
 86 |           } catch (error) {
 87 |             // Skip tables we can't access
 88 |             return null;
 89 |           }
 90 |         })
 91 |       );
 92 | 
 93 |       // Filter out null entries (tables we couldn't access)
 94 |       const accessibleSchemas = tableSchemas.filter((schema) => schema !== null);
 95 | 
 96 |       if (accessibleSchemas.length === 0) {
 97 |         return formatPromptErrorResponse(
 98 |           `No accessible tables found. You may not have sufficient permissions to access table schemas.`,
 99 |           "NO_ACCESSIBLE_TABLES"
100 |         );
101 |       }
102 | 
103 |       // Generate a schema description for context
104 |       const schemaContext =
105 |         accessibleSchemas.length > 0
106 |           ? `Available tables and their columns:\n${accessibleSchemas
107 |               .map(
108 |                 (schema) =>
109 |                   `- ${schema!.table}: ${schema!.columns
110 |                     .map((col) => `${col.name} (${col.type})`)
111 |                     .join(", ")}`
112 |               )
113 |               .join("\n")}`
114 |           : "No schema information available.";
115 | 
116 |       // Example queries for the given dialect to use as reference
117 |       const dialectExamples: Record<SQLDialect, string[]> = {
118 |         postgres: [
119 |           "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day'",
120 |           "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
121 |           "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)",
122 |         ],
123 |         sqlite: [
124 |           "SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
125 |           "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
126 |           "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)",
127 |         ],
128 |         mysql: [
129 |           "SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
130 |           "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
131 |           "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)",
132 |         ],
133 |         mssql: [
134 |           "SELECT * FROM users WHERE created_at > DATEADD(day, -1, GETDATE())",
135 |           "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
136 |           "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)",
137 |         ],
138 |         ansi: [
139 |           "SELECT * FROM users WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '1' DAY",
140 |           "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
141 |           "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)",
142 |         ],
143 |       };
144 | 
145 |       // Build a prompt that would help generate the SQL
146 |       // In a real implementation, this would call an AI model
147 |       const schemaInfo = schema ? `in schema '${schema}'` : "across all schemas";
148 |       const prompt = `
149 | Generate a ${sqlDialect} SQL query based on this description: "${description}"
150 | 
151 | ${schemaContext}
152 | Working ${schemaInfo}
153 | 
154 | The query should:
155 | 1. Be written for ${sqlDialect} dialect
156 | 2. Use only the available tables and columns
157 | 3. Prioritize readability
158 | 4. Include appropriate comments
159 | 5. Be compatible with ${sqlDialect} syntax
160 | `;
161 | 
162 |       // In a real implementation, this would be the result from an AI model call
163 |       // For this demo, we'll generate a simple SQL query based on the description
164 |       let generatedSQL: string;
165 | 
166 |       // Very simple pattern matching for demo purposes
167 |       // In a real implementation, this would use a language model
168 |       if (description.toLowerCase().includes("count")) {
169 |         const schemaPrefix = schema ? `-- Schema: ${schema}\n` : "";
170 |         generatedSQL = `${schemaPrefix}-- Count query generated from: "${description}"
171 | SELECT COUNT(*) AS count 
172 | FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]!.table : "table_name"};`;
173 |       } else if (
174 |         description.toLowerCase().includes("average") ||
175 |         description.toLowerCase().includes("avg")
176 |       ) {
177 |         const table = accessibleSchemas.length > 0 ? accessibleSchemas[0]!.table : "table_name";
178 |         const numericColumn =
179 |           accessibleSchemas.length > 0
180 |             ? accessibleSchemas[0]!.columns.find((col) =>
181 |                 ["int", "numeric", "decimal", "float", "real", "double"].some((t) =>
182 |                   col.type.includes(t)
183 |                 )
184 |               )?.name || "numeric_column"
185 |             : "numeric_column";
186 | 
187 |         const schemaPrefix = schema ? `-- Schema: ${schema}\n` : "";
188 |         generatedSQL = `${schemaPrefix}-- Average query generated from: "${description}"
189 | SELECT AVG(${numericColumn}) AS average
190 | FROM ${table};`;
191 |       } else if (description.toLowerCase().includes("join")) {
192 |         const schemaPrefix = schema ? `-- Schema: ${schema}\n` : "";
193 |         generatedSQL = `${schemaPrefix}-- Join query generated from: "${description}"
194 | SELECT t1.*, t2.*
195 | FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"} t1
196 | JOIN ${accessibleSchemas.length > 1 ? accessibleSchemas[1]?.table : "table2"} t2
197 |   ON t1.id = t2.${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"}_id;`;
198 |       } else {
199 |         // Default to a simple SELECT
200 |         const table = accessibleSchemas.length > 0 ? accessibleSchemas[0]!.table : "table_name";
201 |         const schemaPrefix = schema ? `-- Schema: ${schema}\n` : "";
202 |         generatedSQL = `${schemaPrefix}-- Query generated from: "${description}"
203 | SELECT * 
204 | FROM ${table}
205 | LIMIT 10;`;
206 |       }
207 | 
208 |       // Return the generated SQL with explanations
209 |       return formatPromptSuccessResponse(
210 |         generatedSQL,
211 |         // Add references to example queries that could help
212 |         dialectExamples[sqlDialect]
213 |       );
214 |     } catch (error) {
215 |       return formatPromptErrorResponse(
216 |         `Error generating SQL query schema information: ${(error as Error).message}`,
217 |         "SCHEMA_RETRIEVAL_ERROR"
218 |       );
219 |     }
220 |   } catch (error) {
221 |     return formatPromptErrorResponse(
222 |       `Failed to generate SQL: ${(error as Error).message}`,
223 |       "SQL_GENERATION_ERROR"
224 |     );
225 |   }
226 | }
227 | 
```

--------------------------------------------------------------------------------
/src/connectors/__tests__/shared/integration-test-base.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2 | import type { Connector } from '../../interface.js';
  3 | 
  4 | export interface DatabaseTestConfig {
  5 |   expectedSchemas: string[];
  6 |   expectedTables: string[];
  7 |   expectedTestSchemaTable?: string;
  8 |   testSchema?: string;
  9 |   supportsStoredProcedures?: boolean;
 10 |   expectedStoredProcedures?: string[];
 11 | }
 12 | 
 13 | export interface TestContainer {
 14 |   getConnectionUri(): string;
 15 |   stop(): Promise<void>;
 16 | }
 17 | 
 18 | /**
 19 |  * Base class for database integration tests that provides common test patterns
 20 |  */
 21 | export abstract class IntegrationTestBase<TContainer extends TestContainer> {
 22 |   protected container!: TContainer;
 23 |   public connector!: Connector;
 24 |   protected connectionString!: string;
 25 |   public config: DatabaseTestConfig;
 26 | 
 27 |   constructor(config: DatabaseTestConfig) {
 28 |     this.config = config;
 29 |   }
 30 | 
 31 |   /**
 32 |    * Abstract methods that must be implemented by specific database test classes
 33 |    */
 34 |   abstract createContainer(): Promise<TContainer>;
 35 |   abstract createConnector(): Connector;
 36 |   abstract setupTestData(connector: Connector): Promise<void>;
 37 | 
 38 |   /**
 39 |    * Setup method to be called in beforeAll
 40 |    */
 41 |   async setup(): Promise<void> {
 42 |     console.log('Starting database container...');
 43 |     
 44 |     this.container = await this.createContainer();
 45 |     console.log('Container started, getting connection details...');
 46 |     
 47 |     this.connectionString = this.container.getConnectionUri();
 48 |     console.log('Connection URI:', this.connectionString);
 49 |     
 50 |     this.connector = this.createConnector();
 51 |     await this.connector.connect(this.connectionString);
 52 |     console.log('Connected to database');
 53 |     
 54 |     await this.setupTestData(this.connector);
 55 |     console.log('Test data setup complete');
 56 |   }
 57 | 
 58 |   /**
 59 |    * Cleanup method to be called in afterAll
 60 |    */
 61 |   async cleanup(): Promise<void> {
 62 |     if (this.connector) {
 63 |       await this.connector.disconnect();
 64 |     }
 65 |     if (this.container) {
 66 |       await this.container.stop();
 67 |     }
 68 |   }
 69 | 
 70 |   /**
 71 |    * Common test suite that can be reused across different database types
 72 |    */
 73 |   createTestSuite(suiteName: string): void {
 74 |     describe(suiteName, () => {
 75 |       beforeAll(async () => {
 76 |         await this.setup();
 77 |       }, 120000);
 78 | 
 79 |       afterAll(async () => {
 80 |         await this.cleanup();
 81 |       });
 82 | 
 83 |       this.createConnectionTests();
 84 |       this.createSchemaTests();
 85 |       this.createTableTests();
 86 |       this.createSQLExecutionTests();
 87 |       
 88 |       if (this.config.supportsStoredProcedures) {
 89 |         this.createStoredProcedureTests();
 90 |       }
 91 |       
 92 |       this.createErrorHandlingTests();
 93 |     });
 94 |   }
 95 | 
 96 |   createConnectionTests(): void {
 97 |     describe('Connection', () => {
 98 |       it('should connect successfully to database container', async () => {
 99 |         expect(this.connector).toBeDefined();
100 |       });
101 | 
102 |       it('should parse DSN correctly', async () => {
103 |         const sampleDSN = this.connector.dsnParser.getSampleDSN();
104 |         expect(sampleDSN).toContain('://');
105 |         expect(this.connector.dsnParser.isValidDSN(sampleDSN)).toBe(true);
106 |       });
107 | 
108 |       it('should validate DSN format', () => {
109 |         const sampleDSN = this.connector.dsnParser.getSampleDSN();
110 |         expect(this.connector.dsnParser.isValidDSN(sampleDSN)).toBe(true);
111 |         expect(this.connector.dsnParser.isValidDSN('invalid-dsn')).toBe(false);
112 |       });
113 |     });
114 |   }
115 | 
116 |   createSchemaTests(): void {
117 |     describe('Schema Operations', () => {
118 |       it('should list schemas', async () => {
119 |         const schemas = await this.connector.getSchemas();
120 |         this.config.expectedSchemas.forEach(expectedSchema => {
121 |           expect(schemas).toContain(expectedSchema);
122 |         });
123 |       });
124 | 
125 |       it('should list tables in default schema', async () => {
126 |         const tables = await this.connector.getTables();
127 |         this.config.expectedTables.forEach(expectedTable => {
128 |           expect(tables).toContain(expectedTable);
129 |         });
130 |       });
131 | 
132 |       if (this.config.testSchema && this.config.expectedTestSchemaTable) {
133 |         it('should list tables in specific schema', async () => {
134 |           const tables = await this.connector.getTables(this.config.testSchema);
135 |           expect(tables).toContain(this.config.expectedTestSchemaTable);
136 |         });
137 |       }
138 | 
139 |       it('should check if table exists', async () => {
140 |         const firstTable = this.config.expectedTables[0];
141 |         expect(await this.connector.tableExists(firstTable)).toBe(true);
142 |         expect(await this.connector.tableExists('nonexistent_table')).toBe(false);
143 |         
144 |         if (this.config.testSchema && this.config.expectedTestSchemaTable) {
145 |           expect(await this.connector.tableExists(this.config.expectedTestSchemaTable, this.config.testSchema)).toBe(true);
146 |           expect(await this.connector.tableExists(this.config.expectedTestSchemaTable, 'public')).toBe(false);
147 |         }
148 |       });
149 |     });
150 |   }
151 | 
152 |   createTableTests(): void {
153 |     describe('Table Schema Operations', () => {
154 |       it('should get table schema for users table', async () => {
155 |         const schema = await this.connector.getTableSchema('users');
156 |         expect(schema.length).toBeGreaterThan(0);
157 |         
158 |         const idColumn = schema.find(col => col.column_name === 'id');
159 |         expect(idColumn).toBeDefined();
160 |         expect(idColumn?.is_nullable).toBe('NO');
161 |         
162 |         const nameColumn = schema.find(col => col.column_name === 'name');
163 |         expect(nameColumn).toBeDefined();
164 |       });
165 | 
166 |       it('should get table indexes', async () => {
167 |         const indexes = await this.connector.getTableIndexes('users');
168 |         expect(indexes.length).toBeGreaterThan(0);
169 |         
170 |         const primaryIndex = indexes.find(idx => idx.is_primary);
171 |         expect(primaryIndex).toBeDefined();
172 |         expect(primaryIndex?.column_names).toContain('id');
173 |         
174 |         // Some databases automatically create unique indexes, others handle unique constraints differently
175 |         // We'll just verify we got at least the primary key index
176 |         expect(indexes.length).toBeGreaterThanOrEqual(1);
177 |       });
178 |     });
179 |   }
180 | 
181 |   createSQLExecutionTests(): void {
182 |     describe('SQL Execution', () => {
183 |       it('should execute simple SELECT query', async () => {
184 |         const result = await this.connector.executeSQL('SELECT COUNT(*) as count FROM users', {});
185 |         expect(result.rows).toHaveLength(1);
186 |         expect(Number(result.rows[0].count)).toBeGreaterThanOrEqual(3);
187 |       });
188 | 
189 |       it('should execute INSERT and SELECT', async () => {
190 |         const insertResult = await this.connector.executeSQL(
191 |           "INSERT INTO users (name, email, age) VALUES ('Test User', '[email protected]', 25)", {}
192 |         );
193 |         expect(insertResult).toBeDefined();
194 |         
195 |         const selectResult = await this.connector.executeSQL(
196 |           "SELECT * FROM users WHERE email = '[email protected]'", {}
197 |         );
198 |         expect(selectResult.rows).toHaveLength(1);
199 |         expect(selectResult.rows[0].name).toBe('Test User');
200 |         expect(Number(selectResult.rows[0].age)).toBe(25);
201 |       });
202 | 
203 |       it('should handle complex queries with joins', async () => {
204 |         const result = await this.connector.executeSQL(`
205 |           SELECT u.name, COUNT(o.id) as order_count
206 |           FROM users u
207 |           LEFT JOIN orders o ON u.id = o.user_id
208 |           GROUP BY u.id, u.name
209 |           HAVING COUNT(o.id) > 0
210 |           ORDER BY order_count DESC
211 |         `, {});
212 |         
213 |         expect(result.rows.length).toBeGreaterThan(0);
214 |         expect(result.rows[0]).toHaveProperty('name');
215 |         expect(result.rows[0]).toHaveProperty('order_count');
216 |       });
217 |     });
218 |   }
219 | 
220 |   createStoredProcedureTests(): void {
221 |     describe('Stored Procedures', () => {
222 |       it('should list stored procedures', async () => {
223 |         const procedures = await this.connector.getStoredProcedures();
224 |         if (this.config.expectedStoredProcedures) {
225 |           this.config.expectedStoredProcedures.forEach(expectedProc => {
226 |             expect(procedures).toContain(expectedProc);
227 |           });
228 |         }
229 |       });
230 | 
231 |       if (this.config.expectedStoredProcedures?.length) {
232 |         it('should get stored procedure details', async () => {
233 |           const procedureName = this.config.expectedStoredProcedures[0];
234 |           const procedure = await this.connector.getStoredProcedureDetail(procedureName);
235 |           expect(procedure.procedure_name).toBe(procedureName);
236 |           expect(procedure.procedure_type).toMatch(/function|procedure/);
237 |         });
238 |       }
239 |     });
240 |   }
241 | 
242 |   createErrorHandlingTests(): void {
243 |     describe('Error Handling', () => {
244 |       it('should handle invalid SQL gracefully', async () => {
245 |         await expect(
246 |           this.connector.executeSQL('SELECT * FROM nonexistent_table', {})
247 |         ).rejects.toThrow();
248 |       });
249 | 
250 |       it('should handle connection errors', async () => {
251 |         const newConnector = this.createConnector();
252 |         await expect(
253 |           newConnector.executeSQL('SELECT 1', {})
254 |         ).rejects.toThrow(/Not connected to.*database/);
255 |       });
256 | 
257 |       it('should handle invalid table schema requests', async () => {
258 |         const result = await this.connector.getTableSchema('nonexistent_table');
259 |         expect(Array.isArray(result)).toBe(true);
260 |         expect(result.length).toBe(0);
261 |       });
262 |     });
263 |   }
264 | }
```

--------------------------------------------------------------------------------
/src/connectors/__tests__/postgres-ssh.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
  2 | import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
  3 | import { PostgresConnector } from '../postgres/index.js';
  4 | import { ConnectorManager } from '../manager.js';
  5 | import { ConnectorRegistry } from '../interface.js';
  6 | import { SSHTunnel } from '../../utils/ssh-tunnel.js';
  7 | import type { SSHTunnelConfig } from '../../types/ssh.js';
  8 | import * as sshConfigParser from '../../utils/ssh-config-parser.js';
  9 | 
 10 | describe('PostgreSQL SSH Tunnel Simple Integration Tests', () => {
 11 |   let postgresContainer: StartedPostgreSqlContainer;
 12 | 
 13 |   beforeAll(async () => {
 14 |     // Register PostgreSQL connector
 15 |     ConnectorRegistry.register(new PostgresConnector());
 16 |     
 17 |     // Start PostgreSQL container
 18 |     postgresContainer = await new PostgreSqlContainer('postgres:15-alpine')
 19 |       .withDatabase('testdb')
 20 |       .withUsername('testuser')
 21 |       .withPassword('testpass')
 22 |       .start();
 23 |   }, 60000); // 1 minute timeout for container startup
 24 | 
 25 |   afterAll(async () => {
 26 |     await postgresContainer?.stop();
 27 |   });
 28 | 
 29 |   describe('SSH Tunnel Basic Functionality', () => {
 30 |     it('should establish SSH tunnel and connect to local port', async () => {
 31 |       // For this test, we'll create a mock SSH tunnel that just forwards to the same port
 32 |       // This tests the tunnel establishment logic without needing a real SSH server
 33 |       const tunnel = new SSHTunnel();
 34 |       
 35 |       // Test that the tunnel correctly reports its state
 36 |       expect(tunnel.getIsConnected()).toBe(false);
 37 |       expect(tunnel.getTunnelInfo()).toBeNull();
 38 |     });
 39 | 
 40 |     it('should parse DSN correctly when SSH tunnel is configured', async () => {
 41 |       const manager = new ConnectorManager();
 42 |       
 43 |       // Test DSN parsing with getDefaultPort
 44 |       const testCases = [
 45 |         { dsn: 'postgres://user:pass@host:5432/db', expectedPort: 5432 },
 46 |         { dsn: 'mysql://user:pass@host:3306/db', expectedPort: 3306 },
 47 |         { dsn: 'mariadb://user:pass@host:3306/db', expectedPort: 3306 },
 48 |         { dsn: 'sqlserver://user:pass@host:1433/db', expectedPort: 1433 },
 49 |       ];
 50 |       
 51 |       for (const testCase of testCases) {
 52 |         // Access private method through reflection for testing
 53 |         const port = (manager as any).getDefaultPort(testCase.dsn);
 54 |         expect(port).toBe(testCase.expectedPort);
 55 |       }
 56 |     });
 57 | 
 58 |     it('should handle connection without SSH tunnel', async () => {
 59 |       const manager = new ConnectorManager();
 60 |       
 61 |       // Make sure no SSH config is set
 62 |       delete process.env.SSH_HOST;
 63 |       
 64 |       const dsn = postgresContainer.getConnectionUri();
 65 |       
 66 |       await manager.connectWithDSN(dsn);
 67 |       
 68 |       // Test that connection works
 69 |       const connector = manager.getConnector();
 70 |       const result = await connector.executeSQL('SELECT 1 as test', {});
 71 |       expect(result.rows).toHaveLength(1);
 72 |       expect(result.rows[0].test).toBe(1);
 73 |       
 74 |       await manager.disconnect();
 75 |     });
 76 | 
 77 |     it('should fail gracefully when SSH config is invalid', async () => {
 78 |       const manager = new ConnectorManager();
 79 |       
 80 |       // Set invalid SSH config (missing required fields)
 81 |       process.env.SSH_HOST = 'example.com';
 82 |       // Missing SSH_USER
 83 |       
 84 |       try {
 85 |         const dsn = postgresContainer.getConnectionUri();
 86 |         await expect(manager.connectWithDSN(dsn)).rejects.toThrow(/SSH tunnel configuration requires/);
 87 |       } finally {
 88 |         delete process.env.SSH_HOST;
 89 |       }
 90 |     });
 91 | 
 92 |     it('should validate SSH authentication method', async () => {
 93 |       const manager = new ConnectorManager();
 94 |       
 95 |       // Set SSH config without authentication method
 96 |       process.env.SSH_HOST = 'example.com';
 97 |       process.env.SSH_USER = 'testuser';
 98 |       // Missing both SSH_PASSWORD and SSH_KEY
 99 |       
100 |       try {
101 |         const dsn = postgresContainer.getConnectionUri();
102 |         await expect(manager.connectWithDSN(dsn)).rejects.toThrow(/SSH tunnel configuration requires either/);
103 |       } finally {
104 |         delete process.env.SSH_HOST;
105 |         delete process.env.SSH_USER;
106 |       }
107 |     });
108 | 
109 |     it('should handle SSH config file resolution', async () => {
110 |       const manager = new ConnectorManager();
111 |       
112 |       // Mock the SSH config parser functions
113 |       const mockParseSSHConfig = vi.spyOn(sshConfigParser, 'parseSSHConfig');
114 |       const mockLooksLikeSSHAlias = vi.spyOn(sshConfigParser, 'looksLikeSSHAlias');
115 |       
116 |       // Spy on the SSH tunnel establish method to verify the config values
117 |       const mockSSHTunnelEstablish = vi.spyOn(SSHTunnel.prototype, 'establish');
118 |       
119 |       try {
120 |         // Configure mocks to simulate SSH config file lookup with specific values
121 |         mockLooksLikeSSHAlias.mockReturnValue(true);
122 |         mockParseSSHConfig.mockReturnValue({
123 |           host: 'bastion.example.com',
124 |           username: 'sshuser',
125 |           port: 2222,
126 |           privateKey: '/home/user/.ssh/id_rsa'
127 |         });
128 |         
129 |         // Mock SSH tunnel establish to capture the config and prevent actual connection
130 |         mockSSHTunnelEstablish.mockRejectedValue(new Error('SSH connection failed (expected in test)'));
131 |         
132 |         // Set SSH host alias (would normally come from command line)
133 |         process.env.SSH_HOST = 'mybastion';
134 |         
135 |         const dsn = postgresContainer.getConnectionUri();
136 |         
137 |         // This should fail during SSH connection (expected), but we can verify the config parsing
138 |         await expect(manager.connectWithDSN(dsn)).rejects.toThrow();
139 |         
140 |         // Verify that SSH config parsing functions were called correctly
141 |         expect(mockLooksLikeSSHAlias).toHaveBeenCalledWith('mybastion');
142 |         expect(mockParseSSHConfig).toHaveBeenCalledWith('mybastion', expect.stringContaining('.ssh/config'));
143 |         
144 |         // Verify that SSH tunnel was attempted with the correct config values from SSH config
145 |         expect(mockSSHTunnelEstablish).toHaveBeenCalledTimes(1);
146 |         const sshTunnelCall = mockSSHTunnelEstablish.mock.calls[0];
147 |         const [sshConfig, tunnelOptions] = sshTunnelCall;
148 |         
149 |         // Debug: Log the actual values being passed (for verification)
150 |         // SSH Config should contain the values from our mocked SSH config file
151 |         // Tunnel Options should contain database connection details from the container DSN
152 |         
153 |         // Verify SSH config values were properly resolved from the SSH config file
154 |         expect(sshConfig).toMatchObject({
155 |           host: 'bastion.example.com',    // Should use HostName from SSH config
156 |           username: 'sshuser',           // Should use User from SSH config  
157 |           port: 2222,                    // Should use Port from SSH config
158 |           privateKey: '/home/user/.ssh/id_rsa' // Should use IdentityFile from SSH config
159 |         });
160 |         
161 |         // Verify tunnel options are correctly set up for the database connection
162 |         expect(tunnelOptions).toMatchObject({
163 |           targetHost: expect.any(String), // Database host from DSN
164 |           targetPort: expect.any(Number)  // Database port from DSN
165 |         });
166 |         
167 |         // The localPort might be undefined for dynamic allocation, so check separately if it exists
168 |         if (tunnelOptions.localPort !== undefined) {
169 |           expect(typeof tunnelOptions.localPort).toBe('number');
170 |         }
171 |         
172 |         // Verify that the target database details from the DSN are preserved
173 |         const originalDsnUrl = new URL(dsn);
174 |         expect(tunnelOptions.targetHost).toBe(originalDsnUrl.hostname);
175 |         expect(tunnelOptions.targetPort).toBe(parseInt(originalDsnUrl.port));
176 |         
177 |       } finally {
178 |         // Clean up
179 |         delete process.env.SSH_HOST;
180 |         mockParseSSHConfig.mockRestore();
181 |         mockLooksLikeSSHAlias.mockRestore();
182 |         mockSSHTunnelEstablish.mockRestore();
183 |       }
184 |     });
185 | 
186 |     it('should skip SSH config lookup for direct hostnames', async () => {
187 |       const manager = new ConnectorManager();
188 |       
189 |       // Mock the SSH config parser functions
190 |       const mockParseSSHConfig = vi.spyOn(sshConfigParser, 'parseSSHConfig');
191 |       const mockLooksLikeSSHAlias = vi.spyOn(sshConfigParser, 'looksLikeSSHAlias');
192 |       
193 |       try {
194 |         // Configure mocks - direct hostname should not trigger SSH config lookup
195 |         mockLooksLikeSSHAlias.mockReturnValue(false);
196 |         
197 |         // Set a direct hostname with required SSH credentials
198 |         process.env.SSH_HOST = 'ssh.example.com';
199 |         process.env.SSH_USER = 'sshuser';
200 |         process.env.SSH_PASSWORD = 'sshpass';
201 |         
202 |         const dsn = postgresContainer.getConnectionUri();
203 |         
204 |         // This should fail during actual SSH connection, but we can verify the parsing behavior
205 |         await expect(manager.connectWithDSN(dsn)).rejects.toThrow();
206 |         
207 |         // Verify that SSH config parsing was checked but not executed
208 |         expect(mockLooksLikeSSHAlias).toHaveBeenCalledWith('ssh.example.com');
209 |         expect(mockParseSSHConfig).not.toHaveBeenCalled();
210 |         
211 |       } finally {
212 |         // Clean up
213 |         delete process.env.SSH_HOST;
214 |         delete process.env.SSH_USER;
215 |         delete process.env.SSH_PASSWORD;
216 |         mockParseSSHConfig.mockRestore();
217 |         mockLooksLikeSSHAlias.mockRestore();
218 |       }
219 |     });
220 |   });
221 | });
```

--------------------------------------------------------------------------------
/src/tools/__tests__/execute-sql.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | import { executeSqlToolHandler } from '../execute-sql.js';
  3 | import { ConnectorManager } from '../../connectors/manager.js';
  4 | import { isReadOnlyMode } from '../../config/env.js';
  5 | import type { Connector, ConnectorType, SQLResult } from '../../connectors/interface.js';
  6 | 
  7 | // Mock dependencies
  8 | vi.mock('../../connectors/manager.js');
  9 | vi.mock('../../config/env.js');
 10 | 
 11 | // Mock connector for testing
 12 | const createMockConnector = (id: ConnectorType = 'sqlite'): Connector => ({
 13 |   id,
 14 |   name: 'Mock Connector',
 15 |   dsnParser: {} as any,
 16 |   connect: vi.fn(),
 17 |   disconnect: vi.fn(),
 18 |   getSchemas: vi.fn(),
 19 |   getTables: vi.fn(),
 20 |   tableExists: vi.fn(),
 21 |   getTableSchema: vi.fn(),
 22 |   getTableIndexes: vi.fn(),
 23 |   getStoredProcedures: vi.fn(),
 24 |   getStoredProcedureDetail: vi.fn(),
 25 |   executeSQL: vi.fn(),
 26 | });
 27 | 
 28 | // Helper function to parse tool response
 29 | const parseToolResponse = (response: any) => {
 30 |   return JSON.parse(response.content[0].text);
 31 | };
 32 | 
 33 | describe('execute-sql tool', () => {
 34 |   let mockConnector: Connector;
 35 |   const mockGetCurrentConnector = vi.mocked(ConnectorManager.getCurrentConnector);
 36 |   const mockGetCurrentExecuteOptions = vi.mocked(ConnectorManager.getCurrentExecuteOptions);
 37 |   const mockIsReadOnlyMode = vi.mocked(isReadOnlyMode);
 38 | 
 39 |   beforeEach(() => {
 40 |     mockConnector = createMockConnector('sqlite');
 41 |     mockGetCurrentConnector.mockReturnValue(mockConnector);
 42 |     mockGetCurrentExecuteOptions.mockReturnValue({});
 43 |     mockIsReadOnlyMode.mockReturnValue(false);
 44 |   });
 45 | 
 46 |   afterEach(() => {
 47 |     vi.clearAllMocks();
 48 |   });
 49 | 
 50 |   describe('single statement execution', () => {
 51 |     it('should execute a single SELECT statement successfully', async () => {
 52 |       const mockResult: SQLResult = { rows: [{ id: 1, name: 'test' }] };
 53 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
 54 | 
 55 |       const result = await executeSqlToolHandler({ sql: 'SELECT * FROM users' }, null);
 56 |       const parsedResult = parseToolResponse(result);
 57 | 
 58 |       expect(parsedResult.success).toBe(true);
 59 |       expect(parsedResult.data.rows).toEqual([{ id: 1, name: 'test' }]);
 60 |       expect(parsedResult.data.count).toBe(1);
 61 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith('SELECT * FROM users', {});
 62 |     });
 63 | 
 64 |     it('should handle execution errors', async () => {
 65 |       vi.mocked(mockConnector.executeSQL).mockRejectedValue(new Error('Database error'));
 66 | 
 67 |       const result = await executeSqlToolHandler({ sql: 'SELECT * FROM invalid_table' }, null);
 68 | 
 69 |       expect(result.isError).toBe(true);
 70 |       const parsedResult = parseToolResponse(result);
 71 |       expect(parsedResult.success).toBe(false);
 72 |       expect(parsedResult.error).toBe('Database error');
 73 |       expect(parsedResult.code).toBe('EXECUTION_ERROR');
 74 |     });
 75 |   });
 76 | 
 77 |   describe('multi-statement execution', () => {
 78 |     it('should pass multi-statement SQL directly to connector', async () => {
 79 |       const mockResult: SQLResult = { rows: [{ id: 1 }] };
 80 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
 81 | 
 82 |       const sql = 'SELECT * FROM users; SELECT * FROM roles;';
 83 |       const result = await executeSqlToolHandler({ sql }, null);
 84 |       const parsedResult = parseToolResponse(result);
 85 | 
 86 |       expect(parsedResult.success).toBe(true);
 87 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
 88 |     });
 89 |   });
 90 | 
 91 |   describe('read-only mode validation', () => {
 92 |     beforeEach(() => {
 93 |       mockIsReadOnlyMode.mockReturnValue(true);
 94 |     });
 95 | 
 96 |     it('should allow single SELECT statement in read-only mode', async () => {
 97 |       const mockResult: SQLResult = { rows: [{ id: 1 }] };
 98 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
 99 | 
100 |       const result = await executeSqlToolHandler({ sql: 'SELECT * FROM users' }, null);
101 |       const parsedResult = parseToolResponse(result);
102 | 
103 |       expect(parsedResult.success).toBe(true);
104 |       expect(mockConnector.executeSQL).toHaveBeenCalled();
105 |     });
106 | 
107 |     it('should allow multiple read-only statements in read-only mode', async () => {
108 |       const mockResult: SQLResult = { rows: [] };
109 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
110 | 
111 |       const sql = 'SELECT * FROM users; SELECT * FROM roles;';
112 |       const result = await executeSqlToolHandler({ sql }, null);
113 |       const parsedResult = parseToolResponse(result);
114 | 
115 |       expect(parsedResult.success).toBe(true);
116 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
117 |     });
118 | 
119 |     it('should reject single INSERT statement in read-only mode', async () => {
120 |       const result = await executeSqlToolHandler({ sql: "INSERT INTO users (name) VALUES ('test')" }, null);
121 | 
122 |       expect(result.isError).toBe(true);
123 |       const parsedResult = parseToolResponse(result);
124 |       expect(parsedResult.success).toBe(false);
125 |       expect(parsedResult.error).toContain('Read-only mode is enabled');
126 |       expect(parsedResult.code).toBe('READONLY_VIOLATION');
127 |       expect(mockConnector.executeSQL).not.toHaveBeenCalled();
128 |     });
129 | 
130 |     it('should reject multi-statement with any write operation in read-only mode', async () => {
131 |       const sql = "SELECT * FROM users; INSERT INTO users (name) VALUES ('test'); SELECT COUNT(*) FROM users;";
132 |       const result = await executeSqlToolHandler({ sql }, null);
133 | 
134 |       expect(result.isError).toBe(true);
135 |       const parsedResult = parseToolResponse(result);
136 |       expect(parsedResult.success).toBe(false);
137 |       expect(parsedResult.error).toContain('Read-only mode is enabled');
138 |       expect(parsedResult.code).toBe('READONLY_VIOLATION');
139 |       expect(mockConnector.executeSQL).not.toHaveBeenCalled();
140 |     });
141 | 
142 |   });
143 | 
144 |   describe('SQL comments handling', () => {
145 |     it('should allow SELECT with single-line comment in read-only mode', async () => {
146 |       mockIsReadOnlyMode.mockReturnValue(true);
147 |       const mockResult: SQLResult = { rows: [{ id: 1 }] };
148 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
149 | 
150 |       const sql = '-- Fetch active users\nSELECT * FROM users WHERE active = TRUE';
151 |       const result = await executeSqlToolHandler({ sql }, null);
152 |       const parsedResult = parseToolResponse(result);
153 | 
154 |       expect(parsedResult.success).toBe(true);
155 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
156 |     });
157 | 
158 |     it('should allow SELECT with multi-line comment in read-only mode', async () => {
159 |       mockIsReadOnlyMode.mockReturnValue(true);
160 |       const mockResult: SQLResult = { rows: [] };
161 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
162 | 
163 |       const sql = '/* This query fetches\n   all products */\nSELECT * FROM products';
164 |       const result = await executeSqlToolHandler({ sql }, null);
165 |       const parsedResult = parseToolResponse(result);
166 | 
167 |       expect(parsedResult.success).toBe(true);
168 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
169 |     });
170 | 
171 |     it('should handle multiple statements with comments in read-only mode', async () => {
172 |       mockIsReadOnlyMode.mockReturnValue(true);
173 |       const mockResult: SQLResult = { rows: [] };
174 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
175 | 
176 |       const sql = '-- First query\nSELECT * FROM users;\n/* Second query */\nSELECT * FROM roles;';
177 |       const result = await executeSqlToolHandler({ sql }, null);
178 |       const parsedResult = parseToolResponse(result);
179 | 
180 |       expect(parsedResult.success).toBe(true);
181 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
182 |     });
183 | 
184 |     it('should reject INSERT with comment in read-only mode', async () => {
185 |       mockIsReadOnlyMode.mockReturnValue(true);
186 | 
187 |       const sql = '-- Insert new user\nINSERT INTO users (name) VALUES (\'test\')';
188 |       const result = await executeSqlToolHandler({ sql }, null);
189 | 
190 |       expect(result.isError).toBe(true);
191 |       const parsedResult = parseToolResponse(result);
192 |       expect(parsedResult.success).toBe(false);
193 |       expect(parsedResult.code).toBe('READONLY_VIOLATION');
194 |       expect(mockConnector.executeSQL).not.toHaveBeenCalled();
195 |     });
196 | 
197 |     it('should handle query that is only comments as read-only', async () => {
198 |       mockIsReadOnlyMode.mockReturnValue(true);
199 |       const mockResult: SQLResult = { rows: [] };
200 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
201 | 
202 |       const sql = '-- Just a comment\n/* Another comment */';
203 |       const result = await executeSqlToolHandler({ sql }, null);
204 |       const parsedResult = parseToolResponse(result);
205 | 
206 |       expect(parsedResult.success).toBe(true);
207 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
208 |     });
209 | 
210 |     it('should handle inline comments correctly', async () => {
211 |       mockIsReadOnlyMode.mockReturnValue(true);
212 |       const mockResult: SQLResult = { rows: [] };
213 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
214 | 
215 |       const sql = 'SELECT id, -- user id\n       name -- user name\nFROM users';
216 |       const result = await executeSqlToolHandler({ sql }, null);
217 |       const parsedResult = parseToolResponse(result);
218 | 
219 |       expect(parsedResult.success).toBe(true);
220 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith(sql, {});
221 |     });
222 |   });
223 | 
224 | 
225 |   describe('edge cases', () => {
226 |     it('should handle empty SQL string', async () => {
227 |       const mockResult: SQLResult = { rows: [] };
228 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
229 | 
230 |       const result = await executeSqlToolHandler({ sql: '' }, null);
231 |       const parsedResult = parseToolResponse(result);
232 | 
233 |       expect(parsedResult.success).toBe(true);
234 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith('', {});
235 |     });
236 | 
237 |     it('should handle SQL with only semicolons and whitespace', async () => {
238 |       const mockResult: SQLResult = { rows: [] };
239 |       vi.mocked(mockConnector.executeSQL).mockResolvedValue(mockResult);
240 | 
241 |       const result = await executeSqlToolHandler({ sql: '   ;  ;  ; ' }, null);
242 |       const parsedResult = parseToolResponse(result);
243 | 
244 |       expect(parsedResult.success).toBe(true);
245 |       expect(mockConnector.executeSQL).toHaveBeenCalledWith('   ;  ;  ; ', {});
246 |     });
247 |   });
248 | });
```

--------------------------------------------------------------------------------
/src/config/__tests__/env.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { buildDSNFromEnvParams, resolveDSN, resolveId } from '../env.js';
  3 | 
  4 | describe('Environment Configuration Tests', () => {
  5 |   // Store original env values to restore after tests
  6 |   const originalEnv = { ...process.env };
  7 | 
  8 |   beforeEach(() => {
  9 |     // Clear relevant environment variables before each test
 10 |     delete process.env.DB_TYPE;
 11 |     delete process.env.DB_HOST;
 12 |     delete process.env.DB_PORT;
 13 |     delete process.env.DB_USER;
 14 |     delete process.env.DB_PASSWORD;
 15 |     delete process.env.DB_NAME;
 16 |     delete process.env.DSN;
 17 |     delete process.env.ID;
 18 |   });
 19 | 
 20 |   afterEach(() => {
 21 |     // Restore original environment
 22 |     process.env = { ...originalEnv };
 23 |   });
 24 | 
 25 |   describe('buildDSNFromEnvParams', () => {
 26 |     it('should build PostgreSQL DSN with all parameters', () => {
 27 |       process.env.DB_TYPE = 'postgres';
 28 |       process.env.DB_HOST = 'localhost';
 29 |       process.env.DB_PORT = '5432';
 30 |       process.env.DB_USER = 'testuser';
 31 |       process.env.DB_PASSWORD = 'testpass';
 32 |       process.env.DB_NAME = 'testdb';
 33 | 
 34 |       const result = buildDSNFromEnvParams();
 35 | 
 36 |       expect(result).toEqual({
 37 |         dsn: 'postgres://testuser:testpass@localhost:5432/testdb',
 38 |         source: 'individual environment variables'
 39 |       });
 40 |     });
 41 | 
 42 |     it('should build MySQL DSN with default port when port not specified', () => {
 43 |       process.env.DB_TYPE = 'mysql';
 44 |       process.env.DB_HOST = 'mysql.example.com';
 45 |       process.env.DB_USER = 'admin';
 46 |       process.env.DB_PASSWORD = 'secret';
 47 |       process.env.DB_NAME = 'myapp';
 48 | 
 49 |       const result = buildDSNFromEnvParams();
 50 | 
 51 |       expect(result).toEqual({
 52 |         dsn: 'mysql://admin:[email protected]:3306/myapp',
 53 |         source: 'individual environment variables'
 54 |       });
 55 |     });
 56 | 
 57 |     it('should build MariaDB DSN with default port', () => {
 58 |       process.env.DB_TYPE = 'mariadb';
 59 |       process.env.DB_HOST = 'mariadb.example.com';
 60 |       process.env.DB_USER = 'user';
 61 |       process.env.DB_PASSWORD = 'pass';
 62 |       process.env.DB_NAME = 'database';
 63 | 
 64 |       const result = buildDSNFromEnvParams();
 65 | 
 66 |       expect(result).toEqual({
 67 |         dsn: 'mariadb://user:[email protected]:3306/database',
 68 |         source: 'individual environment variables'
 69 |       });
 70 |     });
 71 | 
 72 |     it('should build SQL Server DSN with default port', () => {
 73 |       process.env.DB_TYPE = 'sqlserver';
 74 |       process.env.DB_HOST = 'sqlserver.example.com';
 75 |       process.env.DB_USER = 'sa';
 76 |       process.env.DB_PASSWORD = 'strongpass';
 77 |       process.env.DB_NAME = 'master';
 78 | 
 79 |       const result = buildDSNFromEnvParams();
 80 | 
 81 |       expect(result).toEqual({
 82 |         dsn: 'sqlserver://sa:[email protected]:1433/master',
 83 |         source: 'individual environment variables'
 84 |       });
 85 |     });
 86 | 
 87 |     it('should build SQLite DSN with only DB_TYPE and DB_NAME', () => {
 88 |       process.env.DB_TYPE = 'sqlite';
 89 |       process.env.DB_NAME = '/path/to/database.db';
 90 | 
 91 |       const result = buildDSNFromEnvParams();
 92 | 
 93 |       expect(result).toEqual({
 94 |         dsn: 'sqlite:////path/to/database.db',
 95 |         source: 'individual environment variables'
 96 |       });
 97 |     });
 98 | 
 99 |     it('should handle postgresql type and normalize to postgres protocol', () => {
100 |       process.env.DB_TYPE = 'postgresql';
101 |       process.env.DB_HOST = 'localhost';
102 |       process.env.DB_USER = 'user';
103 |       process.env.DB_PASSWORD = 'pass';
104 |       process.env.DB_NAME = 'db';
105 | 
106 |       const result = buildDSNFromEnvParams();
107 | 
108 |       expect(result?.dsn).toBe('postgres://user:pass@localhost:5432/db');
109 |     });
110 | 
111 |     it('should properly encode special characters in password', () => {
112 |       process.env.DB_TYPE = 'postgres';
113 |       process.env.DB_HOST = 'localhost';
114 |       process.env.DB_USER = 'user';
115 |       process.env.DB_PASSWORD = 'test@pass:with/special#chars&more=special';
116 |       process.env.DB_NAME = 'db';
117 | 
118 |       const result = buildDSNFromEnvParams();
119 | 
120 |       expect(result?.dsn).toBe(
121 |         'postgres://user:test%40pass%3Awith%2Fspecial%23chars%26more%3Dspecial@localhost:5432/db'
122 |       );
123 |     });
124 | 
125 |     it('should properly encode special characters in username', () => {
126 |       process.env.DB_TYPE = 'postgres';
127 |       process.env.DB_HOST = 'localhost';
128 |       process.env.DB_USER = '[email protected]';
129 |       process.env.DB_PASSWORD = 'pass';
130 |       process.env.DB_NAME = 'db';
131 | 
132 |       const result = buildDSNFromEnvParams();
133 | 
134 |       expect(result?.dsn).toBe(
135 |         'postgres://user%40domain.com:pass@localhost:5432/db'
136 |       );
137 |     });
138 | 
139 |     it('should properly encode special characters in database name', () => {
140 |       process.env.DB_TYPE = 'postgres';
141 |       process.env.DB_HOST = 'localhost';
142 |       process.env.DB_USER = 'user';
143 |       process.env.DB_PASSWORD = 'pass';
144 |       process.env.DB_NAME = 'my-db@test';
145 | 
146 |       const result = buildDSNFromEnvParams();
147 | 
148 |       expect(result?.dsn).toBe(
149 |         'postgres://user:pass@localhost:5432/my-db%40test'
150 |       );
151 |     });
152 | 
153 |     it('should handle SQLite with special characters in file path', () => {
154 |       process.env.DB_TYPE = 'sqlite';
155 |       process.env.DB_NAME = '/tmp/test_db@#$.db';
156 | 
157 |       const result = buildDSNFromEnvParams();
158 | 
159 |       expect(result).toEqual({
160 |         dsn: 'sqlite:////tmp/test_db@#$.db',
161 |         source: 'individual environment variables'
162 |       });
163 |     });
164 | 
165 |     it('should return null when required parameters are missing for non-SQLite databases', () => {
166 |       process.env.DB_TYPE = 'postgres';
167 |       process.env.DB_HOST = 'localhost';
168 |       // Missing DB_USER, DB_PASSWORD, DB_NAME
169 | 
170 |       const result = buildDSNFromEnvParams();
171 | 
172 |       expect(result).toBeNull();
173 |     });
174 | 
175 |     it('should return null when DB_TYPE is missing', () => {
176 |       process.env.DB_HOST = 'localhost';
177 |       process.env.DB_USER = 'user';
178 |       process.env.DB_PASSWORD = 'pass';
179 |       process.env.DB_NAME = 'db';
180 | 
181 |       const result = buildDSNFromEnvParams();
182 | 
183 |       expect(result).toBeNull();
184 |     });
185 | 
186 |     it('should return null when SQLite is missing DB_NAME', () => {
187 |       process.env.DB_TYPE = 'sqlite';
188 |       // Missing DB_NAME
189 | 
190 |       const result = buildDSNFromEnvParams();
191 | 
192 |       expect(result).toBeNull();
193 |     });
194 | 
195 |     it('should throw error for unsupported database type', () => {
196 |       process.env.DB_TYPE = 'oracle';
197 |       process.env.DB_HOST = 'localhost';
198 |       process.env.DB_USER = 'user';
199 |       process.env.DB_PASSWORD = 'pass';
200 |       process.env.DB_NAME = 'db';
201 | 
202 |       expect(() => buildDSNFromEnvParams()).toThrow(
203 |         'Unsupported DB_TYPE: oracle. Supported types: postgres, postgresql, mysql, mariadb, sqlserver, sqlite'
204 |       );
205 |     });
206 | 
207 |     it('should use custom port when provided', () => {
208 |       process.env.DB_TYPE = 'postgres';
209 |       process.env.DB_HOST = 'localhost';
210 |       process.env.DB_PORT = '9999';
211 |       process.env.DB_USER = 'user';
212 |       process.env.DB_PASSWORD = 'pass';
213 |       process.env.DB_NAME = 'db';
214 | 
215 |       const result = buildDSNFromEnvParams();
216 | 
217 |       expect(result?.dsn).toBe('postgres://user:pass@localhost:9999/db');
218 |     });
219 | 
220 |     it('should return null for empty password (required field)', () => {
221 |       process.env.DB_TYPE = 'postgres';
222 |       process.env.DB_HOST = 'localhost';
223 |       process.env.DB_USER = 'user';
224 |       process.env.DB_PASSWORD = '';
225 |       process.env.DB_NAME = 'db';
226 | 
227 |       const result = buildDSNFromEnvParams();
228 | 
229 |       expect(result).toBeNull();
230 |     });
231 |   });
232 | 
233 |   describe('resolveDSN integration with individual parameters', () => {
234 |     it('should use DSN when both DSN and individual parameters are provided', () => {
235 |       process.env.DSN = 'postgres://direct:dsn@localhost:5432/directdb';
236 |       process.env.DB_TYPE = 'mysql';
237 |       process.env.DB_HOST = 'localhost';
238 |       process.env.DB_USER = 'user';
239 |       process.env.DB_PASSWORD = 'pass';
240 |       process.env.DB_NAME = 'db';
241 | 
242 |       const result = resolveDSN();
243 | 
244 |       expect(result).toEqual({
245 |         dsn: 'postgres://direct:dsn@localhost:5432/directdb',
246 |         source: 'environment variable'
247 |       });
248 |     });
249 | 
250 |     it('should fall back to individual parameters when DSN is not provided', () => {
251 |       process.env.DB_TYPE = 'postgres';
252 |       process.env.DB_HOST = 'localhost';
253 |       process.env.DB_USER = 'user';
254 |       process.env.DB_PASSWORD = 'pass';
255 |       process.env.DB_NAME = 'db';
256 | 
257 |       const result = resolveDSN();
258 | 
259 |       expect(result).toEqual({
260 |         dsn: 'postgres://user:pass@localhost:5432/db',
261 |         source: 'individual environment variables'
262 |       });
263 |     });
264 | 
265 |     it('should return null when neither DSN nor complete individual parameters are provided', () => {
266 |       process.env.DB_TYPE = 'postgres';
267 |       process.env.DB_HOST = 'localhost';
268 |       // Missing required parameters
269 | 
270 |       const result = resolveDSN();
271 | 
272 |       expect(result).toBeNull();
273 |     });
274 | 
275 |     it('should handle SQLite individual parameters correctly', () => {
276 |       process.env.DB_TYPE = 'sqlite';
277 |       process.env.DB_NAME = ':memory:';
278 | 
279 |       const result = resolveDSN();
280 | 
281 |       expect(result).toEqual({
282 |         dsn: 'sqlite:///:memory:',
283 |         source: 'individual environment variables'
284 |       });
285 |     });
286 |   });
287 | 
288 |   describe('edge cases and complex scenarios', () => {
289 |     it('should handle password with all special URL characters', () => {
290 |       process.env.DB_TYPE = 'postgres';
291 |       process.env.DB_HOST = 'localhost';
292 |       process.env.DB_USER = 'user';
293 |       process.env.DB_PASSWORD = '!@#$%^&*()+={}[]|\\:";\'<>?,./~`';
294 |       process.env.DB_NAME = 'db';
295 | 
296 |       const result = buildDSNFromEnvParams();
297 | 
298 |       // Verify it builds without error and contains encoded characters
299 |       expect(result).toBeTruthy();
300 |       // Note: encodeURIComponent doesn't encode ! so it remains as !
301 |       expect(result?.dsn).toContain('!'); // ! is not encoded
302 |       expect(result?.dsn).toContain('%40'); // @
303 |       expect(result?.dsn).toContain('%23'); // #
304 |       expect(result?.dsn).toContain('%24'); // $
305 |       expect(result?.dsn).toContain('%25'); // %
306 |     });
307 | 
308 |     it('should handle database names with Unicode characters', () => {
309 |       process.env.DB_TYPE = 'postgres';
310 |       process.env.DB_HOST = 'localhost';
311 |       process.env.DB_USER = 'user';
312 |       process.env.DB_PASSWORD = 'pass';
313 |       process.env.DB_NAME = 'тест_база_данных'; // Cyrillic characters
314 | 
315 |       const result = buildDSNFromEnvParams();
316 | 
317 |       expect(result).toBeTruthy();
318 |       expect(result?.dsn).toContain('%D1%82%D0%B5%D1%81%D1%82'); // Encoded Cyrillic
319 |     });
320 | 
321 |     it('should be case insensitive for database type', () => {
322 |       process.env.DB_TYPE = 'POSTGRES';
323 |       process.env.DB_HOST = 'localhost';
324 |       process.env.DB_USER = 'user';
325 |       process.env.DB_PASSWORD = 'pass';
326 |       process.env.DB_NAME = 'db';
327 | 
328 |       const result = buildDSNFromEnvParams();
329 | 
330 |       expect(result?.dsn).toBe('postgres://user:pass@localhost:5432/db');
331 |     });
332 |   });
333 | 
334 |   describe('resolveId', () => {
335 |     it('should return null when ID is not provided', () => {
336 |       const result = resolveId();
337 | 
338 |       expect(result).toBeNull();
339 |     });
340 | 
341 |     it('should resolve ID from environment variable', () => {
342 |       process.env.ID = 'prod';
343 | 
344 |       const result = resolveId();
345 | 
346 |       expect(result).toEqual({
347 |         id: 'prod',
348 |         source: 'environment variable'
349 |       });
350 |     });
351 | 
352 |     it('should handle different ID formats', () => {
353 |       process.env.ID = 'staging-db-01';
354 | 
355 |       const result = resolveId();
356 | 
357 |       expect(result).toEqual({
358 |         id: 'staging-db-01',
359 |         source: 'environment variable'
360 |       });
361 |     });
362 | 
363 |     it('should handle numeric IDs as strings', () => {
364 |       process.env.ID = '123';
365 | 
366 |       const result = resolveId();
367 | 
368 |       expect(result).toEqual({
369 |         id: '123',
370 |         source: 'environment variable'
371 |       });
372 |     });
373 |   });
374 | });
```

--------------------------------------------------------------------------------
/src/__tests__/json-rpc-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2 | import { spawn, ChildProcess } from 'child_process';
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | import os from 'os';
  6 | 
  7 | describe('JSON RPC Integration Tests', () => {
  8 |   let serverProcess: ChildProcess | null = null;
  9 |   let testDbPath: string;
 10 |   let baseUrl: string;
 11 |   const testPort = 3001;
 12 | 
 13 |   beforeAll(async () => {
 14 |     // Create a temporary SQLite database file
 15 |     const tempDir = os.tmpdir();
 16 |     testDbPath = path.join(tempDir, `json_rpc_test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.db`);
 17 |     
 18 |     baseUrl = `http://localhost:${testPort}`;
 19 |     
 20 |     // Start the server as a child process
 21 |     serverProcess = spawn('pnpm', ['dev'], {
 22 |       env: {
 23 |         ...process.env,
 24 |         DSN: `sqlite://${testDbPath}`,
 25 |         TRANSPORT: 'http',
 26 |         PORT: testPort.toString(),
 27 |         NODE_ENV: 'test'
 28 |       },
 29 |       stdio: 'pipe'
 30 |     });
 31 | 
 32 |     // Handle server output
 33 |     serverProcess.stdout?.on('data', (data) => {
 34 |       console.log(`Server stdout: ${data}`);
 35 |     });
 36 | 
 37 |     serverProcess.stderr?.on('data', (data) => {
 38 |       console.error(`Server stderr: ${data}`);
 39 |     });
 40 | 
 41 |     // Wait for server to start up
 42 |     let serverReady = false;
 43 |     for (let i = 0; i < 20; i++) {
 44 |       try {
 45 |         await new Promise(resolve => setTimeout(resolve, 1000));
 46 |         const response = await fetch(`${baseUrl}/message`, {
 47 |           method: 'POST',
 48 |           headers: { 
 49 |             'Content-Type': 'application/json',
 50 |             'Accept': 'application/json, text/event-stream'
 51 |           },
 52 |           body: JSON.stringify({
 53 |             jsonrpc: '2.0',
 54 |             id: 'health-check',
 55 |             method: 'notifications/initialized'
 56 |           })
 57 |         });
 58 |         if (response.status < 500) {
 59 |           serverReady = true;
 60 |           break;
 61 |         }
 62 |       } catch (e) {
 63 |         // Server not ready yet, continue waiting
 64 |       }
 65 |     }
 66 |     
 67 |     if (!serverReady) {
 68 |       throw new Error('Server did not start within expected time');
 69 |     }
 70 |     
 71 |     // Create test tables and data via HTTP request
 72 |     await makeJsonRpcCall('execute_sql', {
 73 |       sql: `
 74 |         CREATE TABLE IF NOT EXISTS users (
 75 |           id INTEGER PRIMARY KEY AUTOINCREMENT,
 76 |           name VARCHAR(100) NOT NULL,
 77 |           email VARCHAR(100) UNIQUE NOT NULL,
 78 |           age INTEGER
 79 |         );
 80 |         
 81 |         CREATE TABLE IF NOT EXISTS orders (
 82 |           id INTEGER PRIMARY KEY AUTOINCREMENT,
 83 |           user_id INTEGER REFERENCES users(id),
 84 |           total DECIMAL(10,2),
 85 |           created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 86 |         );
 87 |         
 88 |         INSERT INTO users (name, email, age) VALUES 
 89 |         ('John Doe', '[email protected]', 30),
 90 |         ('Jane Smith', '[email protected]', 25),
 91 |         ('Bob Johnson', '[email protected]', 35);
 92 |         
 93 |         INSERT INTO orders (user_id, total) VALUES 
 94 |         (1, 99.99),
 95 |         (1, 149.50),
 96 |         (2, 75.25);
 97 |       `
 98 |     });
 99 |   }, 30000);
100 | 
101 |   afterAll(async () => {
102 |     // Kill the server process if it's still running
103 |     if (serverProcess) {
104 |       serverProcess.kill('SIGTERM');
105 |       
106 |       // Wait for process to exit
107 |       await new Promise((resolve) => {
108 |         if (serverProcess) {
109 |           serverProcess.on('exit', resolve);
110 |           setTimeout(() => {
111 |             if (serverProcess && !serverProcess.killed) {
112 |               serverProcess.kill('SIGKILL');
113 |             }
114 |             resolve(void 0);
115 |           }, 5000);
116 |         } else {
117 |           resolve(void 0);
118 |         }
119 |       });
120 |     }
121 |     
122 |     // Clean up the test database file
123 |     if (testDbPath && fs.existsSync(testDbPath)) {
124 |       fs.unlinkSync(testDbPath);
125 |     }
126 |   });
127 | 
128 |   async function makeJsonRpcCall(method: string, params: any): Promise<any> {
129 |     const response = await fetch(`${baseUrl}/message`, {
130 |       method: 'POST',
131 |       headers: {
132 |         'Content-Type': 'application/json',
133 |         'Accept': 'application/json, text/event-stream',
134 |       },
135 |       body: JSON.stringify({
136 |         jsonrpc: '2.0',
137 |         id: Math.random().toString(36).substr(2, 9),
138 |         method: 'tools/call',
139 |         params: {
140 |           name: method,
141 |           arguments: params
142 |         }
143 |       })
144 |     });
145 | 
146 |     if (!response.ok) {
147 |       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
148 |     }
149 | 
150 |     // Handle different response types
151 |     const contentType = response.headers.get('content-type');
152 |     if (contentType?.includes('text/event-stream')) {
153 |       // Handle SSE response
154 |       const text = await response.text();
155 |       const lines = text.split('\n').filter(line => line.startsWith('data: '));
156 |       if (lines.length > 0) {
157 |         return JSON.parse(lines[0].substring(6)); // Remove 'data: ' prefix
158 |       }
159 |       throw new Error('No data in SSE response');
160 |     } else {
161 |       return await response.json();
162 |     }
163 |   }
164 | 
165 |   describe('execute_sql JSON RPC calls', () => {
166 |     it('should execute a simple SELECT query successfully', async () => {
167 |       const response = await makeJsonRpcCall('execute_sql', {
168 |         sql: 'SELECT * FROM users WHERE age > 25 ORDER BY age'
169 |       });
170 |       
171 |       expect(response).toHaveProperty('result');
172 |       expect(response.result).toHaveProperty('content');
173 |       expect(Array.isArray(response.result.content)).toBe(true);
174 |       
175 |       const content = JSON.parse(response.result.content[0].text);
176 |       expect(content.success).toBe(true);
177 |       expect(content.data).toHaveProperty('rows');
178 |       expect(content.data).toHaveProperty('count');
179 |       expect(content.data.rows).toHaveLength(2);
180 |       expect(content.data.rows[0].name).toBe('John Doe');
181 |       expect(content.data.rows[1].name).toBe('Bob Johnson');
182 |     });
183 | 
184 |     it('should execute a JOIN query successfully', async () => {
185 |       const response = await makeJsonRpcCall('execute_sql', {
186 |         sql: `
187 |           SELECT u.name, u.email, o.total 
188 |           FROM users u 
189 |           JOIN orders o ON u.id = o.user_id 
190 |           WHERE u.age >= 30
191 |           ORDER BY o.total DESC
192 |         `
193 |       });
194 |       
195 |       expect(response).toHaveProperty('result');
196 |       const content = JSON.parse(response.result.content[0].text);
197 |       expect(content.success).toBe(true);
198 |       expect(content.data.rows).toHaveLength(2);
199 |       expect(content.data.rows[0].total).toBe(149.50);
200 |       expect(content.data.rows[1].total).toBe(99.99);
201 |     });
202 | 
203 |     it('should execute aggregate queries successfully', async () => {
204 |       const response = await makeJsonRpcCall('execute_sql', {
205 |         sql: `
206 |           SELECT 
207 |             COUNT(*) as user_count,
208 |             AVG(age) as avg_age,
209 |             MIN(age) as min_age,
210 |             MAX(age) as max_age
211 |           FROM users
212 |         `
213 |       });
214 |       
215 |       expect(response).toHaveProperty('result');
216 |       const content = JSON.parse(response.result.content[0].text);
217 |       expect(content.success).toBe(true);
218 |       expect(content.data.rows).toHaveLength(1);
219 |       expect(content.data.rows[0].user_count).toBe(3);
220 |       expect(content.data.rows[0].avg_age).toBe(30);
221 |       expect(content.data.rows[0].min_age).toBe(25);
222 |       expect(content.data.rows[0].max_age).toBe(35);
223 |     });
224 | 
225 |     it('should handle multiple statements in a single call', async () => {
226 |       const response = await makeJsonRpcCall('execute_sql', {
227 |         sql: `
228 |           INSERT INTO users (name, email, age) VALUES ('Test User', '[email protected]', 28);
229 |           SELECT COUNT(*) as total_users FROM users;
230 |         `
231 |       });
232 |       
233 |       expect(response).toHaveProperty('result');
234 |       const content = JSON.parse(response.result.content[0].text);
235 |       expect(content.success).toBe(true);
236 |       expect(content.data.rows).toHaveLength(1);
237 |       expect(content.data.rows[0].total_users).toBe(4);
238 |     });
239 | 
240 |     it('should handle SQLite-specific functions', async () => {
241 |       const response = await makeJsonRpcCall('execute_sql', {
242 |         sql: `
243 |           SELECT 
244 |             sqlite_version() as version,
245 |             datetime('now') as current_time,
246 |             upper('hello world') as uppercase,
247 |             length('test string') as str_length
248 |         `
249 |       });
250 |       
251 |       expect(response).toHaveProperty('result');
252 |       const content = JSON.parse(response.result.content[0].text);
253 |       expect(content.success).toBe(true);
254 |       expect(content.data.rows).toHaveLength(1);
255 |       expect(content.data.rows[0].version).toBeDefined();
256 |       expect(content.data.rows[0].uppercase).toBe('HELLO WORLD');
257 |       expect(content.data.rows[0].str_length).toBe(11);
258 |     });
259 | 
260 |     it('should return error for invalid SQL', async () => {
261 |       const response = await makeJsonRpcCall('execute_sql', {
262 |         sql: 'SELECT * FROM non_existent_table'
263 |       });
264 |       
265 |       expect(response).toHaveProperty('result');
266 |       const content = JSON.parse(response.result.content[0].text);
267 |       expect(content.success).toBe(false);
268 |       expect(content.error).toContain('no such table');
269 |       expect(content.code).toBe('EXECUTION_ERROR');
270 |     });
271 | 
272 |     it('should handle empty result sets', async () => {
273 |       const response = await makeJsonRpcCall('execute_sql', {
274 |         sql: 'SELECT * FROM users WHERE age > 100'
275 |       });
276 |       
277 |       expect(response).toHaveProperty('result');
278 |       const content = JSON.parse(response.result.content[0].text);
279 |       expect(content.success).toBe(true);
280 |       expect(content.data.rows).toHaveLength(0);
281 |       expect(content.data.count).toBe(0);
282 |     });
283 | 
284 |     it('should work with SQLite transactions', async () => {
285 |       const response = await makeJsonRpcCall('execute_sql', {
286 |         sql: `
287 |           BEGIN TRANSACTION;
288 |           INSERT INTO users (name, email, age) VALUES ('Transaction User', '[email protected]', 40);
289 |           COMMIT;
290 |           SELECT * FROM users WHERE email = '[email protected]';
291 |         `
292 |       });
293 |       
294 |       expect(response).toHaveProperty('result');
295 |       const content = JSON.parse(response.result.content[0].text);
296 |       expect(content.success).toBe(true);
297 |       expect(content.data.rows).toHaveLength(1);
298 |       expect(content.data.rows[0].name).toBe('Transaction User');
299 |       expect(content.data.rows[0].age).toBe(40);
300 |     });
301 | 
302 |     it('should handle PRAGMA statements', async () => {
303 |       const response = await makeJsonRpcCall('execute_sql', {
304 |         sql: 'PRAGMA table_info(users)'
305 |       });
306 |       
307 |       expect(response).toHaveProperty('result');
308 |       const content = JSON.parse(response.result.content[0].text);
309 |       expect(content.success).toBe(true);
310 |       expect(content.data.rows.length).toBeGreaterThan(0);
311 |       expect(content.data.rows.some((row: any) => row.name === 'id')).toBe(true);
312 |       expect(content.data.rows.some((row: any) => row.name === 'name')).toBe(true);
313 |     });
314 |   });
315 | 
316 |   describe('JSON RPC protocol compliance', () => {
317 |     it('should return proper JSON RPC response structure', async () => {
318 |       const requestId = Math.random().toString(36).substr(2, 9);
319 |       const response = await makeJsonRpcCall('execute_sql', {
320 |         sql: 'SELECT 1 as test'
321 |       });
322 |       
323 |       expect(response).toHaveProperty('jsonrpc', '2.0');
324 |       expect(response).toHaveProperty('id');
325 |       expect(response).toHaveProperty('result');
326 |       expect(response.result).toHaveProperty('content');
327 |     });
328 | 
329 |     it('should handle malformed requests gracefully', async () => {
330 |       const response = await fetch(`${baseUrl}/message`, {
331 |         method: 'POST',
332 |         headers: {
333 |           'Content-Type': 'application/json',
334 |           'Accept': 'application/json, text/event-stream',
335 |         },
336 |         body: JSON.stringify({
337 |           // Missing required jsonrpc field
338 |           id: 'test',
339 |           method: 'tools/call',
340 |           params: {
341 |             name: 'execute_sql',
342 |             arguments: { sql: 'SELECT 1' }
343 |           }
344 |         })
345 |       });
346 | 
347 |       // The server should still respond, but with an error
348 |       expect(response.status).toBeLessThan(500);
349 |     });
350 |   });
351 | });
```

--------------------------------------------------------------------------------
/src/prompts/db-explainer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { ConnectorManager } from "../connectors/manager.js";
  3 | import {
  4 |   formatPromptSuccessResponse,
  5 |   formatPromptErrorResponse,
  6 | } from "../utils/response-formatter.js";
  7 | 
  8 | // Schema for database explainer prompt
  9 | export const dbExplainerSchema = {
 10 |   schema: z.string().optional().describe("Optional database schema to use"),
 11 |   table: z.string().optional().describe("Optional specific table to explain"),
 12 | };
 13 | 
 14 | /**
 15 |  * Database Explainer Prompt Handler
 16 |  * Provides explanations about database elements
 17 |  */
 18 | export async function dbExplainerPromptHandler(
 19 |   {
 20 |     schema,
 21 |     table,
 22 |   }: {
 23 |     schema?: string;
 24 |     table?: string;
 25 |   },
 26 |   _extra: any
 27 | ): Promise<{
 28 |   messages: {
 29 |     role: "assistant" | "user";
 30 |     content: {
 31 |       type: "text";
 32 |       text: string;
 33 |     };
 34 |   }[];
 35 |   references?: string[];
 36 |   error?: string;
 37 |   code?: string;
 38 |   _meta?: Record<string, unknown>;
 39 |   [key: string]: unknown;
 40 | }> {
 41 |   try {
 42 |     const connector = ConnectorManager.getCurrentConnector();
 43 | 
 44 |     // Verify schema exists if provided
 45 |     if (schema) {
 46 |       const availableSchemas = await connector.getSchemas();
 47 |       if (!availableSchemas.includes(schema)) {
 48 |         return formatPromptErrorResponse(
 49 |           `Schema '${schema}' does not exist or cannot be accessed. Available schemas: ${availableSchemas.join(", ")}`,
 50 |           "SCHEMA_NOT_FOUND"
 51 |         );
 52 |       }
 53 |     }
 54 | 
 55 |     // Get list of available tables in the specified schema
 56 |     const tables = await connector.getTables(schema);
 57 | 
 58 |     // Process the table parameter if provided
 59 |     const normalizedTable = table?.toLowerCase() || "";
 60 | 
 61 |     // Check if table parameter matches a table in the database
 62 |     const matchingTable = tables.find((t) => t.toLowerCase() === normalizedTable);
 63 |     if (matchingTable && table) {
 64 |       try {
 65 |         // Explain the table
 66 |         const columns = await connector.getTableSchema(matchingTable, schema);
 67 | 
 68 |         if (columns.length === 0) {
 69 |           return formatPromptErrorResponse(
 70 |             `Table '${matchingTable}' exists but has no columns or cannot be accessed.`,
 71 |             "EMPTY_TABLE_SCHEMA"
 72 |           );
 73 |         }
 74 | 
 75 |         // Create a table structure description
 76 |         const schemaInfo = schema ? ` in schema '${schema}'` : "";
 77 |         const tableDescription = `Table: ${matchingTable}${schemaInfo}
 78 |       
 79 | Structure:
 80 | ${columns.map((col) => `- ${col.column_name} (${col.data_type})${col.is_nullable === "YES" ? ", nullable" : ""}${col.column_default ? `, default: ${col.column_default}` : ""}`).join("\n")}
 81 | 
 82 | Purpose:
 83 | This table appears to store ${determineTablePurpose(matchingTable, columns)}
 84 | 
 85 | Relationships:
 86 | ${determineRelationships(matchingTable, columns)}`;
 87 | 
 88 |         return formatPromptSuccessResponse(tableDescription);
 89 |       } catch (error) {
 90 |         return formatPromptErrorResponse(
 91 |           `Error retrieving schema for table '${matchingTable}': ${(error as Error).message}`,
 92 |           "TABLE_SCHEMA_ERROR"
 93 |         );
 94 |       }
 95 |     }
 96 | 
 97 |     // Check if table parameter has a table.column format
 98 |     if (table && table.includes(".")) {
 99 |       const [tableName, columnName] = table.split(".");
100 |       const tableExists = tables.find((t) => t.toLowerCase() === tableName.toLowerCase());
101 | 
102 |       if (!tableExists) {
103 |         // Table part of table.column doesn't exist
104 |         return formatPromptErrorResponse(
105 |           `Table '${tableName}' does not exist in schema '${schema || "default"}'. Available tables: ${tables.slice(0, 10).join(", ")}${tables.length > 10 ? "..." : ""}`,
106 |           "TABLE_NOT_FOUND"
107 |         );
108 |       }
109 | 
110 |       try {
111 |         // Get column info
112 |         const columns = await connector.getTableSchema(tableName, schema);
113 |         const column = columns.find(
114 |           (c) => c.column_name.toLowerCase() === columnName.toLowerCase()
115 |         );
116 | 
117 |         if (column) {
118 |           const columnDescription = `Column: ${tableName}.${column.column_name}
119 |           
120 | Type: ${column.data_type}
121 | Nullable: ${column.is_nullable === "YES" ? "Yes" : "No"}
122 | Default: ${column.column_default || "None"}
123 | 
124 | Purpose:
125 | ${determineColumnPurpose(column.column_name, column.data_type)}`;
126 | 
127 |           return formatPromptSuccessResponse(columnDescription);
128 |         } else {
129 |           // Column doesn't exist in the table
130 |           return formatPromptErrorResponse(
131 |             `Column '${columnName}' does not exist in table '${tableName}'. Available columns: ${columns.map((c) => c.column_name).join(", ")}`,
132 |             "COLUMN_NOT_FOUND"
133 |           );
134 |         }
135 |       } catch (error) {
136 |         return formatPromptErrorResponse(
137 |           `Error accessing table schema: ${(error as Error).message}`,
138 |           "SCHEMA_ACCESS_ERROR"
139 |         );
140 |       }
141 |     }
142 | 
143 |     // If no specific table was provided or the table refers to the database itself,
144 |     // provide a database overview
145 |     // This will trigger if:
146 |     // 1. No table parameter was provided
147 |     // 2. The table parameter is a database overview keyword
148 |     if (!table || ["database", "db", "schema", "overview", "all"].includes(normalizedTable)) {
149 |       const schemaInfo = schema ? `in schema '${schema}'` : "across all schemas";
150 | 
151 |       let dbOverview = `Database Overview ${schemaInfo}
152 | 
153 | Tables: ${tables.length}
154 | ${tables.map((t) => `- ${t}`).join("\n")}
155 | 
156 | This database ${describeDatabasePurpose(tables)}`;
157 | 
158 |       return formatPromptSuccessResponse(dbOverview);
159 |     }
160 | 
161 |     // If we get here and table was provided but not found,
162 |     // check for partial matches to suggest alternatives
163 |     if (table && !normalizedTable.includes(".")) {
164 |       // Search for partial matches
165 |       const possibleTableMatches = tables.filter(
166 |         (t) =>
167 |           t.toLowerCase().includes(normalizedTable) || normalizedTable.includes(t.toLowerCase())
168 |       );
169 | 
170 |       if (possibleTableMatches.length > 0) {
171 |         // Found partial matches, suggest them to the user
172 |         return formatPromptSuccessResponse(
173 |           `Table "${table}" not found. Did you mean one of these tables?\n\n${possibleTableMatches.join("\n")}`
174 |         );
175 |       } else {
176 |         // No matches at all, return a clear error with available tables
177 |         const schemaInfo = schema ? `in schema '${schema}'` : "in the database";
178 |         return formatPromptErrorResponse(
179 |           `Table "${table}" does not exist ${schemaInfo}. Available tables: ${tables.slice(0, 10).join(", ")}${tables.length > 10 ? "..." : ""}`,
180 |           "TABLE_NOT_FOUND"
181 |         );
182 |       }
183 |     }
184 |   } catch (error) {
185 |     return formatPromptErrorResponse(
186 |       `Error explaining database: ${(error as Error).message}`,
187 |       "EXPLANATION_ERROR"
188 |     );
189 |   }
190 | 
191 |   // If no condition was met and no other return was triggered
192 |   return formatPromptErrorResponse(
193 |     `Unable to process request for schema: ${schema}, table: ${table}`,
194 |     "UNHANDLED_REQUEST"
195 |   );
196 | }
197 | 
198 | /**
199 |  * Helper function to make an educated guess about the purpose of a table
200 |  * based on its name and columns
201 |  */
202 | function determineTablePurpose(tableName: string, columns: any[]): string {
203 |   const lowerTableName = tableName.toLowerCase();
204 |   const columnNames = columns.map((c) => c.column_name.toLowerCase());
205 | 
206 |   // Check for common patterns
207 |   if (
208 |     lowerTableName.includes("user") ||
209 |     columnNames.includes("username") ||
210 |     columnNames.includes("email")
211 |   ) {
212 |     return "user information and profiles";
213 |   }
214 | 
215 |   if (lowerTableName.includes("order") || lowerTableName.includes("purchase")) {
216 |     return "order or purchase transactions";
217 |   }
218 | 
219 |   if (lowerTableName.includes("product") || lowerTableName.includes("item")) {
220 |     return "product or item information";
221 |   }
222 | 
223 |   if (lowerTableName.includes("log") || columnNames.includes("timestamp")) {
224 |     return "event or activity logs";
225 |   }
226 | 
227 |   if (columnNames.includes("created_at") && columnNames.includes("updated_at")) {
228 |     return "tracking timestamped data records";
229 |   }
230 | 
231 |   // Default
232 |   return "data related to " + tableName;
233 | }
234 | 
235 | /**
236 |  * Helper function to determine potential relationships based on column names
237 |  */
238 | function determineRelationships(tableName: string, columns: any[]): string {
239 |   const potentialRelationships = [];
240 | 
241 |   // Look for _id columns which often indicate foreign keys
242 |   const idColumns = columns.filter(
243 |     (c) =>
244 |       c.column_name.toLowerCase().endsWith("_id") &&
245 |       !c.column_name.toLowerCase().startsWith(tableName.toLowerCase())
246 |   );
247 | 
248 |   if (idColumns.length > 0) {
249 |     idColumns.forEach((col) => {
250 |       const referencedTable = col.column_name.toLowerCase().replace("_id", "");
251 |       potentialRelationships.push(
252 |         `May have a relationship with the "${referencedTable}" table (via ${col.column_name})`
253 |       );
254 |     });
255 |   }
256 | 
257 |   // Check if the table itself might be referenced by others
258 |   if (columns.some((c) => c.column_name.toLowerCase() === "id")) {
259 |     potentialRelationships.push(
260 |       `May be referenced by other tables as "${tableName.toLowerCase()}_id"`
261 |     );
262 |   }
263 | 
264 |   return potentialRelationships.length > 0
265 |     ? potentialRelationships.join("\n")
266 |     : "No obvious relationships identified based on column names";
267 | }
268 | 
269 | /**
270 |  * Helper function to determine the purpose of a column based on naming patterns
271 |  */
272 | function determineColumnPurpose(columnName: string, dataType: string): string {
273 |   const lowerColumnName = columnName.toLowerCase();
274 | 
275 |   if (lowerColumnName === "id") {
276 |     return "Primary identifier for records in this table";
277 |   }
278 | 
279 |   if (lowerColumnName.endsWith("_id")) {
280 |     const referencedTable = lowerColumnName.replace("_id", "");
281 |     return `Foreign key reference to the "${referencedTable}" table`;
282 |   }
283 | 
284 |   if (lowerColumnName.includes("name")) {
285 |     return "Stores name information";
286 |   }
287 | 
288 |   if (lowerColumnName.includes("email")) {
289 |     return "Stores email address information";
290 |   }
291 | 
292 |   if (lowerColumnName.includes("password") || lowerColumnName.includes("hash")) {
293 |     return "Stores security credential information (likely hashed)";
294 |   }
295 | 
296 |   if (lowerColumnName === "created_at" || lowerColumnName === "created_on") {
297 |     return "Timestamp for when the record was created";
298 |   }
299 | 
300 |   if (lowerColumnName === "updated_at" || lowerColumnName === "modified_at") {
301 |     return "Timestamp for when the record was last updated";
302 |   }
303 | 
304 |   if (lowerColumnName.includes("date") || lowerColumnName.includes("time")) {
305 |     return "Stores date or time information";
306 |   }
307 | 
308 |   if (
309 |     lowerColumnName.includes("price") ||
310 |     lowerColumnName.includes("cost") ||
311 |     lowerColumnName.includes("amount")
312 |   ) {
313 |     return "Stores monetary value information";
314 |   }
315 | 
316 |   // Data type specific purposes
317 |   if (dataType.includes("boolean")) {
318 |     return "Stores a true/false flag";
319 |   }
320 | 
321 |   if (dataType.includes("json")) {
322 |     return "Stores structured JSON data";
323 |   }
324 | 
325 |   if (dataType.includes("text") || dataType.includes("varchar") || dataType.includes("char")) {
326 |     return "Stores text information";
327 |   }
328 | 
329 |   // Default
330 |   return `Stores ${dataType} data`;
331 | }
332 | 
333 | /**
334 |  * Helper function to describe the overall database purpose based on tables
335 |  */
336 | function describeDatabasePurpose(tables: string[]): string {
337 |   const tableNames = tables.map((t) => t.toLowerCase());
338 | 
339 |   if (tableNames.some((t) => t.includes("user")) && tableNames.some((t) => t.includes("order"))) {
340 |     return "appears to be an e-commerce or customer order management system";
341 |   }
342 | 
343 |   if (
344 |     tableNames.some((t) => t.includes("patient")) ||
345 |     tableNames.some((t) => t.includes("medical"))
346 |   ) {
347 |     return "appears to be related to healthcare or medical record management";
348 |   }
349 | 
350 |   if (
351 |     tableNames.some((t) => t.includes("student")) ||
352 |     tableNames.some((t) => t.includes("course"))
353 |   ) {
354 |     return "appears to be related to education or student management";
355 |   }
356 | 
357 |   if (
358 |     tableNames.some((t) => t.includes("employee")) ||
359 |     tableNames.some((t) => t.includes("payroll"))
360 |   ) {
361 |     return "appears to be related to HR or employee management";
362 |   }
363 | 
364 |   if (
365 |     tableNames.some((t) => t.includes("inventory")) ||
366 |     tableNames.some((t) => t.includes("stock"))
367 |   ) {
368 |     return "appears to be related to inventory or stock management";
369 |   }
370 | 
371 |   // Default
372 |   return "contains multiple tables that store related information";
373 | }
374 | 
```

--------------------------------------------------------------------------------
/src/connectors/postgres/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import pg from "pg";
  2 | const { Pool } = pg;
  3 | import {
  4 |   Connector,
  5 |   ConnectorType,
  6 |   ConnectorRegistry,
  7 |   DSNParser,
  8 |   SQLResult,
  9 |   TableColumn,
 10 |   TableIndex,
 11 |   StoredProcedure,
 12 |   ExecuteOptions,
 13 | } from "../interface.js";
 14 | import { SafeURL } from "../../utils/safe-url.js";
 15 | import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js";
 16 | import { SQLRowLimiter } from "../../utils/sql-row-limiter.js";
 17 | 
 18 | /**
 19 |  * PostgreSQL DSN Parser
 20 |  * Handles DSN strings like: postgres://user:password@localhost:5432/dbname?sslmode=disable
 21 |  * Supported SSL modes:
 22 |  * - sslmode=disable: No SSL
 23 |  * - sslmode=require: SSL connection without certificate verification
 24 |  * - Any other value: SSL with certificate verification
 25 |  */
 26 | class PostgresDSNParser implements DSNParser {
 27 |   async parse(dsn: string): Promise<pg.PoolConfig> {
 28 |     // Basic validation
 29 |     if (!this.isValidDSN(dsn)) {
 30 |       const obfuscatedDSN = obfuscateDSNPassword(dsn);
 31 |       const expectedFormat = this.getSampleDSN();
 32 |       throw new Error(
 33 |         `Invalid PostgreSQL DSN format.\nProvided: ${obfuscatedDSN}\nExpected: ${expectedFormat}`
 34 |       );
 35 |     }
 36 | 
 37 |     try {
 38 |       // Use the SafeURL helper instead of the built-in URL
 39 |       // This will handle special characters in passwords, etc.
 40 |       const url = new SafeURL(dsn);
 41 | 
 42 |       const config: pg.PoolConfig = {
 43 |         host: url.hostname,
 44 |         port: url.port ? parseInt(url.port) : 5432,
 45 |         database: url.pathname ? url.pathname.substring(1) : '', // Remove leading '/' if exists
 46 |         user: url.username,
 47 |         password: url.password,
 48 |       };
 49 | 
 50 |       // Handle query parameters (like sslmode, etc.)
 51 |       url.forEachSearchParam((value, key) => {
 52 |         if (key === "sslmode") {
 53 |           if (value === "disable") {
 54 |             config.ssl = false;
 55 |           } else if (value === "require") {
 56 |             config.ssl = { rejectUnauthorized: false };
 57 |           } else {
 58 |             config.ssl = true;
 59 |           }
 60 |         }
 61 |         // Add other parameters as needed
 62 |       });
 63 | 
 64 |       return config;
 65 |     } catch (error) {
 66 |       throw new Error(
 67 |         `Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
 68 |       );
 69 |     }
 70 |   }
 71 | 
 72 |   getSampleDSN(): string {
 73 |     return "postgres://postgres:password@localhost:5432/postgres?sslmode=require";
 74 |   }
 75 | 
 76 |   isValidDSN(dsn: string): boolean {
 77 |     try {
 78 |       return dsn.startsWith('postgres://') || dsn.startsWith('postgresql://');
 79 |     } catch (error) {
 80 |       return false;
 81 |     }
 82 |   }
 83 | }
 84 | 
 85 | /**
 86 |  * PostgreSQL Connector Implementation
 87 |  */
 88 | export class PostgresConnector implements Connector {
 89 |   id: ConnectorType = "postgres";
 90 |   name = "PostgreSQL";
 91 |   dsnParser = new PostgresDSNParser();
 92 | 
 93 |   private pool: pg.Pool | null = null;
 94 | 
 95 |   async connect(dsn: string): Promise<void> {
 96 |     try {
 97 |       const config = await this.dsnParser.parse(dsn);
 98 |       this.pool = new Pool(config);
 99 | 
100 |       // Test the connection
101 |       const client = await this.pool.connect();
102 |       console.error("Successfully connected to PostgreSQL database");
103 |       client.release();
104 |     } catch (err) {
105 |       console.error("Failed to connect to PostgreSQL database:", err);
106 |       throw err;
107 |     }
108 |   }
109 | 
110 |   async disconnect(): Promise<void> {
111 |     if (this.pool) {
112 |       await this.pool.end();
113 |       this.pool = null;
114 |     }
115 |   }
116 | 
117 |   async getSchemas(): Promise<string[]> {
118 |     if (!this.pool) {
119 |       throw new Error("Not connected to database");
120 |     }
121 | 
122 |     const client = await this.pool.connect();
123 |     try {
124 |       const result = await client.query(`
125 |         SELECT schema_name
126 |         FROM information_schema.schemata
127 |         WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
128 |         ORDER BY schema_name
129 |       `);
130 | 
131 |       return result.rows.map((row) => row.schema_name);
132 |     } finally {
133 |       client.release();
134 |     }
135 |   }
136 | 
137 |   async getTables(schema?: string): Promise<string[]> {
138 |     if (!this.pool) {
139 |       throw new Error("Not connected to database");
140 |     }
141 | 
142 |     const client = await this.pool.connect();
143 |     try {
144 |       // In PostgreSQL, use 'public' as the default schema if none specified
145 |       // 'public' is the standard default schema in PostgreSQL databases
146 |       const schemaToUse = schema || "public";
147 | 
148 |       const result = await client.query(
149 |         `
150 |         SELECT table_name 
151 |         FROM information_schema.tables 
152 |         WHERE table_schema = $1
153 |         ORDER BY table_name
154 |       `,
155 |         [schemaToUse]
156 |       );
157 | 
158 |       return result.rows.map((row) => row.table_name);
159 |     } finally {
160 |       client.release();
161 |     }
162 |   }
163 | 
164 |   async tableExists(tableName: string, schema?: string): Promise<boolean> {
165 |     if (!this.pool) {
166 |       throw new Error("Not connected to database");
167 |     }
168 | 
169 |     const client = await this.pool.connect();
170 |     try {
171 |       // In PostgreSQL, use 'public' as the default schema if none specified
172 |       const schemaToUse = schema || "public";
173 | 
174 |       const result = await client.query(
175 |         `
176 |         SELECT EXISTS (
177 |           SELECT FROM information_schema.tables 
178 |           WHERE table_schema = $1 
179 |           AND table_name = $2
180 |         )
181 |       `,
182 |         [schemaToUse, tableName]
183 |       );
184 | 
185 |       return result.rows[0].exists;
186 |     } finally {
187 |       client.release();
188 |     }
189 |   }
190 | 
191 |   async getTableIndexes(tableName: string, schema?: string): Promise<TableIndex[]> {
192 |     if (!this.pool) {
193 |       throw new Error("Not connected to database");
194 |     }
195 | 
196 |     const client = await this.pool.connect();
197 |     try {
198 |       // In PostgreSQL, use 'public' as the default schema if none specified
199 |       const schemaToUse = schema || "public";
200 | 
201 |       // Query to get all indexes for the table
202 |       const result = await client.query(
203 |         `
204 |         SELECT 
205 |           i.relname as index_name,
206 |           array_agg(a.attname) as column_names,
207 |           ix.indisunique as is_unique,
208 |           ix.indisprimary as is_primary
209 |         FROM 
210 |           pg_class t,
211 |           pg_class i,
212 |           pg_index ix,
213 |           pg_attribute a,
214 |           pg_namespace ns
215 |         WHERE 
216 |           t.oid = ix.indrelid
217 |           AND i.oid = ix.indexrelid
218 |           AND a.attrelid = t.oid
219 |           AND a.attnum = ANY(ix.indkey)
220 |           AND t.relkind = 'r'
221 |           AND t.relname = $1
222 |           AND ns.oid = t.relnamespace
223 |           AND ns.nspname = $2
224 |         GROUP BY 
225 |           i.relname, 
226 |           ix.indisunique,
227 |           ix.indisprimary
228 |         ORDER BY 
229 |           i.relname
230 |       `,
231 |         [tableName, schemaToUse]
232 |       );
233 | 
234 |       return result.rows.map((row) => ({
235 |         index_name: row.index_name,
236 |         column_names: row.column_names,
237 |         is_unique: row.is_unique,
238 |         is_primary: row.is_primary,
239 |       }));
240 |     } finally {
241 |       client.release();
242 |     }
243 |   }
244 | 
245 |   async getTableSchema(tableName: string, schema?: string): Promise<TableColumn[]> {
246 |     if (!this.pool) {
247 |       throw new Error("Not connected to database");
248 |     }
249 | 
250 |     const client = await this.pool.connect();
251 |     try {
252 |       // In PostgreSQL, use 'public' as the default schema if none specified
253 |       // Tables are created in the 'public' schema by default unless otherwise specified
254 |       const schemaToUse = schema || "public";
255 | 
256 |       // Get table columns
257 |       const result = await client.query(
258 |         `
259 |         SELECT 
260 |           column_name, 
261 |           data_type, 
262 |           is_nullable,
263 |           column_default
264 |         FROM information_schema.columns
265 |         WHERE table_schema = $1
266 |         AND table_name = $2
267 |         ORDER BY ordinal_position
268 |       `,
269 |         [schemaToUse, tableName]
270 |       );
271 | 
272 |       return result.rows;
273 |     } finally {
274 |       client.release();
275 |     }
276 |   }
277 | 
278 |   async getStoredProcedures(schema?: string): Promise<string[]> {
279 |     if (!this.pool) {
280 |       throw new Error("Not connected to database");
281 |     }
282 | 
283 |     const client = await this.pool.connect();
284 |     try {
285 |       // In PostgreSQL, use 'public' as the default schema if none specified
286 |       const schemaToUse = schema || "public";
287 | 
288 |       // Get stored procedures and functions from PostgreSQL
289 |       const result = await client.query(
290 |         `
291 |         SELECT 
292 |           routine_name
293 |         FROM information_schema.routines
294 |         WHERE routine_schema = $1
295 |         ORDER BY routine_name
296 |       `,
297 |         [schemaToUse]
298 |       );
299 | 
300 |       return result.rows.map((row) => row.routine_name);
301 |     } finally {
302 |       client.release();
303 |     }
304 |   }
305 | 
306 |   async getStoredProcedureDetail(procedureName: string, schema?: string): Promise<StoredProcedure> {
307 |     if (!this.pool) {
308 |       throw new Error("Not connected to database");
309 |     }
310 | 
311 |     const client = await this.pool.connect();
312 |     try {
313 |       // In PostgreSQL, use 'public' as the default schema if none specified
314 |       const schemaToUse = schema || "public";
315 | 
316 |       // Get stored procedure details from PostgreSQL
317 |       const result = await client.query(
318 |         `
319 |         SELECT 
320 |           routine_name as procedure_name,
321 |           routine_type,
322 |           CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
323 |           external_language as language,
324 |           data_type as return_type,
325 |           routine_definition as definition,
326 |           (
327 |             SELECT string_agg(
328 |               parameter_name || ' ' || 
329 |               parameter_mode || ' ' || 
330 |               data_type,
331 |               ', '
332 |             )
333 |             FROM information_schema.parameters
334 |             WHERE specific_schema = $1
335 |             AND specific_name = $2
336 |             AND parameter_name IS NOT NULL
337 |           ) as parameter_list
338 |         FROM information_schema.routines
339 |         WHERE routine_schema = $1
340 |         AND routine_name = $2
341 |       `,
342 |         [schemaToUse, procedureName]
343 |       );
344 | 
345 |       if (result.rows.length === 0) {
346 |         throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
347 |       }
348 | 
349 |       const procedure = result.rows[0];
350 | 
351 |       // If routine_definition is NULL, try to get the procedure body with pg_get_functiondef
352 |       let definition = procedure.definition;
353 | 
354 |       try {
355 |         // Get the OID for the procedure/function
356 |         const oidResult = await client.query(
357 |           `
358 |           SELECT p.oid, p.prosrc
359 |           FROM pg_proc p
360 |           JOIN pg_namespace n ON p.pronamespace = n.oid
361 |           WHERE p.proname = $1
362 |           AND n.nspname = $2
363 |         `,
364 |           [procedureName, schemaToUse]
365 |         );
366 | 
367 |         if (oidResult.rows.length > 0) {
368 |           // If definition is still null, get the full definition
369 |           if (!definition) {
370 |             const oid = oidResult.rows[0].oid;
371 |             const defResult = await client.query(`SELECT pg_get_functiondef($1)`, [oid]);
372 |             if (defResult.rows.length > 0) {
373 |               definition = defResult.rows[0].pg_get_functiondef;
374 |             } else {
375 |               // Fall back to prosrc if pg_get_functiondef fails
376 |               definition = oidResult.rows[0].prosrc;
377 |             }
378 |           }
379 |         }
380 |       } catch (err) {
381 |         // Ignore errors trying to get definition - it's optional
382 |         console.error(`Error getting procedure definition: ${err}`);
383 |       }
384 | 
385 |       return {
386 |         procedure_name: procedure.procedure_name,
387 |         procedure_type: procedure.procedure_type,
388 |         language: procedure.language || "sql",
389 |         parameter_list: procedure.parameter_list || "",
390 |         return_type: procedure.return_type !== "void" ? procedure.return_type : undefined,
391 |         definition: definition || undefined,
392 |       };
393 |     } finally {
394 |       client.release();
395 |     }
396 |   }
397 | 
398 | 
399 |   async executeSQL(sql: string, options: ExecuteOptions): Promise<SQLResult> {
400 |     if (!this.pool) {
401 |       throw new Error("Not connected to database");
402 |     }
403 | 
404 |     const client = await this.pool.connect();
405 |     try {
406 |       // Check if this is a multi-statement query
407 |       const statements = sql.split(';')
408 |         .map(statement => statement.trim())
409 |         .filter(statement => statement.length > 0);
410 | 
411 |       if (statements.length === 1) {
412 |         // Single statement - apply maxRows if applicable
413 |         const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
414 |         
415 |         return await client.query(processedStatement);
416 |       } else {
417 |         // Multiple statements - execute all in same session for transaction consistency
418 |         let allRows: any[] = [];
419 |         
420 |         // Execute within a transaction to ensure session consistency
421 |         await client.query('BEGIN');
422 |         try {
423 |           for (let statement of statements) {
424 |             // Apply maxRows limit to SELECT queries if specified
425 |             const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
426 |             
427 |             const result = await client.query(processedStatement);
428 |             // Collect rows from SELECT/WITH/EXPLAIN statements
429 |             if (result.rows && result.rows.length > 0) {
430 |               allRows.push(...result.rows);
431 |             }
432 |           }
433 |           await client.query('COMMIT');
434 |         } catch (error) {
435 |           await client.query('ROLLBACK');
436 |           throw error;
437 |         }
438 | 
439 |         return { rows: allRows };
440 |       }
441 |     } finally {
442 |       client.release();
443 |     }
444 |   }
445 | }
446 | 
447 | // Create and register the connector
448 | const postgresConnector = new PostgresConnector();
449 | ConnectorRegistry.register(postgresConnector);
450 | 
```

--------------------------------------------------------------------------------
/resources/images/logo-icon-only.svg:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="utf-8" ?>
 2 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1726" height="1726">
 3 | 	<path transform="scale(3.37017 3.37017)" d="M254.218 57.5893C271.394 55.6769 272.54 75.2483 275.511 87.1378L295.294 165.462C299.884 183.301 307.629 197.039 328.502 199.183C334.057 199.754 339.715 198.929 345.246 198.378L412.049 191.183C425.378 189.601 438.767 187.757 452.145 186.658C468.778 185.292 477.924 203.888 465.058 215.324C461.545 218.446 456.446 220.607 452.34 222.893L433.42 233.654C409.633 247.433 383.743 265.504 362.153 282.306C335.092 303.364 338.726 310.498 351.273 342.507C360.435 365.88 370.358 388.992 380.434 411.991C385.123 422.693 395.262 438.162 385.686 449.038C372.682 463.808 357.348 444.473 348.315 436.069C330.537 419.527 278.769 366.218 257.218 367.308C243.678 361.753 220.536 385.323 211.706 392.946C192.763 409.301 174.154 426.001 155.998 443.222C152.993 446.072 149.925 449.376 146.372 451.53C135.603 458.059 120.717 450.01 122.345 436.978C122.781 433.48 124.842 429.998 126.198 426.784L143.092 386.939C148.482 374.162 153.896 361.423 159.017 348.53L165.816 331.022C171.383 316.316 173.417 305.043 161.809 291.308C147.414 274.274 77.2887 232.452 57.6713 221.362C50.2817 217.184 41.9927 214.05 40.663 204.506C39.4719 195.956 45.9445 187.566 54.5122 186.467C59.8027 185.789 73.9458 188.172 80.0187 188.887L158.966 197.829C181 200.286 199.279 201.349 212.689 178.85C216.312 172.771 217.358 166.288 218.867 159.511L235.986 86.0643C238.769 74.094 239.159 59.403 254.218 57.5893Z"/>
 4 | 	<path fill="#FD8D31" transform="scale(3.37017 3.37017)" d="M128.679 438.854C129.843 434.72 132.026 430.714 133.69 426.744L151.444 385.252C159.251 366.672 168.271 345.946 174.752 326.903C179.159 313.954 176.797 300.279 168.956 289.184C156.929 272.165 89.5353 231.76 71.2351 221.298C66.6739 218.69 50.3012 210.101 48.5148 206.191C46.3001 201.344 48.6188 194.222 54.3268 193.23C58.3095 192.538 75.2131 195.312 80.2394 195.943L159.41 205.008C183.62 207.554 205.326 206.532 219.076 181.689C223.022 174.56 224.443 166.481 226.263 158.63L230.959 138.031C234.847 120.83 238.661 103.575 242.918 86.464C245.024 78.0039 245.663 59.2654 260.185 65.3291C266.746 69.5161 266.869 80.971 268.774 88.3972L287.296 162.02C289.244 169.794 290.747 177.7 294.78 184.737C306.318 204.874 324.863 207.248 345.284 205.09L435.232 195.685C440.299 195.173 453.658 192.907 457.726 193.917C463.485 195.348 465.613 202.997 462.601 207.725C460.243 211.426 444.677 219.051 440.304 221.589C422.184 232.106 353.665 274.442 341.87 291.207C328.523 310.18 338.108 327.74 345.485 346.332C354.151 368.169 363.222 389.841 372.623 411.367L379.697 427.504C382.7 434.386 387.272 445.297 374.948 447.426C369.533 448.362 362.597 439.929 359.11 436.705L327.814 407.663C312.391 393.306 296.527 378.648 278.702 367.308C263.115 359.278 248.666 355.809 233.635 366.23C222.889 373.68 210.985 384.81 200.988 393.604C187.077 405.84 173.076 417.862 159.555 430.535C152.284 437.349 135.402 458.902 128.679 438.854Z"/>
 5 | 	<path fill="#F55E24" transform="scale(3.37017 3.37017)" d="M128.679 438.854C129.843 434.72 132.026 430.714 133.69 426.744L151.444 385.252C159.251 366.672 168.271 345.946 174.752 326.903C179.159 313.954 176.797 300.279 168.956 289.184C156.929 272.165 89.5353 231.76 71.2351 221.298C66.6739 218.69 50.3012 210.101 48.5148 206.191C46.3001 201.344 48.6188 194.222 54.3268 193.23C58.3095 192.538 75.2131 195.312 80.2394 195.943L159.41 205.008C183.62 207.554 205.326 206.532 219.076 181.689C223.022 174.56 224.443 166.481 226.263 158.63L230.959 138.031C234.847 120.83 238.661 103.575 242.918 86.464C245.024 78.0039 245.663 59.2654 260.185 65.3291C258.694 72.6523 255.427 87.0523 254.961 94.1515C245.625 97.2213 244.814 106.899 251.781 112.618C250.683 115.05 247.36 126.823 248.272 128.937C242.859 133.294 241.835 138.071 243.938 144.466C242.729 146.635 237.711 172.197 236.832 176.25C233.107 193.426 230.169 208.706 211.605 215.254C195.421 220.963 165.798 214.52 148.819 211.572C145.716 211.033 122.203 205.926 120.744 208.802C126.902 212.518 128.4 220.072 123.81 225.802C124.799 227.092 132.843 230.927 134.603 230.638L134.837 230.302C137.019 241.771 147.78 245.323 156.716 237.444C158.114 238.968 171.257 246.089 172.958 245.471C176.478 251.699 178.359 250.829 183.447 252.937C184.27 255.93 189.006 258.655 191.123 260.923C203.085 273.735 207.503 291.18 200.797 307.642C195.6 320.403 185.921 331.387 179.91 343.939C178.465 346.957 166.545 374.201 167.808 376.509C165.229 379.996 164.088 383.07 164.911 387.349C162.952 389.167 161.488 396.264 159.058 399.323L159.069 399.641L160.386 399.904C164.72 400.733 167.29 402.169 169.163 406.133C172.706 403.402 177.469 397.673 181.141 394.343C184.207 391.561 187.74 389.291 190.902 386.602C211.079 369.446 233.151 344.441 262.273 356.373C266.72 358.194 276.837 362.856 278.702 367.308C263.115 359.278 248.666 355.809 233.635 366.23C222.889 373.68 210.985 384.81 200.988 393.604C187.077 405.84 173.076 417.862 159.555 430.535C152.284 437.349 135.402 458.902 128.679 438.854Z"/>
 6 | 	<path transform="scale(3.37017 3.37017)" d="M123.81 225.802C120.192 228.905 115.27 230.658 110.623 228.739C97.3721 223.266 106.039 201.28 120.744 208.802C126.902 212.518 128.4 220.072 123.81 225.802Z"/>
 7 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M113.582 211.793C122.878 211.04 124.738 221.388 116.656 224.681C107.385 225.491 105.746 215.161 113.582 211.793Z"/>
 8 | 	<path transform="scale(3.37017 3.37017)" d="M160.386 399.904C164.72 400.733 167.29 402.169 169.163 406.133C170.345 415.432 162.49 424.4 154.345 418.198C147.229 412.78 150.881 400.54 160.386 399.904Z"/>
 9 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M160.386 404.507C167.432 404.743 165.93 413.303 160.84 415.456C153.817 416.64 154.106 406.554 160.386 404.507Z"/>
10 | 	<path transform="scale(3.37017 3.37017)" d="M81.2319 200.032C94.4824 199.513 97.401 215.81 84.8788 219.423C71.6412 222.251 68.8434 203.193 81.2319 200.032Z"/>
11 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M82.0675 204.52C89.5931 204.287 90.3433 212.812 84.0014 215.317C76.4218 215.821 75.7216 206.921 82.0675 204.52Z"/>
12 | 	<path transform="scale(3.37017 3.37017)" d="M172.958 245.471C168.569 231.986 180.189 222.032 191.606 228.114C204.617 235.045 197.944 253.887 183.447 252.937C178.359 250.829 176.478 251.699 172.958 245.471Z"/>
13 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M183.447 231.229C195.032 229.114 197.396 245.746 186.605 248.485C174.966 249.156 172.2 233.935 183.447 231.229Z"/>
14 | 	<path transform="scale(3.37017 3.37017)" d="M134.837 230.302C134.436 207.522 170.976 216.896 156.716 237.444C147.78 245.323 137.019 241.771 134.837 230.302Z"/>
15 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M146.741 221.289C157.687 221.279 158.046 234.484 148.062 237.444C137.68 237.077 136.363 223.803 146.741 221.289Z"/>
16 | 	<path transform="scale(3.37017 3.37017)" d="M248.272 128.937C259.864 122.951 271.645 132.32 267.345 143.497C263.287 154.044 249.351 153.017 243.938 144.466C241.835 138.071 242.859 133.294 248.272 128.937Z"/>
17 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M253.602 131.553C264.29 130.46 267.592 143.105 257.218 146.617C247.185 147.799 243.64 134.972 253.602 131.553Z"/>
18 | 	<path transform="scale(3.37017 3.37017)" d="M167.808 376.509C186.465 361.929 192.265 398.337 172.403 394.166C168.692 393.386 166.436 390.594 164.911 387.349C164.088 383.07 165.229 379.996 167.808 376.509Z"/>
19 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M175.458 378.289C182.179 378.368 183.539 387.689 175.458 390.334C167.325 390.219 166.529 380.489 175.458 378.289Z"/>
20 | 	<path transform="scale(3.37017 3.37017)" d="M254.961 94.1515C274.48 90.3718 270.386 122.153 251.781 112.618C244.814 106.899 245.625 97.2213 254.961 94.1515Z"/>
21 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M257.218 98.5842C261.322 98.8268 262.358 100.553 263.379 104.431C262.444 107.456 261.392 109.009 258.073 109.733C250.274 109.883 249.68 99.8028 257.218 98.5842Z"/>
22 | 	<path transform="scale(3.37017 3.37017)" d="M220.337 242.087C234.571 236.695 246.057 264.337 224.515 270.263C202.975 271.058 202.904 243.408 220.337 242.087Z"/>
23 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M220.337 246.783C232.189 243.105 237.056 261.195 224.515 265.519C210.695 267.904 207.419 248.962 220.337 246.783Z"/>
24 | 	<path transform="scale(3.37017 3.37017)" d="M254.218 208.023C256.404 206.512 260.392 207.787 262.506 208.89C274.603 215.202 271.686 231.897 258.073 235.831C238.43 238.855 234.863 207.989 254.218 208.023Z"/>
25 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M254.218 211.793C267.272 211.256 269.682 227.847 257.218 231.229C243.991 232.795 241.667 214.917 254.218 211.793Z"/>
26 | 	<path transform="scale(3.37017 3.37017)" d="M286.716 242.087C304.499 240.139 308.829 266.307 290.29 270.263C269.843 271.053 267.795 244.847 286.716 242.087Z"/>
27 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M286.716 246.783C298.801 245.49 302.05 261.574 290.29 265.519C277.21 267.497 273.392 250.215 286.716 246.783Z"/>
28 | 	<path transform="scale(3.37017 3.37017)" d="M229.475 285.32C251.081 287.455 248.211 310.226 233.006 313.199C215.396 314.413 209.73 291.958 229.475 285.32Z"/>
29 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M229.475 290.551C241.836 289.75 244.195 304.57 233.006 308.536C220.418 310.273 217.025 293.587 229.475 290.551Z"/>
30 | 	<path transform="scale(3.37017 3.37017)" d="M278.217 285.32C295.473 284.095 299.853 307.798 282.178 312.186C263.316 314.789 259.234 288.092 278.217 285.32Z"/>
31 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M278.217 289.793C289.964 289.013 292.951 303.076 282.178 307.605C268.937 308.925 265.976 293.469 278.217 289.793Z"/>
32 | 	<path transform="scale(3.37017 3.37017)" d="M325.556 230.302C327.808 229.278 331.813 231.475 333.529 232.844C343.52 240.808 337.282 255.342 325.556 256.34C306.916 256.153 306.956 230.547 325.556 230.302Z"/>
33 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M323.846 234.788C335.462 233.63 336.894 248.528 326.818 251.688C315.296 253.664 311.989 238.071 323.846 234.788Z"/>
34 | 	<path transform="scale(3.37017 3.37017)" d="M254.218 163.805C272.189 162.481 275.373 184.857 258.943 189.299C241.66 191.811 236.859 168.488 254.218 163.805Z"/>
35 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M254.961 168.287C266.149 167.698 268.61 181.915 258.073 184.807C246.411 185.755 243.759 171.423 254.961 168.287Z"/>
36 | 	<path transform="scale(3.37017 3.37017)" d="M208.3 319.345C224.317 320.298 224.324 342.747 208.3 343.829C192.019 343.756 189.857 320.834 208.3 319.345Z"/>
37 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M206.072 323.566C216.489 322.726 220.013 335.518 209.319 339.369C198.414 340.457 196.156 327.32 206.072 323.566Z"/>
38 | 	<path transform="scale(3.37017 3.37017)" d="M302.592 319.345C318.341 318.177 321.79 340.072 305.194 343.829C288.526 343.866 286.041 321.806 302.592 319.345Z"/>
39 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M303.383 323.566C313.6 324.03 314.762 336.316 305.194 339.369C293.925 339.039 292.925 325.568 303.383 323.566Z"/>
40 | 	<path transform="scale(3.37017 3.37017)" d="M254.218 260.832C269.91 259.811 273.26 280.946 257.218 285.32C240.632 286.406 237.547 263.035 254.218 260.832Z"/>
41 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M254.961 265.519C264.049 264.222 266.296 277.959 256.648 280.756C245.961 280.906 244.025 267.38 254.961 265.519Z"/>
42 | 	<path transform="scale(3.37017 3.37017)" d="M353.69 221.289C367.23 218.813 374.012 240.198 356.1 245.471C338.48 244.15 338.66 222.674 353.69 221.289Z"/>
43 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M353.69 225.802C363.729 224.935 365.661 238.305 356.1 240.434C346.144 241.246 343.812 229.245 353.69 225.802Z"/>
44 | 	<path transform="scale(3.37017 3.37017)" d="M320.007 346.795C335.795 345.639 338.265 366.031 323.846 369.465C309.207 371.849 304.931 350.352 320.007 346.795Z"/>
45 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M320.901 351.553C330.747 351.303 331.471 362.536 322.894 365.121C313.635 365.623 312.185 353.699 320.901 351.553Z"/>
46 | 	<path transform="scale(3.37017 3.37017)" d="M189.426 345.81C202.095 344.264 207.386 363.619 192.228 368.086C177.656 370.311 172.849 348.486 189.426 345.81Z"/>
47 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M189.426 350.31C197.807 350.382 199.953 360.231 192.228 363.718C182.294 365.326 180.35 352.439 189.426 350.31Z"/>
48 | 	<path transform="scale(3.37017 3.37017)" d="M381.943 213.792C397.351 212.902 400.265 230.436 386.409 235.831C371.33 236.875 367.895 218.105 381.943 213.792Z"/>
49 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M383.387 218.059C392.487 218.77 392.912 228.856 384.802 231.229C376.13 231.467 375.25 219.935 383.387 218.059Z"/>
50 | 	<path transform="scale(3.37017 3.37017)" d="M335.987 375.31C349.382 374.117 351.073 391.56 338.494 394.505C325.002 395.546 322.662 377.936 335.987 375.31Z"/>
51 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M335.987 379.776C343.115 378.749 344.033 387.391 338.494 390.334C331.336 390.898 328.983 382.608 335.987 379.776Z"/>
52 | 	<path transform="scale(3.37017 3.37017)" d="M409.503 208.802C421.599 207.954 424.356 223.488 412.779 227.01C400.21 228.762 397.155 211.746 409.503 208.802Z"/>
53 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M410.369 213.792C416.604 213.292 417.502 220.572 411.682 222.692C405.388 222.866 403.965 214.971 410.369 213.792Z"/>
54 | 	<path transform="scale(3.37017 3.37017)" d="M351.509 400.782C363.61 399.999 366.545 415.826 354.825 418.946C341.645 419.89 339.897 403.753 351.509 400.782Z"/>
55 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M352.292 405.258C358.624 405.033 359.119 412.574 353.69 414.528C347.125 414.8 346.894 407.664 352.292 405.258Z"/>
56 | 	<path transform="scale(3.37017 3.37017)" d="M432.748 203.591C443.007 201.734 447.103 215.722 435.865 219.423C425.245 219.591 421.212 205.704 432.748 203.591Z"/>
57 | 	<path fill="#7BBCDE" transform="scale(3.37017 3.37017)" d="M432.748 208.023C437.64 207.603 439.89 211.851 435.865 215.317C431.352 214.911 428.02 210.741 432.748 208.023Z"/>
58 | </svg>
59 | 
```

--------------------------------------------------------------------------------
/src/connectors/sqlite/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * SQLite Connector Implementation
  3 |  *
  4 |  * Implements SQLite database connectivity for DBHub using better-sqlite3
  5 |  * To use this connector: Set DSN=sqlite:///path/to/database.db in your .env file
  6 |  */
  7 | 
  8 | import {
  9 |   Connector,
 10 |   ConnectorType,
 11 |   ConnectorRegistry,
 12 |   DSNParser,
 13 |   SQLResult,
 14 |   TableColumn,
 15 |   TableIndex,
 16 |   StoredProcedure,
 17 |   ExecuteOptions,
 18 | } from "../interface.js";
 19 | import Database from "better-sqlite3";
 20 | import { SafeURL } from "../../utils/safe-url.js";
 21 | import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js";
 22 | import { SQLRowLimiter } from "../../utils/sql-row-limiter.js";
 23 | 
 24 | /**
 25 |  * SQLite DSN Parser
 26 |  * Handles DSN strings like:
 27 |  * - sqlite:///path/to/database.db (absolute path)
 28 |  * - sqlite://./relative/path/to/database.db (relative path)
 29 |  * - sqlite:///:memory: (in-memory database)
 30 |  */
 31 | class SQLiteDSNParser implements DSNParser {
 32 |   async parse(dsn: string): Promise<{ dbPath: string }> {
 33 |     // Basic validation
 34 |     if (!this.isValidDSN(dsn)) {
 35 |       const obfuscatedDSN = obfuscateDSNPassword(dsn);
 36 |       const expectedFormat = this.getSampleDSN();
 37 |       throw new Error(
 38 |         `Invalid SQLite DSN format.\nProvided: ${obfuscatedDSN}\nExpected: ${expectedFormat}`
 39 |       );
 40 |     }
 41 | 
 42 |     try {
 43 |       // Use SafeURL helper to handle special characters properly
 44 |       const url = new SafeURL(dsn);
 45 |       let dbPath: string;
 46 | 
 47 |       // Handle in-memory database
 48 |       if (url.hostname === "" && url.pathname === "/:memory:") {
 49 |         dbPath = ":memory:";
 50 |       }
 51 |       // Handle file paths
 52 |       else {
 53 |         // Get the path part, handling both relative and absolute paths
 54 |         if (url.pathname.startsWith("//")) {
 55 |           // Absolute path: sqlite:///path/to/db.sqlite
 56 |           dbPath = url.pathname.substring(2); // Remove leading //
 57 |         } else {
 58 |           // Relative path: sqlite://./path/to/db.sqlite
 59 |           dbPath = url.pathname;
 60 |         }
 61 |       }
 62 | 
 63 |       return { dbPath };
 64 |     } catch (error) {
 65 |       throw new Error(
 66 |         `Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
 67 |       );
 68 |     }
 69 |   }
 70 | 
 71 |   getSampleDSN(): string {
 72 |     return "sqlite:///path/to/database.db";
 73 |   }
 74 | 
 75 |   isValidDSN(dsn: string): boolean {
 76 |     try {
 77 |       return dsn.startsWith('sqlite://');
 78 |     } catch (error) {
 79 |       return false;
 80 |     }
 81 |   }
 82 | }
 83 | 
 84 | interface SQLiteTableInfo {
 85 |   name: string;
 86 |   type: string;
 87 |   notnull: number;
 88 |   dflt_value: string | null;
 89 |   pk: number;
 90 | }
 91 | 
 92 | interface SQLiteTableNameRow {
 93 |   name: string;
 94 | }
 95 | 
 96 | export class SQLiteConnector implements Connector {
 97 |   id: ConnectorType = "sqlite";
 98 |   name = "SQLite";
 99 |   dsnParser = new SQLiteDSNParser();
100 | 
101 |   private db: Database.Database | null = null;
102 |   private dbPath: string = ":memory:"; // Default to in-memory database
103 | 
104 |   async connect(dsn: string, initScript?: string): Promise<void> {
105 |     const config = await this.dsnParser.parse(dsn);
106 |     this.dbPath = config.dbPath;
107 | 
108 |     try {
109 |       this.db = new Database(this.dbPath);
110 |       console.error("Successfully connected to SQLite database");
111 | 
112 |       // If an initialization script is provided, run it
113 |       if (initScript) {
114 |         this.db.exec(initScript);
115 |         console.error("Successfully initialized database with script");
116 |       }
117 |     } catch (error) {
118 |       console.error("Failed to connect to SQLite database:", error);
119 |       throw error;
120 |     }
121 |   }
122 | 
123 |   async disconnect(): Promise<void> {
124 |     if (this.db) {
125 |       try {
126 |         // Check if the database is still open before attempting to close
127 |         if (!this.db.inTransaction) {
128 |           this.db.close();
129 |         } else {
130 |           // If in transaction, try to rollback first
131 |           try {
132 |             this.db.exec('ROLLBACK');
133 |           } catch (rollbackError) {
134 |             // Ignore rollback errors, proceed with close
135 |           }
136 |           this.db.close();
137 |         }
138 |         this.db = null;
139 |       } catch (error) {
140 |         // Log the error but don't throw to prevent test failures
141 |         console.error('Error during SQLite disconnect:', error);
142 |         this.db = null;
143 |       }
144 |     }
145 |     return Promise.resolve();
146 |   }
147 | 
148 |   async getSchemas(): Promise<string[]> {
149 |     if (!this.db) {
150 |       throw new Error("Not connected to SQLite database");
151 |     }
152 | 
153 |     // SQLite doesn't have the concept of schemas like PostgreSQL or MySQL
154 |     // It has a concept of "attached databases" where each database has a name
155 |     // The default database is called 'main', and others can be attached with names
156 |     // We always return 'main' as the default schema name
157 |     return ["main"];
158 |   }
159 | 
160 |   async getTables(schema?: string): Promise<string[]> {
161 |     if (!this.db) {
162 |       throw new Error("Not connected to SQLite database");
163 |     }
164 | 
165 |     // In SQLite, schema parameter is ignored since SQLite doesn't have schemas like PostgreSQL
166 |     // SQLite has a single namespace for tables within a database file
167 |     // You could use 'schema.table' syntax if you have attached databases, but we're
168 |     // accessing the 'main' database by default
169 |     try {
170 |       const rows = this.db
171 |         .prepare(
172 |           `
173 |         SELECT name FROM sqlite_master 
174 |         WHERE type='table' AND name NOT LIKE 'sqlite_%'
175 |         ORDER BY name
176 |       `
177 |         )
178 |         .all() as SQLiteTableNameRow[];
179 | 
180 |       return rows.map((row) => row.name);
181 |     } catch (error) {
182 |       throw error;
183 |     }
184 |   }
185 | 
186 |   async tableExists(tableName: string, schema?: string): Promise<boolean> {
187 |     if (!this.db) {
188 |       throw new Error("Not connected to SQLite database");
189 |     }
190 | 
191 |     // In SQLite, schema parameter is ignored since there's only one schema per database file
192 |     // All tables exist in a single namespace within the SQLite database
193 |     try {
194 |       const row = this.db
195 |         .prepare(
196 |           `
197 |         SELECT name FROM sqlite_master 
198 |         WHERE type='table' AND name = ?
199 |       `
200 |         )
201 |         .get(tableName) as SQLiteTableNameRow | undefined;
202 | 
203 |       return !!row;
204 |     } catch (error) {
205 |       throw error;
206 |     }
207 |   }
208 | 
209 |   async getTableIndexes(tableName: string, schema?: string): Promise<TableIndex[]> {
210 |     if (!this.db) {
211 |       throw new Error("Not connected to SQLite database");
212 |     }
213 | 
214 |     // In SQLite, schema parameter is ignored (no schema concept)
215 |     try {
216 |       // Get all indexes for the specified table
217 |       const indexInfoRows = this.db
218 |         .prepare(
219 |           `
220 |         SELECT 
221 |           name as index_name,
222 |           0 as is_unique
223 |         FROM sqlite_master 
224 |         WHERE type = 'index' 
225 |         AND tbl_name = ?
226 |       `
227 |         )
228 |         .all(tableName) as { index_name: string; is_unique: number }[];
229 | 
230 |       // Get unique info from PRAGMA index_list which provides the unique flag
231 |       const indexListRows = this.db
232 |         .prepare(`PRAGMA index_list(${tableName})`)
233 |         .all() as { name: string; unique: number }[];
234 |       
235 |       // Create a map of index names to unique status
236 |       const indexUniqueMap = new Map<string, boolean>();
237 |       for (const indexListRow of indexListRows) {
238 |         indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
239 |       }
240 | 
241 |       // Get the primary key info
242 |       const tableInfo = this.db
243 |         .prepare(`PRAGMA table_info(${tableName})`)
244 |         .all() as SQLiteTableInfo[];
245 | 
246 |       // Find primary key columns
247 |       const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
248 | 
249 |       const results: TableIndex[] = [];
250 | 
251 |       // Add regular indexes
252 |       for (const indexInfo of indexInfoRows) {
253 |         // Get the columns for this index
254 |         const indexDetailRows = this.db
255 |           .prepare(`PRAGMA index_info(${indexInfo.index_name})`)
256 |           .all() as {
257 |           name: string;
258 |         }[];
259 |         const columnNames = indexDetailRows.map((row) => row.name);
260 | 
261 |         results.push({
262 |           index_name: indexInfo.index_name,
263 |           column_names: columnNames,
264 |           is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
265 |           is_primary: false,
266 |         });
267 |       }
268 | 
269 |       // Add primary key if it exists
270 |       if (pkColumns.length > 0) {
271 |         results.push({
272 |           index_name: "PRIMARY",
273 |           column_names: pkColumns,
274 |           is_unique: true,
275 |           is_primary: true,
276 |         });
277 |       }
278 | 
279 |       return results;
280 |     } catch (error) {
281 |       throw error;
282 |     }
283 |   }
284 | 
285 |   async getTableSchema(tableName: string, schema?: string): Promise<TableColumn[]> {
286 |     if (!this.db) {
287 |       throw new Error("Not connected to SQLite database");
288 |     }
289 | 
290 |     // In SQLite, schema parameter is ignored for the following reasons:
291 |     // 1. SQLite doesn't have schemas in the same way as PostgreSQL or MySQL
292 |     // 2. Each SQLite database file is its own separate namespace
293 |     // 3. The PRAGMA commands operate on the current database connection
294 |     try {
295 |       const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all() as SQLiteTableInfo[];
296 | 
297 |       // Convert SQLite schema format to our standard TableColumn format
298 |       const columns = rows.map((row) => ({
299 |         column_name: row.name,
300 |         data_type: row.type,
301 |         // In SQLite, primary key columns are automatically NOT NULL even if notnull=0
302 |         is_nullable: (row.notnull === 1 || row.pk > 0) ? "NO" : "YES",
303 |         column_default: row.dflt_value,
304 |       }));
305 | 
306 |       return columns;
307 |     } catch (error) {
308 |       throw error;
309 |     }
310 |   }
311 | 
312 |   async getStoredProcedures(schema?: string): Promise<string[]> {
313 |     if (!this.db) {
314 |       throw new Error("Not connected to SQLite database");
315 |     }
316 | 
317 |     // SQLite doesn't have built-in stored procedures like other databases.
318 |     // While SQLite does support user-defined functions, these are registered through
319 |     // the C/C++ API or language bindings and cannot be introspected through SQL.
320 |     // Triggers exist in SQLite but they're not the same as stored procedures.
321 |     //
322 |     // We return an empty array because:
323 |     // 1. SQLite has no native stored procedure concept
324 |     // 2. User-defined functions cannot be listed via SQL queries
325 |     // 3. We don't want to misrepresent triggers as stored procedures
326 | 
327 |     return [];
328 |   }
329 | 
330 |   async getStoredProcedureDetail(procedureName: string, schema?: string): Promise<StoredProcedure> {
331 |     if (!this.db) {
332 |       throw new Error("Not connected to SQLite database");
333 |     }
334 | 
335 |     // SQLite doesn't have true stored procedures:
336 |     // 1. SQLite doesn't support the CREATE PROCEDURE syntax
337 |     // 2. User-defined functions are created programmatically, not stored in the DB
338 |     // 3. Cannot introspect program-defined functions through SQL
339 | 
340 |     // Throw an error since SQLite doesn't support stored procedures
341 |     throw new Error(
342 |       "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
343 |     );
344 |   }
345 | 
346 | 
347 |   async executeSQL(sql: string, options: ExecuteOptions): Promise<SQLResult> {
348 |     if (!this.db) {
349 |       throw new Error("Not connected to SQLite database");
350 |     }
351 | 
352 |     try {
353 |       // Check if this is a multi-statement query
354 |       const statements = sql.split(';')
355 |         .map(statement => statement.trim())
356 |         .filter(statement => statement.length > 0);
357 | 
358 |       if (statements.length === 1) {
359 |         // Single statement - determine if it returns data
360 |         let processedStatement = statements[0];
361 |         const trimmedStatement = statements[0].toLowerCase().trim();
362 |         const isReadStatement = trimmedStatement.startsWith('select') || 
363 |                                trimmedStatement.startsWith('with') || 
364 |                                trimmedStatement.startsWith('explain') ||
365 |                                trimmedStatement.startsWith('analyze') ||
366 |                                (trimmedStatement.startsWith('pragma') && 
367 |                                 (trimmedStatement.includes('table_info') || 
368 |                                  trimmedStatement.includes('index_info') ||
369 |                                  trimmedStatement.includes('index_list') ||
370 |                                  trimmedStatement.includes('foreign_key_list')));
371 | 
372 |         // Apply maxRows limit to SELECT queries if specified (not PRAGMA/ANALYZE)
373 |         if (options.maxRows) {
374 |           processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
375 |         }
376 | 
377 |         if (isReadStatement) {
378 |           const rows = this.db.prepare(processedStatement).all();
379 |           return { rows };
380 |         } else {
381 |           // Use run() for statements that don't return data
382 |           this.db.prepare(processedStatement).run();
383 |           return { rows: [] };
384 |         }
385 |       } else {
386 |         // Multiple statements - use native .exec() for optimal performance
387 |         // Note: .exec() doesn't return results, so we need to handle SELECT statements differently
388 |         const readStatements = [];
389 |         const writeStatements = [];
390 |         
391 |         // Separate read and write operations
392 |         for (const statement of statements) {
393 |           const trimmedStatement = statement.toLowerCase().trim();
394 |           if (trimmedStatement.startsWith('select') || 
395 |               trimmedStatement.startsWith('with') || 
396 |               trimmedStatement.startsWith('explain') ||
397 |               trimmedStatement.startsWith('analyze') ||
398 |               (trimmedStatement.startsWith('pragma') && 
399 |                (trimmedStatement.includes('table_info') || 
400 |                 trimmedStatement.includes('index_info') ||
401 |                 trimmedStatement.includes('index_list') ||
402 |                 trimmedStatement.includes('foreign_key_list')))) {
403 |             readStatements.push(statement);
404 |           } else {
405 |             writeStatements.push(statement);
406 |           }
407 |         }
408 | 
409 |         // Execute write statements using native .exec() for optimal performance
410 |         if (writeStatements.length > 0) {
411 |           this.db.exec(writeStatements.join('; '));
412 |         }
413 | 
414 |         // Execute read statements individually to collect results
415 |         let allRows: any[] = [];
416 |         for (let statement of readStatements) {
417 |           // Apply maxRows limit to SELECT queries if specified
418 |           statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
419 |           const result = this.db.prepare(statement).all();
420 |           allRows.push(...result);
421 |         }
422 | 
423 |         return { rows: allRows };
424 |       }
425 |     } catch (error) {
426 |       throw error;
427 |     }
428 |   }
429 | }
430 | 
431 | // Register the SQLite connector
432 | const sqliteConnector = new SQLiteConnector();
433 | ConnectorRegistry.register(sqliteConnector);
434 | 
```

--------------------------------------------------------------------------------
/src/connectors/__tests__/mysql.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2 | import { MySqlContainer, StartedMySqlContainer } from '@testcontainers/mysql';
  3 | import { MySQLConnector } from '../mysql/index.js';
  4 | import { IntegrationTestBase, type TestContainer, type DatabaseTestConfig } from './shared/integration-test-base.js';
  5 | import type { Connector } from '../interface.js';
  6 | 
  7 | class MySQLTestContainer implements TestContainer {
  8 |   constructor(private container: StartedMySqlContainer) {}
  9 |   
 10 |   getConnectionUri(): string {
 11 |     return this.container.getConnectionUri();
 12 |   }
 13 |   
 14 |   async stop(): Promise<void> {
 15 |     await this.container.stop();
 16 |   }
 17 | }
 18 | 
 19 | class MySQLIntegrationTest extends IntegrationTestBase<MySQLTestContainer> {
 20 |   constructor() {
 21 |     const config: DatabaseTestConfig = {
 22 |       expectedSchemas: ['testdb', 'information_schema'],
 23 |       expectedTables: ['users', 'orders', 'products'],
 24 |       supportsStoredProcedures: false // Disabled due to container privilege restrictions
 25 |     };
 26 |     super(config);
 27 |   }
 28 | 
 29 |   async createContainer(): Promise<MySQLTestContainer> {
 30 |     const container = await new MySqlContainer('mysql:8.0')
 31 |       .withDatabase('testdb')
 32 |       .withRootPassword('rootpass')
 33 |       .start();
 34 |     
 35 |     return new MySQLTestContainer(container);
 36 |   }
 37 | 
 38 |   createConnector(): Connector {
 39 |     return new MySQLConnector();
 40 |   }
 41 | 
 42 |   createSSLTests(): void {
 43 |     describe('SSL Connection Tests', () => {
 44 |       it('should handle SSL mode disable connection', async () => {
 45 |         const baseUri = this.connectionString;
 46 |         const sslDisabledUri = baseUri.includes('?') ? 
 47 |           `${baseUri}&sslmode=disable` : 
 48 |           `${baseUri}?sslmode=disable`;
 49 |         
 50 |         const sslDisabledConnector = new MySQLConnector();
 51 |         
 52 |         // Should connect successfully with sslmode=disable
 53 |         await expect(sslDisabledConnector.connect(sslDisabledUri)).resolves.not.toThrow();
 54 |         
 55 |         // Check SSL status - cipher should be empty when SSL is disabled
 56 |         const result = await sslDisabledConnector.executeSQL("SHOW SESSION STATUS LIKE 'Ssl_cipher'", {});
 57 |         expect(result.rows).toHaveLength(1);
 58 |         expect(result.rows[0].Variable_name).toBe('Ssl_cipher');
 59 |         expect(result.rows[0].Value).toBe('');
 60 |         
 61 |         await sslDisabledConnector.disconnect();
 62 |       });
 63 | 
 64 |       it('should handle SSL mode require connection', async () => {
 65 |         const baseUri = this.connectionString;
 66 |         const sslRequiredUri = baseUri.includes('?') ? 
 67 |           `${baseUri}&sslmode=require` : 
 68 |           `${baseUri}?sslmode=require`;
 69 |         
 70 |         const sslRequiredConnector = new MySQLConnector();
 71 |         
 72 |         // In test containers, SSL may not be supported, so we expect either success or SSL not supported error
 73 |         try {
 74 |           await sslRequiredConnector.connect(sslRequiredUri);
 75 |           
 76 |           // If connection succeeds, check SSL status - cipher should be non-empty when SSL is enabled
 77 |           const result = await sslRequiredConnector.executeSQL("SHOW SESSION STATUS LIKE 'Ssl_cipher'", {});
 78 |           expect(result.rows).toHaveLength(1);
 79 |           expect(result.rows[0].Variable_name).toBe('Ssl_cipher');
 80 |           expect(result.rows[0].Value).not.toBe('');
 81 |           expect(result.rows[0].Value).toBeTruthy();
 82 |           
 83 |           await sslRequiredConnector.disconnect();
 84 |         } catch (error) {
 85 |           // If SSL is not supported by the test container, that's expected
 86 |           expect(error instanceof Error).toBe(true);
 87 |           expect((error as Error).message).toMatch(/SSL|does not support SSL/);
 88 |         }
 89 |       });
 90 |     });
 91 |   }
 92 | 
 93 |   async setupTestData(connector: Connector): Promise<void> {
 94 |     // Create users table
 95 |     await connector.executeSQL(`
 96 |       CREATE TABLE IF NOT EXISTS users (
 97 |         id INT AUTO_INCREMENT PRIMARY KEY,
 98 |         name VARCHAR(100) NOT NULL,
 99 |         email VARCHAR(100) UNIQUE NOT NULL,
100 |         age INT
101 |       )
102 |     `, {});
103 | 
104 |     // Create orders table
105 |     await connector.executeSQL(`
106 |       CREATE TABLE IF NOT EXISTS orders (
107 |         id INT AUTO_INCREMENT PRIMARY KEY,
108 |         user_id INT,
109 |         total DECIMAL(10,2),
110 |         created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
111 |         FOREIGN KEY (user_id) REFERENCES users(id)
112 |       )
113 |     `, {});
114 | 
115 |     // Create products table in main database
116 |     await connector.executeSQL(`
117 |       CREATE TABLE IF NOT EXISTS products (
118 |         id INT AUTO_INCREMENT PRIMARY KEY,
119 |         name VARCHAR(100) NOT NULL,
120 |         price DECIMAL(10,2)
121 |       )
122 |     `, {});
123 | 
124 |     // Insert test data
125 |     await connector.executeSQL(`
126 |       INSERT IGNORE INTO users (name, email, age) VALUES 
127 |       ('John Doe', '[email protected]', 30),
128 |       ('Jane Smith', '[email protected]', 25),
129 |       ('Bob Johnson', '[email protected]', 35)
130 |     `, {});
131 | 
132 |     await connector.executeSQL(`
133 |       INSERT IGNORE INTO orders (user_id, total) VALUES 
134 |       (1, 99.99),
135 |       (1, 149.50),
136 |       (2, 75.25)
137 |     `, {});
138 | 
139 |     await connector.executeSQL(`
140 |       INSERT IGNORE INTO products (name, price) VALUES 
141 |       ('Widget A', 19.99),
142 |       ('Widget B', 29.99)
143 |     `, {});
144 | 
145 |     // Note: Stored procedures/functions are skipped in tests due to container privilege restrictions
146 |   }
147 | }
148 | 
149 | // Create the test suite
150 | const mysqlTest = new MySQLIntegrationTest();
151 | 
152 | describe('MySQL Connector Integration Tests', () => {
153 |   beforeAll(async () => {
154 |     await mysqlTest.setup();
155 |   }, 120000);
156 | 
157 |   afterAll(async () => {
158 |     await mysqlTest.cleanup();
159 |   });
160 | 
161 |   // Include all common tests
162 |   mysqlTest.createConnectionTests();
163 |   mysqlTest.createSchemaTests();
164 |   mysqlTest.createTableTests();
165 |   mysqlTest.createSQLExecutionTests();
166 |   if (mysqlTest.config.supportsStoredProcedures) {
167 |     mysqlTest.createStoredProcedureTests();
168 |   }
169 |   mysqlTest.createErrorHandlingTests();
170 |   mysqlTest.createSSLTests();
171 | 
172 |   describe('MySQL-specific Features', () => {
173 |     it('should execute multiple statements with native support', async () => {
174 |       // First insert the test data
175 |       await mysqlTest.connector.executeSQL(`
176 |         INSERT INTO users (name, email, age) VALUES ('Multi User 1', '[email protected]', 30);
177 |         INSERT INTO users (name, email, age) VALUES ('Multi User 2', '[email protected]', 35);
178 |       `, {});
179 |       
180 |       // Then check the count
181 |       const result = await mysqlTest.connector.executeSQL(
182 |         "SELECT COUNT(*) as total FROM users WHERE email LIKE 'multi%'",
183 |         {}
184 |       );
185 |       
186 |       expect(result.rows).toHaveLength(1);
187 |       expect(Number(result.rows[0].total)).toBe(2);
188 |     });
189 | 
190 |     it('should handle MySQL-specific data types', async () => {
191 |       await mysqlTest.connector.executeSQL(`
192 |         CREATE TABLE IF NOT EXISTS mysql_types_test (
193 |           id INT AUTO_INCREMENT PRIMARY KEY,
194 |           json_data JSON,
195 |           timestamp_val TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
196 |           enum_val ENUM('small', 'medium', 'large') DEFAULT 'medium'
197 |         )
198 |       `, {});
199 | 
200 |       await mysqlTest.connector.executeSQL(`
201 |         INSERT INTO mysql_types_test (json_data, enum_val) 
202 |         VALUES ('{"key": "value"}', 'large')
203 |       `, {});
204 | 
205 |       const result = await mysqlTest.connector.executeSQL(
206 |         'SELECT * FROM mysql_types_test WHERE id = LAST_INSERT_ID()',
207 |         {}
208 |       );
209 |       
210 |       expect(result.rows).toHaveLength(1);
211 |       expect(result.rows[0].enum_val).toBe('large');
212 |       expect(result.rows[0].json_data).toBeDefined();
213 |     });
214 | 
215 |     it('should handle MySQL auto-increment properly', async () => {
216 |       const insertResult = await mysqlTest.connector.executeSQL(
217 |         "INSERT INTO users (name, email, age) VALUES ('Auto Inc Test', '[email protected]', 40)",
218 |         {}
219 |       );
220 |       
221 |       expect(insertResult).toBeDefined();
222 |       
223 |       const selectResult = await mysqlTest.connector.executeSQL(
224 |         'SELECT LAST_INSERT_ID() as last_id',
225 |         {}
226 |       );
227 |       
228 |       expect(selectResult.rows).toHaveLength(1);
229 |       expect(Number(selectResult.rows[0].last_id)).toBeGreaterThan(0);
230 |     });
231 | 
232 |     it('should work with MySQL-specific functions', async () => {
233 |       const result = await mysqlTest.connector.executeSQL(`
234 |         SELECT 
235 |           VERSION() as mysql_version,
236 |           DATABASE() as current_db,
237 |           USER() as current_user_info,
238 |           NOW() as timestamp_val
239 |       `, {});
240 |       
241 |       expect(result.rows).toHaveLength(1);
242 |       expect(result.rows[0].mysql_version).toBeDefined();
243 |       expect(result.rows[0].current_db).toBe('testdb');
244 |       expect(result.rows[0].current_user_info).toBeDefined();
245 |       expect(result.rows[0].timestamp_val).toBeDefined();
246 |     });
247 | 
248 |     it('should handle MySQL transactions correctly', async () => {
249 |       // Test explicit transaction
250 |       await mysqlTest.connector.executeSQL(`
251 |         START TRANSACTION;
252 |         INSERT INTO users (name, email, age) VALUES ('Transaction Test 1', '[email protected]', 45);
253 |         INSERT INTO users (name, email, age) VALUES ('Transaction Test 2', '[email protected]', 50);
254 |         COMMIT;
255 |       `, {});
256 |       
257 |       const result = await mysqlTest.connector.executeSQL(
258 |         "SELECT COUNT(*) as count FROM users WHERE email LIKE 'trans%@example.com'",
259 |         {}
260 |       );
261 |       expect(Number(result.rows[0].count)).toBe(2);
262 |     });
263 | 
264 |     it('should handle MySQL rollback correctly', async () => {
265 |       // Get initial count
266 |       const beforeResult = await mysqlTest.connector.executeSQL(
267 |         "SELECT COUNT(*) as count FROM users WHERE email = '[email protected]'",
268 |         {}
269 |       );
270 |       const beforeCount = Number(beforeResult.rows[0].count);
271 |       
272 |       // Test rollback
273 |       await mysqlTest.connector.executeSQL(`
274 |         START TRANSACTION;
275 |         INSERT INTO users (name, email, age) VALUES ('Rollback Test', '[email protected]', 55);
276 |         ROLLBACK;
277 |       `, {});
278 |       
279 |       const afterResult = await mysqlTest.connector.executeSQL(
280 |         "SELECT COUNT(*) as count FROM users WHERE email = '[email protected]'",
281 |         {}
282 |       );
283 |       const afterCount = Number(afterResult.rows[0].count);
284 |       
285 |       expect(afterCount).toBe(beforeCount);
286 |     });
287 | 
288 |     it('should respect maxRows limit for SELECT queries', async () => {
289 |       // Test basic SELECT with maxRows limit
290 |       const result = await mysqlTest.connector.executeSQL(
291 |         'SELECT * FROM users ORDER BY id',
292 |         { maxRows: 2 }
293 |       );
294 |       
295 |       expect(result.rows).toHaveLength(2);
296 |       expect(result.rows[0]).toHaveProperty('name');
297 |       expect(result.rows[1]).toHaveProperty('name');
298 |     });
299 | 
300 |     it('should respect existing LIMIT clause when lower than maxRows', async () => {
301 |       // Test when existing LIMIT is lower than maxRows
302 |       const result = await mysqlTest.connector.executeSQL(
303 |         'SELECT * FROM users ORDER BY id LIMIT 1',
304 |         { maxRows: 3 }
305 |       );
306 |       
307 |       expect(result.rows).toHaveLength(1);
308 |       expect(result.rows[0]).toHaveProperty('name');
309 |     });
310 | 
311 |     it('should use maxRows when existing LIMIT is higher', async () => {
312 |       // Test when existing LIMIT is higher than maxRows
313 |       const result = await mysqlTest.connector.executeSQL(
314 |         'SELECT * FROM users ORDER BY id LIMIT 10',
315 |         { maxRows: 2 }
316 |       );
317 |       
318 |       expect(result.rows).toHaveLength(2);
319 |       expect(result.rows[0]).toHaveProperty('name');
320 |       expect(result.rows[1]).toHaveProperty('name');
321 |     });
322 | 
323 |     it('should not affect non-SELECT queries', async () => {
324 |       // Test that maxRows doesn't affect INSERT/UPDATE/DELETE
325 |       const insertResult = await mysqlTest.connector.executeSQL(
326 |         "INSERT INTO users (name, email, age) VALUES ('MaxRows Test', '[email protected]', 25)",
327 |         { maxRows: 1 }
328 |       );
329 |       
330 |       expect(insertResult.rows).toHaveLength(0); // INSERTs don't return rows by default
331 |       
332 |       // Verify the insert worked
333 |       const selectResult = await mysqlTest.connector.executeSQL(
334 |         "SELECT * FROM users WHERE email = '[email protected]'",
335 |         {}
336 |       );
337 |       expect(selectResult.rows).toHaveLength(1);
338 |       expect(selectResult.rows[0].name).toBe('MaxRows Test');
339 |     });
340 | 
341 |     it('should handle maxRows with complex queries', async () => {
342 |       // Test maxRows with JOIN queries
343 |       const result = await mysqlTest.connector.executeSQL(`
344 |         SELECT u.name, o.total 
345 |         FROM users u 
346 |         JOIN orders o ON u.id = o.user_id 
347 |         ORDER BY o.total DESC
348 |       `, { maxRows: 2 });
349 |       
350 |       expect(result.rows.length).toBeLessThanOrEqual(2);
351 |       expect(result.rows.length).toBeGreaterThan(0);
352 |       expect(result.rows[0]).toHaveProperty('name');
353 |       expect(result.rows[0]).toHaveProperty('total');
354 |     });
355 | 
356 |     it('should not apply maxRows to CTE queries (WITH clause)', async () => {
357 |       // Test that maxRows is not applied to CTE queries (WITH clause)
358 |       try {
359 |         const result = await mysqlTest.connector.executeSQL(`
360 |           WITH user_summary AS (
361 |             SELECT name, age FROM users WHERE age IS NOT NULL
362 |           )
363 |           SELECT * FROM user_summary ORDER BY age
364 |         `, { maxRows: 2 });
365 |         
366 |         // Should return all rows since WITH queries are not limited
367 |         expect(result.rows.length).toBeGreaterThan(2);
368 |         expect(result.rows[0]).toHaveProperty('name');
369 |         expect(result.rows[0]).toHaveProperty('age');
370 |       } catch (error) {
371 |         // Some MySQL versions might not support CTE, that's okay
372 |         console.log('CTE not supported in this MySQL version, skipping test');
373 |       }
374 |     });
375 | 
376 |     it('should handle maxRows with multiple SELECT statements', async () => {
377 |       // Test maxRows with multiple SELECT statements only  
378 |       const result = await mysqlTest.connector.executeSQL(`
379 |         SELECT name FROM users WHERE age > 20 ORDER BY name LIMIT 10;
380 |         SELECT name FROM users WHERE age > 25 ORDER BY name LIMIT 10;
381 |       `, { maxRows: 1 });
382 |       
383 |       // Should return only 1 row from each SELECT statement (due to maxRows limit)
384 |       // MySQL multi-statement may return more complex results, so we check that maxRows was applied
385 |       expect(result.rows.length).toBeGreaterThan(0);
386 |       expect(result.rows.length).toBeLessThanOrEqual(2); // At most 1 from each SELECT
387 |       if (result.rows.length > 0) {
388 |         expect(result.rows[0]).toHaveProperty('name');
389 |       }
390 |     });
391 | 
392 |     it('should ignore maxRows when not specified', async () => {
393 |       // Test without maxRows - should return all rows
394 |       const result = await mysqlTest.connector.executeSQL(
395 |         'SELECT * FROM users ORDER BY id',
396 |         {}
397 |       );
398 |       
399 |       // Should return all users (at least the original 3 plus any added in previous tests)
400 |       expect(result.rows.length).toBeGreaterThanOrEqual(3);
401 |     });
402 |   });
403 | });
```
Page 2/8FirstPrevNextLast