blob: 9e4ff2d6dea66e9de5edb407f172de832c9a63fc [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::future::Future;
use paimon::io::{FileIO, FileRead};
use paimon::spec::BlobDescriptor;
use paimon_datafusion::runtime::runtime;
use paimon_datafusion::BlobReaderRegistry;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use crate::error::to_py_err;
fn block_on_runtime<F>(future: F, panic_error: &'static str) -> F::Output
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
if tokio::runtime::Handle::try_current().is_ok() {
let handle = runtime();
std::thread::spawn(move || handle.block_on(future))
.join()
.expect(panic_error)
} else {
runtime().block_on(future)
}
}
#[pyclass(name = "BlobReaderRegistry", skip_from_py_object)]
#[derive(Clone)]
pub(crate) struct PyBlobReaderRegistry {
inner: BlobReaderRegistry,
}
impl PyBlobReaderRegistry {
pub(crate) fn new(inner: BlobReaderRegistry) -> Self {
Self { inner }
}
}
#[pymethods]
impl PyBlobReaderRegistry {
fn open_blob_descriptor_stream(&self, raw_value: &[u8]) -> PyResult<Option<PyBlobInputStream>> {
if !BlobDescriptor::is_blob_descriptor(raw_value) {
return Ok(None);
}
let descriptor = BlobDescriptor::deserialize(raw_value).map_err(to_py_err)?;
let Some(file_io) = self.inner.resolve(descriptor.uri()) else {
return Ok(None);
};
Ok(Some(PyBlobInputStream::new(file_io, descriptor)?))
}
}
#[pyclass(name = "BlobInputStream")]
struct PyBlobInputStream {
file_io: FileIO,
uri: String,
offset: u64,
length: Option<u64>,
position: u64,
closed: bool,
}
impl PyBlobInputStream {
fn new(file_io: FileIO, descriptor: BlobDescriptor) -> PyResult<Self> {
if descriptor.offset() < 0 {
return Err(PyValueError::new_err(format!(
"BlobDescriptor has negative offset: {}",
descriptor.offset()
)));
}
if descriptor.length() < -1 {
return Err(PyValueError::new_err(format!(
"BlobDescriptor has invalid length: {}",
descriptor.length()
)));
}
Ok(Self {
file_io,
uri: descriptor.uri().to_string(),
offset: descriptor.offset() as u64,
length: (descriptor.length() >= 0).then_some(descriptor.length() as u64),
position: 0,
closed: false,
})
}
fn ensure_open(&self) -> PyResult<()> {
if self.closed {
Err(PyValueError::new_err("I/O operation on closed file."))
} else {
Ok(())
}
}
fn stream_length(&self, py: Python<'_>) -> PyResult<u64> {
if let Some(length) = self.length {
return Ok(length);
}
let file_io = self.file_io.clone();
let uri = self.uri.clone();
let offset = self.offset;
py.detach(|| {
block_on_runtime(
async move {
let input = file_io.new_input(&uri).map_err(to_py_err)?;
let metadata = input.metadata().await.map_err(to_py_err)?;
Ok(metadata.size.saturating_sub(offset))
},
"paimon blob metadata read thread panicked",
)
})
}
fn read_bytes(&mut self, py: Python<'_>, size: isize) -> PyResult<Vec<u8>> {
self.ensure_open()?;
let stream_length = self.stream_length(py)?;
let remaining = stream_length.saturating_sub(self.position);
if remaining == 0 || size == 0 {
return Ok(Vec::new());
}
let to_read = if size < 0 {
remaining
} else {
remaining.min(size as u64)
};
let start = self.offset + self.position;
let end = start + to_read;
let file_io = self.file_io.clone();
let uri = self.uri.clone();
let bytes = py.detach(|| {
block_on_runtime(
async move {
let input = file_io.new_input(&uri).map_err(to_py_err)?;
let reader = input.reader().await.map_err(to_py_err)?;
let bytes = reader.read(start..end).await.map_err(to_py_err)?;
Ok::<_, PyErr>(bytes.to_vec())
},
"paimon blob range read thread panicked",
)
})?;
self.position += bytes.len() as u64;
Ok(bytes)
}
}
#[pymethods]
impl PyBlobInputStream {
fn readable(&self) -> bool {
true
}
fn seekable(&self) -> bool {
true
}
fn tell(&self) -> u64 {
self.position
}
#[getter]
fn closed(&self) -> bool {
self.closed
}
fn __enter__(slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> {
slf
}
fn __exit__(
&mut self,
_exc_type: &Bound<'_, PyAny>,
_exc: &Bound<'_, PyAny>,
_traceback: &Bound<'_, PyAny>,
) -> bool {
self.close();
false
}
#[pyo3(signature = (size = -1))]
fn read<'py>(&mut self, py: Python<'py>, size: isize) -> PyResult<Bound<'py, PyBytes>> {
let bytes = self.read_bytes(py, size)?;
Ok(PyBytes::new(py, &bytes))
}
#[pyo3(signature = (pos, whence = 0))]
fn seek(&mut self, py: Python<'_>, pos: i64, whence: i32) -> PyResult<u64> {
self.ensure_open()?;
let base = match whence {
0 => 0,
1 => self.position as i64,
2 => self.stream_length(py)? as i64,
other => return Err(PyValueError::new_err(format!("Invalid whence: {other}"))),
};
let target = base
.checked_add(pos)
.ok_or_else(|| PyValueError::new_err("Seek position overflow"))?;
if target < 0 {
return Err(PyValueError::new_err(format!(
"Negative seek position: {target}"
)));
}
self.position = target as u64;
Ok(self.position)
}
fn close(&mut self) {
self.closed = true;
}
}