altcodepro-polydb-python 2.1.0__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 (51) hide show
  1. altcodepro_polydb_python-2.1.0.dist-info/METADATA +378 -0
  2. altcodepro_polydb_python-2.1.0.dist-info/RECORD +51 -0
  3. altcodepro_polydb_python-2.1.0.dist-info/WHEEL +5 -0
  4. altcodepro_polydb_python-2.1.0.dist-info/licenses/LICENSE +21 -0
  5. altcodepro_polydb_python-2.1.0.dist-info/top_level.txt +1 -0
  6. polydb/__init__.py +64 -0
  7. polydb/adapters/AzureBlobStorageAdapter.py +77 -0
  8. polydb/adapters/AzureFileStorageAdapter.py +79 -0
  9. polydb/adapters/AzureQueueAdapter.py +61 -0
  10. polydb/adapters/AzureTableStorageAdapter.py +182 -0
  11. polydb/adapters/DynamoDBAdapter.py +216 -0
  12. polydb/adapters/EFSAdapter.py +50 -0
  13. polydb/adapters/FirestoreAdapter.py +193 -0
  14. polydb/adapters/GCPStorageAdapter.py +81 -0
  15. polydb/adapters/MongoDBAdapter.py +136 -0
  16. polydb/adapters/PostgreSQLAdapter.py +453 -0
  17. polydb/adapters/PubSubAdapter.py +83 -0
  18. polydb/adapters/S3Adapter.py +86 -0
  19. polydb/adapters/S3CompatibleAdapter.py +90 -0
  20. polydb/adapters/SQSAdapter.py +84 -0
  21. polydb/adapters/VercelKVAdapter.py +327 -0
  22. polydb/adapters/__init__.py +0 -0
  23. polydb/advanced_query.py +147 -0
  24. polydb/audit/AuditStorage.py +136 -0
  25. polydb/audit/__init__.py +7 -0
  26. polydb/audit/context.py +53 -0
  27. polydb/audit/manager.py +47 -0
  28. polydb/audit/models.py +86 -0
  29. polydb/base/NoSQLKVAdapter.py +301 -0
  30. polydb/base/ObjectStorageAdapter.py +42 -0
  31. polydb/base/QueueAdapter.py +27 -0
  32. polydb/base/SharedFilesAdapter.py +32 -0
  33. polydb/base/__init__.py +0 -0
  34. polydb/batch.py +163 -0
  35. polydb/cache.py +204 -0
  36. polydb/databaseFactory.py +748 -0
  37. polydb/decorators.py +21 -0
  38. polydb/errors.py +82 -0
  39. polydb/factory.py +107 -0
  40. polydb/models.py +39 -0
  41. polydb/monitoring.py +313 -0
  42. polydb/multitenancy.py +197 -0
  43. polydb/py.typed +0 -0
  44. polydb/query.py +150 -0
  45. polydb/registry.py +71 -0
  46. polydb/retry.py +76 -0
  47. polydb/schema.py +205 -0
  48. polydb/security.py +458 -0
  49. polydb/types.py +127 -0
  50. polydb/utils.py +61 -0
  51. polydb/validation.py +131 -0
@@ -0,0 +1,748 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from typing import Any, Callable, List, Optional, Tuple, Union
6
+ from datetime import datetime
7
+ 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
14
+ from .errors import AdapterConfigurationError
15
+ from .registry import ModelRegistry
16
+ from .types import JsonDict, Lookup, ModelMeta
17
+ from .audit.manager import AuditManager
18
+ from .audit.context import AuditContext
19
+ from .query import Operator, QueryBuilder
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _DEFAULT_RETRY = retry(
24
+ wait=wait_exponential(multiplier=0.5, min=0.5, max=6),
25
+ stop=stop_after_attempt(3),
26
+ reraise=True,
27
+ )
28
+
29
+
30
+ class DatabaseFactory:
31
+ """Universal CRUD with cache, soft delete, audit, multi-tenancy, RLS, encryption, monitoring"""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ provider: Optional[Any] = None,
37
+ cloud_factory: Optional[Any] = None,
38
+ tenant_registry: Optional[TenantRegistry] = None,
39
+ enable_retries: bool = True,
40
+ enable_audit: bool = True,
41
+ enable_audit_reads: bool = False,
42
+ enable_cache: bool = True,
43
+ soft_delete: bool = False,
44
+ use_redis_cache: bool = False,
45
+ enable_monitoring: bool = False,
46
+ enable_encryption: bool = False,
47
+ enable_rls: bool = False,
48
+ ):
49
+ from .factory import CloudDatabaseFactory
50
+
51
+ self._enable_retries = enable_retries
52
+ self._enable_audit = enable_audit
53
+ self._enable_audit_reads = enable_audit_reads
54
+ self._enable_cache = enable_cache
55
+ self._soft_delete = soft_delete
56
+
57
+ # Monitoring
58
+ if enable_monitoring:
59
+ self.metrics = MetricsCollector()
60
+ self.health = HealthCheck(self)
61
+ else:
62
+ self.metrics = None
63
+ self.health = None
64
+
65
+ # Redis cache (only if explicitly enabled + URL present)
66
+ self._cache: Optional[RedisCacheEngine] = None
67
+ self.cache_warmer: Optional[CacheWarmer] = None
68
+ if enable_cache and use_redis_cache:
69
+ redis_url = os.getenv("REDIS_CACHE_URL")
70
+ if redis_url:
71
+ self._cache = RedisCacheEngine(redis_url=redis_url)
72
+ self.cache_warmer = CacheWarmer(self, self._cache)
73
+ else:
74
+ logger.warning("use_redis_cache=True but REDIS_CACHE_URL not set")
75
+
76
+ # Encryption
77
+ self.encryption = FieldEncryption() if enable_encryption else None
78
+
79
+ # Always available (can be no-op if not configured)
80
+ self.masking = DataMasking()
81
+
82
+ # Row-level security
83
+ self.rls = RowLevelSecurity() if enable_rls else None
84
+
85
+ # Multi-tenancy
86
+ self.tenant_registry = tenant_registry
87
+ self.tenant_enforcer = TenantIsolationEnforcer(tenant_registry) if tenant_registry else None
88
+
89
+ self.batch = BatchOperations(self)
90
+ self._cloud_factory = cloud_factory or CloudDatabaseFactory(provider=provider)
91
+ self._provider_name = self._cloud_factory.provider.value
92
+
93
+ self._sql = self._cloud_factory.get_sql()
94
+ self._nosql = self._cloud_factory.get_nosql_kv()
95
+
96
+ if not self._sql or not self._nosql:
97
+ raise AdapterConfigurationError("Adapters not initialized")
98
+
99
+ self._audit = AuditManager() if enable_audit else None
100
+
101
+ def _meta(self, model: Union[type, str]) -> ModelMeta:
102
+ return ModelRegistry.get(model)
103
+
104
+ def _model_type(self, model: Union[type, str]) -> type:
105
+ return ModelRegistry.resolve(model)
106
+
107
+ def _model_name(self, model: Union[type, str]) -> str:
108
+ return model.__name__ if isinstance(model, type) else str(model)
109
+
110
+ def _current_tenant_id(self) -> Optional[str]:
111
+ tenant = TenantContext.get_tenant()
112
+ return tenant.tenant_id if tenant else None
113
+
114
+ def _current_actor_id(self) -> Optional[str]:
115
+ return AuditContext.actor_id.get()
116
+
117
+ def _inject_tenant(self, data: JsonDict) -> JsonDict:
118
+ tenant_id = self._current_tenant_id()
119
+ if tenant_id and "tenant_id" not in data:
120
+ data = dict(data)
121
+ data["tenant_id"] = tenant_id
122
+ return data
123
+
124
+ def _inject_audit_fields(self, data: JsonDict, is_create: bool = False) -> JsonDict:
125
+ data = dict(data)
126
+ actor_id = self._current_actor_id()
127
+ now = datetime.utcnow().isoformat()
128
+
129
+ if is_create:
130
+ if "created_at" not in data:
131
+ data["created_at"] = now
132
+ if "created_by" not in data and actor_id:
133
+ data["created_by"] = actor_id
134
+
135
+ if "updated_at" not in data:
136
+ data["updated_at"] = now
137
+ if "updated_by" not in data and actor_id:
138
+ data["updated_by"] = actor_id
139
+
140
+ return data
141
+
142
+ def _apply_soft_delete_filter(self, query: Optional[Lookup]) -> Lookup:
143
+ if not self._soft_delete:
144
+ return query or {}
145
+
146
+ result = dict(query or {})
147
+ result.setdefault("deleted_at", None)
148
+ return result
149
+
150
+ def _compute_field_changes(
151
+ self, before: Optional[JsonDict], after: Optional[JsonDict]
152
+ ) -> Optional[List[str]]:
153
+ if not before or not after:
154
+ return None
155
+ changed = [
156
+ key
157
+ for key in set(before.keys()) | set(after.keys())
158
+ if before.get(key) != after.get(key)
159
+ ]
160
+ return changed or None
161
+
162
+ def _audit_safe(
163
+ self,
164
+ *,
165
+ action: str,
166
+ model: Union[type, str],
167
+ entity_id: Optional[Any],
168
+ meta: ModelMeta,
169
+ success: bool,
170
+ before: Optional[JsonDict],
171
+ after: Optional[JsonDict],
172
+ error: Optional[str],
173
+ ):
174
+ if not self._audit:
175
+ return
176
+
177
+ try:
178
+ changed_fields = self._compute_field_changes(before, after)
179
+ self._audit.record(
180
+ action=action,
181
+ model=self._model_name(model),
182
+ entity_id=str(entity_id) if entity_id else None,
183
+ storage_type=meta.storage,
184
+ provider=self._provider_name,
185
+ success=success,
186
+ before=before,
187
+ after=after,
188
+ changed_fields=changed_fields,
189
+ error=error,
190
+ )
191
+ except Exception as exc:
192
+ logger.error(f"Audit recording failed: {exc}")
193
+
194
+ def _run(self, fn: Callable[[], Any]) -> Any:
195
+ if not self._enable_retries:
196
+ return fn()
197
+ retry_fn = _DEFAULT_RETRY(fn)
198
+ return retry_fn()
199
+
200
+ def create(self, model: Union[type, str], data: JsonDict) -> JsonDict:
201
+ model_type = self._model_type(model)
202
+ ModelValidator.validate_and_raise(model_type)
203
+
204
+ model_name = self._model_name(model)
205
+ meta = self._meta(model)
206
+
207
+ tenant_id = self._current_tenant_id()
208
+ actor_id = self._current_actor_id()
209
+
210
+ # Security & policies
211
+ if self.tenant_enforcer:
212
+ data = self.tenant_enforcer.enforce_write(model_name, data)
213
+ if self.rls:
214
+ data = self.rls.enforce_write(model_name, data)
215
+
216
+ data = self._inject_tenant(data)
217
+ data = self._inject_audit_fields(data, is_create=True)
218
+
219
+ # Encryption (uses meta.encrypted_fields – assumed defined on model)
220
+ encrypted_fields = getattr(meta, "encrypted_fields", [])
221
+ if self.encryption and encrypted_fields:
222
+ data = self.encryption.encrypt_fields(data, encrypted_fields)
223
+
224
+ before = None
225
+ after_plain = None
226
+ success = False
227
+ error: Optional[str] = None
228
+ entity_id: Optional[Any] = None
229
+
230
+ def _op() -> JsonDict:
231
+ nonlocal after_plain, success, entity_id
232
+
233
+ if meta.storage == "sql" and meta.table:
234
+ result = self._sql.insert(meta.table, data)
235
+ else:
236
+ result = self._nosql.put(model_type, data)
237
+
238
+ entity_id = result.get("id")
239
+ # Decrypt for audit / returned value (plain text)
240
+ after_plain = result
241
+ if self.encryption and encrypted_fields:
242
+ after_plain = self.encryption.decrypt_fields(result, encrypted_fields)
243
+
244
+ success = True
245
+
246
+ # Cache invalidation
247
+ if self._enable_cache and self._cache:
248
+ self._cache.invalidate(model_name)
249
+
250
+ return after_plain
251
+
252
+ monitor_ctx = (
253
+ PerformanceMonitor(self.metrics, "create", model_name, tenant_id)
254
+ if self.metrics
255
+ else None
256
+ )
257
+ try:
258
+ if monitor_ctx:
259
+ with monitor_ctx as m:
260
+ result = self._run(_op)
261
+ m.rows_affected = 1 # type: ignore
262
+ return result
263
+ else:
264
+ return self._run(_op)
265
+ except Exception as exc:
266
+ error = str(exc)
267
+ raise
268
+ finally:
269
+ self._audit_safe(
270
+ action="create",
271
+ model=model,
272
+ entity_id=entity_id,
273
+ meta=meta,
274
+ success=success,
275
+ before=before,
276
+ after=after_plain,
277
+ error=error,
278
+ )
279
+
280
+ def read(
281
+ self,
282
+ model: Union[type, str],
283
+ query: Optional[Lookup] = None,
284
+ *,
285
+ limit: Optional[int] = None,
286
+ offset: Optional[int] = None,
287
+ no_cache: bool = False,
288
+ cache_ttl: Optional[int] = None,
289
+ include_deleted: bool = False,
290
+ ) -> List[JsonDict]:
291
+
292
+ model_name = self._model_name(model)
293
+ meta = self._meta(model)
294
+ tenant_id = self._current_tenant_id()
295
+ actor_id = self._current_actor_id()
296
+
297
+ query = self._apply_soft_delete_filter(query if not include_deleted else None)
298
+
299
+ # Multi-tenancy & RLS filters
300
+ if self.tenant_enforcer:
301
+ query = self.tenant_enforcer.enforce_read(model_name, query or {})
302
+ if self.rls:
303
+ query = self.rls.enforce_read(model_name, query or {})
304
+
305
+ use_external_cache = self._enable_cache and self._cache and getattr(meta, "cache", False)
306
+ encrypted_fields = getattr(meta, "encrypted_fields", [])
307
+
308
+ def _op() -> List[JsonDict]:
309
+ if meta.storage == "sql" and meta.table:
310
+ raw_rows = self._sql.select(meta.table, query, limit=limit, offset=offset)
311
+ else:
312
+ model_type = self._model_type(model)
313
+ eff_no_cache = no_cache or use_external_cache
314
+ eff_ttl = cache_ttl if cache_ttl is not None else getattr(meta, "cache_ttl", None)
315
+ raw_rows = self._nosql.query(
316
+ model_type,
317
+ query=query,
318
+ limit=limit,
319
+ no_cache=eff_no_cache, # type: ignore
320
+ cache_ttl=None if eff_no_cache else eff_ttl,
321
+ )
322
+
323
+ # Decrypt + mask
324
+ if self.encryption and encrypted_fields:
325
+ raw_rows = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw_rows]
326
+ rows = [
327
+ self.masking.mask(r, model=model_name, actor_id=actor_id, tenant_id=tenant_id)
328
+ for r in raw_rows
329
+ ]
330
+
331
+ # Set external cache
332
+ if use_external_cache and not no_cache:
333
+ ttl = cache_ttl or getattr(meta, "cache_ttl", 300)
334
+ self._cache.set(model_name, query or {}, rows, ttl) # type: ignore
335
+
336
+ return rows
337
+
338
+ # Cache check
339
+ rows: List[JsonDict] = []
340
+ cached = None
341
+ if use_external_cache and not no_cache:
342
+ cached = self._cache.get(model_name, query or {}) # type: ignore
343
+ if cached is not None:
344
+ rows = cached
345
+ else:
346
+ # Run op with monitoring
347
+ monitor_ctx = (
348
+ PerformanceMonitor(self.metrics, "read", model_name, tenant_id)
349
+ if self.metrics
350
+ else None
351
+ )
352
+ if monitor_ctx:
353
+ with monitor_ctx as m:
354
+ rows = self._run(_op)
355
+ m.rows_returned = len(rows) # type: ignore
356
+ else:
357
+ rows = self._run(_op)
358
+
359
+ # Audit on success
360
+ if self._audit and self._enable_audit_reads:
361
+ self._audit_safe(
362
+ action="read",
363
+ model=model,
364
+ entity_id=None,
365
+ meta=meta,
366
+ success=True,
367
+ before=None,
368
+ after={"count": len(rows)},
369
+ error=None,
370
+ )
371
+
372
+ return rows
373
+
374
+ def read_one(
375
+ self,
376
+ model: Union[type, str],
377
+ query: Lookup,
378
+ *,
379
+ no_cache: bool = False,
380
+ include_deleted: bool = False,
381
+ ) -> Optional[JsonDict]:
382
+ rows = self.read(
383
+ model,
384
+ query=query,
385
+ limit=1,
386
+ no_cache=no_cache,
387
+ include_deleted=include_deleted,
388
+ )
389
+ return rows[0] if rows else None
390
+
391
+ def read_page(
392
+ self,
393
+ model: Union[type, str],
394
+ query: Lookup,
395
+ *,
396
+ page_size: int = 100,
397
+ continuation_token: Optional[str] = None,
398
+ include_deleted: bool = False,
399
+ ) -> Tuple[List[JsonDict], Optional[str]] | None:
400
+ model_name = self._model_name(model)
401
+ meta = self._meta(model)
402
+ tenant_id = self._current_tenant_id()
403
+ actor_id = self._current_actor_id()
404
+
405
+ query = self._apply_soft_delete_filter(query if not include_deleted else None)
406
+
407
+ if self.tenant_enforcer:
408
+ query = self.tenant_enforcer.enforce_read(model_name, query or {})
409
+ if self.rls:
410
+ query = self.rls.enforce_read(model_name, query or {})
411
+
412
+ encrypted_fields = getattr(meta, "encrypted_fields", [])
413
+
414
+ def _op() -> Tuple[List[JsonDict], Optional[str]]:
415
+ if meta.storage == "sql" and meta.table:
416
+ raw_rows, next_token = self._sql.select_page(
417
+ meta.table, query, page_size, continuation_token
418
+ )
419
+ else:
420
+ model_type = self._model_type(model)
421
+ raw_rows, next_token = self._nosql.query_page(
422
+ model_type, query, page_size, continuation_token
423
+ )
424
+
425
+ if self.encryption and encrypted_fields:
426
+ raw_rows = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw_rows]
427
+ rows = [
428
+ self.masking.mask(r, model=model_name, actor_id=actor_id, tenant_id=tenant_id)
429
+ for r in raw_rows
430
+ ]
431
+ return rows, next_token
432
+
433
+ # Run with monitoring
434
+ monitor_ctx = (
435
+ PerformanceMonitor(self.metrics, "read_page", model_name, tenant_id)
436
+ if self.metrics
437
+ else None
438
+ )
439
+ result: Optional[Tuple[List[JsonDict], Optional[str]]] = None
440
+ try:
441
+ if monitor_ctx:
442
+ with monitor_ctx as m:
443
+ result = self._run(_op)
444
+ m.rows_returned = len(result[0]) # type: ignore
445
+ else:
446
+ result = self._run(_op)
447
+
448
+ # Audit on success
449
+ if self._audit and self._enable_audit_reads and result:
450
+ count = len(result[0])
451
+ self._audit_safe(
452
+ action="read_page",
453
+ model=model,
454
+ entity_id=None,
455
+ meta=meta,
456
+ success=True,
457
+ before=None,
458
+ after={"count": count},
459
+ error=None,
460
+ )
461
+
462
+ return result
463
+ except Exception:
464
+ raise
465
+
466
+ def update(
467
+ self,
468
+ model: Union[type, str],
469
+ entity_id: Union[Any, Lookup],
470
+ data: JsonDict,
471
+ *,
472
+ etag: Optional[str] = None,
473
+ replace: bool = False,
474
+ ) -> JsonDict:
475
+ model_name = self._model_name(model)
476
+ meta = self._meta(model)
477
+ tenant_id = self._current_tenant_id()
478
+
479
+ data = self._inject_audit_fields(data, is_create=False)
480
+
481
+ # Security on changed fields
482
+ if self.tenant_enforcer:
483
+ data = self.tenant_enforcer.enforce_write(model_name, data)
484
+ if self.rls:
485
+ data = self.rls.enforce_write(model_name, data)
486
+
487
+ encrypted_fields = getattr(meta, "encrypted_fields", [])
488
+ if self.encryption and encrypted_fields:
489
+ data = self.encryption.encrypt_fields(data, [f for f in encrypted_fields if f in data])
490
+
491
+ before = self._fetch_before(model, meta, entity_id, etag=etag)
492
+ after_plain = None
493
+ success = False
494
+ error: Optional[str] = None
495
+
496
+ def _op() -> JsonDict:
497
+ nonlocal after_plain, success
498
+
499
+ if meta.storage == "sql" and meta.table:
500
+ result = self._sql.update(meta.table, entity_id, data)
501
+ else:
502
+ model_type = self._model_type(model)
503
+ result = self._nosql.patch(model_type, entity_id, data, etag=etag, replace=replace)
504
+
505
+ after_plain = result
506
+ if self.encryption and encrypted_fields:
507
+ after_plain = self.encryption.decrypt_fields(result, encrypted_fields)
508
+
509
+ success = True
510
+
511
+ if self._enable_cache and self._cache:
512
+ self._cache.invalidate(model_name)
513
+
514
+ return after_plain
515
+
516
+ monitor_ctx = (
517
+ PerformanceMonitor(self.metrics, "update", model_name, tenant_id)
518
+ if self.metrics
519
+ else None
520
+ )
521
+ try:
522
+ if monitor_ctx:
523
+ with monitor_ctx as m:
524
+ result = self._run(_op)
525
+ m.rows_affected = 1 # type: ignore
526
+ return result
527
+ else:
528
+ return self._run(_op)
529
+ except Exception as exc:
530
+ error = str(exc)
531
+ raise
532
+ finally:
533
+ self._audit_safe(
534
+ action="update",
535
+ model=model,
536
+ entity_id=entity_id if not isinstance(entity_id, dict) else None,
537
+ meta=meta,
538
+ success=success,
539
+ before=before,
540
+ after=after_plain,
541
+ error=error,
542
+ )
543
+
544
+ def upsert(self, model: Union[type, str], data: JsonDict, *, replace: bool = False) -> JsonDict:
545
+ model_name = self._model_name(model)
546
+ meta = self._meta(model)
547
+ tenant_id = self._current_tenant_id()
548
+
549
+ if self.tenant_enforcer:
550
+ data = self.tenant_enforcer.enforce_write(model_name, data)
551
+ if self.rls:
552
+ data = self.rls.enforce_write(model_name, data)
553
+
554
+ data = self._inject_tenant(data)
555
+ data = self._inject_audit_fields(data, is_create=True)
556
+
557
+ encrypted_fields = getattr(meta, "encrypted_fields", [])
558
+ if self.encryption and encrypted_fields:
559
+ data = self.encryption.encrypt_fields(data, encrypted_fields)
560
+
561
+ after_plain = None
562
+ success = False
563
+ error: Optional[str] = None
564
+
565
+ def _op() -> JsonDict:
566
+ nonlocal after_plain, success
567
+
568
+ if meta.storage == "sql" and meta.table:
569
+ result = self._sql.upsert(meta.table, data)
570
+ else:
571
+ model_type = self._model_type(model)
572
+ result = self._nosql.upsert(model_type, data, replace=replace)
573
+
574
+ after_plain = result
575
+ if self.encryption and encrypted_fields:
576
+ after_plain = self.encryption.decrypt_fields(result, encrypted_fields)
577
+
578
+ success = True
579
+
580
+ if self._enable_cache and self._cache:
581
+ self._cache.invalidate(model_name)
582
+
583
+ return after_plain
584
+
585
+ monitor_ctx = (
586
+ PerformanceMonitor(self.metrics, "upsert", model_name, tenant_id)
587
+ if self.metrics
588
+ else None
589
+ )
590
+ try:
591
+ if monitor_ctx:
592
+ with monitor_ctx as m:
593
+ result = self._run(_op)
594
+ m.rows_affected = 1 # type: ignore
595
+ return result
596
+ else:
597
+ return self._run(_op)
598
+ except Exception as exc:
599
+ error = str(exc)
600
+ raise
601
+ finally:
602
+ self._audit_safe(
603
+ action="upsert",
604
+ model=model,
605
+ entity_id=None,
606
+ meta=meta,
607
+ success=success,
608
+ before=None,
609
+ after=after_plain,
610
+ error=error,
611
+ )
612
+
613
+ def delete(
614
+ self,
615
+ model: Union[type, str],
616
+ entity_id: Union[Any, Lookup],
617
+ *,
618
+ etag: Optional[str] = None,
619
+ hard: bool = False,
620
+ ) -> JsonDict:
621
+ meta = self._meta(model)
622
+ model_name = self._model_name(model)
623
+ tenant_id = self._current_tenant_id()
624
+
625
+ if self._soft_delete and not hard:
626
+ now = datetime.utcnow().isoformat()
627
+ delete_payload = {
628
+ "deleted_at": now,
629
+ "deleted_by": self._current_actor_id(),
630
+ }
631
+ return self.update(model, entity_id, delete_payload)
632
+
633
+ before = self._fetch_before(model, meta, entity_id, etag=etag)
634
+ success = False
635
+ error: Optional[str] = None
636
+
637
+ def _op() -> JsonDict:
638
+ nonlocal success
639
+
640
+ if meta.storage == "sql" and meta.table:
641
+ result = self._sql.delete(meta.table, entity_id)
642
+ else:
643
+ model_type = self._model_type(model)
644
+ result = self._nosql.delete(model_type, entity_id, etag=etag)
645
+
646
+ success = True
647
+
648
+ if self._enable_cache and self._cache:
649
+ self._cache.invalidate(model_name)
650
+
651
+ return result
652
+
653
+ monitor_ctx = (
654
+ PerformanceMonitor(self.metrics, "delete", model_name, tenant_id)
655
+ if self.metrics
656
+ else None
657
+ )
658
+ try:
659
+ if monitor_ctx:
660
+ with monitor_ctx as m:
661
+ result = self._run(_op)
662
+ m.rows_affected = 1 # type: ignore
663
+ return result
664
+ else:
665
+ return self._run(_op)
666
+ except Exception as exc:
667
+ error = str(exc)
668
+ raise
669
+ finally:
670
+ self._audit_safe(
671
+ action="delete",
672
+ model=model,
673
+ entity_id=entity_id if not isinstance(entity_id, dict) else None,
674
+ meta=meta,
675
+ success=success,
676
+ before=before,
677
+ after=None,
678
+ error=error,
679
+ )
680
+
681
+ def _fetch_before(
682
+ self,
683
+ model: Union[type, str],
684
+ meta: ModelMeta,
685
+ entity_id: Union[Any, Lookup],
686
+ *,
687
+ etag: Optional[str] = None,
688
+ ) -> Optional[JsonDict]:
689
+ lookup = {"id": entity_id} if not isinstance(entity_id, dict) else entity_id
690
+ # read_one already applies tenant + RLS + decryption + masking
691
+ return self.read_one(model, lookup, no_cache=True, include_deleted=True)
692
+
693
+ def query_linq(
694
+ self, model: Union[type, str], builder: QueryBuilder
695
+ ) -> Union[List[JsonDict], int]:
696
+ model_name = self._model_name(model)
697
+ meta = self._meta(model)
698
+ extra_filter = {}
699
+ if self.tenant_enforcer:
700
+ extra_filter = self.tenant_enforcer.enforce_read(model_name, extra_filter)
701
+ if self.rls:
702
+ extra_filter = self.rls.enforce_read(model_name, extra_filter)
703
+
704
+ if extra_filter:
705
+ for field, value in extra_filter.items():
706
+ builder = builder.where(field, Operator.EQ, value)
707
+
708
+ tenant_id = self._current_tenant_id()
709
+
710
+ def _op():
711
+ if meta.storage == "sql" and meta.table:
712
+ return self._sql.query_linq(meta.table, builder)
713
+ else:
714
+ model_type = self._model_type(model)
715
+ return self._nosql.query_linq(model_type, builder)
716
+
717
+ # Run with monitoring
718
+ monitor_ctx = (
719
+ PerformanceMonitor(self.metrics, "query_linq", model_name, tenant_id)
720
+ if self.metrics
721
+ else None
722
+ )
723
+ result: Union[List[JsonDict], int]
724
+ try:
725
+ if monitor_ctx:
726
+ with monitor_ctx as m:
727
+ result = self._run(_op)
728
+ if isinstance(result, list):
729
+ m.rows_returned = len(result) # type: ignore
730
+ else:
731
+ result = self._run(_op)
732
+
733
+ # Audit on success
734
+ if self._audit and self._enable_audit_reads and isinstance(result, list):
735
+ self._audit_safe(
736
+ action="query_linq",
737
+ model=model,
738
+ entity_id=None,
739
+ meta=meta,
740
+ success=True,
741
+ before=None,
742
+ after={"count": len(result)},
743
+ error=None,
744
+ )
745
+
746
+ return result
747
+ except Exception:
748
+ raise