altcodepro-polydb-python 2.5.12__py3-none-any.whl → 2.5.19__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.
- {altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/METADATA +1 -1
- {altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/RECORD +12 -12
- polydb/PolyDB.py +4 -0
- polydb/__init__.py +1 -1
- polydb/adapters/AzureTableStorageAdapter.py +45 -2
- polydb/adapters/PostgreSQLAdapter.py +49 -1
- polydb/base/NoSQLKVAdapter.py +42 -2
- polydb/databaseFactory.py +29 -6
- polydb/schema.py +19 -1
- {altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/top_level.txt +0 -0
{altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.19
|
|
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
|
{altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/RECORD
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
altcodepro_polydb_python-2.5.
|
|
2
|
-
polydb/PolyDB.py,sha256=
|
|
3
|
-
polydb/__init__.py,sha256=
|
|
1
|
+
altcodepro_polydb_python-2.5.19.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
|
|
2
|
+
polydb/PolyDB.py,sha256=Lp0QtG7D89Z6P6nt9fVoT8rVHAy8xlE2kFRSS70r-mk,23672
|
|
3
|
+
polydb/__init__.py,sha256=p9dAWJ3-JNCeHgb1WzkTMkuURFQy-RUNI4QeD9zBVf0,1876
|
|
4
4
|
polydb/advanced_query.py,sha256=M9sKmdF2PsLZWSc-zKpXRcwaDumbYYTsibmL_IAjKP4,6931
|
|
5
5
|
polydb/aio.py,sha256=0EQ6hXrf6N4If6pcht6L9B7qz0PU2yGahyGzEss2eQg,8340
|
|
6
6
|
polydb/batch.py,sha256=ulvSdtVeofsOiVNVAQwrE9tXIIaP8fjiW1Sbpz3dcv0,4093
|
|
7
7
|
polydb/cache.py,sha256=ArI0zRbuhAYd6kn4TPVMBHBptdRmuZyy05_hwIATUsQ,11784
|
|
8
8
|
polydb/cloudDatabaseFactory.py,sha256=eJiNKycdaC9nO2BKWPjkig3GaNGkNjoJci6yVJC7A_c,25301
|
|
9
|
-
polydb/databaseFactory.py,sha256=
|
|
9
|
+
polydb/databaseFactory.py,sha256=8A6Nmx0TTBxyVxLBMTBPp-sxuO7SeUWF5eVlG0I8WCE,54546
|
|
10
10
|
polydb/errors.py,sha256=AtXCLdzkHE2ncUaE1P0wH5HmIyW3NXx9KHL-fCejjA4,1832
|
|
11
11
|
polydb/json_safe.py,sha256=jj3ZA6gImGwgTO0dCZVcckjeUHtewa2HP6OA1-u6VZ0,137
|
|
12
12
|
polydb/models.py,sha256=Pq_63FXu1IiTxl-RshZ2vmoe-5m5k6qft-tXpHLn-70,12600
|
|
@@ -17,7 +17,7 @@ polydb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
17
17
|
polydb/query.py,sha256=17V-e70df9NaiaUYPN5_MQz7O0vRdBaCR4xlZU3F2YE,8511
|
|
18
18
|
polydb/registry.py,sha256=RD_elvFXcmhTdCyZDm2f3ej0elxqhArnSJ2aO9k5VCU,2352
|
|
19
19
|
polydb/retry.py,sha256=0rILN3-auuzomOXPveIAvgnhBbtXXGNKvxg6qcRgN4g,4926
|
|
20
|
-
polydb/schema.py,sha256=
|
|
20
|
+
polydb/schema.py,sha256=jDjUuOaz2DANddhMTSxMwZ1MB-EOVDgDrCHbxkp-iM4,7712
|
|
21
21
|
polydb/security.py,sha256=IF8Oh6Y6YU9xSjz_pi6lotfJuhIUJgP7cCmpGc8bz6w,20158
|
|
22
22
|
polydb/types.py,sha256=Z1R8z06-WYoh7cnn0oE_2N10v6hdx7VJD7TNL5ynWhA,2667
|
|
23
23
|
polydb/utils.py,sha256=YS-BhBpQfQsOf-z_i1zw_kvMYwJ05Zn1S2Xt6K4_2ak,3171
|
|
@@ -27,7 +27,7 @@ polydb/adapters/AzureBlobStorageAdapter.py,sha256=ZTovSC_CVduGnIoIQUUeNxDQlf-IbF
|
|
|
27
27
|
polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
|
|
28
28
|
polydb/adapters/AzureKeyVaultAdapter.py,sha256=7Nizs9qGh6cytbZNgV78f-odOO4Z6sUlwm_HQYqt-pc,2339
|
|
29
29
|
polydb/adapters/AzureQueueAdapter.py,sha256=1e2HiFvkH1BI_JKS9fgays2J41kOfH6pUPbZDe1wmWA,13658
|
|
30
|
-
polydb/adapters/AzureTableStorageAdapter.py,sha256=
|
|
30
|
+
polydb/adapters/AzureTableStorageAdapter.py,sha256=u2DsUIMOjYnBX_2G4abaLCjRaVOCORQQnwC4e0XrJ0Q,35567
|
|
31
31
|
polydb/adapters/BlockchainBlobAdapter.py,sha256=85H3PxiL9rGDtK-sTsW26jOuUQ_SGipLSPu3s-juZRA,3298
|
|
32
32
|
polydb/adapters/BlockchainFileAdapter.py,sha256=lZbRyC18LghZcVh5QyIRP7tCftNF_Ep1FKG0KRkvMmk,8335
|
|
33
33
|
polydb/adapters/BlockchainKVAdapter.py,sha256=pvpiPshDE5Z_IRcSBvBwgAXT--86lgEyIsEUeqcmoE8,5422
|
|
@@ -41,7 +41,7 @@ polydb/adapters/GCPSecretManagerAdapter.py,sha256=ZFL2FGh7uTaBfpUo-HKXZAxS9s-w0w
|
|
|
41
41
|
polydb/adapters/GCPStorageAdapter.py,sha256=ce7vCOW46XbdjiupAgMgkgopy3IV7nM5YvIwEhLIKvQ,7133
|
|
42
42
|
polydb/adapters/KafkaQueueAdapter.py,sha256=_FrcI6JLIoJHPOtSIL2q1uXHeao9ZkGMyUnT68p36HQ,14329
|
|
43
43
|
polydb/adapters/MongoDBAdapter.py,sha256=Rs1L9gbGj2hHiJuz62Ms7uRK6sppHc1aIO4MNrtZ3Zo,11750
|
|
44
|
-
polydb/adapters/PostgreSQLAdapter.py,sha256=
|
|
44
|
+
polydb/adapters/PostgreSQLAdapter.py,sha256=n73HfBIXCF8Em4bHPvRVvipOJKdt9qZuZWdzRNCxS3w,48166
|
|
45
45
|
polydb/adapters/RabbitMQAdapter.py,sha256=rMeqWXQmnfRX_pakaZX6q1bHkZ8E1boQ_Pkj3byl4E4,19903
|
|
46
46
|
polydb/adapters/S3Adapter.py,sha256=ZkMR91u1e4SnrXSUbV3siKFYdw0ykYHJfyP2cUJUVso,7564
|
|
47
47
|
polydb/adapters/S3CompatibleAdapter.py,sha256=Qj9SUtCnOFNL9c05wNVjICxZacFCnee-Wn_-hh0lwwE,7006
|
|
@@ -57,7 +57,7 @@ polydb/audit/__init__.py,sha256=VANh47kW5V9gi7anUCZ7LklQ6ofaFRmBKrPVitRxdw0,245
|
|
|
57
57
|
polydb/audit/context.py,sha256=jbw58vDoslY2mEcS0qUBlEhYg54EoMk4Xcfuq1L3FuY,1945
|
|
58
58
|
polydb/audit/manager.py,sha256=DgBkDcZlf_EotUm-RlKA1CWAoVi3DIUlRCsN4wzsfgc,1237
|
|
59
59
|
polydb/audit/models.py,sha256=XHT-te2XpmdywfakKm1cVH09yi3C9Fa4FaBTZaJ5vqY,7044
|
|
60
|
-
polydb/base/NoSQLKVAdapter.py,sha256=
|
|
60
|
+
polydb/base/NoSQLKVAdapter.py,sha256=TJ9jyYiLA26WmxJjGuL5DbQ00ZN-RYTV-IznN7RwMw4,16927
|
|
61
61
|
polydb/base/ObjectStorageAdapter.py,sha256=mr6UMGmAFOBKlJeoyubaUJ7x7BDZMlylqnuggIatEv0,2289
|
|
62
62
|
polydb/base/QueueAdapter.py,sha256=PzTlEuc37TXEnYNdsrrmxzFworbieWBxljDXJ-qq4XQ,8206
|
|
63
63
|
polydb/base/SecretsAdapter.py,sha256=RaWejh21oQji0TdEBa7P5JgD9y6N06M1aCBbLvzTQU0,990
|
|
@@ -68,7 +68,7 @@ polydb/observability/logging.py,sha256=FWYky-Mg1IhLpwuaKuLIsTROBZc9bI_-AO-y53nvE
|
|
|
68
68
|
polydb/services/__init__.py,sha256=Zs3PZbHutgPdPjJatygvtYvKPZMBt1qKSqESbFRPHJs,241
|
|
69
69
|
polydb/services/compliance_service.py,sha256=UMjCbQ4z13-w6pT4RLQpMi_u1zbShEpiz8kawA2WsSU,5075
|
|
70
70
|
polydb/services/security_service.py,sha256=gGo_j9Ca_rgvxS7s6C5JUktugQBj1PV1DXpMnOYAGbI,4992
|
|
71
|
-
altcodepro_polydb_python-2.5.
|
|
72
|
-
altcodepro_polydb_python-2.5.
|
|
73
|
-
altcodepro_polydb_python-2.5.
|
|
74
|
-
altcodepro_polydb_python-2.5.
|
|
71
|
+
altcodepro_polydb_python-2.5.19.dist-info/METADATA,sha256=jxSgbkqTALqYYLf5XwA6qNZPY--DLXDSdsxCwbItZR8,15599
|
|
72
|
+
altcodepro_polydb_python-2.5.19.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
73
|
+
altcodepro_polydb_python-2.5.19.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
|
|
74
|
+
altcodepro_polydb_python-2.5.19.dist-info/RECORD,,
|
polydb/PolyDB.py
CHANGED
|
@@ -158,6 +158,8 @@ class PolyDB:
|
|
|
158
158
|
cache_ttl: Optional[int] = None,
|
|
159
159
|
include_deleted: bool = False,
|
|
160
160
|
engine_override: Optional[EngineOverride] = None,
|
|
161
|
+
fields: Optional[List[str]] = None,
|
|
162
|
+
omit: Optional[List[str]] = None,
|
|
161
163
|
) -> List[JsonDict]:
|
|
162
164
|
return self.db.read(
|
|
163
165
|
model,
|
|
@@ -168,6 +170,8 @@ class PolyDB:
|
|
|
168
170
|
cache_ttl=cache_ttl,
|
|
169
171
|
include_deleted=include_deleted,
|
|
170
172
|
engine_override=engine_override,
|
|
173
|
+
fields=fields,
|
|
174
|
+
omit=omit,
|
|
171
175
|
)
|
|
172
176
|
|
|
173
177
|
def read_one(
|
polydb/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ PolyDB - Enterprise Cloud-Independent Database Abstraction
|
|
|
4
4
|
Full LINQ support, field-level audit, cache, soft delete, overflow storage
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.5.
|
|
7
|
+
__version__ = "2.5.18"
|
|
8
8
|
|
|
9
9
|
from .aio import AsyncDatabaseFactory, AsyncPolyDB
|
|
10
10
|
from .audit.context import AuditContext
|
|
@@ -125,6 +125,19 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
125
125
|
s = re.sub(r"[\\/#\?\x00-\x1f\x7f:+ ]", "_", s)
|
|
126
126
|
return s[:1024] if len(s) > 1024 else s
|
|
127
127
|
|
|
128
|
+
def _check_overflow(self, data: JsonDict): # type: ignore[override]
|
|
129
|
+
"""Azure overflows per PROPERTY inside `_put_raw`, never per record.
|
|
130
|
+
|
|
131
|
+
The base class' whole-record overflow replaces the record with a stub
|
|
132
|
+
of underscore keys (`_overflow`, `_blob_key`, ...), but `_put_raw`
|
|
133
|
+
drops every key starting with "_" when it builds the entity -- so the
|
|
134
|
+
stub's pointer was never stored and the real fields of the write were
|
|
135
|
+
silently lost (only an orphaned blob remained). Returning the data
|
|
136
|
+
untouched lets `_put_raw` spill each >30KB property on its own, which
|
|
137
|
+
`_get_raw`/`_query_raw` already rehydrate.
|
|
138
|
+
"""
|
|
139
|
+
return data, None
|
|
140
|
+
|
|
128
141
|
def _sanitize_prop_name(self, name: Any) -> str:
|
|
129
142
|
s = str(name)
|
|
130
143
|
s = re.sub(r"[^A-Za-z0-9_]", "_", s)
|
|
@@ -620,9 +633,35 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
620
633
|
return None
|
|
621
634
|
raise NoSQLError(f"Azure Table get failed: {str(e)}")
|
|
622
635
|
|
|
636
|
+
# `fields` / `omit` are sent to Azure as `$select`, so unrequested properties never leave the service.
|
|
637
|
+
SUPPORTS_SELECT_PUSHDOWN = True
|
|
638
|
+
|
|
639
|
+
def _select_properties(self, select: Optional[List[str]]) -> Optional[List[str]]:
|
|
640
|
+
"""The Azure property names to `$select` for the requested field names -- the requested ones, plus what `_unpack_entity`
|
|
641
|
+
needs to rebuild a row (the keys, the model tag, and the sanitized-name map). A per-property blob overflow is stored in
|
|
642
|
+
the property itself, so selecting the property is enough for `_restore_overflow_properties`. None = no pushdown: if any
|
|
643
|
+
requested name is rewritten by `_sanitize_prop_name` (so its stored name could carry a collision suffix), the whole row is
|
|
644
|
+
fetched and trimmed afterwards instead of risking a silently missing column."""
|
|
645
|
+
if not select:
|
|
646
|
+
return None
|
|
647
|
+
props: List[str] = []
|
|
648
|
+
for name in select:
|
|
649
|
+
safe = self._sanitize_prop_name(name)
|
|
650
|
+
if safe != str(name):
|
|
651
|
+
return None
|
|
652
|
+
props.append(safe)
|
|
653
|
+
for needed in ("PartitionKey", "RowKey", _MODEL_FIELD, "__keymap__"):
|
|
654
|
+
if needed not in props:
|
|
655
|
+
props.append(needed)
|
|
656
|
+
return props
|
|
657
|
+
|
|
623
658
|
@retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
|
|
624
659
|
def _query_raw(
|
|
625
|
-
self,
|
|
660
|
+
self,
|
|
661
|
+
model: type,
|
|
662
|
+
filters: Dict[str, Any],
|
|
663
|
+
limit: Optional[int],
|
|
664
|
+
select: Optional[List[str]] = None,
|
|
626
665
|
) -> List[JsonDict]:
|
|
627
666
|
try:
|
|
628
667
|
table_client = self._get_table_client(model)
|
|
@@ -672,7 +711,11 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
672
711
|
else:
|
|
673
712
|
query_filter = " and ".join(parts)
|
|
674
713
|
|
|
675
|
-
|
|
714
|
+
props = self._select_properties(select)
|
|
715
|
+
if props is None:
|
|
716
|
+
entities = table_client.query_entities(query_filter=query_filter)
|
|
717
|
+
else:
|
|
718
|
+
entities = table_client.query_entities(query_filter=query_filter, select=props)
|
|
676
719
|
|
|
677
720
|
results: List[JsonDict] = []
|
|
678
721
|
count = 0
|
|
@@ -365,6 +365,50 @@ class PostgreSQLAdapter:
|
|
|
365
365
|
return [self._serialize_param(p) for p in params]
|
|
366
366
|
|
|
367
367
|
def _deserialize_row(self, row: JsonDict) -> JsonDict:
|
|
368
|
+
"""Best-effort convenience decode: any string column value that
|
|
369
|
+
LOOKS like JSON (starts/ends with `{}`/`[]`) is `json.loads`'d,
|
|
370
|
+
regardless of the column's own declared type. This is a real,
|
|
371
|
+
deliberate, LOAD-BEARING convenience a real caller depends on --
|
|
372
|
+
e.g. universal-interprter's `observability.py`'s `Observability.
|
|
373
|
+
query()` writes a `logs.emit` payload as `json.dumps(...)` into a
|
|
374
|
+
plain TEXT column (`payload`, never JSONB -- see that module's
|
|
375
|
+
own comment) and its own docstring explicitly promises "logs.
|
|
376
|
+
query's own `payload` field gets a real dict back"; removing this
|
|
377
|
+
heuristic (briefly tried, then reverted -- see this repo's own
|
|
378
|
+
2.5.16/2.5.17 CLAUDE.md history) silently broke that real,
|
|
379
|
+
already-shipped contract. `query_log.py`'s own `fields` column is
|
|
380
|
+
the identical shape, just defended a second way: it independently
|
|
381
|
+
`json.loads`'s its own value with an `isinstance(..., str)` guard
|
|
382
|
+
rather than relying on this method at all -- proof this repo has
|
|
383
|
+
real callers on both sides (some lean on this adapter-level
|
|
384
|
+
decode, some defend for themselves).
|
|
385
|
+
|
|
386
|
+
**The real, honest trade-off, unavoidable at this layer**: a
|
|
387
|
+
genuinely non-JSON TEXT/VARCHAR string whose own real content
|
|
388
|
+
merely LOOKS JSON-shaped (e.g. a prompt template's own `content`
|
|
389
|
+
column holding a literal JSON-encoded email template -- see
|
|
390
|
+
universal-interprter's `prompt_template_store.py`) gets coerced
|
|
391
|
+
from `str` to `dict`/`list` here too, since this method has no
|
|
392
|
+
way to distinguish "real app data serialized as JSON text" from
|
|
393
|
+
"a legitimately JSON-shaped string that must stay a string" --
|
|
394
|
+
both are simply "a string that starts with `{` and ends with
|
|
395
|
+
`}`." This adapter deliberately does NOT try to solve that
|
|
396
|
+
ambiguity generically (declaring the column JSONB instead sidesteps
|
|
397
|
+
it entirely -- psycopg2 registers global JSON/JSONB typecasters
|
|
398
|
+
for OIDs 114/3802 automatically at import time, so a genuine
|
|
399
|
+
`json`/`jsonb` column never even reaches this heuristic, `isinstance
|
|
400
|
+
(v, str)` already `False` by the time this method sees it); a
|
|
401
|
+
caller that must keep a TEXT column's string content literal
|
|
402
|
+
despite it being JSON-shaped owns that coercion at ITS OWN layer
|
|
403
|
+
instead (see `prompt_template_store.py`'s own `_coerce_content_to_str`
|
|
404
|
+
for the concrete pattern: re-`json.dumps()` a `content` value that
|
|
405
|
+
came back non-`str`, restoring the real string every caller of
|
|
406
|
+
that store is promised). This adapter stays a "dumb storage
|
|
407
|
+
layer" in the sense that it never MODELS what a column's real
|
|
408
|
+
semantic type should be -- it does still apply this one generic,
|
|
409
|
+
best-effort convenience decode uniformly, exactly as it always
|
|
410
|
+
has.
|
|
411
|
+
"""
|
|
368
412
|
for k, v in list(row.items()):
|
|
369
413
|
if isinstance(v, str):
|
|
370
414
|
s = v.strip()
|
|
@@ -443,8 +487,12 @@ class PostgreSQLAdapter:
|
|
|
443
487
|
tx: Optional[Any] = None,
|
|
444
488
|
*,
|
|
445
489
|
session_vars: Optional[Dict[str, str]] = None,
|
|
490
|
+
fields: Optional[List[str]] = None,
|
|
446
491
|
) -> List[JsonDict]:
|
|
447
492
|
table = validate_table_name(table)
|
|
493
|
+
if fields:
|
|
494
|
+
for f in fields:
|
|
495
|
+
validate_column_name(f)
|
|
448
496
|
conn = tx
|
|
449
497
|
own_conn = False
|
|
450
498
|
if not conn:
|
|
@@ -454,7 +502,7 @@ class PostgreSQLAdapter:
|
|
|
454
502
|
try:
|
|
455
503
|
self._apply_session_vars(conn, session_vars)
|
|
456
504
|
cursor = conn.cursor()
|
|
457
|
-
sql = f"SELECT * FROM {table}"
|
|
505
|
+
sql = f"SELECT {', '.join(fields)} FROM {table}" if fields else f"SELECT * FROM {table}"
|
|
458
506
|
params: List[Any] = []
|
|
459
507
|
|
|
460
508
|
if query:
|
polydb/base/NoSQLKVAdapter.py
CHANGED
|
@@ -17,9 +17,43 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from ..models import PartitionConfig
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def project_rows(
|
|
21
|
+
rows: List[JsonDict], fields: Optional[List[str]] = None, omit: Optional[List[str]] = None
|
|
22
|
+
) -> List[JsonDict]:
|
|
23
|
+
"""The exact `fields` / `omit` semantics every NoSQL read shares: `fields` keeps only those keys, `omit` drops those keys
|
|
24
|
+
(`fields` wins if both are given). Applied by the base `query()` AFTER overflow rehydration, so it is correct for every
|
|
25
|
+
adapter whether or not it also pushes the projection down to the store."""
|
|
26
|
+
if fields:
|
|
27
|
+
keep = set(fields)
|
|
28
|
+
return [{k: v for k, v in r.items() if k in keep} for r in rows]
|
|
29
|
+
if omit:
|
|
30
|
+
drop = set(omit)
|
|
31
|
+
return [{k: v for k, v in r.items() if k not in drop} for r in rows]
|
|
32
|
+
return rows
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def select_columns(
|
|
36
|
+
fields: Optional[List[str]], omit: Optional[List[str]], columns: Optional[List[str]]
|
|
37
|
+
) -> Optional[List[str]]:
|
|
38
|
+
"""The column list a store can be asked for: `fields` as given, or -- for `omit` -- the model's known `columns` minus the
|
|
39
|
+
omitted ones. None means "no column list can be derived" (no projection, or `omit` on a model whose columns are unknown):
|
|
40
|
+
the read then fetches whole rows and `project_rows` trims them."""
|
|
41
|
+
if fields:
|
|
42
|
+
return list(fields)
|
|
43
|
+
if omit and columns:
|
|
44
|
+
drop = set(omit)
|
|
45
|
+
return [c for c in columns if c not in drop]
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
20
49
|
class NoSQLKVAdapter:
|
|
21
50
|
"""Base with auto-overflow and LINQ support"""
|
|
22
51
|
|
|
52
|
+
# An adapter sets this True when its `_query_raw` accepts `select=[property names]` and really sends it to the store (Azure
|
|
53
|
+
# Table's `$select`). False (the default) means `fields`/`omit` are applied after the rows come back -- same result, no
|
|
54
|
+
# network saving. Opt in per adapter only once the adapter's own rehydration is proven to survive a narrowed row.
|
|
55
|
+
SUPPORTS_SELECT_PUSHDOWN = False
|
|
56
|
+
|
|
23
57
|
def __init__(
|
|
24
58
|
self,
|
|
25
59
|
partition_config: Optional[PartitionConfig] = None,
|
|
@@ -242,9 +276,15 @@ class NoSQLKVAdapter:
|
|
|
242
276
|
limit: Optional[int] = None,
|
|
243
277
|
no_cache: bool = False,
|
|
244
278
|
cache_ttl: Optional[int] = None,
|
|
279
|
+
fields: Optional[List[str]] = None,
|
|
280
|
+
omit: Optional[List[str]] = None,
|
|
245
281
|
) -> List[JsonDict]:
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
select = select_columns(fields, omit, getattr(model, "__polydb__", {}).get("columns"))
|
|
283
|
+
if select is not None and self.SUPPORTS_SELECT_PUSHDOWN:
|
|
284
|
+
results = self._query_raw(model, query or {}, limit, select=select)
|
|
285
|
+
else:
|
|
286
|
+
results = self._query_raw(model, query or {}, limit)
|
|
287
|
+
return project_rows([self._retrieve_overflow(r) for r in results], fields, omit)
|
|
248
288
|
|
|
249
289
|
def query_page(
|
|
250
290
|
self, model: type, query: Lookup, page_size: int, continuation_token: Optional[str] = None
|
polydb/databaseFactory.py
CHANGED
|
@@ -24,7 +24,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential
|
|
|
24
24
|
from .adapters.PostgreSQLAdapter import PostgreSQLAdapter
|
|
25
25
|
from .audit.context import AuditContext
|
|
26
26
|
from .audit.manager import AuditManager
|
|
27
|
-
from .base.NoSQLKVAdapter import NoSQLKVAdapter
|
|
27
|
+
from .base.NoSQLKVAdapter import NoSQLKVAdapter, project_rows, select_columns
|
|
28
28
|
from .batch import BatchOperations
|
|
29
29
|
from .cache import CacheWarmer, RedisCacheEngine
|
|
30
30
|
from .cloudDatabaseFactory import CloudDatabaseFactory
|
|
@@ -453,30 +453,53 @@ class DatabaseFactory:
|
|
|
453
453
|
include_deleted: bool = False,
|
|
454
454
|
engine_override: Optional[EngineOverride] = None,
|
|
455
455
|
session_vars: Optional[Dict[str, str]] = None,
|
|
456
|
+
fields: Optional[List[str]] = None,
|
|
457
|
+
omit: Optional[List[str]] = None,
|
|
456
458
|
) -> List[JsonDict]:
|
|
459
|
+
"""`fields` returns only those keys; `omit` returns every key except those (`fields` wins if both are given). SQL
|
|
460
|
+
names the columns in the SELECT; NoSQL adapters that can ($select on Azure Table) send the property list to the store,
|
|
461
|
+
the rest trim the rows after the read -- same result either way. `omit` needs the model's `__polydb__["columns"]` to
|
|
462
|
+
be turned into a column list for the store; without it the whole row is fetched and trimmed. A projected read never
|
|
463
|
+
goes through the external cache (its key does not include the projection)."""
|
|
457
464
|
name = _model_name(model)
|
|
458
465
|
meta = _extract_meta(model)
|
|
466
|
+
columns = (getattr(model, "__polydb__", None) or {}).get("columns") if isinstance(model, type) else None
|
|
467
|
+
projected = bool(fields or omit)
|
|
468
|
+
sql_columns = select_columns(fields, omit, columns)
|
|
459
469
|
|
|
460
470
|
if self._soft_delete and not include_deleted:
|
|
461
471
|
query = self._apply_soft_delete_filter(query)
|
|
462
472
|
|
|
463
473
|
adapters = self._adapters_for(model, meta, engine_override)
|
|
464
|
-
use_external_cache =
|
|
474
|
+
use_external_cache = (
|
|
475
|
+
self._enable_cache and self._cache and getattr(meta, "cache", False) and not projected
|
|
476
|
+
)
|
|
465
477
|
encrypted_fields = getattr(meta, "encrypted_fields", [])
|
|
466
478
|
|
|
467
479
|
def _op() -> List[JsonDict]:
|
|
468
480
|
if self._is_sql(meta, engine_override):
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
481
|
+
if sql_columns is not None:
|
|
482
|
+
raw = adapters.sql.select(
|
|
483
|
+
meta.table, query, limit=limit, offset=offset,
|
|
484
|
+
session_vars=session_vars, fields=sql_columns,
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
raw = adapters.sql.select(
|
|
488
|
+
meta.table, query, limit=limit, offset=offset, session_vars=session_vars
|
|
489
|
+
)
|
|
490
|
+
raw = project_rows(raw, fields, omit)
|
|
472
491
|
else:
|
|
473
492
|
cls = (
|
|
474
493
|
model
|
|
475
494
|
if isinstance(model, type)
|
|
476
495
|
else type(name, (), {"__polydb__": meta.__dict__})
|
|
477
496
|
)
|
|
497
|
+
nosql_kwargs: Dict[str, Any] = {}
|
|
498
|
+
if projected:
|
|
499
|
+
nosql_kwargs = {"fields": fields, "omit": omit}
|
|
478
500
|
raw = adapters.nosql.query(
|
|
479
|
-
cls, query=query, limit=limit, no_cache=no_cache or bool(use_external_cache)
|
|
501
|
+
cls, query=query, limit=limit, no_cache=no_cache or bool(use_external_cache),
|
|
502
|
+
**nosql_kwargs,
|
|
480
503
|
)
|
|
481
504
|
if self.encryption and encrypted_fields:
|
|
482
505
|
raw = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw]
|
polydb/schema.py
CHANGED
|
@@ -61,6 +61,17 @@ class Index:
|
|
|
61
61
|
name: str
|
|
62
62
|
columns: List[str]
|
|
63
63
|
unique: bool = False
|
|
64
|
+
# Postgres index access method -- "btree" (the default, and the only
|
|
65
|
+
# one Postgres actually permits combined with UNIQUE) plus the real
|
|
66
|
+
# non-default methods a caller may need for a specific column shape:
|
|
67
|
+
# "gin"/"gist" (full-text search, JSONB containment, array overlap),
|
|
68
|
+
# "hash" (equality-only, smaller than btree for that one case), "brin"
|
|
69
|
+
# (large, naturally-ordered, append-mostly columns -- cheap to
|
|
70
|
+
# maintain relative to btree at real scale). Never validated against
|
|
71
|
+
# Postgres's own real per-type support here -- that's a live DDL
|
|
72
|
+
# concern the caller's own database will enforce; this dataclass only
|
|
73
|
+
# carries the caller's choice through to the generated statement.
|
|
74
|
+
using: str = "btree"
|
|
64
75
|
|
|
65
76
|
|
|
66
77
|
class SchemaBuilder:
|
|
@@ -128,10 +139,17 @@ class SchemaBuilder:
|
|
|
128
139
|
|
|
129
140
|
for idx in self.indexes:
|
|
130
141
|
unique = "UNIQUE " if idx.unique else ""
|
|
142
|
+
# "btree" omitted outright rather than spelled out as
|
|
143
|
+
# "USING btree" -- it's Postgres's own implicit default, and
|
|
144
|
+
# every index this codebase generated before `using` existed
|
|
145
|
+
# rendered without a USING clause at all; emitting it only for
|
|
146
|
+
# a non-default method keeps every pre-existing caller's own
|
|
147
|
+
# generated SQL text byte-for-byte unchanged.
|
|
148
|
+
using = f"USING {idx.using} " if idx.using != "btree" else ""
|
|
131
149
|
cols = ", ".join(validate_column_name(c) for c in idx.columns)
|
|
132
150
|
sql = (
|
|
133
151
|
f"CREATE {unique}INDEX IF NOT EXISTS {validate_table_name(idx.name)} "
|
|
134
|
-
f"ON {table_name}({cols});"
|
|
152
|
+
f"ON {table_name}{' ' if using else ''}{using}({cols});"
|
|
135
153
|
)
|
|
136
154
|
statements.append(sql)
|
|
137
155
|
|
{altcodepro_polydb_python-2.5.12.dist-info → altcodepro_polydb_python-2.5.19.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|