Skip to main content

hawdb_cascades/
cost.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct PlanCost {
17    pub estimated_rows: u64,
18    pub cost: u64,
19}
20
21impl PlanCost {
22    pub fn with_cardinality_floor(self) -> Self {
23        Self {
24            estimated_rows: self.estimated_rows.max(1),
25            ..self
26        }
27    }
28}
29
30/// Cardinality and unweighted logical work, plus the weighted scalar total.
31///
32/// CPU work is the scalar normalization unit. Random/sequential work describes
33/// access patterns, not physical file pages; output work counts produced rows.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct PlanCostBreakdown {
36    pub estimated_rows: u64,
37    pub cost: u64,
38    pub cpu: u64,
39    pub random_io: u64,
40    pub sequential_io: u64,
41    pub output_rows: u64,
42}
43
44// CPU is the scalar normalization unit: from_scalar must not weight a cost twice.
45// These are conservative logical-work weights, not physical page counts or time.
46// Random access costs more than sequential access even when the OS caches bytes.
47const CPU_WEIGHT: u64 = 1;
48const RANDOM_ACCESS_WEIGHT: u64 = 2;
49const SEQUENTIAL_ACCESS_WEIGHT: u64 = 1;
50const OUTPUT_WEIGHT: u64 = 1;
51
52impl PlanCostBreakdown {
53    pub fn with_cardinality_floor(self) -> Self {
54        Self {
55            estimated_rows: self.estimated_rows.max(1),
56            ..self
57        }
58    }
59
60    pub fn new(
61        estimated_rows: u64,
62        cpu: u64,
63        random_io: u64,
64        sequential_io: u64,
65        output_rows: u64,
66    ) -> Self {
67        Self {
68            estimated_rows,
69            cost: cpu
70                .saturating_mul(CPU_WEIGHT)
71                .saturating_add(random_io.saturating_mul(RANDOM_ACCESS_WEIGHT))
72                .saturating_add(sequential_io.saturating_mul(SEQUENTIAL_ACCESS_WEIGHT))
73                .saturating_add(output_rows.saturating_mul(OUTPUT_WEIGHT)),
74            cpu,
75            random_io,
76            sequential_io,
77            output_rows,
78        }
79        .with_cardinality_floor()
80    }
81
82    pub fn from_scalar(cost: PlanCost) -> Self {
83        Self::new(cost.estimated_rows, cost.cost, 0, 0, 0)
84    }
85
86    pub fn with_cpu(self, estimated_rows: u64, cpu: u64, output_rows: u64) -> Self {
87        Self::new(
88            estimated_rows,
89            self.cpu.saturating_add(cpu),
90            self.random_io,
91            self.sequential_io,
92            self.output_rows.saturating_add(output_rows),
93        )
94    }
95
96    pub fn with_random_io(self, estimated_rows: u64, random_io: u64, output_rows: u64) -> Self {
97        Self::new(
98            estimated_rows,
99            self.cpu,
100            self.random_io.saturating_add(random_io),
101            self.sequential_io,
102            self.output_rows.saturating_add(output_rows),
103        )
104    }
105
106    pub fn combine_with_cpu(
107        left: Self,
108        right: Self,
109        estimated_rows: u64,
110        cpu: u64,
111        output_rows: u64,
112    ) -> Self {
113        Self::new(
114            estimated_rows,
115            left.cpu.saturating_add(right.cpu).saturating_add(cpu),
116            left.random_io.saturating_add(right.random_io),
117            left.sequential_io.saturating_add(right.sequential_io),
118            left.output_rows
119                .saturating_add(right.output_rows)
120                .saturating_add(output_rows),
121        )
122    }
123
124    pub fn as_plan_cost(self) -> PlanCost {
125        PlanCost {
126            estimated_rows: self.estimated_rows,
127            cost: self.cost,
128        }
129        .with_cardinality_floor()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::{PlanCost, PlanCostBreakdown};
136
137    #[test]
138    fn cost_breakdown_sums_stable_components() {
139        let cost = PlanCostBreakdown::new(7, 10, 20, 30, 40);
140
141        assert_eq!(cost.estimated_rows, 7);
142        assert_eq!(cost.cost, 120);
143        assert_eq!(
144            cost.as_plan_cost(),
145            PlanCost {
146                estimated_rows: 7,
147                cost: 120,
148            }
149        );
150    }
151
152    #[test]
153    fn cost_breakdown_combines_child_costs_with_parent_cpu() {
154        let left = PlanCostBreakdown::new(2, 3, 5, 7, 11);
155        let right = PlanCostBreakdown::new(13, 17, 19, 23, 29);
156
157        let combined = PlanCostBreakdown::combine_with_cpu(left, right, 26, 31, 37);
158
159        assert_eq!(combined.estimated_rows, 26);
160        assert_eq!(combined.cpu, 51);
161        assert_eq!(combined.random_io, 24);
162        assert_eq!(combined.sequential_io, 30);
163        assert_eq!(combined.output_rows, 77);
164        assert_eq!(combined.cost, 206);
165    }
166
167    #[test]
168    fn cost_breakdown_preserves_a_non_zero_cardinality_floor() {
169        let cost = PlanCostBreakdown::new(0, 0, 0, 0, 0);
170
171        assert_eq!(cost.estimated_rows, 1);
172        assert_eq!(cost.as_plan_cost().estimated_rows, 1);
173    }
174
175    #[test]
176    fn scalar_cost_preserves_a_non_zero_cardinality_floor() {
177        let cost = PlanCost {
178            estimated_rows: 0,
179            cost: 7,
180        }
181        .with_cardinality_floor();
182
183        assert_eq!(cost.estimated_rows, 1);
184        assert_eq!(cost.cost, 7);
185    }
186}