altcodepro-polydb-python 2.5.22__tar.gz → 2.5.24__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 (133) hide show
  1. {altcodepro_polydb_python-2.5.22/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.24}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +2 -0
  5. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/__init__.py +1 -1
  6. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AzureTableStorageAdapter.py +23 -1
  7. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/databaseFactory.py +7 -0
  8. altcodepro_polydb_python-2.5.24/tests/test_azure_table_key_field_filter.py +55 -0
  9. altcodepro_polydb_python-2.5.24/tests/test_create_unique_violation.py +48 -0
  10. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_read_projection.py +1 -1
  11. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/LICENSE +0 -0
  12. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/MANIFEST.in +0 -0
  13. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/README.md +0 -0
  14. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements-aws.txt +0 -0
  15. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements-azure.txt +0 -0
  16. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements-dev.txt +0 -0
  17. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements-gcp.txt +0 -0
  18. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements-generic.txt +0 -0
  19. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/requirements.txt +0 -0
  20. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/setup.cfg +0 -0
  21. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/setup.py +0 -0
  22. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  23. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  24. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  25. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/PolyDB.py +0 -0
  26. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AzureKeyVaultAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/EFSAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/S3Adapter.py +0 -0
  47. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/SQSAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/VaultAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  53. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  54. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/adapters/__init__.py +0 -0
  55. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/advanced_query.py +0 -0
  56. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/aio.py +0 -0
  57. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/audit/AuditStorage.py +0 -0
  58. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/audit/__init__.py +0 -0
  59. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/audit/context.py +0 -0
  60. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/audit/manager.py +0 -0
  61. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/audit/models.py +0 -0
  62. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  63. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  64. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/QueueAdapter.py +0 -0
  65. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/SecretsAdapter.py +0 -0
  66. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/SharedFilesAdapter.py +0 -0
  67. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/__init__.py +0 -0
  68. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/base/constraints.py +0 -0
  69. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/batch.py +0 -0
  70. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/cache.py +0 -0
  71. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/cloudDatabaseFactory.py +0 -0
  72. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/errors.py +0 -0
  73. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/json_safe.py +0 -0
  74. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/models.py +0 -0
  75. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/monitoring.py +0 -0
  76. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/multitenancy.py +0 -0
  77. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/observability/__init__.py +0 -0
  78. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/observability/logging.py +0 -0
  79. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/overflow_gc.py +0 -0
  80. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/py.typed +0 -0
  81. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/query.py +0 -0
  82. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/registry.py +0 -0
  83. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/retry.py +0 -0
  84. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/schema.py +0 -0
  85. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/security.py +0 -0
  86. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/services/__init__.py +0 -0
  87. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/services/compliance_service.py +0 -0
  88. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/services/security_service.py +0 -0
  89. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/types.py +0 -0
  90. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/utils.py +0 -0
  91. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/src/polydb/validation.py +0 -0
  92. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_advanced_query_injection.py +0 -0
  93. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_async_api.py +0 -0
  94. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_atomic_decrement.py +0 -0
  95. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_audit_chain_cutover.py +0 -0
  96. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_audit_hmac.py +0 -0
  97. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_aws.py +0 -0
  98. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure.py +0 -0
  99. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_keyvault_credential.py +0 -0
  100. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_queue_receipt_handle.py +0 -0
  101. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_empty_filter.py +0 -0
  102. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_id_property_persisted.py +0 -0
  103. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_id_return_value_sanitization_mismatch.py +0 -0
  104. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_large_record_patch_roundtrip.py +0 -0
  105. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_self_heal.py +0 -0
  106. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_azure_table_unpack_entity.py +0 -0
  107. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_blockchain.py +0 -0
  108. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_blockchain_overflow_guard.py +0 -0
  109. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_cloud_factory.py +0 -0
  110. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_factory_wiring.py +0 -0
  111. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_field_encryption.py +0 -0
  112. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_gcp.py +0 -0
  113. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_kafka.py +0 -0
  114. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_like_pattern_escaping.py +0 -0
  115. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_mongodb.py +0 -0
  116. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_monitoring_health.py +0 -0
  117. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_multi_engine.py +0 -0
  118. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_multitenancy_ddl_injection.py +0 -0
  119. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_nosql_constraints.py +0 -0
  120. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_nosql_kv_overflow_write_path.py +0 -0
  121. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_nosql_operator_injection.py +0 -0
  122. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_observability.py +0 -0
  123. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_overflow_gc.py +0 -0
  124. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_postgresql.py +0 -0
  125. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_postgresql_deserialize_row.py +0 -0
  126. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_queue_extend_delay_cancel.py +0 -0
  127. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_rabbitmq.py +0 -0
  128. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_redis_kv.py +0 -0
  129. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_schema_ddl_injection.py +0 -0
  130. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_secrets_aws.py +0 -0
  131. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_secrets_vault.py +0 -0
  132. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/tests/test_session_vars.py +0 -0
  133. {altcodepro_polydb_python-2.5.22 → altcodepro_polydb_python-2.5.24}/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.22
3
+ Version: 2.5.24
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.22"
7
+ version = "2.5.24"
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.22
3
+ Version: 2.5.24
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
@@ -96,12 +96,14 @@ tests/test_azure_queue_receipt_handle.py
96
96
  tests/test_azure_table_empty_filter.py
97
97
  tests/test_azure_table_id_property_persisted.py
98
98
  tests/test_azure_table_id_return_value_sanitization_mismatch.py
99
+ tests/test_azure_table_key_field_filter.py
99
100
  tests/test_azure_table_large_record_patch_roundtrip.py
100
101
  tests/test_azure_table_self_heal.py
101
102
  tests/test_azure_table_unpack_entity.py
102
103
  tests/test_blockchain.py
103
104
  tests/test_blockchain_overflow_guard.py
104
105
  tests/test_cloud_factory.py
106
+ tests/test_create_unique_violation.py
105
107
  tests/test_factory_wiring.py
106
108
  tests/test_field_encryption.py
107
109
  tests/test_gcp.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.22"
7
+ __version__ = "2.5.24"
8
8
 
9
9
  from .aio import AsyncDatabaseFactory, AsyncPolyDB
10
10
  from .audit.context import AuditContext
@@ -750,11 +750,33 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
750
750
 
751
751
  eff_filters = dict(filters or {})
752
752
  parts: List[str] = []
753
+ # An equality on the model's own pk/rk FIELD is a key lookup: filtering the same value as an ordinary
754
+ # property made Azure scan the whole table (1.2s for one project's artifacts). Only valid when the key is
755
+ # built straight from that field (no partition_config template), which is how _get_pk_rk derives it.
756
+ pk_field = rk_field = None
757
+ declared = getattr(model, "__polydb__", None) or {}
758
+ if not getattr(self, "partition_config", None):
759
+ # Only fields the model declares itself; the defaults (tenant_id / id) are left as property filters
760
+ # so rows written before a model declared its keys keep matching.
761
+ pk_field = declared.get("pk_field") or None
762
+ rk_field = declared.get("rk_field") or None
753
763
  for orig_k, orig_v in eff_filters.items():
754
764
  if orig_v is None:
755
765
  continue
756
766
 
757
- if orig_k in ("partition_key", "PartitionKey"):
767
+ if (
768
+ (pk_field or rk_field)
769
+ and isinstance(orig_v, (str, int))
770
+ and not isinstance(orig_v, bool)
771
+ and orig_k in (pk_field, rk_field)
772
+ ):
773
+ # Keys are sanitized on write, so the key filter uses the sanitized value; the original
774
+ # property filter below is kept too because sanitizing can map two raw values to one key.
775
+ key_name = "PartitionKey" if orig_k == pk_field else "RowKey"
776
+ safe = self._sanitize_pk_rk(orig_v).replace("'", "''")
777
+ parts.append(f"{key_name} eq '{safe}'")
778
+ sk = self._sanitize_prop_name(orig_k)
779
+ elif orig_k in ("partition_key", "PartitionKey"):
758
780
  sk = "PartitionKey"
759
781
  elif orig_k in ("row_key", "RowKey"):
760
782
  sk = "RowKey"
@@ -457,6 +457,13 @@ class DatabaseFactory:
457
457
  conflict_cols = _parse_unique_violation_columns(exc)
458
458
  if not conflict_cols or not all(c in data for c in conflict_cols):
459
459
  raise
460
+ if not set(conflict_cols) <= {"id", "tenant_id"}:
461
+ # A declared-unique NON-key column: a second row with the same value is
462
+ # a violation, never a silent overwrite of the existing row.
463
+ from .base.constraints import UniqueConstraintError
464
+
465
+ col = conflict_cols[0]
466
+ raise UniqueConstraintError(col, data.get(col)) from exc
460
467
  where = {c: data[c] for c in conflict_cols}
461
468
  logger.warning(
462
469
  "insert %s hit unique violation on %s — falling through to update",
@@ -0,0 +1,55 @@
1
+ """An equality on the model's pk/rk field must be sent as PartitionKey/RowKey, not as a property filter
2
+ (a property filter scans the whole table)."""
3
+ import pytest
4
+
5
+ pytest.importorskip("azure.data.tables")
6
+
7
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
8
+
9
+
10
+ class _Model:
11
+ __name__ = "artifacts"
12
+ __polydb__ = {"pk_field": "project_slug", "rk_field": "artifact_id"}
13
+
14
+
15
+ class _Fake:
16
+ def __init__(self):
17
+ self.calls = []
18
+
19
+ def query_entities(self, query_filter=None, **kw):
20
+ self.calls.append(query_filter)
21
+ return []
22
+
23
+
24
+ def _adapter(fake, partition_config=None):
25
+ a = object.__new__(AzureTableStorageAdapter)
26
+ a._get_table_client = lambda model: fake
27
+ a.partition_config = partition_config
28
+ return a
29
+
30
+
31
+ def test_pk_field_equality_becomes_partition_key():
32
+ f = _Fake()
33
+ _adapter(f)._query_raw(_Model, {"project_id": "p1", "project_slug": "p1", "tenant_id": "t"}, 500)
34
+ flt = f.calls[0]
35
+ assert "PartitionKey eq 'p1'" in flt
36
+ assert "project_slug eq 'p1'" in flt # exactness filter kept
37
+ assert "project_id eq 'p1'" in flt and "tenant_id eq 't'" in flt
38
+
39
+
40
+ def test_rk_field_equality_becomes_row_key():
41
+ f = _Fake()
42
+ _adapter(f)._query_raw(_Model, {"artifact_id": "a1"}, 1)
43
+ assert f.calls[0] == "RowKey eq 'a1' and artifact_id eq 'a1'"
44
+
45
+
46
+ def test_partition_config_keeps_property_filter():
47
+ f = _Fake()
48
+ _adapter(f, partition_config=object())._query_raw(_Model, {"project_slug": "p1"}, 1)
49
+ assert "PartitionKey" not in f.calls[0]
50
+
51
+
52
+ def test_key_value_is_sanitized_like_the_write_path():
53
+ f = _Fake()
54
+ _adapter(f)._query_raw(_Model, {"project_slug": "a:b"}, 1)
55
+ assert "PartitionKey eq 'a_b'" in f.calls[0]
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from polydb.base.constraints import UniqueConstraintError
4
+ from polydb.databaseFactory import DatabaseFactory
5
+
6
+
7
+ class _Sql:
8
+ def __init__(self, msg):
9
+ self.msg, self.updated = msg, False
10
+
11
+ def insert(self, table, data, session_vars=None):
12
+ raise Exception(self.msg)
13
+
14
+ def update(self, *a, **k):
15
+ self.updated = True
16
+ return {"id": "1"}
17
+
18
+
19
+ class M:
20
+ __polydb__ = {"storage": "sql", "table": "users"}
21
+
22
+
23
+ def _factory(msg):
24
+ f = DatabaseFactory.__new__(DatabaseFactory)
25
+ sql = _Sql(msg)
26
+ f._inject_audit_fields = lambda d, is_create: d
27
+ f.encryption = None
28
+ f._enable_cache = False
29
+ f._cache = None
30
+ f.metrics = None
31
+ f._audit_write = lambda *a, **k: None
32
+ f._run = lambda fn: fn()
33
+ f._is_sql = lambda *a, **k: True
34
+ f._adapters_for = lambda *a, **k: type("A", (), {"sql": sql})()
35
+ return f, sql
36
+
37
+
38
+ def test_non_key_unique_violation_raises():
39
+ f, sql = _factory("duplicate key value violates unique constraint\nKey (email)=(a@b.c) already exists")
40
+ with pytest.raises(UniqueConstraintError):
41
+ f.create(M, {"email": "a@b.c", "id": "2"})
42
+ assert not sql.updated
43
+
44
+
45
+ def test_primary_key_conflict_still_overwrites():
46
+ f, sql = _factory("duplicate key value violates unique constraint\nKey (id, tenant_id)=(1, t) already exists")
47
+ f.create(M, {"id": "1", "tenant_id": "t", "x": 1})
48
+ assert sql.updated
@@ -101,7 +101,7 @@ def test_azure_sends_select_with_the_keys_the_row_needs():
101
101
  c = _Client([ENT])
102
102
  rows = _adp(c).query(_Model, {"tenant_id": "t1"}, fields=["name", "status"])
103
103
  (flt, kw), = c.kwargs
104
- assert flt == "tenant_id eq 't1'"
104
+ assert flt == "PartitionKey eq 't1' and tenant_id eq 't1'"
105
105
  assert set(kw["select"]) == {"name", "status", "PartitionKey", "RowKey", "__polydb_model__", "__keymap__"}
106
106
  assert rows == [{"name": "n", "status": "ok"}] # id / tenant_id added by unpack are trimmed back out
107
107