1use std::path::Path;
2
3use crate::data::datatable::DataValue;
4use crate::sql::functions::{ArgCount, FunctionCategory, FunctionSignature, SqlFunction};
5use anyhow::{anyhow, Result};
6
7fn as_str(v: &DataValue) -> Option<String> {
8 match v {
9 DataValue::String(s) => Some(s.clone()),
10 DataValue::InternedString(s) => Some(s.as_ref().to_string()),
11 DataValue::Null => None,
12 _ => Some(v.to_string()),
13 }
14}
15
16pub struct BasenameFunction;
18
19impl SqlFunction for BasenameFunction {
20 fn signature(&self) -> FunctionSignature {
21 FunctionSignature {
22 name: "BASENAME",
23 category: FunctionCategory::String,
24 arg_count: ArgCount::Fixed(1),
25 description: "Return the last component of a path (file name with extension)",
26 returns: "STRING (or NULL if path has no file name)",
27 examples: vec![
28 "SELECT BASENAME('/home/me/src/main.rs') -- 'main.rs'",
29 "SELECT BASENAME(path) FROM files",
30 ],
31 }
32 }
33
34 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
35 self.validate_args(args)?;
36 let Some(s) = as_str(&args[0]) else {
37 return Ok(DataValue::Null);
38 };
39 match Path::new(&s).file_name() {
40 Some(name) => Ok(DataValue::String(name.to_string_lossy().into_owned())),
41 None => Ok(DataValue::Null),
42 }
43 }
44}
45
46pub struct DirnameFunction;
48
49impl SqlFunction for DirnameFunction {
50 fn signature(&self) -> FunctionSignature {
51 FunctionSignature {
52 name: "DIRNAME",
53 category: FunctionCategory::String,
54 arg_count: ArgCount::Fixed(1),
55 description: "Return the parent directory of a path",
56 returns: "STRING (or NULL if path has no parent)",
57 examples: vec![
58 "SELECT DIRNAME('/home/me/src/main.rs') -- '/home/me/src'",
59 "SELECT DIRNAME(path) FROM files",
60 ],
61 }
62 }
63
64 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
65 self.validate_args(args)?;
66 let Some(s) = as_str(&args[0]) else {
67 return Ok(DataValue::Null);
68 };
69 match Path::new(&s).parent() {
70 Some(parent) => {
71 let out = parent.to_string_lossy();
72 if out.is_empty() {
73 Ok(DataValue::Null)
74 } else {
75 Ok(DataValue::String(out.into_owned()))
76 }
77 }
78 None => Ok(DataValue::Null),
79 }
80 }
81}
82
83pub struct ExtensionFunction;
85
86impl SqlFunction for ExtensionFunction {
87 fn signature(&self) -> FunctionSignature {
88 FunctionSignature {
89 name: "EXTENSION",
90 category: FunctionCategory::String,
91 arg_count: ArgCount::Fixed(1),
92 description: "Return the file extension (without leading dot), or NULL if none",
93 returns: "STRING or NULL",
94 examples: vec![
95 "SELECT EXTENSION('/home/me/src/main.rs') -- 'rs'",
96 "SELECT EXTENSION('README') -- NULL",
97 "SELECT EXTENSION(path) FROM files",
98 ],
99 }
100 }
101
102 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
103 self.validate_args(args)?;
104 let Some(s) = as_str(&args[0]) else {
105 return Ok(DataValue::Null);
106 };
107 match Path::new(&s).extension() {
108 Some(ext) => Ok(DataValue::String(ext.to_string_lossy().into_owned())),
109 None => Ok(DataValue::Null),
110 }
111 }
112}
113
114pub struct StemFunction;
116
117impl SqlFunction for StemFunction {
118 fn signature(&self) -> FunctionSignature {
119 FunctionSignature {
120 name: "STEM",
121 category: FunctionCategory::String,
122 arg_count: ArgCount::Fixed(1),
123 description: "Return the file name without its extension",
124 returns: "STRING or NULL",
125 examples: vec![
126 "SELECT STEM('/home/me/src/main.rs') -- 'main'",
127 "SELECT STEM('archive.tar.gz') -- 'archive.tar'",
128 "SELECT STEM(path) FROM files",
129 ],
130 }
131 }
132
133 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
134 self.validate_args(args)?;
135 let Some(s) = as_str(&args[0]) else {
136 return Ok(DataValue::Null);
137 };
138 match Path::new(&s).file_stem() {
139 Some(stem) => Ok(DataValue::String(stem.to_string_lossy().into_owned())),
140 None => Ok(DataValue::Null),
141 }
142 }
143}
144
145pub struct PathDepthFunction;
147
148impl SqlFunction for PathDepthFunction {
149 fn signature(&self) -> FunctionSignature {
150 FunctionSignature {
151 name: "PATH_DEPTH",
152 category: FunctionCategory::String,
153 arg_count: ArgCount::Fixed(1),
154 description: "Return the number of components in a path",
155 returns: "INTEGER",
156 examples: vec![
157 "SELECT PATH_DEPTH('/home/me/src/main.rs') -- 5",
158 "SELECT PATH_DEPTH('src/main.rs') -- 2",
159 ],
160 }
161 }
162
163 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
164 self.validate_args(args)?;
165 let Some(s) = as_str(&args[0]) else {
166 return Ok(DataValue::Null);
167 };
168 let count = Path::new(&s).components().count() as i64;
169 Ok(DataValue::Integer(count))
170 }
171}
172
173pub struct PathPartFunction;
175
176impl SqlFunction for PathPartFunction {
177 fn signature(&self) -> FunctionSignature {
178 FunctionSignature {
179 name: "PATH_PART",
180 category: FunctionCategory::String,
181 arg_count: ArgCount::Fixed(2),
182 description: "Return the nth component of a path (1-based; negative counts from the end, -1 = last)",
183 returns: "STRING or NULL if index out of range",
184 examples: vec![
185 "SELECT PATH_PART('/home/me/src/main.rs', 1) -- 'home'",
186 "SELECT PATH_PART('/home/me/src/main.rs', -1) -- 'main.rs'",
187 "SELECT PATH_PART('/home/me/src/main.rs', -2) -- 'src'",
188 ],
189 }
190 }
191
192 fn evaluate(&self, args: &[DataValue]) -> Result<DataValue> {
193 self.validate_args(args)?;
194 let Some(s) = as_str(&args[0]) else {
195 return Ok(DataValue::Null);
196 };
197 let n = match &args[1] {
198 DataValue::Integer(n) => *n,
199 DataValue::Float(f) => *f as i64,
200 DataValue::Null => return Ok(DataValue::Null),
201 _ => return Err(anyhow!("PATH_PART index must be an integer")),
202 };
203
204 let parts: Vec<String> = Path::new(&s)
205 .components()
206 .map(|c| c.as_os_str().to_string_lossy().into_owned())
207 .collect();
208
209 if parts.is_empty() || n == 0 {
210 return Ok(DataValue::Null);
211 }
212
213 let idx = if n > 0 {
214 (n - 1) as usize
215 } else {
216 let from_end = (-n) as usize;
217 if from_end > parts.len() {
218 return Ok(DataValue::Null);
219 }
220 parts.len() - from_end
221 };
222
223 match parts.get(idx) {
224 Some(part) => Ok(DataValue::String(part.clone())),
225 None => Ok(DataValue::Null),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn s(v: &str) -> DataValue {
235 DataValue::String(v.to_string())
236 }
237
238 #[test]
239 fn basename_extracts_file_name() {
240 let f = BasenameFunction;
241 assert_eq!(
242 f.evaluate(&[s("/home/me/src/main.rs")]).unwrap(),
243 s("main.rs")
244 );
245 assert_eq!(f.evaluate(&[s("main.rs")]).unwrap(), s("main.rs"));
246 assert_eq!(f.evaluate(&[DataValue::Null]).unwrap(), DataValue::Null);
247 }
248
249 #[test]
250 fn dirname_extracts_parent() {
251 let f = DirnameFunction;
252 assert_eq!(
253 f.evaluate(&[s("/home/me/src/main.rs")]).unwrap(),
254 s("/home/me/src")
255 );
256 assert_eq!(f.evaluate(&[s("main.rs")]).unwrap(), DataValue::Null);
257 }
258
259 #[test]
260 fn extension_returns_ext_or_null() {
261 let f = ExtensionFunction;
262 assert_eq!(f.evaluate(&[s("main.rs")]).unwrap(), s("rs"));
263 assert_eq!(f.evaluate(&[s("archive.tar.gz")]).unwrap(), s("gz"));
264 assert_eq!(f.evaluate(&[s("README")]).unwrap(), DataValue::Null);
265 assert_eq!(f.evaluate(&[s(".gitignore")]).unwrap(), DataValue::Null);
266 }
267
268 #[test]
269 fn stem_strips_last_extension() {
270 let f = StemFunction;
271 assert_eq!(f.evaluate(&[s("main.rs")]).unwrap(), s("main"));
272 assert_eq!(
273 f.evaluate(&[s("archive.tar.gz")]).unwrap(),
274 s("archive.tar")
275 );
276 assert_eq!(f.evaluate(&[s("README")]).unwrap(), s("README"));
277 }
278
279 #[test]
280 fn path_depth_counts_components() {
281 let f = PathDepthFunction;
282 assert_eq!(
284 f.evaluate(&[s("/home/me/src/main.rs")]).unwrap(),
285 DataValue::Integer(5)
286 );
287 assert_eq!(
288 f.evaluate(&[s("src/main.rs")]).unwrap(),
289 DataValue::Integer(2)
290 );
291 }
292
293 #[test]
294 fn path_part_handles_positive_and_negative() {
295 let f = PathPartFunction;
296 let p = s("/home/me/src/main.rs");
297 assert_eq!(
298 f.evaluate(&[p.clone(), DataValue::Integer(-1)]).unwrap(),
299 s("main.rs")
300 );
301 assert_eq!(
302 f.evaluate(&[p.clone(), DataValue::Integer(-2)]).unwrap(),
303 s("src")
304 );
305 assert_eq!(
306 f.evaluate(&[p.clone(), DataValue::Integer(2)]).unwrap(),
307 s("home")
308 );
309 assert_eq!(
310 f.evaluate(&[p.clone(), DataValue::Integer(99)]).unwrap(),
311 DataValue::Null
312 );
313 assert_eq!(
314 f.evaluate(&[p, DataValue::Integer(0)]).unwrap(),
315 DataValue::Null
316 );
317 }
318}