sqlleaf 0.0.1__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.1/LICENSE +21 -0
- sqlleaf-0.0.1/PKG-INFO +46 -0
- sqlleaf-0.0.1/README.md +32 -0
- sqlleaf-0.0.1/pyproject.toml +51 -0
- sqlleaf-0.0.1/setup.cfg +4 -0
- sqlleaf-0.0.1/sqlleaf/__init__.py +4 -0
- sqlleaf-0.0.1/sqlleaf/dialects/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/p_classes.py +275 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/p_generator.py +538 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
- sqlleaf-0.0.1/sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
- sqlleaf-0.0.1/sqlleaf/exception.py +67 -0
- sqlleaf-0.0.1/sqlleaf/lineage.py +326 -0
- sqlleaf-0.0.1/sqlleaf/mappings.py +376 -0
- sqlleaf-0.0.1/sqlleaf/models/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/models/context.py +153 -0
- sqlleaf-0.0.1/sqlleaf/models/hook.py +9 -0
- sqlleaf-0.0.1/sqlleaf/models/node/__init__.py +27 -0
- sqlleaf-0.0.1/sqlleaf/models/node/base.py +148 -0
- sqlleaf-0.0.1/sqlleaf/models/node/column.py +345 -0
- sqlleaf-0.0.1/sqlleaf/models/node/dynamodb.py +39 -0
- sqlleaf-0.0.1/sqlleaf/models/node/function.py +33 -0
- sqlleaf-0.0.1/sqlleaf/models/node/interval.py +21 -0
- sqlleaf-0.0.1/sqlleaf/models/node/json.py +57 -0
- sqlleaf-0.0.1/sqlleaf/models/node/literal.py +17 -0
- sqlleaf-0.0.1/sqlleaf/models/node/null.py +14 -0
- sqlleaf-0.0.1/sqlleaf/models/node/pivot.py +34 -0
- sqlleaf-0.0.1/sqlleaf/models/node/program.py +27 -0
- sqlleaf-0.0.1/sqlleaf/models/node/sequence.py +18 -0
- sqlleaf-0.0.1/sqlleaf/models/node/star.py +11 -0
- sqlleaf-0.0.1/sqlleaf/models/node/stream.py +14 -0
- sqlleaf-0.0.1/sqlleaf/models/node/user_defined_function.py +33 -0
- sqlleaf-0.0.1/sqlleaf/models/node/var.py +7 -0
- sqlleaf-0.0.1/sqlleaf/models/node/variable.py +8 -0
- sqlleaf-0.0.1/sqlleaf/models/node/window.py +22 -0
- sqlleaf-0.0.1/sqlleaf/models/query/__init__.py +33 -0
- sqlleaf-0.0.1/sqlleaf/models/query/base.py +248 -0
- sqlleaf-0.0.1/sqlleaf/models/query/call.py +111 -0
- sqlleaf-0.0.1/sqlleaf/models/query/copy.py +144 -0
- sqlleaf-0.0.1/sqlleaf/models/query/ctas.py +61 -0
- sqlleaf-0.0.1/sqlleaf/models/query/database.py +48 -0
- sqlleaf-0.0.1/sqlleaf/models/query/delete.py +26 -0
- sqlleaf-0.0.1/sqlleaf/models/query/execute.py +66 -0
- sqlleaf-0.0.1/sqlleaf/models/query/holder.py +49 -0
- sqlleaf-0.0.1/sqlleaf/models/query/insert.py +69 -0
- sqlleaf-0.0.1/sqlleaf/models/query/merge.py +29 -0
- sqlleaf-0.0.1/sqlleaf/models/query/multitable_insert.py +42 -0
- sqlleaf-0.0.1/sqlleaf/models/query/prepare.py +61 -0
- sqlleaf-0.0.1/sqlleaf/models/query/procedure.py +72 -0
- sqlleaf-0.0.1/sqlleaf/models/query/put.py +38 -0
- sqlleaf-0.0.1/sqlleaf/models/query/replace.py +32 -0
- sqlleaf-0.0.1/sqlleaf/models/query/schema.py +47 -0
- sqlleaf-0.0.1/sqlleaf/models/query/select.py +27 -0
- sqlleaf-0.0.1/sqlleaf/models/query/sequence.py +33 -0
- sqlleaf-0.0.1/sqlleaf/models/query/set.py +74 -0
- sqlleaf-0.0.1/sqlleaf/models/query/stage.py +78 -0
- sqlleaf-0.0.1/sqlleaf/models/query/table.py +220 -0
- sqlleaf-0.0.1/sqlleaf/models/query/trigger.py +42 -0
- sqlleaf-0.0.1/sqlleaf/models/query/type.py +42 -0
- sqlleaf-0.0.1/sqlleaf/models/query/unload.py +64 -0
- sqlleaf-0.0.1/sqlleaf/models/query/update.py +45 -0
- sqlleaf-0.0.1/sqlleaf/models/query/user_defined_function.py +297 -0
- sqlleaf-0.0.1/sqlleaf/models/query/values.py +20 -0
- sqlleaf-0.0.1/sqlleaf/models/query/view.py +41 -0
- sqlleaf-0.0.1/sqlleaf/path.py +194 -0
- sqlleaf-0.0.1/sqlleaf/processors/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/processors/collector/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/processors/collector/collector.py +827 -0
- sqlleaf-0.0.1/sqlleaf/processors/collector/substitute.py +396 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/__init__.py +5 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/athena.py +45 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/base.py +572 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/mysql.py +19 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/postgres.py +182 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/redshift.py +192 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/dialects/snowflake.py +90 -0
- sqlleaf-0.0.1/sqlleaf/processors/generator/generator.py +495 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/__init__.py +31 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/base.py +899 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/call.py +9 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/copy.py +63 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/ctas.py +39 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/delete.py +14 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/execute.py +9 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/expressions/__init__.py +11 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/expressions/row.py +451 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/expressions/values.py +318 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/insert.py +125 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/merge.py +14 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/multitable_insert.py +14 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/replace.py +23 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/transformer.py +141 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/udf.py +235 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/unload.py +30 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/update.py +174 -0
- sqlleaf-0.0.1/sqlleaf/processors/transformer/values.py +10 -0
- sqlleaf-0.0.1/sqlleaf/settings/__init__.py +19 -0
- sqlleaf-0.0.1/sqlleaf/settings/athena.py +23 -0
- sqlleaf-0.0.1/sqlleaf/settings/base.py +37 -0
- sqlleaf-0.0.1/sqlleaf/settings/postgres.py +52 -0
- sqlleaf-0.0.1/sqlleaf/system_tables/load.py +0 -0
- sqlleaf-0.0.1/sqlleaf/typing.py +93 -0
- sqlleaf-0.0.1/sqlleaf/util/__init__.py +34 -0
- sqlleaf-0.0.1/sqlleaf/util/expression.py +282 -0
- sqlleaf-0.0.1/sqlleaf/util/graph.py +116 -0
- sqlleaf-0.0.1/sqlleaf/util/helpers.py +42 -0
- sqlleaf-0.0.1/sqlleaf/util/iterators.py +37 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/PKG-INFO +46 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/SOURCES.txt +115 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/dependency_links.txt +1 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/requires.txt +3 -0
- sqlleaf-0.0.1/sqlleaf.egg-info/top_level.txt +1 -0
- sqlleaf-0.0.1/tests/test_hooks.py +64 -0
sqlleaf-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zac@sqlleaf.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sqlleaf-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlleaf
|
|
3
|
+
Version: 0.0.1
|
|
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: build>=1.6.1
|
|
11
|
+
Requires-Dist: networkx==3.6.1
|
|
12
|
+
Requires-Dist: sqlglot==30.17.0
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# sqlleaf
|
|
16
|
+
## Trace your data
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
-- File: example.sql
|
|
23
|
+
CREATE TABLE users (name VARCHAR(50), birthday DATE);
|
|
24
|
+
CREATE TABLE logins (name VARCHAR(50));
|
|
25
|
+
|
|
26
|
+
INSERT INTO users (name, birthday)
|
|
27
|
+
SELECT LOWER(name) AS name, CURRENT_TIMESTAMP AS birthday
|
|
28
|
+
FROM logins;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
sql = open("example.sql").read()
|
|
33
|
+
|
|
34
|
+
import sqlleaf
|
|
35
|
+
lineage = sqlleaf.Lineage()
|
|
36
|
+
lineage.generate(sql=sql, dialect="postgres")
|
|
37
|
+
lineage.print_paths()
|
|
38
|
+
```
|
|
39
|
+
Output:
|
|
40
|
+
```
|
|
41
|
+
column[logins.name] -> function[LOWER] -> column[users.name]
|
|
42
|
+
function[CURRENT_TIMESTAMP] -> column[users.birthday]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
This project is under heavy development; version 0.1 will be released soon.
|
sqlleaf-0.0.1/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```sql
|
|
8
|
+
-- File: example.sql
|
|
9
|
+
CREATE TABLE users (name VARCHAR(50), birthday DATE);
|
|
10
|
+
CREATE TABLE logins (name VARCHAR(50));
|
|
11
|
+
|
|
12
|
+
INSERT INTO users (name, birthday)
|
|
13
|
+
SELECT LOWER(name) AS name, CURRENT_TIMESTAMP AS birthday
|
|
14
|
+
FROM logins;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
sql = open("example.sql").read()
|
|
19
|
+
|
|
20
|
+
import sqlleaf
|
|
21
|
+
lineage = sqlleaf.Lineage()
|
|
22
|
+
lineage.generate(sql=sql, dialect="postgres")
|
|
23
|
+
lineage.print_paths()
|
|
24
|
+
```
|
|
25
|
+
Output:
|
|
26
|
+
```
|
|
27
|
+
column[logins.name] -> function[LOWER] -> column[users.name]
|
|
28
|
+
function[CURRENT_TIMESTAMP] -> column[users.birthday]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
This project is under heavy development; version 0.1 will be released soon.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sqlleaf"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
authors = [
|
|
5
|
+
{name = "zac", email = "zac@sqlleaf.io"}
|
|
6
|
+
]
|
|
7
|
+
description = "Trace your data"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"build>=1.6.1",
|
|
12
|
+
"networkx==3.6.1",
|
|
13
|
+
"sqlglot==30.17.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/sqlleaf-io/sqlleaf"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools>=61.0"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
include-package-data = false
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["sqlleaf", "sqlleaf.*"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 120
|
|
31
|
+
|
|
32
|
+
[tool.ruff.format]
|
|
33
|
+
# Like Black, use double quotes for strings.
|
|
34
|
+
quote-style = "double"
|
|
35
|
+
preview = true
|
|
36
|
+
|
|
37
|
+
# Like Black, indent with spaces, rather than tabs.
|
|
38
|
+
indent-style = "space"
|
|
39
|
+
|
|
40
|
+
# Like Black, respect magic trailing commas.
|
|
41
|
+
skip-magic-trailing-comma = false
|
|
42
|
+
|
|
43
|
+
# Automatically detect the appropriate line ending.
|
|
44
|
+
line-ending = "auto"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["F", "E", "W", "I"]
|
|
48
|
+
ignore = ["E501"]
|
|
49
|
+
|
|
50
|
+
[tool.vulture]
|
|
51
|
+
min_confidence = 100
|
sqlleaf-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from sqlleaf.lineage import Lineage as Lineage
|
|
2
|
+
from sqlleaf.processors.collector.collector import collect_queries as collect_queries
|
|
3
|
+
from sqlleaf.processors.generator.generator import generate_lineage_for_query as generate_lineage_for_query
|
|
4
|
+
from sqlleaf.processors.transformer.transformer import transform_query as transform_query
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PGBlock(exp.Block):
|
|
7
|
+
"""A BEGIN ... END block."""
|
|
8
|
+
arg_types = {
|
|
9
|
+
"expressions": False,
|
|
10
|
+
"begin": False,
|
|
11
|
+
"declare": False,
|
|
12
|
+
"exception": False,
|
|
13
|
+
"label": False,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PGPerform(exp.Select):
|
|
18
|
+
"""A PERFORM statement."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PGDeclareItem(exp.DeclareItem):
|
|
22
|
+
"""A variable declaration item inside a DECLARE section."""
|
|
23
|
+
arg_types = {
|
|
24
|
+
**exp.DeclareItem.arg_types,
|
|
25
|
+
"expression": False,
|
|
26
|
+
"value": False,
|
|
27
|
+
"assign": False,
|
|
28
|
+
"constant": False,
|
|
29
|
+
"collate": False,
|
|
30
|
+
"not_null": False,
|
|
31
|
+
"alias_for": False,
|
|
32
|
+
"cursor": False,
|
|
33
|
+
"scroll": False,
|
|
34
|
+
"cursor_args": False,
|
|
35
|
+
"query": False,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PGTypeRef(exp.Expression):
|
|
40
|
+
"""A `<reference>%TYPE` or `<reference>%ROWTYPE` type expression used in DECLARE items."""
|
|
41
|
+
arg_types = {"this": True, "array": False, "rowtype": False}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PGCursorArg(exp.Expression):
|
|
45
|
+
"""A cursor argument declaration."""
|
|
46
|
+
arg_types = {"this": True, "kind": True}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PGException(exp.Expression):
|
|
50
|
+
"""An EXCEPTION section."""
|
|
51
|
+
arg_types = {"whens": True}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PGWhen(exp.Expression):
|
|
55
|
+
"""A WHEN ... THEN ... entry inside an EXCEPTION section."""
|
|
56
|
+
arg_types = {"condition": True, "then": True}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PGCase(exp.CaseStatement):
|
|
60
|
+
"""A CASE statement."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PGIfBranch(exp.Expression):
|
|
64
|
+
"""An IF/ELSIF branch."""
|
|
65
|
+
arg_types = {"condition": True, "then": True}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PGIf(exp.CaseStatement):
|
|
69
|
+
"""An IF statement."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PGLoop(exp.LoopBlock):
|
|
73
|
+
"""A LOOP statement."""
|
|
74
|
+
|
|
75
|
+
arg_types = {
|
|
76
|
+
"body": True,
|
|
77
|
+
"label": False,
|
|
78
|
+
"end_label": False,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PGForIn(exp.Expression):
|
|
83
|
+
"""A FOR statement."""
|
|
84
|
+
arg_types = {
|
|
85
|
+
"this": True,
|
|
86
|
+
"query": False,
|
|
87
|
+
"reverse": False,
|
|
88
|
+
"start": False,
|
|
89
|
+
"end": False,
|
|
90
|
+
"step": False,
|
|
91
|
+
"expressions": False,
|
|
92
|
+
"label": False,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PGCursorCall(exp.Expression):
|
|
97
|
+
"""A bound cursor invocation."""
|
|
98
|
+
arg_types = {
|
|
99
|
+
"this": True,
|
|
100
|
+
"expressions": False,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PGForEach(exp.Expression):
|
|
105
|
+
"""A FOREACH statement."""
|
|
106
|
+
arg_types = {
|
|
107
|
+
"this": True,
|
|
108
|
+
"slice": False,
|
|
109
|
+
"expression": True,
|
|
110
|
+
"expressions": False,
|
|
111
|
+
"label": False,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PGExit(exp.Leave):
|
|
116
|
+
"""An EXIT statement."""
|
|
117
|
+
arg_types = {"this": False, "when": False}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PGContinue(exp.Iterate):
|
|
121
|
+
"""A CONTINUE statement.
|
|
122
|
+
"""
|
|
123
|
+
arg_types = {"this": False, "when": False}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PGRaise(exp.Expression):
|
|
127
|
+
"""A RAISE statement."""
|
|
128
|
+
|
|
129
|
+
arg_types = {
|
|
130
|
+
"level": False,
|
|
131
|
+
"message": False,
|
|
132
|
+
"expressions": False,
|
|
133
|
+
"condition": False,
|
|
134
|
+
"sqlstate": False,
|
|
135
|
+
"using": False,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class PGAssert(exp.Expression):
|
|
140
|
+
"""An ASSERT statement."""
|
|
141
|
+
arg_types = {"condition": True, "message": False}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class PGGetDiagnostics(exp.Expression):
|
|
145
|
+
"""A GET DIAGNOSTICS statement."""
|
|
146
|
+
arg_types = {"current": False, "stacked": False, "expressions": True}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class PGOpenCursor(exp.Expression):
|
|
150
|
+
"""An OPEN cursor statement."""
|
|
151
|
+
arg_types = {"this": True, "expression": False, "scroll": False, "expressions": False}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class PGFetch(exp.Fetch):
|
|
155
|
+
"""A FETCH statement."""
|
|
156
|
+
arg_types = {
|
|
157
|
+
**exp.Fetch.arg_types,
|
|
158
|
+
"this": True,
|
|
159
|
+
"expressions": True,
|
|
160
|
+
"preposition": False,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PGExecute(exp.Execute):
|
|
165
|
+
"""A dynamic EXECUTE statement."""
|
|
166
|
+
arg_types = {
|
|
167
|
+
**exp.Execute.arg_types,
|
|
168
|
+
"strict": False,
|
|
169
|
+
"using": False,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class PGInto(exp.Into):
|
|
174
|
+
"""An INTO clause."""
|
|
175
|
+
arg_types = {
|
|
176
|
+
**exp.Into.arg_types,
|
|
177
|
+
"strict": False,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class PGReturning(exp.Returning):
|
|
182
|
+
"""A RETURNING clause that may carry an INTO [STRICT] target list.
|
|
183
|
+
|
|
184
|
+
The existing `into` argument will hold a `PGInto` instance when present.
|
|
185
|
+
"""
|
|
186
|
+
# Inherit arg_types from exp.Returning
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PGMove(exp.Expression):
|
|
190
|
+
"""A MOVE statement."""
|
|
191
|
+
arg_types = {
|
|
192
|
+
"this": True,
|
|
193
|
+
"direction": False,
|
|
194
|
+
"preposition": False,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PGClose(exp.Expression):
|
|
199
|
+
"""A CLOSE cursor statement."""
|
|
200
|
+
arg_types = {"this": True}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class PGNext(exp.Expression):
|
|
204
|
+
"""The NEXT direction."""
|
|
205
|
+
arg_types = {"this": False}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PGPrior(exp.Expression):
|
|
209
|
+
"""The PRIOR direction."""
|
|
210
|
+
arg_types = {"this": False}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class PGFirst(exp.Expression):
|
|
214
|
+
"""The FIRST direction."""
|
|
215
|
+
arg_types = {"this": False}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class PGLast(exp.Expression):
|
|
219
|
+
"""The LAST direction."""
|
|
220
|
+
arg_types = {"this": False}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class PGForward(exp.Expression):
|
|
224
|
+
"""The FORWARD direction."""
|
|
225
|
+
arg_types = {"this": False, "all": False}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class PGBackward(exp.Expression):
|
|
229
|
+
"""The BACKWARD direction."""
|
|
230
|
+
arg_types = {"this": False, "all": False}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class PGAbsolute(exp.Expression):
|
|
234
|
+
"""The ABSOLUTE <count> direction."""
|
|
235
|
+
arg_types = {"this": True}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class PGRelative(exp.Expression):
|
|
239
|
+
"""The RELATIVE <count> direction."""
|
|
240
|
+
arg_types = {"this": True}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class PGAll(exp.Expression):
|
|
244
|
+
"""The ALL direction."""
|
|
245
|
+
arg_types = {"this": False}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PGSqlState(exp.Expression):
|
|
249
|
+
"""The SQLSTATE statement."""
|
|
250
|
+
arg_types = {"this": False}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class PGFound(exp.Expression):
|
|
254
|
+
"""The special PL/pgSQL boolean status variable FOUND."""
|
|
255
|
+
arg_types = {"this": False}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class PGUpdate(exp.Update):
|
|
259
|
+
"""An UPDATE statement with optional WHERE CURRENT OF cursor."""
|
|
260
|
+
arg_types = {**exp.Update.arg_types, "current_of": False}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class PGDelete(exp.Delete):
|
|
264
|
+
"""A DELETE statement with optional WHERE CURRENT OF cursor."""
|
|
265
|
+
arg_types = {**exp.Delete.arg_types, "current_of": False}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class PGReturn(exp.Return):
|
|
269
|
+
"""A RETURN statement."""
|
|
270
|
+
arg_types = {"this": False, "query": False, "next": False}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class PGOthers(exp.Expression):
|
|
274
|
+
"""The OTHERS keyword in EXCEPTION WHEN OTHERS THEN ..."""
|
|
275
|
+
arg_types = {"this": False}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot.dialects.postgres import Postgres
|
|
4
|
+
from sqlglot.tokens import Token
|
|
5
|
+
|
|
6
|
+
from sqlleaf.dialects.plpgsql.p_generator import PlPgSQLGenerator
|
|
7
|
+
from sqlleaf.dialects.plpgsql.p_parser import PlPgSQLParser
|
|
8
|
+
from sqlleaf.dialects.plpgsql.p_tokenizer import PLPGSQL_KEYWORD_TOKEN_NAMES, TokenType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlPgSQL(Postgres):
|
|
12
|
+
"""
|
|
13
|
+
A custom dialect that extends Postgres to parse PL/pgSQL.
|
|
14
|
+
"""
|
|
15
|
+
class Tokenizer(Postgres.Tokenizer):
|
|
16
|
+
KEYWORDS = {
|
|
17
|
+
**Postgres.Tokenizer.KEYWORDS,
|
|
18
|
+
**{keyword: getattr(TokenType, keyword) for keyword in PLPGSQL_KEYWORD_TOKEN_NAMES},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# In base sqlglot, FETCH and EXECUTE are treated as COMMANDs, which swallow the rest
|
|
22
|
+
# of the statement as a single STRING token, so we exclude them.
|
|
23
|
+
COMMANDS = Postgres.Tokenizer.COMMANDS - {TokenType.FETCH, TokenType.EXECUTE}
|
|
24
|
+
|
|
25
|
+
def tokenize(self, sql: str) -> list[Token]:
|
|
26
|
+
tokens = super().tokenize(sql)
|
|
27
|
+
return self._merge_range_tokens(tokens)
|
|
28
|
+
|
|
29
|
+
def _merge_range_tokens(self, tokens: list[Token]) -> list[Token]:
|
|
30
|
+
def adjacent(a: Token, b: Token) -> bool:
|
|
31
|
+
return a.end + 1 == b.start
|
|
32
|
+
|
|
33
|
+
result: list[Token] = []
|
|
34
|
+
i = 0
|
|
35
|
+
n = len(tokens)
|
|
36
|
+
while i < n:
|
|
37
|
+
tok = tokens[i]
|
|
38
|
+
nxt = tokens[i + 1] if i + 1 < n else None
|
|
39
|
+
|
|
40
|
+
# Pattern 0: LABEL full pattern << id >>
|
|
41
|
+
# Merge only when we see LT LT <identifier-like> GT GT with strict adjacency
|
|
42
|
+
if (
|
|
43
|
+
i + 4 < n
|
|
44
|
+
and tok.token_type == TokenType.LT
|
|
45
|
+
and nxt is not None and nxt.token_type == TokenType.LT and adjacent(tok, nxt)
|
|
46
|
+
):
|
|
47
|
+
ident_tok = tokens[i + 2]
|
|
48
|
+
gt1 = tokens[i + 3]
|
|
49
|
+
gt2 = tokens[i + 4]
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
ident_tok is not None
|
|
53
|
+
and ident_tok.text
|
|
54
|
+
and (ident_tok.text[0].isalpha() or ident_tok.text[0] == "_")
|
|
55
|
+
and all(ch.isalnum() or ch == "_" for ch in ident_tok.text)
|
|
56
|
+
and gt1.token_type == TokenType.GT
|
|
57
|
+
and gt2.token_type == TokenType.GT
|
|
58
|
+
and adjacent(nxt, ident_tok)
|
|
59
|
+
and adjacent(ident_tok, gt1)
|
|
60
|
+
and adjacent(gt1, gt2)
|
|
61
|
+
):
|
|
62
|
+
# Create LABEL_BEGIN spanning the two '<'
|
|
63
|
+
lbegin = Token(
|
|
64
|
+
TokenType.LABEL_BEGIN,
|
|
65
|
+
"<<",
|
|
66
|
+
line=tok.line,
|
|
67
|
+
col=tok.col,
|
|
68
|
+
start=tok.start,
|
|
69
|
+
end=nxt.end,
|
|
70
|
+
comments=tok.comments,
|
|
71
|
+
)
|
|
72
|
+
# Keep the identifier token as-is
|
|
73
|
+
# Create LABEL_END spanning the two '>'
|
|
74
|
+
lend = Token(
|
|
75
|
+
TokenType.LABEL_END,
|
|
76
|
+
">>",
|
|
77
|
+
line=gt1.line,
|
|
78
|
+
col=gt1.col,
|
|
79
|
+
start=gt1.start,
|
|
80
|
+
end=gt2.end,
|
|
81
|
+
comments=gt1.comments,
|
|
82
|
+
)
|
|
83
|
+
result.append(lbegin)
|
|
84
|
+
result.append(ident_tok)
|
|
85
|
+
result.append(lend)
|
|
86
|
+
i += 5
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Pattern 1: NUMBER('...' endswith '.') immediately followed by DOT -> DDOT
|
|
90
|
+
if (
|
|
91
|
+
nxt is not None
|
|
92
|
+
and tok.token_type == TokenType.NUMBER
|
|
93
|
+
and tok.text.endswith(".")
|
|
94
|
+
and nxt.token_type == TokenType.DOT
|
|
95
|
+
and adjacent(tok, nxt)
|
|
96
|
+
):
|
|
97
|
+
number = Token(
|
|
98
|
+
TokenType.NUMBER,
|
|
99
|
+
tok.text[:-1],
|
|
100
|
+
line=tok.line,
|
|
101
|
+
col=tok.col,
|
|
102
|
+
start=tok.start,
|
|
103
|
+
end=tok.end - 1,
|
|
104
|
+
comments=tok.comments,
|
|
105
|
+
)
|
|
106
|
+
ddot = Token(
|
|
107
|
+
TokenType.DDOT,
|
|
108
|
+
"..",
|
|
109
|
+
line=nxt.line,
|
|
110
|
+
col=nxt.col,
|
|
111
|
+
start=tok.end,
|
|
112
|
+
end=nxt.end,
|
|
113
|
+
comments=nxt.comments,
|
|
114
|
+
)
|
|
115
|
+
result.append(number)
|
|
116
|
+
result.append(ddot)
|
|
117
|
+
i += 2
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Pattern 2: DOT immediately followed by DOT -> DDOT
|
|
121
|
+
if (
|
|
122
|
+
nxt is not None
|
|
123
|
+
and tok.token_type == TokenType.DOT
|
|
124
|
+
and nxt.token_type == TokenType.DOT
|
|
125
|
+
and adjacent(tok, nxt)
|
|
126
|
+
):
|
|
127
|
+
ddot = Token(
|
|
128
|
+
TokenType.DDOT,
|
|
129
|
+
"..",
|
|
130
|
+
line=tok.line,
|
|
131
|
+
col=tok.col,
|
|
132
|
+
start=tok.start,
|
|
133
|
+
end=nxt.end,
|
|
134
|
+
comments=tok.comments,
|
|
135
|
+
)
|
|
136
|
+
result.append(ddot)
|
|
137
|
+
i += 2
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
result.append(tok)
|
|
141
|
+
i += 1
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
Generator = PlPgSQLGenerator
|
|
146
|
+
Parser = PlPgSQLParser
|
|
147
|
+
|
|
148
|
+
plpgsql = PlPgSQL
|