1use std::cell::RefCell;
2use std::cmp::Ordering;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::{Arc, OnceLock};
6
7use memchr::{memchr, memchr2, memmem};
8
9use crate::{StorageClass, StrictColumnType, StrictTypeError, TypeAffinity};
10
11const VALUE_POOL_CAP: usize = 256;
20
21thread_local! {
22 static VALUE_POOL: RefCell<Vec<SqliteValue>> = const { RefCell::new(Vec::new()) };
28}
29
30#[cfg(test)]
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32struct ValuePoolStats {
33 slab_alloc_count: usize,
34 slab_return_count: usize,
35 global_alloc_fallback_count: usize,
36 slab_high_water_mark: usize,
37}
38
39#[cfg(test)]
40impl ValuePoolStats {
41 const fn new() -> Self {
42 Self {
43 slab_alloc_count: 0,
44 slab_return_count: 0,
45 global_alloc_fallback_count: 0,
46 slab_high_water_mark: 0,
47 }
48 }
49}
50
51#[cfg(test)]
52thread_local! {
53 static VALUE_POOL_TEST_STATS: RefCell<ValuePoolStats> =
54 const { RefCell::new(ValuePoolStats::new()) };
55}
56
57#[cfg(test)]
58fn reset_value_pool_test_stats() {
59 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow_mut() = ValuePoolStats::new());
60}
61
62#[cfg(test)]
63fn value_pool_test_stats_snapshot() -> ValuePoolStats {
64 VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow())
65}
66
67#[cfg(test)]
68fn record_value_pool_acquire(hit: bool) {
69 VALUE_POOL_TEST_STATS.with(|stats| {
70 let mut stats = stats.borrow_mut();
71 if hit {
72 stats.slab_alloc_count += 1;
73 } else {
74 stats.global_alloc_fallback_count += 1;
75 }
76 });
77}
78
79#[cfg(test)]
80fn record_value_pool_return(pool_len: usize) {
81 VALUE_POOL_TEST_STATS.with(|stats| {
82 let mut stats = stats.borrow_mut();
83 stats.slab_return_count += 1;
84 stats.slab_high_water_mark = stats.slab_high_water_mark.max(pool_len);
85 });
86}
87
88#[inline]
101pub fn pool_acquire() -> Option<SqliteValue> {
102 let value = VALUE_POOL.with(|pool| pool.borrow_mut().pop());
103 #[cfg(test)]
104 record_value_pool_acquire(value.is_some());
105 value
106}
107
108#[inline]
116pub fn pool_return(value: SqliteValue) {
117 VALUE_POOL.with(|pool| {
118 let mut pool = pool.borrow_mut();
119 if pool.len() < VALUE_POOL_CAP {
120 pool.push(value);
121 #[cfg(test)]
122 record_value_pool_return(pool.len());
123 }
124 });
126}
127
128#[inline]
131pub fn pool_return_reusable(value: SqliteValue) {
132 if value_preserves_reusable_heap_storage(&value) {
133 pool_return(value);
134 }
135}
136
137#[inline]
142pub fn pool_clear() {
143 VALUE_POOL.with(|pool| pool.borrow_mut().clear());
144}
145
146#[inline]
150pub fn pool_len() -> usize {
151 VALUE_POOL.with(|pool| pool.borrow().len())
152}
153
154#[inline]
155fn value_preserves_reusable_heap_storage(value: &SqliteValue) -> bool {
156 match value {
157 SqliteValue::Text(text) => matches!(&text.repr, SmallTextRepr::HeapOwned { .. }),
158 SqliteValue::Blob(bytes) => Arc::strong_count(bytes) == 1,
159 _ => false,
160 }
161}
162
163const SMALL_TEXT_INLINE_CAP: usize = 23;
171
172pub struct SmallText {
179 repr: SmallTextRepr,
181}
182
183enum SmallTextRepr {
185 Inline {
187 len: u8,
188 buf: [u8; SMALL_TEXT_INLINE_CAP],
189 },
190 HeapOwned {
195 text: String,
196 shared: OnceLock<Arc<str>>,
197 },
198 HeapShared(Arc<str>),
200}
201
202impl Clone for SmallText {
203 fn clone(&self) -> Self {
204 Self {
205 repr: self.repr.clone(),
206 }
207 }
208}
209
210impl Clone for SmallTextRepr {
211 fn clone(&self) -> Self {
212 match self {
213 Self::Inline { len, buf } => Self::Inline {
214 len: *len,
215 buf: *buf,
216 },
217 Self::HeapOwned { text, shared } => {
218 let shared = Arc::clone(shared.get_or_init(|| Arc::from(text.as_str())));
219 Self::HeapShared(shared)
220 }
221 Self::HeapShared(text) => Self::HeapShared(Arc::clone(text)),
222 }
223 }
224}
225
226impl SmallText {
227 #[inline]
229 pub fn new(s: &str) -> Self {
230 if s.len() <= SMALL_TEXT_INLINE_CAP {
231 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
232 buf[..s.len()].copy_from_slice(s.as_bytes());
233 Self {
234 repr: SmallTextRepr::Inline {
235 len: s.len() as u8,
236 buf,
237 },
238 }
239 } else {
240 Self {
241 repr: SmallTextRepr::HeapOwned {
242 text: s.to_owned(),
243 shared: OnceLock::new(),
244 },
245 }
246 }
247 }
248
249 #[inline]
251 pub fn from_string<S>(s: S) -> Self
252 where
253 S: Into<String> + AsRef<str>,
254 {
255 if s.as_ref().len() <= SMALL_TEXT_INLINE_CAP {
256 Self::new(s.as_ref())
257 } else {
258 Self {
259 repr: SmallTextRepr::HeapOwned {
260 text: s.into(),
261 shared: OnceLock::new(),
262 },
263 }
264 }
265 }
266
267 #[inline]
269 pub fn from_arc(arc: Arc<str>) -> Self {
270 if arc.len() <= SMALL_TEXT_INLINE_CAP {
271 Self::new(&arc)
272 } else {
273 Self {
274 repr: SmallTextRepr::HeapShared(arc),
275 }
276 }
277 }
278
279 #[inline]
282 pub fn overwrite(&mut self, s: &str) {
283 if s.len() <= SMALL_TEXT_INLINE_CAP {
284 let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
285 buf[..s.len()].copy_from_slice(s.as_bytes());
286 self.repr = SmallTextRepr::Inline {
287 len: s.len() as u8,
288 buf,
289 };
290 return;
291 }
292
293 match &mut self.repr {
294 SmallTextRepr::HeapOwned { text, shared } => {
295 text.clear();
296 text.push_str(s);
297 if shared.get().is_some() {
298 *shared = OnceLock::new();
299 }
300 }
301 _ => {
302 self.repr = SmallTextRepr::HeapOwned {
303 text: s.to_owned(),
304 shared: OnceLock::new(),
305 };
306 }
307 }
308 }
309
310 #[inline]
320 pub fn as_str(&self) -> &str {
321 match &self.repr {
322 SmallTextRepr::Inline { len, buf } => simdutf8::basic::from_utf8(&buf[..*len as usize])
323 .expect("SmallText inline representation must always contain valid UTF-8"),
324 SmallTextRepr::HeapOwned { text, .. } => text.as_str(),
325 SmallTextRepr::HeapShared(text) => text,
326 }
327 }
328
329 #[inline]
343 #[must_use]
344 pub fn as_bytes_direct(&self) -> &[u8] {
345 match &self.repr {
346 SmallTextRepr::Inline { len, buf } => &buf[..*len as usize],
347 SmallTextRepr::HeapOwned { text, .. } => text.as_bytes(),
348 SmallTextRepr::HeapShared(text) => text.as_bytes(),
349 }
350 }
351
352 #[inline]
354 pub fn len(&self) -> usize {
355 match &self.repr {
356 SmallTextRepr::Inline { len, .. } => *len as usize,
357 SmallTextRepr::HeapOwned { text, .. } => text.len(),
358 SmallTextRepr::HeapShared(text) => text.len(),
359 }
360 }
361
362 #[inline]
364 pub fn is_empty(&self) -> bool {
365 self.len() == 0
366 }
367
368 #[inline]
370 pub fn is_inline(&self) -> bool {
371 matches!(&self.repr, SmallTextRepr::Inline { .. })
372 }
373
374 #[inline]
376 pub fn into_arc(self) -> Arc<str> {
377 match self.repr {
378 SmallTextRepr::Inline { len, buf } => {
379 let s = simdutf8::basic::from_utf8(&buf[..len as usize])
381 .expect("SmallText inline representation must always contain valid UTF-8");
382 Arc::from(s)
383 }
384 SmallTextRepr::HeapOwned { text, shared } => shared
385 .into_inner()
386 .unwrap_or_else(|| Arc::<str>::from(text)),
387 SmallTextRepr::HeapShared(text) => text,
388 }
389 }
390}
391
392impl Default for SmallText {
393 #[inline]
394 fn default() -> Self {
395 Self::new("")
396 }
397}
398
399impl fmt::Debug for SmallText {
400 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401 fmt::Debug::fmt(self.as_str(), f)
402 }
403}
404
405impl fmt::Display for SmallText {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 fmt::Display::fmt(self.as_str(), f)
408 }
409}
410
411impl PartialEq for SmallText {
412 #[inline]
413 fn eq(&self, other: &Self) -> bool {
414 self.as_str() == other.as_str()
415 }
416}
417
418impl Eq for SmallText {}
419
420impl PartialOrd for SmallText {
421 #[inline]
422 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
423 Some(self.cmp(other))
424 }
425}
426
427impl Ord for SmallText {
428 #[inline]
429 fn cmp(&self, other: &Self) -> Ordering {
430 self.as_str().cmp(other.as_str())
431 }
432}
433
434impl Hash for SmallText {
435 #[inline]
436 fn hash<H: Hasher>(&self, state: &mut H) {
437 self.as_str().hash(state);
438 }
439}
440
441impl From<&str> for SmallText {
442 #[inline]
443 fn from(s: &str) -> Self {
444 Self::new(s)
445 }
446}
447
448impl From<String> for SmallText {
449 #[inline]
450 fn from(s: String) -> Self {
451 Self::from_string(s)
452 }
453}
454
455impl From<Arc<str>> for SmallText {
456 #[inline]
457 fn from(arc: Arc<str>) -> Self {
458 Self::from_arc(arc)
459 }
460}
461
462impl AsRef<str> for SmallText {
463 #[inline]
464 fn as_ref(&self) -> &str {
465 self.as_str()
466 }
467}
468
469impl std::ops::Deref for SmallText {
470 type Target = str;
471
472 #[inline]
473 fn deref(&self) -> &Self::Target {
474 self.as_str()
475 }
476}
477
478impl std::borrow::Borrow<str> for SmallText {
479 #[inline]
480 fn borrow(&self) -> &str {
481 self.as_str()
482 }
483}
484
485impl serde::Serialize for SmallText {
487 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488 where
489 S: serde::Serializer,
490 {
491 serializer.serialize_str(self.as_str())
492 }
493}
494
495impl<'de> serde::Deserialize<'de> for SmallText {
496 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497 where
498 D: serde::Deserializer<'de>,
499 {
500 let s = String::deserialize(deserializer)?;
501 Ok(Self::from_string(s))
502 }
503}
504
505fn scan_numeric_prefix(bytes: &[u8]) -> usize {
511 if bytes.is_empty() {
512 return 0;
513 }
514
515 let mut i = 0usize;
516 if bytes[i] == b'+' || bytes[i] == b'-' {
517 i += 1;
518 }
519
520 let mut has_digit = false;
521 while i < bytes.len() && bytes[i].is_ascii_digit() {
522 has_digit = true;
523 i += 1;
524 }
525
526 if i < bytes.len() && bytes[i] == b'.' {
527 i += 1;
528 while i < bytes.len() && bytes[i].is_ascii_digit() {
529 has_digit = true;
530 i += 1;
531 }
532 }
533
534 if !has_digit {
535 return 0;
536 }
537
538 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
539 let exp_start = i;
540 i += 1;
541 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
542 i += 1;
543 }
544 if i < bytes.len() && bytes[i].is_ascii_digit() {
545 while i < bytes.len() && bytes[i].is_ascii_digit() {
546 i += 1;
547 }
548 } else {
549 i = exp_start;
550 }
551 }
552
553 i
554}
555
556#[allow(clippy::cast_possible_truncation)]
558fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
559 let mut start = 0;
560 while start < b.len() && b[start].is_ascii_whitespace() {
561 start += 1;
562 }
563 let trimmed = &b[start..];
564 let end = scan_numeric_prefix(trimmed);
565 if end == 0 {
566 return 0;
567 }
568 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
571 let f = s.parse::<f64>().unwrap_or(0.0);
572 #[allow(clippy::manual_clamp)]
573 if f >= i64::MAX as f64 {
574 i64::MAX
575 } else if f <= i64::MIN as f64 {
576 i64::MIN
577 } else {
578 f as i64
579 }
580}
581
582#[allow(clippy::cast_possible_truncation)]
584fn parse_integer_prefix(s: &str) -> i64 {
585 parse_integer_prefix_bytes(s.as_bytes())
586}
587
588fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
590 let mut start = 0;
591 while start < b.len() && b[start].is_ascii_whitespace() {
592 start += 1;
593 }
594 let trimmed = &b[start..];
595 let end = scan_numeric_prefix(trimmed);
596 if end == 0 {
597 return 0.0;
598 }
599 let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
602 s.parse::<f64>().unwrap_or(0.0)
603}
604
605fn parse_float_prefix(s: &str) -> f64 {
607 parse_float_prefix_bytes(s.as_bytes())
608}
609
610fn trim_sqlite_ascii_whitespace(s: &str) -> &str {
611 s.trim_matches(|ch: char| ch.is_ascii_whitespace())
612}
613
614fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
615 let trimmed = trim_sqlite_ascii_whitespace(s);
616 let end = scan_numeric_prefix(trimmed.as_bytes());
617 if end == 0 {
618 return SqliteValue::Integer(0);
619 }
620
621 let prefix = &trimmed[..end];
622 let is_integer_syntax = !prefix
623 .as_bytes()
624 .iter()
625 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
626
627 if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
628 return SqliteValue::Integer(value);
629 }
630
631 if let Ok(value) = prefix.parse::<f64>() {
632 if value.is_finite()
633 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
634 {
635 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
636 let truncated = value as i64;
637 #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
638 if truncated as f64 == value {
639 return SqliteValue::Integer(truncated);
640 }
641 }
642 return SqliteValue::Float(value);
643 }
644
645 SqliteValue::Integer(0)
646}
647
648#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
653pub enum SqliteValue {
654 Null,
656 Integer(i64),
658 Float(f64),
660 Text(SmallText),
666 Blob(Arc<[u8]>),
670}
671
672impl SqliteValue {
673 pub const fn affinity(&self) -> TypeAffinity {
675 match self {
676 Self::Null | Self::Blob(_) => TypeAffinity::Blob,
677 Self::Integer(_) => TypeAffinity::Integer,
678 Self::Float(_) => TypeAffinity::Real,
679 Self::Text(_) => TypeAffinity::Text,
680 }
681 }
682
683 pub const fn storage_class(&self) -> StorageClass {
685 match self {
686 Self::Null => StorageClass::Null,
687 Self::Integer(_) => StorageClass::Integer,
688 Self::Float(_) => StorageClass::Real,
689 Self::Text(_) => StorageClass::Text,
690 Self::Blob(_) => StorageClass::Blob,
691 }
692 }
693
694 #[must_use]
706 #[allow(
707 clippy::cast_possible_truncation,
708 clippy::cast_precision_loss,
709 clippy::float_cmp
710 )]
711 pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
712 match affinity {
713 TypeAffinity::Blob => self,
714 TypeAffinity::Text => match self {
715 Self::Null | Self::Text(_) | Self::Blob(_) => self,
716 Self::Integer(_) | Self::Float(_) => {
717 let t = self.to_text();
718 Self::Text(SmallText::from_string(t))
719 }
720 },
721 TypeAffinity::Numeric | TypeAffinity::Integer => match &self {
722 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
723 Self::Float(f) => {
724 if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
725 let i = *f as i64;
726 if (i as f64) == *f {
727 return Self::Integer(i);
728 }
729 }
730 self
731 }
732 _ => self,
733 },
734 TypeAffinity::Real => match &self {
735 Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
736 .map(|v| match v {
737 Self::Integer(i) => Self::Float(i as f64),
738 other => other,
739 })
740 .unwrap_or(self),
741 Self::Integer(i) => Self::Float(*i as f64),
742 _ => self,
743 },
744 }
745 }
746
747 #[allow(clippy::cast_precision_loss)]
754 pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
755 if matches!(self, Self::Null) {
756 return Ok(self);
757 }
758 match col_type {
759 StrictColumnType::Any => Ok(self),
760 StrictColumnType::Integer => match self {
761 Self::Integer(_) => Ok(self),
762 other => Err(StrictTypeError {
763 expected: col_type,
764 actual: other.storage_class(),
765 }),
766 },
767 StrictColumnType::Real => match self {
768 Self::Float(_) => Ok(self),
769 Self::Integer(i) => Ok(Self::Float(i as f64)),
770 other => Err(StrictTypeError {
771 expected: col_type,
772 actual: other.storage_class(),
773 }),
774 },
775 StrictColumnType::Text => match self {
776 Self::Text(_) => Ok(self),
777 other => Err(StrictTypeError {
778 expected: col_type,
779 actual: other.storage_class(),
780 }),
781 },
782 StrictColumnType::Blob => match self {
783 Self::Blob(_) => Ok(self),
784 other => Err(StrictTypeError {
785 expected: col_type,
786 actual: other.storage_class(),
787 }),
788 },
789 }
790 }
791
792 #[inline(always)]
794 #[allow(clippy::inline_always)]
795 pub const fn is_null(&self) -> bool {
796 matches!(self, Self::Null)
797 }
798
799 #[inline]
801 pub const fn as_integer(&self) -> Option<i64> {
802 match self {
803 Self::Integer(i) => Some(*i),
804 _ => None,
805 }
806 }
807
808 #[inline]
810 pub fn as_float(&self) -> Option<f64> {
811 match self {
812 Self::Float(f) => Some(*f),
813 _ => None,
814 }
815 }
816
817 #[inline]
819 pub fn as_text(&self) -> Option<&str> {
820 match self {
821 Self::Text(s) => Some(s),
822 _ => None,
823 }
824 }
825
826 #[inline]
828 pub fn as_blob(&self) -> Option<&[u8]> {
829 match self {
830 Self::Blob(b) => Some(b),
831 _ => None,
832 }
833 }
834
835 #[inline(always)]
843 #[allow(clippy::inline_always)]
844 #[allow(clippy::cast_possible_truncation)]
845 pub fn to_integer(&self) -> i64 {
846 match self {
847 Self::Null => 0,
848 Self::Integer(i) => *i,
849 Self::Float(f) => *f as i64,
850 Self::Text(s) => parse_integer_prefix(s),
851 Self::Blob(b) => parse_integer_prefix_bytes(b),
852 }
853 }
854
855 #[inline(always)]
863 #[allow(clippy::inline_always)]
864 #[allow(clippy::cast_precision_loss)]
865 pub fn to_float(&self) -> f64 {
866 match self {
867 Self::Null => 0.0,
868 Self::Integer(i) => *i as f64,
869 Self::Float(f) => *f,
870 Self::Text(s) => parse_float_prefix(s),
871 Self::Blob(b) => parse_float_prefix_bytes(b),
872 }
873 }
874
875 #[must_use]
882 pub fn to_sum_numeric_value(&self) -> Self {
883 match self {
884 Self::Null => Self::Null,
885 Self::Integer(i) => Self::Integer(*i),
886 Self::Float(f) => Self::Float(*f),
887 Self::Text(s) => {
888 let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
889 if let Ok(integer) = trimmed.parse::<i64>() {
890 Self::Integer(integer)
891 } else {
892 Self::Float(parse_float_prefix(s))
893 }
894 }
895 Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
896 }
897 }
898
899 #[inline]
905 #[must_use]
906 pub fn as_text_str(&self) -> Option<&str> {
907 match self {
908 Self::Text(s) => Some(s),
909 _ => None,
910 }
911 }
912
913 #[inline]
915 #[must_use]
916 pub fn as_blob_bytes(&self) -> Option<&[u8]> {
917 match self {
918 Self::Blob(b) => Some(b),
919 _ => None,
920 }
921 }
922
923 pub fn to_text(&self) -> String {
929 match self {
930 Self::Null => String::new(),
931 Self::Integer(i) => i.to_string(),
932 Self::Float(f) => format_sqlite_float(*f),
933 Self::Text(s) => s.to_string(),
934 Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
935 }
936 }
937
938 #[must_use]
944 pub fn cast_to_numeric(&self) -> Self {
945 match self {
946 Self::Null => Self::Null,
947 Self::Integer(i) => Self::Integer(*i),
948 Self::Float(f) => Self::Float(*f),
949 Self::Text(s) => cast_text_prefix_to_numeric(s),
950 Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
951 }
952 }
953
954 pub const fn typeof_str(&self) -> &'static str {
958 match self {
959 Self::Null => "null",
960 Self::Integer(_) => "integer",
961 Self::Float(_) => "real",
962 Self::Text(_) => "text",
963 Self::Blob(_) => "blob",
964 }
965 }
966
967 pub fn sql_length(&self) -> Option<i64> {
974 match self {
975 Self::Null => None,
976 Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
977 Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
978 Self::Integer(_) | Self::Float(_) => {
979 let t = self.to_text();
980 Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
981 }
982 }
983 }
984
985 pub fn unique_eq(&self, other: &Self) -> bool {
991 if self.is_null() || other.is_null() {
992 return false;
993 }
994 matches!(self.partial_cmp(other), Some(Ordering::Equal))
995 }
996
997 fn float_result_or_null(result: f64) -> Self {
1001 if result.is_nan() {
1002 Self::Null
1003 } else {
1004 Self::Float(result)
1005 }
1006 }
1007
1008 #[inline]
1014 pub fn is_integer_numeric_type(&self) -> bool {
1015 fn text_is_integer_numeric_type(s: &str) -> bool {
1016 let trimmed = s.trim_start();
1017 let end = scan_numeric_prefix(trimmed.as_bytes());
1018 end > 0
1019 && !trimmed.as_bytes()[..end]
1020 .iter()
1021 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1022 }
1023
1024 match self {
1025 Self::Integer(_) => true,
1026 Self::Float(_) | Self::Null => false,
1027 Self::Text(s) => text_is_integer_numeric_type(s),
1028 Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1029 }
1030 }
1031
1032 #[inline]
1037 fn is_float_numeric_type(&self) -> bool {
1038 fn text_is_float(s: &str) -> bool {
1039 let trimmed = s.trim_start();
1040 let end = scan_numeric_prefix(trimmed.as_bytes());
1041 end > 0
1042 && trimmed.as_bytes()[..end]
1043 .iter()
1044 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1045 }
1046 match self {
1047 Self::Float(_) => true,
1048 Self::Integer(_) | Self::Null => false,
1049 Self::Text(s) => text_is_float(s),
1050 Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1051 }
1052 }
1053
1054 #[inline(always)]
1062 #[allow(clippy::inline_always)]
1063 #[must_use]
1064 #[allow(clippy::cast_precision_loss)]
1065 pub fn sql_add(&self, other: &Self) -> Self {
1066 match (self, other) {
1067 (Self::Null, _) | (_, Self::Null) => Self::Null,
1068 (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1069 Some(result) => Self::Integer(result),
1070 None => Self::float_result_or_null(*a as f64 + *b as f64),
1071 },
1072 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1076 let a = self.to_integer();
1077 let b = other.to_integer();
1078 match a.checked_add(b) {
1079 Some(result) => Self::Integer(result),
1080 None => Self::float_result_or_null(a as f64 + b as f64),
1081 }
1082 }
1083 _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1084 }
1085 }
1086
1087 #[inline(always)]
1091 #[allow(clippy::inline_always)]
1092 #[must_use]
1093 #[allow(clippy::cast_precision_loss)]
1094 pub fn sql_sub(&self, other: &Self) -> Self {
1095 match (self, other) {
1096 (Self::Null, _) | (_, Self::Null) => Self::Null,
1097 (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1098 Some(result) => Self::Integer(result),
1099 None => Self::float_result_or_null(*a as f64 - *b as f64),
1100 },
1101 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1102 let a = self.to_integer();
1103 let b = other.to_integer();
1104 match a.checked_sub(b) {
1105 Some(result) => Self::Integer(result),
1106 None => Self::float_result_or_null(a as f64 - b as f64),
1107 }
1108 }
1109 _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1110 }
1111 }
1112
1113 #[inline(always)]
1117 #[allow(clippy::inline_always)]
1118 #[must_use]
1119 #[allow(clippy::cast_precision_loss)]
1120 pub fn sql_mul(&self, other: &Self) -> Self {
1121 match (self, other) {
1122 (Self::Null, _) | (_, Self::Null) => Self::Null,
1123 (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1124 Some(result) => Self::Integer(result),
1125 None => Self::float_result_or_null(*a as f64 * *b as f64),
1126 },
1127 (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1128 (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1129 (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1130 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1131 let a = self.to_integer();
1132 let b = other.to_integer();
1133 match a.checked_mul(b) {
1134 Some(result) => Self::Integer(result),
1135 None => Self::float_result_or_null(a as f64 * b as f64),
1136 }
1137 }
1138 _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1139 }
1140 }
1141
1142 const fn sort_class(&self) -> u8 {
1144 match self {
1145 Self::Null => 0,
1146 Self::Integer(_) | Self::Float(_) => 1,
1147 Self::Text(_) => 2,
1148 Self::Blob(_) => 3,
1149 }
1150 }
1151}
1152
1153pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1161 assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1162 a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1163}
1164
1165pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1172 if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1173 return sql_like_fast_path_matches(kind, literal, text);
1174 }
1175
1176 sql_like_inner(
1177 &pattern.chars().collect::<Vec<_>>(),
1178 &text.chars().collect::<Vec<_>>(),
1179 escape,
1180 0,
1181 0,
1182 )
1183}
1184
1185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1186pub enum SqlLikeFastPathKind {
1187 MatchAll,
1188 Exact,
1189 Prefix,
1190 Suffix,
1191 Contains,
1192}
1193
1194impl SqlLikeFastPathKind {
1195 #[must_use]
1196 pub const fn opcode_tag(self) -> i32 {
1197 match self {
1198 Self::MatchAll => 0,
1199 Self::Exact => 1,
1200 Self::Prefix => 2,
1201 Self::Suffix => 3,
1202 Self::Contains => 4,
1203 }
1204 }
1205
1206 #[must_use]
1207 pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1208 match tag {
1209 0 => Some(Self::MatchAll),
1210 1 => Some(Self::Exact),
1211 2 => Some(Self::Prefix),
1212 3 => Some(Self::Suffix),
1213 4 => Some(Self::Contains),
1214 _ => None,
1215 }
1216 }
1217}
1218
1219#[must_use]
1220pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1221 match kind {
1222 SqlLikeFastPathKind::MatchAll => true,
1223 SqlLikeFastPathKind::Exact => ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes()),
1224 SqlLikeFastPathKind::Prefix => ascii_ci_starts_with(text, literal),
1225 SqlLikeFastPathKind::Suffix => ascii_ci_ends_with(text, literal),
1226 SqlLikeFastPathKind::Contains => ascii_ci_contains(text, literal),
1227 }
1228}
1229
1230pub struct SqlLikeFastPathMatcher<'a> {
1232 kind: SqlLikeFastPathKind,
1233 literal: &'a str,
1234 contains_finder: Option<memmem::Finder<'a>>,
1235}
1236
1237impl<'a> SqlLikeFastPathMatcher<'a> {
1238 #[must_use]
1239 pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1240 let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1241 .then(|| memmem::Finder::new(literal.as_bytes()));
1242 Self {
1243 kind,
1244 literal,
1245 contains_finder,
1246 }
1247 }
1248
1249 #[must_use]
1250 pub fn matches(&self, text: &str) -> bool {
1251 if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1252 let text_bytes = text.as_bytes();
1253 let needle_bytes = self.literal.as_bytes();
1254 if needle_bytes.len() > text_bytes.len() {
1255 return false;
1256 }
1257 if finder.find(text_bytes).is_some() {
1258 return true;
1259 }
1260 return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1261 }
1262 sql_like_fast_path_matches(self.kind, self.literal, text)
1263 }
1264}
1265
1266#[must_use]
1267pub fn classify_sql_like_fast_path(
1268 pattern: &str,
1269 escape: Option<char>,
1270) -> Option<(SqlLikeFastPathKind, &str)> {
1271 if escape.is_some() || pattern.contains('_') {
1272 return None;
1273 }
1274 if !pattern.contains('%') {
1275 return Some((SqlLikeFastPathKind::Exact, pattern));
1276 }
1277 if pattern.chars().all(|ch| ch == '%') {
1278 return Some((SqlLikeFastPathKind::MatchAll, ""));
1279 }
1280
1281 let trimmed_start = pattern.trim_start_matches('%');
1282 let trimmed_end = pattern.trim_end_matches('%');
1283 if pattern.starts_with('%') && pattern.ends_with('%') {
1284 let core = trimmed_start.trim_end_matches('%');
1285 if core.is_empty() {
1286 return Some((SqlLikeFastPathKind::MatchAll, ""));
1287 }
1288 if !core.contains('%') {
1289 return Some((SqlLikeFastPathKind::Contains, core));
1290 }
1291 }
1292 if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1293 {
1294 return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1295 }
1296 if !pattern.ends_with('%')
1297 && trimmed_start.len() < pattern.len()
1298 && !trimmed_start.contains('%')
1299 {
1300 return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1301 }
1302 None
1303}
1304
1305fn sql_like_inner(
1306 pattern: &[char],
1307 text: &[char],
1308 escape: Option<char>,
1309 pi: usize,
1310 ti: usize,
1311) -> bool {
1312 let mut pi = pi;
1313 let mut ti = ti;
1314
1315 while pi < pattern.len() {
1316 let pc = pattern[pi];
1317
1318 if Some(pc) == escape {
1320 pi += 1;
1321 if pi >= pattern.len() {
1322 return false; }
1324 if ti >= text.len() || !ascii_ci_eq(pattern[pi], text[ti]) {
1326 return false;
1327 }
1328 pi += 1;
1329 ti += 1;
1330 continue;
1331 }
1332
1333 match pc {
1334 '%' => {
1335 while pi < pattern.len() && pattern[pi] == '%' {
1337 pi += 1;
1338 }
1339 if pi >= pattern.len() {
1341 return true;
1342 }
1343 for start in ti..=text.len() {
1345 if sql_like_inner(pattern, text, escape, pi, start) {
1346 return true;
1347 }
1348 }
1349 return false;
1350 }
1351 '_' => {
1352 if ti >= text.len() {
1353 return false;
1354 }
1355 pi += 1;
1356 ti += 1;
1357 }
1358 _ => {
1359 if ti >= text.len() || !ascii_ci_eq(pc, text[ti]) {
1360 return false;
1361 }
1362 pi += 1;
1363 ti += 1;
1364 }
1365 }
1366 }
1367 ti >= text.len()
1368}
1369
1370fn ascii_ci_eq(a: char, b: char) -> bool {
1372 if a == b {
1373 return true;
1374 }
1375 a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1377}
1378
1379#[inline]
1380fn ascii_fold_byte(byte: u8) -> u8 {
1381 byte.to_ascii_lowercase()
1382}
1383
1384#[inline]
1385fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1386 left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1387}
1388
1389fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1390 if left.len() != right.len() {
1391 return false;
1392 }
1393 let mut idx = 0;
1394 while idx < left.len() {
1395 if !ascii_ci_eq_byte(left[idx], right[idx]) {
1396 return false;
1397 }
1398 idx += 1;
1399 }
1400 true
1401}
1402
1403fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1404 let text = text.as_bytes();
1405 let prefix = prefix.as_bytes();
1406 text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1407}
1408
1409fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1410 let text = text.as_bytes();
1411 let suffix = suffix.as_bytes();
1412 text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1413}
1414
1415fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1416 let text = text.as_bytes();
1417 let needle = needle.as_bytes();
1418 if needle.is_empty() {
1419 return true;
1420 }
1421 if needle.len() > text.len() {
1422 return false;
1423 }
1424 if memmem::find(text, needle).is_some() {
1425 return true;
1426 }
1427
1428 ascii_ci_contains_folded_scan(text, needle)
1429}
1430
1431fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1432 if needle.is_empty() {
1433 return true;
1434 }
1435 if needle.len() > text.len() {
1436 return false;
1437 }
1438 let max_start = text.len() - needle.len();
1439 let first = needle[0];
1440 let first_folded = ascii_fold_byte(first);
1441 let first_alt = if first.is_ascii_alphabetic() {
1442 first_folded.to_ascii_uppercase()
1443 } else {
1444 first_folded
1445 };
1446 let mut start = 0;
1447 while start <= max_start {
1448 let rel = if first_folded == first_alt {
1449 memchr(first_folded, &text[start..=max_start])
1450 } else {
1451 memchr2(first_folded, first_alt, &text[start..=max_start])
1452 };
1453 let Some(rel) = rel else {
1454 break;
1455 };
1456 start += rel;
1457 if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1458 return true;
1459 }
1460 start += 1;
1461 }
1462 false
1463}
1464
1465#[derive(Debug, Clone)]
1472pub struct SumAccumulator {
1473 int_sum: i64,
1475 float_sum: f64,
1478 float_err: f64,
1480 has_value: bool,
1482 is_float: bool,
1484 overflow: bool,
1486}
1487
1488impl Default for SumAccumulator {
1489 fn default() -> Self {
1490 Self::new()
1491 }
1492}
1493
1494#[inline]
1496fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1497 let s = *sum;
1498 let t = s + value;
1499 if s.abs() > value.abs() {
1500 *err += (s - t) + value;
1501 } else {
1502 *err += (value - t) + s;
1503 }
1504 *sum = t;
1505}
1506
1507impl SumAccumulator {
1508 pub const fn new() -> Self {
1510 Self {
1511 int_sum: 0,
1512 float_sum: 0.0,
1513 float_err: 0.0,
1514 has_value: false,
1515 is_float: false,
1516 overflow: false,
1517 }
1518 }
1519
1520 #[allow(clippy::cast_precision_loss)]
1522 pub fn accumulate(&mut self, val: &SqliteValue) {
1523 match val.to_sum_numeric_value() {
1524 SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1525 SqliteValue::Integer(i) => {
1526 self.has_value = true;
1527 if !self.is_float && !self.overflow {
1528 match self.int_sum.checked_add(i) {
1529 Some(result) => self.int_sum = result,
1530 None => self.overflow = true,
1531 }
1532 }
1533 kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1534 }
1535 SqliteValue::Float(f) => {
1536 self.has_value = true;
1537 self.is_float = true;
1538 kbn_step(&mut self.float_sum, &mut self.float_err, f);
1539 }
1540 }
1541 }
1542
1543 pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1547 if !self.is_float && self.overflow {
1548 return Err(SumOverflowError);
1549 }
1550 if !self.has_value {
1551 return Ok(SqliteValue::Null);
1552 }
1553 if self.is_float {
1554 Ok(SqliteValue::Float(self.float_sum + self.float_err))
1555 } else {
1556 Ok(SqliteValue::Integer(self.int_sum))
1557 }
1558 }
1559}
1560
1561#[derive(Debug, Clone, PartialEq, Eq)]
1563pub struct SumOverflowError;
1564
1565impl fmt::Display for SumOverflowError {
1566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1567 f.write_str("integer overflow in sum()")
1568 }
1569}
1570
1571impl fmt::Display for SqliteValue {
1572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1573 match self {
1574 Self::Null => f.write_str("NULL"),
1575 Self::Integer(i) => write!(f, "{i}"),
1576 Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1577 Self::Text(s) => write!(f, "'{s}'"),
1578 Self::Blob(b) => {
1579 f.write_str("X'")?;
1580 for byte in b.iter() {
1581 write!(f, "{byte:02X}")?;
1582 }
1583 f.write_str("'")
1584 }
1585 }
1586 }
1587}
1588
1589impl PartialEq for SqliteValue {
1590 fn eq(&self, other: &Self) -> bool {
1591 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1592 }
1593}
1594
1595impl Eq for SqliteValue {}
1596
1597impl PartialOrd for SqliteValue {
1598 #[inline]
1599 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1600 Some(self.cmp(other))
1601 }
1602}
1603
1604impl Ord for SqliteValue {
1605 #[inline]
1606 fn cmp(&self, other: &Self) -> Ordering {
1607 let class_a = self.sort_class();
1609 let class_b = other.sort_class();
1610
1611 if class_a != class_b {
1612 return class_a.cmp(&class_b);
1613 }
1614
1615 match (self, other) {
1616 (Self::Null, Self::Null) => Ordering::Equal,
1617 (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
1618 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
1619 (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
1620 (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
1621 (Self::Text(a), Self::Text(b)) => a.cmp(b),
1622 (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
1623 _ => unreachable!(),
1624 }
1625 }
1626}
1627
1628impl From<i64> for SqliteValue {
1629 fn from(i: i64) -> Self {
1630 Self::Integer(i)
1631 }
1632}
1633
1634impl From<i32> for SqliteValue {
1635 fn from(i: i32) -> Self {
1636 Self::Integer(i64::from(i))
1637 }
1638}
1639
1640impl From<f64> for SqliteValue {
1641 fn from(f: f64) -> Self {
1642 Self::float_result_or_null(f)
1643 }
1644}
1645
1646impl From<String> for SqliteValue {
1647 fn from(s: String) -> Self {
1648 Self::Text(SmallText::from_string(s))
1651 }
1652}
1653
1654impl From<&str> for SqliteValue {
1655 fn from(s: &str) -> Self {
1656 Self::Text(SmallText::new(s))
1657 }
1658}
1659
1660impl From<Arc<str>> for SqliteValue {
1661 fn from(s: Arc<str>) -> Self {
1662 Self::Text(SmallText::from_arc(s))
1663 }
1664}
1665
1666impl From<Vec<u8>> for SqliteValue {
1667 fn from(b: Vec<u8>) -> Self {
1668 Self::Blob(Arc::from(b))
1671 }
1672}
1673
1674impl From<&[u8]> for SqliteValue {
1675 fn from(b: &[u8]) -> Self {
1676 Self::Blob(Arc::from(b))
1677 }
1678}
1679
1680impl From<Arc<[u8]>> for SqliteValue {
1681 fn from(b: Arc<[u8]>) -> Self {
1682 Self::Blob(b)
1683 }
1684}
1685
1686impl<T: Into<Self>> From<Option<T>> for SqliteValue {
1687 fn from(opt: Option<T>) -> Self {
1688 match opt {
1689 Some(v) => v.into(),
1690 None => Self::Null,
1691 }
1692 }
1693}
1694
1695#[allow(
1699 clippy::cast_possible_truncation,
1700 clippy::cast_precision_loss,
1701 clippy::float_cmp
1702)]
1703fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
1704 let trimmed = trim_sqlite_ascii_whitespace(s);
1705 if trimmed.is_empty() {
1706 return None;
1707 }
1708 if let Ok(i) = trimmed.parse::<i64>() {
1710 return Some(SqliteValue::Integer(i));
1711 }
1712 if let Ok(f) = trimmed.parse::<f64>() {
1716 if !f.is_finite() {
1717 let lower = trimmed.to_ascii_lowercase();
1718 if lower.contains("inf") || lower.contains("nan") {
1719 return None;
1720 }
1721 }
1722 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
1725 #[allow(clippy::cast_possible_truncation)]
1726 let i = f as i64;
1727 #[allow(clippy::cast_precision_loss)]
1728 if (i as f64) == f {
1729 return Some(SqliteValue::Integer(i));
1730 }
1731 }
1732 return Some(SqliteValue::Float(f));
1733 }
1734 None
1735}
1736
1737#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1742pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
1743 if r.is_nan() {
1744 return Ordering::Greater;
1746 }
1747 if r < -9_223_372_036_854_775_808.0 {
1749 return Ordering::Greater;
1750 }
1751 if r >= 9_223_372_036_854_775_808.0 {
1752 return Ordering::Less;
1753 }
1754 let y = r as i64;
1756 match i.cmp(&y) {
1757 Ordering::Less => Ordering::Less,
1758 Ordering::Greater => Ordering::Greater,
1759 Ordering::Equal => {
1761 let s = i as f64;
1762 s.partial_cmp(&r).unwrap_or(Ordering::Equal)
1763 }
1764 }
1765}
1766
1767#[must_use]
1773pub fn format_sqlite_float(f: f64) -> String {
1774 if f.is_nan() {
1775 return "NaN".to_owned();
1776 }
1777 if f.is_infinite() {
1778 return if f.is_sign_positive() {
1779 "Inf".to_owned()
1780 } else {
1781 "-Inf".to_owned()
1782 };
1783 }
1784 let abs = f.abs();
1789 let s = if abs == 0.0 {
1790 if f.is_sign_negative() {
1792 "-0.0".to_owned()
1793 } else {
1794 "0.0".to_owned()
1795 }
1796 } else {
1797 let exp = abs.log10().floor() as i32;
1800 if exp >= 15 || exp < -4 {
1801 let mut s = format!("{f:.14e}");
1803 if let Some(e_pos) = s.find('e') {
1805 let mantissa = &s[..e_pos];
1806 let exp_str = &s[e_pos + 1..]; let trimmed = mantissa.trim_end_matches('0');
1808 let trimmed = if trimmed.ends_with('.') {
1810 format!("{trimmed}0")
1811 } else {
1812 trimmed.to_owned()
1813 };
1814 let (exp_sign, exp_digits) = if let Some(rest) = exp_str.strip_prefix('-') {
1817 ("-", rest)
1818 } else if let Some(rest) = exp_str.strip_prefix('+') {
1819 ("+", rest)
1820 } else {
1821 ("+", exp_str)
1822 };
1823 let exp_num: u32 = exp_digits.parse().unwrap_or(0);
1824 s = format!("{trimmed}e{exp_sign}{exp_num:02}");
1825 }
1826 s
1827 } else {
1828 #[allow(clippy::cast_sign_loss)]
1830 let decimal_places = (14 - exp).max(0) as usize;
1831 let mut s = format!("{f:.decimal_places$}");
1832 if s.contains('.') {
1834 let trimmed = s.trim_end_matches('0');
1835 s = if trimmed.ends_with('.') {
1836 format!("{trimmed}0")
1837 } else {
1838 trimmed.to_owned()
1839 };
1840 } else {
1841 s.push_str(".0");
1842 }
1843 s
1844 }
1845 };
1846 s
1847}
1848
1849#[cfg(test)]
1850#[allow(clippy::float_cmp, clippy::approx_constant)]
1851mod tests {
1852 use super::*;
1853
1854 struct ValuePoolTestGuard;
1855
1856 impl ValuePoolTestGuard {
1857 fn new() -> Self {
1858 pool_clear();
1859 reset_value_pool_test_stats();
1860 Self
1861 }
1862 }
1863
1864 impl Drop for ValuePoolTestGuard {
1865 fn drop(&mut self) {
1866 pool_clear();
1867 reset_value_pool_test_stats();
1868 }
1869 }
1870
1871 fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
1872 let stats = value_pool_test_stats_snapshot();
1873 eprintln!(
1874 "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
1875 stats.slab_alloc_count,
1876 stats.slab_return_count,
1877 stats.global_alloc_fallback_count,
1878 stats.slab_high_water_mark,
1879 pool_len(),
1880 );
1881 stats
1882 }
1883
1884 #[test]
1885 fn test_slab_basic_alloc_dealloc() {
1886 let _guard = ValuePoolTestGuard::new();
1887 const ROUND_TRIP_COUNT: usize = 100;
1888
1889 assert_eq!(pool_len(), 0);
1890 assert_eq!(pool_acquire(), None);
1891 assert_eq!(
1892 value_pool_test_stats_snapshot(),
1893 ValuePoolStats {
1894 slab_alloc_count: 0,
1895 slab_return_count: 0,
1896 global_alloc_fallback_count: 1,
1897 slab_high_water_mark: 0,
1898 }
1899 );
1900
1901 reset_value_pool_test_stats();
1902 for value in 0..ROUND_TRIP_COUNT {
1903 pool_return(SqliteValue::Integer(value as i64));
1904 }
1905 assert_eq!(pool_len(), ROUND_TRIP_COUNT);
1906 assert_eq!(
1907 value_pool_test_stats_snapshot(),
1908 ValuePoolStats {
1909 slab_alloc_count: 0,
1910 slab_return_count: ROUND_TRIP_COUNT,
1911 global_alloc_fallback_count: 0,
1912 slab_high_water_mark: ROUND_TRIP_COUNT,
1913 }
1914 );
1915
1916 reset_value_pool_test_stats();
1917 for expected in (0..ROUND_TRIP_COUNT).rev() {
1918 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
1919 }
1920 assert_eq!(pool_len(), 0);
1921 assert_eq!(
1922 log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
1923 ValuePoolStats {
1924 slab_alloc_count: ROUND_TRIP_COUNT,
1925 slab_return_count: 0,
1926 global_alloc_fallback_count: 0,
1927 slab_high_water_mark: 0,
1928 }
1929 );
1930 }
1931
1932 #[test]
1933 fn test_slab_exhaustion_fallback() {
1934 let _guard = ValuePoolTestGuard::new();
1935
1936 for value in 0..=VALUE_POOL_CAP {
1937 pool_return(SqliteValue::Integer(value as i64));
1938 }
1939 assert_eq!(pool_len(), VALUE_POOL_CAP);
1940 assert_eq!(
1941 value_pool_test_stats_snapshot(),
1942 ValuePoolStats {
1943 slab_alloc_count: 0,
1944 slab_return_count: VALUE_POOL_CAP,
1945 global_alloc_fallback_count: 0,
1946 slab_high_water_mark: VALUE_POOL_CAP,
1947 }
1948 );
1949
1950 reset_value_pool_test_stats();
1951 for _ in 0..VALUE_POOL_CAP {
1952 assert!(pool_acquire().is_some());
1953 }
1954 assert_eq!(pool_acquire(), None);
1955 assert_eq!(pool_len(), 0);
1956 assert_eq!(
1957 log_value_pool_test_stats("test_slab_exhaustion_fallback"),
1958 ValuePoolStats {
1959 slab_alloc_count: VALUE_POOL_CAP,
1960 slab_return_count: 0,
1961 global_alloc_fallback_count: 1,
1962 slab_high_water_mark: 0,
1963 }
1964 );
1965 }
1966
1967 #[test]
1968 fn test_slab_no_leak() {
1969 let _guard = ValuePoolTestGuard::new();
1970 const ITERATIONS: usize = 10_000;
1971
1972 let (weak_tx, weak_rx) = std::sync::mpsc::channel();
1973 let (release_tx, release_rx) = std::sync::mpsc::channel();
1974
1975 let worker = std::thread::spawn(move || {
1976 pool_clear();
1977 reset_value_pool_test_stats();
1978
1979 let mut pooled_weak = None;
1980 let mut overflow_weak = None;
1981 for value in 0..ITERATIONS {
1982 let payload: Arc<[u8]> =
1983 Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
1984 if value == 0 {
1985 pooled_weak = Some(Arc::downgrade(&payload));
1986 } else if value == ITERATIONS - 1 {
1987 overflow_weak = Some(Arc::downgrade(&payload));
1988 }
1989 pool_return(SqliteValue::Blob(payload));
1990 }
1991
1992 assert_eq!(
1993 pool_len(),
1994 VALUE_POOL_CAP,
1995 "the slab must retain at most VALUE_POOL_CAP entries",
1996 );
1997 weak_tx
1998 .send((
1999 pooled_weak.expect("capture pooled weak handle"),
2000 overflow_weak.expect("capture overflow weak handle"),
2001 log_value_pool_test_stats("test_slab_no_leak"),
2002 ))
2003 .expect("send slab leak stats");
2004 release_rx.recv().expect("wait for release");
2005 });
2006
2007 let (pooled_weak, overflow_weak, stats) =
2008 weak_rx.recv().expect("receive weak blob handles");
2009 assert!(
2010 pooled_weak.upgrade().is_some(),
2011 "pooled blob should remain alive while the owning thread is running"
2012 );
2013 assert!(
2014 overflow_weak.upgrade().is_none(),
2015 "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2016 );
2017 assert_eq!(
2018 stats,
2019 ValuePoolStats {
2020 slab_alloc_count: 0,
2021 slab_return_count: VALUE_POOL_CAP,
2022 global_alloc_fallback_count: 0,
2023 slab_high_water_mark: VALUE_POOL_CAP,
2024 }
2025 );
2026
2027 release_tx.send(()).expect("release worker thread");
2028 worker.join().expect("join worker");
2029
2030 assert!(
2031 pooled_weak.upgrade().is_none(),
2032 "thread-local slab contents must be dropped when the thread exits"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_slab_thread_local_isolation() {
2038 let _guard = ValuePoolTestGuard::new();
2039
2040 pool_return(SqliteValue::Integer(11));
2041 assert_eq!(pool_len(), 1);
2042
2043 let worker = std::thread::spawn(|| {
2044 pool_clear();
2045 reset_value_pool_test_stats();
2046
2047 assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2048 pool_return(SqliteValue::Integer(22));
2049 assert_eq!(pool_len(), 1);
2050 assert_eq!(
2051 value_pool_test_stats_snapshot(),
2052 ValuePoolStats {
2053 slab_alloc_count: 0,
2054 slab_return_count: 1,
2055 global_alloc_fallback_count: 0,
2056 slab_high_water_mark: 1,
2057 }
2058 );
2059 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
2060 assert_eq!(pool_len(), 0);
2061 });
2062 worker.join().expect("join worker");
2063
2064 assert_eq!(
2065 pool_len(),
2066 1,
2067 "worker thread slab operations must not affect the caller thread"
2068 );
2069 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
2070 assert_eq!(pool_len(), 0);
2071 let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
2072 assert_eq!(
2073 stats,
2074 ValuePoolStats {
2075 slab_alloc_count: 1,
2076 slab_return_count: 1,
2077 global_alloc_fallback_count: 0,
2078 slab_high_water_mark: 1,
2079 }
2080 );
2081 }
2082
2083 #[test]
2084 fn test_slab_zero_malloc_steady_state() {
2085 let _guard = ValuePoolTestGuard::new();
2086 const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
2087 const ITERATIONS: usize = 1_000;
2088 const INITIAL_TEXT: &str =
2089 "steady-state pooled string backing store for bd-nsvud warmup payload";
2090 const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
2091
2092 assert!(
2093 REUSED_TEXT.len() <= INITIAL_TEXT.len(),
2094 "steady-state overwrite must fit within the warmed heap allocation"
2095 );
2096
2097 for _ in 0..WARM_POOL_DEPTH {
2098 pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
2099 }
2100 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2101
2102 reset_value_pool_test_stats();
2103 for _ in 0..ITERATIONS {
2104 let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
2105 let SqliteValue::Text(existing) = &mut reused else {
2106 panic!("warmed slab entry should remain a text value");
2107 };
2108 let original_ptr = existing.as_str().as_ptr();
2109 existing.overwrite(REUSED_TEXT);
2110 assert_eq!(
2111 existing.as_str().as_ptr(),
2112 original_ptr,
2113 "steady-state overwrite should reuse the warmed heap buffer",
2114 );
2115 assert_eq!(existing.as_str(), REUSED_TEXT);
2116 pool_return(reused);
2117 }
2118
2119 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2120 assert_eq!(
2121 log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
2122 ValuePoolStats {
2123 slab_alloc_count: ITERATIONS,
2124 slab_return_count: ITERATIONS,
2125 global_alloc_fallback_count: 0,
2126 slab_high_water_mark: WARM_POOL_DEPTH,
2127 }
2128 );
2129 }
2130
2131 #[test]
2132 fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
2133 let text = SmallText::new("this string is definitely longer than twenty three bytes");
2134 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2135 panic!("long text should start in heap-owned mode");
2136 };
2137 assert!(
2138 shared.get().is_none(),
2139 "long text should not allocate Arc eagerly before cloning"
2140 );
2141
2142 let cloned = text.clone();
2143
2144 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2145 panic!("original text should remain heap-owned after clone");
2146 };
2147 assert!(
2148 shared.get().is_some(),
2149 "first clone should materialize a shared Arc lazily"
2150 );
2151 assert!(
2152 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2153 "cloned text should use the shared Arc representation"
2154 );
2155 assert_eq!(text.as_str(), cloned.as_str());
2156 }
2157
2158 #[test]
2159 fn test_small_text_overwrite_reuses_unique_heap_buffer() {
2160 let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
2161 let (original_ptr, original_capacity) = match &text.repr {
2162 SmallTextRepr::HeapOwned { text, shared } => {
2163 assert!(shared.get().is_none(), "fresh heap text should be unshared");
2164 (text.as_ptr(), text.capacity())
2165 }
2166 _ => panic!("long text should start in heap-owned mode"),
2167 };
2168
2169 text.overwrite("another long string that still fits the same allocation");
2170
2171 match &text.repr {
2172 SmallTextRepr::HeapOwned { text, shared } => {
2173 assert!(
2174 shared.get().is_none(),
2175 "overwrite should keep text single-owner"
2176 );
2177 assert_eq!(text.as_ptr(), original_ptr);
2178 assert_eq!(text.capacity(), original_capacity);
2179 assert_eq!(
2180 text.as_str(),
2181 "another long string that still fits the same allocation"
2182 );
2183 }
2184 _ => panic!("overwrite should keep long text in heap-owned mode"),
2185 }
2186 }
2187
2188 #[test]
2189 fn test_small_text_overwrite_detaches_from_shared_arc() {
2190 let original = "this string is definitely longer than twenty three bytes";
2191 let mut text = SmallText::new(original);
2192 let (original_ptr, original_capacity) = match &text.repr {
2193 SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
2194 _ => panic!("long text should start in heap-owned mode"),
2195 };
2196 let replacement = "replacement text that must not mutate the shared clone";
2197 assert!(
2198 replacement.len() <= original_capacity,
2199 "replacement should fit the original heap allocation for this regression",
2200 );
2201 let clone = text.clone();
2202
2203 text.overwrite(replacement);
2204
2205 assert_eq!(
2206 clone.as_str(),
2207 original,
2208 "existing shared clones must keep the original contents"
2209 );
2210 assert_eq!(text.as_str(), replacement);
2211 match &text.repr {
2212 SmallTextRepr::HeapOwned { text, shared } => {
2213 assert_eq!(
2214 text.as_ptr(),
2215 original_ptr,
2216 "overwriting a cloned long string should keep the owned buffer",
2217 );
2218 assert_eq!(
2219 text.capacity(),
2220 original_capacity,
2221 "detaching from the shared cache should preserve capacity",
2222 );
2223 assert!(
2224 shared.get().is_none(),
2225 "overwrite should reset the lazy shared cache after detaching"
2226 );
2227 }
2228 _ => panic!("overwrite should restore heap-owned mode"),
2229 }
2230 }
2231
2232 #[test]
2233 fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
2234 let _guard = ValuePoolTestGuard::new();
2235
2236 pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
2237 assert_eq!(
2238 pool_len(),
2239 0,
2240 "inline text should not occupy reusable slab slots",
2241 );
2242
2243 let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
2244 let _clone = owned_text.clone();
2245 pool_return_reusable(SqliteValue::Text(owned_text));
2246 assert_eq!(
2247 pool_len(),
2248 1,
2249 "heap-owned text should stay reusable even after serving shared clones",
2250 );
2251 assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
2252 assert_eq!(pool_len(), 0);
2253
2254 let shared_text =
2255 Arc::<str>::from("this string is definitely longer than twenty three bytes");
2256 pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
2257 &shared_text,
2258 ))));
2259 assert_eq!(
2260 pool_len(),
2261 0,
2262 "arc-backed shared text should not enter the reusable slab",
2263 );
2264
2265 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
2266 pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
2267 assert_eq!(
2268 pool_len(),
2269 0,
2270 "shared blob allocations should not displace reusable slab entries",
2271 );
2272
2273 let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
2274 pool_return_reusable(SqliteValue::Blob(unique_blob));
2275 assert_eq!(
2276 pool_len(),
2277 1,
2278 "unique blob allocations should remain eligible for slab reuse",
2279 );
2280 }
2281
2282 #[test]
2283 fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
2284 let text = Arc::new(SmallText::new(
2285 "this string is definitely longer than twenty three bytes",
2286 ));
2287 let expected = text.as_str().to_owned();
2288 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2289 panic!("long text should start in heap-owned mode");
2290 };
2291 assert!(
2292 shared.get().is_none(),
2293 "shared Arc should still be lazy before concurrent clones"
2294 );
2295
2296 let barrier = Arc::new(std::sync::Barrier::new(5));
2297 let mut workers = Vec::new();
2298 for _ in 0..4 {
2299 let text = Arc::clone(&text);
2300 let barrier = Arc::clone(&barrier);
2301 let expected = expected.clone();
2302 workers.push(std::thread::spawn(move || {
2303 barrier.wait();
2304 for _ in 0..64 {
2305 let cloned = (*text).clone();
2306 assert_eq!(cloned.as_str(), expected);
2307 assert!(
2308 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2309 "concurrent clone should reuse the shared Arc representation"
2310 );
2311 }
2312 }));
2313 }
2314
2315 barrier.wait();
2316 for worker in workers {
2317 worker
2318 .join()
2319 .expect("join concurrent small-text clone worker");
2320 }
2321
2322 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2323 panic!("original text should remain heap-owned after clone promotion");
2324 };
2325 let shared = shared
2326 .get()
2327 .expect("concurrent clones should promote the lazy shared Arc");
2328 assert_eq!(shared.as_ref(), expected);
2329 assert_eq!(text.as_str(), expected);
2330 }
2331
2332 #[test]
2333 fn null_properties() {
2334 let v = SqliteValue::Null;
2335 assert!(v.is_null());
2336 assert_eq!(v.to_integer(), 0);
2337 assert_eq!(v.to_float(), 0.0);
2338 assert_eq!(v.to_text(), "");
2339 assert_eq!(v.to_string(), "NULL");
2340 }
2341
2342 #[test]
2343 fn integer_properties() {
2344 let v = SqliteValue::Integer(42);
2345 assert!(!v.is_null());
2346 assert_eq!(v.as_integer(), Some(42));
2347 assert_eq!(v.to_integer(), 42);
2348 assert_eq!(v.to_float(), 42.0);
2349 assert_eq!(v.to_text(), "42");
2350 }
2351
2352 #[test]
2353 fn float_properties() {
2354 let v = SqliteValue::Float(3.14);
2355 assert_eq!(v.as_float(), Some(3.14));
2356 assert_eq!(v.to_integer(), 3);
2357 assert_eq!(v.to_text(), "3.14");
2358 }
2359
2360 #[test]
2361 fn text_properties() {
2362 let v = SqliteValue::Text(SmallText::new("hello"));
2363 assert_eq!(v.as_text(), Some("hello"));
2364 assert_eq!(v.to_integer(), 0);
2365 assert_eq!(v.to_float(), 0.0);
2366 }
2367
2368 #[test]
2369 fn text_numeric_coercion() {
2370 let v = SqliteValue::Text(SmallText::new("123"));
2371 assert_eq!(v.to_integer(), 123);
2372 assert_eq!(v.to_float(), 123.0);
2373
2374 let v = SqliteValue::Text(SmallText::new("3.14"));
2375 assert_eq!(v.to_integer(), 3);
2376 assert_eq!(v.to_float(), 3.14);
2377 }
2378
2379 #[test]
2380 fn text_numeric_coercion_ignores_hex_text_prefixes() {
2381 let v = SqliteValue::Text(SmallText::new("0x10"));
2382 assert_eq!(v.to_integer(), 0);
2383 assert_eq!(v.to_float(), 0.0);
2384
2385 let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
2386 assert_eq!(v.to_integer(), 0);
2387 assert_eq!(v.to_float(), 0.0);
2388 }
2389
2390 #[test]
2391 fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
2392 assert_eq!(
2393 SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
2394 SqliteValue::Integer(123)
2395 );
2396 assert_eq!(
2397 SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
2398 SqliteValue::Float(0.0)
2399 );
2400 assert_eq!(
2401 SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
2402 SqliteValue::Float(123.0)
2403 );
2404 assert_eq!(
2405 SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
2406 SqliteValue::Float(1.0)
2407 );
2408 assert_eq!(
2409 SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
2410 SqliteValue::Float(123.0)
2411 );
2412 assert_eq!(
2413 SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
2414 SqliteValue::Float(0.0)
2415 );
2416 assert_eq!(
2417 SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
2418 SqliteValue::Float(123.0)
2419 );
2420 }
2421
2422 #[test]
2423 fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
2424 assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
2425 assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
2426 assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
2427 assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
2428 }
2429
2430 #[test]
2431 fn test_sqlite_value_integer_real_comparison_equal() {
2432 let int_value = SqliteValue::Integer(3);
2433 let real_value = SqliteValue::Float(3.0);
2434 assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
2435 assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
2436 }
2437
2438 #[test]
2439 fn test_sqlite_value_text_to_integer_coercion() {
2440 let text_value = SqliteValue::Text(SmallText::new("123"));
2441 let coerced = text_value.apply_affinity(TypeAffinity::Integer);
2442 assert_eq!(coerced, SqliteValue::Integer(123));
2443 }
2444
2445 #[test]
2446 fn blob_properties() {
2447 let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
2448 assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
2449 assert_eq!(v.to_integer(), 0);
2450 assert_eq!(v.to_float(), 0.0);
2451 assert_eq!(v.to_text(), "\u{07AD}");
2454 }
2455
2456 #[test]
2457 fn display_formatting() {
2458 assert_eq!(SqliteValue::Null.to_string(), "NULL");
2459 assert_eq!(SqliteValue::Integer(42).to_string(), "42");
2460 assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
2461 assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
2462 assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
2463 assert_eq!(
2464 SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
2465 "X'CAFE'"
2466 );
2467 }
2468
2469 #[test]
2470 fn sort_order_null_first() {
2471 let null = SqliteValue::Null;
2472 let int = SqliteValue::Integer(0);
2473 let text = SqliteValue::Text(SmallText::new(""));
2474 let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
2475
2476 assert!(null < int);
2477 assert!(int < text);
2478 assert!(text < blob);
2479 }
2480
2481 #[test]
2482 fn sort_order_integers() {
2483 let a = SqliteValue::Integer(1);
2484 let b = SqliteValue::Integer(2);
2485 assert!(a < b);
2486 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
2487 }
2488
2489 #[test]
2490 fn sort_order_mixed_numeric() {
2491 let int = SqliteValue::Integer(1);
2492 let float = SqliteValue::Float(1.5);
2493 assert!(int < float);
2494
2495 let int = SqliteValue::Integer(2);
2496 assert!(int > float);
2497 }
2498
2499 #[test]
2500 fn test_int_float_precision_at_i64_boundary() {
2501 let imax = SqliteValue::Integer(i64::MAX);
2505 let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
2506 assert_eq!(
2507 imax.partial_cmp(&fmax),
2508 Some(Ordering::Less),
2509 "i64::MAX must be Less than 9223372036854775808.0"
2510 );
2511
2512 let a = SqliteValue::Integer(i64::MAX);
2514 let b = SqliteValue::Integer(i64::MAX - 1);
2515 let f = SqliteValue::Float(i64::MAX as f64);
2516 assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
2518 assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
2520 assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
2521 }
2522
2523 #[test]
2524 fn test_int_float_precision_symmetric() {
2525 let i = SqliteValue::Integer(i64::MAX);
2527 let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
2528 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
2529 }
2530
2531 #[test]
2532 fn test_int_float_exact_representation() {
2533 let i = SqliteValue::Integer(42);
2535 let f = SqliteValue::Float(42.0);
2536 assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
2537 assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
2538
2539 let i = SqliteValue::Integer(3);
2541 let f = SqliteValue::Float(3.5);
2542 assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
2543 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
2544 }
2545
2546 #[test]
2547 fn from_conversions() {
2548 assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
2549 assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
2550 assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
2551 assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
2552 assert_eq!(
2553 SqliteValue::from(String::from("world")).as_text(),
2554 Some("world")
2555 );
2556 assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
2557 assert!(SqliteValue::from(None::<i64>).is_null());
2558 assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
2559 }
2560
2561 #[test]
2562 fn affinity() {
2563 assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
2564 assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
2565 assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
2566 assert_eq!(
2567 SqliteValue::Text(SmallText::new("")).affinity(),
2568 TypeAffinity::Text
2569 );
2570 assert_eq!(
2571 SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
2572 TypeAffinity::Blob
2573 );
2574 }
2575
2576 #[test]
2577 fn null_equality() {
2578 let a = SqliteValue::Null;
2580 let b = SqliteValue::Null;
2581 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
2582 }
2583
2584 #[test]
2587 fn test_storage_class_variants() {
2588 assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
2589 assert_eq!(
2590 SqliteValue::Integer(42).storage_class(),
2591 StorageClass::Integer
2592 );
2593 assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
2594 assert_eq!(
2595 SqliteValue::Text("hi".into()).storage_class(),
2596 StorageClass::Text
2597 );
2598 assert_eq!(
2599 SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
2600 StorageClass::Blob
2601 );
2602 }
2603
2604 #[test]
2605 fn test_type_affinity_advisory_text_into_integer_ok() {
2606 let val = SqliteValue::Text("hello".into());
2609 let coerced = val.apply_affinity(TypeAffinity::Integer);
2610 assert!(coerced.as_text().is_some());
2611 assert_eq!(coerced.as_text().unwrap(), "hello");
2612
2613 let val = SqliteValue::Text("42".into());
2615 let coerced = val.apply_affinity(TypeAffinity::Integer);
2616 assert_eq!(coerced.as_integer(), Some(42));
2617 }
2618
2619 #[test]
2620 fn test_type_affinity_advisory_integer_into_text_ok() {
2621 let val = SqliteValue::Integer(42);
2623 let coerced = val.apply_affinity(TypeAffinity::Text);
2624 assert_eq!(coerced.as_text(), Some("42"));
2625 }
2626
2627 #[test]
2628 fn test_type_affinity_comparison_coercion_matches_oracle() {
2629 let val = SqliteValue::Text("123".into());
2631 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2632 assert_eq!(coerced.as_integer(), Some(123));
2633
2634 let val = SqliteValue::Text("3.14".into());
2636 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2637 assert_eq!(coerced.as_float(), Some(3.14));
2638
2639 let val = SqliteValue::Text("hello".into());
2641 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2642 assert!(coerced.as_text().is_some());
2643
2644 let val = SqliteValue::Integer(42);
2646 let coerced = val.apply_affinity(TypeAffinity::Blob);
2647 assert_eq!(coerced.as_integer(), Some(42));
2648
2649 let val = SqliteValue::Float(5.0);
2651 let coerced = val.apply_affinity(TypeAffinity::Integer);
2652 assert_eq!(coerced.as_integer(), Some(5));
2653
2654 let val = SqliteValue::Float(5.5);
2656 let coerced = val.apply_affinity(TypeAffinity::Integer);
2657 assert_eq!(coerced.as_float(), Some(5.5));
2658
2659 let val = SqliteValue::Integer(7);
2661 let coerced = val.apply_affinity(TypeAffinity::Real);
2662 assert_eq!(coerced.as_float(), Some(7.0));
2663
2664 let val = SqliteValue::Text("9".into());
2666 let coerced = val.apply_affinity(TypeAffinity::Real);
2667 assert_eq!(coerced.as_float(), Some(9.0));
2668 }
2669
2670 #[test]
2671 fn test_cast_to_numeric_uses_sqlite_cast_rules() {
2672 assert_eq!(
2673 SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
2674 SqliteValue::Integer(123)
2675 );
2676 assert_eq!(
2677 SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
2678 SqliteValue::Integer(150)
2679 );
2680 assert_eq!(
2681 SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
2682 SqliteValue::Integer(0)
2683 );
2684 assert_eq!(
2685 SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
2686 SqliteValue::Integer(123)
2687 );
2688
2689 match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
2690 SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
2691 other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
2692 }
2693 }
2694
2695 #[test]
2696 fn test_strict_table_rejects_text_into_integer() {
2697 let val = SqliteValue::Text("hello".into());
2698 let result = val.validate_strict(StrictColumnType::Integer);
2699 assert!(result.is_err());
2700 let err = result.unwrap_err();
2701 assert_eq!(err.expected, StrictColumnType::Integer);
2702 assert_eq!(err.actual, StorageClass::Text);
2703 }
2704
2705 #[test]
2706 fn test_strict_table_allows_exact_type() {
2707 let val = SqliteValue::Integer(42);
2709 assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
2710
2711 let val = SqliteValue::Float(3.14);
2713 assert!(val.validate_strict(StrictColumnType::Real).is_ok());
2714
2715 let val = SqliteValue::Text("hello".into());
2717 assert!(val.validate_strict(StrictColumnType::Text).is_ok());
2718
2719 let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
2721 assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
2722
2723 assert!(
2725 SqliteValue::Null
2726 .validate_strict(StrictColumnType::Integer)
2727 .is_ok()
2728 );
2729 assert!(
2730 SqliteValue::Null
2731 .validate_strict(StrictColumnType::Text)
2732 .is_ok()
2733 );
2734
2735 let val = SqliteValue::Integer(42);
2737 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
2738 let val = SqliteValue::Text("hi".into());
2739 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
2740 }
2741
2742 #[test]
2743 fn test_strict_real_accepts_integer_with_coercion() {
2744 let val = SqliteValue::Integer(42);
2746 let result = val.validate_strict(StrictColumnType::Real).unwrap();
2747 assert_eq!(result.as_float(), Some(42.0));
2748 }
2749
2750 #[test]
2751 fn test_strict_rejects_wrong_storage_classes() {
2752 assert!(
2754 SqliteValue::Float(3.14)
2755 .validate_strict(StrictColumnType::Integer)
2756 .is_err()
2757 );
2758
2759 assert!(
2761 SqliteValue::Blob(Arc::from([1u8].as_slice()))
2762 .validate_strict(StrictColumnType::Text)
2763 .is_err()
2764 );
2765
2766 assert!(
2768 SqliteValue::Integer(1)
2769 .validate_strict(StrictColumnType::Text)
2770 .is_err()
2771 );
2772
2773 assert!(
2775 SqliteValue::Text("x".into())
2776 .validate_strict(StrictColumnType::Blob)
2777 .is_err()
2778 );
2779 }
2780
2781 #[test]
2782 fn test_strict_column_type_parsing() {
2783 assert_eq!(
2784 StrictColumnType::from_type_name("INT"),
2785 Some(StrictColumnType::Integer)
2786 );
2787 assert_eq!(
2788 StrictColumnType::from_type_name("INTEGER"),
2789 Some(StrictColumnType::Integer)
2790 );
2791 assert_eq!(
2792 StrictColumnType::from_type_name("REAL"),
2793 Some(StrictColumnType::Real)
2794 );
2795 assert_eq!(
2796 StrictColumnType::from_type_name("TEXT"),
2797 Some(StrictColumnType::Text)
2798 );
2799 assert_eq!(
2800 StrictColumnType::from_type_name("BLOB"),
2801 Some(StrictColumnType::Blob)
2802 );
2803 assert_eq!(
2804 StrictColumnType::from_type_name("ANY"),
2805 Some(StrictColumnType::Any)
2806 );
2807 assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
2809 assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
2810 }
2811
2812 #[test]
2813 fn test_affinity_advisory_never_rejects() {
2814 let values = vec![
2816 SqliteValue::Null,
2817 SqliteValue::Integer(42),
2818 SqliteValue::Float(3.14),
2819 SqliteValue::Text("hello".into()),
2820 SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
2821 ];
2822 let affinities = [
2823 TypeAffinity::Integer,
2824 TypeAffinity::Text,
2825 TypeAffinity::Blob,
2826 TypeAffinity::Real,
2827 TypeAffinity::Numeric,
2828 ];
2829 for val in &values {
2830 for aff in &affinities {
2831 let _ = val.clone().apply_affinity(*aff);
2833 }
2834 }
2835 }
2836
2837 #[test]
2840 fn test_unique_allows_multiple_nulls_single_column() {
2841 let a = SqliteValue::Null;
2843 let b = SqliteValue::Null;
2844 assert!(!a.unique_eq(&b));
2845 }
2846
2847 #[test]
2848 fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
2849 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
2852 let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
2853 assert!(!unique_key_duplicates(&row_a, &row_b));
2854
2855 let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
2857 let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
2858 assert!(!unique_key_duplicates(&row_a, &row_b));
2859
2860 let row_a = [SqliteValue::Null, SqliteValue::Null];
2862 let row_b = [SqliteValue::Null, SqliteValue::Null];
2863 assert!(!unique_key_duplicates(&row_a, &row_b));
2864 }
2865
2866 #[test]
2867 fn test_unique_rejects_duplicate_non_null() {
2868 let a = SqliteValue::Integer(42);
2870 let b = SqliteValue::Integer(42);
2871 assert!(a.unique_eq(&b));
2872
2873 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2875 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2876 assert!(unique_key_duplicates(&row_a, &row_b));
2877
2878 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2880 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
2881 assert!(!unique_key_duplicates(&row_a, &row_b));
2882 }
2883
2884 #[test]
2885 fn test_unique_null_vs_non_null_distinct() {
2886 let a = SqliteValue::Null;
2888 let b = SqliteValue::Integer(1);
2889 assert!(!a.unique_eq(&b));
2890 assert!(!b.unique_eq(&a));
2891
2892 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
2894 let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
2895 assert!(!unique_key_duplicates(&row_a, &row_b));
2896 }
2897
2898 #[test]
2901 #[allow(clippy::cast_precision_loss)]
2902 fn test_integer_overflow_promotes_real_expr_add() {
2903 let max = SqliteValue::Integer(i64::MAX);
2904 let one = SqliteValue::Integer(1);
2905 let result = max.sql_add(&one);
2906 assert!(result.as_integer().is_none());
2908 assert!(result.as_float().is_some());
2909 assert!(result.as_float().unwrap() >= i64::MAX as f64);
2911 }
2912
2913 #[test]
2914 fn test_integer_overflow_promotes_real_expr_mul() {
2915 let max = SqliteValue::Integer(i64::MAX);
2916 let two = SqliteValue::Integer(2);
2917 let result = max.sql_mul(&two);
2918 assert!(result.as_float().is_some());
2920 }
2921
2922 #[test]
2923 fn test_integer_overflow_promotes_real_expr_sub() {
2924 let min = SqliteValue::Integer(i64::MIN);
2925 let one = SqliteValue::Integer(1);
2926 let result = min.sql_sub(&one);
2927 assert!(result.as_float().is_some());
2929 }
2930
2931 #[test]
2932 fn test_sum_overflow_errors() {
2933 let mut acc = SumAccumulator::new();
2934 acc.accumulate(&SqliteValue::Integer(i64::MAX));
2935 acc.accumulate(&SqliteValue::Integer(1));
2936 let result = acc.finish();
2937 assert!(result.is_err());
2938 }
2939
2940 #[test]
2941 fn test_sum_overflow_then_float_returns_real() {
2942 let mut acc = SumAccumulator::new();
2943 acc.accumulate(&SqliteValue::Integer(i64::MAX));
2944 acc.accumulate(&SqliteValue::Integer(1));
2945 acc.accumulate(&SqliteValue::Float(0.5));
2946 let result = acc.finish().unwrap();
2947 assert!(matches!(result, SqliteValue::Float(_)));
2948 }
2949
2950 #[test]
2951 fn test_sum_text_integer_literals_stay_integer() {
2952 let mut acc = SumAccumulator::new();
2953 acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
2954 acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
2955 let result = acc.finish().unwrap();
2956 assert_eq!(result.as_integer(), Some(3));
2957 }
2958
2959 #[test]
2960 fn test_sum_non_numeric_text_returns_real_zero() {
2961 let mut acc = SumAccumulator::new();
2962 acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
2963 let result = acc.finish().unwrap();
2964 assert_eq!(result.as_float(), Some(0.0));
2965 }
2966
2967 #[test]
2968 fn test_no_overflow_stays_integer() {
2969 let a = SqliteValue::Integer(100);
2971 let b = SqliteValue::Integer(200);
2972 let result = a.sql_add(&b);
2973 assert_eq!(result.as_integer(), Some(300));
2974
2975 let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
2977 assert_eq!(result.as_integer(), Some(42));
2978
2979 let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
2981 assert_eq!(result.as_integer(), Some(42));
2982 }
2983
2984 #[test]
2985 fn test_sum_null_only_returns_null() {
2986 let mut acc = SumAccumulator::new();
2987 acc.accumulate(&SqliteValue::Null);
2988 acc.accumulate(&SqliteValue::Null);
2989 let result = acc.finish().unwrap();
2990 assert!(result.is_null());
2991 }
2992
2993 #[test]
2994 fn test_sum_mixed_int_float() {
2995 let mut acc = SumAccumulator::new();
2996 acc.accumulate(&SqliteValue::Integer(10));
2997 acc.accumulate(&SqliteValue::Float(2.5));
2998 acc.accumulate(&SqliteValue::Integer(3));
2999 let result = acc.finish().unwrap();
3000 assert_eq!(result.as_float(), Some(15.5));
3002 }
3003
3004 #[test]
3005 fn test_sum_integer_only() {
3006 let mut acc = SumAccumulator::new();
3007 acc.accumulate(&SqliteValue::Integer(10));
3008 acc.accumulate(&SqliteValue::Integer(20));
3009 acc.accumulate(&SqliteValue::Integer(30));
3010 let result = acc.finish().unwrap();
3011 assert_eq!(result.as_integer(), Some(60));
3012 }
3013
3014 #[test]
3015 fn test_sql_arithmetic_null_propagation() {
3016 let n = SqliteValue::Null;
3017 let i = SqliteValue::Integer(42);
3018 assert!(n.sql_add(&i).is_null());
3019 assert!(i.sql_add(&n).is_null());
3020 assert!(n.sql_sub(&i).is_null());
3021 assert!(n.sql_mul(&i).is_null());
3022 }
3023
3024 #[test]
3025 fn test_sql_inf_arithmetic_nan_normalized_to_null() {
3026 let pos_inf = SqliteValue::Float(f64::INFINITY);
3028 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3029 assert!(pos_inf.sql_add(&neg_inf).is_null());
3030
3031 assert!(pos_inf.sql_sub(&pos_inf).is_null());
3033 }
3034
3035 #[test]
3036 fn test_sql_mul_zero_times_inf_normalized_to_null() {
3037 let zero = SqliteValue::Float(0.0);
3039 let pos_inf = SqliteValue::Float(f64::INFINITY);
3040 assert!(zero.sql_mul(&pos_inf).is_null());
3041 assert!(
3042 SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
3043 "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
3044 );
3045 }
3046
3047 #[test]
3048 fn test_sql_mul_mixed_int_float_stays_real() {
3049 let left = SqliteValue::Integer(10);
3050 let right = SqliteValue::Float(0.25);
3051 assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
3052 assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
3053 }
3054
3055 #[test]
3056 fn test_sql_inf_propagates_when_not_nan() {
3057 let pos_inf = SqliteValue::Float(f64::INFINITY);
3058 let one = SqliteValue::Integer(1);
3059 let add_result = pos_inf.sql_add(&one);
3060 assert!(
3061 matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
3062 "expected +Inf propagation, got {add_result:?}"
3063 );
3064
3065 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3066 let sub_result = neg_inf.sql_sub(&one);
3067 assert!(
3068 matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
3069 "expected -Inf propagation, got {sub_result:?}"
3070 );
3071 }
3072
3073 #[test]
3074 fn test_from_f64_nan_normalizes_to_null() {
3075 let value = SqliteValue::from(f64::NAN);
3076 assert!(value.is_null());
3077 }
3078
3079 #[test]
3080 fn test_inf_comparisons_against_finite_values() {
3081 let pos_inf = SqliteValue::Float(f64::INFINITY);
3082 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3083 let finite_hi = SqliteValue::Float(1.0e308);
3084 let finite_lo = SqliteValue::Float(-1.0e308);
3085
3086 assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
3087 assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
3088 }
3089
3090 #[test]
3093 fn test_empty_string_is_not_null() {
3094 let empty = SqliteValue::Text(SmallText::new(""));
3095 assert!(!empty.is_null());
3097 assert!(!empty.is_null());
3099 assert!(SqliteValue::Null.is_null());
3101 }
3102
3103 #[test]
3104 fn test_length_empty_string_zero() {
3105 let empty = SqliteValue::Text(SmallText::new(""));
3106 assert_eq!(empty.sql_length(), Some(0));
3107 }
3108
3109 #[test]
3110 fn test_typeof_empty_string_text() {
3111 let empty = SqliteValue::Text(SmallText::new(""));
3112 assert_eq!(empty.typeof_str(), "text");
3113 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3115 }
3116
3117 #[test]
3118 fn test_empty_string_comparisons() {
3119 let empty1 = SqliteValue::Text(SmallText::new(""));
3120 let empty2 = SqliteValue::Text(SmallText::new(""));
3121 assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
3123
3124 let null = SqliteValue::Null;
3128 assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
3129 }
3130
3131 #[test]
3132 fn test_typeof_all_variants() {
3133 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3134 assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
3135 assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
3136 assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
3137 assert_eq!(
3138 SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
3139 "blob"
3140 );
3141 }
3142
3143 #[test]
3144 fn test_sql_length_all_types() {
3145 assert_eq!(SqliteValue::Null.sql_length(), None);
3147 assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
3149 assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
3150 assert_eq!(
3152 SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
3153 Some(3)
3154 );
3155 assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
3157 assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); }
3160
3161 #[test]
3164 fn test_like_ascii_case_insensitive() {
3165 assert!(sql_like("A", "a", None));
3166 assert!(sql_like("a", "A", None));
3167 assert!(sql_like("hello", "HELLO", None));
3168 assert!(sql_like("HELLO", "hello", None));
3169 assert!(sql_like("HeLLo", "hEllO", None));
3170 }
3171
3172 #[test]
3173 fn test_like_unicode_case_sensitive_without_icu() {
3174 assert!(!sql_like("ä", "Ä", None));
3176 assert!(!sql_like("Ä", "ä", None));
3177 assert!(sql_like("ä", "ä", None));
3179 }
3180
3181 #[test]
3182 fn test_like_fast_path_does_not_fold_ascii_punctuation() {
3183 assert!(!sql_like("[", "{", None));
3184 assert!(!sql_like("@", "`", None));
3185 }
3186
3187 #[test]
3188 fn test_like_escape_handling() {
3189 assert!(sql_like("100\\%", "100%", Some('\\')));
3191 assert!(!sql_like("100\\%", "100x", Some('\\')));
3192
3193 assert!(sql_like("a\\_b", "a_b", Some('\\')));
3195 assert!(!sql_like("a\\_b", "axb", Some('\\')));
3196 }
3197
3198 #[test]
3199 fn test_like_wildcards_basic() {
3200 assert!(sql_like("%", "", None));
3202 assert!(sql_like("%", "anything", None));
3203 assert!(sql_like("a%", "abc", None));
3204 assert!(sql_like("%c", "abc", None));
3205 assert!(sql_like("a%c", "abc", None));
3206 assert!(sql_like("a%c", "aXYZc", None));
3207 assert!(!sql_like("a%c", "abd", None));
3208
3209 assert!(sql_like("_", "x", None));
3211 assert!(!sql_like("_", "", None));
3212 assert!(!sql_like("_", "xy", None));
3213 assert!(sql_like("a_c", "abc", None));
3214 assert!(!sql_like("a_c", "abbc", None));
3215 }
3216
3217 #[test]
3218 fn test_like_combined_wildcards() {
3219 assert!(sql_like("%_", "a", None));
3220 assert!(!sql_like("%_", "", None));
3221 assert!(sql_like("_%_", "ab", None));
3222 assert!(!sql_like("_%_", "a", None));
3223 assert!(sql_like("%a%b%", "xaybz", None));
3224 assert!(!sql_like("%a%b%", "xyz", None));
3225 }
3226
3227 #[test]
3228 fn test_like_exact_match() {
3229 assert!(sql_like("hello", "hello", None));
3230 assert!(!sql_like("hello", "world", None));
3231 assert!(sql_like("", "", None));
3232 assert!(!sql_like("a", "", None));
3233 assert!(!sql_like("", "a", None));
3234 }
3235
3236 #[test]
3237 fn test_like_fast_path_repeated_percent_shapes() {
3238 assert!(sql_like("ab%%", "ABcd", None));
3239 assert!(sql_like("%%cd", "abCD", None));
3240 assert!(sql_like("%%bc%%", "xxBCyy", None));
3241 assert!(sql_like("%%%%", "anything", None));
3242 }
3243
3244 #[test]
3245 fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
3246 assert!(sql_like("%éL%", "héllo", None));
3247 assert!(!sql_like("%Él%", "héllo", None));
3248 assert!(sql_like("Stra%", "straße", None));
3249 }
3250
3251 #[test]
3252 fn test_like_contains_fast_path_handles_overlapping_matches() {
3253 assert!(sql_like("%ana%", "bananas", None));
3254 assert!(sql_like("%NAN%", "baNanas", None));
3255 assert!(!sql_like("%ananasx%", "bananas", None));
3256 }
3257
3258 #[test]
3259 fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
3260 assert!(sql_like("%ß%", "straße", None));
3261 assert!(!sql_like("%SS%", "straße", None));
3262 }
3263
3264 #[test]
3267 fn test_format_sqlite_float_whole_number() {
3268 assert_eq!(format_sqlite_float(120.0), "120.0");
3269 assert_eq!(format_sqlite_float(0.0), "0.0");
3270 assert_eq!(format_sqlite_float(-42.0), "-42.0");
3271 assert_eq!(format_sqlite_float(1.0), "1.0");
3272 }
3273
3274 #[test]
3275 fn test_format_sqlite_float_fractional() {
3276 assert_eq!(format_sqlite_float(3.14), "3.14");
3277 assert_eq!(format_sqlite_float(0.5), "0.5");
3278 assert_eq!(format_sqlite_float(-0.001), "-0.001");
3279 }
3280
3281 #[test]
3282 fn test_format_sqlite_float_special() {
3283 assert_eq!(format_sqlite_float(f64::NAN), "NaN");
3284 assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
3285 assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
3286 }
3287
3288 #[test]
3289 fn test_format_sqlite_float_negative_zero() {
3290 assert_eq!(format_sqlite_float(-0.0), "-0.0");
3292 assert_eq!(format_sqlite_float(0.0), "0.0");
3293 }
3294
3295 #[test]
3296 fn test_float_to_text_includes_decimal_point() {
3297 let v = SqliteValue::Float(100.0);
3298 assert_eq!(v.to_text(), "100.0");
3299 let v = SqliteValue::Float(3.14);
3300 assert_eq!(v.to_text(), "3.14");
3301 }
3302
3303 #[test]
3306 fn test_scan_numeric_prefix_bare_dot() {
3307 assert_eq!(scan_numeric_prefix(b"."), 0);
3309 assert_eq!(scan_numeric_prefix(b"-."), 0);
3310 assert_eq!(scan_numeric_prefix(b"+."), 0);
3311 assert_eq!(scan_numeric_prefix(b"..1"), 0);
3312 }
3313
3314 #[test]
3315 fn test_scan_numeric_prefix_valid() {
3316 assert_eq!(scan_numeric_prefix(b"123"), 3);
3317 assert_eq!(scan_numeric_prefix(b"3.14"), 4);
3318 assert_eq!(scan_numeric_prefix(b".5"), 2);
3319 assert_eq!(scan_numeric_prefix(b"1e10"), 4);
3320 assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
3321 assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
3322 assert_eq!(scan_numeric_prefix(b"0.0"), 3);
3323 }
3324
3325 #[test]
3326 fn test_scan_numeric_prefix_empty_and_non_numeric() {
3327 assert_eq!(scan_numeric_prefix(b""), 0);
3328 assert_eq!(scan_numeric_prefix(b"abc"), 0);
3329 assert_eq!(scan_numeric_prefix(b"+"), 0);
3330 assert_eq!(scan_numeric_prefix(b"-"), 0);
3331 }
3332}