altcodepro-polydb-python 2.3.24__tar.gz → 2.3.26__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 (92) hide show
  1. {altcodepro_polydb_python-2.3.24/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.26}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  5. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/__init__.py +3 -1
  6. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/AzureTableStorageAdapter.py +16 -1
  7. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/models.py +35 -0
  8. altcodepro_polydb_python-2.3.26/src/polydb/utils.py +94 -0
  9. altcodepro_polydb_python-2.3.26/tests/test_azure_table_empty_filter.py +70 -0
  10. altcodepro_polydb_python-2.3.24/src/polydb/utils.py +0 -61
  11. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/LICENSE +0 -0
  12. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/MANIFEST.in +0 -0
  13. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/README.md +0 -0
  14. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements-aws.txt +0 -0
  15. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements-azure.txt +0 -0
  16. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements-dev.txt +0 -0
  17. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements-gcp.txt +0 -0
  18. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements-generic.txt +0 -0
  19. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/requirements.txt +0 -0
  20. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/setup.cfg +0 -0
  21. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/setup.py +0 -0
  22. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  23. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  24. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  25. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/PolyDB.py +0 -0
  26. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/EFSAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/S3Adapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/SQSAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/adapters/__init__.py +0 -0
  49. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/advanced_query.py +0 -0
  50. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/audit/AuditStorage.py +0 -0
  51. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/audit/__init__.py +0 -0
  52. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/audit/context.py +0 -0
  53. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/audit/manager.py +0 -0
  54. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/audit/models.py +0 -0
  55. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  56. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  57. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/base/QueueAdapter.py +0 -0
  58. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/base/SharedFilesAdapter.py +0 -0
  59. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/base/__init__.py +0 -0
  60. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/batch.py +0 -0
  61. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/cache.py +0 -0
  62. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/cloudDatabaseFactory.py +0 -0
  63. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/databaseFactory.py +0 -0
  64. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/decorators.py +0 -0
  65. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/errors.py +0 -0
  66. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/json_safe.py +0 -0
  67. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/monitoring.py +0 -0
  68. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/multitenancy.py +0 -0
  69. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/observability/__init__.py +0 -0
  70. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/observability/logging.py +0 -0
  71. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/py.typed +0 -0
  72. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/query.py +0 -0
  73. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/registry.py +0 -0
  74. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/retry.py +0 -0
  75. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/schema.py +0 -0
  76. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/security.py +0 -0
  77. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/services/__init__.py +0 -0
  78. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/services/compliance_service.py +0 -0
  79. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/services/security_service.py +0 -0
  80. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/types.py +0 -0
  81. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/src/polydb/validation.py +0 -0
  82. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_atomic_decrement.py +0 -0
  83. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_aws.py +0 -0
  84. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_azure.py +0 -0
  85. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_blockchain.py +0 -0
  86. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_cloud_factory.py +0 -0
  87. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_gcp.py +0 -0
  88. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_mongodb.py +0 -0
  89. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_multi_engine.py +0 -0
  90. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_observability.py +0 -0
  91. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/tests/test_postgresql.py +0 -0
  92. {altcodepro_polydb_python-2.3.24 → altcodepro_polydb_python-2.3.26}/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.3.24
3
+ Version: 2.3.26
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.3.24"
7
+ version = "2.3.26"
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.3.24
3
+ Version: 2.3.26
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
@@ -78,6 +78,7 @@ src/polydb/services/security_service.py
78
78
  tests/test_atomic_decrement.py
79
79
  tests/test_aws.py
80
80
  tests/test_azure.py
81
+ tests/test_azure_table_empty_filter.py
81
82
  tests/test_blockchain.py
82
83
  tests/test_cloud_factory.py
83
84
  tests/test_gcp.py
@@ -8,7 +8,7 @@ __version__ = "2.2.3"
8
8
 
9
9
  from .cloudDatabaseFactory import CloudDatabaseFactory
10
10
  from .databaseFactory import DatabaseFactory
11
- from .models import CloudProvider, PartitionConfig
11
+ from .models import CloudProvider, PartitionConfig, MongoConfig, CosmosMongoConfig
12
12
  from .query import QueryBuilder, Operator
13
13
  from .audit.context import AuditContext
14
14
  from .cache import RedisCacheEngine as CacheEngine
@@ -36,6 +36,8 @@ __all__ = [
36
36
  # Models & Config
37
37
  "CloudProvider",
38
38
  "PartitionConfig",
39
+ "MongoConfig",
40
+ "CosmosMongoConfig",
39
41
  # Query
40
42
  "QueryBuilder",
41
43
  "Operator",
@@ -567,7 +567,18 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
567
567
  sval = str(ev).replace("'", "''")
568
568
  parts.append(f"{sk} eq '{sval}'")
569
569
 
570
- query_filter = " and ".join(parts) if parts else None
570
+ # Empty-filter contract: if the caller passed filter keys but every
571
+ # condition dropped out (e.g. all values None/unresolvable), the query
572
+ # must match NOTHING — return [] (→ None for a single-object read).
573
+ # Returning all entities here (query_filter=None) is how an unmatched
574
+ # read_one accidentally returned a stray context row. Only a genuinely
575
+ # empty filter dict means "list all".
576
+ if not parts:
577
+ if eff_filters:
578
+ return []
579
+ query_filter = None
580
+ else:
581
+ query_filter = " and ".join(parts)
571
582
 
572
583
  entities = table_client.query_entities(query_filter=query_filter)
573
584
 
@@ -670,6 +681,10 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
670
681
  else:
671
682
  sval = str(ev).replace("'", "''")
672
683
  parts.append(f"{sk} eq '{sval}'")
684
+ # Same empty-filter contract as _query_raw: filter keys that all resolved
685
+ # to no condition must match NOTHING, not list the whole table.
686
+ if not parts and eff_filters:
687
+ return PageResult(items=[], next_cursor=None, has_more=False)
673
688
  query_filter = " and ".join(parts) if parts else None
674
689
 
675
690
  azure_ct = None
@@ -323,3 +323,38 @@ class MongoConfig(StorageConfig):
323
323
  super().__init__(CloudProvider.MONGODB, name)
324
324
  self.mongo_uri = mongo_uri
325
325
  self.db_name = db_name
326
+
327
+
328
+ class CosmosMongoConfig(MongoConfig):
329
+ """Azure Cosmos DB via the **MongoDB API**.
330
+
331
+ Cosmos-for-Mongo speaks the MongoDB wire protocol, so it needs no adapter of
332
+ its own — it rides the existing ``MongoDBAdapter`` (and thus the whole polydb
333
+ stack) unchanged. This gives the best of both: Azure-native, serverless /
334
+ autoscale, managed billing + monitoring **and** portability — the same app
335
+ runs on MongoDB Atlas by swapping the connection string.
336
+
337
+ Pass a full ``connection_string`` (recommended — works for both RU and vCore),
338
+ or ``account`` + ``key`` to build the standard Cosmos-for-Mongo (RU) URI.
339
+ """
340
+
341
+ def __init__(
342
+ self,
343
+ name: str = "cosmos",
344
+ *,
345
+ connection_string: str = "",
346
+ account: str = "",
347
+ key: str = "",
348
+ db_name: str = "",
349
+ port: int = 10255,
350
+ ):
351
+ uri = connection_string
352
+ if not uri and account and key:
353
+ uri = (
354
+ f"mongodb://{account}:{key}@{account}.mongo.cosmos.azure.com:{port}/"
355
+ f"?ssl=true&replicaSet=globaldb&retrywrites=false"
356
+ f"&maxIdleTimeMS=120000&appName=@{account}@"
357
+ )
358
+ super().__init__(name=name, mongo_uri=uri, db_name=db_name)
359
+ self.is_cosmos = True
360
+ self.account = account
@@ -0,0 +1,94 @@
1
+ # src/polydb/utils.py
2
+ """
3
+ Utility functions for validation and logging
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import logging
9
+ from typing import Dict, Any
10
+ from .errors import ValidationError
11
+
12
+ # Loggers already configured by setup_logger(), so per-request adapter
13
+ # construction doesn't reconfigure (and re-clobber) them repeatedly.
14
+ _configured_loggers: set[str] = set()
15
+
16
+
17
+ def validate_table_name(table: str) -> str:
18
+ """
19
+ Validate table name to prevent SQL injection
20
+ Only allows alphanumeric, underscore, and hyphen
21
+ """
22
+ if not re.match(r'^[a-zA-Z0-9_-]+$', table):
23
+ raise ValidationError(
24
+ f"Invalid table name: '{table}'. Only alphanumeric, underscore, and hyphen allowed."
25
+ )
26
+ return table
27
+
28
+
29
+ def validate_column_name(column: str) -> str:
30
+ """
31
+ Validate column name to prevent SQL injection
32
+ Only allows alphanumeric and underscore
33
+ """
34
+ if not re.match(r'^[a-zA-Z0-9_]+$', column):
35
+ raise ValidationError(
36
+ f"Invalid column name: '{column}'. Only alphanumeric and underscore allowed."
37
+ )
38
+ return column
39
+
40
+
41
+ def validate_columns(data: Dict[str, Any]) -> Dict[str, Any]:
42
+ """
43
+ Validate all column names in data dictionary
44
+ """
45
+ for key in data.keys():
46
+ validate_column_name(key)
47
+ return data
48
+
49
+
50
+ def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger:
51
+ """Return a logger, configuring it at most once.
52
+
53
+ Adapters are constructed per request, so running the full setup on every
54
+ call would (a) reset a level the embedding app deliberately set and (b)
55
+ re-attach a duplicate plain-text handler — which is why "Initialized Azure
56
+ Queue Storage client" spammed on every request in both plain and JSON form.
57
+
58
+ Behaviour:
59
+ * Idempotent — a given logger name is configured only once.
60
+ * Host-managed root — when the embedding app owns root logging (it sets the
61
+ ``ALTCODEPRO_ROOT_LOGGING_MANAGED`` sentinel), polydb attaches NO handler
62
+ of its own and does NOT force a level. Records propagate to the host's
63
+ formatter and honour whatever level the host pinned (so the host can
64
+ quiet noisy adapters). This is the standard "library shouldn't seize
65
+ logging" contract.
66
+ * Standalone — with no host managing root, keep the original behaviour:
67
+ set the level and install a plain StreamHandler.
68
+ """
69
+ logger = logging.getLogger(name)
70
+ if name in _configured_loggers:
71
+ return logger
72
+ _configured_loggers.add(name)
73
+
74
+ host_managed = bool(os.getenv("ALTCODEPRO_ROOT_LOGGING_MANAGED"))
75
+ if host_managed:
76
+ # Let the host own formatting and level; just propagate.
77
+ return logger
78
+
79
+ # Respect a level the caller already pinned explicitly on this logger.
80
+ if logger.level == logging.NOTSET:
81
+ logger.setLevel(level)
82
+
83
+ # Clear existing handlers to avoid duplication in multiprocess scenarios
84
+ if logger.handlers:
85
+ logger.handlers.clear()
86
+
87
+ handler = logging.StreamHandler()
88
+ formatter = logging.Formatter(
89
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
90
+ )
91
+ handler.setFormatter(formatter)
92
+ logger.addHandler(handler)
93
+
94
+ return logger
@@ -0,0 +1,70 @@
1
+ """Unit tests for the Azure Table empty-filter contract (no Azurite needed).
2
+
3
+ Contract: a filter that carried keys but resolved to no conditions (e.g. every
4
+ value was None/unresolvable) must match NOTHING — [] for a query, None for
5
+ read_one — never a full-table scan that returns a stray context row. Only a
6
+ genuinely empty filter dict ({}) means "list all".
7
+
8
+ This is the bug where read_one("Plan", {tier: None}) returned an unrelated User
9
+ row because the empty query_filter scanned the whole table.
10
+ """
11
+
12
+ import pytest
13
+
14
+ pytest.importorskip("azure.data.tables") # adapter needs the SDK to import
15
+
16
+ from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
17
+
18
+
19
+ class _Model:
20
+ __name__ = "Plan"
21
+
22
+
23
+ class _FakeTableClient:
24
+ def __init__(self, entities):
25
+ self.entities = entities
26
+ self.calls = []
27
+
28
+ def query_entities(self, query_filter=None, **kw):
29
+ self.calls.append(query_filter)
30
+ # Emulate Azure Table: a None filter returns EVERY entity.
31
+ return list(self.entities) if query_filter is None else []
32
+
33
+
34
+ _STRAY = [{"PartitionKey": "t1", "RowKey": "user@x.com",
35
+ "__polydb_model__": "User", "email": "user@x.com"}]
36
+
37
+
38
+ def _adapter(fake):
39
+ adp = object.__new__(AzureTableStorageAdapter) # bypass live-connection __init__
40
+ adp._get_table_client = lambda model: fake
41
+ return adp
42
+
43
+
44
+ def test_none_valued_filter_matches_nothing():
45
+ fake = _FakeTableClient(_STRAY)
46
+ res = _adapter(fake)._query_raw(_Model, {"tier": None}, 1)
47
+ assert res == [] # no stray record
48
+ assert fake.calls == [] # and no full-table scan was issued
49
+
50
+
51
+ def test_empty_filter_lists_all():
52
+ fake = _FakeTableClient(_STRAY)
53
+ res = _adapter(fake)._query_raw(_Model, {}, 10)
54
+ assert fake.calls == [None] # genuine list-all
55
+ assert len(res) == 1
56
+
57
+
58
+ def test_real_filter_queries_normally():
59
+ fake = _FakeTableClient(_STRAY)
60
+ _adapter(fake)._query_raw(_Model, {"tier": "free"}, 1)
61
+ assert fake.calls and "tier eq 'free'" in (fake.calls[0] or "")
62
+
63
+
64
+ def test_query_paged_none_valued_filter_matches_nothing():
65
+ from polydb.models import PageRequest
66
+
67
+ fake = _FakeTableClient(_STRAY)
68
+ page = _adapter(fake).query_paged(_Model, PageRequest(filters={"tier": None}, limit=10))
69
+ assert page.items == [] and page.has_more is False
70
+ assert fake.calls == [] # short-circuited before any scan
@@ -1,61 +0,0 @@
1
- # src/polydb/utils.py
2
- """
3
- Utility functions for validation and logging
4
- """
5
-
6
- import re
7
- import logging
8
- from typing import Dict, Any
9
- from .errors import ValidationError
10
-
11
-
12
- def validate_table_name(table: str) -> str:
13
- """
14
- Validate table name to prevent SQL injection
15
- Only allows alphanumeric, underscore, and hyphen
16
- """
17
- if not re.match(r'^[a-zA-Z0-9_-]+$', table):
18
- raise ValidationError(
19
- f"Invalid table name: '{table}'. Only alphanumeric, underscore, and hyphen allowed."
20
- )
21
- return table
22
-
23
-
24
- def validate_column_name(column: str) -> str:
25
- """
26
- Validate column name to prevent SQL injection
27
- Only allows alphanumeric and underscore
28
- """
29
- if not re.match(r'^[a-zA-Z0-9_]+$', column):
30
- raise ValidationError(
31
- f"Invalid column name: '{column}'. Only alphanumeric and underscore allowed."
32
- )
33
- return column
34
-
35
-
36
- def validate_columns(data: Dict[str, Any]) -> Dict[str, Any]:
37
- """
38
- Validate all column names in data dictionary
39
- """
40
- for key in data.keys():
41
- validate_column_name(key)
42
- return data
43
-
44
-
45
- def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger:
46
- """Setup logger with consistent format"""
47
- logger = logging.getLogger(name)
48
- logger.setLevel(level)
49
-
50
- # Clear existing handlers to avoid duplication in multiprocess scenarios
51
- if logger.hasHandlers():
52
- logger.handlers.clear()
53
-
54
- handler = logging.StreamHandler()
55
- formatter = logging.Formatter(
56
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
57
- )
58
- handler.setFormatter(formatter)
59
- logger.addHandler(handler)
60
-
61
- return logger