altcodepro-polydb-python 2.2.2__py3-none-any.whl → 2.2.4__py3-none-any.whl

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 (38) hide show
  1. altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
  2. altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
  3. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
  4. polydb/__init__.py +2 -2
  5. polydb/adapters/AzureBlobStorageAdapter.py +146 -41
  6. polydb/adapters/AzureFileStorageAdapter.py +148 -43
  7. polydb/adapters/AzureQueueAdapter.py +96 -34
  8. polydb/adapters/AzureTableStorageAdapter.py +462 -119
  9. polydb/adapters/BlockchainBlobAdapter.py +111 -0
  10. polydb/adapters/BlockchainKVAdapter.py +152 -0
  11. polydb/adapters/BlockchainQueueAdapter.py +116 -0
  12. polydb/adapters/DynamoDBAdapter.py +463 -176
  13. polydb/adapters/FirestoreAdapter.py +320 -148
  14. polydb/adapters/GCPPubSubAdapter.py +217 -0
  15. polydb/adapters/GCPStorageAdapter.py +184 -39
  16. polydb/adapters/MongoDBAdapter.py +159 -39
  17. polydb/adapters/PostgreSQLAdapter.py +285 -83
  18. polydb/adapters/S3Adapter.py +172 -35
  19. polydb/adapters/S3CompatibleAdapter.py +62 -8
  20. polydb/adapters/SQSAdapter.py +121 -44
  21. polydb/adapters/VercelBlobAdapter.py +196 -0
  22. polydb/adapters/VercelKVAdapter.py +275 -283
  23. polydb/adapters/VercelQueueAdapter.py +61 -0
  24. polydb/audit/AuditStorage.py +1 -1
  25. polydb/base/NoSQLKVAdapter.py +113 -101
  26. polydb/base/ObjectStorageAdapter.py +42 -6
  27. polydb/base/QueueAdapter.py +2 -2
  28. polydb/base/SharedFilesAdapter.py +2 -2
  29. polydb/cloudDatabaseFactory.py +200 -0
  30. polydb/databaseFactory.py +434 -101
  31. polydb/models.py +63 -1
  32. polydb/query.py +111 -42
  33. altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
  34. altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
  35. polydb/adapters/PubSubAdapter.py +0 -85
  36. polydb/factory.py +0 -107
  37. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
  38. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,19 @@
1
1
  # src/polydb/adapters/AzureTableStorageAdapter.py
2
+
3
+ from __future__ import annotations
4
+
2
5
  import os
6
+ import re
7
+ import json
8
+ import base64
9
+ import hashlib
3
10
  import threading
11
+ from datetime import datetime, date
12
+ from decimal import Decimal
4
13
  from typing import Any, Dict, List, Optional
5
- from polydb.base.NoSQLKVAdapter import NoSQLKVAdapter
14
+ from uuid import UUID
15
+
16
+ from ..base.NoSQLKVAdapter import NoSQLKVAdapter
6
17
  from ..json_safe import json_safe
7
18
  from ..errors import NoSQLError, ConnectionError
8
19
  from ..retry import retry
@@ -10,174 +21,506 @@ from ..types import JsonDict
10
21
  from ..models import PartitionConfig
11
22
 
12
23
 
24
+ _BYTES_PREFIX = "@@polydb_bytes@@:"
25
+ _JSON_PREFIX = "@@polydb_json@@:"
26
+ _BASE64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
27
+
28
+ # ensures model isolation across the same table
29
+ _MODEL_FIELD = "__polydb_model__"
30
+
31
+
13
32
  class AzureTableStorageAdapter(NoSQLKVAdapter):
14
- """Azure Table Storage with Azure Blob overflow (limit: 1MB per entity)"""
15
-
33
+ """
34
+ Azure Table Storage adapter with:
35
+ - Any-type payload support (dict/list/custom objects -> JSON)
36
+ - Key sanitization for PartitionKey/RowKey and property names
37
+ - Query support for scalar fields
38
+ - Blob overflow for entities > 1MB
39
+ - Model isolation using __polydb_model__
40
+ - Always returns id (derived from RowKey if missing)
41
+ """
42
+
16
43
  AZURE_TABLE_MAX_SIZE = 1024 * 1024 # 1MB
17
-
18
- def __init__(self, partition_config: Optional[PartitionConfig] = None):
44
+ _RESERVED = {"PartitionKey", "RowKey", "Timestamp", "etag", "ETag"}
45
+
46
+ def __init__(
47
+ self,
48
+ partition_config: Optional[PartitionConfig] = None,
49
+ connection_string: str = "",
50
+ table_name="",
51
+ container_name="",
52
+ ):
19
53
  super().__init__(partition_config)
20
54
  self.max_size = self.AZURE_TABLE_MAX_SIZE
21
- self.connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING") or ""
22
- self.table_name = os.getenv("AZURE_TABLE_NAME", "defaulttable") or ""
23
- self.container_name = os.getenv("AZURE_CONTAINER_NAME", "overflow") or ""
55
+ self.connection_string = (
56
+ connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING") or ""
57
+ )
58
+ self.table_name = table_name or os.getenv("AZURE_TABLE_NAME", "defaulttable") or ""
59
+ self.container_name = container_name or os.getenv("AZURE_CONTAINER_NAME", "overflow") or ""
60
+
61
+ if not self.connection_string:
62
+ raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING must be set")
63
+
24
64
  self._client = None
25
65
  self._table_client = None
26
66
  self._blob_service = None
27
67
  self._client_lock = threading.Lock()
28
68
  self._initialize_client()
29
-
69
+
30
70
  def _initialize_client(self):
31
71
  try:
32
72
  from azure.data.tables import TableServiceClient
33
73
  from azure.storage.blob import BlobServiceClient
34
-
74
+
35
75
  with self._client_lock:
36
76
  if not self._client:
37
77
  self._client = TableServiceClient.from_connection_string(self.connection_string)
38
78
  self._table_client = self._client.get_table_client(self.table_name)
39
- self._blob_service = BlobServiceClient.from_connection_string(self.connection_string)
79
+
80
+ try:
81
+ self._client.create_table_if_not_exists(self.table_name)
82
+ except Exception:
83
+ pass
84
+
85
+ self._blob_service = BlobServiceClient.from_connection_string(
86
+ self.connection_string
87
+ )
88
+
40
89
  try:
41
90
  self._blob_service.create_container(self.container_name)
42
- except:
43
- pass # Already exists
44
-
91
+ except Exception:
92
+ pass
93
+
45
94
  self.logger.info("Azure Table Storage initialized with Blob overflow")
46
95
  except Exception as e:
47
96
  raise ConnectionError(f"Azure Table init failed: {str(e)}")
48
-
97
+
98
+ # -----------------------------
99
+ # Key / property sanitization
100
+ # -----------------------------
101
+
102
+ def _sanitize_pk_rk(self, value: Any) -> str:
103
+ s = str(value)
104
+ s = re.sub(r"[\\/#\?\x00-\x1f\x7f:+ ]", "_", s)
105
+ return s[:1024] if len(s) > 1024 else s
106
+
107
+ def _sanitize_prop_name(self, name: Any) -> str:
108
+ s = str(name)
109
+ s = re.sub(r"[^A-Za-z0-9_]", "_", s)
110
+ if not re.match(r"^[A-Za-z_]", s):
111
+ s = f"f_{s}"
112
+ if s in self._RESERVED:
113
+ s = f"f_{s}"
114
+ return s[:255] if len(s) > 255 else s
115
+
116
+ # -----------------------------
117
+ # Value encoding / decoding
118
+ # -----------------------------
119
+
120
+ def _encode_value(self, v: Any) -> Any:
121
+ if v is None:
122
+ return None
123
+
124
+ if isinstance(v, bytes):
125
+ return _BYTES_PREFIX + base64.b64encode(v).decode("ascii")
126
+
127
+ if isinstance(v, (dict, list)):
128
+ return _JSON_PREFIX + json.dumps(v, default=json_safe)
129
+
130
+ if isinstance(v, UUID):
131
+ return str(v)
132
+
133
+ if isinstance(v, Decimal):
134
+ return float(v)
135
+
136
+ if isinstance(v, date) and not isinstance(v, datetime):
137
+ return v.isoformat()
138
+
139
+ if isinstance(v, datetime):
140
+ return v
141
+
142
+ if isinstance(v, (str, bool, int, float)):
143
+ return v
144
+
145
+ return _JSON_PREFIX + json.dumps(v, default=json_safe)
146
+
147
+ def _decode_value(self, v: Any) -> Any:
148
+ if isinstance(v, str):
149
+ if v.startswith(_JSON_PREFIX):
150
+ payload = v[len(_JSON_PREFIX) :]
151
+ try:
152
+ return json.loads(payload)
153
+ except Exception:
154
+ return v
155
+
156
+ if v.startswith(_BYTES_PREFIX):
157
+ payload = v[len(_BYTES_PREFIX) :].strip()
158
+ if (len(payload) % 4) == 1:
159
+ return v
160
+ if not _BASE64_RE.match(payload):
161
+ return v
162
+ pad = (-len(payload)) % 4
163
+ if pad:
164
+ payload = payload + ("=" * pad)
165
+ try:
166
+ return base64.b64decode(payload, validate=True)
167
+ except TypeError:
168
+ try:
169
+ return base64.b64decode(payload)
170
+ except Exception:
171
+ return v
172
+ except Exception:
173
+ return v
174
+ return v
175
+
176
+ if isinstance(v, dict) and "__type__" in v:
177
+ t = v.get("__type__")
178
+ if t == "json":
179
+ raw = v.get("value")
180
+ try:
181
+ return json.loads(raw or "null")
182
+ except Exception:
183
+ return raw
184
+ if t == "bytes":
185
+ payload = (v.get("b64") or "").strip()
186
+ if (len(payload) % 4) == 1:
187
+ return b""
188
+ if payload and not _BASE64_RE.match(payload):
189
+ return b""
190
+ pad = (-len(payload)) % 4
191
+ if pad:
192
+ payload = payload + ("=" * pad)
193
+ try:
194
+ return base64.b64decode(payload, validate=True)
195
+ except TypeError:
196
+ try:
197
+ return base64.b64decode(payload)
198
+ except Exception:
199
+ return b""
200
+ except Exception:
201
+ return b""
202
+ if t == "str":
203
+ return v.get("value")
204
+
205
+ return v
206
+
207
+ # -----------------------------
208
+ # Entity pack/unpack
209
+ # -----------------------------
210
+
211
+ def _pack_entity(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
212
+ entity: JsonDict = {"PartitionKey": pk, "RowKey": rk}
213
+
214
+ # ✅ model isolation
215
+ entity[_MODEL_FIELD] = model.__qualname__
216
+
217
+ keymap: Dict[str, str] = {}
218
+ revmap: Dict[str, str] = {}
219
+
220
+ for orig_key, orig_val in (data or {}).items():
221
+ if str(orig_key) in self._RESERVED or str(orig_key) in ("PartitionKey", "RowKey"):
222
+ continue
223
+
224
+ skey = revmap.get(str(orig_key))
225
+ if not skey:
226
+ skey = self._sanitize_prop_name(orig_key)
227
+ base = skey
228
+ i = 1
229
+ while skey in entity:
230
+ skey = f"{base}_{i}"
231
+ i += 1
232
+ revmap[str(orig_key)] = skey
233
+ keymap[skey] = str(orig_key)
234
+
235
+ entity[skey] = self._encode_value(orig_val)
236
+
237
+ entity["__keymap__"] = json.dumps(keymap, default=json_safe)
238
+ return entity
239
+
240
+ def _unpack_entity(self, entity: JsonDict) -> JsonDict:
241
+ if not entity:
242
+ return {}
243
+
244
+ raw = dict(entity)
245
+ raw.pop("etag", None)
246
+ raw.pop("ETag", None)
247
+ raw.pop("Timestamp", None)
248
+
249
+ keymap_str = raw.pop("__keymap__", None)
250
+ keymap: Dict[str, str] = {}
251
+ if keymap_str:
252
+ try:
253
+ keymap = json.loads(keymap_str)
254
+ except Exception:
255
+ keymap = {}
256
+
257
+ out: JsonDict = {}
258
+
259
+ pk = raw.get("PartitionKey")
260
+ rk = raw.get("RowKey")
261
+ if pk is not None:
262
+ out["PartitionKey"] = pk
263
+ if rk is not None:
264
+ out["RowKey"] = rk
265
+
266
+ for k, v in raw.items():
267
+ if k in ("PartitionKey", "RowKey"):
268
+ continue
269
+
270
+ # keep internal metadata fields too
271
+ if k.startswith("_") or k in (_MODEL_FIELD,):
272
+ out[k] = v
273
+ continue
274
+
275
+ orig_key = keymap.get(k, k)
276
+ out[orig_key] = self._decode_value(v)
277
+
278
+ # ✅ guarantee id for tests & ergonomics
279
+ if "id" not in out and rk is not None:
280
+ out["id"] = rk
281
+
282
+ return out
283
+
284
+ def _entity_size_bytes(self, entity: JsonDict) -> int:
285
+ return len(json.dumps(entity, default=json_safe).encode("utf-8"))
286
+
287
+ def _blob_key(self, pk: str, rk: str, checksum: str) -> str:
288
+ return f"{pk}/{rk}/{checksum}.json"
289
+
290
+ def _blob_upload(self, blob_key: str, data_bytes: bytes):
291
+ if not self._blob_service:
292
+ return
293
+ blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
294
+ blob_client.upload_blob(data_bytes, overwrite=True)
295
+
296
+ def _blob_download(self, blob_key: str) -> bytes:
297
+ if not self._blob_service:
298
+ raise NoSQLError("Blob service not initialized")
299
+ blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
300
+ return blob_client.download_blob().readall()
301
+
302
+ def _blob_delete(self, blob_key: str):
303
+ if not self._blob_service:
304
+ return
305
+ blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
306
+ try:
307
+ blob_client.delete_blob()
308
+ except Exception:
309
+ pass
310
+
311
+ # -----------------------------
312
+ # Required NoSQLKVAdapter hooks
313
+ # -----------------------------
314
+
49
315
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
50
316
  def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
51
317
  try:
52
- import json
53
- import hashlib
54
-
55
- data_copy = dict(data)
56
- data_copy['PartitionKey'] = pk
57
- data_copy['RowKey'] = rk
58
-
59
- # Check size
60
- data_bytes = json.dumps(data_copy,default=json_safe).encode()
61
- data_size = len(data_bytes)
62
-
63
- if data_size > self.AZURE_TABLE_MAX_SIZE:
64
- # Store in Blob
65
- blob_id = hashlib.md5(data_bytes).hexdigest()
66
- blob_key = f"overflow/{pk}/{rk}/{blob_id}.json"
67
-
68
- if self._blob_service:
69
- blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
70
- blob_client.upload_blob(data_bytes, overwrite=True)
71
- self.logger.info(f"Stored overflow to Blob: {blob_key} ({data_size} bytes)")
72
-
73
- # Store reference in table
74
- reference_data = {
75
- 'PartitionKey': pk,
76
- 'RowKey': rk,
77
- '_overflow': True,
78
- '_blob_key': blob_key,
79
- '_size': data_size,
80
- '_checksum': blob_id,
318
+ if not self._table_client:
319
+ raise NoSQLError("Azure Table client not initialized")
320
+
321
+ safe_pk = self._sanitize_pk_rk(pk)
322
+ safe_rk = self._sanitize_pk_rk(rk)
323
+
324
+ entity = self._pack_entity(model, safe_pk, safe_rk, data)
325
+
326
+ size = self._entity_size_bytes(entity)
327
+ if size > self.AZURE_TABLE_MAX_SIZE:
328
+ full_payload_bytes = json.dumps(entity, default=json_safe).encode("utf-8")
329
+ checksum = hashlib.md5(full_payload_bytes).hexdigest()
330
+ blob_key = self._blob_key(safe_pk, safe_rk, checksum)
331
+ self._blob_upload(blob_key, full_payload_bytes)
332
+
333
+ reference_entity: JsonDict = {
334
+ "PartitionKey": safe_pk,
335
+ "RowKey": safe_rk,
336
+ _MODEL_FIELD: model.__qualname__,
337
+ "_overflow": True,
338
+ "_blob_key": blob_key,
339
+ "_size": len(full_payload_bytes),
340
+ "_checksum": checksum,
341
+ "__keymap__": entity.get("__keymap__", "{}"),
81
342
  }
82
-
83
- if self._table_client:
84
- self._table_client.upsert_entity(reference_data)
85
- else:
86
- # Store directly in table
87
- if self._table_client:
88
- self._table_client.upsert_entity(data_copy)
89
-
90
- return {'PartitionKey': pk, 'RowKey': rk}
343
+
344
+ # keep a small index of scalars for basic filtering
345
+ kept = 0
346
+ for k, v in entity.items():
347
+ if k in ("PartitionKey", "RowKey", "__keymap__", _MODEL_FIELD):
348
+ continue
349
+ if k.startswith("_"):
350
+ continue
351
+ if v is None or isinstance(v, (str, bool, int, float, datetime)):
352
+ reference_entity[k] = v
353
+ kept += 1
354
+ if kept >= 50:
355
+ break
356
+
357
+ self._table_client.upsert_entity(reference_entity)
358
+ return {
359
+ "PartitionKey": safe_pk,
360
+ "RowKey": safe_rk,
361
+ "_overflow": True,
362
+ "id": safe_rk,
363
+ }
364
+
365
+ self._table_client.upsert_entity(entity)
366
+ return {"PartitionKey": safe_pk, "RowKey": safe_rk, "id": safe_rk}
367
+
91
368
  except Exception as e:
92
369
  raise NoSQLError(f"Azure Table put failed: {str(e)}")
93
-
370
+
94
371
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
95
372
  def _get_raw(self, model: type, pk: str, rk: str) -> Optional[JsonDict]:
96
373
  try:
97
- import json
98
- import hashlib
99
-
100
374
  if not self._table_client:
101
375
  return None
102
-
103
- entity = self._table_client.get_entity(pk, rk)
376
+
377
+ safe_pk = self._sanitize_pk_rk(pk)
378
+ safe_rk = self._sanitize_pk_rk(rk)
379
+
380
+ entity = self._table_client.get_entity(safe_pk, safe_rk)
104
381
  entity_dict = dict(entity)
105
-
106
- # Check if overflow
107
- if entity_dict.get('_overflow'):
108
- blob_key = entity_dict.get('_blob_key')
109
- checksum = entity_dict.get('_checksum')
110
-
111
- if blob_key and self._blob_service:
112
- blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
113
- blob_data = blob_client.download_blob().readall()
114
-
115
- # Verify checksum
116
- actual_checksum = hashlib.md5(blob_data).hexdigest()
117
- if actual_checksum != checksum:
118
- raise NoSQLError(f"Checksum mismatch: expected {checksum}, got {actual_checksum}")
119
-
120
- retrieved = json.loads(blob_data.decode())
121
- self.logger.debug(f"Retrieved overflow from Blob: {blob_key}")
122
- return retrieved
123
-
124
- return entity_dict
382
+
383
+ # model isolation
384
+ if entity_dict.get(_MODEL_FIELD) != model.__qualname__:
385
+ return None
386
+
387
+ if entity_dict.get("_overflow"):
388
+ blob_key = entity_dict.get("_blob_key")
389
+ checksum = entity_dict.get("_checksum")
390
+ if not blob_key:
391
+ raise NoSQLError("Overflow entity missing _blob_key")
392
+
393
+ blob_data = self._blob_download(blob_key)
394
+ actual_checksum = hashlib.md5(blob_data).hexdigest()
395
+ if checksum and actual_checksum != checksum:
396
+ raise NoSQLError(
397
+ f"Checksum mismatch: expected {checksum}, got {actual_checksum}"
398
+ )
399
+
400
+ restored = json.loads(blob_data.decode("utf-8"))
401
+ out = self._unpack_entity(restored)
402
+ if "id" not in out:
403
+ out["id"] = safe_rk
404
+ return out
405
+
406
+ out = self._unpack_entity(entity_dict)
407
+ if "id" not in out:
408
+ out["id"] = safe_rk
409
+ return out
410
+
125
411
  except Exception as e:
126
412
  if "ResourceNotFound" in str(e):
127
413
  return None
128
414
  raise NoSQLError(f"Azure Table get failed: {str(e)}")
129
-
415
+
130
416
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
131
- def _query_raw(self, model: type, filters: Dict[str, Any], limit: Optional[int]) -> List[JsonDict]:
417
+ def _query_raw(
418
+ self, model: type, filters: Dict[str, Any], limit: Optional[int]
419
+ ) -> List[JsonDict]:
132
420
  try:
133
421
  if not self._table_client:
134
422
  return []
135
-
136
- query_filter = None
137
- if filters:
138
- filter_parts = []
139
- for k, v in filters.items():
140
- if isinstance(v, str):
141
- filter_parts.append(f"{k} eq '{v}'")
142
- elif isinstance(v, bool):
143
- filter_parts.append(f"{k} eq {str(v).lower()}")
423
+
424
+ # always enforce model filter
425
+ eff_filters = dict(filters or {})
426
+ eff_filters[_MODEL_FIELD] = model.__qualname__
427
+
428
+ parts: List[str] = []
429
+ for orig_k, orig_v in eff_filters.items():
430
+ if orig_v is None:
431
+ continue
432
+
433
+ if orig_k in ("partition_key", "PartitionKey"):
434
+ sk = "PartitionKey"
435
+ elif orig_k in ("row_key", "RowKey"):
436
+ sk = "RowKey"
437
+ else:
438
+ sk = (
439
+ self._sanitize_prop_name(orig_k) if orig_k != _MODEL_FIELD else _MODEL_FIELD
440
+ )
441
+
442
+ ev = self._encode_value(orig_v)
443
+
444
+ if ev is None:
445
+ parts.append(f"{sk} eq null")
446
+ elif isinstance(ev, bool):
447
+ parts.append(f"{sk} eq {str(ev).lower()}")
448
+ elif isinstance(ev, (int, float)):
449
+ parts.append(f"{sk} eq {ev}")
450
+ elif isinstance(ev, datetime):
451
+ iso = ev.isoformat()
452
+ if not iso.endswith("Z"):
453
+ iso = iso + "Z"
454
+ parts.append(f"{sk} eq datetime'{iso}'")
455
+ else:
456
+ sval = str(ev).replace("'", "''")
457
+ parts.append(f"{sk} eq '{sval}'")
458
+
459
+ query_filter = " and ".join(parts) if parts else None
460
+
461
+ entities = self._table_client.query_entities(query_filter=query_filter) # type: ignore
462
+
463
+ results: List[JsonDict] = []
464
+ count = 0
465
+ for ent in entities:
466
+ ent_dict = dict(ent)
467
+
468
+ # defensive: enforce model even if query_filter omitted
469
+ if ent_dict.get(_MODEL_FIELD) != model.__qualname__:
470
+ continue
471
+
472
+ if ent_dict.get("_overflow"):
473
+ blob_key = ent_dict.get("_blob_key")
474
+ if blob_key:
475
+ blob_data = self._blob_download(blob_key)
476
+ restored = json.loads(blob_data.decode("utf-8"))
477
+ out = self._unpack_entity(restored)
144
478
  else:
145
- filter_parts.append(f"{k} eq {v}")
146
-
147
- query_filter = " and ".join(filter_parts)
148
-
149
- entities = self._table_client.query_entities(
150
- query_filter=query_filter, # type: ignore
151
- results_per_page=limit
152
- )
153
-
154
- return [dict(entity) for entity in entities]
479
+ out = self._unpack_entity(ent_dict)
480
+ else:
481
+ out = self._unpack_entity(ent_dict)
482
+
483
+ # guarantee id
484
+ if "id" not in out and out.get("RowKey") is not None:
485
+ out["id"] = out["RowKey"]
486
+
487
+ results.append(out)
488
+
489
+ count += 1
490
+ if limit and count >= limit:
491
+ break
492
+
493
+ return results
494
+
155
495
  except Exception as e:
156
496
  raise NoSQLError(f"Azure Table query failed: {str(e)}")
157
-
497
+
158
498
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
159
499
  def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
160
500
  try:
161
501
  if not self._table_client:
162
- return {'deleted': False}
163
-
164
- # Check if overflow before deleting
502
+ return {"deleted": False}
503
+
504
+ safe_pk = self._sanitize_pk_rk(pk)
505
+ safe_rk = self._sanitize_pk_rk(rk)
506
+
507
+ # read to check model + overflow
165
508
  try:
166
- entity = self._table_client.get_entity(pk, rk)
509
+ entity = self._table_client.get_entity(safe_pk, safe_rk)
167
510
  entity_dict = dict(entity)
168
-
169
- if entity_dict.get('_overflow'):
170
- blob_key = entity_dict.get('_blob_key')
171
- if blob_key and self._blob_service:
172
- blob_client = self._blob_service.get_blob_client(self.container_name, blob_key)
173
- blob_client.delete_blob()
174
- self.logger.debug(f"Deleted overflow blob: {blob_key}")
175
- except:
176
- pass # Entity might not exist or no overflow
177
-
178
- # Delete table entity
179
- self._table_client.delete_entity(pk, rk, etag=etag)
180
- return {'deleted': True, 'PartitionKey': pk, 'RowKey': rk}
511
+
512
+ if entity_dict.get(_MODEL_FIELD) != model.__qualname__:
513
+ return {"deleted": False}
514
+
515
+ if entity_dict.get("_overflow"):
516
+ blob_key = entity_dict.get("_blob_key")
517
+ if blob_key:
518
+ self._blob_delete(blob_key)
519
+ except Exception:
520
+ pass
521
+
522
+ self._table_client.delete_entity(safe_pk, safe_rk, etag=etag)
523
+ return {"deleted": True, "PartitionKey": safe_pk, "RowKey": safe_rk, "id": safe_rk}
524
+
181
525
  except Exception as e:
182
526
  raise NoSQLError(f"Azure Table delete failed: {str(e)}")
183
-