feat(postgres): list user-defined types with details and DDL for PG-family databases
This commit is contained in:
1 parent
0c6c1fada2
commit
154c460657
59 files changed
+6538
-150
No files matched your search
@@ -2532,6 +2532,8 @@ mod tests {
|
||||
schema: Some("public".to_string()),
|
||||
valid: None,
|
||||
signature: Some(signature.to_string()),
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -1127,6 +1127,7 @@ pub enum AgentMethod {
|
||||
CompletionAssistantSearchV1,
|
||||
GetObjectSource,
|
||||
GetColumns,
|
||||
GetCustomTypeDetails,
|
||||
ListIndexes,
|
||||
ListForeignKeys,
|
||||
ListTriggers,
|
||||
@@ -1208,6 +1209,7 @@ impl AgentMethod {
|
||||
Self::GetObjectSource => "get_object_source",
|
||||
Self::GetTableDdl => "get_table_ddl",
|
||||
Self::GetColumns => "get_columns",
|
||||
Self::GetCustomTypeDetails => "get_type_details",
|
||||
Self::ListIndexes => "list_indexes",
|
||||
Self::ListForeignKeys => "list_foreign_keys",
|
||||
Self::ListTriggers => "list_triggers",
|
||||
@@ -2060,6 +2062,21 @@ impl AgentDriverClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_custom_type_details<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
name: &str,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.call_method_with_timeout(
|
||||
AgentMethod::GetCustomTypeDetails,
|
||||
agent_type_details_params(database, schema, name),
|
||||
timeout_duration,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_table_comment<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
database: &str,
|
||||
@@ -2756,6 +2773,10 @@ pub fn agent_object_source_params<K: Serialize>(database: &str, schema: &str, na
|
||||
serde_json::json!({ "database": database, "schema": schema, "name": name, "object_type": object_type })
|
||||
}
|
||||
|
||||
pub fn agent_type_details_params(database: &str, schema: &str, name: &str) -> Value {
|
||||
serde_json::json!({ "database": database, "schema": schema, "name": name })
|
||||
}
|
||||
|
||||
pub fn agent_close_query_session_params(session_id: &str) -> Value {
|
||||
serde_json::json!({ "sessionId": session_id })
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ pub async fn list_tables_filtered(
|
||||
}
|
||||
|
||||
pub async fn list_objects(pool: &Pool, schema: &str) -> Result<Vec<ObjectInfo>, String> {
|
||||
let mut objects = db::postgres::list_objects(pool, schema).await?;
|
||||
let mut objects = db::postgres::list_objects(pool, schema, true, true, false).await?;
|
||||
let names = objects.iter().map(|object| object.name.clone()).collect::<Vec<_>>();
|
||||
let external_names = external_table_names(pool, schema, &names).await.unwrap_or_else(|error| {
|
||||
log::debug!("[cloudberry][list_objects:external-table-fallback] error={error}");
|
||||
|
||||
@@ -42,6 +42,8 @@ pub async fn list_objects(pool: &MySqlPool, database: &str) -> Result<Vec<Object
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -105,6 +107,8 @@ fn plugin_object(
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: if comment_parts.is_empty() { None } else { Some(comment_parts.join(", ")) },
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -2952,6 +2952,8 @@ fn row_to_object(row: &mysql_async::Row, database: &str) -> ObjectInfo {
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: get_opt_str(row, "object_comment")
|
||||
.map(|s| fix_potential_double_encoding(&s))
|
||||
.filter(|s| !s.is_empty()),
|
||||
@@ -3226,6 +3228,8 @@ fn table_infos_to_objects(
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: table.comment,
|
||||
created_at: meta.and_then(|meta| meta.created_at.clone()),
|
||||
updated_at: meta.and_then(|meta| meta.updated_at.clone()),
|
||||
@@ -5491,6 +5495,8 @@ mod tests {
|
||||
schema: Some("app".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -126,6 +126,8 @@ pub async fn list_objects(pool: &mysql_async::Pool, schema: &str) -> Result<Vec<
|
||||
.or_else(|| (!schema.trim().is_empty()).then(|| schema.to_string())),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
+1537
-51
File diff suppressed because it is too large.
Load diff
@@ -14,6 +14,8 @@ pub async fn list_objects(pool: &Pool, schema: &str) -> Result<Vec<ObjectInfo>,
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: t.comment.clone(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -2319,6 +2319,8 @@ pub async fn list_objects(client: &mut SqlServerClient, schema: &str) -> Result<
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: row.get::<&str, _>(4).filter(|s: &&str| !s.is_empty()).map(|s: &str| s.to_string()),
|
||||
created_at: row.get::<chrono::NaiveDateTime, _>(2).map(|value| value.to_string()),
|
||||
updated_at: row.get::<chrono::NaiveDateTime, _>(3).map(|value| value.to_string()),
|
||||
|
||||
@@ -2383,6 +2383,8 @@ async fn external_driver_presto_like_objects(
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -2600,6 +2602,10 @@ mod tests {
|
||||
TDENGINE_COMMENT_SEARCH_TIMEOUT, TDENGINE_LIKE_PATTERN_MAX_BYTES,
|
||||
};
|
||||
use super::{list_databases_core, list_tables_core};
|
||||
use super::{
|
||||
object_types_include_custom_types, object_types_include_relations, object_types_include_routines,
|
||||
object_types_only_custom_types, supports_custom_type_details, supports_pg_custom_type_objects,
|
||||
};
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType};
|
||||
use crate::storage::Storage;
|
||||
@@ -2772,6 +2778,96 @@ mod tests {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_pg_custom_type_objects_only_for_verified_family() {
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss, DatabaseType::Gaussdb] {
|
||||
assert!(supports_pg_custom_type_objects(&test_connection_config(db_type)), "{db_type:?}");
|
||||
}
|
||||
for db_type in [
|
||||
DatabaseType::Kingbase,
|
||||
DatabaseType::Vastbase,
|
||||
DatabaseType::Highgo,
|
||||
DatabaseType::Uxdb,
|
||||
DatabaseType::Kwdb,
|
||||
DatabaseType::Redshift,
|
||||
] {
|
||||
assert!(!supports_pg_custom_type_objects(&test_connection_config(db_type)), "{db_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_custom_type_details_covers_five_verified_families() {
|
||||
for db_type in [
|
||||
DatabaseType::Postgres,
|
||||
DatabaseType::OpenGauss,
|
||||
DatabaseType::Gaussdb,
|
||||
DatabaseType::Kingbase,
|
||||
DatabaseType::Vastbase,
|
||||
] {
|
||||
assert!(supports_custom_type_details(&test_connection_config(db_type)), "{db_type:?}");
|
||||
}
|
||||
for db_type in [
|
||||
DatabaseType::Highgo,
|
||||
DatabaseType::Uxdb,
|
||||
DatabaseType::Kwdb,
|
||||
DatabaseType::Redshift,
|
||||
DatabaseType::Mysql,
|
||||
DatabaseType::Xugu,
|
||||
] {
|
||||
assert!(!supports_custom_type_details(&test_connection_config(db_type)), "{db_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_types_include_custom_types_only_when_unfiltered_or_type_requested() {
|
||||
assert!(object_types_include_custom_types(None));
|
||||
assert!(object_types_include_custom_types(Some(&["TYPE".to_string()])));
|
||||
assert!(object_types_include_custom_types(Some(&["type_body".to_string()])));
|
||||
assert!(object_types_include_custom_types(Some(&["table".to_string(), "type".to_string()])));
|
||||
assert!(!object_types_include_custom_types(Some(&["TABLE".to_string()])));
|
||||
assert!(!object_types_include_custom_types(Some(&["FUNCTION".to_string()])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_types_select_independent_catalog_branches() {
|
||||
assert!(object_types_include_relations(None));
|
||||
assert!(object_types_include_routines(None));
|
||||
assert!(object_types_include_custom_types(None));
|
||||
|
||||
assert!(object_types_include_relations(Some(&["TABLE".to_string()])));
|
||||
assert!(object_types_include_relations(Some(&["VIEW".to_string()])));
|
||||
assert!(object_types_include_relations(Some(&["SEQUENCE".to_string()])));
|
||||
assert!(!object_types_include_routines(Some(&["TABLE".to_string()])));
|
||||
assert!(!object_types_include_custom_types(Some(&["TABLE".to_string()])));
|
||||
|
||||
assert!(object_types_include_routines(Some(&["PROCEDURE".to_string()])));
|
||||
assert!(object_types_include_routines(Some(&["FUNCTION".to_string()])));
|
||||
assert!(!object_types_include_relations(Some(&["FUNCTION".to_string()])));
|
||||
assert!(!object_types_include_custom_types(Some(&["FUNCTION".to_string()])));
|
||||
|
||||
assert!(object_types_include_custom_types(Some(&["TYPE".to_string()])));
|
||||
assert!(!object_types_include_relations(Some(&["TYPE".to_string()])));
|
||||
assert!(!object_types_include_routines(Some(&["TYPE".to_string()])));
|
||||
|
||||
// The sidebar type group sends the TYPE_BODY companion kind as well;
|
||||
// it must select the type branch alone, never relations or routines.
|
||||
let type_group = ["TYPE".to_string(), "TYPE_BODY".to_string()];
|
||||
assert!(object_types_include_custom_types(Some(&type_group)));
|
||||
assert!(!object_types_include_relations(Some(&type_group)));
|
||||
assert!(!object_types_include_routines(Some(&type_group)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_types_only_custom_types_detects_dedicated_type_requests() {
|
||||
assert!(!object_types_only_custom_types(None));
|
||||
assert!(object_types_only_custom_types(Some(&["TYPE".to_string()])));
|
||||
assert!(object_types_only_custom_types(Some(&["TYPE".to_string(), "TYPE_BODY".to_string()])));
|
||||
assert!(object_types_only_custom_types(Some(&["type_body".to_string()])));
|
||||
assert!(!object_types_only_custom_types(Some(&["TYPE".to_string(), "TABLE".to_string()])));
|
||||
assert!(!object_types_only_custom_types(Some(&["TABLE".to_string()])));
|
||||
assert!(!object_types_only_custom_types(Some(&[])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_metadata_uses_unique_ephemeral_sessions_only_for_agents() {
|
||||
let oracle = test_connection_config(DatabaseType::Oracle);
|
||||
@@ -3238,6 +3334,8 @@ for line in sys.stdin:
|
||||
schema: Some("app".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -4138,6 +4236,8 @@ for line in sys.stdin:
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -4152,6 +4252,8 @@ for line in sys.stdin:
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -4166,6 +4268,8 @@ for line in sys.stdin:
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -4770,10 +4874,19 @@ async fn list_objects_once(
|
||||
return Ok(unpaged_object_list(objects));
|
||||
}
|
||||
Ok(objects) => {
|
||||
if object_types_only_custom_types(object_types) {
|
||||
// A dedicated type request: the agent is authoritative.
|
||||
// The native fallback never lists types, so running it
|
||||
// would turn a real empty schema or a catalog error into
|
||||
// a misleading empty type group.
|
||||
return Ok(unpaged_object_list(objects));
|
||||
}
|
||||
if let Some(config) = fallback_config.as_ref() {
|
||||
match native_postgres_metadata_pool(state, connection_id, database, config).await {
|
||||
Ok(Some(pool)) => {
|
||||
return db::postgres::list_objects(&pool, schema).await.map(unpaged_object_list)
|
||||
return db::postgres::list_objects(&pool, schema, true, true, false)
|
||||
.await
|
||||
.map(unpaged_object_list)
|
||||
}
|
||||
Ok(None) => return Ok(unpaged_object_list(objects)),
|
||||
Err(error) => {
|
||||
@@ -4790,18 +4903,25 @@ async fn list_objects_once(
|
||||
return Ok(unpaged_object_list(objects));
|
||||
}
|
||||
Err(agent_error) => {
|
||||
if object_types_only_custom_types(object_types) {
|
||||
// Preserve the type catalog error instead of masking it
|
||||
// with a relation/function fallback that cannot serve
|
||||
// user-defined types.
|
||||
return Err(agent_error);
|
||||
}
|
||||
if let Some(config) = fallback_config.as_ref() {
|
||||
if let Some(pool) =
|
||||
native_postgres_metadata_pool(state, connection_id, database, config).await?
|
||||
{
|
||||
return db::postgres::list_objects(&pool, schema).await.map(unpaged_object_list).map_err(
|
||||
|fallback_error| {
|
||||
return db::postgres::list_objects(&pool, schema, true, true, false)
|
||||
.await
|
||||
.map(unpaged_object_list)
|
||||
.map_err(|fallback_error| {
|
||||
crate::db::agent_driver::append_legacy_error_context(
|
||||
&agent_error,
|
||||
&format!("Native PostgreSQL metadata fallback failed: {fallback_error}"),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Err(agent_error);
|
||||
@@ -4836,7 +4956,15 @@ async fn list_objects_once(
|
||||
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_cloudberry_config) => {
|
||||
db::cloudberry::list_objects(p, schema).await.map(unpaged_object_list)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(unpaged_object_list),
|
||||
PoolKind::Postgres(p) => {
|
||||
let include_relations = object_types_include_relations(object_types);
|
||||
let include_routines = object_types_include_routines(object_types);
|
||||
let include_custom_types = db_config.as_ref().is_some_and(supports_pg_custom_type_objects)
|
||||
&& object_types_include_custom_types(object_types);
|
||||
db::postgres::list_objects(p, schema, include_relations, include_routines, include_custom_types)
|
||||
.await
|
||||
.map(unpaged_object_list)
|
||||
}
|
||||
_ => {
|
||||
drop(connections);
|
||||
Ok(unpaged_object_list(
|
||||
@@ -4849,6 +4977,8 @@ async fn list_objects_once(
|
||||
schema: if schema.is_empty() { None } else { Some(schema.to_string()) },
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -4905,7 +5035,9 @@ async fn list_completion_objects_once(
|
||||
if let Some(config) = fallback_config.as_ref() {
|
||||
match native_postgres_metadata_pool(state, connection_id, database, config).await {
|
||||
Ok(Some(pool)) => {
|
||||
return db::postgres::list_objects(&pool, schema).await.map(filter_completion_objects)
|
||||
return db::postgres::list_objects(&pool, schema, true, true, false)
|
||||
.await
|
||||
.map(filter_completion_objects)
|
||||
}
|
||||
Ok(None) => objects,
|
||||
Err(error) => {
|
||||
@@ -4928,7 +5060,7 @@ async fn list_completion_objects_once(
|
||||
if let Some(pool) =
|
||||
native_postgres_metadata_pool(state, connection_id, database, config).await?
|
||||
{
|
||||
return db::postgres::list_objects(&pool, schema)
|
||||
return db::postgres::list_objects(&pool, schema, true, true, false)
|
||||
.await
|
||||
.map(filter_completion_objects)
|
||||
.map_err(|fallback_error| {
|
||||
@@ -4960,7 +5092,9 @@ async fn list_completion_objects_once(
|
||||
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_cloudberry_config) => {
|
||||
db::cloudberry::list_objects(p, schema).await.map(filter_completion_objects)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(filter_completion_objects),
|
||||
PoolKind::Postgres(p) => {
|
||||
db::postgres::list_objects(p, schema, true, true, false).await.map(filter_completion_objects)
|
||||
}
|
||||
PoolKind::SqlServer(_) => {
|
||||
drop(connections);
|
||||
let outcome =
|
||||
@@ -6237,6 +6371,52 @@ fn is_cloudberry_config(config: &ConnectionConfig) -> bool {
|
||||
matches!(config.driver_profile.as_deref(), Some("cloudberry"))
|
||||
}
|
||||
|
||||
/// Whether a native PostgreSQL connection should list user-defined types.
|
||||
///
|
||||
/// Only databases with a verified `pg_type` catalog contract are enabled.
|
||||
/// Other PG-protocol connections (Redshift, QuestDB, Cloudberry, KWDB, ...)
|
||||
/// keep the legacy object list even though they share `PoolKind::Postgres`.
|
||||
fn supports_pg_custom_type_objects(config: &ConnectionConfig) -> bool {
|
||||
matches!(config.db_type, DatabaseType::Postgres | DatabaseType::OpenGauss | DatabaseType::Gaussdb)
|
||||
}
|
||||
|
||||
/// Whether a typed object-list request needs the pg_class relation branch.
|
||||
///
|
||||
/// `None` means the caller wants the full object list (object browser “all
|
||||
/// objects” view), so every branch is selected. Group loads only request their
|
||||
/// own kinds (e.g. `["TABLE"]`), which skips the other catalog scans entirely.
|
||||
fn object_types_include_relations(object_types: Option<&[String]>) -> bool {
|
||||
object_types.is_none_or(|types| {
|
||||
types.iter().any(|t| {
|
||||
matches!(
|
||||
t.to_ascii_uppercase().as_str(),
|
||||
"TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "SEQUENCE" | "FOREIGN_TABLE" | "PARTITIONED_TABLE"
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn object_types_include_routines(object_types: Option<&[String]>) -> bool {
|
||||
object_types
|
||||
.is_none_or(|types| types.iter().any(|t| matches!(t.to_ascii_uppercase().as_str(), "PROCEDURE" | "FUNCTION")))
|
||||
}
|
||||
|
||||
fn object_types_include_custom_types(object_types: Option<&[String]>) -> bool {
|
||||
object_types
|
||||
.is_none_or(|types| types.iter().any(|t| t.eq_ignore_ascii_case("TYPE") || t.eq_ignore_ascii_case("TYPE_BODY")))
|
||||
}
|
||||
|
||||
/// Whether the object-type filter exclusively asks for user-defined types.
|
||||
///
|
||||
/// Used to keep agent errors visible: the native PostgreSQL fallback never
|
||||
/// lists custom types, so running it for a dedicated type request would mask a
|
||||
/// real catalog failure as an empty type group.
|
||||
fn object_types_only_custom_types(object_types: Option<&[String]>) -> bool {
|
||||
object_types.is_some_and(|types| {
|
||||
!types.is_empty() && types.iter().all(|t| t.eq_ignore_ascii_case("TYPE") || t.eq_ignore_ascii_case("TYPE_BODY"))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_default_oracle_agent_config(config: &ConnectionConfig) -> bool {
|
||||
// Only the default go-oracle agent handles filtered/paged metadata; legacy profiles keep Rust fallback paging.
|
||||
matches!(config.db_type, DatabaseType::Oracle)
|
||||
@@ -6880,6 +7060,67 @@ async fn read_mysql_object_source_row(
|
||||
.ok_or_else(|| "Failed to read object source".to_string())
|
||||
}
|
||||
|
||||
/// Whether a connection may serve custom type details (phase 2). Kept
|
||||
/// separate from listing support so a future per-kind DDL capability can be
|
||||
/// toggled independently.
|
||||
fn supports_custom_type_details(config: &ConnectionConfig) -> bool {
|
||||
matches!(
|
||||
config.db_type,
|
||||
DatabaseType::Postgres
|
||||
| DatabaseType::OpenGauss
|
||||
| DatabaseType::Gaussdb
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Vastbase
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_custom_type_details_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
name: &str,
|
||||
) -> Result<db::CustomTypeDetails, String> {
|
||||
retry_metadata_connection(state, connection_id, Some(database), || {
|
||||
get_custom_type_details_once(state, connection_id, database, schema, name)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_custom_type_details_once(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
name: &str,
|
||||
) -> Result<db::CustomTypeDetails, String> {
|
||||
let pool_key = state.get_or_create_metadata_pool_for_session(connection_id, Some(database), None).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
let Some(config) = db_config.as_ref() else {
|
||||
return Err("connection not found".to_string());
|
||||
};
|
||||
if !supports_custom_type_details(config) {
|
||||
return Err(format!("custom type details are not supported for {:?} connections", config.db_type));
|
||||
}
|
||||
{
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_pool!(&connections, &pool_key, Agent) {
|
||||
let timeout_duration = agent_metadata_timeout(db_config.as_ref());
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
return client
|
||||
.get_custom_type_details::<db::CustomTypeDetails>(database, schema, name, timeout_duration)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
let connections = state.connections.read().await;
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
match pool {
|
||||
PoolKind::Postgres(p) => db::postgres::get_custom_type_details(p, schema, name).await,
|
||||
_ => Err("custom type details are not supported for this connection type".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_object_source_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
@@ -7109,6 +7350,8 @@ async fn oracle_agent_list_objects(
|
||||
schema,
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -239,6 +239,8 @@ mod tests {
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -251,6 +253,8 @@ mod tests {
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -263,6 +267,8 @@ mod tests {
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -315,6 +321,8 @@ mod tests {
|
||||
schema: Some("HR".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -327,6 +335,8 @@ mod tests {
|
||||
schema: Some("HR".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(in crate::schema) async fn list_objects(
|
||||
PoolKind::Postgres(p) if config.is_some_and(is_cloudberry_config) => {
|
||||
db::cloudberry::list_objects(p, schema).await.map(Some)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(Some),
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema, true, true, false).await.map(Some),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ pub(in crate::schema) async fn list_completion_objects(
|
||||
PoolKind::Postgres(p) if config.is_some_and(is_cloudberry_config) => {
|
||||
db::cloudberry::list_objects(p, schema).await.map(Some)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(Some),
|
||||
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema, true, true, false).await.map(Some),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ pub struct ObjectInfo {
|
||||
pub valid: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub signature: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom_type_kind: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub has_members: Option<bool>,
|
||||
pub comment: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
@@ -510,6 +514,118 @@ pub struct OwnerInfo {
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CustomTypeKind {
|
||||
Base,
|
||||
Composite,
|
||||
Domain,
|
||||
Enum,
|
||||
Range,
|
||||
Multirange,
|
||||
}
|
||||
|
||||
impl CustomTypeKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Base => "base",
|
||||
Self::Composite => "composite",
|
||||
Self::Domain => "domain",
|
||||
Self::Enum => "enum",
|
||||
Self::Range => "range",
|
||||
Self::Multirange => "multirange",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A member of a user-defined type.
|
||||
///
|
||||
/// Composite types expose fields (`name`/`data_type`/`ordinal`/`nullable`/
|
||||
/// `default`/`comment`), enums expose values (`ordinal`/`enum_value`), and
|
||||
/// domains/ranges/base types expose neither (their members list stays empty;
|
||||
/// the UI shows an explanatory empty state instead).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomTypeMember {
|
||||
pub name: String,
|
||||
pub data_type: String,
|
||||
pub ordinal: i32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nullable: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enum_value: Option<String>,
|
||||
}
|
||||
|
||||
/// A domain CHECK constraint, keeping its name so generated DDL never invents
|
||||
/// duplicate constraint identifiers.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomTypeDomainConstraint {
|
||||
pub name: String,
|
||||
pub definition: String,
|
||||
}
|
||||
|
||||
/// Category-specific type attributes. Fields that do not apply to a category
|
||||
/// are `None`, never empty strings, so the UI can distinguish “unknown” from
|
||||
/// “not applicable”.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomTypeProperties {
|
||||
pub base_type: Option<String>,
|
||||
pub not_null: Option<bool>,
|
||||
pub default: Option<String>,
|
||||
pub collation: Option<String>,
|
||||
#[serde(default)]
|
||||
pub domain_constraints: Vec<CustomTypeDomainConstraint>,
|
||||
pub range_subtype: Option<String>,
|
||||
pub range_multirange_name: Option<String>,
|
||||
pub range_canonical_function: Option<String>,
|
||||
pub range_subtype_diff_function: Option<String>,
|
||||
pub range_subtype_opclass: Option<String>,
|
||||
pub input_function: Option<String>,
|
||||
pub output_function: Option<String>,
|
||||
pub receive_function: Option<String>,
|
||||
pub send_function: Option<String>,
|
||||
pub analyze_function: Option<String>,
|
||||
pub internallength: Option<i32>,
|
||||
pub passed_by_value: Option<bool>,
|
||||
pub alignment: Option<String>,
|
||||
pub storage: Option<String>,
|
||||
}
|
||||
|
||||
/// Generated `CREATE TYPE` text for a user-defined type.
|
||||
///
|
||||
/// `complete = true` means the text can be executed standalone in the current
|
||||
/// schema. `complete = false` means the text is for viewing only (it may depend
|
||||
/// on other types/functions or internal attributes); `warnings` must be shown
|
||||
/// in the UI and the text must never be presented as executable source.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomTypeDdl {
|
||||
pub sql: String,
|
||||
pub complete: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomTypeDetails {
|
||||
pub name: String,
|
||||
pub schema: String,
|
||||
pub kind: CustomTypeKind,
|
||||
pub comment: Option<String>,
|
||||
#[serde(default)]
|
||||
pub members: Vec<CustomTypeMember>,
|
||||
pub properties: CustomTypeProperties,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ddl: Option<CustomTypeDdl>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ObjectInfo, ObjectSourceKind, QueryMessage, SpatialColumn, SpatialColumnBuilder};
|
||||
|
||||
@@ -361,6 +361,7 @@ async fn main() {
|
||||
.route("/schema/completion-objects", get(routes::schema::list_completion_objects))
|
||||
.route("/schema/completion-assistant", post(routes::schema::completion_assistant_search))
|
||||
.route("/schema/object-source", get(routes::schema::get_object_source))
|
||||
.route("/schema/custom-type-details", get(routes::schema::get_custom_type_details))
|
||||
.route("/schema/columns", get(routes::schema::list_columns))
|
||||
.route("/schema/all-columns", get(routes::schema::get_all_columns))
|
||||
.route("/schema/data-types", get(routes::schema::list_data_types))
|
||||
|
||||
@@ -250,6 +250,8 @@ pub async fn list_objects(
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
custom_type_kind: None,
|
||||
has_members: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -332,6 +334,19 @@ pub async fn get_object_source(
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_custom_type_details(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<dbx_core::db::CustomTypeDetails>, AppError> {
|
||||
let database = q.database.as_deref().unwrap_or("");
|
||||
let schema = q.schema.as_deref().unwrap_or("");
|
||||
let name = q.table.as_deref().unwrap_or("");
|
||||
let result = dbx_core::schema::get_custom_type_details_core(&state.app, &q.connection_id, database, schema, name)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn list_columns(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
|
||||
Reference in new issue
Block a user