feat(manticore): add Manticore Search support

Add Manticore Search table data editing, structure editing, metadata handling, and SQL completion support.
This commit is contained in:
Vergil Lai
2026-06-14 16:01:02 +08:00
committed by GitHub
parent 93d2e7dc18
commit f385386284
34 changed files with 1250 additions and 130 deletions
@@ -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<string, string[]> = {
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) => {
</SelectContent>
</Select>
</div>
<Button v-if="activeTab === 'columns'" size="sm" :class="structureToolbarButtonClass" :disabled="!structureCapabilities.addColumn" @click="addColumn">
<Button v-if="activeTab === 'columns'" size="sm" :class="structureToolbarButtonClass" :disabled="!canAddColumn" @click="addColumn">
<Plus :class="structureIconClass" />
{{ t("structureEditor.addColumn") }}
</Button>
@@ -926,8 +975,8 @@ watch(activeTab, (tab) => {
<table class="border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: visibleColWidths.reduce((a, w) => a + w, 0) + 'px' }">
<thead class="sticky top-0 z-10 bg-background">
<tr>
<th v-for="(label, i) in colLabels" :key="i" :class="[structureHeaderCellClass, { 'text-center': i === 5 }]" :style="{ width: visibleColWidths[i] + 'px', minWidth: visibleColWidths[i] + 'px' }">
{{ label }}
<th v-for="(columnLabel, i) in colLabels" :key="columnLabel.key" :class="[structureHeaderCellClass, { 'text-center': columnLabel.key === 'primaryKey' }]" :style="{ width: visibleColWidths[i] + 'px', minWidth: visibleColWidths[i] + 'px' }">
{{ columnLabel.label }}
<div v-if="i < colLabels.length - 1" class="absolute right-0 top-0 z-20 h-full w-1 cursor-col-resize hover:bg-primary/30" :class="colResizing?.col === columnWidthIndex(i) ? 'bg-primary/30' : ''" @mousedown="onColResize($event, i)" />
</th>
</tr>
@@ -958,16 +1007,16 @@ watch(activeTab, (tab) => {
/>
<Input v-else :model-value="splitDataType(column.dataType).baseType" :class="[structureMonoControlClass, 'w-full']" disabled />
</td>
<td :class="structureCellClass">
<Input :model-value="splitDataType(column.dataType).params" :class="structureMonoControlClass" :disabled="isColumnTypeDisabled(column)" @update:model-value="column.dataType = combineDataTypeForDatabase(databaseType, splitDataType(column.dataType).baseType, String($event))" />
<td v-if="columnEditorControls.length" :class="structureCellClass">
<Input :model-value="splitDataType(column.dataType).params" :class="structureMonoControlClass" :disabled="isColumnLengthDisabled(column)" @update:model-value="column.dataType = combineDataTypeForDatabase(databaseType, splitDataType(column.dataType).baseType, String($event))" />
</td>
<td :class="structureCellClass">
<td v-if="columnEditorControls.nullable" :class="structureCellClass">
<label class="flex items-center gap-1.5">
<input v-model="column.isNullable" type="checkbox" :class="structureCheckboxClass" :disabled="isColumnNullableDisabled(column)" />
<span>{{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
</label>
</td>
<td :class="[structureCellClass, 'text-center']">
<td v-if="columnEditorControls.primaryKey" :class="[structureCellClass, 'text-center']">
<input
v-model="column.isPrimaryKey"
type="checkbox"
@@ -980,10 +1029,10 @@ watch(activeTab, (tab) => {
"
/>
</td>
<td :class="structureCellClass">
<td v-if="columnEditorControls.defaultValue" :class="structureCellClass">
<Input v-model="column.defaultValue" :class="structureMonoControlClass" :disabled="isColumnDefaultDisabled(column)" />
</td>
<td :class="structureCellClass">
<td v-if="columnEditorControls.comment" :class="structureCellClass">
<div class="flex min-w-0 items-center gap-1">
<Input v-model="column.comment" :class="[structureControlClass, 'flex-1']" :disabled="isColumnCommentDisabled(column)" />
<Popover>
@@ -1013,8 +1062,31 @@ watch(activeTab, (tab) => {
</td>
<td v-if="showExtendedProperties" :class="structureCellClass">
<div class="flex items-center gap-2">
<!-- Manticore Search: character data type properties -->
<template v-if="databaseType === 'manticoresearch'">
<template v-if="isManticoreTextColumn(column)">
<label class="flex items-center gap-1 whitespace-nowrap">
<input :checked="!!column.extra.manticoreIndexed" type="checkbox" :class="structureCheckboxClass" :disabled="isManticoreColumnPropertyDisabled(column)" @change="column.extra.manticoreIndexed = ($event.target as HTMLInputElement).checked" />
indexed
</label>
<label class="flex items-center gap-1 whitespace-nowrap">
<input :checked="!!column.extra.manticoreStored" type="checkbox" :class="structureCheckboxClass" :disabled="isManticoreColumnPropertyDisabled(column)" @change="column.extra.manticoreStored = ($event.target as HTMLInputElement).checked" />
stored
</label>
<label class="flex items-center gap-1 whitespace-nowrap">
<input :checked="!!column.extra.manticoreAttribute" type="checkbox" :class="structureCheckboxClass" :disabled="isManticoreColumnPropertyDisabled(column)" @change="column.extra.manticoreAttribute = ($event.target as HTMLInputElement).checked" />
attribute
</label>
</template>
<template v-else-if="isManticoreJsonColumn(column)">
<label class="flex items-center gap-1 whitespace-nowrap">
<input :checked="!!column.extra.manticoreSecondaryIndex" type="checkbox" :class="structureCheckboxClass" :disabled="isManticoreColumnPropertyDisabled(column)" @change="column.extra.manticoreSecondaryIndex = ($event.target as HTMLInputElement).checked" />
secondary_index
</label>
</template>
</template>
<!-- MySQL: AUTO_INCREMENT + ON UPDATE CURRENT_TIMESTAMP -->
<template v-if="structureDialect === 'mysql'">
<template v-else-if="structureDialect === 'mysql'">
<label class="flex items-center gap-1 whitespace-nowrap">
<input v-model="column.extra.autoIncrement" type="checkbox" :class="structureCheckboxClass" />
{{ t("structureEditor.autoIncrement") }}
@@ -163,4 +163,4 @@ export function connectionOptionSubtitle(connection?: ConnectionPresentationConf
export function connectionRedactedOptionSubtitle(connection?: ConnectionPresentationConfig): string {
return [connectionDriverLabel(connection), connectionRedactedEndpointLabel(connection)].filter(Boolean).join(" · ");
}
}
@@ -87,4 +87,4 @@ export function supportsTableTruncate(dbType?: DatabaseType): boolean {
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
}
}
@@ -9,10 +9,12 @@ export interface DatabaseObjectCapabilities {
}
const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"];
const TABLE_FUNCTION_OBJECTS: SidebarObjectKind[] = ["TABLE", "FUNCTION"];
const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"];
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"];
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
const TABLE_FUNCTION_TYPES = new Set<DatabaseType>(["manticoresearch"]);
const TABLE_VIEW_ONLY_TYPES = new Set<DatabaseType>(["sqlite", "rqlite", "turso", "duckdb", "clickhouse", "doris", "starrocks", "databend", "hive", "trino", "cassandra", "bigquery", "kylin", "tdengine", "iotdb", "neo4j"]);
const ORACLE_PACKAGE_TYPES = new Set<DatabaseType>(["oracle", "oceanbase-oracle"]);
@@ -30,6 +32,7 @@ export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjec
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObjectKind[] {
if (!dbType) return [...TABLE_VIEW_OBJECTS];
if (ORACLE_PACKAGE_TYPES.has(dbType)) return [...ORACLE_OBJECTS];
if (TABLE_FUNCTION_TYPES.has(dbType)) return [...TABLE_FUNCTION_OBJECTS];
if (TABLE_VIEW_ONLY_TYPES.has(dbType)) return [...TABLE_VIEW_OBJECTS];
if (POSTGRES_SEQUENCE_TYPES.has(dbType)) return [...POSTGRES_OBJECTS];
return [...ROUTINE_OBJECTS];
@@ -44,4 +47,4 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
if (value.includes("PROC")) return "PROCEDURE";
if (value.includes("FUNC")) return "FUNCTION";
return "TABLE";
}
}
@@ -47,6 +47,7 @@ const DEFAULT_CAPABILITY: DatabaseCapability = {
const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
"mysql",
"manticoresearch",
"postgres",
"sqlite",
"rqlite",
@@ -103,6 +104,16 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
transaction: false,
},
},
manticoresearch: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
neo4j: {
syntheticKey: "neo4j-element-id",
},
+83 -2
View File
@@ -416,6 +416,8 @@ const MYSQL_SQL_KEYWORDS = [
"DATE_FORMAT",
];
const MANTICORESEARCH_SQL_KEYWORDS = ["FACET", "MATCH", "SHOW", "SHOW META", "SHOW TABLES", "CALL", "CALL PQ", "PQ", "META", "TABLES", "OPTION", "WITHIN GROUP ORDER BY"];
const SQLITE_SQL_KEYWORDS = ["AUTOINCREMENT", "INTEGER", "BLOB", "BOOLEAN", "WITHOUT ROWID", "VACUUM", "PRAGMA", "JSON_EXTRACT", "JSON_SET", "STRFTIME"];
const SQLSERVER_SQL_KEYWORDS = ["TOP", "IDENTITY", "UNIQUEIDENTIFIER", "NVARCHAR", "DATETIME2", "DATETIMEOFFSET", "BIT", "GO", "MERGE", "OUTPUT", "TRY_CAST", "TRY_CONVERT", "OPENJSON", "JSON_VALUE", "JSON_QUERY"];
@@ -427,6 +429,7 @@ const DATABASE_SQL_KEYWORDS: Partial<Record<DatabaseType, string[]>> = {
rqlite: SQLITE_SQL_KEYWORDS,
turso: SQLITE_SQL_KEYWORDS,
sqlserver: SQLSERVER_SQL_KEYWORDS,
manticoresearch: MANTICORESEARCH_SQL_KEYWORDS,
};
// Keywords that appear in nearly every SQL query — boosted so frequency beats length tie-breaking.
@@ -680,6 +683,39 @@ export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = [
},
];
const MANTICORESEARCH_SQL_SNIPPETS: SqlSnippet[] = [
{
id: "builtin-manticore-match",
label: "match query",
prefix: "match",
body: "MATCH('query')",
},
{
id: "builtin-manticore-facet",
label: "facet",
prefix: "facet",
body: "FACET column",
},
{
id: "builtin-manticore-show-meta",
label: "show meta",
prefix: "m",
body: "SHOW META;",
},
{
id: "builtin-manticore-show-tables",
label: "show tables",
prefix: "tab",
body: "SHOW TABLES;",
},
{
id: "builtin-manticore-call-pq",
label: "call pq",
prefix: "p",
body: "CALL PQ ('pq', ('{\"title\":\"query\"}'));",
},
];
const SQL_FUNCTION_SIGNATURES = new Map<string, string[]>([
// Aggregate
["COUNT", ["expression"]],
@@ -810,6 +846,40 @@ const SQLSERVER_FUNCTION_SIGNATURES = new Map<string, string[]>([
["NEWID", []],
]);
const MANTICORESEARCH_FUNCTION_SIGNATURES = new Map<string, string[]>([
["MATCH", ["query"]],
["BM25F", ["field=weight", "...fields"]],
["EXIST", ["attribute", "default"]],
["IDF", ["keyword"]],
["PACKEDFACTORS", []],
["QUERY", []],
["REMAP", ["expression", "from_values", "to_values"]],
["SNIPPET", ["field", "query"]],
["WEIGHT", []],
["ZONESPANLIST", []],
["BIGINT", ["expression"]],
["DOUBLE", ["expression"]],
["INTEGER", ["expression"]],
["SINT", ["expression"]],
["TO_STRING", ["expression"]],
["UINT", ["expression"]],
["UINT64", ["expression"]],
["GEODIST", ["lat1", "lon1", "lat2", "lon2"]],
["CONTAINS", ["polygon", "point"]],
["POLY2D", ["...points"]],
["CRC32", ["expression"]],
["FIBONACCI", ["number"]],
["KNN_DIST", []],
["NOW", []],
["DATE_FORMAT", ["timestamp", "format"]],
["DAY", ["timestamp"]],
["MONTH", ["timestamp"]],
["YEAR", ["timestamp"]],
["HOUR", ["timestamp"]],
["MINUTE", ["timestamp"]],
["SECOND", ["timestamp"]],
]);
const DATABASE_FUNCTION_SIGNATURES: Partial<Record<DatabaseType, Map<string, string[]>>> = {
mysql: MYSQL_FUNCTION_SIGNATURES,
postgres: POSTGRES_FUNCTION_SIGNATURES,
@@ -817,6 +887,7 @@ const DATABASE_FUNCTION_SIGNATURES: Partial<Record<DatabaseType, Map<string, str
rqlite: SQLITE_FUNCTION_SIGNATURES,
turso: SQLITE_FUNCTION_SIGNATURES,
sqlserver: SQLSERVER_FUNCTION_SIGNATURES,
manticoresearch: MANTICORESEARCH_FUNCTION_SIGNATURES,
};
const COMMON_SQL_FUNCTION_NAMES = new Set([
@@ -1076,10 +1147,20 @@ class SqlCompletionProvider {
}
if (!context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) {
this.items.push(...buildSnippetItems(context.prefix, this.input.snippets ?? DEFAULT_SQL_SNIPPETS));
const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS);
this.items.push(...buildSnippetItems(context.prefix, snippets));
this.items.push(...buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType));
}
if (this.databaseType === "manticoresearch" && context.exclusiveRoutineSuggestions) {
this.items.push(
...buildSnippetItems(
context.prefix,
MANTICORESEARCH_SQL_SNIPPETS.filter((snippet) => snippet.id === "builtin-manticore-call-pq"),
),
);
}
if (context.preferredKeywords.length > 0) {
this.items.push(...buildPreferredKeywordItems(context.prefix, context.preferredKeywords));
}
@@ -3265,4 +3346,4 @@ function countTopLevelCommas(text: string): number {
}
return count;
}
}
@@ -21,6 +21,10 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
foreignKeys: false,
triggers: false,
},
manticoresearch: {
foreignKeys: false,
triggers: false,
},
elasticsearch: {
indexes: false,
foreignKeys: false,
@@ -36,4 +40,4 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
export function getTableMetadataCapabilities(dbType?: DatabaseType): TableMetadataCapabilities {
return { ...defaultCapabilities, ...(dbType ? capabilityByType[dbType] : undefined) };
}
}
@@ -237,6 +237,13 @@ const influxdbCapabilities = capabilities({
comment: false,
});
const manticoreSearchCapabilities = capabilities({
dialect: "mysql",
createTable: true,
addColumn: true,
dropColumn: true,
});
const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>> = {
mysql: mysqlCapabilities,
doris: mysqlCapabilities,
@@ -271,6 +278,7 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
clickhouse: clickhouseCapabilities,
informix: informixCapabilities,
influxdb: influxdbCapabilities,
manticoresearch: manticoreSearchCapabilities,
};
export function getTableStructureCapabilities(dbType?: DatabaseType): TableStructureCapabilities {
@@ -281,3 +289,8 @@ export function canEditTableStructure(dbType?: DatabaseType): boolean {
const caps = getTableStructureCapabilities(dbType);
return caps.createTable || caps.addColumn || caps.alterExistingColumn || caps.createIndex || caps.dropIndex;
}
export function canAddTableStructureColumn(dbType: DatabaseType | undefined, isCreateMode: boolean): boolean {
const caps = getTableStructureCapabilities(dbType);
return isCreateMode ? caps.createTable : caps.addColumn;
}
@@ -10,6 +10,10 @@ export interface ColumnExtra {
autoIncrement?: boolean;
onUpdateCurrentTimestamp?: boolean;
identity?: ColumnIdentity;
manticoreIndexed?: boolean;
manticoreStored?: boolean;
manticoreAttribute?: boolean;
manticoreSecondaryIndex?: boolean;
}
export interface EditableStructureColumn {
@@ -86,4 +90,4 @@ export interface BuildSingleColumnAlterSqlOptions {
schema?: string;
tableName: string;
column: EditableStructureColumn;
}
}
@@ -236,6 +236,7 @@ export const DATA_TYPE_OPTIONS: Record<string, string[]> = {
"MultiPolygon",
"JSON",
],
manticoresearch: ["text", "string", "int", "bit", "bigint", "bool", "timestamp", "float", "json", "float_vector", "multi", "mva"],
informix: [
"smallint",
"integer",
@@ -291,6 +292,43 @@ export function getDataTypeOptions(dbType: DatabaseType | undefined): string[] {
return DATA_TYPE_OPTIONS[key] ?? [];
}
export interface ColumnEditorControls {
length: boolean;
nullable: boolean;
primaryKey: boolean;
defaultValue: boolean;
comment: boolean;
}
const DEFAULT_COLUMN_EDITOR_CONTROLS: ColumnEditorControls = {
length: true,
nullable: true,
primaryKey: true,
defaultValue: true,
comment: true,
};
export function getColumnEditorControls(dbType: DatabaseType | undefined): ColumnEditorControls {
if (dbType === "manticoresearch") {
return {
length: true,
nullable: false,
primaryKey: false,
defaultValue: false,
comment: false,
};
}
return DEFAULT_COLUMN_EDITOR_CONTROLS;
}
export function isProtectedManticoreIdColumn(dbType: DatabaseType | undefined, columnName: string): boolean {
return dbType === "manticoresearch" && columnName.trim().toLowerCase() === "id";
}
export function canEditManticoreColumnProperties(dbType: DatabaseType | undefined, hasOriginalColumn: boolean): boolean {
return dbType === "manticoresearch" && !hasOriginalColumn;
}
export const DEFAULT_TYPE_LENGTHS: Record<string, string> = {
tinyint: "4",
"tinyint unsigned": "4",
@@ -363,11 +401,68 @@ export function parseExtraToColumnExtra(extra: string | null | undefined, databa
};
}
}
} else if (databaseType === "manticoresearch") {
const tokens = new Set(lower.split(/\s+/).filter(Boolean));
if (tokens.has("indexed")) result.manticoreIndexed = true;
if (tokens.has("stored")) result.manticoreStored = true;
if (tokens.has("attribute")) result.manticoreAttribute = true;
if (/secondary_index\s*=\s*['"]?1['"]?/.test(lower)) result.manticoreSecondaryIndex = true;
}
return result;
}
const MANTICORE_COLUMN_PROPERTY_TOKENS = new Set(["indexed", "stored", "attribute"]);
function splitManticoreDdlColumnLine(line: string): { name: string; dataType: string; extra: string } | null {
const trimmed = line.trim().replace(/,$/, "").trim();
if (!trimmed || trimmed.startsWith(")") || trimmed.startsWith("(")) return null;
let name = "";
let rest = "";
const quoted = trimmed.match(/^`((?:``|[^`])+)`\s+(.+)$/);
if (quoted) {
name = quoted[1]!.replace(/``/g, "`");
rest = quoted[2]!.trim();
} else {
const plain = trimmed.match(/^([A-Za-z_][\w$]*)\s+(.+)$/);
if (!plain) return null;
name = plain[1]!;
rest = plain[2]!.trim();
}
const parts = rest.split(/\s+/).filter(Boolean);
const dataType = parts.shift() ?? "";
const properties = parts.filter((part) => {
const normalized = part.toLowerCase();
return MANTICORE_COLUMN_PROPERTY_TOKENS.has(normalized) || /^secondary_index\s*=/.test(normalized);
});
if (!name || !dataType || properties.length === 0) return null;
return { name, dataType, extra: properties.join(" ") };
}
export function applyManticoreDdlColumnExtras(columns: ColumnInfo[], ddl: string): ColumnInfo[] {
if (!ddl.trim()) return columns;
const extrasByColumn = new Map<string, { dataType: string; extra: string }>();
for (const line of ddl.split(/\r?\n/)) {
const parsed = splitManticoreDdlColumnLine(line);
if (parsed) extrasByColumn.set(parsed.name.toLowerCase(), { dataType: parsed.dataType, extra: parsed.extra });
}
if (extrasByColumn.size === 0) return columns;
return columns.map((column) => {
const ddlColumn = extrasByColumn.get(column.name.toLowerCase());
if (!ddlColumn) return column;
const existingExtra = column.extra?.trim();
return {
...column,
data_type: ddlColumn.dataType || column.data_type,
extra: existingExtra ? `${existingExtra} ${ddlColumn.extra}` : ddlColumn.extra,
};
});
}
export function createColumnDrafts(columns: ColumnInfo[], databaseType?: DatabaseType): EditableStructureColumn[] {
return columns.map((column, index) => ({
id: `existing:${column.name}`,
+1 -1
View File
@@ -95,4 +95,4 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec
return databaseNames;
}
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
}
}