use inillucent_driver::Status;
use crate::json::{self, Json};
#[derive(Debug, Clone)]
pub struct Failed {
pub status: Status,
pub message: String,
pub feature: Option<String>,
pub offset: Option<u32>,
}
impl Failed {
pub fn said(status: Status, message: impl Into<String>) -> Failed {
Failed {
status,
message: message.into(),
feature: None,
offset: None,
}
}
pub fn misuse(message: impl Into<String>) -> Failed {
Failed::said(Status::InvalidState, message)
}
pub fn unsupported(feature: impl Into<String>, message: impl Into<String>) -> Failed {
let feature = feature.into();
Failed {
status: Status::Unsupported,
message: message.into(),
feature: Some(feature),
offset: None,
}
}
pub fn from_engine(error: &inillucent_base::DbError) -> Failed {
let classified = inillucent_driver::Error::from_engine(error, false);
Failed {
status: classified.status,
message: classified.message,
feature: classified.feature,
offset: classified.offset,
}
}
pub fn from_shell(failure: &crate::shell::Failure) -> Failed {
match failure.error.as_ref() {
Some(error) => Failed::from_engine(error),
None => Failed {
status: Status::Syntax,
message: failure.message.clone(),
feature: None,
offset: failure.offset,
},
}
}
pub fn to_json(&self, command: &str) -> Json {
let mut pairs = vec![
("ok", Json::Bool(false)),
("command", json::text(command)),
("status", json::text(self.status.name())),
("message", json::text(&self.message)),
];
if let Some(feature) = &self.feature {
pairs.push(("feature", json::text(feature)));
}
if let Some(offset) = self.offset {
pairs.push(("offset", Json::Int(i64::from(offset))));
}
pairs.push(("text", json::text(self.to_text())));
json::object(pairs)
}
pub fn to_text(&self) -> String {
let mut line = format!("Error [{}]: {}", self.status.name(), self.message);
if let Some(feature) = &self.feature {
line.push_str(&format!("\n not built yet: {feature}"));
}
if let Some(offset) = self.offset {
line.push_str(&format!("\n at byte {offset} of the statement"));
}
line
}
pub fn exit_code(&self) -> i32 {
match self.status {
Status::Unsupported => 3,
_ => 1,
}
}
}
#[derive(Debug, Clone)]
pub struct Column {
pub name: String,
pub kind: String,
}
#[derive(Debug, Clone)]
pub struct Outcome {
pub command: String,
pub columns: Vec<Column>,
pub rows: Vec<Vec<Json>>,
pub total: usize,
pub more: bool,
pub changes: i64,
pub last_insert_rowid: i64,
pub elapsed_ms: f64,
pub text: String,
pub extra: Vec<(String, Json)>,
}
impl Outcome {
pub fn said(command: &str, text: impl Into<String>) -> Outcome {
Outcome {
command: command.to_string(),
columns: Vec::new(),
rows: Vec::new(),
total: 0,
more: false,
changes: 0,
last_insert_rowid: 0,
elapsed_ms: 0.0,
text: text.into(),
extra: Vec::new(),
}
}
pub fn with(mut self, name: &str, value: Json) -> Outcome {
self.extra.push((name.to_string(), value));
self
}
pub fn with_recovery(
mut self,
recovery: &inillucent_driver::Recovery,
strays: &[u64],
) -> Outcome {
if recovery.recovered || recovery.dropped > 0 {
self.extra.push((
"recovered".to_string(),
json::object(vec![
("records_scanned", Json::Int(recovery.scanned as i64)),
("records_applied", Json::Int(recovery.applied as i64)),
("records_dropped", Json::Int(recovery.dropped as i64)),
(
"transactions_committed",
Json::Int(recovery.committed as i64),
),
("transactions_discarded", Json::Int(recovery.losers as i64)),
]),
));
self.text = format!(
"{}{}recovered the log: {} records scanned, {} applied, {} transactions committed, {} discarded.",
self.text,
if self.text.is_empty() { "" } else { "\n" },
recovery.scanned,
recovery.applied,
recovery.committed,
recovery.losers
);
}
if recovery.dropped > 0 {
self.text = format!(
"{}
{} log record(s) were DROPPED: they name a tree this recovery had no shape for. That is usually a table dropped inside the replayed window, and it is how task-1932 and task-2033 both lost rows silently. Run `inillucent integrity-check` and compare the row counts you expect.",
self.text, recovery.dropped
);
}
if !strays.is_empty() {
let named: Vec<Json> = strays
.iter()
.map(|sequence| Json::Int(*sequence as i64))
.collect();
self.extra
.push(("stray_log_segments".to_string(), Json::Array(named)));
let listed = strays
.iter()
.map(|sequence| format!("{sequence:010}"))
.collect::<Vec<String>>()
.join(", ");
self.text = format!(
"{}{}log segments beside this database that its chain does not reach: {listed}. Nothing replays them.",
self.text,
if self.text.is_empty() { "" } else { "\n" }
);
}
self
}
pub fn to_json(&self) -> Json {
let columns = self
.columns
.iter()
.map(|column| {
json::object(vec![
("name", json::text(&column.name)),
("type", json::text(&column.kind)),
])
})
.collect();
let rows = self
.rows
.iter()
.map(|row| Json::Array(row.clone()))
.collect();
let mut pairs = vec![
("ok", Json::Bool(true)),
("command", json::text(&self.command)),
("columns", Json::Array(columns)),
("rows", Json::Array(rows)),
("row_count", Json::Int(self.rows.len() as i64)),
("total", Json::Int(self.total as i64)),
("more", Json::Bool(self.more)),
("changes", Json::Int(self.changes)),
("last_insert_rowid", Json::Int(self.last_insert_rowid)),
("elapsed_ms", Json::Real(self.elapsed_ms)),
];
let mut object = json::object(std::mem::take(&mut pairs));
if let Json::Object(members) = &mut object {
for (name, value) in &self.extra {
members.push((name.clone(), value.clone()));
}
members.push(("text".to_string(), json::text(&self.text)));
}
object
}
}
fn class_of(value: &Json) -> &'static str {
match value {
Json::Null => "null",
Json::Int(_) => "integer",
Json::Real(_) => "real",
Json::Text(_) => "text",
Json::Bool(_) => "integer",
Json::Array(_) | Json::Object(_) => "blob",
}
}
pub fn columns_from(names: &[String], rows: &[Vec<Json>]) -> Vec<Column> {
names
.iter()
.enumerate()
.map(|(nth, name)| {
let mut kind: Option<&'static str> = None;
for row in rows {
let Some(value) = row.get(nth) else { continue };
if matches!(value, Json::Null) {
continue;
}
let seen = class_of(value);
kind = match kind {
None => Some(seen),
Some(previous) if previous == seen => Some(previous),
Some(_) => Some("mixed"),
};
}
Column {
name: name.clone(),
kind: kind.unwrap_or("null").to_string(),
}
})
.collect()
}
pub fn table(columns: &[Column], rows: &[Vec<Json>], null: &str) -> String {
if columns.is_empty() {
return String::new();
}
let cells: Vec<Vec<String>> = rows
.iter()
.map(|row| {
columns
.iter()
.enumerate()
.map(|(nth, _)| match row.get(nth) {
Some(Json::Null) | None => null.to_string(),
Some(Json::Text(text)) => text.clone(),
Some(other) => other.write(),
})
.collect()
})
.collect();
let widths: Vec<usize> = columns
.iter()
.enumerate()
.map(|(nth, column)| {
let widest = cells
.iter()
.filter_map(|row| row.get(nth))
.map(|cell| cell.chars().count())
.max()
.unwrap_or(0);
widest.max(column.name.chars().count())
})
.collect();
let mut lines = Vec::with_capacity(rows.len() + 2);
lines.push(join_padded(
&columns
.iter()
.map(|column| column.name.clone())
.collect::<Vec<_>>(),
&widths,
));
lines.push(
widths
.iter()
.map(|width| "-".repeat(*width))
.collect::<Vec<_>>()
.join(" "),
);
for row in &cells {
lines.push(join_padded(row, &widths));
}
lines.join("\n")
}
fn join_padded(cells: &[String], widths: &[usize]) -> String {
let padded: Vec<String> = cells
.iter()
.enumerate()
.map(|(nth, cell)| {
let width = widths.get(nth).copied().unwrap_or(0);
let short = width.saturating_sub(cell.chars().count());
format!("{cell}{}", " ".repeat(short))
})
.collect();
padded.join(" ").trim_end().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_column_type_is_observed() {
let names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let rows = vec![
vec![Json::Int(1), Json::Null, Json::Null],
vec![json::text("x"), json::text("y"), Json::Null],
];
let columns = columns_from(&names, &rows);
assert_eq!(columns[0].kind, "mixed");
assert_eq!(columns[1].kind, "text");
assert_eq!(columns[2].kind, "null");
}
#[test]
fn the_table_lines_up() {
let columns = columns_from(&["id".to_string(), "name".to_string()], &[]);
let rows = vec![
vec![Json::Int(1), json::text("Ada")],
vec![Json::Int(1000), json::text("B")],
];
let drawn = table(&columns, &rows, "");
let lines: Vec<&str> = drawn.lines().collect();
assert_eq!(lines[0], "id name");
assert_eq!(lines[1], "---- ----");
assert_eq!(lines[2], "1 Ada");
assert_eq!(lines[3], "1000 B");
}
#[test]
fn a_null_is_drawn_as_the_placeholder() {
let columns = columns_from(&["a".to_string()], &[]);
let drawn = table(&columns, &[vec![Json::Null]], "NULL");
assert!(drawn.contains("NULL"));
}
#[test]
fn unsupported_exits_three() {
assert_eq!(Failed::unsupported("vacuum", "not built").exit_code(), 3);
assert_eq!(Failed::misuse("nope").exit_code(), 1);
}
#[test]
fn the_outcome_carries_both_views() {
let outcome = Outcome::said("version", "0.1.0").with("engine", json::text("inillucent"));
let written = outcome.to_json().write();
assert!(written.starts_with("{\"ok\":true,\"command\":\"version\""));
assert!(written.contains("\"engine\":\"inillucent\""));
assert!(written.ends_with("\"text\":\"0.1.0\"}"));
}
}