1use super::Dataset;
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use std::{
5 fs::File,
6 io::{self, Read, Seek, SeekFrom},
7 marker::PhantomData,
8 path::Path,
9 sync::{Arc, Mutex},
10 time::UNIX_EPOCH,
11};
12
13fn invalid(message: impl Into<String>) -> io::Error {
14 io::Error::new(io::ErrorKind::InvalidData, message.into())
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub enum JsonlBlankLines {
20 Reject,
22 Skip,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct JsonlIndexOptions {
29 pub source_id: String,
31 pub blank_lines: JsonlBlankLines,
33 pub max_record_bytes: Option<u64>,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39struct FileIdentity {
40 length: u64,
41 modified: Option<(u64, u32)>,
42}
43
44impl FileIdentity {
45 fn read(file: &File) -> io::Result<Self> {
46 let metadata = file.metadata()?;
47 if !metadata.is_file() {
48 return Err(invalid("indexed JSONL requires a regular immutable file"));
49 }
50 let modified = metadata.modified().ok()
51 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
52 .map(|duration| (duration.as_secs(), duration.subsec_nanos()));
53 Ok(Self { length: metadata.len(), modified })
54 }
55
56 fn check(&self, file: &File) -> io::Result<()> {
57 if self != &Self::read(file)? {
58 return Err(invalid("JSONL source length or modification time changed"));
59 }
60 Ok(())
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct JsonlRecordLocation {
67 pub offset: u64,
69 pub length: u64,
71 pub line_number: u64,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
81pub struct JsonlIndexState {
82 version: u32,
83 options: JsonlIndexOptions,
84 identity: FileIdentity,
85 processed_bytes: u64,
86 pending_start: u64,
87 pending_nonblank: bool,
88 next_line: u64,
89 skipped_lines: u64,
90 records: Vec<JsonlRecordLocation>,
91 finished: bool,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct JsonlIndexProgress {
97 pub processed_bytes: u64,
99 pub total_bytes: u64,
101 pub records: usize,
103 pub skipped_lines: u64,
105 pub finished: bool,
107}
108
109impl JsonlIndexState {
110 pub fn options(&self) -> &JsonlIndexOptions { &self.options }
112
113 pub fn records(&self) -> &[JsonlRecordLocation] { &self.records }
115
116 pub fn progress(&self) -> JsonlIndexProgress {
118 JsonlIndexProgress {
119 processed_bytes: self.processed_bytes,
120 total_bytes: self.identity.length,
121 records: self.records.len(),
122 skipped_lines: self.skipped_lines,
123 finished: self.finished,
124 }
125 }
126
127 pub fn validate(&self) -> io::Result<()> {
129 if self.version != 1 || self.options.source_id.is_empty() || self.next_line == 0 {
130 return Err(invalid("invalid JSONL checkpoint version or source identity"));
131 }
132 if self.pending_start > self.processed_bytes || self.processed_bytes > self.identity.length
133 || (self.pending_start == self.processed_bytes && self.pending_nonblank)
134 || (self.finished && (self.processed_bytes != self.identity.length
135 || self.pending_start != self.processed_bytes)) {
136 return Err(invalid("JSONL checkpoint byte accounting is inconsistent"));
137 }
138 let completed_lines = u64::try_from(self.records.len())
139 .ok().and_then(|count| count.checked_add(self.skipped_lines));
140 if completed_lines.and_then(|count| count.checked_add(1)) != Some(self.next_line) {
141 return Err(invalid("JSONL checkpoint physical row accounting is inconsistent"));
142 }
143 let pending_length = self.processed_bytes - self.pending_start;
144 if self.options.max_record_bytes.is_some_and(|limit| pending_length > limit) {
145 return Err(invalid("JSONL checkpoint contains an oversized pending row"));
146 }
147 let mut previous_end = 0;
148 let mut previous_line = 0;
149 for row in &self.records {
150 let end = row.offset.checked_add(row.length)
151 .ok_or_else(|| invalid("JSONL row byte range overflow"))?;
152 if row.length == 0 || row.offset < previous_end || end > self.pending_start
153 || row.line_number <= previous_line || row.line_number >= self.next_line
154 || self.options.max_record_bytes.is_some_and(|limit| row.length > limit) {
155 return Err(invalid("JSONL checkpoint row locations are inconsistent"));
156 }
157 previous_end = end.checked_add(1).unwrap_or(end);
158 previous_line = row.line_number;
159 }
160 Ok(())
161 }
162}
163
164pub struct JsonlIndexBuilder {
170 file: File,
171 state: JsonlIndexState,
172}
173
174impl JsonlIndexBuilder {
175 pub fn open(path: impl AsRef<Path>, options: JsonlIndexOptions) -> io::Result<Self> {
177 if options.source_id.is_empty() {
178 return Err(invalid("supply an immutable JSONL source identity"));
179 }
180 let file = File::open(path)?;
181 let identity = FileIdentity::read(&file)?;
182 let state = JsonlIndexState {
183 version: 1, options, identity, processed_bytes: 0, pending_start: 0,
184 pending_nonblank: false, next_line: 1, skipped_lines: 0,
185 records: Vec::new(), finished: false,
186 };
187 Ok(Self { file, state })
188 }
189
190 pub fn resume(path: impl AsRef<Path>, source_id: &str, state: JsonlIndexState) -> io::Result<Self> {
193 state.validate()?;
194 if source_id != state.options.source_id {
195 return Err(invalid("JSONL continuation belongs to a different source version"));
196 }
197 let file = File::open(path)?;
198 state.identity.check(&file)?;
199 Ok(Self { file, state })
200 }
201
202 pub fn state(&self) -> &JsonlIndexState { &self.state }
204
205 pub fn progress(&self) -> JsonlIndexProgress { self.state.progress() }
207
208 fn finish_line(&mut self) -> io::Result<()> {
209 let length = self.state.processed_bytes - self.state.pending_start;
210 let next_line = self.state.next_line.checked_add(1)
211 .ok_or_else(|| invalid("JSONL physical line count overflow"))?;
212 if !self.state.pending_nonblank {
213 match self.state.options.blank_lines {
214 JsonlBlankLines::Reject => return Err(invalid(format!(
215 "blank JSONL row at physical line {}", self.state.next_line))),
216 JsonlBlankLines::Skip => self.state.skipped_lines += 1,
217 }
218 } else {
219 self.state.records.push(JsonlRecordLocation {
220 offset: self.state.pending_start, length, line_number: self.state.next_line,
221 });
222 }
223 self.state.next_line = next_line;
224 self.state.pending_nonblank = false;
225 Ok(())
226 }
227
228 pub fn scan_step(&mut self, byte_budget: u64) -> io::Result<JsonlIndexProgress> {
232 self.state.identity.check(&self.file)?;
233 if self.state.finished || byte_budget == 0 { return Ok(self.progress()); }
234 self.file.seek(SeekFrom::Start(self.state.processed_bytes))?;
235 let mut remaining = byte_budget.min(self.state.identity.length - self.state.processed_bytes);
236 let mut buffer = [0u8; 16 * 1024];
237 while remaining != 0 {
238 let length = remaining.min(buffer.len() as u64) as usize;
239 self.file.read_exact(&mut buffer[..length])?;
240 for &byte in &buffer[..length] {
241 if byte == b'\n' {
242 self.finish_line()?;
243 self.state.processed_bytes += 1;
244 self.state.pending_start = self.state.processed_bytes;
245 } else {
246 let row_length = self.state.processed_bytes - self.state.pending_start;
247 if self.state.options.max_record_bytes.is_some_and(|limit| row_length >= limit) {
248 return Err(invalid(format!("JSONL row {} exceeds the selected byte limit", self.state.next_line)));
249 }
250 self.state.pending_nonblank |= !matches!(byte, b' ' | b'\t' | b'\r');
251 self.state.processed_bytes += 1;
252 }
253 }
254 remaining -= length as u64;
255 }
256 if self.state.processed_bytes == self.state.identity.length {
257 if self.state.pending_start != self.state.processed_bytes {
258 self.finish_line()?;
259 self.state.pending_start = self.state.processed_bytes;
260 }
261 self.state.finished = true;
262 }
263 self.state.identity.check(&self.file)?;
264 Ok(self.progress())
265 }
266
267 pub fn into_dataset<I: DeserializeOwned>(self) -> io::Result<IndexedJsonlDataset<I>> {
269 self.state.validate()?;
270 if !self.state.finished { return Err(invalid("JSONL indexing has not reached EOF")); }
271 self.state.identity.check(&self.file)?;
272 Ok(IndexedJsonlDataset {
273 file: Arc::new(Mutex::new(self.file)), state: Arc::new(self.state), item: PhantomData,
274 })
275 }
276}
277
278pub struct IndexedJsonlDataset<I> {
284 file: Arc<Mutex<File>>,
285 state: Arc<JsonlIndexState>,
286 item: PhantomData<fn() -> I>,
287}
288
289impl<I> Clone for IndexedJsonlDataset<I> {
290 fn clone(&self) -> Self {
291 Self { file: self.file.clone(), state: self.state.clone(), item: PhantomData }
292 }
293}
294
295impl<I: DeserializeOwned> IndexedJsonlDataset<I> {
296 pub fn from_index(path: impl AsRef<Path>, source_id: &str, state: JsonlIndexState) -> io::Result<Self> {
298 JsonlIndexBuilder::resume(path, source_id, state)?.into_dataset()
299 }
300
301 pub fn index(&self) -> &JsonlIndexState { &self.state }
303
304 pub fn get_raw(&self, index: usize) -> io::Result<Option<Vec<u8>>> {
307 let Some(row) = self.state.records.get(index) else { return Ok(None); };
308 let mut file = self.file.lock().map_err(|_| io::Error::other("JSONL source lock poisoned"))?;
309 self.state.identity.check(&file)?;
310 let bytes = Self::read_row(&mut file, row)?;
311 self.state.identity.check(&file)?;
312 Ok(Some(bytes))
313 }
314
315 fn read_row(file: &mut File, row: &JsonlRecordLocation) -> io::Result<Vec<u8>> {
316 let length = usize::try_from(row.length)
317 .map_err(|_| invalid("JSONL row is larger than the process address space"))?;
318 let mut bytes = Vec::new();
319 bytes.try_reserve_exact(length).map_err(|error| io::Error::other(error.to_string()))?;
320 bytes.resize(length, 0);
321 file.seek(SeekFrom::Start(row.offset))?;
322 file.read_exact(&mut bytes)?;
323 Ok(bytes)
324 }
325
326 pub fn get_result(&self, index: usize) -> io::Result<Option<I>> {
328 let Some(bytes) = self.get_raw(index)? else { return Ok(None); };
329 serde_json::from_slice(&bytes).map(Some).map_err(|error| invalid(format!(
330 "JSONL dataset index {index}, physical line {}: {error}",
331 self.state.records[index].line_number)))
332 }
333
334 pub fn get_many_raw(&self, indices: &[usize]) -> io::Result<Option<Vec<Vec<u8>>>> {
338 if indices.iter().any(|&index| index >= self.state.records.len()) { return Ok(None); }
339 if indices.is_empty() { return Ok(Some(Vec::new())); }
340 let mut file = self.file.lock().map_err(|_| io::Error::other("JSONL source lock poisoned"))?;
341 self.state.identity.check(&file)?;
342 let rows = indices.iter().map(|&index| Self::read_row(&mut file, &self.state.records[index]))
343 .collect::<io::Result<Vec<_>>>()?;
344 self.state.identity.check(&file)?;
345 Ok(Some(rows))
346 }
347
348 pub fn get_many_result(&self, indices: &[usize]) -> io::Result<Option<Vec<I>>> {
352 let Some(rows) = self.get_many_raw(indices)? else { return Ok(None); };
353 indices.iter().zip(rows).map(|(&index, bytes)| {
354 serde_json::from_slice(&bytes).map_err(|error| invalid(format!(
355 "JSONL dataset index {index}, physical line {}: {error}",
356 self.state.records[index].line_number)))
357 }).collect::<io::Result<Vec<_>>>().map(Some)
358 }
359}
360
361impl<I: DeserializeOwned> Dataset<I> for IndexedJsonlDataset<I> {
362 fn get(&self, index: usize) -> Option<I> {
365 self.get_result(index).unwrap_or_else(|error| panic!("indexed JSONL read failed: {error}"))
366 }
367
368 fn get_many(&self, indices: &[usize]) -> Option<Vec<I>> {
369 self.get_many_result(indices).unwrap_or_else(|error| panic!("indexed JSONL batch read failed: {error}"))
370 }
371
372 fn len(&self) -> usize { self.state.records.len() }
373}