altcodepro-polydb-python 2.5.8__tar.gz → 2.5.10__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 (117) hide show
  1. {altcodepro_polydb_python-2.5.8/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.10}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +2 -0
  5. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/__init__.py +1 -1
  6. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AzureTableStorageAdapter.py +47 -1
  7. altcodepro_polydb_python-2.5.10/tests/test_azure_table_id_property_persisted.py +175 -0
  8. altcodepro_polydb_python-2.5.10/tests/test_azure_table_id_return_value_sanitization_mismatch.py +151 -0
  9. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/LICENSE +0 -0
  10. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/MANIFEST.in +0 -0
  11. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/README.md +0 -0
  12. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements-aws.txt +0 -0
  13. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements-azure.txt +0 -0
  14. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements-dev.txt +0 -0
  15. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements-gcp.txt +0 -0
  16. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements-generic.txt +0 -0
  17. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/requirements.txt +0 -0
  18. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/setup.cfg +0 -0
  19. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/setup.py +0 -0
  20. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  21. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  22. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  23. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/PolyDB.py +0 -0
  24. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
  25. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AzureKeyVaultAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/EFSAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/S3Adapter.py +0 -0
  45. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/SQSAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/VaultAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/adapters/__init__.py +0 -0
  53. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/advanced_query.py +0 -0
  54. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/audit/AuditStorage.py +0 -0
  55. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/audit/__init__.py +0 -0
  56. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/audit/context.py +0 -0
  57. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/audit/manager.py +0 -0
  58. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/audit/models.py +0 -0
  59. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  60. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  61. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/QueueAdapter.py +0 -0
  62. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/SecretsAdapter.py +0 -0
  63. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/SharedFilesAdapter.py +0 -0
  64. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/base/__init__.py +0 -0
  65. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/batch.py +0 -0
  66. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/cache.py +0 -0
  67. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/cloudDatabaseFactory.py +0 -0
  68. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/databaseFactory.py +0 -0
  69. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/decorators.py +0 -0
  70. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/errors.py +0 -0
  71. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/json_safe.py +0 -0
  72. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/models.py +0 -0
  73. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/monitoring.py +0 -0
  74. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/multitenancy.py +0 -0
  75. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/observability/__init__.py +0 -0
  76. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/observability/logging.py +0 -0
  77. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/py.typed +0 -0
  78. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/query.py +0 -0
  79. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/registry.py +0 -0
  80. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/retry.py +0 -0
  81. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/schema.py +0 -0
  82. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/security.py +0 -0
  83. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/services/__init__.py +0 -0
  84. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/services/compliance_service.py +0 -0
  85. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/services/security_service.py +0 -0
  86. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/types.py +0 -0
  87. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/utils.py +0 -0
  88. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/src/polydb/validation.py +0 -0
  89. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_advanced_query_injection.py +0 -0
  90. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_atomic_decrement.py +0 -0
  91. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_audit_chain_cutover.py +0 -0
  92. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_audit_hmac.py +0 -0
  93. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_aws.py +0 -0
  94. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_azure.py +0 -0
  95. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_azure_queue_receipt_handle.py +0 -0
  96. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_azure_table_empty_filter.py +0 -0
  97. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_azure_table_unpack_entity.py +0 -0
  98. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_blockchain.py +0 -0
  99. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_cloud_factory.py +0 -0
  100. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_field_encryption.py +0 -0
  101. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_gcp.py +0 -0
  102. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_kafka.py +0 -0
  103. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_like_pattern_escaping.py +0 -0
  104. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_mongodb.py +0 -0
  105. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_multi_engine.py +0 -0
  106. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_multitenancy_ddl_injection.py +0 -0
  107. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_nosql_operator_injection.py +0 -0
  108. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_observability.py +0 -0
  109. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_postgresql.py +0 -0
  110. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_queue_extend_delay_cancel.py +0 -0
  111. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_rabbitmq.py +0 -0
  112. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_redis_kv.py +0 -0
  113. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_schema_ddl_injection.py +0 -0
  114. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_secrets_aws.py +0 -0
  115. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_secrets_vault.py +0 -0
  116. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/tests/test_session_vars.py +0 -0
  117. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.10}/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.5.8
3
+ Version: 2.5.10
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.5.8"
7
+ version = "2.5.10"
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.5.8
3
+ Version: 2.5.10
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
@@ -90,6 +90,8 @@ tests/test_aws.py
90
90
  tests/test_azure.py
91
91
  tests/test_azure_queue_receipt_handle.py
92
92
  tests/test_azure_table_empty_filter.py
93
+ tests/test_azure_table_id_property_persisted.py
94
+ tests/test_azure_table_id_return_value_sanitization_mismatch.py
93
95
  tests/test_azure_table_unpack_entity.py
94
96
  tests/test_blockchain.py
95
97
  tests/test_cloud_factory.py
@@ -4,7 +4,7 @@ PolyDB - Enterprise Cloud-Independent Database Abstraction
4
4
  Full LINQ support, field-level audit, cache, soft delete, overflow storage
5
5
  """
6
6
 
7
- __version__ = "2.5.8"
7
+ __version__ = "2.5.10"
8
8
 
9
9
  from .cloudDatabaseFactory import CloudDatabaseFactory
10
10
  from .databaseFactory import DatabaseFactory
@@ -493,6 +493,33 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
493
493
  table_client = self._get_table_client(model)
494
494
  safe_pk = self._sanitize_pk_rk(pk)
495
495
  safe_rk = self._sanitize_pk_rk(rk)
496
+ # Persist "id" as a REAL, queryable property whenever the
497
+ # caller's own data didn't already supply one -- not just
498
+ # synthesized onto this call's own return value below
499
+ # (`restored["id"] = safe_rk`), which only ever helped the
500
+ # ONE caller holding that return value. _query_raw already
501
+ # does the equivalent synthesis on the READ side (`out["id"]
502
+ # = ent_dict["RowKey"]` when absent) for a query's own
503
+ # results, but that can't fix a *filtered* read: a later
504
+ # `read_one(model, {"id": X, ...})` -- the ordinary
505
+ # id-addressed lookup every `update()`/`patch()` call makes
506
+ # for its own "before" read -- builds a real OData `id eq
507
+ # 'X'` filter against whatever property is actually named
508
+ # "id" on the stored entity. When the original create's own
509
+ # data never included one (true for essentially any model
510
+ # whose caller doesn't explicitly generate an id, default
511
+ # pk_field/rk_field or a custom mapping alike), that property
512
+ # never existed, so the filter matched nothing -- `before`
513
+ # came back None, and DatabaseFactory.update()'s own pk/rk
514
+ # recovery (see this file's own 2.5.7 changelog entry for the
515
+ # closely related bug) had nothing to recover from, falling
516
+ # through to a literal `str(None) == "None"` PartitionKey.
517
+ # Reproduced directly before this fix, not assumed: a create
518
+ # with no "id" in its own payload, followed by
519
+ # read_one(model, {"id": <the id the create call itself
520
+ # returned>}), always returned None.
521
+ if "id" not in data:
522
+ data = {**data, "id": safe_rk}
496
523
  entity = self._pack_entity(model, safe_pk, safe_rk, data)
497
524
  MAX_PROPERTY_CHARS = 30 * 1024
498
525
 
@@ -539,7 +566,26 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
539
566
  table_client.upsert_entity(reference_entity)
540
567
 
541
568
  restored = self._unpack_entity(model, entity)
542
- restored["id"] = safe_rk
569
+ # Mirror the write-side condition above (line ~521): only
570
+ # synthesize "id" onto the return value when the caller's own
571
+ # data didn't already supply one. Unconditionally overwriting
572
+ # it with safe_rk here was a real, reproduced bug -- whenever
573
+ # the caller's own id needed RowKey sanitization (contains a
574
+ # character Azure forbids in a RowKey: ':', ' ', '#', '?',
575
+ # '/', '\'), the PERSISTED "id" property kept the caller's
576
+ # original, unsanitized value (data["id"], written untouched
577
+ # via reference_entity above) while this return value handed
578
+ # back the sanitized safe_rk instead -- two different strings
579
+ # for the same row. Any caller holding onto that return
580
+ # value's own "id" (e.g. building a follow-up
581
+ # read_one(model, {"id": ...}) call, or an id.map() over a
582
+ # create_many result) would then filter for a value that
583
+ # never matches any real stored "id" property, silently
584
+ # getting nothing back. Reproduced directly before this fix:
585
+ # data={"id": "a:b"} on a model with a rk_field whose value
586
+ # contains ":" persisted "id"="a:b" but returned "id"="a_b".
587
+ if "id" not in data:
588
+ restored["id"] = safe_rk
543
589
 
544
590
  return restored
545
591
 
@@ -0,0 +1,175 @@
1
+ """Unit tests (no Azurite needed) for a real, reproduced bug: `_put_raw`
2
+ never persisted a real "id" property on the entity unless the caller's own
3
+ write payload already had one -- only synthesized it onto that one call's
4
+ own RETURN value (`restored["id"] = safe_rk`). A later id-addressed lookup
5
+ (`read_one(model, {"id": X, ...})`, the "before" read every
6
+ `DatabaseFactory.update()`/`patch()` call makes) builds a real OData
7
+ `id eq 'X'` filter against whatever property is actually named "id" on the
8
+ stored entity -- which, for any model whose caller doesn't explicitly set
9
+ "id" at create time (default pk_field/rk_field or a custom deterministic
10
+ mapping alike), never existed. `before` therefore always came back None,
11
+ cascading into `DatabaseFactory.update()`'s own pk/rk recovery logic
12
+ finding nothing to recover from and falling through to a literal
13
+ `str(None) == "None"` PartitionKey -- the same class of bug this file's own
14
+ 2.5.7 changelog entry already documents for a related codepath.
15
+
16
+ Reproduces the bug on the OLD code shape directly (a create whose own
17
+ payload has no "id" never lands a real "id" property), then proves the fix
18
+ closes it (a create with no "id" DOES land a real, filterable "id"
19
+ property equal to the physical RowKey, and a create that ALREADY supplies
20
+ its own "id" is left completely untouched).
21
+ """
22
+
23
+ import pytest
24
+
25
+ pytest.importorskip("azure.data.tables") # adapter needs the SDK to import
26
+
27
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
28
+
29
+
30
+ class _Model:
31
+ __name__ = "users"
32
+
33
+
34
+ class _FakeTableClient:
35
+ """A minimal, real filter-evaluating fake -- unlike
36
+ test_azure_table_empty_filter.py's own fake (which only needs to tell
37
+ a None filter apart from a non-None one), this one has to actually
38
+ match `_query_raw`'s real OData-shaped filter string against stored
39
+ entities, since that's the exact mechanism this bug lives in."""
40
+
41
+ def __init__(self):
42
+ self.store: dict[tuple, dict] = {}
43
+
44
+ def upsert_entity(self, entity, **kw):
45
+ self.store[(entity["PartitionKey"], entity["RowKey"])] = dict(entity)
46
+
47
+ def query_entities(self, query_filter=None, **kw):
48
+ if query_filter is None:
49
+ return list(self.store.values())
50
+ conditions = []
51
+ for part in query_filter.split(" and "):
52
+ prop, _, raw_val = part.partition(" eq ")
53
+ if raw_val.startswith("'") and raw_val.endswith("'"):
54
+ val = raw_val[1:-1].replace("''", "'")
55
+ elif raw_val == "null":
56
+ val = None
57
+ elif raw_val in ("true", "false"):
58
+ val = raw_val == "true"
59
+ else:
60
+ val = float(raw_val) if "." in raw_val else int(raw_val)
61
+ conditions.append((prop, val))
62
+ return [
63
+ e for e in self.store.values() if all(e.get(prop) == val for prop, val in conditions)
64
+ ]
65
+
66
+
67
+ def _adapter(fake):
68
+ adp = object.__new__(AzureTableStorageAdapter) # bypass live-connection __init__
69
+ adp._get_table_client = lambda model: fake
70
+ adp._blob_service = None
71
+ adp.object_storage = None
72
+ return adp
73
+
74
+
75
+ def test_a_create_with_no_id_in_its_own_payload_still_lands_a_real_filterable_id_property():
76
+ """The actual bug: users.identity_key-style deterministic addressing
77
+ (pk_field=rk_field="identity_key", "id" never in the create payload)."""
78
+ fake = _FakeTableClient()
79
+ adapter = _adapter(fake)
80
+
81
+ data = {
82
+ "identity_key": "email:sireesh.psvs@gmail.com",
83
+ "tenant_id": "altcodepro",
84
+ "email": "sireesh.psvs@gmail.com",
85
+ "auth_method": "password",
86
+ }
87
+ created = adapter._put_raw(
88
+ _Model, "email:sireesh.psvs@gmail.com", "email:sireesh.psvs@gmail.com", data
89
+ )
90
+
91
+ returned_id = created["id"]
92
+ assert returned_id == "email_sireesh.psvs@gmail.com" # sanitized (colon -> underscore)
93
+
94
+ # The load-bearing assertion: the STORED entity itself (not just this
95
+ # one call's own return value) now has a real "id" property.
96
+ stored = fake.store[("email_sireesh.psvs@gmail.com", "email_sireesh.psvs@gmail.com")]
97
+ assert stored["id"] == returned_id
98
+
99
+ # And the ordinary id-addressed lookup every update()/patch() "before"
100
+ # read performs now actually finds the row -- reproduced BROKEN before
101
+ # this fix (would have returned []).
102
+ found = adapter._query_raw(_Model, {"id": returned_id, "tenant_id": "altcodepro"}, None)
103
+ assert len(found) == 1
104
+ assert found[0]["email"] == "sireesh.psvs@gmail.com"
105
+
106
+
107
+ def test_a_create_that_already_supplies_its_own_id_is_left_untouched():
108
+ fake = _FakeTableClient()
109
+ adapter = _adapter(fake)
110
+
111
+ data = {"id": "caller-chosen-id", "tenant_id": "t1", "name": "x"}
112
+ created = adapter._put_raw(_Model, "t1", "caller-chosen-id", data)
113
+
114
+ assert created["id"] == "caller-chosen-id"
115
+ stored = fake.store[("t1", "caller-chosen-id")]
116
+ assert stored["id"] == "caller-chosen-id"
117
+
118
+
119
+ def test_two_different_identities_never_collide_and_both_are_independently_findable():
120
+ fake = _FakeTableClient()
121
+ adapter = _adapter(fake)
122
+
123
+ for email in ("sireesh.psvs@gmail.com", "pangaluri2010@gmail.com"):
124
+ key = f"email:{email}"
125
+ adapter._put_raw(
126
+ _Model,
127
+ key,
128
+ key,
129
+ {
130
+ "identity_key": key,
131
+ "tenant_id": "altcodepro",
132
+ "email": email,
133
+ },
134
+ )
135
+
136
+ assert len(fake.store) == 2
137
+ for email in ("sireesh.psvs@gmail.com", "pangaluri2010@gmail.com"):
138
+ rows = adapter._query_raw(_Model, {"email": email}, None)
139
+ assert len(rows) == 1 and rows[0]["email"] == email
140
+
141
+
142
+ def test_repeat_put_for_the_same_identity_key_upserts_the_same_row_not_a_duplicate():
143
+ """The actual, originally-reported symptom this whole chain of fixes
144
+ exists to close: a second signup attempt for the same email must
145
+ collapse onto the SAME physical row, never mint a new one."""
146
+ fake = _FakeTableClient()
147
+ adapter = _adapter(fake)
148
+
149
+ key = "email:sireesh.psvs@gmail.com"
150
+ adapter._put_raw(
151
+ _Model,
152
+ key,
153
+ key,
154
+ {
155
+ "identity_key": key,
156
+ "tenant_id": "altcodepro",
157
+ "email": "sireesh.psvs@gmail.com",
158
+ "display_name": "Sireesh",
159
+ },
160
+ )
161
+ adapter._put_raw(
162
+ _Model,
163
+ key,
164
+ key,
165
+ {
166
+ "identity_key": key,
167
+ "tenant_id": "altcodepro",
168
+ "email": "sireesh.psvs@gmail.com",
169
+ "display_name": "Sireesh Updated",
170
+ },
171
+ )
172
+
173
+ assert len(fake.store) == 1
174
+ only_row = next(iter(fake.store.values()))
175
+ assert only_row["display_name"] == "Sireesh Updated"
@@ -0,0 +1,151 @@
1
+ """Unit tests (no Azurite needed) for a real, reproduced bug: `_put_raw`
2
+ unconditionally overwrote its own RETURN value's "id" field with `safe_rk`
3
+ (the Azure-RowKey-sanitized value) even when the caller's own write payload
4
+ already supplied a real "id" -- so whenever that caller-supplied id needed
5
+ RowKey sanitization (contains a character Azure forbids in a RowKey: ':',
6
+ ' ', '#', '?', '/', '\\'), the PERSISTED entity's "id" property (written
7
+ verbatim from the caller's own `data`, untouched) and the RETURNED value's
8
+ "id" (always `safe_rk`) silently disagreed.
9
+
10
+ This is a distinct bug from the one `test_azure_table_id_property_persisted.py`
11
+ already covers and fixed (2.5.9: a create with NO "id" in its own payload
12
+ never landed a real "id" property at all) -- that fix's own condition
13
+ (`if "id" not in data: data = {**data, "id": safe_rk}`) only ever touches
14
+ the WRITE side. The return-value line right below it (`restored["id"] =
15
+ safe_rk`) had no matching condition, so it fired even when `data["id"]`
16
+ was already present and different from `safe_rk`.
17
+
18
+ Real, reproduced downstream symptom (altcodepro-blueprint-engine): the
19
+ `artifacts` model deliberately builds a colon-joined deterministic id
20
+ (`project_id + ':' + definition_id`) with `rk_field` pointing at that same
21
+ value. `core.db.create_many`'s own CEL-built row data already supplies
22
+ this "id" explicitly. The persisted entity's "id" property therefore kept
23
+ the real, colon-joined value -- but every returned row's own "id" (what a
24
+ workflow's `steps.<create_step>.created.map(c, ...c.id...)` reads to
25
+ enqueue a follow-up job) came back with the colon sanitized to an
26
+ underscore instead. The follow-up job's own `core.db.get(id=<that wrong,
27
+ sanitized value>)` then built an OData `id eq '<sanitized>'` filter that
28
+ matched nothing -- every real row existed, but was unreachable by the very
29
+ id its own creation step just handed out.
30
+ """
31
+
32
+ import pytest
33
+
34
+ pytest.importorskip("azure.data.tables") # adapter needs the SDK to import
35
+
36
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
37
+
38
+
39
+ class _Model:
40
+ __name__ = "artifacts"
41
+
42
+
43
+ class _FakeTableClient:
44
+ """The same real filter-evaluating fake
45
+ test_azure_table_id_property_persisted.py already establishes -- reused
46
+ verbatim rather than re-derived, since this bug lives in the identical
47
+ `_query_raw` OData-filter mechanism."""
48
+
49
+ def __init__(self):
50
+ self.store: dict[tuple, dict] = {}
51
+
52
+ def upsert_entity(self, entity, **kw):
53
+ self.store[(entity["PartitionKey"], entity["RowKey"])] = dict(entity)
54
+
55
+ def query_entities(self, query_filter=None, **kw):
56
+ if query_filter is None:
57
+ return list(self.store.values())
58
+ conditions = []
59
+ for part in query_filter.split(" and "):
60
+ prop, _, raw_val = part.partition(" eq ")
61
+ if raw_val.startswith("'") and raw_val.endswith("'"):
62
+ val = raw_val[1:-1].replace("''", "'")
63
+ elif raw_val == "null":
64
+ val = None
65
+ elif raw_val in ("true", "false"):
66
+ val = raw_val == "true"
67
+ else:
68
+ val = float(raw_val) if "." in raw_val else int(raw_val)
69
+ conditions.append((prop, val))
70
+ return [
71
+ e for e in self.store.values() if all(e.get(prop) == val for prop, val in conditions)
72
+ ]
73
+
74
+
75
+ def _adapter(fake):
76
+ adp = object.__new__(AzureTableStorageAdapter) # bypass live-connection __init__
77
+ adp._get_table_client = lambda model: fake
78
+ adp._blob_service = None
79
+ adp.object_storage = None
80
+ return adp
81
+
82
+
83
+ def test_a_caller_supplied_id_needing_rowkey_sanitization_is_returned_unsanitized():
84
+ """The actual reported bug, reproduced with the real shape:
85
+ project_id + ':' + definition_id, used as both "id" and the rk_field
86
+ value -- ':' is not a legal Azure RowKey character."""
87
+ fake = _FakeTableClient()
88
+ adapter = _adapter(fake)
89
+
90
+ caller_id = "485533b9-8d0f-4e5f-92da-670cedfe92f5:backend.backend_schema"
91
+ data = {
92
+ "id": caller_id,
93
+ "artifact_id": caller_id,
94
+ "project_slug": "community-platform-187",
95
+ "tenant_id": "altcodepro",
96
+ "name": "Backend Schema",
97
+ }
98
+ created = adapter._put_raw(_Model, "community-platform-187", caller_id, data)
99
+
100
+ # The load-bearing assertion: the RETURN value's "id" must equal what
101
+ # the caller actually supplied -- never the sanitized RowKey. Before
102
+ # the fix this was "485533b9-...-670cedfe92f5_backend.backend_schema"
103
+ # (colon silently replaced with underscore).
104
+ assert created["id"] == caller_id
105
+
106
+ # And it must agree with what's genuinely persisted -- the real
107
+ # invariant this bug violated (two different strings for the same
108
+ # row's own "id").
109
+ safe_rk = adapter._sanitize_pk_rk(caller_id)
110
+ stored = fake.store[("community-platform-187", safe_rk)]
111
+ assert stored["id"] == caller_id == created["id"]
112
+
113
+ # The actual downstream failure this caused: a follow-up step (e.g.
114
+ # start_artifact_generation's own load_artifact) doing
115
+ # core.db.get(id=<the id create_many just returned>) must find the
116
+ # row. Reproduced BROKEN before this fix -- querying by the correct,
117
+ # returned id found nothing, because the stored property and the
118
+ # returned value disagreed.
119
+ found = adapter._query_raw(_Model, {"id": created["id"], "tenant_id": "altcodepro"}, None)
120
+ assert len(found) == 1
121
+ assert found[0]["name"] == "Backend Schema"
122
+
123
+
124
+ def test_a_caller_supplied_id_needing_no_sanitization_is_unaffected():
125
+ """Negative control: when the caller's own id is already RowKey-safe,
126
+ safe_rk == the caller's id anyway, so this bug was invisible -- exactly
127
+ why it slipped past every existing test until now."""
128
+ fake = _FakeTableClient()
129
+ adapter = _adapter(fake)
130
+
131
+ data = {"id": "caller-chosen-id", "tenant_id": "t1", "name": "x"}
132
+ created = adapter._put_raw(_Model, "t1", "caller-chosen-id", data)
133
+
134
+ assert created["id"] == "caller-chosen-id"
135
+ stored = fake.store[("t1", "caller-chosen-id")]
136
+ assert stored["id"] == "caller-chosen-id"
137
+
138
+
139
+ def test_no_id_in_payload_still_synthesizes_safe_rk_onto_both_sides():
140
+ """The 2.5.9 case must still work identically -- this fix only adds a
141
+ condition around the return-value line, it must not touch the
142
+ no-id-supplied path at all."""
143
+ fake = _FakeTableClient()
144
+ adapter = _adapter(fake)
145
+
146
+ data = {"identity_key": "email:x@example.com", "tenant_id": "altcodepro"}
147
+ created = adapter._put_raw(_Model, "email:x@example.com", "email:x@example.com", data)
148
+
149
+ assert created["id"] == "email_x@example.com"
150
+ stored = fake.store[("email_x@example.com", "email_x@example.com")]
151
+ assert stored["id"] == created["id"] == "email_x@example.com"