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