use core::fmt::{self, Write};
use core::ops::Deref;
extern crate std;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use rusqlite::Connection;
use rusqlite::session::Session;
use std::io::Cursor;
use crate::DynTable;
use crate::PatchSet;
use crate::Reverse;
use crate::differential_testing::run_differential_test;
use crate::parser::ParsedDiffSet;
use crate::schema::SimpleTable;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SqlType {
Integer,
Text,
Real,
Blob,
}
impl fmt::Display for SqlType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Integer => f.write_str("INTEGER"),
Self::Text => f.write_str("TEXT"),
Self::Real => f.write_str("REAL"),
Self::Blob => f.write_str("BLOB"),
}
}
}
impl<'a> arbitrary::Arbitrary<'a> for SqlType {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(*u.choose(&[Self::Integer, Self::Text, Self::Real, Self::Blob])?)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TypedSimpleTable {
table: SimpleTable,
column_types: Vec<SqlType>,
}
impl TypedSimpleTable {
#[must_use]
pub fn new(name: &str, columns: &[(&str, SqlType)], pk_indices: &[usize]) -> Self {
let col_names: Vec<&str> = columns.iter().map(|(n, _)| *n).collect();
let col_types: Vec<SqlType> = columns.iter().map(|(_, t)| *t).collect();
Self {
table: SimpleTable::new(name, &col_names, pk_indices),
column_types: col_types,
}
}
#[must_use]
pub fn from_table_schema(schema: &crate::parser::TableSchema<String>) -> Self {
let ncols = schema.number_of_columns();
let mut pk_flags_buf = vec![0u8; ncols];
schema.write_pk_flags(&mut pk_flags_buf);
let mut pk_cols: Vec<(usize, u8)> = pk_flags_buf
.iter()
.enumerate()
.filter_map(|(i, &ord)| if ord > 0 { Some((i, ord)) } else { None })
.collect();
pk_cols.sort_by_key(|&(_, ord)| ord);
let pk_indices: Vec<usize> = pk_cols.into_iter().map(|(i, _)| i).collect();
let columns: Vec<(String, SqlType)> = (0..ncols)
.map(|i| (format!("c{i}"), SqlType::Blob))
.collect();
let col_refs: Vec<(&str, SqlType)> =
columns.iter().map(|(n, t)| (n.as_str(), *t)).collect();
Self::new(schema.name(), &col_refs, &pk_indices)
}
#[must_use]
pub fn column_types(&self) -> &[SqlType] {
&self.column_types
}
}
impl Deref for TypedSimpleTable {
type Target = SimpleTable;
fn deref(&self) -> &Self::Target {
&self.table
}
}
impl fmt::Display for TypedSimpleTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pk_indices = self.table.pk_indices();
let columns = self.table.column_names();
let single_pk = pk_indices.len() == 1;
write!(f, "CREATE TABLE \"{}\" (", self.table.name())?;
for (i, (col_name, col_type)) in columns.iter().zip(&self.column_types).enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "\"{col_name}\" {col_type}")?;
if single_pk && pk_indices[0] == i {
f.write_str(" PRIMARY KEY")?;
}
}
if !single_pk && !pk_indices.is_empty() {
f.write_str(", PRIMARY KEY(")?;
for (j, &pk_idx) in pk_indices.iter().enumerate() {
if j > 0 {
f.write_str(", ")?;
}
write!(f, "\"{}\"", columns[pk_idx])?;
}
f.write_char(')')?;
}
f.write_char(')')
}
}
impl<'a> arbitrary::Arbitrary<'a> for TypedSimpleTable {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let name_len = u.int_in_range(1..=8)?;
let name: String = (0..name_len)
.map(|_| u.int_in_range(b'a'..=b'z').map(char::from))
.collect::<arbitrary::Result<_>>()?;
Self::arbitrary_with_name(u, &name)
}
}
impl TypedSimpleTable {
fn arbitrary_with_name(
u: &mut arbitrary::Unstructured<'_>,
name: &str,
) -> arbitrary::Result<Self> {
use arbitrary::Arbitrary;
let ncols: usize = u.int_in_range(1..=8)?;
let columns: Vec<(&str, SqlType)> = Vec::new(); let mut col_data: Vec<(String, SqlType)> = Vec::with_capacity(ncols);
for i in 0..ncols {
let ty = SqlType::arbitrary(u)?;
col_data.push((format!("c{i}"), ty));
}
let col_refs: Vec<(&str, SqlType)> =
col_data.iter().map(|(n, t)| (n.as_str(), *t)).collect();
drop(columns);
let npk: usize = u.int_in_range(1..=ncols)?;
let mut available: Vec<usize> = (0..ncols).collect();
let mut pk_indices = Vec::with_capacity(npk);
for _ in 0..npk {
#[allow(clippy::range_minus_one)]
let idx = u.int_in_range(0..=available.len() - 1)?;
pk_indices.push(available.remove(idx));
}
Ok(Self::new(name, &col_refs, &pk_indices))
}
}
#[derive(Debug, Clone)]
pub struct FuzzSchemas(pub Vec<TypedSimpleTable>);
impl Deref for FuzzSchemas {
type Target = [TypedSimpleTable];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> arbitrary::Arbitrary<'a> for FuzzSchemas {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let ntables: usize = u.int_in_range(1..=5)?;
let mut tables = Vec::with_capacity(ntables);
for i in 0..ntables {
let name = format!("t{i}");
tables.push(TypedSimpleTable::arbitrary_with_name(u, &name)?);
}
Ok(Self(tables))
}
}
pub fn test_roundtrip(input: &[u8]) {
let Ok(parsed) = ParsedDiffSet::try_from(input) else {
return; };
let serialized: Vec<u8> = parsed.clone().into();
let reparsed = ParsedDiffSet::try_from(serialized.as_slice())
.expect("Re-parsing our own output should never fail");
assert_eq!(parsed, reparsed, "Roundtrip mismatch");
}
pub fn test_apply_roundtrip(schemas: &[TypedSimpleTable], changeset_bytes: &[u8]) {
let Ok(parsed) = ParsedDiffSet::try_from(changeset_bytes) else {
return;
};
let serialized: Vec<u8> = parsed.clone().into();
let reparsed = ParsedDiffSet::try_from(serialized.as_slice())
.expect("Re-parsing our own output should never fail");
assert_eq!(parsed, reparsed, "Roundtrip mismatch");
let Ok(conn) = Connection::open_in_memory() else {
return;
};
for schema in schemas {
let ddl = schema.to_string();
if conn.execute(&ddl, []).is_err() {
return; }
}
let _ = apply_changeset(&conn, &serialized);
}
pub fn test_reverse_idempotent(input: &[u8]) {
let Ok(parsed) = ParsedDiffSet::try_from(input) else {
return;
};
let ParsedDiffSet::Changeset(changeset) = parsed else {
return;
};
let reversed = changeset.clone().reverse();
let double_reversed = reversed.clone().reverse();
assert_eq!(
changeset, double_reversed,
"Double reverse should equal original"
);
let original_bytes = changeset.build();
let double_reversed_bytes = double_reversed.build();
assert_eq!(
original_bytes, double_reversed_bytes,
"Binary representation should be identical after double reverse"
);
assert_eq!(
changeset.len(),
reversed.len(),
"Reversed changeset should have same number of operations"
);
if changeset.is_empty() {
assert!(
reversed.is_empty(),
"Empty changeset should reverse to empty"
);
}
}
pub fn test_sql_roundtrip(schemas: &[TypedSimpleTable], sql: &str) {
let mut builder: PatchSet<SimpleTable, String, Vec<u8>> = PatchSet::new();
for schema in schemas {
builder.add_table(&**schema);
}
if builder.digest_sql(sql).is_err() {
return;
}
if builder.is_empty() {
return;
}
let bytes = builder.build();
let reparsed = ParsedDiffSet::try_from(bytes.as_slice())
.expect("Serialized patchset should be re-parseable");
let reparsed_bytes: Vec<u8> = reparsed.into();
assert_eq!(
bytes, reparsed_bytes,
"Binary round-trip mismatch after SQL digest"
);
}
pub fn test_differential(schemas: &[TypedSimpleTable], sql: &str) {
let mut builder: PatchSet<SimpleTable, String, Vec<u8>> = PatchSet::new();
for schema in schemas {
builder.add_table(&**schema);
}
if builder.digest_sql(sql).is_err() || builder.is_empty() {
return;
}
let create_sqls: Vec<String> = schemas.iter().map(ToString::to_string).collect();
let create_sql_refs: Vec<&str> = create_sqls.iter().map(String::as_str).collect();
let simples: Vec<SimpleTable> = schemas.iter().map(|s| (**s).clone()).collect();
run_differential_test(&simples, &create_sql_refs, &[sql]);
}
#[must_use]
pub fn session_changeset_and_patchset(statements: &[&str]) -> (Vec<u8>, Vec<u8>) {
fn run_session(statements: &[&str], extract: impl Fn(&mut Session<'_>) -> Vec<u8>) -> Vec<u8> {
let conn = Connection::open_in_memory().unwrap();
for &sql in statements {
if sql.trim().to_uppercase().starts_with("CREATE TABLE") {
conn.execute(sql, []).unwrap();
}
}
let mut session = Session::new(&conn).unwrap();
session.attach::<&str>(None).unwrap();
for &sql in statements {
if !sql.trim().to_uppercase().starts_with("CREATE TABLE") {
conn.execute(sql, []).unwrap();
}
}
extract(&mut session)
}
let changeset = run_session(statements, |session| {
let mut buf = Vec::new();
session.changeset_strm(&mut buf).unwrap();
buf
});
let patchset = run_session(statements, |session| {
let mut buf = Vec::new();
session.patchset_strm(&mut buf).unwrap();
buf
});
(changeset, patchset)
}
#[must_use]
pub fn session_changeset_and_patchset_with_setup(
setup: &[&str],
tracked: &[&str],
) -> (Vec<u8>, Vec<u8>) {
fn run_session(
setup: &[&str],
tracked: &[&str],
extract: impl Fn(&mut Session<'_>) -> Vec<u8>,
) -> Vec<u8> {
let conn = Connection::open_in_memory().unwrap();
for &sql in setup {
conn.execute(sql, []).unwrap();
}
let mut session = Session::new(&conn).unwrap();
session.attach::<&str>(None).unwrap();
for &sql in tracked {
conn.execute(sql, []).unwrap();
}
extract(&mut session)
}
let changeset = run_session(setup, tracked, |session| {
let mut buf = Vec::new();
session.changeset_strm(&mut buf).unwrap();
buf
});
let patchset = run_session(setup, tracked, |session| {
let mut buf = Vec::new();
session.patchset_strm(&mut buf).unwrap();
buf
});
(changeset, patchset)
}
#[must_use]
pub fn byte_diff_report(label: &str, expected: &[u8], actual: &[u8]) -> String {
if expected == actual {
return format!("{label}: MATCH ({} bytes)", expected.len());
}
let mut report = format!(
"{label}: MISMATCH\n expected len: {}\n actual len: {}\n",
expected.len(),
actual.len()
);
let min_len = expected.len().min(actual.len());
let first_diff = (0..min_len).find(|&i| expected[i] != actual[i]);
if let Some(pos) = first_diff {
let _ = writeln!(
report,
" first diff at byte {pos}: expected 0x{:02x}, actual 0x{:02x}",
expected[pos], actual[pos]
);
let start = pos.saturating_sub(4);
let end = (pos + 8).min(min_len);
let _ = writeln!(
report,
" expected[{start}..{end}]: {:02x?}",
&expected[start..end]
);
let _ = writeln!(
report,
" actual [{start}..{end}]: {:02x?}",
&actual[start..end]
);
} else {
report.push_str(" common prefix matches, difference is in length only\n");
}
let _ = writeln!(report, " expected: {expected:02x?}");
let _ = writeln!(report, " actual: {actual:02x?}");
report
}
pub fn assert_bit_parity(sql_statements: &[&str], our_changeset: &[u8], our_patchset: &[u8]) {
let (sqlite_changeset, sqlite_patchset) = session_changeset_and_patchset(sql_statements);
let cs_report = byte_diff_report("changeset", &sqlite_changeset, our_changeset);
let ps_report = byte_diff_report("patchset", &sqlite_patchset, our_patchset);
assert!(
sqlite_changeset == our_changeset && sqlite_patchset == our_patchset,
"Bit parity failure!\n\n{cs_report}\n{ps_report}\n\nSQL:\n{}",
sql_statements.join("\n")
);
}
pub fn assert_patchset_sql_parity(schemas: &[SimpleTable], sql_statements: &[&str]) {
let mut patchset = PatchSet::<SimpleTable, String, Vec<u8>>::new();
let schema_map: std::collections::HashMap<&str, &SimpleTable> =
schemas.iter().map(|s| (s.name(), s)).collect();
for dml in sql_statements
.iter()
.filter(|s| !s.trim().to_uppercase().starts_with("CREATE"))
{
let upper = dml.trim().to_uppercase();
let table_name = if upper.starts_with("INSERT INTO") {
dml.trim()["INSERT INTO".len()..].split_whitespace().next()
} else if upper.starts_with("UPDATE") {
dml.trim()["UPDATE".len()..].split_whitespace().next()
} else if upper.starts_with("DELETE FROM") {
dml.trim()["DELETE FROM".len()..].split_whitespace().next()
} else {
None
};
if let Some(name) = table_name
&& let Some(schema) = schema_map.get(name)
{
patchset.add_table(schema);
}
patchset.digest_sql(dml).unwrap();
}
let our_patchset: Vec<u8> = patchset.build();
let (_, sqlite_patchset) = session_changeset_and_patchset(sql_statements);
let ps_report = byte_diff_report("patchset", &sqlite_patchset, &our_patchset);
assert!(
sqlite_patchset == our_patchset,
"Patchset bit parity failure!\n\n{ps_report}\n\nSQL:\n{}",
sql_statements.join("\n")
);
}
pub fn apply_changeset(conn: &Connection, changeset: &[u8]) -> Result<(), rusqlite::Error> {
use rusqlite::session::{ChangesetItem, ConflictAction, ConflictType};
let mut cursor = Cursor::new(changeset);
conn.apply_strm(
&mut cursor,
None::<fn(&str) -> bool>,
|_conflict_type: ConflictType, _item: ChangesetItem| ConflictAction::SQLITE_CHANGESET_ABORT,
)
}
pub fn get_all_rows(conn: &Connection, table_name: &str) -> Vec<Vec<String>> {
let query = format!("SELECT * FROM {table_name} ORDER BY rowid");
let Ok(mut stmt) = conn.prepare(&query) else {
return Vec::new();
};
let column_count = stmt.column_count();
let rows_result = stmt.query_map([], |row| {
let mut values = Vec::new();
for i in 0..column_count {
let value: rusqlite::types::Value = row.get(i).unwrap_or(rusqlite::types::Value::Null);
values.push(format!("{value:?}"));
}
Ok(values)
});
let mut rows: Vec<Vec<String>> = match rows_result {
Ok(mapped) => mapped.filter_map(Result::ok).collect(),
Err(_) => Vec::new(),
};
rows.sort();
rows
}
pub fn compare_db_states(conn1: &Connection, conn2: &Connection, create_table_sqls: &[String]) {
for create_sql in create_table_sqls {
let table_name = extract_table_name(create_sql);
let rows1 = get_all_rows(conn1, &table_name);
let rows2 = get_all_rows(conn2, &table_name);
assert_eq!(
rows1, rows2,
"Database state mismatch for table '{table_name}'!\nDB1: {rows1:?}\nDB2: {rows2:?}"
);
}
}
#[must_use]
pub fn extract_table_name(create_sql: &str) -> String {
let lower = create_sql.to_lowercase();
let start = lower.find("create table").unwrap() + "create table".len();
let rest = &create_sql[start..].trim_start();
let rest = if rest.to_lowercase().starts_with("if not exists") {
rest["if not exists".len()..].trim_start()
} else {
rest
};
let end = rest
.find(|c: char| c.is_whitespace() || c == '(')
.unwrap_or(rest.len());
rest[..end].to_string()
}
pub fn run_crash_dir_regression(
crash_dir: &str,
fuzz_source_dir: &str,
time_limit: std::time::Duration,
test_fn: impl Fn(&[u8]),
) -> usize {
use std::fs;
use std::time::Instant;
let _ = fs::create_dir_all(crash_dir);
if let Ok(fuzz_entries) = fs::read_dir(fuzz_source_dir) {
for entry in fuzz_entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "fuzz") {
let dest = format!(
"{}/{}",
crash_dir,
path.file_name().unwrap().to_string_lossy()
);
if !std::path::Path::new(&dest).exists() {
let _ = fs::copy(&path, &dest);
}
}
}
}
let Ok(entries) = fs::read_dir(crash_dir) else {
return 0;
};
let mut tested = 0;
let mut failures: Vec<String> = Vec::new();
let mut slow_inputs: Vec<String> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let data = match fs::read(&path) {
Ok(d) => d,
Err(e) => {
failures.push(format!("{}: read error: {e}", path.display()));
continue;
}
};
let filename = path.file_name().unwrap().to_string_lossy().to_string();
let start = Instant::now();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
test_fn(&data);
}));
if let Err(panic) = result {
let msg = if let Some(s) = panic.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
failures.push(format!("{filename}: {msg}"));
}
let elapsed = start.elapsed();
if elapsed > time_limit {
slow_inputs.push(format!(
"{filename}: {:.3}s (limit: {:.1}s) [{} bytes]",
elapsed.as_secs_f64(),
time_limit.as_secs_f64(),
data.len(),
));
}
tested += 1;
}
assert!(
failures.is_empty(),
"Failures in {}/{tested} crash files:\n{}",
failures.len(),
failures.join("\n")
);
assert!(
slow_inputs.is_empty(),
"Timeout-class bugs: {}/{tested} inputs exceeded {:.1}s limit:\n{}",
slow_inputs.len(),
time_limit.as_secs_f64(),
slow_inputs.join("\n")
);
tested
}
const ALL_WIRE_TYPES: [crate::wire::WireType; 14] = [
crate::wire::WireType::Bool,
crate::wire::WireType::Int,
crate::wire::WireType::Real,
crate::wire::WireType::Text,
crate::wire::WireType::Bytes,
crate::wire::WireType::Uuid,
crate::wire::WireType::Decimal,
crate::wire::WireType::Timestamp,
crate::wire::WireType::TimestampTz,
crate::wire::WireType::Date,
crate::wire::WireType::Time,
crate::wire::WireType::Interval,
crate::wire::WireType::Json,
crate::wire::WireType::Jsonb,
];
pub fn test_wire_pg_binary(input: &[u8]) {
use crate::wire::{PgBinary, PgBinaryColumn, TypeMap, WireAdapter};
let types: TypeMap<PgBinary, alloc::string::String, Vec<u8>> = TypeMap::defaults();
for wire_type in ALL_WIRE_TYPES {
for raw in [Some(input), None] {
let _ = types.decode(PgBinaryColumn {
column_name: "c",
wire_type,
raw,
});
}
}
}
#[cfg(feature = "pg-walstream")]
pub fn test_wire_pg_walstream(input: &[u8]) {
use crate::pg_walstream::{ColumnValue, PgWalstream, PgWalstreamColumn};
use crate::wire::{TypeMap, WireAdapter};
let Ok(text) = core::str::from_utf8(input) else {
return;
};
let text_cv = ColumnValue::text(text);
let types: TypeMap<PgWalstream, alloc::string::String, Vec<u8>> = TypeMap::defaults();
for wire_type in ALL_WIRE_TYPES {
let _ = types.decode(PgWalstreamColumn {
column_name: "c",
wire_type,
data: &text_cv,
});
}
}
#[cfg(feature = "wal2json")]
pub fn test_wire_wal2json(input: &[u8]) {
use crate::wal2json::{Wal2Json, Wal2JsonColumn};
use crate::wire::{TypeMap, WireAdapter};
let Ok(text) = core::str::from_utf8(input) else {
return;
};
let string_val = serde_json::Value::String(text.into());
let parsed_val =
serde_json::from_str::<serde_json::Value>(text).unwrap_or(serde_json::Value::Null);
let types: TypeMap<Wal2Json, alloc::string::String, Vec<u8>> = TypeMap::defaults();
for wire_type in ALL_WIRE_TYPES {
for value in [&string_val, &parsed_val] {
let _ = types.decode(Wal2JsonColumn {
column_name: "c",
wire_type,
value,
});
}
}
}
#[cfg(feature = "maxwell")]
pub fn test_wire_maxwell(input: &[u8]) {
use crate::maxwell::{Maxwell, MaxwellColumn};
use crate::wire::{TypeMap, WireAdapter};
let Ok(text) = core::str::from_utf8(input) else {
return;
};
let string_val = serde_json::Value::String(text.into());
let parsed_val =
serde_json::from_str::<serde_json::Value>(text).unwrap_or(serde_json::Value::Null);
let types: TypeMap<Maxwell, alloc::string::String, Vec<u8>> = TypeMap::defaults();
for wire_type in ALL_WIRE_TYPES {
for value in [&string_val, &parsed_val] {
let _ = types.decode(MaxwellColumn {
column_name: "c",
wire_type,
value,
});
}
}
}