Skip to content
Open
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
140 changes: 140 additions & 0 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3703,6 +3703,146 @@ pub async fn get_view_columns<R: Runtime>(
result
}

#[tauri::command]
pub async fn get_materialized_views<R: Runtime>(
app: AppHandle<R>,
connection_id: String,
schema: Option<String>,
) -> Result<Vec<crate::models::ViewInfo>, String> {
log::info!("Fetching materialized views for connection: {}", connection_id);

let saved_conn = find_connection_by_id(&app, &connection_id)?;
let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?;
let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?;
let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?;

let drv = driver_for(&saved_conn.params.driver).await?;
let result = drv.get_materialized_views(&params, schema.as_deref()).await;

match &result {
Ok(views) => log::info!(
"Retrieved {} materialized views from {}",
views.len(),
params.database
),
Err(e) => log::error!(
"Failed to get materialized views from {}: {}",
params.database,
e
),
}

result
}

#[tauri::command]
pub async fn get_materialized_view_columns<R: Runtime>(
app: AppHandle<R>,
connection_id: String,
view_name: String,
schema: Option<String>,
) -> Result<Vec<TableColumn>, String> {
log::info!(
"Fetching materialized view columns for: {} on connection: {}",
view_name,
connection_id
);

let saved_conn = find_connection_by_id(&app, &connection_id)?;
let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?;
let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?;
let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?;

let drv = driver_for(&saved_conn.params.driver).await?;
let result = drv
.get_materialized_view_columns(&params, &view_name, schema.as_deref())
.await;

match &result {
Ok(columns) => log::info!(
"Retrieved {} columns for materialized view {}",
columns.len(),
view_name
),
Err(e) => log::error!(
"Failed to get materialized view columns for {}: {}",
view_name,
e
),
}

result
}

#[tauri::command]
pub async fn get_materialized_view_definition<R: Runtime>(
app: AppHandle<R>,
connection_id: String,
view_name: String,
schema: Option<String>,
) -> Result<String, String> {
log::info!(
"Fetching materialized view definition for: {} on connection: {}",
view_name,
connection_id
);

let saved_conn = find_connection_by_id(&app, &connection_id)?;
let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?;
let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?;
let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?;

let drv = driver_for(&saved_conn.params.driver).await?;
let result = drv
.get_materialized_view_definition(&params, &view_name, schema.as_deref())
.await;

match &result {
Ok(_) => log::info!(
"Successfully retrieved materialized view definition for {}",
view_name
),
Err(e) => log::error!(
"Failed to get materialized view definition for {}: {}",
view_name,
e
),
}

result
}

#[tauri::command]
pub async fn refresh_materialized_view<R: Runtime>(
app: AppHandle<R>,
connection_id: String,
view_name: String,
schema: Option<String>,
) -> Result<(), String> {
log::info!(
"Refreshing materialized view: {} on connection: {}",
view_name,
connection_id
);

let saved_conn = find_connection_by_id(&app, &connection_id)?;
let expanded_params = expand_ssh_connection_params(&app, &saved_conn.params).await?;
let expanded_params = expand_k8s_connection_params(&app, &expanded_params).await?;
let params = resolve_connection_params_with_id(&expanded_params, &connection_id)?;

let drv = driver_for(&saved_conn.params.driver).await?;
let result = drv
.refresh_materialized_view(&params, &view_name, schema.as_deref())
.await;

match &result {
Ok(_) => log::info!("Successfully refreshed materialized view: {}", view_name),
Err(e) => log::error!("Failed to refresh materialized view {}: {}", view_name, e),
}

result
}

#[tauri::command]
pub async fn get_triggers<R: Runtime>(
app: AppHandle<R>,
Expand Down
44 changes: 44 additions & 0 deletions src-tauri/src/drivers/driver_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ pub struct DriverCapabilities {
pub schemas: bool,
/// Supports views.
pub views: bool,
/// Supports materialized views (e.g. PostgreSQL). Gates the
/// "Materialized Views" tree group in the UI. Defaults to `false`.
#[serde(default)]
pub materialized_views: bool,
/// Supports stored procedures and functions.
pub routines: bool,
/// File-based database (e.g. SQLite); no host/port required.
Expand Down Expand Up @@ -336,6 +340,46 @@ pub trait DatabaseDriver: Send + Sync {
schema: Option<&str>,
) -> Result<(), String>;

// --- Materialized views -------------------------------------------------
// Default impls return empty / unsupported so drivers without materialized
// views (MySQL, SQLite, plugins) need no changes; the UI hides the group
// unless `DriverCapabilities::materialized_views` is set.

async fn get_materialized_views(
&self,
_params: &ConnectionParams,
_schema: Option<&str>,
) -> Result<Vec<ViewInfo>, String> {
Ok(Vec::new())
}

async fn get_materialized_view_columns(
&self,
_params: &ConnectionParams,
_view_name: &str,
_schema: Option<&str>,
) -> Result<Vec<TableColumn>, String> {
Ok(Vec::new())
}

async fn get_materialized_view_definition(
&self,
_params: &ConnectionParams,
_view_name: &str,
_schema: Option<&str>,
) -> Result<String, String> {
Err("Materialized views are not supported by this driver".to_string())
}

async fn refresh_materialized_view(
&self,
_params: &ConnectionParams,
_view_name: &str,
_schema: Option<&str>,
) -> Result<(), String> {
Err("Materialized views are not supported by this driver".to_string())
}

// --- Routines -----------------------------------------------------------

async fn get_routines(
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/drivers/mysql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,7 @@ impl MysqlDriver {
capabilities: DriverCapabilities {
schemas: false,
views: true,
materialized_views: false,
routines: true,
file_based: false,
folder_based: false,
Expand Down
152 changes: 151 additions & 1 deletion src-tauri/src/drivers/postgres/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ pub async fn get_indexes(
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
WHERE
t.relkind = 'r'
t.relkind IN ('r', 'm')
AND n.nspname = $1
AND t.relname = $2
ORDER BY
Expand Down Expand Up @@ -1113,6 +1113,120 @@ pub async fn get_view_columns(
.collect())
}

pub async fn get_materialized_views(
params: &ConnectionParams,
schema: &str,
) -> Result<Vec<ViewInfo>, String> {
log::debug!(
"PostgreSQL: Fetching materialized views for database: {} schema: {}",
params.database,
schema
);
let pool = get_postgres_pool(params).await?;
let rows = query_all(
&pool,
"SELECT matviewname as name FROM pg_matviews WHERE schemaname = $1 ORDER BY matviewname ASC",
&[&schema],
)
.await?;

let views: Vec<ViewInfo> = rows
.iter()
.map(|r| ViewInfo {
name: r.try_get("name").unwrap_or_default(),
definition: None,
})
.collect();
Ok(views)
}

/// Materialized views are not exposed via `information_schema.columns`, so their
/// columns must be read from the system catalog (`pg_attribute`/`pg_class`).
pub async fn get_materialized_view_columns(
params: &ConnectionParams,
view_name: &str,
schema: &str,
) -> Result<Vec<TableColumn>, String> {
let pool = get_postgres_pool(params).await?;
let query = r#"
SELECT
a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS data_type,
a.attnotnull AS not_null
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind = 'm'
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
"#;

let rows = query_all(&pool, &query, &[&schema, &view_name]).await?;

Ok(rows
.iter()
.map(|r| TableColumn {
name: r.try_get("column_name").unwrap_or_default(),
data_type: r.try_get("data_type").unwrap_or_default(),
is_pk: false,
is_nullable: !r.try_get::<_, bool>("not_null").unwrap_or(false),
is_auto_increment: false,
default_value: None,
character_maximum_length: None,
})
.collect())
}

pub async fn get_materialized_view_definition(
params: &ConnectionParams,
view_name: &str,
schema: &str,
) -> Result<String, String> {
let pool = get_postgres_pool(params).await?;
let qualified = format!(
"\"{}\".\"{}\"",
escape_identifier(schema),
escape_identifier(view_name)
);

let client = pool.get().await.map_err(|e| e.to_string())?;

let row = client
.query_one(
"SELECT pg_get_viewdef($1::regclass, true) as definition",
&[&qualified],
)
.await
.map_err(|e| format!("Failed to get materialized view definition: {}", e))?;

let definition: String = row.try_get("definition").unwrap_or_default();
Ok(format!(
"CREATE MATERIALIZED VIEW {} AS\n{}",
qualified, definition
))
}

pub async fn refresh_materialized_view(
params: &ConnectionParams,
view_name: &str,
schema: &str,
) -> Result<(), String> {
let pool = get_postgres_pool(params).await?;
let query = format!(
"REFRESH MATERIALIZED VIEW \"{}\".\"{}\"",
escape_identifier(schema),
escape_identifier(view_name)
);

let client = pool.get().await.map_err(|e| e.to_string())?;
client
.execute(&query, &[])
.await
.map_err(|e| format!("Failed to refresh materialized view: {}", e))?;

Ok(())
}

pub async fn get_routines(
params: &ConnectionParams,
schema: &str,
Expand Down Expand Up @@ -1353,6 +1467,7 @@ impl PostgresDriver {
capabilities: DriverCapabilities {
schemas: true,
views: true,
materialized_views: true,
routines: true,
file_based: false,
folder_based: false,
Expand Down Expand Up @@ -1545,6 +1660,41 @@ impl DatabaseDriver for PostgresDriver {
drop_view(params, view_name, self.resolve_schema(schema)).await
}

async fn get_materialized_views(
&self,
params: &crate::models::ConnectionParams,
schema: Option<&str>,
) -> Result<Vec<crate::models::ViewInfo>, String> {
get_materialized_views(params, self.resolve_schema(schema)).await
}

async fn get_materialized_view_columns(
&self,
params: &crate::models::ConnectionParams,
view_name: &str,
schema: Option<&str>,
) -> Result<Vec<crate::models::TableColumn>, String> {
get_materialized_view_columns(params, view_name, self.resolve_schema(schema)).await
}

async fn get_materialized_view_definition(
&self,
params: &crate::models::ConnectionParams,
view_name: &str,
schema: Option<&str>,
) -> Result<String, String> {
get_materialized_view_definition(params, view_name, self.resolve_schema(schema)).await
}

async fn refresh_materialized_view(
&self,
params: &crate::models::ConnectionParams,
view_name: &str,
schema: Option<&str>,
) -> Result<(), String> {
refresh_materialized_view(params, view_name, self.resolve_schema(schema)).await
}

async fn get_routines(
&self,
params: &crate::models::ConnectionParams,
Expand Down
Loading