From d5b483b05061fa326b5aec0bc29bdd5db90c7ec8 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 25 Jun 2026 22:58:25 +0800 Subject: [PATCH] fix(tdengine): fix table data preview --- .../com/dbx/agent/tdengine/TDengineAgent.java | 13 ++- apps/desktop/src/components/grid/DataGrid.vue | 3 + .../src/components/sidebar/TreeItem.vue | 4 +- .../src/composables/useDataGridActions.ts | 1 + apps/desktop/src/lib/tableDataExport.ts | 4 +- apps/desktop/src/lib/tableSelectSql.ts | 1 + apps/desktop/src/lib/visibleDatabases.ts | 1 + apps/desktop/src/stores/queryStore.ts | 1 + crates/dbx-core/src/csv_export.rs | 1 + .../dbx-core/src/sql_dialect/table_select.rs | 36 ++++++- crates/dbx-core/src/sql_dialect/tests.rs | 96 +++++++++++++++---- crates/dbx-core/src/sql_dialect/types.rs | 2 + 12 files changed, 135 insertions(+), 28 deletions(-) diff --git a/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java b/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java index 19839efcf..e8099ab77 100644 --- a/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java +++ b/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java @@ -81,7 +81,10 @@ public final class TDengineAgent extends BaseDatabaseAgent { try (java.sql.Statement stmt = requireConnected().createStatement(); ResultSet rs = stmt.executeQuery("SHOW DATABASES")) { while (rs.next()) { - result.add(new DatabaseInfo(rs.getString(1))); + String name = rs.getString(1); + if (!isSystemDatabase(name)) { + result.add(new DatabaseInfo(name)); + } } } return result; @@ -323,6 +326,14 @@ public final class TDengineAgent extends BaseDatabaseAgent { return "`" + identifier.replace("`", "``") + "`"; } + private static boolean isSystemDatabase(String name) { + if (name == null) { + return false; + } + String normalized = name.trim().toLowerCase(Locale.ROOT); + return "information_schema".equals(normalized) || "performance_schema".equals(normalized); + } + private static Integer parseNumericPrecision(String dataType) { return parseIntGroup(NUMERIC_PRECISION_PATTERN, dataType, 2); } diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 8523c2b71..987e49839 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -198,6 +198,7 @@ const props = defineProps<{ tableMeta?: { schema?: string; tableName: string; + tableType?: string; columns: ColumnInfo[]; primaryKeys: string[]; }; @@ -3858,6 +3859,7 @@ async function applyOrderBySearch() { databaseType: resolvedDatabaseType.value, schema: tableMeta.schema, tableName: tableMeta.tableName, + tableType: tableMeta.tableType, columns: tableMeta.columns.map((column) => column.name), primaryKeys: tableMeta.primaryKeys, orderBy: orderByClause, @@ -3888,6 +3890,7 @@ async function applyWhereFilter() { databaseType: resolvedDatabaseType.value, schema: tableMeta.schema, tableName: tableMeta.tableName, + tableType: tableMeta.tableType, columns: tableMeta.columns.map((column) => column.name), primaryKeys: tableMeta.primaryKeys, orderBy: orderByInput.value.trim() || (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index f8cccf32c..d660258dc 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -904,7 +904,7 @@ async function openData() { dbType: config?.db_type, }); const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema); - const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE"; + const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : (node.tableType ?? "TABLE"); const isSameDataTableTab = (tab: (typeof queryStore.tabs)[number]) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label; const activateExistingSameTableTab = () => { const existing = queryStore.tabs.find(isSameDataTableTab); @@ -1072,6 +1072,7 @@ async function openData() { databaseType: effectiveDbType, schema: tableSchema, tableName: node.label, + tableType, columns: columns.map((column) => column.name), primaryKeys, limit, @@ -2719,6 +2720,7 @@ async function exportDataLegacy(format: "csv" | "json" | "sql") { databaseType: effectiveDbType, schema: node.schema, tableName: node.label, + tableType: node.tableType, columns: queryColumns, executePage: (sql) => api.executeQuery(connectionId, database, sql), }); diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index 6e82b5fad..746d56e65 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -37,6 +37,7 @@ export function useDataGridActions(activeTab: ComputedRef) databaseType: effectiveDbType, schema: tableMeta?.schema, tableName: tableMeta?.tableName ?? "", + tableType: tableMeta?.tableType, columns: tableMeta?.columns.map((column) => column.name), primaryKeys, includeRowId: useRowId, diff --git a/apps/desktop/src/lib/tableDataExport.ts b/apps/desktop/src/lib/tableDataExport.ts index 0ff90443c..688c87998 100644 --- a/apps/desktop/src/lib/tableDataExport.ts +++ b/apps/desktop/src/lib/tableDataExport.ts @@ -7,9 +7,10 @@ export interface FetchTableDataForExportOptions { databaseType?: DatabaseType; schema?: string; tableName: string; + tableType?: string; columns?: string[]; pageSize?: number; - buildPageSql?: (options: { databaseType?: DatabaseType; schema?: string; tableName: string; columns?: string[]; limit: number; offset: number }) => Promise | string; + buildPageSql?: (options: { databaseType?: DatabaseType; schema?: string; tableName: string; tableType?: string; columns?: string[]; limit: number; offset: number }) => Promise | string; executePage: (sql: string) => Promise; } @@ -25,6 +26,7 @@ export async function fetchTableDataForExport(options: FetchTableDataForExportOp databaseType: options.databaseType, schema: options.schema, tableName: options.tableName, + tableType: options.tableType, columns: options.columns, limit: pageSize, offset, diff --git a/apps/desktop/src/lib/tableSelectSql.ts b/apps/desktop/src/lib/tableSelectSql.ts index e4878078b..852d55305 100644 --- a/apps/desktop/src/lib/tableSelectSql.ts +++ b/apps/desktop/src/lib/tableSelectSql.ts @@ -7,6 +7,7 @@ export interface BuildTableSelectSqlOptions { databaseType?: DatabaseType; schema?: string; tableName: string; + tableType?: string; primaryKeys?: string[]; columns?: string[]; fallbackOrderColumns?: string[]; diff --git a/apps/desktop/src/lib/visibleDatabases.ts b/apps/desktop/src/lib/visibleDatabases.ts index c1b99c83e..e7ad2cd4b 100644 --- a/apps/desktop/src/lib/visibleDatabases.ts +++ b/apps/desktop/src/lib/visibleDatabases.ts @@ -17,6 +17,7 @@ const SYSTEM_DATABASE_RULES: Partial>> vastbase: new Set(["template0", "template1"]), redshift: new Set(["template0", "template1"]), clickhouse: new Set(["information_schema", "system"]), + tdengine: new Set(["information_schema", "performance_schema"]), sqlserver: new Set(["master", "model", "msdb", "tempdb"]), mongodb: new Set(["admin", "config", "local"]), oracle: new Set([ diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 2a87f592a..832a7ba0d 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -2172,6 +2172,7 @@ export const useQueryStore = defineStore("query", () => { databaseType: effectiveDbType, schema: tableMeta.schema, tableName: tableMeta.tableName, + tableType: tableMeta.tableType, columns: tableMeta.columns.map((column) => column.name), primaryKeys, whereInput: tab.whereInput, diff --git a/crates/dbx-core/src/csv_export.rs b/crates/dbx-core/src/csv_export.rs index d15afbfcb..697a239a4 100644 --- a/crates/dbx-core/src/csv_export.rs +++ b/crates/dbx-core/src/csv_export.rs @@ -120,6 +120,7 @@ pub async fn export_table_data_csv_core(state: &AppState, options: TableCsvExpor database_type: Some(database_type), schema: options.schema.clone(), table_name: options.table_name.clone(), + table_type: None, primary_keys: Vec::new(), columns: options.columns.clone(), fallback_order_columns: Vec::new(), diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index 46574e914..0986c3f4a 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -33,7 +33,11 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String let select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { format!("ROWIDTOCHAR(t.ROWID) AS \"{DBX_ROWID_COLUMN}\", t.*") } else { - build_select_columns(database_type, &options.columns) + build_select_columns( + database_type, + &options.columns, + tdengine_should_include_tbname(database_type, options.table_type.as_deref()), + ) }; let rownum_select_columns = quoted_table_columns_or_star(database_type, &options.columns); let page_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) { @@ -209,16 +213,40 @@ pub(super) fn is_tdengine_tbname(database_type: Option, name: &str database_type == Some(DatabaseType::Tdengine) && name.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN) } -pub(super) fn build_select_columns(database_type: Option, columns: &[String]) -> String { +fn tdengine_should_include_tbname(database_type: Option, table_type: Option<&str>) -> bool { + if database_type != Some(DatabaseType::Tdengine) { + return false; + } + matches!( + table_type.map(|value| value.trim().to_ascii_uppercase()), + Some(value) if value == "STABLE" || value == "SUPER TABLE" || value == "SUPERTABLE" + ) +} + +pub(super) fn build_select_columns( + database_type: Option, + columns: &[String], + include_tdengine_tbname: bool, +) -> String { if columns.is_empty() { return "*".to_string(); } if database_type == Some(DatabaseType::Tdengine) { let mut tdengine_columns = Vec::new(); - if !columns.iter().any(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)) { + if include_tdengine_tbname + && !columns.iter().any(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)) + { tdengine_columns.push(DBX_TDENGINE_TBNAME_COLUMN.to_string()); } - tdengine_columns.extend(columns.iter().cloned()); + tdengine_columns.extend( + columns + .iter() + .filter(|column| include_tdengine_tbname || !column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)) + .cloned(), + ); + if tdengine_columns.is_empty() { + return "*".to_string(); + } return tdengine_columns .iter() .map(|column| { diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index 697db110e..66f803b59 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -224,6 +224,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Mysql), schema: None, table_name: "users".to_string(), + table_type: None, primary_keys: vec!["id".to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -240,6 +241,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Goldendb), schema: None, table_name: "sys_dic".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -256,6 +258,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "orders".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -272,6 +275,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Xugu), schema: Some("DBX_TEST".to_string()), table_name: "PRODUCTS".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -288,6 +292,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::StarRocks), schema: None, table_name: "sales_report".to_string(), + table_type: None, primary_keys: Vec::new(), columns: vec!["customer_name".to_string(), "amount".to_string()], fallback_order_columns: Vec::new(), @@ -304,6 +309,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Db2), schema: Some("DB2INST1".to_string()), table_name: "ORDERS".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -336,6 +342,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::OceanbaseOracle), schema: Some("DBXTEST".to_string()), table_name: "ORDERS".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -352,6 +359,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::OceanbaseOracle), schema: Some("DBXTEST".to_string()), table_name: "ORDERS".to_string(), + table_type: None, primary_keys: vec!["ID".to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -368,6 +376,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Db2), schema: Some("DB2INST1".to_string()), table_name: "ORDERS".to_string(), + table_type: None, primary_keys: vec!["ID".to_string()], columns: vec!["ID".to_string(), "AMOUNT".to_string()], fallback_order_columns: Vec::new(), @@ -384,6 +393,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Iris), schema: Some("Ens".to_string()), table_name: "AlarmResponse".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -400,6 +410,7 @@ fn builds_table_data_where_and_schema_queries() { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "device2".to_string(), + table_type: None, primary_keys: Vec::new(), columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -420,6 +431,7 @@ fn builds_informix_table_data_with_skip_first_pagination() { database_type: Some(DatabaseType::Informix), schema: Some("ignored".to_string()), table_name: "users".to_string(), + table_type: None, primary_keys: vec!["id".to_string()], columns: vec!["id".to_string(), "name".to_string()], fallback_order_columns: Vec::new(), @@ -452,6 +464,7 @@ fn explicit_table_data_order_is_preserved() { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "country_gdp".to_string(), + table_type: None, primary_keys: vec!["year".to_string()], columns: vec!["iso3".to_string(), "year".to_string(), "gdp_pc".to_string()], fallback_order_columns: Vec::new(), @@ -471,6 +484,7 @@ fn builds_iris_table_data_sql_with_literal_top_and_quoted_object() { database_type: Some(DatabaseType::Iris), schema: Some("Ens".to_string()), table_name: "AlarmResponse".to_string(), + table_type: None, primary_keys: vec!["ID".to_string()], columns: vec!["ID".to_string(), "Status".to_string()], fallback_order_columns: Vec::new(), @@ -493,32 +507,68 @@ fn builds_iris_table_data_sql_with_literal_top_and_quoted_object() { #[test] fn builds_table_data_special_column_queries() { assert_eq!( - build_table_data_select_sql(TableDataSelectSqlOptions { - database_type: Some(DatabaseType::Tdengine), - schema: Some("test_db".to_string()), - table_name: "meters".to_string(), - primary_keys: vec!["ts".to_string()], - columns: vec![ - "ts".to_string(), - "current".to_string(), - "voltage".to_string(), - "location".to_string(), - "groupid".to_string(), - ], - fallback_order_columns: Vec::new(), - order_by: None, - limit: Some(100), - offset: None, - where_input: None, - include_row_id: false, - }), - "SELECT tbname, `ts` AS `ts`, `current` AS `current`, `voltage` AS `voltage`, `location` AS `location`, `groupid` AS `groupid` FROM `test_db`.`meters` LIMIT 100;" - ); + build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Tdengine), + schema: Some("test_db".to_string()), + table_name: "meters".to_string(), + table_type: Some("STABLE".to_string()), + primary_keys: vec!["ts".to_string()], + columns: vec![ + "ts".to_string(), + "current".to_string(), + "voltage".to_string(), + "location".to_string(), + "groupid".to_string(), + ], + fallback_order_columns: Vec::new(), + order_by: None, + limit: Some(100), + offset: None, + where_input: None, + include_row_id: false, + }), + "SELECT tbname, `ts` AS `ts`, `current` AS `current`, `voltage` AS `voltage`, `location` AS `location`, `groupid` AS `groupid` FROM `test_db`.`meters` LIMIT 100;" + ); + assert_eq!( + build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Tdengine), + schema: Some("test_db".to_string()), + table_name: "d1001".to_string(), + table_type: Some("TABLE".to_string()), + primary_keys: vec!["ts".to_string()], + columns: vec!["ts".to_string(), "current".to_string()], + fallback_order_columns: Vec::new(), + order_by: None, + limit: Some(100), + offset: None, + where_input: None, + include_row_id: false, + }), + "SELECT `ts` AS `ts`, `current` AS `current` FROM `test_db`.`d1001` LIMIT 100;" + ); + assert_eq!( + build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Tdengine), + schema: Some("test_db".to_string()), + table_name: "d1001".to_string(), + table_type: Some("TABLE".to_string()), + primary_keys: vec!["ts".to_string()], + columns: vec!["tbname".to_string(), "ts".to_string(), "current".to_string()], + fallback_order_columns: Vec::new(), + order_by: None, + limit: Some(100), + offset: None, + where_input: None, + include_row_id: false, + }), + "SELECT `ts` AS `ts`, `current` AS `current` FROM `test_db`.`d1001` LIMIT 100;" + ); assert_eq!( build_table_data_select_sql(TableDataSelectSqlOptions { database_type: Some(DatabaseType::Hive), schema: None, table_name: "departments".to_string(), + table_type: None, primary_keys: Vec::new(), columns: vec!["id".to_string(), "name".to_string()], fallback_order_columns: Vec::new(), @@ -539,6 +589,7 @@ fn builds_sqlserver_table_data_pages() { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo".to_string()), table_name: "accounts".to_string(), + table_type: None, primary_keys: vec!["id".to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -555,6 +606,7 @@ fn builds_sqlserver_table_data_pages() { database_type: Some(DatabaseType::SqlServer), schema: Some("sales".to_string()), table_name: "orders".to_string(), + table_type: None, primary_keys: vec!["order_id".to_string()], columns: vec!["order_id".to_string(), "customer".to_string()], fallback_order_columns: Vec::new(), @@ -575,6 +627,7 @@ fn builds_oracle_and_neo4j_table_data_queries() { database_type: Some(DatabaseType::Oracle), schema: Some("DBXTEST".to_string()), table_name: "DBX_LOAD_TABLE_006".to_string(), + table_type: None, primary_keys: vec![DBX_ROWID_COLUMN.to_string()], columns: Vec::new(), fallback_order_columns: Vec::new(), @@ -607,6 +660,7 @@ fn builds_oracle_and_neo4j_table_data_queries() { database_type: Some(DatabaseType::Neo4j), schema: None, table_name: "Employee".to_string(), + table_type: None, primary_keys: vec!["id".to_string()], columns: vec!["id".to_string(), "first name".to_string(), "role".to_string()], fallback_order_columns: Vec::new(), diff --git a/crates/dbx-core/src/sql_dialect/types.rs b/crates/dbx-core/src/sql_dialect/types.rs index 2ddfdd458..cfd5f061d 100644 --- a/crates/dbx-core/src/sql_dialect/types.rs +++ b/crates/dbx-core/src/sql_dialect/types.rs @@ -24,6 +24,8 @@ pub struct TableDataSelectSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub table_type: Option, #[serde(default)] pub primary_keys: Vec, #[serde(default)]