@@ -29,31 +29,30 @@ use pyo3::prelude::*;
2929use pyo3:: types:: PyCapsule ;
3030use tokio:: runtime:: Handle ;
3131
32- /// A partition stream that wraps a Python object implementing `__arrow_c_stream__` .
32+ /// A partition stream that wraps a Python factory function that creates streams .
3333///
34- /// The stream is consumed lazily - only when `execute()` is called during query execution.
34+ /// The factory is called lazily on each `execute()` invocation, allowing
35+ /// the same table to be queried multiple times.
3536struct PyArrowStreamPartition {
3637 schema : SchemaRef ,
37- /// The Python object, wrapped in Option so it can be taken (consumed) exactly once .
38- /// We use std::sync::Mutex for Send + Sync .
39- py_stream : std :: sync :: Mutex < Option < Py < PyAny > > > ,
38+ /// A Python callable (factory) that returns a fresh stream implementing `__arrow_c_stream__` .
39+ /// Called on each execute() to create a new stream .
40+ stream_factory : Py < PyAny > ,
4041}
4142
4243impl PyArrowStreamPartition {
43- fn new ( py_obj : Py < PyAny > , schema : SchemaRef ) -> Self {
44+ fn new ( stream_factory : Py < PyAny > , schema : SchemaRef ) -> Self {
4445 Self {
4546 schema,
46- py_stream : std :: sync :: Mutex :: new ( Some ( py_obj ) ) ,
47+ stream_factory ,
4748 }
4849 }
4950}
5051
5152impl Debug for PyArrowStreamPartition {
5253 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
53- let consumed = self . py_stream . lock ( ) . unwrap ( ) . is_none ( ) ;
5454 f. debug_struct ( "PyArrowStreamPartition" )
5555 . field ( "schema" , & self . schema )
56- . field ( "consumed" , & consumed)
5756 . finish ( )
5857 }
5958}
@@ -64,14 +63,14 @@ impl PartitionStream for PyArrowStreamPartition {
6463 }
6564
6665 fn execute ( & self , _ctx : Arc < TaskContext > ) -> SendableRecordBatchStream {
67- // Take the Python object (can only be done once)
68- let py_obj = self . py_stream . lock ( ) . unwrap ( ) . take ( ) ;
66+ // Call the factory to get a fresh stream for this execution
67+ let batches: Vec < RecordBatch > = Python :: with_gil ( |py| {
68+ // Call the factory to get a fresh stream
69+ let stream_result = self . stream_factory . call0 ( py) ;
6970
70- let batches: Vec < RecordBatch > = match py_obj {
71- Some ( obj) => {
72- // Acquire the GIL and consume the stream
73- Python :: with_gil ( |py| {
74- let bound = obj. bind ( py) ;
71+ match stream_result {
72+ Ok ( stream_obj) => {
73+ let bound = stream_obj. bind ( py) ;
7574
7675 match ArrowArrayStreamReader :: from_pyarrow_bound ( bound) {
7776 Ok ( reader) => {
@@ -93,13 +92,13 @@ impl PartitionStream for PyArrowStreamPartition {
9392 vec ! [ ]
9493 }
9594 }
96- } )
95+ }
96+ Err ( e) => {
97+ eprintln ! ( "Warning: Failed to call stream factory: {e}" ) ;
98+ vec ! [ ]
99+ }
97100 }
98- None => {
99- // Stream already consumed, return empty
100- vec ! [ ]
101- }
102- } ;
101+ } ) ;
103102
104103 Box :: pin (
105104 MemoryStream :: try_new ( batches, Arc :: clone ( & self . schema ) , None )
@@ -108,31 +107,40 @@ impl PartitionStream for PyArrowStreamPartition {
108107 }
109108}
110109
111- /// A lazy table provider that wraps a Python Arrow stream.
110+ /// A lazy table provider that wraps a Python stream factory .
112111///
113112/// This class implements the `__datafusion_table_provider__` protocol, allowing
114113/// it to be registered with DataFusion's `SessionContext.register_table()`.
115114///
116115/// Data is NOT read until query execution time - this enables true lazy evaluation.
116+ /// The factory function is called on each query execution to create a fresh stream,
117+ /// allowing the same table to be queried multiple times.
117118///
118119/// # Example
119120///
120121/// ```python
121122/// from datafusion import SessionContext
122123/// from xarray_sql import LazyArrowStreamTable, XarrayRecordBatchReader
123124///
124- /// # Create a lazy reader (implements __arrow_c_stream__)
125- /// reader = XarrayRecordBatchReader(ds, chunks={'time': 240})
125+ /// # Create a factory that produces lazy readers
126+ /// def make_reader():
127+ /// return XarrayRecordBatchReader(ds, chunks={'time': 240})
128+ ///
129+ /// # Get schema from a sample reader
130+ /// sample = make_reader()
131+ /// schema = sample.schema
126132///
127- /// # Wrap in lazy table - NO DATA LOADED
128- /// table = LazyArrowStreamTable(reader )
133+ /// # Wrap factory in lazy table - NO DATA LOADED
134+ /// table = LazyArrowStreamTable(make_reader, schema )
129135///
130136/// # Register with DataFusion - STILL NO DATA LOADED
131137/// ctx = SessionContext()
132138/// ctx.register_table("air", table)
133139///
134140/// # Data only loaded HERE during collect()
141+ /// # Each query creates a fresh stream via the factory
135142/// result = ctx.sql("SELECT AVG(air) FROM air").collect()
143+ /// result2 = ctx.sql("SELECT * FROM air LIMIT 10").collect() # Works!
136144/// ```
137145#[ pyclass( name = "LazyArrowStreamTable" ) ]
138146struct LazyArrowStreamTable {
@@ -142,29 +150,39 @@ struct LazyArrowStreamTable {
142150
143151#[ pymethods]
144152impl LazyArrowStreamTable {
145- /// Create a new LazyArrowStreamTable from a Python object implementing `__arrow_c_stream__` .
153+ /// Create a new LazyArrowStreamTable from a stream factory function .
146154 ///
147155 /// Args:
148- /// stream: A Python object implementing the Arrow PyCapsule interface (`__arrow_c_stream__`).
149- /// This includes `pyarrow.RecordBatchReader`, `XarrayRecordBatchReader`, etc.
156+ /// stream_factory: A callable that returns a Python object implementing
157+ /// the Arrow PyCapsule interface (`__arrow_c_stream__`).
158+ /// Called on each query execution to create a fresh stream.
159+ /// schema: A PyArrow Schema for the table. Required since the factory
160+ /// hasn't been called yet.
150161 ///
151162 /// Raises:
152- /// TypeError: If the object does not implement `__arrow_c_stream__` .
163+ /// TypeError: If the schema is not a valid PyArrow Schema .
153164 #[ new]
154- fn new ( stream : & Bound < ' _ , PyAny > ) -> PyResult < Self > {
155- // Get the schema via the .schema attribute WITHOUT consuming the stream
156- // This is important because calling __arrow_c_stream__ would consume the stream
157- let schema = get_schema_from_stream ( stream) ?;
165+ fn new ( stream_factory : & Bound < ' _ , PyAny > , schema : & Bound < ' _ , PyAny > ) -> PyResult < Self > {
166+ // Convert the PyArrow schema to Arrow schema
167+ use arrow:: datatypes:: Schema ;
168+ use arrow:: pyarrow:: FromPyArrow ;
169+
170+ let arrow_schema = Schema :: from_pyarrow_bound ( schema) . map_err ( |e| {
171+ pyo3:: exceptions:: PyTypeError :: new_err ( format ! ( "Failed to convert schema: {e}" ) )
172+ } ) ?;
173+ let schema_ref = Arc :: new ( arrow_schema) ;
158174
159- // Create the partition stream with the Python object
160- let partition = PyArrowStreamPartition :: new ( stream. clone ( ) . unbind ( ) , schema. clone ( ) ) ;
175+ // Create the partition stream with the factory
176+ let partition =
177+ PyArrowStreamPartition :: new ( stream_factory. clone ( ) . unbind ( ) , schema_ref. clone ( ) ) ;
161178
162179 // Create the StreamingTable
163- let table = StreamingTable :: try_new ( schema, vec ! [ Arc :: new( partition) ] ) . map_err ( |e| {
164- pyo3:: exceptions:: PyRuntimeError :: new_err ( format ! (
165- "Failed to create StreamingTable: {e}"
166- ) )
167- } ) ?;
180+ let table =
181+ StreamingTable :: try_new ( schema_ref, vec ! [ Arc :: new( partition) ] ) . map_err ( |e| {
182+ pyo3:: exceptions:: PyRuntimeError :: new_err ( format ! (
183+ "Failed to create StreamingTable: {e}"
184+ ) )
185+ } ) ?;
168186
169187 Ok ( Self {
170188 table : Arc :: new ( table) ,
@@ -218,27 +236,6 @@ impl LazyArrowStreamTable {
218236 }
219237}
220238
221- /// Get schema from a Python object that has a schema attribute.
222- ///
223- /// This extracts the schema WITHOUT consuming the stream, which is
224- /// important for lazy evaluation.
225- fn get_schema_from_stream ( stream : & Bound < ' _ , PyAny > ) -> PyResult < SchemaRef > {
226- use arrow:: datatypes:: Schema ;
227- use arrow:: pyarrow:: FromPyArrow ;
228-
229- let py_schema = stream. getattr ( "schema" ) . map_err ( |e| {
230- pyo3:: exceptions:: PyTypeError :: new_err ( format ! (
231- "Object must have a 'schema' attribute (e.g., RecordBatchReader): {e}"
232- ) )
233- } ) ?;
234-
235- let schema = Schema :: from_pyarrow_bound ( & py_schema) . map_err ( |e| {
236- pyo3:: exceptions:: PyTypeError :: new_err ( format ! ( "Failed to convert schema: {e}" ) )
237- } ) ?;
238-
239- Ok ( Arc :: new ( schema) )
240- }
241-
242239/// Python module initialization
243240#[ pymodule]
244241fn _native ( m : & Bound < ' _ , PyModule > ) -> PyResult < ( ) > {
0 commit comments