diff --git a/opteryx/connectors/__init__.py b/opteryx/connectors/__init__.py index 11fc1eb67..d3521f0fe 100644 --- a/opteryx/connectors/__init__.py +++ b/opteryx/connectors/__init__.py @@ -106,7 +106,6 @@ def connector_factory(dataset, statistics, **config): from opteryx.connectors import file_connector return file_connector.FileConnector(dataset=dataset, statistics=statistics) - # fall back to the default connector (local disk if not set) connector = _storage_prefixes.get("_default", DiskConnector) diff --git a/opteryx/planner/logical_planner/logical_planner.py b/opteryx/planner/logical_planner/logical_planner.py index 94efcc00f..43655ad87 100644 --- a/opteryx/planner/logical_planner/logical_planner.py +++ b/opteryx/planner/logical_planner/logical_planner.py @@ -64,6 +64,8 @@ from opteryx.managers.expression import get_all_nodes_of_type from opteryx.models import Node from opteryx.planner.logical_planner import logical_planner_builders +from opteryx.planner.views import is_view +from opteryx.planner.views import view_as_plan from opteryx.third_party.travers import Graph @@ -663,6 +665,28 @@ def create_node_relation(relation): root_node = step_id else: # pragma: no cover raise NotImplementedError(relation["relation"]["Derived"]) + elif is_view(relation["relation"]["Table"]["name"][0]["value"]): + # We're a view, we need to add it to the plan as a subquery + view_name = relation["relation"]["Table"]["name"][0]["value"] + sub_plan = view_as_plan(view_name=view_name) + plan_head = sub_plan.get_exit_points()[0] + + # Replace the exit node with a subquery node + # We call the subquery the name of the view if we don't have an alias + # and we use the colums from the exit node + subquery_node = LogicalPlanNode(node_type=LogicalPlanStepType.Subquery) + subquery_node.alias = ( + view_name + if relation["relation"]["Table"]["alias"] is None + else relation["relation"]["Table"]["alias"]["name"]["value"] + ) + subquery_node.columns = sub_plan[plan_head].columns + sub_plan[plan_head] = subquery_node + root_node = plan_head + + # DEBUG: log (f"VIEW PLAN") + # DEBUG: log (sub_plan) + elif relation["relation"]["Table"]["args"]: function = relation["relation"]["Table"] function_name = function["name"][0]["value"].upper() diff --git a/opteryx/planner/views/__init__.py b/opteryx/planner/views/__init__.py new file mode 100644 index 000000000..b0cc3d396 --- /dev/null +++ b/opteryx/planner/views/__init__.py @@ -0,0 +1,47 @@ +# 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 orjson + +from opteryx.managers.expression import NodeType +from opteryx.third_party.travers import Graph + + +def _load_views(): + try: + with open("views.json", "rb") as defs: + return orjson.loads(defs.read()) + except Exception as err: + print(f"[OPTERYX] Unable to open views definition file. {err}") + return {} + + +VIEWS = _load_views() + + +def is_view(view_name: str) -> bool: + return view_name in VIEWS + + +def view_as_plan(view_name: str) -> Graph: + from opteryx.planner.logical_planner import do_logical_planning_phase + from opteryx.third_party import sqloxide + from opteryx.utils.sql import clean_statement + from opteryx.utils.sql import remove_comments + + operation = VIEWS.get(view_name)["statement"] + + clean_sql = clean_statement(remove_comments(operation)) + parsed_statements = sqloxide.parse_sql(clean_sql, dialect="mysql") + logical_plan, _, _ = next(do_logical_planning_phase(parsed_statements)) + + return logical_plan diff --git a/tests/sql_battery/test_shapes_and_errors_battery.py b/tests/sql_battery/test_shapes_and_errors_battery.py index db4842887..f6ada6ecc 100644 --- a/tests/sql_battery/test_shapes_and_errors_battery.py +++ b/tests/sql_battery/test_shapes_and_errors_battery.py @@ -1346,6 +1346,7 @@ ("SELECT * FROM $astronauts WHERE LIST_CONTAINS_ANY(missions, @@user_memberships)", 3, 19, None), ("SELECT $missions.* FROM $missions INNER JOIN $user ON Mission = value WHERE attribute = 'membership'", 1, 8, None), + # TEST FUNCTIONS ("EXECUTE PLANETS_BY_ID (id=1)", 1, 20, None), # simple case ("EXECUTE PLANETS_BY_ID (1)", None, None, ParameterError), # simple case) ("EXECUTE PLANETS_BY_ID (name=1)", None, None, ParameterError), # simple case) @@ -1355,6 +1356,20 @@ ("EXECUTE GET_SATELLITES_BY_PLANET_NAME(name='Jupiter')", 67, 1, SqlError), # string param ("EXECUTE multiply_two_numbers (one=1.0, two=9.9)", 1, 1, None), # multiple params + # TEST VIEWS + ("SELECT * FROM mission_reports", 177, 1, None), + ("SELECT * FROM mission_reports AS MR", 177, 1, None), + ("SELECT MR.* FROM mission_reports AS MR", 177, 1, None), + ("SELECT satellite_name FROM mission_reports", 177, 1, None), + ("SELECT MR.satellite_name FROM mission_reports AS MR", 177, 1, None), + ("SELECT satellite_name FROM mission_reports AS MR WHERE satellite_name ILIKE '%a%'", 90, 1, None), + ("SELECT satellite_name FROM mission_reports AS MR WHERE satellite_name ILIKE '%a%'", 90, 1, None), + ("SELECT * FROM mission_reports INNER JOIN $satellites ON satellite_name = name", 177, 9, None), + ("SELECT * FROM my_mission_reports", 3, 19, None), + ("SELECT * FROM my_mission_reports WHERE year = 1963", 2, 19, None), + ("SELECT * FROM my_mission_reports ORDER BY name", 3, 19, None), + ("SELECT name, status FROM my_mission_reports", 3, 2, None), + # These are queries which have been found to return the wrong result or not run correctly # FILTERING ON FUNCTIONS ("SELECT DATE(birth_date) FROM $astronauts FOR TODAY WHERE DATE(birth_date) < '1930-01-01'", 14, 1, None), diff --git a/views.json b/views.json new file mode 100644 index 000000000..be8d2fd59 --- /dev/null +++ b/views.json @@ -0,0 +1,8 @@ +{ + "mission_reports": { + "statement": "/* A test case for VIEW functionality */ SELECT s.name AS satellite_name FROM $satellites AS s INNER JOIN $planets AS p ON p.id = s.planetId" + }, + "my_mission_reports": { + "statement": "/* A test case for row-permissions functionality */ SELECT * FROM $astronauts WHERE LIST_CONTAINS_ANY(missions, @@user_memberships)" + } +} \ No newline at end of file