sqlcompose 0.0.0__py3-none-any.whl
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.
- sqlcompose/__init__.py +6 -0
- sqlcompose/__main__.py +30 -0
- sqlcompose/core/compat.py +32 -0
- sqlcompose/core/functions.py +117 -0
- sqlcompose/core/include.py +24 -0
- sqlcompose-0.0.0.dist-info/LICENSE +21 -0
- sqlcompose-0.0.0.dist-info/METADATA +87 -0
- sqlcompose-0.0.0.dist-info/RECORD +10 -0
- sqlcompose-0.0.0.dist-info/WHEEL +4 -0
- sqlcompose-0.0.0.dist-info/entry_points.txt +3 -0
sqlcompose/__init__.py
ADDED
sqlcompose/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
from sys import exit
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
|
|
5
|
+
from sqlcompose.core.functions import load, loads
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
try:
|
|
10
|
+
parser = ArgumentParser(prog = "sqlcompose")
|
|
11
|
+
parser.add_argument("input", type=str, help = "SQL expression or location of an SQL file")
|
|
12
|
+
pargs = parser.parse_args()
|
|
13
|
+
|
|
14
|
+
if path.isfile(pargs.input):
|
|
15
|
+
sql = load(pargs.input)
|
|
16
|
+
else:
|
|
17
|
+
sql = loads(pargs.input)
|
|
18
|
+
|
|
19
|
+
print(sql)
|
|
20
|
+
|
|
21
|
+
return
|
|
22
|
+
except SystemExit:
|
|
23
|
+
raise # let argparse print error(s)
|
|
24
|
+
except Exception as ex:
|
|
25
|
+
print(f"Unexpected error: {ex}")
|
|
26
|
+
exit(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
|
|
3
|
+
WINDOWS_PATH_SEP = "\\"
|
|
4
|
+
UNIX_PATH_SEP = "/"
|
|
5
|
+
|
|
6
|
+
def fix_path(file_path: str) -> str:
|
|
7
|
+
"""Replaces all path separators, be they Linux or Windows style
|
|
8
|
+
to the standard path separator of the system.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
file_path (str): The file path to fix.
|
|
12
|
+
"""
|
|
13
|
+
for str in [ WINDOWS_PATH_SEP, UNIX_PATH_SEP ]:
|
|
14
|
+
if str != path.sep:
|
|
15
|
+
file_path = file_path.replace(str, path.sep)
|
|
16
|
+
|
|
17
|
+
return file_path
|
|
18
|
+
|
|
19
|
+
def get_relative_path(file_path: str, root: str) -> str:
|
|
20
|
+
"""Get the path relative to root path.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_path (str): The path.
|
|
24
|
+
root (str): The root path.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: The relative path.
|
|
28
|
+
"""
|
|
29
|
+
if root == file_path:
|
|
30
|
+
return file_path
|
|
31
|
+
else:
|
|
32
|
+
return path.relpath(file_path, path.commonprefix([root, file_path]))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
from typing import Sequence, MutableSequence, MutableMapping, cast
|
|
3
|
+
from os import path
|
|
4
|
+
from re import compile, sub, escape, IGNORECASE
|
|
5
|
+
from textwrap import indent
|
|
6
|
+
|
|
7
|
+
from sqlcompose.core.include import Include
|
|
8
|
+
from sqlcompose.core.compat import fix_path, get_relative_path
|
|
9
|
+
|
|
10
|
+
REGEX_INCLUDE = compile(r"\$INCLUDE\(([^\)]+)\)", IGNORECASE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def loads(sql: str) -> str:
|
|
14
|
+
"""Compose SQL
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
sql (str): The query text
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
str: The composed query
|
|
21
|
+
"""
|
|
22
|
+
return cast(str, compose(sql, "SQL", path.curdir, path.curdir))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load(filename: str) -> str:
|
|
26
|
+
"""Compose SQL
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
filename (str): The path of the file containing the SQL
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: The composed query
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
filename = fix_path(filename)
|
|
37
|
+
|
|
38
|
+
if not path.isfile(filename):
|
|
39
|
+
raise FileNotFoundError(filename)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
with open(filename, "r", encoding="utf-8") as file:
|
|
43
|
+
return cast(str, compose(file.read(), filename, filename, path.dirname(filename)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compose(
|
|
47
|
+
sql: str,
|
|
48
|
+
name: str,
|
|
49
|
+
file_path: str,
|
|
50
|
+
root: str,
|
|
51
|
+
level: int = 1,
|
|
52
|
+
included: MutableSequence[str] | None = None,
|
|
53
|
+
included1: MutableMapping[str, tuple[str, int]] | None = None
|
|
54
|
+
) -> str | None:
|
|
55
|
+
|
|
56
|
+
file_path = fix_path(file_path)
|
|
57
|
+
included = included or []
|
|
58
|
+
included1 = included1 or {}
|
|
59
|
+
includes: list[Include] = []
|
|
60
|
+
parent = included[len(included)-2] if len(included) > 1 else None
|
|
61
|
+
|
|
62
|
+
if name in included and included1[name][1] == level:
|
|
63
|
+
#duplicate include at the same level - return none, to use the previously composed SQL
|
|
64
|
+
return None
|
|
65
|
+
elif name in included:
|
|
66
|
+
raise Exception(f"Circular dependency detected: File \"{get_relative_path(file_path, root)}\" has already been already included")
|
|
67
|
+
else:
|
|
68
|
+
included.append(name)
|
|
69
|
+
included1[name] = (file_path, level)
|
|
70
|
+
|
|
71
|
+
index = 1
|
|
72
|
+
|
|
73
|
+
for match in REGEX_INCLUDE.finditer(sql):
|
|
74
|
+
file_path_inner = fix_path(path.join(path.dirname(file_path), match.group(1)))
|
|
75
|
+
try:
|
|
76
|
+
with open(file_path_inner, "r", encoding="utf-8") as file:
|
|
77
|
+
composed = compose(file.read(), match.group(1), file_path_inner, root, level=level+1)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
if not parent is None:
|
|
80
|
+
raise FileNotFoundError(f"Include failed: File \"{get_relative_path(file_path_inner, root)}\" which was referred to in \"{get_relative_path(parent, root)}\", was not found...")
|
|
81
|
+
else:
|
|
82
|
+
raise FileNotFoundError(f"Include failed: File \"{get_relative_path(file_path_inner, root)}\" was not found...")
|
|
83
|
+
|
|
84
|
+
if composed is not None:
|
|
85
|
+
includes.append(
|
|
86
|
+
Include(
|
|
87
|
+
composed,
|
|
88
|
+
f"Q_{level}_{index}",
|
|
89
|
+
match.group(0),
|
|
90
|
+
match.group(1)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
index = index + 1
|
|
94
|
+
|
|
95
|
+
for include in includes:
|
|
96
|
+
sql = sub(escape(include.match), include.name, sql)
|
|
97
|
+
|
|
98
|
+
return wrap_cte_sql(includes, sql, level, name)
|
|
99
|
+
|
|
100
|
+
def wrap_cte_sql(includes: Sequence[Include], sql: str, level: int, source: str) -> str:
|
|
101
|
+
sql_output = ""
|
|
102
|
+
indent_str = " "
|
|
103
|
+
|
|
104
|
+
if len(includes) > 0:
|
|
105
|
+
for include in includes:
|
|
106
|
+
sql_output = "WITH " if sql_output == "" else sql_output + ", "
|
|
107
|
+
sql_output = f"{sql_output}{include.name} AS (\n{include.sql}\n)"
|
|
108
|
+
|
|
109
|
+
sql_output = sql_output + ", Q_{0} AS (\n{1}\n)\nSELECT * FROM Q_{0}".format(level, indent("--{1}\n{0}".format(sql, source), indent_str)) #+ "\.format(level)
|
|
110
|
+
else:
|
|
111
|
+
sql_output = f"--{source}\n{sql}"
|
|
112
|
+
|
|
113
|
+
return sql_output if level == 1 else indent(sql_output, indent_str)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Include():
|
|
2
|
+
__slots__ = [ "__sql", "__name", "__match", "__source" ]
|
|
3
|
+
|
|
4
|
+
def __init__(self, sql: str, name: str, match: str, source: str):
|
|
5
|
+
self.__sql = sql
|
|
6
|
+
self.__name = name
|
|
7
|
+
self.__match = match
|
|
8
|
+
self.__source = source
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def sql(self) -> str:
|
|
12
|
+
return self.__sql
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return self.__name
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def match(self) -> str:
|
|
20
|
+
return self.__match
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def source(self) -> str:
|
|
24
|
+
return self.__source
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Dagrofa
|
|
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.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sqlcompose
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Anders Madsen
|
|
7
|
+
Author-email: anders.madsen@alphavue.com
|
|
8
|
+
Requires-Python: >=3.10,<=3.13
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Project-URL: Repository, https://github.com/apmadsen/sqlcompose
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# sqlcompose: Composition of linked SQL files
|
|
20
|
+
sqlcompose allows you to compose sql files from multiple files by introducing `INCLUDE` keywords. The SQL output is composed as CTE's or Common Table Expressions.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
__Execute the script directly:__
|
|
24
|
+
```
|
|
25
|
+
sqlcompose query.sql
|
|
26
|
+
sqlcompose "select * from $INCLUDE(included-query1.sql)"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
__Import it in another script:__
|
|
30
|
+
```python
|
|
31
|
+
from sqlcompose import load, loads
|
|
32
|
+
# method 1 : loading from a file
|
|
33
|
+
sql1 = load("query.sql")
|
|
34
|
+
|
|
35
|
+
# method 2 : loading from an SQL string
|
|
36
|
+
sql2 = loads("""
|
|
37
|
+
select *
|
|
38
|
+
from dataset.table main
|
|
39
|
+
inner join $INCLUDE(other.sql) other
|
|
40
|
+
on other.field = main.field
|
|
41
|
+
""")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Preparing SQL scripts
|
|
45
|
+
Insert a `$INCLUDE(filename)` where the reference to the file should be in the resulting SQL, keeping in mind that references are loaded relative to the file loaded or the current working dir in case of an SQL string.
|
|
46
|
+
|
|
47
|
+
```sql
|
|
48
|
+
--main-query.sql
|
|
49
|
+
select * from $INCLUDE(includes\included-query2.sql)
|
|
50
|
+
```
|
|
51
|
+
```sql
|
|
52
|
+
--included-query1.sql
|
|
53
|
+
select 1 as test
|
|
54
|
+
```
|
|
55
|
+
```sql
|
|
56
|
+
--included-query2.sql
|
|
57
|
+
select * from $INCLUDE(included-query1.sql)
|
|
58
|
+
union all
|
|
59
|
+
select * from $INCLUDE(nested\included-query3.sql)
|
|
60
|
+
```
|
|
61
|
+
```sql
|
|
62
|
+
--nested\included-query3.sql
|
|
63
|
+
select 1 as test
|
|
64
|
+
```
|
|
65
|
+
Which outputs:
|
|
66
|
+
```sql
|
|
67
|
+
WITH Q_1_1 AS (
|
|
68
|
+
WITH Q_2_1 AS (
|
|
69
|
+
--includes\included-query1.sql
|
|
70
|
+
select 1 as test
|
|
71
|
+
), Q_2_2 AS (
|
|
72
|
+
--includes\nested\included-query3.sql
|
|
73
|
+
select 1 as test
|
|
74
|
+
), Q_2 AS (
|
|
75
|
+
--includes\included-query2.sql
|
|
76
|
+
select * from Q_2_1
|
|
77
|
+
union all
|
|
78
|
+
select * from Q_2_2
|
|
79
|
+
)
|
|
80
|
+
SELECT * FROM Q_2
|
|
81
|
+
), Q_1 AS (
|
|
82
|
+
--test\main-query.sql
|
|
83
|
+
select * from Q_1_1
|
|
84
|
+
)
|
|
85
|
+
SELECT * FROM Q_1
|
|
86
|
+
```
|
|
87
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sqlcompose/__init__.py,sha256=yAFSzkLv3bCZ6e4OWvws6aYjwHfLQcxhGVzK12Fs_CQ,89
|
|
2
|
+
sqlcompose/__main__.py,sha256=5TpsgP8V1SC14siCyUcHdEYWm9vDwxaUtqUgCC5f_4w,691
|
|
3
|
+
sqlcompose/core/compat.py,sha256=pIF8Lk-5MhwAlHD4_RCMXYOxiDyTJIA5nlbR5_Dyw0g,819
|
|
4
|
+
sqlcompose/core/functions.py,sha256=eHK_STVYOGDn3dtShTa0xaasYQi220tF12bQH9qEEUI,3693
|
|
5
|
+
sqlcompose/core/include.py,sha256=ejNaz2p2Lq1pUnwMxtBqE4p_hBOtG8OynqRpECLEbNQ,541
|
|
6
|
+
sqlcompose-0.0.0.dist-info/LICENSE,sha256=WtJk2ScYuo_lEP42z3DYU191mO2oU8z00nPsL9KAWCQ,1063
|
|
7
|
+
sqlcompose-0.0.0.dist-info/METADATA,sha256=dPh6avQdIOJ6NO20-Z3h8iRUEFM8VtVEzxSPhjcOqrk,2269
|
|
8
|
+
sqlcompose-0.0.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
9
|
+
sqlcompose-0.0.0.dist-info/entry_points.txt,sha256=N3eGXJzvw5yx5WgFFmX0L6Nw0-ojm90OfhtlC-C6t_4,51
|
|
10
|
+
sqlcompose-0.0.0.dist-info/RECORD,,
|