clickhouse-orm 3.2.0__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/PKG-INFO +4 -1
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/__init__.py +3 -0
- clickhouse_orm-4.0.0/clickhouse_orm/codec.py +260 -0
- clickhouse_orm-4.0.0/clickhouse_orm/compiler.py +85 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/database.py +120 -169
- clickhouse_orm-4.0.0/clickhouse_orm/driver.py +119 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/engines.py +38 -29
- clickhouse_orm-4.0.0/clickhouse_orm/exceptions.py +79 -0
- clickhouse_orm-4.0.0/clickhouse_orm/executor.py +59 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/fields.py +182 -39
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/migrations.py +11 -7
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/models.py +51 -42
- clickhouse_orm-4.0.0/clickhouse_orm/native.py +381 -0
- clickhouse_orm-4.0.0/clickhouse_orm/params.py +161 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/query.py +98 -22
- clickhouse_orm-4.0.0/clickhouse_orm/utils.py +276 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/pyproject.toml +5 -1
- clickhouse_orm-3.2.0/clickhouse_orm/utils.py +0 -176
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/LICENSE +0 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/README.md +0 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/funcs.py +0 -0
- {clickhouse_orm-3.2.0 → clickhouse_orm-4.0.0}/clickhouse_orm/system_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clickhouse_orm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: A simple ORM for working with the Clickhouse database. Maintainance fork of infi.clickhouse_orm.
|
|
5
5
|
Author-email: Oliver Margetts <oliver.margetts@gmail.com>
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -18,12 +18,15 @@ Classifier: Topic :: Database
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: pytz
|
|
21
|
+
Requires-Dist: clickhouse-driver==0.2.10 ; extra == "dev"
|
|
21
22
|
Requires-Dist: docker==7.1.0 ; extra == "dev"
|
|
22
23
|
Requires-Dist: pytest==9.0.2 ; extra == "dev"
|
|
23
24
|
Requires-Dist: ruff==0.14.14 ; extra == "dev"
|
|
25
|
+
Requires-Dist: clickhouse-driver>=0.2.7 ; extra == "native"
|
|
24
26
|
Project-URL: Homepage, https://github.com/SuadeLabs/clickhouse_orm
|
|
25
27
|
Project-URL: Repository, https://github.com/SuadeLabs/clickhouse_orm
|
|
26
28
|
Provides-Extra: dev
|
|
29
|
+
Provides-Extra: native
|
|
27
30
|
|
|
28
31
|
A fork of [infi.clikchouse_orm](https://github.com/Infinidat/infi.clickhouse_orm) aimed at more frequent maintenance and bugfixes.
|
|
29
32
|
|
|
@@ -2,8 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from inspect import isclass
|
|
4
4
|
|
|
5
|
+
from .codec import * # noqa: F401, F403
|
|
6
|
+
from .compiler import * # noqa: F401, F403
|
|
5
7
|
from .database import * # noqa: F401, F403
|
|
6
8
|
from .engines import * # noqa: F401, F403
|
|
9
|
+
from .executor import * # noqa: F401, F403
|
|
7
10
|
from .fields import * # noqa: F401, F403
|
|
8
11
|
from .funcs import * # noqa: F401, F403
|
|
9
12
|
from .migrations import * # noqa: F401, F403
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""A codec converts model instances to and from the wire format used to exchange data with ClickHouse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import codecs
|
|
7
|
+
import datetime
|
|
8
|
+
import re
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import pytz
|
|
13
|
+
|
|
14
|
+
from .compiler import quote_identifier
|
|
15
|
+
from .fields import BaseEnumField
|
|
16
|
+
from .models import Model, ModelBase
|
|
17
|
+
from .utils import parse_array, parse_tsv, parse_tuple_type, split_type_args, unescape
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
21
|
+
|
|
22
|
+
#: A column description: its name and ClickHouse type, e.g. `("id", "UInt64")`.
|
|
23
|
+
Column = tuple[str, str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RowResult:
|
|
27
|
+
"""
|
|
28
|
+
The result of a query whose rows are returned as plain tuples rather than model instances.
|
|
29
|
+
|
|
30
|
+
The layout matches `clickhouse_driver.Client.execute(..., with_column_types=True)`:
|
|
31
|
+
`columns` is a list of `(name, type)` pairs, and iterating yields one tuple per row.
|
|
32
|
+
Rows are streamed from the server, so the result can only be iterated once.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, columns: list[Column], rows: Iterator[tuple[Any, ...]]):
|
|
36
|
+
self.columns = columns
|
|
37
|
+
self._rows = rows
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def column_names(self) -> list[str]:
|
|
41
|
+
"""The names of the result columns, in order. Names are not necessarily unique."""
|
|
42
|
+
return [name for name, _ in self.columns]
|
|
43
|
+
|
|
44
|
+
def __iter__(self) -> Iterator[tuple[Any, ...]]:
|
|
45
|
+
return self._rows
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
return f"<{self.__class__.__name__} columns={self.columns!r}>"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Codec(abc.ABC):
|
|
52
|
+
"""
|
|
53
|
+
Base class for codecs. A codec is paired with a driver (see `Driver.codec`): it builds the statements whose data
|
|
54
|
+
it encodes, and decodes the responses returned by the driver's `send`.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
#: The ClickHouse format name appended to SELECT queries in a `FORMAT` clause, or `None` to send queries as is.
|
|
58
|
+
select_format: str | None
|
|
59
|
+
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def encode_inserts(
|
|
62
|
+
self, model_class: type[Model], instances: Iterable[Model], batch_size: int = 1000
|
|
63
|
+
) -> Iterator[tuple[str, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Serialises model instances for insertion. Yields `(statement, data)` pairs, each to be sent to the driver
|
|
66
|
+
as `send(statement, data=data)`. Statements use the `$table` placeholder for the model's table.
|
|
67
|
+
|
|
68
|
+
- `model_class`: the model class of all the instances.
|
|
69
|
+
- `instances`: the instances to serialise.
|
|
70
|
+
- `batch_size`: the maximum number of instances per chunk of data, for codecs which send data in chunks.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@abc.abstractmethod
|
|
74
|
+
def decode(
|
|
75
|
+
self,
|
|
76
|
+
response: Any,
|
|
77
|
+
model_class: type[Model] | None = None,
|
|
78
|
+
timezone: datetime.tzinfo = pytz.utc,
|
|
79
|
+
) -> Iterator[Model]:
|
|
80
|
+
"""
|
|
81
|
+
Deserialises the response to a SELECT query into model instances.
|
|
82
|
+
|
|
83
|
+
- `response`: the response returned by the driver's `send`.
|
|
84
|
+
- `model_class`: the model class matching the query's columns,
|
|
85
|
+
or `None` for getting back instances of an ad-hoc model.
|
|
86
|
+
- `timezone`: the server's timezone, passed to the `to_python` of custom fields.
|
|
87
|
+
|
|
88
|
+
Datetimes are converted according to their column's type, as in `decode_rows`, before being assigned.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@abc.abstractmethod
|
|
92
|
+
def decode_rows(self, response: Any, timezone: datetime.tzinfo = pytz.utc) -> RowResult:
|
|
93
|
+
"""
|
|
94
|
+
Deserialises the response to a SELECT query into a `RowResult`.
|
|
95
|
+
|
|
96
|
+
Values use the same Python types as `clickhouse_driver`, so that results do not depend on the driver.
|
|
97
|
+
In particular, `DateTime` columns without an explicit timezone are returned as naive datetimes (wall-clock
|
|
98
|
+
times in the server's timezone), columns with a timezone as aware datetimes in that timezone, and enums
|
|
99
|
+
as their names.
|
|
100
|
+
|
|
101
|
+
- `response`: the response returned by the driver's `send`.
|
|
102
|
+
- `timezone`: the server's timezone.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TSVCodec(Codec):
|
|
107
|
+
"""
|
|
108
|
+
A codec using ClickHouse's tab-separated formats: `TabSeparatedWithNamesAndTypes` for reading,
|
|
109
|
+
and `TabSeparated` (or `TSKV` for models with function expressions as defaults) for writing.
|
|
110
|
+
Responses must provide `iter_lines()` (see `DriverResponse`), and insert data is sent as chunks of bytes.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
select_format = "TabSeparatedWithNamesAndTypes"
|
|
114
|
+
|
|
115
|
+
def insert_format(self, model_class: type[Model]) -> str:
|
|
116
|
+
"""Returns the ClickHouse format name used when inserting `model_class` instances."""
|
|
117
|
+
# TSKV lets ClickHouse evaluate defaults for fields omitted from the row
|
|
118
|
+
return "TSKV" if model_class.has_funcs_as_defaults() else "TabSeparated"
|
|
119
|
+
|
|
120
|
+
def encode_inserts(self, model_class, instances, batch_size=1000):
|
|
121
|
+
fields_list = ",".join(quote_identifier(name) for name in model_class.fields(writable=True))
|
|
122
|
+
statement = "INSERT INTO $table (%s) FORMAT %s" % (fields_list, self.insert_format(model_class))
|
|
123
|
+
yield statement, self.encode(model_class, instances, batch_size)
|
|
124
|
+
|
|
125
|
+
def encode(self, model_class: type[Model], instances: Iterable[Model], batch_size: int = 1000) -> Iterator[bytes]:
|
|
126
|
+
"""Serialises model instances into chunks of at most `batch_size` lines in the model's insert format."""
|
|
127
|
+
buf = BytesIO()
|
|
128
|
+
lines = 0
|
|
129
|
+
for instance in instances:
|
|
130
|
+
buf.write(instance.to_db_string())
|
|
131
|
+
lines += 1
|
|
132
|
+
if lines >= batch_size:
|
|
133
|
+
yield buf.getvalue()
|
|
134
|
+
buf = BytesIO()
|
|
135
|
+
lines = 0
|
|
136
|
+
if lines:
|
|
137
|
+
yield buf.getvalue()
|
|
138
|
+
|
|
139
|
+
def decode(self, response, model_class=None, timezone=pytz.utc):
|
|
140
|
+
lines = response.iter_lines()
|
|
141
|
+
field_names = parse_tsv(next(lines))
|
|
142
|
+
field_types = parse_tsv(next(lines))
|
|
143
|
+
model_class = model_class or ModelBase.create_ad_hoc_model(zip(field_names, field_types))
|
|
144
|
+
parsers = [_cell_parser(db_type, timezone) for db_type in field_types]
|
|
145
|
+
for line in lines:
|
|
146
|
+
# skip blank line left by WITH TOTALS modifier
|
|
147
|
+
if line:
|
|
148
|
+
cells = line.split(b"\t")
|
|
149
|
+
yield model_class(**{name: parse(cell) for name, parse, cell in zip(field_names, parsers, cells)})
|
|
150
|
+
|
|
151
|
+
def decode_rows(self, response, timezone=pytz.utc):
|
|
152
|
+
lines = response.iter_lines()
|
|
153
|
+
columns = list(zip(parse_tsv(next(lines)), parse_tsv(next(lines))))
|
|
154
|
+
converters = [(_row_converter(db_type, timezone), _is_quoted_type(db_type)) for _, db_type in columns]
|
|
155
|
+
return RowResult(columns, self._iter_rows(lines, converters))
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _iter_rows(lines, converters):
|
|
159
|
+
for line in lines:
|
|
160
|
+
# skip blank line left by WITH TOTALS modifier
|
|
161
|
+
if line:
|
|
162
|
+
yield tuple(
|
|
163
|
+
None if value == b"\\N" else convert(value if quoted else codecs.escape_decode(value)[0])
|
|
164
|
+
for (convert, quoted), value in zip(converters, line.split(b"\t"))
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Types the ORM cannot parse yet are returned as their ClickHouse text representation
|
|
169
|
+
_TEXT_TYPE_PREFIXES = ("Map(", "Nested(", "Variant(", "Dynamic", "JSON", "Object(", "AggregateFunction(")
|
|
170
|
+
_DATETIME_TYPE = re.compile(r"\bDateTime(64)?\b")
|
|
171
|
+
_QUOTED_STRING = re.compile(r"'(?:[^'\\]|\\.)*'")
|
|
172
|
+
_QUOTED_TYPE_PREFIXES = ("Array(", "Tuple(", "Map(", "Nested(")
|
|
173
|
+
_BIG_INT_TYPES = frozenset(["Int128", "UInt128", "Int256", "UInt256"])
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _decode_string(value: bytes) -> str | bytes:
|
|
177
|
+
# Like clickhouse_driver, fall back to bytes for strings which are not valid UTF-8
|
|
178
|
+
try:
|
|
179
|
+
return value.decode("utf-8")
|
|
180
|
+
except UnicodeDecodeError:
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _is_quoted_type(db_type: str) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Whether TSV cells of `db_type` are written in the quoted text format, without escaping them for TSV: this is
|
|
187
|
+
the case for composite types such as arrays, whose elements are quoted and escaped as in SQL.
|
|
188
|
+
"""
|
|
189
|
+
if db_type.startswith("SimpleAggregateFunction("):
|
|
190
|
+
return _is_quoted_type(split_type_args(db_type[24:-1])[1])
|
|
191
|
+
return db_type.startswith(_QUOTED_TYPE_PREFIXES)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _cell_parser(db_type: str, timezone: datetime.tzinfo) -> Callable[[bytes], Any]:
|
|
195
|
+
"""
|
|
196
|
+
Returns a function converting a TSV cell of `db_type` to the value assigned to a model field: its text, except
|
|
197
|
+
for datetimes, which are converted according to the column type (naive for columns without a timezone, and
|
|
198
|
+
aware in the column's timezone otherwise), like `select_rows` and `clickhouse_driver` do.
|
|
199
|
+
"""
|
|
200
|
+
quoted = _is_quoted_type(db_type)
|
|
201
|
+
if _has_datetime(db_type):
|
|
202
|
+
convert = _row_converter(db_type, timezone)
|
|
203
|
+
if quoted:
|
|
204
|
+
return convert
|
|
205
|
+
return lambda cell: None if cell == b"\\N" else convert(codecs.escape_decode(cell)[0])
|
|
206
|
+
if quoted:
|
|
207
|
+
return lambda cell: cell.decode("utf-8")
|
|
208
|
+
return lambda cell: unescape(cell.decode("utf-8"))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _has_datetime(db_type: str) -> bool:
|
|
212
|
+
"""Whether `db_type` is or contains a `DateTime` / `DateTime64` type (ignoring quoted strings, such as enum labels)."""
|
|
213
|
+
return _DATETIME_TYPE.search(_QUOTED_STRING.sub("", db_type)) is not None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _row_converter(db_type: str, timezone: datetime.tzinfo) -> Callable[[bytes], Any]:
|
|
217
|
+
"""
|
|
218
|
+
Returns a function converting a non-NULL TSV value of `db_type` to its Python value. The value must be
|
|
219
|
+
unescaped, unless it is written in the quoted text format (see `_is_quoted_type`).
|
|
220
|
+
"""
|
|
221
|
+
for wrapper in ("Nullable(", "LowCardinality("):
|
|
222
|
+
if db_type.startswith(wrapper):
|
|
223
|
+
return _row_converter(db_type[len(wrapper) : -1], timezone)
|
|
224
|
+
if db_type.startswith("SimpleAggregateFunction("):
|
|
225
|
+
return _row_converter(split_type_args(db_type[24:-1])[1], timezone)
|
|
226
|
+
if db_type == "String" or db_type.startswith(_TEXT_TYPE_PREFIXES):
|
|
227
|
+
return _decode_string
|
|
228
|
+
if db_type.startswith("FixedString("):
|
|
229
|
+
return lambda value: _decode_string(value.rstrip(b"\0"))
|
|
230
|
+
if db_type == "Bool":
|
|
231
|
+
return lambda value: value == b"true"
|
|
232
|
+
if db_type in _BIG_INT_TYPES:
|
|
233
|
+
return int
|
|
234
|
+
if db_type == "Date32":
|
|
235
|
+
return lambda value: datetime.date.fromisoformat(value.decode())
|
|
236
|
+
if db_type == "Nothing":
|
|
237
|
+
return lambda value: None
|
|
238
|
+
if db_type.startswith("Array("):
|
|
239
|
+
# Convert the elements like values of the inner type (e.g. enums to names and datetimes to naive datetimes)
|
|
240
|
+
convert = _row_converter(db_type[len("Array(") : -1], timezone)
|
|
241
|
+
return lambda value: [None if item is None else convert(item.encode()) for item in parse_array(value.decode())]
|
|
242
|
+
if db_type.startswith("Tuple("):
|
|
243
|
+
# Each element is converted like a value of its type (elements of unsupported types are returned as text)
|
|
244
|
+
converters = [_row_converter(element_type, timezone) for _, element_type in parse_tuple_type(db_type)]
|
|
245
|
+
return lambda value: tuple(
|
|
246
|
+
None if item is None else convert(item.encode())
|
|
247
|
+
for convert, item in zip(converters, parse_array(value.decode()))
|
|
248
|
+
)
|
|
249
|
+
try:
|
|
250
|
+
field = ModelBase.create_ad_hoc_field(db_type)
|
|
251
|
+
except (NotImplementedError, AssertionError):
|
|
252
|
+
# Unsupported types (e.g. Map or Dynamic) are rejected by the field classes with either exception
|
|
253
|
+
return _decode_string
|
|
254
|
+
if isinstance(field, BaseEnumField):
|
|
255
|
+
return lambda value: field.to_python(value.decode(), timezone).name
|
|
256
|
+
# Datetimes are naive, unless the column has a timezone (which the ad-hoc field then has too)
|
|
257
|
+
return lambda value: field.to_python(value.decode(), timezone)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
__all__ = ["Codec", "RowResult", "TSVCodec"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SQL generation helpers that do not require a connection to a ClickHouse server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import warnings
|
|
7
|
+
from string import Template
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .models import Model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(frozen=True)
|
|
15
|
+
class ServerCapabilities:
|
|
16
|
+
"""
|
|
17
|
+
Optional ClickHouse server features that affect the generated DDL.
|
|
18
|
+
|
|
19
|
+
The defaults describe a modern server. Use `ServerCapabilities.from_version()` to
|
|
20
|
+
derive the flags from a server version, or override individual flags as needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Custom partitioning keys (PARTITION BY / ORDER BY) were introduced in 1.1.54310
|
|
24
|
+
has_custom_partitioning: bool = True
|
|
25
|
+
# Column compression codecs were introduced in 19.1.16
|
|
26
|
+
has_codec_support: bool = True
|
|
27
|
+
# LowCardinality columns were introduced in 19.0
|
|
28
|
+
has_low_cardinality_support: bool = True
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_version(cls, server_version: tuple[int, ...]) -> ServerCapabilities:
|
|
32
|
+
"""Returns the capabilities of a ClickHouse server with the given version tuple."""
|
|
33
|
+
return cls(
|
|
34
|
+
has_custom_partitioning=server_version >= (1, 1, 54310),
|
|
35
|
+
has_codec_support=server_version >= (19, 1, 16),
|
|
36
|
+
has_low_cardinality_support=server_version >= (19, 0),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def quote_identifier(name: str) -> str:
|
|
41
|
+
"""Wraps an identifier (database, table, column name) in backticks."""
|
|
42
|
+
return "`%s`" % name
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def qualified_name(db_name: str, table_name: str) -> str:
|
|
46
|
+
"""Returns the backtick-quoted, database-qualified name of a table."""
|
|
47
|
+
return "%s.%s" % (quote_identifier(db_name), quote_identifier(table_name))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def model_table_ref(db_name: str, model_class: type[Model]) -> str:
|
|
51
|
+
"""Returns the qualified table name of a model, taking system models into account."""
|
|
52
|
+
return qualified_name("system" if model_class.is_system_model() else db_name, model_class.table_name())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def substitute(query: str, db_name: str, model_class: type[Model] | None = None) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Replaces the `$db` and (if `model_class` is given) `$table` placeholders in a query
|
|
58
|
+
with fully-qualified, backtick-quoted names.
|
|
59
|
+
"""
|
|
60
|
+
if "$" not in query:
|
|
61
|
+
return query
|
|
62
|
+
mapping = {"db": quote_identifier(db_name)}
|
|
63
|
+
if model_class:
|
|
64
|
+
mapping["table"] = model_table_ref(db_name, model_class)
|
|
65
|
+
return Template(query).safe_substitute(mapping)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_ddl_target(db_name: Any, capabilities: ServerCapabilities | None, caller: str):
|
|
69
|
+
"""
|
|
70
|
+
Normalises the arguments of the DDL generation methods into a `(db_name, capabilities)` pair.
|
|
71
|
+
|
|
72
|
+
Passing a `Database` instance instead of a database name is supported for backwards
|
|
73
|
+
compatibility, but is deprecated.
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(db_name, str):
|
|
76
|
+
return db_name, capabilities or ServerCapabilities()
|
|
77
|
+
warnings.warn(
|
|
78
|
+
f"Passing a Database to {caller}() is deprecated, pass the database name and a ServerCapabilities instead",
|
|
79
|
+
DeprecationWarning,
|
|
80
|
+
stacklevel=3,
|
|
81
|
+
)
|
|
82
|
+
return db_name.db_name, capabilities or db_name.capabilities
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["ServerCapabilities"]
|