dotorm 2.0.8__py3-none-any.whl
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.
- dotorm/__init__.py +87 -0
- dotorm/access.py +151 -0
- dotorm/builder/__init__.py +0 -0
- dotorm/builder/builder.py +72 -0
- dotorm/builder/helpers.py +63 -0
- dotorm/builder/mixins/__init__.py +11 -0
- dotorm/builder/mixins/crud.py +246 -0
- dotorm/builder/mixins/m2m.py +110 -0
- dotorm/builder/mixins/relations.py +96 -0
- dotorm/builder/protocol.py +63 -0
- dotorm/builder/request_builder.py +144 -0
- dotorm/components/__init__.py +18 -0
- dotorm/components/dialect.py +99 -0
- dotorm/components/filter_parser.py +195 -0
- dotorm/databases/__init__.py +13 -0
- dotorm/databases/abstract/__init__.py +25 -0
- dotorm/databases/abstract/dialect.py +134 -0
- dotorm/databases/abstract/pool.py +10 -0
- dotorm/databases/abstract/session.py +67 -0
- dotorm/databases/abstract/types.py +36 -0
- dotorm/databases/clickhouse/__init__.py +8 -0
- dotorm/databases/clickhouse/pool.py +60 -0
- dotorm/databases/clickhouse/session.py +100 -0
- dotorm/databases/mysql/__init__.py +13 -0
- dotorm/databases/mysql/pool.py +69 -0
- dotorm/databases/mysql/session.py +128 -0
- dotorm/databases/mysql/transaction.py +39 -0
- dotorm/databases/postgres/__init__.py +23 -0
- dotorm/databases/postgres/pool.py +133 -0
- dotorm/databases/postgres/session.py +174 -0
- dotorm/databases/postgres/transaction.py +82 -0
- dotorm/decorators.py +379 -0
- dotorm/exceptions.py +9 -0
- dotorm/fields.py +604 -0
- dotorm/integrations/__init__.py +0 -0
- dotorm/integrations/pydantic.py +275 -0
- dotorm/model.py +802 -0
- dotorm/orm/__init__.py +15 -0
- dotorm/orm/mixins/__init__.py +13 -0
- dotorm/orm/mixins/access.py +67 -0
- dotorm/orm/mixins/ddl.py +250 -0
- dotorm/orm/mixins/many2many.py +175 -0
- dotorm/orm/mixins/primary.py +218 -0
- dotorm/orm/mixins/relations.py +513 -0
- dotorm/orm/protocol.py +147 -0
- dotorm/orm/utils.py +39 -0
- dotorm-2.0.8.dist-info/METADATA +1240 -0
- dotorm-2.0.8.dist-info/RECORD +50 -0
- dotorm-2.0.8.dist-info/WHEEL +4 -0
- dotorm-2.0.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Many2many query builder."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..protocol import BuilderProtocol
|
|
7
|
+
from ...orm.model import DotModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Many2ManyMixin:
|
|
11
|
+
"""Mixin for many-to-many relation queries."""
|
|
12
|
+
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
def build_get_many2many(
|
|
16
|
+
self: "BuilderProtocol",
|
|
17
|
+
id: int,
|
|
18
|
+
relation_table: "DotModel",
|
|
19
|
+
many2many_table: str,
|
|
20
|
+
column1: str,
|
|
21
|
+
column2: str,
|
|
22
|
+
fields: list[str],
|
|
23
|
+
order: Literal["desc", "asc"] = "desc",
|
|
24
|
+
start: int | None = None,
|
|
25
|
+
end: int | None = None,
|
|
26
|
+
sort: str = "id",
|
|
27
|
+
limit: int | None = 10,
|
|
28
|
+
) -> tuple[str, tuple]:
|
|
29
|
+
"""Build SELECT for M2M relation."""
|
|
30
|
+
if not fields:
|
|
31
|
+
fields = relation_table.get_store_fields()
|
|
32
|
+
|
|
33
|
+
# явно указать для sql запроса что эти поля относятся
|
|
34
|
+
# к связанной таблице
|
|
35
|
+
fields_prefixed = [f"p.{field}" for field in fields]
|
|
36
|
+
fields_select_stmt = ", ".join(fields_prefixed)
|
|
37
|
+
|
|
38
|
+
stmt = f"""
|
|
39
|
+
SELECT {fields_select_stmt}
|
|
40
|
+
FROM {relation_table.__table__} p
|
|
41
|
+
JOIN {many2many_table} pt ON p.id = pt.{column1}
|
|
42
|
+
JOIN {self.table} t ON pt.{column2} = t.id
|
|
43
|
+
WHERE t.id = %s
|
|
44
|
+
ORDER BY {sort} {order}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
val: tuple = (id,)
|
|
48
|
+
|
|
49
|
+
if end is not None and start is not None:
|
|
50
|
+
stmt += "LIMIT %s OFFSET %s"
|
|
51
|
+
val += (end - start, start)
|
|
52
|
+
elif limit:
|
|
53
|
+
stmt += "LIMIT %s"
|
|
54
|
+
val += (limit,)
|
|
55
|
+
|
|
56
|
+
return stmt, val
|
|
57
|
+
|
|
58
|
+
def build_get_many2many_multiple(
|
|
59
|
+
self: "BuilderProtocol",
|
|
60
|
+
ids: list[int],
|
|
61
|
+
relation_table: "DotModel",
|
|
62
|
+
many2many_table: str,
|
|
63
|
+
column1: str,
|
|
64
|
+
column2: str,
|
|
65
|
+
fields: list[str] | None = None,
|
|
66
|
+
limit: int = 80,
|
|
67
|
+
) -> tuple[str, tuple]:
|
|
68
|
+
"""
|
|
69
|
+
Оптимизированная версия, когда необходимо получить сразу несколько свзяей m2m
|
|
70
|
+
у нескольких записей. Не просто один список на одну записиь.
|
|
71
|
+
А N списков на N записей.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
tuple[str, tuple]: SQL statement and parameter values
|
|
75
|
+
"""
|
|
76
|
+
if not fields:
|
|
77
|
+
fields = relation_table.get_store_fields()
|
|
78
|
+
|
|
79
|
+
# явно указать для sql запроса что эти поля относятся
|
|
80
|
+
# к связанной таблице
|
|
81
|
+
fields_prefixed = [f"p.{field}" for field in fields]
|
|
82
|
+
|
|
83
|
+
# добавляем ид из таблицы связи для последующего маппинга записей
|
|
84
|
+
# имеется ввиду за один запрос достаются все записи для всех ид
|
|
85
|
+
# а далее в питоне для каждого ид остаются только его
|
|
86
|
+
fields_prefixed.append(f"pt.{column2} as m2m_id")
|
|
87
|
+
|
|
88
|
+
fields_select_stmt = ", ".join(fields_prefixed)
|
|
89
|
+
query_placeholders = ", ".join(["%s"] * len(ids))
|
|
90
|
+
|
|
91
|
+
stmt = f"""
|
|
92
|
+
SELECT {fields_select_stmt}
|
|
93
|
+
FROM {relation_table.__table__} p
|
|
94
|
+
JOIN {many2many_table} pt ON p.id = pt.{column1}
|
|
95
|
+
JOIN {self.table} t ON pt.{column2} = t.id
|
|
96
|
+
WHERE t.id IN ({query_placeholders})
|
|
97
|
+
LIMIT %s
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
val = (*ids, limit)
|
|
101
|
+
return stmt, val
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# SELECT * FROM (
|
|
105
|
+
# SELECT p.*, pt.column2 as m2m_id,
|
|
106
|
+
# ROW_NUMBER() OVER (PARTITION BY pt.column2 ORDER BY p.id) as rn
|
|
107
|
+
# FROM relation_table p
|
|
108
|
+
# JOIN m2m_table pt ON p.id = pt.column1
|
|
109
|
+
# WHERE pt.column2 IN (%s, %s, ...)
|
|
110
|
+
# ) sub WHERE rn <= {limit_per_parent}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Relations query builder."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Self
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..protocol import BuilderProtocol
|
|
7
|
+
|
|
8
|
+
from ..request_builder import RequestBuilder
|
|
9
|
+
from ...fields import Field, Many2many, Many2one, One2many
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RelationsMixin:
|
|
13
|
+
"""Mixin for relation queries."""
|
|
14
|
+
|
|
15
|
+
__slots__ = ()
|
|
16
|
+
|
|
17
|
+
def build_search_relation(
|
|
18
|
+
self: "BuilderProtocol",
|
|
19
|
+
fields_relation: list[tuple[str, Field]],
|
|
20
|
+
records: list | None = None,
|
|
21
|
+
) -> list[RequestBuilder]:
|
|
22
|
+
"""
|
|
23
|
+
Build optimized queries for loading relations.
|
|
24
|
+
Avoids N+1 by batching relation queries.
|
|
25
|
+
"""
|
|
26
|
+
if records is None:
|
|
27
|
+
records = []
|
|
28
|
+
|
|
29
|
+
request_list: list[RequestBuilder] = []
|
|
30
|
+
ids: list[int] = [record.id for record in records]
|
|
31
|
+
|
|
32
|
+
if not ids:
|
|
33
|
+
return request_list
|
|
34
|
+
|
|
35
|
+
for name, field in fields_relation:
|
|
36
|
+
# Default fields for relations
|
|
37
|
+
fields = ["id"]
|
|
38
|
+
if field.relation_table and field.relation_table.get_fields().get(
|
|
39
|
+
"name"
|
|
40
|
+
):
|
|
41
|
+
fields.append("name")
|
|
42
|
+
|
|
43
|
+
req: RequestBuilder | None = None
|
|
44
|
+
|
|
45
|
+
if isinstance(field, One2many):
|
|
46
|
+
stmt, val = field.relation_table._builder.build_search(
|
|
47
|
+
fields=[*fields, field.relation_table_field],
|
|
48
|
+
filter=[(field.relation_table_field, "in", ids)],
|
|
49
|
+
)
|
|
50
|
+
req = RequestBuilder(
|
|
51
|
+
stmt=stmt,
|
|
52
|
+
value=val,
|
|
53
|
+
field_name=name,
|
|
54
|
+
field=field,
|
|
55
|
+
fields=fields,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
elif isinstance(field, Many2many):
|
|
59
|
+
stmt, val = self.build_get_many2many_multiple(
|
|
60
|
+
ids=ids,
|
|
61
|
+
relation_table=field.relation_table,
|
|
62
|
+
many2many_table=field.many2many_table,
|
|
63
|
+
column1=field.column1,
|
|
64
|
+
column2=field.column2,
|
|
65
|
+
fields=fields,
|
|
66
|
+
)
|
|
67
|
+
req = RequestBuilder(
|
|
68
|
+
stmt=stmt,
|
|
69
|
+
value=val,
|
|
70
|
+
field_name=name,
|
|
71
|
+
field=field,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
elif isinstance(field, Many2one):
|
|
75
|
+
ids_m2o: list[int] = [
|
|
76
|
+
getattr(record, name) for record in records
|
|
77
|
+
]
|
|
78
|
+
# оставляем только уникальные ид, так как в m2o несколько записей
|
|
79
|
+
# могут ссылаться на одну сущность
|
|
80
|
+
ids_m2o = list(set(ids_m2o))
|
|
81
|
+
|
|
82
|
+
stmt, val = field.relation_table._builder.build_search(
|
|
83
|
+
fields=fields,
|
|
84
|
+
filter=[("id", "in", ids_m2o)],
|
|
85
|
+
)
|
|
86
|
+
req = RequestBuilder(
|
|
87
|
+
stmt=stmt,
|
|
88
|
+
value=val,
|
|
89
|
+
field_name=name,
|
|
90
|
+
field=field,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if req:
|
|
94
|
+
request_list.append(req)
|
|
95
|
+
|
|
96
|
+
return request_list
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Protocol defining what mixins expect from the base class."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from ..components.filter_parser import FilterExpression
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..model import DotModel
|
|
9
|
+
from ..fields import Field
|
|
10
|
+
from ..components.dialect import Dialect
|
|
11
|
+
from ..components.filter_parser import FilterParser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class BuilderProtocol(Protocol):
|
|
16
|
+
"""Contract that mixins expect from the base class."""
|
|
17
|
+
|
|
18
|
+
table: str
|
|
19
|
+
fields: dict[str, "Field"]
|
|
20
|
+
dialect: "Dialect"
|
|
21
|
+
filter_parser: "FilterParser"
|
|
22
|
+
|
|
23
|
+
def get_store_fields(self) -> list[str]:
|
|
24
|
+
"""Returns field names that are stored in DB."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def build_search(
|
|
28
|
+
self,
|
|
29
|
+
fields: list[str] | None = None,
|
|
30
|
+
start: int | None = None,
|
|
31
|
+
end: int | None = None,
|
|
32
|
+
limit: int = 80,
|
|
33
|
+
order: Literal["DESC", "ASC", "desc", "asc"] = "DESC",
|
|
34
|
+
sort: str = "id",
|
|
35
|
+
filter: FilterExpression | None = None,
|
|
36
|
+
raw: bool = False,
|
|
37
|
+
) -> tuple[str, tuple]: ...
|
|
38
|
+
|
|
39
|
+
def build_get_many2many_multiple(
|
|
40
|
+
self: "BuilderProtocol",
|
|
41
|
+
ids: list[int],
|
|
42
|
+
relation_table: "DotModel",
|
|
43
|
+
many2many_table: str,
|
|
44
|
+
column1: str,
|
|
45
|
+
column2: str,
|
|
46
|
+
fields: list[str] | None = None,
|
|
47
|
+
limit: int = 80,
|
|
48
|
+
) -> tuple[str, tuple]: ...
|
|
49
|
+
|
|
50
|
+
def build_get_many2many(
|
|
51
|
+
self,
|
|
52
|
+
id: int,
|
|
53
|
+
relation_table: "DotModel",
|
|
54
|
+
many2many_table: str,
|
|
55
|
+
column1: str,
|
|
56
|
+
column2: str,
|
|
57
|
+
fields: list[str],
|
|
58
|
+
order: Literal["desc", "asc"] = "desc",
|
|
59
|
+
start: int | None = None,
|
|
60
|
+
end: int | None = None,
|
|
61
|
+
sort: str = "id",
|
|
62
|
+
limit: int | None = 10,
|
|
63
|
+
) -> tuple[str, tuple]: ...
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Request builder for relation queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass, field as d_field
|
|
5
|
+
from typing import Any, Callable, ClassVar
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
from ..fields import (
|
|
9
|
+
AttachmentMany2one,
|
|
10
|
+
AttachmentOne2many,
|
|
11
|
+
Field,
|
|
12
|
+
Many2many,
|
|
13
|
+
Many2one,
|
|
14
|
+
One2many,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FetchMode(Enum):
|
|
19
|
+
"""Database cursor fetch mode."""
|
|
20
|
+
|
|
21
|
+
FETCHALL = auto()
|
|
22
|
+
FETCHONE = auto()
|
|
23
|
+
FETCHMANY = auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Type aliases for relation fields
|
|
27
|
+
RelationField = Many2many | One2many | Many2one
|
|
28
|
+
FormRelationField = (
|
|
29
|
+
Many2many | One2many | Many2one | AttachmentMany2one | AttachmentOne2many
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class RequestBuilder:
|
|
35
|
+
"""
|
|
36
|
+
Container for relation query request.
|
|
37
|
+
|
|
38
|
+
Immutable data container with computed properties for
|
|
39
|
+
determining how to process query results.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
stmt: str | None
|
|
43
|
+
value: Any
|
|
44
|
+
field_name: str
|
|
45
|
+
field: Field
|
|
46
|
+
fields: list[str] = d_field(default_factory=lambda: ["id", "name"])
|
|
47
|
+
fetch_mode: FetchMode = FetchMode.FETCHALL
|
|
48
|
+
|
|
49
|
+
# Mapping for prepare functions based on field type
|
|
50
|
+
_PREPARE_FUNCS: ClassVar[dict[type, str]] = {
|
|
51
|
+
Many2many: "prepare_list_ids",
|
|
52
|
+
One2many: "prepare_list_ids",
|
|
53
|
+
Many2one: "prepare_list_ids",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def function_cursor(self) -> str:
|
|
58
|
+
"""Returns cursor method name based on fetch mode."""
|
|
59
|
+
return self.fetch_mode.name.lower()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def function_prepare(self) -> Callable:
|
|
63
|
+
"""
|
|
64
|
+
Returns appropriate prepare function based on field type.
|
|
65
|
+
|
|
66
|
+
For relation fields (Many2many, One2many, Many2one),
|
|
67
|
+
returns prepare_list_ids from relation_table.
|
|
68
|
+
"""
|
|
69
|
+
for field_type, method_name in self._PREPARE_FUNCS.items():
|
|
70
|
+
if isinstance(self.field, field_type):
|
|
71
|
+
return getattr(self.field.relation_table, method_name)
|
|
72
|
+
|
|
73
|
+
# Fallback for non-relation fields
|
|
74
|
+
# TODO: помоему тут ошибка relation_table всегда будет пустое
|
|
75
|
+
# а реквест билдер всеравно не используется в не связей
|
|
76
|
+
# и else никогда не вызывается
|
|
77
|
+
return getattr(self.field.relation_table, "prepare_list_id")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True)
|
|
81
|
+
class RequestBuilderForm(RequestBuilder):
|
|
82
|
+
"""
|
|
83
|
+
Container for form relation query request.
|
|
84
|
+
|
|
85
|
+
Extends RequestBuilder with form-specific prepare functions.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Override mapping for form context
|
|
89
|
+
_PREPARE_FUNCS: ClassVar[dict[type, str]] = {
|
|
90
|
+
Many2many: "prepare_form_ids",
|
|
91
|
+
One2many: "prepare_form_ids",
|
|
92
|
+
Many2one: "prepare_form_ids",
|
|
93
|
+
AttachmentMany2one: "prepare_form_ids",
|
|
94
|
+
AttachmentOne2many: "prepare_form_ids",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def function_prepare(self) -> Callable:
|
|
99
|
+
"""
|
|
100
|
+
Returns appropriate prepare function for form context.
|
|
101
|
+
|
|
102
|
+
Form context uses prepare_form_ids/prepare_form_id methods.
|
|
103
|
+
"""
|
|
104
|
+
for field_type, method_name in self._PREPARE_FUNCS.items():
|
|
105
|
+
if isinstance(self.field, field_type):
|
|
106
|
+
return getattr(self.field.relation_table, method_name)
|
|
107
|
+
|
|
108
|
+
return getattr(self.field.relation_table, "prepare_form_id")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_request_builder(
|
|
112
|
+
stmt: str | None,
|
|
113
|
+
value: Any,
|
|
114
|
+
field_name: str,
|
|
115
|
+
field: Field,
|
|
116
|
+
fields: list[str] | None = None,
|
|
117
|
+
*,
|
|
118
|
+
form_mode: bool = False,
|
|
119
|
+
) -> RequestBuilder:
|
|
120
|
+
"""
|
|
121
|
+
Factory function for creating appropriate RequestBuilder.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
stmt: SQL statement
|
|
125
|
+
value: Query parameters
|
|
126
|
+
field_name: Name of the field
|
|
127
|
+
field: Field instance
|
|
128
|
+
fields: Fields to select (default: ["id", "name"])
|
|
129
|
+
form_mode: If True, creates RequestBuilderForm
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
RequestBuilder or RequestBuilderForm instance
|
|
133
|
+
"""
|
|
134
|
+
if fields is None:
|
|
135
|
+
fields = ["id", "name"]
|
|
136
|
+
|
|
137
|
+
cls = RequestBuilderForm if form_mode else RequestBuilder
|
|
138
|
+
return cls(
|
|
139
|
+
stmt=stmt,
|
|
140
|
+
value=value,
|
|
141
|
+
field_name=field_name,
|
|
142
|
+
field=field,
|
|
143
|
+
fields=fields,
|
|
144
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extracted components for cleaner architecture.
|
|
3
|
+
|
|
4
|
+
These components can be used standalone or by existing ORM classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .dialect import Dialect, POSTGRES, MYSQL, get_dialect
|
|
8
|
+
from .filter_parser import FilterParser, FilterExpression, FilterTriplet
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Dialect",
|
|
12
|
+
"POSTGRES",
|
|
13
|
+
"MYSQL",
|
|
14
|
+
"get_dialect",
|
|
15
|
+
"FilterParser",
|
|
16
|
+
"FilterExpression",
|
|
17
|
+
"FilterTriplet",
|
|
18
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database dialect definitions.
|
|
3
|
+
|
|
4
|
+
Centralizes all dialect-specific logic in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Dialect:
|
|
13
|
+
"""
|
|
14
|
+
Database dialect configuration.
|
|
15
|
+
|
|
16
|
+
Contains all dialect-specific settings:
|
|
17
|
+
- name: dialect identifier
|
|
18
|
+
- escape: character for escaping identifiers ("` for MySQL, " for Postgres)
|
|
19
|
+
- placeholder: parameter placeholder style (%s for MySQL, $N for Postgres)
|
|
20
|
+
- supports_returning: whether INSERT ... RETURNING is supported
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: Literal["postgres", "mysql", "clickhouse"]
|
|
24
|
+
escape: str
|
|
25
|
+
placeholder: str
|
|
26
|
+
supports_returning: bool
|
|
27
|
+
|
|
28
|
+
def escape_identifier(self, identifier: str) -> str:
|
|
29
|
+
"""Escape a column/table name."""
|
|
30
|
+
return f"{self.escape}{identifier}{self.escape}"
|
|
31
|
+
|
|
32
|
+
def make_placeholders(self, count: int, start: int = 1) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Generate placeholder string for given count of parameters.
|
|
35
|
+
|
|
36
|
+
For MySQL: %s, %s, %s
|
|
37
|
+
For Postgres: $1, $2, $3
|
|
38
|
+
For Clickhouse: %s, %s, %s
|
|
39
|
+
"""
|
|
40
|
+
if self.name == "postgres":
|
|
41
|
+
return ", ".join(f"${i}" for i in range(start, start + count))
|
|
42
|
+
else:
|
|
43
|
+
return ", ".join(["%s"] * count)
|
|
44
|
+
|
|
45
|
+
def make_placeholder(self, index: int = 1) -> str:
|
|
46
|
+
"""Generate single placeholder."""
|
|
47
|
+
if self.name == "postgres":
|
|
48
|
+
return f"${index}"
|
|
49
|
+
return "%s"
|
|
50
|
+
|
|
51
|
+
def get_no_transaction_session(self):
|
|
52
|
+
"""Get appropriate session class for this dialect."""
|
|
53
|
+
if self.name == "postgres":
|
|
54
|
+
from ..databases.postgres.session import NoTransactionSession
|
|
55
|
+
|
|
56
|
+
return NoTransactionSession
|
|
57
|
+
elif self.name == "mysql":
|
|
58
|
+
from ..databases.mysql.session import NoTransactionSession
|
|
59
|
+
|
|
60
|
+
return NoTransactionSession
|
|
61
|
+
else:
|
|
62
|
+
from ..databases.clickhouse.session import NoTransactionSession
|
|
63
|
+
|
|
64
|
+
return NoTransactionSession
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Pre-defined dialects
|
|
68
|
+
POSTGRES = Dialect(
|
|
69
|
+
name="postgres",
|
|
70
|
+
escape='"',
|
|
71
|
+
placeholder="$",
|
|
72
|
+
supports_returning=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
MYSQL = Dialect(
|
|
76
|
+
name="mysql",
|
|
77
|
+
escape="`",
|
|
78
|
+
placeholder="%s",
|
|
79
|
+
supports_returning=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
CLICKHOUSE = Dialect(
|
|
83
|
+
name="clickhouse",
|
|
84
|
+
escape="`",
|
|
85
|
+
placeholder="%s",
|
|
86
|
+
supports_returning=False,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_dialect(name: str) -> Dialect:
|
|
91
|
+
"""Get dialect by name."""
|
|
92
|
+
if name == "postgres":
|
|
93
|
+
return POSTGRES
|
|
94
|
+
elif name == "mysql":
|
|
95
|
+
return MYSQL
|
|
96
|
+
elif name == "clickhouse":
|
|
97
|
+
return CLICKHOUSE
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Unknown dialect: {name}")
|