mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 00:22:30 +00:00
feat(query): add paged result fetching
This commit is contained in:
@@ -71,6 +71,7 @@ export const executeBatch = forward("executeBatch");
|
||||
export const executeScript = forward("executeScript");
|
||||
export const executeInTransaction = forward("executeInTransaction");
|
||||
export const cancelQuery = forward("cancelQuery");
|
||||
export const closeQuerySession = forward("closeQuerySession");
|
||||
|
||||
// AI
|
||||
export const aiComplete = forward("aiComplete");
|
||||
|
||||
+8
-2
@@ -254,8 +254,9 @@ export async function executeQuery(
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
options?: { maxRows?: number; fetchSize?: number; pageSize?: number; resultSessionId?: string },
|
||||
): Promise<QueryResult> {
|
||||
return post("/api/query/execute", { connectionId, database, sql, schema, executionId });
|
||||
return post("/api/query/execute", { connectionId, database, sql, schema, executionId, ...options });
|
||||
}
|
||||
|
||||
export async function executeMulti(
|
||||
@@ -264,8 +265,13 @@ export async function executeMulti(
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
options?: { maxRows?: number; fetchSize?: number; pageSize?: number; resultSessionId?: string },
|
||||
): Promise<QueryResult[]> {
|
||||
return post("/api/query/execute-multi", { connectionId, database, sql, schema, executionId });
|
||||
return post("/api/query/execute-multi", { connectionId, database, sql, schema, executionId, ...options });
|
||||
}
|
||||
|
||||
export async function closeQuerySession(connectionId: string, database: string, sessionId: string): Promise<boolean> {
|
||||
return post("/api/query/close-session", { connectionId, database, sessionId });
|
||||
}
|
||||
|
||||
export async function executeBatch(
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { DatabaseType } from "../types/database.ts";
|
||||
import { usesFetchFirst } from "./databaseCapabilities.ts";
|
||||
import { quoteTableIdentifier } from "./tableSelectSql.ts";
|
||||
import { findStatementAtCursor } from "./sqlStatementSplit.ts";
|
||||
|
||||
export interface PaginatedQuerySqlResult {
|
||||
ok: true;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
export interface PaginatedQuerySqlError {
|
||||
ok: false;
|
||||
reason: "empty" | "multi" | "not_select" | "unsupported";
|
||||
}
|
||||
|
||||
export interface QueryPaginationExecutionPlan {
|
||||
sqlToExecute: string;
|
||||
pageSql?: string;
|
||||
pageLimit?: number;
|
||||
pageOffset?: number;
|
||||
countSql?: string;
|
||||
useAgentResultSession: boolean;
|
||||
}
|
||||
|
||||
const unsupportedPaginationTypes = new Set<DatabaseType | undefined>(["neo4j", "mongodb", "redis", "elasticsearch"]);
|
||||
|
||||
export function buildQueryPaginationExecutionPlan({
|
||||
sql,
|
||||
queryBaseSql,
|
||||
databaseType,
|
||||
pagination,
|
||||
useAgentCursor,
|
||||
}: {
|
||||
sql: string;
|
||||
queryBaseSql: string;
|
||||
databaseType: DatabaseType | undefined;
|
||||
pagination: { limit: number; offset: number; sessionId?: string };
|
||||
useAgentCursor: boolean;
|
||||
}): QueryPaginationExecutionPlan {
|
||||
const plan: QueryPaginationExecutionPlan = {
|
||||
sqlToExecute: sql,
|
||||
useAgentResultSession: false,
|
||||
};
|
||||
const counted = buildCountQuerySql(queryBaseSql, databaseType);
|
||||
if (counted.ok) {
|
||||
plan.countSql = counted.sql;
|
||||
}
|
||||
|
||||
if (pagination.sessionId) {
|
||||
plan.pageLimit = pagination.limit;
|
||||
plan.pageOffset = pagination.offset;
|
||||
plan.useAgentResultSession = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
if (useAgentCursor && pagination.offset === 0) {
|
||||
plan.sqlToExecute = queryBaseSql;
|
||||
plan.pageLimit = pagination.limit;
|
||||
plan.pageOffset = pagination.offset;
|
||||
plan.useAgentResultSession = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
const paginated = buildPaginatedQuerySql(sql, databaseType, pagination.limit, pagination.offset);
|
||||
if (paginated.ok) {
|
||||
plan.sqlToExecute = paginated.sql;
|
||||
plan.pageSql = paginated.sql;
|
||||
plan.pageLimit = pagination.limit;
|
||||
plan.pageOffset = pagination.offset;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function buildPaginatedQuerySql(
|
||||
originalSql: string,
|
||||
databaseType: DatabaseType | undefined,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): PaginatedQuerySqlResult | PaginatedQuerySqlError {
|
||||
const statement = singleSelectableStatement(originalSql);
|
||||
if (!statement.ok) return statement;
|
||||
if (unsupportedPaginationTypes.has(databaseType)) return { ok: false, reason: "unsupported" };
|
||||
|
||||
const safeLimit = Math.max(1, Math.floor(limit));
|
||||
const safeOffset = Math.max(0, Math.floor(offset));
|
||||
const alias = quoteTableIdentifier(databaseType, "dbx_page");
|
||||
const base = `SELECT * FROM (${statement.sql}) ${alias}`;
|
||||
|
||||
if (databaseType === "sqlserver") {
|
||||
return {
|
||||
ok: true,
|
||||
sql: `${base} ORDER BY (SELECT NULL) OFFSET ${safeOffset} ROWS FETCH NEXT ${safeLimit} ROWS ONLY`,
|
||||
};
|
||||
}
|
||||
|
||||
if (usesFetchFirst(databaseType)) {
|
||||
const offsetSql = safeOffset ? ` OFFSET ${safeOffset} ROWS` : "";
|
||||
return { ok: true, sql: `${base}${offsetSql} FETCH FIRST ${safeLimit} ROWS ONLY` };
|
||||
}
|
||||
|
||||
const offsetSql = safeOffset ? ` OFFSET ${safeOffset}` : "";
|
||||
return { ok: true, sql: `${base} LIMIT ${safeLimit}${offsetSql};` };
|
||||
}
|
||||
|
||||
export function buildCountQuerySql(
|
||||
originalSql: string,
|
||||
databaseType: DatabaseType | undefined,
|
||||
): PaginatedQuerySqlResult | PaginatedQuerySqlError {
|
||||
const statement = singleSelectableStatement(originalSql);
|
||||
if (!statement.ok) return statement;
|
||||
if (unsupportedPaginationTypes.has(databaseType)) return { ok: false, reason: "unsupported" };
|
||||
|
||||
const alias = quoteTableIdentifier(databaseType, "dbx_count");
|
||||
return { ok: true, sql: `SELECT COUNT(*) AS dbx_total_rows FROM (${statement.sql}) ${alias};` };
|
||||
}
|
||||
|
||||
function singleSelectableStatement(
|
||||
originalSql: string,
|
||||
): { ok: true; sql: string } | Pick<PaginatedQuerySqlError, "ok" | "reason"> {
|
||||
const baseSql = originalSql.trim();
|
||||
if (!baseSql) return { ok: false, reason: "empty" };
|
||||
|
||||
const statement = findStatementAtCursor(baseSql, 0)
|
||||
.trim()
|
||||
.replace(/;+\s*$/, "")
|
||||
.trim();
|
||||
if (!statement) return { ok: false, reason: "empty" };
|
||||
if (statement.length !== baseSql.replace(/;+\s*$/, "").trim().length) {
|
||||
return { ok: false, reason: "multi" };
|
||||
}
|
||||
if (!/^\s*(SELECT|WITH)\b/i.test(statement)) {
|
||||
return { ok: false, reason: "not_select" };
|
||||
}
|
||||
|
||||
return { ok: true, sql: statement };
|
||||
}
|
||||
+8
-2
@@ -181,8 +181,9 @@ export async function executeQuery(
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
options?: { maxRows?: number; fetchSize?: number; pageSize?: number; resultSessionId?: string },
|
||||
): Promise<QueryResult> {
|
||||
return invoke("execute_query", { connectionId, database, sql, schema, executionId });
|
||||
return invoke("execute_query", { connectionId, database, sql, schema, executionId, ...options });
|
||||
}
|
||||
|
||||
export async function executeMulti(
|
||||
@@ -191,14 +192,19 @@ export async function executeMulti(
|
||||
sql: string,
|
||||
schema?: string,
|
||||
executionId?: string,
|
||||
options?: { maxRows?: number; fetchSize?: number; pageSize?: number; resultSessionId?: string },
|
||||
): Promise<QueryResult[]> {
|
||||
return invoke("execute_multi", { connectionId, database, sql, schema, executionId });
|
||||
return invoke("execute_multi", { connectionId, database, sql, schema, executionId, ...options });
|
||||
}
|
||||
|
||||
export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return invoke("cancel_query", { executionId });
|
||||
}
|
||||
|
||||
export async function closeQuerySession(connectionId: string, database: string, sessionId: string): Promise<boolean> {
|
||||
return invoke("close_query_session", { connectionId, database, sessionId });
|
||||
}
|
||||
|
||||
export async function executeBatch(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
|
||||
Reference in New Issue
Block a user