diff --git a/docs/Release Notes/Change Log.md b/docs/Release Notes/Change Log.md index d7ab70aa6..a4ae766b2 100644 --- a/docs/Release Notes/Change Log.md +++ b/docs/Release Notes/Change Log.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [[#366](https://github.com/mabel-dev/opteryx/issues/336)] Implement 'function not found' suggestions. ([@joocer](https://github.com/joocer)) - [[#443](https://github.com/mabel-dev/opteryx/issues/443)] Introduce a CLI. ([@joocer](https://github.com/joocer)) +- [[#351](https://github.com/mabel-dev/opteryx/issues/351)] Support `SHOW FUNCTIONS`. ([@joocer](https://github.com/joocer)) **Changed** diff --git a/docs/SQL Reference/02 Statements.md b/docs/SQL Reference/02 Statements.md index 00363d97a..ffd0cefe7 100644 --- a/docs/SQL Reference/02 Statements.md +++ b/docs/SQL Reference/02 Statements.md @@ -139,10 +139,10 @@ LIMIT count List the columns in a relation along with their data type. Without any modifiers, `SHOW COLUMNS` only reads a single page of data before returning. ~~~sql - SHOW [EXTENDED] [FULL] COLUMNS - FROM relation - LIKE pattern - FOR period +SHOW [EXTENDED] [FULL] COLUMNS +FROM relation +LIKE pattern + FOR period ~~~ ### EXTENDED modifier @@ -175,4 +175,12 @@ FOR DATES IN range The `FOR` clause specifies the date to review data for. Although this supports the full syntax as per the `SELECT` statements. -See [Temporality](https://mabel-dev.github.io/opteryx/SQL%20Reference/09%20Temporality/) for more information on `FOR` syntax and functionality. \ No newline at end of file +See [Temporality](https://mabel-dev.github.io/opteryx/SQL%20Reference/09%20Temporality/) for more information on `FOR` syntax and functionality. + +## SHOW FUNCTIONS + +List the functions and aggregators supported by the engine. + +~~~sql +SHOW FUNCTIONS +~~~ \ No newline at end of file diff --git a/opteryx/managers/query/planner/planner.py b/opteryx/managers/query/planner/planner.py index 6422c0bcf..2dccd64c5 100644 --- a/opteryx/managers/query/planner/planner.py +++ b/opteryx/managers/query/planner/planner.py @@ -118,6 +118,8 @@ def create_plan(self, sql: str = None, ast: dict = None): self._explain_planner(self._ast, self._statistics) elif "ShowColumns" in self._ast[0]: self._show_columns_planner(self._ast, self._statistics) + elif "ShowVariable" in self._ast[0]: + self._show_variable_planner(self._ast, self._statistics) else: # pragma: no cover raise SqlError("Unknown or unsupported Query type.") @@ -720,6 +722,40 @@ def _extract_identifiers(self, ast): return list(set(identifiers)) + def _show_variable_planner(self, ast, statistics): + """ + SHOW only really has a single node. + + All of the keywords should up as a 'values' list in the variable in the ast. + + The last word is the variable, preceeding words are modifiers. + """ + + directives = self._extract_directives(ast) + + keywords = [ + value["value"].upper() for value in ast[0]["ShowVariable"]["variable"] + ] + if keywords[-1] == "FUNCTIONS": + show_node = "show_functions" + node = operators.ShowFunctionsNode( + directives=directives, + statistics=statistics, + ) + self.add_operator(show_node, operator=node) + else: # pragma: no cover + raise SqlError(f"SHOW statement type not supported for `{keywords[-1]}`.") + + name_column = ExpressionTreeNode(NodeType.IDENTIFIER, value="name") + + order_by_node = operators.SortNode( + directives=directives, + statistics=statistics, + order=[([name_column], "ascending")], + ) + self.add_operator("order", operator=order_by_node) + self.link_operators(show_node, "order") + def _naive_select_planner(self, ast, statistics): """ The naive planner only works on single tables and always puts operations in diff --git a/opteryx/managers/query/planner/temporal.py b/opteryx/managers/query/planner/temporal.py index 8eff96ae0..57bed6491 100644 --- a/opteryx/managers/query/planner/temporal.py +++ b/opteryx/managers/query/planner/temporal.py @@ -51,6 +51,7 @@ r"FULL\sOUTER\sJOIN", r"JOIN", r"WITH", + r"SHOW", ] COMBINE_WHITESPACE_REGEX = re.compile(r"\s+") diff --git a/opteryx/operators/__init__.py b/opteryx/operators/__init__.py index d99d35e1d..3a3e31ce1 100644 --- a/opteryx/operators/__init__.py +++ b/opteryx/operators/__init__.py @@ -27,7 +27,8 @@ from .outer_join_node import OuterJoinNode # LEFT/RIGHT/FULL OUTER JOIN from .projection_node import ProjectionNode # remove unwanted columns including renames from .selection_node import SelectionNode # filter unwanted rows -from .show_columns import ShowColumnsNode # column details +from .show_columns_node import ShowColumnsNode # column details +from .show_functions_node import ShowFunctionsNode # supported functions from .sort_node import SortNode # order by selected columns diff --git a/opteryx/operators/show_columns.py b/opteryx/operators/show_columns_node.py similarity index 100% rename from opteryx/operators/show_columns.py rename to opteryx/operators/show_columns_node.py diff --git a/opteryx/operators/show_functions_node.py b/opteryx/operators/show_functions_node.py new file mode 100644 index 000000000..3e16f5edf --- /dev/null +++ b/opteryx/operators/show_functions_node.py @@ -0,0 +1,60 @@ +# 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. + +""" +Show Functions Node + +This is a SQL Query Execution Plan Node. +""" +from typing import Iterable +import pyarrow + +from opteryx import operators, functions +from opteryx.models import Columns, QueryDirectives, QueryStatistics +from opteryx.operators import BasePlanNode + + +class ShowFunctionsNode(BasePlanNode): + def __init__( + self, directives: QueryDirectives, statistics: QueryStatistics, **config + ): + super().__init__(directives=directives, statistics=statistics) + self._full = config.get("full") + self._extended = config.get("extended") + + @property + def name(self): # pragma: no cover + return "Show Functions" + + @property + def config(self): # pragma: no cover + return "" + + def execute(self) -> Iterable: + + buffer = [] + + for function in functions.functions(): + buffer.append({"name": function, "type": "function"}) + for aggregate in operators.aggregators(): + buffer.append({"name": aggregate, "type": "aggregator"}) + + table = pyarrow.Table.from_pylist(buffer) + table = Columns.create_table_metadata( + table=table, + expected_rows=len(buffer), + name="show_columns", + table_aliases=[], + ) + + yield table + return diff --git a/tests/query_execution/test_show_functions.py b/tests/query_execution/test_show_functions.py new file mode 100644 index 000000000..e58df9f0a --- /dev/null +++ b/tests/query_execution/test_show_functions.py @@ -0,0 +1,30 @@ +""" +Test show functions works; the number of functions is constantly changing so test it's +more than it was when we last reviewed this test. +""" +import os +import sys + +sys.path.insert(1, os.path.join(sys.path[0], "../..")) + + +def test_documentation_connect_example(): + + import opteryx + + conn = opteryx.connect() + cur = conn.cursor() + cur.execute("SHOW FUNCTIONS") + rows = cur.fetchall() + + # below here is not in the documentation + rows = list(rows) + assert len(rows) > 85, len(rows) + conn.close() + + +if __name__ == "__main__": # pragma: no cover + + test_documentation_connect_example() + + print("✅ okay")