altcodepro-polydb-python 2.5.4__py3-none-any.whl → 2.5.6__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.
@@ -2,7 +2,7 @@
2
2
  import os
3
3
  import threading
4
4
  import time
5
- from typing import Any, Iterator, List, Optional, Tuple, Union
5
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
6
6
  import hashlib
7
7
  from contextlib import contextmanager
8
8
  import json
@@ -20,6 +20,20 @@ from ..utils import validate_table_name, validate_column_name
20
20
  from ..query import QueryBuilder, Operator
21
21
  from ..types import JsonDict, Lookup
22
22
 
23
+ # Postgres session variables this adapter is willing to SET (scoped to one
24
+ # transaction, via set_config(..., is_local=True)) on a caller's behalf.
25
+ # `session_vars` is a keyword-only parameter no request body/query string
26
+ # ever reaches directly -- every caller is trusted engine code, never
27
+ # attacker input -- but this allow-list is a second, independent guard
28
+ # against a future caller (or a bug in one) asking this adapter to SET an
29
+ # arbitrary Postgres GUC (search_path, role, statement_timeout, ...) it was
30
+ # never built to accept. polydb stays tenant-unaware on purpose (see
31
+ # databaseFactory.py's own module docstring) -- this mechanism is generic
32
+ # ("set these session vars for one transaction"), not tenant-specific; the
33
+ # one entry below is simply the one real caller (universal_engine's RLS
34
+ # policies) needs today. Add a name here only when a real caller needs it.
35
+ ALLOWED_SESSION_VARS = frozenset({"app.tenant_id"})
36
+
23
37
 
24
38
  class PostgreSQLAdapter:
25
39
  """PostgreSQL with full LINQ support, connection pooling, JSON/JSONB support"""
@@ -208,6 +222,18 @@ class PostgreSQLAdapter:
208
222
  # ---------------------------------------------------------------------
209
223
 
210
224
  def reset_pool(self):
225
+ # `self._lock` is a plain threading.Lock (not reentrant) --
226
+ # _initialize_pool() below acquires it itself, so it must NOT
227
+ # still be held when we call it, or this self-deadlocks (found by
228
+ # actually calling reset_pool() for the first time anywhere in
229
+ # this codebase, in tests/test_session_vars.py's pooled-connection
230
+ # leak test -- no prior caller ever exercised this method). The
231
+ # tiny window here where self._pool is briefly None before
232
+ # _initialize_pool() re-acquires the lock and sets it back is
233
+ # safe: that method's own `if not self._pool` check under its own
234
+ # lock is exactly what already protects concurrent callers of it
235
+ # (e.g. _get_connection()'s own lazy re-init), so it's not a new
236
+ # exposure.
211
237
  with self._lock:
212
238
  if self._pool:
213
239
  try:
@@ -215,7 +241,7 @@ class PostgreSQLAdapter:
215
241
  except Exception:
216
242
  pass
217
243
  self._pool = None
218
- self._initialize_pool()
244
+ self._initialize_pool()
219
245
 
220
246
  def begin_transaction(self) -> Any:
221
247
  conn = self._get_connection()
@@ -223,6 +249,40 @@ class PostgreSQLAdapter:
223
249
  conn.autocommit = False
224
250
  return conn
225
251
 
252
+ def _apply_session_vars(self, conn: Any, session_vars: Optional[Dict[str, str]]) -> None:
253
+ """SET LOCAL-equivalent for a fixed, allow-listed set of Postgres
254
+ session variables, scoped to whatever transaction `conn` is
255
+ currently in -- reverts automatically at COMMIT/ROLLBACK, never
256
+ outlives the transaction it was set for. This is the fix for the
257
+ gap README documents: `SET LOCAL <var> = <value>` only means
258
+ anything on the exact connection/transaction the real query after
259
+ it runs on, so it has to be issued here, immediately before the
260
+ query, on the same `conn` -- never as a separate call the caller
261
+ could race against a pool checkin/checkout.
262
+
263
+ Uses `set_config(name, value, is_local)` rather than a literal
264
+ `SET LOCAL {key} = %s` string: `SET` itself has no bind-parameter
265
+ syntax in Postgres (the value has no placeholder form), but
266
+ `set_config` is an ordinary SQL function, so both the name and the
267
+ value are real query parameters -- nothing about either is ever
268
+ f-string'd into the SQL text. `is_local=True` is what gives this
269
+ `SET LOCAL`'s exact semantics (reverts at the end of the current
270
+ transaction) rather than `SET`'s session-lifetime one -- the
271
+ session-lifetime form is exactly the pooled-connection leak this
272
+ whole mechanism exists to avoid (see tests/test_session_vars.py's
273
+ leak-across-checkout proof).
274
+ """
275
+ if not session_vars:
276
+ return
277
+ with conn.cursor() as cur:
278
+ for key, value in session_vars.items():
279
+ if key not in ALLOWED_SESSION_VARS:
280
+ raise DatabaseError(
281
+ f"session_vars: {key!r} is not in the allow-list of Postgres "
282
+ f"session variables this adapter will set"
283
+ )
284
+ cur.execute("SELECT set_config(%s, %s, true)", [key, str(value)])
285
+
226
286
  def commit(self, tx: Any):
227
287
  if tx:
228
288
  tx.commit()
@@ -298,7 +358,14 @@ class PostgreSQLAdapter:
298
358
  # ---------------------------------------------------------------------
299
359
 
300
360
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
301
- def insert(self, table: str, data: JsonDict, tx: Optional[Any] = None) -> JsonDict:
361
+ def insert(
362
+ self,
363
+ table: str,
364
+ data: JsonDict,
365
+ tx: Optional[Any] = None,
366
+ *,
367
+ session_vars: Optional[Dict[str, str]] = None,
368
+ ) -> JsonDict:
302
369
  table = validate_table_name(table)
303
370
  for k in data.keys():
304
371
  validate_column_name(k)
@@ -310,6 +377,13 @@ class PostgreSQLAdapter:
310
377
  own_conn = True
311
378
 
312
379
  try:
380
+ # Applied regardless of who owns `conn` -- see this method's
381
+ # own `session_vars` parameter doc on why a caller-supplied tx
382
+ # still honors it (databaseFactory.py's create() is own_conn;
383
+ # universal_engine's idempotency.py/batches.py call insert-
384
+ # shaped methods on their own already-open tx and need this
385
+ # exact same call to take effect on it).
386
+ self._apply_session_vars(conn, session_vars)
313
387
  cursor = conn.cursor()
314
388
  columns = ", ".join(data.keys())
315
389
  placeholders = ", ".join(["%s"] * len(data))
@@ -343,6 +417,8 @@ class PostgreSQLAdapter:
343
417
  limit: Optional[int] = None,
344
418
  offset: Optional[int] = None,
345
419
  tx: Optional[Any] = None,
420
+ *,
421
+ session_vars: Optional[Dict[str, str]] = None,
346
422
  ) -> List[JsonDict]:
347
423
  table = validate_table_name(table)
348
424
  conn = tx
@@ -352,6 +428,7 @@ class PostgreSQLAdapter:
352
428
  own_conn = True
353
429
 
354
430
  try:
431
+ self._apply_session_vars(conn, session_vars)
355
432
  cursor = conn.cursor()
356
433
  sql = f"SELECT * FROM {table}"
357
434
  params: List[Any] = []
@@ -411,9 +488,13 @@ class PostgreSQLAdapter:
411
488
  page_size: int,
412
489
  continuation_token: Optional[str] = None,
413
490
  tx: Optional[Any] = None,
491
+ *,
492
+ session_vars: Optional[Dict[str, str]] = None,
414
493
  ) -> Tuple[List[JsonDict], Optional[str]]:
415
494
  offset = int(continuation_token) if continuation_token else 0
416
- results = self.select(table, query, limit=page_size + 1, offset=offset, tx=tx)
495
+ results = self.select(
496
+ table, query, limit=page_size + 1, offset=offset, tx=tx, session_vars=session_vars
497
+ )
417
498
  has_more = len(results) > page_size
418
499
  if has_more:
419
500
  results = results[:page_size]
@@ -431,6 +512,8 @@ class PostgreSQLAdapter:
431
512
  entity_id: Union[Any, Lookup],
432
513
  data: JsonDict,
433
514
  tx: Optional[Any] = None,
515
+ *,
516
+ session_vars: Optional[Dict[str, str]] = None,
434
517
  ) -> JsonDict:
435
518
  table = validate_table_name(table)
436
519
  for k in data.keys():
@@ -443,6 +526,7 @@ class PostgreSQLAdapter:
443
526
  own_conn = True
444
527
 
445
528
  try:
529
+ self._apply_session_vars(conn, session_vars)
446
530
  cursor = conn.cursor()
447
531
  set_clause = ", ".join([f"{k} = %s" for k in data.keys()])
448
532
  params: List[Any] = [self._serialize_value(v) for v in data.values()]
@@ -485,7 +569,14 @@ class PostgreSQLAdapter:
485
569
  # ---------------------------------------------------------------------
486
570
 
487
571
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
488
- def upsert(self, table: str, data: JsonDict, tx: Optional[Any] = None) -> JsonDict:
572
+ def upsert(
573
+ self,
574
+ table: str,
575
+ data: JsonDict,
576
+ tx: Optional[Any] = None,
577
+ *,
578
+ session_vars: Optional[Dict[str, str]] = None,
579
+ ) -> JsonDict:
489
580
  table = validate_table_name(table)
490
581
  for k in data.keys():
491
582
  validate_column_name(k)
@@ -497,6 +588,7 @@ class PostgreSQLAdapter:
497
588
  own_conn = True
498
589
 
499
590
  try:
591
+ self._apply_session_vars(conn, session_vars)
500
592
  cursor = conn.cursor()
501
593
  columns = ", ".join(data.keys())
502
594
  placeholders = ", ".join(["%s"] * len(data))
@@ -548,7 +640,12 @@ class PostgreSQLAdapter:
548
640
 
549
641
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
550
642
  def delete(
551
- self, table: str, entity_id: Union[Any, Lookup], tx: Optional[Any] = None
643
+ self,
644
+ table: str,
645
+ entity_id: Union[Any, Lookup],
646
+ tx: Optional[Any] = None,
647
+ *,
648
+ session_vars: Optional[Dict[str, str]] = None,
552
649
  ) -> JsonDict:
553
650
  table = validate_table_name(table)
554
651
  conn = tx
@@ -558,6 +655,7 @@ class PostgreSQLAdapter:
558
655
  own_conn = True
559
656
 
560
657
  try:
658
+ self._apply_session_vars(conn, session_vars)
561
659
  cursor = conn.cursor()
562
660
  params: List[Any] = []
563
661
  if isinstance(entity_id, dict):
@@ -742,6 +840,7 @@ class PostgreSQLAdapter:
742
840
  *,
743
841
  fetch: bool = False,
744
842
  fetch_one: bool = False,
843
+ session_vars: Optional[Dict[str, str]] = None,
745
844
  ) -> Union[None, JsonDict, List[JsonDict]]:
746
845
  conn = tx
747
846
  own_conn = False
@@ -751,6 +850,15 @@ class PostgreSQLAdapter:
751
850
 
752
851
  cursor = None
753
852
  try:
853
+ # execute() is the raw-SQL escape hatch a few callers already
854
+ # use for a query with no generic-CRUD equivalent (an atomic
855
+ # claim, a bulk UPDATE, an ORDER BY) -- those are exactly the
856
+ # callers RLS-protected engine-infrastructure tables need this
857
+ # for (see universal_engine's idempotency.py/batches.py),
858
+ # since they run several statements against the same `tx` and
859
+ # only need the session var set once per transaction, same as
860
+ # every other method above.
861
+ self._apply_session_vars(conn, session_vars)
754
862
  cursor = conn.cursor()
755
863
  self.logger.debug("Executing raw SQL (%d params)", len(params or []))
756
864
  exec_params = self._serialize_params(params or [])