fix(sql): map uniqueidentifier to uuid for sqlserver-to-postgresql

This commit is contained in:
lxk955
2026-10-11 08:46:36 +08:00
committed by GitHub
parent c744598425
commit 25feadbe37
9 changed files with 369 additions and 3 deletions
@@ -22,6 +22,12 @@ describe("query result transfer value literals", () => {
expect(queryResultTransferSqlLiteral("0x0001fe", "postgres", "timestamp", "bytea", "sqlserver")).toBe("decode('0001fe', 'hex')");
});
it("maps uniqueidentifier and uuid to target types appropriately", () => {
expect(queryResultTransferTargetType("uniqueidentifier", "postgres", "sqlserver")).toBe("uuid");
expect(queryResultTransferTargetType("uuid", "sqlserver", "postgres")).toBe("uniqueidentifier");
expect(queryResultTransferTargetType("uniqueidentifier", "mysql", "sqlserver")).toBe("char(36)");
});
it("fails closed for an unverified binary dialect", () => {
expect(queryResultTransferSqlLiteral("0x0001fe", "hive", "binary", "BINARY")).toBeUndefined();
});
@@ -260,6 +260,12 @@ export function queryResultTransferTargetType(sourceType: string | undefined, da
if (databaseType === "h2" || databaseType === "db2") return "BLOB";
return "BLOB";
}
if (/^(?:uuid|uniqueidentifier)$/.test(type)) {
if (POSTGRES_TYPES.has(databaseType)) return "uuid";
if (databaseType === "sqlserver") return "uniqueidentifier";
if (databaseType === "mysql") return "char(36)";
return textTargetType(databaseType);
}
return textTargetType(databaseType);
}
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { findPreset } from "@/lib/fieldMappingPresets";
describe("fieldMappingPresets", () => {
it("contains sqlserver-to-postgresql preset", () => {
const preset = findPreset("sqlserver", "postgresql");
expect(preset).toBeDefined();
expect(preset?.id).toBe("sqlserver-to-postgresql");
const uuidMapping = preset?.mappings.find((m) => m.sourceType === "UNIQUEIDENTIFIER");
expect(uuidMapping).toBeDefined();
expect(uuidMapping?.targetType).toBe("UUID");
expect(uuidMapping?.paramStrategy).toBe("strip");
});
it("supports postgres alias for postgresql", () => {
const preset = findPreset("sqlserver", "postgres");
expect(preset).toBeDefined();
expect(preset?.id).toBe("sqlserver-to-postgresql");
});
it("supports reverse match postgresql to sqlserver", () => {
const preset = findPreset("postgresql", "sqlserver");
expect(preset).toBeDefined();
expect(preset?.id).toBe("sqlserver-to-postgresql-reverse");
const uuidMapping = preset?.mappings.find((m) => m.sourceType === "UUID");
expect(uuidMapping).toBeDefined();
expect(uuidMapping?.targetType).toBe("UNIQUEIDENTIFIER");
});
it("supports reverse match postgres to sqlserver", () => {
const preset = findPreset("postgres", "sqlserver");
expect(preset).toBeDefined();
expect(preset?.id).toBe("sqlserver-to-postgresql-reverse");
});
});
+48 -2
View File
@@ -85,15 +85,61 @@ export const FIELD_MAPPING_PRESETS: FieldMappingPreset[] = [
{ sourceType: "BOOLEAN", targetType: "NUMBER(1)", paramStrategy: "custom", customParams: "(1)" },
],
},
{
id: "sqlserver-to-postgresql",
label: "SQL Server → PostgreSQL",
sourceDialect: "sqlserver",
targetDialect: "postgresql",
mappings: [
{ sourceType: "VARCHAR", targetType: "VARCHAR", paramStrategy: "preserve" },
{ sourceType: "NVARCHAR", targetType: "VARCHAR", paramStrategy: "preserve" },
{ sourceType: "CHAR", targetType: "CHAR", paramStrategy: "preserve" },
{ sourceType: "NCHAR", targetType: "CHAR", paramStrategy: "preserve" },
{ sourceType: "TEXT", targetType: "TEXT", paramStrategy: "strip" },
{ sourceType: "NTEXT", targetType: "TEXT", paramStrategy: "strip" },
{ sourceType: "INT", targetType: "INTEGER", paramStrategy: "preserve" },
{ sourceType: "BIGINT", targetType: "BIGINT", paramStrategy: "preserve" },
{ sourceType: "SMALLINT", targetType: "SMALLINT", paramStrategy: "preserve" },
{ sourceType: "TINYINT", targetType: "SMALLINT", paramStrategy: "preserve" },
{ sourceType: "BIT", targetType: "BOOLEAN", paramStrategy: "strip" },
{ sourceType: "DECIMAL", targetType: "NUMERIC", paramStrategy: "preserve" },
{ sourceType: "NUMERIC", targetType: "NUMERIC", paramStrategy: "preserve" },
{ sourceType: "MONEY", targetType: "NUMERIC", paramStrategy: "strip" },
{ sourceType: "SMALLMONEY", targetType: "NUMERIC", paramStrategy: "strip" },
{ sourceType: "FLOAT", targetType: "DOUBLE PRECISION", paramStrategy: "strip" },
{ sourceType: "REAL", targetType: "REAL", paramStrategy: "strip" },
{ sourceType: "DATE", targetType: "DATE", paramStrategy: "preserve" },
{ sourceType: "DATETIME", targetType: "TIMESTAMP", paramStrategy: "preserve" },
{ sourceType: "DATETIME2", targetType: "TIMESTAMP", paramStrategy: "preserve" },
{ sourceType: "SMALLDATETIME", targetType: "TIMESTAMP", paramStrategy: "strip" },
{ sourceType: "DATETIMEOFFSET", targetType: "TIMESTAMPTZ", paramStrategy: "preserve" },
{ sourceType: "TIME", targetType: "TIME", paramStrategy: "preserve" },
{ sourceType: "UNIQUEIDENTIFIER", targetType: "UUID", paramStrategy: "strip" },
{ sourceType: "VARBINARY", targetType: "BYTEA", paramStrategy: "strip" },
{ sourceType: "BINARY", targetType: "BYTEA", paramStrategy: "strip" },
{ sourceType: "IMAGE", targetType: "BYTEA", paramStrategy: "strip" },
{ sourceType: "XML", targetType: "XML", paramStrategy: "strip" },
],
},
];
function normalizeDialect(dialect: string): string {
const d = dialect.trim().toLowerCase();
if (d === "postgres" || d === "postgresql") return "postgresql";
if (d === "sqlserver" || d === "mssql") return "sqlserver";
return d;
}
export function findPreset(sourceDialect: string, targetDialect: string): FieldMappingPreset | undefined {
const src = normalizeDialect(sourceDialect);
const tgt = normalizeDialect(targetDialect);
// Look for exact forward match
const forward = FIELD_MAPPING_PRESETS.find((p) => p.sourceDialect === sourceDialect && p.targetDialect === targetDialect);
const forward = FIELD_MAPPING_PRESETS.find((p) => p.sourceDialect === src && p.targetDialect === tgt);
if (forward) return forward;
// Look for reverse match and auto-generate bidirectional preset
const reverse = FIELD_MAPPING_PRESETS.find((p) => p.sourceDialect === targetDialect && p.targetDialect === sourceDialect);
const reverse = FIELD_MAPPING_PRESETS.find((p) => p.sourceDialect === tgt && p.targetDialect === src);
if (reverse) {
return {
id: `${reverse.id}-reverse`,
@@ -363,6 +363,11 @@ const SQLITE_TYPE_MAP: &[TypeMapEntry] = &[
TypeMapEntry { source_base: "JSON", target_template: "TEXT" },
];
const POSTGRES_TYPE_MAP: &[TypeMapEntry] = &[
TypeMapEntry { source_base: "UNIQUEIDENTIFIER", target_template: "UUID" },
TypeMapEntry { source_base: "GUID", target_template: "UUID" },
];
// ---------------------------------------------------------------------------
// Profile families (shared shapes; registration is the only DatabaseType match)
// ---------------------------------------------------------------------------
@@ -469,7 +474,7 @@ fn postgres_family(db: DatabaseType) -> DdlDialectProfile {
owner_alter_template: Some(OWNER_ALTER),
drop_table_template: DROP_TABLE,
lock_timeout_sql: Some("SET lock_timeout = '3s';"),
type_map: &[],
type_map: POSTGRES_TYPE_MAP,
}
}
@@ -870,6 +870,220 @@ impl TypeMappingMatrix {
requires_cast: false,
});
}
(DialectKind::SqlServer, DialectKind::Postgres) => {
rules.push(TypeConversionRule {
source_type: "UNIQUEIDENTIFIER".into(),
target_type: "UUID".into(),
precision_loss: false,
requires_cast: true,
});
rules.push(TypeConversionRule {
source_type: "BIT".into(),
target_type: "BOOLEAN".into(),
precision_loss: false,
requires_cast: true,
});
rules.push(TypeConversionRule {
source_type: "TINYINT".into(),
target_type: "SMALLINT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "SMALLINT".into(),
target_type: "SMALLINT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "INT".into(),
target_type: "INTEGER".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "BIGINT".into(),
target_type: "BIGINT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "REAL".into(),
target_type: "REAL".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "FLOAT".into(),
target_type: "DOUBLE PRECISION".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "MONEY".into(),
target_type: "NUMERIC(19,4)".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "SMALLMONEY".into(),
target_type: "NUMERIC(10,4)".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "NVARCHAR(MAX)".into(),
target_type: "TEXT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "VARCHAR(MAX)".into(),
target_type: "TEXT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "NTEXT".into(),
target_type: "TEXT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "TEXT".into(),
target_type: "TEXT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "VARBINARY(MAX)".into(),
target_type: "BYTEA".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "IMAGE".into(),
target_type: "BYTEA".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "ROWVERSION".into(),
target_type: "BYTEA".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "DATETIME".into(),
target_type: "TIMESTAMP".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "DATETIME2".into(),
target_type: "TIMESTAMP".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "SMALLDATETIME".into(),
target_type: "TIMESTAMP".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "DATETIMEOFFSET".into(),
target_type: "TIMESTAMPTZ".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "XML".into(),
target_type: "XML".into(),
precision_loss: false,
requires_cast: false,
});
}
(DialectKind::Postgres, DialectKind::SqlServer) => {
rules.push(TypeConversionRule {
source_type: "UUID".into(),
target_type: "UNIQUEIDENTIFIER".into(),
precision_loss: false,
requires_cast: true,
});
rules.push(TypeConversionRule {
source_type: "BOOLEAN".into(),
target_type: "BIT".into(),
precision_loss: false,
requires_cast: true,
});
rules.push(TypeConversionRule {
source_type: "SMALLINT".into(),
target_type: "SMALLINT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "INTEGER".into(),
target_type: "INT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "BIGINT".into(),
target_type: "BIGINT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "REAL".into(),
target_type: "REAL".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "DOUBLE PRECISION".into(),
target_type: "FLOAT".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "TEXT".into(),
target_type: "NVARCHAR(MAX)".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "BYTEA".into(),
target_type: "VARBINARY(MAX)".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "TIMESTAMP".into(),
target_type: "DATETIME2".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "TIMESTAMPTZ".into(),
target_type: "DATETIMEOFFSET".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "JSONB".into(),
target_type: "NVARCHAR(MAX)".into(),
precision_loss: false,
requires_cast: false,
});
rules.push(TypeConversionRule {
source_type: "JSON".into(),
target_type: "NVARCHAR(MAX)".into(),
precision_loss: false,
requires_cast: false,
});
}
_ => {}
}
rules
@@ -1146,4 +1360,12 @@ mod tests {
assert!(!desc.supports_schemas);
assert!(!desc.supports_partitioning);
}
#[test]
fn sqlserver_to_postgres_mapping() {
let matrix = TypeMappingMatrix::for_dialects(DialectKind::SqlServer, DialectKind::Postgres);
let (converted, requires_cast) = matrix.convert_type("uniqueidentifier");
assert_eq!(converted, "UUID");
assert!(requires_cast);
}
}
@@ -196,6 +196,8 @@ impl TypeInferenceEngine for DefaultTypeInferenceEngine {
("boolean", "bool"),
("timestamp", "datetime"),
("datetime", "timestamp"),
("uniqueidentifier", "uuid"),
("uuid", "uniqueidentifier"),
];
if exact_matches.contains(&(source_lower.as_str(), target_lower.as_str())) {
return SCORE_ALIAS;
@@ -620,4 +620,13 @@ mod tests {
assert_eq!(normalize_len_params("(unclosed"), "(unclosed");
assert_eq!(normalize_len_params(")"), ")");
}
#[test]
fn postgres_rewrites_uniqueidentifier_to_uuid() {
let target = DatabaseType::Postgres;
let source = Some(DialectKind::SqlServer);
assert_eq!(rewrite_column_type("uniqueidentifier", target, source), "UUID");
assert_eq!(rewrite_column_type("UNIQUEIDENTIFIER", target, source), "UUID");
assert_eq!(rewrite_column_type("uniqueidentifier", target, None), "UUID");
}
}
+33
View File
@@ -3219,6 +3219,19 @@ fn column_types_equal_for_dialects(
if source_type.eq_ignore_ascii_case(target_type) {
return true;
}
match (source_dialect, target_dialect) {
(Some(DialectKind::SqlServer), Some(DialectKind::Postgres))
if source_type.eq_ignore_ascii_case("uniqueidentifier") && target_type.eq_ignore_ascii_case("uuid") =>
{
return true;
}
(Some(DialectKind::Postgres), Some(DialectKind::SqlServer))
if source_type.eq_ignore_ascii_case("uuid") && target_type.eq_ignore_ascii_case("uniqueidentifier") =>
{
return true;
}
_ => {}
}
if source_dialect != Some(DialectKind::Mysql) || target_dialect != Some(DialectKind::Mysql) {
return false;
}
@@ -3268,6 +3281,8 @@ fn column_type_similarity_score(source_type: &str, target_type: &str) -> f64 {
("boolean", "bool"),
("timestamp", "datetime"),
("datetime", "timestamp"),
("uniqueidentifier", "uuid"),
("uuid", "uniqueidentifier"),
];
if exact_matches.contains(&(s.as_str(), t.as_str())) {
return 1.0;
@@ -15738,4 +15753,22 @@ mod tests {
assert!(sql.contains("CREATE OR REPLACE FORCE VIEW DBX_TGT.V_NEW"), "{sql}");
assert!(!sql.contains("DBX_TEST.V_NEW"), "{sql}");
}
#[test]
fn uniqueidentifier_and_uuid_cross_dialect_equivalence() {
assert!(column_types_equal_for_dialects(
"uniqueidentifier",
"uuid",
Some(DialectKind::SqlServer),
Some(DialectKind::Postgres),
));
assert!(column_types_equal_for_dialects(
"uuid",
"uniqueidentifier",
Some(DialectKind::Postgres),
Some(DialectKind::SqlServer),
));
assert_eq!(column_type_similarity_score("uniqueidentifier", "uuid"), 1.0);
assert_eq!(column_type_similarity_score("uuid", "uniqueidentifier"), 1.0);
}
}