mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(sql): map uniqueidentifier to uuid for sqlserver-to-postgresql
This commit is contained in:
+6
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user