sqlcompose 0.0.3__tar.gz → 0.0.4a2__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.
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/LICENSE +1 -1
- {sqlcompose-0.0.3/src/sqlcompose.egg-info → sqlcompose-0.0.4a2}/PKG-INFO +27 -12
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/README.md +21 -8
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/pyproject.toml +13 -8
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/__init__.py +3 -3
- sqlcompose-0.0.4a2/src/sqlcompose/core/app.py +30 -0
- sqlcompose-0.0.4a2/src/sqlcompose/core/compat.py +75 -0
- sqlcompose-0.0.4a2/src/sqlcompose/core/file_not_found_err.py +11 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/core/functions.py +7 -5
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2/src/sqlcompose.egg-info}/PKG-INFO +27 -12
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose.egg-info/SOURCES.txt +1 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose.egg-info/requires.txt +1 -0
- sqlcompose-0.0.4a2/tests/test_app.py +48 -0
- sqlcompose-0.0.4a2/tests/test_compat.py +48 -0
- sqlcompose-0.0.4a2/tests/test_functions.py +42 -0
- sqlcompose-0.0.3/src/sqlcompose/core/app.py +0 -22
- sqlcompose-0.0.3/src/sqlcompose/core/compat.py +0 -32
- sqlcompose-0.0.3/tests/test_app.py +0 -29
- sqlcompose-0.0.3/tests/test_compat.py +0 -52
- sqlcompose-0.0.3/tests/test_functions.py +0 -40
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/setup.cfg +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/__main__.py +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/core/circular_dependency_error.py +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/core/include.py +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose/py.typed +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose.egg-info/dependency_links.txt +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose.egg-info/entry_points.txt +0 -0
- {sqlcompose-0.0.3 → sqlcompose-0.0.4a2}/src/sqlcompose.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlcompose
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Composition of
|
|
3
|
+
Version: 0.0.4a2
|
|
4
|
+
Summary: Composition of SQL files
|
|
5
5
|
Author-email: Anders Madsen <anders.madsen@alphavue.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: repository, https://github.com/apmadsen/sqlcompose
|
|
8
8
|
Keywords: sql,composition,windows,linux
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -18,39 +18,54 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Programming Language :: Python
|
|
22
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
24
25
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python:
|
|
26
|
+
Requires-Python: <3.15,>=3.10
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
License-File: LICENSE
|
|
28
29
|
Provides-Extra: test
|
|
29
30
|
Requires-Dist: pytest>=8.3.0; extra == "test"
|
|
30
31
|
Requires-Dist: pytest-cov>=6.1.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-mock>=3.15; extra == "test"
|
|
31
33
|
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test.yml)
|
|
34
36
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test-coverage.yml)
|
|
35
|
-

|
|
37
|
+
[](https://github.com/apmadsen/sqlcompose/releases)
|
|
36
38
|

|
|
37
39
|

|
|
40
|
+
[](https://pepy.tech/projects/sqlcompose)
|
|
38
41
|
|
|
42
|
+
# sqlcompose: Composition of SQL files
|
|
43
|
+
sqlcompose lets you to compose sql files from multiple files by introducing `$INCLUDE` keywords. The SQL output is composed as CTE's or Common Table Expressions.
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
Using composition, you reduce both complexity and duplication of code thus adhering to the DRY principle.
|
|
46
|
+
|
|
47
|
+
### SQL Dialect
|
|
48
|
+
sqlcompose outputs SQL as standard ANSI SQL. Note though, that no validation is done on either the input or the output.
|
|
42
49
|
|
|
43
50
|
## Examples
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
|
|
52
|
+
### 1. Execute the script with the filename as an argument and output to the console:
|
|
53
|
+
```bash
|
|
46
54
|
sqlcompose query.sql
|
|
47
55
|
```
|
|
56
|
+
|
|
57
|
+
### 2. Pipe data into application and output to a file
|
|
58
|
+
```bash
|
|
59
|
+
cat query.sql | sqlcompose > output.sql
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Execute the script with SQL string as argument
|
|
48
63
|
```bash
|
|
49
|
-
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
50
|
-
sqlcompose "select * from $INCLUDE(included-query1.sql)" # on windows
|
|
64
|
+
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
51
65
|
```
|
|
66
|
+
> NOTE: Different consoles have different limitations, so you may have to switch from single to double quotes to allow for using the dollar sign.
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
### 4. Import it in another python application or package
|
|
54
69
|
```python
|
|
55
70
|
from sqlcompose import load, loads
|
|
56
71
|
# method 1 : loading from a file
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test.yml)
|
|
2
2
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test-coverage.yml)
|
|
3
|
-

|
|
3
|
+
[](https://github.com/apmadsen/sqlcompose/releases)
|
|
4
4
|

|
|
5
5
|

|
|
6
|
+
[](https://pepy.tech/projects/sqlcompose)
|
|
6
7
|
|
|
8
|
+
# sqlcompose: Composition of SQL files
|
|
9
|
+
sqlcompose lets you to compose sql files from multiple files by introducing `$INCLUDE` keywords. The SQL output is composed as CTE's or Common Table Expressions.
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
Using composition, you reduce both complexity and duplication of code thus adhering to the DRY principle.
|
|
12
|
+
|
|
13
|
+
### SQL Dialect
|
|
14
|
+
sqlcompose outputs SQL as standard ANSI SQL. Note though, that no validation is done on either the input or the output.
|
|
10
15
|
|
|
11
16
|
## Examples
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
|
|
18
|
+
### 1. Execute the script with the filename as an argument and output to the console:
|
|
19
|
+
```bash
|
|
14
20
|
sqlcompose query.sql
|
|
15
21
|
```
|
|
22
|
+
|
|
23
|
+
### 2. Pipe data into application and output to a file
|
|
24
|
+
```bash
|
|
25
|
+
cat query.sql | sqlcompose > output.sql
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 3. Execute the script with SQL string as argument
|
|
16
29
|
```bash
|
|
17
|
-
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
18
|
-
sqlcompose "select * from $INCLUDE(included-query1.sql)" # on windows
|
|
30
|
+
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
19
31
|
```
|
|
32
|
+
> NOTE: Different consoles have different limitations, so you may have to switch from single to double quotes to allow for using the dollar sign.
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
### 4. Import it in another python application or package
|
|
22
35
|
```python
|
|
23
36
|
from sqlcompose import load, loads
|
|
24
37
|
# method 1 : loading from a file
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlcompose"
|
|
3
3
|
dynamic = ["version"]
|
|
4
|
-
description = "Composition of
|
|
4
|
+
description = "Composition of SQL files"
|
|
5
5
|
keywords = ["sql", "composition", "windows", "linux"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Anders Madsen", email = "anders.madsen@alphavue.com" }
|
|
9
9
|
]
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = [ "LICENSE"]
|
|
11
12
|
classifiers = [
|
|
12
13
|
"Development Status :: 5 - Production/Stable",
|
|
13
14
|
"Development Status :: 6 - Mature",
|
|
@@ -21,13 +22,16 @@ classifiers = [
|
|
|
21
22
|
"Programming Language :: Python :: 3.11",
|
|
22
23
|
"Programming Language :: Python :: 3.12",
|
|
23
24
|
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
24
26
|
"Programming Language :: Python",
|
|
25
27
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
28
|
"Topic :: Software Development :: Libraries",
|
|
27
29
|
"Typing :: Typed"
|
|
28
30
|
]
|
|
29
|
-
dependencies = [
|
|
30
|
-
|
|
31
|
+
dependencies = [
|
|
32
|
+
|
|
33
|
+
]
|
|
34
|
+
requires-python = ">= 3.10, < 3.15"
|
|
31
35
|
|
|
32
36
|
[project.urls]
|
|
33
37
|
repository = "https://github.com/apmadsen/sqlcompose"
|
|
@@ -39,11 +43,12 @@ repository = "https://github.com/apmadsen/sqlcompose"
|
|
|
39
43
|
test = [
|
|
40
44
|
"pytest>=8.3.0",
|
|
41
45
|
"pytest-cov>=6.1.0",
|
|
46
|
+
"pytest-mock>=3.15"
|
|
42
47
|
]
|
|
43
48
|
|
|
44
|
-
[tool.setuptools
|
|
45
|
-
|
|
49
|
+
[tool.setuptools-git-versioning]
|
|
50
|
+
enabled = true
|
|
46
51
|
|
|
47
52
|
[build-system]
|
|
48
|
-
requires = ["setuptools >= 77.0.3"]
|
|
49
|
-
build-backend = "setuptools.build_meta"
|
|
53
|
+
requires = ["setuptools >= 77.0.3", "setuptools-git-versioning >= 2.1.0"]
|
|
54
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from sqlcompose.core.functions import load, loads
|
|
2
2
|
from sqlcompose.core.circular_dependency_error import CircularDependencyError
|
|
3
|
+
from sqlcompose.core.file_not_found_err import FileNotFoundErr
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
'load',
|
|
6
7
|
'loads',
|
|
7
8
|
'CircularDependencyError',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
__version__ = "0.0.3"
|
|
9
|
+
'FileNotFoundErr',
|
|
10
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
|
|
3
|
+
from sqlcompose.core.functions import load, loads
|
|
4
|
+
from sqlcompose.core.compat import is_file, get_piped_input
|
|
5
|
+
from sqlcompose.core.file_not_found_err import FileNotFoundErr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def app(args: list[str]) -> tuple[str, int]:
|
|
9
|
+
try:
|
|
10
|
+
from sys import stdin # iport must be done here to allow for test patching
|
|
11
|
+
|
|
12
|
+
if not stdin.isatty() and ( piped := get_piped_input(stdin) ):
|
|
13
|
+
sql = loads(piped)
|
|
14
|
+
else:
|
|
15
|
+
parser = ArgumentParser(prog = "sqlcompose")
|
|
16
|
+
parser.add_argument("input", type=str, help = "SQL expression or location of an SQL file")
|
|
17
|
+
pargs = parser.parse_args(args)
|
|
18
|
+
|
|
19
|
+
if is_file(pargs.input):
|
|
20
|
+
sql = load(pargs.input)
|
|
21
|
+
else:
|
|
22
|
+
sql = loads(pargs.input)
|
|
23
|
+
|
|
24
|
+
return sql, 0
|
|
25
|
+
except SystemExit as ex:
|
|
26
|
+
return str(ex), 2
|
|
27
|
+
except (FileNotFoundErr, FileNotFoundError) as ex:
|
|
28
|
+
return str(ex), 3
|
|
29
|
+
except Exception as ex: # pragma: no cover
|
|
30
|
+
return f"Unexpected error: {ex}", 1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from os import path, sep
|
|
2
|
+
from typing import TextIO
|
|
3
|
+
from re import compile
|
|
4
|
+
from threading import Thread, Event
|
|
5
|
+
|
|
6
|
+
RX_FILE = compile(r"^[\w,\s-]+\.[A-Za-z]{3}$")
|
|
7
|
+
WINDOWS_PATH_SEP = "\\"
|
|
8
|
+
UNIX_PATH_SEP = "/"
|
|
9
|
+
|
|
10
|
+
def fix_path(file_path: str) -> str:
|
|
11
|
+
"""Replaces all path separators, be they Linux or Windows style
|
|
12
|
+
to the standard path separator of the system.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
file_path (str): The file path to fix.
|
|
16
|
+
"""
|
|
17
|
+
for str in [ WINDOWS_PATH_SEP, UNIX_PATH_SEP ]:
|
|
18
|
+
if str != path.sep:
|
|
19
|
+
file_path = file_path.replace(str, path.sep)
|
|
20
|
+
|
|
21
|
+
return file_path
|
|
22
|
+
|
|
23
|
+
def get_relative_path(file_path: str, root: str) -> str:
|
|
24
|
+
"""Get the path relative to root path.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
file_path (str): The path.
|
|
28
|
+
root (str): The root path.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The relative path.
|
|
32
|
+
"""
|
|
33
|
+
if root == file_path:
|
|
34
|
+
return file_path
|
|
35
|
+
else:
|
|
36
|
+
return path.relpath(file_path, path.commonprefix([root, file_path]))
|
|
37
|
+
|
|
38
|
+
def is_file(text: str) -> bool:
|
|
39
|
+
if path.isfile(text):
|
|
40
|
+
return True
|
|
41
|
+
elif sep in text and is_file(path.basename(text)):
|
|
42
|
+
return True
|
|
43
|
+
elif RX_FILE.match(text):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def get_piped_input(pipe: TextIO) -> str:
|
|
49
|
+
"""Reads input from pipe (usually sys.stdin) without blocking.
|
|
50
|
+
"""
|
|
51
|
+
output: list[str] = []
|
|
52
|
+
ev_started = Event()
|
|
53
|
+
ev_done = Event()
|
|
54
|
+
|
|
55
|
+
def fn(output: list[str]):
|
|
56
|
+
try:
|
|
57
|
+
ev_started.set()
|
|
58
|
+
output.append(pipe.read())
|
|
59
|
+
except OSError:
|
|
60
|
+
pass
|
|
61
|
+
finally:
|
|
62
|
+
ev_done.set()
|
|
63
|
+
|
|
64
|
+
thread = Thread(target = fn, args = (output,))
|
|
65
|
+
thread.start()
|
|
66
|
+
|
|
67
|
+
ev_started.wait()
|
|
68
|
+
|
|
69
|
+
if ev_done.wait(0.1): # if there is in fact anything in the pipe, we expect it to be read within 0.1 second
|
|
70
|
+
thread.join()
|
|
71
|
+
else:
|
|
72
|
+
pass # pragma: no cover
|
|
73
|
+
# the thread will block indefinitely, nothing to do about it
|
|
74
|
+
|
|
75
|
+
return output[0] if any(output) else ""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
class FileNotFoundErr(Exception):
|
|
3
|
+
__slots__ = [ "__filename" ]
|
|
4
|
+
|
|
5
|
+
def __init__(self, filename: str, error: str | None = None):
|
|
6
|
+
super().__init__(error or f"File {filename} not found")
|
|
7
|
+
self.__filename = filename
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def filename(self) -> str:
|
|
11
|
+
return self.__filename # pragma: no cover
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from os import path
|
|
2
2
|
from typing import Sequence
|
|
3
|
-
from os import path
|
|
4
3
|
from re import compile, sub, escape, IGNORECASE
|
|
5
4
|
from textwrap import indent
|
|
6
5
|
|
|
7
6
|
from sqlcompose.core.circular_dependency_error import CircularDependencyError
|
|
8
7
|
from sqlcompose.core.include import Include
|
|
8
|
+
from sqlcompose.core.file_not_found_err import FileNotFoundErr
|
|
9
9
|
from sqlcompose.core.compat import fix_path, get_relative_path
|
|
10
10
|
|
|
11
11
|
REGEX_INCLUDE = compile(r"\$INCLUDE\(([^\)]+)\)", IGNORECASE)
|
|
@@ -37,7 +37,7 @@ def load(filename: str) -> str:
|
|
|
37
37
|
filename = fix_path(filename)
|
|
38
38
|
|
|
39
39
|
if not path.isfile(filename):
|
|
40
|
-
raise
|
|
40
|
+
raise FileNotFoundErr(filename)
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
with open(filename, "r", encoding="utf-8") as file:
|
|
@@ -88,11 +88,13 @@ def compose(
|
|
|
88
88
|
)
|
|
89
89
|
)
|
|
90
90
|
index = index + 1
|
|
91
|
-
except FileNotFoundError:
|
|
91
|
+
except (FileNotFoundErr, FileNotFoundError) as ex:
|
|
92
|
+
filename = get_relative_path(file_path_inner, root)
|
|
92
93
|
if parent is not None:
|
|
93
|
-
raise
|
|
94
|
+
raise FileNotFoundErr(filename, f"Include failed: File \"{get_relative_path(file_path_inner, root)}\" which was referred to in \"{get_relative_path(parent, root)}\", was not found...") from ex
|
|
94
95
|
else:
|
|
95
|
-
raise
|
|
96
|
+
raise FileNotFoundErr(filename, f"Include failed: File \"{get_relative_path(file_path_inner, root)}\" was not found...") from ex
|
|
97
|
+
|
|
96
98
|
|
|
97
99
|
for include in includes:
|
|
98
100
|
sql = sub(escape(include.match), include.name, sql)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlcompose
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Composition of
|
|
3
|
+
Version: 0.0.4a2
|
|
4
|
+
Summary: Composition of SQL files
|
|
5
5
|
Author-email: Anders Madsen <anders.madsen@alphavue.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: repository, https://github.com/apmadsen/sqlcompose
|
|
8
8
|
Keywords: sql,composition,windows,linux
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -18,39 +18,54 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Programming Language :: Python
|
|
22
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
24
25
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python:
|
|
26
|
+
Requires-Python: <3.15,>=3.10
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
License-File: LICENSE
|
|
28
29
|
Provides-Extra: test
|
|
29
30
|
Requires-Dist: pytest>=8.3.0; extra == "test"
|
|
30
31
|
Requires-Dist: pytest-cov>=6.1.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-mock>=3.15; extra == "test"
|
|
31
33
|
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test.yml)
|
|
34
36
|
[](https://github.com/apmadsen/sqlcompose/actions/workflows/python-test-coverage.yml)
|
|
35
|
-

|
|
37
|
+
[](https://github.com/apmadsen/sqlcompose/releases)
|
|
36
38
|

|
|
37
39
|

|
|
40
|
+
[](https://pepy.tech/projects/sqlcompose)
|
|
38
41
|
|
|
42
|
+
# sqlcompose: Composition of SQL files
|
|
43
|
+
sqlcompose lets you to compose sql files from multiple files by introducing `$INCLUDE` keywords. The SQL output is composed as CTE's or Common Table Expressions.
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
Using composition, you reduce both complexity and duplication of code thus adhering to the DRY principle.
|
|
46
|
+
|
|
47
|
+
### SQL Dialect
|
|
48
|
+
sqlcompose outputs SQL as standard ANSI SQL. Note though, that no validation is done on either the input or the output.
|
|
42
49
|
|
|
43
50
|
## Examples
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
|
|
52
|
+
### 1. Execute the script with the filename as an argument and output to the console:
|
|
53
|
+
```bash
|
|
46
54
|
sqlcompose query.sql
|
|
47
55
|
```
|
|
56
|
+
|
|
57
|
+
### 2. Pipe data into application and output to a file
|
|
58
|
+
```bash
|
|
59
|
+
cat query.sql | sqlcompose > output.sql
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Execute the script with SQL string as argument
|
|
48
63
|
```bash
|
|
49
|
-
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
50
|
-
sqlcompose "select * from $INCLUDE(included-query1.sql)" # on windows
|
|
64
|
+
sqlcompose 'select * from $INCLUDE(included-query1.sql)'
|
|
51
65
|
```
|
|
66
|
+
> NOTE: Different consoles have different limitations, so you may have to switch from single to double quotes to allow for using the dollar sign.
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
### 4. Import it in another python application or package
|
|
54
69
|
```python
|
|
55
70
|
from sqlcompose import load, loads
|
|
56
71
|
# method 1 : loading from a file
|
|
@@ -13,6 +13,7 @@ src/sqlcompose.egg-info/top_level.txt
|
|
|
13
13
|
src/sqlcompose/core/app.py
|
|
14
14
|
src/sqlcompose/core/circular_dependency_error.py
|
|
15
15
|
src/sqlcompose/core/compat.py
|
|
16
|
+
src/sqlcompose/core/file_not_found_err.py
|
|
16
17
|
src/sqlcompose/core/functions.py
|
|
17
18
|
src/sqlcompose/core/include.py
|
|
18
19
|
tests/test_app.py
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
# pyright: basic
|
|
3
|
+
from os import path, chdir, curdir
|
|
4
|
+
from sys import stdin
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pytest import fixture, raises as assert_raises
|
|
7
|
+
from pytest_mock import MockerFixture
|
|
8
|
+
|
|
9
|
+
from sqlcompose.core.app import app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_missing_args():
|
|
13
|
+
_result, code = app([])
|
|
14
|
+
assert code == 2
|
|
15
|
+
|
|
16
|
+
def test_existing_file_by_path():
|
|
17
|
+
result, code = app([path.join("tests", "main-query.sql")])
|
|
18
|
+
assert len(result) > 0
|
|
19
|
+
assert code == 0
|
|
20
|
+
|
|
21
|
+
def test_missing_file_by_path():
|
|
22
|
+
result, code = app([path.join("tests", "nonexisting.sql")])
|
|
23
|
+
assert len(result) > 0
|
|
24
|
+
assert code == 3
|
|
25
|
+
|
|
26
|
+
result, code = app([f"SELECT * FROM $INCLUDE({path.join('tests', 'nonexisting.sql')})"])
|
|
27
|
+
assert len(result) > 0
|
|
28
|
+
assert code == 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_sql():
|
|
32
|
+
result, code = app([f"SELECT * FROM $INCLUDE({path.join('tests', 'main-query.sql')})"])
|
|
33
|
+
assert len(result) > 0
|
|
34
|
+
assert code == 0
|
|
35
|
+
|
|
36
|
+
def test_pipe(mocker: MockerFixture):
|
|
37
|
+
chdir("tests")
|
|
38
|
+
try:
|
|
39
|
+
with open("main-query.sql", "rt", encoding="utf8") as input:
|
|
40
|
+
mocker.patch(f"sys.stdin", input)
|
|
41
|
+
try:
|
|
42
|
+
result, code = app([])
|
|
43
|
+
assert len(result) > 0
|
|
44
|
+
assert code == 0
|
|
45
|
+
finally:
|
|
46
|
+
mocker.resetall()
|
|
47
|
+
finally:
|
|
48
|
+
chdir("..")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
# pyright: basic
|
|
3
|
+
from pytest import fixture, raises as assert_raises
|
|
4
|
+
from sys import platform
|
|
5
|
+
|
|
6
|
+
from sqlcompose.core.compat import fix_path, get_relative_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_fix_path():
|
|
10
|
+
if platform == "win32":
|
|
11
|
+
tests = {
|
|
12
|
+
"sql\\file.sql" : "sql\\file.sql",
|
|
13
|
+
"sql\\file.sql" : "sql/file.sql",
|
|
14
|
+
}
|
|
15
|
+
else:
|
|
16
|
+
tests = {
|
|
17
|
+
"sql/file.sql" : "sql\\file.sql",
|
|
18
|
+
"sql/file.sql" : "sql/file.sql",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for expected_result, file_path in tests.items():
|
|
22
|
+
result = fix_path(file_path)
|
|
23
|
+
assert result == expected_result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_get_relative_path():
|
|
27
|
+
if platform == "win32":
|
|
28
|
+
tests = {
|
|
29
|
+
"sql\\file.sql" : ("sql\\file.sql", "sql\\file.sql"),
|
|
30
|
+
"sql\\file.sql" : ("sql\\file.sql", "c:\\app\\"),
|
|
31
|
+
"sql\\file.sql" : ("c:\\app\\sql\\file.sql", "c:\\app\\"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
p = "sql\\file.sql"
|
|
35
|
+
assert p == get_relative_path(p, p)
|
|
36
|
+
else:
|
|
37
|
+
tests = {
|
|
38
|
+
"sql/file.sql" : ("sql/file.sql", "sql/file.sql"),
|
|
39
|
+
"sql/file.sql" : ("sql/file.sql", "/app/"),
|
|
40
|
+
"sql/file.sql" : ("/app/sql/file.sql", "/app/"),
|
|
41
|
+
}
|
|
42
|
+
p = "sql/file.sql"
|
|
43
|
+
assert p == get_relative_path(p, p)
|
|
44
|
+
|
|
45
|
+
for expected_result, (file_path, root) in tests.items():
|
|
46
|
+
result = get_relative_path(file_path, root)
|
|
47
|
+
assert result == expected_result
|
|
48
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
# pyright: basic
|
|
3
|
+
from pytest import fixture, raises as assert_raises
|
|
4
|
+
|
|
5
|
+
from sqlcompose import loads, load, CircularDependencyError, FileNotFoundErr
|
|
6
|
+
from sqlcompose.core.functions import compose
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_nonexisting():
|
|
10
|
+
with assert_raises(FileNotFoundErr):
|
|
11
|
+
load("nonexisting.sql")
|
|
12
|
+
with assert_raises(FileNotFoundErr):
|
|
13
|
+
load("tests/non_existing_include.sql")
|
|
14
|
+
with assert_raises(FileNotFoundErr):
|
|
15
|
+
compose("select * from $INCLUDE(some_file_that_does_not_exist.sql)", "SQL", ".", ".")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
filename = "tests/existing_include.sql"
|
|
19
|
+
with open(filename, "r", encoding="utf-8") as file:
|
|
20
|
+
with assert_raises(FileNotFoundErr):
|
|
21
|
+
compose(file.read(), filename, filename, ".")
|
|
22
|
+
|
|
23
|
+
def test_reuse_composition():
|
|
24
|
+
result = compose("select * from $INCLUDE(tests/includes/included-query3.sql)", "SQL", ".", ".")
|
|
25
|
+
# print(result)
|
|
26
|
+
assert len(result) > 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_existing_file_by_path():
|
|
30
|
+
result = load("tests/main-query.sql")
|
|
31
|
+
# print(result)
|
|
32
|
+
assert len(result) > 0
|
|
33
|
+
|
|
34
|
+
def test_sql():
|
|
35
|
+
result = loads("SELECT * FROM $INCLUDE(tests/main-query.sql)")
|
|
36
|
+
# print(result)
|
|
37
|
+
assert len(result) > 0
|
|
38
|
+
|
|
39
|
+
def test_circular_dependency():
|
|
40
|
+
with assert_raises(CircularDependencyError):
|
|
41
|
+
load("tests/circular_left.sql")
|
|
42
|
+
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from os import path
|
|
2
|
-
from argparse import ArgumentParser
|
|
3
|
-
|
|
4
|
-
from sqlcompose.core.functions import load, loads
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def app(args: list[str]) -> tuple[str, int ]:
|
|
8
|
-
try:
|
|
9
|
-
parser = ArgumentParser(prog = "sqlcompose")
|
|
10
|
-
parser.add_argument("input", type=str, help = "SQL expression or location of an SQL file")
|
|
11
|
-
pargs = parser.parse_args(args)
|
|
12
|
-
|
|
13
|
-
if path.isfile(pargs.input):
|
|
14
|
-
sql = load(pargs.input)
|
|
15
|
-
else:
|
|
16
|
-
sql = loads(pargs.input)
|
|
17
|
-
|
|
18
|
-
return sql, 0
|
|
19
|
-
except SystemExit as ex:
|
|
20
|
-
return str(ex), 2
|
|
21
|
-
except Exception as ex: # pragma: no cover
|
|
22
|
-
return f"Unexpected error: {ex}", 1
|
|
@@ -1,32 +0,0 @@
|
|
|
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]))
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# pyright: basic
|
|
2
|
-
from unittest import TestCase, main
|
|
3
|
-
|
|
4
|
-
from sqlcompose.core.app import app
|
|
5
|
-
|
|
6
|
-
class Test(TestCase):
|
|
7
|
-
# def test_nonexisting(self):
|
|
8
|
-
# _result, code = app(["nonexisting.sql"])
|
|
9
|
-
# self.assertEqual(code, 1)
|
|
10
|
-
|
|
11
|
-
def test_missing_args(self):
|
|
12
|
-
_result, code = app([])
|
|
13
|
-
self.assertEqual(code, 2)
|
|
14
|
-
|
|
15
|
-
def test_existing_file_by_path(self):
|
|
16
|
-
result, code = app(["tests/main-query.sql"])
|
|
17
|
-
self.assertGreater(len(result), 0)
|
|
18
|
-
self.assertEqual(code, 0)
|
|
19
|
-
|
|
20
|
-
def test_sql(self):
|
|
21
|
-
result, code = app(["SELECT * FROM $INCLUDE(tests/main-query.sql)"])
|
|
22
|
-
# print(result)
|
|
23
|
-
self.assertGreater(len(result), 0)
|
|
24
|
-
self.assertEqual(code, 0)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if __name__ == '__main__':
|
|
28
|
-
main()
|
|
29
|
-
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# pyright: basic
|
|
2
|
-
from unittest import TestCase, main
|
|
3
|
-
from sys import platform
|
|
4
|
-
|
|
5
|
-
from sqlcompose.core.compat import fix_path, get_relative_path
|
|
6
|
-
|
|
7
|
-
class Test(TestCase):
|
|
8
|
-
|
|
9
|
-
def test_fix_path(self):
|
|
10
|
-
if platform == "win32":
|
|
11
|
-
tests = {
|
|
12
|
-
"sql\\file.sql" : "sql\\file.sql",
|
|
13
|
-
"sql\\file.sql" : "sql/file.sql",
|
|
14
|
-
}
|
|
15
|
-
else:
|
|
16
|
-
tests = {
|
|
17
|
-
"sql/file.sql" : "sql\\file.sql",
|
|
18
|
-
"sql/file.sql" : "sql/file.sql",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
for expected_result, file_path in tests.items():
|
|
22
|
-
result = fix_path(file_path)
|
|
23
|
-
self.assertEqual(result, expected_result)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_get_relative_path(self):
|
|
27
|
-
if platform == "win32":
|
|
28
|
-
tests = {
|
|
29
|
-
"sql\\file.sql" : ("sql\\file.sql", "sql\\file.sql"),
|
|
30
|
-
"sql\\file.sql" : ("sql\\file.sql", "c:\\app\\"),
|
|
31
|
-
"sql\\file.sql" : ("c:\\app\\sql\\file.sql", "c:\\app\\"),
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
p = "sql\\file.sql"
|
|
35
|
-
self.assertEqual(p, get_relative_path(p, p))
|
|
36
|
-
else:
|
|
37
|
-
tests = {
|
|
38
|
-
"sql/file.sql" : ("sql/file.sql", "sql/file.sql"),
|
|
39
|
-
"sql/file.sql" : ("sql/file.sql", "/app/"),
|
|
40
|
-
"sql/file.sql" : ("/app/sql/file.sql", "/app/"),
|
|
41
|
-
}
|
|
42
|
-
p = "sql/file.sql"
|
|
43
|
-
self.assertEqual(p, get_relative_path(p, p))
|
|
44
|
-
|
|
45
|
-
for expected_result, (file_path, root) in tests.items():
|
|
46
|
-
result = get_relative_path(file_path, root)
|
|
47
|
-
self.assertEqual(result, expected_result)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if __name__ == '__main__':
|
|
51
|
-
main()
|
|
52
|
-
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# pyright: basic
|
|
2
|
-
from unittest import TestCase, main
|
|
3
|
-
|
|
4
|
-
from sqlcompose import loads, load, CircularDependencyError
|
|
5
|
-
from sqlcompose.core.functions import compose
|
|
6
|
-
|
|
7
|
-
class Test(TestCase):
|
|
8
|
-
def test_nonexisting(self):
|
|
9
|
-
self.assertRaises(FileNotFoundError, load, "nonexisting.sql")
|
|
10
|
-
self.assertRaises(FileNotFoundError, load, "tests/non_existing_include.sql")
|
|
11
|
-
self.assertRaises(FileNotFoundError, compose, "select * from $INCLUDE(some_file_that_does_not_exist.sql)", "SQL", ".", ".")
|
|
12
|
-
|
|
13
|
-
filename = "tests/existing_include.sql"
|
|
14
|
-
with open(filename, "r", encoding="utf-8") as file:
|
|
15
|
-
self.assertRaises(FileNotFoundError, compose, file.read(), filename, filename, ".")
|
|
16
|
-
|
|
17
|
-
def test_reuse_composition(self):
|
|
18
|
-
result = compose("select * from $INCLUDE(tests/includes/included-query3.sql)", "SQL", ".", ".")
|
|
19
|
-
# print(result)
|
|
20
|
-
self.assertTrue(len(result) > 0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_existing_file_by_path(self):
|
|
24
|
-
result = load("tests/main-query.sql")
|
|
25
|
-
# print(result)
|
|
26
|
-
self.assertTrue(len(result) > 0)
|
|
27
|
-
|
|
28
|
-
def test_sql(self):
|
|
29
|
-
result = loads("SELECT * FROM $INCLUDE(tests/main-query.sql)")
|
|
30
|
-
# print(result)
|
|
31
|
-
self.assertTrue(len(result) > 0)
|
|
32
|
-
|
|
33
|
-
def test_circular_dependency(self):
|
|
34
|
-
self.assertRaises(CircularDependencyError, load, "tests/circular_left.sql")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if __name__ == '__main__':
|
|
39
|
-
main()
|
|
40
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|