use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
use crate::db::types::ColumnSchema;
const NAME_W: usize = 20;
const BADGE_W: usize = 14;
const TYPE_W: usize = 16;
pub enum EditFieldMode {
Navigate,
Editing,
}
pub enum EditRecordAction {
None,
Back,
Save(String),
SaveMongo { id: String, doc_json: String },
InsertMongo { doc_json: String },
OpenNested(usize), }
pub struct EditRecordScreen {
pub table_name: String,
pub schema: Vec<ColumnSchema>,
pub original_values: Vec<String>,
pub current_values: Vec<String>,
pub selected_field: usize,
pub cursor_pos: usize,
pub mode: EditFieldMode,
pub scroll_offset: usize,
pub status: Option<String>,
pub validation_errors: Vec<Option<String>>,
pub is_nosql: bool,
pub is_array: bool,
pub is_insert: bool,
pub is_nested: bool, pub field_name_input: Option<String>, }
impl EditRecordScreen {
pub fn new(table_name: String, schema: Vec<ColumnSchema>, values: Vec<String>) -> Self {
let n = values.len();
Self {
table_name,
original_values: values.clone(),
current_values: values,
schema,
selected_field: 0,
cursor_pos: 0,
mode: EditFieldMode::Navigate,
scroll_offset: 0,
status: None,
validation_errors: vec![None; n],
is_nosql: false,
is_array: false,
is_insert: false,
is_nested: false,
field_name_input: None,
}
}
pub fn set_error(&mut self, msg: String) {
self.status = Some(format!("Error: {msg}"));
self.mode = EditFieldMode::Navigate;
}
pub fn handle_key(&mut self, key: KeyEvent) -> EditRecordAction {
if self.field_name_input.is_some() {
return self.handle_field_name_input(key);
}
match self.mode {
EditFieldMode::Navigate => self.handle_navigate(key),
EditFieldMode::Editing => self.handle_edit(key),
}
}
fn handle_field_name_input(&mut self, key: KeyEvent) -> EditRecordAction {
match key.code {
KeyCode::Esc => {
self.field_name_input = None;
}
KeyCode::Enter => {
let name = self.field_name_input.take().unwrap_or_default();
let name = name.trim().to_string();
if !name.is_empty() {
let new_idx = self.schema.len();
self.schema.push(ColumnSchema {
name,
type_name: "string".to_string(),
is_pk: false,
is_nullable: true,
fk: None,
});
self.current_values.push(String::new());
self.original_values.push(String::new());
self.validation_errors.push(None);
self.selected_field = new_idx;
self.cursor_pos = 0;
self.mode = EditFieldMode::Editing;
}
}
KeyCode::Backspace => {
if let Some(ref mut s) = self.field_name_input {
s.pop();
}
}
KeyCode::Char(c) => {
if let Some(ref mut s) = self.field_name_input {
s.push(c);
}
}
_ => {}
}
EditRecordAction::None
}
fn handle_navigate(&mut self, key: KeyEvent) -> EditRecordAction {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => EditRecordAction::Back,
KeyCode::Char('j') | KeyCode::Down => {
self.move_field(1);
EditRecordAction::None
}
KeyCode::Char('k') | KeyCode::Up => {
self.move_field(-1);
EditRecordAction::None
}
KeyCode::Enter => {
if let Some(col) = self.schema.get(self.selected_field)
&& !col.is_pk {
if self.is_nosql && (col.type_name == "object" || col.type_name == "array") {
return EditRecordAction::OpenNested(self.selected_field);
}
self.cursor_pos = self.current_values[self.selected_field].chars().count();
self.mode = EditFieldMode::Editing;
self.status = None;
}
EditRecordAction::None
}
KeyCode::Char('i') => {
if let Some(col) = self.schema.get(self.selected_field)
&& !col.is_pk {
self.cursor_pos = self.current_values[self.selected_field].chars().count();
self.mode = EditFieldMode::Editing;
self.status = None;
}
EditRecordAction::None
}
KeyCode::Char('a') if self.is_nosql && !self.is_array => {
self.field_name_input = Some(String::new());
EditRecordAction::None
}
KeyCode::Char('a') if self.is_array => {
let new_idx = self.schema.len();
self.schema.push(ColumnSchema {
name: format!("[{new_idx}]"),
type_name: "string".into(),
is_pk: false,
is_nullable: true,
fk: None,
});
self.current_values.push(String::new());
self.original_values.push(String::new());
self.validation_errors.push(None);
self.selected_field = new_idx;
self.cursor_pos = 0;
self.mode = EditFieldMode::Editing;
EditRecordAction::None
}
KeyCode::Char('D') if self.is_array && !self.schema.is_empty() => {
let idx = self.selected_field;
self.schema.remove(idx);
self.current_values.remove(idx);
self.original_values.remove(idx);
self.validation_errors.remove(idx);
for (i, col) in self.schema.iter_mut().enumerate() {
col.name = format!("[{i}]");
}
if self.selected_field >= self.schema.len() && self.selected_field > 0 {
self.selected_field -= 1;
}
EditRecordAction::None
}
KeyCode::Char(' ') => {
if let Some(col) = self.schema.get(self.selected_field)
&& !col.is_pk && is_bool_type(&col.type_name) {
let val = &mut self.current_values[self.selected_field];
*val = if is_truthy(val) { "false".to_string() } else { "true".to_string() };
}
EditRecordAction::None
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let error_count = self.validation_errors.iter().filter(|e| e.is_some()).count();
if error_count > 0 {
self.status = Some(format!("{error_count} field(s) with invalid format"));
return EditRecordAction::None;
}
if self.is_nosql && self.is_nested {
self.status = Some("Esc: confirm & go back to parent".into());
EditRecordAction::None
} else if self.is_nosql && self.is_insert {
match self.build_mongo_insert() {
Ok(doc_json) => EditRecordAction::InsertMongo { doc_json },
Err(msg) => {
self.status = Some(msg);
EditRecordAction::None
}
}
} else if self.is_nosql {
match self.build_mongo_replace() {
Ok((id, doc_json)) => EditRecordAction::SaveMongo { id, doc_json },
Err(msg) => {
self.status = Some(msg);
EditRecordAction::None
}
}
} else {
let sql = self.build_update_sql();
if sql.starts_with("-- ") {
self.status = Some(sql);
EditRecordAction::None
} else {
EditRecordAction::Save(sql)
}
}
}
_ => EditRecordAction::None,
}
}
fn handle_edit(&mut self, key: KeyEvent) -> EditRecordAction {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
if let Some(col) = self.schema.get(self.selected_field) {
let val = &self.current_values[self.selected_field];
self.validation_errors[self.selected_field] = validate_field(&col.type_name, val);
}
if self.is_array {
let val = self.current_values[self.selected_field].clone();
if let Some(col) = self.schema.get_mut(self.selected_field) {
col.type_name = infer_json_type(&val);
}
}
self.mode = EditFieldMode::Navigate;
}
KeyCode::Left => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
KeyCode::Right => {
let len = self.current_values[self.selected_field].chars().count();
if self.cursor_pos < len {
self.cursor_pos += 1;
}
}
KeyCode::Home => {
self.cursor_pos = 0;
}
KeyCode::End => {
self.cursor_pos = self.current_values[self.selected_field].chars().count();
}
KeyCode::Backspace => {
if self.cursor_pos > 0 {
let val = &mut self.current_values[self.selected_field];
let mut chars: Vec<char> = val.chars().collect();
chars.remove(self.cursor_pos - 1);
*val = chars.into_iter().collect();
self.cursor_pos -= 1;
}
}
KeyCode::Delete => {
let val = &mut self.current_values[self.selected_field];
let len = val.chars().count();
if self.cursor_pos < len {
let mut chars: Vec<char> = val.chars().collect();
chars.remove(self.cursor_pos);
*val = chars.into_iter().collect();
}
}
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
let val = &mut self.current_values[self.selected_field];
let mut chars: Vec<char> = val.chars().collect();
chars.insert(self.cursor_pos, c);
*val = chars.into_iter().collect();
self.cursor_pos += 1;
}
_ => {}
}
EditRecordAction::None
}
fn move_field(&mut self, delta: i64) {
let count = self.schema.len();
if count == 0 { return; }
let next = (self.selected_field as i64 + delta).clamp(0, count as i64 - 1) as usize;
self.selected_field = next;
}
pub fn build_update_sql(&self) -> String {
let Some(pk_idx) = self.schema.iter().position(|c| c.is_pk) else {
return "-- No primary key: cannot generate UPDATE".to_string();
};
let pk_col = &self.schema[pk_idx].name;
let pk_orig = &self.original_values[pk_idx];
let changes: Vec<String> = self.schema.iter()
.zip(self.current_values.iter())
.zip(self.original_values.iter())
.filter(|((col, cur), orig)| !col.is_pk && *cur != *orig)
.map(|((col, cur), _)| {
format!("\"{}\" = {}", col.name, sql_literal(cur, &col.type_name))
})
.collect();
if changes.is_empty() {
return "-- No changes".to_string();
}
format!(
"UPDATE \"{}\" SET {} WHERE \"{}\" = {}",
self.table_name,
changes.join(", "),
pk_col,
sql_literal(pk_orig, &self.schema[pk_idx].type_name)
)
}
pub fn build_mongo_replace(&self) -> Result<(String, String), String> {
let id_idx = self.schema.iter().position(|c| c.is_pk || c.name == "_id")
.ok_or_else(|| "-- No _id field: cannot replace document".to_string())?;
let id = self.original_values[id_idx].clone();
let no_changes = self.schema.iter().zip(self.current_values.iter()).zip(self.original_values.iter())
.filter(|((col, _), _)| !col.is_pk && col.name != "_id")
.all(|((_, cur), orig)| cur == orig);
if no_changes {
return Err("-- No changes".to_string());
}
let mut map = serde_json::Map::new();
for (i, col) in self.schema.iter().enumerate() {
if col.is_pk || col.name == "_id" { continue; }
let json_val = mongo_field_to_json(&self.current_values[i], &col.type_name);
map.insert(col.name.clone(), json_val);
}
let doc_json = serde_json::Value::Object(map).to_string();
Ok((id, doc_json))
}
pub fn build_mongo_insert(&self) -> Result<String, String> {
let mut map = serde_json::Map::new();
for (col, val) in self.schema.iter().zip(self.current_values.iter()) {
if val.is_empty() { continue; }
map.insert(col.name.clone(), mongo_field_to_json(val, &col.type_name));
}
if map.is_empty() {
return Err("-- Document is empty".to_string());
}
serde_json::to_string(&map).map_err(|e| e.to_string())
}
pub fn reconstruct_nested_json(&self) -> String {
let mut map = serde_json::Map::new();
for (col, val) in self.schema.iter().zip(self.current_values.iter()) {
map.insert(col.name.clone(), mongo_field_to_json(val, &col.type_name));
}
serde_json::Value::Object(map).to_string()
}
pub fn reconstruct_nested_array(&self) -> String {
let arr: Vec<serde_json::Value> = self.schema.iter()
.zip(self.current_values.iter())
.map(|(col, val)| mongo_field_to_json(val, &col.type_name))
.collect();
serde_json::Value::Array(arr).to_string()
}
pub fn draw(f: &mut Frame<'_>, screen: &mut EditRecordScreen, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0), Constraint::Length(4), Constraint::Length(3), ])
.split(area);
let visible_rows = (chunks[0].height as usize).saturating_sub(2);
if screen.selected_field < screen.scroll_offset {
screen.scroll_offset = screen.selected_field;
} else if screen.selected_field >= screen.scroll_offset + visible_rows && visible_rows > 0 {
screen.scroll_offset = screen.selected_field - visible_rows + 1;
}
let val_w = (chunks[0].width as usize).saturating_sub(2 + NAME_W + BADGE_W + TYPE_W);
let mut lines: Vec<Line> = vec![];
let end = (screen.scroll_offset + visible_rows).min(screen.schema.len());
for i in screen.scroll_offset..end {
let col = &screen.schema[i];
let is_sel = i == screen.selected_field;
let is_editing = is_sel && matches!(screen.mode, EditFieldMode::Editing);
let cur_val = &screen.current_values[i];
let orig_val = &screen.original_values[i];
let changed = cur_val != orig_val && !col.is_pk;
let has_error = screen.validation_errors.get(i).and_then(|e| e.as_ref()).is_some();
let sel_str = if is_sel { "> " } else { " " };
let name_str = format!("{:<width$}", col.name, width = NAME_W - 2);
let (badge_str, badge_style) = if col.is_pk {
(
format!("{:<BADGE_W$}", "[PK]"),
Style::default().fg(Color::Cyan),
)
} else if let Some(ref fk) = col.fk {
let max_t = BADGE_W.saturating_sub(4);
let t = if fk.table.len() > max_t { &fk.table[..max_t] } else { &fk.table };
(
format!("[→{:<width$}]", t, width = BADGE_W - 3),
Style::default().fg(Color::Magenta),
)
} else if col.type_name == "object" {
(format!("{:<BADGE_W$}", "[obj]"), Style::default().fg(Color::Green))
} else if col.type_name == "array" {
(format!("{:<BADGE_W$}", "[arr]"), Style::default().fg(Color::Green))
} else {
(
format!("{:<BADGE_W$}", ""),
Style::default(),
)
};
let display_val = if cur_val.chars().count() > val_w && val_w > 0 {
let s: String = cur_val.chars().take(val_w.saturating_sub(1)).collect();
format!("{s}…")
} else {
cur_val.clone()
};
let name_style = if col.is_pk {
Style::default().fg(Color::Gray)
} else if is_sel {
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
} else if changed {
Style::default().fg(Color::Green)
} else {
Style::default()
};
let val_style = if col.is_pk {
Style::default().fg(Color::Gray)
} else if has_error && !is_editing {
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
} else if is_editing {
Style::default().fg(Color::White).bg(Color::DarkGray)
} else if is_sel {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else if cur_val == "NULL" {
Style::default().fg(Color::Gray)
} else if changed {
Style::default().fg(Color::Green)
} else {
Style::default()
};
let raw_type = &col.type_name;
let type_str = format!(
"{:<TYPE_W$}",
if raw_type.chars().count() > TYPE_W {
raw_type.chars().take(TYPE_W - 1).collect::<String>() + "…"
} else {
raw_type.clone()
}
);
let type_style = Style::default().fg(Color::Blue);
lines.push(Line::from(vec![
Span::styled(format!("{sel_str}{name_str}"), name_style),
Span::styled(badge_str, badge_style),
Span::styled(type_str, type_style),
Span::styled(display_val, val_style),
]));
}
let title_suffix = screen.status.as_deref()
.map(|s| format!(" — {s}"))
.unwrap_or_default();
let title = format!(" Edit: {}{} ", screen.table_name, title_suffix);
f.render_widget(
Paragraph::new(Text::from(lines))
.block(
Block::default()
.borders(Borders::ALL)
.title(title)
.title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
),
chunks[0],
);
if matches!(screen.mode, EditFieldMode::Editing) {
let field_row = screen.selected_field.saturating_sub(screen.scroll_offset);
let visible_cursor = screen.cursor_pos.min(val_w);
let cursor_x = chunks[0].x + 1 + (NAME_W + BADGE_W + TYPE_W) as u16 + visible_cursor as u16;
let cursor_y = chunks[0].y + 1 + field_row as u16;
if cursor_y < chunks[0].y + chunks[0].height.saturating_sub(1) {
f.set_cursor(cursor_x, cursor_y);
}
}
let (preview_text, preview_title) = if screen.is_array {
(screen.reconstruct_nested_array(), " Array Preview ")
} else if screen.is_nosql && screen.is_nested {
(screen.reconstruct_nested_json(), " Object Preview ")
} else if screen.is_nosql && screen.is_insert {
let content = match screen.build_mongo_insert() {
Ok(doc) => doc,
Err(msg) => msg,
};
(content, " New Document Preview ")
} else if screen.is_nosql {
let content = match screen.build_mongo_replace() {
Ok((_, doc)) => doc,
Err(msg) => msg,
};
(content, " Document Preview ")
} else {
(screen.build_update_sql(), " SQL Preview ")
};
let preview_style = if preview_text.starts_with("-- ") {
Style::default().fg(Color::Gray)
} else {
Style::default().fg(Color::Green)
};
f.render_widget(
Paragraph::new(preview_text.as_str())
.block(Block::default().borders(Borders::ALL).title(preview_title))
.style(preview_style)
.wrap(Wrap { trim: false }),
chunks[1],
);
let (help_text, help_style) = if let Some(ref name) = screen.field_name_input {
(
format!(" New field name: {name}_ Enter: confirm Esc: cancel"),
Style::default().fg(Color::Yellow),
)
} else if matches!(screen.mode, EditFieldMode::Editing) {
let hint = screen.schema.get(screen.selected_field)
.and_then(|col| format_hint(&col.type_name))
.map(|h| format!(" Format: {h} ← →: cursor Backspace/Del Enter/Esc: done"))
.unwrap_or_else(|| " ← →: cursor Backspace/Del: delete Enter/Esc: done".into());
(hint, Style::default().fg(Color::Cyan))
} else if let Some(err) = screen.schema.get(screen.selected_field)
.and_then(|_| screen.validation_errors.get(screen.selected_field))
.and_then(|e| e.as_ref())
{
(format!(" ✗ {err}"), Style::default().fg(Color::Red))
} else if screen.is_array {
(
" j/k: item Enter: edit a: add D: delete Esc: confirm & back".into(),
Style::default().fg(Color::Gray),
)
} else {
let on_obj = screen.schema.get(screen.selected_field)
.map(|c| screen.is_nosql && c.type_name == "object")
.unwrap_or(false);
let add_hint = if screen.is_nosql && !screen.is_array { " a: add field" } else { "" };
let hint = if on_obj {
format!(" j/k: field Enter: drill-in i: edit JSON Ctrl+S: save{add_hint} Esc: back")
} else {
format!(" j/k: field Enter/i: edit Space: toggle bool Ctrl+S: save{add_hint} Esc: back")
};
(hint, Style::default().fg(Color::Gray))
};
f.render_widget(
Paragraph::new(help_text.as_str())
.block(Block::default().borders(Borders::ALL))
.style(help_style),
chunks[2],
);
}
}
fn validate_field(type_name: &str, value: &str) -> Option<String> {
if value.is_empty() || value == "NULL" { return None; }
let tn = type_name.to_uppercase();
if (tn.contains("DATE") && !tn.contains("DATETIME") && !tn.contains("TIMESTAMP"))
|| tn == "DATE"
{
if NaiveDate::parse_from_str(value, "%Y-%m-%d").is_err() {
return Some("expected YYYY-MM-DD".into());
}
} else if tn == "TIME" {
if NaiveTime::parse_from_str(value, "%H:%M:%S").is_err()
&& NaiveTime::parse_from_str(value, "%H:%M").is_err()
{
return Some("expected HH:MM:SS".into());
}
} else if tn.contains("TIMESTAMP") || tn.contains("DATETIME") {
let ok = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S").is_ok()
|| NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S").is_ok()
|| chrono::DateTime::parse_from_rfc3339(value).is_ok();
if !ok {
return Some("expected YYYY-MM-DD HH:MM:SS".into());
}
} else if tn.contains("UUID") {
if !is_valid_uuid(value) {
return Some("expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".into());
}
} else if tn.contains("JSON") || tn == "OBJECT" || tn == "ARRAY" {
if serde_json::from_str::<serde_json::Value>(value).is_err() {
return Some("invalid JSON".into());
}
} else if (tn.contains("INT") || tn == "BIGINT" || tn == "SMALLINT")
&& !tn.contains("INTERVAL")
{
if value.parse::<i64>().is_err() {
return Some("expected integer".into());
}
} else if tn.contains("FLOAT") || tn.contains("REAL") || tn.contains("DOUBLE")
|| tn.contains("NUMERIC") || tn.contains("DECIMAL")
{
if value.parse::<f64>().is_err() {
return Some("expected number".into());
}
} else if tn.contains("INET") || tn.contains("CIDR") {
let host = value.split('/').next().unwrap_or(value);
if host.parse::<std::net::IpAddr>().is_err() {
return Some("expected IP address".into());
}
}
None
}
fn format_hint(type_name: &str) -> Option<&'static str> {
let tn = type_name.to_uppercase();
if (tn.contains("DATE") && !tn.contains("DATETIME") && !tn.contains("TIMESTAMP"))
|| tn == "DATE"
{
Some("YYYY-MM-DD")
} else if tn == "TIME" {
Some("HH:MM:SS")
} else if tn.contains("TIMESTAMP") || tn.contains("DATETIME") {
Some("YYYY-MM-DD HH:MM:SS")
} else if tn.contains("UUID") {
Some("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
} else if tn.contains("JSON") || tn == "OBJECT" {
Some("valid JSON object { }")
} else if tn == "ARRAY" {
Some("valid JSON array [ ]")
} else if tn.contains("INET") || tn.contains("CIDR") {
Some("IP address e.g. 192.168.1.1")
} else {
None
}
}
fn is_valid_uuid(s: &str) -> bool {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 5 { return false; }
let lens = [8, 4, 4, 4, 12];
parts.iter().zip(lens.iter()).all(|(p, &l)| {
p.len() == l && p.chars().all(|c| c.is_ascii_hexdigit())
})
}
fn is_bool_type(type_name: &str) -> bool {
let tn = type_name.to_uppercase();
tn.contains("BOOL") || tn == "TINYINT(1)"
}
fn is_truthy(val: &str) -> bool {
matches!(val.to_lowercase().as_str(), "true" | "t" | "1" | "yes" | "on")
}
fn infer_json_type(val: &str) -> String {
if val.is_empty() || val == "NULL" {
return "string".to_string();
}
if val.eq_ignore_ascii_case("true") || val.eq_ignore_ascii_case("false") {
return "bool".to_string();
}
if val.parse::<i64>().is_ok() {
return "int".to_string();
}
if val.parse::<f64>().is_ok() {
return "float".to_string();
}
"string".to_string()
}
fn mongo_field_to_json(val: &str, type_name: &str) -> serde_json::Value {
if val == "NULL" {
return serde_json::Value::Null;
}
match type_name {
"object" | "array" => {
serde_json::from_str(val).unwrap_or(serde_json::Value::String(val.to_string()))
}
"int" => val.parse::<i64>()
.ok()
.map(serde_json::Value::from)
.unwrap_or(serde_json::Value::String(val.to_string())),
"float" => val.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::String(val.to_string())),
"bool" => serde_json::Value::Bool(is_truthy(val)),
_ => serde_json::Value::String(val.to_string()),
}
}
fn sql_literal(val: &str, type_name: &str) -> String {
if val == "NULL" {
return "NULL".to_string();
}
let tn = type_name.to_uppercase();
if is_bool_type(type_name) {
return if is_truthy(val) { "TRUE".to_string() } else { "FALSE".to_string() };
}
if (tn.ends_with("[]") || tn.ends_with(" ARRAY"))
&& let Ok(arr) = serde_json::from_str::<serde_json::Value>(val)
&& let Some(items) = arr.as_array() {
let parts: Vec<String> = items.iter().map(|v| match v {
serde_json::Value::Null => "NULL".to_string(),
serde_json::Value::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => format!("'{}'", s.replace('\'', "''")),
other => format!("'{}'", other.to_string().replace('\'', "''")),
}).collect();
return format!("[{}]", parts.join(", "));
}
let is_num = tn.contains("INT") || tn.contains("FLOAT") || tn.contains("REAL")
|| tn.contains("NUMERIC") || tn.contains("DECIMAL") || tn.contains("DOUBLE")
|| tn.contains("NUMBER");
if is_num && val.parse::<f64>().is_ok() {
val.to_string()
} else {
format!("'{}'", val.replace('\'', "''"))
}
}