Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Explain command support #444

Merged
merged 11 commits into from
Nov 6, 2022
5 changes: 5 additions & 0 deletions eva/binder/statement_binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from eva.parser.alias import Alias
from eva.parser.create_mat_view_statement import CreateMaterializedViewStatement
from eva.parser.drop_statement import DropTableStatement
from eva.parser.explain_statement import ExplainStatement
from eva.parser.load_statement import LoadDataStatement
from eva.parser.select_statement import SelectStatement
from eva.parser.statement import AbstractStatement
Expand Down Expand Up @@ -75,6 +76,10 @@ def _bind_abstract_expr(self, node: AbstractExpression):
for child in node.children:
self.bind(child)

@bind.register(ExplainStatement)
def _bind_explain_statement(self, node: ExplainStatement):
self.bind(node.explainable_stmt)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, bind will create catalog entries. @gaurav274 This can be a consideration to move catalog operations to executors.


@bind.register(SelectStatement)
def _bind_select_statement(self, node: SelectStatement):
self.bind(node.from_table)
Expand Down
41 changes: 41 additions & 0 deletions eva/executor/explain_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# coding=utf-8
# Copyright 2018-2022 EVA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pandas as pd

from eva.executor.abstract_executor import AbstractExecutor
from eva.models.storage.batch import Batch
from eva.planner.abstract_plan import AbstractPlan
from eva.planner.explain_plan import ExplainPlan


class ExplainExecutor(AbstractExecutor):
def __init__(self, node: ExplainPlan):
super().__init__(node)

def validate(self):
pass

def exec(self):
# Traverse optimized physical plan, which is commonly supported.
# Logical plan can be also printted by passing explainable_opr
# attribute of the node, but is not done for now.
plan_str = self._exec(self._node.children[0], 0)
yield Batch(pd.DataFrame([plan_str]))

def _exec(self, node: AbstractPlan, depth: int):
cur_str = " " * depth * 4 + "|__ " + str(node.__class__.__name__) + "\n"
for child in node.children:
cur_str += self._exec(child, depth + 1)
return cur_str
12 changes: 9 additions & 3 deletions eva/executor/plan_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from eva.executor.create_udf_executor import CreateUDFExecutor
from eva.executor.drop_executor import DropExecutor
from eva.executor.drop_udf_executor import DropUDFExecutor
from eva.executor.explain_executor import ExplainExecutor
from eva.executor.function_scan_executor import FunctionScanExecutor
from eva.executor.hash_join_executor import HashJoinExecutor
from eva.executor.insert_executor import InsertExecutor
Expand Down Expand Up @@ -121,9 +122,14 @@ def _build_execution_tree(self, plan: AbstractPlan) -> AbstractExecutor:
executor_node = PredicateExecutor(node=plan)
elif plan_opr_type == PlanOprType.SHOW_INFO:
executor_node = ShowInfoExecutor(node=plan)
# Build Executor Tree for children
for children in plan.children:
executor_node.append_child(self._build_execution_tree(children))
elif plan_opr_type == PlanOprType.EXPLAIN:
executor_node = ExplainExecutor(node=plan)

# EXPLAIN does not need to build execution tree for its children
if plan_opr_type != PlanOprType.EXPLAIN:
# Build Executor Tree for children
for children in plan.children:
executor_node.append_child(self._build_execution_tree(children))

return executor_node

Expand Down
4 changes: 2 additions & 2 deletions eva/models/server/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ def __str__(self):
if self.query_time is not None:
return (
"@status: %s\n"
"@batch: %s\n"
"@batch: \n %s\n"
"@query_time: %s" % (self.status, self.batch, self.query_time)
)
else:
return (
"@status: %s\n"
"@batch: %s\n"
"@batch: \n %s\n"
"@error: %s" % (self.status, self.batch, self.error)
)
11 changes: 4 additions & 7 deletions eva/models/storage/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,10 @@ def compare_is_contained(cls, batch1: Batch, batch2: Batch) -> None:
)

def __str__(self) -> str:
# reduce the nesting depth to accelerate printing ndarray objects
with pd.option_context("display.pprint_nest_depth", 1):
return (
"Batch Object:\n"
"@dataframe: %s\n"
"@batch_size: %d\n" % (self._frames, len(self))
)
with pd.option_context(
"display.pprint_nest_depth", 1, "display.max_colwidth", 100
):
return f"{self._frames}"

def __eq__(self, other: Batch):
return self._frames[sorted(self.columns)].equals(
Expand Down
21 changes: 21 additions & 0 deletions eva/optimizer/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class OperatorType(IntEnum):
LOGICAL_CREATE_MATERIALIZED_VIEW = auto()
LOGICAL_SHOW = auto()
LOGICALDROPUDF = auto()
LOGICALEXPLAIN = auto()
LOGICALDELIMITER = auto()


Expand Down Expand Up @@ -1057,3 +1058,23 @@ def __eq__(self, other):

def __hash__(self) -> int:
return super().__hash__()


class LogicalExplain(Operator):
def __init__(self, children: List = None):
super().__init__(OperatorType.LOGICALEXPLAIN, children)
assert len(children) == 1, "EXPLAIN command only takes one child"
self._explainable_opr = children[0]

@property
def explainable_opr(self):
return self._explainable_opr

def __eq__(self, other):
is_subtree_equal = super().__eq__(other)
if not isinstance(other, LogicalExplain):
return False
return is_subtree_equal and self._explainable_opr == other.explainable_opr

def __hash__(self) -> int:
return hash((super().__hash__(), self._explainable_opr))
21 changes: 21 additions & 0 deletions eva/optimizer/rules/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from eva.optimizer.rules.rules_base import Promise, Rule, RuleType
from eva.parser.types import JoinType
from eva.planner.create_mat_view_plan import CreateMaterializedViewPlan
from eva.planner.explain_plan import ExplainPlan
from eva.planner.hash_join_build_plan import HashJoinBuildPlan
from eva.planner.predicate_plan import PredicatePlan
from eva.planner.project_plan import ProjectPlan
Expand All @@ -42,6 +43,7 @@
LogicalCreateUDF,
LogicalDrop,
LogicalDropUDF,
LogicalExplain,
LogicalFilter,
LogicalFunctionScan,
LogicalGet,
Expand Down Expand Up @@ -807,5 +809,24 @@ def apply(self, before: LogicalShow, context: OptimizerContext):
return after


class LogicalExplainToPhysical(Rule):
def __init__(self):
pattern = Pattern(OperatorType.LOGICALEXPLAIN)
pattern.append_child(Pattern(OperatorType.DUMMY))
super().__init__(RuleType.LOGICAL_EXPLAIN_TO_PHYSICAL, pattern)

def promise(self):
return Promise.LOGICAL_EXPLAIN_TO_PHYSICAL

def check(self, grp_id: int, context: OptimizerContext):
return True

def apply(self, before: LogicalExplain, context: OptimizerContext):
after = ExplainPlan(before.explainable_opr)
for child in before.children:
after.append_child(child)
return after


# IMPLEMENTATION RULES END
##############################################
2 changes: 2 additions & 0 deletions eva/optimizer/rules/rules_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class RuleType(Flag):
LOGICAL_PROJECT_TO_PHYSICAL = auto()
LOGICAL_SHOW_TO_PHYSICAL = auto()
LOGICAL_DROP_UDF_TO_PHYSICAL = auto()
LOGICAL_EXPLAIN_TO_PHYSICAL = auto()
IMPLEMENTATION_DELIMETER = auto()

NUM_RULES = auto()
Expand Down Expand Up @@ -103,6 +104,7 @@ class Promise(IntEnum):
LOGICAL_PROJECT_TO_PHYSICAL = auto()
LOGICAL_SHOW_TO_PHYSICAL = auto()
LOGICAL_DROP_UDF_TO_PHYSICAL = auto()
LOGICAL_EXPLAIN_TO_PHYSICAL = auto()
IMPLEMENTATION_DELIMETER = auto()

# TRANSFORMATION RULES (LOGICAL -> LOGICAL)
Expand Down
2 changes: 2 additions & 0 deletions eva/optimizer/rules/rules_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
LogicalDerivedGetToPhysical,
LogicalDropToPhysical,
LogicalDropUDFToPhysical,
LogicalExplainToPhysical,
LogicalFilterToPhysical,
LogicalFunctionScanToPhysical,
)
Expand Down Expand Up @@ -110,6 +111,7 @@ def __init__(self):
if ray_enabled
else SequentialLogicalProjectToPhysical(),
LogicalShowToPhysical(),
LogicalExplainToPhysical(),
]

if ray_enabled:
Expand Down
8 changes: 8 additions & 0 deletions eva/optimizer/statement_to_opr_convertor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
LogicalCreateUDF,
LogicalDrop,
LogicalDropUDF,
LogicalExplain,
LogicalFilter,
LogicalFunctionScan,
LogicalGet,
Expand All @@ -40,6 +41,7 @@
from eva.parser.create_udf_statement import CreateUDFStatement
from eva.parser.drop_statement import DropTableStatement
from eva.parser.drop_udf_statement import DropUDFStatement
from eva.parser.explain_statement import ExplainStatement
from eva.parser.insert_statement import InsertTableStatement
from eva.parser.load_statement import LoadDataStatement
from eva.parser.rename_statement import RenameTableStatement
Expand Down Expand Up @@ -292,6 +294,10 @@ def visit_show(self, statement: ShowStatement):
show_opr = LogicalShow(statement.show_type)
self._plan = show_opr

def visit_explain(self, statement: ExplainStatement):
explain_opr = LogicalExplain([self.visit(statement.explainable_stmt)])
self._plan = explain_opr

def visit(self, statement: AbstractStatement):
"""Based on the instance of the statement the corresponding
visit is called.
Expand Down Expand Up @@ -322,6 +328,8 @@ def visit(self, statement: AbstractStatement):
self.visit_materialized_view(statement)
elif isinstance(statement, ShowStatement):
self.visit_show(statement)
elif isinstance(statement, ExplainStatement):
self.visit_explain(statement)
return self._plan

@property
Expand Down
11 changes: 10 additions & 1 deletion eva/parser/evaql/evaql_parser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ dmlStatement
;

utilityStatement
: simpleDescribeStatement | helpStatement | showStatement
: simpleDescribeStatement | helpStatement | showStatement | explainStatement
;

explainableStatement
: selectStatement | insertStatement | updateStatement | deleteStatement
| createMaterializedView
;

// Data Definition Language
Expand Down Expand Up @@ -378,6 +383,10 @@ showStatement
: SHOW (UDFS | TABLES)
;

explainStatement
: EXPLAIN explainableStatement
;

// Common Clauses

// DB Objects
Expand Down
38 changes: 38 additions & 0 deletions eva/parser/explain_statement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# coding=utf-8
# Copyright 2018-2022 EVA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from eva.parser.statement import AbstractStatement
from eva.parser.types import StatementType


class ExplainStatement(AbstractStatement):
def __init__(self, explainable_stmt: AbstractStatement):
super().__init__(StatementType.EXPLAIN)
self._explainable_stmt = explainable_stmt

def __str__(self) -> str:
print_str = "EXPLAIN {}".format(str(self._explainable_stmt))
return print_str

@property
def explainable_stmt(self) -> AbstractStatement:
return self._explainable_stmt

def __eq__(self, other):
if not isinstance(other, ExplainStatement):
return False
return self._explainable_stmt == other.explainable_stmt

def __hash__(self) -> int:
return hash((super().__hash__(), self.explainable_stmt))
2 changes: 2 additions & 0 deletions eva/parser/parser_visitor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from eva.parser.parser_visitor._common_clauses_ids import CommonClauses
from eva.parser.parser_visitor._create_statements import CreateTable
from eva.parser.parser_visitor._drop_statement import DropTable
from eva.parser.parser_visitor._explain_statement import Explain
from eva.parser.parser_visitor._expressions import Expressions
from eva.parser.parser_visitor._functions import Functions
from eva.parser.parser_visitor._insert_statements import Insert
Expand Down Expand Up @@ -48,6 +49,7 @@ class ParserVisitor(
RenameTable,
DropTable,
Show,
Explain,
):
def visitRoot(self, ctx: evaql_parser.RootContext):
for child in ctx.children:
Expand Down
23 changes: 23 additions & 0 deletions eva/parser/parser_visitor/_explain_statement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# coding=utf-8
# Copyright 2018-2022 EVA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from eva.parser.evaql.evaql_parser import evaql_parser
from eva.parser.evaql.evaql_parserVisitor import evaql_parserVisitor
from eva.parser.explain_statement import ExplainStatement


class Explain(evaql_parserVisitor):
def visitExplainStatement(self, ctx: evaql_parser.ExplainStatementContext):
explainable_stmt = self.visit(ctx.explainableStatement())
return ExplainStatement(explainable_stmt)
1 change: 1 addition & 0 deletions eva/parser/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class StatementType(Enum):
CREATE_MATERIALIZED_VIEW = (auto(),)
SHOW = (auto(),)
DROP_UDF = auto()
EXPLAIN = (auto(),)
# add other types


Expand Down
4 changes: 4 additions & 0 deletions eva/planner/abstract_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def is_logical(self):
def __hash__(self) -> int:
return hash(self.opr_type)

@abstractmethod
def __str__(self) -> str:
return "AbstractPlan"

def __copy__(self):
# deepcopy the children
cls = self.__class__
Expand Down
7 changes: 7 additions & 0 deletions eva/planner/create_mat_view_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ def if_not_exists(self):
def columns(self):
return self._columns

def __str__(self):
return "CreateMaterializedViewPlan(view={}, \
columns={}, \
if_not_exists={})".format(
self._view, self._columns, self._if_not_exists
)

def __hash__(self) -> int:
return hash(
(super().__hash__(), self.view, self.if_not_exists, tuple(self.columns))
Expand Down
Loading