sqliac 0.1.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.
Files changed (48) hide show
  1. sqliac-0.1.0/LICENSE +18 -0
  2. sqliac-0.1.0/PKG-INFO +44 -0
  3. sqliac-0.1.0/pyproject.toml +45 -0
  4. sqliac-0.1.0/setup.cfg +4 -0
  5. sqliac-0.1.0/src/sqliac/__init__.py +20 -0
  6. sqliac-0.1.0/src/sqliac/__main__.py +6 -0
  7. sqliac-0.1.0/src/sqliac/adapters/__init__.py +17 -0
  8. sqliac-0.1.0/src/sqliac/adapters/base.py +154 -0
  9. sqliac-0.1.0/src/sqliac/adapters/connection_manager.py +135 -0
  10. sqliac-0.1.0/src/sqliac/adapters/factory.py +30 -0
  11. sqliac-0.1.0/src/sqliac/cli.py +315 -0
  12. sqliac-0.1.0/src/sqliac/constants.py +59 -0
  13. sqliac-0.1.0/src/sqliac/definitions_loader.py +205 -0
  14. sqliac-0.1.0/src/sqliac/drift.py +340 -0
  15. sqliac-0.1.0/src/sqliac/errors.py +58 -0
  16. sqliac-0.1.0/src/sqliac/execution_plan.py +205 -0
  17. sqliac-0.1.0/src/sqliac/main.py +33 -0
  18. sqliac-0.1.0/src/sqliac/providers_loader.py +237 -0
  19. sqliac-0.1.0/src/sqliac/scheduler.py +303 -0
  20. sqliac-0.1.0/src/sqliac/template_engine.py +154 -0
  21. sqliac-0.1.0/src/sqliac/templates/__init__.py +0 -0
  22. sqliac-0.1.0/src/sqliac/templates/definitions/database.toml +12 -0
  23. sqliac-0.1.0/src/sqliac/templates/definitions/schema.toml +18 -0
  24. sqliac-0.1.0/src/sqliac/templates/definitions/table.toml +46 -0
  25. sqliac-0.1.0/src/sqliac/templates/provider/config.toml +58 -0
  26. sqliac-0.1.0/src/sqliac/templates/provider/credentials.toml +5 -0
  27. sqliac-0.1.0/src/sqliac/templates/provider/database/ddl_template.sql +31 -0
  28. sqliac-0.1.0/src/sqliac/templates/provider/database/state.sql +34 -0
  29. sqliac-0.1.0/src/sqliac/templates/provider/schema/ddl_template.sql +19 -0
  30. sqliac-0.1.0/src/sqliac/templates/provider/schema/state.sql +11 -0
  31. sqliac-0.1.0/src/sqliac/templates/provider/table/ddl_template.sql +62 -0
  32. sqliac-0.1.0/src/sqliac/templates/provider/table/state.sql +18 -0
  33. sqliac-0.1.0/src/sqliac/utils.py +61 -0
  34. sqliac-0.1.0/src/sqliac/value_sanitizer.py +85 -0
  35. sqliac-0.1.0/src/sqliac.egg-info/PKG-INFO +44 -0
  36. sqliac-0.1.0/src/sqliac.egg-info/SOURCES.txt +46 -0
  37. sqliac-0.1.0/src/sqliac.egg-info/dependency_links.txt +1 -0
  38. sqliac-0.1.0/src/sqliac.egg-info/entry_points.txt +2 -0
  39. sqliac-0.1.0/src/sqliac.egg-info/requires.txt +10 -0
  40. sqliac-0.1.0/src/sqliac.egg-info/top_level.txt +1 -0
  41. sqliac-0.1.0/tests/test_cli.py +21 -0
  42. sqliac-0.1.0/tests/test_definitions_loader.py +82 -0
  43. sqliac-0.1.0/tests/test_drift.py +119 -0
  44. sqliac-0.1.0/tests/test_execution_plan.py +65 -0
  45. sqliac-0.1.0/tests/test_providers_loader.py +58 -0
  46. sqliac-0.1.0/tests/test_scheduler.py +94 -0
  47. sqliac-0.1.0/tests/test_template_engine.py +66 -0
  48. sqliac-0.1.0/tests/test_value_sanitizer.py +44 -0
sqliac-0.1.0/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ruskgo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
sqliac-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqliac
3
+ Version: 0.1.0
4
+ Summary: An alternative to Terraform for SQL databases without using a state.
5
+ Author-email: Ruslan Gonzalez Konstantinov <rus.kgo.gmail@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 ruskgo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
11
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
12
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
14
+ following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all copies or substantial
17
+ portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
20
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
21
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
23
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ Project-URL: Homepage, https://codeberg.org/ruskgo/sqliac
26
+ Project-URL: Bug Tracker, https://codeberg.org/ruskgo/sqliac/issues
27
+ Keywords: sql,iac,terraform-alternative,database
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: Topic :: Database
33
+ Requires-Python: >=3.12
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: Jinja2
37
+ Requires-Dist: rich
38
+ Requires-Dist: sqlparse
39
+ Requires-Dist: dictdiffer
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
42
+ Provides-Extra: snowflake
43
+ Requires-Dist: sqliac-snowflake; extra == "snowflake"
44
+ Dynamic: license-file
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "sqliac"
3
+ version = "0.1.0"
4
+ description = "An alternative to Terraform for SQL databases without using a state."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "Ruslan Gonzalez Konstantinov", email = "rus.kgo.gmail@example.com" },
10
+ ]
11
+ keywords = ["sql", "iac", "terraform-alternative", "database"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Database",
18
+ ]
19
+ dependencies = ["Jinja2", "rich", "sqlparse", "dictdiffer"]
20
+ [project.urls]
21
+ Homepage = "https://codeberg.org/ruskgo/sqliac"
22
+ "Bug Tracker" = "https://codeberg.org/ruskgo/sqliac/issues"
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=9.1.1"]
26
+ snowflake = ["sqliac-snowflake"]
27
+
28
+ [tool.uv.workspace]
29
+ members = ["sqliac_adapters/sqliac_snowflake"]
30
+
31
+ [tool.uv.sources]
32
+ sqliac-snowflake = { workspace = true }
33
+
34
+ [project.scripts]
35
+ sqliac = "sqliac.cli:main"
36
+
37
+ [tool.setuptools.package-data]
38
+ sqliac = ["templates/**/*.toml", "templates/**/*.sql"]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [build-system]
44
+ requires = ["setuptools>=61.0.0", "wheel"]
45
+ build-backend = "setuptools.build_meta"
sqliac-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ """sqliac - SQL Infrastructure as Code."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Ruslan Gonzalez Konstantinov"
5
+ __license__ = "MIT"
6
+ __prog_name__ = "sqliac"
7
+
8
+ from sqliac.constants import TemplateType, DDLCommand, IacAction, RunMode, Paths
9
+
10
+ __all__ = [
11
+ # Version
12
+ "__version__",
13
+ "__author__",
14
+ "__license__",
15
+ "TemplateType",
16
+ "DDLCommand",
17
+ "IacAction",
18
+ "RunMode",
19
+ "Paths",
20
+ ]
@@ -0,0 +1,6 @@
1
+ """SQLIaC entry point."""
2
+
3
+ from sqliac import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli.main()
@@ -0,0 +1,17 @@
1
+ """Adapters package for sqliac."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .base import (
6
+ BaseAdapter,
7
+ BaseCredentials,
8
+ )
9
+ from .factory import AdapterFactory
10
+
11
+ __all__ = [
12
+ # Base classes
13
+ "BaseAdapter",
14
+ "BaseCredentials",
15
+ # Factory
16
+ "AdapterFactory",
17
+ ]
@@ -0,0 +1,154 @@
1
+ """Simplified, production-grade SQL adapter layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import os
7
+ import tomllib
8
+ from pathlib import Path
9
+ from tomllib import TOMLDecodeError
10
+ from typing import Any, Type
11
+
12
+ import sqlparse
13
+
14
+ from sqliac import Paths
15
+ from sqliac.adapters.connection_manager import ThreadConnectionManager
16
+ from sqliac.errors import RustyError
17
+ from sqliac.template_engine import TemplateEngine
18
+
19
+
20
+ class BaseCredentials(abc.ABC):
21
+ """Abstract base class establishing database connection credentials."""
22
+
23
+ @property
24
+ @abc.abstractmethod
25
+ def dialect(self) -> str:
26
+ """The identifier string for the target database system."""
27
+ raise NotImplementedError(
28
+ f"Credentials class {self.__class__.__name__} must implement 'dialect' property."
29
+ )
30
+
31
+ def __post_init__(self):
32
+ """Validate if the required fields are present."""
33
+ raise NotImplementedError(
34
+ f"Credentials class {self.__class__.__name__} must implement post init validation."
35
+ )
36
+
37
+
38
+ class BaseAdapter(abc.ABC):
39
+ """Abstract base class handling configuration lifecycles and query execution."""
40
+
41
+ # Define the concrete Connection Manager class
42
+ ConnectionManager: Type[ThreadConnectionManager]
43
+
44
+ # Standard Types
45
+ Row = tuple[Any, ...] | None
46
+
47
+ def __init__(self) -> None:
48
+ # Load environment credentials immediately upon initialization
49
+ self.credentials = self._load_credentials()
50
+ self.connections = self.ConnectionManager(self.credentials)
51
+
52
+ @property
53
+ @abc.abstractmethod
54
+ def dialect(self) -> str:
55
+ """The identifier string for the target database system."""
56
+ pass
57
+
58
+ @property
59
+ @abc.abstractmethod
60
+ def credentials_class(self) -> type[BaseCredentials]:
61
+ """The concrete Credentials class associated with this adapter."""
62
+ raise NotImplementedError(
63
+ f"Adapter class {self.__class__.__name__} must implement 'credentials_class' property."
64
+ )
65
+
66
+ @abc.abstractmethod
67
+ def _fetch_row(self, cursor: Any) -> Row:
68
+ """Process an open database cursor into standard formatted row."""
69
+ raise NotImplementedError(
70
+ f"Adapter class {self.__class__.__name__} must implement '_fetch_row' method."
71
+ )
72
+
73
+ def _load_credentials(self) -> BaseCredentials:
74
+ """Load from file or environment variables prefixed with the dialect name."""
75
+ target_path = self._resolve_path(Paths.PROVIDER_CREDENTIALS_FILE)
76
+ if target_path.is_file():
77
+ try:
78
+ with open(target_path, "rb") as f:
79
+ config = tomllib.load(f)
80
+
81
+ if self.dialect in config:
82
+ for key, value in config[self.dialect].items():
83
+ os.environ[f"{self.dialect.upper()}_{key.upper()}"] = str(value)
84
+
85
+ except TOMLDecodeError as err:
86
+ raise RustyError(
87
+ error="invalid TOML file",
88
+ file=str(target_path),
89
+ help="check TOML formatting in the definition file",
90
+ ) from err
91
+
92
+ except PermissionError:
93
+ raise RustyError(
94
+ error="read permission denied",
95
+ file=str(target_path),
96
+ help="the file might be open by another application or missing read permissions",
97
+ ) from None
98
+
99
+ prefix = f"{self.dialect.upper()}_"
100
+ credentials = {
101
+ key.replace(prefix, "").lower(): val
102
+ for key, val in os.environ.items()
103
+ if key.startswith(prefix)
104
+ }
105
+
106
+ return self.credentials_class(**credentials)
107
+
108
+ @staticmethod
109
+ def _resolve_path(path: Path):
110
+ return path if path.is_absolute() else Path.cwd() / path
111
+
112
+ def _split_statements(self, sql: str) -> list[str]:
113
+ """Cleans syntax comments and splits batches into single statements."""
114
+ formatted = TemplateEngine.pretty_sql(sql)
115
+ return [str(s).strip() for s in sqlparse.parse(formatted) if str(s).strip()]
116
+
117
+ def execute(self, sql: str) -> Row:
118
+ """Executes one or more SQL statements sequentially within a clean connection lifecycle."""
119
+ statements = self._split_statements(sql)
120
+
121
+ with self.connections.get_connection() as conn:
122
+ try:
123
+ cursor = conn.cursor()
124
+ except Exception as err:
125
+ raise RustyError(
126
+ error="failed to get the connection cursor", details=str(err)
127
+ )
128
+
129
+ for statement in statements:
130
+ try:
131
+ if cursor:
132
+ cursor.execute(statement)
133
+ except RustyError:
134
+ raise
135
+ except Exception as err:
136
+ raise RustyError(
137
+ error="SQL statement execution error",
138
+ sql=TemplateEngine.pretty_sql(sql=statement),
139
+ details=str(err),
140
+ ) from err
141
+
142
+ return self._fetch_row(cursor)
143
+
144
+ def cleanup(self) -> None:
145
+ """Cleans up all managed connections."""
146
+ self.connections.cleanup_all()
147
+
148
+ def __enter__(self):
149
+ """Context manager entry."""
150
+ return self
151
+
152
+ def __exit__(self, exc_type, exc_val, exc_tb):
153
+ """Context manager exit - cleanup connections."""
154
+ self.cleanup()
@@ -0,0 +1,135 @@
1
+ """Manages database connections with thread safety and connection pooling."""
2
+
3
+ import abc
4
+ import threading
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from typing import Any, ClassVar, Generator
8
+
9
+
10
+ @dataclass
11
+ class ConnectionState:
12
+ """Represents the current state of a database connection.
13
+
14
+ Args:
15
+ name(str): Unique identifier for this connection thread
16
+ state(str): Current state ('init', 'open', 'closed', 'fail')
17
+ handle(str): The underlying database connection object
18
+ credentials(str): Credentials used to establish this connection
19
+ """
20
+
21
+ name: str
22
+ state: str = "init"
23
+ handle: Any | None = None
24
+ credentials: Any | None = None
25
+
26
+
27
+ class ThreadConnectionManager(abc.ABC):
28
+ """Manages database connections with thread safety."""
29
+
30
+ # Each adapter implementation must define its type of credentials
31
+ CredentialsClass: ClassVar[type[Any]]
32
+
33
+ def __init__(self, credentials: Any):
34
+ """Initialize the connection manager.
35
+
36
+ Args:
37
+ credentials: Database credentials for establishing connections
38
+ """
39
+ self.credentials = credentials
40
+
41
+ # Dictionary to store one connection per thread
42
+ # Key: thread name, Value: ConnectionState object
43
+ self.thread_connections: dict[str, ConnectionState] = {}
44
+
45
+ # Reentrant lock allows the same thread to acquire it multiple times
46
+ self.lock = threading.RLock()
47
+
48
+ def get_thread_identifier(self) -> str:
49
+ """Get unique identifier for the current thread."""
50
+ return threading.current_thread().name
51
+
52
+ def get_thread_connection(self) -> ConnectionState:
53
+ """Get or create connection for the current thread."""
54
+ thread_id = self.get_thread_identifier()
55
+
56
+ # Thread-safe access to the connections dictionary
57
+ with self.lock:
58
+ if thread_id not in self.thread_connections:
59
+ # Create a new connection state for this thread
60
+ self.thread_connections[thread_id] = ConnectionState(
61
+ name=thread_id,
62
+ credentials=self.credentials,
63
+ )
64
+
65
+ return self.thread_connections[thread_id]
66
+
67
+ @abc.abstractmethod
68
+ def open(self, connection: ConnectionState) -> ConnectionState:
69
+ """Open a new database connection.
70
+
71
+ This method must be implemented by each database adapter to handle
72
+ the specific connection logic for that database.
73
+
74
+ Args:
75
+ connection: ConnectionState object to populate with connection
76
+
77
+ Returns:
78
+ Updated ConnectionState with opened connection
79
+ """
80
+ raise NotImplementedError(
81
+ f"{self.__class__.__name__} must implement the 'open' method"
82
+ )
83
+
84
+ def close(self, conn_state: ConnectionState) -> ConnectionState:
85
+ """Close a database connection."""
86
+ if conn_state.state == "closed":
87
+ return conn_state
88
+
89
+ if conn_state.handle is not None:
90
+ try:
91
+ # Handle custom connection types
92
+ conn_state.handle.close()
93
+ except Exception as e:
94
+ # Log warning instead of raising, as we are cleaning up
95
+ print(f"warning: error closing connection {conn_state.name}: {e}")
96
+ pass
97
+
98
+ # Update state
99
+ conn_state.state = "closed"
100
+ conn_state.handle = None
101
+
102
+ return conn_state
103
+
104
+ def cleanup_all(self) -> None:
105
+ """Close all open connections across all threads."""
106
+ with self.lock:
107
+ for connection in self.thread_connections.values():
108
+ if connection.state == "open":
109
+ self.close(connection)
110
+ # Clear the connections dictionary
111
+ self.thread_connections.clear()
112
+
113
+ @contextmanager
114
+ def get_connection(self) -> Generator[Any]:
115
+ """Context manager to get and manage a database connection.
116
+
117
+ Yields:
118
+ SQL server connection object ready for use
119
+ """
120
+ # Get or create connection state for this thread
121
+ conn_state = self.get_thread_connection()
122
+
123
+ # Open connection if not already open
124
+ if conn_state.state != "open":
125
+ self.close(conn_state)
126
+ # Ensure the concrete open method is called
127
+ conn_state = self.open(conn_state)
128
+
129
+ try:
130
+ # Yield the actual database connection handle
131
+ yield conn_state.handle # pyright: ignore[reportReturnType]
132
+ except Exception:
133
+ # Mark connection as failed on exception
134
+ conn_state.state = "fail"
135
+ raise
@@ -0,0 +1,30 @@
1
+ """Adapter factory for creating database adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import entry_points
6
+
7
+ from sqliac.errors import RustyError
8
+
9
+
10
+ class AdapterFactory:
11
+ GROUP = "sqliac.adapters"
12
+
13
+ @classmethod
14
+ def list_adapters(cls) -> list[str]:
15
+ return sorted(ep.name for ep in entry_points(group=cls.GROUP))
16
+
17
+ @classmethod
18
+ def get_adapter(cls, provider: str):
19
+ adapters = {ep.name: ep for ep in entry_points(group=cls.GROUP)}
20
+ try:
21
+ adapter_cls = adapters[provider]
22
+ except KeyError:
23
+ available = ", ".join(sorted(adapters)) or "none installed"
24
+ raise RustyError(
25
+ error=f"adapter `{provider}` is not installed",
26
+ help=f"install it, e.g. `pip install sqliac-{provider}`",
27
+ details=f"available adapters: {available}",
28
+ )
29
+
30
+ return adapter_cls.load()()