use super::Dataset;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{
fs::File,
io::{self, Read, Seek, SeekFrom},
marker::PhantomData,
path::Path,
sync::{Arc, Mutex},
time::UNIX_EPOCH,
};
fn invalid(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message.into())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonlBlankLines {
Reject,
Skip,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlIndexOptions {
pub source_id: String,
pub blank_lines: JsonlBlankLines,
pub max_record_bytes: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct FileIdentity {
length: u64,
modified: Option<(u64, u32)>,
}
impl FileIdentity {
fn read(file: &File) -> io::Result<Self> {
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(invalid("indexed JSONL requires a regular immutable file"));
}
let modified = metadata.modified().ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|duration| (duration.as_secs(), duration.subsec_nanos()));
Ok(Self { length: metadata.len(), modified })
}
fn check(&self, file: &File) -> io::Result<()> {
if self != &Self::read(file)? {
return Err(invalid("JSONL source length or modification time changed"));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlRecordLocation {
pub offset: u64,
pub length: u64,
pub line_number: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlIndexState {
version: u32,
options: JsonlIndexOptions,
identity: FileIdentity,
processed_bytes: u64,
pending_start: u64,
pending_nonblank: bool,
next_line: u64,
skipped_lines: u64,
records: Vec<JsonlRecordLocation>,
finished: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct JsonlIndexProgress {
pub processed_bytes: u64,
pub total_bytes: u64,
pub records: usize,
pub skipped_lines: u64,
pub finished: bool,
}
impl JsonlIndexState {
pub fn options(&self) -> &JsonlIndexOptions { &self.options }
pub fn records(&self) -> &[JsonlRecordLocation] { &self.records }
pub fn progress(&self) -> JsonlIndexProgress {
JsonlIndexProgress {
processed_bytes: self.processed_bytes,
total_bytes: self.identity.length,
records: self.records.len(),
skipped_lines: self.skipped_lines,
finished: self.finished,
}
}
pub fn validate(&self) -> io::Result<()> {
if self.version != 1 || self.options.source_id.is_empty() || self.next_line == 0 {
return Err(invalid("invalid JSONL checkpoint version or source identity"));
}
if self.pending_start > self.processed_bytes || self.processed_bytes > self.identity.length
|| (self.pending_start == self.processed_bytes && self.pending_nonblank)
|| (self.finished && (self.processed_bytes != self.identity.length
|| self.pending_start != self.processed_bytes)) {
return Err(invalid("JSONL checkpoint byte accounting is inconsistent"));
}
let completed_lines = u64::try_from(self.records.len())
.ok().and_then(|count| count.checked_add(self.skipped_lines));
if completed_lines.and_then(|count| count.checked_add(1)) != Some(self.next_line) {
return Err(invalid("JSONL checkpoint physical row accounting is inconsistent"));
}
let pending_length = self.processed_bytes - self.pending_start;
if self.options.max_record_bytes.is_some_and(|limit| pending_length > limit) {
return Err(invalid("JSONL checkpoint contains an oversized pending row"));
}
let mut previous_end = 0;
let mut previous_line = 0;
for row in &self.records {
let end = row.offset.checked_add(row.length)
.ok_or_else(|| invalid("JSONL row byte range overflow"))?;
if row.length == 0 || row.offset < previous_end || end > self.pending_start
|| row.line_number <= previous_line || row.line_number >= self.next_line
|| self.options.max_record_bytes.is_some_and(|limit| row.length > limit) {
return Err(invalid("JSONL checkpoint row locations are inconsistent"));
}
previous_end = end.checked_add(1).unwrap_or(end);
previous_line = row.line_number;
}
Ok(())
}
}
pub struct JsonlIndexBuilder {
file: File,
state: JsonlIndexState,
}
impl JsonlIndexBuilder {
pub fn open(path: impl AsRef<Path>, options: JsonlIndexOptions) -> io::Result<Self> {
if options.source_id.is_empty() {
return Err(invalid("supply an immutable JSONL source identity"));
}
let file = File::open(path)?;
let identity = FileIdentity::read(&file)?;
let state = JsonlIndexState {
version: 1, options, identity, processed_bytes: 0, pending_start: 0,
pending_nonblank: false, next_line: 1, skipped_lines: 0,
records: Vec::new(), finished: false,
};
Ok(Self { file, state })
}
pub fn resume(path: impl AsRef<Path>, source_id: &str, state: JsonlIndexState) -> io::Result<Self> {
state.validate()?;
if source_id != state.options.source_id {
return Err(invalid("JSONL continuation belongs to a different source version"));
}
let file = File::open(path)?;
state.identity.check(&file)?;
Ok(Self { file, state })
}
pub fn state(&self) -> &JsonlIndexState { &self.state }
pub fn progress(&self) -> JsonlIndexProgress { self.state.progress() }
fn finish_line(&mut self) -> io::Result<()> {
let length = self.state.processed_bytes - self.state.pending_start;
let next_line = self.state.next_line.checked_add(1)
.ok_or_else(|| invalid("JSONL physical line count overflow"))?;
if !self.state.pending_nonblank {
match self.state.options.blank_lines {
JsonlBlankLines::Reject => return Err(invalid(format!(
"blank JSONL row at physical line {}", self.state.next_line))),
JsonlBlankLines::Skip => self.state.skipped_lines += 1,
}
} else {
self.state.records.push(JsonlRecordLocation {
offset: self.state.pending_start, length, line_number: self.state.next_line,
});
}
self.state.next_line = next_line;
self.state.pending_nonblank = false;
Ok(())
}
pub fn scan_step(&mut self, byte_budget: u64) -> io::Result<JsonlIndexProgress> {
self.state.identity.check(&self.file)?;
if self.state.finished || byte_budget == 0 { return Ok(self.progress()); }
self.file.seek(SeekFrom::Start(self.state.processed_bytes))?;
let mut remaining = byte_budget.min(self.state.identity.length - self.state.processed_bytes);
let mut buffer = [0u8; 16 * 1024];
while remaining != 0 {
let length = remaining.min(buffer.len() as u64) as usize;
self.file.read_exact(&mut buffer[..length])?;
for &byte in &buffer[..length] {
if byte == b'\n' {
self.finish_line()?;
self.state.processed_bytes += 1;
self.state.pending_start = self.state.processed_bytes;
} else {
let row_length = self.state.processed_bytes - self.state.pending_start;
if self.state.options.max_record_bytes.is_some_and(|limit| row_length >= limit) {
return Err(invalid(format!("JSONL row {} exceeds the selected byte limit", self.state.next_line)));
}
self.state.pending_nonblank |= !matches!(byte, b' ' | b'\t' | b'\r');
self.state.processed_bytes += 1;
}
}
remaining -= length as u64;
}
if self.state.processed_bytes == self.state.identity.length {
if self.state.pending_start != self.state.processed_bytes {
self.finish_line()?;
self.state.pending_start = self.state.processed_bytes;
}
self.state.finished = true;
}
self.state.identity.check(&self.file)?;
Ok(self.progress())
}
pub fn into_dataset<I: DeserializeOwned>(self) -> io::Result<IndexedJsonlDataset<I>> {
self.state.validate()?;
if !self.state.finished { return Err(invalid("JSONL indexing has not reached EOF")); }
self.state.identity.check(&self.file)?;
Ok(IndexedJsonlDataset {
file: Arc::new(Mutex::new(self.file)), state: Arc::new(self.state), item: PhantomData,
})
}
}
pub struct IndexedJsonlDataset<I> {
file: Arc<Mutex<File>>,
state: Arc<JsonlIndexState>,
item: PhantomData<fn() -> I>,
}
impl<I> Clone for IndexedJsonlDataset<I> {
fn clone(&self) -> Self {
Self { file: self.file.clone(), state: self.state.clone(), item: PhantomData }
}
}
impl<I: DeserializeOwned> IndexedJsonlDataset<I> {
pub fn from_index(path: impl AsRef<Path>, source_id: &str, state: JsonlIndexState) -> io::Result<Self> {
JsonlIndexBuilder::resume(path, source_id, state)?.into_dataset()
}
pub fn index(&self) -> &JsonlIndexState { &self.state }
pub fn get_raw(&self, index: usize) -> io::Result<Option<Vec<u8>>> {
let Some(row) = self.state.records.get(index) else { return Ok(None); };
let mut file = self.file.lock().map_err(|_| io::Error::other("JSONL source lock poisoned"))?;
self.state.identity.check(&file)?;
let bytes = Self::read_row(&mut file, row)?;
self.state.identity.check(&file)?;
Ok(Some(bytes))
}
fn read_row(file: &mut File, row: &JsonlRecordLocation) -> io::Result<Vec<u8>> {
let length = usize::try_from(row.length)
.map_err(|_| invalid("JSONL row is larger than the process address space"))?;
let mut bytes = Vec::new();
bytes.try_reserve_exact(length).map_err(|error| io::Error::other(error.to_string()))?;
bytes.resize(length, 0);
file.seek(SeekFrom::Start(row.offset))?;
file.read_exact(&mut bytes)?;
Ok(bytes)
}
pub fn get_result(&self, index: usize) -> io::Result<Option<I>> {
let Some(bytes) = self.get_raw(index)? else { return Ok(None); };
serde_json::from_slice(&bytes).map(Some).map_err(|error| invalid(format!(
"JSONL dataset index {index}, physical line {}: {error}",
self.state.records[index].line_number)))
}
pub fn get_many_raw(&self, indices: &[usize]) -> io::Result<Option<Vec<Vec<u8>>>> {
if indices.iter().any(|&index| index >= self.state.records.len()) { return Ok(None); }
if indices.is_empty() { return Ok(Some(Vec::new())); }
let mut file = self.file.lock().map_err(|_| io::Error::other("JSONL source lock poisoned"))?;
self.state.identity.check(&file)?;
let rows = indices.iter().map(|&index| Self::read_row(&mut file, &self.state.records[index]))
.collect::<io::Result<Vec<_>>>()?;
self.state.identity.check(&file)?;
Ok(Some(rows))
}
pub fn get_many_result(&self, indices: &[usize]) -> io::Result<Option<Vec<I>>> {
let Some(rows) = self.get_many_raw(indices)? else { return Ok(None); };
indices.iter().zip(rows).map(|(&index, bytes)| {
serde_json::from_slice(&bytes).map_err(|error| invalid(format!(
"JSONL dataset index {index}, physical line {}: {error}",
self.state.records[index].line_number)))
}).collect::<io::Result<Vec<_>>>().map(Some)
}
}
impl<I: DeserializeOwned> Dataset<I> for IndexedJsonlDataset<I> {
fn get(&self, index: usize) -> Option<I> {
self.get_result(index).unwrap_or_else(|error| panic!("indexed JSONL read failed: {error}"))
}
fn get_many(&self, indices: &[usize]) -> Option<Vec<I>> {
self.get_many_result(indices).unwrap_or_else(|error| panic!("indexed JSONL batch read failed: {error}"))
}
fn len(&self) -> usize { self.state.records.len() }
}