|
1 | 1 | import { requireBackendSession } from "@/lib/require-backend-session"; |
2 | 2 | import { NextResponse } from "next/server"; |
3 | | -import { Pool as PgPool } from "pg"; |
4 | | -import mysql from "mysql2/promise"; |
| 3 | +import { getSqlPool, releaseSqlPool, SqlDbType } from "@/lib/sql-client-pool"; |
| 4 | +import { splitSqlStatements } from "@/lib/sql-split"; |
5 | 5 |
|
6 | 6 | const MAX_ROWS = 5000; |
7 | | -const TIMEOUT_MS = 30000; |
8 | 7 |
|
9 | | -export async function POST(request: Request) { |
10 | | - const authError = await requireBackendSession(request); |
11 | | - if (authError) return authError; |
12 | | - |
13 | | - try { |
14 | | - const { type, host, port, database, username, password, ssl, query, limit } = |
15 | | - await request.json(); |
16 | | - |
17 | | - if (!type || !host || !database || !query) { |
18 | | - return NextResponse.json( |
19 | | - { error: "type, host, database, and query are required" }, |
20 | | - { status: 400 } |
21 | | - ); |
22 | | - } |
23 | | - |
24 | | - const rowLimit = Math.min(Number(limit) || 500, MAX_ROWS); |
25 | | - const start = Date.now(); |
26 | | - |
27 | | - if (type === "postgresql") { |
28 | | - const pool = new PgPool({ |
29 | | - host, |
30 | | - port: port || 5432, |
31 | | - database, |
32 | | - user: username, |
33 | | - password, |
34 | | - ssl: ssl ? { rejectUnauthorized: false } : false, |
35 | | - connectionTimeoutMillis: TIMEOUT_MS, |
36 | | - statement_timeout: TIMEOUT_MS, |
37 | | - }); |
38 | | - |
39 | | - const client = await pool.connect(); |
40 | | - try { |
41 | | - // Split statements by semicolon and execute each, returning last result |
42 | | - const statements = query |
43 | | - .split(";") |
44 | | - .map((s: string) => s.trim()) |
45 | | - .filter(Boolean); |
46 | | - |
47 | | - let result = null; |
48 | | - for (const stmt of statements) { |
49 | | - result = await client.query(stmt); |
50 | | - } |
51 | | - |
52 | | - const elapsed = Date.now() - start; |
53 | | - const rows = result?.rows?.slice(0, rowLimit) ?? []; |
54 | | - const columns = result?.fields?.map((f: { name: string }) => f.name) ?? []; |
55 | | - const rowCount = result?.rowCount ?? rows.length; |
56 | | - |
57 | | - return NextResponse.json({ rows, columns, rowCount, executionTime: elapsed }); |
58 | | - } finally { |
59 | | - client.release(); |
60 | | - await pool.end(); |
61 | | - } |
62 | | - } |
63 | | - |
64 | | - if (type === "mysql" || type === "mariadb") { |
65 | | - const conn = await mysql.createConnection({ |
66 | | - host, |
67 | | - port: port || 3306, |
68 | | - database, |
69 | | - user: username, |
70 | | - password, |
71 | | - ssl: ssl ? { rejectUnauthorized: false } : undefined, |
72 | | - connectTimeout: TIMEOUT_MS, |
73 | | - multipleStatements: true, |
74 | | - }); |
75 | | - |
76 | | - try { |
77 | | - const [rawRows, rawFields] = await conn.execute(query); |
78 | | - const elapsed = Date.now() - start; |
| 8 | +interface StatementResult { |
| 9 | + rows: Record<string, unknown>[]; |
| 10 | + columns: string[]; |
| 11 | + rowCount: number; |
| 12 | +} |
79 | 13 |
|
80 | | - // multipleStatements may return arrays of result sets |
81 | | - const isMulti = Array.isArray(rawRows) && Array.isArray(rawRows[0]); |
82 | | - const rows = isMulti |
83 | | - ? (((rawRows as unknown) as unknown[][]).at(-1) as Record<string, unknown>[]) ?? [] |
84 | | - : (rawRows as Record<string, unknown>[]); |
| 14 | +export async function POST(request: Request) { |
| 15 | + const authError = await requireBackendSession(request); |
| 16 | + if (authError) return authError; |
| 17 | + |
| 18 | + try { |
| 19 | + const { type, host, port, database, username, password, ssl, query, limit } = |
| 20 | + await request.json(); |
| 21 | + |
| 22 | + if (!type || !host || !database || !query) { |
| 23 | + return NextResponse.json( |
| 24 | + { error: "type, host, database, and query are required" }, |
| 25 | + { status: 400 } |
| 26 | + ); |
| 27 | + } |
85 | 28 |
|
86 | | - const fields = isMulti |
87 | | - ? (((rawFields as unknown) as unknown[][]).at(-1) as { name: string }[]) ?? [] |
88 | | - : (rawFields as { name: string }[]); |
| 29 | + const rowLimit = Math.min(Number(limit) || 500, MAX_ROWS); |
| 30 | + const start = Date.now(); |
| 31 | + const statements = splitSqlStatements(query); |
| 32 | + if (statements.length === 0) { |
| 33 | + return NextResponse.json({ error: "No SQL statement provided" }, { status: 400 }); |
| 34 | + } |
89 | 35 |
|
90 | | - const slicedRows = Array.isArray(rows) ? rows.slice(0, rowLimit) : []; |
91 | | - const columns = Array.isArray(fields) ? fields.map((f) => f.name) : []; |
| 36 | + const handle = await getSqlPool({ |
| 37 | + type: type as SqlDbType, |
| 38 | + host, |
| 39 | + port: Number(port) || 0, |
| 40 | + database, |
| 41 | + username, |
| 42 | + password, |
| 43 | + ssl: Boolean(ssl), |
| 44 | + }); |
92 | 45 |
|
93 | | - return NextResponse.json({ |
94 | | - rows: slicedRows, |
95 | | - columns, |
96 | | - rowCount: Array.isArray(rows) ? rows.length : 0, |
97 | | - executionTime: elapsed, |
98 | | - }); |
99 | | - } finally { |
100 | | - await conn.end(); |
101 | | - } |
| 46 | + try { |
| 47 | + const results: StatementResult[] = []; |
| 48 | + |
| 49 | + for (const stmt of statements) { |
| 50 | + // ponytail: cooperative abort between statements only. True mid-statement |
| 51 | + // cancel needs pg_cancel_backend / conn.destroy(); add if long queries need killing. |
| 52 | + if (request.signal.aborted) throw new Error("Query aborted"); |
| 53 | + |
| 54 | + if (handle.pg) { |
| 55 | + const r = await handle.pg.query(stmt); |
| 56 | + const rows = (r.rows ?? []) as Record<string, unknown>[]; |
| 57 | + results.push({ |
| 58 | + rows: rows.slice(0, rowLimit), |
| 59 | + columns: r.fields?.map((f: { name: string }) => f.name) ?? [], |
| 60 | + rowCount: r.rowCount ?? rows.length, |
| 61 | + }); |
| 62 | + } else if (handle.mysql) { |
| 63 | + const [rawRows, rawFields] = await handle.mysql.query(stmt); |
| 64 | + const rows = Array.isArray(rawRows) ? (rawRows as Record<string, unknown>[]) : []; |
| 65 | + const fields = Array.isArray(rawFields) ? (rawFields as { name: string }[]) : []; |
| 66 | + results.push({ |
| 67 | + rows: rows.slice(0, rowLimit), |
| 68 | + columns: fields.map((f) => f.name), |
| 69 | + rowCount: rows.length, |
| 70 | + }); |
102 | 71 | } |
103 | | - |
104 | | - return NextResponse.json({ error: `Unsupported database type: ${type}` }, { status: 400 }); |
105 | | - } catch (error: unknown) { |
106 | | - const message = error instanceof Error ? error.message : String(error); |
107 | | - return NextResponse.json({ error: message }, { status: 500 }); |
| 72 | + } |
| 73 | + |
| 74 | + const last = results[results.length - 1] ?? { rows: [], columns: [], rowCount: 0 }; |
| 75 | + return NextResponse.json({ |
| 76 | + rows: last.rows, |
| 77 | + columns: last.columns, |
| 78 | + rowCount: last.rowCount, |
| 79 | + executionTime: Date.now() - start, |
| 80 | + results, |
| 81 | + }); |
| 82 | + } finally { |
| 83 | + releaseSqlPool(handle.key); |
108 | 84 | } |
| 85 | + } catch (error: unknown) { |
| 86 | + const message = error instanceof Error ? error.message : String(error); |
| 87 | + return NextResponse.json({ error: message }, { status: 500 }); |
| 88 | + } |
109 | 89 | } |
0 commit comments