feat(sqlite): support table structure changes

This commit is contained in:
onenewcode authored and GitHub committed 2026-07-10 20:02:56 +08:00
1 parent f7eec00c8e
commit fc8d407a62
25 files changed
+2320 -50

No files matched your search

@@ -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']" />