fix(sqlserver): restrict identity to compatible column types

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipgandstaff authored and GitHub committed 2026-07-01 12:48:37 +08:00
1 parent a224a6747c
commit 5d2ce9f189
13 files changed
+144 -12

No files matched your search

@@ -42,6 +42,7 @@ import {
getDataTypeOptions,
getDefaultLengthForType,
isDataTypeLengthDisabled,
isSqlServerIdentityCompatibleDataType,
isProtectedManticoreIdColumn,
parseExtraToColumnExtra,
splitDataType,
@@ -1017,7 +1018,19 @@ function isSqlServerIdentityChecked(column: EditableStructureColumn): boolean {
}
function canEditSqlServerIdentity(column: EditableStructureColumn): boolean {
return !column.original && !column.markedForDrop;
return !column.original && !column.markedForDrop && isSqlServerIdentityCompatibleDataType(column.dataType);
}
function clearSqlServerIdentity(column: EditableStructureColumn) {
column.extra.autoIncrement = false;
column.extra.identity = undefined;
}
function syncSqlServerIdentityForDataType(column: EditableStructureColumn) {
if (databaseType.value !== "sqlserver") return;
if (!isSqlServerIdentityChecked(column)) return;
if (isSqlServerIdentityCompatibleDataType(column.dataType)) return;
clearSqlServerIdentity(column);
}
function ensureSqlServerIdentity(column: EditableStructureColumn) {
@@ -1034,8 +1047,7 @@ function setSqlServerIdentity(column: EditableStructureColumn, checked: boolean)
ensureSqlServerIdentity(column);
column.isNullable = false;
} else {
column.extra.autoIncrement = false;
column.extra.identity = undefined;
clearSqlServerIdentity(column);
}
}
@@ -1059,6 +1071,16 @@ function updateSqlServerIdentityIncrement(column: EditableStructureColumn, value
column.extra.identity!.increment = parseOptionalNumberInput(value);
}
function updateColumnDataType(column: EditableStructureColumn, baseType: string) {
column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType));
syncSqlServerIdentityForDataType(column);
}
function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) {
column.dataType = combineDataTypeForDatabase(databaseType.value, splitDataType(column.dataType).baseType, String(value));
syncSqlServerIdentityForDataType(column);
}
function moveColumnTo(index: number, insertionIndex: number) {
if (!canDropColumnAt(index, insertionIndex)) return;
const nextColumns = [...columns.value];
@@ -1797,17 +1819,12 @@ watch(activeTab, (tab) => {
:loading-text="t('common.loading')"
:allow-custom="true"
:trigger-class="[structureMonoControlClass, 'w-full']"
@update:model-value="(v: string) => (column.dataType = combineDataTypeForDatabase(databaseType, v, getDefaultLengthForType(databaseType, v)))"
@update:model-value="(v: string) => updateColumnDataType(column, v)"
/>
<Input v-else :model-value="splitDataType(column.dataType).baseType" :class="[structureMonoControlClass, 'w-full']" disabled />
</td>
<td v-if="columnEditorControls.length" :class="structureCellClass">
<Input
:model-value="dataTypeLengthInputValue(databaseType, column.dataType)"
:class="structureMonoControlClass"
:disabled="isColumnLengthDisabled(column)"
@update:model-value="column.dataType = combineDataTypeForDatabase(databaseType, splitDataType(column.dataType).baseType, String($event))"
/>
<Input :model-value="dataTypeLengthInputValue(databaseType, column.dataType)" :class="structureMonoControlClass" :disabled="isColumnLengthDisabled(column)" @update:model-value="updateColumnDataTypeLength(column, $event)" />
</td>
<td v-if="columnEditorControls.nullable" :class="structureCellClass">
<label class="flex items-center gap-1.5">
@@ -1967,7 +1984,7 @@ watch(activeTab, (tab) => {
</template>
<!-- SQL Server: IDENTITY -->
<template v-else-if="structureDialect === 'sqlserver'">
<label :class="structurePropertyLabelClass" :title="t('structureEditor.identity')">
<label :class="structurePropertyLabelClass" :title="canEditSqlServerIdentity(column) || isSqlServerIdentityChecked(column) ? t('structureEditor.identity') : t('structureEditor.sqlServerIdentityTypeHint')">
<input :checked="isSqlServerIdentityChecked(column)" type="checkbox" :class="[structureCheckboxClass, 'shrink-0']" :disabled="!canEditSqlServerIdentity(column)" @change="setSqlServerIdentity(column, ($event.target as HTMLInputElement).checked)" />
<span class="min-w-0 truncate">{{ t("structureEditor.autoIncrement") }}</span>
</label>
+1
View File
@@ -1591,6 +1591,7 @@ export default {
identityGeneration: "Generation",
identitySeed: "Seed",
identityIncrement: "Increment",
sqlServerIdentityTypeHint: "SQL Server identity only supports tinyint, smallint, int, bigint, and decimal/numeric with scale 0",
},
diagram: {
title: "Relationship Diagram",
+1
View File
@@ -1572,6 +1572,7 @@ export default withEnglishFallback({
identityGeneration: "Generación",
identitySeed: "Semilla",
identityIncrement: "Incremento",
sqlServerIdentityTypeHint: "La identidad de SQL Server solo admite tinyint, smallint, int, bigint y decimal/numeric con escala 0",
},
diagram: {
title: "Diagrama de relaciones",
+1
View File
@@ -1592,6 +1592,7 @@ export default withEnglishFallback({
identityGeneration: "Generazione",
identitySeed: "Seed",
identityIncrement: "Incremento",
sqlServerIdentityTypeHint: "Identity di SQL Server supporta solo tinyint, smallint, int, bigint e decimal/numeric con scala 0",
},
diagram: {
title: "Diagramma delle Relazioni",
+1
View File
@@ -1606,6 +1606,7 @@ export default withEnglishFallback({
identityGeneration: "生成",
identitySeed: "開始値",
identityIncrement: "増分",
sqlServerIdentityTypeHint: "SQL Server の ID 列は tinyint、smallint、int、bigint、scale 0 の decimal/numeric のみ対応します",
},
diagram: {
title: "リレーション図",
+1
View File
@@ -1584,6 +1584,7 @@ export default withEnglishFallback({
identityGeneration: "Geração",
identitySeed: "Valor inicial",
identityIncrement: "Incremento",
sqlServerIdentityTypeHint: "A identidade do SQL Server só aceita tinyint, smallint, int, bigint e decimal/numeric com escala 0",
},
diagram: {
title: "Diagrama de relacionamentos",
+1
View File
@@ -1594,6 +1594,7 @@ export default withEnglishFallback({
identityGeneration: "生成方式",
identitySeed: "起始值",
identityIncrement: "增量",
sqlServerIdentityTypeHint: "SQL Server 自增仅支持 tinyint、smallint、int、bigint、decimal/numeric(小数位为 0)",
},
diagram: {
title: "关系图",
+1
View File
@@ -1528,6 +1528,7 @@ export default withEnglishFallback({
identityGeneration: "產生方式",
identitySeed: "起始值",
identityIncrement: "增量",
sqlServerIdentityTypeHint: "SQL Server 自動遞增僅支援 tinyint、smallint、int、bigint、decimal/numeric(小數位為 0)",
},
diagram: {
title: "關係圖",
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { combineDataTypeForDatabase, createColumnDrafts, dataTypeLengthInputValue, isDataTypeLengthDisabled, splitDataType } from "../tableStructureEditorState";
import { combineDataTypeForDatabase, createColumnDrafts, dataTypeLengthInputValue, isDataTypeLengthDisabled, isSqlServerIdentityCompatibleDataType, splitDataType } from "../tableStructureEditorState";
describe("tableStructureEditorState", () => {
it("keeps mysql unsigned attributes in the editable base type", () => {
@@ -70,4 +70,13 @@ describe("tableStructureEditorState", () => {
expect(drafts.map((draft) => draft.defaultValue)).toEqual(["''", "1", "sysdatetime()", "'prefix (internal)'"]);
expect(drafts.map((draft) => draft.original?.column_default)).toEqual(["''", "1", "sysdatetime()", "'prefix (internal)'"]);
});
it("limits SQL Server identity columns to supported data types", () => {
expect(isSqlServerIdentityCompatibleDataType("int")).toBe(true);
expect(isSqlServerIdentityCompatibleDataType("bigint")).toBe(true);
expect(isSqlServerIdentityCompatibleDataType("numeric(18, 0)")).toBe(true);
expect(isSqlServerIdentityCompatibleDataType("decimal(10)")).toBe(true);
expect(isSqlServerIdentityCompatibleDataType("varchar(255)")).toBe(false);
expect(isSqlServerIdentityCompatibleDataType("numeric(18, 2)")).toBe(false);
});
});
@@ -725,6 +725,20 @@ export function splitDataType(raw: string): { baseType: string; params: string }
return { baseType, params };
}
export function isSqlServerIdentityCompatibleDataType(rawDataType: string): boolean {
const { baseType, params } = splitDataType(rawDataType);
const normalized = baseType.trim().replace(/\s+/g, " ").toLowerCase();
if (["tinyint", "smallint", "int", "integer", "bigint"].includes(normalized)) return true;
if (normalized !== "decimal" && normalized !== "numeric") return false;
const normalizedParams = params.replace(/\s+/g, "");
if (!normalizedParams) return true;
const parts = normalizedParams.split(",");
if (parts.length === 1) return /^\d+$/.test(parts[0] ?? "");
if (parts.length !== 2) return false;
const [precision, scale] = parts;
return /^\d+$/.test(precision ?? "") && scale === "0";
}
export function combineDataType(baseType: string, params: string): string {
const type = baseType.trim();
const p = params.trim();