fix(kingbase): save edited view definitions

This commit is contained in:
zipg 2026-07-20 15:41:01 +08:00 committed by GitHub
parent 2dad288907
commit b7db9b66db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 232 additions and 26 deletions

View File

@ -133,15 +133,7 @@ pub fn build_executable_object_source_statements(input: EditableObjectSourceSqlI
return Ok(vec![executable_duckdb_view_ddl(input.schema.as_deref(), &input.name, source)]);
}
if matches!(
input.database_type,
DatabaseType::Postgres
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
| DatabaseType::Questdb
) && input.object_type == ObjectSourceKind::View
{
if is_postgres_like(input.database_type) && input.object_type == ObjectSourceKind::View {
if let Some(sql) = executable_postgres_view_ddl(source) {
return Ok(vec![sql]);
}
@ -190,14 +182,8 @@ pub fn build_executable_object_source_sql(input: EditableObjectSourceSqlInput) -
/// statements.
pub fn build_editable_object_source(input: EditableObjectSourceSqlInput) -> String {
let source = input.source.clone();
if matches!(
input.database_type,
DatabaseType::Postgres
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
| DatabaseType::Questdb
) && input.object_type == ObjectSourceKind::View
if is_postgres_like(input.database_type)
&& input.object_type == ObjectSourceKind::View
&& source_starts_with_create_or_alter(&source)
{
// Some providers return full view DDL instead of a bare SELECT body.
@ -368,28 +354,50 @@ fn mysql_routine_script_delimiter(source: &str) -> &'static str {
}
fn source_starts_with_create_or_alter(source: &str) -> bool {
Regex::new(r"(?i)^\s*(?:CREATE|ALTER)\s+").unwrap().is_match(source)
let executable = &source[leading_sql_statement_start(source)..];
Regex::new(r"(?i)^(?:CREATE|ALTER)\s+").unwrap().is_match(executable)
}
fn source_starts_with_alter(source: &str) -> bool {
Regex::new(r"(?i)^\s*ALTER\s+").unwrap().is_match(source)
let executable = &source[leading_sql_statement_start(source)..];
Regex::new(r"(?i)^ALTER\s+").unwrap().is_match(executable)
}
fn executable_postgres_view_ddl(source: &str) -> Option<String> {
let trimmed = source.trim();
if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(trimmed) {
let statement_start = leading_sql_statement_start(trimmed);
let executable = &trimmed[statement_start..];
if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(executable) {
return Some(ensure_semicolon(trimmed));
}
let create_view = Regex::new(r"(?i)^CREATE\s+((?:(?:TEMP|TEMPORARY)\s+)?(?:RECURSIVE\s+)?VIEW\s+)").unwrap();
if create_view.is_match(trimmed) {
let replaced = create_view.replace(trimmed, "CREATE OR REPLACE $1");
return Some(ensure_semicolon(replaced.as_ref()));
// Kingbase extends PostgreSQL's prefix with FORCE after the optional RECURSIVE modifier.
let create_view =
Regex::new(r"(?i)^CREATE\s+((?:(?:TEMP|TEMPORARY)\s+)?(?:RECURSIVE\s+)?(?:FORCE\s+)?VIEW\s+)").unwrap();
if create_view.is_match(executable) {
let replaced = create_view.replace(executable, "CREATE OR REPLACE $1");
return Some(ensure_semicolon(&format!("{}{}", &trimmed[..statement_start], replaced)));
}
None
}
fn leading_sql_statement_start(source: &str) -> usize {
let mut index = 0;
loop {
index = skip_sql_whitespace(source, index);
if let Some(end) = sql_line_comment_end(source, index) {
index = end;
continue;
}
if let Some(end) = sql_block_comment_end(source, index) {
index = end;
continue;
}
return index;
}
}
fn executable_oracle_view_ddl(schema: Option<&str>, name: &str, source: &str) -> String {
let trimmed = source.trim();
if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(trimmed) || source_starts_with_alter(trimmed) {
@ -613,8 +621,27 @@ fn sql_block_comment_end(source: &str, start: usize) -> Option<usize> {
if !source[start..].starts_with("/*") {
return None;
}
let rest = &source[start + 2..];
Some(start + 2 + rest.find("*/").map(|index| index + 2).unwrap_or(rest.len()))
// PostgreSQL and Kingbase default to nested SQL block comments, so match the outer terminator.
let mut depth = 1;
let mut index = start + 2;
while index < source.len() {
if source[index..].starts_with("/*") {
depth += 1;
index += 2;
} else if source[index..].starts_with("*/") {
depth -= 1;
index += 2;
if depth == 0 {
return Some(index);
}
} else {
index += source[index..].chars().next().unwrap().len_utf8();
}
}
// Preserve the existing safe behavior: an unclosed leading comment consumes the remaining source.
Some(source.len())
}
fn read_quoted_sql_identifier(source: &str, start: usize) -> Option<(usize, String)> {
@ -1083,6 +1110,180 @@ mod tests {
assert_eq!(sql, format!("{source};"));
}
#[test]
fn kingbase_view_body_opens_as_create_or_replace_view() {
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("dbx_issue3895_repro".to_string()),
name: "edited_view".to_string(),
source: "SELECT id, label FROM source_rows WHERE id >= 1".to_string(),
});
assert_eq!(
sql,
"CREATE OR REPLACE VIEW \"dbx_issue3895_repro\".\"edited_view\" AS\nSELECT id, label FROM source_rows WHERE id >= 1;"
);
}
#[test]
fn kingbase_view_create_source_saves_as_create_or_replace_view() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("dbx_issue3895_repro".to_string()),
name: "edited_view".to_string(),
source: "CREATE VIEW dbx_issue3895_repro.edited_view AS SELECT id, label FROM source_rows".to_string(),
})
.unwrap();
assert_eq!(sql, "CREATE OR REPLACE VIEW dbx_issue3895_repro.edited_view AS SELECT id, label FROM source_rows;");
}
#[test]
fn kingbase_view_create_force_source_saves_as_create_or_replace_force_view() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "CREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
})
.unwrap();
assert_eq!(sql, "CREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;");
}
#[test]
fn kingbase_view_create_recursive_force_source_preserves_modifier_order() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "CREATE RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
})
.unwrap();
assert_eq!(sql, "CREATE OR REPLACE RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;");
}
#[test]
fn kingbase_view_create_temporary_recursive_force_source_preserves_legal_modifiers() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "CREATE TEMPORARY RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
})
.unwrap();
assert_eq!(
sql,
"CREATE OR REPLACE TEMPORARY RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;"
);
}
#[test]
fn kingbase_view_create_or_replace_force_source_saves_without_rewrapping() {
let source = "CREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows";
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: source.to_string(),
})
.unwrap();
assert_eq!(sql, format!("{source};"));
}
#[test]
fn kingbase_view_create_source_preserves_leading_line_comment() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "-- keep this note\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
})
.unwrap();
assert_eq!(sql, "-- keep this note\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;");
}
#[test]
fn kingbase_view_create_source_preserves_leading_block_comment() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "/* CREATE VIEW in this comment is ignored */\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows"
.to_string(),
})
.unwrap();
assert_eq!(
sql,
"/* CREATE VIEW in this comment is ignored */\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;"
);
}
#[test]
fn kingbase_view_create_source_preserves_leading_nested_block_comment() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: "/* outer /* nested */ comment */\nCREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows"
.to_string(),
})
.unwrap();
assert_eq!(
sql,
"/* outer /* nested */ comment */\nCREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;"
);
}
#[test]
fn kingbase_view_create_source_preserves_leading_multi_level_block_comment() {
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source:
"/* level 1 /* level 2 /* level 3 */ level 2 */ level 1 */\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows"
.to_string(),
})
.unwrap();
assert_eq!(
sql,
"/* level 1 /* level 2 /* level 3 */ level 2 */ level 1 */\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;"
);
}
#[test]
fn kingbase_view_unclosed_leading_block_comment_keeps_safe_body_fallback() {
let source = "/* unclosed comment\nCREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows";
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
database_type: DatabaseType::Kingbase,
object_type: ObjectSourceKind::View,
schema: Some("app".to_string()),
name: "edited_view".to_string(),
source: source.to_string(),
})
.unwrap();
assert_eq!(sql, format!("CREATE OR REPLACE VIEW \"app\".\"edited_view\" AS\n{source};"));
}
#[test]
fn postgres_view_alter_source_saves_without_body_wrapping() {
let source = "ALTER VIEW public.active_users SET (security_barrier = true)";

View File

@ -25,6 +25,7 @@ pub(crate) fn current_app_support_info() -> AppSupportInfo {
}
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn format_support_info_for_native_about() -> String {
let info = current_app_support_info();
let operating_system = format_operating_system(&info);
@ -32,6 +33,7 @@ pub(crate) fn format_support_info_for_native_about() -> String {
["Desktop".to_string(), unknown_if_empty(&operating_system), unknown_if_empty(&info.arch)].join("")
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn format_support_info_for_clipboard() -> String {
let info = current_app_support_info();
let operating_system = format_operating_system(&info);
@ -45,6 +47,7 @@ pub(crate) fn format_support_info_for_clipboard() -> String {
.join("\n")
}
#[cfg(any(target_os = "macos", test))]
fn format_operating_system(info: &AppSupportInfo) -> String {
let operating_system = [Some(info.os_name.as_str()), info.os_version.as_deref()]
.into_iter()
@ -55,6 +58,7 @@ fn format_operating_system(info: &AppSupportInfo) -> String {
operating_system
}
#[cfg(any(target_os = "macos", test))]
fn normalize_app_version(version: &str) -> String {
let trimmed = version.trim();
if trimmed.is_empty() {
@ -67,6 +71,7 @@ fn normalize_app_version(version: &str) -> String {
}
}
#[cfg(any(target_os = "macos", test))]
fn unknown_if_empty(value: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {