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:
parent
93d2e7dc18
commit
f385386284
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -387,7 +387,7 @@
|
|||
"metadataConnectionScoped": false,
|
||||
"skipTcpProbe": false,
|
||||
"defaultPort": 9306,
|
||||
"supportLevel": "browse",
|
||||
"supportLevel": "operate",
|
||||
"capabilities": {
|
||||
"queryExecution": true,
|
||||
"metadataBrowse": true,
|
||||
|
|
@ -395,8 +395,8 @@
|
|||
"objectSource": false,
|
||||
"schemaSearch": false,
|
||||
"diagram": false,
|
||||
"tableDataEdit": false,
|
||||
"tableStructureEdit": false,
|
||||
"tableDataEdit": true,
|
||||
"tableStructureEdit": true,
|
||||
"tableImport": false,
|
||||
"dataTransfer": false,
|
||||
"sqlFileExecution": true,
|
||||
|
|
|
|||
|
|
@ -252,7 +252,10 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND ");
|
||||
statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("UPDATE {table} SET {sets} WHERE {where_clause}"),
|
||||
));
|
||||
}
|
||||
statements
|
||||
}
|
||||
|
|
@ -528,7 +531,10 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
|||
row,
|
||||
column_info,
|
||||
);
|
||||
statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("UPDATE {table} SET {sets} WHERE {where_clause}"),
|
||||
));
|
||||
}
|
||||
|
||||
for row_index in &options.deleted_rows {
|
||||
|
|
@ -542,7 +548,8 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
|||
row,
|
||||
column_info,
|
||||
);
|
||||
statements.push(format!("DELETE FROM {table} WHERE {where_clause};"));
|
||||
statements
|
||||
.push(data_grid_statement(options.database_type, format!("DELETE FROM {table} WHERE {where_clause}")));
|
||||
}
|
||||
|
||||
for row in &options.new_rows {
|
||||
|
|
@ -568,7 +575,10 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("INSERT INTO {table} ({columns}) VALUES ({values})"),
|
||||
));
|
||||
}
|
||||
|
||||
statements
|
||||
|
|
@ -591,7 +601,8 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
|||
for row in &options.new_rows {
|
||||
let where_clause = build_row_where(options.database_type, &save_columns, row, column_info);
|
||||
if !where_clause.is_empty() {
|
||||
statements.push(format!("DELETE FROM {table} WHERE {where_clause};"));
|
||||
statements
|
||||
.push(data_grid_statement(options.database_type, format!("DELETE FROM {table} WHERE {where_clause}")));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -617,7 +628,10 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("INSERT INTO {table} ({columns}) VALUES ({values})"),
|
||||
));
|
||||
}
|
||||
|
||||
for (row_index, changes) in &options.dirty_rows {
|
||||
|
|
@ -668,9 +682,12 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
|||
predicates.extend(writable_changes.iter().map(|((_, value), column)| {
|
||||
build_column_predicate(options.database_type, column, value, column_info_for(column_info, column))
|
||||
}));
|
||||
statements.push(format!(
|
||||
"UPDATE {table} SET {sets} WHERE {};",
|
||||
predicates.into_iter().filter(|part| !part.is_empty()).collect::<Vec<_>>().join(" AND ")
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!(
|
||||
"UPDATE {table} SET {sets} WHERE {}",
|
||||
predicates.into_iter().filter(|part| !part.is_empty()).collect::<Vec<_>>().join(" AND ")
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -749,6 +766,11 @@ pub fn format_grid_sql_literal(
|
|||
return format_pg_array_sql_literal(arr);
|
||||
}
|
||||
let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string);
|
||||
if database_type == Some(DatabaseType::ManticoreSearch) {
|
||||
if let Some(typed_value) = manticore_typed_attribute_value(&text, column_info) {
|
||||
return format_grid_sql_literal(&typed_value, database_type, column_info);
|
||||
}
|
||||
}
|
||||
if text.is_empty() {
|
||||
return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string();
|
||||
}
|
||||
|
|
@ -779,6 +801,20 @@ fn is_bit_column_type(data_type: &str) -> bool {
|
|||
lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == "bit")
|
||||
}
|
||||
|
||||
fn manticore_typed_attribute_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Option<Value> {
|
||||
let data_type = column_info?.data_type.to_ascii_lowercase();
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("true") {
|
||||
return Some(Value::Bool(true));
|
||||
}
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("false") {
|
||||
return Some(Value::Bool(false));
|
||||
}
|
||||
if is_numeric_type(&data_type) && is_numeric_literal(text) {
|
||||
return text.parse::<serde_json::Number>().ok().map(Value::Number);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn format_mysql_bit_literal_text(text: &str) -> Option<String> {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.eq_ignore_ascii_case("true") {
|
||||
|
|
@ -1032,6 +1068,14 @@ fn build_column_predicate(
|
|||
}
|
||||
}
|
||||
|
||||
fn data_grid_statement(database_type: Option<DatabaseType>, sql: String) -> String {
|
||||
if database_type == Some(DatabaseType::ManticoreSearch) {
|
||||
sql
|
||||
} else {
|
||||
format!("{sql};")
|
||||
}
|
||||
}
|
||||
|
||||
fn uses_mysql_binary_text_predicate(
|
||||
database_type: Option<DatabaseType>,
|
||||
value: &Value,
|
||||
|
|
@ -1250,6 +1294,7 @@ fn uses_keyless_row_predicate(database_type: Option<DatabaseType>) -> bool {
|
|||
database_type,
|
||||
Some(
|
||||
DatabaseType::Mysql
|
||||
| DatabaseType::ManticoreSearch
|
||||
| DatabaseType::Postgres
|
||||
| DatabaseType::Sqlite
|
||||
| DatabaseType::DuckDb
|
||||
|
|
@ -1568,6 +1613,35 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_manticore_save_statements_without_trailing_semicolons() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
table_meta: DataGridTableMeta {
|
||||
schema: None,
|
||||
table_name: "rt_products".to_string(),
|
||||
primary_keys: vec![],
|
||||
columns: Some(vec![column("id", "bigint", false, None), column("title", "text", true, None)]),
|
||||
},
|
||||
columns: vec!["id".to_string(), "title".to_string()],
|
||||
source_columns: None,
|
||||
rows: vec![vec![json!("1"), json!("old")], vec![json!("2"), json!("deleted")]],
|
||||
dirty_rows: vec![(0, vec![(1, json!("new"))])],
|
||||
deleted_rows: vec![1],
|
||||
new_rows: vec![vec![json!("3"), json!("inserted")]],
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"UPDATE `rt_products` SET `title` = 'new' WHERE `id` = 1 AND `title` = 'old'",
|
||||
"DELETE FROM `rt_products` WHERE `id` = 2 AND `title` = 'deleted'",
|
||||
"INSERT INTO `rt_products` (`id`, `title`) VALUES (3, 'inserted')",
|
||||
]
|
||||
);
|
||||
assert!(result.rollback_statements.iter().all(|statement| !statement.ends_with(';')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_duplicate_inserted_primary_keys() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
use mysql_async::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::{ColumnInfo, IndexInfo, ObjectInfo};
|
||||
|
||||
use super::mysql::{self, MySqlPool};
|
||||
|
||||
fn quote_identifier(value: &str) -> String {
|
||||
format!("`{}`", value.replace('`', "``"))
|
||||
}
|
||||
|
||||
fn row_get<T, I>(row: &mysql_async::Row, index: I) -> Option<T>
|
||||
where
|
||||
T: mysql_async::prelude::FromValue,
|
||||
I: mysql_async::prelude::ColumnIndex,
|
||||
{
|
||||
row.get_opt::<T, I>(index).and_then(|result| result.ok())
|
||||
}
|
||||
|
||||
fn get_str_by_name(row: &mysql_async::Row, name: &str) -> String {
|
||||
row_get::<String, _>(row, name)
|
||||
.or_else(|| row_get::<Vec<u8>, _>(row, name).map(|bytes| String::from_utf8_lossy(&bytes).to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_opt_str(row: &mysql_async::Row, name: &str) -> Option<String> {
|
||||
row_get::<String, _>(row, name)
|
||||
.or_else(|| row_get::<Vec<u8>, _>(row, name).map(|bytes| String::from_utf8_lossy(&bytes).to_string()))
|
||||
}
|
||||
|
||||
fn preferred_metadata(primary: Option<String>, fallback: Option<String>) -> Option<String> {
|
||||
primary.filter(|value| !value.trim().is_empty()).or_else(|| fallback.filter(|value| !value.trim().is_empty()))
|
||||
}
|
||||
|
||||
pub async fn list_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
|
||||
let (tables, functions) = tokio::join!(mysql::list_tables_show(pool, database), list_udf_objects(pool, database));
|
||||
let mut objects: Vec<ObjectInfo> = tables?
|
||||
.into_iter()
|
||||
.map(|table| ObjectInfo {
|
||||
name: table.name,
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
parent_schema: table.parent_schema,
|
||||
parent_name: table.parent_name,
|
||||
})
|
||||
.collect();
|
||||
|
||||
match functions {
|
||||
Ok(functions) => objects.extend(functions),
|
||||
Err(err) => log::warn!("Skipping UDFs for Manticore Search database `{}` in object browser: {}", database, err),
|
||||
}
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
async fn list_udf_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let result = conn.query_iter("SHOW PLUGINS").await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|err| err.to_string())?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
plugin_object(
|
||||
&get_str_by_name(row, "Type"),
|
||||
&get_str_by_name(row, "Name"),
|
||||
get_opt_str(row, "Library").as_deref(),
|
||||
get_opt_str(row, "Extra").as_deref(),
|
||||
database,
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn plugin_object(
|
||||
plugin_type: &str,
|
||||
name: &str,
|
||||
library: Option<&str>,
|
||||
extra: Option<&str>,
|
||||
database: &str,
|
||||
) -> Option<ObjectInfo> {
|
||||
if !plugin_type.eq_ignore_ascii_case("udf") {
|
||||
return None;
|
||||
}
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut comment_parts = Vec::new();
|
||||
if let Some(library) = library.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
comment_parts.push(format!("SONAME {library}"));
|
||||
}
|
||||
if let Some(return_type) = extra.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
comment_parts.push(format!("RETURNS {return_type}"));
|
||||
}
|
||||
Some(ObjectInfo {
|
||||
name: name.to_string(),
|
||||
object_type: "FUNCTION".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
comment: if comment_parts.is_empty() { None } else { Some(comment_parts.join(", ")) },
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
parent_schema: None,
|
||||
parent_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_columns(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let mut columns = mysql::get_columns_show(pool, database, table).await?;
|
||||
let properties = column_properties(pool, database, table).await.unwrap_or_default();
|
||||
for column in &mut columns {
|
||||
if let Some(property) = properties.get(&column.name.to_lowercase()) {
|
||||
column.extra = preferred_metadata(column.extra.take(), Some(property.clone()));
|
||||
}
|
||||
}
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
async fn column_properties(pool: &MySqlPool, database: &str, table: &str) -> Result<HashMap<String, String>, String> {
|
||||
let sql = show_columns_sql(database, table, true);
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = match conn.query_iter(&sql).await {
|
||||
Ok(result) => result.collect_and_drop().await.map_err(|err| err.to_string())?,
|
||||
Err(_) => {
|
||||
let sql = show_columns_sql(database, table, false);
|
||||
let result = conn.query_iter(&sql).await.map_err(|err| err.to_string())?;
|
||||
result.collect_and_drop().await.map_err(|err| err.to_string())?
|
||||
}
|
||||
};
|
||||
Ok(rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
let name = get_str_by_name(row, "Field").trim().to_string();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
get_opt_str(row, "Properties")
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|properties| (name.to_lowercase(), properties))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn show_columns_sql(database: &str, table: &str, full: bool) -> String {
|
||||
let prefix = if full { "SHOW FULL COLUMNS FROM" } else { "SHOW COLUMNS FROM" };
|
||||
if database.trim().is_empty() {
|
||||
format!("{prefix} {}", quote_identifier(table))
|
||||
} else {
|
||||
format!("{prefix} {}.{}", quote_identifier(database), quote_identifier(table))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_indexes(pool: &MySqlPool, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let sql = list_indexes_sql(table);
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let result = conn.query_iter(&sql).await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(rows.iter().map(index_info_from_row).collect())
|
||||
}
|
||||
|
||||
fn list_indexes_sql(table: &str) -> String {
|
||||
format!("SHOW TABLE INDEXES FROM {}", quote_identifier(table))
|
||||
}
|
||||
|
||||
fn index_info_from_row(row: &mysql_async::Row) -> IndexInfo {
|
||||
let name = get_str_by_name(row, "Name");
|
||||
let enabled = get_str_by_name(row, "Enabled");
|
||||
let percent = get_str_by_name(row, "Percent");
|
||||
let comment_parts: Vec<String> = [
|
||||
(!enabled.is_empty()).then(|| format!("Enabled: {enabled}")),
|
||||
(!percent.is_empty()).then(|| format!("Percent: {percent}")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
IndexInfo {
|
||||
name: name.clone(),
|
||||
columns: vec![name],
|
||||
is_unique: false,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: Some(get_str_by_name(row, "Type")),
|
||||
included_columns: None,
|
||||
comment: (!comment_parts.is_empty()).then(|| comment_parts.join(", ")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn show_metadata_uses_unqualified_table_names_when_database_is_empty() {
|
||||
assert_eq!(show_columns_sql("", "idx", true), "SHOW FULL COLUMNS FROM `idx`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_extra_falls_back_to_properties() {
|
||||
assert_eq!(
|
||||
preferred_metadata(Some("auto_increment".to_string()), Some("indexed attribute".to_string())),
|
||||
Some("auto_increment".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
preferred_metadata(Some(" ".to_string()), Some("indexed attribute".to_string())),
|
||||
Some("indexed attribute".to_string())
|
||||
);
|
||||
assert_eq!(preferred_metadata(None, Some(" ".to_string())), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexes_sql_uses_show_table_indexes() {
|
||||
assert_eq!(list_indexes_sql("materials"), "SHOW TABLE INDEXES FROM `materials`");
|
||||
assert_eq!(list_indexes_sql("odd`name"), "SHOW TABLE INDEXES FROM `odd``name`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udf_plugin_maps_to_function_object() {
|
||||
let object = plugin_object("udf", "sequence", Some("udfexample.dll"), Some("INT"), "app").expect("udf plugin");
|
||||
|
||||
assert_eq!(object.name, "sequence");
|
||||
assert_eq!(object.object_type, "FUNCTION");
|
||||
assert_eq!(object.schema.as_deref(), Some("app"));
|
||||
assert_eq!(object.comment.as_deref(), Some("SONAME udfexample.dll, RETURNS INT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_udf_plugins_are_not_sidebar_functions() {
|
||||
assert!(plugin_object("ranker", "bm25", Some("ranker.so"), None, "app").is_none());
|
||||
assert!(plugin_object("udf", " ", Some("udf.so"), Some("INT"), "app").is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ pub mod elasticsearch_driver;
|
|||
pub mod elasticsearch_sql;
|
||||
pub mod file_validator;
|
||||
pub mod influxdb_driver;
|
||||
pub mod manticoresearch;
|
||||
pub mod mongo_driver;
|
||||
pub mod mysql;
|
||||
pub mod ob_oracle;
|
||||
|
|
|
|||
|
|
@ -698,6 +698,20 @@ mod tests {
|
|||
assert_eq!(filtered.into_iter().map(|database| database.name).collect::<Vec<_>>(), vec!["Manticore"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_show_metadata_uses_unqualified_table_names() {
|
||||
let config = test_connection_config(DatabaseType::ManticoreSearch);
|
||||
|
||||
assert_eq!(super::mysql_show_metadata_database_for_config(Some(&config), "Manticore"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doris_show_metadata_keeps_database_qualifier() {
|
||||
let config = test_connection_config(DatabaseType::Doris);
|
||||
|
||||
assert_eq!(super::mysql_show_metadata_database_for_config(Some(&config), "analytics"), "analytics");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doris_database_list_keeps_system_databases() {
|
||||
let databases = vec![test_database_info("information_schema"), test_database_info("analytics")];
|
||||
|
|
@ -922,6 +936,8 @@ async fn list_objects_once(
|
|||
// Note: mysql and ob_oracle take different second args (database vs schema)
|
||||
if *mode == MysqlMode::OceanBaseOracle {
|
||||
db::ob_oracle::list_objects(p, schema).await
|
||||
} else if db_config.as_ref().is_some_and(is_manticoresearch_config) {
|
||||
db::manticoresearch::list_objects(p, database).await
|
||||
} else if db_config.as_ref().is_some_and(is_doris_family_config) {
|
||||
db::mysql::list_table_objects_show(p, database).await
|
||||
} else {
|
||||
|
|
@ -1215,8 +1231,13 @@ pub async fn get_columns_core(
|
|||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_manticoresearch_config) => {
|
||||
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
|
||||
db::manticoresearch::get_columns(p, metadata_database, table).await.map(deduplicate_column_infos)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_doris_family_config) => {
|
||||
db::mysql::get_columns_show(p, database, table).await.map(deduplicate_column_infos)
|
||||
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
|
||||
db::mysql::get_columns_show(p, metadata_database, table).await.map(deduplicate_column_infos)
|
||||
}
|
||||
PoolKind::Mysql(p, mode) => {
|
||||
dispatch_mysql!(p, mode, db::mysql::get_columns, db::ob_oracle::get_columns, database, table)
|
||||
|
|
@ -1282,6 +1303,7 @@ pub async fn list_indexes_core(
|
|||
table: &str,
|
||||
) -> Result<Vec<db::IndexInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
||||
{
|
||||
let connections = state.connections.read().await;
|
||||
|
|
@ -1294,6 +1316,9 @@ pub async fn list_indexes_core(
|
|||
|
||||
match pool {
|
||||
PoolKind::Mysql(p, mode) => {
|
||||
if db_config.as_ref().is_some_and(is_manticoresearch_config) {
|
||||
return db::manticoresearch::list_indexes(p, table).await;
|
||||
}
|
||||
dispatch_mysql!(p, mode, db::mysql::list_indexes, db::ob_oracle::list_indexes, schema, table)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_indexes(p, schema, table).await,
|
||||
|
|
@ -1514,6 +1539,14 @@ fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
|
|||
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
|
||||
}
|
||||
|
||||
fn mysql_show_metadata_database_for_config<'a>(config: Option<&ConnectionConfig>, database: &'a str) -> &'a str {
|
||||
if config.is_some_and(is_manticoresearch_config) {
|
||||
""
|
||||
} else {
|
||||
database
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_mysql_system_databases_for_config(
|
||||
databases: Vec<db::DatabaseInfo>,
|
||||
config: Option<&ConnectionConfig>,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ pub(in crate::schema) async fn list_objects(
|
|||
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
|
||||
db::ob_oracle::list_objects(p, schema).await.map(Some)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
|
||||
db::manticoresearch::list_objects(p, database).await.map(Some)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
|
||||
db::mysql::list_table_objects_show(p, database).await.map(Some)
|
||||
}
|
||||
|
|
@ -103,8 +106,13 @@ pub(in crate::schema) async fn get_columns(
|
|||
table: &str,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
match pool {
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
|
||||
let metadata_database = mysql_show_metadata_database_for_config(config, database);
|
||||
db::manticoresearch::get_columns(p, metadata_database, table).await
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
|
||||
db::mysql::get_columns_show(p, database, table).await
|
||||
let metadata_database = mysql_show_metadata_database_for_config(config, database);
|
||||
db::mysql::get_columns_show(p, metadata_database, table).await
|
||||
}
|
||||
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
|
||||
db::ob_oracle::get_columns(p, database, table).await
|
||||
|
|
@ -273,6 +281,17 @@ fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
|
|||
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
|
||||
}
|
||||
|
||||
fn mysql_show_metadata_database_for_config<'a>(
|
||||
config: Option<&ConnectionConfig>,
|
||||
database: &'a str,
|
||||
) -> &'a str {
|
||||
if config.is_some_and(is_manticoresearch_config) {
|
||||
""
|
||||
} else {
|
||||
database
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_mysql_system_databases_for_config(
|
||||
databases: Vec<db::DatabaseInfo>,
|
||||
config: Option<&ConnectionConfig>,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use super::comments::build_sqlserver_column_comment_sql;
|
|||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
|
||||
use super::util::{
|
||||
clean, format_default_for_sql, normalize_default, original_comment, original_default, qualified_table, quote_ident,
|
||||
quote_string,
|
||||
clean, format_default_for_sql, is_protected_manticore_id_column, normalize_default, original_comment,
|
||||
original_default, qualified_table, quote_ident, quote_string,
|
||||
};
|
||||
use crate::table_structure_sql::ColumnExtra;
|
||||
|
||||
|
|
@ -30,6 +30,10 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
|
|||
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
if is_protected_manticore_id_column(dialect, &original.name) {
|
||||
warnings.push("Manticore Search id column cannot be dropped from this editor.".to_string());
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
|
|
@ -95,6 +99,22 @@ fn is_column_extra_empty(extra: &ColumnExtra) -> bool {
|
|||
!extra.auto_increment.unwrap_or(false)
|
||||
&& !extra.on_update_current_timestamp.unwrap_or(false)
|
||||
&& extra.identity.is_none()
|
||||
&& !extra.manticore_indexed.unwrap_or(false)
|
||||
&& !extra.manticore_stored.unwrap_or(false)
|
||||
&& !extra.manticore_attribute.unwrap_or(false)
|
||||
&& !extra.manticore_secondary_index.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn original_manticore_extra_flags(extra: &str) -> (bool, bool, bool, bool) {
|
||||
let lower = extra.to_lowercase();
|
||||
(
|
||||
lower.split_whitespace().any(|token| token == "indexed"),
|
||||
lower.split_whitespace().any(|token| token == "stored"),
|
||||
lower.split_whitespace().any(|token| token == "attribute"),
|
||||
lower.contains("secondary_index='1'")
|
||||
|| lower.contains("secondary_index=\"1\"")
|
||||
|| lower.contains("secondary_index=1"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool {
|
||||
|
|
@ -103,8 +123,18 @@ pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool
|
|||
match (current_extra, original.extra.as_deref()) {
|
||||
// Neither has extra → no change
|
||||
(None, None | Some("")) => false,
|
||||
// Current extra is empty (all None) → no effective extra
|
||||
(Some(curr), _) if is_column_extra_empty(curr) => false,
|
||||
// Current extra is empty (all None) → changed only if the original had effective extra
|
||||
(Some(curr), None | Some("")) if is_column_extra_empty(curr) => false,
|
||||
(Some(curr), Some(orig)) if is_column_extra_empty(curr) => {
|
||||
let (indexed, stored, attribute, secondary_index) = original_manticore_extra_flags(orig);
|
||||
let orig_lower = orig.to_lowercase();
|
||||
orig_lower.contains("auto_increment")
|
||||
|| orig_lower.contains("on update")
|
||||
|| indexed
|
||||
|| stored
|
||||
|| attribute
|
||||
|| secondary_index
|
||||
}
|
||||
// Extra added or removed
|
||||
(Some(_), None | Some("")) => true,
|
||||
(None, Some(_)) => true,
|
||||
|
|
@ -116,8 +146,18 @@ pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool
|
|||
let curr_has_on_update = curr.on_update_current_timestamp.unwrap_or(false);
|
||||
let orig_has_on_update = orig_lower.contains("on update");
|
||||
let curr_has_identity = curr.identity.is_some();
|
||||
let curr_manticore = (
|
||||
curr.manticore_indexed.unwrap_or(false),
|
||||
curr.manticore_stored.unwrap_or(false),
|
||||
curr.manticore_attribute.unwrap_or(false),
|
||||
curr.manticore_secondary_index.unwrap_or(false),
|
||||
);
|
||||
let orig_manticore = original_manticore_extra_flags(orig);
|
||||
// identity is harder to detect in free-form original.extra, so treat it as changed if present
|
||||
curr_has_ai != orig_has_ai || curr_has_on_update != orig_has_on_update || curr_has_identity
|
||||
curr_has_ai != orig_has_ai
|
||||
|| curr_has_on_update != orig_has_on_update
|
||||
|| curr_has_identity
|
||||
|| curr_manticore != orig_manticore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,44 @@ pub(super) fn column_data_type(dialect: StructureDialect, column: &EditableStruc
|
|||
if dialect == StructureDialect::ClickHouse {
|
||||
return clickhouse_column_type(column);
|
||||
}
|
||||
if dialect == StructureDialect::ManticoreSearch {
|
||||
return manticore_column_type(column);
|
||||
}
|
||||
normalize_column_data_type(dialect, &column.data_type)
|
||||
}
|
||||
|
||||
fn manticore_column_type(column: &EditableStructureColumn) -> String {
|
||||
let data_type = normalize_column_data_type(StructureDialect::ManticoreSearch, &column.data_type);
|
||||
let normalized = data_type.trim().to_ascii_lowercase();
|
||||
if normalized == "json" {
|
||||
let Some(extra) = column.extra.as_ref() else {
|
||||
return data_type;
|
||||
};
|
||||
if extra.manticore_secondary_index.unwrap_or(false) {
|
||||
return format!("{data_type} secondary_index='1'");
|
||||
}
|
||||
return data_type;
|
||||
}
|
||||
if !matches!(normalized.as_str(), "text" | "string") {
|
||||
return data_type;
|
||||
}
|
||||
|
||||
let Some(extra) = column.extra.as_ref() else {
|
||||
return data_type;
|
||||
};
|
||||
let mut parts = vec![data_type];
|
||||
if extra.manticore_stored.unwrap_or(false) {
|
||||
parts.push("stored".to_string());
|
||||
}
|
||||
if extra.manticore_attribute.unwrap_or(false) {
|
||||
parts.push("attribute".to_string());
|
||||
}
|
||||
if extra.manticore_indexed.unwrap_or(false) {
|
||||
parts.push("indexed".to_string());
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
pub(super) fn normalize_column_data_type(dialect: StructureDialect, data_type: &str) -> String {
|
||||
let trimmed = data_type.trim();
|
||||
let Some(open_index) = trimmed.find('(') else {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use super::comments::build_sqlserver_column_comment_sql;
|
|||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::types::{EditableStructureColumn, TableStructureSqlOptions};
|
||||
use super::util::{
|
||||
clean, normalize_default, original_comment, original_default, qualified_table, quote_ident, quote_string,
|
||||
clean, is_protected_manticore_id_column, normalize_default, original_comment, original_default, qualified_table,
|
||||
quote_ident, quote_string,
|
||||
};
|
||||
|
||||
pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) -> Vec<String> {
|
||||
|
|
@ -34,6 +35,10 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
|||
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
|
||||
continue;
|
||||
}
|
||||
if is_protected_manticore_id_column(dialect, &original.name) {
|
||||
warnings.push("Manticore Search id column cannot be dropped from this editor.".to_string());
|
||||
continue;
|
||||
}
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,14 +31,17 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu
|
|||
for column in &active_columns {
|
||||
let data_type = column_data_type(dialect, column);
|
||||
let mut parts = vec![quote_ident(dialect, &column.name), data_type];
|
||||
if !column.is_nullable && !column.is_primary_key && dialect != StructureDialect::ClickHouse {
|
||||
if !column.is_nullable
|
||||
&& !column.is_primary_key
|
||||
&& !matches!(dialect, StructureDialect::ClickHouse | StructureDialect::ManticoreSearch)
|
||||
{
|
||||
parts.push("NOT NULL".to_string());
|
||||
}
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
}
|
||||
let default_value = normalize_default(Some(&column.default_value));
|
||||
if !default_value.is_empty() {
|
||||
if !default_value.is_empty() && dialect != StructureDialect::ManticoreSearch {
|
||||
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
|
||||
}
|
||||
if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) {
|
||||
|
|
@ -52,7 +55,10 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu
|
|||
column_definitions.push(parts.join(" "));
|
||||
}
|
||||
|
||||
let pk_columns: Vec<_> = active_columns.iter().filter(|column| column.is_primary_key).collect();
|
||||
let pk_columns: Vec<_> = active_columns
|
||||
.iter()
|
||||
.filter(|column| column.is_primary_key && dialect != StructureDialect::ManticoreSearch)
|
||||
.collect();
|
||||
if !pk_columns.is_empty() {
|
||||
let pk_list = pk_columns.iter().map(|column| quote_ident(dialect, &column.name)).collect::<Vec<_>>().join(", ");
|
||||
column_definitions.push(format!("PRIMARY KEY ({pk_list})"));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub(super) enum StructureDialect {
|
|||
Oracle,
|
||||
H2,
|
||||
ClickHouse,
|
||||
ManticoreSearch,
|
||||
Informix,
|
||||
Unsupported,
|
||||
}
|
||||
|
|
@ -204,6 +205,12 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
|||
comment: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::ManticoreSearch) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::ManticoreSearch,
|
||||
add_column: true,
|
||||
drop_column: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::Informix) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::Informix,
|
||||
add_column: true,
|
||||
|
|
@ -245,6 +252,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
|
|||
StructureDialect::Oracle => "oracle",
|
||||
StructureDialect::H2 => "h2",
|
||||
StructureDialect::ClickHouse => "clickhouse",
|
||||
StructureDialect::ManticoreSearch => "manticoresearch",
|
||||
StructureDialect::Informix => "informix",
|
||||
StructureDialect::Unsupported => "this database",
|
||||
}
|
||||
|
|
@ -262,6 +270,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option<Dat
|
|||
StructureDialect::Oracle => Some(DatabaseType::Oracle),
|
||||
StructureDialect::H2 => Some(DatabaseType::H2),
|
||||
StructureDialect::ClickHouse => Some(DatabaseType::ClickHouse),
|
||||
StructureDialect::ManticoreSearch => Some(DatabaseType::ManticoreSearch),
|
||||
StructureDialect::Informix => Some(DatabaseType::Informix),
|
||||
StructureDialect::Unsupported => None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,76 @@ fn mysql_create_index_with_comment() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_create_table_sql_only() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.is_nullable = false;
|
||||
let mut views = column("views");
|
||||
views.data_type = "int".to_string();
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title, views],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `title` text,\n `views` int\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_add_and_drop_column_sql() {
|
||||
let mut old_code = column("code");
|
||||
old_code.data_type = "string".to_string();
|
||||
old_code.marked_for_drop = true;
|
||||
old_code.original = Some(ColumnInfo {
|
||||
name: "code".to_string(),
|
||||
data_type: "string".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra =
|
||||
Some(ColumnExtra { manticore_attribute: Some(true), manticore_indexed: Some(true), ..Default::default() });
|
||||
let mut resource = column("resource");
|
||||
resource.data_type = "json".to_string();
|
||||
resource.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![old_code, name, resource],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `materials` DROP COLUMN `code`;",
|
||||
"ALTER TABLE `materials` ADD COLUMN `name` string attribute indexed;",
|
||||
"ALTER TABLE `materials` ADD COLUMN `resource` json secondary_index='1';",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gbase8a_uses_limited_mysql_ddl() {
|
||||
let mut renamed = column("display_email");
|
||||
|
|
@ -278,6 +348,193 @@ fn gbase8a_uses_limited_mysql_ddl() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_does_not_drop_id_column() {
|
||||
let mut id = column("id");
|
||||
id.data_type = "bigint".to_string();
|
||||
id.marked_for_drop = true;
|
||||
id.original = Some(ColumnInfo {
|
||||
name: "id".to_string(),
|
||||
data_type: "bigint".to_string(),
|
||||
is_nullable: false,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, Vec::<String>::new());
|
||||
assert_eq!(result.warnings, vec!["Manticore Search id column cannot be dropped from this editor."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_warns_when_existing_column_properties_change() {
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra = Some(ColumnExtra {
|
||||
manticore_indexed: Some(true),
|
||||
manticore_stored: Some(true),
|
||||
manticore_attribute: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
name.original = Some(ColumnInfo {
|
||||
name: "name".to_string(),
|
||||
data_type: "string".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut resource = column("resource");
|
||||
resource.data_type = "json".to_string();
|
||||
resource.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
resource.original = Some(ColumnInfo {
|
||||
name: "resource".to_string(),
|
||||
data_type: "json".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut old_resource = column("old_resource");
|
||||
old_resource.data_type = "json".to_string();
|
||||
old_resource.extra = Some(ColumnExtra::default());
|
||||
old_resource.original = Some(ColumnInfo {
|
||||
name: "old_resource".to_string(),
|
||||
data_type: "json".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("secondary_index='1'".to_string()),
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![name, resource, old_resource],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.warnings,
|
||||
vec![
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_ignores_mysql_column_options() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.is_nullable = false;
|
||||
title.is_primary_key = true;
|
||||
title.default_value = "'untitled'".to_string();
|
||||
title.comment = "Title text".to_string();
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `title` text\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_text_column_properties() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.extra =
|
||||
Some(ColumnExtra { manticore_indexed: Some(true), manticore_stored: Some(true), ..Default::default() });
|
||||
let mut sku = column("sku");
|
||||
sku.data_type = "string".to_string();
|
||||
sku.extra =
|
||||
Some(ColumnExtra { manticore_indexed: Some(true), manticore_attribute: Some(true), ..Default::default() });
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra = Some(ColumnExtra {
|
||||
manticore_indexed: Some(true),
|
||||
manticore_stored: Some(true),
|
||||
manticore_attribute: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title, sku, name],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"CREATE TABLE `materials` (\n `title` text stored indexed,\n `sku` string attribute indexed,\n `name` string stored attribute indexed\n);"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_json_secondary_index_property() {
|
||||
let mut metadata = column("metadata");
|
||||
metadata.data_type = "json".to_string();
|
||||
metadata.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![metadata],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `metadata` json secondary_index='1'\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_unique_index_with_comment_and_btree() {
|
||||
let mut idx = index("uniq_users_email", &["email"]);
|
||||
|
|
@ -854,7 +1111,7 @@ fn mysql_create_table_with_auto_increment() {
|
|||
col.data_type = "int".to_string();
|
||||
col.is_nullable = false;
|
||||
col.is_primary_key = true;
|
||||
col.extra = Some(ColumnExtra { auto_increment: Some(true), on_update_current_timestamp: None, identity: None });
|
||||
col.extra = Some(ColumnExtra { auto_increment: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
|
|
@ -879,7 +1136,7 @@ fn mysql_create_table_with_on_update_current_timestamp() {
|
|||
col.data_type = "timestamp".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { auto_increment: None, on_update_current_timestamp: Some(true), identity: None });
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
|
|
@ -903,9 +1160,8 @@ fn postgres_create_table_with_identity() {
|
|||
col.data_type = "integer".to_string();
|
||||
col.is_nullable = false;
|
||||
col.extra = Some(ColumnExtra {
|
||||
auto_increment: None,
|
||||
on_update_current_timestamp: None,
|
||||
identity: Some(ColumnIdentity { generation: Some("BY DEFAULT".to_string()), seed: None, increment: None }),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
|
|
@ -931,8 +1187,8 @@ fn sqlserver_create_table_with_identity() {
|
|||
col.is_nullable = false;
|
||||
col.extra = Some(ColumnExtra {
|
||||
auto_increment: Some(true),
|
||||
on_update_current_timestamp: None,
|
||||
identity: Some(ColumnIdentity { generation: None, seed: Some(100), increment: Some(5) }),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub struct EditableStructureColumn {
|
|||
pub marked_for_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ColumnExtra {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -34,6 +34,14 @@ pub struct ColumnExtra {
|
|||
pub on_update_current_timestamp: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub identity: Option<ColumnIdentity>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_indexed: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_stored: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_attribute: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_secondary_index: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, t
|
|||
|
||||
pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
|
||||
match dialect {
|
||||
StructureDialect::Mysql => format!("`{}`", name.replace('`', "``")),
|
||||
StructureDialect::Mysql | StructureDialect::ManticoreSearch => format!("`{}`", name.replace('`', "``")),
|
||||
StructureDialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
|
||||
StructureDialect::Informix if is_simple_informix_identifier(name) => name.to_string(),
|
||||
_ => format!("\"{}\"", name.replace('"', "\"\"")),
|
||||
|
|
@ -42,6 +42,10 @@ pub(super) fn clean(value: &str) -> String {
|
|||
value.trim().to_string()
|
||||
}
|
||||
|
||||
pub(super) fn is_protected_manticore_id_column(dialect: StructureDialect, column_name: &str) -> bool {
|
||||
dialect == StructureDialect::ManticoreSearch && column_name.trim().eq_ignore_ascii_case("id")
|
||||
}
|
||||
|
||||
pub(super) fn is_temporal_type_for_default(dialect: StructureDialect, base_type: &str) -> bool {
|
||||
let normalized = base_type.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
match dialect {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ fn maps_agent_database_types_to_driver_keys() {
|
|||
assert_eq!(agent_key(&DatabaseType::Firebird, None), Some("firebird"));
|
||||
assert_eq!(agent_key(&DatabaseType::Exasol, None), Some("exasol"));
|
||||
assert_eq!(agent_key(&DatabaseType::OceanbaseOracle, None), Some("oceanbase-oracle"));
|
||||
assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase"));
|
||||
assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase8a"));
|
||||
assert_eq!(agent_key(&DatabaseType::Access, None), Some("access"));
|
||||
assert_eq!(agent_key(&DatabaseType::Oracle, None), Some("oracle"));
|
||||
assert_eq!(agent_key(&DatabaseType::Databend, None), Some("databend"));
|
||||
|
|
@ -279,6 +279,14 @@ fn driver_manifest_declares_expected_product_capabilities() {
|
|||
assert!(!jdbc.capabilities.table_structure_edit);
|
||||
assert!(!jdbc.capabilities.user_admin);
|
||||
|
||||
let manticore = find_driver(DatabaseType::ManticoreSearch);
|
||||
assert_eq!(manticore.support_level, "operate");
|
||||
assert!(manticore.capabilities.metadata_browse);
|
||||
assert!(manticore.capabilities.sql_file_execution);
|
||||
assert!(manticore.capabilities.table_structure_edit);
|
||||
assert!(!manticore.capabilities.object_browser);
|
||||
assert!(manticore.capabilities.table_data_edit);
|
||||
|
||||
let redis = find_driver(DatabaseType::Redis);
|
||||
assert_eq!(redis.support_level, "connect");
|
||||
assert!(!redis.capabilities.object_browser);
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
# Query Result Archive Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add `.dbxresults` export/import so a query tab's saved execution-result runs can be restored later.
|
||||
|
||||
**Architecture:** Add a focused archive codec around the existing tab-result-cache snapshot codec. The query store exposes bytes-in/bytes-out methods, while Vue components handle file dialogs, browser download/upload, and toasts.
|
||||
|
||||
**Tech Stack:** Vue 3, Pinia, TypeScript, MessagePack via `@msgpack/msgpack`, existing DBX tab-result-cache snapshot codec, Vitest.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Archive Codec
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/desktop/src/lib/queryResultArchive.ts`
|
||||
- Test: `packages/app-tests/queryResultArchive.test.ts`
|
||||
|
||||
- [ ] Write failing tests for encoding/decoding a query result archive with multiple runs, rejecting invalid bytes, and producing a binary payload smaller than equivalent JSON for repeated rows.
|
||||
- [ ] Run `pnpm vitest run packages/app-tests/queryResultArchive.test.ts` and confirm it fails because the module does not exist.
|
||||
- [ ] Implement `encodeQueryResultArchive`, `decodeQueryResultArchive`, `defaultQueryResultArchiveFileName`, and archive metadata types.
|
||||
- [ ] Run `pnpm vitest run packages/app-tests/queryResultArchive.test.ts` and confirm it passes.
|
||||
- [ ] Commit `feat(query): add result archive codec`.
|
||||
|
||||
### Task 2: Query Store Import/Export
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/desktop/src/stores/queryStore.ts`
|
||||
- Test: `packages/app-tests/queryStore.test.ts`
|
||||
|
||||
- [ ] Write a failing store test that exports a query tab with two result runs and imports the archive into a new tab with the active run restored.
|
||||
- [ ] Run the focused test and confirm it fails because store archive methods do not exist.
|
||||
- [ ] Add `exportResultArchive(tabId)` and `importResultArchive(bytes)` to the query store. Export should read evicted cache payloads when needed. Import should create a new query tab and project the active archived run into the result grid.
|
||||
- [ ] Run the focused store test and confirm it passes.
|
||||
- [ ] Commit `feat(query): restore result archives`.
|
||||
|
||||
### Task 3: UI Actions
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/desktop/src/components/layout/ContentArea.vue`
|
||||
- Modify: `apps/desktop/src/components/layout/EditorToolbar.vue`
|
||||
- Modify: `apps/desktop/src/App.vue`
|
||||
- Modify: `apps/desktop/src/i18n/locales/en.ts`
|
||||
- Modify: `apps/desktop/src/i18n/locales/zh-CN.ts`
|
||||
|
||||
- [ ] Add an import icon button to the query editor toolbar and wire it to App.
|
||||
- [ ] Add an export button to the result pane when query output exists.
|
||||
- [ ] Implement Tauri save/open using `@tauri-apps/plugin-dialog` and `@tauri-apps/plugin-fs`; implement browser fallback using Blob download and an `<input type="file">`.
|
||||
- [ ] Add English and Simplified Chinese UI strings.
|
||||
- [ ] Run `pnpm typecheck` and confirm it passes.
|
||||
- [ ] Commit `feat(query): add result archive actions`.
|
||||
|
||||
### Task 4: Final Verification
|
||||
|
||||
- [ ] Run `pnpm typecheck`.
|
||||
- [ ] Run `pnpm vitest run packages/app-tests/queryResultArchive.test.ts packages/app-tests/queryStore.test.ts packages/app-tests/openTabsPersistence.test.ts packages/app-tests/tabResultCache.test.ts packages/app-tests/tabPresentation.test.ts`.
|
||||
- [ ] Run `pnpm build`.
|
||||
- [ ] Confirm `git status --short` is clean after commits.
|
||||
|
|
@ -129,6 +129,15 @@ test("describes table editing capabilities for special database engines", () =>
|
|||
transaction: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(getDatabaseCapability("manticoresearch").tableData, {
|
||||
insert: true,
|
||||
updateRequiresPrimaryKey: false,
|
||||
deleteRequiresPrimaryKey: false,
|
||||
keylessRowPredicate: true,
|
||||
requiresTransactionalTableForExistingRows: false,
|
||||
transaction: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(getDatabaseCapability("jdbc").tableData, {
|
||||
insert: false,
|
||||
updateRequiresPrimaryKey: true,
|
||||
|
|
@ -227,7 +236,7 @@ test("describes feature support through capability helpers", () => {
|
|||
assert.equal(supportsTableStructureEditing("informix"), true);
|
||||
assert.equal(supportsTableStructureEditing("rqlite"), true);
|
||||
assert.equal(supportsTableStructureEditing("mongodb"), false);
|
||||
assert.equal(supportsTableStructureEditing("manticoresearch"), false);
|
||||
assert.equal(supportsTableStructureEditing("manticoresearch"), true);
|
||||
assert.equal(supportsDatabaseCreation("clickhouse"), true);
|
||||
assert.equal(supportsDatabaseCreation("manticoresearch"), false);
|
||||
assert.equal(supportsDatabaseCreation("sqlite"), false);
|
||||
|
|
@ -257,7 +266,7 @@ test("describes feature support through capability helpers", () => {
|
|||
test("loads product support levels and capabilities from the driver manifest", () => {
|
||||
assert.equal(manifestDatabaseTypes().includes("mysql"), true);
|
||||
assert.equal(databaseSupportLevel("mysql"), "operate");
|
||||
assert.equal(databaseSupportLevel("manticoresearch"), "browse");
|
||||
assert.equal(databaseSupportLevel("manticoresearch"), "operate");
|
||||
assert.equal(databaseSupportLevel("jdbc"), "browse");
|
||||
assert.equal(databaseSupportLevel("redis"), "connect");
|
||||
|
||||
|
|
@ -290,8 +299,8 @@ test("loads product support levels and capabilities from the driver manifest", (
|
|||
queryExecution: true,
|
||||
metadataBrowse: true,
|
||||
objectBrowser: false,
|
||||
tableDataEdit: false,
|
||||
tableStructureEdit: false,
|
||||
tableDataEdit: true,
|
||||
tableStructureEdit: true,
|
||||
sqlFileExecution: true,
|
||||
userAdmin: false,
|
||||
},
|
||||
|
|
@ -310,6 +319,7 @@ test("object browser entry follows database tree shape", () => {
|
|||
|
||||
test("sidebar object capability registry describes object groups by database type", () => {
|
||||
assert.deepEqual(sidebarObjectKindsForDatabase("databend"), ["TABLE", "VIEW"]);
|
||||
assert.deepEqual(sidebarObjectKindsForDatabase("manticoresearch"), ["TABLE", "FUNCTION"]);
|
||||
assert.deepEqual(sidebarObjectKindsForDatabase("postgres"), ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"]);
|
||||
assert.deepEqual(sidebarObjectKindsForDatabase("oracle"), ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -115,6 +115,50 @@ test("suggests PostgreSQL-specific data types and functions", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("suggests Manticore Search SQL functions and command snippets", () => {
|
||||
const matchItems = buildSqlCompletionItems("select * from products where mat", "select * from products where mat".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
const facetItems = buildSqlCompletionItems("select * from products fac", "select * from products fac".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
const showItems = buildSqlCompletionItems("show m", "show m".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
const showTablesItems = buildSqlCompletionItems("show tab", "show tab".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
const callPqItems = buildSqlCompletionItems("call p", "call p".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
const rankingItems = buildSqlCompletionItems("select bm", "select bm".length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
databaseType: "manticoresearch",
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
matchItems.some((item) => item.type === "function" && item.label === "MATCH" && item.apply === "MATCH(${query})"),
|
||||
);
|
||||
assert.ok(facetItems.some((item) => item.type === "keyword" && item.label === "FACET"));
|
||||
assert.ok(showItems.some((item) => item.type === "snippet" && item.label === "show meta" && item.apply === "SHOW META;"));
|
||||
assert.ok(showTablesItems.some((item) => item.type === "snippet" && item.label === "show tables" && item.apply === "SHOW TABLES;"));
|
||||
assert.ok(
|
||||
callPqItems.some((item) => item.type === "snippet" && item.label === "call pq" && item.apply === "CALL PQ ('pq', ('{\"title\":\"query\"}'));"),
|
||||
);
|
||||
assert.ok(rankingItems.some((item) => item.type === "function" && item.label === "BM25F"));
|
||||
});
|
||||
|
||||
test("MongoDB completion avoids SQL keywords", () => {
|
||||
const items = buildSqlCompletionItems("fi", 2, {
|
||||
tables: [],
|
||||
|
|
|
|||
|
|
@ -52,12 +52,14 @@ test("allows updateable SQL table data editing even without declared primary key
|
|||
assert.equal(isTableDataEditable("informix", []), true);
|
||||
assert.equal(isTableDataEditable("tdengine", []), true);
|
||||
assert.equal(isTableDataEditable("mysql", []), true);
|
||||
assert.equal(isTableDataEditable("manticoresearch", []), true);
|
||||
assert.equal(isTableDataEditable("postgres", []), true);
|
||||
assert.equal(isTableDataEditable("postgres", ["id"]), true);
|
||||
});
|
||||
|
||||
test("does not use transactional grid saves for non-transactional engines", () => {
|
||||
assert.equal(supportsDataGridTransaction("hive"), false);
|
||||
assert.equal(supportsDataGridTransaction("manticoresearch"), false);
|
||||
assert.equal(supportsDataGridTransaction("trino"), false);
|
||||
assert.equal(supportsDataGridTransaction("jdbc"), false);
|
||||
assert.equal(supportsDataGridTransaction("yashandb"), true);
|
||||
|
|
@ -75,6 +77,7 @@ test("allows existing row edits according to database-specific key requirements"
|
|||
assert.equal(canEditExistingTableRows("trino", undefined, []), false);
|
||||
assert.equal(canEditExistingTableRows("trino", undefined, ["id"]), true);
|
||||
assert.equal(canEditExistingTableRows("mysql", undefined, []), true);
|
||||
assert.equal(canEditExistingTableRows("manticoresearch", undefined, []), true);
|
||||
assert.equal(canEditExistingTableRows("postgres", undefined, []), true);
|
||||
assert.equal(canEditExistingTableRows("sqlite", undefined, []), true);
|
||||
assert.equal(canEditExistingTableRows("sqlite", undefined, ["id"]), true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { getTableMetadataCapabilities } from "../../apps/desktop/src/lib/tableMetadataCapabilities.ts";
|
||||
|
||||
test("manticore search exposes secondary indexes but hides relational constraints", () => {
|
||||
assert.deepEqual(getTableMetadataCapabilities("manticoresearch"), {
|
||||
columns: true,
|
||||
indexes: true,
|
||||
foreignKeys: false,
|
||||
triggers: false,
|
||||
ddl: true,
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { canEditTableStructure, getTableStructureCapabilities } from "../../apps/desktop/src/lib/tableStructureCapabilities.ts";
|
||||
import { canAddTableStructureColumn, canEditTableStructure, getTableStructureCapabilities } from "../../apps/desktop/src/lib/tableStructureCapabilities.ts";
|
||||
|
||||
test("sqlite-family and duckdb do not support table comments", () => {
|
||||
for (const dbType of ["sqlite", "rqlite", "duckdb"] as const) {
|
||||
|
|
@ -108,6 +108,32 @@ test("limited analytic engines can open the editor for supported operations only
|
|||
assert.equal(canEditTableStructure("clickhouse"), true);
|
||||
});
|
||||
|
||||
test("manticore search can open the editor for limited table structure changes", () => {
|
||||
const caps = getTableStructureCapabilities("manticoresearch");
|
||||
assert.equal(caps.dialect, "mysql");
|
||||
assert.equal(caps.createTable, true);
|
||||
assert.equal(caps.addColumn, true);
|
||||
assert.equal(caps.dropColumn, true);
|
||||
assert.equal(caps.renameColumn, false);
|
||||
assert.equal(caps.alterExistingColumn, false);
|
||||
assert.equal(caps.createIndex, false);
|
||||
assert.equal(caps.dropIndex, false);
|
||||
assert.equal(canEditTableStructure("manticoresearch"), true);
|
||||
assert.equal(canAddTableStructureColumn("manticoresearch", true), true);
|
||||
assert.equal(canAddTableStructureColumn("manticoresearch", false), true);
|
||||
});
|
||||
|
||||
test("manticore search keeps generic index DDL disabled", () => {
|
||||
const caps = getTableStructureCapabilities("manticoresearch");
|
||||
assert.equal(caps.createIndex, false);
|
||||
assert.equal(caps.dropIndex, false);
|
||||
assert.equal(caps.rebuildIndex, false);
|
||||
assert.equal(caps.indexType, false);
|
||||
assert.equal(caps.indexInclude, false);
|
||||
assert.equal(caps.indexFilter, false);
|
||||
assert.equal(caps.indexComment, false);
|
||||
});
|
||||
|
||||
test("informix exposes conservative structure editing capabilities", () => {
|
||||
const caps = getTableStructureCapabilities("informix");
|
||||
assert.equal(caps.dialect, "informix");
|
||||
|
|
@ -126,7 +152,7 @@ test("informix exposes conservative structure editing capabilities", () => {
|
|||
});
|
||||
|
||||
test("unsupported non-relational databases do not open the structure editor", () => {
|
||||
for (const dbType of ["redis", "mongodb", "elasticsearch", "manticoresearch", "neo4j", undefined] as const) {
|
||||
for (const dbType of ["redis", "mongodb", "elasticsearch", "neo4j", undefined] as const) {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
assert.equal(caps.dialect, "unsupported");
|
||||
assert.equal(canEditTableStructure(dbType), false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getDataTypeOptions, normalizeDataTypeParams, parseExtraToColumnExtra, toColumnNames } from "../../apps/desktop/src/lib/tableStructureEditorState.ts";
|
||||
import { applyManticoreDdlColumnExtras, buildStructureTargetLabel, canEditManticoreColumnProperties, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getColumnEditorControls, getDataTypeOptions, isProtectedManticoreIdColumn, normalizeDataTypeParams, parseExtraToColumnExtra, toColumnNames } from "../../apps/desktop/src/lib/tableStructureEditorState.ts";
|
||||
import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
const columns: ColumnInfo[] = [
|
||||
|
|
@ -77,6 +77,30 @@ test("creates editable column drafts from column metadata", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("applies manticore column properties from ddl", () => {
|
||||
const manticoreColumns: ColumnInfo[] = [
|
||||
{ name: "name", data_type: "string", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: null },
|
||||
{ name: "code", data_type: "string", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: null },
|
||||
{ name: "resource", data_type: "json", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: null },
|
||||
];
|
||||
const ddl = `CREATE TABLE materials (
|
||||
name string indexed attribute,
|
||||
code string attribute,
|
||||
resource json secondary_index='1'
|
||||
)`;
|
||||
|
||||
const drafts = createColumnDrafts(applyManticoreDdlColumnExtras(manticoreColumns, ddl), "manticoresearch");
|
||||
|
||||
assert.deepEqual(
|
||||
drafts.map((draft) => ({ name: draft.name, dataType: draft.dataType, extra: draft.extra })),
|
||||
[
|
||||
{ name: "name", dataType: "string", extra: { manticoreIndexed: true, manticoreAttribute: true } },
|
||||
{ name: "code", dataType: "string", extra: { manticoreAttribute: true } },
|
||||
{ name: "resource", dataType: "json", extra: { manticoreSecondaryIndex: true } },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("parses MySQL extra string to ColumnExtra", () => {
|
||||
assert.deepEqual(parseExtraToColumnExtra("auto_increment", "mysql"), { autoIncrement: true });
|
||||
assert.deepEqual(parseExtraToColumnExtra("on update CURRENT_TIMESTAMP", "mysql"), {
|
||||
|
|
@ -119,6 +143,20 @@ test("parses SQL Server identity extra string to ColumnExtra", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("parses Manticore Search text properties to ColumnExtra", () => {
|
||||
assert.deepEqual(parseExtraToColumnExtra("stored indexed", "manticoresearch"), {
|
||||
manticoreStored: true,
|
||||
manticoreIndexed: true,
|
||||
});
|
||||
assert.deepEqual(parseExtraToColumnExtra("attribute indexed", "manticoresearch"), {
|
||||
manticoreAttribute: true,
|
||||
manticoreIndexed: true,
|
||||
});
|
||||
assert.deepEqual(parseExtraToColumnExtra("secondary_index='1'", "manticoresearch"), {
|
||||
manticoreSecondaryIndex: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("creates editable index drafts and splits pasted column lists", () => {
|
||||
const drafts = createIndexDrafts(indexes);
|
||||
|
||||
|
|
@ -184,3 +222,31 @@ test("returns data type options for compatible table structure editors", () => {
|
|||
assert.equal(getDataTypeOptions("dameng").includes("varchar2"), true);
|
||||
assert.equal(getDataTypeOptions("sqlserver").includes("nvarchar"), true);
|
||||
});
|
||||
|
||||
test("returns Manticore Search data type options", () => {
|
||||
assert.deepEqual(getDataTypeOptions("manticoresearch"), ["text", "string", "int", "bit", "bigint", "bool", "timestamp", "float", "json", "float_vector", "multi", "mva"]);
|
||||
});
|
||||
|
||||
test("returns Manticore Search column editor controls", () => {
|
||||
assert.deepEqual(getColumnEditorControls("manticoresearch"), {
|
||||
length: true,
|
||||
nullable: false,
|
||||
primaryKey: false,
|
||||
defaultValue: false,
|
||||
comment: false,
|
||||
});
|
||||
assert.equal(getColumnEditorControls("mysql").nullable, true);
|
||||
});
|
||||
|
||||
test("protects Manticore Search id column from destructive structure edits", () => {
|
||||
assert.equal(isProtectedManticoreIdColumn("manticoresearch", "id"), true);
|
||||
assert.equal(isProtectedManticoreIdColumn("manticoresearch", "ID"), true);
|
||||
assert.equal(isProtectedManticoreIdColumn("manticoresearch", "name"), false);
|
||||
assert.equal(isProtectedManticoreIdColumn("mysql", "id"), false);
|
||||
});
|
||||
|
||||
test("allows Manticore Search column properties only before the column exists", () => {
|
||||
assert.equal(canEditManticoreColumnProperties("manticoresearch", false), true);
|
||||
assert.equal(canEditManticoreColumnProperties("manticoresearch", true), false);
|
||||
assert.equal(canEditManticoreColumnProperties("mysql", false), false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,12 +79,12 @@ test("driver manifest declares support levels and product capabilities", () => {
|
|||
assert.equal(jdbc?.capabilities.tableStructureEdit, false);
|
||||
|
||||
const manticore = manifest.drivers.find((driver) => driver.dbType === "manticoresearch");
|
||||
assert.equal(manticore?.supportLevel, "browse");
|
||||
assert.equal(manticore?.supportLevel, "operate");
|
||||
assert.equal(manticore?.capabilities.queryExecution, true);
|
||||
assert.equal(manticore?.capabilities.metadataBrowse, true);
|
||||
assert.equal(manticore?.capabilities.objectBrowser, false);
|
||||
assert.equal(manticore?.capabilities.tableDataEdit, false);
|
||||
assert.equal(manticore?.capabilities.tableStructureEdit, false);
|
||||
assert.equal(manticore?.capabilities.tableDataEdit, true);
|
||||
assert.equal(manticore?.capabilities.tableStructureEdit, true);
|
||||
assert.equal(manticore?.capabilities.databaseCreate, false);
|
||||
assert.equal(manticore?.capabilities.userAdmin, false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue