fix(grid): refine total row counting
This commit is contained in:
parent
67e4c5d881
commit
fecefb1589
|
|
@ -158,6 +158,7 @@ import {
|
|||
isToggleTransposeShortcut,
|
||||
} from "@/lib/keyboardShortcuts";
|
||||
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGridScrollGutter";
|
||||
import { canGoNextDataGridPage } from "@/lib/dataGridPagination";
|
||||
import { CANVAS_DATA_GRID_ROW_HEIGHT, drawCanvasDataGrid } from "@/lib/canvasDataGridRenderer";
|
||||
import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGridSaveUi";
|
||||
import {
|
||||
|
|
@ -237,6 +238,7 @@ const props = defineProps<{
|
|||
pageLimit?: number;
|
||||
countSql?: string;
|
||||
totalRowCount?: number;
|
||||
totalRowCountLoading?: boolean;
|
||||
loading?: boolean;
|
||||
cacheKey?: string;
|
||||
onExecuteSql?: (sql: string) => Promise<void>;
|
||||
|
|
@ -1796,12 +1798,31 @@ watch(
|
|||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const canGoNextPage = computed(() => props.result.has_more === true || props.result.rows.length >= pageSize.value);
|
||||
const canJumpLastPage = computed(() => canGoNextPage.value && (!!props.tableMeta || !!props.countSql));
|
||||
const manualTotalRowCount = ref<number | undefined>(undefined);
|
||||
const manualTotalRowCountLoading = ref(false);
|
||||
const showTruncationWarning = computed(
|
||||
() => props.result.truncated === true && typeof props.pageLimit !== "number" && props.result.has_more !== true,
|
||||
);
|
||||
const isResultsContext = computed(() => props.context === "results");
|
||||
const displayedTotalRowCount = computed(() => props.totalRowCount ?? manualTotalRowCount.value);
|
||||
const hasKnownTotalRowCount = computed(
|
||||
() => typeof displayedTotalRowCount.value === "number" && displayedTotalRowCount.value >= 0,
|
||||
);
|
||||
const canGoNextPage = computed(() => {
|
||||
return canGoNextDataGridPage({
|
||||
hasMore: props.result.has_more,
|
||||
rowCount: props.result.rows.length,
|
||||
pageSize: pageSize.value,
|
||||
pageOffset: props.pageOffset,
|
||||
currentPage: currentPage.value,
|
||||
totalRowCount: hasKnownTotalRowCount.value ? displayedTotalRowCount.value : undefined,
|
||||
});
|
||||
});
|
||||
const canJumpLastPage = computed(() => canGoNextPage.value && (!!props.tableMeta || !!props.countSql));
|
||||
const totalRowCountBusy = computed(() => props.totalRowCountLoading === true || manualTotalRowCountLoading.value);
|
||||
const canCalculateTotalRowCount = computed(
|
||||
() => !isResultsContext.value && !!props.connectionId && (!!props.tableMeta || !!props.countSql),
|
||||
);
|
||||
const showQueryEditReadyBadge = computed(
|
||||
() => isResultsContext.value && hasData.value && !!props.editable && !!props.tableMeta,
|
||||
);
|
||||
|
|
@ -1859,6 +1880,21 @@ function currentOrderBy(): string | undefined {
|
|||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.countSql ?? "",
|
||||
props.tableMeta?.schema ?? "",
|
||||
props.tableMeta?.tableName ?? "",
|
||||
currentWhereInput() ?? "",
|
||||
props.database ?? "",
|
||||
props.connectionId ?? "",
|
||||
props.result,
|
||||
],
|
||||
() => {
|
||||
manualTotalRowCount.value = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
function syncOrderByInputWithSort(column: string | null, direction: "asc" | "desc" | null) {
|
||||
const nextOrderByInput = column && direction ? `${queryColumnRef(column)} ${direction.toUpperCase()}` : "";
|
||||
orderByInput.value = nextOrderByInput;
|
||||
|
|
@ -1915,20 +1951,11 @@ function applyCustomPageSize() {
|
|||
|
||||
async function lastPage() {
|
||||
if (!props.connectionId) return;
|
||||
let sql = props.countSql;
|
||||
let schema = props.schema;
|
||||
if (props.tableMeta) {
|
||||
sql = await buildDataGridCountSql({
|
||||
databaseType: props.databaseType,
|
||||
schema: props.tableMeta.schema,
|
||||
tableName: props.tableMeta.tableName,
|
||||
whereInput: currentWhereInput(),
|
||||
});
|
||||
schema = props.tableMeta.schema;
|
||||
}
|
||||
const countTarget = await buildCurrentCountTarget();
|
||||
const sql = countTarget?.sql;
|
||||
if (!sql) return;
|
||||
try {
|
||||
const result = await api.executeQuery(props.connectionId, props.database ?? "", sql, schema);
|
||||
const result = await api.executeQuery(props.connectionId, props.database ?? "", sql, countTarget.schema);
|
||||
const total = Number(result.rows?.[0]?.[0] ?? 0);
|
||||
if (total <= 0) return;
|
||||
const lastPageNum = Math.ceil(total / pageSize.value);
|
||||
|
|
@ -1941,6 +1968,43 @@ async function lastPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function buildCurrentCountTarget(): Promise<{ sql: string; schema?: string } | undefined> {
|
||||
if (props.countSql) return { sql: props.countSql, schema: props.schema };
|
||||
if (props.tableMeta) {
|
||||
const sql = await buildDataGridCountSql({
|
||||
databaseType: props.databaseType,
|
||||
schema: props.tableMeta.schema,
|
||||
tableName: props.tableMeta.tableName,
|
||||
whereInput: currentWhereInput(),
|
||||
});
|
||||
return { sql, schema: props.context === "table-data" ? undefined : (props.tableMeta.schema ?? props.schema) };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function calculateTotalRowCount() {
|
||||
if (!props.connectionId || manualTotalRowCountLoading.value) return;
|
||||
manualTotalRowCountLoading.value = true;
|
||||
try {
|
||||
const countTarget = await buildCurrentCountTarget();
|
||||
if (!countTarget?.sql) return;
|
||||
const result = await api.executeQuery(
|
||||
props.connectionId,
|
||||
props.database ?? "",
|
||||
countTarget.sql,
|
||||
countTarget.schema,
|
||||
);
|
||||
const total = Number(result.rows?.[0]?.[0] ?? 0);
|
||||
if (Number.isFinite(total) && total >= 0) {
|
||||
manualTotalRowCount.value = total;
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("grid.calculateTotalRowsFailed", { message: e?.message || String(e) }), 5000);
|
||||
} finally {
|
||||
manualTotalRowCountLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Editing (composable) ---
|
||||
|
||||
interface RowItem {
|
||||
|
|
@ -7649,9 +7713,23 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<span v-if="hasData" class="shrink-0">
|
||||
{{ t("grid.totalRows", { count: result.rows.length }) }}
|
||||
<span v-if="typeof totalRowCount === 'number' && totalRowCount > 0" class="text-muted-foreground/70">{{
|
||||
t("grid.totalRowCount", { count: totalRowCount })
|
||||
}}</span>
|
||||
<span
|
||||
v-if="typeof displayedTotalRowCount === 'number' && displayedTotalRowCount >= 0"
|
||||
class="text-muted-foreground/70"
|
||||
>{{ t("grid.totalRowCount", { count: displayedTotalRowCount }) }}</span
|
||||
>
|
||||
<span v-else-if="totalRowCountBusy" class="text-muted-foreground/70">
|
||||
{{ t("grid.totalRowCountLoading") }}
|
||||
</span>
|
||||
<button
|
||||
v-else-if="canCalculateTotalRowCount"
|
||||
type="button"
|
||||
class="text-muted-foreground/70 hover:text-foreground hover:underline underline-offset-2 disabled:pointer-events-none"
|
||||
:disabled="manualTotalRowCountLoading"
|
||||
@click="calculateTotalRowCount"
|
||||
>
|
||||
{{ t("grid.calculateTotalRowsInline") }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="showTruncationWarning" class="shrink-0 text-amber-500 text-xs">(truncated)</span>
|
||||
<span v-if="!hasData" class="shrink-0">{{ t("grid.rowsAffected", { count: result.affected_rows }) }}</span>
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
|||
:page-limit="activeTab.resultPageLimit"
|
||||
:count-sql="activeTab.resultCountSql"
|
||||
:total-row-count="activeTab.resultTotalRowCount"
|
||||
:total-row-count-loading="activeTab.resultTotalRowCountLoading"
|
||||
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
|
||||
:full-export-result="() => queryStore.fetchTabResultForExport(activeTab.id)"
|
||||
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
resultBaseSql: tab.resultBaseSql ?? tab.sql,
|
||||
resultSortedSql: tab.resultSortedSql,
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -118,6 +119,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
resultSortedSql: tab.resultSortedSql,
|
||||
pagination: { offset, limit, sessionId },
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -161,6 +163,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
resultBaseSql: baseSql,
|
||||
resultSortedSql: undefined,
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -183,6 +186,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
|||
resultBaseSql: baseSql,
|
||||
resultSortedSql: built.sql,
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -403,6 +403,10 @@ export default {
|
|||
rows: "{count} rows",
|
||||
totalRows: "Total {count} rows",
|
||||
totalRowCount: "({count} total)",
|
||||
totalRowCountLoading: "(counting...)",
|
||||
calculateTotalRows: "Count total rows",
|
||||
calculateTotalRowsInline: "(count total rows)",
|
||||
calculateTotalRowsFailed: "Count failed: {message}",
|
||||
rowsAffected: "{count} rows affected",
|
||||
querySuccess: "Query executed successfully",
|
||||
noRows: "No data",
|
||||
|
|
|
|||
|
|
@ -396,6 +396,10 @@ export default {
|
|||
rows: "{count} filas",
|
||||
totalRows: "Total {count} filas",
|
||||
totalRowCount: "({count} en total)",
|
||||
totalRowCountLoading: "(contando...)",
|
||||
calculateTotalRows: "Contar filas totales",
|
||||
calculateTotalRowsInline: "(contar filas totales)",
|
||||
calculateTotalRowsFailed: "Error al contar: {message}",
|
||||
rowsAffected: "{count} filas afectadas",
|
||||
querySuccess: "Consulta ejecutada exitosamente",
|
||||
noRows: "Sin datos",
|
||||
|
|
|
|||
|
|
@ -408,6 +408,10 @@ export default {
|
|||
rows: "{count} righe",
|
||||
totalRows: "Totale {count} righe",
|
||||
totalRowCount: "({count} in totale)",
|
||||
totalRowCountLoading: "(conteggio...)",
|
||||
calculateTotalRows: "Conta righe totali",
|
||||
calculateTotalRowsInline: "(conta righe totali)",
|
||||
calculateTotalRowsFailed: "Conteggio non riuscito: {message}",
|
||||
rowsAffected: "{count} righe interessate",
|
||||
querySuccess: "Query eseguita con successo",
|
||||
noRows: "Nessun dato",
|
||||
|
|
|
|||
|
|
@ -148,11 +148,13 @@ export default {
|
|||
mysqlTlsModeRequired: "Obrigatório",
|
||||
mysqlTlsModeVerifyCa: "Verificar CA",
|
||||
mysqlTlsModeVerifyIdentity: "Verificar Identidade",
|
||||
mysqlCaCertHint: "Necessário para Verificar CA e Verificar Identidade quando o certificado do servidor usa uma CA privada.",
|
||||
mysqlCaCertHint:
|
||||
"Necessário para Verificar CA e Verificar Identidade quando o certificado do servidor usa uma CA privada.",
|
||||
mysqlClientCert: "Autenticação do Cliente",
|
||||
mysqlClientCertPlaceholder: "/caminho/para/client.crt",
|
||||
mysqlClientKeyPlaceholder: "/caminho/para/client.key",
|
||||
mysqlClientCertHint: "O certificado e a chave privada do cliente devem ser fornecidos juntos quando o MySQL exige mTLS.",
|
||||
mysqlClientCertHint:
|
||||
"O certificado e a chave privada do cliente devem ser fornecidos juntos quando o MySQL exige mTLS.",
|
||||
mysqlClientCertBrowse: "Escolher certificado do cliente",
|
||||
mysqlClientKeyBrowse: "Escolher chave privada do cliente",
|
||||
postgresSslMode: "Modo TLS",
|
||||
|
|
@ -269,13 +271,15 @@ export default {
|
|||
jdbcUrl: "URL JDBC",
|
||||
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
|
||||
jdbcDriverClass: "Classe do Driver (opcional)",
|
||||
jdbcDriverClassPlaceholder: "A maioria dos drivers se registra automaticamente; use com.vendor.jdbc.Driver se necessário",
|
||||
jdbcDriverClassPlaceholder:
|
||||
"A maioria dos drivers se registra automaticamente; use com.vendor.jdbc.Driver se necessário",
|
||||
jdbcDriverPaths: "JARs do Driver",
|
||||
jdbcDriverSelectPlaceholder: "Escolher driver importado",
|
||||
jdbcDriverPathsPlaceholder: "/caminho/para/driver.jar\n/caminho/para/outro-driver.jar",
|
||||
jdbcDriverBrowse: "Escolher JAR do driver JDBC",
|
||||
jdbcDocs: "Ver documentação JDBC",
|
||||
jdbcPluginHint: "Instale primeiro o plugin JDBC do DBX e depois importe o JAR do driver JDBC do fornecedor do banco de dados.",
|
||||
jdbcPluginHint:
|
||||
"Instale primeiro o plugin JDBC do DBX e depois importe o JAR do driver JDBC do fornecedor do banco de dados.",
|
||||
dmCompatHint: "Requer o driver ODBC DM8 instalado no seu sistema.",
|
||||
dmDownload: "Baixar da Dameng",
|
||||
mongoLegacyHint:
|
||||
|
|
@ -403,6 +407,10 @@ export default {
|
|||
rows: "{count} linhas",
|
||||
totalRows: "Total de {count} linhas",
|
||||
totalRowCount: "({count} no total)",
|
||||
totalRowCountLoading: "(contando...)",
|
||||
calculateTotalRows: "Contar total de linhas",
|
||||
calculateTotalRowsInline: "(contar total de linhas)",
|
||||
calculateTotalRowsFailed: "Falha ao contar: {message}",
|
||||
rowsAffected: "{count} linhas afetadas",
|
||||
querySuccess: "Consulta executada com sucesso",
|
||||
noRows: "Sem dados",
|
||||
|
|
@ -643,21 +651,27 @@ export default {
|
|||
queryEditUnsupported: {
|
||||
"not-select": "Apenas resultados de consultas SELECT podem ser editados diretamente.",
|
||||
cte: "Consultas com WITH/CTE ainda não são editáveis. Use um SELECT simples de tabela única.",
|
||||
"set-operation": "Resultados de UNION, INTERSECT ou EXCEPT não podem ser mapeados com segurança de volta às linhas de origem.",
|
||||
"set-operation":
|
||||
"Resultados de UNION, INTERSECT ou EXCEPT não podem ser mapeados com segurança de volta às linhas de origem.",
|
||||
aggregation: "Resultados de DISTINCT, GROUP BY, HAVING e agregações não podem ser editados diretamente.",
|
||||
"external-source":
|
||||
"Arquivos externos e resultados de funções de tabela não podem ser gravados de volta diretamente. Importe-os para uma tabela do banco de dados antes de editar.",
|
||||
"complex-source": "JOINs, múltiplas tabelas e subconsultas não podem ser mapeados com segurança para uma única linha de origem.",
|
||||
"complex-source":
|
||||
"JOINs, múltiplas tabelas e subconsultas não podem ser mapeados com segurança para uma única linha de origem.",
|
||||
"computed-columns":
|
||||
"Expressões computadas ou resultados de funções não podem ser gravados de volta. Selecione os nomes brutos das colunas em vez disso.",
|
||||
"no-table": "Nenhuma tabela de origem editável foi detectada.",
|
||||
"no-primary-key": "A tabela de destino não possui chave primária, portanto as linhas não podem ser atualizadas ou excluídas com segurança.",
|
||||
"no-primary-key":
|
||||
"A tabela de destino não possui chave primária, portanto as linhas não podem ser atualizadas ou excluídas com segurança.",
|
||||
"primary-key-not-returned":
|
||||
"O resultado está sem a coluna bruta da chave primária. Inclua-a pelo seu nome de coluna original.",
|
||||
"aliased-columns": "As colunas do resultado usam aliases ou expressões. Selecione colunas editáveis pelos seus nomes originais.",
|
||||
"metadata-unavailable": "O DBX não conseguiu carregar os metadados da tabela, portanto a edição do resultado está desabilitada.",
|
||||
"aliased-columns":
|
||||
"As colunas do resultado usam aliases ou expressões. Selecione colunas editáveis pelos seus nomes originais.",
|
||||
"metadata-unavailable":
|
||||
"O DBX não conseguiu carregar os metadados da tabela, portanto a edição do resultado está desabilitada.",
|
||||
},
|
||||
sortUnsupported: "Este SQL não suporta a ordenação do resultado completo. Tente novamente com uma única consulta SELECT.",
|
||||
sortUnsupported:
|
||||
"Este SQL não suporta a ordenação do resultado completo. Tente novamente com uma única consulta SELECT.",
|
||||
truncatedHint: "Resultados truncados em {count} linhas. Use a paginação no rodapé ou ajuste as linhas por página.",
|
||||
},
|
||||
exportProgress: {
|
||||
|
|
@ -746,14 +760,18 @@ export default {
|
|||
sameName: "Mesmo Nome",
|
||||
},
|
||||
description: {
|
||||
foreignKeyIncoming: "{target} aponta para o campo atual através de uma chave estrangeira. Esta é uma dependência verificada.",
|
||||
foreignKeyOutgoing: "O campo atual referencia {target} através de uma chave estrangeira. Esta é uma dependência verificada.",
|
||||
viewLikely: "A definição da visão menciona tanto a tabela quanto o campo de destino, geralmente indicando dependência de consulta.",
|
||||
foreignKeyIncoming:
|
||||
"{target} aponta para o campo atual através de uma chave estrangeira. Esta é uma dependência verificada.",
|
||||
foreignKeyOutgoing:
|
||||
"O campo atual referencia {target} através de uma chave estrangeira. Esta é uma dependência verificada.",
|
||||
viewLikely:
|
||||
"A definição da visão menciona tanto a tabela quanto o campo de destino, geralmente indicando dependência de consulta.",
|
||||
viewPossible:
|
||||
"A definição da visão menciona um campo de mesmo nome, mas não a tabela de destino, então precisa de confirmação.",
|
||||
historyLikely:
|
||||
"Uma instrução SQL histórica menciona tanto a tabela quanto o campo de destino. Use-a como contexto de análise de impacto.",
|
||||
historyPossible: "Uma instrução SQL histórica menciona um campo de mesmo nome. Pode estar relacionado, mas precisa de contexto.",
|
||||
historyPossible:
|
||||
"Uma instrução SQL histórica menciona um campo de mesmo nome. Pode estar relacionado, mas precisa de contexto.",
|
||||
sameName:
|
||||
"Outra tabela possui um campo de mesmo nome. Isso pode compartilhar significado de negócio, mas não é uma dependência verificada do banco de dados.",
|
||||
},
|
||||
|
|
@ -873,7 +891,8 @@ export default {
|
|||
fixWithAi: "Corrigir com AI",
|
||||
truncated: "Contexto truncado",
|
||||
contextSummary: "{database} · {tables} tabelas",
|
||||
autoSqlBlocked: "O SQL gerado pela AI pareceu arriscado demais para execução automática. Revise-o manualmente antes de executar.",
|
||||
autoSqlBlocked:
|
||||
"O SQL gerado pela AI pareceu arriscado demais para execução automática. Revise-o manualmente antes de executar.",
|
||||
agentSteps: {
|
||||
generated: "SQL gerado",
|
||||
noSql: "Nenhum SQL encontrado",
|
||||
|
|
@ -970,7 +989,8 @@ export default {
|
|||
structureDocYes: "Sim",
|
||||
structureDocNo: "Não",
|
||||
structureDocCopied: "Estrutura copiada",
|
||||
structureDocCopyFallbackHint: "A cópia automática foi bloqueada. Clique em Copiar novamente ou selecione o conteúdo abaixo.",
|
||||
structureDocCopyFallbackHint:
|
||||
"A cópia automática foi bloqueada. Clique em Copiar novamente ou selecione o conteúdo abaixo.",
|
||||
saveStructure: "Salvar SQL",
|
||||
exportStructureCopied: "Estrutura copiada",
|
||||
createTable: "Criar Tabela",
|
||||
|
|
@ -1003,7 +1023,8 @@ export default {
|
|||
batchDrop: "Remover selecionados ({count})",
|
||||
executeProcedure: "Executar Procedimento",
|
||||
confirmExecuteProcedureTitle: "Executar Procedimento",
|
||||
confirmExecuteProcedureMessage: 'Executar o procedimento "{name}"? Você pode preencher ou ajustar os valores dos parâmetros primeiro.',
|
||||
confirmExecuteProcedureMessage:
|
||||
'Executar o procedimento "{name}"? Você pode preencher ou ajustar os valores dos parâmetros primeiro.',
|
||||
loadingProcedureParameters: "Carregando parâmetros...",
|
||||
procedureParametersUnavailable:
|
||||
"Não foi possível carregar os metadados dos parâmetros. Você ainda pode editar o SQL abaixo e executá-lo.",
|
||||
|
|
@ -1035,7 +1056,8 @@ export default {
|
|||
confirmDropTriggerTitle: "Remover Gatilho",
|
||||
confirmDropTableChildObjectMessage: 'Tem certeza de que deseja remover "{name}" de "{table}"?',
|
||||
confirmBatchDropTitle: "Remover Objetos Selecionados",
|
||||
confirmBatchDropMessage: "Tem certeza de que deseja remover {count} objetos selecionados? Esta ação não pode ser desfeita.",
|
||||
confirmBatchDropMessage:
|
||||
"Tem certeza de que deseja remover {count} objetos selecionados? Esta ação não pode ser desfeita.",
|
||||
confirmDropProcedureTitle: "Remover Procedimento",
|
||||
confirmDropProcedureMessage: 'Tem certeza de que deseja remover o procedimento "{name}"?',
|
||||
confirmDropFunctionTitle: "Remover Função",
|
||||
|
|
@ -1059,7 +1081,8 @@ export default {
|
|||
'Tem certeza de que deseja remover o banco de dados "{name}"? Isso excluirá permanentemente o banco de dados e todos os seus dados.',
|
||||
createDatabaseSuccess: 'Banco de dados "{name}" criado',
|
||||
createDuckDbFileSuccess: 'Arquivo de banco de dados DuckDB "{name}" criado e anexado',
|
||||
createDuckDbFileDesktopOnly: "A criação de arquivos de banco de dados DuckDB só está disponível no aplicativo desktop",
|
||||
createDuckDbFileDesktopOnly:
|
||||
"A criação de arquivos de banco de dados DuckDB só está disponível no aplicativo desktop",
|
||||
dropDatabaseSuccess: 'Banco de dados "{name}" removido',
|
||||
createDatabaseNamePlaceholder: "Nome do banco de dados",
|
||||
createDatabaseCharset: "Conjunto de caracteres",
|
||||
|
|
@ -1137,7 +1160,8 @@ export default {
|
|||
dropSelected: "Remover selecionados",
|
||||
clearSelection: "Limpar seleção",
|
||||
confirmBatchDropTitle: "Remover tabelas selecionadas",
|
||||
confirmBatchDropMessage: "Tem certeza de que deseja remover {count} tabelas selecionadas? Esta ação não pode ser desfeita.",
|
||||
confirmBatchDropMessage:
|
||||
"Tem certeza de que deseja remover {count} tabelas selecionadas? Esta ação não pode ser desfeita.",
|
||||
batchDropSuccess: "{count} tabelas removidas",
|
||||
},
|
||||
structureEditor: {
|
||||
|
|
@ -1389,7 +1413,8 @@ export default {
|
|||
},
|
||||
dangerDialog: {
|
||||
title: "Operação perigosa",
|
||||
message: "Esta instrução SQL pode modificar ou excluir dados de forma irreversível. Tem certeza de que deseja executá-la?",
|
||||
message:
|
||||
"Esta instrução SQL pode modificar ou excluir dados de forma irreversível. Tem certeza de que deseja executá-la?",
|
||||
suppressFuturePrompts: "Não perguntar novamente para SQL perigoso",
|
||||
deleteMessage: "Esta operação de exclusão pode ser irreversível. Continuar?",
|
||||
deleteConfirm: "Confirmar exclusão",
|
||||
|
|
@ -1537,7 +1562,8 @@ export default {
|
|||
selectedTables: "{selected}/{total} selecionadas",
|
||||
selectAllTables: "Selecionar tudo",
|
||||
deselectAllTables: "Desmarcar tudo",
|
||||
selectSourceTables: "Selecione primeiro a conexão e o banco de dados de origem, depois escolha as tabelas para comparar",
|
||||
selectSourceTables:
|
||||
"Selecione primeiro a conexão e o banco de dados de origem, depois escolha as tabelas para comparar",
|
||||
noTables: "Nenhuma tabela disponível para comparação",
|
||||
autoMatchHint: "O modo em lote corresponde automaticamente as tabelas de destino pelo mesmo nome",
|
||||
matchedTables: "{matched}/{total} tabelas correspondidas",
|
||||
|
|
@ -1620,7 +1646,8 @@ export default {
|
|||
"Quando desativado, nenhum ícone é exibido, mas fechar a janela ainda oculta o DBX em segundo plano como antes.",
|
||||
dataGridDisplay: "Exibição da grade de dados",
|
||||
showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes",
|
||||
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnCommentsInHeaderDescription:
|
||||
"Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna",
|
||||
compactColumnHeaderActionsDescription:
|
||||
"Mover as ferramentas de formatação e filtro local para um menu de mais opções para que os nomes das colunas tenham prioridade.",
|
||||
|
|
@ -1675,7 +1702,8 @@ export default {
|
|||
snippetsAddTitle: "Adicionar snippet",
|
||||
snippetsEditTitle: "Editar snippet",
|
||||
syncWebDavTitle: "Sincronização WebDAV",
|
||||
syncWebDavDescription: "Envie ou restaure um snapshot do DBX a partir de um serviço de armazenamento compatível com WebDAV.",
|
||||
syncWebDavDescription:
|
||||
"Envie ou restaure um snapshot do DBX a partir de um serviço de armazenamento compatível com WebDAV.",
|
||||
syncEndpoint: "URL do WebDAV",
|
||||
syncUsername: "Nome de usuário",
|
||||
syncPassword: "Senha",
|
||||
|
|
@ -1703,7 +1731,8 @@ export default {
|
|||
syncDownloadSuccess: "Baixados e aplicados {bytes} bytes de {path}.",
|
||||
syncSecretsApplied: "Os segredos criptografados foram restaurados.",
|
||||
syncSecretsSkipped: "Havia segredos criptografados, mas não foram restaurados.",
|
||||
syncDownloadConfirm: "Baixar e aplicar o snapshot remoto do DBX? Os metadados locais e os SQL salvos serão substituídos.",
|
||||
syncDownloadConfirm:
|
||||
"Baixar e aplicar o snapshot remoto do DBX? Os metadados locais e os SQL salvos serão substituídos.",
|
||||
apply: "Aplicar",
|
||||
applyAndClose: "Aplicar e fechar",
|
||||
reset: "Redefinir",
|
||||
|
|
@ -1764,7 +1793,8 @@ export default {
|
|||
jdbcDeleteSuccess: "Driver removido",
|
||||
jdbcNoDrivers: "Nenhum driver JDBC importado ainda.",
|
||||
mcpTitle: "Servidor MCP",
|
||||
mcpDescription: "Verifique a instalação e a versão do Servidor MCP do DBX usado pelo Claude Code, Cursor e outros agentes.",
|
||||
mcpDescription:
|
||||
"Verifique a instalação e a versão do Servidor MCP do DBX usado pelo Claude Code, Cursor e outros agentes.",
|
||||
mcpChecking: "Verificando",
|
||||
mcpStatusUnknown: "Não verificado",
|
||||
mcpStatusError: "Falha na verificação",
|
||||
|
|
@ -1784,12 +1814,15 @@ export default {
|
|||
mcpCodexConfig: "Configuração do Codex",
|
||||
mcpCodexConfigPath: "O Codex pode usar ~/.codex/config.toml ou um .codex/config.toml no nível do projeto.",
|
||||
mcpReadonlyMode: "Modo somente leitura",
|
||||
mcpReadonlyModeDescription: "Adiciona DBX_MCP_ALLOW_WRITES=0 à configuração de exemplo para que a sessão MCP permaneça apenas para consultas.",
|
||||
mcpReadonlyModeDescription:
|
||||
"Adiciona DBX_MCP_ALLOW_WRITES=0 à configuração de exemplo para que a sessão MCP permaneça apenas para consultas.",
|
||||
mcpAllowDangerous: "Permitir SQL perigoso",
|
||||
mcpAllowDangerousDescription:
|
||||
"Adiciona DBX_MCP_ALLOW_DANGEROUS_SQL=1 à configuração de exemplo para que instruções como DROP, TRUNCATE, ALTER e similares sejam permitidas.",
|
||||
mcpDetectionTiming: "O DBX verifica automaticamente quando esta página é aberta; use Verificar novamente para atualizar.",
|
||||
mcpNpmBoundary: "O DBX apenas verifica e explica o status do MCP; a instalação e as atualizações ainda são feitas pelo npm.",
|
||||
mcpDetectionTiming:
|
||||
"O DBX verifica automaticamente quando esta página é aberta; use Verificar novamente para atualizar.",
|
||||
mcpNpmBoundary:
|
||||
"O DBX apenas verifica e explica o status do MCP; a instalação e as atualizações ainda são feitas pelo npm.",
|
||||
mcpRefresh: "Verificar novamente",
|
||||
mcpGuide: "Guia do MCP",
|
||||
aboutDescription: "Uma ferramenta de gerenciamento de banco de dados leve e de código aberto.",
|
||||
|
|
|
|||
|
|
@ -399,6 +399,10 @@ export default {
|
|||
rows: "{count} 行",
|
||||
totalRows: "共 {count} 行",
|
||||
totalRowCount: "(总计 {count} 行)",
|
||||
totalRowCountLoading: "(统计中...)",
|
||||
calculateTotalRows: "统计总行数",
|
||||
calculateTotalRowsInline: "(统计总行数)",
|
||||
calculateTotalRowsFailed: "统计失败:{message}",
|
||||
rowsAffected: "影响 {count} 行",
|
||||
querySuccess: "查询执行成功",
|
||||
noRows: "暂无数据",
|
||||
|
|
|
|||
|
|
@ -399,6 +399,10 @@ export default {
|
|||
rows: "{count} 列",
|
||||
totalRows: "共 {count} 筆",
|
||||
totalRowCount: "(總計 {count} 筆)",
|
||||
totalRowCountLoading: "(統計中...)",
|
||||
calculateTotalRows: "統計總筆數",
|
||||
calculateTotalRowsInline: "(統計總筆數)",
|
||||
calculateTotalRowsFailed: "統計失敗:{message}",
|
||||
rowsAffected: "影響 {count} 筆",
|
||||
querySuccess: "查詢執行成功",
|
||||
noRows: "暫無資料",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
export interface CanGoNextDataGridPageOptions {
|
||||
hasMore?: boolean;
|
||||
rowCount: number;
|
||||
pageSize: number;
|
||||
pageOffset?: number;
|
||||
currentPage?: number;
|
||||
totalRowCount?: number;
|
||||
}
|
||||
|
||||
export function canGoNextDataGridPage(options: CanGoNextDataGridPageOptions): boolean {
|
||||
if (options.hasMore === true) return true;
|
||||
|
||||
const pageSize = Math.max(1, options.pageSize);
|
||||
const totalRowCount = options.totalRowCount;
|
||||
if (typeof totalRowCount === "number" && Number.isFinite(totalRowCount) && totalRowCount >= 0) {
|
||||
const currentOffset =
|
||||
typeof options.pageOffset === "number" && options.pageOffset >= 0
|
||||
? options.pageOffset
|
||||
: Math.max(0, (options.currentPage ?? 1) - 1) * pageSize;
|
||||
return currentOffset + pageSize < totalRowCount;
|
||||
}
|
||||
|
||||
return options.rowCount >= pageSize;
|
||||
}
|
||||
|
|
@ -797,6 +797,83 @@ export const useQueryStore = defineStore("query", () => {
|
|||
})();
|
||||
}
|
||||
|
||||
function setQueryTotalRowCountIfCurrent(
|
||||
tabId: string,
|
||||
executionId: string,
|
||||
result: QueryResult,
|
||||
totalRowCount: number | undefined,
|
||||
) {
|
||||
const current = tabs.value.find((t) => t.id === tabId);
|
||||
if (current?.mode !== "query") return;
|
||||
if (current.executionId !== executionId && current.result !== result) return;
|
||||
current.resultTotalRowCount = totalRowCount;
|
||||
current.resultTotalRowCountLoading = false;
|
||||
}
|
||||
|
||||
function countQueryTotalRowsInBackground(options: {
|
||||
tabId: string;
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
countSql?: string;
|
||||
result: QueryResult;
|
||||
pageLimit?: number;
|
||||
pageOffset?: number;
|
||||
executionId: string;
|
||||
traceId: string;
|
||||
elapsed: () => string;
|
||||
timeoutSecs: number;
|
||||
}) {
|
||||
const resultRowCount = options.result.rows.length;
|
||||
if (!options.countSql || resultRowCount <= 0) {
|
||||
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined);
|
||||
return;
|
||||
}
|
||||
const countSql = options.countSql;
|
||||
|
||||
if (typeof options.pageLimit === "number" && resultRowCount < options.pageLimit) {
|
||||
setQueryTotalRowCountIfCurrent(
|
||||
options.tabId,
|
||||
options.executionId,
|
||||
options.result,
|
||||
(options.pageOffset ?? 0) + resultRowCount,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
console.info("[DBX][executeTabSql:count:start]", { traceId: options.traceId, elapsed: options.elapsed() });
|
||||
const countResult = await api.executeQuery(
|
||||
options.connectionId,
|
||||
options.database,
|
||||
countSql,
|
||||
options.schema,
|
||||
undefined,
|
||||
{ timeoutSecs: options.timeoutSecs },
|
||||
);
|
||||
const total = Number(countResult.rows?.[0]?.[0] ?? 0);
|
||||
if (!Number.isFinite(total) || total < 0) {
|
||||
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined);
|
||||
return;
|
||||
}
|
||||
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, total);
|
||||
console.info("[DBX][executeTabSql:count:done]", {
|
||||
traceId: options.traceId,
|
||||
total,
|
||||
elapsed: options.elapsed(),
|
||||
});
|
||||
} catch (error) {
|
||||
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined);
|
||||
console.warn("[DBX][executeTabSql:count:error]", {
|
||||
traceId: options.traceId,
|
||||
elapsed: options.elapsed(),
|
||||
error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async function executeTabSql(
|
||||
id: string,
|
||||
sql: string,
|
||||
|
|
@ -806,6 +883,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
pagination?: { limit: number; offset: number; sessionId?: string };
|
||||
mongoSafety?: MongoAggregateSafetyOptions;
|
||||
preserveResultDuringExecution?: boolean;
|
||||
preserveTotalRowCountDuringExecution?: boolean;
|
||||
},
|
||||
) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
|
|
@ -819,7 +897,10 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.isCancelling = false;
|
||||
tab.executionId = executionId;
|
||||
tab.lastExecutedSql = sql;
|
||||
tab.resultTotalRowCount = undefined;
|
||||
if (!options?.preserveTotalRowCountDuringExecution) {
|
||||
tab.resultTotalRowCount = undefined;
|
||||
}
|
||||
tab.resultTotalRowCountLoading = false;
|
||||
const previousResultSessionClose = closeResultSession(tab, options?.pagination?.sessionId);
|
||||
if (!options?.preserveResultDuringExecution || !tab.result) {
|
||||
clearResultPayload(tab);
|
||||
|
|
@ -1095,7 +1176,27 @@ export const useQueryStore = defineStore("query", () => {
|
|||
current.resultPageOffset = pageOffset;
|
||||
current.resultCountSql = countSql;
|
||||
current.resultSessionId = current.result?.session_id ?? undefined;
|
||||
if (!options?.preserveTotalRowCountDuringExecution) {
|
||||
current.resultTotalRowCount = undefined;
|
||||
}
|
||||
current.resultTotalRowCountLoading = current.mode === "query" && !!current.result && !!countSql;
|
||||
touchResult(current);
|
||||
if (current.mode === "query" && current.result) {
|
||||
countQueryTotalRowsInBackground({
|
||||
tabId: id,
|
||||
connectionId: current.connectionId,
|
||||
database: current.database,
|
||||
schema: current.schema,
|
||||
countSql,
|
||||
result: current.result,
|
||||
pageLimit,
|
||||
pageOffset,
|
||||
executionId,
|
||||
traceId,
|
||||
elapsed,
|
||||
timeoutSecs: queryTimeoutSecs,
|
||||
});
|
||||
}
|
||||
console.info("[DBX][executeTabSql:result:assigned]", {
|
||||
traceId,
|
||||
activeResultIndex: current.activeResultIndex,
|
||||
|
|
@ -1104,35 +1205,6 @@ export const useQueryStore = defineStore("query", () => {
|
|||
backendMs: current.result?.execution_time_ms,
|
||||
elapsed: elapsed(),
|
||||
});
|
||||
if (countSql && current.result?.rows.length) {
|
||||
// When the result set is smaller than the page size we already have
|
||||
// all rows — compute the total directly instead of running COUNT(*).
|
||||
const resultRowCount = current.result.rows.length;
|
||||
if (pageLimit !== undefined && resultRowCount < pageLimit) {
|
||||
current.resultTotalRowCount = (pageOffset ?? 0) + resultRowCount;
|
||||
} else {
|
||||
const capturedExecutionId = executionId;
|
||||
const capturedTabId = id;
|
||||
const capturedCountSql = countSql;
|
||||
const capturedConnectionId = tab.connectionId;
|
||||
const capturedDatabase = tab.database;
|
||||
const capturedSchema = tab.schema;
|
||||
api
|
||||
.executeQuery(capturedConnectionId, capturedDatabase ?? "", capturedCountSql, capturedSchema)
|
||||
.then((countResult) => {
|
||||
const tabAfterCount = tabs.value.find((t) => t.id === capturedTabId);
|
||||
if (tabAfterCount?.executionId === capturedExecutionId) {
|
||||
const total = Number(countResult.rows?.[0]?.[0] ?? 0);
|
||||
if (total > 0) {
|
||||
tabAfterCount.resultTotalRowCount = total;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// COUNT query failed — silently ignore
|
||||
});
|
||||
}
|
||||
}
|
||||
if (current.mode === "query" && current.result)
|
||||
analyzeQueryMetadataInBackground(id, queryBaseSql, current.result, traceId, elapsed);
|
||||
} else {
|
||||
|
|
@ -1160,6 +1232,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||
current.resultPageOffset = pageOffset;
|
||||
current.resultCountSql = countSql;
|
||||
current.resultSessionId = undefined;
|
||||
current.resultTotalRowCount = undefined;
|
||||
current.resultTotalRowCountLoading = false;
|
||||
touchResult(current);
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@ export interface QueryTab {
|
|||
resultPageOffset?: number;
|
||||
resultCountSql?: string;
|
||||
resultTotalRowCount?: number;
|
||||
resultTotalRowCountLoading?: boolean;
|
||||
resultSessionId?: string;
|
||||
resultAccessedAt?: number;
|
||||
pinned?: boolean;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { canGoNextDataGridPage } from "../../apps/desktop/src/lib/dataGridPagination.ts";
|
||||
|
||||
test("known total disables next page at the last exact page", () => {
|
||||
assert.equal(
|
||||
canGoNextDataGridPage({
|
||||
rowCount: 1,
|
||||
pageSize: 1,
|
||||
pageOffset: 8,
|
||||
totalRowCount: 9,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("known total allows next page before the last page", () => {
|
||||
assert.equal(
|
||||
canGoNextDataGridPage({
|
||||
rowCount: 1,
|
||||
pageSize: 1,
|
||||
pageOffset: 7,
|
||||
totalRowCount: 9,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("backend hasMore takes precedence over a stale known total", () => {
|
||||
assert.equal(
|
||||
canGoNextDataGridPage({
|
||||
hasMore: true,
|
||||
rowCount: 1,
|
||||
pageSize: 1,
|
||||
pageOffset: 8,
|
||||
totalRowCount: 9,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("unknown total falls back to full-page heuristic", () => {
|
||||
assert.equal(canGoNextDataGridPage({ rowCount: 1, pageSize: 1 }), true);
|
||||
assert.equal(canGoNextDataGridPage({ rowCount: 0, pageSize: 1 }), false);
|
||||
});
|
||||
|
|
@ -1167,6 +1167,176 @@ test("query execution is scoped to the tab client session", async () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("query execution keeps automatically counting total rows in the background", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const connectionStore = useConnectionStore();
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
connectionStore.addEphemeralConnection(conn("conn-1"));
|
||||
const tabId = store.createTab("conn-1", "db", "Query", "query", "public");
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
assert.ok(tab);
|
||||
|
||||
let resolveCount: ((value: Response) => void) | undefined;
|
||||
let countBody: any;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/prepare-pagination-plan") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
sqlToExecute: "select id from users limit 100",
|
||||
pageSql: "select id from users limit 100",
|
||||
pageLimit: 100,
|
||||
pageOffset: 0,
|
||||
countSql: "select count(*) from users",
|
||||
useAgentResultSession: false,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/execute-multi") {
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: Array.from({ length: 100 }, (_, index) => [index + 1]),
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
]),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/execute") {
|
||||
countBody = JSON.parse(String(init?.body ?? "{}"));
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveCount = resolve;
|
||||
});
|
||||
}
|
||||
if (url === "/api/query/analyze-editability") {
|
||||
return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
await store.executeTabSql(tabId, "select id from users");
|
||||
|
||||
assert.equal(tab.executionId, undefined);
|
||||
assert.equal(tab.resultTotalRowCount, undefined);
|
||||
assert.equal(tab.resultTotalRowCountLoading, true);
|
||||
assert.equal(countBody.sql, "select count(*) from users");
|
||||
assert.equal(countBody.schema, "public");
|
||||
|
||||
resolveCount?.(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
columns: ["count"],
|
||||
rows: [[250]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
await waitFor(() => tab.resultTotalRowCount === 250);
|
||||
assert.equal(tab.resultTotalRowCountLoading, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("paginated query execution keeps the previous total while refreshing it in the background", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const connectionStore = useConnectionStore();
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
connectionStore.addEphemeralConnection(conn("conn-1"));
|
||||
const tabId = store.createTab("conn-1", "db", "Query", "query", "public");
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
assert.ok(tab);
|
||||
tab.resultTotalRowCount = 250;
|
||||
|
||||
let resolveCount: ((value: Response) => void) | undefined;
|
||||
globalThis.fetch = (async (input) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/prepare-pagination-plan") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
sqlToExecute: "select id from users limit 100 offset 100",
|
||||
pageSql: "select id from users limit 100 offset 100",
|
||||
pageLimit: 100,
|
||||
pageOffset: 100,
|
||||
countSql: "select count(*) from users",
|
||||
useAgentResultSession: false,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/execute-multi") {
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: Array.from({ length: 100 }, (_, index) => [index + 101]),
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
]),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/execute") {
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveCount = resolve;
|
||||
});
|
||||
}
|
||||
if (url === "/api/query/analyze-editability") {
|
||||
return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
await store.executeTabSql(tabId, "select id from users", {
|
||||
pagination: { limit: 100, offset: 100 },
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
});
|
||||
|
||||
assert.equal(tab.resultTotalRowCount, 250);
|
||||
assert.equal(tab.resultTotalRowCountLoading, true);
|
||||
|
||||
resolveCount?.(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
columns: ["count"],
|
||||
rows: [[275]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
await waitFor(() => tab.resultTotalRowCount === 275);
|
||||
assert.equal(tab.resultTotalRowCountLoading, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("multi statement execution shows the first result set by default", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
|
|
|
|||
Loading…
Reference in New Issue