feat(postgres): support editing table owners
This commit is contained in:
32 files changed
+556
-36
No files matched your search
@@ -7190,6 +7190,14 @@ pub async fn list_owners(pool: &Pool, schema: &str) -> Result<Vec<OwnerInfo>, St
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_table_owner(pool: &Pool, schema: &str, table: &str) -> Result<Option<String>, String> {
|
||||
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
|
||||
let params: [&(dyn tokio_postgres::types::ToSql + Sync); 2] = [&schema, &table];
|
||||
let rows = postgres_query_cached(&client, POSTGRES_TABLE_OWNER_SQL, ¶ms).await.map_err(pg_error_to_string)?;
|
||||
|
||||
Ok(rows.first().map(|row| pg_row_try_string(row, 0)).filter(|owner| !owner.is_empty()))
|
||||
}
|
||||
|
||||
pub async fn get_table_access(pool: &Pool, schema: &str, table: &str) -> Result<PostgresTableAccessInfo, String> {
|
||||
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
|
||||
let params: [&(dyn tokio_postgres::types::ToSql + Sync); 2] = [&schema, &table];
|
||||
|
||||
@@ -7049,6 +7049,25 @@ pub async fn list_owners_core(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_table_owner_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
retry_metadata_connection(state, connection_id, Some(database), || async {
|
||||
let pool_key = state.get_or_create_metadata_pool_for_session(connection_id, Some(database), None).await?;
|
||||
let pool = clone_metadata_pool(state, &pool_key).await.ok_or("Pool not found")?;
|
||||
|
||||
match &pool {
|
||||
PoolKind::Postgres(p) => db::postgres::get_table_owner(p, schema, table).await,
|
||||
_ => Ok(None),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Whether to widen or normalize a single-table DDL fetch for its caller.
|
||||
///
|
||||
/// Database export and table transfer render one relation at a time because
|
||||
|
||||
@@ -6,6 +6,7 @@ mod create_table;
|
||||
mod dialect;
|
||||
mod foreign_keys;
|
||||
mod indexes;
|
||||
mod owner;
|
||||
mod sqlite_rebuild;
|
||||
mod triggers;
|
||||
mod types;
|
||||
@@ -17,6 +18,7 @@ mod tests;
|
||||
|
||||
pub use column_alter::build_single_column_alter_sql;
|
||||
pub use create_table::build_create_table_sql;
|
||||
pub use owner::build_table_owner_change_sql;
|
||||
pub use sqlite_rebuild::{apply_sqlite_table_structure_change, preview_sqlite_table_structure_change};
|
||||
pub use types::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use super::dialect::StructureDialect;
|
||||
use super::types::{TableOwnerChangeSqlOptions, TableStructureSqlResult};
|
||||
use super::util::{qualified_table, quote_ident};
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
pub fn build_table_owner_change_sql(options: TableOwnerChangeSqlOptions) -> TableStructureSqlResult {
|
||||
let owner = &options.owner;
|
||||
if owner == &options.original_owner {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings: Vec::new() };
|
||||
}
|
||||
if owner.is_empty() {
|
||||
return TableStructureSqlResult {
|
||||
statements: Vec::new(),
|
||||
warnings: vec!["Table owner cannot be empty.".to_string()],
|
||||
};
|
||||
}
|
||||
if options.database_type != Some(DatabaseType::Postgres) {
|
||||
return TableStructureSqlResult {
|
||||
statements: Vec::new(),
|
||||
warnings: vec!["Changing the table owner is currently supported only for PostgreSQL.".to_string()],
|
||||
};
|
||||
}
|
||||
|
||||
let table = qualified_table(StructureDialect::Postgres, options.schema.as_deref(), &options.table_name);
|
||||
let owner = quote_ident(StructureDialect::Postgres, owner);
|
||||
TableStructureSqlResult { statements: vec![format!("ALTER TABLE {table} OWNER TO {owner};")], warnings: Vec::new() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn options(owner: &str, original_owner: &str) -> TableOwnerChangeSqlOptions {
|
||||
TableOwnerChangeSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("app schema".to_string()),
|
||||
table_name: "order\"items".to_string(),
|
||||
owner: owner.to_string(),
|
||||
original_owner: original_owner.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_quoted_postgres_owner_change() {
|
||||
let result = build_table_owner_change_sql(options("reporting\"role", "app_user"));
|
||||
assert!(result.warnings.is_empty());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE \"app schema\".\"order\"\"items\" OWNER TO \"reporting\"\"role\";".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_whitespace_in_postgres_role_names() {
|
||||
let changed = build_table_owner_change_sql(options(" app_user ", "app_user"));
|
||||
assert!(changed.warnings.is_empty());
|
||||
assert_eq!(
|
||||
changed.statements,
|
||||
vec!["ALTER TABLE \"app schema\".\"order\"\"items\" OWNER TO \" app_user \";".to_string()]
|
||||
);
|
||||
|
||||
let unchanged = build_table_owner_change_sql(options(" app_user ", " app_user "));
|
||||
assert!(unchanged.statements.is_empty());
|
||||
assert!(unchanged.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_or_unsupported_owner_changes() {
|
||||
let empty = build_table_owner_change_sql(options("", "app_user"));
|
||||
assert!(empty.statements.is_empty());
|
||||
assert_eq!(empty.warnings, vec!["Table owner cannot be empty."]);
|
||||
|
||||
let mut unsupported = options("reporting", "app_user");
|
||||
unsupported.database_type = Some(DatabaseType::Mysql);
|
||||
let unsupported = build_table_owner_change_sql(unsupported);
|
||||
assert!(unsupported.statements.is_empty());
|
||||
assert_eq!(unsupported.warnings, vec!["Changing the table owner is currently supported only for PostgreSQL."]);
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,20 @@ pub struct TableStructureSqlResult {
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TableOwnerChangeSqlOptions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub database_type: Option<DatabaseType>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub schema: Option<String>,
|
||||
pub table_name: String,
|
||||
#[serde(default)]
|
||||
pub owner: String,
|
||||
#[serde(default)]
|
||||
pub original_owner: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SqliteTableStructurePreview {
|
||||
|
||||
@@ -435,6 +435,7 @@ async fn main() {
|
||||
.route("/schema/sequences", get(routes::schema::list_sequences))
|
||||
.route("/schema/rules", get(routes::schema::list_rules))
|
||||
.route("/schema/owners", get(routes::schema::list_owners))
|
||||
.route("/schema/table-owner", get(routes::schema::get_table_owner))
|
||||
.route("/schema/extensions", get(routes::schema::list_extensions))
|
||||
.route("/schema/available-extensions", get(routes::schema::list_available_extensions))
|
||||
.route("/schema/ddl", get(routes::schema::get_ddl))
|
||||
@@ -509,6 +510,7 @@ async fn main() {
|
||||
)
|
||||
.route("/query/build-view-ddl-sql", post(routes::query::build_view_ddl_sql))
|
||||
.route("/query/build-table-structure-change-sql", post(routes::query::build_table_structure_change_sql))
|
||||
.route("/query/build-table-owner-change-sql", post(routes::query::build_table_owner_change_sql))
|
||||
.route(
|
||||
"/query/preview-sqlite-table-structure-change",
|
||||
post(routes::query::preview_sqlite_table_structure_change),
|
||||
|
||||
@@ -267,6 +267,12 @@ pub struct BuildTableStructureSqlRequest {
|
||||
pub options: dbx_core::table_structure_sql::TableStructureSqlOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuildTableOwnerChangeSqlRequest {
|
||||
pub options: dbx_core::table_structure_sql::TableOwnerChangeSqlOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreviewSqliteTableStructureChangeRequest {
|
||||
@@ -947,6 +953,12 @@ pub async fn build_table_structure_change_sql(
|
||||
Json(dbx_core::table_structure_sql::build_table_structure_change_sql(req.options))
|
||||
}
|
||||
|
||||
pub async fn build_table_owner_change_sql(
|
||||
Json(req): Json<BuildTableOwnerChangeSqlRequest>,
|
||||
) -> Json<dbx_core::table_structure_sql::TableStructureSqlResult> {
|
||||
Json(dbx_core::table_structure_sql::build_table_owner_change_sql(req.options))
|
||||
}
|
||||
|
||||
pub async fn preview_sqlite_table_structure_change(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<PreviewSqliteTableStructureChangeRequest>,
|
||||
|
||||
@@ -790,6 +790,19 @@ pub async fn list_owners(
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError::from(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn get_table_owner(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let database = q.database.as_deref().unwrap_or("");
|
||||
let schema = q.schema.as_deref().unwrap_or("");
|
||||
let table = q.table.as_deref().unwrap_or("");
|
||||
let result = dbx_core::schema::get_table_owner_core(&state.app, &q.connection_id, database, schema, table)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError::from(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn list_extensions(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
|
||||
Reference in new issue
Block a user