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 to_record_text_bytes(&self, encoding: TextEncoding) -> Cow<'_, [u8]> {
387 match encoding {
388 TextEncoding::Utf8 => Cow::Borrowed(self.as_bytes_direct()),
389 TextEncoding::Utf16le | TextEncoding::Utf16be => {
390 let little_endian = matches!(encoding, TextEncoding::Utf16le);
391 let text = self.as_str();
392 let mut bytes = Vec::with_capacity(text.len().saturating_mul(2));
393 for unit in text.encode_utf16() {
394 let pair = if little_endian {
395 unit.to_le_bytes()
396 } else {
397 unit.to_be_bytes()
398 };
399 bytes.extend_from_slice(&pair);
400 }
401 Cow::Owned(bytes)
402 }
403 }
404 }
405
406 #[inline]
407 fn from_raw_bytes(bytes: Arc<[u8]>) -> Self {
408 debug_assert!(simdutf8::basic::from_utf8(&bytes).is_err());
409 let lossy: Arc<str> = Arc::from(String::from_utf8_lossy(&bytes).into_owned());
410 Self {
411 repr: SmallTextRepr::Raw { bytes, lossy },
412 }
413 }
414
415 #[inline]
418 pub fn overwrite(&mut self, s: &str) {
419 if s.len() <= SMALL_TEXT_INLINE_CAP {
420 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
421 buf[..s.len()].copy_from_slice(s.as_bytes());
422 self.repr = SmallTextRepr::Inline {
423 len: s.len() as u8,
424 buf,
425 };
426 return;
427 }
428
429 match &mut self.repr {
430 SmallTextRepr::HeapOwned { text, shared } => {
431 text.clear();
432 text.push_str(s);
433 if shared.get().is_some() {
434 *shared = OnceLock::new();
435 }
436 }
437 _ => {
438 self.repr = SmallTextRepr::HeapOwned {
439 text: s.to_owned(),
440 shared: OnceLock::new(),
441 };
442 }
443 }
444 }
445
446 #[inline]
448 pub fn overwrite_bytes(&mut self, bytes: &[u8]) {
449 match simdutf8::basic::from_utf8(bytes) {
450 Ok(text) => self.overwrite(text),
451 Err(_) => *self = Self::from_raw_bytes(Arc::from(bytes)),
452 }
453 }
454
455 #[inline]
465 pub fn as_str(&self) -> &str {
466 match &self.repr {
467 SmallTextRepr::Inline { len, buf } => simdutf8::basic::from_utf8(&buf[..*len as usize])
468 .expect("SmallText inline representation must always contain valid UTF-8"),
469 SmallTextRepr::HeapOwned { text, .. } => text.as_str(),
470 SmallTextRepr::HeapShared(text) => text,
471 SmallTextRepr::Raw { lossy, .. } => lossy,
472 }
473 }
474
475 #[inline]
477 #[must_use]
478 pub fn as_str_checked(&self) -> Option<&str> {
479 match &self.repr {
480 SmallTextRepr::Raw { .. } => None,
481 _ => Some(self.as_str()),
482 }
483 }
484
485 #[inline]
487 #[must_use]
488 pub fn is_valid_utf8(&self) -> bool {
489 !matches!(&self.repr, SmallTextRepr::Raw { .. })
490 }
491
492 #[inline]
505 #[must_use]
506 pub fn as_bytes_direct(&self) -> &[u8] {
507 match &self.repr {
508 SmallTextRepr::Inline { len, buf } => &buf[..*len as usize],
509 SmallTextRepr::HeapOwned { text, .. } => text.as_bytes(),
510 SmallTextRepr::HeapShared(text) => text.as_bytes(),
511 SmallTextRepr::Raw { bytes, .. } => bytes,
512 }
513 }
514
515 #[inline]
517 pub fn len(&self) -> usize {
518 match &self.repr {
519 SmallTextRepr::Inline { len, .. } => *len as usize,
520 SmallTextRepr::HeapOwned { text, .. } => text.len(),
521 SmallTextRepr::HeapShared(text) => text.len(),
522 SmallTextRepr::Raw { bytes, .. } => bytes.len(),
523 }
524 }
525
526 #[inline]
528 pub fn is_empty(&self) -> bool {
529 self.len() == 0
530 }
531
532 #[inline]
534 pub fn is_inline(&self) -> bool {
535 matches!(&self.repr, SmallTextRepr::Inline { .. })
536 }
537
538 #[inline]
540 pub fn into_arc(self) -> Arc<str> {
541 match self.repr {
542 SmallTextRepr::Inline { len, buf } => {
543 let s = simdutf8::basic::from_utf8(&buf[..len as usize])
545 .expect("SmallText inline representation must always contain valid UTF-8");
546 Arc::from(s)
547 }
548 SmallTextRepr::HeapOwned { text, shared } => shared
549 .into_inner()
550 .unwrap_or_else(|| Arc::<str>::from(text)),
551 SmallTextRepr::HeapShared(text) => text,
552 SmallTextRepr::Raw { lossy, .. } => lossy,
553 }
554 }
555}
556
557impl Default for SmallText {
558 #[inline]
559 fn default() -> Self {
560 Self::new("")
561 }
562}
563
564impl fmt::Debug for SmallText {
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 fmt::Debug::fmt(self.as_str(), f)
567 }
568}
569
570impl fmt::Display for SmallText {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572 fmt::Display::fmt(self.as_str(), f)
573 }
574}
575
576impl PartialEq for SmallText {
577 #[inline]
578 fn eq(&self, other: &Self) -> bool {
579 #[cfg(feature = "bench-internals")]
580 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
581 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
582 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
583 return self.as_bytes_direct() == other.as_bytes_direct();
584 }
585 match (self.as_str_checked(), other.as_str_checked()) {
586 (Some(left), Some(right)) => left == right,
587 _ => self.as_bytes_direct() == other.as_bytes_direct(),
588 }
589 }
590}
591
592impl Eq for SmallText {}
593
594impl PartialOrd for SmallText {
595 #[inline]
596 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
597 Some(self.cmp(other))
598 }
599}
600
601impl Ord for SmallText {
602 #[inline]
603 fn cmp(&self, other: &Self) -> Ordering {
604 #[cfg(feature = "bench-internals")]
605 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
606 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
607 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
608 return self.as_bytes_direct().cmp(other.as_bytes_direct());
609 }
610 match (self.as_str_checked(), other.as_str_checked()) {
611 (Some(left), Some(right)) => left.cmp(right),
612 _ => self.as_bytes_direct().cmp(other.as_bytes_direct()),
613 }
614 }
615}
616
617impl Hash for SmallText {
618 #[inline]
619 fn hash<H: Hasher>(&self, state: &mut H) {
620 #[cfg(feature = "bench-internals")]
621 if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
622 SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
623 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
624 state.write(self.as_bytes_direct());
625 state.write_u8(0xff);
626 return;
627 }
628 if let Some(text) = self.as_str_checked() {
629 text.hash(state);
630 } else {
631 state.write(self.as_bytes_direct());
632 state.write_u8(0xff);
633 }
634 }
635}
636
637impl From<&str> for SmallText {
638 #[inline]
639 fn from(s: &str) -> Self {
640 Self::new(s)
641 }
642}
643
644impl From<String> for SmallText {
645 #[inline]
646 fn from(s: String) -> Self {
647 Self::from_string(s)
648 }
649}
650
651impl From<Arc<str>> for SmallText {
652 #[inline]
653 fn from(arc: Arc<str>) -> Self {
654 Self::from_arc(arc)
655 }
656}
657
658impl AsRef<str> for SmallText {
659 #[inline]
660 fn as_ref(&self) -> &str {
661 self.as_str()
662 }
663}
664
665impl std::ops::Deref for SmallText {
666 type Target = str;
667
668 #[inline]
669 fn deref(&self) -> &Self::Target {
670 self.as_str()
671 }
672}
673
674impl std::borrow::Borrow<str> for SmallText {
675 #[inline]
676 fn borrow(&self) -> &str {
677 self.as_str()
678 }
679}
680
681impl serde::Serialize for SmallText {
683 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
684 where
685 S: serde::Serializer,
686 {
687 let Some(text) = self.as_str_checked() else {
688 return Err(serde::ser::Error::custom(
689 "SQLite TEXT containing invalid UTF-8 cannot be serialized as a Rust string",
690 ));
691 };
692 serializer.serialize_str(text)
693 }
694}
695
696impl<'de> serde::Deserialize<'de> for SmallText {
697 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
698 where
699 D: serde::Deserializer<'de>,
700 {
701 let s = String::deserialize(deserializer)?;
702 Ok(Self::from_string(s))
703 }
704}
705
706fn scan_numeric_prefix(bytes: &[u8]) -> usize {
712 if bytes.is_empty() {
713 return 0;
714 }
715
716 let mut i = 0usize;
717 if bytes[i] == b'+' || bytes[i] == b'-' {
718 i += 1;
719 }
720
721 let mut has_digit = false;
722 while i < bytes.len() && bytes[i].is_ascii_digit() {
723 has_digit = true;
724 i += 1;
725 }
726
727 if i < bytes.len() && bytes[i] == b'.' {
728 i += 1;
729 while i < bytes.len() && bytes[i].is_ascii_digit() {
730 has_digit = true;
731 i += 1;
732 }
733 }
734
735 if !has_digit {
736 return 0;
737 }
738
739 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
740 let exp_start = i;
741 i += 1;
742 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
743 i += 1;
744 }
745 if i < bytes.len() && bytes[i].is_ascii_digit() {
746 while i < bytes.len() && bytes[i].is_ascii_digit() {
747 i += 1;
748 }
749 } else {
750 i = exp_start;
751 }
752 }
753
754 i
755}
756
757#[allow(clippy::cast_possible_truncation)]
759fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
760 let mut start = 0;
761 while start < b.len() && b[start].is_ascii_whitespace() {
762 start += 1;
763 }
764 let trimmed = &b[start..];
765 let end = scan_numeric_prefix(trimmed);
766 if end == 0 {
767 return 0;
768 }
769 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
772 let f = s.parse::<f64>().unwrap_or(0.0);
773 #[allow(clippy::manual_clamp)]
774 if f >= i64::MAX as f64 {
775 i64::MAX
776 } else if f <= i64::MIN as f64 {
777 i64::MIN
778 } else {
779 f as i64
780 }
781}
782
783#[allow(clippy::cast_possible_truncation)]
785fn parse_integer_prefix(s: &str) -> i64 {
786 parse_integer_prefix_bytes(s.as_bytes())
787}
788
789fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
791 let mut start = 0;
792 while start < b.len() && b[start].is_ascii_whitespace() {
793 start += 1;
794 }
795 let trimmed = &b[start..];
796 let end = scan_numeric_prefix(trimmed);
797 if end == 0 {
798 return 0.0;
799 }
800 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
803 s.parse::<f64>().unwrap_or(0.0)
804}
805
806fn parse_float_prefix(s: &str) -> f64 {
808 parse_float_prefix_bytes(s.as_bytes())
809}
810
811fn trim_sqlite_ascii_whitespace(s: &str) -> &str {
812 s.trim_matches(|ch: char| ch.is_ascii_whitespace())
813}
814
815fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
816 let trimmed = trim_sqlite_ascii_whitespace(s);
817 let end = scan_numeric_prefix(trimmed.as_bytes());
818 if end == 0 {
819 return SqliteValue::Integer(0);
820 }
821
822 let prefix = &trimmed[..end];
823 let is_integer_syntax = !prefix
824 .as_bytes()
825 .iter()
826 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
827
828 if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
829 return SqliteValue::Integer(value);
830 }
831
832 if let Ok(value) = prefix.parse::<f64>() {
833 if value.is_finite()
834 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
835 {
836 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
837 let truncated = value as i64;
838 #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
839 if truncated as f64 == value {
840 return SqliteValue::Integer(truncated);
841 }
842 }
843 return SqliteValue::Float(value);
844 }
845
846 SqliteValue::Integer(0)
847}
848
849#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
854pub enum SqliteValue {
855 Null,
857 Integer(i64),
859 Float(f64),
861 Text(SmallText),
867 Blob(Arc<[u8]>),
871}
872
873impl SqliteValue {
874 pub const fn affinity(&self) -> TypeAffinity {
876 match self {
877 Self::Null | Self::Blob(_) => TypeAffinity::Blob,
878 Self::Integer(_) => TypeAffinity::Integer,
879 Self::Float(_) => TypeAffinity::Real,
880 Self::Text(_) => TypeAffinity::Text,
881 }
882 }
883
884 pub const fn storage_class(&self) -> StorageClass {
886 match self {
887 Self::Null => StorageClass::Null,
888 Self::Integer(_) => StorageClass::Integer,
889 Self::Float(_) => StorageClass::Real,
890 Self::Text(_) => StorageClass::Text,
891 Self::Blob(_) => StorageClass::Blob,
892 }
893 }
894
895 #[must_use]
907 #[allow(
908 clippy::cast_possible_truncation,
909 clippy::cast_precision_loss,
910 clippy::float_cmp
911 )]
912 pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
913 match affinity {
914 TypeAffinity::Blob => self,
915 TypeAffinity::Text => match self {
916 Self::Null | Self::Text(_) | Self::Blob(_) => self,
917 Self::Integer(_) | Self::Float(_) => {
918 let t = self.to_text();
919 Self::Text(SmallText::from_string(t))
920 }
921 },
922 TypeAffinity::Numeric | TypeAffinity::Integer => match &self {
923 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
924 Self::Float(f) => {
925 if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
926 let i = *f as i64;
927 if (i as f64) == *f {
928 return Self::Integer(i);
929 }
930 }
931 self
932 }
933 _ => self,
934 },
935 TypeAffinity::Real => match &self {
936 Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
937 .map(|v| match v {
938 Self::Integer(i) => Self::Float(i as f64),
939 other => other,
940 })
941 .unwrap_or(self),
942 Self::Integer(i) => Self::Float(*i as f64),
943 _ => self,
944 },
945 }
946 }
947
948 #[allow(clippy::cast_precision_loss)]
955 pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
956 if matches!(self, Self::Null) {
957 return Ok(self);
958 }
959 match col_type {
960 StrictColumnType::Any => Ok(self),
961 StrictColumnType::Integer => match self {
962 Self::Integer(_) => Ok(self),
963 Self::Float(fl) => {
968 #[allow(clippy::cast_possible_truncation)]
969 let as_int = fl as i64;
970 #[allow(clippy::float_cmp)]
971 if as_int as f64 == fl {
972 Ok(Self::Integer(as_int))
973 } else {
974 Err(StrictTypeError {
975 expected: col_type,
976 actual: StorageClass::Real,
977 })
978 }
979 }
980 Self::Text(s) => match try_coerce_text_to_numeric(s.as_str()) {
985 Some(v @ Self::Integer(_)) => Ok(v),
986 _ => Err(StrictTypeError {
987 expected: col_type,
988 actual: StorageClass::Text,
989 }),
990 },
991 other => Err(StrictTypeError {
992 expected: col_type,
993 actual: other.storage_class(),
994 }),
995 },
996 StrictColumnType::Real => match self {
997 Self::Float(_) => Ok(self),
998 Self::Integer(i) => Ok(Self::Float(i as f64)),
999 Self::Text(s) => match try_coerce_text_to_numeric(s.as_str()) {
1002 Some(Self::Integer(i)) => Ok(Self::Float(i as f64)),
1003 Some(v @ Self::Float(_)) => Ok(v),
1004 _ => Err(StrictTypeError {
1005 expected: col_type,
1006 actual: StorageClass::Text,
1007 }),
1008 },
1009 other => Err(StrictTypeError {
1010 expected: col_type,
1011 actual: other.storage_class(),
1012 }),
1013 },
1014 StrictColumnType::Text => match self {
1015 Self::Text(_) => Ok(self),
1016 Self::Integer(i) => Ok(Self::Text(SmallText::from_string(i.to_string()))),
1021 Self::Float(fl) => {
1022 Ok(Self::Text(SmallText::from_string(format_sqlite_float(fl))))
1023 }
1024 other => Err(StrictTypeError {
1025 expected: col_type,
1026 actual: other.storage_class(),
1027 }),
1028 },
1029 StrictColumnType::Blob => match self {
1030 Self::Blob(_) => Ok(self),
1031 other => Err(StrictTypeError {
1032 expected: col_type,
1033 actual: other.storage_class(),
1034 }),
1035 },
1036 }
1037 }
1038
1039 #[inline(always)]
1041 #[allow(clippy::inline_always)]
1042 pub const fn is_null(&self) -> bool {
1043 matches!(self, Self::Null)
1044 }
1045
1046 #[inline]
1048 pub const fn as_integer(&self) -> Option<i64> {
1049 match self {
1050 Self::Integer(i) => Some(*i),
1051 _ => None,
1052 }
1053 }
1054
1055 #[inline]
1057 pub fn as_float(&self) -> Option<f64> {
1058 match self {
1059 Self::Float(f) => Some(*f),
1060 _ => None,
1061 }
1062 }
1063
1064 #[inline]
1066 pub fn as_text(&self) -> Option<&str> {
1067 match self {
1068 Self::Text(s) => Some(s),
1069 _ => None,
1070 }
1071 }
1072
1073 #[inline]
1075 pub fn as_blob(&self) -> Option<&[u8]> {
1076 match self {
1077 Self::Blob(b) => Some(b),
1078 _ => None,
1079 }
1080 }
1081
1082 #[inline(always)]
1090 #[allow(clippy::inline_always)]
1091 #[allow(clippy::cast_possible_truncation)]
1092 pub fn to_integer(&self) -> i64 {
1093 match self {
1094 Self::Null => 0,
1095 Self::Integer(i) => *i,
1096 Self::Float(f) => *f as i64,
1097 Self::Text(s) => parse_integer_prefix(s),
1098 Self::Blob(b) => parse_integer_prefix_bytes(b),
1099 }
1100 }
1101
1102 #[inline(always)]
1110 #[allow(clippy::inline_always)]
1111 #[allow(clippy::cast_precision_loss)]
1112 pub fn to_float(&self) -> f64 {
1113 match self {
1114 Self::Null => 0.0,
1115 Self::Integer(i) => *i as f64,
1116 Self::Float(f) => *f,
1117 Self::Text(s) => parse_float_prefix(s),
1118 Self::Blob(b) => parse_float_prefix_bytes(b),
1119 }
1120 }
1121
1122 #[must_use]
1129 pub fn to_sum_numeric_value(&self) -> Self {
1130 match self {
1131 Self::Null => Self::Null,
1132 Self::Integer(i) => Self::Integer(*i),
1133 Self::Float(f) => Self::Float(*f),
1134 Self::Text(s) => {
1135 let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
1136 if let Ok(integer) = trimmed.parse::<i64>() {
1137 Self::Integer(integer)
1138 } else {
1139 Self::Float(parse_float_prefix(s))
1140 }
1141 }
1142 Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
1143 }
1144 }
1145
1146 #[inline]
1152 #[must_use]
1153 pub fn as_text_str(&self) -> Option<&str> {
1154 match self {
1155 Self::Text(s) => Some(s),
1156 _ => None,
1157 }
1158 }
1159
1160 #[inline]
1162 #[must_use]
1163 pub fn as_blob_bytes(&self) -> Option<&[u8]> {
1164 match self {
1165 Self::Blob(b) => Some(b),
1166 _ => None,
1167 }
1168 }
1169
1170 pub fn to_text(&self) -> String {
1177 match self {
1178 Self::Null => String::new(),
1179 Self::Integer(i) => i.to_string(),
1180 Self::Float(f) => format_sqlite_float(*f),
1181 Self::Text(s) => s.to_string(),
1182 Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
1183 }
1184 }
1185
1186 #[must_use]
1192 pub fn cast_to_numeric(&self) -> Self {
1193 match self {
1194 Self::Null => Self::Null,
1195 Self::Integer(i) => Self::Integer(*i),
1196 Self::Float(f) => Self::Float(*f),
1197 Self::Text(s) => cast_text_prefix_to_numeric(s),
1198 Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
1199 }
1200 }
1201
1202 pub const fn typeof_str(&self) -> &'static str {
1206 match self {
1207 Self::Null => "null",
1208 Self::Integer(_) => "integer",
1209 Self::Float(_) => "real",
1210 Self::Text(_) => "text",
1211 Self::Blob(_) => "blob",
1212 }
1213 }
1214
1215 pub fn sql_length(&self) -> Option<i64> {
1222 match self {
1223 Self::Null => None,
1224 Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
1225 Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
1226 Self::Integer(_) | Self::Float(_) => {
1227 let t = self.to_text();
1228 Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
1229 }
1230 }
1231 }
1232
1233 pub fn unique_eq(&self, other: &Self) -> bool {
1239 if self.is_null() || other.is_null() {
1240 return false;
1241 }
1242 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1243 }
1244
1245 fn float_result_or_null(result: f64) -> Self {
1249 if result.is_nan() {
1250 Self::Null
1251 } else {
1252 Self::Float(result)
1253 }
1254 }
1255
1256 #[inline]
1262 pub fn is_integer_numeric_type(&self) -> bool {
1263 fn text_is_integer_numeric_type(s: &str) -> bool {
1264 let trimmed = s.trim_start();
1265 let end = scan_numeric_prefix(trimmed.as_bytes());
1266 end > 0
1267 && !trimmed.as_bytes()[..end]
1268 .iter()
1269 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1270 }
1271
1272 match self {
1273 Self::Integer(_) => true,
1274 Self::Float(_) | Self::Null => false,
1275 Self::Text(s) => text_is_integer_numeric_type(s),
1276 Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1277 }
1278 }
1279
1280 #[inline]
1285 fn is_float_numeric_type(&self) -> bool {
1286 fn text_is_float(s: &str) -> bool {
1287 let trimmed = s.trim_start();
1288 let end = scan_numeric_prefix(trimmed.as_bytes());
1289 end > 0
1290 && trimmed.as_bytes()[..end]
1291 .iter()
1292 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1293 }
1294 match self {
1295 Self::Float(_) => true,
1296 Self::Integer(_) | Self::Null => false,
1297 Self::Text(s) => text_is_float(s),
1298 Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1299 }
1300 }
1301
1302 #[inline(always)]
1310 #[allow(clippy::inline_always)]
1311 #[must_use]
1312 #[allow(clippy::cast_precision_loss)]
1313 pub fn sql_add(&self, other: &Self) -> Self {
1314 match (self, other) {
1315 (Self::Null, _) | (_, Self::Null) => Self::Null,
1316 (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1317 Some(result) => Self::Integer(result),
1318 None => Self::float_result_or_null(*a as f64 + *b as f64),
1319 },
1320 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1324 let a = self.to_integer();
1325 let b = other.to_integer();
1326 match a.checked_add(b) {
1327 Some(result) => Self::Integer(result),
1328 None => Self::float_result_or_null(a as f64 + b as f64),
1329 }
1330 }
1331 _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1332 }
1333 }
1334
1335 #[inline(always)]
1339 #[allow(clippy::inline_always)]
1340 #[must_use]
1341 #[allow(clippy::cast_precision_loss)]
1342 pub fn sql_sub(&self, other: &Self) -> Self {
1343 match (self, other) {
1344 (Self::Null, _) | (_, Self::Null) => Self::Null,
1345 (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1346 Some(result) => Self::Integer(result),
1347 None => Self::float_result_or_null(*a as f64 - *b as f64),
1348 },
1349 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1350 let a = self.to_integer();
1351 let b = other.to_integer();
1352 match a.checked_sub(b) {
1353 Some(result) => Self::Integer(result),
1354 None => Self::float_result_or_null(a as f64 - b as f64),
1355 }
1356 }
1357 _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1358 }
1359 }
1360
1361 #[inline(always)]
1365 #[allow(clippy::inline_always)]
1366 #[must_use]
1367 #[allow(clippy::cast_precision_loss)]
1368 pub fn sql_mul(&self, other: &Self) -> Self {
1369 match (self, other) {
1370 (Self::Null, _) | (_, Self::Null) => Self::Null,
1371 (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1372 Some(result) => Self::Integer(result),
1373 None => Self::float_result_or_null(*a as f64 * *b as f64),
1374 },
1375 (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1376 (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1377 (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1378 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1379 let a = self.to_integer();
1380 let b = other.to_integer();
1381 match a.checked_mul(b) {
1382 Some(result) => Self::Integer(result),
1383 None => Self::float_result_or_null(a as f64 * b as f64),
1384 }
1385 }
1386 _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1387 }
1388 }
1389
1390 const fn sort_class(&self) -> u8 {
1392 match self {
1393 Self::Null => 0,
1394 Self::Integer(_) | Self::Float(_) => 1,
1395 Self::Text(_) => 2,
1396 Self::Blob(_) => 3,
1397 }
1398 }
1399}
1400
1401pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1409 assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1410 a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1411}
1412
1413pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1420 sql_like_cased(pattern, text, escape, false)
1421}
1422
1423#[must_use]
1431pub fn sql_like_cased(
1432 pattern: &str,
1433 text: &str,
1434 escape: Option<char>,
1435 case_sensitive: bool,
1436) -> bool {
1437 if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1438 return sql_like_fast_path_matches_cased(kind, literal, text, case_sensitive);
1439 }
1440
1441 sql_like_inner(
1442 &pattern.chars().collect::<Vec<_>>(),
1443 &text.chars().collect::<Vec<_>>(),
1444 escape,
1445 0,
1446 0,
1447 case_sensitive,
1448 )
1449}
1450
1451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1452pub enum SqlLikeFastPathKind {
1453 MatchAll,
1454 Exact,
1455 Prefix,
1456 Suffix,
1457 Contains,
1458}
1459
1460impl SqlLikeFastPathKind {
1461 #[must_use]
1462 pub const fn opcode_tag(self) -> i32 {
1463 match self {
1464 Self::MatchAll => 0,
1465 Self::Exact => 1,
1466 Self::Prefix => 2,
1467 Self::Suffix => 3,
1468 Self::Contains => 4,
1469 }
1470 }
1471
1472 #[must_use]
1473 pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1474 match tag {
1475 0 => Some(Self::MatchAll),
1476 1 => Some(Self::Exact),
1477 2 => Some(Self::Prefix),
1478 3 => Some(Self::Suffix),
1479 4 => Some(Self::Contains),
1480 _ => None,
1481 }
1482 }
1483}
1484
1485#[must_use]
1486pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1487 sql_like_fast_path_matches_cased(kind, literal, text, false)
1488}
1489
1490#[must_use]
1495pub fn sql_like_fast_path_matches_cased(
1496 kind: SqlLikeFastPathKind,
1497 literal: &str,
1498 text: &str,
1499 case_sensitive: bool,
1500) -> bool {
1501 match kind {
1502 SqlLikeFastPathKind::MatchAll => true,
1503 SqlLikeFastPathKind::Exact => {
1504 if case_sensitive {
1505 literal.as_bytes() == text.as_bytes()
1506 } else {
1507 ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes())
1508 }
1509 }
1510 SqlLikeFastPathKind::Prefix => {
1511 if case_sensitive {
1512 text.as_bytes().starts_with(literal.as_bytes())
1513 } else {
1514 ascii_ci_starts_with(text, literal)
1515 }
1516 }
1517 SqlLikeFastPathKind::Suffix => {
1518 if case_sensitive {
1519 text.as_bytes().ends_with(literal.as_bytes())
1520 } else {
1521 ascii_ci_ends_with(text, literal)
1522 }
1523 }
1524 SqlLikeFastPathKind::Contains => {
1525 if case_sensitive {
1526 literal.is_empty() || memmem::find(text.as_bytes(), literal.as_bytes()).is_some()
1527 } else {
1528 ascii_ci_contains(text, literal)
1529 }
1530 }
1531 }
1532}
1533
1534pub struct SqlLikeFastPathMatcher<'a> {
1536 kind: SqlLikeFastPathKind,
1537 literal: &'a str,
1538 contains_finder: Option<memmem::Finder<'a>>,
1539 case_sensitive: bool,
1540}
1541
1542impl<'a> SqlLikeFastPathMatcher<'a> {
1543 #[must_use]
1544 pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1545 Self::new_cased(kind, literal, false)
1546 }
1547
1548 #[must_use]
1550 pub fn new_cased(kind: SqlLikeFastPathKind, literal: &'a str, case_sensitive: bool) -> Self {
1551 let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1552 .then(|| memmem::Finder::new(literal.as_bytes()));
1553 Self {
1554 kind,
1555 literal,
1556 contains_finder,
1557 case_sensitive,
1558 }
1559 }
1560
1561 #[must_use]
1562 pub fn matches(&self, text: &str) -> bool {
1563 if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1564 let text_bytes = text.as_bytes();
1565 let needle_bytes = self.literal.as_bytes();
1566 if needle_bytes.len() > text_bytes.len() {
1567 return false;
1568 }
1569 if finder.find(text_bytes).is_some() {
1570 return true;
1571 }
1572 if self.case_sensitive {
1576 return false;
1577 }
1578 return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1579 }
1580 sql_like_fast_path_matches_cased(self.kind, self.literal, text, self.case_sensitive)
1581 }
1582}
1583
1584#[must_use]
1585pub fn classify_sql_like_fast_path(
1586 pattern: &str,
1587 escape: Option<char>,
1588) -> Option<(SqlLikeFastPathKind, &str)> {
1589 if escape.is_some() || pattern.contains('_') {
1590 return None;
1591 }
1592 if !pattern.contains('%') {
1593 return Some((SqlLikeFastPathKind::Exact, pattern));
1594 }
1595 if pattern.chars().all(|ch| ch == '%') {
1596 return Some((SqlLikeFastPathKind::MatchAll, ""));
1597 }
1598
1599 let trimmed_start = pattern.trim_start_matches('%');
1600 let trimmed_end = pattern.trim_end_matches('%');
1601 if pattern.starts_with('%') && pattern.ends_with('%') {
1602 let core = trimmed_start.trim_end_matches('%');
1603 if core.is_empty() {
1604 return Some((SqlLikeFastPathKind::MatchAll, ""));
1605 }
1606 if !core.contains('%') {
1607 return Some((SqlLikeFastPathKind::Contains, core));
1608 }
1609 }
1610 if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1611 {
1612 return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1613 }
1614 if !pattern.ends_with('%')
1615 && trimmed_start.len() < pattern.len()
1616 && !trimmed_start.contains('%')
1617 {
1618 return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1619 }
1620 None
1621}
1622
1623fn sql_like_inner(
1624 pattern: &[char],
1625 text: &[char],
1626 escape: Option<char>,
1627 pi: usize,
1628 ti: usize,
1629 case_sensitive: bool,
1630) -> bool {
1631 let mut pi = pi;
1632 let mut ti = ti;
1633
1634 while pi < pattern.len() {
1635 let pc = pattern[pi];
1636
1637 if Some(pc) == escape {
1639 pi += 1;
1640 if pi >= pattern.len() {
1641 return false; }
1643 if ti >= text.len() || !chars_eq(pattern[pi], text[ti], case_sensitive) {
1645 return false;
1646 }
1647 pi += 1;
1648 ti += 1;
1649 continue;
1650 }
1651
1652 match pc {
1653 '%' => {
1654 while pi < pattern.len() && pattern[pi] == '%' {
1656 pi += 1;
1657 }
1658 if pi >= pattern.len() {
1660 return true;
1661 }
1662 for start in ti..=text.len() {
1664 if sql_like_inner(pattern, text, escape, pi, start, case_sensitive) {
1665 return true;
1666 }
1667 }
1668 return false;
1669 }
1670 '_' => {
1671 if ti >= text.len() {
1672 return false;
1673 }
1674 pi += 1;
1675 ti += 1;
1676 }
1677 _ => {
1678 if ti >= text.len() || !chars_eq(pc, text[ti], case_sensitive) {
1679 return false;
1680 }
1681 pi += 1;
1682 ti += 1;
1683 }
1684 }
1685 }
1686 ti >= text.len()
1687}
1688
1689#[inline]
1692fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool {
1693 if case_sensitive {
1694 a == b
1695 } else {
1696 ascii_ci_eq(a, b)
1697 }
1698}
1699
1700fn ascii_ci_eq(a: char, b: char) -> bool {
1702 if a == b {
1703 return true;
1704 }
1705 a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1707}
1708
1709#[inline]
1710fn ascii_fold_byte(byte: u8) -> u8 {
1711 byte.to_ascii_lowercase()
1712}
1713
1714#[inline]
1715fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1716 left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1717}
1718
1719fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1720 if left.len() != right.len() {
1721 return false;
1722 }
1723 let mut idx = 0;
1724 while idx < left.len() {
1725 if !ascii_ci_eq_byte(left[idx], right[idx]) {
1726 return false;
1727 }
1728 idx += 1;
1729 }
1730 true
1731}
1732
1733fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1734 let text = text.as_bytes();
1735 let prefix = prefix.as_bytes();
1736 text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1737}
1738
1739fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1740 let text = text.as_bytes();
1741 let suffix = suffix.as_bytes();
1742 text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1743}
1744
1745fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1746 let text = text.as_bytes();
1747 let needle = needle.as_bytes();
1748 if needle.is_empty() {
1749 return true;
1750 }
1751 if needle.len() > text.len() {
1752 return false;
1753 }
1754 if memmem::find(text, needle).is_some() {
1755 return true;
1756 }
1757
1758 ascii_ci_contains_folded_scan(text, needle)
1759}
1760
1761fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1762 if needle.is_empty() {
1763 return true;
1764 }
1765 if needle.len() > text.len() {
1766 return false;
1767 }
1768 let max_start = text.len() - needle.len();
1769 let first = needle[0];
1770 let first_folded = ascii_fold_byte(first);
1771 let first_alt = if first.is_ascii_alphabetic() {
1772 first_folded.to_ascii_uppercase()
1773 } else {
1774 first_folded
1775 };
1776 let mut start = 0;
1777 while start <= max_start {
1778 let rel = if first_folded == first_alt {
1779 memchr(first_folded, &text[start..=max_start])
1780 } else {
1781 memchr2(first_folded, first_alt, &text[start..=max_start])
1782 };
1783 let Some(rel) = rel else {
1784 break;
1785 };
1786 start += rel;
1787 if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1788 return true;
1789 }
1790 start += 1;
1791 }
1792 false
1793}
1794
1795#[derive(Debug, Clone)]
1802pub struct SumAccumulator {
1803 int_sum: i64,
1805 float_sum: f64,
1808 float_err: f64,
1810 has_value: bool,
1812 is_float: bool,
1814 overflow: bool,
1816}
1817
1818impl Default for SumAccumulator {
1819 fn default() -> Self {
1820 Self::new()
1821 }
1822}
1823
1824#[inline]
1827fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1828 let s = *sum;
1829 let t = s + value;
1830 if s.abs() > value.abs() {
1831 *err += (s - t) + value;
1832 } else {
1833 *err += (value - t) + s;
1834 }
1835 *sum = t;
1836}
1837
1838impl SumAccumulator {
1839 pub const fn new() -> Self {
1841 Self {
1842 int_sum: 0,
1843 float_sum: 0.0,
1844 float_err: 0.0,
1845 has_value: false,
1846 is_float: false,
1847 overflow: false,
1848 }
1849 }
1850
1851 #[allow(clippy::cast_precision_loss)]
1853 pub fn accumulate(&mut self, val: &SqliteValue) {
1854 match val.to_sum_numeric_value() {
1855 SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1856 SqliteValue::Integer(i) => {
1857 self.has_value = true;
1858 if !self.is_float && !self.overflow {
1859 match self.int_sum.checked_add(i) {
1860 Some(result) => self.int_sum = result,
1861 None => self.overflow = true,
1862 }
1863 }
1864 kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1865 }
1866 SqliteValue::Float(f) => {
1867 self.has_value = true;
1868 self.is_float = true;
1869 kbn_step(&mut self.float_sum, &mut self.float_err, f);
1870 }
1871 }
1872 }
1873
1874 pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1878 if !self.is_float && self.overflow {
1879 return Err(SumOverflowError);
1880 }
1881 if !self.has_value {
1882 return Ok(SqliteValue::Null);
1883 }
1884 if self.is_float {
1885 Ok(SqliteValue::Float(self.float_sum + self.float_err))
1886 } else {
1887 Ok(SqliteValue::Integer(self.int_sum))
1888 }
1889 }
1890}
1891
1892#[derive(Debug, Clone, PartialEq, Eq)]
1894pub struct SumOverflowError;
1895
1896impl fmt::Display for SumOverflowError {
1897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1898 f.write_str("integer overflow in sum()")
1899 }
1900}
1901
1902impl fmt::Display for SqliteValue {
1903 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1904 match self {
1905 Self::Null => f.write_str("NULL"),
1906 Self::Integer(i) => write!(f, "{i}"),
1907 Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1908 Self::Text(s) => write!(f, "'{s}'"),
1909 Self::Blob(b) => {
1910 f.write_str("X'")?;
1911 for byte in b.iter() {
1912 write!(f, "{byte:02X}")?;
1913 }
1914 f.write_str("'")
1915 }
1916 }
1917 }
1918}
1919
1920impl PartialEq for SqliteValue {
1921 fn eq(&self, other: &Self) -> bool {
1922 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1923 }
1924}
1925
1926impl Eq for SqliteValue {}
1927
1928impl PartialOrd for SqliteValue {
1929 #[inline]
1930 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1931 Some(self.cmp(other))
1932 }
1933}
1934
1935impl Ord for SqliteValue {
1936 #[inline]
1937 fn cmp(&self, other: &Self) -> Ordering {
1938 let class_a = self.sort_class();
1940 let class_b = other.sort_class();
1941
1942 if class_a != class_b {
1943 return class_a.cmp(&class_b);
1944 }
1945
1946 match (self, other) {
1947 (Self::Null, Self::Null) => Ordering::Equal,
1948 (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
1949 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
1950 (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
1951 (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
1952 (Self::Text(a), Self::Text(b)) => a.cmp(b),
1953 (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
1954 _ => unreachable!(),
1955 }
1956 }
1957}
1958
1959impl From<i64> for SqliteValue {
1960 fn from(i: i64) -> Self {
1961 Self::Integer(i)
1962 }
1963}
1964
1965impl From<i32> for SqliteValue {
1966 fn from(i: i32) -> Self {
1967 Self::Integer(i64::from(i))
1968 }
1969}
1970
1971impl From<f64> for SqliteValue {
1972 fn from(f: f64) -> Self {
1973 Self::float_result_or_null(f)
1974 }
1975}
1976
1977impl From<String> for SqliteValue {
1978 fn from(s: String) -> Self {
1979 Self::Text(SmallText::from_string(s))
1982 }
1983}
1984
1985impl From<&str> for SqliteValue {
1986 fn from(s: &str) -> Self {
1987 Self::Text(SmallText::new(s))
1988 }
1989}
1990
1991impl From<Arc<str>> for SqliteValue {
1992 fn from(s: Arc<str>) -> Self {
1993 Self::Text(SmallText::from_arc(s))
1994 }
1995}
1996
1997impl From<Vec<u8>> for SqliteValue {
1998 fn from(b: Vec<u8>) -> Self {
1999 Self::Blob(Arc::from(b))
2002 }
2003}
2004
2005impl From<&[u8]> for SqliteValue {
2006 fn from(b: &[u8]) -> Self {
2007 Self::Blob(Arc::from(b))
2008 }
2009}
2010
2011impl From<Arc<[u8]>> for SqliteValue {
2012 fn from(b: Arc<[u8]>) -> Self {
2013 Self::Blob(b)
2014 }
2015}
2016
2017impl<T: Into<Self>> From<Option<T>> for SqliteValue {
2018 fn from(opt: Option<T>) -> Self {
2019 match opt {
2020 Some(v) => v.into(),
2021 None => Self::Null,
2022 }
2023 }
2024}
2025
2026#[allow(
2030 clippy::cast_possible_truncation,
2031 clippy::cast_precision_loss,
2032 clippy::float_cmp
2033)]
2034fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
2035 let trimmed = trim_sqlite_ascii_whitespace(s);
2036 if trimmed.is_empty() {
2037 return None;
2038 }
2039 if let Ok(i) = trimmed.parse::<i64>() {
2041 return Some(SqliteValue::Integer(i));
2042 }
2043 if let Ok(f) = trimmed.parse::<f64>() {
2047 if !f.is_finite() {
2048 let lower = trimmed.to_ascii_lowercase();
2049 if lower.contains("inf") || lower.contains("nan") {
2050 return None;
2051 }
2052 }
2053 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
2056 #[allow(clippy::cast_possible_truncation)]
2057 let i = f as i64;
2058 #[allow(clippy::cast_precision_loss)]
2059 if (i as f64) == f {
2060 return Some(SqliteValue::Integer(i));
2061 }
2062 }
2063 return Some(SqliteValue::Float(f));
2064 }
2065 None
2066}
2067
2068#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
2073pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
2074 if r.is_nan() {
2075 return Ordering::Greater;
2077 }
2078 if r < -9_223_372_036_854_775_808.0 {
2080 return Ordering::Greater;
2081 }
2082 if r >= 9_223_372_036_854_775_808.0 {
2083 return Ordering::Less;
2084 }
2085 let y = r as i64;
2087 match i.cmp(&y) {
2088 Ordering::Less => Ordering::Less,
2089 Ordering::Greater => Ordering::Greater,
2090 Ordering::Equal => {
2092 let s = i as f64;
2093 s.partial_cmp(&r).unwrap_or(Ordering::Equal)
2094 }
2095 }
2096}
2097
2098#[must_use]
2105pub fn format_sqlite_float(f: f64) -> String {
2106 if f.is_nan() {
2107 return "NaN".to_owned();
2108 }
2109 if f.is_infinite() {
2110 return if f.is_sign_positive() {
2111 "Inf".to_owned()
2112 } else {
2113 "-Inf".to_owned()
2114 };
2115 }
2116 render_sqlite_float_decode(&sqlite_float_decode(f))
2117}
2118
2119const SQLITE_FLOAT_SIGNIFICANT_DIGITS: usize = 17;
2120const SQLITE_FLOAT_MAX_ROUND_DIGITS: usize = 20;
2121const SQLITE_FLOAT_GENERIC_PRECISION: i32 = 16;
2122const SQLITE_POWERS_OF_TEN_FIRST: i32 = -348;
2123const SQLITE_POWERS_OF_TEN_LAST: i32 = 347;
2124
2125#[derive(Debug)]
2126struct SqliteFloatDecode {
2127 digits: Vec<u8>,
2128 decimal_point: i32,
2129 negative: bool,
2130}
2131
2132fn render_sqlite_float_decode(decoded: &SqliteFloatDecode) -> String {
2133 let exponent = decoded.decimal_point - 1;
2134 if !(-4..=SQLITE_FLOAT_GENERIC_PRECISION).contains(&exponent) {
2135 return render_sqlite_float_exponential(decoded, exponent);
2136 }
2137 render_sqlite_float_fixed(decoded, exponent)
2138}
2139
2140fn render_sqlite_float_fixed(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2141 let mut out = String::with_capacity(decoded.digits.len() + 8);
2142 if decoded.negative {
2143 out.push('-');
2144 }
2145
2146 let mut precision = SQLITE_FLOAT_GENERIC_PRECISION - exponent;
2147 let mut digit_idx = 0usize;
2148 let mut e2 = decoded.decimal_point - 1;
2149
2150 if e2 < 0 {
2151 out.push('0');
2152 } else {
2153 while e2 >= 0 {
2154 if let Some(&digit) = decoded.digits.get(digit_idx) {
2155 out.push(char::from(digit));
2156 digit_idx += 1;
2157 } else {
2158 out.push('0');
2159 }
2160 e2 -= 1;
2161 }
2162 }
2163
2164 out.push('.');
2165
2166 if e2 < -1 && precision > 0 {
2167 let zero_count = (-1 - e2).min(precision);
2168 for _ in 0..zero_count {
2169 out.push('0');
2170 }
2171 precision -= zero_count;
2172 }
2173
2174 if precision > 0 {
2175 let digits_after_decimal =
2176 (decoded.digits.len().saturating_sub(digit_idx)).min(precision as usize);
2177 for &digit in &decoded.digits[digit_idx..digit_idx + digits_after_decimal] {
2178 out.push(char::from(digit));
2179 }
2180 }
2181
2182 trim_sqlite_float_tail(&mut out);
2183 out
2184}
2185
2186fn render_sqlite_float_exponential(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2187 let mut out = String::with_capacity(decoded.digits.len() + 8);
2188 if decoded.negative {
2189 out.push('-');
2190 }
2191
2192 let first = decoded.digits.first().copied().unwrap_or(b'0');
2193 out.push(char::from(first));
2194 out.push('.');
2195 let digits_after_decimal =
2196 (decoded.digits.len().saturating_sub(1)).min(SQLITE_FLOAT_GENERIC_PRECISION as usize);
2197 for &digit in decoded.digits.iter().skip(1).take(digits_after_decimal) {
2198 out.push(char::from(digit));
2199 }
2200 trim_sqlite_float_tail(&mut out);
2201
2202 out.push('e');
2203 let mut abs_exp = exponent;
2204 if abs_exp < 0 {
2205 out.push('-');
2206 abs_exp = -abs_exp;
2207 } else {
2208 out.push('+');
2209 }
2210 if abs_exp >= 100 {
2211 out.push(char::from(b'0' + (abs_exp / 100) as u8));
2212 abs_exp %= 100;
2213 }
2214 out.push(char::from(b'0' + (abs_exp / 10) as u8));
2215 out.push(char::from(b'0' + (abs_exp % 10) as u8));
2216 out
2217}
2218
2219fn trim_sqlite_float_tail(out: &mut String) {
2220 while out.ends_with('0') {
2221 out.pop();
2222 }
2223 if out.ends_with('.') {
2224 out.push('0');
2225 }
2226}
2227
2228fn sqlite_float_decode(f: f64) -> SqliteFloatDecode {
2229 let negative = f < 0.0;
2230 let r = if negative { -f } else { f };
2231 if r == 0.0 {
2232 return SqliteFloatDecode {
2233 digits: vec![b'0'],
2234 decimal_point: 1,
2235 negative: false,
2236 };
2237 }
2238
2239 let bits = r.to_bits();
2240 let raw_exponent = ((bits >> 52) & 0x7ff) as i32;
2241 let mut mantissa = bits & 0x000f_ffff_ffff_ffff;
2242 let binary_exponent = if raw_exponent == 0 {
2243 let leading = mantissa.leading_zeros();
2244 mantissa <<= leading;
2245 -1074 - leading as i32
2246 } else {
2247 mantissa = (mantissa << 11) | (1_u64 << 63);
2248 raw_exponent - 1086
2249 };
2250
2251 let (decimal, decimal_exponent) = sqlite_fp2_convert10(mantissa, binary_exponent, 18);
2252 let mut digits = decimal.to_string().into_bytes();
2253 let mut digit_count = digits.len();
2254 let mut decimal_point = digit_count as i32 + decimal_exponent;
2255 let mut round_at = SQLITE_FLOAT_SIGNIFICANT_DIGITS;
2256
2257 if round_at < digit_count || digit_count > SQLITE_FLOAT_MAX_ROUND_DIGITS {
2258 if round_at == SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2259 round_at = sqlite_adjust_17_digit_rounding(
2260 r,
2261 &digits,
2262 decimal_exponent,
2263 digit_count,
2264 decimal_point,
2265 round_at,
2266 );
2267 }
2268 if digits.get(round_at).copied().unwrap_or(b'0') >= b'5' {
2269 let mut idx = round_at - 1;
2270 loop {
2271 digits[idx] += 1;
2272 if digits[idx] <= b'9' {
2273 break;
2274 }
2275 digits[idx] = b'0';
2276 if idx == 0 {
2277 digits.insert(0, b'1');
2278 round_at += 1;
2279 decimal_point += 1;
2280 break;
2281 }
2282 idx -= 1;
2283 }
2284 }
2285 digit_count = round_at;
2286 digits.truncate(digit_count);
2287 }
2288
2289 while digit_count > 1 && digits[digit_count - 1] == b'0' {
2290 digit_count -= 1;
2291 }
2292 digits.truncate(digit_count);
2293
2294 SqliteFloatDecode {
2295 digits,
2296 decimal_point,
2297 negative,
2298 }
2299}
2300
2301fn sqlite_adjust_17_digit_rounding(
2302 r: f64,
2303 digits: &[u8],
2304 decimal_exponent: i32,
2305 digit_count: usize,
2306 decimal_point: i32,
2307 round_at: usize,
2308) -> usize {
2309 if digits.len() <= SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2310 return round_at;
2311 }
2312
2313 if digits[15] == b'9' && digits[14] == b'9' {
2314 let mut keep = 14usize;
2315 while keep > 0 && digits[keep - 1] == b'9' {
2316 keep -= 1;
2317 }
2318 let candidate = if keep == 0 {
2319 1
2320 } else {
2321 decimal_digits_to_u64(&digits[..keep]) + 1
2322 };
2323 if r == sqlite_fp10_convert2(
2324 candidate,
2325 decimal_exponent + digit_count as i32 - keep as i32,
2326 ) {
2327 return keep + 1;
2328 }
2329 } else if decimal_point >= digit_count as i32
2330 || (digits[15] == b'0' && digits[14] == b'0' && digits[13] == b'0')
2331 {
2332 let mut keep = 13usize;
2333 while keep > 0 && digits[keep - 1] == b'0' {
2334 keep -= 1;
2335 }
2336 if keep > 0 {
2337 let candidate = decimal_digits_to_u64(&digits[..keep]);
2338 if r == sqlite_fp10_convert2(
2339 candidate,
2340 decimal_exponent + digit_count as i32 - keep as i32,
2341 ) {
2342 return keep + 1;
2343 }
2344 }
2345 }
2346
2347 round_at
2348}
2349
2350fn decimal_digits_to_u64(digits: &[u8]) -> u64 {
2351 digits
2352 .iter()
2353 .fold(0_u64, |acc, digit| acc * 10 + u64::from(*digit - b'0'))
2354}
2355
2356fn sqlite_fp2_convert10(mantissa: u64, binary_exponent: i32, digits: i32) -> (u64, i32) {
2357 let power = digits - 1 - pwr2_to_10(binary_exponent + 63);
2358 let (power_hi, power_lo) = power_of_ten(power);
2359 let (mut high, _) = sqlite_multiply_128(mantissa, power_hi);
2360 let _ = power_lo;
2361 if digits == 18 {
2362 high >>= -(binary_exponent + pwr10_to_2(power) + 2) as u32;
2363 (high.wrapping_add((high << 1) & 2) >> 1, -power)
2364 } else {
2365 high >>= -(binary_exponent + pwr10_to_2(power) + 1) as u32;
2366 (high, -power)
2367 }
2368}
2369
2370fn sqlite_fp10_convert2(decimal: u64, power: i32) -> f64 {
2371 if power < SQLITE_POWERS_OF_TEN_FIRST {
2372 return 0.0;
2373 }
2374 if power > SQLITE_POWERS_OF_TEN_LAST {
2375 return f64::INFINITY;
2376 }
2377
2378 let bit_width = 64 - decimal.leading_zeros() as i32;
2379 let binary_power = pwr10_to_2(power);
2380 let mut exponent = 53 - bit_width - binary_power;
2381 if exponent > 1074 {
2382 if exponent >= 1130 {
2383 return 0.0;
2384 }
2385 exponent = 1074;
2386 }
2387
2388 let shift = -(exponent - (64 - bit_width) + binary_power + 3);
2389 let shift = shift.clamp(0, 63) as u32;
2390 let (mut power_hi, mut power_lo) = power_of_ten(power);
2391 if power_lo != 0 {
2392 power_hi = power_hi.wrapping_add(1);
2393 power_lo = !power_lo;
2394 }
2395
2396 let shifted_decimal = decimal << (64 - bit_width);
2397 let (mut high, low) = sqlite_multiply_128(shifted_decimal, power_hi);
2398 let mid1 = (low >> 32) as u32;
2399 let mut sticky = 1_u64;
2400 if (high & low_mask(shift)) == 0 {
2401 let (mid2_high, _) = sqlite_multiply_128(shifted_decimal, u64::from(power_lo) << 32);
2402 let mid2 = (mid2_high >> 32) as u32;
2403 sticky = u64::from(mid1.wrapping_sub(mid2) > 1);
2404 high = high.wrapping_sub(u64::from(mid1 < mid2));
2405 }
2406
2407 let mut rounded = (high >> shift) | sticky;
2408 let adjust = u32::from(rounded >= (1_u64 << 55) - 2);
2409 if adjust != 0 {
2410 rounded = (rounded >> adjust) | (rounded & 1);
2411 exponent -= adjust as i32;
2412 }
2413
2414 let mut bits = (rounded + 1 + ((rounded >> 2) & 1)) >> 2;
2415 if exponent <= -972 {
2416 return f64::INFINITY;
2417 }
2418 if (bits & (1_u64 << 52)) != 0 {
2419 bits = (bits & !(1_u64 << 52)) | ((1075 - exponent) as u64) << 52;
2420 }
2421 f64::from_bits(bits)
2422}
2423
2424fn low_mask(bits: u32) -> u64 {
2425 if bits == 0 { 0 } else { (1_u64 << bits) - 1 }
2426}
2427
2428fn sqlite_multiply_128(left: u64, right: u64) -> (u64, u64) {
2429 let product = u128::from(left) * u128::from(right);
2430 ((product >> 64) as u64, product as u64)
2431}
2432
2433fn sqlite_multiply_160(high: u64, low: u32, right: u64) -> (u64, u32) {
2434 let product =
2435 u128::from(high) * u128::from(right) + ((u128::from(low) * u128::from(right)) >> 32);
2436 (
2437 (product >> 64) as u64,
2438 ((product >> 32) & u128::from(u32::MAX)) as u32,
2439 )
2440}
2441
2442fn pwr10_to_2(power: i32) -> i32 {
2443 (power * 108_853) >> 15
2444}
2445
2446fn pwr2_to_10(power: i32) -> i32 {
2447 (power * 78_913) >> 18
2448}
2449
2450fn power_of_ten(power: i32) -> (u64, u32) {
2451 const BASE: [u64; 27] = [
2452 0x8000_0000_0000_0000,
2453 0xa000_0000_0000_0000,
2454 0xc800_0000_0000_0000,
2455 0xfa00_0000_0000_0000,
2456 0x9c40_0000_0000_0000,
2457 0xc350_0000_0000_0000,
2458 0xf424_0000_0000_0000,
2459 0x9896_8000_0000_0000,
2460 0xbebc_2000_0000_0000,
2461 0xee6b_2800_0000_0000,
2462 0x9502_f900_0000_0000,
2463 0xba43_b740_0000_0000,
2464 0xe8d4_a510_0000_0000,
2465 0x9184_e72a_0000_0000,
2466 0xb5e6_20f4_8000_0000,
2467 0xe35f_a931_a000_0000,
2468 0x8e1b_c9bf_0400_0000,
2469 0xb1a2_bc2e_c500_0000,
2470 0xde0b_6b3a_7640_0000,
2471 0x8ac7_2304_89e8_0000,
2472 0xad78_ebc5_ac62_0000,
2473 0xd8d7_26b7_177a_8000,
2474 0x8786_7832_6eac_9000,
2475 0xa968_163f_0a57_b400,
2476 0xd3c2_1bce_cced_a100,
2477 0x8459_5161_4014_84a0,
2478 0xa56f_a5b9_9019_a5c8,
2479 ];
2480 const SCALE: [u64; 26] = [
2481 0x8049_a4ac_0c58_11ae,
2482 0xcf42_894a_5dce_35ea,
2483 0xa76c_5823_38ed_2621,
2484 0x873e_4f75_e222_4e68,
2485 0xda7f_5bf5_9096_6848,
2486 0xb080_392c_c434_9dec,
2487 0x8e93_8662_882a_f53e,
2488 0xe658_29b3_046b_0afa,
2489 0xba12_1a46_50e4_ddeb,
2490 0x964e_858c_91ba_2655,
2491 0xf2d5_6790_ab41_c2a2,
2492 0xc428_d05a_a475_1e4c,
2493 0x9e74_d1b7_91e0_7e48,
2494 0xcccc_cccc_cccc_cccc,
2495 0xcecb_8f27_f420_0f3a,
2496 0xa70c_3c40_a64e_6c51,
2497 0x86f0_ac99_b4e8_dafd,
2498 0xda01_ee64_1a70_8de9,
2499 0xb01a_e745_b101_e9e4,
2500 0x8e41_ade9_fbeb_c27d,
2501 0xe5d3_ef28_2a24_2e81,
2502 0xb9a7_4a06_37ce_2ee1,
2503 0x95f8_3d0a_1fb6_9cd9,
2504 0xf24a_01a7_3cf2_dccf,
2505 0xc3b8_3581_09e8_4f07,
2506 0x9e19_db92_b4e3_1ba9,
2507 ];
2508 const SCALE_LO: [u32; 26] = [
2509 0x205b_896d,
2510 0x5206_4cad,
2511 0xaf2a_f2b8,
2512 0x5a77_44a7,
2513 0xaf39_a475,
2514 0xbd8d_794e,
2515 0x547e_b47b,
2516 0x0cb4_a5a3,
2517 0x92f3_4d62,
2518 0x3a6a_07f9,
2519 0xfae2_7299,
2520 0xaa97_e14c,
2521 0x775e_a265,
2522 0xcccc_cccc,
2523 0x0000_0000,
2524 0x9990_90b6,
2525 0x69a0_28bb,
2526 0xe80e_6f48,
2527 0x5ec0_5dd0,
2528 0x1458_8f14,
2529 0x8f16_68c9,
2530 0x6d95_3e2c,
2531 0x4abd_af10,
2532 0xbc63_3b39,
2533 0x0a86_2f81,
2534 0x6c07_a2c2,
2535 ];
2536
2537 debug_assert!((SQLITE_POWERS_OF_TEN_FIRST..=SQLITE_POWERS_OF_TEN_LAST).contains(&power));
2538
2539 let (group, offset) = if power < 0 {
2540 if power == -1 {
2541 return (SCALE[13], SCALE_LO[13]);
2542 }
2543 let mut group = power / 27;
2544 let mut offset = power % 27;
2545 if offset != 0 {
2546 group -= 1;
2547 offset += 27;
2548 }
2549 (group, offset)
2550 } else if power < 27 {
2551 return (BASE[power as usize], 0);
2552 } else {
2553 (power / 27, power % 27)
2554 };
2555
2556 let scale_idx = (group + 13) as usize;
2557 let mut high = SCALE[scale_idx];
2558 if offset == 0 {
2559 return (high, SCALE_LO[scale_idx]);
2560 }
2561
2562 let (scaled, mut low) = sqlite_multiply_160(high, SCALE_LO[scale_idx], BASE[offset as usize]);
2563 high = scaled;
2564 if (high & (1_u64 << 63)) == 0 {
2565 high = (high << 1) | u64::from(low >> 31);
2566 low = (low << 1) | 1;
2567 }
2568 (high, low)
2569}
2570
2571#[cfg(test)]
2572#[allow(clippy::float_cmp, clippy::approx_constant)]
2573mod tests {
2574 use super::*;
2575
2576 struct ValuePoolTestGuard;
2577
2578 impl ValuePoolTestGuard {
2579 fn new() -> Self {
2580 pool_clear();
2581 reset_value_pool_test_stats();
2582 Self
2583 }
2584 }
2585
2586 impl Drop for ValuePoolTestGuard {
2587 fn drop(&mut self) {
2588 pool_clear();
2589 reset_value_pool_test_stats();
2590 }
2591 }
2592
2593 fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
2594 let stats = value_pool_test_stats_snapshot();
2595 eprintln!(
2596 "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
2597 stats.slab_alloc_count,
2598 stats.slab_return_count,
2599 stats.global_alloc_fallback_count,
2600 stats.slab_high_water_mark,
2601 pool_len(),
2602 );
2603 stats
2604 }
2605
2606 fn utf16_record_bytes(text: &str, little_endian: bool) -> Vec<u8> {
2607 text.encode_utf16()
2608 .flat_map(|unit| {
2609 if little_endian {
2610 unit.to_le_bytes()
2611 } else {
2612 unit.to_be_bytes()
2613 }
2614 })
2615 .collect()
2616 }
2617
2618 #[test]
2619 fn from_record_text_bytes_utf8_passthrough() {
2620 assert_eq!(
2621 SmallText::from_record_text_bytes(b"table", TextEncoding::Utf8).as_str(),
2622 "table"
2623 );
2624 assert_eq!(
2625 SmallText::from_record_text_bytes(b"", TextEncoding::Utf8).as_str(),
2626 ""
2627 );
2628 }
2629
2630 #[test]
2631 fn from_record_text_bytes_utf16_le_and_be_round_trip() {
2632 for text in ["table", "café", "日本語", "😀 grin", ""] {
2633 let le = SmallText::from_record_text_bytes(
2634 &utf16_record_bytes(text, true),
2635 TextEncoding::Utf16le,
2636 );
2637 assert_eq!(le.as_str(), text, "utf16le decode of {text:?}");
2638 let be = SmallText::from_record_text_bytes(
2639 &utf16_record_bytes(text, false),
2640 TextEncoding::Utf16be,
2641 );
2642 assert_eq!(be.as_str(), text, "utf16be decode of {text:?}");
2643 }
2644 }
2645
2646 #[test]
2647 fn from_record_text_bytes_utf16_sqlite_master_ascii_case() {
2648 let bytes = utf16_record_bytes("table", true);
2652 assert_eq!(bytes, vec![b't', 0, b'a', 0, b'b', 0, b'l', 0, b'e', 0]);
2653 assert_eq!(
2654 SmallText::from_record_text_bytes(&bytes, TextEncoding::Utf16le).as_str(),
2655 "table"
2656 );
2657 assert_ne!(SmallText::from_bytes(&bytes).as_str(), "table");
2659 }
2660
2661 #[test]
2662 fn from_record_text_bytes_utf16_lone_surrogate_becomes_replacement() {
2663 let bytes = 0xD800_u16.to_le_bytes().to_vec();
2666 assert_eq!(
2667 SmallText::from_record_text_bytes(&bytes, TextEncoding::Utf16le).as_str(),
2668 "\u{FFFD}"
2669 );
2670 }
2671
2672 #[test]
2673 fn utf16_decoded_text_compares_by_code_point_regardless_of_storage_encoding() {
2674 let samples = [
2683 "", "A", "Apple", "apple", "banana", "café", "cafz", "z", "Καλημέρα", "日本", "日本語",
2684 "😀", "😀grin",
2685 ];
2686 for a in samples {
2687 for b in samples {
2688 let expected = SmallText::new(a).cmp(&SmallText::new(b));
2689 for (enc, le) in [
2690 (TextEncoding::Utf16le, true),
2691 (TextEncoding::Utf16be, false),
2692 ] {
2693 let da = SmallText::from_record_text_bytes(&utf16_record_bytes(a, le), enc);
2694 let db = SmallText::from_record_text_bytes(&utf16_record_bytes(b, le), enc);
2695 assert_eq!(
2696 da.cmp(&db),
2697 expected,
2698 "bd-bld9w.4 {enc:?}: cmp({a:?}, {b:?}) must equal code-point order"
2699 );
2700 assert_eq!(
2703 da.cmp(&SmallText::new(a)),
2704 std::cmp::Ordering::Equal,
2705 "bd-bld9w.4 {enc:?}: decoded {a:?} must equal its UTF-8 form"
2706 );
2707 }
2708 }
2709 }
2710 }
2711
2712 #[test]
2713 fn to_record_text_bytes_round_trips_from_record_text_bytes() {
2714 for text in ["", "table", "café", "日本語 mix", "😀 grin 🎉"] {
2715 for encoding in [
2716 TextEncoding::Utf8,
2717 TextEncoding::Utf16le,
2718 TextEncoding::Utf16be,
2719 ] {
2720 let value = SmallText::new(text);
2721 let encoded = value.to_record_text_bytes(encoding);
2722 let decoded = SmallText::from_record_text_bytes(&encoded, encoding);
2723 assert_eq!(decoded.as_str(), text, "round-trip {text:?} via {encoding:?}");
2724 }
2725 }
2726 let value = SmallText::new("borrow me");
2728 assert!(matches!(
2729 value.to_record_text_bytes(TextEncoding::Utf8),
2730 Cow::Borrowed(_)
2731 ));
2732 let table = SmallText::new("table");
2734 let le = table.to_record_text_bytes(TextEncoding::Utf16le);
2735 assert_eq!(&*le, &[b't', 0, b'a', 0, b'b', 0, b'l', 0, b'e', 0][..]);
2736 }
2737
2738 #[test]
2739 fn test_slab_basic_alloc_dealloc() {
2740 let _guard = ValuePoolTestGuard::new();
2741 const ROUND_TRIP_COUNT: usize = 100;
2742
2743 assert_eq!(pool_len(), 0);
2744 assert_eq!(pool_acquire(), None);
2745 assert_eq!(
2746 value_pool_test_stats_snapshot(),
2747 ValuePoolStats {
2748 slab_alloc_count: 0,
2749 slab_return_count: 0,
2750 global_alloc_fallback_count: 1,
2751 slab_high_water_mark: 0,
2752 }
2753 );
2754
2755 reset_value_pool_test_stats();
2756 for value in 0..ROUND_TRIP_COUNT {
2757 pool_return(SqliteValue::Integer(value as i64));
2758 }
2759 assert_eq!(pool_len(), ROUND_TRIP_COUNT);
2760 assert_eq!(
2761 value_pool_test_stats_snapshot(),
2762 ValuePoolStats {
2763 slab_alloc_count: 0,
2764 slab_return_count: ROUND_TRIP_COUNT,
2765 global_alloc_fallback_count: 0,
2766 slab_high_water_mark: ROUND_TRIP_COUNT,
2767 }
2768 );
2769
2770 reset_value_pool_test_stats();
2771 for expected in (0..ROUND_TRIP_COUNT).rev() {
2772 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
2773 }
2774 assert_eq!(pool_len(), 0);
2775 assert_eq!(
2776 log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
2777 ValuePoolStats {
2778 slab_alloc_count: ROUND_TRIP_COUNT,
2779 slab_return_count: 0,
2780 global_alloc_fallback_count: 0,
2781 slab_high_water_mark: 0,
2782 }
2783 );
2784 }
2785
2786 #[test]
2787 fn test_slab_exhaustion_fallback() {
2788 let _guard = ValuePoolTestGuard::new();
2789
2790 for value in 0..=VALUE_POOL_CAP {
2791 pool_return(SqliteValue::Integer(value as i64));
2792 }
2793 assert_eq!(pool_len(), VALUE_POOL_CAP);
2794 assert_eq!(
2795 value_pool_test_stats_snapshot(),
2796 ValuePoolStats {
2797 slab_alloc_count: 0,
2798 slab_return_count: VALUE_POOL_CAP,
2799 global_alloc_fallback_count: 0,
2800 slab_high_water_mark: VALUE_POOL_CAP,
2801 }
2802 );
2803
2804 reset_value_pool_test_stats();
2805 for _ in 0..VALUE_POOL_CAP {
2806 assert!(pool_acquire().is_some());
2807 }
2808 assert_eq!(pool_acquire(), None);
2809 assert_eq!(pool_len(), 0);
2810 assert_eq!(
2811 log_value_pool_test_stats("test_slab_exhaustion_fallback"),
2812 ValuePoolStats {
2813 slab_alloc_count: VALUE_POOL_CAP,
2814 slab_return_count: 0,
2815 global_alloc_fallback_count: 1,
2816 slab_high_water_mark: 0,
2817 }
2818 );
2819 }
2820
2821 #[test]
2822 fn test_slab_no_leak() {
2823 let _guard = ValuePoolTestGuard::new();
2824 const ITERATIONS: usize = 10_000;
2825
2826 let (weak_tx, weak_rx) = std::sync::mpsc::channel();
2827 let (release_tx, release_rx) = std::sync::mpsc::channel();
2828
2829 let worker = std::thread::spawn(move || {
2830 pool_clear();
2831 reset_value_pool_test_stats();
2832
2833 let mut pooled_weak = None;
2834 let mut overflow_weak = None;
2835 for value in 0..ITERATIONS {
2836 let payload: Arc<[u8]> =
2837 Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
2838 if value == 0 {
2839 pooled_weak = Some(Arc::downgrade(&payload));
2840 } else if value == ITERATIONS - 1 {
2841 overflow_weak = Some(Arc::downgrade(&payload));
2842 }
2843 pool_return(SqliteValue::Blob(payload));
2844 }
2845
2846 assert_eq!(
2847 pool_len(),
2848 VALUE_POOL_CAP,
2849 "the slab must retain at most VALUE_POOL_CAP entries",
2850 );
2851 weak_tx
2852 .send((
2853 pooled_weak.expect("capture pooled weak handle"),
2854 overflow_weak.expect("capture overflow weak handle"),
2855 log_value_pool_test_stats("test_slab_no_leak"),
2856 ))
2857 .expect("send slab leak stats");
2858 release_rx.recv().expect("wait for release");
2859 });
2860
2861 let (pooled_weak, overflow_weak, stats) =
2862 weak_rx.recv().expect("receive weak blob handles");
2863 assert!(
2864 pooled_weak.upgrade().is_some(),
2865 "pooled blob should remain alive while the owning thread is running"
2866 );
2867 assert!(
2868 overflow_weak.upgrade().is_none(),
2869 "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2870 );
2871 assert_eq!(
2872 stats,
2873 ValuePoolStats {
2874 slab_alloc_count: 0,
2875 slab_return_count: VALUE_POOL_CAP,
2876 global_alloc_fallback_count: 0,
2877 slab_high_water_mark: VALUE_POOL_CAP,
2878 }
2879 );
2880
2881 release_tx.send(()).expect("release worker thread");
2882 worker.join().expect("join worker");
2883
2884 assert!(
2885 pooled_weak.upgrade().is_none(),
2886 "thread-local slab contents must be dropped when the thread exits"
2887 );
2888 }
2889
2890 #[test]
2891 fn test_slab_thread_local_isolation() {
2892 let _guard = ValuePoolTestGuard::new();
2893
2894 pool_return(SqliteValue::Integer(11));
2895 assert_eq!(pool_len(), 1);
2896
2897 let worker = std::thread::spawn(|| {
2898 pool_clear();
2899 reset_value_pool_test_stats();
2900
2901 assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2902 pool_return(SqliteValue::Integer(22));
2903 assert_eq!(pool_len(), 1);
2904 assert_eq!(
2905 value_pool_test_stats_snapshot(),
2906 ValuePoolStats {
2907 slab_alloc_count: 0,
2908 slab_return_count: 1,
2909 global_alloc_fallback_count: 0,
2910 slab_high_water_mark: 1,
2911 }
2912 );
2913 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
2914 assert_eq!(pool_len(), 0);
2915 });
2916 worker.join().expect("join worker");
2917
2918 assert_eq!(
2919 pool_len(),
2920 1,
2921 "worker thread slab operations must not affect the caller thread"
2922 );
2923 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
2924 assert_eq!(pool_len(), 0);
2925 let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
2926 assert_eq!(
2927 stats,
2928 ValuePoolStats {
2929 slab_alloc_count: 1,
2930 slab_return_count: 1,
2931 global_alloc_fallback_count: 0,
2932 slab_high_water_mark: 1,
2933 }
2934 );
2935 }
2936
2937 #[test]
2938 fn test_slab_zero_malloc_steady_state() {
2939 let _guard = ValuePoolTestGuard::new();
2940 const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
2941 const ITERATIONS: usize = 1_000;
2942 const INITIAL_TEXT: &str =
2943 "steady-state pooled string backing store for bd-nsvud warmup payload";
2944 const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
2945
2946 assert!(
2947 REUSED_TEXT.len() <= INITIAL_TEXT.len(),
2948 "steady-state overwrite must fit within the warmed heap allocation"
2949 );
2950
2951 for _ in 0..WARM_POOL_DEPTH {
2952 pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
2953 }
2954 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2955
2956 reset_value_pool_test_stats();
2957 for _ in 0..ITERATIONS {
2958 let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
2959 let SqliteValue::Text(existing) = &mut reused else {
2960 panic!("warmed slab entry should remain a text value");
2961 };
2962 let original_ptr = existing.as_str().as_ptr();
2963 existing.overwrite(REUSED_TEXT);
2964 assert_eq!(
2965 existing.as_str().as_ptr(),
2966 original_ptr,
2967 "steady-state overwrite should reuse the warmed heap buffer",
2968 );
2969 assert_eq!(existing.as_str(), REUSED_TEXT);
2970 pool_return(reused);
2971 }
2972
2973 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2974 assert_eq!(
2975 log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
2976 ValuePoolStats {
2977 slab_alloc_count: ITERATIONS,
2978 slab_return_count: ITERATIONS,
2979 global_alloc_fallback_count: 0,
2980 slab_high_water_mark: WARM_POOL_DEPTH,
2981 }
2982 );
2983 }
2984
2985 #[test]
2986 fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
2987 let text = SmallText::new("this string is definitely longer than twenty three bytes");
2988 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2989 panic!("long text should start in heap-owned mode");
2990 };
2991 assert!(
2992 shared.get().is_none(),
2993 "long text should not allocate Arc eagerly before cloning"
2994 );
2995
2996 let cloned = text.clone();
2997
2998 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2999 panic!("original text should remain heap-owned after clone");
3000 };
3001 assert!(
3002 shared.get().is_some(),
3003 "first clone should materialize a shared Arc lazily"
3004 );
3005 assert!(
3006 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
3007 "cloned text should use the shared Arc representation"
3008 );
3009 assert_eq!(text.as_str(), cloned.as_str());
3010 }
3011
3012 #[test]
3013 fn test_small_text_invalid_utf8_clone_compare_and_serde_fail_closed() {
3014 let bytes: &[u8] = &[0x80, 0xC0, 0xAF];
3015 let text = SmallText::from_bytes(bytes);
3016 assert!(!text.is_valid_utf8());
3017 assert_eq!(text.as_str_checked(), None);
3018 assert_eq!(text.as_bytes_direct(), bytes);
3019 assert_eq!(text.len(), bytes.len());
3020 assert!(!text.is_inline());
3021
3022 let cloned = text.clone();
3023 assert_eq!(cloned, text);
3024 assert_eq!(cloned.as_bytes_direct(), bytes);
3025 assert_ne!(
3026 text,
3027 SmallText::new("\u{FFFD}\u{FFFD}\u{FFFD}"),
3028 "lossy display text must not define SQLite TEXT equality"
3029 );
3030
3031 let error = serde_json::to_string(&text)
3032 .expect_err("raw TEXT must not be silently substituted during string serialization");
3033 assert!(
3034 error.to_string().contains("invalid UTF-8"),
3035 "serialization failure must explain the unsupported Rust-string boundary: {error}"
3036 );
3037 }
3038
3039 #[test]
3040 fn test_small_text_overwrite_reuses_unique_heap_buffer() {
3041 let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
3042 let (original_ptr, original_capacity) = match &text.repr {
3043 SmallTextRepr::HeapOwned { text, shared } => {
3044 assert!(shared.get().is_none(), "fresh heap text should be unshared");
3045 (text.as_ptr(), text.capacity())
3046 }
3047 _ => panic!("long text should start in heap-owned mode"),
3048 };
3049
3050 text.overwrite("another long string that still fits the same allocation");
3051
3052 match &text.repr {
3053 SmallTextRepr::HeapOwned { text, shared } => {
3054 assert!(
3055 shared.get().is_none(),
3056 "overwrite should keep text single-owner"
3057 );
3058 assert_eq!(text.as_ptr(), original_ptr);
3059 assert_eq!(text.capacity(), original_capacity);
3060 assert_eq!(
3061 text.as_str(),
3062 "another long string that still fits the same allocation"
3063 );
3064 }
3065 _ => panic!("overwrite should keep long text in heap-owned mode"),
3066 }
3067 }
3068
3069 #[test]
3070 fn test_small_text_overwrite_detaches_from_shared_arc() {
3071 let original = "this string is definitely longer than twenty three bytes";
3072 let mut text = SmallText::new(original);
3073 let (original_ptr, original_capacity) = match &text.repr {
3074 SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
3075 _ => panic!("long text should start in heap-owned mode"),
3076 };
3077 let replacement = "replacement text that must not mutate the shared clone";
3078 assert!(
3079 replacement.len() <= original_capacity,
3080 "replacement should fit the original heap allocation for this regression",
3081 );
3082 let clone = text.clone();
3083
3084 text.overwrite(replacement);
3085
3086 assert_eq!(
3087 clone.as_str(),
3088 original,
3089 "existing shared clones must keep the original contents"
3090 );
3091 assert_eq!(text.as_str(), replacement);
3092 match &text.repr {
3093 SmallTextRepr::HeapOwned { text, shared } => {
3094 assert_eq!(
3095 text.as_ptr(),
3096 original_ptr,
3097 "overwriting a cloned long string should keep the owned buffer",
3098 );
3099 assert_eq!(
3100 text.capacity(),
3101 original_capacity,
3102 "detaching from the shared cache should preserve capacity",
3103 );
3104 assert!(
3105 shared.get().is_none(),
3106 "overwrite should reset the lazy shared cache after detaching"
3107 );
3108 }
3109 _ => panic!("overwrite should restore heap-owned mode"),
3110 }
3111 }
3112
3113 #[test]
3114 fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
3115 let _guard = ValuePoolTestGuard::new();
3116
3117 pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
3118 assert_eq!(
3119 pool_len(),
3120 0,
3121 "inline text should not occupy reusable slab slots",
3122 );
3123
3124 let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
3125 let _clone = owned_text.clone();
3126 pool_return_reusable(SqliteValue::Text(owned_text));
3127 assert_eq!(
3128 pool_len(),
3129 1,
3130 "heap-owned text should stay reusable even after serving shared clones",
3131 );
3132 assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
3133 assert_eq!(pool_len(), 0);
3134
3135 let shared_text =
3136 Arc::<str>::from("this string is definitely longer than twenty three bytes");
3137 pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
3138 &shared_text,
3139 ))));
3140 assert_eq!(
3141 pool_len(),
3142 0,
3143 "arc-backed shared text should not enter the reusable slab",
3144 );
3145
3146 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
3147 pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
3148 assert_eq!(
3149 pool_len(),
3150 0,
3151 "shared blob allocations should not displace reusable slab entries",
3152 );
3153
3154 let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
3155 pool_return_reusable(SqliteValue::Blob(unique_blob));
3156 assert_eq!(
3157 pool_len(),
3158 1,
3159 "unique blob allocations should remain eligible for slab reuse",
3160 );
3161 }
3162
3163 #[test]
3164 fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
3165 let text = Arc::new(SmallText::new(
3166 "this string is definitely longer than twenty three bytes",
3167 ));
3168 let expected = text.as_str().to_owned();
3169 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3170 panic!("long text should start in heap-owned mode");
3171 };
3172 assert!(
3173 shared.get().is_none(),
3174 "shared Arc should still be lazy before concurrent clones"
3175 );
3176
3177 let barrier = Arc::new(std::sync::Barrier::new(5));
3178 let mut workers = Vec::new();
3179 for _ in 0..4 {
3180 let text = Arc::clone(&text);
3181 let barrier = Arc::clone(&barrier);
3182 let expected = expected.clone();
3183 workers.push(std::thread::spawn(move || {
3184 barrier.wait();
3185 for _ in 0..64 {
3186 let cloned = (*text).clone();
3187 assert_eq!(cloned.as_str(), expected);
3188 assert!(
3189 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
3190 "concurrent clone should reuse the shared Arc representation"
3191 );
3192 }
3193 }));
3194 }
3195
3196 barrier.wait();
3197 for worker in workers {
3198 worker
3199 .join()
3200 .expect("join concurrent small-text clone worker");
3201 }
3202
3203 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
3204 panic!("original text should remain heap-owned after clone promotion");
3205 };
3206 let shared = shared
3207 .get()
3208 .expect("concurrent clones should promote the lazy shared Arc");
3209 assert_eq!(shared.as_ref(), expected);
3210 assert_eq!(text.as_str(), expected);
3211 }
3212
3213 #[test]
3214 fn null_properties() {
3215 let v = SqliteValue::Null;
3216 assert!(v.is_null());
3217 assert_eq!(v.to_integer(), 0);
3218 assert_eq!(v.to_float(), 0.0);
3219 assert_eq!(v.to_text(), "");
3220 assert_eq!(v.to_string(), "NULL");
3221 }
3222
3223 #[test]
3224 fn integer_properties() {
3225 let v = SqliteValue::Integer(42);
3226 assert!(!v.is_null());
3227 assert_eq!(v.as_integer(), Some(42));
3228 assert_eq!(v.to_integer(), 42);
3229 assert_eq!(v.to_float(), 42.0);
3230 assert_eq!(v.to_text(), "42");
3231 }
3232
3233 #[test]
3234 fn float_properties() {
3235 let v = SqliteValue::Float(3.14);
3236 assert_eq!(v.as_float(), Some(3.14));
3237 assert_eq!(v.to_integer(), 3);
3238 assert_eq!(v.to_text(), "3.14");
3239 }
3240
3241 #[test]
3242 fn text_properties() {
3243 let v = SqliteValue::Text(SmallText::new("hello"));
3244 assert_eq!(v.as_text(), Some("hello"));
3245 assert_eq!(v.to_integer(), 0);
3246 assert_eq!(v.to_float(), 0.0);
3247 }
3248
3249 #[test]
3250 fn text_numeric_coercion() {
3251 let v = SqliteValue::Text(SmallText::new("123"));
3252 assert_eq!(v.to_integer(), 123);
3253 assert_eq!(v.to_float(), 123.0);
3254
3255 let v = SqliteValue::Text(SmallText::new("3.14"));
3256 assert_eq!(v.to_integer(), 3);
3257 assert_eq!(v.to_float(), 3.14);
3258 }
3259
3260 #[test]
3261 fn text_numeric_coercion_ignores_hex_text_prefixes() {
3262 let v = SqliteValue::Text(SmallText::new("0x10"));
3263 assert_eq!(v.to_integer(), 0);
3264 assert_eq!(v.to_float(), 0.0);
3265
3266 let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
3267 assert_eq!(v.to_integer(), 0);
3268 assert_eq!(v.to_float(), 0.0);
3269 }
3270
3271 #[test]
3272 fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
3273 assert_eq!(
3274 SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
3275 SqliteValue::Integer(123)
3276 );
3277 assert_eq!(
3278 SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
3279 SqliteValue::Float(0.0)
3280 );
3281 assert_eq!(
3282 SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
3283 SqliteValue::Float(123.0)
3284 );
3285 assert_eq!(
3286 SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
3287 SqliteValue::Float(1.0)
3288 );
3289 assert_eq!(
3290 SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
3291 SqliteValue::Float(123.0)
3292 );
3293 assert_eq!(
3294 SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
3295 SqliteValue::Float(0.0)
3296 );
3297 assert_eq!(
3298 SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
3299 SqliteValue::Float(123.0)
3300 );
3301 }
3302
3303 #[test]
3304 fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
3305 assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
3306 assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
3307 assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
3308 assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
3309 }
3310
3311 #[test]
3312 fn test_sqlite_value_integer_real_comparison_equal() {
3313 let int_value = SqliteValue::Integer(3);
3314 let real_value = SqliteValue::Float(3.0);
3315 assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
3316 assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
3317 }
3318
3319 #[test]
3320 fn test_sqlite_value_text_to_integer_coercion() {
3321 let text_value = SqliteValue::Text(SmallText::new("123"));
3322 let coerced = text_value.apply_affinity(TypeAffinity::Integer);
3323 assert_eq!(coerced, SqliteValue::Integer(123));
3324 }
3325
3326 #[test]
3327 fn blob_properties() {
3328 let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
3329 assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
3330 assert_eq!(v.to_integer(), 0);
3331 assert_eq!(v.to_float(), 0.0);
3332 assert_eq!(v.to_text(), "\u{07AD}");
3335 }
3336
3337 #[test]
3338 fn display_formatting() {
3339 assert_eq!(SqliteValue::Null.to_string(), "NULL");
3340 assert_eq!(SqliteValue::Integer(42).to_string(), "42");
3341 assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
3342 assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
3343 assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
3344 assert_eq!(
3345 SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
3346 "X'CAFE'"
3347 );
3348 }
3349
3350 #[test]
3351 fn sort_order_null_first() {
3352 let null = SqliteValue::Null;
3353 let int = SqliteValue::Integer(0);
3354 let text = SqliteValue::Text(SmallText::new(""));
3355 let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
3356
3357 assert!(null < int);
3358 assert!(int < text);
3359 assert!(text < blob);
3360 }
3361
3362 #[test]
3363 fn sort_order_integers() {
3364 let a = SqliteValue::Integer(1);
3365 let b = SqliteValue::Integer(2);
3366 assert!(a < b);
3367 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
3368 }
3369
3370 #[test]
3371 fn sort_order_mixed_numeric() {
3372 let int = SqliteValue::Integer(1);
3373 let float = SqliteValue::Float(1.5);
3374 assert!(int < float);
3375
3376 let int = SqliteValue::Integer(2);
3377 assert!(int > float);
3378 }
3379
3380 #[test]
3381 fn test_int_float_precision_at_i64_boundary() {
3382 let imax = SqliteValue::Integer(i64::MAX);
3386 let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
3387 assert_eq!(
3388 imax.partial_cmp(&fmax),
3389 Some(Ordering::Less),
3390 "i64::MAX must be Less than 9223372036854775808.0"
3391 );
3392
3393 let a = SqliteValue::Integer(i64::MAX);
3395 let b = SqliteValue::Integer(i64::MAX - 1);
3396 let f = SqliteValue::Float(i64::MAX as f64);
3397 assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
3399 assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
3401 assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
3402 }
3403
3404 #[test]
3405 fn test_int_float_precision_symmetric() {
3406 let i = SqliteValue::Integer(i64::MAX);
3408 let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
3409 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3410 }
3411
3412 #[test]
3413 fn test_int_float_exact_representation() {
3414 let i = SqliteValue::Integer(42);
3416 let f = SqliteValue::Float(42.0);
3417 assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
3418 assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
3419
3420 let i = SqliteValue::Integer(3);
3422 let f = SqliteValue::Float(3.5);
3423 assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
3424 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3425 }
3426
3427 #[test]
3428 fn from_conversions() {
3429 assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
3430 assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
3431 assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
3432 assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
3433 assert_eq!(
3434 SqliteValue::from(String::from("world")).as_text(),
3435 Some("world")
3436 );
3437 assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
3438 assert!(SqliteValue::from(None::<i64>).is_null());
3439 assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
3440 }
3441
3442 #[test]
3443 fn affinity() {
3444 assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
3445 assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
3446 assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
3447 assert_eq!(
3448 SqliteValue::Text(SmallText::new("")).affinity(),
3449 TypeAffinity::Text
3450 );
3451 assert_eq!(
3452 SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
3453 TypeAffinity::Blob
3454 );
3455 }
3456
3457 #[test]
3458 fn null_equality() {
3459 let a = SqliteValue::Null;
3461 let b = SqliteValue::Null;
3462 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3463 }
3464
3465 #[test]
3468 fn test_storage_class_variants() {
3469 assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
3470 assert_eq!(
3471 SqliteValue::Integer(42).storage_class(),
3472 StorageClass::Integer
3473 );
3474 assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
3475 assert_eq!(
3476 SqliteValue::Text("hi".into()).storage_class(),
3477 StorageClass::Text
3478 );
3479 assert_eq!(
3480 SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
3481 StorageClass::Blob
3482 );
3483 }
3484
3485 #[test]
3486 fn test_type_affinity_advisory_text_into_integer_ok() {
3487 let val = SqliteValue::Text("hello".into());
3490 let coerced = val.apply_affinity(TypeAffinity::Integer);
3491 assert!(coerced.as_text().is_some());
3492 assert_eq!(coerced.as_text().unwrap(), "hello");
3493
3494 let val = SqliteValue::Text("42".into());
3496 let coerced = val.apply_affinity(TypeAffinity::Integer);
3497 assert_eq!(coerced.as_integer(), Some(42));
3498 }
3499
3500 #[test]
3501 fn test_type_affinity_advisory_integer_into_text_ok() {
3502 let val = SqliteValue::Integer(42);
3504 let coerced = val.apply_affinity(TypeAffinity::Text);
3505 assert_eq!(coerced.as_text(), Some("42"));
3506 }
3507
3508 #[test]
3509 fn test_type_affinity_comparison_coercion_matches_oracle() {
3510 let val = SqliteValue::Text("123".into());
3512 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3513 assert_eq!(coerced.as_integer(), Some(123));
3514
3515 let val = SqliteValue::Text("3.14".into());
3517 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3518 assert_eq!(coerced.as_float(), Some(3.14));
3519
3520 let val = SqliteValue::Text("hello".into());
3522 let coerced = val.apply_affinity(TypeAffinity::Numeric);
3523 assert!(coerced.as_text().is_some());
3524
3525 let val = SqliteValue::Integer(42);
3527 let coerced = val.apply_affinity(TypeAffinity::Blob);
3528 assert_eq!(coerced.as_integer(), Some(42));
3529
3530 let val = SqliteValue::Float(5.0);
3532 let coerced = val.apply_affinity(TypeAffinity::Integer);
3533 assert_eq!(coerced.as_integer(), Some(5));
3534
3535 let val = SqliteValue::Float(5.5);
3537 let coerced = val.apply_affinity(TypeAffinity::Integer);
3538 assert_eq!(coerced.as_float(), Some(5.5));
3539
3540 let val = SqliteValue::Integer(7);
3542 let coerced = val.apply_affinity(TypeAffinity::Real);
3543 assert_eq!(coerced.as_float(), Some(7.0));
3544
3545 let val = SqliteValue::Text("9".into());
3547 let coerced = val.apply_affinity(TypeAffinity::Real);
3548 assert_eq!(coerced.as_float(), Some(9.0));
3549 }
3550
3551 #[test]
3552 fn test_cast_to_numeric_uses_sqlite_cast_rules() {
3553 assert_eq!(
3554 SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
3555 SqliteValue::Integer(123)
3556 );
3557 assert_eq!(
3558 SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
3559 SqliteValue::Integer(150)
3560 );
3561 assert_eq!(
3562 SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
3563 SqliteValue::Integer(0)
3564 );
3565 assert_eq!(
3566 SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
3567 SqliteValue::Integer(123)
3568 );
3569
3570 match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
3571 SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
3572 other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
3573 }
3574 }
3575
3576 #[test]
3577 fn test_strict_table_rejects_text_into_integer() {
3578 let val = SqliteValue::Text("hello".into());
3579 let result = val.validate_strict(StrictColumnType::Integer);
3580 assert!(result.is_err());
3581 let err = result.unwrap_err();
3582 assert_eq!(err.expected, StrictColumnType::Integer);
3583 assert_eq!(err.actual, StorageClass::Text);
3584 }
3585
3586 #[test]
3587 fn test_strict_table_allows_exact_type() {
3588 let val = SqliteValue::Integer(42);
3590 assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
3591
3592 let val = SqliteValue::Float(3.14);
3594 assert!(val.validate_strict(StrictColumnType::Real).is_ok());
3595
3596 let val = SqliteValue::Text("hello".into());
3598 assert!(val.validate_strict(StrictColumnType::Text).is_ok());
3599
3600 let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
3602 assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
3603
3604 assert!(
3606 SqliteValue::Null
3607 .validate_strict(StrictColumnType::Integer)
3608 .is_ok()
3609 );
3610 assert!(
3611 SqliteValue::Null
3612 .validate_strict(StrictColumnType::Text)
3613 .is_ok()
3614 );
3615
3616 let val = SqliteValue::Integer(42);
3618 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3619 let val = SqliteValue::Text("hi".into());
3620 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3621 }
3622
3623 #[test]
3624 fn test_strict_real_accepts_integer_with_coercion() {
3625 let val = SqliteValue::Integer(42);
3627 let result = val.validate_strict(StrictColumnType::Real).unwrap();
3628 assert_eq!(result.as_float(), Some(42.0));
3629 }
3630
3631 #[test]
3632 fn test_strict_rejects_wrong_storage_classes() {
3633 assert!(
3635 SqliteValue::Float(3.14)
3636 .validate_strict(StrictColumnType::Integer)
3637 .is_err()
3638 );
3639
3640 assert!(
3642 SqliteValue::Blob(Arc::from([1u8].as_slice()))
3643 .validate_strict(StrictColumnType::Text)
3644 .is_err()
3645 );
3646
3647 assert_eq!(
3649 SqliteValue::Integer(1)
3650 .validate_strict(StrictColumnType::Text)
3651 .unwrap(),
3652 SqliteValue::Text("1".into())
3653 );
3654
3655 assert!(
3657 SqliteValue::Text("x".into())
3658 .validate_strict(StrictColumnType::Blob)
3659 .is_err()
3660 );
3661 }
3662
3663 #[test]
3664 fn test_strict_column_type_parsing() {
3665 assert_eq!(
3666 StrictColumnType::from_type_name("INT"),
3667 Some(StrictColumnType::Integer)
3668 );
3669 assert_eq!(
3670 StrictColumnType::from_type_name("INTEGER"),
3671 Some(StrictColumnType::Integer)
3672 );
3673 assert_eq!(
3674 StrictColumnType::from_type_name("REAL"),
3675 Some(StrictColumnType::Real)
3676 );
3677 assert_eq!(
3678 StrictColumnType::from_type_name("TEXT"),
3679 Some(StrictColumnType::Text)
3680 );
3681 assert_eq!(
3682 StrictColumnType::from_type_name("BLOB"),
3683 Some(StrictColumnType::Blob)
3684 );
3685 assert_eq!(
3686 StrictColumnType::from_type_name("ANY"),
3687 Some(StrictColumnType::Any)
3688 );
3689 assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
3691 assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
3692 }
3693
3694 #[test]
3695 fn test_affinity_advisory_never_rejects() {
3696 let values = vec![
3698 SqliteValue::Null,
3699 SqliteValue::Integer(42),
3700 SqliteValue::Float(3.14),
3701 SqliteValue::Text("hello".into()),
3702 SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
3703 ];
3704 let affinities = [
3705 TypeAffinity::Integer,
3706 TypeAffinity::Text,
3707 TypeAffinity::Blob,
3708 TypeAffinity::Real,
3709 TypeAffinity::Numeric,
3710 ];
3711 for val in &values {
3712 for aff in &affinities {
3713 let _ = val.clone().apply_affinity(*aff);
3715 }
3716 }
3717 }
3718
3719 #[test]
3722 fn test_unique_allows_multiple_nulls_single_column() {
3723 let a = SqliteValue::Null;
3725 let b = SqliteValue::Null;
3726 assert!(!a.unique_eq(&b));
3727 }
3728
3729 #[test]
3730 fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
3731 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3734 let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
3735 assert!(!unique_key_duplicates(&row_a, &row_b));
3736
3737 let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
3739 let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
3740 assert!(!unique_key_duplicates(&row_a, &row_b));
3741
3742 let row_a = [SqliteValue::Null, SqliteValue::Null];
3744 let row_b = [SqliteValue::Null, SqliteValue::Null];
3745 assert!(!unique_key_duplicates(&row_a, &row_b));
3746 }
3747
3748 #[test]
3749 fn test_unique_rejects_duplicate_non_null() {
3750 let a = SqliteValue::Integer(42);
3752 let b = SqliteValue::Integer(42);
3753 assert!(a.unique_eq(&b));
3754
3755 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3757 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3758 assert!(unique_key_duplicates(&row_a, &row_b));
3759
3760 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3762 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
3763 assert!(!unique_key_duplicates(&row_a, &row_b));
3764 }
3765
3766 #[test]
3767 fn test_unique_null_vs_non_null_distinct() {
3768 let a = SqliteValue::Null;
3770 let b = SqliteValue::Integer(1);
3771 assert!(!a.unique_eq(&b));
3772 assert!(!b.unique_eq(&a));
3773
3774 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3776 let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
3777 assert!(!unique_key_duplicates(&row_a, &row_b));
3778 }
3779
3780 #[test]
3783 #[allow(clippy::cast_precision_loss)]
3784 fn test_integer_overflow_promotes_real_expr_add() {
3785 let max = SqliteValue::Integer(i64::MAX);
3786 let one = SqliteValue::Integer(1);
3787 let result = max.sql_add(&one);
3788 assert!(result.as_integer().is_none());
3790 assert!(result.as_float().is_some());
3791 assert!(result.as_float().unwrap() >= i64::MAX as f64);
3793 }
3794
3795 #[test]
3796 fn test_integer_overflow_promotes_real_expr_mul() {
3797 let max = SqliteValue::Integer(i64::MAX);
3798 let two = SqliteValue::Integer(2);
3799 let result = max.sql_mul(&two);
3800 assert!(result.as_float().is_some());
3802 }
3803
3804 #[test]
3805 fn test_integer_overflow_promotes_real_expr_sub() {
3806 let min = SqliteValue::Integer(i64::MIN);
3807 let one = SqliteValue::Integer(1);
3808 let result = min.sql_sub(&one);
3809 assert!(result.as_float().is_some());
3811 }
3812
3813 #[test]
3814 fn test_sum_overflow_errors() {
3815 let mut acc = SumAccumulator::new();
3816 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3817 acc.accumulate(&SqliteValue::Integer(1));
3818 let result = acc.finish();
3819 assert!(result.is_err());
3820 }
3821
3822 #[test]
3823 fn test_sum_overflow_then_float_returns_real() {
3824 let mut acc = SumAccumulator::new();
3825 acc.accumulate(&SqliteValue::Integer(i64::MAX));
3826 acc.accumulate(&SqliteValue::Integer(1));
3827 acc.accumulate(&SqliteValue::Float(0.5));
3828 let result = acc.finish().unwrap();
3829 assert!(matches!(result, SqliteValue::Float(_)));
3830 }
3831
3832 #[test]
3833 fn test_sum_text_integer_literals_stay_integer() {
3834 let mut acc = SumAccumulator::new();
3835 acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
3836 acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
3837 let result = acc.finish().unwrap();
3838 assert_eq!(result.as_integer(), Some(3));
3839 }
3840
3841 #[test]
3842 fn test_sum_non_numeric_text_returns_real_zero() {
3843 let mut acc = SumAccumulator::new();
3844 acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
3845 let result = acc.finish().unwrap();
3846 assert_eq!(result.as_float(), Some(0.0));
3847 }
3848
3849 #[test]
3850 fn test_no_overflow_stays_integer() {
3851 let a = SqliteValue::Integer(100);
3853 let b = SqliteValue::Integer(200);
3854 let result = a.sql_add(&b);
3855 assert_eq!(result.as_integer(), Some(300));
3856
3857 let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
3859 assert_eq!(result.as_integer(), Some(42));
3860
3861 let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
3863 assert_eq!(result.as_integer(), Some(42));
3864 }
3865
3866 #[test]
3867 fn test_sum_null_only_returns_null() {
3868 let mut acc = SumAccumulator::new();
3869 acc.accumulate(&SqliteValue::Null);
3870 acc.accumulate(&SqliteValue::Null);
3871 let result = acc.finish().unwrap();
3872 assert!(result.is_null());
3873 }
3874
3875 #[test]
3876 fn test_sum_mixed_int_float() {
3877 let mut acc = SumAccumulator::new();
3878 acc.accumulate(&SqliteValue::Integer(10));
3879 acc.accumulate(&SqliteValue::Float(2.5));
3880 acc.accumulate(&SqliteValue::Integer(3));
3881 let result = acc.finish().unwrap();
3882 assert_eq!(result.as_float(), Some(15.5));
3884 }
3885
3886 #[test]
3887 fn test_sum_integer_only() {
3888 let mut acc = SumAccumulator::new();
3889 acc.accumulate(&SqliteValue::Integer(10));
3890 acc.accumulate(&SqliteValue::Integer(20));
3891 acc.accumulate(&SqliteValue::Integer(30));
3892 let result = acc.finish().unwrap();
3893 assert_eq!(result.as_integer(), Some(60));
3894 }
3895
3896 #[test]
3897 fn test_sql_arithmetic_null_propagation() {
3898 let n = SqliteValue::Null;
3899 let i = SqliteValue::Integer(42);
3900 assert!(n.sql_add(&i).is_null());
3901 assert!(i.sql_add(&n).is_null());
3902 assert!(n.sql_sub(&i).is_null());
3903 assert!(n.sql_mul(&i).is_null());
3904 }
3905
3906 #[test]
3907 fn test_sql_inf_arithmetic_nan_normalized_to_null() {
3908 let pos_inf = SqliteValue::Float(f64::INFINITY);
3910 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3911 assert!(pos_inf.sql_add(&neg_inf).is_null());
3912
3913 assert!(pos_inf.sql_sub(&pos_inf).is_null());
3915 }
3916
3917 #[test]
3918 fn test_sql_mul_zero_times_inf_normalized_to_null() {
3919 let zero = SqliteValue::Float(0.0);
3921 let pos_inf = SqliteValue::Float(f64::INFINITY);
3922 assert!(zero.sql_mul(&pos_inf).is_null());
3923 assert!(
3924 SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
3925 "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
3926 );
3927 }
3928
3929 #[test]
3930 fn test_sql_mul_mixed_int_float_stays_real() {
3931 let left = SqliteValue::Integer(10);
3932 let right = SqliteValue::Float(0.25);
3933 assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
3934 assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
3935 }
3936
3937 #[test]
3938 fn test_sql_inf_propagates_when_not_nan() {
3939 let pos_inf = SqliteValue::Float(f64::INFINITY);
3940 let one = SqliteValue::Integer(1);
3941 let add_result = pos_inf.sql_add(&one);
3942 assert!(
3943 matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
3944 "expected +Inf propagation, got {add_result:?}"
3945 );
3946
3947 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3948 let sub_result = neg_inf.sql_sub(&one);
3949 assert!(
3950 matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
3951 "expected -Inf propagation, got {sub_result:?}"
3952 );
3953 }
3954
3955 #[test]
3956 fn test_from_f64_nan_normalizes_to_null() {
3957 let value = SqliteValue::from(f64::NAN);
3958 assert!(value.is_null());
3959 }
3960
3961 #[test]
3962 fn test_inf_comparisons_against_finite_values() {
3963 let pos_inf = SqliteValue::Float(f64::INFINITY);
3964 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3965 let finite_hi = SqliteValue::Float(1.0e308);
3966 let finite_lo = SqliteValue::Float(-1.0e308);
3967
3968 assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
3969 assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
3970 }
3971
3972 #[test]
3975 fn test_empty_string_is_not_null() {
3976 let empty = SqliteValue::Text(SmallText::new(""));
3977 assert!(!empty.is_null());
3979 assert!(!empty.is_null());
3981 assert!(SqliteValue::Null.is_null());
3983 }
3984
3985 #[test]
3986 fn test_length_empty_string_zero() {
3987 let empty = SqliteValue::Text(SmallText::new(""));
3988 assert_eq!(empty.sql_length(), Some(0));
3989 }
3990
3991 #[test]
3992 fn test_typeof_empty_string_text() {
3993 let empty = SqliteValue::Text(SmallText::new(""));
3994 assert_eq!(empty.typeof_str(), "text");
3995 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3997 }
3998
3999 #[test]
4000 fn test_empty_string_comparisons() {
4001 let empty1 = SqliteValue::Text(SmallText::new(""));
4002 let empty2 = SqliteValue::Text(SmallText::new(""));
4003 assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
4005
4006 let null = SqliteValue::Null;
4010 assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
4011 }
4012
4013 #[test]
4014 fn test_typeof_all_variants() {
4015 assert_eq!(SqliteValue::Null.typeof_str(), "null");
4016 assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
4017 assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
4018 assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
4019 assert_eq!(
4020 SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
4021 "blob"
4022 );
4023 }
4024
4025 #[test]
4026 fn test_sql_length_all_types() {
4027 assert_eq!(SqliteValue::Null.sql_length(), None);
4029 assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
4031 assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
4032 assert_eq!(
4034 SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
4035 Some(3)
4036 );
4037 assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
4039 assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); }
4042
4043 #[test]
4046 fn test_like_ascii_case_insensitive() {
4047 assert!(sql_like("A", "a", None));
4048 assert!(sql_like("a", "A", None));
4049 assert!(sql_like("hello", "HELLO", None));
4050 assert!(sql_like("HELLO", "hello", None));
4051 assert!(sql_like("HeLLo", "hEllO", None));
4052 }
4053
4054 #[test]
4055 fn test_like_unicode_case_sensitive_without_icu() {
4056 assert!(!sql_like("ä", "Ä", None));
4058 assert!(!sql_like("Ä", "ä", None));
4059 assert!(sql_like("ä", "ä", None));
4061 }
4062
4063 #[test]
4064 fn test_like_fast_path_does_not_fold_ascii_punctuation() {
4065 assert!(!sql_like("[", "{", None));
4066 assert!(!sql_like("@", "`", None));
4067 }
4068
4069 #[test]
4070 fn test_like_escape_handling() {
4071 assert!(sql_like("100\\%", "100%", Some('\\')));
4073 assert!(!sql_like("100\\%", "100x", Some('\\')));
4074
4075 assert!(sql_like("a\\_b", "a_b", Some('\\')));
4077 assert!(!sql_like("a\\_b", "axb", Some('\\')));
4078 }
4079
4080 #[test]
4081 fn test_like_wildcards_basic() {
4082 assert!(sql_like("%", "", None));
4084 assert!(sql_like("%", "anything", None));
4085 assert!(sql_like("a%", "abc", None));
4086 assert!(sql_like("%c", "abc", None));
4087 assert!(sql_like("a%c", "abc", None));
4088 assert!(sql_like("a%c", "aXYZc", None));
4089 assert!(!sql_like("a%c", "abd", None));
4090
4091 assert!(sql_like("_", "x", None));
4093 assert!(!sql_like("_", "", None));
4094 assert!(!sql_like("_", "xy", None));
4095 assert!(sql_like("a_c", "abc", None));
4096 assert!(!sql_like("a_c", "abbc", None));
4097 }
4098
4099 #[test]
4100 fn test_like_combined_wildcards() {
4101 assert!(sql_like("%_", "a", None));
4102 assert!(!sql_like("%_", "", None));
4103 assert!(sql_like("_%_", "ab", None));
4104 assert!(!sql_like("_%_", "a", None));
4105 assert!(sql_like("%a%b%", "xaybz", None));
4106 assert!(!sql_like("%a%b%", "xyz", None));
4107 }
4108
4109 #[test]
4110 fn test_like_exact_match() {
4111 assert!(sql_like("hello", "hello", None));
4112 assert!(!sql_like("hello", "world", None));
4113 assert!(sql_like("", "", None));
4114 assert!(!sql_like("a", "", None));
4115 assert!(!sql_like("", "a", None));
4116 }
4117
4118 #[test]
4119 fn test_like_fast_path_repeated_percent_shapes() {
4120 assert!(sql_like("ab%%", "ABcd", None));
4121 assert!(sql_like("%%cd", "abCD", None));
4122 assert!(sql_like("%%bc%%", "xxBCyy", None));
4123 assert!(sql_like("%%%%", "anything", None));
4124 }
4125
4126 #[test]
4127 fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
4128 assert!(sql_like("%éL%", "héllo", None));
4129 assert!(!sql_like("%Él%", "héllo", None));
4130 assert!(sql_like("Stra%", "straße", None));
4131 }
4132
4133 #[test]
4134 fn test_like_contains_fast_path_handles_overlapping_matches() {
4135 assert!(sql_like("%ana%", "bananas", None));
4136 assert!(sql_like("%NAN%", "baNanas", None));
4137 assert!(!sql_like("%ananasx%", "bananas", None));
4138 }
4139
4140 #[test]
4141 fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
4142 assert!(sql_like("%ß%", "straße", None));
4143 assert!(!sql_like("%SS%", "straße", None));
4144 }
4145
4146 #[test]
4149 fn test_format_sqlite_float_whole_number() {
4150 assert_eq!(format_sqlite_float(120.0), "120.0");
4151 assert_eq!(format_sqlite_float(0.0), "0.0");
4152 assert_eq!(format_sqlite_float(-42.0), "-42.0");
4153 assert_eq!(format_sqlite_float(1.0), "1.0");
4154 }
4155
4156 #[test]
4157 fn test_format_sqlite_float_fractional() {
4158 assert_eq!(format_sqlite_float(3.14), "3.14");
4159 assert_eq!(format_sqlite_float(0.5), "0.5");
4160 assert_eq!(format_sqlite_float(-0.001), "-0.001");
4161 }
4162
4163 #[test]
4164 fn test_format_sqlite_float_special() {
4165 assert_eq!(format_sqlite_float(f64::NAN), "NaN");
4166 assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
4167 assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
4168 }
4169
4170 #[test]
4171 fn test_format_sqlite_float_negative_zero() {
4172 assert_eq!(format_sqlite_float(-0.0), "0.0");
4174 assert_eq!(format_sqlite_float(0.0), "0.0");
4175 }
4176
4177 #[test]
4178 fn test_format_sqlite_float_matches_sqlite_17_digit_text_contract() {
4179 assert_eq!(format_sqlite_float(0.1 + 0.2), "0.30000000000000004");
4180 assert_eq!(format_sqlite_float(1.0 / 3.0), "0.33333333333333332");
4181 assert_eq!(format_sqlite_float(2.0 / 3.0), "0.66666666666666663");
4182 assert_eq!(format_sqlite_float(1.5e16), "15000000000000000.0");
4183 assert_eq!(
4184 format_sqlite_float(123_456_789_012_345.6),
4185 "123456789012345.59"
4186 );
4187 assert_eq!(format_sqlite_float(1.0e308), "1.0e+308");
4188 assert_eq!(format_sqlite_float(1.0e-308), "1.0e-308");
4189 assert_eq!(
4190 format_sqlite_float(9.223_372_036_854_776e18),
4191 "9.2233720368547758e+18"
4192 );
4193 }
4194
4195 #[test]
4196 fn test_float_to_text_includes_decimal_point() {
4197 let v = SqliteValue::Float(100.0);
4198 assert_eq!(v.to_text(), "100.0");
4199 let v = SqliteValue::Float(3.14);
4200 assert_eq!(v.to_text(), "3.14");
4201 }
4202
4203 #[test]
4206 fn test_scan_numeric_prefix_bare_dot() {
4207 assert_eq!(scan_numeric_prefix(b"."), 0);
4209 assert_eq!(scan_numeric_prefix(b"-."), 0);
4210 assert_eq!(scan_numeric_prefix(b"+."), 0);
4211 assert_eq!(scan_numeric_prefix(b"..1"), 0);
4212 }
4213
4214 #[test]
4215 fn test_scan_numeric_prefix_valid() {
4216 assert_eq!(scan_numeric_prefix(b"123"), 3);
4217 assert_eq!(scan_numeric_prefix(b"3.14"), 4);
4218 assert_eq!(scan_numeric_prefix(b".5"), 2);
4219 assert_eq!(scan_numeric_prefix(b"1e10"), 4);
4220 assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
4221 assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
4222 assert_eq!(scan_numeric_prefix(b"0.0"), 3);
4223 }
4224
4225 #[test]
4226 fn test_scan_numeric_prefix_empty_and_non_numeric() {
4227 assert_eq!(scan_numeric_prefix(b""), 0);
4228 assert_eq!(scan_numeric_prefix(b"abc"), 0);
4229 assert_eq!(scan_numeric_prefix(b"+"), 0);
4230 assert_eq!(scan_numeric_prefix(b"-"), 0);
4231 }
4232}