ddsql 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.
ddsql-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Artem Davydov
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.
ddsql-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: ddsql
3
+ Version: 0.0.1
4
+ Summary: SQL Query Library
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: python,ddsql
8
+ Author: davyddd
9
+ Requires-Python: >=3.9,<3.15
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: ddutils (<0.2.0)
19
+ Requires-Dist: jinja2 (>=3.0.0,<4.0.0)
20
+ Project-URL: Repository, https://github.com/davyddd/ddsql
21
+ Description-Content-Type: text/markdown
22
+
23
+ # DDSQL
24
+
25
+ [![pypi](https://img.shields.io/pypi/v/ddsql.svg)](https://pypi.python.org/pypi/ddsql)
26
+ [![downloads](https://static.pepy.tech/badge/ddsql/month)](https://pepy.tech/project/ddsql)
27
+ [![versions](https://img.shields.io/pypi/pyversions/ddsql.svg)](https://github.com/davyddd/ddsql)
28
+ [![codecov](https://codecov.io/gh/davyddd/ddsql/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddsql)
29
+ [![license](https://img.shields.io/github/license/davyddd/ddsql.svg)](https://github.com/davyddd/ddsql/blob/main/LICENSE)
30
+
31
+ **DDSQL** is a Python library for building SQL queries with Jinja2 template rendering and database adapter support.
32
+ Query results are automatically deserialized into typed models.
33
+
34
+ ## Installation
35
+
36
+ Install the library using pip:
37
+ ```bash
38
+ pip install ddsql
39
+ ```
40
+
41
+ ## Serializer
42
+
43
+ The serializer converts Python types to their SQL representations.
44
+ You can use one of the built-in serializers (`PostgresSerializer`, `ClickhouseSerializer`)
45
+ or create your own by inheriting from `BaseSerializer`.
46
+
47
+ **Serialization Table**
48
+
49
+ | Python Type | Base | PostgreSQL | ClickHouse |
50
+ |----------------------|--------------------------|-------------------------------------|---------------------------------------------------|
51
+ | `None` | `NULL` | `NULL` | `NULL` |
52
+ | `bool` | `true`/`false` | `true`/`false` | `true`/`false` |
53
+ | `int` | `123` | `123` | `123` |
54
+ | `float`/`Decimal` | `45.67` | `45.67` | `45.67` |
55
+ | `str` | `'value'` | `'value'` | `'value'` |
56
+ | `UUID` | `'550e8400-...'` | `'550e8400-...'::uuid` | `toUUID('550e8400-...')` |
57
+ | `datetime` | `'2025-01-01T12:00:00'` | `'2025-01-01T12:00:00'::timestamp` | `parseDateTimeBestEffort('2025-01-01T12:00:00')` |
58
+ | `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
59
+ | `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
60
+
61
+ If you need to serialize a type not listed in the table, override the `serialize_other_object` method in your serializer:
62
+
63
+ ```python
64
+ from ddsql.serializers import BaseSerializer
65
+
66
+
67
+ class CustomSerializer(BaseSerializer):
68
+ def serialize_other_object(self, value):
69
+ if isinstance(value, CustomType):
70
+ return ...
71
+ ...
72
+ ```
73
+
74
+ To serialize values in SQL templates, wrap parameters with `serialize_value`:
75
+
76
+ ```sql
77
+ SELECT *
78
+ FROM users
79
+ WHERE
80
+ name = {{ serialize_value(name) }}
81
+ AND created_at > {{ serialize_value(created_at) }}
82
+ ```
83
+
84
+ To add custom functions to templates, override the `template_functions` property:
85
+
86
+ ```python
87
+ from ddsql.serializers import BaseSerializer
88
+
89
+
90
+ class CustomSerializer(BaseSerializer):
91
+ @property
92
+ def template_functions(self):
93
+ return {
94
+ **super().template_functions,
95
+ 'some_function': ...,
96
+ }
97
+ ```
98
+
99
+ ## Adapter
100
+
101
+ `Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
102
+ and define two required elements:
103
+ - **serializer** – an instance of a serializer for converting Python types to SQL representations;
104
+ - **_execute** method – the database-specific query execution logic.
105
+
106
+ ```python
107
+ from ddsql.adapter import Adapter
108
+ from ddsql.serializers import PostgresSerializer
109
+
110
+
111
+ class PostgresAdapter(Adapter):
112
+ serializer = PostgresSerializer()
113
+
114
+ async def _execute(self) -> Sequence[Dict[str, Any]]:
115
+ query = await self.get_query() # get the rendered SQL query
116
+ async with Atomic() as postgres_session:
117
+ result = await postgres_session.execute(text(query))
118
+ return [dict(zip(result.keys(), row)) for row in result.fetchall()]
119
+ ```
120
+
121
+ ## SQLBase
122
+
123
+ `SQLBase` is configured once per project and defines which adapters are available for query execution.
124
+ It serves as the central point that connects queries with database adapters.
125
+
126
+ Create a subclass with one or more adapters:
127
+
128
+ ```python
129
+ from ddsql.sqlbase import SQLBase
130
+ from ddsql.adapter import AdapterDescriptor
131
+
132
+
133
+ class SQL(SQLBase):
134
+ postgres: PostgresAdapter = AdapterDescriptor(PostgresAdapter)
135
+ clickhouse: ClickhouseAdapter = AdapterDescriptor(ClickhouseAdapter)
136
+ ```
137
+
138
+ Execution example:
139
+
140
+ ```python
141
+ from ddsql.query import Query
142
+
143
+
144
+ query = Query(...)
145
+ result = await SQL(query=query).with_params(email='test@test.test', is_deleted=False).postgres.execute()
146
+ ```
147
+
148
+ ## Query
149
+
150
+ `Query` knows where to get the template from and how to render a SQL query.
151
+ It also handles result deserialization via the `build_result` method,
152
+ which wraps raw database rows into the specified model (called internally by `Adapter.execute`).
153
+
154
+ Required parameters:
155
+ - **model** – a declarative class (e.g., dataclass) describing the output result structure;
156
+ - **text** or **path** – the SQL template source.
157
+
158
+ ### Inline Template (text)
159
+
160
+ ```python
161
+ from ddsql.query import Query
162
+
163
+
164
+ query = Query(
165
+ model=User,
166
+ text='SELECT user_id, name FROM users WHERE user_id = {{ serialize_value(user_id) }}'
167
+ )
168
+ ```
169
+
170
+ ### File Template (path)
171
+
172
+ For file-based templates, set the `SQL_TEMPLATES_DIR` environment variable to the directory containing your SQL files:
173
+
174
+ ```bash
175
+ export SQL_TEMPLATES_DIR=/app/src/templates/sql/
176
+ ```
177
+
178
+ Then use a relative path:
179
+
180
+ ```python
181
+ from ddsql.query import Query
182
+
183
+
184
+ # Loads template from /app/src/templates/sql/users/get_by_id.sql
185
+ query = Query(
186
+ model=User,
187
+ path='users/get_by_id.sql'
188
+ )
189
+ ```
190
+
191
+ ### Result
192
+
193
+ The result of query execution is a `Result` object that wraps the data into the specified model:
194
+ - `get()` – returns the first row as a model instance, or `None` if empty;
195
+ - `get_list()` – returns all rows as a tuple of model instances;
196
+ - `rows` – attribute for accessing raw data.
197
+
198
+ ## Complete Example
199
+
200
+ ```python
201
+ from dataclasses import dataclass
202
+ from datetime import datetime
203
+ from typing import Optional
204
+
205
+ from ddsql.query import Query
206
+ from ddsql.sqlbase import SQLBase
207
+ from ddsql.adapter import Adapter, AdapterDescriptor
208
+ from ddsql.serializers import PostgresSerializer
209
+
210
+
211
+ class PostgresAdapter(Adapter):
212
+ serializer = PostgresSerializer()
213
+
214
+ async def _execute(self):
215
+ ...
216
+
217
+
218
+ class SQL(SQLBase):
219
+ postgres: PostgresAdapter = AdapterDescriptor(PostgresAdapter)
220
+
221
+
222
+ @dataclass
223
+ class User:
224
+ user_id: int
225
+ name: str
226
+ email: Optional[str]
227
+ created_at: datetime
228
+ is_deleted: bool
229
+
230
+
231
+ query = Query(
232
+ model=User,
233
+ text='''
234
+ SELECT *
235
+ FROM users
236
+ WHERE
237
+ created_at > {{ serialize_value(created_after) }}
238
+ LIMIT {{ limit }}
239
+ '''
240
+ )
241
+
242
+
243
+ async def get_users():
244
+ result = await (
245
+ SQL(query=query)
246
+ .with_params(created_after=datetime(2025, 1, 1))
247
+ .with_params(limit=10)
248
+ .postgres
249
+ .execute()
250
+ )
251
+ return result.get_list()
252
+ ```
ddsql-0.0.1/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # DDSQL
2
+
3
+ [![pypi](https://img.shields.io/pypi/v/ddsql.svg)](https://pypi.python.org/pypi/ddsql)
4
+ [![downloads](https://static.pepy.tech/badge/ddsql/month)](https://pepy.tech/project/ddsql)
5
+ [![versions](https://img.shields.io/pypi/pyversions/ddsql.svg)](https://github.com/davyddd/ddsql)
6
+ [![codecov](https://codecov.io/gh/davyddd/ddsql/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddsql)
7
+ [![license](https://img.shields.io/github/license/davyddd/ddsql.svg)](https://github.com/davyddd/ddsql/blob/main/LICENSE)
8
+
9
+ **DDSQL** is a Python library for building SQL queries with Jinja2 template rendering and database adapter support.
10
+ Query results are automatically deserialized into typed models.
11
+
12
+ ## Installation
13
+
14
+ Install the library using pip:
15
+ ```bash
16
+ pip install ddsql
17
+ ```
18
+
19
+ ## Serializer
20
+
21
+ The serializer converts Python types to their SQL representations.
22
+ You can use one of the built-in serializers (`PostgresSerializer`, `ClickhouseSerializer`)
23
+ or create your own by inheriting from `BaseSerializer`.
24
+
25
+ **Serialization Table**
26
+
27
+ | Python Type | Base | PostgreSQL | ClickHouse |
28
+ |----------------------|--------------------------|-------------------------------------|---------------------------------------------------|
29
+ | `None` | `NULL` | `NULL` | `NULL` |
30
+ | `bool` | `true`/`false` | `true`/`false` | `true`/`false` |
31
+ | `int` | `123` | `123` | `123` |
32
+ | `float`/`Decimal` | `45.67` | `45.67` | `45.67` |
33
+ | `str` | `'value'` | `'value'` | `'value'` |
34
+ | `UUID` | `'550e8400-...'` | `'550e8400-...'::uuid` | `toUUID('550e8400-...')` |
35
+ | `datetime` | `'2025-01-01T12:00:00'` | `'2025-01-01T12:00:00'::timestamp` | `parseDateTimeBestEffort('2025-01-01T12:00:00')` |
36
+ | `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
37
+ | `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
38
+
39
+ If you need to serialize a type not listed in the table, override the `serialize_other_object` method in your serializer:
40
+
41
+ ```python
42
+ from ddsql.serializers import BaseSerializer
43
+
44
+
45
+ class CustomSerializer(BaseSerializer):
46
+ def serialize_other_object(self, value):
47
+ if isinstance(value, CustomType):
48
+ return ...
49
+ ...
50
+ ```
51
+
52
+ To serialize values in SQL templates, wrap parameters with `serialize_value`:
53
+
54
+ ```sql
55
+ SELECT *
56
+ FROM users
57
+ WHERE
58
+ name = {{ serialize_value(name) }}
59
+ AND created_at > {{ serialize_value(created_at) }}
60
+ ```
61
+
62
+ To add custom functions to templates, override the `template_functions` property:
63
+
64
+ ```python
65
+ from ddsql.serializers import BaseSerializer
66
+
67
+
68
+ class CustomSerializer(BaseSerializer):
69
+ @property
70
+ def template_functions(self):
71
+ return {
72
+ **super().template_functions,
73
+ 'some_function': ...,
74
+ }
75
+ ```
76
+
77
+ ## Adapter
78
+
79
+ `Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
80
+ and define two required elements:
81
+ - **serializer** – an instance of a serializer for converting Python types to SQL representations;
82
+ - **_execute** method – the database-specific query execution logic.
83
+
84
+ ```python
85
+ from ddsql.adapter import Adapter
86
+ from ddsql.serializers import PostgresSerializer
87
+
88
+
89
+ class PostgresAdapter(Adapter):
90
+ serializer = PostgresSerializer()
91
+
92
+ async def _execute(self) -> Sequence[Dict[str, Any]]:
93
+ query = await self.get_query() # get the rendered SQL query
94
+ async with Atomic() as postgres_session:
95
+ result = await postgres_session.execute(text(query))
96
+ return [dict(zip(result.keys(), row)) for row in result.fetchall()]
97
+ ```
98
+
99
+ ## SQLBase
100
+
101
+ `SQLBase` is configured once per project and defines which adapters are available for query execution.
102
+ It serves as the central point that connects queries with database adapters.
103
+
104
+ Create a subclass with one or more adapters:
105
+
106
+ ```python
107
+ from ddsql.sqlbase import SQLBase
108
+ from ddsql.adapter import AdapterDescriptor
109
+
110
+
111
+ class SQL(SQLBase):
112
+ postgres: PostgresAdapter = AdapterDescriptor(PostgresAdapter)
113
+ clickhouse: ClickhouseAdapter = AdapterDescriptor(ClickhouseAdapter)
114
+ ```
115
+
116
+ Execution example:
117
+
118
+ ```python
119
+ from ddsql.query import Query
120
+
121
+
122
+ query = Query(...)
123
+ result = await SQL(query=query).with_params(email='test@test.test', is_deleted=False).postgres.execute()
124
+ ```
125
+
126
+ ## Query
127
+
128
+ `Query` knows where to get the template from and how to render a SQL query.
129
+ It also handles result deserialization via the `build_result` method,
130
+ which wraps raw database rows into the specified model (called internally by `Adapter.execute`).
131
+
132
+ Required parameters:
133
+ - **model** – a declarative class (e.g., dataclass) describing the output result structure;
134
+ - **text** or **path** – the SQL template source.
135
+
136
+ ### Inline Template (text)
137
+
138
+ ```python
139
+ from ddsql.query import Query
140
+
141
+
142
+ query = Query(
143
+ model=User,
144
+ text='SELECT user_id, name FROM users WHERE user_id = {{ serialize_value(user_id) }}'
145
+ )
146
+ ```
147
+
148
+ ### File Template (path)
149
+
150
+ For file-based templates, set the `SQL_TEMPLATES_DIR` environment variable to the directory containing your SQL files:
151
+
152
+ ```bash
153
+ export SQL_TEMPLATES_DIR=/app/src/templates/sql/
154
+ ```
155
+
156
+ Then use a relative path:
157
+
158
+ ```python
159
+ from ddsql.query import Query
160
+
161
+
162
+ # Loads template from /app/src/templates/sql/users/get_by_id.sql
163
+ query = Query(
164
+ model=User,
165
+ path='users/get_by_id.sql'
166
+ )
167
+ ```
168
+
169
+ ### Result
170
+
171
+ The result of query execution is a `Result` object that wraps the data into the specified model:
172
+ - `get()` – returns the first row as a model instance, or `None` if empty;
173
+ - `get_list()` – returns all rows as a tuple of model instances;
174
+ - `rows` – attribute for accessing raw data.
175
+
176
+ ## Complete Example
177
+
178
+ ```python
179
+ from dataclasses import dataclass
180
+ from datetime import datetime
181
+ from typing import Optional
182
+
183
+ from ddsql.query import Query
184
+ from ddsql.sqlbase import SQLBase
185
+ from ddsql.adapter import Adapter, AdapterDescriptor
186
+ from ddsql.serializers import PostgresSerializer
187
+
188
+
189
+ class PostgresAdapter(Adapter):
190
+ serializer = PostgresSerializer()
191
+
192
+ async def _execute(self):
193
+ ...
194
+
195
+
196
+ class SQL(SQLBase):
197
+ postgres: PostgresAdapter = AdapterDescriptor(PostgresAdapter)
198
+
199
+
200
+ @dataclass
201
+ class User:
202
+ user_id: int
203
+ name: str
204
+ email: Optional[str]
205
+ created_at: datetime
206
+ is_deleted: bool
207
+
208
+
209
+ query = Query(
210
+ model=User,
211
+ text='''
212
+ SELECT *
213
+ FROM users
214
+ WHERE
215
+ created_at > {{ serialize_value(created_after) }}
216
+ LIMIT {{ limit }}
217
+ '''
218
+ )
219
+
220
+
221
+ async def get_users():
222
+ result = await (
223
+ SQL(query=query)
224
+ .with_params(created_after=datetime(2025, 1, 1))
225
+ .with_params(limit=10)
226
+ .postgres
227
+ .execute()
228
+ )
229
+ return result.get_list()
230
+ ```
@@ -0,0 +1,3 @@
1
+ from . import adapter, query, serializers, sqlbase
2
+
3
+ __all__ = ('adapter', 'query', 'sqlbase', 'serializers')
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Sequence, Type, TypeVar
5
+
6
+ from ddutils.annotation_helpers import is_subclass
7
+
8
+ from ddsql.serializers import BaseSerializer
9
+
10
+ if TYPE_CHECKING:
11
+ from ddsql.query import Result
12
+ from ddsql.sqlbase import SQLBase
13
+
14
+
15
+ class Adapter(ABC):
16
+ serializer: BaseSerializer
17
+
18
+ @classmethod
19
+ def __init_subclass__(cls, **kwargs):
20
+ serializer = getattr(cls, 'serializer', None)
21
+ serializer_class = getattr(serializer, '__class__', None)
22
+ if not is_subclass(serializer_class, BaseSerializer):
23
+ raise NotImplementedError(
24
+ 'Subclass of Adapter must define a valid serializer attribute that is a subclass of Serializer'
25
+ )
26
+
27
+ def __init__(self, sql: SQLBase) -> None:
28
+ self.sql = sql
29
+
30
+ async def get_query(self) -> str:
31
+ return await self.sql.query.render_template(
32
+ params=self.sql.params, template_functions=self.serializer.template_functions
33
+ )
34
+
35
+ async def execute(self) -> Result:
36
+ return self.sql.query.build_result(await self._execute())
37
+
38
+ @abstractmethod
39
+ async def _execute(self) -> Sequence[Dict[str, Any]]: # noqa: UP006
40
+ ...
41
+
42
+
43
+ AdapterT = TypeVar('AdapterT', bound=Adapter)
44
+
45
+
46
+ class AdapterDescriptor(Generic[AdapterT]):
47
+ adapter_class: Type[AdapterT] # noqa: UP006
48
+
49
+ def __init__(self, adapter_class: Type[AdapterT]): # noqa: UP006
50
+ self.adapter_class = adapter_class
51
+
52
+ def __get__(self, sql: SQLBase, sql_class: Optional[Type[SQLBase]] = None) -> AdapterT: # noqa: UP006, UP007
53
+ return self.adapter_class(sql)
54
+
55
+
56
+ __all__ = ('Adapter', 'AdapterDescriptor')
File without changes
@@ -0,0 +1,81 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Generic, Optional, Sequence, Tuple, Type, TypeVar
4
+
5
+ from jinja2 import Environment, FileSystemLoader, Template
6
+
7
+ SQL_TEMPLATES_DIR = os.getenv('SQL_TEMPLATES_DIR')
8
+
9
+
10
+ DataT = TypeVar('DataT')
11
+
12
+
13
+ class Result(Generic[DataT]):
14
+ rows: Sequence[dict[str, Any]]
15
+ model: Type[DataT]
16
+
17
+ def __init__(self, rows: Sequence[dict[str, Any]], model: Type[DataT]):
18
+ self.rows = rows
19
+ self.model = model
20
+
21
+ def get(self) -> Optional[DataT]:
22
+ if not self.rows:
23
+ return None
24
+ return self.model(**self.rows[0])
25
+
26
+ def get_list(self) -> Tuple[DataT, ...]:
27
+ return tuple(self.model(**row) for row in self.rows)
28
+
29
+
30
+ class Query(Generic[DataT]):
31
+ model: Type[DataT]
32
+ template: Template
33
+
34
+ def __init__(self, model: Type[DataT], text: Optional[str] = None, path: Optional[str] = None):
35
+ self.model = model
36
+
37
+ self.template = self.get_template(text, path)
38
+
39
+ @staticmethod
40
+ def get_template(text: Optional[str] = None, path: Optional[str] = None) -> Template:
41
+ if text:
42
+ file_system_loader = FileSystemLoader(Path('.'))
43
+ method = 'from_string'
44
+ query = text
45
+ elif path:
46
+ if SQL_TEMPLATES_DIR is None:
47
+ raise ValueError(
48
+ 'SQL Templates dir is not defined. Make sure the SQL_TEMPLATES_DIR environment variable is set correctly.'
49
+ )
50
+ file_system_loader = FileSystemLoader(Path(SQL_TEMPLATES_DIR))
51
+ method = 'get_template'
52
+ query = path
53
+ else:
54
+ raise ValueError('One of `text` or `path` must be specified')
55
+
56
+ environment = Environment(
57
+ loader=file_system_loader, trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, enable_async=True
58
+ )
59
+
60
+ return getattr(environment, method)(query)
61
+
62
+ def update_globals(self, envs: Optional[Dict[str, Any]] = None):
63
+ if envs:
64
+ self.template.environment.globals.update(envs)
65
+
66
+ @staticmethod
67
+ def format_sql(sql: str) -> str:
68
+ strings = sql.split('\n')
69
+ return '\n'.join([string.strip() for string in strings])
70
+
71
+ async def render_template(self, params: Dict[str, Any], template_functions: Optional[Dict[str, Any]] = None) -> str:
72
+ self.update_globals(template_functions)
73
+
74
+ rendered_template = await self.template.render_async(**params)
75
+ return self.format_sql(rendered_template)
76
+
77
+ def build_result(self, rows: Sequence[dict[str, Any]]) -> Result[DataT]:
78
+ return Result(rows=rows, model=self.model)
79
+
80
+
81
+ __all__ = ('Result', 'Query')
@@ -0,0 +1,5 @@
1
+ from .base import BaseSerializer
2
+ from .clickhouse import ClickhouseSerializer
3
+ from .postgres import PostgresSerializer
4
+
5
+ __all__ = ('BaseSerializer', 'ClickhouseSerializer', 'PostgresSerializer')
@@ -0,0 +1,71 @@
1
+ from collections.abc import Callable, Collection, Mapping
2
+ from datetime import date, datetime
3
+ from decimal import Decimal
4
+ from typing import Any, Dict, Union
5
+ from uuid import UUID
6
+
7
+
8
+ class BaseSerializer:
9
+ @property
10
+ def template_functions(self) -> Dict[str, Callable[[Any], str]]:
11
+ return {'serialize_value': self.serialize_value}
12
+
13
+ def serialize_value(self, value: Any) -> str:
14
+ if value is None:
15
+ return self.serialize_none(value)
16
+ elif isinstance(value, bool):
17
+ return self.serialize_bool(value)
18
+ elif isinstance(value, (int, float, Decimal)):
19
+ return self.serialize_number(value)
20
+ elif isinstance(value, str):
21
+ return self.serialize_string(value)
22
+ elif isinstance(value, UUID):
23
+ return self.serialize_uuid(value)
24
+ elif isinstance(value, datetime):
25
+ return self.serialize_datetime(value)
26
+ elif isinstance(value, date):
27
+ # the check for date must come after datetime,
28
+ # because a datetime instance can also be identified as a date
29
+ return self.serialize_date(value)
30
+ elif isinstance(value, Collection) and not isinstance(value, Mapping):
31
+ return self.serialize_collection(value)
32
+ else:
33
+ return self.serialize_other_object(value)
34
+
35
+ @staticmethod
36
+ def serialize_none(value) -> str: # noqa: ARG004
37
+ return 'NULL'
38
+
39
+ @staticmethod
40
+ def serialize_bool(value: bool) -> str:
41
+ return f'{value}'.lower()
42
+
43
+ @staticmethod
44
+ def serialize_number(value: Union[int, float, Decimal]) -> str:
45
+ return f'{value}'
46
+
47
+ @staticmethod
48
+ def serialize_string(value: str) -> str:
49
+ return f"'{value}'"
50
+
51
+ @staticmethod
52
+ def serialize_uuid(value: UUID) -> str:
53
+ return f"'{value}'"
54
+
55
+ @staticmethod
56
+ def serialize_datetime(value: datetime) -> str:
57
+ return f"'{value.isoformat()}'"
58
+
59
+ @staticmethod
60
+ def serialize_date(value: date) -> str:
61
+ return f"'{value.isoformat()}'"
62
+
63
+ def serialize_collection(self, value: Collection) -> str:
64
+ items = ', '.join(self.serialize_value(item) for item in value)
65
+ return f'({items})'
66
+
67
+ def serialize_other_object(self, value: Any) -> str:
68
+ raise NotImplementedError()
69
+
70
+
71
+ __all__ = ('BaseSerializer',)
@@ -0,0 +1,21 @@
1
+ from datetime import date, datetime
2
+ from uuid import UUID
3
+
4
+ from ddsql.serializers import BaseSerializer
5
+
6
+
7
+ class ClickhouseSerializer(BaseSerializer):
8
+ @staticmethod
9
+ def serialize_uuid(value: UUID) -> str:
10
+ return f"toUUID('{value}')"
11
+
12
+ @staticmethod
13
+ def serialize_datetime(value: datetime) -> str:
14
+ return f"parseDateTimeBestEffort('{value.isoformat()}')"
15
+
16
+ @staticmethod
17
+ def serialize_date(value: date) -> str:
18
+ return f"toDate('{value.isoformat()}')"
19
+
20
+
21
+ __all__ = ('ClickhouseSerializer',)
@@ -0,0 +1,21 @@
1
+ from datetime import date, datetime
2
+ from uuid import UUID
3
+
4
+ from ddsql.serializers import BaseSerializer
5
+
6
+
7
+ class PostgresSerializer(BaseSerializer):
8
+ @staticmethod
9
+ def serialize_uuid(value: UUID) -> str:
10
+ return f"'{value}'::uuid"
11
+
12
+ @staticmethod
13
+ def serialize_datetime(value: datetime) -> str:
14
+ return f"'{value.isoformat()}'::timestamp"
15
+
16
+ @staticmethod
17
+ def serialize_date(value: date) -> str:
18
+ return f"'{value.isoformat()}'::date"
19
+
20
+
21
+ __all__ = ('PostgresSerializer',)
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from typing import TYPE_CHECKING, Any, Dict
5
+
6
+ from ddutils.annotation_helpers import is_subclass
7
+ from ddutils.class_helpers import classproperty
8
+
9
+ from ddsql.adapter import Adapter
10
+
11
+ if TYPE_CHECKING:
12
+ from ddsql.query import Query
13
+
14
+
15
+ class SQLBase(ABC):
16
+ """
17
+ Abstract base class for SQL query execution with adapter-based database connections.
18
+
19
+ This class provides a foundation for executing SQL queries against different database backends
20
+ using adapters. It handles query preparation, parameter management, and adapter selection.
21
+
22
+ Subclasses must define at least one adapter as a class attribute.
23
+
24
+ Attributes:
25
+ query: The SQL query to execute, either as a Query object or raw SQL string
26
+ params: Dictionary of parameters to be used in the query
27
+ """
28
+
29
+ query: Query
30
+ params: Dict[str, Any]
31
+
32
+ @classmethod
33
+ def __init_subclass__(cls, **kwargs):
34
+ if not cls.has_adapters:
35
+ raise NotImplementedError('Subclasses must define at least one adapter')
36
+
37
+ @classproperty
38
+ def has_adapters(cls) -> bool:
39
+ for field in cls.__annotations__:
40
+ adapter = getattr(cls, field, None)
41
+ adapter_class = getattr(adapter, '__class__', None)
42
+ if adapter_class and is_subclass(adapter_class, Adapter):
43
+ return True
44
+
45
+ return False
46
+
47
+ def __init__(self, query: Query) -> None:
48
+ self.query = query
49
+ self.params = {}
50
+
51
+ def with_params(self, **params: Any) -> SQLBase:
52
+ self.params = {**self.params, **params}
53
+ return self
54
+
55
+
56
+ __all__ = ('SQLBase',)
@@ -0,0 +1,33 @@
1
+ [tool.poetry]
2
+ name = "ddsql"
3
+ description = "SQL Query Library"
4
+ version = "0.0.1"
5
+ authors = ["davyddd"]
6
+ license = "MIT"
7
+ repository = "https://github.com/davyddd/ddsql"
8
+ readme = "README.md"
9
+ keywords = ["python", "ddsql"]
10
+ packages = [{include = "ddsql"}]
11
+
12
+ [tool.poetry.dependencies]
13
+ python = ">=3.9,<3.15"
14
+ ddutils = "<0.2.0"
15
+ jinja2 = ">=3.0.0,<4.0.0"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ ipdb = "0.13.9"
19
+ ipython = "8.12.3"
20
+
21
+ [tool.poetry.group.test.dependencies]
22
+ pytest = "8.1.1"
23
+ pytest-cov = "5.0.0"
24
+ parameterized = "0.9.0"
25
+ typing-extensions = "^4.12.2"
26
+
27
+ [tool.poetry.group.linter.dependencies]
28
+ mypy = "1.18.2"
29
+ ruff = "0.14.4"
30
+
31
+ [build-system]
32
+ requires = ["poetry-core>=1.0.0"]
33
+ build-backend = "poetry.core.masonry.api"