moose-lib 0.6.81__tar.gz → 0.6.82__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.
Potentially problematic release.
This version of moose-lib might be problematic. Click here for more details.
- {moose_lib-0.6.81 → moose_lib-0.6.82}/PKG-INFO +1 -1
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/__init__.py +2 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/data_models.py +5 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/main.py +28 -20
- moose_lib-0.6.82/moose_lib/query_builder.py +198 -0
- moose_lib-0.6.82/moose_lib/utilities/sql.py +34 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/PKG-INFO +1 -1
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/SOURCES.txt +2 -0
- moose_lib-0.6.82/tests/test_query_builder.py +38 -0
- moose_lib-0.6.81/moose_lib/utilities/sql.py +0 -10
- {moose_lib-0.6.81 → moose_lib-0.6.82}/README.md +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/blocks.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/clients/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/clients/redis_client.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/commons.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/config_file.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/runtime.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/_registry.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/consumption.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/ingest_api.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/ingest_pipeline.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/life_cycle.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/materialized_view.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/olap_table.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/registry.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/sql_resource.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/stream.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/types.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/view.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/workflow.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2_serializer.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/internal.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/query_param.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/streaming/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/streaming/streaming_function_runner.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/utilities/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/dependency_links.txt +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/requires.txt +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/top_level.txt +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/setup.cfg +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/setup.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/__init__.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/conftest.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_moose.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_redis_client.py +0 -0
- {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_s3queue_config.py +0 -0
|
@@ -147,6 +147,11 @@ class Column(BaseModel):
|
|
|
147
147
|
default: str | None = None
|
|
148
148
|
annotations: list[Tuple[str, Any]] = []
|
|
149
149
|
|
|
150
|
+
def to_expr(self):
|
|
151
|
+
# Lazy import to avoid circular dependency at import time
|
|
152
|
+
from .query_builder import ColumnRef
|
|
153
|
+
return ColumnRef(self)
|
|
154
|
+
|
|
150
155
|
|
|
151
156
|
def py_type_to_column_type(t: type, mds: list[Any]) -> Tuple[bool, list[Any], DataType]:
|
|
152
157
|
# handle Annotated[Optional[Annotated[...], ...]
|
|
@@ -27,6 +27,7 @@ from .data_models import Column
|
|
|
27
27
|
from .config.runtime import RuntimeClickHouseConfig
|
|
28
28
|
|
|
29
29
|
from moose_lib.commons import EnhancedJSONEncoder
|
|
30
|
+
from .query_builder import Query
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass
|
|
@@ -184,7 +185,29 @@ class QueryClient:
|
|
|
184
185
|
def __call__(self, input, variables):
|
|
185
186
|
return self.execute(input, variables)
|
|
186
187
|
|
|
187
|
-
def execute(self, input, variables, row_type: Type[BaseModel] = None):
|
|
188
|
+
def execute(self, input: Union[str, Query], variables = None, row_type: Type[BaseModel] = None):
|
|
189
|
+
"""
|
|
190
|
+
Execute a query.
|
|
191
|
+
|
|
192
|
+
- If `input` is a `Query`, do not supply `variables`.
|
|
193
|
+
- If `input` is a `str` intended for `string.Formatter` interpolation, `variables` must be a dict
|
|
194
|
+
mapping placeholder names to values.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
input: Either a `Query` object or a `Formatter`-style SQL template string.
|
|
198
|
+
variables: Dict used to fill a `Formatter` string; must be omitted when `input` is a `Query`.
|
|
199
|
+
row_type: Optional Pydantic model class to map result rows into.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A list of row dicts, or a list of `row_type` instances if provided.
|
|
203
|
+
"""
|
|
204
|
+
if isinstance(input, Query):
|
|
205
|
+
if variables is not None:
|
|
206
|
+
raise ValueError("Do not supply variables when you provide Query")
|
|
207
|
+
sql, params = input.to_sql_and_params()
|
|
208
|
+
print(f"[QueryClient] | Query: {sql}")
|
|
209
|
+
return self.execute_raw(sql, params, row_type)
|
|
210
|
+
|
|
188
211
|
params = {}
|
|
189
212
|
values: dict[str, Any] = {}
|
|
190
213
|
preview_params = {}
|
|
@@ -200,25 +223,10 @@ class QueryClient:
|
|
|
200
223
|
params[variable_name] = f'{{p{i}: Identifier}}'
|
|
201
224
|
values[f'p{i}'] = value.name
|
|
202
225
|
else:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
params[variable_name] = f'{{p{i}: DateTime}}'
|
|
208
|
-
values[f'p{i}'] = value
|
|
209
|
-
elif isinstance(value, int):
|
|
210
|
-
params[variable_name] = f'{{p{i}: Int64}}'
|
|
211
|
-
values[f'p{i}'] = value
|
|
212
|
-
elif isinstance(value, float):
|
|
213
|
-
params[variable_name] = f'{{p{i}: Float64}}'
|
|
214
|
-
values[f'p{i}'] = value
|
|
215
|
-
elif isinstance(value, str):
|
|
216
|
-
params[variable_name] = f'{{p{i}: String}}'
|
|
217
|
-
values[f'p{i}'] = value
|
|
218
|
-
else:
|
|
219
|
-
print(f"unhandled type in QueryClient {type(value)}", file=sys.stderr)
|
|
220
|
-
params[variable_name] = f'{{p{i}: String}}'
|
|
221
|
-
values[f'p{i}'] = str(value)
|
|
226
|
+
from moose_lib.utilities.sql import clickhouse_param_type_for_value
|
|
227
|
+
ch_type = clickhouse_param_type_for_value(value)
|
|
228
|
+
params[variable_name] = f'{{p{i}: {ch_type}}}'
|
|
229
|
+
values[f'p{i}'] = value
|
|
222
230
|
preview_params[variable_name] = self._format_value_for_preview(value)
|
|
223
231
|
|
|
224
232
|
clickhouse_query = input.format_map(params)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Callable, Literal
|
|
3
|
+
|
|
4
|
+
import sqlglot as sg
|
|
5
|
+
import sqlglot.expressions as sge
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .data_models import Column, Key
|
|
9
|
+
from .dmv2 import OlapTable, IngestPipeline, IngestPipelineConfig
|
|
10
|
+
from moose_lib.utilities.sql import clickhouse_param_type_for_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Params:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._counter = 0
|
|
16
|
+
self.bindings: dict[str, object] = {}
|
|
17
|
+
|
|
18
|
+
def bind(self, value: object, name: str | None = None, ch_type: str | None = None) -> sge.Expression:
|
|
19
|
+
if name is None:
|
|
20
|
+
name = f"p{self._counter}"
|
|
21
|
+
self._counter += 1
|
|
22
|
+
|
|
23
|
+
if ch_type is None:
|
|
24
|
+
ch_type = clickhouse_param_type_for_value(value)
|
|
25
|
+
|
|
26
|
+
expr = sg.parse_one(f"{{{name}: {ch_type}}}", dialect="clickhouse")
|
|
27
|
+
self.bindings[name] = value
|
|
28
|
+
return expr
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_column(col: Column, table_name: str | None = None) -> sge.Column:
|
|
32
|
+
col_name = getattr(col, "name")
|
|
33
|
+
table_ident = None
|
|
34
|
+
if table_name is not None:
|
|
35
|
+
table_ident = sge.Identifier(this=table_name, quoted=True)
|
|
36
|
+
elif hasattr(col, "table_name") and getattr(col, "table_name"):
|
|
37
|
+
table_ident = sge.Identifier(this=getattr(col, "table_name"), quoted=True)
|
|
38
|
+
|
|
39
|
+
return sge.Column(
|
|
40
|
+
this=sge.Identifier(this=col_name, quoted=True),
|
|
41
|
+
table=table_ident,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
type Predicate = Callable[["Query"], sge.Expression]
|
|
46
|
+
|
|
47
|
+
# Order-by type annotations
|
|
48
|
+
type OrderDirection = Literal["asc", "desc"]
|
|
49
|
+
type OrderExpr = sge.Expression | Column | "ColumnRef"
|
|
50
|
+
type OrderItem = OrderExpr | tuple[OrderExpr, OrderDirection]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ColumnRef:
|
|
54
|
+
def __init__(self, column: Column):
|
|
55
|
+
self._column = column
|
|
56
|
+
|
|
57
|
+
def _binary_op(self, op_name: str, value: object) -> Predicate:
|
|
58
|
+
def resolve(query: "Query") -> sge.Expression:
|
|
59
|
+
table_name = query._from_table.name if query._from_table is not None else None
|
|
60
|
+
left = to_column(self._column, table_name)
|
|
61
|
+
right = query.params.bind(value)
|
|
62
|
+
op = getattr(left, op_name)
|
|
63
|
+
return op(right)
|
|
64
|
+
|
|
65
|
+
return resolve
|
|
66
|
+
|
|
67
|
+
def eq(self, value: object) -> Predicate:
|
|
68
|
+
return self._binary_op("eq", value)
|
|
69
|
+
|
|
70
|
+
def ne(self, value: object) -> Predicate:
|
|
71
|
+
return self._binary_op("neq", value)
|
|
72
|
+
|
|
73
|
+
def lt(self, value: object) -> Predicate:
|
|
74
|
+
return self._binary_op("__lt__", value)
|
|
75
|
+
|
|
76
|
+
def le(self, value: object) -> Predicate:
|
|
77
|
+
return self._binary_op("__le__", value)
|
|
78
|
+
|
|
79
|
+
def gt(self, value: object) -> Predicate:
|
|
80
|
+
return self._binary_op("__gt__", value)
|
|
81
|
+
|
|
82
|
+
def ge(self, value: object) -> Predicate:
|
|
83
|
+
return self._binary_op("__ge__", value)
|
|
84
|
+
|
|
85
|
+
def in_(self, values: list[object]) -> Predicate:
|
|
86
|
+
def resolve(query: "Query") -> sge.Expression:
|
|
87
|
+
table_name = query._from_table.name if query._from_table is not None else None
|
|
88
|
+
left = to_column(self._column, table_name)
|
|
89
|
+
rights = [query.params.bind(v) for v in values]
|
|
90
|
+
return left.isin(*rights)
|
|
91
|
+
|
|
92
|
+
return resolve
|
|
93
|
+
|
|
94
|
+
def is_null(self) -> Predicate:
|
|
95
|
+
def resolve(query: "Query") -> sge.Expression:
|
|
96
|
+
table_name = query._from_table.name if query._from_table is not None else None
|
|
97
|
+
left = to_column(self._column, table_name)
|
|
98
|
+
return left.is_(sge.Null())
|
|
99
|
+
|
|
100
|
+
return resolve
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def col(column: Column) -> ColumnRef:
|
|
104
|
+
return ColumnRef(column)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Query:
|
|
108
|
+
def __init__(self):
|
|
109
|
+
self.params = Params()
|
|
110
|
+
self.inner: sge.Select = sge.Select()
|
|
111
|
+
self._from_table: OlapTable | None = None
|
|
112
|
+
|
|
113
|
+
def from_(self, table: OlapTable) -> "Query":
|
|
114
|
+
self._from_table = table
|
|
115
|
+
self.inner = self.inner.from_(table.name)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def select(self, *cols: Column) -> "Query":
|
|
119
|
+
sge_cols = [to_column(c, self._from_table.name if self._from_table is not None else None) for c in cols]
|
|
120
|
+
self.inner = self.inner.select(*sge_cols)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def where(self, predicate_or_expr) -> "Query":
|
|
124
|
+
if callable(predicate_or_expr):
|
|
125
|
+
expr = predicate_or_expr(self)
|
|
126
|
+
else:
|
|
127
|
+
expr = predicate_or_expr
|
|
128
|
+
self.inner = self.inner.where(expr)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def order_by(self, *items: OrderItem) -> "Query":
|
|
132
|
+
orders: list[sge.Expression] = []
|
|
133
|
+
table_name = self._from_table.name if self._from_table is not None else None
|
|
134
|
+
|
|
135
|
+
for item in items:
|
|
136
|
+
desc = False
|
|
137
|
+
expr: sge.Expression
|
|
138
|
+
|
|
139
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
140
|
+
col_like, direction = item
|
|
141
|
+
if isinstance(direction, str):
|
|
142
|
+
desc = direction.lower() == "desc"
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError("order_by direction must be 'asc' or 'desc'")
|
|
145
|
+
if isinstance(col_like, Column):
|
|
146
|
+
expr = to_column(col_like, table_name)
|
|
147
|
+
elif isinstance(col_like, ColumnRef):
|
|
148
|
+
expr = to_column(col_like._column, table_name)
|
|
149
|
+
else:
|
|
150
|
+
expr = col_like
|
|
151
|
+
else:
|
|
152
|
+
if isinstance(item, Column):
|
|
153
|
+
expr = to_column(item, table_name)
|
|
154
|
+
elif isinstance(item, ColumnRef):
|
|
155
|
+
expr = to_column(item._column, table_name)
|
|
156
|
+
else:
|
|
157
|
+
expr = item
|
|
158
|
+
|
|
159
|
+
orders.append(sge.Ordered(this=expr, desc=desc))
|
|
160
|
+
|
|
161
|
+
self.inner = self.inner.order_by(*orders)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def to_sql(self) -> str:
|
|
165
|
+
return self.inner.sql(dialect="clickhouse")
|
|
166
|
+
|
|
167
|
+
def to_sql_and_params(self) -> tuple[str, dict[str, object]]:
|
|
168
|
+
return self.to_sql(), dict(self.params.bindings)
|
|
169
|
+
|
|
170
|
+
def limit(self, n: int) -> "Query":
|
|
171
|
+
self.inner = self.inner.limit(n)
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def and_(*predicates_or_exprs) -> Predicate:
|
|
176
|
+
def resolve(query: "Query") -> sge.Expression:
|
|
177
|
+
exprs = [p(query) if callable(p) else p for p in predicates_or_exprs]
|
|
178
|
+
if not exprs:
|
|
179
|
+
raise ValueError("and_ requires at least one predicate")
|
|
180
|
+
combined = exprs[0]
|
|
181
|
+
for e in exprs[1:]:
|
|
182
|
+
combined = combined.and_(e)
|
|
183
|
+
return combined
|
|
184
|
+
|
|
185
|
+
return resolve
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def or_(*predicates_or_exprs) -> Predicate:
|
|
189
|
+
def resolve(query: "Query") -> sge.Expression:
|
|
190
|
+
exprs = [p(query) if callable(p) else p for p in predicates_or_exprs]
|
|
191
|
+
if not exprs:
|
|
192
|
+
raise ValueError("or_ requires at least one predicate")
|
|
193
|
+
combined = exprs[0]
|
|
194
|
+
for e in exprs[1:]:
|
|
195
|
+
combined = combined.or_(e)
|
|
196
|
+
return combined
|
|
197
|
+
|
|
198
|
+
return resolve
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def quote_identifier(name: str) -> str:
|
|
5
|
+
"""Quote a ClickHouse identifier with backticks if not already quoted.
|
|
6
|
+
|
|
7
|
+
Backticks allow special characters (e.g., hyphens) in identifiers.
|
|
8
|
+
"""
|
|
9
|
+
if name.startswith("`") and name.endswith("`"):
|
|
10
|
+
return name
|
|
11
|
+
return f"`{name}`"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def clickhouse_param_type_for_value(value: Any) -> str:
|
|
19
|
+
"""Infer ClickHouse typed parameter annotation for a Python value.
|
|
20
|
+
|
|
21
|
+
Normalized to common scalar types used in placeholders.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(value, bool):
|
|
24
|
+
return "Bool"
|
|
25
|
+
if isinstance(value, int):
|
|
26
|
+
return "Int64"
|
|
27
|
+
if isinstance(value, float):
|
|
28
|
+
return "Float64"
|
|
29
|
+
if isinstance(value, datetime):
|
|
30
|
+
return "DateTime"
|
|
31
|
+
if not isinstance(value, str):
|
|
32
|
+
print(f"unhandled type {type(value)}", file=sys.stderr)
|
|
33
|
+
return "String"
|
|
34
|
+
|
|
@@ -7,6 +7,7 @@ moose_lib/data_models.py
|
|
|
7
7
|
moose_lib/dmv2_serializer.py
|
|
8
8
|
moose_lib/internal.py
|
|
9
9
|
moose_lib/main.py
|
|
10
|
+
moose_lib/query_builder.py
|
|
10
11
|
moose_lib/query_param.py
|
|
11
12
|
moose_lib.egg-info/PKG-INFO
|
|
12
13
|
moose_lib.egg-info/SOURCES.txt
|
|
@@ -39,5 +40,6 @@ moose_lib/utilities/sql.py
|
|
|
39
40
|
tests/__init__.py
|
|
40
41
|
tests/conftest.py
|
|
41
42
|
tests/test_moose.py
|
|
43
|
+
tests/test_query_builder.py
|
|
42
44
|
tests/test_redis_client.py
|
|
43
45
|
tests/test_s3queue_config.py
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from moose_lib.query_builder import Query, col
|
|
4
|
+
from moose_lib.dmv2 import IngestPipeline, IngestPipelineConfig
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from moose_lib.data_models import Key
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Bar(BaseModel):
|
|
10
|
+
primary_key: Key[str]
|
|
11
|
+
utc_timestamp: datetime
|
|
12
|
+
has_text: bool
|
|
13
|
+
text_length: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_simple_select_and_where():
|
|
17
|
+
bar_model = IngestPipeline[Bar]("Bar", IngestPipelineConfig(
|
|
18
|
+
ingest=False,
|
|
19
|
+
stream=True,
|
|
20
|
+
table=True,
|
|
21
|
+
dead_letter_queue=True
|
|
22
|
+
))
|
|
23
|
+
bar_cols = bar_model.get_table().cols
|
|
24
|
+
|
|
25
|
+
q1 = Query().from_(bar_model.get_table()).select(bar_cols.has_text, bar_cols.text_length)
|
|
26
|
+
assert q1.to_sql() == 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar'
|
|
27
|
+
|
|
28
|
+
q2 = (
|
|
29
|
+
Query()
|
|
30
|
+
.from_(bar_model.get_table())
|
|
31
|
+
.select(bar_cols.has_text, bar_cols.text_length)
|
|
32
|
+
.where(col(bar_cols.has_text).eq(True))
|
|
33
|
+
)
|
|
34
|
+
sql, params = q2.to_sql_and_params()
|
|
35
|
+
assert sql == 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar WHERE "Bar"."has_text" = {p0: Bool}'
|
|
36
|
+
assert params == {"p0": True}
|
|
37
|
+
|
|
38
|
+
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
def quote_identifier(name: str) -> str:
|
|
2
|
-
"""Quote a ClickHouse identifier with backticks if not already quoted.
|
|
3
|
-
|
|
4
|
-
Backticks allow special characters (e.g., hyphens) in identifiers.
|
|
5
|
-
"""
|
|
6
|
-
if name.startswith("`") and name.endswith("`"):
|
|
7
|
-
return name
|
|
8
|
-
return f"`{name}`"
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|