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