1use std::cell::RefCell;
2use std::cmp::Ordering;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::{Arc, OnceLock};
6
7use memchr::{memchr, memchr2, memmem};
8
9use crate::{StorageClass, StrictColumnType, StrictTypeError, TypeAffinity};
10
11const VALUE_POOL_CAP: usize = 256;
20
21thread_local! {
22 static VALUE_POOL: RefCell<Vec<SqliteValue>> = const { RefCell::new(Vec::new()) };
28}
29
30#[cfg(test)]
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32struct ValuePoolStats {
33 slab_alloc_count: usize,
34 slab_return_count: usize,
35 global_alloc_fallback_count: usize,
36 slab_high_water_mark: usize,
37}
38
39#[cfg(test)]
40impl ValuePoolStats {
41 const fn new() -> Self {
42 Self {
43 slab_alloc_count: 0,
44 slab_return_count: 0,
45 global_alloc_fallback_count: 0,
46 slab_high_water_mark: 0,
47 }
48 }
49}
50
51#[cfg(test)]
52thread_local! {
53 static VALUE_POOL_TEST_STATS: RefCell<ValuePoolStats> =
54 const { RefCell::new(ValuePoolStats::new()) };
55}
56
57#[cfg(test)]
58fn reset_value_pool_test_stats() {
59 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow_mut() = ValuePoolStats::new());
60}
61
62#[cfg(test)]
63fn value_pool_test_stats_snapshot() -> ValuePoolStats {
64 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow())
65}
66
67#[cfg(test)]
68fn record_value_pool_acquire(hit: bool) {
69 VALUE_POOL_TEST_STATS.with(|stats| {
70 let mut stats = stats.borrow_mut();
71 if hit {
72 stats.slab_alloc_count += 1;
73 } else {
74 stats.global_alloc_fallback_count += 1;
75 }
76 });
77}
78
79#[cfg(test)]
80fn record_value_pool_return(pool_len: usize) {
81 VALUE_POOL_TEST_STATS.with(|stats| {
82 let mut stats = stats.borrow_mut();
83 stats.slab_return_count += 1;
84 stats.slab_high_water_mark = stats.slab_high_water_mark.max(pool_len);
85 });
86}
87
88#[inline]
101pub fn pool_acquire() -> Option<SqliteValue> {
102 let value = VALUE_POOL.with(|pool| pool.borrow_mut().pop());
103 #[cfg(test)]
104 record_value_pool_acquire(value.is_some());
105 value
106}
107
108#[inline]
116pub fn pool_return(value: SqliteValue) {
117 VALUE_POOL.with(|pool| {
118 let mut pool = pool.borrow_mut();
119 if pool.len() < VALUE_POOL_CAP {
120 pool.push(value);
121 #[cfg(test)]
122 record_value_pool_return(pool.len());
123 }
124 });
126}
127
128#[inline]
131pub fn pool_return_reusable(value: SqliteValue) {
132 if value_preserves_reusable_heap_storage(&value) {
133 pool_return(value);
134 }
135}
136
137#[inline]
142pub fn pool_clear() {
143 VALUE_POOL.with(|pool| pool.borrow_mut().clear());
144}
145
146#[inline]
150pub fn pool_len() -> usize {
151 VALUE_POOL.with(|pool| pool.borrow().len())
152}
153
154#[inline]
155fn value_preserves_reusable_heap_storage(value: &SqliteValue) -> bool {
156 match value {
157 SqliteValue::Text(text) => matches!(&text.repr, SmallTextRepr::HeapOwned { .. }),
158 SqliteValue::Blob(bytes) => Arc::strong_count(bytes) == 1,
159 _ => false,
160 }
161}
162
163const SMALL_TEXT_INLINE_CAP: usize = 23;
171
172#[cfg(feature = "bench-internals")]
173static SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH: std::sync::atomic::AtomicBool =
174 std::sync::atomic::AtomicBool::new(false);
175#[cfg(feature = "bench-internals")]
176static SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH: std::sync::atomic::AtomicU64 =
177 std::sync::atomic::AtomicU64::new(0);
178
179#[cfg(feature = "bench-internals")]
181#[doc(hidden)]
182pub fn set_small_text_direct_traits_for_bench(enabled: bool) {
183 SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.store(enabled, std::sync::atomic::Ordering::Relaxed);
184}
185
186#[cfg(feature = "bench-internals")]
188#[doc(hidden)]
189pub fn reset_small_text_direct_trait_hits_for_bench() {
190 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.store(0, std::sync::atomic::Ordering::Relaxed);
191}
192
193#[cfg(feature = "bench-internals")]
195#[doc(hidden)]
196#[must_use]
197pub fn small_text_direct_trait_hits_for_bench() -> u64 {
198 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed)
199}
200
201pub struct SmallText {
209 repr: SmallTextRepr,
211}
212
213enum SmallTextRepr {
215 Inline {
217 len: u8,
218 buf: [u8; SMALL_TEXT_INLINE_CAP],
219 },
220 HeapOwned {
225 text: String,
226 shared: OnceLock<Arc<str>>,
227 },
228 HeapShared(Arc<str>),
230 Raw { bytes: Arc<[u8]>, lossy: Arc<str> },
235}
236
237impl Clone for SmallText {
238 fn clone(&self) -> Self {
239 Self {
240 repr: self.repr.clone(),
241 }
242 }
243}
244
245impl Clone for SmallTextRepr {
246 fn clone(&self) -> Self {
247 match self {
248 Self::Inline { len, buf } => Self::Inline {
249 len: *len,
250 buf: *buf,
251 },
252 Self::HeapOwned { text, shared } => {
253 let shared = Arc::clone(shared.get_or_init(|| Arc::from(text.as_str())));
254 Self::HeapShared(shared)
255 }
256 Self::HeapShared(text) => Self::HeapShared(Arc::clone(text)),
257 Self::Raw { bytes, lossy } => Self::Raw {
258 bytes: Arc::clone(bytes),
259 lossy: Arc::clone(lossy),
260 },
261 }
262 }
263}
264
265impl SmallText {
266 #[inline]
268 pub fn new(s: &str) -> Self {
269 if s.len() <= SMALL_TEXT_INLINE_CAP {
270 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
271 buf[..s.len()].copy_from_slice(s.as_bytes());
272 Self {
273 repr: SmallTextRepr::Inline {
274 len: s.len() as u8,
275 buf,
276 },
277 }
278 } else {
279 Self {
280 repr: SmallTextRepr::HeapOwned {
281 text: s.to_owned(),
282 shared: OnceLock::new(),
283 },
284 }
285 }
286 }
287
288 #[inline]
290 pub fn from_string<S>(s: S) -> Self
291 where
292 S: Into<String> + AsRef<str>,
293 {
294 if s.as_ref().len() <= SMALL_TEXT_INLINE_CAP {
295 Self::new(s.as_ref())
296 } else {
297 Self {
298 repr: SmallTextRepr::HeapOwned {
299 text: s.into(),
300 shared: OnceLock::new(),
301 },
302 }
303 }
304 }
305
306 #[inline]
308 pub fn from_arc(arc: Arc<str>) -> Self {
309 if arc.len() <= SMALL_TEXT_INLINE_CAP {
310 Self::new(&arc)
311 } else {
312 Self {
313 repr: SmallTextRepr::HeapShared(arc),
314 }
315 }
316 }
317
318 #[inline]
323 pub fn from_bytes(bytes: &[u8]) -> Self {
324 match simdutf8::basic::from_utf8(bytes) {
325 Ok(text) => Self::new(text),
326 Err(_) => Self::from_raw_bytes(Arc::from(bytes)),
327 }
328 }
329
330 #[inline]
333 pub fn from_arc_bytes(bytes: Arc<[u8]>) -> Self {
334 match simdutf8::basic::from_utf8(&bytes) {
335 Ok(text) => Self::new(text),
336 Err(_) => Self::from_raw_bytes(bytes),
337 }
338 }
339
340 #[inline]
341 fn from_raw_bytes(bytes: Arc<[u8]>) -> Self {
342 debug_assert!(simdutf8::basic::from_utf8(&bytes).is_err());
343 let lossy: Arc<str> = Arc::from(String::from_utf8_lossy(&bytes).into_owned());
344 Self {
345 repr: SmallTextRepr::Raw { bytes, lossy },
346 }
347 }
348
349 #[inline]
352 pub fn overwrite(&mut self, s: &str) {
353 if s.len() <= SMALL_TEXT_INLINE_CAP {
354 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
355 buf[..s.len()].copy_from_slice(s.as_bytes());
356 self.repr = SmallTextRepr::Inline {
357 len: s.len() as u8,
358 buf,
359 };
360 return;
361 }
362
363 match &mut self.repr {
364 SmallTextRepr::HeapOwned { text, shared } => {
365 text.clear();
366 text.push_str(s);
367 if shared.get().is_some() {
368 *shared = OnceLock::new();
369 }
370 }
371 _ => {
372 self.repr = SmallTextRepr::HeapOwned {
373 text: s.to_owned(),
374 shared: OnceLock::new(),
375 };
376 }
377 }
378 }
379
380 #[inline]
382 pub fn overwrite_bytes(&mut self, bytes: &[u8]) {
383 match simdutf8::basic::from_utf8(bytes) {
384 Ok(text) => self.overwrite(text),
385 Err(_) => *self = Self::from_raw_bytes(Arc::from(bytes)),
386 }
387 }
388
389 #[inline]
399 pub fn as_str(&self) -> &str {
400 match &self.repr {
401 SmallTextRepr::Inline { len, buf } => simdutf8::basic::from_utf8(&buf[..*len as usize])
402 .expect("SmallText inline representation must always contain valid UTF-8"),
403 SmallTextRepr::HeapOwned { text, .. } => text.as_str(),
404 SmallTextRepr::HeapShared(text) => text,
405 SmallTextRepr::Raw { lossy, .. } => lossy,
406 }
407 }
408
409 #[inline]
411 #[must_use]
412 pub fn as_str_checked(&self) -> Option<&str> {
413 match &self.repr {
414 SmallTextRepr::Raw { .. } => None,
415 _ => Some(self.as_str()),
416 }
417 }
418
419 #[inline]
421 #[must_use]
422 pub fn is_valid_utf8(&self) -> bool {
423 !matches!(&self.repr, SmallTextRepr::Raw { .. })
424 }
425
426 #[inline]
439 #[must_use]
440 pub fn as_bytes_direct(&self) -> &[u8] {
441 match &self.repr {
442 SmallTextRepr::Inline { len, buf } => &buf[..*len as usize],
443 SmallTextRepr::HeapOwned { text, .. } => text.as_bytes(),
444 SmallTextRepr::HeapShared(text) => text.as_bytes(),
445 SmallTextRepr::Raw { bytes, .. } => bytes,
446 }
447 }
448
449 #[inline]
451 pub fn len(&self) -> usize {
452 match &self.repr {
453 SmallTextRepr::Inline { len, .. } => *len as usize,
454 SmallTextRepr::HeapOwned { text, .. } => text.len(),
455 SmallTextRepr::HeapShared(text) => text.len(),
456 SmallTextRepr::Raw { bytes, .. } => bytes.len(),
457 }
458 }
459
460 #[inline]
462 pub fn is_empty(&self) -> bool {
463 self.len() == 0
464 }
465
466 #[inline]
468 pub fn is_inline(&self) -> bool {
469 matches!(&self.repr, SmallTextRepr::Inline { .. })
470 }
471
472 #[inline]
474 pub fn into_arc(self) -> Arc<str> {
475 match self.repr {
476 SmallTextRepr::Inline { len, buf } => {
477 let s = simdutf8::basic::from_utf8(&buf[..len as usize])
479 .expect("SmallText inline representation must always contain valid UTF-8");
480 Arc::from(s)
481 }
482 SmallTextRepr::HeapOwned { text, shared } => shared
483 .into_inner()
484 .unwrap_or_else(|| Arc::<str>::from(text)),
485 SmallTextRepr::HeapShared(text) => text,
486 SmallTextRepr::Raw { lossy, .. } => lossy,
487 }
488 }
489}
490
491impl Default for SmallText {
492 #[inline]
493 fn default() -> Self {
494 Self::new("")
495 }
496}
497
498impl fmt::Debug for SmallText {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 fmt::Debug::fmt(self.as_str(), f)
501 }
502}
503
504impl fmt::Display for SmallText {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 fmt::Display::fmt(self.as_str(), f)
507 }
508}
509
510impl PartialEq for SmallText {
511 #[inline]
512 fn eq(&self, other: &Self) -> bool {
513 #[cfg(feature = "bench-internals")]
514 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
515 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
516 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
517 return self.as_bytes_direct() == other.as_bytes_direct();
518 }
519 match (self.as_str_checked(), other.as_str_checked()) {
520 (Some(left), Some(right)) => left == right,
521 _ => self.as_bytes_direct() == other.as_bytes_direct(),
522 }
523 }
524}
525
526impl Eq for SmallText {}
527
528impl PartialOrd for SmallText {
529 #[inline]
530 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
531 Some(self.cmp(other))
532 }
533}
534
535impl Ord for SmallText {
536 #[inline]
537 fn cmp(&self, other: &Self) -> Ordering {
538 #[cfg(feature = "bench-internals")]
539 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
540 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
541 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
542 return self.as_bytes_direct().cmp(other.as_bytes_direct());
543 }
544 match (self.as_str_checked(), other.as_str_checked()) {
545 (Some(left), Some(right)) => left.cmp(right),
546 _ => self.as_bytes_direct().cmp(other.as_bytes_direct()),
547 }
548 }
549}
550
551impl Hash for SmallText {
552 #[inline]
553 fn hash<H: Hasher>(&self, state: &mut H) {
554 #[cfg(feature = "bench-internals")]
555 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
556 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
557 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
558 state.write(self.as_bytes_direct());
559 state.write_u8(0xff);
560 return;
561 }
562 if let Some(text) = self.as_str_checked() {
563 text.hash(state);
564 } else {
565 state.write(self.as_bytes_direct());
566 state.write_u8(0xff);
567 }
568 }
569}
570
571impl From<&str> for SmallText {
572 #[inline]
573 fn from(s: &str) -> Self {
574 Self::new(s)
575 }
576}
577
578impl From<String> for SmallText {
579 #[inline]
580 fn from(s: String) -> Self {
581 Self::from_string(s)
582 }
583}
584
585impl From<Arc<str>> for SmallText {
586 #[inline]
587 fn from(arc: Arc<str>) -> Self {
588 Self::from_arc(arc)
589 }
590}
591
592impl AsRef<str> for SmallText {
593 #[inline]
594 fn as_ref(&self) -> &str {
595 self.as_str()
596 }
597}
598
599impl std::ops::Deref for SmallText {
600 type Target = str;
601
602 #[inline]
603 fn deref(&self) -> &Self::Target {
604 self.as_str()
605 }
606}
607
608impl std::borrow::Borrow<str> for SmallText {
609 #[inline]
610 fn borrow(&self) -> &str {
611 self.as_str()
612 }
613}
614
615impl serde::Serialize for SmallText {
617 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
618 where
619 S: serde::Serializer,
620 {
621 let Some(text) = self.as_str_checked() else {
622 return Err(serde::ser::Error::custom(
623 "SQLite TEXT containing invalid UTF-8 cannot be serialized as a Rust string",
624 ));
625 };
626 serializer.serialize_str(text)
627 }
628}
629
630impl<'de> serde::Deserialize<'de> for SmallText {
631 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
632 where
633 D: serde::Deserializer<'de>,
634 {
635 let s = String::deserialize(deserializer)?;
636 Ok(Self::from_string(s))
637 }
638}
639
640fn scan_numeric_prefix(bytes: &[u8]) -> usize {
646 if bytes.is_empty() {
647 return 0;
648 }
649
650 let mut i = 0usize;
651 if bytes[i] == b'+' || bytes[i] == b'-' {
652 i += 1;
653 }
654
655 let mut has_digit = false;
656 while i < bytes.len() && bytes[i].is_ascii_digit() {
657 has_digit = true;
658 i += 1;
659 }
660
661 if i < bytes.len() && bytes[i] == b'.' {
662 i += 1;
663 while i < bytes.len() && bytes[i].is_ascii_digit() {
664 has_digit = true;
665 i += 1;
666 }
667 }
668
669 if !has_digit {
670 return 0;
671 }
672
673 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
674 let exp_start = i;
675 i += 1;
676 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
677 i += 1;
678 }
679 if i < bytes.len() && bytes[i].is_ascii_digit() {
680 while i < bytes.len() && bytes[i].is_ascii_digit() {
681 i += 1;
682 }
683 } else {
684 i = exp_start;
685 }
686 }
687
688 i
689}
690
691#[allow(clippy::cast_possible_truncation)]
693fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
694 let mut start = 0;
695 while start < b.len() && b[start].is_ascii_whitespace() {
696 start += 1;
697 }
698 let trimmed = &b[start..];
699 let end = scan_numeric_prefix(trimmed);
700 if end == 0 {
701 return 0;
702 }
703 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
706 let f = s.parse::<f64>().unwrap_or(0.0);
707 #[allow(clippy::manual_clamp)]
708 if f >= i64::MAX as f64 {
709 i64::MAX
710 } else if f <= i64::MIN as f64 {
711 i64::MIN
712 } else {
713 f as i64
714 }
715}
716
717#[allow(clippy::cast_possible_truncation)]
719fn parse_integer_prefix(s: &str) -> i64 {
720 parse_integer_prefix_bytes(s.as_bytes())
721}
722
723fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
725 let mut start = 0;
726 while start < b.len() && b[start].is_ascii_whitespace() {
727 start += 1;
728 }
729 let trimmed = &b[start..];
730 let end = scan_numeric_prefix(trimmed);
731 if end == 0 {
732 return 0.0;
733 }
734 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
737 s.parse::<f64>().unwrap_or(0.0)
738}
739
740fn parse_float_prefix(s: &str) -> f64 {
742 parse_float_prefix_bytes(s.as_bytes())
743}
744
745fn trim_sqlite_ascii_whitespace(s: &str) -> &str {
746 s.trim_matches(|ch: char| ch.is_ascii_whitespace())
747}
748
749fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
750 let trimmed = trim_sqlite_ascii_whitespace(s);
751 let end = scan_numeric_prefix(trimmed.as_bytes());
752 if end == 0 {
753 return SqliteValue::Integer(0);
754 }
755
756 let prefix = &trimmed[..end];
757 let is_integer_syntax = !prefix
758 .as_bytes()
759 .iter()
760 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
761
762 if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
763 return SqliteValue::Integer(value);
764 }
765
766 if let Ok(value) = prefix.parse::<f64>() {
767 if value.is_finite()
768 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
769 {
770 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
771 let truncated = value as i64;
772 #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
773 if truncated as f64 == value {
774 return SqliteValue::Integer(truncated);
775 }
776 }
777 return SqliteValue::Float(value);
778 }
779
780 SqliteValue::Integer(0)
781}
782
783#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
788pub enum SqliteValue {
789 Null,
791 Integer(i64),
793 Float(f64),
795 Text(SmallText),
801 Blob(Arc<[u8]>),
805}
806
807impl SqliteValue {
808 pub const fn affinity(&self) -> TypeAffinity {
810 match self {
811 Self::Null | Self::Blob(_) => TypeAffinity::Blob,
812 Self::Integer(_) => TypeAffinity::Integer,
813 Self::Float(_) => TypeAffinity::Real,
814 Self::Text(_) => TypeAffinity::Text,
815 }
816 }
817
818 pub const fn storage_class(&self) -> StorageClass {
820 match self {
821 Self::Null => StorageClass::Null,
822 Self::Integer(_) => StorageClass::Integer,
823 Self::Float(_) => StorageClass::Real,
824 Self::Text(_) => StorageClass::Text,
825 Self::Blob(_) => StorageClass::Blob,
826 }
827 }
828
829 #[must_use]
841 #[allow(
842 clippy::cast_possible_truncation,
843 clippy::cast_precision_loss,
844 clippy::float_cmp
845 )]
846 pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
847 match affinity {
848 TypeAffinity::Blob => self,
849 TypeAffinity::Text => match self {
850 Self::Null | Self::Text(_) | Self::Blob(_) => self,
851 Self::Integer(_) | Self::Float(_) => {
852 let t = self.to_text();
853 Self::Text(SmallText::from_string(t))
854 }
855 },
856 TypeAffinity::Numeric | TypeAffinity::Integer => match &self {
857 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
858 Self::Float(f) => {
859 if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
860 let i = *f as i64;
861 if (i as f64) == *f {
862 return Self::Integer(i);
863 }
864 }
865 self
866 }
867 _ => self,
868 },
869 TypeAffinity::Real => match &self {
870 Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
871 .map(|v| match v {
872 Self::Integer(i) => Self::Float(i as f64),
873 other => other,
874 })
875 .unwrap_or(self),
876 Self::Integer(i) => Self::Float(*i as f64),
877 _ => self,
878 },
879 }
880 }
881
882 #[allow(clippy::cast_precision_loss)]
889 pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
890 if matches!(self, Self::Null) {
891 return Ok(self);
892 }
893 match col_type {
894 StrictColumnType::Any => Ok(self),
895 StrictColumnType::Integer => match self {
896 Self::Integer(_) => Ok(self),
897 other => Err(StrictTypeError {
898 expected: col_type,
899 actual: other.storage_class(),
900 }),
901 },
902 StrictColumnType::Real => match self {
903 Self::Float(_) => Ok(self),
904 Self::Integer(i) => Ok(Self::Float(i as f64)),
905 other => Err(StrictTypeError {
906 expected: col_type,
907 actual: other.storage_class(),
908 }),
909 },
910 StrictColumnType::Text => match self {
911 Self::Text(_) => Ok(self),
912 other => Err(StrictTypeError {
913 expected: col_type,
914 actual: other.storage_class(),
915 }),
916 },
917 StrictColumnType::Blob => match self {
918 Self::Blob(_) => Ok(self),
919 other => Err(StrictTypeError {
920 expected: col_type,
921 actual: other.storage_class(),
922 }),
923 },
924 }
925 }
926
927 #[inline(always)]
929 #[allow(clippy::inline_always)]
930 pub const fn is_null(&self) -> bool {
931 matches!(self, Self::Null)
932 }
933
934 #[inline]
936 pub const fn as_integer(&self) -> Option<i64> {
937 match self {
938 Self::Integer(i) => Some(*i),
939 _ => None,
940 }
941 }
942
943 #[inline]
945 pub fn as_float(&self) -> Option<f64> {
946 match self {
947 Self::Float(f) => Some(*f),
948 _ => None,
949 }
950 }
951
952 #[inline]
954 pub fn as_text(&self) -> Option<&str> {
955 match self {
956 Self::Text(s) => Some(s),
957 _ => None,
958 }
959 }
960
961 #[inline]
963 pub fn as_blob(&self) -> Option<&[u8]> {
964 match self {
965 Self::Blob(b) => Some(b),
966 _ => None,
967 }
968 }
969
970 #[inline(always)]
978 #[allow(clippy::inline_always)]
979 #[allow(clippy::cast_possible_truncation)]
980 pub fn to_integer(&self) -> i64 {
981 match self {
982 Self::Null => 0,
983 Self::Integer(i) => *i,
984 Self::Float(f) => *f as i64,
985 Self::Text(s) => parse_integer_prefix(s),
986 Self::Blob(b) => parse_integer_prefix_bytes(b),
987 }
988 }
989
990 #[inline(always)]
998 #[allow(clippy::inline_always)]
999 #[allow(clippy::cast_precision_loss)]
1000 pub fn to_float(&self) -> f64 {
1001 match self {
1002 Self::Null => 0.0,
1003 Self::Integer(i) => *i as f64,
1004 Self::Float(f) => *f,
1005 Self::Text(s) => parse_float_prefix(s),
1006 Self::Blob(b) => parse_float_prefix_bytes(b),
1007 }
1008 }
1009
1010 #[must_use]
1017 pub fn to_sum_numeric_value(&self) -> Self {
1018 match self {
1019 Self::Null => Self::Null,
1020 Self::Integer(i) => Self::Integer(*i),
1021 Self::Float(f) => Self::Float(*f),
1022 Self::Text(s) => {
1023 let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
1024 if let Ok(integer) = trimmed.parse::<i64>() {
1025 Self::Integer(integer)
1026 } else {
1027 Self::Float(parse_float_prefix(s))
1028 }
1029 }
1030 Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
1031 }
1032 }
1033
1034 #[inline]
1040 #[must_use]
1041 pub fn as_text_str(&self) -> Option<&str> {
1042 match self {
1043 Self::Text(s) => Some(s),
1044 _ => None,
1045 }
1046 }
1047
1048 #[inline]
1050 #[must_use]
1051 pub fn as_blob_bytes(&self) -> Option<&[u8]> {
1052 match self {
1053 Self::Blob(b) => Some(b),
1054 _ => None,
1055 }
1056 }
1057
1058 pub fn to_text(&self) -> String {
1065 match self {
1066 Self::Null => String::new(),
1067 Self::Integer(i) => i.to_string(),
1068 Self::Float(f) => format_sqlite_float(*f),
1069 Self::Text(s) => s.to_string(),
1070 Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
1071 }
1072 }
1073
1074 #[must_use]
1080 pub fn cast_to_numeric(&self) -> Self {
1081 match self {
1082 Self::Null => Self::Null,
1083 Self::Integer(i) => Self::Integer(*i),
1084 Self::Float(f) => Self::Float(*f),
1085 Self::Text(s) => cast_text_prefix_to_numeric(s),
1086 Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
1087 }
1088 }
1089
1090 pub const fn typeof_str(&self) -> &'static str {
1094 match self {
1095 Self::Null => "null",
1096 Self::Integer(_) => "integer",
1097 Self::Float(_) => "real",
1098 Self::Text(_) => "text",
1099 Self::Blob(_) => "blob",
1100 }
1101 }
1102
1103 pub fn sql_length(&self) -> Option<i64> {
1110 match self {
1111 Self::Null => None,
1112 Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
1113 Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
1114 Self::Integer(_) | Self::Float(_) => {
1115 let t = self.to_text();
1116 Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
1117 }
1118 }
1119 }
1120
1121 pub fn unique_eq(&self, other: &Self) -> bool {
1127 if self.is_null() || other.is_null() {
1128 return false;
1129 }
1130 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1131 }
1132
1133 fn float_result_or_null(result: f64) -> Self {
1137 if result.is_nan() {
1138 Self::Null
1139 } else {
1140 Self::Float(result)
1141 }
1142 }
1143
1144 #[inline]
1150 pub fn is_integer_numeric_type(&self) -> bool {
1151 fn text_is_integer_numeric_type(s: &str) -> bool {
1152 let trimmed = s.trim_start();
1153 let end = scan_numeric_prefix(trimmed.as_bytes());
1154 end > 0
1155 && !trimmed.as_bytes()[..end]
1156 .iter()
1157 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1158 }
1159
1160 match self {
1161 Self::Integer(_) => true,
1162 Self::Float(_) | Self::Null => false,
1163 Self::Text(s) => text_is_integer_numeric_type(s),
1164 Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1165 }
1166 }
1167
1168 #[inline]
1173 fn is_float_numeric_type(&self) -> bool {
1174 fn text_is_float(s: &str) -> bool {
1175 let trimmed = s.trim_start();
1176 let end = scan_numeric_prefix(trimmed.as_bytes());
1177 end > 0
1178 && trimmed.as_bytes()[..end]
1179 .iter()
1180 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1181 }
1182 match self {
1183 Self::Float(_) => true,
1184 Self::Integer(_) | Self::Null => false,
1185 Self::Text(s) => text_is_float(s),
1186 Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1187 }
1188 }
1189
1190 #[inline(always)]
1198 #[allow(clippy::inline_always)]
1199 #[must_use]
1200 #[allow(clippy::cast_precision_loss)]
1201 pub fn sql_add(&self, other: &Self) -> Self {
1202 match (self, other) {
1203 (Self::Null, _) | (_, Self::Null) => Self::Null,
1204 (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1205 Some(result) => Self::Integer(result),
1206 None => Self::float_result_or_null(*a as f64 + *b as f64),
1207 },
1208 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1212 let a = self.to_integer();
1213 let b = other.to_integer();
1214 match a.checked_add(b) {
1215 Some(result) => Self::Integer(result),
1216 None => Self::float_result_or_null(a as f64 + b as f64),
1217 }
1218 }
1219 _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1220 }
1221 }
1222
1223 #[inline(always)]
1227 #[allow(clippy::inline_always)]
1228 #[must_use]
1229 #[allow(clippy::cast_precision_loss)]
1230 pub fn sql_sub(&self, other: &Self) -> Self {
1231 match (self, other) {
1232 (Self::Null, _) | (_, Self::Null) => Self::Null,
1233 (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1234 Some(result) => Self::Integer(result),
1235 None => Self::float_result_or_null(*a as f64 - *b as f64),
1236 },
1237 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1238 let a = self.to_integer();
1239 let b = other.to_integer();
1240 match a.checked_sub(b) {
1241 Some(result) => Self::Integer(result),
1242 None => Self::float_result_or_null(a as f64 - b as f64),
1243 }
1244 }
1245 _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1246 }
1247 }
1248
1249 #[inline(always)]
1253 #[allow(clippy::inline_always)]
1254 #[must_use]
1255 #[allow(clippy::cast_precision_loss)]
1256 pub fn sql_mul(&self, other: &Self) -> Self {
1257 match (self, other) {
1258 (Self::Null, _) | (_, Self::Null) => Self::Null,
1259 (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1260 Some(result) => Self::Integer(result),
1261 None => Self::float_result_or_null(*a as f64 * *b as f64),
1262 },
1263 (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1264 (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1265 (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1266 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1267 let a = self.to_integer();
1268 let b = other.to_integer();
1269 match a.checked_mul(b) {
1270 Some(result) => Self::Integer(result),
1271 None => Self::float_result_or_null(a as f64 * b as f64),
1272 }
1273 }
1274 _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1275 }
1276 }
1277
1278 const fn sort_class(&self) -> u8 {
1280 match self {
1281 Self::Null => 0,
1282 Self::Integer(_) | Self::Float(_) => 1,
1283 Self::Text(_) => 2,
1284 Self::Blob(_) => 3,
1285 }
1286 }
1287}
1288
1289pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1297 assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1298 a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1299}
1300
1301pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1308 sql_like_cased(pattern, text, escape, false)
1309}
1310
1311#[must_use]
1319pub fn sql_like_cased(
1320 pattern: &str,
1321 text: &str,
1322 escape: Option<char>,
1323 case_sensitive: bool,
1324) -> bool {
1325 if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1326 return sql_like_fast_path_matches_cased(kind, literal, text, case_sensitive);
1327 }
1328
1329 sql_like_inner(
1330 &pattern.chars().collect::<Vec<_>>(),
1331 &text.chars().collect::<Vec<_>>(),
1332 escape,
1333 0,
1334 0,
1335 case_sensitive,
1336 )
1337}
1338
1339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1340pub enum SqlLikeFastPathKind {
1341 MatchAll,
1342 Exact,
1343 Prefix,
1344 Suffix,
1345 Contains,
1346}
1347
1348impl SqlLikeFastPathKind {
1349 #[must_use]
1350 pub const fn opcode_tag(self) -> i32 {
1351 match self {
1352 Self::MatchAll => 0,
1353 Self::Exact => 1,
1354 Self::Prefix => 2,
1355 Self::Suffix => 3,
1356 Self::Contains => 4,
1357 }
1358 }
1359
1360 #[must_use]
1361 pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1362 match tag {
1363 0 => Some(Self::MatchAll),
1364 1 => Some(Self::Exact),
1365 2 => Some(Self::Prefix),
1366 3 => Some(Self::Suffix),
1367 4 => Some(Self::Contains),
1368 _ => None,
1369 }
1370 }
1371}
1372
1373#[must_use]
1374pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1375 sql_like_fast_path_matches_cased(kind, literal, text, false)
1376}
1377
1378#[must_use]
1383pub fn sql_like_fast_path_matches_cased(
1384 kind: SqlLikeFastPathKind,
1385 literal: &str,
1386 text: &str,
1387 case_sensitive: bool,
1388) -> bool {
1389 match kind {
1390 SqlLikeFastPathKind::MatchAll => true,
1391 SqlLikeFastPathKind::Exact => {
1392 if case_sensitive {
1393 literal.as_bytes() == text.as_bytes()
1394 } else {
1395 ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes())
1396 }
1397 }
1398 SqlLikeFastPathKind::Prefix => {
1399 if case_sensitive {
1400 text.as_bytes().starts_with(literal.as_bytes())
1401 } else {
1402 ascii_ci_starts_with(text, literal)
1403 }
1404 }
1405 SqlLikeFastPathKind::Suffix => {
1406 if case_sensitive {
1407 text.as_bytes().ends_with(literal.as_bytes())
1408 } else {
1409 ascii_ci_ends_with(text, literal)
1410 }
1411 }
1412 SqlLikeFastPathKind::Contains => {
1413 if case_sensitive {
1414 literal.is_empty() || memmem::find(text.as_bytes(), literal.as_bytes()).is_some()
1415 } else {
1416 ascii_ci_contains(text, literal)
1417 }
1418 }
1419 }
1420}
1421
1422pub struct SqlLikeFastPathMatcher<'a> {
1424 kind: SqlLikeFastPathKind,
1425 literal: &'a str,
1426 contains_finder: Option<memmem::Finder<'a>>,
1427 case_sensitive: bool,
1428}
1429
1430impl<'a> SqlLikeFastPathMatcher<'a> {
1431 #[must_use]
1432 pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1433 Self::new_cased(kind, literal, false)
1434 }
1435
1436 #[must_use]
1438 pub fn new_cased(kind: SqlLikeFastPathKind, literal: &'a str, case_sensitive: bool) -> Self {
1439 let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1440 .then(|| memmem::Finder::new(literal.as_bytes()));
1441 Self {
1442 kind,
1443 literal,
1444 contains_finder,
1445 case_sensitive,
1446 }
1447 }
1448
1449 #[must_use]
1450 pub fn matches(&self, text: &str) -> bool {
1451 if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1452 let text_bytes = text.as_bytes();
1453 let needle_bytes = self.literal.as_bytes();
1454 if needle_bytes.len() > text_bytes.len() {
1455 return false;
1456 }
1457 if finder.find(text_bytes).is_some() {
1458 return true;
1459 }
1460 if self.case_sensitive {
1464 return false;
1465 }
1466 return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1467 }
1468 sql_like_fast_path_matches_cased(self.kind, self.literal, text, self.case_sensitive)
1469 }
1470}
1471
1472#[must_use]
1473pub fn classify_sql_like_fast_path(
1474 pattern: &str,
1475 escape: Option<char>,
1476) -> Option<(SqlLikeFastPathKind, &str)> {
1477 if escape.is_some() || pattern.contains('_') {
1478 return None;
1479 }
1480 if !pattern.contains('%') {
1481 return Some((SqlLikeFastPathKind::Exact, pattern));
1482 }
1483 if pattern.chars().all(|ch| ch == '%') {
1484 return Some((SqlLikeFastPathKind::MatchAll, ""));
1485 }
1486
1487 let trimmed_start = pattern.trim_start_matches('%');
1488 let trimmed_end = pattern.trim_end_matches('%');
1489 if pattern.starts_with('%') && pattern.ends_with('%') {
1490 let core = trimmed_start.trim_end_matches('%');
1491 if core.is_empty() {
1492 return Some((SqlLikeFastPathKind::MatchAll, ""));
1493 }
1494 if !core.contains('%') {
1495 return Some((SqlLikeFastPathKind::Contains, core));
1496 }
1497 }
1498 if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1499 {
1500 return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1501 }
1502 if !pattern.ends_with('%')
1503 && trimmed_start.len() < pattern.len()
1504 && !trimmed_start.contains('%')
1505 {
1506 return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1507 }
1508 None
1509}
1510
1511fn sql_like_inner(
1512 pattern: &[char],
1513 text: &[char],
1514 escape: Option<char>,
1515 pi: usize,
1516 ti: usize,
1517 case_sensitive: bool,
1518) -> bool {
1519 let mut pi = pi;
1520 let mut ti = ti;
1521
1522 while pi < pattern.len() {
1523 let pc = pattern[pi];
1524
1525 if Some(pc) == escape {
1527 pi += 1;
1528 if pi >= pattern.len() {
1529 return false; }
1531 if ti >= text.len() || !chars_eq(pattern[pi], text[ti], case_sensitive) {
1533 return false;
1534 }
1535 pi += 1;
1536 ti += 1;
1537 continue;
1538 }
1539
1540 match pc {
1541 '%' => {
1542 while pi < pattern.len() && pattern[pi] == '%' {
1544 pi += 1;
1545 }
1546 if pi >= pattern.len() {
1548 return true;
1549 }
1550 for start in ti..=text.len() {
1552 if sql_like_inner(pattern, text, escape, pi, start, case_sensitive) {
1553 return true;
1554 }
1555 }
1556 return false;
1557 }
1558 '_' => {
1559 if ti >= text.len() {
1560 return false;
1561 }
1562 pi += 1;
1563 ti += 1;
1564 }
1565 _ => {
1566 if ti >= text.len() || !chars_eq(pc, text[ti], case_sensitive) {
1567 return false;
1568 }
1569 pi += 1;
1570 ti += 1;
1571 }
1572 }
1573 }
1574 ti >= text.len()
1575}
1576
1577#[inline]
1580fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool {
1581 if case_sensitive {
1582 a == b
1583 } else {
1584 ascii_ci_eq(a, b)
1585 }
1586}
1587
1588fn ascii_ci_eq(a: char, b: char) -> bool {
1590 if a == b {
1591 return true;
1592 }
1593 a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1595}
1596
1597#[inline]
1598fn ascii_fold_byte(byte: u8) -> u8 {
1599 byte.to_ascii_lowercase()
1600}
1601
1602#[inline]
1603fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1604 left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1605}
1606
1607fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1608 if left.len() != right.len() {
1609 return false;
1610 }
1611 let mut idx = 0;
1612 while idx < left.len() {
1613 if !ascii_ci_eq_byte(left[idx], right[idx]) {
1614 return false;
1615 }
1616 idx += 1;
1617 }
1618 true
1619}
1620
1621fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1622 let text = text.as_bytes();
1623 let prefix = prefix.as_bytes();
1624 text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1625}
1626
1627fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1628 let text = text.as_bytes();
1629 let suffix = suffix.as_bytes();
1630 text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1631}
1632
1633fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1634 let text = text.as_bytes();
1635 let needle = needle.as_bytes();
1636 if needle.is_empty() {
1637 return true;
1638 }
1639 if needle.len() > text.len() {
1640 return false;
1641 }
1642 if memmem::find(text, needle).is_some() {
1643 return true;
1644 }
1645
1646 ascii_ci_contains_folded_scan(text, needle)
1647}
1648
1649fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1650 if needle.is_empty() {
1651 return true;
1652 }
1653 if needle.len() > text.len() {
1654 return false;
1655 }
1656 let max_start = text.len() - needle.len();
1657 let first = needle[0];
1658 let first_folded = ascii_fold_byte(first);
1659 let first_alt = if first.is_ascii_alphabetic() {
1660 first_folded.to_ascii_uppercase()
1661 } else {
1662 first_folded
1663 };
1664 let mut start = 0;
1665 while start <= max_start {
1666 let rel = if first_folded == first_alt {
1667 memchr(first_folded, &text[start..=max_start])
1668 } else {
1669 memchr2(first_folded, first_alt, &text[start..=max_start])
1670 };
1671 let Some(rel) = rel else {
1672 break;
1673 };
1674 start += rel;
1675 if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1676 return true;
1677 }
1678 start += 1;
1679 }
1680 false
1681}
1682
1683#[derive(Debug, Clone)]
1690pub struct SumAccumulator {
1691 int_sum: i64,
1693 float_sum: f64,
1696 float_err: f64,
1698 has_value: bool,
1700 is_float: bool,
1702 overflow: bool,
1704}
1705
1706impl Default for SumAccumulator {
1707 fn default() -> Self {
1708 Self::new()
1709 }
1710}
1711
1712#[inline]
1715fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1716 let s = *sum;
1717 let t = s + value;
1718 if s.abs() > value.abs() {
1719 *err += (s - t) + value;
1720 } else {
1721 *err += (value - t) + s;
1722 }
1723 *sum = t;
1724}
1725
1726impl SumAccumulator {
1727 pub const fn new() -> Self {
1729 Self {
1730 int_sum: 0,
1731 float_sum: 0.0,
1732 float_err: 0.0,
1733 has_value: false,
1734 is_float: false,
1735 overflow: false,
1736 }
1737 }
1738
1739 #[allow(clippy::cast_precision_loss)]
1741 pub fn accumulate(&mut self, val: &SqliteValue) {
1742 match val.to_sum_numeric_value() {
1743 SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1744 SqliteValue::Integer(i) => {
1745 self.has_value = true;
1746 if !self.is_float && !self.overflow {
1747 match self.int_sum.checked_add(i) {
1748 Some(result) => self.int_sum = result,
1749 None => self.overflow = true,
1750 }
1751 }
1752 kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1753 }
1754 SqliteValue::Float(f) => {
1755 self.has_value = true;
1756 self.is_float = true;
1757 kbn_step(&mut self.float_sum, &mut self.float_err, f);
1758 }
1759 }
1760 }
1761
1762 pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1766 if !self.is_float && self.overflow {
1767 return Err(SumOverflowError);
1768 }
1769 if !self.has_value {
1770 return Ok(SqliteValue::Null);
1771 }
1772 if self.is_float {
1773 Ok(SqliteValue::Float(self.float_sum + self.float_err))
1774 } else {
1775 Ok(SqliteValue::Integer(self.int_sum))
1776 }
1777 }
1778}
1779
1780#[derive(Debug, Clone, PartialEq, Eq)]
1782pub struct SumOverflowError;
1783
1784impl fmt::Display for SumOverflowError {
1785 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786 f.write_str("integer overflow in sum()")
1787 }
1788}
1789
1790impl fmt::Display for SqliteValue {
1791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1792 match self {
1793 Self::Null => f.write_str("NULL"),
1794 Self::Integer(i) => write!(f, "{i}"),
1795 Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1796 Self::Text(s) => write!(f, "'{s}'"),
1797 Self::Blob(b) => {
1798 f.write_str("X'")?;
1799 for byte in b.iter() {
1800 write!(f, "{byte:02X}")?;
1801 }
1802 f.write_str("'")
1803 }
1804 }
1805 }
1806}
1807
1808impl PartialEq for SqliteValue {
1809 fn eq(&self, other: &Self) -> bool {
1810 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1811 }
1812}
1813
1814impl Eq for SqliteValue {}
1815
1816impl PartialOrd for SqliteValue {
1817 #[inline]
1818 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1819 Some(self.cmp(other))
1820 }
1821}
1822
1823impl Ord for SqliteValue {
1824 #[inline]
1825 fn cmp(&self, other: &Self) -> Ordering {
1826 let class_a = self.sort_class();
1828 let class_b = other.sort_class();
1829
1830 if class_a != class_b {
1831 return class_a.cmp(&class_b);
1832 }
1833
1834 match (self, other) {
1835 (Self::Null, Self::Null) => Ordering::Equal,
1836 (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
1837 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
1838 (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
1839 (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
1840 (Self::Text(a), Self::Text(b)) => a.cmp(b),
1841 (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
1842 _ => unreachable!(),
1843 }
1844 }
1845}
1846
1847impl From<i64> for SqliteValue {
1848 fn from(i: i64) -> Self {
1849 Self::Integer(i)
1850 }
1851}
1852
1853impl From<i32> for SqliteValue {
1854 fn from(i: i32) -> Self {
1855 Self::Integer(i64::from(i))
1856 }
1857}
1858
1859impl From<f64> for SqliteValue {
1860 fn from(f: f64) -> Self {
1861 Self::float_result_or_null(f)
1862 }
1863}
1864
1865impl From<String> for SqliteValue {
1866 fn from(s: String) -> Self {
1867 Self::Text(SmallText::from_string(s))
1870 }
1871}
1872
1873impl From<&str> for SqliteValue {
1874 fn from(s: &str) -> Self {
1875 Self::Text(SmallText::new(s))
1876 }
1877}
1878
1879impl From<Arc<str>> for SqliteValue {
1880 fn from(s: Arc<str>) -> Self {
1881 Self::Text(SmallText::from_arc(s))
1882 }
1883}
1884
1885impl From<Vec<u8>> for SqliteValue {
1886 fn from(b: Vec<u8>) -> Self {
1887 Self::Blob(Arc::from(b))
1890 }
1891}
1892
1893impl From<&[u8]> for SqliteValue {
1894 fn from(b: &[u8]) -> Self {
1895 Self::Blob(Arc::from(b))
1896 }
1897}
1898
1899impl From<Arc<[u8]>> for SqliteValue {
1900 fn from(b: Arc<[u8]>) -> Self {
1901 Self::Blob(b)
1902 }
1903}
1904
1905impl<T: Into<Self>> From<Option<T>> for SqliteValue {
1906 fn from(opt: Option<T>) -> Self {
1907 match opt {
1908 Some(v) => v.into(),
1909 None => Self::Null,
1910 }
1911 }
1912}
1913
1914#[allow(
1918 clippy::cast_possible_truncation,
1919 clippy::cast_precision_loss,
1920 clippy::float_cmp
1921)]
1922fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
1923 let trimmed = trim_sqlite_ascii_whitespace(s);
1924 if trimmed.is_empty() {
1925 return None;
1926 }
1927 if let Ok(i) = trimmed.parse::<i64>() {
1929 return Some(SqliteValue::Integer(i));
1930 }
1931 if let Ok(f) = trimmed.parse::<f64>() {
1935 if !f.is_finite() {
1936 let lower = trimmed.to_ascii_lowercase();
1937 if lower.contains("inf") || lower.contains("nan") {
1938 return None;
1939 }
1940 }
1941 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
1944 #[allow(clippy::cast_possible_truncation)]
1945 let i = f as i64;
1946 #[allow(clippy::cast_precision_loss)]
1947 if (i as f64) == f {
1948 return Some(SqliteValue::Integer(i));
1949 }
1950 }
1951 return Some(SqliteValue::Float(f));
1952 }
1953 None
1954}
1955
1956#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1961pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
1962 if r.is_nan() {
1963 return Ordering::Greater;
1965 }
1966 if r < -9_223_372_036_854_775_808.0 {
1968 return Ordering::Greater;
1969 }
1970 if r >= 9_223_372_036_854_775_808.0 {
1971 return Ordering::Less;
1972 }
1973 let y = r as i64;
1975 match i.cmp(&y) {
1976 Ordering::Less => Ordering::Less,
1977 Ordering::Greater => Ordering::Greater,
1978 Ordering::Equal => {
1980 let s = i as f64;
1981 s.partial_cmp(&r).unwrap_or(Ordering::Equal)
1982 }
1983 }
1984}
1985
1986#[must_use]
1993pub fn format_sqlite_float(f: f64) -> String {
1994 if f.is_nan() {
1995 return "NaN".to_owned();
1996 }
1997 if f.is_infinite() {
1998 return if f.is_sign_positive() {
1999 "Inf".to_owned()
2000 } else {
2001 "-Inf".to_owned()
2002 };
2003 }
2004 render_sqlite_float_decode(&sqlite_float_decode(f))
2005}
2006
2007const SQLITE_FLOAT_SIGNIFICANT_DIGITS: usize = 17;
2008const SQLITE_FLOAT_MAX_ROUND_DIGITS: usize = 20;
2009const SQLITE_FLOAT_GENERIC_PRECISION: i32 = 16;
2010const SQLITE_POWERS_OF_TEN_FIRST: i32 = -348;
2011const SQLITE_POWERS_OF_TEN_LAST: i32 = 347;
2012
2013#[derive(Debug)]
2014struct SqliteFloatDecode {
2015 digits: Vec<u8>,
2016 decimal_point: i32,
2017 negative: bool,
2018}
2019
2020fn render_sqlite_float_decode(decoded: &SqliteFloatDecode) -> String {
2021 let exponent = decoded.decimal_point - 1;
2022 if !(-4..=SQLITE_FLOAT_GENERIC_PRECISION).contains(&exponent) {
2023 return render_sqlite_float_exponential(decoded, exponent);
2024 }
2025 render_sqlite_float_fixed(decoded, exponent)
2026}
2027
2028fn render_sqlite_float_fixed(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2029 let mut out = String::with_capacity(decoded.digits.len() + 8);
2030 if decoded.negative {
2031 out.push('-');
2032 }
2033
2034 let mut precision = SQLITE_FLOAT_GENERIC_PRECISION - exponent;
2035 let mut digit_idx = 0usize;
2036 let mut e2 = decoded.decimal_point - 1;
2037
2038 if e2 < 0 {
2039 out.push('0');
2040 } else {
2041 while e2 >= 0 {
2042 if let Some(&digit) = decoded.digits.get(digit_idx) {
2043 out.push(char::from(digit));
2044 digit_idx += 1;
2045 } else {
2046 out.push('0');
2047 }
2048 e2 -= 1;
2049 }
2050 }
2051
2052 out.push('.');
2053
2054 if e2 < -1 && precision > 0 {
2055 let zero_count = (-1 - e2).min(precision);
2056 for _ in 0..zero_count {
2057 out.push('0');
2058 }
2059 precision -= zero_count;
2060 }
2061
2062 if precision > 0 {
2063 let digits_after_decimal =
2064 (decoded.digits.len().saturating_sub(digit_idx)).min(precision as usize);
2065 for &digit in &decoded.digits[digit_idx..digit_idx + digits_after_decimal] {
2066 out.push(char::from(digit));
2067 }
2068 }
2069
2070 trim_sqlite_float_tail(&mut out);
2071 out
2072}
2073
2074fn render_sqlite_float_exponential(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2075 let mut out = String::with_capacity(decoded.digits.len() + 8);
2076 if decoded.negative {
2077 out.push('-');
2078 }
2079
2080 let first = decoded.digits.first().copied().unwrap_or(b'0');
2081 out.push(char::from(first));
2082 out.push('.');
2083 let digits_after_decimal =
2084 (decoded.digits.len().saturating_sub(1)).min(SQLITE_FLOAT_GENERIC_PRECISION as usize);
2085 for &digit in decoded.digits.iter().skip(1).take(digits_after_decimal) {
2086 out.push(char::from(digit));
2087 }
2088 trim_sqlite_float_tail(&mut out);
2089
2090 out.push('e');
2091 let mut abs_exp = exponent;
2092 if abs_exp < 0 {
2093 out.push('-');
2094 abs_exp = -abs_exp;
2095 } else {
2096 out.push('+');
2097 }
2098 if abs_exp >= 100 {
2099 out.push(char::from(b'0' + (abs_exp / 100) as u8));
2100 abs_exp %= 100;
2101 }
2102 out.push(char::from(b'0' + (abs_exp / 10) as u8));
2103 out.push(char::from(b'0' + (abs_exp % 10) as u8));
2104 out
2105}
2106
2107fn trim_sqlite_float_tail(out: &mut String) {
2108 while out.ends_with('0') {
2109 out.pop();
2110 }
2111 if out.ends_with('.') {
2112 out.push('0');
2113 }
2114}
2115
2116fn sqlite_float_decode(f: f64) -> SqliteFloatDecode {
2117 let negative = f < 0.0;
2118 let r = if negative { -f } else { f };
2119 if r == 0.0 {
2120 return SqliteFloatDecode {
2121 digits: vec![b'0'],
2122 decimal_point: 1,
2123 negative: false,
2124 };
2125 }
2126
2127 let bits = r.to_bits();
2128 let raw_exponent = ((bits >> 52) & 0x7ff) as i32;
2129 let mut mantissa = bits & 0x000f_ffff_ffff_ffff;
2130 let binary_exponent = if raw_exponent == 0 {
2131 let leading = mantissa.leading_zeros();
2132 mantissa <<= leading;
2133 -1074 - leading as i32
2134 } else {
2135 mantissa = (mantissa << 11) | (1_u64 << 63);
2136 raw_exponent - 1086
2137 };
2138
2139 let (decimal, decimal_exponent) = sqlite_fp2_convert10(mantissa, binary_exponent, 18);
2140 let mut digits = decimal.to_string().into_bytes();
2141 let mut digit_count = digits.len();
2142 let mut decimal_point = digit_count as i32 + decimal_exponent;
2143 let mut round_at = SQLITE_FLOAT_SIGNIFICANT_DIGITS;
2144
2145 if round_at < digit_count || digit_count > SQLITE_FLOAT_MAX_ROUND_DIGITS {
2146 if round_at == SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2147 round_at = sqlite_adjust_17_digit_rounding(
2148 r,
2149 &digits,
2150 decimal_exponent,
2151 digit_count,
2152 decimal_point,
2153 round_at,
2154 );
2155 }
2156 if digits.get(round_at).copied().unwrap_or(b'0') >= b'5' {
2157 let mut idx = round_at - 1;
2158 loop {
2159 digits[idx] += 1;
2160 if digits[idx] <= b'9' {
2161 break;
2162 }
2163 digits[idx] = b'0';
2164 if idx == 0 {
2165 digits.insert(0, b'1');
2166 round_at += 1;
2167 decimal_point += 1;
2168 break;
2169 }
2170 idx -= 1;
2171 }
2172 }
2173 digit_count = round_at;
2174 digits.truncate(digit_count);
2175 }
2176
2177 while digit_count > 1 && digits[digit_count - 1] == b'0' {
2178 digit_count -= 1;
2179 }
2180 digits.truncate(digit_count);
2181
2182 SqliteFloatDecode {
2183 digits,
2184 decimal_point,
2185 negative,
2186 }
2187}
2188
2189fn sqlite_adjust_17_digit_rounding(
2190 r: f64,
2191 digits: &[u8],
2192 decimal_exponent: i32,
2193 digit_count: usize,
2194 decimal_point: i32,
2195 round_at: usize,
2196) -> usize {
2197 if digits.len() <= SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2198 return round_at;
2199 }
2200
2201 if digits[15] == b'9' && digits[14] == b'9' {
2202 let mut keep = 14usize;
2203 while keep > 0 && digits[keep - 1] == b'9' {
2204 keep -= 1;
2205 }
2206 let candidate = if keep == 0 {
2207 1
2208 } else {
2209 decimal_digits_to_u64(&digits[..keep]) + 1
2210 };
2211 if r == sqlite_fp10_convert2(
2212 candidate,
2213 decimal_exponent + digit_count as i32 - keep as i32,
2214 ) {
2215 return keep + 1;
2216 }
2217 } else if decimal_point >= digit_count as i32
2218 || (digits[15] == b'0' && digits[14] == b'0' && digits[13] == b'0')
2219 {
2220 let mut keep = 13usize;
2221 while keep > 0 && digits[keep - 1] == b'0' {
2222 keep -= 1;
2223 }
2224 if keep > 0 {
2225 let candidate = decimal_digits_to_u64(&digits[..keep]);
2226 if r == sqlite_fp10_convert2(
2227 candidate,
2228 decimal_exponent + digit_count as i32 - keep as i32,
2229 ) {
2230 return keep + 1;
2231 }
2232 }
2233 }
2234
2235 round_at
2236}
2237
2238fn decimal_digits_to_u64(digits: &[u8]) -> u64 {
2239 digits
2240 .iter()
2241 .fold(0_u64, |acc, digit| acc * 10 + u64::from(*digit - b'0'))
2242}
2243
2244fn sqlite_fp2_convert10(mantissa: u64, binary_exponent: i32, digits: i32) -> (u64, i32) {
2245 let power = digits - 1 - pwr2_to_10(binary_exponent + 63);
2246 let (power_hi, power_lo) = power_of_ten(power);
2247 let (mut high, _) = sqlite_multiply_128(mantissa, power_hi);
2248 let _ = power_lo;
2249 if digits == 18 {
2250 high >>= -(binary_exponent + pwr10_to_2(power) + 2) as u32;
2251 (high.wrapping_add((high << 1) & 2) >> 1, -power)
2252 } else {
2253 high >>= -(binary_exponent + pwr10_to_2(power) + 1) as u32;
2254 (high, -power)
2255 }
2256}
2257
2258fn sqlite_fp10_convert2(decimal: u64, power: i32) -> f64 {
2259 if power < SQLITE_POWERS_OF_TEN_FIRST {
2260 return 0.0;
2261 }
2262 if power > SQLITE_POWERS_OF_TEN_LAST {
2263 return f64::INFINITY;
2264 }
2265
2266 let bit_width = 64 - decimal.leading_zeros() as i32;
2267 let binary_power = pwr10_to_2(power);
2268 let mut exponent = 53 - bit_width - binary_power;
2269 if exponent > 1074 {
2270 if exponent >= 1130 {
2271 return 0.0;
2272 }
2273 exponent = 1074;
2274 }
2275
2276 let shift = -(exponent - (64 - bit_width) + binary_power + 3);
2277 let shift = shift.clamp(0, 63) as u32;
2278 let (mut power_hi, mut power_lo) = power_of_ten(power);
2279 if power_lo != 0 {
2280 power_hi = power_hi.wrapping_add(1);
2281 power_lo = !power_lo;
2282 }
2283
2284 let shifted_decimal = decimal << (64 - bit_width);
2285 let (mut high, low) = sqlite_multiply_128(shifted_decimal, power_hi);
2286 let mid1 = (low >> 32) as u32;
2287 let mut sticky = 1_u64;
2288 if (high & low_mask(shift)) == 0 {
2289 let (mid2_high, _) = sqlite_multiply_128(shifted_decimal, u64::from(power_lo) << 32);
2290 let mid2 = (mid2_high >> 32) as u32;
2291 sticky = u64::from(mid1.wrapping_sub(mid2) > 1);
2292 high = high.wrapping_sub(u64::from(mid1 < mid2));
2293 }
2294
2295 let mut rounded = (high >> shift) | sticky;
2296 let adjust = u32::from(rounded >= (1_u64 << 55) - 2);
2297 if adjust != 0 {
2298 rounded = (rounded >> adjust) | (rounded & 1);
2299 exponent -= adjust as i32;
2300 }
2301
2302 let mut bits = (rounded + 1 + ((rounded >> 2) & 1)) >> 2;
2303 if exponent <= -972 {
2304 return f64::INFINITY;
2305 }
2306 if (bits & (1_u64 << 52)) != 0 {
2307 bits = (bits & !(1_u64 << 52)) | ((1075 - exponent) as u64) << 52;
2308 }
2309 f64::from_bits(bits)
2310}
2311
2312fn low_mask(bits: u32) -> u64 {
2313 if bits == 0 { 0 } else { (1_u64 << bits) - 1 }
2314}
2315
2316fn sqlite_multiply_128(left: u64, right: u64) -> (u64, u64) {
2317 let product = u128::from(left) * u128::from(right);
2318 ((product >> 64) as u64, product as u64)
2319}
2320
2321fn sqlite_multiply_160(high: u64, low: u32, right: u64) -> (u64, u32) {
2322 let product =
2323 u128::from(high) * u128::from(right) + ((u128::from(low) * u128::from(right)) >> 32);
2324 (
2325 (product >> 64) as u64,
2326 ((product >> 32) & u128::from(u32::MAX)) as u32,
2327 )
2328}
2329
2330fn pwr10_to_2(power: i32) -> i32 {
2331 (power * 108_853) >> 15
2332}
2333
2334fn pwr2_to_10(power: i32) -> i32 {
2335 (power * 78_913) >> 18
2336}
2337
2338fn power_of_ten(power: i32) -> (u64, u32) {
2339 const BASE: [u64; 27] = [
2340 0x8000_0000_0000_0000,
2341 0xa000_0000_0000_0000,
2342 0xc800_0000_0000_0000,
2343 0xfa00_0000_0000_0000,
2344 0x9c40_0000_0000_0000,
2345 0xc350_0000_0000_0000,
2346 0xf424_0000_0000_0000,
2347 0x9896_8000_0000_0000,
2348 0xbebc_2000_0000_0000,
2349 0xee6b_2800_0000_0000,
2350 0x9502_f900_0000_0000,
2351 0xba43_b740_0000_0000,
2352 0xe8d4_a510_0000_0000,
2353 0x9184_e72a_0000_0000,
2354 0xb5e6_20f4_8000_0000,
2355 0xe35f_a931_a000_0000,
2356 0x8e1b_c9bf_0400_0000,
2357 0xb1a2_bc2e_c500_0000,
2358 0xde0b_6b3a_7640_0000,
2359 0x8ac7_2304_89e8_0000,
2360 0xad78_ebc5_ac62_0000,
2361 0xd8d7_26b7_177a_8000,
2362 0x8786_7832_6eac_9000,
2363 0xa968_163f_0a57_b400,
2364 0xd3c2_1bce_cced_a100,
2365 0x8459_5161_4014_84a0,
2366 0xa56f_a5b9_9019_a5c8,
2367 ];
2368 const SCALE: [u64; 26] = [
2369 0x8049_a4ac_0c58_11ae,
2370 0xcf42_894a_5dce_35ea,
2371 0xa76c_5823_38ed_2621,
2372 0x873e_4f75_e222_4e68,
2373 0xda7f_5bf5_9096_6848,
2374 0xb080_392c_c434_9dec,
2375 0x8e93_8662_882a_f53e,
2376 0xe658_29b3_046b_0afa,
2377 0xba12_1a46_50e4_ddeb,
2378 0x964e_858c_91ba_2655,
2379 0xf2d5_6790_ab41_c2a2,
2380 0xc428_d05a_a475_1e4c,
2381 0x9e74_d1b7_91e0_7e48,
2382 0xcccc_cccc_cccc_cccc,
2383 0xcecb_8f27_f420_0f3a,
2384 0xa70c_3c40_a64e_6c51,
2385 0x86f0_ac99_b4e8_dafd,
2386 0xda01_ee64_1a70_8de9,
2387 0xb01a_e745_b101_e9e4,
2388 0x8e41_ade9_fbeb_c27d,
2389 0xe5d3_ef28_2a24_2e81,
2390 0xb9a7_4a06_37ce_2ee1,
2391 0x95f8_3d0a_1fb6_9cd9,
2392 0xf24a_01a7_3cf2_dccf,
2393 0xc3b8_3581_09e8_4f07,
2394 0x9e19_db92_b4e3_1ba9,
2395 ];
2396 const SCALE_LO: [u32; 26] = [
2397 0x205b_896d,
2398 0x5206_4cad,
2399 0xaf2a_f2b8,
2400 0x5a77_44a7,
2401 0xaf39_a475,
2402 0xbd8d_794e,
2403 0x547e_b47b,
2404 0x0cb4_a5a3,
2405 0x92f3_4d62,
2406 0x3a6a_07f9,
2407 0xfae2_7299,
2408 0xaa97_e14c,
2409 0x775e_a265,
2410 0xcccc_cccc,
2411 0x0000_0000,
2412 0x9990_90b6,
2413 0x69a0_28bb,
2414 0xe80e_6f48,
2415 0x5ec0_5dd0,
2416 0x1458_8f14,
2417 0x8f16_68c9,
2418 0x6d95_3e2c,
2419 0x4abd_af10,
2420 0xbc63_3b39,
2421 0x0a86_2f81,
2422 0x6c07_a2c2,
2423 ];
2424
2425 debug_assert!((SQLITE_POWERS_OF_TEN_FIRST..=SQLITE_POWERS_OF_TEN_LAST).contains(&power));
2426
2427 let (group, offset) = if power < 0 {
2428 if power == -1 {
2429 return (SCALE[13], SCALE_LO[13]);
2430 }
2431 let mut group = power / 27;
2432 let mut offset = power % 27;
2433 if offset != 0 {
2434 group -= 1;
2435 offset += 27;
2436 }
2437 (group, offset)
2438 } else if power < 27 {
2439 return (BASE[power as usize], 0);
2440 } else {
2441 (power / 27, power % 27)
2442 };
2443
2444 let scale_idx = (group + 13) as usize;
2445 let mut high = SCALE[scale_idx];
2446 if offset == 0 {
2447 return (high, SCALE_LO[scale_idx]);
2448 }
2449
2450 let (scaled, mut low) = sqlite_multiply_160(high, SCALE_LO[scale_idx], BASE[offset as usize]);
2451 high = scaled;
2452 if (high & (1_u64 << 63)) == 0 {
2453 high = (high << 1) | u64::from(low >> 31);
2454 low = (low << 1) | 1;
2455 }
2456 (high, low)
2457}
2458
2459#[cfg(test)]
2460#[allow(clippy::float_cmp, clippy::approx_constant)]
2461mod tests {
2462 use super::*;
2463
2464 struct ValuePoolTestGuard;
2465
2466 impl ValuePoolTestGuard {
2467 fn new() -> Self {
2468 pool_clear();
2469 reset_value_pool_test_stats();
2470 Self
2471 }
2472 }
2473
2474 impl Drop for ValuePoolTestGuard {
2475 fn drop(&mut self) {
2476 pool_clear();
2477 reset_value_pool_test_stats();
2478 }
2479 }
2480
2481 fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
2482 let stats = value_pool_test_stats_snapshot();
2483 eprintln!(
2484 "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
2485 stats.slab_alloc_count,
2486 stats.slab_return_count,
2487 stats.global_alloc_fallback_count,
2488 stats.slab_high_water_mark,
2489 pool_len(),
2490 );
2491 stats
2492 }
2493
2494 #[test]
2495 fn test_slab_basic_alloc_dealloc() {
2496 let _guard = ValuePoolTestGuard::new();
2497 const ROUND_TRIP_COUNT: usize = 100;
2498
2499 assert_eq!(pool_len(), 0);
2500 assert_eq!(pool_acquire(), None);
2501 assert_eq!(
2502 value_pool_test_stats_snapshot(),
2503 ValuePoolStats {
2504 slab_alloc_count: 0,
2505 slab_return_count: 0,
2506 global_alloc_fallback_count: 1,
2507 slab_high_water_mark: 0,
2508 }
2509 );
2510
2511 reset_value_pool_test_stats();
2512 for value in 0..ROUND_TRIP_COUNT {
2513 pool_return(SqliteValue::Integer(value as i64));
2514 }
2515 assert_eq!(pool_len(), ROUND_TRIP_COUNT);
2516 assert_eq!(
2517 value_pool_test_stats_snapshot(),
2518 ValuePoolStats {
2519 slab_alloc_count: 0,
2520 slab_return_count: ROUND_TRIP_COUNT,
2521 global_alloc_fallback_count: 0,
2522 slab_high_water_mark: ROUND_TRIP_COUNT,
2523 }
2524 );
2525
2526 reset_value_pool_test_stats();
2527 for expected in (0..ROUND_TRIP_COUNT).rev() {
2528 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
2529 }
2530 assert_eq!(pool_len(), 0);
2531 assert_eq!(
2532 log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
2533 ValuePoolStats {
2534 slab_alloc_count: ROUND_TRIP_COUNT,
2535 slab_return_count: 0,
2536 global_alloc_fallback_count: 0,
2537 slab_high_water_mark: 0,
2538 }
2539 );
2540 }
2541
2542 #[test]
2543 fn test_slab_exhaustion_fallback() {
2544 let _guard = ValuePoolTestGuard::new();
2545
2546 for value in 0..=VALUE_POOL_CAP {
2547 pool_return(SqliteValue::Integer(value as i64));
2548 }
2549 assert_eq!(pool_len(), VALUE_POOL_CAP);
2550 assert_eq!(
2551 value_pool_test_stats_snapshot(),
2552 ValuePoolStats {
2553 slab_alloc_count: 0,
2554 slab_return_count: VALUE_POOL_CAP,
2555 global_alloc_fallback_count: 0,
2556 slab_high_water_mark: VALUE_POOL_CAP,
2557 }
2558 );
2559
2560 reset_value_pool_test_stats();
2561 for _ in 0..VALUE_POOL_CAP {
2562 assert!(pool_acquire().is_some());
2563 }
2564 assert_eq!(pool_acquire(), None);
2565 assert_eq!(pool_len(), 0);
2566 assert_eq!(
2567 log_value_pool_test_stats("test_slab_exhaustion_fallback"),
2568 ValuePoolStats {
2569 slab_alloc_count: VALUE_POOL_CAP,
2570 slab_return_count: 0,
2571 global_alloc_fallback_count: 1,
2572 slab_high_water_mark: 0,
2573 }
2574 );
2575 }
2576
2577 #[test]
2578 fn test_slab_no_leak() {
2579 let _guard = ValuePoolTestGuard::new();
2580 const ITERATIONS: usize = 10_000;
2581
2582 let (weak_tx, weak_rx) = std::sync::mpsc::channel();
2583 let (release_tx, release_rx) = std::sync::mpsc::channel();
2584
2585 let worker = std::thread::spawn(move || {
2586 pool_clear();
2587 reset_value_pool_test_stats();
2588
2589 let mut pooled_weak = None;
2590 let mut overflow_weak = None;
2591 for value in 0..ITERATIONS {
2592 let payload: Arc<[u8]> =
2593 Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
2594 if value == 0 {
2595 pooled_weak = Some(Arc::downgrade(&payload));
2596 } else if value == ITERATIONS - 1 {
2597 overflow_weak = Some(Arc::downgrade(&payload));
2598 }
2599 pool_return(SqliteValue::Blob(payload));
2600 }
2601
2602 assert_eq!(
2603 pool_len(),
2604 VALUE_POOL_CAP,
2605 "the slab must retain at most VALUE_POOL_CAP entries",
2606 );
2607 weak_tx
2608 .send((
2609 pooled_weak.expect("capture pooled weak handle"),
2610 overflow_weak.expect("capture overflow weak handle"),
2611 log_value_pool_test_stats("test_slab_no_leak"),
2612 ))
2613 .expect("send slab leak stats");
2614 release_rx.recv().expect("wait for release");
2615 });
2616
2617 let (pooled_weak, overflow_weak, stats) =
2618 weak_rx.recv().expect("receive weak blob handles");
2619 assert!(
2620 pooled_weak.upgrade().is_some(),
2621 "pooled blob should remain alive while the owning thread is running"
2622 );
2623 assert!(
2624 overflow_weak.upgrade().is_none(),
2625 "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2626 );
2627 assert_eq!(
2628 stats,
2629 ValuePoolStats {
2630 slab_alloc_count: 0,
2631 slab_return_count: VALUE_POOL_CAP,
2632 global_alloc_fallback_count: 0,
2633 slab_high_water_mark: VALUE_POOL_CAP,
2634 }
2635 );
2636
2637 release_tx.send(()).expect("release worker thread");
2638 worker.join().expect("join worker");
2639
2640 assert!(
2641 pooled_weak.upgrade().is_none(),
2642 "thread-local slab contents must be dropped when the thread exits"
2643 );
2644 }
2645
2646 #[test]
2647 fn test_slab_thread_local_isolation() {
2648 let _guard = ValuePoolTestGuard::new();
2649
2650 pool_return(SqliteValue::Integer(11));
2651 assert_eq!(pool_len(), 1);
2652
2653 let worker = std::thread::spawn(|| {
2654 pool_clear();
2655 reset_value_pool_test_stats();
2656
2657 assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2658 pool_return(SqliteValue::Integer(22));
2659 assert_eq!(pool_len(), 1);
2660 assert_eq!(
2661 value_pool_test_stats_snapshot(),
2662 ValuePoolStats {
2663 slab_alloc_count: 0,
2664 slab_return_count: 1,
2665 global_alloc_fallback_count: 0,
2666 slab_high_water_mark: 1,
2667 }
2668 );
2669 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
2670 assert_eq!(pool_len(), 0);
2671 });
2672 worker.join().expect("join worker");
2673
2674 assert_eq!(
2675 pool_len(),
2676 1,
2677 "worker thread slab operations must not affect the caller thread"
2678 );
2679 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
2680 assert_eq!(pool_len(), 0);
2681 let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
2682 assert_eq!(
2683 stats,
2684 ValuePoolStats {
2685 slab_alloc_count: 1,
2686 slab_return_count: 1,
2687 global_alloc_fallback_count: 0,
2688 slab_high_water_mark: 1,
2689 }
2690 );
2691 }
2692
2693 #[test]
2694 fn test_slab_zero_malloc_steady_state() {
2695 let _guard = ValuePoolTestGuard::new();
2696 const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
2697 const ITERATIONS: usize = 1_000;
2698 const INITIAL_TEXT: &str =
2699 "steady-state pooled string backing store for bd-nsvud warmup payload";
2700 const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
2701
2702 assert!(
2703 REUSED_TEXT.len() <= INITIAL_TEXT.len(),
2704 "steady-state overwrite must fit within the warmed heap allocation"
2705 );
2706
2707 for _ in 0..WARM_POOL_DEPTH {
2708 pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
2709 }
2710 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2711
2712 reset_value_pool_test_stats();
2713 for _ in 0..ITERATIONS {
2714 let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
2715 let SqliteValue::Text(existing) = &mut reused else {
2716 panic!("warmed slab entry should remain a text value");
2717 };
2718 let original_ptr = existing.as_str().as_ptr();
2719 existing.overwrite(REUSED_TEXT);
2720 assert_eq!(
2721 existing.as_str().as_ptr(),
2722 original_ptr,
2723 "steady-state overwrite should reuse the warmed heap buffer",
2724 );
2725 assert_eq!(existing.as_str(), REUSED_TEXT);
2726 pool_return(reused);
2727 }
2728
2729 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2730 assert_eq!(
2731 log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
2732 ValuePoolStats {
2733 slab_alloc_count: ITERATIONS,
2734 slab_return_count: ITERATIONS,
2735 global_alloc_fallback_count: 0,
2736 slab_high_water_mark: WARM_POOL_DEPTH,
2737 }
2738 );
2739 }
2740
2741 #[test]
2742 fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
2743 let text = SmallText::new("this string is definitely longer than twenty three bytes");
2744 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2745 panic!("long text should start in heap-owned mode");
2746 };
2747 assert!(
2748 shared.get().is_none(),
2749 "long text should not allocate Arc eagerly before cloning"
2750 );
2751
2752 let cloned = text.clone();
2753
2754 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2755 panic!("original text should remain heap-owned after clone");
2756 };
2757 assert!(
2758 shared.get().is_some(),
2759 "first clone should materialize a shared Arc lazily"
2760 );
2761 assert!(
2762 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2763 "cloned text should use the shared Arc representation"
2764 );
2765 assert_eq!(text.as_str(), cloned.as_str());
2766 }
2767
2768 #[test]
2769 fn test_small_text_invalid_utf8_clone_compare_and_serde_fail_closed() {
2770 let bytes: &[u8] = &[0x80, 0xC0, 0xAF];
2771 let text = SmallText::from_bytes(bytes);
2772 assert!(!text.is_valid_utf8());
2773 assert_eq!(text.as_str_checked(), None);
2774 assert_eq!(text.as_bytes_direct(), bytes);
2775 assert_eq!(text.len(), bytes.len());
2776 assert!(!text.is_inline());
2777
2778 let cloned = text.clone();
2779 assert_eq!(cloned, text);
2780 assert_eq!(cloned.as_bytes_direct(), bytes);
2781 assert_ne!(
2782 text,
2783 SmallText::new("\u{FFFD}\u{FFFD}\u{FFFD}"),
2784 "lossy display text must not define SQLite TEXT equality"
2785 );
2786
2787 let error = serde_json::to_string(&text)
2788 .expect_err("raw TEXT must not be silently substituted during string serialization");
2789 assert!(
2790 error.to_string().contains("invalid UTF-8"),
2791 "serialization failure must explain the unsupported Rust-string boundary: {error}"
2792 );
2793 }
2794
2795 #[test]
2796 fn test_small_text_overwrite_reuses_unique_heap_buffer() {
2797 let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
2798 let (original_ptr, original_capacity) = match &text.repr {
2799 SmallTextRepr::HeapOwned { text, shared } => {
2800 assert!(shared.get().is_none(), "fresh heap text should be unshared");
2801 (text.as_ptr(), text.capacity())
2802 }
2803 _ => panic!("long text should start in heap-owned mode"),
2804 };
2805
2806 text.overwrite("another long string that still fits the same allocation");
2807
2808 match &text.repr {
2809 SmallTextRepr::HeapOwned { text, shared } => {
2810 assert!(
2811 shared.get().is_none(),
2812 "overwrite should keep text single-owner"
2813 );
2814 assert_eq!(text.as_ptr(), original_ptr);
2815 assert_eq!(text.capacity(), original_capacity);
2816 assert_eq!(
2817 text.as_str(),
2818 "another long string that still fits the same allocation"
2819 );
2820 }
2821 _ => panic!("overwrite should keep long text in heap-owned mode"),
2822 }
2823 }
2824
2825 #[test]
2826 fn test_small_text_overwrite_detaches_from_shared_arc() {
2827 let original = "this string is definitely longer than twenty three bytes";
2828 let mut text = SmallText::new(original);
2829 let (original_ptr, original_capacity) = match &text.repr {
2830 SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
2831 _ => panic!("long text should start in heap-owned mode"),
2832 };
2833 let replacement = "replacement text that must not mutate the shared clone";
2834 assert!(
2835 replacement.len() <= original_capacity,
2836 "replacement should fit the original heap allocation for this regression",
2837 );
2838 let clone = text.clone();
2839
2840 text.overwrite(replacement);
2841
2842 assert_eq!(
2843 clone.as_str(),
2844 original,
2845 "existing shared clones must keep the original contents"
2846 );
2847 assert_eq!(text.as_str(), replacement);
2848 match &text.repr {
2849 SmallTextRepr::HeapOwned { text, shared } => {
2850 assert_eq!(
2851 text.as_ptr(),
2852 original_ptr,
2853 "overwriting a cloned long string should keep the owned buffer",
2854 );
2855 assert_eq!(
2856 text.capacity(),
2857 original_capacity,
2858 "detaching from the shared cache should preserve capacity",
2859 );
2860 assert!(
2861 shared.get().is_none(),
2862 "overwrite should reset the lazy shared cache after detaching"
2863 );
2864 }
2865 _ => panic!("overwrite should restore heap-owned mode"),
2866 }
2867 }
2868
2869 #[test]
2870 fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
2871 let _guard = ValuePoolTestGuard::new();
2872
2873 pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
2874 assert_eq!(
2875 pool_len(),
2876 0,
2877 "inline text should not occupy reusable slab slots",
2878 );
2879
2880 let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
2881 let _clone = owned_text.clone();
2882 pool_return_reusable(SqliteValue::Text(owned_text));
2883 assert_eq!(
2884 pool_len(),
2885 1,
2886 "heap-owned text should stay reusable even after serving shared clones",
2887 );
2888 assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
2889 assert_eq!(pool_len(), 0);
2890
2891 let shared_text =
2892 Arc::<str>::from("this string is definitely longer than twenty three bytes");
2893 pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
2894 &shared_text,
2895 ))));
2896 assert_eq!(
2897 pool_len(),
2898 0,
2899 "arc-backed shared text should not enter the reusable slab",
2900 );
2901
2902 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
2903 pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
2904 assert_eq!(
2905 pool_len(),
2906 0,
2907 "shared blob allocations should not displace reusable slab entries",
2908 );
2909
2910 let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
2911 pool_return_reusable(SqliteValue::Blob(unique_blob));
2912 assert_eq!(
2913 pool_len(),
2914 1,
2915 "unique blob allocations should remain eligible for slab reuse",
2916 );
2917 }
2918
2919 #[test]
2920 fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
2921 let text = Arc::new(SmallText::new(
2922 "this string is definitely longer than twenty three bytes",
2923 ));
2924 let expected = text.as_str().to_owned();
2925 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2926 panic!("long text should start in heap-owned mode");
2927 };
2928 assert!(
2929 shared.get().is_none(),
2930 "shared Arc should still be lazy before concurrent clones"
2931 );
2932
2933 let barrier = Arc::new(std::sync::Barrier::new(5));
2934 let mut workers = Vec::new();
2935 for _ in 0..4 {
2936 let text = Arc::clone(&text);
2937 let barrier = Arc::clone(&barrier);
2938 let expected = expected.clone();
2939 workers.push(std::thread::spawn(move || {
2940 barrier.wait();
2941 for _ in 0..64 {
2942 let cloned = (*text).clone();
2943 assert_eq!(cloned.as_str(), expected);
2944 assert!(
2945 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2946 "concurrent clone should reuse the shared Arc representation"
2947 );
2948 }
2949 }));
2950 }
2951
2952 barrier.wait();
2953 for worker in workers {
2954 worker
2955 .join()
2956 .expect("join concurrent small-text clone worker");
2957 }
2958
2959 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2960 panic!("original text should remain heap-owned after clone promotion");
2961 };
2962 let shared = shared
2963 .get()
2964 .expect("concurrent clones should promote the lazy shared Arc");
2965 assert_eq!(shared.as_ref(), expected);
2966 assert_eq!(text.as_str(), expected);
2967 }
2968
2969 #[test]
2970 fn null_properties() {
2971 let v = SqliteValue::Null;
2972 assert!(v.is_null());
2973 assert_eq!(v.to_integer(), 0);
2974 assert_eq!(v.to_float(), 0.0);
2975 assert_eq!(v.to_text(), "");
2976 assert_eq!(v.to_string(), "NULL");
2977 }
2978
2979 #[test]
2980 fn integer_properties() {
2981 let v = SqliteValue::Integer(42);
2982 assert!(!v.is_null());
2983 assert_eq!(v.as_integer(), Some(42));
2984 assert_eq!(v.to_integer(), 42);
2985 assert_eq!(v.to_float(), 42.0);
2986 assert_eq!(v.to_text(), "42");
2987 }
2988
2989 #[test]
2990 fn float_properties() {
2991 let v = SqliteValue::Float(3.14);
2992 assert_eq!(v.as_float(), Some(3.14));
2993 assert_eq!(v.to_integer(), 3);
2994 assert_eq!(v.to_text(), "3.14");
2995 }
2996
2997 #[test]
2998 fn text_properties() {
2999 let v = SqliteValue::Text(SmallText::new("hello"));
3000 assert_eq!(v.as_text(), Some("hello"));
3001 assert_eq!(v.to_integer(), 0);
3002 assert_eq!(v.to_float(), 0.0);
3003 }
3004
3005 #[test]
3006 fn text_numeric_coercion() {
3007 let v = SqliteValue::Text(SmallText::new("123"));
3008 assert_eq!(v.to_integer(), 123);
3009 assert_eq!(v.to_float(), 123.0);
3010
3011 let v = SqliteValue::Text(SmallText::new("3.14"));
3012 assert_eq!(v.to_integer(), 3);
3013 assert_eq!(v.to_float(), 3.14);
3014 }
3015
3016 #[test]
3017 fn text_numeric_coercion_ignores_hex_text_prefixes() {
3018 let v = SqliteValue::Text(SmallText::new("0x10"));
3019 assert_eq!(v.to_integer(), 0);
3020 assert_eq!(v.to_float(), 0.0);
3021
3022 let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
3023 assert_eq!(v.to_integer(), 0);
3024 assert_eq!(v.to_float(), 0.0);
3025 }
3026
3027 #[test]
3028 fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
3029 assert_eq!(
3030 SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
3031 SqliteValue::Integer(123)
3032 );
3033 assert_eq!(
3034 SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
3035 SqliteValue::Float(0.0)
3036 );
3037 assert_eq!(
3038 SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
3039 SqliteValue::Float(123.0)
3040 );
3041 assert_eq!(
3042 SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
3043 SqliteValue::Float(1.0)
3044 );
3045 assert_eq!(
3046 SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
3047 SqliteValue::Float(123.0)
3048 );
3049 assert_eq!(
3050 SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
3051 SqliteValue::Float(0.0)
3052 );
3053 assert_eq!(
3054 SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
3055 SqliteValue::Float(123.0)
3056 );
3057 }
3058
3059 #[test]
3060 fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
3061 assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
3062 assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
3063 assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
3064 assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
3065 }
3066
3067 #[test]
3068 fn test_sqlite_value_integer_real_comparison_equal() {
3069 let int_value = SqliteValue::Integer(3);
3070 let real_value = SqliteValue::Float(3.0);
3071 assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
3072 assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
3073 }
3074
3075 #[test]
3076 fn test_sqlite_value_text_to_integer_coercion() {
3077 let text_value = SqliteValue::Text(SmallText::new("123"));
3078 let coerced = text_value.apply_affinity(TypeAffinity::Integer);
3079 assert_eq!(coerced, SqliteValue::Integer(123));
3080 }
3081
3082 #[test]
3083 fn blob_properties() {
3084 let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
3085 assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
3086 assert_eq!(v.to_integer(), 0);
3087 assert_eq!(v.to_float(), 0.0);
3088 assert_eq!(v.to_text(), "\u{07AD}");
3091 }
3092
3093 #[test]
3094 fn display_formatting() {
3095 assert_eq!(SqliteValue::Null.to_string(), "NULL");
3096 assert_eq!(SqliteValue::Integer(42).to_string(), "42");
3097 assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
3098 assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
3099 assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
3100 assert_eq!(
3101 SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
3102 "X'CAFE'"
3103 );
3104 }
3105
3106 #[test]
3107 fn sort_order_null_first() {
3108 let null = SqliteValue::Null;
3109 let int = SqliteValue::Integer(0);
3110 let text = SqliteValue::Text(SmallText::new(""));
3111 let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
3112
3113 assert!(null < int);
3114 assert!(int < text);
3115 assert!(text < blob);
3116 }
3117
3118 #[test]
3119 fn sort_order_integers() {
3120 let a = SqliteValue::Integer(1);
3121 let b = SqliteValue::Integer(2);
3122 assert!(a < b);
3123 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
3124 }
3125
3126 #[test]
3127 fn sort_order_mixed_numeric() {
3128 let int = SqliteValue::Integer(1);
3129 let float = SqliteValue::Float(1.5);
3130 assert!(int < float);
3131
3132 let int = SqliteValue::Integer(2);
3133 assert!(int > float);
3134 }
3135
3136 #[test]
3137 fn test_int_float_precision_at_i64_boundary() {
3138 let imax = SqliteValue::Integer(i64::MAX);
3142 let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
3143 assert_eq!(
3144 imax.partial_cmp(&fmax),
3145 Some(Ordering::Less),
3146 "i64::MAX must be Less than 9223372036854775808.0"
3147 );
3148
3149 let a = SqliteValue::Integer(i64::MAX);
3151 let b = SqliteValue::Integer(i64::MAX - 1);
3152 let f = SqliteValue::Float(i64::MAX as f64);
3153 assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
3155 assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
3157 assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
3158 }
3159
3160 #[test]
3161 fn test_int_float_precision_symmetric() {
3162 let i = SqliteValue::Integer(i64::MAX);
3164 let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
3165 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3166 }
3167
3168 #[test]
3169 fn test_int_float_exact_representation() {
3170 let i = SqliteValue::Integer(42);
3172 let f = SqliteValue::Float(42.0);
3173 assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
3174 assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
3175
3176 let i = SqliteValue::Integer(3);
3178 let f = SqliteValue::Float(3.5);
3179 assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
3180 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3181 }
3182
3183 #[test]
3184 fn from_conversions() {
3185 assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
3186 assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
3187 assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
3188 assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
3189 assert_eq!(
3190 SqliteValue::from(String::from("world")).as_text(),
3191 Some("world")
3192 );
3193 assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
3194 assert!(SqliteValue::from(None::<i64>).is_null());
3195 assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
3196 }
3197
3198 #[test]
3199 fn affinity() {
3200 assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
3201 assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
3202 assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
3203 assert_eq!(
3204 SqliteValue::Text(SmallText::new("")).affinity(),
3205 TypeAffinity::Text
3206 );
3207 assert_eq!(
3208 SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
3209 TypeAffinity::Blob
3210 );
3211 }
3212
3213 #[test]
3214 fn null_equality() {
3215 let a = SqliteValue::Null;
3217 let b = SqliteValue::Null;
3218 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3219 }
3220
3221 #[test]
3224 fn test_storage_class_variants() {
3225 assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
3226 assert_eq!(
3227 SqliteValue::Integer(42).storage_class(),
3228 StorageClass::Integer
3229 );
3230 assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
3231 assert_eq!(
3232 SqliteValue::Text("hi".into()).storage_class(),
3233 StorageClass::Text
3234 );
3235 assert_eq!(
3236 SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
3237 StorageClass::Blob
3238 );
3239 }
3240
3241 #[test]
3242 fn test_type_affinity_advisory_text_into_integer_ok() {
3243 let val = SqliteValue::Text("hello".into());
3246 let coerced = val.apply_affinity(TypeAffinity::Integer);
3247 assert!(coerced.as_text().is_some());
3248 assert_eq!(coerced.as_text().unwrap(), "hello");
3249
3250 let val = SqliteValue::Text("42".into());
3252 let coerced = val.apply_affinity(TypeAffinity::Integer);
3253 assert_eq!(coerced.as_integer(), Some(42));
3254 }
3255
3256 #[test]
3257 fn test_type_affinity_advisory_integer_into_text_ok() {
3258 let val = SqliteValue::Integer(42);
3260 let coerced = val.apply_affinity(TypeAffinity::Text);
3261 assert_eq!(coerced.as_text(), Some("42"));
3262 }
3263
3264 #[test]
3265 fn test_type_affinity_comparison_coercion_matches_oracle() {
3266 let val = SqliteValue::Text("123".into());
3268 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3269 assert_eq!(coerced.as_integer(), Some(123));
3270
3271 let val = SqliteValue::Text("3.14".into());
3273 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3274 assert_eq!(coerced.as_float(), Some(3.14));
3275
3276 let val = SqliteValue::Text("hello".into());
3278 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3279 assert!(coerced.as_text().is_some());
3280
3281 let val = SqliteValue::Integer(42);
3283 let coerced = val.apply_affinity(TypeAffinity::Blob);
3284 assert_eq!(coerced.as_integer(), Some(42));
3285
3286 let val = SqliteValue::Float(5.0);
3288 let coerced = val.apply_affinity(TypeAffinity::Integer);
3289 assert_eq!(coerced.as_integer(), Some(5));
3290
3291 let val = SqliteValue::Float(5.5);
3293 let coerced = val.apply_affinity(TypeAffinity::Integer);
3294 assert_eq!(coerced.as_float(), Some(5.5));
3295
3296 let val = SqliteValue::Integer(7);
3298 let coerced = val.apply_affinity(TypeAffinity::Real);
3299 assert_eq!(coerced.as_float(), Some(7.0));
3300
3301 let val = SqliteValue::Text("9".into());
3303 let coerced = val.apply_affinity(TypeAffinity::Real);
3304 assert_eq!(coerced.as_float(), Some(9.0));
3305 }
3306
3307 #[test]
3308 fn test_cast_to_numeric_uses_sqlite_cast_rules() {
3309 assert_eq!(
3310 SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
3311 SqliteValue::Integer(123)
3312 );
3313 assert_eq!(
3314 SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
3315 SqliteValue::Integer(150)
3316 );
3317 assert_eq!(
3318 SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
3319 SqliteValue::Integer(0)
3320 );
3321 assert_eq!(
3322 SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
3323 SqliteValue::Integer(123)
3324 );
3325
3326 match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
3327 SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
3328 other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
3329 }
3330 }
3331
3332 #[test]
3333 fn test_strict_table_rejects_text_into_integer() {
3334 let val = SqliteValue::Text("hello".into());
3335 let result = val.validate_strict(StrictColumnType::Integer);
3336 assert!(result.is_err());
3337 let err = result.unwrap_err();
3338 assert_eq!(err.expected, StrictColumnType::Integer);
3339 assert_eq!(err.actual, StorageClass::Text);
3340 }
3341
3342 #[test]
3343 fn test_strict_table_allows_exact_type() {
3344 let val = SqliteValue::Integer(42);
3346 assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
3347
3348 let val = SqliteValue::Float(3.14);
3350 assert!(val.validate_strict(StrictColumnType::Real).is_ok());
3351
3352 let val = SqliteValue::Text("hello".into());
3354 assert!(val.validate_strict(StrictColumnType::Text).is_ok());
3355
3356 let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
3358 assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
3359
3360 assert!(
3362 SqliteValue::Null
3363 .validate_strict(StrictColumnType::Integer)
3364 .is_ok()
3365 );
3366 assert!(
3367 SqliteValue::Null
3368 .validate_strict(StrictColumnType::Text)
3369 .is_ok()
3370 );
3371
3372 let val = SqliteValue::Integer(42);
3374 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3375 let val = SqliteValue::Text("hi".into());
3376 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3377 }
3378
3379 #[test]
3380 fn test_strict_real_accepts_integer_with_coercion() {
3381 let val = SqliteValue::Integer(42);
3383 let result = val.validate_strict(StrictColumnType::Real).unwrap();
3384 assert_eq!(result.as_float(), Some(42.0));
3385 }
3386
3387 #[test]
3388 fn test_strict_rejects_wrong_storage_classes() {
3389 assert!(
3391 SqliteValue::Float(3.14)
3392 .validate_strict(StrictColumnType::Integer)
3393 .is_err()
3394 );
3395
3396 assert!(
3398 SqliteValue::Blob(Arc::from([1u8].as_slice()))
3399 .validate_strict(StrictColumnType::Text)
3400 .is_err()
3401 );
3402
3403 assert!(
3405 SqliteValue::Integer(1)
3406 .validate_strict(StrictColumnType::Text)
3407 .is_err()
3408 );
3409
3410 assert!(
3412 SqliteValue::Text("x".into())
3413 .validate_strict(StrictColumnType::Blob)
3414 .is_err()
3415 );
3416 }
3417
3418 #[test]
3419 fn test_strict_column_type_parsing() {
3420 assert_eq!(
3421 StrictColumnType::from_type_name("INT"),
3422 Some(StrictColumnType::Integer)
3423 );
3424 assert_eq!(
3425 StrictColumnType::from_type_name("INTEGER"),
3426 Some(StrictColumnType::Integer)
3427 );
3428 assert_eq!(
3429 StrictColumnType::from_type_name("REAL"),
3430 Some(StrictColumnType::Real)
3431 );
3432 assert_eq!(
3433 StrictColumnType::from_type_name("TEXT"),
3434 Some(StrictColumnType::Text)
3435 );
3436 assert_eq!(
3437 StrictColumnType::from_type_name("BLOB"),
3438 Some(StrictColumnType::Blob)
3439 );
3440 assert_eq!(
3441 StrictColumnType::from_type_name("ANY"),
3442 Some(StrictColumnType::Any)
3443 );
3444 assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
3446 assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
3447 }
3448
3449 #[test]
3450 fn test_affinity_advisory_never_rejects() {
3451 let values = vec![
3453 SqliteValue::Null,
3454 SqliteValue::Integer(42),
3455 SqliteValue::Float(3.14),
3456 SqliteValue::Text("hello".into()),
3457 SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
3458 ];
3459 let affinities = [
3460 TypeAffinity::Integer,
3461 TypeAffinity::Text,
3462 TypeAffinity::Blob,
3463 TypeAffinity::Real,
3464 TypeAffinity::Numeric,
3465 ];
3466 for val in &values {
3467 for aff in &affinities {
3468 let _ = val.clone().apply_affinity(*aff);
3470 }
3471 }
3472 }
3473
3474 #[test]
3477 fn test_unique_allows_multiple_nulls_single_column() {
3478 let a = SqliteValue::Null;
3480 let b = SqliteValue::Null;
3481 assert!(!a.unique_eq(&b));
3482 }
3483
3484 #[test]
3485 fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
3486 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3489 let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
3490 assert!(!unique_key_duplicates(&row_a, &row_b));
3491
3492 let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
3494 let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
3495 assert!(!unique_key_duplicates(&row_a, &row_b));
3496
3497 let row_a = [SqliteValue::Null, SqliteValue::Null];
3499 let row_b = [SqliteValue::Null, SqliteValue::Null];
3500 assert!(!unique_key_duplicates(&row_a, &row_b));
3501 }
3502
3503 #[test]
3504 fn test_unique_rejects_duplicate_non_null() {
3505 let a = SqliteValue::Integer(42);
3507 let b = SqliteValue::Integer(42);
3508 assert!(a.unique_eq(&b));
3509
3510 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3512 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3513 assert!(unique_key_duplicates(&row_a, &row_b));
3514
3515 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3517 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
3518 assert!(!unique_key_duplicates(&row_a, &row_b));
3519 }
3520
3521 #[test]
3522 fn test_unique_null_vs_non_null_distinct() {
3523 let a = SqliteValue::Null;
3525 let b = SqliteValue::Integer(1);
3526 assert!(!a.unique_eq(&b));
3527 assert!(!b.unique_eq(&a));
3528
3529 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3531 let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
3532 assert!(!unique_key_duplicates(&row_a, &row_b));
3533 }
3534
3535 #[test]
3538 #[allow(clippy::cast_precision_loss)]
3539 fn test_integer_overflow_promotes_real_expr_add() {
3540 let max = SqliteValue::Integer(i64::MAX);
3541 let one = SqliteValue::Integer(1);
3542 let result = max.sql_add(&one);
3543 assert!(result.as_integer().is_none());
3545 assert!(result.as_float().is_some());
3546 assert!(result.as_float().unwrap() >= i64::MAX as f64);
3548 }
3549
3550 #[test]
3551 fn test_integer_overflow_promotes_real_expr_mul() {
3552 let max = SqliteValue::Integer(i64::MAX);
3553 let two = SqliteValue::Integer(2);
3554 let result = max.sql_mul(&two);
3555 assert!(result.as_float().is_some());
3557 }
3558
3559 #[test]
3560 fn test_integer_overflow_promotes_real_expr_sub() {
3561 let min = SqliteValue::Integer(i64::MIN);
3562 let one = SqliteValue::Integer(1);
3563 let result = min.sql_sub(&one);
3564 assert!(result.as_float().is_some());
3566 }
3567
3568 #[test]
3569 fn test_sum_overflow_errors() {
3570 let mut acc = SumAccumulator::new();
3571 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3572 acc.accumulate(&SqliteValue::Integer(1));
3573 let result = acc.finish();
3574 assert!(result.is_err());
3575 }
3576
3577 #[test]
3578 fn test_sum_overflow_then_float_returns_real() {
3579 let mut acc = SumAccumulator::new();
3580 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3581 acc.accumulate(&SqliteValue::Integer(1));
3582 acc.accumulate(&SqliteValue::Float(0.5));
3583 let result = acc.finish().unwrap();
3584 assert!(matches!(result, SqliteValue::Float(_)));
3585 }
3586
3587 #[test]
3588 fn test_sum_text_integer_literals_stay_integer() {
3589 let mut acc = SumAccumulator::new();
3590 acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
3591 acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
3592 let result = acc.finish().unwrap();
3593 assert_eq!(result.as_integer(), Some(3));
3594 }
3595
3596 #[test]
3597 fn test_sum_non_numeric_text_returns_real_zero() {
3598 let mut acc = SumAccumulator::new();
3599 acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
3600 let result = acc.finish().unwrap();
3601 assert_eq!(result.as_float(), Some(0.0));
3602 }
3603
3604 #[test]
3605 fn test_no_overflow_stays_integer() {
3606 let a = SqliteValue::Integer(100);
3608 let b = SqliteValue::Integer(200);
3609 let result = a.sql_add(&b);
3610 assert_eq!(result.as_integer(), Some(300));
3611
3612 let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
3614 assert_eq!(result.as_integer(), Some(42));
3615
3616 let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
3618 assert_eq!(result.as_integer(), Some(42));
3619 }
3620
3621 #[test]
3622 fn test_sum_null_only_returns_null() {
3623 let mut acc = SumAccumulator::new();
3624 acc.accumulate(&SqliteValue::Null);
3625 acc.accumulate(&SqliteValue::Null);
3626 let result = acc.finish().unwrap();
3627 assert!(result.is_null());
3628 }
3629
3630 #[test]
3631 fn test_sum_mixed_int_float() {
3632 let mut acc = SumAccumulator::new();
3633 acc.accumulate(&SqliteValue::Integer(10));
3634 acc.accumulate(&SqliteValue::Float(2.5));
3635 acc.accumulate(&SqliteValue::Integer(3));
3636 let result = acc.finish().unwrap();
3637 assert_eq!(result.as_float(), Some(15.5));
3639 }
3640
3641 #[test]
3642 fn test_sum_integer_only() {
3643 let mut acc = SumAccumulator::new();
3644 acc.accumulate(&SqliteValue::Integer(10));
3645 acc.accumulate(&SqliteValue::Integer(20));
3646 acc.accumulate(&SqliteValue::Integer(30));
3647 let result = acc.finish().unwrap();
3648 assert_eq!(result.as_integer(), Some(60));
3649 }
3650
3651 #[test]
3652 fn test_sql_arithmetic_null_propagation() {
3653 let n = SqliteValue::Null;
3654 let i = SqliteValue::Integer(42);
3655 assert!(n.sql_add(&i).is_null());
3656 assert!(i.sql_add(&n).is_null());
3657 assert!(n.sql_sub(&i).is_null());
3658 assert!(n.sql_mul(&i).is_null());
3659 }
3660
3661 #[test]
3662 fn test_sql_inf_arithmetic_nan_normalized_to_null() {
3663 let pos_inf = SqliteValue::Float(f64::INFINITY);
3665 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3666 assert!(pos_inf.sql_add(&neg_inf).is_null());
3667
3668 assert!(pos_inf.sql_sub(&pos_inf).is_null());
3670 }
3671
3672 #[test]
3673 fn test_sql_mul_zero_times_inf_normalized_to_null() {
3674 let zero = SqliteValue::Float(0.0);
3676 let pos_inf = SqliteValue::Float(f64::INFINITY);
3677 assert!(zero.sql_mul(&pos_inf).is_null());
3678 assert!(
3679 SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
3680 "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
3681 );
3682 }
3683
3684 #[test]
3685 fn test_sql_mul_mixed_int_float_stays_real() {
3686 let left = SqliteValue::Integer(10);
3687 let right = SqliteValue::Float(0.25);
3688 assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
3689 assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
3690 }
3691
3692 #[test]
3693 fn test_sql_inf_propagates_when_not_nan() {
3694 let pos_inf = SqliteValue::Float(f64::INFINITY);
3695 let one = SqliteValue::Integer(1);
3696 let add_result = pos_inf.sql_add(&one);
3697 assert!(
3698 matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
3699 "expected +Inf propagation, got {add_result:?}"
3700 );
3701
3702 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3703 let sub_result = neg_inf.sql_sub(&one);
3704 assert!(
3705 matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
3706 "expected -Inf propagation, got {sub_result:?}"
3707 );
3708 }
3709
3710 #[test]
3711 fn test_from_f64_nan_normalizes_to_null() {
3712 let value = SqliteValue::from(f64::NAN);
3713 assert!(value.is_null());
3714 }
3715
3716 #[test]
3717 fn test_inf_comparisons_against_finite_values() {
3718 let pos_inf = SqliteValue::Float(f64::INFINITY);
3719 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3720 let finite_hi = SqliteValue::Float(1.0e308);
3721 let finite_lo = SqliteValue::Float(-1.0e308);
3722
3723 assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
3724 assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
3725 }
3726
3727 #[test]
3730 fn test_empty_string_is_not_null() {
3731 let empty = SqliteValue::Text(SmallText::new(""));
3732 assert!(!empty.is_null());
3734 assert!(!empty.is_null());
3736 assert!(SqliteValue::Null.is_null());
3738 }
3739
3740 #[test]
3741 fn test_length_empty_string_zero() {
3742 let empty = SqliteValue::Text(SmallText::new(""));
3743 assert_eq!(empty.sql_length(), Some(0));
3744 }
3745
3746 #[test]
3747 fn test_typeof_empty_string_text() {
3748 let empty = SqliteValue::Text(SmallText::new(""));
3749 assert_eq!(empty.typeof_str(), "text");
3750 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3752 }
3753
3754 #[test]
3755 fn test_empty_string_comparisons() {
3756 let empty1 = SqliteValue::Text(SmallText::new(""));
3757 let empty2 = SqliteValue::Text(SmallText::new(""));
3758 assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
3760
3761 let null = SqliteValue::Null;
3765 assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
3766 }
3767
3768 #[test]
3769 fn test_typeof_all_variants() {
3770 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3771 assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
3772 assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
3773 assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
3774 assert_eq!(
3775 SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
3776 "blob"
3777 );
3778 }
3779
3780 #[test]
3781 fn test_sql_length_all_types() {
3782 assert_eq!(SqliteValue::Null.sql_length(), None);
3784 assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
3786 assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
3787 assert_eq!(
3789 SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
3790 Some(3)
3791 );
3792 assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
3794 assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); }
3797
3798 #[test]
3801 fn test_like_ascii_case_insensitive() {
3802 assert!(sql_like("A", "a", None));
3803 assert!(sql_like("a", "A", None));
3804 assert!(sql_like("hello", "HELLO", None));
3805 assert!(sql_like("HELLO", "hello", None));
3806 assert!(sql_like("HeLLo", "hEllO", None));
3807 }
3808
3809 #[test]
3810 fn test_like_unicode_case_sensitive_without_icu() {
3811 assert!(!sql_like("ä", "Ä", None));
3813 assert!(!sql_like("Ä", "ä", None));
3814 assert!(sql_like("ä", "ä", None));
3816 }
3817
3818 #[test]
3819 fn test_like_fast_path_does_not_fold_ascii_punctuation() {
3820 assert!(!sql_like("[", "{", None));
3821 assert!(!sql_like("@", "`", None));
3822 }
3823
3824 #[test]
3825 fn test_like_escape_handling() {
3826 assert!(sql_like("100\\%", "100%", Some('\\')));
3828 assert!(!sql_like("100\\%", "100x", Some('\\')));
3829
3830 assert!(sql_like("a\\_b", "a_b", Some('\\')));
3832 assert!(!sql_like("a\\_b", "axb", Some('\\')));
3833 }
3834
3835 #[test]
3836 fn test_like_wildcards_basic() {
3837 assert!(sql_like("%", "", None));
3839 assert!(sql_like("%", "anything", None));
3840 assert!(sql_like("a%", "abc", None));
3841 assert!(sql_like("%c", "abc", None));
3842 assert!(sql_like("a%c", "abc", None));
3843 assert!(sql_like("a%c", "aXYZc", None));
3844 assert!(!sql_like("a%c", "abd", None));
3845
3846 assert!(sql_like("_", "x", None));
3848 assert!(!sql_like("_", "", None));
3849 assert!(!sql_like("_", "xy", None));
3850 assert!(sql_like("a_c", "abc", None));
3851 assert!(!sql_like("a_c", "abbc", None));
3852 }
3853
3854 #[test]
3855 fn test_like_combined_wildcards() {
3856 assert!(sql_like("%_", "a", None));
3857 assert!(!sql_like("%_", "", None));
3858 assert!(sql_like("_%_", "ab", None));
3859 assert!(!sql_like("_%_", "a", None));
3860 assert!(sql_like("%a%b%", "xaybz", None));
3861 assert!(!sql_like("%a%b%", "xyz", None));
3862 }
3863
3864 #[test]
3865 fn test_like_exact_match() {
3866 assert!(sql_like("hello", "hello", None));
3867 assert!(!sql_like("hello", "world", None));
3868 assert!(sql_like("", "", None));
3869 assert!(!sql_like("a", "", None));
3870 assert!(!sql_like("", "a", None));
3871 }
3872
3873 #[test]
3874 fn test_like_fast_path_repeated_percent_shapes() {
3875 assert!(sql_like("ab%%", "ABcd", None));
3876 assert!(sql_like("%%cd", "abCD", None));
3877 assert!(sql_like("%%bc%%", "xxBCyy", None));
3878 assert!(sql_like("%%%%", "anything", None));
3879 }
3880
3881 #[test]
3882 fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
3883 assert!(sql_like("%éL%", "héllo", None));
3884 assert!(!sql_like("%Él%", "héllo", None));
3885 assert!(sql_like("Stra%", "straße", None));
3886 }
3887
3888 #[test]
3889 fn test_like_contains_fast_path_handles_overlapping_matches() {
3890 assert!(sql_like("%ana%", "bananas", None));
3891 assert!(sql_like("%NAN%", "baNanas", None));
3892 assert!(!sql_like("%ananasx%", "bananas", None));
3893 }
3894
3895 #[test]
3896 fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
3897 assert!(sql_like("%ß%", "straße", None));
3898 assert!(!sql_like("%SS%", "straße", None));
3899 }
3900
3901 #[test]
3904 fn test_format_sqlite_float_whole_number() {
3905 assert_eq!(format_sqlite_float(120.0), "120.0");
3906 assert_eq!(format_sqlite_float(0.0), "0.0");
3907 assert_eq!(format_sqlite_float(-42.0), "-42.0");
3908 assert_eq!(format_sqlite_float(1.0), "1.0");
3909 }
3910
3911 #[test]
3912 fn test_format_sqlite_float_fractional() {
3913 assert_eq!(format_sqlite_float(3.14), "3.14");
3914 assert_eq!(format_sqlite_float(0.5), "0.5");
3915 assert_eq!(format_sqlite_float(-0.001), "-0.001");
3916 }
3917
3918 #[test]
3919 fn test_format_sqlite_float_special() {
3920 assert_eq!(format_sqlite_float(f64::NAN), "NaN");
3921 assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
3922 assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
3923 }
3924
3925 #[test]
3926 fn test_format_sqlite_float_negative_zero() {
3927 assert_eq!(format_sqlite_float(-0.0), "0.0");
3929 assert_eq!(format_sqlite_float(0.0), "0.0");
3930 }
3931
3932 #[test]
3933 fn test_format_sqlite_float_matches_sqlite_17_digit_text_contract() {
3934 assert_eq!(format_sqlite_float(0.1 + 0.2), "0.30000000000000004");
3935 assert_eq!(format_sqlite_float(1.0 / 3.0), "0.33333333333333332");
3936 assert_eq!(format_sqlite_float(2.0 / 3.0), "0.66666666666666663");
3937 assert_eq!(format_sqlite_float(1.5e16), "15000000000000000.0");
3938 assert_eq!(
3939 format_sqlite_float(123_456_789_012_345.6),
3940 "123456789012345.59"
3941 );
3942 assert_eq!(format_sqlite_float(1.0e308), "1.0e+308");
3943 assert_eq!(format_sqlite_float(1.0e-308), "1.0e-308");
3944 assert_eq!(
3945 format_sqlite_float(9.223_372_036_854_776e18),
3946 "9.2233720368547758e+18"
3947 );
3948 }
3949
3950 #[test]
3951 fn test_float_to_text_includes_decimal_point() {
3952 let v = SqliteValue::Float(100.0);
3953 assert_eq!(v.to_text(), "100.0");
3954 let v = SqliteValue::Float(3.14);
3955 assert_eq!(v.to_text(), "3.14");
3956 }
3957
3958 #[test]
3961 fn test_scan_numeric_prefix_bare_dot() {
3962 assert_eq!(scan_numeric_prefix(b"."), 0);
3964 assert_eq!(scan_numeric_prefix(b"-."), 0);
3965 assert_eq!(scan_numeric_prefix(b"+."), 0);
3966 assert_eq!(scan_numeric_prefix(b"..1"), 0);
3967 }
3968
3969 #[test]
3970 fn test_scan_numeric_prefix_valid() {
3971 assert_eq!(scan_numeric_prefix(b"123"), 3);
3972 assert_eq!(scan_numeric_prefix(b"3.14"), 4);
3973 assert_eq!(scan_numeric_prefix(b".5"), 2);
3974 assert_eq!(scan_numeric_prefix(b"1e10"), 4);
3975 assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
3976 assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
3977 assert_eq!(scan_numeric_prefix(b"0.0"), 3);
3978 }
3979
3980 #[test]
3981 fn test_scan_numeric_prefix_empty_and_non_numeric() {
3982 assert_eq!(scan_numeric_prefix(b""), 0);
3983 assert_eq!(scan_numeric_prefix(b"abc"), 0);
3984 assert_eq!(scan_numeric_prefix(b"+"), 0);
3985 assert_eq!(scan_numeric_prefix(b"-"), 0);
3986 }
3987}