1use crate::backend::types::{FilterExpr, Scalar};
5use crate::runtime::context::QueryContext;
6use crate::runtime::l0::L0Buffer;
7use crate::runtime::l0_visibility;
8use crate::storage::main_vertex::MainVertexDataset;
9use crate::storage::manager::StorageManager;
10use crate::storage::value_codec::CrdtDecodeMode;
11use anyhow::{Result, anyhow};
12use arrow_array::{Array, BooleanArray, RecordBatch, UInt64Array};
13use lru::LruCache;
14use metrics;
15use std::collections::HashMap;
16use std::num::NonZeroUsize;
17use std::sync::Arc;
18use tokio::sync::Mutex;
19use tracing::{debug, instrument, warn};
20use uni_common::Properties;
21use uni_common::Value;
22use uni_common::core::id::{Eid, Vid};
23use uni_common::core::schema::{DataType, SchemaManager};
24use uni_crdt::Crdt;
25
26pub struct PropertyManager {
27 storage: Arc<StorageManager>,
28 schema_manager: Arc<SchemaManager>,
29 plugin_registry: Arc<uni_plugin::PluginRegistry>,
37 vertex_cache: Option<Mutex<LruCache<(Vid, String), Value>>>,
39 edge_cache: Option<Mutex<LruCache<(uni_common::core::id::Eid, String), Value>>>,
40 cache_capacity: usize,
41}
42
43impl PropertyManager {
44 pub fn new(
50 storage: Arc<StorageManager>,
51 schema_manager: Arc<SchemaManager>,
52 capacity: usize,
53 ) -> Self {
54 Self::with_plugin_registry(
55 storage,
56 schema_manager,
57 capacity,
58 Arc::new(uni_plugin::PluginRegistry::new()),
59 )
60 }
61
62 pub fn with_plugin_registry(
70 storage: Arc<StorageManager>,
71 schema_manager: Arc<SchemaManager>,
72 capacity: usize,
73 plugin_registry: Arc<uni_plugin::PluginRegistry>,
74 ) -> Self {
75 let (vertex_cache, edge_cache) = if capacity == 0 {
77 (None, None)
78 } else {
79 let cap = NonZeroUsize::new(capacity).unwrap();
80 (
81 Some(Mutex::new(LruCache::new(cap))),
82 Some(Mutex::new(LruCache::new(cap))),
83 )
84 };
85
86 Self {
87 storage,
88 schema_manager,
89 plugin_registry,
90 vertex_cache,
91 edge_cache,
92 cache_capacity: capacity,
93 }
94 }
95
96 pub fn cache_size(&self) -> usize {
97 self.cache_capacity
98 }
99
100 pub fn caching_enabled(&self) -> bool {
102 self.cache_capacity > 0
103 }
104
105 pub async fn clear_cache(&self) {
108 if let Some(ref cache) = self.vertex_cache {
109 cache.lock().await.clear();
110 }
111 if let Some(ref cache) = self.edge_cache {
112 cache.lock().await.clear();
113 }
114 }
115
116 pub async fn invalidate_vertex(&self, _vid: Vid) {
118 if let Some(ref cache) = self.vertex_cache {
119 let mut cache = cache.lock().await;
120 cache.clear();
124 }
125 }
126
127 pub async fn invalidate_edge(&self, _eid: uni_common::core::id::Eid) {
129 if let Some(ref cache) = self.edge_cache {
130 let mut cache = cache.lock().await;
131 cache.clear();
133 }
134 }
135
136 #[instrument(skip(self, ctx), level = "trace")]
137 pub async fn get_edge_prop(
138 &self,
139 eid: uni_common::core::id::Eid,
140 prop: &str,
141 ctx: Option<&QueryContext>,
142 ) -> Result<Value> {
143 if l0_visibility::is_edge_deleted(eid, ctx) {
145 return Ok(Value::Null);
146 }
147
148 if let Some(val) = l0_visibility::lookup_edge_prop(eid, prop, ctx) {
150 return Ok(val);
151 }
152
153 if let Some(ref cache) = self.edge_cache {
155 let mut cache = cache.lock().await;
156 if let Some(val) = cache.get(&(eid, prop.to_string())) {
157 debug!(eid = ?eid, prop, "Cache HIT");
158 metrics::counter!("uni_property_cache_hits_total", "type" => "edge").increment(1);
159 return Ok(val.clone());
160 } else {
161 debug!(eid = ?eid, prop, "Cache MISS");
162 metrics::counter!("uni_property_cache_misses_total", "type" => "edge").increment(1);
163 }
164 }
165
166 let all = self.get_all_edge_props_with_ctx(eid, ctx).await?;
168 let val = all
169 .as_ref()
170 .and_then(|props| props.get(prop).cloned())
171 .unwrap_or(Value::Null);
172
173 if let Some(ref cache) = self.edge_cache {
175 let mut cache = cache.lock().await;
176 if let Some(ref props) = all {
177 for (prop_name, prop_val) in props {
178 cache.put((eid, prop_name.clone()), prop_val.clone());
179 }
180 } else {
181 cache.put((eid, prop.to_string()), Value::Null);
183 }
184 }
185
186 Ok(val)
187 }
188
189 pub async fn get_all_edge_props_with_ctx(
190 &self,
191 eid: uni_common::core::id::Eid,
192 ctx: Option<&QueryContext>,
193 ) -> Result<Option<Properties>> {
194 if l0_visibility::is_edge_deleted(eid, ctx) {
196 return Ok(None);
197 }
198
199 let mut final_props = l0_visibility::accumulate_edge_props(eid, ctx).unwrap_or_default();
201
202 let storage_props = self.fetch_all_edge_props_from_storage(eid).await?;
204
205 if final_props.is_empty() && storage_props.is_none() {
207 if l0_visibility::edge_exists_in_l0(eid, ctx) {
208 return Ok(Some(Properties::new()));
209 }
210 return Ok(None);
211 }
212
213 if let Some(sp) = storage_props {
215 for (k, v) in sp {
216 final_props.entry(k).or_insert(v);
217 }
218 }
219
220 Ok(Some(final_props))
221 }
222
223 async fn fetch_all_edge_props_from_storage(&self, eid: Eid) -> Result<Option<Properties>> {
224 self.fetch_all_edge_props_from_storage_with_hint(eid, None)
226 .await
227 }
228
229 async fn fetch_all_edge_props_from_storage_with_hint(
230 &self,
231 eid: Eid,
232 type_name_hint: Option<&str>,
233 ) -> Result<Option<Properties>> {
234 let schema = self.schema_manager.schema();
235 let backend = self.storage.backend();
236
237 let type_names: Vec<&str> = if let Some(hint) = type_name_hint {
239 vec![hint]
240 } else {
241 schema.edge_types.keys().map(|s| s.as_str()).collect()
243 };
244
245 for type_name in type_names {
246 let type_props = schema.properties.get(type_name);
247
248 if self.storage.delta_dataset(type_name, "fwd").is_err() {
251 continue; }
253
254 use crate::backend::table_names;
256 use crate::backend::types::ScanRequest;
257
258 let table_name = table_names::delta_table_name(type_name, "fwd");
259 if !backend.table_exists(&table_name).await.unwrap_or(false) {
260 continue; }
262
263 let base_filter = FilterExpr::equals("eid", Scalar::UInt(eid.as_u64()));
264 let filter_expr = self.storage.apply_version_filter(base_filter);
265
266 let batches = match backend
267 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
268 .await
269 {
270 Ok(b) => b,
271 Err(_) => continue,
272 };
273
274 let mut rows: Vec<(u64, u8, Properties)> = Vec::new();
276
277 for batch in batches {
278 let op_col = match batch.column_by_name("op") {
279 Some(c) => c
280 .as_any()
281 .downcast_ref::<arrow_array::UInt8Array>()
282 .unwrap(),
283 None => continue,
284 };
285 let ver_col = match batch.column_by_name("_version") {
286 Some(c) => c.as_any().downcast_ref::<UInt64Array>().unwrap(),
287 None => continue,
288 };
289
290 for row in 0..batch.num_rows() {
291 let ver = ver_col.value(row);
292 let op = op_col.value(row);
293 let mut props = Properties::new();
294
295 if op != 1 {
296 if let Some(tp) = type_props {
298 for (p_name, p_meta) in tp {
299 if let Some(col) = batch.column_by_name(p_name)
300 && !col.is_null(row)
301 {
302 let val =
303 Self::value_from_column(col.as_ref(), &p_meta.r#type, row)?;
304 props.insert(p_name.clone(), val);
305 }
306 }
307 }
308 }
309 rows.push((ver, op, props));
310 }
311 }
312
313 if rows.is_empty() {
314 continue;
315 }
316
317 rows.sort_by_key(|(ver, _, _)| *ver);
319
320 let mut merged_props: Properties = Properties::new();
324 let mut is_deleted = false;
325
326 for (_, op, props) in rows {
327 if op == 1 {
328 is_deleted = true;
330 merged_props.clear();
331 } else {
332 is_deleted = false;
333 for (p_name, p_val) in props {
334 let is_crdt = type_props
336 .and_then(|tp| tp.get(&p_name))
337 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
338 .unwrap_or(false);
339
340 if is_crdt {
341 if let Some(existing) = merged_props.get(&p_name) {
343 if let Ok(merged) = self.merge_crdt_values(existing, &p_val) {
344 merged_props.insert(p_name, merged);
345 }
346 } else {
347 merged_props.insert(p_name, p_val);
348 }
349 } else {
350 merged_props.insert(p_name, p_val);
352 }
353 }
354 }
355 }
356
357 if is_deleted {
358 return Ok(None);
359 }
360
361 if !merged_props.is_empty() {
362 return Ok(Some(merged_props));
363 }
364 }
365
366 use crate::storage::main_edge::MainEdgeDataset;
373 if let Some(props) = MainEdgeDataset::find_props_by_eid(
374 self.storage.backend(),
375 eid,
376 self.storage.version_high_water_mark(),
377 )
378 .await?
379 {
380 return Ok(Some(props));
381 }
382
383 Ok(None)
384 }
385
386 pub async fn flushed_edge_key_conflict(
405 &self,
406 edge_type: &str,
407 key_values: &[(String, Value)],
408 exclude_eid: Option<Eid>,
409 ) -> Result<bool> {
410 if key_values.is_empty() {
411 return Ok(false);
412 }
413 use crate::backend::table_names;
414 use crate::backend::types::ScanRequest;
415
416 let table_name = table_names::delta_table_name(edge_type, "fwd");
417 let backend = self.storage.backend();
418 if !backend.table_exists(&table_name).await.unwrap_or(false) {
419 return Ok(false);
420 }
421
422 let (probe_prop, probe_val) = &key_values[0];
428 let probe_scalar = match probe_val {
429 Value::String(s) => Scalar::Str(s.clone()),
430 Value::Int(n) => Scalar::Int(*n),
431 Value::Float(f) => Scalar::Float(*f),
432 Value::Bool(b) => Scalar::Bool(*b),
433 _ => return Ok(false),
436 };
437 let base_filter = FilterExpr::all([
438 FilterExpr::equals(probe_prop.as_str(), probe_scalar),
439 FilterExpr::equals("op", Scalar::Int(0)),
440 ]);
441 let filter_expr = self.storage.apply_version_filter(base_filter);
442
443 let batches = backend
444 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
445 .await?;
446
447 let mut candidates: std::collections::HashSet<u64> = std::collections::HashSet::new();
449 for batch in &batches {
450 let Some(eid_col) = batch
451 .column_by_name("eid")
452 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
453 else {
454 continue;
455 };
456 for row in 0..batch.num_rows() {
457 if !eid_col.is_null(row) {
458 candidates.insert(eid_col.value(row));
459 }
460 }
461 }
462
463 let exclude = exclude_eid.map(|e| e.as_u64());
464 for raw in candidates {
465 if Some(raw) == exclude {
466 continue;
467 }
468 let Some(props) = self
469 .fetch_all_edge_props_from_storage_with_hint(Eid::new(raw), Some(edge_type))
470 .await?
471 else {
472 continue; };
474 if key_values.iter().all(|(p, v)| props.get(p) == Some(v)) {
476 return Ok(true);
477 }
478 }
479 Ok(false)
480 }
481
482 pub async fn get_batch_vertex_props(
484 &self,
485 vids: &[Vid],
486 properties: &[&str],
487 ctx: Option<&QueryContext>,
488 ) -> Result<HashMap<Vid, Properties>> {
489 let schema = self.schema_manager.schema();
490 let mut result = HashMap::new();
491 let mut tombstoned: std::collections::HashSet<Vid> = std::collections::HashSet::new();
494 let mut best_version: HashMap<Vid, u64> = HashMap::new();
499 let wants_all = properties.contains(&"_all_props");
503 if vids.is_empty() {
504 return Ok(result);
505 }
506
507 let labels_to_scan: Vec<String> = {
512 let mut needed: std::collections::HashSet<String> = std::collections::HashSet::new();
513 let mut all_resolved = true;
514 for &vid in vids {
515 if let Some(labels) = self.storage.get_labels_from_index(vid) {
516 needed.extend(labels);
517 } else {
518 all_resolved = false;
519 break;
520 }
521 }
522 if all_resolved {
523 needed.into_iter().collect()
524 } else {
525 schema.labels.keys().cloned().collect() }
527 };
528
529 for label_name in &labels_to_scan {
531 let label_schema_props = schema.properties.get(label_name);
534 let valid_props: Vec<&str> = if wants_all {
535 label_schema_props
536 .map(|props| props.keys().map(String::as_str).collect())
537 .unwrap_or_default()
538 } else {
539 properties
540 .iter()
541 .cloned()
542 .filter(|p| label_schema_props.is_some_and(|props| props.contains_key(*p)))
543 .collect()
544 };
545 let ds = match self.storage.vertex_dataset(label_name) {
554 Ok(ds) => ds,
555 Err(_) => continue,
556 };
557 let backend = self.storage.backend();
558 let vtable_name = ds.table_name();
559
560 if !backend.table_exists(&vtable_name).await.unwrap_or(false) {
561 continue; }
563
564 let base_filter =
565 FilterExpr::one_of("_vid", vids.iter().map(|v| Scalar::UInt(v.as_u64())));
566
567 let final_filter = self.storage.apply_version_filter(base_filter);
568
569 let mut columns: Vec<String> = Vec::with_capacity(valid_props.len() + 4);
571 columns.push("_vid".to_string());
572 columns.push("_version".to_string());
573 columns.push("_deleted".to_string());
574 columns.extend(valid_props.iter().map(|s| s.to_string()));
575 columns.push("overflow_json".to_string());
577
578 use crate::backend::types::ScanRequest;
579 let request = ScanRequest::all(&vtable_name)
580 .with_filter(final_filter)
581 .with_columns(columns);
582
583 let batches: Vec<RecordBatch> = match backend.scan(request).await {
584 Ok(b) => b,
585 Err(e) => {
586 warn!(
587 label = %label_name,
588 error = %e,
589 "failed to scan label table, skipping"
590 );
591 continue;
592 }
593 };
594 for batch in batches {
595 let vid_col = match batch
596 .column_by_name("_vid")
597 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>())
598 {
599 Some(c) => c,
600 None => continue,
601 };
602 let del_col = match batch
603 .column_by_name("_deleted")
604 .and_then(|col| col.as_any().downcast_ref::<BooleanArray>())
605 {
606 Some(c) => c,
607 None => continue,
608 };
609 let ver_col = batch
610 .column_by_name("_version")
611 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>());
612
613 for row in 0..batch.num_rows() {
614 let vid = Vid::from(vid_col.value(row));
615 let version = ver_col
616 .map(|c| if c.is_null(row) { 0 } else { c.value(row) })
617 .unwrap_or(0);
618
619 if best_version.get(&vid).is_some_and(|bv| version < *bv) {
621 continue;
622 }
623 best_version.insert(vid, version);
624
625 if del_col.value(row) {
626 result.remove(&vid);
627 tombstoned.insert(vid);
628 continue;
629 }
630
631 tombstoned.remove(&vid);
633 let label_props = schema.properties.get(label_name);
634 let mut props =
635 Self::extract_row_properties(&batch, row, &valid_props, label_props)?;
636 Self::merge_overflow_into_props(&batch, row, properties, &mut props)?;
637 result.insert(vid, props);
638 }
639 }
640 }
641
642 let missing: Vec<Vid> = vids
647 .iter()
648 .copied()
649 .filter(|vid| !result.contains_key(vid) && !tombstoned.contains(vid))
650 .collect();
651 self.main_table_fallback(&missing, &mut result).await?;
652
653 if let Some(ctx) = ctx {
655 for pending_l0_arc in &ctx.pending_flush_l0s {
657 let pending_l0 = pending_l0_arc.read();
658 self.overlay_l0_batch(vids, &pending_l0, properties, &mut result);
659 }
660
661 let l0 = ctx.l0.read();
663 self.overlay_l0_batch(vids, &l0, properties, &mut result);
664
665 if self.storage.version_high_water_mark().is_none()
669 && let Some(tx_l0_arc) = &ctx.transaction_l0
670 {
671 let tx_l0 = tx_l0_arc.read();
672 self.overlay_l0_batch(vids, &tx_l0, properties, &mut result);
673 }
674 }
675
676 Ok(result)
677 }
678
679 fn overlay_l0_batch(
680 &self,
681 vids: &[Vid],
682 l0: &L0Buffer,
683 properties: &[&str],
684 result: &mut HashMap<Vid, Properties>,
685 ) {
686 let schema = self.schema_manager.schema();
687 let wants_all = properties.contains(&"_all_props");
691 for &vid in vids {
692 if l0.vertex_tombstones.contains(&vid) {
694 let tombstone_version = l0.vertex_versions.get(&vid).copied().unwrap_or(0);
701 if self
702 .storage
703 .version_high_water_mark()
704 .is_some_and(|hwm| tombstone_version > hwm)
705 {
706 continue;
707 }
708 result.remove(&vid);
709 continue;
710 }
711 if let Some(l0_props) = l0.vertex_properties.get(&vid) {
713 let entry_version = l0.vertex_versions.get(&vid).copied().unwrap_or(0);
715 if self
716 .storage
717 .version_high_water_mark()
718 .is_some_and(|hwm| entry_version > hwm)
719 {
720 continue;
721 }
722
723 let entry = result.entry(vid).or_default();
724 let labels = l0.get_vertex_labels(vid);
726
727 for (k, v) in l0_props {
728 if wants_all || properties.contains(&k.as_str()) {
729 let is_crdt = labels
731 .and_then(|label_list| {
732 label_list.iter().find_map(|ln| {
733 schema
734 .properties
735 .get(ln)
736 .and_then(|lp| lp.get(k))
737 .filter(|pm| matches!(pm.r#type, DataType::Crdt(_)))
738 })
739 })
740 .is_some();
741
742 if is_crdt {
743 let existing = entry.entry(k.clone()).or_insert(Value::Null);
744 *existing = self.merge_crdt_values(existing, v).unwrap_or(v.clone());
745 } else {
746 entry.insert(k.clone(), v.clone());
747 }
748 }
749 }
750 }
751 }
752 }
753
754 pub async fn get_batch_edge_props(
757 &self,
758 eids: &[uni_common::core::id::Eid],
759 properties: &[&str],
760 ctx: Option<&QueryContext>,
761 ) -> Result<HashMap<Vid, Properties>> {
762 let schema = self.schema_manager.schema();
763 let mut result = HashMap::new();
764 if eids.is_empty() {
765 return Ok(result);
766 }
767 let mut best_version: HashMap<uni_common::core::id::Eid, u64> = HashMap::new();
772
773 let types_to_scan: Vec<String> = {
778 if let Some(ctx) = ctx {
779 let mut needed: std::collections::HashSet<String> =
780 std::collections::HashSet::new();
781 let mut all_resolved = true;
782 for &eid in eids {
783 if let Some(etype) = ctx.l0.read().get_edge_type(eid) {
784 needed.insert(etype.to_string());
785 } else {
786 all_resolved = false;
787 break;
788 }
789 }
790 if all_resolved {
791 needed.into_iter().collect()
792 } else {
793 schema.edge_types.keys().cloned().collect() }
795 } else {
796 schema.edge_types.keys().cloned().collect() }
798 };
799
800 for type_name in &types_to_scan {
802 let type_props = schema.properties.get(type_name);
803 let valid_props: Vec<&str> = properties
804 .iter()
805 .cloned()
806 .filter(|p| type_props.is_some_and(|props| props.contains_key(*p)))
807 .collect();
808 let delta_ds = match self.storage.delta_dataset(type_name, "fwd") {
811 Ok(ds) => ds,
812 Err(_) => continue,
813 };
814 let backend = self.storage.backend();
815 let dtable_name = delta_ds.table_name();
816
817 if !backend.table_exists(&dtable_name).await.unwrap_or(false) {
818 continue; }
820
821 let base_filter =
822 FilterExpr::one_of("eid", eids.iter().map(|e| Scalar::UInt(e.as_u64())));
823
824 let final_filter = self.storage.apply_version_filter(base_filter);
825
826 let mut columns: Vec<String> = Vec::with_capacity(valid_props.len() + 4);
828 columns.push("eid".to_string());
829 columns.push("_version".to_string());
830 columns.push("op".to_string());
831 columns.extend(valid_props.iter().map(|s| s.to_string()));
832 columns.push("overflow_json".to_string());
834
835 use crate::backend::types::ScanRequest;
836 let request = ScanRequest::all(&dtable_name)
837 .with_filter(final_filter)
838 .with_columns(columns);
839
840 let batches: Vec<RecordBatch> = match backend.scan(request).await {
841 Ok(b) => b,
842 Err(e) => {
843 warn!(
844 edge_type = %type_name,
845 error = %e,
846 "failed to scan edge delta table, skipping"
847 );
848 continue;
849 }
850 };
851 for batch in batches {
852 let eid_col = match batch
853 .column_by_name("eid")
854 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>())
855 {
856 Some(c) => c,
857 None => continue,
858 };
859 let op_col = match batch
860 .column_by_name("op")
861 .and_then(|col| col.as_any().downcast_ref::<arrow_array::UInt8Array>())
862 {
863 Some(c) => c,
864 None => continue,
865 };
866 let ver_col = batch
867 .column_by_name("_version")
868 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>());
869
870 for row in 0..batch.num_rows() {
871 let eid = uni_common::core::id::Eid::from(eid_col.value(row));
872 let version = ver_col
873 .map(|c| if c.is_null(row) { 0 } else { c.value(row) })
874 .unwrap_or(0);
875
876 if best_version.get(&eid).is_some_and(|bv| version < *bv) {
878 continue;
879 }
880 best_version.insert(eid, version);
881
882 if op_col.value(row) == 1 {
884 result.remove(&Vid::from(eid.as_u64()));
885 continue;
886 }
887
888 let mut props =
889 Self::extract_row_properties(&batch, row, &valid_props, type_props)?;
890 Self::merge_overflow_into_props(&batch, row, properties, &mut props)?;
891 result.insert(Vid::from(eid.as_u64()), props);
893 }
894 }
895 }
896
897 if let Some(ctx) = ctx {
899 for pending_l0_arc in &ctx.pending_flush_l0s {
901 let pending_l0 = pending_l0_arc.read();
902 self.overlay_l0_edge_batch(eids, &pending_l0, properties, &mut result);
903 }
904
905 let l0 = ctx.l0.read();
907 self.overlay_l0_edge_batch(eids, &l0, properties, &mut result);
908
909 if self.storage.version_high_water_mark().is_none()
913 && let Some(tx_l0_arc) = &ctx.transaction_l0
914 {
915 let tx_l0 = tx_l0_arc.read();
916 self.overlay_l0_edge_batch(eids, &tx_l0, properties, &mut result);
917 }
918 }
919
920 {
941 use crate::storage::main_edge::MainEdgeDataset;
942 for &eid in eids {
943 if l0_visibility::is_edge_deleted(eid, ctx) {
944 continue;
945 }
946 let key = uni_common::core::id::Vid::from(eid.as_u64());
948 let missing_any = match result.get(&key) {
949 None => true,
950 Some(found) => properties.iter().any(|p| !found.contains_key(*p)),
951 };
952 if !missing_any {
953 continue;
954 }
955 if let Some(props) = MainEdgeDataset::find_props_by_eid(
956 self.storage.backend(),
957 eid,
958 self.storage.version_high_water_mark(),
959 )
960 .await?
961 {
962 let entry = result.entry(key).or_default();
963 for (k, v) in props {
964 entry.entry(k).or_insert(v);
965 }
966 }
967 }
968 }
969
970 Ok(result)
971 }
972
973 fn overlay_l0_edge_batch(
974 &self,
975 eids: &[uni_common::core::id::Eid],
976 l0: &L0Buffer,
977 properties: &[&str],
978 result: &mut HashMap<Vid, Properties>,
979 ) {
980 let schema = self.schema_manager.schema();
981 for &eid in eids {
982 let vid_key = Vid::from(eid.as_u64());
983 if l0.tombstones.contains_key(&eid) {
984 result.remove(&vid_key);
985 continue;
986 }
987 if let Some(l0_props) = l0.edge_properties.get(&eid) {
988 let entry_version = l0.edge_versions.get(&eid).copied().unwrap_or(0);
990 if self
991 .storage
992 .version_high_water_mark()
993 .is_some_and(|hwm| entry_version > hwm)
994 {
995 continue;
996 }
997
998 let entry = result.entry(vid_key).or_default();
999 let type_name = l0.get_edge_type(eid);
1001
1002 let include_all = properties.contains(&"_all_props");
1003 for (k, v) in l0_props {
1004 if include_all || properties.contains(&k.as_str()) {
1005 let is_crdt = type_name
1007 .and_then(|tn| schema.properties.get(tn))
1008 .and_then(|tp| tp.get(k))
1009 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1010 .unwrap_or(false);
1011
1012 if is_crdt {
1013 let existing = entry.entry(k.clone()).or_insert(Value::Null);
1014 *existing = self.merge_crdt_values(existing, v).unwrap_or(v.clone());
1015 } else {
1016 entry.insert(k.clone(), v.clone());
1017 }
1018 }
1019 }
1020 }
1021 }
1022 }
1023
1024 pub async fn get_batch_labels(
1026 &self,
1027 vids: &[Vid],
1028 ctx: Option<&QueryContext>,
1029 ) -> Result<HashMap<Vid, Vec<String>>> {
1030 let mut result = HashMap::new();
1031 if vids.is_empty() {
1032 return Ok(result);
1033 }
1034
1035 if let Some(ctx) = ctx {
1037 let mut collect_labels = |l0: &L0Buffer| {
1038 for &vid in vids {
1039 if let Some(labels) = l0.get_vertex_labels(vid) {
1040 result
1041 .entry(vid)
1042 .or_default()
1043 .extend(labels.iter().cloned());
1044 }
1045 }
1046 };
1047
1048 for l0_arc in &ctx.pending_flush_l0s {
1049 collect_labels(&l0_arc.read());
1050 }
1051 collect_labels(&ctx.l0.read());
1052 if let Some(tx_l0_arc) = &ctx.transaction_l0 {
1053 collect_labels(&tx_l0_arc.read());
1054 }
1055 }
1056
1057 let mut vids_needing_lancedb = Vec::new();
1059
1060 fn merge_labels(existing: &mut Vec<String>, new_labels: Vec<String>) {
1062 for l in new_labels {
1063 if !existing.contains(&l) {
1064 existing.push(l);
1065 }
1066 }
1067 }
1068
1069 for &vid in vids {
1070 if result.contains_key(&vid) {
1071 continue; }
1073
1074 if let Some(labels) = self.storage.get_labels_from_index(vid) {
1075 merge_labels(result.entry(vid).or_default(), labels);
1076 } else {
1077 vids_needing_lancedb.push(vid);
1078 }
1079 }
1080
1081 if !vids_needing_lancedb.is_empty() {
1083 let backend = self.storage.backend();
1084 let version = self.storage.version_high_water_mark();
1085 let storage_labels = MainVertexDataset::find_batch_labels_by_vids(
1086 backend,
1087 &vids_needing_lancedb,
1088 version,
1089 )
1090 .await?;
1091
1092 for (vid, labels) in storage_labels {
1093 merge_labels(result.entry(vid).or_default(), labels);
1094 }
1095 }
1096
1097 for labels in result.values_mut() {
1099 labels.sort();
1100 labels.dedup();
1101 }
1102
1103 Ok(result)
1104 }
1105
1106 pub async fn get_all_vertex_props(&self, vid: Vid) -> Result<Properties> {
1107 Ok(self
1108 .get_all_vertex_props_with_ctx(vid, None)
1109 .await?
1110 .unwrap_or_default())
1111 }
1112
1113 pub async fn get_all_vertex_props_with_ctx(
1114 &self,
1115 vid: Vid,
1116 ctx: Option<&QueryContext>,
1117 ) -> Result<Option<Properties>> {
1118 if l0_visibility::is_vertex_deleted(vid, ctx) {
1120 return Ok(None);
1121 }
1122
1123 let l0_props = l0_visibility::accumulate_vertex_props(vid, ctx);
1125
1126 let storage_props_opt = self.fetch_all_props_from_storage(vid).await?;
1128
1129 if l0_props.is_none() && storage_props_opt.is_none() {
1131 return Ok(None);
1132 }
1133
1134 let mut final_props = l0_props.unwrap_or_default();
1135
1136 if let Some(storage_props) = storage_props_opt {
1138 for (k, v) in storage_props {
1139 final_props.entry(k).or_insert(v);
1140 }
1141 }
1142
1143 if let Some(ctx) = ctx {
1146 let labels = l0_visibility::get_vertex_labels(vid, ctx);
1148 for label in &labels {
1149 self.normalize_crdt_properties(&mut final_props, label)?;
1150 }
1151 }
1152
1153 Ok(Some(final_props))
1154 }
1155
1156 pub async fn get_batch_vertex_props_for_label(
1167 &self,
1168 vids: &[Vid],
1169 label: &str,
1170 ctx: Option<&QueryContext>,
1171 ) -> Result<HashMap<Vid, Properties>> {
1172 self.get_batch_vertex_props_for_label_projected(vids, label, ctx, None)
1173 .await
1174 }
1175
1176 pub async fn get_batch_vertex_props_for_label_projected(
1185 &self,
1186 vids: &[Vid],
1187 label: &str,
1188 ctx: Option<&QueryContext>,
1189 requested_props: Option<&[String]>,
1190 ) -> Result<HashMap<Vid, Properties>> {
1191 let mut result: HashMap<Vid, Properties> = HashMap::new();
1192 let mut need_storage: Vec<Vid> = Vec::new();
1193
1194 for &vid in vids {
1196 if l0_visibility::is_vertex_deleted(vid, ctx) {
1197 continue;
1198 }
1199 let l0_props = l0_visibility::accumulate_vertex_props(vid, ctx);
1200 if let Some(props) = l0_props {
1201 result.insert(vid, props);
1202 } else {
1203 need_storage.push(vid);
1204 }
1205 }
1206
1207 if need_storage.is_empty() {
1209 if ctx.is_some() {
1211 for props in result.values_mut() {
1212 self.normalize_crdt_properties(props, label)?;
1213 }
1214 }
1215 return Ok(result);
1216 }
1217
1218 let schema = self.schema_manager.schema();
1220 let label_props = schema.properties.get(label);
1221
1222 let mut prop_names: Vec<String> = Vec::new();
1223 if let Some(props) = label_props {
1224 prop_names = match requested_props {
1225 Some(reqs) => reqs
1229 .iter()
1230 .filter(|r| props.contains_key(r.as_str()))
1231 .cloned()
1232 .collect(),
1233 None => props.keys().cloned().collect(),
1234 };
1235 }
1236
1237 let mut columns: Vec<String> = vec![
1238 "_vid".to_string(),
1239 "_deleted".to_string(),
1240 "_version".to_string(),
1241 ];
1242 columns.extend(prop_names.iter().cloned());
1243 columns.push("overflow_json".to_string());
1244
1245 let base_filter = FilterExpr::one_of(
1247 "_vid",
1248 need_storage.iter().map(|v| Scalar::UInt(v.as_u64())),
1249 );
1250
1251 let filter_expr = self.storage.apply_version_filter(base_filter);
1252
1253 let table_name = crate::backend::table_names::vertex_table_name(label);
1254 let batches: Vec<RecordBatch> = self
1255 .storage
1256 .backend()
1257 .scan(
1258 crate::backend::types::ScanRequest::all(&table_name)
1259 .with_filter(filter_expr.clone())
1260 .with_columns(columns.clone()),
1261 )
1262 .await?;
1263
1264 let prop_name_refs: Vec<&str> = prop_names.iter().map(|s| s.as_str()).collect();
1265
1266 let mut per_vid_best_version: HashMap<Vid, u64> = HashMap::new();
1268 let mut per_vid_props: HashMap<Vid, Properties> = HashMap::new();
1269
1270 for batch in batches {
1271 let vid_col = match batch
1272 .column_by_name("_vid")
1273 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1274 {
1275 Some(c) => c,
1276 None => continue,
1277 };
1278 let deleted_col = match batch
1279 .column_by_name("_deleted")
1280 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
1281 {
1282 Some(c) => c,
1283 None => continue,
1284 };
1285 let version_col = match batch
1286 .column_by_name("_version")
1287 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1288 {
1289 Some(c) => c,
1290 None => continue,
1291 };
1292
1293 for row in 0..batch.num_rows() {
1294 let vid = Vid::from(vid_col.value(row));
1295 let version = version_col.value(row);
1296
1297 if deleted_col.value(row) {
1298 if per_vid_best_version
1299 .get(&vid)
1300 .is_none_or(|&best| version >= best)
1301 {
1302 per_vid_best_version.insert(vid, version);
1303 per_vid_props.remove(&vid);
1304 }
1305 continue;
1306 }
1307
1308 let mut current_props =
1309 Self::extract_row_properties(&batch, row, &prop_name_refs, label_props)?;
1310
1311 if let Some(overflow_props) = Self::extract_overflow_properties(&batch, row)? {
1312 for (k, v) in overflow_props {
1313 current_props.entry(k).or_insert(v);
1314 }
1315 }
1316
1317 let best = per_vid_best_version.get(&vid).copied();
1318 let mut best_opt = best;
1319 let mut merged = per_vid_props.remove(&vid);
1320 self.merge_versioned_props(
1321 current_props,
1322 version,
1323 &mut best_opt,
1324 &mut merged,
1325 label_props,
1326 )?;
1327 if let Some(v) = best_opt {
1328 per_vid_best_version.insert(vid, v);
1329 }
1330 if let Some(p) = merged {
1331 per_vid_props.insert(vid, p);
1332 }
1333 }
1334 }
1335
1336 for (vid, storage_props) in per_vid_props {
1338 let entry = result.entry(vid).or_default();
1339 for (k, v) in storage_props {
1340 entry.entry(k).or_insert(v);
1341 }
1342 }
1343
1344 let missing: Vec<Vid> = need_storage
1351 .iter()
1352 .copied()
1353 .filter(|vid| !result.contains_key(vid) && !per_vid_best_version.contains_key(vid))
1354 .collect();
1355 self.main_table_fallback(&missing, &mut result).await?;
1356
1357 if ctx.is_some() {
1359 for props in result.values_mut() {
1360 self.normalize_crdt_properties(props, label)?;
1361 }
1362 }
1363
1364 Ok(result)
1365 }
1366
1367 async fn main_table_fallback(
1382 &self,
1383 missing: &[Vid],
1384 out: &mut HashMap<Vid, Properties>,
1385 ) -> Result<()> {
1386 if missing.is_empty() {
1387 return Ok(());
1388 }
1389 let main_props = MainVertexDataset::find_batch_props_by_vids(
1390 self.storage.backend(),
1391 missing,
1392 self.storage.version_high_water_mark(),
1393 )
1394 .await?;
1395 for (vid, props) in main_props {
1396 out.entry(vid).or_insert(props);
1397 }
1398 Ok(())
1399 }
1400
1401 pub async fn get_batch_edge_props_for_type(
1415 &self,
1416 eids: &[Eid],
1417 type_name: &str,
1418 ctx: Option<&QueryContext>,
1419 ) -> Result<HashMap<Eid, Properties>> {
1420 use crate::backend::table_names;
1421 use crate::backend::types::ScanRequest;
1422
1423 let mut result: HashMap<Eid, Properties> = HashMap::new();
1424 if eids.is_empty() {
1425 return Ok(result);
1426 }
1427
1428 let mut need_storage: Vec<Eid> = Vec::new();
1431 for &eid in eids {
1432 if l0_visibility::is_edge_deleted(eid, ctx) {
1433 continue;
1434 }
1435 let l0_props = l0_visibility::accumulate_edge_props(eid, ctx);
1436 if let Some(props) = l0_props {
1440 result.insert(eid, props);
1441 }
1442 need_storage.push(eid);
1443 }
1444
1445 if need_storage.is_empty() {
1446 return Ok(result);
1447 }
1448
1449 let schema = self.schema_manager.schema();
1451 let type_props = schema.properties.get(type_name);
1452
1453 if self.storage.delta_dataset(type_name, "fwd").is_err() {
1454 return Ok(result);
1455 }
1456
1457 let table_name = table_names::delta_table_name(type_name, "fwd");
1458 let backend = self.storage.backend();
1459 if !backend.table_exists(&table_name).await.unwrap_or(false) {
1460 return Ok(result);
1461 }
1462
1463 let base_filter =
1464 FilterExpr::one_of("eid", need_storage.iter().map(|e| Scalar::UInt(e.as_u64())));
1465 let filter_expr = self.storage.apply_version_filter(base_filter);
1466
1467 let batches = match backend
1468 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
1469 .await
1470 {
1471 Ok(b) => b,
1472 Err(_) => return Ok(result), };
1474
1475 let mut per_eid_rows: HashMap<Eid, Vec<(u64, u8, Properties)>> = HashMap::new();
1477 for batch in batches {
1478 let eid_col = match batch
1479 .column_by_name("eid")
1480 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1481 {
1482 Some(c) => c,
1483 None => continue,
1484 };
1485 let op_col = match batch
1486 .column_by_name("op")
1487 .and_then(|c| c.as_any().downcast_ref::<arrow_array::UInt8Array>())
1488 {
1489 Some(c) => c,
1490 None => continue,
1491 };
1492 let ver_col = match batch
1493 .column_by_name("_version")
1494 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1495 {
1496 Some(c) => c,
1497 None => continue,
1498 };
1499
1500 for row in 0..batch.num_rows() {
1501 let eid = Eid::from(eid_col.value(row));
1502 let ver = ver_col.value(row);
1503 let op = op_col.value(row);
1504 let mut props = Properties::new();
1505
1506 if op != 1
1507 && let Some(tp) = type_props
1508 {
1509 for (p_name, p_meta) in tp {
1510 if let Some(col) = batch.column_by_name(p_name)
1511 && !col.is_null(row)
1512 {
1513 let val = Self::value_from_column(col.as_ref(), &p_meta.r#type, row)?;
1514 props.insert(p_name.clone(), val);
1515 }
1516 }
1517 }
1518 per_eid_rows.entry(eid).or_default().push((ver, op, props));
1519 }
1520 }
1521
1522 for (eid, mut rows) in per_eid_rows {
1523 rows.sort_by_key(|(ver, _, _)| *ver);
1524
1525 let mut merged_props: Properties = Properties::new();
1526 let mut is_deleted = false;
1527
1528 for (_, op, props) in rows {
1529 if op == 1 {
1530 is_deleted = true;
1531 merged_props.clear();
1532 } else {
1533 is_deleted = false;
1534 for (p_name, p_val) in props {
1535 let is_crdt = type_props
1536 .and_then(|tp| tp.get(&p_name))
1537 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1538 .unwrap_or(false);
1539 if is_crdt {
1540 if let Some(existing) = merged_props.get(&p_name) {
1541 if let Ok(merged) = self.merge_crdt_values(existing, &p_val) {
1542 merged_props.insert(p_name, merged);
1543 }
1544 } else {
1545 merged_props.insert(p_name, p_val);
1546 }
1547 } else {
1548 merged_props.insert(p_name, p_val);
1549 }
1550 }
1551 }
1552 }
1553
1554 if is_deleted {
1555 result.remove(&eid);
1559 continue;
1560 }
1561
1562 let entry = result.entry(eid).or_default();
1565 for (k, v) in merged_props {
1566 entry.entry(k).or_insert(v);
1567 }
1568 }
1569
1570 use crate::storage::main_edge::MainEdgeDataset;
1581 for &eid in eids {
1582 if l0_visibility::is_edge_deleted(eid, ctx) {
1583 continue;
1584 }
1585 let needs_fallback = result.get(&eid).is_none_or(|p| p.is_empty());
1586 if !needs_fallback {
1587 continue;
1588 }
1589 if let Some(props) = MainEdgeDataset::find_props_by_eid(
1590 self.storage.backend(),
1591 eid,
1592 self.storage.version_high_water_mark(),
1593 )
1594 .await?
1595 {
1596 let entry = result.entry(eid).or_default();
1597 for (k, v) in props {
1598 entry.entry(k).or_insert(v);
1599 }
1600 }
1601 }
1602
1603 Ok(result)
1604 }
1605
1606 fn normalize_crdt_properties(&self, props: &mut Properties, label: &str) -> Result<()> {
1610 let schema = self.schema_manager.schema();
1611 let label_props = match schema.properties.get(label) {
1612 Some(p) => p,
1613 None => return Ok(()),
1614 };
1615
1616 for (prop_name, prop_meta) in label_props {
1617 if let DataType::Crdt(_) = prop_meta.r#type
1618 && let Some(val) = props.get_mut(prop_name)
1619 {
1620 *val = Value::from(Self::parse_crdt_value(val)?);
1621 }
1622 }
1623
1624 Ok(())
1625 }
1626
1627 fn extract_row_properties(
1629 batch: &RecordBatch,
1630 row: usize,
1631 prop_names: &[&str],
1632 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1633 ) -> Result<Properties> {
1634 let mut props = Properties::new();
1635 for name in prop_names {
1636 let col = match batch.column_by_name(name) {
1637 Some(col) => col,
1638 None => continue,
1639 };
1640 if col.is_null(row) {
1641 continue;
1642 }
1643 if let Some(prop_meta) = label_props.and_then(|p| p.get(*name)) {
1644 let val = Self::value_from_column(col.as_ref(), &prop_meta.r#type, row)?;
1645 props.insert((*name).to_string(), val);
1646 }
1647 }
1648 Ok(props)
1649 }
1650
1651 fn extract_overflow_properties(batch: &RecordBatch, row: usize) -> Result<Option<Properties>> {
1656 use arrow_array::LargeBinaryArray;
1657
1658 let overflow_col = match batch.column_by_name("overflow_json") {
1659 Some(col) => col,
1660 None => return Ok(None), };
1662
1663 if overflow_col.is_null(row) {
1664 return Ok(None);
1665 }
1666
1667 let binary_array = overflow_col
1668 .as_any()
1669 .downcast_ref::<LargeBinaryArray>()
1670 .ok_or_else(|| anyhow!("overflow_json is not LargeBinaryArray"))?;
1671
1672 let jsonb_bytes = binary_array.value(row);
1673
1674 match uni_common::cypher_value_codec::decode(jsonb_bytes)
1678 .map_err(|e| anyhow!("Failed to decode CypherValue: {}", e))?
1679 {
1680 Value::Map(map) => Ok(Some(map)),
1681 Value::Null => Ok(None),
1682 other => Err(anyhow!(
1683 "overflow_json decoded to a non-map value: {other:?}"
1684 )),
1685 }
1686 }
1687
1688 fn merge_overflow_into_props(
1695 batch: &RecordBatch,
1696 row: usize,
1697 properties: &[&str],
1698 props: &mut Properties,
1699 ) -> Result<()> {
1700 use arrow_array::LargeBinaryArray;
1701
1702 let overflow_col = match batch.column_by_name("overflow_json") {
1703 Some(col) if !col.is_null(row) => col,
1704 _ => return Ok(()),
1705 };
1706
1707 if properties.contains(&"overflow_json")
1709 && let Some(binary_array) = overflow_col.as_any().downcast_ref::<LargeBinaryArray>()
1710 {
1711 let jsonb_bytes = binary_array.value(row);
1712 let bytes_list: Vec<Value> =
1713 jsonb_bytes.iter().map(|&b| Value::Int(b as i64)).collect();
1714 props.insert("overflow_json".to_string(), Value::List(bytes_list));
1715 }
1716
1717 let wants_all = properties.contains(&"_all_props");
1720 if let Some(overflow_props) = Self::extract_overflow_properties(batch, row)? {
1721 for (k, v) in overflow_props {
1722 if wants_all || properties.contains(&k.as_str()) {
1723 props.entry(k).or_insert(v);
1724 }
1725 }
1726 }
1727
1728 Ok(())
1729 }
1730
1731 fn merge_crdt_into(
1733 &self,
1734 target: &mut Properties,
1735 source: Properties,
1736 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1737 crdt_only: bool,
1738 ) -> Result<()> {
1739 for (k, v) in source {
1740 if let Some(prop_meta) = label_props.and_then(|p| p.get(&k)) {
1741 if let DataType::Crdt(_) = prop_meta.r#type {
1742 let existing_v = target.entry(k).or_insert(Value::Null);
1743 *existing_v = self.merge_crdt_values(existing_v, &v)?;
1744 } else if !crdt_only {
1745 target.insert(k, v);
1746 }
1747 }
1748 }
1749 Ok(())
1750 }
1751
1752 fn merge_versioned_props(
1754 &self,
1755 current_props: Properties,
1756 version: u64,
1757 best_version: &mut Option<u64>,
1758 best_props: &mut Option<Properties>,
1759 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1760 ) -> Result<()> {
1761 if best_version.is_none_or(|best| version > best) {
1762 if let Some(mut existing_props) = best_props.take() {
1764 let mut merged = current_props;
1766 for (k, v) in merged.iter_mut() {
1767 if let Some(prop_meta) = label_props.and_then(|p| p.get(k))
1768 && let DataType::Crdt(_) = prop_meta.r#type
1769 && let Some(existing_val) = existing_props.remove(k)
1770 {
1771 *v = self.merge_crdt_values(v, &existing_val)?;
1772 }
1773 }
1774 *best_props = Some(merged);
1775 } else {
1776 *best_props = Some(current_props);
1777 }
1778 *best_version = Some(version);
1779 } else if Some(version) == *best_version {
1780 if let Some(existing_props) = best_props.as_mut() {
1782 self.merge_crdt_into(existing_props, current_props, label_props, false)?;
1783 } else {
1784 *best_props = Some(current_props);
1785 }
1786 } else {
1787 if let Some(existing_props) = best_props.as_mut() {
1789 self.merge_crdt_into(existing_props, current_props, label_props, true)?;
1790 }
1791 }
1792 Ok(())
1793 }
1794
1795 async fn fetch_all_props_from_storage(&self, vid: Vid) -> Result<Option<Properties>> {
1796 let schema = self.schema_manager.schema();
1799 let mut merged_props: Option<Properties> = None;
1800 let mut global_best_version: Option<u64> = None;
1801
1802 let label_names: Vec<String> = if let Some(labels) = self.storage.get_labels_from_index(vid)
1804 {
1805 labels
1806 } else {
1807 schema.labels.keys().cloned().collect() };
1809
1810 for label_name in &label_names {
1811 let label_props = schema.properties.get(label_name);
1812
1813 let mut prop_names: Vec<String> = Vec::new();
1815 if let Some(props) = label_props {
1816 prop_names = props.keys().cloned().collect();
1817 }
1818
1819 let mut columns: Vec<String> = vec!["_deleted".to_string(), "_version".to_string()];
1821 columns.extend(prop_names.iter().cloned());
1822 columns.push("overflow_json".to_string());
1824
1825 let base_filter = FilterExpr::equals("_vid", Scalar::UInt(vid.as_u64()));
1827
1828 let filter_expr = self.storage.apply_version_filter(base_filter);
1829
1830 let table_name = crate::backend::table_names::vertex_table_name(label_name);
1831 let batches: Vec<RecordBatch> = match self
1832 .storage
1833 .backend()
1834 .scan(
1835 crate::backend::types::ScanRequest::all(&table_name)
1836 .with_filter(filter_expr.clone())
1837 .with_columns(columns.clone()),
1838 )
1839 .await
1840 {
1841 Ok(b) => b,
1842 Err(_) => continue,
1843 };
1844
1845 let prop_name_refs: Vec<&str> = prop_names.iter().map(|s| s.as_str()).collect();
1847
1848 for batch in batches {
1849 let deleted_col = match batch
1850 .column_by_name("_deleted")
1851 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
1852 {
1853 Some(c) => c,
1854 None => continue,
1855 };
1856 let version_col = match batch
1857 .column_by_name("_version")
1858 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1859 {
1860 Some(c) => c,
1861 None => continue,
1862 };
1863
1864 for row in 0..batch.num_rows() {
1865 let version = version_col.value(row);
1866
1867 if deleted_col.value(row) {
1868 if global_best_version.is_none_or(|best| version >= best) {
1869 global_best_version = Some(version);
1870 merged_props = None;
1871 }
1872 continue;
1873 }
1874
1875 let mut current_props =
1876 Self::extract_row_properties(&batch, row, &prop_name_refs, label_props)?;
1877
1878 if let Some(overflow_props) = Self::extract_overflow_properties(&batch, row)? {
1880 for (k, v) in overflow_props {
1882 current_props.entry(k).or_insert(v);
1883 }
1884 }
1885
1886 self.merge_versioned_props(
1887 current_props,
1888 version,
1889 &mut global_best_version,
1890 &mut merged_props,
1891 label_props,
1892 )?;
1893 }
1894 }
1895 }
1896
1897 if merged_props.is_none()
1902 && global_best_version.is_none()
1903 && let Some(main_props) = MainVertexDataset::find_props_by_vid(
1904 self.storage.backend(),
1905 vid,
1906 self.storage.version_high_water_mark(),
1907 )
1908 .await?
1909 {
1910 return Ok(Some(main_props));
1911 }
1912
1913 Ok(merged_props)
1914 }
1915
1916 pub async fn get_vertex_prop(&self, vid: Vid, prop: &str) -> Result<Value> {
1917 self.get_vertex_prop_with_ctx(vid, prop, None).await
1918 }
1919
1920 #[instrument(skip(self, ctx), level = "trace")]
1921 pub async fn get_vertex_prop_with_ctx(
1922 &self,
1923 vid: Vid,
1924 prop: &str,
1925 ctx: Option<&QueryContext>,
1926 ) -> Result<Value> {
1927 if l0_visibility::is_vertex_deleted(vid, ctx) {
1929 return Ok(Value::Null);
1930 }
1931
1932 let schema = self.schema_manager.schema();
1935 let labels = ctx
1936 .map(|c| l0_visibility::get_vertex_labels(vid, c))
1937 .unwrap_or_default();
1938
1939 let is_crdt = if !labels.is_empty() {
1940 labels.iter().any(|ln| {
1942 schema
1943 .properties
1944 .get(ln)
1945 .and_then(|lp| lp.get(prop))
1946 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1947 .unwrap_or(false)
1948 })
1949 } else {
1950 schema.properties.values().any(|label_props| {
1952 label_props
1953 .get(prop)
1954 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1955 .unwrap_or(false)
1956 })
1957 };
1958
1959 if is_crdt {
1961 let l0_val = self.accumulate_crdt_from_l0(vid, prop, ctx)?;
1963 return self.finalize_crdt_lookup(vid, prop, l0_val).await;
1964 }
1965
1966 if let Some(val) = l0_visibility::lookup_vertex_prop(vid, prop, ctx) {
1968 return Ok(val);
1969 }
1970
1971 if let Some(ref cache) = self.vertex_cache {
1973 let mut cache = cache.lock().await;
1974 if let Some(val) = cache.get(&(vid, prop.to_string())) {
1975 debug!(vid = ?vid, prop, "Cache HIT");
1976 metrics::counter!("uni_property_cache_hits_total", "type" => "vertex").increment(1);
1977 return Ok(val.clone());
1978 } else {
1979 debug!(vid = ?vid, prop, "Cache MISS");
1980 metrics::counter!("uni_property_cache_misses_total", "type" => "vertex")
1981 .increment(1);
1982 }
1983 }
1984
1985 let storage_val = self.fetch_prop_from_storage(vid, prop).await?;
1987
1988 if let Some(ref cache) = self.vertex_cache {
1990 let mut cache = cache.lock().await;
1991 cache.put((vid, prop.to_string()), storage_val.clone());
1992 }
1993
1994 Ok(storage_val)
1995 }
1996
1997 fn accumulate_crdt_from_l0(
1999 &self,
2000 vid: Vid,
2001 prop: &str,
2002 ctx: Option<&QueryContext>,
2003 ) -> Result<Value> {
2004 let mut merged = Value::Null;
2005 l0_visibility::visit_l0_buffers(ctx, |l0| {
2006 if let Some(props) = l0.vertex_properties.get(&vid)
2007 && let Some(val) = props.get(prop)
2008 {
2009 if let Ok(new_merged) = self.merge_crdt_values(&merged, val) {
2011 merged = new_merged;
2012 }
2013 }
2014 false });
2016 Ok(merged)
2017 }
2018
2019 async fn finalize_crdt_lookup(&self, vid: Vid, prop: &str, l0_val: Value) -> Result<Value> {
2021 let cached_val = if let Some(ref cache) = self.vertex_cache {
2023 let mut cache = cache.lock().await;
2024 cache.get(&(vid, prop.to_string())).cloned()
2025 } else {
2026 None
2027 };
2028
2029 if let Some(val) = cached_val {
2030 let merged = self.merge_crdt_values(&val, &l0_val)?;
2031 return Ok(merged);
2032 }
2033
2034 let storage_val = self.fetch_prop_from_storage(vid, prop).await?;
2036
2037 if let Some(ref cache) = self.vertex_cache {
2039 let mut cache = cache.lock().await;
2040 cache.put((vid, prop.to_string()), storage_val.clone());
2041 }
2042
2043 self.merge_crdt_values(&storage_val, &l0_val)
2045 }
2046
2047 async fn fetch_prop_from_storage(&self, vid: Vid, prop: &str) -> Result<Value> {
2048 let schema = self.schema_manager.schema();
2051 let mut best_version: Option<u64> = None;
2052 let mut best_value: Option<Value> = None;
2053
2054 let label_names: Vec<String> = if let Some(labels) = self.storage.get_labels_from_index(vid)
2056 {
2057 labels
2058 } else {
2059 schema.labels.keys().cloned().collect() };
2061
2062 for label_name in &label_names {
2063 let prop_meta = schema
2065 .properties
2066 .get(label_name)
2067 .and_then(|props| props.get(prop));
2068
2069 let base_filter = FilterExpr::equals("_vid", Scalar::UInt(vid.as_u64()));
2073
2074 let filter_expr = self.storage.apply_version_filter(base_filter);
2075
2076 let mut columns = vec![
2078 "_deleted".to_string(),
2079 "_version".to_string(),
2080 "overflow_json".to_string(),
2081 ];
2082
2083 if prop_meta.is_some() {
2085 columns.push(prop.to_string());
2086 }
2087
2088 let table_name = crate::backend::table_names::vertex_table_name(label_name);
2089 let batches: Vec<RecordBatch> = match self
2090 .storage
2091 .backend()
2092 .scan(
2093 crate::backend::types::ScanRequest::all(&table_name)
2094 .with_filter(filter_expr.clone())
2095 .with_columns(columns),
2096 )
2097 .await
2098 {
2099 Ok(b) => b,
2100 Err(_) => continue,
2101 };
2102
2103 for batch in batches {
2104 let deleted_col = match batch
2105 .column_by_name("_deleted")
2106 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
2107 {
2108 Some(c) => c,
2109 None => continue,
2110 };
2111 let version_col = match batch
2112 .column_by_name("_version")
2113 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
2114 {
2115 Some(c) => c,
2116 None => continue,
2117 };
2118 for row in 0..batch.num_rows() {
2119 let version = version_col.value(row);
2120
2121 if deleted_col.value(row) {
2122 if best_version.is_none_or(|best| version >= best) {
2123 best_version = Some(version);
2124 best_value = None;
2125 }
2126 continue;
2127 }
2128
2129 let mut val = None;
2131 if let Some(meta) = prop_meta
2132 && let Some(col) = batch.column_by_name(prop)
2133 {
2134 val = Some(if col.is_null(row) {
2135 Value::Null
2136 } else {
2137 Self::value_from_column(col, &meta.r#type, row)?
2138 });
2139 }
2140
2141 if val.is_none()
2143 && let Some(overflow_props) =
2144 Self::extract_overflow_properties(&batch, row)?
2145 && let Some(overflow_val) = overflow_props.get(prop)
2146 {
2147 val = Some(overflow_val.clone());
2148 }
2149
2150 if let Some(v) = val {
2152 if let Some(meta) = prop_meta {
2153 self.merge_prop_value(
2155 v,
2156 version,
2157 &meta.r#type,
2158 &mut best_version,
2159 &mut best_value,
2160 )?;
2161 } else {
2162 if best_version.is_none_or(|best| version >= best) {
2164 best_version = Some(version);
2165 best_value = Some(v);
2166 }
2167 }
2168 }
2169 }
2170 }
2171 }
2172
2173 if best_value.is_none()
2179 && best_version.is_none()
2180 && let Some(main_props) = MainVertexDataset::find_props_by_vid(
2181 self.storage.backend(),
2182 vid,
2183 self.storage.version_high_water_mark(),
2184 )
2185 .await?
2186 {
2187 return Ok(main_props.get(prop).cloned().unwrap_or(Value::Null));
2188 }
2189
2190 Ok(best_value.unwrap_or(Value::Null))
2191 }
2192
2193 pub fn value_from_column(col: &dyn Array, data_type: &DataType, row: usize) -> Result<Value> {
2195 crate::storage::value_codec::decode_column_value(
2196 col,
2197 data_type,
2198 row,
2199 CrdtDecodeMode::Strict,
2200 )
2201 }
2202
2203 pub fn merge_crdt_values(&self, a: &Value, b: &Value) -> Result<Value> {
2216 if a.is_null() {
2220 return Self::parse_crdt_value(b).map(Value::from);
2221 }
2222 if b.is_null() {
2223 return Self::parse_crdt_value(a).map(Value::from);
2224 }
2225
2226 let a_parsed = Self::parse_crdt_value(a)?;
2227 let b_parsed = Self::parse_crdt_value(b)?;
2228
2229 let mut crdt_a: Crdt = serde_json::from_value(a_parsed)?;
2230 let crdt_b: Crdt = serde_json::from_value(b_parsed)?;
2231 crdt_a
2237 .merge_via_registry(&crdt_b, &self.plugin_registry)
2238 .map_err(|e| anyhow::anyhow!("{e}"))?;
2239 Ok(Value::from(serde_json::to_value(crdt_a)?))
2240 }
2241
2242 fn parse_crdt_value(val: &Value) -> Result<serde_json::Value> {
2245 if let Value::String(s) = val {
2246 serde_json::from_str(s).map_err(|e| anyhow!("Failed to parse CRDT JSON string: {}", e))
2248 } else {
2249 Ok(serde_json::Value::from(val.clone()))
2251 }
2252 }
2253
2254 fn merge_prop_value(
2256 &self,
2257 val: Value,
2258 version: u64,
2259 data_type: &DataType,
2260 best_version: &mut Option<u64>,
2261 best_value: &mut Option<Value>,
2262 ) -> Result<()> {
2263 if let DataType::Crdt(_) = data_type {
2264 self.merge_crdt_prop_value(val, version, best_version, best_value)
2265 } else {
2266 if best_version.is_none_or(|best| version >= best) {
2268 *best_version = Some(version);
2269 *best_value = Some(val);
2270 }
2271 Ok(())
2272 }
2273 }
2274
2275 fn merge_crdt_prop_value(
2277 &self,
2278 val: Value,
2279 version: u64,
2280 best_version: &mut Option<u64>,
2281 best_value: &mut Option<Value>,
2282 ) -> Result<()> {
2283 if best_version.is_none_or(|best| version > best) {
2284 if let Some(existing) = best_value.take() {
2286 *best_value = Some(self.merge_crdt_values(&val, &existing)?);
2287 } else {
2288 *best_value = Some(val);
2289 }
2290 *best_version = Some(version);
2291 } else if Some(version) == *best_version {
2292 let existing = best_value.get_or_insert(Value::Null);
2294 *existing = self.merge_crdt_values(existing, &val)?;
2295 } else {
2296 if let Some(existing) = best_value.as_mut() {
2298 *existing = self.merge_crdt_values(existing, &val)?;
2299 }
2300 }
2301 Ok(())
2302 }
2303}