altcodepro-polydb-python 2.5.8__tar.gz → 2.5.9__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 (116) hide show
  1. {altcodepro_polydb_python-2.5.8/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.9}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  5. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/__init__.py +1 -1
  6. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AzureTableStorageAdapter.py +27 -0
  7. altcodepro_polydb_python-2.5.9/tests/test_azure_table_id_property_persisted.py +175 -0
  8. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/LICENSE +0 -0
  9. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/MANIFEST.in +0 -0
  10. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/README.md +0 -0
  11. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements-aws.txt +0 -0
  12. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements-azure.txt +0 -0
  13. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements-dev.txt +0 -0
  14. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements-gcp.txt +0 -0
  15. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements-generic.txt +0 -0
  16. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/requirements.txt +0 -0
  17. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/setup.cfg +0 -0
  18. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/setup.py +0 -0
  19. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  20. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  21. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  22. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/PolyDB.py +0 -0
  23. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
  24. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  25. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AzureKeyVaultAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/EFSAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/S3Adapter.py +0 -0
  44. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/SQSAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/VaultAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/adapters/__init__.py +0 -0
  52. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/advanced_query.py +0 -0
  53. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/audit/AuditStorage.py +0 -0
  54. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/audit/__init__.py +0 -0
  55. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/audit/context.py +0 -0
  56. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/audit/manager.py +0 -0
  57. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/audit/models.py +0 -0
  58. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  59. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  60. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/QueueAdapter.py +0 -0
  61. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/SecretsAdapter.py +0 -0
  62. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/SharedFilesAdapter.py +0 -0
  63. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/base/__init__.py +0 -0
  64. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/batch.py +0 -0
  65. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/cache.py +0 -0
  66. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/cloudDatabaseFactory.py +0 -0
  67. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/databaseFactory.py +0 -0
  68. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/decorators.py +0 -0
  69. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/errors.py +0 -0
  70. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/json_safe.py +0 -0
  71. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/models.py +0 -0
  72. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/monitoring.py +0 -0
  73. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/multitenancy.py +0 -0
  74. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/observability/__init__.py +0 -0
  75. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/observability/logging.py +0 -0
  76. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/py.typed +0 -0
  77. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/query.py +0 -0
  78. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/registry.py +0 -0
  79. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/retry.py +0 -0
  80. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/schema.py +0 -0
  81. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/security.py +0 -0
  82. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/services/__init__.py +0 -0
  83. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/services/compliance_service.py +0 -0
  84. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/services/security_service.py +0 -0
  85. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/types.py +0 -0
  86. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/utils.py +0 -0
  87. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/src/polydb/validation.py +0 -0
  88. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_advanced_query_injection.py +0 -0
  89. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_atomic_decrement.py +0 -0
  90. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_audit_chain_cutover.py +0 -0
  91. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_audit_hmac.py +0 -0
  92. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_aws.py +0 -0
  93. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_azure.py +0 -0
  94. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_azure_queue_receipt_handle.py +0 -0
  95. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_azure_table_empty_filter.py +0 -0
  96. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_azure_table_unpack_entity.py +0 -0
  97. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_blockchain.py +0 -0
  98. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_cloud_factory.py +0 -0
  99. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_field_encryption.py +0 -0
  100. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_gcp.py +0 -0
  101. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_kafka.py +0 -0
  102. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_like_pattern_escaping.py +0 -0
  103. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_mongodb.py +0 -0
  104. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_multi_engine.py +0 -0
  105. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_multitenancy_ddl_injection.py +0 -0
  106. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_nosql_operator_injection.py +0 -0
  107. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_observability.py +0 -0
  108. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_postgresql.py +0 -0
  109. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_queue_extend_delay_cancel.py +0 -0
  110. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_rabbitmq.py +0 -0
  111. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_redis_kv.py +0 -0
  112. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_schema_ddl_injection.py +0 -0
  113. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_secrets_aws.py +0 -0
  114. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_secrets_vault.py +0 -0
  115. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/tests/test_session_vars.py +0 -0
  116. {altcodepro_polydb_python-2.5.8 → altcodepro_polydb_python-2.5.9}/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.9
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.9"
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.9
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,7 @@ 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
93
94
  tests/test_azure_table_unpack_entity.py
94
95
  tests/test_blockchain.py
95
96
  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.9"
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
 
@@ -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"