altcodepro-polydb-python 2.2.2__py3-none-any.whl → 2.2.4__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.
- altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
- altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
- polydb/__init__.py +2 -2
- polydb/adapters/AzureBlobStorageAdapter.py +146 -41
- polydb/adapters/AzureFileStorageAdapter.py +148 -43
- polydb/adapters/AzureQueueAdapter.py +96 -34
- polydb/adapters/AzureTableStorageAdapter.py +462 -119
- polydb/adapters/BlockchainBlobAdapter.py +111 -0
- polydb/adapters/BlockchainKVAdapter.py +152 -0
- polydb/adapters/BlockchainQueueAdapter.py +116 -0
- polydb/adapters/DynamoDBAdapter.py +463 -176
- polydb/adapters/FirestoreAdapter.py +320 -148
- polydb/adapters/GCPPubSubAdapter.py +217 -0
- polydb/adapters/GCPStorageAdapter.py +184 -39
- polydb/adapters/MongoDBAdapter.py +159 -39
- polydb/adapters/PostgreSQLAdapter.py +285 -83
- polydb/adapters/S3Adapter.py +172 -35
- polydb/adapters/S3CompatibleAdapter.py +62 -8
- polydb/adapters/SQSAdapter.py +121 -44
- polydb/adapters/VercelBlobAdapter.py +196 -0
- polydb/adapters/VercelKVAdapter.py +275 -283
- polydb/adapters/VercelQueueAdapter.py +61 -0
- polydb/audit/AuditStorage.py +1 -1
- polydb/base/NoSQLKVAdapter.py +113 -101
- polydb/base/ObjectStorageAdapter.py +42 -6
- polydb/base/QueueAdapter.py +2 -2
- polydb/base/SharedFilesAdapter.py +2 -2
- polydb/cloudDatabaseFactory.py +200 -0
- polydb/databaseFactory.py +434 -101
- polydb/models.py +63 -1
- polydb/query.py +111 -42
- altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
- altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
- polydb/adapters/PubSubAdapter.py +0 -85
- polydb/factory.py +0 -107
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/top_level.txt +0 -0
polydb/databaseFactory.py
CHANGED
|
@@ -2,21 +2,23 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
9
|
+
from .batch import BatchOperations
|
|
10
|
+
from .cache import CacheWarmer, RedisCacheEngine
|
|
11
|
+
from .monitoring import HealthCheck, MetricsCollector, PerformanceMonitor
|
|
12
|
+
from .multitenancy import TenantContext, TenantIsolationEnforcer, TenantRegistry
|
|
13
|
+
from .security import DataMasking, FieldEncryption, RowLevelSecurity
|
|
14
|
+
from .validation import ModelValidator
|
|
14
15
|
from .errors import AdapterConfigurationError
|
|
15
16
|
from .registry import ModelRegistry
|
|
16
17
|
from .types import JsonDict, Lookup, ModelMeta
|
|
17
18
|
from .audit.manager import AuditManager
|
|
18
19
|
from .audit.context import AuditContext
|
|
19
20
|
from .query import Operator, QueryBuilder
|
|
21
|
+
from .cloudDatabaseFactory import CloudDatabaseFactory
|
|
20
22
|
from dotenv import load_dotenv
|
|
21
23
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
@@ -28,14 +30,114 @@ _DEFAULT_RETRY = retry(
|
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Engine descriptor — one per registered SQL or NoSQL engine
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EngineConfig:
|
|
40
|
+
"""
|
|
41
|
+
Describes a single SQL or NoSQL engine that DatabaseFactory can route to.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
name : str
|
|
46
|
+
Logical name for this engine, e.g. "primary", "archive", "analytics".
|
|
47
|
+
cloud_factory : CloudDatabaseFactory
|
|
48
|
+
Fully initialised factory bound to this engine's credentials /
|
|
49
|
+
connection string.
|
|
50
|
+
sql_models : set[str] | None
|
|
51
|
+
Model class names that should be routed to this engine's SQL adapter.
|
|
52
|
+
None means "match nothing by default" (use as fallback only).
|
|
53
|
+
nosql_models : set[str] | None
|
|
54
|
+
Model class names routed to this engine's NoSQL adapter.
|
|
55
|
+
is_default_sql : bool
|
|
56
|
+
If True, this engine's SQL adapter is used for any model not matched
|
|
57
|
+
by an explicit sql_models list in any engine.
|
|
58
|
+
is_default_nosql : bool
|
|
59
|
+
Same concept for NoSQL.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
cloud_factory: CloudDatabaseFactory
|
|
64
|
+
sql_models: Optional[Set[str]] = None # explicit allow-list
|
|
65
|
+
nosql_models: Optional[Set[str]] = None # explicit allow-list
|
|
66
|
+
is_default_sql: bool = False
|
|
67
|
+
is_default_nosql: bool = False
|
|
68
|
+
|
|
69
|
+
# Lazily initialised adapters
|
|
70
|
+
_sql: Any = field(default=None, init=False, repr=False)
|
|
71
|
+
_nosql: Any = field(default=None, init=False, repr=False)
|
|
72
|
+
|
|
73
|
+
def sql(self) -> Any:
|
|
74
|
+
if self._sql is None:
|
|
75
|
+
self._sql = self.cloud_factory.get_sql()
|
|
76
|
+
return self._sql
|
|
77
|
+
|
|
78
|
+
def nosql(self) -> Any:
|
|
79
|
+
if self._nosql is None:
|
|
80
|
+
self._nosql = self.cloud_factory.get_nosql_kv()
|
|
81
|
+
return self._nosql
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Per-call override — passed into any CRUD method via `engine_override=`
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class EngineOverride:
|
|
91
|
+
"""
|
|
92
|
+
Optionally supplied at call-site to bypass routing and target a specific
|
|
93
|
+
engine (and optionally force SQL or NoSQL regardless of model metadata).
|
|
94
|
+
|
|
95
|
+
Example
|
|
96
|
+
-------
|
|
97
|
+
db.create(MyModel, data, engine_override=EngineOverride(engine_name="archive"))
|
|
98
|
+
db.read(MyModel, engine_override=EngineOverride(engine_name="analytics", force_sql=True))
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
engine_name: str
|
|
102
|
+
force_sql: bool = False
|
|
103
|
+
force_nosql: bool = False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Internal resolved pair
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class _ResolvedAdapters:
|
|
113
|
+
sql: Any
|
|
114
|
+
nosql: Any
|
|
115
|
+
engine_name: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# DatabaseFactory
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
31
123
|
class DatabaseFactory:
|
|
32
|
-
"""
|
|
124
|
+
"""
|
|
125
|
+
Universal CRUD with:
|
|
126
|
+
- Multi-engine routing (multiple SQL + NoSQL engines simultaneously)
|
|
127
|
+
- Routing by model name (sql_models / nosql_models allow-lists per engine)
|
|
128
|
+
- Per-call engine override via `engine_override=EngineOverride(...)`
|
|
129
|
+
- Cache, soft-delete, audit, multi-tenancy, RLS, encryption, monitoring
|
|
130
|
+
"""
|
|
33
131
|
|
|
34
132
|
def __init__(
|
|
35
133
|
self,
|
|
36
134
|
*,
|
|
135
|
+
# ── single-engine convenience (backwards-compatible) ──────────────
|
|
37
136
|
provider: Optional[Any] = None,
|
|
38
|
-
cloud_factory: Optional[
|
|
137
|
+
cloud_factory: Optional[CloudDatabaseFactory] = None,
|
|
138
|
+
# ── multi-engine ─────────────────────────────────────────────────
|
|
139
|
+
engines: Optional[List[EngineConfig]] = None,
|
|
140
|
+
# ── existing feature flags ────────────────────────────────────────
|
|
39
141
|
tenant_registry: Optional[TenantRegistry] = None,
|
|
40
142
|
enable_retries: bool = True,
|
|
41
143
|
enable_audit: bool = True,
|
|
@@ -47,15 +149,13 @@ class DatabaseFactory:
|
|
|
47
149
|
enable_encryption: bool = False,
|
|
48
150
|
enable_rls: bool = False,
|
|
49
151
|
):
|
|
50
|
-
from .factory import CloudDatabaseFactory
|
|
51
|
-
|
|
52
152
|
self._enable_retries = enable_retries
|
|
53
153
|
self._enable_audit = enable_audit
|
|
54
154
|
self._enable_audit_reads = enable_audit_reads
|
|
55
155
|
self._enable_cache = enable_cache
|
|
56
156
|
self._soft_delete = soft_delete
|
|
57
157
|
|
|
58
|
-
#
|
|
158
|
+
# ── monitoring ────────────────────────────────────────────────────
|
|
59
159
|
if enable_monitoring:
|
|
60
160
|
self.metrics = MetricsCollector()
|
|
61
161
|
self.health = HealthCheck(self)
|
|
@@ -63,7 +163,7 @@ class DatabaseFactory:
|
|
|
63
163
|
self.metrics = None
|
|
64
164
|
self.health = None
|
|
65
165
|
|
|
66
|
-
# Redis cache
|
|
166
|
+
# ── Redis cache ───────────────────────────────────────────────────
|
|
67
167
|
self._cache: Optional[RedisCacheEngine] = None
|
|
68
168
|
self.cache_warmer: Optional[CacheWarmer] = None
|
|
69
169
|
try:
|
|
@@ -79,30 +179,200 @@ class DatabaseFactory:
|
|
|
79
179
|
else:
|
|
80
180
|
logger.warning("use_redis_cache=True but REDIS_CACHE_URL not set")
|
|
81
181
|
|
|
82
|
-
#
|
|
182
|
+
# ── security ──────────────────────────────────────────────────────
|
|
83
183
|
self.encryption = FieldEncryption() if enable_encryption else None
|
|
84
|
-
|
|
85
|
-
# Always available (can be no-op if not configured)
|
|
86
184
|
self.masking = DataMasking()
|
|
87
|
-
|
|
88
|
-
# Row-level security
|
|
89
185
|
self.rls = RowLevelSecurity() if enable_rls else None
|
|
90
186
|
|
|
91
|
-
#
|
|
187
|
+
# ── multi-tenancy ─────────────────────────────────────────────────
|
|
92
188
|
self.tenant_registry = tenant_registry
|
|
93
189
|
self.tenant_enforcer = TenantIsolationEnforcer(tenant_registry) if tenant_registry else None
|
|
94
190
|
|
|
95
191
|
self.batch = BatchOperations(self)
|
|
96
|
-
self.
|
|
97
|
-
|
|
192
|
+
self._audit = AuditManager() if enable_audit else None
|
|
193
|
+
|
|
194
|
+
# ── engine registry ───────────────────────────────────────────────
|
|
195
|
+
# Build from the `engines` list if supplied; otherwise fall back to
|
|
196
|
+
# the legacy single-factory path for backwards compatibility.
|
|
197
|
+
self._engines: List[EngineConfig] = []
|
|
198
|
+
|
|
199
|
+
if engines:
|
|
200
|
+
self._engines = engines
|
|
201
|
+
# Validate: exactly one default SQL and one default NoSQL
|
|
202
|
+
default_sql = [e for e in engines if e.is_default_sql]
|
|
203
|
+
default_nosql = [e for e in engines if e.is_default_nosql]
|
|
204
|
+
if len(default_sql) > 1:
|
|
205
|
+
raise AdapterConfigurationError("More than one engine marked is_default_sql=True")
|
|
206
|
+
if len(default_nosql) > 1:
|
|
207
|
+
raise AdapterConfigurationError("More than one engine marked is_default_nosql=True")
|
|
208
|
+
if not default_sql:
|
|
209
|
+
logger.warning(
|
|
210
|
+
"No engine has is_default_sql=True; SQL models without an "
|
|
211
|
+
"explicit engine mapping will raise AdapterConfigurationError."
|
|
212
|
+
)
|
|
213
|
+
if not default_nosql:
|
|
214
|
+
logger.warning(
|
|
215
|
+
"No engine has is_default_nosql=True; NoSQL models without "
|
|
216
|
+
"an explicit engine mapping will raise AdapterConfigurationError."
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
# Legacy single-engine path
|
|
220
|
+
_cf = cloud_factory or CloudDatabaseFactory(provider=provider)
|
|
221
|
+
_engine = EngineConfig(
|
|
222
|
+
name="primary",
|
|
223
|
+
cloud_factory=_cf,
|
|
224
|
+
is_default_sql=True,
|
|
225
|
+
is_default_nosql=True,
|
|
226
|
+
)
|
|
227
|
+
self._engines = [_engine]
|
|
98
228
|
|
|
99
|
-
|
|
100
|
-
self.
|
|
229
|
+
# Convenience: build name → EngineConfig index for O(1) override lookup
|
|
230
|
+
self._engine_by_name: Dict[str, EngineConfig] = {e.name: e for e in self._engines}
|
|
101
231
|
|
|
102
|
-
|
|
103
|
-
|
|
232
|
+
# Expose a primary provider name for audit (first engine)
|
|
233
|
+
self._provider_name = self._engines[0].cloud_factory.provider.value
|
|
104
234
|
|
|
105
|
-
|
|
235
|
+
# -----------------------------------------------------------------------
|
|
236
|
+
# Engine routing
|
|
237
|
+
# -----------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def _resolve_adapters(
|
|
240
|
+
self,
|
|
241
|
+
model_name: str,
|
|
242
|
+
storage: str, # "sql" or "nosql"
|
|
243
|
+
override: Optional[EngineOverride] = None,
|
|
244
|
+
) -> _ResolvedAdapters:
|
|
245
|
+
"""
|
|
246
|
+
Return (sql_adapter, nosql_adapter, engine_name) for the given model.
|
|
247
|
+
|
|
248
|
+
Resolution order
|
|
249
|
+
----------------
|
|
250
|
+
1. EngineOverride.engine_name (per-call explicit override)
|
|
251
|
+
2. Explicit sql_models / nosql_models allow-list on any engine
|
|
252
|
+
3. is_default_sql / is_default_nosql fallback
|
|
253
|
+
"""
|
|
254
|
+
# 1. Per-call override
|
|
255
|
+
if override:
|
|
256
|
+
engine = self._engine_by_name.get(override.engine_name)
|
|
257
|
+
if engine is None:
|
|
258
|
+
raise AdapterConfigurationError(
|
|
259
|
+
f"EngineOverride references unknown engine '{override.engine_name}'. "
|
|
260
|
+
f"Registered engines: {list(self._engine_by_name)}"
|
|
261
|
+
)
|
|
262
|
+
return _ResolvedAdapters(
|
|
263
|
+
sql=engine.sql(),
|
|
264
|
+
nosql=engine.nosql(),
|
|
265
|
+
engine_name=engine.name,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# 2. Explicit allow-list match
|
|
269
|
+
for engine in self._engines:
|
|
270
|
+
if storage == "sql" and engine.sql_models and model_name in engine.sql_models:
|
|
271
|
+
return _ResolvedAdapters(
|
|
272
|
+
sql=engine.sql(),
|
|
273
|
+
nosql=engine.nosql(),
|
|
274
|
+
engine_name=engine.name,
|
|
275
|
+
)
|
|
276
|
+
if storage == "nosql" and engine.nosql_models and model_name in engine.nosql_models:
|
|
277
|
+
return _ResolvedAdapters(
|
|
278
|
+
sql=engine.sql(),
|
|
279
|
+
nosql=engine.nosql(),
|
|
280
|
+
engine_name=engine.name,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# 3. Default fallback
|
|
284
|
+
for engine in self._engines:
|
|
285
|
+
if storage == "sql" and engine.is_default_sql:
|
|
286
|
+
return _ResolvedAdapters(
|
|
287
|
+
sql=engine.sql(),
|
|
288
|
+
nosql=engine.nosql(),
|
|
289
|
+
engine_name=engine.name,
|
|
290
|
+
)
|
|
291
|
+
if storage == "nosql" and engine.is_default_nosql:
|
|
292
|
+
return _ResolvedAdapters(
|
|
293
|
+
sql=engine.sql(),
|
|
294
|
+
nosql=engine.nosql(),
|
|
295
|
+
engine_name=engine.name,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
raise AdapterConfigurationError(
|
|
299
|
+
f"No engine found for model='{model_name}' storage='{storage}'. "
|
|
300
|
+
f"Add an explicit mapping or mark an engine as default."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _adapters_for(
|
|
304
|
+
self,
|
|
305
|
+
model: Union[type, str],
|
|
306
|
+
meta: ModelMeta,
|
|
307
|
+
override: Optional[EngineOverride] = None,
|
|
308
|
+
) -> _ResolvedAdapters:
|
|
309
|
+
"""Determine storage type from meta, then resolve adapters."""
|
|
310
|
+
model_name = self._model_name(model)
|
|
311
|
+
# EngineOverride can force the storage type
|
|
312
|
+
if override and override.force_sql:
|
|
313
|
+
storage = "sql"
|
|
314
|
+
elif override and override.force_nosql:
|
|
315
|
+
storage = "nosql"
|
|
316
|
+
else:
|
|
317
|
+
storage = "sql" if (meta.storage == "sql" and meta.table) else "nosql"
|
|
318
|
+
return self._resolve_adapters(model_name, storage, override)
|
|
319
|
+
|
|
320
|
+
# -----------------------------------------------------------------------
|
|
321
|
+
# Runtime engine registration helpers
|
|
322
|
+
# -----------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def register_engine(self, engine: EngineConfig) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Add or replace an engine at runtime.
|
|
327
|
+
|
|
328
|
+
Useful when connection strings are only known after initialisation
|
|
329
|
+
(e.g. per-tenant sharding resolved from a config store).
|
|
330
|
+
"""
|
|
331
|
+
if engine.name in self._engine_by_name:
|
|
332
|
+
logger.info("Replacing existing engine '%s'", engine.name)
|
|
333
|
+
self._engines = [e for e in self._engines if e.name != engine.name]
|
|
334
|
+
self._engines.append(engine)
|
|
335
|
+
self._engine_by_name[engine.name] = engine
|
|
336
|
+
logger.info(
|
|
337
|
+
"Registered engine '%s' (provider=%s)", engine.name, engine.cloud_factory.provider.value
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def unregister_engine(self, name: str) -> None:
|
|
341
|
+
"""Remove an engine by name."""
|
|
342
|
+
if name not in self._engine_by_name:
|
|
343
|
+
raise AdapterConfigurationError(f"Engine '{name}' not registered.")
|
|
344
|
+
self._engines = [e for e in self._engines if e.name != name]
|
|
345
|
+
del self._engine_by_name[name]
|
|
346
|
+
logger.info("Unregistered engine '%s'", name)
|
|
347
|
+
|
|
348
|
+
def get_engine(self, name: str) -> EngineConfig:
|
|
349
|
+
if name not in self._engine_by_name:
|
|
350
|
+
raise AdapterConfigurationError(
|
|
351
|
+
f"Engine '{name}' not found. Registered: {list(self._engine_by_name)}"
|
|
352
|
+
)
|
|
353
|
+
return self._engine_by_name[name]
|
|
354
|
+
|
|
355
|
+
# -----------------------------------------------------------------------
|
|
356
|
+
# Backwards-compatible _sql / _nosql properties (primary engine)
|
|
357
|
+
# -----------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def _sql(self) -> Any:
|
|
361
|
+
for e in self._engines:
|
|
362
|
+
if e.is_default_sql:
|
|
363
|
+
return e.sql()
|
|
364
|
+
return self._engines[0].sql()
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def _nosql(self) -> Any:
|
|
368
|
+
for e in self._engines:
|
|
369
|
+
if e.is_default_nosql:
|
|
370
|
+
return e.nosql()
|
|
371
|
+
return self._engines[0].nosql()
|
|
372
|
+
|
|
373
|
+
# -----------------------------------------------------------------------
|
|
374
|
+
# Internal helpers (unchanged from original)
|
|
375
|
+
# -----------------------------------------------------------------------
|
|
106
376
|
|
|
107
377
|
def _meta(self, model: Union[type, str]) -> ModelMeta:
|
|
108
378
|
return ModelRegistry.get(model)
|
|
@@ -114,15 +384,12 @@ class DatabaseFactory:
|
|
|
114
384
|
return model.__name__ if isinstance(model, type) else str(model)
|
|
115
385
|
|
|
116
386
|
def _current_tenant_id(self) -> Optional[str]:
|
|
117
|
-
# Prefer TenantContext if present
|
|
118
387
|
try:
|
|
119
388
|
tenant = TenantContext.get_tenant()
|
|
120
389
|
if tenant and tenant.tenant_id:
|
|
121
390
|
return tenant.tenant_id
|
|
122
391
|
except Exception:
|
|
123
392
|
pass
|
|
124
|
-
|
|
125
|
-
# Fallback to AuditContext
|
|
126
393
|
return AuditContext.tenant_id.get()
|
|
127
394
|
|
|
128
395
|
def _current_actor_id(self) -> Optional[str]:
|
|
@@ -130,10 +397,8 @@ class DatabaseFactory:
|
|
|
130
397
|
|
|
131
398
|
def _inject_tenant(self, data: JsonDict) -> JsonDict:
|
|
132
399
|
tenant_id = self._current_tenant_id()
|
|
133
|
-
|
|
134
400
|
if not tenant_id:
|
|
135
|
-
|
|
136
|
-
|
|
401
|
+
return data
|
|
137
402
|
data = dict(data)
|
|
138
403
|
data.setdefault("tenant_id", tenant_id)
|
|
139
404
|
return data
|
|
@@ -142,21 +407,20 @@ class DatabaseFactory:
|
|
|
142
407
|
data = dict(data)
|
|
143
408
|
actor_id = self._current_actor_id()
|
|
144
409
|
now = datetime.utcnow().isoformat()
|
|
145
|
-
|
|
146
410
|
if is_create:
|
|
147
411
|
if "created_at" not in data:
|
|
148
412
|
data["created_at"] = now
|
|
149
413
|
if "created_by" not in data and actor_id:
|
|
150
414
|
data["created_by"] = actor_id
|
|
151
|
-
|
|
152
415
|
if "updated_at" not in data:
|
|
153
416
|
data["updated_at"] = now
|
|
154
417
|
if "updated_by" not in data and actor_id:
|
|
155
418
|
data["updated_by"] = actor_id
|
|
156
|
-
|
|
157
419
|
return data
|
|
158
420
|
|
|
159
421
|
def _apply_soft_delete_filter(self, query: Optional[Lookup]) -> Lookup:
|
|
422
|
+
if not self._soft_delete:
|
|
423
|
+
return query or {}
|
|
160
424
|
result = dict(query or {})
|
|
161
425
|
result.setdefault("deleted_at", None)
|
|
162
426
|
return result
|
|
@@ -184,18 +448,19 @@ class DatabaseFactory:
|
|
|
184
448
|
before: Optional[JsonDict],
|
|
185
449
|
after: Optional[JsonDict],
|
|
186
450
|
error: Optional[str],
|
|
451
|
+
engine_name: Optional[str] = None,
|
|
187
452
|
):
|
|
188
453
|
if not self._audit:
|
|
189
454
|
return
|
|
190
|
-
|
|
191
455
|
try:
|
|
192
456
|
changed_fields = self._compute_field_changes(before, after)
|
|
457
|
+
provider = engine_name or self._provider_name
|
|
193
458
|
self._audit.record(
|
|
194
459
|
action=action,
|
|
195
460
|
model=self._model_name(model),
|
|
196
461
|
entity_id=str(entity_id) if entity_id else None,
|
|
197
462
|
storage_type=meta.storage,
|
|
198
|
-
provider=
|
|
463
|
+
provider=provider,
|
|
199
464
|
success=success,
|
|
200
465
|
before=before,
|
|
201
466
|
after=after,
|
|
@@ -211,17 +476,29 @@ class DatabaseFactory:
|
|
|
211
476
|
retry_fn = _DEFAULT_RETRY(fn)
|
|
212
477
|
return retry_fn()
|
|
213
478
|
|
|
214
|
-
|
|
479
|
+
# -----------------------------------------------------------------------
|
|
480
|
+
# CRUD — all methods now accept optional engine_override kwarg
|
|
481
|
+
# -----------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
def create(
|
|
484
|
+
self,
|
|
485
|
+
model: Union[type, str],
|
|
486
|
+
data: JsonDict,
|
|
487
|
+
*,
|
|
488
|
+
engine_override: Optional[EngineOverride] = None,
|
|
489
|
+
) -> JsonDict:
|
|
215
490
|
model_type = self._model_type(model)
|
|
216
|
-
|
|
491
|
+
meta = self._meta(model)
|
|
492
|
+
|
|
493
|
+
# Only enforce validator when model actually declares __polydb__
|
|
494
|
+
if getattr(model_type, "__polydb__", None):
|
|
495
|
+
ModelValidator.validate_and_raise(model_type)
|
|
217
496
|
|
|
218
497
|
model_name = self._model_name(model)
|
|
219
498
|
meta = self._meta(model)
|
|
220
|
-
|
|
221
499
|
tenant_id = self._current_tenant_id()
|
|
222
500
|
actor_id = self._current_actor_id()
|
|
223
501
|
|
|
224
|
-
# Security & policies
|
|
225
502
|
if self.tenant_enforcer:
|
|
226
503
|
data = self.tenant_enforcer.enforce_write(model_name, data)
|
|
227
504
|
if self.rls:
|
|
@@ -230,11 +507,12 @@ class DatabaseFactory:
|
|
|
230
507
|
data = self._inject_tenant(data)
|
|
231
508
|
data = self._inject_audit_fields(data, is_create=True)
|
|
232
509
|
|
|
233
|
-
# Encryption (uses meta.encrypted_fields – assumed defined on model)
|
|
234
510
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
235
511
|
if self.encryption and encrypted_fields:
|
|
236
512
|
data = self.encryption.encrypt_fields(data, encrypted_fields)
|
|
237
513
|
|
|
514
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
515
|
+
|
|
238
516
|
before = None
|
|
239
517
|
after_plain = None
|
|
240
518
|
success = False
|
|
@@ -244,20 +522,22 @@ class DatabaseFactory:
|
|
|
244
522
|
def _op() -> JsonDict:
|
|
245
523
|
nonlocal after_plain, success, entity_id
|
|
246
524
|
|
|
247
|
-
if
|
|
248
|
-
|
|
525
|
+
if (
|
|
526
|
+
meta.storage == "sql"
|
|
527
|
+
and meta.table
|
|
528
|
+
and not (engine_override and engine_override.force_nosql)
|
|
529
|
+
):
|
|
530
|
+
result = adapters.sql.insert(meta.table, data)
|
|
249
531
|
else:
|
|
250
|
-
result =
|
|
532
|
+
result = adapters.nosql.put(model_type, data)
|
|
251
533
|
|
|
252
534
|
entity_id = result.get("id")
|
|
253
|
-
# Decrypt for audit / returned value (plain text)
|
|
254
535
|
after_plain = result
|
|
255
536
|
if self.encryption and encrypted_fields:
|
|
256
537
|
after_plain = self.encryption.decrypt_fields(result, encrypted_fields)
|
|
257
538
|
|
|
258
539
|
success = True
|
|
259
540
|
|
|
260
|
-
# Cache invalidation
|
|
261
541
|
if self._enable_cache and self._cache:
|
|
262
542
|
self._cache.invalidate(model_name)
|
|
263
543
|
|
|
@@ -289,6 +569,7 @@ class DatabaseFactory:
|
|
|
289
569
|
before=before,
|
|
290
570
|
after=after_plain,
|
|
291
571
|
error=error,
|
|
572
|
+
engine_name=adapters.engine_name,
|
|
292
573
|
)
|
|
293
574
|
|
|
294
575
|
def read(
|
|
@@ -301,42 +582,46 @@ class DatabaseFactory:
|
|
|
301
582
|
no_cache: bool = False,
|
|
302
583
|
cache_ttl: Optional[int] = None,
|
|
303
584
|
include_deleted: bool = False,
|
|
585
|
+
engine_override: Optional[EngineOverride] = None,
|
|
304
586
|
) -> List[JsonDict]:
|
|
305
|
-
|
|
306
587
|
model_name = self._model_name(model)
|
|
307
588
|
meta = self._meta(model)
|
|
308
589
|
tenant_id = self._current_tenant_id()
|
|
309
590
|
actor_id = self._current_actor_id()
|
|
310
|
-
|
|
311
|
-
|
|
591
|
+
|
|
592
|
+
if self._soft_delete and not include_deleted:
|
|
593
|
+
query = self._apply_soft_delete_filter(query)
|
|
312
594
|
if self.tenant_enforcer:
|
|
313
595
|
query = self.tenant_enforcer.enforce_read(model_name, query or {})
|
|
314
596
|
if self.rls:
|
|
315
597
|
query = self.rls.enforce_read(model_name, query or {})
|
|
316
|
-
# Inject tenant filter (mandatory isolation)
|
|
317
|
-
tenant_id = self._current_tenant_id()
|
|
318
598
|
if tenant_id:
|
|
319
599
|
query = dict(query or {})
|
|
320
600
|
query.setdefault("tenant_id", tenant_id)
|
|
601
|
+
|
|
602
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
321
603
|
use_external_cache = self._enable_cache and self._cache and getattr(meta, "cache", False)
|
|
322
604
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
323
605
|
|
|
324
606
|
def _op() -> List[JsonDict]:
|
|
325
|
-
if
|
|
326
|
-
|
|
607
|
+
if (
|
|
608
|
+
meta.storage == "sql"
|
|
609
|
+
and meta.table
|
|
610
|
+
and not (engine_override and engine_override.force_nosql)
|
|
611
|
+
):
|
|
612
|
+
raw_rows = adapters.sql.select(meta.table, query, limit=limit, offset=offset)
|
|
327
613
|
else:
|
|
328
614
|
model_type = self._model_type(model)
|
|
329
615
|
eff_no_cache = no_cache or use_external_cache
|
|
330
616
|
eff_ttl = cache_ttl if cache_ttl is not None else getattr(meta, "cache_ttl", None)
|
|
331
|
-
raw_rows =
|
|
617
|
+
raw_rows = adapters.nosql.query(
|
|
332
618
|
model_type,
|
|
333
619
|
query=query,
|
|
334
620
|
limit=limit,
|
|
335
|
-
no_cache=eff_no_cache,
|
|
621
|
+
no_cache=eff_no_cache,
|
|
336
622
|
cache_ttl=None if eff_no_cache else eff_ttl,
|
|
337
623
|
)
|
|
338
624
|
|
|
339
|
-
# Decrypt + mask
|
|
340
625
|
if self.encryption and encrypted_fields:
|
|
341
626
|
raw_rows = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw_rows]
|
|
342
627
|
rows = [
|
|
@@ -344,14 +629,12 @@ class DatabaseFactory:
|
|
|
344
629
|
for r in raw_rows
|
|
345
630
|
]
|
|
346
631
|
|
|
347
|
-
# Set external cache
|
|
348
632
|
if use_external_cache and not no_cache:
|
|
349
633
|
ttl = cache_ttl or getattr(meta, "cache_ttl", 300)
|
|
350
634
|
self._cache.set(model_name, query or {}, rows, ttl) # type: ignore
|
|
351
635
|
|
|
352
636
|
return rows
|
|
353
637
|
|
|
354
|
-
# Cache check
|
|
355
638
|
rows: List[JsonDict] = []
|
|
356
639
|
cached = None
|
|
357
640
|
if use_external_cache and not no_cache:
|
|
@@ -359,7 +642,6 @@ class DatabaseFactory:
|
|
|
359
642
|
if cached is not None:
|
|
360
643
|
rows = cached
|
|
361
644
|
else:
|
|
362
|
-
# Run op with monitoring
|
|
363
645
|
monitor_ctx = (
|
|
364
646
|
PerformanceMonitor(self.metrics, "read", model_name, tenant_id)
|
|
365
647
|
if self.metrics
|
|
@@ -372,7 +654,6 @@ class DatabaseFactory:
|
|
|
372
654
|
else:
|
|
373
655
|
rows = self._run(_op)
|
|
374
656
|
|
|
375
|
-
# Audit on success
|
|
376
657
|
if self._audit and self._enable_audit_reads:
|
|
377
658
|
self._audit_safe(
|
|
378
659
|
action="read",
|
|
@@ -383,6 +664,7 @@ class DatabaseFactory:
|
|
|
383
664
|
before=None,
|
|
384
665
|
after={"count": len(rows)},
|
|
385
666
|
error=None,
|
|
667
|
+
engine_name=adapters.engine_name,
|
|
386
668
|
)
|
|
387
669
|
|
|
388
670
|
return rows
|
|
@@ -394,6 +676,7 @@ class DatabaseFactory:
|
|
|
394
676
|
*,
|
|
395
677
|
no_cache: bool = False,
|
|
396
678
|
include_deleted: bool = False,
|
|
679
|
+
engine_override: Optional[EngineOverride] = None,
|
|
397
680
|
) -> Optional[JsonDict]:
|
|
398
681
|
rows = self.read(
|
|
399
682
|
model,
|
|
@@ -401,6 +684,7 @@ class DatabaseFactory:
|
|
|
401
684
|
limit=1,
|
|
402
685
|
no_cache=no_cache,
|
|
403
686
|
include_deleted=include_deleted,
|
|
687
|
+
engine_override=engine_override,
|
|
404
688
|
)
|
|
405
689
|
return rows[0] if rows else None
|
|
406
690
|
|
|
@@ -412,33 +696,38 @@ class DatabaseFactory:
|
|
|
412
696
|
page_size: int = 100,
|
|
413
697
|
continuation_token: Optional[str] = None,
|
|
414
698
|
include_deleted: bool = False,
|
|
415
|
-
|
|
699
|
+
engine_override: Optional[EngineOverride] = None,
|
|
700
|
+
) -> Optional[Tuple[List[JsonDict], Optional[str]]]:
|
|
416
701
|
model_name = self._model_name(model)
|
|
417
702
|
meta = self._meta(model)
|
|
418
703
|
tenant_id = self._current_tenant_id()
|
|
419
704
|
actor_id = self._current_actor_id()
|
|
420
705
|
|
|
421
|
-
|
|
422
|
-
|
|
706
|
+
if self._soft_delete and not include_deleted:
|
|
707
|
+
query = self._apply_soft_delete_filter(query)
|
|
423
708
|
if self.tenant_enforcer:
|
|
424
709
|
query = self.tenant_enforcer.enforce_read(model_name, query or {})
|
|
425
710
|
if self.rls:
|
|
426
711
|
query = self.rls.enforce_read(model_name, query or {})
|
|
427
|
-
# Inject tenant filter (mandatory isolation)
|
|
428
|
-
tenant_id = self._current_tenant_id()
|
|
429
712
|
if tenant_id:
|
|
430
713
|
query = dict(query or {})
|
|
431
714
|
query.setdefault("tenant_id", tenant_id)
|
|
715
|
+
|
|
716
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
432
717
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
433
718
|
|
|
434
719
|
def _op() -> Tuple[List[JsonDict], Optional[str]]:
|
|
435
|
-
if
|
|
436
|
-
|
|
720
|
+
if (
|
|
721
|
+
meta.storage == "sql"
|
|
722
|
+
and meta.table
|
|
723
|
+
and not (engine_override and engine_override.force_nosql)
|
|
724
|
+
):
|
|
725
|
+
raw_rows, next_token = adapters.sql.select_page(
|
|
437
726
|
meta.table, query, page_size, continuation_token
|
|
438
727
|
)
|
|
439
728
|
else:
|
|
440
729
|
model_type = self._model_type(model)
|
|
441
|
-
raw_rows, next_token =
|
|
730
|
+
raw_rows, next_token = adapters.nosql.query_page(
|
|
442
731
|
model_type, query, page_size, continuation_token
|
|
443
732
|
)
|
|
444
733
|
|
|
@@ -450,7 +739,6 @@ class DatabaseFactory:
|
|
|
450
739
|
]
|
|
451
740
|
return rows, next_token
|
|
452
741
|
|
|
453
|
-
# Run with monitoring
|
|
454
742
|
monitor_ctx = (
|
|
455
743
|
PerformanceMonitor(self.metrics, "read_page", model_name, tenant_id)
|
|
456
744
|
if self.metrics
|
|
@@ -465,9 +753,7 @@ class DatabaseFactory:
|
|
|
465
753
|
else:
|
|
466
754
|
result = self._run(_op)
|
|
467
755
|
|
|
468
|
-
# Audit on success
|
|
469
756
|
if self._audit and self._enable_audit_reads and result:
|
|
470
|
-
count = len(result[0])
|
|
471
757
|
self._audit_safe(
|
|
472
758
|
action="read_page",
|
|
473
759
|
model=model,
|
|
@@ -475,8 +761,9 @@ class DatabaseFactory:
|
|
|
475
761
|
meta=meta,
|
|
476
762
|
success=True,
|
|
477
763
|
before=None,
|
|
478
|
-
after={"count":
|
|
764
|
+
after={"count": len(result[0])},
|
|
479
765
|
error=None,
|
|
766
|
+
engine_name=adapters.engine_name,
|
|
480
767
|
)
|
|
481
768
|
|
|
482
769
|
return result
|
|
@@ -491,6 +778,7 @@ class DatabaseFactory:
|
|
|
491
778
|
*,
|
|
492
779
|
etag: Optional[str] = None,
|
|
493
780
|
replace: bool = False,
|
|
781
|
+
engine_override: Optional[EngineOverride] = None,
|
|
494
782
|
) -> JsonDict:
|
|
495
783
|
model_name = self._model_name(model)
|
|
496
784
|
meta = self._meta(model)
|
|
@@ -498,7 +786,6 @@ class DatabaseFactory:
|
|
|
498
786
|
|
|
499
787
|
data = self._inject_audit_fields(data, is_create=False)
|
|
500
788
|
|
|
501
|
-
# Security on changed fields
|
|
502
789
|
if self.tenant_enforcer:
|
|
503
790
|
data = self.tenant_enforcer.enforce_write(model_name, data)
|
|
504
791
|
if self.rls:
|
|
@@ -508,7 +795,10 @@ class DatabaseFactory:
|
|
|
508
795
|
if self.encryption and encrypted_fields:
|
|
509
796
|
data = self.encryption.encrypt_fields(data, [f for f in encrypted_fields if f in data])
|
|
510
797
|
|
|
511
|
-
|
|
798
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
799
|
+
before = self._fetch_before(
|
|
800
|
+
model, meta, entity_id, etag=etag, engine_override=engine_override
|
|
801
|
+
)
|
|
512
802
|
after_plain = None
|
|
513
803
|
success = False
|
|
514
804
|
error: Optional[str] = None
|
|
@@ -516,14 +806,19 @@ class DatabaseFactory:
|
|
|
516
806
|
def _op() -> JsonDict:
|
|
517
807
|
nonlocal after_plain, success
|
|
518
808
|
|
|
519
|
-
if
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
809
|
+
if (
|
|
810
|
+
meta.storage == "sql"
|
|
811
|
+
and meta.table
|
|
812
|
+
and not (engine_override and engine_override.force_nosql)
|
|
813
|
+
):
|
|
814
|
+
if tenant_id and isinstance(entity_id, dict):
|
|
815
|
+
entity_id.setdefault("tenant_id", tenant_id)
|
|
816
|
+
result = adapters.sql.update(meta.table, entity_id, data)
|
|
524
817
|
else:
|
|
525
818
|
model_type = self._model_type(model)
|
|
526
|
-
result =
|
|
819
|
+
result = adapters.nosql.patch(
|
|
820
|
+
model_type, entity_id, data, etag=etag, replace=replace
|
|
821
|
+
)
|
|
527
822
|
|
|
528
823
|
after_plain = result
|
|
529
824
|
if self.encryption and encrypted_fields:
|
|
@@ -562,9 +857,17 @@ class DatabaseFactory:
|
|
|
562
857
|
before=before,
|
|
563
858
|
after=after_plain,
|
|
564
859
|
error=error,
|
|
860
|
+
engine_name=adapters.engine_name,
|
|
565
861
|
)
|
|
566
862
|
|
|
567
|
-
def upsert(
|
|
863
|
+
def upsert(
|
|
864
|
+
self,
|
|
865
|
+
model: Union[type, str],
|
|
866
|
+
data: JsonDict,
|
|
867
|
+
*,
|
|
868
|
+
replace: bool = False,
|
|
869
|
+
engine_override: Optional[EngineOverride] = None,
|
|
870
|
+
) -> JsonDict:
|
|
568
871
|
model_name = self._model_name(model)
|
|
569
872
|
meta = self._meta(model)
|
|
570
873
|
tenant_id = self._current_tenant_id()
|
|
@@ -581,6 +884,7 @@ class DatabaseFactory:
|
|
|
581
884
|
if self.encryption and encrypted_fields:
|
|
582
885
|
data = self.encryption.encrypt_fields(data, encrypted_fields)
|
|
583
886
|
|
|
887
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
584
888
|
after_plain = None
|
|
585
889
|
success = False
|
|
586
890
|
error: Optional[str] = None
|
|
@@ -588,11 +892,15 @@ class DatabaseFactory:
|
|
|
588
892
|
def _op() -> JsonDict:
|
|
589
893
|
nonlocal after_plain, success
|
|
590
894
|
|
|
591
|
-
if
|
|
592
|
-
|
|
895
|
+
if (
|
|
896
|
+
meta.storage == "sql"
|
|
897
|
+
and meta.table
|
|
898
|
+
and not (engine_override and engine_override.force_nosql)
|
|
899
|
+
):
|
|
900
|
+
result = adapters.sql.upsert(meta.table, data)
|
|
593
901
|
else:
|
|
594
902
|
model_type = self._model_type(model)
|
|
595
|
-
result =
|
|
903
|
+
result = adapters.nosql.upsert(model_type, data, replace=replace)
|
|
596
904
|
|
|
597
905
|
after_plain = result
|
|
598
906
|
if self.encryption and encrypted_fields:
|
|
@@ -631,6 +939,7 @@ class DatabaseFactory:
|
|
|
631
939
|
before=None,
|
|
632
940
|
after=after_plain,
|
|
633
941
|
error=error,
|
|
942
|
+
engine_name=adapters.engine_name,
|
|
634
943
|
)
|
|
635
944
|
|
|
636
945
|
def delete(
|
|
@@ -640,6 +949,7 @@ class DatabaseFactory:
|
|
|
640
949
|
*,
|
|
641
950
|
etag: Optional[str] = None,
|
|
642
951
|
hard: bool = False,
|
|
952
|
+
engine_override: Optional[EngineOverride] = None,
|
|
643
953
|
) -> JsonDict:
|
|
644
954
|
meta = self._meta(model)
|
|
645
955
|
model_name = self._model_name(model)
|
|
@@ -651,20 +961,27 @@ class DatabaseFactory:
|
|
|
651
961
|
"deleted_at": now,
|
|
652
962
|
"deleted_by": self._current_actor_id(),
|
|
653
963
|
}
|
|
654
|
-
return self.update(model, entity_id, delete_payload)
|
|
964
|
+
return self.update(model, entity_id, delete_payload, engine_override=engine_override)
|
|
655
965
|
|
|
656
|
-
|
|
966
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
967
|
+
before = self._fetch_before(
|
|
968
|
+
model, meta, entity_id, etag=etag, engine_override=engine_override
|
|
969
|
+
)
|
|
657
970
|
success = False
|
|
658
971
|
error: Optional[str] = None
|
|
659
972
|
|
|
660
973
|
def _op() -> JsonDict:
|
|
661
974
|
nonlocal success
|
|
662
975
|
|
|
663
|
-
if
|
|
664
|
-
|
|
976
|
+
if (
|
|
977
|
+
meta.storage == "sql"
|
|
978
|
+
and meta.table
|
|
979
|
+
and not (engine_override and engine_override.force_nosql)
|
|
980
|
+
):
|
|
981
|
+
result = adapters.sql.delete(meta.table, entity_id)
|
|
665
982
|
else:
|
|
666
983
|
model_type = self._model_type(model)
|
|
667
|
-
result =
|
|
984
|
+
result = adapters.nosql.delete(model_type, entity_id, etag=etag)
|
|
668
985
|
|
|
669
986
|
success = True
|
|
670
987
|
|
|
@@ -699,6 +1016,7 @@ class DatabaseFactory:
|
|
|
699
1016
|
before=before,
|
|
700
1017
|
after=None,
|
|
701
1018
|
error=error,
|
|
1019
|
+
engine_name=adapters.engine_name,
|
|
702
1020
|
)
|
|
703
1021
|
|
|
704
1022
|
def _fetch_before(
|
|
@@ -708,36 +1026,51 @@ class DatabaseFactory:
|
|
|
708
1026
|
entity_id: Union[Any, Lookup],
|
|
709
1027
|
*,
|
|
710
1028
|
etag: Optional[str] = None,
|
|
1029
|
+
engine_override: Optional[EngineOverride] = None,
|
|
711
1030
|
) -> Optional[JsonDict]:
|
|
712
1031
|
lookup = {"id": entity_id} if not isinstance(entity_id, dict) else entity_id
|
|
713
|
-
|
|
714
|
-
|
|
1032
|
+
return self.read_one(
|
|
1033
|
+
model,
|
|
1034
|
+
lookup,
|
|
1035
|
+
no_cache=True,
|
|
1036
|
+
include_deleted=True,
|
|
1037
|
+
engine_override=engine_override,
|
|
1038
|
+
)
|
|
715
1039
|
|
|
716
1040
|
def query_linq(
|
|
717
|
-
self,
|
|
1041
|
+
self,
|
|
1042
|
+
model: Union[type, str],
|
|
1043
|
+
builder: QueryBuilder,
|
|
1044
|
+
*,
|
|
1045
|
+
engine_override: Optional[EngineOverride] = None,
|
|
718
1046
|
) -> Union[List[JsonDict], int]:
|
|
719
1047
|
model_name = self._model_name(model)
|
|
720
1048
|
meta = self._meta(model)
|
|
721
|
-
extra_filter = {}
|
|
1049
|
+
extra_filter: Lookup = {}
|
|
1050
|
+
|
|
722
1051
|
if self.tenant_enforcer:
|
|
723
1052
|
extra_filter = self.tenant_enforcer.enforce_read(model_name, extra_filter)
|
|
724
1053
|
if self.rls:
|
|
725
1054
|
extra_filter = self.rls.enforce_read(model_name, extra_filter)
|
|
726
1055
|
|
|
727
1056
|
if extra_filter:
|
|
728
|
-
for
|
|
729
|
-
builder = builder.where(
|
|
1057
|
+
for filter_field, filter_value in extra_filter.items():
|
|
1058
|
+
builder = builder.where(filter_field, Operator.EQ, filter_value)
|
|
730
1059
|
|
|
731
1060
|
tenant_id = self._current_tenant_id()
|
|
1061
|
+
adapters = self._adapters_for(model, meta, engine_override)
|
|
732
1062
|
|
|
733
1063
|
def _op():
|
|
734
|
-
if
|
|
735
|
-
|
|
1064
|
+
if (
|
|
1065
|
+
meta.storage == "sql"
|
|
1066
|
+
and meta.table
|
|
1067
|
+
and not (engine_override and engine_override.force_nosql)
|
|
1068
|
+
):
|
|
1069
|
+
return adapters.sql.query_linq(meta.table, builder)
|
|
736
1070
|
else:
|
|
737
1071
|
model_type = self._model_type(model)
|
|
738
|
-
return
|
|
1072
|
+
return adapters.nosql.query_linq(model_type, builder)
|
|
739
1073
|
|
|
740
|
-
# Run with monitoring
|
|
741
1074
|
monitor_ctx = (
|
|
742
1075
|
PerformanceMonitor(self.metrics, "query_linq", model_name, tenant_id)
|
|
743
1076
|
if self.metrics
|
|
@@ -753,7 +1086,6 @@ class DatabaseFactory:
|
|
|
753
1086
|
else:
|
|
754
1087
|
result = self._run(_op)
|
|
755
1088
|
|
|
756
|
-
# Audit on success
|
|
757
1089
|
if self._audit and self._enable_audit_reads and isinstance(result, list):
|
|
758
1090
|
self._audit_safe(
|
|
759
1091
|
action="query_linq",
|
|
@@ -764,6 +1096,7 @@ class DatabaseFactory:
|
|
|
764
1096
|
before=None,
|
|
765
1097
|
after={"count": len(result)},
|
|
766
1098
|
error=None,
|
|
1099
|
+
engine_name=adapters.engine_name,
|
|
767
1100
|
)
|
|
768
1101
|
|
|
769
1102
|
return result
|