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.
- sqlleaf-0.0.2/PKG-INFO +229 -0
- sqlleaf-0.0.2/README.md +216 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/pyproject.toml +9 -7
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/__init__.py +1 -0
- sqlleaf-0.0.2/sqlleaf/dialects/__init__.py +1 -0
- sqlleaf-0.0.2/sqlleaf/dialects/plpgsql/__init__.py +1 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_classes.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_generator.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_parser.py +4 -4
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/exception.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/lineage.py +8 -65
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/mappings.py +94 -34
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/context.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/base.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/__init__.py +51 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/base.py +12 -21
- sqlleaf-0.0.2/sqlleaf/models/query/block.py +35 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/call.py +4 -4
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/copy.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/ctas.py +6 -6
- sqlleaf-0.0.2/sqlleaf/models/query/declare_item.py +33 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/execute.py +2 -2
- sqlleaf-0.0.2/sqlleaf/models/query/execute_dynamic.py +75 -0
- sqlleaf-0.0.2/sqlleaf/models/query/fetch.py +25 -0
- sqlleaf-0.0.2/sqlleaf/models/query/for_in.py +31 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/holder.py +2 -2
- sqlleaf-0.0.2/sqlleaf/models/query/if_.py +35 -0
- sqlleaf-0.0.2/sqlleaf/models/query/loop.py +31 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/multitable_insert.py +1 -1
- sqlleaf-0.0.2/sqlleaf/models/query/open.py +54 -0
- sqlleaf-0.0.2/sqlleaf/models/query/perform.py +12 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/prepare.py +1 -1
- sqlleaf-0.0.2/sqlleaf/models/query/procedure.py +124 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/set.py +43 -11
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/stage.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/table.py +10 -10
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/type.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/unload.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/user_defined_function.py +20 -28
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/view.py +3 -3
- sqlleaf-0.0.2/sqlleaf/models/query/while_.py +30 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/path.py +8 -8
- sqlleaf-0.0.2/sqlleaf/processors/collector/classify.py +115 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/collector/collector.py +178 -284
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/base.py +50 -5
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/generator.py +61 -8
- sqlleaf-0.0.2/sqlleaf/processors/substitutor/substitute.py +781 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/__init__.py +6 -2
- sqlleaf-0.0.2/sqlleaf/processors/transformer/assignment.py +18 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/base.py +87 -88
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/call.py +2 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/copy.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/ctas.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/delete.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/execute.py +2 -1
- sqlleaf-0.0.2/sqlleaf/processors/transformer/execute_dynamic.py +10 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/__init__.py +5 -1
- sqlleaf-0.0.2/sqlleaf/processors/transformer/expressions/format.py +251 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/row.py +12 -12
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/values.py +115 -89
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/insert.py +3 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/merge.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/multitable_insert.py +2 -0
- sqlleaf-0.0.2/sqlleaf/processors/transformer/open.py +78 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/replace.py +3 -1
- sqlleaf-0.0.2/sqlleaf/processors/transformer/transformer.py +183 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/unload.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/update.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/values.py +3 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/__init__.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/athena.py +1 -1
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/base.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/settings/postgres.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/__init__.py +2 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/expression.py +25 -4
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/graph.py +3 -3
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/helpers.py +2 -2
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/util/iterators.py +17 -6
- sqlleaf-0.0.2/sqlleaf.egg-info/PKG-INFO +229 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf.egg-info/SOURCES.txt +18 -5
- sqlleaf-0.0.2/sqlleaf.egg-info/requires.txt +2 -0
- sqlleaf-0.0.1/PKG-INFO +0 -46
- sqlleaf-0.0.1/README.md +0 -32
- sqlleaf-0.0.1/sqlleaf/models/query/procedure.py +0 -72
- sqlleaf-0.0.1/sqlleaf/processors/collector/substitute.py +0 -396
- sqlleaf-0.0.1/sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/transformer.py +0 -141
- sqlleaf-0.0.1/sqlleaf/processors/transformer/udf.py +0 -235
- sqlleaf-0.0.1/sqlleaf/system_tables/load.py +0 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/PKG-INFO +0 -46
- sqlleaf-0.0.1/sqlleaf.egg-info/requires.txt +0 -3
- sqlleaf-0.0.1/tests/test_hooks.py +0 -64
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/LICENSE +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/setup.cfg +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_dialect.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/dialects/plpgsql/p_tokenizer.py +0 -0
- {sqlleaf-0.0.1/sqlleaf/dialects → sqlleaf-0.0.2/sqlleaf/models}/__init__.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/hook.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/__init__.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/column.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/dynamodb.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/function.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/interval.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/json.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/literal.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/null.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/pivot.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/program.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/sequence.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/star.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/stream.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/user_defined_function.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/var.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/variable.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/node/window.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/database.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/delete.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/insert.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/merge.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/put.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/replace.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/schema.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/select.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/sequence.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/trigger.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/update.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/models/query/values.py +0 -0
- {sqlleaf-0.0.1/sqlleaf/dialects/plpgsql → sqlleaf-0.0.2/sqlleaf/processors}/__init__.py +0 -0
- {sqlleaf-0.0.1/sqlleaf/models → sqlleaf-0.0.2/sqlleaf/processors/collector}/__init__.py +0 -0
- {sqlleaf-0.0.1/sqlleaf/processors → sqlleaf-0.0.2/sqlleaf/processors/generator}/__init__.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/__init__.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/athena.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/mysql.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/postgres.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/redshift.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/generator/dialects/snowflake.py +0 -0
- {sqlleaf-0.0.1/sqlleaf/processors/collector → sqlleaf-0.0.2/sqlleaf/processors/substitutor}/__init__.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/processors/transformer/expressions/functional_notation.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf/typing.py +0 -0
- {sqlleaf-0.0.1 → sqlleaf-0.0.2}/sqlleaf.egg-info/dependency_links.txt +0 -0
- {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.
|
sqlleaf-0.0.2/README.md
ADDED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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) ->
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
14
|
+
raise cls(message)
|
|
15
15
|
elif ERROR_LEVEL == "continue":
|
|
16
16
|
logger.error(message)
|
|
17
17
|
|