Skip to content

Commit 7a4c84e

Browse files
authored
Merge pull request #234 from itsmeakhil/akhil/sql-client-phase0
Akhil/sql client phase0
2 parents 820da5c + 550d4fd commit 7a4c84e

7 files changed

Lines changed: 483 additions & 203 deletions

File tree

apps/web/src/app/api/sql-client/connect/route.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { requireBackendSession } from "@/lib/require-backend-session";
22
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";
54

65
export async function POST(request: Request) {
76
const authError = await requireBackendSession(request);
@@ -14,37 +13,29 @@ export async function POST(request: Request) {
1413
return NextResponse.json({ error: "type, host, and database are required" }, { status: 400 });
1514
}
1615

17-
if (type === "postgresql") {
18-
const pool = new PgPool({
16+
if (type === "postgresql" || type === "mysql" || type === "mariadb") {
17+
const handle = await getSqlPool({
18+
type: type as SqlDbType,
1919
host,
20-
port: port || 5432,
20+
port: Number(port) || 0,
2121
database,
22-
user: username,
22+
username,
2323
password,
24-
ssl: ssl ? { rejectUnauthorized: false } : false,
25-
connectionTimeoutMillis: 10000,
24+
ssl: Boolean(ssl),
2625
});
27-
const client = await pool.connect();
28-
const res = await client.query("SELECT version()");
29-
client.release();
30-
await pool.end();
31-
return NextResponse.json({ success: true, version: res.rows[0]?.version });
32-
}
3326

34-
if (type === "mysql" || type === "mariadb") {
35-
const conn = await mysql.createConnection({
36-
host,
37-
port: port || 3306,
38-
database,
39-
user: username,
40-
password,
41-
ssl: ssl ? { rejectUnauthorized: false } : undefined,
42-
connectTimeout: 10000,
43-
});
44-
const [rows] = await conn.execute("SELECT VERSION() as version");
45-
await conn.end();
46-
const version = (rows as { version: string }[])[0]?.version;
47-
return NextResponse.json({ success: true, version });
27+
try {
28+
if (handle.pg) {
29+
const res = await handle.pg.query("SELECT version()");
30+
return NextResponse.json({ success: true, version: res.rows[0]?.version });
31+
}
32+
33+
const [rows] = await handle.mysql!.query("SELECT VERSION() as version");
34+
const version = (rows as { version: string }[])[0]?.version;
35+
return NextResponse.json({ success: true, version });
36+
} finally {
37+
releaseSqlPool(handle.key);
38+
}
4839
}
4940

5041
return NextResponse.json({ error: `Unsupported database type: ${type}` }, { status: 400 });
Lines changed: 77 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,89 @@
11
import { requireBackendSession } from "@/lib/require-backend-session";
22
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";
55

66
const MAX_ROWS = 5000;
7-
const TIMEOUT_MS = 30000;
87

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+
}
7913

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+
}
8528

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+
}
8935

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+
});
9245

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+
});
10271
}
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);
10884
}
85+
} catch (error: unknown) {
86+
const message = error instanceof Error ? error.message : String(error);
87+
return NextResponse.json({ error: message }, { status: 500 });
88+
}
10989
}

0 commit comments

Comments
 (0)