1use crate::runtime::context::QueryContext;
5use crate::runtime::l0::L0Buffer;
6use crate::runtime::l0_visibility;
7use crate::storage::main_vertex::MainVertexDataset;
8use crate::storage::manager::StorageManager;
9use crate::storage::value_codec::CrdtDecodeMode;
10use anyhow::{Result, anyhow};
11use arrow_array::{Array, BooleanArray, RecordBatch, UInt64Array};
12use lru::LruCache;
13use metrics;
14use std::collections::HashMap;
15use std::num::NonZeroUsize;
16use std::sync::Arc;
17use tokio::sync::Mutex;
18use tracing::{debug, instrument, warn};
19use uni_common::Properties;
20use uni_common::Value;
21use uni_common::core::id::{Eid, Vid};
22use uni_common::core::schema::{DataType, SchemaManager};
23use uni_crdt::Crdt;
24
25pub struct PropertyManager {
26 storage: Arc<StorageManager>,
27 schema_manager: Arc<SchemaManager>,
28 plugin_registry: Arc<uni_plugin::PluginRegistry>,
36 vertex_cache: Option<Mutex<LruCache<(Vid, String), Value>>>,
38 edge_cache: Option<Mutex<LruCache<(uni_common::core::id::Eid, String), Value>>>,
39 cache_capacity: usize,
40}
41
42impl PropertyManager {
43 pub fn new(
49 storage: Arc<StorageManager>,
50 schema_manager: Arc<SchemaManager>,
51 capacity: usize,
52 ) -> Self {
53 Self::with_plugin_registry(
54 storage,
55 schema_manager,
56 capacity,
57 Arc::new(uni_plugin::PluginRegistry::new()),
58 )
59 }
60
61 pub fn with_plugin_registry(
69 storage: Arc<StorageManager>,
70 schema_manager: Arc<SchemaManager>,
71 capacity: usize,
72 plugin_registry: Arc<uni_plugin::PluginRegistry>,
73 ) -> Self {
74 let (vertex_cache, edge_cache) = if capacity == 0 {
76 (None, None)
77 } else {
78 let cap = NonZeroUsize::new(capacity).unwrap();
79 (
80 Some(Mutex::new(LruCache::new(cap))),
81 Some(Mutex::new(LruCache::new(cap))),
82 )
83 };
84
85 Self {
86 storage,
87 schema_manager,
88 plugin_registry,
89 vertex_cache,
90 edge_cache,
91 cache_capacity: capacity,
92 }
93 }
94
95 pub fn cache_size(&self) -> usize {
96 self.cache_capacity
97 }
98
99 pub fn caching_enabled(&self) -> bool {
101 self.cache_capacity > 0
102 }
103
104 pub async fn clear_cache(&self) {
107 if let Some(ref cache) = self.vertex_cache {
108 cache.lock().await.clear();
109 }
110 if let Some(ref cache) = self.edge_cache {
111 cache.lock().await.clear();
112 }
113 }
114
115 pub async fn invalidate_vertex(&self, _vid: Vid) {
117 if let Some(ref cache) = self.vertex_cache {
118 let mut cache = cache.lock().await;
119 cache.clear();
123 }
124 }
125
126 pub async fn invalidate_edge(&self, _eid: uni_common::core::id::Eid) {
128 if let Some(ref cache) = self.edge_cache {
129 let mut cache = cache.lock().await;
130 cache.clear();
132 }
133 }
134
135 #[instrument(skip(self, ctx), level = "trace")]
136 pub async fn get_edge_prop(
137 &self,
138 eid: uni_common::core::id::Eid,
139 prop: &str,
140 ctx: Option<&QueryContext>,
141 ) -> Result<Value> {
142 if l0_visibility::is_edge_deleted(eid, ctx) {
144 return Ok(Value::Null);
145 }
146
147 if let Some(val) = l0_visibility::lookup_edge_prop(eid, prop, ctx) {
149 return Ok(val);
150 }
151
152 if let Some(ref cache) = self.edge_cache {
154 let mut cache = cache.lock().await;
155 if let Some(val) = cache.get(&(eid, prop.to_string())) {
156 debug!(eid = ?eid, prop, "Cache HIT");
157 metrics::counter!("uni_property_cache_hits_total", "type" => "edge").increment(1);
158 return Ok(val.clone());
159 } else {
160 debug!(eid = ?eid, prop, "Cache MISS");
161 metrics::counter!("uni_property_cache_misses_total", "type" => "edge").increment(1);
162 }
163 }
164
165 let all = self.get_all_edge_props_with_ctx(eid, ctx).await?;
167 let val = all
168 .as_ref()
169 .and_then(|props| props.get(prop).cloned())
170 .unwrap_or(Value::Null);
171
172 if let Some(ref cache) = self.edge_cache {
174 let mut cache = cache.lock().await;
175 if let Some(ref props) = all {
176 for (prop_name, prop_val) in props {
177 cache.put((eid, prop_name.clone()), prop_val.clone());
178 }
179 } else {
180 cache.put((eid, prop.to_string()), Value::Null);
182 }
183 }
184
185 Ok(val)
186 }
187
188 pub async fn get_all_edge_props_with_ctx(
189 &self,
190 eid: uni_common::core::id::Eid,
191 ctx: Option<&QueryContext>,
192 ) -> Result<Option<Properties>> {
193 if l0_visibility::is_edge_deleted(eid, ctx) {
195 return Ok(None);
196 }
197
198 let mut final_props = l0_visibility::accumulate_edge_props(eid, ctx).unwrap_or_default();
200
201 let storage_props = self.fetch_all_edge_props_from_storage(eid).await?;
203
204 if final_props.is_empty() && storage_props.is_none() {
206 if l0_visibility::edge_exists_in_l0(eid, ctx) {
207 return Ok(Some(Properties::new()));
208 }
209 return Ok(None);
210 }
211
212 if let Some(sp) = storage_props {
214 for (k, v) in sp {
215 final_props.entry(k).or_insert(v);
216 }
217 }
218
219 Ok(Some(final_props))
220 }
221
222 async fn fetch_all_edge_props_from_storage(&self, eid: Eid) -> Result<Option<Properties>> {
223 self.fetch_all_edge_props_from_storage_with_hint(eid, None)
225 .await
226 }
227
228 async fn fetch_all_edge_props_from_storage_with_hint(
229 &self,
230 eid: Eid,
231 type_name_hint: Option<&str>,
232 ) -> Result<Option<Properties>> {
233 let schema = self.schema_manager.schema();
234 let backend = self.storage.backend();
235
236 let type_names: Vec<&str> = if let Some(hint) = type_name_hint {
238 vec![hint]
239 } else {
240 schema.edge_types.keys().map(|s| s.as_str()).collect()
242 };
243
244 for type_name in type_names {
245 let type_props = schema.properties.get(type_name);
246
247 if self.storage.delta_dataset(type_name, "fwd").is_err() {
250 continue; }
252
253 use crate::backend::table_names;
255 use crate::backend::types::ScanRequest;
256
257 let table_name = table_names::delta_table_name(type_name, "fwd");
258 if !backend.table_exists(&table_name).await.unwrap_or(false) {
259 continue; }
261
262 let base_filter = format!("eid = {}", eid.as_u64());
263 let filter_expr = self.storage.apply_version_filter(base_filter);
264
265 let batches = match backend
266 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
267 .await
268 {
269 Ok(b) => b,
270 Err(_) => continue,
271 };
272
273 let mut rows: Vec<(u64, u8, Properties)> = Vec::new();
275
276 for batch in batches {
277 let op_col = match batch.column_by_name("op") {
278 Some(c) => c
279 .as_any()
280 .downcast_ref::<arrow_array::UInt8Array>()
281 .unwrap(),
282 None => continue,
283 };
284 let ver_col = match batch.column_by_name("_version") {
285 Some(c) => c.as_any().downcast_ref::<UInt64Array>().unwrap(),
286 None => continue,
287 };
288
289 for row in 0..batch.num_rows() {
290 let ver = ver_col.value(row);
291 let op = op_col.value(row);
292 let mut props = Properties::new();
293
294 if op != 1 {
295 if let Some(tp) = type_props {
297 for (p_name, p_meta) in tp {
298 if let Some(col) = batch.column_by_name(p_name)
299 && !col.is_null(row)
300 {
301 let val =
302 Self::value_from_column(col.as_ref(), &p_meta.r#type, row)?;
303 props.insert(p_name.clone(), val);
304 }
305 }
306 }
307 }
308 rows.push((ver, op, props));
309 }
310 }
311
312 if rows.is_empty() {
313 continue;
314 }
315
316 rows.sort_by_key(|(ver, _, _)| *ver);
318
319 let mut merged_props: Properties = Properties::new();
323 let mut is_deleted = false;
324
325 for (_, op, props) in rows {
326 if op == 1 {
327 is_deleted = true;
329 merged_props.clear();
330 } else {
331 is_deleted = false;
332 for (p_name, p_val) in props {
333 let is_crdt = type_props
335 .and_then(|tp| tp.get(&p_name))
336 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
337 .unwrap_or(false);
338
339 if is_crdt {
340 if let Some(existing) = merged_props.get(&p_name) {
342 if let Ok(merged) = self.merge_crdt_values(existing, &p_val) {
343 merged_props.insert(p_name, merged);
344 }
345 } else {
346 merged_props.insert(p_name, p_val);
347 }
348 } else {
349 merged_props.insert(p_name, p_val);
351 }
352 }
353 }
354 }
355
356 if is_deleted {
357 return Ok(None);
358 }
359
360 if !merged_props.is_empty() {
361 return Ok(Some(merged_props));
362 }
363 }
364
365 use crate::storage::main_edge::MainEdgeDataset;
367 if let Some(props) = MainEdgeDataset::find_props_by_eid(self.storage.backend(), eid).await?
368 {
369 return Ok(Some(props));
370 }
371
372 Ok(None)
373 }
374
375 pub async fn flushed_edge_key_conflict(
394 &self,
395 edge_type: &str,
396 key_values: &[(String, Value)],
397 exclude_eid: Option<Eid>,
398 ) -> Result<bool> {
399 if key_values.is_empty() {
400 return Ok(false);
401 }
402 use crate::backend::table_names;
403 use crate::backend::types::ScanRequest;
404
405 let table_name = table_names::delta_table_name(edge_type, "fwd");
406 let backend = self.storage.backend();
407 if !backend.table_exists(&table_name).await.unwrap_or(false) {
408 return Ok(false);
409 }
410
411 let (probe_prop, probe_val) = &key_values[0];
417 let val_sql = match probe_val {
418 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
419 Value::Int(n) => n.to_string(),
420 Value::Float(f) => f.to_string(),
421 Value::Bool(b) => b.to_string(),
422 _ => return Ok(false),
425 };
426 let base_filter = format!("{probe_prop} = {val_sql} AND op = 0");
427 let filter_expr = self.storage.apply_version_filter(base_filter);
428
429 let batches = backend
430 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
431 .await?;
432
433 let mut candidates: std::collections::HashSet<u64> = std::collections::HashSet::new();
435 for batch in &batches {
436 let Some(eid_col) = batch
437 .column_by_name("eid")
438 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
439 else {
440 continue;
441 };
442 for row in 0..batch.num_rows() {
443 if !eid_col.is_null(row) {
444 candidates.insert(eid_col.value(row));
445 }
446 }
447 }
448
449 let exclude = exclude_eid.map(|e| e.as_u64());
450 for raw in candidates {
451 if Some(raw) == exclude {
452 continue;
453 }
454 let Some(props) = self
455 .fetch_all_edge_props_from_storage_with_hint(Eid::new(raw), Some(edge_type))
456 .await?
457 else {
458 continue; };
460 if key_values.iter().all(|(p, v)| props.get(p) == Some(v)) {
462 return Ok(true);
463 }
464 }
465 Ok(false)
466 }
467
468 pub async fn get_batch_vertex_props(
470 &self,
471 vids: &[Vid],
472 properties: &[&str],
473 ctx: Option<&QueryContext>,
474 ) -> Result<HashMap<Vid, Properties>> {
475 let schema = self.schema_manager.schema();
476 let mut result = HashMap::new();
477 let mut tombstoned: std::collections::HashSet<Vid> = std::collections::HashSet::new();
480 let mut best_version: HashMap<Vid, u64> = HashMap::new();
485 let wants_all = properties.contains(&"_all_props");
489 if vids.is_empty() {
490 return Ok(result);
491 }
492
493 let labels_to_scan: Vec<String> = {
498 let mut needed: std::collections::HashSet<String> = std::collections::HashSet::new();
499 let mut all_resolved = true;
500 for &vid in vids {
501 if let Some(labels) = self.storage.get_labels_from_index(vid) {
502 needed.extend(labels);
503 } else {
504 all_resolved = false;
505 break;
506 }
507 }
508 if all_resolved {
509 needed.into_iter().collect()
510 } else {
511 schema.labels.keys().cloned().collect() }
513 };
514
515 for label_name in &labels_to_scan {
517 let label_schema_props = schema.properties.get(label_name);
520 let valid_props: Vec<&str> = if wants_all {
521 label_schema_props
522 .map(|props| props.keys().map(String::as_str).collect())
523 .unwrap_or_default()
524 } else {
525 properties
526 .iter()
527 .cloned()
528 .filter(|p| label_schema_props.is_some_and(|props| props.contains_key(*p)))
529 .collect()
530 };
531 let ds = match self.storage.vertex_dataset(label_name) {
540 Ok(ds) => ds,
541 Err(_) => continue,
542 };
543 let backend = self.storage.backend();
544 let vtable_name = ds.table_name();
545
546 if !backend.table_exists(&vtable_name).await.unwrap_or(false) {
547 continue; }
549
550 let vid_list = vids
552 .iter()
553 .map(|v| v.as_u64().to_string())
554 .collect::<Vec<_>>()
555 .join(",");
556 let base_filter = format!("_vid IN ({})", vid_list);
557
558 let final_filter = self.storage.apply_version_filter(base_filter);
559
560 let mut columns: Vec<String> = Vec::with_capacity(valid_props.len() + 4);
562 columns.push("_vid".to_string());
563 columns.push("_version".to_string());
564 columns.push("_deleted".to_string());
565 columns.extend(valid_props.iter().map(|s| s.to_string()));
566 columns.push("overflow_json".to_string());
568
569 use crate::backend::types::ScanRequest;
570 let request = ScanRequest::all(&vtable_name)
571 .with_filter(final_filter)
572 .with_columns(columns);
573
574 let batches: Vec<RecordBatch> = match backend.scan(request).await {
575 Ok(b) => b,
576 Err(e) => {
577 warn!(
578 label = %label_name,
579 error = %e,
580 "failed to scan label table, skipping"
581 );
582 continue;
583 }
584 };
585 for batch in batches {
586 let vid_col = match batch
587 .column_by_name("_vid")
588 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>())
589 {
590 Some(c) => c,
591 None => continue,
592 };
593 let del_col = match batch
594 .column_by_name("_deleted")
595 .and_then(|col| col.as_any().downcast_ref::<BooleanArray>())
596 {
597 Some(c) => c,
598 None => continue,
599 };
600 let ver_col = batch
601 .column_by_name("_version")
602 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>());
603
604 for row in 0..batch.num_rows() {
605 let vid = Vid::from(vid_col.value(row));
606 let version = ver_col
607 .map(|c| if c.is_null(row) { 0 } else { c.value(row) })
608 .unwrap_or(0);
609
610 if best_version.get(&vid).is_some_and(|bv| version < *bv) {
612 continue;
613 }
614 best_version.insert(vid, version);
615
616 if del_col.value(row) {
617 result.remove(&vid);
618 tombstoned.insert(vid);
619 continue;
620 }
621
622 tombstoned.remove(&vid);
624 let label_props = schema.properties.get(label_name);
625 let mut props =
626 Self::extract_row_properties(&batch, row, &valid_props, label_props)?;
627 Self::merge_overflow_into_props(&batch, row, properties, &mut props)?;
628 result.insert(vid, props);
629 }
630 }
631 }
632
633 let missing: Vec<Vid> = vids
638 .iter()
639 .copied()
640 .filter(|vid| !result.contains_key(vid) && !tombstoned.contains(vid))
641 .collect();
642 self.main_table_fallback(&missing, &mut result).await?;
643
644 if let Some(ctx) = ctx {
646 for pending_l0_arc in &ctx.pending_flush_l0s {
648 let pending_l0 = pending_l0_arc.read();
649 self.overlay_l0_batch(vids, &pending_l0, properties, &mut result);
650 }
651
652 let l0 = ctx.l0.read();
654 self.overlay_l0_batch(vids, &l0, properties, &mut result);
655
656 if self.storage.version_high_water_mark().is_none()
660 && let Some(tx_l0_arc) = &ctx.transaction_l0
661 {
662 let tx_l0 = tx_l0_arc.read();
663 self.overlay_l0_batch(vids, &tx_l0, properties, &mut result);
664 }
665 }
666
667 Ok(result)
668 }
669
670 fn overlay_l0_batch(
671 &self,
672 vids: &[Vid],
673 l0: &L0Buffer,
674 properties: &[&str],
675 result: &mut HashMap<Vid, Properties>,
676 ) {
677 let schema = self.schema_manager.schema();
678 let wants_all = properties.contains(&"_all_props");
682 for &vid in vids {
683 if l0.vertex_tombstones.contains(&vid) {
685 let tombstone_version = l0.vertex_versions.get(&vid).copied().unwrap_or(0);
692 if self
693 .storage
694 .version_high_water_mark()
695 .is_some_and(|hwm| tombstone_version > hwm)
696 {
697 continue;
698 }
699 result.remove(&vid);
700 continue;
701 }
702 if let Some(l0_props) = l0.vertex_properties.get(&vid) {
704 let entry_version = l0.vertex_versions.get(&vid).copied().unwrap_or(0);
706 if self
707 .storage
708 .version_high_water_mark()
709 .is_some_and(|hwm| entry_version > hwm)
710 {
711 continue;
712 }
713
714 let entry = result.entry(vid).or_default();
715 let labels = l0.get_vertex_labels(vid);
717
718 for (k, v) in l0_props {
719 if wants_all || properties.contains(&k.as_str()) {
720 let is_crdt = labels
722 .and_then(|label_list| {
723 label_list.iter().find_map(|ln| {
724 schema
725 .properties
726 .get(ln)
727 .and_then(|lp| lp.get(k))
728 .filter(|pm| matches!(pm.r#type, DataType::Crdt(_)))
729 })
730 })
731 .is_some();
732
733 if is_crdt {
734 let existing = entry.entry(k.clone()).or_insert(Value::Null);
735 *existing = self.merge_crdt_values(existing, v).unwrap_or(v.clone());
736 } else {
737 entry.insert(k.clone(), v.clone());
738 }
739 }
740 }
741 }
742 }
743 }
744
745 pub async fn get_batch_edge_props(
748 &self,
749 eids: &[uni_common::core::id::Eid],
750 properties: &[&str],
751 ctx: Option<&QueryContext>,
752 ) -> Result<HashMap<Vid, Properties>> {
753 let schema = self.schema_manager.schema();
754 let mut result = HashMap::new();
755 if eids.is_empty() {
756 return Ok(result);
757 }
758 let mut best_version: HashMap<uni_common::core::id::Eid, u64> = HashMap::new();
763
764 let types_to_scan: Vec<String> = {
769 if let Some(ctx) = ctx {
770 let mut needed: std::collections::HashSet<String> =
771 std::collections::HashSet::new();
772 let mut all_resolved = true;
773 for &eid in eids {
774 if let Some(etype) = ctx.l0.read().get_edge_type(eid) {
775 needed.insert(etype.to_string());
776 } else {
777 all_resolved = false;
778 break;
779 }
780 }
781 if all_resolved {
782 needed.into_iter().collect()
783 } else {
784 schema.edge_types.keys().cloned().collect() }
786 } else {
787 schema.edge_types.keys().cloned().collect() }
789 };
790
791 for type_name in &types_to_scan {
793 let type_props = schema.properties.get(type_name);
794 let valid_props: Vec<&str> = properties
795 .iter()
796 .cloned()
797 .filter(|p| type_props.is_some_and(|props| props.contains_key(*p)))
798 .collect();
799 let delta_ds = match self.storage.delta_dataset(type_name, "fwd") {
802 Ok(ds) => ds,
803 Err(_) => continue,
804 };
805 let backend = self.storage.backend();
806 let dtable_name = delta_ds.table_name();
807
808 if !backend.table_exists(&dtable_name).await.unwrap_or(false) {
809 continue; }
811
812 let eid_list = eids
813 .iter()
814 .map(|e| e.as_u64().to_string())
815 .collect::<Vec<_>>()
816 .join(",");
817 let base_filter = format!("eid IN ({})", eid_list);
818
819 let final_filter = self.storage.apply_version_filter(base_filter);
820
821 let mut columns: Vec<String> = Vec::with_capacity(valid_props.len() + 4);
823 columns.push("eid".to_string());
824 columns.push("_version".to_string());
825 columns.push("op".to_string());
826 columns.extend(valid_props.iter().map(|s| s.to_string()));
827 columns.push("overflow_json".to_string());
829
830 use crate::backend::types::ScanRequest;
831 let request = ScanRequest::all(&dtable_name)
832 .with_filter(final_filter)
833 .with_columns(columns);
834
835 let batches: Vec<RecordBatch> = match backend.scan(request).await {
836 Ok(b) => b,
837 Err(e) => {
838 warn!(
839 edge_type = %type_name,
840 error = %e,
841 "failed to scan edge delta table, skipping"
842 );
843 continue;
844 }
845 };
846 for batch in batches {
847 let eid_col = match batch
848 .column_by_name("eid")
849 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>())
850 {
851 Some(c) => c,
852 None => continue,
853 };
854 let op_col = match batch
855 .column_by_name("op")
856 .and_then(|col| col.as_any().downcast_ref::<arrow_array::UInt8Array>())
857 {
858 Some(c) => c,
859 None => continue,
860 };
861 let ver_col = batch
862 .column_by_name("_version")
863 .and_then(|col| col.as_any().downcast_ref::<UInt64Array>());
864
865 for row in 0..batch.num_rows() {
866 let eid = uni_common::core::id::Eid::from(eid_col.value(row));
867 let version = ver_col
868 .map(|c| if c.is_null(row) { 0 } else { c.value(row) })
869 .unwrap_or(0);
870
871 if best_version.get(&eid).is_some_and(|bv| version < *bv) {
873 continue;
874 }
875 best_version.insert(eid, version);
876
877 if op_col.value(row) == 1 {
879 result.remove(&Vid::from(eid.as_u64()));
880 continue;
881 }
882
883 let mut props =
884 Self::extract_row_properties(&batch, row, &valid_props, type_props)?;
885 Self::merge_overflow_into_props(&batch, row, properties, &mut props)?;
886 result.insert(Vid::from(eid.as_u64()), props);
888 }
889 }
890 }
891
892 if let Some(ctx) = ctx {
894 for pending_l0_arc in &ctx.pending_flush_l0s {
896 let pending_l0 = pending_l0_arc.read();
897 self.overlay_l0_edge_batch(eids, &pending_l0, properties, &mut result);
898 }
899
900 let l0 = ctx.l0.read();
902 self.overlay_l0_edge_batch(eids, &l0, properties, &mut result);
903
904 if self.storage.version_high_water_mark().is_none()
908 && let Some(tx_l0_arc) = &ctx.transaction_l0
909 {
910 let tx_l0 = tx_l0_arc.read();
911 self.overlay_l0_edge_batch(eids, &tx_l0, properties, &mut result);
912 }
913 }
914
915 Ok(result)
916 }
917
918 fn overlay_l0_edge_batch(
919 &self,
920 eids: &[uni_common::core::id::Eid],
921 l0: &L0Buffer,
922 properties: &[&str],
923 result: &mut HashMap<Vid, Properties>,
924 ) {
925 let schema = self.schema_manager.schema();
926 for &eid in eids {
927 let vid_key = Vid::from(eid.as_u64());
928 if l0.tombstones.contains_key(&eid) {
929 result.remove(&vid_key);
930 continue;
931 }
932 if let Some(l0_props) = l0.edge_properties.get(&eid) {
933 let entry_version = l0.edge_versions.get(&eid).copied().unwrap_or(0);
935 if self
936 .storage
937 .version_high_water_mark()
938 .is_some_and(|hwm| entry_version > hwm)
939 {
940 continue;
941 }
942
943 let entry = result.entry(vid_key).or_default();
944 let type_name = l0.get_edge_type(eid);
946
947 let include_all = properties.contains(&"_all_props");
948 for (k, v) in l0_props {
949 if include_all || properties.contains(&k.as_str()) {
950 let is_crdt = type_name
952 .and_then(|tn| schema.properties.get(tn))
953 .and_then(|tp| tp.get(k))
954 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
955 .unwrap_or(false);
956
957 if is_crdt {
958 let existing = entry.entry(k.clone()).or_insert(Value::Null);
959 *existing = self.merge_crdt_values(existing, v).unwrap_or(v.clone());
960 } else {
961 entry.insert(k.clone(), v.clone());
962 }
963 }
964 }
965 }
966 }
967 }
968
969 pub async fn get_batch_labels(
971 &self,
972 vids: &[Vid],
973 ctx: Option<&QueryContext>,
974 ) -> Result<HashMap<Vid, Vec<String>>> {
975 let mut result = HashMap::new();
976 if vids.is_empty() {
977 return Ok(result);
978 }
979
980 if let Some(ctx) = ctx {
982 let mut collect_labels = |l0: &L0Buffer| {
983 for &vid in vids {
984 if let Some(labels) = l0.get_vertex_labels(vid) {
985 result
986 .entry(vid)
987 .or_default()
988 .extend(labels.iter().cloned());
989 }
990 }
991 };
992
993 for l0_arc in &ctx.pending_flush_l0s {
994 collect_labels(&l0_arc.read());
995 }
996 collect_labels(&ctx.l0.read());
997 if let Some(tx_l0_arc) = &ctx.transaction_l0 {
998 collect_labels(&tx_l0_arc.read());
999 }
1000 }
1001
1002 let mut vids_needing_lancedb = Vec::new();
1004
1005 fn merge_labels(existing: &mut Vec<String>, new_labels: Vec<String>) {
1007 for l in new_labels {
1008 if !existing.contains(&l) {
1009 existing.push(l);
1010 }
1011 }
1012 }
1013
1014 for &vid in vids {
1015 if result.contains_key(&vid) {
1016 continue; }
1018
1019 if let Some(labels) = self.storage.get_labels_from_index(vid) {
1020 merge_labels(result.entry(vid).or_default(), labels);
1021 } else {
1022 vids_needing_lancedb.push(vid);
1023 }
1024 }
1025
1026 if !vids_needing_lancedb.is_empty() {
1028 let backend = self.storage.backend();
1029 let version = self.storage.version_high_water_mark();
1030 let storage_labels = MainVertexDataset::find_batch_labels_by_vids(
1031 backend,
1032 &vids_needing_lancedb,
1033 version,
1034 )
1035 .await?;
1036
1037 for (vid, labels) in storage_labels {
1038 merge_labels(result.entry(vid).or_default(), labels);
1039 }
1040 }
1041
1042 for labels in result.values_mut() {
1044 labels.sort();
1045 labels.dedup();
1046 }
1047
1048 Ok(result)
1049 }
1050
1051 pub async fn get_all_vertex_props(&self, vid: Vid) -> Result<Properties> {
1052 Ok(self
1053 .get_all_vertex_props_with_ctx(vid, None)
1054 .await?
1055 .unwrap_or_default())
1056 }
1057
1058 pub async fn get_all_vertex_props_with_ctx(
1059 &self,
1060 vid: Vid,
1061 ctx: Option<&QueryContext>,
1062 ) -> Result<Option<Properties>> {
1063 if l0_visibility::is_vertex_deleted(vid, ctx) {
1065 return Ok(None);
1066 }
1067
1068 let l0_props = l0_visibility::accumulate_vertex_props(vid, ctx);
1070
1071 let storage_props_opt = self.fetch_all_props_from_storage(vid).await?;
1073
1074 if l0_props.is_none() && storage_props_opt.is_none() {
1076 return Ok(None);
1077 }
1078
1079 let mut final_props = l0_props.unwrap_or_default();
1080
1081 if let Some(storage_props) = storage_props_opt {
1083 for (k, v) in storage_props {
1084 final_props.entry(k).or_insert(v);
1085 }
1086 }
1087
1088 if let Some(ctx) = ctx {
1091 let labels = l0_visibility::get_vertex_labels(vid, ctx);
1093 for label in &labels {
1094 self.normalize_crdt_properties(&mut final_props, label)?;
1095 }
1096 }
1097
1098 Ok(Some(final_props))
1099 }
1100
1101 pub async fn get_batch_vertex_props_for_label(
1112 &self,
1113 vids: &[Vid],
1114 label: &str,
1115 ctx: Option<&QueryContext>,
1116 ) -> Result<HashMap<Vid, Properties>> {
1117 self.get_batch_vertex_props_for_label_projected(vids, label, ctx, None)
1118 .await
1119 }
1120
1121 pub async fn get_batch_vertex_props_for_label_projected(
1130 &self,
1131 vids: &[Vid],
1132 label: &str,
1133 ctx: Option<&QueryContext>,
1134 requested_props: Option<&[String]>,
1135 ) -> Result<HashMap<Vid, Properties>> {
1136 let mut result: HashMap<Vid, Properties> = HashMap::new();
1137 let mut need_storage: Vec<Vid> = Vec::new();
1138
1139 for &vid in vids {
1141 if l0_visibility::is_vertex_deleted(vid, ctx) {
1142 continue;
1143 }
1144 let l0_props = l0_visibility::accumulate_vertex_props(vid, ctx);
1145 if let Some(props) = l0_props {
1146 result.insert(vid, props);
1147 } else {
1148 need_storage.push(vid);
1149 }
1150 }
1151
1152 if need_storage.is_empty() {
1154 if ctx.is_some() {
1156 for props in result.values_mut() {
1157 self.normalize_crdt_properties(props, label)?;
1158 }
1159 }
1160 return Ok(result);
1161 }
1162
1163 let schema = self.schema_manager.schema();
1165 let label_props = schema.properties.get(label);
1166
1167 let mut prop_names: Vec<String> = Vec::new();
1168 if let Some(props) = label_props {
1169 prop_names = match requested_props {
1170 Some(reqs) => reqs
1174 .iter()
1175 .filter(|r| props.contains_key(r.as_str()))
1176 .cloned()
1177 .collect(),
1178 None => props.keys().cloned().collect(),
1179 };
1180 }
1181
1182 let mut columns: Vec<String> = vec![
1183 "_vid".to_string(),
1184 "_deleted".to_string(),
1185 "_version".to_string(),
1186 ];
1187 columns.extend(prop_names.iter().cloned());
1188 columns.push("overflow_json".to_string());
1189
1190 let vid_list: String = need_storage
1192 .iter()
1193 .map(|v| v.as_u64().to_string())
1194 .collect::<Vec<_>>()
1195 .join(", ");
1196 let base_filter = format!("_vid IN ({})", vid_list);
1197
1198 let filter_expr = self.storage.apply_version_filter(base_filter);
1199
1200 let table_name = crate::backend::table_names::vertex_table_name(label);
1201 let batches: Vec<RecordBatch> = self
1202 .storage
1203 .backend()
1204 .scan(
1205 crate::backend::types::ScanRequest::all(&table_name)
1206 .with_filter(&filter_expr)
1207 .with_columns(columns.clone()),
1208 )
1209 .await?;
1210
1211 let prop_name_refs: Vec<&str> = prop_names.iter().map(|s| s.as_str()).collect();
1212
1213 let mut per_vid_best_version: HashMap<Vid, u64> = HashMap::new();
1215 let mut per_vid_props: HashMap<Vid, Properties> = HashMap::new();
1216
1217 for batch in batches {
1218 let vid_col = match batch
1219 .column_by_name("_vid")
1220 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1221 {
1222 Some(c) => c,
1223 None => continue,
1224 };
1225 let deleted_col = match batch
1226 .column_by_name("_deleted")
1227 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
1228 {
1229 Some(c) => c,
1230 None => continue,
1231 };
1232 let version_col = match batch
1233 .column_by_name("_version")
1234 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1235 {
1236 Some(c) => c,
1237 None => continue,
1238 };
1239
1240 for row in 0..batch.num_rows() {
1241 let vid = Vid::from(vid_col.value(row));
1242 let version = version_col.value(row);
1243
1244 if deleted_col.value(row) {
1245 if per_vid_best_version
1246 .get(&vid)
1247 .is_none_or(|&best| version >= best)
1248 {
1249 per_vid_best_version.insert(vid, version);
1250 per_vid_props.remove(&vid);
1251 }
1252 continue;
1253 }
1254
1255 let mut current_props =
1256 Self::extract_row_properties(&batch, row, &prop_name_refs, label_props)?;
1257
1258 if let Some(overflow_props) = Self::extract_overflow_properties(&batch, row)? {
1259 for (k, v) in overflow_props {
1260 current_props.entry(k).or_insert(v);
1261 }
1262 }
1263
1264 let best = per_vid_best_version.get(&vid).copied();
1265 let mut best_opt = best;
1266 let mut merged = per_vid_props.remove(&vid);
1267 self.merge_versioned_props(
1268 current_props,
1269 version,
1270 &mut best_opt,
1271 &mut merged,
1272 label_props,
1273 )?;
1274 if let Some(v) = best_opt {
1275 per_vid_best_version.insert(vid, v);
1276 }
1277 if let Some(p) = merged {
1278 per_vid_props.insert(vid, p);
1279 }
1280 }
1281 }
1282
1283 for (vid, storage_props) in per_vid_props {
1285 let entry = result.entry(vid).or_default();
1286 for (k, v) in storage_props {
1287 entry.entry(k).or_insert(v);
1288 }
1289 }
1290
1291 let missing: Vec<Vid> = need_storage
1298 .iter()
1299 .copied()
1300 .filter(|vid| !result.contains_key(vid) && !per_vid_best_version.contains_key(vid))
1301 .collect();
1302 self.main_table_fallback(&missing, &mut result).await?;
1303
1304 if ctx.is_some() {
1306 for props in result.values_mut() {
1307 self.normalize_crdt_properties(props, label)?;
1308 }
1309 }
1310
1311 Ok(result)
1312 }
1313
1314 async fn main_table_fallback(
1329 &self,
1330 missing: &[Vid],
1331 out: &mut HashMap<Vid, Properties>,
1332 ) -> Result<()> {
1333 if missing.is_empty() {
1334 return Ok(());
1335 }
1336 let main_props = MainVertexDataset::find_batch_props_by_vids(
1337 self.storage.backend(),
1338 missing,
1339 self.storage.version_high_water_mark(),
1340 )
1341 .await?;
1342 for (vid, props) in main_props {
1343 out.entry(vid).or_insert(props);
1344 }
1345 Ok(())
1346 }
1347
1348 pub async fn get_batch_edge_props_for_type(
1362 &self,
1363 eids: &[Eid],
1364 type_name: &str,
1365 ctx: Option<&QueryContext>,
1366 ) -> Result<HashMap<Eid, Properties>> {
1367 use crate::backend::table_names;
1368 use crate::backend::types::ScanRequest;
1369
1370 let mut result: HashMap<Eid, Properties> = HashMap::new();
1371 if eids.is_empty() {
1372 return Ok(result);
1373 }
1374
1375 let mut need_storage: Vec<Eid> = Vec::new();
1378 for &eid in eids {
1379 if l0_visibility::is_edge_deleted(eid, ctx) {
1380 continue;
1381 }
1382 let l0_props = l0_visibility::accumulate_edge_props(eid, ctx);
1383 if let Some(props) = l0_props {
1387 result.insert(eid, props);
1388 }
1389 need_storage.push(eid);
1390 }
1391
1392 if need_storage.is_empty() {
1393 return Ok(result);
1394 }
1395
1396 let schema = self.schema_manager.schema();
1398 let type_props = schema.properties.get(type_name);
1399
1400 if self.storage.delta_dataset(type_name, "fwd").is_err() {
1401 return Ok(result);
1402 }
1403
1404 let table_name = table_names::delta_table_name(type_name, "fwd");
1405 let backend = self.storage.backend();
1406 if !backend.table_exists(&table_name).await.unwrap_or(false) {
1407 return Ok(result);
1408 }
1409
1410 let eid_list: String = need_storage
1411 .iter()
1412 .map(|e| e.as_u64().to_string())
1413 .collect::<Vec<_>>()
1414 .join(", ");
1415 let base_filter = format!("eid IN ({})", eid_list);
1416 let filter_expr = self.storage.apply_version_filter(base_filter);
1417
1418 let batches = match backend
1419 .scan(ScanRequest::all(&table_name).with_filter(filter_expr))
1420 .await
1421 {
1422 Ok(b) => b,
1423 Err(_) => return Ok(result), };
1425
1426 let mut per_eid_rows: HashMap<Eid, Vec<(u64, u8, Properties)>> = HashMap::new();
1428 for batch in batches {
1429 let eid_col = match batch
1430 .column_by_name("eid")
1431 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1432 {
1433 Some(c) => c,
1434 None => continue,
1435 };
1436 let op_col = match batch
1437 .column_by_name("op")
1438 .and_then(|c| c.as_any().downcast_ref::<arrow_array::UInt8Array>())
1439 {
1440 Some(c) => c,
1441 None => continue,
1442 };
1443 let ver_col = match batch
1444 .column_by_name("_version")
1445 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1446 {
1447 Some(c) => c,
1448 None => continue,
1449 };
1450
1451 for row in 0..batch.num_rows() {
1452 let eid = Eid::from(eid_col.value(row));
1453 let ver = ver_col.value(row);
1454 let op = op_col.value(row);
1455 let mut props = Properties::new();
1456
1457 if op != 1
1458 && let Some(tp) = type_props
1459 {
1460 for (p_name, p_meta) in tp {
1461 if let Some(col) = batch.column_by_name(p_name)
1462 && !col.is_null(row)
1463 {
1464 let val = Self::value_from_column(col.as_ref(), &p_meta.r#type, row)?;
1465 props.insert(p_name.clone(), val);
1466 }
1467 }
1468 }
1469 per_eid_rows.entry(eid).or_default().push((ver, op, props));
1470 }
1471 }
1472
1473 for (eid, mut rows) in per_eid_rows {
1474 rows.sort_by_key(|(ver, _, _)| *ver);
1475
1476 let mut merged_props: Properties = Properties::new();
1477 let mut is_deleted = false;
1478
1479 for (_, op, props) in rows {
1480 if op == 1 {
1481 is_deleted = true;
1482 merged_props.clear();
1483 } else {
1484 is_deleted = false;
1485 for (p_name, p_val) in props {
1486 let is_crdt = type_props
1487 .and_then(|tp| tp.get(&p_name))
1488 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1489 .unwrap_or(false);
1490 if is_crdt {
1491 if let Some(existing) = merged_props.get(&p_name) {
1492 if let Ok(merged) = self.merge_crdt_values(existing, &p_val) {
1493 merged_props.insert(p_name, merged);
1494 }
1495 } else {
1496 merged_props.insert(p_name, p_val);
1497 }
1498 } else {
1499 merged_props.insert(p_name, p_val);
1500 }
1501 }
1502 }
1503 }
1504
1505 if is_deleted {
1506 result.remove(&eid);
1510 continue;
1511 }
1512
1513 let entry = result.entry(eid).or_default();
1516 for (k, v) in merged_props {
1517 entry.entry(k).or_insert(v);
1518 }
1519 }
1520
1521 use crate::storage::main_edge::MainEdgeDataset;
1532 for &eid in eids {
1533 if l0_visibility::is_edge_deleted(eid, ctx) {
1534 continue;
1535 }
1536 let needs_fallback = result.get(&eid).is_none_or(|p| p.is_empty());
1537 if !needs_fallback {
1538 continue;
1539 }
1540 if let Some(props) =
1541 MainEdgeDataset::find_props_by_eid(self.storage.backend(), eid).await?
1542 {
1543 let entry = result.entry(eid).or_default();
1544 for (k, v) in props {
1545 entry.entry(k).or_insert(v);
1546 }
1547 }
1548 }
1549
1550 Ok(result)
1551 }
1552
1553 fn normalize_crdt_properties(&self, props: &mut Properties, label: &str) -> Result<()> {
1557 let schema = self.schema_manager.schema();
1558 let label_props = match schema.properties.get(label) {
1559 Some(p) => p,
1560 None => return Ok(()),
1561 };
1562
1563 for (prop_name, prop_meta) in label_props {
1564 if let DataType::Crdt(_) = prop_meta.r#type
1565 && let Some(val) = props.get_mut(prop_name)
1566 {
1567 *val = Value::from(Self::parse_crdt_value(val)?);
1568 }
1569 }
1570
1571 Ok(())
1572 }
1573
1574 fn extract_row_properties(
1576 batch: &RecordBatch,
1577 row: usize,
1578 prop_names: &[&str],
1579 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1580 ) -> Result<Properties> {
1581 let mut props = Properties::new();
1582 for name in prop_names {
1583 let col = match batch.column_by_name(name) {
1584 Some(col) => col,
1585 None => continue,
1586 };
1587 if col.is_null(row) {
1588 continue;
1589 }
1590 if let Some(prop_meta) = label_props.and_then(|p| p.get(*name)) {
1591 let val = Self::value_from_column(col.as_ref(), &prop_meta.r#type, row)?;
1592 props.insert((*name).to_string(), val);
1593 }
1594 }
1595 Ok(props)
1596 }
1597
1598 fn extract_overflow_properties(batch: &RecordBatch, row: usize) -> Result<Option<Properties>> {
1603 use arrow_array::LargeBinaryArray;
1604
1605 let overflow_col = match batch.column_by_name("overflow_json") {
1606 Some(col) => col,
1607 None => return Ok(None), };
1609
1610 if overflow_col.is_null(row) {
1611 return Ok(None);
1612 }
1613
1614 let binary_array = overflow_col
1615 .as_any()
1616 .downcast_ref::<LargeBinaryArray>()
1617 .ok_or_else(|| anyhow!("overflow_json is not LargeBinaryArray"))?;
1618
1619 let jsonb_bytes = binary_array.value(row);
1620
1621 match uni_common::cypher_value_codec::decode(jsonb_bytes)
1625 .map_err(|e| anyhow!("Failed to decode CypherValue: {}", e))?
1626 {
1627 Value::Map(map) => Ok(Some(map)),
1628 Value::Null => Ok(None),
1629 other => Err(anyhow!(
1630 "overflow_json decoded to a non-map value: {other:?}"
1631 )),
1632 }
1633 }
1634
1635 fn merge_overflow_into_props(
1642 batch: &RecordBatch,
1643 row: usize,
1644 properties: &[&str],
1645 props: &mut Properties,
1646 ) -> Result<()> {
1647 use arrow_array::LargeBinaryArray;
1648
1649 let overflow_col = match batch.column_by_name("overflow_json") {
1650 Some(col) if !col.is_null(row) => col,
1651 _ => return Ok(()),
1652 };
1653
1654 if properties.contains(&"overflow_json")
1656 && let Some(binary_array) = overflow_col.as_any().downcast_ref::<LargeBinaryArray>()
1657 {
1658 let jsonb_bytes = binary_array.value(row);
1659 let bytes_list: Vec<Value> =
1660 jsonb_bytes.iter().map(|&b| Value::Int(b as i64)).collect();
1661 props.insert("overflow_json".to_string(), Value::List(bytes_list));
1662 }
1663
1664 let wants_all = properties.contains(&"_all_props");
1667 if let Some(overflow_props) = Self::extract_overflow_properties(batch, row)? {
1668 for (k, v) in overflow_props {
1669 if wants_all || properties.contains(&k.as_str()) {
1670 props.entry(k).or_insert(v);
1671 }
1672 }
1673 }
1674
1675 Ok(())
1676 }
1677
1678 fn merge_crdt_into(
1680 &self,
1681 target: &mut Properties,
1682 source: Properties,
1683 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1684 crdt_only: bool,
1685 ) -> Result<()> {
1686 for (k, v) in source {
1687 if let Some(prop_meta) = label_props.and_then(|p| p.get(&k)) {
1688 if let DataType::Crdt(_) = prop_meta.r#type {
1689 let existing_v = target.entry(k).or_insert(Value::Null);
1690 *existing_v = self.merge_crdt_values(existing_v, &v)?;
1691 } else if !crdt_only {
1692 target.insert(k, v);
1693 }
1694 }
1695 }
1696 Ok(())
1697 }
1698
1699 fn merge_versioned_props(
1701 &self,
1702 current_props: Properties,
1703 version: u64,
1704 best_version: &mut Option<u64>,
1705 best_props: &mut Option<Properties>,
1706 label_props: Option<&HashMap<String, uni_common::core::schema::PropertyMeta>>,
1707 ) -> Result<()> {
1708 if best_version.is_none_or(|best| version > best) {
1709 if let Some(mut existing_props) = best_props.take() {
1711 let mut merged = current_props;
1713 for (k, v) in merged.iter_mut() {
1714 if let Some(prop_meta) = label_props.and_then(|p| p.get(k))
1715 && let DataType::Crdt(_) = prop_meta.r#type
1716 && let Some(existing_val) = existing_props.remove(k)
1717 {
1718 *v = self.merge_crdt_values(v, &existing_val)?;
1719 }
1720 }
1721 *best_props = Some(merged);
1722 } else {
1723 *best_props = Some(current_props);
1724 }
1725 *best_version = Some(version);
1726 } else if Some(version) == *best_version {
1727 if let Some(existing_props) = best_props.as_mut() {
1729 self.merge_crdt_into(existing_props, current_props, label_props, false)?;
1730 } else {
1731 *best_props = Some(current_props);
1732 }
1733 } else {
1734 if let Some(existing_props) = best_props.as_mut() {
1736 self.merge_crdt_into(existing_props, current_props, label_props, true)?;
1737 }
1738 }
1739 Ok(())
1740 }
1741
1742 async fn fetch_all_props_from_storage(&self, vid: Vid) -> Result<Option<Properties>> {
1743 let schema = self.schema_manager.schema();
1746 let mut merged_props: Option<Properties> = None;
1747 let mut global_best_version: Option<u64> = None;
1748
1749 let label_names: Vec<String> = if let Some(labels) = self.storage.get_labels_from_index(vid)
1751 {
1752 labels
1753 } else {
1754 schema.labels.keys().cloned().collect() };
1756
1757 for label_name in &label_names {
1758 let label_props = schema.properties.get(label_name);
1759
1760 let mut prop_names: Vec<String> = Vec::new();
1762 if let Some(props) = label_props {
1763 prop_names = props.keys().cloned().collect();
1764 }
1765
1766 let mut columns: Vec<String> = vec!["_deleted".to_string(), "_version".to_string()];
1768 columns.extend(prop_names.iter().cloned());
1769 columns.push("overflow_json".to_string());
1771
1772 let base_filter = format!("_vid = {}", vid.as_u64());
1774
1775 let filter_expr = self.storage.apply_version_filter(base_filter);
1776
1777 let table_name = crate::backend::table_names::vertex_table_name(label_name);
1778 let batches: Vec<RecordBatch> = match self
1779 .storage
1780 .backend()
1781 .scan(
1782 crate::backend::types::ScanRequest::all(&table_name)
1783 .with_filter(&filter_expr)
1784 .with_columns(columns.clone()),
1785 )
1786 .await
1787 {
1788 Ok(b) => b,
1789 Err(_) => continue,
1790 };
1791
1792 let prop_name_refs: Vec<&str> = prop_names.iter().map(|s| s.as_str()).collect();
1794
1795 for batch in batches {
1796 let deleted_col = match batch
1797 .column_by_name("_deleted")
1798 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
1799 {
1800 Some(c) => c,
1801 None => continue,
1802 };
1803 let version_col = match batch
1804 .column_by_name("_version")
1805 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
1806 {
1807 Some(c) => c,
1808 None => continue,
1809 };
1810
1811 for row in 0..batch.num_rows() {
1812 let version = version_col.value(row);
1813
1814 if deleted_col.value(row) {
1815 if global_best_version.is_none_or(|best| version >= best) {
1816 global_best_version = Some(version);
1817 merged_props = None;
1818 }
1819 continue;
1820 }
1821
1822 let mut current_props =
1823 Self::extract_row_properties(&batch, row, &prop_name_refs, label_props)?;
1824
1825 if let Some(overflow_props) = Self::extract_overflow_properties(&batch, row)? {
1827 for (k, v) in overflow_props {
1829 current_props.entry(k).or_insert(v);
1830 }
1831 }
1832
1833 self.merge_versioned_props(
1834 current_props,
1835 version,
1836 &mut global_best_version,
1837 &mut merged_props,
1838 label_props,
1839 )?;
1840 }
1841 }
1842 }
1843
1844 if merged_props.is_none()
1849 && global_best_version.is_none()
1850 && let Some(main_props) = MainVertexDataset::find_props_by_vid(
1851 self.storage.backend(),
1852 vid,
1853 self.storage.version_high_water_mark(),
1854 )
1855 .await?
1856 {
1857 return Ok(Some(main_props));
1858 }
1859
1860 Ok(merged_props)
1861 }
1862
1863 pub async fn get_vertex_prop(&self, vid: Vid, prop: &str) -> Result<Value> {
1864 self.get_vertex_prop_with_ctx(vid, prop, None).await
1865 }
1866
1867 #[instrument(skip(self, ctx), level = "trace")]
1868 pub async fn get_vertex_prop_with_ctx(
1869 &self,
1870 vid: Vid,
1871 prop: &str,
1872 ctx: Option<&QueryContext>,
1873 ) -> Result<Value> {
1874 if l0_visibility::is_vertex_deleted(vid, ctx) {
1876 return Ok(Value::Null);
1877 }
1878
1879 let schema = self.schema_manager.schema();
1882 let labels = ctx
1883 .map(|c| l0_visibility::get_vertex_labels(vid, c))
1884 .unwrap_or_default();
1885
1886 let is_crdt = if !labels.is_empty() {
1887 labels.iter().any(|ln| {
1889 schema
1890 .properties
1891 .get(ln)
1892 .and_then(|lp| lp.get(prop))
1893 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1894 .unwrap_or(false)
1895 })
1896 } else {
1897 schema.properties.values().any(|label_props| {
1899 label_props
1900 .get(prop)
1901 .map(|pm| matches!(pm.r#type, DataType::Crdt(_)))
1902 .unwrap_or(false)
1903 })
1904 };
1905
1906 if is_crdt {
1908 let l0_val = self.accumulate_crdt_from_l0(vid, prop, ctx)?;
1910 return self.finalize_crdt_lookup(vid, prop, l0_val).await;
1911 }
1912
1913 if let Some(val) = l0_visibility::lookup_vertex_prop(vid, prop, ctx) {
1915 return Ok(val);
1916 }
1917
1918 if let Some(ref cache) = self.vertex_cache {
1920 let mut cache = cache.lock().await;
1921 if let Some(val) = cache.get(&(vid, prop.to_string())) {
1922 debug!(vid = ?vid, prop, "Cache HIT");
1923 metrics::counter!("uni_property_cache_hits_total", "type" => "vertex").increment(1);
1924 return Ok(val.clone());
1925 } else {
1926 debug!(vid = ?vid, prop, "Cache MISS");
1927 metrics::counter!("uni_property_cache_misses_total", "type" => "vertex")
1928 .increment(1);
1929 }
1930 }
1931
1932 let storage_val = self.fetch_prop_from_storage(vid, prop).await?;
1934
1935 if let Some(ref cache) = self.vertex_cache {
1937 let mut cache = cache.lock().await;
1938 cache.put((vid, prop.to_string()), storage_val.clone());
1939 }
1940
1941 Ok(storage_val)
1942 }
1943
1944 fn accumulate_crdt_from_l0(
1946 &self,
1947 vid: Vid,
1948 prop: &str,
1949 ctx: Option<&QueryContext>,
1950 ) -> Result<Value> {
1951 let mut merged = Value::Null;
1952 l0_visibility::visit_l0_buffers(ctx, |l0| {
1953 if let Some(props) = l0.vertex_properties.get(&vid)
1954 && let Some(val) = props.get(prop)
1955 {
1956 if let Ok(new_merged) = self.merge_crdt_values(&merged, val) {
1958 merged = new_merged;
1959 }
1960 }
1961 false });
1963 Ok(merged)
1964 }
1965
1966 async fn finalize_crdt_lookup(&self, vid: Vid, prop: &str, l0_val: Value) -> Result<Value> {
1968 let cached_val = if let Some(ref cache) = self.vertex_cache {
1970 let mut cache = cache.lock().await;
1971 cache.get(&(vid, prop.to_string())).cloned()
1972 } else {
1973 None
1974 };
1975
1976 if let Some(val) = cached_val {
1977 let merged = self.merge_crdt_values(&val, &l0_val)?;
1978 return Ok(merged);
1979 }
1980
1981 let storage_val = self.fetch_prop_from_storage(vid, prop).await?;
1983
1984 if let Some(ref cache) = self.vertex_cache {
1986 let mut cache = cache.lock().await;
1987 cache.put((vid, prop.to_string()), storage_val.clone());
1988 }
1989
1990 self.merge_crdt_values(&storage_val, &l0_val)
1992 }
1993
1994 async fn fetch_prop_from_storage(&self, vid: Vid, prop: &str) -> Result<Value> {
1995 let schema = self.schema_manager.schema();
1998 let mut best_version: Option<u64> = None;
1999 let mut best_value: Option<Value> = None;
2000
2001 let label_names: Vec<String> = if let Some(labels) = self.storage.get_labels_from_index(vid)
2003 {
2004 labels
2005 } else {
2006 schema.labels.keys().cloned().collect() };
2008
2009 for label_name in &label_names {
2010 let prop_meta = schema
2012 .properties
2013 .get(label_name)
2014 .and_then(|props| props.get(prop));
2015
2016 let base_filter = format!("_vid = {}", vid.as_u64());
2020
2021 let filter_expr = self.storage.apply_version_filter(base_filter);
2022
2023 let mut columns = vec![
2025 "_deleted".to_string(),
2026 "_version".to_string(),
2027 "overflow_json".to_string(),
2028 ];
2029
2030 if prop_meta.is_some() {
2032 columns.push(prop.to_string());
2033 }
2034
2035 let table_name = crate::backend::table_names::vertex_table_name(label_name);
2036 let batches: Vec<RecordBatch> = match self
2037 .storage
2038 .backend()
2039 .scan(
2040 crate::backend::types::ScanRequest::all(&table_name)
2041 .with_filter(&filter_expr)
2042 .with_columns(columns),
2043 )
2044 .await
2045 {
2046 Ok(b) => b,
2047 Err(_) => continue,
2048 };
2049
2050 for batch in batches {
2051 let deleted_col = match batch
2052 .column_by_name("_deleted")
2053 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
2054 {
2055 Some(c) => c,
2056 None => continue,
2057 };
2058 let version_col = match batch
2059 .column_by_name("_version")
2060 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
2061 {
2062 Some(c) => c,
2063 None => continue,
2064 };
2065 for row in 0..batch.num_rows() {
2066 let version = version_col.value(row);
2067
2068 if deleted_col.value(row) {
2069 if best_version.is_none_or(|best| version >= best) {
2070 best_version = Some(version);
2071 best_value = None;
2072 }
2073 continue;
2074 }
2075
2076 let mut val = None;
2078 if let Some(meta) = prop_meta
2079 && let Some(col) = batch.column_by_name(prop)
2080 {
2081 val = Some(if col.is_null(row) {
2082 Value::Null
2083 } else {
2084 Self::value_from_column(col, &meta.r#type, row)?
2085 });
2086 }
2087
2088 if val.is_none()
2090 && let Some(overflow_props) =
2091 Self::extract_overflow_properties(&batch, row)?
2092 && let Some(overflow_val) = overflow_props.get(prop)
2093 {
2094 val = Some(overflow_val.clone());
2095 }
2096
2097 if let Some(v) = val {
2099 if let Some(meta) = prop_meta {
2100 self.merge_prop_value(
2102 v,
2103 version,
2104 &meta.r#type,
2105 &mut best_version,
2106 &mut best_value,
2107 )?;
2108 } else {
2109 if best_version.is_none_or(|best| version >= best) {
2111 best_version = Some(version);
2112 best_value = Some(v);
2113 }
2114 }
2115 }
2116 }
2117 }
2118 }
2119
2120 if best_value.is_none()
2126 && best_version.is_none()
2127 && let Some(main_props) = MainVertexDataset::find_props_by_vid(
2128 self.storage.backend(),
2129 vid,
2130 self.storage.version_high_water_mark(),
2131 )
2132 .await?
2133 {
2134 return Ok(main_props.get(prop).cloned().unwrap_or(Value::Null));
2135 }
2136
2137 Ok(best_value.unwrap_or(Value::Null))
2138 }
2139
2140 pub fn value_from_column(col: &dyn Array, data_type: &DataType, row: usize) -> Result<Value> {
2142 crate::storage::value_codec::decode_column_value(
2143 col,
2144 data_type,
2145 row,
2146 CrdtDecodeMode::Strict,
2147 )
2148 }
2149
2150 pub fn merge_crdt_values(&self, a: &Value, b: &Value) -> Result<Value> {
2163 if a.is_null() {
2167 return Self::parse_crdt_value(b).map(Value::from);
2168 }
2169 if b.is_null() {
2170 return Self::parse_crdt_value(a).map(Value::from);
2171 }
2172
2173 let a_parsed = Self::parse_crdt_value(a)?;
2174 let b_parsed = Self::parse_crdt_value(b)?;
2175
2176 let mut crdt_a: Crdt = serde_json::from_value(a_parsed)?;
2177 let crdt_b: Crdt = serde_json::from_value(b_parsed)?;
2178 crdt_a
2184 .merge_via_registry(&crdt_b, &self.plugin_registry)
2185 .map_err(|e| anyhow::anyhow!("{e}"))?;
2186 Ok(Value::from(serde_json::to_value(crdt_a)?))
2187 }
2188
2189 fn parse_crdt_value(val: &Value) -> Result<serde_json::Value> {
2192 if let Value::String(s) = val {
2193 serde_json::from_str(s).map_err(|e| anyhow!("Failed to parse CRDT JSON string: {}", e))
2195 } else {
2196 Ok(serde_json::Value::from(val.clone()))
2198 }
2199 }
2200
2201 fn merge_prop_value(
2203 &self,
2204 val: Value,
2205 version: u64,
2206 data_type: &DataType,
2207 best_version: &mut Option<u64>,
2208 best_value: &mut Option<Value>,
2209 ) -> Result<()> {
2210 if let DataType::Crdt(_) = data_type {
2211 self.merge_crdt_prop_value(val, version, best_version, best_value)
2212 } else {
2213 if best_version.is_none_or(|best| version >= best) {
2215 *best_version = Some(version);
2216 *best_value = Some(val);
2217 }
2218 Ok(())
2219 }
2220 }
2221
2222 fn merge_crdt_prop_value(
2224 &self,
2225 val: Value,
2226 version: u64,
2227 best_version: &mut Option<u64>,
2228 best_value: &mut Option<Value>,
2229 ) -> Result<()> {
2230 if best_version.is_none_or(|best| version > best) {
2231 if let Some(existing) = best_value.take() {
2233 *best_value = Some(self.merge_crdt_values(&val, &existing)?);
2234 } else {
2235 *best_value = Some(val);
2236 }
2237 *best_version = Some(version);
2238 } else if Some(version) == *best_version {
2239 let existing = best_value.get_or_insert(Value::Null);
2241 *existing = self.merge_crdt_values(existing, &val)?;
2242 } else {
2243 if let Some(existing) = best_value.as_mut() {
2245 *existing = self.merge_crdt_values(existing, &val)?;
2246 }
2247 }
2248 Ok(())
2249 }
2250}