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.
Files changed (38) hide show
  1. altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
  2. altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
  3. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
  4. polydb/__init__.py +2 -2
  5. polydb/adapters/AzureBlobStorageAdapter.py +146 -41
  6. polydb/adapters/AzureFileStorageAdapter.py +148 -43
  7. polydb/adapters/AzureQueueAdapter.py +96 -34
  8. polydb/adapters/AzureTableStorageAdapter.py +462 -119
  9. polydb/adapters/BlockchainBlobAdapter.py +111 -0
  10. polydb/adapters/BlockchainKVAdapter.py +152 -0
  11. polydb/adapters/BlockchainQueueAdapter.py +116 -0
  12. polydb/adapters/DynamoDBAdapter.py +463 -176
  13. polydb/adapters/FirestoreAdapter.py +320 -148
  14. polydb/adapters/GCPPubSubAdapter.py +217 -0
  15. polydb/adapters/GCPStorageAdapter.py +184 -39
  16. polydb/adapters/MongoDBAdapter.py +159 -39
  17. polydb/adapters/PostgreSQLAdapter.py +285 -83
  18. polydb/adapters/S3Adapter.py +172 -35
  19. polydb/adapters/S3CompatibleAdapter.py +62 -8
  20. polydb/adapters/SQSAdapter.py +121 -44
  21. polydb/adapters/VercelBlobAdapter.py +196 -0
  22. polydb/adapters/VercelKVAdapter.py +275 -283
  23. polydb/adapters/VercelQueueAdapter.py +61 -0
  24. polydb/audit/AuditStorage.py +1 -1
  25. polydb/base/NoSQLKVAdapter.py +113 -101
  26. polydb/base/ObjectStorageAdapter.py +42 -6
  27. polydb/base/QueueAdapter.py +2 -2
  28. polydb/base/SharedFilesAdapter.py +2 -2
  29. polydb/cloudDatabaseFactory.py +200 -0
  30. polydb/databaseFactory.py +434 -101
  31. polydb/models.py +63 -1
  32. polydb/query.py +111 -42
  33. altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
  34. altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
  35. polydb/adapters/PubSubAdapter.py +0 -85
  36. polydb/factory.py +0 -107
  37. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
  38. {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 typing import Any, Callable, List, Optional, Tuple, Union
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 polydb.batch import BatchOperations
9
- from polydb.cache import CacheWarmer, RedisCacheEngine
10
- from polydb.monitoring import HealthCheck, MetricsCollector, PerformanceMonitor
11
- from polydb.multitenancy import TenantContext, TenantIsolationEnforcer, TenantRegistry
12
- from polydb.security import DataMasking, FieldEncryption, RowLevelSecurity
13
- from polydb.validation import ModelValidator
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
- """Universal CRUD with cache, soft delete, audit, multi-tenancy, RLS, encryption, monitoring"""
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[Any] = None,
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
- # Monitoring
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 (only if explicitly enabled + URL present)
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
- # Encryption
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
- # Multi-tenancy
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._cloud_factory = cloud_factory or CloudDatabaseFactory(provider=provider)
97
- self._provider_name = self._cloud_factory.provider.value
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
- self._sql = self._cloud_factory.get_sql()
100
- self._nosql = self._cloud_factory.get_nosql_kv()
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
- if not self._sql or not self._nosql:
103
- raise AdapterConfigurationError("Adapters not initialized")
232
+ # Expose a primary provider name for audit (first engine)
233
+ self._provider_name = self._engines[0].cloud_factory.provider.value
104
234
 
105
- self._audit = AuditManager() if enable_audit else None
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
- raise ValueError("Tenant ID is required but not set in AuditContext or TenantContext")
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=self._provider_name,
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
- def create(self, model: Union[type, str], data: JsonDict) -> JsonDict:
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
- ModelValidator.validate_and_raise(model_type)
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 meta.storage == "sql" and meta.table:
248
- result = self._sql.insert(meta.table, data)
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 = self._nosql.put(model_type, data)
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
- query = self._apply_soft_delete_filter(query) if include_deleted else query
311
- # Multi-tenancy & RLS filters
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 meta.storage == "sql" and meta.table:
326
- raw_rows = self._sql.select(meta.table, query, limit=limit, offset=offset)
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 = self._nosql.query(
617
+ raw_rows = adapters.nosql.query(
332
618
  model_type,
333
619
  query=query,
334
620
  limit=limit,
335
- no_cache=eff_no_cache, # type: ignore
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
- ) -> Tuple[List[JsonDict], Optional[str]] | None:
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
- query = self._apply_soft_delete_filter(query) if include_deleted else query
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 meta.storage == "sql" and meta.table:
436
- raw_rows, next_token = self._sql.select_page(
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 = self._nosql.query_page(
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": 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
- before = self._fetch_before(model, meta, entity_id, etag=etag)
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 meta.storage == "sql" and meta.table:
520
- if tenant_id:
521
- if isinstance(entity_id, dict):
522
- entity_id.setdefault("tenant_id", tenant_id)
523
- result = self._sql.update(meta.table, entity_id, data)
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 = self._nosql.patch(model_type, entity_id, data, etag=etag, replace=replace)
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(self, model: Union[type, str], data: JsonDict, *, replace: bool = False) -> JsonDict:
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 meta.storage == "sql" and meta.table:
592
- result = self._sql.upsert(meta.table, data)
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 = self._nosql.upsert(model_type, data, replace=replace)
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
- before = self._fetch_before(model, meta, entity_id, etag=etag)
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 meta.storage == "sql" and meta.table:
664
- result = self._sql.delete(meta.table, entity_id)
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 = self._nosql.delete(model_type, entity_id, etag=etag)
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
- # read_one already applies tenant + RLS + decryption + masking
714
- return self.read_one(model, lookup, no_cache=True, include_deleted=True)
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, model: Union[type, str], builder: QueryBuilder
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 field, value in extra_filter.items():
729
- builder = builder.where(field, Operator.EQ, value)
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 meta.storage == "sql" and meta.table:
735
- return self._sql.query_linq(meta.table, builder)
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 self._nosql.query_linq(model_type, builder)
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