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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.5.12
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
@@ -1,12 +1,12 @@
1
- altcodepro_polydb_python-2.5.12.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
- polydb/PolyDB.py,sha256=_6YaeX7-9G9fK6hj4I-JbsfdVIwAotx7E_4IJqdGcjQ,23536
3
- polydb/__init__.py,sha256=EAtfxsNp7-RDyYP3yEz8Rb9TL41ASloYJrQghAaSny0,1876
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=eE53-luKwMr-5J5Ezsh9gLjZkq1aYG7UiyLTU846MZ8,53058
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=7Nz1EHcl4FWpdfsTonoFpMrkb0HcWKOiBpW_TTpPlrE,6447
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=Ff4xbpqJKMN6EdMcsR9WMMFkMaceXmzb32v-nIiqVRo,33340
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=NPmf-s4UMGUFtQsqlAufrGtWI0N1vDvBYNSGAFEnBW4,45040
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=eGI-AyO_9nKnNchKKA5s9gxJaYJHlPGmcxu6ROhsp0I,14814
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.12.dist-info/METADATA,sha256=YOVEJeI2-82-3kaLNgwPT4E3cT20C10GkPV1B9_0HdU,15599
72
- altcodepro_polydb_python-2.5.12.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
73
- altcodepro_polydb_python-2.5.12.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
74
- altcodepro_polydb_python-2.5.12.dist-info/RECORD,,
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.12"
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, model: type, filters: Dict[str, Any], limit: Optional[int]
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
- entities = table_client.query_entities(query_filter=query_filter)
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:
@@ -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
- results = self._query_raw(model, query or {}, limit)
247
- return [self._retrieve_overflow(r) for r in results]
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 = self._enable_cache and self._cache and getattr(meta, "cache", False)
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
- raw = adapters.sql.select(
470
- meta.table, query, limit=limit, offset=offset, session_vars=session_vars
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