altcodepro-polydb-python 2.3.6__tar.gz → 2.3.8__tar.gz

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 (81) hide show
  1. {altcodepro_polydb_python-2.3.6/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.8}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/AzureTableStorageAdapter.py +140 -145
  5. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/base/NoSQLKVAdapter.py +5 -6
  6. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/databaseFactory.py +22 -2
  7. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/LICENSE +0 -0
  8. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/MANIFEST.in +0 -0
  9. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/README.md +0 -0
  10. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/example_usage.py +0 -0
  11. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements-aws.txt +0 -0
  12. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements-azure.txt +0 -0
  13. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements-dev.txt +0 -0
  14. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements-gcp.txt +0 -0
  15. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements-generic.txt +0 -0
  16. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/requirements.txt +0 -0
  17. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/setup.cfg +0 -0
  18. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/setup.py +0 -0
  19. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
  20. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  21. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  22. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  23. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/PolyDB.py +0 -0
  24. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/__init__.py +0 -0
  25. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/EFSAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/S3Adapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/SQSAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/adapters/__init__.py +0 -0
  45. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/advanced_query.py +0 -0
  46. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/audit/AuditStorage.py +0 -0
  47. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/audit/__init__.py +0 -0
  48. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/audit/context.py +0 -0
  49. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/audit/manager.py +0 -0
  50. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/audit/models.py +0 -0
  51. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/base/QueueAdapter.py +0 -0
  53. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/base/SharedFilesAdapter.py +0 -0
  54. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/base/__init__.py +0 -0
  55. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/batch.py +0 -0
  56. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/cache.py +0 -0
  57. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/cloudDatabaseFactory.py +0 -0
  58. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/decorators.py +0 -0
  59. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/errors.py +0 -0
  60. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/json_safe.py +0 -0
  61. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/models.py +0 -0
  62. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/monitoring.py +0 -0
  63. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/multitenancy.py +0 -0
  64. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/py.typed +0 -0
  65. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/query.py +0 -0
  66. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/registry.py +0 -0
  67. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/retry.py +0 -0
  68. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/schema.py +0 -0
  69. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/security.py +0 -0
  70. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/types.py +0 -0
  71. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/utils.py +0 -0
  72. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/src/polydb/validation.py +0 -0
  73. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_aws.py +0 -0
  74. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_azure.py +0 -0
  75. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_blockchain.py +0 -0
  76. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_cloud_factory.py +0 -0
  77. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_gcp.py +0 -0
  78. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_mongodb.py +0 -0
  79. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_multi_engine.py +0 -0
  80. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_postgresql.py +0 -0
  81. {altcodepro_polydb_python-2.3.6 → altcodepro_polydb_python-2.3.8}/tests/test_vercel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.3.6
3
+ Version: 2.3.8
4
4
  Summary: Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety
5
5
  Author: AltCodePro
6
6
  Project-URL: Homepage, https://github.com/altcodepro/polydb-python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "altcodepro-polydb-python"
7
- version = "2.3.6"
7
+ version = "2.3.8"
8
8
  description = "Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.3.6
3
+ Version: 2.3.8
4
4
  Summary: Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety
5
5
  Author: AltCodePro
6
6
  Project-URL: Homepage, https://github.com/altcodepro/polydb-python
@@ -30,6 +30,8 @@ _BASE64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
30
30
  # ensures model isolation across the same table
31
31
  _MODEL_FIELD = "__polydb_model__"
32
32
 
33
+ logging.getLogger("azure").setLevel(logging.ERROR)
34
+
33
35
 
34
36
  class AzureTableStorageAdapter(NoSQLKVAdapter):
35
37
  """
@@ -199,33 +201,46 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
199
201
  # -----------------------------
200
202
  # Entity pack/unpack
201
203
  # -----------------------------
202
-
203
204
  def _pack_entity(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
204
- entity: JsonDict = {"PartitionKey": pk, "RowKey": rk}
205
+ """Pack data into Azure Table Storage entity format.
205
206
 
207
+ This version is much simpler, more readable, and fixes the fragile
208
+ revmap/skey logic that was likely causing normal fields to disappear.
209
+ """
210
+ entity: JsonDict = {
211
+ "PartitionKey": pk,
212
+ "RowKey": rk,
213
+ }
206
214
  entity[_MODEL_FIELD] = model.__qualname__
207
-
208
215
  keymap: Dict[str, str] = {}
209
- revmap: Dict[str, str] = {}
210
216
 
211
217
  for orig_key, orig_val in (data or {}).items():
212
- if str(orig_key) in self._RESERVED or str(orig_key) in ("PartitionKey", "RowKey"):
218
+ orig_key_str = str(orig_key)
219
+
220
+ # Skip Azure-reserved or special keys
221
+ if orig_key_str in self._RESERVED or orig_key_str in ("PartitionKey", "RowKey"):
213
222
  continue
214
223
 
215
- skey = revmap.get(str(orig_key))
216
- if not skey:
217
- skey = self._sanitize_prop_name(orig_key)
218
- base = skey
219
- i = 1
220
- while skey in entity:
221
- skey = f"{base}_{i}"
222
- i += 1
223
- revmap[str(orig_key)] = skey
224
- keymap[skey] = str(orig_key)
224
+ # Sanitize property name so it is valid for Azure Table Storage
225
+ skey = self._sanitize_prop_name(orig_key)
226
+
227
+ # Prevent key collisions (extremely rare but safe)
228
+ base = skey
229
+ counter = 1
230
+ while skey in entity:
231
+ skey = f"{base}_{counter}"
232
+ counter += 1
225
233
 
234
+ # Remember the original key name so _unpack_entity can restore it
235
+ keymap[skey] = orig_key_str
236
+
237
+ # Encode the value (must return something Azure Table accepts)
226
238
  entity[skey] = self._encode_value(orig_val)
227
239
 
228
- entity["__keymap__"] = json.dumps(keymap, default=json_safe)
240
+ # Store keymap only if we actually have fields (internal use)
241
+ if keymap:
242
+ entity["__keymap__"] = json.dumps(keymap, default=json_safe)
243
+
229
244
  return entity
230
245
 
231
246
  def _unpack_entity(self, entity: JsonDict) -> JsonDict:
@@ -374,102 +389,118 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
374
389
  # Now return the client
375
390
  return self._client.get_table_client(table_name=table_name)
376
391
 
392
+ def _restore_overflow_properties(self, entity_dict: JsonDict) -> JsonDict:
393
+ """Detect and restore any large properties stored in Blob Storage.
394
+
395
+ Called by both _get_raw and _query_raw.
396
+ """
397
+ restored = {}
398
+
399
+ for k, v in entity_dict.items():
400
+ # Internal fields (starting with _) are kept as-is
401
+ if k.startswith("_"):
402
+ restored[k] = v
403
+ continue
404
+
405
+ # Is this an overflow metadata JSON string?
406
+ if isinstance(v, str) and v.startswith("{") and '"_overflow":' in v:
407
+ try:
408
+ metadata = json.loads(v)
409
+ if not metadata.get("_overflow"):
410
+ restored[k] = v
411
+ continue
412
+
413
+ blob_key = metadata.get("_blob_key")
414
+ checksum = metadata.get("_checksum")
415
+
416
+ if not blob_key:
417
+ restored[k] = v
418
+ continue
419
+
420
+ # Download real value from blob
421
+ blob_data = self._blob_download(blob_key)
422
+
423
+ # Optional but very safe: checksum validation
424
+ if checksum:
425
+ actual_checksum = hashlib.md5(blob_data).hexdigest()
426
+ if actual_checksum != checksum:
427
+ logger.warning(
428
+ f"Checksum mismatch for blob {blob_key} (property '{k}')"
429
+ )
430
+
431
+ # Restore original value
432
+ actual_value = json.loads(blob_data.decode("utf-8"))
433
+ restored[k] = actual_value
434
+ logger.debug(f"Restored large property '{k}' from blob: {blob_key}")
435
+
436
+ continue
437
+
438
+ except Exception as e:
439
+ logger.error(f"Failed to restore overflow property '{k}': {e}")
440
+ # Fall back to raw metadata instead of crashing
441
+ restored[k] = v
442
+ continue
443
+
444
+ # Normal (non-overflow) property
445
+ restored[k] = v
446
+
447
+ return restored
448
+
377
449
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
378
450
  def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
379
451
  try:
380
452
  self._table_client = self._get_table_client(model)
381
453
  safe_pk = self._sanitize_pk_rk(pk)
382
454
  safe_rk = self._sanitize_pk_rk(rk)
383
-
384
455
  # Pack entity (encoded for Azure Table)
385
456
  entity = self._pack_entity(model, safe_pk, safe_rk, data)
386
-
387
457
  # ---------------------------------------------------
388
458
  # SIZE ESTIMATION (use ORIGINAL payload, not packed)
389
459
  # ---------------------------------------------------
390
460
  MAX_PROPERTY_CHARS = 30 * 1024 # ~30K safe under UTF-16 32K limit
391
- MAX_ENTITY_SIZE = 40 * 1024 # conservative total threshold
392
461
 
393
462
  def _is_large_string(val: Any) -> bool:
394
463
  return isinstance(val, str) and len(val) > MAX_PROPERTY_CHARS
395
464
 
396
- has_large_property = False
397
- large_key = None
398
-
465
+ large_val_dict = {}
399
466
  for key, value in entity.items():
400
467
  if _is_large_string(value):
401
- has_large_property = True
402
- large_key = key
403
468
  logger.warning(
404
469
  f"LARGE PROPERTY DETECTED: {key} length={len(value)} chars → forcing blob overflow"
405
470
  )
406
- break
407
-
408
- payload_json = json.dumps(data, default=json_safe)
409
- payload_bytes = payload_json.encode("utf-8")
410
- payload_size = len(payload_bytes)
411
-
412
- force_blob = has_large_property or payload_size > MAX_ENTITY_SIZE
413
-
414
- # ---------------------------------------------------
415
- # BLOB OVERFLOW PATH
416
- # ---------------------------------------------------
417
- if force_blob:
418
- logger.info(
419
- f"Entity exceeds safe limits → using blob storage "
420
- f"(size={payload_size // 1024} KB, large_key={large_key})"
421
- )
422
-
423
- checksum = hashlib.md5(payload_bytes).hexdigest()
424
- blob_key = self._blob_key(safe_pk, safe_rk, checksum)
425
-
426
- # Upload ORIGINAL payload (not packed entity)
427
- self._blob_upload(blob_key, payload_bytes)
428
-
429
- # Build reference entity
430
- reference_entity = {
431
- "PartitionKey": safe_pk,
432
- "RowKey": safe_rk,
433
- _MODEL_FIELD: model.__qualname__,
434
- "_overflow": True,
435
- "_blob_key": blob_key,
436
- "_size": payload_size,
437
- "_checksum": checksum,
438
- "__keymap__": entity.get("__keymap__", "{}"),
439
- }
440
-
441
- # Keep only small queryable fields
442
- for k, v in entity.items():
443
- if k in ("PartitionKey", "RowKey", "__keymap__", _MODEL_FIELD):
444
- continue
445
- if k.startswith("_"):
446
- continue
447
-
448
- if isinstance(v, (str, bool, int, float)):
449
- if isinstance(v, str) and len(v) > 2000:
450
- continue
451
- reference_entity[k] = v
452
-
453
- elif isinstance(v, datetime):
454
- reference_entity[k] = v
455
- self._table_client = self._get_table_client(model)
456
- self._table_client.upsert_entity(reference_entity)
457
-
458
- logger.info(f"Stored in blob: {blob_key} ({payload_size // 1024} KB)")
459
-
460
- restored = self._unpack_entity(entity)
461
- restored["_overflow"] = True
462
- restored["_blob_key"] = blob_key
463
- restored["_checksum"] = checksum
464
- restored["_size"] = payload_size
465
- restored["id"] = safe_rk
466
-
467
- return restored
471
+ payload_json = json.dumps(value, default=json_safe)
472
+ payload_bytes = payload_json.encode("utf-8")
473
+ payload_size = len(payload_bytes)
474
+ checksum = hashlib.md5(payload_bytes).hexdigest()
475
+ val_key = self._sanitize_blob_part(key)
476
+ blob_key = self._blob_key(f"{safe_pk}_{safe_rk}", val_key, checksum)
477
+ self._blob_upload(blob_key, payload_bytes)
478
+ logger.info(f"Stored in blob: {blob_key} ({payload_size // 1024} KB)")
479
+ large_val_dict[key] = {
480
+ "PartitionKey": safe_pk,
481
+ "RowKey": safe_rk,
482
+ _MODEL_FIELD: model.__qualname__,
483
+ "_overflow": True,
484
+ "_blob_key": blob_key,
485
+ "_size": payload_size,
486
+ "_checksum": checksum,
487
+ }
488
+
489
+ reference_entity = {
490
+ _MODEL_FIELD: model.__qualname__,
491
+ }
492
+ for k, v in entity.items():
493
+ if k.startswith("_"):
494
+ continue
495
+ if k in large_val_dict:
496
+ metadata = large_val_dict[k]
497
+ reference_entity[k] = json.dumps(metadata, default=json_safe) # ← JSON string
498
+ logger.info(f"Overflow reference stored for {k} → {metadata['_blob_key']}")
499
+ else:
500
+ reference_entity[k] = v
468
501
 
469
- # ---------------------------------------------------
470
- # NORMAL TABLE STORAGE
471
- # ---------------------------------------------------
472
- self._table_client.upsert_entity(entity)
502
+ self._table_client = self._get_table_client(model)
503
+ self._table_client.upsert_entity(reference_entity)
473
504
 
474
505
  restored = self._unpack_entity(entity)
475
506
  restored["id"] = safe_rk
@@ -488,55 +519,37 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
488
519
  safe_pk = self._sanitize_pk_rk(pk)
489
520
  safe_rk = self._sanitize_pk_rk(rk)
490
521
  self._table_client = self._get_table_client(model)
522
+
491
523
  entity = self._table_client.get_entity(safe_pk, safe_rk)
492
524
  entity_dict = dict(entity)
493
525
 
494
- # model isolation
526
+ # Model isolation check
495
527
  if entity_dict.get(_MODEL_FIELD) != model.__qualname__:
496
528
  return None
497
529
 
498
- if entity_dict.get("_overflow"):
499
- blob_key = entity_dict.get("_blob_key")
500
- checksum = entity_dict.get("_checksum")
501
- if not blob_key:
502
- raise NoSQLError("Overflow entity missing _blob_key")
503
-
504
- blob_data = self._blob_download(blob_key)
505
- actual_checksum = hashlib.md5(blob_data).hexdigest()
506
- if checksum and actual_checksum != checksum:
507
- raise NoSQLError(
508
- f"Checksum mismatch: expected {checksum}, got {actual_checksum}"
509
- )
510
-
511
- restored = json.loads(blob_data.decode("utf-8"))
512
- out = self._unpack_entity(restored)
513
- out["_overflow"] = True
514
- out["_blob_key"] = blob_key
515
- out["_checksum"] = checksum
516
- if "id" not in out:
517
- out["id"] = safe_rk
518
- return out
530
+ # Restore any large fields that were moved to blob
531
+ restored_entity = self._restore_overflow_properties(entity_dict)
519
532
 
520
- out = self._unpack_entity(entity_dict)
533
+ out = self._unpack_entity(restored_entity)
521
534
  if "id" not in out:
522
535
  out["id"] = safe_rk
536
+
523
537
  return out
524
538
 
525
539
  except Exception as e:
526
540
  if "ResourceNotFound" in str(e):
527
541
  return None
528
542
  raise NoSQLError(f"Azure Table get failed: {str(e)}")
529
-
543
+
530
544
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
531
545
  def _query_raw(
532
546
  self, model: type, filters: Dict[str, Any], limit: Optional[int]
533
547
  ) -> List[JsonDict]:
534
548
  try:
535
549
  self._table_client = self._get_table_client(model)
536
- # always enforce model filter
537
- eff_filters = dict(filters or {})
538
- eff_filters[_MODEL_FIELD] = model.__qualname__
539
550
 
551
+ # Build query filter (your original logic kept unchanged)
552
+ eff_filters = dict(filters or {})
540
553
  parts: List[str] = []
541
554
  for orig_k, orig_v in eff_filters.items():
542
555
  if orig_v is None:
@@ -548,7 +561,9 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
548
561
  sk = "RowKey"
549
562
  else:
550
563
  sk = (
551
- self._sanitize_prop_name(orig_k) if orig_k != _MODEL_FIELD else _MODEL_FIELD
564
+ self._sanitize_prop_name(orig_k)
565
+ if orig_k != _MODEL_FIELD
566
+ else _MODEL_FIELD
552
567
  )
553
568
 
554
569
  ev = self._encode_value(orig_v)
@@ -570,40 +585,21 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
570
585
 
571
586
  query_filter = " and ".join(parts) if parts else None
572
587
 
573
- entities = self._table_client.query_entities(query_filter=query_filter) # type: ignore
588
+ entities = self._table_client.query_entities(query_filter=query_filter)
574
589
 
575
590
  results: List[JsonDict] = []
576
591
  count = 0
577
592
  for ent in entities:
578
593
  ent_dict = dict(ent)
579
594
 
580
- # defensive: enforce model even if query_filter omitted
581
- if ent_dict.get(_MODEL_FIELD) != model.__qualname__:
582
- continue
595
+ # Restore any large fields from blob
596
+ restored_entity = self._restore_overflow_properties(ent_dict)
583
597
 
584
- if ent_dict.get("_overflow"):
585
- blob_key = ent_dict.get("_blob_key")
586
- checksum = ent_dict.get("_checksum")
587
- if blob_key:
588
- try:
589
- blob_data = self._blob_download(blob_key)
590
- actual_checksum = hashlib.md5(blob_data).hexdigest()
591
- if checksum and actual_checksum != checksum:
592
- raise NoSQLError("Checksum mismatch")
593
-
594
- restored = json.loads(blob_data.decode("utf-8"))
595
- out = self._unpack_entity(restored)
596
- except Exception as e:
597
- logger.error(f"Blob read failed, falling back to table: {e}")
598
- out = self._unpack_entity(ent_dict)
599
- else:
600
- out = self._unpack_entity(ent_dict)
601
- else:
602
- out = self._unpack_entity(ent_dict)
598
+ out = self._unpack_entity(restored_entity)
603
599
 
604
- # guarantee id
605
- if "id" not in out and out.get("RowKey") is not None:
606
- out["id"] = out["RowKey"]
600
+ # Guarantee 'id' field
601
+ if "id" not in out and "RowKey" in ent_dict:
602
+ out["id"] = ent_dict["RowKey"]
607
603
 
608
604
  results.append(out)
609
605
 
@@ -615,7 +611,6 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
615
611
 
616
612
  except Exception as e:
617
613
  raise NoSQLError(f"Azure Table query failed: {str(e)}")
618
-
619
614
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
620
615
  def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
621
616
  try:
@@ -36,8 +36,8 @@ class NoSQLKVAdapter:
36
36
  def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
37
37
  """Extract PK/RK from model metadata"""
38
38
  meta = getattr(model, "__polydb__", {})
39
- pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
40
- rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
39
+ pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
40
+ rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
41
41
 
42
42
  if self.partition_config:
43
43
  try:
@@ -217,8 +217,7 @@ class NoSQLKVAdapter:
217
217
  # Protocol implementation
218
218
  def put(self, model: type, data: JsonDict) -> JsonDict:
219
219
  pk, rk = self._get_pk_rk(model, data)
220
- store_data, _ = self._check_overflow(data)
221
- return self._put_raw(model, pk, rk, store_data)
220
+ return self._put_raw(model, pk, rk, data)
222
221
 
223
222
  def query(
224
223
  self,
@@ -257,9 +256,9 @@ class NoSQLKVAdapter:
257
256
  if isinstance(entity_id, dict):
258
257
  pk = entity_id.get("partition_key") or entity_id.get("pk")
259
258
  rk = entity_id.get("row_key") or entity_id.get("rk") or entity_id.get("id")
260
-
259
+
261
260
  else:
262
- pk, rk = self._get_pk_rk(model, {"id": entity_id})
261
+ pk, rk = self._get_pk_rk(model, {"id": entity_id, **data})
263
262
 
264
263
  if not replace:
265
264
  existing = self._get_raw(model, pk, rk) # type: ignore
@@ -554,18 +554,38 @@ class DatabaseFactory:
554
554
  after_plain = None
555
555
  success = False
556
556
  error: Optional[str] = None
557
-
558
557
  def _op() -> JsonDict:
559
558
  nonlocal after_plain, success
560
559
  if self._is_sql(meta, engine_override):
561
560
  result = adapters.sql.update(meta.table, entity_id, data)
562
561
  else:
562
+ pkey = data.get("PartitionKey") or data.get("partition_key") or data.get("pk")
563
+ en_id = entity_id
564
+ if not pkey and before:
565
+ pkey = (
566
+ before.get("PartitionKey")
567
+ or before.get("partition_key")
568
+ or before.get("pk")
569
+ )
570
+ if pkey:
571
+ if isinstance(en_id, dict):
572
+ en_pk = (
573
+ en_id.get("PartitionKey")
574
+ or en_id.get("partition_key")
575
+ or en_id.get("pk")
576
+ )
577
+ if not en_pk:
578
+ en_id["partition_key"] = pkey
579
+ elif isinstance(en_id, str):
580
+ en_id = {"partition_key": pkey, "id": entity_id}
581
+
563
582
  cls = (
564
583
  model
565
584
  if isinstance(model, type)
566
585
  else type(name, (), {"__polydb__": meta.__dict__})
567
586
  )
568
- result = adapters.nosql.patch(cls, entity_id, data, etag=etag, replace=replace)
587
+
588
+ result = adapters.nosql.patch(cls, en_id, data, etag=etag, replace=replace)
569
589
  after_plain = result
570
590
  if self.encryption and encrypted_fields:
571
591
  after_plain = self.encryption.decrypt_fields(result, encrypted_fields)