1mod checkpoint;
4mod configuration;
5mod index_api;
6mod maintenance;
7mod mutation;
8mod query_api;
9mod query_contract;
10mod query_engine;
11mod resource;
12mod validation;
13
14#[cfg(feature = "async")]
15mod async_api;
16
17use crate::config::{ConfigBuilder, IoBackend};
18use crate::doc::{Doc, DocumentMap};
19use crate::error::{Error, Result};
20use crate::index::IndexRegistry;
21use crate::schema::{AddColumnOption, AlterColumnOption, CollectionSchema, FieldSchema};
22use crate::stats::{assess_collection_health, CollectionHealthInput, StatsRegistry, StatsSnapshot};
23pub use crate::stats::{CollectionHealth, CollectionHealthStatus, IndexStat};
24use crate::storage::StorageHandle;
25use crate::storage_ceilings::StorageCeilings;
26use checkpoint::{
27 append_prepared_schema_change, persist_index_cache, publish_prepared_schema_change,
28};
29pub use configuration::CollectionOptions;
30use configuration::{options_config, resolved_storage_ceilings};
31pub use maintenance::{
32 CollectionMaintenanceHealth, CollectionMaintenanceOptions, CollectionMaintenancePhase,
33 CollectionMaintenanceRuntime,
34};
35pub use mutation::{DocWriteResult, WriteResult};
36use rayon::prelude::*;
37pub use resource::CollectionResourceLimits;
38use resource::ResourceUsage;
39use serde::{Deserialize, Serialize};
40use std::path::{Path, PathBuf};
41use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
42use std::sync::{Arc, Mutex, RwLock};
43use validation::{normalize_doc, parse_default_expression, validate_doc};
44
45const MAX_SCHEMA_WORKERS: usize = 256;
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct CollectionStats {
54 pub doc_count: u64,
55 pub indexes: Vec<IndexStat>,
56 pub revision: u64,
57 #[serde(default)]
58 pub index_cache_hit: bool,
59 #[serde(default)]
62 pub io_backend: IoBackend,
63 pub read_only: bool,
64 pub wal_active_seq: u64,
65 pub wal_checkpoint_seq: u64,
66 pub wal_ops_since_checkpoint: u64,
67 pub wal_bytes_since_checkpoint: u64,
68 #[serde(default)]
70 pub accounted_document_bytes: u64,
71 #[serde(default)]
73 pub estimated_index_bytes: u64,
74 #[serde(default)]
76 pub accounted_bytes: u64,
77 #[serde(default)]
79 pub resource_limits: CollectionResourceLimits,
80 #[serde(default)]
82 pub storage_ceilings: StorageCeilings,
83 #[serde(default)]
85 pub resource_limit_rejections: u64,
86}
87
88#[derive(Debug, Clone)]
89struct CollectionState {
90 path: PathBuf,
91 schema: CollectionSchema,
92 docs: Arc<DocumentMap>,
93 revision: u64,
94 options: CollectionOptions,
95 config: ConfigBuilder,
96 stats: Arc<StatsRegistry>,
97 indexes: Arc<IndexRegistry>,
98 index_cache_hit: bool,
99 resource_usage: ResourceUsage,
100}
101
102#[derive(Debug, Clone)]
103struct CollectionSnapshot {
104 schema: CollectionSchema,
105 docs: Arc<DocumentMap>,
106 revision: u64,
107 stats: Arc<StatsRegistry>,
108 indexes: Arc<IndexRegistry>,
109 resource_limits: CollectionResourceLimits,
110}
111
112#[derive(Debug)]
113struct CollectionInner {
114 state: RwLock<CollectionState>,
115 storage: Mutex<StorageHandle>,
116 writer: Mutex<()>,
117 closed: AtomicBool,
118 maintenance_claimed: AtomicBool,
119}
120
121#[derive(Clone, Debug)]
123pub struct Collection {
124 inner: Arc<CollectionInner>,
125}
126
127impl Collection {
128 pub fn create_and_open(
129 path: &str,
130 schema: &CollectionSchema,
131 options: Option<&CollectionOptions>,
132 ) -> Result<Self> {
133 let options = options.cloned().unwrap_or_default();
134 let config = options_config(&options);
135 let ceilings = resolved_storage_ceilings(&options);
136 let root = Path::new(path);
137 schema.validate()?;
138 let docs = DocumentMap::new();
139 let indexes = IndexRegistry::build(schema, &docs, 0)?;
140 let resource_usage = options
141 .resource_limits
142 .enforce_state(schema, &docs, &indexes)?;
143 let storage = StorageHandle::create(root, schema, options.read_only, ceilings)?;
144 let state = CollectionState {
145 path: root.to_path_buf(),
146 schema: schema.clone(),
147 docs: Arc::new(docs),
148 revision: 0,
149 options,
150 config,
151 stats: Arc::new(StatsRegistry::default()),
152 indexes: Arc::new(indexes),
153 index_cache_hit: false,
154 resource_usage,
155 };
156 Ok(Self {
157 inner: Arc::new(CollectionInner {
158 state: RwLock::new(state),
159 storage: Mutex::new(storage),
160 writer: Mutex::new(()),
161 closed: AtomicBool::new(false),
162 maintenance_claimed: AtomicBool::new(false),
163 }),
164 })
165 }
166
167 pub fn create(
168 path: &str,
169 schema: &CollectionSchema,
170 options: Option<&CollectionOptions>,
171 ) -> Result<Self> {
172 Self::create_and_open(path, schema, options)
173 }
174
175 pub fn open(path: &str, options: Option<&CollectionOptions>) -> Result<Self> {
176 let options = options.cloned().unwrap_or_default();
177 let config = options_config(&options);
178 let ceilings = resolved_storage_ceilings(&options);
179 let (storage, schema, docs) =
180 StorageHandle::open(Path::new(path), options.read_only, ceilings)?;
181 if schema.name.trim().is_empty() {
182 return Err(Error::internal("persisted collection has an empty name"));
183 }
184 let revision = storage.manifest.revision;
185 let mut recovered_docs = DocumentMap::new();
186 for doc in docs {
187 let doc = normalize_doc(&schema, &doc).map_err(|error| {
188 Error::internal(format!(
189 "persisted document cannot be normalized: {}",
190 error.message
191 ))
192 })?;
193 validate_doc(&schema, &doc, true).map_err(|error| {
194 Error::internal(format!("persisted document is invalid: {}", error.message))
195 })?;
196 let id = doc
197 .get_pk()
198 .ok_or_else(|| Error::internal("persisted document has no primary key"))?
199 .to_string();
200 if recovered_docs.insert(id.clone(), Arc::new(doc)).is_some() {
201 return Err(Error::internal(format!(
202 "persisted collection contains duplicate primary key '{id}'"
203 )));
204 }
205 }
206 let docs = recovered_docs;
207 let cached_indexes = storage.read_index_cache().ok().flatten().and_then(|bytes| {
208 let diskann_file = storage.open_diskann_file().ok().flatten();
209 IndexRegistry::restore_cache(
210 &bytes,
211 diskann_file,
212 config.io_backend,
213 &schema,
214 &docs,
215 revision,
216 &storage.index_cache_identity(),
217 storage.ceilings,
218 )
219 });
220 let index_cache_hit = cached_indexes.is_some();
221 let indexes = cached_indexes.map_or_else(
222 || {
223 IndexRegistry::build(&schema, &docs, revision).map_err(|error| {
224 Error::internal(format!(
225 "rebuild persisted indexes at revision {revision}: {}",
226 error.message
227 ))
228 })
229 },
230 Ok,
231 )?;
232 let resource_usage = options
233 .resource_limits
234 .enforce_state(&schema, &docs, &indexes)?;
235 if !index_cache_hit && !options.read_only {
236 persist_index_cache(&storage, &schema, &indexes, revision, false);
237 }
238 let state = CollectionState {
239 path: PathBuf::from(path),
240 schema: schema.clone(),
241 docs: Arc::new(docs),
242 revision,
243 options,
244 config,
245 stats: Arc::new(StatsRegistry::default()),
246 indexes: Arc::new(indexes),
247 index_cache_hit,
248 resource_usage,
249 };
250 Ok(Self {
251 inner: Arc::new(CollectionInner {
252 state: RwLock::new(state),
253 storage: Mutex::new(storage),
254 writer: Mutex::new(()),
255 closed: AtomicBool::new(false),
256 maintenance_claimed: AtomicBool::new(false),
257 }),
258 })
259 }
260
261 pub fn path(&self) -> PathBuf {
262 self.inner
263 .state
264 .read()
265 .map(|state| state.path.clone())
266 .unwrap_or_default()
267 }
268
269 pub fn is_open(&self) -> bool {
270 !self.inner.closed.load(AtomicOrdering::Acquire)
271 }
272
273 pub fn flush(&self) -> Result<()> {
274 self.ensure_open()?;
275 let _writer = self
276 .inner
277 .writer
278 .lock()
279 .map_err(|_| Error::internal("writer lock poisoned"))?;
280 let (schema, docs, indexes, revision) = {
281 let state = self
282 .inner
283 .state
284 .read()
285 .map_err(|_| Error::internal("collection state lock poisoned"))?;
286 (
287 state.schema.clone(),
288 Arc::clone(&state.docs),
289 Arc::clone(&state.indexes),
290 state.revision,
291 )
292 };
293 let mut storage = self
294 .inner
295 .storage
296 .lock()
297 .map_err(|_| Error::internal("storage lock poisoned"))?;
298 storage.checkpoint(&schema, docs.as_ref(), revision, true)?;
299 persist_index_cache(&storage, &schema, &indexes, revision, true);
300 Ok(())
301 }
302
303 pub fn close(self) -> Result<()> {
304 if self.is_open() {
305 let read_only = self
306 .inner
307 .state
308 .read()
309 .map_err(|_| Error::internal("collection state lock poisoned"))?
310 .options
311 .read_only;
312 if !read_only {
313 self.flush()?;
314 }
315 self.inner.closed.store(true, AtomicOrdering::Release);
316 }
317 Ok(())
318 }
319
320 pub fn destroy(self) -> Result<()> {
321 let path = self.path();
322 self.close()?;
323 if path.exists() {
324 std::fs::remove_dir_all(&path)
325 .map_err(|e| Error::internal(format!("destroy collection: {e}")))?;
326 }
327 Ok(())
328 }
329
330 pub fn schema(&self) -> Result<CollectionSchema> {
331 self.ensure_open()?;
332 self.inner
333 .state
334 .read()
335 .map(|state| state.schema.clone())
336 .map_err(|_| Error::internal("collection state lock poisoned"))
337 }
338
339 pub fn stats(&self) -> Result<CollectionStats> {
340 self.ensure_open()?;
341 self.collect_stats().map(|(stats, _)| stats)
342 }
343
344 pub fn health(&self) -> Result<CollectionHealth> {
350 let (stats, storage_revision) = self.collect_stats()?;
351 Ok(assess_collection_health(CollectionHealthInput {
352 is_open: self.is_open(),
353 revision: stats.revision,
354 storage_revision,
355 doc_count: stats.doc_count,
356 indexes: &stats.indexes,
357 read_only: stats.read_only,
358 wal_ops_since_checkpoint: stats.wal_ops_since_checkpoint,
359 wal_bytes_since_checkpoint: stats.wal_bytes_since_checkpoint,
360 maintenance_active: self.inner.maintenance_claimed.load(AtomicOrdering::Acquire),
361 }))
362 }
363
364 fn collect_stats(&self) -> Result<(CollectionStats, u64)> {
365 let state = self
366 .inner
367 .state
368 .read()
369 .map_err(|_| Error::internal("collection state lock poisoned"))?;
370 let storage = self
371 .inner
372 .storage
373 .lock()
374 .map_err(|_| Error::internal("storage lock poisoned"))?;
375 let mut indexes = state
376 .indexes
377 .stats(&state.schema, &state.docs, state.revision);
378 indexes.sort_by(|left, right| left.name.cmp(&right.name));
379 let usage = state.resource_usage;
380 Ok((
381 CollectionStats {
382 doc_count: state.docs.len() as u64,
383 indexes,
384 revision: state.revision,
385 index_cache_hit: state.index_cache_hit,
386 io_backend: state.config.io_backend,
387 read_only: state.options.read_only,
388 wal_active_seq: storage.manifest.wal_active_seq,
389 wal_checkpoint_seq: storage.manifest.wal_checkpoint_seq,
390 wal_ops_since_checkpoint: storage.manifest.wal_ops_since_checkpoint,
391 wal_bytes_since_checkpoint: storage.manifest.wal_bytes_since_checkpoint,
392 accounted_document_bytes: usage.documents,
393 estimated_index_bytes: usage.indexes,
394 accounted_bytes: usage.total,
395 resource_limits: state.options.resource_limits,
396 storage_ceilings: storage.ceilings,
397 resource_limit_rejections: state
398 .stats
399 .resource_limit_rejections
400 .load(AtomicOrdering::Relaxed),
401 },
402 storage.manifest.revision,
403 ))
404 }
405
406 pub fn stats_snapshot(&self) -> Result<StatsSnapshot> {
407 let basic = self.stats()?;
408 let state = self
409 .inner
410 .state
411 .read()
412 .map_err(|_| Error::internal("collection state lock poisoned"))?;
413 let registry = Arc::clone(&state.stats);
414 Ok(StatsSnapshot {
415 collection_name: state.schema.name.clone(),
416 revision: basic.revision,
417 doc_count: basic.doc_count,
418 query_count: registry.query_count.load(AtomicOrdering::Relaxed),
419 fts_query_count: registry.fts_query_count.load(AtomicOrdering::Relaxed),
420 fts_index_query_count: registry.fts_index_query_count.load(AtomicOrdering::Relaxed),
421 ann_query_count: registry.ann_query_count.load(AtomicOrdering::Relaxed),
422 diskann_query_count: registry.diskann_query_count.load(AtomicOrdering::Relaxed),
423 diskann_mmap_query_count: registry
424 .diskann_mmap_query_count
425 .load(AtomicOrdering::Relaxed),
426 diskann_sector_read_count: registry
427 .diskann_sector_read_count
428 .load(AtomicOrdering::Relaxed),
429 exact_query_count: registry.exact_query_count.load(AtomicOrdering::Relaxed),
430 filtered_query_count: registry.filtered_query_count.load(AtomicOrdering::Relaxed),
431 scalar_index_query_count: registry
432 .scalar_index_query_count
433 .load(AtomicOrdering::Relaxed),
434 radius_query_count: registry.radius_query_count.load(AtomicOrdering::Relaxed),
435 candidates_scanned: registry.candidates_scanned.load(AtomicOrdering::Relaxed),
436 indexed_field_count: basic.indexes.len(),
437 indexes: basic.indexes,
438 index_cache_hit: basic.index_cache_hit,
439 io_backend: basic.io_backend,
440 read_only: basic.read_only,
441 wal_active_seq: basic.wal_active_seq,
442 wal_checkpoint_seq: basic.wal_checkpoint_seq,
443 wal_ops_since_checkpoint: basic.wal_ops_since_checkpoint,
444 wal_bytes_since_checkpoint: basic.wal_bytes_since_checkpoint,
445 accounted_document_bytes: basic.accounted_document_bytes,
446 estimated_index_bytes: basic.estimated_index_bytes,
447 accounted_bytes: basic.accounted_bytes,
448 resource_limits: basic.resource_limits,
449 resource_limit_rejections: basic.resource_limit_rejections,
450 })
451 }
452
453 pub fn count(&self) -> Result<usize> {
454 self.ensure_open()?;
455 self.inner
456 .state
457 .read()
458 .map(|state| state.docs.len())
459 .map_err(|_| Error::internal("collection state lock poisoned"))
460 }
461
462 pub fn add_column(&self, field_schema: &FieldSchema, default_expr: Option<&str>) -> Result<()> {
467 self.add_column_with_options(field_schema, default_expr, AddColumnOption::default())
468 }
469
470 pub fn add_column_with_options(
471 &self,
472 field_schema: &FieldSchema,
473 default_expr: Option<&str>,
474 option: AddColumnOption,
475 ) -> Result<()> {
476 self.ensure_open()?;
477 let _writer = self
478 .inner
479 .writer
480 .lock()
481 .map_err(|_| Error::internal("writer lock poisoned"))?;
482 let state = self
483 .inner
484 .state
485 .write()
486 .map_err(|_| Error::internal("collection state lock poisoned"))?;
487 ensure_writable(&state.options)?;
488 let mut next = state.clone();
489 next.schema.add_field(field_schema)?;
490 let default = default_expr
491 .map(|expression| parse_default_expression(expression, field_schema.data_type))
492 .transpose()?;
493 if let Some(value) = default {
494 next.docs = Arc::new(transform_documents_with_concurrency(
495 &next.docs,
496 option.concurrency,
497 |doc| doc.set_field_value(&field_schema.name, value.clone()),
498 )?);
499 }
500 validate_documents_with_concurrency(&next.schema, &next.docs, option.concurrency)?;
501 let config = state.config.clone();
502 let previous_docs = Arc::clone(&state.docs);
503 let previous_revision = state.revision;
504 let previous_schema = state.schema.clone();
505 drop(state);
506 finish_schema_commit(
507 self,
508 &previous_docs,
509 previous_revision,
510 &previous_schema,
511 next,
512 &config,
513 )
514 }
515
516 pub fn drop_column(&self, name: &str) -> Result<()> {
517 self.ensure_open()?;
518 let _writer = self
519 .inner
520 .writer
521 .lock()
522 .map_err(|_| Error::internal("writer lock poisoned"))?;
523 let state = self
524 .inner
525 .state
526 .write()
527 .map_err(|_| Error::internal("collection state lock poisoned"))?;
528 ensure_writable(&state.options)?;
529 let mut next = state.clone();
530 next.schema.drop_field(name)?;
531 next.docs = Arc::new(transform_documents(&next.docs, |doc| {
532 doc.remove_field(name)
533 })?);
534 let config = state.config.clone();
535 let previous_docs = Arc::clone(&state.docs);
536 let previous_revision = state.revision;
537 let previous_schema = state.schema.clone();
538 drop(state);
539 finish_schema_commit(
540 self,
541 &previous_docs,
542 previous_revision,
543 &previous_schema,
544 next,
545 &config,
546 )
547 }
548
549 pub fn rename_column(&self, old_name: &str, new_name: &str) -> Result<()> {
550 if old_name.trim().is_empty() || old_name.contains('\0') {
551 return Err(Error::invalid_argument("old field name is invalid"));
552 }
553 if new_name.trim().is_empty() || new_name.contains('\0') {
554 return Err(Error::invalid_argument("new field name is invalid"));
555 }
556 if old_name == new_name {
557 return Ok(());
558 }
559 self.ensure_open()?;
560 let _writer = self
561 .inner
562 .writer
563 .lock()
564 .map_err(|_| Error::internal("writer lock poisoned"))?;
565 let state = self
566 .inner
567 .state
568 .write()
569 .map_err(|_| Error::internal("collection state lock poisoned"))?;
570 ensure_writable(&state.options)?;
571 let mut next = state.clone();
572 if next.schema.has_field(new_name) {
573 return Err(Error::already_exists(format!(
574 "field '{new_name}' already exists"
575 )));
576 }
577 if let Some(field) = next
578 .schema
579 .fields
580 .iter_mut()
581 .find(|field| field.name == old_name)
582 {
583 field.name = new_name.to_string();
584 } else if let Some(field) = next
585 .schema
586 .vectors
587 .iter_mut()
588 .find(|field| field.name == old_name)
589 {
590 field.name = new_name.to_string();
591 } else {
592 return Err(Error::not_found(format!("field '{old_name}' not found")));
593 }
594 next.docs = Arc::new(transform_documents(&next.docs, |doc| {
595 if let Some(value) = doc.field(old_name).cloned() {
596 doc.remove_field(old_name)?;
597 doc.set_field_value(new_name, value)?;
598 } else if let Some(value) = doc.vector(old_name).cloned() {
599 doc.remove_field(old_name)?;
600 doc.set_vector_value(new_name, value)?;
601 }
602 Ok(())
603 })?);
604 let config = state.config.clone();
605 let previous_docs = Arc::clone(&state.docs);
606 let previous_revision = state.revision;
607 let previous_schema = state.schema.clone();
608 drop(state);
609 finish_schema_commit(
610 self,
611 &previous_docs,
612 previous_revision,
613 &previous_schema,
614 next,
615 &config,
616 )
617 }
618
619 pub fn alter_column(
620 &self,
621 field_schema: &FieldSchema,
622 option: AlterColumnOption,
623 ) -> Result<()> {
624 self.ensure_open()?;
625 let _writer = self
626 .inner
627 .writer
628 .lock()
629 .map_err(|_| Error::internal("writer lock poisoned"))?;
630 let state = self
631 .inner
632 .state
633 .write()
634 .map_err(|_| Error::internal("collection state lock poisoned"))?;
635 ensure_writable(&state.options)?;
636 let mut next = state.clone();
637 let target = next
638 .schema
639 .fields
640 .iter_mut()
641 .find(|field| field.name == field_schema.name)
642 .ok_or_else(|| Error::not_found(format!("field '{}' not found", field_schema.name)))?;
643 if target.data_type != field_schema.data_type || target.dimension != field_schema.dimension
644 {
645 return Err(Error::invalid_argument(
646 "altering a field's data type or dimension would invalidate existing data",
647 ));
648 }
649 *target = field_schema.clone();
650 next.schema.validate()?;
651 validate_documents_with_concurrency(&next.schema, &next.docs, option.concurrency)?;
652 let config = state.config.clone();
653 let previous_docs = Arc::clone(&state.docs);
654 let previous_revision = state.revision;
655 let previous_schema = state.schema.clone();
656 drop(state);
657 finish_schema_commit(
658 self,
659 &previous_docs,
660 previous_revision,
661 &previous_schema,
662 next,
663 &config,
664 )
665 }
666
667 fn snapshot_state(&self) -> Result<CollectionSnapshot> {
668 let state = self
669 .inner
670 .state
671 .read()
672 .map_err(|_| Error::internal("collection state lock poisoned"))?;
673 Ok(CollectionSnapshot {
674 schema: state.schema.clone(),
675 docs: state.docs.clone(),
676 revision: state.revision,
677 stats: Arc::clone(&state.stats),
678 indexes: state.indexes.clone(),
679 resource_limits: state.options.resource_limits,
680 })
681 }
682
683 fn ensure_open(&self) -> Result<()> {
684 if self.inner.closed.load(AtomicOrdering::Acquire) {
685 Err(Error::failed_precondition("collection is closed"))
686 } else {
687 Ok(())
688 }
689 }
690
691 #[cfg(test)]
692 pub(crate) fn test_arm_wal_sync_stall(&self) -> crate::storage::StallGate {
693 let storage = self.inner.storage.lock().expect("storage lock poisoned");
694 storage.arm_wal_sync_stall()
695 }
696
697 #[cfg(test)]
698 pub(crate) fn test_arm_diskann_write_fault(&self) {
699 let storage = self.inner.storage.lock().expect("storage lock poisoned");
700 storage.arm_diskann_write_fault();
701 }
702
703 #[cfg(test)]
704 pub(crate) fn test_diskann_write_fault_fired(&self) -> bool {
705 let storage = self.inner.storage.lock().expect("storage lock poisoned");
706 storage.diskann_write_fault_fired()
707 }
708}
709
710fn ensure_writable(options: &CollectionOptions) -> Result<()> {
711 if options.read_only {
712 Err(Error::permission_denied("collection is read-only"))
713 } else {
714 Ok(())
715 }
716}
717
718fn ensure_same_generation(current: &CollectionState, expected: &CollectionState) -> Result<()> {
719 if current.revision == expected.revision && current.schema == expected.schema {
720 Ok(())
721 } else {
722 Err(Error::failed_precondition(
723 "collection generation changed during index construction",
724 ))
725 }
726}
727
728fn transform_documents(
729 docs: &DocumentMap,
730 transform: impl Fn(&mut Doc) -> Result<()> + Send + Sync,
731) -> Result<DocumentMap> {
732 transform_documents_with_concurrency(docs, 0, transform)
733}
734
735fn transform_documents_with_concurrency(
742 docs: &DocumentMap,
743 concurrency: u32,
744 transform: impl Fn(&mut Doc) -> Result<()> + Send + Sync,
745) -> Result<DocumentMap> {
746 let entries: Vec<(String, Arc<Doc>)> = docs
747 .iter()
748 .map(|(id, doc)| (id.clone(), Arc::clone(doc)))
749 .collect();
750 let transform_one = |(id, doc): &(String, Arc<Doc>)| {
751 let mut next = doc.as_ref().clone();
752 let result = transform(&mut next).map(|()| next);
753 (id.clone(), result)
754 };
755 let transformed: Vec<(String, Result<Doc>)> =
756 if let Some(threads) = schema_worker_count(concurrency, entries.len())? {
757 let pool = rayon::ThreadPoolBuilder::new()
758 .num_threads(threads)
759 .build()
760 .map_err(|error| {
761 Error::resource_exhausted(format!("build schema worker pool: {error}"))
762 })?;
763 pool.install(|| entries.par_iter().map(transform_one).collect())
764 } else {
765 entries.iter().map(transform_one).collect()
766 };
767 let mut output = DocumentMap::new();
768 for (id, result) in transformed {
769 output.insert(id, Arc::new(result?));
770 }
771 Ok(output)
772}
773
774fn validate_documents_with_concurrency(
778 schema: &CollectionSchema,
779 docs: &DocumentMap,
780 concurrency: u32,
781) -> Result<()> {
782 let entries: Vec<Arc<Doc>> = docs.values().cloned().collect();
783 let validate_one = |doc: &Arc<Doc>| validate_doc(schema, doc, true);
784 let Some(threads) = schema_worker_count(concurrency, entries.len())? else {
785 for doc in &entries {
786 validate_one(doc)?;
787 }
788 return Ok(());
789 };
790 let pool = rayon::ThreadPoolBuilder::new()
791 .num_threads(threads)
792 .build()
793 .map_err(|error| Error::resource_exhausted(format!("build schema worker pool: {error}")))?;
794 let results: Vec<Result<()>> = pool.install(|| entries.par_iter().map(validate_one).collect());
795 for result in results {
796 result?;
797 }
798 Ok(())
799}
800
801fn schema_worker_count(concurrency: u32, work_items: usize) -> Result<Option<usize>> {
807 if concurrency == 0 || work_items < 2 {
808 return Ok(None);
809 }
810 let requested = usize::try_from(concurrency)
811 .map_err(|_| Error::resource_exhausted("schema concurrency exceeds this platform"))?;
812 let available = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
813 let threads = requested
814 .min(work_items)
815 .min(available)
816 .min(MAX_SCHEMA_WORKERS);
817 Ok((threads > 1).then_some(threads))
818}
819
820fn finish_schema_commit(
821 collection: &Collection,
822 previous_docs: &Arc<DocumentMap>,
823 previous_revision: u64,
824 previous_schema: &CollectionSchema,
825 next: CollectionState,
826 config: &ConfigBuilder,
827) -> Result<()> {
828 let next = prepare_schema_change(next)?;
829 {
830 let mut storage = collection
831 .inner
832 .storage
833 .lock()
834 .map_err(|_| Error::internal("storage lock poisoned"))?;
835 append_prepared_schema_change(
836 &mut storage,
837 previous_docs,
838 previous_revision,
839 &next,
840 config,
841 )?;
842 }
843 let mut state = collection
844 .inner
845 .state
846 .write()
847 .map_err(|_| Error::internal("collection state lock poisoned"))?;
848 if state.revision != previous_revision || state.schema != *previous_schema {
849 return Err(Error::failed_precondition(
850 "collection generation changed during index construction",
851 ));
852 }
853 let mut storage = collection
854 .inner
855 .storage
856 .lock()
857 .map_err(|_| Error::internal("storage lock poisoned"))?;
858 publish_prepared_schema_change(&mut storage, &mut state, next, config)
859}
860
861fn prepare_schema_change(mut next: CollectionState) -> Result<CollectionState> {
862 let revision = next_revision(next.revision)?;
863 next.revision = revision;
864 next.indexes = Arc::new(IndexRegistry::build(&next.schema, &next.docs, revision)?);
865 next.resource_usage =
866 match next
867 .options
868 .resource_limits
869 .enforce_state(&next.schema, &next.docs, &next.indexes)
870 {
871 Ok(usage) => usage,
872 Err(error) => {
873 next.stats.record_resource_limit_rejection();
874 return Err(error);
875 }
876 };
877 Ok(next)
878}
879
880fn next_revision(current: u64) -> Result<u64> {
881 current
882 .checked_add(1)
883 .ok_or_else(|| Error::resource_exhausted("collection revision overflow"))
884}
885
886#[cfg(test)]
887mod ga_contract;
888#[cfg(test)]
889mod tests;