1#[cfg(feature = "parallel")]
7use rayon::prelude::*;
8
9use crate::error::TurtleResult;
10use oxirs_core::model::Triple;
11use std::io::{BufRead, BufReader, Read};
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug, Clone)]
16pub struct ParallelConfig {
17 pub num_threads: usize,
19 pub chunk_size: usize,
25 pub lenient: bool,
27}
28
29impl Default for ParallelConfig {
30 fn default() -> Self {
31 Self {
32 num_threads: 0, chunk_size: 10_000,
34 lenient: false,
35 }
36 }
37}
38
39impl ParallelConfig {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn with_num_threads(mut self, num_threads: usize) -> Self {
47 self.num_threads = num_threads;
48 self
49 }
50
51 pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
53 self.chunk_size = chunk_size;
54 self
55 }
56
57 pub fn lenient(mut self, lenient: bool) -> Self {
59 self.lenient = lenient;
60 self
61 }
62}
63
64#[derive(Debug, Default)]
72pub struct ParallelParseOutcome {
73 pub triples: Vec<Triple>,
75 pub errors: Vec<crate::error::TurtleParseError>,
77}
78
79#[cfg(feature = "parallel")]
81pub struct ParallelParser<R: Read> {
82 reader: BufReader<R>,
83 config: ParallelConfig,
84}
85
86#[cfg(feature = "parallel")]
87impl<R: Read + Send + Sync> ParallelParser<R> {
88 pub fn new(reader: R) -> Self {
90 Self::with_config(reader, ParallelConfig::default())
91 }
92
93 pub fn with_config(reader: R, config: ParallelConfig) -> Self {
95 Self {
96 reader: BufReader::new(reader),
97 config,
98 }
99 }
100
101 pub fn parse_all(&mut self) -> TurtleResult<ParallelParseOutcome> {
124 use std::io::Read;
125
126 let mut content = String::new();
128 self.reader
129 .read_to_string(&mut content)
130 .map_err(crate::error::TurtleParseError::io)?;
131
132 let boundaries = crate::statement_boundary::statement_boundaries(&content);
135 let mut prefix_header = String::new();
136 let mut data_statements = String::new();
137 let mut start = 0usize;
138 for &end in &boundaries {
139 let statement = &content[start..end];
140 let trimmed = statement.trim_start();
141 if trimmed.starts_with("@prefix")
142 || trimmed.starts_with("@base")
143 || trimmed.starts_with("PREFIX")
144 || trimmed.starts_with("prefix")
145 || trimmed.starts_with("BASE")
146 || trimmed.starts_with("base")
147 {
148 prefix_header.push_str(statement);
149 } else {
150 data_statements.push_str(statement);
151 }
152 start = end;
153 }
154 if start < content.len() {
159 data_statements.push_str(&content[start..]);
160 }
161
162 let chunks = crate::statement_boundary::split_into_statement_chunks(
165 &data_statements,
166 self.config.chunk_size,
167 );
168
169 let prefix_arc = std::sync::Arc::new(prefix_header);
171 let results: Vec<TurtleResult<Vec<Triple>>> = chunks
172 .par_iter()
173 .map(|chunk| {
174 let chunk_text = format!("{prefix_arc}{chunk}");
175 self.parse_chunk(&chunk_text)
176 })
177 .collect();
178
179 let mut all_triples = Vec::new();
181 let mut errors = Vec::new();
182 for result in results {
183 match result {
184 Ok(triples) => all_triples.extend(triples),
185 Err(e) if self.config.lenient => {
186 errors.push(e);
187 }
188 Err(e) => return Err(e),
189 }
190 }
191
192 Ok(ParallelParseOutcome {
193 triples: all_triples,
194 errors,
195 })
196 }
197
198 fn parse_chunk(&self, chunk: &str) -> TurtleResult<Vec<Triple>> {
200 use crate::turtle::TurtleParser;
201 let parser = TurtleParser::new();
202 parser.parse_document(chunk)
203 }
204}
205
206#[cfg(feature = "parallel")]
208pub struct ParallelStreamingParser<R: Read + Send + Sync> {
209 reader: Arc<Mutex<BufReader<R>>>,
210 config: ParallelConfig,
211}
212
213#[cfg(feature = "parallel")]
214impl<R: Read + Send + Sync + 'static> ParallelStreamingParser<R> {
215 pub fn new(reader: R) -> Self {
217 Self::with_config(reader, ParallelConfig::default())
218 }
219
220 pub fn with_config(reader: R, config: ParallelConfig) -> Self {
222 Self {
223 reader: Arc::new(Mutex::new(BufReader::new(reader))),
224 config,
225 }
226 }
227
228 pub fn process_batches<F>(&mut self, mut processor: F) -> TurtleResult<usize>
232 where
233 F: FnMut(Vec<Triple>) + Send,
234 {
235 let batch_size = self.config.chunk_size;
236 let mut total_triples = 0;
237 let mut batches = Vec::new();
238 let mut prefixes = String::new();
239
240 loop {
242 let mut reader_guard = self.reader.lock().expect("lock should not be poisoned");
243 let mut batch_content = String::new();
244 let mut lines_read = 0;
245
246 while lines_read < batch_size {
247 let mut line = String::new();
248 match reader_guard.read_line(&mut line) {
249 Ok(0) => break, Ok(_) => {
251 let trimmed = line.trim();
253 if (trimmed.starts_with("@prefix")
254 || trimmed.starts_with("@base")
255 || trimmed.starts_with("PREFIX")
256 || trimmed.starts_with("BASE"))
257 && !prefixes.contains(trimmed)
258 {
259 prefixes.push_str(&line);
260 }
261 batch_content.push_str(&line);
262 lines_read += 1;
263 }
264 Err(e) => return Err(crate::error::TurtleParseError::io(e)),
265 }
266 }
267
268 if batch_content.is_empty() {
269 break;
270 }
271
272 batches.push(batch_content);
273 }
274
275 let prefix_arc = Arc::new(prefixes);
277 let results: Vec<TurtleResult<Vec<Triple>>> = batches
278 .par_iter()
279 .map(|batch| {
280 use crate::turtle::TurtleParser;
281 let parser = TurtleParser::new();
282 let doc_with_prefixes = format!("{}{}", prefix_arc, batch);
283 parser.parse_document(&doc_with_prefixes)
284 })
285 .collect();
286
287 for result in results {
289 match result {
290 Ok(triples) => {
291 total_triples += triples.len();
292 processor(triples);
293 }
294 Err(e) if self.config.lenient => {
295 eprintln!("Warning: Parse error in batch: {}", e);
296 }
297 Err(e) => return Err(e),
298 }
299 }
300
301 Ok(total_triples)
302 }
303}
304
305#[cfg(not(feature = "parallel"))]
306compile_error!("Parallel processing requires the 'parallel' feature to be enabled");
307
308#[cfg(all(test, feature = "parallel"))]
309mod tests {
310 use super::*;
311 use std::io::Cursor;
312
313 #[test]
314 fn test_parallel_parser_basic() {
315 let turtle = r#"
316 @prefix ex: <http://example.org/> .
317 ex:alice ex:name "Alice" .
318 ex:bob ex:name "Bob" .
319 ex:charlie ex:name "Charlie" .
320 "#;
321
322 let mut parser = ParallelParser::new(Cursor::new(turtle));
323 let result = parser.parse_all();
324
325 assert!(result.is_ok());
326 let outcome = result.expect("result should be Ok");
327 assert_eq!(outcome.triples.len(), 3);
328 assert!(outcome.errors.is_empty());
329 }
330
331 #[test]
332 fn test_parallel_parser_large_document() {
333 let mut turtle = String::from("@prefix ex: <http://example.org/> .\n");
334 for i in 0..1000 {
335 turtle.push_str(&format!("ex:subject{} ex:predicate \"object{}\" .\n", i, i));
336 }
337
338 let config = ParallelConfig::default().with_chunk_size(100);
339 let mut parser = ParallelParser::with_config(Cursor::new(turtle), config);
340 let result = parser.parse_all();
341
342 match &result {
343 Ok(outcome) => {
344 assert_eq!(outcome.triples.len(), 1000);
345 assert!(outcome.errors.is_empty());
346 }
347 Err(e) => {
348 panic!("Parse failed: {:?}", e);
349 }
350 }
351 }
352
353 #[test]
354 fn test_parallel_streaming_parser() {
355 let mut turtle = String::from("@prefix ex: <http://example.org/> .\n");
356 for i in 0..500 {
357 turtle.push_str(&format!("ex:subject{} ex:predicate \"object{}\" .\n", i, i));
358 }
359
360 let config = ParallelConfig::default().with_chunk_size(100);
361 let mut parser = ParallelStreamingParser::with_config(Cursor::new(turtle), config);
362
363 let mut total_processed = 0;
364 let result = parser.process_batches(|triples| {
365 total_processed += triples.len();
366 });
367
368 match &result {
369 Ok(count) => {
370 assert_eq!(*count, 500);
371 assert_eq!(total_processed, 500);
372 }
373 Err(e) => {
374 panic!("Parse failed: {:?}", e);
375 }
376 }
377 }
378
379 #[test]
380 fn test_parallel_parser_lenient_mode() {
381 let turtle = r#"
382 @prefix ex: <http://example.org/> .
383 ex:alice ex:name "Alice" .
384 invalid syntax here
385 ex:bob ex:name "Bob" .
386 "#;
387
388 let config = ParallelConfig::default().lenient(true);
389 let mut parser = ParallelParser::with_config(Cursor::new(turtle), config);
390 let result = parser.parse_all();
391
392 assert!(result.is_ok());
395 let outcome = result.expect("result should be Ok");
396 assert!(
397 !outcome.errors.is_empty(),
398 "lenient mode should report the chunk parse error instead of silently discarding it"
399 );
400 }
401
402 #[test]
403 fn test_parallel_parser_does_not_split_multiline_statement() {
404 let turtle = concat!(
410 "@prefix ex: <http://example.org/> .\n",
411 "ex:alice\n",
412 " ex:name \"Alice\" ;\n",
413 " ex:age \"30\" ;\n",
414 " ex:email \"alice@example.org\" .\n",
415 "ex:bob ex:name \"Bob\" .\n",
416 );
417
418 let config = ParallelConfig::default().with_chunk_size(1);
419 let mut parser = ParallelParser::with_config(Cursor::new(turtle), config);
420 let result = parser.parse_all();
421
422 let outcome = result.expect("parsing should succeed");
423 assert!(outcome.errors.is_empty());
424 assert_eq!(outcome.triples.len(), 4);
426 }
427}