Skip to main content

reifydb_engine/bulk_insert/
validation.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{iter, sync::Arc};
5
6use arrow_array::{ArrayRef, RecordBatch};
7use arrow_schema::{FieldRef, Schema};
8use reifydb_core::{
9	interface::catalog::{column::Column, sumtype::SumType},
10	internal_error,
11	value::{batch::batch_with, column::builder::ColumnBuilder},
12};
13use reifydb_value::{
14	error::Error,
15	fragment::Fragment,
16	params::Params,
17	value::{
18		Value,
19		column_view::{ColumnView, ViewData},
20		system_columns::{column_view, is_system_field},
21	},
22};
23
24use super::coerce::RowCoercer;
25use crate::{Result, error::EngineError, vm::instruction::dml::series_insert::VariantTags};
26
27const TAG: &str = "tag";
28
29struct Segment<'a> {
30	columns: &'a [Column],
31	tagged: bool,
32	source_name: &'a str,
33	coercer: &'a RowCoercer,
34}
35
36pub type CoercedTags = (Vec<Option<u8>>, Option<(usize, Error)>);
37
38pub(super) fn coerce_columns(
39	rows: &[Params],
40	batches: &[(usize, RecordBatch)],
41	columns: &[Column],
42	sumtype: Option<&SumType>,
43	source_name: &str,
44	coercer: &RowCoercer,
45) -> Result<(Vec<(FieldRef, ArrayRef)>, Option<CoercedTags>)> {
46	let segment = Segment {
47		columns,
48		tagged: sumtype.is_some(),
49		source_name,
50		coercer,
51	};
52	let total = rows.len() + batches.iter().map(|(_, batch)| batch.num_rows()).sum::<usize>();
53	let mut builders: Vec<ColumnBuilder> =
54		columns.iter().map(|col| ColumnBuilder::with_capacity(col.constraint.get_type(), total)).collect();
55	let mut tags = sumtype.map(|sumtype| VariantTags::new(sumtype, total));
56	let mut taken = 0;
57	let mut first_row = 0;
58	for (at, batch) in batches {
59		first_row +=
60			collect_rows_to_columns(&segment, &rows[taken..*at], first_row, &mut builders, tags.as_mut())?;
61		taken = *at;
62		first_row += coerce_batch(&segment, batch, first_row, &mut builders, tags.as_mut())?;
63	}
64	collect_rows_to_columns(&segment, &rows[taken..], first_row, &mut builders, tags.as_mut())?;
65	let columns = builders.into_iter().zip(columns).map(|(builder, column)| builder.finish(&column.name)).collect();
66	Ok((columns, tags.map(VariantTags::finish)))
67}
68
69fn coerce_batch(
70	segment: &Segment<'_>,
71	batch: &RecordBatch,
72	first_row: usize,
73	builders: &mut [ColumnBuilder],
74	tags: Option<&mut VariantTags<'_>>,
75) -> Result<usize> {
76	let rows = batch.num_rows();
77	if rows == 0 {
78		return Ok(0);
79	}
80
81	let views = segment.columns.iter().map(|col| column_view(batch, &col.name)).collect::<Result<Vec<_>>>()?;
82	let mut failure: Option<(usize, Error)> = None;
83	for ((builder, column), view) in builders.iter_mut().zip(segment.columns).zip(&views) {
84		let limit = failure.as_ref().map_or(rows, |(row, _)| *row);
85		let Some(view) = view else {
86			for _ in 0..rows {
87				builder.push_none();
88			}
89			continue;
90		};
91		let per_cell =
92			matches!(view.data, ViewData::Any { .. } | ViewData::Digest { .. } | ViewData::None { .. });
93		if per_cell {
94			if let Some(found) = coerce_cells(segment, column, view, first_row, limit, builder) {
95				failure = Some(found);
96			}
97			continue;
98		}
99		if view.base_type() == *column.constraint.get_type().inner_type() {
100			builder.append_values(view)?;
101			continue;
102		}
103		match segment.coercer.cast_column(view, column) {
104			Ok(cast) => builder.append_values(&ColumnView::try_from(&cast)?)?,
105			Err(_) => match coerce_cells(segment, column, view, first_row, limit, builder) {
106				Some(found) => failure = Some(found),
107				None if failure.is_some() => {}
108				None => {
109					return Err(internal_error!(
110						"bulk column {} failed its column cast where every cell cast passes",
111						column.name
112					));
113				}
114			},
115		}
116	}
117	if let Some((_, error)) = failure {
118		return Err(error);
119	}
120
121	if let Some(field) = batch.schema().fields().iter().find(|field| {
122		is_system_field(field)
123			|| !(segment.columns.iter().any(|c| &c.name == field.name())
124				|| (segment.tagged && field.name() == TAG))
125	}) {
126		return Err(EngineError::BulkInsertColumnNotFound {
127			fragment: Fragment::None,
128			table_name: segment.source_name.to_string(),
129			column: field.name().to_string(),
130		}
131		.into());
132	}
133
134	if let Some(tags) = tags {
135		match column_view(batch, TAG)? {
136			Some(view) => {
137				for row in 0..rows {
138					tags.push(view.get_value(row));
139				}
140			}
141			None => {
142				for _ in 0..rows {
143					tags.push(Value::none());
144				}
145			}
146		}
147	}
148	Ok(rows)
149}
150
151fn coerce_cells(
152	segment: &Segment<'_>,
153	column: &Column,
154	view: &ColumnView<'_>,
155	first_row: usize,
156	limit: usize,
157	builder: &mut ColumnBuilder,
158) -> Option<(usize, Error)> {
159	for row in 0..limit {
160		match segment.coercer.coerce(view.get_value(row), column, segment.source_name, first_row + row) {
161			Ok(value) => builder.push_value(value),
162			Err(error) => return Some((row, error)),
163		}
164	}
165	None
166}
167
168fn collect_rows_to_columns(
169	segment: &Segment<'_>,
170	rows: &[Params],
171	first_row: usize,
172	builders: &mut [ColumnBuilder],
173	tags: Option<&mut VariantTags<'_>>,
174) -> Result<usize> {
175	let columns = segment.columns;
176	let source_name = segment.source_name;
177	let num_cols = columns.len();
178	let too_many =
179		rows.iter().position(|params| matches!(params, Params::Positional(vals) if vals.len() > num_cols));
180	let accepted = &rows[..too_many.unwrap_or(rows.len())];
181	match params_batch(segment, accepted)? {
182		Some(batch) => {
183			coerce_batch(segment, &batch, first_row, builders, None)?;
184		}
185		None => coerce_rows(segment, accepted, first_row, builders)?,
186	}
187	if let Some(Params::Positional(vals)) = too_many.map(|row| &rows[row]) {
188		return Err(EngineError::BulkInsertTooManyValues {
189			fragment: Fragment::None,
190			expected: num_cols,
191			actual: vals.len(),
192		}
193		.into());
194	}
195
196	for params in rows {
197		if let Params::Named(map) = params {
198			for name in map.keys() {
199				if !(columns.iter().any(|c| &c.name == name) || (segment.tagged && name == TAG)) {
200					return Err(EngineError::BulkInsertColumnNotFound {
201						fragment: Fragment::None,
202						table_name: source_name.to_string(),
203						column: name.to_string(),
204					}
205					.into());
206				}
207			}
208		}
209	}
210
211	if let Some(tags) = tags {
212		for params in rows {
213			tags.push(match params {
214				Params::Named(map) => map.get(TAG).cloned().unwrap_or(Value::none()),
215				_ => Value::none(),
216			});
217		}
218	}
219
220	Ok(rows.len())
221}
222
223fn params_batch(segment: &Segment<'_>, rows: &[Params]) -> Result<Option<RecordBatch>> {
224	let cell = |params: &Params, index: usize, name: &str| match params {
225		Params::Named(map) => map.get(name).cloned(),
226		Params::Positional(vals) => vals.get(index).cloned(),
227		Params::None => None,
228	};
229	let mut fields = Vec::with_capacity(segment.columns.len());
230	for (index, column) in segment.columns.iter().enumerate() {
231		match params_column(&column.name, rows.iter().map(|params| cell(params, index, &column.name)).collect())
232		{
233			ParamsColumn::Typed(field) => fields.push(field),
234			ParamsColumn::Absent => {}
235			ParamsColumn::Mixed => return Ok(None),
236		}
237	}
238	batch_with(&Arc::new(Schema::empty()), fields, rows.len()).map(Some)
239}
240
241enum ParamsColumn {
242	Typed((FieldRef, ArrayRef)),
243	Absent,
244	Mixed,
245}
246
247fn params_column(name: &str, values: Vec<Option<Value>>) -> ParamsColumn {
248	let present = |value: &&Option<Value>| matches!(value, Some(value) if !matches!(value, Value::None { .. }));
249	let mut types = values.iter().filter(present).flatten().map(Value::get_type);
250	let Some(first) = types.next() else {
251		return ParamsColumn::Absent;
252	};
253	if !types.all(|other| other == first) {
254		return ParamsColumn::Mixed;
255	}
256	let mut builder = ColumnBuilder::with_capacity(first, values.len());
257	for value in values {
258		match value {
259			Some(value) if !matches!(value, Value::None { .. }) => builder.push_value(value),
260			_ => builder.push_none(),
261		}
262	}
263	ParamsColumn::Typed(builder.finish(name))
264}
265
266fn coerce_rows(segment: &Segment<'_>, rows: &[Params], first_row: usize, builders: &mut [ColumnBuilder]) -> Result<()> {
267	let columns = segment.columns;
268	let source_name = segment.source_name;
269	let coercer = segment.coercer;
270
271	for (row_idx, params) in rows.iter().enumerate() {
272		match params {
273			Params::Named(map) => {
274				for (col_idx, col) in columns.iter().enumerate() {
275					let value = map.get(&col.name).cloned().unwrap_or(Value::none());
276					builders[col_idx].push_value(coercer.coerce(
277						value,
278						col,
279						source_name,
280						first_row + row_idx,
281					)?);
282				}
283			}
284			Params::Positional(vals) => {
285				for ((col_data, col), val) in builders
286					.iter_mut()
287					.zip(columns.iter())
288					.zip(vals.iter().map(Some).chain(iter::repeat(None)))
289				{
290					let value = val.cloned().unwrap_or(Value::none());
291					col_data.push_value(coercer.coerce(
292						value,
293						col,
294						source_name,
295						first_row + row_idx,
296					)?);
297				}
298			}
299			Params::None => {
300				for col_data in builders.iter_mut() {
301					col_data.push_none();
302				}
303			}
304		}
305	}
306	Ok(())
307}