feat(elasticsearch): add SQL-model execution with mapping-aware completion

- Route SELECT * FROM <index> through /_search for simple browses; aggregates/projections stay on /_sql
- Drive completion columns from /_mapping (flattened dotted fields, multi-fields)
- Switch editor completion to SQL when the active statement starts with SELECT/WITH on ES
- Skip generic SQL semantic diagnostics for ES (hyphenated wildcard indices caused 500s)
- Wire ES into server-side pagination plan; derive completion context from line blocks
- Fix result-grid pagination exceeding in-memory result sets (allRowsLoaded)
This commit is contained in:
Yong Studt authored and GitHub committed 2026-06-08 17:53:05 +08:00
1 parent 465ed195cf
commit 2628fb8374
11 files changed
+749 -26

No files matched your search

@@ -29,6 +29,7 @@ import {
getSqlFunctionSignatureHelp,
getSqlCompletionContext,
getSqlCompletionResultValidFor,
isSqlLikeCompletionStatement,
shouldAutoOpenSqlCompletion,
extractCteDefinitions,
} from "@/lib/sqlCompletion";
@@ -739,7 +740,13 @@ async function refreshSemanticDiagnostics() {
setSemanticDiagnostics([]);
return;
}
if (!shouldRunSqlSemanticDiagnostics(sql, currentView.state.selection.main.head)) {
if (props.databaseType === "elasticsearch") {
setSemanticDiagnostics([]);
return;
}
if (
!shouldRunSqlSemanticDiagnostics(sql, currentView.state.selection.main.head, { databaseType: props.databaseType })
) {
scheduleSemanticDiagnostics(1200);
return;
}
@@ -933,15 +940,17 @@ async function provideSqlCompletions(
explicit: boolean,
) {
if (!props.connectionId) return null;
const fullDoc = currentState.doc.toString();
if (props.databaseType === "elasticsearch") {
return provideElasticsearchCompletions(currentState, position, explicit);
if (!isSqlLikeCompletionStatement(fullDoc, position)) {
return provideElasticsearchCompletions(currentState, position, explicit);
}
}
const hasDatabase = props.database != null;
const epoch = ++completionEpoch;
try {
const fullDoc = currentState.doc.toString();
if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position)) return null;
const completionContext = getSqlCompletionContext(fullDoc, position);
+35 -3
View File
@@ -1857,10 +1857,30 @@ 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);
// affected_rows reported by the backend can be larger than the rows we
// actually have in memory — e.g. ES auto-pages SELECT * on a big index and
// reports the index's true match count. Surface that in the status bar so
// the user sees the real total, but do NOT use it to unlock pagination:
// we don't have those rows, so letting the user page into them would just
// show blank screens.
const inferredBackendTotalRowCount = computed(() => {
const affected = props.result.affected_rows;
if (typeof affected !== "number" || !Number.isFinite(affected)) return undefined;
if (affected <= props.result.rows.length) return undefined;
return affected;
});
const serverKnownTotalRowCount = computed(() => props.totalRowCount ?? manualTotalRowCount.value);
const displayedTotalRowCount = computed(() => serverKnownTotalRowCount.value ?? inferredBackendTotalRowCount.value);
// Only a server-confirmed total drives pagination — an inferred total means
// rows exist that we never fetched, so navigation must stay inside rows.length.
const hasKnownTotalRowCount = computed(
() => typeof displayedTotalRowCount.value === "number" && displayedTotalRowCount.value >= 0,
() => typeof serverKnownTotalRowCount.value === "number" && serverKnownTotalRowCount.value >= 0,
);
// When context=results and the caller hasn't configured server-side
// pagination (no pageLimit), the backend handed us every row up-front and
// rowCount IS the total. Without this hint, the "page is full → assume more"
// fallback in canGoNextDataGridPage lets the user keep clicking next forever.
const allRowsLoaded = computed(() => isResultsContext.value && props.pageLimit === undefined);
const canGoNextPage = computed(() => {
return canGoNextDataGridPage({
hasMore: props.result.has_more,
@@ -1869,10 +1889,13 @@ const canGoNextPage = computed(() => {
pageOffset: props.pageOffset,
currentPage: currentPage.value,
totalRowCount: hasKnownTotalRowCount.value ? displayedTotalRowCount.value : undefined,
allRowsLoaded: allRowsLoaded.value,
});
});
const canJumpLastPage = computed(
() => canGoNextPage.value && (hasKnownTotalRowCount.value || !!props.tableMeta || !!props.countSql),
() =>
canGoNextPage.value &&
(hasKnownTotalRowCount.value || allRowsLoaded.value || !!props.tableMeta || !!props.countSql),
);
const totalRowCountBusy = computed(() => props.totalRowCountLoading === true || manualTotalRowCountLoading.value);
const canCalculateTotalRowCount = computed(
@@ -2015,6 +2038,15 @@ async function lastPage() {
emit("paginate", (lastPageNum - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy());
return;
}
if (allRowsLoaded.value) {
const total = props.result.rows.length;
if (total <= 0) return;
const lastPageNum = Math.ceil(total / pageSize.value);
if (lastPageNum <= currentPage.value) return;
currentPage.value = lastPageNum;
resetGridVerticalScroll(true);
return;
}
if (!props.connectionId) return;
const countTarget = await buildCurrentCountTarget();
const sql = countTarget?.sql;