diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts index fa9ec06a26..f86d34d374 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts @@ -374,6 +374,18 @@ export const QUESTDB_TYPE_LENGTHS: Record = { decimal: "10,2", }; +export const SQLSERVER_TYPE_LENGTHS: Record = { + decimal: "10,0", + numeric: "10,0", + float: "53", + char: "1", + nchar: "1", + varchar: "255", + nvarchar: "255", + binary: "1", + varbinary: "255", +}; + export const DEFAULT_TYPE_LENGTH_DISABLES: string[] = []; export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [ @@ -425,6 +437,8 @@ export const POSTGRES_TYPE_LENGTH_DISABLES: string[] = [ export const ORACLE_LIKE_TYPE_LENGTH_DISABLES: string[] = ["binary_double", "binary_float", "bigint", "boolean", "bool", "byte", "date", "double", "double precision", "float", "integer", "int", "long", "long raw", "nclob", "real", "smallint", "text", "tinyint"]; +export const SQLSERVER_TYPE_LENGTH_DISABLES: string[] = ["bigint", "bit", "date", "datetime", "image", "int", "integer", "money", "ntext", "real", "smalldatetime", "smallint", "smallmoney", "sql_variant", "text", "timestamp", "tinyint", "uniqueidentifier", "xml"]; + export function parseExtraToColumnExtra(extra: string | null | undefined, databaseType?: DatabaseType): ColumnExtra { const result: ColumnExtra = {}; if (!extra) return result; @@ -900,6 +914,8 @@ export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseT const key = baseType.trim().toLowerCase(); if (_dbType === "questdb") { return QUESTDB_TYPE_LENGTHS[key] ?? ""; + } else if (_dbType === "sqlserver") { + return SQLSERVER_TYPE_LENGTHS[key] ?? ""; } else { return DEFAULT_TYPE_LENGTHS[key] ?? ""; } @@ -916,6 +932,9 @@ export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, base } else if (isOracleLikeStructureType(_dbType)) { // Dameng/Oracle integer aliases have fixed precision; MySQL-style display widths generate invalid DDL. return ORACLE_LIKE_TYPE_LENGTH_DISABLES.includes(key); + } else if (_dbType === "sqlserver") { + // SQL Server exact integer and legacy LOB types do not accept MySQL-style display widths. + return SQLSERVER_TYPE_LENGTH_DISABLES.includes(key); } else if (isMysqlLikeStructureType(_dbType)) { return key === "enum" || key === "set"; } else { diff --git a/crates/dbx-core/src/table_structure_sql/column_format.rs b/crates/dbx-core/src/table_structure_sql/column_format.rs index 58aac43389..13bb785709 100644 --- a/crates/dbx-core/src/table_structure_sql/column_format.rs +++ b/crates/dbx-core/src/table_structure_sql/column_format.rs @@ -143,6 +143,11 @@ pub(super) fn normalize_column_data_type(dialect: StructureDialect, data_type: & return base_type.to_string(); } + if dialect == StructureDialect::SqlServer && is_sqlserver_lengthless_type(base_type) { + // SQL Server exact integer and legacy LOB types do not accept MySQL-style display widths. + return base_type.to_string(); + } + if is_temporal_precision_type(dialect, base_type) { return if is_valid_temporal_precision(params, dialect) { format!("{base_type}({params})") @@ -180,6 +185,32 @@ fn is_oracle_lengthless_type(base_type: &str) -> bool { ) } +fn is_sqlserver_lengthless_type(base_type: &str) -> bool { + let normalized = base_type.split_whitespace().collect::>().join(" ").to_ascii_lowercase(); + matches!( + normalized.as_str(), + "bigint" + | "bit" + | "date" + | "datetime" + | "image" + | "int" + | "integer" + | "money" + | "ntext" + | "real" + | "smalldatetime" + | "smallint" + | "smallmoney" + | "sql_variant" + | "text" + | "timestamp" + | "tinyint" + | "uniqueidentifier" + | "xml" + ) +} + fn normalize_mysql_numeric_attribute_type(base_type: &str, params: &str) -> Option { let mut parts: Vec<&str> = base_type.split_whitespace().collect(); let type_name = parts.first().copied()?.to_ascii_lowercase(); diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index 017371eb5e..ad8c2e113c 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -1269,6 +1269,28 @@ fn builds_sql_server_quoted_column_and_index_statements() { ); } +#[test] +fn sqlserver_strips_mysql_display_width_from_fixed_integer_types() { + let mut id = column("id"); + id.data_type = "int(11)".to_string(); + id.is_nullable = false; + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::SqlServer), + schema: Some("dbo".to_string()), + table_name: "users".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.warnings, Vec::::new()); + assert_eq!(result.statements, vec!["ALTER TABLE [dbo].[users] ADD [id] int NOT NULL;"]); +} + #[test] fn sqlserver_default_changes_drop_old_constraints_with_isolated_batches() { let mut sku = column("sku"); diff --git a/packages/app-tests/tableStructureEditorState.test.ts b/packages/app-tests/tableStructureEditorState.test.ts index ed5343ade4..1571da405a 100644 --- a/packages/app-tests/tableStructureEditorState.test.ts +++ b/packages/app-tests/tableStructureEditorState.test.ts @@ -7,6 +7,7 @@ import { combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, + dataTypeLengthInputValue, generateIndexName, generateUniqueIndexName, getColumnEditorControls, @@ -281,6 +282,16 @@ test("recognizes SQL Server identity-compatible data types", () => { assert.equal(isSqlServerIdentityCompatibleDataType("numeric(18,2)"), false); }); +test("does not add MySQL-style display widths to SQL Server integer types", () => { + assert.equal(combineDataTypeForDatabase("sqlserver", "int", "11"), "int"); + assert.equal(combineDataTypeForDatabase("sqlserver", "integer", "11"), "integer"); + assert.equal(combineDataTypeForDatabase("sqlserver", "bigint", "20"), "bigint"); + assert.equal(dataTypeLengthInputValue("sqlserver", "int(11)"), ""); + assert.equal(combineDataTypeForDatabase("sqlserver", "decimal", "10,0"), "decimal(10,0)"); + assert.equal(combineDataTypeForDatabase("sqlserver", "varchar", "255"), "varchar(255)"); + assert.equal(combineDataTypeForDatabase("sqlserver", "float", "53"), "float(53)"); +}); + test("parses Manticore Search text properties to ColumnExtra", () => { assert.deepEqual(parseExtraToColumnExtra("stored indexed", "manticoresearch"), { manticoreStored: true,