feat(data-grid): support manual reference display values
This commit is contained in:
24 files changed
+893
-90
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in new issue
Block a user