Repository: mabel-dev/opteryx

Issue:
✨ EXIT node approach is flawed
it uses the projection as its config, it shouldn't be configured and should work out what to do based on the schemas.

- the $derived table should be ignored
- if the projection contains calculations, they should be in the $projection table
- if there's no $projection table, use the other tables, if there's more than one then qualify the column names


Discussion:
code has been updated so it doesn't crash, but it's not quite working right yet.
The projection node needs to qualify columns when the result dataset has multiple columns

It probably currently won't work with a SELECT * and a JOIN clause.
I think the problem is actually with the projection, mapping columns to a virtual `$projection` relation is making it difficult to trace them back when there's queries like `SELECT A.ID, B.ID`, they both have the name `ID` in the `$projection` relation and I found myself adding more and more logic to cope with edge cases. I think creating the `$projection` relation should be removed and the logic that is needed put into the SUBQUERY and EXIT binders.
refactored, mostly right now, an edge case where the columns have aliases in the schema appears to still be broken


Resolving patch:
diff --git a/opteryx/components/binder/binder_visitor.py b/opteryx/components/binder/binder_visitor.py
--- a/opteryx/components/binder/binder_visitor.py
+++ b/opteryx/components/binder/binder_visitor.py
@@ -275,14 +275,13 @@ def visit_exit(self, node: Node, context: BindingContext) -> Tuple[Node, Binding
 
         def name_column(qualifier, column):
             if len(context.schemas) > 1 or needs_qualifier:
-                if len(column.aliases) == 1:
-                    return column.aliases[0]
+                # if len(column.aliases) == 1:
+                #    return column.aliases[0]
                 return f"{qualifier}.{column.name}"
             return column.name
 
         for qualifier, schema in context.schemas.items():
             for column in schema.columns:
-                print(column.name, qualifier)
                 column_reference = Node(
                     node_type=NodeType.IDENTIFIER,
                     name=column.name,
@@ -426,6 +425,19 @@ def visit_project(self, node: Node, context: BindingContext) -> Tuple[Node, Bind
 
         all_identities = [c.schema_column.identity for c in node.columns]
 
+        if len(set(all_identities)) != len(all_identities):
+            from collections import Counter
+
+            from opteryx.exceptions import AmbiguousIdentifierError
+
+            duplicates = [column for column, count in Counter(all_identities).items() if count > 1]
+            matches = {
+                c.query_column for c in node.columns if c.schema_column.identity in duplicates
+            }
+            raise AmbiguousIdentifierError(
+                message=f"Query result contains multiple instances of the same column(s) - `{'`, `'.join(matches)}`"
+            )
+
         columns = []
         for relation, schema in list(context.schemas.items()):
             schema_columns = [
diff --git a/opteryx/managers/schemes/mabel_partitions.py b/opteryx/managers/schemes/mabel_partitions.py
--- a/opteryx/managers/schemes/mabel_partitions.py
+++ b/opteryx/managers/schemes/mabel_partitions.py
@@ -24,9 +24,10 @@
 class UnsupportedSegementationError(DataError):
     """Exception raised for unsupported segmentations."""
 
-    def __init__(self, dataset: str):
+    def __init__(self, dataset: str, segments: set = None):
         self.dataset = dataset
-        message = f"'{dataset}' cannot be read, only 'by_hour' segments can be read."
+        self.segments = segments
+        message = f"'{dataset}' contains unsupported segmentation (`{'`, `'.join(segments)}`), only 'by_hour' segments are supported."
         super().__init__(message)
 
 
@@ -75,10 +76,9 @@ def _inner(*, timestamp):
             # Call your method to get the list of blob names
             blob_names = blob_list_getter(prefix=date_path)
 
-            if len({_extract_by(blob) for blob in blob_names} - {"by_hour", None}) > 0:
-                from opteryx.exceptions import UnsupportedSegementationError
-
-                raise UnsupportedSegementationError(dataset=prefix)
+            segments = {_extract_by(blob) for blob in blob_names}
+            if len(segments - {"by_hour", None}) > 0:
+                raise UnsupportedSegementationError(dataset=prefix, segments=segments)
 
             # Filter for the specific hour, if hour folders exist - prefer by_hour segements
             if any(f"/by_hour/hour={hour:02d}/" in blob_name for blob_name in blob_names):


Test patch: