1#[cfg(test)]
24use crate::ast::{PolydatNode, Value};
25
26thread_local! {
38 static DATA_BASE_DIR: std::cell::RefCell<Option<std::path::PathBuf>> =
39 const { std::cell::RefCell::new(None) };
40}
41
42pub fn set_data_base_dir(dir: Option<std::path::PathBuf>) -> Option<std::path::PathBuf> {
48 DATA_BASE_DIR.with(|d| d.replace(dir))
49}
50
51fn resolve_data_path(filename: &str) -> std::borrow::Cow<'_, str> {
57 let p = std::path::Path::new(filename);
58 if p.is_absolute() || p.exists() {
59 return std::borrow::Cow::Borrowed(filename);
60 }
61 DATA_BASE_DIR.with(|d| {
62 if let Some(base) = d.borrow().as_ref() {
63 let candidate = base.join(filename);
64 if candidate.exists() {
65 return std::borrow::Cow::Owned(candidate.to_string_lossy().into_owned());
66 }
67 }
68 std::borrow::Cow::Borrowed(filename)
69 })
70}
71
72fn read_csv_column(filename: &str, column: &str) -> Vec<String> {
78 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
79 .unwrap_or_else(|e| panic!("csv_field: failed to read '{filename}': {e}"));
80 let records =
81 split_csv_records(&content).unwrap_or_else(|e| panic!("csv_field: '{filename}': {e}"));
82 let mut lines = records.into_iter();
83
84 let header_line = lines
85 .next()
86 .unwrap_or_else(|| panic!("csv_field: '{filename}' is empty"));
87 let headers = split_csv_line(header_line)
88 .unwrap_or_else(|e| panic!("csv_field: '{filename}' header: {e}"));
89
90 let col_idx = if let Ok(idx) = column.parse::<usize>() {
91 idx
92 } else {
93 headers
94 .iter()
95 .position(|h| h.trim() == column)
96 .unwrap_or_else(|| {
97 panic!(
98 "csv_field: column '{column}' not found in '{filename}'. Available: {}",
99 headers.join(", ")
100 )
101 })
102 };
103
104 let mut values = Vec::new();
105 for (row, line) in lines.enumerate() {
106 let fields = split_csv_line(line)
107 .unwrap_or_else(|e| panic!("csv_field: '{filename}' row {}: {e}", row + 1));
108 let val = fields.get(col_idx).map_or("", |f| f.trim()).to_string();
109 values.push(val);
110 }
111
112 if values.is_empty() {
113 panic!("csv_field: '{filename}' has no data rows");
114 }
115 values
116}
117
118#[crate::polydat_node(category = Data)]
129fn csv_field(
130 ordinal: u64,
131 filename: crate::derive_support::Const<&str>,
132 column: crate::derive_support::Const<&str>,
133 #[poly_const(read_csv_column, from = (filename, column))] values: &Vec<String>,
134) -> String {
135 let _ = filename;
136 let _ = column;
137 let idx = ordinal as usize % values.len();
138 values[idx].clone()
139}
140
141fn read_csv_data_rows(filename: &str) -> Vec<String> {
146 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
147 .unwrap_or_else(|e| panic!("csv_row: failed to read '{filename}': {e}"));
148 let records =
149 split_csv_records(&content).unwrap_or_else(|e| panic!("csv_row: '{filename}': {e}"));
150 let rows: Vec<String> = records
151 .into_iter()
152 .skip(1) .filter(|l| !l.trim().is_empty())
154 .map(|l| l.to_string())
155 .collect();
156 if rows.is_empty() {
157 panic!("csv_row: '{filename}' has no data rows");
158 }
159 rows
160}
161
162fn read_csv_row_count(filename: &str) -> u64 {
165 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
166 .unwrap_or_else(|e| panic!("csv_row_count: failed to read '{filename}': {e}"));
167 let records =
168 split_csv_records(&content).unwrap_or_else(|e| panic!("csv_row_count: '{filename}': {e}"));
169 records
170 .into_iter()
171 .skip(1)
172 .filter(|l| !l.trim().is_empty())
173 .count() as u64
174}
175
176#[crate::polydat_node(category = Data)]
187fn csv_row(
188 ordinal: u64,
189 filename: crate::derive_support::Const<&str>,
190 #[poly_const(read_csv_data_rows, from = filename)] rows: &Vec<String>,
191) -> String {
192 let idx = ordinal as usize % rows.len();
193 rows[idx].clone()
194}
195
196#[crate::polydat_node(category = Data)]
204fn csv_row_count(
205 filename: crate::derive_support::Const<&str>,
206 #[poly_const(read_csv_row_count, from = filename)] count: &u64,
207) -> u64 {
208 *count
209}
210
211fn read_jsonl_field(filename: &str, path: &str) -> Vec<String> {
218 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
219 .unwrap_or_else(|e| panic!("jsonl_field: failed to read '{filename}': {e}"));
220 let mut values = Vec::new();
221 for (line_num, line) in content.lines().enumerate() {
222 let trimmed = line.trim();
223 if trimmed.is_empty() {
224 continue;
225 }
226 let parsed: serde_json::Value = serde_json::from_str(trimmed)
227 .unwrap_or_else(|e| panic!("jsonl_field: parse error at line {}: {e}", line_num + 1));
228 let val = resolve_json_path(&parsed, path);
229 values.push(val);
230 }
231 if values.is_empty() {
232 panic!("jsonl_field: '{filename}' has no lines");
233 }
234 values
235}
236
237#[crate::polydat_node(category = Data)]
246fn jsonl_field(
247 ordinal: u64,
248 filename: crate::derive_support::Const<&str>,
249 path: crate::derive_support::Const<&str>,
250 #[poly_const(read_jsonl_field, from = (filename, path))] values: &Vec<String>,
251) -> String {
252 let _ = filename;
253 let _ = path;
254 let idx = ordinal as usize % values.len();
255 values[idx].clone()
256}
257
258fn read_jsonl_lines(filename: &str) -> Vec<String> {
261 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
262 .unwrap_or_else(|e| panic!("jsonl_row: failed to read '{filename}': {e}"));
263 let rows: Vec<String> = content
264 .lines()
265 .filter(|l| !l.trim().is_empty())
266 .map(|l| l.to_string())
267 .collect();
268 if rows.is_empty() {
269 panic!("jsonl_row: '{filename}' has no lines");
270 }
271 rows
272}
273
274fn read_jsonl_row_count(filename: &str) -> u64 {
277 let content = std::fs::read_to_string(resolve_data_path(filename).as_ref())
278 .unwrap_or_else(|e| panic!("jsonl_row_count: failed to read '{filename}': {e}"));
279 content.lines().filter(|l| !l.trim().is_empty()).count() as u64
280}
281
282#[crate::polydat_node(category = Data)]
289fn jsonl_row(
290 ordinal: u64,
291 filename: crate::derive_support::Const<&str>,
292 #[poly_const(read_jsonl_lines, from = filename)] rows: &Vec<String>,
293) -> String {
294 let idx = ordinal as usize % rows.len();
295 rows[idx].clone()
296}
297
298#[crate::polydat_node(category = Data)]
303fn jsonl_row_count(
304 filename: crate::derive_support::Const<&str>,
305 #[poly_const(read_jsonl_row_count, from = filename)] count: &u64,
306) -> u64 {
307 *count
308}
309
310fn split_csv_records(content: &str) -> Result<Vec<&str>, String> {
325 let bytes = content.as_bytes();
326 let mut records = Vec::new();
327 let mut start = 0;
328 let mut in_quotes = false;
329 let mut at_field_start = true;
330 let mut line = 1;
331 let mut quote_line = 1;
332 let mut i = 0;
333 while i < bytes.len() {
334 let b = bytes[i];
335 if in_quotes {
336 match b {
337 b'"' if bytes.get(i + 1) == Some(&b'"') => i += 1,
338 b'"' => {
339 in_quotes = false;
340 at_field_start = false;
341 }
342 b'\n' => line += 1,
343 _ => {}
344 }
345 } else {
346 match b {
347 b'"' if at_field_start => {
348 in_quotes = true;
349 quote_line = line;
350 }
351 b',' => at_field_start = true,
352 b'\n' => {
353 let end = if i > start && bytes[i - 1] == b'\r' {
354 i - 1
355 } else {
356 i
357 };
358 records.push(&content[start..end]);
359 start = i + 1;
360 line += 1;
361 at_field_start = true;
362 }
363 _ => at_field_start = false,
364 }
365 }
366 i += 1;
367 }
368 if in_quotes {
369 return Err(format!(
370 "unterminated quoted field starting at line {quote_line}"
371 ));
372 }
373 if start < bytes.len() {
374 records.push(&content[start..]);
375 }
376 Ok(records)
377}
378
379fn split_csv_line(line: &str) -> Result<Vec<std::borrow::Cow<'_, str>>, String> {
383 use std::borrow::Cow;
384 let bytes = line.as_bytes();
385 let mut fields = Vec::new();
386 let mut i = 0;
387 loop {
388 if bytes.get(i) == Some(&b'"') {
389 let mut field = String::new();
390 i += 1;
391 let mut seg = i;
392 loop {
393 match bytes.get(i) {
394 None => return Err("unterminated quoted field".to_string()),
395 Some(b'"') => {
396 field.push_str(&line[seg..i]);
397 if bytes.get(i + 1) == Some(&b'"') {
398 field.push('"');
399 i += 2;
400 seg = i;
401 } else {
402 i += 1;
403 break;
404 }
405 }
406 Some(_) => i += 1,
407 }
408 }
409 fields.push(Cow::Owned(field));
410 match bytes.get(i) {
411 None => return Ok(fields),
412 Some(b',') => i += 1,
413 Some(_) => {
414 return Err(format!(
415 "unexpected text after the closing quote of field {}",
416 fields.len()
417 ));
418 }
419 }
420 } else {
421 let start = i;
422 while i < bytes.len() && bytes[i] != b',' {
423 i += 1;
424 }
425 fields.push(Cow::Borrowed(&line[start..i]));
426 if i == bytes.len() {
427 return Ok(fields);
428 }
429 i += 1;
430 }
431 }
432}
433
434fn resolve_json_path(value: &serde_json::Value, path: &str) -> String {
436 let mut current = value;
437 for key in path.split('.') {
438 match current {
439 serde_json::Value::Object(map) => {
440 current = match map.get(key) {
441 Some(v) => v,
442 None => return String::new(),
443 };
444 }
445 serde_json::Value::Array(arr) => {
446 if let Ok(idx) = key.parse::<usize>() {
447 current = match arr.get(idx) {
448 Some(v) => v,
449 None => return String::new(),
450 };
451 } else {
452 return String::new();
453 }
454 }
455 _ => return String::new(),
456 }
457 }
458 match current {
459 serde_json::Value::String(s) => s.clone(),
460 serde_json::Value::Null => String::new(),
461 other => other.to_string(),
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use std::io::Write;
469
470 fn write_temp_csv(name: &str, content: &str) -> String {
471 let path = std::env::temp_dir().join(name);
472 let mut f = std::fs::File::create(&path).unwrap();
473 f.write_all(content.as_bytes()).unwrap();
474 path.to_str().unwrap().to_string()
475 }
476
477 #[test]
478 fn csv_field_by_name() {
479 let path = write_temp_csv(
480 "test_csv_field.csv",
481 "name,age,city\nalice,30,paris\nbob,25,london\n",
482 );
483 let node = CsvField::new(path, "name".to_string());
484 let mut out = [Value::None];
485 node.eval(&[Value::U64(0)], &mut out);
486 assert_eq!(out[0].to_display_string(), "alice");
487 node.eval(&[Value::U64(1)], &mut out);
488 assert_eq!(out[0].to_display_string(), "bob");
489 node.eval(&[Value::U64(2)], &mut out);
491 assert_eq!(out[0].to_display_string(), "alice");
492 }
493
494 #[test]
495 fn relative_path_resolves_against_data_base_dir() {
496 let dir = std::env::temp_dir().join("nbrs_datafile_base_test");
500 std::fs::create_dir_all(&dir).unwrap();
501 let file = dir.join("base_rows.jsonl");
502 std::fs::write(&file, "{\"v\": 7}\n").unwrap();
503
504 let prev = set_data_base_dir(None);
506 assert_eq!(
507 resolve_data_path("base_rows.jsonl").as_ref(),
508 "base_rows.jsonl"
509 );
510
511 set_data_base_dir(Some(dir.clone()));
514 assert_eq!(
515 resolve_data_path("base_rows.jsonl").as_ref(),
516 file.to_string_lossy()
517 );
518 assert_eq!(
519 read_jsonl_field("base_rows.jsonl", "v"),
520 vec!["7".to_string()]
521 );
522
523 let abs = file.to_string_lossy().into_owned();
525 assert_eq!(resolve_data_path(&abs).as_ref(), abs);
526
527 set_data_base_dir(prev);
528 }
529
530 #[test]
531 fn csv_field_by_index() {
532 let path = write_temp_csv("test_csv_idx.csv", "name,age,city\nalice,30,paris\n");
533 let node = CsvField::new(path, "1".to_string());
534 let mut out = [Value::None];
535 node.eval(&[Value::U64(0)], &mut out);
536 assert_eq!(out[0].to_display_string(), "30");
537 }
538
539 #[test]
540 fn csv_row_returns_full_line() {
541 let path = write_temp_csv("test_csv_row.csv", "a,b,c\n1,2,3\n4,5,6\n");
542 let node = CsvRow::new(path);
545 let mut out = [Value::None];
546 node.eval(&[Value::U64(0)], &mut out);
547 assert_eq!(out[0].to_display_string(), "1,2,3");
548 }
549
550 #[test]
551 fn csv_row_count_excludes_header() {
552 let path = write_temp_csv("test_csv_count.csv", "h1,h2\na,b\nc,d\ne,f\n");
553 let node = CsvRowCount::new(path);
554 let mut out = [Value::None];
555 node.eval(&[], &mut out);
556 assert_eq!(out[0].as_u64(), 3);
557 }
558
559 fn fields(line: &str) -> Vec<String> {
562 split_csv_line(line)
563 .unwrap()
564 .into_iter()
565 .map(|f| f.into_owned())
566 .collect()
567 }
568
569 #[test]
570 fn csv_quoted_field_keeps_embedded_comma() {
571 assert_eq!(
572 fields("\"Shook, Jonathan\",42"),
573 vec!["Shook, Jonathan", "42"]
574 );
575 let path = write_temp_csv(
576 "test_csv_quoted_comma.csv",
577 "name,age\n\"Shook, Jonathan\",42\nbob,25\n",
578 );
579 let node = CsvField::new(path.clone(), "name".to_string());
580 let mut out = [Value::None];
581 node.eval(&[Value::U64(0)], &mut out);
582 assert_eq!(out[0].to_display_string(), "Shook, Jonathan");
583 let node = CsvField::new(path, "age".to_string());
585 node.eval(&[Value::U64(0)], &mut out);
586 assert_eq!(out[0].to_display_string(), "42");
587 }
588
589 #[test]
590 fn csv_quoted_field_collapses_doubled_quote() {
591 assert_eq!(fields("\"say \"\"hi\"\"\",x"), vec!["say \"hi\"", "x"]);
592 assert_eq!(fields("\"\"\"\""), vec!["\""]);
593 let path = write_temp_csv("test_csv_quoted_dq.csv", "q\n\"say \"\"hi\"\"\"\n");
594 let node = CsvField::new(path, "q".to_string());
595 let mut out = [Value::None];
596 node.eval(&[Value::U64(0)], &mut out);
597 assert_eq!(out[0].to_display_string(), "say \"hi\"");
598 }
599
600 #[test]
601 fn csv_quoted_field_keeps_embedded_newline() {
602 let path = write_temp_csv(
605 "test_csv_quoted_nl.csv",
606 "id,note\n1,\"line one\nline two\"\n2,\"crlf\r\nhere\"\r\n3,plain\n",
607 );
608 let node = CsvField::new(path.clone(), "note".to_string());
609 let mut out = [Value::None];
610 node.eval(&[Value::U64(0)], &mut out);
611 assert_eq!(out[0].to_display_string(), "line one\nline two");
612 node.eval(&[Value::U64(1)], &mut out);
613 assert_eq!(out[0].to_display_string(), "crlf\r\nhere");
614 node.eval(&[Value::U64(2)], &mut out);
615 assert_eq!(out[0].to_display_string(), "plain");
616
617 let node = CsvRowCount::new(path.clone());
618 node.eval(&[], &mut out);
619 assert_eq!(out[0].as_u64(), 3);
620
621 let node = CsvRow::new(path);
623 node.eval(&[Value::U64(0)], &mut out);
624 assert_eq!(out[0].to_display_string(), "1,\"line one\nline two\"");
625 node.eval(&[Value::U64(1)], &mut out);
626 assert_eq!(out[0].to_display_string(), "2,\"crlf\r\nhere\"");
627 }
628
629 #[test]
630 fn csv_empty_quoted_field() {
631 assert_eq!(fields("a,\"\",c"), vec!["a", "", "c"]);
632 assert_eq!(fields("\"\""), vec![""]);
633 assert_eq!(fields("\"\","), vec!["", ""]);
634 let path = write_temp_csv("test_csv_quoted_empty.csv", "a,b,c\n1,\"\",3\n");
635 let node = CsvField::new(path, "b".to_string());
636 let mut out = [Value::None];
637 node.eval(&[Value::U64(0)], &mut out);
638 assert_eq!(out[0].to_display_string(), "");
639 }
640
641 #[test]
642 fn csv_mixed_quoted_and_unquoted_fields() {
643 assert_eq!(
646 fields("plain,\"quoted, one\", spaced ,\"\",it\"s,\"last\""),
647 vec!["plain", "quoted, one", " spaced ", "", "it\"s", "last"]
648 );
649 assert_eq!(fields(""), vec![""]);
650 assert_eq!(fields("a,"), vec!["a", ""]);
651 let path = write_temp_csv(
652 "test_csv_quoted_mixed.csv",
653 "a,b,c,d\nplain,\"quoted, one\", spaced ,\"last\"\n",
654 );
655 let mut out = [Value::None];
656 for (col, want) in [
657 ("a", "plain"),
658 ("b", "quoted, one"),
659 ("c", "spaced"),
660 ("d", "last"),
661 ] {
662 let node = CsvField::new(path.clone(), col.to_string());
663 node.eval(&[Value::U64(0)], &mut out);
664 assert_eq!(out[0].to_display_string(), want, "column {col}");
665 }
666 }
667
668 #[test]
669 fn csv_unterminated_quote_is_an_error() {
670 let err = split_csv_records("a,b\n1,\"open\n2,x\n").unwrap_err();
671 assert_eq!(err, "unterminated quoted field starting at line 2");
672 assert_eq!(
673 split_csv_line("\"open").unwrap_err(),
674 "unterminated quoted field"
675 );
676 assert!(
677 split_csv_line("\"a\"b,c")
678 .unwrap_err()
679 .contains("after the closing quote")
680 );
681 }
682
683 #[test]
684 #[should_panic(expected = "csv_field: ")]
685 fn csv_field_unterminated_quote_panics_with_loader_error() {
686 let path = write_temp_csv("test_csv_unterminated.csv", "a,b\n1,\"open\n2,x\n");
687 let _ = CsvField::new(path, "b".to_string());
688 }
689
690 #[test]
691 #[should_panic(expected = "unterminated quoted field starting at line 2")]
692 fn csv_row_count_unterminated_quote_panics_with_loader_error() {
693 let path = write_temp_csv("test_csv_unterminated_count.csv", "a,b\n1,\"open\n2,x\n");
694 let _ = CsvRowCount::new(path);
695 }
696
697 #[test]
698 fn jsonl_field_top_level() {
699 let path = write_temp_csv(
700 "test_jsonl_field.jsonl",
701 "{\"name\":\"alice\",\"age\":30}\n{\"name\":\"bob\",\"age\":25}\n",
702 );
703 let node = JsonlField::new(path, "name".to_string());
704 let mut out = [Value::None];
705 node.eval(&[Value::U64(0)], &mut out);
706 assert_eq!(out[0].to_display_string(), "alice");
707 node.eval(&[Value::U64(1)], &mut out);
708 assert_eq!(out[0].to_display_string(), "bob");
709 }
710
711 #[test]
712 fn jsonl_field_nested_path() {
713 let path = write_temp_csv(
714 "test_jsonl_nested.jsonl",
715 "{\"user\":{\"name\":\"alice\"}}\n{\"user\":{\"name\":\"bob\"}}\n",
716 );
717 let node = JsonlField::new(path, "user.name".to_string());
718 let mut out = [Value::None];
719 node.eval(&[Value::U64(0)], &mut out);
720 assert_eq!(out[0].to_display_string(), "alice");
721 }
722
723 #[test]
724 fn jsonl_row_returns_full_json() {
725 let path = write_temp_csv("test_jsonl_row.jsonl", "{\"a\":1}\n{\"b\":2}\n");
726 let node = JsonlRow::new(path);
727 let mut out = [Value::None];
728 node.eval(&[Value::U64(0)], &mut out);
729 assert!(out[0].to_display_string().contains("\"a\":1"));
730 }
731
732 #[test]
733 fn jsonl_row_count() {
734 let path = write_temp_csv(
735 "test_jsonl_count.jsonl",
736 "{\"a\":1}\n{\"b\":2}\n{\"c\":3}\n",
737 );
738 let node = JsonlRowCount::new(path);
739 let mut out = [Value::None];
740 node.eval(&[], &mut out);
741 assert_eq!(out[0].as_u64(), 3);
742 }
743}