| // 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; |
| } |
| } |