use crate::{
collection::ArrayVectorU8,
database::{
DatabaseTy,
schema_manager::{
Repeatability, SchemaManagerError,
toml_parser::{Expr, toml},
},
},
misc::str_split1,
};
use alloc::string::String;
use std::io::{BufRead, BufReader, Read};
#[derive(Debug, Default)]
pub struct MigrationCfg {
pub dbs: ArrayVectorU8<DatabaseTy, { DatabaseTy::len() }>,
pub repeatability: Option<Repeatability>,
}
#[derive(Debug, Default)]
pub struct ParsedMigration {
pub cfg: MigrationCfg,
pub sql_in: String,
pub sql_out: String,
}
#[inline]
pub fn parse_unified_migration<R>(read: R) -> crate::Result<ParsedMigration>
where
R: Read,
{
let mut br = BufReader::new(read);
let mut overall_buffer = String::with_capacity(16);
let mut parsed_migration = ParsedMigration::default();
iterations(&mut overall_buffer, &mut br, |_| false)?;
if let Some(rslt) = overall_buffer.split("-- wtx dbs").nth(1) {
for db_str in str_split1(rslt, b',') {
if let Ok(db) = db_str.trim().try_into() {
let is_not_already_inserted = !parsed_migration.cfg.dbs.contains(&db);
if is_not_already_inserted {
parsed_migration.cfg.dbs.push(db)?;
}
}
}
iterations(&mut overall_buffer, &mut br, |_| false)?;
}
if let Some(rslt) = overall_buffer.split("-- wtx repeatability").nth(1) {
if let Ok(repeatability) = rslt.trim().try_into() {
parsed_migration.cfg.repeatability = Some(repeatability);
}
iterations(&mut overall_buffer, &mut br, |_| false)?;
}
if !overall_buffer.contains("-- wtx IN") {
return Err(SchemaManagerError::IncompleteSqlFile.into());
}
iterations(&mut overall_buffer, &mut br, |str_read| !str_read.contains("-- wtx OUT"))?;
if let Some(rslt) = overall_buffer.rsplit("-- wtx OUT").nth(1) {
parsed_migration.sql_in = rslt.trim().into();
} else {
parsed_migration.sql_in = overall_buffer.trim().into();
return Ok(parsed_migration);
}
iterations(&mut overall_buffer, &mut br, |_| true)?;
parsed_migration.sql_out = overall_buffer.trim().into();
if parsed_migration.sql_in.is_empty() {
return Err(SchemaManagerError::IncompleteSqlFile.into());
}
Ok(parsed_migration)
}
pub(crate) fn parse_migration_toml<R>(read: R) -> crate::Result<MigrationCfg>
where
R: Read,
{
let mut migration_toml = MigrationCfg { dbs: ArrayVectorU8::new(), repeatability: None };
for (ident, toml_expr) in toml(read)? {
match (ident.as_str(), toml_expr) {
("dbs", Expr::Array(array)) => {
for str in array.into_iter() {
let Ok(elem) = str.as_str().try_into() else {
continue;
};
migration_toml.dbs.push(elem)?;
}
}
("repeatability", Expr::String(s)) => {
let Ok(elem) = s.as_str().try_into() else {
continue;
};
migration_toml.repeatability = Some(elem);
}
_ => {}
}
}
Ok(migration_toml)
}
fn iterations<F, R>(
overall_buffer: &mut String,
br: &mut BufReader<R>,
mut cb: F,
) -> crate::Result<()>
where
F: FnMut(&str) -> bool,
R: Read,
{
overall_buffer.clear();
let mut bytes_read = 0;
loop {
let curr_bytes_read = br.read_line(overall_buffer)?;
if curr_bytes_read == 0 {
break;
}
let Some(str_read) = overall_buffer.get(bytes_read..) else { break };
let trimmed = str_read.trim();
bytes_read = bytes_read.saturating_add(curr_bytes_read);
if trimmed.is_empty() || trimmed.starts_with("//") {
continue;
}
if !cb(trimmed) {
break;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::database::{
DatabaseTy,
schema_manager::{Repeatability, migration_parser::parse_unified_migration},
};
#[test]
fn does_not_take_into_consideration_white_spaces_and_comments() {
let s = "// FOO\n\t\n-- wtx IN\nSOMETHING\nFOO\n";
let rslt = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!("SOMETHING\nFOO", rslt.sql_in);
}
#[test]
fn must_have_obrigatory_params() {
assert!(parse_unified_migration(&[][..]).is_err());
}
#[test]
fn parses_optional_dbs() {
let s = "-- wtx IN\nSOMETHING";
let no_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(no_declaration.cfg.dbs.is_empty());
let s = "-- wtx dbs\n-- wtx IN\nSOMETHING";
let with_initial_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(with_initial_declaration.cfg.dbs.is_empty());
let s = "-- wtx dbs bird,apple\n-- wtx IN\nSOMETHING";
let with_incorrect_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(with_incorrect_declaration.cfg.dbs.is_empty());
let s = "-- wtx dbs postgres\n-- wtx IN\nSOMETHING";
let two_dbs = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!(two_dbs.cfg.dbs[0], DatabaseTy::Postgres);
}
#[test]
fn parses_down() {
let s = "\n-- wtx IN\n\nSOMETHING\nFOO\n\n-- wtx OUT\n\nBAR\n";
let rslt = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!("SOMETHING\nFOO", rslt.sql_in);
assert_eq!("BAR", rslt.sql_out);
}
#[test]
fn parses_repeatability() {
let s = "-- wtx IN\nSOMETHING";
let no_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(no_declaration.cfg.repeatability.is_none());
let s = "-- wtx repeatability\n-- wtx IN\nSOMETHING";
let with_initial_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(with_initial_declaration.cfg.repeatability.is_none());
let s = "-- wtx repeatability FOO\n-- wtx IN\nSOMETHING";
let with_incorrect_declaration = parse_unified_migration(s.as_bytes()).unwrap();
assert!(with_incorrect_declaration.cfg.repeatability.is_none());
let s = "-- wtx repeatability always\n-- wtx IN\nSOMETHING";
let always = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!(always.cfg.repeatability, Some(Repeatability::Always));
let s = "-- wtx repeatability on-checksum-change\n-- wtx IN\nSOMETHING";
let on_checksum_change = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!(on_checksum_change.cfg.repeatability, Some(Repeatability::OnChecksumChange));
}
#[test]
fn parses_mandatory_params() {
let s = "-- wtx IN\n\nSOMETHING\nFOO";
let rslt = parse_unified_migration(s.as_bytes()).unwrap();
assert_eq!("SOMETHING\nFOO", rslt.sql_in);
}
}