Skip to main content

re_server/rerun_cloud/
mod.rs

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
207// ---
208
209pub 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    /// Broadcast a catalog event to all `WatchEvents` subscribers.
233    fn notify(&self, kind: watch_events_response::Kind) {
234        // A send error just means there are no subscribers, which is fine.
235        let _ = self
236            .events_tx
237            .send(WatchEventsResponse { kind: Some(kind) })
238            .ok();
239    }
240
241    /// Returns all the chunk stores of the specified dataset and segment ids. If `segment_ids`
242    /// is `None`, return stores of all segments.
243    ///
244    /// Returns (segment id, layer name, store) tuples.
245    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                        // TODO(RR-5155): Support enumerating OPFS directories for prefix registration.
276                        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                        // Recursively walk the directory and grab all '.rrd' files
310                        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(&current_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
512/// Verifies that the referenced blueprint dataset (if any) exists and is itself a blueprint dataset.
513///
514/// Internal consistency of the `DatasetDetails`/`TableDetails` is checked separately via their
515/// `validate_consistency` methods.
516fn 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
540/// Same as [`validate_blueprint_dataset`], for the asset dataset.
541fn 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        // NOTE: Reminder that this is only fully filled iff CI=1.
572        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        // The local server has no authentication, so grant full access.
590        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    // --- Catalog ---
654
655    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        // `entry_kinds` (new, repeated) always wins over the legacy singular `entry_kind` when
669        // both are set. `ENTRY_KIND_UNSPECIFIED` is rejected outright; unknown *positive* values
670        // (kinds newer than this server knows about) are intentionally allowed through and
671        // simply match no entry, so a client requesting them degrades gracefully instead of
672        // erroring out (forward compat, mirrors Rerun Hub).
673        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        // The effective set of raw `EntryKind` values to match against. `None` for the
683        // kind-less default.
684        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            // Legacy singular field (pre hub 0.15)
688            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            // When neither the new `entry_kinds` nor legacy `entry_kind` (singular)
704            // are specified we fall back to the legacy default.
705            //
706            // See RR-5186.
707            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            // this is a find. Degrade a NotFound to an empty result set.
713            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            // `Dataset` and `AssetDataset` are both backed by `StoreKind::Recording`, so a
719            // request for just one of them still has to fetch the whole recording family and
720            // filter by actual kind below (an asset dataset otherwise leaks into
721            // `entry_kind=Dataset` results).
722            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        // The asset dataset reference is server-managed: unless the client explicitly points it
819        // at a new asset dataset, keep the stored one. Recording datasets created before asset
820        // datasets were introduced have none, so create the missing one on demand, and replace a
821        // reference left dangling by a deleted asset dataset the same way.
822        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        // Backwards compatibility: tables created before table blueprints had no associated
886        // blueprint dataset. If a client updates such a table without providing one, create the
887        // missing dataset on demand.
888        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    // --- Manifest Registry ---
961    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: _, // OSS doesn't even have statuses
1023        } = request.into_inner().try_into()?;
1024
1025        // As per our proto conventions, an empty list means "all":
1026        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    // TODO(RR-2017): This endpoint is in need of a deep redesign. For now it defaults to
1052    // overwriting the "base" layer.
1053    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            // Support both new "rerun:segment_id" and legacy "rerun:partition_id" keys
1077            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        // Build handles and register in pool first
1115        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        // Limit the scope of the lock here to prevent deadlocks
1150        // when reading and writing to the same table
1151        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    /* Query schemas */
1205
1206    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        // project columns
1253        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        // project columns
1314        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        // Datasets created before asset datasets were introduced don't have one, which simply
1408        // means no assets were ever registered. One is created on demand when the dataset entry
1409        // is next updated.
1410        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        // TODO(RR-4979): Filter by properties here.
1417        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    /* Queries */
1438
1439    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            //TODO(RR-2613): we must do a much better job at handling these
1459            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            // Logged at a low debug-level, because of https://github.com/rerun-io/rerun/pull/12578
1470            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        // RR-4355: per-segment index value pushdown.
1481        //
1482        // If the request has `query.latest_at.per_segment_values`, build a
1483        // map keyed by segment id so the per-segment chunk-fetch loop below
1484        // can apply it. The ext `try_from` already validated that lengths
1485        // match `segment_ids` and that there are no duplicates.
1486        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        // As per our proto conventions, an empty list means "all":
1505        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        // Compute the union of timelines across every (segment, layer) touched by this query, so
1524        // every response we emit below carries the same `{timeline}:start` columns and the client
1525        // can concatenate them. Individual responses fill in `None` for timelines their chunks
1526        // don't contain.
1527        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                // Build metadata for all relevant chunks (physical + virtual).
1542
1543                let metadata_vec: Vec<ChunkMetadata> = if let Some(query) = &query {
1544                    // RR-4355: per-segment index values pushdown.
1545                    //
1546                    // When the request carries `per_segment_values`, fan out
1547                    // `get_chunks_for_query_results` once per value for this
1548                    // segment with a synthesized latest-at, then dedup. Per
1549                    // the proto contract (`cloud.proto`):
1550                    //   "An empty values list for a segment means no temporal
1551                    //    chunks are returned for that segment (only static
1552                    //    data)."
1553                    // For the empty case we run a single static-only query
1554                    // instead of returning nothing, so static chunks still
1555                    // surface.
1556                    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                // Seed with the full set of timelines the query can see so the response schema
1655                // matches every other response in this stream, even for segments/layers whose
1656                // chunks don't use all those timelines.
1657                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                    // Filter by static/temporal data
1682                    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                    // OSS server stores decoded data, so compressed == uncompressed.
1715                    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    // NOTE: OSS server does not detect source drift (a registered rrd file
1762    // being mutated after registration) which Rerun Hub implements.
1763    // Consider if worth having parity (RR-4577).
1764    async fn fetch_chunks(
1765        &self,
1766        request: tonic::Request<re_protos::cloud::v1alpha1::FetchChunksRequest>,
1767    ) -> tonic::Result<tonic::Response<Self::FetchChunksStream>> {
1768        // worth noting that FetchChunks is not per-dataset request, it simply contains chunk infos
1769        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    // --- Table APIs ---
1848
1849    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    // --- Tasks service ---
1981
1982    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            // Look up the task in the registry, falling back to success for unknown IDs
1995            // (including legacy dummy IDs and stale task IDs)
1996            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        // All tasks finish immediately in the OSS server
2028        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        // All tasks finish immediately in the OSS server, so we can delegate to `query_tasks
2042        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        // Cancelling tasks is a noop in the OSS server
2062        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                    // Create a directory in the storage directory. We use a tuid to avoid collisions
2106                    // and avoid any sanitization issue with the provided table name.
2107                    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
2161/// Retrieves the entry ID based on HTTP headers.
2162fn 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
2181/// Return the equivalent latest at query
2182fn 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
2189/// Metadata for a single chunk, extractable from either a physical `Chunk` or a manifest.
2190struct 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
2230/// Returns physical chunks and missing virtual chunk IDs for a query.
2231fn 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    // Contract: a Query with neither `latest_at` nor `range` means "all chunks", regardless of
2238    // entity filter. This is exercised by the shared `re_redap_tests::query_dataset` "default" test
2239    // case.
2240    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        // Per `cloud.proto`: `(select_all_entity_paths=false, entity_paths=[])`
2251        // is a valid query that selects no entities and yields no results.
2252        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            // Range tightening for virtual chunks. `range_relevant_chunks_for_all_components`
2291            // post-filters physical chunks against the per-chunk timeline range, but the
2292            // start-time-indexed scan that produces `missing_virtual` can pull in chunks
2293            // whose actual time range falls outside the query (the index lookup widens by
2294            // the longest chunk interval). Without this drop, lazy stores leak those
2295            // chunks to the client and rows outside the requested range show up in the
2296            // result set. Latest-at is unaffected because it doesn't fan out via
2297            // `missing_virtual` here.
2298            for chunk_id in results.missing_virtual {
2299                let keep = match resolved {
2300                    // Eager stores already went through the physical post-filter above.
2301                    ResolvedStore::Eager(_) => true,
2302                    ResolvedStore::Lazy(lazy) => match lazy.timeline_ranges().get(&chunk_id) {
2303                        // No temporal entry => static chunk; let it through (matches the
2304                        // `chunk.is_static() && include_static` branch of the physical filter).
2305                        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    // Remove any virtual IDs that turned out to be physical in another entity's result.
2319    for id in &seen_physical {
2320        all_missing.remove(id);
2321    }
2322
2323    (all_chunks, all_missing.into_iter().collect())
2324}
2325
2326/// Streams `num_bytes` of pseudo-random (incompressible) bytes back to the client,
2327/// split into ~1 MiB chunks.
2328fn 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    /// Datasets created before asset datasets were introduced don't have one. Querying assets on
2343    /// such a dataset returns no assets, and updating its entry creates the missing asset dataset.
2344    #[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}