├── output ├── .gitkeep ├── table-lineage.dot └── column-lineage.dot ├── images ├── column-lineage.png ├── table-lineage.png └── query-column-lineage.png ├── profiles_db_template.conf ├── sql ├── query-access-history-json.sql ├── query-access-history-simple.sql ├── query-access-history.sql ├── create-script.sql └── create-script-annotated.sql ├── .gitignore ├── README.md ├── data-lineage.py └── LICENSE /output/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/column-lineage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristiscu/data-lineage-tool/HEAD/images/column-lineage.png -------------------------------------------------------------------------------- /images/table-lineage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristiscu/data-lineage-tool/HEAD/images/table-lineage.png -------------------------------------------------------------------------------- /images/query-column-lineage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristiscu/data-lineage-tool/HEAD/images/query-column-lineage.png -------------------------------------------------------------------------------- /profiles_db_template.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | account = your-snowflake-account 3 | user = your-username 4 | 5 | [connection-1] 6 | account = ... 7 | user = ... 8 | 9 | [connection-2] 10 | account = ... 11 | user = ... 12 | -------------------------------------------------------------------------------- /output/table-lineage.dot: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | rankdir=LR; 3 | _START_ [label=""] 4 | T1 [shape=Mrecord label="T1"]; 5 | T6 [shape=Mrecord label="T6"]; 6 | T2 [shape=Mrecord label="T2"]; 7 | S1 [shape=Mrecord label="S1"]; 8 | T3 [shape=Mrecord label="T3"]; 9 | T4 [shape=Mrecord label="T4"]; 10 | T7 [shape=Mrecord label="T7"]; 11 | _START_ -> T1 [tooltip=""]; 12 | _START_ -> S1 [tooltip="copy into @S1 from T1;"]; 13 | _START_ -> T3 [tooltip=""]; 14 | T1 -> T6 [tooltip=""]; 15 | T1 -> T2 [tooltip=""]; 16 | T1 -> T4 [tooltip=""]; 17 | T6 -> T7 [tooltip=""]; 18 | } -------------------------------------------------------------------------------- /sql/query-access-history-json.sql: -------------------------------------------------------------------------------- 1 | select qh.query_text, 2 | ah.objects_modified, ah.object_modified_by_ddl, 3 | ah.direct_objects_accessed, ah.base_objects_accessed 4 | from snowflake.account_usage.access_history ah 5 | left join snowflake.account_usage.query_history qh 6 | on ah.query_id = qh.query_id 7 | where ah.objects_modified::string like '%{{database}}%' 8 | or ah.object_modified_by_ddl::string like '%{{database}}%' 9 | or ah.direct_objects_accessed::string like '%{{database}}%' 10 | or ah.base_objects_accessed::string like '%{{database}}%' 11 | order by ah.query_start_time; 12 | -------------------------------------------------------------------------------- /sql/query-access-history-simple.sql: -------------------------------------------------------------------------------- 1 | select qh.query_text, 2 | trim(ifnull(src.value:objectName::string, '') 3 | || '.' || ifnull(src.value:columnName::string, ''), '.') as source, 4 | trim(ifnull(om.value:objectName::string, '') 5 | || '.' || ifnull(col.value:columnName::string, ''), '.') as target, 6 | ah.objects_modified 7 | from snowflake.account_usage.access_history ah 8 | left join snowflake.account_usage.query_history qh 9 | on ah.query_id = qh.query_id, 10 | lateral flatten(input => objects_modified) om, 11 | lateral flatten(input => om.value: "columns", outer => true) col, 12 | lateral flatten(input => col.value:directSources, outer => true) src 13 | where ifnull(src.value:objectName::string, '') like '{{database}}%' 14 | or ifnull(om.value:objectName::string, '') like '{{database}}%' 15 | order by ah.query_start_time; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | # custom 52 | build/ 53 | *.spec 54 | profiles_db.conf 55 | venv/ -------------------------------------------------------------------------------- /sql/query-access-history.sql: -------------------------------------------------------------------------------- 1 | select qh.query_text, 2 | trim(ifnull(src.value:objectName::string, '') 3 | || '.' || ifnull(src.value:columnName::string, ''), '.') as source, 4 | trim(ifnull(om.value:objectName::string, '') 5 | || '.' || ifnull(col.value:columnName::string, ''), '.') as target, 6 | obj.value:objectName::string as sourceAccessed, 7 | ah.objects_modified, ah.direct_objects_accessed, ah.base_objects_accessed 8 | from snowflake.account_usage.access_history ah 9 | left join snowflake.account_usage.query_history qh 10 | on ah.query_id = qh.query_id, 11 | lateral flatten(input => objects_modified) om, 12 | lateral flatten(input => om.value: "columns", outer => true) col, 13 | lateral flatten(input => col.value:directSources, outer => true) src, 14 | lateral flatten(input => direct_objects_accessed, outer => true) obj 15 | where ifnull(src.value:objectName::string, '') like '{{database}}%' 16 | or ifnull(om.value:objectName::string, '') like '{{database}}%' 17 | or ifnull(obj.value:objectName::string, '') like '{{database}}%' 18 | order by ah.query_start_time; -------------------------------------------------------------------------------- /sql/create-script.sql: -------------------------------------------------------------------------------- 1 | -- create test database and schema 2 | create or replace database test_db; 3 | create schema test_schema; 4 | 5 | -- create new table, then populate it with dynamic content 6 | create table T1(content variant); 7 | insert into T1(content) select parse_json('{"name": "A", "id":1}'); 8 | 9 | -- create new table, then populate it with content of the previous table 10 | create table T6(content variant); 11 | insert into T6 select * from T1; 12 | 13 | -- create new table, and directly populate it with T1 content 14 | create table T2 as select content:"name" as name, content:"id" as id from T1; 15 | 16 | -- create new user stage, then copy T1's content here 17 | create stage S1; 18 | copy into @S1 from T1; 19 | 20 | -- create new table, then copy from the stage content 21 | create table T3(customer_info variant); 22 | copy into T3 from @S1; 23 | 24 | -- create new table, then add T1's content 25 | create table T4(name string, id string, address string); 26 | insert into T4(name, id) select content:"name", content:"id" from T1; 27 | 28 | -- create new table, and directly populate it with T6 content 29 | create table T7 as select * from T6; 30 | -------------------------------------------------------------------------------- /output/column-lineage.dot: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | rankdir=LR; 3 | _START_ [label=""] 4 | T1 [shape=Mrecord label=" T1| CONTENT"]; 5 | T6 [shape=Mrecord label=" T6| CONTENT"]; 6 | T2 [shape=Mrecord label=" T2| ID| NAME"]; 7 | S1 [shape=Mrecord label=" S1"]; 8 | T3 [shape=Mrecord label=" T3| CUSTOMER_INFO"]; 9 | T4 [shape=Mrecord label=" T4| ID| NAME"]; 10 | T7 [shape=Mrecord label=" T7| CONTENT"]; 11 | _START_ -> T1:CONTENT [tooltip="insert into T1(content) select parse_json('{'name': 'A', 'id':1}');"]; 12 | _START_ -> S1 [tooltip="copy into @S1 from T1;"]; 13 | _START_ -> T3:CUSTOMER_INFO [tooltip="copy into T3 from @S1;"]; 14 | T1:CONTENT -> T6:CONTENT [tooltip="insert into T6 select * from T1;"]; 15 | T1:CONTENT -> T2:ID [tooltip="create table T2 as select content:'name' as name, content:'id' as id from T1;"]; 16 | T1:CONTENT -> T2:NAME [tooltip="create table T2 as select content:'name' as name, content:'id' as id from T1;"]; 17 | T1:CONTENT -> T4:ID [tooltip="insert into T4(name, id) select content:'name', content:'id' from T1;"]; 18 | T1:CONTENT -> T4:NAME [tooltip="insert into T4(name, id) select content:'name', content:'id' from T1;"]; 19 | T6:CONTENT -> T7:CONTENT [tooltip="create table T7 as select * from T6;"]; 20 | } -------------------------------------------------------------------------------- /sql/create-script-annotated.sql: -------------------------------------------------------------------------------- 1 | -- [object_modified_by_ddl] 2 | create or replace database test_db; 3 | 4 | -- [object_modified_by_ddl] 5 | create schema test_schema; 6 | 7 | -- [object_modified_by_ddl] 8 | create table T1(content variant); 9 | 10 | -- --> T1.content [direct_objects_accessed] 11 | insert into T1(content) select parse_json('{"name": "A", "id":1}'); 12 | 13 | -- [object_modified_by_ddl] 14 | create table T6(content variant); 15 | 16 | -- T1.content --> T6.content [direct_objects_accessed] 17 | insert into T6 select * from T1; 18 | 19 | -- T1.name/id --> T2.name/id [direct_objects_accessed] 20 | create table T2 as select content:"name" as name, content:"id" as id from T1; 21 | 22 | -- [object_modified_by_ddl] 23 | create stage S1; 24 | 25 | -- T1 --> S1 [direct_objects_accessed] 26 | copy into @S1 from T1; 27 | 28 | -- [object_modified_by_ddl] 29 | create table T3(customer_info variant); 30 | 31 | -- S1 --> T3 [direct_objects_accessed] 32 | copy into T3 from @S1; 33 | 34 | -- [object_modified_by_ddl] 35 | create table T4(name string, id string, address string); 36 | 37 | -- T1.content --> T4.content [direct_objects_accessed] 38 | insert into T4(name, id) select content:"name", content:"id" from T1; 39 | 40 | -- T6.content --> T7.content [direct_objects_accessed] 41 | create table T7 as select * from T6; 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Data Lineage Viewer for Snowflake 2 | ================================= 3 | 4 | Queries the [ACCESS_HISTORY](https://docs.snowflake.com/en/sql-reference/account-usage/access_history) and [QUERY_HISTORY](https://docs.snowflake.com/en/sql-reference/account-usage/query_history) views, from the SNOWFLAKE.ACCOUNT_USAGE schema, and generates two interactive GraphViz visual diagrams online, in a visual editor: 5 | 6 | * one for [**column lineage**](http://magjac.com/graphviz-visual-editor/?dot=digraph%20structs%20%7B%0A%09rankdir%3DLR%3B%0A%09_START_%20%5Blabel%3D%22%22%5D%0A%09T1%20%5Bshape%3DMrecord%20label%3D%22%3CT1%3E%20T1%7C%3CCONTENT%3E%20CONTENT%22%5D%3B%0A%09T6%20%5Bshape%3DMrecord%20label%3D%22%3CT6%3E%20T6%7C%3CCONTENT%3E%20CONTENT%22%5D%3B%0A%09T2%20%5Bshape%3DMrecord%20label%3D%22%3CT2%3E%20T2%7C%3CID%3E%20ID%7C%3CNAME%3E%20NAME%22%5D%3B%0A%09S1%20%5Bshape%3DMrecord%20label%3D%22%3CS1%3E%20S1%22%5D%3B%0A%09T3%20%5Bshape%3DMrecord%20label%3D%22%3CT3%3E%20T3%7C%3CCUSTOMER_INFO%3E%20CUSTOMER_INFO%22%5D%3B%0A%09T4%20%5Bshape%3DMrecord%20label%3D%22%3CT4%3E%20T4%7C%3CID%3E%20ID%7C%3CNAME%3E%20NAME%22%5D%3B%0A%09T7%20%5Bshape%3DMrecord%20label%3D%22%3CT7%3E%20T7%7C%3CCONTENT%3E%20CONTENT%22%5D%3B%0A%09_START_%20-%3E%20T1%3ACONTENT%20%5Btooltip%3D%22insert%20into%20T1%28content%29%20select%20parse_json%28%27%7B%27name%27%3A%20%27A%27%2C%20%27id%27%3A1%7D%27%29%3B%22%5D%3B%0A%09_START_%20-%3E%20S1%20%5Btooltip%3D%22copy%20into%20%40S1%20from%20T1%3B%22%5D%3B%0A%09_START_%20-%3E%20T3%3ACUSTOMER_INFO%20%5Btooltip%3D%22copy%20into%20T3%20from%20%40S1%3B%22%5D%3B%0A%09T1%3ACONTENT%20-%3E%20T6%3ACONTENT%20%5Btooltip%3D%22insert%20into%20T6%20select%20%2A%20from%20T1%3B%22%5D%3B%0A%09T1%3ACONTENT%20-%3E%20T2%3AID%20%5Btooltip%3D%22create%20table%20T2%20as%20select%20content%3A%27name%27%20as%20name%2C%20content%3A%27id%27%20as%20id%20from%20T1%3B%22%5D%3B%0A%09T1%3ACONTENT%20-%3E%20T2%3ANAME%20%5Btooltip%3D%22create%20table%20T2%20as%20select%20content%3A%27name%27%20as%20name%2C%20content%3A%27id%27%20as%20id%20from%20T1%3B%22%5D%3B%0A%09T1%3ACONTENT%20-%3E%20T4%3AID%20%5Btooltip%3D%22insert%20into%20T4%28name%2C%20id%29%20select%20content%3A%27name%27%2C%20content%3A%27id%27%20from%20T1%3B%22%5D%3B%0A%09T1%3ACONTENT%20-%3E%20T4%3ANAME%20%5Btooltip%3D%22insert%20into%20T4%28name%2C%20id%29%20select%20content%3A%27name%27%2C%20content%3A%27id%27%20from%20T1%3B%22%5D%3B%0A%09T6%3ACONTENT%20-%3E%20T7%3ACONTENT%20%5Btooltip%3D%22create%20table%20T7%20as%20select%20%2A%20from%20T6%3B%22%5D%3B%0A%7D) 7 | * the other for [**table lineage**](http://magjac.com/graphviz-visual-editor/?dot=digraph%20structs%20%7B%0A%09rankdir%3DLR%3B%0A%09_START_%20%5Blabel%3D%22%22%5D%0A%09T1%20%5Bshape%3DMrecord%20label%3D%22T1%22%5D%3B%0A%09T6%20%5Bshape%3DMrecord%20label%3D%22T6%22%5D%3B%0A%09T2%20%5Bshape%3DMrecord%20label%3D%22T2%22%5D%3B%0A%09S1%20%5Bshape%3DMrecord%20label%3D%22S1%22%5D%3B%0A%09T3%20%5Bshape%3DMrecord%20label%3D%22T3%22%5D%3B%0A%09T4%20%5Bshape%3DMrecord%20label%3D%22T4%22%5D%3B%0A%09T7%20%5Bshape%3DMrecord%20label%3D%22T7%22%5D%3B%0A%09_START_%20-%3E%20T1%20%5Btooltip%3D%22%22%5D%3B%0A%09_START_%20-%3E%20S1%20%5Btooltip%3D%22copy%20into%20%40S1%20from%20T1%3B%22%5D%3B%0A%09_START_%20-%3E%20T3%20%5Btooltip%3D%22%22%5D%3B%0A%09T1%20-%3E%20T6%20%5Btooltip%3D%22%22%5D%3B%0A%09T1%20-%3E%20T2%20%5Btooltip%3D%22%22%5D%3B%0A%09T1%20-%3E%20T4%20%5Btooltip%3D%22%22%5D%3B%0A%09T6%20-%3E%20T7%20%5Btooltip%3D%22%22%5D%3B%0A%7D) 8 | 9 | The generated DOT Graphviz models are also saved in the output/ folder. 10 | 11 | # The Query Result for the Lineage Graph 12 | 13 | To repro, you may run the **sql/create-script.sql** file in Snowflake, which is similar to the one described [in the documentation samples](https://docs.snowflake.com/en/user-guide/access-history#example-column-lineage). Wait a few hours, until the changes are propagated in the ACCOUNT_USAGE views. 14 | 15 | Then run the **sql/query-access-history.sql** query to get lineage info about the created test tables. {{database}} must be replaced by TEST_DB for our use case here: 16 | 17 | ![Table Lineage](/images/query-column-lineage.png) 18 | 19 | # The Column Lineage Graph 20 | 21 | This diagram shows how data moved between the table columns in the TEST_DB database. You may go over the links and the SQL query that made that transformation appears as tooltip. 22 | 23 | ![Table Lineage](/images/column-lineage.png) 24 | 25 | # The Table Lineage Graph 26 | 27 | The table lineage graph is a simplified derived diagram, in which there are no columns, and all column dependencies appear as one single link at the container table level: 28 | 29 | ![Table Lineage](/images/table-lineage.png) 30 | 31 | # Database Profile File 32 | 33 | To connect to Snowflake, create a **profiles_db.conf** copy of the **profiles_db_template.conf** file, and customize it with your own Snowflake connection parameters, the user name and the account. Your top [default] profile is the active profile, considered by our tool. 34 | 35 | Save your password in a SNOWFLAKE_PASSWORD local environment variable. Never add the password or any other sensitive information to your code or to profile files. All names must be case sensitive, with no quotes. 36 | 37 | # CLI Executable File 38 | 39 | Without an executable, you can use the source file directly: 40 | 41 | **python data-lineage.py TEST_DB** 42 | 43 | To compile into a CLI executable: 44 | 45 | **pip install pyinstaller** 46 | **pyinstaller --onefile data-lineage.py** 47 | **dist/data-lineage TEST_DB** 48 | -------------------------------------------------------------------------------- /data-lineage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created By: Cristian Scutaru 3 | Creation Date: Aug 2023 4 | Company: XtractPro Software 5 | """ 6 | 7 | import os, webbrowser, configparser, argparse, urllib.parse 8 | import snowflake.connector 9 | 10 | def addTable(tables, isSource, name, alt=None): 11 | 12 | # source object is sometimes empty, but given by source Accessed 13 | if alt is not None and name is None: name = alt 14 | if name is None or len(name) == 0: 15 | name = "_START_" if isSource else "_END_" 16 | 17 | # skip database and schema name prefix 18 | parts = name.split('.') 19 | if (len(parts)) == 3: name = parts[2] 20 | elif (len(parts)) == 4: name = parts[2] + '.' + parts[3] 21 | 22 | # add table if not there 23 | parts = name.split('.') 24 | tableName = parts[0] 25 | if tableName not in tables: tables[tableName] = Table(tableName) 26 | table = tables[tableName] 27 | 28 | # add table column if not there 29 | columnName = parts[1] if len(parts) > 1 else None 30 | if columnName is not None and columnName not in table.columns: 31 | table.columns[columnName] = Column(columnName, tableName) 32 | return name 33 | 34 | def addLink(tables, sourceName, targetName, tooltip): 35 | 36 | # create link between columns or tables 37 | link = Link(sourceName, targetName, tooltip) 38 | key = link.getLinkKey() 39 | 40 | # add link to source table or column 41 | parts = sourceName.split('.') 42 | sourceTableName = parts[0] 43 | table = tables[sourceTableName] 44 | if len(parts) == 1: table.links[key] = link 45 | else: table.columns[parts[1]].links[key] = link 46 | 47 | # add redundant link between tables only 48 | targetTableName = targetName.split('.')[0] 49 | key = f'{sourceTableName} -> {targetTableName}' 50 | if key not in table.links: 51 | link = Link(sourceTableName, targetTableName) 52 | table.links[key] = link 53 | return None 54 | 55 | class Table: 56 | def __init__(self, name): 57 | self.columns = {} 58 | self.links = {} 59 | self.name = name 60 | 61 | def getNodeSignature(self, skipColumns=False): 62 | if self.name == "_START_" or self.name == "_END_": 63 | return f'\n\t{self.name} [label=""]' 64 | else: 65 | return f'\n\t{self.name} [shape=Mrecord label="{self.getColumnNames(skipColumns)}"];' 66 | 67 | def getColumnNames(self, skipColumns=False): 68 | if skipColumns: return self.name 69 | s = f"<{self.name}> {self.name}" 70 | for columnName in self.columns: 71 | s += f"|<{columnName}> {columnName}" 72 | return s 73 | 74 | class Column: 75 | def __init__(self, name, tableName): 76 | self.links = {} 77 | self.name = name 78 | self.fullname = tableName + "." + name 79 | 80 | class Link: 81 | def __init__(self, source, target, tooltip=""): 82 | self.source = source 83 | self.target = target 84 | self.tooltip = tooltip 85 | print(f'{self.getLinkKey()} [{tooltip}]') 86 | 87 | def getLinkSignature(self): 88 | return f'\n\t{self.getLinkKey()} [tooltip="{self.tooltip}"];' 89 | 90 | def getLinkKey(self): 91 | source = self.source.replace(".", ":") 92 | target = self.target.replace(".", ":") 93 | return f'{source} -> {target}' 94 | 95 | # create dot graph link, for GraphViz 96 | def makeLineage(tables, tablesOnly=False): 97 | 98 | # show all table nodes 99 | s = '' 100 | for tableName in tables: 101 | s += tables[tableName].getNodeSignature(tablesOnly) 102 | 103 | # show all links 104 | for tableName in tables: 105 | table = tables[tableName] 106 | 107 | # show only links between tables if tablesOnly 108 | for linkKey in table.links: 109 | link = table.links[linkKey] 110 | if (tablesOnly and '.' not in link.target) \ 111 | or (not tablesOnly and len(link.tooltip) > 0): 112 | s += link.getLinkSignature() 113 | 114 | # show links between columns if not tablesOnly 115 | if not tablesOnly: 116 | for columnName in table.columns: 117 | column = table.columns[columnName] 118 | for linkKey in column.links: 119 | link = column.links[linkKey] 120 | s += link.getLinkSignature() 121 | 122 | # make Graphviz dot graph and save in file 123 | s = (f'digraph structs {{\n\trankdir=LR;{s}\n}}') 124 | print(s) 125 | filename = f"output/table-lineage.dot" if tablesOnly else f"output/column-lineage.dot" 126 | with open(filename, "w") as file: 127 | file.write(s) 128 | 129 | # URL encode as query string for remote Graphviz Visual Editor 130 | s = urllib.parse.quote(s) 131 | s = f'http://magjac.com/graphviz-visual-editor/?dot={s}' 132 | print(s) 133 | return s 134 | 135 | def main(argv): 136 | 137 | # get database name 138 | argparser = argparse.ArgumentParser() 139 | argparser.add_argument('database') 140 | args = argparser.parse_args() 141 | 142 | # connect to Snowflake 143 | parser = configparser.ConfigParser() 144 | parser.read("profiles_db.conf") 145 | 146 | con = snowflake.connector.connect( 147 | account=parser.get("default", "account"), 148 | user=parser.get("default", "user"), 149 | password = os.getenv('SNOWFLAKE_PASSWORD')) 150 | cur = con.cursor() 151 | 152 | # load column lineage for the direct objects, for the database 153 | with open(f"sql/query-access-history.sql", "r") as file: 154 | sql = file.read() 155 | sql = sql.replace("{{database}}", args.database) 156 | results = cur.execute(sql).fetchall() 157 | con.close() 158 | 159 | # separate result entries into a graph object model 160 | tables = {}; 161 | for row in results: 162 | addLink(tables, 163 | sourceName=addTable(tables, True, str(row[1]), str(row[3])), 164 | targetName=addTable(tables, False, str(row[2])), 165 | tooltip=str(row[0]).replace('"', "'")) 166 | 167 | # generate two online GraphViz interactive images 168 | webbrowser.open(makeLineage(tables, False)) 169 | webbrowser.open(makeLineage(tables, True)) 170 | 171 | if __name__ == "__main__": 172 | main('') 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | --------------------------------------------------------------------------------