1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2#[cfg(not(target_arch = "wasm32"))]
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use arrow::array::{BinaryArray, BooleanArray, StringArray};
7use arrow::record_batch::RecordBatch;
8use datafusion::prelude::SessionContext;
9use futures::StreamExt as _;
10use nohash_hasher::{IntMap, IntSet};
11use re_protos::common::v1alpha1::TaskId;
12use tonic::{Code, Request, Response, Status};
13
14use re_arrow_util::RecordBatchExt as _;
15use re_chunk_store::{
16 Chunk, ChunkId, ChunkStore, ChunkStoreHandle, ChunkTrackingMode, LatestAtQuery, RangeQuery,
17};
18use re_log_encoding::ToTransport as _;
19use re_log_types::{AbsoluteTimeRange, EntityPath, EntryId, StoreId, StoreKind, TimelineName};
20#[cfg(not(target_arch = "wasm32"))]
21use re_protos::cloud::v1alpha1::ext::{CreateTableEntryResponse, ProviderDetails};
22use re_protos::cloud::v1alpha1::ext::{
23 QueryDatasetDataframe, QueryTasksDataframe, RegisterWithDatasetDataframe,
24 ScanDatasetManifestDataframe, ScanSegmentTableDataframe,
25};
26use re_protos::cloud::v1alpha1::rerun_cloud_service_server::RerunCloudService;
27use re_protos::cloud::v1alpha1::{
28 CancelTasksRequest, CancelTasksResponse, DeleteEntryResponse, DoBandwidthTestResponse,
29 EntryCreatedEvent, EntryDeletedEvent, EntryDetails, EntryKind, EventKind, FetchChunksRequest,
30 GetDatasetManifestSchemaRequest, GetDatasetManifestSchemaResponse, GetDatasetSchemaResponse,
31 GetRrdManifestResponse, GetSegmentTableSchemaResponse, QueryDatasetResponse,
32 QueryTasksOnCompletionRequest, QueryTasksOnCompletionResponse, QueryTasksRequest,
33 QueryTasksResponse, RegisterTableRequest, RegisterTableResponse, ScanDatasetManifestRequest,
34 ScanDatasetManifestResponse, ScanSegmentTableResponse, ScanTableResponse, SegmentIdFilter,
35 WatchEventsResponse, segment_id_filter, watch_events_response,
36};
37use re_protos::common::v1alpha1::ext::{DatasetKind, IfDuplicateBehavior, SegmentId};
38use re_protos::headers::RerunHeadersExtractorExt as _;
39use re_protos::missing_field;
40use re_protos::{
41 EntryName,
42 cloud::v1alpha1::ext::{
43 self, CreateDatasetEntryRequest, CreateDatasetEntryResponse, CreateTableEntryRequest,
44 DataSource, EntryDetailsUpdate, QueryDatasetRequest, ReadDatasetEntryResponse,
45 ReadTableEntryResponse, TableInsertMode, UpdateDatasetEntryRequest,
46 UpdateDatasetEntryResponse, UpdateEntryRequest, UpdateEntryResponse,
47 UpdateTableEntryRequest, UpdateTableEntryResponse,
48 },
49};
50#[cfg(not(target_arch = "wasm32"))]
51use re_tuid::Tuid;
52use re_types_core::LayerName;
53
54mod register_with_dataset;
55use self::register_with_dataset::{RegisterWithDatasetResult, do_register_with_dataset};
56
57#[cfg(not(target_arch = "wasm32"))]
58use crate::NamedPath;
59#[cfg(not(target_arch = "wasm32"))]
60use crate::OnError;
61use crate::store::{
62 ChunkKey, Dataset, InMemoryStore, ResolvedStore, StoreSlotId, Table, TaskResult,
63};
64use crate::store::{LayerInfo, TASK_ID_SUCCESS};
65
66#[derive(Debug)]
67#[cfg_attr(target_arch = "wasm32", derive(Clone, Copy, Default))]
68pub struct RerunCloudHandlerSettings {
69 #[cfg(not(target_arch = "wasm32"))]
70 storage_dir: tempfile::TempDir,
71}
72
73#[cfg(not(target_arch = "wasm32"))]
74impl Default for RerunCloudHandlerSettings {
75 fn default() -> Self {
76 Self {
77 #[cfg(not(target_arch = "wasm32"))]
78 storage_dir: create_data_dir().expect("Failed to create data directory"),
79 }
80 }
81}
82
83#[cfg(not(target_arch = "wasm32"))]
84fn create_data_dir() -> Result<tempfile::TempDir, crate::store::Error> {
85 Ok(tempfile::Builder::new().prefix("rerun-data-").tempdir()?)
86}
87
88fn apply_segment_id_filter(
89 batch: RecordBatch,
90 filter: Option<&SegmentIdFilter>,
91) -> tonic::Result<RecordBatch> {
92 let Some(filter) = filter else {
93 return Ok(batch);
94 };
95 let Some(strategy) = filter.strategy.as_ref() else {
96 return Ok(batch);
97 };
98 let (ids, scan_only) = match strategy {
99 segment_id_filter::Strategy::ScanOnly(ids) => (&ids.segment_ids, true),
100 segment_id_filter::Strategy::Skip(ids) => (&ids.segment_ids, false),
101 };
102 let ids = ids.iter().map(String::as_str).collect::<HashSet<_>>();
103
104 let segment_ids = batch
105 .column_by_name(ScanSegmentTableDataframe::COLUMN_RERUN_SEGMENT_ID_NAME)
106 .ok_or_else(|| Status::internal("segment ID column is missing"))?
107 .as_any()
108 .downcast_ref::<StringArray>()
109 .ok_or_else(|| Status::internal("segment ID column is not UTF-8"))?;
110 let mask = segment_ids
111 .iter()
112 .map(|segment_id| segment_id.map(|segment_id| ids.contains(segment_id) == scan_only))
113 .collect::<BooleanArray>();
114
115 arrow::compute::filter_record_batch(&batch, &mask)
116 .map_err(|err| Status::internal(format!("Unable to apply segment ID filter: {err:#}")))
117}
118
119#[derive(Default)]
120pub struct RerunCloudHandlerBuilder {
121 settings: RerunCloudHandlerSettings,
122 store: InMemoryStore,
123}
124
125impl RerunCloudHandlerBuilder {
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 #[cfg(not(target_arch = "wasm32"))]
131 pub async fn with_directory_as_dataset(
132 mut self,
133 directory: &NamedPath,
134 on_duplicate: IfDuplicateBehavior,
135 on_error: crate::OnError,
136 ) -> Result<Self, crate::store::Error> {
137 self.store
138 .load_directory_as_dataset(directory, on_duplicate, on_error)
139 .await?;
140
141 Ok(self)
142 }
143
144 #[cfg(not(target_arch = "wasm32"))]
145 pub async fn with_rrds_as_dataset(
146 mut self,
147 dataset_name: EntryName,
148 rrd_paths: Vec<PathBuf>,
149 on_duplicate: IfDuplicateBehavior,
150 on_error: crate::OnError,
151 ) -> Result<Self, crate::store::Error> {
152 let dataset_id = self.store.create_dataset(dataset_name, None)?;
153
154 for rrd_path in rrd_paths {
155 let load_result = self
156 .store
157 .register_rrd_to_dataset(
158 dataset_id,
159 &rrd_path,
160 None,
161 on_duplicate,
162 StoreKind::Recording,
163 )
164 .await;
165 match load_result {
166 Ok(_segment_ids) => {}
167 Err(err) => match on_error {
168 OnError::Continue => {
169 re_log::warn!("Failed loading file {}: {err}", rrd_path.display());
170 }
171 OnError::Abort => {
172 return Err(err);
173 }
174 },
175 }
176 }
177
178 Ok(self)
179 }
180
181 #[cfg(all(feature = "lance", not(target_arch = "wasm32")))]
182 pub async fn with_directory_as_table(
183 mut self,
184 path: &NamedPath,
185 on_duplicate: IfDuplicateBehavior,
186 ) -> Result<Self, crate::store::Error> {
187 self.store
188 .load_directory_as_table(path, on_duplicate)
189 .await?;
190
191 Ok(self)
192 }
193
194 pub fn with_eager_chunk_store_config(
195 mut self,
196 config: re_chunk_store::ChunkStoreConfig,
197 ) -> Self {
198 self.store.set_eager_chunk_store_config(config);
199 self
200 }
201
202 pub fn build(self) -> RerunCloudHandler {
203 RerunCloudHandler::new(self.settings, self.store)
204 }
205}
206
207pub struct RerunCloudHandler {
210 #[cfg(not(target_arch = "wasm32"))]
211 settings: RerunCloudHandlerSettings,
212 eager_chunk_store_config: re_chunk_store::ChunkStoreConfig,
213 store: tokio::sync::RwLock<InMemoryStore>,
214 events_tx: tokio::sync::broadcast::Sender<WatchEventsResponse>,
215}
216
217impl RerunCloudHandler {
218 pub fn new(settings: RerunCloudHandlerSettings, store: InMemoryStore) -> Self {
219 #[cfg(target_arch = "wasm32")]
220 let _ = settings;
221 let eager_chunk_store_config = store.eager_chunk_store_config();
222 let (events_tx, _) = tokio::sync::broadcast::channel(1024);
223 Self {
224 #[cfg(not(target_arch = "wasm32"))]
225 settings,
226 eager_chunk_store_config,
227 store: tokio::sync::RwLock::new(store),
228 events_tx,
229 }
230 }
231
232 fn notify(&self, kind: watch_events_response::Kind) {
234 let _ = self
236 .events_tx
237 .send(WatchEventsResponse { kind: Some(kind) })
238 .ok();
239 }
240
241 async fn get_chunk_stores(
246 &self,
247 dataset_id: EntryId,
248 segment_ids: Option<&[SegmentId]>,
249 ) -> tonic::Result<Vec<(SegmentId, LayerName, StoreSlotId, ResolvedStore)>> {
250 let store = self.store.read().await;
251 let dataset = store.dataset(dataset_id)?;
252
253 Ok(dataset
254 .segments_from_ids(segment_ids)
255 .flat_map(|(segment_id, segment)| {
256 segment.iter_sources().map(|(layer_name, source)| {
257 (
258 segment_id.clone(),
259 layer_name.clone(),
260 source.store_slot_id(),
261 source.resolved_store().clone(),
262 )
263 })
264 })
265 .collect())
266 }
267
268 #[cfg_attr(target_arch = "wasm32", expect(clippy::unused_async))]
269 async fn resolve_data_sources(data_sources: &[DataSource]) -> tonic::Result<Vec<DataSource>> {
270 let mut resolved = Vec::<DataSource>::with_capacity(data_sources.len());
271 for source in data_sources {
272 if source.is_prefix {
273 cfg_select! {
274 target_arch = "wasm32" => {
275 return Err(tonic::Status::invalid_argument(
277 "prefix data sources are not supported on wasm",
278 ));
279 }
280 _ => {
281 if source.storage_url.scheme() == "memory" {
282 return Err(tonic::Status::invalid_argument(
283 "memory:// URLs cannot be used as prefix data sources",
284 ));
285 }
286 let path = source.storage_url.to_file_path().map_err(|_err| {
287 tonic::Status::invalid_argument(format!(
288 "getting file path from {:?}",
289 source.storage_url
290 ))
291 })?;
292 let meta =
293 tokio::fs::metadata(&path)
294 .await
295 .map_err(|err| match err.kind() {
296 std::io::ErrorKind::NotFound => tonic::Status::invalid_argument(
297 format!("Directory not found: {path:?}"),
298 ),
299 _ => tonic::Status::invalid_argument(format!(
300 "Failed to read directory metadata {path:?}: {err:#}"
301 )),
302 })?;
303 if !meta.is_dir() {
304 return Err(tonic::Status::invalid_argument(format!(
305 "expected prefix / directory but got an object ({path:?})"
306 )));
307 }
308
309 let mut dirs_to_visit = vec![path];
311 let mut files = Vec::new();
312
313 while let Some(current_dir) = dirs_to_visit.pop() {
314 let mut entries =
315 tokio::fs::read_dir(¤t_dir).await.map_err(|err| {
316 tonic::Status::internal(format!(
317 "Failed to read directory {current_dir:?}: {err:#}"
318 ))
319 })?;
320
321 while let Some(entry) = entries.next_entry().await.map_err(|err| {
322 tonic::Status::internal(format!(
323 "Failed to read directory entry: {err:#}"
324 ))
325 })? {
326 let entry_path = entry.path();
327 let file_type = entry.file_type().await.map_err(|err| {
328 tonic::Status::internal(format!(
329 "Failed to read directory entry metadata: {err:#}"
330 ))
331 })?;
332
333 if file_type.is_dir() {
334 dirs_to_visit.push(entry_path);
335 } else if let Some(extension) = entry_path.extension()
336 && extension == "rrd"
337 {
338 files.push(entry_path);
339 }
340 }
341 }
342
343 if files.is_empty() {
344 return Err(tonic::Status::invalid_argument(format!(
345 "no rrd files found in {:?}",
346 source.storage_url
347 )));
348 }
349
350 for file_path in files {
351 let mut file_url = source.storage_url.clone();
352 file_url.set_path(&file_path.to_string_lossy());
353 resolved.push(DataSource {
354 storage_url: file_url,
355 is_prefix: false,
356 ..source.clone()
357 });
358 }
359 }
360 }
361 } else {
362 resolved.push(source.clone());
363 }
364 }
365
366 Ok(resolved)
367 }
368}
369
370impl std::fmt::Debug for RerunCloudHandler {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 f.debug_struct("RerunCloudHandler").finish()
373 }
374}
375
376macro_rules! decl_stream {
377 ($stream:ident<manifest:$resp:ident>) => {
378 pub type $stream = std::pin::Pin<
379 Box<
380 dyn futures::Stream<Item = tonic::Result<re_protos::cloud::v1alpha1::$resp>> + Send,
381 >,
382 >;
383 };
384
385 ($stream:ident<rerun_cloud:$resp:ident>) => {
386 pub type $stream = std::pin::Pin<
387 Box<
388 dyn futures::Stream<Item = tonic::Result<re_protos::cloud::v1alpha1::$resp>> + Send,
389 >,
390 >;
391 };
392
393 ($stream:ident<tasks:$resp:ident>) => {
394 pub type $stream = std::pin::Pin<
395 Box<
396 dyn futures::Stream<Item = tonic::Result<re_protos::cloud::v1alpha1::$resp>> + Send,
397 >,
398 >;
399 };
400}
401
402decl_stream!(DoBandwidthTestResponseStream<rerun_cloud:DoBandwidthTestResponse>);
403decl_stream!(WatchEventsResponseStream<rerun_cloud:WatchEventsResponse>);
404decl_stream!(FetchChunksResponseStream<manifest:FetchChunksResponse>);
405decl_stream!(GetAssetsForSegmentResponseStream<rerun_cloud:GetAssetsForSegmentResponse>);
406decl_stream!(GetRrdManifestResponseStream<manifest:GetRrdManifestResponse>);
407decl_stream!(QueryDatasetResponseStream<manifest:QueryDatasetResponse>);
408decl_stream!(QueryTasksOnCompletionResponseStream<tasks:QueryTasksOnCompletionResponse>);
409decl_stream!(ScanDatasetManifestResponseStream<manifest:ScanDatasetManifestResponse>);
410decl_stream!(ScanSegmentTableResponseStream<manifest:ScanSegmentTableResponse>);
411decl_stream!(ScanTableResponseStream<rerun_cloud:ScanTableResponse>);
412decl_stream!(UnregisterFromDatasetResponseStream<manifest:UnregisterFromDatasetResponse>);
413
414impl RerunCloudHandler {
415 async fn find_datasets(
416 &self,
417 entry_id: Option<EntryId>,
418 name: Option<EntryName>,
419 store_kind: Option<StoreKind>,
420 ) -> tonic::Result<Vec<EntryDetails>> {
421 let store = self.store.read().await;
422
423 let dataset = match (entry_id, name) {
424 (None, None) => None,
425
426 (Some(entry_id), None) => Some(store.dataset(entry_id)?),
427
428 (None, Some(name)) => Some(store.dataset_by_name(&name)?),
429
430 (Some(entry_id), Some(name)) => {
431 let dataset = store.dataset_by_name(&name)?;
432 if dataset.id() != entry_id {
433 return Err(tonic::Status::not_found(format!(
434 "Dataset with ID {entry_id} not found"
435 )));
436 }
437 Some(dataset)
438 }
439 };
440
441 let dataset_iter = if let Some(dataset) = dataset {
442 itertools::Either::Left(std::iter::once(dataset))
443 } else {
444 itertools::Either::Right(store.iter_datasets())
445 };
446
447 Ok(dataset_iter
448 .filter(|dataset| {
449 store_kind.is_none_or(|store_kind| dataset.store_kind() == store_kind)
450 })
451 .map(Dataset::as_entry_details)
452 .map(Into::into)
453 .collect())
454 }
455
456 async fn find_tables(
457 &self,
458 entry_id: Option<EntryId>,
459 name: Option<EntryName>,
460 ) -> tonic::Result<Vec<EntryDetails>> {
461 let store = self.store.read().await;
462
463 let table = match (entry_id, name) {
464 (None, None) => None,
465
466 (Some(entry_id), None) => {
467 let Some(table) = store.table(entry_id) else {
468 return Err(tonic::Status::not_found(format!(
469 "Table with ID {entry_id} not found"
470 )));
471 };
472 Some(table)
473 }
474
475 (None, Some(name)) => {
476 let Some(table) = store.table_by_name(&name) else {
477 return Err(tonic::Status::not_found(format!(
478 "Table with name {name} not found"
479 )));
480 };
481 Some(table)
482 }
483
484 (Some(entry_id), Some(name)) => {
485 let Some(table) = store.table_by_name(&name) else {
486 return Err(tonic::Status::not_found(format!(
487 "Table with name {name} not found"
488 )));
489 };
490 if table.id() != entry_id {
491 return Err(tonic::Status::not_found(format!(
492 "Table with ID {entry_id} not found"
493 )));
494 }
495 Some(table)
496 }
497 };
498
499 let table_iter = if let Some(table) = table {
500 itertools::Either::Left(std::iter::once(table))
501 } else {
502 itertools::Either::Right(store.iter_tables())
503 };
504
505 Ok(table_iter
506 .map(Table::as_entry_details)
507 .map(Into::into)
508 .collect())
509 }
510}
511
512fn validate_blueprint_dataset(
517 store: &InMemoryStore,
518 blueprint_dataset: Option<EntryId>,
519 entry_kind: &str,
520) -> tonic::Result<()> {
521 let Some(blueprint_dataset) = blueprint_dataset else {
522 return Ok(());
523 };
524
525 let blueprint_dataset = store.dataset(blueprint_dataset).map_err(|err| {
526 tonic::Status::invalid_argument(format!(
527 "{entry_kind} blueprint dataset does not exist: {err}"
528 ))
529 })?;
530
531 if blueprint_dataset.store_kind() != StoreKind::Blueprint {
532 return Err(tonic::Status::invalid_argument(format!(
533 "{entry_kind} blueprint dataset must be a blueprint dataset"
534 )));
535 }
536
537 Ok(())
538}
539
540fn validate_asset_dataset(
542 store: &InMemoryStore,
543 asset_dataset: Option<EntryId>,
544) -> tonic::Result<()> {
545 let Some(asset_dataset) = asset_dataset else {
546 return Ok(());
547 };
548
549 let asset_dataset = store.dataset(asset_dataset).map_err(|err| {
550 tonic::Status::invalid_argument(format!("asset dataset does not exist: {err}"))
551 })?;
552
553 let kind = asset_dataset.dataset_kind();
554 if kind != DatasetKind::Asset {
555 return Err(tonic::Status::invalid_argument(format!(
556 "asset dataset reference must point to an asset dataset, this is a {kind:?} dataset"
557 )));
558 }
559
560 Ok(())
561}
562
563#[tonic::async_trait]
564impl RerunCloudService for RerunCloudHandler {
565 async fn version(
566 &self,
567 request: tonic::Request<re_protos::cloud::v1alpha1::VersionRequest>,
568 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::VersionResponse>> {
569 let re_protos::cloud::v1alpha1::VersionRequest {} = request.into_inner();
570
571 let build_info = re_build_info::build_info!();
573
574 Ok(tonic::Response::new(
575 re_protos::cloud::v1alpha1::VersionResponse {
576 build_info: Some(build_info.into()),
577 version: re_build_info::exposed_version().to_owned(),
578 cloud_provider: None,
579 cloud_region: None,
580 features: re_protos::cloud::v1alpha1::features::all_supported_features(),
581 },
582 ))
583 }
584
585 async fn who_am_i(
586 &self,
587 _request: tonic::Request<re_protos::cloud::v1alpha1::WhoAmIRequest>,
588 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::WhoAmIResponse>> {
589 Ok(tonic::Response::new(
591 re_protos::cloud::v1alpha1::WhoAmIResponse {
592 user_id: None,
593 can_read: true,
594 can_write: true,
595 },
596 ))
597 }
598
599 type DoBandwidthTestStream = DoBandwidthTestResponseStream;
600
601 async fn do_bandwidth_test(
602 &self,
603 request: tonic::Request<re_protos::cloud::v1alpha1::DoBandwidthTestRequest>,
604 ) -> tonic::Result<tonic::Response<Self::DoBandwidthTestStream>> {
605 let re_protos::cloud::v1alpha1::DoBandwidthTestRequest { num_bytes } = request.into_inner();
606 let max = ext::MAX_BANDWIDTH_TEST_BYTES;
607 if num_bytes > max {
608 return Err(Status::invalid_argument(format!(
609 "num_bytes ({num_bytes}) exceeds the maximum of {max}"
610 )));
611 }
612 Ok(tonic::Response::new(
613 Box::pin(bandwidth_test_stream(num_bytes)) as Self::DoBandwidthTestStream,
614 ))
615 }
616
617 type WatchEventsStream = WatchEventsResponseStream;
618
619 async fn watch_events(
620 &self,
621 request: Request<re_protos::cloud::v1alpha1::WatchEventsRequest>,
622 ) -> tonic::Result<tonic::Response<Self::WatchEventsStream>> {
623 let rx = self.events_tx.subscribe();
624
625 let kinds = request.into_inner().kinds;
626
627 let stream = futures::stream::unfold((rx, kinds), |(mut rx, kinds)| async move {
628 loop {
629 match rx.recv().await {
630 Ok(event) => {
631 if kinds.is_empty() {
632 return Some((Ok(event), (rx, kinds)));
633 }
634
635 let subscribed = event.kind.is_some_and(|kind| kind.is_entry_kind())
636 && kinds.contains(&EventKind::entry());
637
638 if subscribed {
639 return Some((Ok(event), (rx, kinds)));
640 }
641 }
642 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
643 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
644 }
645 }
646 });
647
648 Ok(tonic::Response::new(
649 Box::pin(stream) as Self::WatchEventsStream
650 ))
651 }
652
653 async fn find_entries(
656 &self,
657 request: tonic::Request<re_protos::cloud::v1alpha1::FindEntriesRequest>,
658 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::FindEntriesResponse>> {
659 let filter = request.into_inner().filter.unwrap_or_default();
660
661 let entry_id = filter.id.map(TryInto::try_into).transpose()?;
662 let name = filter
663 .name
664 .map(EntryName::new)
665 .transpose()
666 .map_err(|err| Status::invalid_argument(err.to_string()))?;
667
668 if filter
674 .entry_kinds
675 .contains(&(EntryKind::Unspecified as i32))
676 {
677 return Err(Status::invalid_argument(
678 "find_entries: entry_kinds must not contain ENTRY_KIND_UNSPECIFIED",
679 ));
680 }
681
682 let effective_kinds: Option<Vec<i32>> = if !filter.entry_kinds.is_empty() {
685 Some(filter.entry_kinds)
686 } else if let Some(kind) = filter.entry_kind {
687 let kind = EntryKind::try_from(kind).map_err(|err| {
689 Status::invalid_argument(format!("find_entries: invalid entry kind {err}"))
690 })?;
691 if kind == EntryKind::Unspecified {
692 return Err(Status::invalid_argument(
693 "find_entries: entry kind unspecified",
694 ));
695 }
696 Some(vec![kind as i32])
697 } else {
698 None
699 };
700
701 let matches_kind = |raw_kind: i32| match &effective_kinds {
702 Some(kinds) => kinds.contains(&raw_kind),
703 None => EntryKind::try_from(raw_kind).is_ok_and(EntryKind::is_legacy_default_kind),
708 };
709
710 let soften_not_found = |result: tonic::Result<Vec<EntryDetails>>| match result {
711 Ok(entries) => Ok(entries),
712 Err(err) if err.code() == Code::NotFound => Ok(vec![]),
714 Err(err) => Err(err),
715 };
716
717 let mut entries = if effective_kinds.is_some() {
718 let mut entries = Vec::new();
723 if matches_kind(EntryKind::Dataset as i32)
724 || matches_kind(EntryKind::AssetDataset as i32)
725 {
726 let result = self
727 .find_datasets(entry_id, name.clone(), Some(StoreKind::Recording))
728 .await;
729 entries.extend(soften_not_found(result)?);
730 }
731 if matches_kind(EntryKind::BlueprintDataset as i32) {
732 let result = self
733 .find_datasets(entry_id, name.clone(), Some(StoreKind::Blueprint))
734 .await;
735 entries.extend(soften_not_found(result)?);
736 }
737 if matches_kind(EntryKind::Table as i32) {
738 let result = self.find_tables(entry_id, name.clone()).await;
739 entries.extend(soften_not_found(result)?);
740 }
741 entries
742 } else {
743 let datasets = self.find_datasets(entry_id, name.clone(), None).await;
744 let mut datasets = soften_not_found(datasets)?;
745 let tables = self.find_tables(entry_id, name.clone()).await;
746 datasets.extend(soften_not_found(tables)?);
747 datasets
748 };
749
750 entries.retain(|entry| matches_kind(entry.entry_kind));
751
752 let response = re_protos::cloud::v1alpha1::FindEntriesResponse { entries };
753
754 Ok(tonic::Response::new(response))
755 }
756
757 async fn create_dataset_entry(
758 &self,
759 request: tonic::Request<re_protos::cloud::v1alpha1::CreateDatasetEntryRequest>,
760 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::CreateDatasetEntryResponse>>
761 {
762 let CreateDatasetEntryRequest {
763 name: dataset_name,
764 id: dataset_id,
765 } = request.into_inner().try_into()?;
766
767 let mut store = self.store.write().await;
768 let dataset_id = store.create_dataset(dataset_name, dataset_id)?;
769 let dataset = store.dataset(dataset_id)?;
770
771 self.notify(watch_events_response::Kind::EntryCreated(
772 EntryCreatedEvent {
773 id: Some(dataset_id.into()),
774 },
775 ));
776
777 Ok(tonic::Response::new(
778 CreateDatasetEntryResponse {
779 dataset: dataset.as_dataset_entry(),
780 }
781 .into(),
782 ))
783 }
784
785 async fn read_dataset_entry(
786 &self,
787 request: tonic::Request<re_protos::cloud::v1alpha1::ReadDatasetEntryRequest>,
788 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::ReadDatasetEntryResponse>> {
789 let store = self.store.read().await;
790 let entry_id = get_entry_id_from_headers(&store, &request)?;
791 let dataset = store.dataset(entry_id)?;
792
793 Ok(tonic::Response::new(
794 ReadDatasetEntryResponse {
795 dataset_entry: dataset.as_dataset_entry(),
796 }
797 .into(),
798 ))
799 }
800
801 async fn update_dataset_entry(
802 &self,
803 request: tonic::Request<re_protos::cloud::v1alpha1::UpdateDatasetEntryRequest>,
804 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::UpdateDatasetEntryResponse>>
805 {
806 let request: UpdateDatasetEntryRequest = request.into_inner().try_into()?;
807
808 request
809 .dataset_details
810 .validate_consistency()
811 .map_err(|err| tonic::Status::invalid_argument(err.to_string()))?;
812
813 let mut store = self.store.write().await;
814 validate_blueprint_dataset(&store, request.dataset_details.blueprint_dataset, "dataset")?;
815
816 let mut dataset_details = request.dataset_details;
817
818 let dataset = store.dataset(request.id)?;
823 let stored_asset_dataset = dataset.dataset_details().asset_dataset;
824 let dataset_kind = dataset.dataset_kind();
825 let client_chosen_asset_dataset = dataset_details.asset_dataset.is_some()
826 && dataset_details.asset_dataset != stored_asset_dataset;
827 if client_chosen_asset_dataset {
828 validate_asset_dataset(&store, dataset_details.asset_dataset)?;
829 } else if dataset_kind == DatasetKind::Recording {
830 let existing = stored_asset_dataset.filter(|id| store.dataset(*id).is_ok());
831 dataset_details.asset_dataset = Some(match existing {
832 Some(existing) => existing,
833 None => store.create_asset_dataset_for_entry(request.id)?,
834 });
835 }
836
837 let dataset = store.dataset_mut(request.id)?;
838
839 dataset.set_dataset_details(dataset_details);
840
841 Ok(tonic::Response::new(
842 UpdateDatasetEntryResponse {
843 dataset_entry: dataset.as_dataset_entry(),
844 }
845 .into(),
846 ))
847 }
848
849 async fn read_table_entry(
850 &self,
851 request: tonic::Request<re_protos::cloud::v1alpha1::ReadTableEntryRequest>,
852 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::ReadTableEntryResponse>> {
853 let store = self.store.read().await;
854
855 let id = request
856 .into_inner()
857 .id
858 .ok_or_else(|| Status::invalid_argument("No table entry ID provided"))?
859 .try_into()?;
860
861 let table = store.table(id).ok_or_else(|| {
862 tonic::Status::not_found(format!("table with entry ID '{id}' not found"))
863 })?;
864
865 Ok(tonic::Response::new(
866 ReadTableEntryResponse {
867 table_entry: table.as_table_entry(),
868 }
869 .try_into()?,
870 ))
871 }
872
873 async fn update_table_entry(
874 &self,
875 request: tonic::Request<re_protos::cloud::v1alpha1::UpdateTableEntryRequest>,
876 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::UpdateTableEntryResponse>> {
877 let request: UpdateTableEntryRequest = request.into_inner().try_into()?;
878
879 let mut store = self.store.write().await;
880 store.table(request.id).ok_or_else(|| {
881 tonic::Status::not_found(format!("table with entry ID '{}' not found", request.id))
882 })?;
883
884 let mut table_details = request.table_details;
885 if table_details.blueprint_dataset.is_none()
889 && table_details.default_blueprint_segment.is_some()
890 {
891 table_details.blueprint_dataset = Some(
892 match store
893 .table(request.id)
894 .and_then(|table| table.table_details().blueprint_dataset)
895 {
896 Some(blueprint_dataset) => blueprint_dataset,
897 None => store.create_blueprint_dataset_for_entry(request.id)?,
898 },
899 );
900 }
901
902 table_details
903 .validate_consistency()
904 .map_err(|err| tonic::Status::invalid_argument(err.to_string()))?;
905 validate_blueprint_dataset(&store, table_details.blueprint_dataset, "table")?;
906
907 let table = store.table_mut(request.id).ok_or_else(|| {
908 tonic::Status::not_found(format!("table with entry ID '{}' not found", request.id))
909 })?;
910 table.set_table_details(table_details);
911
912 Ok(tonic::Response::new(
913 UpdateTableEntryResponse {
914 table_entry: table.as_table_entry(),
915 }
916 .try_into()?,
917 ))
918 }
919
920 async fn delete_entry(
921 &self,
922 request: tonic::Request<re_protos::cloud::v1alpha1::DeleteEntryRequest>,
923 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::DeleteEntryResponse>> {
924 let entry_id = request.into_inner().try_into()?;
925
926 self.store.write().await.delete_entry(entry_id)?;
927
928 self.notify(watch_events_response::Kind::EntryDeleted(
929 EntryDeletedEvent {
930 id: Some(entry_id.into()),
931 },
932 ));
933
934 Ok(tonic::Response::new(DeleteEntryResponse {}))
935 }
936
937 async fn update_entry(
938 &self,
939 request: tonic::Request<re_protos::cloud::v1alpha1::UpdateEntryRequest>,
940 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::UpdateEntryResponse>> {
941 let UpdateEntryRequest {
942 id: entry_id,
943 entry_details_update: EntryDetailsUpdate { name },
944 } = request.into_inner().try_into()?;
945
946 let mut store = self.store.write().await;
947
948 if let Some(name) = name {
949 store.rename_entry(entry_id, name)?;
950 }
951
952 Ok(tonic::Response::new(
953 UpdateEntryResponse {
954 entry_details: store.entry_details(entry_id)?,
955 }
956 .into(),
957 ))
958 }
959
960 async fn register_with_dataset(
962 &self,
963 request: tonic::Request<re_protos::cloud::v1alpha1::RegisterWithDatasetRequest>,
964 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::RegisterWithDatasetResponse>>
965 {
966 let mut store = self.store.write().await;
967
968 let dataset_id = get_entry_id_from_headers(&store, &request)?;
969
970 let ext::RegisterWithDatasetRequest {
971 data_sources,
972 on_duplicate,
973 } = request.into_inner().try_into()?;
974
975 let data_sources = Self::resolve_data_sources(&data_sources).await?;
976 if data_sources.is_empty() {
977 return Err(tonic::Status::invalid_argument(
978 "no data sources to register",
979 ));
980 }
981
982 let RegisterWithDatasetResult {
983 segment_ids,
984 segment_layers,
985 segment_types,
986 storage_urls,
987 task_ids,
988 } = do_register_with_dataset(&mut store, dataset_id, data_sources, on_duplicate).await?;
989
990 let record_batch = RegisterWithDatasetDataframe {
991 rerun_segment_id: segment_ids.into(),
992 rerun_segment_layer: segment_layers.into(),
993 rerun_segment_type: segment_types.into(),
994 rerun_storage_url: storage_urls.into(),
995 rerun_task_id: task_ids.into(),
996 }
997 .into_record_batch()
998 .map_err(|err| tonic::Status::internal(format!("Failed to create dataframe: {err:#}")))?;
999 Ok(tonic::Response::new(
1000 re_protos::cloud::v1alpha1::RegisterWithDatasetResponse {
1001 data: Some(record_batch.into()),
1002 },
1003 ))
1004 }
1005
1006 type UnregisterFromDatasetStream = UnregisterFromDatasetResponseStream;
1007
1008 async fn unregister_from_dataset(
1009 &self,
1010 request: tonic::Request<re_protos::cloud::v1alpha1::UnregisterFromDatasetRequest>,
1011 ) -> tonic::Result<Response<Self::UnregisterFromDatasetStream>> {
1012 let mut store = self.store.write().await;
1013
1014 let entry_id = get_entry_id_from_headers(&store, &request)?;
1015 request.get_ref().sanity_check()?;
1016
1017 let dataset = store.dataset_mut(entry_id)?;
1018
1019 let ext::UnregisterFromDatasetRequest {
1020 segments_to_drop,
1021 layers_to_drop,
1022 force: _, } = request.into_inner().try_into()?;
1024
1025 let segments_to_drop: Option<HashSet<&SegmentId>> =
1027 (!segments_to_drop.is_empty()).then(|| segments_to_drop.iter().collect());
1028 let layers_to_drop: Option<HashSet<&LayerName>> =
1029 (!layers_to_drop.is_empty()).then(|| layers_to_drop.iter().collect());
1030
1031 _ = dataset
1032 .remove_layers(segments_to_drop.as_ref(), layers_to_drop.as_ref())
1033 .await?;
1034
1035 store.cleanup_store_pool();
1036
1037 let stream = futures::stream::once(async move {
1038 Ok(re_protos::cloud::v1alpha1::UnregisterFromDatasetResponse {
1039 data: Some(ScanDatasetManifestDataframe::empty_record_batch().into()),
1040 task_id: Some(TaskId {
1041 id: TASK_ID_SUCCESS.to_owned(),
1042 }),
1043 })
1044 });
1045
1046 Ok(tonic::Response::new(
1047 Box::pin(stream) as Self::UnregisterFromDatasetStream
1048 ))
1049 }
1050
1051 async fn write_chunks(
1054 &self,
1055 request: tonic::Request<tonic::Streaming<re_protos::cloud::v1alpha1::WriteChunksRequest>>,
1056 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::WriteChunksResponse>> {
1057 let entry_id = get_entry_id_from_headers(&*self.store.read().await, &request)?;
1058 #[expect(deprecated)]
1059 let application_id = re_log_types::ApplicationId::from_entry_id(entry_id);
1060
1061 let mut request = request.into_inner();
1062
1063 let mut chunk_stores: HashMap<_, _> = HashMap::default();
1064
1065 while let Some(chunk_msg) = request.next().await {
1066 let chunk_msg = chunk_msg?;
1067
1068 let chunk_batch: RecordBatch = chunk_msg
1069 .chunk
1070 .ok_or_else(|| tonic::Status::invalid_argument("no chunk in WriteChunksRequest"))?
1071 .try_into()
1072 .map_err(|err| {
1073 tonic::Status::internal(format!("Could not decode chunk: {err:#}"))
1074 })?;
1075
1076 let schema = chunk_batch.schema();
1078 let metadata = schema.metadata();
1079 let segment_id: SegmentId = metadata
1080 .get("rerun:segment_id")
1081 .or_else(|| metadata.get("rerun:partition_id"))
1082 .ok_or_else(|| {
1083 tonic::Status::invalid_argument(
1084 "Received chunk without 'rerun:segment_id' metadata",
1085 )
1086 })?
1087 .clone()
1088 .into();
1089
1090 let chunk = Arc::new(Chunk::from_chunk_record_batch(&chunk_batch).map_err(|err| {
1091 tonic::Status::internal(format!("error decoding chunk from record batch: {err:#}"))
1092 })?);
1093
1094 chunk_stores
1095 .entry(segment_id.clone())
1096 .or_insert_with(|| {
1097 ChunkStore::new(
1098 StoreId::new(
1099 StoreKind::Recording,
1100 application_id.clone(),
1101 segment_id.clone(),
1102 ),
1103 self.eager_chunk_store_config.clone(),
1104 )
1105 })
1106 .insert_chunk(&chunk)
1107 .map_err(|err| {
1108 tonic::Status::internal(format!("error adding chunk to store: {err:#}"))
1109 })?;
1110 }
1111
1112 let mut store = self.store.write().await;
1113
1114 let handles: Vec<_> = chunk_stores
1116 .into_iter()
1117 .map(|(segment_id, chunk_store)| {
1118 let resolved = ResolvedStore::Eager(ChunkStoreHandle::new(chunk_store));
1119 let store_slot_id = store.register_store(&resolved);
1120 (segment_id, store_slot_id, resolved)
1121 })
1122 .collect();
1123
1124 let dataset = store.dataset_mut(entry_id)?;
1125
1126 for (entity_path, store_slot_id, resolved) in handles {
1127 dataset
1128 .add_source(
1129 entity_path,
1130 Arc::new(LayerInfo {
1131 name: LayerName::base(),
1132 }),
1133 store_slot_id,
1134 resolved,
1135 IfDuplicateBehavior::Error,
1136 )
1137 .await?;
1138 }
1139
1140 Ok(tonic::Response::new(
1141 re_protos::cloud::v1alpha1::WriteChunksResponse {},
1142 ))
1143 }
1144
1145 async fn write_table(
1146 &self,
1147 request: tonic::Request<tonic::Streaming<re_protos::cloud::v1alpha1::WriteTableRequest>>,
1148 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::WriteTableResponse>> {
1149 let entry_id = {
1152 let store = self.store.read().await;
1153 get_entry_id_from_headers(&store, &request)?
1154 };
1155
1156 let mut request = request.into_inner();
1157
1158 while let Some(write_msg) = request.next().await {
1159 let write_msg = write_msg?;
1160
1161 let rb = write_msg
1162 .dataframe_part
1163 .ok_or_else(|| {
1164 tonic::Status::invalid_argument("no data frame in WriteTableRequest")
1165 })?
1166 .try_into()
1167 .map_err(|err| {
1168 tonic::Status::internal(format!("Could not decode chunk: {err:#}"))
1169 })?;
1170
1171 let insert_op = TableInsertMode::try_from(write_msg.insert_mode)
1172 .map_err(|err| Status::invalid_argument(err.to_string()))?;
1173
1174 cfg_select! {
1175 feature = "lance" => {
1176 let mut store = self.store.write().await;
1177 let Some(table) = store.table_mut(entry_id) else {
1178 return Err(tonic::Status::not_found("table not found"));
1179 };
1180 table.write_table(rb, insert_op).await.map_err(|err| {
1181 tonic::Status::internal(format!("error writing to table: {err:#}"))
1182 })?;
1183 }
1184 _ => {
1185 let mut table = {
1186 let store = self.store.read().await;
1187 store
1188 .table(entry_id)
1189 .cloned()
1190 .ok_or_else(|| tonic::Status::not_found("table not found"))?
1191 };
1192 table.write_table(rb, insert_op).await.map_err(|err| {
1193 tonic::Status::internal(format!("error writing to table: {err:#}"))
1194 })?;
1195 }
1196 }
1197 }
1198
1199 Ok(tonic::Response::new(
1200 re_protos::cloud::v1alpha1::WriteTableResponse {},
1201 ))
1202 }
1203
1204 async fn get_segment_table_schema(
1207 &self,
1208 request: tonic::Request<re_protos::cloud::v1alpha1::GetSegmentTableSchemaRequest>,
1209 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::GetSegmentTableSchemaResponse>>
1210 {
1211 let store = self.store.read().await;
1212
1213 let entry_id = get_entry_id_from_headers(&store, &request)?;
1214 let dataset = store.dataset(entry_id)?;
1215 let record_batch = dataset.segment_table().await.map_err(|err| {
1216 tonic::Status::internal(format!("Unable to read segment table: {err:#}"))
1217 })?;
1218
1219 Ok(tonic::Response::new(GetSegmentTableSchemaResponse {
1220 schema: Some(
1221 record_batch
1222 .schema_ref()
1223 .as_ref()
1224 .try_into()
1225 .map_err(|err| {
1226 tonic::Status::internal(format!(
1227 "unable to serialize Arrow schema: {err:#}"
1228 ))
1229 })?,
1230 ),
1231 }))
1232 }
1233
1234 type ScanSegmentTableStream = ScanSegmentTableResponseStream;
1235
1236 async fn scan_segment_table(
1237 &self,
1238 request: tonic::Request<re_protos::cloud::v1alpha1::ScanSegmentTableRequest>,
1239 ) -> tonic::Result<tonic::Response<Self::ScanSegmentTableStream>> {
1240 let (mut record_batch, request) = {
1241 let store = self.store.read().await;
1242 let entry_id = get_entry_id_from_headers(&store, &request)?;
1243 let dataset = store.dataset(entry_id)?;
1244 let record_batch = dataset.segment_table().await.map_err(|err| {
1245 tonic::Status::internal(format!("Unable to read segment table: {err:#}"))
1246 })?;
1247 (record_batch, request.into_inner())
1248 };
1249
1250 record_batch = apply_segment_id_filter(record_batch, request.segment_id_filter.as_ref())?;
1251
1252 if !request.columns.is_empty() {
1254 record_batch = record_batch
1255 .project_columns(request.columns.iter().map(|s| s.as_str()))
1256 .map_err(|err| {
1257 tonic::Status::invalid_argument(format!("Unable to project columns: {err:#}"))
1258 })?;
1259 }
1260
1261 let stream = futures::stream::once(async move {
1262 Ok(ScanSegmentTableResponse {
1263 data: Some(record_batch.into()),
1264 })
1265 });
1266
1267 Ok(tonic::Response::new(
1268 Box::pin(stream) as Self::ScanSegmentTableStream
1269 ))
1270 }
1271
1272 async fn get_dataset_manifest_schema(
1273 &self,
1274 request: Request<GetDatasetManifestSchemaRequest>,
1275 ) -> tonic::Result<Response<GetDatasetManifestSchemaResponse>> {
1276 let store = self.store.read().await;
1277
1278 let entry_id = get_entry_id_from_headers(&store, &request)?;
1279 let dataset = store.dataset(entry_id)?;
1280 let record_batch = dataset.dataset_manifest().await?;
1281
1282 Ok(tonic::Response::new(GetDatasetManifestSchemaResponse {
1283 schema: Some(
1284 record_batch
1285 .schema_ref()
1286 .as_ref()
1287 .try_into()
1288 .map_err(|err| {
1289 tonic::Status::internal(format!(
1290 "unable to serialize Arrow schema: {err:#}"
1291 ))
1292 })?,
1293 ),
1294 }))
1295 }
1296
1297 type ScanDatasetManifestStream = ScanDatasetManifestResponseStream;
1298
1299 async fn scan_dataset_manifest(
1300 &self,
1301 request: Request<ScanDatasetManifestRequest>,
1302 ) -> tonic::Result<Response<Self::ScanDatasetManifestStream>> {
1303 let (mut record_batch, request) = {
1304 let store = self.store.read().await;
1305 let entry_id = get_entry_id_from_headers(&store, &request)?;
1306 let dataset = store.dataset(entry_id)?;
1307 let record_batch = dataset.dataset_manifest().await?;
1308 (record_batch, request.into_inner())
1309 };
1310
1311 record_batch = apply_segment_id_filter(record_batch, request.segment_id_filter.as_ref())?;
1312
1313 if !request.columns.is_empty() {
1315 record_batch = record_batch
1316 .project_columns(request.columns.iter().map(|s| s.as_str()))
1317 .map_err(|err| {
1318 tonic::Status::invalid_argument(format!("Unable to project columns: {err:#}"))
1319 })?;
1320 }
1321
1322 let stream = futures::stream::once(async move {
1323 Ok(ScanDatasetManifestResponse {
1324 data: Some(record_batch.into()),
1325 })
1326 });
1327
1328 Ok(tonic::Response::new(
1329 Box::pin(stream) as Self::ScanDatasetManifestStream
1330 ))
1331 }
1332
1333 async fn get_dataset_schema(
1334 &self,
1335 request: tonic::Request<re_protos::cloud::v1alpha1::GetDatasetSchemaRequest>,
1336 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::GetDatasetSchemaResponse>> {
1337 let store = self.store.read().await;
1338 let entry_id = get_entry_id_from_headers(&store, &request)?;
1339
1340 let dataset = store.dataset(entry_id)?;
1341 let schema = dataset.schema().map_err(|err| {
1342 tonic::Status::internal(format!("Unable to read dataset schema: {err:#}"))
1343 })?;
1344
1345 Ok(tonic::Response::new(GetDatasetSchemaResponse {
1346 schema: Some((&schema).try_into().map_err(|err| {
1347 tonic::Status::internal(format!("Unable to serialize Arrow schema: {err:#}"))
1348 })?),
1349 }))
1350 }
1351
1352 type GetRrdManifestStream = GetRrdManifestResponseStream;
1353
1354 async fn get_rrd_manifest(
1355 &self,
1356 request: tonic::Request<re_protos::cloud::v1alpha1::GetRrdManifestRequest>,
1357 ) -> tonic::Result<tonic::Response<Self::GetRrdManifestStream>> {
1358 let store = self.store.read().await;
1359 let entry_id = get_entry_id_from_headers(&store, &request)?;
1360
1361 let request = request.into_inner();
1362 let segment_id = request
1363 .segment_id
1364 .ok_or_else(|| {
1365 missing_field!(
1366 re_protos::cloud::v1alpha1::GetRrdManifestRequest,
1367 "segment_id"
1368 )
1369 })?
1370 .try_into()?;
1371
1372 let dataset = store.dataset(entry_id)?;
1373 let rrd_manifest = dataset.rrd_manifest(&segment_id)?;
1374
1375 let rrd_manifest_stream =
1376 futures::stream::once(futures::future::ok(GetRrdManifestResponse {
1377 rrd_manifest: Some(rrd_manifest.to_transport(()).map_err(|err| {
1378 tonic::Status::internal(format!("Unable to compute RRD manifest: {err:#}"))
1379 })?),
1380 manifest_key: None,
1381 }));
1382
1383 Ok(tonic::Response::new(
1384 Box::pin(rrd_manifest_stream) as Self::GetRrdManifestStream
1385 ))
1386 }
1387
1388 type GetAssetsForSegmentStream = GetAssetsForSegmentResponseStream;
1389
1390 async fn get_assets_for_segment(
1391 &self,
1392 request: tonic::Request<re_protos::cloud::v1alpha1::GetAssetsForSegmentRequest>,
1393 ) -> tonic::Result<tonic::Response<Self::GetAssetsForSegmentStream>> {
1394 let store = self.store.read().await;
1395
1396 let dataset_id = get_entry_id_from_headers(&store, &request)?;
1397
1398 let dataset = store.dataset(dataset_id)?;
1399
1400 let dataset_kind = dataset.dataset_kind();
1401 if dataset_kind != DatasetKind::Recording {
1402 return Err(tonic::Status::invalid_argument(format!(
1403 "assets can only be queried on recording datasets, this is a {dataset_kind:?} dataset"
1404 )));
1405 }
1406
1407 let Some(asset_dataset) = dataset.dataset_details().asset_dataset else {
1411 return Ok(tonic::Response::new(
1412 Box::pin(futures::stream::empty()) as Self::GetAssetsForSegmentStream
1413 ));
1414 };
1415
1416 let asset_segment_ids = store
1418 .dataset(asset_dataset)?
1419 .segments()
1420 .keys()
1421 .cloned()
1422 .map(Into::into)
1423 .collect();
1424
1425 let response = futures::stream::once(futures::future::ok(
1426 re_protos::cloud::v1alpha1::GetAssetsForSegmentResponse {
1427 assets_entry: Some(asset_dataset.into()),
1428 asset_segment_ids,
1429 },
1430 ));
1431
1432 Ok(tonic::Response::new(
1433 Box::pin(response) as Self::GetAssetsForSegmentStream
1434 ))
1435 }
1436
1437 type QueryDatasetStream = QueryDatasetResponseStream;
1440
1441 async fn query_dataset(
1442 &self,
1443 request: tonic::Request<re_protos::cloud::v1alpha1::QueryDatasetRequest>,
1444 ) -> tonic::Result<tonic::Response<Self::QueryDatasetStream>> {
1445 if !request.get_ref().chunk_ids.is_empty() {
1446 return Err(tonic::Status::unimplemented(
1447 "query_dataset: querying specific chunk ids is not implemented",
1448 ));
1449 }
1450
1451 let entry_id = get_entry_id_from_headers(&*self.store.read().await, &request)?;
1452
1453 let QueryDatasetRequest {
1454 segment_ids,
1455 entity_paths,
1456 select_all_entity_paths,
1457
1458 chunk_ids: requested_chunk_ids,
1460 fuzzy_descriptors: _,
1461 exclude_static_data,
1462 exclude_temporal_data,
1463 scan_parameters,
1464 query,
1465 generate_direct_urls: _,
1466 } = request.into_inner().try_into()?;
1467
1468 if scan_parameters.is_some() {
1469 re_log::debug_once!(" scan_parameters are not yet implemented and will be ignored");
1471 }
1472
1473 let entity_paths: IntSet<EntityPath> = entity_paths.into_iter().collect();
1474 if select_all_entity_paths && !entity_paths.is_empty() {
1475 return Err(tonic::Status::invalid_argument(
1476 "cannot specify entity paths if `select_all_entity_paths` is true",
1477 ));
1478 }
1479
1480 let per_segment_index_values: Option<BTreeMap<SegmentId, Vec<re_log_types::TimeInt>>> =
1487 match query.as_ref().and_then(|q| q.latest_at.as_ref()) {
1488 Some(la) if !la.per_segment_values.is_empty() => Some(
1489 std::iter::zip(&segment_ids, &la.per_segment_values)
1490 .map(|(sid, values)| {
1491 (
1492 sid.clone(),
1493 values
1494 .iter()
1495 .map(|v| re_log_types::TimeInt::new_temporal(*v))
1496 .collect(),
1497 )
1498 })
1499 .collect(),
1500 ),
1501 _ => None,
1502 };
1503
1504 let segments_of_interest = (!segment_ids.is_empty()).then_some(segment_ids.as_slice());
1506
1507 let chunk_stores = self
1508 .get_chunk_stores(entry_id, segments_of_interest)
1509 .await?;
1510
1511 if chunk_stores.is_empty() {
1512 let stream = futures::stream::iter([{
1513 let batch = QueryDatasetDataframe::empty_record_batch();
1514 let data = Some(batch.into());
1515 Ok(QueryDatasetResponse { data })
1516 }]);
1517
1518 return Ok(tonic::Response::new(
1519 Box::pin(stream) as Self::QueryDatasetStream
1520 ));
1521 }
1522
1523 let all_timelines: BTreeMap<String, arrow::datatypes::DataType> = chunk_stores
1528 .iter()
1529 .flat_map(|(_, _, _, resolved)| {
1530 resolved
1531 .schema()
1532 .timelines()
1533 .into_values()
1534 .map(|tl| (tl.name().as_str().to_owned(), tl.datatype()))
1535 .collect::<Vec<_>>()
1536 })
1537 .collect();
1538
1539 let stream = futures::stream::iter(chunk_stores.into_iter().map(
1540 move |(segment_id, layer_name, store_slot_id, resolved)| {
1541 let metadata_vec: Vec<ChunkMetadata> = if let Some(query) = &query {
1544 let (chunks, missing_virtual) = if let Some(map) = &per_segment_index_values {
1557 if let Some(values) = map.get(&segment_id) {
1558 let synthesized: Vec<re_log_types::TimeInt> = if values.is_empty() {
1559 vec![re_log_types::TimeInt::STATIC]
1560 } else {
1561 values.clone()
1562 };
1563 let mut all_chunks: Vec<Arc<Chunk>> = Vec::new();
1564 let mut all_missing: BTreeSet<ChunkId> = BTreeSet::new();
1565 let mut seen: BTreeSet<ChunkId> = BTreeSet::new();
1566 for v in &synthesized {
1567 let mut q = query.clone();
1568 if let Some(la) = q.latest_at.as_mut() {
1569 la.at = *v;
1570 la.per_segment_values = Vec::new();
1571 }
1572 let (cs, missing) = get_chunks_for_query_results(
1573 &resolved,
1574 &entity_paths,
1575 select_all_entity_paths,
1576 &q,
1577 );
1578 for c in cs {
1579 if seen.insert(c.id()) {
1580 all_chunks.push(c);
1581 }
1582 }
1583 all_missing.extend(missing);
1584 }
1585 for id in &seen {
1586 all_missing.remove(id);
1587 }
1588 (all_chunks, all_missing.into_iter().collect())
1589 } else {
1590 (Vec::new(), Vec::new())
1591 }
1592 } else {
1593 get_chunks_for_query_results(
1594 &resolved,
1595 &entity_paths,
1596 select_all_entity_paths,
1597 query,
1598 )
1599 };
1600
1601 let mut metas: Vec<_> = chunks
1602 .iter()
1603 .map(|c| ChunkMetadata::from_chunk(c))
1604 .collect();
1605 if let ResolvedStore::Lazy(lazy) = &resolved {
1606 for chunk_id in &missing_virtual {
1607 if let Some(idx) = lazy.chunk_row_index(chunk_id) {
1608 metas.push(ChunkMetadata::from_manifest(
1609 lazy.manifest(),
1610 *chunk_id,
1611 idx,
1612 lazy.timeline_ranges().get(chunk_id),
1613 ));
1614 }
1615 }
1616 }
1617 metas
1618 } else {
1619 match &resolved {
1620 ResolvedStore::Eager(h) => h
1621 .read()
1622 .iter_physical_chunks()
1623 .map(|c| ChunkMetadata::from_chunk(c))
1624 .collect(),
1625 ResolvedStore::Lazy(lazy) => lazy
1626 .manifest()
1627 .col_chunk_ids()
1628 .iter()
1629 .enumerate()
1630 .map(|(idx, &chunk_id)| {
1631 ChunkMetadata::from_manifest(
1632 lazy.manifest(),
1633 chunk_id,
1634 idx,
1635 lazy.timeline_ranges().get(&chunk_id),
1636 )
1637 })
1638 .collect(),
1639 }
1640 };
1641
1642 let num_chunks = metadata_vec.len();
1643
1644 let mut chunk_ids = Vec::with_capacity(num_chunks);
1645 let mut chunk_segment_ids = Vec::with_capacity(num_chunks);
1646 let mut chunk_keys = Vec::with_capacity(num_chunks);
1647 let mut chunk_entity_path = Vec::with_capacity(num_chunks);
1648 let mut chunk_is_static = Vec::with_capacity(num_chunks);
1649 let mut chunk_byte_sizes = Vec::with_capacity(num_chunks);
1650 let mut chunk_byte_sizes_uncompressed = Vec::with_capacity(num_chunks);
1651 let mut chunk_direct_urls = Vec::with_capacity(num_chunks);
1652 let mut chunk_direct_url_expiry = Vec::with_capacity(num_chunks);
1653
1654 let mut timelines: BTreeMap<
1658 String,
1659 (arrow::datatypes::DataType, Vec<Option<i64>>),
1660 > = all_timelines
1661 .iter()
1662 .map(|(name, dtype)| {
1663 (
1664 name.clone(),
1665 (dtype.clone(), Vec::with_capacity(num_chunks)),
1666 )
1667 })
1668 .collect();
1669
1670 for meta in &metadata_vec {
1671 if !select_all_entity_paths && !entity_paths.contains(&meta.entity_path) {
1672 continue;
1673 }
1674
1675 if !requested_chunk_ids.is_empty()
1676 && !requested_chunk_ids.contains(&meta.chunk_id)
1677 {
1678 continue;
1679 }
1680
1681 if exclude_static_data && meta.is_static {
1683 continue;
1684 }
1685 if exclude_temporal_data && !meta.is_static {
1686 continue;
1687 }
1688
1689 let mut missing_timelines: BTreeSet<String> =
1690 timelines.keys().cloned().collect();
1691 for (timeline_name, range) in &meta.timelines {
1692 let timeline_name = timeline_name.as_str();
1693 missing_timelines.remove(timeline_name);
1694
1695 let timeline_data = timelines
1696 .get_mut(timeline_name)
1697 .expect("timeline was pre-seeded from chunk stores");
1698
1699 timeline_data.1.push(Some(range.min().as_i64()));
1700 }
1701 for timeline_name in missing_timelines {
1702 let timeline_data = timelines
1703 .get_mut(&timeline_name)
1704 .expect("timeline_names already checked");
1705
1706 timeline_data.1.push(None);
1707 }
1708
1709 chunk_segment_ids.push(segment_id.clone());
1710 chunk_ids.push(meta.chunk_id);
1711 chunk_entity_path.push(meta.entity_path.clone());
1712 chunk_is_static.push(meta.is_static);
1713 chunk_byte_sizes.push(meta.byte_size);
1714 chunk_byte_sizes_uncompressed.push(Some(meta.byte_size));
1716
1717 chunk_keys.push(
1718 ChunkKey {
1719 chunk_id: meta.chunk_id,
1720 store_slot_id,
1721 }
1722 .encode()?,
1723 );
1724
1725 chunk_direct_urls.push(None);
1726 chunk_direct_url_expiry.push(None);
1727 }
1728
1729 let chunk_layer_names = vec![layer_name.clone(); chunk_ids.len()];
1730 let chunk_key_refs = chunk_keys.iter().map(|v| v.as_slice()).collect();
1731 let batch = QueryDatasetResponse::create_dataframe_with_timelines(
1732 chunk_ids,
1733 chunk_segment_ids,
1734 chunk_layer_names,
1735 chunk_key_refs,
1736 chunk_entity_path,
1737 chunk_is_static,
1738 chunk_byte_sizes,
1739 chunk_byte_sizes_uncompressed,
1740 chunk_direct_urls,
1741 chunk_direct_url_expiry,
1742 &timelines,
1743 )
1744 .map_err(|err| {
1745 tonic::Status::internal(format!("Failed to create dataframe: {err:#}"))
1746 })?;
1747
1748 let data = Some(batch.into());
1749
1750 Ok(QueryDatasetResponse { data })
1751 },
1752 ));
1753
1754 Ok(tonic::Response::new(
1755 Box::pin(stream) as Self::QueryDatasetStream
1756 ))
1757 }
1758
1759 type FetchChunksStream = FetchChunksResponseStream;
1760
1761 async fn fetch_chunks(
1765 &self,
1766 request: tonic::Request<re_protos::cloud::v1alpha1::FetchChunksRequest>,
1767 ) -> tonic::Result<tonic::Response<Self::FetchChunksStream>> {
1768 let request = request.into_inner();
1770
1771 let mut chunk_keys = vec![];
1772 for chunk_info_data in request.chunk_infos {
1773 let chunk_info_batch: RecordBatch = chunk_info_data.try_into().map_err(|err| {
1774 tonic::Status::internal(format!("Failed to decode chunk_info: {err:#}"))
1775 })?;
1776
1777 let schema = chunk_info_batch.schema();
1778
1779 let chunk_key_col_idx = schema
1780 .column_with_name(FetchChunksRequest::FIELD_CHUNK_KEY)
1781 .ok_or_else(|| {
1782 tonic::Status::invalid_argument(format!(
1783 "Missing {} column",
1784 FetchChunksRequest::FIELD_CHUNK_KEY
1785 ))
1786 })?
1787 .0;
1788
1789 let chunk_keys_arr = chunk_info_batch
1790 .column(chunk_key_col_idx)
1791 .as_any()
1792 .downcast_ref::<BinaryArray>()
1793 .ok_or_else(|| {
1794 tonic::Status::invalid_argument(format!(
1795 "{} must be binary array",
1796 FetchChunksRequest::FIELD_CHUNK_KEY
1797 ))
1798 })?;
1799
1800 for chunk_key in chunk_keys_arr {
1801 let chunk_key = chunk_key.ok_or_else(|| {
1802 tonic::Status::invalid_argument(format!(
1803 "{} must not be null",
1804 FetchChunksRequest::FIELD_CHUNK_KEY
1805 ))
1806 })?;
1807
1808 let chunk_key = ChunkKey::decode(chunk_key)?;
1809 chunk_keys.push(chunk_key);
1810 }
1811 }
1812
1813 let chunks = self
1814 .store
1815 .read()
1816 .await
1817 .chunks_from_chunk_keys(&chunk_keys)
1818 .await?;
1819
1820 let stream = futures::stream::iter(chunks).map(|(store_id, chunk)| {
1821 let arrow_msg = re_log_types::ArrowMsg {
1822 chunk_id: *chunk.id(),
1823 batch: chunk.to_record_batch().map_err(|err| {
1824 tonic::Status::internal(format!(
1825 "failed to convert chunk to record batch: {err:#}"
1826 ))
1827 })?,
1828 on_release: None,
1829 };
1830
1831 let compression = re_log_encoding::Compression::Off;
1832
1833 let encoded_chunk = arrow_msg
1834 .to_transport((store_id, compression))
1835 .map_err(|err| tonic::Status::internal(format!("encoding failed: {err:#}")))?;
1836
1837 Ok(re_protos::cloud::v1alpha1::FetchChunksResponse {
1838 chunks: vec![encoded_chunk],
1839 })
1840 });
1841
1842 Ok(tonic::Response::new(
1843 Box::pin(stream) as Self::FetchChunksStream
1844 ))
1845 }
1846
1847 async fn register_table(
1850 &self,
1851 request: tonic::Request<RegisterTableRequest>,
1852 ) -> tonic::Result<tonic::Response<RegisterTableResponse>> {
1853 cfg_select! {
1854 target_arch = "wasm32" => {
1855 let _ = request;
1856 return Err(tonic::Status::unimplemented(
1857 "register_table is not supported on wasm",
1858 ));
1859 }
1860 _ => {
1861 #[cfg_attr(not(feature = "lance"), expect(unused_mut))]
1862 let mut store = self.store.write().await;
1863 let request = request.into_inner();
1864 let Some(provider_details) = request.provider_details else {
1865 return Err(tonic::Status::invalid_argument("Missing provider details"));
1866 };
1867 #[cfg_attr(not(feature = "lance"), expect(unused_variables))]
1868 let lance_table = match ProviderDetails::try_from(&provider_details) {
1869 Ok(ProviderDetails::LanceTable(lance_table)) => lance_table.table_url,
1870 Ok(ProviderDetails::SystemTable(_)) => Err(Status::invalid_argument(
1871 "System tables cannot be registered",
1872 ))?,
1873 Err(err) => return Err(err.into()),
1874 }
1875 .to_file_path()
1876 .map_err(|()| tonic::Status::invalid_argument("Invalid lance table path"))?;
1877
1878 cfg_select! {
1879 feature = "lance" => {
1880 let named_path = NamedPath {
1881 name: Some(request.name.clone()),
1882 path: lance_table,
1883 };
1884
1885 let entry_id = store
1886 .load_directory_as_table(&named_path, IfDuplicateBehavior::Error)
1887 .await?;
1888 }
1889 _ => {
1890 let entry_id = EntryId::new();
1891 }
1892 }
1893
1894 let table_entry = store
1895 .table(entry_id)
1896 .ok_or_else(|| Status::internal("table missing that was just registered"))?
1897 .as_table_entry();
1898
1899 let response = RegisterTableResponse {
1900 table_entry: Some(table_entry.try_into()?),
1901 };
1902
1903 self.notify(watch_events_response::Kind::EntryCreated(
1904 EntryCreatedEvent {
1905 id: Some(entry_id.into()),
1906 },
1907 ));
1908
1909 Ok(response.into())
1910 }
1911 }
1912 }
1913
1914 async fn get_table_schema(
1915 &self,
1916 request: tonic::Request<re_protos::cloud::v1alpha1::GetTableSchemaRequest>,
1917 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::GetTableSchemaResponse>> {
1918 let store = self.store.read().await;
1919 let Some(entry_id) = request.into_inner().table_id else {
1920 return Err(Status::not_found("Table ID not specified in request"));
1921 };
1922 let entry_id = entry_id.try_into()?;
1923
1924 let table = store
1925 .table(entry_id)
1926 .ok_or_else(|| Status::not_found(format!("Entry with ID {entry_id} not found")))?;
1927
1928 let schema = table.schema();
1929
1930 Ok(tonic::Response::new(
1931 re_protos::cloud::v1alpha1::GetTableSchemaResponse {
1932 schema: Some(schema.as_ref().try_into().map_err(|err| {
1933 Status::internal(format!("Unable to serialize Arrow schema: {err:#}"))
1934 })?),
1935 },
1936 ))
1937 }
1938
1939 type ScanTableStream = ScanTableResponseStream;
1940
1941 async fn scan_table(
1942 &self,
1943 request: tonic::Request<re_protos::cloud::v1alpha1::ScanTableRequest>,
1944 ) -> tonic::Result<tonic::Response<Self::ScanTableStream>> {
1945 let Some(entry_id) = request.into_inner().table_id else {
1946 return Err(Status::not_found("Table ID not specified in request"));
1947 };
1948 let entry_id = entry_id.try_into()?;
1949
1950 let provider = {
1951 let store = self.store.read().await;
1952 let table = store
1953 .table(entry_id)
1954 .ok_or_else(|| Status::not_found(format!("Entry with ID {entry_id} not found")))?;
1955 table.provider()
1956 };
1957
1958 let ctx = SessionContext::default();
1959 let plan = provider
1960 .scan(&ctx.state(), None, &[], None)
1961 .await
1962 .map_err(|err| Status::internal(format!("failed to scan table: {err:#}")))?;
1963
1964 let stream = plan
1965 .execute(0, ctx.task_ctx())
1966 .map_err(|err| tonic::Status::from_error(Box::new(err)))?;
1967
1968 let resp_stream = stream.map(|batch| {
1969 let batch = batch.map_err(|err| tonic::Status::from_error(Box::new(err)))?;
1970 Ok(ScanTableResponse {
1971 dataframe_part: Some(batch.into()),
1972 })
1973 });
1974
1975 Ok(tonic::Response::new(
1976 Box::pin(resp_stream) as Self::ScanTableStream
1977 ))
1978 }
1979
1980 async fn query_tasks(
1983 &self,
1984 request: tonic::Request<QueryTasksRequest>,
1985 ) -> tonic::Result<tonic::Response<QueryTasksResponse>> {
1986 let task_ids = request.into_inner().ids;
1987 let store = self.store.read().await;
1988
1989 let mut ids = Vec::with_capacity(task_ids.len());
1990 let mut exec_statuses = Vec::with_capacity(task_ids.len());
1991 let mut msgs = Vec::with_capacity(task_ids.len());
1992
1993 for task_id in task_ids {
1994 let result = store
1997 .task_registry()
1998 .get(&task_id)
1999 .unwrap_or_else(TaskResult::success);
2000
2001 ids.push(task_id);
2002 exec_statuses.push(result.exec_status);
2003 msgs.push(if result.msgs.is_empty() {
2004 None
2005 } else {
2006 Some(result.msgs)
2007 });
2008 }
2009
2010 let num_tasks = ids.len();
2011 let rb = QueryTasksDataframe {
2012 task_id: ids.into(),
2013 kind: vec![None::<String>; num_tasks].into(),
2014 data: vec![None::<String>; num_tasks].into(),
2015 exec_status: exec_statuses.into(),
2016 msgs: msgs.into(),
2017 blob_len: vec![None::<u64>; num_tasks].into(),
2018 lease_owner: vec![None::<String>; num_tasks].into(),
2019 lease_expiration: vec![None::<i64>; num_tasks].into(),
2020 attempts: vec![1_u8; num_tasks].into(),
2021 creation_time: vec![None::<i64>; num_tasks].into(),
2022 last_update_time: vec![None::<i64>; num_tasks].into(),
2023 }
2024 .into_record_batch()
2025 .map_err(|err| tonic::Status::internal(format!("Failed to create dataframe: {err:#}")))?;
2026
2027 Ok(tonic::Response::new(QueryTasksResponse {
2029 data: Some(rb.into()),
2030 }))
2031 }
2032
2033 type QueryTasksOnCompletionStream = QueryTasksOnCompletionResponseStream;
2034
2035 async fn query_tasks_on_completion(
2036 &self,
2037 request: tonic::Request<QueryTasksOnCompletionRequest>,
2038 ) -> tonic::Result<tonic::Response<Self::QueryTasksOnCompletionStream>> {
2039 let task_ids = request.into_inner().ids;
2040
2041 let response_data = self
2043 .query_tasks(tonic::Request::new(QueryTasksRequest { ids: task_ids }))
2044 .await?
2045 .into_inner()
2046 .data;
2047
2048 Ok(tonic::Response::new(
2049 Box::pin(futures::stream::once(async move {
2050 Ok(QueryTasksOnCompletionResponse {
2051 data: response_data,
2052 })
2053 })) as Self::QueryTasksOnCompletionStream,
2054 ))
2055 }
2056
2057 async fn cancel_tasks(
2058 &self,
2059 _request: tonic::Request<CancelTasksRequest>,
2060 ) -> tonic::Result<tonic::Response<CancelTasksResponse>> {
2061 Ok(tonic::Response::new(CancelTasksResponse {}))
2063 }
2064
2065 async fn do_maintenance(
2066 &self,
2067 _request: tonic::Request<re_protos::cloud::v1alpha1::DoMaintenanceRequest>,
2068 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::DoMaintenanceResponse>> {
2069 Err(tonic::Status::unimplemented(
2070 "do_maintenance not implemented",
2071 ))
2072 }
2073
2074 async fn do_global_maintenance(
2075 &self,
2076 _request: tonic::Request<re_protos::cloud::v1alpha1::DoGlobalMaintenanceRequest>,
2077 ) -> tonic::Result<tonic::Response<re_protos::cloud::v1alpha1::DoGlobalMaintenanceResponse>>
2078 {
2079 Err(tonic::Status::unimplemented(
2080 "do_global_maintenance not implemented",
2081 ))
2082 }
2083
2084 async fn create_table_entry(
2085 &self,
2086 request: Request<re_protos::cloud::v1alpha1::CreateTableEntryRequest>,
2087 ) -> tonic::Result<Response<re_protos::cloud::v1alpha1::CreateTableEntryResponse>> {
2088 let request: CreateTableEntryRequest = request.into_inner().try_into()?;
2089 let table_name = request.name;
2090
2091 let schema = Arc::new(request.schema);
2092
2093 cfg_select! {
2094 target_arch = "wasm32" => {
2095 let Some(details) = request.provider_details else {
2096 return Err(tonic::Status::unimplemented(
2097 "filesystem-backed table creation is not supported on wasm",
2098 ));
2099 };
2100 }
2101 _ => {
2102 let details = if let Some(details) = request.provider_details {
2103 details
2104 } else {
2105 let table_path = self
2108 .settings
2109 .storage_dir
2110 .path()
2111 .join(format!("lance-{}", Tuid::new()));
2112 ProviderDetails::LanceTable(ext::LanceTable {
2113 table_url: url::Url::from_directory_path(table_path).map_err(|_err| {
2114 Status::internal(format!(
2115 "Failed to create table directory in {:?}",
2116 self.settings.storage_dir.path()
2117 ))
2118 })?,
2119 })
2120 };
2121 }
2122 }
2123
2124 cfg_select! {
2125 target_arch = "wasm32" => {
2126 let _ = (table_name, schema, details);
2127 return Err(tonic::Status::unimplemented(
2128 "filesystem-backed table creation is not supported on wasm",
2129 ));
2130 }
2131 _ => {
2132 let table = match details {
2133 ProviderDetails::LanceTable(table) => {
2134 self.store
2135 .write()
2136 .await
2137 .create_table_entry(table_name, &table.table_url, schema)
2138 .await?
2139 }
2140 ProviderDetails::SystemTable(_) => {
2141 return Err(tonic::Status::invalid_argument(
2142 "Creating system tables is not supported",
2143 ));
2144 }
2145 };
2146
2147 self.notify(watch_events_response::Kind::EntryCreated(
2148 EntryCreatedEvent {
2149 id: Some(table.details.id.into()),
2150 },
2151 ));
2152
2153 Ok(Response::new(
2154 CreateTableEntryResponse { table }.try_into()?,
2155 ))
2156 }
2157 }
2158 }
2159}
2160
2161fn get_entry_id_from_headers<T>(
2163 store: &InMemoryStore,
2164 req: &tonic::Request<T>,
2165) -> tonic::Result<EntryId> {
2166 if let Some(entry_id) = req.entry_id()? {
2167 Ok(entry_id)
2168 } else if let Some(dataset_name) = req.entry_name()? {
2169 Ok(store.dataset_by_name(&dataset_name)?.id())
2170 } else {
2171 const HEADERS: &[&str] = &[
2172 re_protos::headers::RERUN_HTTP_HEADER_ENTRY_ID,
2173 re_protos::headers::RERUN_HTTP_HEADER_ENTRY_NAME,
2174 ];
2175 Err(tonic::Status::invalid_argument(format!(
2176 "missing mandatory {HEADERS:?} HTTP headers"
2177 )))
2178 }
2179}
2180
2181fn latest_at_or_static(latest_at: &ext::QueryLatestAt) -> LatestAtQuery {
2183 match &latest_at.index {
2184 Some(index) => LatestAtQuery::new(*index, latest_at.at),
2185 None => LatestAtQuery::new_static(),
2186 }
2187}
2188
2189struct ChunkMetadata {
2191 chunk_id: ChunkId,
2192 entity_path: EntityPath,
2193 is_static: bool,
2194 byte_size: u64,
2195 timelines: IntMap<TimelineName, AbsoluteTimeRange>,
2196}
2197
2198impl ChunkMetadata {
2199 fn from_chunk(chunk: &Chunk) -> Self {
2200 let timelines = chunk
2201 .timelines()
2202 .values()
2203 .map(|col| (*col.timeline().name(), col.time_range()))
2204 .collect();
2205 Self {
2206 chunk_id: chunk.id(),
2207 entity_path: chunk.entity_path().clone(),
2208 is_static: chunk.is_static(),
2209 byte_size: re_byte_size::SizeBytes::total_size_bytes(chunk),
2210 timelines,
2211 }
2212 }
2213
2214 fn from_manifest(
2215 manifest: &re_log_encoding::RrdManifest,
2216 chunk_id: ChunkId,
2217 row_idx: usize,
2218 chunk_timelines: Option<&IntMap<TimelineName, AbsoluteTimeRange>>,
2219 ) -> Self {
2220 Self {
2221 chunk_id,
2222 entity_path: EntityPath::from(manifest.col_chunk_entity_path_raw().value(row_idx)),
2223 is_static: manifest.col_chunk_is_static_raw().value(row_idx),
2224 byte_size: manifest.col_chunk_byte_size_uncompressed()[row_idx],
2225 timelines: chunk_timelines.cloned().unwrap_or_default(),
2226 }
2227 }
2228}
2229
2230fn get_chunks_for_query_results(
2232 resolved: &ResolvedStore,
2233 entity_paths: &IntSet<EntityPath>,
2234 select_all_entity_paths: bool,
2235 query: &ext::Query,
2236) -> (Vec<Arc<Chunk>>, Vec<ChunkId>) {
2237 if query.latest_at.is_none() && query.range.is_none() {
2241 return match resolved {
2242 ResolvedStore::Eager(h) => (h.read().iter_physical_chunks().cloned().collect(), vec![]),
2243 ResolvedStore::Lazy(lazy) => (vec![], lazy.manifest().col_chunk_ids().to_vec()),
2244 };
2245 }
2246
2247 let paths = if select_all_entity_paths {
2248 resolved.all_entities()
2249 } else if entity_paths.is_empty() {
2250 return (Vec::new(), Vec::new());
2253 } else {
2254 entity_paths.clone()
2255 };
2256
2257 let mut all_chunks: Vec<Arc<Chunk>> = vec![];
2258 let mut all_missing: BTreeSet<ChunkId> = BTreeSet::new();
2259 let mut seen_physical: BTreeSet<ChunkId> = BTreeSet::new();
2260
2261 for entity_path in &paths {
2262 if let Some(latest_at) = &query.latest_at {
2263 let latest_at_q = latest_at_or_static(latest_at);
2264 let results = resolved.latest_at_relevant_chunks_for_all_components(
2265 ChunkTrackingMode::Report,
2266 &latest_at_q,
2267 entity_path,
2268 true,
2269 );
2270 for chunk in results.chunks {
2271 if seen_physical.insert(chunk.id()) {
2272 all_chunks.push(chunk);
2273 }
2274 }
2275 all_missing.extend(results.missing_virtual);
2276 }
2277 if let Some(range) = &query.range {
2278 let range_q = RangeQuery::new(range.index, range.index_range);
2279 let results = resolved.range_relevant_chunks_for_all_components(
2280 ChunkTrackingMode::Report,
2281 &range_q,
2282 entity_path,
2283 true,
2284 );
2285 for chunk in results.chunks {
2286 if seen_physical.insert(chunk.id()) {
2287 all_chunks.push(chunk);
2288 }
2289 }
2290 for chunk_id in results.missing_virtual {
2299 let keep = match resolved {
2300 ResolvedStore::Eager(_) => true,
2302 ResolvedStore::Lazy(lazy) => match lazy.timeline_ranges().get(&chunk_id) {
2303 None => true,
2306 Some(per_timeline) => per_timeline
2307 .get(&range.index)
2308 .is_some_and(|time_range| time_range.intersects(range.index_range)),
2309 },
2310 };
2311 if keep {
2312 all_missing.insert(chunk_id);
2313 }
2314 }
2315 }
2316 }
2317
2318 for id in &seen_physical {
2320 all_missing.remove(id);
2321 }
2322
2323 (all_chunks, all_missing.into_iter().collect())
2324}
2325
2326fn bandwidth_test_stream(
2329 num_bytes: u64,
2330) -> impl futures::Stream<Item = tonic::Result<DoBandwidthTestResponse>> + Send {
2331 futures::stream::iter(ext::BandwidthTestPayloadIter::new(num_bytes).map(Ok))
2332}
2333
2334#[cfg(test)]
2335mod tests {
2336 use super::*;
2337
2338 use futures::TryStreamExt as _;
2339 use re_protos::cloud::v1alpha1::GetAssetsForSegmentRequest;
2340 use re_protos::headers::RerunHeadersInjectorExt as _;
2341
2342 #[tokio::test]
2345 async fn legacy_dataset_without_asset_dataset() {
2346 let handler = RerunCloudHandlerBuilder::new().build();
2347
2348 let dataset_id = EntryId::new();
2349 handler
2350 .store
2351 .write()
2352 .await
2353 .create_dataset_impl(
2354 EntryName::new("legacy_dataset").unwrap(),
2355 dataset_id,
2356 DatasetKind::Recording,
2357 None,
2358 )
2359 .unwrap();
2360
2361 let responses: Vec<_> = handler
2362 .get_assets_for_segment(
2363 tonic::Request::new(GetAssetsForSegmentRequest {}).with_entry_id(dataset_id),
2364 )
2365 .await
2366 .expect("querying assets should succeed without an asset dataset")
2367 .into_inner()
2368 .try_collect()
2369 .await
2370 .unwrap();
2371 assert!(
2372 responses.is_empty(),
2373 "a dataset without an asset dataset should have no assets"
2374 );
2375
2376 let updated: ext::DatasetEntry = handler
2377 .update_dataset_entry(tonic::Request::new(
2378 UpdateDatasetEntryRequest {
2379 id: dataset_id,
2380 dataset_details: Default::default(),
2381 }
2382 .into(),
2383 ))
2384 .await
2385 .expect("updating the entry should succeed")
2386 .into_inner()
2387 .dataset
2388 .unwrap()
2389 .try_into()
2390 .unwrap();
2391
2392 let asset_dataset_id = updated
2393 .dataset_details
2394 .asset_dataset
2395 .expect("updating the entry should create the missing asset dataset");
2396 let store = handler.store.read().await;
2397 assert_eq!(
2398 store.dataset(asset_dataset_id).unwrap().dataset_kind(),
2399 DatasetKind::Asset,
2400 );
2401 }
2402}