Files
hiJu 0e47ca771d feat(db): 新增 TDengine 时序数据库适配器与连接支持
- 适配器:基于官方 taos 0.12 驱动(ws-rustls,经 taosAdapter :6041 WebSocket
  协议,纯 Rust 无本地 C 依赖),实现 DatabasePlugin/DbConnection 全量接口:
  数据库/超级表/子表列举、DESCRIBE(含 TAG 识别)、分页(LIMIT n OFFSET m)、
  语句执行/中断、EXPLAIN 等能力声明,内置 19 个单元测试
- 连接模型:DatabaseType 新增 TDengine 内置变体(builtin_all/as_str/from_str/
  图标映射),连接校验使用 SELECT SERVER_STATUS()
- 连接表单:DbFormConfig::tdengine() 声明式配置(常规/高级/SSL/SSH/备注),
  端口默认 6041、用户默认 root
- db_view 扩散:插件注册、扩展菜单、SQL 编辑器、数据对比/分页/类型映射等
  适配;索引 DDL 测试跳过 TDengine(无二级索引概念)
- 周边打通:连接导入协议、onetcli 连接构建/Schema、TDengine CLI 命令
  (taos -h/-P/-u/-d 与 jdbc:TAOS-RS://)、中英繁语言包
- 图标:新增 tdengine_color/tdengine_line_color 彩色与线条图标

同时包含后续 MQTT 提交所需的共享基础:models.rs 中的 MqttParams/
MqttVersion 连接参数模型、ConnectionType::Mqtt 变体、mqtt(线/面)图标、
以及工作区 Cargo.toml 的 taos/rumqttc 依赖与 mqtt 成员声明。
2026-09-04 10:20:48 +08:00

144 lines
4.1 KiB
Rust

use one_core::storage::DatabaseType;
use crate::DatabasePlugin;
use crate::executor::QueryColumnMeta;
use crate::types::{ColumnInfo, TableCellValue};
pub(crate) fn quote_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
pub(crate) fn parse_boolean(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"true" | "t" | "1" => Some(true),
"false" | "f" | "0" => Some(false),
_ => None,
}
}
pub(crate) fn strict_numeric_literal(value: &str) -> Option<&str> {
let value = value.trim();
if value.is_empty() {
return None;
}
let bytes = value.as_bytes();
let mut index = usize::from(matches!(bytes.first(), Some(b'+') | Some(b'-')));
if index == bytes.len() {
return None;
}
let integer_start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
let integer_digits = index - integer_start;
let mut fractional_digits = 0;
if bytes.get(index) == Some(&b'.') {
index += 1;
let fractional_start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
fractional_digits = index - fractional_start;
}
if integer_digits == 0 && fractional_digits == 0 {
return None;
}
if matches!(bytes.get(index), Some(b'e') | Some(b'E')) {
index += 1;
if matches!(bytes.get(index), Some(b'+') | Some(b'-')) {
index += 1;
}
let exponent_start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
if index == exponent_start {
return None;
}
}
(index == bytes.len()).then_some(value)
}
pub(crate) fn format_binary_literal_for_database(
database_type: &DatabaseType,
bytes: &[u8],
) -> String {
let hex = hex::encode(bytes);
match database_type {
DatabaseType::PostgreSQL => format!("decode('{hex}', 'hex')"),
DatabaseType::MSSQL => format!("0x{hex}"),
DatabaseType::Oracle => format!("HEXTORAW('{hex}')"),
DatabaseType::DuckDB => format!("from_hex('{hex}')"),
DatabaseType::ClickHouse => format!("unhex('{hex}')"),
// TDengine 与 MySQL 同臂处理(X'..' 十六进制字面量)。
DatabaseType::MySQL
| DatabaseType::SQLite
| DatabaseType::TDengine
| DatabaseType::External { .. } => {
format!("X'{hex}'")
}
}
}
pub(crate) fn format_table_value_for_database(
database_type: &DatabaseType,
value: &TableCellValue,
column: Option<&ColumnInfo>,
) -> String {
match value {
TableCellValue::Null => "NULL".to_string(),
TableCellValue::Binary(bytes) => format_binary_literal_for_database(database_type, bytes),
TableCellValue::Text(value) => {
format_special_table_value_for_database(database_type, value, column)
.unwrap_or_else(|| quote_string(value))
}
}
}
pub(crate) fn format_query_text_value<P>(
plugin: &P,
value: Option<&str>,
meta: Option<&QueryColumnMeta>,
) -> String
where
P: DatabasePlugin + ?Sized,
{
let Some(value) = value else {
return "NULL".to_string();
};
let column = meta.map(column_info_from_query_meta);
plugin.format_table_change_value(&TableCellValue::Text(value.to_string()), column.as_ref())
}
fn column_info_from_query_meta(meta: &QueryColumnMeta) -> ColumnInfo {
ColumnInfo {
name: meta.name.clone(),
data_type: meta.db_type.clone(),
is_nullable: meta.nullable,
is_primary_key: false,
default_value: None,
comment: None,
charset: None,
collation: None,
}
}
pub(crate) fn format_special_table_value_for_database(
database_type: &DatabaseType,
value: &str,
column: Option<&ColumnInfo>,
) -> Option<String> {
let column = column?;
crate::sql_literal_values::format_special_table_value(database_type, value, &column.data_type)
}
#[cfg(test)]
#[path = "sql_literal_tests.rs"]
mod tests;