use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use polars::io::csv::read::_csv_read_internal::CountLines;
use polars::io::csv::read::CommentPrefix;
use polars::lazy::dsl::{DslPlan, FileScanDsl, FunctionExpr, ScanSources, StringFunction};
use polars::prelude::*;
use polars_buffer::Buffer;
#[cfg(not(test))]
const CHUNK: usize = 1 << 20;
#[cfg(test)]
const CHUNK: usize = 64;
#[cfg(test)]
thread_local! {
static COUNTED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
const RUN_NAME: &str = "CSV_RUN";
pub(crate) struct CsvMarks {
scan: DslPlan,
source: Source,
head: CsvReadOptions,
run: CsvReadOptions,
schema: SchemaRef,
known: Mutex<Known>,
}
enum Source {
Path(PathBuf),
Buffer(Buffer<u8>),
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Stamp {
len: u64,
modified: Option<SystemTime>,
inode: u64,
ends: u64,
}
const STAMP_BYTES: usize = 4096;
#[derive(Default)]
struct Known {
stamp: Option<Stamp>,
at: Vec<(usize, usize)>,
broken: bool,
}
impl Known {
fn floor(&self, row: usize) -> Option<(usize, usize)> {
let i = self.at.partition_point(|&(r, _)| r <= row);
self.at.get(i.checked_sub(1)?).copied()
}
fn mark(&mut self, row: usize, byte: usize) {
let i = self.at.partition_point(|&(r, _)| r < row);
match self.at.get(i) {
Some(&(r, b)) if r == row => self.broken |= b != byte,
next => {
let before = i.checked_sub(1).map(|j| self.at[j]);
let in_order =
before.is_none_or(|(_, b)| b < byte) && next.is_none_or(|&(_, b)| byte < b);
if in_order {
self.at.insert(i, (row, byte));
} else {
self.broken = true;
}
}
}
}
}
impl CsvMarks {
pub(crate) fn of(lf: &LazyFrame) -> Option<Arc<CsvMarks>> {
let mut scans = (&lf.logical_plan)
.into_iter()
.filter(|node| matches!(node, DslPlan::Scan { .. }));
let scan = scans.next()?.clone();
if scans.next().is_some() {
return None;
}
let DslPlan::Scan {
sources,
unified_scan_args: args,
scan_type,
..
} = &scan
else {
return None;
};
let FileScanDsl::Csv { options } = &**scan_type else {
return None;
};
if args.row_index.is_some()
|| args.include_file_paths.is_some()
|| args.pre_slice.is_some()
|| options.n_rows.is_some()
|| options.ignore_errors
{
return None;
}
let source = match sources {
ScanSources::Paths(paths) if paths.len() == 1 && !args.glob => {
let path = PathBuf::from(paths[0].as_str());
if crate::cloud::source::is_remote_url(&path) {
return None;
}
Source::Path(path)
}
ScanSources::Buffers(buffers) if buffers.len() == 1 => {
Source::Buffer(buffers[0].clone())
}
_ => return None,
};
let DslPlan::Scan { cached_ir, .. } = &scan else {
return None;
};
if !cached_ir.lock().is_ok_and(|ir| ir.is_some()) {
return None;
}
let schema = LazyFrame::from(scan.clone()).collect_schema().ok()?;
let run = crate::loading::follow::run_options(options, &schema)?;
let mut head = (**options).clone();
head.schema = Some(schema.clone());
Some(Arc::new(CsvMarks {
scan,
source,
head,
run,
schema,
known: Mutex::default(),
}))
}
pub(crate) fn window(
self: &Arc<Self>,
lf: &LazyFrame,
start: usize,
len: usize,
) -> Option<LazyFrame> {
let run = LazyFrame::anonymous_scan(
Arc::new(Run {
marks: self.clone(),
start,
len,
}),
ScanArgsAnonymous {
schema: Some(self.schema.clone()),
name: RUN_NAME,
..Default::default()
},
)
.ok()?;
let mut plan = lf.logical_plan.clone();
self.replace(&mut plan, &run.logical_plan).then(|| {
let mut out = lf.clone();
out.logical_plan = plan;
out
})
}
fn replace(&self, plan: &mut DslPlan, with: &DslPlan) -> bool {
match plan {
DslPlan::IR { dsl, .. } => {
let mut inner = Arc::unwrap_or_clone(dsl.clone());
let replaced = self.replace(&mut inner, with);
*plan = inner;
replaced
}
DslPlan::Scan { cached_ir, .. } => {
let DslPlan::Scan {
cached_ir: ours, ..
} = &self.scan
else {
return false;
};
if !Arc::ptr_eq(cached_ir, ours) {
return false;
}
*plan = with.clone();
true
}
DslPlan::Select { input, expr, .. } if expr.iter().all(per_row) => {
self.replace(Arc::make_mut(input), with)
}
DslPlan::HStack { input, exprs, .. } if exprs.iter().all(per_row) => {
self.replace(Arc::make_mut(input), with)
}
DslPlan::MapFunction { input, function }
if matches!(function.to_string().as_str(), "RENAME" | "FILL_NAN") =>
{
self.replace(Arc::make_mut(input), with)
}
_ => false,
}
}
fn bytes(&self) -> PolarsResult<(Buffer<u8>, std::sync::MutexGuard<'_, Known>)> {
let (bytes, stamp) = match &self.source {
Source::Buffer(buffer) => (buffer.clone(), None),
Source::Path(path) => {
let file = std::fs::File::open(path)?;
let meta = file.metadata()?;
let bytes = if meta.len() == 0 {
Buffer::new()
} else {
Buffer::from_owner(unsafe { memmap2::Mmap::map(&file)? })
};
let stamp = Stamp {
len: meta.len(),
modified: meta.modified().ok(),
inode: inode(&meta),
ends: ends_hash(&bytes),
};
(bytes, Some(stamp))
}
};
let mut known = self.known.lock().unwrap_or_else(|e| e.into_inner());
if known.stamp != stamp || (known.at.is_empty() && !known.broken) {
*known = Known {
stamp,
..Known::default()
};
if let Ok(Some(first)) = self.first_row(&bytes) {
known.at.push((0, first));
}
}
Ok((bytes, known))
}
fn first_row(&self, bytes: &Buffer<u8>) -> PolarsResult<Option<usize>> {
let mut reader =
polars::io::utils::compression::ByteSourceReader::from_memory(bytes.clone())?;
let (_, rest) = polars::io::csv::read::streaming::read_until_start_and_infer_schema(
&self.head,
None,
Some(bytes.len()),
None,
&mut reader,
)?;
if rest.is_empty() {
return Ok(Some(bytes.len()));
}
let at = (rest.as_ptr() as usize).checked_sub(bytes.as_ptr() as usize);
Ok(at.filter(|&at| at + rest.len() <= bytes.len()))
}
fn read(
&self,
start: usize,
len: usize,
columns: Option<&[PlSmallStr]>,
) -> PolarsResult<DataFrame> {
match self.read_from_marks(start, len, columns) {
Some(df) => Ok(df),
None => self.through_polars(start, len, columns),
}
}
fn through_polars(
&self,
start: usize,
len: usize,
columns: Option<&[PlSmallStr]>,
) -> PolarsResult<DataFrame> {
let mut lf = LazyFrame::from(self.scan.clone());
if let Some(columns) = columns {
lf = lf.select(
columns
.iter()
.map(|name| col(name.clone()))
.collect::<Vec<_>>(),
);
}
lf.slice(start as i64, len as IdxSize).collect()
}
fn read_from_marks(
&self,
start: usize,
len: usize,
columns: Option<&[PlSmallStr]>,
) -> Option<DataFrame> {
let (bytes, mut known) = self.bytes().ok()?;
if known.broken {
return None;
}
let begin = known.floor(start)?;
let counter = Counter::of(&self.run);
let (row, from) = counter.seek(&bytes, &mut known, begin, start);
let (end_row, to) = if row < start {
(row, from)
} else {
counter.seek(&bytes, &mut known, (row, from), start + len)
};
if known.broken {
return None;
}
let counted = end_row.saturating_sub(start);
if counted == 0 {
return None;
}
let first = self.schema.iter_names().next().cloned();
let parse_columns = match columns {
Some([]) => first.as_ref().map(std::slice::from_ref),
columns => columns,
};
match self.parse(&bytes[from..to], parse_columns) {
Ok(df) if df.height() == counted => match columns {
Some([]) => Some(DataFrame::empty_with_height(counted)),
_ => Some(df),
},
_ => {
known.broken = true;
None
}
}
}
fn parse(&self, run: &[u8], columns: Option<&[PlSmallStr]>) -> PolarsResult<DataFrame> {
let mut options = self.run.clone();
if let Some(columns) = columns {
options.columns = Some(columns.iter().cloned().collect());
}
let df = options
.into_reader_with_file_handle(std::io::Cursor::new(run))
.finish()?;
project(df, columns)
}
}
fn project(df: DataFrame, columns: Option<&[PlSmallStr]>) -> PolarsResult<DataFrame> {
match columns {
Some(columns) => df.select(columns.iter().cloned()),
None => Ok(df),
}
}
fn per_row(expr: &Expr) -> bool {
match expr {
Expr::Column(_) => true,
Expr::Literal(value) => value.is_scalar(),
Expr::Alias(inner, _) | Expr::KeepName(inner) => per_row(inner),
Expr::Cast { expr, .. } => per_row(expr),
Expr::BinaryExpr { left, right, .. } => per_row(left) && per_row(right),
Expr::Ternary {
predicate,
truthy,
falsy,
} => per_row(predicate) && per_row(truthy) && per_row(falsy),
Expr::Function {
function: FunctionExpr::StringExpr(StringFunction::Strptime(_, options)),
input,
} => options.format.is_some() && input.iter().all(per_row),
Expr::Function { input, function } => {
PER_ROW_FUNCTIONS.contains(&function.to_string().as_str()) && input.iter().all(per_row)
}
_ => false,
}
}
const PER_ROW_FUNCTIONS: &[&str] = &[
"str.strip_chars",
"str.strip_chars_start",
"str.strip_chars_end",
"str.to_integer",
"str.replace",
"str.replace_all",
"str.to_lowercase",
"str.to_uppercase",
"str.len_bytes",
"str.len_chars",
"str.contains",
"str.starts_with",
"str.ends_with",
"is_null",
"is_not_null",
"fill_null",
"coalesce",
"abs",
"round",
"not",
];
#[cfg(unix)]
fn inode(meta: &std::fs::Metadata) -> u64 {
std::os::unix::fs::MetadataExt::ino(meta)
}
#[cfg(not(unix))]
fn inode(_meta: &std::fs::Metadata) -> u64 {
0
}
fn ends_hash(bytes: &[u8]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes[..bytes.len().min(STAMP_BYTES)].hash(&mut hasher);
bytes[bytes.len().saturating_sub(STAMP_BYTES)..].hash(&mut hasher);
hasher.finish()
}
struct Counter {
lines: CountLines,
quote: Option<u8>,
separator: u8,
eol: u8,
comment: Option<Vec<u8>>,
}
impl Counter {
fn of(options: &CsvReadOptions) -> Counter {
let parse = &options.parse_options;
Counter {
lines: CountLines::new(
parse.quote_char,
parse.eol_char,
parse.comment_prefix.clone(),
),
quote: parse.quote_char,
separator: parse.separator,
eol: parse.eol_char,
comment: parse.comment_prefix.as_ref().map(|prefix| match prefix {
CommentPrefix::Single(c) => vec![*c],
CommentPrefix::Multi(s) => s.as_bytes().to_vec(),
}),
}
}
fn seek(
&self,
bytes: &[u8],
known: &mut Known,
at: (usize, usize),
target: usize,
) -> (usize, usize) {
let (mut row, mut pos) = at;
let mut spans = vec![(row, pos)];
while row < target && pos < bytes.len() {
let end = match self.row_end(bytes, pos) {
RowEnd::At(end) => end,
RowEnd::Open if self.only_comments(&bytes[pos..]) => {
pos = bytes.len();
break;
}
RowEnd::Open => bytes.len(),
RowEnd::Stray => {
known.broken = true;
return (row, pos);
}
};
#[cfg(test)]
COUNTED.with(|counted| counted.set(counted.get() + end - pos));
pos = end;
row += 1;
if spans.last().is_some_and(|&(_, at)| pos - at >= CHUNK) && pos < bytes.len() {
known.mark(row, pos);
spans.push((row, pos));
}
}
spans.push((row, pos));
if !self.chunker_agrees_on(bytes, &spans) {
known.broken = true;
} else if row == target && pos < bytes.len() {
known.mark(row, pos);
}
(row, pos)
}
fn chunker_agrees(&self, bytes: &[u8], from: (usize, usize), to: (usize, usize)) -> bool {
if to.1 == from.1 {
return to.0 == from.0;
}
let last = to.1 == bytes.len();
self.lines.count_rows(&bytes[from.1..to.1], last) == (to.0 - from.0, to.1 - from.1)
}
fn chunker_agrees_on(&self, bytes: &[u8], points: &[(usize, usize)]) -> bool {
let spans: Vec<_> = points.windows(2).map(|w| (w[0], w[1])).collect();
let threads = std::thread::available_parallelism()
.map_or(1, |n| n.get())
.min(8);
if spans.len() < 4 || threads < 2 {
return spans
.iter()
.all(|&(from, to)| self.chunker_agrees(bytes, from, to));
}
let per = spans.len().div_ceil(threads);
std::thread::scope(|scope| {
let parts: Vec<_> = spans
.chunks(per)
.map(|part| {
scope.spawn(move || {
part.iter()
.all(|&(from, to)| self.chunker_agrees(bytes, from, to))
})
})
.collect();
parts.into_iter().all(|part| part.join().unwrap_or(false))
})
}
fn only_comments(&self, mut rest: &[u8]) -> bool {
let Some(prefix) = &self.comment else {
return false;
};
while rest.starts_with(prefix) {
match memchr::memchr(self.eol, rest) {
Some(n) => rest = &rest[n + 1..],
None => return true,
}
}
rest.is_empty()
}
fn row_end(&self, bytes: &[u8], pos: usize) -> RowEnd {
self.find_row_end(bytes, pos).unwrap_or(RowEnd::Open)
}
fn find_row_end(&self, bytes: &[u8], mut pos: usize) -> Option<RowEnd> {
if let Some(prefix) = &self.comment {
while bytes[pos..].starts_with(prefix) {
pos += memchr::memchr(self.eol, &bytes[pos..])? + 1;
}
}
let quote = self.quote;
let mut field_start = pos;
let mut i = pos;
loop {
if quote.is_some_and(|q| bytes.get(i) == Some(&q)) && i == field_start {
let q = quote.unwrap_or_default();
i += 1;
loop {
i += memchr::memchr(q, bytes.get(i..)?)?;
if bytes.get(i + 1) == Some(&q) {
i += 2;
} else {
i += 1;
break;
}
}
continue;
}
let rest = bytes.get(i..)?;
let n = match quote {
Some(q) => memchr::memchr3(self.separator, self.eol, q, rest)?,
None => memchr::memchr2(self.separator, self.eol, rest)?,
};
i += n;
let c = bytes[i];
if c == self.eol {
return Some(RowEnd::At(i + 1));
}
if Some(c) == quote {
return Some(RowEnd::Stray);
}
i += 1;
if c == self.separator {
field_start = i;
}
}
}
}
enum RowEnd {
At(usize),
Open,
Stray,
}
struct Run {
marks: Arc<CsvMarks>,
start: usize,
len: usize,
}
impl AnonymousScan for Run {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn schema(&self, _infer_schema_length: Option<usize>) -> PolarsResult<SchemaRef> {
Ok(self.marks.schema.clone())
}
fn allows_projection_pushdown(&self) -> bool {
true
}
fn scan(&self, args: AnonymousScanArgs) -> PolarsResult<DataFrame> {
let len = args.n_rows.map_or(self.len, |n| n.min(self.len));
self.marks
.read(self.start, len, args.with_columns.as_deref())
}
}
#[cfg(test)]
#[path = "csv_marks_tests.rs"]
mod tests;