reata / sqllineage

SQL Lineage Analysis Tool powered by Python
MIT License
1.19k stars 215 forks source link

feat:reset default schema name to real schema name #525

Closed delphisharp closed 5 months ago

delphisharp commented 5 months ago

dear reata: Modify the default schema name to the real schema name

delphisharp commented 5 months ago
from networkx import DiGraph
from sqllineage.core.models import Table
g = DiGraph()
a = Table("a")
b = Table("b")
g.add_edge(a, b)
print(g.has_node(a))
print(g.has_node(b))
print(g.has_edge(a, b))

a.schema.raw_name = "main"

print(g.has_node(a))
for node in g.nodes():
    print(node.__class__, node )
    node:Table
    if node == a:
        print('equal===a')
    if node ==b:
        print('equal===b')

for node in g.nodes():
    if node.raw_name=='a' :
        if node == a:
            print('----')
            print(g.has_node(a))
            print(g.has_node(node))
            print(g.nodes)
            print('----')

print(g.has_node(b))
print(g.has_edge(a, b))
True
True
True
False
<class 'sqllineage.core.models.Table'> main.a
equal===a
<class 'sqllineage.core.models.Table'> main.b
equal===b
----
False
False
[Table: main.a, Table: main.b]
----
False
False

print(g.has_node(node)) why result is False? I'm confused

maoxingda commented 5 months ago

直接调试一步步跟进去就知道了,一个是hash值的对比,一个是内容的对比

delphisharp commented 5 months ago
from networkx import DiGraph
from sqllineage.core.models import Table
g = DiGraph()
a = Table("a")
b = Table("b")
g.add_edge(a, b)

for node in g.nodes():
    if node in g.nodes():
        print(f'find node {node.raw_name}',)
    else:
        print("can't find node {node.raw_name}" )

a.schema.raw_name='ods'

for node in g.nodes():
    if node in g.nodes():
        print(f'find node {node.raw_name}', )
    else:
        print(f"can't find node {node.raw_name}")
find node a
find node b
can't find node a
can't find node b

What I'm confused about is that getting node from nodes use for loop, but can't be found using in nodes

我感到疑惑的是, 通过for 循环得到的node,用 in nodes 找不到。不管怎么取,获取他们不应该是同一个对象吗?

maoxingda commented 5 months ago

因为 a.schema.raw_name='ods'改变了hash值,在graph中就定位不到了

maoxingda commented 5 months ago

你可以单步调试第二个for循环跟进去就知道了

maoxingda commented 5 months ago
    from icecream import ic
    from networkx import DiGraph
    from sqllineage.core.models import Table

    g = DiGraph()

    a = Table("a")
    b = Table("b")
    ic(hash(a), hash(b))
    ic(a.schema is b.schema)  # True 证明是同一个对象,所以下面的操作会影响到b
    a.schema.raw_name = "main"
    ic(hash(a), hash(b))  # hash值改变了,而graph是用hash值定位节点的
image
maoxingda commented 5 months ago

这次够清楚了吧😜😜😜

delphisharp commented 5 months ago
a.schema.raw_name='ods'
for _ in range(10):
    x = y =0
    for i in range(10):
        for node in g.nodes():
            if node in g.nodes():
                x=x+1
            else:
                y=y+1
    print(x,y )
find node a
find node b
10 10
10 10
10 10
10 10
10 10
10 10
10 10
10 10
10 10
10 10

多次执行,大概1/10 的几率,偶发性出现这个结果。 按理说无论执行多少次值应该都是 (0 20) If executed multiple times, there is about a 1/10 chance that this result will occur sporadically. It stands to reason that no matter how many times it is executed, the value should be (0 20)

conda env : python 3.10.13 networkX==3.2.1

maoxingda commented 5 months ago

这个是hash函数本身的原因,这个我也发现了,但是不太了解Python内置hash函数内部的实现,不好解释

maoxingda commented 5 months ago
image

不确定它说的对不对,但是我自己多次运行同样的代码,确实生成的hash值不同

maoxingda commented 5 months ago

根据我观察到现象:好像是进程只要启动了,那在进程结束之前,相同的对象生成的hash值都是一样的。但是如果重新启动进程,那就可能不一样啦

delphisharp commented 5 months ago

我困惑的是,都是从 networkX取的node, 从for 取到的node 居然不在 nodes里面! 这违反了我的常识。 难道不能修改 networkX的node的值? 我又踩坑了? What confuses me is that the nodes obtained from networkX are all obtained, but the nodes obtained from for are not in nodes! This goes against my common sense Can't I modify the value of networkX node? Did I step into a trap again?

maoxingda commented 5 months ago

for循环只是一个遍历,永远都能取到。但是in操作就不一定了(因为调用了hash函数)。我跑了好多次,也出来了10 10的结果。我感觉是两个不同的对象,生成的hash值碰撞了,可惜我出现这个结果的时候,没有把hash值都打出来。我加上hash值打印之后没有复现了

maoxingda commented 5 months ago

这玩意,估计得了解一下Python内置hash函数的具体实现,才能解释清楚了

maoxingda commented 5 months ago

for循环只是一个遍历,永远都能取到。但是in操作就不一定了(因为调用了hash函数)。我跑了好多次,也出来了10 10的结果。我感觉是两个不同的对象,生成的hash值碰撞了,可惜我出现这个结果的时候,没有把hash值都打出来。我加上hash值打印之后没有复现了

这个地方确切地说应该是同一个对象,在对象内容(schema)被修改之后,生成的hash值与修改之前,发生了hash碰撞

delphisharp commented 5 months ago

g.add_node

Notes

    A hashable object is one that can be used as a key in a Python
    dictionary. This includes strings, numbers, tuples of strings
    and numbers, etc.

    On many platforms hashable items also include mutables such as
    NetworkX Graphs, though one should be careful that the hash
    doesn't change on mutables.
    """

networkX的内部确实用的hashable, 当 node 发生变更时, hash 不会自动变化。所以 in nodes 方法 通过hash 查找就会查不到。 嗯嗯嗯,打开了新世界。 所以每次用新库都是万分小心。。。

NetworkX does use hashable internally. When node changes, hash value will not change automatically. Therefore, the in nodes method cannot be found through hash search. Yeah, yeah, a new world has opened up. So be extremely careful every time you use a new library. . .

maoxingda commented 5 months ago

https://www.cnblogs.com/mollyviron/p/13207765.html

delphisharp commented 5 months ago

for循环只是一个遍历,永远都能取到。但是in操作就不一定了(因为调用了hash函数)。我跑了好多次,也出来了10 10的结果。我感觉是两个不同的对象,生成的hash值碰撞了,可惜我出现这个结果的时候,没有把hash值都打出来。我加上hash值打印之后没有复现了

这个地方确切地说应该是同一个对象,在对象内容(schema)被修改之后,生成的hash值与修改之前,发生了hash碰撞

我更倾向有缓存的说法,但我没法证明。位数也不低 , hash 这么容易碰撞的话那也太不靠谱了。

maoxingda commented 5 months ago

for循环只是一个遍历,永远都能取到。但是in操作就不一定了(因为调用了hash函数)。我跑了好多次,也出来了10 10的结果。我感觉是两个不同的对象,生成的hash值碰撞了,可惜我出现这个结果的时候,没有把hash值都打出来。我加上hash值打印之后没有复现了

这个地方确切地说应该是同一个对象,在对象内容(schema)被修改之后,生成的hash值与修改之前,发生了hash碰撞

我更倾向有缓存的说法,但我没法证明。位数也不低 , hash 这么容易碰撞的话那也太不靠谱了。

如果是缓存,那对象内容都发生变更了,缓存还不失效么,感觉也不怎么靠谱

maoxingda commented 5 months ago

for循环只是一个遍历,永远都能取到。但是in操作就不一定了(因为调用了hash函数)。我跑了好多次,也出来了10 10的结果。我感觉是两个不同的对象,生成的hash值碰撞了,可惜我出现这个结果的时候,没有把hash值都打出来。我加上hash值打印之后没有复现了

这个地方确切地说应该是同一个对象,在对象内容(schema)被修改之后,生成的hash值与修改之前,发生了hash碰撞

我更倾向有缓存的说法,但我没法证明。位数也不低 , hash 这么容易碰撞的话那也太不靠谱了。

如果是缓存,那对象内容都发生变更了,缓存还不失效么,感觉也不怎么靠谱

还有一个点,如果是缓存的话,我觉得结果应该是20 0而不是10 10

delphisharp commented 5 months ago
import string
from networkx import DiGraph
from sqllineage.core.models import Table,Schema
from secrets import token_hex,choice

class NewTable(Table):
    def __init__(self, name: str,schema: Schema = Schema(), **kwargs):
        super().__init__(name, schema, **kwargs)
        self._hash_value = int(''.join(choice(string.digits) for i in range(8)))
        # print(self._hash_value)

    def __hash__(self):
        return self._hash_value

g = DiGraph()
a = NewTable("a")
b = NewTable("b")
g.add_edge(a, b)

print('before change schema')
for node in g.nodes():
    if node in g.nodes():
        print(f'find node {node.raw_name}', )
    else:
        print("can't find node {node.raw_name}")

a.schema.raw_name = 'ods'
print('after change schema')
for node in g.nodes():
    if node in g.nodes():
        print('ok')
    else:
        print('false')

for _ in range(10):
    x = y = 0
    for _ in range(10):
        for node in g.nodes():
            if node in g.nodes():
                x = x + 1
            else:
                y = y + 1
    print(x, y)
before change schema
find node a
find node b
after change schema
ok
ok
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0

这样改感觉就OK了。 覆盖了 Table的 hash 方法,一个实体类生成一个动态的hash值。

delphisharp commented 5 months ago
import string
from networkx import DiGraph
from sqllineage.core.models import Table,Schema
from secrets import token_hex,choice

class NewTable(Table):
    def __init__(self, name: str,schema: Schema = Schema(), **kwargs):
        super().__init__(name, schema, **kwargs)
        self._hash_value = int(''.join(choice(string.digits) for i in range(8)))
        # print(self._hash_value)

    def __hash__(self):
        return self._hash_value

g = DiGraph()
a = NewTable("a")
b = NewTable("b")
g.add_edge(a, b)

print('before change schema')
for node in g.nodes():
    if node in g.nodes():
        print(f'find node {node.raw_name}', )
    else:
        print("can't find node {node.raw_name}")

a.schema.raw_name = 'ods'
print('after change schema')
for node in g.nodes():
    if node in g.nodes():
        print('ok')
    else:
        print('false')

for _ in range(10):
    x = y = 0
    for _ in range(10):
        for node in g.nodes():
            if node in g.nodes():
                x = x + 1
            else:
                y = y + 1
    print(x, y)
before change schema
find node a
find node b
after change schema
ok
ok
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0
20 0

这样改感觉就OK了。 覆盖了 Table的 hash 方法,一个实体类生成一个动态的hash值。

这样改的话,当 holder.node 发生变更时, networkX的 node 同步变更还保持了关联。

Schema() Column 的hash 也是走的raw_name, 想想这样是否合理? 保证了唯一性?也同时导致了 输入输出都是一个表的情况,无法解析血缘。 我是认为 每个SQL涉及的对象,都是一个节点,不要合并。 在结果输出中合并,而非在处理过程中。

If changed in this way, when holder.node changes, networkX's node synchronization changes will still remain relevant.

The hash of Schema() Column also uses raw_name. Is this reasonable? Uniqueness guaranteed? It also leads to the situation where the input and output are both tables, making it impossible to analyze blood relationships. I think that every object involved in SQL is a node and should not be merged. Merged in the resulting output, not during processing.

maoxingda commented 5 months ago

你这相当于自己实现了hash函数,你这个实现方法hash碰撞会不会很高

maoxingda commented 5 months ago

还有你这个hash实现跟对象内容完全没关系,如果遇到相同内容的两个不同对象,需要hash值一样的时候,咋搞

delphisharp commented 5 months ago

你这相当于自己实现了hash函数,你这个实现方法hash碰撞会不会很高

参见 https://docs.python.org/zh-cn/3.10/library/secrets.html 我觉得可用度很高。 当然位数可以再扩大点。 都号称 secrets 模块是操作系统提供的最安全地随机性来源

delphisharp commented 5 months ago

还有你这个hash实现跟对象内容完全没关系,如果遇到相同内容的两个不同对象,需要hash值一样的时候,咋搞

就是我刚才说的问题,SQL语句里面的对象,如果重复,在图里面,应该是一个还是多个的问题。 如果我重新开发,我会用多个对象,方便完整的表达前后上下文。至少图信息是可以看得懂的,为后期的语法优化,语法改写提供基础。 现在图 如果重复只有一个对象,通过图去看、调试时,并不直观。

当然,就目前情况下,怎么做我没主意,毕竟对SQLLineage了解不透彻。

maoxingda commented 5 months ago

脱离sqllineage我感觉一个对象的hash值跟这个对象内容完全没关系,应该不是最佳实践。至少我没见过这样的实现

delphisharp commented 5 months ago

在我的设想, insert select 语句,拆解为 insert 部分和 select 部分。其中 select 部分是个 递归类(自包含)的类,每个子查询就是个 select, 每个最底层的select 引入元数据信息 生成 输入和输出,
每个 select 的 上层, 引入输出,生成元数据,生成 自己的输出和输入)以此类推 生成整体的 select 最后生成完整的 树形关系, 再转为 图或者 直接生成结果并缓存起来。

maoxingda commented 5 months ago

你这个想法,我也有,但是没去实践。目前的血缘解析是由外而内的,我也在想由内而外(也就是先解析最内层查询)会不会是更好的设计

delphisharp commented 5 months ago

脱离sqllineage我感觉一个对象的hash值跟这个对象内容完全没关系,应该不是最佳实践。至少我没见过这样的实现

正确的做法,应该是在 networkX中,node 仅仅是个 id,不是对象,其他所有业务信息都在 propery里面。 现在已经这样实现了,只能顺着改了。

我还担心这样有没有内存泄漏的问题, 每次新增了新的Schema,然后hash 也一样;那新增的表对象,他的schema是指向之前的还是新增出来的? 那多出来schema类,没地方用?跑哪里去? 超过我的能力了。。。

delphisharp commented 5 months ago

你这个想法,我也有,但是没去实践。目前的血缘解析是由外而内的,我也在想由内而外(也就是先解析最内层查询)会不会是更好的设计

是的,这样输入和输出的关联性比较好做 。我想的是用 广度搜索生成队列顺序执行。

maoxingda commented 5 months ago

可以尝试