1use std::slice;
36
37use crate::ingress::{MAX_ARRAY_DIMS, MAX_NDARRAY_LEAF_ELEMS};
38use crate::{Result, error};
39
40use super::chunk::ValidityDescriptor;
41use super::wire::{
42 F32_NULL, F64_NULL, I8_NULL, I16_NULL, I32_NULL, I64_NULL, QWP_TYPE_BOOLEAN, QWP_TYPE_BYTE,
43 QWP_TYPE_CHAR, QWP_TYPE_DATE, QWP_TYPE_DECIMAL64, QWP_TYPE_DECIMAL128, QWP_TYPE_DECIMAL256,
44 QWP_TYPE_DOUBLE, QWP_TYPE_DOUBLE_ARRAY, QWP_TYPE_FLOAT, QWP_TYPE_GEOHASH, QWP_TYPE_INT,
45 QWP_TYPE_IPV4, QWP_TYPE_LONG, QWP_TYPE_LONG256, QWP_TYPE_SHORT, QWP_TYPE_TIMESTAMP,
46 QWP_TYPE_TIMESTAMP_NANOS, QWP_TYPE_UUID, write_qwp_varint,
47};
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum NumpyDtype {
58 I64Direct,
60 F64Direct,
61 DateI64Direct,
62 TimestampMicrosDirect,
63 TimestampNanosDirect,
64 LongDirect,
65 UuidDirect,
66 Long256Direct,
67 Ipv4Direct,
68 CharDirect,
69
70 I8Direct,
73 I16Direct,
74 I32Direct,
75
76 I8WidenToI32,
79 I16WidenToI32,
80 I32WidenToI64,
81
82 U8WidenToI32,
85 U16WidenToI32,
86 U32WidenToI64,
87 U64WidenToI64,
88
89 F32Direct,
91 F16Widen,
92
93 Bool,
95 DatetimeSecToMicros,
96 DatetimeMinuteToMicros,
97 DatetimeHourToMicros,
98 DatetimeDayToMicros,
99 DatetimeWeekToMicros,
100 DatetimeMonthToMicros,
101 DatetimeYearToMicros,
102
103 Decimal64 {
105 scale: u8,
106 },
107 Decimal128 {
108 scale: u8,
109 },
110 Decimal256 {
111 scale: u8,
112 },
113
114 GeohashI8 {
116 bits: u8,
117 },
118 GeohashI16 {
119 bits: u8,
120 },
121 GeohashI32 {
122 bits: u8,
123 },
124 GeohashI64 {
125 bits: u8,
126 },
127
128 F64Ndarray {
133 ndim: u8,
134 shape: [u32; MAX_ARRAY_DIMS],
135 },
136}
137
138impl NumpyDtype {
139 pub fn wire_type(&self) -> u8 {
141 use NumpyDtype as D;
142 match self {
143 D::I8Direct => QWP_TYPE_BYTE,
144 D::I16Direct => QWP_TYPE_SHORT,
145 D::I32Direct
146 | D::I8WidenToI32
147 | D::I16WidenToI32
148 | D::U8WidenToI32
149 | D::U16WidenToI32 => QWP_TYPE_INT,
150 D::I64Direct
151 | D::LongDirect
152 | D::I32WidenToI64
153 | D::U32WidenToI64
154 | D::U64WidenToI64 => QWP_TYPE_LONG,
155 D::F64Direct => QWP_TYPE_DOUBLE,
156 D::F32Direct | D::F16Widen => QWP_TYPE_FLOAT,
157 D::Bool => QWP_TYPE_BOOLEAN,
158 D::DateI64Direct => QWP_TYPE_DATE,
159 D::TimestampMicrosDirect
160 | D::DatetimeSecToMicros
161 | D::DatetimeMinuteToMicros
162 | D::DatetimeHourToMicros
163 | D::DatetimeDayToMicros
164 | D::DatetimeWeekToMicros
165 | D::DatetimeMonthToMicros
166 | D::DatetimeYearToMicros => QWP_TYPE_TIMESTAMP,
167 D::TimestampNanosDirect => QWP_TYPE_TIMESTAMP_NANOS,
168 D::UuidDirect => QWP_TYPE_UUID,
169 D::Long256Direct => QWP_TYPE_LONG256,
170 D::Ipv4Direct => QWP_TYPE_IPV4,
171 D::CharDirect => QWP_TYPE_CHAR,
172 D::Decimal64 { .. } => QWP_TYPE_DECIMAL64,
173 D::Decimal128 { .. } => QWP_TYPE_DECIMAL128,
174 D::Decimal256 { .. } => QWP_TYPE_DECIMAL256,
175 D::GeohashI8 { .. }
176 | D::GeohashI16 { .. }
177 | D::GeohashI32 { .. }
178 | D::GeohashI64 { .. } => QWP_TYPE_GEOHASH,
179 D::F64Ndarray { .. } => QWP_TYPE_DOUBLE_ARRAY,
180 }
181 }
182
183 pub fn bytes_per_row(&self) -> usize {
190 use NumpyDtype as D;
191 match self {
192 D::Bool | D::I8Direct => 1,
193 D::I16Direct | D::CharDirect => 2,
194 D::I32Direct
195 | D::I8WidenToI32
196 | D::I16WidenToI32
197 | D::U8WidenToI32
198 | D::U16WidenToI32
199 | D::F32Direct
200 | D::F16Widen
201 | D::Ipv4Direct => 4,
202 D::I64Direct
203 | D::F64Direct
204 | D::LongDirect
205 | D::DateI64Direct
206 | D::TimestampMicrosDirect
207 | D::TimestampNanosDirect
208 | D::DatetimeSecToMicros
209 | D::DatetimeMinuteToMicros
210 | D::DatetimeHourToMicros
211 | D::DatetimeDayToMicros
212 | D::DatetimeWeekToMicros
213 | D::DatetimeMonthToMicros
214 | D::DatetimeYearToMicros
215 | D::I32WidenToI64
216 | D::U32WidenToI64
217 | D::U64WidenToI64
218 | D::Decimal64 { .. } => 8,
219 D::UuidDirect | D::Decimal128 { .. } => 16,
220 D::Long256Direct | D::Decimal256 { .. } => 32,
221 D::GeohashI8 { .. } => 1,
222 D::GeohashI16 { .. } => 2,
223 D::GeohashI32 { .. } => 4,
224 D::GeohashI64 { .. } => 8,
225 D::F64Ndarray { ndim, shape } => {
226 let nd = (*ndim as usize).min(shape.len());
230 let mut leaf: usize = 1;
231 for &d in &shape[..nd] {
232 leaf = leaf.saturating_mul(d as usize);
233 }
234 (1usize)
235 .saturating_add(4usize.saturating_mul(nd))
236 .saturating_add(8usize.saturating_mul(leaf))
237 }
238 }
239 }
240
241 pub fn validate(&self) -> Result<()> {
248 if let NumpyDtype::F64Ndarray { ndim, shape } = self {
249 let nd = *ndim as usize;
250 if nd == 0 {
251 return Err(error::fmt!(InvalidApiCall, "F64Ndarray ndim must be >= 1"));
252 }
253 if nd > MAX_ARRAY_DIMS {
254 return Err(error::fmt!(
255 InvalidApiCall,
256 "F64Ndarray ndim must be <= {} (MAX_ARRAY_DIMS), got {}",
257 MAX_ARRAY_DIMS,
258 nd
259 ));
260 }
261 let mut leaf_count: usize = 1;
262 for (i, &dim) in shape[..nd].iter().enumerate() {
263 if dim == 0 {
264 return Err(error::fmt!(
265 InvalidApiCall,
266 "F64Ndarray shape[{}] must be >= 1, got 0",
267 i
268 ));
269 }
270 leaf_count = leaf_count.checked_mul(dim as usize).ok_or_else(|| {
271 error::fmt!(InvalidApiCall, "F64Ndarray shape product overflows usize")
272 })?;
273 if leaf_count > MAX_NDARRAY_LEAF_ELEMS {
274 return Err(error::fmt!(
275 InvalidApiCall,
276 "F64Ndarray shape product exceeds MAX_NDARRAY_LEAF_ELEMS ({}) at dim {}",
277 MAX_NDARRAY_LEAF_ELEMS,
278 i
279 ));
280 }
281 }
282 }
283 let geohash_bits = match self {
284 NumpyDtype::GeohashI8 { bits } => Some((*bits, 8u8)),
285 NumpyDtype::GeohashI16 { bits } => Some((*bits, 16u8)),
286 NumpyDtype::GeohashI32 { bits } => Some((*bits, 32u8)),
287 NumpyDtype::GeohashI64 { bits } => Some((*bits, 60u8)),
288 _ => None,
289 };
290 if let Some((bits, max_bits)) = geohash_bits
291 && (bits == 0 || bits > max_bits)
292 {
293 return Err(error::fmt!(
294 InvalidApiCall,
295 "geohash bits must be in 1..={}, got {}",
296 max_bits,
297 bits
298 ));
299 }
300 let decimal_scale = match self {
301 NumpyDtype::Decimal64 { scale } => Some((*scale, 18u8)),
302 NumpyDtype::Decimal128 { scale } => Some((*scale, 38u8)),
303 NumpyDtype::Decimal256 { scale } => Some((*scale, 76u8)),
304 _ => None,
305 };
306 if let Some((scale, max_scale)) = decimal_scale
307 && scale > max_scale
308 {
309 return Err(error::fmt!(
310 InvalidApiCall,
311 "decimal scale must be <= {}, got {}",
312 max_scale,
313 scale
314 ));
315 }
316 Ok(())
317 }
318
319 pub fn source_elem_size(&self) -> Result<usize> {
339 use NumpyDtype as D;
340 let n = match self {
341 D::I8Direct | D::I8WidenToI32 | D::U8WidenToI32 | D::Bool | D::GeohashI8 { .. } => 1,
342 D::I16Direct
343 | D::I16WidenToI32
344 | D::U16WidenToI32
345 | D::F16Widen
346 | D::CharDirect
347 | D::GeohashI16 { .. } => 2,
348 D::I32Direct
349 | D::I32WidenToI64
350 | D::U32WidenToI64
351 | D::F32Direct
352 | D::Ipv4Direct
353 | D::GeohashI32 { .. } => 4,
354 D::I64Direct
355 | D::F64Direct
356 | D::LongDirect
357 | D::DateI64Direct
358 | D::TimestampMicrosDirect
359 | D::TimestampNanosDirect
360 | D::U64WidenToI64
361 | D::DatetimeSecToMicros
362 | D::DatetimeMinuteToMicros
363 | D::DatetimeHourToMicros
364 | D::DatetimeDayToMicros
365 | D::DatetimeWeekToMicros
366 | D::DatetimeMonthToMicros
367 | D::DatetimeYearToMicros
368 | D::GeohashI64 { .. }
369 | D::Decimal64 { .. } => 8,
370 D::UuidDirect | D::Decimal128 { .. } => 16,
371 D::Long256Direct | D::Decimal256 { .. } => 32,
372 D::F64Ndarray { ndim, shape } => {
373 let nd = *ndim as usize;
374 if nd == 0 || nd > MAX_ARRAY_DIMS {
375 return Err(error::fmt!(
376 InvalidApiCall,
377 "F64Ndarray ndim must be in 1..={}, got {}",
378 MAX_ARRAY_DIMS,
379 nd
380 ));
381 }
382 let leaf: usize = shape[..nd]
383 .iter()
384 .copied()
385 .map(|d| d as usize)
386 .try_fold(1usize, |acc, d| acc.checked_mul(d))
387 .ok_or_else(|| {
388 error::fmt!(InvalidApiCall, "F64Ndarray shape product overflows usize")
389 })?;
390 return leaf.checked_mul(8).ok_or_else(|| {
391 error::fmt!(InvalidApiCall, "F64Ndarray row size overflows usize")
392 });
393 }
394 };
395 Ok(n)
396 }
397}
398
399pub(crate) unsafe fn emit_into_wire(
412 out: &mut Vec<u8>,
413 dtype: NumpyDtype,
414 data: *const u8,
415 row_count: usize,
416 validity: Option<&ValidityDescriptor>,
417) -> Result<()> {
418 use NumpyDtype as D;
419 match dtype {
420 D::I64Direct | D::LongDirect => unsafe {
422 emit_sentinel_le::<i64, 8>(
423 out,
424 data,
425 row_count,
426 validity,
427 I64_NULL.to_le_bytes(),
428 i64::to_le_bytes,
429 )
430 },
431 D::F64Direct => unsafe {
432 emit_sentinel_le::<f64, 8>(
433 out,
434 data,
435 row_count,
436 validity,
437 F64_NULL.to_le_bytes(),
438 f64::to_le_bytes,
439 )
440 },
441 D::CharDirect => unsafe {
442 emit_sentinel_le::<u16, 2>(out, data, row_count, validity, [0u8; 2], u16::to_le_bytes)
443 },
444
445 D::DateI64Direct => unsafe {
447 emit_bitmap_le::<i64, 8>(out, data, row_count, validity, i64::to_le_bytes)
448 },
449 D::TimestampMicrosDirect | D::TimestampNanosDirect => unsafe {
450 emit_bitmap_le::<i64, 8>(out, data, row_count, validity, i64::to_le_bytes)
451 },
452 D::Ipv4Direct => unsafe {
453 emit_bitmap_le::<u32, 4>(out, data, row_count, validity, u32::to_le_bytes)
454 },
455 D::UuidDirect => unsafe { emit_bitmap_fsb::<16>(out, data, row_count, validity) },
456 D::Long256Direct => unsafe { emit_bitmap_fsb::<32>(out, data, row_count, validity) },
457
458 D::I8Direct => unsafe {
460 emit_sentinel_le::<i8, 1>(out, data, row_count, validity, [I8_NULL as u8], |v| {
461 [v as u8]
462 })
463 },
464 D::I16Direct => unsafe {
465 emit_sentinel_le::<i16, 2>(
466 out,
467 data,
468 row_count,
469 validity,
470 I16_NULL.to_le_bytes(),
471 i16::to_le_bytes,
472 )
473 },
474 D::I32Direct => unsafe {
475 emit_sentinel_le::<i32, 4>(
476 out,
477 data,
478 row_count,
479 validity,
480 I32_NULL.to_le_bytes(),
481 i32::to_le_bytes,
482 )
483 },
484
485 D::I8WidenToI32 => unsafe {
487 emit_widen_i32_sentinel::<i8>(out, data, row_count, validity, I32_NULL, |v| v as i32)
488 },
489 D::I16WidenToI32 => unsafe {
490 emit_widen_i32_sentinel::<i16>(out, data, row_count, validity, I32_NULL, |v| v as i32)
491 },
492 D::I32WidenToI64 => unsafe {
493 emit_widen_i64_sentinel::<i32>(out, data, row_count, validity, I64_NULL, |v| v as i64)
494 },
495
496 D::U8WidenToI32 => unsafe {
499 emit_widen_i32_sentinel::<u8>(out, data, row_count, validity, I32_NULL, |v| v as i32)
500 },
501 D::U16WidenToI32 => unsafe {
502 emit_widen_i32_sentinel::<u16>(out, data, row_count, validity, I32_NULL, |v| v as i32)
503 },
504 D::U32WidenToI64 => unsafe {
505 emit_widen_i64_sentinel::<u32>(out, data, row_count, validity, I64_NULL, |v| v as i64)
506 },
507 D::U64WidenToI64 => unsafe { emit_u64_widen_i64_checked(out, data, row_count, validity)? },
508
509 D::F32Direct => unsafe {
511 emit_sentinel_le::<f32, 4>(
512 out,
513 data,
514 row_count,
515 validity,
516 F32_NULL.to_le_bytes(),
517 f32::to_le_bytes,
518 )
519 },
520
521 D::F16Widen => unsafe { emit_f16_to_f32(out, data, row_count, validity) },
523
524 D::Bool => unsafe { emit_bool(out, data, row_count, validity) },
526
527 D::DatetimeSecToMicros => unsafe {
529 emit_i64_to_micros(out, data, row_count, validity, "s", |v| {
530 v.checked_mul(1_000_000)
531 })?
532 },
533 D::DatetimeMinuteToMicros => unsafe {
534 emit_i64_to_micros(out, data, row_count, validity, "m", |v| {
535 v.checked_mul(60_000_000)
536 })?
537 },
538 D::DatetimeHourToMicros => unsafe {
539 emit_i64_to_micros(out, data, row_count, validity, "h", |v| {
540 v.checked_mul(3_600_000_000)
541 })?
542 },
543 D::DatetimeDayToMicros => unsafe {
544 emit_i64_to_micros(out, data, row_count, validity, "D", |v| {
545 v.checked_mul(86_400_000_000)
546 })?
547 },
548 D::DatetimeWeekToMicros => unsafe {
549 emit_i64_to_micros(out, data, row_count, validity, "W", |v| {
550 v.checked_mul(604_800_000_000)
551 })?
552 },
553 D::DatetimeMonthToMicros => unsafe {
559 let mut last: Option<(i64, i64)> = None;
560 emit_i64_to_micros(out, data, row_count, validity, "M", |v| {
561 if let Some((k, r)) = last
562 && k == v
563 {
564 return Some(r);
565 }
566 let r = month_offset_to_micros(v)?;
567 last = Some((v, r));
568 Some(r)
569 })?
570 },
571 D::DatetimeYearToMicros => unsafe {
572 let mut last: Option<(i64, i64)> = None;
573 emit_i64_to_micros(out, data, row_count, validity, "Y", |v| {
574 if let Some((k, r)) = last
575 && k == v
576 {
577 return Some(r);
578 }
579 let r = year_offset_to_micros(v)?;
580 last = Some((v, r));
581 Some(r)
582 })?
583 },
584
585 D::Decimal64 { scale } => unsafe {
587 emit_decimal::<8>(out, scale, data, row_count, validity)
588 },
589 D::Decimal128 { scale } => unsafe {
590 emit_decimal::<16>(out, scale, data, row_count, validity)
591 },
592 D::Decimal256 { scale } => unsafe {
593 emit_decimal::<32>(out, scale, data, row_count, validity)
594 },
595
596 D::GeohashI8 { bits } => unsafe {
598 emit_geohash::<1>(out, bits, data, row_count, validity)?
599 },
600 D::GeohashI16 { bits } => unsafe {
601 emit_geohash::<2>(out, bits, data, row_count, validity)?
602 },
603 D::GeohashI32 { bits } => unsafe {
604 emit_geohash::<4>(out, bits, data, row_count, validity)?
605 },
606 D::GeohashI64 { bits } => unsafe {
607 emit_geohash::<8>(out, bits, data, row_count, validity)?
608 },
609
610 D::F64Ndarray { ndim, shape } => unsafe {
612 emit_f64_ndarray(out, ndim, shape, data, row_count, validity)?
613 },
614 }
615 Ok(())
616}
617
618#[inline]
625unsafe fn emit_sentinel_le<T, const N: usize>(
626 out: &mut Vec<u8>,
627 data: *const u8,
628 row_count: usize,
629 validity: Option<&ValidityDescriptor>,
630 sentinel: [u8; N],
631 to_le: impl Fn(T) -> [u8; N],
632) where
633 T: Copy,
634{
635 out.push(0);
636 out.reserve(N * row_count);
637 let typed = data as *const T;
638 let data_start = out.len();
639 if cfg!(target_endian = "little") {
640 if row_count > 0 {
641 let bytes = unsafe { slice::from_raw_parts(data, row_count * N) };
642 out.extend_from_slice(bytes);
643 }
644 } else {
645 for i in 0..row_count {
646 out.extend_from_slice(&to_le(unsafe { typed.add(i).read_unaligned() }));
647 }
648 }
649 let Some(v) = validity.filter(|v| v.has_nulls()) else {
652 return;
653 };
654 let mut i = 0usize;
655 while i < row_count {
656 let byte_idx = i / 8;
657 let bit_off = i % 8;
658 if bit_off == 0 && i + 8 <= row_count && unsafe { *v.bits.add(byte_idx) } == 0xFF {
659 i += 8;
660 continue;
661 }
662 if !unsafe { v.is_valid(i) } {
663 let off = data_start + i * N;
664 out[off..off + N].copy_from_slice(&sentinel);
665 }
666 i += 1;
667 }
668}
669
670#[inline]
674unsafe fn emit_bitmap_le<T, const N: usize>(
675 out: &mut Vec<u8>,
676 data: *const u8,
677 row_count: usize,
678 validity: Option<&ValidityDescriptor>,
679 to_le: impl Fn(T) -> [u8; N],
680) where
681 T: Copy,
682{
683 let typed = data as *const T;
684 match validity.filter(|v| v.has_nulls()) {
685 None => {
686 out.push(0);
687 out.reserve(N * row_count);
688 if cfg!(target_endian = "little") {
689 if row_count > 0 {
690 let bytes = unsafe { slice::from_raw_parts(data, row_count * N) };
691 out.extend_from_slice(bytes);
692 }
693 } else {
694 for i in 0..row_count {
695 let value = unsafe { typed.add(i).read_unaligned() };
696 out.extend_from_slice(&to_le(value));
697 }
698 }
699 }
700 Some(v) => {
701 out.push(1);
702 unsafe { write_qwp_bitmap_from_validity(out, v) };
703 out.reserve(N * v.non_null_count);
704 for i in 0..row_count {
705 if unsafe { v.is_valid(i) } {
706 let value = unsafe { typed.add(i).read_unaligned() };
707 out.extend_from_slice(&to_le(value));
708 }
709 }
710 }
711 }
712}
713
714#[inline]
716unsafe fn emit_bitmap_fsb<const N: usize>(
717 out: &mut Vec<u8>,
718 data: *const u8,
719 row_count: usize,
720 validity: Option<&ValidityDescriptor>,
721) {
722 match validity.filter(|v| v.has_nulls()) {
723 None => {
724 out.push(0);
725 out.reserve(N * row_count);
726 if row_count > 0 {
727 let bytes = unsafe { slice::from_raw_parts(data, N * row_count) };
728 out.extend_from_slice(bytes);
729 }
730 }
731 Some(v) => {
732 out.push(1);
733 unsafe { write_qwp_bitmap_from_validity(out, v) };
734 out.reserve(N * v.non_null_count);
735 for i in 0..row_count {
736 if unsafe { v.is_valid(i) } {
737 let row_start = unsafe { data.add(i * N) };
738 let row = unsafe { slice::from_raw_parts(row_start, N) };
739 out.extend_from_slice(row);
740 }
741 }
742 }
743 }
744}
745
746#[inline]
749unsafe fn emit_widen_i32_sentinel<T>(
750 out: &mut Vec<u8>,
751 data: *const u8,
752 row_count: usize,
753 validity: Option<&ValidityDescriptor>,
754 sentinel: i32,
755 widen: impl Fn(T) -> i32,
756) where
757 T: Copy,
758{
759 out.push(0);
760 out.reserve(4 * row_count);
761 let typed = data as *const T;
762 let sentinel_bytes = sentinel.to_le_bytes();
763 match validity {
764 None => {
765 for i in 0..row_count {
766 let v = unsafe { typed.add(i).read_unaligned() };
767 out.extend_from_slice(&widen(v).to_le_bytes());
768 }
769 }
770 Some(v) => {
771 for i in 0..row_count {
772 if unsafe { v.is_valid(i) } {
773 let raw = unsafe { typed.add(i).read_unaligned() };
774 out.extend_from_slice(&widen(raw).to_le_bytes());
775 } else {
776 out.extend_from_slice(&sentinel_bytes);
777 }
778 }
779 }
780 }
781}
782
783#[inline]
786unsafe fn emit_widen_i64_sentinel<T>(
787 out: &mut Vec<u8>,
788 data: *const u8,
789 row_count: usize,
790 validity: Option<&ValidityDescriptor>,
791 sentinel: i64,
792 widen: impl Fn(T) -> i64,
793) where
794 T: Copy,
795{
796 out.push(0);
797 out.reserve(8 * row_count);
798 let typed = data as *const T;
799 let sentinel_bytes = sentinel.to_le_bytes();
800 match validity {
801 None => {
802 for i in 0..row_count {
803 let v = unsafe { typed.add(i).read_unaligned() };
804 out.extend_from_slice(&widen(v).to_le_bytes());
805 }
806 }
807 Some(v) => {
808 for i in 0..row_count {
809 if unsafe { v.is_valid(i) } {
810 let raw = unsafe { typed.add(i).read_unaligned() };
811 out.extend_from_slice(&widen(raw).to_le_bytes());
812 } else {
813 out.extend_from_slice(&sentinel_bytes);
814 }
815 }
816 }
817 }
818}
819
820#[inline]
821fn u64_to_i64_checked(v: u64, row: usize) -> Result<i64> {
822 if v > i64::MAX as u64 {
823 return Err(error::fmt!(
824 InvalidApiCall,
825 "u64 value {} at row {} does not fit QuestDB LONG (max i64::MAX)",
826 v,
827 row
828 ));
829 }
830 Ok(v as i64)
831}
832
833unsafe fn emit_u64_widen_i64_checked(
834 out: &mut Vec<u8>,
835 data: *const u8,
836 row_count: usize,
837 validity: Option<&ValidityDescriptor>,
838) -> Result<()> {
839 let typed = data as *const u64;
840 if validity.is_none() && row_count > 0 {
841 let mut acc: u64 = 0;
842 for i in 0..row_count {
843 acc |= unsafe { typed.add(i).read_unaligned() };
844 }
845 if acc < (1u64 << 63) {
846 unsafe {
847 emit_widen_i64_sentinel::<u64>(out, data, row_count, validity, I64_NULL, |v| {
848 v as i64
849 })
850 };
851 return Ok(());
852 }
853 }
854 out.push(0);
855 out.reserve(8 * row_count);
856 let sentinel_bytes = I64_NULL.to_le_bytes();
857 match validity {
858 None => {
859 for i in 0..row_count {
860 let v = unsafe { typed.add(i).read_unaligned() };
861 out.extend_from_slice(&u64_to_i64_checked(v, i)?.to_le_bytes());
862 }
863 }
864 Some(v) => {
865 for i in 0..row_count {
866 if unsafe { v.is_valid(i) } {
867 let raw = unsafe { typed.add(i).read_unaligned() };
868 out.extend_from_slice(&u64_to_i64_checked(raw, i)?.to_le_bytes());
869 } else {
870 out.extend_from_slice(&sentinel_bytes);
871 }
872 }
873 }
874 }
875 Ok(())
876}
877
878unsafe fn emit_f16_to_f32(
884 out: &mut Vec<u8>,
885 data: *const u8,
886 row_count: usize,
887 validity: Option<&ValidityDescriptor>,
888) {
889 out.push(0);
890 out.reserve(4 * row_count);
891 let typed = data as *const u16;
892 let sentinel = F32_NULL.to_le_bytes();
893 match validity {
894 None => {
895 for i in 0..row_count {
896 let bits = unsafe { typed.add(i).read_unaligned() };
897 out.extend_from_slice(&f16_bits_to_f32(bits).to_le_bytes());
898 }
899 }
900 Some(v) => {
901 for i in 0..row_count {
902 if unsafe { v.is_valid(i) } {
903 let bits = unsafe { typed.add(i).read_unaligned() };
904 out.extend_from_slice(&f16_bits_to_f32(bits).to_le_bytes());
905 } else {
906 out.extend_from_slice(&sentinel);
907 }
908 }
909 }
910 }
911}
912
913#[inline]
917fn f16_bits_to_f32(bits: u16) -> f32 {
918 let sign = ((bits >> 15) as u32) << 31;
919 let exp = ((bits >> 10) & 0x1F) as u32;
920 let mant = (bits & 0x3FF) as u32;
921 let f32_bits = match exp {
922 0 => {
923 if mant == 0 {
924 sign
926 } else {
927 let mut m = mant;
930 let mut e: i32 = -14;
931 while (m & 0x400) == 0 {
932 m <<= 1;
933 e -= 1;
934 }
935 m &= 0x3FF;
936 let exp_f32 = ((e + 127) as u32) << 23;
937 sign | exp_f32 | (m << 13)
938 }
939 }
940 31 => {
941 sign | (0xFFu32 << 23) | (mant << 13)
943 }
944 _ => {
945 let exp_f32 = (exp + (127 - 15)) << 23;
946 sign | exp_f32 | (mant << 13)
947 }
948 };
949 f32::from_bits(f32_bits)
950}
951
952unsafe fn emit_bool(
955 out: &mut Vec<u8>,
956 data: *const u8,
957 row_count: usize,
958 validity: Option<&ValidityDescriptor>,
959) {
960 out.push(0);
961 let bitmap_bytes = row_count.div_ceil(8);
962 out.reserve(bitmap_bytes);
963 if validity.is_none() {
964 let full_chunks = row_count / 8;
965 let tail = row_count % 8;
966 for chunk_idx in 0..full_chunks {
967 let base = chunk_idx * 8;
968 let src = unsafe { data.add(base) };
969 let b0 = unsafe { *src };
970 let b1 = unsafe { *src.add(1) };
971 let b2 = unsafe { *src.add(2) };
972 let b3 = unsafe { *src.add(3) };
973 let b4 = unsafe { *src.add(4) };
974 let b5 = unsafe { *src.add(5) };
975 let b6 = unsafe { *src.add(6) };
976 let b7 = unsafe { *src.add(7) };
977 let packed = u8::from(b0 != 0)
978 | (u8::from(b1 != 0) << 1)
979 | (u8::from(b2 != 0) << 2)
980 | (u8::from(b3 != 0) << 3)
981 | (u8::from(b4 != 0) << 4)
982 | (u8::from(b5 != 0) << 5)
983 | (u8::from(b6 != 0) << 6)
984 | (u8::from(b7 != 0) << 7);
985 out.push(packed);
986 }
987 if tail != 0 {
988 let base = full_chunks * 8;
989 let mut packed = 0u8;
990 for i in 0..tail {
991 let b = unsafe { *data.add(base + i) };
992 if b != 0 {
993 packed |= 1u8 << i;
994 }
995 }
996 out.push(packed);
997 }
998 return;
999 }
1000 let v = validity.unwrap();
1001 let mut packed = 0u8;
1002 let mut bit_idx = 0u8;
1003 for i in 0..row_count {
1004 let raw = unsafe { *data.add(i) };
1005 if unsafe { v.is_valid(i) } && raw != 0 {
1006 packed |= 1u8 << bit_idx;
1007 }
1008 bit_idx += 1;
1009 if bit_idx == 8 {
1010 out.push(packed);
1011 packed = 0;
1012 bit_idx = 0;
1013 }
1014 }
1015 if bit_idx != 0 {
1016 out.push(packed);
1017 }
1018}
1019
1020#[inline]
1025unsafe fn emit_i64_to_micros<F>(
1026 out: &mut Vec<u8>,
1027 data: *const u8,
1028 row_count: usize,
1029 validity: Option<&ValidityDescriptor>,
1030 unit_label: &str,
1031 mut convert: F,
1032) -> Result<()>
1033where
1034 F: FnMut(i64) -> Option<i64>,
1035{
1036 let typed = data as *const i64;
1037 let make_err = |i: usize, value: i64| {
1038 error::fmt!(
1039 InvalidApiCall,
1040 "datetime64[{}] value at row {} ({}) overflows i64 when converted to microseconds",
1041 unit_label,
1042 i,
1043 value
1044 )
1045 };
1046 match validity.filter(|v| v.has_nulls()) {
1051 None => {
1052 out.push(0);
1053 out.reserve(8 * row_count);
1054 for i in 0..row_count {
1055 let value = unsafe { typed.add(i).read_unaligned() };
1056 let micros = if value == I64_NULL {
1057 I64_NULL
1058 } else {
1059 convert(value).ok_or_else(|| make_err(i, value))?
1060 };
1061 out.extend_from_slice(µs.to_le_bytes());
1062 }
1063 }
1064 Some(v) => {
1065 out.push(1);
1066 unsafe { write_qwp_bitmap_from_validity(out, v) };
1067 out.reserve(8 * v.non_null_count);
1068 for i in 0..row_count {
1069 if !unsafe { v.is_valid(i) } {
1070 continue;
1071 }
1072 let value = unsafe { typed.add(i).read_unaligned() };
1073 let micros = if value == I64_NULL {
1074 I64_NULL
1075 } else {
1076 convert(value).ok_or_else(|| make_err(i, value))?
1077 };
1078 out.extend_from_slice(µs.to_le_bytes());
1079 }
1080 }
1081 }
1082 Ok(())
1083}
1084
1085fn year_offset_to_micros(year_offset: i64) -> Option<i64> {
1088 if !(-292_277..=292_277).contains(&year_offset) {
1091 return None;
1092 }
1093 let year = 1970 + year_offset;
1094 let days = days_from_civil(year, 1, 1);
1095 days.checked_mul(86_400_000_000)
1096}
1097
1098fn month_offset_to_micros(month_offset: i64) -> Option<i64> {
1101 let year_offset = month_offset.div_euclid(12);
1102 let month_in_year = month_offset.rem_euclid(12) as u32 + 1; if !(-292_277..=292_277).contains(&year_offset) {
1104 return None;
1105 }
1106 let year = 1970 + year_offset;
1107 let days = days_from_civil(year, month_in_year, 1);
1108 days.checked_mul(86_400_000_000)
1109}
1110
1111fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
1117 let y = if m <= 2 { y - 1 } else { y };
1118 let era = if y >= 0 { y } else { y - 399 } / 400;
1119 let yoe = (y - era * 400) as u64; let m_adj = if m > 2 { m - 3 } else { m + 9 } as u64;
1121 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe as i64 - 719_468
1124}
1125
1126#[inline]
1131unsafe fn emit_decimal<const N: usize>(
1132 out: &mut Vec<u8>,
1133 scale: u8,
1134 data: *const u8,
1135 row_count: usize,
1136 validity: Option<&ValidityDescriptor>,
1137) {
1138 match validity.filter(|v| v.has_nulls()) {
1139 None => {
1140 out.push(0);
1141 out.reserve(1 + N * row_count);
1142 out.push(scale);
1143 if row_count > 0 {
1144 let bytes = unsafe { slice::from_raw_parts(data, N * row_count) };
1145 out.extend_from_slice(bytes);
1146 }
1147 }
1148 Some(v) => {
1149 out.push(1);
1150 unsafe { write_qwp_bitmap_from_validity(out, v) };
1151 out.reserve(1 + N * v.non_null_count);
1152 out.push(scale);
1153 for i in 0..row_count {
1154 if unsafe { v.is_valid(i) } {
1155 let row_start = unsafe { data.add(i * N) };
1156 let row = unsafe { slice::from_raw_parts(row_start, N) };
1157 out.extend_from_slice(row);
1158 }
1159 }
1160 }
1161 }
1162}
1163
1164#[inline]
1174unsafe fn emit_geohash<const SRC: usize>(
1175 out: &mut Vec<u8>,
1176 bits: u8,
1177 data: *const u8,
1178 row_count: usize,
1179 validity: Option<&ValidityDescriptor>,
1180) -> Result<()> {
1181 let elem = (bits as usize).div_ceil(8);
1182 if elem > SRC {
1183 return Err(error::fmt!(
1184 InvalidApiCall,
1185 "numpy geohash bits ({bits}) exceeds source dtype width ({SRC} bytes)"
1186 ));
1187 }
1188 match validity.filter(|v| v.has_nulls()) {
1189 None => {
1190 out.push(0);
1191 out.reserve(1 + elem * row_count);
1192 write_qwp_varint(out, bits as u64);
1193 if elem == SRC && row_count > 0 {
1194 let bytes = unsafe { slice::from_raw_parts(data, SRC * row_count) };
1195 out.extend_from_slice(bytes);
1196 } else {
1197 for i in 0..row_count {
1198 let row_start = unsafe { data.add(i * SRC) };
1199 let row = unsafe { slice::from_raw_parts(row_start, elem) };
1200 out.extend_from_slice(row);
1201 }
1202 }
1203 }
1204 Some(v) => {
1205 out.push(1);
1206 unsafe { write_qwp_bitmap_from_validity(out, v) };
1207 out.reserve(1 + elem * v.non_null_count);
1208 write_qwp_varint(out, bits as u64);
1209 for i in 0..row_count {
1210 if unsafe { v.is_valid(i) } {
1211 let row_start = unsafe { data.add(i * SRC) };
1212 let row = unsafe { slice::from_raw_parts(row_start, elem) };
1213 out.extend_from_slice(row);
1214 }
1215 }
1216 }
1217 }
1218 Ok(())
1219}
1220
1221#[inline]
1227unsafe fn emit_f64_ndarray(
1228 out: &mut Vec<u8>,
1229 ndim: u8,
1230 shape: [u32; MAX_ARRAY_DIMS],
1231 data: *const u8,
1232 row_count: usize,
1233 validity: Option<&ValidityDescriptor>,
1234) -> Result<()> {
1235 let nd = ndim as usize;
1236 if nd == 0 || nd > MAX_ARRAY_DIMS {
1237 return Err(error::fmt!(
1238 InvalidApiCall,
1239 "F64Ndarray ndim {} must be in 1..={}",
1240 nd,
1241 MAX_ARRAY_DIMS
1242 ));
1243 }
1244 let leaf_count: usize = shape[..nd]
1245 .iter()
1246 .copied()
1247 .map(|d| d as usize)
1248 .try_fold(1usize, usize::checked_mul)
1249 .ok_or_else(|| error::fmt!(InvalidApiCall, "F64Ndarray shape overflows usize"))?;
1250 if leaf_count > MAX_NDARRAY_LEAF_ELEMS {
1251 return Err(error::fmt!(
1252 InvalidApiCall,
1253 "F64Ndarray shape product {} exceeds MAX_NDARRAY_LEAF_ELEMS ({})",
1254 leaf_count,
1255 MAX_NDARRAY_LEAF_ELEMS
1256 ));
1257 }
1258 let row_payload = 1usize
1259 .checked_add(4usize.saturating_mul(nd))
1260 .and_then(|v| v.checked_add(8usize.saturating_mul(leaf_count)))
1261 .ok_or_else(|| error::fmt!(InvalidApiCall, "F64Ndarray row payload overflows usize"))?;
1262 let row_bytes = leaf_count
1263 .checked_mul(8)
1264 .ok_or_else(|| error::fmt!(InvalidApiCall, "F64Ndarray row size overflows usize"))?;
1265
1266 let validity = validity.filter(|v| v.has_nulls());
1267 let non_null_rows = match validity {
1268 None => {
1269 out.push(0);
1270 row_count
1271 }
1272 Some(v) => {
1273 out.push(1);
1274 unsafe { write_qwp_bitmap_from_validity(out, v) };
1275 v.non_null_count
1276 }
1277 };
1278 let reserve_bytes = non_null_rows.checked_mul(row_payload).ok_or_else(|| {
1279 error::fmt!(
1280 InvalidApiCall,
1281 "F64Ndarray reservation overflows usize ({} rows * {} bytes/row)",
1282 non_null_rows,
1283 row_payload
1284 )
1285 })?;
1286 out.try_reserve(reserve_bytes).map_err(|_| {
1287 error::fmt!(
1288 InvalidApiCall,
1289 "F64Ndarray reservation of {} bytes failed",
1290 reserve_bytes
1291 )
1292 })?;
1293
1294 let header_len = 1 + 4 * nd;
1295 let mut header: [u8; 1 + 4 * MAX_ARRAY_DIMS] = [0u8; 1 + 4 * MAX_ARRAY_DIMS];
1296 header[0] = ndim;
1297 for (i, &d) in shape[..nd].iter().enumerate() {
1298 let off = 1 + 4 * i;
1299 header[off..off + 4].copy_from_slice(&d.to_le_bytes());
1300 }
1301 let header = &header[..header_len];
1302
1303 for row in 0..row_count {
1304 if let Some(v) = validity
1305 && !unsafe { v.is_valid(row) }
1306 {
1307 continue;
1308 }
1309 out.extend_from_slice(header);
1310 let src = unsafe { data.add(row * row_bytes) };
1311 if cfg!(target_endian = "little") {
1312 if row_bytes > 0 {
1313 out.extend_from_slice(unsafe { slice::from_raw_parts(src, row_bytes) });
1314 }
1315 } else {
1316 for i in 0..leaf_count {
1317 let bits = unsafe { (src.add(i * 8) as *const u64).read_unaligned() };
1318 out.extend_from_slice(&bits.to_le_bytes());
1319 }
1320 }
1321 }
1322 Ok(())
1323}
1324
1325unsafe fn write_qwp_bitmap_from_validity(out: &mut Vec<u8>, v: &ValidityDescriptor) {
1327 let src = unsafe { slice::from_raw_parts(v.bits, v.byte_len()) };
1328 super::wire::write_qwp_bitmap_invert(out, src, v.bit_len);
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333 use super::super::Validity;
1334 use super::super::chunk::Chunk;
1335 use super::super::encoder::{EncodeScratch, encode_chunk_into};
1336 use super::*;
1337 use crate::ingress::TimestampUnit;
1338 use crate::ingress::buffer::SymbolGlobalDict;
1339
1340 fn encode(chunk: &Chunk<'_>) -> Vec<u8> {
1341 let mut out = Vec::new();
1342 let mut dict = SymbolGlobalDict::new();
1343 let mut scratch = EncodeScratch::new();
1344 encode_chunk_into(&mut out, chunk, &mut dict, &mut scratch, false).unwrap();
1345 out
1346 }
1347
1348 fn encode_err(chunk: &Chunk<'_>) -> crate::Error {
1349 let mut out = Vec::new();
1350 let mut dict = SymbolGlobalDict::new();
1351 let mut scratch = EncodeScratch::new();
1352 encode_chunk_into(&mut out, chunk, &mut dict, &mut scratch, false).unwrap_err()
1353 }
1354
1355 #[test]
1356 fn chunk_row_count_above_max_rejected_before_read() {
1357 let buf = [0u8; 8];
1361 let mut chunk = Chunk::new("t");
1362 unsafe {
1363 chunk
1364 .push_numpy_deferred(
1365 "v",
1366 NumpyDtype::I8Direct,
1367 buf.as_ptr(),
1368 super::super::MAX_CHUNK_ROWS + 1,
1369 None,
1370 )
1371 .unwrap();
1372 }
1373 let err = encode_err(&chunk);
1374 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1375 assert!(err.msg().contains("MAX_CHUNK_ROWS"), "{}", err.msg());
1376 }
1377
1378 #[test]
1379 fn source_elem_size_matches_read_stride() {
1380 use NumpyDtype as D;
1381 assert_eq!(D::I8Direct.source_elem_size().unwrap(), 1);
1384 assert_eq!(D::Bool.source_elem_size().unwrap(), 1);
1385 assert_eq!(D::I8WidenToI32.source_elem_size().unwrap(), 1); assert_eq!(D::U8WidenToI32.source_elem_size().unwrap(), 1);
1387 assert_eq!(D::F16Widen.source_elem_size().unwrap(), 2); assert_eq!(D::CharDirect.source_elem_size().unwrap(), 2);
1389 assert_eq!(D::I32WidenToI64.source_elem_size().unwrap(), 4); assert_eq!(D::Ipv4Direct.source_elem_size().unwrap(), 4);
1391 assert_eq!(D::F32Direct.source_elem_size().unwrap(), 4);
1392 assert_eq!(D::I64Direct.source_elem_size().unwrap(), 8);
1393 assert_eq!(D::U64WidenToI64.source_elem_size().unwrap(), 8);
1394 assert_eq!(D::DatetimeSecToMicros.source_elem_size().unwrap(), 8);
1395 assert_eq!(D::UuidDirect.source_elem_size().unwrap(), 16);
1396 assert_eq!(D::Long256Direct.source_elem_size().unwrap(), 32);
1397 assert_eq!(D::Decimal64 { scale: 0 }.source_elem_size().unwrap(), 8);
1398 assert_eq!(D::Decimal128 { scale: 0 }.source_elem_size().unwrap(), 16);
1399 assert_eq!(D::Decimal256 { scale: 0 }.source_elem_size().unwrap(), 32);
1400 assert_eq!(D::GeohashI8 { bits: 1 }.source_elem_size().unwrap(), 1);
1402 assert_eq!(D::GeohashI64 { bits: 1 }.source_elem_size().unwrap(), 8);
1403 let mut shape = [0u32; MAX_ARRAY_DIMS];
1405 shape[0] = 2;
1406 shape[1] = 3;
1407 assert_eq!(
1408 D::F64Ndarray { ndim: 2, shape }.source_elem_size().unwrap(),
1409 2 * 3 * 8
1410 );
1411 }
1412
1413 #[test]
1414 fn geohash_dtype_rejects_invalid_bits() {
1415 assert!(NumpyDtype::GeohashI8 { bits: 0 }.validate().is_err());
1416 assert!(NumpyDtype::GeohashI8 { bits: 9 }.validate().is_err());
1417 assert!(NumpyDtype::GeohashI64 { bits: 61 }.validate().is_err());
1418 assert!(NumpyDtype::GeohashI8 { bits: 8 }.validate().is_ok());
1419 assert!(NumpyDtype::GeohashI64 { bits: 60 }.validate().is_ok());
1420 }
1421
1422 #[test]
1423 fn decimal_dtype_rejects_scale_above_width_max() {
1424 assert!(NumpyDtype::Decimal64 { scale: 18 }.validate().is_ok());
1425 assert!(NumpyDtype::Decimal128 { scale: 38 }.validate().is_ok());
1426 assert!(NumpyDtype::Decimal256 { scale: 76 }.validate().is_ok());
1427
1428 for dtype in [
1429 NumpyDtype::Decimal64 { scale: 19 },
1430 NumpyDtype::Decimal128 { scale: 39 },
1431 NumpyDtype::Decimal256 { scale: 77 },
1432 ] {
1433 let err = dtype.validate().unwrap_err();
1434 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1435 assert!(err.msg().contains("decimal scale"), "{}", err.msg());
1436 }
1437 }
1438
1439 #[test]
1440 fn f16_bits_to_f32_matches_half_crate_for_all_bit_patterns() {
1441 for bits in 0u32..=0xFFFFu32 {
1448 let bits = bits as u16;
1449 let local = f16_bits_to_f32(bits);
1450 let reference = half::f16::from_bits(bits).to_f32();
1451 if reference.is_nan() {
1452 assert!(local.is_nan(), "bits={:#06x}", bits);
1453 } else {
1454 assert_eq!(local.to_bits(), reference.to_bits(), "bits={:#06x}", bits);
1455 }
1456 }
1457 }
1458
1459 #[test]
1460 fn i8_direct_matches_column_i8() {
1461 let src = [1i8, -2, 3];
1462 let ts = [10i64, 20, 30];
1463
1464 let mut a = Chunk::new("t");
1465 unsafe {
1466 a.push_numpy_deferred(
1467 "v",
1468 NumpyDtype::I8Direct,
1469 src.as_ptr() as *const u8,
1470 src.len(),
1471 None,
1472 )
1473 .unwrap();
1474 }
1475 a.at_nanos(&ts).unwrap();
1476 let bytes_a = encode(&a);
1477
1478 let mut b = Chunk::new("t");
1479 b.column_i8("v", &src, None).unwrap();
1480 b.at_nanos(&ts).unwrap();
1481 let bytes_b = encode(&b);
1482
1483 assert_eq!(
1484 bytes_a, bytes_b,
1485 "I8Direct must produce byte-identical wire to column_i8"
1486 );
1487 }
1488
1489 #[test]
1490 fn i16_direct_matches_column_i16() {
1491 let src = [1i16, -2, 3];
1492 let ts = [10i64, 20, 30];
1493
1494 let mut a = Chunk::new("t");
1495 unsafe {
1496 a.push_numpy_deferred(
1497 "v",
1498 NumpyDtype::I16Direct,
1499 src.as_ptr() as *const u8,
1500 src.len(),
1501 None,
1502 )
1503 .unwrap();
1504 }
1505 a.at_nanos(&ts).unwrap();
1506 let bytes_a = encode(&a);
1507
1508 let mut b = Chunk::new("t");
1509 b.column_i16("v", &src, None).unwrap();
1510 b.at_nanos(&ts).unwrap();
1511 let bytes_b = encode(&b);
1512
1513 assert_eq!(
1514 bytes_a, bytes_b,
1515 "I16Direct must produce byte-identical wire to column_i16"
1516 );
1517 }
1518
1519 #[test]
1520 fn i32_direct_matches_column_i32() {
1521 let src = [1i32, -2, 3];
1522 let ts = [10i64, 20, 30];
1523
1524 let mut a = Chunk::new("t");
1525 unsafe {
1526 a.push_numpy_deferred(
1527 "v",
1528 NumpyDtype::I32Direct,
1529 src.as_ptr() as *const u8,
1530 src.len(),
1531 None,
1532 )
1533 .unwrap();
1534 }
1535 a.at_nanos(&ts).unwrap();
1536 let bytes_a = encode(&a);
1537
1538 let mut b = Chunk::new("t");
1539 b.column_i32("v", &src, None).unwrap();
1540 b.at_nanos(&ts).unwrap();
1541 let bytes_b = encode(&b);
1542
1543 assert_eq!(
1544 bytes_a, bytes_b,
1545 "I32Direct must produce byte-identical wire to column_i32"
1546 );
1547 }
1548
1549 #[test]
1550 fn u8_widen_matches_column_i32() {
1551 let src = [0u8, 1, 200, 255];
1554 let widened: [i32; 4] = [0, 1, 200, 255];
1555 let ts = [10i64, 20, 30, 40];
1556
1557 let mut a = Chunk::new("t");
1558 unsafe {
1559 a.push_numpy_deferred("v", NumpyDtype::U8WidenToI32, src.as_ptr(), src.len(), None)
1560 .unwrap();
1561 }
1562 a.at_nanos(&ts).unwrap();
1563 let bytes_a = encode(&a);
1564
1565 let mut b = Chunk::new("t");
1566 b.column_i32("v", &widened, None).unwrap();
1567 b.at_nanos(&ts).unwrap();
1568 let bytes_b = encode(&b);
1569
1570 assert_eq!(
1571 bytes_a, bytes_b,
1572 "U8WidenToI32 must produce byte-identical wire to column_i32 over the widened data"
1573 );
1574 }
1575
1576 #[test]
1577 fn u16_widen_matches_column_i32() {
1578 let src = [0u16, 1, 30000, 65535];
1579 let widened: [i32; 4] = [0, 1, 30000, 65535];
1580 let ts = [10i64, 20, 30, 40];
1581
1582 let mut a = Chunk::new("t");
1583 unsafe {
1584 a.push_numpy_deferred(
1585 "v",
1586 NumpyDtype::U16WidenToI32,
1587 src.as_ptr() as *const u8,
1588 src.len(),
1589 None,
1590 )
1591 .unwrap();
1592 }
1593 a.at_nanos(&ts).unwrap();
1594 let bytes_a = encode(&a);
1595
1596 let mut b = Chunk::new("t");
1597 b.column_i32("v", &widened, None).unwrap();
1598 b.at_nanos(&ts).unwrap();
1599 let bytes_b = encode(&b);
1600
1601 assert_eq!(
1602 bytes_a, bytes_b,
1603 "U16WidenToI32 must produce byte-identical wire to column_i32 over the widened data"
1604 );
1605 }
1606
1607 #[test]
1608 fn i8_widen_matches_column_i32() {
1609 let src = [-128i8, -1, 0, 1, 127];
1612 let widened: [i32; 5] = [-128, -1, 0, 1, 127];
1613 let ts = [10i64, 20, 30, 40, 50];
1614
1615 let mut a = Chunk::new("t");
1616 unsafe {
1617 a.push_numpy_deferred(
1618 "v",
1619 NumpyDtype::I8WidenToI32,
1620 src.as_ptr() as *const u8,
1621 src.len(),
1622 None,
1623 )
1624 .unwrap();
1625 }
1626 a.at_nanos(&ts).unwrap();
1627 let bytes_a = encode(&a);
1628
1629 let mut b = Chunk::new("t");
1630 b.column_i32("v", &widened, None).unwrap();
1631 b.at_nanos(&ts).unwrap();
1632 let bytes_b = encode(&b);
1633
1634 assert_eq!(
1635 bytes_a, bytes_b,
1636 "I8WidenToI32 must produce byte-identical wire to column_i32 over the widened data"
1637 );
1638 }
1639
1640 #[test]
1641 fn i16_widen_matches_column_i32() {
1642 let src = [i16::MIN, -1, 0, 1, i16::MAX];
1643 let widened: [i32; 5] = [i16::MIN as i32, -1, 0, 1, i16::MAX as i32];
1644 let ts = [10i64, 20, 30, 40, 50];
1645
1646 let mut a = Chunk::new("t");
1647 unsafe {
1648 a.push_numpy_deferred(
1649 "v",
1650 NumpyDtype::I16WidenToI32,
1651 src.as_ptr() as *const u8,
1652 src.len(),
1653 None,
1654 )
1655 .unwrap();
1656 }
1657 a.at_nanos(&ts).unwrap();
1658 let bytes_a = encode(&a);
1659
1660 let mut b = Chunk::new("t");
1661 b.column_i32("v", &widened, None).unwrap();
1662 b.at_nanos(&ts).unwrap();
1663 let bytes_b = encode(&b);
1664
1665 assert_eq!(
1666 bytes_a, bytes_b,
1667 "I16WidenToI32 must produce byte-identical wire to column_i32 over the widened data"
1668 );
1669 }
1670
1671 #[test]
1672 fn i32_widen_matches_column_i64() {
1673 let src = [i32::MIN, -1, 0, 1, i32::MAX];
1676 let widened: [i64; 5] = [i32::MIN as i64, -1, 0, 1, i32::MAX as i64];
1677 let ts = [10i64, 20, 30, 40, 50];
1678
1679 let mut a = Chunk::new("t");
1680 unsafe {
1681 a.push_numpy_deferred(
1682 "v",
1683 NumpyDtype::I32WidenToI64,
1684 src.as_ptr() as *const u8,
1685 src.len(),
1686 None,
1687 )
1688 .unwrap();
1689 }
1690 a.at_nanos(&ts).unwrap();
1691 let bytes_a = encode(&a);
1692
1693 let mut b = Chunk::new("t");
1694 b.column_i64("v", &widened, None).unwrap();
1695 b.at_nanos(&ts).unwrap();
1696 let bytes_b = encode(&b);
1697
1698 assert_eq!(
1699 bytes_a, bytes_b,
1700 "I32WidenToI64 must produce byte-identical wire to column_i64 over the widened data"
1701 );
1702 }
1703
1704 #[test]
1705 fn u64_widen_within_i64_range_matches_column_i64() {
1706 let src = [0u64, 42, i64::MAX as u64];
1707 let widened: [i64; 3] = [0, 42, i64::MAX];
1708 let ts = [10i64, 20, 30];
1709
1710 let mut a = Chunk::new("t");
1711 unsafe {
1712 a.push_numpy_deferred(
1713 "v",
1714 NumpyDtype::U64WidenToI64,
1715 src.as_ptr() as *const u8,
1716 src.len(),
1717 None,
1718 )
1719 .unwrap();
1720 }
1721 a.at_nanos(&ts).unwrap();
1722 let bytes_a = encode(&a);
1723
1724 let mut b = Chunk::new("t");
1725 b.column_i64("v", &widened, None).unwrap();
1726 b.at_nanos(&ts).unwrap();
1727 let bytes_b = encode(&b);
1728
1729 assert_eq!(
1730 bytes_a, bytes_b,
1731 "U64WidenToI64 must produce signed LONG wire for values within i64::MAX"
1732 );
1733 }
1734
1735 #[test]
1736 fn u64_widen_above_i64_max_rejects() {
1737 let src = [i64::MAX as u64 + 1];
1738 let ts = [10i64];
1739
1740 let mut chunk = Chunk::new("t");
1741 unsafe {
1742 chunk
1743 .push_numpy_deferred(
1744 "v",
1745 NumpyDtype::U64WidenToI64,
1746 src.as_ptr() as *const u8,
1747 src.len(),
1748 None,
1749 )
1750 .unwrap();
1751 }
1752 chunk.at_nanos(&ts).unwrap();
1753 let err = encode_err(&chunk);
1754 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1755 assert!(
1756 err.msg().contains("does not fit QuestDB LONG"),
1757 "{}",
1758 err.msg()
1759 );
1760 }
1761
1762 #[test]
1763 fn nullable_u64_widen_above_i64_max_rejects() {
1764 let src = [0u64, i64::MAX as u64 + 1];
1765 let ts = [10i64, 20];
1766 let validity_bits = [0b0000_0010u8];
1767 let validity = Validity::from_bitmap(&validity_bits, src.len()).unwrap();
1768
1769 let mut chunk = Chunk::new("t");
1770 unsafe {
1771 chunk
1772 .push_numpy_deferred(
1773 "v",
1774 NumpyDtype::U64WidenToI64,
1775 src.as_ptr() as *const u8,
1776 src.len(),
1777 Some(&validity),
1778 )
1779 .unwrap();
1780 }
1781 chunk.at_nanos(&ts).unwrap();
1782 let err = encode_err(&chunk);
1783 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1784 assert!(
1785 err.msg().contains("does not fit QuestDB LONG"),
1786 "{}",
1787 err.msg()
1788 );
1789 }
1790
1791 #[test]
1792 fn f32_direct_matches_column_f32() {
1793 let src = [1.5f32, -2.25, 3.125, f32::NAN];
1794 let ts = [10i64, 20, 30, 40];
1795
1796 let mut a = Chunk::new("t");
1797 unsafe {
1798 a.push_numpy_deferred(
1799 "v",
1800 NumpyDtype::F32Direct,
1801 src.as_ptr() as *const u8,
1802 src.len(),
1803 None,
1804 )
1805 .unwrap();
1806 }
1807 a.at_nanos(&ts).unwrap();
1808 let bytes_a = encode(&a);
1809
1810 let mut b = Chunk::new("t");
1811 b.column_f32("v", &src, None).unwrap();
1812 b.at_nanos(&ts).unwrap();
1813 let bytes_b = encode(&b);
1814
1815 assert_eq!(
1816 bytes_a, bytes_b,
1817 "F32Direct must produce byte-identical wire to column_f32"
1818 );
1819 }
1820
1821 #[test]
1822 fn bool_with_null_matches_column_bool() {
1823 let raw = [1u8, 0, 1, 1];
1824 let ts = [1i64, 2, 3, 4];
1825 let v_bits = [0b0000_1011u8];
1827 let v = Validity::from_bitmap(&v_bits, 4).unwrap();
1828
1829 let mut a = Chunk::new("t");
1830 unsafe {
1831 a.push_numpy_deferred("b", NumpyDtype::Bool, raw.as_ptr(), raw.len(), Some(&v))
1832 .unwrap();
1833 }
1834 a.at_nanos(&ts).unwrap();
1835 let bytes_a = encode(&a);
1836
1837 let mut packed = vec![0u8; raw.len().div_ceil(8)];
1838 for (i, &b) in raw.iter().enumerate() {
1839 if b != 0 {
1840 packed[i / 8] |= 1u8 << (i % 8);
1841 }
1842 }
1843 let mut b = Chunk::new("t");
1844 b.column_bool("b", &packed, raw.len(), Some(&v)).unwrap();
1845 b.at_nanos(&ts).unwrap();
1846 let bytes_b = encode(&b);
1847
1848 assert_eq!(
1849 bytes_a, bytes_b,
1850 "Bool numpy emit must match column_bool over the equivalent packed bitmap"
1851 );
1852 }
1853
1854 #[test]
1855 fn timestamp_nanos_direct_matches_column_ts_nanos() {
1856 let src = [1_000i64, 2_000, 3_000];
1857 let ts = [1i64, 2, 3];
1858
1859 let mut a = Chunk::new("t");
1860 unsafe {
1861 a.push_numpy_deferred(
1862 "ts",
1863 NumpyDtype::TimestampNanosDirect,
1864 src.as_ptr() as *const u8,
1865 src.len(),
1866 None,
1867 )
1868 .unwrap();
1869 }
1870 a.at_nanos(&ts).unwrap();
1871 let bytes_a = encode(&a);
1872
1873 let mut b = Chunk::new("t");
1874 b.column_ts("ts", &src, TimestampUnit::Nanos, None).unwrap();
1875 b.at_nanos(&ts).unwrap();
1876 let bytes_b = encode(&b);
1877
1878 assert_eq!(
1879 bytes_a, bytes_b,
1880 "TimestampNanosDirect must produce byte-identical wire to column_ts(Nanos)"
1881 );
1882 }
1883
1884 fn encode_datetime_col(dtype: NumpyDtype, src_le_bytes: &[u8], row_count: usize) -> Vec<u8> {
1886 let ts: Vec<i64> = (0..row_count as i64).collect();
1887 let mut chunk = Chunk::new("t");
1888 unsafe {
1889 chunk
1890 .push_numpy_deferred("v", dtype, src_le_bytes.as_ptr(), row_count, None)
1891 .unwrap();
1892 }
1893 chunk.at_nanos(&ts).unwrap();
1894 encode(&chunk)
1895 }
1896
1897 fn encode_micros_col(values: &[i64]) -> Vec<u8> {
1899 let ts: Vec<i64> = (0..values.len() as i64).collect();
1900 let mut chunk = Chunk::new("t");
1901 chunk
1902 .column_ts("v", values, TimestampUnit::Micros, None)
1903 .unwrap();
1904 chunk.at_nanos(&ts).unwrap();
1905 encode(&chunk)
1906 }
1907
1908 #[test]
1909 fn datetime_day_matches_column_ts_micros() {
1910 let src = [0i64, 1, 18262]; let expected = [0i64, 86_400_000_000, 18262 * 86_400_000_000];
1912 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1913 assert_eq!(
1914 encode_datetime_col(NumpyDtype::DatetimeDayToMicros, &raw, src.len()),
1915 encode_micros_col(&expected),
1916 );
1917 }
1918
1919 #[test]
1920 fn datetime_nat_maps_to_null_not_error() {
1921 let src = [0i64, i64::MIN, 1];
1925 let expected = [0i64, i64::MIN, 86_400_000_000];
1926 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1927 assert_eq!(
1928 encode_datetime_col(NumpyDtype::DatetimeDayToMicros, &raw, src.len()),
1929 encode_micros_col(&expected),
1930 );
1931 }
1932
1933 #[test]
1934 fn datetime_hour_matches_column_ts_micros() {
1935 let src = [0i64, 1, 24];
1936 let expected = [0i64, 3_600_000_000, 24 * 3_600_000_000];
1937 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1938 assert_eq!(
1939 encode_datetime_col(NumpyDtype::DatetimeHourToMicros, &raw, src.len()),
1940 encode_micros_col(&expected),
1941 );
1942 }
1943
1944 #[test]
1945 fn datetime_minute_matches_column_ts_micros() {
1946 let src = [0i64, 1, 60];
1947 let expected = [0i64, 60_000_000, 60 * 60_000_000];
1948 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1949 assert_eq!(
1950 encode_datetime_col(NumpyDtype::DatetimeMinuteToMicros, &raw, src.len()),
1951 encode_micros_col(&expected),
1952 );
1953 }
1954
1955 #[test]
1956 fn datetime_year_matches_calendar() {
1957 let src = [0i64, 50, -1];
1959 let expected = [0i64, 18262 * 86_400_000_000, -365 * 86_400_000_000];
1960 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1961 assert_eq!(
1962 encode_datetime_col(NumpyDtype::DatetimeYearToMicros, &raw, src.len()),
1963 encode_micros_col(&expected),
1964 );
1965 }
1966
1967 #[test]
1968 fn datetime_month_matches_calendar() {
1969 let src = [0i64, 1, 13, -1];
1972 let expected = [
1973 0i64,
1974 31 * 86_400_000_000,
1975 (365 + 31) * 86_400_000_000,
1976 -31 * 86_400_000_000,
1977 ];
1978 let raw: Vec<u8> = src.iter().flat_map(|v| v.to_le_bytes()).collect();
1979 assert_eq!(
1980 encode_datetime_col(NumpyDtype::DatetimeMonthToMicros, &raw, src.len()),
1981 encode_micros_col(&expected),
1982 );
1983 }
1984
1985 #[test]
1986 fn datetime_year_out_of_range_rejected() {
1987 let bad = [10_000_000i64]; let ts = [1i64];
1989 let mut chunk = Chunk::new("t");
1990 unsafe {
1991 chunk
1992 .push_numpy_deferred(
1993 "ts",
1994 NumpyDtype::DatetimeYearToMicros,
1995 bad.as_ptr() as *const u8,
1996 bad.len(),
1997 None,
1998 )
1999 .unwrap();
2000 }
2001 chunk.at_nanos(&ts).unwrap();
2002 let err = {
2003 let mut out = Vec::new();
2004 let mut dict = SymbolGlobalDict::new();
2005 let mut scratch = EncodeScratch::new();
2006 encode_chunk_into(&mut out, &chunk, &mut dict, &mut scratch, false).unwrap_err()
2007 };
2008 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
2009 assert!(err.msg().contains("overflows"));
2010 }
2011
2012 #[test]
2013 fn datetime_sec_overflow_rejected() {
2014 let bad = [i64::MAX];
2015 let ts = [1i64];
2016
2017 let mut chunk = Chunk::new("t");
2018 unsafe {
2019 chunk
2020 .push_numpy_deferred(
2021 "ts",
2022 NumpyDtype::DatetimeSecToMicros,
2023 bad.as_ptr() as *const u8,
2024 bad.len(),
2025 None,
2026 )
2027 .unwrap();
2028 }
2029 chunk.at_nanos(&ts).unwrap();
2030 let err = {
2031 let mut out = Vec::new();
2032 let mut dict = SymbolGlobalDict::new();
2033 let mut scratch = EncodeScratch::new();
2034 encode_chunk_into(&mut out, &chunk, &mut dict, &mut scratch, false).unwrap_err()
2035 };
2036 assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
2037 assert!(err.msg().contains("overflows"));
2038 }
2039
2040 #[test]
2041 fn f64_ndarray_1d_no_validity_layout() {
2042 let rows: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
2047 let ts = [10i64, 20];
2048 let mut shape = [0u32; MAX_ARRAY_DIMS];
2049 shape[0] = 3;
2050
2051 let mut chunk = Chunk::new("t");
2052 unsafe {
2053 chunk
2054 .push_numpy_deferred(
2055 "v",
2056 NumpyDtype::F64Ndarray { ndim: 1, shape },
2057 rows.as_ptr() as *const u8,
2058 2,
2059 None,
2060 )
2061 .unwrap();
2062 }
2063 chunk.at_nanos(&ts).unwrap();
2064 let bytes = encode(&chunk);
2065
2066 let mut body: Vec<u8> = Vec::new();
2069 body.push(0u8); for row_chunk in rows.chunks_exact(3) {
2071 body.push(1u8); body.extend_from_slice(&3u32.to_le_bytes()); for &v in row_chunk {
2074 body.extend_from_slice(&v.to_le_bytes());
2075 }
2076 }
2077 assert!(
2078 bytes.windows(body.len()).any(|w| w == body.as_slice()),
2079 "expected ndarray column body subsequence in encoded frame"
2080 );
2081 }
2082
2083 #[test]
2084 fn f16_bits_to_f32_known_values() {
2085 assert_eq!(f16_bits_to_f32(0x0000), 0.0f32);
2087 assert_eq!(f16_bits_to_f32(0x8000).to_bits(), (-0.0f32).to_bits());
2089 assert_eq!(f16_bits_to_f32(0x3C00), 1.0f32);
2091 assert_eq!(f16_bits_to_f32(0xC000), -2.0f32);
2093 assert!(f16_bits_to_f32(0x7C00).is_infinite() && f16_bits_to_f32(0x7C00) > 0.0);
2095 let v = f16_bits_to_f32(0x0001);
2097 assert_eq!(v, 2.0f32.powi(-24));
2098 }
2099}