feat(sqlite): support table structure changes
This commit is contained in:
1 parent
f7eec00c8e
commit
fc8d407a62
25 files changed
+2320
-50
No files matched your search
@@ -156,6 +156,7 @@ import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
|
||||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities";
|
||||
import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory";
|
||||
import { getDataGridConditionSuggestionPosition } from "@/lib/dataGrid/dataGridConditionSuggestionPosition";
|
||||
@@ -346,7 +347,9 @@ const columnTypeMap = computed(() => {
|
||||
}
|
||||
return map;
|
||||
});
|
||||
const resolvedDatabaseType = computed(() => props.databaseType ?? effectiveDatabaseTypeForConnection(connectionStore.getConfig(props.connectionId ?? "")));
|
||||
const resolvedConnectionConfig = computed(() => connectionStore.getConfig(props.connectionId ?? ""));
|
||||
const resolvedDatabaseType = computed(() => props.databaseType ?? effectiveDatabaseTypeForConnection(resolvedConnectionConfig.value));
|
||||
const tableStructureCapabilities = computed(() => getTableStructureCapabilities(resolvedDatabaseType.value, resolvedConnectionConfig.value?.db_type));
|
||||
|
||||
const columnCommentMap = computed(() => {
|
||||
const map = new Map<string, string>();
|
||||
@@ -7467,6 +7470,7 @@ async function copyHeaderColumn() {
|
||||
|
||||
const canCopyAlterColumnSql = computed(() => {
|
||||
if (!contextHeaderColumn.value || !props.tableMeta?.columns) return false;
|
||||
if (tableStructureCapabilities.value.alterStrategy !== "direct") return false;
|
||||
return props.tableMeta.columns.some((c) => c.name.toLowerCase() === contextHeaderColumn.value!.toLowerCase());
|
||||
});
|
||||
|
||||
@@ -7825,7 +7829,7 @@ function toggleCellDetailPanelLayout() {
|
||||
|
||||
const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(props.databaseType));
|
||||
const canOpenTableStructureEditor = computed(() => !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && supportsTableStructureEditing(resolvedDatabaseType.value));
|
||||
const mongoConnectionConfig = computed(() => connectionStore.getConfig(props.connectionId ?? ""));
|
||||
const mongoConnectionConfig = resolvedConnectionConfig;
|
||||
const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && mongoConnectionConfig.value?.db_type === "mongodb" && mongoConnectionConfig.value?.driver_profile !== "mongodb-legacy");
|
||||
const tableInfoTabs = computed(() => {
|
||||
const tabs: TableInfoTabItem[] = [];
|
||||
|
||||
@@ -23,7 +23,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
|
||||
import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
|
||||
import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
|
||||
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
|
||||
import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
getColumnEditorControls,
|
||||
getDataTypeOptions,
|
||||
getDefaultLengthForType,
|
||||
hasExistingColumnTypeChange,
|
||||
isDataTypeLengthDisabled,
|
||||
isMysqlEnumDataType,
|
||||
isMysqlCharacterDataType,
|
||||
@@ -151,6 +152,7 @@ const columns = ref<EditableStructureColumn[]>([]);
|
||||
const indexes = ref<EditableStructureIndex[]>([]);
|
||||
const pendingStatements = ref<string[]>([]);
|
||||
const warnings = ref<string[]>([]);
|
||||
const sqliteSchemaRevision = ref<string>();
|
||||
const foreignKeys = ref<EditableStructureForeignKey[]>([]);
|
||||
const triggers = ref<EditableStructureTrigger[]>([]);
|
||||
const secondaryMetadataLoading = computed(() => indexesLoading.value || foreignKeysLoading.value || triggersLoading.value);
|
||||
@@ -591,7 +593,7 @@ function onIndexColResize(e: MouseEvent, col: number) {
|
||||
|
||||
const connection = computed(() => (props.connectionId ? store.getConfig(props.connectionId) : undefined));
|
||||
const databaseType = computed(() => tableStructureDatabaseTypeForConnection(connection.value));
|
||||
const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value));
|
||||
const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value, connection.value?.db_type));
|
||||
const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value));
|
||||
const structureDialect = computed(() => structureCapabilities.value.dialect);
|
||||
const isTableCommentDisabled = computed(() => !structureCapabilities.value.comment);
|
||||
@@ -790,6 +792,8 @@ 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 usesSqliteRebuildStrategy = computed(() => !isCreateMode.value && structureCapabilities.value.alterStrategy === "sqlite-rebuild");
|
||||
const hasSqliteTypeChange = computed(() => usesSqliteRebuildStrategy.value && hasExistingColumnTypeChange(columns.value));
|
||||
const canAddColumn = computed(() => canAddTableStructureColumn(databaseType.value, isCreateMode.value));
|
||||
const newTableName = ref("");
|
||||
const tableComment = ref("");
|
||||
@@ -977,6 +981,7 @@ function clearSqlPreviewState() {
|
||||
sqlPreviewLoading.value = false;
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
}
|
||||
|
||||
function dataTypeOptionsCacheKey(connectionId: string, database: string) {
|
||||
@@ -1031,49 +1036,33 @@ async function loadDynamicDataTypeOptions() {
|
||||
}
|
||||
|
||||
function scheduleSqlPreviewRefresh() {
|
||||
if (hydratingRestoredDraft || needsColumnDraftMetadataHydration()) {
|
||||
if (sqlPreviewDebounceTimer) {
|
||||
clearTimeout(sqlPreviewDebounceTimer);
|
||||
sqlPreviewDebounceTimer = undefined;
|
||||
}
|
||||
sqlPreviewRequestId++;
|
||||
deferredSqlPreviewRefresh = false;
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqlPreviewLoading.value = true;
|
||||
return;
|
||||
}
|
||||
if (!hasPendingStructureChanges()) {
|
||||
clearSqlPreviewState();
|
||||
return;
|
||||
}
|
||||
if (!isCreateMode.value && secondaryMetadataLoading.value) {
|
||||
if (sqlPreviewDebounceTimer) {
|
||||
clearTimeout(sqlPreviewDebounceTimer);
|
||||
sqlPreviewDebounceTimer = undefined;
|
||||
}
|
||||
deferredSqlPreviewRefresh = true;
|
||||
sqlPreviewLoading.value = true;
|
||||
return;
|
||||
if (sqlPreviewDebounceTimer) {
|
||||
clearTimeout(sqlPreviewDebounceTimer);
|
||||
sqlPreviewDebounceTimer = undefined;
|
||||
}
|
||||
sqlPreviewRequestId++;
|
||||
deferredSqlPreviewRefresh = false;
|
||||
if (sqlPreviewDebounceTimer) clearTimeout(sqlPreviewDebounceTimer);
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
if (!hasPendingStructureChanges()) {
|
||||
sqlPreviewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
sqlPreviewLoading.value = true;
|
||||
if (hydratingRestoredDraft || needsColumnDraftMetadataHydration()) return;
|
||||
if (!isCreateMode.value && secondaryMetadataLoading.value) {
|
||||
deferredSqlPreviewRefresh = true;
|
||||
return;
|
||||
}
|
||||
sqlPreviewDebounceTimer = setTimeout(() => {
|
||||
sqlPreviewDebounceTimer = undefined;
|
||||
void refreshSqlPreview();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
async function refreshSqlPreview() {
|
||||
const requestId = ++sqlPreviewRequestId;
|
||||
if (!hasPendingStructureChanges()) {
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqlPreviewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
sqlPreviewLoading.value = true;
|
||||
const options = {
|
||||
function structureChangeOptions(): BuildTableStructureChangeSqlOptions {
|
||||
return {
|
||||
databaseType: databaseType.value,
|
||||
schema: props.schema,
|
||||
tableName: isCreateMode.value ? newTableName.value : props.tableName || "",
|
||||
@@ -1084,22 +1073,47 @@ async function refreshSqlPreview() {
|
||||
tableComment: tableComment.value,
|
||||
originalTableComment: isCreateMode.value ? undefined : originalTableComment.value,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshSqlPreview() {
|
||||
const requestId = ++sqlPreviewRequestId;
|
||||
if (!hasPendingStructureChanges()) {
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
sqlPreviewLoading.value = false;
|
||||
return;
|
||||
}
|
||||
sqlPreviewLoading.value = true;
|
||||
const options = structureChangeOptions();
|
||||
try {
|
||||
const result = isCreateMode.value ? await api.buildCreateTableSql(options) : await api.buildTableStructureChangeSql(options);
|
||||
const result = isCreateMode.value ? await api.buildCreateTableSql(options) : hasSqliteTypeChange.value ? await api.previewSqliteTableStructureChange(props.connectionId, props.database, options) : await api.buildTableStructureChangeSql(options);
|
||||
if (requestId !== sqlPreviewRequestId) return;
|
||||
pendingStatements.value = result.statements;
|
||||
warnings.value = result.warnings;
|
||||
sqliteSchemaRevision.value = "schemaRevision" in result && typeof result.schemaRevision === "string" ? result.schemaRevision : undefined;
|
||||
} catch (e: any) {
|
||||
if (requestId !== sqlPreviewRequestId) return;
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [e?.message || String(e)];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
} finally {
|
||||
if (requestId === sqlPreviewRequestId) sqlPreviewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const canApply = computed(
|
||||
() => !loading.value && !saving.value && !postSaveRefreshing.value && !secondaryMetadataLoading.value && !sqlPreviewLoading.value && pendingStatements.value.length > 0 && warnings.value.length === 0 && !!props.connectionId && (isCreateMode.value ? !!newTableName.value.trim() : !!props.tableName),
|
||||
() =>
|
||||
!loading.value &&
|
||||
!saving.value &&
|
||||
!postSaveRefreshing.value &&
|
||||
!secondaryMetadataLoading.value &&
|
||||
!sqlPreviewLoading.value &&
|
||||
pendingStatements.value.length > 0 &&
|
||||
warnings.value.length === 0 &&
|
||||
(!hasSqliteTypeChange.value || !!sqliteSchemaRevision.value) &&
|
||||
!!props.connectionId &&
|
||||
(isCreateMode.value ? !!newTableName.value.trim() : !!props.tableName),
|
||||
);
|
||||
|
||||
function clearDraft() {
|
||||
@@ -1120,6 +1134,7 @@ function resetState() {
|
||||
indexes.value = [];
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
foreignKeys.value = [];
|
||||
triggers.value = [];
|
||||
ddlContent.value = "";
|
||||
@@ -1965,7 +1980,7 @@ async function recordStructureHistory(sql: string, start: number, success: boole
|
||||
success,
|
||||
error,
|
||||
activity_kind: "schema_change",
|
||||
operation: primarySqlOperation(sql),
|
||||
operation: hasSqliteTypeChange.value ? "ALTER TABLE" : primarySqlOperation(sql),
|
||||
target: isCreateMode.value ? newTableName.value.trim() : props.tableName,
|
||||
affected_rows: success ? result?.affected_rows : undefined,
|
||||
});
|
||||
@@ -1998,12 +2013,14 @@ async function applyChanges() {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const connection = store.getConfig(props.connectionId);
|
||||
const timeoutSecs = queryTimeoutSecsForConnection(connection);
|
||||
const result = await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, timeoutSecs);
|
||||
const result = hasSqliteTypeChange.value
|
||||
? await api.applySqliteTableStructureChange(props.connectionId, props.database, structureChangeOptions(), sqliteSchemaRevision.value!)
|
||||
: await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, queryTimeoutSecsForConnection(connection));
|
||||
await recordStructureHistory(sql, startedAt, true, result);
|
||||
toast(t("structureEditor.saved"), 2500);
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqliteSchemaRevision.value = undefined;
|
||||
ddlFetched.value = false;
|
||||
ddlContent.value = "";
|
||||
if (isCreateMode.value) {
|
||||
@@ -2194,7 +2211,7 @@ watch([() => props.connectionId, () => props.database, databaseType], () => {
|
||||
});
|
||||
|
||||
watch(
|
||||
[isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes, foreignKeys, triggers],
|
||||
[isCreateMode, () => props.connectionId, () => props.database, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes, foreignKeys, triggers],
|
||||
() => {
|
||||
scheduleSqlPreviewRefresh();
|
||||
syncDraftToParent();
|
||||
@@ -2955,6 +2972,10 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!sqlPreviewCollapsed" class="min-h-0 flex-1 overflow-auto p-2.5">
|
||||
<div v-if="hasSqliteTypeChange" class="mb-2 flex gap-1.5 rounded-md border border-blue-300/40 bg-blue-500/10 px-[var(--structure-cell-px)] py-[var(--structure-cell-py)] text-[length:var(--structure-font-size)] text-blue-700 dark:text-blue-300">
|
||||
<Info :class="[structureIconClass, 'mt-0.5 shrink-0']" />
|
||||
<span>{{ t("structureEditor.sqliteRebuildNotice") }}</span>
|
||||
</div>
|
||||
<div v-if="warnings.length" class="mb-2 space-y-1">
|
||||
<div v-for="warning in warnings" :key="warning" class="flex gap-1.5 rounded-md border border-yellow-300/40 bg-yellow-500/10 px-[var(--structure-cell-px)] py-[var(--structure-cell-py)] text-[length:var(--structure-font-size)] text-yellow-700 dark:text-yellow-300">
|
||||
<AlertTriangle :class="[structureIconClass, 'mt-0.5 shrink-0']" />
|
||||
|
||||
Reference in new issue
Block a user