1pub mod append;
13pub mod append_report;
15pub mod coverage;
16pub mod error;
17mod optimize;
18pub mod scan;
19
20#[cfg(test)]
21pub(crate) mod test_util;
22
23#[cfg(test)]
24mod latest_snapshot_tests;
25
26use std::pin::Pin;
27
28use arrow::array::RecordBatch;
29use futures::Stream;
30use snafu::prelude::*;
31
32use crate::table::error::{
33 AlreadyExistsSnafu, EmptyTableSnafu, IndexSpecSnafu, NotTimeSeriesSnafu,
34 SchemaCompatibilitySnafu, TransactionLogSnafu, UnsupportedFormatVersionSnafu,
35};
36
37use crate::{
38 metadata::{
39 schema_compat::ensure_index_spec_matches_schema, table_metadata::TABLE_FORMAT_VERSION,
40 },
41 storage::TableLocation,
42 transaction_log::{
43 IndexSpec, LogAction, TableKind, TableMeta, TableState, TransactionLogStore,
44 },
45};
46
47pub use error::TableError;
48pub use optimize::OptimizeReport;
49
50pub type TimeSeriesScan = Pin<Box<dyn Stream<Item = Result<RecordBatch, TableError>> + Send>>;
54
55#[derive(Debug, Clone)]
63pub struct TimeSeriesTable {
64 log: TransactionLogStore,
65 state: TableState,
66 index: IndexSpec,
67}
68
69impl TimeSeriesTable {
70 pub fn state(&self) -> &TableState {
72 &self.state
73 }
74
75 #[allow(dead_code)]
80 pub(crate) fn state_mut(&mut self) -> &mut TableState {
81 &mut self.state
82 }
83
84 pub fn index_spec(&self) -> &IndexSpec {
86 &self.index
87 }
88
89 pub fn location(&self) -> &TableLocation {
91 self.log.location()
92 }
93
94 pub async fn open(location: TableLocation) -> Result<Self, TableError> {
102 let log = TransactionLogStore::new(location.clone());
103
104 let current_version = log
107 .load_current_version()
108 .await
109 .context(TransactionLogSnafu)?;
110
111 if current_version == 0 {
112 return EmptyTableSnafu.fail();
113 }
114
115 let state = log
117 .rebuild_table_state()
118 .await
119 .context(TransactionLogSnafu)?;
120
121 let index = match &state.table_meta.kind {
123 TableKind::TimeSeries(spec) => spec.clone(),
124 other => {
125 return NotTimeSeriesSnafu {
126 kind: other.clone(),
127 }
128 .fail();
129 }
130 };
131
132 Ok(Self { log, state, index })
133 }
134
135 pub async fn create(
144 location: TableLocation,
145 table_meta: TableMeta,
146 ) -> Result<Self, TableError> {
147 if table_meta.format_version() != TABLE_FORMAT_VERSION {
148 return UnsupportedFormatVersionSnafu {
149 expected: TABLE_FORMAT_VERSION,
150 found: table_meta.format_version(),
151 }
152 .fail();
153 }
154
155 let index = match &table_meta.kind {
158 TableKind::TimeSeries(spec) => spec.clone(),
159 other => {
160 return NotTimeSeriesSnafu {
161 kind: other.clone(),
162 }
163 .fail();
164 }
165 };
166 index.validate().context(IndexSpecSnafu)?;
167 if let Some(schema) = &table_meta.logical_schema {
168 ensure_index_spec_matches_schema(schema, &index).context(SchemaCompatibilitySnafu)?;
169 }
170
171 let log = TransactionLogStore::new(location.clone());
172
173 let current_version = log
176 .load_current_version()
177 .await
178 .context(TransactionLogSnafu)?;
179
180 if current_version != 0 {
181 return AlreadyExistsSnafu { current_version }.fail();
182 }
183
184 let actions = vec![LogAction::UpdateTableMeta(table_meta.clone())];
186
187 let new_version = log
188 .commit_with_expected_version(0, actions)
189 .await
190 .context(TransactionLogSnafu)?;
191
192 debug_assert_eq!(new_version, 1);
193
194 let state = log
197 .rebuild_table_state()
198 .await
199 .context(TransactionLogSnafu)?;
200 Ok(Self { log, state, index })
201 }
202
203 pub async fn current_version(&self) -> Result<u64, TableError> {
205 self.log
206 .load_current_version()
207 .await
208 .context(TransactionLogSnafu)
209 }
210
211 pub async fn load_latest_state(&self) -> Result<TableState, TableError> {
213 self.log
214 .rebuild_table_state()
215 .await
216 .context(TransactionLogSnafu)
217 }
218
219 pub async fn refresh(&mut self) -> Result<bool, TableError> {
221 let current = self
222 .log
223 .load_current_version()
224 .await
225 .context(TransactionLogSnafu)?;
226
227 if current == self.state.version {
228 return Ok(false);
229 }
230
231 let state = self
232 .log
233 .rebuild_table_state()
234 .await
235 .context(TransactionLogSnafu)?;
236
237 let index = match &state.table_meta.kind {
238 TableKind::TimeSeries(spec) => spec.clone(),
239 other => {
240 return NotTimeSeriesSnafu {
241 kind: other.clone(),
242 }
243 .fail();
244 }
245 };
246
247 self.state = state;
248 self.index = index;
249 Ok(true)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 use crate::storage::{StorageLocation, layout};
258 use crate::table::test_util::*;
259 use crate::transaction_log::{CommitError, IndexKind, TimeBucket, TransactionLogStore};
260
261 use tempfile::TempDir;
262
263 #[tokio::test]
264 async fn create_initializes_log_and_state() -> TestResult {
265 let tmp = TempDir::new()?;
266 let location = TableLocation::local(tmp.path());
267
268 let meta = make_basic_table_meta();
269 let table = TimeSeriesTable::create(location.clone(), meta).await?;
270
271 assert_eq!(table.state().version, 1);
273 assert_eq!(TABLE_FORMAT_VERSION, 6);
274 assert_eq!(
275 table.state().table_meta.format_version(),
276 TABLE_FORMAT_VERSION
277 );
278 assert!(table.state().segments.is_empty());
279
280 let root = match table.location().storage() {
282 StorageLocation::Local(p) => p.clone(),
283 };
284
285 let log_dir = root.join(layout::log_rel_dir());
286 assert!(log_dir.is_dir());
287
288 let current_path = root.join(layout::current_rel_path());
289 let current_contents = tokio::fs::read_to_string(¤t_path).await?;
290 assert_eq!(current_contents.trim(), "1");
291
292 Ok(())
293 }
294
295 #[tokio::test]
296 async fn create_rejects_unsupported_format_without_writing_log() -> TestResult {
297 let tmp = TempDir::new()?;
298 let location = TableLocation::local(tmp.path());
299
300 for found in [TABLE_FORMAT_VERSION - 1, TABLE_FORMAT_VERSION + 1] {
301 let mut meta = make_basic_table_meta();
302 meta.format_version = found;
303
304 let err = TimeSeriesTable::create(location.clone(), meta)
305 .await
306 .expect_err("unsupported format version should be rejected");
307 assert!(matches!(
308 err,
309 TableError::UnsupportedFormatVersion {
310 expected: TABLE_FORMAT_VERSION,
311 found: actual,
312 } if actual == found
313 ));
314 assert!(!tmp.path().join(layout::log_rel_dir()).exists());
315 }
316
317 Ok(())
318 }
319
320 #[tokio::test]
321 async fn open_round_trip_after_create() -> TestResult {
322 let tmp = TempDir::new()?;
323 let location = TableLocation::local(tmp.path());
324
325 let meta = make_basic_table_meta();
326 let created = TimeSeriesTable::create(location.clone(), meta).await?;
327
328 let reopened = TimeSeriesTable::open(location.clone()).await?;
329
330 assert_eq!(created.state().version, reopened.state().version);
331 assert_eq!(created.index_spec(), reopened.index_spec());
332 Ok(())
333 }
334
335 #[tokio::test]
336 async fn open_rejects_every_non_current_format_with_typed_error() -> TestResult {
337 for found in [TABLE_FORMAT_VERSION - 1, TABLE_FORMAT_VERSION + 1] {
338 let tmp = TempDir::new()?;
339 let location = TableLocation::local(tmp.path());
340 let log = TransactionLogStore::new(location.clone());
341 let mut meta = make_basic_table_meta();
342 meta.format_version = found;
343 log.commit_with_expected_version(0, vec![LogAction::UpdateTableMeta(meta)])
344 .await?;
345
346 let error = TimeSeriesTable::open(location)
347 .await
348 .expect_err("non-current table format must fail");
349
350 assert!(matches!(
351 error,
352 TableError::TransactionLog {
353 source: CommitError::UnsupportedFormatVersion {
354 expected: TABLE_FORMAT_VERSION,
355 found: actual,
356 },
357 } if actual == u64::from(found)
358 ));
359 }
360 Ok(())
361 }
362
363 #[tokio::test]
364 async fn open_empty_root_errors() -> TestResult {
365 let tmp = TempDir::new()?;
366 let location = TableLocation::local(tmp.path());
367
368 let result = TimeSeriesTable::open(location).await;
370 assert!(matches!(result, Err(TableError::EmptyTable)));
371 Ok(())
372 }
373
374 #[tokio::test]
375 async fn create_fails_if_table_already_exists() -> TestResult {
376 let tmp = TempDir::new()?;
377 let location = TableLocation::local(tmp.path());
378
379 let meta = make_basic_table_meta();
380 let _first = TimeSeriesTable::create(location.clone(), meta.clone()).await?;
381
382 let result = TimeSeriesTable::create(location.clone(), meta).await;
384 assert!(matches!(result, Err(TableError::AlreadyExists { .. })));
385 Ok(())
386 }
387
388 #[tokio::test]
389 async fn refresh_returns_false_when_no_new_commits() -> TestResult {
390 let tmp = TempDir::new()?;
391 let location = TableLocation::local(tmp.path());
392
393 let meta = make_basic_table_meta();
394 let mut table = TimeSeriesTable::create(location.clone(), meta).await?;
395
396 let refreshed = table.refresh().await?;
397 assert!(!refreshed);
398 assert_eq!(table.state().version, 1);
399 Ok(())
400 }
401
402 #[tokio::test]
403 async fn refresh_updates_state_and_index_on_change() -> TestResult {
404 let tmp = TempDir::new()?;
405 let location = TableLocation::local(tmp.path());
406
407 let meta = make_basic_table_meta();
408 let mut table = TimeSeriesTable::create(location.clone(), meta.clone()).await?;
409
410 let mut updated_meta = meta.clone();
411 if let TableKind::TimeSeries(spec) = &mut updated_meta.kind {
412 spec.kind = IndexKind::Timestamp {
413 bucket: TimeBucket::Minutes(5),
414 timezone: None,
415 };
416 }
417
418 let log = TransactionLogStore::new(location.clone());
419 let new_version = log
420 .commit_with_expected_version(1, vec![LogAction::UpdateTableMeta(updated_meta.clone())])
421 .await?;
422 assert_eq!(new_version, 2);
423
424 let refreshed = table.refresh().await?;
425 assert!(refreshed);
426 assert_eq!(table.state().version, 2);
427
428 match &table.state().table_meta.kind {
429 TableKind::TimeSeries(spec) => assert_eq!(
430 spec.kind,
431 IndexKind::Timestamp {
432 bucket: TimeBucket::Minutes(5),
433 timezone: None
434 }
435 ),
436 other => panic!("expected time series table kind, got {other:?}"),
437 }
438 assert_eq!(
439 table.index_spec().kind,
440 IndexKind::Timestamp {
441 bucket: TimeBucket::Minutes(5),
442 timezone: None
443 }
444 );
445 Ok(())
446 }
447}