altcodepro-polydb-python 2.3.26__tar.gz → 2.3.27__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 (92) hide show
  1. {altcodepro_polydb_python-2.3.26/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.27}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  5. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/AzureTableStorageAdapter.py +24 -17
  6. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/base/NoSQLKVAdapter.py +16 -0
  7. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/databaseFactory.py +45 -3
  8. altcodepro_polydb_python-2.3.27/tests/test_azure_table_unpack_entity.py +76 -0
  9. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/LICENSE +0 -0
  10. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/MANIFEST.in +0 -0
  11. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/README.md +0 -0
  12. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements-aws.txt +0 -0
  13. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements-azure.txt +0 -0
  14. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements-dev.txt +0 -0
  15. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements-gcp.txt +0 -0
  16. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements-generic.txt +0 -0
  17. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/requirements.txt +0 -0
  18. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/setup.cfg +0 -0
  19. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/setup.py +0 -0
  20. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  21. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  22. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  23. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/PolyDB.py +0 -0
  24. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/__init__.py +0 -0
  25. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/EFSAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/S3Adapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/SQSAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/adapters/__init__.py +0 -0
  48. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/advanced_query.py +0 -0
  49. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/audit/AuditStorage.py +0 -0
  50. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/audit/__init__.py +0 -0
  51. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/audit/context.py +0 -0
  52. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/audit/manager.py +0 -0
  53. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/audit/models.py +0 -0
  54. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  55. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/base/QueueAdapter.py +0 -0
  56. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/base/SharedFilesAdapter.py +0 -0
  57. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/base/__init__.py +0 -0
  58. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/batch.py +0 -0
  59. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/cache.py +0 -0
  60. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/cloudDatabaseFactory.py +0 -0
  61. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/decorators.py +0 -0
  62. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/errors.py +0 -0
  63. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/json_safe.py +0 -0
  64. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/models.py +0 -0
  65. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/monitoring.py +0 -0
  66. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/multitenancy.py +0 -0
  67. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/observability/__init__.py +0 -0
  68. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/observability/logging.py +0 -0
  69. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/py.typed +0 -0
  70. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/query.py +0 -0
  71. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/registry.py +0 -0
  72. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/retry.py +0 -0
  73. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/schema.py +0 -0
  74. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/security.py +0 -0
  75. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/services/__init__.py +0 -0
  76. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/services/compliance_service.py +0 -0
  77. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/services/security_service.py +0 -0
  78. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/types.py +0 -0
  79. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/utils.py +0 -0
  80. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/src/polydb/validation.py +0 -0
  81. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_atomic_decrement.py +0 -0
  82. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_aws.py +0 -0
  83. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_azure.py +0 -0
  84. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_azure_table_empty_filter.py +0 -0
  85. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_blockchain.py +0 -0
  86. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_cloud_factory.py +0 -0
  87. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_gcp.py +0 -0
  88. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_mongodb.py +0 -0
  89. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_multi_engine.py +0 -0
  90. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_observability.py +0 -0
  91. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/tests/test_postgresql.py +0 -0
  92. {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.27}/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.26
3
+ Version: 2.3.27
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.26"
7
+ version = "2.3.27"
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.26
3
+ Version: 2.3.27
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
@@ -79,6 +79,7 @@ tests/test_atomic_decrement.py
79
79
  tests/test_aws.py
80
80
  tests/test_azure.py
81
81
  tests/test_azure_table_empty_filter.py
82
+ tests/test_azure_table_unpack_entity.py
82
83
  tests/test_blockchain.py
83
84
  tests/test_cloud_factory.py
84
85
  tests/test_gcp.py
@@ -255,7 +255,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
255
255
 
256
256
  return entity
257
257
 
258
- def _unpack_entity(self, entity: JsonDict) -> JsonDict:
258
+ def _unpack_entity(self, model: type, entity: JsonDict) -> JsonDict:
259
259
  if not entity:
260
260
  return {}
261
261
 
@@ -263,6 +263,9 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
263
263
  raw.pop("etag", None)
264
264
  raw.pop("ETag", None)
265
265
  raw.pop("Timestamp", None)
266
+ # Internal: which model a row belongs to, used only to isolate models
267
+ # sharing a table. Never part of the public record shape.
268
+ raw.pop(_MODEL_FIELD, None)
266
269
 
267
270
  keymap_str = raw.pop("__keymap__", None)
268
271
  keymap: Dict[str, str] = {}
@@ -272,26 +275,30 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
272
275
  except Exception:
273
276
  keymap = {}
274
277
 
275
- out: JsonDict = {}
276
-
277
- pk = raw.get("PartitionKey")
278
- rk = raw.get("RowKey")
279
- if pk is not None:
280
- out["PartitionKey"] = pk
281
- if rk is not None:
282
- out["RowKey"] = rk
278
+ pk = raw.pop("PartitionKey", None)
279
+ rk = raw.pop("RowKey", None)
283
280
 
281
+ out: JsonDict = {}
284
282
  for k, v in raw.items():
285
- if k in ("PartitionKey", "RowKey"):
286
- continue
287
-
288
- if k.startswith("_") or k in (_MODEL_FIELD,):
283
+ if k.startswith("_"):
289
284
  out[k] = v
290
285
  continue
291
286
 
292
287
  orig_key = keymap.get(k, k)
293
288
  out[orig_key] = self._decode_value(v)
294
289
 
290
+ # PartitionKey/RowKey are Azure Table Storage's own physical column
291
+ # names — never return them as-is. Map each back to the domain field
292
+ # it was derived from (model's declared partition_key/sort_key, same
293
+ # resolution _get_pk_rk used to write them), so the same field name
294
+ # round-trips on read as it did on write. Guarded so we never clobber
295
+ # a real property that already carries the value (the common case —
296
+ # pk/rk fields are usually also stored as regular properties).
297
+ pk_field, rk_field = self._pk_rk_field_names(model)
298
+ if pk is not None and pk_field not in out:
299
+ out[pk_field] = pk
300
+ if rk is not None and rk_field not in out:
301
+ out[rk_field] = rk
295
302
  if "id" not in out and rk is not None:
296
303
  out["id"] = rk
297
304
 
@@ -491,7 +498,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
491
498
 
492
499
  table_client.upsert_entity(reference_entity)
493
500
 
494
- restored = self._unpack_entity(entity)
501
+ restored = self._unpack_entity(model, entity)
495
502
  restored["id"] = safe_rk
496
503
 
497
504
  return restored
@@ -517,7 +524,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
517
524
 
518
525
  restored_entity = self._restore_overflow_properties(entity_dict)
519
526
 
520
- out = self._unpack_entity(restored_entity)
527
+ out = self._unpack_entity(model, restored_entity)
521
528
  if "id" not in out:
522
529
  out["id"] = safe_rk
523
530
 
@@ -589,7 +596,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
589
596
 
590
597
  restored_entity = self._restore_overflow_properties(ent_dict)
591
598
 
592
- out = self._unpack_entity(restored_entity)
599
+ out = self._unpack_entity(model, restored_entity)
593
600
 
594
601
  if "id" not in out and "RowKey" in ent_dict:
595
602
  out["id"] = ent_dict["RowKey"]
@@ -705,7 +712,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
705
712
  for ent in page:
706
713
  ent_dict = dict(ent)
707
714
  restored = self._restore_overflow_properties(ent_dict)
708
- out = self._unpack_entity(restored)
715
+ out = self._unpack_entity(model, restored)
709
716
  if "id" not in out and "RowKey" in ent_dict:
710
717
  out["id"] = ent_dict["RowKey"]
711
718
  items.append(out)
@@ -34,6 +34,22 @@ class NoSQLKVAdapter:
34
34
  self._lock = threading.Lock()
35
35
  self.max_size = 1024 * 1024 # 1MB
36
36
 
37
+ def _pk_rk_field_names(self, model: type) -> Tuple[str, str]:
38
+ """
39
+ The domain field names PK/RK were derived from — the mirror image of
40
+ _get_pk_rk's field resolution. Adapters use this to translate a
41
+ physical PartitionKey/RowKey (or pk/sk, whatever the backend calls
42
+ them) back into the model's own field name before returning a record,
43
+ instead of leaking storage-internal column names to callers. Falls
44
+ back to the same defaults _get_pk_rk uses (tenant_id / id) when a
45
+ model declares no explicit mapping, so unmapped models still round-
46
+ trip cleanly on upsert without erroring.
47
+ """
48
+ meta = getattr(model, "__polydb__", {})
49
+ pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
50
+ rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
51
+ return pk_field, rk_field
52
+
37
53
  def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
38
54
  """Extract PK/RK from model metadata"""
39
55
  meta = getattr(model, "__polydb__", {})
@@ -543,14 +543,32 @@ class DatabaseFactory:
543
543
  if self._is_sql(meta, engine_override):
544
544
  result = adapters.sql.update(meta.table, entity_id, data)
545
545
  else:
546
+ # A scalar entity_id is usually the record's "id" property,
547
+ # not its physical PartitionKey/RowKey — those come from the
548
+ # model's declared pk_field/rk_field (x-metadata partition_key
549
+ # /sort_key), which for most schema models is a different
550
+ # field entirely (e.g. Artifact: pk=project_slug, rk=
551
+ # template_key). Recover both from the patch payload, or —
552
+ # more reliably — from the record loaded just above as
553
+ # `before`, so a plain id-addressed update() still lands on
554
+ # the entity's real physical key instead of a mismatched
555
+ # default partition / the id used as a row key it never had.
546
556
  pkey = data.get("PartitionKey") or data.get("partition_key") or data.get("pk")
547
- en_id = entity_id
557
+ if not pkey and meta.pk_field:
558
+ pkey = data.get(meta.pk_field)
548
559
  if not pkey and before:
549
560
  pkey = (
550
561
  before.get("PartitionKey")
551
562
  or before.get("partition_key")
552
563
  or before.get("pk")
564
+ or (before.get(meta.pk_field) if meta.pk_field else None)
565
+ )
566
+ rkey = None
567
+ if meta.rk_field and meta.rk_field != "id":
568
+ rkey = data.get(meta.rk_field) or (
569
+ before.get(meta.rk_field) if before else None
553
570
  )
571
+ en_id = entity_id
554
572
  if pkey:
555
573
  if isinstance(en_id, dict):
556
574
  en_pk = (
@@ -560,8 +578,16 @@ class DatabaseFactory:
560
578
  )
561
579
  if not en_pk:
562
580
  en_id["partition_key"] = pkey
581
+ en_rk = (
582
+ en_id.get("RowKey")
583
+ or en_id.get("row_key")
584
+ or en_id.get("rk")
585
+ or en_id.get("id")
586
+ )
587
+ if not en_rk and rkey:
588
+ en_id["row_key"] = rkey
563
589
  elif isinstance(en_id, str):
564
- en_id = {"partition_key": pkey, "id": entity_id}
590
+ en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
565
591
 
566
592
  cls = (
567
593
  model
@@ -702,7 +728,23 @@ class DatabaseFactory:
702
728
  if isinstance(model, type)
703
729
  else type(name, (), {"__polydb__": meta.__dict__})
704
730
  )
705
- result = adapters.nosql.delete(cls, entity_id, etag=etag)
731
+ # Same physical-key recovery as update() (see there for why a
732
+ # scalar entity_id alone isn't enough once a model's pk_field
733
+ # /rk_field differ from "id").
734
+ en_id = entity_id
735
+ if not isinstance(en_id, dict) and before:
736
+ pkey = (
737
+ before.get("PartitionKey")
738
+ or before.get("partition_key")
739
+ or before.get("pk")
740
+ or (before.get(meta.pk_field) if meta.pk_field else None)
741
+ )
742
+ if pkey:
743
+ rkey = None
744
+ if meta.rk_field and meta.rk_field != "id":
745
+ rkey = before.get(meta.rk_field)
746
+ en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
747
+ result = adapters.nosql.delete(cls, en_id, etag=etag)
706
748
  success = True
707
749
  if self._enable_cache and self._cache:
708
750
  try:
@@ -0,0 +1,76 @@
1
+ """Unit tests for AzureTableStorageAdapter._unpack_entity (no Azurite needed).
2
+
3
+ Contract: PartitionKey/RowKey/__polydb_model__ are Azure Table Storage
4
+ internals and must never leak into a returned record. Each gets mapped back
5
+ to the domain field it was derived from (the model's declared
6
+ partition_key/sort_key — the same fields _get_pk_rk used to write them), and
7
+ only fills in that field if it isn't already present from a regular stored
8
+ property. Models with no explicit mapping still round-trip cleanly via the
9
+ same defaults _get_pk_rk uses (tenant_id / id) instead of erroring.
10
+ """
11
+
12
+ import pytest
13
+
14
+ pytest.importorskip("azure.data.tables") # adapter needs the SDK to import
15
+
16
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
17
+
18
+
19
+ class _ArtifactComment:
20
+ __name__ = "ArtifactComment"
21
+ __polydb__ = {"partition_key": "artifact_id", "sort_key": "created_at"}
22
+
23
+
24
+ class _Unmapped:
25
+ __name__ = "Unmapped"
26
+ # no __polydb__ at all
27
+
28
+
29
+ def _adapter():
30
+ return object.__new__(AzureTableStorageAdapter) # bypass live-connection __init__
31
+
32
+
33
+ def test_mapped_model_remaps_pk_rk_and_strips_internals():
34
+ entity = {
35
+ "PartitionKey": "artifact-123",
36
+ "RowKey": "2026-07-13T21_04_53",
37
+ "__polydb_model__": "ArtifactComment",
38
+ "artifact_id": "artifact-123",
39
+ "created_at": "2026-07-13T21:04:53",
40
+ "body": "hello",
41
+ }
42
+ out = _adapter()._unpack_entity(_ArtifactComment, entity)
43
+ assert "PartitionKey" not in out
44
+ assert "RowKey" not in out
45
+ assert "__polydb_model__" not in out
46
+ assert out["artifact_id"] == "artifact-123"
47
+ assert out["created_at"] == "2026-07-13T21:04:53" # real property wins over RowKey
48
+ assert out["body"] == "hello"
49
+
50
+
51
+ def test_unmapped_model_falls_back_without_erroring():
52
+ entity = {
53
+ "PartitionKey": "t1",
54
+ "RowKey": "row1",
55
+ "__polydb_model__": "Unmapped",
56
+ "email": "user@x.com",
57
+ }
58
+ out = _adapter()._unpack_entity(_Unmapped, entity)
59
+ assert "PartitionKey" not in out
60
+ assert "RowKey" not in out
61
+ assert "__polydb_model__" not in out
62
+ assert out["tenant_id"] == "t1"
63
+ assert out["id"] == "row1"
64
+ assert out["email"] == "user@x.com"
65
+
66
+
67
+ def test_pk_rk_fill_in_only_when_field_missing():
68
+ entity = {
69
+ "PartitionKey": "artifact-999",
70
+ "RowKey": "row-1",
71
+ "__polydb_model__": "ArtifactComment",
72
+ "body": "no explicit artifact_id property",
73
+ }
74
+ out = _adapter()._unpack_entity(_ArtifactComment, entity)
75
+ assert out["artifact_id"] == "artifact-999"
76
+ assert out["created_at"] == "row-1"