Skip to content

Commit d80a8ca

Browse files
author
Peng Ren
committed
Support view creation with SQL
1 parent 6d4b758 commit d80a8ca

7 files changed

Lines changed: 476 additions & 1 deletion

File tree

pymongosql/executor.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import logging
3+
import re
34
from abc import ABC, abstractmethod
45
from dataclasses import dataclass
56
from typing import Any, Dict, Optional, Sequence, Union
@@ -14,6 +15,7 @@
1415
from .sql.parser import SQLParser
1516
from .sql.query_builder import QueryExecutionPlan
1617
from .sql.update_builder import UpdateExecutionPlan
18+
from .sql.view_builder import ViewExecutionPlan
1719

1820
_logger = logging.getLogger(__name__)
1921

@@ -642,10 +644,124 @@ def execute(
642644
return self._execute_execution_plan(self._execution_plan, connection, parameters)
643645

644646

647+
class ViewExecution(ExecutionStrategy):
648+
"""Execution strategy for view statements (CREATE VIEW, DROP VIEW)."""
649+
650+
_DDL_PATTERN = re.compile(
651+
r"^\s*(CREATE\s+VIEW|DROP\s+VIEW)\b",
652+
re.IGNORECASE,
653+
)
654+
655+
@property
656+
def execution_plan(self) -> ViewExecutionPlan:
657+
return self._execution_plan
658+
659+
def supports(self, context: ExecutionContext) -> bool:
660+
return bool(self._DDL_PATTERN.match(context.query))
661+
662+
def _parse_sql(self, sql: str) -> ViewExecutionPlan:
663+
normalized = " ".join(sql.split())
664+
665+
# CREATE VIEW view_name ON collection_name AS 'pipeline_json'
666+
create_match = re.match(
667+
r"CREATE\s+VIEW\s+(\w+)\s+ON\s+(\w+)\s+AS\s+'(.*)'",
668+
normalized,
669+
re.IGNORECASE | re.DOTALL,
670+
)
671+
if create_match:
672+
import json
673+
674+
view_name = create_match.group(1)
675+
source_collection = create_match.group(2)
676+
pipeline_str = create_match.group(3)
677+
try:
678+
pipeline = json.loads(pipeline_str)
679+
except json.JSONDecodeError as e:
680+
raise SqlSyntaxError(f"Invalid pipeline JSON in CREATE VIEW: {e}")
681+
682+
if not isinstance(pipeline, list):
683+
raise SqlSyntaxError("Pipeline must be a JSON array")
684+
685+
return ViewExecutionPlan(
686+
collection=view_name,
687+
ddl_type="create_view",
688+
view_on=source_collection,
689+
pipeline=pipeline,
690+
)
691+
692+
# DROP VIEW view_name
693+
drop_match = re.match(
694+
r"DROP\s+VIEW\s+(\w+)\s*$",
695+
normalized,
696+
re.IGNORECASE,
697+
)
698+
if drop_match:
699+
view_name = drop_match.group(1)
700+
return ViewExecutionPlan(
701+
collection=view_name,
702+
ddl_type="drop_view",
703+
)
704+
705+
raise SqlSyntaxError(f"Unsupported DDL statement: {sql}")
706+
707+
def _execute_execution_plan(
708+
self,
709+
execution_plan: ViewExecutionPlan,
710+
connection: Any = None,
711+
parameters: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
712+
) -> Optional[Dict[str, Any]]:
713+
try:
714+
if not connection:
715+
raise OperationalError("No connection provided")
716+
717+
db = connection.database
718+
719+
if execution_plan.ddl_type == "create_view":
720+
command = {
721+
"create": execution_plan.collection,
722+
"viewOn": execution_plan.view_on,
723+
"pipeline": execution_plan.pipeline,
724+
}
725+
_logger.debug(f"Executing MongoDB create view command: {command}")
726+
return _run_db_command(db, command, connection, "create view")
727+
728+
elif execution_plan.ddl_type == "drop_view":
729+
# MongoDB drops views with the regular drop command
730+
command = {"drop": execution_plan.collection}
731+
_logger.debug(f"Executing MongoDB drop view command: {command}")
732+
return _run_db_command(db, command, connection, "drop view")
733+
734+
else:
735+
raise ProgrammingError(f"Unknown DDL type: {execution_plan.ddl_type}")
736+
737+
except PyMongoError as e:
738+
_logger.error(f"MongoDB DDL execution failed: {e}")
739+
raise DatabaseError(f"DDL execution failed: {e}")
740+
except (ProgrammingError, DatabaseError, OperationalError):
741+
raise
742+
except Exception as e:
743+
_logger.error(f"Unexpected error during DDL execution: {e}")
744+
raise OperationalError(f"DDL execution error: {e}")
745+
746+
def execute(
747+
self,
748+
context: ExecutionContext,
749+
connection: Any,
750+
parameters: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
751+
) -> Optional[Dict[str, Any]]:
752+
_logger.debug(f"Using DDL execution for query: {context.query[:100]}")
753+
self._execution_plan = self._parse_sql(context.query)
754+
755+
if not self._execution_plan.validate():
756+
raise SqlSyntaxError("Generated DDL plan is invalid")
757+
758+
return self._execute_execution_plan(self._execution_plan, connection, parameters)
759+
760+
645761
class ExecutionPlanFactory:
646762
"""Factory for creating appropriate execution strategy based on query context"""
647763

648-
_strategies = [StandardQueryExecution(), InsertExecution(), UpdateExecution(), DeleteExecution()]
764+
_strategies = [ViewExecution(), StandardQueryExecution(), InsertExecution(), UpdateExecution(), DeleteExecution()]
649765

650766
@classmethod
651767
def get_strategy(cls, context: ExecutionContext) -> ExecutionStrategy:

pymongosql/sql/partiql/PartiQLParser.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,13 @@ ddl
8686
createCommand
8787
: CREATE TABLE qualifiedName ( PAREN_LEFT tableDef PAREN_RIGHT )? # CreateTable
8888
| CREATE INDEX ON symbolPrimitive PAREN_LEFT pathSimple ( COMMA pathSimple )* PAREN_RIGHT # CreateIndex
89+
| CREATE VIEW symbolPrimitive ON symbolPrimitive AS LITERAL_STRING # CreateView
8990
;
9091

9192
dropCommand
9293
: DROP TABLE qualifiedName # DropTable
9394
| DROP INDEX target=symbolPrimitive ON on=symbolPrimitive # DropIndex
95+
| DROP VIEW symbolPrimitive # DropView
9496
;
9597

9698
tableDef

pymongosql/sql/partiql/PartiQLParser.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,6 +2542,43 @@ def accept(self, visitor:ParseTreeVisitor):
25422542
return visitor.visitChildren(self)
25432543

25442544

2545+
class CreateViewContext(CreateCommandContext):
2546+
2547+
def __init__(self, parser, ctx:ParserRuleContext): # actually a PartiQLParser.CreateCommandContext
2548+
super().__init__(parser)
2549+
self.copyFrom(ctx)
2550+
2551+
def CREATE(self):
2552+
return self.getToken(PartiQLParser.CREATE, 0)
2553+
def VIEW(self):
2554+
return self.getToken(PartiQLParser.VIEW, 0)
2555+
def ON(self):
2556+
return self.getToken(PartiQLParser.ON, 0)
2557+
def AS(self):
2558+
return self.getToken(PartiQLParser.AS, 0)
2559+
def symbolPrimitive(self, i:int=None):
2560+
if i is None:
2561+
return self.getTypedRuleContexts(PartiQLParser.SymbolPrimitiveContext)
2562+
else:
2563+
return self.getTypedRuleContext(PartiQLParser.SymbolPrimitiveContext,i)
2564+
2565+
def LITERAL_STRING(self):
2566+
return self.getToken(PartiQLParser.LITERAL_STRING, 0)
2567+
2568+
def enterRule(self, listener:ParseTreeListener):
2569+
if hasattr( listener, "enterCreateView" ):
2570+
listener.enterCreateView(self)
2571+
2572+
def exitRule(self, listener:ParseTreeListener):
2573+
if hasattr( listener, "exitCreateView" ):
2574+
listener.exitCreateView(self)
2575+
2576+
def accept(self, visitor:ParseTreeVisitor):
2577+
if hasattr( visitor, "visitCreateView" ):
2578+
return visitor.visitCreateView(self)
2579+
else:
2580+
return visitor.visitChildren(self)
2581+
25452582

25462583
def createCommand(self):
25472584

@@ -2698,6 +2735,34 @@ def accept(self, visitor:ParseTreeVisitor):
26982735
return visitor.visitChildren(self)
26992736

27002737

2738+
class DropViewContext(DropCommandContext):
2739+
2740+
def __init__(self, parser, ctx:ParserRuleContext): # actually a PartiQLParser.DropCommandContext
2741+
super().__init__(parser)
2742+
self.copyFrom(ctx)
2743+
2744+
def DROP(self):
2745+
return self.getToken(PartiQLParser.DROP, 0)
2746+
def VIEW(self):
2747+
return self.getToken(PartiQLParser.VIEW, 0)
2748+
def symbolPrimitive(self):
2749+
return self.getTypedRuleContext(PartiQLParser.SymbolPrimitiveContext,0)
2750+
2751+
def enterRule(self, listener:ParseTreeListener):
2752+
if hasattr( listener, "enterDropView" ):
2753+
listener.enterDropView(self)
2754+
2755+
def exitRule(self, listener:ParseTreeListener):
2756+
if hasattr( listener, "exitDropView" ):
2757+
listener.exitDropView(self)
2758+
2759+
def accept(self, visitor:ParseTreeVisitor):
2760+
if hasattr( visitor, "visitDropView" ):
2761+
return visitor.visitDropView(self)
2762+
else:
2763+
return visitor.visitChildren(self)
2764+
2765+
27012766

27022767
def dropCommand(self):
27032768

pymongosql/sql/partiql/PartiQLParserListener.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ def exitCreateIndex(self, ctx:PartiQLParser.CreateIndexContext):
188188
pass
189189

190190

191+
# Enter a parse tree produced by PartiQLParser#CreateView.
192+
def enterCreateView(self, ctx:PartiQLParser.CreateViewContext):
193+
pass
194+
195+
# Exit a parse tree produced by PartiQLParser#CreateView.
196+
def exitCreateView(self, ctx:PartiQLParser.CreateViewContext):
197+
pass
198+
199+
191200
# Enter a parse tree produced by PartiQLParser#DropTable.
192201
def enterDropTable(self, ctx:PartiQLParser.DropTableContext):
193202
pass
@@ -206,6 +215,15 @@ def exitDropIndex(self, ctx:PartiQLParser.DropIndexContext):
206215
pass
207216

208217

218+
# Enter a parse tree produced by PartiQLParser#DropView.
219+
def enterDropView(self, ctx:PartiQLParser.DropViewContext):
220+
pass
221+
222+
# Exit a parse tree produced by PartiQLParser#DropView.
223+
def exitDropView(self, ctx:PartiQLParser.DropViewContext):
224+
pass
225+
226+
209227
# Enter a parse tree produced by PartiQLParser#tableDef.
210228
def enterTableDef(self, ctx:PartiQLParser.TableDefContext):
211229
pass

pymongosql/sql/partiql/PartiQLParserVisitor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ def visitCreateIndex(self, ctx:PartiQLParser.CreateIndexContext):
109109
return self.visitChildren(ctx)
110110

111111

112+
# Visit a parse tree produced by PartiQLParser#CreateView.
113+
def visitCreateView(self, ctx:PartiQLParser.CreateViewContext):
114+
return self.visitChildren(ctx)
115+
116+
112117
# Visit a parse tree produced by PartiQLParser#DropTable.
113118
def visitDropTable(self, ctx:PartiQLParser.DropTableContext):
114119
return self.visitChildren(ctx)
@@ -119,6 +124,11 @@ def visitDropIndex(self, ctx:PartiQLParser.DropIndexContext):
119124
return self.visitChildren(ctx)
120125

121126

127+
# Visit a parse tree produced by PartiQLParser#DropView.
128+
def visitDropView(self, ctx:PartiQLParser.DropViewContext):
129+
return self.visitChildren(ctx)
130+
131+
122132
# Visit a parse tree produced by PartiQLParser#tableDef.
123133
def visitTableDef(self, ctx:PartiQLParser.TableDefContext):
124134
return self.visitChildren(ctx)

pymongosql/sql/view_builder.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: utf-8 -*-
2+
import logging
3+
from dataclasses import dataclass, field
4+
from typing import Any, Dict, List, Optional
5+
6+
from .builder import ExecutionPlan
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
@dataclass
12+
class ViewExecutionPlan(ExecutionPlan):
13+
"""Execution plan for view statements (CREATE VIEW, DROP VIEW)."""
14+
15+
ddl_type: str = "" # "create_view" or "drop_view"
16+
view_on: Optional[str] = None # Source collection for CREATE VIEW
17+
pipeline: Optional[List[Dict[str, Any]]] = field(default_factory=list)
18+
19+
def to_dict(self) -> Dict[str, Any]:
20+
result = {
21+
"ddl_type": self.ddl_type,
22+
"collection": self.collection,
23+
}
24+
if self.ddl_type == "create_view":
25+
result["view_on"] = self.view_on
26+
result["pipeline"] = self.pipeline
27+
return result
28+
29+
def validate(self) -> bool:
30+
errors = self.validate_base()
31+
32+
if not self.ddl_type:
33+
errors.append("DDL type is required")
34+
35+
if self.ddl_type == "create_view":
36+
if not self.view_on:
37+
errors.append("Source collection (ON) is required for CREATE VIEW")
38+
if self.pipeline is None:
39+
errors.append("Pipeline (AS) is required for CREATE VIEW")
40+
41+
if errors:
42+
for err in errors:
43+
_logger.warning(f"View plan validation error: {err}")
44+
return False
45+
46+
return True

0 commit comments

Comments
 (0)