Skip to main content

persistant_core/
provider.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! **"Which backends can give me all of this?"** The consumer lists the capabilities it needs
3//! ([`ProviderNeeds`]); [`providers`] returns every [`Provider`] in a table whose recorded matrix rows
4//! say yes to **every** one, and whose declared durability reaches the minimum. [`rank`] orders those
5//! with [`choose`](crate::choose::choose). Nothing here opens a backend: opening is the facade's
6//! `registry` (a provider says what endpoint it needs, [`EndpointSpec`], and nothing more).
7//!
8//! The rules are `docs/DESIGN-KINDS.md` section 2.1's, applied to a field of providers instead of one
9//! named backend:
10//!
11//! - A capability is a boolean. A provider has it only if the [`Matrix`] says [`Lookup::On`] on one of
12//!   its [`Surface`]s and [`Lookup::Off`] on none. No row is off.
13//! - **No closest match.** If no provider satisfies every need, the answer is [`Unmet`]: for each
14//!   provider, each needed id it lacks (with the matrix's own word, [`Lookup`]) and any durability
15//!   shortfall. A partial list is never returned.
16//! - The table is data the caller passes in; this module holds no registry and does no I/O.
17//!
18//! ```
19//! use persistant_core::{CapabilityId, Config, Matrix};
20//! use persistant_core::choose::Durability;
21//! use persistant_core::provider::{providers, EndpointSpec, Provider, ProviderNeeds, Surface};
22//!
23//! let matrix = Matrix::parse("subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n\
24//!     capability\tkv.get\tmem\t1\t\tyes\t\t\t\n").unwrap();
25//! let table = [Provider::new("mem", vec![Surface::new("mem", "1", Config::new())], None, EndpointSpec::None)];
26//! let needs = ProviderNeeds::new(vec![CapabilityId::new("kv.get").unwrap()], Durability::Volatile);
27//! assert_eq!(providers(&table, &needs, &matrix).unwrap().len(), 1);
28//! let needs = ProviderNeeds::new(vec![CapabilityId::new("kv.ttl").unwrap()], Durability::Volatile);
29//! assert!(providers(&table, &needs, &matrix).is_err()); // names `kv.ttl` as unmet on `mem`
30//! ```
31//!
32//! From `providers()` to a working handle (the open half lands with the facade's `registry`):
33//! 1. build a [`ProviderNeeds`] from the capability ids and the lowest durability you accept;
34//! 2. call [`providers`] with the table and the matrix; `Err(Unmet)` names what each backend lacks;
35//! 3. optionally [`rank`] the result by a measured workload;
36//! 4. take the first [`Provider`]; its [`EndpointSpec`] says whether it needs a path, a URL or nothing;
37//! 5. pass it and your endpoint to the registry's `open` for a handle.
38
39use crate::choose::{Candidate, Declared, Durability, Eligibility, Need, NoneEligible};
40use crate::{CapabilityId, Config, Lookup, Matrix};
41use std::fmt;
42
43/// What a provider needs to be told to open: the endpoint information a configuration cannot guess.
44#[derive(Debug, Clone, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum EndpointSpec {
47    /// Nothing (in-process memory).
48    None,
49    /// A directory or file on the local filesystem.
50    Path,
51    /// A connection URL whose scheme is one of these (`postgres`, `mysql`, `sqlite`).
52    Url {
53        /// The accepted URL schemes.
54        schemes: Vec<String>,
55    },
56}
57
58/// One place the matrix records facts about a provider: a `(backend, version, config)` triple. A
59/// provider that is a kind built on another (key-value on PostgreSQL) has one surface per layer,
60/// because the matrix files name them separately (`kv-on-relational:postgres` and `postgres`).
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Surface {
63    /// Backend name as in the matrix.
64    pub backend: String,
65    /// Backend version as in the matrix.
66    pub version: String,
67    /// The configuration facts the rows are matched against.
68    pub config: Config,
69}
70
71impl Surface {
72    /// A surface for `backend` at `version` with `config`.
73    pub fn new(backend: impl Into<String>, version: impl Into<String>, config: Config) -> Self {
74        Surface {
75            backend: backend.into(),
76            version: version.into(),
77            config,
78        }
79    }
80}
81
82/// A backend a program could be given: what the matrix is asked about, what it declares about
83/// durability, and what endpoint it needs to be opened.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Provider {
86    name: String,
87    surfaces: Vec<Surface>,
88    declared: Option<Declared>,
89    endpoint: EndpointSpec,
90}
91
92impl Provider {
93    /// A provider called `name` (unique within its table). `surfaces` must not be empty: the first
94    /// is the **primary** one, the layer a program talks to, which [`rank`] matches speed rows on.
95    ///
96    /// # Panics
97    /// If `surfaces` is empty: a provider with nothing to look up could satisfy anything.
98    pub fn new(
99        name: impl Into<String>,
100        surfaces: Vec<Surface>,
101        declared: Option<Declared>,
102        endpoint: EndpointSpec,
103    ) -> Self {
104        assert!(
105            !surfaces.is_empty(),
106            "a Provider needs at least one Surface"
107        );
108        Provider {
109            name: name.into(),
110            surfaces,
111            declared,
112            endpoint,
113        }
114    }
115
116    /// The provider's name.
117    pub fn name(&self) -> &str {
118        &self.name
119    }
120
121    /// Where the matrix is asked, primary first.
122    pub fn surfaces(&self) -> &[Surface] {
123        &self.surfaces
124    }
125
126    /// The primary surface.
127    pub fn primary(&self) -> &Surface {
128        &self.surfaces[0]
129    }
130
131    /// Declared durability, if the provider's table has one. `None` is unknown, not volatile.
132    pub fn declared(&self) -> Option<Declared> {
133        self.declared
134    }
135
136    /// What it needs to open.
137    pub fn endpoint(&self) -> &EndpointSpec {
138        &self.endpoint
139    }
140
141    fn lookup(&self, matrix: &Matrix, id: &CapabilityId) -> Lookup {
142        let mut on = false;
143        let mut off = None;
144        for s in &self.surfaces {
145            match matrix.lookup(id, &s.backend, &s.version, &s.config) {
146                Lookup::On => on = true,
147                Lookup::Off(r) => {
148                    off.get_or_insert(r);
149                }
150                _ => {}
151            }
152        }
153        match (on, off) {
154            (_, Some(r)) => Lookup::Off(r),
155            (true, None) => Lookup::On,
156            (false, None) => Lookup::NoRow,
157        }
158    }
159}
160
161/// What the program needs: capability ids that must all be on, and the lowest durability class.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct ProviderNeeds {
164    ids: Vec<CapabilityId>,
165    min_durability: Durability,
166    preferred: Vec<CapabilityId>,
167}
168
169impl ProviderNeeds {
170    /// Every id in `ids` must be on, and durability must reach `min_durability`.
171    pub fn new(ids: Vec<CapabilityId>, min_durability: Durability) -> Self {
172        ProviderNeeds {
173            ids,
174            min_durability,
175            preferred: Vec::new(),
176        }
177    }
178
179    /// What matters most, **most important first**. A preferred id never excludes a provider; it
180    /// only orders the ones that qualify ([`sorted`]).
181    #[must_use]
182    pub fn prefer(mut self, preferred: Vec<CapabilityId>) -> Self {
183        self.preferred = preferred;
184        self
185    }
186
187    /// The preferred ids, most important first.
188    pub fn preferred(&self) -> &[CapabilityId] {
189        &self.preferred
190    }
191
192    /// The needed ids.
193    pub fn ids(&self) -> &[CapabilityId] {
194        &self.ids
195    }
196
197    /// The lowest acceptable durability.
198    pub fn min_durability(&self) -> Durability {
199        self.min_durability
200    }
201}
202
203/// Why a provider's durability fell short.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum DurabilityShortfall {
206    /// It declares a lower class.
207    Below(Durability),
208    /// It declares nothing, and more than `Volatile` was asked for.
209    Unknown,
210}
211
212/// What one provider lacks.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct ProviderUnmet {
215    /// The provider's name.
216    pub provider: String,
217    /// Each needed id it does not have, with what the matrix said ([`Lookup::Off`] or
218    /// [`Lookup::NoRow`]). Never contains [`Lookup::On`].
219    pub missing: Vec<(CapabilityId, Lookup)>,
220    /// Set if its durability is below the minimum.
221    pub durability: Option<DurabilityShortfall>,
222}
223
224/// No provider satisfies the whole need. Lists **every** provider considered with what it lacks.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct Unmet {
227    /// One entry per provider in the table; an empty table gives an empty list.
228    pub providers: Vec<ProviderUnmet>,
229}
230
231impl fmt::Display for Unmet {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        if self.providers.is_empty() {
234            return f.write_str("no provider is registered");
235        }
236        f.write_str("no provider satisfies the need:")?;
237        for p in &self.providers {
238            write!(f, " [{}:", p.provider)?;
239            for (id, l) in &p.missing {
240                match l {
241                    Lookup::Off(r) => write!(f, " {} off({r})", id.as_str())?,
242                    _ => write!(f, " {} no-row", id.as_str())?,
243                }
244            }
245            match p.durability {
246                Some(DurabilityShortfall::Below(d)) => write!(f, " durability {}", d.as_str())?,
247                Some(DurabilityShortfall::Unknown) => f.write_str(" durability unknown")?,
248                None => {}
249            }
250            f.write_str("]")?;
251        }
252        Ok(())
253    }
254}
255
256impl std::error::Error for Unmet {}
257
258/// Every provider in `table` that has all of `needs`, in table order.
259///
260/// A provider qualifies if each needed id is [`Lookup::On`] on at least one of its surfaces and
261/// [`Lookup::Off`] on none, and its declared durability is at least `needs.min_durability()`
262/// (undeclared fails unless `Volatile` is enough). If none qualifies the error names, for every
263/// provider, what it lacks. No closest match is offered.
264pub fn providers(
265    table: &[Provider],
266    needs: &ProviderNeeds,
267    matrix: &Matrix,
268) -> Result<Vec<Provider>, Unmet> {
269    let mut ok = Vec::new();
270    let mut unmet = Vec::new();
271    for p in table {
272        let missing: Vec<_> = needs
273            .ids
274            .iter()
275            .filter_map(|id| match p.lookup(matrix, id) {
276                Lookup::On => None,
277                other => Some((id.clone(), other)),
278            })
279            .collect();
280        let durability = match p.declared {
281            Some(d) if d.class < needs.min_durability => Some(DurabilityShortfall::Below(d.class)),
282            None if needs.min_durability > Durability::Volatile => {
283                Some(DurabilityShortfall::Unknown)
284            }
285            _ => None,
286        };
287        if missing.is_empty() && durability.is_none() {
288            ok.push(p.clone());
289        } else {
290            unmet.push(ProviderUnmet {
291                provider: p.name.clone(),
292                missing,
293                durability,
294            });
295        }
296    }
297    if ok.is_empty() {
298        return Err(Unmet { providers: unmet });
299    }
300    Ok(ok)
301}
302
303/// Order `qualified` (the result of [`providers`]) with [`choose`](crate::choose::choose): fastest
304/// median first for `need.rank_by` in `need.environment`, matched on each provider's primary surface.
305/// Eligibility comes from the recorded rows already checked, so `need.leaves` is not consulted. A
306/// provider with no measurement is listed as unmeasured, not hidden; if none is measured the result is
307/// the error that carries them.
308pub fn rank(
309    qualified: &[Provider],
310    need: &Need,
311    speed: &crate::bench::SpeedTable,
312) -> Result<crate::choose::Choice, NoneEligible> {
313    let candidates: Vec<_> = qualified
314        .iter()
315        .map(|p| Candidate {
316            backend: p.primary().backend.clone(),
317            version: p.primary().version.clone(),
318            config: p.primary().config.clone(),
319            eligibility: Eligibility::Eligible,
320        })
321        .collect();
322    let declared = |backend: &str| {
323        qualified
324            .iter()
325            .find(|p| p.primary().backend == backend)
326            .and_then(|p| p.declared)
327    };
328    crate::choose::choose(&candidates, need, speed, &declared)
329}
330
331/// One provider in a sorted answer, with what it was sorted on.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct Sorted {
334    /// The provider.
335    pub provider: Provider,
336    /// Each preferred id, in the order of importance given, and whether this provider has it.
337    pub preferred: Vec<(CapabilityId, bool)>,
338    /// Median time of the ranking workload in the ranking environment, if measured.
339    pub p50_ns: Option<u64>,
340}
341
342/// Order `qualified` (the result of [`providers`]) for a setup UI: by the preferred ids in order of
343/// importance (a provider having the first preferred id outranks any that lacks it, whatever the
344/// rest), then by measured median time, smaller first, with unmeasured last, then by name. Nothing is
345/// dropped: every provider passed in is in the result, with the evidence it was sorted on. `speed`
346/// is the [`Need`] naming the workload and environment, and the table; without it nothing is measured.
347pub fn sorted(
348    qualified: &[Provider],
349    needs: &ProviderNeeds,
350    matrix: &Matrix,
351    speed: Option<(&Need, &crate::bench::SpeedTable)>,
352) -> Vec<Sorted> {
353    use crate::bench::Outcome;
354    let mut out: Vec<Sorted> = qualified
355        .iter()
356        .map(|p| {
357            let p50_ns = speed.and_then(|(need, table)| {
358                let s = p.primary();
359                table.rows().iter().find_map(|r| match r.outcome {
360                    Outcome::Measured(st)
361                        if r.workload == need.rank_by
362                            && r.backend == s.backend
363                            && r.version == s.version
364                            && r.config == s.config
365                            && r.env.comparable_key() == need.environment =>
366                    {
367                        Some(st.p50_ns)
368                    }
369                    _ => None,
370                })
371            });
372            Sorted {
373                provider: p.clone(),
374                preferred: needs
375                    .preferred
376                    .iter()
377                    .map(|id| (id.clone(), p.lookup(matrix, id) == Lookup::On))
378                    .collect(),
379                p50_ns,
380            }
381        })
382        .collect();
383    out.sort_by(|a, b| {
384        let met = |x: &Sorted| x.preferred.iter().map(|(_, m)| !*m).collect::<Vec<_>>();
385        met(a)
386            .cmp(&met(b))
387            .then_with(|| match (a.p50_ns, b.p50_ns) {
388                (Some(x), Some(y)) => x.cmp(&y),
389                (Some(_), None) => std::cmp::Ordering::Less,
390                (None, Some(_)) => std::cmp::Ordering::Greater,
391                (None, None) => std::cmp::Ordering::Equal,
392            })
393            .then_with(|| a.provider.name.cmp(&b.provider.name))
394    });
395    out
396}
397
398/// [`providers`] then [`sorted`]: the one call a setup UI makes.
399pub fn select(
400    table: &[Provider],
401    needs: &ProviderNeeds,
402    matrix: &Matrix,
403    speed: Option<(&Need, &crate::bench::SpeedTable)>,
404) -> Result<Vec<Sorted>, Unmet> {
405    let q = providers(table, needs, matrix)?;
406    Ok(sorted(&q, needs, matrix, speed))
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::Reason;
413
414    const HEADER: &str =
415        "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n";
416
417    fn row(id: &str, backend: &str, yes: bool) -> String {
418        let (e, r) = if yes { ("yes", "") } else { ("no", "wrong") };
419        format!("capability\t{id}\t{backend}\t1\t\t{e}\t{r}\t\t\n")
420    }
421
422    fn matrix(rows: &[(&str, &str, bool)]) -> Matrix {
423        let mut s = HEADER.to_string();
424        for (i, b, y) in rows {
425            s.push_str(&row(i, b, *y));
426        }
427        Matrix::parse(&s).unwrap()
428    }
429
430    fn id(s: &str) -> CapabilityId {
431        CapabilityId::new(s).unwrap()
432    }
433
434    fn decl(class: Durability) -> Option<Declared> {
435        Some(Declared {
436            class,
437            basis: "test",
438            verified: None,
439        })
440    }
441
442    fn prov(name: &str, backends: &[&str], d: Option<Declared>) -> Provider {
443        let s = backends
444            .iter()
445            .map(|b| Surface::new(*b, "1", Config::new()))
446            .collect();
447        Provider::new(name, s, d, EndpointSpec::None)
448    }
449
450    fn needs(ids: &[&str], d: Durability) -> ProviderNeeds {
451        ProviderNeeds::new(ids.iter().map(|i| id(i)).collect(), d)
452    }
453
454    #[test]
455    fn every_id_must_be_on_and_a_known_bad_flips_the_provider_out() {
456        let table = [
457            prov("a", &["a"], decl(Durability::Machine)),
458            prov("b", &["b"], decl(Durability::Machine)),
459        ];
460        let m = matrix(&[
461            ("kv.get", "a", true),
462            ("kv.ttl", "a", true),
463            ("kv.get", "b", true),
464            ("kv.ttl", "b", true),
465        ]);
466        let n = needs(&["kv.get", "kv.ttl"], Durability::Process);
467        let got = providers(&table, &n, &m).unwrap();
468        assert_eq!(
469            got.iter().map(Provider::name).collect::<Vec<_>>(),
470            ["a", "b"]
471        );
472        // Known-bad: the same need, one id Off on `b`: `b` is out and `a` stays.
473        let m = matrix(&[
474            ("kv.get", "a", true),
475            ("kv.ttl", "a", true),
476            ("kv.get", "b", true),
477            ("kv.ttl", "b", false),
478        ]);
479        let got = providers(&table, &n, &m).unwrap();
480        assert_eq!(got.iter().map(Provider::name).collect::<Vec<_>>(), ["a"]);
481    }
482
483    #[test]
484    fn nothing_satisfying_names_each_providers_unmet_id_and_offers_no_partial() {
485        let table = [
486            prov("a", &["a"], decl(Durability::Machine)),
487            prov("b", &["b"], decl(Durability::Machine)),
488        ];
489        // a has kv.get only; b has kv.ttl only (wrong for get). Neither has both.
490        let m = matrix(&[
491            ("kv.get", "a", true),
492            ("kv.ttl", "b", true),
493            ("kv.get", "b", false),
494        ]);
495        let e = providers(
496            &table,
497            &needs(&["kv.get", "kv.ttl"], Durability::Volatile),
498            &m,
499        )
500        .unwrap_err();
501        assert_eq!(e.providers.len(), 2);
502        assert_eq!(e.providers[0].provider, "a");
503        assert_eq!(e.providers[0].missing, vec![(id("kv.ttl"), Lookup::NoRow)]);
504        assert_eq!(e.providers[1].provider, "b");
505        assert_eq!(
506            e.providers[1].missing,
507            vec![(id("kv.get"), Lookup::Off(Reason::Wrong))]
508        );
509        let text = e.to_string();
510        assert!(text.contains("[a: kv.ttl no-row]"), "{text}");
511        assert!(text.contains("[b: kv.get off(wrong)]"), "{text}");
512    }
513
514    #[test]
515    fn durability_is_a_boolean_gate_and_unknown_fails_above_volatile() {
516        let table = [
517            prov("vol", &["v"], decl(Durability::Volatile)),
518            prov("unk", &["u"], None),
519        ];
520        let m = matrix(&[("kv.get", "v", true), ("kv.get", "u", true)]);
521        let e = providers(&table, &needs(&["kv.get"], Durability::Process), &m).unwrap_err();
522        assert_eq!(
523            e.providers[0].durability,
524            Some(DurabilityShortfall::Below(Durability::Volatile))
525        );
526        assert_eq!(
527            e.providers[1].durability,
528            Some(DurabilityShortfall::Unknown)
529        );
530        assert!(e.providers.iter().all(|p| p.missing.is_empty()));
531        // Positive control: Volatile is enough, so both qualify.
532        let got = providers(&table, &needs(&["kv.get"], Durability::Volatile), &m).unwrap();
533        assert_eq!(got.len(), 2);
534    }
535
536    #[test]
537    fn surfaces_combine_across_layers_and_off_on_any_layer_wins() {
538        let table = [prov("pg", &["kv-on-pg", "pg"], decl(Durability::Machine))];
539        let m = matrix(&[("kv.get", "kv-on-pg", true), ("rel.tx", "pg", true)]);
540        let n = needs(&["kv.get", "rel.tx"], Durability::Machine);
541        assert_eq!(providers(&table, &n, &m).unwrap().len(), 1);
542        let m = matrix(&[
543            ("kv.get", "kv-on-pg", true),
544            ("rel.tx", "pg", true),
545            ("rel.tx", "kv-on-pg", false),
546        ]);
547        let e = providers(&table, &n, &m).unwrap_err();
548        assert_eq!(
549            e.providers[0].missing,
550            vec![(id("rel.tx"), Lookup::Off(Reason::Wrong))]
551        );
552    }
553
554    #[test]
555    fn an_empty_table_is_an_error_and_an_empty_need_takes_everything() {
556        let m = matrix(&[]);
557        let e = providers(&[], &needs(&[], Durability::Volatile), &m).unwrap_err();
558        assert!(e.providers.is_empty());
559        assert_eq!(e.to_string(), "no provider is registered");
560        let table = [prov("a", &["a"], decl(Durability::Volatile))];
561        assert_eq!(
562            providers(&table, &needs(&[], Durability::Volatile), &m)
563                .unwrap()
564                .len(),
565            1
566        );
567    }
568
569    #[test]
570    fn rank_orders_by_the_speed_table_through_choose() {
571        use crate::bench::{Environment, Outcome, SpeedRow, SpeedTable, Stats};
572        let env = Environment {
573            machine: "m".into(),
574            os: "linux".into(),
575            arch: "x86_64".into(),
576            date: "2026-10-21".into(),
577            digest: None,
578            rerun: "cmd".into(),
579        };
580        let stats = |p50| Stats {
581            n: 5,
582            p50_ns: p50,
583            p95_ns: p50,
584            p99_ns: p50,
585            max_ns: p50,
586            ops_per_sec: 1,
587        };
588        let mk = |b: &str, p50| SpeedRow {
589            workload: "w".into(),
590            backend: b.into(),
591            version: "1".into(),
592            config: Config::new(),
593            env: env.clone(),
594            outcome: Outcome::Measured(stats(p50)),
595        };
596        let speed = SpeedTable::from_rows(vec![mk("slow", 90), mk("fast", 10)]);
597        let q = [
598            prov("slow", &["slow"], decl(Durability::Volatile)),
599            prov("fast", &["fast"], decl(Durability::Volatile)),
600            prov("unmeasured", &["nope"], decl(Durability::Volatile)),
601        ];
602        let need = Need {
603            leaves: vec![],
604            min_durability: Durability::Volatile,
605            rank_by: "w".into(),
606            environment: env.comparable_key(),
607        };
608        let c = rank(&q, &need, &speed).unwrap();
609        let names: Vec<_> = c.ranked.iter().map(|e| e.backend.as_str()).collect();
610        assert_eq!(names, ["fast", "slow"]);
611        assert_eq!(c.unmeasured.len(), 1);
612        assert_eq!(c.unmeasured[0].backend, "nope");
613    }
614
615    #[test]
616    fn first_preferred_outweighs_all_later_ones_then_speed_then_name() {
617        use crate::bench::{Environment, Outcome, SpeedRow, SpeedTable, Stats};
618        let d = decl(Durability::Volatile);
619        let table = [
620            prov("a", &["a"], d),
621            prov("b", &["b"], d),
622            prov("c", &["c"], d),
623            prov("d", &["d"], d),
624        ];
625        // All have kv.get (required). a: p2 only. b: p1 only. c: p1 and p2. d: neither.
626        let m = matrix(&[
627            ("kv.get", "a", true),
628            ("kv.get", "b", true),
629            ("kv.get", "c", true),
630            ("kv.get", "d", true),
631            ("kv.ttl", "a", true),
632            ("kv.ttl", "b", true),
633            ("kv.ttl", "c", true),
634            ("kv.cas", "c", true),
635            ("kv.cas", "a", true),
636        ]);
637        // Importance: kv.ttl first, kv.cas second.
638        let n = needs(&["kv.get"], Durability::Volatile).prefer(vec![id("kv.ttl"), id("kv.cas")]);
639        let got = select(&table, &n, &m, None).unwrap();
640        let names: Vec<_> = got.iter().map(|s| s.provider.name()).collect();
641        // a and c have both preferred ids and tie, so name order: a, c; then b (first only); then d.
642        assert_eq!(names, ["a", "c", "b", "d"]);
643        // Evidence is carried.
644        assert_eq!(
645            got[2].preferred,
646            vec![(id("kv.ttl"), true), (id("kv.cas"), false)]
647        );
648
649        // Speed breaks a tie, unmeasured last, and a preferred id never excludes.
650        let env = Environment {
651            machine: "m".into(),
652            os: "linux".into(),
653            arch: "x86_64".into(),
654            date: "2026-10-21".into(),
655            digest: None,
656            rerun: "cmd".into(),
657        };
658        let st = |p50| Stats {
659            n: 5,
660            p50_ns: p50,
661            p95_ns: p50,
662            p99_ns: p50,
663            max_ns: p50,
664            ops_per_sec: 1,
665        };
666        let mk = |b: &str, p50| SpeedRow {
667            workload: "w".into(),
668            backend: b.into(),
669            version: "1".into(),
670            config: Config::new(),
671            env: env.clone(),
672            outcome: Outcome::Measured(st(p50)),
673        };
674        let speed = SpeedTable::from_rows(vec![mk("a", 90), mk("c", 10)]);
675        let need = Need {
676            leaves: vec![],
677            min_durability: Durability::Volatile,
678            rank_by: "w".into(),
679            environment: env.comparable_key(),
680        };
681        let got = select(&table, &n, &m, Some((&need, &speed))).unwrap();
682        let names: Vec<_> = got.iter().map(|s| s.provider.name()).collect();
683        assert_eq!(names, ["c", "a", "b", "d"]);
684        assert_eq!(got[0].p50_ns, Some(10));
685        assert_eq!(got[3].p50_ns, None);
686    }
687
688    #[test]
689    fn select_is_an_error_when_the_required_set_is_unmet_whatever_is_preferred() {
690        let table = [prov("a", &["a"], decl(Durability::Volatile))];
691        let m = matrix(&[("kv.ttl", "a", true)]);
692        let n = needs(&["kv.get"], Durability::Volatile).prefer(vec![id("kv.ttl")]);
693        assert!(select(&table, &n, &m, None).is_err());
694    }
695}