Skip to content

Commit d0b585b

Browse files
committed
Add unregister method
1 parent b588a4e commit d0b585b

6 files changed

Lines changed: 146 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ let json = writer.render(&spec)?;
209209

210210
- `execute_sql(sql)` - Run SQL, return DataFrame
211211
- `register(name, df)` - Register DataFrame as table
212+
- `unregister(name)` - Unregister a previously registered table
212213
- Implementation: `DuckDBReader`
213214

214215
**Writer trait** (output format abstraction):
@@ -1000,6 +1001,7 @@ Optional methods for custom readers:
10001001

10011002
- `supports_register() -> bool` - Return `True` if registration is supported
10021003
- `register(name: str, df: polars.DataFrame) -> None` - Register a DataFrame as a table
1004+
- `unregister(name: str) -> None` - Unregister a previously registered table
10031005

10041006
Native readers (e.g., `DuckDBReader`) use an optimized fast path, while custom Python readers are automatically bridged via IPC serialization.
10051007

ggsql-python/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ reader = ggsql.DuckDBReader("duckdb:///path/to/file.db") # File database
126126
**Methods:**
127127

128128
- `register(name: str, df: polars.DataFrame)` - Register a DataFrame as a queryable table
129+
- `unregister(name: str)` - Unregister a previously registered table
129130
- `execute_sql(sql: str) -> polars.DataFrame` - Execute SQL and return results
130131
- `supports_register() -> bool` - Check if registration is supported
131132

@@ -266,6 +267,7 @@ json_output = writer.render(spec)
266267

267268
- `supports_register() -> bool` - Return `True` if your reader supports DataFrame registration
268269
- `register(name: str, df: polars.DataFrame) -> None` - Register a DataFrame as a queryable table
270+
- `unregister(name: str) -> None` - Unregister a previously registered table
269271

270272
```python
271273
class AdvancedReader:
@@ -283,6 +285,9 @@ class AdvancedReader:
283285

284286
def register(self, name: str, df: pl.DataFrame) -> None:
285287
self.tables[name] = df
288+
289+
def unregister(self, name: str) -> None:
290+
del self.tables[name]
286291
```
287292

288293
Native readers like `DuckDBReader` use an optimized fast path, while custom Python readers are automatically bridged via IPC serialization.

ggsql-python/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ impl Reader for PyReaderBridge {
159159
Ok(())
160160
})
161161
}
162+
163+
fn unregister(&mut self, name: &str) -> ggsql::Result<()> {
164+
Python::attach(|py| {
165+
self.obj
166+
.bind(py)
167+
.call_method1("unregister", (name,))
168+
.map_err(|e| {
169+
GgsqlError::ReaderError(format!("Reader.unregister() failed: {}", e))
170+
})?;
171+
Ok(())
172+
})
173+
}
162174
}
163175

164176
// ============================================================================
@@ -249,6 +261,23 @@ impl PyDuckDBReader {
249261
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
250262
}
251263

264+
/// Unregister a previously registered table.
265+
///
266+
/// Parameters
267+
/// ----------
268+
/// name : str
269+
/// The table name to unregister.
270+
///
271+
/// Raises
272+
/// ------
273+
/// ValueError
274+
/// If the table wasn't registered via this reader or unregistration fails.
275+
fn unregister(&mut self, name: &str) -> PyResult<()> {
276+
self.inner
277+
.unregister(name)
278+
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
279+
}
280+
252281
/// Execute a SQL query and return the result as a DataFrame.
253282
///
254283
/// Parameters

src/doc/API.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ pub trait Reader {
377377
/// Register a DataFrame as a queryable table
378378
fn register(&mut self, name: &str, df: DataFrame) -> Result<()>;
379379

380+
/// Unregister a previously registered table
381+
fn unregister(&mut self, name: &str) -> Result<()>;
382+
380383
/// Check if this reader supports DataFrame registration
381384
fn supports_register(&self) -> bool;
382385
}
@@ -423,6 +426,13 @@ class DuckDBReader:
423426
df: Polars DataFrame or narwhals-compatible DataFrame
424427
"""
425428

429+
def unregister(self, name: str) -> None:
430+
"""Unregister a previously registered table.
431+
432+
Args:
433+
name: Table name to unregister
434+
"""
435+
426436
def execute_sql(self, sql: str) -> polars.DataFrame:
427437
"""Execute SQL and return a Polars DataFrame."""
428438

src/reader/duckdb.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use duckdb::vtab::arrow::{arrow_recordbatch_to_query_params, ArrowVTab};
1010
use duckdb::{params, Connection};
1111
use polars::io::SerWriter;
1212
use polars::prelude::*;
13+
use std::collections::HashSet;
1314
use std::io::Cursor;
1415

1516
/// DuckDB database reader
@@ -32,6 +33,7 @@ use std::io::Cursor;
3233
/// ```
3334
pub struct DuckDBReader {
3435
conn: Connection,
36+
registered_tables: HashSet<String>,
3537
}
3638

3739
impl DuckDBReader {
@@ -75,7 +77,10 @@ impl DuckDBReader {
7577
GgsqlError::ReaderError(format!("Failed to register arrow function: {}", e))
7678
})?;
7779

78-
Ok(Self { conn })
80+
Ok(Self {
81+
conn,
82+
registered_tables: HashSet::new(),
83+
})
7984
}
8085

8186
/// Get a reference to the underlying DuckDB connection
@@ -523,6 +528,30 @@ impl Reader for DuckDBReader {
523528
GgsqlError::ReaderError(format!("Failed to register table '{}': {}", name, e))
524529
})?;
525530

531+
// Track the table so we can unregister it later
532+
self.registered_tables.insert(name.to_string());
533+
534+
Ok(())
535+
}
536+
537+
fn unregister(&mut self, name: &str) -> Result<()> {
538+
// Only allow unregistering tables we created via register()
539+
if !self.registered_tables.contains(name) {
540+
return Err(GgsqlError::ReaderError(format!(
541+
"Table '{}' was not registered via this reader",
542+
name
543+
)));
544+
}
545+
546+
// Drop the temp table
547+
let sql = format!("DROP TABLE IF EXISTS \"{}\"", name);
548+
self.conn.execute(&sql, []).map_err(|e| {
549+
GgsqlError::ReaderError(format!("Failed to unregister table '{}': {}", name, e))
550+
})?;
551+
552+
// Remove from tracking
553+
self.registered_tables.remove(name);
554+
526555
Ok(())
527556
}
528557

@@ -704,4 +733,54 @@ mod tests {
704733
assert_eq!(result.shape(), (0, 2));
705734
assert_eq!(result.get_column_names(), vec!["x", "y"]);
706735
}
736+
737+
#[test]
738+
fn test_unregister() {
739+
let mut reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap();
740+
let df = DataFrame::new(vec![Column::new("x".into(), vec![1i32, 2, 3])]).unwrap();
741+
742+
reader.register("test_data", df).unwrap();
743+
744+
// Should be queryable
745+
let result = reader.execute_sql("SELECT * FROM test_data").unwrap();
746+
assert_eq!(result.height(), 3);
747+
748+
// Unregister
749+
reader.unregister("test_data").unwrap();
750+
751+
// Should no longer exist
752+
let result = reader.execute_sql("SELECT * FROM test_data");
753+
assert!(result.is_err());
754+
}
755+
756+
#[test]
757+
fn test_unregister_not_registered() {
758+
let mut reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap();
759+
760+
// Create a table directly (not via register)
761+
reader
762+
.connection()
763+
.execute("CREATE TABLE user_table (x INT)", params![])
764+
.unwrap();
765+
766+
// Should fail - we didn't register this via register()
767+
let result = reader.unregister("user_table");
768+
assert!(result.is_err());
769+
let err = result.unwrap_err().to_string();
770+
assert!(err.contains("was not registered via this reader"));
771+
}
772+
773+
#[test]
774+
fn test_reregister_after_unregister() {
775+
let mut reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap();
776+
let df = DataFrame::new(vec![Column::new("x".into(), vec![1i32, 2, 3])]).unwrap();
777+
778+
reader.register("data", df.clone()).unwrap();
779+
reader.unregister("data").unwrap();
780+
781+
// Should be able to register again
782+
reader.register("data", df).unwrap();
783+
let result = reader.execute_sql("SELECT * FROM data").unwrap();
784+
assert_eq!(result.height(), 3);
785+
}
707786
}

src/reader/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ pub trait Reader {
147147
)))
148148
}
149149

150+
/// Unregister a previously registered table
151+
///
152+
/// # Arguments
153+
///
154+
/// * `name` - The table name to unregister
155+
///
156+
/// # Returns
157+
///
158+
/// `Ok(())` on success.
159+
///
160+
/// # Default Implementation
161+
///
162+
/// Returns an error by default. Override for readers that support registration.
163+
fn unregister(&mut self, name: &str) -> Result<()> {
164+
Err(GgsqlError::ReaderError(format!(
165+
"This reader does not support unregistering table '{}'",
166+
name
167+
)))
168+
}
169+
150170
/// Check if this reader supports DataFrame registration
151171
///
152172
/// # Returns

0 commit comments

Comments
 (0)