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 => match &self {
722 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
723 _ => self,
724 },
725 TypeAffinity::Integer => match &self {
726 Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
727 Self::Float(f) => {
728 if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
729 let i = *f as i64;
730 if (i as f64) == *f {
731 return Self::Integer(i);
732 }
733 }
734 self
735 }
736 _ => self,
737 },
738 TypeAffinity::Real => match &self {
739 Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
740 .map(|v| match v {
741 Self::Integer(i) => Self::Float(i as f64),
742 other => other,
743 })
744 .unwrap_or(self),
745 Self::Integer(i) => Self::Float(*i as f64),
746 _ => self,
747 },
748 }
749 }
750
751 #[allow(clippy::cast_precision_loss)]
758 pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
759 if matches!(self, Self::Null) {
760 return Ok(self);
761 }
762 match col_type {
763 StrictColumnType::Any => Ok(self),
764 StrictColumnType::Integer => match self {
765 Self::Integer(_) => Ok(self),
766 other => Err(StrictTypeError {
767 expected: col_type,
768 actual: other.storage_class(),
769 }),
770 },
771 StrictColumnType::Real => match self {
772 Self::Float(_) => Ok(self),
773 Self::Integer(i) => Ok(Self::Float(i as f64)),
774 other => Err(StrictTypeError {
775 expected: col_type,
776 actual: other.storage_class(),
777 }),
778 },
779 StrictColumnType::Text => match self {
780 Self::Text(_) => Ok(self),
781 other => Err(StrictTypeError {
782 expected: col_type,
783 actual: other.storage_class(),
784 }),
785 },
786 StrictColumnType::Blob => match self {
787 Self::Blob(_) => Ok(self),
788 other => Err(StrictTypeError {
789 expected: col_type,
790 actual: other.storage_class(),
791 }),
792 },
793 }
794 }
795
796 #[inline(always)]
798 #[allow(clippy::inline_always)]
799 pub const fn is_null(&self) -> bool {
800 matches!(self, Self::Null)
801 }
802
803 #[inline]
805 pub const fn as_integer(&self) -> Option<i64> {
806 match self {
807 Self::Integer(i) => Some(*i),
808 _ => None,
809 }
810 }
811
812 #[inline]
814 pub fn as_float(&self) -> Option<f64> {
815 match self {
816 Self::Float(f) => Some(*f),
817 _ => None,
818 }
819 }
820
821 #[inline]
823 pub fn as_text(&self) -> Option<&str> {
824 match self {
825 Self::Text(s) => Some(s),
826 _ => None,
827 }
828 }
829
830 #[inline]
832 pub fn as_blob(&self) -> Option<&[u8]> {
833 match self {
834 Self::Blob(b) => Some(b),
835 _ => None,
836 }
837 }
838
839 #[inline(always)]
847 #[allow(clippy::inline_always)]
848 #[allow(clippy::cast_possible_truncation)]
849 pub fn to_integer(&self) -> i64 {
850 match self {
851 Self::Null => 0,
852 Self::Integer(i) => *i,
853 Self::Float(f) => *f as i64,
854 Self::Text(s) => parse_integer_prefix(s),
855 Self::Blob(b) => parse_integer_prefix_bytes(b),
856 }
857 }
858
859 #[inline(always)]
867 #[allow(clippy::inline_always)]
868 #[allow(clippy::cast_precision_loss)]
869 pub fn to_float(&self) -> f64 {
870 match self {
871 Self::Null => 0.0,
872 Self::Integer(i) => *i as f64,
873 Self::Float(f) => *f,
874 Self::Text(s) => parse_float_prefix(s),
875 Self::Blob(b) => parse_float_prefix_bytes(b),
876 }
877 }
878
879 #[must_use]
886 pub fn to_sum_numeric_value(&self) -> Self {
887 match self {
888 Self::Null => Self::Null,
889 Self::Integer(i) => Self::Integer(*i),
890 Self::Float(f) => Self::Float(*f),
891 Self::Text(s) => {
892 let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
893 if let Ok(integer) = trimmed.parse::<i64>() {
894 Self::Integer(integer)
895 } else {
896 Self::Float(parse_float_prefix(s))
897 }
898 }
899 Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
900 }
901 }
902
903 #[inline]
909 #[must_use]
910 pub fn as_text_str(&self) -> Option<&str> {
911 match self {
912 Self::Text(s) => Some(s),
913 _ => None,
914 }
915 }
916
917 #[inline]
919 #[must_use]
920 pub fn as_blob_bytes(&self) -> Option<&[u8]> {
921 match self {
922 Self::Blob(b) => Some(b),
923 _ => None,
924 }
925 }
926
927 pub fn to_text(&self) -> String {
933 match self {
934 Self::Null => String::new(),
935 Self::Integer(i) => i.to_string(),
936 Self::Float(f) => format_sqlite_float(*f),
937 Self::Text(s) => s.to_string(),
938 Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
939 }
940 }
941
942 #[must_use]
948 pub fn cast_to_numeric(&self) -> Self {
949 match self {
950 Self::Null => Self::Null,
951 Self::Integer(i) => Self::Integer(*i),
952 Self::Float(f) => Self::Float(*f),
953 Self::Text(s) => cast_text_prefix_to_numeric(s),
954 Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
955 }
956 }
957
958 pub const fn typeof_str(&self) -> &'static str {
962 match self {
963 Self::Null => "null",
964 Self::Integer(_) => "integer",
965 Self::Float(_) => "real",
966 Self::Text(_) => "text",
967 Self::Blob(_) => "blob",
968 }
969 }
970
971 pub fn sql_length(&self) -> Option<i64> {
978 match self {
979 Self::Null => None,
980 Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
981 Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
982 Self::Integer(_) | Self::Float(_) => {
983 let t = self.to_text();
984 Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
985 }
986 }
987 }
988
989 pub fn unique_eq(&self, other: &Self) -> bool {
995 if self.is_null() || other.is_null() {
996 return false;
997 }
998 matches!(self.partial_cmp(other), Some(Ordering::Equal))
999 }
1000
1001 fn float_result_or_null(result: f64) -> Self {
1005 if result.is_nan() {
1006 Self::Null
1007 } else {
1008 Self::Float(result)
1009 }
1010 }
1011
1012 #[inline]
1018 pub fn is_integer_numeric_type(&self) -> bool {
1019 fn text_is_integer_numeric_type(s: &str) -> bool {
1020 let trimmed = s.trim_start();
1021 let end = scan_numeric_prefix(trimmed.as_bytes());
1022 end > 0
1023 && !trimmed.as_bytes()[..end]
1024 .iter()
1025 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1026 }
1027
1028 match self {
1029 Self::Integer(_) => true,
1030 Self::Float(_) | Self::Null => false,
1031 Self::Text(s) => text_is_integer_numeric_type(s),
1032 Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1033 }
1034 }
1035
1036 #[inline]
1041 fn is_float_numeric_type(&self) -> bool {
1042 fn text_is_float(s: &str) -> bool {
1043 let trimmed = s.trim_start();
1044 let end = scan_numeric_prefix(trimmed.as_bytes());
1045 end > 0
1046 && trimmed.as_bytes()[..end]
1047 .iter()
1048 .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1049 }
1050 match self {
1051 Self::Float(_) => true,
1052 Self::Integer(_) | Self::Null => false,
1053 Self::Text(s) => text_is_float(s),
1054 Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1055 }
1056 }
1057
1058 #[inline(always)]
1066 #[allow(clippy::inline_always)]
1067 #[must_use]
1068 #[allow(clippy::cast_precision_loss)]
1069 pub fn sql_add(&self, other: &Self) -> Self {
1070 match (self, other) {
1071 (Self::Null, _) | (_, Self::Null) => Self::Null,
1072 (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1073 Some(result) => Self::Integer(result),
1074 None => Self::float_result_or_null(*a as f64 + *b as f64),
1075 },
1076 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1080 let a = self.to_integer();
1081 let b = other.to_integer();
1082 match a.checked_add(b) {
1083 Some(result) => Self::Integer(result),
1084 None => Self::float_result_or_null(a as f64 + b as f64),
1085 }
1086 }
1087 _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1088 }
1089 }
1090
1091 #[inline(always)]
1095 #[allow(clippy::inline_always)]
1096 #[must_use]
1097 #[allow(clippy::cast_precision_loss)]
1098 pub fn sql_sub(&self, other: &Self) -> Self {
1099 match (self, other) {
1100 (Self::Null, _) | (_, Self::Null) => Self::Null,
1101 (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1102 Some(result) => Self::Integer(result),
1103 None => Self::float_result_or_null(*a as f64 - *b as f64),
1104 },
1105 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1106 let a = self.to_integer();
1107 let b = other.to_integer();
1108 match a.checked_sub(b) {
1109 Some(result) => Self::Integer(result),
1110 None => Self::float_result_or_null(a as f64 - b as f64),
1111 }
1112 }
1113 _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1114 }
1115 }
1116
1117 #[inline(always)]
1121 #[allow(clippy::inline_always)]
1122 #[must_use]
1123 #[allow(clippy::cast_precision_loss)]
1124 pub fn sql_mul(&self, other: &Self) -> Self {
1125 match (self, other) {
1126 (Self::Null, _) | (_, Self::Null) => Self::Null,
1127 (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1128 Some(result) => Self::Integer(result),
1129 None => Self::float_result_or_null(*a as f64 * *b as f64),
1130 },
1131 (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1132 (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1133 (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1134 _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1135 let a = self.to_integer();
1136 let b = other.to_integer();
1137 match a.checked_mul(b) {
1138 Some(result) => Self::Integer(result),
1139 None => Self::float_result_or_null(a as f64 * b as f64),
1140 }
1141 }
1142 _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1143 }
1144 }
1145
1146 const fn sort_class(&self) -> u8 {
1148 match self {
1149 Self::Null => 0,
1150 Self::Integer(_) | Self::Float(_) => 1,
1151 Self::Text(_) => 2,
1152 Self::Blob(_) => 3,
1153 }
1154 }
1155}
1156
1157pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1165 assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1166 a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1167}
1168
1169pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1176 if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1177 return sql_like_fast_path_matches(kind, literal, text);
1178 }
1179
1180 sql_like_inner(
1181 &pattern.chars().collect::<Vec<_>>(),
1182 &text.chars().collect::<Vec<_>>(),
1183 escape,
1184 0,
1185 0,
1186 )
1187}
1188
1189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1190pub enum SqlLikeFastPathKind {
1191 MatchAll,
1192 Exact,
1193 Prefix,
1194 Suffix,
1195 Contains,
1196}
1197
1198impl SqlLikeFastPathKind {
1199 #[must_use]
1200 pub const fn opcode_tag(self) -> i32 {
1201 match self {
1202 Self::MatchAll => 0,
1203 Self::Exact => 1,
1204 Self::Prefix => 2,
1205 Self::Suffix => 3,
1206 Self::Contains => 4,
1207 }
1208 }
1209
1210 #[must_use]
1211 pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1212 match tag {
1213 0 => Some(Self::MatchAll),
1214 1 => Some(Self::Exact),
1215 2 => Some(Self::Prefix),
1216 3 => Some(Self::Suffix),
1217 4 => Some(Self::Contains),
1218 _ => None,
1219 }
1220 }
1221}
1222
1223#[must_use]
1224pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1225 match kind {
1226 SqlLikeFastPathKind::MatchAll => true,
1227 SqlLikeFastPathKind::Exact => ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes()),
1228 SqlLikeFastPathKind::Prefix => ascii_ci_starts_with(text, literal),
1229 SqlLikeFastPathKind::Suffix => ascii_ci_ends_with(text, literal),
1230 SqlLikeFastPathKind::Contains => ascii_ci_contains(text, literal),
1231 }
1232}
1233
1234pub struct SqlLikeFastPathMatcher<'a> {
1236 kind: SqlLikeFastPathKind,
1237 literal: &'a str,
1238 contains_finder: Option<memmem::Finder<'a>>,
1239}
1240
1241impl<'a> SqlLikeFastPathMatcher<'a> {
1242 #[must_use]
1243 pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1244 let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1245 .then(|| memmem::Finder::new(literal.as_bytes()));
1246 Self {
1247 kind,
1248 literal,
1249 contains_finder,
1250 }
1251 }
1252
1253 #[must_use]
1254 pub fn matches(&self, text: &str) -> bool {
1255 if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1256 let text_bytes = text.as_bytes();
1257 let needle_bytes = self.literal.as_bytes();
1258 if needle_bytes.len() > text_bytes.len() {
1259 return false;
1260 }
1261 if finder.find(text_bytes).is_some() {
1262 return true;
1263 }
1264 return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1265 }
1266 sql_like_fast_path_matches(self.kind, self.literal, text)
1267 }
1268}
1269
1270#[must_use]
1271pub fn classify_sql_like_fast_path(
1272 pattern: &str,
1273 escape: Option<char>,
1274) -> Option<(SqlLikeFastPathKind, &str)> {
1275 if escape.is_some() || pattern.contains('_') {
1276 return None;
1277 }
1278 if !pattern.contains('%') {
1279 return Some((SqlLikeFastPathKind::Exact, pattern));
1280 }
1281 if pattern.chars().all(|ch| ch == '%') {
1282 return Some((SqlLikeFastPathKind::MatchAll, ""));
1283 }
1284
1285 let trimmed_start = pattern.trim_start_matches('%');
1286 let trimmed_end = pattern.trim_end_matches('%');
1287 if pattern.starts_with('%') && pattern.ends_with('%') {
1288 let core = trimmed_start.trim_end_matches('%');
1289 if core.is_empty() {
1290 return Some((SqlLikeFastPathKind::MatchAll, ""));
1291 }
1292 if !core.contains('%') {
1293 return Some((SqlLikeFastPathKind::Contains, core));
1294 }
1295 }
1296 if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1297 {
1298 return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1299 }
1300 if !pattern.ends_with('%')
1301 && trimmed_start.len() < pattern.len()
1302 && !trimmed_start.contains('%')
1303 {
1304 return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1305 }
1306 None
1307}
1308
1309fn sql_like_inner(
1310 pattern: &[char],
1311 text: &[char],
1312 escape: Option<char>,
1313 pi: usize,
1314 ti: usize,
1315) -> bool {
1316 let mut pi = pi;
1317 let mut ti = ti;
1318
1319 while pi < pattern.len() {
1320 let pc = pattern[pi];
1321
1322 if Some(pc) == escape {
1324 pi += 1;
1325 if pi >= pattern.len() {
1326 return false; }
1328 if ti >= text.len() || !ascii_ci_eq(pattern[pi], text[ti]) {
1330 return false;
1331 }
1332 pi += 1;
1333 ti += 1;
1334 continue;
1335 }
1336
1337 match pc {
1338 '%' => {
1339 while pi < pattern.len() && pattern[pi] == '%' {
1341 pi += 1;
1342 }
1343 if pi >= pattern.len() {
1345 return true;
1346 }
1347 for start in ti..=text.len() {
1349 if sql_like_inner(pattern, text, escape, pi, start) {
1350 return true;
1351 }
1352 }
1353 return false;
1354 }
1355 '_' => {
1356 if ti >= text.len() {
1357 return false;
1358 }
1359 pi += 1;
1360 ti += 1;
1361 }
1362 _ => {
1363 if ti >= text.len() || !ascii_ci_eq(pc, text[ti]) {
1364 return false;
1365 }
1366 pi += 1;
1367 ti += 1;
1368 }
1369 }
1370 }
1371 ti >= text.len()
1372}
1373
1374fn ascii_ci_eq(a: char, b: char) -> bool {
1376 if a == b {
1377 return true;
1378 }
1379 a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1381}
1382
1383#[inline]
1384fn ascii_fold_byte(byte: u8) -> u8 {
1385 byte.to_ascii_lowercase()
1386}
1387
1388#[inline]
1389fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1390 left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1391}
1392
1393fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1394 if left.len() != right.len() {
1395 return false;
1396 }
1397 let mut idx = 0;
1398 while idx < left.len() {
1399 if !ascii_ci_eq_byte(left[idx], right[idx]) {
1400 return false;
1401 }
1402 idx += 1;
1403 }
1404 true
1405}
1406
1407fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1408 let text = text.as_bytes();
1409 let prefix = prefix.as_bytes();
1410 text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1411}
1412
1413fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1414 let text = text.as_bytes();
1415 let suffix = suffix.as_bytes();
1416 text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1417}
1418
1419fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1420 let text = text.as_bytes();
1421 let needle = needle.as_bytes();
1422 if needle.is_empty() {
1423 return true;
1424 }
1425 if needle.len() > text.len() {
1426 return false;
1427 }
1428 if memmem::find(text, needle).is_some() {
1429 return true;
1430 }
1431
1432 ascii_ci_contains_folded_scan(text, needle)
1433}
1434
1435fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1436 if needle.is_empty() {
1437 return true;
1438 }
1439 if needle.len() > text.len() {
1440 return false;
1441 }
1442 let max_start = text.len() - needle.len();
1443 let first = needle[0];
1444 let first_folded = ascii_fold_byte(first);
1445 let first_alt = if first.is_ascii_alphabetic() {
1446 first_folded.to_ascii_uppercase()
1447 } else {
1448 first_folded
1449 };
1450 let mut start = 0;
1451 while start <= max_start {
1452 let rel = if first_folded == first_alt {
1453 memchr(first_folded, &text[start..=max_start])
1454 } else {
1455 memchr2(first_folded, first_alt, &text[start..=max_start])
1456 };
1457 let Some(rel) = rel else {
1458 break;
1459 };
1460 start += rel;
1461 if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1462 return true;
1463 }
1464 start += 1;
1465 }
1466 false
1467}
1468
1469#[derive(Debug, Clone)]
1476pub struct SumAccumulator {
1477 int_sum: i64,
1479 float_sum: f64,
1482 float_err: f64,
1484 has_value: bool,
1486 is_float: bool,
1488 overflow: bool,
1490}
1491
1492impl Default for SumAccumulator {
1493 fn default() -> Self {
1494 Self::new()
1495 }
1496}
1497
1498#[inline]
1500fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1501 let s = *sum;
1502 let t = s + value;
1503 if s.abs() > value.abs() {
1504 *err += (s - t) + value;
1505 } else {
1506 *err += (value - t) + s;
1507 }
1508 *sum = t;
1509}
1510
1511impl SumAccumulator {
1512 pub const fn new() -> Self {
1514 Self {
1515 int_sum: 0,
1516 float_sum: 0.0,
1517 float_err: 0.0,
1518 has_value: false,
1519 is_float: false,
1520 overflow: false,
1521 }
1522 }
1523
1524 #[allow(clippy::cast_precision_loss)]
1526 pub fn accumulate(&mut self, val: &SqliteValue) {
1527 match val.to_sum_numeric_value() {
1528 SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1529 SqliteValue::Integer(i) => {
1530 self.has_value = true;
1531 if !self.is_float && !self.overflow {
1532 match self.int_sum.checked_add(i) {
1533 Some(result) => self.int_sum = result,
1534 None => self.overflow = true,
1535 }
1536 }
1537 kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1538 }
1539 SqliteValue::Float(f) => {
1540 self.has_value = true;
1541 self.is_float = true;
1542 kbn_step(&mut self.float_sum, &mut self.float_err, f);
1543 }
1544 }
1545 }
1546
1547 pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1551 if !self.is_float && self.overflow {
1552 return Err(SumOverflowError);
1553 }
1554 if !self.has_value {
1555 return Ok(SqliteValue::Null);
1556 }
1557 if self.is_float {
1558 Ok(SqliteValue::Float(self.float_sum + self.float_err))
1559 } else {
1560 Ok(SqliteValue::Integer(self.int_sum))
1561 }
1562 }
1563}
1564
1565#[derive(Debug, Clone, PartialEq, Eq)]
1567pub struct SumOverflowError;
1568
1569impl fmt::Display for SumOverflowError {
1570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1571 f.write_str("integer overflow in sum()")
1572 }
1573}
1574
1575impl fmt::Display for SqliteValue {
1576 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1577 match self {
1578 Self::Null => f.write_str("NULL"),
1579 Self::Integer(i) => write!(f, "{i}"),
1580 Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1581 Self::Text(s) => write!(f, "'{s}'"),
1582 Self::Blob(b) => {
1583 f.write_str("X'")?;
1584 for byte in b.iter() {
1585 write!(f, "{byte:02X}")?;
1586 }
1587 f.write_str("'")
1588 }
1589 }
1590 }
1591}
1592
1593impl PartialEq for SqliteValue {
1594 fn eq(&self, other: &Self) -> bool {
1595 matches!(self.partial_cmp(other), Some(Ordering::Equal))
1596 }
1597}
1598
1599impl Eq for SqliteValue {}
1600
1601impl PartialOrd for SqliteValue {
1602 #[inline]
1603 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1604 Some(self.cmp(other))
1605 }
1606}
1607
1608impl Ord for SqliteValue {
1609 #[inline]
1610 fn cmp(&self, other: &Self) -> Ordering {
1611 let class_a = self.sort_class();
1613 let class_b = other.sort_class();
1614
1615 if class_a != class_b {
1616 return class_a.cmp(&class_b);
1617 }
1618
1619 match (self, other) {
1620 (Self::Null, Self::Null) => Ordering::Equal,
1621 (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
1622 (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
1623 (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
1624 (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
1625 (Self::Text(a), Self::Text(b)) => a.cmp(b),
1626 (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
1627 _ => unreachable!(),
1628 }
1629 }
1630}
1631
1632impl From<i64> for SqliteValue {
1633 fn from(i: i64) -> Self {
1634 Self::Integer(i)
1635 }
1636}
1637
1638impl From<i32> for SqliteValue {
1639 fn from(i: i32) -> Self {
1640 Self::Integer(i64::from(i))
1641 }
1642}
1643
1644impl From<f64> for SqliteValue {
1645 fn from(f: f64) -> Self {
1646 Self::float_result_or_null(f)
1647 }
1648}
1649
1650impl From<String> for SqliteValue {
1651 fn from(s: String) -> Self {
1652 Self::Text(SmallText::from_string(s))
1655 }
1656}
1657
1658impl From<&str> for SqliteValue {
1659 fn from(s: &str) -> Self {
1660 Self::Text(SmallText::new(s))
1661 }
1662}
1663
1664impl From<Arc<str>> for SqliteValue {
1665 fn from(s: Arc<str>) -> Self {
1666 Self::Text(SmallText::from_arc(s))
1667 }
1668}
1669
1670impl From<Vec<u8>> for SqliteValue {
1671 fn from(b: Vec<u8>) -> Self {
1672 Self::Blob(Arc::from(b))
1675 }
1676}
1677
1678impl From<&[u8]> for SqliteValue {
1679 fn from(b: &[u8]) -> Self {
1680 Self::Blob(Arc::from(b))
1681 }
1682}
1683
1684impl From<Arc<[u8]>> for SqliteValue {
1685 fn from(b: Arc<[u8]>) -> Self {
1686 Self::Blob(b)
1687 }
1688}
1689
1690impl<T: Into<Self>> From<Option<T>> for SqliteValue {
1691 fn from(opt: Option<T>) -> Self {
1692 match opt {
1693 Some(v) => v.into(),
1694 None => Self::Null,
1695 }
1696 }
1697}
1698
1699#[allow(
1703 clippy::cast_possible_truncation,
1704 clippy::cast_precision_loss,
1705 clippy::float_cmp
1706)]
1707fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
1708 let trimmed = trim_sqlite_ascii_whitespace(s);
1709 if trimmed.is_empty() {
1710 return None;
1711 }
1712 if let Ok(i) = trimmed.parse::<i64>() {
1714 return Some(SqliteValue::Integer(i));
1715 }
1716 if let Ok(f) = trimmed.parse::<f64>() {
1720 if !f.is_finite() {
1721 let lower = trimmed.to_ascii_lowercase();
1722 if lower.contains("inf") || lower.contains("nan") {
1723 return None;
1724 }
1725 }
1726 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
1729 #[allow(clippy::cast_possible_truncation)]
1730 let i = f as i64;
1731 #[allow(clippy::cast_precision_loss)]
1732 if (i as f64) == f {
1733 return Some(SqliteValue::Integer(i));
1734 }
1735 }
1736 return Some(SqliteValue::Float(f));
1737 }
1738 None
1739}
1740
1741#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1746pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
1747 if r.is_nan() {
1748 return Ordering::Greater;
1750 }
1751 if r < -9_223_372_036_854_775_808.0 {
1753 return Ordering::Greater;
1754 }
1755 if r >= 9_223_372_036_854_775_808.0 {
1756 return Ordering::Less;
1757 }
1758 let y = r as i64;
1760 match i.cmp(&y) {
1761 Ordering::Less => Ordering::Less,
1762 Ordering::Greater => Ordering::Greater,
1763 Ordering::Equal => {
1765 let s = i as f64;
1766 s.partial_cmp(&r).unwrap_or(Ordering::Equal)
1767 }
1768 }
1769}
1770
1771#[must_use]
1777pub fn format_sqlite_float(f: f64) -> String {
1778 if f.is_nan() {
1779 return "NaN".to_owned();
1780 }
1781 if f.is_infinite() {
1782 return if f.is_sign_positive() {
1783 "Inf".to_owned()
1784 } else {
1785 "-Inf".to_owned()
1786 };
1787 }
1788 let abs = f.abs();
1793 let s = if abs == 0.0 {
1794 if f.is_sign_negative() {
1796 "-0.0".to_owned()
1797 } else {
1798 "0.0".to_owned()
1799 }
1800 } else {
1801 let exp = abs.log10().floor() as i32;
1804 if exp >= 15 || exp < -4 {
1805 let mut s = format!("{f:.14e}");
1807 if let Some(e_pos) = s.find('e') {
1809 let mantissa = &s[..e_pos];
1810 let exp_str = &s[e_pos + 1..]; let trimmed = mantissa.trim_end_matches('0');
1812 let trimmed = if trimmed.ends_with('.') {
1814 format!("{trimmed}0")
1815 } else {
1816 trimmed.to_owned()
1817 };
1818 let (exp_sign, exp_digits) = if let Some(rest) = exp_str.strip_prefix('-') {
1821 ("-", rest)
1822 } else if let Some(rest) = exp_str.strip_prefix('+') {
1823 ("+", rest)
1824 } else {
1825 ("+", exp_str)
1826 };
1827 let exp_num: u32 = exp_digits.parse().unwrap_or(0);
1828 s = format!("{trimmed}e{exp_sign}{exp_num:02}");
1829 }
1830 s
1831 } else {
1832 #[allow(clippy::cast_sign_loss)]
1834 let decimal_places = (14 - exp).max(0) as usize;
1835 let mut s = format!("{f:.decimal_places$}");
1836 if s.contains('.') {
1838 let trimmed = s.trim_end_matches('0');
1839 s = if trimmed.ends_with('.') {
1840 format!("{trimmed}0")
1841 } else {
1842 trimmed.to_owned()
1843 };
1844 } else {
1845 s.push_str(".0");
1846 }
1847 s
1848 }
1849 };
1850 s
1851}
1852
1853#[cfg(test)]
1854#[allow(clippy::float_cmp, clippy::approx_constant)]
1855mod tests {
1856 use super::*;
1857
1858 struct ValuePoolTestGuard;
1859
1860 impl ValuePoolTestGuard {
1861 fn new() -> Self {
1862 pool_clear();
1863 reset_value_pool_test_stats();
1864 Self
1865 }
1866 }
1867
1868 impl Drop for ValuePoolTestGuard {
1869 fn drop(&mut self) {
1870 pool_clear();
1871 reset_value_pool_test_stats();
1872 }
1873 }
1874
1875 fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
1876 let stats = value_pool_test_stats_snapshot();
1877 eprintln!(
1878 "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
1879 stats.slab_alloc_count,
1880 stats.slab_return_count,
1881 stats.global_alloc_fallback_count,
1882 stats.slab_high_water_mark,
1883 pool_len(),
1884 );
1885 stats
1886 }
1887
1888 #[test]
1889 fn test_slab_basic_alloc_dealloc() {
1890 let _guard = ValuePoolTestGuard::new();
1891 const ROUND_TRIP_COUNT: usize = 100;
1892
1893 assert_eq!(pool_len(), 0);
1894 assert_eq!(pool_acquire(), None);
1895 assert_eq!(
1896 value_pool_test_stats_snapshot(),
1897 ValuePoolStats {
1898 slab_alloc_count: 0,
1899 slab_return_count: 0,
1900 global_alloc_fallback_count: 1,
1901 slab_high_water_mark: 0,
1902 }
1903 );
1904
1905 reset_value_pool_test_stats();
1906 for value in 0..ROUND_TRIP_COUNT {
1907 pool_return(SqliteValue::Integer(value as i64));
1908 }
1909 assert_eq!(pool_len(), ROUND_TRIP_COUNT);
1910 assert_eq!(
1911 value_pool_test_stats_snapshot(),
1912 ValuePoolStats {
1913 slab_alloc_count: 0,
1914 slab_return_count: ROUND_TRIP_COUNT,
1915 global_alloc_fallback_count: 0,
1916 slab_high_water_mark: ROUND_TRIP_COUNT,
1917 }
1918 );
1919
1920 reset_value_pool_test_stats();
1921 for expected in (0..ROUND_TRIP_COUNT).rev() {
1922 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
1923 }
1924 assert_eq!(pool_len(), 0);
1925 assert_eq!(
1926 log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
1927 ValuePoolStats {
1928 slab_alloc_count: ROUND_TRIP_COUNT,
1929 slab_return_count: 0,
1930 global_alloc_fallback_count: 0,
1931 slab_high_water_mark: 0,
1932 }
1933 );
1934 }
1935
1936 #[test]
1937 fn test_slab_exhaustion_fallback() {
1938 let _guard = ValuePoolTestGuard::new();
1939
1940 for value in 0..=VALUE_POOL_CAP {
1941 pool_return(SqliteValue::Integer(value as i64));
1942 }
1943 assert_eq!(pool_len(), VALUE_POOL_CAP);
1944 assert_eq!(
1945 value_pool_test_stats_snapshot(),
1946 ValuePoolStats {
1947 slab_alloc_count: 0,
1948 slab_return_count: VALUE_POOL_CAP,
1949 global_alloc_fallback_count: 0,
1950 slab_high_water_mark: VALUE_POOL_CAP,
1951 }
1952 );
1953
1954 reset_value_pool_test_stats();
1955 for _ in 0..VALUE_POOL_CAP {
1956 assert!(pool_acquire().is_some());
1957 }
1958 assert_eq!(pool_acquire(), None);
1959 assert_eq!(pool_len(), 0);
1960 assert_eq!(
1961 log_value_pool_test_stats("test_slab_exhaustion_fallback"),
1962 ValuePoolStats {
1963 slab_alloc_count: VALUE_POOL_CAP,
1964 slab_return_count: 0,
1965 global_alloc_fallback_count: 1,
1966 slab_high_water_mark: 0,
1967 }
1968 );
1969 }
1970
1971 #[test]
1972 fn test_slab_no_leak() {
1973 let _guard = ValuePoolTestGuard::new();
1974 const ITERATIONS: usize = 10_000;
1975
1976 let (weak_tx, weak_rx) = std::sync::mpsc::channel();
1977 let (release_tx, release_rx) = std::sync::mpsc::channel();
1978
1979 let worker = std::thread::spawn(move || {
1980 pool_clear();
1981 reset_value_pool_test_stats();
1982
1983 let mut pooled_weak = None;
1984 let mut overflow_weak = None;
1985 for value in 0..ITERATIONS {
1986 let payload: Arc<[u8]> =
1987 Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
1988 if value == 0 {
1989 pooled_weak = Some(Arc::downgrade(&payload));
1990 } else if value == ITERATIONS - 1 {
1991 overflow_weak = Some(Arc::downgrade(&payload));
1992 }
1993 pool_return(SqliteValue::Blob(payload));
1994 }
1995
1996 assert_eq!(
1997 pool_len(),
1998 VALUE_POOL_CAP,
1999 "the slab must retain at most VALUE_POOL_CAP entries",
2000 );
2001 weak_tx
2002 .send((
2003 pooled_weak.expect("capture pooled weak handle"),
2004 overflow_weak.expect("capture overflow weak handle"),
2005 log_value_pool_test_stats("test_slab_no_leak"),
2006 ))
2007 .expect("send slab leak stats");
2008 release_rx.recv().expect("wait for release");
2009 });
2010
2011 let (pooled_weak, overflow_weak, stats) =
2012 weak_rx.recv().expect("receive weak blob handles");
2013 assert!(
2014 pooled_weak.upgrade().is_some(),
2015 "pooled blob should remain alive while the owning thread is running"
2016 );
2017 assert!(
2018 overflow_weak.upgrade().is_none(),
2019 "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2020 );
2021 assert_eq!(
2022 stats,
2023 ValuePoolStats {
2024 slab_alloc_count: 0,
2025 slab_return_count: VALUE_POOL_CAP,
2026 global_alloc_fallback_count: 0,
2027 slab_high_water_mark: VALUE_POOL_CAP,
2028 }
2029 );
2030
2031 release_tx.send(()).expect("release worker thread");
2032 worker.join().expect("join worker");
2033
2034 assert!(
2035 pooled_weak.upgrade().is_none(),
2036 "thread-local slab contents must be dropped when the thread exits"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_slab_thread_local_isolation() {
2042 let _guard = ValuePoolTestGuard::new();
2043
2044 pool_return(SqliteValue::Integer(11));
2045 assert_eq!(pool_len(), 1);
2046
2047 let worker = std::thread::spawn(|| {
2048 pool_clear();
2049 reset_value_pool_test_stats();
2050
2051 assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2052 pool_return(SqliteValue::Integer(22));
2053 assert_eq!(pool_len(), 1);
2054 assert_eq!(
2055 value_pool_test_stats_snapshot(),
2056 ValuePoolStats {
2057 slab_alloc_count: 0,
2058 slab_return_count: 1,
2059 global_alloc_fallback_count: 0,
2060 slab_high_water_mark: 1,
2061 }
2062 );
2063 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
2064 assert_eq!(pool_len(), 0);
2065 });
2066 worker.join().expect("join worker");
2067
2068 assert_eq!(
2069 pool_len(),
2070 1,
2071 "worker thread slab operations must not affect the caller thread"
2072 );
2073 assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
2074 assert_eq!(pool_len(), 0);
2075 let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
2076 assert_eq!(
2077 stats,
2078 ValuePoolStats {
2079 slab_alloc_count: 1,
2080 slab_return_count: 1,
2081 global_alloc_fallback_count: 0,
2082 slab_high_water_mark: 1,
2083 }
2084 );
2085 }
2086
2087 #[test]
2088 fn test_slab_zero_malloc_steady_state() {
2089 let _guard = ValuePoolTestGuard::new();
2090 const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
2091 const ITERATIONS: usize = 1_000;
2092 const INITIAL_TEXT: &str =
2093 "steady-state pooled string backing store for bd-nsvud warmup payload";
2094 const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
2095
2096 assert!(
2097 REUSED_TEXT.len() <= INITIAL_TEXT.len(),
2098 "steady-state overwrite must fit within the warmed heap allocation"
2099 );
2100
2101 for _ in 0..WARM_POOL_DEPTH {
2102 pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
2103 }
2104 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2105
2106 reset_value_pool_test_stats();
2107 for _ in 0..ITERATIONS {
2108 let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
2109 let SqliteValue::Text(existing) = &mut reused else {
2110 panic!("warmed slab entry should remain a text value");
2111 };
2112 let original_ptr = existing.as_str().as_ptr();
2113 existing.overwrite(REUSED_TEXT);
2114 assert_eq!(
2115 existing.as_str().as_ptr(),
2116 original_ptr,
2117 "steady-state overwrite should reuse the warmed heap buffer",
2118 );
2119 assert_eq!(existing.as_str(), REUSED_TEXT);
2120 pool_return(reused);
2121 }
2122
2123 assert_eq!(pool_len(), WARM_POOL_DEPTH);
2124 assert_eq!(
2125 log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
2126 ValuePoolStats {
2127 slab_alloc_count: ITERATIONS,
2128 slab_return_count: ITERATIONS,
2129 global_alloc_fallback_count: 0,
2130 slab_high_water_mark: WARM_POOL_DEPTH,
2131 }
2132 );
2133 }
2134
2135 #[test]
2136 fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
2137 let text = SmallText::new("this string is definitely longer than twenty three bytes");
2138 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2139 panic!("long text should start in heap-owned mode");
2140 };
2141 assert!(
2142 shared.get().is_none(),
2143 "long text should not allocate Arc eagerly before cloning"
2144 );
2145
2146 let cloned = text.clone();
2147
2148 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2149 panic!("original text should remain heap-owned after clone");
2150 };
2151 assert!(
2152 shared.get().is_some(),
2153 "first clone should materialize a shared Arc lazily"
2154 );
2155 assert!(
2156 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2157 "cloned text should use the shared Arc representation"
2158 );
2159 assert_eq!(text.as_str(), cloned.as_str());
2160 }
2161
2162 #[test]
2163 fn test_small_text_overwrite_reuses_unique_heap_buffer() {
2164 let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
2165 let (original_ptr, original_capacity) = match &text.repr {
2166 SmallTextRepr::HeapOwned { text, shared } => {
2167 assert!(shared.get().is_none(), "fresh heap text should be unshared");
2168 (text.as_ptr(), text.capacity())
2169 }
2170 _ => panic!("long text should start in heap-owned mode"),
2171 };
2172
2173 text.overwrite("another long string that still fits the same allocation");
2174
2175 match &text.repr {
2176 SmallTextRepr::HeapOwned { text, shared } => {
2177 assert!(
2178 shared.get().is_none(),
2179 "overwrite should keep text single-owner"
2180 );
2181 assert_eq!(text.as_ptr(), original_ptr);
2182 assert_eq!(text.capacity(), original_capacity);
2183 assert_eq!(
2184 text.as_str(),
2185 "another long string that still fits the same allocation"
2186 );
2187 }
2188 _ => panic!("overwrite should keep long text in heap-owned mode"),
2189 }
2190 }
2191
2192 #[test]
2193 fn test_small_text_overwrite_detaches_from_shared_arc() {
2194 let original = "this string is definitely longer than twenty three bytes";
2195 let mut text = SmallText::new(original);
2196 let (original_ptr, original_capacity) = match &text.repr {
2197 SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
2198 _ => panic!("long text should start in heap-owned mode"),
2199 };
2200 let replacement = "replacement text that must not mutate the shared clone";
2201 assert!(
2202 replacement.len() <= original_capacity,
2203 "replacement should fit the original heap allocation for this regression",
2204 );
2205 let clone = text.clone();
2206
2207 text.overwrite(replacement);
2208
2209 assert_eq!(
2210 clone.as_str(),
2211 original,
2212 "existing shared clones must keep the original contents"
2213 );
2214 assert_eq!(text.as_str(), replacement);
2215 match &text.repr {
2216 SmallTextRepr::HeapOwned { text, shared } => {
2217 assert_eq!(
2218 text.as_ptr(),
2219 original_ptr,
2220 "overwriting a cloned long string should keep the owned buffer",
2221 );
2222 assert_eq!(
2223 text.capacity(),
2224 original_capacity,
2225 "detaching from the shared cache should preserve capacity",
2226 );
2227 assert!(
2228 shared.get().is_none(),
2229 "overwrite should reset the lazy shared cache after detaching"
2230 );
2231 }
2232 _ => panic!("overwrite should restore heap-owned mode"),
2233 }
2234 }
2235
2236 #[test]
2237 fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
2238 let _guard = ValuePoolTestGuard::new();
2239
2240 pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
2241 assert_eq!(
2242 pool_len(),
2243 0,
2244 "inline text should not occupy reusable slab slots",
2245 );
2246
2247 let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
2248 let _clone = owned_text.clone();
2249 pool_return_reusable(SqliteValue::Text(owned_text));
2250 assert_eq!(
2251 pool_len(),
2252 1,
2253 "heap-owned text should stay reusable even after serving shared clones",
2254 );
2255 assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
2256 assert_eq!(pool_len(), 0);
2257
2258 let shared_text =
2259 Arc::<str>::from("this string is definitely longer than twenty three bytes");
2260 pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
2261 &shared_text,
2262 ))));
2263 assert_eq!(
2264 pool_len(),
2265 0,
2266 "arc-backed shared text should not enter the reusable slab",
2267 );
2268
2269 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
2270 pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
2271 assert_eq!(
2272 pool_len(),
2273 0,
2274 "shared blob allocations should not displace reusable slab entries",
2275 );
2276
2277 let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
2278 pool_return_reusable(SqliteValue::Blob(unique_blob));
2279 assert_eq!(
2280 pool_len(),
2281 1,
2282 "unique blob allocations should remain eligible for slab reuse",
2283 );
2284 }
2285
2286 #[test]
2287 fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
2288 let text = Arc::new(SmallText::new(
2289 "this string is definitely longer than twenty three bytes",
2290 ));
2291 let expected = text.as_str().to_owned();
2292 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2293 panic!("long text should start in heap-owned mode");
2294 };
2295 assert!(
2296 shared.get().is_none(),
2297 "shared Arc should still be lazy before concurrent clones"
2298 );
2299
2300 let barrier = Arc::new(std::sync::Barrier::new(5));
2301 let mut workers = Vec::new();
2302 for _ in 0..4 {
2303 let text = Arc::clone(&text);
2304 let barrier = Arc::clone(&barrier);
2305 let expected = expected.clone();
2306 workers.push(std::thread::spawn(move || {
2307 barrier.wait();
2308 for _ in 0..64 {
2309 let cloned = (*text).clone();
2310 assert_eq!(cloned.as_str(), expected);
2311 assert!(
2312 matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2313 "concurrent clone should reuse the shared Arc representation"
2314 );
2315 }
2316 }));
2317 }
2318
2319 barrier.wait();
2320 for worker in workers {
2321 worker
2322 .join()
2323 .expect("join concurrent small-text clone worker");
2324 }
2325
2326 let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2327 panic!("original text should remain heap-owned after clone promotion");
2328 };
2329 let shared = shared
2330 .get()
2331 .expect("concurrent clones should promote the lazy shared Arc");
2332 assert_eq!(shared.as_ref(), expected);
2333 assert_eq!(text.as_str(), expected);
2334 }
2335
2336 #[test]
2337 fn null_properties() {
2338 let v = SqliteValue::Null;
2339 assert!(v.is_null());
2340 assert_eq!(v.to_integer(), 0);
2341 assert_eq!(v.to_float(), 0.0);
2342 assert_eq!(v.to_text(), "");
2343 assert_eq!(v.to_string(), "NULL");
2344 }
2345
2346 #[test]
2347 fn integer_properties() {
2348 let v = SqliteValue::Integer(42);
2349 assert!(!v.is_null());
2350 assert_eq!(v.as_integer(), Some(42));
2351 assert_eq!(v.to_integer(), 42);
2352 assert_eq!(v.to_float(), 42.0);
2353 assert_eq!(v.to_text(), "42");
2354 }
2355
2356 #[test]
2357 fn float_properties() {
2358 let v = SqliteValue::Float(3.14);
2359 assert_eq!(v.as_float(), Some(3.14));
2360 assert_eq!(v.to_integer(), 3);
2361 assert_eq!(v.to_text(), "3.14");
2362 }
2363
2364 #[test]
2365 fn text_properties() {
2366 let v = SqliteValue::Text(SmallText::new("hello"));
2367 assert_eq!(v.as_text(), Some("hello"));
2368 assert_eq!(v.to_integer(), 0);
2369 assert_eq!(v.to_float(), 0.0);
2370 }
2371
2372 #[test]
2373 fn text_numeric_coercion() {
2374 let v = SqliteValue::Text(SmallText::new("123"));
2375 assert_eq!(v.to_integer(), 123);
2376 assert_eq!(v.to_float(), 123.0);
2377
2378 let v = SqliteValue::Text(SmallText::new("3.14"));
2379 assert_eq!(v.to_integer(), 3);
2380 assert_eq!(v.to_float(), 3.14);
2381 }
2382
2383 #[test]
2384 fn text_numeric_coercion_ignores_hex_text_prefixes() {
2385 let v = SqliteValue::Text(SmallText::new("0x10"));
2386 assert_eq!(v.to_integer(), 0);
2387 assert_eq!(v.to_float(), 0.0);
2388
2389 let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
2390 assert_eq!(v.to_integer(), 0);
2391 assert_eq!(v.to_float(), 0.0);
2392 }
2393
2394 #[test]
2395 fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
2396 assert_eq!(
2397 SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
2398 SqliteValue::Integer(123)
2399 );
2400 assert_eq!(
2401 SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
2402 SqliteValue::Float(0.0)
2403 );
2404 assert_eq!(
2405 SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
2406 SqliteValue::Float(123.0)
2407 );
2408 assert_eq!(
2409 SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
2410 SqliteValue::Float(1.0)
2411 );
2412 assert_eq!(
2413 SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
2414 SqliteValue::Float(123.0)
2415 );
2416 assert_eq!(
2417 SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
2418 SqliteValue::Float(0.0)
2419 );
2420 assert_eq!(
2421 SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
2422 SqliteValue::Float(123.0)
2423 );
2424 }
2425
2426 #[test]
2427 fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
2428 assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
2429 assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
2430 assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
2431 assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
2432 }
2433
2434 #[test]
2435 fn test_sqlite_value_integer_real_comparison_equal() {
2436 let int_value = SqliteValue::Integer(3);
2437 let real_value = SqliteValue::Float(3.0);
2438 assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
2439 assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
2440 }
2441
2442 #[test]
2443 fn test_sqlite_value_text_to_integer_coercion() {
2444 let text_value = SqliteValue::Text(SmallText::new("123"));
2445 let coerced = text_value.apply_affinity(TypeAffinity::Integer);
2446 assert_eq!(coerced, SqliteValue::Integer(123));
2447 }
2448
2449 #[test]
2450 fn blob_properties() {
2451 let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
2452 assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
2453 assert_eq!(v.to_integer(), 0);
2454 assert_eq!(v.to_float(), 0.0);
2455 assert_eq!(v.to_text(), "\u{07AD}");
2458 }
2459
2460 #[test]
2461 fn display_formatting() {
2462 assert_eq!(SqliteValue::Null.to_string(), "NULL");
2463 assert_eq!(SqliteValue::Integer(42).to_string(), "42");
2464 assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
2465 assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
2466 assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
2467 assert_eq!(
2468 SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
2469 "X'CAFE'"
2470 );
2471 }
2472
2473 #[test]
2474 fn sort_order_null_first() {
2475 let null = SqliteValue::Null;
2476 let int = SqliteValue::Integer(0);
2477 let text = SqliteValue::Text(SmallText::new(""));
2478 let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
2479
2480 assert!(null < int);
2481 assert!(int < text);
2482 assert!(text < blob);
2483 }
2484
2485 #[test]
2486 fn sort_order_integers() {
2487 let a = SqliteValue::Integer(1);
2488 let b = SqliteValue::Integer(2);
2489 assert!(a < b);
2490 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
2491 }
2492
2493 #[test]
2494 fn sort_order_mixed_numeric() {
2495 let int = SqliteValue::Integer(1);
2496 let float = SqliteValue::Float(1.5);
2497 assert!(int < float);
2498
2499 let int = SqliteValue::Integer(2);
2500 assert!(int > float);
2501 }
2502
2503 #[test]
2504 fn test_int_float_precision_at_i64_boundary() {
2505 let imax = SqliteValue::Integer(i64::MAX);
2509 let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
2510 assert_eq!(
2511 imax.partial_cmp(&fmax),
2512 Some(Ordering::Less),
2513 "i64::MAX must be Less than 9223372036854775808.0"
2514 );
2515
2516 let a = SqliteValue::Integer(i64::MAX);
2518 let b = SqliteValue::Integer(i64::MAX - 1);
2519 let f = SqliteValue::Float(i64::MAX as f64);
2520 assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
2522 assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
2524 assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
2525 }
2526
2527 #[test]
2528 fn test_int_float_precision_symmetric() {
2529 let i = SqliteValue::Integer(i64::MAX);
2531 let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
2532 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
2533 }
2534
2535 #[test]
2536 fn test_int_float_exact_representation() {
2537 let i = SqliteValue::Integer(42);
2539 let f = SqliteValue::Float(42.0);
2540 assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
2541 assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
2542
2543 let i = SqliteValue::Integer(3);
2545 let f = SqliteValue::Float(3.5);
2546 assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
2547 assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
2548 }
2549
2550 #[test]
2551 fn from_conversions() {
2552 assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
2553 assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
2554 assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
2555 assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
2556 assert_eq!(
2557 SqliteValue::from(String::from("world")).as_text(),
2558 Some("world")
2559 );
2560 assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
2561 assert!(SqliteValue::from(None::<i64>).is_null());
2562 assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
2563 }
2564
2565 #[test]
2566 fn affinity() {
2567 assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
2568 assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
2569 assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
2570 assert_eq!(
2571 SqliteValue::Text(SmallText::new("")).affinity(),
2572 TypeAffinity::Text
2573 );
2574 assert_eq!(
2575 SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
2576 TypeAffinity::Blob
2577 );
2578 }
2579
2580 #[test]
2581 fn null_equality() {
2582 let a = SqliteValue::Null;
2584 let b = SqliteValue::Null;
2585 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
2586 }
2587
2588 #[test]
2591 fn test_storage_class_variants() {
2592 assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
2593 assert_eq!(
2594 SqliteValue::Integer(42).storage_class(),
2595 StorageClass::Integer
2596 );
2597 assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
2598 assert_eq!(
2599 SqliteValue::Text("hi".into()).storage_class(),
2600 StorageClass::Text
2601 );
2602 assert_eq!(
2603 SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
2604 StorageClass::Blob
2605 );
2606 }
2607
2608 #[test]
2609 fn test_type_affinity_advisory_text_into_integer_ok() {
2610 let val = SqliteValue::Text("hello".into());
2613 let coerced = val.apply_affinity(TypeAffinity::Integer);
2614 assert!(coerced.as_text().is_some());
2615 assert_eq!(coerced.as_text().unwrap(), "hello");
2616
2617 let val = SqliteValue::Text("42".into());
2619 let coerced = val.apply_affinity(TypeAffinity::Integer);
2620 assert_eq!(coerced.as_integer(), Some(42));
2621 }
2622
2623 #[test]
2624 fn test_type_affinity_advisory_integer_into_text_ok() {
2625 let val = SqliteValue::Integer(42);
2627 let coerced = val.apply_affinity(TypeAffinity::Text);
2628 assert_eq!(coerced.as_text(), Some("42"));
2629 }
2630
2631 #[test]
2632 fn test_type_affinity_comparison_coercion_matches_oracle() {
2633 let val = SqliteValue::Text("123".into());
2635 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2636 assert_eq!(coerced.as_integer(), Some(123));
2637
2638 let val = SqliteValue::Text("3.14".into());
2640 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2641 assert_eq!(coerced.as_float(), Some(3.14));
2642
2643 let val = SqliteValue::Text("hello".into());
2645 let coerced = val.apply_affinity(TypeAffinity::Numeric);
2646 assert!(coerced.as_text().is_some());
2647
2648 let val = SqliteValue::Integer(42);
2650 let coerced = val.apply_affinity(TypeAffinity::Blob);
2651 assert_eq!(coerced.as_integer(), Some(42));
2652
2653 let val = SqliteValue::Float(5.0);
2655 let coerced = val.apply_affinity(TypeAffinity::Integer);
2656 assert_eq!(coerced.as_integer(), Some(5));
2657
2658 let val = SqliteValue::Float(5.5);
2660 let coerced = val.apply_affinity(TypeAffinity::Integer);
2661 assert_eq!(coerced.as_float(), Some(5.5));
2662
2663 let val = SqliteValue::Integer(7);
2665 let coerced = val.apply_affinity(TypeAffinity::Real);
2666 assert_eq!(coerced.as_float(), Some(7.0));
2667
2668 let val = SqliteValue::Text("9".into());
2670 let coerced = val.apply_affinity(TypeAffinity::Real);
2671 assert_eq!(coerced.as_float(), Some(9.0));
2672 }
2673
2674 #[test]
2675 fn test_cast_to_numeric_uses_sqlite_cast_rules() {
2676 assert_eq!(
2677 SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
2678 SqliteValue::Integer(123)
2679 );
2680 assert_eq!(
2681 SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
2682 SqliteValue::Integer(150)
2683 );
2684 assert_eq!(
2685 SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
2686 SqliteValue::Integer(0)
2687 );
2688 assert_eq!(
2689 SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
2690 SqliteValue::Integer(123)
2691 );
2692
2693 match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
2694 SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
2695 other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
2696 }
2697 }
2698
2699 #[test]
2700 fn test_strict_table_rejects_text_into_integer() {
2701 let val = SqliteValue::Text("hello".into());
2702 let result = val.validate_strict(StrictColumnType::Integer);
2703 assert!(result.is_err());
2704 let err = result.unwrap_err();
2705 assert_eq!(err.expected, StrictColumnType::Integer);
2706 assert_eq!(err.actual, StorageClass::Text);
2707 }
2708
2709 #[test]
2710 fn test_strict_table_allows_exact_type() {
2711 let val = SqliteValue::Integer(42);
2713 assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
2714
2715 let val = SqliteValue::Float(3.14);
2717 assert!(val.validate_strict(StrictColumnType::Real).is_ok());
2718
2719 let val = SqliteValue::Text("hello".into());
2721 assert!(val.validate_strict(StrictColumnType::Text).is_ok());
2722
2723 let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
2725 assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
2726
2727 assert!(
2729 SqliteValue::Null
2730 .validate_strict(StrictColumnType::Integer)
2731 .is_ok()
2732 );
2733 assert!(
2734 SqliteValue::Null
2735 .validate_strict(StrictColumnType::Text)
2736 .is_ok()
2737 );
2738
2739 let val = SqliteValue::Integer(42);
2741 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
2742 let val = SqliteValue::Text("hi".into());
2743 assert!(val.validate_strict(StrictColumnType::Any).is_ok());
2744 }
2745
2746 #[test]
2747 fn test_strict_real_accepts_integer_with_coercion() {
2748 let val = SqliteValue::Integer(42);
2750 let result = val.validate_strict(StrictColumnType::Real).unwrap();
2751 assert_eq!(result.as_float(), Some(42.0));
2752 }
2753
2754 #[test]
2755 fn test_strict_rejects_wrong_storage_classes() {
2756 assert!(
2758 SqliteValue::Float(3.14)
2759 .validate_strict(StrictColumnType::Integer)
2760 .is_err()
2761 );
2762
2763 assert!(
2765 SqliteValue::Blob(Arc::from([1u8].as_slice()))
2766 .validate_strict(StrictColumnType::Text)
2767 .is_err()
2768 );
2769
2770 assert!(
2772 SqliteValue::Integer(1)
2773 .validate_strict(StrictColumnType::Text)
2774 .is_err()
2775 );
2776
2777 assert!(
2779 SqliteValue::Text("x".into())
2780 .validate_strict(StrictColumnType::Blob)
2781 .is_err()
2782 );
2783 }
2784
2785 #[test]
2786 fn test_strict_column_type_parsing() {
2787 assert_eq!(
2788 StrictColumnType::from_type_name("INT"),
2789 Some(StrictColumnType::Integer)
2790 );
2791 assert_eq!(
2792 StrictColumnType::from_type_name("INTEGER"),
2793 Some(StrictColumnType::Integer)
2794 );
2795 assert_eq!(
2796 StrictColumnType::from_type_name("REAL"),
2797 Some(StrictColumnType::Real)
2798 );
2799 assert_eq!(
2800 StrictColumnType::from_type_name("TEXT"),
2801 Some(StrictColumnType::Text)
2802 );
2803 assert_eq!(
2804 StrictColumnType::from_type_name("BLOB"),
2805 Some(StrictColumnType::Blob)
2806 );
2807 assert_eq!(
2808 StrictColumnType::from_type_name("ANY"),
2809 Some(StrictColumnType::Any)
2810 );
2811 assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
2813 assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
2814 }
2815
2816 #[test]
2817 fn test_affinity_advisory_never_rejects() {
2818 let values = vec![
2820 SqliteValue::Null,
2821 SqliteValue::Integer(42),
2822 SqliteValue::Float(3.14),
2823 SqliteValue::Text("hello".into()),
2824 SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
2825 ];
2826 let affinities = [
2827 TypeAffinity::Integer,
2828 TypeAffinity::Text,
2829 TypeAffinity::Blob,
2830 TypeAffinity::Real,
2831 TypeAffinity::Numeric,
2832 ];
2833 for val in &values {
2834 for aff in &affinities {
2835 let _ = val.clone().apply_affinity(*aff);
2837 }
2838 }
2839 }
2840
2841 #[test]
2844 fn test_unique_allows_multiple_nulls_single_column() {
2845 let a = SqliteValue::Null;
2847 let b = SqliteValue::Null;
2848 assert!(!a.unique_eq(&b));
2849 }
2850
2851 #[test]
2852 fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
2853 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
2856 let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
2857 assert!(!unique_key_duplicates(&row_a, &row_b));
2858
2859 let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
2861 let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
2862 assert!(!unique_key_duplicates(&row_a, &row_b));
2863
2864 let row_a = [SqliteValue::Null, SqliteValue::Null];
2866 let row_b = [SqliteValue::Null, SqliteValue::Null];
2867 assert!(!unique_key_duplicates(&row_a, &row_b));
2868 }
2869
2870 #[test]
2871 fn test_unique_rejects_duplicate_non_null() {
2872 let a = SqliteValue::Integer(42);
2874 let b = SqliteValue::Integer(42);
2875 assert!(a.unique_eq(&b));
2876
2877 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2879 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2880 assert!(unique_key_duplicates(&row_a, &row_b));
2881
2882 let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
2884 let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
2885 assert!(!unique_key_duplicates(&row_a, &row_b));
2886 }
2887
2888 #[test]
2889 fn test_unique_null_vs_non_null_distinct() {
2890 let a = SqliteValue::Null;
2892 let b = SqliteValue::Integer(1);
2893 assert!(!a.unique_eq(&b));
2894 assert!(!b.unique_eq(&a));
2895
2896 let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
2898 let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
2899 assert!(!unique_key_duplicates(&row_a, &row_b));
2900 }
2901
2902 #[test]
2905 #[allow(clippy::cast_precision_loss)]
2906 fn test_integer_overflow_promotes_real_expr_add() {
2907 let max = SqliteValue::Integer(i64::MAX);
2908 let one = SqliteValue::Integer(1);
2909 let result = max.sql_add(&one);
2910 assert!(result.as_integer().is_none());
2912 assert!(result.as_float().is_some());
2913 assert!(result.as_float().unwrap() >= i64::MAX as f64);
2915 }
2916
2917 #[test]
2918 fn test_integer_overflow_promotes_real_expr_mul() {
2919 let max = SqliteValue::Integer(i64::MAX);
2920 let two = SqliteValue::Integer(2);
2921 let result = max.sql_mul(&two);
2922 assert!(result.as_float().is_some());
2924 }
2925
2926 #[test]
2927 fn test_integer_overflow_promotes_real_expr_sub() {
2928 let min = SqliteValue::Integer(i64::MIN);
2929 let one = SqliteValue::Integer(1);
2930 let result = min.sql_sub(&one);
2931 assert!(result.as_float().is_some());
2933 }
2934
2935 #[test]
2936 fn test_sum_overflow_errors() {
2937 let mut acc = SumAccumulator::new();
2938 acc.accumulate(&SqliteValue::Integer(i64::MAX));
2939 acc.accumulate(&SqliteValue::Integer(1));
2940 let result = acc.finish();
2941 assert!(result.is_err());
2942 }
2943
2944 #[test]
2945 fn test_sum_overflow_then_float_returns_real() {
2946 let mut acc = SumAccumulator::new();
2947 acc.accumulate(&SqliteValue::Integer(i64::MAX));
2948 acc.accumulate(&SqliteValue::Integer(1));
2949 acc.accumulate(&SqliteValue::Float(0.5));
2950 let result = acc.finish().unwrap();
2951 assert!(matches!(result, SqliteValue::Float(_)));
2952 }
2953
2954 #[test]
2955 fn test_sum_text_integer_literals_stay_integer() {
2956 let mut acc = SumAccumulator::new();
2957 acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
2958 acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
2959 let result = acc.finish().unwrap();
2960 assert_eq!(result.as_integer(), Some(3));
2961 }
2962
2963 #[test]
2964 fn test_sum_non_numeric_text_returns_real_zero() {
2965 let mut acc = SumAccumulator::new();
2966 acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
2967 let result = acc.finish().unwrap();
2968 assert_eq!(result.as_float(), Some(0.0));
2969 }
2970
2971 #[test]
2972 fn test_no_overflow_stays_integer() {
2973 let a = SqliteValue::Integer(100);
2975 let b = SqliteValue::Integer(200);
2976 let result = a.sql_add(&b);
2977 assert_eq!(result.as_integer(), Some(300));
2978
2979 let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
2981 assert_eq!(result.as_integer(), Some(42));
2982
2983 let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
2985 assert_eq!(result.as_integer(), Some(42));
2986 }
2987
2988 #[test]
2989 fn test_sum_null_only_returns_null() {
2990 let mut acc = SumAccumulator::new();
2991 acc.accumulate(&SqliteValue::Null);
2992 acc.accumulate(&SqliteValue::Null);
2993 let result = acc.finish().unwrap();
2994 assert!(result.is_null());
2995 }
2996
2997 #[test]
2998 fn test_sum_mixed_int_float() {
2999 let mut acc = SumAccumulator::new();
3000 acc.accumulate(&SqliteValue::Integer(10));
3001 acc.accumulate(&SqliteValue::Float(2.5));
3002 acc.accumulate(&SqliteValue::Integer(3));
3003 let result = acc.finish().unwrap();
3004 assert_eq!(result.as_float(), Some(15.5));
3006 }
3007
3008 #[test]
3009 fn test_sum_integer_only() {
3010 let mut acc = SumAccumulator::new();
3011 acc.accumulate(&SqliteValue::Integer(10));
3012 acc.accumulate(&SqliteValue::Integer(20));
3013 acc.accumulate(&SqliteValue::Integer(30));
3014 let result = acc.finish().unwrap();
3015 assert_eq!(result.as_integer(), Some(60));
3016 }
3017
3018 #[test]
3019 fn test_sql_arithmetic_null_propagation() {
3020 let n = SqliteValue::Null;
3021 let i = SqliteValue::Integer(42);
3022 assert!(n.sql_add(&i).is_null());
3023 assert!(i.sql_add(&n).is_null());
3024 assert!(n.sql_sub(&i).is_null());
3025 assert!(n.sql_mul(&i).is_null());
3026 }
3027
3028 #[test]
3029 fn test_sql_inf_arithmetic_nan_normalized_to_null() {
3030 let pos_inf = SqliteValue::Float(f64::INFINITY);
3032 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3033 assert!(pos_inf.sql_add(&neg_inf).is_null());
3034
3035 assert!(pos_inf.sql_sub(&pos_inf).is_null());
3037 }
3038
3039 #[test]
3040 fn test_sql_mul_zero_times_inf_normalized_to_null() {
3041 let zero = SqliteValue::Float(0.0);
3043 let pos_inf = SqliteValue::Float(f64::INFINITY);
3044 assert!(zero.sql_mul(&pos_inf).is_null());
3045 assert!(
3046 SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
3047 "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
3048 );
3049 }
3050
3051 #[test]
3052 fn test_sql_mul_mixed_int_float_stays_real() {
3053 let left = SqliteValue::Integer(10);
3054 let right = SqliteValue::Float(0.25);
3055 assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
3056 assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
3057 }
3058
3059 #[test]
3060 fn test_sql_inf_propagates_when_not_nan() {
3061 let pos_inf = SqliteValue::Float(f64::INFINITY);
3062 let one = SqliteValue::Integer(1);
3063 let add_result = pos_inf.sql_add(&one);
3064 assert!(
3065 matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
3066 "expected +Inf propagation, got {add_result:?}"
3067 );
3068
3069 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3070 let sub_result = neg_inf.sql_sub(&one);
3071 assert!(
3072 matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
3073 "expected -Inf propagation, got {sub_result:?}"
3074 );
3075 }
3076
3077 #[test]
3078 fn test_from_f64_nan_normalizes_to_null() {
3079 let value = SqliteValue::from(f64::NAN);
3080 assert!(value.is_null());
3081 }
3082
3083 #[test]
3084 fn test_inf_comparisons_against_finite_values() {
3085 let pos_inf = SqliteValue::Float(f64::INFINITY);
3086 let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3087 let finite_hi = SqliteValue::Float(1.0e308);
3088 let finite_lo = SqliteValue::Float(-1.0e308);
3089
3090 assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
3091 assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
3092 }
3093
3094 #[test]
3097 fn test_empty_string_is_not_null() {
3098 let empty = SqliteValue::Text(SmallText::new(""));
3099 assert!(!empty.is_null());
3101 assert!(!empty.is_null());
3103 assert!(SqliteValue::Null.is_null());
3105 }
3106
3107 #[test]
3108 fn test_length_empty_string_zero() {
3109 let empty = SqliteValue::Text(SmallText::new(""));
3110 assert_eq!(empty.sql_length(), Some(0));
3111 }
3112
3113 #[test]
3114 fn test_typeof_empty_string_text() {
3115 let empty = SqliteValue::Text(SmallText::new(""));
3116 assert_eq!(empty.typeof_str(), "text");
3117 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3119 }
3120
3121 #[test]
3122 fn test_empty_string_comparisons() {
3123 let empty1 = SqliteValue::Text(SmallText::new(""));
3124 let empty2 = SqliteValue::Text(SmallText::new(""));
3125 assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
3127
3128 let null = SqliteValue::Null;
3132 assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
3133 }
3134
3135 #[test]
3136 fn test_typeof_all_variants() {
3137 assert_eq!(SqliteValue::Null.typeof_str(), "null");
3138 assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
3139 assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
3140 assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
3141 assert_eq!(
3142 SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
3143 "blob"
3144 );
3145 }
3146
3147 #[test]
3148 fn test_sql_length_all_types() {
3149 assert_eq!(SqliteValue::Null.sql_length(), None);
3151 assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
3153 assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
3154 assert_eq!(
3156 SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
3157 Some(3)
3158 );
3159 assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
3161 assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); }
3164
3165 #[test]
3168 fn test_like_ascii_case_insensitive() {
3169 assert!(sql_like("A", "a", None));
3170 assert!(sql_like("a", "A", None));
3171 assert!(sql_like("hello", "HELLO", None));
3172 assert!(sql_like("HELLO", "hello", None));
3173 assert!(sql_like("HeLLo", "hEllO", None));
3174 }
3175
3176 #[test]
3177 fn test_like_unicode_case_sensitive_without_icu() {
3178 assert!(!sql_like("ä", "Ä", None));
3180 assert!(!sql_like("Ä", "ä", None));
3181 assert!(sql_like("ä", "ä", None));
3183 }
3184
3185 #[test]
3186 fn test_like_fast_path_does_not_fold_ascii_punctuation() {
3187 assert!(!sql_like("[", "{", None));
3188 assert!(!sql_like("@", "`", None));
3189 }
3190
3191 #[test]
3192 fn test_like_escape_handling() {
3193 assert!(sql_like("100\\%", "100%", Some('\\')));
3195 assert!(!sql_like("100\\%", "100x", Some('\\')));
3196
3197 assert!(sql_like("a\\_b", "a_b", Some('\\')));
3199 assert!(!sql_like("a\\_b", "axb", Some('\\')));
3200 }
3201
3202 #[test]
3203 fn test_like_wildcards_basic() {
3204 assert!(sql_like("%", "", None));
3206 assert!(sql_like("%", "anything", None));
3207 assert!(sql_like("a%", "abc", None));
3208 assert!(sql_like("%c", "abc", None));
3209 assert!(sql_like("a%c", "abc", None));
3210 assert!(sql_like("a%c", "aXYZc", None));
3211 assert!(!sql_like("a%c", "abd", None));
3212
3213 assert!(sql_like("_", "x", None));
3215 assert!(!sql_like("_", "", None));
3216 assert!(!sql_like("_", "xy", None));
3217 assert!(sql_like("a_c", "abc", None));
3218 assert!(!sql_like("a_c", "abbc", None));
3219 }
3220
3221 #[test]
3222 fn test_like_combined_wildcards() {
3223 assert!(sql_like("%_", "a", None));
3224 assert!(!sql_like("%_", "", None));
3225 assert!(sql_like("_%_", "ab", None));
3226 assert!(!sql_like("_%_", "a", None));
3227 assert!(sql_like("%a%b%", "xaybz", None));
3228 assert!(!sql_like("%a%b%", "xyz", None));
3229 }
3230
3231 #[test]
3232 fn test_like_exact_match() {
3233 assert!(sql_like("hello", "hello", None));
3234 assert!(!sql_like("hello", "world", None));
3235 assert!(sql_like("", "", None));
3236 assert!(!sql_like("a", "", None));
3237 assert!(!sql_like("", "a", None));
3238 }
3239
3240 #[test]
3241 fn test_like_fast_path_repeated_percent_shapes() {
3242 assert!(sql_like("ab%%", "ABcd", None));
3243 assert!(sql_like("%%cd", "abCD", None));
3244 assert!(sql_like("%%bc%%", "xxBCyy", None));
3245 assert!(sql_like("%%%%", "anything", None));
3246 }
3247
3248 #[test]
3249 fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
3250 assert!(sql_like("%éL%", "héllo", None));
3251 assert!(!sql_like("%Él%", "héllo", None));
3252 assert!(sql_like("Stra%", "straße", None));
3253 }
3254
3255 #[test]
3256 fn test_like_contains_fast_path_handles_overlapping_matches() {
3257 assert!(sql_like("%ana%", "bananas", None));
3258 assert!(sql_like("%NAN%", "baNanas", None));
3259 assert!(!sql_like("%ananasx%", "bananas", None));
3260 }
3261
3262 #[test]
3263 fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
3264 assert!(sql_like("%ß%", "straße", None));
3265 assert!(!sql_like("%SS%", "straße", None));
3266 }
3267
3268 #[test]
3271 fn test_format_sqlite_float_whole_number() {
3272 assert_eq!(format_sqlite_float(120.0), "120.0");
3273 assert_eq!(format_sqlite_float(0.0), "0.0");
3274 assert_eq!(format_sqlite_float(-42.0), "-42.0");
3275 assert_eq!(format_sqlite_float(1.0), "1.0");
3276 }
3277
3278 #[test]
3279 fn test_format_sqlite_float_fractional() {
3280 assert_eq!(format_sqlite_float(3.14), "3.14");
3281 assert_eq!(format_sqlite_float(0.5), "0.5");
3282 assert_eq!(format_sqlite_float(-0.001), "-0.001");
3283 }
3284
3285 #[test]
3286 fn test_format_sqlite_float_special() {
3287 assert_eq!(format_sqlite_float(f64::NAN), "NaN");
3288 assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
3289 assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
3290 }
3291
3292 #[test]
3293 fn test_format_sqlite_float_negative_zero() {
3294 assert_eq!(format_sqlite_float(-0.0), "-0.0");
3296 assert_eq!(format_sqlite_float(0.0), "0.0");
3297 }
3298
3299 #[test]
3300 fn test_float_to_text_includes_decimal_point() {
3301 let v = SqliteValue::Float(100.0);
3302 assert_eq!(v.to_text(), "100.0");
3303 let v = SqliteValue::Float(3.14);
3304 assert_eq!(v.to_text(), "3.14");
3305 }
3306
3307 #[test]
3310 fn test_scan_numeric_prefix_bare_dot() {
3311 assert_eq!(scan_numeric_prefix(b"."), 0);
3313 assert_eq!(scan_numeric_prefix(b"-."), 0);
3314 assert_eq!(scan_numeric_prefix(b"+."), 0);
3315 assert_eq!(scan_numeric_prefix(b"..1"), 0);
3316 }
3317
3318 #[test]
3319 fn test_scan_numeric_prefix_valid() {
3320 assert_eq!(scan_numeric_prefix(b"123"), 3);
3321 assert_eq!(scan_numeric_prefix(b"3.14"), 4);
3322 assert_eq!(scan_numeric_prefix(b".5"), 2);
3323 assert_eq!(scan_numeric_prefix(b"1e10"), 4);
3324 assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
3325 assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
3326 assert_eq!(scan_numeric_prefix(b"0.0"), 3);
3327 }
3328
3329 #[test]
3330 fn test_scan_numeric_prefix_empty_and_non_numeric() {
3331 assert_eq!(scan_numeric_prefix(b""), 0);
3332 assert_eq!(scan_numeric_prefix(b"abc"), 0);
3333 assert_eq!(scan_numeric_prefix(b"+"), 0);
3334 assert_eq!(scan_numeric_prefix(b"-"), 0);
3335 }
3336}