Skip to main content

reifydb_core/value/column/
columns.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	hash::Hash,
6	ops::{Index, IndexMut},
7};
8
9use indexmap::IndexMap;
10use reifydb_codec::row::{
11	bytes::EncodedBytes,
12	shape::{RowFamily, RowShape},
13};
14use reifydb_value::{
15	Result,
16	fragment::Fragment,
17	reifydb_assertions,
18	value::{
19		Value,
20		constraint::Constraint,
21		datetime::{CREATED_AT_COLUMN_NAME, DateTime, TIME_COLUMN_NAME, UPDATED_AT_COLUMN_NAME},
22		partition::Partition,
23		row_number::{ROW_NUMBER_COLUMN_NAME, RowNumber},
24		system_columns::{RowStamps, SystemColumns},
25		value_type::ValueType,
26	},
27};
28use serde::{Deserialize, Serialize};
29
30use crate::{
31	interface::catalog::column::Column as CatalogColumn,
32	return_internal_error,
33	row::Row,
34	value::column::{ColumnBuffer, ColumnWithName, data::Column, headers::ColumnHeaders},
35};
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Columns {
39	pub system: SystemColumns,
40	pub columns: Vec<ColumnBuffer>,
41	pub names: Vec<Fragment>,
42}
43
44impl Columns {
45	#[inline]
46	pub fn row_numbers(&self) -> &[RowNumber] {
47		self.system.row_numbers()
48	}
49
50	#[inline]
51	pub fn partitions(&self) -> &[Partition] {
52		self.system.partitions()
53	}
54
55	#[inline]
56	pub fn created_at(&self) -> &[DateTime] {
57		self.system.created_at()
58	}
59
60	#[inline]
61	pub fn updated_at(&self) -> &[DateTime] {
62		self.system.updated_at()
63	}
64
65	#[inline]
66	pub fn time(&self) -> &[DateTime] {
67		self.system.time()
68	}
69
70	pub fn system_column(&self, name: &str) -> Option<ColumnBuffer> {
71		let name = name.strip_prefix('#').unwrap_or(name);
72		let row_count = self.row_count();
73
74		if name == ROW_NUMBER_COLUMN_NAME && self.row_numbers().len() == row_count {
75			let values: Vec<u64> = self.row_numbers().iter().map(|r| r.value()).collect();
76			return Some(ColumnBuffer::uint8(values));
77		}
78		if name == CREATED_AT_COLUMN_NAME && self.created_at().len() == row_count {
79			return Some(ColumnBuffer::datetime(self.created_at().to_vec()));
80		}
81		if name == UPDATED_AT_COLUMN_NAME && self.updated_at().len() == row_count {
82			return Some(ColumnBuffer::datetime(self.updated_at().to_vec()));
83		}
84		if name == TIME_COLUMN_NAME && self.time().len() == row_count {
85			return Some(ColumnBuffer::datetime(self.time().to_vec()));
86		}
87		None
88	}
89}
90
91#[derive(Debug, Clone, Copy)]
92pub struct ColumnRef<'a> {
93	name: &'a Fragment,
94	data: &'a ColumnBuffer,
95}
96
97impl Index<usize> for Columns {
98	type Output = ColumnBuffer;
99
100	fn index(&self, index: usize) -> &Self::Output {
101		&self.columns[index]
102	}
103}
104
105impl IndexMut<usize> for Columns {
106	fn index_mut(&mut self, index: usize) -> &mut Self::Output {
107		&mut self.columns[index]
108	}
109}
110
111impl<'a> ColumnRef<'a> {
112	pub fn new(name: &'a Fragment, data: &'a ColumnBuffer) -> Self {
113		Self {
114			name,
115			data,
116		}
117	}
118
119	pub fn name(&self) -> &'a Fragment {
120		self.name
121	}
122
123	pub fn data(&self) -> &'a ColumnBuffer {
124		self.data
125	}
126
127	pub fn get_type(&self) -> ValueType {
128		self.data.get_type()
129	}
130
131	pub fn column(&self) -> Column {
132		Column::from_column_buffer(self.data.clone())
133	}
134
135	pub fn with_new_data(&self, data: ColumnBuffer) -> ColumnWithName {
136		ColumnWithName::new(self.name.clone(), data)
137	}
138}
139
140fn value_to_buffer(value: Value) -> ColumnBuffer {
141	match value {
142		Value::None {
143			inner,
144		} => ColumnBuffer::none_typed(inner, 1),
145		Value::Boolean(v) => ColumnBuffer::bool([v]),
146		Value::Float4(v) => ColumnBuffer::float4([v.into()]),
147		Value::Float8(v) => ColumnBuffer::float8([v.into()]),
148		Value::Int1(v) => ColumnBuffer::int1([v]),
149		Value::Int2(v) => ColumnBuffer::int2([v]),
150		Value::Int4(v) => ColumnBuffer::int4([v]),
151		Value::Int8(v) => ColumnBuffer::int8([v]),
152		Value::Int16(v) => ColumnBuffer::int16([v]),
153		Value::Utf8(v) => ColumnBuffer::utf8([v]),
154		Value::Uint1(v) => ColumnBuffer::uint1([v]),
155		Value::Uint2(v) => ColumnBuffer::uint2([v]),
156		Value::Uint4(v) => ColumnBuffer::uint4([v]),
157		Value::Uint8(v) => ColumnBuffer::uint8([v]),
158		Value::Uint16(v) => ColumnBuffer::uint16([v]),
159		Value::Date(v) => ColumnBuffer::date([v]),
160		Value::DateTime(v) => ColumnBuffer::datetime([v]),
161		Value::Time(v) => ColumnBuffer::time([v]),
162		Value::Duration(v) => ColumnBuffer::duration([v]),
163		Value::IdentityId(v) => ColumnBuffer::identity_id([v]),
164		Value::Uuid4(v) => ColumnBuffer::uuid4([v]),
165		Value::Uuid7(v) => ColumnBuffer::uuid7([v]),
166		Value::Blob(v) => ColumnBuffer::blob([v]),
167		Value::Int(v) => ColumnBuffer::int(vec![v]),
168		Value::Uint(v) => ColumnBuffer::uint(vec![v]),
169		Value::Decimal(v) => ColumnBuffer::decimal(vec![v]),
170		Value::DictionaryId(v) => ColumnBuffer::dictionary_id(vec![v]),
171		Value::Any(v) => ColumnBuffer::any(vec![*v]),
172		Value::Type(v) => ColumnBuffer::any(vec![Value::Type(v)]),
173		Value::List(v) => ColumnBuffer::any(vec![Value::List(v)]),
174		Value::Record(v) => ColumnBuffer::any(vec![Value::Record(v)]),
175		Value::Tuple(v) => ColumnBuffer::any(vec![Value::Tuple(v)]),
176	}
177}
178
179impl Columns {
180	pub fn scalar_value(&self) -> Value {
181		reifydb_assertions! {
182			assert_eq!(self.len(), 1, "scalar_value() requires exactly 1 column, got {}", self.len());
183			assert_eq!(
184				self.row_count(),
185				1,
186				"scalar_value() requires exactly 1 row, got {}",
187				self.row_count()
188			);
189		}
190		self.columns[0].get_value(0)
191	}
192
193	pub fn new(columns: Vec<ColumnWithName>) -> Self {
194		let n = columns.first().map_or(0, |c| c.data.len());
195		assert!(columns.iter().all(|c| c.data.len() == n));
196
197		let mut names = Vec::with_capacity(columns.len());
198		let mut buffers = Vec::with_capacity(columns.len());
199		for c in columns {
200			names.push(c.name);
201			buffers.push(c.data);
202		}
203
204		Self {
205			system: SystemColumns::empty(),
206			columns: buffers,
207			names,
208		}
209	}
210
211	pub fn with_system(columns: Vec<ColumnWithName>, system: SystemColumns) -> Self {
212		let n = columns.first().map_or(0, |c| c.data.len());
213		assert!(columns.iter().all(|c| c.data.len() == n));
214		system.assert_invariants(n, "Columns::with_system");
215
216		let mut names = Vec::with_capacity(columns.len());
217		let mut buffers = Vec::with_capacity(columns.len());
218		for c in columns {
219			names.push(c.name);
220			buffers.push(c.data);
221		}
222
223		Self {
224			system,
225			columns: buffers,
226			names,
227		}
228	}
229
230	pub fn single_row<'b>(rows: impl IntoIterator<Item = (&'b str, Value)>) -> Columns {
231		let mut names = Vec::new();
232		let mut buffers = Vec::new();
233		for (name, value) in rows {
234			names.push(Fragment::internal(name));
235			buffers.push(value_to_buffer(value));
236		}
237		Self {
238			system: SystemColumns::empty(),
239			columns: buffers,
240			names,
241		}
242	}
243
244	pub fn with_row_numbers(mut self, row_numbers: Vec<RowNumber>) -> Self {
245		let n = row_numbers.len();
246		self.system = SystemColumns::new(
247			row_numbers,
248			self.system.partitions().to_vec(),
249			self.system.created_at().to_vec(),
250			self.system.updated_at().to_vec(),
251			self.system.time().to_vec(),
252		);
253		self.system.assert_invariants(n, "Columns::with_row_numbers");
254		self
255	}
256
257	pub fn from_catalog_columns(cols: &[CatalogColumn]) -> Self {
258		let mut names = Vec::with_capacity(cols.len());
259		let mut buffers = Vec::with_capacity(cols.len());
260		for col in cols {
261			names.push(Fragment::internal(&col.name));
262			buffers.push(ColumnBuffer::with_capacity(col.constraint.get_type(), 0));
263		}
264		Self {
265			system: SystemColumns::empty(),
266			columns: buffers,
267			names,
268		}
269	}
270
271	pub fn apply_headers(&mut self, headers: &ColumnHeaders) {
272		let n = self.len();
273		let names = &mut self.names;
274		for (i, name) in headers.columns.iter().enumerate() {
275			if i < n {
276				names[i] = name.clone();
277			}
278		}
279	}
280}
281
282impl Columns {
283	pub fn number(&self) -> RowNumber {
284		assert_eq!(self.row_count(), 1, "number() requires exactly 1 row, got {}", self.row_count());
285		if self.row_numbers().is_empty() {
286			RowNumber(0)
287		} else {
288			self.row_numbers()[0]
289		}
290	}
291
292	pub fn shape(&self) -> (usize, usize) {
293		let row_count = if !self.row_numbers().is_empty() {
294			self.row_numbers().len()
295		} else {
296			self.columns.first().map(|c| c.len()).unwrap_or(0)
297		};
298		(row_count, self.len())
299	}
300
301	pub fn heap_size(&self) -> usize {
302		let data: usize = self.columns.iter().map(|c| c.heap_size()).sum();
303		let names: usize = self.names.iter().map(|n| n.text().len()).sum();
304		data + names + self.system.heap_size()
305	}
306
307	pub fn len(&self) -> usize {
308		self.columns.len()
309	}
310
311	pub fn is_empty(&self) -> bool {
312		self.columns.is_empty()
313	}
314
315	pub fn iter(&self) -> impl Iterator<Item = ColumnRef<'_>> + '_ {
316		self.names.iter().zip(self.columns.iter()).map(|(n, d)| ColumnRef::new(n, d))
317	}
318
319	pub fn first(&self) -> Option<ColumnRef<'_>> {
320		self.get(0)
321	}
322
323	pub fn last(&self) -> Option<ColumnRef<'_>> {
324		let n = self.len();
325		if n == 0 {
326			None
327		} else {
328			self.get(n - 1)
329		}
330	}
331
332	pub fn get(&self, index: usize) -> Option<ColumnRef<'_>> {
333		if index < self.len() {
334			Some(ColumnRef::new(&self.names[index], &self.columns[index]))
335		} else {
336			None
337		}
338	}
339
340	pub fn name_at(&self, index: usize) -> &Fragment {
341		&self.names[index]
342	}
343
344	pub fn data_at(&self, index: usize) -> &ColumnBuffer {
345		&self.columns[index]
346	}
347
348	pub fn data_at_mut(&mut self, index: usize) -> &mut ColumnBuffer {
349		&mut self.columns[index]
350	}
351
352	pub fn row(&self, i: usize) -> Vec<Value> {
353		self.columns.iter().map(|c| c.get_value(i)).collect()
354	}
355
356	pub fn column(&self, name: &str) -> Option<ColumnRef<'_>> {
357		self.names.iter().position(|n| n.text() == name).and_then(|i| self.get(i))
358	}
359
360	pub fn row_count(&self) -> usize {
361		if !self.row_numbers().is_empty() {
362			self.row_numbers().len()
363		} else {
364			self.columns.first().map_or(0, |col| col.len())
365		}
366	}
367
368	pub fn has_rows(&self) -> bool {
369		self.row_count() > 0
370	}
371
372	pub fn is_scalar(&self) -> bool {
373		self.len() == 1 && self.row_count() == 1
374	}
375
376	pub fn get_row(&self, index: usize) -> Vec<Value> {
377		self.columns.iter().map(|col| col.get_value(index)).collect()
378	}
379
380	#[track_caller]
381	pub fn assert_invariants(&self, ctx: &str) {
382		let n = self.columns.first().map_or(0, |c| c.len());
383		for (i, col) in self.columns.iter().enumerate() {
384			assert_eq!(
385				col.len(),
386				n,
387				"{ctx}: Columns column[{i}] has length {} but columns[0] has length {n}",
388				col.len(),
389			);
390		}
391		self.system.assert_invariants(n, ctx);
392	}
393}
394
395impl Columns {
396	pub fn from_rows(names: &[&str], result_rows: &[Vec<Value>]) -> Self {
397		let column_count = names.len();
398
399		let mut name_vec: Vec<Fragment> = names.iter().map(Fragment::internal).collect();
400		let mut buffers: Vec<ColumnBuffer> =
401			(0..column_count).map(|_| ColumnBuffer::none_typed(ValueType::Boolean, 0)).collect();
402
403		for row in result_rows {
404			assert_eq!(row.len(), column_count, "row length does not match column count");
405			for (i, value) in row.iter().enumerate() {
406				buffers[i].push_value(value.clone());
407			}
408		}
409
410		let _ = &mut name_vec;
411		Self {
412			system: SystemColumns::empty(),
413			columns: buffers,
414			names: name_vec,
415		}
416	}
417
418	pub fn from_encoded_bytes(shape: &RowShape, ids: &[RowNumber], bytes_slice: &[EncodedBytes]) -> Self {
419		assert_eq!(ids.len(), bytes_slice.len(), "ids length must match rows length");
420		let fields = shape.fields();
421		let row_count = bytes_slice.len();
422
423		let mut columns_vec: Vec<ColumnWithName> = Vec::with_capacity(fields.len());
424		for field in fields.iter() {
425			let mut data = ColumnBuffer::with_capacity(field.constraint.get_type(), row_count);
426			if field.constraint.get_type() == ValueType::DictionaryId
427				&& let ColumnBuffer::DictionaryId(container) = &mut data
428				&& let Some(Constraint::Dictionary(dict_id, _)) = field.constraint.constraint()
429			{
430				container.set_dictionary_id(*dict_id);
431			}
432			columns_vec.push(ColumnWithName {
433				name: Fragment::internal(&field.name),
434				data,
435			});
436		}
437
438		for encoded in bytes_slice {
439			for (i, _) in fields.iter().enumerate() {
440				push_keeping_option(&mut columns_vec[i].data, shape.get_value(encoded, i));
441			}
442		}
443
444		let row_numbers: Vec<RowNumber> = ids.to_vec();
445		let (created_at, updated_at): (Vec<DateTime>, Vec<DateTime>) = match shape.family() {
446			RowFamily::Pod => (Vec::new(), Vec::new()),
447			_ => (
448				bytes_slice.iter().map(|r| shape.created_at(r)).collect(),
449				bytes_slice.iter().map(|r| shape.updated_at(r)).collect(),
450			),
451		};
452		let time: Vec<DateTime> = bytes_slice.iter().filter_map(|r| shape.time(r)).collect();
453
454		Self::with_system(
455			columns_vec,
456			SystemColumns::new(row_numbers, Vec::new(), created_at, updated_at, time),
457		)
458	}
459}
460
461impl Columns {
462	pub fn empty() -> Self {
463		Self {
464			system: SystemColumns::empty(),
465			columns: Vec::new(),
466			names: Vec::new(),
467		}
468	}
469}
470
471impl Default for Columns {
472	fn default() -> Self {
473		Self::empty()
474	}
475}
476
477impl Columns {
478	pub fn extract_by_indices(&self, indices: &[usize]) -> Columns {
479		if indices.is_empty() {
480			return Columns::empty();
481		}
482
483		let mut new_buffers: Vec<ColumnBuffer> = Vec::with_capacity(self.columns.len());
484		for col in self.columns.iter() {
485			let mut new_data = col.empty_like(indices.len());
486			for &idx in indices {
487				push_keeping_option(&mut new_data, col.get_value(idx));
488			}
489			new_buffers.push(new_data);
490		}
491
492		Columns {
493			system: self.system.permute(indices),
494			columns: new_buffers,
495			names: self.names.clone(),
496		}
497	}
498
499	pub fn extract_row(&self, index: usize) -> Columns {
500		self.extract_by_indices(&[index])
501	}
502
503	pub fn append(&mut self, source: Columns) -> Result<()> {
504		if source.row_count() == 0 {
505			return Ok(());
506		}
507		if self.columns.is_empty() {
508			*self = source;
509			return Ok(());
510		}
511
512		self.validate_append_compatibility(&source)?;
513		self.system.extend(&source.system)?;
514		self.extend_data_columns(source.columns)?;
515		Ok(())
516	}
517
518	#[inline]
519	fn validate_append_compatibility(&self, source: &Columns) -> Result<()> {
520		if self.columns.len() != source.columns.len() {
521			return_internal_error!(
522				"Columns::append: column count mismatch (self={}, source={})",
523				self.columns.len(),
524				source.columns.len()
525			);
526		}
527		Ok(())
528	}
529
530	#[inline]
531	fn extend_data_columns(&mut self, source_columns: Vec<ColumnBuffer>) -> Result<()> {
532		let dest_cols = &mut self.columns;
533		reifydb_assertions! {
534			let dest_len = dest_cols.len();
535			let src_len = source_columns.len();
536			assert!(
537				dest_len == src_len,
538				"append extends destination columns by source index, so a source with more columns than \
539				 the destination would index dest_cols out of bounds and panic mid-append, leaving self \
540				 partially extended (dest_len={dest_len}, src_len={src_len})"
541			);
542		}
543		for (i, src_col) in source_columns.into_iter().enumerate() {
544			dest_cols[i].extend(src_col)?;
545		}
546		Ok(())
547	}
548
549	pub fn concat(batches: Vec<Columns>) -> Result<Option<Columns>> {
550		let mut iter = batches.into_iter();
551		let mut merged = match iter.next() {
552			Some(first) => first,
553			None => return Ok(None),
554		};
555		for cols in iter {
556			merged.append(cols)?;
557		}
558		if merged.row_count() == 0 {
559			return Ok(None);
560		}
561		Ok(Some(merged))
562	}
563
564	pub fn remove_row(&mut self, row_number: RowNumber) -> bool {
565		let pos = self.row_numbers().iter().position(|&r| r == row_number);
566		let Some(idx) = pos else {
567			return false;
568		};
569
570		let kept_indices: Vec<usize> = (0..self.row_count()).filter(|&i| i != idx).collect();
571		*self = self.extract_by_indices(&kept_indices);
572		true
573	}
574
575	pub fn project_by_names(&self, names: &[String]) -> Columns {
576		let mut new_names = Vec::new();
577		let mut new_buffers = Vec::new();
578
579		for name in names {
580			if let Some(pos) = self.names.iter().position(|n| n.text() == name.as_str()) {
581				new_names.push(self.names[pos].clone());
582				new_buffers.push(self.columns[pos].clone());
583			}
584		}
585
586		if new_buffers.is_empty() {
587			return Columns::empty();
588		}
589
590		Columns {
591			system: self.system.clone(),
592			columns: new_buffers,
593			names: new_names,
594		}
595	}
596
597	pub fn partition_by_keys<K: Hash + Eq + Clone>(&self, keys: &[K]) -> IndexMap<K, Columns> {
598		assert_eq!(keys.len(), self.row_count(), "keys length must match row count");
599
600		let mut key_to_indices: IndexMap<K, Vec<usize>> = IndexMap::new();
601		for (idx, key) in keys.iter().enumerate() {
602			key_to_indices.entry(key.clone()).or_default().push(idx);
603		}
604
605		key_to_indices.into_iter().map(|(key, indices)| (key, self.extract_by_indices(&indices))).collect()
606	}
607
608	pub fn from_row(row: &Row) -> Self {
609		let mut out = Columns::empty();
610		out.reset_from_row(row);
611		out
612	}
613
614	pub fn reset_from_row(&mut self, row: &Row) {
615		let field_count = row.shape.fields().len();
616
617		self.system.clear();
618		self.columns.clear();
619		self.names.clear();
620
621		self.columns.reserve(field_count);
622		self.names.reserve(field_count);
623
624		let (created_at, updated_at) = match row.shape.family() {
625			RowFamily::Pod | RowFamily::Operator => (None, None),
626			_ => (Some(row.shape.created_at(&row.encoded)), Some(row.shape.updated_at(&row.encoded))),
627		};
628
629		self.system.push(RowStamps {
630			row_number: Some(row.number),
631			partition: None,
632			created_at,
633			updated_at,
634			time: row.shape.time(&row.encoded),
635		});
636
637		for (idx, field) in row.shape.fields().iter().enumerate() {
638			let value = row.shape.get_value(&row.encoded, idx);
639
640			let column_type = if matches!(value, Value::None { .. }) {
641				field.constraint.get_type()
642			} else {
643				value.get_type()
644			};
645
646			let mut data = if column_type.is_option() {
647				ColumnBuffer::none_typed(column_type.clone(), 0)
648			} else {
649				ColumnBuffer::with_capacity(column_type.clone(), 1)
650			};
651			data.push_value(value);
652
653			if column_type == ValueType::DictionaryId
654				&& let ColumnBuffer::DictionaryId(container) = &mut data
655				&& let Some(Constraint::Dictionary(dict_id, _)) = field.constraint.constraint()
656			{
657				container.set_dictionary_id(*dict_id);
658			}
659
660			let name = row.shape.get_field_name(idx).expect("RowShape missing name for field");
661
662			self.names.push(Fragment::internal(name));
663			self.columns.push(data);
664		}
665	}
666}
667
668fn push_keeping_option(buffer: &mut ColumnBuffer, value: Value) {
669	match buffer {
670		ColumnBuffer::Option {
671			inner,
672			bitvec,
673		} if !matches!(value, Value::None { .. }) => {
674			inner.push_value(value);
675			bitvec.push(true);
676		}
677		data => data.push_value(value),
678	}
679}
680
681#[cfg(test)]
682pub mod tests {
683	use std::str::FromStr;
684
685	use reifydb_value::value::{
686		blob::Blob,
687		constraint::{bytes::MaxBytes, precision::Precision, scale::Scale},
688		date::Date,
689		datetime::DateTime,
690		decimal::Decimal,
691		dictionary::{DictionaryEntryId, DictionaryId},
692		duration::Duration,
693		identity::IdentityId,
694		int::Int,
695		time::Time,
696		uint::Uint,
697		uuid::{Uuid4, Uuid7},
698	};
699	use uuid::{Timestamp, Uuid};
700
701	use super::*;
702
703	fn uuid7_at(a: u64, b: u16) -> Uuid7 {
704		Uuid7::from(Uuid::new_v7(Timestamp::from_gregorian_time(a, b)))
705	}
706
707	/// Compares `get_value` of the extraction against the source, so it covers every `ColumnBuffer`
708	/// variant without hand-constructing each `Value`.
709	fn assert_extract_preserves_values(buffer: ColumnBuffer, indices: &[usize]) {
710		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
711		let extracted = original.extract_by_indices(indices);
712
713		assert_eq!(extracted.len(), 1, "column count must be preserved");
714		assert_eq!(extracted.row_count(), indices.len(), "row count must equal number of indices");
715
716		let src = original.data_at(0);
717		let dst = extracted.data_at(0);
718		assert_eq!(dst.get_type(), src.get_type(), "value type must be preserved");
719		for (j, &idx) in indices.iter().enumerate() {
720			assert_eq!(
721				dst.get_value(j),
722				src.get_value(idx),
723				"value at extracted row {j} must equal source row {idx}"
724			);
725		}
726	}
727
728	#[test]
729	fn extract_by_indices_preserves_bool_values() {
730		assert_extract_preserves_values(ColumnBuffer::bool([true, false, true, false]), &[3, 1, 2]);
731	}
732
733	#[test]
734	fn extract_by_indices_preserves_float4_values() {
735		assert_extract_preserves_values(ColumnBuffer::float4([1.0f32, 2.5, -3.0, 4.25]), &[3, 1, 2]);
736	}
737
738	#[test]
739	fn extract_by_indices_preserves_float8_values() {
740		assert_extract_preserves_values(ColumnBuffer::float8([1.0f64, 2.5, -3.0, 4.25]), &[3, 1, 2]);
741	}
742
743	#[test]
744	fn extract_by_indices_preserves_int1_values() {
745		assert_extract_preserves_values(ColumnBuffer::int1([-1i8, 2, -3, 4]), &[3, 1, 2]);
746	}
747
748	#[test]
749	fn extract_by_indices_preserves_int2_values() {
750		assert_extract_preserves_values(ColumnBuffer::int2([-1i16, 2, -3, 4]), &[3, 1, 2]);
751	}
752
753	#[test]
754	fn extract_by_indices_preserves_int4_values() {
755		assert_extract_preserves_values(ColumnBuffer::int4([-1i32, 2, -3, 4]), &[3, 1, 2]);
756	}
757
758	#[test]
759	fn extract_by_indices_preserves_int8_values() {
760		assert_extract_preserves_values(ColumnBuffer::int8([-1i64, 2, -3, 4]), &[3, 1, 2]);
761	}
762
763	#[test]
764	fn extract_by_indices_preserves_int16_values() {
765		assert_extract_preserves_values(ColumnBuffer::int16([-1i128, 2, -3, 4]), &[3, 1, 2]);
766	}
767
768	#[test]
769	fn extract_by_indices_preserves_uint1_values() {
770		assert_extract_preserves_values(ColumnBuffer::uint1([1u8, 2, 3, 4]), &[3, 1, 2]);
771	}
772
773	#[test]
774	fn extract_by_indices_preserves_uint2_values() {
775		assert_extract_preserves_values(ColumnBuffer::uint2([1u16, 2, 3, 4]), &[3, 1, 2]);
776	}
777
778	#[test]
779	fn extract_by_indices_preserves_uint4_values() {
780		assert_extract_preserves_values(ColumnBuffer::uint4([1u32, 2, 3, 4]), &[3, 1, 2]);
781	}
782
783	#[test]
784	fn extract_by_indices_preserves_uint8_values() {
785		assert_extract_preserves_values(ColumnBuffer::uint8([1u64, 2, 3, 4]), &[3, 1, 2]);
786	}
787
788	#[test]
789	fn extract_by_indices_preserves_uint16_values() {
790		assert_extract_preserves_values(ColumnBuffer::uint16([1u128, 2, 3, 4]), &[3, 1, 2]);
791	}
792
793	#[test]
794	fn extract_by_indices_preserves_utf8_values() {
795		assert_extract_preserves_values(ColumnBuffer::utf8(["a", "bb", "ccc", "dddd"]), &[3, 1, 2]);
796	}
797
798	#[test]
799	fn extract_by_indices_preserves_date_values() {
800		let data = [
801			Date::from_ymd(2025, 1, 1).unwrap(),
802			Date::from_ymd(2025, 6, 15).unwrap(),
803			Date::from_ymd(2024, 12, 31).unwrap(),
804			Date::from_ymd(2000, 2, 29).unwrap(),
805		];
806		assert_extract_preserves_values(ColumnBuffer::date(data), &[3, 1, 2]);
807	}
808
809	#[test]
810	fn extract_by_indices_preserves_datetime_values() {
811		let data = [
812			DateTime::from_epoch_secs(1000).unwrap(),
813			DateTime::from_epoch_secs(2000).unwrap(),
814			DateTime::from_epoch_secs(3000).unwrap(),
815			DateTime::from_epoch_secs(4000).unwrap(),
816		];
817		assert_extract_preserves_values(ColumnBuffer::datetime(data), &[3, 1, 2]);
818	}
819
820	#[test]
821	fn extract_by_indices_preserves_time_values() {
822		let data = [
823			Time::from_hms(0, 0, 0).unwrap(),
824			Time::from_hms(12, 30, 45).unwrap(),
825			Time::from_hms(23, 59, 59).unwrap(),
826			Time::from_hms(6, 15, 0).unwrap(),
827		];
828		assert_extract_preserves_values(ColumnBuffer::time(data), &[3, 1, 2]);
829	}
830
831	#[test]
832	fn extract_by_indices_preserves_duration_values() {
833		let data = [
834			Duration::from_days(1).unwrap(),
835			Duration::from_days(7).unwrap(),
836			Duration::from_days(30).unwrap(),
837			Duration::from_days(365).unwrap(),
838		];
839		assert_extract_preserves_values(ColumnBuffer::duration(data), &[3, 1, 2]);
840	}
841
842	#[test]
843	fn extract_by_indices_preserves_identity_id_values() {
844		let data = [IdentityId::root(), IdentityId::system(), IdentityId::anonymous(), IdentityId::root()];
845		assert_extract_preserves_values(ColumnBuffer::identity_id(data), &[3, 1, 2]);
846	}
847
848	#[test]
849	fn extract_by_indices_preserves_uuid4_values() {
850		let data = [Uuid4::generate(), Uuid4::generate(), Uuid4::generate(), Uuid4::generate()];
851		assert_extract_preserves_values(ColumnBuffer::uuid4(data), &[3, 1, 2]);
852	}
853
854	#[test]
855	fn extract_by_indices_preserves_uuid7_values() {
856		let data = [uuid7_at(1, 1), uuid7_at(1, 2), uuid7_at(2, 1), uuid7_at(2, 2)];
857		assert_extract_preserves_values(ColumnBuffer::uuid7(data), &[3, 1, 2]);
858	}
859
860	#[test]
861	fn extract_by_indices_preserves_blob_values() {
862		let data = [
863			Blob::new(vec![1]),
864			Blob::new(vec![2, 3]),
865			Blob::new(vec![4, 5, 6]),
866			Blob::new(vec![7, 8, 9, 10]),
867		];
868		assert_extract_preserves_values(ColumnBuffer::blob(data), &[3, 1, 2]);
869	}
870
871	#[test]
872	fn extract_by_indices_preserves_int_values() {
873		let data = [Int::from(-1i64), Int::from(2i64), Int::from(-3i64), Int::from(4i64)];
874		assert_extract_preserves_values(ColumnBuffer::int(data), &[3, 1, 2]);
875	}
876
877	#[test]
878	fn extract_by_indices_preserves_uint_values() {
879		let data = [Uint::from(1u64), Uint::from(2u64), Uint::from(3u64), Uint::from(4u64)];
880		assert_extract_preserves_values(ColumnBuffer::uint(data), &[3, 1, 2]);
881	}
882
883	#[test]
884	fn extract_by_indices_preserves_decimal_values() {
885		let data = [
886			Decimal::from_str("1.50").unwrap(),
887			Decimal::from_str("2.25").unwrap(),
888			Decimal::from_str("-3.75").unwrap(),
889			Decimal::from_str("4.00").unwrap(),
890		];
891		assert_extract_preserves_values(ColumnBuffer::decimal(data), &[3, 1, 2]);
892	}
893
894	#[test]
895	fn extract_by_indices_preserves_any_values() {
896		let data = [Value::Int4(1), Value::Utf8("two".to_string()), Value::Boolean(true), Value::none()];
897		assert_extract_preserves_values(ColumnBuffer::any(data), &[3, 1, 2]);
898	}
899
900	#[test]
901	fn extract_by_indices_preserves_dictionary_id_values() {
902		let data = [
903			DictionaryEntryId::U2(10),
904			DictionaryEntryId::U2(20),
905			DictionaryEntryId::U2(30),
906			DictionaryEntryId::U2(40),
907		];
908		assert_extract_preserves_values(ColumnBuffer::dictionary_id(data), &[3, 1, 2]);
909	}
910
911	#[test]
912	fn extract_by_indices_preserves_option_values_including_none() {
913		let mut buffer = ColumnBuffer::with_capacity(ValueType::Option(Box::new(ValueType::Int4)), 0);
914		buffer.push_value(Value::Int4(1));
915		buffer.push_value(Value::none());
916		buffer.push_value(Value::Int4(3));
917		buffer.push_value(Value::none());
918		assert_extract_preserves_values(buffer, &[3, 1, 2, 0]);
919	}
920
921	#[test]
922	fn extract_by_indices_empty_indices_yields_empty_columns() {
923		let original = Columns::new(vec![ColumnWithName::int4("c", [1, 2, 3])]);
924		let extracted = original.extract_by_indices(&[]);
925		assert_eq!(extracted.row_count(), 0);
926		assert!(extracted.is_empty());
927	}
928
929	#[test]
930	fn extract_by_indices_full_identity_reproduces_all_rows() {
931		assert_extract_preserves_values(ColumnBuffer::int4([10, 20, 30, 40]), &[0, 1, 2, 3]);
932	}
933
934	#[test]
935	fn heap_size_grows_with_row_count() {
936		let small = Columns::new(vec![ColumnWithName::int4("c", [1i32, 2, 3, 4])]);
937		let large = Columns::new(vec![ColumnWithName::int4("c", 0..4000i32)]);
938		assert!(
939			large.heap_size() > small.heap_size() + 4000,
940			"heap_size must scale with the number of buffered rows (small={}, large={})",
941			small.heap_size(),
942			large.heap_size()
943		);
944	}
945
946	#[test]
947	fn heap_size_counts_utf8_payload_not_just_row_count() {
948		// Two columns with the same row count but different string payloads must not report the same
949		// footprint; a budget ignoring varlen content lets a wide-string result blow past the cap.
950		let short = Columns::new(vec![ColumnWithName::new("c", ColumnBuffer::utf8(["a", "b", "c"]))]);
951		let long_value = "x".repeat(4096);
952		let long = Columns::new(vec![ColumnWithName::new(
953			"c",
954			ColumnBuffer::utf8([long_value.clone(), long_value.clone(), long_value.clone()]),
955		)]);
956		assert_eq!(short.row_count(), long.row_count(), "same row count is the point of the test");
957		assert!(
958			long.heap_size() >= short.heap_size() + 3 * 4096,
959			"heap_size must account for utf8 payload bytes (short={}, long={})",
960			short.heap_size(),
961			long.heap_size()
962		);
963	}
964
965	#[test]
966	fn extract_by_indices_duplicate_index_duplicates_row() {
967		let original = Columns::new(vec![ColumnWithName::int4("c", [10, 20, 30])]);
968		let extracted = original.extract_by_indices(&[1, 1, 1]);
969		assert_eq!(extracted.row_count(), 3);
970		assert_eq!(extracted.data_at(0).get_value(0), Value::Int4(20));
971		assert_eq!(extracted.data_at(0).get_value(1), Value::Int4(20));
972		assert_eq!(extracted.data_at(0).get_value(2), Value::Int4(20));
973	}
974
975	#[test]
976	fn extract_by_indices_extracts_multiple_columns_consistently() {
977		let original = Columns::new(vec![
978			ColumnWithName::int4("id", [1, 2, 3, 4]),
979			ColumnWithName::utf8(
980				"name",
981				["a".to_string(), "b".to_string(), "c".to_string(), "d".to_string()],
982			),
983			ColumnWithName::bool("flag", [true, false, true, false]),
984		]);
985		let extracted = original.extract_by_indices(&[2, 0]);
986
987		assert_eq!(extracted.len(), 3);
988		assert_eq!(extracted.row_count(), 2);
989		assert_eq!(extracted.column("id").unwrap().data().get_value(0), Value::Int4(3));
990		assert_eq!(extracted.column("id").unwrap().data().get_value(1), Value::Int4(1));
991		assert_eq!(extracted.column("name").unwrap().data().get_value(0), Value::Utf8("c".to_string()));
992		assert_eq!(extracted.column("name").unwrap().data().get_value(1), Value::Utf8("a".to_string()));
993		assert_eq!(extracted.column("flag").unwrap().data().get_value(0), Value::Boolean(true));
994		assert_eq!(extracted.column("flag").unwrap().data().get_value(1), Value::Boolean(true));
995	}
996
997	#[test]
998	fn extract_by_indices_extracts_system_columns_in_order() {
999		let columns = vec![ColumnWithName::int4("id", [10, 20, 30, 40])];
1000		let row_numbers = vec![RowNumber::from(1), RowNumber::from(2), RowNumber::from(3), RowNumber::from(4)];
1001		let created_at = vec![
1002			DateTime::from_epoch_secs(1000).unwrap(),
1003			DateTime::from_epoch_secs(2000).unwrap(),
1004			DateTime::from_epoch_secs(3000).unwrap(),
1005			DateTime::from_epoch_secs(4000).unwrap(),
1006		];
1007		let updated_at = vec![
1008			DateTime::from_epoch_secs(1100).unwrap(),
1009			DateTime::from_epoch_secs(2200).unwrap(),
1010			DateTime::from_epoch_secs(3300).unwrap(),
1011			DateTime::from_epoch_secs(4400).unwrap(),
1012		];
1013		let time = created_at.clone();
1014		let original = Columns::with_system(
1015			columns,
1016			SystemColumns::new(row_numbers, Vec::new(), created_at, updated_at, time),
1017		);
1018
1019		let extracted = original.extract_by_indices(&[3, 0]);
1020
1021		let rns: Vec<RowNumber> = extracted.row_numbers().iter().cloned().collect();
1022		assert_eq!(rns, vec![RowNumber::from(4), RowNumber::from(1)], "row_numbers must follow indices");
1023		assert_eq!(
1024			extracted.created_at().iter().cloned().collect::<Vec<_>>(),
1025			vec![DateTime::from_epoch_secs(4000).unwrap(), DateTime::from_epoch_secs(1000).unwrap()],
1026			"created_at must follow indices"
1027		);
1028		assert_eq!(
1029			extracted.updated_at().iter().cloned().collect::<Vec<_>>(),
1030			vec![DateTime::from_epoch_secs(4400).unwrap(), DateTime::from_epoch_secs(1100).unwrap()],
1031			"updated_at must follow indices"
1032		);
1033	}
1034
1035	/// Regression: the change accumulator coalesces row-keyed inserts by calling `extract_row`
1036	/// per row, and a deferred view over a dictionary-encoded column then decodes using the
1037	/// buffer's `dictionary_id`. If extraction drops that metadata the view can no longer resolve
1038	/// the dictionary and inserts are silently lost. This pins that `extract_by_indices` carries
1039	/// the `dictionary_id` through.
1040	#[test]
1041	fn extract_by_indices_preserves_dictionary_id_metadata() {
1042		let mut buffer = ColumnBuffer::dictionary_id([
1043			DictionaryEntryId::U2(10),
1044			DictionaryEntryId::U2(20),
1045			DictionaryEntryId::U2(30),
1046		]);
1047		match &mut buffer {
1048			ColumnBuffer::DictionaryId(container) => container.set_dictionary_id(DictionaryId(42)),
1049			_ => unreachable!("dictionary_id factory must build a DictionaryId buffer"),
1050		}
1051
1052		let original = Columns::new(vec![ColumnWithName::new("token", buffer)]);
1053		let extracted = original.extract_by_indices(&[2, 0]);
1054
1055		match extracted.data_at(0) {
1056			ColumnBuffer::DictionaryId(container) => {
1057				assert_eq!(
1058					container.dictionary_id(),
1059					Some(DictionaryId(42)),
1060					"dictionary_id metadata must survive extraction"
1061				);
1062			}
1063			other => panic!("expected DictionaryId buffer, got {:?}", other.get_type()),
1064		}
1065	}
1066
1067	#[test]
1068	fn extract_by_indices_preserves_utf8_max_bytes_metadata() {
1069		let mut buffer = ColumnBuffer::utf8(["a", "bb", "ccc"]);
1070		match &mut buffer {
1071			ColumnBuffer::Utf8 {
1072				max_bytes,
1073				..
1074			} => *max_bytes = MaxBytes::new(255),
1075			_ => unreachable!(),
1076		}
1077
1078		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
1079		let extracted = original.extract_by_indices(&[2, 0]);
1080
1081		match extracted.data_at(0) {
1082			ColumnBuffer::Utf8 {
1083				max_bytes,
1084				..
1085			} => assert_eq!(*max_bytes, MaxBytes::new(255), "Utf8 max_bytes must survive extraction"),
1086			other => panic!("expected Utf8 buffer, got {:?}", other.get_type()),
1087		}
1088	}
1089
1090	#[test]
1091	fn extract_by_indices_preserves_blob_max_bytes_metadata() {
1092		let mut buffer = ColumnBuffer::blob([Blob::new(vec![1]), Blob::new(vec![2, 3]), Blob::new(vec![4])]);
1093		match &mut buffer {
1094			ColumnBuffer::Blob {
1095				max_bytes,
1096				..
1097			} => *max_bytes = MaxBytes::new(1024),
1098			_ => unreachable!(),
1099		}
1100
1101		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
1102		let extracted = original.extract_by_indices(&[2, 0]);
1103
1104		match extracted.data_at(0) {
1105			ColumnBuffer::Blob {
1106				max_bytes,
1107				..
1108			} => assert_eq!(*max_bytes, MaxBytes::new(1024), "Blob max_bytes must survive extraction"),
1109			other => panic!("expected Blob buffer, got {:?}", other.get_type()),
1110		}
1111	}
1112
1113	#[test]
1114	fn extract_by_indices_preserves_int_max_bytes_metadata() {
1115		let mut buffer = ColumnBuffer::int([Int::from(1i64), Int::from(2i64), Int::from(3i64)]);
1116		match &mut buffer {
1117			ColumnBuffer::Int {
1118				max_bytes,
1119				..
1120			} => *max_bytes = MaxBytes::new(16),
1121			_ => unreachable!(),
1122		}
1123
1124		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
1125		let extracted = original.extract_by_indices(&[2, 0]);
1126
1127		match extracted.data_at(0) {
1128			ColumnBuffer::Int {
1129				max_bytes,
1130				..
1131			} => assert_eq!(*max_bytes, MaxBytes::new(16), "Int max_bytes must survive extraction"),
1132			other => panic!("expected Int buffer, got {:?}", other.get_type()),
1133		}
1134	}
1135
1136	#[test]
1137	fn extract_by_indices_preserves_uint_max_bytes_metadata() {
1138		let mut buffer = ColumnBuffer::uint([Uint::from(1u64), Uint::from(2u64), Uint::from(3u64)]);
1139		match &mut buffer {
1140			ColumnBuffer::Uint {
1141				max_bytes,
1142				..
1143			} => *max_bytes = MaxBytes::new(8),
1144			_ => unreachable!(),
1145		}
1146
1147		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
1148		let extracted = original.extract_by_indices(&[2, 0]);
1149
1150		match extracted.data_at(0) {
1151			ColumnBuffer::Uint {
1152				max_bytes,
1153				..
1154			} => assert_eq!(*max_bytes, MaxBytes::new(8), "Uint max_bytes must survive extraction"),
1155			other => panic!("expected Uint buffer, got {:?}", other.get_type()),
1156		}
1157	}
1158
1159	#[test]
1160	fn extract_by_indices_preserves_decimal_precision_and_scale_metadata() {
1161		let mut buffer = ColumnBuffer::decimal([
1162			Decimal::from_str("1.50").unwrap(),
1163			Decimal::from_str("2.25").unwrap(),
1164			Decimal::from_str("3.75").unwrap(),
1165		]);
1166		match &mut buffer {
1167			ColumnBuffer::Decimal {
1168				precision,
1169				scale,
1170				..
1171			} => {
1172				*precision = Precision::new(10);
1173				*scale = Scale::new(2);
1174			}
1175			_ => unreachable!(),
1176		}
1177
1178		let original = Columns::new(vec![ColumnWithName::new("c", buffer)]);
1179		let extracted = original.extract_by_indices(&[2, 0]);
1180
1181		match extracted.data_at(0) {
1182			ColumnBuffer::Decimal {
1183				precision,
1184				scale,
1185				..
1186			} => {
1187				assert_eq!(*precision, Precision::new(10), "Decimal precision must survive extraction");
1188				assert_eq!(*scale, Scale::new(2), "Decimal scale must survive extraction");
1189			}
1190			other => panic!("expected Decimal buffer, got {:?}", other.get_type()),
1191		}
1192	}
1193
1194	#[test]
1195	fn test_single_row_temporal_types() {
1196		let date = Date::from_ymd(2025, 1, 15).unwrap();
1197		let datetime = DateTime::from_epoch_secs(1642694400).unwrap();
1198		let time = Time::from_hms(14, 30, 45).unwrap();
1199		let duration = Duration::from_days(30).unwrap();
1200
1201		let columns = Columns::single_row([
1202			("date_col", Value::Date(date.clone())),
1203			("datetime_col", Value::DateTime(datetime.clone())),
1204			("time_col", Value::Time(time.clone())),
1205			("interval_col", Value::Duration(duration.clone())),
1206		]);
1207
1208		assert_eq!(columns.len(), 4);
1209		assert_eq!(columns.shape(), (1, 4));
1210
1211		assert_eq!(columns.column("date_col").unwrap().data().get_value(0), Value::Date(date));
1212		assert_eq!(columns.column("datetime_col").unwrap().data().get_value(0), Value::DateTime(datetime));
1213		assert_eq!(columns.column("time_col").unwrap().data().get_value(0), Value::Time(time));
1214		assert_eq!(columns.column("interval_col").unwrap().data().get_value(0), Value::Duration(duration));
1215	}
1216
1217	#[test]
1218	fn test_single_row_mixed_types() {
1219		let date = Date::from_ymd(2025, 7, 15).unwrap();
1220		let time = Time::from_hms(9, 15, 30).unwrap();
1221
1222		let columns = Columns::single_row([
1223			("bool_col", Value::Boolean(true)),
1224			("int_col", Value::Int4(42)),
1225			("str_col", Value::Utf8("hello".to_string())),
1226			("date_col", Value::Date(date.clone())),
1227			("time_col", Value::Time(time.clone())),
1228			("none_col", Value::none()),
1229		]);
1230
1231		assert_eq!(columns.len(), 6);
1232		assert_eq!(columns.shape(), (1, 6));
1233
1234		assert_eq!(columns.column("bool_col").unwrap().data().get_value(0), Value::Boolean(true));
1235		assert_eq!(columns.column("int_col").unwrap().data().get_value(0), Value::Int4(42));
1236		assert_eq!(columns.column("str_col").unwrap().data().get_value(0), Value::Utf8("hello".to_string()));
1237		assert_eq!(columns.column("date_col").unwrap().data().get_value(0), Value::Date(date));
1238		assert_eq!(columns.column("time_col").unwrap().data().get_value(0), Value::Time(time));
1239		assert_eq!(columns.column("none_col").unwrap().data().get_value(0), Value::none());
1240	}
1241
1242	#[test]
1243	fn test_single_row_none_of_int4_is_int4_typed() {
1244		// value_to_buffer must keep the `inner` type a `Value::None` carries; forcing one hardcoded
1245		// column type would silently mistype every all-none column.
1246		let columns = Columns::single_row([("n", Value::none_of(ValueType::Int4))]);
1247		match columns.column("n").unwrap().data().get_value(0) {
1248			Value::None {
1249				inner,
1250			} => assert_eq!(inner, ValueType::Int4),
1251			other => panic!("expected Value::None, got {other:?}"),
1252		}
1253	}
1254
1255	#[test]
1256	fn test_single_row_none_of_utf8_is_utf8_typed() {
1257		let columns = Columns::single_row([("n", Value::none_of(ValueType::Utf8))]);
1258		match columns.column("n").unwrap().data().get_value(0) {
1259			Value::None {
1260				inner,
1261			} => assert_eq!(inner, ValueType::Utf8),
1262			other => panic!("expected Value::None, got {other:?}"),
1263		}
1264	}
1265
1266	#[test]
1267	fn test_single_row_bare_none_is_any_typed() {
1268		let columns = Columns::single_row([("n", Value::none())]);
1269		match columns.column("n").unwrap().data().get_value(0) {
1270			Value::None {
1271				inner,
1272			} => assert_eq!(inner, ValueType::Any),
1273			other => panic!("expected Value::None, got {other:?}"),
1274		}
1275	}
1276
1277	#[test]
1278	fn test_single_row_none_of_nested_option_collapses_to_base_type() {
1279		// none_typed unwraps a nested Option(inner) to its base type, so an Option<Option<Duration>>
1280		// none lands in a Duration-typed column.
1281		let inner_ty = ValueType::Option(Box::new(ValueType::Duration));
1282		let columns = Columns::single_row([("n", Value::none_of(inner_ty))]);
1283		match columns.column("n").unwrap().data().get_value(0) {
1284			Value::None {
1285				inner,
1286			} => assert_eq!(inner, ValueType::Duration),
1287			other => panic!("expected Value::None, got {other:?}"),
1288		}
1289	}
1290
1291	#[test]
1292	fn test_single_row_none_of_boolean_is_boolean_typed() {
1293		// Boolean is value_to_buffer's fallback type, so this case alone proves nothing; it is kept
1294		// for symmetry with the other inner types above.
1295		let columns = Columns::single_row([("n", Value::none_of(ValueType::Boolean))]);
1296		match columns.column("n").unwrap().data().get_value(0) {
1297			Value::None {
1298				inner,
1299			} => assert_eq!(inner, ValueType::Boolean),
1300			other => panic!("expected Value::None, got {other:?}"),
1301		}
1302	}
1303
1304	#[test]
1305	fn test_single_row_normal_column_names_work() {
1306		let columns = Columns::single_row([("normal_column", Value::Int4(42))]);
1307		assert_eq!(columns.len(), 1);
1308		assert_eq!(columns.column("normal_column").unwrap().data().get_value(0), Value::Int4(42));
1309	}
1310
1311	#[test]
1312	fn with_row_numbers_leaves_an_absent_sidecar_absent() {
1313		// A timeless batch must stay timeless; a filled #time reads downstream as a real time zero.
1314		let columns = Columns::new(vec![ColumnWithName::new("v", ColumnBuffer::int4([1, 2, 3]))])
1315			.with_row_numbers(vec![RowNumber(1), RowNumber(2), RowNumber(3)]);
1316
1317		assert_eq!(columns.system.row_numbers().len(), 3);
1318		assert!(columns.system.time().is_empty(), "#time must stay absent");
1319		assert!(columns.system.created_at().is_empty(), "created_at must stay absent");
1320		assert!(columns.system.updated_at().is_empty(), "updated_at must stay absent");
1321	}
1322
1323	#[test]
1324	fn with_row_numbers_keeps_a_populated_sidecar() {
1325		let stamps = vec![DateTime::from_nanos(10), DateTime::from_nanos(20)];
1326		let columns = Columns::with_system(
1327			vec![ColumnWithName::new("v", ColumnBuffer::int4([1, 2]))],
1328			SystemColumns::new(
1329				vec![RowNumber(7), RowNumber(8)],
1330				Vec::new(),
1331				Vec::new(),
1332				Vec::new(),
1333				stamps.clone(),
1334			),
1335		)
1336		.with_row_numbers(vec![RowNumber(1), RowNumber(2)]);
1337
1338		assert_eq!(columns.system.time(), stamps.as_slice());
1339		assert_eq!(columns.system.row_numbers(), &[RowNumber(1), RowNumber(2)]);
1340	}
1341
1342	#[test]
1343	#[should_panic(expected = "Columns::with_row_numbers")]
1344	fn with_row_numbers_panics_on_a_partial_sidecar() {
1345		// A sidecar that covers only some rows must stop the write, never be padded to fit.
1346		let columns = Columns::with_system(
1347			vec![ColumnWithName::new("v", ColumnBuffer::int4([1, 2, 3]))],
1348			SystemColumns::new(
1349				vec![RowNumber(1), RowNumber(2), RowNumber(3)],
1350				Vec::new(),
1351				Vec::new(),
1352				Vec::new(),
1353				vec![DateTime::from_nanos(10), DateTime::from_nanos(20), DateTime::from_nanos(30)],
1354			),
1355		);
1356
1357		let _ = columns.with_row_numbers(vec![RowNumber(1), RowNumber(2)]);
1358	}
1359}