sqlcompose 0.0.0__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.
@@ -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,68 @@
1
+ # sqlcompose: Composition of linked SQL files
2
+ 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.
3
+
4
+ ## Examples
5
+ __Execute the script directly:__
6
+ ```
7
+ sqlcompose query.sql
8
+ sqlcompose "select * from $INCLUDE(included-query1.sql)"
9
+ ```
10
+
11
+ __Import it in another script:__
12
+ ```python
13
+ from sqlcompose import load, loads
14
+ # method 1 : loading from a file
15
+ sql1 = load("query.sql")
16
+
17
+ # method 2 : loading from an SQL string
18
+ sql2 = loads("""
19
+ select *
20
+ from dataset.table main
21
+ inner join $INCLUDE(other.sql) other
22
+ on other.field = main.field
23
+ """)
24
+ ```
25
+
26
+ ## Preparing SQL scripts
27
+ 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.
28
+
29
+ ```sql
30
+ --main-query.sql
31
+ select * from $INCLUDE(includes\included-query2.sql)
32
+ ```
33
+ ```sql
34
+ --included-query1.sql
35
+ select 1 as test
36
+ ```
37
+ ```sql
38
+ --included-query2.sql
39
+ select * from $INCLUDE(included-query1.sql)
40
+ union all
41
+ select * from $INCLUDE(nested\included-query3.sql)
42
+ ```
43
+ ```sql
44
+ --nested\included-query3.sql
45
+ select 1 as test
46
+ ```
47
+ Which outputs:
48
+ ```sql
49
+ WITH Q_1_1 AS (
50
+ WITH Q_2_1 AS (
51
+ --includes\included-query1.sql
52
+ select 1 as test
53
+ ), Q_2_2 AS (
54
+ --includes\nested\included-query3.sql
55
+ select 1 as test
56
+ ), Q_2 AS (
57
+ --includes\included-query2.sql
58
+ select * from Q_2_1
59
+ union all
60
+ select * from Q_2_2
61
+ )
62
+ SELECT * FROM Q_2
63
+ ), Q_1 AS (
64
+ --test\main-query.sql
65
+ select * from Q_1_1
66
+ )
67
+ SELECT * FROM Q_1
68
+ ```
@@ -0,0 +1,24 @@
1
+ [tool.poetry]
2
+ name = "sqlcompose"
3
+ version = "0.0.0"
4
+ description = ""
5
+ readme = "README.md"
6
+ authors = ["Anders Madsen <anders.madsen@alphavue.com>"]
7
+ license = "MIT"
8
+ repository = "https://github.com/apmadsen/sqlcompose"
9
+ classifiers = [
10
+ "Operating System :: OS Independent"
11
+ ]
12
+ packages = [
13
+ { include = "sqlcompose", from = "src" }
14
+ ]
15
+
16
+ [tool.poetry.scripts]
17
+ "sqlcompose" = "sqlcompose.main:main"
18
+
19
+ [tool.poetry.dependencies]
20
+ python = ">=3.10,<=3.13"
21
+
22
+ [build-system]
23
+ requires = ["poetry-core>=1.0.0"]
24
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,6 @@
1
+ from sqlcompose.core.functions import load, loads
2
+
3
+ __all__ = [
4
+ 'load',
5
+ 'loads',
6
+ ]
@@ -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