altcodepro-polydb-python 2.5.18__tar.gz → 2.5.20__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.
- {altcodepro_polydb_python-2.5.18/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.20}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/pyproject.toml +1 -1
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +2 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/PolyDB.py +4 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/__init__.py +1 -1
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AzureTableStorageAdapter.py +32 -2
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/DynamoDBAdapter.py +7 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/PostgreSQLAdapter.py +5 -1
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/NoSQLKVAdapter.py +45 -3
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/databaseFactory.py +144 -24
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/schema.py +41 -4
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/types.py +2 -0
- altcodepro_polydb_python-2.5.20/tests/test_factory_wiring.py +247 -0
- altcodepro_polydb_python-2.5.20/tests/test_read_projection.py +169 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/LICENSE +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/README.md +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/setup.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AzureKeyVaultAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/VaultAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/VercelFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/aio.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/audit/AuditStorage.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/SecretsAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/cloudDatabaseFactory.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/observability/__init__.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/observability/logging.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/overflow_gc.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/query.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/services/__init__.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/services/compliance_service.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/services/security_service.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_advanced_query_injection.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_async_api.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_atomic_decrement.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_audit_chain_cutover.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_audit_hmac.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_queue_receipt_handle.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_table_empty_filter.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_table_id_property_persisted.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_table_id_return_value_sanitization_mismatch.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_table_large_record_patch_roundtrip.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_azure_table_unpack_entity.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_blockchain_overflow_guard.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_field_encryption.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_kafka.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_like_pattern_escaping.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_monitoring_health.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_multitenancy_ddl_injection.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_nosql_kv_overflow_write_path.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_nosql_operator_injection.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_observability.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_overflow_gc.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_postgresql_deserialize_row.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_queue_extend_delay_cancel.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_rabbitmq.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_redis_kv.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_schema_ddl_injection.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_secrets_aws.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_secrets_vault.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/tests/test_session_vars.py +0 -0
- {altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/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.
|
|
3
|
+
Version: 2.5.20
|
|
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.
|
|
7
|
+
version = "2.5.20"
|
|
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.
|
|
3
|
+
Version: 2.5.20
|
|
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
|
|
@@ -99,6 +99,7 @@ tests/test_azure_table_unpack_entity.py
|
|
|
99
99
|
tests/test_blockchain.py
|
|
100
100
|
tests/test_blockchain_overflow_guard.py
|
|
101
101
|
tests/test_cloud_factory.py
|
|
102
|
+
tests/test_factory_wiring.py
|
|
102
103
|
tests/test_field_encryption.py
|
|
103
104
|
tests/test_gcp.py
|
|
104
105
|
tests/test_kafka.py
|
|
@@ -115,6 +116,7 @@ tests/test_postgresql.py
|
|
|
115
116
|
tests/test_postgresql_deserialize_row.py
|
|
116
117
|
tests/test_queue_extend_delay_cancel.py
|
|
117
118
|
tests/test_rabbitmq.py
|
|
119
|
+
tests/test_read_projection.py
|
|
118
120
|
tests/test_redis_kv.py
|
|
119
121
|
tests/test_schema_ddl_injection.py
|
|
120
122
|
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(
|
|
@@ -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.
|
|
7
|
+
__version__ = "2.5.20"
|
|
8
8
|
|
|
9
9
|
from .aio import AsyncDatabaseFactory, AsyncPolyDB
|
|
10
10
|
from .audit.context import AuditContext
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
@@ -196,6 +196,13 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
196
196
|
def _blob_key(self, model: type, pk: str, rk: str, checksum: str) -> str:
|
|
197
197
|
return f"overflow/{self._table_name(model)}/{pk}/{rk}/{checksum}.json"
|
|
198
198
|
|
|
199
|
+
def _check_overflow(self, data: JsonDict): # type: ignore[override]
|
|
200
|
+
"""DynamoDB overflows inside `_put_raw` (`_maybe_overflow_to_s3`, into this adapter's own bucket), never in the
|
|
201
|
+
base class. The base whole-record overflow replaced the record with a stub pointing at a key in the separate
|
|
202
|
+
object store, which `_resolve_overflow` (it reads this adapter's bucket) can never find -- the record was lost
|
|
203
|
+
on read. Returning the data untouched leaves the one working path in charge."""
|
|
204
|
+
return data, None
|
|
205
|
+
|
|
199
206
|
def _maybe_overflow_to_s3(
|
|
200
207
|
self, model: type, pk: str, rk: str, payload: JsonDict
|
|
201
208
|
) -> Optional[JsonDict]:
|
|
@@ -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,16 +276,24 @@ 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
|
-
|
|
247
|
-
|
|
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
|
|
251
291
|
) -> Tuple[List[JsonDict], Optional[str]]:
|
|
252
292
|
# Basic implementation - override per provider
|
|
253
293
|
offset = int(continuation_token) if continuation_token else 0
|
|
254
|
-
|
|
294
|
+
# Fetch through the end of THIS page (+1 to learn whether another follows) and drop what earlier pages already
|
|
295
|
+
# returned -- reading from the start every time made every page after the first a repeat of the first.
|
|
296
|
+
results = self.query(model, query, limit=offset + page_size + 1)[offset:]
|
|
255
297
|
|
|
256
298
|
has_more = len(results) > page_size
|
|
257
299
|
if has_more:
|
{altcodepro_polydb_python-2.5.18 → altcodepro_polydb_python-2.5.20}/src/polydb/databaseFactory.py
RENAMED
|
@@ -19,12 +19,12 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
21
21
|
|
|
22
|
-
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
22
|
+
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
|
|
23
23
|
|
|
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
|
|
@@ -32,6 +32,7 @@ from .errors import AdapterConfigurationError
|
|
|
32
32
|
from .models import PageRequest, PageResult
|
|
33
33
|
from .monitoring import HealthCheck, MetricsCollector, PerformanceMonitor
|
|
34
34
|
from .query import QueryBuilder
|
|
35
|
+
from .retry import _is_non_retryable
|
|
35
36
|
from .security import DataMasking, FieldEncryption
|
|
36
37
|
from .types import JsonDict, Lookup, ModelMeta
|
|
37
38
|
|
|
@@ -53,6 +54,31 @@ _DEFAULT_RETRY = retry(
|
|
|
53
54
|
reraise=True,
|
|
54
55
|
)
|
|
55
56
|
|
|
57
|
+
_TRANSIENT_MARKERS = (
|
|
58
|
+
"timeout", "timed out", "connection reset", "connection refused", "connection closed", "connection aborted",
|
|
59
|
+
"server closed the connection", "temporarily unavailable", "too many requests", "throttl", "service unavailable",
|
|
60
|
+
"serverbusy", "503", "could not connect",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_transient(exc: BaseException) -> bool:
|
|
65
|
+
if _is_non_retryable(exc):
|
|
66
|
+
return False
|
|
67
|
+
if isinstance(exc, (ConnectionError, TimeoutError)):
|
|
68
|
+
return True
|
|
69
|
+
s = str(exc).lower()
|
|
70
|
+
return any(m in s for m in _TRANSIENT_MARKERS)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _TRANSIENT_RETRY(fn):
|
|
74
|
+
return retry(
|
|
75
|
+
retry=retry_if_exception(_is_transient),
|
|
76
|
+
wait=wait_exponential(multiplier=0.5, min=0.5, max=6),
|
|
77
|
+
stop=stop_after_attempt(3),
|
|
78
|
+
reraise=True,
|
|
79
|
+
)(fn)
|
|
80
|
+
|
|
81
|
+
|
|
56
82
|
_UNIQUE_VIOLATION_MARKERS = (
|
|
57
83
|
"23505", # Postgres SQLSTATE
|
|
58
84
|
"duplicate key value violates", # Postgres message
|
|
@@ -140,6 +166,7 @@ def _extract_meta(model: Union[type, str]) -> ModelMeta:
|
|
|
140
166
|
provider=raw.get("provider"),
|
|
141
167
|
cache=raw.get("cache", False),
|
|
142
168
|
cache_ttl=raw.get("cache_ttl"),
|
|
169
|
+
encrypted_fields=tuple(raw.get("encrypted_fields") or ()),
|
|
143
170
|
)
|
|
144
171
|
return ModelMeta(storage="nosql", table=None, collection=None)
|
|
145
172
|
|
|
@@ -348,7 +375,42 @@ class DatabaseFactory:
|
|
|
348
375
|
return result
|
|
349
376
|
|
|
350
377
|
def _run(self, fn: Callable[[], Any]) -> Any:
|
|
351
|
-
|
|
378
|
+
"""Run one storage operation, retrying only TRANSIENT failures (connection loss, timeouts, throttling) with
|
|
379
|
+
exponential backoff when retries are enabled. Constraint violations, bad arguments and anything the retry
|
|
380
|
+
classifier marks non-retryable are raised at once -- a retry could not change their outcome."""
|
|
381
|
+
if not self._enable_retries:
|
|
382
|
+
return fn()
|
|
383
|
+
return _TRANSIENT_RETRY(fn)()
|
|
384
|
+
|
|
385
|
+
def _audit_write(
|
|
386
|
+
self, action: str, name: str, meta: ModelMeta, entity_id: Any, success: bool,
|
|
387
|
+
before: Optional[JsonDict], after: Optional[JsonDict], error: Optional[str],
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Record a write in the audit log. Never raises: an audit failure must not turn a completed write into an
|
|
390
|
+
error. Encrypted fields are masked so the log never holds their plaintext."""
|
|
391
|
+
if not (self._enable_audit and self._audit):
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
def _mask(row: Optional[JsonDict]) -> Optional[JsonDict]:
|
|
395
|
+
if not row or not meta.encrypted_fields:
|
|
396
|
+
return row
|
|
397
|
+
return {k: ("[encrypted]" if k in meta.encrypted_fields else v) for k, v in row.items()}
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
before_m, after_m = _mask(before), _mask(after)
|
|
401
|
+
changed = (
|
|
402
|
+
sorted(k for k in set(after_m or {}) | set(before_m or {}) if (before_m or {}).get(k) != (after_m or {}).get(k))
|
|
403
|
+
if before_m is not None and after_m is not None else None
|
|
404
|
+
)
|
|
405
|
+
if isinstance(entity_id, dict):
|
|
406
|
+
entity_id = entity_id.get("id")
|
|
407
|
+
self._audit.record(
|
|
408
|
+
action=action, model=name, entity_id=None if entity_id is None else str(entity_id),
|
|
409
|
+
storage_type=meta.storage, provider=meta.provider or "default", success=success,
|
|
410
|
+
before=before_m, after=after_m, error=error, changed_fields=changed,
|
|
411
|
+
)
|
|
412
|
+
except Exception as exc: # noqa: BLE001
|
|
413
|
+
logger.warning("Audit write failed (non-fatal): %s", exc)
|
|
352
414
|
|
|
353
415
|
def _is_sql(self, meta: ModelMeta, override: Optional[EngineOverride] = None) -> bool:
|
|
354
416
|
if override and override.force_sql:
|
|
@@ -432,9 +494,12 @@ class DatabaseFactory:
|
|
|
432
494
|
with monitor as m:
|
|
433
495
|
result = self._run(_op)
|
|
434
496
|
m.rows_affected = 1
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
497
|
+
else:
|
|
498
|
+
result = self._run(_op)
|
|
499
|
+
self._audit_write("create", name, meta, entity_id, True, None, after_plain, None)
|
|
500
|
+
return result
|
|
501
|
+
except Exception as exc:
|
|
502
|
+
self._audit_write("create", name, meta, entity_id, False, None, None, str(exc))
|
|
438
503
|
raise
|
|
439
504
|
|
|
440
505
|
# ──────────────────────────────────────────────────────────
|
|
@@ -453,43 +518,83 @@ class DatabaseFactory:
|
|
|
453
518
|
include_deleted: bool = False,
|
|
454
519
|
engine_override: Optional[EngineOverride] = None,
|
|
455
520
|
session_vars: Optional[Dict[str, str]] = None,
|
|
521
|
+
fields: Optional[List[str]] = None,
|
|
522
|
+
omit: Optional[List[str]] = None,
|
|
456
523
|
) -> List[JsonDict]:
|
|
524
|
+
"""`fields` returns only those keys; `omit` returns every key except those (`fields` wins if both are given). SQL
|
|
525
|
+
names the columns in the SELECT; NoSQL adapters that can ($select on Azure Table) send the property list to the store,
|
|
526
|
+
the rest trim the rows after the read -- same result either way. `omit` needs the model's `__polydb__["columns"]` to
|
|
527
|
+
be turned into a column list for the store; without it the whole row is fetched and trimmed. A projected read never
|
|
528
|
+
goes through the external cache (its key does not include the projection)."""
|
|
457
529
|
name = _model_name(model)
|
|
458
530
|
meta = _extract_meta(model)
|
|
531
|
+
columns = (getattr(model, "__polydb__", None) or {}).get("columns") if isinstance(model, type) else None
|
|
532
|
+
projected = bool(fields or omit)
|
|
533
|
+
sql_columns = select_columns(fields, omit, columns)
|
|
459
534
|
|
|
535
|
+
drop_deleted = False
|
|
460
536
|
if self._soft_delete and not include_deleted:
|
|
461
|
-
|
|
537
|
+
if self._is_sql(meta, engine_override):
|
|
538
|
+
query = self._apply_soft_delete_filter(query)
|
|
539
|
+
else:
|
|
540
|
+
# A NoSQL equality filter on `deleted_at = None` matches nothing when the attribute was never written
|
|
541
|
+
# (Azure Table, DynamoDB), so filter the returned rows instead.
|
|
542
|
+
drop_deleted = True
|
|
462
543
|
|
|
463
544
|
adapters = self._adapters_for(model, meta, engine_override)
|
|
464
|
-
use_external_cache =
|
|
545
|
+
use_external_cache = (
|
|
546
|
+
self._enable_cache and self._cache and getattr(meta, "cache", False) and not projected
|
|
547
|
+
)
|
|
465
548
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
549
|
+
# The cache key must cover everything that changes the result: the query, the window, and the session variables
|
|
550
|
+
# (e.g. the tenant a row-level-security policy filters by) -- otherwise two callers share an entry.
|
|
551
|
+
cache_key_query = {
|
|
552
|
+
"q": query or {}, "limit": limit, "offset": offset,
|
|
553
|
+
"sv": sorted((session_vars or {}).items()), "e": bool(engine_override),
|
|
554
|
+
}
|
|
466
555
|
|
|
467
556
|
def _op() -> List[JsonDict]:
|
|
468
557
|
if self._is_sql(meta, engine_override):
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
558
|
+
if sql_columns is not None:
|
|
559
|
+
raw = adapters.sql.select(
|
|
560
|
+
meta.table, query, limit=limit, offset=offset,
|
|
561
|
+
session_vars=session_vars, fields=sql_columns,
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
raw = adapters.sql.select(
|
|
565
|
+
meta.table, query, limit=limit, offset=offset, session_vars=session_vars
|
|
566
|
+
)
|
|
567
|
+
raw = project_rows(raw, fields, omit)
|
|
472
568
|
else:
|
|
473
569
|
cls = (
|
|
474
570
|
model
|
|
475
571
|
if isinstance(model, type)
|
|
476
572
|
else type(name, (), {"__polydb__": meta.__dict__})
|
|
477
573
|
)
|
|
574
|
+
nosql_kwargs: Dict[str, Any] = {}
|
|
575
|
+
if projected:
|
|
576
|
+
nosql_kwargs = {"fields": fields, "omit": omit}
|
|
577
|
+
skip = offset or 0
|
|
478
578
|
raw = adapters.nosql.query(
|
|
479
|
-
cls, query=query, limit=limit
|
|
579
|
+
cls, query=query, limit=(limit + skip) if limit else None,
|
|
580
|
+
no_cache=no_cache or bool(use_external_cache), **nosql_kwargs,
|
|
480
581
|
)
|
|
582
|
+
if skip:
|
|
583
|
+
raw = raw[skip:] # NoSQL adapters take no offset: read through it, then drop the skipped rows
|
|
584
|
+
if drop_deleted:
|
|
585
|
+
raw = [r for r in raw if not r.get("deleted_at")]
|
|
481
586
|
if self.encryption and encrypted_fields:
|
|
482
587
|
raw = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw]
|
|
483
588
|
if self._cache and use_external_cache and not no_cache:
|
|
484
589
|
ttl = cache_ttl or getattr(meta, "cache_ttl", 300)
|
|
485
590
|
try:
|
|
486
|
-
self._cache.set(name,
|
|
591
|
+
self._cache.set(name, cache_key_query, raw, ttl)
|
|
487
592
|
except Exception as _ce:
|
|
488
593
|
logger.warning("Cache set failed (non-fatal): %s", _ce)
|
|
489
594
|
return raw
|
|
490
595
|
|
|
491
596
|
if self._cache and use_external_cache and not no_cache:
|
|
492
|
-
cached = self._cache.get(name,
|
|
597
|
+
cached = self._cache.get(name, cache_key_query)
|
|
493
598
|
if cached is not None:
|
|
494
599
|
return cached
|
|
495
600
|
|
|
@@ -654,9 +759,12 @@ class DatabaseFactory:
|
|
|
654
759
|
with monitor as m:
|
|
655
760
|
result = self._run(_op)
|
|
656
761
|
m.rows_affected = 1
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
762
|
+
else:
|
|
763
|
+
result = self._run(_op)
|
|
764
|
+
self._audit_write("update", name, meta, entity_id, True, before, after_plain, None)
|
|
765
|
+
return result
|
|
766
|
+
except Exception as exc:
|
|
767
|
+
self._audit_write("update", name, meta, entity_id, False, before, None, str(exc))
|
|
660
768
|
raise
|
|
661
769
|
|
|
662
770
|
# ──────────────────────────────────────────────────────────
|
|
@@ -714,9 +822,12 @@ class DatabaseFactory:
|
|
|
714
822
|
with monitor as m:
|
|
715
823
|
result = self._run(_op)
|
|
716
824
|
m.rows_affected = 1
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
825
|
+
else:
|
|
826
|
+
result = self._run(_op)
|
|
827
|
+
self._audit_write("upsert", name, meta, (after_plain or {}).get('id'), True, None, after_plain, None)
|
|
828
|
+
return result
|
|
829
|
+
except Exception as exc:
|
|
830
|
+
self._audit_write("upsert", name, meta, None, False, None, None, str(exc))
|
|
720
831
|
raise
|
|
721
832
|
|
|
722
833
|
# ──────────────────────────────────────────────────────────
|
|
@@ -827,9 +938,12 @@ class DatabaseFactory:
|
|
|
827
938
|
with monitor as m:
|
|
828
939
|
result = self._run(_op)
|
|
829
940
|
m.rows_affected = 1
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
941
|
+
else:
|
|
942
|
+
result = self._run(_op)
|
|
943
|
+
self._audit_write("delete", name, meta, entity_id, True, before, None, None)
|
|
944
|
+
return result
|
|
945
|
+
except Exception as exc:
|
|
946
|
+
self._audit_write("delete", name, meta, entity_id, False, before, None, str(exc))
|
|
833
947
|
raise
|
|
834
948
|
|
|
835
949
|
# ──────────────────────────────────────────────────────────
|
|
@@ -883,8 +997,12 @@ class DatabaseFactory:
|
|
|
883
997
|
name = _model_name(model)
|
|
884
998
|
meta = _extract_meta(model)
|
|
885
999
|
|
|
1000
|
+
drop_deleted = False
|
|
886
1001
|
if self._soft_delete and not include_deleted:
|
|
887
|
-
|
|
1002
|
+
if self._is_sql(meta, engine_override):
|
|
1003
|
+
query = self._apply_soft_delete_filter(query)
|
|
1004
|
+
else:
|
|
1005
|
+
drop_deleted = True
|
|
888
1006
|
|
|
889
1007
|
adapters = self._adapters_for(model, meta, engine_override)
|
|
890
1008
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
@@ -901,6 +1019,8 @@ class DatabaseFactory:
|
|
|
901
1019
|
else type(name, (), {"__polydb__": meta.__dict__})
|
|
902
1020
|
)
|
|
903
1021
|
raw, token = adapters.nosql.query_page(cls, query, page_size, continuation_token)
|
|
1022
|
+
if drop_deleted:
|
|
1023
|
+
raw = [r for r in raw if not r.get("deleted_at")]
|
|
904
1024
|
if self.encryption and encrypted_fields:
|
|
905
1025
|
raw = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw]
|
|
906
1026
|
return raw, token
|
|
@@ -131,6 +131,27 @@ class SchemaBuilder:
|
|
|
131
131
|
|
|
132
132
|
return sql
|
|
133
133
|
|
|
134
|
+
def to_add_missing_columns(self, table_name: str) -> List[str]:
|
|
135
|
+
"""`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` for every non-key column. `CREATE TABLE IF NOT EXISTS` never adds a
|
|
136
|
+
column that was declared after the table was first created; appending these to the same migration makes an edited
|
|
137
|
+
column list reach existing tables. Added columns are always nullable (existing rows have no value) and never
|
|
138
|
+
UNIQUE; a declared default is kept."""
|
|
139
|
+
validate_table_name(table_name)
|
|
140
|
+
keys = set(self.primary_keys or [])
|
|
141
|
+
out: List[str] = []
|
|
142
|
+
for col in self.columns:
|
|
143
|
+
if col.name in keys or col.primary_key:
|
|
144
|
+
continue
|
|
145
|
+
parts = [validate_column_name(col.name)]
|
|
146
|
+
if col.type == ColumnType.VARCHAR and col.max_length:
|
|
147
|
+
parts.append(f"VARCHAR({col.max_length})")
|
|
148
|
+
else:
|
|
149
|
+
parts.append(col.type.value)
|
|
150
|
+
if col.default is not None:
|
|
151
|
+
parts.append(_render_default(col.default))
|
|
152
|
+
out.append(f"ALTER TABLE {table_name} ADD COLUMN IF NOT EXISTS {' '.join(parts)};")
|
|
153
|
+
return out
|
|
154
|
+
|
|
134
155
|
def to_create_indexes(self, table_name: str) -> List[str]:
|
|
135
156
|
"""Generate CREATE INDEX statements"""
|
|
136
157
|
statements = []
|
|
@@ -185,15 +206,31 @@ class MigrationManager:
|
|
|
185
206
|
|
|
186
207
|
# Check if already applied
|
|
187
208
|
existing = self.sql.execute(
|
|
188
|
-
"SELECT version FROM polydb_migrations WHERE version = %s", [version], fetch_one=True
|
|
209
|
+
"SELECT version, checksum FROM polydb_migrations WHERE version = %s", [version], fetch_one=True
|
|
189
210
|
)
|
|
190
211
|
|
|
191
|
-
if existing:
|
|
192
|
-
return False
|
|
193
|
-
|
|
194
212
|
# Calculate checksum
|
|
195
213
|
checksum = hashlib.sha256(up_sql.encode()).hexdigest()
|
|
196
214
|
|
|
215
|
+
if existing:
|
|
216
|
+
stored = existing.get("checksum") if isinstance(existing, dict) else None
|
|
217
|
+
if not stored or stored == checksum:
|
|
218
|
+
return False
|
|
219
|
+
# The DDL under this version was edited (a column added, an index changed). Re-apply it -- the statements
|
|
220
|
+
# this codebase generates are idempotent (IF NOT EXISTS) -- and remember the new checksum. A failure here
|
|
221
|
+
# must not stop a deployment from starting: keep the old checksum so the next start tries again.
|
|
222
|
+
try:
|
|
223
|
+
self.sql.execute(up_sql)
|
|
224
|
+
self.sql.execute(
|
|
225
|
+
"UPDATE polydb_migrations SET checksum = %s WHERE version = %s", [checksum, version]
|
|
226
|
+
)
|
|
227
|
+
return True
|
|
228
|
+
except Exception as exc: # noqa: BLE001
|
|
229
|
+
import logging
|
|
230
|
+
|
|
231
|
+
logging.getLogger(__name__).warning("Re-applying edited migration %s failed (kept old): %s", version, exc)
|
|
232
|
+
return False
|
|
233
|
+
|
|
197
234
|
try:
|
|
198
235
|
# Execute migration
|
|
199
236
|
self.sql.execute(up_sql)
|
|
@@ -24,6 +24,8 @@ class ModelMeta:
|
|
|
24
24
|
provider: Optional[str] = None
|
|
25
25
|
cache: bool = False
|
|
26
26
|
cache_ttl: Optional[int] = None
|
|
27
|
+
# Fields stored encrypted when the factory has encryption enabled (a tuple: the dataclass is frozen/hashable).
|
|
28
|
+
encrypted_fields: Tuple[str, ...] = ()
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@runtime_checkable
|