1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::cmp::Ordering;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::sync::{Arc, OnceLock};
7
8use memchr::{memchr, memchr2, memmem};
9
10use crate::{StorageClass, StrictColumnType, StrictTypeError, TextEncoding, TypeAffinity};
11
12const VALUE_POOL_CAP: usize = 256;
21
22thread_local! {
23 static VALUE_POOL: RefCell<Vec<SqliteValue>> = const { RefCell::new(Vec::new()) };
29}
30
31#[cfg(test)]
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33struct ValuePoolStats {
34 slab_alloc_count: usize,
35 slab_return_count: usize,
36 global_alloc_fallback_count: usize,
37 slab_high_water_mark: usize,
38}
39
40#[cfg(test)]
41impl ValuePoolStats {
42 const fn new() -> Self {
43 Self {
44 slab_alloc_count: 0,
45 slab_return_count: 0,
46 global_alloc_fallback_count: 0,
47 slab_high_water_mark: 0,
48 }
49 }
50}
51
52#[cfg(test)]
53thread_local! {
54 static VALUE_POOL_TEST_STATS: RefCell<ValuePoolStats> =
55 const { RefCell::new(ValuePoolStats::new()) };
56}
57
58#[cfg(test)]
59fn reset_value_pool_test_stats() {
60 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow_mut() = ValuePoolStats::new());
61}
62
63#[cfg(test)]
64fn value_pool_test_stats_snapshot() -> ValuePoolStats {
65 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow())
66}
67
68#[cfg(test)]
69fn record_value_pool_acquire(hit: bool) {
70 VALUE_POOL_TEST_STATS.with(|stats| {
71 let mut stats = stats.borrow_mut();
72 if hit {
73 stats.slab_alloc_count += 1;
74 } else {
75 stats.global_alloc_fallback_count += 1;
76 }
77 });
78}
79
80#[cfg(test)]
81fn record_value_pool_return(pool_len: usize) {
82 VALUE_POOL_TEST_STATS.with(|stats| {
83 let mut stats = stats.borrow_mut();
84 stats.slab_return_count += 1;
85 stats.slab_high_water_mark = stats.slab_high_water_mark.max(pool_len);
86 });
87}
88
89#[inline]
102pub fn pool_acquire() -> Option<SqliteValue> {
103 let value = VALUE_POOL.with(|pool| pool.borrow_mut().pop());
104 #[cfg(test)]
105 record_value_pool_acquire(value.is_some());
106 value
107}
108
109#[inline]
117pub fn pool_return(value: SqliteValue) {
118 VALUE_POOL.with(|pool| {
119 let mut pool = pool.borrow_mut();
120 if pool.len() < VALUE_POOL_CAP {
121 pool.push(value);
122 #[cfg(test)]
123 record_value_pool_return(pool.len());
124 }
125 });
127}
128
129#[inline]
132pub fn pool_return_reusable(value: SqliteValue) {
133 if value_preserves_reusable_heap_storage(&value) {
134 pool_return(value);
135 }
136}
137
138#[inline]
143pub fn pool_clear() {
144 VALUE_POOL.with(|pool| pool.borrow_mut().clear());
145}
146
147#[inline]
151pub fn pool_len() -> usize {
152 VALUE_POOL.with(|pool| pool.borrow().len())
153}
154
155#[inline]
156fn value_preserves_reusable_heap_storage(value: &SqliteValue) -> bool {
157 match value {
158 SqliteValue::Text(text) => matches!(&text.repr, SmallTextRepr::HeapOwned { .. }),
159 SqliteValue::Blob(bytes) => Arc::strong_count(bytes) == 1,
160 _ => false,
161 }
162}
163
164const SMALL_TEXT_INLINE_CAP: usize = 23;
172
173#[cfg(feature = "bench-internals")]
174static SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH: std::sync::atomic::AtomicBool =
175 std::sync::atomic::AtomicBool::new(false);
176#[cfg(feature = "bench-internals")]
177static SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH: std::sync::atomic::AtomicU64 =
178 std::sync::atomic::AtomicU64::new(0);
179
180#[cfg(feature = "bench-internals")]
182#[doc(hidden)]
183pub fn set_small_text_direct_traits_for_bench(enabled: bool) {
184 SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.store(enabled, std::sync::atomic::Ordering::Relaxed);
185}
186
187#[cfg(feature = "bench-internals")]
189#[doc(hidden)]
190pub fn reset_small_text_direct_trait_hits_for_bench() {
191 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.store(0, std::sync::atomic::Ordering::Relaxed);
192}
193
194#[cfg(feature = "bench-internals")]
196#[doc(hidden)]
197#[must_use]
198pub fn small_text_direct_trait_hits_for_bench() -> u64 {
199 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed)
200}
201
202pub struct SmallText {
210 repr: SmallTextRepr,
212}
213
214enum SmallTextRepr {
216 Inline {
218 len: u8,
219 buf: [u8; SMALL_TEXT_INLINE_CAP],
220 },
221 HeapOwned {
226 text: String,
227 shared: OnceLock<Arc<str>>,
228 },
229 HeapShared(Arc<str>),
231 Raw { bytes: Arc<[u8]>, lossy: Arc<str> },
236}
237
238impl Clone for SmallText {
239 fn clone(&self) -> Self {
240 Self {
241 repr: self.repr.clone(),
242 }
243 }
244}
245
246impl Clone for SmallTextRepr {
247 fn clone(&self) -> Self {
248 match self {
249 Self::Inline { len, buf } => Self::Inline {
250 len: *len,
251 buf: *buf,
252 },
253 Self::HeapOwned { text, shared } => {
254 let shared = Arc::clone(shared.get_or_init(|| Arc::from(text.as_str())));
255 Self::HeapShared(shared)
256 }
257 Self::HeapShared(text) => Self::HeapShared(Arc::clone(text)),
258 Self::Raw { bytes, lossy } => Self::Raw {
259 bytes: Arc::clone(bytes),
260 lossy: Arc::clone(lossy),
261 },
262 }
263 }
264}
265
266impl SmallText {
267 #[inline]
269 pub fn new(s: &str) -> Self {
270 if s.len() <= SMALL_TEXT_INLINE_CAP {
271 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
272 buf[..s.len()].copy_from_slice(s.as_bytes());
273 Self {
274 repr: SmallTextRepr::Inline {
275 len: s.len() as u8,
276 buf,
277 },
278 }
279 } else {
280 Self {
281 repr: SmallTextRepr::HeapOwned {
282 text: s.to_owned(),
283 shared: OnceLock::new(),
284 },
285 }
286 }
287 }
288
289 #[inline]
291 pub fn from_string<S>(s: S) -> Self
292 where
293 S: Into<String> + AsRef<str>,
294 {
295 if s.as_ref().len() <= SMALL_TEXT_INLINE_CAP {
296 Self::new(s.as_ref())
297 } else {
298 Self {
299 repr: SmallTextRepr::HeapOwned {
300 text: s.into(),
301 shared: OnceLock::new(),
302 },
303 }
304 }
305 }
306
307 #[inline]
309 pub fn from_arc(arc: Arc<str>) -> Self {
310 if arc.len() <= SMALL_TEXT_INLINE_CAP {
311 Self::new(&arc)
312 } else {
313 Self {
314 repr: SmallTextRepr::HeapShared(arc),
315 }
316 }
317 }
318
319 #[inline]
324 pub fn from_bytes(bytes: &[u8]) -> Self {
325 match simdutf8::basic::from_utf8(bytes) {
326 Ok(text) => Self::new(text),
327 Err(_) => Self::from_raw_bytes(Arc::from(bytes)),
328 }
329 }
330
331 #[inline]
334 pub fn from_arc_bytes(bytes: Arc<[u8]>) -> Self {
335 match simdutf8::basic::from_utf8(&bytes) {
336 Ok(text) => Self::new(text),
337 Err(_) => Self::from_raw_bytes(bytes),
338 }
339 }
340
341 #[must_use]
353 pub fn from_record_text_bytes(bytes: &[u8], encoding: TextEncoding) -> Self {
354 match encoding {
355 TextEncoding::Utf8 => Self::from_bytes(bytes),
356 TextEncoding::Utf16le | TextEncoding::Utf16be => {
357 let little_endian = matches!(encoding, TextEncoding::Utf16le);
358 let (pairs, _trailing_odd_byte) = bytes.as_chunks::<2>();
361 let units = pairs.iter().map(|pair| {
362 if little_endian {
363 u16::from_le_bytes(*pair)
364 } else {
365 u16::from_be_bytes(*pair)
366 }
367 });
368 let decoded: String = char::decode_utf16(units)
369 .map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER))
370 .collect();
371 Self::from_string(decoded)
372 }
373 }
374 }
375
376 #[must_use]
386 pub fn from_record_text_bytes_shared(bytes: &[u8], encoding: TextEncoding) -> Self {
387 match encoding {
388 TextEncoding::Utf8 => Self::from_bytes_shared(bytes),
389 TextEncoding::Utf16le | TextEncoding::Utf16be => {
390 let little_endian = matches!(encoding, TextEncoding::Utf16le);
391 let (pairs, _trailing_odd_byte) = bytes.as_chunks::<2>();
392 let units = pairs.iter().map(|pair| {
393 if little_endian {
394 u16::from_le_bytes(*pair)
395 } else {
396 u16::from_be_bytes(*pair)
397 }
398 });
399 let decoded: String = char::decode_utf16(units)
400 .map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER))
401 .collect();
402 Self::from_str_shared(&decoded)
403 }
404 }
405 }
406
407 #[inline]
411 fn from_bytes_shared(bytes: &[u8]) -> Self {
412 match simdutf8::basic::from_utf8(bytes) {
413 Ok(text) => Self::from_str_shared(text),
414 Err(_) => Self::from_raw_bytes(Arc::from(bytes)),
415 }
416 }
417
418 #[inline]
420 fn from_str_shared(s: &str) -> Self {
421 if s.len() <= SMALL_TEXT_INLINE_CAP {
422 Self::new(s)
423 } else {
424 Self {
425 repr: SmallTextRepr::HeapShared(Arc::from(s)),
426 }
427 }
428 }
429
430 #[must_use]
440 pub fn to_record_text_bytes(&self, encoding: TextEncoding) -> Cow<'_, [u8]> {
441 match encoding {
442 TextEncoding::Utf8 => Cow::Borrowed(self.as_bytes_direct()),
443 TextEncoding::Utf16le | TextEncoding::Utf16be => {
444 let little_endian = matches!(encoding, TextEncoding::Utf16le);
445 let text = self.as_str();
446 let mut bytes = Vec::with_capacity(text.len().saturating_mul(2));
447 for unit in text.encode_utf16() {
448 let pair = if little_endian {
449 unit.to_le_bytes()
450 } else {
451 unit.to_be_bytes()
452 };
453 bytes.extend_from_slice(&pair);
454 }
455 Cow::Owned(bytes)
456 }
457 }
458 }
459
460 #[inline]
461 fn from_raw_bytes(bytes: Arc<[u8]>) -> Self {
462 debug_assert!(simdutf8::basic::from_utf8(&bytes).is_err());
463 let lossy: Arc<str> = Arc::from(String::from_utf8_lossy(&bytes).into_owned());
464 Self {
465 repr: SmallTextRepr::Raw { bytes, lossy },
466 }
467 }
468
469 #[inline]
472 pub fn overwrite(&mut self, s: &str) {
473 if s.len() <= SMALL_TEXT_INLINE_CAP {
474 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
475 buf[..s.len()].copy_from_slice(s.as_bytes());
476 self.repr = SmallTextRepr::Inline {
477 len: s.len() as u8,
478 buf,
479 };
480 return;
481 }
482
483 match &mut self.repr {
484 SmallTextRepr::HeapOwned { text, shared } => {
485 text.clear();
486 text.push_str(s);
487 if shared.get().is_some() {
488 *shared = OnceLock::new();
489 }
490 }
491 _ => {
492 self.repr = SmallTextRepr::HeapOwned {
493 text: s.to_owned(),
494 shared: OnceLock::new(),
495 };
496 }
497 }
498 }
499
500 #[inline]
502 pub fn overwrite_bytes(&mut self, bytes: &[u8]) {
503 match simdutf8::basic::from_utf8(bytes) {
504 Ok(text) => self.overwrite(text),
505 Err(_) => *self = Self::from_raw_bytes(Arc::from(bytes)),
506 }
507 }
508
509 #[inline]
519 pub fn as_str(&self) -> &str {
520 match &self.repr {
521 SmallTextRepr::Inline { len, buf } => simdutf8::basic::from_utf8(&buf[..*len as usize])
522 .expect("SmallText inline representation must always contain valid UTF-8"),
523 SmallTextRepr::HeapOwned { text, .. } => text.as_str(),
524 SmallTextRepr::HeapShared(text) => text,
525 SmallTextRepr::Raw { lossy, .. } => lossy,
526 }
527 }
528
529 #[inline]
531 #[must_use]
532 pub fn as_str_checked(&self) -> Option<&str> {
533 match &self.repr {
534 SmallTextRepr::Raw { .. } => None,
535 _ => Some(self.as_str()),
536 }
537 }
538
539 #[inline]
541 #[must_use]
542 pub fn is_valid_utf8(&self) -> bool {
543 !matches!(&self.repr, SmallTextRepr::Raw { .. })
544 }
545
546 #[inline]
559 #[must_use]
560 pub fn as_bytes_direct(&self) -> &[u8] {
561 match &self.repr {
562 SmallTextRepr::Inline { len, buf } => &buf[..*len as usize],
563 SmallTextRepr::HeapOwned { text, .. } => text.as_bytes(),
564 SmallTextRepr::HeapShared(text) => text.as_bytes(),
565 SmallTextRepr::Raw { bytes, .. } => bytes,
566 }
567 }
568
569 #[inline]
571 pub fn len(&self) -> usize {
572 match &self.repr {
573 SmallTextRepr::Inline { len, .. } => *len as usize,
574 SmallTextRepr::HeapOwned { text, .. } => text.len(),
575 SmallTextRepr::HeapShared(text) => text.len(),
576 SmallTextRepr::Raw { bytes, .. } => bytes.len(),
577 }
578 }
579
580 #[inline]
582 pub fn is_empty(&self) -> bool {
583 self.len() == 0
584 }
585
586 #[inline]
588 pub fn is_inline(&self) -> bool {
589 matches!(&self.repr, SmallTextRepr::Inline { .. })
590 }
591
592 #[inline]
594 pub fn into_arc(self) -> Arc<str> {
595 match self.repr {
596 SmallTextRepr::Inline { len, buf } => {
597 let s = simdutf8::basic::from_utf8(&buf[..len as usize])
599 .expect("SmallText inline representation must always contain valid UTF-8");
600 Arc::from(s)
601 }
602 SmallTextRepr::HeapOwned { text, shared } => shared
603 .into_inner()
604 .unwrap_or_else(|| Arc::<str>::from(text)),
605 SmallTextRepr::HeapShared(text) => text,
606 SmallTextRepr::Raw { lossy, .. } => lossy,
607 }
608 }
609}
610
611impl Default for SmallText {
612 #[inline]
613 fn default() -> Self {
614 Self::new("")
615 }
616}
617
618impl fmt::Debug for SmallText {
619 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620 fmt::Debug::fmt(self.as_str(), f)
621 }
622}
623
624impl fmt::Display for SmallText {
625 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626 fmt::Display::fmt(self.as_str(), f)
627 }
628}
629
630impl PartialEq for SmallText {
631 #[inline]
632 fn eq(&self, other: &Self) -> bool {
633 #[cfg(feature = "bench-internals")]
634 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
635 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
636 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
637 return self.as_bytes_direct() == other.as_bytes_direct();
638 }
639 match (self.as_str_checked(), other.as_str_checked()) {
640 (Some(left), Some(right)) => left == right,
641 _ => self.as_bytes_direct() == other.as_bytes_direct(),
642 }
643 }
644}
645
646impl Eq for SmallText {}
647
648impl PartialOrd for SmallText {
649 #[inline]
650 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
651 Some(self.cmp(other))
652 }
653}
654
655impl Ord for SmallText {
656 #[inline]
657 fn cmp(&self, other: &Self) -> Ordering {
658 #[cfg(feature = "bench-internals")]
659 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
660 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
661 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
662 return self.as_bytes_direct().cmp(other.as_bytes_direct());
663 }
664 match (self.as_str_checked(), other.as_str_checked()) {
665 (Some(left), Some(right)) => left.cmp(right),
666 _ => self.as_bytes_direct().cmp(other.as_bytes_direct()),
667 }
668 }
669}
670
671impl Hash for SmallText {
672 #[inline]
673 fn hash<H: Hasher>(&self, state: &mut H) {
674 #[cfg(feature = "bench-internals")]
675 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
676 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
677 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
678 state.write(self.as_bytes_direct());
679 state.write_u8(0xff);
680 return;
681 }
682 if let Some(text) = self.as_str_checked() {
683 text.hash(state);
684 } else {
685 state.write(self.as_bytes_direct());
686 state.write_u8(0xff);
687 }
688 }
689}
690
691impl From<&str> for SmallText {
692 #[inline]
693 fn from(s: &str) -> Self {
694 Self::new(s)
695 }
696}
697
698impl From<String> for SmallText {
699 #[inline]
700 fn from(s: String) -> Self {
701 Self::from_string(s)
702 }
703}
704
705impl From<Arc<str>> for SmallText {
706 #[inline]
707 fn from(arc: Arc<str>) -> Self {
708 Self::from_arc(arc)
709 }
710}
711
712impl AsRef<str> for SmallText {
713 #[inline]
714 fn as_ref(&self) -> &str {
715 self.as_str()
716 }
717}
718
719impl std::ops::Deref for SmallText {
720 type Target = str;
721
722 #[inline]
723 fn deref(&self) -> &Self::Target {
724 self.as_str()
725 }
726}
727
728impl std::borrow::Borrow<str> for SmallText {
729 #[inline]
730 fn borrow(&self) -> &str {
731 self.as_str()
732 }
733}
734
735impl serde::Serialize for SmallText {
737 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
738 where
739 S: serde::Serializer,
740 {
741 let Some(text) = self.as_str_checked() else {
742 return Err(serde::ser::Error::custom(
743 "SQLite TEXT containing invalid UTF-8 cannot be serialized as a Rust string",
744 ));
745 };
746 serializer.serialize_str(text)
747 }
748}
749
750impl<'de> serde::Deserialize<'de> for SmallText {
751 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
752 where
753 D: serde::Deserializer<'de>,
754 {
755 let s = String::deserialize(deserializer)?;
756 Ok(Self::from_string(s))
757 }
758}
759
760fn scan_numeric_prefix(bytes: &[u8]) -> usize {
766 if bytes.is_empty() {
767 return 0;
768 }
769
770 let mut i = 0usize;
771 if bytes[i] == b'+' || bytes[i] == b'-' {
772 i += 1;
773 }
774
775 let mut has_digit = false;
776 while i < bytes.len() && bytes[i].is_ascii_digit() {
777 has_digit = true;
778 i += 1;
779 }
780
781 if i < bytes.len() && bytes[i] == b'.' {
782 i += 1;
783 while i < bytes.len() && bytes[i].is_ascii_digit() {
784 has_digit = true;
785 i += 1;
786 }
787 }
788
789 if !has_digit {
790 return 0;
791 }
792
793 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
794 let exp_start = i;
795 i += 1;
796 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
797 i += 1;
798 }
799 if i < bytes.len() && bytes[i].is_ascii_digit() {
800 while i < bytes.len() && bytes[i].is_ascii_digit() {
801 i += 1;
802 }
803 } else {
804 i = exp_start;
805 }
806 }
807
808 i
809}
810
811#[allow(clippy::cast_possible_truncation)]
813fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
814 let mut start = 0;
815 while start < b.len() && b[start].is_ascii_whitespace() {
816 start += 1;
817 }
818 let trimmed = &b[start..];
819 let end = scan_numeric_prefix(trimmed);
820 if end == 0 {
821 return 0;
822 }
823 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
826 let f = s.parse::<f64>().unwrap_or(0.0);
827 #[allow(clippy::manual_clamp)]
828 if f >= i64::MAX as f64 {
829 i64::MAX
830 } else if f <= i64::MIN as f64 {
831 i64::MIN
832 } else {
833 f as i64
834 }
835}
836
837#[allow(clippy::cast_possible_truncation)]
839fn parse_integer_prefix(s: &str) -> i64 {
840 parse_integer_prefix_bytes(s.as_bytes())
841}
842
843fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
845 let mut start = 0;
846 while start < b.len() && b[start].is_ascii_whitespace() {
847 start += 1;
848 }
849 let trimmed = &b[start..];
850 let end = scan_numeric_prefix(trimmed);
851 if end == 0 {
852 return 0.0;
853 }
854 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
857 s.parse::<f64>().unwrap_or(0.0)
858}
859
860fn parse_float_prefix(s: &str) -> f64 {
862 parse_float_prefix_bytes(s.as_bytes())
863}
864
865fn trim_sqlite_ascii_whitespace(s: &str) -> &str {
866 s.trim_matches(|ch: char| ch.is_ascii_whitespace())
867}
868
869fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
870 let trimmed = trim_sqlite_ascii_whitespace(s);
871 let end = scan_numeric_prefix(trimmed.as_bytes());
872 if end == 0 {
873 return SqliteValue::Integer(0);
874 }
875
876 let prefix = &trimmed[..end];
877 let is_integer_syntax = !prefix
878 .as_bytes()
879 .iter()
880 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
881
882 if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
883 return SqliteValue::Integer(value);
884 }
885
886 if let Ok(value) = prefix.parse::<f64>() {
887 if value.is_finite()
888 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
889 {
890 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
891 let truncated = value as i64;
892 #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
893 if truncated as f64 == value {
894 return SqliteValue::Integer(truncated);
895 }
896 }
897 return SqliteValue::Float(value);
898 }
899
900 SqliteValue::Integer(0)
901}
902
903#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
908pub enum SqliteValue {
909 Null,
911 Integer(i64),
913 Float(f64),
915 Text(SmallText),
921 Blob(Arc<[u8]>),
925}
926
927impl SqliteValue {
928 pub const fn affinity(&self) -> TypeAffinity {
930 match self {
931 Self::Null | Self::Blob(_) => TypeAffinity::Blob,
932 Self::Integer(_) => TypeAffinity::Integer,
933 Self::Float(_) => TypeAffinity::Real,
934 Self::Text(_) => TypeAffinity::Text,
935 }
936 }
937
938 pub const fn storage_class(&self) -> StorageClass {
940 match self {
941 Self::Null => StorageClass::Null,
942 Self::Integer(_) => StorageClass::Integer,
943 Self::Float(_) => StorageClass::Real,
944 Self::Text(_) => StorageClass::Text,
945 Self::Blob(_) => StorageClass::Blob,
946 }
947 }
948
949 #[must_use]
961 #[allow(
962 clippy::cast_possible_truncation,
963 clippy::cast_precision_loss,
964 clippy::float_cmp
965 )]
966 pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
967 match affinity {
968 TypeAffinity::Blob => self,
969 TypeAffinity::Text => match self {
970 Self::Null | Self::Text(_) | Self::Blob(_) => self,
971 Self::Integer(_) | Self::Float(_) => {
972 let t = self.to_text();
973 Self::Text(SmallText::from_string(t))
974 }
975 },
976 TypeAffinity::Numeric | TypeAffinity::Integer => match &self {
977 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
978 Self::Float(f) => {
979 if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
980 let i = *f as i64;
981 if (i as f64) == *f {
982 return Self::Integer(i);
983 }
984 }
985 self
986 }
987 _ => self,
988 },
989 TypeAffinity::Real => match &self {
990 Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
991 .map(|v| match v {
992 Self::Integer(i) => Self::Float(i as f64),
993 other => other,
994 })
995 .unwrap_or(self),
996 Self::Integer(i) => Self::Float(*i as f64),
997 _ => self,
998 },
999 }
1000 }
1001
1002 #[allow(clippy::cast_precision_loss)]
1009 pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
1010 if matches!(self, Self::Null) {
1011 return Ok(self);
1012 }
1013 match col_type {
1014 StrictColumnType::Any => Ok(self),
1015 StrictColumnType::Integer => match self {
1016 Self::Integer(_) => Ok(self),
1017 Self::Float(fl) => {
1022 #[allow(clippy::cast_possible_truncation)]
1023 let as_int = fl as i64;
1024 #[allow(clippy::float_cmp)]
1025 if as_int as f64 == fl {
1026 Ok(Self::Integer(as_int))
1027 } else {
1028 Err(StrictTypeError {
1029 expected: col_type,
1030 actual: StorageClass::Real,
1031 })
1032 }
1033 }
1034 Self::Text(s) => match try_coerce_text_to_numeric(s.as_str()) {
1039 Some(v @ Self::Integer(_)) => Ok(v),
1040 _ => Err(StrictTypeError {
1041 expected: col_type,
1042 actual: StorageClass::Text,
1043 }),
1044 },
1045 other => Err(StrictTypeError {
1046 expected: col_type,
1047 actual: other.storage_class(),
1048 }),
1049 },
1050 StrictColumnType::Real => match self {
1051 Self::Float(_) => Ok(self),
1052 Self::Integer(i) => Ok(Self::Float(i as f64)),
1053 Self::Text(s) => match try_coerce_text_to_numeric(s.as_str()) {
1056 Some(Self::Integer(i)) => Ok(Self::Float(i as f64)),
1057 Some(v @ Self::Float(_)) => Ok(v),
1058 _ => Err(StrictTypeError {
1059 expected: col_type,
1060 actual: StorageClass::Text,
1061 }),
1062 },
1063 other => Err(StrictTypeError {
1064 expected: col_type,
1065 actual: other.storage_class(),
1066 }),
1067 },
1068 StrictColumnType::Text => match self {
1069 Self::Text(_) => Ok(self),
1070 Self::Integer(i) => Ok(Self::Text(SmallText::from_string(i.to_string()))),
1075 Self::Float(fl) => {
1076 Ok(Self::Text(SmallText::from_string(format_sqlite_float(fl))))
1077 }
1078 other => Err(StrictTypeError {
1079 expected: col_type,
1080 actual: other.storage_class(),
1081 }),
1082 },
1083 StrictColumnType::Blob => match self {
1084 Self::Blob(_) => Ok(self),
1085 other => Err(StrictTypeError {
1086 expected: col_type,
1087 actual: other.storage_class(),
1088 }),
1089 },
1090 }
1091 }
1092
1093 #[inline(always)]
1095 #[allow(clippy::inline_always)]
1096 pub const fn is_null(&self) -> bool {
1097 matches!(self, Self::Null)
1098 }
1099
1100 #[inline]
1102 pub const fn as_integer(&self) -> Option<i64> {
1103 match self {
1104 Self::Integer(i) => Some(*i),
1105 _ => None,
1106 }
1107 }
1108
1109 #[inline]
1111 pub fn as_float(&self) -> Option<f64> {
1112 match self {
1113 Self::Float(f) => Some(*f),
1114 _ => None,
1115 }
1116 }
1117
1118 #[inline]
1120 pub fn as_text(&self) -> Option<&str> {
1121 match self {
1122 Self::Text(s) => Some(s),
1123 _ => None,
1124 }
1125 }
1126
1127 #[inline]
1129 pub fn as_blob(&self) -> Option<&[u8]> {
1130 match self {
1131 Self::Blob(b) => Some(b),
1132 _ => None,
1133 }
1134 }
1135
1136 #[inline(always)]
1144 #[allow(clippy::inline_always)]
1145 #[allow(clippy::cast_possible_truncation)]
1146 pub fn to_integer(&self) -> i64 {
1147 match self {
1148 Self::Null => 0,
1149 Self::Integer(i) => *i,
1150 Self::Float(f) => *f as i64,
1151 Self::Text(s) => parse_integer_prefix(s),
1152 Self::Blob(b) => parse_integer_prefix_bytes(b),
1153 }
1154 }
1155
1156 #[inline(always)]
1164 #[allow(clippy::inline_always)]
1165 #[allow(clippy::cast_precision_loss)]
1166 pub fn to_float(&self) -> f64 {
1167 match self {
1168 Self::Null => 0.0,
1169 Self::Integer(i) => *i as f64,
1170 Self::Float(f) => *f,
1171 Self::Text(s) => parse_float_prefix(s),
1172 Self::Blob(b) => parse_float_prefix_bytes(b),
1173 }
1174 }
1175
1176 #[must_use]
1183 pub fn to_sum_numeric_value(&self) -> Self {
1184 match self {
1185 Self::Null => Self::Null,
1186 Self::Integer(i) => Self::Integer(*i),
1187 Self::Float(f) => Self::Float(*f),
1188 Self::Text(s) => {
1189 let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
1190 if let Ok(integer) = trimmed.parse::<i64>() {
1191 Self::Integer(integer)
1192 } else {
1193 Self::Float(parse_float_prefix(s))
1194 }
1195 }
1196 Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
1197 }
1198 }
1199
1200 #[inline]
1206 #[must_use]
1207 pub fn as_text_str(&self) -> Option<&str> {
1208 match self {
1209 Self::Text(s) => Some(s),
1210 _ => None,
1211 }
1212 }
1213
1214 #[inline]
1216 #[must_use]
1217 pub fn as_blob_bytes(&self) -> Option<&[u8]> {
1218 match self {
1219 Self::Blob(b) => Some(b),
1220 _ => None,
1221 }
1222 }
1223
1224 pub fn to_text(&self) -> String {
1231 match self {
1232 Self::Null => String::new(),
1233 Self::Integer(i) => i.to_string(),
1234 Self::Float(f) => format_sqlite_float(*f),
1235 Self::Text(s) => s.to_string(),
1236 Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
1237 }
1238 }
1239
1240 #[must_use]
1246 pub fn cast_to_numeric(&self) -> Self {
1247 match self {
1248 Self::Null => Self::Null,
1249 Self::Integer(i) => Self::Integer(*i),
1250 Self::Float(f) => Self::Float(*f),
1251 Self::Text(s) => cast_text_prefix_to_numeric(s),
1252 Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
1253 }
1254 }
1255
1256 pub const fn typeof_str(&self) -> &'static str {
1260 match self {
1261 Self::Null => "null",
1262 Self::Integer(_) => "integer",
1263 Self::Float(_) => "real",
1264 Self::Text(_) => "text",
1265 Self::Blob(_) => "blob",
1266 }
1267 }
1268
1269 pub fn sql_length(&self) -> Option<i64> {
1276 match self {
1277 Self::Null => None,
1278 Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
1279 Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
1280 Self::Integer(_) | Self::Float(_) => {
1281 let t = self.to_text();
1282 Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
1283 }
1284 }
1285 }
1286
1287 pub fn unique_eq(&self, other: &Self) -> bool {
1293 if self.is_null() || other.is_null() {
1294 return false;
1295 }
1296 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1297 }
1298
1299 fn float_result_or_null(result: f64) -> Self {
1303 if result.is_nan() {
1304 Self::Null
1305 } else {
1306 Self::Float(result)
1307 }
1308 }
1309
1310 #[inline]
1316 pub fn is_integer_numeric_type(&self) -> bool {
1317 fn text_is_integer_numeric_type(s: &str) -> bool {
1318 let trimmed = s.trim_start();
1319 let end = scan_numeric_prefix(trimmed.as_bytes());
1320 end > 0
1321 && !trimmed.as_bytes()[..end]
1322 .iter()
1323 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1324 }
1325
1326 match self {
1327 Self::Integer(_) => true,
1328 Self::Float(_) | Self::Null => false,
1329 Self::Text(s) => text_is_integer_numeric_type(s),
1330 Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1331 }
1332 }
1333
1334 #[inline]
1339 fn is_float_numeric_type(&self) -> bool {
1340 fn text_is_float(s: &str) -> bool {
1341 let trimmed = s.trim_start();
1342 let end = scan_numeric_prefix(trimmed.as_bytes());
1343 end > 0
1344 && trimmed.as_bytes()[..end]
1345 .iter()
1346 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1347 }
1348 match self {
1349 Self::Float(_) => true,
1350 Self::Integer(_) | Self::Null => false,
1351 Self::Text(s) => text_is_float(s),
1352 Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1353 }
1354 }
1355
1356 #[inline(always)]
1364 #[allow(clippy::inline_always)]
1365 #[must_use]
1366 #[allow(clippy::cast_precision_loss)]
1367 pub fn sql_add(&self, other: &Self) -> Self {
1368 match (self, other) {
1369 (Self::Null, _) | (_, Self::Null) => Self::Null,
1370 (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1371 Some(result) => Self::Integer(result),
1372 None => Self::float_result_or_null(*a as f64 + *b as f64),
1373 },
1374 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1378 let a = self.to_integer();
1379 let b = other.to_integer();
1380 match a.checked_add(b) {
1381 Some(result) => Self::Integer(result),
1382 None => Self::float_result_or_null(a as f64 + b as f64),
1383 }
1384 }
1385 _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1386 }
1387 }
1388
1389 #[inline(always)]
1393 #[allow(clippy::inline_always)]
1394 #[must_use]
1395 #[allow(clippy::cast_precision_loss)]
1396 pub fn sql_sub(&self, other: &Self) -> Self {
1397 match (self, other) {
1398 (Self::Null, _) | (_, Self::Null) => Self::Null,
1399 (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1400 Some(result) => Self::Integer(result),
1401 None => Self::float_result_or_null(*a as f64 - *b as f64),
1402 },
1403 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1404 let a = self.to_integer();
1405 let b = other.to_integer();
1406 match a.checked_sub(b) {
1407 Some(result) => Self::Integer(result),
1408 None => Self::float_result_or_null(a as f64 - b as f64),
1409 }
1410 }
1411 _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1412 }
1413 }
1414
1415 #[inline(always)]
1419 #[allow(clippy::inline_always)]
1420 #[must_use]
1421 #[allow(clippy::cast_precision_loss)]
1422 pub fn sql_mul(&self, other: &Self) -> Self {
1423 match (self, other) {
1424 (Self::Null, _) | (_, Self::Null) => Self::Null,
1425 (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1426 Some(result) => Self::Integer(result),
1427 None => Self::float_result_or_null(*a as f64 * *b as f64),
1428 },
1429 (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1430 (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1431 (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1432 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1433 let a = self.to_integer();
1434 let b = other.to_integer();
1435 match a.checked_mul(b) {
1436 Some(result) => Self::Integer(result),
1437 None => Self::float_result_or_null(a as f64 * b as f64),
1438 }
1439 }
1440 _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1441 }
1442 }
1443
1444 const fn sort_class(&self) -> u8 {
1446 match self {
1447 Self::Null => 0,
1448 Self::Integer(_) | Self::Float(_) => 1,
1449 Self::Text(_) => 2,
1450 Self::Blob(_) => 3,
1451 }
1452 }
1453}
1454
1455pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1463 assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1464 a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1465}
1466
1467pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1474 sql_like_cased(pattern, text, escape, false)
1475}
1476
1477#[must_use]
1485pub fn sql_like_cased(
1486 pattern: &str,
1487 text: &str,
1488 escape: Option<char>,
1489 case_sensitive: bool,
1490) -> bool {
1491 if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1492 return sql_like_fast_path_matches_cased(kind, literal, text, case_sensitive);
1493 }
1494
1495 sql_like_inner(
1496 &pattern.chars().collect::<Vec<_>>(),
1497 &text.chars().collect::<Vec<_>>(),
1498 escape,
1499 0,
1500 0,
1501 case_sensitive,
1502 )
1503}
1504
1505#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1506pub enum SqlLikeFastPathKind {
1507 MatchAll,
1508 Exact,
1509 Prefix,
1510 Suffix,
1511 Contains,
1512}
1513
1514impl SqlLikeFastPathKind {
1515 #[must_use]
1516 pub const fn opcode_tag(self) -> i32 {
1517 match self {
1518 Self::MatchAll => 0,
1519 Self::Exact => 1,
1520 Self::Prefix => 2,
1521 Self::Suffix => 3,
1522 Self::Contains => 4,
1523 }
1524 }
1525
1526 #[must_use]
1527 pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1528 match tag {
1529 0 => Some(Self::MatchAll),
1530 1 => Some(Self::Exact),
1531 2 => Some(Self::Prefix),
1532 3 => Some(Self::Suffix),
1533 4 => Some(Self::Contains),
1534 _ => None,
1535 }
1536 }
1537}
1538
1539#[must_use]
1540pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1541 sql_like_fast_path_matches_cased(kind, literal, text, false)
1542}
1543
1544#[must_use]
1549pub fn sql_like_fast_path_matches_cased(
1550 kind: SqlLikeFastPathKind,
1551 literal: &str,
1552 text: &str,
1553 case_sensitive: bool,
1554) -> bool {
1555 match kind {
1556 SqlLikeFastPathKind::MatchAll => true,
1557 SqlLikeFastPathKind::Exact => {
1558 if case_sensitive {
1559 literal.as_bytes() == text.as_bytes()
1560 } else {
1561 ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes())
1562 }
1563 }
1564 SqlLikeFastPathKind::Prefix => {
1565 if case_sensitive {
1566 text.as_bytes().starts_with(literal.as_bytes())
1567 } else {
1568 ascii_ci_starts_with(text, literal)
1569 }
1570 }
1571 SqlLikeFastPathKind::Suffix => {
1572 if case_sensitive {
1573 text.as_bytes().ends_with(literal.as_bytes())
1574 } else {
1575 ascii_ci_ends_with(text, literal)
1576 }
1577 }
1578 SqlLikeFastPathKind::Contains => {
1579 if case_sensitive {
1580 literal.is_empty() || memmem::find(text.as_bytes(), literal.as_bytes()).is_some()
1581 } else {
1582 ascii_ci_contains(text, literal)
1583 }
1584 }
1585 }
1586}
1587
1588pub struct SqlLikeFastPathMatcher<'a> {
1590 kind: SqlLikeFastPathKind,
1591 literal: &'a str,
1592 contains_finder: Option<memmem::Finder<'a>>,
1593 case_sensitive: bool,
1594}
1595
1596impl<'a> SqlLikeFastPathMatcher<'a> {
1597 #[must_use]
1598 pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1599 Self::new_cased(kind, literal, false)
1600 }
1601
1602 #[must_use]
1604 pub fn new_cased(kind: SqlLikeFastPathKind, literal: &'a str, case_sensitive: bool) -> Self {
1605 let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1606 .then(|| memmem::Finder::new(literal.as_bytes()));
1607 Self {
1608 kind,
1609 literal,
1610 contains_finder,
1611 case_sensitive,
1612 }
1613 }
1614
1615 #[must_use]
1616 pub fn matches(&self, text: &str) -> bool {
1617 if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1618 let text_bytes = text.as_bytes();
1619 let needle_bytes = self.literal.as_bytes();
1620 if needle_bytes.len() > text_bytes.len() {
1621 return false;
1622 }
1623 if finder.find(text_bytes).is_some() {
1624 return true;
1625 }
1626 if self.case_sensitive {
1630 return false;
1631 }
1632 return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1633 }
1634 sql_like_fast_path_matches_cased(self.kind, self.literal, text, self.case_sensitive)
1635 }
1636}
1637
1638#[must_use]
1639pub fn classify_sql_like_fast_path(
1640 pattern: &str,
1641 escape: Option<char>,
1642) -> Option<(SqlLikeFastPathKind, &str)> {
1643 if escape.is_some() || pattern.contains('_') {
1644 return None;
1645 }
1646 if !pattern.contains('%') {
1647 return Some((SqlLikeFastPathKind::Exact, pattern));
1648 }
1649 if pattern.chars().all(|ch| ch == '%') {
1650 return Some((SqlLikeFastPathKind::MatchAll, ""));
1651 }
1652
1653 let trimmed_start = pattern.trim_start_matches('%');
1654 let trimmed_end = pattern.trim_end_matches('%');
1655 if pattern.starts_with('%') && pattern.ends_with('%') {
1656 let core = trimmed_start.trim_end_matches('%');
1657 if core.is_empty() {
1658 return Some((SqlLikeFastPathKind::MatchAll, ""));
1659 }
1660 if !core.contains('%') {
1661 return Some((SqlLikeFastPathKind::Contains, core));
1662 }
1663 }
1664 if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1665 {
1666 return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1667 }
1668 if !pattern.ends_with('%')
1669 && trimmed_start.len() < pattern.len()
1670 && !trimmed_start.contains('%')
1671 {
1672 return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1673 }
1674 None
1675}
1676
1677fn sql_like_inner(
1678 pattern: &[char],
1679 text: &[char],
1680 escape: Option<char>,
1681 pi: usize,
1682 ti: usize,
1683 case_sensitive: bool,
1684) -> bool {
1685 let mut pi = pi;
1686 let mut ti = ti;
1687
1688 while pi < pattern.len() {
1689 let pc = pattern[pi];
1690
1691 if Some(pc) == escape {
1693 pi += 1;
1694 if pi >= pattern.len() {
1695 return false; }
1697 if ti >= text.len() || !chars_eq(pattern[pi], text[ti], case_sensitive) {
1699 return false;
1700 }
1701 pi += 1;
1702 ti += 1;
1703 continue;
1704 }
1705
1706 match pc {
1707 '%' => {
1708 while pi < pattern.len() && pattern[pi] == '%' {
1710 pi += 1;
1711 }
1712 if pi >= pattern.len() {
1714 return true;
1715 }
1716 for start in ti..=text.len() {
1718 if sql_like_inner(pattern, text, escape, pi, start, case_sensitive) {
1719 return true;
1720 }
1721 }
1722 return false;
1723 }
1724 '_' => {
1725 if ti >= text.len() {
1726 return false;
1727 }
1728 pi += 1;
1729 ti += 1;
1730 }
1731 _ => {
1732 if ti >= text.len() || !chars_eq(pc, text[ti], case_sensitive) {
1733 return false;
1734 }
1735 pi += 1;
1736 ti += 1;
1737 }
1738 }
1739 }
1740 ti >= text.len()
1741}
1742
1743#[inline]
1746fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool {
1747 if case_sensitive {
1748 a == b
1749 } else {
1750 ascii_ci_eq(a, b)
1751 }
1752}
1753
1754fn ascii_ci_eq(a: char, b: char) -> bool {
1756 if a == b {
1757 return true;
1758 }
1759 a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1761}
1762
1763#[inline]
1764fn ascii_fold_byte(byte: u8) -> u8 {
1765 byte.to_ascii_lowercase()
1766}
1767
1768#[inline]
1769fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1770 left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1771}
1772
1773fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1774 if left.len() != right.len() {
1775 return false;
1776 }
1777 let mut idx = 0;
1778 while idx < left.len() {
1779 if !ascii_ci_eq_byte(left[idx], right[idx]) {
1780 return false;
1781 }
1782 idx += 1;
1783 }
1784 true
1785}
1786
1787fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1788 let text = text.as_bytes();
1789 let prefix = prefix.as_bytes();
1790 text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1791}
1792
1793fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1794 let text = text.as_bytes();
1795 let suffix = suffix.as_bytes();
1796 text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1797}
1798
1799fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1800 let text = text.as_bytes();
1801 let needle = needle.as_bytes();
1802 if needle.is_empty() {
1803 return true;
1804 }
1805 if needle.len() > text.len() {
1806 return false;
1807 }
1808 if memmem::find(text, needle).is_some() {
1809 return true;
1810 }
1811
1812 ascii_ci_contains_folded_scan(text, needle)
1813}
1814
1815fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1816 if needle.is_empty() {
1817 return true;
1818 }
1819 if needle.len() > text.len() {
1820 return false;
1821 }
1822 let max_start = text.len() - needle.len();
1823 let first = needle[0];
1824 let first_folded = ascii_fold_byte(first);
1825 let first_alt = if first.is_ascii_alphabetic() {
1826 first_folded.to_ascii_uppercase()
1827 } else {
1828 first_folded
1829 };
1830 let mut start = 0;
1831 while start <= max_start {
1832 let rel = if first_folded == first_alt {
1833 memchr(first_folded, &text[start..=max_start])
1834 } else {
1835 memchr2(first_folded, first_alt, &text[start..=max_start])
1836 };
1837 let Some(rel) = rel else {
1838 break;
1839 };
1840 start += rel;
1841 if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1842 return true;
1843 }
1844 start += 1;
1845 }
1846 false
1847}
1848
1849#[derive(Debug, Clone)]
1856pub struct SumAccumulator {
1857 int_sum: i64,
1859 float_sum: f64,
1862 float_err: f64,
1864 has_value: bool,
1866 is_float: bool,
1868 overflow: bool,
1870}
1871
1872impl Default for SumAccumulator {
1873 fn default() -> Self {
1874 Self::new()
1875 }
1876}
1877
1878#[inline]
1881fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1882 let s = *sum;
1883 let t = s + value;
1884 if s.abs() > value.abs() {
1885 *err += (s - t) + value;
1886 } else {
1887 *err += (value - t) + s;
1888 }
1889 *sum = t;
1890}
1891
1892impl SumAccumulator {
1893 pub const fn new() -> Self {
1895 Self {
1896 int_sum: 0,
1897 float_sum: 0.0,
1898 float_err: 0.0,
1899 has_value: false,
1900 is_float: false,
1901 overflow: false,
1902 }
1903 }
1904
1905 #[allow(clippy::cast_precision_loss)]
1907 pub fn accumulate(&mut self, val: &SqliteValue) {
1908 match val.to_sum_numeric_value() {
1909 SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1910 SqliteValue::Integer(i) => {
1911 self.has_value = true;
1912 if !self.is_float && !self.overflow {
1913 match self.int_sum.checked_add(i) {
1914 Some(result) => self.int_sum = result,
1915 None => self.overflow = true,
1916 }
1917 }
1918 kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1919 }
1920 SqliteValue::Float(f) => {
1921 self.has_value = true;
1922 self.is_float = true;
1923 kbn_step(&mut self.float_sum, &mut self.float_err, f);
1924 }
1925 }
1926 }
1927
1928 pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1932 if !self.is_float && self.overflow {
1933 return Err(SumOverflowError);
1934 }
1935 if !self.has_value {
1936 return Ok(SqliteValue::Null);
1937 }
1938 if self.is_float {
1939 Ok(SqliteValue::Float(self.float_sum + self.float_err))
1940 } else {
1941 Ok(SqliteValue::Integer(self.int_sum))
1942 }
1943 }
1944}
1945
1946#[derive(Debug, Clone, PartialEq, Eq)]
1948pub struct SumOverflowError;
1949
1950impl fmt::Display for SumOverflowError {
1951 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1952 f.write_str("integer overflow in sum()")
1953 }
1954}
1955
1956impl fmt::Display for SqliteValue {
1957 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1958 match self {
1959 Self::Null => f.write_str("NULL"),
1960 Self::Integer(i) => write!(f, "{i}"),
1961 Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1962 Self::Text(s) => write!(f, "'{s}'"),
1963 Self::Blob(b) => {
1964 f.write_str("X'")?;
1965 for byte in b.iter() {
1966 write!(f, "{byte:02X}")?;
1967 }
1968 f.write_str("'")
1969 }
1970 }
1971 }
1972}
1973
1974impl PartialEq for SqliteValue {
1975 fn eq(&self, other: &Self) -> bool {
1976 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1977 }
1978}
1979
1980impl Eq for SqliteValue {}
1981
1982impl PartialOrd for SqliteValue {
1983 #[inline]
1984 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1985 Some(self.cmp(other))
1986 }
1987}
1988
1989impl Ord for SqliteValue {
1990 #[inline]
1991 fn cmp(&self, other: &Self) -> Ordering {
1992 let class_a = self.sort_class();
1994 let class_b = other.sort_class();
1995
1996 if class_a != class_b {
1997 return class_a.cmp(&class_b);
1998 }
1999
2000 match (self, other) {
2001 (Self::Null, Self::Null) => Ordering::Equal,
2002 (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
2003 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
2004 (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
2005 (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
2006 (Self::Text(a), Self::Text(b)) => a.cmp(b),
2007 (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
2008 _ => unreachable!(),
2009 }
2010 }
2011}
2012
2013impl From<i64> for SqliteValue {
2014 fn from(i: i64) -> Self {
2015 Self::Integer(i)
2016 }
2017}
2018
2019impl From<i32> for SqliteValue {
2020 fn from(i: i32) -> Self {
2021 Self::Integer(i64::from(i))
2022 }
2023}
2024
2025impl From<f64> for SqliteValue {
2026 fn from(f: f64) -> Self {
2027 Self::float_result_or_null(f)
2028 }
2029}
2030
2031impl From<String> for SqliteValue {
2032 fn from(s: String) -> Self {
2033 Self::Text(SmallText::from_string(s))
2036 }
2037}
2038
2039impl From<&str> for SqliteValue {
2040 fn from(s: &str) -> Self {
2041 Self::Text(SmallText::new(s))
2042 }
2043}
2044
2045impl From<Arc<str>> for SqliteValue {
2046 fn from(s: Arc<str>) -> Self {
2047 Self::Text(SmallText::from_arc(s))
2048 }
2049}
2050
2051impl From<Vec<u8>> for SqliteValue {
2052 fn from(b: Vec<u8>) -> Self {
2053 Self::Blob(Arc::from(b))
2056 }
2057}
2058
2059impl From<&[u8]> for SqliteValue {
2060 fn from(b: &[u8]) -> Self {
2061 Self::Blob(Arc::from(b))
2062 }
2063}
2064
2065impl From<Arc<[u8]>> for SqliteValue {
2066 fn from(b: Arc<[u8]>) -> Self {
2067 Self::Blob(b)
2068 }
2069}
2070
2071impl<T: Into<Self>> From<Option<T>> for SqliteValue {
2072 fn from(opt: Option<T>) -> Self {
2073 match opt {
2074 Some(v) => v.into(),
2075 None => Self::Null,
2076 }
2077 }
2078}
2079
2080#[allow(
2084 clippy::cast_possible_truncation,
2085 clippy::cast_precision_loss,
2086 clippy::float_cmp
2087)]
2088fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
2089 let trimmed = trim_sqlite_ascii_whitespace(s);
2090 if trimmed.is_empty() {
2091 return None;
2092 }
2093 if let Ok(i) = trimmed.parse::<i64>() {
2095 return Some(SqliteValue::Integer(i));
2096 }
2097 if let Ok(f) = trimmed.parse::<f64>() {
2101 if !f.is_finite() {
2102 let lower = trimmed.to_ascii_lowercase();
2103 if lower.contains("inf") || lower.contains("nan") {
2104 return None;
2105 }
2106 }
2107 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
2110 #[allow(clippy::cast_possible_truncation)]
2111 let i = f as i64;
2112 #[allow(clippy::cast_precision_loss)]
2113 if (i as f64) == f {
2114 return Some(SqliteValue::Integer(i));
2115 }
2116 }
2117 return Some(SqliteValue::Float(f));
2118 }
2119 None
2120}
2121
2122#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
2127pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
2128 if r.is_nan() {
2129 return Ordering::Greater;
2131 }
2132 if r < -9_223_372_036_854_775_808.0 {
2134 return Ordering::Greater;
2135 }
2136 if r >= 9_223_372_036_854_775_808.0 {
2137 return Ordering::Less;
2138 }
2139 let y = r as i64;
2141 match i.cmp(&y) {
2142 Ordering::Less => Ordering::Less,
2143 Ordering::Greater => Ordering::Greater,
2144 Ordering::Equal => {
2146 let s = i as f64;
2147 s.partial_cmp(&r).unwrap_or(Ordering::Equal)
2148 }
2149 }
2150}
2151
2152#[must_use]
2159pub fn format_sqlite_float(f: f64) -> String {
2160 if f.is_nan() {
2161 return "NaN".to_owned();
2162 }
2163 if f.is_infinite() {
2164 return if f.is_sign_positive() {
2165 "Inf".to_owned()
2166 } else {
2167 "-Inf".to_owned()
2168 };
2169 }
2170 render_sqlite_float_decode(&sqlite_float_decode(f))
2171}
2172
2173const SQLITE_FLOAT_SIGNIFICANT_DIGITS: usize = 17;
2174const SQLITE_FLOAT_MAX_ROUND_DIGITS: usize = 20;
2175const SQLITE_FLOAT_GENERIC_PRECISION: i32 = 16;
2176const SQLITE_POWERS_OF_TEN_FIRST: i32 = -348;
2177const SQLITE_POWERS_OF_TEN_LAST: i32 = 347;
2178
2179#[derive(Debug)]
2180struct SqliteFloatDecode {
2181 digits: Vec<u8>,
2182 decimal_point: i32,
2183 negative: bool,
2184}
2185
2186fn render_sqlite_float_decode(decoded: &SqliteFloatDecode) -> String {
2187 let exponent = decoded.decimal_point - 1;
2188 if !(-4..=SQLITE_FLOAT_GENERIC_PRECISION).contains(&exponent) {
2189 return render_sqlite_float_exponential(decoded, exponent);
2190 }
2191 render_sqlite_float_fixed(decoded, exponent)
2192}
2193
2194fn render_sqlite_float_fixed(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2195 let mut out = String::with_capacity(decoded.digits.len() + 8);
2196 if decoded.negative {
2197 out.push('-');
2198 }
2199
2200 let mut precision = SQLITE_FLOAT_GENERIC_PRECISION - exponent;
2201 let mut digit_idx = 0usize;
2202 let mut e2 = decoded.decimal_point - 1;
2203
2204 if e2 < 0 {
2205 out.push('0');
2206 } else {
2207 while e2 >= 0 {
2208 if let Some(&digit) = decoded.digits.get(digit_idx) {
2209 out.push(char::from(digit));
2210 digit_idx += 1;
2211 } else {
2212 out.push('0');
2213 }
2214 e2 -= 1;
2215 }
2216 }
2217
2218 out.push('.');
2219
2220 if e2 < -1 && precision > 0 {
2221 let zero_count = (-1 - e2).min(precision);
2222 for _ in 0..zero_count {
2223 out.push('0');
2224 }
2225 precision -= zero_count;
2226 }
2227
2228 if precision > 0 {
2229 let digits_after_decimal =
2230 (decoded.digits.len().saturating_sub(digit_idx)).min(precision as usize);
2231 for &digit in &decoded.digits[digit_idx..digit_idx + digits_after_decimal] {
2232 out.push(char::from(digit));
2233 }
2234 }
2235
2236 trim_sqlite_float_tail(&mut out);
2237 out
2238}
2239
2240fn render_sqlite_float_exponential(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2241 let mut out = String::with_capacity(decoded.digits.len() + 8);
2242 if decoded.negative {
2243 out.push('-');
2244 }
2245
2246 let first = decoded.digits.first().copied().unwrap_or(b'0');
2247 out.push(char::from(first));
2248 out.push('.');
2249 let digits_after_decimal =
2250 (decoded.digits.len().saturating_sub(1)).min(SQLITE_FLOAT_GENERIC_PRECISION as usize);
2251 for &digit in decoded.digits.iter().skip(1).take(digits_after_decimal) {
2252 out.push(char::from(digit));
2253 }
2254 trim_sqlite_float_tail(&mut out);
2255
2256 out.push('e');
2257 let mut abs_exp = exponent;
2258 if abs_exp < 0 {
2259 out.push('-');
2260 abs_exp = -abs_exp;
2261 } else {
2262 out.push('+');
2263 }
2264 if abs_exp >= 100 {
2265 out.push(char::from(b'0' + (abs_exp / 100) as u8));
2266 abs_exp %= 100;
2267 }
2268 out.push(char::from(b'0' + (abs_exp / 10) as u8));
2269 out.push(char::from(b'0' + (abs_exp % 10) as u8));
2270 out
2271}
2272
2273fn trim_sqlite_float_tail(out: &mut String) {
2274 while out.ends_with('0') {
2275 out.pop();
2276 }
2277 if out.ends_with('.') {
2278 out.push('0');
2279 }
2280}
2281
2282#[must_use]
2299pub fn sqlite_float_altform2_digits(f: f64) -> (Vec<u8>, i32) {
2300 if !f.is_finite() || f == 0.0 {
2301 return (vec![b'0'], 0);
2302 }
2303 let r = f.abs();
2304 let rendered = format!("{r:.40e}");
2310 let (mantissa_part, exp_part) = rendered
2311 .split_once('e')
2312 .expect("Rust scientific formatting always emits an exponent");
2313 let exponent: i32 = exp_part
2314 .parse()
2315 .expect("Rust scientific exponent is a decimal integer");
2316 let mut digits: Vec<u8> = mantissa_part.bytes().filter(|&b| b != b'.').collect();
2317 let target = if exponent >= 18 { 19 } else { 18 };
2320 digits.truncate(target);
2321 (digits, exponent)
2322}
2323
2324fn sqlite_float_decode(f: f64) -> SqliteFloatDecode {
2325 let negative = f < 0.0;
2326 let r = if negative { -f } else { f };
2327 if r == 0.0 {
2328 return SqliteFloatDecode {
2329 digits: vec![b'0'],
2330 decimal_point: 1,
2331 negative: false,
2332 };
2333 }
2334
2335 let bits = r.to_bits();
2336 let raw_exponent = ((bits >> 52) & 0x7ff) as i32;
2337 let mut mantissa = bits & 0x000f_ffff_ffff_ffff;
2338 let binary_exponent = if raw_exponent == 0 {
2339 let leading = mantissa.leading_zeros();
2340 mantissa <<= leading;
2341 -1074 - leading as i32
2342 } else {
2343 mantissa = (mantissa << 11) | (1_u64 << 63);
2344 raw_exponent - 1086
2345 };
2346
2347 let (decimal, decimal_exponent) = sqlite_fp2_convert10(mantissa, binary_exponent, 18);
2348 let mut digits = decimal.to_string().into_bytes();
2349 let mut digit_count = digits.len();
2350 let mut decimal_point = digit_count as i32 + decimal_exponent;
2351 let mut round_at = SQLITE_FLOAT_SIGNIFICANT_DIGITS;
2352
2353 if round_at < digit_count || digit_count > SQLITE_FLOAT_MAX_ROUND_DIGITS {
2354 if round_at == SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2355 round_at = sqlite_adjust_17_digit_rounding(
2356 r,
2357 &digits,
2358 decimal_exponent,
2359 digit_count,
2360 decimal_point,
2361 round_at,
2362 );
2363 }
2364 if digits.get(round_at).copied().unwrap_or(b'0') >= b'5' {
2365 let mut idx = round_at - 1;
2366 loop {
2367 digits[idx] += 1;
2368 if digits[idx] <= b'9' {
2369 break;
2370 }
2371 digits[idx] = b'0';
2372 if idx == 0 {
2373 digits.insert(0, b'1');
2374 round_at += 1;
2375 decimal_point += 1;
2376 break;
2377 }
2378 idx -= 1;
2379 }
2380 }
2381 digit_count = round_at;
2382 digits.truncate(digit_count);
2383 }
2384
2385 while digit_count > 1 && digits[digit_count - 1] == b'0' {
2386 digit_count -= 1;
2387 }
2388 digits.truncate(digit_count);
2389
2390 SqliteFloatDecode {
2391 digits,
2392 decimal_point,
2393 negative,
2394 }
2395}
2396
2397fn sqlite_adjust_17_digit_rounding(
2398 r: f64,
2399 digits: &[u8],
2400 decimal_exponent: i32,
2401 digit_count: usize,
2402 decimal_point: i32,
2403 round_at: usize,
2404) -> usize {
2405 if digits.len() <= SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2406 return round_at;
2407 }
2408
2409 if digits[15] == b'9' && digits[14] == b'9' {
2410 let mut keep = 14usize;
2411 while keep > 0 && digits[keep - 1] == b'9' {
2412 keep -= 1;
2413 }
2414 let candidate = if keep == 0 {
2415 1
2416 } else {
2417 decimal_digits_to_u64(&digits[..keep]) + 1
2418 };
2419 if r == sqlite_fp10_convert2(
2420 candidate,
2421 decimal_exponent + digit_count as i32 - keep as i32,
2422 ) {
2423 return keep + 1;
2424 }
2425 } else if decimal_point >= digit_count as i32
2426 || (digits[15] == b'0' && digits[14] == b'0' && digits[13] == b'0')
2427 {
2428 let mut keep = 13usize;
2429 while keep > 0 && digits[keep - 1] == b'0' {
2430 keep -= 1;
2431 }
2432 if keep > 0 {
2433 let candidate = decimal_digits_to_u64(&digits[..keep]);
2434 if r == sqlite_fp10_convert2(
2435 candidate,
2436 decimal_exponent + digit_count as i32 - keep as i32,
2437 ) {
2438 return keep + 1;
2439 }
2440 }
2441 }
2442
2443 round_at
2444}
2445
2446fn decimal_digits_to_u64(digits: &[u8]) -> u64 {
2447 digits
2448 .iter()
2449 .fold(0_u64, |acc, digit| acc * 10 + u64::from(*digit - b'0'))
2450}
2451
2452fn sqlite_fp2_convert10(mantissa: u64, binary_exponent: i32, digits: i32) -> (u64, i32) {
2453 let power = digits - 1 - pwr2_to_10(binary_exponent + 63);
2454 let (power_hi, power_lo) = power_of_ten(power);
2455 let (mut high, _) = sqlite_multiply_128(mantissa, power_hi);
2456 let _ = power_lo;
2457 if digits == 18 {
2458 high >>= -(binary_exponent + pwr10_to_2(power) + 2) as u32;
2459 (high.wrapping_add((high << 1) & 2) >> 1, -power)
2460 } else {
2461 high >>= -(binary_exponent + pwr10_to_2(power) + 1) as u32;
2462 (high, -power)
2463 }
2464}
2465
2466fn sqlite_fp10_convert2(decimal: u64, power: i32) -> f64 {
2467 if power < SQLITE_POWERS_OF_TEN_FIRST {
2468 return 0.0;
2469 }
2470 if power > SQLITE_POWERS_OF_TEN_LAST {
2471 return f64::INFINITY;
2472 }
2473
2474 let bit_width = 64 - decimal.leading_zeros() as i32;
2475 let binary_power = pwr10_to_2(power);
2476 let mut exponent = 53 - bit_width - binary_power;
2477 if exponent > 1074 {
2478 if exponent >= 1130 {
2479 return 0.0;
2480 }
2481 exponent = 1074;
2482 }
2483
2484 let shift = -(exponent - (64 - bit_width) + binary_power + 3);
2485 let shift = shift.clamp(0, 63) as u32;
2486 let (mut power_hi, mut power_lo) = power_of_ten(power);
2487 if power_lo != 0 {
2488 power_hi = power_hi.wrapping_add(1);
2489 power_lo = !power_lo;
2490 }
2491
2492 let shifted_decimal = decimal << (64 - bit_width);
2493 let (mut high, low) = sqlite_multiply_128(shifted_decimal, power_hi);
2494 let mid1 = (low >> 32) as u32;
2495 let mut sticky = 1_u64;
2496 if (high & low_mask(shift)) == 0 {
2497 let (mid2_high, _) = sqlite_multiply_128(shifted_decimal, u64::from(power_lo) << 32);
2498 let mid2 = (mid2_high >> 32) as u32;
2499 sticky = u64::from(mid1.wrapping_sub(mid2) > 1);
2500 high = high.wrapping_sub(u64::from(mid1 < mid2));
2501 }
2502
2503 let mut rounded = (high >> shift) | sticky;
2504 let adjust = u32::from(rounded >= (1_u64 << 55) - 2);
2505 if adjust != 0 {
2506 rounded = (rounded >> adjust) | (rounded & 1);
2507 exponent -= adjust as i32;
2508 }
2509
2510 let mut bits = (rounded + 1 + ((rounded >> 2) & 1)) >> 2;
2511 if exponent <= -972 {
2512 return f64::INFINITY;
2513 }
2514 if (bits & (1_u64 << 52)) != 0 {
2515 bits = (bits & !(1_u64 << 52)) | ((1075 - exponent) as u64) << 52;
2516 }
2517 f64::from_bits(bits)
2518}
2519
2520fn low_mask(bits: u32) -> u64 {
2521 if bits == 0 { 0 } else { (1_u64 << bits) - 1 }
2522}
2523
2524fn sqlite_multiply_128(left: u64, right: u64) -> (u64, u64) {
2525 let product = u128::from(left) * u128::from(right);
2526 ((product >> 64) as u64, product as u64)
2527}
2528
2529fn sqlite_multiply_160(high: u64, low: u32, right: u64) -> (u64, u32) {
2530 let product =
2531 u128::from(high) * u128::from(right) + ((u128::from(low) * u128::from(right)) >> 32);
2532 (
2533 (product >> 64) as u64,
2534 ((product >> 32) & u128::from(u32::MAX)) as u32,
2535 )
2536}
2537
2538fn pwr10_to_2(power: i32) -> i32 {
2539 (power * 108_853) >> 15
2540}
2541
2542fn pwr2_to_10(power: i32) -> i32 {
2543 (power * 78_913) >> 18
2544}
2545
2546fn power_of_ten(power: i32) -> (u64, u32) {
2547 const BASE: [u64; 27] = [
2548 0x8000_0000_0000_0000,
2549 0xa000_0000_0000_0000,
2550 0xc800_0000_0000_0000,
2551 0xfa00_0000_0000_0000,
2552 0x9c40_0000_0000_0000,
2553 0xc350_0000_0000_0000,
2554 0xf424_0000_0000_0000,
2555 0x9896_8000_0000_0000,
2556 0xbebc_2000_0000_0000,
2557 0xee6b_2800_0000_0000,
2558 0x9502_f900_0000_0000,
2559 0xba43_b740_0000_0000,
2560 0xe8d4_a510_0000_0000,
2561 0x9184_e72a_0000_0000,
2562 0xb5e6_20f4_8000_0000,
2563 0xe35f_a931_a000_0000,
2564 0x8e1b_c9bf_0400_0000,
2565 0xb1a2_bc2e_c500_0000,
2566 0xde0b_6b3a_7640_0000,
2567 0x8ac7_2304_89e8_0000,
2568 0xad78_ebc5_ac62_0000,
2569 0xd8d7_26b7_177a_8000,
2570 0x8786_7832_6eac_9000,
2571 0xa968_163f_0a57_b400,
2572 0xd3c2_1bce_cced_a100,
2573 0x8459_5161_4014_84a0,
2574 0xa56f_a5b9_9019_a5c8,
2575 ];
2576 const SCALE: [u64; 26] = [
2577 0x8049_a4ac_0c58_11ae,
2578 0xcf42_894a_5dce_35ea,
2579 0xa76c_5823_38ed_2621,
2580 0x873e_4f75_e222_4e68,
2581 0xda7f_5bf5_9096_6848,
2582 0xb080_392c_c434_9dec,
2583 0x8e93_8662_882a_f53e,
2584 0xe658_29b3_046b_0afa,
2585 0xba12_1a46_50e4_ddeb,
2586 0x964e_858c_91ba_2655,
2587 0xf2d5_6790_ab41_c2a2,
2588 0xc428_d05a_a475_1e4c,
2589 0x9e74_d1b7_91e0_7e48,
2590 0xcccc_cccc_cccc_cccc,
2591 0xcecb_8f27_f420_0f3a,
2592 0xa70c_3c40_a64e_6c51,
2593 0x86f0_ac99_b4e8_dafd,
2594 0xda01_ee64_1a70_8de9,
2595 0xb01a_e745_b101_e9e4,
2596 0x8e41_ade9_fbeb_c27d,
2597 0xe5d3_ef28_2a24_2e81,
2598 0xb9a7_4a06_37ce_2ee1,
2599 0x95f8_3d0a_1fb6_9cd9,
2600 0xf24a_01a7_3cf2_dccf,
2601 0xc3b8_3581_09e8_4f07,
2602 0x9e19_db92_b4e3_1ba9,
2603 ];
2604 const SCALE_LO: [u32; 26] = [
2605 0x205b_896d,
2606 0x5206_4cad,
2607 0xaf2a_f2b8,
2608 0x5a77_44a7,
2609 0xaf39_a475,
2610 0xbd8d_794e,
2611 0x547e_b47b,
2612 0x0cb4_a5a3,
2613 0x92f3_4d62,
2614 0x3a6a_07f9,
2615 0xfae2_7299,
2616 0xaa97_e14c,
2617 0x775e_a265,
2618 0xcccc_cccc,
2619 0x0000_0000,
2620 0x9990_90b6,
2621 0x69a0_28bb,
2622 0xe80e_6f48,
2623 0x5ec0_5dd0,
2624 0x1458_8f14,
2625 0x8f16_68c9,
2626 0x6d95_3e2c,
2627 0x4abd_af10,
2628 0xbc63_3b39,
2629 0x0a86_2f81,
2630 0x6c07_a2c2,
2631 ];
2632
2633 debug_assert!((SQLITE_POWERS_OF_TEN_FIRST..=SQLITE_POWERS_OF_TEN_LAST).contains(&power));
2634
2635 let (group, offset) = if power < 0 {
2636 if power == -1 {
2637 return (SCALE[13], SCALE_LO[13]);
2638 }
2639 let mut group = power / 27;
2640 let mut offset = power % 27;
2641 if offset != 0 {
2642 group -= 1;
2643 offset += 27;
2644 }
2645 (group, offset)
2646 } else if power < 27 {
2647 return (BASE[power as usize], 0);
2648 } else {
2649 (power / 27, power % 27)
2650 };
2651
2652 let scale_idx = (group + 13) as usize;
2653 let mut high = SCALE[scale_idx];
2654 if offset == 0 {
2655 return (high, SCALE_LO[scale_idx]);
2656 }
2657
2658 let (scaled, mut low) = sqlite_multiply_160(high, SCALE_LO[scale_idx], BASE[offset as usize]);
2659 high = scaled;
2660 if (high & (1_u64 << 63)) == 0 {
2661 high = (high << 1) | u64::from(low >> 31);
2662 low = (low << 1) | 1;
2663 }
2664 (high, low)
2665}
2666
2667#[cfg(test)]
2668#[allow(clippy::float_cmp, clippy::approx_constant)]
2669mod tests {
2670 use super::*;
2671
2672 struct ValuePoolTestGuard;
2673
2674 impl ValuePoolTestGuard {
2675 fn new() -> Self {
2676 pool_clear();
2677 reset_value_pool_test_stats();
2678 Self
2679 }
2680 }
2681
2682 impl Drop for ValuePoolTestGuard {
2683 fn drop(&mut self) {
2684 pool_clear();
2685 reset_value_pool_test_stats();
2686 }
2687 }
2688
2689 fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
2690 let stats = value_pool_test_stats_snapshot();
2691 eprintln!(
2692 "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
2693 stats.slab_alloc_count,
2694 stats.slab_return_count,
2695 stats.global_alloc_fallback_count,
2696 stats.slab_high_water_mark,
2697 pool_len(),
2698 );
2699 stats
2700 }
2701
2702 fn utf16_record_bytes(text: &str, little_endian: bool) -> Vec<u8> {
2703 text.encode_utf16()
2704 .flat_map(|unit| {
2705 if little_endian {
2706 unit.to_le_bytes()
2707 } else {
2708 unit.to_be_bytes()
2709 }
2710 })
2711 .collect()
2712 }
2713
2714 #[test]
2715 fn from_record_text_bytes_utf8_passthrough() {
2716 assert_eq!(
2717 SmallText::from_record_text_bytes(b"table", TextEncoding::Utf8).as_str(),
2718 "table"
2719 );
2720 assert_eq!(
2721 SmallText::from_record_text_bytes(b"", TextEncoding::Utf8).as_str(),
2722 ""
2723 );
2724 }
2725
2726 #[test]
2727 fn from_record_text_bytes_utf16_le_and_be_round_trip() {
2728 for text in ["table", "café", "日本語", "😀 grin", ""] {
2729 let le = SmallText::from_record_text_bytes(
2730 &utf16_record_bytes(text, true),
2731 TextEncoding::Utf16le,
2732 );
2733 assert_eq!(le.as_str(), text, "utf16le decode of {text:?}");
2734 let be = SmallText::from_record_text_bytes(
2735 &utf16_record_bytes(text, false),
2736 TextEncoding::Utf16be,
2737 );
2738 assert_eq!(be.as_str(), text, "utf16be decode of {text:?}");
2739 }
2740 }
2741
2742 #[test]
2743 fn from_record_text_bytes_utf16_sqlite_master_ascii_case() {
2744 let bytes = utf16_record_bytes("table", true);
2748 assert_eq!(bytes, vec![b't', 0, b'a', 0, b'b', 0, b'l', 0, b'e', 0]);
2749 assert_eq!(
2750 SmallText::from_record_text_bytes(&bytes, TextEncoding::Utf16le).as_str(),
2751 "table"
2752 );
2753 assert_ne!(SmallText::from_bytes(&bytes).as_str(), "table");
2755 }
2756
2757 #[test]
2758 fn from_record_text_bytes_utf16_lone_surrogate_becomes_replacement() {
2759 let bytes = 0xD800_u16.to_le_bytes().to_vec();
2762 assert_eq!(
2763 SmallText::from_record_text_bytes(&bytes, TextEncoding::Utf16le).as_str(),
2764 "\u{FFFD}"
2765 );
2766 }
2767
2768 #[test]
2769 fn utf16_decoded_text_compares_by_code_point_regardless_of_storage_encoding() {
2770 let samples = [
2779 "", "A", "Apple", "apple", "banana", "café", "cafz", "z", "Καλημέρα", "日本", "日本語",
2780 "😀", "😀grin",
2781 ];
2782 for a in samples {
2783 for b in samples {
2784 let expected = SmallText::new(a).cmp(&SmallText::new(b));
2785 for (enc, le) in [
2786 (TextEncoding::Utf16le, true),
2787 (TextEncoding::Utf16be, false),
2788 ] {
2789 let da = SmallText::from_record_text_bytes(&utf16_record_bytes(a, le), enc);
2790 let db = SmallText::from_record_text_bytes(&utf16_record_bytes(b, le), enc);
2791 assert_eq!(
2792 da.cmp(&db),
2793 expected,
2794 "bd-bld9w.4 {enc:?}: cmp({a:?}, {b:?}) must equal code-point order"
2795 );
2796 assert_eq!(
2799 da.cmp(&SmallText::new(a)),
2800 std::cmp::Ordering::Equal,
2801 "bd-bld9w.4 {enc:?}: decoded {a:?} must equal its UTF-8 form"
2802 );
2803 }
2804 }
2805 }
2806 }
2807
2808 #[test]
2809 fn to_record_text_bytes_round_trips_from_record_text_bytes() {
2810 for text in ["", "table", "café", "日本語 mix", "😀 grin 🎉"] {
2811 for encoding in [
2812 TextEncoding::Utf8,
2813 TextEncoding::Utf16le,
2814 TextEncoding::Utf16be,
2815 ] {
2816 let value = SmallText::new(text);
2817 let encoded = value.to_record_text_bytes(encoding);
2818 let decoded = SmallText::from_record_text_bytes(&encoded, encoding);
2819 assert_eq!(decoded.as_str(), text, "round-trip {text:?} via {encoding:?}");
2820 }
2821 }
2822 let value = SmallText::new("borrow me");
2824 assert!(matches!(
2825 value.to_record_text_bytes(TextEncoding::Utf8),
2826 Cow::Borrowed(_)
2827 ));
2828 let table = SmallText::new("table");
2830 let le = table.to_record_text_bytes(TextEncoding::Utf16le);
2831 assert_eq!(&*le, &[b't', 0, b'a', 0, b'b', 0, b'l', 0, b'e', 0][..]);
2832 }
2833
2834 #[test]
2835 fn test_slab_basic_alloc_dealloc() {
2836 let _guard = ValuePoolTestGuard::new();
2837 const ROUND_TRIP_COUNT: usize = 100;
2838
2839 assert_eq!(pool_len(), 0);
2840 assert_eq!(pool_acquire(), None);
2841 assert_eq!(
2842 value_pool_test_stats_snapshot(),
2843 ValuePoolStats {
2844 slab_alloc_count: 0,
2845 slab_return_count: 0,
2846 global_alloc_fallback_count: 1,
2847 slab_high_water_mark: 0,
2848 }
2849 );
2850
2851 reset_value_pool_test_stats();
2852 for value in 0..ROUND_TRIP_COUNT {
2853 pool_return(SqliteValue::Integer(value as i64));
2854 }
2855 assert_eq!(pool_len(), ROUND_TRIP_COUNT);
2856 assert_eq!(
2857 value_pool_test_stats_snapshot(),
2858 ValuePoolStats {
2859 slab_alloc_count: 0,
2860 slab_return_count: ROUND_TRIP_COUNT,
2861 global_alloc_fallback_count: 0,
2862 slab_high_water_mark: ROUND_TRIP_COUNT,
2863 }
2864 );
2865
2866 reset_value_pool_test_stats();
2867 for expected in (0..ROUND_TRIP_COUNT).rev() {
2868 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
2869 }
2870 assert_eq!(pool_len(), 0);
2871 assert_eq!(
2872 log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
2873 ValuePoolStats {
2874 slab_alloc_count: ROUND_TRIP_COUNT,
2875 slab_return_count: 0,
2876 global_alloc_fallback_count: 0,
2877 slab_high_water_mark: 0,
2878 }
2879 );
2880 }
2881
2882 #[test]
2883 fn test_slab_exhaustion_fallback() {
2884 let _guard = ValuePoolTestGuard::new();
2885
2886 for value in 0..=VALUE_POOL_CAP {
2887 pool_return(SqliteValue::Integer(value as i64));
2888 }
2889 assert_eq!(pool_len(), VALUE_POOL_CAP);
2890 assert_eq!(
2891 value_pool_test_stats_snapshot(),
2892 ValuePoolStats {
2893 slab_alloc_count: 0,
2894 slab_return_count: VALUE_POOL_CAP,
2895 global_alloc_fallback_count: 0,
2896 slab_high_water_mark: VALUE_POOL_CAP,
2897 }
2898 );
2899
2900 reset_value_pool_test_stats();
2901 for _ in 0..VALUE_POOL_CAP {
2902 assert!(pool_acquire().is_some());
2903 }
2904 assert_eq!(pool_acquire(), None);
2905 assert_eq!(pool_len(), 0);
2906 assert_eq!(
2907 log_value_pool_test_stats("test_slab_exhaustion_fallback"),
2908 ValuePoolStats {
2909 slab_alloc_count: VALUE_POOL_CAP,
2910 slab_return_count: 0,
2911 global_alloc_fallback_count: 1,
2912 slab_high_water_mark: 0,
2913 }
2914 );
2915 }
2916
2917 #[test]
2918 fn test_slab_no_leak() {
2919 let _guard = ValuePoolTestGuard::new();
2920 const ITERATIONS: usize = 10_000;
2921
2922 let (weak_tx, weak_rx) = std::sync::mpsc::channel();
2923 let (release_tx, release_rx) = std::sync::mpsc::channel();
2924
2925 let worker = std::thread::spawn(move || {
2926 pool_clear();
2927 reset_value_pool_test_stats();
2928
2929 let mut pooled_weak = None;
2930 let mut overflow_weak = None;
2931 for value in 0..ITERATIONS {
2932 let payload: Arc<[u8]> =
2933 Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
2934 if value == 0 {
2935 pooled_weak = Some(Arc::downgrade(&payload));
2936 } else if value == ITERATIONS - 1 {
2937 overflow_weak = Some(Arc::downgrade(&payload));
2938 }
2939 pool_return(SqliteValue::Blob(payload));
2940 }
2941
2942 assert_eq!(
2943 pool_len(),
2944 VALUE_POOL_CAP,
2945 "the slab must retain at most VALUE_POOL_CAP entries",
2946 );
2947 weak_tx
2948 .send((
2949 pooled_weak.expect("capture pooled weak handle"),
2950 overflow_weak.expect("capture overflow weak handle"),
2951 log_value_pool_test_stats("test_slab_no_leak"),
2952 ))
2953 .expect("send slab leak stats");
2954 release_rx.recv().expect("wait for release");
2955 });
2956
2957 let (pooled_weak, overflow_weak, stats) =
2958 weak_rx.recv().expect("receive weak blob handles");
2959 assert!(
2960 pooled_weak.upgrade().is_some(),
2961 "pooled blob should remain alive while the owning thread is running"
2962 );
2963 assert!(
2964 overflow_weak.upgrade().is_none(),
2965 "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2966 );
2967 assert_eq!(
2968 stats,
2969 ValuePoolStats {
2970 slab_alloc_count: 0,
2971 slab_return_count: VALUE_POOL_CAP,
2972 global_alloc_fallback_count: 0,
2973 slab_high_water_mark: VALUE_POOL_CAP,
2974 }
2975 );
2976
2977 release_tx.send(()).expect("release worker thread");
2978 worker.join().expect("join worker");
2979
2980 assert!(
2981 pooled_weak.upgrade().is_none(),
2982 "thread-local slab contents must be dropped when the thread exits"
2983 );
2984 }
2985
2986 #[test]
2987 fn test_slab_thread_local_isolation() {
2988 let _guard = ValuePoolTestGuard::new();
2989
2990 pool_return(SqliteValue::Integer(11));
2991 assert_eq!(pool_len(), 1);
2992
2993 let worker = std::thread::spawn(|| {
2994 pool_clear();
2995 reset_value_pool_test_stats();
2996
2997 assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2998 pool_return(SqliteValue::Integer(22));
2999 assert_eq!(pool_len(), 1);
3000 assert_eq!(
3001 value_pool_test_stats_snapshot(),
3002 ValuePoolStats {
3003 slab_alloc_count: 0,
3004 slab_return_count: 1,
3005 global_alloc_fallback_count: 0,
3006 slab_high_water_mark: 1,
3007 }
3008 );
3009 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
3010 assert_eq!(pool_len(), 0);
3011 });
3012 worker.join().expect("join worker");
3013
3014 assert_eq!(
3015 pool_len(),
3016 1,
3017 "worker thread slab operations must not affect the caller thread"
3018 );
3019 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
3020 assert_eq!(pool_len(), 0);
3021 let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
3022 assert_eq!(
3023 stats,
3024 ValuePoolStats {
3025 slab_alloc_count: 1,
3026 slab_return_count: 1,
3027 global_alloc_fallback_count: 0,
3028 slab_high_water_mark: 1,
3029 }
3030 );
3031 }
3032
3033 #[test]
3034 fn test_slab_zero_malloc_steady_state() {
3035 let _guard = ValuePoolTestGuard::new();
3036 const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
3037 const ITERATIONS: usize = 1_000;
3038 const INITIAL_TEXT: &str =
3039 "steady-state pooled string backing store for bd-nsvud warmup payload";
3040 const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
3041
3042 assert!(
3043 REUSED_TEXT.len() <= INITIAL_TEXT.len(),
3044 "steady-state overwrite must fit within the warmed heap allocation"
3045 );
3046
3047 for _ in 0..WARM_POOL_DEPTH {
3048 pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
3049 }
3050 assert_eq!(pool_len(), WARM_POOL_DEPTH);
3051
3052 reset_value_pool_test_stats();
3053 for _ in 0..ITERATIONS {
3054 let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
3055 let SqliteValue::Text(existing) = &mut reused else {
3056 panic!("warmed slab entry should remain a text value");
3057 };
3058 let original_ptr = existing.as_str().as_ptr();
3059 existing.overwrite(REUSED_TEXT);
3060 assert_eq!(
3061 existing.as_str().as_ptr(),
3062 original_ptr,
3063 "steady-state overwrite should reuse the warmed heap buffer",
3064 );
3065 assert_eq!(existing.as_str(), REUSED_TEXT);
3066 pool_return(reused);
3067 }
3068
3069 assert_eq!(pool_len(), WARM_POOL_DEPTH);
3070 assert_eq!(
3071 log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
3072 ValuePoolStats {
3073 slab_alloc_count: ITERATIONS,
3074 slab_return_count: ITERATIONS,
3075 global_alloc_fallback_count: 0,
3076 slab_high_water_mark: WARM_POOL_DEPTH,
3077 }
3078 );
3079 }
3080
3081 #[test]
3082 fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
3083 let text = SmallText::new("this string is definitely longer than twenty three bytes");
3084 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3085 panic!("long text should start in heap-owned mode");
3086 };
3087 assert!(
3088 shared.get().is_none(),
3089 "long text should not allocate Arc eagerly before cloning"
3090 );
3091
3092 let cloned = text.clone();
3093
3094 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3095 panic!("original text should remain heap-owned after clone");
3096 };
3097 assert!(
3098 shared.get().is_some(),
3099 "first clone should materialize a shared Arc lazily"
3100 );
3101 assert!(
3102 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
3103 "cloned text should use the shared Arc representation"
3104 );
3105 assert_eq!(text.as_str(), cloned.as_str());
3106 }
3107
3108 #[test]
3109 fn test_small_text_invalid_utf8_clone_compare_and_serde_fail_closed() {
3110 let bytes: &[u8] = &[0x80, 0xC0, 0xAF];
3111 let text = SmallText::from_bytes(bytes);
3112 assert!(!text.is_valid_utf8());
3113 assert_eq!(text.as_str_checked(), None);
3114 assert_eq!(text.as_bytes_direct(), bytes);
3115 assert_eq!(text.len(), bytes.len());
3116 assert!(!text.is_inline());
3117
3118 let cloned = text.clone();
3119 assert_eq!(cloned, text);
3120 assert_eq!(cloned.as_bytes_direct(), bytes);
3121 assert_ne!(
3122 text,
3123 SmallText::new("\u{FFFD}\u{FFFD}\u{FFFD}"),
3124 "lossy display text must not define SQLite TEXT equality"
3125 );
3126
3127 let error = serde_json::to_string(&text)
3128 .expect_err("raw TEXT must not be silently substituted during string serialization");
3129 assert!(
3130 error.to_string().contains("invalid UTF-8"),
3131 "serialization failure must explain the unsupported Rust-string boundary: {error}"
3132 );
3133 }
3134
3135 #[test]
3136 fn test_small_text_overwrite_reuses_unique_heap_buffer() {
3137 let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
3138 let (original_ptr, original_capacity) = match &text.repr {
3139 SmallTextRepr::HeapOwned { text, shared } => {
3140 assert!(shared.get().is_none(), "fresh heap text should be unshared");
3141 (text.as_ptr(), text.capacity())
3142 }
3143 _ => panic!("long text should start in heap-owned mode"),
3144 };
3145
3146 text.overwrite("another long string that still fits the same allocation");
3147
3148 match &text.repr {
3149 SmallTextRepr::HeapOwned { text, shared } => {
3150 assert!(
3151 shared.get().is_none(),
3152 "overwrite should keep text single-owner"
3153 );
3154 assert_eq!(text.as_ptr(), original_ptr);
3155 assert_eq!(text.capacity(), original_capacity);
3156 assert_eq!(
3157 text.as_str(),
3158 "another long string that still fits the same allocation"
3159 );
3160 }
3161 _ => panic!("overwrite should keep long text in heap-owned mode"),
3162 }
3163 }
3164
3165 #[test]
3166 fn test_small_text_overwrite_detaches_from_shared_arc() {
3167 let original = "this string is definitely longer than twenty three bytes";
3168 let mut text = SmallText::new(original);
3169 let (original_ptr, original_capacity) = match &text.repr {
3170 SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
3171 _ => panic!("long text should start in heap-owned mode"),
3172 };
3173 let replacement = "replacement text that must not mutate the shared clone";
3174 assert!(
3175 replacement.len() <= original_capacity,
3176 "replacement should fit the original heap allocation for this regression",
3177 );
3178 let clone = text.clone();
3179
3180 text.overwrite(replacement);
3181
3182 assert_eq!(
3183 clone.as_str(),
3184 original,
3185 "existing shared clones must keep the original contents"
3186 );
3187 assert_eq!(text.as_str(), replacement);
3188 match &text.repr {
3189 SmallTextRepr::HeapOwned { text, shared } => {
3190 assert_eq!(
3191 text.as_ptr(),
3192 original_ptr,
3193 "overwriting a cloned long string should keep the owned buffer",
3194 );
3195 assert_eq!(
3196 text.capacity(),
3197 original_capacity,
3198 "detaching from the shared cache should preserve capacity",
3199 );
3200 assert!(
3201 shared.get().is_none(),
3202 "overwrite should reset the lazy shared cache after detaching"
3203 );
3204 }
3205 _ => panic!("overwrite should restore heap-owned mode"),
3206 }
3207 }
3208
3209 #[test]
3210 fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
3211 let _guard = ValuePoolTestGuard::new();
3212
3213 pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
3214 assert_eq!(
3215 pool_len(),
3216 0,
3217 "inline text should not occupy reusable slab slots",
3218 );
3219
3220 let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
3221 let _clone = owned_text.clone();
3222 pool_return_reusable(SqliteValue::Text(owned_text));
3223 assert_eq!(
3224 pool_len(),
3225 1,
3226 "heap-owned text should stay reusable even after serving shared clones",
3227 );
3228 assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
3229 assert_eq!(pool_len(), 0);
3230
3231 let shared_text =
3232 Arc::<str>::from("this string is definitely longer than twenty three bytes");
3233 pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
3234 &shared_text,
3235 ))));
3236 assert_eq!(
3237 pool_len(),
3238 0,
3239 "arc-backed shared text should not enter the reusable slab",
3240 );
3241
3242 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
3243 pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
3244 assert_eq!(
3245 pool_len(),
3246 0,
3247 "shared blob allocations should not displace reusable slab entries",
3248 );
3249
3250 let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
3251 pool_return_reusable(SqliteValue::Blob(unique_blob));
3252 assert_eq!(
3253 pool_len(),
3254 1,
3255 "unique blob allocations should remain eligible for slab reuse",
3256 );
3257 }
3258
3259 #[test]
3260 fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
3261 let text = Arc::new(SmallText::new(
3262 "this string is definitely longer than twenty three bytes",
3263 ));
3264 let expected = text.as_str().to_owned();
3265 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3266 panic!("long text should start in heap-owned mode");
3267 };
3268 assert!(
3269 shared.get().is_none(),
3270 "shared Arc should still be lazy before concurrent clones"
3271 );
3272
3273 let barrier = Arc::new(std::sync::Barrier::new(5));
3274 let mut workers = Vec::new();
3275 for _ in 0..4 {
3276 let text = Arc::clone(&text);
3277 let barrier = Arc::clone(&barrier);
3278 let expected = expected.clone();
3279 workers.push(std::thread::spawn(move || {
3280 barrier.wait();
3281 for _ in 0..64 {
3282 let cloned = (*text).clone();
3283 assert_eq!(cloned.as_str(), expected);
3284 assert!(
3285 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
3286 "concurrent clone should reuse the shared Arc representation"
3287 );
3288 }
3289 }));
3290 }
3291
3292 barrier.wait();
3293 for worker in workers {
3294 worker
3295 .join()
3296 .expect("join concurrent small-text clone worker");
3297 }
3298
3299 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3300 panic!("original text should remain heap-owned after clone promotion");
3301 };
3302 let shared = shared
3303 .get()
3304 .expect("concurrent clones should promote the lazy shared Arc");
3305 assert_eq!(shared.as_ref(), expected);
3306 assert_eq!(text.as_str(), expected);
3307 }
3308
3309 #[test]
3310 fn null_properties() {
3311 let v = SqliteValue::Null;
3312 assert!(v.is_null());
3313 assert_eq!(v.to_integer(), 0);
3314 assert_eq!(v.to_float(), 0.0);
3315 assert_eq!(v.to_text(), "");
3316 assert_eq!(v.to_string(), "NULL");
3317 }
3318
3319 #[test]
3320 fn integer_properties() {
3321 let v = SqliteValue::Integer(42);
3322 assert!(!v.is_null());
3323 assert_eq!(v.as_integer(), Some(42));
3324 assert_eq!(v.to_integer(), 42);
3325 assert_eq!(v.to_float(), 42.0);
3326 assert_eq!(v.to_text(), "42");
3327 }
3328
3329 #[test]
3330 fn float_properties() {
3331 let v = SqliteValue::Float(3.14);
3332 assert_eq!(v.as_float(), Some(3.14));
3333 assert_eq!(v.to_integer(), 3);
3334 assert_eq!(v.to_text(), "3.14");
3335 }
3336
3337 #[test]
3338 fn text_properties() {
3339 let v = SqliteValue::Text(SmallText::new("hello"));
3340 assert_eq!(v.as_text(), Some("hello"));
3341 assert_eq!(v.to_integer(), 0);
3342 assert_eq!(v.to_float(), 0.0);
3343 }
3344
3345 #[test]
3346 fn text_numeric_coercion() {
3347 let v = SqliteValue::Text(SmallText::new("123"));
3348 assert_eq!(v.to_integer(), 123);
3349 assert_eq!(v.to_float(), 123.0);
3350
3351 let v = SqliteValue::Text(SmallText::new("3.14"));
3352 assert_eq!(v.to_integer(), 3);
3353 assert_eq!(v.to_float(), 3.14);
3354 }
3355
3356 #[test]
3357 fn text_numeric_coercion_ignores_hex_text_prefixes() {
3358 let v = SqliteValue::Text(SmallText::new("0x10"));
3359 assert_eq!(v.to_integer(), 0);
3360 assert_eq!(v.to_float(), 0.0);
3361
3362 let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
3363 assert_eq!(v.to_integer(), 0);
3364 assert_eq!(v.to_float(), 0.0);
3365 }
3366
3367 #[test]
3368 fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
3369 assert_eq!(
3370 SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
3371 SqliteValue::Integer(123)
3372 );
3373 assert_eq!(
3374 SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
3375 SqliteValue::Float(0.0)
3376 );
3377 assert_eq!(
3378 SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
3379 SqliteValue::Float(123.0)
3380 );
3381 assert_eq!(
3382 SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
3383 SqliteValue::Float(1.0)
3384 );
3385 assert_eq!(
3386 SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
3387 SqliteValue::Float(123.0)
3388 );
3389 assert_eq!(
3390 SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
3391 SqliteValue::Float(0.0)
3392 );
3393 assert_eq!(
3394 SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
3395 SqliteValue::Float(123.0)
3396 );
3397 }
3398
3399 #[test]
3400 fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
3401 assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
3402 assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
3403 assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
3404 assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
3405 }
3406
3407 #[test]
3408 fn test_sqlite_value_integer_real_comparison_equal() {
3409 let int_value = SqliteValue::Integer(3);
3410 let real_value = SqliteValue::Float(3.0);
3411 assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
3412 assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
3413 }
3414
3415 #[test]
3416 fn test_sqlite_value_text_to_integer_coercion() {
3417 let text_value = SqliteValue::Text(SmallText::new("123"));
3418 let coerced = text_value.apply_affinity(TypeAffinity::Integer);
3419 assert_eq!(coerced, SqliteValue::Integer(123));
3420 }
3421
3422 #[test]
3423 fn blob_properties() {
3424 let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
3425 assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
3426 assert_eq!(v.to_integer(), 0);
3427 assert_eq!(v.to_float(), 0.0);
3428 assert_eq!(v.to_text(), "\u{07AD}");
3431 }
3432
3433 #[test]
3434 fn display_formatting() {
3435 assert_eq!(SqliteValue::Null.to_string(), "NULL");
3436 assert_eq!(SqliteValue::Integer(42).to_string(), "42");
3437 assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
3438 assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
3439 assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
3440 assert_eq!(
3441 SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
3442 "X'CAFE'"
3443 );
3444 }
3445
3446 #[test]
3447 fn sort_order_null_first() {
3448 let null = SqliteValue::Null;
3449 let int = SqliteValue::Integer(0);
3450 let text = SqliteValue::Text(SmallText::new(""));
3451 let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
3452
3453 assert!(null < int);
3454 assert!(int < text);
3455 assert!(text < blob);
3456 }
3457
3458 #[test]
3459 fn sort_order_integers() {
3460 let a = SqliteValue::Integer(1);
3461 let b = SqliteValue::Integer(2);
3462 assert!(a < b);
3463 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
3464 }
3465
3466 #[test]
3467 fn sort_order_mixed_numeric() {
3468 let int = SqliteValue::Integer(1);
3469 let float = SqliteValue::Float(1.5);
3470 assert!(int < float);
3471
3472 let int = SqliteValue::Integer(2);
3473 assert!(int > float);
3474 }
3475
3476 #[test]
3477 fn test_int_float_precision_at_i64_boundary() {
3478 let imax = SqliteValue::Integer(i64::MAX);
3482 let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
3483 assert_eq!(
3484 imax.partial_cmp(&fmax),
3485 Some(Ordering::Less),
3486 "i64::MAX must be Less than 9223372036854775808.0"
3487 );
3488
3489 let a = SqliteValue::Integer(i64::MAX);
3491 let b = SqliteValue::Integer(i64::MAX - 1);
3492 let f = SqliteValue::Float(i64::MAX as f64);
3493 assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
3495 assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
3497 assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
3498 }
3499
3500 #[test]
3501 fn test_int_float_precision_symmetric() {
3502 let i = SqliteValue::Integer(i64::MAX);
3504 let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
3505 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3506 }
3507
3508 #[test]
3509 fn test_int_float_exact_representation() {
3510 let i = SqliteValue::Integer(42);
3512 let f = SqliteValue::Float(42.0);
3513 assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
3514 assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
3515
3516 let i = SqliteValue::Integer(3);
3518 let f = SqliteValue::Float(3.5);
3519 assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
3520 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3521 }
3522
3523 #[test]
3524 fn from_conversions() {
3525 assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
3526 assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
3527 assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
3528 assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
3529 assert_eq!(
3530 SqliteValue::from(String::from("world")).as_text(),
3531 Some("world")
3532 );
3533 assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
3534 assert!(SqliteValue::from(None::<i64>).is_null());
3535 assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
3536 }
3537
3538 #[test]
3539 fn affinity() {
3540 assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
3541 assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
3542 assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
3543 assert_eq!(
3544 SqliteValue::Text(SmallText::new("")).affinity(),
3545 TypeAffinity::Text
3546 );
3547 assert_eq!(
3548 SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
3549 TypeAffinity::Blob
3550 );
3551 }
3552
3553 #[test]
3554 fn null_equality() {
3555 let a = SqliteValue::Null;
3557 let b = SqliteValue::Null;
3558 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3559 }
3560
3561 #[test]
3564 fn test_storage_class_variants() {
3565 assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
3566 assert_eq!(
3567 SqliteValue::Integer(42).storage_class(),
3568 StorageClass::Integer
3569 );
3570 assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
3571 assert_eq!(
3572 SqliteValue::Text("hi".into()).storage_class(),
3573 StorageClass::Text
3574 );
3575 assert_eq!(
3576 SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
3577 StorageClass::Blob
3578 );
3579 }
3580
3581 #[test]
3582 fn test_type_affinity_advisory_text_into_integer_ok() {
3583 let val = SqliteValue::Text("hello".into());
3586 let coerced = val.apply_affinity(TypeAffinity::Integer);
3587 assert!(coerced.as_text().is_some());
3588 assert_eq!(coerced.as_text().unwrap(), "hello");
3589
3590 let val = SqliteValue::Text("42".into());
3592 let coerced = val.apply_affinity(TypeAffinity::Integer);
3593 assert_eq!(coerced.as_integer(), Some(42));
3594 }
3595
3596 #[test]
3597 fn test_type_affinity_advisory_integer_into_text_ok() {
3598 let val = SqliteValue::Integer(42);
3600 let coerced = val.apply_affinity(TypeAffinity::Text);
3601 assert_eq!(coerced.as_text(), Some("42"));
3602 }
3603
3604 #[test]
3605 fn test_type_affinity_comparison_coercion_matches_oracle() {
3606 let val = SqliteValue::Text("123".into());
3608 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3609 assert_eq!(coerced.as_integer(), Some(123));
3610
3611 let val = SqliteValue::Text("3.14".into());
3613 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3614 assert_eq!(coerced.as_float(), Some(3.14));
3615
3616 let val = SqliteValue::Text("hello".into());
3618 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3619 assert!(coerced.as_text().is_some());
3620
3621 let val = SqliteValue::Integer(42);
3623 let coerced = val.apply_affinity(TypeAffinity::Blob);
3624 assert_eq!(coerced.as_integer(), Some(42));
3625
3626 let val = SqliteValue::Float(5.0);
3628 let coerced = val.apply_affinity(TypeAffinity::Integer);
3629 assert_eq!(coerced.as_integer(), Some(5));
3630
3631 let val = SqliteValue::Float(5.5);
3633 let coerced = val.apply_affinity(TypeAffinity::Integer);
3634 assert_eq!(coerced.as_float(), Some(5.5));
3635
3636 let val = SqliteValue::Integer(7);
3638 let coerced = val.apply_affinity(TypeAffinity::Real);
3639 assert_eq!(coerced.as_float(), Some(7.0));
3640
3641 let val = SqliteValue::Text("9".into());
3643 let coerced = val.apply_affinity(TypeAffinity::Real);
3644 assert_eq!(coerced.as_float(), Some(9.0));
3645 }
3646
3647 #[test]
3648 fn test_cast_to_numeric_uses_sqlite_cast_rules() {
3649 assert_eq!(
3650 SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
3651 SqliteValue::Integer(123)
3652 );
3653 assert_eq!(
3654 SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
3655 SqliteValue::Integer(150)
3656 );
3657 assert_eq!(
3658 SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
3659 SqliteValue::Integer(0)
3660 );
3661 assert_eq!(
3662 SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
3663 SqliteValue::Integer(123)
3664 );
3665
3666 match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
3667 SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
3668 other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
3669 }
3670 }
3671
3672 #[test]
3673 fn test_strict_table_rejects_text_into_integer() {
3674 let val = SqliteValue::Text("hello".into());
3675 let result = val.validate_strict(StrictColumnType::Integer);
3676 assert!(result.is_err());
3677 let err = result.unwrap_err();
3678 assert_eq!(err.expected, StrictColumnType::Integer);
3679 assert_eq!(err.actual, StorageClass::Text);
3680 }
3681
3682 #[test]
3683 fn test_strict_table_allows_exact_type() {
3684 let val = SqliteValue::Integer(42);
3686 assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
3687
3688 let val = SqliteValue::Float(3.14);
3690 assert!(val.validate_strict(StrictColumnType::Real).is_ok());
3691
3692 let val = SqliteValue::Text("hello".into());
3694 assert!(val.validate_strict(StrictColumnType::Text).is_ok());
3695
3696 let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
3698 assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
3699
3700 assert!(
3702 SqliteValue::Null
3703 .validate_strict(StrictColumnType::Integer)
3704 .is_ok()
3705 );
3706 assert!(
3707 SqliteValue::Null
3708 .validate_strict(StrictColumnType::Text)
3709 .is_ok()
3710 );
3711
3712 let val = SqliteValue::Integer(42);
3714 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3715 let val = SqliteValue::Text("hi".into());
3716 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3717 }
3718
3719 #[test]
3720 fn test_strict_real_accepts_integer_with_coercion() {
3721 let val = SqliteValue::Integer(42);
3723 let result = val.validate_strict(StrictColumnType::Real).unwrap();
3724 assert_eq!(result.as_float(), Some(42.0));
3725 }
3726
3727 #[test]
3728 fn test_strict_rejects_wrong_storage_classes() {
3729 assert!(
3731 SqliteValue::Float(3.14)
3732 .validate_strict(StrictColumnType::Integer)
3733 .is_err()
3734 );
3735
3736 assert!(
3738 SqliteValue::Blob(Arc::from([1u8].as_slice()))
3739 .validate_strict(StrictColumnType::Text)
3740 .is_err()
3741 );
3742
3743 assert_eq!(
3745 SqliteValue::Integer(1)
3746 .validate_strict(StrictColumnType::Text)
3747 .unwrap(),
3748 SqliteValue::Text("1".into())
3749 );
3750
3751 assert!(
3753 SqliteValue::Text("x".into())
3754 .validate_strict(StrictColumnType::Blob)
3755 .is_err()
3756 );
3757 }
3758
3759 #[test]
3760 fn test_strict_column_type_parsing() {
3761 assert_eq!(
3762 StrictColumnType::from_type_name("INT"),
3763 Some(StrictColumnType::Integer)
3764 );
3765 assert_eq!(
3766 StrictColumnType::from_type_name("INTEGER"),
3767 Some(StrictColumnType::Integer)
3768 );
3769 assert_eq!(
3770 StrictColumnType::from_type_name("REAL"),
3771 Some(StrictColumnType::Real)
3772 );
3773 assert_eq!(
3774 StrictColumnType::from_type_name("TEXT"),
3775 Some(StrictColumnType::Text)
3776 );
3777 assert_eq!(
3778 StrictColumnType::from_type_name("BLOB"),
3779 Some(StrictColumnType::Blob)
3780 );
3781 assert_eq!(
3782 StrictColumnType::from_type_name("ANY"),
3783 Some(StrictColumnType::Any)
3784 );
3785 assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
3787 assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
3788 }
3789
3790 #[test]
3791 fn test_affinity_advisory_never_rejects() {
3792 let values = vec![
3794 SqliteValue::Null,
3795 SqliteValue::Integer(42),
3796 SqliteValue::Float(3.14),
3797 SqliteValue::Text("hello".into()),
3798 SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
3799 ];
3800 let affinities = [
3801 TypeAffinity::Integer,
3802 TypeAffinity::Text,
3803 TypeAffinity::Blob,
3804 TypeAffinity::Real,
3805 TypeAffinity::Numeric,
3806 ];
3807 for val in &values {
3808 for aff in &affinities {
3809 let _ = val.clone().apply_affinity(*aff);
3811 }
3812 }
3813 }
3814
3815 #[test]
3818 fn test_unique_allows_multiple_nulls_single_column() {
3819 let a = SqliteValue::Null;
3821 let b = SqliteValue::Null;
3822 assert!(!a.unique_eq(&b));
3823 }
3824
3825 #[test]
3826 fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
3827 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3830 let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
3831 assert!(!unique_key_duplicates(&row_a, &row_b));
3832
3833 let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
3835 let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
3836 assert!(!unique_key_duplicates(&row_a, &row_b));
3837
3838 let row_a = [SqliteValue::Null, SqliteValue::Null];
3840 let row_b = [SqliteValue::Null, SqliteValue::Null];
3841 assert!(!unique_key_duplicates(&row_a, &row_b));
3842 }
3843
3844 #[test]
3845 fn test_unique_rejects_duplicate_non_null() {
3846 let a = SqliteValue::Integer(42);
3848 let b = SqliteValue::Integer(42);
3849 assert!(a.unique_eq(&b));
3850
3851 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3853 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3854 assert!(unique_key_duplicates(&row_a, &row_b));
3855
3856 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3858 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
3859 assert!(!unique_key_duplicates(&row_a, &row_b));
3860 }
3861
3862 #[test]
3863 fn test_unique_null_vs_non_null_distinct() {
3864 let a = SqliteValue::Null;
3866 let b = SqliteValue::Integer(1);
3867 assert!(!a.unique_eq(&b));
3868 assert!(!b.unique_eq(&a));
3869
3870 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3872 let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
3873 assert!(!unique_key_duplicates(&row_a, &row_b));
3874 }
3875
3876 #[test]
3879 #[allow(clippy::cast_precision_loss)]
3880 fn test_integer_overflow_promotes_real_expr_add() {
3881 let max = SqliteValue::Integer(i64::MAX);
3882 let one = SqliteValue::Integer(1);
3883 let result = max.sql_add(&one);
3884 assert!(result.as_integer().is_none());
3886 assert!(result.as_float().is_some());
3887 assert!(result.as_float().unwrap() >= i64::MAX as f64);
3889 }
3890
3891 #[test]
3892 fn test_integer_overflow_promotes_real_expr_mul() {
3893 let max = SqliteValue::Integer(i64::MAX);
3894 let two = SqliteValue::Integer(2);
3895 let result = max.sql_mul(&two);
3896 assert!(result.as_float().is_some());
3898 }
3899
3900 #[test]
3901 fn test_integer_overflow_promotes_real_expr_sub() {
3902 let min = SqliteValue::Integer(i64::MIN);
3903 let one = SqliteValue::Integer(1);
3904 let result = min.sql_sub(&one);
3905 assert!(result.as_float().is_some());
3907 }
3908
3909 #[test]
3910 fn test_sum_overflow_errors() {
3911 let mut acc = SumAccumulator::new();
3912 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3913 acc.accumulate(&SqliteValue::Integer(1));
3914 let result = acc.finish();
3915 assert!(result.is_err());
3916 }
3917
3918 #[test]
3919 fn test_sum_overflow_then_float_returns_real() {
3920 let mut acc = SumAccumulator::new();
3921 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3922 acc.accumulate(&SqliteValue::Integer(1));
3923 acc.accumulate(&SqliteValue::Float(0.5));
3924 let result = acc.finish().unwrap();
3925 assert!(matches!(result, SqliteValue::Float(_)));
3926 }
3927
3928 #[test]
3929 fn test_sum_text_integer_literals_stay_integer() {
3930 let mut acc = SumAccumulator::new();
3931 acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
3932 acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
3933 let result = acc.finish().unwrap();
3934 assert_eq!(result.as_integer(), Some(3));
3935 }
3936
3937 #[test]
3938 fn test_sum_non_numeric_text_returns_real_zero() {
3939 let mut acc = SumAccumulator::new();
3940 acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
3941 let result = acc.finish().unwrap();
3942 assert_eq!(result.as_float(), Some(0.0));
3943 }
3944
3945 #[test]
3946 fn test_no_overflow_stays_integer() {
3947 let a = SqliteValue::Integer(100);
3949 let b = SqliteValue::Integer(200);
3950 let result = a.sql_add(&b);
3951 assert_eq!(result.as_integer(), Some(300));
3952
3953 let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
3955 assert_eq!(result.as_integer(), Some(42));
3956
3957 let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
3959 assert_eq!(result.as_integer(), Some(42));
3960 }
3961
3962 #[test]
3963 fn test_sum_null_only_returns_null() {
3964 let mut acc = SumAccumulator::new();
3965 acc.accumulate(&SqliteValue::Null);
3966 acc.accumulate(&SqliteValue::Null);
3967 let result = acc.finish().unwrap();
3968 assert!(result.is_null());
3969 }
3970
3971 #[test]
3972 fn test_sum_mixed_int_float() {
3973 let mut acc = SumAccumulator::new();
3974 acc.accumulate(&SqliteValue::Integer(10));
3975 acc.accumulate(&SqliteValue::Float(2.5));
3976 acc.accumulate(&SqliteValue::Integer(3));
3977 let result = acc.finish().unwrap();
3978 assert_eq!(result.as_float(), Some(15.5));
3980 }
3981
3982 #[test]
3983 fn test_sum_integer_only() {
3984 let mut acc = SumAccumulator::new();
3985 acc.accumulate(&SqliteValue::Integer(10));
3986 acc.accumulate(&SqliteValue::Integer(20));
3987 acc.accumulate(&SqliteValue::Integer(30));
3988 let result = acc.finish().unwrap();
3989 assert_eq!(result.as_integer(), Some(60));
3990 }
3991
3992 #[test]
3993 fn test_sql_arithmetic_null_propagation() {
3994 let n = SqliteValue::Null;
3995 let i = SqliteValue::Integer(42);
3996 assert!(n.sql_add(&i).is_null());
3997 assert!(i.sql_add(&n).is_null());
3998 assert!(n.sql_sub(&i).is_null());
3999 assert!(n.sql_mul(&i).is_null());
4000 }
4001
4002 #[test]
4003 fn test_sql_inf_arithmetic_nan_normalized_to_null() {
4004 let pos_inf = SqliteValue::Float(f64::INFINITY);
4006 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
4007 assert!(pos_inf.sql_add(&neg_inf).is_null());
4008
4009 assert!(pos_inf.sql_sub(&pos_inf).is_null());
4011 }
4012
4013 #[test]
4014 fn test_sql_mul_zero_times_inf_normalized_to_null() {
4015 let zero = SqliteValue::Float(0.0);
4017 let pos_inf = SqliteValue::Float(f64::INFINITY);
4018 assert!(zero.sql_mul(&pos_inf).is_null());
4019 assert!(
4020 SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
4021 "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
4022 );
4023 }
4024
4025 #[test]
4026 fn test_sql_mul_mixed_int_float_stays_real() {
4027 let left = SqliteValue::Integer(10);
4028 let right = SqliteValue::Float(0.25);
4029 assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
4030 assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
4031 }
4032
4033 #[test]
4034 fn test_sql_inf_propagates_when_not_nan() {
4035 let pos_inf = SqliteValue::Float(f64::INFINITY);
4036 let one = SqliteValue::Integer(1);
4037 let add_result = pos_inf.sql_add(&one);
4038 assert!(
4039 matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
4040 "expected +Inf propagation, got {add_result:?}"
4041 );
4042
4043 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
4044 let sub_result = neg_inf.sql_sub(&one);
4045 assert!(
4046 matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
4047 "expected -Inf propagation, got {sub_result:?}"
4048 );
4049 }
4050
4051 #[test]
4052 fn test_from_f64_nan_normalizes_to_null() {
4053 let value = SqliteValue::from(f64::NAN);
4054 assert!(value.is_null());
4055 }
4056
4057 #[test]
4058 fn test_inf_comparisons_against_finite_values() {
4059 let pos_inf = SqliteValue::Float(f64::INFINITY);
4060 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
4061 let finite_hi = SqliteValue::Float(1.0e308);
4062 let finite_lo = SqliteValue::Float(-1.0e308);
4063
4064 assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
4065 assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
4066 }
4067
4068 #[test]
4071 fn test_empty_string_is_not_null() {
4072 let empty = SqliteValue::Text(SmallText::new(""));
4073 assert!(!empty.is_null());
4075 assert!(!empty.is_null());
4077 assert!(SqliteValue::Null.is_null());
4079 }
4080
4081 #[test]
4082 fn test_length_empty_string_zero() {
4083 let empty = SqliteValue::Text(SmallText::new(""));
4084 assert_eq!(empty.sql_length(), Some(0));
4085 }
4086
4087 #[test]
4088 fn test_typeof_empty_string_text() {
4089 let empty = SqliteValue::Text(SmallText::new(""));
4090 assert_eq!(empty.typeof_str(), "text");
4091 assert_eq!(SqliteValue::Null.typeof_str(), "null");
4093 }
4094
4095 #[test]
4096 fn test_empty_string_comparisons() {
4097 let empty1 = SqliteValue::Text(SmallText::new(""));
4098 let empty2 = SqliteValue::Text(SmallText::new(""));
4099 assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
4101
4102 let null = SqliteValue::Null;
4106 assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
4107 }
4108
4109 #[test]
4110 fn test_typeof_all_variants() {
4111 assert_eq!(SqliteValue::Null.typeof_str(), "null");
4112 assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
4113 assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
4114 assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
4115 assert_eq!(
4116 SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
4117 "blob"
4118 );
4119 }
4120
4121 #[test]
4122 fn test_sql_length_all_types() {
4123 assert_eq!(SqliteValue::Null.sql_length(), None);
4125 assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
4127 assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
4128 assert_eq!(
4130 SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
4131 Some(3)
4132 );
4133 assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
4135 assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); }
4138
4139 #[test]
4142 fn test_like_ascii_case_insensitive() {
4143 assert!(sql_like("A", "a", None));
4144 assert!(sql_like("a", "A", None));
4145 assert!(sql_like("hello", "HELLO", None));
4146 assert!(sql_like("HELLO", "hello", None));
4147 assert!(sql_like("HeLLo", "hEllO", None));
4148 }
4149
4150 #[test]
4151 fn test_like_unicode_case_sensitive_without_icu() {
4152 assert!(!sql_like("ä", "Ä", None));
4154 assert!(!sql_like("Ä", "ä", None));
4155 assert!(sql_like("ä", "ä", None));
4157 }
4158
4159 #[test]
4160 fn test_like_fast_path_does_not_fold_ascii_punctuation() {
4161 assert!(!sql_like("[", "{", None));
4162 assert!(!sql_like("@", "`", None));
4163 }
4164
4165 #[test]
4166 fn test_like_escape_handling() {
4167 assert!(sql_like("100\\%", "100%", Some('\\')));
4169 assert!(!sql_like("100\\%", "100x", Some('\\')));
4170
4171 assert!(sql_like("a\\_b", "a_b", Some('\\')));
4173 assert!(!sql_like("a\\_b", "axb", Some('\\')));
4174 }
4175
4176 #[test]
4177 fn test_like_wildcards_basic() {
4178 assert!(sql_like("%", "", None));
4180 assert!(sql_like("%", "anything", None));
4181 assert!(sql_like("a%", "abc", None));
4182 assert!(sql_like("%c", "abc", None));
4183 assert!(sql_like("a%c", "abc", None));
4184 assert!(sql_like("a%c", "aXYZc", None));
4185 assert!(!sql_like("a%c", "abd", None));
4186
4187 assert!(sql_like("_", "x", None));
4189 assert!(!sql_like("_", "", None));
4190 assert!(!sql_like("_", "xy", None));
4191 assert!(sql_like("a_c", "abc", None));
4192 assert!(!sql_like("a_c", "abbc", None));
4193 }
4194
4195 #[test]
4196 fn test_like_combined_wildcards() {
4197 assert!(sql_like("%_", "a", None));
4198 assert!(!sql_like("%_", "", None));
4199 assert!(sql_like("_%_", "ab", None));
4200 assert!(!sql_like("_%_", "a", None));
4201 assert!(sql_like("%a%b%", "xaybz", None));
4202 assert!(!sql_like("%a%b%", "xyz", None));
4203 }
4204
4205 #[test]
4206 fn test_like_exact_match() {
4207 assert!(sql_like("hello", "hello", None));
4208 assert!(!sql_like("hello", "world", None));
4209 assert!(sql_like("", "", None));
4210 assert!(!sql_like("a", "", None));
4211 assert!(!sql_like("", "a", None));
4212 }
4213
4214 #[test]
4215 fn test_like_fast_path_repeated_percent_shapes() {
4216 assert!(sql_like("ab%%", "ABcd", None));
4217 assert!(sql_like("%%cd", "abCD", None));
4218 assert!(sql_like("%%bc%%", "xxBCyy", None));
4219 assert!(sql_like("%%%%", "anything", None));
4220 }
4221
4222 #[test]
4223 fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
4224 assert!(sql_like("%éL%", "héllo", None));
4225 assert!(!sql_like("%Él%", "héllo", None));
4226 assert!(sql_like("Stra%", "straße", None));
4227 }
4228
4229 #[test]
4230 fn test_like_contains_fast_path_handles_overlapping_matches() {
4231 assert!(sql_like("%ana%", "bananas", None));
4232 assert!(sql_like("%NAN%", "baNanas", None));
4233 assert!(!sql_like("%ananasx%", "bananas", None));
4234 }
4235
4236 #[test]
4237 fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
4238 assert!(sql_like("%ß%", "straße", None));
4239 assert!(!sql_like("%SS%", "straße", None));
4240 }
4241
4242 #[test]
4245 fn test_sqlite_float_altform2_digits_matches_stock_bd_ixizz() {
4246 fn check(f: f64, digits: &str, exp: i32) {
4249 let (d, e) = super::sqlite_float_altform2_digits(f);
4250 assert_eq!(
4251 (String::from_utf8(d).unwrap().as_str(), e),
4252 (digits, exp),
4253 "sqlite_float_altform2_digits({f})"
4254 );
4255 }
4256 check(2.0 / 3.0, "666666666666666629", -1);
4258 check(1.0 / 3.0, "333333333333333314", -1);
4260 check(0.1, "100000000000000005", -1);
4262 check(1.0 / 7.0, "142857142857142849", -1);
4264 check(1e300, "1000000000000000052", 300);
4266 check(2.5, "250000000000000000", 0);
4269 check(5.0, "500000000000000000", 0);
4270 check(100.0, "100000000000000000", 2);
4271 check(0.5, "500000000000000000", -1);
4272 }
4273
4274 #[test]
4275 fn test_format_sqlite_float_whole_number() {
4276 assert_eq!(format_sqlite_float(120.0), "120.0");
4277 assert_eq!(format_sqlite_float(0.0), "0.0");
4278 assert_eq!(format_sqlite_float(-42.0), "-42.0");
4279 assert_eq!(format_sqlite_float(1.0), "1.0");
4280 }
4281
4282 #[test]
4283 fn test_format_sqlite_float_fractional() {
4284 assert_eq!(format_sqlite_float(3.14), "3.14");
4285 assert_eq!(format_sqlite_float(0.5), "0.5");
4286 assert_eq!(format_sqlite_float(-0.001), "-0.001");
4287 }
4288
4289 #[test]
4290 fn test_format_sqlite_float_special() {
4291 assert_eq!(format_sqlite_float(f64::NAN), "NaN");
4292 assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
4293 assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
4294 }
4295
4296 #[test]
4297 fn test_format_sqlite_float_negative_zero() {
4298 assert_eq!(format_sqlite_float(-0.0), "0.0");
4300 assert_eq!(format_sqlite_float(0.0), "0.0");
4301 }
4302
4303 #[test]
4304 fn test_format_sqlite_float_matches_sqlite_17_digit_text_contract() {
4305 assert_eq!(format_sqlite_float(0.1 + 0.2), "0.30000000000000004");
4306 assert_eq!(format_sqlite_float(1.0 / 3.0), "0.33333333333333332");
4307 assert_eq!(format_sqlite_float(2.0 / 3.0), "0.66666666666666663");
4308 assert_eq!(format_sqlite_float(1.5e16), "15000000000000000.0");
4309 assert_eq!(
4310 format_sqlite_float(123_456_789_012_345.6),
4311 "123456789012345.59"
4312 );
4313 assert_eq!(format_sqlite_float(1.0e308), "1.0e+308");
4314 assert_eq!(format_sqlite_float(1.0e-308), "1.0e-308");
4315 assert_eq!(
4316 format_sqlite_float(9.223_372_036_854_776e18),
4317 "9.2233720368547758e+18"
4318 );
4319 }
4320
4321 #[test]
4322 fn test_float_to_text_includes_decimal_point() {
4323 let v = SqliteValue::Float(100.0);
4324 assert_eq!(v.to_text(), "100.0");
4325 let v = SqliteValue::Float(3.14);
4326 assert_eq!(v.to_text(), "3.14");
4327 }
4328
4329 #[test]
4332 fn test_scan_numeric_prefix_bare_dot() {
4333 assert_eq!(scan_numeric_prefix(b"."), 0);
4335 assert_eq!(scan_numeric_prefix(b"-."), 0);
4336 assert_eq!(scan_numeric_prefix(b"+."), 0);
4337 assert_eq!(scan_numeric_prefix(b"..1"), 0);
4338 }
4339
4340 #[test]
4341 fn test_scan_numeric_prefix_valid() {
4342 assert_eq!(scan_numeric_prefix(b"123"), 3);
4343 assert_eq!(scan_numeric_prefix(b"3.14"), 4);
4344 assert_eq!(scan_numeric_prefix(b".5"), 2);
4345 assert_eq!(scan_numeric_prefix(b"1e10"), 4);
4346 assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
4347 assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
4348 assert_eq!(scan_numeric_prefix(b"0.0"), 3);
4349 }
4350
4351 #[test]
4352 fn test_scan_numeric_prefix_empty_and_non_numeric() {
4353 assert_eq!(scan_numeric_prefix(b""), 0);
4354 assert_eq!(scan_numeric_prefix(b"abc"), 0);
4355 assert_eq!(scan_numeric_prefix(b"+"), 0);
4356 assert_eq!(scan_numeric_prefix(b"-"), 0);
4357 }
4358}