#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Int(i64),
Float(f64),
Decimal(String),
Text(String),
Bool(bool),
Bytes(Vec<u8>),
Json(serde_json::Value),
}
#[derive(Debug, Clone)]
pub struct Column {
pub name: String,
}
#[derive(Debug, Clone)]
pub struct Row {
pub values: Vec<Value>,
}
#[derive(Debug, Clone)]
pub struct TablePage {
pub columns: Vec<Column>,
pub rows: Vec<Row>,
pub rowids: Option<Vec<i64>>,
pub offset: usize,
pub has_more: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SchemaColumn {
pub name: String,
pub col_type: String,
pub notnull: bool,
pub pk: bool,
}
#[derive(Debug, Clone)]
pub enum QueryOutcome {
Rows {
columns: Vec<Column>,
rows: Vec<Row>,
truncated: bool,
next_token: Option<String>,
read_only: bool,
},
Affected(u64),
Executed,
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Null => write!(f, ""),
Value::Int(i) => write!(f, "{i}"),
Value::Float(x) => write!(f, "{x}"),
Value::Decimal(value) => write!(f, "{value}"),
Value::Text(s) => write!(f, "{s}"),
Value::Bool(value) => write!(f, "{value}"),
Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
Value::Json(value) => write!(f, "{value}"),
}
}
}
impl Value {
pub fn type_name(&self) -> &'static str {
match self {
Value::Null => "null",
Value::Int(_) => "integer",
Value::Float(_) => "real",
Value::Decimal(_) => "number",
Value::Text(_) => "text",
Value::Bool(_) => "boolean",
Value::Bytes(_) => "binary",
Value::Json(_) => "json",
}
}
pub fn detail_text(&self) -> String {
match self {
Value::Null => "NULL".to_string(),
Value::Bytes(value) => {
let hex = value
.iter()
.take(32)
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
if value.len() > 32 {
format!("0x{hex}... ({} bytes)", value.len())
} else {
format!("0x{hex} ({} bytes)", value.len())
}
}
Value::Json(value) => {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
value => value.to_string(),
}
}
pub fn edited(&self, text: &str) -> Result<Self, String> {
match self {
Value::Null if text.is_empty() => Ok(Value::Null),
Value::Null | Value::Text(_) => Ok(Value::Text(text.to_string())),
Value::Int(_) => text
.parse()
.map(Value::Int)
.map_err(|_| "expected an integer".to_string()),
Value::Float(_) => text
.parse()
.map(Value::Float)
.map_err(|_| "expected a number".to_string()),
Value::Decimal(_) => text
.parse::<serde_json::Number>()
.map(|_| Value::Decimal(text.to_string()))
.map_err(|_| "expected a number".to_string()),
Value::Bool(_) => text
.parse()
.map(Value::Bool)
.map_err(|_| "expected true or false".to_string()),
Value::Bytes(_) => Err("binary values cannot be edited in the TUI".to_string()),
Value::Json(_) => serde_json::from_str(text)
.map(Value::Json)
.map_err(|error| format!("invalid JSON: {error}")),
}
}
pub fn to_json(&self) -> serde_json::Value {
match self {
Value::Null => serde_json::Value::Null,
Value::Int(value) => (*value).into(),
Value::Float(value) => serde_json::Number::from_f64(*value)
.map(serde_json::Value::Number)
.unwrap_or_else(|| serde_json::Value::String(value.to_string())),
Value::Decimal(value) => value
.parse::<serde_json::Number>()
.map(serde_json::Value::Number)
.unwrap_or_else(|_| serde_json::Value::String(value.clone())),
Value::Text(value) => value.clone().into(),
Value::Bool(value) => (*value).into(),
Value::Bytes(value) => serde_json::json!({
"type": "bytes",
"hex": value.iter().map(|byte| format!("{byte:02x}")).collect::<String>(),
}),
Value::Json(value) => value.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn displays_int_as_plain_number() {
assert_eq!(Value::Int(42).to_string(), "42");
}
#[test]
fn displays_null_as_empty_string() {
assert_eq!(Value::Null.to_string(), "");
}
#[test]
fn displays_text_unquoted() {
assert_eq!(Value::Text("hello".to_string()).to_string(), "hello");
}
#[test]
fn displays_bytes_with_length() {
assert_eq!(Value::Bytes(vec![1, 2, 3]).to_string(), "<3 bytes>");
}
#[test]
fn edits_preserve_numeric_and_null_types() {
assert_eq!(Value::Int(1).edited("42"), Ok(Value::Int(42)));
assert_eq!(Value::Null.edited(""), Ok(Value::Null));
assert_eq!(Value::Null.edited("hello"), Ok(Value::Text("hello".into())));
assert!(Value::Float(1.0).edited("hello").is_err());
}
}