1use crate::metadata::TableMetadata;
2use crate::runtime::TableSchemaTransformFactories;
3#[cfg(feature = "ffi")]
4use crate::table::build_scan_options_for_fields;
5use crate::table::{CompiledTable, compile_table, decode_table_scan_row, validate_name};
6use crate::{Result, TableError, TableSchema, Value};
7use bytes::Bytes;
8use cobble::{
9 Config, ReadOnlyDbBuilder, ScanOptions, ScanSplit, ScanSplitScanner, ShardSnapshotRef,
10 VolumeDescriptor, VolumeUsageKind,
11};
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14
15const TABLE_SCAN_PLAN_FORMAT: &str = "cobble-table-scan-plan";
16const TABLE_SCAN_PLAN_VERSION: u32 = 1;
17
18#[derive(Clone, Serialize, Deserialize)]
23pub struct TableScanPlan {
24 format: String,
25 version: u32,
26 name: String,
27 global_snapshot_id: u64,
28 total_buckets: u32,
29 metadata: TableMetadata,
30 shards: Vec<ShardSnapshotRef>,
31 source_volumes: Vec<VolumeDescriptor>,
32 #[serde(skip)]
33 auth_source: Option<Config>,
34}
35
36impl TableScanPlan {
37 pub(crate) fn from_global_reader(
38 name: String,
39 metadata: TableMetadata,
40 global_snapshot_id: u64,
41 total_buckets: u32,
42 shards: Vec<ShardSnapshotRef>,
43 config: Config,
44 ) -> Result<Self> {
45 let plan = Self {
46 format: TABLE_SCAN_PLAN_FORMAT.to_string(),
47 version: TABLE_SCAN_PLAN_VERSION,
48 name,
49 global_snapshot_id,
50 total_buckets,
51 metadata,
52 shards,
53 source_volumes: source_volumes(&config),
54 auth_source: Some(config),
55 };
56 plan.validate()?;
57 Ok(plan)
58 }
59
60 pub fn snapshot_id(&self) -> u64 {
62 self.global_snapshot_id
63 }
64
65 pub fn total_buckets(&self) -> u32 {
67 self.total_buckets
68 }
69
70 pub fn schema(&self) -> &TableSchema {
72 &self.metadata.schema
73 }
74
75 pub fn data_size_bytes(&self) -> u64 {
77 self.shards.iter().fold(0_u64, |total, shard| {
78 total.saturating_add(shard.data_size_bytes)
79 })
80 }
81
82 pub fn splits(&self) -> Result<Vec<TableScanSplit>> {
84 self.validate()?;
85 Ok(self
86 .shards
87 .iter()
88 .cloned()
89 .map(|shard| TableScanSplit {
90 format: self.format.clone(),
91 version: self.version,
92 name: self.name.clone(),
93 global_snapshot_id: self.global_snapshot_id,
94 total_buckets: self.total_buckets,
95 metadata: self.metadata.clone(),
96 split: full_scan_split(shard),
97 source_volumes: self.source_volumes.clone(),
98 auth_source: self.auth_source.clone(),
99 })
100 .collect())
101 }
102
103 fn validate(&self) -> Result<()> {
104 validate_plan(
105 &self.format,
106 self.version,
107 &self.name,
108 self.total_buckets,
109 &self.metadata,
110 &self.source_volumes,
111 )?;
112 if self.shards.is_empty() {
113 return Err(TableError::InvalidSchema(
114 "table scan plan has no shard snapshots".to_string(),
115 ));
116 }
117 Ok(())
118 }
119}
120
121#[derive(Clone, Serialize, Deserialize)]
123pub struct TableScanSplit {
124 format: String,
125 version: u32,
126 name: String,
127 global_snapshot_id: u64,
128 total_buckets: u32,
129 metadata: TableMetadata,
130 split: ScanSplit,
131 source_volumes: Vec<VolumeDescriptor>,
132 #[serde(skip)]
133 auth_source: Option<Config>,
134}
135
136impl TableScanSplit {
137 pub fn snapshot_id(&self) -> u64 {
139 self.global_snapshot_id
140 }
141
142 pub fn shard_snapshot_id(&self) -> u64 {
144 self.split.shard.snapshot_id
145 }
146
147 pub fn create_scanner(&self, runtime: Config) -> Result<TableScanSplitScanner> {
149 self.create_scanner_with_transforms(runtime, &TableSchemaTransformFactories::default())
150 }
151
152 pub fn scanner_builder(&self, runtime: Config) -> TableScanSplitScannerBuilder {
154 TableScanSplitScannerBuilder {
155 split: self.clone(),
156 runtime,
157 transforms: TableSchemaTransformFactories::default(),
158 }
159 }
160
161 fn create_scanner_with_transforms(
162 &self,
163 runtime: Config,
164 transforms: &TableSchemaTransformFactories,
165 ) -> Result<TableScanSplitScanner> {
166 let compiled = compile_table(self.metadata.clone(), self.total_buckets)?;
167 let scanner = self.create_raw_scanner_with_transforms(
168 runtime,
169 transforms,
170 ScanOptions::default().with_column_family(self.name.clone()),
171 )?;
172 Ok(TableScanSplitScanner {
173 inner: scanner,
174 compiled,
175 })
176 }
177
178 fn create_raw_scanner_with_transforms(
179 &self,
180 runtime: Config,
181 transforms: &TableSchemaTransformFactories,
182 scan_options: ScanOptions,
183 ) -> Result<ScanSplitScanner> {
184 self.validate()?;
185 let credential_source = self.auth_source.as_ref().unwrap_or(&runtime);
186 let source_volumes = self
187 .source_volumes
188 .iter()
189 .map(|volume| volume.with_credentials_from(credential_source))
190 .collect::<Vec<_>>();
191 let mut config = runtime_read_config(runtime);
192 config.volumes.extend(source_volumes);
193 config.total_buckets = self.total_buckets;
194
195 let builder = transforms.apply_to(ReadOnlyDbBuilder::new(config))?;
196 let scanner = self
197 .split
198 .create_scanner_with_builder(builder, &scan_options)?;
199 Ok(scanner)
200 }
201
202 #[cfg(feature = "ffi")]
203 pub(crate) fn create_projected_raw_scanner(
204 &self,
205 runtime: Config,
206 field_names: &[String],
207 read_ahead_bytes: i64,
208 ) -> Result<ScanSplitScanner> {
209 let compiled = compile_table(self.metadata.clone(), self.total_buckets)?;
210 let mut options = build_scan_options_for_fields(&self.name, compiled, field_names)?;
211 options.read_ahead_bytes = size::Size::from_const(read_ahead_bytes);
212 self.create_raw_scanner_with_transforms(
213 runtime,
214 &TableSchemaTransformFactories::default(),
215 options,
216 )
217 }
218
219 fn validate(&self) -> Result<()> {
220 validate_plan(
221 &self.format,
222 self.version,
223 &self.name,
224 self.total_buckets,
225 &self.metadata,
226 &self.source_volumes,
227 )?;
228 if self.split.shard.ranges.is_empty() {
229 return Err(TableError::InvalidSchema(
230 "table scan split has no bucket ranges".to_string(),
231 ));
232 }
233 if self.split.start.is_some()
234 || self.split.end.is_some()
235 || self.split.start_bucket.is_some()
236 || self.split.start_key_exclusive.is_some()
237 || self.split.end_bucket.is_some()
238 || self.split.end_key_inclusive.is_some()
239 {
240 return Err(TableError::InvalidSchema(
241 "table scan split must cover its complete shard".to_string(),
242 ));
243 }
244 Ok(())
245 }
246}
247
248pub struct TableScanSplitScannerBuilder {
250 split: TableScanSplit,
251 runtime: Config,
252 transforms: TableSchemaTransformFactories,
253}
254
255impl TableScanSplitScannerBuilder {
256 pub fn register_schema_transform<F, T>(
258 mut self,
259 transform_type: impl Into<String>,
260 factory: F,
261 ) -> Result<Self>
262 where
263 F: Fn(&[u8]) -> cobble::Result<T> + Send + Sync + 'static,
264 T: Fn(Option<Bytes>) -> cobble::Result<Option<Bytes>> + Send + Sync + 'static,
265 {
266 self.transforms.register(transform_type, factory)?;
267 Ok(self)
268 }
269
270 pub fn open(self) -> Result<TableScanSplitScanner> {
272 self.split
273 .create_scanner_with_transforms(self.runtime, &self.transforms)
274 }
275}
276
277pub struct TableScanSplitScanner {
279 inner: ScanSplitScanner,
280 compiled: Arc<CompiledTable>,
281}
282
283impl Iterator for TableScanSplitScanner {
284 type Item = Result<Vec<Value>>;
285
286 fn next(&mut self) -> Option<Self::Item> {
287 self.inner.next().map(|row| {
288 let (_, key, columns) = row?;
289 decode_table_scan_row(&self.compiled, &key, &columns)
290 })
291 }
292}
293
294fn full_scan_split(shard: ShardSnapshotRef) -> ScanSplit {
295 ScanSplit {
296 shard,
297 start: None,
298 end: None,
299 start_bucket: None,
300 start_key_exclusive: None,
301 end_bucket: None,
302 end_key_inclusive: None,
303 }
304}
305
306fn validate_plan(
307 format: &str,
308 version: u32,
309 name: &str,
310 total_buckets: u32,
311 metadata: &TableMetadata,
312 source_volumes: &[VolumeDescriptor],
313) -> Result<()> {
314 if format != TABLE_SCAN_PLAN_FORMAT {
315 return Err(TableError::InvalidSchema(format!(
316 "unsupported table scan plan format: {format}"
317 )));
318 }
319 if version != TABLE_SCAN_PLAN_VERSION {
320 return Err(TableError::InvalidSchema(format!(
321 "unsupported table scan plan version: {version}"
322 )));
323 }
324 validate_name(name.to_string())?;
325 metadata.validate()?;
326 if !(1..=u16::MAX as u32 + 1).contains(&total_buckets) {
327 return Err(TableError::InvalidSchema(
328 "table scan plan total_buckets must be in range 1..=65536".to_string(),
329 ));
330 }
331 if !source_volumes
332 .iter()
333 .any(|volume| volume.supports(VolumeUsageKind::Meta))
334 || source_volumes.iter().any(|volume| {
335 volume.supports(VolumeUsageKind::PrimaryDataPriorityHigh)
336 || volume.supports(VolumeUsageKind::PrimaryDataPriorityMedium)
337 || volume.supports(VolumeUsageKind::PrimaryDataPriorityLow)
338 || volume.supports(VolumeUsageKind::Cache)
339 || volume.supports(VolumeUsageKind::Readonly)
340 })
341 {
342 return Err(TableError::InvalidSchema(
343 "table scan plan has invalid source volumes".to_string(),
344 ));
345 }
346 Ok(())
347}
348
349fn source_volumes(config: &Config) -> Vec<VolumeDescriptor> {
350 config
351 .volumes
352 .iter()
353 .filter_map(|source| {
354 let mut volume = source.clone();
355 volume.kinds = 0;
356 for kind in [VolumeUsageKind::Meta, VolumeUsageKind::Snapshot] {
357 if source.supports(kind) {
358 volume.set_usage(kind);
359 }
360 }
361 (volume.kinds != 0).then_some(volume.without_credentials())
362 })
363 .collect()
364}
365
366fn runtime_read_config(mut runtime: Config) -> Config {
367 runtime.volumes = runtime
368 .volumes
369 .into_iter()
370 .filter_map(|source| {
371 let mut volume = source.clone();
372 volume.kinds = 0;
373 for kind in [
374 VolumeUsageKind::PrimaryDataPriorityHigh,
375 VolumeUsageKind::PrimaryDataPriorityMedium,
376 VolumeUsageKind::PrimaryDataPriorityLow,
377 VolumeUsageKind::Cache,
378 VolumeUsageKind::Readonly,
379 ] {
380 if source.supports(kind) {
381 volume.set_usage(kind);
382 }
383 }
384 (volume.kinds != 0).then_some(volume)
385 })
386 .collect();
387 runtime
388}