diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 0bd351c212..2a87bd6bcc 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -102,7 +102,7 @@ import { type TableAdminSqlOptions, } from "@/lib/dbAdminSql"; import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/objectRenameSql"; -import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor"; +import { buildEditableObjectSource, buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor"; import { buildViewDdl } from "@/lib/viewDdl"; import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sqlFormatter"; import DdlViewDialog from "@/components/objects/DdlViewDialog.vue"; @@ -1751,9 +1751,17 @@ function viewObjectSource() { return api.getObjectSource(node.connectionId!, node.database!, schema, node.label, objectType as any); }) .then(async (result) => { + const databaseType = currentDatabaseType(); + if (!databaseType) throw new Error("Connection type is unavailable."); const tabId = queryStore.createTab(node.connectionId!, node.database!, `Source - ${node.label}`); - const formatted = await formatSqlForDisplay(result.source, sqlFormatDialectForDbType(currentDatabaseType()), settingsStore.editorSettings.sqlFormatter); - queryStore.updateSql(tabId, formatted); + const editable = await buildEditableObjectSource({ + databaseType, + objectType, + schema, + name: node.label, + source: result.source, + }); + queryStore.updateSql(tabId, editable); if (objectType !== "SEQUENCE") { queryStore.setObjectSource(tabId, { schema, diff --git a/apps/desktop/src/lib/objectSourceEditor.ts b/apps/desktop/src/lib/objectSourceEditor.ts index c9c1a63556..75bc84a235 100644 --- a/apps/desktop/src/lib/objectSourceEditor.ts +++ b/apps/desktop/src/lib/objectSourceEditor.ts @@ -37,6 +37,10 @@ export async function buildExecutableObjectSourceSql(input: BuildEditableObjectS return api.buildExecutableObjectSourceSql(input); } +export function buildEditableObjectSource(input: BuildEditableObjectSourceSqlInput): Promise { + return api.buildEditableObjectSource(input); +} + export function objectSourceSaveExecutionMode(_databaseType: DatabaseType): ObjectSourceSaveExecutionMode { return "single"; } diff --git a/crates/dbx-core/src/object_source_sql.rs b/crates/dbx-core/src/object_source_sql.rs index e1c8550cf9..17aa2d5fec 100644 --- a/crates/dbx-core/src/object_source_sql.rs +++ b/crates/dbx-core/src/object_source_sql.rs @@ -137,6 +137,13 @@ pub fn build_executable_object_source_statements(input: EditableObjectSourceSqlI | DatabaseType::Questdb ) && input.object_type == ObjectSourceKind::View { + if let Some(sql) = executable_postgres_view_ddl(source) { + return Ok(vec![sql]); + } + if source_starts_with_alter(source) { + // ALTER VIEW is already executable DDL, but it is not a view body. + return Ok(vec![ensure_semicolon(source)]); + } return Ok(vec![format!( "CREATE OR REPLACE VIEW {} AS\n{}", postgres_qualified_name(input.schema.as_deref(), &input.name), @@ -164,6 +171,19 @@ 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 + && source_starts_with_create_or_alter(&source) + { + // Some providers return full view DDL instead of a bare SELECT body. + return ensure_semicolon(source.trim()); + } match build_executable_object_source_statements(input) { Ok(statements) => statements.into_iter().next().unwrap_or_default(), Err(_) => ensure_semicolon(source.trim()), @@ -285,6 +305,29 @@ fn ensure_semicolon(sql: &str) -> String { } } +fn source_starts_with_create_or_alter(source: &str) -> bool { + Regex::new(r"(?i)^\s*(?:CREATE|ALTER)\s+").unwrap().is_match(source) +} + +fn source_starts_with_alter(source: &str) -> bool { + Regex::new(r"(?i)^\s*ALTER\s+").unwrap().is_match(source) +} + +fn executable_postgres_view_ddl(source: &str) -> Option { + let trimmed = source.trim(); + if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(trimmed) { + 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())); + } + + None +} + fn postgres_qualified_name(schema: Option<&str>, name: &str) -> String { schema .into_iter() @@ -547,6 +590,67 @@ mod tests { ); } + #[test] + fn postgres_view_create_source_opens_without_rewrapping_or_reformatting() { + let source = "CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true"; + let sql = build_editable_object_source(EditableObjectSourceSqlInput { + database_type: DatabaseType::Postgres, + object_type: ObjectSourceKind::View, + schema: Some("public".to_string()), + name: "active_users".to_string(), + source: source.to_string(), + }); + + assert_eq!(sql, format!("{source};")); + } + + #[test] + fn postgres_view_create_source_saves_as_create_or_replace_view() { + let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput { + database_type: DatabaseType::Postgres, + object_type: ObjectSourceKind::View, + schema: Some("public".to_string()), + name: "active_users".to_string(), + source: "CREATE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true".to_string(), + }) + .unwrap(); + + assert_eq!( + sql, + "CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true;" + ); + } + + #[test] + fn postgres_view_create_or_replace_source_saves_without_rewrapping() { + let source = "CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true"; + let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput { + database_type: DatabaseType::Postgres, + object_type: ObjectSourceKind::View, + schema: Some("public".to_string()), + name: "active_users".to_string(), + source: source.to_string(), + }) + .unwrap(); + + assert_eq!(sql, format!("{source};")); + } + + #[test] + fn postgres_view_alter_source_saves_without_body_wrapping() { + let source = "ALTER VIEW public.active_users SET (security_barrier = true)"; + let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput { + database_type: DatabaseType::Postgres, + object_type: ObjectSourceKind::View, + schema: Some("public".to_string()), + name: "active_users".to_string(), + source: source.to_string(), + }) + .unwrap(); + + assert_eq!(sql, format!("{source};")); + } + #[test] fn postgres_compatible_view_body_opens_as_create_or_replace_view() { for database_type in [DatabaseType::OpenGauss, DatabaseType::Kwdb] {