meterstore 0.3.0

Hot/cold tiered store for metering time series — PostgreSQL for the recent window, Apache Iceberg for history.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
//! `meterstore.system.*` — operational state as queryable tables.
//!
//! The handle already exposes the watermark and the invariant check as methods,
//! which serves a program. It does not serve the person holding a pager at 3am,
//! who has a SQL client and a question: is archival keeping up, is anything
//! stranded in the wrong tier, will inserts fail tonight.
//!
//! These are snapshots, computed when queried. They are deliberately cheap —
//! counts and a watermark read — because a diagnostic that is expensive to run
//! is one nobody runs during an incident.

use std::sync::Arc;

use datafusion::datasource::MemTable;
use datafusion::prelude::SessionContext;
use time::OffsetDateTime;

use crate::arrow::array::{
    BooleanArray, Int64Array, RecordBatch, StringArray, TimestampMicrosecondArray,
};
use crate::arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
use crate::config::ValidatedTableConfig;
use crate::error::{Error, Result};
use crate::tiering::store::{ColdStore, HotStore};

/// The schema name system tables are registered under.
pub const SCHEMA: &str = "system";

/// One row of `meterstore.system.tables`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableStatus {
    /// The physical table.
    pub table: String,
    /// Where cold ends and hot begins.
    pub watermark: OffsetDateTime,
    /// How far behind wall clock the watermark sits.
    pub watermark_lag_seconds: i64,
    /// Hot partitions that exist, attached or detached.
    ///
    /// Was `hot_rows`, which was **always `-1`** — counting the hot tier means
    /// scanning it, so the column was declared, documented and never computed. A
    /// diagnostic that reports a sentinel is worse than one that is absent,
    /// because an operator reads it as a number. Partitions answer the questions
    /// row counts were wanted for — is archival keeping up, is anything left
    /// behind — and are a catalog lookup rather than a scan.
    pub hot_partitions: i64,
    /// Partitions that can still hold a row written now or later.
    ///
    /// **Reaching zero stops inserts outright.** The one number on this row that
    /// predicts a hard failure rather than describing one.
    pub partitions_ahead: i64,
    /// Rows below the watermark that are still in PostgreSQL.
    ///
    /// Must be zero. Anything else means a query can return wrong results,
    /// because the tier split assumes a row's interval start decides where it
    /// lives.
    pub invariant_violations: i64,
    /// Whether the tiers partition the data as they should.
    pub healthy: bool,
}

fn status_schema() -> SchemaRef {
    Arc::new(Schema::new(vec![
        Field::new("table", DataType::Utf8, false),
        Field::new(
            "watermark",
            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
            false,
        ),
        Field::new("watermark_lag_seconds", DataType::Int64, false),
        Field::new("hot_partitions", DataType::Int64, false),
        Field::new("partitions_ahead", DataType::Int64, false),
        Field::new("invariant_violations", DataType::Int64, false),
        Field::new("healthy", DataType::Boolean, false),
    ]))
}

/// One row of `meterstore.system.config`.
///
/// Configuration is worth exposing because the settings that matter interact:
/// a partition step that disagrees with the archival step, or a settlement lag
/// shorter than a window, are both accepted individually and wrong together.
/// Seeing them side by side is how that gets noticed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigEntry {
    /// The table the setting belongs to.
    ///
    /// Carried per row rather than implied by the relation, because a session
    /// may host several tables (§15.3) and "which table is this `settlement_lag`
    /// for" is the first question a row raises once there is more than one.
    pub table: String,
    /// Setting name.
    pub setting: String,
    /// Its value, rendered.
    pub value: String,
}

fn config_schema() -> SchemaRef {
    Arc::new(Schema::new(vec![
        Field::new("table", DataType::Utf8, false),
        Field::new("setting", DataType::Utf8, false),
        Field::new("value", DataType::Utf8, false),
    ]))
}

/// Gathers operational state and registers it as queryable tables.
pub struct SystemTables<'a> {
    hot: &'a Arc<dyn HotStore>,
    cold: &'a Arc<dyn ColdStore>,
    config: &'a ValidatedTableConfig,
}

impl<'a> SystemTables<'a> {
    /// Build a collector over one store's tiers.
    pub fn new(
        hot: &'a Arc<dyn HotStore>,
        cold: &'a Arc<dyn ColdStore>,
        config: &'a ValidatedTableConfig,
    ) -> Self {
        Self { hot, cold, config }
    }

    /// Current status of the managed table.
    pub async fn status(&self, now: OffsetDateTime) -> Result<TableStatus> {
        let table = self.config.name();
        let watermark = self.cold.watermark(table).await?;
        let violations = self.hot.invariant_violations(table, watermark).await? as i64;

        crate::observe::metrics()
            .invariant_violations
            .record(violations.max(0) as u64, &crate::observe::table(table));

        // A catalog lookup, not a scan — which is the whole reason this replaced
        // a row count. `None` means the store cannot enumerate its partitions, so
        // the columns report `-1` for *that store* rather than inventing a
        // plausible number; every store this crate ships can answer.
        let partitions = self.hot.partition_starts(table).await?;
        let (hot_partitions, partitions_ahead) = match &partitions {
            Some(starts) => (
                starts.len() as i64,
                crate::tiering::store::partitions_ahead(starts, now, self.config.partition_step())
                    as i64,
            ),
            None => (-1, -1),
        };

        Ok(TableStatus {
            table: table.to_string(),
            watermark: watermark.get(),
            watermark_lag_seconds: (now - watermark.get()).whole_seconds(),
            hot_partitions,
            partitions_ahead,
            invariant_violations: violations,
            // Healthy is the absence of violations **and** a write frontier that
            // still exists. A store whose partitions have run out is not
            // returning wrong answers, but the next insert fails — and an
            // operator reading one health column should not have to know that
            // is tracked somewhere else.
            healthy: violations == 0 && partitions_ahead != 0,
        })
    }

    /// The settings that decide archival behaviour.
    pub fn config_entries(&self) -> Vec<ConfigEntry> {
        let c = self.config;
        let entry = |setting: &str, value: String| ConfigEntry {
            table: c.name().to_string(),
            setting: setting.to_string(),
            value,
        };
        vec![
            entry("table", c.name().to_string()),
            entry("merge_key", c.merge_key().join(", ")),
            entry(
                "partition_step",
                format!("{}s", c.partition_step().whole_seconds()),
            ),
            entry(
                "archival_step",
                format!("{}s", c.archival_step().whole_seconds()),
            ),
            entry(
                "settlement_lag",
                format!("{}s", c.settlement_lag().whole_seconds()),
            ),
            entry(
                "partition_headroom",
                format!("{}s", c.partition_headroom().whole_seconds()),
            ),
            entry(
                "expected_hot_partitions",
                c.expected_hot_partitions().to_string(),
            ),
            entry("target_file_size", c.target_file_size().to_string()),
            entry("scan_chunk_rows", c.scan_chunk_rows().to_string()),
            entry(
                "identity_columns",
                c.identity_columns()
                    .iter()
                    .map(|f| f.name().clone())
                    .collect::<Vec<_>>()
                    .join(", "),
            ),
        ]
    }

    /// The SQL an external engine must apply to read the raw table correctly.
    ///
    /// This is the primary mitigation for the version-resolution trap (§13.7.2),
    /// and it is a correctness matter rather than an ergonomic one: an engine
    /// reading the Iceberg files directly sees **every version** of a corrected
    /// interval, and a naive `SELECT SUM(value)` double-counts each one —
    /// silently, in a number someone will bill from.
    ///
    /// Exposed as a queryable row rather than only as a Rust method, because the
    /// person who needs it is holding a Trino session, not a compiler.
    pub fn resolution_entries(&self, raw_table: &str) -> Vec<ConfigEntry> {
        let table = self.config.name().to_string();
        vec![
            ConfigEntry {
                table: table.clone(),
                setting: "raw_table".to_string(),
                value: raw_table.to_string(),
            },
            ConfigEntry {
                table: table.clone(),
                setting: "resolution_sql".to_string(),
                value: crate::planner::version::resolution_sql_with_key(
                    raw_table,
                    &self.config.merge_key(),
                    &self.config.extra_columns(),
                    None,
                ),
            },
            ConfigEntry {
                table,
                setting: "warning".to_string(),
                value: format!(
                    "{raw_table} holds every version of every reading. Summing it without \
                     the SQL above double-counts every corrected interval."
                ),
            },
        ]
    }

    /// Every committed state of the cold table, newest first.
    pub async fn snapshot_entries(&self) -> Result<Vec<crate::tiering::store::SnapshotInfo>> {
        self.cold.snapshots(self.config.name()).await
    }

    /// Register the system tables into a session under the `system` schema.
    ///
    /// A real schema rather than dotted table names, so `system.tables` parses
    /// as a qualified reference and reads like every other catalog.
    ///
    /// Snapshots taken now. Re-register to refresh — deliberately explicit, so a
    /// query never silently pays for a round trip to both tiers.
    pub async fn register(&self, ctx: &SessionContext, now: OffsetDateTime) -> Result<()> {
        register_all(ctx, std::slice::from_ref(self), now).await
    }
}

/// Register the system tables for **every** table a session hosts.
///
/// One relation per concern, one row set per table, discriminated by the
/// `table` column. A session with several tables (§15.3) otherwise gets either
/// four relations per table — which no operator wants to `UNION` by hand — or
/// one relation whose rows cannot be attributed.
///
/// Re-registering replaces: these are snapshots computed when asked, and a
/// second call is a refresh rather than a duplicate.
pub async fn register_all(
    ctx: &SessionContext,
    tables: &[SystemTables<'_>],
    now: OffsetDateTime,
) -> Result<()> {
    use datafusion::catalog::MemorySchemaProvider;
    use datafusion::catalog::SchemaProvider;

    let catalog = ctx
        .catalog("datafusion")
        .ok_or_else(|| Error::Storage("default catalog missing".into()))?;

    let schema = match catalog.schema(SCHEMA) {
        Some(existing) => existing,
        None => {
            let created: Arc<dyn SchemaProvider> = Arc::new(MemorySchemaProvider::new());
            catalog
                .register_schema(SCHEMA, Arc::clone(&created))
                .map_err(|e| Error::Storage(e.to_string()))?;
            created
        }
    };

    let mut statuses = Vec::with_capacity(tables.len());
    let mut settings = Vec::new();
    let mut resolution = Vec::new();
    let mut snapshots = Vec::new();

    for t in tables {
        statuses.push(t.status(now).await?);
        settings.extend(t.config_entries());

        // The mitigation an external engine needs, reachable from the SQL client
        // the operator already has open.
        let raw = if t.config.name().ends_with("_versions") {
            t.config.name().to_string()
        } else {
            format!("{}_versions", t.config.name())
        };
        resolution.extend(t.resolution_entries(&raw));

        // Snapshots are what a reproducible read pins to, so finding one must
        // not require reading Iceberg metadata by hand.
        snapshots.push(snapshot_batch(
            t.config.name(),
            &t.snapshot_entries().await?,
        )?);
    }

    let register = |name: &str, schema_ref: SchemaRef, batches: Vec<RecordBatch>| {
        schema
            .register_table(
                name.to_string(),
                Arc::new(MemTable::try_new(schema_ref, vec![batches])?),
            )
            .map_err(|e| Error::Storage(e.to_string()))?;
        Ok::<(), Error>(())
    };

    register("tables", status_schema(), vec![status_batch(&statuses)?])?;
    register("config", config_schema(), vec![config_batch(&settings)?])?;
    register(
        "resolution",
        config_schema(),
        vec![config_batch(&resolution)?],
    )?;
    register("snapshots", snapshot_schema(), snapshots)?;

    Ok(())
}

/// The schema of `system.snapshots`.
fn snapshot_schema() -> SchemaRef {
    Arc::new(Schema::new(vec![
        Field::new("table", DataType::Utf8, false),
        Field::new("snapshot_id", DataType::Int64, false),
        Field::new(
            "committed_at",
            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
            false,
        ),
        Field::new(
            "watermark",
            DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
            true,
        ),
        Field::new("rows", DataType::Int64, true),
        Field::new("written_by_meterstore", DataType::Boolean, false),
    ]))
}

/// Encode the snapshot list as a batch.
pub fn snapshot_batch(
    table: &str,
    rows: &[crate::tiering::store::SnapshotInfo],
) -> Result<RecordBatch> {
    let micros = |t: OffsetDateTime| (t.unix_timestamp_nanos() / 1_000) as i64;
    Ok(RecordBatch::try_new(
        snapshot_schema(),
        vec![
            Arc::new(StringArray::from(vec![table; rows.len()])),
            Arc::new(Int64Array::from(
                rows.iter().map(|r| r.snapshot_id).collect::<Vec<_>>(),
            )),
            Arc::new(
                TimestampMicrosecondArray::from(
                    rows.iter()
                        .map(|r| micros(r.committed_at))
                        .collect::<Vec<_>>(),
                )
                .with_timezone("UTC"),
            ),
            Arc::new(
                TimestampMicrosecondArray::from(
                    rows.iter()
                        .map(|r| r.watermark.map(|w| micros(w.get())))
                        .collect::<Vec<_>>(),
                )
                .with_timezone("UTC"),
            ),
            Arc::new(Int64Array::from(
                rows.iter()
                    .map(|r| r.rows.map(|n| i64::try_from(n).unwrap_or(i64::MAX)))
                    .collect::<Vec<_>>(),
            )),
            // A snapshot with no watermark was written by something else — an
            // out-of-band compaction, say. Legitimate, readable, and worth being
            // able to see, because it explains a gap in the watermark column.
            Arc::new(BooleanArray::from(
                rows.iter()
                    .map(|r| r.watermark.is_some())
                    .collect::<Vec<_>>(),
            )),
        ],
    )?)
}

/// Encode statuses as a batch.
pub fn status_batch(rows: &[TableStatus]) -> Result<RecordBatch> {
    let micros = |t: OffsetDateTime| (t.unix_timestamp_nanos() / 1_000) as i64;
    Ok(RecordBatch::try_new(
        status_schema(),
        vec![
            Arc::new(StringArray::from(
                rows.iter().map(|r| r.table.as_str()).collect::<Vec<_>>(),
            )),
            Arc::new(
                TimestampMicrosecondArray::from(
                    rows.iter().map(|r| micros(r.watermark)).collect::<Vec<_>>(),
                )
                .with_timezone("UTC"),
            ),
            Arc::new(Int64Array::from(
                rows.iter()
                    .map(|r| r.watermark_lag_seconds)
                    .collect::<Vec<_>>(),
            )),
            Arc::new(Int64Array::from(
                rows.iter().map(|r| r.hot_partitions).collect::<Vec<_>>(),
            )),
            Arc::new(Int64Array::from(
                rows.iter().map(|r| r.partitions_ahead).collect::<Vec<_>>(),
            )),
            Arc::new(Int64Array::from(
                rows.iter()
                    .map(|r| r.invariant_violations)
                    .collect::<Vec<_>>(),
            )),
            Arc::new(BooleanArray::from(
                rows.iter().map(|r| r.healthy).collect::<Vec<_>>(),
            )),
        ],
    )?)
}

/// Encode configuration entries as a batch.
pub fn config_batch(rows: &[ConfigEntry]) -> Result<RecordBatch> {
    Ok(RecordBatch::try_new(
        config_schema(),
        vec![
            Arc::new(StringArray::from(
                rows.iter().map(|r| r.table.as_str()).collect::<Vec<_>>(),
            )),
            Arc::new(StringArray::from(
                rows.iter().map(|r| r.setting.as_str()).collect::<Vec<_>>(),
            )),
            Arc::new(StringArray::from(
                rows.iter().map(|r| r.value.as_str()).collect::<Vec<_>>(),
            )),
        ],
    )?)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::TableConfig;
    use time::macros::datetime;

    fn status(violations: i64) -> TableStatus {
        TableStatus {
            table: "readings_versions".to_string(),
            watermark: datetime!(2026-07-20 00:00 UTC),
            watermark_lag_seconds: 86_400,
            hot_partitions: 21,
            partitions_ahead: 14,
            invariant_violations: violations,
            healthy: violations == 0,
        }
    }

    #[test]
    fn a_status_batch_matches_its_schema() {
        let batch = status_batch(&[status(0)]).unwrap();
        assert_eq!(batch.schema(), status_schema());
        assert_eq!(batch.num_rows(), 1);
    }

    #[test]
    fn health_covers_both_ways_a_table_stops_working() {
        // Wrong answers now, and no answers shortly: a table with no partition
        // ahead of the frontier rejects the next insert, and an operator reading
        // one health column should not have to know that lives elsewhere.
        assert!(status(0).healthy);
        assert!(!status(1).healthy);
    }

    #[test]
    fn the_write_runway_is_counted_from_partitions_that_exist() {
        use crate::tiering::store::partitions_ahead;
        use time::Duration;

        let starts = [
            datetime!(2026-07-18 00:00 UTC),
            datetime!(2026-07-19 00:00 UTC),
            datetime!(2026-07-20 00:00 UTC),
            datetime!(2026-07-21 00:00 UTC),
        ];
        // Mid-day: the partition holding `now` counts, and so does every later
        // one — those are where the next writes land.
        assert_eq!(
            partitions_ahead(&starts, datetime!(2026-07-20 13:47 UTC), Duration::DAY),
            2
        );
        // Past the last one: the very next insert has nowhere to go. This is the
        // value the old configuration-derived gauge could never produce.
        assert_eq!(
            partitions_ahead(&starts, datetime!(2026-07-22 00:00 UTC), Duration::DAY),
            0
        );
        assert_eq!(
            partitions_ahead(&[], datetime!(2026-07-20 00:00 UTC), Duration::DAY),
            0
        );
    }

    #[test]
    fn config_exposes_the_settings_that_interact() {
        // partition_step vs archival_step, and settlement_lag vs a window, are
        // each valid alone and wrong together. Showing them together is the
        // point of the table.
        let config = TableConfig::new("readings").build().unwrap();
        let hot: Arc<dyn HotStore> = Arc::new(NoStore);
        let cold: Arc<dyn ColdStore> = Arc::new(NoStore);
        let entries = SystemTables::new(&hot, &cold, &config).config_entries();

        let names: Vec<_> = entries.iter().map(|e| e.setting.as_str()).collect();
        for expected in [
            "partition_step",
            "archival_step",
            "settlement_lag",
            "merge_key",
            "expected_hot_partitions",
        ] {
            assert!(names.contains(&expected), "{expected} missing");
        }
    }

    #[test]
    fn config_reports_the_merge_key_including_identity_columns() {
        // If this disagrees with the table's primary key, corrections silently
        // fail to supersede — so it is worth being able to read it back.
        let config = TableConfig::new("readings")
            .identity_column(Field::new("tenant", DataType::Utf8, false))
            .build()
            .unwrap();
        let hot: Arc<dyn HotStore> = Arc::new(NoStore);
        let cold: Arc<dyn ColdStore> = Arc::new(NoStore);
        let entries = SystemTables::new(&hot, &cold, &config).config_entries();

        let key = entries.iter().find(|e| e.setting == "merge_key").unwrap();
        assert!(key.value.contains("tenant"));
    }

    #[test]
    fn a_config_batch_matches_its_schema() {
        let batch = config_batch(&[ConfigEntry {
            table: "readings_versions".to_string(),
            setting: "x".into(),
            value: "y".into(),
        }])
        .unwrap();
        assert_eq!(batch.schema(), config_schema());
    }

    /// A store that is never called — these tests exercise pure config.
    struct NoStore;

    #[async_trait::async_trait]
    impl HotStore for NoStore {
        async fn append_reporting(
            &self,
            _: &str,
            _: &[String],
            _: &[RecordBatch],
        ) -> Result<Vec<crate::session::Displacement>> {
            Ok(Vec::new())
        }

        async fn drop_table(&self, _: &str) -> Result<()> {
            Ok(())
        }

        async fn create_tables(&self, _: &str, _: &[String], _: &[Field]) -> Result<()> {
            unreachable!()
        }
        async fn append(&self, _: &str, _: &[String], _: &[RecordBatch]) -> Result<u64> {
            unreachable!()
        }
        async fn scan_range(
            &self,
            _: &str,
            _: crate::planner::TimeRange,
            _: &crate::tiering::store::ScanSpec,
        ) -> Result<crate::tiering::store::BatchStream> {
            unreachable!()
        }
        async fn ensure_partitions(
            &self,
            _: &str,
            _: OffsetDateTime,
            _: OffsetDateTime,
            _: time::Duration,
        ) -> Result<Vec<crate::tiering::store::PartitionId>> {
            unreachable!()
        }
        async fn partition_exists(&self, _: &crate::tiering::store::PartitionId) -> Result<bool> {
            unreachable!()
        }
        async fn detach_partition(&self, _: &crate::tiering::store::PartitionId) -> Result<()> {
            unreachable!()
        }
        async fn scan_detached(
            &self,
            _: &crate::tiering::store::PartitionId,
            _: &crate::tiering::store::ScanSpec,
        ) -> Result<crate::tiering::store::BatchStream> {
            unreachable!()
        }
        async fn drop_partition(&self, _: &crate::tiering::store::PartitionId) -> Result<()> {
            unreachable!()
        }
        async fn orphaned_partitions(
            &self,
            _: &str,
        ) -> Result<Vec<crate::tiering::store::PartitionId>> {
            unreachable!()
        }
        async fn invariant_violations(
            &self,
            _: &str,
            _: crate::watermark::TieringWatermark,
        ) -> Result<u64> {
            unreachable!()
        }
    }

    #[async_trait::async_trait]
    impl ColdStore for NoStore {
        async fn purge_table(&self, _: &str) -> Result<()> {
            Ok(())
        }

        async fn create_tables(&self, _: &str, _: &[String], _: &[Field]) -> Result<()> {
            unreachable!()
        }
        async fn watermark(&self, _: &str) -> Result<crate::watermark::TieringWatermark> {
            unreachable!()
        }
        async fn append_and_commit(
            &self,
            _: &str,
            _: crate::tiering::store::BatchStream,
            _: crate::tiering::store::WriteHints,
            _: crate::watermark::ArchivalWindow,
        ) -> Result<crate::tiering::store::CommitInfo> {
            unreachable!()
        }
        async fn append_only(
            &self,
            _: &str,
            _: crate::tiering::store::BatchStream,
            _: crate::tiering::store::WriteHints,
        ) -> Result<crate::tiering::store::CommitInfo> {
            unreachable!()
        }
        async fn expire_snapshots(
            &self,
            _: &str,
            _: time::Duration,
            _: usize,
            _: OffsetDateTime,
        ) -> Result<usize> {
            unreachable!()
        }
    }
}