feat(postgres): list user-defined types with details and DDL for PG-family databases

This commit is contained in:
amwps290 authored and GitHub committed 2026-08-11 15:00:24 +08:00
1 parent 0c6c1fada2
commit 154c460657
59 files changed
+6538 -150

No files matched your search

+2
View File
@@ -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,
+21
View File
@@ -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 })
}
+1 -1
View File
@@ -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,
+6
View File
@@ -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,
+2
View File
@@ -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,
File diff suppressed because it is too large. Load diff
+2
View File
@@ -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,
+2
View File
@@ -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()),
+252 -9
View File
@@ -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),
}
}
+116
View File
@@ -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};
+1
View File
@@ -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))
+15
View File
@@ -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>,