feat(query): add paged result fetching

This commit is contained in:
t8y2
2026-05-17 00:10:18 +08:00
parent 320db12209
commit f4566221b1
25 changed files with 885 additions and 59 deletions
+33 -14
View File
@@ -111,6 +111,7 @@ const props = defineProps<{
databaseType?: DatabaseType;
connectionId?: string;
database?: string;
schema?: string;
context?: "results" | "table-data";
sourceColumns?: Array<string | undefined>;
queryEditabilityReason?: string;
@@ -121,6 +122,9 @@ const props = defineProps<{
columns: ColumnInfo[];
primaryKeys: string[];
};
pageOffset?: number;
pageLimit?: number;
countSql?: string;
loading?: boolean;
onExecuteSql?: (sql: string) => Promise<void>;
customSave?: (changes: {
@@ -951,7 +955,16 @@ watch(
// --- Pagination ---
const pageSize = ref(settingsStore.editorSettings.pageSize);
const currentPage = ref(1);
const isFullPage = computed(() => props.result.rows.length >= pageSize.value);
watch(
() => [props.pageOffset, props.pageLimit],
([offset, limit]) => {
if (typeof offset !== "number" || typeof limit !== "number" || limit <= 0) return;
pageSize.value = limit;
currentPage.value = Math.floor(offset / limit) + 1;
},
);
const canGoNextPage = computed(() => props.result.has_more === true || props.result.rows.length >= pageSize.value);
const canJumpLastPage = computed(() => canGoNextPage.value && (!!props.tableMeta || !!props.countSql));
const isResultsContext = computed(() => props.context === "results");
const resultEditStatus = computed(() => {
if (!isResultsContext.value || !hasData.value) return null;
@@ -1036,7 +1049,7 @@ function prevPage() {
emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy());
}
function nextPage() {
if (!isFullPage.value) return;
if (!canGoNextPage.value) return;
currentPage.value++;
resetGridVerticalScroll(true);
emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy());
@@ -1050,17 +1063,23 @@ function changePageSize(size: number) {
}
async function lastPage() {
if (!props.connectionId || !props.tableMeta) return;
const table = qualifiedTableName({
databaseType: props.databaseType,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
});
const predicate = normalizeWhereInput(currentWhereInput());
const where = predicate ? ` WHERE (${predicate})` : "";
const sql = `SELECT COUNT(*) AS cnt FROM ${table}${where}`;
if (!props.connectionId) return;
let sql = props.countSql;
let schema = props.schema;
if (props.tableMeta) {
const table = qualifiedTableName({
databaseType: props.databaseType,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
});
const predicate = normalizeWhereInput(currentWhereInput());
const where = predicate ? ` WHERE (${predicate})` : "";
sql = `SELECT COUNT(*) AS cnt FROM ${table}${where}`;
schema = props.tableMeta.schema;
}
if (!sql) return;
try {
const result = await api.executeQuery(props.connectionId, props.database ?? "", sql, props.tableMeta.schema);
const result = await api.executeQuery(props.connectionId, props.database ?? "", sql, schema);
const total = Number(result.rows?.[0]?.[0] ?? 0);
if (total <= 0) return;
const lastPageNum = Math.ceil(total / pageSize.value);
@@ -3523,10 +3542,10 @@ defineExpose({
<ChevronLeft class="h-3 w-3" />
</Button>
<span>{{ currentPage }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!isFullPage" @click="nextPage">
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canGoNextPage" @click="nextPage">
<ChevronRight class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!isFullPage" @click="lastPage">
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canJumpLastPage" @click="lastPage">
<ChevronsRight class="h-3 w-3" />
</Button>
</span>
+4
View File
@@ -304,7 +304,11 @@ defineExpose({ focusSearch });
:database-type="activeConnection?.db_type"
:connection-id="activeTab.connectionId"
:database="activeTab.database"
:schema="activeTab.schema"
:table-meta="activeTab.tableMeta"
:page-offset="activeTab.resultPageOffset"
:page-limit="activeTab.resultPageLimit"
:count-sql="activeTab.resultCountSql"
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
@reload="
(
+2
View File
@@ -471,12 +471,14 @@ async function openData() {
elapsed: elapsed(),
});
const pks = editablePrimaryKeys(config.db_type, columns);
const limit = settingsStore.editorSettings.pageSize;
const sql = buildTableSelectSql({
databaseType: config.db_type,
schema: node.schema,
tableName: node.label,
columns: columns.map((column) => column.name),
primaryKeys: pks,
limit,
includeRowId: usesSyntheticRowIdKey(config.db_type, pks),
});
console.info("[DBX][openData:sql-built]", {
+21 -1
View File
@@ -2,6 +2,7 @@ import { type ComputedRef } from "vue";
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { buildSortedQuerySql } from "@/lib/queryResultSort";
@@ -13,6 +14,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const { toast } = useToast();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
function quoteIdent(tab: QueryTab, name: string): string {
const config = connectionStore.getConfig(tab.connectionId);
@@ -41,6 +43,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
primaryKeys,
fallbackOrderColumns,
includeRowId: useRowId,
limit: options.limit ?? settingsStore.editorSettings.pageSize,
...options,
});
}
@@ -87,7 +90,24 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
async function onPaginate(offset: number, limit: number, whereInput?: string, orderBy?: string) {
const tab = activeTab.value;
if (!tab?.tableMeta) return;
if (!tab) return;
if (tab.mode !== "data") {
const baseSql = tab.resultSortedSql ?? tab.resultBaseSql ?? tab.lastExecutedSql ?? tab.sql;
if (!baseSql.trim()) return;
const expectedNextOffset = (tab.resultPageOffset ?? 0) + (tab.resultPageLimit ?? limit);
const sessionId =
tab.result?.has_more && tab.result?.session_id && offset === expectedNextOffset && limit === tab.resultPageLimit
? tab.result.session_id
: undefined;
await queryStore.executeTabSql(tab.id, baseSql, {
resultBaseSql: tab.resultBaseSql ?? tab.sql,
resultSortedSql: tab.resultSortedSql,
pagination: { offset, limit, sessionId },
});
return;
}
if (!tab.tableMeta) return;
tab.whereInput = whereInput ?? "";
const sql = buildTableSql(tab, { limit, offset, whereInput, orderBy });
queryStore.updateSql(tab.id, sql);
+6
View File
@@ -3,6 +3,7 @@ import { buildTableSelectSql } from "@/lib/tableSelectSql";
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
export type NavigationTarget = {
connectionId: string;
@@ -16,6 +17,8 @@ export type NavigationTarget = {
async function openTableTarget(target: NavigationTarget) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const pageLimit = settingsStore.editorSettings.pageSize;
connectionStore.activeConnectionId = target.connectionId;
const config = connectionStore.getConfig(target.connectionId);
@@ -37,6 +40,7 @@ async function openTableTarget(target: NavigationTarget) {
columns: columns.map((column) => column.name),
primaryKeys,
whereInput: target.whereInput,
limit: pageLimit,
});
queryStore.updateSql(tabId, sql);
queryStore.setTableMeta(tabId, {
@@ -53,6 +57,7 @@ async function openTableTarget(target: NavigationTarget) {
schema: target.schema,
tableName: target.tableName,
whereInput: target.whereInput,
limit: pageLimit,
});
queryStore.updateSql(tabId, sql);
queryStore.setTableMeta(tabId, {
@@ -83,6 +88,7 @@ async function openTableTarget(target: NavigationTarget) {
primaryKeys,
columns: columns.map((column) => column.name),
includeRowId: true,
limit: pageLimit,
});
queryStore.updateSql(tabId, newSql);
await queryStore.executeTabSql(tabId, newSql);
+1
View File
@@ -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
View File
@@ -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(
+136
View File
@@ -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
View File
@@ -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,
+82 -5
View File
@@ -14,9 +14,12 @@ import {
} from "@/lib/sqlAnalysis";
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
import { mongoDocumentsToQueryResult, parseMongoFindCommand } from "@/lib/mongoShellCommand";
import { buildQueryPaginationExecutionPlan } from "@/lib/queryResultPagination";
import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities";
import { editablePrimaryKeys } from "@/lib/tableEditing";
import * as api from "@/lib/api";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import type { SavedSqlFile } from "@/types/database";
@@ -46,6 +49,19 @@ export const useQueryStore = defineStore("query", () => {
const activeTabId = ref<string | null>(restored.activeTabId);
const MAX_CACHED_RESULTS = 10;
async function closeResultSession(tab: QueryTab | undefined, preserveSessionId?: string) {
const sessionId = tab?.resultSessionId ?? tab?.result?.session_id;
if (!tab || !sessionId || sessionId === preserveSessionId) return;
try {
await api.closeQuerySession(tab.connectionId, tab.database, sessionId);
} catch (error) {
console.warn("[DBX][query-session:close:error]", { tabId: tab.id, sessionId, error });
} finally {
if (tab.resultSessionId === sessionId) tab.resultSessionId = undefined;
if (tab.result?.session_id === sessionId) tab.result.session_id = undefined;
}
}
const _persistSnapshot = computed(() =>
tabs.value.map((t) => ({
id: t.id,
@@ -147,6 +163,7 @@ export const useQueryStore = defineStore("query", () => {
if (idx < 0) return;
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
void closeResultSession(tabs.value[idx]);
tabs.value[idx].result = undefined;
tabs.value[idx].results = undefined;
tabs.value.splice(idx, 1);
@@ -158,6 +175,7 @@ export const useQueryStore = defineStore("query", () => {
function closeOtherTabs(id: string) {
tabs.value.filter((tab) => tab.id !== id && tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id));
tabs.value.filter((tab) => tab.id !== id && tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id));
tabs.value.filter((tab) => tab.id !== id).forEach((tab) => void closeResultSession(tab));
const next = closeOtherTabsState(tabs.value, activeTabId.value, id);
tabs.value = next.tabs;
activeTabId.value = next.activeTabId;
@@ -166,6 +184,7 @@ export const useQueryStore = defineStore("query", () => {
function closeAllTabs() {
tabs.value.filter((tab) => tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id));
tabs.value.filter((tab) => tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id));
tabs.value.forEach((tab) => void closeResultSession(tab));
const next = closeAllTabsState(tabs.value, activeTabId.value);
tabs.value = next.tabs;
activeTabId.value = next.activeTabId;
@@ -410,7 +429,11 @@ export const useQueryStore = defineStore("query", () => {
async function executeTabSql(
id: string,
sql: string,
options?: { resultBaseSql?: string; resultSortedSql?: string | undefined },
options?: {
resultBaseSql?: string;
resultSortedSql?: string | undefined;
pagination?: { limit: number; offset: number; sessionId?: string };
},
) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || !sql.trim()) return;
@@ -432,9 +455,35 @@ export const useQueryStore = defineStore("query", () => {
schema: tab.schema,
sql,
});
const queryBaseSql = options?.resultBaseSql ?? sql;
let sqlToExecute = sql;
let pageSql: string | undefined;
let pageLimit: number | undefined;
let pageOffset: number | undefined;
let countSql: string | undefined;
let useAgentResultSession = false;
try {
const connStore = useConnectionStore();
const conn = connStore.getConfig(tab.connectionId);
const useAgentCursor = !!conn?.db_type && AGENT_DRIVER_TYPES.has(conn.db_type);
await closeResultSession(tab, options?.pagination?.sessionId);
if (tab.mode === "query") {
const settingsStore = useSettingsStore();
const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 };
const plan = buildQueryPaginationExecutionPlan({
sql,
queryBaseSql,
databaseType: conn?.db_type,
pagination,
useAgentCursor,
});
sqlToExecute = plan.sqlToExecute;
pageSql = plan.pageSql;
pageLimit = plan.pageLimit;
pageOffset = plan.pageOffset;
countSql = plan.countSql;
useAgentResultSession = plan.useAgentResultSession;
}
const mongoFind = conn?.db_type === "mongodb" ? parseMongoFindCommand(sql) : null;
if (mongoFind) {
console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoFind.collection });
@@ -469,7 +518,25 @@ export const useQueryStore = defineStore("query", () => {
}
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
const results = await api.executeMulti(tab.connectionId, tab.database, sql, tab.schema, executionId);
const executionOptions =
typeof pageLimit === "number"
? useAgentResultSession
? {
maxRows: 10000,
fetchSize: pageLimit,
pageSize: pageLimit,
resultSessionId: options?.pagination?.sessionId,
}
: { maxRows: pageLimit, fetchSize: pageLimit }
: undefined;
const results = await api.executeMulti(
tab.connectionId,
tab.database,
sqlToExecute,
tab.schema,
executionId,
executionOptions,
);
console.info("[DBX][executeTabSql:execute-multi:done]", {
traceId,
resultCount: results.length,
@@ -488,10 +555,15 @@ export const useQueryStore = defineStore("query", () => {
current.activeResultIndex = undefined;
current.result = results[0];
}
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultBaseSql = queryBaseSql;
current.resultSortedSql = options?.resultSortedSql;
current.resultPageSql = pageSql;
current.resultPageLimit = pageLimit;
current.resultPageOffset = pageOffset;
current.resultCountSql = countSql;
current.resultSessionId = current.result?.session_id ?? undefined;
console.info("[DBX][executeTabSql:metadata:start]", { traceId, elapsed: elapsed() });
await analyzeQueryMetadata(current, current.resultBaseSql);
await analyzeQueryMetadata(current, queryBaseSql);
console.info("[DBX][executeTabSql:metadata:done]", { traceId, elapsed: elapsed() });
} else {
console.warn("[DBX][executeTabSql:stale-result]", {
@@ -511,8 +583,13 @@ export const useQueryStore = defineStore("query", () => {
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
if (current.mode !== "data") current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultBaseSql = queryBaseSql;
current.resultSortedSql = options?.resultSortedSql;
current.resultPageSql = pageSql;
current.resultPageLimit = pageLimit;
current.resultPageOffset = pageOffset;
current.resultCountSql = countSql;
current.resultSessionId = undefined;
}
} finally {
const current = tabs.value.find((t) => t.id === id);
+7
View File
@@ -175,6 +175,8 @@ export interface QueryResult {
affected_rows: number;
execution_time_ms: number;
truncated?: boolean;
session_id?: string | null;
has_more?: boolean;
}
export type TreeNodeType =
@@ -253,6 +255,11 @@ export interface QueryTab {
lastExecutedSql?: string;
resultBaseSql?: string;
resultSortedSql?: string;
resultPageSql?: string;
resultPageLimit?: number;
resultPageOffset?: number;
resultCountSql?: string;
resultSessionId?: string;
pinned?: boolean;
result?: QueryResult;
results?: QueryResult[];