use std::fmt::{self, Debug, Formatter};
use std::marker::PhantomData;
use std::slice;
use crate::ingress::TimestampUnit;
use crate::{Result, error};
#[cfg(feature = "arrow-ingress")]
use super::arrow_batch;
use super::numpy_wire;
use super::validity::{Validity, check_row_count};
use super::wire::{
QWP_TYPE_BINARY, QWP_TYPE_BOOLEAN, QWP_TYPE_BYTE, QWP_TYPE_DATE, QWP_TYPE_DOUBLE,
QWP_TYPE_FLOAT, QWP_TYPE_INT, QWP_TYPE_IPV4, QWP_TYPE_LONG, QWP_TYPE_LONG256, QWP_TYPE_SHORT,
QWP_TYPE_SYMBOL, QWP_TYPE_TIMESTAMP, QWP_TYPE_TIMESTAMP_NANOS, QWP_TYPE_UUID, QWP_TYPE_VARCHAR,
validate_column_name,
};
#[cfg(feature = "arrow-ingress")]
pub struct ImportedArrowColumn {
field: arrow::datatypes::Field,
array: arrow::array::ArrayRef,
kind: arrow_batch::ColumnKind,
}
#[cfg(feature = "arrow-ingress")]
impl ImportedArrowColumn {
pub unsafe fn import_from_ffi(
array: &mut arrow::ffi::FFI_ArrowArray,
schema: &arrow::ffi::FFI_ArrowSchema,
symbol: Option<bool>,
) -> Result<Self> {
use arrow::array::make_array;
let imported_array = unsafe { std::ptr::read(array) };
array.release = None;
let mut field = arrow::datatypes::Field::try_from(schema)
.map_err(|err| error::fmt!(ArrowIngest, "schema conversion failed: {}", err))?;
if let Some(want_symbol) = symbol {
let mut metadata = field.metadata().clone();
metadata.insert(
crate::arrow_metadata::SYMBOL.to_string(),
if want_symbol { "true" } else { "false" }.to_string(),
);
field = field.with_metadata(metadata);
}
let array_data = unsafe { arrow::ffi::from_ffi(imported_array, schema) }
.map_err(|err| error::fmt!(ArrowIngest, "from_ffi failed: {}", err))?;
array_data
.validate()
.map_err(|err| error::fmt!(ArrowIngest, "Arrow array validation failed: {}", err))?;
let array = make_array(array_data);
let kind = arrow_batch::classify(&field, array.as_ref())?;
Ok(Self { field, array, kind })
}
pub fn len(&self) -> usize {
self.array.len()
}
pub fn is_empty(&self) -> bool {
self.array.is_empty()
}
pub fn field(&self) -> &arrow::datatypes::Field {
&self.field
}
fn slice(&self, row_offset: usize, row_count: usize) -> Result<arrow::array::ArrayRef> {
let array_len = self.array.len();
let slice_end = row_offset.checked_add(row_count).ok_or_else(|| {
error::fmt!(
InvalidApiCall,
"row_offset {} + row_count {} overflows",
row_offset,
row_count
)
})?;
if slice_end > array_len {
return Err(error::fmt!(
InvalidApiCall,
"slice [{}, {}) out of range for array length {}",
row_offset,
slice_end,
array_len
));
}
Ok(if row_offset == 0 && row_count == array_len {
self.array.clone()
} else {
self.array.slice(row_offset, row_count)
})
}
}
#[derive(Clone, Copy)]
pub(crate) struct ValidityDescriptor {
pub(crate) bits: *const u8,
pub(crate) bit_len: usize,
pub(crate) non_null_count: usize,
}
impl ValidityDescriptor {
fn from_validity(v: &Validity<'_>) -> Self {
Self {
bits: v.bits.as_ptr(),
bit_len: v.bit_len,
non_null_count: v.non_null_count(),
}
}
#[inline]
pub(crate) unsafe fn is_valid(&self, idx: usize) -> bool {
debug_assert!(idx < self.bit_len);
let byte = unsafe { *self.bits.add(idx / 8) };
(byte >> (idx % 8)) & 1 == 1
}
#[inline]
pub(crate) fn byte_len(&self) -> usize {
self.bit_len.div_ceil(8)
}
#[inline]
pub(crate) fn has_nulls(&self) -> bool {
self.non_null_count < self.bit_len
}
unsafe fn slice_rows(&self, row_offset: usize, row_count: usize) -> Self {
debug_assert_eq!(row_offset % 8, 0);
debug_assert!(row_offset + row_count <= self.bit_len);
let bits = unsafe { self.bits.add(row_offset / 8) };
let mut non_null_count = 0usize;
for i in 0..row_count {
let byte = unsafe { *bits.add(i / 8) };
non_null_count += ((byte >> (i % 8)) & 1) as usize;
}
Self {
bits,
bit_len: row_count,
non_null_count,
}
}
}
pub(crate) enum ColumnKind {
Byte {
data: *const i8,
},
Short {
data: *const i16,
},
Int {
data: *const i32,
},
Long {
data: *const i64,
},
Float {
data: *const f32,
},
Double {
data: *const f64,
},
Bool {
bits: *const u8,
},
Ipv4 {
data: *const u32,
},
TsNanos {
data: *const i64,
},
TsMicros {
data: *const i64,
},
DateMillis {
data: *const i64,
},
Uuid {
data: *const [u8; 16],
},
Long256 {
data: *const [u8; 32],
},
Varchar {
offsets: *const i32,
offsets_len: usize,
bytes: *const u8,
bytes_len: usize,
},
VarcharLarge {
offsets: *const i64,
offsets_len: usize,
bytes: *const u8,
bytes_len: usize,
},
Binary {
offsets: *const i32,
offsets_len: usize,
bytes: *const u8,
bytes_len: usize,
},
Symbol {
codes: SymbolCodesPtr,
dict_offsets: SymbolOffsetsPtr,
dict_offsets_len: usize,
dict_bytes: *const u8,
dict_bytes_len: usize,
},
#[cfg(feature = "arrow-ingress")]
ArrowDeferred {
arrow_kind: arrow_batch::ColumnKind,
arr: arrow::array::ArrayRef,
},
NumpyDeferred {
dtype: numpy_wire::NumpyDtype,
data: *const u8,
src_stride: usize,
row_count: usize,
},
}
#[derive(Clone, Copy)]
pub(crate) enum SymbolCodesPtr {
I8(*const i8),
I16(*const i16),
I32(*const i32),
}
impl SymbolCodesPtr {
#[inline]
pub(crate) unsafe fn read_i64(&self, i: usize) -> i64 {
unsafe {
match self {
SymbolCodesPtr::I8(p) => *p.add(i) as i64,
SymbolCodesPtr::I16(p) => *p.add(i) as i64,
SymbolCodesPtr::I32(p) => *p.add(i) as i64,
}
}
}
#[inline]
unsafe fn offset_rows(self, n: usize) -> Self {
unsafe {
match self {
SymbolCodesPtr::I8(p) => SymbolCodesPtr::I8(p.add(n)),
SymbolCodesPtr::I16(p) => SymbolCodesPtr::I16(p.add(n)),
SymbolCodesPtr::I32(p) => SymbolCodesPtr::I32(p.add(n)),
}
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum SymbolOffsetsPtr {
I32(*const i32),
I64(*const i64),
}
impl SymbolOffsetsPtr {
#[inline]
pub(crate) unsafe fn read_i64(&self, i: usize) -> i64 {
unsafe {
match self {
SymbolOffsetsPtr::I32(p) => *p.add(i) as i64,
SymbolOffsetsPtr::I64(p) => *p.add(i),
}
}
}
}
impl ColumnKind {
unsafe fn slice_rows(&self, row_offset: usize, row_count: usize) -> Self {
debug_assert_eq!(row_offset % 8, 0);
unsafe {
match self {
ColumnKind::Byte { data } => ColumnKind::Byte {
data: data.add(row_offset),
},
ColumnKind::Short { data } => ColumnKind::Short {
data: data.add(row_offset),
},
ColumnKind::Int { data } => ColumnKind::Int {
data: data.add(row_offset),
},
ColumnKind::Long { data } => ColumnKind::Long {
data: data.add(row_offset),
},
ColumnKind::Float { data } => ColumnKind::Float {
data: data.add(row_offset),
},
ColumnKind::Double { data } => ColumnKind::Double {
data: data.add(row_offset),
},
ColumnKind::Bool { bits } => ColumnKind::Bool {
bits: bits.add(row_offset / 8),
},
ColumnKind::Ipv4 { data } => ColumnKind::Ipv4 {
data: data.add(row_offset),
},
ColumnKind::TsNanos { data } => ColumnKind::TsNanos {
data: data.add(row_offset),
},
ColumnKind::TsMicros { data } => ColumnKind::TsMicros {
data: data.add(row_offset),
},
ColumnKind::DateMillis { data } => ColumnKind::DateMillis {
data: data.add(row_offset),
},
ColumnKind::Uuid { data } => ColumnKind::Uuid {
data: data.add(row_offset),
},
ColumnKind::Long256 { data } => ColumnKind::Long256 {
data: data.add(row_offset),
},
ColumnKind::Varchar {
offsets,
offsets_len: _,
bytes,
bytes_len,
} => ColumnKind::Varchar {
offsets: offsets.add(row_offset),
offsets_len: row_count + 1,
bytes: *bytes,
bytes_len: *bytes_len,
},
ColumnKind::VarcharLarge {
offsets,
offsets_len: _,
bytes,
bytes_len,
} => ColumnKind::VarcharLarge {
offsets: offsets.add(row_offset),
offsets_len: row_count + 1,
bytes: *bytes,
bytes_len: *bytes_len,
},
ColumnKind::Binary {
offsets,
offsets_len: _,
bytes,
bytes_len,
} => ColumnKind::Binary {
offsets: offsets.add(row_offset),
offsets_len: row_count + 1,
bytes: *bytes,
bytes_len: *bytes_len,
},
ColumnKind::Symbol {
codes,
dict_offsets,
dict_offsets_len,
dict_bytes,
dict_bytes_len,
} => ColumnKind::Symbol {
codes: codes.offset_rows(row_offset),
dict_offsets: *dict_offsets,
dict_offsets_len: *dict_offsets_len,
dict_bytes: *dict_bytes,
dict_bytes_len: *dict_bytes_len,
},
#[cfg(feature = "arrow-ingress")]
ColumnKind::ArrowDeferred { arrow_kind, arr } => ColumnKind::ArrowDeferred {
arrow_kind: *arrow_kind,
arr: arr.slice(row_offset, row_count),
},
ColumnKind::NumpyDeferred {
dtype,
data,
src_stride,
row_count: _,
} => ColumnKind::NumpyDeferred {
dtype: *dtype,
data: data.add(row_offset * src_stride),
src_stride: *src_stride,
row_count,
},
}
}
}
}
impl DesignatedTsDescriptor {
unsafe fn slice_rows(&self, row_offset: usize) -> Self {
Self {
unit: self.unit,
data: match self.data {
DesignatedTsData::Column(ptr) => {
DesignatedTsData::Column(unsafe { ptr.add(row_offset) })
}
DesignatedTsData::Scalar(value) => DesignatedTsData::Scalar(value),
},
}
}
}
pub(crate) struct ColumnDescriptor {
pub(crate) name: String,
pub(crate) wire_type: u8,
pub(crate) kind: ColumnKind,
pub(crate) validity: Option<ValidityDescriptor>,
}
#[derive(Clone, Copy)]
pub(crate) enum DesignatedTsUnit {
Micros,
Nanos,
Millis,
Seconds,
}
impl DesignatedTsUnit {
pub(crate) fn wire_type(&self) -> u8 {
match self {
DesignatedTsUnit::Nanos => QWP_TYPE_TIMESTAMP_NANOS,
_ => QWP_TYPE_TIMESTAMP,
}
}
pub(crate) fn scale(&self) -> i64 {
match self {
DesignatedTsUnit::Millis => 1_000,
DesignatedTsUnit::Seconds => 1_000_000,
_ => 1,
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum DesignatedTsData {
Column(*const i64),
Scalar(i64),
}
pub(crate) struct DesignatedTsDescriptor {
pub(crate) unit: DesignatedTsUnit,
pub(crate) data: DesignatedTsData,
}
pub struct Chunk<'a> {
pub(crate) table: String,
pub(crate) row_count: Option<usize>,
pub(crate) columns: Vec<ColumnDescriptor>,
pub(crate) designated_ts: Option<DesignatedTsDescriptor>,
pub(crate) server_now: bool,
_marker: PhantomData<&'a ()>,
}
impl<'a> Chunk<'a> {
pub fn new(table: impl Into<String>) -> Self {
Self {
table: table.into(),
row_count: None,
columns: Vec::new(),
designated_ts: None,
server_now: false,
_marker: PhantomData,
}
}
pub fn table(&self) -> &str {
&self.table
}
pub fn row_count(&self) -> usize {
self.row_count.unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.row_count.is_none() && self.designated_ts.is_none()
}
pub fn clear(&mut self) {
self.row_count = None;
self.columns.clear();
self.designated_ts = None;
self.server_now = false;
}
pub(crate) unsafe fn slice_rows(&self, row_offset: usize, row_count: usize) -> Chunk<'a> {
let columns = self
.columns
.iter()
.map(|col| ColumnDescriptor {
name: col.name.clone(),
wire_type: col.wire_type,
kind: unsafe { col.kind.slice_rows(row_offset, row_count) },
validity: col
.validity
.as_ref()
.map(|v| unsafe { v.slice_rows(row_offset, row_count) }),
})
.collect();
Chunk {
table: self.table.clone(),
row_count: Some(row_count),
columns,
designated_ts: self
.designated_ts
.as_ref()
.map(|ts| unsafe { ts.slice_rows(row_offset) }),
server_now: self.server_now,
_marker: PhantomData,
}
}
pub fn column_i8(
&mut self,
name: &str,
data: &'a [i8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_BYTE,
ColumnKind::Byte {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_i16(
&mut self,
name: &str,
data: &'a [i16],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_SHORT,
ColumnKind::Short {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_i32(
&mut self,
name: &str,
data: &'a [i32],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_INT,
ColumnKind::Int {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_i64(
&mut self,
name: &str,
data: &'a [i64],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_LONG,
ColumnKind::Long {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_f32(
&mut self,
name: &str,
data: &'a [f32],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_FLOAT,
ColumnKind::Float {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_f64(
&mut self,
name: &str,
data: &'a [f64],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_DOUBLE,
ColumnKind::Double {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_bool(
&mut self,
name: &str,
data: &'a [u8],
row_count: usize,
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let bytes_required = row_count.div_ceil(8);
if data.len() < bytes_required {
return Err(error::fmt!(
InvalidApiCall,
"Boolean column data too short: {} bytes for {} rows (need at least {})",
data.len(),
row_count,
bytes_required
));
}
let row_count = check_row_count(self.row_count, row_count, validity)?;
self.push_column(
name,
QWP_TYPE_BOOLEAN,
ColumnKind::Bool {
bits: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_uuid(
&mut self,
name: &str,
data: &'a [[u8; 16]],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_UUID,
ColumnKind::Uuid {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_long256(
&mut self,
name: &str,
data: &'a [[u8; 32]],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_LONG256,
ColumnKind::Long256 {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_ipv4(
&mut self,
name: &str,
data: &'a [u32],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_IPV4,
ColumnKind::Ipv4 {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_ts(
&mut self,
name: &str,
data: &'a [i64],
unit: TimestampUnit,
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
let (wire_type, kind) = match unit {
TimestampUnit::Nanos => (
QWP_TYPE_TIMESTAMP_NANOS,
ColumnKind::TsNanos {
data: data.as_ptr(),
},
),
TimestampUnit::Micros => (
QWP_TYPE_TIMESTAMP,
ColumnKind::TsMicros {
data: data.as_ptr(),
},
),
};
self.push_column(name, wire_type, kind, validity, row_count)
}
pub fn column_date(
&mut self,
name: &str,
data: &'a [i64],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, data.len(), validity)?;
self.push_column(
name,
QWP_TYPE_DATE,
ColumnKind::DateMillis {
data: data.as_ptr(),
},
validity,
row_count,
)
}
pub fn column_str(
&mut self,
name: &str,
offsets: &'a [i32],
bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
if offsets.is_empty() {
return Err(error::fmt!(
InvalidApiCall,
"VARCHAR offsets must have at least one entry (row_count + 1)"
));
}
let row_count = offsets.len() - 1;
let row_count = check_row_count(self.row_count, row_count, validity)?;
validate_varchar_offsets(offsets, bytes.len())?;
validate_varchar_utf8_cells(bytes, offsets)?;
self.push_column(
name,
QWP_TYPE_VARCHAR,
ColumnKind::Varchar {
offsets: offsets.as_ptr(),
offsets_len: offsets.len(),
bytes: bytes.as_ptr(),
bytes_len: bytes.len(),
},
validity,
row_count,
)
}
pub fn column_str_large(
&mut self,
name: &str,
offsets: &'a [i64],
bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
if offsets.is_empty() {
return Err(error::fmt!(
InvalidApiCall,
"LargeVARCHAR offsets must have at least one entry (row_count + 1)"
));
}
let row_count = offsets.len() - 1;
let row_count = check_row_count(self.row_count, row_count, validity)?;
validate_varchar_offsets_i64(offsets, bytes.len())?;
validate_varchar_utf8_cells(bytes, offsets)?;
self.push_column(
name,
QWP_TYPE_VARCHAR,
ColumnKind::VarcharLarge {
offsets: offsets.as_ptr(),
offsets_len: offsets.len(),
bytes: bytes.as_ptr(),
bytes_len: bytes.len(),
},
validity,
row_count,
)
}
pub fn column_binary(
&mut self,
name: &str,
offsets: &'a [i32],
bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
if offsets.is_empty() {
return Err(error::fmt!(
InvalidApiCall,
"BINARY offsets must have at least one entry (row_count + 1)"
));
}
let row_count = offsets.len() - 1;
let row_count = check_row_count(self.row_count, row_count, validity)?;
validate_varchar_offsets(offsets, bytes.len())?;
self.push_column(
name,
QWP_TYPE_BINARY,
ColumnKind::Binary {
offsets: offsets.as_ptr(),
offsets_len: offsets.len(),
bytes: bytes.as_ptr(),
bytes_len: bytes.len(),
},
validity,
row_count,
)
}
pub fn symbol_i8(
&mut self,
name: &str,
codes: &'a [i8],
dict_offsets: &'a [i32],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I8(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I32(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
pub fn symbol_i16(
&mut self,
name: &str,
codes: &'a [i16],
dict_offsets: &'a [i32],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I16(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I32(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
pub fn symbol_i32(
&mut self,
name: &str,
codes: &'a [i32],
dict_offsets: &'a [i32],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I32(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I32(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
pub fn symbol_large_i8(
&mut self,
name: &str,
codes: &'a [i8],
dict_offsets: &'a [i64],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I8(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I64(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
pub fn symbol_large_i16(
&mut self,
name: &str,
codes: &'a [i16],
dict_offsets: &'a [i64],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I16(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I64(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
pub fn symbol_large_i32(
&mut self,
name: &str,
codes: &'a [i32],
dict_offsets: &'a [i64],
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
self.push_symbol(
name,
SymbolCodesPtr::I32(codes.as_ptr()),
codes.len(),
SymbolOffsetsPtr::I64(dict_offsets.as_ptr()),
dict_offsets.len(),
dict_bytes,
validity,
)
}
#[allow(clippy::too_many_arguments)]
fn push_symbol(
&mut self,
name: &str,
codes: SymbolCodesPtr,
codes_len: usize,
dict_offsets: SymbolOffsetsPtr,
dict_offsets_len: usize,
dict_bytes: &'a [u8],
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
let row_count = check_row_count(self.row_count, codes_len, validity)?;
if dict_offsets_len == 0 {
return Err(error::fmt!(
InvalidApiCall,
"symbol dict offsets must have at least one entry (dict_len + 1)"
));
}
let dict_len = dict_offsets_len - 1;
if dict_len > super::MAX_SYMBOL_DICT_ENTRIES {
return Err(error::fmt!(
InvalidApiCall,
"symbol dict has {dict_len} entries, exceeding the per-column maximum of {}",
super::MAX_SYMBOL_DICT_ENTRIES
));
}
match dict_offsets {
SymbolOffsetsPtr::I32(p) => {
let offsets = unsafe { slice::from_raw_parts(p, dict_offsets_len) };
validate_varchar_offsets(offsets, dict_bytes.len())?;
validate_varchar_utf8_cells(dict_bytes, offsets)?;
}
SymbolOffsetsPtr::I64(p) => {
let offsets = unsafe { slice::from_raw_parts(p, dict_offsets_len) };
validate_varchar_offsets_i64(offsets, dict_bytes.len())?;
validate_varchar_utf8_cells(dict_bytes, offsets)?;
}
}
let bounds_check = match codes {
SymbolCodesPtr::I8(p) => unsafe { range_check_codes(p, codes_len, dict_len, validity) },
SymbolCodesPtr::I16(p) => unsafe {
range_check_codes(p, codes_len, dict_len, validity)
},
SymbolCodesPtr::I32(p) => unsafe {
range_check_codes(p, codes_len, dict_len, validity)
},
};
bounds_check?;
self.push_column(
name,
QWP_TYPE_SYMBOL,
ColumnKind::Symbol {
codes,
dict_offsets,
dict_offsets_len,
dict_bytes: dict_bytes.as_ptr(),
dict_bytes_len: dict_bytes.len(),
},
validity,
row_count,
)
}
pub unsafe fn push_numpy_deferred(
&mut self,
name: &str,
dtype: numpy_wire::NumpyDtype,
data: *const u8,
row_count: usize,
validity: Option<&Validity<'a>>,
) -> Result<&mut Self> {
if data.is_null() && row_count != 0 {
return Err(error::fmt!(
InvalidApiCall,
"push_numpy_deferred: data pointer is NULL with row_count = {}",
row_count
));
}
dtype.validate()?;
let src_stride = dtype.source_elem_size()?;
let row_count = check_row_count(self.row_count, row_count, validity)?;
let wire_type = dtype.wire_type();
self.push_column(
name,
wire_type,
ColumnKind::NumpyDeferred {
dtype,
data,
src_stride,
row_count,
},
validity,
row_count,
)
}
pub fn at_micros(&mut self, data: &'a [i64]) -> Result<&mut Self> {
self.set_designated_ts(DesignatedTsUnit::Micros, data)
}
pub fn at_nanos(&mut self, data: &'a [i64]) -> Result<&mut Self> {
self.set_designated_ts(DesignatedTsUnit::Nanos, data)
}
pub fn at_millis(&mut self, data: &'a [i64]) -> Result<&mut Self> {
self.set_designated_ts(DesignatedTsUnit::Millis, data)
}
pub fn at_seconds(&mut self, data: &'a [i64]) -> Result<&mut Self> {
self.set_designated_ts(DesignatedTsUnit::Seconds, data)
}
pub fn at_now(&mut self) -> Result<&mut Self> {
if self.designated_ts.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"designated timestamp already set on this chunk"
));
}
self.server_now = true;
Ok(self)
}
pub fn at_scalar_nanos(&mut self, nanos: i64) -> Result<&mut Self> {
if self.designated_ts.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"designated timestamp already set on this chunk"
));
}
if self.server_now {
return Err(error::fmt!(
InvalidApiCall,
"scalar designated timestamp conflicts with at_now \
(server-assigned timestamps) already set on this chunk"
));
}
if nanos < 0 {
return Err(error::fmt!(
InvalidTimestamp,
"scalar designated timestamp is negative ({})",
nanos
));
}
self.designated_ts = Some(DesignatedTsDescriptor {
unit: DesignatedTsUnit::Nanos,
data: DesignatedTsData::Scalar(nanos),
});
Ok(self)
}
fn set_designated_ts(&mut self, unit: DesignatedTsUnit, data: &'a [i64]) -> Result<&mut Self> {
if self.designated_ts.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"designated timestamp already set on this chunk"
));
}
if self.server_now {
return Err(error::fmt!(
InvalidApiCall,
"designated timestamp column conflicts with at_now \
(server-assigned timestamps) already set on this chunk"
));
}
let row_count = check_row_count(self.row_count, data.len(), None)?;
self.designated_ts = Some(DesignatedTsDescriptor {
unit,
data: DesignatedTsData::Column(data.as_ptr()),
});
self.row_count = Some(row_count);
Ok(self)
}
fn push_column(
&mut self,
name: &str,
wire_type: u8,
kind: ColumnKind,
validity: Option<&Validity<'_>>,
row_count: usize,
) -> Result<&mut Self> {
validate_column_name(name)?;
self.guard_unique_name(name)?;
let validity = validity.map(ValidityDescriptor::from_validity);
self.columns.push(ColumnDescriptor {
name: name.to_owned(),
wire_type,
kind,
validity,
});
self.row_count = Some(row_count);
Ok(self)
}
#[cfg(feature = "arrow-ingress")]
pub fn push_arrow_column(
&mut self,
name: &str,
field: &arrow::datatypes::Field,
arr: arrow::array::ArrayRef,
) -> Result<&mut Self> {
if field.data_type() != arr.data_type() {
return Err(error::fmt!(
InvalidApiCall,
"column {:?}: field data type {:?} does not match array data type {:?}",
name,
field.data_type(),
arr.data_type()
));
}
let kind = arrow_batch::classify(field, arr.as_ref())?;
self.push_arrow_deferred(name, kind, arr)
}
#[cfg(feature = "arrow-ingress")]
pub fn push_imported_arrow_slice(
&mut self,
name: &str,
imported: &ImportedArrowColumn,
row_offset: usize,
row_count: usize,
) -> Result<&mut Self> {
let arr = imported.slice(row_offset, row_count)?;
self.push_arrow_deferred(name, imported.kind, arr)
}
#[cfg(feature = "arrow-ingress")]
pub(crate) fn push_arrow_deferred(
&mut self,
name: &str,
arrow_kind: arrow_batch::ColumnKind,
arr: arrow::array::ArrayRef,
) -> Result<&mut Self> {
validate_column_name(name)?;
self.guard_unique_name(name)?;
let row_count = check_row_count(self.row_count, arr.len(), None)?;
let has_nulls = arr.null_count() > 0;
let wire_type = arrow_batch::wire_type_byte(arrow_kind, has_nulls);
self.columns.push(ColumnDescriptor {
name: name.to_owned(),
wire_type,
kind: ColumnKind::ArrowDeferred { arrow_kind, arr },
validity: None,
});
self.row_count = Some(row_count);
Ok(self)
}
fn guard_unique_name(&self, name: &str) -> Result<()> {
if self.columns.iter().any(|c| c.name == name) {
return Err(error::fmt!(
InvalidApiCall,
"duplicate column name in chunk: {:?}",
name
));
}
Ok(())
}
}
fn validate_varchar_utf8_cells<O>(bytes: &[u8], offsets: &[O]) -> Result<()>
where
O: Copy + Into<i64>,
{
let base: i64 = offsets[0].into();
let end: i64 = offsets[offsets.len() - 1].into();
let span = std::str::from_utf8(&bytes[base as usize..end as usize])
.map_err(|e| error::fmt!(InvalidApiCall, "VARCHAR bytes are not valid UTF-8: {}", e))?;
for o in offsets.get(1..offsets.len() - 1).unwrap_or(&[]) {
let off: i64 = (*o).into();
if !span.is_char_boundary((off - base) as usize) {
return Err(error::fmt!(
InvalidApiCall,
"VARCHAR offset {} splits a multi-byte UTF-8 character",
off
));
}
}
Ok(())
}
fn validate_varchar_offsets(offsets: &[i32], bytes_len: usize) -> Result<()> {
let mut prev = offsets[0];
if prev < 0 {
return Err(error::fmt!(
InvalidApiCall,
"VARCHAR offsets must be non-negative (offsets[0] = {})",
prev
));
}
for (i, &off) in offsets.iter().enumerate().skip(1) {
if off < prev {
return Err(error::fmt!(
InvalidApiCall,
"VARCHAR offsets must be non-decreasing (offsets[{}] = {} < offsets[{}] = {})",
i,
off,
i - 1,
prev
));
}
prev = off;
}
if (prev as usize) > bytes_len {
return Err(error::fmt!(
InvalidApiCall,
"VARCHAR offsets exceed bytes buffer: last offset = {}, bytes_len = {}",
prev,
bytes_len
));
}
Ok(())
}
fn validate_varchar_offsets_i64(offsets: &[i64], bytes_len: usize) -> Result<()> {
let first = offsets[0];
if first < 0 {
return Err(error::fmt!(
InvalidApiCall,
"LargeVARCHAR offsets must be non-negative (offsets[0] = {})",
first
));
}
let mut prev = first;
for (i, &off) in offsets.iter().enumerate().skip(1) {
if off < prev {
return Err(error::fmt!(
InvalidApiCall,
"LargeVARCHAR offsets must be non-decreasing (offsets[{}] = {} < offsets[{}] = {})",
i,
off,
i - 1,
prev
));
}
prev = off;
}
let last = prev;
if (last as u64) > bytes_len as u64 {
return Err(error::fmt!(
InvalidApiCall,
"LargeVARCHAR offsets exceed bytes buffer: last offset = {}, bytes_len = {}",
last,
bytes_len
));
}
let span = last - first;
if span > u32::MAX as i64 {
return Err(error::fmt!(
InvalidApiCall,
"LargeVARCHAR slice span exceeds QWP uint32 limit: \
last - first = {} - {} = {} > {} (u32::MAX)",
last,
first,
span,
u32::MAX
));
}
Ok(())
}
unsafe fn range_check_codes<T>(
p: *const T,
codes_len: usize,
dict_len: usize,
validity: Option<&Validity<'_>>,
) -> Result<()>
where
T: Copy + Into<i64>,
{
for i in 0..codes_len {
if validity.is_some_and(|v| !v.is_valid(i)) {
continue;
}
let code = unsafe { (*p.add(i)).into() };
if code < 0 || (code as usize) >= dict_len {
return Err(error::fmt!(
InvalidApiCall,
"symbol code out of range: row {} -> {} (dict_len = {})",
i,
code,
dict_len
));
}
}
Ok(())
}
impl Debug for Chunk<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Chunk")
.field("table", &self.table)
.field("row_count", &self.row_count())
.field("columns", &self.columns.len())
.field("has_designated_ts", &self.designated_ts.is_some())
.field("server_now", &self.server_now)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locks_row_count_on_first_column() {
let mut chunk = Chunk::new("t");
let a = [1i64, 2, 3];
chunk.column_i64("a", &a, None).unwrap();
assert_eq!(chunk.row_count(), 3);
let b = [4i64, 5];
let err = chunk.column_i64("b", &b, None).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("row_count"));
}
#[test]
fn column_name_validation_matches_canonical_validator() {
use crate::ingress::ColumnName;
for name in [
"ok_col", "a_b", "bad?col", "a.b", "a,b", "a/b", "a-b", "", ] {
let canonical_rejects = ColumnName::new(name).is_err();
let mut chunk = Chunk::new("t");
let data = [1i64];
let chunk_rejects = chunk.column_i64(name, &data, None).is_err();
assert_eq!(
chunk_rejects, canonical_rejects,
"column name {name:?}: chunk path and ColumnName::new disagree \
(chunk_rejects={chunk_rejects}, canonical_rejects={canonical_rejects})"
);
}
}
#[test]
fn rejects_duplicate_column_name() {
let mut chunk = Chunk::new("t");
let a1 = [1i64];
chunk.column_i64("a", &a1, None).unwrap();
let a2 = [2i64];
let err = chunk.column_i64("a", &a2, None).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("duplicate"));
}
#[test]
fn rejects_invalid_validity_length() {
let mut chunk = Chunk::new("t");
let bits = [0xFFu8];
let v = Validity::from_bitmap(&bits, 8).unwrap();
let data = [1i64, 2, 3];
let err = chunk.column_i64("a", &data, Some(&v)).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("Validity bitmap"));
}
#[test]
fn designated_ts_sets_row_count() {
let mut chunk = Chunk::new("t");
let ts = [1i64, 2, 3];
chunk.at_micros(&ts).unwrap();
assert_eq!(chunk.row_count(), 3);
let ts2 = [4i64, 5, 6];
let err = chunk.at_nanos(&ts2).unwrap_err();
assert!(err.msg().contains("designated"));
}
#[test]
fn clear_resets_columns_but_keeps_table() {
let mut chunk = Chunk::new("t");
let a = [1i64];
let ts = [10i64];
chunk.column_i64("a", &a, None).unwrap();
chunk.at_nanos(&ts).unwrap();
chunk.clear();
assert_eq!(chunk.row_count(), 0);
assert!(chunk.is_empty());
assert_eq!(chunk.table(), "t");
}
#[test]
fn varchar_rejects_negative_offset() {
let mut chunk = Chunk::new("t");
let offsets = [-1i32, 1, 2];
let err = chunk.column_str("v", &offsets, b"ab", None).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("non-negative"));
}
#[test]
fn varchar_rejects_non_monotonic_offsets() {
let mut chunk = Chunk::new("t");
let offsets = [0i32, 5, 3];
let err = chunk.column_str("v", &offsets, b"abcde", None).unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("non-decreasing"));
}
#[test]
fn varchar_rejects_invalid_utf8() {
let mut chunk = Chunk::new("t");
let offsets = [0i32, 2];
let err = chunk
.column_str("v", &offsets, &[0xff, 0xfe], None)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("UTF-8"));
}
#[test]
fn varchar_rejects_offset_splitting_multibyte_char() {
let mut chunk = Chunk::new("t");
let offsets = [0i32, 1, 2];
let err = chunk
.column_str("v", &offsets, &[0xC3, 0xA9], None)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("splits a multi-byte"));
}
#[test]
fn varchar_accepts_char_aligned_multibyte_offsets() {
let mut chunk = Chunk::new("t");
chunk
.column_str("v", &[0i32, 1, 3], &[0x61, 0xC3, 0xA9], None)
.unwrap();
assert_eq!(chunk.row_count(), 2);
}
#[test]
fn symbol_rejects_out_of_range_code() {
let mut chunk = Chunk::new("t");
let codes = [0i32, 99];
let dict_offsets = [0i32, 5];
let err = chunk
.symbol_i32("sym", &codes, &dict_offsets, b"alpha", None)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("out of range"));
}
#[test]
fn symbol_rejects_invalid_utf8_dict_bytes() {
let mut chunk = Chunk::new("t");
let codes = [0i32];
let dict_offsets = [0i32, 2];
let err = chunk
.symbol_i32("sym", &codes, &dict_offsets, &[0xff, 0xfe], None)
.unwrap_err();
assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
assert!(err.msg().contains("UTF-8"));
}
#[test]
fn symbol_accepts_valid_multibyte_utf8_dict_bytes() {
let mut chunk = Chunk::new("t");
let codes = [0i32, 1];
let dict_offsets = [0i32, 1, 3];
chunk
.symbol_i32("sym", &codes, &dict_offsets, &[0x61, 0xC3, 0xA9], None)
.expect("valid UTF-8 dictionary is accepted");
assert_eq!(chunk.row_count(), 2);
}
#[test]
fn symbol_skips_null_codes() {
let mut chunk = Chunk::new("t");
let codes = [0i32, 99];
let dict_offsets = [0i32, 5];
let bits = [0b0000_0001];
let v = Validity::from_bitmap(&bits, 2).unwrap();
chunk
.symbol_i32("sym", &codes, &dict_offsets, b"alpha", Some(&v))
.expect("null row's bogus code is ignored");
}
#[test]
fn numpy_split_advances_by_source_stride_not_wire_width() {
use numpy_wire::NumpyDtype;
let cases = [
NumpyDtype::I8WidenToI32,
NumpyDtype::U8WidenToI32,
NumpyDtype::I16WidenToI32,
NumpyDtype::I32WidenToI64,
];
for (i, dtype) in cases.into_iter().enumerate() {
let expected_stride = dtype.source_elem_size().unwrap();
let wire = dtype.bytes_per_row();
assert!(
expected_stride < wire,
"case {i}: expected a widening dtype"
);
let buf = vec![0u8; 16 * wire];
let mut chunk = Chunk::new("t");
unsafe {
chunk
.push_numpy_deferred("a", dtype, buf.as_ptr(), 16, None)
.unwrap();
}
let sliced = unsafe { chunk.slice_rows(8, 8) };
match &sliced.columns[0].kind {
ColumnKind::NumpyDeferred {
data, src_stride, ..
} => {
assert_eq!(
*src_stride, expected_stride,
"case {i}: stored source stride is wrong"
);
let want = unsafe { buf.as_ptr().add(8 * expected_stride) };
let buggy = unsafe { buf.as_ptr().add(8 * wire) };
assert_eq!(
*data, want,
"case {i}: pointer not advanced by the source stride"
);
assert_ne!(
*data, buggy,
"case {i}: pointer advanced by the wire width (the bug)"
);
}
_ => panic!("case {i}: expected a NumpyDeferred column"),
}
}
}
}