use crate::shell::Shell;
use inillucent_driver::vfs::Vfs;
use inillucent_value::Value;
pub fn sha3sum(shell: &mut Shell, arguments: &[&str]) {
let mut width = 224u32;
let mut schema = false;
let mut like: Option<String> = None;
let mut separate = false;
for argument in arguments {
let trimmed = argument.trim_start_matches('-');
if argument.starts_with('-') {
match trimmed {
"schema" => schema = true,
"sha3-224" | "sha3-256" | "sha3-384" | "sha3-512" => {
width = trimmed.get(5..).and_then(|n| n.parse().ok()).unwrap_or(224);
}
other => {
shell.complain(&format!("Unknown option \"-{other}\" on \"sha3sum\""));
return;
}
}
continue;
}
if like.is_some() {
shell.complain("Usage: .sha3sum ?OPTIONS? ?LIKE-PATTERN?");
return;
}
like = Some((*argument).to_string());
separate = true;
}
let tables = match hashable_tables(shell, schema) {
Some(names) => names,
None => return,
};
let mut whole = inillucent_base::sha3::Sha3::new(width);
let mut lines: Vec<String> = Vec::new();
for table in tables {
if let Some(pattern) = &like {
if !like_matches(shell, pattern, &table) {
continue;
}
}
let query = content_query(&table);
if separate {
let mut one = inillucent_base::sha3::Sha3::new(width);
if !hash_query(shell, &query, &mut one) {
return;
}
lines.push(format!("{}|{table}", hex(&one.finish())));
} else if !hash_query(shell, &query, &mut whole) {
return;
}
}
if separate {
for line in lines {
shell.say(&line);
}
return;
}
let digest = hex(&whole.finish());
shell.say(&digest);
}
fn hashable_tables(shell: &mut Shell, schema: bool) -> Option<Vec<String>> {
let sql = if schema {
"SELECT lower(name) FROM sqlite_schema WHERE type='table' \
UNION ALL SELECT 'sqlite_schema' ORDER BY 1"
} else {
"SELECT lower(name) FROM sqlite_schema WHERE type='table' \
AND name NOT LIKE 'sqlite__%' ESCAPE '_' ORDER BY 1"
};
match shell.collect(sql) {
Ok((_, rows)) => Some(rows.iter().map(|row| text_of(row.first())).collect()),
Err(failure) => {
shell.complain(&format!("Error: {}", failure.message));
None
}
}
}
fn content_query(table: &str) -> String {
match table {
"sqlite_schema" => {
"SELECT type,name,tbl_name,sql FROM sqlite_schema ORDER BY name;".to_string()
}
"sqlite_sequence" => "SELECT name,seq FROM sqlite_sequence ORDER BY name;".to_string(),
"sqlite_stat1" => "SELECT tbl,idx,stat FROM sqlite_stat1 ORDER BY tbl,idx;".to_string(),
other => format!(
"SELECT * FROM \"{}\" NOT INDEXED;",
other.replace('"', "\"\"")
),
}
}
fn hash_query(shell: &mut Shell, sql: &str, sponge: &mut inillucent_base::sha3::Sha3) -> bool {
sponge.update(format!("S{}:", sql.len()).as_bytes());
sponge.update(sql.as_bytes());
let rows = match shell.collect(sql) {
Ok((_, rows)) => rows,
Err(failure) => {
shell.complain(&format!("Error: {}", failure.message));
return false;
}
};
for row in rows {
sponge.update(b"R");
for value in &row {
sponge.update(&encoded(value));
}
}
true
}
fn encoded(value: &Value<'static>) -> Vec<u8> {
match value {
Value::Null => b"N".to_vec(),
Value::Integer(number) => {
let mut out = b"I".to_vec();
out.extend_from_slice(&number.to_be_bytes());
out
}
Value::Real(number) => {
let mut out = b"F".to_vec();
out.extend_from_slice(&number.to_bits().to_be_bytes());
out
}
Value::Text(text) => {
let bytes = text.utf8_bytes();
let mut out = format!("T{}:", bytes.len()).into_bytes();
out.extend_from_slice(&bytes);
out
}
Value::Blob(blob) => {
let bytes = blob.raw();
let mut out = format!("B{}:", bytes.len()).into_bytes();
out.extend_from_slice(bytes);
out
}
}
}
fn like_matches(shell: &mut Shell, pattern: &str, name: &str) -> bool {
let sql = format!(
"SELECT '{}' LIKE '{}'",
name.replace('\'', "''"),
pattern.replace('\'', "''")
);
matches!(
shell
.collect(&sql)
.ok()
.and_then(|(_, rows)| rows.first().and_then(|row| row.first()).cloned()),
Some(Value::Integer(1))
)
}
pub fn limit(shell: &mut Shell, arguments: &[&str]) {
use inillucent_base::limits::Limit;
let register: [(&str, Limit); 13] = [
("length", Limit::Length),
("sql_length", Limit::SqlLength),
("column", Limit::Column),
("expr_depth", Limit::ExprDepth),
("parser_depth", Limit::ParserDepth),
("compound_select", Limit::CompoundSelect),
("vdbe_op", Limit::VdbeOp),
("function_arg", Limit::FunctionArg),
("attached", Limit::Attached),
("like_pattern_length", Limit::LikePatternLength),
("variable_number", Limit::VariableNumber),
("trigger_depth", Limit::TriggerDepth),
("worker_threads", Limit::WorkerThreads),
];
let Some(wanted) = arguments.first() else {
for (name, limit) in register {
let value = shell.limit(limit);
shell.say(&format!("{name:>20} {value}"));
}
return;
};
let matched: Vec<&(&str, Limit)> = register
.iter()
.filter(|(name, _)| name.starts_with(&wanted.to_ascii_lowercase()))
.collect();
match matched.as_slice() {
[] => shell.complain(&format!(
"unknown limit: \"{wanted}\"\nenter \".limits\" with no arguments for a list."
)),
[(name, limit)] => {
let (name, limit) = (*name, *limit);
if let Some(value) = arguments.get(1) {
match value.parse::<i64>() {
Ok(requested) => {
shell.set_limit(limit, requested);
}
Err(_) => {
shell.complain(&format!("not a number: \"{value}\""));
return;
}
}
}
let now = shell.limit(limit);
shell.say(&format!("{name:>20} {now}"));
}
_ => shell.complain(&format!("ambiguous limit: \"{wanted}\"")),
}
}
pub fn selftest(shell: &mut Shell, arguments: &[&str]) {
if arguments.contains(&"--init") {
shell.complain("Error: .selftest --init is not implemented by this engine");
return;
}
let has_table = shell
.collect("SELECT count(*) FROM sqlite_schema WHERE type='table' AND lower(name)='selftest'")
.ok()
.and_then(|(_, rows)| rows.first().and_then(|row| row.first()).cloned())
.map(|value| matches!(value, Value::Integer(count) if count > 0))
.unwrap_or(false);
let checks: Vec<(i64, String, String, String)> = if has_table {
match shell.collect("SELECT tno,op,cmd,ans FROM selftest ORDER BY tno") {
Ok((_, rows)) => rows
.iter()
.map(|row| {
(
match row.first() {
Some(Value::Integer(number)) => *number,
_ => 0,
},
text_of(row.get(1)),
text_of(row.get(2)),
text_of(row.get(3)),
)
})
.collect(),
Err(failure) => {
shell.complain(&format!("Error: {}", failure.message));
return;
}
}
} else {
vec![
(
0,
"memo".to_string(),
"Missing SELFTEST table - default checks only".to_string(),
String::new(),
),
(
1,
"run".to_string(),
"PRAGMA integrity_check".to_string(),
"ok".to_string(),
),
]
};
let mut errors = 0usize;
let mut tests = 0usize;
for (tno, operation, command, expected) in checks {
match operation.as_str() {
"memo" => shell.say(&command),
"run" => {
tests = tests.saturating_add(1);
match shell.collect(&command) {
Ok((_, rows)) => {
let got = rows
.iter()
.flat_map(|row| row.iter().map(|value| text_of(Some(value))))
.collect::<Vec<_>>()
.join(" ");
if got != expected {
errors = errors.saturating_add(1);
shell.say(&format!("{tno}: Expected: [{expected}]"));
shell.say(&format!("{tno}: Got: [{got}]"));
}
}
Err(failure) => {
errors = errors.saturating_add(1);
shell.say(&format!("{tno}: error-code-1: {}", failure.message));
}
}
}
other => {
shell.complain(&format!(
"Unknown operation \"{other}\" on selftest line {tno}"
));
break;
}
}
}
shell.say(&format!("{errors} errors out of {tests} tests"));
}
pub fn lint(shell: &mut Shell, arguments: &[&str]) {
let Some(which) = arguments.first() else {
shell.complain("Usage: .lint fkey-indexes");
return;
};
if !"fkey-indexes".starts_with(which) {
shell.complain(&format!("Error: unknown lint: \"{which}\""));
return;
}
let Ok((_, tables)) = shell.collect(
"SELECT name FROM sqlite_schema WHERE type='table' \
AND name NOT LIKE 'sqlite__%' ESCAPE '_' ORDER BY name",
) else {
shell.complain("Error: could not read the schema");
return;
};
let names: Vec<String> = tables.iter().map(|row| text_of(row.first())).collect();
for child in names {
let quoted = child.replace('\'', "''");
let Ok((_, keys)) = shell.collect(&format!(
"SELECT id, seq, \"table\", \"from\", \"to\" FROM pragma_foreign_key_list('{quoted}') \
ORDER BY id, seq"
)) else {
continue;
};
let mut grouped: Vec<(i64, String, Vec<String>)> = Vec::new();
for row in &keys {
let id = match row.first() {
Some(Value::Integer(number)) => *number,
_ => 0,
};
let parent = text_of(row.get(2));
let column = text_of(row.get(3));
match grouped.last_mut() {
Some((held, _, columns)) if *held == id => columns.push(column),
_ => grouped.push((id, parent, vec![column])),
}
}
for (_, parent, columns) in grouped {
if child_is_indexed(shell, &child, &columns) {
continue;
}
let parent_key = parent_key_columns(shell, &parent);
shell.say(&format!(
"CREATE INDEX '{}_{}' ON '{}'({}); --> {}({})",
child,
columns.join("_"),
child,
columns
.iter()
.map(|column| format!("'{column}'"))
.collect::<Vec<_>>()
.join(", "),
parent,
parent_key
));
}
}
}
fn child_is_indexed(shell: &mut Shell, table: &str, columns: &[String]) -> bool {
let quoted = table.replace('\'', "''");
let Ok((_, indexes)) = shell.collect(&format!(
"SELECT name FROM pragma_index_list('{quoted}') ORDER BY seq"
)) else {
return false;
};
for row in &indexes {
let index = text_of(row.first()).replace('\'', "''");
let Ok((_, entries)) = shell.collect(&format!(
"SELECT name FROM pragma_index_info('{index}') ORDER BY seqno"
)) else {
continue;
};
let held: Vec<String> = entries.iter().map(|row| text_of(row.first())).collect();
if held.len() >= columns.len()
&& held
.iter()
.zip(columns.iter())
.all(|(left, right)| left.eq_ignore_ascii_case(right))
{
return true;
}
}
false
}
fn parent_key_columns(shell: &mut Shell, parent: &str) -> String {
let quoted = parent.replace('\'', "''");
let Ok((_, columns)) = shell.collect(&format!(
"SELECT name FROM pragma_table_info('{quoted}') WHERE pk > 0 ORDER BY pk"
)) else {
return String::new();
};
columns
.iter()
.map(|row| text_of(row.first()))
.collect::<Vec<_>>()
.join(", ")
}
fn text_of(value: Option<&Value<'static>>) -> String {
match value {
Some(Value::Text(text)) => String::from_utf8_lossy(&text.utf8_bytes()).into_owned(),
Some(Value::Integer(number)) => number.to_string(),
Some(Value::Real(number)) => crate::render::literal(&Value::Real(*number)),
Some(Value::Blob(blob)) => String::from_utf8_lossy(blob.raw()).into_owned(),
Some(Value::Null) | None => String::new(),
}
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
pub fn recover(shell: &mut Shell, arguments: &[&str]) {
for argument in arguments {
if argument.starts_with('-') {
shell.complain(&format!(
"Error: unknown option \"{argument}\" on \".recover\""
));
return;
}
}
shell.say(".dbconfig defensive off");
shell.say("BEGIN;");
shell.say("PRAGMA writable_schema = on;");
shell.say("PRAGMA foreign_keys = off;");
for (name, pragma) in [
("encoding", "PRAGMA encoding"),
("page_size", "PRAGMA page_size"),
("auto_vacuum", "PRAGMA auto_vacuum"),
("user_version", "PRAGMA user_version"),
("application_id", "PRAGMA application_id"),
] {
let value = shell.scalar(pragma).unwrap_or_default();
shell.say(&format!("PRAGMA {name} = '{value}';"));
}
let Ok((_, objects)) = shell.collect(
"SELECT type, name, sql FROM sqlite_schema \
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite__%' ESCAPE '_' \
ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 ELSE 2 END, rowid",
) else {
shell.complain("Error: could not read the schema");
return;
};
let described: Vec<(String, String, String)> = objects
.iter()
.map(|row| {
(
text_of(row.first()),
text_of(row.get(1)),
text_of(row.get(2)),
)
})
.collect();
for (kind, name, sql) in &described {
shell.say(&format!("{sql};"));
if kind != "table" {
continue;
}
recover_rows(shell, name);
}
shell.say("PRAGMA writable_schema = off;");
shell.say("COMMIT;");
}
fn recover_rows(shell: &mut Shell, table: &str) {
let quoted = table.replace('\'', "''");
let Ok((columns, rows)) =
shell.collect(&format!("SELECT * FROM \"{}\"", table.replace('"', "\"\"")))
else {
return;
};
if rows.is_empty() {
return;
}
let names = columns
.iter()
.map(|column| format!("'{}'", column.replace('\'', "''")))
.collect::<Vec<_>>()
.join(", ");
for row in rows {
let values = row
.iter()
.map(crate::render::literal)
.collect::<Vec<_>>()
.join(", ");
shell.say(&format!(
"INSERT OR IGNORE INTO '{quoted}'({names}) VALUES ({values});"
));
}
}
pub fn stats(shell: &mut Shell, arguments: &[&str]) {
match arguments.first().copied() {
None => {
let lines = statistics(shell);
for line in lines {
shell.say(&line);
}
}
Some(word) => shell.stats = crate::dot::truthy(Some(word)),
}
}
pub fn statistics(shell: &Shell) -> Vec<String> {
let stats = shell.cache_stats();
let bytes = shell.pool_bytes();
let fetches = stats.hits.saturating_add(stats.misses);
vec![
line("Page cache bytes:", &bytes.to_string()),
line("Page cache fetches:", &fetches.to_string()),
line("Page cache hits:", &stats.hits.to_string()),
line("Page cache misses:", &stats.misses.to_string()),
line("Page cache rewarms:", &stats.rewarms.to_string()),
line("Frames cooled:", &stats.cooled.to_string()),
line("Frames evicted:", &stats.evicted.to_string()),
line("Pages read from the file:", &stats.reads.to_string()),
line("Pages written to the file:", &stats.writes.to_string()),
]
}
fn line(label: &str, value: &str) -> String {
format!("{label:<36} {value}")
}
pub fn vfs_list(shell: &mut Shell) {
let entries = installed();
let last = entries.len().saturating_sub(1);
for (at, entry) in entries.iter().enumerate() {
let current = if at == 0 { " <--- CURRENT" } else { "" };
shell.say(&format!("vfs.zName = \"{}\"{current}", entry.name));
shell.say(&format!("vfs.iVersion = {}", entry.version));
shell.say(&format!("vfs.szOsFile = {}", entry.file_size));
shell.say(&format!("vfs.mxPathname = {}", entry.path_limit));
if at != last {
shell.say("-----------------------------------");
}
}
}
pub fn vfs_info(shell: &mut Shell, name_only: bool) {
let Some(entry) = installed().into_iter().next() else {
return;
};
if name_only {
shell.say(&entry.name);
return;
}
shell.say(&format!("vfs.zName = \"{}\"", entry.name));
shell.say(&format!("vfs.iVersion = {}", entry.version));
shell.say(&format!("vfs.szOsFile = {}", entry.file_size));
shell.say(&format!("vfs.mxPathname = {}", entry.path_limit));
}
struct VfsEntry {
name: String,
version: u32,
file_size: usize,
path_limit: u32,
}
fn installed() -> Vec<VfsEntry> {
vec![
VfsEntry {
name: inillucent_driver::vfs::OsVfs::new().name().to_string(),
version: 3,
file_size: std::mem::size_of::<std::fs::File>(),
path_limit: 32_768,
},
VfsEntry {
name: "memdb".to_string(),
version: 3,
file_size: std::mem::size_of::<Vec<u8>>(),
path_limit: 32_768,
},
]
}
pub fn dbinfo(shell: &mut Shell, arguments: &[&str]) {
const WIDTH: usize = 21;
let _ = arguments;
let schema_size = integer_of(
shell,
"SELECT coalesce(sum(length(sql)), 0) FROM sqlite_schema",
);
let rows: Vec<(&str, String)> = vec![
(
"database page size:",
integer_of(shell, "PRAGMA page_size;").to_string(),
),
("write format:", "1".to_string()),
("read format:", "1".to_string()),
("reserved bytes:", "0".to_string()),
(
"file change counter:",
integer_of(shell, "PRAGMA data_version;").to_string(),
),
(
"database page count:",
integer_of(shell, "PRAGMA page_count;").to_string(),
),
(
"freelist page count:",
integer_of(shell, "PRAGMA freelist_count;").to_string(),
),
(
"schema cookie:",
integer_of(shell, "PRAGMA schema_version;").to_string(),
),
("schema format:", "4".to_string()),
("default cache size:", "0".to_string()),
("autovacuum top root:", "0".to_string()),
(
"incremental vacuum:",
integer_of(shell, "PRAGMA auto_vacuum;").to_string(),
),
("text encoding:", "1 (utf8)".to_string()),
(
"user version:",
integer_of(shell, "PRAGMA user_version;").to_string(),
),
(
"application id:",
integer_of(shell, "PRAGMA application_id;").to_string(),
),
("software version:", "3053004".to_string()),
("number of tables:", counted(shell, "table").to_string()),
("number of indexes:", counted(shell, "index").to_string()),
("number of triggers:", counted(shell, "trigger").to_string()),
("number of views:", counted(shell, "view").to_string()),
("schema size:", schema_size.to_string()),
(
"data version",
integer_of(shell, "PRAGMA data_version;").to_string(),
),
];
for (name, value) in rows {
shell.say(&format!("{name:<WIDTH$}{value}"));
}
}
fn integer_of(shell: &Shell, sql: &str) -> i64 {
shell
.column(sql)
.first()
.and_then(|text| text.parse::<i64>().ok())
.unwrap_or(0)
}
fn counted(shell: &Shell, kind: &str) -> i64 {
integer_of(
shell,
&format!("SELECT count(*) FROM sqlite_schema WHERE type = '{kind}'"),
)
}
pub fn dbtotxt(shell: &mut Shell) {
let path = shell.path().to_string();
let Ok(bytes) = std::fs::read(&path) else {
shell.complain(&format!("Error: cannot read \"{path}\""));
return;
};
let page_size = usize::try_from(integer_of(shell, "PRAGMA page_size;"))
.ok()
.filter(|size| *size > 0)
.unwrap_or_else(|| bytes.len().max(1));
let name = std::path::Path::new(&path)
.file_name()
.map(|held| held.to_string_lossy().into_owned())
.unwrap_or_else(|| path.clone());
shell.say(&format!(
"| size {} pagesize {page_size} filename {name}",
bytes.len()
));
let mut page = 0usize;
while page.saturating_mul(page_size) < bytes.len() {
let start = page.saturating_mul(page_size);
let end = start.saturating_add(page_size).min(bytes.len());
shell.say(&format!("| page {} offset {start}", page.saturating_add(1)));
for line in hex_lines(bytes.get(start..end).unwrap_or(&[])) {
shell.say(&line);
}
page = page.saturating_add(1);
}
shell.say(&format!("| end {name}"));
}
fn hex_lines(page: &[u8]) -> Vec<String> {
const ROW: usize = 16;
const FIRST_PRINTABLE: u8 = 0x20;
const PAST_PRINTABLE: u8 = 0x7f;
let mut lines = Vec::new();
for (index, chunk) in page.chunks(ROW).enumerate() {
if chunk.iter().all(|byte| *byte == 0) {
continue;
}
let hex: Vec<String> = chunk.iter().map(|byte| format!("{byte:02x}")).collect();
let text: String = chunk
.iter()
.map(|byte| {
if (FIRST_PRINTABLE..PAST_PRINTABLE).contains(byte) {
*byte as char
} else {
'.'
}
})
.collect();
lines.push(format!(
"| {:>6}: {} {text}",
index.saturating_mul(ROW),
hex.join(" ")
));
}
lines
}
pub fn intck(shell: &mut Shell, arguments: &[&str]) {
let _ = arguments;
let trees = counted(shell, "table").saturating_add(counted(shell, "index"));
let report = shell.column("PRAGMA integrity_check;");
let errors = report.iter().filter(|line| *line != "ok").count();
let steps = trees.saturating_add(1);
shell.say(&format!("{steps} steps, {errors} errors"));
for line in report.clone().iter().filter(|line| *line != "ok") {
shell.say(line);
}
}
pub fn filectrl(shell: &mut Shell, arguments: &[&str]) {
let Some(name) = arguments.first().map(|word| word.to_ascii_lowercase()) else {
shell.say("Available file-controls:");
for line in [
" .filectrl chunk_size SIZE",
" .filectrl data_version ",
" .filectrl has_moved ",
" .filectrl lock_timeout MILLISEC",
" .filectrl persist_wal [BOOLEAN]",
" .filectrl psow [BOOLEAN]",
" .filectrl reserve_bytes [N]",
" .filectrl size_limit [LIMIT]",
" .filectrl tempfilename ",
] {
shell.say(line);
}
return;
};
match name.as_str() {
"chunk_size" => {}
"lock_timeout" => {
if let Some(value) = arguments.get(1).and_then(|word| word.parse::<i64>().ok()) {
let _ = shell.collect(&format!("PRAGMA busy_timeout = {value};"));
}
}
"data_version" => {
let value = integer_of(shell, "PRAGMA data_version;").to_string();
shell.say(&value);
}
"has_moved" => {
let moved = !std::path::Path::new(shell.path()).exists();
shell.say(&i64::from(moved).to_string());
}
"persist_wal" => {
let value = shell
.column("PRAGMA journal_mode;")
.first()
.map(|mode| i64::from(mode == "persist"))
.unwrap_or(0);
shell.say(&value.to_string());
}
"psow" => shell.say("1"),
"reserve_bytes" => shell.say("0"),
"size_limit" => {
let value = integer_of(shell, "PRAGMA max_page_count;")
.saturating_mul(integer_of(shell, "PRAGMA page_size;"));
shell.say(&value.to_string());
}
"tempfilename" => {
let name = std::env::temp_dir().join(format!(
"inillucent_{:016x}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|held| held.as_nanos())
.unwrap_or(0)
));
shell.say(&name.to_string_lossy());
}
other => {
shell.complain(&format!("Error: unknown file-control: {other}"));
shell.complain("Use \".filectrl --help\" for help");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_content_query_ends_in_a_semicolon() {
for table in ["sqlite_schema", "sqlite_sequence", "sqlite_stat1", "orders"] {
let sql = content_query(table);
assert!(
sql.ends_with(';'),
"`{table}`'s query is hashed as text and the reference's ends in a semicolon; this one is {sql:?}"
);
}
}
#[test]
fn an_ordinary_table_is_read_not_indexed_and_quoted() {
assert_eq!(
content_query("orders"),
"SELECT * FROM \"orders\" NOT INDEXED;"
);
assert_eq!(
content_query("a\"b"),
"SELECT * FROM \"a\"\"b\" NOT INDEXED;",
"a quote in a table name is doubled, or the statement is a different one"
);
}
#[test]
fn the_catalog_tables_name_their_columns_and_their_order() {
assert_eq!(
content_query("sqlite_schema"),
"SELECT type,name,tbl_name,sql FROM sqlite_schema ORDER BY name;"
);
assert_eq!(
content_query("sqlite_sequence"),
"SELECT name,seq FROM sqlite_sequence ORDER BY name;"
);
assert_eq!(
content_query("sqlite_stat1"),
"SELECT tbl,idx,stat FROM sqlite_stat1 ORDER BY tbl,idx;"
);
}
#[test]
fn each_value_kind_encodes_under_its_own_tag() {
assert_eq!(encoded(&Value::Null), b"N".to_vec());
assert_eq!(
encoded(&Value::Integer(1)).first().copied(),
Some(b'I'),
"an integer is tagged, so 1 does not hash as the text \"1\""
);
assert_eq!(
encoded(&Value::Integer(1)).len(),
9,
"the tag and eight bytes"
);
assert_eq!(encoded(&Value::Real(1.0)).len(), 9);
assert_ne!(
encoded(&Value::Integer(1)),
encoded(&Value::Real(1.0)),
"an integer and a real of the same value are different values"
);
}
#[test]
fn a_digest_renders_as_lowercase_hex() {
assert_eq!(hex(&[0x00, 0x0f, 0xa0, 0xff]), "000fa0ff");
assert_eq!(hex(&[]), "");
assert_eq!(
hex(&[0xde, 0xad]).len(),
4,
"two characters a byte, with the leading zero kept"
);
}
#[test]
fn a_statistics_line_pads_the_label() {
let rendered = line("Bytes received by read():", "4096");
assert!(rendered.starts_with("Bytes received by read():"));
assert_eq!(
rendered.find("4096"),
Some(37),
"the value starts one space past a 36-wide label, which is where the reference puts it"
);
}
}