david8 0.dev5__tar.gz → 0.2.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.
Files changed (46) hide show
  1. {david8-0.dev5 → david8-0.2.0b1}/PKG-INFO +4 -4
  2. {david8-0.dev5 → david8-0.2.0b1}/README.md +1 -1
  3. david8-0.2.0b1/david8/cast_types.py +16 -0
  4. david8-0.2.0b1/david8/core/arg_convertors.py +8 -0
  5. {david8-0.dev5 → david8-0.2.0b1}/david8/core/base_aliased.py +9 -0
  6. {david8-0.dev5 → david8-0.2.0b1}/david8/core/base_dml.py +35 -31
  7. {david8-0.dev5 → david8-0.2.0b1}/david8/core/fn_generator.py +54 -0
  8. {david8-0.dev5 → david8-0.2.0b1}/david8/core/sql_92_join.py +1 -1
  9. {david8-0.dev5 → david8-0.2.0b1}/david8/expressions.py +11 -1
  10. david8-0.2.0b1/david8/functions.py +35 -0
  11. david8-0.2.0b1/david8/logical_operators.py +43 -0
  12. {david8-0.dev5 → david8-0.2.0b1}/david8/predicates.py +18 -0
  13. {david8-0.dev5 → david8-0.2.0b1}/david8.egg-info/PKG-INFO +4 -4
  14. {david8-0.dev5 → david8-0.2.0b1}/david8.egg-info/SOURCES.txt +2 -0
  15. {david8-0.dev5 → david8-0.2.0b1}/pyproject.toml +3 -3
  16. {david8-0.dev5 → david8-0.2.0b1}/tests/test_expressions.py +19 -2
  17. david8-0.2.0b1/tests/test_functions.py +259 -0
  18. {david8-0.dev5 → david8-0.2.0b1}/tests/test_join.py +57 -0
  19. {david8-0.dev5 → david8-0.2.0b1}/tests/test_logical_operators.py +34 -1
  20. {david8-0.dev5 → david8-0.2.0b1}/tests/test_predicates.py +48 -5
  21. david8-0.dev5/david8/functions.py +0 -9
  22. david8-0.dev5/david8/logical_operators.py +0 -27
  23. david8-0.dev5/tests/test_functions.py +0 -110
  24. {david8-0.dev5 → david8-0.2.0b1}/LICENSE +0 -0
  25. {david8-0.dev5 → david8-0.2.0b1}/david8/__init__.py +0 -0
  26. {david8-0.dev5 → david8-0.2.0b1}/david8/core/__init__.py +0 -0
  27. {david8-0.dev5 → david8-0.2.0b1}/david8/core/base_dialect.py +0 -0
  28. {david8-0.dev5 → david8-0.2.0b1}/david8/core/base_params.py +0 -0
  29. {david8-0.dev5 → david8-0.2.0b1}/david8/core/base_query_builder.py +0 -0
  30. {david8-0.dev5 → david8-0.2.0b1}/david8/core/log.py +0 -0
  31. {david8-0.dev5 → david8-0.2.0b1}/david8/joins.py +0 -0
  32. {david8-0.dev5 → david8-0.2.0b1}/david8/param_styles.py +0 -0
  33. {david8-0.dev5 → david8-0.2.0b1}/david8/protocols/__init__.py +0 -0
  34. {david8-0.dev5 → david8-0.2.0b1}/david8/protocols/dialect.py +0 -0
  35. {david8-0.dev5 → david8-0.2.0b1}/david8/protocols/dml.py +0 -0
  36. {david8-0.dev5 → david8-0.2.0b1}/david8/protocols/query_builder.py +0 -0
  37. {david8-0.dev5 → david8-0.2.0b1}/david8/protocols/sql.py +0 -0
  38. {david8-0.dev5 → david8-0.2.0b1}/david8.egg-info/dependency_links.txt +0 -0
  39. {david8-0.dev5 → david8-0.2.0b1}/david8.egg-info/top_level.txt +0 -0
  40. {david8-0.dev5 → david8-0.2.0b1}/setup.cfg +0 -0
  41. {david8-0.dev5 → david8-0.2.0b1}/tests/test_group_by.py +0 -0
  42. {david8-0.dev5 → david8-0.2.0b1}/tests/test_order_by.py +0 -0
  43. {david8-0.dev5 → david8-0.2.0b1}/tests/test_select.py +0 -0
  44. {david8-0.dev5 → david8-0.2.0b1}/tests/test_union.py +0 -0
  45. {david8-0.dev5 → david8-0.2.0b1}/tests/test_where_predicates.py +0 -0
  46. {david8-0.dev5 → david8-0.2.0b1}/tests/test_with_select.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: david8
3
- Version: 0.dev5
4
- Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL
3
+ Version: 0.2.0b1
4
+ Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite
5
5
  Author-email: Danila Ganchar <danila.ganchar@gmail.com>
6
6
  Maintainer-email: Danila Ganchar <danila.ganchar@gmail.com>
7
7
  Project-URL: Homepage, https://github.com/d-ganchar/david8
@@ -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 :: 2 - Pre-Alpha
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 MySQL
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 MySQL
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,16 @@
1
+ text = 'TEXT'
2
+ integer = 'INTEGER'
3
+ smallint = 'SMALLINT'
4
+ bigint = 'BIGINT'
5
+ date_ = 'DATE'
6
+ time_ = 'TIME'
7
+ timestamp_ = 'TIMESTAMP'
8
+ boolean = 'BOOLEAN'
9
+
10
+
11
+ def varchar(char_length: int) -> str:
12
+ return f'VARCHAR({char_length})'
13
+
14
+
15
+ def char(char_length: int) -> str:
16
+ return f'CHAR({char_length})'
@@ -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
- self.dialect.quote_ident(column)
73
- if isinstance(column, str) else column.get_sql(self.dialect)
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(self.dialect) for predicate in self.where_conditions)}"
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'{self.dialect.quote_ident(alias)} AS ({query.get_sql(self.dialect)})'
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(self.dialect)})'
107
+ source = f'({self.from_query_expr.get_sql(dialect)})'
108
108
  elif self.from_table_value:
109
- source = self.dialect.quote_ident(self.from_table_value)
109
+ source = dialect.quote_ident(self.from_table_value)
110
110
  if self.from_db_value:
111
- source = f'{self.dialect.quote_ident(self.from_db_value)}.{source}'
111
+ source = f'{dialect.quote_ident(self.from_db_value)}.{source}'
112
112
  else:
113
113
  return ''
114
114
 
115
- source = f'{source} AS {self.dialect.quote_ident(self.source_alias)}' if self.source_alias else source
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(self.dialect)}"
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"{self.dialect.quote_ident(f) if isinstance(f, str) else str(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(self.dialect) for p in self.having_expressions)}"
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(self.dialect)
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
- with_query = self._with_queries_to_sql()
174
- select = self._columns_to_sql()
175
- from_ref = self._from_to_sql()
176
- joins = self._joins_to_sql()
177
- where = self._where_to_sql()
178
- group_by = self._group_by_to_sql()
179
- having = self._having_to_sql()
180
- union = self._union_to_sql()
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 dialect is None:
186
- log.info('%s %s', sql, self.get_parameters())
189
+ if log_query:
190
+ log.info('%s\n%s', sql, self.get_parameters())
187
191
 
188
192
  return sql
189
193
 
@@ -1,5 +1,6 @@
1
1
  import dataclasses
2
2
 
3
+ from david8.core.arg_convertors import to_col_or_expr
3
4
  from david8.core.base_aliased import BaseAliased
4
5
  from david8.protocols.dialect import DialectProtocol
5
6
  from david8.protocols.sql import ExprProtocol, FunctionProtocol
@@ -9,6 +10,17 @@ from david8.protocols.sql import ExprProtocol, FunctionProtocol
9
10
  class Function(BaseAliased, FunctionProtocol):
10
11
  name: str
11
12
 
13
+ def _get_sql(self, dialect: DialectProtocol) -> str:
14
+ return f"{self.name}()"
15
+
16
+
17
+ @dataclasses.dataclass(slots=True, kw_only=True)
18
+ class ZeroArgsCallableFactory:
19
+ name: str = ''
20
+
21
+ def __call__(self) -> FunctionProtocol:
22
+ return Function(self.name)
23
+
12
24
 
13
25
  @dataclasses.dataclass(slots=True, kw_only=True)
14
26
  class FnCallableFactory:
@@ -65,3 +77,45 @@ class _OneArgDistinctFn(Function):
65
77
  class OneArgDistinctCallableFactory(FnCallableFactory):
66
78
  def __call__(self, column: str, distinct: bool = False) -> FunctionProtocol:
67
79
  return _OneArgDistinctFn(self.name, column, distinct)
80
+
81
+
82
+ @dataclasses.dataclass(slots=True)
83
+ class _StrArgFn(Function):
84
+ """
85
+ upper(col_name)
86
+ lower(col_name)
87
+ etc
88
+ """
89
+ value: str | ExprProtocol = ''
90
+
91
+ def _get_sql(self, dialect: DialectProtocol) -> str:
92
+ if isinstance(self.value, str):
93
+ value = dialect.quote_ident(self.value)
94
+ else:
95
+ value = self.value.get_sql(dialect)
96
+ return f"{self.name}({value})"
97
+
98
+
99
+ @dataclasses.dataclass(slots=True)
100
+ class StrArgCallableFactory(FnCallableFactory):
101
+ value: str = ''
102
+
103
+ def __call__(self, value: str | ExprProtocol) -> FunctionProtocol:
104
+ return _StrArgFn(self.name, value)
105
+
106
+
107
+ @dataclasses.dataclass(slots=True)
108
+ class _CastFn(Function):
109
+ value: str | ExprProtocol
110
+ cast_type: str
111
+
112
+ def _get_sql(self, dialect: DialectProtocol) -> str:
113
+ value = to_col_or_expr(self.value, dialect)
114
+ return f"{self.name}({value} AS {self.cast_type})"
115
+
116
+
117
+ @dataclasses.dataclass(slots=True)
118
+ class CastCallableFactory(FnCallableFactory):
119
+ name = 'CAST'
120
+ def __call__(self, value: str | ExprProtocol, cast_type: str) -> FunctionProtocol:
121
+ return _CastFn('CAST', value, cast_type)
@@ -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,35 @@
1
+ from david8.core.fn_generator import (
2
+ CastCallableFactory as _CastCallableFactory,
3
+ )
4
+ from david8.core.fn_generator import (
5
+ OneArgDistinctCallableFactory as _AggDistinctCallableFactory,
6
+ )
7
+ from david8.core.fn_generator import (
8
+ SeparatedStrArgsCallableFactory as _SeparatedStrArgsCallableFactory,
9
+ )
10
+ from david8.core.fn_generator import (
11
+ StrArgCallableFactory as _StrArgCallableFactory,
12
+ )
13
+ from david8.core.fn_generator import (
14
+ ZeroArgsCallableFactory as _ZeroArgsCallableFactory,
15
+ )
16
+
17
+ # length('col_name') | length(val('MyVAR')) | length(param('myParam')) | length(concat('col1', 'col2'))
18
+ lower = _StrArgCallableFactory(name='lower')
19
+ upper = _StrArgCallableFactory(name='upper')
20
+ length = _StrArgCallableFactory(name='length')
21
+ trim = _StrArgCallableFactory(name='trim')
22
+
23
+ # count('name', True) => count(DISTINCT name), min_('age', True) => min(DISTINCT age) = 33
24
+ count = _AggDistinctCallableFactory(name='count')
25
+ avg = _AggDistinctCallableFactory(name='avg')
26
+ sum_ = _AggDistinctCallableFactory(name='sum')
27
+ max_ = _AggDistinctCallableFactory(name='max')
28
+ min_ = _AggDistinctCallableFactory(name='min')
29
+
30
+ concat = _SeparatedStrArgsCallableFactory(name='concat', separator=', ')
31
+
32
+ now_ = _ZeroArgsCallableFactory(name='now')
33
+ uuid_ = _ZeroArgsCallableFactory(name='uuid')
34
+
35
+ cast = _CastCallableFactory()
@@ -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,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: david8
3
- Version: 0.dev5
4
- Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL
3
+ Version: 0.2.0b1
4
+ Summary: Lightweight Python SQL query builder with support for multiple dialects: ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite
5
5
  Author-email: Danila Ganchar <danila.ganchar@gmail.com>
6
6
  Maintainer-email: Danila Ganchar <danila.ganchar@gmail.com>
7
7
  Project-URL: Homepage, https://github.com/d-ganchar/david8
@@ -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 :: 2 - Pre-Alpha
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 MySQL
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)
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  david8/__init__.py
5
+ david8/cast_types.py
5
6
  david8/expressions.py
6
7
  david8/functions.py
7
8
  david8/joins.py
@@ -13,6 +14,7 @@ david8.egg-info/SOURCES.txt
13
14
  david8.egg-info/dependency_links.txt
14
15
  david8.egg-info/top_level.txt
15
16
  david8/core/__init__.py
17
+ david8/core/arg_convertors.py
16
18
  david8/core/base_aliased.py
17
19
  david8/core/base_dialect.py
18
20
  david8/core/base_dml.py
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "david8"
3
- version = "0.dev.5"
4
- description = "Lightweight Python SQL query builder with support for multiple dialects: ClickHouse and PostgreSQL"
3
+ version = "0.2.0b1"
4
+ description = "Lightweight Python SQL query builder with support for multiple dialects: ClickHouse, PostgreSQL, DuckDB, MySQL and SQLite"
5
5
  authors = [{name = "Danila Ganchar", email = "danila.ganchar@gmail.com"}]
6
6
  maintainers = [{name = "Danila Ganchar", email = "danila.ganchar@gmail.com"}]
7
7
  license-files = ["LICENSE"]
@@ -12,7 +12,7 @@ dependencies = [
12
12
  ]
13
13
 
14
14
  classifiers = [
15
- "Development Status :: 2 - Pre-Alpha",
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)
@@ -0,0 +1,259 @@
1
+ from parameterized import parameterized
2
+
3
+ from david8 import QueryBuilderProtocol
4
+ from david8.cast_types import bigint, char, date_, integer, smallint, text, time_, timestamp_, varchar
5
+ from david8.expressions import param, val
6
+ from david8.functions import avg, cast, concat, count, length, lower, max_, min_, now_, sum_, trim, upper, uuid_
7
+ from david8.logical_operators import and_, or_, xor
8
+ from david8.predicates import eq_e
9
+ from david8.protocols.dml import FunctionProtocol
10
+ from tests.base_test import BaseTest
11
+
12
+
13
+ class TestFunctions(BaseTest):
14
+
15
+ @parameterized.expand([
16
+ (
17
+ BaseTest.qb,
18
+ "SELECT concat(col_name1, 'val1', %(p1)s, '1', '1.5', concat(col_name2, "
19
+ "'val2', %(p2)s, '2', '2.5')), concat(col3, %(p3)s, col_name3) AS alias FROM test",
20
+ ),
21
+ (
22
+ BaseTest.qb_w,
23
+ 'SELECT concat("col_name1", \'val1\', %(p1)s, \'1\', \'1.5\', '
24
+ 'concat("col_name2", \'val2\', %(p2)s, \'2\', \'2.5\')), concat("col3", '
25
+ '%(p3)s, "col_name3") AS "alias" FROM "test"'
26
+ )
27
+ ])
28
+ def test_concat(self, qb: QueryBuilderProtocol, exp_sql: str):
29
+ query = (
30
+ qb
31
+ .select(
32
+ concat(
33
+ 'col_name1',
34
+ val('val1'),
35
+ param('param1'),
36
+ 1,
37
+ 1.5,
38
+ concat(
39
+ 'col_name2',
40
+ val('val2'),
41
+ param('param2'),
42
+ 2,
43
+ 2.5,
44
+ ),
45
+ ),
46
+ concat('col3', param('param3'), 'col_name3').as_('alias')
47
+ )
48
+ .from_table('test')
49
+ )
50
+
51
+ self.assertEqual(query.get_sql(), exp_sql)
52
+ self.assertEqual({'p1': 'param1', 'p2': 'param2', 'p3': 'param3'}, query.get_parameters())
53
+
54
+
55
+ class TestAggFunctions(BaseTest):
56
+
57
+ def test_agg_functions(self):
58
+ query = (
59
+ self.qb
60
+ .select('*')
61
+ .from_table('test')
62
+ .having(eq_e(count('*'), val(1)))
63
+ )
64
+
65
+ for expr in [
66
+ eq_e(count('name'), val(2)),
67
+ eq_e(max_('price'), val(1000)),
68
+ eq_e(min_('age'), val(27)),
69
+ eq_e(sum_('money'), val(100)),
70
+ eq_e(avg('success'), val(99)),
71
+ eq_e(count('name', True), val(3)),
72
+ eq_e(max_('price', True), val(2000)),
73
+ eq_e(min_('age', True), val(33)),
74
+ eq_e(sum_('money', True), val(200)),
75
+ eq_e(avg('success', True), val(299)),
76
+ ]:
77
+ query.having(expr)
78
+
79
+ sql = query.get_sql()
80
+ self.assertEqual(
81
+ sql,
82
+ 'SELECT * FROM test HAVING count(*) = 1 AND count(name) = 2 AND max(price) = 1000 AND min(age) = 27 AND '
83
+ 'sum(money) = 100 AND avg(success) = 99 AND count(DISTINCT name) = 3 AND max(DISTINCT price) = 2000 AND '
84
+ 'min(DISTINCT age) = 33 AND sum(DISTINCT money) = 200 AND avg(DISTINCT success) = 299'
85
+ )
86
+
87
+ def test_agg_logical_operators(self):
88
+ query = (
89
+ self.qb
90
+ .select('*')
91
+ .from_table('test')
92
+ .having(
93
+ or_(
94
+ eq_e(count('name'), val(2)),
95
+ eq_e(max_('price'), val(1000)),
96
+ and_(
97
+ eq_e(min_('age'), val(27)),
98
+ eq_e(sum_('money'), val(100)),
99
+ ),
100
+ xor(
101
+ eq_e(avg('success'), val(99)),
102
+ eq_e(avg('happiness'), val(101)),
103
+ )
104
+ )
105
+ )
106
+ )
107
+
108
+ self.assertEqual(
109
+ query.get_sql(),
110
+ 'SELECT * FROM test HAVING (count(name) = 2 OR max(price) = 1000 OR (min(age) = 27 '
111
+ 'AND sum(money) = 100) OR (avg(success) = 99 XOR avg(happiness) = 101))'
112
+ )
113
+
114
+ @parameterized.expand([
115
+ # length
116
+ (
117
+ length(concat('col1', 'col2')),
118
+ 'SELECT length(concat(col1, col2))',
119
+ 'SELECT length(concat("col1", "col2"))',
120
+ {},
121
+ ),
122
+ (
123
+ length('col_name'),
124
+ 'SELECT length(col_name)',
125
+ 'SELECT length("col_name")',
126
+ {},
127
+ ),
128
+ (
129
+ length(val('MyVAR')),
130
+ "SELECT length('MyVAR')",
131
+ "SELECT length('MyVAR')",
132
+ {},
133
+ ),
134
+ (
135
+ length(param('myParam')),
136
+ 'SELECT length(%(p1)s)',
137
+ 'SELECT length(%(p1)s)',
138
+ {'p1': 'myParam'},
139
+ ),
140
+ # upper
141
+ (
142
+ upper('col_name'),
143
+ 'SELECT upper(col_name)',
144
+ 'SELECT upper("col_name")',
145
+ {},
146
+ ),
147
+ (
148
+ upper(val('MyVAR')),
149
+ "SELECT upper('MyVAR')",
150
+ "SELECT upper('MyVAR')",
151
+ {},
152
+ ),
153
+ (
154
+ upper(param('myParam')),
155
+ 'SELECT upper(%(p1)s)',
156
+ 'SELECT upper(%(p1)s)',
157
+ {'p1': 'myParam'},
158
+ ),
159
+ # lower
160
+ (
161
+ lower('col_name'),
162
+ 'SELECT lower(col_name)',
163
+ 'SELECT lower("col_name")',
164
+ {},
165
+ ),
166
+ (
167
+ lower(val('MyVAR')),
168
+ "SELECT lower('MyVAR')",
169
+ "SELECT lower('MyVAR')",
170
+ {},
171
+ ),
172
+ (
173
+ lower(param('myParam')),
174
+ 'SELECT lower(%(p1)s)',
175
+ 'SELECT lower(%(p1)s)',
176
+ {'p1': 'myParam'},
177
+ ),
178
+ # trim
179
+ (
180
+ trim('col_name'),
181
+ 'SELECT trim(col_name)',
182
+ 'SELECT trim("col_name")',
183
+ {},
184
+ ),
185
+ (
186
+ trim(val('MyVAR')),
187
+ "SELECT trim('MyVAR')",
188
+ "SELECT trim('MyVAR')",
189
+ {},
190
+ ),
191
+ (
192
+ trim(param('myParam')),
193
+ 'SELECT trim(%(p1)s)',
194
+ 'SELECT trim(%(p1)s)',
195
+ {'p1': 'myParam'},
196
+ ),
197
+ ])
198
+ def test_str_arg_fn(self, fn: FunctionProtocol, sql_exp: str, sql_expr2: str, exp_param: dict):
199
+ query = self.qb.select(fn)
200
+ self.assertEqual(query.get_sql(), sql_exp)
201
+ self.assertEqual(query.get_parameters(), exp_param)
202
+
203
+ query = self.qb_w.select(fn)
204
+ self.assertEqual(query.get_sql(), sql_expr2)
205
+ self.assertEqual(query.get_parameters(), exp_param)
206
+
207
+ @parameterized.expand([
208
+ (
209
+ now_(),
210
+ 'SELECT now()',
211
+ ),
212
+ (
213
+ uuid_(),
214
+ 'SELECT uuid()',
215
+ ),
216
+ ])
217
+ def test_zero_arg_fn(self, fn: FunctionProtocol, sql_exp: str):
218
+ self.assertEqual(self.qb.select(fn).get_sql(), sql_exp)
219
+
220
+ @parameterized.expand([
221
+ (
222
+ cast('col_name', integer),
223
+ 'SELECT CAST(col_name AS INTEGER)',
224
+ ),
225
+ (
226
+ cast('col_name', bigint),
227
+ 'SELECT CAST(col_name AS BIGINT)',
228
+ ),
229
+ (
230
+ cast('col_name', text),
231
+ 'SELECT CAST(col_name AS TEXT)',
232
+ ),
233
+ (
234
+ cast('col_name', char(9)),
235
+ 'SELECT CAST(col_name AS CHAR(9))',
236
+ ),
237
+ (
238
+ cast('col_name', varchar(9)),
239
+ 'SELECT CAST(col_name AS VARCHAR(9))',
240
+ ),
241
+ (
242
+ cast(val('1'), smallint).as_('small_int_val'),
243
+ "SELECT CAST('1' AS SMALLINT) AS small_int_val",
244
+ ),
245
+ (
246
+ cast(val('2025-11-27 15:54:34.173122+00'), timestamp_),
247
+ "SELECT CAST('2025-11-27 15:54:34.173122+00' AS TIMESTAMP)",
248
+ ),
249
+ (
250
+ cast(val('2025-11-27 15:54:34.173122+00'), date_),
251
+ "SELECT CAST('2025-11-27 15:54:34.173122+00' AS DATE)",
252
+ ),
253
+ (
254
+ cast(val('2025-11-27 15:54:34.173122+00'), time_),
255
+ "SELECT CAST('2025-11-27 15:54:34.173122+00' AS TIME)",
256
+ ),
257
+ ])
258
+ def test_cast(self, fn: FunctionProtocol, sql_exp: str):
259
+ self.assertEqual(self.qb.select(fn).get_sql(), sql_exp)
@@ -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 david8.logical_operators import and_, or_, xor
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
- between('age', 14, 18).as_('is_valid'),
35
+ between('age', 14, 18).as_('is_valid'),
34
36
  'SELECT age BETWEEN %(p1)s AND %(p2)s AS is_valid',
35
- {'p1': 14, 'p2': 18}
37
+ {'p1': 14, 'p2': 18}
36
38
  ),
37
39
  (
38
- between('created_day', val('2025-01-01'), val('2026-01-01')),
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)
@@ -1,110 +0,0 @@
1
- from parameterized import parameterized
2
-
3
- from david8 import QueryBuilderProtocol
4
- from david8.expressions import param, val
5
- from david8.functions import avg, concat, count, max_, min_, sum_
6
- from david8.logical_operators import and_, or_, xor
7
- from david8.predicates import eq_e
8
- from tests.base_test import BaseTest
9
-
10
-
11
- class TestFunctions(BaseTest):
12
-
13
- @parameterized.expand([
14
- (
15
- BaseTest.qb,
16
- "SELECT concat(col_name1, 'val1', %(p1)s, '1', '1.5', concat(col_name2, "
17
- "'val2', %(p2)s, '2', '2.5')), concat(col3, %(p3)s, col_name3) AS alias FROM test",
18
- ),
19
- (
20
- BaseTest.qb_w,
21
- 'SELECT concat("col_name1", \'val1\', %(p1)s, \'1\', \'1.5\', '
22
- 'concat("col_name2", \'val2\', %(p2)s, \'2\', \'2.5\')), concat("col3", '
23
- '%(p3)s, "col_name3") AS "alias" FROM "test"'
24
- )
25
- ])
26
- def test_concat(self, qb: QueryBuilderProtocol, exp_sql: str):
27
- query = (
28
- qb
29
- .select(
30
- concat(
31
- 'col_name1',
32
- val('val1'),
33
- param('param1'),
34
- 1,
35
- 1.5,
36
- concat(
37
- 'col_name2',
38
- val('val2'),
39
- param('param2'),
40
- 2,
41
- 2.5,
42
- ),
43
- ),
44
- concat('col3', param('param3'), 'col_name3').as_('alias')
45
- )
46
- .from_table('test')
47
- )
48
-
49
- self.assertEqual(query.get_sql(), exp_sql)
50
- self.assertEqual({'p1': 'param1', 'p2': 'param2', 'p3': 'param3'}, query.get_parameters())
51
-
52
-
53
- class TestAggFunctions(BaseTest):
54
-
55
- def test_agg_functions(self):
56
- query = (
57
- self.qb
58
- .select('*')
59
- .from_table('test')
60
- .having(eq_e(count('*'), val(1)))
61
- )
62
-
63
- for expr in [
64
- eq_e(count('name'), val(2)),
65
- eq_e(max_('price'), val(1000)),
66
- eq_e(min_('age'), val(27)),
67
- eq_e(sum_('money'), val(100)),
68
- eq_e(avg('success'), val(99)),
69
- eq_e(count('name', True), val(3)),
70
- eq_e(max_('price', True), val(2000)),
71
- eq_e(min_('age', True), val(33)),
72
- eq_e(sum_('money', True), val(200)),
73
- eq_e(avg('success', True), val(299)),
74
- ]:
75
- query.having(expr)
76
-
77
- sql = query.get_sql()
78
- self.assertEqual(
79
- sql,
80
- 'SELECT * FROM test HAVING count(*) = 1 AND count(name) = 2 AND max(price) = 1000 AND min(age) = 27 AND '
81
- 'sum(money) = 100 AND avg(success) = 99 AND count(DISTINCT name) = 3 AND max(DISTINCT price) = 2000 AND '
82
- 'min(DISTINCT age) = 33 AND sum(DISTINCT money) = 200 AND avg(DISTINCT success) = 299'
83
- )
84
-
85
- def test_agg_logical_operators(self):
86
- query = (
87
- self.qb
88
- .select('*')
89
- .from_table('test')
90
- .having(
91
- or_(
92
- eq_e(count('name'), val(2)),
93
- eq_e(max_('price'), val(1000)),
94
- and_(
95
- eq_e(min_('age'), val(27)),
96
- eq_e(sum_('money'), val(100)),
97
- ),
98
- xor(
99
- eq_e(avg('success'), val(99)),
100
- eq_e(avg('happiness'), val(101)),
101
- )
102
- )
103
- )
104
- )
105
-
106
- self.assertEqual(
107
- query.get_sql(),
108
- 'SELECT * FROM test HAVING (count(name) = 2 OR max(price) = 1000 OR (min(age) = 27 '
109
- 'AND sum(money) = 100) OR (avg(success) = 99 XOR avg(happiness) = 101))'
110
- )
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