1pub mod operation_name;
2pub mod optimization;
3pub mod payload_ops;
4pub mod point_ops;
5#[cfg(feature = "staging")]
6pub mod staging;
7pub mod vector_name_ops;
8pub mod vector_ops;
9
10use std::collections::HashSet;
11
12use crate::segment::json_path::JsonPath;
13use crate::segment::types::{PayloadFieldSchema, PointIdType, VectorNameBuf};
14use serde::{Deserialize, Serialize};
15use strum::{EnumDiscriminants, EnumIter};
16
17pub use self::vector_name_ops::{
18 CreateVectorName, DeleteVectorName, VectorNameConfig, VectorNameOperations,
19};
20use crate::shard::PeerId;
21use crate::shard::operations::point_ops::PointOperations;
22
23#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, EnumDiscriminants, Hash)]
24#[strum_discriminants(derive(EnumIter))]
25#[serde(untagged, rename_all = "snake_case")]
26pub enum CollectionUpdateOperations {
27 PointOperation(point_ops::PointOperations),
28 VectorOperation(vector_ops::VectorOperations),
29 PayloadOperation(payload_ops::PayloadOps),
30 FieldIndexOperation(FieldIndexOperations),
31 VectorNameOperation(VectorNameOperations),
32 #[cfg(feature = "staging")]
34 StagingOperation(staging::StagingOperations),
35}
36
37impl CollectionUpdateOperations {
38 pub fn is_upsert_points(&self) -> bool {
39 matches!(
40 self,
41 Self::PointOperation(point_ops::PointOperations::UpsertPoints(_))
42 )
43 }
44
45 pub fn is_delete_points(&self) -> bool {
46 matches!(
47 self,
48 Self::PointOperation(point_ops::PointOperations::DeletePoints { .. })
49 )
50 }
51
52 pub fn point_ids(&self) -> Option<Vec<PointIdType>> {
53 match self {
54 Self::PointOperation(op) => op.point_ids(),
55 Self::VectorOperation(op) => op.point_ids(),
56 Self::PayloadOperation(op) => op.point_ids(),
57 Self::FieldIndexOperation(_) => None,
58 Self::VectorNameOperation(_) => None,
59 #[cfg(feature = "staging")]
60 Self::StagingOperation(_) => None,
61 }
62 }
63
64 pub fn consumes_quota(&self) -> bool {
77 match self {
78 Self::PointOperation(op) => match op {
79 PointOperations::UpsertPoints(_)
80 | PointOperations::UpsertPointsConditional(_)
81 | PointOperations::UpsertPointsRaw(_) => true,
82 PointOperations::DeletePoints { .. } | PointOperations::DeletePointsByFilter(_) => {
83 false
84 }
85 PointOperations::SyncPoints(_) | PointOperations::SyncPointsRaw(_) => false,
86 },
87 Self::VectorOperation(op) => match op {
88 vector_ops::VectorOperations::UpdateVectors(_) => true,
89 vector_ops::VectorOperations::DeleteVectors(..)
91 | vector_ops::VectorOperations::DeleteVectorsByFilter(..) => true,
92 },
93 Self::PayloadOperation(op) => match op {
94 payload_ops::PayloadOps::SetPayload(_)
95 | payload_ops::PayloadOps::OverwritePayload(_) => true,
96 payload_ops::PayloadOps::DeletePayload(_)
98 | payload_ops::PayloadOps::ClearPayload { .. }
99 | payload_ops::PayloadOps::ClearPayloadByFilter(_) => true,
100 },
101 Self::FieldIndexOperation(_) | Self::VectorNameOperation(_) => false,
106 #[cfg(feature = "staging")]
107 Self::StagingOperation(_) => false,
108 }
109 }
110
111 pub fn upsert_point_ids(&self) -> Option<Vec<PointIdType>> {
114 match self {
115 Self::PointOperation(op) => match op {
116 PointOperations::UpsertPoints(op) => Some(op.point_ids()),
117 PointOperations::UpsertPointsConditional(op) => Some(op.points_op.point_ids()),
118 PointOperations::DeletePoints { .. } => None,
119 PointOperations::DeletePointsByFilter(_) => None,
120 PointOperations::SyncPoints(op) => {
121 Some(op.points.iter().map(|point| point.id).collect())
122 }
123 PointOperations::UpsertPointsRaw(points) => {
124 Some(points.iter().map(|point| point.id).collect())
125 }
126 PointOperations::SyncPointsRaw(op) => {
127 Some(op.points.iter().map(|point| point.id).collect())
128 }
129 },
130 Self::VectorOperation(_) => None,
131 Self::PayloadOperation(_) => None,
132 Self::FieldIndexOperation(_) => None,
133 Self::VectorNameOperation(_) => None,
134 #[cfg(feature = "staging")]
135 Self::StagingOperation(_) => None,
136 }
137 }
138
139 pub fn retain_point_ids<F>(&mut self, filter: F)
140 where
141 F: Fn(&PointIdType) -> bool,
142 {
143 match self {
144 Self::PointOperation(op) => op.retain_point_ids(filter),
145 Self::VectorOperation(op) => op.retain_point_ids(filter),
146 Self::PayloadOperation(op) => op.retain_point_ids(filter),
147 Self::FieldIndexOperation(_) => (),
148 Self::VectorNameOperation(_) => (),
149 #[cfg(feature = "staging")]
150 Self::StagingOperation(_) => (),
151 }
152 }
153
154 pub fn retain_vector_names(&mut self, valid: &HashSet<VectorNameBuf>) {
171 match self {
172 Self::PointOperation(op) => op.retain_vector_names(valid),
173 Self::VectorOperation(op) => op.retain_vector_names(valid),
174 Self::PayloadOperation(_) => (),
175 Self::FieldIndexOperation(_) => (),
176 Self::VectorNameOperation(_) => (),
177 #[cfg(feature = "staging")]
178 Self::StagingOperation(_) => (),
179 }
180 }
181
182 pub fn created_vector_name(&self) -> Option<&VectorNameBuf> {
189 match self {
190 Self::VectorNameOperation(VectorNameOperations::CreateVectorName(op)) => {
191 Some(&op.vector_name)
192 }
193 Self::VectorNameOperation(VectorNameOperations::DeleteVectorName(_))
194 | Self::PointOperation(_)
195 | Self::VectorOperation(_)
196 | Self::PayloadOperation(_)
197 | Self::FieldIndexOperation(_) => None,
198 #[cfg(feature = "staging")]
199 Self::StagingOperation(_) => None,
200 }
201 }
202}
203
204#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, EnumDiscriminants, Hash)]
205#[strum_discriminants(derive(EnumIter))]
206#[serde(rename_all = "snake_case")]
207pub enum FieldIndexOperations {
208 CreateIndex(CreateIndex),
210 DeleteIndex(JsonPath),
212}
213
214#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash)]
215#[serde(rename_all = "snake_case")]
216pub struct CreateIndex {
217 pub field_name: JsonPath,
218 pub field_schema: Option<PayloadFieldSchema>,
219}
220
221#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
222pub struct OperationWithClockTag {
223 #[serde(flatten)]
224 pub operation: CollectionUpdateOperations,
225
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub clock_tag: Option<ClockTag>,
228}
229
230impl OperationWithClockTag {
231 pub fn new(
232 operation: impl Into<CollectionUpdateOperations>,
233 clock_tag: Option<ClockTag>,
234 ) -> Self {
235 Self {
236 operation: operation.into(),
237 clock_tag,
238 }
239 }
240}
241
242impl From<CollectionUpdateOperations> for OperationWithClockTag {
243 fn from(operation: CollectionUpdateOperations) -> Self {
244 Self::new(operation, None)
245 }
246}
247
248#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
249pub struct ClockTag {
250 pub peer_id: PeerId,
251 pub clock_id: u32,
252 pub clock_tick: u64,
253 pub token: ClockToken,
255 pub force: bool,
256}
257
258pub type ClockToken = u64;
259
260impl ClockTag {
261 pub fn new(peer_id: PeerId, clock_id: u32, clock_tick: u64) -> Self {
262 let random_token = rand::random();
263 Self::new_with_token(peer_id, clock_id, clock_tick, random_token)
264 }
265
266 pub fn new_with_token(
267 peer_id: PeerId,
268 clock_id: u32,
269 clock_tick: u64,
270 token: ClockToken,
271 ) -> Self {
272 Self {
273 peer_id,
274 clock_id,
275 clock_tick,
276 token,
277 force: false,
278 }
279 }
280
281 pub fn force(mut self, force: bool) -> Self {
282 self.force = force;
283 self
284 }
285}
286
287#[cfg(feature = "api")]
288impl From<api::grpc::qdrant::ClockTag> for ClockTag {
289 fn from(tag: api::grpc::qdrant::ClockTag) -> Self {
290 let api::grpc::qdrant::ClockTag {
291 peer_id,
292 clock_id,
293 clock_tick,
294 token,
295 force,
296 } = tag;
297 Self {
298 peer_id,
299 clock_id,
300 clock_tick,
301 token,
302 force,
303 }
304 }
305}
306
307#[cfg(feature = "api")]
308impl From<ClockTag> for api::grpc::qdrant::ClockTag {
309 fn from(tag: ClockTag) -> Self {
310 let ClockTag {
311 peer_id,
312 clock_id,
313 clock_tick,
314 token,
315 force,
316 } = tag;
317 Self {
318 peer_id,
319 clock_id,
320 clock_tick,
321 token,
322 force,
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 #![expect(clippy::wildcard_enum_match_arm, reason = "test code")]
330
331 use proptest::prelude::*;
332 use crate::segment::types::*;
333
334 use super::payload_ops::*;
335 use super::point_ops::*;
336 use super::vector_ops::*;
337 use super::*;
338
339 proptest::proptest! {
340 #[test]
341 fn operation_with_clock_tag_json(operation in any::<OperationWithClockTag>()) {
342 let input = serde_json::to_string(&operation).unwrap();
344 let output: OperationWithClockTag = serde_json::from_str(&input).unwrap();
345 assert_eq!(operation, output);
346
347 let input = serde_json::to_string(&operation.operation).unwrap();
349 let output: OperationWithClockTag = serde_json::from_str(&input).unwrap();
350 assert_eq!(operation.operation, output.operation);
351
352 match serde_json::to_value(&operation.operation).unwrap() {
355 serde_json::Value::Object(map) if map.len() == 1 => (),
356 _ => panic!("TODO"),
357 };
358 }
359
360 #[test]
361 fn operation_with_clock_tag_cbor(operation in any::<OperationWithClockTag>()) {
362 let input = serde_cbor::to_vec(&operation).unwrap();
364 let output: OperationWithClockTag = serde_cbor::from_slice(&input).unwrap();
365 assert_eq!(operation, output);
366
367 let input = serde_cbor::to_vec(&operation.operation).unwrap();
369 let output: OperationWithClockTag = serde_cbor::from_slice(&input).unwrap();
370 assert_eq!(operation.operation, output.operation);
371 }
372 }
373
374 impl Arbitrary for OperationWithClockTag {
375 type Parameters = ();
376 type Strategy = BoxedStrategy<Self>;
377
378 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
379 any::<(CollectionUpdateOperations, Option<ClockTag>)>()
380 .prop_map(|(operation, clock_tag)| Self::new(operation, clock_tag))
381 .boxed()
382 }
383 }
384
385 impl Arbitrary for ClockTag {
386 type Parameters = ();
387 type Strategy = BoxedStrategy<Self>;
388
389 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
390 any::<(PeerId, u32, u64)>()
391 .prop_map(|(peer_id, clock_id, clock_tick)| {
392 Self::new(peer_id, clock_id, clock_tick)
393 })
394 .boxed()
395 }
396 }
397
398 impl Arbitrary for CollectionUpdateOperations {
399 type Parameters = ();
400 type Strategy = BoxedStrategy<Self>;
401
402 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
403 prop_oneof![
404 any::<point_ops::PointOperations>().prop_map(Self::PointOperation),
405 any::<vector_ops::VectorOperations>().prop_map(Self::VectorOperation),
406 any::<payload_ops::PayloadOps>().prop_map(Self::PayloadOperation),
407 any::<FieldIndexOperations>().prop_map(Self::FieldIndexOperation),
408 any::<VectorNameOperations>().prop_map(Self::VectorNameOperation),
409 ]
410 .boxed()
411 }
412 }
413
414 impl Arbitrary for point_ops::PointOperations {
415 type Parameters = ();
416 type Strategy = BoxedStrategy<Self>;
417
418 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
419 let upsert = Self::UpsertPoints(PointInsertOperationsInternal::PointsList(Vec::new()));
420 let delete = Self::DeletePoints { ids: Vec::new() };
421
422 let delete_by_filter = Self::DeletePointsByFilter(Filter {
423 should: None,
424 min_should: None,
425 must: None,
426 must_not: None,
427 });
428
429 let sync = Self::SyncPoints(PointSyncOperation {
430 from_id: None,
431 to_id: None,
432 points: Vec::new(),
433 });
434
435 let raw_point = PointStructRawPersisted {
437 id: 1.into(),
438 vectors: vec![("dense".to_string(), vec![0, 1, 2, 3, 255])].into(),
439 payload: None,
440 };
441
442 let upsert_raw = Self::UpsertPointsRaw(vec![raw_point.clone()]);
443
444 let sync_raw = Self::SyncPointsRaw(PointSyncRawOperation {
445 from_id: Some(1.into()),
446 to_id: None,
447 points: vec![raw_point],
448 });
449
450 prop_oneof![
451 Just(upsert),
452 Just(delete),
453 Just(delete_by_filter),
454 Just(sync),
455 Just(upsert_raw),
456 Just(sync_raw),
457 ]
458 .boxed()
459 }
460 }
461
462 impl Arbitrary for vector_ops::VectorOperations {
463 type Parameters = ();
464 type Strategy = BoxedStrategy<Self>;
465
466 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
467 let update = Self::UpdateVectors(UpdateVectorsOp {
468 points: Vec::new(),
469 update_filter: None,
470 });
471
472 let delete = Self::DeleteVectors(
473 PointIdsList {
474 points: Vec::new(),
475 #[cfg(feature = "api")]
476 shard_key: None,
477 },
478 Vec::new(),
479 );
480
481 let delete_by_filter = Self::DeleteVectorsByFilter(
482 Filter {
483 should: None,
484 min_should: None,
485 must: None,
486 must_not: None,
487 },
488 Vec::new(),
489 );
490
491 prop_oneof![Just(update), Just(delete), Just(delete_by_filter),].boxed()
492 }
493 }
494
495 impl Arbitrary for payload_ops::PayloadOps {
496 type Parameters = ();
497 type Strategy = BoxedStrategy<Self>;
498
499 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
500 let set = Self::SetPayload(SetPayloadOp {
501 payload: Payload(Default::default()),
502 points: None,
503 filter: None,
504 key: None,
505 });
506
507 let overwrite = Self::OverwritePayload(SetPayloadOp {
508 payload: Payload(Default::default()),
509 points: None,
510 filter: None,
511 key: None,
512 });
513
514 let delete = Self::DeletePayload(DeletePayloadOp {
515 keys: Vec::new(),
516 points: None,
517 filter: None,
518 });
519
520 let clear = Self::ClearPayload { points: Vec::new() };
521
522 let clear_by_filter = Self::ClearPayloadByFilter(Filter {
523 should: None,
524 min_should: None,
525 must: None,
526 must_not: None,
527 });
528
529 prop_oneof![
530 Just(set),
531 Just(overwrite),
532 Just(delete),
533 Just(clear),
534 Just(clear_by_filter),
535 ]
536 .boxed()
537 }
538 }
539
540 impl Arbitrary for FieldIndexOperations {
541 type Parameters = ();
542 type Strategy = BoxedStrategy<Self>;
543
544 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
545 let create = Self::CreateIndex(CreateIndex {
546 field_name: "field_name".parse().unwrap(),
547 field_schema: None,
548 });
549
550 let delete = Self::DeleteIndex("field_name".parse().unwrap());
551
552 prop_oneof![Just(create), Just(delete),].boxed()
553 }
554 }
555
556 impl Arbitrary for VectorNameOperations {
557 type Parameters = ();
558 type Strategy = BoxedStrategy<Self>;
559
560 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
561 use crate::shard::operations::vector_name_ops::{
562 self as vnops, DenseVectorConfig, SparseVectorConfig,
563 };
564
565 let create_dense = Self::CreateVectorName(CreateVectorName {
566 vector_name: "test_vector".into(),
567 config: vnops::VectorNameConfig::dense(DenseVectorConfig {
568 size: 4,
569 distance: Distance::Cosine,
570 multivector_config: None,
571 datatype: None,
572 }),
573 });
574
575 let create_sparse = Self::CreateVectorName(CreateVectorName {
576 vector_name: "sparse_test".into(),
577 config: vnops::VectorNameConfig::sparse(SparseVectorConfig {
578 modifier: None,
579 datatype: None,
580 }),
581 });
582
583 let delete = Self::DeleteVectorName(DeleteVectorName {
584 vector_name: "test_vector".into(),
585 });
586
587 prop_oneof![Just(create_dense), Just(create_sparse), Just(delete),].boxed()
588 }
589 }
590
591 #[test]
592 fn test_delete_by_filter_with_has_id_uuids_cbor_roundtrip() {
593 let uuids: Vec<PointIdType> = vec![ExtendedPointId::Uuid(
594 uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
595 )];
596
597 let filter = Filter {
598 should: None,
599 min_should: None,
600 must: None,
601 must_not: Some(vec![Condition::HasId(HasIdCondition::from(
602 uuids.into_iter().collect::<ahash::AHashSet<_>>(),
603 ))]),
604 };
605
606 let operation = CollectionUpdateOperations::PointOperation(
607 PointOperations::DeletePointsByFilter(filter),
608 );
609
610 let cbor_bytes = serde_cbor::to_vec(&operation).unwrap();
611 let deserialized: CollectionUpdateOperations = serde_cbor::from_slice(&cbor_bytes).unwrap();
612
613 assert_eq!(operation, deserialized);
614 }
615
616 #[test]
617 fn test_wal_roundtrip_delete_by_filter_with_has_id_uuids() {
618 use crate::shard::wal::WalRawRecord;
619
620 let uuids: Vec<PointIdType> = vec![ExtendedPointId::Uuid(
621 uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
622 )];
623
624 let filter = Filter {
625 should: None,
626 min_should: None,
627 must: None,
628 must_not: Some(vec![Condition::HasId(HasIdCondition::from(
629 uuids.into_iter().collect::<ahash::AHashSet<_>>(),
630 ))]),
631 };
632
633 let operation = CollectionUpdateOperations::PointOperation(
634 PointOperations::DeletePointsByFilter(filter),
635 );
636
637 let raw = WalRawRecord::new(&operation).unwrap();
638 let deserialized: CollectionUpdateOperations = raw.deserialize().unwrap();
639
640 assert_eq!(operation, deserialized);
641 }
642}