Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/db/locales/db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,10 @@ ObjectView:
en: "%{count} view(s)"
zh-CN: "%{count} 个视图"
zh-HK: "%{count} 個檢視"
materialized_views:
en: "%{count} materialized view(s)"
zh-CN: "%{count} 个物化视图"
zh-HK: "%{count} 個實體化檢視"
functions:
en: "%{count} function(s)"
zh-CN: "%{count} 个函数"
Expand Down
3 changes: 3 additions & 0 deletions crates/db/src/compare/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,9 @@ pub fn table_schema_from_metadata(
object_type: match table.object_type {
TableObjectType::Table => SchemaObjectType::Table,
TableObjectType::View => SchemaObjectType::View,
// 外部表不在结构比较范围内(调用方按 `is_ddl_comparable` 过滤),
// 万一传入也按表处理,避免凭空生成新对象类型。
TableObjectType::ForeignTable => SchemaObjectType::Table,
},
columns: columns
.into_iter()
Expand Down
113 changes: 107 additions & 6 deletions crates/db/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::runtime_contract::require_tokio_runtime;
use crate::sqlite::SqlitePlugin;
use crate::{
DbNode, DbNodeType, ExecOptions, SqlErrorInfo, SqlResult, SqlSource, TableDesign,
TableSaveResponse,
TableObjectType, TableSaveResponse,
};
use dashmap::DashMap;
use gpui::{AppContext, AsyncApp, Global, Task};
Expand Down Expand Up @@ -1431,12 +1431,40 @@ impl GlobalDbState {
database: String,
schema: Option<String>,
table_name: String,
) -> anyhow::Result<SqlResult> {
self.drop_table_like(
cx,
config_id,
database,
schema,
table_name,
TableObjectType::Table,
)
.await
}

/// 删除「表」目录下的对象。
///
/// 外部表必须使用 `DROP FOREIGN TABLE`,否则 PostgreSQL 会拒绝执行。
pub async fn drop_table_like(
&self,
cx: &mut AsyncApp,
config_id: String,
database: String,
schema: Option<String>,
table_name: String,
object_type: TableObjectType,
) -> anyhow::Result<SqlResult> {
let mut config = self
.get_config(&config_id)
.ok_or_else(|| anyhow::anyhow!("Connection not found: {}", config_id))?;
let plugin = self.get_plugin(&config.database_type)?;
let sql = plugin.drop_table(&database, schema.as_deref(), &table_name);
let sql = match object_type {
TableObjectType::ForeignTable => {
plugin.drop_foreign_table(&database, schema.as_deref(), &table_name)
}
_ => plugin.drop_table(&database, schema.as_deref(), &table_name),
};

// For non-Oracle databases, modify config.database to switch database
if config.database_type != DatabaseType::Oracle {
Expand Down Expand Up @@ -1497,12 +1525,64 @@ impl GlobalDbState {
database: String,
old_name: String,
new_name: String,
) -> anyhow::Result<SqlResult> {
self.rename_table_like(
cx,
config_id,
database,
None,
old_name,
new_name,
TableObjectType::Table,
)
.await
}

/// 重命名「表」目录下的对象(外部表需 `ALTER FOREIGN TABLE`)。
pub async fn rename_table_like(
&self,
cx: &mut AsyncApp,
config_id: String,
database: String,
schema: Option<String>,
old_name: String,
new_name: String,
object_type: TableObjectType,
) -> anyhow::Result<SqlResult> {
let mut config = self
.get_config(&config_id)
.ok_or_else(|| anyhow::anyhow!("Connection not found: {}", config_id))?;
let plugin = self.get_plugin(&config.database_type)?;
let sql = plugin.rename_table(&database, &old_name, &new_name);
let sql = match object_type {
TableObjectType::ForeignTable => {
plugin.rename_foreign_table(&database, schema.as_deref(), &old_name, &new_name)
}
_ => plugin.rename_table(&database, &old_name, &new_name),
};

if config.database_type != DatabaseType::Oracle {
config.database = Some(database);
}

let result = self.execute_with_session(cx, config, sql, None).await?;

Self::wrapper_operation_result(result)
}

/// 删除物化视图(`DROP MATERIALIZED VIEW`)。
pub async fn drop_materialized_view(
&self,
cx: &mut AsyncApp,
config_id: String,
database: String,
schema: Option<String>,
view_name: String,
) -> anyhow::Result<SqlResult> {
let mut config = self
.get_config(&config_id)
.ok_or_else(|| anyhow::anyhow!("Connection not found: {}", config_id))?;
let plugin = self.get_plugin(&config.database_type)?;
let sql = plugin.drop_materialized_view(&database, schema.as_deref(), &view_name);

if config.database_type != DatabaseType::Oracle {
config.database = Some(database);
Expand Down Expand Up @@ -2868,6 +2948,21 @@ impl GlobalDbState {
})
}

/// List materialized views(仅 PostgreSQL 等支持物化视图的数据库)
pub async fn list_materialized_views_view(
&self,
cx: &mut AsyncApp,
connection_id: String,
database: String,
schema: Option<String>,
) -> anyhow::Result<crate::types::ObjectView> {
with_plugin_session_db!(self, cx, connection_id, database.clone(), |plugin, conn| {
plugin
.list_materialized_views_view(&*conn, &database, schema)
.await
})
}

/// List functions view
pub async fn list_functions_view(
&self,
Expand Down Expand Up @@ -3124,11 +3219,17 @@ impl GlobalDbState {
.list_tables_view(&*conn, &database, schema)
.await
.ok(),
DbNodeType::Table | DbNodeType::ColumnsFolder => plugin
.list_columns_view(&*conn, &database, schema, &table)
DbNodeType::Table | DbNodeType::ForeignTable | DbNodeType::ColumnsFolder => {
plugin
.list_columns_view(&*conn, &database, schema, &table)
.await
.ok()
}
DbNodeType::ViewsFolder => plugin.list_views_view(&*conn, &database).await.ok(),
DbNodeType::MaterializedViewsFolder => plugin
.list_materialized_views_view(&*conn, &database, schema)
.await
.ok(),
DbNodeType::ViewsFolder => plugin.list_views_view(&*conn, &database).await.ok(),
DbNodeType::FunctionsFolder => plugin
.list_functions_view_in_schema(&*conn, &database, schema)
.await
Expand Down
1 change: 1 addition & 0 deletions crates/db/src/mysql/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ fn build_mysql_ui_manifest() -> DatabaseUiManifest {
supports_schema: false,
uses_schema_as_database: false,
supports_views: true,
supports_materialized_views: false,
supports_indexes: true,
supports_users: true,
supports_user_create: true,
Expand Down
148 changes: 146 additions & 2 deletions crates/db/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,58 @@ impl SqlCompletionInfo {
}
}

fn table_like_node_type(object_type: TableObjectType) -> DbNodeType {
match object_type {
TableObjectType::ForeignTable => DbNodeType::ForeignTable,
_ => DbNodeType::Table,
}
}

/// 构建「物化视图」目录节点及其子节点。
///
/// `folder_id` 同时作为子节点 id 前缀,与其他目录(Views/Functions)保持一致。
fn build_materialized_views_folder(
node: &DbNode,
base_metadata: &HashMap<String, String>,
views: Vec<ViewInfo>,
folder_id: String,
) -> DbNode {
let mut folder = DbNode::new(
folder_id.clone(),
"DbTree.MaterializedViews".to_string(),
DbNodeType::MaterializedViewsFolder,
node.connection_id.clone(),
node.database_type.clone(),
)
.with_parent_context(node.id.clone())
.with_metadata(base_metadata.clone());

if !views.is_empty() {
let mut children: Vec<DbNode> = views
.into_iter()
.map(|view| {
let mut metadata = base_metadata.clone();
if let Some(comment) = view.comment.filter(|comment| !comment.is_empty()) {
metadata.insert("comment".to_string(), comment);
}
DbNode::new(
format!("{}:{}", folder_id, view.name),
view.name.clone(),
DbNodeType::MaterializedView,
node.connection_id.clone(),
node.database_type.clone(),
)
.with_parent_context(folder_id.clone())
.with_metadata(metadata)
})
.collect();
children.sort();
folder.set_children(children);
}

folder
}

fn routine_node(
routine: FunctionInfo,
node_type: DbNodeType,
Expand Down Expand Up @@ -685,6 +737,30 @@ pub trait DatabasePlugin: Send + Sync {
database: &str,
) -> Result<ObjectView>;

// === Materialized View Operations ===
/// 列举物化视图。
///
/// 默认返回空集:只有支持物化视图的数据库(如 PostgreSQL)才需要重写,
/// 并需同时打开 `DatabaseUiCapabilities::supports_materialized_views`。
async fn list_materialized_views(
&self,
_connection: &dyn DbConnection,
_database: &str,
_schema: Option<String>,
) -> Result<Vec<ViewInfo>> {
Ok(Vec::new())
}

/// 物化视图对象列表视图(表格形式)。
async fn list_materialized_views_view(
&self,
_connection: &dyn DbConnection,
_database: &str,
_schema: Option<String>,
) -> Result<ObjectView> {
Ok(ObjectView::default())
}

// === Function Operations ===

async fn list_functions(
Expand Down Expand Up @@ -1104,7 +1180,7 @@ pub trait DatabasePlugin: Send + Sync {
DbNode::new(
format!("{}:table_folder:{}", id, table_info.name),
table_info.name.clone(),
DbNodeType::Table,
table_like_node_type(table_info.object_type),
node.connection_id.clone(),
node.database_type.clone(),
)
Expand Down Expand Up @@ -1162,6 +1238,20 @@ pub trait DatabasePlugin: Send + Sync {
nodes.push(views_folder);
}

// Materialized views folder(仅支持物化视图的数据库,如 PostgreSQL)
if capabilities.supports_materialized_views {
let matviews = self
.list_materialized_views(connection, database, schema.clone())
.await
.unwrap_or_default();
nodes.push(build_materialized_views_folder(
node,
&metadata,
matviews,
format!("{}:matviews_folder", id),
));
}

// Functions folder
if capabilities.supports_functions {
let functions = self
Expand Down Expand Up @@ -1341,6 +1431,7 @@ pub trait DatabasePlugin: Send + Sync {
}
DbNodeType::TablesFolder
| DbNodeType::ViewsFolder
| DbNodeType::MaterializedViewsFolder
| DbNodeType::FunctionsFolder
| DbNodeType::ProceduresFolder
| DbNodeType::SequencesFolder => {
Expand Down Expand Up @@ -1393,7 +1484,35 @@ pub trait DatabasePlugin: Send + Sync {
DbNode::new(
format!("{}:{}", id, t.name),
t.name.clone(),
DbNodeType::Table,
table_like_node_type(t.object_type),
node.connection_id.clone(),
node.database_type.clone(),
)
.with_parent_context(id)
.with_metadata(meta)
})
.collect();
children.sort();
Ok(children)
}
DbNodeType::MaterializedViewsFolder => {
if !self.capabilities().supports_materialized_views {
return Ok(Vec::new());
}
let views = self
.list_materialized_views(connection, database, schema)
.await?;
let mut children: Vec<DbNode> = views
.into_iter()
.map(|v| {
let mut meta = node.metadata.clone();
if let Some(comment) = v.comment.filter(|comment| !comment.is_empty()) {
meta.insert("comment".to_string(), comment);
}
DbNode::new(
format!("{}:{}", id, v.name),
v.name.clone(),
DbNodeType::MaterializedView,
node.connection_id.clone(),
node.database_type.clone(),
)
Expand Down Expand Up @@ -2826,6 +2945,31 @@ pub trait DatabasePlugin: Send + Sync {
/// Rename table
fn rename_table(&self, database: &str, old_name: &str, new_name: &str) -> String;

/// 删除外部表(`DROP FOREIGN TABLE`)。
///
/// 默认退化为 `drop_table`:没有外部表概念的数据库不会走到这个分支。
fn drop_foreign_table(&self, database: &str, schema: Option<&str>, table: &str) -> String {
self.drop_table(database, schema, table)
}

/// 重命名外部表(`ALTER FOREIGN TABLE ... RENAME TO ...`)。
///
/// `schema` 非空时限定旧表名,避免依赖会话 `search_path` 而改错 schema 下的同名表。
fn rename_foreign_table(
&self,
database: &str,
_schema: Option<&str>,
old_name: &str,
new_name: &str,
) -> String {
self.rename_table(database, old_name, new_name)
}

/// 删除物化视图(`DROP MATERIALIZED VIEW`)。
fn drop_materialized_view(&self, database: &str, _schema: Option<&str>, view: &str) -> String {
self.drop_view(database, view)
}

/// Build native backup-table SQL.
/// 默认实现使用 `CREATE TABLE ... AS SELECT ...`,数据库插件可按方言覆盖。
fn build_backup_table_sql(
Expand Down
Loading
Loading