sqlleaf 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. sqlleaf-0.0.2/PKG-INFO +229 -0
  2. sqlleaf-0.0.2/README.md +216 -0
  3. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/pyproject.toml +9 -7
  4. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/__init__.py +1 -0
  5. sqlleaf-0.0.2/sqlleaf/dialects/__init__.py +1 -0
  6. sqlleaf-0.0.2/sqlleaf/dialects/plpgsql/__init__.py +1 -0
  7. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_classes.py +1 -1
  8. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_generator.py +2 -2
  9. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_parser.py +4 -4
  10. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/exception.py +2 -2
  11. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/lineage.py +8 -65
  12. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/mappings.py +94 -34
  13. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/context.py +1 -1
  14. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/base.py +1 -1
  15. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/__init__.py +51 -0
  16. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/base.py +12 -21
  17. sqlleaf-0.0.2/sqlleaf/models/query/block.py +35 -0
  18. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/call.py +4 -4
  19. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/copy.py +2 -2
  20. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/ctas.py +6 -6
  21. sqlleaf-0.0.2/sqlleaf/models/query/declare_item.py +33 -0
  22. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/execute.py +2 -2
  23. sqlleaf-0.0.2/sqlleaf/models/query/execute_dynamic.py +75 -0
  24. sqlleaf-0.0.2/sqlleaf/models/query/fetch.py +25 -0
  25. sqlleaf-0.0.2/sqlleaf/models/query/for_in.py +31 -0
  26. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/holder.py +2 -2
  27. sqlleaf-0.0.2/sqlleaf/models/query/if_.py +35 -0
  28. sqlleaf-0.0.2/sqlleaf/models/query/loop.py +31 -0
  29. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/multitable_insert.py +1 -1
  30. sqlleaf-0.0.2/sqlleaf/models/query/open.py +54 -0
  31. sqlleaf-0.0.2/sqlleaf/models/query/perform.py +12 -0
  32. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/prepare.py +1 -1
  33. sqlleaf-0.0.2/sqlleaf/models/query/procedure.py +124 -0
  34. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/set.py +43 -11
  35. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/stage.py +1 -1
  36. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/table.py +10 -10
  37. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/type.py +1 -1
  38. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/unload.py +1 -1
  39. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/user_defined_function.py +20 -28
  40. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/view.py +3 -3
  41. sqlleaf-0.0.2/sqlleaf/models/query/while_.py +30 -0
  42. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/path.py +8 -8
  43. sqlleaf-0.0.2/sqlleaf/processors/collector/classify.py +115 -0
  44. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/collector/collector.py +178 -284
  45. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/base.py +50 -5
  46. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/generator.py +61 -8
  47. sqlleaf-0.0.2/sqlleaf/processors/substitutor/substitute.py +781 -0
  48. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/__init__.py +6 -2
  49. sqlleaf-0.0.2/sqlleaf/processors/transformer/assignment.py +18 -0
  50. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/base.py +87 -88
  51. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/call.py +2 -1
  52. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/copy.py +2 -0
  53. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/ctas.py +2 -0
  54. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/delete.py +2 -0
  55. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/execute.py +2 -1
  56. sqlleaf-0.0.2/sqlleaf/processors/transformer/execute_dynamic.py +10 -0
  57. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/__init__.py +5 -1
  58. sqlleaf-0.0.2/sqlleaf/processors/transformer/expressions/format.py +251 -0
  59. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/row.py +12 -12
  60. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/values.py +115 -89
  61. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/insert.py +3 -1
  62. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/merge.py +2 -0
  63. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/multitable_insert.py +2 -0
  64. sqlleaf-0.0.2/sqlleaf/processors/transformer/open.py +78 -0
  65. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/replace.py +3 -1
  66. sqlleaf-0.0.2/sqlleaf/processors/transformer/transformer.py +183 -0
  67. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/unload.py +2 -0
  68. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/update.py +2 -0
  69. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/values.py +3 -2
  70. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/__init__.py +2 -2
  71. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/athena.py +1 -1
  72. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/base.py +2 -2
  73. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/postgres.py +2 -2
  74. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/__init__.py +2 -0
  75. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/expression.py +25 -4
  76. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/graph.py +3 -3
  77. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/helpers.py +2 -2
  78. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/iterators.py +17 -6
  79. sqlleaf-0.0.2/sqlleaf.egg-info/PKG-INFO +229 -0
  80. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf.egg-info/SOURCES.txt +18 -5
  81. sqlleaf-0.0.2/sqlleaf.egg-info/requires.txt +2 -0
  82. sqlleaf-0.0.1/PKG-INFO +0 -46
  83. sqlleaf-0.0.1/README.md +0 -32
  84. sqlleaf-0.0.1/sqlleaf/models/query/procedure.py +0 -72
  85. sqlleaf-0.0.1/sqlleaf/processors/collector/substitute.py +0 -396
  86. sqlleaf-0.0.1/sqlleaf/processors/generator/__init__.py +0 -0
  87. sqlleaf-0.0.1/sqlleaf/processors/transformer/transformer.py +0 -141
  88. sqlleaf-0.0.1/sqlleaf/processors/transformer/udf.py +0 -235
  89. sqlleaf-0.0.1/sqlleaf/system_tables/load.py +0 -0
  90. sqlleaf-0.0.1/sqlleaf.egg-info/PKG-INFO +0 -46
  91. sqlleaf-0.0.1/sqlleaf.egg-info/requires.txt +0 -3
  92. sqlleaf-0.0.1/tests/test_hooks.py +0 -64
  93. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/LICENSE +0 -0
  94. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/setup.cfg +0 -0
  95. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_dialect.py +0 -0
  96. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_tokenizer.py +0 -0
  97. {sqlleaf-0.0.1/sqlleaf/dialects → sqlleaf-0.0.2/sqlleaf/models}/__init__.py +0 -0
  98. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/hook.py +0 -0
  99. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/__init__.py +0 -0
  100. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/column.py +0 -0
  101. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/dynamodb.py +0 -0
  102. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/function.py +0 -0
  103. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/interval.py +0 -0
  104. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/json.py +0 -0
  105. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/literal.py +0 -0
  106. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/null.py +0 -0
  107. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/pivot.py +0 -0
  108. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/program.py +0 -0
  109. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/sequence.py +0 -0
  110. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/star.py +0 -0
  111. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/stream.py +0 -0
  112. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/user_defined_function.py +0 -0
  113. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/var.py +0 -0
  114. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/variable.py +0 -0
  115. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/window.py +0 -0
  116. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/database.py +0 -0
  117. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/delete.py +0 -0
  118. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/insert.py +0 -0
  119. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/merge.py +0 -0
  120. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/put.py +0 -0
  121. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/replace.py +0 -0
  122. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/schema.py +0 -0
  123. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/select.py +0 -0
  124. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/sequence.py +0 -0
  125. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/trigger.py +0 -0
  126. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/update.py +0 -0
  127. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/values.py +0 -0
  128. {sqlleaf-0.0.1/sqlleaf/dialects/plpgsql → sqlleaf-0.0.2/sqlleaf/processors}/__init__.py +0 -0
  129. {sqlleaf-0.0.1/sqlleaf/models → sqlleaf-0.0.2/sqlleaf/processors/collector}/__init__.py +0 -0
  130. {sqlleaf-0.0.1/sqlleaf/processors → sqlleaf-0.0.2/sqlleaf/processors/generator}/__init__.py +0 -0
  131. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/__init__.py +0 -0
  132. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/athena.py +0 -0
  133. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/mysql.py +0 -0
  134. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/postgres.py +0 -0
  135. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/redshift.py +0 -0
  136. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/snowflake.py +0 -0
  137. {sqlleaf-0.0.1/sqlleaf/processors/collector → sqlleaf-0.0.2/sqlleaf/processors/substitutor}/__init__.py +0 -0
  138. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/functional_notation.py +0 -0
  139. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/typing.py +0 -0
  140. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf.egg-info/dependency_links.txt +0 -0
  141. {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf.egg-info/top_level.txt +0 -0
sqlleaf-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlleaf
3
+ Version: 0.0.2
4
+ Summary: Trace your data
5
+ Author-email: zac <zac@sqlleaf.io>
6
+ Project-URL: Homepage, https://github.com/sqlleaf-io/sqlleaf
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: networkx==3.6.1
11
+ Requires-Dist: sqlglot==30.22.0
12
+ Dynamic: license-file
13
+
14
+ # sqlleaf
15
+ ## Trace your data
16
+ sqlleaf is a tool to help you to understand how your data flows across your data systems by producing detailed, column-level lineage across any SQL query and dialect.
17
+
18
+ It extends beyond traditional lineage tools by mapping relationships across all kinds of expressions, such as columns, functions, literals and variables.
19
+
20
+ ## Example
21
+ Add some SQL queries to a file:
22
+ ```sql
23
+ -- File: example.sql
24
+ CREATE TABLE users (name VARCHAR(50), birthday DATE);
25
+ CREATE TABLE logins (name VARCHAR(50));
26
+
27
+ INSERT INTO users (name, birthday)
28
+ SELECT LOWER(name) AS name, CURRENT_TIMESTAMP AS birthday
29
+ FROM logins;
30
+ ```
31
+ Add some Python code:
32
+ ```python
33
+ sql = open("example.sql").read()
34
+
35
+ import sqlleaf
36
+ lineage = sqlleaf.Lineage()
37
+ lineage.generate(sql=sql, dialect="postgres")
38
+ lineage.print_paths()
39
+ ```
40
+ View the lineage printed to the console:
41
+ ```
42
+ column[logins.name] -> function[LOWER] -> column[users.name]
43
+ function[CURRENT_TIMESTAMP] -> column[users.birthday]
44
+ ```
45
+
46
+ ## Applications
47
+ sqlleaf allows you to build a wide range of new applications on top of SQL databases, such as:
48
+ - data tracing and tagging systems, such as personally identifiable information tracking
49
+ - lineage-based SQL validators
50
+ - auditing systems and historical analysis
51
+ - identification of unused columns and tables
52
+ - support for database migration correctness
53
+ - impact analysis of changes to queries
54
+
55
+ ## Features
56
+ ### Types of queries
57
+ sqlleaf traces lineage across a wide range of DML queries, such as `INSERT`, `UPDATE`, `MERGE`, `CREATE TABLE AS`, `COPY`, `UNLOAD`, CTEs, and many more.
58
+
59
+ It parses over 30 dialects, extending functionality from the underlying SQL transpiler `sqlglot`.
60
+
61
+ Here are some basic queries as examples:
62
+ ```sql
63
+ -- Basic CTAS
64
+ CREATE TABLE dst AS SELECT * FROM src;
65
+
66
+ -- Basic UPDATE
67
+ UPDATE dst t SET first_name = 'john', age = s.age FROM src s;
68
+
69
+ -- An INSERT with a CTE (Common Table Expression)
70
+ WITH my_cte AS (SELECT 1 AS num) INSERT INTO dst (age) SELECT c.num FROM my_cte c;
71
+ ```
72
+
73
+ ### Support for dynamic SQL inside PL/pgSQL
74
+ You can trace lineage across variables and complex, dynamically constructed statements. This works by analyzing the procedure's inner statements, substituting any arguments and variables, and transforming the statements into the desired queries.
75
+
76
+ For example, given a query to calculate the row count of an arbitrary table:
77
+ ```sql
78
+ -- Define a PL/pgSQL procedure in Postgres
79
+ CREATE PROCEDURE get_maximum_id(schema_name TEXT, table_name TEXT)
80
+ AS $$
81
+ BEGIN
82
+ EXECUTE format('INSERT INTO target (my_value) SELECT MAX(id) FROM %I.%I', schema_name, table_name);
83
+ END;
84
+ $$ LANGUAGE plpgsql;
85
+
86
+ -- Call it
87
+ CALL get_maximum_id('public', 'users');
88
+ ```
89
+ sqlleaf extracts and transforms this into the statement:
90
+ ```sql
91
+ INSERT INTO target (my_value) SELECT MAX(id) FROM public.users;
92
+ ```
93
+ and then generates its lineage:
94
+ ```
95
+ column[public.users.id] -> function[MAX] -> column[target.my_value]
96
+ ```
97
+
98
+ ### User-defined functions, procedures and types
99
+ Given a user-defined function or a procedure, sqlleaf will extract the statement's inner queries so that they can be referenced by the calling query:
100
+ ```sql
101
+ -- Create the function
102
+ CREATE FUNCTION hello() RETURNS TEXT AS $$
103
+ SELECT 'Hello';
104
+ $$ LANGUAGE SQL;
105
+
106
+ -- Insert its value into a new table
107
+ CREATE TABLE dst (my_name VARCHAR);
108
+ INSERT INTO dst (my_name) SELECT hello();
109
+ ```
110
+ This will perform inline transformations to produce a new query:
111
+ ```sql
112
+ INSERT INTO dst (my_name) SELECT (SELECT 'hello') AS name;
113
+ ```
114
+ with lineage:
115
+ `literal['Hello'] -> column[dst.name]`
116
+
117
+ ### Pivot, Unpivot
118
+ Complex transformations such as pivots, unpivots, laterals and table functions are supported.
119
+
120
+ ```sql
121
+ -- UNPIVOT example
122
+ INSERT INTO dst
123
+ SELECT name, amount
124
+ FROM src
125
+ UNPIVOT (
126
+ amount FOR name IN (john_total AS 'john')
127
+ );
128
+ ```
129
+ produces lineage: \
130
+ `literal["john"] -> unpivot[] -> column[target.name]` \
131
+ `column[source.john_total] -> unpivot[] -> column[target.amount]`
132
+
133
+ ## Customization
134
+ You can bring your own logic for processing SQL expressions and their nodes.
135
+
136
+ For example, let's say you only want to include certain functions inside the lineage graph, such as `SUM()`. You would do this by defining a *user-defined hook* and that excludes everything except `SUM()`:
137
+ ```python
138
+ import sqlleaf
139
+ lineage = sqlleaf.Lineage()
140
+
141
+ from sqlleaf.lineage import Hook
142
+ from sqlleaf.models.node import FunctionNode
143
+
144
+ # Register a hook that only collects certain functions
145
+ hook = Hook(name="only_sum", kind=FunctionNode, func=lambda n: n.name == "SUM")
146
+ lineage.register_hooks(hooks=[hook])
147
+ ```
148
+ When you run `lineage.generate()`, the only functions that are included as nodes are those named `SUM`.
149
+
150
+ ## Searching the lineage
151
+ To map the lineage into a graph, sqlleaf is built on top of the popular library `networkx`.
152
+ If you need to search the graph, you can use any of network'x graph traversal functions.
153
+ There are currently no dedicated search APIs for sqlleaf. These will be developed in an upcoming major release.
154
+
155
+ There are various functions for printing the lineage: `print_paths()`, `print_tree()`, and `print_json()`.
156
+
157
+
158
+ ## Examples
159
+ Below are examples of SQL queries and their corresponding lineages.
160
+
161
+ #### 1. Simple INSERT
162
+ ```sql
163
+ INSERT INTO dst (name) VALUES (3);
164
+ ```
165
+ `literal[3] -> column[dst.name]`
166
+
167
+ #### 2. JSONPath selectors
168
+ ```sql
169
+ INSERT INTO dst SELECT jsonblob ->> 'fruits' -> 'apple' AS name FROM src;
170
+ ```
171
+ `column[src.name] → jsonpath[.fruits.apple] → column[dst.name]`
172
+
173
+ #### 3. Using variables via SET
174
+ ```snowflake
175
+ SET b = 'src';
176
+ CREATE TABLE dst AS SELECT col1 FROM IDENTIFIER($b);
177
+ ```
178
+ `column[SRC.COL1] → column[DST.COL1]`
179
+
180
+ #### 4. COPY queries
181
+ ```sql
182
+ COPY dst TO STDOUT;
183
+ ```
184
+ `column[dst.col1] → stream[stdout]` \
185
+ `column[dst.col2] → stream[stdout] `
186
+
187
+ ```sql
188
+ COPY dst TO PROGRAM 'gzip > /tmp/data.csv.gz';
189
+ ```
190
+ `column[dst.col1] → program[gzip]` \
191
+ `column[dst.col2] → program[gzip] `
192
+
193
+ #### 5. UNLOAD query
194
+ ```redshift
195
+ UNLOAD ('SELECT name FROM src') TO 's3://dst-bucket/name-prefix' IAM_ROLE '...';
196
+ ```
197
+ `column[src.name] → column[name path=s3://dst-bucket/name-prefix]`
198
+
199
+ #### 6. Sequences
200
+ ```sql
201
+ CREATE SEQUENCE my_serial START 101;
202
+ INSERT INTO dst (age) SELECT nextval('my_serial') as age;
203
+ ```
204
+ `sequence[my_serial] → column[dst.age]`
205
+
206
+ #### 7. An UPDATE with inner CTEs containing UPDATEs that return their old and new columns
207
+ ```sql
208
+ WITH first_cte AS (
209
+ UPDATE fruit.raw
210
+ SET name = 'pear'
211
+ RETURNING age, old.age as old_age, new.age as new_age
212
+ ),
213
+ second_cte AS (
214
+ UPDATE fruit.raw AS r
215
+ SET name = 'tomato'
216
+ RETURNING *, OLD.*, NEW.*
217
+ )
218
+ UPDATE fruit.processed
219
+ SET age = first_cte.age
220
+ FROM first_cte;
221
+ ```
222
+ `column[fruit.raw.age] -> column[first_cte.age] -> column[fruit.processed.age]` \
223
+ `literal["pear"] -> column[fruit.raw.name]` \
224
+ `literal["tomato"] -> column[fruit.raw.name`
225
+
226
+ ## Limitations
227
+ - Many of the dialects are incomplete. Postgres and PL/pgSQL have been mapped out extensively, but most of the dialects do not have their own specific features parsed into graphs yet. Dialects that do have features are Athena, Redshift, MySQL and Snowflake; the others have not been implemented at all yet.
228
+
229
+ - sqlleaf does not support data deletion. Statements like `DELETE`, `TRUNCATE` or `DROP TABLE` currently have no effect. They will in the future.
@@ -0,0 +1,216 @@
1
+ # sqlleaf
2
+ ## Trace your data
3
+ sqlleaf is a tool to help you to understand how your data flows across your data systems by producing detailed, column-level lineage across any SQL query and dialect.
4
+
5
+ It extends beyond traditional lineage tools by mapping relationships across all kinds of expressions, such as columns, functions, literals and variables.
6
+
7
+ ## Example
8
+ Add some SQL queries to a file:
9
+ ```sql
10
+ -- File: example.sql
11
+ CREATE TABLE users (name VARCHAR(50), birthday DATE);
12
+ CREATE TABLE logins (name VARCHAR(50));
13
+
14
+ INSERT INTO users (name, birthday)
15
+ SELECT LOWER(name) AS name, CURRENT_TIMESTAMP AS birthday
16
+ FROM logins;
17
+ ```
18
+ Add some Python code:
19
+ ```python
20
+ sql = open("example.sql").read()
21
+
22
+ import sqlleaf
23
+ lineage = sqlleaf.Lineage()
24
+ lineage.generate(sql=sql, dialect="postgres")
25
+ lineage.print_paths()
26
+ ```
27
+ View the lineage printed to the console:
28
+ ```
29
+ column[logins.name] -> function[LOWER] -> column[users.name]
30
+ function[CURRENT_TIMESTAMP] -> column[users.birthday]
31
+ ```
32
+
33
+ ## Applications
34
+ sqlleaf allows you to build a wide range of new applications on top of SQL databases, such as:
35
+ - data tracing and tagging systems, such as personally identifiable information tracking
36
+ - lineage-based SQL validators
37
+ - auditing systems and historical analysis
38
+ - identification of unused columns and tables
39
+ - support for database migration correctness
40
+ - impact analysis of changes to queries
41
+
42
+ ## Features
43
+ ### Types of queries
44
+ sqlleaf traces lineage across a wide range of DML queries, such as `INSERT`, `UPDATE`, `MERGE`, `CREATE TABLE AS`, `COPY`, `UNLOAD`, CTEs, and many more.
45
+
46
+ It parses over 30 dialects, extending functionality from the underlying SQL transpiler `sqlglot`.
47
+
48
+ Here are some basic queries as examples:
49
+ ```sql
50
+ -- Basic CTAS
51
+ CREATE TABLE dst AS SELECT * FROM src;
52
+
53
+ -- Basic UPDATE
54
+ UPDATE dst t SET first_name = 'john', age = s.age FROM src s;
55
+
56
+ -- An INSERT with a CTE (Common Table Expression)
57
+ WITH my_cte AS (SELECT 1 AS num) INSERT INTO dst (age) SELECT c.num FROM my_cte c;
58
+ ```
59
+
60
+ ### Support for dynamic SQL inside PL/pgSQL
61
+ You can trace lineage across variables and complex, dynamically constructed statements. This works by analyzing the procedure's inner statements, substituting any arguments and variables, and transforming the statements into the desired queries.
62
+
63
+ For example, given a query to calculate the row count of an arbitrary table:
64
+ ```sql
65
+ -- Define a PL/pgSQL procedure in Postgres
66
+ CREATE PROCEDURE get_maximum_id(schema_name TEXT, table_name TEXT)
67
+ AS $$
68
+ BEGIN
69
+ EXECUTE format('INSERT INTO target (my_value) SELECT MAX(id) FROM %I.%I', schema_name, table_name);
70
+ END;
71
+ $$ LANGUAGE plpgsql;
72
+
73
+ -- Call it
74
+ CALL get_maximum_id('public', 'users');
75
+ ```
76
+ sqlleaf extracts and transforms this into the statement:
77
+ ```sql
78
+ INSERT INTO target (my_value) SELECT MAX(id) FROM public.users;
79
+ ```
80
+ and then generates its lineage:
81
+ ```
82
+ column[public.users.id] -> function[MAX] -> column[target.my_value]
83
+ ```
84
+
85
+ ### User-defined functions, procedures and types
86
+ Given a user-defined function or a procedure, sqlleaf will extract the statement's inner queries so that they can be referenced by the calling query:
87
+ ```sql
88
+ -- Create the function
89
+ CREATE FUNCTION hello() RETURNS TEXT AS $$
90
+ SELECT 'Hello';
91
+ $$ LANGUAGE SQL;
92
+
93
+ -- Insert its value into a new table
94
+ CREATE TABLE dst (my_name VARCHAR);
95
+ INSERT INTO dst (my_name) SELECT hello();
96
+ ```
97
+ This will perform inline transformations to produce a new query:
98
+ ```sql
99
+ INSERT INTO dst (my_name) SELECT (SELECT 'hello') AS name;
100
+ ```
101
+ with lineage:
102
+ `literal['Hello'] -> column[dst.name]`
103
+
104
+ ### Pivot, Unpivot
105
+ Complex transformations such as pivots, unpivots, laterals and table functions are supported.
106
+
107
+ ```sql
108
+ -- UNPIVOT example
109
+ INSERT INTO dst
110
+ SELECT name, amount
111
+ FROM src
112
+ UNPIVOT (
113
+ amount FOR name IN (john_total AS 'john')
114
+ );
115
+ ```
116
+ produces lineage: \
117
+ `literal["john"] -> unpivot[] -> column[target.name]` \
118
+ `column[source.john_total] -> unpivot[] -> column[target.amount]`
119
+
120
+ ## Customization
121
+ You can bring your own logic for processing SQL expressions and their nodes.
122
+
123
+ For example, let's say you only want to include certain functions inside the lineage graph, such as `SUM()`. You would do this by defining a *user-defined hook* and that excludes everything except `SUM()`:
124
+ ```python
125
+ import sqlleaf
126
+ lineage = sqlleaf.Lineage()
127
+
128
+ from sqlleaf.lineage import Hook
129
+ from sqlleaf.models.node import FunctionNode
130
+
131
+ # Register a hook that only collects certain functions
132
+ hook = Hook(name="only_sum", kind=FunctionNode, func=lambda n: n.name == "SUM")
133
+ lineage.register_hooks(hooks=[hook])
134
+ ```
135
+ When you run `lineage.generate()`, the only functions that are included as nodes are those named `SUM`.
136
+
137
+ ## Searching the lineage
138
+ To map the lineage into a graph, sqlleaf is built on top of the popular library `networkx`.
139
+ If you need to search the graph, you can use any of network'x graph traversal functions.
140
+ There are currently no dedicated search APIs for sqlleaf. These will be developed in an upcoming major release.
141
+
142
+ There are various functions for printing the lineage: `print_paths()`, `print_tree()`, and `print_json()`.
143
+
144
+
145
+ ## Examples
146
+ Below are examples of SQL queries and their corresponding lineages.
147
+
148
+ #### 1. Simple INSERT
149
+ ```sql
150
+ INSERT INTO dst (name) VALUES (3);
151
+ ```
152
+ `literal[3] -> column[dst.name]`
153
+
154
+ #### 2. JSONPath selectors
155
+ ```sql
156
+ INSERT INTO dst SELECT jsonblob ->> 'fruits' -> 'apple' AS name FROM src;
157
+ ```
158
+ `column[src.name] → jsonpath[.fruits.apple] → column[dst.name]`
159
+
160
+ #### 3. Using variables via SET
161
+ ```snowflake
162
+ SET b = 'src';
163
+ CREATE TABLE dst AS SELECT col1 FROM IDENTIFIER($b);
164
+ ```
165
+ `column[SRC.COL1] → column[DST.COL1]`
166
+
167
+ #### 4. COPY queries
168
+ ```sql
169
+ COPY dst TO STDOUT;
170
+ ```
171
+ `column[dst.col1] → stream[stdout]` \
172
+ `column[dst.col2] → stream[stdout] `
173
+
174
+ ```sql
175
+ COPY dst TO PROGRAM 'gzip > /tmp/data.csv.gz';
176
+ ```
177
+ `column[dst.col1] → program[gzip]` \
178
+ `column[dst.col2] → program[gzip] `
179
+
180
+ #### 5. UNLOAD query
181
+ ```redshift
182
+ UNLOAD ('SELECT name FROM src') TO 's3://dst-bucket/name-prefix' IAM_ROLE '...';
183
+ ```
184
+ `column[src.name] → column[name path=s3://dst-bucket/name-prefix]`
185
+
186
+ #### 6. Sequences
187
+ ```sql
188
+ CREATE SEQUENCE my_serial START 101;
189
+ INSERT INTO dst (age) SELECT nextval('my_serial') as age;
190
+ ```
191
+ `sequence[my_serial] → column[dst.age]`
192
+
193
+ #### 7. An UPDATE with inner CTEs containing UPDATEs that return their old and new columns
194
+ ```sql
195
+ WITH first_cte AS (
196
+ UPDATE fruit.raw
197
+ SET name = 'pear'
198
+ RETURNING age, old.age as old_age, new.age as new_age
199
+ ),
200
+ second_cte AS (
201
+ UPDATE fruit.raw AS r
202
+ SET name = 'tomato'
203
+ RETURNING *, OLD.*, NEW.*
204
+ )
205
+ UPDATE fruit.processed
206
+ SET age = first_cte.age
207
+ FROM first_cte;
208
+ ```
209
+ `column[fruit.raw.age] -> column[first_cte.age] -> column[fruit.processed.age]` \
210
+ `literal["pear"] -> column[fruit.raw.name]` \
211
+ `literal["tomato"] -> column[fruit.raw.name`
212
+
213
+ ## Limitations
214
+ - Many of the dialects are incomplete. Postgres and PL/pgSQL have been mapped out extensively, but most of the dialects do not have their own specific features parsed into graphs yet. Dialects that do have features are Athena, Redshift, MySQL and Snowflake; the others have not been implemented at all yet.
215
+
216
+ - sqlleaf does not support data deletion. Statements like `DELETE`, `TRUNCATE` or `DROP TABLE` currently have no effect. They will in the future.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlleaf"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  authors = [
5
5
  {name = "zac", email = "zac@sqlleaf.io"}
6
6
  ]
@@ -8,9 +8,14 @@ description = "Trace your data"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.14"
10
10
  dependencies = [
11
- "build>=1.6.1",
12
11
  "networkx==3.6.1",
13
- "sqlglot==30.17.0",
12
+ "sqlglot==30.22.0",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ test = [
17
+ "pytest>=9.1.1",
18
+ "pytest-xdist>=3.8.0",
14
19
  ]
15
20
 
16
21
  [project.urls]
@@ -45,7 +50,4 @@ line-ending = "auto"
45
50
 
46
51
  [tool.ruff.lint]
47
52
  select = ["F", "E", "W", "I"]
48
- ignore = ["E501"]
49
-
50
- [tool.vulture]
51
- min_confidence = 100
53
+ ignore = ["E501", "E731"]
@@ -2,3 +2,4 @@ from sqlleaf.lineage import Lineage as Lineage
2
2
  from sqlleaf.processors.collector.collector import collect_queries as collect_queries
3
3
  from sqlleaf.processors.generator.generator import generate_lineage_for_query as generate_lineage_for_query
4
4
  from sqlleaf.processors.transformer.transformer import transform_query as transform_query
5
+ from sqlleaf.dialects import PlPgSQL
@@ -0,0 +1 @@
1
+ from sqlleaf.dialects.plpgsql.p_dialect import PlPgSQL
@@ -0,0 +1 @@
1
+ from sqlleaf.dialects.plpgsql import p_classes as pgexp
@@ -146,7 +146,7 @@ class PGGetDiagnostics(exp.Expression):
146
146
  arg_types = {"current": False, "stacked": False, "expressions": True}
147
147
 
148
148
 
149
- class PGOpenCursor(exp.Expression):
149
+ class PGOpen(exp.Expression):
150
150
  """An OPEN cursor statement."""
151
151
  arg_types = {"this": True, "expression": False, "scroll": False, "expressions": False}
152
152
 
@@ -25,7 +25,7 @@ class PlPgSQLGenerator(PostgresGenerator):
25
25
  PGContinue: lambda self, e: self.pgcontinue_sql(e),
26
26
  PGRaise: lambda self, e: self.pgraise_sql(e),
27
27
  PGSqlState: lambda self, e: self.pgsqlstate_sql(e),
28
- PGOpenCursor: lambda self, e: self.pgopencursor_sql(e),
28
+ PGOpen: lambda self, e: self.pgopen_sql(e),
29
29
  PGFetch: lambda self, e: self.pgfetch_sql(e),
30
30
  PGMove: lambda self, e: self.pgmove_sql(e),
31
31
  PGClose: lambda self, e: self.pgclose_sql(e),
@@ -419,7 +419,7 @@ class PlPgSQLGenerator(PostgresGenerator):
419
419
  return f"{self.sql(dir_expr)} {prep} {self.sql(expression.this)}"
420
420
  return self.sql(expression.this)
421
421
 
422
- def pgopencursor_sql(self, expression: PGOpenCursor) -> str:
422
+ def pgopen_sql(self, expression: PGOpen) -> str:
423
423
  # Unbound form: OPEN c [NO|SCROLL] FOR <query>
424
424
  if expression.args.get("expression") is not None:
425
425
  parts: list[str] = ["OPEN", self.sql(expression.this)]
@@ -1053,7 +1053,7 @@ class PlPgSQLParser(PostgresParser):
1053
1053
 
1054
1054
  return args
1055
1055
 
1056
- def _parse_pgopen(self) -> PGOpenCursor:
1056
+ def _parse_pgopen(self) -> PGOpen:
1057
1057
  # Cursor variable identifier
1058
1058
  cursor = self._parse_id_var()
1059
1059
 
@@ -1062,7 +1062,7 @@ class PlPgSQLParser(PostgresParser):
1062
1062
  if self._match(TokenType.L_PAREN, advance=False):
1063
1063
  # Parse cursor call arguments directly (positional, name := value, name => value)
1064
1064
  args = self._parse_cursor_call_args()
1065
- return self.expression(PGOpenCursor(this=cursor, expressions=args))
1065
+ return self.expression(PGOpen(this=cursor, expressions=args))
1066
1066
 
1067
1067
  # Optional [[NO] SCROLL]
1068
1068
  scroll: bool | None = None
@@ -1075,7 +1075,7 @@ class PlPgSQLParser(PostgresParser):
1075
1075
  # If there's no FOR and no SCROLL/NO SCROLL, treat as bound cursor with zero args: OPEN c;
1076
1076
  if not self._match(TokenType.FOR):
1077
1077
  if scroll is None:
1078
- return self.expression(PGOpenCursor(this=cursor))
1078
+ return self.expression(PGOpen(this=cursor))
1079
1079
  # If SCROLL/NO SCROLL was provided, FOR is required
1080
1080
  self._match_expect(TokenType.FOR)
1081
1081
 
@@ -1088,7 +1088,7 @@ class PlPgSQLParser(PostgresParser):
1088
1088
  if query is None:
1089
1089
  self.raise_error("Expected query after OPEN ... FOR")
1090
1090
 
1091
- return self.expression(PGOpenCursor(this=cursor, expression=query, scroll=scroll))
1091
+ return self.expression(PGOpen(this=cursor, expression=query, scroll=scroll))
1092
1092
 
1093
1093
  def _parse_pgfetch(self) -> PGFetch:
1094
1094
  # Parse optional direction, optional preposition (required when direction given), and cursor
@@ -6,12 +6,12 @@ ERROR_LEVEL = "stop"
6
6
  logger = logging.getLogger("sqlleaf")
7
7
 
8
8
 
9
- def raise_error(clazz: type, message: str):
9
+ def raise_error(cls: type, message: str):
10
10
  """
11
11
  Raise a custom exception based on the error level.
12
12
  """
13
13
  if ERROR_LEVEL == "stop":
14
- raise clazz(message)
14
+ raise cls(message)
15
15
  elif ERROR_LEVEL == "continue":
16
16
  logger.error(message)
17
17