feat: add Tauri v2 Rust backend with PostgreSQL support
Add Rust backend: AppState with connection pool management, TuskError handling, ConnectionConfig model, and all commands for connections, schema browsing, query execution, data CRUD, and CSV/JSON export via sqlx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
342
src-tauri/src/commands/schema.rs
Normal file
342
src-tauri/src/commands/schema.rs
Normal file
@@ -0,0 +1,342 @@
|
||||
use crate::error::{TuskError, TuskResult};
|
||||
use crate::models::schema::{ColumnInfo, ConstraintInfo, IndexInfo, SchemaObject};
|
||||
use crate::state::AppState;
|
||||
use sqlx::Row;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_databases(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> TuskResult<Vec<String>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT datname FROM pg_database \
|
||||
WHERE datistemplate = false \
|
||||
ORDER BY datname",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_schemas(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> TuskResult<Vec<String>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT schema_name FROM information_schema.schemata \
|
||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
|
||||
ORDER BY schema_name",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows.iter().map(|r| r.get::<String, _>(0)).collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_tables(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
) -> TuskResult<Vec<SchemaObject>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT table_name FROM information_schema.tables \
|
||||
WHERE table_schema = $1 AND table_type = 'BASE TABLE' \
|
||||
ORDER BY table_name",
|
||||
)
|
||||
.bind(&schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| SchemaObject {
|
||||
name: r.get(0),
|
||||
object_type: "table".to_string(),
|
||||
schema: schema.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_views(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
) -> TuskResult<Vec<SchemaObject>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT table_name FROM information_schema.views \
|
||||
WHERE table_schema = $1 \
|
||||
ORDER BY table_name",
|
||||
)
|
||||
.bind(&schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| SchemaObject {
|
||||
name: r.get(0),
|
||||
object_type: "view".to_string(),
|
||||
schema: schema.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_functions(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
) -> TuskResult<Vec<SchemaObject>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT routine_name FROM information_schema.routines \
|
||||
WHERE routine_schema = $1 AND routine_type = 'FUNCTION' \
|
||||
ORDER BY routine_name",
|
||||
)
|
||||
.bind(&schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| SchemaObject {
|
||||
name: r.get(0),
|
||||
object_type: "function".to_string(),
|
||||
schema: schema.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_indexes(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
) -> TuskResult<Vec<SchemaObject>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT indexname FROM pg_indexes \
|
||||
WHERE schemaname = $1 \
|
||||
ORDER BY indexname",
|
||||
)
|
||||
.bind(&schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| SchemaObject {
|
||||
name: r.get(0),
|
||||
object_type: "index".to_string(),
|
||||
schema: schema.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_sequences(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
) -> TuskResult<Vec<SchemaObject>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT sequence_name FROM information_schema.sequences \
|
||||
WHERE sequence_schema = $1 \
|
||||
ORDER BY sequence_name",
|
||||
)
|
||||
.bind(&schema)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| SchemaObject {
|
||||
name: r.get(0),
|
||||
object_type: "sequence".to_string(),
|
||||
schema: schema.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_table_columns(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> TuskResult<Vec<ColumnInfo>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT \
|
||||
c.column_name, \
|
||||
c.data_type, \
|
||||
c.is_nullable, \
|
||||
c.column_default, \
|
||||
c.ordinal_position::int, \
|
||||
c.character_maximum_length::int, \
|
||||
COALESCE(( \
|
||||
SELECT true FROM information_schema.table_constraints tc \
|
||||
JOIN information_schema.key_column_usage kcu \
|
||||
ON tc.constraint_name = kcu.constraint_name \
|
||||
AND tc.table_schema = kcu.table_schema \
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY' \
|
||||
AND tc.table_schema = $1 \
|
||||
AND tc.table_name = $2 \
|
||||
AND kcu.column_name = c.column_name \
|
||||
LIMIT 1 \
|
||||
), false) as is_pk \
|
||||
FROM information_schema.columns c \
|
||||
WHERE c.table_schema = $1 AND c.table_name = $2 \
|
||||
ORDER BY c.ordinal_position",
|
||||
)
|
||||
.bind(&schema)
|
||||
.bind(&table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| ColumnInfo {
|
||||
name: r.get::<String, _>(0),
|
||||
data_type: r.get::<String, _>(1),
|
||||
is_nullable: r.get::<String, _>(2) == "YES",
|
||||
column_default: r.get::<Option<String>, _>(3),
|
||||
ordinal_position: r.get::<i32, _>(4),
|
||||
character_maximum_length: r.get::<Option<i32>, _>(5),
|
||||
is_primary_key: r.get::<bool, _>(6),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_table_constraints(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> TuskResult<Vec<ConstraintInfo>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT \
|
||||
tc.constraint_name, \
|
||||
tc.constraint_type, \
|
||||
array_agg(kcu.column_name ORDER BY kcu.ordinal_position)::text[] as columns \
|
||||
FROM information_schema.table_constraints tc \
|
||||
JOIN information_schema.key_column_usage kcu \
|
||||
ON tc.constraint_name = kcu.constraint_name \
|
||||
AND tc.table_schema = kcu.table_schema \
|
||||
WHERE tc.table_schema = $1 AND tc.table_name = $2 \
|
||||
GROUP BY tc.constraint_name, tc.constraint_type \
|
||||
ORDER BY tc.constraint_type, tc.constraint_name",
|
||||
)
|
||||
.bind(&schema)
|
||||
.bind(&table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| ConstraintInfo {
|
||||
name: r.get::<String, _>(0),
|
||||
constraint_type: r.get::<String, _>(1),
|
||||
columns: r.get::<Vec<String>, _>(2),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_table_indexes(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
) -> TuskResult<Vec<IndexInfo>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT \
|
||||
i.relname as index_name, \
|
||||
pg_get_indexdef(i.oid) as index_def, \
|
||||
ix.indisunique as is_unique, \
|
||||
ix.indisprimary as is_primary \
|
||||
FROM pg_index ix \
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid \
|
||||
JOIN pg_class t ON t.oid = ix.indrelid \
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace \
|
||||
WHERE n.nspname = $1 AND t.relname = $2 \
|
||||
ORDER BY i.relname",
|
||||
)
|
||||
.bind(&schema)
|
||||
.bind(&table)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| IndexInfo {
|
||||
name: r.get::<String, _>(0),
|
||||
definition: r.get::<String, _>(1),
|
||||
is_unique: r.get::<bool, _>(2),
|
||||
is_primary: r.get::<bool, _>(3),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
Reference in New Issue
Block a user