1use std::fmt;
9
10use datafusion::common::extensions_options;
11use datafusion::config::{ConfigExtension, ConfigField, Visit};
12use datafusion::error::{DataFusionError, Result};
13use datafusion::prelude::SessionConfig;
14
15pub const DEFAULT_INDEX_STORAGE_LIMIT: u64 = 4 << 30;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ByteSize(pub u64);
22
23const UNITS: [(&str, u64); 5] = [
24 ("TiB", 1 << 40),
25 ("GiB", 1 << 30),
26 ("MiB", 1 << 20),
27 ("KiB", 1 << 10),
28 ("B", 1),
29];
30
31impl ByteSize {
32 pub fn parse(text: &str) -> Result<Self> {
35 let invalid = || {
36 DataFusionError::Configuration(format!(
37 "'{text}' is not a size; write bytes (1048576) or a number with KiB, MiB, GiB \
38 or TiB (8GiB)"
39 ))
40 };
41 let text = text.trim();
42 let split = text
43 .find(|c: char| !(c.is_ascii_digit() || c == '.'))
44 .unwrap_or(text.len());
45 let (number, unit) = (text[..split].trim(), text[split..].trim());
46 let scale = match unit {
47 "" => 1,
48 unit => {
49 UNITS
50 .iter()
51 .find(|(name, _)| name.eq_ignore_ascii_case(unit))
52 .ok_or_else(invalid)?
53 .1
54 }
55 };
56 if let Ok(n) = number.parse::<u64>() {
57 return n.checked_mul(scale).map(ByteSize).ok_or_else(invalid);
58 }
59 let n: f64 = number.parse().map_err(|_| invalid())?;
60 let bytes = n * scale as f64;
61 if !bytes.is_finite() || bytes < 0.0 || bytes >= u64::MAX as f64 {
62 return Err(invalid());
63 }
64 Ok(ByteSize(bytes as u64))
65 }
66}
67
68impl fmt::Display for ByteSize {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 let (name, scale) = UNITS
72 .iter()
73 .find(|(_, scale)| self.0 != 0 && self.0.is_multiple_of(*scale))
74 .copied()
75 .unwrap_or(("B", 1));
76 if name == "B" {
77 write!(f, "{}", self.0)
78 } else {
79 write!(f, "{}{name}", self.0 / scale)
80 }
81 }
82}
83
84impl ConfigField for ByteSize {
85 fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
86 v.some(key, self, description)
87 }
88
89 fn set(&mut self, _key: &str, value: &str) -> Result<()> {
90 *self = ByteSize::parse(value)?;
91 Ok(())
92 }
93}
94
95extensions_options! {
96 pub struct UrsaOptions {
98 pub index_storage_limit: ByteSize, default = ByteSize(DEFAULT_INDEX_STORAGE_LIMIT)
101 }
102}
103
104impl ConfigExtension for UrsaOptions {
105 const PREFIX: &'static str = "ursa";
106}
107
108pub(crate) fn with_options(mut config: SessionConfig) -> SessionConfig {
110 if config.options().extensions.get::<UrsaOptions>().is_none() {
111 config
112 .options_mut()
113 .extensions
114 .insert(UrsaOptions::default());
115 }
116 config
117}
118
119pub fn index_storage_limit(config: &SessionConfig) -> u64 {
121 config
122 .options()
123 .extensions
124 .get::<UrsaOptions>()
125 .map_or(DEFAULT_INDEX_STORAGE_LIMIT, |o| o.index_storage_limit.0)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn sizes_parse_and_print() {
134 for (text, bytes) in [
135 ("0", 0),
136 ("1048576", 1 << 20),
137 ("512KiB", 512 << 10),
138 ("64 MiB", 64 << 20),
139 ("8GiB", 8 << 30),
140 ("8gib", 8 << 30),
141 ("1.5GiB", 3 << 29),
142 ("2TiB", 2 << 40),
143 ("100B", 100),
144 ] {
145 assert_eq!(ByteSize::parse(text).unwrap(), ByteSize(bytes), "{text}");
146 }
147 for bad in [
148 "",
149 "GiB",
150 "8GB",
151 "-1",
152 "8 gigabytes",
153 "1e3",
154 "99999999999TiB",
155 ] {
156 assert!(ByteSize::parse(bad).is_err(), "{bad}");
157 }
158 assert_eq!(ByteSize(4 << 30).to_string(), "4GiB");
159 assert_eq!(ByteSize(1536 << 20).to_string(), "1536MiB");
160 assert_eq!(ByteSize(1000).to_string(), "1000");
161 assert_eq!(ByteSize(0).to_string(), "0");
162 }
163}