david8 0.dev5__tar.gz → 0.1.0b1__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.
- {david8-0.dev5 → david8-0.1.0b1}/PKG-INFO +3 -3
- {david8-0.dev5 → david8-0.1.0b1}/README.md +1 -1
- david8-0.1.0b1/david8/core/arg_convertors.py +8 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/base_aliased.py +9 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/base_dml.py +35 -31
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/fn_generator.py +25 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/sql_92_join.py +1 -1
- {david8-0.dev5 → david8-0.1.0b1}/david8/expressions.py +11 -1
- david8-0.1.0b1/david8/functions.py +24 -0
- david8-0.1.0b1/david8/logical_operators.py +43 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/predicates.py +18 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8.egg-info/PKG-INFO +3 -3
- {david8-0.dev5 → david8-0.1.0b1}/david8.egg-info/SOURCES.txt +1 -0
- {david8-0.dev5 → david8-0.1.0b1}/pyproject.toml +2 -2
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_expressions.py +19 -2
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_functions.py +95 -1
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_join.py +57 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_logical_operators.py +34 -1
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_predicates.py +48 -5
- david8-0.dev5/david8/functions.py +0 -9
- david8-0.dev5/david8/logical_operators.py +0 -27
- {david8-0.dev5 → david8-0.1.0b1}/LICENSE +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/__init__.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/__init__.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/base_dialect.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/base_params.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/base_query_builder.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/core/log.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/joins.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/param_styles.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/protocols/__init__.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/protocols/dialect.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/protocols/dml.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/protocols/query_builder.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8/protocols/sql.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8.egg-info/dependency_links.txt +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/david8.egg-info/top_level.txt +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/setup.cfg +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_group_by.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_order_by.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_select.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_union.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_where_predicates.py +0 -0
- {david8-0.dev5 → david8-0.1.0b1}/tests/test_with_select.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: david8
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.0b1
|
|
4
4
|
Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL
|
|
5
5
|
Author-email: Danila Ganchar <danila.ganchar@gmail.com>
|
|
6
6
|
Maintainer-email: Danila Ganchar <danila.ganchar@gmail.com>
|
|
@@ -10,7 +10,7 @@ Project-URL: Issues, https://github.com/d-ganchar/david8/issues
|
|
|
10
10
|
Project-URL: CI, https://github.com/d-ganchar/david8/actions
|
|
11
11
|
Project-URL: Documentation, https://github.com/d-ganchar/david8/wiki
|
|
12
12
|
Project-URL: Source, https://github.com/d-ganchar/david8
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Topic :: Database
|
|
@@ -37,6 +37,6 @@ predictable and ergonomic workflow
|
|
|
37
37
|
|
|
38
38
|
- Zero dependencies
|
|
39
39
|
- Zero global objects
|
|
40
|
-
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, and
|
|
40
|
+
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite
|
|
41
41
|
|
|
42
42
|
See [Wiki](https://github.com/d-ganchar/david8/wiki)
|
|
@@ -12,6 +12,6 @@ predictable and ergonomic workflow
|
|
|
12
12
|
|
|
13
13
|
- Zero dependencies
|
|
14
14
|
- Zero global objects
|
|
15
|
-
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, and
|
|
15
|
+
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite
|
|
16
16
|
|
|
17
17
|
See [Wiki](https://github.com/d-ganchar/david8/wiki)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ..protocols.dialect import DialectProtocol
|
|
2
|
+
from ..protocols.sql import ExprProtocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def to_col_or_expr(value: str | ExprProtocol, dialect: DialectProtocol) -> str:
|
|
6
|
+
if isinstance(value, str):
|
|
7
|
+
return dialect.quote_ident(value)
|
|
8
|
+
return value.get_sql(dialect)
|
|
@@ -35,6 +35,15 @@ class Value(BaseAliased, ValueProtocol):
|
|
|
35
35
|
return f'{self._value}'
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class SqlType(BaseAliased):
|
|
39
|
+
def __init__(self, name: str) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._name = name
|
|
42
|
+
|
|
43
|
+
def _get_sql(self, dialect: DialectProtocol) -> str:
|
|
44
|
+
return f'{self._name}'
|
|
45
|
+
|
|
46
|
+
|
|
38
47
|
class Parameter(BaseAliased, ParameterProtocol):
|
|
39
48
|
def __init__(self, value: str | int | float, fixed_name: bool = False) -> None:
|
|
40
49
|
super().__init__()
|
|
@@ -67,18 +67,18 @@ class BaseSelect(SelectProtocol):
|
|
|
67
67
|
self.limit_value = value
|
|
68
68
|
return self
|
|
69
69
|
|
|
70
|
-
def _columns_to_sql(self) -> str:
|
|
70
|
+
def _columns_to_sql(self, dialect: DialectProtocol) -> str:
|
|
71
71
|
return ', '.join(
|
|
72
|
-
|
|
73
|
-
if isinstance(column, str) else column.get_sql(
|
|
72
|
+
dialect.quote_ident(column)
|
|
73
|
+
if isinstance(column, str) else column.get_sql(dialect)
|
|
74
74
|
for column in self.select_columns
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
def _where_to_sql(self) -> str:
|
|
77
|
+
def _where_to_sql(self, dialect: DialectProtocol) -> str:
|
|
78
78
|
if not self.where_conditions:
|
|
79
79
|
return ''
|
|
80
80
|
|
|
81
|
-
return f" WHERE {' AND '.join(predicate.get_sql(
|
|
81
|
+
return f" WHERE {' AND '.join(predicate.get_sql(dialect) for predicate in self.where_conditions)}"
|
|
82
82
|
|
|
83
83
|
def _order_by_to_sql(self) -> str:
|
|
84
84
|
if not self.order_by_expressions:
|
|
@@ -91,60 +91,60 @@ class BaseSelect(SelectProtocol):
|
|
|
91
91
|
|
|
92
92
|
return f" ORDER BY {', '.join(order_items)}"
|
|
93
93
|
|
|
94
|
-
def _with_queries_to_sql(self) -> str:
|
|
94
|
+
def _with_queries_to_sql(self, dialect: DialectProtocol) -> str:
|
|
95
95
|
if not self.with_queries:
|
|
96
96
|
return ''
|
|
97
97
|
|
|
98
98
|
with_items = ', '.join(
|
|
99
|
-
f'{
|
|
99
|
+
f'{dialect.quote_ident(alias)} AS ({query.get_sql(dialect)})'
|
|
100
100
|
for alias, query in self.with_queries
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
return f'WITH {with_items} '
|
|
104
104
|
|
|
105
|
-
def _from_to_sql(self) -> str:
|
|
105
|
+
def _from_to_sql(self, dialect: DialectProtocol) -> str:
|
|
106
106
|
if self.from_query_expr:
|
|
107
|
-
source = f'({self.from_query_expr.get_sql(
|
|
107
|
+
source = f'({self.from_query_expr.get_sql(dialect)})'
|
|
108
108
|
elif self.from_table_value:
|
|
109
|
-
source =
|
|
109
|
+
source = dialect.quote_ident(self.from_table_value)
|
|
110
110
|
if self.from_db_value:
|
|
111
|
-
source = f'{
|
|
111
|
+
source = f'{dialect.quote_ident(self.from_db_value)}.{source}'
|
|
112
112
|
else:
|
|
113
113
|
return ''
|
|
114
114
|
|
|
115
|
-
source = f'{source} AS {
|
|
115
|
+
source = f'{source} AS {dialect.quote_ident(self.source_alias)}' if self.source_alias else source
|
|
116
116
|
return f' FROM {source}'
|
|
117
117
|
|
|
118
|
-
def _union_to_sql(self) -> str:
|
|
118
|
+
def _union_to_sql(self, dialect: DialectProtocol) -> str:
|
|
119
119
|
if not self.unions:
|
|
120
120
|
return ''
|
|
121
121
|
|
|
122
122
|
return ' ' + ' '.join(
|
|
123
|
-
f"UNION{' ALL' if union_type else ''} {query.get_sql(
|
|
123
|
+
f"UNION{' ALL' if union_type else ''} {query.get_sql(dialect)}"
|
|
124
124
|
for union_type, query in self.unions
|
|
125
125
|
)
|
|
126
126
|
|
|
127
|
-
def _group_by_to_sql(self) -> str:
|
|
127
|
+
def _group_by_to_sql(self, dialect: DialectProtocol) -> str:
|
|
128
128
|
if not self.group_by_expressions:
|
|
129
129
|
return ''
|
|
130
130
|
|
|
131
131
|
return ' GROUP BY ' + ', '.join(
|
|
132
|
-
f"{
|
|
132
|
+
f"{dialect.quote_ident(f) if isinstance(f, str) else str(f)}"
|
|
133
133
|
for f in self.group_by_expressions
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
-
def _having_to_sql(self) -> str:
|
|
136
|
+
def _having_to_sql(self, dialect: DialectProtocol) -> str:
|
|
137
137
|
if not self.having_expressions:
|
|
138
138
|
return ''
|
|
139
139
|
|
|
140
|
-
return f" HAVING {' AND '.join(p.get_sql(
|
|
140
|
+
return f" HAVING {' AND '.join(p.get_sql(dialect) for p in self.having_expressions)}"
|
|
141
141
|
|
|
142
|
-
def _joins_to_sql(self) -> str:
|
|
142
|
+
def _joins_to_sql(self, dialect: DialectProtocol) -> str:
|
|
143
143
|
if not self.joins:
|
|
144
144
|
return ''
|
|
145
145
|
|
|
146
146
|
return ' ' + ' '.join(
|
|
147
|
-
join.get_sql(
|
|
147
|
+
join.get_sql(dialect)
|
|
148
148
|
for join in self.joins
|
|
149
149
|
)
|
|
150
150
|
|
|
@@ -169,21 +169,25 @@ class BaseSelect(SelectProtocol):
|
|
|
169
169
|
"""
|
|
170
170
|
if dialect is None:
|
|
171
171
|
self.dialect.get_paramstyle().reset_parameters()
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
dialect = self.dialect
|
|
173
|
+
log_query = True
|
|
174
|
+
else:
|
|
175
|
+
log_query = False
|
|
176
|
+
|
|
177
|
+
with_query = self._with_queries_to_sql(dialect)
|
|
178
|
+
select = self._columns_to_sql(dialect)
|
|
179
|
+
from_ref = self._from_to_sql(dialect)
|
|
180
|
+
joins = self._joins_to_sql(dialect)
|
|
181
|
+
where = self._where_to_sql(dialect)
|
|
182
|
+
group_by = self._group_by_to_sql(dialect)
|
|
183
|
+
having = self._having_to_sql(dialect)
|
|
184
|
+
union = self._union_to_sql(dialect)
|
|
181
185
|
order_by = self._order_by_to_sql()
|
|
182
186
|
|
|
183
187
|
limit = f' LIMIT {self.limit_value}' if self.limit_value else ''
|
|
184
188
|
sql = f'{with_query}SELECT {select}{from_ref}{joins}{where}{group_by}{order_by}{having}{limit}{union}'
|
|
185
|
-
if
|
|
186
|
-
log.info('%s
|
|
189
|
+
if log_query:
|
|
190
|
+
log.info('%s\n%s', sql, self.get_parameters())
|
|
187
191
|
|
|
188
192
|
return sql
|
|
189
193
|
|
|
@@ -65,3 +65,28 @@ class _OneArgDistinctFn(Function):
|
|
|
65
65
|
class OneArgDistinctCallableFactory(FnCallableFactory):
|
|
66
66
|
def __call__(self, column: str, distinct: bool = False) -> FunctionProtocol:
|
|
67
67
|
return _OneArgDistinctFn(self.name, column, distinct)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclasses.dataclass(slots=True)
|
|
71
|
+
class _StrArgFn(Function):
|
|
72
|
+
"""
|
|
73
|
+
upper(col_name)
|
|
74
|
+
lower(col_name)
|
|
75
|
+
etc
|
|
76
|
+
"""
|
|
77
|
+
value: str | ExprProtocol = ''
|
|
78
|
+
|
|
79
|
+
def _get_sql(self, dialect: DialectProtocol) -> str:
|
|
80
|
+
if isinstance(self.value, str):
|
|
81
|
+
value = dialect.quote_ident(self.value)
|
|
82
|
+
else:
|
|
83
|
+
value = self.value.get_sql(dialect)
|
|
84
|
+
return f"{self.name}({value})"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclasses.dataclass(slots=True)
|
|
88
|
+
class StrArgCallableFactory(FnCallableFactory):
|
|
89
|
+
value: str = ''
|
|
90
|
+
|
|
91
|
+
def __call__(self, value: str | ExprProtocol) -> FunctionProtocol:
|
|
92
|
+
return _StrArgFn(self.name, value)
|
|
@@ -21,7 +21,7 @@ class Sql92Join(Sql92JoinProtocol):
|
|
|
21
21
|
|
|
22
22
|
def get_sql(self, dialect: DialectProtocol) -> str:
|
|
23
23
|
if self.from_query:
|
|
24
|
-
source = self.from_query.get_sql(dialect)
|
|
24
|
+
source = f'({self.from_query.get_sql(dialect)})'
|
|
25
25
|
else:
|
|
26
26
|
table, db = self.from_table
|
|
27
27
|
source = dialect.quote_ident(table)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from david8.core.base_aliased import Column as _Column
|
|
2
2
|
from david8.core.base_aliased import Parameter as _Parameter
|
|
3
|
+
from david8.core.base_aliased import SqlType as _SqlType
|
|
3
4
|
from david8.core.base_aliased import Value as _Value
|
|
4
|
-
from david8.protocols.sql import ValueProtocol
|
|
5
|
+
from david8.protocols.sql import AliasedProtocol, ValueProtocol
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def val(value: str | int | float) -> ValueProtocol:
|
|
@@ -12,3 +13,12 @@ def col(name: str) -> _Column:
|
|
|
12
13
|
|
|
13
14
|
def param(value: str | int | float, fixed_name: bool = False) -> _Parameter:
|
|
14
15
|
return _Parameter(value, fixed_name)
|
|
16
|
+
|
|
17
|
+
def true() -> AliasedProtocol:
|
|
18
|
+
return _SqlType('TRUE')
|
|
19
|
+
|
|
20
|
+
def false() -> AliasedProtocol:
|
|
21
|
+
return _SqlType('FALSE')
|
|
22
|
+
|
|
23
|
+
def null() -> AliasedProtocol:
|
|
24
|
+
return _SqlType('NULL')
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from david8.core.fn_generator import (
|
|
2
|
+
OneArgDistinctCallableFactory as _AggDistinctCallableFactory,
|
|
3
|
+
)
|
|
4
|
+
from david8.core.fn_generator import (
|
|
5
|
+
SeparatedStrArgsCallableFactory as _SeparatedStrArgsCallableFactory,
|
|
6
|
+
)
|
|
7
|
+
from david8.core.fn_generator import (
|
|
8
|
+
StrArgCallableFactory as _StrArgCallableFactory,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# length('col_name') | length(val('MyVAR')) | length(param('myParam')) | length(concat('col1', 'col2'))
|
|
12
|
+
lower = _StrArgCallableFactory(name='lower')
|
|
13
|
+
upper = _StrArgCallableFactory(name='upper')
|
|
14
|
+
length = _StrArgCallableFactory(name='length')
|
|
15
|
+
trim = _StrArgCallableFactory(name='trim')
|
|
16
|
+
|
|
17
|
+
# count('name', True) => count(DISTINCT name), min_('age', True) => min(DISTINCT age) = 33
|
|
18
|
+
count = _AggDistinctCallableFactory(name='count')
|
|
19
|
+
avg = _AggDistinctCallableFactory(name='avg')
|
|
20
|
+
sum_ = _AggDistinctCallableFactory(name='sum')
|
|
21
|
+
max_ = _AggDistinctCallableFactory(name='max')
|
|
22
|
+
min_ = _AggDistinctCallableFactory(name='min')
|
|
23
|
+
|
|
24
|
+
concat = _SeparatedStrArgsCallableFactory(name='concat', separator=', ')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from .core.arg_convertors import to_col_or_expr
|
|
5
|
+
from .protocols.dialect import DialectProtocol
|
|
6
|
+
from .protocols.sql import ExprProtocol, LogicalOperatorProtocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(slots=True)
|
|
10
|
+
class _ArgsLogicalOperator(LogicalOperatorProtocol):
|
|
11
|
+
_name: str
|
|
12
|
+
_conditions: Union[ExprProtocol, 'LogicalOperatorProtocol', ...]
|
|
13
|
+
|
|
14
|
+
def get_sql(self, dialect: DialectProtocol) -> str:
|
|
15
|
+
conditions = f' {self._name} '.join(c.get_sql(dialect) for c in self._conditions)
|
|
16
|
+
return f'({conditions})'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass(slots=True)
|
|
20
|
+
class _NotLogicalOperator(LogicalOperatorProtocol):
|
|
21
|
+
_value: str | ExprProtocol
|
|
22
|
+
|
|
23
|
+
def get_sql(self, dialect: DialectProtocol) -> str:
|
|
24
|
+
return f'NOT {to_col_or_expr(self._value, dialect)}'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def or_(*args: ExprProtocol | LogicalOperatorProtocol) -> LogicalOperatorProtocol:
|
|
28
|
+
return _ArgsLogicalOperator(_name='OR', _conditions=args)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def and_(*args: ExprProtocol | LogicalOperatorProtocol) -> LogicalOperatorProtocol:
|
|
32
|
+
return _ArgsLogicalOperator(_name='AND', _conditions=args)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def xor(*args: ExprProtocol | LogicalOperatorProtocol) -> LogicalOperatorProtocol:
|
|
36
|
+
return _ArgsLogicalOperator(_name='XOR', _conditions=args)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def not_(value: str | ExprProtocol) -> LogicalOperatorProtocol:
|
|
40
|
+
"""
|
|
41
|
+
.where(not_('column_name')) => WHERE NOT column_name
|
|
42
|
+
"""
|
|
43
|
+
return _NotLogicalOperator(_value=value)
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
from .core.arg_convertors import to_col_or_expr
|
|
1
2
|
from .core.base_aliased import BaseAliased as _BaseAliased
|
|
2
3
|
from .protocols.dialect import DialectProtocol
|
|
3
4
|
from .protocols.sql import ExprProtocol, PredicateProtocol
|
|
4
5
|
|
|
5
6
|
|
|
7
|
+
class _IsPredicate(PredicateProtocol, _BaseAliased):
|
|
8
|
+
def __init__(self, left: str | ExprProtocol, right: str | ExprProtocol):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self._left = left
|
|
11
|
+
self._right = right
|
|
12
|
+
|
|
13
|
+
def _get_sql(self, dialect: DialectProtocol) -> str:
|
|
14
|
+
left = to_col_or_expr(self._left, dialect)
|
|
15
|
+
right = to_col_or_expr(self._right, dialect)
|
|
16
|
+
return f'{left} IS {right}'
|
|
17
|
+
|
|
18
|
+
|
|
6
19
|
class _LeftColRightParamPredicate(PredicateProtocol, _BaseAliased):
|
|
7
20
|
def __init__(
|
|
8
21
|
self,
|
|
@@ -104,6 +117,11 @@ def between(
|
|
|
104
117
|
) -> PredicateProtocol:
|
|
105
118
|
return _BetweenPredicate(column, start, end)
|
|
106
119
|
|
|
120
|
+
# .where(is_('is_active', false)) => WHERE is_active IS FALSE
|
|
121
|
+
# .where(is_('is_active', not_(false))) => WHERE is_active IS NOT FALSE
|
|
122
|
+
def is_(left: str | ExprProtocol, right: str | ExprProtocol) -> PredicateProtocol:
|
|
123
|
+
return _IsPredicate(left, right)
|
|
124
|
+
|
|
107
125
|
# columns predicates. example: WHERE col_name = col_name2, col_name != col_name2 ...
|
|
108
126
|
def eq_c(left_column: str, right_column: str) -> PredicateProtocol:
|
|
109
127
|
return _LeftColRightColPredicate(left_column, right_column, '=')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: david8
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.0b1
|
|
4
4
|
Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL
|
|
5
5
|
Author-email: Danila Ganchar <danila.ganchar@gmail.com>
|
|
6
6
|
Maintainer-email: Danila Ganchar <danila.ganchar@gmail.com>
|
|
@@ -10,7 +10,7 @@ Project-URL: Issues, https://github.com/d-ganchar/david8/issues
|
|
|
10
10
|
Project-URL: CI, https://github.com/d-ganchar/david8/actions
|
|
11
11
|
Project-URL: Documentation, https://github.com/d-ganchar/david8/wiki
|
|
12
12
|
Project-URL: Source, https://github.com/d-ganchar/david8
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Topic :: Database
|
|
@@ -37,6 +37,6 @@ predictable and ergonomic workflow
|
|
|
37
37
|
|
|
38
38
|
- Zero dependencies
|
|
39
39
|
- Zero global objects
|
|
40
|
-
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, and
|
|
40
|
+
- Built-in base support for ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite
|
|
41
41
|
|
|
42
42
|
See [Wiki](https://github.com/d-ganchar/david8/wiki)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "david8"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.1.0b1"
|
|
4
4
|
description = "Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL"
|
|
5
5
|
authors = [{name = "Danila Ganchar", email = "danila.ganchar@gmail.com"}]
|
|
6
6
|
maintainers = [{name = "Danila Ganchar", email = "danila.ganchar@gmail.com"}]
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Topic :: Software Development :: Libraries",
|
|
18
18
|
"Topic :: Database",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from parameterized import parameterized
|
|
2
2
|
|
|
3
3
|
from david8 import get_default_qb
|
|
4
|
-
from david8.expressions import col, param, val
|
|
4
|
+
from david8.expressions import col, false, null, param, true, val
|
|
5
5
|
from david8.param_styles import (
|
|
6
6
|
FormatParamStyle,
|
|
7
7
|
NamedParamStyle,
|
|
@@ -10,7 +10,7 @@ from david8.param_styles import (
|
|
|
10
10
|
QMarkParamStyle,
|
|
11
11
|
)
|
|
12
12
|
from david8.protocols.dialect import ParamStyleProtocol
|
|
13
|
-
from david8.protocols.sql import QueryProtocol
|
|
13
|
+
from david8.protocols.sql import AliasedProtocol, QueryProtocol
|
|
14
14
|
from tests.base_test import BaseTest
|
|
15
15
|
|
|
16
16
|
|
|
@@ -129,3 +129,20 @@ class TestExpressions(BaseTest):
|
|
|
129
129
|
self.assertEqual(query.get_parameters(), exp_dict_params)
|
|
130
130
|
self.assertEqual(query.get_list_parameters(), exp_list_params)
|
|
131
131
|
self.assertEqual(query.get_tuple_parameters(), tuple(exp_list_params))
|
|
132
|
+
|
|
133
|
+
@parameterized.expand([
|
|
134
|
+
(
|
|
135
|
+
true(),
|
|
136
|
+
'SELECT TRUE',
|
|
137
|
+
),
|
|
138
|
+
(
|
|
139
|
+
false(),
|
|
140
|
+
'SELECT FALSE',
|
|
141
|
+
),
|
|
142
|
+
(
|
|
143
|
+
null(),
|
|
144
|
+
'SELECT NULL',
|
|
145
|
+
),
|
|
146
|
+
])
|
|
147
|
+
def test_sql_type(self, expr: AliasedProtocol, exp_sql: str):
|
|
148
|
+
self.assertEqual(self.qb.select(expr).get_sql(), exp_sql)
|
|
@@ -2,9 +2,10 @@ from parameterized import parameterized
|
|
|
2
2
|
|
|
3
3
|
from david8 import QueryBuilderProtocol
|
|
4
4
|
from david8.expressions import param, val
|
|
5
|
-
from david8.functions import avg, concat, count, max_, min_, sum_
|
|
5
|
+
from david8.functions import avg, concat, count, length, lower, max_, min_, sum_, trim, upper
|
|
6
6
|
from david8.logical_operators import and_, or_, xor
|
|
7
7
|
from david8.predicates import eq_e
|
|
8
|
+
from david8.protocols.dml import FunctionProtocol
|
|
8
9
|
from tests.base_test import BaseTest
|
|
9
10
|
|
|
10
11
|
|
|
@@ -108,3 +109,96 @@ class TestAggFunctions(BaseTest):
|
|
|
108
109
|
'SELECT * FROM test HAVING (count(name) = 2 OR max(price) = 1000 OR (min(age) = 27 '
|
|
109
110
|
'AND sum(money) = 100) OR (avg(success) = 99 XOR avg(happiness) = 101))'
|
|
110
111
|
)
|
|
112
|
+
|
|
113
|
+
@parameterized.expand([
|
|
114
|
+
# length
|
|
115
|
+
(
|
|
116
|
+
length(concat('col1', 'col2')),
|
|
117
|
+
'SELECT length(concat(col1, col2))',
|
|
118
|
+
'SELECT length(concat("col1", "col2"))',
|
|
119
|
+
{},
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
length('col_name'),
|
|
123
|
+
'SELECT length(col_name)',
|
|
124
|
+
'SELECT length("col_name")',
|
|
125
|
+
{},
|
|
126
|
+
),
|
|
127
|
+
(
|
|
128
|
+
length(val('MyVAR')),
|
|
129
|
+
"SELECT length('MyVAR')",
|
|
130
|
+
"SELECT length('MyVAR')",
|
|
131
|
+
{},
|
|
132
|
+
),
|
|
133
|
+
(
|
|
134
|
+
length(param('myParam')),
|
|
135
|
+
'SELECT length(%(p1)s)',
|
|
136
|
+
'SELECT length(%(p1)s)',
|
|
137
|
+
{'p1': 'myParam'},
|
|
138
|
+
),
|
|
139
|
+
# upper
|
|
140
|
+
(
|
|
141
|
+
upper('col_name'),
|
|
142
|
+
'SELECT upper(col_name)',
|
|
143
|
+
'SELECT upper("col_name")',
|
|
144
|
+
{},
|
|
145
|
+
),
|
|
146
|
+
(
|
|
147
|
+
upper(val('MyVAR')),
|
|
148
|
+
"SELECT upper('MyVAR')",
|
|
149
|
+
"SELECT upper('MyVAR')",
|
|
150
|
+
{},
|
|
151
|
+
),
|
|
152
|
+
(
|
|
153
|
+
upper(param('myParam')),
|
|
154
|
+
'SELECT upper(%(p1)s)',
|
|
155
|
+
'SELECT upper(%(p1)s)',
|
|
156
|
+
{'p1': 'myParam'},
|
|
157
|
+
),
|
|
158
|
+
# lower
|
|
159
|
+
(
|
|
160
|
+
lower('col_name'),
|
|
161
|
+
'SELECT lower(col_name)',
|
|
162
|
+
'SELECT lower("col_name")',
|
|
163
|
+
{},
|
|
164
|
+
),
|
|
165
|
+
(
|
|
166
|
+
lower(val('MyVAR')),
|
|
167
|
+
"SELECT lower('MyVAR')",
|
|
168
|
+
"SELECT lower('MyVAR')",
|
|
169
|
+
{},
|
|
170
|
+
),
|
|
171
|
+
(
|
|
172
|
+
lower(param('myParam')),
|
|
173
|
+
'SELECT lower(%(p1)s)',
|
|
174
|
+
'SELECT lower(%(p1)s)',
|
|
175
|
+
{'p1': 'myParam'},
|
|
176
|
+
),
|
|
177
|
+
# trim
|
|
178
|
+
(
|
|
179
|
+
trim('col_name'),
|
|
180
|
+
'SELECT trim(col_name)',
|
|
181
|
+
'SELECT trim("col_name")',
|
|
182
|
+
{},
|
|
183
|
+
),
|
|
184
|
+
(
|
|
185
|
+
trim(val('MyVAR')),
|
|
186
|
+
"SELECT trim('MyVAR')",
|
|
187
|
+
"SELECT trim('MyVAR')",
|
|
188
|
+
{},
|
|
189
|
+
),
|
|
190
|
+
(
|
|
191
|
+
trim(param('myParam')),
|
|
192
|
+
'SELECT trim(%(p1)s)',
|
|
193
|
+
'SELECT trim(%(p1)s)',
|
|
194
|
+
{'p1': 'myParam'},
|
|
195
|
+
),
|
|
196
|
+
])
|
|
197
|
+
def test_str_arg_fn(self, fn: FunctionProtocol, sql_exp: str, sql_expr2: str, exp_param: dict):
|
|
198
|
+
query = self.qb.select(fn)
|
|
199
|
+
self.assertEqual(query.get_sql(), sql_exp)
|
|
200
|
+
self.assertEqual(query.get_parameters(), exp_param)
|
|
201
|
+
|
|
202
|
+
query = self.qb_w.select(fn)
|
|
203
|
+
self.assertEqual(query.get_sql(), sql_expr2)
|
|
204
|
+
self.assertEqual(query.get_parameters(), exp_param)
|
|
@@ -118,3 +118,60 @@ class TestJoin(BaseTest):
|
|
|
118
118
|
])
|
|
119
119
|
def test_simple_join_using(self, query: QueryProtocol, exp_sql: str):
|
|
120
120
|
self.assertEqual(query.get_sql(), exp_sql)
|
|
121
|
+
|
|
122
|
+
@parameterized.expand([
|
|
123
|
+
# using
|
|
124
|
+
(
|
|
125
|
+
BaseTest
|
|
126
|
+
.qb
|
|
127
|
+
.select('*')
|
|
128
|
+
.from_table('orders')
|
|
129
|
+
.join(
|
|
130
|
+
left()
|
|
131
|
+
.query(BaseTest.qb.select('*').from_table('users'))
|
|
132
|
+
.using('order_id', 'user_id')
|
|
133
|
+
),
|
|
134
|
+
'SELECT * FROM orders LEFT JOIN (SELECT * FROM users) USING (order_id, user_id)'
|
|
135
|
+
),
|
|
136
|
+
(
|
|
137
|
+
BaseTest
|
|
138
|
+
.qb_w
|
|
139
|
+
.select('*')
|
|
140
|
+
.from_table('orders')
|
|
141
|
+
.join(
|
|
142
|
+
left()
|
|
143
|
+
.query(BaseTest.qb.select('*').from_table('users'))
|
|
144
|
+
.using('order_id', 'user_id')
|
|
145
|
+
),
|
|
146
|
+
'SELECT "*" FROM "orders" LEFT JOIN (SELECT "*" FROM "users") USING ("order_id", "user_id")'
|
|
147
|
+
),
|
|
148
|
+
# on
|
|
149
|
+
(
|
|
150
|
+
BaseTest
|
|
151
|
+
.qb
|
|
152
|
+
.select('*')
|
|
153
|
+
.from_table('users', 'u')
|
|
154
|
+
.join(
|
|
155
|
+
left()
|
|
156
|
+
.query(BaseTest.qb.select('*').from_table('users'))
|
|
157
|
+
.on(eq_c('o.user_id', 'u.id'))
|
|
158
|
+
.as_('o')
|
|
159
|
+
),
|
|
160
|
+
'SELECT * FROM users AS u LEFT JOIN (SELECT * FROM users) AS o ON (o.user_id = u.id)'
|
|
161
|
+
),
|
|
162
|
+
(
|
|
163
|
+
BaseTest
|
|
164
|
+
.qb_w
|
|
165
|
+
.select('*')
|
|
166
|
+
.from_table('users', 'u')
|
|
167
|
+
.join(
|
|
168
|
+
left()
|
|
169
|
+
.query(BaseTest.qb.select('*').from_table('users'))
|
|
170
|
+
.on(eq_c('o.user_id', 'u.id'))
|
|
171
|
+
.as_('o')
|
|
172
|
+
),
|
|
173
|
+
'SELECT "*" FROM "users" AS "u" LEFT JOIN (SELECT "*" FROM "users") AS "o" ON ("o"."user_id" = "u"."id")'
|
|
174
|
+
),
|
|
175
|
+
])
|
|
176
|
+
def test_join_from_query(self, query: QueryProtocol, exp_sql: str):
|
|
177
|
+
self.assertEqual(query.get_sql(), exp_sql)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from parameterized import parameterized
|
|
2
|
+
|
|
3
|
+
from david8.expressions import false, true
|
|
4
|
+
from david8.logical_operators import and_, not_, or_, xor
|
|
2
5
|
from david8.predicates import eq
|
|
6
|
+
from david8.protocols.sql import LogicalOperatorProtocol
|
|
3
7
|
from tests.base_test import BaseTest
|
|
4
8
|
|
|
5
9
|
|
|
@@ -81,3 +85,32 @@ class TestLogicalOperators(BaseTest):
|
|
|
81
85
|
)
|
|
82
86
|
|
|
83
87
|
self.assertEqual(query.get_parameters(), {'p1': 1, 'p2': 2, 'p3': 3, 'p4': 4, 'p5': 5})
|
|
88
|
+
|
|
89
|
+
@parameterized.expand([
|
|
90
|
+
(
|
|
91
|
+
not_('column_name'),
|
|
92
|
+
'SELECT NOT column_name',
|
|
93
|
+
'SELECT NOT "column_name"',
|
|
94
|
+
{}
|
|
95
|
+
),
|
|
96
|
+
(
|
|
97
|
+
not_(true()),
|
|
98
|
+
'SELECT NOT TRUE',
|
|
99
|
+
'SELECT NOT TRUE',
|
|
100
|
+
{}
|
|
101
|
+
),
|
|
102
|
+
(
|
|
103
|
+
not_(false()),
|
|
104
|
+
'SELECT NOT FALSE',
|
|
105
|
+
'SELECT NOT FALSE',
|
|
106
|
+
{}
|
|
107
|
+
),
|
|
108
|
+
])
|
|
109
|
+
def test_not(self, logical: LogicalOperatorProtocol, exp_sql: str, exp_sql2: str, exp_params: dict) -> None:
|
|
110
|
+
query = BaseTest.qb.select(logical)
|
|
111
|
+
self.assertEqual(query.get_sql(), exp_sql)
|
|
112
|
+
self.assertEqual(query.get_parameters(), exp_params)
|
|
113
|
+
|
|
114
|
+
query = BaseTest.qb_w.select(logical)
|
|
115
|
+
self.assertEqual(query.get_sql(), exp_sql2)
|
|
116
|
+
self.assertEqual(query.get_parameters(), exp_params)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from parameterized import parameterized
|
|
2
2
|
|
|
3
|
-
from david8.expressions import param, val
|
|
3
|
+
from david8.expressions import false, null, param, true, val
|
|
4
|
+
from david8.logical_operators import not_
|
|
4
5
|
from david8.predicates import (
|
|
5
6
|
between,
|
|
6
7
|
eq,
|
|
@@ -12,6 +13,7 @@ from david8.predicates import (
|
|
|
12
13
|
gt,
|
|
13
14
|
gt_c,
|
|
14
15
|
gt_e,
|
|
16
|
+
is_,
|
|
15
17
|
le,
|
|
16
18
|
le_c,
|
|
17
19
|
le_e,
|
|
@@ -30,14 +32,14 @@ class TestPredicates(BaseTest):
|
|
|
30
32
|
@parameterized.expand([
|
|
31
33
|
# between
|
|
32
34
|
(
|
|
33
|
-
|
|
35
|
+
between('age', 14, 18).as_('is_valid'),
|
|
34
36
|
'SELECT age BETWEEN %(p1)s AND %(p2)s AS is_valid',
|
|
35
|
-
|
|
37
|
+
{'p1': 14, 'p2': 18}
|
|
36
38
|
),
|
|
37
39
|
(
|
|
38
|
-
|
|
40
|
+
between('created_day', val('2025-01-01'), val('2026-01-01')),
|
|
39
41
|
"SELECT created_day BETWEEN '2025-01-01' AND '2026-01-01'",
|
|
40
|
-
|
|
42
|
+
{}
|
|
41
43
|
),
|
|
42
44
|
# eq
|
|
43
45
|
(
|
|
@@ -199,6 +201,47 @@ class TestPredicates(BaseTest):
|
|
|
199
201
|
'SELECT 1 != %(p1)s',
|
|
200
202
|
{'p1': 1}
|
|
201
203
|
),
|
|
204
|
+
# is
|
|
205
|
+
(
|
|
206
|
+
is_('is_active', true()),
|
|
207
|
+
'SELECT is_active IS TRUE',
|
|
208
|
+
{},
|
|
209
|
+
),
|
|
210
|
+
(
|
|
211
|
+
is_('is_active', not_(true())),
|
|
212
|
+
'SELECT is_active IS NOT TRUE',
|
|
213
|
+
{},
|
|
214
|
+
),
|
|
215
|
+
(
|
|
216
|
+
is_('is_active', false()),
|
|
217
|
+
'SELECT is_active IS FALSE',
|
|
218
|
+
{},
|
|
219
|
+
),
|
|
220
|
+
(
|
|
221
|
+
is_('is_active', not_(false())),
|
|
222
|
+
'SELECT is_active IS NOT FALSE',
|
|
223
|
+
{},
|
|
224
|
+
),
|
|
225
|
+
(
|
|
226
|
+
is_('is_active', null()),
|
|
227
|
+
'SELECT is_active IS NULL',
|
|
228
|
+
{},
|
|
229
|
+
),
|
|
230
|
+
(
|
|
231
|
+
is_('is_active', not_(null())),
|
|
232
|
+
'SELECT is_active IS NOT NULL',
|
|
233
|
+
{},
|
|
234
|
+
),
|
|
235
|
+
(
|
|
236
|
+
is_('last_update_dt', 'last_login_dt'),
|
|
237
|
+
'SELECT last_update_dt IS last_login_dt',
|
|
238
|
+
{}
|
|
239
|
+
),
|
|
240
|
+
(
|
|
241
|
+
is_('last_update_dt', param(1)),
|
|
242
|
+
'SELECT last_update_dt IS %(p1)s',
|
|
243
|
+
{'p1': 1}
|
|
244
|
+
),
|
|
202
245
|
])
|
|
203
246
|
def test_predicate(self, predicate: PredicateProtocol, exp_sql: str, exp_params: dict) -> None:
|
|
204
247
|
query = BaseTest.qb.select(predicate)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from david8.core.fn_generator import OneArgDistinctCallableFactory as _AggDistinctCallableFactory
|
|
2
|
-
from david8.core.fn_generator import SeparatedStrArgsCallableFactory as _SeparatedStrArgsCallableFactory
|
|
3
|
-
|
|
4
|
-
count = _AggDistinctCallableFactory(name='count')
|
|
5
|
-
avg = _AggDistinctCallableFactory(name='avg')
|
|
6
|
-
sum_ = _AggDistinctCallableFactory(name='sum')
|
|
7
|
-
max_ = _AggDistinctCallableFactory(name='max')
|
|
8
|
-
min_ = _AggDistinctCallableFactory(name='min')
|
|
9
|
-
concat = _SeparatedStrArgsCallableFactory(name='concat', separator=', ')
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
from typing import Union
|
|
3
|
-
|
|
4
|
-
from .protocols.dialect import DialectProtocol
|
|
5
|
-
from .protocols.sql import ExprProtocol, LogicalOperatorProtocol
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclasses.dataclass(slots=True)
|
|
9
|
-
class _LogicalOperator(LogicalOperatorProtocol):
|
|
10
|
-
_name: str
|
|
11
|
-
_conditions: Union[ExprProtocol, 'LogicalOperatorProtocol', ...]
|
|
12
|
-
|
|
13
|
-
def get_sql(self, dialect: DialectProtocol) -> str:
|
|
14
|
-
conditions = f' {self._name} '.join(c.get_sql(dialect) for c in self._conditions)
|
|
15
|
-
return f'({conditions})'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def or_(*args: ExprProtocol | LogicalOperatorProtocol):
|
|
19
|
-
return _LogicalOperator(_name='OR', _conditions=args)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def and_(*args: ExprProtocol | LogicalOperatorProtocol):
|
|
23
|
-
return _LogicalOperator(_name='AND', _conditions=args)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def xor(*args: ExprProtocol | LogicalOperatorProtocol):
|
|
27
|
-
return _LogicalOperator(_name='XOR', _conditions=args)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|