From 67c28dbe67209effad83d93b850caba5ee1e20e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 3 May 2023 11:12:28 +0200 Subject: Merging upstream version 11.7.1. Signed-off-by: Daniel Baumann --- docs/sqlglot/lineage.html | 1018 ++++++++++++++++++++++++--------------------- 1 file changed, 536 insertions(+), 482 deletions(-) (limited to 'docs/sqlglot/lineage.html') diff --git a/docs/sqlglot/lineage.html b/docs/sqlglot/lineage.html index a9038d5..1c375ad 100644 --- a/docs/sqlglot/lineage.html +++ b/docs/sqlglot/lineage.html @@ -3,7 +3,7 @@ - + sqlglot.lineage API documentation @@ -102,216 +102,243 @@ 20 expression: exp.Expression 21 source: exp.Expression 22 downstream: t.List[Node] = field(default_factory=list) - 23 - 24 def walk(self) -> t.Iterator[Node]: - 25 yield self - 26 - 27 for d in self.downstream: - 28 if isinstance(d, Node): - 29 yield from d.walk() - 30 else: - 31 yield d - 32 - 33 def to_html(self, **opts) -> LineageHTML: - 34 return LineageHTML(self, **opts) - 35 + 23 alias: str = "" + 24 + 25 def walk(self) -> t.Iterator[Node]: + 26 yield self + 27 + 28 for d in self.downstream: + 29 if isinstance(d, Node): + 30 yield from d.walk() + 31 else: + 32 yield d + 33 + 34 def to_html(self, **opts) -> LineageHTML: + 35 return LineageHTML(self, **opts) 36 - 37def lineage( - 38 column: str | exp.Column, - 39 sql: str | exp.Expression, - 40 schema: t.Optional[t.Dict | Schema] = None, - 41 sources: t.Optional[t.Dict[str, str | exp.Subqueryable]] = None, - 42 rules: t.Sequence[t.Callable] = (qualify_tables, qualify_columns, expand_laterals), - 43 dialect: DialectType = None, - 44) -> Node: - 45 """Build the lineage graph for a column of a SQL query. - 46 - 47 Args: - 48 column: The column to build the lineage for. - 49 sql: The SQL string or expression. - 50 schema: The schema of tables. - 51 sources: A mapping of queries which will be used to continue building lineage. - 52 rules: Optimizer rules to apply, by default only qualifying tables and columns. - 53 dialect: The dialect of input SQL. - 54 - 55 Returns: - 56 A lineage node. - 57 """ - 58 - 59 expression = maybe_parse(sql, dialect=dialect) - 60 - 61 if sources: - 62 expression = exp.expand( - 63 expression, - 64 { - 65 k: t.cast(exp.Subqueryable, maybe_parse(v, dialect=dialect)) - 66 for k, v in sources.items() - 67 }, - 68 ) - 69 - 70 optimized = optimize(expression, schema=schema, rules=rules) - 71 scope = build_scope(optimized) - 72 tables: t.Dict[str, Node] = {} + 37 + 38def lineage( + 39 column: str | exp.Column, + 40 sql: str | exp.Expression, + 41 schema: t.Optional[t.Dict | Schema] = None, + 42 sources: t.Optional[t.Dict[str, str | exp.Subqueryable]] = None, + 43 rules: t.Sequence[t.Callable] = (qualify_tables, qualify_columns, expand_laterals), + 44 dialect: DialectType = None, + 45) -> Node: + 46 """Build the lineage graph for a column of a SQL query. + 47 + 48 Args: + 49 column: The column to build the lineage for. + 50 sql: The SQL string or expression. + 51 schema: The schema of tables. + 52 sources: A mapping of queries which will be used to continue building lineage. + 53 rules: Optimizer rules to apply, by default only qualifying tables and columns. + 54 dialect: The dialect of input SQL. + 55 + 56 Returns: + 57 A lineage node. + 58 """ + 59 + 60 expression = maybe_parse(sql, dialect=dialect) + 61 + 62 if sources: + 63 expression = exp.expand( + 64 expression, + 65 { + 66 k: t.cast(exp.Subqueryable, maybe_parse(v, dialect=dialect)) + 67 for k, v in sources.items() + 68 }, + 69 ) + 70 + 71 optimized = optimize(expression, schema=schema, rules=rules) + 72 scope = build_scope(optimized) 73 74 def to_node( 75 column_name: str, 76 scope: Scope, 77 scope_name: t.Optional[str] = None, 78 upstream: t.Optional[Node] = None, - 79 ) -> Node: - 80 if isinstance(scope.expression, exp.Union): - 81 for scope in scope.union_scopes: - 82 node = to_node( - 83 column_name, - 84 scope=scope, - 85 scope_name=scope_name, - 86 upstream=upstream, - 87 ) - 88 return node - 89 - 90 select = next(select for select in scope.selects if select.alias_or_name == column_name) - 91 source = optimize(scope.expression.select(select, append=False), schema=schema, rules=rules) - 92 select = source.selects[0] - 93 - 94 node = Node( - 95 name=f"{scope_name}.{column_name}" if scope_name else column_name, - 96 source=source, - 97 expression=select, - 98 ) - 99 -100 if upstream: -101 upstream.downstream.append(node) -102 -103 for c in set(select.find_all(exp.Column)): -104 table = c.table -105 source = scope.sources[table] + 79 alias: t.Optional[str] = None, + 80 ) -> Node: + 81 aliases = { + 82 dt.alias: dt.comments[0].split()[1] + 83 for dt in scope.derived_tables + 84 if dt.comments and dt.comments[0].startswith("source: ") + 85 } + 86 if isinstance(scope.expression, exp.Union): + 87 for scope in scope.union_scopes: + 88 node = to_node( + 89 column_name, + 90 scope=scope, + 91 scope_name=scope_name, + 92 upstream=upstream, + 93 alias=aliases.get(scope_name), + 94 ) + 95 return node + 96 + 97 # Find the specific select clause that is the source of the column we want. + 98 # This can either be a specific, named select or a generic `*` clause. + 99 select = next( +100 (select for select in scope.selects if select.alias_or_name == column_name), +101 exp.Star() if scope.expression.is_star else None, +102 ) +103 +104 if not select: +105 raise ValueError(f"Could not find {column_name} in {scope.expression}") 106 -107 if isinstance(source, Scope): -108 to_node( -109 c.name, -110 scope=source, -111 scope_name=table, -112 upstream=node, -113 ) -114 else: -115 if table not in tables: -116 tables[table] = Node(name=c.sql(), source=source, expression=source) -117 node.downstream.append(tables[table]) +107 if isinstance(scope.expression, exp.Select): +108 # For better ergonomics in our node labels, replace the full select with +109 # a version that has only the column we care about. +110 # "x", SELECT x, y FROM foo +111 # => "x", SELECT x FROM foo +112 source = optimize( +113 scope.expression.select(select, append=False), schema=schema, rules=rules +114 ) +115 select = source.selects[0] +116 else: +117 source = scope.expression 118 -119 return node -120 -121 return to_node(column if isinstance(column, str) else column.name, scope) -122 -123 -124class LineageHTML: -125 """Node to HTML generator using vis.js. -126 -127 https://visjs.github.io/vis-network/docs/network/ -128 """ -129 -130 def __init__( -131 self, -132 node: Node, -133 dialect: DialectType = None, -134 imports: bool = True, -135 **opts: t.Any, -136 ): -137 self.node = node -138 self.imports = imports -139 -140 self.options = { -141 "height": "500px", -142 "width": "100%", -143 "layout": { -144 "hierarchical": { -145 "enabled": True, -146 "nodeSpacing": 200, -147 "sortMethod": "directed", -148 }, -149 }, -150 "interaction": { -151 "dragNodes": False, -152 "selectable": False, -153 }, -154 "physics": { -155 "enabled": False, -156 }, -157 "edges": { -158 "arrows": "to", -159 }, -160 "nodes": { -161 "font": "20px monaco", -162 "shape": "box", -163 "widthConstraint": { -164 "maximum": 300, -165 }, -166 }, -167 **opts, -168 } -169 -170 self.nodes = {} -171 self.edges = [] -172 -173 for node in node.walk(): -174 if isinstance(node.expression, exp.Table): -175 label = f"FROM {node.expression.this}" -176 title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>" -177 group = 1 -178 else: -179 label = node.expression.sql(pretty=True, dialect=dialect) -180 source = node.source.transform( -181 lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>") -182 if n is node.expression -183 else n, -184 copy=False, -185 ).sql(pretty=True, dialect=dialect) -186 title = f"<pre>{source}</pre>" -187 group = 0 -188 -189 node_id = id(node) -190 -191 self.nodes[node_id] = { -192 "id": node_id, -193 "label": label, -194 "title": title, -195 "group": group, -196 } -197 -198 for d in node.downstream: -199 self.edges.append({"from": node_id, "to": id(d)}) -200 -201 def __str__(self): -202 nodes = json.dumps(list(self.nodes.values())) -203 edges = json.dumps(self.edges) -204 options = json.dumps(self.options) -205 imports = ( -206 """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script> -207 <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script> -208 <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />""" -209 if self.imports -210 else "" -211 ) -212 -213 return f"""<div> -214 <div id="sqlglot-lineage"></div> -215 {imports} -216 <script type="text/javascript"> -217 var nodes = new vis.DataSet({nodes}) -218 nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0]) -219 -220 new vis.Network( -221 document.getElementById("sqlglot-lineage"), -222 {{ -223 nodes: nodes, -224 edges: new vis.DataSet({edges}) -225 }}, -226 {options}, -227 ) -228 </script> -229</div>""" -230 -231 def _repr_html_(self) -> str: -232 return self.__str__() +119 # Create the node for this step in the lineage chain, and attach it to the previous one. +120 node = Node( +121 name=f"{scope_name}.{column_name}" if scope_name else column_name, +122 source=source, +123 expression=select, +124 alias=alias or "", +125 ) +126 if upstream: +127 upstream.downstream.append(node) +128 +129 # Find all columns that went into creating this one to list their lineage nodes. +130 for c in set(select.find_all(exp.Column)): +131 table = c.table +132 source = scope.sources.get(table) +133 +134 if isinstance(source, Scope): +135 # The table itself came from a more specific scope. Recurse into that one using the unaliased column name. +136 to_node( +137 c.name, scope=source, scope_name=table, upstream=node, alias=aliases.get(table) +138 ) +139 else: +140 # The source is not a scope - we've reached the end of the line. At this point, if a source is not found +141 # it means this column's lineage is unknown. This can happen if the definition of a source used in a query +142 # is not passed into the `sources` map. +143 source = source or exp.Placeholder() +144 node.downstream.append(Node(name=c.sql(), source=source, expression=source)) +145 +146 return node +147 +148 return to_node(column if isinstance(column, str) else column.name, scope) +149 +150 +151class LineageHTML: +152 """Node to HTML generator using vis.js. +153 +154 https://visjs.github.io/vis-network/docs/network/ +155 """ +156 +157 def __init__( +158 self, +159 node: Node, +160 dialect: DialectType = None, +161 imports: bool = True, +162 **opts: t.Any, +163 ): +164 self.node = node +165 self.imports = imports +166 +167 self.options = { +168 "height": "500px", +169 "width": "100%", +170 "layout": { +171 "hierarchical": { +172 "enabled": True, +173 "nodeSpacing": 200, +174 "sortMethod": "directed", +175 }, +176 }, +177 "interaction": { +178 "dragNodes": False, +179 "selectable": False, +180 }, +181 "physics": { +182 "enabled": False, +183 }, +184 "edges": { +185 "arrows": "to", +186 }, +187 "nodes": { +188 "font": "20px monaco", +189 "shape": "box", +190 "widthConstraint": { +191 "maximum": 300, +192 }, +193 }, +194 **opts, +195 } +196 +197 self.nodes = {} +198 self.edges = [] +199 +200 for node in node.walk(): +201 if isinstance(node.expression, exp.Table): +202 label = f"FROM {node.expression.this}" +203 title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>" +204 group = 1 +205 else: +206 label = node.expression.sql(pretty=True, dialect=dialect) +207 source = node.source.transform( +208 lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>") +209 if n is node.expression +210 else n, +211 copy=False, +212 ).sql(pretty=True, dialect=dialect) +213 title = f"<pre>{source}</pre>" +214 group = 0 +215 +216 node_id = id(node) +217 +218 self.nodes[node_id] = { +219 "id": node_id, +220 "label": label, +221 "title": title, +222 "group": group, +223 } +224 +225 for d in node.downstream: +226 self.edges.append({"from": node_id, "to": id(d)}) +227 +228 def __str__(self): +229 nodes = json.dumps(list(self.nodes.values())) +230 edges = json.dumps(self.edges) +231 options = json.dumps(self.options) +232 imports = ( +233 """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script> +234 <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script> +235 <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />""" +236 if self.imports +237 else "" +238 ) +239 +240 return f"""<div> +241 <div id="sqlglot-lineage"></div> +242 {imports} +243 <script type="text/javascript"> +244 var nodes = new vis.DataSet({nodes}) +245 nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0]) +246 +247 new vis.Network( +248 document.getElementById("sqlglot-lineage"), +249 {{ +250 nodes: nodes, +251 edges: new vis.DataSet({edges}) +252 }}, +253 {options}, +254 ) +255 </script> +256</div>""" +257 +258 def _repr_html_(self) -> str: +259 return self.__str__() @@ -334,18 +361,19 @@ 21 expression: exp.Expression 22 source: exp.Expression 23 downstream: t.List[Node] = field(default_factory=list) -24 -25 def walk(self) -> t.Iterator[Node]: -26 yield self -27 -28 for d in self.downstream: -29 if isinstance(d, Node): -30 yield from d.walk() -31 else: -32 yield d -33 -34 def to_html(self, **opts) -> LineageHTML: -35 return LineageHTML(self, **opts) +24 alias: str = "" +25 +26 def walk(self) -> t.Iterator[Node]: +27 yield self +28 +29 for d in self.downstream: +30 if isinstance(d, Node): +31 yield from d.walk() +32 else: +33 yield d +34 +35 def to_html(self, **opts) -> LineageHTML: +36 return LineageHTML(self, **opts) @@ -354,7 +382,7 @@
- Node( name: str, expression: sqlglot.expressions.Expression, source: sqlglot.expressions.Expression, downstream: List[sqlglot.lineage.Node] = <factory>) + Node( name: str, expression: sqlglot.expressions.Expression, source: sqlglot.expressions.Expression, downstream: List[sqlglot.lineage.Node] = <factory>, alias: str = '')
@@ -374,14 +402,14 @@
-
25    def walk(self) -> t.Iterator[Node]:
-26        yield self
-27
-28        for d in self.downstream:
-29            if isinstance(d, Node):
-30                yield from d.walk()
-31            else:
-32                yield d
+            
26    def walk(self) -> t.Iterator[Node]:
+27        yield self
+28
+29        for d in self.downstream:
+30            if isinstance(d, Node):
+31                yield from d.walk()
+32            else:
+33                yield d
 
@@ -399,8 +427,8 @@
-
34    def to_html(self, **opts) -> LineageHTML:
-35        return LineageHTML(self, **opts)
+            
35    def to_html(self, **opts) -> LineageHTML:
+36        return LineageHTML(self, **opts)
 
@@ -413,97 +441,123 @@
def - lineage( column: str | sqlglot.expressions.Column, sql: str | sqlglot.expressions.Expression, schema: Union[Dict, sqlglot.schema.Schema, NoneType] = None, sources: Optional[Dict[str, str | sqlglot.expressions.Subqueryable]] = None, rules: Sequence[Callable] = (<function qualify_tables at 0x7f1311384310>, <function qualify_columns at 0x7f13113b71c0>, <function expand_laterals at 0x7f13113b5870>), dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None) -> sqlglot.lineage.Node: + lineage( column: str | sqlglot.expressions.Column, sql: str | sqlglot.expressions.Expression, schema: Union[Dict, sqlglot.schema.Schema, NoneType] = None, sources: Optional[Dict[str, str | sqlglot.expressions.Subqueryable]] = None, rules: Sequence[Callable] = (<function qualify_tables at 0x7fac3cdc5630>, <function qualify_columns at 0x7fac3cdc4550>, <function expand_laterals at 0x7fac3cd8eb90>), dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None) -> sqlglot.lineage.Node:
-
 38def lineage(
- 39    column: str | exp.Column,
- 40    sql: str | exp.Expression,
- 41    schema: t.Optional[t.Dict | Schema] = None,
- 42    sources: t.Optional[t.Dict[str, str | exp.Subqueryable]] = None,
- 43    rules: t.Sequence[t.Callable] = (qualify_tables, qualify_columns, expand_laterals),
- 44    dialect: DialectType = None,
- 45) -> Node:
- 46    """Build the lineage graph for a column of a SQL query.
- 47
- 48    Args:
- 49        column: The column to build the lineage for.
- 50        sql: The SQL string or expression.
- 51        schema: The schema of tables.
- 52        sources: A mapping of queries which will be used to continue building lineage.
- 53        rules: Optimizer rules to apply, by default only qualifying tables and columns.
- 54        dialect: The dialect of input SQL.
- 55
- 56    Returns:
- 57        A lineage node.
- 58    """
- 59
- 60    expression = maybe_parse(sql, dialect=dialect)
- 61
- 62    if sources:
- 63        expression = exp.expand(
- 64            expression,
- 65            {
- 66                k: t.cast(exp.Subqueryable, maybe_parse(v, dialect=dialect))
- 67                for k, v in sources.items()
- 68            },
- 69        )
- 70
- 71    optimized = optimize(expression, schema=schema, rules=rules)
- 72    scope = build_scope(optimized)
- 73    tables: t.Dict[str, Node] = {}
+            
 39def lineage(
+ 40    column: str | exp.Column,
+ 41    sql: str | exp.Expression,
+ 42    schema: t.Optional[t.Dict | Schema] = None,
+ 43    sources: t.Optional[t.Dict[str, str | exp.Subqueryable]] = None,
+ 44    rules: t.Sequence[t.Callable] = (qualify_tables, qualify_columns, expand_laterals),
+ 45    dialect: DialectType = None,
+ 46) -> Node:
+ 47    """Build the lineage graph for a column of a SQL query.
+ 48
+ 49    Args:
+ 50        column: The column to build the lineage for.
+ 51        sql: The SQL string or expression.
+ 52        schema: The schema of tables.
+ 53        sources: A mapping of queries which will be used to continue building lineage.
+ 54        rules: Optimizer rules to apply, by default only qualifying tables and columns.
+ 55        dialect: The dialect of input SQL.
+ 56
+ 57    Returns:
+ 58        A lineage node.
+ 59    """
+ 60
+ 61    expression = maybe_parse(sql, dialect=dialect)
+ 62
+ 63    if sources:
+ 64        expression = exp.expand(
+ 65            expression,
+ 66            {
+ 67                k: t.cast(exp.Subqueryable, maybe_parse(v, dialect=dialect))
+ 68                for k, v in sources.items()
+ 69            },
+ 70        )
+ 71
+ 72    optimized = optimize(expression, schema=schema, rules=rules)
+ 73    scope = build_scope(optimized)
  74
  75    def to_node(
  76        column_name: str,
  77        scope: Scope,
  78        scope_name: t.Optional[str] = None,
  79        upstream: t.Optional[Node] = None,
- 80    ) -> Node:
- 81        if isinstance(scope.expression, exp.Union):
- 82            for scope in scope.union_scopes:
- 83                node = to_node(
- 84                    column_name,
- 85                    scope=scope,
- 86                    scope_name=scope_name,
- 87                    upstream=upstream,
- 88                )
- 89            return node
- 90
- 91        select = next(select for select in scope.selects if select.alias_or_name == column_name)
- 92        source = optimize(scope.expression.select(select, append=False), schema=schema, rules=rules)
- 93        select = source.selects[0]
- 94
- 95        node = Node(
- 96            name=f"{scope_name}.{column_name}" if scope_name else column_name,
- 97            source=source,
- 98            expression=select,
- 99        )
-100
-101        if upstream:
-102            upstream.downstream.append(node)
-103
-104        for c in set(select.find_all(exp.Column)):
-105            table = c.table
-106            source = scope.sources[table]
+ 80        alias: t.Optional[str] = None,
+ 81    ) -> Node:
+ 82        aliases = {
+ 83            dt.alias: dt.comments[0].split()[1]
+ 84            for dt in scope.derived_tables
+ 85            if dt.comments and dt.comments[0].startswith("source: ")
+ 86        }
+ 87        if isinstance(scope.expression, exp.Union):
+ 88            for scope in scope.union_scopes:
+ 89                node = to_node(
+ 90                    column_name,
+ 91                    scope=scope,
+ 92                    scope_name=scope_name,
+ 93                    upstream=upstream,
+ 94                    alias=aliases.get(scope_name),
+ 95                )
+ 96            return node
+ 97
+ 98        # Find the specific select clause that is the source of the column we want.
+ 99        # This can either be a specific, named select or a generic `*` clause.
+100        select = next(
+101            (select for select in scope.selects if select.alias_or_name == column_name),
+102            exp.Star() if scope.expression.is_star else None,
+103        )
+104
+105        if not select:
+106            raise ValueError(f"Could not find {column_name} in {scope.expression}")
 107
-108            if isinstance(source, Scope):
-109                to_node(
-110                    c.name,
-111                    scope=source,
-112                    scope_name=table,
-113                    upstream=node,
-114                )
-115            else:
-116                if table not in tables:
-117                    tables[table] = Node(name=c.sql(), source=source, expression=source)
-118                node.downstream.append(tables[table])
+108        if isinstance(scope.expression, exp.Select):
+109            # For better ergonomics in our node labels, replace the full select with
+110            # a version that has only the column we care about.
+111            #   "x", SELECT x, y FROM foo
+112            #     => "x", SELECT x FROM foo
+113            source = optimize(
+114                scope.expression.select(select, append=False), schema=schema, rules=rules
+115            )
+116            select = source.selects[0]
+117        else:
+118            source = scope.expression
 119
-120        return node
-121
-122    return to_node(column if isinstance(column, str) else column.name, scope)
+120        # Create the node for this step in the lineage chain, and attach it to the previous one.
+121        node = Node(
+122            name=f"{scope_name}.{column_name}" if scope_name else column_name,
+123            source=source,
+124            expression=select,
+125            alias=alias or "",
+126        )
+127        if upstream:
+128            upstream.downstream.append(node)
+129
+130        # Find all columns that went into creating this one to list their lineage nodes.
+131        for c in set(select.find_all(exp.Column)):
+132            table = c.table
+133            source = scope.sources.get(table)
+134
+135            if isinstance(source, Scope):
+136                # The table itself came from a more specific scope. Recurse into that one using the unaliased column name.
+137                to_node(
+138                    c.name, scope=source, scope_name=table, upstream=node, alias=aliases.get(table)
+139                )
+140            else:
+141                # The source is not a scope - we've reached the end of the line. At this point, if a source is not found
+142                # it means this column's lineage is unknown. This can happen if the definition of a source used in a query
+143                # is not passed into the `sources` map.
+144                source = source or exp.Placeholder()
+145                node.downstream.append(Node(name=c.sql(), source=source, expression=source))
+146
+147        return node
+148
+149    return to_node(column if isinstance(column, str) else column.name, scope)
 
@@ -540,115 +594,115 @@
-
125class LineageHTML:
-126    """Node to HTML generator using vis.js.
-127
-128    https://visjs.github.io/vis-network/docs/network/
-129    """
-130
-131    def __init__(
-132        self,
-133        node: Node,
-134        dialect: DialectType = None,
-135        imports: bool = True,
-136        **opts: t.Any,
-137    ):
-138        self.node = node
-139        self.imports = imports
-140
-141        self.options = {
-142            "height": "500px",
-143            "width": "100%",
-144            "layout": {
-145                "hierarchical": {
-146                    "enabled": True,
-147                    "nodeSpacing": 200,
-148                    "sortMethod": "directed",
-149                },
-150            },
-151            "interaction": {
-152                "dragNodes": False,
-153                "selectable": False,
-154            },
-155            "physics": {
-156                "enabled": False,
-157            },
-158            "edges": {
-159                "arrows": "to",
-160            },
-161            "nodes": {
-162                "font": "20px monaco",
-163                "shape": "box",
-164                "widthConstraint": {
-165                    "maximum": 300,
-166                },
-167            },
-168            **opts,
-169        }
-170
-171        self.nodes = {}
-172        self.edges = []
-173
-174        for node in node.walk():
-175            if isinstance(node.expression, exp.Table):
-176                label = f"FROM {node.expression.this}"
-177                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
-178                group = 1
-179            else:
-180                label = node.expression.sql(pretty=True, dialect=dialect)
-181                source = node.source.transform(
-182                    lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>")
-183                    if n is node.expression
-184                    else n,
-185                    copy=False,
-186                ).sql(pretty=True, dialect=dialect)
-187                title = f"<pre>{source}</pre>"
-188                group = 0
-189
-190            node_id = id(node)
-191
-192            self.nodes[node_id] = {
-193                "id": node_id,
-194                "label": label,
-195                "title": title,
-196                "group": group,
-197            }
-198
-199            for d in node.downstream:
-200                self.edges.append({"from": node_id, "to": id(d)})
-201
-202    def __str__(self):
-203        nodes = json.dumps(list(self.nodes.values()))
-204        edges = json.dumps(self.edges)
-205        options = json.dumps(self.options)
-206        imports = (
-207            """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script>
-208  <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script>
-209  <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />"""
-210            if self.imports
-211            else ""
-212        )
-213
-214        return f"""<div>
-215  <div id="sqlglot-lineage"></div>
-216  {imports}
-217  <script type="text/javascript">
-218    var nodes = new vis.DataSet({nodes})
-219    nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0])
-220
-221    new vis.Network(
-222        document.getElementById("sqlglot-lineage"),
-223        {{
-224            nodes: nodes,
-225            edges: new vis.DataSet({edges})
-226        }},
-227        {options},
-228    )
-229  </script>
-230</div>"""
-231
-232    def _repr_html_(self) -> str:
-233        return self.__str__()
+            
152class LineageHTML:
+153    """Node to HTML generator using vis.js.
+154
+155    https://visjs.github.io/vis-network/docs/network/
+156    """
+157
+158    def __init__(
+159        self,
+160        node: Node,
+161        dialect: DialectType = None,
+162        imports: bool = True,
+163        **opts: t.Any,
+164    ):
+165        self.node = node
+166        self.imports = imports
+167
+168        self.options = {
+169            "height": "500px",
+170            "width": "100%",
+171            "layout": {
+172                "hierarchical": {
+173                    "enabled": True,
+174                    "nodeSpacing": 200,
+175                    "sortMethod": "directed",
+176                },
+177            },
+178            "interaction": {
+179                "dragNodes": False,
+180                "selectable": False,
+181            },
+182            "physics": {
+183                "enabled": False,
+184            },
+185            "edges": {
+186                "arrows": "to",
+187            },
+188            "nodes": {
+189                "font": "20px monaco",
+190                "shape": "box",
+191                "widthConstraint": {
+192                    "maximum": 300,
+193                },
+194            },
+195            **opts,
+196        }
+197
+198        self.nodes = {}
+199        self.edges = []
+200
+201        for node in node.walk():
+202            if isinstance(node.expression, exp.Table):
+203                label = f"FROM {node.expression.this}"
+204                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
+205                group = 1
+206            else:
+207                label = node.expression.sql(pretty=True, dialect=dialect)
+208                source = node.source.transform(
+209                    lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>")
+210                    if n is node.expression
+211                    else n,
+212                    copy=False,
+213                ).sql(pretty=True, dialect=dialect)
+214                title = f"<pre>{source}</pre>"
+215                group = 0
+216
+217            node_id = id(node)
+218
+219            self.nodes[node_id] = {
+220                "id": node_id,
+221                "label": label,
+222                "title": title,
+223                "group": group,
+224            }
+225
+226            for d in node.downstream:
+227                self.edges.append({"from": node_id, "to": id(d)})
+228
+229    def __str__(self):
+230        nodes = json.dumps(list(self.nodes.values()))
+231        edges = json.dumps(self.edges)
+232        options = json.dumps(self.options)
+233        imports = (
+234            """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script>
+235  <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script>
+236  <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />"""
+237            if self.imports
+238            else ""
+239        )
+240
+241        return f"""<div>
+242  <div id="sqlglot-lineage"></div>
+243  {imports}
+244  <script type="text/javascript">
+245    var nodes = new vis.DataSet({nodes})
+246    nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0])
+247
+248    new vis.Network(
+249        document.getElementById("sqlglot-lineage"),
+250        {{
+251            nodes: nodes,
+252            edges: new vis.DataSet({edges})
+253        }},
+254        {options},
+255    )
+256  </script>
+257</div>"""
+258
+259    def _repr_html_(self) -> str:
+260        return self.__str__()
 
@@ -668,76 +722,76 @@
-
131    def __init__(
-132        self,
-133        node: Node,
-134        dialect: DialectType = None,
-135        imports: bool = True,
-136        **opts: t.Any,
-137    ):
-138        self.node = node
-139        self.imports = imports
-140
-141        self.options = {
-142            "height": "500px",
-143            "width": "100%",
-144            "layout": {
-145                "hierarchical": {
-146                    "enabled": True,
-147                    "nodeSpacing": 200,
-148                    "sortMethod": "directed",
-149                },
-150            },
-151            "interaction": {
-152                "dragNodes": False,
-153                "selectable": False,
-154            },
-155            "physics": {
-156                "enabled": False,
-157            },
-158            "edges": {
-159                "arrows": "to",
-160            },
-161            "nodes": {
-162                "font": "20px monaco",
-163                "shape": "box",
-164                "widthConstraint": {
-165                    "maximum": 300,
-166                },
-167            },
-168            **opts,
-169        }
-170
-171        self.nodes = {}
-172        self.edges = []
-173
-174        for node in node.walk():
-175            if isinstance(node.expression, exp.Table):
-176                label = f"FROM {node.expression.this}"
-177                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
-178                group = 1
-179            else:
-180                label = node.expression.sql(pretty=True, dialect=dialect)
-181                source = node.source.transform(
-182                    lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>")
-183                    if n is node.expression
-184                    else n,
-185                    copy=False,
-186                ).sql(pretty=True, dialect=dialect)
-187                title = f"<pre>{source}</pre>"
-188                group = 0
-189
-190            node_id = id(node)
-191
-192            self.nodes[node_id] = {
-193                "id": node_id,
-194                "label": label,
-195                "title": title,
-196                "group": group,
-197            }
-198
-199            for d in node.downstream:
-200                self.edges.append({"from": node_id, "to": id(d)})
+            
158    def __init__(
+159        self,
+160        node: Node,
+161        dialect: DialectType = None,
+162        imports: bool = True,
+163        **opts: t.Any,
+164    ):
+165        self.node = node
+166        self.imports = imports
+167
+168        self.options = {
+169            "height": "500px",
+170            "width": "100%",
+171            "layout": {
+172                "hierarchical": {
+173                    "enabled": True,
+174                    "nodeSpacing": 200,
+175                    "sortMethod": "directed",
+176                },
+177            },
+178            "interaction": {
+179                "dragNodes": False,
+180                "selectable": False,
+181            },
+182            "physics": {
+183                "enabled": False,
+184            },
+185            "edges": {
+186                "arrows": "to",
+187            },
+188            "nodes": {
+189                "font": "20px monaco",
+190                "shape": "box",
+191                "widthConstraint": {
+192                    "maximum": 300,
+193                },
+194            },
+195            **opts,
+196        }
+197
+198        self.nodes = {}
+199        self.edges = []
+200
+201        for node in node.walk():
+202            if isinstance(node.expression, exp.Table):
+203                label = f"FROM {node.expression.this}"
+204                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
+205                group = 1
+206            else:
+207                label = node.expression.sql(pretty=True, dialect=dialect)
+208                source = node.source.transform(
+209                    lambda n: exp.Tag(this=n, prefix="<b>", postfix="</b>")
+210                    if n is node.expression
+211                    else n,
+212                    copy=False,
+213                ).sql(pretty=True, dialect=dialect)
+214                title = f"<pre>{source}</pre>"
+215                group = 0
+216
+217            node_id = id(node)
+218
+219            self.nodes[node_id] = {
+220                "id": node_id,
+221                "label": label,
+222                "title": title,
+223                "group": group,
+224            }
+225
+226            for d in node.downstream:
+227                self.edges.append({"from": node_id, "to": id(d)})
 
-- cgit v1.2.3