feat(meilisearch): add native document database support
This commit is contained in:
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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user