@@ -337,6 +337,9 @@ impl PyDuckDBReader {
337337 /// ----------
338338 /// query : str
339339 /// The ggsql query (SQL + VISUALISE clause).
340+ /// data : dict[str, polars.DataFrame] | None
341+ /// Optional dictionary mapping table names to DataFrames. Tables are
342+ /// registered before execution and unregistered afterward (even on error).
340343 ///
341344 /// Returns
342345 /// -------
@@ -354,11 +357,48 @@ impl PyDuckDBReader {
354357 /// >>> spec = reader.execute("SELECT 1 AS x, 2 AS y VISUALISE x, y DRAW point")
355358 /// >>> writer = VegaLiteWriter()
356359 /// >>> json_output = writer.render(spec)
357- fn execute ( & self , query : & str ) -> PyResult < PySpec > {
358- self . inner
360+ #[ pyo3( signature = ( query, * , data=None ) ) ]
361+ fn execute ( & self , py : Python < ' _ > , query : & str , data : Option < & Bound < ' _ , PyDict > > ) -> PyResult < PySpec > {
362+ // Register DataFrames from data dict
363+ let registered_names = if let Some ( data_dict) = data {
364+ self . register_data_dict ( py, data_dict) ?
365+ } else {
366+ vec ! [ ]
367+ } ;
368+
369+ // Execute query (capture result, don't return early)
370+ let result = self . inner
359371 . execute ( query)
360372 . map ( |s| PySpec { inner : s } )
361- . map_err ( ggsql_err_to_py)
373+ . map_err ( ggsql_err_to_py) ;
374+
375+ // Cleanup: unregister temporary tables (even on error)
376+ for name in & registered_names {
377+ let _ = self . inner . unregister ( name) ;
378+ }
379+
380+ result
381+ }
382+ }
383+
384+ impl PyDuckDBReader {
385+ /// Register DataFrames from a Python dict. Returns list of registered names for cleanup.
386+ /// This is a private Rust helper, not exposed to Python.
387+ fn register_data_dict (
388+ & self ,
389+ py : Python < ' _ > ,
390+ data : & Bound < ' _ , PyDict > ,
391+ ) -> PyResult < Vec < String > > {
392+ let mut names = Vec :: new ( ) ;
393+ for ( key, value) in data. iter ( ) {
394+ let name: String = key. extract ( ) ?;
395+ let df = py_to_polars ( py, & value) ?;
396+ self . inner
397+ . register ( & name, df, true )
398+ . map_err ( ggsql_err_to_py) ?;
399+ names. push ( name) ;
400+ }
401+ Ok ( names)
362402 }
363403}
364404
@@ -729,6 +769,9 @@ fn validate(query: &str) -> PyResult<PyValidated> {
729769/// The database reader to execute SQL against. Can be a native Reader
730770/// for optimal performance, or any Python object with an
731771/// `execute_sql(sql: str) -> polars.DataFrame` method.
772+ /// data : dict[str, polars.DataFrame] | None
773+ /// Optional dictionary mapping table names to DataFrames. Tables are
774+ /// registered before execution and unregistered afterward (even on error).
732775///
733776/// Returns
734777/// -------
@@ -755,19 +798,80 @@ fn validate(query: &str) -> PyResult<PyValidated> {
755798/// >>> reader = MyReader()
756799/// >>> spec = execute("SELECT * FROM data VISUALISE x, y DRAW point", reader)
757800#[ pyfunction]
758- fn execute ( query : & str , reader : & Bound < ' _ , PyAny > ) -> PyResult < PySpec > {
759- // Fast path: try all known native reader types
760- // Add new native readers to this list as they're implemented
761- try_native_readers ! ( query, reader, PyDuckDBReader ) ;
801+ #[ pyo3( signature = ( query, reader, * , data=None ) ) ]
802+ fn execute ( py : Python < ' _ > , query : & str , reader : & Bound < ' _ , PyAny > , data : Option < & Bound < ' _ , PyDict > > ) -> PyResult < PySpec > {
803+ // Native reader fast path: DuckDBReader
804+ // Note: we can't use the try_native_readers! macro here because it uses `return`
805+ // which would skip cleanup of registered tables.
806+ if let Ok ( native) = reader. downcast :: < PyDuckDBReader > ( ) {
807+ // Register DataFrames if provided
808+ let registered_names = if let Some ( data_dict) = data {
809+ native. borrow ( ) . register_data_dict ( py, data_dict) ?
810+ } else {
811+ vec ! [ ]
812+ } ;
813+
814+ // Execute (capture result for cleanup)
815+ let result = native. borrow ( ) . inner . execute ( query)
816+ . map ( |s| PySpec { inner : s } )
817+ . map_err ( ggsql_err_to_py) ;
818+
819+ // Cleanup: unregister temporary tables (even on error)
820+ for name in & registered_names {
821+ let _ = native. borrow ( ) . inner . unregister ( name) ;
822+ }
823+
824+ return result;
825+ }
762826
763827 // Bridge path: wrap Python object as Reader
828+ // Register DataFrames if provided
829+ let registered_names = if let Some ( data_dict) = data {
830+ register_data_on_reader ( py, reader, data_dict) ?
831+ } else {
832+ vec ! [ ]
833+ } ;
834+
764835 let bridge = PyReaderBridge {
765836 obj : reader. clone ( ) . unbind ( ) ,
766837 } ;
767- bridge
838+ let result = bridge
768839 . execute ( query)
769840 . map ( |s| PySpec { inner : s } )
770- . map_err ( ggsql_err_to_py)
841+ . map_err ( ggsql_err_to_py) ;
842+
843+ // Cleanup for bridge path
844+ for name in & registered_names {
845+ let _ = call_unregister ( py, reader, name) ;
846+ }
847+
848+ result
849+ }
850+
851+ /// Register DataFrames from a Python dict onto a Python reader object.
852+ /// Returns list of registered names for cleanup.
853+ fn register_data_on_reader (
854+ py : Python < ' _ > ,
855+ reader : & Bound < ' _ , PyAny > ,
856+ data : & Bound < ' _ , PyDict > ,
857+ ) -> PyResult < Vec < String > > {
858+ let mut names = Vec :: new ( ) ;
859+ for ( key, value) in data. iter ( ) {
860+ let name: String = key. extract ( ) ?;
861+ let df = py_to_polars ( py, & value) ?;
862+ let py_df = polars_to_py ( py, & df) ?;
863+ reader. call_method ( "register" , ( & name, py_df, true ) , None ) ?;
864+ names. push ( name) ;
865+ }
866+ Ok ( names)
867+ }
868+
869+ /// Call unregister on a reader if the method exists.
870+ fn call_unregister ( _py : Python < ' _ > , reader : & Bound < ' _ , PyAny > , name : & str ) -> PyResult < ( ) > {
871+ if reader. hasattr ( "unregister" ) ? {
872+ reader. call_method1 ( "unregister" , ( name, ) ) ?;
873+ }
874+ Ok ( ( ) )
771875}
772876
773877// ============================================================================
0 commit comments