Skip to content

Commit 0be34ef

Browse files
prmoore77claude
andcommitted
fix: skip pagination wrapping for DDL and function calls
DDL statements (CREATE, ALTER, DROP, etc.) and function calls (CALL) were being incorrectly wrapped with pagination SQL, causing syntax errors. Now only SELECT, WITH, TABLE, and VALUES statements are paginated. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d57cf06 commit 0be34ef

File tree

1 file changed

+47
-16
lines changed

1 file changed

+47
-16
lines changed

src/backend/src/routes/api.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ apiRouter.post('/disconnect', async (req: Request, res: Response) => {
7979
}
8080
});
8181

82+
// Check if SQL statement can be paginated (only SELECT-like queries)
83+
function canPaginate(sql: string): boolean {
84+
// Normalize: trim whitespace and remove leading comments
85+
let normalized = sql.trim();
86+
87+
// Remove block comments /* ... */
88+
normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, '');
89+
// Remove line comments -- ...
90+
normalized = normalized.replace(/--[^\n]*/g, '');
91+
// Trim again after removing comments
92+
normalized = normalized.trim();
93+
94+
// Get the first keyword (case-insensitive)
95+
const firstWord = normalized.split(/\s+/)[0]?.toUpperCase() || '';
96+
97+
// Only SELECT, WITH (CTE), TABLE, and VALUES can be paginated
98+
// Note: EXPLAIN could return rows but wrapping it would change semantics
99+
const paginatableKeywords = ['SELECT', 'WITH', 'TABLE', 'VALUES'];
100+
101+
return paginatableKeywords.includes(firstWord);
102+
}
103+
82104
// Execute SQL query with optional pagination
83105
apiRouter.post('/query', async (req: Request, res: Response) => {
84106
try {
@@ -100,26 +122,35 @@ apiRouter.post('/query', async (req: Request, res: Response) => {
100122
return;
101123
}
102124

103-
// Apply pagination by wrapping the query
104-
const pageLimit = typeof limit === 'number' ? limit : 1000; // Default page size
105-
const pageOffset = typeof offset === 'number' ? offset : 0;
125+
// Only apply pagination to SELECT-like queries
126+
if (canPaginate(sql)) {
127+
const pageLimit = typeof limit === 'number' ? limit : 1000; // Default page size
128+
const pageOffset = typeof offset === 'number' ? offset : 0;
106129

107-
// Request one extra row to detect if there are more results
108-
const paginatedSql = `SELECT * FROM (${sql.replace(/;+\s*$/, '')}) AS __paginated_query LIMIT ${pageLimit + 1} OFFSET ${pageOffset}`;
130+
// Request one extra row to detect if there are more results
131+
const paginatedSql = `SELECT * FROM (${sql.replace(/;+\s*$/, '')}) AS __paginated_query LIMIT ${pageLimit + 1} OFFSET ${pageOffset}`;
109132

110-
const result = await service.execute(paginatedSql);
133+
const result = await service.execute(paginatedSql);
111134

112-
// Check if there are more results
113-
const hasMore = result.rows.length > pageLimit;
114-
if (hasMore) {
115-
result.rows = result.rows.slice(0, pageLimit);
116-
result.rowCount = pageLimit;
117-
}
135+
// Check if there are more results
136+
const hasMore = result.rows.length > pageLimit;
137+
if (hasMore) {
138+
result.rows = result.rows.slice(0, pageLimit);
139+
result.rowCount = pageLimit;
140+
}
118141

119-
res.json({
120-
...result,
121-
hasMore,
122-
});
142+
res.json({
143+
...result,
144+
hasMore,
145+
});
146+
} else {
147+
// DDL, DML, and other non-SELECT statements: execute directly without pagination
148+
const result = await service.execute(sql);
149+
res.json({
150+
...result,
151+
hasMore: false,
152+
});
153+
}
123154
} catch (error) {
124155
const message = error instanceof Error ? error.message : 'Query execution failed';
125156
res.status(500).json({ error: message });

0 commit comments

Comments
 (0)