altcodepro-polydb-python 2.5.18__tar.gz → 2.5.19__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 (126) hide show
  1. {altcodepro_polydb_python-2.5.18/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.19}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  5. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/PolyDB.py +4 -0
  6. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AzureTableStorageAdapter.py +32 -2
  7. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/PostgreSQLAdapter.py +5 -1
  8. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/NoSQLKVAdapter.py +42 -2
  9. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/databaseFactory.py +29 -6
  10. altcodepro_polydb_python-2.5.19/tests/test_read_projection.py +169 -0
  11. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/LICENSE +0 -0
  12. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/MANIFEST.in +0 -0
  13. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/README.md +0 -0
  14. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements-aws.txt +0 -0
  15. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements-azure.txt +0 -0
  16. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements-dev.txt +0 -0
  17. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements-gcp.txt +0 -0
  18. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements-generic.txt +0 -0
  19. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/requirements.txt +0 -0
  20. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/setup.cfg +0 -0
  21. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/setup.py +0 -0
  22. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  23. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  24. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  25. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/__init__.py +0 -0
  26. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AzureKeyVaultAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/EFSAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/S3Adapter.py +0 -0
  46. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/SQSAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/VaultAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  53. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/adapters/__init__.py +0 -0
  54. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/advanced_query.py +0 -0
  55. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/aio.py +0 -0
  56. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/audit/AuditStorage.py +0 -0
  57. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/audit/__init__.py +0 -0
  58. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/audit/context.py +0 -0
  59. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/audit/manager.py +0 -0
  60. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/audit/models.py +0 -0
  61. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  62. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/QueueAdapter.py +0 -0
  63. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/SecretsAdapter.py +0 -0
  64. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/SharedFilesAdapter.py +0 -0
  65. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/base/__init__.py +0 -0
  66. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/batch.py +0 -0
  67. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/cache.py +0 -0
  68. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/cloudDatabaseFactory.py +0 -0
  69. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/errors.py +0 -0
  70. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/json_safe.py +0 -0
  71. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/models.py +0 -0
  72. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/monitoring.py +0 -0
  73. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/multitenancy.py +0 -0
  74. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/observability/__init__.py +0 -0
  75. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/observability/logging.py +0 -0
  76. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/overflow_gc.py +0 -0
  77. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/py.typed +0 -0
  78. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/query.py +0 -0
  79. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/registry.py +0 -0
  80. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/retry.py +0 -0
  81. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/schema.py +0 -0
  82. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/security.py +0 -0
  83. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/services/__init__.py +0 -0
  84. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/services/compliance_service.py +0 -0
  85. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/services/security_service.py +0 -0
  86. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/types.py +0 -0
  87. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/utils.py +0 -0
  88. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/src/polydb/validation.py +0 -0
  89. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_advanced_query_injection.py +0 -0
  90. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_async_api.py +0 -0
  91. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_atomic_decrement.py +0 -0
  92. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_audit_chain_cutover.py +0 -0
  93. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_audit_hmac.py +0 -0
  94. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_aws.py +0 -0
  95. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure.py +0 -0
  96. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_queue_receipt_handle.py +0 -0
  97. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_table_empty_filter.py +0 -0
  98. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_table_id_property_persisted.py +0 -0
  99. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_table_id_return_value_sanitization_mismatch.py +0 -0
  100. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_table_large_record_patch_roundtrip.py +0 -0
  101. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_azure_table_unpack_entity.py +0 -0
  102. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_blockchain.py +0 -0
  103. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_blockchain_overflow_guard.py +0 -0
  104. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_cloud_factory.py +0 -0
  105. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_field_encryption.py +0 -0
  106. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_gcp.py +0 -0
  107. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_kafka.py +0 -0
  108. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_like_pattern_escaping.py +0 -0
  109. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_mongodb.py +0 -0
  110. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_monitoring_health.py +0 -0
  111. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_multi_engine.py +0 -0
  112. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_multitenancy_ddl_injection.py +0 -0
  113. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_nosql_kv_overflow_write_path.py +0 -0
  114. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_nosql_operator_injection.py +0 -0
  115. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_observability.py +0 -0
  116. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_overflow_gc.py +0 -0
  117. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_postgresql.py +0 -0
  118. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_postgresql_deserialize_row.py +0 -0
  119. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_queue_extend_delay_cancel.py +0 -0
  120. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_rabbitmq.py +0 -0
  121. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_redis_kv.py +0 -0
  122. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_schema_ddl_injection.py +0 -0
  123. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_secrets_aws.py +0 -0
  124. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_secrets_vault.py +0 -0
  125. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/tests/test_session_vars.py +0 -0
  126. {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.19}/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.18
3
+ Version: 2.5.19
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.18"
7
+ version = "2.5.19"
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.18
3
+ Version: 2.5.19
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
@@ -115,6 +115,7 @@ tests/test_postgresql.py
115
115
  tests/test_postgresql_deserialize_row.py
116
116
  tests/test_queue_extend_delay_cancel.py
117
117
  tests/test_rabbitmq.py
118
+ tests/test_read_projection.py
118
119
  tests/test_redis_kv.py
119
120
  tests/test_schema_ddl_injection.py
120
121
  tests/test_secrets_aws.py
@@ -158,6 +158,8 @@ class PolyDB:
158
158
  cache_ttl: Optional[int] = None,
159
159
  include_deleted: bool = False,
160
160
  engine_override: Optional[EngineOverride] = None,
161
+ fields: Optional[List[str]] = None,
162
+ omit: Optional[List[str]] = None,
161
163
  ) -> List[JsonDict]:
162
164
  return self.db.read(
163
165
  model,
@@ -168,6 +170,8 @@ class PolyDB:
168
170
  cache_ttl=cache_ttl,
169
171
  include_deleted=include_deleted,
170
172
  engine_override=engine_override,
173
+ fields=fields,
174
+ omit=omit,
171
175
  )
172
176
 
173
177
  def read_one(
@@ -633,9 +633,35 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
633
633
  return None
634
634
  raise NoSQLError(f"Azure Table get failed: {str(e)}")
635
635
 
636
+ # `fields` / `omit` are sent to Azure as `$select`, so unrequested properties never leave the service.
637
+ SUPPORTS_SELECT_PUSHDOWN = True
638
+
639
+ def _select_properties(self, select: Optional[List[str]]) -> Optional[List[str]]:
640
+ """The Azure property names to `$select` for the requested field names -- the requested ones, plus what `_unpack_entity`
641
+ needs to rebuild a row (the keys, the model tag, and the sanitized-name map). A per-property blob overflow is stored in
642
+ the property itself, so selecting the property is enough for `_restore_overflow_properties`. None = no pushdown: if any
643
+ requested name is rewritten by `_sanitize_prop_name` (so its stored name could carry a collision suffix), the whole row is
644
+ fetched and trimmed afterwards instead of risking a silently missing column."""
645
+ if not select:
646
+ return None
647
+ props: List[str] = []
648
+ for name in select:
649
+ safe = self._sanitize_prop_name(name)
650
+ if safe != str(name):
651
+ return None
652
+ props.append(safe)
653
+ for needed in ("PartitionKey", "RowKey", _MODEL_FIELD, "__keymap__"):
654
+ if needed not in props:
655
+ props.append(needed)
656
+ return props
657
+
636
658
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
637
659
  def _query_raw(
638
- self, model: type, filters: Dict[str, Any], limit: Optional[int]
660
+ self,
661
+ model: type,
662
+ filters: Dict[str, Any],
663
+ limit: Optional[int],
664
+ select: Optional[List[str]] = None,
639
665
  ) -> List[JsonDict]:
640
666
  try:
641
667
  table_client = self._get_table_client(model)
@@ -685,7 +711,11 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
685
711
  else:
686
712
  query_filter = " and ".join(parts)
687
713
 
688
- entities = table_client.query_entities(query_filter=query_filter)
714
+ props = self._select_properties(select)
715
+ if props is None:
716
+ entities = table_client.query_entities(query_filter=query_filter)
717
+ else:
718
+ entities = table_client.query_entities(query_filter=query_filter, select=props)
689
719
 
690
720
  results: List[JsonDict] = []
691
721
  count = 0
@@ -487,8 +487,12 @@ class PostgreSQLAdapter:
487
487
  tx: Optional[Any] = None,
488
488
  *,
489
489
  session_vars: Optional[Dict[str, str]] = None,
490
+ fields: Optional[List[str]] = None,
490
491
  ) -> List[JsonDict]:
491
492
  table = validate_table_name(table)
493
+ if fields:
494
+ for f in fields:
495
+ validate_column_name(f)
492
496
  conn = tx
493
497
  own_conn = False
494
498
  if not conn:
@@ -498,7 +502,7 @@ class PostgreSQLAdapter:
498
502
  try:
499
503
  self._apply_session_vars(conn, session_vars)
500
504
  cursor = conn.cursor()
501
- sql = f"SELECT * FROM {table}"
505
+ sql = f"SELECT {', '.join(fields)} FROM {table}" if fields else f"SELECT * FROM {table}"
502
506
  params: List[Any] = []
503
507
 
504
508
  if query:
@@ -17,9 +17,43 @@ if TYPE_CHECKING:
17
17
  from ..models import PartitionConfig
18
18
 
19
19
 
20
+ def project_rows(
21
+ rows: List[JsonDict], fields: Optional[List[str]] = None, omit: Optional[List[str]] = None
22
+ ) -> List[JsonDict]:
23
+ """The exact `fields` / `omit` semantics every NoSQL read shares: `fields` keeps only those keys, `omit` drops those keys
24
+ (`fields` wins if both are given). Applied by the base `query()` AFTER overflow rehydration, so it is correct for every
25
+ adapter whether or not it also pushes the projection down to the store."""
26
+ if fields:
27
+ keep = set(fields)
28
+ return [{k: v for k, v in r.items() if k in keep} for r in rows]
29
+ if omit:
30
+ drop = set(omit)
31
+ return [{k: v for k, v in r.items() if k not in drop} for r in rows]
32
+ return rows
33
+
34
+
35
+ def select_columns(
36
+ fields: Optional[List[str]], omit: Optional[List[str]], columns: Optional[List[str]]
37
+ ) -> Optional[List[str]]:
38
+ """The column list a store can be asked for: `fields` as given, or -- for `omit` -- the model's known `columns` minus the
39
+ omitted ones. None means "no column list can be derived" (no projection, or `omit` on a model whose columns are unknown):
40
+ the read then fetches whole rows and `project_rows` trims them."""
41
+ if fields:
42
+ return list(fields)
43
+ if omit and columns:
44
+ drop = set(omit)
45
+ return [c for c in columns if c not in drop]
46
+ return None
47
+
48
+
20
49
  class NoSQLKVAdapter:
21
50
  """Base with auto-overflow and LINQ support"""
22
51
 
52
+ # An adapter sets this True when its `_query_raw` accepts `select=[property names]` and really sends it to the store (Azure
53
+ # Table's `$select`). False (the default) means `fields`/`omit` are applied after the rows come back -- same result, no
54
+ # network saving. Opt in per adapter only once the adapter's own rehydration is proven to survive a narrowed row.
55
+ SUPPORTS_SELECT_PUSHDOWN = False
56
+
23
57
  def __init__(
24
58
  self,
25
59
  partition_config: Optional[PartitionConfig] = None,
@@ -242,9 +276,15 @@ class NoSQLKVAdapter:
242
276
  limit: Optional[int] = None,
243
277
  no_cache: bool = False,
244
278
  cache_ttl: Optional[int] = None,
279
+ fields: Optional[List[str]] = None,
280
+ omit: Optional[List[str]] = None,
245
281
  ) -> List[JsonDict]:
246
- results = self._query_raw(model, query or {}, limit)
247
- return [self._retrieve_overflow(r) for r in results]
282
+ select = select_columns(fields, omit, getattr(model, "__polydb__", {}).get("columns"))
283
+ if select is not None and self.SUPPORTS_SELECT_PUSHDOWN:
284
+ results = self._query_raw(model, query or {}, limit, select=select)
285
+ else:
286
+ results = self._query_raw(model, query or {}, limit)
287
+ return project_rows([self._retrieve_overflow(r) for r in results], fields, omit)
248
288
 
249
289
  def query_page(
250
290
  self, model: type, query: Lookup, page_size: int, continuation_token: Optional[str] = None
@@ -24,7 +24,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential
24
24
  from .adapters.PostgreSQLAdapter import PostgreSQLAdapter
25
25
  from .audit.context import AuditContext
26
26
  from .audit.manager import AuditManager
27
- from .base.NoSQLKVAdapter import NoSQLKVAdapter
27
+ from .base.NoSQLKVAdapter import NoSQLKVAdapter, project_rows, select_columns
28
28
  from .batch import BatchOperations
29
29
  from .cache import CacheWarmer, RedisCacheEngine
30
30
  from .cloudDatabaseFactory import CloudDatabaseFactory
@@ -453,30 +453,53 @@ class DatabaseFactory:
453
453
  include_deleted: bool = False,
454
454
  engine_override: Optional[EngineOverride] = None,
455
455
  session_vars: Optional[Dict[str, str]] = None,
456
+ fields: Optional[List[str]] = None,
457
+ omit: Optional[List[str]] = None,
456
458
  ) -> List[JsonDict]:
459
+ """`fields` returns only those keys; `omit` returns every key except those (`fields` wins if both are given). SQL
460
+ names the columns in the SELECT; NoSQL adapters that can ($select on Azure Table) send the property list to the store,
461
+ the rest trim the rows after the read -- same result either way. `omit` needs the model's `__polydb__["columns"]` to
462
+ be turned into a column list for the store; without it the whole row is fetched and trimmed. A projected read never
463
+ goes through the external cache (its key does not include the projection)."""
457
464
  name = _model_name(model)
458
465
  meta = _extract_meta(model)
466
+ columns = (getattr(model, "__polydb__", None) or {}).get("columns") if isinstance(model, type) else None
467
+ projected = bool(fields or omit)
468
+ sql_columns = select_columns(fields, omit, columns)
459
469
 
460
470
  if self._soft_delete and not include_deleted:
461
471
  query = self._apply_soft_delete_filter(query)
462
472
 
463
473
  adapters = self._adapters_for(model, meta, engine_override)
464
- use_external_cache = self._enable_cache and self._cache and getattr(meta, "cache", False)
474
+ use_external_cache = (
475
+ self._enable_cache and self._cache and getattr(meta, "cache", False) and not projected
476
+ )
465
477
  encrypted_fields = getattr(meta, "encrypted_fields", [])
466
478
 
467
479
  def _op() -> List[JsonDict]:
468
480
  if self._is_sql(meta, engine_override):
469
- raw = adapters.sql.select(
470
- meta.table, query, limit=limit, offset=offset, session_vars=session_vars
471
- )
481
+ if sql_columns is not None:
482
+ raw = adapters.sql.select(
483
+ meta.table, query, limit=limit, offset=offset,
484
+ session_vars=session_vars, fields=sql_columns,
485
+ )
486
+ else:
487
+ raw = adapters.sql.select(
488
+ meta.table, query, limit=limit, offset=offset, session_vars=session_vars
489
+ )
490
+ raw = project_rows(raw, fields, omit)
472
491
  else:
473
492
  cls = (
474
493
  model
475
494
  if isinstance(model, type)
476
495
  else type(name, (), {"__polydb__": meta.__dict__})
477
496
  )
497
+ nosql_kwargs: Dict[str, Any] = {}
498
+ if projected:
499
+ nosql_kwargs = {"fields": fields, "omit": omit}
478
500
  raw = adapters.nosql.query(
479
- cls, query=query, limit=limit, no_cache=no_cache or bool(use_external_cache)
501
+ cls, query=query, limit=limit, no_cache=no_cache or bool(use_external_cache),
502
+ **nosql_kwargs,
480
503
  )
481
504
  if self.encryption and encrypted_fields:
482
505
  raw = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw]
@@ -0,0 +1,169 @@
1
+ """`fields` / `omit` on read: SQL names the columns, Azure Table sends `$select`, every other NoSQL adapter trims after the read.
2
+
3
+ Unit level (no Azurite / Postgres): fake table client, fake cursor.
4
+ """
5
+
6
+ import pytest
7
+
8
+ from polydb.base.NoSQLKVAdapter import NoSQLKVAdapter, project_rows, select_columns
9
+
10
+
11
+ # ---------------- pure helpers -------------------------------------------------------------------------------------------
12
+
13
+ def test_project_rows_fields_omit_and_fields_wins():
14
+ rows = [{"id": "1", "a": 1, "b": 2, "big": "x" * 10}]
15
+ assert project_rows(rows, ["id", "a"], None) == [{"id": "1", "a": 1}]
16
+ assert project_rows(rows, None, ["big"]) == [{"id": "1", "a": 1, "b": 2}]
17
+ assert project_rows(rows, ["a"], ["a"]) == [{"a": 1}]
18
+ assert project_rows(rows, None, None) is rows
19
+
20
+
21
+ def test_select_columns_from_fields_or_columns_minus_omit():
22
+ cols = ["id", "a", "b", "big"]
23
+ assert select_columns(["a"], None, cols) == ["a"]
24
+ assert select_columns(None, ["big"], cols) == ["id", "a", "b"]
25
+ assert select_columns(None, ["big"], None) is None # columns unknown: cannot derive a list, fetch whole rows
26
+ assert select_columns(None, None, cols) is None
27
+
28
+
29
+ # ---------------- base adapter: pushdown flag ----------------------------------------------------------------------------
30
+
31
+ class _Adapter(NoSQLKVAdapter):
32
+ def __init__(self, pushdown):
33
+ super().__init__()
34
+ self.SUPPORTS_SELECT_PUSHDOWN = pushdown
35
+ self.calls = []
36
+ def _query_raw(self, model, filters, limit, select=None):
37
+ self.calls.append(select)
38
+ return [{"id": "1", "a": 1, "b": 2, "big": "zzz"}]
39
+
40
+
41
+ class _M:
42
+ __polydb__ = {"columns": ["id", "a", "b", "big"]}
43
+
44
+
45
+ def test_without_pushdown_rows_are_trimmed_after_the_read():
46
+ a = _Adapter(False)
47
+ assert a.query(_M, {"x": 1}, fields=["id", "a"]) == [{"id": "1", "a": 1}]
48
+ assert a.calls == [None] # the store was asked for whole rows
49
+
50
+
51
+ def test_with_pushdown_the_select_list_reaches_the_store_and_result_is_still_exact():
52
+ a = _Adapter(True)
53
+ assert a.query(_M, {}, fields=["a"]) == [{"a": 1}]
54
+ assert a.query(_M, {}, omit=["big"]) == [{"id": "1", "a": 1, "b": 2}]
55
+ assert a.calls == [["a"], ["id", "a", "b"]]
56
+
57
+
58
+ def test_omit_on_a_model_without_known_columns_is_client_side_only():
59
+ class _Bare: pass
60
+ a = _Adapter(True)
61
+ assert a.query(_Bare, {}, omit=["big"]) == [{"id": "1", "a": 1, "b": 2}]
62
+ assert a.calls == [None]
63
+
64
+
65
+ def test_no_projection_keeps_the_old_call_shape():
66
+ a = _Adapter(True)
67
+ assert a.query(_M, {}) == [{"id": "1", "a": 1, "b": 2, "big": "zzz"}]
68
+ assert a.calls == [None]
69
+
70
+
71
+ # ---------------- Azure Table: $select -----------------------------------------------------------------------------------
72
+
73
+ azure = pytest.importorskip("azure.data.tables")
74
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
75
+
76
+
77
+ class _Model:
78
+ __name__ = "Artifact"
79
+ __qualname__ = "Artifact"
80
+ __polydb__ = {"pk_field": "tenant_id", "rk_field": "id"}
81
+
82
+
83
+ class _Client:
84
+ def __init__(self, entities):
85
+ self.entities, self.kwargs = entities, []
86
+ def query_entities(self, query_filter=None, **kw):
87
+ self.kwargs.append((query_filter, kw)); return list(self.entities)
88
+
89
+
90
+ ENT = {"PartitionKey": "t1", "RowKey": "r1", "__polydb_model__": "Artifact", "__keymap__": "{}", "id": "r1",
91
+ "name": "n", "status": "ok"}
92
+
93
+
94
+ def _adp(client):
95
+ adp = object.__new__(AzureTableStorageAdapter)
96
+ adp._get_table_client = lambda model: client
97
+ return adp
98
+
99
+
100
+ def test_azure_sends_select_with_the_keys_the_row_needs():
101
+ c = _Client([ENT])
102
+ rows = _adp(c).query(_Model, {"tenant_id": "t1"}, fields=["name", "status"])
103
+ (flt, kw), = c.kwargs
104
+ assert flt == "tenant_id eq 't1'"
105
+ assert set(kw["select"]) == {"name", "status", "PartitionKey", "RowKey", "__polydb_model__", "__keymap__"}
106
+ assert rows == [{"name": "n", "status": "ok"}] # id / tenant_id added by unpack are trimmed back out
107
+
108
+
109
+ def test_azure_omit_becomes_select_of_the_remaining_known_columns():
110
+ class _M2(_Model):
111
+ __polydb__ = {"pk_field": "tenant_id", "rk_field": "id", "columns": ["id", "name", "status", "content"]}
112
+ c = _Client([ENT])
113
+ rows = _adp(c).query(_M2, {}, omit=["content"])
114
+ (_, kw), = c.kwargs
115
+ assert "content" not in kw["select"] and {"id", "name", "status"} <= set(kw["select"])
116
+ assert rows == [{"id": "r1", "name": "n", "status": "ok", "tenant_id": "t1"}] # omit drops only what was named
117
+
118
+
119
+ def test_azure_no_projection_sends_no_select():
120
+ c = _Client([ENT])
121
+ _adp(c).query(_Model, {})
122
+ assert c.kwargs == [(None, {})]
123
+
124
+
125
+ def test_azure_falls_back_to_whole_rows_when_a_name_would_be_rewritten():
126
+ c = _Client([dict(ENT, f_1x="v")])
127
+ _adp(c).query(_Model, {}, fields=["1x"]) # "1x" is stored as "f_1x": cannot be selected by its own name
128
+ assert c.kwargs == [(None, {})]
129
+
130
+
131
+ # ---------------- Postgres: named columns --------------------------------------------------------------------------------
132
+
133
+ from polydb.adapters.PostgreSQLAdapter import PostgreSQLAdapter # noqa: E402
134
+
135
+
136
+ class _Cur:
137
+ description = [("id",), ("status",)]
138
+ def fetchall(self): return [("1", "ok")]
139
+ def close(self): pass
140
+ class _Conn:
141
+ def cursor(self): return _Cur()
142
+ def commit(self): pass
143
+ def rollback(self): pass
144
+
145
+
146
+ def _pg(captured):
147
+ a = object.__new__(PostgreSQLAdapter)
148
+ a._get_connection = lambda: _Conn()
149
+ a._return_connection = lambda c: None
150
+ a._apply_session_vars = lambda c, v: None
151
+ a._timed_execute = lambda cur, sql, params, **k: captured.append((sql, params))
152
+ a._serialize_params = lambda p: p
153
+ a._deserialize_row = lambda r: r
154
+ return a
155
+
156
+
157
+ def test_postgres_select_names_the_columns():
158
+ cap = []
159
+ rows = _pg(cap).select("orders", {"tenant_id": "t"}, limit=5, fields=["id", "status"])
160
+ assert cap[0][0].startswith("SELECT id, status FROM orders WHERE tenant_id = %s")
161
+ assert rows == [{"id": "1", "status": "ok"}]
162
+
163
+
164
+ def test_postgres_select_without_fields_is_unchanged_and_bad_names_are_refused():
165
+ cap = []
166
+ _pg(cap).select("orders", {})
167
+ assert cap[0][0] == "SELECT * FROM orders"
168
+ with pytest.raises(Exception):
169
+ _pg([]).select("orders", {}, fields=["id; DROP TABLE orders"])