feat(meilisearch): add native document database support

This commit is contained in:
t8y2 committed 2026-08-13 02:59:56 +08:00
1 parent 292d0c8b54
commit a740e2e9ce
79 files changed
+1801 -170

No files matched your search

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { documentFilterModeOptionsFor, documentStoreProviderFor } from "@/lib/app/documentStoreProvider";
describe("Meilisearch document store provider", () => {
it("uses a dedicated provider and describes the DBX fetch operation", () => {
const provider = documentStoreProviderFor("meilisearch");
expect(provider.kind).toBe("meilisearch");
expect(
provider.queryPreview({
collection: "movies",
filterJson: '{"status":"published"}',
sortJson: '{"rating":-1}',
skip: 20,
limit: 10,
}),
).toBe('DBX MEILISEARCH FETCH DOCUMENTS\nindex: "movies"\noffset: 20\nlimit: 10\nfilter:\n{\n "status": "published"\n}\nsort:\n{\n "rating": -1\n}');
});
it("does not offer structured contains modes by default", () => {
expect(documentFilterModeOptionsFor("meilisearch").map((option) => option.value)).not.toEqual(expect.arrayContaining(["like", "not-like"]));
expect(documentFilterModeOptionsFor("elasticsearch").map((option) => option.value)).toEqual(expect.arrayContaining(["like", "not-like"]));
});
});
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from "vitest";
import { serializeDocumentStoreId } from "@/lib/app/documentJsonValues";
import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview } from "@/lib/app/documentStoreSave";
import { parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
describe("Meilisearch document saves", () => {
it("preserves the distinction between string and numeric document ids", () => {
expect(serializeDocumentStoreId("001", "meilisearch")).toBe('__dbx_meilisearch_string_id__"001"');
expect(serializeDocumentStoreId(1, "meilisearch")).toBe("1");
expect(serializeDocumentStoreId(parseJsonPreservingLargeNumbers("9007199254740993"), "meilisearch")).toBe("9007199254740993");
});
it("describes DBX write semantics without inventing a native primary-key field", () => {
const preview = formatMeilisearchDocumentOperationPreview({
action: "update",
index: "movies",
id: "001",
document: { title: "Arrival", rating: 9 },
});
expect(preview).toContain("DBX MEILISEARCH UPDATE DOCUMENT");
expect(preview).toContain('index: "movies"');
expect(preview).toContain('id: "001"');
expect(preview).toContain('"title": "Arrival"');
expect(preview).not.toContain("PUT /indexes/");
});
it("writes the new identity before deleting the old document", async () => {
const calls: string[] = [];
const update = vi.fn(async (id: string) => {
calls.push(`update:${id}`);
return 1;
});
const remove = vi.fn(async (id: string) => {
calls.push(`delete:${id}`);
return 1;
});
await applyDocumentStoreIdentityPlan({
kind: "meilisearch",
plan: {
action: "rekey",
writeId: serializeDocumentStoreId("002", "meilisearch"),
deleteId: serializeDocumentStoreId("001", "meilisearch"),
},
document: { _id: "002", title: "Arrival" },
apis: {
insert: vi.fn(async () => ""),
update,
delete: remove,
},
});
expect(calls).toEqual(['update:__dbx_meilisearch_string_id__"002"', 'delete:__dbx_meilisearch_string_id__"001"']);
expect(update).toHaveBeenCalledWith('__dbx_meilisearch_string_id__"002"', '{"title":"Arrival"}', undefined);
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
describe("Meilisearch connection URLs", () => {
it("parses the dedicated scheme with the default port and API key", () => {
expect(parseConnectionUrl("meilisearch://:secret@search.example.com")).toMatchObject({
dbType: "meilisearch",
driverProfile: "meilisearch",
driverLabel: "Meilisearch",
host: "search.example.com",
port: 7700,
username: "",
password: "secret",
ssl: false,
});
});
it("keeps Meilisearch selected for HTTPS URLs", () => {
expect(parseConnectionUrl("https://search.example.com:8443?insecure=true", "meilisearch")).toMatchObject({
dbType: "meilisearch",
driverProfile: "meilisearch",
port: 8443,
urlParams: "insecure=true",
ssl: true,
});
});
it("exposes the Meilisearch scheme profile", () => {
expect(connectionProfileForScheme("meilisearch")).toEqual({
type: "meilisearch",
profile: "meilisearch",
label: "Meilisearch",
defaultPort: 7700,
});
});
});
@@ -41,6 +41,7 @@ describe("dataGridColumnNameCopy", () => {
expect(supportsColumnNameQuoting("redis")).toBe(false);
expect(supportsColumnNameQuoting("elasticsearch")).toBe(false);
expect(supportsColumnNameQuoting("easysearch")).toBe(false);
expect(supportsColumnNameQuoting("meilisearch")).toBe(false);
expect(supportsColumnNameQuoting("qdrant")).toBe(false);
expect(supportsColumnNameQuoting("jdbc")).toBe(false);
expect(supportsColumnNameQuoting("iotdb")).toBe(false);
@@ -6,6 +6,7 @@ describe("dataGridPreviewLabelKey", () => {
expect(dataGridPreviewLabelKey("mongodb")).toBe("toolbar.previewQuery");
expect(dataGridPreviewLabelKey("elasticsearch")).toBe("toolbar.previewQuery");
expect(dataGridPreviewLabelKey("easysearch")).toBe("toolbar.previewQuery");
expect(dataGridPreviewLabelKey("meilisearch")).toBe("toolbar.previewQuery");
});
it("keeps the SQL wording for SQL databases", () => {
@@ -4,7 +4,9 @@ import { databaseRuntimeMode, usesAgentCursorForQuery } from "@/lib/database/dat
describe("databaseDriverManifest", () => {
it("uses agent cursor only for agent or external runtimes", () => {
expect(databaseRuntimeMode("gaussdb")).toBe("native");
expect(databaseRuntimeMode("meilisearch")).toBe("native");
expect(usesAgentCursorForQuery("gaussdb")).toBe(false);
expect(usesAgentCursorForQuery("meilisearch")).toBe(false);
expect(usesAgentCursorForQuery("mysql")).toBe(false);
expect(usesAgentCursorForQuery("jdbc")).toBe(true);
expect(usesAgentCursorForQuery("prestosql")).toBe(true);
@@ -107,6 +107,7 @@ describe("supportsSqlInListPaste", () => {
expect(supportsSqlInListPaste("mongodb")).toBe(false);
expect(supportsSqlInListPaste("elasticsearch")).toBe(false);
expect(supportsSqlInListPaste("easysearch")).toBe(false);
expect(supportsSqlInListPaste("meilisearch")).toBe(false);
expect(supportsSqlInListPaste("qdrant")).toBe(false);
expect(supportsSqlInListPaste("milvus")).toBe(false);
expect(supportsSqlInListPaste("weaviate")).toBe(false);
@@ -474,7 +474,7 @@ POST /orders/_search
HEAD /orders`;
for (const databaseType of ["elasticsearch", "easysearch"] as const) {
for (const databaseType of ["elasticsearch", "easysearch", "meilisearch"] as const) {
expect(rangeSqlTexts(splitSqlStatementRanges(sql, databaseType))).toEqual(["GET /_nodes/stats/jvm?pretty", 'POST /orders/_search\n{\n "query": { "match_all": {} }\n}', "HEAD /orders"]);
expect(hasMultipleExecutionTargets(sql, databaseType)).toBe(true);
}
@@ -1447,6 +1447,7 @@ describe("supportsExecutionTargetPicker", () => {
expect(supportsExecutionTargetPicker("mongodb")).toBe(false);
expect(supportsExecutionTargetPicker("elasticsearch")).toBe(true);
expect(supportsExecutionTargetPicker("easysearch")).toBe(true);
expect(supportsExecutionTargetPicker("meilisearch")).toBe(true);
expect(supportsExecutionTargetPicker("qdrant")).toBe(false);
expect(supportsExecutionTargetPicker("milvus")).toBe(false);
expect(supportsExecutionTargetPicker("weaviate")).toBe(false);
@@ -29,17 +29,20 @@ export function parseDocumentStoreInputValue(raw: MongoInputValue, kind: Documen
}
export function stringifyDocumentStoreValue(value: unknown, kind: DocumentStoreKind, indent?: number): string {
return kind === "elasticsearch" ? stringifyJsonPreservingLargeNumbers(value, indent) : JSON.stringify(value, null, indent ?? undefined);
return kind === "mongodb" ? JSON.stringify(value, null, indent ?? undefined) : stringifyJsonPreservingLargeNumbers(value, indent);
}
export function documentStoreValueForGrid(value: unknown, kind: DocumentStoreKind): MongoInputValue {
if (kind === "elasticsearch" && isLosslessJsonNumber(value)) return value.raw;
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
return stringifyDocumentStoreValue(value, kind);
}
export function serializeDocumentStoreId(value: unknown, kind: DocumentStoreKind): string {
return kind === "elasticsearch" ? String(value) : serializeMongoDocumentId(value);
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
if (kind === "elasticsearch") return String(value);
if (kind === "meilisearch") return typeof value === "string" ? `__dbx_meilisearch_string_id__${JSON.stringify(value)}` : String(value);
return serializeMongoDocumentId(value);
}
/**
@@ -1,10 +1,10 @@
import type { ComposerTranslation } from "vue-i18n";
import { normalizeJsonArgument } from "@dbx-app/mongo-shell";
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
import { quoteUnquotedObjectKeys } from "@/lib/mongo/mongoShellCommand";
import { formatMongoShellLiteral } from "@/lib/mongo/mongoDocumentValues";
export type DocumentStoreKind = "mongodb" | "elasticsearch";
export type DocumentStoreKind = "mongodb" | "elasticsearch" | "meilisearch";
export type DocumentFilterMode = "equals" | "not-equals" | "like" | "not-like" | "greater-than" | "greater-than-or-equal" | "less-than" | "less-than-or-equal" | "is-null" | "is-not-null";
export type DocumentFilterValueType = "auto" | "string" | "number" | "boolean" | "object-id" | "date" | "int32" | "int64" | "decimal128" | "json";
export type ElasticsearchBoolClause = "filter" | "must" | "should" | "must_not";
@@ -64,6 +64,10 @@ export const documentFilterModeOptions: Array<{ value: DocumentFilterMode; label
{ value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" },
];
export function documentFilterModeOptionsFor(kind: DocumentStoreKind): Array<{ value: DocumentFilterMode; labelKey: string }> {
return kind === "meilisearch" ? documentFilterModeOptions.filter((option) => option.value !== "like" && option.value !== "not-like") : documentFilterModeOptions;
}
export const documentFilterValueTypeOptions: Array<{ value: DocumentFilterValueType; labelKey: string }> = [
{ value: "auto", labelKey: "grid.filterBuilderValueTypeAuto" },
{ value: "string", labelKey: "grid.filterBuilderValueTypeString" },
@@ -130,8 +134,36 @@ const elasticsearchDocumentProvider: DocumentStoreProvider = {
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
};
const meilisearchDocumentProvider: DocumentStoreProvider = {
kind: "meilisearch",
filterInputLabel: "filter",
sortInputLabel: "sort",
documentsLabel: ({ total }) => `${total} Documents`,
queryPreview: ({ collection, filterJson, sortJson, skip, limit }) => {
const lines = ["DBX MEILISEARCH FETCH DOCUMENTS", `index: ${JSON.stringify(collection)}`, `offset: ${skip}`, `limit: ${limit}`];
const filter = documentStorePreviewJson(filterJson);
if (filter) lines.push("filter:", filter);
const sort = documentStorePreviewJson(sortJson);
if (sort) lines.push("sort:", sort);
return lines.join("\n");
},
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
};
function documentStorePreviewJson(value?: string): string | null {
const trimmed = value?.trim();
if (!trimmed || trimmed === "{}") return null;
try {
return JSON.stringify(JSON.parse(trimmed), null, 2);
} catch {
return trimmed;
}
}
export function documentStoreProviderFor(databaseType?: DatabaseType): DocumentStoreProvider {
return isElasticsearchCompatibleDatabaseType(databaseType) ? elasticsearchDocumentProvider : mongoDocumentProvider;
if (isElasticsearchCompatibleDatabaseType(databaseType)) return elasticsearchDocumentProvider;
if (isMeilisearchDatabaseType(databaseType)) return meilisearchDocumentProvider;
return mongoDocumentProvider;
}
export function defaultDocumentFilterRule(id: string, fieldName = ""): DocumentFilterRule {
@@ -639,6 +671,7 @@ type DocumentQueryInputNormalizer = (input: string) => string | null;
const documentQueryInputNormalizers: Record<DocumentStoreKind, DocumentQueryInputNormalizer> = {
mongodb: normalizeJsonArgument,
elasticsearch: quoteUnquotedObjectKeys,
meilisearch: quoteUnquotedObjectKeys,
};
function normalizeDocumentQueryObjectInput(input: string, kind?: DocumentStoreKind): string {
+11 -4
View File
@@ -7,6 +7,13 @@ export type DocumentStoreWriteApis = {
delete: (id: string, routing?: string) => Promise<number>;
};
export function formatMeilisearchDocumentOperationPreview(options: { action: "insert" | "upsert" | "update" | "delete"; index: string; id?: unknown; document?: Record<string, unknown> }): string {
const lines = [`DBX MEILISEARCH ${options.action.toUpperCase()} DOCUMENT`, `index: ${JSON.stringify(options.index)}`];
if (options.id !== undefined) lines.push(`id: ${stringifyDocumentStoreValue(options.id, "meilisearch")}`);
if (options.document) lines.push("document:", stringifyDocumentStoreValue(options.document, "meilisearch", 2));
return lines.join("\n");
}
/**
* Write a document body under a known identity.
* - `put`: Elasticsearch index-by-id / Mongo update-by-id (identity via path, not body).
@@ -48,8 +55,8 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
return;
}
// Rekey write: ES uses put under the new id/routing; Mongo inserts a new document then deletes the old id.
if (kind === "elasticsearch") {
// Path-identity stores write under the new id first; Mongo inserts a new document.
if (kind !== "mongodb") {
await writeDocumentStoreDocument({
kind,
op: "put",
@@ -71,10 +78,10 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
await apis.delete(plan.deleteId, plan.deleteRouting);
}
/** Insert a new document (optional explicit ES id uses put). */
/** Insert a new document (optional explicit path identity uses put). */
export async function insertDocumentStoreDocument(options: { kind: DocumentStoreKind; document: Record<string, unknown>; explicitId?: string | null; routing?: string; apis: Pick<DocumentStoreWriteApis, "insert" | "update"> }): Promise<void> {
const { kind, document, explicitId, routing, apis } = options;
if (kind === "elasticsearch" && explicitId) {
if (kind !== "mongodb" && explicitId) {
await writeDocumentStoreDocument({
kind,
op: "put",
+1
View File
@@ -699,6 +699,7 @@ export const documentUpdateDocument = forward("documentUpdateDocument");
export const mongoUpdateDocument = forward("mongoUpdateDocument");
export const mongoUpdateDocuments = forward("mongoUpdateDocuments");
export const documentDeleteDocument = forward("documentDeleteDocument");
export const documentSaveMeilisearchBatch = forward("documentSaveMeilisearchBatch");
export const mongoDeleteDocument = forward("mongoDeleteDocument");
export const mongoDeleteDocuments = forward("mongoDeleteDocuments");
export const mongoFindOneAndUpdate = forward("mongoFindOneAndUpdate");
+10
View File
@@ -3668,6 +3668,16 @@ export async function documentDeleteDocument(connectionId: string, database: str
});
}
export async function documentSaveMeilisearchBatch(connectionId: string, collection: string, updates: Array<{ id: string; docJson: string }>, deleteIds: string[], inserts: string[]): Promise<number> {
return post("/api/document-store/save-meilisearch-batch", {
connectionId,
collection,
updates,
deleteIds,
inserts,
});
}
export async function mongoDeleteDocuments(connectionId: string, database: string, collection: string, filterJson: string, many: boolean): Promise<{ affected_rows: number }> {
return post("/api/mongo/delete-documents", {
connectionId,
+10
View File
@@ -3615,6 +3615,16 @@ export async function documentDeleteDocument(connectionId: string, database: str
});
}
export async function documentSaveMeilisearchBatch(connectionId: string, collection: string, updates: Array<{ id: string; docJson: string }>, deleteIds: string[], inserts: string[]): Promise<number> {
return invoke("document_save_meilisearch_batch", {
connectionId,
collection,
updates,
deleteIds,
inserts,
});
}
export async function mongoDeleteDocuments(connectionId: string, database: string, collection: string, filterJson: string, many: boolean): Promise<{ affected_rows: number }> {
const affectedRows = await invoke<number>("mongo_delete_documents", {
connectionId,
@@ -129,6 +129,7 @@ export function connectionDisplayUrlScheme(connection: Pick<ConnectionConfig, "d
return "mssql";
case "elasticsearch":
case "easysearch":
case "meilisearch":
case "qdrant":
case "milvus":
case "weaviate":
@@ -207,6 +208,7 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
case "elasticsearch":
case "easysearch":
case "meilisearch":
case "qdrant":
case "milvus":
case "weaviate":
@@ -53,6 +53,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
oracle: { type: "oracle", profile: "oracle", label: "Oracle", defaultPort: 1521 },
elasticsearch: { type: "elasticsearch", profile: "elasticsearch", label: "Elasticsearch", defaultPort: 9200 },
easysearch: { type: "easysearch", profile: "easysearch", label: "Easysearch", defaultPort: 9200 },
meilisearch: { type: "meilisearch", profile: "meilisearch", label: "Meilisearch", defaultPort: 7700 },
qdrant: { type: "qdrant", profile: "qdrant", label: "Qdrant", defaultPort: 6333 },
milvus: { type: "milvus", profile: "milvus", label: "Milvus", defaultPort: 19530 },
weaviate: { type: "weaviate", profile: "weaviate", label: "Weaviate", defaultPort: 8080 },
@@ -82,6 +83,7 @@ const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
clickhouse: SCHEME_PROFILES.clickhouse,
elasticsearch: SCHEME_PROFILES.elasticsearch,
easysearch: SCHEME_PROFILES.easysearch,
meilisearch: SCHEME_PROFILES.meilisearch,
qdrant: SCHEME_PROFILES.qdrant,
milvus: SCHEME_PROFILES.milvus,
weaviate: SCHEME_PROFILES.weaviate,
@@ -5,7 +5,7 @@ const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
// non-database services expose their own root objects rather than database namespaces.
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
type VisibleDatabaseConnectionFields = Pick<
ConnectionConfig,
@@ -25,7 +25,7 @@ export function isColumnNameCopySeparator(value: unknown): value is ColumnNameCo
}
// SQL 执行能力是产品能力,不代表数据库支持 SQL 标识符引用。
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
// quoteTableIdentifier 对 jdbc/iotdb 原样返回(无引用字符可用)。
const UNQUOTABLE_DATABASE_TYPES = new Set<DatabaseType>(["jdbc", "iotdb"]);
@@ -1,6 +1,6 @@
import type { DatabaseType } from "@/types/database";
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch"]);
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch", "meilisearch"]);
export function dataGridPreviewLabelKey(databaseType?: DatabaseType): "toolbar.previewQuery" | "toolbar.previewSql" {
return databaseType && NON_SQL_PREVIEW_DATABASE_TYPES.has(databaseType) ? "toolbar.previewQuery" : "toolbar.previewSql";
@@ -30,6 +30,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
oracle: { deferred: "Oracle schemas are users; database creation is not a normal connected DDL action" },
elasticsearch: { deferred: "index creation is not modeled as database creation" },
easysearch: { deferred: "index creation is not modeled as database creation" },
meilisearch: { deferred: "index creation is not modeled as database creation" },
hbase: { deferred: "namespace creation needs dedicated HBase namespace options" },
qdrant: { deferred: "collection creation is separate from database creation" },
milvus: { deferred: "collection/database lifecycle needs a dedicated vector workflow" },
@@ -30,6 +30,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
oracle: { deferred: "Oracle database properties are instance/user/tablespace administration" },
elasticsearch: { deferred: "index settings are not database properties" },
easysearch: { deferred: "index settings are not database properties" },
meilisearch: { deferred: "index settings are not database properties" },
hbase: { deferred: "namespace and table properties need a dedicated HBase workflow" },
qdrant: { deferred: "collection settings are not database properties" },
milvus: { deferred: "collection/database settings need a dedicated vector workflow" },
@@ -109,6 +109,7 @@ const SQL_EXECUTION_TARGET_REGISTRY: Partial<Record<DatabaseType, SqlExecutionTa
// safely apply a selected index/collection/database to the actual request.
elasticsearch: { scope: "connection" },
easysearch: { scope: "connection" },
meilisearch: { scope: "connection" },
qdrant: { scope: "connection" },
milvus: { scope: "connection" },
weaviate: { scope: "connection" },
@@ -40,7 +40,7 @@ export function sortSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">,
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "dameng-users" && child.type !== "dameng-roles" && child.type !== "saved-sql-root");
const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes];
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
return withConnectionUtilityOrder(sortByLabel(regularChildren));
}
@@ -285,6 +285,7 @@ export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, opt
options.databaseType === "mongodb" ||
options.databaseType === "elasticsearch" ||
options.databaseType === "easysearch" ||
options.databaseType === "meilisearch" ||
options.databaseType === "qdrant" ||
options.databaseType === "milvus" ||
options.databaseType === "weaviate" ||
@@ -304,6 +305,7 @@ export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number,
options.databaseType === "mongodb" ||
options.databaseType === "elasticsearch" ||
options.databaseType === "easysearch" ||
options.databaseType === "meilisearch" ||
options.databaseType === "qdrant" ||
options.databaseType === "milvus" ||
options.databaseType === "weaviate" ||
+14 -10
View File
@@ -2,7 +2,7 @@ import type { SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
import { cursorBelongsToTrailingStatementDelimiter } from "@/lib/sql/statementDelimiter";
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
/**
* A contiguous range of SQL text expressed as document offsets plus the
@@ -14,18 +14,22 @@ export interface SqlTextRange {
sql: string;
}
const ELASTICSEARCH_REST_REQUEST = /^(?:GET|POST|PUT|DELETE|HEAD)\s+\S+/i;
const ELASTICSEARCH_REST_REQUEST = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+\S+/i;
function isHttpJsonRestDatabaseType(databaseType?: DatabaseType): boolean {
return isElasticsearchCompatibleDatabaseType(databaseType) || isMeilisearchDatabaseType(databaseType);
}
export function elasticsearchRestRequestRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
if (!isElasticsearchCompatibleDatabaseType(databaseType)) return [];
if (!isHttpJsonRestDatabaseType(databaseType)) return [];
const requests = splitSqlStatementRanges(sql, databaseType);
return requests.length > 0 && requests.every((request) => ELASTICSEARCH_REST_REQUEST.test(request.sql)) ? requests : [];
}
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
export function supportsExecutionTargetPicker(databaseType?: DatabaseType): boolean {
return !!databaseType && (databaseType === "redis" || isElasticsearchCompatibleDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
return !!databaseType && (databaseType === "redis" || isHttpJsonRestDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
}
export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean {
@@ -58,11 +62,11 @@ interface ElasticsearchRequestLineCandidate {
}
export interface ElasticsearchRestRequestTarget {
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
path: string;
}
const ELASTICSEARCH_REST_REQUEST_LINE = /^\s*(?:GET|POST|PUT|DELETE|HEAD)\s+\S+/i;
const ELASTICSEARCH_REST_REQUEST_LINE = /^\s*(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+\S+/i;
function leadingElasticsearchPreambleEnd(value: string): number {
let offset = 0;
@@ -145,7 +149,7 @@ function elasticsearchRequestLineCandidates(lines: ElasticsearchRequestLine[]):
export function parseElasticsearchRestRequestTarget(value: string): ElasticsearchRestRequestTarget | null {
const requestLine = stripLeadingElasticsearchComments(value).split("\n", 1)[0]?.trim() ?? "";
const match = requestLine.match(/^(GET|POST|PUT|DELETE|HEAD)\s+(\S+)/i);
const match = requestLine.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\S+)/i);
if (!match) return null;
return {
method: match[1].toUpperCase() as ElasticsearchRestRequestTarget["method"],
@@ -284,7 +288,7 @@ const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
* whitespace are excluded so editor highlights stay tight).
*/
export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
if (isElasticsearchCompatibleDatabaseType(databaseType)) {
if (isHttpJsonRestDatabaseType(databaseType)) {
const requests = splitElasticsearchRestRequestRanges(sql);
if (requests) return requests;
}
@@ -594,7 +598,7 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
// Cursor in indentation or inter-statement whitespace immediately before
// the statement should still target that statement, while the returned
// execution range remains tight around the SQL text itself.
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (isElasticsearchCompatibleDatabaseType(databaseType) && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (isHttpJsonRestDatabaseType(databaseType) && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
const previous = statements[index - 1];
if (previous && isCursorInTrailingDelimiterGap(sql, previous.to, pos)) {
const previousSoftRanges = splitStatementRangeAtSoftStarts(sql, previous, databaseType, parameterOptions);
@@ -43,6 +43,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
triggers: false,
ddl: false,
},
meilisearch: {
indexes: false,
foreignKeys: false,
triggers: false,
ddl: false,
},
hbase: {
indexes: false,
foreignKeys: false,