rakko-orm 1.0.0a0__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.
Files changed (143) hide show
  1. rakko/__init__.py +613 -0
  2. rakko/__main__.py +10 -0
  3. rakko/apps.py +356 -0
  4. rakko/backends/__init__.py +4 -0
  5. rakko/backends/asyncpg/__init__.py +7 -0
  6. rakko/backends/asyncpg/client.py +223 -0
  7. rakko/backends/asyncpg/executor.py +20 -0
  8. rakko/backends/asyncpg/schema_generator.py +17 -0
  9. rakko/backends/base/__init__.py +4 -0
  10. rakko/backends/base/client.py +438 -0
  11. rakko/backends/base/config_generator.py +249 -0
  12. rakko/backends/base/executor.py +764 -0
  13. rakko/backends/base/schema_generator.py +580 -0
  14. rakko/backends/base_postgres/__init__.py +4 -0
  15. rakko/backends/base_postgres/client.py +170 -0
  16. rakko/backends/base_postgres/executor.py +149 -0
  17. rakko/backends/base_postgres/schema_generator.py +84 -0
  18. rakko/backends/mssql/__init__.py +7 -0
  19. rakko/backends/mssql/client.py +127 -0
  20. rakko/backends/mssql/executor.py +17 -0
  21. rakko/backends/mssql/schema_generator.py +125 -0
  22. rakko/backends/mysql/__init__.py +7 -0
  23. rakko/backends/mysql/client.py +312 -0
  24. rakko/backends/mysql/executor.py +154 -0
  25. rakko/backends/mysql/schema_generator.py +101 -0
  26. rakko/backends/odbc/__init__.py +4 -0
  27. rakko/backends/odbc/client.py +221 -0
  28. rakko/backends/odbc/executor.py +20 -0
  29. rakko/backends/oracle/__init__.py +7 -0
  30. rakko/backends/oracle/client.py +136 -0
  31. rakko/backends/oracle/executor.py +27 -0
  32. rakko/backends/oracle/schema_generator.py +120 -0
  33. rakko/backends/psycopg/__init__.py +7 -0
  34. rakko/backends/psycopg/client.py +288 -0
  35. rakko/backends/psycopg/executor.py +14 -0
  36. rakko/backends/psycopg/schema_generator.py +17 -0
  37. rakko/backends/sqlite/__init__.py +14 -0
  38. rakko/backends/sqlite/client.py +299 -0
  39. rakko/backends/sqlite/executor.py +88 -0
  40. rakko/backends/sqlite/schema_generator.py +32 -0
  41. rakko/cli/__init__.py +13 -0
  42. rakko/cli/cli.py +878 -0
  43. rakko/cli/utils.py +181 -0
  44. rakko/config.py +315 -0
  45. rakko/connection.py +345 -0
  46. rakko/context.py +571 -0
  47. rakko/contrib/__init__.py +4 -0
  48. rakko/contrib/aiohttp/__init__.py +99 -0
  49. rakko/contrib/blacksheep/__init__.py +121 -0
  50. rakko/contrib/fastapi/__init__.py +301 -0
  51. rakko/contrib/mysql/__init__.py +4 -0
  52. rakko/contrib/mysql/fields.py +63 -0
  53. rakko/contrib/mysql/functions.py +19 -0
  54. rakko/contrib/mysql/indexes.py +28 -0
  55. rakko/contrib/mysql/json_functions.py +80 -0
  56. rakko/contrib/mysql/search.py +49 -0
  57. rakko/contrib/postgres/__init__.py +4 -0
  58. rakko/contrib/postgres/array_functions.py +35 -0
  59. rakko/contrib/postgres/fields.py +105 -0
  60. rakko/contrib/postgres/functions.py +43 -0
  61. rakko/contrib/postgres/indexes.py +33 -0
  62. rakko/contrib/postgres/json_functions.py +113 -0
  63. rakko/contrib/postgres/regex.py +31 -0
  64. rakko/contrib/postgres/search.py +483 -0
  65. rakko/contrib/pydantic/__init__.py +16 -0
  66. rakko/contrib/pydantic/base.py +147 -0
  67. rakko/contrib/pydantic/creator.py +681 -0
  68. rakko/contrib/pydantic/descriptions.py +207 -0
  69. rakko/contrib/pydantic/utils.py +38 -0
  70. rakko/contrib/pylint/__init__.py +158 -0
  71. rakko/contrib/quart/__init__.py +117 -0
  72. rakko/contrib/sanic/__init__.py +116 -0
  73. rakko/contrib/sqlite/__init__.py +4 -0
  74. rakko/contrib/sqlite/functions.py +16 -0
  75. rakko/contrib/sqlite/regex.py +46 -0
  76. rakko/contrib/starlette/__init__.py +180 -0
  77. rakko/contrib/test/__init__.py +336 -0
  78. rakko/contrib/test/condition.py +42 -0
  79. rakko/converters.py +160 -0
  80. rakko/exceptions.py +130 -0
  81. rakko/expressions.py +962 -0
  82. rakko/fields/__init__.py +111 -0
  83. rakko/fields/base.py +685 -0
  84. rakko/fields/data.py +1308 -0
  85. rakko/fields/db_defaults.py +105 -0
  86. rakko/fields/relational.py +703 -0
  87. rakko/filters.py +639 -0
  88. rakko/functions.py +142 -0
  89. rakko/indexes.py +165 -0
  90. rakko/log.py +8 -0
  91. rakko/manager.py +29 -0
  92. rakko/migrations/__init__.py +41 -0
  93. rakko/migrations/api/__init__.py +16 -0
  94. rakko/migrations/api/migrate.py +77 -0
  95. rakko/migrations/api/plan.py +76 -0
  96. rakko/migrations/api/sqlmigrate.py +69 -0
  97. rakko/migrations/autodetector.py +203 -0
  98. rakko/migrations/constraints.py +31 -0
  99. rakko/migrations/exceptions.py +10 -0
  100. rakko/migrations/executor.py +368 -0
  101. rakko/migrations/expressions.py +13 -0
  102. rakko/migrations/graph.py +158 -0
  103. rakko/migrations/loader.py +127 -0
  104. rakko/migrations/migration.py +189 -0
  105. rakko/migrations/operations.py +1288 -0
  106. rakko/migrations/recorder.py +104 -0
  107. rakko/migrations/schema_editor/__init__.py +25 -0
  108. rakko/migrations/schema_editor/asyncpg.py +11 -0
  109. rakko/migrations/schema_editor/base.py +945 -0
  110. rakko/migrations/schema_editor/base_postgres.py +171 -0
  111. rakko/migrations/schema_editor/data.py +18 -0
  112. rakko/migrations/schema_editor/mssql.py +339 -0
  113. rakko/migrations/schema_editor/mysql.py +408 -0
  114. rakko/migrations/schema_editor/oracle.py +259 -0
  115. rakko/migrations/schema_editor/psycopg.py +11 -0
  116. rakko/migrations/schema_editor/sqlite.py +424 -0
  117. rakko/migrations/schema_generator/__init__.py +5 -0
  118. rakko/migrations/schema_generator/operation_generator.py +163 -0
  119. rakko/migrations/schema_generator/state.py +224 -0
  120. rakko/migrations/schema_generator/state_apps.py +165 -0
  121. rakko/migrations/schema_generator/state_diff.py +419 -0
  122. rakko/migrations/writer.py +515 -0
  123. rakko/models.py +1854 -0
  124. rakko/py.typed +0 -0
  125. rakko/query_api.py +134 -0
  126. rakko/query_utils.py +238 -0
  127. rakko/queryset.py +2743 -0
  128. rakko/relations.py +85 -0
  129. rakko/router.py +55 -0
  130. rakko/schema_quoting.py +62 -0
  131. rakko/signals.py +73 -0
  132. rakko/timezone.py +173 -0
  133. rakko/transactions.py +69 -0
  134. rakko/utils.py +50 -0
  135. rakko/validators.py +412 -0
  136. rakko/warnings.py +22 -0
  137. rakko_orm-1.0.0a0.dist-info/METADATA +114 -0
  138. rakko_orm-1.0.0a0.dist-info/RECORD +143 -0
  139. rakko_orm-1.0.0a0.dist-info/WHEEL +4 -0
  140. rakko_orm-1.0.0a0.dist-info/entry_points.txt +5 -0
  141. rakko_orm-1.0.0a0.dist-info/licenses/CONTRIBUTORS.rst +86 -0
  142. rakko_orm-1.0.0a0.dist-info/licenses/LICENSE.txt +201 -0
  143. rakko_orm-1.0.0a0.dist-info/licenses/NOTICE.txt +22 -0
rakko/apps.py ADDED
@@ -0,0 +1,356 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ import importlib
8
+ import warnings
9
+ from collections.abc import Callable, Iterable, Iterator
10
+ from copy import copy
11
+ from inspect import isclass
12
+ from itertools import chain
13
+ from types import ModuleType
14
+ from typing import Any, cast
15
+
16
+ from pypika_tortoise import Table
17
+ from pypika_tortoise.queries import QueryBuilder
18
+
19
+ from rakko.connection import ConnectionHandler
20
+ from rakko.exceptions import ConfigurationError
21
+ from rakko.fields.relational import (
22
+ BackwardFKRelation,
23
+ BackwardOneToOneRelation,
24
+ ForeignKeyFieldInstance,
25
+ ManyToManyFieldInstance,
26
+ OneToOneFieldInstance,
27
+ )
28
+ from rakko.filters import get_m2m_filters
29
+ from rakko.models import Model
30
+
31
+
32
+ class Apps:
33
+ def __init__(
34
+ self,
35
+ config: dict[str, dict[str, Any]] | None,
36
+ connections: ConnectionHandler,
37
+ table_name_generator: Callable[[type[Model]], str] | None = None,
38
+ *,
39
+ validate_connections: bool = True,
40
+ ) -> None:
41
+ self.apps: dict[str, dict[str, type[Model]]] = {}
42
+ self._config = config or {}
43
+ self._connections = connections
44
+ self._table_name_generator = table_name_generator
45
+ self._validate_connections = validate_connections
46
+ if self._config:
47
+ self._load_from_config()
48
+
49
+ @staticmethod
50
+ def _discover_models(models_path: ModuleType | str, app_label: str) -> list[type[Model]]:
51
+ if isinstance(models_path, ModuleType):
52
+ module = models_path
53
+ else:
54
+ try:
55
+ module = importlib.import_module(models_path)
56
+ except ImportError:
57
+ raise ConfigurationError(f'Module "{models_path}" not found')
58
+ discovered_models: list[type[Model]] = []
59
+ if possible_models := getattr(module, "__models__", None):
60
+ try:
61
+ possible_models = [*possible_models]
62
+ except TypeError:
63
+ possible_models = None
64
+ if not possible_models:
65
+ possible_models = [getattr(module, attr_name) for attr_name in dir(module)]
66
+ for attr in possible_models:
67
+ if isclass(attr) and issubclass(attr, Model) and not attr._meta.abstract:
68
+ if attr._meta.app and attr._meta.app != app_label:
69
+ continue
70
+ attr._meta.app = app_label
71
+ discovered_models.append(attr)
72
+ if not discovered_models:
73
+ warnings.warn(f'Module "{models_path}" has no models', RuntimeWarning, stacklevel=4)
74
+ return discovered_models
75
+
76
+ def init_app(
77
+ self,
78
+ label: str,
79
+ module_list: Iterable[ModuleType | str],
80
+ _init_relations: bool = True,
81
+ ) -> dict[str, type[Model]]:
82
+ app_models: list[type[Model]] = []
83
+ for module in module_list:
84
+ app_models += self._discover_models(module, label)
85
+
86
+ self.apps[label] = {model.__name__: model for model in app_models}
87
+
88
+ if _init_relations:
89
+ self._init_relations()
90
+
91
+ return self.apps[label]
92
+
93
+ def _load_from_config(self) -> None:
94
+ if self._connections is None:
95
+ raise ConfigurationError("ConnectionHandler is required to load from config")
96
+ for name, info in self._config.items():
97
+ default_connection = info.get("default_connection", "default")
98
+ if self._validate_connections:
99
+ try:
100
+ self._connections.get(default_connection)
101
+ except KeyError:
102
+ raise ConfigurationError(
103
+ f'Unknown connection "{default_connection}" for app "{name}"'
104
+ )
105
+ else:
106
+ if default_connection not in self._connections.db_config:
107
+ raise ConfigurationError(
108
+ f'Unknown connection "{default_connection}" for app "{name}"'
109
+ )
110
+
111
+ self.init_app(name, info["models"], _init_relations=False)
112
+
113
+ for model in self.apps[name].values():
114
+ model._meta.default_connection = default_connection
115
+
116
+ self._init_relations()
117
+ if self._validate_connections:
118
+ self._build_initial_querysets()
119
+
120
+ def _build_initial_querysets(self) -> None:
121
+ for app in self.apps.values():
122
+ for model in app.values():
123
+ model._meta.finalise_model()
124
+ model._meta.basetable = Table(name=model._meta.db_table, schema=model._meta.schema)
125
+ basequery = model._meta.db.query_class.from_(model._meta.basetable)
126
+ model._meta.basequery = cast(QueryBuilder, basequery)
127
+ model._meta.basequery_all_fields = cast(
128
+ QueryBuilder, basequery.select(*model._meta.db_fields)
129
+ )
130
+
131
+ def _init_relations(self) -> None:
132
+ def get_related_model(related_app_name: str, related_model_name: str) -> type[Model]:
133
+ """
134
+ Test, if app and model really exist. Throws a ConfigurationError with a hopefully
135
+ helpful message. If successful, returns the requested model.
136
+
137
+ :raises ConfigurationError: If no such app exists.
138
+ """
139
+ try:
140
+ return self.apps[related_app_name][related_model_name]
141
+ except KeyError:
142
+ if related_app_name not in self.apps:
143
+ raise ConfigurationError(
144
+ f"No app with name '{related_app_name}' registered."
145
+ f" Please check your model names in ForeignKeyFields"
146
+ f" and configurations."
147
+ )
148
+ raise ConfigurationError(
149
+ f"No model with name '{related_model_name}' registered in"
150
+ f" app '{related_app_name}'."
151
+ )
152
+
153
+ def split_reference(reference: str) -> tuple[str, str]:
154
+ """
155
+ Validate, if reference follow the official naming conventions. Throws a
156
+ ConfigurationError with a hopefully helpful message. If successful,
157
+ returns the app and the model name.
158
+
159
+ :raises ConfigurationError: If reference is invalid.
160
+ """
161
+ if len(items := reference.split(".")) != 2: # pragma: nocoverage
162
+ raise ConfigurationError(
163
+ f"'{reference}' is not a valid model reference Bad Reference."
164
+ " Should be something like '<appname>.<modelname>'."
165
+ )
166
+ return items[0], items[1]
167
+
168
+ def init_fk_o2o_field(model: type[Model], field: str, is_o2o: bool = False) -> None:
169
+ fk_object = cast(
170
+ "OneToOneFieldInstance | ForeignKeyFieldInstance", model._meta.fields_map[field]
171
+ )
172
+ reference = fk_object.model_name
173
+ if not isinstance(reference, str):
174
+ related_model: type[Model] = reference
175
+ related_model_name = related_model.__name__
176
+ else:
177
+ related_app_name, related_model_name = split_reference(reference)
178
+ related_model = get_related_model(related_app_name, related_model_name)
179
+
180
+ if to_field := fk_object.to_field:
181
+ related_field = related_model._meta.fields_map.get(to_field)
182
+ if not related_field:
183
+ raise ConfigurationError(
184
+ f'there is no field named "{to_field}" in model "{related_model_name}"'
185
+ )
186
+ if not related_field.unique:
187
+ raise ConfigurationError(
188
+ f'field "{to_field}" in model "{related_model_name}" is not unique'
189
+ )
190
+ else:
191
+ fk_object.to_field = related_model._meta.pk_attr
192
+ related_field = related_model._meta.pk
193
+ key_fk_object = copy(related_field)
194
+ fk_object.to_field_instance = related_field
195
+ fk_object.field_type = fk_object.to_field_instance.field_type
196
+
197
+ key_field = f"{field}_id"
198
+ key_fk_object.reference = fk_object
199
+ key_fk_object.source_field = fk_object.source_field or key_field
200
+ for attr in ("index", "default", "null", "generated", "description", "db_default"):
201
+ setattr(key_fk_object, attr, getattr(fk_object, attr))
202
+ if is_o2o:
203
+ key_fk_object.pk = fk_object.pk
204
+ key_fk_object.unique = fk_object.unique
205
+ else:
206
+ key_fk_object.pk = False
207
+ key_fk_object.unique = False
208
+ model._meta.add_field(key_field, key_fk_object)
209
+ fk_object.related_model = related_model
210
+ fk_object.source_field = key_field
211
+ if (backward_relation_name := fk_object.related_name) is not False:
212
+ if not backward_relation_name:
213
+ backward_relation_name = f"{model._meta.db_table}s"
214
+ if backward_relation_name in related_model._meta.fields:
215
+ raise ConfigurationError(
216
+ f'backward relation "{backward_relation_name}" duplicates in'
217
+ f" model {related_model_name}"
218
+ )
219
+
220
+ fk_relation = (
221
+ BackwardOneToOneRelation(
222
+ model,
223
+ key_field,
224
+ key_fk_object.source_field,
225
+ null=True,
226
+ description=fk_object.description,
227
+ )
228
+ if is_o2o
229
+ else BackwardFKRelation(
230
+ model,
231
+ key_field,
232
+ key_fk_object.source_field,
233
+ null=fk_object.null,
234
+ description=fk_object.description,
235
+ )
236
+ )
237
+ fk_relation.to_field_instance = fk_object.to_field_instance
238
+ if isinstance(backward_relation_name, str):
239
+ related_model._meta.add_field(backward_relation_name, fk_relation)
240
+ if is_o2o and fk_object.pk:
241
+ model._meta.pk_attr = key_field
242
+
243
+ for app_name, app in self.apps.items():
244
+ for model_name, model in app.items():
245
+ if model._meta._inited:
246
+ continue
247
+ model._meta._inited = True
248
+ if not model._meta.db_table or (
249
+ model._meta.auto_table and self._table_name_generator
250
+ ):
251
+ model._meta.db_table = (
252
+ self._table_name_generator(model)
253
+ if self._table_name_generator
254
+ else (model.__name__.lower())
255
+ )
256
+
257
+ for field in sorted(model._meta.fk_fields):
258
+ init_fk_o2o_field(model, field)
259
+
260
+ for field in model._meta.o2o_fields:
261
+ init_fk_o2o_field(model, field, is_o2o=True)
262
+
263
+ for field in list(model._meta.m2m_fields):
264
+ m2m_object = cast(ManyToManyFieldInstance, model._meta.fields_map[field])
265
+ if m2m_object._generated:
266
+ continue
267
+ if not (backward_key := m2m_object.backward_key):
268
+ backward_key = f"{model._meta.db_table}_id"
269
+ if backward_key == m2m_object.forward_key:
270
+ backward_key = f"{model._meta.db_table}_rel_id"
271
+ m2m_object.backward_key = backward_key
272
+
273
+ reference = m2m_object.model_name
274
+ if not isinstance(reference, str):
275
+ related_model = reference
276
+ related_model_name = related_model.__name__
277
+ else:
278
+ related_app_name, related_model_name = split_reference(reference)
279
+ related_model = get_related_model(related_app_name, related_model_name)
280
+
281
+ m2m_object.related_model = related_model
282
+
283
+ if not (backward_relation_name := m2m_object.related_name):
284
+ backward_relation_name = m2m_object.related_name = (
285
+ f"{model._meta.db_table}s"
286
+ )
287
+ if backward_relation_name in related_model._meta.fields:
288
+ raise ConfigurationError(
289
+ f'backward relation "{backward_relation_name}" duplicates in'
290
+ f" model {related_model_name}"
291
+ )
292
+
293
+ if not m2m_object.through:
294
+ related_model_table_name = (
295
+ related_model._meta.db_table or related_model.__name__.lower()
296
+ )
297
+ m2m_object.through = f"{model._meta.db_table}_{related_model_table_name}"
298
+
299
+ m2m_object.through_schema = model._meta.schema
300
+
301
+ m2m_relation = ManyToManyFieldInstance(
302
+ f"{app_name}.{model_name}",
303
+ m2m_object.through,
304
+ forward_key=m2m_object.backward_key,
305
+ backward_key=m2m_object.forward_key,
306
+ related_name=field,
307
+ field_type=model,
308
+ description=m2m_object.description,
309
+ )
310
+ m2m_relation._generated = True
311
+ m2m_relation.through_schema = model._meta.schema
312
+ model._meta.filters.update(get_m2m_filters(field, m2m_object))
313
+ related_model._meta.add_field(backward_relation_name, m2m_relation)
314
+
315
+ def get_model_reference(self, model: type[Model]) -> str:
316
+ return model._meta.full_name
317
+
318
+ def get_model(self, app_label: str, model_name: str) -> type[Model]:
319
+ try:
320
+ return self.apps[app_label][model_name]
321
+ except KeyError:
322
+ if app_label not in self.apps:
323
+ raise ConfigurationError(f"No app with name '{app_label}' registered.")
324
+ raise ConfigurationError(
325
+ f"No model with name '{model_name}' registered in app '{app_label}'."
326
+ )
327
+
328
+ def get_models_iterable(self) -> Iterable[type[Model]]:
329
+ model_list_generator = (
330
+ model_list for model_list in (app.values() for app in self.apps.values())
331
+ )
332
+ return chain.from_iterable(model_list_generator)
333
+
334
+ def clear(self) -> None:
335
+ self.apps.clear()
336
+
337
+ def __contains__(self, key: str) -> bool:
338
+ return key in self.apps
339
+
340
+ def __iter__(self) -> Iterator[str]:
341
+ return self.apps.__iter__()
342
+
343
+ def values(self) -> Iterable[dict[str, type[Model]]]:
344
+ return self.apps.values()
345
+
346
+ def items(self) -> Iterable[tuple[str, dict[str, type[Model]]]]:
347
+ return self.apps.items()
348
+
349
+ def keys(self) -> Iterable[str]:
350
+ return self.apps.keys()
351
+
352
+ def __getitem__(self, key: str) -> dict[str, type[Model]]:
353
+ return self.apps[key]
354
+
355
+ def __setitem__(self, key: str, value: dict[str, type[Model]]) -> None:
356
+ self.apps[key] = value
@@ -0,0 +1,4 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
@@ -0,0 +1,7 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from .client import AsyncpgDBClient
6
+
7
+ client_class = AsyncpgDBClient
@@ -0,0 +1,223 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ from collections.abc import Awaitable, Callable
9
+ from typing import Any, TypeVar, cast
10
+
11
+ import asyncpg
12
+ from asyncpg.transaction import Transaction
13
+
14
+ from rakko.backends.asyncpg.executor import AsyncpgExecutor
15
+ from rakko.backends.asyncpg.schema_generator import AsyncpgSchemaGenerator
16
+ from rakko.backends.base.client import (
17
+ ConnectionWrapper,
18
+ NestedTransactionContext,
19
+ TransactionalDBClient,
20
+ TransactionContext,
21
+ TransactionContextPooled,
22
+ )
23
+ from rakko.backends.base_postgres.client import (
24
+ BasePostgresClient,
25
+ translate_exceptions,
26
+ )
27
+ from rakko.exceptions import (
28
+ DBConnectionError,
29
+ IntegrityError,
30
+ OperationalError,
31
+ TransactionManagementError,
32
+ )
33
+
34
+ FuncType = Callable[..., Any]
35
+ F = TypeVar("F", bound=FuncType)
36
+
37
+
38
+ class AsyncpgDBClient(BasePostgresClient):
39
+ executor_class = AsyncpgExecutor
40
+ schema_generator = AsyncpgSchemaGenerator
41
+ connection_class = asyncpg.connection.Connection
42
+ _pool: asyncpg.Pool | None
43
+ _connection: asyncpg.connection.Connection | None = None
44
+
45
+ async def create_connection(self, with_db: bool) -> None:
46
+ if self.schema:
47
+ self.server_settings["search_path"] = self.schema
48
+
49
+ if self.application_name:
50
+ self.server_settings["application_name"] = self.application_name
51
+
52
+ self._template = {
53
+ "host": self.host,
54
+ "port": self.port,
55
+ "user": self.user,
56
+ "database": self.database if with_db else None,
57
+ "min_size": self.pool_minsize,
58
+ "max_size": self.pool_maxsize,
59
+ "connection_class": self.connection_class,
60
+ "loop": self.loop,
61
+ "server_settings": self.server_settings,
62
+ **self.extra,
63
+ }
64
+ try:
65
+ # asyncpg resolves a callable password itself, once per new connection
66
+ self._pool = await self.create_pool(password=self.password, **self._template)
67
+ await self._post_connect()
68
+ self.log.debug("Created connection pool %s with params: %s", self._pool, self._template)
69
+ except asyncpg.InvalidCatalogNameError as ex:
70
+ msg = "Can't establish connection to "
71
+ if with_db:
72
+ msg += f"database {self.database}"
73
+ else:
74
+ msg += f"default database. Verify environment PGDATABASE. Exception: {ex}"
75
+ raise DBConnectionError(msg) from ex
76
+
77
+ async def create_pool(self, **kwargs) -> asyncpg.Pool:
78
+ return await asyncpg.create_pool(None, **kwargs)
79
+
80
+ async def _expire_connections(self) -> None:
81
+ if self._pool: # pragma: nobranch
82
+ await self._pool.expire_connections()
83
+
84
+ async def _close(self) -> None:
85
+ if self._pool: # pragma: nobranch
86
+ try:
87
+ await asyncio.wait_for(self._pool.close(), 10)
88
+ except asyncio.TimeoutError: # pragma: nocoverage
89
+ self._pool.terminate()
90
+ self._pool = None
91
+ self.log.debug("Closed connection pool %s with params: %s", self._pool, self._template)
92
+
93
+ async def _translate_exceptions[R](
94
+ self, func: Callable[..., Awaitable[R]], *args: Any, **kwargs: Any
95
+ ) -> R:
96
+ try:
97
+ return await func(self, *args, **kwargs)
98
+ except (asyncpg.SyntaxOrAccessError, asyncpg.exceptions.DataError) as exc:
99
+ raise OperationalError(exc) from exc
100
+ except asyncpg.IntegrityConstraintViolationError as exc:
101
+ raise IntegrityError(exc) from exc
102
+ except asyncpg.InvalidTransactionStateError as exc: # pragma: nocoverage
103
+ raise TransactionManagementError(exc) from exc
104
+
105
+ async def db_delete(self) -> None:
106
+ try:
107
+ return await super().db_delete()
108
+ except asyncpg.InvalidCatalogNameError: # pragma: nocoverage
109
+ pass
110
+ await self.close()
111
+
112
+ def _in_transaction(self) -> TransactionContext:
113
+ return TransactionContextPooled(TransactionWrapper(self), self._pool_init_lock)
114
+
115
+ @translate_exceptions
116
+ async def execute_insert(self, query: str, values: list) -> asyncpg.Record | None:
117
+ async with self.acquire_connection() as connection:
118
+ self.log.debug("%s: %s", query, values)
119
+ # TODO: Cache prepared statement
120
+ return await connection.fetchrow(query, *values)
121
+
122
+ @translate_exceptions
123
+ async def execute_many(self, query: str, values: list) -> None:
124
+ async with self.acquire_connection() as connection:
125
+ self.log.debug("%s: %s", query, values)
126
+ # TODO: Consider using copy_records_to_table instead
127
+ transaction = connection.transaction()
128
+ await transaction.start()
129
+ try:
130
+ await connection.executemany(query, values)
131
+ except Exception:
132
+ await transaction.rollback()
133
+ raise
134
+ else:
135
+ await transaction.commit()
136
+
137
+ @translate_exceptions
138
+ async def execute_query(self, query: str, values: list | None = None) -> tuple[int, list[dict]]:
139
+ async with self.acquire_connection() as connection:
140
+ self.log.debug("%s: %s", query, values)
141
+ params = [query, *values] if values else [query]
142
+ normalized = query.lstrip().upper()
143
+ if normalized.startswith(("UPDATE", "DELETE", "INSERT")):
144
+ res = await connection.execute(*params)
145
+ try:
146
+ rows_affected = int(res.split(" ")[1])
147
+ except (AttributeError, IndexError, ValueError): # pragma: nocoverage
148
+ rows_affected = 0
149
+ return rows_affected, []
150
+
151
+ rows = await connection.fetch(*params)
152
+ return len(rows), rows
153
+
154
+ @translate_exceptions
155
+ async def execute_query_dict(self, query: str, values: list | None = None) -> list[dict]:
156
+ async with self.acquire_connection() as connection:
157
+ self.log.debug("%s: %s", query, values)
158
+ params = [query, *values] if values else [query]
159
+ results = await connection.fetch(*params)
160
+ return [dict(i) for i in results]
161
+
162
+
163
+ class TransactionWrapper(AsyncpgDBClient, TransactionalDBClient):
164
+ """A transactional connection wrapper for asyncpg.
165
+
166
+ asyncpg implements nested transactions (savepoints) natively, so we don't need to.
167
+ """
168
+
169
+ _connection: asyncpg.Connection # Put it here to improve type hints
170
+
171
+ def __init__(self, connection: AsyncpgDBClient) -> None:
172
+ self._connection = cast(asyncpg.Connection, connection._connection) # type: ignore[assignment]
173
+ self._lock = asyncio.Lock()
174
+ self.log = connection.log
175
+ self.connection_name = connection.connection_name
176
+ self.transaction: Transaction | None = None
177
+ self._finalized = False
178
+ self._parent: AsyncpgDBClient = connection # type: ignore[assignment]
179
+
180
+ def _in_transaction(self) -> TransactionContext:
181
+ # since we need to store the transaction object for each transaction block,
182
+ # we need to wrap the connection with its own TransactionWrapper
183
+ return NestedTransactionContext(TransactionWrapper(self))
184
+
185
+ def acquire_connection(self) -> ConnectionWrapper[asyncpg.Connection]:
186
+ return ConnectionWrapper(self._lock, self)
187
+
188
+ @translate_exceptions
189
+ async def execute_many(self, query: str, values: list) -> None:
190
+ async with self.acquire_connection() as connection:
191
+ self.log.debug("%s: %s", query, values)
192
+ # TODO: Consider using copy_records_to_table instead
193
+ await connection.executemany(query, values)
194
+
195
+ @translate_exceptions
196
+ async def begin(self) -> None:
197
+ self.transaction = self._connection.transaction()
198
+ await self.transaction.start()
199
+
200
+ async def savepoint(self) -> None:
201
+ return await self.begin()
202
+
203
+ async def commit(self) -> None:
204
+ if not self.transaction:
205
+ raise TransactionManagementError("Transaction is in invalid state")
206
+ if self._finalized:
207
+ raise TransactionManagementError("Transaction already finalised")
208
+ await self.transaction.commit()
209
+ self._finalized = True
210
+
211
+ async def release_savepoint(self) -> None:
212
+ return await self.commit()
213
+
214
+ async def rollback(self) -> None:
215
+ if not self.transaction:
216
+ raise TransactionManagementError("Transaction is in invalid state")
217
+ if self._finalized:
218
+ raise TransactionManagementError("Transaction already finalised")
219
+ await self.transaction.rollback()
220
+ self._finalized = True
221
+
222
+ async def savepoint_rollback(self) -> None:
223
+ await self.rollback()
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Mapping
8
+ from typing import Any, cast
9
+
10
+ import asyncpg
11
+
12
+ from rakko import Model
13
+ from rakko.backends.base_postgres.executor import BasePostgresExecutor
14
+
15
+
16
+ class AsyncpgExecutor(BasePostgresExecutor):
17
+ async def _process_insert_result(
18
+ self, instance: Model, results: asyncpg.Record | Mapping[str, Any] | None
19
+ ) -> None:
20
+ return await super()._process_insert_result(instance, cast(Mapping[str, Any] | None, results))
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rakko.backends.base_postgres.schema_generator import BasePostgresSchemaGenerator
10
+
11
+ if TYPE_CHECKING: # pragma: nocoverage
12
+ from rakko.backends.asyncpg.client import AsyncpgDBClient
13
+
14
+
15
+ class AsyncpgSchemaGenerator(BasePostgresSchemaGenerator):
16
+ def __init__(self, client: AsyncpgDBClient) -> None:
17
+ super().__init__(client)
@@ -0,0 +1,4 @@
1
+ # Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
2
+ # Derived from Tortoise ORM; maintained and modified for Rakko ORM.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+