Skip to main content

reifydb_engine/bulk_insert/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, marker::PhantomData, sync::Arc};
5
6use arrow_array::ArrayRef;
7use arrow_schema::FieldRef;
8use reifydb_catalog::{
9	catalog::Catalog,
10	error::{CatalogError, CatalogObjectKind},
11};
12use reifydb_codec::row::{
13	bytes::{EncodedBytes, RowBuilder, SourceRowBuilder},
14	pod::EncodedPodRow,
15	shape::RowShape,
16};
17use reifydb_core::{
18	common::TimeSource,
19	delta::Delta,
20	durability::Durability,
21	interface::catalog::{
22		column::Column,
23		namespace::Namespace,
24		object::ObjectId,
25		policy::{DataOp, PolicyTargetType},
26		ringbuffer::RingBuffer,
27		series::{Series, SeriesPartitionMetadata, decode_series_partition_metadata},
28		storage::StorageId,
29		table::Table,
30	},
31	internal_error,
32	key::{any::TaggedKey, series::SeriesPartitionMetadataKey},
33	value::batch::batch,
34};
35use reifydb_evaluate::stack::SymbolTable;
36use reifydb_transaction::{
37	multi::transaction::write::Rewrite,
38	transaction::{Transaction, command::CommandTransaction},
39};
40use reifydb_value::{
41	error::Error,
42	fragment::Fragment,
43	value::{
44		Value, column_view::ColumnView, constraint::Constraint, datetime::DateTime, identity::IdentityId,
45		partition::Partition, value_type::ValueType,
46	},
47};
48
49use super::{
50	BulkInsertResult, RingBufferInsertResult, SeriesInsertResult, TableInsertResult,
51	coerce::{RowCoercer, with_row_note},
52	validation::{CoercedTags, coerce_columns},
53};
54use crate::{
55	Result,
56	bulk_insert::storage::{
57		ringbuffer::{PendingRingBufferInsert, RingBufferInsertBuilder},
58		series::{PendingSeriesInsert, SeriesInsertBuilder},
59		table::{PendingTableInsert, TableInsertBuilder},
60	},
61	engine::StandardEngine,
62	error::EngineError,
63	flow_sync::{load_flow_diff_need, sync_transactional_views},
64	partition::resolve_partitions,
65	policy::PolicyEvaluator,
66	transaction::operation::series::{SeriesRows, write_series_rows},
67	vm::{
68		instruction::dml::{
69			coerce::key_out_of_range,
70			columns::{CastColumns, fill_sequences, intern_dictionary_columns},
71			partition::{ringbuffer_batch_partitions, save_all_partition_metadata},
72			primary_key::{Violation, get_primary_key},
73			ringbuffer_insert::insert_ringbuffer_chunks,
74			series_insert::{assign_series_keys, series_batch_partitions, write_inserted_series_metadata},
75			shape::{
76				get_or_create_ringbuffer_shape, get_or_create_series_shape, get_or_create_table_shape,
77			},
78			table_insert::{EncodedTableRows, table_batch_partitions, write_table_rows},
79			time::{RowTime, populator_index},
80		},
81		services::Services,
82	},
83};
84
85pub trait ValidationMode: sealed::Sealed + 'static {
86	const VALIDATED: bool;
87
88	fn run<F, R>(txn: &mut CommandTransaction, total_rows: usize, body: F) -> Result<R>
89	where
90		F: FnOnce(&mut CommandTransaction) -> Result<R>;
91}
92
93pub struct Validated;
94impl ValidationMode for Validated {
95	const VALIDATED: bool = true;
96
97	fn run<F, R>(txn: &mut CommandTransaction, total_rows: usize, body: F) -> Result<R>
98	where
99		F: FnOnce(&mut CommandTransaction) -> Result<R>,
100	{
101		run_checked(txn, total_rows, body)
102	}
103}
104
105pub struct Unchecked;
106impl ValidationMode for Unchecked {
107	const VALIDATED: bool = false;
108
109	fn run<F, R>(txn: &mut CommandTransaction, _total_rows: usize, body: F) -> Result<R>
110	where
111		F: FnOnce(&mut CommandTransaction) -> Result<R>,
112	{
113		txn.execute_bulk_unchecked(body)
114	}
115}
116
117fn run_checked<F, R>(txn: &mut CommandTransaction, total_rows: usize, body: F) -> Result<R>
118where
119	F: FnOnce(&mut CommandTransaction) -> Result<R>,
120{
121	if total_rows > 0 {
122		txn.reserve_writes(total_rows.saturating_mul(2))?;
123	}
124	let r = body(txn)?;
125	txn.commit()?;
126	Ok(r)
127}
128
129pub mod sealed {
130
131	use super::{Unchecked, Validated};
132	pub trait Sealed {}
133	impl Sealed for Validated {}
134	impl Sealed for Unchecked {}
135}
136
137pub struct BulkInsertBuilder<'e, V: ValidationMode = Validated> {
138	engine: &'e StandardEngine,
139	identity: IdentityId,
140	pending_tables: Vec<PendingTableInsert>,
141	pending_ringbuffers: Vec<PendingRingBufferInsert>,
142	pending_series: Vec<PendingSeriesInsert>,
143	_validation: PhantomData<V>,
144}
145
146impl<'e> BulkInsertBuilder<'e, Validated> {
147	pub(crate) fn new(engine: &'e StandardEngine, identity: IdentityId) -> Self {
148		Self {
149			engine,
150			identity,
151			pending_tables: Vec::new(),
152			pending_ringbuffers: Vec::new(),
153			pending_series: Vec::new(),
154			_validation: PhantomData,
155		}
156	}
157}
158
159impl<'e> BulkInsertBuilder<'e, Unchecked> {
160	pub(crate) fn new_unchecked(engine: &'e StandardEngine, identity: IdentityId) -> Self {
161		Self {
162			engine,
163			identity,
164			pending_tables: Vec::new(),
165			pending_ringbuffers: Vec::new(),
166			pending_series: Vec::new(),
167			_validation: PhantomData,
168		}
169	}
170}
171
172impl<'e, V: ValidationMode> BulkInsertBuilder<'e, V> {
173	pub fn table<'a>(&'a mut self, qualified_name: &str) -> TableInsertBuilder<'a, 'e, V> {
174		let (namespace, table) = parse_qualified_name(qualified_name);
175		TableInsertBuilder::new(self, namespace, table)
176	}
177
178	pub fn ringbuffer<'a>(&'a mut self, qualified_name: &str) -> RingBufferInsertBuilder<'a, 'e, V> {
179		let (namespace, ringbuffer) = parse_qualified_name(qualified_name);
180		RingBufferInsertBuilder::new(self, namespace, ringbuffer)
181	}
182
183	pub fn series<'a>(&'a mut self, qualified_name: &str) -> SeriesInsertBuilder<'a, 'e, V> {
184		let (namespace, series) = parse_qualified_name(qualified_name);
185		SeriesInsertBuilder::new(self, namespace, series)
186	}
187
188	pub(super) fn add_table_insert(&mut self, pending: PendingTableInsert) {
189		self.pending_tables.push(pending);
190	}
191
192	pub(super) fn add_ringbuffer_insert(&mut self, pending: PendingRingBufferInsert) {
193		self.pending_ringbuffers.push(pending);
194	}
195
196	pub(super) fn add_series_insert(&mut self, pending: PendingSeriesInsert) {
197		self.pending_series.push(pending);
198	}
199
200	pub fn execute(self) -> Result<BulkInsertResult> {
201		self.run()
202	}
203
204	fn run(self) -> Result<BulkInsertResult> {
205		self.engine.reject_if_read_only()?;
206		let mut txn = self.engine.begin_command(self.identity, Durability::Async)?;
207		let catalog = self.engine.catalog();
208		let services = self.engine.services();
209		let clock = self.engine.clock();
210		let total_rows = self.total_pending_rows();
211		let pending_tables = self.pending_tables;
212		let pending_ringbuffers = self.pending_ringbuffers;
213		let pending_series = self.pending_series;
214		let identity = self.identity;
215		let checked_commit =
216			!pending_ringbuffers.is_empty() || has_keyed_table(&catalog, &mut txn, &pending_tables)?;
217
218		let body = move |txn: &mut CommandTransaction| {
219			let writer = Writer {
220				catalog: &catalog,
221				services: &services,
222				now: clock.now(),
223				coercer: RowCoercer::new(identity),
224			};
225			load_flow_diff_need(writer.catalog, &mut Transaction::Command(txn))?;
226			let result = run_all_pending::<V>(
227				&writer,
228				txn,
229				pending_tables,
230				pending_ringbuffers,
231				pending_series,
232			)?;
233			sync_transactional_views(writer.services, Transaction::Command(txn))?;
234			Ok(result)
235		};
236		if checked_commit {
237			run_checked(&mut txn, total_rows, body)
238		} else {
239			V::run(&mut txn, total_rows, body)
240		}
241	}
242
243	#[inline]
244	fn total_pending_rows(&self) -> usize {
245		self.pending_tables.iter().map(|p| p.row_count()).sum::<usize>()
246			+ self.pending_ringbuffers.iter().map(|p| p.row_count()).sum::<usize>()
247			+ self.pending_series.iter().map(|p| p.row_count()).sum::<usize>()
248	}
249}
250
251struct Writer<'a> {
252	catalog: &'a Catalog,
253	services: &'a Arc<Services>,
254	now: DateTime,
255	coercer: RowCoercer,
256}
257
258#[inline]
259fn run_all_pending<V: ValidationMode>(
260	writer: &Writer<'_>,
261	txn: &mut CommandTransaction,
262	pending_tables: Vec<PendingTableInsert>,
263	pending_ringbuffers: Vec<PendingRingBufferInsert>,
264	pending_series: Vec<PendingSeriesInsert>,
265) -> Result<BulkInsertResult> {
266	let mut result = BulkInsertResult::default();
267	for pending in pending_tables {
268		result.tables.push(execute_table_insert::<V>(writer, txn, &pending)?);
269	}
270	for pending in pending_ringbuffers {
271		result.ringbuffers.push(execute_ringbuffer_insert::<V>(writer, txn, &pending)?);
272	}
273	for pending in pending_series {
274		result.series.push(execute_series_insert::<V>(writer, txn, &pending)?);
275	}
276	Ok(result)
277}
278
279struct Checks<'a> {
280	name: &'a str,
281	columns: &'a [Column],
282	filled: &'a [Vec<usize>],
283	series: Option<&'a Series>,
284	time: &'a TimeSource,
285}
286
287struct Checked {
288	keys: Vec<u64>,
289	tags: Vec<Option<u8>>,
290}
291
292fn check_rows<V: ValidationMode>(
293	checks: &Checks<'_>,
294	columns: &[(FieldRef, ArrayRef)],
295	tags: Option<CoercedTags>,
296	rows: usize,
297) -> Result<Checked> {
298	let views = columns.iter().map(ColumnView::try_from).collect::<Result<Vec<_>>>()?;
299	let mut failure: Option<(usize, Error)> = None;
300	let limit = |failure: &Option<(usize, Error)>| failure.as_ref().map_or(rows, |(row, _)| *row);
301
302	if V::VALIDATED {
303		for ((column, view), filled) in checks.columns.iter().zip(&views).zip(checks.filled) {
304			if let Some(row) = first_constraint_failure(column, view, filled, limit(&failure)) {
305				failure = Some((row, constraint_error(column, view.get_value(row), checks.name, row)));
306			}
307		}
308	}
309
310	let mut keys = Vec::new();
311	if let Some(series) = checks.series {
312		let index = key_column_index(series)?;
313		let (column, view) = (&checks.columns[index], &views[index]);
314		let given = series.key.keys_to_u64(view);
315		let dictionary_key = column.dictionary_id.is_some();
316		for (row, key) in given.iter().enumerate().take(limit(&failure)) {
317			if view.none_at(row) {
318				failure = Some((row, constraint_error(column, view.get_value(row), checks.name, row)));
319				break;
320			}
321			if dictionary_key || key.is_none() {
322				failure = Some((row, key_out_of_range(&series.key, &view.get_value(row))));
323				break;
324			}
325		}
326		if failure.is_none() {
327			keys = given.into_iter().collect::<Option<Vec<u64>>>().ok_or_else(|| {
328				internal_error!("bulk insert into series {} kept a row without a key", series.name)
329			})?;
330		}
331	}
332
333	let mut variant_tags = vec![None; rows];
334	if let Some((tags, tag_failure)) = tags {
335		variant_tags = tags;
336		if let Some((row, error)) = tag_failure
337			&& row < limit(&failure)
338		{
339			failure = Some((row, error));
340		}
341	}
342
343	if let TimeSource::Event {
344		ts,
345	} = checks.time && let Some(index) = checks.columns.iter().position(|column| &column.name == ts)
346		&& checks.columns[index].dictionary_id.is_none()
347	{
348		let view = &views[index];
349		if let Some(row) = (0..limit(&failure)).find(|&row| view.none_at(row)) {
350			failure = Some((
351				row,
352				EngineError::TimePopulatorNotDateTime {
353					object: checks.name.to_string(),
354					column: ts.to_string(),
355					found: format!("{:?}", Value::none()),
356				}
357				.into(),
358			));
359		}
360	}
361
362	if let Some((_, error)) = failure {
363		return Err(error);
364	}
365	Ok(Checked {
366		keys,
367		tags: variant_tags,
368	})
369}
370
371fn first_constraint_failure(column: &Column, view: &ColumnView<'_>, filled: &[usize], limit: usize) -> Option<usize> {
372	let target = column.constraint.get_type();
373	let max_bytes = match column.constraint.constraint() {
374		Some(Constraint::MaxBytes(max)) => Some(usize::from(*max)),
375		_ => None,
376	};
377	(0..limit).find(|&row| {
378		if view.none_at(row) {
379			return !target.is_option() && filled.binary_search(&row).is_err();
380		}
381		match (target.inner_type(), max_bytes) {
382			(ValueType::Utf8, Some(max)) => view.get_str(row).is_some_and(|text| text.len() > max),
383			(ValueType::Blob, Some(max)) => view.get_bytes(row).is_some_and(|bytes| bytes.len() > max),
384			_ => false,
385		}
386	})
387}
388
389fn constraint_error(column: &Column, mut value: Value, name: &str, row: usize) -> Error {
390	let mut error = match column.constraint.coerce(&mut value) {
391		Err(error) => error,
392		Ok(()) => internal_error!("bulk column {} passed the constraint its scan refused", column.name),
393	};
394	error.0.fragment = Fragment::internal(&column.name);
395	with_row_note(error, name, row)
396}
397
398fn key_column_index(series: &Series) -> Result<usize> {
399	let key_col_name = series.key.column();
400	series.columns
401		.iter()
402		.position(|c| c.name == key_col_name)
403		.ok_or_else(|| internal_error!("series {} key column {} not found", series.name, key_col_name))
404}
405
406fn enforce_write_policies(
407	writer: &Writer<'_>,
408	txn: &mut CommandTransaction,
409	namespace: &Namespace,
410	object: &str,
411	target: PolicyTargetType,
412	columns: &[(FieldRef, ArrayRef)],
413) -> Result<()> {
414	let symbols = SymbolTable::new();
415	PolicyEvaluator::new(writer.services, &symbols).enforce_write_policies(
416		&mut Transaction::Command(txn),
417		namespace.name(),
418		object,
419		DataOp::Insert,
420		&batch(columns.to_vec())?,
421		target,
422	)
423}
424
425fn stamp<B: SourceRowBuilder>(row: &mut B, times: &RowTime<'_>, index: usize, now: DateTime) -> Result<()> {
426	row.set_timestamps(now, now);
427	if let Some(time) = times.inserted(index, row.as_slice(), now)? {
428		row.set_time(time);
429	}
430	Ok(())
431}
432
433fn execute_table_insert<V: ValidationMode>(
434	writer: &Writer<'_>,
435	txn: &mut CommandTransaction,
436	pending: &PendingTableInsert,
437) -> Result<TableInsertResult> {
438	let catalog = writer.catalog;
439	let (namespace, table) = resolve_table(catalog, txn, pending)?;
440	let shape = get_or_create_table_shape(catalog, &table, &mut Transaction::Command(txn))?;
441	let (columns, _) =
442		coerce_columns(&pending.rows, &pending.batches, &table.columns, None, &table.name, &writer.coercer)?;
443	let rows = pending.row_count();
444	if rows == 0 {
445		return Ok(empty_table_result(pending));
446	}
447	enforce_write_policies(writer, txn, &namespace, &table.name, PolicyTargetType::Table, &columns)?;
448
449	let filled = auto_increment_rows(&table.columns, &columns)?;
450	check_rows::<V>(
451		&Checks {
452			name: &table.name,
453			columns: &table.columns,
454			filled: &filled,
455			series: None,
456			time: &table.time,
457		},
458		&columns,
459		None,
460		rows,
461	)?;
462	let fills = filled.into_iter().enumerate().filter(|(_, rows)| !rows.is_empty()).collect();
463	let mut cast = [CastColumns::with_fills(columns, fills, rows)];
464	intern_dictionary_columns(catalog, &mut Transaction::Command(txn), &table.columns, None, &mut cast)?;
465	fill_sequences(catalog, &mut Transaction::Command(txn), &table.columns, table.id.into(), &mut cast)?;
466	let mut encoded = EncodedTableRows {
467		rows: (0..rows).map(|_| shape.allocate_table()).collect(),
468		..EncodedTableRows::default()
469	};
470	cast[0].write(&shape, &mut encoded.rows)?;
471	let now = writer.now;
472	let times = RowTime::new(
473		&table.name,
474		&table.time,
475		&shape,
476		populator_index(&table.time, &shape).map(|index| cast[0].view(index)).transpose()?,
477	);
478	for (index, row) in encoded.rows.iter_mut().enumerate() {
479		stamp(row, &times, index, now)?;
480	}
481	table_batch_partitions(&table, &cast[0], &mut encoded)?;
482	write_table_rows(
483		catalog,
484		&mut Transaction::Command(txn),
485		&table,
486		&shape,
487		&mut encoded,
488		&Violation::BulkRow(&table.name),
489	)?;
490	Ok(TableInsertResult {
491		namespace: pending.namespace.clone(),
492		table: pending.table.clone(),
493		inserted: rows as u64,
494	})
495}
496
497fn auto_increment_rows(columns: &[Column], cast: &[(FieldRef, ArrayRef)]) -> Result<Vec<Vec<usize>>> {
498	columns.iter()
499		.zip(cast)
500		.map(|(column, cast)| {
501			if !column.auto_increment {
502				return Ok(Vec::new());
503			}
504			let view = ColumnView::try_from(cast)?;
505			Ok((0..view.len()).filter(|&row| view.none_at(row)).collect())
506		})
507		.collect()
508}
509
510#[inline]
511fn empty_table_result(pending: &PendingTableInsert) -> TableInsertResult {
512	TableInsertResult {
513		namespace: pending.namespace.clone(),
514		table: pending.table.clone(),
515		inserted: 0,
516	}
517}
518
519fn resolve_namespace(catalog: &Catalog, txn: &mut CommandTransaction, name: &str) -> Result<Namespace> {
520	catalog.find_namespace_by_name(&mut Transaction::Command(txn), name)?.ok_or_else(|| {
521		CatalogError::NotFound {
522			kind: CatalogObjectKind::Namespace,
523			namespace: name.to_string(),
524			name: String::new(),
525			fragment: Fragment::None,
526		}
527		.into()
528	})
529}
530
531fn has_keyed_table(
532	catalog: &Catalog,
533	txn: &mut CommandTransaction,
534	pending_tables: &[PendingTableInsert],
535) -> Result<bool> {
536	for pending in pending_tables {
537		let (_, table) = resolve_table(catalog, txn, pending)?;
538		if get_primary_key(catalog, &mut Transaction::Command(txn), &table)?.is_some() {
539			return Ok(true);
540		}
541	}
542	Ok(false)
543}
544
545fn resolve_table(
546	catalog: &Catalog,
547	txn: &mut CommandTransaction,
548	pending: &PendingTableInsert,
549) -> Result<(Namespace, Table)> {
550	let namespace = resolve_namespace(catalog, txn, &pending.namespace)?;
551	let table = catalog
552		.find_table_by_name(&mut Transaction::Command(txn), namespace.id(), &pending.table)?
553		.ok_or_else(|| -> Error {
554			CatalogError::NotFound {
555				kind: CatalogObjectKind::Table,
556				namespace: pending.namespace.to_string(),
557				name: pending.table.to_string(),
558				fragment: Fragment::None,
559			}
560			.into()
561		})?;
562	Ok((namespace, table))
563}
564
565fn execute_ringbuffer_insert<V: ValidationMode>(
566	writer: &Writer<'_>,
567	txn: &mut CommandTransaction,
568	pending: &PendingRingBufferInsert,
569) -> Result<RingBufferInsertResult> {
570	let catalog = writer.catalog;
571	let (namespace, ringbuffer) = resolve_ringbuffer(catalog, txn, pending)?;
572	let shape = get_or_create_ringbuffer_shape(catalog, &ringbuffer, &mut Transaction::Command(txn))?;
573	let (columns, _) = coerce_columns(
574		&pending.rows,
575		&pending.batches,
576		&ringbuffer.columns,
577		None,
578		&ringbuffer.name,
579		&writer.coercer,
580	)?;
581	let rows = pending.row_count();
582	let inserted = if rows == 0 {
583		0
584	} else {
585		insert_ringbuffer_rows::<V>(writer, txn, &namespace, &ringbuffer, &shape, columns, rows)?
586	};
587	Ok(RingBufferInsertResult {
588		namespace: pending.namespace.clone(),
589		ringbuffer: pending.ringbuffer.clone(),
590		inserted,
591	})
592}
593
594#[inline]
595fn resolve_ringbuffer(
596	catalog: &Catalog,
597	txn: &mut CommandTransaction,
598	pending: &PendingRingBufferInsert,
599) -> Result<(Namespace, RingBuffer)> {
600	let namespace = resolve_namespace(catalog, txn, &pending.namespace)?;
601	let ringbuffer = catalog
602		.find_ringbuffer_by_name(&mut Transaction::Command(txn), namespace.id(), &pending.ringbuffer)?
603		.ok_or_else(|| -> Error {
604			CatalogError::NotFound {
605				kind: CatalogObjectKind::RingBuffer,
606				namespace: pending.namespace.to_string(),
607				name: pending.ringbuffer.to_string(),
608				fragment: Fragment::None,
609			}
610			.into()
611		})?;
612	Ok((namespace, ringbuffer))
613}
614
615fn insert_ringbuffer_rows<V: ValidationMode>(
616	writer: &Writer<'_>,
617	txn: &mut CommandTransaction,
618	namespace: &Namespace,
619	ringbuffer: &RingBuffer,
620	shape: &RowShape,
621	columns: Vec<(FieldRef, ArrayRef)>,
622	rows: usize,
623) -> Result<u64> {
624	let catalog = writer.catalog;
625	enforce_write_policies(writer, txn, namespace, &ringbuffer.name, PolicyTargetType::RingBuffer, &columns)?;
626	check_rows::<V>(
627		&Checks {
628			name: &ringbuffer.name,
629			columns: &ringbuffer.columns,
630			filled: &vec![Vec::new(); ringbuffer.columns.len()],
631			series: None,
632			time: &ringbuffer.time,
633		},
634		&columns,
635		None,
636		rows,
637	)?;
638
639	let mut cast = [CastColumns::new(columns, rows)];
640	intern_dictionary_columns(catalog, &mut Transaction::Command(txn), &ringbuffer.columns, None, &mut cast)?;
641	let mut built: Vec<_> = (0..rows).map(|_| shape.allocate_ringbuffer()).collect();
642	cast[0].write(shape, &mut built)?;
643
644	let batch = ringbuffer_batch_partitions(ringbuffer, &cast[0])?;
645	let mut verified = HashMap::new();
646	resolve_partitions(
647		&mut Transaction::Command(txn),
648		ObjectId::ringbuffer(ringbuffer.id),
649		&batch.distinct,
650		&mut verified,
651	)?;
652	let now = writer.now;
653	let times = RowTime::new(
654		&ringbuffer.name,
655		&ringbuffer.time,
656		shape,
657		populator_index(&ringbuffer.time, shape).map(|index| cast[0].view(index)).transpose()?,
658	);
659	let mut entries = Vec::with_capacity(rows);
660	for (row, mut builder) in built.into_iter().enumerate() {
661		stamp(&mut builder, &times, row, now)?;
662		entries.push(builder.freeze_bytes());
663	}
664
665	let mut cache = HashMap::new();
666	let inserted = insert_ringbuffer_chunks(
667		catalog,
668		&mut Transaction::Command(txn),
669		ringbuffer,
670		shape,
671		&entries,
672		&batch.rows,
673		&verified,
674		&mut cache,
675		None,
676	)?;
677	save_all_partition_metadata(catalog, &mut Transaction::Command(txn), ringbuffer, &cache)?;
678	Ok(inserted)
679}
680
681fn execute_series_insert<V: ValidationMode>(
682	writer: &Writer<'_>,
683	txn: &mut CommandTransaction,
684	pending: &PendingSeriesInsert,
685) -> Result<SeriesInsertResult> {
686	let catalog = writer.catalog;
687	let (namespace, series) = resolve_series(catalog, txn, pending)?;
688	let mut metadata_by_partition: HashMap<Partition, SeriesPartitionMetadata> = HashMap::new();
689	let mut inserted_keys: HashMap<Partition, Vec<u64>> = HashMap::new();
690	let shape = get_or_create_series_shape(catalog, &series, &mut Transaction::Command(txn))?;
691	let sumtype = series.tag.map(|id| catalog.get_sumtype(&mut Transaction::Command(txn), id)).transpose()?;
692	let (columns, tags) = coerce_columns(
693		&pending.rows,
694		&pending.batches,
695		&series.columns,
696		sumtype.as_ref(),
697		&series.name,
698		&writer.coercer,
699	)?;
700	let rows = pending.row_count();
701	let inserted = if rows == 0 {
702		0
703	} else {
704		enforce_write_policies(writer, txn, &namespace, &series.name, PolicyTargetType::Series, &columns)?;
705		let checked = check_rows::<V>(
706			&Checks {
707				name: &series.name,
708				columns: &series.columns,
709				filled: &vec![Vec::new(); series.columns.len()],
710				series: Some(&series),
711				time: &series.time,
712			},
713			&columns,
714			tags,
715			rows,
716		)?;
717		insert_series_rows(
718			writer,
719			txn,
720			&series,
721			&shape,
722			columns,
723			checked,
724			&mut metadata_by_partition,
725			&mut inserted_keys,
726		)?
727	};
728	let now = writer.now;
729	for (partition, metadata) in metadata_by_partition {
730		let keys = inserted_keys.remove(&partition).unwrap_or_default();
731		write_inserted_series_metadata(
732			catalog,
733			&mut Transaction::Command(txn),
734			&series,
735			partition,
736			metadata,
737			keys,
738			now,
739			V::VALIDATED,
740		)?;
741		if !V::VALIDATED {
742			txn.rewrite_on(
743				&SeriesPartitionMetadataKey::new(series.id, partition).into(),
744				renumber_series_rows(StorageId::series(series.id), partition),
745			)?;
746		}
747	}
748	Ok(SeriesInsertResult {
749		namespace: pending.namespace.clone(),
750		series: pending.series.clone(),
751		inserted,
752	})
753}
754
755#[inline]
756fn resolve_series(
757	catalog: &Catalog,
758	txn: &mut CommandTransaction,
759	pending: &PendingSeriesInsert,
760) -> Result<(Namespace, Series)> {
761	let namespace = resolve_namespace(catalog, txn, &pending.namespace)?;
762	let series = catalog
763		.find_series_by_name(&mut Transaction::Command(txn), namespace.id(), &pending.series)?
764		.ok_or_else(|| -> Error {
765			CatalogError::NotFound {
766				kind: CatalogObjectKind::Series,
767				namespace: pending.namespace.to_string(),
768				name: pending.series.to_string(),
769				fragment: Fragment::None,
770			}
771			.into()
772		})?;
773	Ok((namespace, series))
774}
775
776fn insert_series_rows(
777	writer: &Writer<'_>,
778	txn: &mut CommandTransaction,
779	series: &Series,
780	shape: &RowShape,
781	columns: Vec<(FieldRef, ArrayRef)>,
782	checked: Checked,
783	metadata_by_partition: &mut HashMap<Partition, SeriesPartitionMetadata>,
784	inserted_keys: &mut HashMap<Partition, Vec<u64>>,
785) -> Result<u64> {
786	let catalog = writer.catalog;
787	let rows = checked.keys.len();
788	let key_index = key_column_index(series)?;
789	let mut cast = [CastColumns::new(columns, rows)];
790	intern_dictionary_columns(
791		catalog,
792		&mut Transaction::Command(txn),
793		&series.columns,
794		Some(&series.key),
795		&mut cast,
796	)?;
797	let [cast] = cast;
798
799	let key_column = series.key_column_data(checked.keys.clone());
800	let mut built: Vec<_> = (0..rows).map(|_| shape.allocate_series()).collect();
801	let mut views = Vec::with_capacity(series.columns.len());
802	views.push(ColumnView::try_from(&key_column)?);
803	for index in 0..series.columns.len() {
804		if index != key_index {
805			views.push(cast.view(index)?);
806		}
807	}
808	shape.write_columns(&mut built, &views)?;
809	let now = writer.now;
810	let times = RowTime::new(
811		&series.name,
812		&series.time,
813		shape,
814		populator_index(&series.time, shape).map(|index| views[index].clone()),
815	);
816
817	let batch = series_batch_partitions(series, &cast, Some(&views[0]))?;
818	resolve_partitions(
819		&mut Transaction::Command(txn),
820		ObjectId::Series(series.id),
821		&batch.distinct,
822		&mut HashMap::new(),
823	)?;
824	let assigned = assign_series_keys(
825		catalog,
826		&mut Transaction::Command(txn),
827		series,
828		&batch.rows,
829		checked.keys.into_iter().map(Some).collect(),
830		&checked.tags,
831		metadata_by_partition,
832		|_| Err(internal_error!("bulk insert into series {} kept a row without a key", series.name)),
833	)?;
834	for (row, &key) in assigned.keys.iter().enumerate() {
835		inserted_keys.entry(batch.rows.get(row).copied().unwrap_or_default()).or_default().push(key);
836	}
837	for (row, builder) in built.iter_mut().enumerate() {
838		stamp(builder, &times, row, now)?;
839	}
840	let written = write_series_rows(
841		&mut Transaction::Command(txn),
842		series,
843		shape,
844		SeriesRows {
845			keys: assigned.storage,
846			partitions: &batch.rows,
847			rows: built,
848		},
849	)?;
850	Ok(written.len() as u64)
851}
852
853fn renumber_series_rows(storage: StorageId, partition: Partition) -> Rewrite {
854	Arc::new(move |deltas, at_base, at_latest| {
855		let counter = |bytes: Option<&EncodedBytes>| -> Result<u64> {
856			Ok(match bytes {
857				Some(bytes) => {
858					decode_series_partition_metadata(EncodedPodRow::view(bytes))?.sequence_counter
859				}
860				None => 0,
861			})
862		};
863		let base = counter(at_base)?;
864		let gap = counter(at_latest)?.checked_sub(base).ok_or_else(|| {
865			internal_error!(
866				"series {:?} partition counter fell below the bulk's snapshot counter {}",
867				storage,
868				base
869			)
870		})?;
871		if gap == 0 {
872			return Ok(());
873		}
874		for delta in deltas.make_mut() {
875			if let Delta::Set {
876				key,
877				..
878			} = delta
879			{
880				match key {
881					TaggedKey::SeriesRow(row) if row.storage == storage && row.sequence > base => {
882						row.sequence += gap;
883					}
884					TaggedKey::PartitionedSeriesRow(row)
885						if row.storage == storage
886							&& row.partition == partition && row.sequence > base =>
887					{
888						row.sequence += gap;
889					}
890					_ => {}
891				}
892			}
893		}
894		Ok(())
895	})
896}
897
898fn parse_qualified_name(qualified_name: &str) -> (String, String) {
899	if let Some((ns, name)) = qualified_name.rsplit_once("::") {
900		(ns.to_string(), name.to_string())
901	} else {
902		("default".to_string(), qualified_name.to_string())
903	}
904}
905
906#[cfg(test)]
907mod tests {
908	use super::*;
909
910	#[test]
911	fn parse_qualified_name_simple() {
912		assert_eq!(parse_qualified_name("table"), ("default".to_string(), "table".to_string()));
913	}
914
915	#[test]
916	fn parse_qualified_name_single_namespace() {
917		assert_eq!(parse_qualified_name("ns::table"), ("ns".to_string(), "table".to_string()));
918	}
919
920	#[test]
921	fn parse_qualified_name_nested_namespace() {
922		assert_eq!(parse_qualified_name("a::b::table"), ("a::b".to_string(), "table".to_string()));
923	}
924
925	#[test]
926	fn parse_qualified_name_deeply_nested_namespace() {
927		assert_eq!(parse_qualified_name("a::b::c::table"), ("a::b::c".to_string(), "table".to_string()));
928	}
929
930	#[test]
931	fn parse_qualified_name_empty_string() {
932		assert_eq!(parse_qualified_name(""), ("default".to_string(), "".to_string()));
933	}
934}