altcodepro-polydb-python 2.3.5__tar.gz → 2.3.7__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 (81) hide show
  1. {altcodepro_polydb_python-2.3.5/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.7}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/AzureTableStorageAdapter.py +131 -116
  5. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/base/NoSQLKVAdapter.py +4 -4
  6. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/cloudDatabaseFactory.py +0 -2
  7. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/databaseFactory.py +8 -4
  8. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/LICENSE +0 -0
  9. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/MANIFEST.in +0 -0
  10. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/README.md +0 -0
  11. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/example_usage.py +0 -0
  12. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements-aws.txt +0 -0
  13. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements-azure.txt +0 -0
  14. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements-dev.txt +0 -0
  15. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements-gcp.txt +0 -0
  16. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements-generic.txt +0 -0
  17. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/requirements.txt +0 -0
  18. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/setup.cfg +0 -0
  19. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/setup.py +0 -0
  20. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
  21. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  22. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  23. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  24. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/PolyDB.py +0 -0
  25. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/__init__.py +0 -0
  26. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/EFSAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/S3Adapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/SQSAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/adapters/__init__.py +0 -0
  46. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/advanced_query.py +0 -0
  47. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/audit/AuditStorage.py +0 -0
  48. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/audit/__init__.py +0 -0
  49. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/audit/context.py +0 -0
  50. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/audit/manager.py +0 -0
  51. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/audit/models.py +0 -0
  52. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  53. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/base/QueueAdapter.py +0 -0
  54. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/base/SharedFilesAdapter.py +0 -0
  55. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/base/__init__.py +0 -0
  56. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/batch.py +0 -0
  57. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/cache.py +0 -0
  58. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/decorators.py +0 -0
  59. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/errors.py +0 -0
  60. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/json_safe.py +0 -0
  61. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/models.py +0 -0
  62. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/monitoring.py +0 -0
  63. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/multitenancy.py +0 -0
  64. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/py.typed +0 -0
  65. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/query.py +0 -0
  66. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/registry.py +0 -0
  67. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/retry.py +0 -0
  68. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/schema.py +0 -0
  69. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/security.py +0 -0
  70. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/types.py +0 -0
  71. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/utils.py +0 -0
  72. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/src/polydb/validation.py +0 -0
  73. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_aws.py +0 -0
  74. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_azure.py +0 -0
  75. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_blockchain.py +0 -0
  76. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_cloud_factory.py +0 -0
  77. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_gcp.py +0 -0
  78. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_mongodb.py +0 -0
  79. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_multi_engine.py +0 -0
  80. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/tests/test_postgresql.py +0 -0
  81. {altcodepro_polydb_python-2.3.5 → altcodepro_polydb_python-2.3.7}/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.5
3
+ Version: 2.3.7
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.5"
7
+ version = "2.3.7"
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.5
3
+ Version: 2.3.7
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
@@ -30,6 +30,8 @@ _BASE64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
30
30
  # ensures model isolation across the same table
31
31
  _MODEL_FIELD = "__polydb_model__"
32
32
 
33
+ logging.getLogger("azure").setLevel(logging.ERROR)
34
+
33
35
 
34
36
  class AzureTableStorageAdapter(NoSQLKVAdapter):
35
37
  """
@@ -49,7 +51,6 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
49
51
  self,
50
52
  partition_config: Optional[PartitionConfig] = None,
51
53
  connection_string: str = "",
52
- table_name="",
53
54
  container_name="",
54
55
  ):
55
56
  super().__init__(partition_config)
@@ -57,14 +58,12 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
57
58
  self.connection_string = (
58
59
  connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING") or ""
59
60
  )
60
- self.table_name = table_name or os.getenv("AZURE_TABLE_NAME", "defaulttable") or ""
61
61
  self.container_name = container_name or os.getenv("AZURE_CONTAINER_NAME", "overflow") or ""
62
62
 
63
63
  if not self.connection_string:
64
64
  raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING must be set")
65
65
 
66
- self._client = None
67
- self._table_client = None
66
+ self._client: Any = None
68
67
  self._blob_service = None
69
68
  self._client_lock = threading.Lock()
70
69
  self._initialize_client()
@@ -77,13 +76,6 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
77
76
  with self._client_lock:
78
77
  if not self._client:
79
78
  self._client = TableServiceClient.from_connection_string(self.connection_string)
80
- self._table_client = self._client.get_table_client(self.table_name)
81
-
82
- try:
83
- self._client.create_table_if_not_exists(self.table_name)
84
- except Exception:
85
- pass
86
-
87
79
  self._blob_service = BlobServiceClient.from_connection_string(
88
80
  self.connection_string
89
81
  )
@@ -209,33 +201,46 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
209
201
  # -----------------------------
210
202
  # Entity pack/unpack
211
203
  # -----------------------------
212
-
213
204
  def _pack_entity(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
214
- entity: JsonDict = {"PartitionKey": pk, "RowKey": rk}
215
-
205
+ """Pack data into Azure Table Storage entity format.
206
+
207
+ This version is much simpler, more readable, and fixes the fragile
208
+ revmap/skey logic that was likely causing normal fields to disappear.
209
+ """
210
+ entity: JsonDict = {
211
+ "PartitionKey": pk,
212
+ "RowKey": rk,
213
+ }
216
214
  entity[_MODEL_FIELD] = model.__qualname__
217
-
218
215
  keymap: Dict[str, str] = {}
219
- revmap: Dict[str, str] = {}
220
216
 
221
217
  for orig_key, orig_val in (data or {}).items():
222
- if str(orig_key) in self._RESERVED or str(orig_key) in ("PartitionKey", "RowKey"):
218
+ orig_key_str = str(orig_key)
219
+
220
+ # Skip Azure-reserved or special keys
221
+ if orig_key_str in self._RESERVED or orig_key_str in ("PartitionKey", "RowKey"):
223
222
  continue
224
223
 
225
- skey = revmap.get(str(orig_key))
226
- if not skey:
227
- skey = self._sanitize_prop_name(orig_key)
228
- base = skey
229
- i = 1
230
- while skey in entity:
231
- skey = f"{base}_{i}"
232
- i += 1
233
- revmap[str(orig_key)] = skey
234
- keymap[skey] = str(orig_key)
224
+ # Sanitize property name so it is valid for Azure Table Storage
225
+ skey = self._sanitize_prop_name(orig_key)
226
+
227
+ # Prevent key collisions (extremely rare but safe)
228
+ base = skey
229
+ counter = 1
230
+ while skey in entity:
231
+ skey = f"{base}_{counter}"
232
+ counter += 1
233
+
234
+ # Remember the original key name so _unpack_entity can restore it
235
+ keymap[skey] = orig_key_str
235
236
 
237
+ # Encode the value (must return something Azure Table accepts)
236
238
  entity[skey] = self._encode_value(orig_val)
237
239
 
238
- entity["__keymap__"] = json.dumps(keymap, default=json_safe)
240
+ # Store keymap only if we actually have fields (internal use)
241
+ if keymap:
242
+ entity["__keymap__"] = json.dumps(keymap, default=json_safe)
243
+
239
244
  return entity
240
245
 
241
246
  def _unpack_entity(self, entity: JsonDict) -> JsonDict:
@@ -323,105 +328,122 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
323
328
  # -----------------------------
324
329
  # Required NoSQLKVAdapter hooks
325
330
  # -----------------------------
331
+ def _sanitize_table_name(self, name: str) -> str:
332
+ """Convert collection_name to valid Azure Table name (alphanumeric only)."""
333
+ if not name:
334
+ return "defaulttable"
335
+
336
+ # Remove all invalid characters, keep only letters and numbers
337
+ sanitized = re.sub(r"[^a-zA-Z0-9]", "", name)
338
+
339
+ # Must start with a letter
340
+ if sanitized and sanitized[0].isdigit():
341
+ sanitized = "t" + sanitized
342
+
343
+ # Must be 3-63 characters
344
+ if len(sanitized) < 3:
345
+ sanitized = sanitized + "table"
346
+ if len(sanitized) > 63:
347
+ sanitized = sanitized[:63]
348
+
349
+ return sanitized.lower()
350
+
351
+ def _get_table_name(self, model: type) -> str:
352
+ """
353
+ Extract collection_name from UDL model definition.
354
+ """
355
+ # Primary source: UDL definition
356
+ definition = getattr(model, "__udl_definition__", None)
357
+ if definition:
358
+ metadata = getattr(definition, "x_metadata", {}) or {}
359
+ collection_name = metadata.get("collection_name")
360
+ if collection_name:
361
+ return self._sanitize_table_name(collection_name)
362
+
363
+ # Fallback: __polydb__ metadata
364
+ polydb_meta = getattr(model, "__polydb__", None)
365
+ if isinstance(polydb_meta, dict):
366
+ collection = polydb_meta.get("collection") or polydb_meta.get("collection_name")
367
+ if collection:
368
+ return self._sanitize_table_name(str(collection))
369
+
370
+ # Last resort
371
+ return os.getenv("AZURE_TABLE_NAME", "defaulttable") or "defaulttable"
372
+
373
+ def _get_table_client(self, model: type):
374
+ """
375
+ Get table client + automatically create the table if it doesn't exist.
376
+ This is the recommended pattern for Azure Table Storage.
377
+ """
378
+ table_name = self._get_table_name(model)
379
+
380
+ try:
381
+ # This is the key call - creates the table if missing
382
+ self._client.create_table_if_not_exists(table_name)
383
+ logger.info(f"✅ Azure Table ensured/created: {table_name}")
384
+ except Exception as e:
385
+ # TableAlreadyExists is normal and safe to ignore
386
+ if "TableAlreadyExists" not in str(e) and "already exists" not in str(e).lower():
387
+ logger.warning(f"Could not create table {table_name}: {e}")
388
+
389
+ # Now return the client
390
+ return self._client.get_table_client(table_name=table_name)
326
391
 
327
392
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
328
393
  def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
329
394
  try:
330
- if not self._table_client:
331
- raise NoSQLError("Azure Table client not initialized")
332
-
395
+ self._table_client = self._get_table_client(model)
333
396
  safe_pk = self._sanitize_pk_rk(pk)
334
397
  safe_rk = self._sanitize_pk_rk(rk)
335
-
336
398
  # Pack entity (encoded for Azure Table)
337
399
  entity = self._pack_entity(model, safe_pk, safe_rk, data)
338
-
339
400
  # ---------------------------------------------------
340
401
  # SIZE ESTIMATION (use ORIGINAL payload, not packed)
341
402
  # ---------------------------------------------------
342
403
  MAX_PROPERTY_CHARS = 30 * 1024 # ~30K safe under UTF-16 32K limit
343
- MAX_ENTITY_SIZE = 40 * 1024 # conservative total threshold
344
404
 
345
405
  def _is_large_string(val: Any) -> bool:
346
406
  return isinstance(val, str) and len(val) > MAX_PROPERTY_CHARS
347
407
 
348
- has_large_property = False
349
- large_key = None
350
-
408
+ large_val_dict = {}
351
409
  for key, value in entity.items():
352
410
  if _is_large_string(value):
353
- has_large_property = True
354
- large_key = key
355
411
  logger.warning(
356
412
  f"LARGE PROPERTY DETECTED: {key} length={len(value)} chars → forcing blob overflow"
357
413
  )
358
- break
359
-
360
- payload_json = json.dumps(data, default=json_safe)
361
- payload_bytes = payload_json.encode("utf-8")
362
- payload_size = len(payload_bytes)
363
-
364
- force_blob = has_large_property or payload_size > MAX_ENTITY_SIZE
365
-
366
- # ---------------------------------------------------
367
- # BLOB OVERFLOW PATH
368
- # ---------------------------------------------------
369
- if force_blob:
370
- logger.info(
371
- f"Entity exceeds safe limits → using blob storage "
372
- f"(size={payload_size // 1024} KB, large_key={large_key})"
373
- )
374
-
375
- checksum = hashlib.md5(payload_bytes).hexdigest()
376
- blob_key = self._blob_key(safe_pk, safe_rk, checksum)
377
-
378
- # Upload ORIGINAL payload (not packed entity)
379
- self._blob_upload(blob_key, payload_bytes)
380
-
381
- # Build reference entity
382
- reference_entity = {
383
- "PartitionKey": safe_pk,
384
- "RowKey": safe_rk,
385
- _MODEL_FIELD: model.__qualname__,
386
- "_overflow": True,
387
- "_blob_key": blob_key,
388
- "_size": payload_size,
389
- "_checksum": checksum,
390
- "__keymap__": entity.get("__keymap__", "{}"),
391
- }
392
-
393
- # Keep only small queryable fields
394
- for k, v in entity.items():
395
- if k in ("PartitionKey", "RowKey", "__keymap__", _MODEL_FIELD):
396
- continue
397
- if k.startswith("_"):
398
- continue
399
-
400
- if isinstance(v, (str, bool, int, float)):
401
- if isinstance(v, str) and len(v) > 2000:
402
- continue
403
- reference_entity[k] = v
404
-
405
- elif isinstance(v, datetime):
406
- reference_entity[k] = v
407
-
408
- self._table_client.upsert_entity(reference_entity)
409
-
410
- logger.info(f"Stored in blob: {blob_key} ({payload_size // 1024} KB)")
411
-
412
- restored = self._unpack_entity(entity)
413
- restored["_overflow"] = True
414
- restored["_blob_key"] = blob_key
415
- restored["_checksum"] = checksum
416
- restored["_size"] = payload_size
417
- restored["id"] = safe_rk
418
-
419
- return restored
414
+ payload_json = json.dumps(value, default=json_safe)
415
+ payload_bytes = payload_json.encode("utf-8")
416
+ payload_size = len(payload_bytes)
417
+ checksum = hashlib.md5(payload_bytes).hexdigest()
418
+ val_key = self._sanitize_blob_part(key)
419
+ blob_key = self._blob_key(f"{safe_pk}_{safe_rk}", val_key, checksum)
420
+ self._blob_upload(blob_key, payload_bytes)
421
+ logger.info(f"Stored in blob: {blob_key} ({payload_size // 1024} KB)")
422
+ large_val_dict[key] = {
423
+ "PartitionKey": safe_pk,
424
+ "RowKey": safe_rk,
425
+ _MODEL_FIELD: model.__qualname__,
426
+ "_overflow": True,
427
+ "_blob_key": blob_key,
428
+ "_size": payload_size,
429
+ "_checksum": checksum,
430
+ }
431
+
432
+ reference_entity = {
433
+ _MODEL_FIELD: model.__qualname__,
434
+ }
435
+ for k, v in entity.items():
436
+ if k.startswith("_"):
437
+ continue
438
+ if k in large_val_dict:
439
+ metadata = large_val_dict[k]
440
+ reference_entity[k] = json.dumps(metadata, default=json_safe) # ← JSON string
441
+ logger.info(f"Overflow reference stored for {k} → {metadata['_blob_key']}")
442
+ else:
443
+ reference_entity[k] = v
420
444
 
421
- # ---------------------------------------------------
422
- # NORMAL TABLE STORAGE
423
- # ---------------------------------------------------
424
- self._table_client.upsert_entity(entity)
445
+ self._table_client = self._get_table_client(model)
446
+ self._table_client.upsert_entity(reference_entity)
425
447
 
426
448
  restored = self._unpack_entity(entity)
427
449
  restored["id"] = safe_rk
@@ -437,12 +459,9 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
437
459
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
438
460
  def _get_raw(self, model: type, pk: str, rk: str) -> Optional[JsonDict]:
439
461
  try:
440
- if not self._table_client:
441
- return None
442
-
443
462
  safe_pk = self._sanitize_pk_rk(pk)
444
463
  safe_rk = self._sanitize_pk_rk(rk)
445
-
464
+ self._table_client = self._get_table_client(model)
446
465
  entity = self._table_client.get_entity(safe_pk, safe_rk)
447
466
  entity_dict = dict(entity)
448
467
 
@@ -487,12 +506,10 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
487
506
  self, model: type, filters: Dict[str, Any], limit: Optional[int]
488
507
  ) -> List[JsonDict]:
489
508
  try:
490
- if not self._table_client:
491
- return []
492
-
509
+ self._table_client = self._get_table_client(model)
493
510
  # always enforce model filter
494
511
  eff_filters = dict(filters or {})
495
- eff_filters[_MODEL_FIELD] = model.__qualname__
512
+ # eff_filters[_MODEL_FIELD] = model.__qualname__
496
513
 
497
514
  parts: List[str] = []
498
515
  for orig_k, orig_v in eff_filters.items():
@@ -576,9 +593,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
576
593
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
577
594
  def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
578
595
  try:
579
- if not self._table_client:
580
- return {"deleted": False}
581
-
596
+ self._table_client = self._get_table_client(model)
582
597
  safe_pk = self._sanitize_pk_rk(pk)
583
598
  safe_rk = self._sanitize_pk_rk(rk)
584
599
 
@@ -36,8 +36,8 @@ class NoSQLKVAdapter:
36
36
  def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
37
37
  """Extract PK/RK from model metadata"""
38
38
  meta = getattr(model, "__polydb__", {})
39
- pk_field = meta.get("pk_field", "id")
40
- rk_field = meta.get("rk_field")
39
+ pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
40
+ rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
41
41
 
42
42
  if self.partition_config:
43
43
  try:
@@ -217,8 +217,7 @@ class NoSQLKVAdapter:
217
217
  # Protocol implementation
218
218
  def put(self, model: type, data: JsonDict) -> JsonDict:
219
219
  pk, rk = self._get_pk_rk(model, data)
220
- store_data, _ = self._check_overflow(data)
221
- return self._put_raw(model, pk, rk, store_data)
220
+ return self._put_raw(model, pk, rk, data)
222
221
 
223
222
  def query(
224
223
  self,
@@ -257,6 +256,7 @@ class NoSQLKVAdapter:
257
256
  if isinstance(entity_id, dict):
258
257
  pk = entity_id.get("partition_key") or entity_id.get("pk")
259
258
  rk = entity_id.get("row_key") or entity_id.get("rk") or entity_id.get("id")
259
+
260
260
  else:
261
261
  pk, rk = self._get_pk_rk(model, {"id": entity_id})
262
262
 
@@ -234,7 +234,6 @@ class CloudDatabaseFactory:
234
234
  from .adapters.AzureTableStorageAdapter import AzureTableStorageAdapter
235
235
 
236
236
  connection_string = ""
237
- table_name = ""
238
237
  container_name = ""
239
238
 
240
239
  if isinstance(cfg, AzureTableConfig):
@@ -245,7 +244,6 @@ class CloudDatabaseFactory:
245
244
  instance = AzureTableStorageAdapter(
246
245
  partition_config=partition_config,
247
246
  connection_string=connection_string,
248
- table_name=table_name,
249
247
  container_name=container_name,
250
248
  )
251
249
 
@@ -20,6 +20,10 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
20
20
 
21
21
  from tenacity import retry, stop_after_attempt, wait_exponential
22
22
 
23
+ from .adapters.PostgreSQLAdapter import PostgreSQLAdapter
24
+
25
+ from .base.NoSQLKVAdapter import NoSQLKVAdapter
26
+
23
27
  from .batch import BatchOperations
24
28
  from .cache import CacheWarmer, RedisCacheEngine
25
29
  from .monitoring import HealthCheck, MetricsCollector, PerformanceMonitor
@@ -81,8 +85,8 @@ class EngineOverride:
81
85
 
82
86
  @dataclass
83
87
  class _ResolvedAdapters:
84
- sql: Any
85
- nosql: Any
88
+ sql: PostgreSQLAdapter
89
+ nosql: NoSQLKVAdapter
86
90
  engine_name: str
87
91
 
88
92
 
@@ -281,14 +285,14 @@ class DatabaseFactory:
281
285
  return self._engine_by_name[name]
282
286
 
283
287
  @property
284
- def _sql(self) -> Any:
288
+ def _sql(self) -> PostgreSQLAdapter:
285
289
  for e in self._engines:
286
290
  if e.is_default_sql:
287
291
  return e.sql()
288
292
  return self._engines[0].sql()
289
293
 
290
294
  @property
291
- def _nosql(self) -> Any:
295
+ def _nosql(self) -> NoSQLKVAdapter:
292
296
  for e in self._engines:
293
297
  if e.is_default_nosql:
294
298
  return e.nosql()