Skip to content

Commit 4b18840

Browse files
author
Peng Ren
committed
Added delete support
1 parent e1bdce9 commit 4b18840

10 files changed

Lines changed: 766 additions & 10 deletions

File tree

pymongosql/executor.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,90 @@ def execute(
260260
return self._execute_execution_plan(self._execution_plan, connection.database, parameters)
261261

262262

263+
class DeleteExecution(ExecutionStrategy):
264+
"""Strategy for executing DELETE statements."""
265+
266+
def __init__(self) -> None:
267+
"""Initialize DELETE execution strategy."""
268+
self._execution_plan: Optional[Any] = None
269+
270+
@property
271+
def execution_plan(self) -> Any:
272+
return self._execution_plan
273+
274+
def supports(self, context: ExecutionContext) -> bool:
275+
return context.query.lstrip().upper().startswith("DELETE")
276+
277+
def _parse_sql(self, sql: str) -> Any:
278+
from .sql.delete_builder import DeleteExecutionPlan
279+
280+
try:
281+
parser = SQLParser(sql)
282+
plan = parser.get_execution_plan()
283+
284+
if not isinstance(plan, DeleteExecutionPlan):
285+
raise SqlSyntaxError("Expected DELETE execution plan")
286+
287+
if not plan.validate():
288+
raise SqlSyntaxError("Generated delete plan is invalid")
289+
290+
return plan
291+
except SqlSyntaxError:
292+
raise
293+
except Exception as e:
294+
_logger.error(f"SQL parsing failed: {e}")
295+
raise SqlSyntaxError(f"Failed to parse SQL: {e}")
296+
297+
def _execute_execution_plan(
298+
self,
299+
execution_plan: Any,
300+
db: Any,
301+
parameters: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
302+
) -> Optional[Dict[str, Any]]:
303+
try:
304+
if not execution_plan.collection:
305+
raise ProgrammingError("No collection specified in delete")
306+
307+
filter_conditions = execution_plan.filter_conditions or {}
308+
309+
# Replace placeholders in filter if parameters provided
310+
if parameters and filter_conditions:
311+
filter_conditions = SQLHelper.replace_placeholders_generic(
312+
filter_conditions, parameters, execution_plan.parameter_style
313+
)
314+
315+
command = {"delete": execution_plan.collection, "deletes": [{"q": filter_conditions, "limit": 0}]}
316+
317+
_logger.debug(f"Executing MongoDB delete command: {command}")
318+
319+
return db.command(command)
320+
except PyMongoError as e:
321+
_logger.error(f"MongoDB delete failed: {e}")
322+
raise DatabaseError(f"Delete execution failed: {e}")
323+
except (ProgrammingError, DatabaseError, OperationalError):
324+
# Re-raise our own errors without wrapping
325+
raise
326+
except Exception as e:
327+
_logger.error(f"Unexpected error during delete execution: {e}")
328+
raise OperationalError(f"Delete execution error: {e}")
329+
330+
def execute(
331+
self,
332+
context: ExecutionContext,
333+
connection: Any,
334+
parameters: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
335+
) -> Optional[Dict[str, Any]]:
336+
_logger.debug(f"Using delete execution for query: {context.query[:100]}")
337+
338+
self._execution_plan = self._parse_sql(context.query)
339+
340+
return self._execute_execution_plan(self._execution_plan, connection.database, parameters)
341+
342+
263343
class ExecutionPlanFactory:
264344
"""Factory for creating appropriate execution strategy based on query context"""
265345

266-
_strategies = [InsertExecution(), StandardQueryExecution()]
346+
_strategies = [DeleteExecution(), InsertExecution(), StandardQueryExecution()]
267347

268348
@classmethod
269349
def get_strategy(cls, context: ExecutionContext) -> ExecutionStrategy:

pymongosql/sql/ast.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from ..error import SqlSyntaxError
66
from .builder import BuilderFactory
7+
from .delete_builder import DeleteExecutionPlan
8+
from .delete_handler import DeleteParseResult
79
from .handler import BaseHandler, HandlerFactory
810
from .insert_builder import InsertExecutionPlan
911
from .insert_handler import InsertParseResult
@@ -34,7 +36,8 @@ class MongoSQLParserVisitor(PartiQLParserVisitor):
3436
def __init__(self) -> None:
3537
super().__init__()
3638
self._parse_result = QueryParseResult.for_visitor()
37-
self._insert_parse_result = InsertParseResult.for_insert_visitor()
39+
self._insert_parse_result = InsertParseResult.for_visitor()
40+
self._delete_parse_result = DeleteParseResult.for_visitor()
3841
# Track current statement kind generically so UPDATE/DELETE can reuse this
3942
self._current_operation: str = "select" # expected values: select | insert | update | delete
4043
self._handlers = self._initialize_handlers()
@@ -47,17 +50,20 @@ def _initialize_handlers(self) -> Dict[str, BaseHandler]:
4750
"from": HandlerFactory.get_visitor_handler("from"),
4851
"where": HandlerFactory.get_visitor_handler("where"),
4952
"insert": HandlerFactory.get_visitor_handler("insert"),
53+
"delete": HandlerFactory.get_visitor_handler("delete"),
5054
}
5155

5256
@property
5357
def parse_result(self) -> QueryParseResult:
5458
"""Get the current parse result"""
5559
return self._parse_result
5660

57-
def parse_to_execution_plan(self) -> Union[QueryExecutionPlan, InsertExecutionPlan]:
61+
def parse_to_execution_plan(self) -> Union[QueryExecutionPlan, InsertExecutionPlan, DeleteExecutionPlan]:
5862
"""Convert the parse result to an execution plan using BuilderFactory."""
5963
if self._current_operation == "insert":
6064
return self._build_insert_plan()
65+
elif self._current_operation == "delete":
66+
return self._build_delete_plan()
6167

6268
return self._build_query_plan()
6369

@@ -91,6 +97,19 @@ def _build_insert_plan(self) -> InsertExecutionPlan:
9197

9298
return builder.build()
9399

100+
def _build_delete_plan(self) -> DeleteExecutionPlan:
101+
"""Build a DELETE execution plan from DELETE parsing."""
102+
_logger.debug(
103+
f"Building DELETE plan with collection: {self._delete_parse_result.collection}, "
104+
f"filters: {self._delete_parse_result.filter_conditions}"
105+
)
106+
builder = BuilderFactory.create_delete_builder().collection(self._delete_parse_result.collection)
107+
108+
if self._delete_parse_result.filter_conditions:
109+
builder.filter_conditions(self._delete_parse_result.filter_conditions)
110+
111+
return builder.build()
112+
94113
def visitRoot(self, ctx: PartiQLParser.RootContext) -> Any:
95114
"""Visit root node and process child nodes"""
96115
_logger.debug("Starting to parse SQL query")
@@ -167,6 +186,57 @@ def visitInsertStatementLegacy(self, ctx: PartiQLParser.InsertStatementLegacyCon
167186
return handler.handle_visitor(ctx, self._insert_parse_result)
168187
return self.visitChildren(ctx)
169188

189+
def visitFromClauseSimpleExplicit(self, ctx: PartiQLParser.FromClauseSimpleExplicitContext) -> Any:
190+
"""Handle FROM clause (explicit form) in DELETE statements."""
191+
if self._current_operation == "delete":
192+
handler = self._handlers.get("delete")
193+
if handler:
194+
return handler.handle_from_clause_explicit(ctx, self._delete_parse_result)
195+
return self.visitChildren(ctx)
196+
197+
def visitFromClauseSimpleImplicit(self, ctx: PartiQLParser.FromClauseSimpleImplicitContext) -> Any:
198+
"""Handle FROM clause (implicit form) in DELETE statements."""
199+
if self._current_operation == "delete":
200+
handler = self._handlers.get("delete")
201+
if handler:
202+
return handler.handle_from_clause_implicit(ctx, self._delete_parse_result)
203+
return self.visitChildren(ctx)
204+
205+
def visitWhereClause(self, ctx: PartiQLParser.WhereClauseContext) -> Any:
206+
"""Handle WHERE clause (generic form used in DELETE, UPDATE)."""
207+
_logger.debug("Processing WHERE clause (generic)")
208+
try:
209+
# For DELETE, use the delete handler
210+
if self._current_operation == "delete":
211+
handler = self._handlers.get("delete")
212+
if handler:
213+
return handler.handle_where_clause(ctx, self._delete_parse_result)
214+
return {}
215+
else:
216+
# For other operations, use the where handler
217+
handler = self._handlers["where"]
218+
if handler:
219+
result = handler.handle_visitor(ctx, self._parse_result)
220+
_logger.debug(f"Extracted filter conditions: {result}")
221+
return result
222+
return {}
223+
except Exception as e:
224+
_logger.warning(f"Error processing WHERE clause: {e}")
225+
return {}
226+
227+
def visitDeleteCommand(self, ctx: PartiQLParser.DeleteCommandContext) -> Any:
228+
"""Handle DELETE statements."""
229+
_logger.debug("Processing DELETE statement")
230+
self._current_operation = "delete"
231+
# Reset delete parse result for this statement
232+
self._delete_parse_result = DeleteParseResult.for_visitor()
233+
# Use delete handler if available
234+
handler = self._handlers.get("delete")
235+
if handler:
236+
handler.handle_visitor(ctx, self._delete_parse_result)
237+
# Visit children to process FROM and WHERE clauses
238+
return self.visitChildren(ctx)
239+
170240
def visitOrderByClause(self, ctx: PartiQLParser.OrderByClauseContext) -> Any:
171241
"""Handle ORDER BY clause for sorting"""
172242
_logger.debug("Processing ORDER BY clause")

pymongosql/sql/builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def create_insert_builder():
4949

5050
return MongoInsertBuilder()
5151

52+
@staticmethod
53+
def create_delete_builder():
54+
"""Create a builder for DELETE queries"""
55+
# Local import to avoid circular dependency during module import
56+
from .delete_builder import MongoDeleteBuilder
57+
58+
return MongoDeleteBuilder()
59+
5260

5361
__all__ = [
5462
"ExecutionPlan",

pymongosql/sql/delete_builder.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
import logging
3+
from dataclasses import dataclass, field
4+
from typing import Any, Dict
5+
6+
from .builder import ExecutionPlan
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
@dataclass
12+
class DeleteExecutionPlan(ExecutionPlan):
13+
"""Execution plan for DELETE operations against MongoDB."""
14+
15+
filter_conditions: Dict[str, Any] = field(default_factory=dict)
16+
parameter_style: str = field(default="qmark") # Parameter placeholder style: qmark (?) or named (:name)
17+
18+
def to_dict(self) -> Dict[str, Any]:
19+
"""Convert delete plan to dictionary representation."""
20+
return {
21+
"collection": self.collection,
22+
"filter": self.filter_conditions,
23+
}
24+
25+
def validate(self) -> bool:
26+
"""Validate the delete plan."""
27+
errors = self.validate_base()
28+
29+
# Note: filter_conditions can be empty for DELETE FROM <collection> (delete all)
30+
# which is valid, so we don't enforce filter presence
31+
32+
if errors:
33+
_logger.error(f"Delete plan validation errors: {errors}")
34+
return False
35+
36+
return True
37+
38+
def copy(self) -> "DeleteExecutionPlan":
39+
"""Create a copy of this delete plan."""
40+
return DeleteExecutionPlan(
41+
collection=self.collection,
42+
filter_conditions=self.filter_conditions.copy() if self.filter_conditions else {},
43+
)
44+
45+
46+
class MongoDeleteBuilder:
47+
"""Builder for constructing DeleteExecutionPlan objects."""
48+
49+
def __init__(self) -> None:
50+
"""Initialize the delete builder."""
51+
self._plan = DeleteExecutionPlan()
52+
53+
def collection(self, collection: str) -> "MongoDeleteBuilder":
54+
"""Set the collection name."""
55+
self._plan.collection = collection
56+
return self
57+
58+
def filter_conditions(self, conditions: Dict[str, Any]) -> "MongoDeleteBuilder":
59+
"""Set the filter conditions for the delete operation."""
60+
if conditions:
61+
self._plan.filter_conditions = conditions
62+
return self
63+
64+
def build(self) -> DeleteExecutionPlan:
65+
"""Build and return the DeleteExecutionPlan."""
66+
if not self._plan.validate():
67+
raise ValueError("Invalid delete plan")
68+
return self._plan

0 commit comments

Comments
 (0)