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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- 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,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",)
|
pyoq/query/statements.py
ADDED
|
@@ -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")
|