feat(data-grid): support manual reference display values

This commit is contained in:
zipg authored and GitHub committed 2026-08-23 01:38:50 +08:00
1 parent b74d49e982
commit 4a1f3e7141
24 files changed
+893 -90

No files matched your search

+14 -6
View File
@@ -3585,7 +3585,7 @@ fn postgres_indexes_for_relations_query_tiers() -> [&'static str; 2] {
fn postgres_indexes_for_relations_sql() -> &'static str {
"SELECT t.oid::bigint AS relid, i.relname AS index_name, \
array_agg(COALESCE(a.attname, pg_get_indexdef(ix.indexrelid, k.n::int, true)) ORDER BY k.n) AS columns, \
ix.indisunique AS is_unique, \
(ix.indisunique AND ix.indisvalid) AS is_unique, \
ix.indisprimary AS is_primary, \
pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \
am.amname AS index_type, \
@@ -3600,7 +3600,7 @@ fn postgres_indexes_for_relations_sql() -> &'static str {
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true \
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0 \
WHERE t.oid = ANY($1::bigint[]) \
GROUP BY t.oid, i.relname, i.oid, ix.indisunique, ix.indisprimary, ix.indpred, ix.indrelid, am.amname, ix.indnkeyatts, ix.indkey \
GROUP BY t.oid, i.relname, i.oid, ix.indisunique, ix.indisvalid, ix.indisprimary, ix.indpred, ix.indrelid, am.amname, ix.indnkeyatts, ix.indkey \
ORDER BY t.oid, i.relname"
}
@@ -3617,7 +3617,7 @@ fn postgres_indexes_for_relations_compat_sql() -> &'static str {
AND a.attnum > 0 \
ORDER BY pos.n \
) AS columns, \
ix.indisunique AS is_unique, \
(ix.indisunique AND ix.indisvalid) AS is_unique, \
ix.indisprimary AS is_primary, \
pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \
am.amname AS index_type, \
@@ -6517,7 +6517,7 @@ async fn execute_query_with_max_rows_inner(
// (schema, table) instead of a batch of oids — see the note there.
const POSTGRES_INDEXES_SQL: &str = "SELECT i.relname AS index_name, \
array_agg(COALESCE(a.attname, pg_get_indexdef(ix.indexrelid, k.n::int, true)) ORDER BY k.n) AS columns, \
ix.indisunique AS is_unique, \
(ix.indisunique AND ix.indisvalid) AS is_unique, \
ix.indisprimary AS is_primary, \
pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \
am.amname AS index_type, \
@@ -6533,7 +6533,7 @@ const POSTGRES_INDEXES_SQL: &str = "SELECT i.relname AS index_name, \
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true \
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0 \
WHERE t.oid = (CASE WHEN $1 = '' THEN quote_ident($2) ELSE quote_ident($1) || '.' || quote_ident($2) END)::regclass \
GROUP BY i.relname, i.oid, ix.indisunique, ix.indisprimary, ix.indpred, ix.indrelid, am.amname, ix.indnkeyatts, ix.indkey \
GROUP BY i.relname, i.oid, ix.indisunique, ix.indisvalid, ix.indisprimary, ix.indpred, ix.indrelid, am.amname, ix.indnkeyatts, ix.indkey \
ORDER BY i.relname";
// Compat-tier sibling of `postgres_indexes_for_relations_compat_sql` (~line
@@ -6548,7 +6548,7 @@ const POSTGRES_INDEXES_COMPAT_SQL: &str = "SELECT i.relname AS index_name, \
AND a.attnum > 0 \
ORDER BY pos.n \
) AS columns, \
ix.indisunique AS is_unique, \
(ix.indisunique AND ix.indisvalid) AS is_unique, \
ix.indisprimary AS is_primary, \
pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \
am.amname AS index_type, \
@@ -10739,6 +10739,14 @@ mod tests {
assert!(!POSTGRES_INDEXES_COMPAT_SQL.contains("WITH ORDINALITY"));
assert!(POSTGRES_INDEXES_COMPAT_SQL.contains("generate_series"));
assert!(POSTGRES_INDEXES_COMPAT_SQL.contains("string_to_array(ix.indkey::text, ' ')"));
for sql in [
POSTGRES_INDEXES_SQL,
POSTGRES_INDEXES_COMPAT_SQL,
postgres_indexes_for_relations_sql(),
postgres_indexes_for_relations_compat_sql(),
] {
assert!(sql.contains("ix.indisunique AND ix.indisvalid"));
}
}
#[test]
+14 -1
View File
@@ -2688,6 +2688,7 @@ fn sqlserver_indexes_sql_with_filter_definition(schema: &str, table: &str, inclu
} else {
"CAST(NULL AS NVARCHAR(MAX)) AS filter_definition"
};
let unfiltered_predicate = if include_filter_definition { " AND i.has_filter = 0" } else { "" };
let object_id = sqlserver_object_id_expression(schema, table);
format!(
"SELECT i.name, \
@@ -2697,7 +2698,7 @@ fn sqlserver_indexes_sql_with_filter_definition(schema: &str, table: &str, inclu
WHERE ic2.object_id = i.object_id AND ic2.index_id = i.index_id AND ic2.is_included_column = 0 \
ORDER BY ic2.key_ordinal \
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '') AS columns, \
i.is_unique, i.is_primary_key, i.type_desc, \
CAST(CASE WHEN i.is_unique = 1 AND i.is_disabled = 0 AND i.is_hypothetical = 0{unfiltered_predicate} THEN 1 ELSE 0 END AS bit) AS is_unique, i.is_primary_key, i.type_desc, \
STUFF((SELECT ',' + c3.name \
FROM sys.index_columns ic3 \
JOIN sys.columns c3 ON ic3.object_id = c3.object_id AND ic3.column_id = c3.column_id \
@@ -3973,11 +3974,23 @@ mod tests {
assert!(sql.contains("MS_Description"));
}
#[test]
fn sqlserver_index_metadata_only_reports_usable_unique_indexes() {
let sql = sqlserver_indexes_sql("dbo", "orders");
assert!(sql.contains("i.has_filter = 0"));
for sql in [sql, sqlserver_legacy_indexes_sql("dbo", "orders")] {
assert!(sql.contains("i.is_unique = 1"));
assert!(sql.contains("i.is_disabled = 0"));
assert!(sql.contains("i.is_hypothetical = 0"));
}
}
#[test]
fn sqlserver_legacy_indexes_sql_omits_filtered_index_metadata() {
let sql = sqlserver_legacy_indexes_sql("dbo", "orders");
assert!(!sql.contains("i.filter_definition"));
assert!(!sql.contains("i.has_filter"));
assert!(sql.contains("CAST(NULL AS NVARCHAR(MAX)) AS filter_definition"));
assert!(sql.contains("ep.value AS index_comment"));
}
+65 -5
View File
@@ -2892,23 +2892,54 @@ mod tests {
oracle_object_statistics_rows_only_sql, oracle_object_statistics_sql,
oracle_object_statistics_user_segments_sql, oracle_table_comment_from_query_result, oracle_table_comment_sql,
oracle_table_comments_sql, presto_like_columns_from_query_result, presto_like_information_schema_columns_sql,
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result, replace_metadata_runtime,
should_query_oracle_columns_via_sql_first, table_comments_from_query_result, table_name_filter_matches,
tdengine_table_comment_like_pattern, tdengine_table_comment_sql, tdengine_table_comments_sql,
uses_mongodb_agent_collection_listing, visible_schema_filter, MetadataErrorAction, MysqlTableListSource,
TableNameFilter, ORACLE_CURRENT_SCHEMA_SQL, TDENGINE_COMMENT_SEARCH_TIMEOUT, TDENGINE_LIKE_PATTERN_MAX_BYTES,
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result,
reference_key_columns_from_indexes, replace_metadata_runtime, should_query_oracle_columns_via_sql_first,
table_comments_from_query_result, table_name_filter_matches, tdengine_table_comment_like_pattern,
tdengine_table_comment_sql, tdengine_table_comments_sql, uses_mongodb_agent_collection_listing,
visible_schema_filter, MetadataErrorAction, MysqlTableListSource, TableNameFilter, ORACLE_CURRENT_SCHEMA_SQL,
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, DatabaseConnectionInfo, DatabaseType};
use crate::storage::Storage;
use std::collections::HashMap;
use std::time::Duration;
#[test]
fn reference_key_columns_require_effective_unfiltered_single_column_uniqueness() {
let index = |name: &str, columns: &[&str], is_unique: bool, is_primary: bool| db::IndexInfo {
name: name.to_string(),
columns: columns.iter().map(|column| (*column).to_string()).collect(),
is_unique,
is_primary,
filter: None,
index_type: None,
included_columns: None,
comment: None,
key_is_expression: Vec::new(),
};
let mut filtered = index("uq_active_code", &["active_code"], true, false);
filtered.filter = Some("active = true".to_string());
let mut expression = index("uq_lower_email", &["lower(email)"], true, false);
expression.key_is_expression = vec![true];
let indexes = vec![
index("clickhouse_primary", &["event_id"], false, true),
index("uq_code", &["code"], true, false),
index("uq_Code", &["Code"], true, false),
index("uq_tenant_code", &["tenant_id", "code"], true, false),
filtered,
expression,
];
assert_eq!(reference_key_columns_from_indexes(&indexes), vec!["code", "Code"]);
}
fn oracle_current_schema_result(columns: &[&str], rows: Vec<Vec<serde_json::Value>>) -> db::QueryResult {
db::QueryResult {
columns: columns.iter().map(|column| (*column).to_string()).collect(),
@@ -6544,6 +6575,35 @@ pub async fn list_indexes_core(
result
}
pub fn reference_key_columns_from_indexes(indexes: &[db::IndexInfo]) -> Vec<String> {
let mut columns = Vec::new();
for index in indexes {
if !index.is_unique
|| index.filter.as_deref().is_some_and(|filter| !filter.trim().is_empty())
|| index.columns.len() != 1
|| index.key_is_expression.first().copied().unwrap_or(false)
{
continue;
}
let column = index.columns[0].trim();
if !column.is_empty() && !columns.iter().any(|existing| existing == column) {
columns.push(column.to_string());
}
}
columns
}
pub async fn list_reference_key_columns_core(
state: &AppState,
connection_id: &str,
database: &str,
schema: &str,
table: &str,
) -> Result<Vec<String>, String> {
let indexes = list_indexes_core(state, connection_id, database, schema, table).await?;
Ok(reference_key_columns_from_indexes(&indexes))
}
async fn list_indexes_core_for_session(
state: &AppState,
connection_id: &str,
+1
View File
@@ -423,6 +423,7 @@ async fn main() {
.route("/schema/all-columns", get(routes::schema::get_all_columns))
.route("/schema/data-types", get(routes::schema::list_data_types))
.route("/schema/indexes", get(routes::schema::list_indexes))
.route("/schema/reference-key-columns", get(routes::schema::list_reference_key_columns))
.route("/schema/foreign-keys", get(routes::schema::list_foreign_keys))
.route("/schema/triggers", get(routes::schema::list_triggers))
.route("/schema/constraints", get(routes::schema::list_constraints))
+20
View File
@@ -545,6 +545,26 @@ pub async fn list_indexes(
Ok(Json(serde_json::to_value(result).map_err(|e| AppError::from(e.to_string()))?))
}
pub async fn list_reference_key_columns(
State(state): State<Arc<WebState>>,
Query(q): Query<SchemaQuery>,
) -> Result<Json<Vec<String>>, 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 catalog = external_doris_catalog(&state, &q.connection_id, q.catalog.as_deref()).await;
let indexes = if let Some(catalog) = catalog.as_deref() {
dbx_core::schema::list_doris_catalog_indexes_core(&state.app, &q.connection_id, catalog, database, table)
.await
.map_err(AppError::from)?
} else {
dbx_core::schema::list_indexes_core(&state.app, &q.connection_id, database, schema, table)
.await
.map_err(AppError::from)?
};
Ok(Json(dbx_core::schema::reference_key_columns_from_indexes(&indexes)))
}
pub async fn list_foreign_keys(
State(state): State<Arc<WebState>>,
Query(q): Query<SchemaQuery>,