fix(elasticsearch): complete mapping fields in Query DSL field slots

This commit is contained in:
onenewcode authored and GitHub committed 2026-09-17 11:48:34 +08:00
1 parent dd84e6c06a
commit c0d6965b5f
5 files changed
+628 -58

No files matched your search

@@ -88,7 +88,15 @@ import { driverProfileHasCompletionCandidates } from "@/lib/database/driverProfi
import { sqlCompletionContextFromSemantic, sqlSemanticSelectStarIsOnlyProjection, sqlSemanticSelectStarQualifierSql, sqlSemanticSelectStarTableSources } from "@/lib/sql/semantic/completion";
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
import {
buildElasticsearchCompletionItemsFromContext,
elasticsearchCompletionNeedsFields,
getElasticsearchCompletionContext,
getElasticsearchCompletionResultValidFor,
shouldAutoOpenElasticsearchCompletion,
type ElasticsearchCompletionField,
type ElasticsearchCompletionItem,
} from "@/lib/elasticsearch/elasticsearchCompletion";
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, mongoCompletionNeedsCollections, mongoCompletionNeedsFields, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
import {
buildSqlServerUseDatabaseCompletionItems,
@@ -4526,9 +4534,11 @@ function shouldApplyCompletionAsSnippet(item: QueryCompletionItem): boolean {
function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectionActionItem) {
const filterText = "filterText" in item && typeof item.filterText === "string" ? item.filterText : undefined;
const labelPresentation = completionLabelPresentation(item.label, filterText);
const sortText = "sortText" in item && typeof item.sortText === "string" ? item.sortText : labelPresentation.sortText;
if (isBatchColumnSelectionAction(item)) {
return {
...labelPresentation,
...(sortText ? { sortText } : {}),
dbxBatchColumnSelectionAction: { sessionKey: item.sessionKey },
type: item.type,
detail: item.detail,
@@ -4545,6 +4555,7 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
if (shouldApplyCompletionAsSnippet(item) && item.apply) {
const completion = codeMirrorSnippetCompletion(item.apply, {
...labelPresentation,
...(sortText ? { sortText } : {}),
type: item.type,
detail: item.detail,
info: item.info,
@@ -4578,6 +4589,7 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
}
return cacheBatchColumnSelectionOption(batchColumnSelection, {
...labelPresentation,
...(sortText ? { sortText } : {}),
...(batchColumnSelection ? { dbxBatchColumnSelection: batchColumnSelection } : {}),
type: item.type,
detail: item.detail,
@@ -4612,6 +4624,7 @@ async function provideElasticsearchCompletions(currentState: import("@codemirror
const completionContext = getElasticsearchCompletionContext(fullDoc, position);
let indices: string[] = [];
let fields: ElasticsearchCompletionField[] = [];
if (props.database != null && completionContext.mode === "path") {
try {
indices = await connectionStore.listElasticsearchCompletionIndices(props.connectionId, props.database);
@@ -4619,9 +4632,16 @@ async function provideElasticsearchCompletions(currentState: import("@codemirror
indices = [];
}
}
if (elasticsearchCompletionNeedsFields(completionContext) && completionContext.index) {
try {
fields = await connectionStore.listElasticsearchCompletionFields(props.connectionId, completionContext.index);
} catch {
fields = [];
}
}
if (epoch !== completionEpoch) return null;
const items = buildElasticsearchCompletionItemsFromContext(completionContext, { indices });
const items = buildElasticsearchCompletionItemsFromContext(completionContext, { indices, fields });
return buildCompletionResult(items, completionContext.from, getElasticsearchCompletionResultValidFor());
}
@@ -4,11 +4,12 @@ export type ElasticsearchJsonSlot = "key" | "value";
export interface ElasticsearchCompletionItem {
label: string;
type: "keyword" | "property" | "text" | "snippet";
type: "keyword" | "property" | "text" | "snippet" | "column";
detail?: string;
info?: string;
apply?: string;
filterText?: string;
sortText?: string;
// Set when `apply` carries snippet fields so the editor expands it instead of
// inserting the raw `${}` markers.
applyAsSnippet?: boolean;
@@ -17,6 +18,11 @@ export interface ElasticsearchCompletionItem {
boost: number;
}
export interface ElasticsearchCompletionField {
name: string;
dataType?: string;
}
export interface ElasticsearchCompletionContext {
mode: ElasticsearchCompletionMode;
prefix: string;
@@ -26,12 +32,15 @@ export interface ElasticsearchCompletionContext {
segmentIndex?: number;
replaceClosingQuote?: '"';
jsonSlot?: ElasticsearchJsonSlot;
jsonFieldSlot?: boolean;
hasKeySeparator?: boolean;
jsonIndent?: string;
index?: string;
}
export interface ElasticsearchCompletionInput {
indices?: string[];
fields?: ElasticsearchCompletionField[];
}
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "HEAD"] as const;
@@ -76,6 +85,20 @@ const JSON_KEYWORDS = ["query", "bool", "must", "should", "must_not", "filter",
const JSON_ARRAY_KEYS = new Set(["must", "should", "must_not", "filter", "sort", "_source", "fields"]);
const JSON_SCALAR_KEYS = new Set(["size", "from", "track_total_hits"]);
// Query-clause objects whose keys are mapping field names (`"term": { "status": ... }`).
const FIELD_NAME_OBJECT_KEYS = new Set(["term", "terms", "match", "match_phrase", "match_phrase_prefix", "match_bool_prefix", "range", "wildcard", "prefix", "fuzzy", "regexp", "span_term"]);
// `terms`/`range` are also aggregation types whose keys are `field`/`ranges`, not mapping names.
const AGGREGATION_TYPE_FIELD_OBJECT_KEYS = new Set(["terms", "range"]);
const AGGREGATION_KEYS = new Set(["aggs", "aggregations"]);
const QUERY_CLAUSE_KEYS = new Set(["query", "bool", "must", "should", "filter", "must_not", "nested", "constant_score"]);
// Arrays whose values (or object-element keys) are mapping field names.
const FIELD_NAME_ARRAY_KEYS = new Set(["sort", "_source", "fields"]);
// Object keys whose JSON value is a mapping field name (`"exists": { "field": "status" }`).
const FIELD_VALUE_KEYS = new Set(["field", "path"]);
// Value scaffold inserted after a completed key so the matching brackets come
// along with it, with the cursor parked where the value goes.
function jsonKeyValueTemplate(key: string): string {
@@ -170,14 +193,17 @@ export function getElasticsearchCompletionContext(text: string, cursor: number):
if (lineStart > 0 || safeCursor > firstLineLimit || looksLikeJsonBody(text, safeCursor)) {
const jsonPrefix = readJsonPrefix(text, safeCursor);
const replaceClosingQuote = jsonPrefix.prefix.startsWith('"') && text[safeCursor] === '"' ? ('"' as const) : undefined;
const inspection = inspectJsonBody(text, jsonPrefix.from);
return {
mode: "json",
prefix: jsonPrefix.prefix,
from: jsonPrefix.from,
replaceClosingQuote,
jsonSlot: readJsonSlot(text, jsonPrefix.from),
jsonSlot: inspection.slot,
jsonFieldSlot: isJsonFieldNameSlot(inspection),
hasKeySeparator: hasJsonKeySeparator(text, safeCursor),
jsonIndent: beforeCursorOnLine.match(/^\s*/)?.[0] ?? "",
index: elasticsearchRequestIndex(text, safeCursor),
};
}
@@ -192,20 +218,81 @@ export function buildElasticsearchCompletionItems(text: string, cursor: number,
export function buildElasticsearchCompletionItemsFromContext(context: ElasticsearchCompletionContext, input: ElasticsearchCompletionInput = {}): ElasticsearchCompletionItem[] {
if (context.mode === "method") return methodItems(context.prefix);
if (context.mode === "json") return jsonItems(context);
if (context.mode === "json") return jsonItems(context, input.fields ?? []);
return pathItems(context, input.indices ?? []);
}
export function elasticsearchCompletionNeedsFields(context: ElasticsearchCompletionContext): boolean {
return context.mode === "json" && context.jsonFieldSlot === true;
}
const MAX_MAPPING_FIELDS = 500;
/** Flatten `GET /{index}/_mapping` into completion fields. Iterative on purpose: a recursive walk is the wrong model for this JSON. */
export function flattenElasticsearchMappingFields(mapping: unknown): ElasticsearchCompletionField[] {
const properties = elasticsearchMappingProperties(mapping);
if (!properties) return [];
const fields: ElasticsearchCompletionField[] = [];
const seenNames = new Set<string>();
const seenObjects = new Set<Record<string, unknown>>();
const stack: Array<{ prefix: string; properties: Record<string, unknown> }> = [{ prefix: "", properties }];
while (stack.length > 0 && fields.length < MAX_MAPPING_FIELDS) {
const current = stack.pop();
if (!current || seenObjects.has(current.properties)) continue;
seenObjects.add(current.properties);
for (const [name, definition] of Object.entries(current.properties)) {
if (fields.length >= MAX_MAPPING_FIELDS) break;
if (!isJsonObject(definition)) continue;
const fieldName = current.prefix ? `${current.prefix}.${name}` : name;
const fieldType = typeof definition.type === "string" ? definition.type : isJsonObject(definition.properties) ? "object" : undefined;
if (fieldType && !seenNames.has(fieldName)) {
seenNames.add(fieldName);
fields.push({ name: fieldName, dataType: fieldType });
}
// Only mapping subtrees: `properties` (object/nested) and `fields` (multi-fields).
// Never walk `index_options`, `path`, analyzer settings, etc.
if (isJsonObject(definition.properties)) stack.push({ prefix: fieldName, properties: definition.properties });
if (isJsonObject(definition.fields)) stack.push({ prefix: fieldName, properties: definition.fields });
}
}
return fields;
}
function isJsonObject(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function elasticsearchMappingProperties(mapping: unknown): Record<string, unknown> | undefined {
if (!isJsonObject(mapping)) return undefined;
const collected: Record<string, unknown> = {};
const consider = (node: unknown) => {
if (!isJsonObject(node)) return;
const mappings = isJsonObject(node.mappings) ? node.mappings : node;
if (isJsonObject(mappings.properties)) {
Object.assign(collected, mappings.properties);
return;
}
for (const typed of Object.values(mappings)) {
if (isJsonObject(typed) && isJsonObject(typed.properties)) Object.assign(collected, typed.properties);
}
};
if (isJsonObject(mapping.mappings)) consider(mapping);
else for (const value of Object.values(mapping)) consider(value);
return Object.keys(collected).length > 0 ? collected : undefined;
}
export function shouldAutoOpenElasticsearchCompletion(text: string, cursor: number): boolean {
const previousChar = text[cursor - 1];
if (!previousChar) return false;
if (/[{,}\]\n\r]/.test(previousChar)) return false;
if (/[\w/_."]/.test(previousChar)) return true;
if (/[\w/_.".-]/.test(previousChar)) return true;
return false;
}
export function getElasticsearchCompletionResultValidFor(): RegExp {
return /[\w/_."]*$/;
return /[\w/_."-]*$/;
}
function methodItems(prefix: string): ElasticsearchCompletionItem[] {
@@ -274,7 +361,8 @@ function indexEndpointItems(prefix: string): ElasticsearchCompletionItem[] {
}));
}
function jsonItems(context: ElasticsearchCompletionContext): ElasticsearchCompletionItem[] {
function jsonItems(context: ElasticsearchCompletionContext, fields: ElasticsearchCompletionField[]): ElasticsearchCompletionItem[] {
if (context.jsonFieldSlot) return fieldItems(context, fields);
const quoted = context.prefix.startsWith('"');
const normalizedPrefix = context.prefix.replace(/^"/, "");
// Only an object key without its own `:` yet may pull the value scaffold in;
@@ -306,6 +394,32 @@ function jsonItems(context: ElasticsearchCompletionContext): ElasticsearchComple
return dedupeAndSort([...snippetItems, ...keyItems]);
}
function fieldItems(context: ElasticsearchCompletionContext, fields: ElasticsearchCompletionField[]): ElasticsearchCompletionItem[] {
const quoted = context.prefix.startsWith('"');
const normalizedPrefix = context.prefix.replace(/^"/, "");
return fields
.filter((field) => matchesElasticsearchFieldPrefix(field.name, normalizedPrefix))
.map((field) => {
const depth = field.name.split(".").length;
return {
label: `"${field.name}"`,
type: "column" as const,
detail: field.dataType || "field",
apply: `"${field.name}"`,
// Quoted labels cannot prefix-match an unquoted typed token (`ci` vs `"city"`),
// so expose the bare field name as filterText for CodeMirror's matcher.
filterText: quoted ? `"${field.name}"` : field.name,
replaceClosingQuote: context.replaceClosingQuote,
// Shallower paths first. Default editor sort uses localeCompare on labels,
// which puts `"nested_comments"` *after* its children; sortText avoids that.
sortText: `${String(depth).padStart(2, "0")}\t${field.name}`,
boost: 200 - depth * 20,
};
})
.sort((left, right) => right.boost - left.boost || (left.sortText ?? left.label).localeCompare(right.sortText ?? right.label))
.slice(0, 100);
}
function indentJsonSnippet(snippet: string, baseIndent: string): string {
if (!baseIndent) return snippet;
return snippet.replace(/\n/g, `\n${baseIndent}`);
@@ -328,25 +442,169 @@ function hasOpenJsonContainer(text: string, cursor: number): boolean {
return stack.length > 0;
}
// Tracks open brackets so an array element or a value after `:` is never
// mistaken for an object key.
function readJsonSlot(text: string, from: number): ElasticsearchJsonSlot {
const stack: string[] = [];
interface JsonFrame {
kind: "{" | "[";
key: string | null;
}
interface JsonInspection {
slot: ElasticsearchJsonSlot;
parentKey: string | null;
parentKind: "{" | "[" | null;
nearestNamedKey: string | null;
currentKey: string | null;
ancestorKeys: string[];
}
// Walks the current REST request body while keeping object keys, so a field
// name inside `"term": { | }` is not treated the same as a DSL key inside `"bool"`.
function inspectJsonBody(text: string, from: number): JsonInspection {
const before = text.slice(0, from);
const requestLine = /[\s\S]*(?:^|\n)[ \t]*(?:GET|POST|PUT|DELETE|HEAD)[ \t][^\n]*/i.exec(before);
const bodyStart = requestLine?.[0].length ?? 0;
const stack: JsonFrame[] = [];
let pendingKey: string | null = null;
let last = "";
for (const char of readRequestBody(text, from)) {
if (/\s/.test(char)) continue;
if (char === "{" || char === "[") stack.push(char);
else if (char === "}" || char === "]") stack.pop();
let index = bodyStart;
while (index < from) {
const char = before[index] ?? "";
if (char === "#" || (char === "/" && before[index + 1] === "/")) {
const newline = before.indexOf("\n", index);
index = newline < 0 ? from : newline;
continue;
}
if (char === '"') {
const string = readJsonString(before, index, from);
if (string.end < 0) break;
index = string.end;
let lookahead = index;
while (lookahead < from && (before[lookahead] === " " || before[lookahead] === "\t")) lookahead++;
if (before[lookahead] === ":") {
pendingKey = string.value;
continue;
}
last = '"';
continue;
}
if (/\s/.test(char)) {
index++;
continue;
}
if (char === "{" || char === "[") {
stack.push({ kind: char, key: pendingKey });
pendingKey = null;
last = char;
index++;
continue;
}
if (char === "}" || char === "]") {
stack.pop();
pendingKey = null;
last = char;
index++;
continue;
}
if (char === ",") {
pendingKey = null;
last = char;
index++;
continue;
}
last = char;
index++;
}
const top = stack[stack.length - 1];
const slot: ElasticsearchJsonSlot = top?.kind === "{" && (last === "{" || last === ",") ? "key" : "value";
let nearestNamedKey: string | null = null;
for (let depth = stack.length - 1; depth >= 0; depth--) {
const key = stack[depth]?.key;
if (key) {
nearestNamedKey = key;
break;
}
}
return {
slot,
parentKey: top?.key ?? null,
parentKind: top?.kind ?? null,
nearestNamedKey,
currentKey: slot === "value" ? pendingKey : null,
ancestorKeys: stack.map((frame) => frame.key).filter((key): key is string => !!key),
};
}
function readJsonString(text: string, start: number, limit: number): { end: number; value: string } {
let index = start + 1;
let escaped = false;
let value = "";
while (index < limit) {
const char = text[index] ?? "";
if (escaped) {
value += char;
escaped = false;
index++;
continue;
}
if (char === "\\") {
escaped = true;
index++;
continue;
}
if (char === '"') return { end: index + 1, value };
value += char;
index++;
}
return { end: -1, value };
}
function isJsonFieldNameSlot(inspection: JsonInspection): boolean {
if (inspection.slot === "key") {
if (inspection.parentKey && FIELD_NAME_OBJECT_KEYS.has(inspection.parentKey)) {
return !isAggregationTypeClause(inspection);
}
// Object-form sort (`"sort": { "createdAt": "desc" }`) uses field names as keys.
// Array-form sort objects (`"sort": [{ "createdAt": ... }]`) have no key of their
// own; `_source`/`fields` objects do not (`includes`/`field`/`format`).
if (inspection.parentKey === "sort") return true;
return inspection.parentKind === "{" && !inspection.parentKey && inspection.nearestNamedKey === "sort";
}
if (inspection.currentKey && FIELD_VALUE_KEYS.has(inspection.currentKey)) return true;
return inspection.parentKind === "[" && !!inspection.parentKey && FIELD_NAME_ARRAY_KEYS.has(inspection.parentKey);
}
function isAggregationTypeClause(inspection: JsonInspection): boolean {
if (!inspection.parentKey || !AGGREGATION_TYPE_FIELD_OBJECT_KEYS.has(inspection.parentKey)) return false;
if (!inspection.ancestorKeys.some((key) => AGGREGATION_KEYS.has(key))) return false;
return !inspection.ancestorKeys.some((key) => QUERY_CLAUSE_KEYS.has(key));
}
function elasticsearchRequestIndex(text: string, cursor: number): string | undefined {
const match = /[\s\S]*(?:^|\n)[ \t]*(?:GET|POST|PUT|DELETE|HEAD)[ \t]+(\S+)/i.exec(text.slice(0, cursor));
const path = match?.[1];
if (!path) return undefined;
const first = (path.split("?")[0] ?? "").split("/").filter(Boolean)[0];
if (!first || first.startsWith("_")) return undefined;
try {
return decodeURIComponent(first);
} catch {
return first;
}
return stack[stack.length - 1] === "{" && (last === "{" || last === ",") ? "key" : "value";
}
function hasJsonKeySeparator(text: string, cursor: number): boolean {
let index = cursor;
// The caret may sit inside the key, so step over the rest of the token and
// its closing quote before looking for the separator.
while (index < text.length && /[\w_]/.test(text[index] ?? "")) index++;
while (index < text.length && /[\w_.-]/.test(text[index] ?? "")) index++;
if (text[index] === '"') index++;
while (text[index] === " " || text[index] === "\t") index++;
return text[index] === ":";
@@ -359,7 +617,7 @@ function looksLikeJsonBody(text: string, cursor: number): boolean {
function readJsonPrefix(text: string, cursor: number): { prefix: string; from: number } {
let from = cursor;
while (from > 0 && /[\w_"]/.test(text[from - 1] ?? "")) from--;
while (from > 0 && /[\w_.\-"]/.test(text[from - 1] ?? "")) from--;
return { prefix: text.slice(from, cursor), from };
}
@@ -391,6 +649,15 @@ function matchesFuzzyPrefix(value: string, prefix: string): boolean {
return !normalizedPrefix || normalizedValue.includes(normalizedPrefix);
}
// Kibana Console: flatten mapping to dotted paths, then prefix-filter the full name.
// `"ne"` matches `nested_comments` and `nested_comments.user`; `"nested_comments."` keeps only children.
// Do not use substring match (`rf_hotness` must not appear for `"ne"`).
function matchesElasticsearchFieldPrefix(fieldName: string, prefix: string): boolean {
const name = fieldName.toLowerCase();
const typed = prefix.toLowerCase();
return !typed || name.startsWith(typed);
}
function dedupeAndSort(items: ElasticsearchCompletionItem[]): ElasticsearchCompletionItem[] {
const seen = new Set<string>();
const deduped: ElasticsearchCompletionItem[] = [];
@@ -138,6 +138,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { decorateDatabaseSavedSqlTreeNodes, indexSavedSqlFilesByDatabase, stripDatabaseSavedSqlTreeNodes, withDatabaseSavedSqlRoot } from "@/lib/savedSql/savedSqlDatabaseTree";
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
import { inferMongoCompletionFields, type MongoCompletionField } from "@/lib/mongo/mongoCompletion";
import { flattenElasticsearchMappingFields, type ElasticsearchCompletionField } from "@/lib/elasticsearch/elasticsearchCompletion";
import { isMongoLegacyDriverProfile } from "@/lib/mongo/mongoCapabilities";
import { mongoCollectionKindFromNode, toMongoCollectionKind, visibleMongoCollections } from "@/lib/sidebar/mongoCollectionMutation";
import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/metadata/completionTreeIndex";
@@ -486,6 +487,7 @@ export const useConnectionStore = defineStore("connection", () => {
const primaryVisibleObjectNames = ref<Record<string, string[]>>({});
const sqlServerCompletionContextCache = ref<Record<string, SqlServerCompletionContext>>({});
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
const elasticsearchCompletionFieldsCache = ref<Record<string, ElasticsearchCompletionField[]>>({});
const redisCompletionKeysCache = ref<Record<string, string[]>>({});
const redisCommandDocsCache = ref<Record<string, RedisCommandDocumentation[]>>({});
const redisCommandDocsCacheGeneration = new Map<string, number>();
@@ -3511,6 +3513,9 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of Object.keys(elasticsearchCompletionIndicesCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete elasticsearchCompletionIndicesCache.value[key];
}
for (const key of Object.keys(elasticsearchCompletionFieldsCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete elasticsearchCompletionFieldsCache.value[key];
}
for (const key of Object.keys(redisCompletionKeysCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete redisCompletionKeysCache.value[key];
}
@@ -7975,6 +7980,21 @@ export const useConnectionStore = defineStore("connection", () => {
return elasticsearchCompletionIndicesCache.value[cacheKey];
}
async function listElasticsearchCompletionFields(connectionId: string, index: string): Promise<ElasticsearchCompletionField[]> {
if (!index) return [];
const cacheKey = `${connectionId}:${index}`;
const cached = elasticsearchCompletionFieldsCache.value[cacheKey];
if (cached) return cached;
return withCompletionInFlight(`${cacheKey}:es-fields`, async () => {
await ensureConnected(connectionId);
const mapping = await api.elasticsearchGetIndexMetadata(connectionId, index, "mapping");
const fields = flattenElasticsearchMappingFields(mapping);
elasticsearchCompletionFieldsCache.value[cacheKey] = fields;
evictOldestCacheEntries(elasticsearchCompletionFieldsCache.value, COMPLETION_CACHE_MAX);
return fields;
});
}
// Upper bound on cached key names per db, to keep completion memory bounded
// (Redis can hold far more keys than we ever want resident for autocomplete).
const REDIS_COMPLETION_KEYS_MAX = 1000;
@@ -9388,6 +9408,7 @@ export const useConnectionStore = defineStore("connection", () => {
refreshCompletionSchemas,
refreshCompletionDatabases,
listElasticsearchCompletionIndices,
listElasticsearchCompletionFields,
listRedisCompletionKeys,
listRedisCompletionCommandDocs,
listMongoCompletionCollections,