mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +00:00
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:
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -95,4 +95,4 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec
|
||||
return databaseNames;
|
||||
}
|
||||
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user