We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bug: `.visualize(label_edges=True)` case ops.NodeList we get ValueError tuple.index(x): x not in tuple
Hi,

There's still one small thing that needs fixing in the great `.visualize(label_edges=True)` feature 😄 :

When running...
```py
import ibis
t = ibis.table((("a", "int32"), ("b", "string")))
expr = t[(t["a"] == 1) & (t["b"] == "x")]

expr.visualize(label_edges=True)
```
...I get:
```
Exception has occurred: ValueError - tuple.index(x): x not in tuple
```
at the following line:
https://github.com/ibis-project/ibis/blob/2c9cfea15fc4d5f61e9099c3b270ea61498b5e45/ibis/expr/visualize.py#L117

This is happening when `v` is an `ops.NodeList` and thus its `v.args` is a tuple `of tuples` and NOT a tuple of nodes.

Given that on the next line we have special logic for `ops.NodeList`, maybe one quick fix could be to use the `.values`/`.args[0]` in such cases:

![image](https://user-images.githubusercontent.com/10154357/192848219-48560533-9cbc-4621-8759-145dcbe95462.png)

Thank you.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ibis/expr/visualize.py`
Content:
```
1 import sys
2 import tempfile
3 from html import escape
4 
5 import graphviz as gv
6 
7 import ibis
8 import ibis.common.exceptions as com
9 import ibis.expr.operations as ops
10 from ibis.common.graph import Graph
11 
12 
13 def get_type(node):
14     try:
15         return str(node.output_dtype)
16     except (AttributeError, NotImplementedError):
17         pass
18 
19     try:
20         schema = node.schema
21     except (AttributeError, NotImplementedError):
22         # TODO(kszucs): this branch should be removed
23         try:
24             # As a last resort try get the name of the output_type class
25             return node.output_type.__name__
26         except (AttributeError, NotImplementedError):
27             return '\u2205'  # empty set character
28     except com.IbisError:
29         assert isinstance(node, ops.Join)
30         left_table_name = getattr(node.left, 'name', None) or ops.genname()
31         left_schema = node.left.schema
32         right_table_name = getattr(node.right, 'name', None) or ops.genname()
33         right_schema = node.right.schema
34         pairs = [
35             (f'{left_table_name}.{left_column}', type)
36             for left_column, type in left_schema.items()
37         ] + [
38             (f'{right_table_name}.{right_column}', type)
39             for right_column, type in right_schema.items()
40         ]
41         schema = ibis.schema(pairs)
42 
43     return (
44         ''.join(
45             '<BR ALIGN="LEFT" /> <I>{}</I>: {}'.format(
46                 escape(name), escape(str(type))
47             )
48             for name, type in zip(schema.names, schema.types)
49         )
50         + '<BR ALIGN="LEFT" />'
51     )
52 
53 
54 def get_label(node):
55     typename = get_type(node)  # Already an escaped string
56     name = type(node).__name__
57     nodename = (
58         node.name
59         if isinstance(
60             node, (ops.Literal, ops.TableColumn, ops.Alias, ops.PhysicalTable)
61         )
62         else None
63     )
64     if nodename is not None:
65         if isinstance(node, ops.TableNode):
66             label_fmt = '<<I>{}</I>: <B>{}</B>{}>'
67         else:
68             label_fmt = '<<I>{}</I>: <B>{}</B><BR ALIGN="LEFT" />:: {}>'
69         label = label_fmt.format(escape(nodename), escape(name), typename)
70     else:
71         if isinstance(node, ops.TableNode):
72             label_fmt = '<<B>{}</B>{}>'
73         else:
74             label_fmt = '<<B>{}</B><BR ALIGN="LEFT" />:: {}>'
75         label = label_fmt.format(escape(name), typename)
76     return label
77 
78 
79 DEFAULT_NODE_ATTRS = {'shape': 'box', 'fontname': 'Deja Vu Sans Mono'}
80 DEFAULT_EDGE_ATTRS = {'fontname': 'Deja Vu Sans Mono'}
81 
82 
83 def to_graph(expr, node_attr=None, edge_attr=None, label_edges: bool = False):
84     graph = Graph.from_bfs(expr.op())
85 
86     g = gv.Digraph(
87         node_attr=node_attr or DEFAULT_NODE_ATTRS,
88         edge_attr=edge_attr or DEFAULT_EDGE_ATTRS,
89     )
90 
91     g.attr(rankdir='BT')
92 
93     seen = set()
94     edges = set()
95 
96     for v, us in graph.items():
97         if isinstance(v, ops.NodeList) and not v:
98             continue
99 
100         vhash = str(hash(v))
101         if v not in seen:
102             g.node(vhash, label=get_label(v))
103             seen.add(v)
104 
105         for u in us:
106             if isinstance(u, ops.NodeList) and not u:
107                 continue
108 
109             uhash = str(hash(u))
110             if u not in seen:
111                 g.node(uhash, label=get_label(u))
112                 seen.add(u)
113             if (edge := (u, v)) not in edges:
114                 if not label_edges:
115                     label = None
116                 else:
117                     index = v.args.index(u)
118                     if isinstance(v, ops.NodeList):
119                         arg_name = f"values[{index}]"
120                     else:
121                         arg_name = v.argnames[index]
122                     label = f"<.{arg_name}>"
123 
124                 g.edge(uhash, vhash, label=label)
125                 edges.add(edge)
126     return g
127 
128 
129 def draw(graph, path=None, format='png', verbose: bool = False):
130     if verbose:
131         print(graph.source, file=sys.stderr)
132 
133     piped_source = graph.pipe(format=format)
134 
135     if path is None:
136         with tempfile.NamedTemporaryFile(
137             delete=False, suffix=f'.{format}', mode='wb'
138         ) as f:
139             f.write(piped_source)
140         return f.name
141     else:
142         with open(path, mode='wb') as f:
143             f.write(piped_source)
144         return path
145 
146 
147 if __name__ == '__main__':
148     from argparse import ArgumentParser
149 
150     from ibis import _
151 
152     p = ArgumentParser(
153         description="Render a GraphViz SVG of an example ibis expression."
154     )
155 
156     p.add_argument(
157         "-v",
158         "--verbose",
159         action="count",
160         default=0,
161         help="Print GraphViz DOT code to stderr.",
162     )
163     p.add_argument(
164         "-l",
165         "--label-edges",
166         action="store_true",
167         help="Show operation inputs as edge labels.",
168     )
169 
170     args = p.parse_args()
171 
172     left = ibis.table(dict(a="int64", b="string"), name="left")
173     right = ibis.table(dict(b="string", c="int64", d="string"), name="right")
174     expr = (
175         left.inner_join(right, "b")
176         .select(left.a, b=right.c, c=right.d)
177         .filter((_.a + _.b * 2 * _.b / _.b**3 > 4) & (_.b > 5))
178         .groupby(_.c)
179         .having(_.a.mean() > 0.0)
180         .aggregate(a_mean=_.a.mean(), b_sum=_.b.sum())
181     )
182     expr.visualize(verbose=args.verbose > 0, label_edges=args.label_edges)
183
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ibis/expr/visualize.py b/ibis/expr/visualize.py
--- a/ibis/expr/visualize.py
+++ b/ibis/expr/visualize.py
@@ -114,10 +114,11 @@
                 if not label_edges:
                     label = None
                 else:
-                    index = v.args.index(u)
                     if isinstance(v, ops.NodeList):
+                        index = v.values.index(u)
                         arg_name = f"values[{index}]"
                     else:
+                        index = v.args.index(u)
                         arg_name = v.argnames[index]
                     label = f"<.{arg_name}>"

{"golden_diff": "diff --git a/ibis/expr/visualize.py b/ibis/expr/visualize.py\n--- a/ibis/expr/visualize.py\n+++ b/ibis/expr/visualize.py\n@@ -114,10 +114,11 @@\n                 if not label_edges:\n                     label = None\n                 else:\n-                    index = v.args.index(u)\n                     if isinstance(v, ops.NodeList):\n+                        index = v.values.index(u)\n                         arg_name = f\"values[{index}]\"\n                     else:\n+                        index = v.args.index(u)\n                         arg_name = v.argnames[index]\n                     label = f\"<.{arg_name}>\"\n", "issue": "bug: `.visualize(label_edges=True)` case ops.NodeList we get ValueError tuple.index(x): x not in tuple\nHi,\r\n\r\nThere's still one small thing that needs fixing in the great `.visualize(label_edges=True)` feature \ud83d\ude04 :\r\n\r\nWhen running...\r\n```py\r\nimport ibis\r\nt = ibis.table(((\"a\", \"int32\"), (\"b\", \"string\")))\r\nexpr = t[(t[\"a\"] == 1) & (t[\"b\"] == \"x\")]\r\n\r\nexpr.visualize(label_edges=True)\r\n```\r\n...I get:\r\n```\r\nException has occurred: ValueError - tuple.index(x): x not in tuple\r\n```\r\nat the following line:\r\nhttps://github.com/ibis-project/ibis/blob/2c9cfea15fc4d5f61e9099c3b270ea61498b5e45/ibis/expr/visualize.py#L117\r\n\r\nThis is happening when `v` is an `ops.NodeList` and thus its `v.args` is a tuple `of tuples` and NOT a tuple of nodes.\r\n\r\nGiven that on the next line we have special logic for `ops.NodeList`, maybe one quick fix could be to use the `.values`/`.args[0]` in such cases:\r\n\r\n![image](https://user-images.githubusercontent.com/10154357/192848219-48560533-9cbc-4621-8759-145dcbe95462.png)\r\n\r\nThank you.\n", "before_files": [{"content": "import sys\nimport tempfile\nfrom html import escape\n\nimport graphviz as gv\n\nimport ibis\nimport ibis.common.exceptions as com\nimport ibis.expr.operations as ops\nfrom ibis.common.graph import Graph\n\n\ndef get_type(node):\n    try:\n        return str(node.output_dtype)\n    except (AttributeError, NotImplementedError):\n        pass\n\n    try:\n        schema = node.schema\n    except (AttributeError, NotImplementedError):\n        # TODO(kszucs): this branch should be removed\n        try:\n            # As a last resort try get the name of the output_type class\n            return node.output_type.__name__\n        except (AttributeError, NotImplementedError):\n            return '\\u2205'  # empty set character\n    except com.IbisError:\n        assert isinstance(node, ops.Join)\n        left_table_name = getattr(node.left, 'name', None) or ops.genname()\n        left_schema = node.left.schema\n        right_table_name = getattr(node.right, 'name', None) or ops.genname()\n        right_schema = node.right.schema\n        pairs = [\n            (f'{left_table_name}.{left_column}', type)\n            for left_column, type in left_schema.items()\n        ] + [\n            (f'{right_table_name}.{right_column}', type)\n            for right_column, type in right_schema.items()\n        ]\n        schema = ibis.schema(pairs)\n\n    return (\n        ''.join(\n            '<BR ALIGN=\"LEFT\" /> <I>{}</I>: {}'.format(\n                escape(name), escape(str(type))\n            )\n            for name, type in zip(schema.names, schema.types)\n        )\n        + '<BR ALIGN=\"LEFT\" />'\n    )\n\n\ndef get_label(node):\n    typename = get_type(node)  # Already an escaped string\n    name = type(node).__name__\n    nodename = (\n        node.name\n        if isinstance(\n            node, (ops.Literal, ops.TableColumn, ops.Alias, ops.PhysicalTable)\n        )\n        else None\n    )\n    if nodename is not None:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<I>{}</I>: <B>{}</B>{}>'\n        else:\n            label_fmt = '<<I>{}</I>: <B>{}</B><BR ALIGN=\"LEFT\" />:: {}>'\n        label = label_fmt.format(escape(nodename), escape(name), typename)\n    else:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<B>{}</B>{}>'\n        else:\n            label_fmt = '<<B>{}</B><BR ALIGN=\"LEFT\" />:: {}>'\n        label = label_fmt.format(escape(name), typename)\n    return label\n\n\nDEFAULT_NODE_ATTRS = {'shape': 'box', 'fontname': 'Deja Vu Sans Mono'}\nDEFAULT_EDGE_ATTRS = {'fontname': 'Deja Vu Sans Mono'}\n\n\ndef to_graph(expr, node_attr=None, edge_attr=None, label_edges: bool = False):\n    graph = Graph.from_bfs(expr.op())\n\n    g = gv.Digraph(\n        node_attr=node_attr or DEFAULT_NODE_ATTRS,\n        edge_attr=edge_attr or DEFAULT_EDGE_ATTRS,\n    )\n\n    g.attr(rankdir='BT')\n\n    seen = set()\n    edges = set()\n\n    for v, us in graph.items():\n        if isinstance(v, ops.NodeList) and not v:\n            continue\n\n        vhash = str(hash(v))\n        if v not in seen:\n            g.node(vhash, label=get_label(v))\n            seen.add(v)\n\n        for u in us:\n            if isinstance(u, ops.NodeList) and not u:\n                continue\n\n            uhash = str(hash(u))\n            if u not in seen:\n                g.node(uhash, label=get_label(u))\n                seen.add(u)\n            if (edge := (u, v)) not in edges:\n                if not label_edges:\n                    label = None\n                else:\n                    index = v.args.index(u)\n                    if isinstance(v, ops.NodeList):\n                        arg_name = f\"values[{index}]\"\n                    else:\n                        arg_name = v.argnames[index]\n                    label = f\"<.{arg_name}>\"\n\n                g.edge(uhash, vhash, label=label)\n                edges.add(edge)\n    return g\n\n\ndef draw(graph, path=None, format='png', verbose: bool = False):\n    if verbose:\n        print(graph.source, file=sys.stderr)\n\n    piped_source = graph.pipe(format=format)\n\n    if path is None:\n        with tempfile.NamedTemporaryFile(\n            delete=False, suffix=f'.{format}', mode='wb'\n        ) as f:\n            f.write(piped_source)\n        return f.name\n    else:\n        with open(path, mode='wb') as f:\n            f.write(piped_source)\n        return path\n\n\nif __name__ == '__main__':\n    from argparse import ArgumentParser\n\n    from ibis import _\n\n    p = ArgumentParser(\n        description=\"Render a GraphViz SVG of an example ibis expression.\"\n    )\n\n    p.add_argument(\n        \"-v\",\n        \"--verbose\",\n        action=\"count\",\n        default=0,\n        help=\"Print GraphViz DOT code to stderr.\",\n    )\n    p.add_argument(\n        \"-l\",\n        \"--label-edges\",\n        action=\"store_true\",\n        help=\"Show operation inputs as edge labels.\",\n    )\n\n    args = p.parse_args()\n\n    left = ibis.table(dict(a=\"int64\", b=\"string\"), name=\"left\")\n    right = ibis.table(dict(b=\"string\", c=\"int64\", d=\"string\"), name=\"right\")\n    expr = (\n        left.inner_join(right, \"b\")\n        .select(left.a, b=right.c, c=right.d)\n        .filter((_.a + _.b * 2 * _.b / _.b**3 > 4) & (_.b > 5))\n        .groupby(_.c)\n        .having(_.a.mean() > 0.0)\n        .aggregate(a_mean=_.a.mean(), b_sum=_.b.sum())\n    )\n    expr.visualize(verbose=args.verbose > 0, label_edges=args.label_edges)\n", "path": "ibis/expr/visualize.py"}], "after_files": [{"content": "import sys\nimport tempfile\nfrom html import escape\n\nimport graphviz as gv\n\nimport ibis\nimport ibis.common.exceptions as com\nimport ibis.expr.operations as ops\nfrom ibis.common.graph import Graph\n\n\ndef get_type(node):\n    try:\n        return str(node.output_dtype)\n    except (AttributeError, NotImplementedError):\n        pass\n\n    try:\n        schema = node.schema\n    except (AttributeError, NotImplementedError):\n        # TODO(kszucs): this branch should be removed\n        try:\n            # As a last resort try get the name of the output_type class\n            return node.output_type.__name__\n        except (AttributeError, NotImplementedError):\n            return '\\u2205'  # empty set character\n    except com.IbisError:\n        assert isinstance(node, ops.Join)\n        left_table_name = getattr(node.left, 'name', None) or ops.genname()\n        left_schema = node.left.schema\n        right_table_name = getattr(node.right, 'name', None) or ops.genname()\n        right_schema = node.right.schema\n        pairs = [\n            (f'{left_table_name}.{left_column}', type)\n            for left_column, type in left_schema.items()\n        ] + [\n            (f'{right_table_name}.{right_column}', type)\n            for right_column, type in right_schema.items()\n        ]\n        schema = ibis.schema(pairs)\n\n    return (\n        ''.join(\n            '<BR ALIGN=\"LEFT\" /> <I>{}</I>: {}'.format(\n                escape(name), escape(str(type))\n            )\n            for name, type in zip(schema.names, schema.types)\n        )\n        + '<BR ALIGN=\"LEFT\" />'\n    )\n\n\ndef get_label(node):\n    typename = get_type(node)  # Already an escaped string\n    name = type(node).__name__\n    nodename = (\n        node.name\n        if isinstance(\n            node, (ops.Literal, ops.TableColumn, ops.Alias, ops.PhysicalTable)\n        )\n        else None\n    )\n    if nodename is not None:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<I>{}</I>: <B>{}</B>{}>'\n        else:\n            label_fmt = '<<I>{}</I>: <B>{}</B><BR ALIGN=\"LEFT\" />:: {}>'\n        label = label_fmt.format(escape(nodename), escape(name), typename)\n    else:\n        if isinstance(node, ops.TableNode):\n            label_fmt = '<<B>{}</B>{}>'\n        else:\n            label_fmt = '<<B>{}</B><BR ALIGN=\"LEFT\" />:: {}>'\n        label = label_fmt.format(escape(name), typename)\n    return label\n\n\nDEFAULT_NODE_ATTRS = {'shape': 'box', 'fontname': 'Deja Vu Sans Mono'}\nDEFAULT_EDGE_ATTRS = {'fontname': 'Deja Vu Sans Mono'}\n\n\ndef to_graph(expr, node_attr=None, edge_attr=None, label_edges: bool = False):\n    graph = Graph.from_bfs(expr.op())\n\n    g = gv.Digraph(\n        node_attr=node_attr or DEFAULT_NODE_ATTRS,\n        edge_attr=edge_attr or DEFAULT_EDGE_ATTRS,\n    )\n\n    g.attr(rankdir='BT')\n\n    seen = set()\n    edges = set()\n\n    for v, us in graph.items():\n        if isinstance(v, ops.NodeList) and not v:\n            continue\n\n        vhash = str(hash(v))\n        if v not in seen:\n            g.node(vhash, label=get_label(v))\n            seen.add(v)\n\n        for u in us:\n            if isinstance(u, ops.NodeList) and not u:\n                continue\n\n            uhash = str(hash(u))\n            if u not in seen:\n                g.node(uhash, label=get_label(u))\n                seen.add(u)\n            if (edge := (u, v)) not in edges:\n                if not label_edges:\n                    label = None\n                else:\n                    if isinstance(v, ops.NodeList):\n                        index = v.values.index(u)\n                        arg_name = f\"values[{index}]\"\n                    else:\n                        index = v.args.index(u)\n                        arg_name = v.argnames[index]\n                    label = f\"<.{arg_name}>\"\n\n                g.edge(uhash, vhash, label=label)\n                edges.add(edge)\n    return g\n\n\ndef draw(graph, path=None, format='png', verbose: bool = False):\n    if verbose:\n        print(graph.source, file=sys.stderr)\n\n    piped_source = graph.pipe(format=format)\n\n    if path is None:\n        with tempfile.NamedTemporaryFile(\n            delete=False, suffix=f'.{format}', mode='wb'\n        ) as f:\n            f.write(piped_source)\n        return f.name\n    else:\n        with open(path, mode='wb') as f:\n            f.write(piped_source)\n        return path\n\n\nif __name__ == '__main__':\n    from argparse import ArgumentParser\n\n    from ibis import _\n\n    p = ArgumentParser(\n        description=\"Render a GraphViz SVG of an example ibis expression.\"\n    )\n\n    p.add_argument(\n        \"-v\",\n        \"--verbose\",\n        action=\"count\",\n        default=0,\n        help=\"Print GraphViz DOT code to stderr.\",\n    )\n    p.add_argument(\n        \"-l\",\n        \"--label-edges\",\n        action=\"store_true\",\n        help=\"Show operation inputs as edge labels.\",\n    )\n\n    args = p.parse_args()\n\n    left = ibis.table(dict(a=\"int64\", b=\"string\"), name=\"left\")\n    right = ibis.table(dict(b=\"string\", c=\"int64\", d=\"string\"), name=\"right\")\n    expr = (\n        left.inner_join(right, \"b\")\n        .select(left.a, b=right.c, c=right.d)\n        .filter((_.a + _.b * 2 * _.b / _.b**3 > 4) & (_.b > 5))\n        .groupby(_.c)\n        .having(_.a.mean() > 0.0)\n        .aggregate(a_mean=_.a.mean(), b_sum=_.b.sum())\n    )\n    expr.visualize(verbose=args.verbose > 0, label_edges=args.label_edges)\n", "path": "ibis/expr/visualize.py"}]}