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,32 @@
1
+ """SQLite runtime connection creation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from pyoq.errors import ConnectionPoolError
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class SQLiteConnectionFactory:
14
+ database: str | Path
15
+ uri: bool = False
16
+
17
+ def __call__(self) -> sqlite3.Connection:
18
+ try:
19
+ connection = sqlite3.connect(
20
+ str(self.database),
21
+ uri=self.uri,
22
+ check_same_thread=False,
23
+ isolation_level=None,
24
+ )
25
+ except sqlite3.Error as error:
26
+ message = f"unable to open SQLite connection: {error}"
27
+ raise ConnectionPoolError(message) from error
28
+ connection.row_factory = None
29
+ return connection
30
+
31
+
32
+ __all__ = ("SQLiteConnectionFactory",)
@@ -0,0 +1,28 @@
1
+ """SQLite connection failure classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+
7
+ _FATAL_PRIMARY_CODES = frozenset(
8
+ {
9
+ sqlite3.SQLITE_CANTOPEN,
10
+ sqlite3.SQLITE_CORRUPT,
11
+ sqlite3.SQLITE_IOERR,
12
+ sqlite3.SQLITE_NOTADB,
13
+ }
14
+ )
15
+
16
+
17
+ def is_connection_failure(error: sqlite3.Error) -> bool:
18
+ if isinstance(error, sqlite3.InterfaceError):
19
+ return True
20
+ if isinstance(error, sqlite3.ProgrammingError):
21
+ return "closed" in str(error).lower()
22
+ error_code = getattr(error, "sqlite_errorcode", None)
23
+ if not isinstance(error_code, int):
24
+ return False
25
+ return error_code & 0xFF in _FATAL_PRIMARY_CODES
26
+
27
+
28
+ __all__ = ("is_connection_failure",)
@@ -0,0 +1,9 @@
1
+ """SQLite identifier rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
6
+
7
+ QUOTING = IdentifierQuoting("SQLite")
8
+
9
+ __all__ = ("QUOTING",)
@@ -0,0 +1,73 @@
1
+ """SQLite compilation capabilities and immutable output."""
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 CompiledQuery, ParameterStyle
9
+ from pyoq.relations.planning import FetchCapabilities
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class SQLiteCapabilities:
14
+ maximum_parameters: int = 999
15
+ parameter_style: ParameterStyle = ParameterStyle.QMARK
16
+ right_join: bool = False
17
+ full_join: bool = False
18
+ null_ordering: bool = True
19
+ lateral_sources: bool = False
20
+ recursive_common_tables: bool = True
21
+ raw_expressions: bool = True
22
+ # Ordering inside an aggregate needs SQLite 3.44 or later.
23
+ nested_collections: bool = True
24
+ ordered_nested_collections: bool = True
25
+ window_functions: bool = True
26
+ returning: bool = True
27
+ upsert: bool = True
28
+
29
+ @property
30
+ def fetch(self) -> FetchCapabilities:
31
+ """What this dialect can do when fetching a collection in one query."""
32
+ return FetchCapabilities(
33
+ self.nested_collections,
34
+ self.ordered_nested_collections,
35
+ )
36
+
37
+ def __post_init__(self) -> None:
38
+ if not _is_positive_integer(self.maximum_parameters):
39
+ message = "SQLite maximum parameters must be a positive integer"
40
+ raise QueryValidationError(message)
41
+ if not _is_parameter_style(self.parameter_style):
42
+ message = "SQLite parameter style must be a ParameterStyle value"
43
+ raise QueryValidationError(message)
44
+ feature_flags: tuple[object, ...] = (
45
+ self.right_join,
46
+ self.full_join,
47
+ self.null_ordering,
48
+ self.lateral_sources,
49
+ self.recursive_common_tables,
50
+ self.raw_expressions,
51
+ self.nested_collections,
52
+ self.ordered_nested_collections,
53
+ self.returning,
54
+ self.upsert,
55
+ )
56
+ if not _all_boolean(feature_flags):
57
+ message = "SQLite capability flags must be boolean"
58
+ raise QueryValidationError(message)
59
+
60
+
61
+ def _is_parameter_style(value: object) -> bool:
62
+ return isinstance(value, ParameterStyle)
63
+
64
+
65
+ def _is_positive_integer(value: object) -> bool:
66
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
67
+
68
+
69
+ def _all_boolean(values: tuple[object, ...]) -> bool:
70
+ return all(isinstance(value, bool) for value in values)
71
+
72
+
73
+ __all__ = ("CompiledQuery", "SQLiteCapabilities")
@@ -0,0 +1,36 @@
1
+ """SQLite typed result operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from typing import cast
7
+
8
+ from pyoq.query.execution import (
9
+ BulkPlan,
10
+ DatabaseCursor,
11
+ QueryOperations,
12
+ WriteProvider,
13
+ )
14
+ from pyoq.query.sqlite.compiler import SQLiteCompiler
15
+ from pyoq.query.sqlite.planning import SQLiteBulkPlanner
16
+ from pyoq.query.write_nodes import WriteNode
17
+
18
+
19
+ class SQLiteQueryOperations(QueryOperations):
20
+ __slots__ = ("_compiler",)
21
+
22
+ def __init__(self, compiler: SQLiteCompiler) -> None:
23
+ self._compiler = compiler
24
+
25
+ @property
26
+ def compiler(self) -> SQLiteCompiler:
27
+ return self._compiler
28
+
29
+ def last_inserted_id(self, cursor: DatabaseCursor) -> int | None:
30
+ return cast("sqlite3.Cursor", cursor).lastrowid
31
+
32
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
33
+ return SQLiteBulkPlanner(self.compiler).plan(statement)
34
+
35
+
36
+ __all__ = ("SQLiteQueryOperations",)
@@ -0,0 +1,50 @@
1
+ """SQLite 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
+ SQLiteParameter = str | int | float | bytes | None
15
+
16
+
17
+ def adapt_parameters(values: tuple[object, ...]) -> tuple[SQLiteParameter, ...]:
18
+ return tuple(adapt_parameter(value) for value in values)
19
+
20
+
21
+ def adapt_parameter(value: object) -> SQLiteParameter:
22
+ if value is None or isinstance(value, (str, int, float, bytes)):
23
+ return value
24
+ if isinstance(value, Decimal):
25
+ return str(value)
26
+ if isinstance(value, (datetime, date, time)):
27
+ return value.isoformat()
28
+ if isinstance(value, timedelta):
29
+ return value.total_seconds()
30
+ if isinstance(value, UUID):
31
+ return str(value)
32
+ if isinstance(value, memoryview):
33
+ return value.tobytes()
34
+ if isinstance(value, Enum):
35
+ return adapt_parameter(value.value)
36
+ if isinstance(value, (list, tuple, dict)):
37
+ return _json_parameter(cast(object, value))
38
+ message = f"unsupported SQLite parameter type: {type(value).__name__}"
39
+ raise ParameterBindingError(message)
40
+
41
+
42
+ def _json_parameter(value: object) -> str:
43
+ try:
44
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
45
+ except (TypeError, ValueError) as error:
46
+ message = "SQLite JSON parameter is not serializable"
47
+ raise ParameterBindingError(message) from error
48
+
49
+
50
+ __all__ = ("SQLiteParameter", "adapt_parameter", "adapt_parameters")
@@ -0,0 +1,20 @@
1
+ """SQLite bulk write planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.execution.planning import BulkPlanner
6
+ from pyoq.query.sqlite.compiler import SQLiteCompiler
7
+
8
+
9
+ class SQLiteBulkPlanner(BulkPlanner):
10
+ __slots__ = ()
11
+
12
+ def __init__(self, compiler: SQLiteCompiler) -> None:
13
+ super().__init__(
14
+ compiler,
15
+ compiler.capabilities.maximum_parameters,
16
+ "SQLite",
17
+ )
18
+
19
+
20
+ __all__ = ("SQLiteBulkPlanner",)
@@ -0,0 +1,50 @@
1
+ """Exclusive synchronous SQLite connection pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from collections.abc import Callable
7
+ from typing import TypeAlias
8
+
9
+ from pyoq.errors import ConnectionPoolError
10
+ from pyoq.query.execution.pooling import (
11
+ ConnectionLease,
12
+ ConnectionPool,
13
+ PoolPolicy,
14
+ PoolStats,
15
+ )
16
+
17
+ SQLiteConnectionFactoryProtocol = Callable[[], sqlite3.Connection]
18
+ SQLiteConnectionLease: TypeAlias = ConnectionLease[sqlite3.Connection]
19
+ SQLitePoolPolicy: TypeAlias = PoolPolicy
20
+ SQLitePoolStats: TypeAlias = PoolStats
21
+
22
+
23
+ class SQLitePool(ConnectionPool[sqlite3.Connection]):
24
+ __slots__ = ()
25
+
26
+ def reset(self, connection: sqlite3.Connection) -> bool:
27
+ try:
28
+ if connection.in_transaction:
29
+ connection.rollback()
30
+ except sqlite3.Error:
31
+ return False
32
+ return True
33
+
34
+ def validate(self, value: object) -> sqlite3.Connection:
35
+ if not isinstance(value, sqlite3.Connection):
36
+ message = "SQLite connection factory returned an invalid connection"
37
+ raise ConnectionPoolError(message)
38
+ return value
39
+
40
+ def dispose(self, connection: sqlite3.Connection) -> None:
41
+ connection.close()
42
+
43
+
44
+ __all__ = (
45
+ "SQLiteConnectionFactoryProtocol",
46
+ "SQLiteConnectionLease",
47
+ "SQLitePool",
48
+ "SQLitePoolPolicy",
49
+ "SQLitePoolStats",
50
+ )
@@ -0,0 +1,13 @@
1
+ """Bounded synchronous SQLite row streaming."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeAlias
6
+
7
+ from pyoq.query.execution.streaming import RowStream, StreamPolicy, StreamResource
8
+
9
+ SQLiteStreamPolicy: TypeAlias = StreamPolicy
10
+ SQLiteStreamResource: TypeAlias = StreamResource
11
+ SQLiteRowStream = RowStream
12
+
13
+ __all__ = ("SQLiteRowStream", "SQLiteStreamPolicy", "SQLiteStreamResource")
@@ -0,0 +1,274 @@
1
+ """Owned SQLite transactions and nested savepoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from collections.abc import Callable
7
+ from enum import Enum
8
+ from typing import TypeVar, cast
9
+
10
+ from pyoq.errors import ExecutionError, QueryValidationError, TransactionStateError
11
+ from pyoq.query.execution import ExecutionControl, TransactionState
12
+ from pyoq.query.select import Query
13
+ from pyoq.query.sqlite.commands import open_cursor, run_sql
14
+ from pyoq.query.sqlite.compiler import SQLiteCompiler
15
+ from pyoq.query.sqlite.model import CompiledQuery
16
+ from pyoq.query.sqlite.operations import SQLiteQueryOperations
17
+ from pyoq.query.sqlite.parameters import adapt_parameters
18
+ from pyoq.query.sqlite.pool import SQLitePool
19
+ from pyoq.query.sqlite.streaming import (
20
+ SQLiteRowStream,
21
+ SQLiteStreamPolicy,
22
+ SQLiteStreamResource,
23
+ )
24
+
25
+ _SQLiteTransactionState = TransactionState[sqlite3.Connection, "SQLiteTransaction"]
26
+
27
+ OperationResult = TypeVar("OperationResult")
28
+ ResultRow = TypeVar("ResultRow")
29
+
30
+
31
+ class TransactionMode(Enum):
32
+ DEFERRED = "DEFERRED"
33
+ IMMEDIATE = "IMMEDIATE"
34
+ EXCLUSIVE = "EXCLUSIVE"
35
+
36
+
37
+ class SQLiteTransaction(SQLiteQueryOperations):
38
+ __slots__ = (
39
+ "_control",
40
+ "_entered",
41
+ "_finished",
42
+ "_mode",
43
+ "_parent",
44
+ "_pool",
45
+ "_savepoint_name",
46
+ "_state",
47
+ )
48
+
49
+ def __init__(
50
+ self,
51
+ pool: SQLitePool,
52
+ compiler: SQLiteCompiler,
53
+ mode: TransactionMode = TransactionMode.DEFERRED,
54
+ control: ExecutionControl | None = None,
55
+ *,
56
+ parent: SQLiteTransaction | None = None,
57
+ state: _SQLiteTransactionState | None = None,
58
+ ) -> None:
59
+ super().__init__(compiler)
60
+ if not _is_transaction_mode(mode):
61
+ raise QueryValidationError("SQLite transaction mode is invalid")
62
+ self._pool = pool
63
+ self._mode = mode
64
+ self._control = control
65
+ self._parent = parent
66
+ self._state = state or _SQLiteTransactionState()
67
+ self._savepoint_name: str | None = None
68
+ self._entered = False
69
+ self._finished = False
70
+
71
+ def __enter__(self) -> SQLiteTransaction:
72
+ if self._entered or self._finished:
73
+ raise TransactionStateError("SQLite transaction cannot be entered twice")
74
+ if self._parent is None:
75
+ self._enter_outer()
76
+ else:
77
+ self._enter_savepoint()
78
+ self._entered = True
79
+ return self
80
+
81
+ def __exit__(
82
+ self,
83
+ exception_type: type[BaseException] | None,
84
+ exception: BaseException | None,
85
+ traceback: object,
86
+ ) -> None:
87
+ if not self._entered or self._finished:
88
+ return
89
+ if self._parent is None:
90
+ self._exit_outer(exception_type)
91
+ else:
92
+ self._exit_savepoint(exception_type)
93
+
94
+ def savepoint(
95
+ self,
96
+ *,
97
+ control: ExecutionControl | None = None,
98
+ ) -> SQLiteTransaction:
99
+ self._state.require_current(self)
100
+ return SQLiteTransaction(
101
+ self._pool,
102
+ self.compiler,
103
+ self._mode,
104
+ control or self._control,
105
+ parent=self,
106
+ state=self._state,
107
+ )
108
+
109
+ def stream(
110
+ self,
111
+ query: Query[ResultRow],
112
+ /,
113
+ *,
114
+ policy: SQLiteStreamPolicy | None = None,
115
+ ) -> SQLiteRowStream[ResultRow]:
116
+ self._state.require_current(self)
117
+ selected_policy = policy or SQLiteStreamPolicy(control=self._control)
118
+ if selected_policy.control is None and self._control is not None:
119
+ selected_policy = SQLiteStreamPolicy(
120
+ selected_policy.batch_size,
121
+ self._control,
122
+ )
123
+ statement = self.compiler.compile(query)
124
+ stream: SQLiteRowStream[ResultRow]
125
+ stream = SQLiteRowStream(
126
+ lambda: self._open_stream(stream, statement, selected_policy),
127
+ selected_policy,
128
+ statement.row_types,
129
+ )
130
+ return stream
131
+
132
+ def _run(
133
+ self,
134
+ statement: CompiledQuery,
135
+ operation: Callable[[sqlite3.Cursor], OperationResult],
136
+ control: ExecutionControl | None,
137
+ ) -> OperationResult:
138
+ parameters = adapt_parameters(statement.parameters)
139
+ connection = self._state.require_current(self)
140
+ return run_sql(
141
+ connection,
142
+ statement.sql,
143
+ parameters,
144
+ operation,
145
+ control or self._control,
146
+ self._state.invalidate,
147
+ )
148
+
149
+ def _enter_outer(self) -> None:
150
+ self._state.adopt(self._pool.checkout(), self)
151
+ try:
152
+ self._run_transaction_sql(f"BEGIN {self._mode.value}")
153
+ except BaseException:
154
+ self._release_outer()
155
+ raise
156
+
157
+ def _enter_savepoint(self) -> None:
158
+ self._state.require_current(cast(SQLiteTransaction, self._parent))
159
+ name = self._state.next_savepoint()
160
+ self._run_transaction_sql(f'SAVEPOINT "{name}"', scope=self._parent)
161
+ self._savepoint_name = name
162
+ self._state.current_scope = self
163
+
164
+ def _exit_outer(self, exception_type: type[BaseException] | None) -> None:
165
+ try:
166
+ self._state.require_scope(self)
167
+ except TransactionStateError:
168
+ self._abort_misnested_outer(exception_type)
169
+ return
170
+ self._state.close_stream()
171
+ command = "ROLLBACK" if exception_type is not None else "COMMIT"
172
+ try:
173
+ if not self._state.broken:
174
+ self._run_transaction_sql(command)
175
+ except ExecutionError:
176
+ self._state.invalidate()
177
+ if exception_type is None:
178
+ raise
179
+ finally:
180
+ self._release_outer()
181
+
182
+ def _exit_savepoint(self, exception_type: type[BaseException] | None) -> None:
183
+ self._state.require_scope(self)
184
+ self._state.close_stream()
185
+ name = cast(str, self._savepoint_name)
186
+ try:
187
+ self._finish_savepoint(name, exception_type)
188
+ except ExecutionError:
189
+ self._state.invalidate()
190
+ if exception_type is None:
191
+ raise
192
+ finally:
193
+ self._state.current_scope = self._parent
194
+ self._finished = True
195
+
196
+ def _finish_savepoint(
197
+ self,
198
+ name: str,
199
+ exception_type: type[BaseException] | None,
200
+ ) -> None:
201
+ if self._state.broken:
202
+ return
203
+ if exception_type is not None:
204
+ self._run_transaction_sql(f'ROLLBACK TO SAVEPOINT "{name}"')
205
+ self._run_transaction_sql(f'RELEASE SAVEPOINT "{name}"')
206
+
207
+ def _run_transaction_sql(
208
+ self,
209
+ sql: str,
210
+ *,
211
+ scope: SQLiteTransaction | None = None,
212
+ ) -> None:
213
+ connection = self._state.require_current(scope or self, allow_stream=True)
214
+ run_sql(
215
+ connection,
216
+ sql,
217
+ (),
218
+ _discard_cursor,
219
+ self._control,
220
+ self._state.invalidate,
221
+ )
222
+
223
+ def _open_stream(
224
+ self,
225
+ stream: SQLiteRowStream[ResultRow],
226
+ statement: CompiledQuery,
227
+ policy: SQLiteStreamPolicy,
228
+ ) -> SQLiteStreamResource:
229
+ parameters = adapt_parameters(statement.parameters)
230
+ connection = self._state.require_current(self)
231
+ session = open_cursor(
232
+ connection,
233
+ statement.sql,
234
+ parameters,
235
+ policy.control,
236
+ self._state.invalidate,
237
+ )
238
+ stored_stream = cast(SQLiteRowStream[object], stream)
239
+ self._state.active_stream = stored_stream
240
+ return SQLiteStreamResource(
241
+ session,
242
+ self._state.release_stream,
243
+ )
244
+
245
+ def _release_outer(self) -> None:
246
+ self._finished = True
247
+ self._state.release()
248
+
249
+ def _abort_misnested_outer(
250
+ self,
251
+ exception_type: type[BaseException] | None,
252
+ ) -> None:
253
+ connection = self._state.require_owner()
254
+ self._state.close_stream()
255
+ try:
256
+ connection.rollback()
257
+ except sqlite3.Error:
258
+ self._state.invalidate()
259
+ finally:
260
+ self._release_outer()
261
+ if exception_type is None:
262
+ message = "SQLite transaction exited with an active nested savepoint"
263
+ raise TransactionStateError(message)
264
+
265
+
266
+ def _discard_cursor(cursor: sqlite3.Cursor) -> None:
267
+ del cursor
268
+
269
+
270
+ def _is_transaction_mode(value: object) -> bool:
271
+ return isinstance(value, TransactionMode)
272
+
273
+
274
+ __all__ = ("SQLiteTransaction", "TransactionMode")
@@ -0,0 +1,35 @@
1
+ """SQLite write statement compilation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.errors import UnsupportedQueryError
6
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
7
+ from pyoq.query.rendering.writes import WriteRenderer
8
+ from pyoq.query.sqlite.context import SQLiteCompilationContext
9
+ from pyoq.query.sqlite.expressions import SQLiteExpressionCompiler
10
+ from pyoq.query.sqlite.identifiers import QUOTING
11
+
12
+
13
+ class SQLiteWriteCompilation(WriteRenderer):
14
+ __slots__ = ("_context",)
15
+
16
+ def __init__(self, context: SQLiteCompilationContext) -> None:
17
+ self._context = context
18
+ super().__init__(SQLiteExpressionCompiler(context))
19
+
20
+ @property
21
+ def quoting(self) -> IdentifierQuoting:
22
+ return QUOTING
23
+
24
+ def require_returning(self) -> None:
25
+ if not self._context.capabilities.returning:
26
+ message = "SQLite RETURNING is disabled by compiler capabilities"
27
+ raise UnsupportedQueryError(message)
28
+
29
+ def require_conflict_resolution(self) -> None:
30
+ if not self._context.capabilities.upsert:
31
+ message = "SQLite conflict resolution is disabled by compiler capabilities"
32
+ raise UnsupportedQueryError(message)
33
+
34
+
35
+ __all__ = ("SQLiteWriteCompilation",)
@@ -0,0 +1,27 @@
1
+ """Dialect-neutral statement contracts shared by every compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, TypeAlias
6
+
7
+ from pyoq.query.execution import CompiledQuery
8
+ from pyoq.query.select_nodes import QueryNode
9
+ from pyoq.query.write_nodes import CallNode, WriteNode
10
+
11
+ StatementNode: TypeAlias = QueryNode | WriteNode | CallNode
12
+
13
+
14
+ class StatementProvider(Protocol):
15
+ @property
16
+ def node(self) -> StatementNode: ...
17
+
18
+
19
+ class StatementCompiler(Protocol):
20
+ def compile(
21
+ self,
22
+ statement: StatementProvider | StatementNode,
23
+ /,
24
+ ) -> CompiledQuery: ...
25
+
26
+
27
+ __all__ = ("StatementCompiler", "StatementNode", "StatementProvider")
pyoq/query/values.py ADDED
@@ -0,0 +1,23 @@
1
+ """Bound query values and scalar family resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ from pyoq.query.expressions import Expression, family_for_value
8
+ from pyoq.query.nodes import BoundValueNode
9
+
10
+ Value = TypeVar("Value")
11
+ InputValue = TypeVar("InputValue")
12
+
13
+
14
+ class BoundValue(Expression[Value], Generic[Value]):
15
+ __slots__ = ()
16
+
17
+
18
+ def bind(value: InputValue, /, *, sensitive: bool = False) -> BoundValue[InputValue]:
19
+ family = family_for_value(value)
20
+ return BoundValue(BoundValueNode(value, family, sensitive), family)
21
+
22
+
23
+ __all__ = ("BoundValue", "bind")