pyoq-sql 1.0.2__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 (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,79 @@
1
+ """MySQL compilation capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pyoq.errors import QueryValidationError
8
+ from pyoq.query.execution import ParameterStyle
9
+ from pyoq.relations.planning import FetchCapabilities
10
+
11
+ _PLACEHOLDER_LIMIT = 65535
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class MySQLCapabilities:
16
+ maximum_parameters: int = _PLACEHOLDER_LIMIT
17
+ parameter_style: ParameterStyle = ParameterStyle.FORMAT
18
+ right_join: bool = True
19
+ full_join: bool = False
20
+ lateral_sources: bool = True
21
+ recursive_common_tables: bool = True
22
+ raw_expressions: bool = True
23
+ # MySQL refuses ORDER BY inside JSON_ARRAYAGG, and ordering in a
24
+ # derived table is discarded rather than honoured.
25
+ nested_collections: bool = True
26
+ ordered_nested_collections: bool = False
27
+ returning: bool = False
28
+ upsert: bool = True
29
+
30
+ @property
31
+ def fetch(self) -> FetchCapabilities:
32
+ """What this dialect can do when fetching a collection in one query."""
33
+ return FetchCapabilities(
34
+ self.nested_collections,
35
+ self.ordered_nested_collections,
36
+ )
37
+
38
+ def __post_init__(self) -> None:
39
+ if not _is_positive_integer(self.maximum_parameters):
40
+ message = "MySQL maximum parameters must be a positive integer"
41
+ raise QueryValidationError(message)
42
+ if self.maximum_parameters > _PLACEHOLDER_LIMIT:
43
+ message = (
44
+ "MySQL cannot bind more than "
45
+ f"{_PLACEHOLDER_LIMIT} placeholders in one statement"
46
+ )
47
+ raise QueryValidationError(message)
48
+ if not _is_parameter_style(self.parameter_style):
49
+ message = "MySQL parameter style must be a ParameterStyle value"
50
+ raise QueryValidationError(message)
51
+ feature_flags: tuple[object, ...] = (
52
+ self.right_join,
53
+ self.full_join,
54
+ self.lateral_sources,
55
+ self.recursive_common_tables,
56
+ self.raw_expressions,
57
+ self.nested_collections,
58
+ self.ordered_nested_collections,
59
+ self.returning,
60
+ self.upsert,
61
+ )
62
+ if not _all_boolean(feature_flags):
63
+ message = "MySQL capability flags must be boolean"
64
+ raise QueryValidationError(message)
65
+
66
+
67
+ def _is_parameter_style(value: object) -> bool:
68
+ return isinstance(value, ParameterStyle)
69
+
70
+
71
+ def _is_positive_integer(value: object) -> bool:
72
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
73
+
74
+
75
+ def _all_boolean(values: tuple[object, ...]) -> bool:
76
+ return all(isinstance(value, bool) for value in values)
77
+
78
+
79
+ __all__ = ("MySQLCapabilities",)
@@ -0,0 +1,43 @@
1
+ """MySQL typed result operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ from pymysql.cursors import Cursor
8
+
9
+ from pyoq.query.execution import (
10
+ BulkPlan,
11
+ DatabaseCursor,
12
+ QueryOperations,
13
+ WriteProvider,
14
+ )
15
+ from pyoq.query.mysql.compiler import MySQLCompiler
16
+ from pyoq.query.mysql.planning import MySQLBulkPlanner
17
+ from pyoq.query.write_nodes import WriteNode
18
+
19
+
20
+ class MySQLQueryOperations(QueryOperations):
21
+ __slots__ = ("_compiler",)
22
+
23
+ def __init__(self, compiler: MySQLCompiler) -> None:
24
+ self._compiler = compiler
25
+
26
+ @property
27
+ def compiler(self) -> MySQLCompiler:
28
+ return self._compiler
29
+
30
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
31
+ return MySQLBulkPlanner(self.compiler).plan(statement)
32
+
33
+ def last_inserted_id(self, cursor: DatabaseCursor) -> int | None:
34
+ """Report the first identifier a multi-row insert generated.
35
+
36
+ MySQL reports the identifier of the first row an insert generated, not
37
+ the last, and reports zero when a statement generated none.
38
+ """
39
+ identifier = cast("Cursor", cursor).lastrowid
40
+ return identifier if identifier > 0 else None
41
+
42
+
43
+ __all__ = ("MySQLQueryOperations",)
@@ -0,0 +1,69 @@
1
+ """MySQL driver parameter adaptation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import date, datetime, time, timedelta
7
+ from decimal import Decimal
8
+ from enum import Enum
9
+ from typing import cast
10
+ from uuid import UUID
11
+
12
+ from pyoq.errors import ParameterBindingError
13
+
14
+ MySQLParameter = (
15
+ str
16
+ | int
17
+ | float
18
+ | bool
19
+ | bytes
20
+ | Decimal
21
+ | date
22
+ | datetime
23
+ | time
24
+ | timedelta
25
+ | None
26
+ )
27
+
28
+ _NATIVE_TYPES = (
29
+ str,
30
+ int,
31
+ float,
32
+ bool,
33
+ bytes,
34
+ Decimal,
35
+ datetime,
36
+ date,
37
+ time,
38
+ timedelta,
39
+ )
40
+
41
+
42
+ def adapt_parameters(values: tuple[object, ...]) -> tuple[MySQLParameter, ...]:
43
+ return tuple(adapt_parameter(value) for value in values)
44
+
45
+
46
+ def adapt_parameter(value: object) -> MySQLParameter:
47
+ if isinstance(value, Enum):
48
+ return adapt_parameter(value.value)
49
+ if value is None or isinstance(value, _NATIVE_TYPES):
50
+ return value
51
+ if isinstance(value, UUID):
52
+ return str(value)
53
+ if isinstance(value, memoryview):
54
+ return value.tobytes()
55
+ if isinstance(value, (list, tuple, dict)):
56
+ return _json_parameter(cast("object", value))
57
+ message = f"unsupported MySQL parameter type: {type(value).__name__}"
58
+ raise ParameterBindingError(message)
59
+
60
+
61
+ def _json_parameter(value: object) -> str:
62
+ try:
63
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
64
+ except (TypeError, ValueError) as error:
65
+ message = "MySQL JSON parameter is not serializable"
66
+ raise ParameterBindingError(message) from error
67
+
68
+
69
+ __all__ = ("MySQLParameter", "adapt_parameter", "adapt_parameters")
@@ -0,0 +1,20 @@
1
+ """MySQL bulk write planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.execution.planning import BulkPlanner
6
+ from pyoq.query.mysql.compiler import MySQLCompiler
7
+
8
+
9
+ class MySQLBulkPlanner(BulkPlanner):
10
+ __slots__ = ()
11
+
12
+ def __init__(self, compiler: MySQLCompiler) -> None:
13
+ super().__init__(
14
+ compiler,
15
+ compiler.capabilities.maximum_parameters,
16
+ "MySQL",
17
+ )
18
+
19
+
20
+ __all__ = ("MySQLBulkPlanner",)
@@ -0,0 +1,67 @@
1
+ """Exclusive synchronous MySQL connection pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import TypeAlias, cast
7
+
8
+ import pymysql
9
+ import pymysql.connections
10
+
11
+ from pyoq.errors import ConnectionPoolError
12
+ from pyoq.query.execution.pooling import (
13
+ ConnectionLease,
14
+ ConnectionPool,
15
+ PoolPolicy,
16
+ PoolStats,
17
+ )
18
+ from pyoq.query.mysql.commands import Connection
19
+
20
+ MySQLConnectionFactoryProtocol: TypeAlias = "Callable[[], Connection]"
21
+ MySQLConnectionLease: TypeAlias = ConnectionLease[Connection]
22
+ MySQLPoolPolicy: TypeAlias = PoolPolicy
23
+ MySQLPoolStats: TypeAlias = PoolStats
24
+
25
+ _RESET_TIMEOUT = "SET @@max_execution_time = 0"
26
+
27
+
28
+ class MySQLPool(ConnectionPool[Connection]):
29
+ __slots__ = ()
30
+
31
+ @property
32
+ def connection_factory(self) -> MySQLConnectionFactoryProtocol:
33
+ return self._factory
34
+
35
+ def reset(self, connection: Connection) -> bool:
36
+ try:
37
+ connection.rollback()
38
+ with connection.cursor() as cursor:
39
+ cursor.execute(_RESET_TIMEOUT, ())
40
+ except pymysql.Error:
41
+ return False
42
+ return connection.open
43
+
44
+ def validate(self, value: object) -> Connection:
45
+ if not isinstance(value, pymysql.connections.Connection):
46
+ message = "MySQL connection factory returned an invalid connection"
47
+ raise ConnectionPoolError(message)
48
+ return cast("Connection", value)
49
+
50
+ def dispose(self, connection: Connection) -> None:
51
+ """Close a connection the driver has not already closed itself.
52
+
53
+ PyMySQL refuses a second close, and it force-closes the socket itself
54
+ after a fatal error, so an open socket is the only thing left to send a
55
+ quit message on.
56
+ """
57
+ if connection.open:
58
+ connection.close()
59
+
60
+
61
+ __all__ = (
62
+ "MySQLConnectionFactoryProtocol",
63
+ "MySQLConnectionLease",
64
+ "MySQLPool",
65
+ "MySQLPoolPolicy",
66
+ "MySQLPoolStats",
67
+ )
@@ -0,0 +1,331 @@
1
+ """Owned MySQL transactions and nested savepoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from enum import Enum
7
+ from typing import TypeVar, cast
8
+
9
+ import pymysql
10
+ from pymysql.cursors import Cursor
11
+
12
+ from pyoq.errors import ExecutionError, QueryValidationError, TransactionStateError
13
+ from pyoq.query.execution import (
14
+ CompiledQuery,
15
+ ExecutionControl,
16
+ RowStream,
17
+ StatementKind,
18
+ StreamPolicy,
19
+ StreamResource,
20
+ TransactionState,
21
+ )
22
+ from pyoq.query.mysql.commands import (
23
+ Connection,
24
+ ExecutionEnvironment,
25
+ FailureHandler,
26
+ open_cursor,
27
+ run_sql,
28
+ )
29
+ from pyoq.query.mysql.compiler import MySQLCompiler
30
+ from pyoq.query.mysql.operations import MySQLQueryOperations
31
+ from pyoq.query.mysql.parameters import adapt_parameters
32
+ from pyoq.query.mysql.pool import MySQLPool
33
+ from pyoq.query.select import Query
34
+
35
+ OperationResult = TypeVar("OperationResult")
36
+ ResultRow = TypeVar("ResultRow")
37
+
38
+ _ISOLATION_STATEMENTS = {
39
+ "READ_COMMITTED": "SET TRANSACTION ISOLATION LEVEL READ COMMITTED",
40
+ "REPEATABLE_READ": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
41
+ "SERIALIZABLE": "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE",
42
+ "READ_ONLY": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
43
+ }
44
+ _READ_ONLY_START = "START TRANSACTION READ ONLY"
45
+ _START = "START TRANSACTION"
46
+
47
+
48
+ class TransactionMode(Enum):
49
+ """MySQL sets isolation before starting, not as part of starting."""
50
+
51
+ READ_COMMITTED = "READ_COMMITTED"
52
+ REPEATABLE_READ = "REPEATABLE_READ"
53
+ SERIALIZABLE = "SERIALIZABLE"
54
+ READ_ONLY = "READ_ONLY"
55
+
56
+ @property
57
+ def isolation_sql(self) -> str:
58
+ return _ISOLATION_STATEMENTS[self.value]
59
+
60
+ @property
61
+ def start_sql(self) -> str:
62
+ return _READ_ONLY_START if self is TransactionMode.READ_ONLY else _START
63
+
64
+
65
+ class MySQLTransactionState(TransactionState[Connection, "MySQLTransaction"]):
66
+ """Adds the one transaction fact MySQL reports that other dialects do not.
67
+
68
+ A deadlock rolls the whole transaction back on the server while leaving the
69
+ connection healthy, and a later ``COMMIT`` then succeeds having committed
70
+ nothing.
71
+ """
72
+
73
+ __slots__ = ("server_rolled_back",)
74
+
75
+ def __init__(self) -> None:
76
+ super().__init__()
77
+ self.server_rolled_back = False
78
+
79
+ def mark_server_rollback(self) -> None:
80
+ self.server_rolled_back = True
81
+
82
+
83
+ class MySQLTransaction(MySQLQueryOperations):
84
+ __slots__ = (
85
+ "_control",
86
+ "_entered",
87
+ "_finished",
88
+ "_mode",
89
+ "_parent",
90
+ "_pool",
91
+ "_savepoint_name",
92
+ "_state",
93
+ )
94
+
95
+ def __init__(
96
+ self,
97
+ pool: MySQLPool,
98
+ compiler: MySQLCompiler,
99
+ mode: TransactionMode = TransactionMode.REPEATABLE_READ,
100
+ control: ExecutionControl | None = None,
101
+ *,
102
+ parent: MySQLTransaction | None = None,
103
+ state: MySQLTransactionState | None = None,
104
+ ) -> None:
105
+ super().__init__(compiler)
106
+ if not _is_transaction_mode(mode):
107
+ raise QueryValidationError("MySQL transaction mode is invalid")
108
+ self._pool = pool
109
+ self._mode = mode
110
+ self._control = control
111
+ self._parent = parent
112
+ self._state = state or MySQLTransactionState()
113
+ self._savepoint_name: str | None = None
114
+ self._entered = False
115
+ self._finished = False
116
+
117
+ def __enter__(self) -> MySQLTransaction:
118
+ if self._entered or self._finished:
119
+ raise TransactionStateError("transaction cannot be entered twice")
120
+ if self._parent is None:
121
+ self._enter_outer()
122
+ else:
123
+ self._enter_savepoint()
124
+ self._entered = True
125
+ return self
126
+
127
+ def __exit__(
128
+ self,
129
+ exception_type: type[BaseException] | None,
130
+ exception: BaseException | None,
131
+ traceback: object,
132
+ ) -> None:
133
+ if not self._entered or self._finished:
134
+ return
135
+ if self._parent is None:
136
+ self._exit_outer(exception_type)
137
+ else:
138
+ self._exit_savepoint(exception_type)
139
+
140
+ def savepoint(
141
+ self,
142
+ *,
143
+ control: ExecutionControl | None = None,
144
+ ) -> MySQLTransaction:
145
+ self._state.require_current(self)
146
+ return MySQLTransaction(
147
+ self._pool,
148
+ self.compiler,
149
+ self._mode,
150
+ control or self._control,
151
+ parent=self,
152
+ state=self._state,
153
+ )
154
+
155
+ def stream(
156
+ self,
157
+ query: Query[ResultRow],
158
+ /,
159
+ *,
160
+ policy: StreamPolicy | None = None,
161
+ ) -> RowStream[ResultRow]:
162
+ self._state.require_current(self)
163
+ selected = policy or StreamPolicy(control=self._control)
164
+ if selected.control is None and self._control is not None:
165
+ selected = StreamPolicy(selected.batch_size, self._control)
166
+ statement = self.compiler.compile(query)
167
+ stream: RowStream[ResultRow]
168
+ stream = RowStream(
169
+ lambda: self._open_stream(stream, statement, selected),
170
+ selected,
171
+ statement.row_types,
172
+ )
173
+ return stream
174
+
175
+ def _environment(self, connection: Connection) -> ExecutionEnvironment:
176
+ return ExecutionEnvironment(
177
+ connection,
178
+ self._pool.connection_factory,
179
+ FailureHandler(self._state.invalidate, self._state.mark_server_rollback),
180
+ )
181
+
182
+ def _run(
183
+ self,
184
+ statement: CompiledQuery,
185
+ operation: Callable[[Cursor], OperationResult],
186
+ control: ExecutionControl | None,
187
+ ) -> OperationResult:
188
+ parameters = adapt_parameters(statement.parameters)
189
+ connection = self._state.require_current(self)
190
+ return run_sql(
191
+ self._environment(connection),
192
+ statement.sql,
193
+ parameters,
194
+ operation,
195
+ control or self._control,
196
+ one_result_set=statement.statement_kind is StatementKind.CALL,
197
+ )
198
+
199
+ def _enter_outer(self) -> None:
200
+ self._state.adopt(self._pool.checkout(), self)
201
+ try:
202
+ self._run_transaction_sql(self._mode.isolation_sql)
203
+ self._run_transaction_sql(self._mode.start_sql)
204
+ except BaseException:
205
+ self._release_outer()
206
+ raise
207
+
208
+ def _enter_savepoint(self) -> None:
209
+ self._state.require_current(cast("MySQLTransaction", self._parent))
210
+ name = self._state.next_savepoint()
211
+ self._run_transaction_sql(f"SAVEPOINT `{name}`", scope=self._parent)
212
+ self._savepoint_name = name
213
+ self._state.current_scope = self
214
+
215
+ def _exit_outer(self, exception_type: type[BaseException] | None) -> None:
216
+ try:
217
+ self._state.require_scope(self)
218
+ except TransactionStateError:
219
+ self._abort_misnested_outer(exception_type)
220
+ return
221
+ self._state.close_stream()
222
+ try:
223
+ self._finish_outer(exception_type)
224
+ except ExecutionError:
225
+ self._state.invalidate()
226
+ if exception_type is None:
227
+ raise
228
+ finally:
229
+ self._release_outer()
230
+
231
+ def _finish_outer(self, exception_type: type[BaseException] | None) -> None:
232
+ if self._state.server_rolled_back:
233
+ if exception_type is None:
234
+ message = (
235
+ "MySQL rolled this transaction back on the server; its work "
236
+ "was discarded and it cannot be committed"
237
+ )
238
+ raise TransactionStateError(message)
239
+ return
240
+ if self._state.broken:
241
+ return
242
+ self._run_transaction_sql("ROLLBACK" if exception_type else "COMMIT")
243
+
244
+ def _exit_savepoint(self, exception_type: type[BaseException] | None) -> None:
245
+ self._state.require_scope(self)
246
+ self._state.close_stream()
247
+ name = cast("str", self._savepoint_name)
248
+ try:
249
+ self._finish_savepoint(name, exception_type)
250
+ except ExecutionError:
251
+ self._state.invalidate()
252
+ if exception_type is None:
253
+ raise
254
+ finally:
255
+ self._state.current_scope = self._parent
256
+ self._finished = True
257
+
258
+ def _finish_savepoint(
259
+ self,
260
+ name: str,
261
+ exception_type: type[BaseException] | None,
262
+ ) -> None:
263
+ if self._state.broken or self._state.server_rolled_back:
264
+ return
265
+ if exception_type is not None:
266
+ self._run_transaction_sql(f"ROLLBACK TO SAVEPOINT `{name}`")
267
+ self._run_transaction_sql(f"RELEASE SAVEPOINT `{name}`")
268
+
269
+ def _run_transaction_sql(
270
+ self,
271
+ sql: str,
272
+ *,
273
+ scope: MySQLTransaction | None = None,
274
+ ) -> None:
275
+ connection = self._state.require_current(scope or self, allow_stream=True)
276
+ run_sql(
277
+ self._environment(connection),
278
+ sql,
279
+ (),
280
+ _discard_cursor,
281
+ self._control,
282
+ )
283
+
284
+ def _open_stream(
285
+ self,
286
+ stream: RowStream[ResultRow],
287
+ statement: CompiledQuery,
288
+ policy: StreamPolicy,
289
+ ) -> StreamResource:
290
+ parameters = adapt_parameters(statement.parameters)
291
+ connection = self._state.require_current(self)
292
+ session = open_cursor(
293
+ self._environment(connection),
294
+ statement.sql,
295
+ parameters,
296
+ policy.control,
297
+ unbuffered=True,
298
+ )
299
+ self._state.active_stream = cast("RowStream[object]", stream)
300
+ return StreamResource(session, self._state.release_stream)
301
+
302
+ def _release_outer(self) -> None:
303
+ self._finished = True
304
+ self._state.release()
305
+
306
+ def _abort_misnested_outer(
307
+ self,
308
+ exception_type: type[BaseException] | None,
309
+ ) -> None:
310
+ connection = self._state.require_owner()
311
+ self._state.close_stream()
312
+ try:
313
+ connection.rollback()
314
+ except pymysql.Error:
315
+ self._state.invalidate()
316
+ finally:
317
+ self._release_outer()
318
+ if exception_type is None:
319
+ message = "transaction exited with an active nested savepoint"
320
+ raise TransactionStateError(message)
321
+
322
+
323
+ def _is_transaction_mode(value: object) -> bool:
324
+ return isinstance(value, TransactionMode)
325
+
326
+
327
+ def _discard_cursor(cursor: Cursor) -> None:
328
+ del cursor
329
+
330
+
331
+ __all__ = ("MySQLTransaction", "MySQLTransactionState", "TransactionMode")
@@ -0,0 +1,73 @@
1
+ """MySQL write statement compilation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.errors import CompilationError, UnsupportedQueryError
6
+ from pyoq.query.mysql.context import MySQLCompilationContext
7
+ from pyoq.query.mysql.expressions import MySQLExpressionCompiler
8
+ from pyoq.query.mysql.identifiers import QUOTING
9
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
10
+ from pyoq.query.rendering.writes import WriteRenderer
11
+ from pyoq.query.write_nodes import ConflictAction, ConflictNode, InsertNode
12
+
13
+ ROW_ALIAS = "new"
14
+
15
+
16
+ class MySQLWriteCompilation(WriteRenderer):
17
+ __slots__ = ("_context",)
18
+
19
+ def __init__(self, context: MySQLCompilationContext) -> None:
20
+ self._context = context
21
+ super().__init__(MySQLExpressionCompiler(context))
22
+
23
+ @property
24
+ def quoting(self) -> IdentifierQuoting:
25
+ return QUOTING
26
+
27
+ def insert_row_alias(self, node: InsertNode) -> str | None:
28
+ if node.conflict is None:
29
+ return None
30
+ return ROW_ALIAS
31
+
32
+ def render_conflict(self, node: ConflictNode) -> str:
33
+ self._require_conflict_support(node)
34
+ assignments = node.assignments if node.action is ConflictAction.UPDATE else ()
35
+ if not assignments:
36
+ message = "MySQL conflict resolution requires an assignment"
37
+ raise CompilationError(message)
38
+ rendered = ", ".join(
39
+ self.compile_assignment(assignment) for assignment in assignments
40
+ )
41
+ return f"ON DUPLICATE KEY UPDATE {rendered}"
42
+
43
+ def require_returning(self) -> None:
44
+ message = (
45
+ "MySQL does not support RETURNING; read the written rows with a "
46
+ "separate query"
47
+ )
48
+ raise UnsupportedQueryError(message)
49
+
50
+ def require_conflict_resolution(self) -> None:
51
+ if not self._context.capabilities.upsert:
52
+ message = "MySQL conflict resolution is disabled by compiler capabilities"
53
+ raise UnsupportedQueryError(message)
54
+
55
+ def _require_conflict_support(self, node: ConflictNode) -> None:
56
+ self.require_conflict_resolution()
57
+ if node.targets:
58
+ message = (
59
+ "MySQL infers the conflicting key and cannot accept a conflict target"
60
+ )
61
+ raise UnsupportedQueryError(message)
62
+ if node.condition is not None:
63
+ message = "MySQL conflict resolution cannot filter with a condition"
64
+ raise UnsupportedQueryError(message)
65
+ if node.action is ConflictAction.NOTHING:
66
+ message = (
67
+ "MySQL has no safe do-nothing conflict resolution; assign the "
68
+ "conflicting columns explicitly"
69
+ )
70
+ raise UnsupportedQueryError(message)
71
+
72
+
73
+ __all__ = ("ROW_ALIAS", "MySQLWriteCompilation")