fix: 修复 PostgreSQL 空字符串默认值处理 (#1970)
Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
parent
23b44cc2b9
commit
c4fe7c8ee8
|
|
@ -520,20 +520,40 @@ export function applyManticoreDdlColumnExtras(columns: ColumnInfo[], ddl: string
|
|||
});
|
||||
}
|
||||
|
||||
function isPostgresTextualType(dataType: string): boolean {
|
||||
const baseType = dataType.split("(")[0]?.trim().replace(/\s+/g, " ").toLowerCase() ?? "";
|
||||
return ["char", "character", "varchar", "character varying", "text", "bpchar", "name", "json", "jsonb", "xml", "bytea", "uuid"].includes(baseType);
|
||||
}
|
||||
|
||||
function stripPostgresStringDefaultCast(defaultValue: string, dataType: string): string {
|
||||
if (!isPostgresTextualType(dataType)) return defaultValue;
|
||||
const trimmed = defaultValue.trim();
|
||||
const match = trimmed.match(/^('(?:''|[^'])*')::\s*((?:character\s+varying)|character|varchar|char|text|bpchar|name|jsonb?|xml|bytea|uuid)(?:\s*\(\s*\d+\s*\))?$/i);
|
||||
return match?.[1] ?? defaultValue;
|
||||
}
|
||||
|
||||
function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
|
||||
const defaultValue = column.column_default ?? "";
|
||||
return databaseType === "postgres" ? stripPostgresStringDefaultCast(defaultValue, column.data_type) : defaultValue;
|
||||
}
|
||||
|
||||
export function createColumnDrafts(columns: ColumnInfo[], databaseType?: DatabaseType): EditableStructureColumn[] {
|
||||
return columns.map((column, index) => ({
|
||||
id: `existing:${column.name}`,
|
||||
name: column.name,
|
||||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable,
|
||||
defaultValue: column.column_default ?? "",
|
||||
comment: column.comment ?? "",
|
||||
isPrimaryKey: column.is_primary_key,
|
||||
extra: parseExtraToColumnExtra(column.extra, databaseType),
|
||||
original: column,
|
||||
originalPosition: index,
|
||||
markedForDrop: false,
|
||||
}));
|
||||
return columns.map((column, index) => {
|
||||
const defaultValue = columnDefaultForEditor(column, databaseType);
|
||||
return {
|
||||
id: `existing:${column.name}`,
|
||||
name: column.name,
|
||||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable,
|
||||
defaultValue,
|
||||
comment: column.comment ?? "",
|
||||
isPrimaryKey: column.is_primary_key,
|
||||
extra: parseExtraToColumnExtra(column.extra, databaseType),
|
||||
original: { ...column, column_default: column.column_default === null ? null : defaultValue },
|
||||
originalPosition: index,
|
||||
markedForDrop: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createIndexDrafts(indexes: IndexInfo[]): EditableStructureIndex[] {
|
||||
|
|
|
|||
|
|
@ -1829,3 +1829,78 @@ fn postgres_varchar_default_is_quoted() {
|
|||
|
||||
assert!(result.statements.iter().any(|s| s.contains("SET DEFAULT 'test label'")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_empty_string_default_is_not_quoted_again() {
|
||||
let mut col = column("sku");
|
||||
col.data_type = "character varying".to_string();
|
||||
col.default_value = "''".to_string();
|
||||
col.original = Some(ColumnInfo {
|
||||
name: "sku".to_string(),
|
||||
data_type: "character varying".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: Some(String::new()),
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("core".to_string()),
|
||||
table_name: "products".to_string(),
|
||||
column: col,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, vec!["ALTER TABLE \"core\".\"products\" ALTER COLUMN \"sku\" SET DEFAULT '';"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_string_default_cast_matches_plain_literal() {
|
||||
let mut col = column("category");
|
||||
col.data_type = "character varying".to_string();
|
||||
col.default_value = "''".to_string();
|
||||
col.original = Some(ColumnInfo {
|
||||
name: "category".to_string(),
|
||||
data_type: "character varying".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: Some("''::character varying".to_string()),
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: Some(String::new()),
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("core".to_string()),
|
||||
table_name: "products".to_string(),
|
||||
column: col,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_integer_default_is_not_quoted() {
|
||||
let mut col = column("stock");
|
||||
col.data_type = "integer".to_string();
|
||||
col.default_value = "0".to_string();
|
||||
col.original = Some(ColumnInfo {
|
||||
name: "stock".to_string(),
|
||||
data_type: "integer".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: Some(String::new()),
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("core".to_string()),
|
||||
table_name: "products".to_string(),
|
||||
column: col,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, vec!["ALTER TABLE \"core\".\"products\" ALTER COLUMN \"stock\" SET DEFAULT 0;"]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,72 @@ pub(super) fn quote_string(value: &str) -> String {
|
|||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn is_sql_string_literal(value: &str) -> bool {
|
||||
let trimmed = value.trim();
|
||||
let Some(inner) = trimmed.strip_prefix('\'').and_then(|value| value.strip_suffix('\'')) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut chars = inner.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '\'' && chars.next_if_eq(&'\'').is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn postgres_string_default_literal(value: &str) -> Option<&str> {
|
||||
let trimmed = value.trim();
|
||||
let inner = trimmed.strip_prefix('\'')?;
|
||||
let mut literal_end = 1;
|
||||
let mut chars = inner.char_indices().peekable();
|
||||
while let Some((index, ch)) = chars.next() {
|
||||
if ch == '\'' {
|
||||
if chars.next_if(|(_, next)| *next == '\'').is_some() {
|
||||
continue;
|
||||
}
|
||||
literal_end += index + ch.len_utf8();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if literal_end == 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let literal = &trimmed[..literal_end];
|
||||
if !is_sql_string_literal(literal) {
|
||||
return None;
|
||||
}
|
||||
let cast_type = trimmed[literal_end..].trim().strip_prefix("::")?.trim();
|
||||
if is_postgres_textual_cast_type(cast_type) {
|
||||
Some(literal)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_postgres_textual_cast_type(value: &str) -> bool {
|
||||
let normalized =
|
||||
value.trim().trim_matches('"').split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
let base_type = normalized.split('(').next().unwrap_or(&normalized).trim();
|
||||
matches!(
|
||||
base_type,
|
||||
"char"
|
||||
| "character"
|
||||
| "varchar"
|
||||
| "character varying"
|
||||
| "text"
|
||||
| "bpchar"
|
||||
| "name"
|
||||
| "json"
|
||||
| "jsonb"
|
||||
| "xml"
|
||||
| "bytea"
|
||||
| "uuid"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn clean(value: &str) -> String {
|
||||
value.trim().to_string()
|
||||
}
|
||||
|
|
@ -211,7 +277,11 @@ pub(super) fn format_default_for_sql(dialect: StructureDialect, data_type: &str,
|
|||
if is_string_type_for_default(dialect, base_type) {
|
||||
// Only skip quoting for function-call expressions like `gen_random_uuid()`.
|
||||
// Simple identifiers like `CURRENT_TIMESTAMP` are not valid defaults for string columns.
|
||||
if default_value.contains('(') || default_value.contains(')') {
|
||||
if is_sql_string_literal(default_value)
|
||||
|| (dialect == StructureDialect::Postgres && postgres_string_default_literal(default_value).is_some())
|
||||
|| default_value.contains('(')
|
||||
|| default_value.contains(')')
|
||||
{
|
||||
return default_value.to_string();
|
||||
}
|
||||
return quote_string(default_value);
|
||||
|
|
@ -223,6 +293,8 @@ pub(super) fn normalize_default(value: Option<&String>) -> String {
|
|||
let trimmed = value.map(|value| value.trim()).unwrap_or("");
|
||||
if trimmed.eq_ignore_ascii_case("null") {
|
||||
String::new()
|
||||
} else if let Some(literal) = postgres_string_default_literal(trimmed) {
|
||||
literal.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,48 @@ test("creates editable column drafts from column metadata", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("normalizes PostgreSQL string default casts in editable column drafts", () => {
|
||||
const drafts = createColumnDrafts(
|
||||
[
|
||||
{
|
||||
name: "category",
|
||||
data_type: "character varying",
|
||||
is_nullable: true,
|
||||
column_default: "''::character varying",
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
data_type: "user_status",
|
||||
is_nullable: true,
|
||||
column_default: "'active'::public.user_status",
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
name: "stock",
|
||||
data_type: "integer",
|
||||
is_nullable: true,
|
||||
column_default: "0",
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
"postgres",
|
||||
);
|
||||
|
||||
assert.equal(drafts[0].defaultValue, "''");
|
||||
assert.equal(drafts[0].original?.column_default, "''");
|
||||
assert.equal(drafts[1].defaultValue, "'active'::public.user_status");
|
||||
assert.equal(drafts[1].original?.column_default, "'active'::public.user_status");
|
||||
assert.equal(drafts[2].defaultValue, "0");
|
||||
assert.equal(drafts[2].original?.column_default, "0");
|
||||
});
|
||||
|
||||
test("applies manticore column properties from ddl", () => {
|
||||
const manticoreColumns: ColumnInfo[] = [
|
||||
{ name: "name", data_type: "string", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: null },
|
||||
|
|
|
|||
Loading…
Reference in New Issue