altcodepro-polydb-python 2.3.26__py3-none-any.whl → 2.3.28__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.3.26
3
+ Version: 2.3.28
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
@@ -1,11 +1,11 @@
1
- altcodepro_polydb_python-2.3.26.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
1
+ altcodepro_polydb_python-2.3.28.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
2
  polydb/PolyDB.py,sha256=MG7-nV59zDvrUQwXNEgV4eetHCeW9TYY9DQqtxu_r7k,23543
3
3
  polydb/__init__.py,sha256=d9KSz888A336904TPEjWPYBtyJLPB0lTdcaOnYp8byY,1535
4
4
  polydb/advanced_query.py,sha256=cxMB-EB-qT3bWXJlhmjnMCUtrzogORWyoEfS50Dy7go,4280
5
5
  polydb/batch.py,sha256=_DjWZa1ZXYSk6MLKqFe0eT7SYVRZtYNqZb9bI8Y2sao,4566
6
6
  polydb/cache.py,sha256=JBXF1XEK-fY80ar8SDE893Z1Z116YtXAEG0PaJ0Nkcw,7658
7
7
  polydb/cloudDatabaseFactory.py,sha256=Gp6L__YtgrkGahD8B7ItzXMHCoj2ZUGDjXLS9w0TujY,17780
8
- polydb/databaseFactory.py,sha256=uBzX7K-k_aFHLQyFemy3XIe3lsOjAvjRFfaC6phITlA,41470
8
+ polydb/databaseFactory.py,sha256=AdEMRtWcwf0elJ9JhrWS5krre-3RVFfIgt0mA93eJJQ,44068
9
9
  polydb/decorators.py,sha256=L_WP2uXP_k8Ac49SUm-mthbM4jWI-XYfHXEyKzumOww,43062
10
10
  polydb/errors.py,sha256=jF_55fbfg6b6mBc116dzU_uBG9qiIKbr5jvmDuJx-Jw,1573
11
11
  polydb/json_safe.py,sha256=R5PrqAGirqjYKPyy-8KH-lSXjLH0FPr2TSGozy4eheU,149
@@ -23,8 +23,8 @@ polydb/utils.py,sha256=tS_KI1vt-VZ6JftbFxbnVxC0xpie1-zYikTGf1nSoFw,3183
23
23
  polydb/validation.py,sha256=a1o1d02k3c6PWQwkBbw_0nEmIgrdB5RR8OcpNQMn4cA,4810
24
24
  polydb/adapters/AzureBlobStorageAdapter.py,sha256=4vD55Z8DBTzBK66jIJbo5bNMY-AQ61MlP0-P2Fv_JgQ,7083
25
25
  polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
26
- polydb/adapters/AzureQueueAdapter.py,sha256=5PrKAX4OQxUD5nReZKrInF_mjQVdFcj2aYd0Xp-HjjQ,5254
27
- polydb/adapters/AzureTableStorageAdapter.py,sha256=CTi8qCVQDujqBGIfLJu2fgA2sNkDt07zrNIdyuQIcl4,27053
26
+ polydb/adapters/AzureQueueAdapter.py,sha256=-MLEEAt2yvYjlB5LqT2TwoRa3rVDHDVzXhm1gykqeDM,8052
27
+ polydb/adapters/AzureTableStorageAdapter.py,sha256=FCWojIRyOQdDyga5sxbHXBfZMdb3T1xn3Q2VX5klebo,27824
28
28
  polydb/adapters/BlockchainBlobAdapter.py,sha256=D01Yua9mkKfaQrxKYApblIyI6DSP0dtNAh4Tav51HJ4,3299
29
29
  polydb/adapters/BlockchainFileAdapter.py,sha256=G749xOVpG20HuKS8zCgi6PMjoJNu-YXK7zitygjLdzM,8335
30
30
  polydb/adapters/BlockchainKVAdapter.py,sha256=UFYHyTgvdW-sZUBqyHEG3Cdx6wSTiF2QowEVWL3XPTg,4564
@@ -35,7 +35,7 @@ polydb/adapters/FirestoreAdapter.py,sha256=e3DEODOZ-UAWULLtGTIED-Ym1JrxOKaOLPQVK
35
35
  polydb/adapters/GCPFilestoreAdapter.py,sha256=yjFQQwsWYWc8mo8XwMViVTWb5_D--xAyTMvE-4AOpNM,3006
36
36
  polydb/adapters/GCPPubSubAdapter.py,sha256=7XNots2VA0ReEDku-rjg-OTYmftIpx5UgnXYDdXNkOo,8692
37
37
  polydb/adapters/GCPStorageAdapter.py,sha256=9yS1Jhcn5_rCRdZ5uOqcRW6Ba-UNb6VOYpwENP-C6Qk,7133
38
- polydb/adapters/MongoDBAdapter.py,sha256=vX3SAHDLbTnHABGesES9N-gYSQqPqdqFLJgd7pYWZzw,7471
38
+ polydb/adapters/MongoDBAdapter.py,sha256=Ak8q_qeAaUTe0Yc2oEefmAbzoODjTWVZQOirW-seBQU,7574
39
39
  polydb/adapters/PostgreSQLAdapter.py,sha256=lUe65kpJkTu6R1Tc1zgl63Fa1PI44UVbWRTKRUSSGak,36579
40
40
  polydb/adapters/S3Adapter.py,sha256=5R0zHAL2SkGFjp1L3bp-IU468bXYdSf6nKx974MN104,7586
41
41
  polydb/adapters/S3CompatibleAdapter.py,sha256=jpafqbAjA8-irdXBrfXa1QJySIzrcUQ6UrFt5h5FAEc,7006
@@ -50,7 +50,7 @@ polydb/audit/__init__.py,sha256=Z7-y5djq3glQ2Yun6nj-13Efpj3oGz9Qc0veS2g06Y4,245
50
50
  polydb/audit/context.py,sha256=-A1FMtmr-2snVAHpTrVT80u-D_MCaqX6AoV4Ku2bz_o,1955
51
51
  polydb/audit/manager.py,sha256=KzaaOf5bDfr4M-CkCAZBG_U_4xIBCKDLRAf3hsm-DAk,1236
52
52
  polydb/audit/models.py,sha256=NapdH5dXU9GMoP9ccbDFaeMWvWbwoh2B5N7Nd3Ci6Cg,3745
53
- polydb/base/NoSQLKVAdapter.py,sha256=aYEnXG3PZVQbpneSgfToJAhJAsRHUxwazIvjRmT-Qak,13588
53
+ polydb/base/NoSQLKVAdapter.py,sha256=U64nXaeHd40GCZV4DQnogZRUxvI8HHLEhNBMv4oddq4,14491
54
54
  polydb/base/ObjectStorageAdapter.py,sha256=VeJ3qXET6H0xd3lJpE8-WSsKs8EyK9S0-9VNR0EJzME,2288
55
55
  polydb/base/QueueAdapter.py,sha256=jFgyG-SUK4nhRNxm2NbzUbwnA9b_5iAC-ikLSUpXRwk,799
56
56
  polydb/base/SharedFilesAdapter.py,sha256=kXbJmtn_cwEyAZ-1AvFrmesCLSwu43ycTV3S4BmwrO4,853
@@ -60,7 +60,7 @@ polydb/observability/logging.py,sha256=gv1SshNjaSkmR98Uv7Qbwq3gkeQUzVCA4ichqHKL6
60
60
  polydb/services/__init__.py,sha256=nMolMXzmhejRSm7f42kU9fCHF0GIpqpejN2kywuBsUQ,241
61
61
  polydb/services/compliance_service.py,sha256=-0ncX7PrXy4-NuyWXIv65cLUsqz0jXzb3LtsD8rlrXE,4983
62
62
  polydb/services/security_service.py,sha256=q7GzK6sbMnBBsc4fuL4oXTd-ui2b-MYQyrWTBvdVNSk,5062
63
- altcodepro_polydb_python-2.3.26.dist-info/METADATA,sha256=D5suhSTtKr4f65k7_le2erc4HEM1xXvAF0eh7wn8FFY,12359
64
- altcodepro_polydb_python-2.3.26.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
65
- altcodepro_polydb_python-2.3.26.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
- altcodepro_polydb_python-2.3.26.dist-info/RECORD,,
63
+ altcodepro_polydb_python-2.3.28.dist-info/METADATA,sha256=mCw1bRtSYL37sOtvm-Rn5fQdjfCwLZPcZkAiOk-PN68,12359
64
+ altcodepro_polydb_python-2.3.28.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
65
+ altcodepro_polydb_python-2.3.28.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
+ altcodepro_polydb_python-2.3.28.dist-info/RECORD,,
@@ -46,6 +46,20 @@ class AzureQueueAdapter(QueueAdapter):
46
46
  name = re.sub(r"-+", "-", name) # collapse multiple dashes
47
47
  return name.strip("-")
48
48
 
49
+ @staticmethod
50
+ def _encode_receipt(message_id: str, pop_receipt: str) -> str:
51
+ # Azure needs BOTH message_id and pop_receipt to delete/update a
52
+ # message, but the generic queue contract (WorkerPool, SQSAdapter)
53
+ # only ever carries a single opaque `receipt_handle` string end to
54
+ # end. Pack both into one string here so ack()/nack() can recover
55
+ # them without any caller needing to know Azure's two-part scheme.
56
+ return f"{message_id}|{pop_receipt}"
57
+
58
+ @staticmethod
59
+ def _decode_receipt(receipt_handle: str) -> "tuple[str, str]":
60
+ message_id, _, pop_receipt = (receipt_handle or "").partition("|")
61
+ return message_id, pop_receipt
62
+
49
63
  def _initialize_client(self) -> None:
50
64
  """Initialize Azure Queue client"""
51
65
  from azure.storage.queue import QueueServiceClient
@@ -114,6 +128,13 @@ class AzureQueueAdapter(QueueAdapter):
114
128
  {
115
129
  "id": msg.id,
116
130
  "pop_receipt": msg.pop_receipt,
131
+ # WorkerPool (and the generic UDL queue contract)
132
+ # reads this key to ack/nack. Without it, ack() is
133
+ # always skipped (falsy receipt_handle), the message
134
+ # is never deleted, and it silently reappears once
135
+ # Azure's visibility timeout elapses — replaying the
136
+ # same task forever even after it already ran.
137
+ "receipt_handle": self._encode_receipt(msg.id, msg.pop_receipt),
117
138
  "body": payload,
118
139
  }
119
140
  )
@@ -156,8 +177,14 @@ class AzureQueueAdapter(QueueAdapter):
156
177
  Preferred usage:
157
178
  ack(pop_receipt=..., message_id=...)
158
179
 
159
- If message_id is not provided, this will fail safely.
180
+ Generic callers (WorkerPool, storage_router.queue_ack) only pass a
181
+ single `receipt_handle` positionally and never supply message_id —
182
+ in that case `pop_receipt` here is actually the combined value
183
+ produced by `receive()` (`"<message_id>|<pop_receipt>"`), so decode
184
+ it rather than failing.
160
185
  """
186
+ if not message_id:
187
+ message_id, pop_receipt = self._decode_receipt(pop_receipt)
161
188
  if not message_id:
162
189
  raise QueueError("AzureQueueAdapter.ack requires message_id")
163
190
  queue_name = self._normalize_queue_name(queue_name)
@@ -166,3 +193,34 @@ class AzureQueueAdapter(QueueAdapter):
166
193
  queue_name=queue_name,
167
194
  pop_receipt=pop_receipt,
168
195
  )
196
+
197
+ def nack(
198
+ self,
199
+ queue_name: str,
200
+ ack_id: str,
201
+ *,
202
+ delay: Optional[int] = None,
203
+ ) -> bool:
204
+ """
205
+ Make a message visible again (optionally after `delay` seconds)
206
+ without deleting it, instead of leaving it to reappear only once
207
+ Azure's default visibility timeout elapses.
208
+
209
+ `ack_id` is the combined `"<message_id>|<pop_receipt>"` receipt
210
+ handle produced by `receive()`. Matches the positional order used
211
+ by storage_router.queue_nack: nack(queue_name, ack_id, delay=...).
212
+ """
213
+ message_id, pop_receipt = self._decode_receipt(ack_id)
214
+ if not message_id:
215
+ raise QueueError("AzureQueueAdapter.nack requires a receipt_handle from receive()")
216
+ try:
217
+ queue_name = self._normalize_queue_name(queue_name)
218
+ queue_client = self._get_queue(queue_name)
219
+ queue_client.update_message(
220
+ message_id,
221
+ pop_receipt=pop_receipt,
222
+ visibility_timeout=delay or 0,
223
+ )
224
+ return True
225
+ except Exception as e:
226
+ raise QueueError(f"Azure Queue nack failed: {e}")
@@ -255,7 +255,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
255
255
 
256
256
  return entity
257
257
 
258
- def _unpack_entity(self, entity: JsonDict) -> JsonDict:
258
+ def _unpack_entity(self, model: type, entity: JsonDict) -> JsonDict:
259
259
  if not entity:
260
260
  return {}
261
261
 
@@ -263,6 +263,9 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
263
263
  raw.pop("etag", None)
264
264
  raw.pop("ETag", None)
265
265
  raw.pop("Timestamp", None)
266
+ # Internal: which model a row belongs to, used only to isolate models
267
+ # sharing a table. Never part of the public record shape.
268
+ raw.pop(_MODEL_FIELD, None)
266
269
 
267
270
  keymap_str = raw.pop("__keymap__", None)
268
271
  keymap: Dict[str, str] = {}
@@ -272,26 +275,30 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
272
275
  except Exception:
273
276
  keymap = {}
274
277
 
275
- out: JsonDict = {}
276
-
277
- pk = raw.get("PartitionKey")
278
- rk = raw.get("RowKey")
279
- if pk is not None:
280
- out["PartitionKey"] = pk
281
- if rk is not None:
282
- out["RowKey"] = rk
278
+ pk = raw.pop("PartitionKey", None)
279
+ rk = raw.pop("RowKey", None)
283
280
 
281
+ out: JsonDict = {}
284
282
  for k, v in raw.items():
285
- if k in ("PartitionKey", "RowKey"):
286
- continue
287
-
288
- if k.startswith("_") or k in (_MODEL_FIELD,):
283
+ if k.startswith("_"):
289
284
  out[k] = v
290
285
  continue
291
286
 
292
287
  orig_key = keymap.get(k, k)
293
288
  out[orig_key] = self._decode_value(v)
294
289
 
290
+ # PartitionKey/RowKey are Azure Table Storage's own physical column
291
+ # names — never return them as-is. Map each back to the domain field
292
+ # it was derived from (model's declared partition_key/sort_key, same
293
+ # resolution _get_pk_rk used to write them), so the same field name
294
+ # round-trips on read as it did on write. Guarded so we never clobber
295
+ # a real property that already carries the value (the common case —
296
+ # pk/rk fields are usually also stored as regular properties).
297
+ pk_field, rk_field = self._pk_rk_field_names(model)
298
+ if pk is not None and pk_field not in out:
299
+ out[pk_field] = pk
300
+ if rk is not None and rk_field not in out:
301
+ out[rk_field] = rk
295
302
  if "id" not in out and rk is not None:
296
303
  out["id"] = rk
297
304
 
@@ -491,7 +498,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
491
498
 
492
499
  table_client.upsert_entity(reference_entity)
493
500
 
494
- restored = self._unpack_entity(entity)
501
+ restored = self._unpack_entity(model, entity)
495
502
  restored["id"] = safe_rk
496
503
 
497
504
  return restored
@@ -517,7 +524,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
517
524
 
518
525
  restored_entity = self._restore_overflow_properties(entity_dict)
519
526
 
520
- out = self._unpack_entity(restored_entity)
527
+ out = self._unpack_entity(model, restored_entity)
521
528
  if "id" not in out:
522
529
  out["id"] = safe_rk
523
530
 
@@ -589,7 +596,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
589
596
 
590
597
  restored_entity = self._restore_overflow_properties(ent_dict)
591
598
 
592
- out = self._unpack_entity(restored_entity)
599
+ out = self._unpack_entity(model, restored_entity)
593
600
 
594
601
  if "id" not in out and "RowKey" in ent_dict:
595
602
  out["id"] = ent_dict["RowKey"]
@@ -705,7 +712,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
705
712
  for ent in page:
706
713
  ent_dict = dict(ent)
707
714
  restored = self._restore_overflow_properties(ent_dict)
708
- out = self._unpack_entity(restored)
715
+ out = self._unpack_entity(model, restored)
709
716
  if "id" not in out and "RowKey" in ent_dict:
710
717
  out["id"] = ent_dict["RowKey"]
711
718
  items.append(out)
@@ -50,7 +50,9 @@ class MongoDBAdapter(NoSQLKVAdapter):
50
50
  self.mongo_uri,
51
51
  maxPoolSize=int(os.getenv("MONGODB_MAX_POOL_SIZE", "10")),
52
52
  minPoolSize=int(os.getenv("MONGODB_MIN_POOL_SIZE", "1")),
53
- serverSelectionTimeoutMS=5000,
53
+ serverSelectionTimeoutMS=int(
54
+ os.getenv("MONGODB_SERVER_SELECTION_TIMEOUT_MS", "5000")
55
+ ),
54
56
  )
55
57
 
56
58
  self._client.server_info()
@@ -34,6 +34,22 @@ class NoSQLKVAdapter:
34
34
  self._lock = threading.Lock()
35
35
  self.max_size = 1024 * 1024 # 1MB
36
36
 
37
+ def _pk_rk_field_names(self, model: type) -> Tuple[str, str]:
38
+ """
39
+ The domain field names PK/RK were derived from — the mirror image of
40
+ _get_pk_rk's field resolution. Adapters use this to translate a
41
+ physical PartitionKey/RowKey (or pk/sk, whatever the backend calls
42
+ them) back into the model's own field name before returning a record,
43
+ instead of leaking storage-internal column names to callers. Falls
44
+ back to the same defaults _get_pk_rk uses (tenant_id / id) when a
45
+ model declares no explicit mapping, so unmapped models still round-
46
+ trip cleanly on upsert without erroring.
47
+ """
48
+ meta = getattr(model, "__polydb__", {})
49
+ pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
50
+ rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
51
+ return pk_field, rk_field
52
+
37
53
  def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
38
54
  """Extract PK/RK from model metadata"""
39
55
  meta = getattr(model, "__polydb__", {})
polydb/databaseFactory.py CHANGED
@@ -109,8 +109,8 @@ class EngineOverride:
109
109
 
110
110
  @dataclass
111
111
  class _ResolvedAdapters:
112
- sql: PostgreSQLAdapter
113
- nosql: NoSQLKVAdapter
112
+ sql: Optional[PostgreSQLAdapter]
113
+ nosql: Optional[NoSQLKVAdapter]
114
114
  engine_name: str
115
115
 
116
116
 
@@ -233,35 +233,38 @@ class DatabaseFactory:
233
233
  def _resolve_adapters(
234
234
  self, model_name: str, storage: str, override: Optional[EngineOverride] = None
235
235
  ) -> _ResolvedAdapters:
236
+ # Only construct the adapter this call actually needs. Every caller
237
+ # (create/read/update/upsert/delete/query*) branches on `storage` and
238
+ # touches exactly one of .sql/.nosql — building both unconditionally
239
+ # meant a purely-SQL deployment with no reachable NoSQL backend (or
240
+ # vice versa) failed on every single call, not just ones that needed
241
+ # the missing engine.
242
+ def _build(engine: "EngineConfig") -> _ResolvedAdapters:
243
+ return _ResolvedAdapters(
244
+ sql=engine.sql() if storage == "sql" else None,
245
+ nosql=engine.nosql() if storage == "nosql" else None,
246
+ engine_name=engine.name,
247
+ )
248
+
236
249
  if override:
237
250
  engine = self._engine_by_name.get(override.engine_name)
238
251
  if engine is None:
239
252
  raise AdapterConfigurationError(
240
253
  f"Unknown engine '{override.engine_name}'. Available: {list(self._engine_by_name)}"
241
254
  )
242
- return _ResolvedAdapters(
243
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
244
- )
255
+ return _build(engine)
245
256
 
246
257
  for engine in self._engines:
247
258
  if storage == "sql" and engine.sql_models and model_name in engine.sql_models:
248
- return _ResolvedAdapters(
249
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
250
- )
259
+ return _build(engine)
251
260
  if storage == "nosql" and engine.nosql_models and model_name in engine.nosql_models:
252
- return _ResolvedAdapters(
253
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
254
- )
261
+ return _build(engine)
255
262
 
256
263
  for engine in self._engines:
257
264
  if storage == "sql" and engine.is_default_sql:
258
- return _ResolvedAdapters(
259
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
260
- )
265
+ return _build(engine)
261
266
  if storage == "nosql" and engine.is_default_nosql:
262
- return _ResolvedAdapters(
263
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
264
- )
267
+ return _build(engine)
265
268
 
266
269
  raise AdapterConfigurationError(f"No engine for model='{model_name}' storage='{storage}'")
267
270
 
@@ -543,14 +546,32 @@ class DatabaseFactory:
543
546
  if self._is_sql(meta, engine_override):
544
547
  result = adapters.sql.update(meta.table, entity_id, data)
545
548
  else:
549
+ # A scalar entity_id is usually the record's "id" property,
550
+ # not its physical PartitionKey/RowKey — those come from the
551
+ # model's declared pk_field/rk_field (x-metadata partition_key
552
+ # /sort_key), which for most schema models is a different
553
+ # field entirely (e.g. Artifact: pk=project_slug, rk=
554
+ # template_key). Recover both from the patch payload, or —
555
+ # more reliably — from the record loaded just above as
556
+ # `before`, so a plain id-addressed update() still lands on
557
+ # the entity's real physical key instead of a mismatched
558
+ # default partition / the id used as a row key it never had.
546
559
  pkey = data.get("PartitionKey") or data.get("partition_key") or data.get("pk")
547
- en_id = entity_id
560
+ if not pkey and meta.pk_field:
561
+ pkey = data.get(meta.pk_field)
548
562
  if not pkey and before:
549
563
  pkey = (
550
564
  before.get("PartitionKey")
551
565
  or before.get("partition_key")
552
566
  or before.get("pk")
567
+ or (before.get(meta.pk_field) if meta.pk_field else None)
553
568
  )
569
+ rkey = None
570
+ if meta.rk_field and meta.rk_field != "id":
571
+ rkey = data.get(meta.rk_field) or (
572
+ before.get(meta.rk_field) if before else None
573
+ )
574
+ en_id = entity_id
554
575
  if pkey:
555
576
  if isinstance(en_id, dict):
556
577
  en_pk = (
@@ -560,8 +581,16 @@ class DatabaseFactory:
560
581
  )
561
582
  if not en_pk:
562
583
  en_id["partition_key"] = pkey
584
+ en_rk = (
585
+ en_id.get("RowKey")
586
+ or en_id.get("row_key")
587
+ or en_id.get("rk")
588
+ or en_id.get("id")
589
+ )
590
+ if not en_rk and rkey:
591
+ en_id["row_key"] = rkey
563
592
  elif isinstance(en_id, str):
564
- en_id = {"partition_key": pkey, "id": entity_id}
593
+ en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
565
594
 
566
595
  cls = (
567
596
  model
@@ -702,7 +731,23 @@ class DatabaseFactory:
702
731
  if isinstance(model, type)
703
732
  else type(name, (), {"__polydb__": meta.__dict__})
704
733
  )
705
- result = adapters.nosql.delete(cls, entity_id, etag=etag)
734
+ # Same physical-key recovery as update() (see there for why a
735
+ # scalar entity_id alone isn't enough once a model's pk_field
736
+ # /rk_field differ from "id").
737
+ en_id = entity_id
738
+ if not isinstance(en_id, dict) and before:
739
+ pkey = (
740
+ before.get("PartitionKey")
741
+ or before.get("partition_key")
742
+ or before.get("pk")
743
+ or (before.get(meta.pk_field) if meta.pk_field else None)
744
+ )
745
+ if pkey:
746
+ rkey = None
747
+ if meta.rk_field and meta.rk_field != "id":
748
+ rkey = before.get(meta.rk_field)
749
+ en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
750
+ result = adapters.nosql.delete(cls, en_id, etag=etag)
706
751
  success = True
707
752
  if self._enable_cache and self._cache:
708
753
  try: