Skip to main content

fsqlite_ext_misc/
lib.rs

1//! Miscellaneous extensions: generate_series, decimal, uuid (§14.7).
2//!
3//! Provides three independent extension families:
4//!
5//! 1. **generate_series(START, STOP \[, STEP\])**: virtual table that generates
6//!    a sequence of integers, commonly used in joins and CTEs.
7//!
8//! 2. **Decimal arithmetic**: exact string-based decimal operations that avoid
9//!    floating-point precision loss. Functions: `decimal`, `decimal_add`,
10//!    `decimal_sub`, `decimal_mul`, `decimal_cmp`.
11//!
12//! 3. **UUID generation**: `uuid()` generates random UUID v4 strings,
13//!    `uuid_str` converts blob to string, `uuid_blob` converts string to blob.
14
15use std::cmp::Ordering;
16use std::sync::Arc;
17
18use fsqlite_error::{FrankenError, Result};
19use fsqlite_func::FunctionRegistry;
20use fsqlite_func::scalar::ScalarFunction;
21use fsqlite_func::vtab::{ColumnContext, IndexInfo, VirtualTable, VirtualTableCursor};
22use fsqlite_types::SqliteValue;
23use fsqlite_types::cx::Cx;
24use rand::RngCore;
25use tracing::{debug, info};
26
27#[must_use]
28pub const fn extension_name() -> &'static str {
29    "misc"
30}
31
32// ══════════════════════════════════════════════════════════════════════
33// generate_series virtual table
34// ══════════════════════════════════════════════════════════════════════
35
36/// Virtual table that generates a sequence of integers.
37///
38/// Usage: `SELECT value FROM generate_series(1, 10)` produces rows 1..=10.
39/// Optional third argument specifies step (default 1).
40pub struct GenerateSeriesTable;
41
42impl VirtualTable for GenerateSeriesTable {
43    type Cursor = GenerateSeriesCursor;
44
45    fn create(_cx: &Cx, _args: &[&str]) -> Result<Self> {
46        Ok(Self)
47    }
48
49    fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
50        Ok(Self)
51    }
52
53    fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
54        // generate_series accepts 2-3 equality constraints on hidden columns
55        // start (col=1), stop (col=2), step (col=3)
56        info.estimated_cost = 1.0;
57        info.estimated_rows = 1000;
58        Ok(())
59    }
60
61    fn open(&self) -> Result<Self::Cursor> {
62        Ok(GenerateSeriesCursor {
63            current: 0,
64            start: 0,
65            stop: 0,
66            step: 1,
67            done: true,
68        })
69    }
70}
71
72/// Cursor for iterating over a generated integer series.
73pub struct GenerateSeriesCursor {
74    current: i64,
75    start: i64,
76    stop: i64,
77    step: i64,
78    done: bool,
79}
80
81impl GenerateSeriesCursor {
82    /// Initialize the cursor from explicit start/stop/step values.
83    #[allow(clippy::similar_names)]
84    pub fn init(&mut self, start: i64, stop: i64, step: i64) -> Result<()> {
85        if step == 0 {
86            return Err(FrankenError::internal(
87                "generate_series: step cannot be zero",
88            ));
89        }
90        self.start = start;
91        self.current = start;
92        self.stop = stop;
93        self.step = step;
94        self.done = if step > 0 { start > stop } else { start < stop };
95        debug!(start, stop, step, "generate_series: initialized cursor");
96        Ok(())
97    }
98}
99
100impl VirtualTableCursor for GenerateSeriesCursor {
101    fn filter(
102        &mut self,
103        _cx: &Cx,
104        _idx_num: i32,
105        _idx_str: Option<&str>,
106        args: &[SqliteValue],
107    ) -> Result<()> {
108        #[allow(clippy::similar_names)]
109        let start = args.first().and_then(SqliteValue::as_integer).unwrap_or(0);
110        let end = args.get(1).and_then(SqliteValue::as_integer).unwrap_or(0);
111        let step = args.get(2).and_then(SqliteValue::as_integer).unwrap_or(1);
112        self.init(start, end, step)
113    }
114
115    fn next(&mut self, _cx: &Cx) -> Result<()> {
116        if self.done {
117            return Ok(());
118        }
119        // Use checked_add to detect overflow and terminate gracefully.
120        // saturating_add would cause an infinite loop when current hits i64::MAX/MIN
121        // because `current > stop` would remain false while current stays saturated.
122        match self.current.checked_add(self.step) {
123            Some(next_val) => {
124                self.current = next_val;
125                self.done = if self.step > 0 {
126                    self.current > self.stop
127                } else {
128                    self.current < self.stop
129                };
130            }
131            None => {
132                // Overflow — we've exhausted the range, terminate iteration
133                self.done = true;
134            }
135        }
136        Ok(())
137    }
138
139    fn eof(&self) -> bool {
140        self.done
141    }
142
143    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
144        let val = match col {
145            0 => SqliteValue::Integer(self.current), // value
146            1 => SqliteValue::Integer(self.start),   // start
147            2 => SqliteValue::Integer(self.stop),    // stop
148            3 => SqliteValue::Integer(self.step),    // step
149            _ => SqliteValue::Null,
150        };
151        ctx.set_value(val);
152        Ok(())
153    }
154
155    fn rowid(&self) -> Result<i64> {
156        Ok(self.current)
157    }
158}
159
160// ══════════════════════════════════════════════════════════════════════
161// Decimal extension — exact string-based arithmetic
162// ══════════════════════════════════════════════════════════════════════
163
164/// Normalize a decimal string to canonical form.
165///
166/// Strips leading zeros (except the one before the decimal point),
167/// ensures there's at least a "0" if the integer part is empty.
168fn decimal_normalize(s: &str) -> Option<String> {
169    let (negative, int_digits, frac_digits) = parse_decimal(s)?;
170    Some(format_decimal(negative, &int_digits, &frac_digits))
171}
172
173/// Parse a decimal string into (negative, integer_digits, fractional_digits).
174fn parse_decimal(s: &str) -> Option<(bool, Vec<u8>, Vec<u8>)> {
175    let s = s.trim();
176    let (negative, s) = if let Some(stripped) = s.strip_prefix('-') {
177        (true, stripped)
178    } else if let Some(stripped) = s.strip_prefix('+') {
179        (false, stripped)
180    } else {
181        (false, s)
182    };
183
184    if s.is_empty() {
185        return None;
186    }
187
188    let (int_str, frac_str) = match s.split_once('.') {
189        Some((i, f)) => (i, f),
190        None => (s, ""),
191    };
192
193    if !int_str.is_empty() && !int_str.bytes().all(|b| b.is_ascii_digit()) {
194        return None;
195    }
196    if !frac_str.is_empty() && !frac_str.bytes().all(|b| b.is_ascii_digit()) {
197        return None;
198    }
199
200    let int_digits: Vec<u8> = int_str.bytes().map(|b| b - b'0').collect();
201    let frac_digits: Vec<u8> = frac_str.bytes().map(|b| b - b'0').collect();
202
203    Some((negative, int_digits, frac_digits))
204}
205
206/// Add two non-negative decimal digit sequences (aligned by decimal point).
207///
208/// Returns (integer_digits, fractional_digits) of the sum.
209fn add_unsigned(int_a: &[u8], frac_a: &[u8], int_b: &[u8], frac_b: &[u8]) -> (Vec<u8>, Vec<u8>) {
210    // Pad fractional parts to equal length
211    let frac_len = frac_a.len().max(frac_b.len());
212    let mut fa: Vec<u8> = frac_a.to_vec();
213    fa.resize(frac_len, 0);
214    let mut fb: Vec<u8> = frac_b.to_vec();
215    fb.resize(frac_len, 0);
216
217    // Add fractional part right-to-left
218    let mut carry: u8 = 0;
219    let mut frac_result = vec![0u8; frac_len];
220    for i in (0..frac_len).rev() {
221        let sum = fa[i] + fb[i] + carry;
222        frac_result[i] = sum % 10;
223        carry = sum / 10;
224    }
225
226    // Pad integer parts to equal length
227    let int_len = int_a.len().max(int_b.len());
228    let mut ia = vec![0u8; int_len - int_a.len()];
229    ia.extend_from_slice(int_a);
230    let mut ib = vec![0u8; int_len - int_b.len()];
231    ib.extend_from_slice(int_b);
232
233    // Add integer part right-to-left
234    let mut int_result = vec![0u8; int_len];
235    for i in (0..int_len).rev() {
236        let sum = ia[i] + ib[i] + carry;
237        int_result[i] = sum % 10;
238        carry = sum / 10;
239    }
240    if carry > 0 {
241        int_result.insert(0, carry);
242    }
243
244    (int_result, frac_result)
245}
246
247/// Subtract unsigned b from unsigned a (assumes a >= b).
248fn sub_unsigned(int_a: &[u8], frac_a: &[u8], int_b: &[u8], frac_b: &[u8]) -> (Vec<u8>, Vec<u8>) {
249    let frac_len = frac_a.len().max(frac_b.len());
250    let mut fa: Vec<u8> = frac_a.to_vec();
251    fa.resize(frac_len, 0);
252    let mut fb: Vec<u8> = frac_b.to_vec();
253    fb.resize(frac_len, 0);
254
255    let mut borrow: i16 = 0;
256    let mut frac_result = vec![0u8; frac_len];
257    for i in (0..frac_len).rev() {
258        let diff = i16::from(fa[i]) - i16::from(fb[i]) - borrow;
259        if diff < 0 {
260            frac_result[i] = u8::try_from(diff + 10).unwrap_or(0);
261            borrow = 1;
262        } else {
263            frac_result[i] = u8::try_from(diff).unwrap_or(0);
264            borrow = 0;
265        }
266    }
267
268    let int_len = int_a.len().max(int_b.len());
269    let mut ia = vec![0u8; int_len - int_a.len()];
270    ia.extend_from_slice(int_a);
271    let mut ib = vec![0u8; int_len - int_b.len()];
272    ib.extend_from_slice(int_b);
273
274    let mut int_result = vec![0u8; int_len];
275    for i in (0..int_len).rev() {
276        let diff = i16::from(ia[i]) - i16::from(ib[i]) - borrow;
277        if diff < 0 {
278            int_result[i] = u8::try_from(diff + 10).unwrap_or(0);
279            borrow = 1;
280        } else {
281            int_result[i] = u8::try_from(diff).unwrap_or(0);
282            borrow = 0;
283        }
284    }
285
286    (int_result, frac_result)
287}
288
289/// Compare two unsigned decimal values.
290fn cmp_unsigned(int_a: &[u8], frac_a: &[u8], int_b: &[u8], frac_b: &[u8]) -> Ordering {
291    // Compare by number of significant integer digits first
292    let ia = strip_leading_zeros(int_a);
293    let ib = strip_leading_zeros(int_b);
294
295    match ia.len().cmp(&ib.len()) {
296        Ordering::Equal => {}
297        ord => return ord,
298    }
299
300    // Same length integer parts — compare digit by digit
301    for (a, b) in ia.iter().zip(ib.iter()) {
302        match a.cmp(b) {
303            Ordering::Equal => {}
304            ord => return ord,
305        }
306    }
307
308    // Integer parts equal — compare fractional parts
309    let frac_len = frac_a.len().max(frac_b.len());
310    for i in 0..frac_len {
311        let a = frac_a.get(i).copied().unwrap_or(0);
312        let b = frac_b.get(i).copied().unwrap_or(0);
313        match a.cmp(&b) {
314            Ordering::Equal => {}
315            ord => return ord,
316        }
317    }
318
319    Ordering::Equal
320}
321
322fn strip_leading_zeros(digits: &[u8]) -> &[u8] {
323    let start = digits.iter().position(|&d| d != 0).unwrap_or(digits.len());
324    if start == digits.len() {
325        // All zeros — return single zero
326        &digits[digits.len().saturating_sub(1)..]
327    } else {
328        &digits[start..]
329    }
330}
331
332/// Format digit vectors back to a decimal string.
333fn format_decimal(negative: bool, int_digits: &[u8], frac_digits: &[u8]) -> String {
334    let int_str: String = strip_leading_zeros(int_digits)
335        .iter()
336        .map(|d| char::from(b'0' + d))
337        .collect();
338    let int_str = if int_str.is_empty() {
339        "0".to_owned()
340    } else {
341        int_str
342    };
343
344    // Trim trailing zeros from fractional part
345    let frac_end = frac_digits
346        .iter()
347        .rposition(|&d| d != 0)
348        .map_or(0, |p| p + 1);
349    let frac = &frac_digits[..frac_end];
350
351    let result = if frac.is_empty() {
352        int_str
353    } else {
354        let frac_str: String = frac.iter().map(|d| char::from(b'0' + d)).collect();
355        format!("{int_str}.{frac_str}")
356    };
357
358    if negative && result != "0" {
359        format!("-{result}")
360    } else {
361        result
362    }
363}
364
365/// Perform decimal addition: a + b.
366fn decimal_add_impl(a: &str, b: &str) -> Option<String> {
367    let (neg_a, int_a, frac_a) = parse_decimal(a)?;
368    let (neg_b, int_b, frac_b) = parse_decimal(b)?;
369
370    let result = match (neg_a, neg_b) {
371        (false, false) => {
372            let (ir, fr) = add_unsigned(&int_a, &frac_a, &int_b, &frac_b);
373            format_decimal(false, &ir, &fr)
374        }
375        (true, true) => {
376            let (ir, fr) = add_unsigned(&int_a, &frac_a, &int_b, &frac_b);
377            format_decimal(true, &ir, &fr)
378        }
379        (false, true) => {
380            // a - |b|
381            match cmp_unsigned(&int_a, &frac_a, &int_b, &frac_b) {
382                Ordering::Less => {
383                    let (ir, fr) = sub_unsigned(&int_b, &frac_b, &int_a, &frac_a);
384                    format_decimal(true, &ir, &fr)
385                }
386                Ordering::Equal => "0".to_owned(),
387                Ordering::Greater => {
388                    let (ir, fr) = sub_unsigned(&int_a, &frac_a, &int_b, &frac_b);
389                    format_decimal(false, &ir, &fr)
390                }
391            }
392        }
393        (true, false) => {
394            // -|a| + b = b - |a|
395            match cmp_unsigned(&int_b, &frac_b, &int_a, &frac_a) {
396                Ordering::Less => {
397                    let (ir, fr) = sub_unsigned(&int_a, &frac_a, &int_b, &frac_b);
398                    format_decimal(true, &ir, &fr)
399                }
400                Ordering::Equal => "0".to_owned(),
401                Ordering::Greater => {
402                    let (ir, fr) = sub_unsigned(&int_b, &frac_b, &int_a, &frac_a);
403                    format_decimal(false, &ir, &fr)
404                }
405            }
406        }
407    };
408    Some(result)
409}
410
411/// Perform decimal subtraction: a - b.
412fn decimal_sub_impl(a: &str, b: &str) -> Option<String> {
413    let b_str = b.trim();
414    if b_str.is_empty() {
415        return None;
416    }
417    // a - b = a + (-b)
418    let neg_b = if let Some(stripped) = b_str.strip_prefix('-') {
419        stripped.to_owned()
420    } else if let Some(stripped) = b_str.strip_prefix('+') {
421        format!("-{stripped}")
422    } else {
423        format!("-{b_str}")
424    };
425    decimal_add_impl(a, &neg_b)
426}
427
428/// Perform decimal multiplication: a * b.
429fn decimal_mul_impl(a: &str, b: &str) -> Option<String> {
430    let (neg_a, int_a, frac_a) = parse_decimal(a)?;
431    let (neg_b, int_b, frac_b) = parse_decimal(b)?;
432
433    let result_negative = neg_a != neg_b;
434    let frac_places = frac_a.len() + frac_b.len();
435
436    // Combine integer and fractional into a single digit sequence
437    let mut digits_a: Vec<u8> = int_a;
438    digits_a.extend_from_slice(&frac_a);
439    let mut digits_b: Vec<u8> = int_b;
440    digits_b.extend_from_slice(&frac_b);
441
442    // Grade-school multiplication
443    let len_a = digits_a.len();
444    let len_b = digits_b.len();
445    let mut product = vec![0u16; len_a + len_b];
446
447    for (i, &da) in digits_a.iter().enumerate().rev() {
448        for (j, &db) in digits_b.iter().enumerate().rev() {
449            let pos = i + j + 1;
450            product[pos] += u16::from(da) * u16::from(db);
451            product[i + j] += product[pos] / 10;
452            product[pos] %= 10;
453        }
454    }
455
456    // Convert to u8
457    // Each cell is guaranteed to be 0-9 after carry propagation
458    let product: Vec<u8> = product
459        .iter()
460        .map(|&d| u8::try_from(d).unwrap_or(0))
461        .collect();
462
463    // Split at decimal point
464    let total_len = product.len();
465    let int_end = total_len.saturating_sub(frac_places);
466    let int_digits = &product[..int_end];
467    let frac_digits = &product[int_end..];
468
469    Some(format_decimal(result_negative, int_digits, frac_digits))
470}
471
472/// Compare two decimal values, returning -1, 0, or 1.
473fn decimal_cmp_impl(a: &str, b: &str) -> Option<i64> {
474    let (neg_a, int_a, frac_a) = parse_decimal(a)?;
475    let (neg_b, int_b, frac_b) = parse_decimal(b)?;
476
477    let a_is_zero = int_a.iter().all(|&d| d == 0) && frac_a.iter().all(|&d| d == 0);
478    let b_is_zero = int_b.iter().all(|&d| d == 0) && frac_b.iter().all(|&d| d == 0);
479
480    if a_is_zero && b_is_zero {
481        return Some(0);
482    }
483
484    let result = match (neg_a && !a_is_zero, neg_b && !b_is_zero) {
485        (true, false) => -1,
486        (false, true) => 1,
487        (true, true) => {
488            // Both negative — larger magnitude is smaller
489            match cmp_unsigned(&int_a, &frac_a, &int_b, &frac_b) {
490                Ordering::Less => 1,
491                Ordering::Equal => 0,
492                Ordering::Greater => -1,
493            }
494        }
495        (false, false) => match cmp_unsigned(&int_a, &frac_a, &int_b, &frac_b) {
496            Ordering::Less => -1,
497            Ordering::Equal => 0,
498            Ordering::Greater => 1,
499        },
500    };
501    Some(result)
502}
503
504// ── Decimal scalar functions ─────────────────────────────────────────
505
506/// `decimal(X)` — convert a value to canonical decimal text.
507pub struct DecimalFunc;
508
509impl ScalarFunction for DecimalFunc {
510    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
511        if args.len() != 1 {
512            return Err(FrankenError::internal(
513                "decimal requires exactly 1 argument",
514            ));
515        }
516        if args[0].is_null() {
517            return Ok(SqliteValue::Null);
518        }
519        let text = args[0].to_text();
520        Ok(SqliteValue::Text(Arc::from(
521            decimal_normalize(&text)
522                .unwrap_or_else(|| text.clone())
523                .as_str(),
524        )))
525    }
526
527    fn num_args(&self) -> i32 {
528        1
529    }
530
531    fn name(&self) -> &'static str {
532        "decimal"
533    }
534}
535
536/// `decimal_add(X, Y)` — exact decimal addition.
537pub struct DecimalAddFunc;
538
539impl ScalarFunction for DecimalAddFunc {
540    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
541        if args.len() != 2 {
542            return Err(FrankenError::internal(
543                "decimal_add requires exactly 2 arguments",
544            ));
545        }
546        if args[0].is_null() || args[1].is_null() {
547            return Ok(SqliteValue::Null);
548        }
549        let a = args[0].to_text();
550        let b = args[1].to_text();
551        debug!(a = %a, b = %b, "decimal_add invoked");
552        Ok(match decimal_add_impl(&a, &b) {
553            Some(result) => SqliteValue::Text(Arc::from(result)),
554            None => SqliteValue::Null,
555        })
556    }
557
558    fn num_args(&self) -> i32 {
559        2
560    }
561
562    fn name(&self) -> &'static str {
563        "decimal_add"
564    }
565}
566
567/// `decimal_sub(X, Y)` — exact decimal subtraction.
568pub struct DecimalSubFunc;
569
570impl ScalarFunction for DecimalSubFunc {
571    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
572        if args.len() != 2 {
573            return Err(FrankenError::internal(
574                "decimal_sub requires exactly 2 arguments",
575            ));
576        }
577        if args[0].is_null() || args[1].is_null() {
578            return Ok(SqliteValue::Null);
579        }
580        let a = args[0].to_text();
581        let b = args[1].to_text();
582        debug!(a = %a, b = %b, "decimal_sub invoked");
583        Ok(match decimal_sub_impl(&a, &b) {
584            Some(result) => SqliteValue::Text(Arc::from(result)),
585            None => SqliteValue::Null,
586        })
587    }
588
589    fn num_args(&self) -> i32 {
590        2
591    }
592
593    fn name(&self) -> &'static str {
594        "decimal_sub"
595    }
596}
597
598/// `decimal_mul(X, Y)` — exact decimal multiplication.
599pub struct DecimalMulFunc;
600
601impl ScalarFunction for DecimalMulFunc {
602    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
603        if args.len() != 2 {
604            return Err(FrankenError::internal(
605                "decimal_mul requires exactly 2 arguments",
606            ));
607        }
608        if args[0].is_null() || args[1].is_null() {
609            return Ok(SqliteValue::Null);
610        }
611        let a = args[0].to_text();
612        let b = args[1].to_text();
613        debug!(a = %a, b = %b, "decimal_mul invoked");
614        Ok(match decimal_mul_impl(&a, &b) {
615            Some(result) => SqliteValue::Text(Arc::from(result)),
616            None => SqliteValue::Null,
617        })
618    }
619
620    fn num_args(&self) -> i32 {
621        2
622    }
623
624    fn name(&self) -> &'static str {
625        "decimal_mul"
626    }
627}
628
629/// `decimal_cmp(X, Y)` — compare two decimals, returning -1, 0, or 1.
630pub struct DecimalCmpFunc;
631
632impl ScalarFunction for DecimalCmpFunc {
633    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
634        if args.len() != 2 {
635            return Err(FrankenError::internal(
636                "decimal_cmp requires exactly 2 arguments",
637            ));
638        }
639        if args[0].is_null() || args[1].is_null() {
640            return Ok(SqliteValue::Null);
641        }
642        let a = args[0].to_text();
643        let b = args[1].to_text();
644        debug!(a = %a, b = %b, "decimal_cmp invoked");
645        Ok(match decimal_cmp_impl(&a, &b) {
646            Some(result) => SqliteValue::Integer(result),
647            None => SqliteValue::Null,
648        })
649    }
650
651    fn num_args(&self) -> i32 {
652        2
653    }
654
655    fn name(&self) -> &'static str {
656        "decimal_cmp"
657    }
658}
659
660// ══════════════════════════════════════════════════════════════════════
661// UUID extension
662// ══════════════════════════════════════════════════════════════════════
663
664/// Generate a random UUID v4 string.
665fn generate_uuid_v4() -> String {
666    let mut bytes = [0u8; 16];
667    rand::thread_rng().fill_bytes(&mut bytes);
668
669    // Set version (4) and variant (10xx)
670    bytes[6] = (bytes[6] & 0x0F) | 0x40; // version 4
671    bytes[8] = (bytes[8] & 0x3F) | 0x80; // variant 10xx
672
673    format!(
674        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
675        bytes[0],
676        bytes[1],
677        bytes[2],
678        bytes[3],
679        bytes[4],
680        bytes[5],
681        bytes[6],
682        bytes[7],
683        bytes[8],
684        bytes[9],
685        bytes[10],
686        bytes[11],
687        bytes[12],
688        bytes[13],
689        bytes[14],
690        bytes[15],
691    )
692}
693
694fn decode_uuid_nibble(byte: u8, position: usize) -> Result<u8> {
695    match byte {
696        b'0'..=b'9' => Ok(byte - b'0'),
697        b'a'..=b'f' => Ok(byte - b'a' + 10),
698        b'A'..=b'F' => Ok(byte - b'A' + 10),
699        _ => Err(FrankenError::internal(format!(
700            "invalid UUID character at position {position}: {byte:?}",
701        ))),
702    }
703}
704
705fn decode_uuid_hex_pair(bytes: &[u8], index: usize) -> Result<u8> {
706    let high = decode_uuid_nibble(bytes[index], index)?;
707    let low = decode_uuid_nibble(bytes[index + 1], index + 1)?;
708    Ok((high << 4) | low)
709}
710
711/// Parse a UUID string into 16 bytes.
712fn uuid_str_to_blob(s: &str) -> Result<Vec<u8>> {
713    let ascii = s.as_bytes();
714    let hex_digits: Vec<u8> = match ascii.len() {
715        32 => ascii.to_vec(),
716        36 => {
717            for hyphen_index in [8usize, 13, 18, 23] {
718                if ascii[hyphen_index] != b'-' {
719                    return Err(FrankenError::internal(format!(
720                        "invalid UUID string: expected '-' at position {hyphen_index}",
721                    )));
722                }
723            }
724
725            let mut digits = Vec::with_capacity(32);
726            for (index, byte) in ascii.iter().copied().enumerate() {
727                if matches!(index, 8 | 13 | 18 | 23) {
728                    continue;
729                }
730                digits.push(byte);
731            }
732            digits
733        }
734        len => {
735            return Err(FrankenError::internal(format!(
736                "invalid UUID string length {len}: expected 32 or 36 characters",
737            )));
738        }
739    };
740
741    let mut bytes = Vec::with_capacity(16);
742    for i in (0..hex_digits.len()).step_by(2) {
743        bytes.push(decode_uuid_hex_pair(&hex_digits, i)?);
744    }
745    Ok(bytes)
746}
747
748/// Format 16 bytes as a UUID string.
749fn blob_to_uuid_str(bytes: &[u8]) -> Result<String> {
750    if bytes.len() != 16 {
751        return Err(FrankenError::internal(format!(
752            "uuid_str: expected 16-byte blob, got {} bytes",
753            bytes.len()
754        )));
755    }
756    Ok(format!(
757        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
758        bytes[0],
759        bytes[1],
760        bytes[2],
761        bytes[3],
762        bytes[4],
763        bytes[5],
764        bytes[6],
765        bytes[7],
766        bytes[8],
767        bytes[9],
768        bytes[10],
769        bytes[11],
770        bytes[12],
771        bytes[13],
772        bytes[14],
773        bytes[15],
774    ))
775}
776
777// ── UUID scalar functions ────────────────────────────────────────────
778
779/// `uuid()` — generate a random UUID v4 string.
780pub struct UuidFunc;
781
782impl ScalarFunction for UuidFunc {
783    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
784        if !args.is_empty() {
785            return Err(FrankenError::internal("uuid takes no arguments"));
786        }
787        let uuid = generate_uuid_v4();
788        debug!(uuid = %uuid, "uuid() generated");
789        Ok(SqliteValue::Text(Arc::from(uuid)))
790    }
791
792    fn is_deterministic(&self) -> bool {
793        false // each call returns a new UUID
794    }
795
796    fn num_args(&self) -> i32 {
797        0
798    }
799
800    fn name(&self) -> &'static str {
801        "uuid"
802    }
803}
804
805/// `uuid_str(X)` — convert a 16-byte UUID blob to its string representation.
806pub struct UuidStrFunc;
807
808impl ScalarFunction for UuidStrFunc {
809    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
810        if args.len() != 1 {
811            return Err(FrankenError::internal(
812                "uuid_str requires exactly 1 argument",
813            ));
814        }
815        if args[0].is_null() {
816            return Ok(SqliteValue::Null);
817        }
818        match &args[0] {
819            SqliteValue::Blob(b) => {
820                let s = blob_to_uuid_str(b)?;
821                Ok(SqliteValue::Text(Arc::from(s)))
822            }
823            SqliteValue::Text(s) => {
824                // If already a string, normalize it
825                let blob = uuid_str_to_blob(s)?;
826                let normalized = blob_to_uuid_str(&blob)?;
827                Ok(SqliteValue::Text(Arc::from(normalized)))
828            }
829            _ => Err(FrankenError::internal(
830                "uuid_str: argument must be a blob or text",
831            )),
832        }
833    }
834
835    fn num_args(&self) -> i32 {
836        1
837    }
838
839    fn name(&self) -> &'static str {
840        "uuid_str"
841    }
842}
843
844/// `uuid_blob(X)` — convert a UUID string to a 16-byte blob.
845pub struct UuidBlobFunc;
846
847impl ScalarFunction for UuidBlobFunc {
848    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
849        if args.len() != 1 {
850            return Err(FrankenError::internal(
851                "uuid_blob requires exactly 1 argument",
852            ));
853        }
854        if args[0].is_null() {
855            return Ok(SqliteValue::Null);
856        }
857        let Some(s) = args[0].as_text() else {
858            return Err(FrankenError::internal("uuid_blob: argument must be text"));
859        };
860        let blob = uuid_str_to_blob(s)?;
861        Ok(SqliteValue::Blob(Arc::from(blob.as_slice())))
862    }
863
864    fn num_args(&self) -> i32 {
865        1
866    }
867
868    fn name(&self) -> &'static str {
869        "uuid_blob"
870    }
871}
872
873// ══════════════════════════════════════════════════════════════════════
874// Registration
875// ══════════════════════════════════════════════════════════════════════
876
877/// Register all miscellaneous scalar functions.
878pub fn register_misc_scalars(registry: &mut FunctionRegistry) {
879    info!("misc extension: registering scalar functions");
880    registry.register_scalar(DecimalFunc);
881    registry.register_scalar(DecimalAddFunc);
882    registry.register_scalar(DecimalSubFunc);
883    registry.register_scalar(DecimalMulFunc);
884    registry.register_scalar(DecimalCmpFunc);
885    registry.register_scalar(UuidFunc);
886    registry.register_scalar(UuidStrFunc);
887    registry.register_scalar(UuidBlobFunc);
888}
889
890// ══════════════════════════════════════════════════════════════════════
891// Tests
892// ══════════════════════════════════════════════════════════════════════
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    #[test]
899    fn test_extension_name_matches_crate_suffix() {
900        let expected = env!("CARGO_PKG_NAME")
901            .strip_prefix("fsqlite-ext-")
902            .expect("extension crates should use fsqlite-ext-* naming");
903        assert_eq!(extension_name(), expected);
904    }
905
906    // ── generate_series ──────────────────────────────────────────────
907
908    #[test]
909    fn test_generate_series_basic() {
910        let table = GenerateSeriesTable;
911        let mut cursor = table.open().unwrap();
912        cursor.init(1, 5, 1).unwrap();
913
914        let mut values = Vec::new();
915        let cx = Cx::new();
916        while !cursor.eof() {
917            let mut ctx = ColumnContext::new();
918            cursor.column(&mut ctx, 0).unwrap();
919            if let Some(SqliteValue::Integer(v)) = ctx.take_value() {
920                values.push(v);
921            }
922            cursor.next(&cx).unwrap();
923        }
924        assert_eq!(values, vec![1, 2, 3, 4, 5]);
925    }
926
927    #[test]
928    fn test_generate_series_step() {
929        let table = GenerateSeriesTable;
930        let mut cursor = table.open().unwrap();
931        cursor.init(0, 10, 2).unwrap();
932
933        let mut values = Vec::new();
934        let cx = Cx::new();
935        while !cursor.eof() {
936            let mut ctx = ColumnContext::new();
937            cursor.column(&mut ctx, 0).unwrap();
938            if let Some(SqliteValue::Integer(v)) = ctx.take_value() {
939                values.push(v);
940            }
941            cursor.next(&cx).unwrap();
942        }
943        assert_eq!(values, vec![0, 2, 4, 6, 8, 10]);
944    }
945
946    #[test]
947    fn test_generate_series_negative_step() {
948        let table = GenerateSeriesTable;
949        let mut cursor = table.open().unwrap();
950        cursor.init(5, 1, -1).unwrap();
951
952        let mut values = Vec::new();
953        let cx = Cx::new();
954        while !cursor.eof() {
955            let mut ctx = ColumnContext::new();
956            cursor.column(&mut ctx, 0).unwrap();
957            if let Some(SqliteValue::Integer(v)) = ctx.take_value() {
958                values.push(v);
959            }
960            cursor.next(&cx).unwrap();
961        }
962        assert_eq!(values, vec![5, 4, 3, 2, 1]);
963    }
964
965    #[test]
966    fn test_generate_series_single() {
967        let table = GenerateSeriesTable;
968        let mut cursor = table.open().unwrap();
969        cursor.init(5, 5, 1).unwrap();
970
971        let mut values = Vec::new();
972        let cx = Cx::new();
973        while !cursor.eof() {
974            let mut ctx = ColumnContext::new();
975            cursor.column(&mut ctx, 0).unwrap();
976            if let Some(SqliteValue::Integer(v)) = ctx.take_value() {
977                values.push(v);
978            }
979            cursor.next(&cx).unwrap();
980        }
981        assert_eq!(values, vec![5]);
982    }
983
984    #[test]
985    fn test_generate_series_empty() {
986        let table = GenerateSeriesTable;
987        let mut cursor = table.open().unwrap();
988        cursor.init(5, 1, 1).unwrap();
989        assert!(
990            cursor.eof(),
991            "positive step with start > stop should be empty"
992        );
993    }
994
995    #[test]
996    fn test_generate_series_step_zero_error() {
997        let table = GenerateSeriesTable;
998        let mut cursor = table.open().unwrap();
999        assert!(cursor.init(1, 10, 0).is_err());
1000    }
1001
1002    #[test]
1003    fn test_generate_series_filter() {
1004        let table = GenerateSeriesTable;
1005        let mut cursor = table.open().unwrap();
1006        let cx = Cx::new();
1007        cursor
1008            .filter(
1009                &cx,
1010                0,
1011                None,
1012                &[
1013                    SqliteValue::Integer(1),
1014                    SqliteValue::Integer(3),
1015                    SqliteValue::Integer(1),
1016                ],
1017            )
1018            .unwrap();
1019
1020        let mut values = Vec::new();
1021        while !cursor.eof() {
1022            let mut ctx = ColumnContext::new();
1023            cursor.column(&mut ctx, 0).unwrap();
1024            if let Some(SqliteValue::Integer(v)) = ctx.take_value() {
1025                values.push(v);
1026            }
1027            cursor.next(&cx).unwrap();
1028        }
1029        assert_eq!(values, vec![1, 2, 3]);
1030    }
1031
1032    // ── decimal ──────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_decimal_normalize() {
1036        assert_eq!(decimal_normalize("1.23"), Some("1.23".to_owned()));
1037        assert_eq!(decimal_normalize("001.230"), Some("1.23".to_owned()));
1038        assert_eq!(decimal_normalize("0.0"), Some("0".to_owned()));
1039        assert_eq!(decimal_normalize("-1.50"), Some("-1.5".to_owned()));
1040        assert_eq!(decimal_normalize("42"), Some("42".to_owned()));
1041    }
1042
1043    #[test]
1044    fn test_decimal_func_basic() {
1045        let args = [SqliteValue::Text("1.23".into())];
1046        let result = DecimalFunc.invoke(&args).unwrap();
1047        assert_eq!(result, SqliteValue::Text("1.23".into()));
1048    }
1049
1050    #[test]
1051    fn test_decimal_func_null() {
1052        let args = [SqliteValue::Null];
1053        let result = DecimalFunc.invoke(&args).unwrap();
1054        assert_eq!(result, SqliteValue::Null);
1055    }
1056
1057    #[test]
1058    fn test_decimal_add() {
1059        let args = [
1060            SqliteValue::Text("1.1".into()),
1061            SqliteValue::Text("2.2".into()),
1062        ];
1063        let result = DecimalAddFunc.invoke(&args).unwrap();
1064        assert_eq!(result, SqliteValue::Text("3.3".into()));
1065    }
1066
1067    #[test]
1068    fn test_decimal_add_no_fp_loss() {
1069        // This would be 0.30000000000000004 in floating point
1070        let args = [
1071            SqliteValue::Text("0.1".into()),
1072            SqliteValue::Text("0.2".into()),
1073        ];
1074        let result = DecimalAddFunc.invoke(&args).unwrap();
1075        assert_eq!(
1076            result,
1077            SqliteValue::Text("0.3".into()),
1078            "decimal_add should avoid floating-point precision loss"
1079        );
1080    }
1081
1082    #[test]
1083    fn test_decimal_sub() {
1084        let args = [
1085            SqliteValue::Text("5.00".into()),
1086            SqliteValue::Text("1.23".into()),
1087        ];
1088        let result = DecimalSubFunc.invoke(&args).unwrap();
1089        assert_eq!(result, SqliteValue::Text("3.77".into()));
1090    }
1091
1092    #[test]
1093    fn test_decimal_sub_negative_result() {
1094        let args = [
1095            SqliteValue::Text("1.0".into()),
1096            SqliteValue::Text("3.0".into()),
1097        ];
1098        let result = DecimalSubFunc.invoke(&args).unwrap();
1099        assert_eq!(result, SqliteValue::Text("-2".into()));
1100    }
1101
1102    #[test]
1103    fn test_decimal_mul() {
1104        let args = [
1105            SqliteValue::Text("1.5".into()),
1106            SqliteValue::Text("2.5".into()),
1107        ];
1108        let result = DecimalMulFunc.invoke(&args).unwrap();
1109        assert_eq!(result, SqliteValue::Text("3.75".into()));
1110    }
1111
1112    #[test]
1113    fn test_decimal_mul_large() {
1114        let args = [
1115            SqliteValue::Text("1.1".into()),
1116            SqliteValue::Text("2.0".into()),
1117        ];
1118        let result = DecimalMulFunc.invoke(&args).unwrap();
1119        assert_eq!(result, SqliteValue::Text("2.2".into()));
1120    }
1121
1122    #[test]
1123    fn test_decimal_cmp_less() {
1124        let args = [
1125            SqliteValue::Text("1.23".into()),
1126            SqliteValue::Text("4.56".into()),
1127        ];
1128        let result = DecimalCmpFunc.invoke(&args).unwrap();
1129        assert_eq!(result, SqliteValue::Integer(-1));
1130    }
1131
1132    #[test]
1133    fn test_decimal_cmp_greater() {
1134        let args = [
1135            SqliteValue::Text("4.56".into()),
1136            SqliteValue::Text("1.23".into()),
1137        ];
1138        let result = DecimalCmpFunc.invoke(&args).unwrap();
1139        assert_eq!(result, SqliteValue::Integer(1));
1140    }
1141
1142    #[test]
1143    fn test_decimal_cmp_equal() {
1144        let args = [
1145            SqliteValue::Text("1.0".into()),
1146            SqliteValue::Text("1.0".into()),
1147        ];
1148        let result = DecimalCmpFunc.invoke(&args).unwrap();
1149        assert_eq!(result, SqliteValue::Integer(0));
1150    }
1151
1152    #[test]
1153    fn test_decimal_cmp_negative() {
1154        let args = [
1155            SqliteValue::Text("-5".into()),
1156            SqliteValue::Text("3".into()),
1157        ];
1158        let result = DecimalCmpFunc.invoke(&args).unwrap();
1159        assert_eq!(result, SqliteValue::Integer(-1));
1160    }
1161
1162    #[test]
1163    fn test_decimal_precision_financial() {
1164        // Common financial precision test: 19.99 * 100 = 1999
1165        let result = decimal_mul_impl("19.99", "100");
1166        assert_eq!(result, Some("1999".to_owned()));
1167
1168        // Chained operations: (10.50 + 3.75) * 2 = 28.50
1169        let sum = decimal_add_impl("10.50", "3.75");
1170        assert_eq!(sum, Some("14.25".to_owned()));
1171        let product = decimal_mul_impl(sum.as_ref().unwrap(), "2");
1172        assert_eq!(product, Some("28.5".to_owned()));
1173    }
1174
1175    // ── uuid ─────────────────────────────────────────────────────────
1176
1177    #[test]
1178    fn test_uuid_v4_format() {
1179        let uuid = generate_uuid_v4();
1180        // UUID v4 format: 8-4-4-4-12 hex characters
1181        let parts: Vec<&str> = uuid.split('-').collect();
1182        assert_eq!(parts.len(), 5, "UUID should have 5 dash-separated parts");
1183        assert_eq!(parts[0].len(), 8);
1184        assert_eq!(parts[1].len(), 4);
1185        assert_eq!(parts[2].len(), 4);
1186        assert_eq!(parts[3].len(), 4);
1187        assert_eq!(parts[4].len(), 12);
1188    }
1189
1190    #[test]
1191    fn test_uuid_v4_version() {
1192        let uuid = generate_uuid_v4();
1193        // Version nibble is the first character of the third group
1194        let version_char = uuid.as_bytes()[14] as char;
1195        assert_eq!(version_char, '4', "UUID v4 must have version nibble = 4");
1196    }
1197
1198    #[test]
1199    fn test_uuid_v4_variant() {
1200        let uuid = generate_uuid_v4();
1201        // Variant bits are the first character of the fourth group
1202        let variant_char = uuid.as_bytes()[19] as char;
1203        let variant_nibble = u8::from_str_radix(&variant_char.to_string(), 16).unwrap();
1204        assert!(
1205            (0x8..=0xB).contains(&variant_nibble),
1206            "UUID v4 variant bits should be 10xx, got {variant_nibble:#X}"
1207        );
1208    }
1209
1210    #[test]
1211    fn test_uuid_uniqueness() {
1212        let mut uuids: Vec<String> = (0..100).map(|_| generate_uuid_v4()).collect();
1213        uuids.sort();
1214        uuids.dedup();
1215        assert_eq!(
1216            uuids.len(),
1217            100,
1218            "100 uuid() calls should produce 100 unique values"
1219        );
1220    }
1221
1222    #[test]
1223    fn test_uuid_func() {
1224        let result = UuidFunc.invoke(&[]).unwrap();
1225        if let SqliteValue::Text(s) = result {
1226            assert_eq!(s.len(), 36, "UUID string should be 36 characters");
1227        } else {
1228            panic!("uuid() should return Text");
1229        }
1230    }
1231
1232    #[test]
1233    fn test_uuid_str_blob_roundtrip() {
1234        let uuid_str = generate_uuid_v4();
1235        let blob = uuid_str_to_blob(&uuid_str).unwrap();
1236        assert_eq!(blob.len(), 16);
1237        let back = blob_to_uuid_str(&blob).unwrap();
1238        assert_eq!(back, uuid_str, "uuid_str(uuid_blob(X)) should roundtrip");
1239    }
1240
1241    #[test]
1242    fn test_uuid_blob_length() {
1243        let result = UuidBlobFunc
1244            .invoke(&[SqliteValue::Text(Arc::from(generate_uuid_v4().as_str()))])
1245            .unwrap();
1246        if let SqliteValue::Blob(b) = result {
1247            assert_eq!(b.len(), 16, "uuid_blob should return 16-byte blob");
1248        } else {
1249            panic!("uuid_blob should return Blob");
1250        }
1251    }
1252
1253    #[test]
1254    fn test_uuid_str_func() {
1255        let uuid = generate_uuid_v4();
1256        let blob = uuid_str_to_blob(&uuid).unwrap();
1257        let result = UuidStrFunc
1258            .invoke(&[SqliteValue::Blob(Arc::from(blob.as_slice()))])
1259            .unwrap();
1260        assert_eq!(result, SqliteValue::Text(Arc::from(uuid.as_str())));
1261    }
1262
1263    // ── registration ─────────────────────────────────────────────────
1264
1265    #[test]
1266    fn test_register_misc_scalars() {
1267        let mut registry = FunctionRegistry::new();
1268        register_misc_scalars(&mut registry);
1269        assert!(registry.find_scalar("decimal", 1).is_some());
1270        assert!(registry.find_scalar("decimal_add", 2).is_some());
1271        assert!(registry.find_scalar("decimal_sub", 2).is_some());
1272        assert!(registry.find_scalar("decimal_mul", 2).is_some());
1273        assert!(registry.find_scalar("decimal_cmp", 2).is_some());
1274        assert!(registry.find_scalar("uuid", 0).is_some());
1275        assert!(registry.find_scalar("uuid_str", 1).is_some());
1276        assert!(registry.find_scalar("uuid_blob", 1).is_some());
1277    }
1278
1279    // ── generate_series: additional edge cases ───────────────────────────
1280
1281    #[test]
1282    fn test_generate_series_large_step() {
1283        let table = GenerateSeriesTable;
1284        let mut cursor = table.open().unwrap();
1285        cursor.init(0, 100, 50).unwrap();
1286        let mut values = Vec::new();
1287        while !cursor.eof() {
1288            values.push(cursor.current);
1289            cursor.next(&Cx::default()).unwrap();
1290        }
1291        assert_eq!(values, vec![0, 50, 100]);
1292    }
1293
1294    #[test]
1295    fn test_generate_series_negative_range() {
1296        let table = GenerateSeriesTable;
1297        let mut cursor = table.open().unwrap();
1298        cursor.init(-5, -1, 1).unwrap();
1299        let mut count = 0;
1300        while !cursor.eof() {
1301            count += 1;
1302            cursor.next(&Cx::default()).unwrap();
1303        }
1304        assert_eq!(count, 5);
1305    }
1306
1307    #[test]
1308    fn test_generate_series_reverse_with_wrong_step_empty() {
1309        let table = GenerateSeriesTable;
1310        let mut cursor = table.open().unwrap();
1311        // start > stop with positive step → empty
1312        cursor.init(10, 1, 1).unwrap();
1313        assert!(cursor.eof());
1314    }
1315
1316    #[test]
1317    fn test_generate_series_forward_with_negative_step_empty() {
1318        let table = GenerateSeriesTable;
1319        let mut cursor = table.open().unwrap();
1320        // start < stop with negative step → empty
1321        cursor.init(1, 10, -1).unwrap();
1322        assert!(cursor.eof());
1323    }
1324
1325    #[test]
1326    fn test_generate_series_rowid() {
1327        let table = GenerateSeriesTable;
1328        let mut cursor = table.open().unwrap();
1329        cursor.init(42, 42, 1).unwrap();
1330        assert_eq!(cursor.rowid().unwrap(), 42);
1331    }
1332
1333    #[test]
1334    fn test_generate_series_column_values() {
1335        let table = GenerateSeriesTable;
1336        let mut cursor = table.open().unwrap();
1337        cursor.init(10, 20, 5).unwrap();
1338        // Column 0 = value (current)
1339        let mut ctx = ColumnContext::new();
1340        cursor.column(&mut ctx, 0).unwrap();
1341        assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(10)));
1342        // Column 2 = stop
1343        let mut ctx2 = ColumnContext::new();
1344        cursor.column(&mut ctx2, 2).unwrap();
1345        assert_eq!(ctx2.take_value(), Some(SqliteValue::Integer(20)));
1346        // Column 3 = step
1347        let mut ctx3 = ColumnContext::new();
1348        cursor.column(&mut ctx3, 3).unwrap();
1349        assert_eq!(ctx3.take_value(), Some(SqliteValue::Integer(5)));
1350        // Column out of range = Null
1351        let mut ctx4 = ColumnContext::new();
1352        cursor.column(&mut ctx4, 99).unwrap();
1353        assert_eq!(ctx4.take_value(), Some(SqliteValue::Null));
1354    }
1355
1356    #[test]
1357    fn test_generate_series_vtable_create_connect() {
1358        let cx = Cx::default();
1359        let _ = GenerateSeriesTable::create(&cx, &[]).unwrap();
1360        let _ = GenerateSeriesTable::connect(&cx, &[]).unwrap();
1361    }
1362
1363    #[test]
1364    fn test_generate_series_best_index() {
1365        let table = GenerateSeriesTable;
1366        let mut info = IndexInfo::new(Vec::new(), Vec::new());
1367        table.best_index(&mut info).unwrap();
1368        assert!(info.estimated_cost > 0.0);
1369        assert!(info.estimated_rows > 0);
1370    }
1371
1372    #[test]
1373    fn test_generate_series_overflow_terminates() {
1374        // Regression test: saturating_add caused infinite loop when current hit i64::MAX.
1375        // With checked_add fix, overflow should terminate iteration gracefully.
1376        let table = GenerateSeriesTable;
1377        let mut cursor = table.open().unwrap();
1378        // Start near i64::MAX with a step that causes overflow
1379        cursor.init(i64::MAX - 2, i64::MAX, 10).unwrap();
1380
1381        let mut values = Vec::new();
1382        let mut iterations = 0;
1383        while !cursor.eof() && iterations < 100 {
1384            values.push(cursor.current);
1385            cursor.next(&Cx::default()).unwrap();
1386            iterations += 1;
1387        }
1388        // Should terminate within a few iterations, not loop forever
1389        assert!(
1390            iterations < 10,
1391            "generate_series should terminate on overflow, got {} iterations",
1392            iterations
1393        );
1394        // Should yield at least the start value
1395        assert!(!values.is_empty(), "should yield at least the start value");
1396        assert_eq!(values[0], i64::MAX - 2);
1397    }
1398
1399    #[test]
1400    fn test_generate_series_negative_overflow_terminates() {
1401        // Same test but for underflow with negative step
1402        let table = GenerateSeriesTable;
1403        let mut cursor = table.open().unwrap();
1404        cursor.init(i64::MIN + 2, i64::MIN, -10).unwrap();
1405
1406        let mut values = Vec::new();
1407        let mut iterations = 0;
1408        while !cursor.eof() && iterations < 100 {
1409            values.push(cursor.current);
1410            cursor.next(&Cx::default()).unwrap();
1411            iterations += 1;
1412        }
1413        assert!(
1414            iterations < 10,
1415            "generate_series should terminate on underflow, got {} iterations",
1416            iterations
1417        );
1418        assert!(!values.is_empty());
1419        assert_eq!(values[0], i64::MIN + 2);
1420    }
1421
1422    // ── Decimal: normalization edge cases ─────────────────────────────────
1423
1424    #[test]
1425    fn test_decimal_normalize_zero() {
1426        assert_eq!(decimal_normalize("0"), Some("0".to_owned()));
1427        assert_eq!(decimal_normalize("0.0"), Some("0".to_owned()));
1428        assert_eq!(decimal_normalize("000.000"), Some("0".to_owned()));
1429    }
1430
1431    #[test]
1432    fn test_decimal_normalize_negative_zero() {
1433        // Negative zero should normalize to "0"
1434        let result = decimal_normalize("-0.0");
1435        assert!(result == Some("0".to_owned()) || result == Some("-0".to_owned()));
1436    }
1437
1438    #[test]
1439    fn test_decimal_normalize_integer() {
1440        assert_eq!(decimal_normalize("42"), Some("42".to_owned()));
1441        assert_eq!(decimal_normalize("00042"), Some("42".to_owned()));
1442    }
1443
1444    #[test]
1445    fn test_decimal_normalize_trailing_zeros() {
1446        assert_eq!(decimal_normalize("1.50000"), Some("1.5".to_owned()));
1447        assert_eq!(decimal_normalize("3.14000"), Some("3.14".to_owned()));
1448    }
1449
1450    // ── Decimal: arithmetic edge cases ───────────────────────────────────
1451
1452    #[test]
1453    fn test_decimal_add_zeros() {
1454        assert_eq!(decimal_add_impl("0", "0"), Some("0".to_owned()));
1455    }
1456
1457    #[test]
1458    fn test_decimal_add_negative_plus_positive() {
1459        let result = decimal_add_impl("-5", "3");
1460        assert_eq!(result, Some("-2".to_owned()));
1461    }
1462
1463    #[test]
1464    fn test_decimal_add_positive_plus_negative() {
1465        let result = decimal_add_impl("3", "-5");
1466        assert_eq!(result, Some("-2".to_owned()));
1467    }
1468
1469    #[test]
1470    fn test_decimal_sub_same_number() {
1471        assert_eq!(decimal_sub_impl("42.5", "42.5"), Some("0".to_owned()));
1472    }
1473
1474    #[test]
1475    fn test_decimal_sub_produces_negative() {
1476        let result = decimal_sub_impl("1", "5");
1477        assert_eq!(result, Some("-4".to_owned()));
1478    }
1479
1480    #[test]
1481    fn test_decimal_mul_by_zero() {
1482        assert_eq!(decimal_mul_impl("12345.6789", "0"), Some("0".to_owned()));
1483    }
1484
1485    #[test]
1486    fn test_decimal_mul_by_one() {
1487        assert_eq!(decimal_mul_impl("3.14", "1"), Some("3.14".to_owned()));
1488    }
1489
1490    #[test]
1491    fn test_decimal_mul_negative_times_negative() {
1492        let result = decimal_mul_impl("-3", "-4");
1493        assert_eq!(result, Some("12".to_owned()));
1494    }
1495
1496    #[test]
1497    fn test_decimal_mul_small_decimals() {
1498        let result = decimal_mul_impl("0.001", "0.001");
1499        assert_eq!(result, Some("0.000001".to_owned()));
1500    }
1501
1502    #[test]
1503    fn test_decimal_cmp_equal_values() {
1504        assert_eq!(decimal_cmp_impl("3.14", "3.14"), Some(0));
1505    }
1506
1507    #[test]
1508    fn test_decimal_cmp_leading_zeros_equal() {
1509        assert_eq!(decimal_cmp_impl("007.50", "7.5"), Some(0));
1510    }
1511
1512    #[test]
1513    fn test_decimal_cmp_negative_ordering() {
1514        assert_eq!(decimal_cmp_impl("-10", "-5"), Some(-1));
1515        assert_eq!(decimal_cmp_impl("-5", "-10"), Some(1));
1516    }
1517
1518    // ── Decimal: scalar function null handling ───────────────────────────
1519
1520    #[test]
1521    fn test_decimal_add_func_null_propagation() {
1522        let result = DecimalAddFunc
1523            .invoke(&[SqliteValue::Null, SqliteValue::Text(Arc::from("1"))])
1524            .unwrap();
1525        assert_eq!(result, SqliteValue::Null);
1526    }
1527
1528    #[test]
1529    fn test_decimal_sub_func_null_propagation() {
1530        let result = DecimalSubFunc
1531            .invoke(&[SqliteValue::Text(Arc::from("1")), SqliteValue::Null])
1532            .unwrap();
1533        assert_eq!(result, SqliteValue::Null);
1534    }
1535
1536    #[test]
1537    fn test_decimal_mul_func_null_propagation() {
1538        let result = DecimalMulFunc
1539            .invoke(&[SqliteValue::Null, SqliteValue::Null])
1540            .unwrap();
1541        assert_eq!(result, SqliteValue::Null);
1542    }
1543
1544    #[test]
1545    fn test_decimal_cmp_func_null_propagation() {
1546        let result = DecimalCmpFunc
1547            .invoke(&[SqliteValue::Null, SqliteValue::Text(Arc::from("1"))])
1548            .unwrap();
1549        assert_eq!(result, SqliteValue::Null);
1550    }
1551
1552    // ── UUID: error cases ────────────────────────────────────────────────
1553
1554    #[test]
1555    fn test_uuid_str_to_blob_invalid_length() {
1556        // Too short
1557        assert!(uuid_str_to_blob("abc").is_err());
1558    }
1559
1560    #[test]
1561    fn test_uuid_str_to_blob_invalid_hex() {
1562        assert!(uuid_str_to_blob("ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ").is_err());
1563    }
1564
1565    #[test]
1566    fn test_uuid_str_to_blob_accepts_compact_hex() {
1567        let uuid = "1234567812344abc8def1234567890ab";
1568        let blob = uuid_str_to_blob(uuid).unwrap();
1569        assert_eq!(
1570            blob_to_uuid_str(&blob).unwrap(),
1571            "12345678-1234-4abc-8def-1234567890ab"
1572        );
1573    }
1574
1575    #[test]
1576    fn test_uuid_str_to_blob_rejects_trailing_garbage() {
1577        assert!(uuid_str_to_blob("12345678-1234-4abc-8def-1234567890ab!!").is_err());
1578    }
1579
1580    #[test]
1581    fn test_uuid_str_to_blob_rejects_misplaced_hyphen() {
1582        assert!(uuid_str_to_blob("1234567-81234-4abc-8def-1234567890ab").is_err());
1583    }
1584
1585    #[test]
1586    fn test_blob_to_uuid_str_wrong_length() {
1587        assert!(blob_to_uuid_str(&[0u8; 15]).is_err());
1588        assert!(blob_to_uuid_str(&[0u8; 17]).is_err());
1589    }
1590
1591    #[test]
1592    fn test_uuid_func_with_args_errors() {
1593        let result = UuidFunc.invoke(&[SqliteValue::Integer(1)]);
1594        assert!(result.is_err());
1595    }
1596
1597    #[test]
1598    fn test_uuid_str_func_null_returns_null() {
1599        let result = UuidStrFunc.invoke(&[SqliteValue::Null]).unwrap();
1600        assert_eq!(result, SqliteValue::Null);
1601    }
1602
1603    #[test]
1604    fn test_uuid_blob_func_null_returns_null() {
1605        let result = UuidBlobFunc.invoke(&[SqliteValue::Null]).unwrap();
1606        assert_eq!(result, SqliteValue::Null);
1607    }
1608
1609    #[test]
1610    fn test_uuid_blob_func_non_text_errors() {
1611        let result = UuidBlobFunc.invoke(&[SqliteValue::Integer(42)]);
1612        assert!(result.is_err());
1613    }
1614
1615    #[test]
1616    fn test_uuid_str_func_normalizes_text() {
1617        // uuid_str with text input should normalize via blob roundtrip
1618        let uuid = generate_uuid_v4();
1619        let result = UuidStrFunc
1620            .invoke(&[SqliteValue::Text(Arc::from(uuid.as_str()))])
1621            .unwrap();
1622        assert_eq!(result, SqliteValue::Text(Arc::from(uuid.as_str())));
1623    }
1624
1625    #[test]
1626    fn test_uuid_str_func_non_blob_non_text_errors() {
1627        let result = UuidStrFunc.invoke(&[SqliteValue::Integer(42)]);
1628        assert!(result.is_err());
1629    }
1630
1631    // ── UUID: format validation ──────────────────────────────────────────
1632
1633    #[test]
1634    fn test_uuid_all_lowercase_hex() {
1635        let uuid = generate_uuid_v4();
1636        // UUID should contain only lowercase hex and dashes
1637        assert!(uuid.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
1638        assert!(!uuid.contains(|c: char| c.is_ascii_uppercase()));
1639    }
1640
1641    #[test]
1642    fn test_uuid_v4_multiple_unique() {
1643        let uuids: Vec<String> = (0..50).map(|_| generate_uuid_v4()).collect();
1644        // All should be unique
1645        let mut sorted = uuids.clone();
1646        sorted.sort();
1647        sorted.dedup();
1648        assert_eq!(sorted.len(), uuids.len(), "all UUIDs should be unique");
1649    }
1650
1651    // ── Scalar function names ────────────────────────────────────────────
1652
1653    #[test]
1654    fn test_scalar_function_names() {
1655        assert_eq!(DecimalFunc.name(), "decimal");
1656        assert_eq!(DecimalAddFunc.name(), "decimal_add");
1657        assert_eq!(DecimalSubFunc.name(), "decimal_sub");
1658        assert_eq!(DecimalMulFunc.name(), "decimal_mul");
1659        assert_eq!(DecimalCmpFunc.name(), "decimal_cmp");
1660        assert_eq!(UuidFunc.name(), "uuid");
1661        assert_eq!(UuidStrFunc.name(), "uuid_str");
1662        assert_eq!(UuidBlobFunc.name(), "uuid_blob");
1663    }
1664
1665    #[test]
1666    fn test_scalar_function_arg_counts() {
1667        assert_eq!(DecimalFunc.num_args(), 1);
1668        assert_eq!(DecimalAddFunc.num_args(), 2);
1669        assert_eq!(DecimalSubFunc.num_args(), 2);
1670        assert_eq!(DecimalMulFunc.num_args(), 2);
1671        assert_eq!(DecimalCmpFunc.num_args(), 2);
1672        assert_eq!(UuidFunc.num_args(), 0);
1673        assert_eq!(UuidStrFunc.num_args(), 1);
1674        assert_eq!(UuidBlobFunc.num_args(), 1);
1675    }
1676
1677    #[test]
1678    fn test_uuid_func_not_deterministic() {
1679        assert!(!UuidFunc.is_deterministic());
1680    }
1681}