From f3853862841f7465bef87bc78d4875d66e2404de Mon Sep 17 00:00:00 2001 From: Vergil Lai Date: Sun, 14 Jun 2026 16:01:02 +0800 Subject: [PATCH] feat(manticore): add Manticore Search support Add Manticore Search table data editing, structure editing, metadata handling, and SQL completion support. --- .../structure/TableStructureEditor.vue | 118 ++++++-- .../desktop/src/lib/connectionPresentation.ts | 2 +- .../desktop/src/lib/databaseFeatureSupport.ts | 2 +- .../src/lib/databaseObjectCapabilities.ts | 5 +- .../src/lib/databaseTableDataCapabilities.ts | 11 + apps/desktop/src/lib/sqlCompletion.ts | 85 +++++- .../src/lib/tableMetadataCapabilities.ts | 6 +- .../src/lib/tableStructureCapabilities.ts | 13 + .../src/lib/tableStructureEditorSql.ts | 6 +- .../src/lib/tableStructureEditorState.ts | 95 +++++++ apps/desktop/src/lib/visibleDatabases.ts | 2 +- .../assets/database-drivers.manifest.json | 6 +- crates/dbx-core/src/data_grid_sql.rs | 92 +++++- crates/dbx-core/src/db/manticoresearch.rs | 235 ++++++++++++++++ crates/dbx-core/src/db/mod.rs | 1 + crates/dbx-core/src/schema.rs | 35 ++- .../dbx-core/src/schema/providers/native.rs | 21 +- .../src/table_structure_sql/column_alter.rs | 50 +++- .../src/table_structure_sql/column_format.rs | 35 +++ .../src/table_structure_sql/columns.rs | 7 +- .../src/table_structure_sql/create_table.rs | 12 +- .../src/table_structure_sql/dialect.rs | 9 + .../dbx-core/src/table_structure_sql/tests.rs | 266 +++++++++++++++++- .../dbx-core/src/table_structure_sql/types.rs | 10 +- .../dbx-core/src/table_structure_sql/util.rs | 6 +- .../dbx-core/tests/database_capabilities.rs | 10 +- .../plans/2026-06-14-query-result-archive.md | 58 ---- .../app-tests/databaseCapabilities.test.ts | 18 +- packages/app-tests/sqlCompletion.test.ts | 44 +++ packages/app-tests/tableEditing.test.ts | 3 + .../tableMetadataCapabilities.test.ts | 13 + .../tableStructureCapabilities.test.ts | 30 +- .../tableStructureEditorState.test.ts | 68 ++++- .../node-core/tests/driver-manifest.test.ts | 6 +- 34 files changed, 1250 insertions(+), 130 deletions(-) create mode 100644 crates/dbx-core/src/db/manticoresearch.rs delete mode 100644 docs/superpowers/plans/2026-06-14-query-result-archive.md create mode 100644 packages/app-tests/tableMetadataCapabilities.test.ts diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index a364a47b2..ada084f44 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -23,7 +23,7 @@ import { copyToClipboard } from "@/lib/clipboard"; import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout"; import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/tableStructureEditorSql"; import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; -import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities"; +import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/tableStructureCapabilities"; import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect"; import { buildStructureTargetLabel, @@ -34,10 +34,14 @@ import { createTriggerDrafts, generateIndexName, generateUniqueIndexName, + getColumnEditorControls, getDataTypeOptions, getDefaultLengthForType, + isProtectedManticoreIdColumn, splitDataType, toColumnNames, + applyManticoreDdlColumnExtras, + canEditManticoreColumnProperties, } from "@/lib/tableStructureEditorState"; import * as api from "@/lib/api"; @@ -250,7 +254,7 @@ function onColResize(e: MouseEvent, col: number) { const onMove = (ev: MouseEvent) => { if (!colResizing.value) return; const delta = ev.clientX - colResizing.value.startX; - colWidths.value[col] = Math.max(structureDensityMetric.value.minColumnWidth, colResizing.value.startW + delta); + colWidths.value[widthIndex] = Math.max(structureDensityMetric.value.minColumnWidth, colResizing.value.startW + delta); }; const onUp = () => { colResizing.value = null; @@ -285,6 +289,7 @@ const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(da const structureDialect = computed(() => structureCapabilities.value.dialect); const isTableCommentDisabled = computed(() => !structureCapabilities.value.comment); const dataTypeOptions = computed(() => getDataTypeOptions(databaseType.value)); +const columnEditorControls = computed(() => getColumnEditorControls(databaseType.value)); const indexTypesByDb: Record = { postgres: ["BTREE", "HASH", "GIST", "SPGIST", "GIN", "BRIN"], @@ -301,21 +306,31 @@ function isPostgresIdentityType(dbType: string | undefined): boolean { const showExtendedProperties = computed(() => { const dt = databaseType.value; - return dt === "mysql" || isPostgresIdentityType(dt) || dt === "sqlserver"; + return dt === "mysql" || dt === "manticoresearch" || isPostgresIdentityType(dt) || dt === "sqlserver"; }); const extendedPropertiesColumnIndex = 8; -const visibleColWidths = computed(() => (showExtendedProperties.value ? colWidths.value : colWidths.value.filter((_, index) => index !== extendedPropertiesColumnIndex))); +const visibleColumnIndexes = computed(() => colLabels.value.map((column) => column.widthIndex)); +const visibleColWidths = computed(() => visibleColumnIndexes.value.map((index) => colWidths.value[index] ?? structureDensityMetric.value.minColumnWidth)); function columnWidthIndex(visibleIndex: number) { - return !showExtendedProperties.value && visibleIndex >= extendedPropertiesColumnIndex ? visibleIndex + 1 : visibleIndex; + return visibleColumnIndexes.value[visibleIndex] ?? visibleIndex; } const colLabels = computed(() => { - const labels = ["#", t("structureEditor.columnName"), t("structureEditor.dataType"), t("structureEditor.length"), t("structureEditor.nullable"), t("structureEditor.primaryKey"), t("structureEditor.defaultValue"), t("structureEditor.comment")]; + const labels = [ + { key: "ordinal", label: "#", widthIndex: 0 }, + { key: "name", label: t("structureEditor.columnName"), widthIndex: 1 }, + { key: "type", label: t("structureEditor.dataType"), widthIndex: 2 }, + ]; + if (columnEditorControls.value.length) labels.push({ key: "length", label: t("structureEditor.length"), widthIndex: 3 }); + if (columnEditorControls.value.nullable) labels.push({ key: "nullable", label: t("structureEditor.nullable"), widthIndex: 4 }); + if (columnEditorControls.value.primaryKey) labels.push({ key: "primaryKey", label: t("structureEditor.primaryKey"), widthIndex: 5 }); + if (columnEditorControls.value.defaultValue) labels.push({ key: "defaultValue", label: t("structureEditor.defaultValue"), widthIndex: 6 }); + if (columnEditorControls.value.comment) labels.push({ key: "comment", label: t("structureEditor.comment"), widthIndex: 7 }); if (showExtendedProperties.value) { - labels.push(t("structureEditor.extendedProperties")); + labels.push({ key: "extendedProperties", label: t("structureEditor.extendedProperties"), widthIndex: extendedPropertiesColumnIndex }); } - labels.push(t("structureEditor.actions")); + labels.push({ key: "actions", label: t("structureEditor.actions"), widthIndex: 9 }); return labels; }); const indexColLabels = computed(() => [t("structureEditor.indexName"), t("structureEditor.indexColumns"), t("structureEditor.unique"), t("structureEditor.indexType"), t("structureEditor.includedColumns"), t("structureEditor.filter"), t("structureEditor.comment"), t("structureEditor.actions")]); @@ -325,11 +340,23 @@ const triggerEventOptions = ["INSERT", "UPDATE", "DELETE"]; const metadataSchema = computed(() => connectionObjectTreeQuerySchema(connection.value, props.database, props.schema)); const refreshVersion = computed(() => (props.connectionId && props.tableName ? queryStore.tableStructureRefreshVersion(props.connectionId, props.database, props.schema, props.tableName) : 0)); const isCreateMode = computed(() => !props.tableName); +const canAddColumn = computed(() => canAddTableStructureColumn(databaseType.value, isCreateMode.value)); const newTableName = ref(""); const tableComment = ref(""); const originalTableComment = ref(""); const targetLabel = computed(() => buildStructureTargetLabel(connection.value?.name, props.database, props.schema, isCreateMode.value ? undefined : props.tableName)); +function isManticoreTextColumn(column: EditableStructureColumn): boolean { + if (databaseType.value !== "manticoresearch") return false; + const baseType = splitDataType(column.dataType).baseType.trim().toLowerCase(); + return baseType === "text" || baseType === "string"; +} + +function isManticoreJsonColumn(column: EditableStructureColumn): boolean { + if (databaseType.value !== "manticoresearch") return false; + return splitDataType(column.dataType).baseType.trim().toLowerCase() === "json"; +} + let sqlPreviewRequestId = 0; let keydownListenerRegistered = false; @@ -388,7 +415,17 @@ async function loadStructure(silent = false) { errorMessage.value = ""; try { await store.ensureConnected(props.connectionId); - const nextColumns = await api.getColumns(props.connectionId, props.database, metadataSchema.value, props.tableName); + let nextColumns = await api.getColumns(props.connectionId, props.database, metadataSchema.value, props.tableName); + if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) { + try { + const ddl = await api.getTableDdl(props.connectionId, props.database, metadataSchema.value, props.tableName); + ddlContent.value = ddl; + ddlFetched.value = true; + nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl); + } catch { + /* ignore — Manticore column properties can still come from SHOW COLUMNS when available */ + } + } const [nextIndexes, nextForeignKeys, nextTriggers] = await Promise.all([ tableMetadataCapabilities.value.indexes ? api.listIndexes(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []) : Promise.resolve([]), tableMetadataCapabilities.value.foreignKeys ? api.listForeignKeys(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []) : Promise.resolve([]), @@ -414,12 +451,13 @@ async function loadStructure(silent = false) { } async function addColumn() { - if (!structureCapabilities.value.addColumn) return; + if (!canAddColumn.value) return; activeTab.value = "columns"; + const dataType = databaseType.value === "manticoresearch" ? combineDataTypeForDatabase(databaseType.value, dataTypeOptions.value[0] ?? "text", getDefaultLengthForType(databaseType.value, dataTypeOptions.value[0] ?? "text")) : "varchar(255)"; columns.value.push({ id: `new:${uuid()}`, name: "", - dataType: "varchar(255)", + dataType, isNullable: true, defaultValue: "", comment: "", @@ -470,6 +508,13 @@ function isColumnTypeDisabled(column: EditableStructureColumn): boolean { return column.markedForDrop || (!!column.original && !structureCapabilities.value.alterType); } +function isColumnLengthDisabled(column: EditableStructureColumn): boolean { + if (isColumnTypeDisabled(column)) return true; + if (databaseType.value !== "manticoresearch") return false; + const baseType = splitDataType(column.dataType).baseType.trim().toLowerCase(); + return baseType !== "bit" && baseType !== "float_vector"; +} + function isColumnNullableDisabled(column: EditableStructureColumn): boolean { return column.markedForDrop || column.isPrimaryKey || (!!column.original && !structureCapabilities.value.alterNullability); } @@ -489,7 +534,11 @@ function isPrimaryKeyDisabled(column: EditableStructureColumn): boolean { } function canDropColumn(column: EditableStructureColumn): boolean { - return !!column.original && !column.isPrimaryKey && structureCapabilities.value.dropColumn; + return !!column.original && !column.isPrimaryKey && !isProtectedManticoreIdColumn(databaseType.value, column.original.name) && structureCapabilities.value.dropColumn; +} + +function isManticoreColumnPropertyDisabled(column: EditableStructureColumn): boolean { + return !canEditManticoreColumnProperties(databaseType.value, !!column.original) || column.markedForDrop; } function addIndex() { @@ -740,7 +789,7 @@ async function applyChanges() { } function addItemForActiveTab(): boolean { - if (activeTab.value === "columns" && structureCapabilities.value.addColumn) { + if (activeTab.value === "columns" && canAddColumn.value) { void addColumn(); return true; } @@ -903,7 +952,7 @@ watch(activeTab, (tab) => { - @@ -926,8 +975,8 @@ watch(activeTab, (tab) => { - @@ -958,16 +1007,16 @@ watch(activeTab, (tab) => { /> - - - - -
- {{ label }} + + {{ columnLabel.label }}
- + + + + { " /> + +
@@ -1013,8 +1062,31 @@ watch(activeTab, (tab) => {
+ + -