use std::collections::HashMap;
const DICTIONARY: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
#[must_use]
pub fn hash(input: &str, len: usize) -> String {
let dict_len = DICTIONARY.len() as u128;
let len_u32 = u32::try_from(len).unwrap_or(u32::MAX);
let combinations_count = dict_len.pow(len_u32);
let p: u128 = 53;
let mut power: u128 = 1;
let mut hash_val: u128 = 0;
for ch in input.chars() {
let code = u128::from(u32::from(ch));
hash_val = (hash_val + (code * power)) % combinations_count;
power = (power * p) % combinations_count;
}
let mut result = Vec::with_capacity(len);
let mut index = hash_val;
for _ in 0..len {
let idx = usize::try_from(index % dict_len).unwrap_or(0);
result.push(DICTIONARY[idx] as char);
index /= dict_len;
}
result.into_iter().rev().collect()
}
#[must_use]
pub fn trim_char(s: &str, c: char) -> String {
s.trim_start_matches(c).trim_end_matches(c).to_string()
}
#[must_use]
pub fn trim_chars(s: &str, chars: &[char]) -> String {
let mut result = s.to_string();
for c in chars {
result = result
.trim_start_matches(*c)
.trim_end_matches(*c)
.to_string();
}
result
}
#[must_use]
pub fn escape_for_sql_default(input: &str, mode: EscapeMode) -> String {
let mut value = input.replace('\\', "\\\\").replace('\'', "''");
if matches!(mode, EscapeMode::PgArray) {
value = value.replace('"', "\\\"");
}
value
}
#[must_use]
pub fn escape_for_rust_literal(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[must_use]
pub fn unescape_from_sql_default(input: &str, mode: EscapeMode) -> String {
let mut res = input.replace("\\\"", "\"").replace("\\\\", "\\");
if !matches!(mode, EscapeMode::Array) {
res = res.replace("''", "'");
}
res
}
#[derive(Debug, Clone, Copy)]
pub enum EscapeMode {
Default,
Array,
PgArray,
}
#[must_use]
pub fn escape_for_ts_literal(input: &str) -> String {
serde_json::to_string(input).unwrap_or_else(|_| format!("\"{input}\""))
}
#[must_use]
pub fn number_for_ts(value: &str) -> (NumberMode, String) {
const I64_MIN_F64: f64 = -9_223_372_036_854_775_808.0;
const I64_MAX_F64: f64 = 9_223_372_036_854_775_807.0;
value.parse::<f64>().map_or_else(
|_| (NumberMode::Number, format!("sql`{value}`")),
|num| {
if num.is_nan() {
(NumberMode::Number, format!("sql`{value}`"))
} else if (I64_MIN_F64..=I64_MAX_F64).contains(&num) {
(NumberMode::Number, value.to_string())
} else {
(NumberMode::BigInt, format!("{value}n"))
}
},
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumberMode {
Number,
BigInt,
}
#[must_use]
pub fn parse_params(type_str: &str) -> Vec<String> {
if let Some(start) = type_str.find('(')
&& let Some(end) = type_str.find(')')
{
let params = &type_str[start + 1..end];
return params.split(',').map(|s| s.trim().to_string()).collect();
}
Vec::new()
}
#[derive(Debug, Clone)]
pub struct ResolverResult<T> {
pub created: Vec<T>,
pub deleted: Vec<T>,
pub renamed_or_moved: Vec<Rename<T>>,
}
#[derive(Debug, Clone)]
pub struct Rename<T> {
pub from: T,
pub to: T,
}
#[must_use]
pub const fn simple_resolver<T: Clone>(created: Vec<T>, deleted: Vec<T>) -> ResolverResult<T> {
ResolverResult {
created,
deleted,
renamed_or_moved: Vec::new(),
}
}
#[must_use]
pub fn inspect<K, V, S>(map: &HashMap<K, V, S>) -> String
where
K: std::fmt::Display,
V: std::fmt::Display,
S: std::hash::BuildHasher,
{
if map.is_empty() {
return String::new();
}
let pairs: Vec<String> = map.iter().map(|(k, v)| format!("{k}: '{v}'")).collect();
format!("{{ {} }}", pairs.join(", "))
}
#[must_use]
pub fn prepare_migration_renames<T>(
table_renames: &[(String, String)],
column_renames: &[(String, String, String)], ) -> Vec<String> {
let mut renames = Vec::new();
for (from, to) in table_renames {
renames.push(format!("table:{from}:{to}"));
}
for (table, from, to) in column_renames {
renames.push(format!("column:{table}:{from}:{to}"));
}
renames
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash() {
let h1 = hash("test", 12);
let h2 = hash("test", 12);
assert_eq!(h1, h2);
assert_eq!(h1.len(), 12);
let h3 = hash("different", 12);
assert_ne!(h1, h3);
}
#[test]
fn test_trim_char() {
assert_eq!(trim_char("'hello'", '\''), "hello");
assert_eq!(trim_char("hello", '\''), "hello");
}
#[test]
fn test_parse_params() {
assert_eq!(parse_params("varchar(255)"), vec!["255"]);
assert_eq!(parse_params("numeric(10,2)"), vec!["10", "2"]);
assert!(parse_params("text").is_empty());
}
#[test]
fn test_number_for_ts() {
let (mode, val) = number_for_ts("123");
assert_eq!(mode, NumberMode::Number);
assert_eq!(val, "123");
let (mode, val) = number_for_ts("100000000000000000000");
assert_eq!(mode, NumberMode::BigInt);
assert!(val.ends_with('n'));
}
#[test]
fn test_escape_for_sql_default() {
assert_eq!(
escape_for_sql_default("it's a test", EscapeMode::Default),
"it''s a test"
);
assert_eq!(
escape_for_sql_default("path\\to\\file", EscapeMode::Default),
"path\\\\to\\\\file"
);
}
#[test]
fn test_escape_for_rust_literal() {
assert_eq!(escape_for_rust_literal("hello"), "hello");
assert_eq!(
escape_for_rust_literal(r#"say "hello""#),
r#"say \"hello\""#
);
assert_eq!(escape_for_rust_literal(r"path\to\file"), r"path\\to\\file");
assert_eq!(
escape_for_rust_literal(r#"a "quoted" path\to\file"#),
r#"a \"quoted\" path\\to\\file"#
);
assert_eq!(
escape_for_rust_literal(r#"SELECT * FROM "users" WHERE name = 'test'"#),
r#"SELECT * FROM \"users\" WHERE name = 'test'"#
);
}
}