@@ -19,10 +19,15 @@ class DuckDBStore(BaseContextManagerStore, BaseStore):
1919 DuckDB is an in-process SQL OLAP database that provides excellent performance
2020 for analytical workloads while supporting standard SQL operations. This store
2121 can operate in memory-only mode or persist data to disk.
22+
23+ Note on connection ownership: When you provide an existing connection, the store
24+ will take ownership and close it when the store is closed or garbage collected.
25+ If you need to reuse a connection, create separate DuckDB connections for each store.
2226 """
2327
2428 _connection : duckdb .DuckDBPyConnection
2529 _is_closed : bool
30+ _owns_connection : bool
2631
2732 @overload
2833 def __init__ (
@@ -34,6 +39,10 @@ def __init__(
3439 ) -> None :
3540 """Initialize the DuckDB store with an existing connection.
3641
42+ Warning: The store will take ownership of the connection and close it
43+ when the store is closed or garbage collected. If you need to reuse
44+ a connection, create separate DuckDB connections for each store.
45+
3746 Args:
3847 connection: An existing DuckDB connection to use.
3948 default_collection: The default collection to use if no collection is provided.
@@ -78,6 +87,7 @@ def __init__(
7887
7988 if connection is not None :
8089 self ._connection = connection
90+ self ._owns_connection = True # We take ownership even of provided connections
8191 else :
8292 # Convert Path to string if needed
8393 if isinstance (database_path , Path ):
@@ -88,6 +98,7 @@ def __init__(
8898 self ._connection = duckdb .connect (":memory:" )
8999 else :
90100 self ._connection = duckdb .connect (database = database_path )
101+ self ._owns_connection = True
91102
92103 self ._is_closed = False
93104 self ._stable_api = False
@@ -96,7 +107,18 @@ def __init__(
96107
97108 @override
98109 async def _setup (self ) -> None :
99- """Initialize the database schema for key-value storage."""
110+ """Initialize the database schema for key-value storage.
111+
112+ Note: The schema stores created_at, ttl, and expires_at as separate columns
113+ in addition to the serialized ManagedEntry in value_json. This duplication
114+ is intentional for future features:
115+ - The expires_at column with its index enables efficient expiration-based
116+ cleanup queries (e.g., DELETE FROM kv_entries WHERE expires_at < now())
117+ - The separate columns allow for metadata queries without deserializing JSON
118+ - Currently, only value_json is read during _get_managed_entry
119+
120+ This design trades storage space for query flexibility and future extensibility.
121+ """
100122 # Create the main table for storing key-value entries
101123 self ._connection .execute ("""
102124 CREATE TABLE IF NOT EXISTS kv_entries (
@@ -116,7 +138,7 @@ async def _setup(self) -> None:
116138 ON kv_entries(collection)
117139 """ )
118140
119- # Create index for expiration-based queries
141+ # Create index for expiration-based queries (for future cleanup features)
120142 self ._connection .execute ("""
121143 CREATE INDEX IF NOT EXISTS idx_kv_expires_at
122144 ON kv_entries(expires_at)
@@ -125,6 +147,10 @@ async def _setup(self) -> None:
125147 @override
126148 async def _get_managed_entry (self , * , key : str , collection : str ) -> ManagedEntry | None :
127149 """Retrieve a managed entry by key from the specified collection."""
150+ if self ._is_closed :
151+ msg = "Cannot operate on closed DuckDBStore"
152+ raise RuntimeError (msg )
153+
128154 result = self ._connection .execute (
129155 "SELECT value_json FROM kv_entries WHERE collection = ? AND key = ?" ,
130156 [collection , key ],
@@ -145,6 +171,10 @@ async def _put_managed_entry(
145171 managed_entry : ManagedEntry ,
146172 ) -> None :
147173 """Store a managed entry by key in the specified collection."""
174+ if self ._is_closed :
175+ msg = "Cannot operate on closed DuckDBStore"
176+ raise RuntimeError (msg )
177+
148178 # Insert or replace the entry
149179 self ._connection .execute (
150180 """
@@ -165,6 +195,10 @@ async def _put_managed_entry(
165195 @override
166196 async def _delete_managed_entry (self , * , key : str , collection : str ) -> bool :
167197 """Delete a managed entry by key from the specified collection."""
198+ if self ._is_closed :
199+ msg = "Cannot operate on closed DuckDBStore"
200+ raise RuntimeError (msg )
201+
168202 result = self ._connection .execute (
169203 "DELETE FROM kv_entries WHERE collection = ? AND key = ? RETURNING key" ,
170204 [collection , key ],
@@ -177,12 +211,16 @@ async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:
177211 @override
178212 async def _close (self ) -> None :
179213 """Close the DuckDB connection."""
180- if not self ._is_closed :
214+ if not self ._is_closed and self . _owns_connection :
181215 self ._connection .close ()
182216 self ._is_closed = True
183217
184218 def __del__ (self ) -> None :
185219 """Clean up the DuckDB connection on deletion."""
186- if not self ._is_closed :
187- self ._connection .close ()
188- self ._is_closed = True
220+ try :
221+ if not self ._is_closed and self ._owns_connection and hasattr (self , "_connection" ):
222+ self ._connection .close ()
223+ self ._is_closed = True
224+ except Exception : # noqa: S110
225+ # Suppress errors during cleanup to avoid issues during interpreter shutdown
226+ pass
0 commit comments