Skip to main content

polyglot_sql/
helper.rs

1//! Helper utilities for SQL processing
2//!
3//! This module provides various utility functions used throughout the codebase:
4//! - Safe sequence access
5//! - Collection normalization
6//! - String manipulation
7//! - Fixed-point transformation
8//! - Topological sorting
9//!
10//! Based on the Python implementation in `sqlglot/helper.py`.
11
12use std::collections::{HashMap, HashSet};
13use std::hash::Hash;
14
15/// Interval units that operate on date components
16pub const DATE_UNITS: &[&str] = &["day", "week", "month", "quarter", "year", "year_month"];
17
18/// Returns the value in `seq` at position `index`, or `None` if `index` is out of bounds.
19///
20/// Supports negative indexing like Python (e.g., -1 for last element).
21///
22/// # Example
23///
24/// ```
25/// use polyglot_sql::helper::seq_get;
26///
27/// let v = vec![1, 2, 3];
28/// assert_eq!(seq_get(&v, 0), Some(&1));
29/// assert_eq!(seq_get(&v, -1), Some(&3));
30/// assert_eq!(seq_get(&v, 10), None);
31/// ```
32pub fn seq_get<T>(seq: &[T], index: isize) -> Option<&T> {
33    let len = seq.len() as isize;
34    if len == 0 {
35        return None;
36    }
37
38    let actual_index = if index < 0 { len + index } else { index };
39
40    if actual_index < 0 || actual_index >= len {
41        None
42    } else {
43        seq.get(actual_index as usize)
44    }
45}
46
47/// Ensures that a value is wrapped in a Vec if it isn't already a collection.
48///
49/// This is a generic trait that can be implemented for different types.
50pub trait EnsureList {
51    type Item;
52    fn ensure_list(self) -> Vec<Self::Item>;
53}
54
55impl<T> EnsureList for Vec<T> {
56    type Item = T;
57    fn ensure_list(self) -> Vec<Self::Item> {
58        self
59    }
60}
61
62impl<T> EnsureList for Option<T> {
63    type Item = T;
64    fn ensure_list(self) -> Vec<Self::Item> {
65        match self {
66            Some(v) => vec![v],
67            None => vec![],
68        }
69    }
70}
71
72/// Wrap a single value in a Vec
73pub fn ensure_list<T>(value: T) -> Vec<T> {
74    vec![value]
75}
76
77/// Wrap an Option in a Vec (empty if None)
78pub fn ensure_list_option<T>(value: Option<T>) -> Vec<T> {
79    match value {
80        Some(v) => vec![v],
81        None => vec![],
82    }
83}
84
85/// Formats any number of string arguments as CSV.
86///
87/// # Example
88///
89/// ```
90/// use polyglot_sql::helper::csv;
91///
92/// assert_eq!(csv(&["a", "b", "c"], ", "), "a, b, c");
93/// assert_eq!(csv(&["a", "", "c"], ", "), "a, c");
94/// ```
95pub fn csv(args: &[&str], sep: &str) -> String {
96    args.iter()
97        .filter(|s| !s.is_empty())
98        .copied()
99        .collect::<Vec<_>>()
100        .join(sep)
101}
102
103/// Formats strings as CSV with default separator ", "
104pub fn csv_default(args: &[&str]) -> String {
105    csv(args, ", ")
106}
107
108/// Applies a transformation to a given expression until a fix point is reached.
109///
110/// # Arguments
111/// * `value` - The initial value to transform
112/// * `func` - The transformation function
113///
114/// # Returns
115/// The transformed value when it stops changing
116///
117/// # Example
118///
119/// ```
120/// use polyglot_sql::helper::while_changing;
121///
122/// // Example: keep dividing by 2 until odd
123/// let result = while_changing(16, |n| if n % 2 == 0 { n / 2 } else { n });
124/// assert_eq!(result, 1);
125/// ```
126pub fn while_changing<T, F>(mut value: T, func: F) -> T
127where
128    T: Clone + PartialEq,
129    F: Fn(T) -> T,
130{
131    loop {
132        let new_value = func(value.clone());
133        if new_value == value {
134            return new_value;
135        }
136        value = new_value;
137    }
138}
139
140/// Applies a transformation until a fix point, using a hash function for comparison.
141///
142/// More efficient than `while_changing` when equality comparison is expensive.
143pub fn while_changing_hash<T, F, H>(mut value: T, func: F, hasher: H) -> T
144where
145    F: Fn(T) -> T,
146    H: Fn(&T) -> u64,
147{
148    loop {
149        let start_hash = hasher(&value);
150        value = func(value);
151        let end_hash = hasher(&value);
152        if start_hash == end_hash {
153            return value;
154        }
155    }
156}
157
158/// Sorts a directed acyclic graph in topological order.
159///
160/// # Arguments
161/// * `dag` - A map from node to its dependencies (nodes it depends on)
162///
163/// # Returns
164/// A sorted list of nodes, or an error if there's a cycle
165///
166/// # Example
167///
168/// ```
169/// use polyglot_sql::helper::tsort;
170/// use std::collections::{HashMap, HashSet};
171///
172/// let mut dag = HashMap::new();
173/// dag.insert("a", HashSet::from(["b", "c"]));
174/// dag.insert("b", HashSet::from(["c"]));
175/// dag.insert("c", HashSet::new());
176///
177/// let sorted = tsort(dag).unwrap();
178/// // c comes before b, b comes before a
179/// assert!(sorted.iter().position(|x| x == &"c") < sorted.iter().position(|x| x == &"b"));
180/// assert!(sorted.iter().position(|x| x == &"b") < sorted.iter().position(|x| x == &"a"));
181/// ```
182pub fn tsort<T>(mut dag: HashMap<T, HashSet<T>>) -> Result<Vec<T>, TsortError>
183where
184    T: Clone + Eq + Hash + Ord,
185{
186    let mut result = Vec::new();
187
188    // Add any missing nodes that appear only as dependencies
189    let all_deps: Vec<T> = dag.values().flat_map(|deps| deps.iter().cloned()).collect();
190
191    for dep in all_deps {
192        dag.entry(dep).or_insert_with(HashSet::new);
193    }
194
195    while !dag.is_empty() {
196        // Find nodes with no dependencies
197        let mut current: Vec<T> = dag
198            .iter()
199            .filter(|(_, deps)| deps.is_empty())
200            .map(|(node, _)| node.clone())
201            .collect();
202
203        if current.is_empty() {
204            return Err(TsortError::CycleDetected);
205        }
206
207        // Sort for deterministic output
208        current.sort();
209
210        // Remove these nodes from the graph
211        for node in &current {
212            dag.remove(node);
213        }
214
215        // Remove these nodes from all dependency lists
216        let current_set: HashSet<_> = current.iter().cloned().collect();
217        for deps in dag.values_mut() {
218            *deps = deps.difference(&current_set).cloned().collect();
219        }
220
221        result.extend(current);
222    }
223
224    Ok(result)
225}
226
227/// Error type for topological sort
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum TsortError {
230    CycleDetected,
231}
232
233impl std::fmt::Display for TsortError {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        match self {
236            TsortError::CycleDetected => write!(f, "Cycle detected in DAG"),
237        }
238    }
239}
240
241impl std::error::Error for TsortError {}
242
243/// Searches for a new name that doesn't conflict with taken names.
244///
245/// # Arguments
246/// * `taken` - A set of names that are already taken
247/// * `base` - The base name to use
248///
249/// # Returns
250/// The original name if available, otherwise `base_2`, `base_3`, etc.
251///
252/// # Example
253///
254/// ```
255/// use polyglot_sql::helper::find_new_name;
256/// use std::collections::HashSet;
257///
258/// let taken = HashSet::from(["col".to_string(), "col_2".to_string()]);
259/// assert_eq!(find_new_name(&taken, "col"), "col_3");
260/// assert_eq!(find_new_name(&taken, "other"), "other");
261/// ```
262pub fn find_new_name(taken: &HashSet<String>, base: &str) -> String {
263    if !taken.contains(base) {
264        return base.to_string();
265    }
266
267    let mut i = 2;
268    loop {
269        let new_name = format!("{}_{}", base, i);
270        if !taken.contains(&new_name) {
271            return new_name;
272        }
273        i += 1;
274    }
275}
276
277/// Creates a name generator that produces sequential names.
278///
279/// Returns a closure that generates names like `prefix0`, `prefix1`, etc.
280///
281/// # Example
282///
283/// ```
284/// use polyglot_sql::helper::name_sequence;
285///
286/// let mut gen = name_sequence("col");
287/// assert_eq!(gen(), "col0");
288/// assert_eq!(gen(), "col1");
289/// assert_eq!(gen(), "col2");
290/// ```
291pub fn name_sequence(prefix: &str) -> impl FnMut() -> String {
292    let prefix = prefix.to_string();
293    let mut counter = 0usize;
294    move || {
295        let name = format!("{}{}", prefix, counter);
296        counter += 1;
297        name
298    }
299}
300
301/// Check if a string can be parsed as an integer
302///
303/// # Example
304///
305/// ```
306/// use polyglot_sql::helper::is_int;
307///
308/// assert!(is_int("123"));
309/// assert!(is_int("-456"));
310/// assert!(!is_int("12.34"));
311/// assert!(!is_int("abc"));
312/// ```
313pub fn is_int(text: &str) -> bool {
314    text.parse::<i64>().is_ok()
315}
316
317/// Check if a string can be parsed as a float
318///
319/// # Example
320///
321/// ```
322/// use polyglot_sql::helper::is_float;
323///
324/// assert!(is_float("12.34"));
325/// assert!(is_float("123"));
326/// assert!(is_float("-1.5e10"));
327/// assert!(!is_float("abc"));
328/// ```
329pub fn is_float(text: &str) -> bool {
330    text.parse::<f64>().is_ok()
331}
332
333/// Check if a string is a valid ISO date (YYYY-MM-DD)
334///
335/// # Example
336///
337/// ```
338/// use polyglot_sql::helper::is_iso_date;
339///
340/// assert!(is_iso_date("2023-01-15"));
341/// assert!(!is_iso_date("01-15-2023"));
342/// assert!(!is_iso_date("not a date"));
343/// ```
344pub fn is_iso_date(text: &str) -> bool {
345    // Simple validation: YYYY-MM-DD format
346    if text.len() != 10 {
347        return false;
348    }
349    let parts: Vec<&str> = text.split('-').collect();
350    if parts.len() != 3 {
351        return false;
352    }
353    if parts[0].len() != 4 || parts[1].len() != 2 || parts[2].len() != 2 {
354        return false;
355    }
356
357    let year: u32 = match parts[0].parse() {
358        Ok(y) => y,
359        Err(_) => return false,
360    };
361    let month: u32 = match parts[1].parse() {
362        Ok(m) => m,
363        Err(_) => return false,
364    };
365    let day: u32 = match parts[2].parse() {
366        Ok(d) => d,
367        Err(_) => return false,
368    };
369
370    if month < 1 || month > 12 {
371        return false;
372    }
373    if day < 1 || day > 31 {
374        return false;
375    }
376
377    // Basic validation of days per month
378    let days_in_month = match month {
379        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
380        4 | 6 | 9 | 11 => 30,
381        2 => {
382            // Leap year check
383            if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
384                29
385            } else {
386                28
387            }
388        }
389        _ => return false,
390    };
391
392    day <= days_in_month
393}
394
395/// Check if a string is a valid ISO datetime
396///
397/// Accepts formats like:
398/// - `2023-01-15T10:30:00`
399/// - `2023-01-15 10:30:00`
400/// - `2023-01-15T10:30:00.123456`
401/// - `2023-01-15T10:30:00+00:00`
402pub fn is_iso_datetime(text: &str) -> bool {
403    // Try to find the date portion
404    if text.len() < 10 {
405        return false;
406    }
407
408    // Check date portion
409    if !is_iso_date(&text[..10]) {
410        return false;
411    }
412
413    // If there's a time portion, validate it
414    if text.len() > 10 {
415        // Must have separator
416        let sep = text.chars().nth(10).expect("length checked above");
417        if sep != 'T' && sep != ' ' {
418            return false;
419        }
420
421        // Get time portion (everything after the separator, excluding timezone)
422        let time_str = &text[11..];
423
424        // Find where the timezone or fractional seconds end
425        let time_end = time_str
426            .find('+')
427            .or_else(|| time_str.rfind('-'))
428            .or_else(|| time_str.find('Z'))
429            .unwrap_or(time_str.len());
430
431        let time_without_tz = &time_str[..time_end];
432
433        // Split by '.' to handle fractional seconds
434        let (time_part, _frac_part) = match time_without_tz.find('.') {
435            Some(idx) => (&time_without_tz[..idx], Some(&time_without_tz[idx + 1..])),
436            None => (time_without_tz, None),
437        };
438
439        // Validate HH:MM:SS
440        if time_part.len() < 8 {
441            // Allow HH:MM format
442            if time_part.len() != 5 {
443                return false;
444            }
445        }
446
447        let parts: Vec<&str> = time_part.split(':').collect();
448        if parts.len() < 2 || parts.len() > 3 {
449            return false;
450        }
451
452        let hour: u32 = match parts[0].parse() {
453            Ok(h) => h,
454            Err(_) => return false,
455        };
456        let minute: u32 = match parts[1].parse() {
457            Ok(m) => m,
458            Err(_) => return false,
459        };
460
461        if hour > 23 || minute > 59 {
462            return false;
463        }
464
465        if parts.len() == 3 {
466            let second: u32 = match parts[2].parse() {
467                Ok(s) => s,
468                Err(_) => return false,
469            };
470            if second > 59 {
471                return false;
472            }
473        }
474    }
475
476    true
477}
478
479/// Converts a camelCase string to UPPER_SNAKE_CASE
480///
481/// # Example
482///
483/// ```
484/// use polyglot_sql::helper::camel_to_snake_case;
485///
486/// assert_eq!(camel_to_snake_case("camelCase"), "CAMEL_CASE");
487/// assert_eq!(camel_to_snake_case("MyHTTPServer"), "MY_H_T_T_P_SERVER");
488/// ```
489pub fn camel_to_snake_case(name: &str) -> String {
490    let mut result = String::with_capacity(name.len() + 4);
491    for (i, ch) in name.chars().enumerate() {
492        if ch.is_uppercase() && i > 0 {
493            result.push('_');
494        }
495        result.push(ch.to_ascii_uppercase());
496    }
497    result
498}
499
500/// Converts a snake_case string to CamelCase
501///
502/// # Example
503///
504/// ```
505/// use polyglot_sql::helper::snake_to_camel_case;
506///
507/// assert_eq!(snake_to_camel_case("snake_case"), "SnakeCase");
508/// assert_eq!(snake_to_camel_case("my_http_server"), "MyHttpServer");
509/// ```
510pub fn snake_to_camel_case(name: &str) -> String {
511    let mut result = String::with_capacity(name.len());
512    let mut capitalize_next = true;
513
514    for ch in name.chars() {
515        if ch == '_' {
516            capitalize_next = true;
517        } else if capitalize_next {
518            result.push(ch.to_ascii_uppercase());
519            capitalize_next = false;
520        } else {
521            result.push(ch.to_ascii_lowercase());
522        }
523    }
524    result
525}
526
527/// Get the nesting depth of a nested HashMap
528///
529/// # Example
530///
531/// ```
532/// use polyglot_sql::helper::dict_depth;
533/// use std::collections::HashMap;
534///
535/// let empty: HashMap<String, ()> = HashMap::new();
536/// assert_eq!(dict_depth(&empty), 1);
537///
538/// let mut nested: HashMap<String, HashMap<String, ()>> = HashMap::new();
539/// nested.insert("a".into(), HashMap::new());
540/// // Note: This returns 1 because we can't traverse into nested hashmaps generically
541/// ```
542pub fn dict_depth<K, V>(d: &HashMap<K, V>) -> usize
543where
544    K: std::hash::Hash + Eq,
545{
546    // In Rust, we can't easily traverse nested hashmaps generically
547    // This is a simplified version that returns 1 for any non-empty hashmap
548    if d.is_empty() {
549        1
550    } else {
551        1
552    }
553}
554
555/// Returns the first element from an iterator
556///
557/// # Example
558///
559/// ```
560/// use polyglot_sql::helper::first;
561/// use std::collections::HashSet;
562///
563/// let set = HashSet::from([1, 2, 3]);
564/// let f = first(set.iter());
565/// assert!(f.is_some());
566/// ```
567pub fn first<I, T>(mut iter: I) -> Option<T>
568where
569    I: Iterator<Item = T>,
570{
571    iter.next()
572}
573
574/// Perform a split on a value and return N words with `None` for missing parts.
575///
576/// # Arguments
577/// * `value` - The string to split
578/// * `sep` - The separator
579/// * `min_num_words` - Minimum number of words in result
580/// * `fill_from_start` - If true, pad with None at start; otherwise at end
581///
582/// # Example
583///
584/// ```
585/// use polyglot_sql::helper::split_num_words;
586///
587/// assert_eq!(
588///     split_num_words("db.table", ".", 3, true),
589///     vec![None, Some("db".to_string()), Some("table".to_string())]
590/// );
591/// assert_eq!(
592///     split_num_words("db.table", ".", 3, false),
593///     vec![Some("db".to_string()), Some("table".to_string()), None]
594/// );
595/// ```
596pub fn split_num_words(
597    value: &str,
598    sep: &str,
599    min_num_words: usize,
600    fill_from_start: bool,
601) -> Vec<Option<String>> {
602    let words: Vec<String> = value.split(sep).map(|s| s.to_string()).collect();
603    let num_words = words.len();
604
605    if num_words >= min_num_words {
606        return words.into_iter().map(Some).collect();
607    }
608
609    let padding = min_num_words - num_words;
610    let mut result = Vec::with_capacity(min_num_words);
611
612    if fill_from_start {
613        result.extend(std::iter::repeat(None).take(padding));
614        result.extend(words.into_iter().map(Some));
615    } else {
616        result.extend(words.into_iter().map(Some));
617        result.extend(std::iter::repeat(None).take(padding));
618    }
619
620    result
621}
622
623/// Flattens a nested collection into a flat iterator
624///
625/// Due to Rust's type system, this is implemented as a recursive function
626/// that works on specific types rather than a generic flatten.
627pub fn flatten<T: Clone>(values: &[Vec<T>]) -> Vec<T> {
628    values.iter().flat_map(|v| v.iter().cloned()).collect()
629}
630
631/// Merge overlapping ranges
632///
633/// # Arguments
634/// * `ranges` - A list of (start, end) tuples representing ranges
635///
636/// # Returns
637/// A list of merged, non-overlapping ranges
638///
639/// # Example
640///
641/// ```
642/// use polyglot_sql::helper::merge_ranges;
643///
644/// let ranges = vec![(1, 3), (2, 6), (8, 10)];
645/// assert_eq!(merge_ranges(ranges), vec![(1, 6), (8, 10)]);
646/// ```
647pub fn merge_ranges<T: Ord + Copy>(mut ranges: Vec<(T, T)>) -> Vec<(T, T)> {
648    if ranges.is_empty() {
649        return vec![];
650    }
651
652    ranges.sort_by(|a, b| a.0.cmp(&b.0));
653
654    let mut merged = vec![ranges[0]];
655
656    for (start, end) in ranges.into_iter().skip(1) {
657        let last = merged
658            .last_mut()
659            .expect("merged initialized with at least one element");
660        if start <= last.1 {
661            last.1 = std::cmp::max(last.1, end);
662        } else {
663            merged.push((start, end));
664        }
665    }
666
667    merged
668}
669
670/// Check if a unit is a date unit (operates on date components)
671pub fn is_date_unit(unit: &str) -> bool {
672    DATE_UNITS.contains(&unit.to_lowercase().as_str())
673}
674
675/// Applies an offset to a given integer literal expression for array indexing.
676///
677/// This is used for dialects that have different array indexing conventions
678/// (0-based vs 1-based indexing).
679///
680/// # Arguments
681/// * `expression` - The index expression (should be an integer literal)
682/// * `offset` - The offset to apply (e.g., 1 for 0-based to 1-based conversion)
683///
684/// # Returns
685/// The expression with the offset applied if it's an integer literal,
686/// otherwise returns the original expression unchanged.
687///
688/// # Example
689///
690/// ```
691/// use polyglot_sql::helper::apply_index_offset;
692///
693/// // Convert 0-based index to 1-based
694/// assert_eq!(apply_index_offset("0", 1), Some("1".to_string()));
695/// assert_eq!(apply_index_offset("5", 1), Some("6".to_string()));
696///
697/// // Not an integer, return None
698/// assert_eq!(apply_index_offset("col", 1), None);
699/// ```
700pub fn apply_index_offset(expression: &str, offset: i64) -> Option<String> {
701    if offset == 0 {
702        return Some(expression.to_string());
703    }
704
705    // Try to parse as integer
706    if let Ok(value) = expression.parse::<i64>() {
707        return Some((value + offset).to_string());
708    }
709
710    // Not an integer literal, can't apply offset
711    None
712}
713
714/// A mapping where all keys return the same value.
715///
716/// This is an optimization for cases like column qualification where many columns
717/// from the same table all map to the same table name. Instead of storing
718/// N copies of the value, we store it once.
719///
720/// # Example
721///
722/// ```
723/// use polyglot_sql::helper::SingleValuedMapping;
724/// use std::collections::HashSet;
725///
726/// let columns = HashSet::from(["id".to_string(), "name".to_string(), "email".to_string()]);
727/// let mapping = SingleValuedMapping::new(columns, "users".to_string());
728///
729/// assert_eq!(mapping.get(&"id".to_string()), Some(&"users".to_string()));
730/// assert_eq!(mapping.get(&"name".to_string()), Some(&"users".to_string()));
731/// assert_eq!(mapping.get(&"unknown".to_string()), None);
732/// assert_eq!(mapping.len(), 3);
733/// ```
734#[derive(Debug, Clone)]
735pub struct SingleValuedMapping<K, V>
736where
737    K: Eq + Hash,
738{
739    keys: HashSet<K>,
740    value: V,
741}
742
743impl<K, V> SingleValuedMapping<K, V>
744where
745    K: Eq + Hash,
746{
747    /// Create a new SingleValuedMapping from a set of keys and a single value.
748    pub fn new(keys: HashSet<K>, value: V) -> Self {
749        Self { keys, value }
750    }
751
752    /// Create from an iterator of keys and a single value.
753    pub fn from_iter<I: IntoIterator<Item = K>>(keys: I, value: V) -> Self {
754        Self {
755            keys: keys.into_iter().collect(),
756            value,
757        }
758    }
759
760    /// Get the value for a key, if the key exists.
761    pub fn get(&self, key: &K) -> Option<&V> {
762        if self.keys.contains(key) {
763            Some(&self.value)
764        } else {
765            None
766        }
767    }
768
769    /// Check if a key exists in the mapping.
770    pub fn contains_key(&self, key: &K) -> bool {
771        self.keys.contains(key)
772    }
773
774    /// Get the number of keys in the mapping.
775    pub fn len(&self) -> usize {
776        self.keys.len()
777    }
778
779    /// Check if the mapping is empty.
780    pub fn is_empty(&self) -> bool {
781        self.keys.is_empty()
782    }
783
784    /// Iterate over all keys.
785    pub fn keys(&self) -> impl Iterator<Item = &K> {
786        self.keys.iter()
787    }
788
789    /// Get a reference to the single value.
790    pub fn value(&self) -> &V {
791        &self.value
792    }
793
794    /// Iterate over key-value pairs (all values are the same).
795    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
796        self.keys.iter().map(move |k| (k, &self.value))
797    }
798}
799
800/// Convert a boolean-like string to an actual boolean.
801///
802/// # Example
803///
804/// ```
805/// use polyglot_sql::helper::to_bool;
806///
807/// assert_eq!(to_bool("true"), Some(true));
808/// assert_eq!(to_bool("1"), Some(true));
809/// assert_eq!(to_bool("false"), Some(false));
810/// assert_eq!(to_bool("0"), Some(false));
811/// assert_eq!(to_bool("maybe"), None);
812/// ```
813pub fn to_bool(value: &str) -> Option<bool> {
814    let lower = value.to_lowercase();
815    match lower.as_str() {
816        "true" | "1" => Some(true),
817        "false" | "0" => Some(false),
818        _ => None,
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825
826    #[test]
827    fn test_seq_get() {
828        let v = vec![1, 2, 3, 4, 5];
829        assert_eq!(seq_get(&v, 0), Some(&1));
830        assert_eq!(seq_get(&v, 4), Some(&5));
831        assert_eq!(seq_get(&v, 5), None);
832        assert_eq!(seq_get(&v, -1), Some(&5));
833        assert_eq!(seq_get(&v, -5), Some(&1));
834        assert_eq!(seq_get(&v, -6), None);
835
836        let empty: Vec<i32> = vec![];
837        assert_eq!(seq_get(&empty, 0), None);
838        assert_eq!(seq_get(&empty, -1), None);
839    }
840
841    #[test]
842    fn test_csv() {
843        assert_eq!(csv(&["a", "b", "c"], ", "), "a, b, c");
844        assert_eq!(csv(&["a", "", "c"], ", "), "a, c");
845        assert_eq!(csv(&["", "", ""], ", "), "");
846        assert_eq!(csv(&["a"], ", "), "a");
847    }
848
849    #[test]
850    fn test_while_changing() {
851        // Halve until odd
852        let result = while_changing(16, |n| if n % 2 == 0 { n / 2 } else { n });
853        assert_eq!(result, 1);
854
855        // Already at fixed point
856        let result = while_changing(5, |n| if n % 2 == 0 { n / 2 } else { n });
857        assert_eq!(result, 5);
858    }
859
860    #[test]
861    fn test_tsort() {
862        let mut dag = HashMap::new();
863        dag.insert("a", HashSet::from(["b", "c"]));
864        dag.insert("b", HashSet::from(["c"]));
865        dag.insert("c", HashSet::new());
866
867        let sorted = tsort(dag).unwrap();
868        assert_eq!(sorted, vec!["c", "b", "a"]);
869    }
870
871    #[test]
872    fn test_tsort_cycle() {
873        let mut dag = HashMap::new();
874        dag.insert("a", HashSet::from(["b"]));
875        dag.insert("b", HashSet::from(["a"]));
876
877        let result = tsort(dag);
878        assert!(result.is_err());
879    }
880
881    #[test]
882    fn test_find_new_name() {
883        let taken = HashSet::from(["col".to_string(), "col_2".to_string()]);
884        assert_eq!(find_new_name(&taken, "col"), "col_3");
885        assert_eq!(find_new_name(&taken, "other"), "other");
886
887        let empty = HashSet::new();
888        assert_eq!(find_new_name(&empty, "col"), "col");
889    }
890
891    #[test]
892    fn test_name_sequence() {
893        let mut gen = name_sequence("a");
894        assert_eq!(gen(), "a0");
895        assert_eq!(gen(), "a1");
896        assert_eq!(gen(), "a2");
897    }
898
899    #[test]
900    fn test_is_int() {
901        assert!(is_int("123"));
902        assert!(is_int("-456"));
903        assert!(is_int("0"));
904        assert!(!is_int("12.34"));
905        assert!(!is_int("abc"));
906        assert!(!is_int(""));
907    }
908
909    #[test]
910    fn test_is_float() {
911        assert!(is_float("12.34"));
912        assert!(is_float("123"));
913        assert!(is_float("-1.5e10"));
914        assert!(is_float("0.0"));
915        assert!(!is_float("abc"));
916        assert!(!is_float(""));
917    }
918
919    #[test]
920    fn test_is_iso_date() {
921        assert!(is_iso_date("2023-01-15"));
922        assert!(is_iso_date("2024-02-29")); // Leap year
923        assert!(!is_iso_date("2023-02-29")); // Not a leap year
924        assert!(!is_iso_date("01-15-2023"));
925        assert!(!is_iso_date("2023-13-01")); // Invalid month
926        assert!(!is_iso_date("2023-01-32")); // Invalid day
927        assert!(!is_iso_date("not a date"));
928    }
929
930    #[test]
931    fn test_is_iso_datetime() {
932        assert!(is_iso_datetime("2023-01-15T10:30:00"));
933        assert!(is_iso_datetime("2023-01-15 10:30:00"));
934        assert!(is_iso_datetime("2023-01-15T10:30:00.123456"));
935        assert!(is_iso_datetime("2023-01-15T10:30:00+00:00"));
936        assert!(is_iso_datetime("2023-01-15"));
937        assert!(!is_iso_datetime("not a datetime"));
938        assert!(!is_iso_datetime("2023-01-15X10:30:00")); // Invalid separator
939    }
940
941    #[test]
942    fn test_camel_to_snake_case() {
943        assert_eq!(camel_to_snake_case("camelCase"), "CAMEL_CASE");
944        assert_eq!(camel_to_snake_case("PascalCase"), "PASCAL_CASE");
945        assert_eq!(camel_to_snake_case("simple"), "SIMPLE");
946    }
947
948    #[test]
949    fn test_snake_to_camel_case() {
950        assert_eq!(snake_to_camel_case("snake_case"), "SnakeCase");
951        assert_eq!(snake_to_camel_case("my_http_server"), "MyHttpServer");
952        assert_eq!(snake_to_camel_case("simple"), "Simple");
953    }
954
955    #[test]
956    fn test_split_num_words() {
957        assert_eq!(
958            split_num_words("db.table", ".", 3, true),
959            vec![None, Some("db".to_string()), Some("table".to_string())]
960        );
961        assert_eq!(
962            split_num_words("db.table", ".", 3, false),
963            vec![Some("db".to_string()), Some("table".to_string()), None]
964        );
965        assert_eq!(
966            split_num_words("catalog.db.table", ".", 3, true),
967            vec![
968                Some("catalog".to_string()),
969                Some("db".to_string()),
970                Some("table".to_string())
971            ]
972        );
973        assert_eq!(
974            split_num_words("db.table", ".", 1, true),
975            vec![Some("db".to_string()), Some("table".to_string())]
976        );
977    }
978
979    #[test]
980    fn test_merge_ranges() {
981        assert_eq!(merge_ranges(vec![(1, 3), (2, 6)]), vec![(1, 6)]);
982        assert_eq!(
983            merge_ranges(vec![(1, 3), (2, 6), (8, 10)]),
984            vec![(1, 6), (8, 10)]
985        );
986        assert_eq!(merge_ranges(vec![(1, 5), (2, 3)]), vec![(1, 5)]);
987        assert_eq!(merge_ranges::<i32>(vec![]), vec![]);
988    }
989
990    #[test]
991    fn test_is_date_unit() {
992        assert!(is_date_unit("day"));
993        assert!(is_date_unit("MONTH"));
994        assert!(is_date_unit("Year"));
995        assert!(!is_date_unit("hour"));
996        assert!(!is_date_unit("minute"));
997    }
998
999    #[test]
1000    fn test_apply_index_offset() {
1001        // Basic offset application
1002        assert_eq!(apply_index_offset("0", 1), Some("1".to_string()));
1003        assert_eq!(apply_index_offset("5", 1), Some("6".to_string()));
1004        assert_eq!(apply_index_offset("10", -1), Some("9".to_string()));
1005
1006        // No offset
1007        assert_eq!(apply_index_offset("5", 0), Some("5".to_string()));
1008
1009        // Negative numbers
1010        assert_eq!(apply_index_offset("-1", 1), Some("0".to_string()));
1011
1012        // Not an integer - returns None
1013        assert_eq!(apply_index_offset("col", 1), None);
1014        assert_eq!(apply_index_offset("1.5", 1), None);
1015        assert_eq!(apply_index_offset("abc", 1), None);
1016    }
1017
1018    #[test]
1019    fn test_single_valued_mapping() {
1020        let columns = HashSet::from(["id".to_string(), "name".to_string(), "email".to_string()]);
1021        let mapping = SingleValuedMapping::new(columns, "users".to_string());
1022
1023        // Get existing keys
1024        assert_eq!(mapping.get(&"id".to_string()), Some(&"users".to_string()));
1025        assert_eq!(mapping.get(&"name".to_string()), Some(&"users".to_string()));
1026        assert_eq!(
1027            mapping.get(&"email".to_string()),
1028            Some(&"users".to_string())
1029        );
1030
1031        // Get non-existing key
1032        assert_eq!(mapping.get(&"unknown".to_string()), None);
1033
1034        // Length
1035        assert_eq!(mapping.len(), 3);
1036        assert!(!mapping.is_empty());
1037
1038        // Contains key
1039        assert!(mapping.contains_key(&"id".to_string()));
1040        assert!(!mapping.contains_key(&"unknown".to_string()));
1041
1042        // Value access
1043        assert_eq!(mapping.value(), &"users".to_string());
1044    }
1045
1046    #[test]
1047    fn test_single_valued_mapping_from_iter() {
1048        let mapping = SingleValuedMapping::from_iter(vec!["a".to_string(), "b".to_string()], 42);
1049
1050        assert_eq!(mapping.get(&"a".to_string()), Some(&42));
1051        assert_eq!(mapping.get(&"b".to_string()), Some(&42));
1052        assert_eq!(mapping.len(), 2);
1053    }
1054
1055    #[test]
1056    fn test_to_bool() {
1057        assert_eq!(to_bool("true"), Some(true));
1058        assert_eq!(to_bool("TRUE"), Some(true));
1059        assert_eq!(to_bool("True"), Some(true));
1060        assert_eq!(to_bool("1"), Some(true));
1061
1062        assert_eq!(to_bool("false"), Some(false));
1063        assert_eq!(to_bool("FALSE"), Some(false));
1064        assert_eq!(to_bool("False"), Some(false));
1065        assert_eq!(to_bool("0"), Some(false));
1066
1067        assert_eq!(to_bool("maybe"), None);
1068        assert_eq!(to_bool("yes"), None);
1069        assert_eq!(to_bool("no"), None);
1070    }
1071}