feat(export): move text exports to Rust

This commit is contained in:
t8y2 committed 2026-05-22 15:41:33 +08:00
1 parent 209eff528d
commit c91fbc9fd6
17 files changed
+367 -142

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ pub mod sql_analysis;
pub mod sql_editability;
pub mod storage;
pub mod table_import;
pub mod text_export;
pub mod transfer;
pub mod types;
pub mod update;
+143
View File
@@ -0,0 +1,143 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryResultTextExportData {
pub columns: Vec<String>,
pub rows: Vec<Vec<Value>>,
}
pub fn format_json(data: &QueryResultTextExportData) -> Result<String, String> {
let rows = data
.rows
.iter()
.map(|row| {
let mut object = Map::new();
for (index, column) in data.columns.iter().enumerate() {
if let Some(value) = row.get(index) {
object.insert(column.clone(), value.clone());
}
}
Value::Object(object)
})
.collect::<Vec<_>>();
serde_json::to_string_pretty(&rows).map_err(|err| err.to_string())
}
pub fn format_markdown(data: &QueryResultTextExportData) -> String {
let normalized_columns = data.columns.iter().map(|column| markdown_cell(column)).collect::<Vec<_>>();
let normalized_rows = data
.rows
.iter()
.map(|row| row.iter().map(|cell| markdown_cell(&display_cell(cell))).collect::<Vec<_>>())
.collect::<Vec<_>>();
let widths = normalized_columns
.iter()
.enumerate()
.map(|(index, column)| {
let row_width = normalized_rows
.iter()
.map(|row| row.get(index).map(|cell| cell.chars().count()).unwrap_or(0))
.max()
.unwrap_or(0);
column.chars().count().max(row_width).max(3)
})
.collect::<Vec<_>>();
let header = format!(
"| {} |",
normalized_columns
.iter()
.enumerate()
.map(|(index, column)| pad(column, widths[index]))
.collect::<Vec<_>>()
.join(" | ")
);
let separator = format!("| {} |", widths.iter().map(|width| "-".repeat(*width)).collect::<Vec<_>>().join(" | "));
let body = normalized_rows
.iter()
.map(|row| {
format!(
"| {} |",
row.iter()
.enumerate()
.map(|(index, cell)| pad(cell, widths.get(index).copied().unwrap_or(3)))
.collect::<Vec<_>>()
.join(" | ")
)
})
.collect::<Vec<_>>()
.join("\n");
[header, separator, body].into_iter().filter(|part| !part.is_empty()).collect::<Vec<_>>().join("\n") + "\n"
}
fn display_cell(value: &Value) -> String {
match value {
Value::Null => "NULL".to_string(),
Value::Bool(value) => value.to_string(),
Value::Number(value) => value.to_string(),
Value::String(value) => value.clone(),
other => other.to_string(),
}
}
fn markdown_cell(value: &str) -> String {
value.replace('|', "\\|").replace("\r\n", "<br>").replace('\n', "<br>")
}
fn pad(value: &str, width: usize) -> String {
let current = value.chars().count();
if current >= width {
return value.to_string();
}
format!("{value}{}", " ".repeat(width - current))
}
#[cfg(test)]
mod tests {
use serde_json::{json, Value};
use super::{format_json, format_markdown, QueryResultTextExportData};
#[test]
fn formats_json_rows_as_objects() {
let out = format_json(&QueryResultTextExportData {
columns: vec!["id".to_string(), "name".to_string(), "active".to_string(), "note".to_string()],
rows: vec![vec![json!(1), json!("Ada"), json!(true), Value::Null]],
})
.unwrap();
assert_eq!(
out,
r#"[
{
"id": 1,
"name": "Ada",
"active": true,
"note": null
}
]"#
);
}
#[test]
fn formats_markdown_with_escaped_pipes_and_newlines() {
let out = format_markdown(&QueryResultTextExportData {
columns: vec!["id".to_string(), "payload|kind".to_string()],
rows: vec![vec![json!(1), json!("a|b")], vec![json!(2), json!("line one\nline two")]],
});
assert_eq!(
out,
[
"| id | payload\\|kind |",
"| --- | -------------------- |",
"| 1 | a\\|b |",
"| 2 | line one<br>line two |",
"",
]
.join("\n")
);
}
}
+2
View File
@@ -128,6 +128,8 @@ async fn main() {
.route("/query/prepare-data-grid-save", post(routes::query::prepare_data_grid_save))
.route("/query/cancel", post(routes::query::cancel_query))
.route("/query/close-session", post(routes::query::close_query_session))
.route("/export/query-result-json", post(routes::text_export::export_query_result_json))
.route("/export/query-result-markdown", post(routes::text_export::export_query_result_markdown))
// Redis
.route("/redis/list-databases", post(routes::redis::list_databases))
.route("/redis/scan-keys", post(routes::redis::scan_keys))
+1
View File
@@ -14,5 +14,6 @@ pub mod schema;
pub mod schema_cache;
pub mod sql_file;
pub mod table_import;
pub mod text_export;
pub mod transfer;
pub mod update;
+38
View File
@@ -0,0 +1,38 @@
use axum::Json;
use dbx_core::text_export::{format_json, format_markdown, QueryResultTextExportData};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::AppError;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryResultTextExportRequest {
pub columns: Vec<String>,
pub rows: Vec<Vec<Value>>,
}
impl QueryResultTextExportRequest {
fn data(self) -> QueryResultTextExportData {
QueryResultTextExportData { columns: self.columns, rows: self.rows }
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryResultTextExportResponse {
pub content: String,
}
pub async fn export_query_result_json(
Json(req): Json<QueryResultTextExportRequest>,
) -> Result<Json<QueryResultTextExportResponse>, AppError> {
let content = format_json(&req.data()).map_err(AppError::from)?;
Ok(Json(QueryResultTextExportResponse { content }))
}
pub async fn export_query_result_markdown(
Json(req): Json<QueryResultTextExportRequest>,
) -> Json<QueryResultTextExportResponse> {
Json(QueryResultTextExportResponse { content: format_markdown(&req.data()) })
}