5050from __future__ import annotations
5151
5252import warnings
53- from typing import Any , Dict , Iterator , List , Optional , TypedDict , Union
53+ from typing import Any , Iterator , List , Optional , TypeVar , TypedDict , Union
5454
5555import pandas as pd
5656
6262# Sentinel for detecting when by_page is explicitly passed to execute()
6363_BY_PAGE_UNSET = object ()
6464
65+ _QB = TypeVar ("_QB" , bound = "_QueryBuilderBase" )
66+
6567
6668class QueryParams (TypedDict , total = False ):
6769 """Typed dictionary returned by :meth:`QueryBuilder.build`.
@@ -171,29 +173,17 @@ def to_odata(self) -> str:
171173 return self .relation
172174
173175
174- class QueryBuilder :
175- """Fluent interface for building OData queries.
176-
177- Provides method chaining for constructing complex queries with
178- composable filter expressions. Can be used standalone (via :meth:`build`)
179- or bound to a client (via :meth:`execute`).
180-
181- :param table: Table schema name to query.
182- :type table: str
183- :raises ValueError: If ``table`` is empty.
184-
185- Example:
186- Standalone query construction::
176+ class _QueryBuilderBase :
177+ """Pure fluent interface for building OData queries — no I/O.
187178
188- from PowerPlatform.Dataverse.models.filters import col
179+ Holds all query state and chaining methods (``select``, ``where``,
180+ ``order_by``, ``top``, ``page_size``, ``count``, ``expand``,
181+ ``include_annotations``, ``include_formatted_values``) and
182+ :meth:`build`.
189183
190- query = (QueryBuilder("account")
191- .select("name")
192- .where(col("statecode") == 0)
193- .top(10))
194- params = query.build()
195- # {"table": "account", "select": ["name"],
196- # "filter": "statecode eq 0", "top": 10}
184+ Subclasses add execution: :class:`QueryBuilder` for sync clients,
185+ :class:`~PowerPlatform.Dataverse.aio.models.async_query_builder.AsyncQueryBuilder`
186+ for async clients.
197187 """
198188
199189 def __init__ (self , table : str ) -> None :
@@ -213,7 +203,7 @@ def __init__(self, table: str) -> None:
213203
214204 # ----------------------------------------------------------------- select
215205
216- def select (self , * columns : str ) -> QueryBuilder :
206+ def select (self : _QB , * columns : str ) -> _QB :
217207 """Select specific columns to retrieve.
218208
219209 Column names are passed as-is; the OData layer lowercases them
@@ -231,7 +221,7 @@ def select(self, *columns: str) -> QueryBuilder:
231221
232222 # ------------------------------------------------------ filter: expression tree
233223
234- def where (self , expression : filters .FilterExpression ) -> QueryBuilder :
224+ def where (self : _QB , expression : filters .FilterExpression ) -> _QB :
235225 """Add a composable filter expression.
236226
237227 Accepts a :class:`~PowerPlatform.Dataverse.models.filters.FilterExpression`
@@ -260,7 +250,7 @@ def where(self, expression: filters.FilterExpression) -> QueryBuilder:
260250
261251 # --------------------------------------------------------------- ordering
262252
263- def order_by (self , column : str , descending : bool = False ) -> QueryBuilder :
253+ def order_by (self : _QB , column : str , descending : bool = False ) -> _QB :
264254 """Add sorting order.
265255
266256 Can be called multiple times for multi-column sorting.
@@ -275,7 +265,7 @@ def order_by(self, column: str, descending: bool = False) -> QueryBuilder:
275265
276266 # --------------------------------------------------------------- pagination
277267
278- def top (self , count : int ) -> QueryBuilder :
268+ def top (self : _QB , count : int ) -> _QB :
279269 """Limit the total number of results.
280270
281271 :param count: Maximum number of records to return (must be >= 1).
@@ -287,7 +277,7 @@ def top(self, count: int) -> QueryBuilder:
287277 self ._top = count
288278 return self
289279
290- def page_size (self , size : int ) -> QueryBuilder :
280+ def page_size (self : _QB , size : int ) -> _QB :
291281 """Set the number of records per page.
292282
293283 Controls how many records are returned in each page/batch
@@ -302,7 +292,7 @@ def page_size(self, size: int) -> QueryBuilder:
302292 self ._page_size = size
303293 return self
304294
305- def count (self ) -> QueryBuilder :
295+ def count (self : _QB ) -> _QB :
306296 """Request a count of matching records in the response.
307297
308298 Adds ``$count=true`` to the query, causing the server to include
@@ -321,7 +311,7 @@ def count(self) -> QueryBuilder:
321311 self ._count = True
322312 return self
323313
324- def include_formatted_values (self ) -> QueryBuilder :
314+ def include_formatted_values (self : _QB ) -> _QB :
325315 """Request formatted values in the response.
326316
327317 Adds ``Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"``
@@ -351,7 +341,7 @@ def include_formatted_values(self) -> QueryBuilder:
351341 self ._include_annotations = "OData.Community.Display.V1.FormattedValue"
352342 return self
353343
354- def include_annotations (self , annotation : str = "*" ) -> QueryBuilder :
344+ def include_annotations (self : _QB , annotation : str = "*" ) -> _QB :
355345 """Request specific OData annotations in the response.
356346
357347 Sets the ``Prefer: odata.include-annotations`` header. Use ``"*"``
@@ -379,7 +369,7 @@ def include_annotations(self, annotation: str = "*") -> QueryBuilder:
379369
380370 # --------------------------------------------------------------- expand
381371
382- def expand (self , * relations : Union [str , ExpandOption ]) -> QueryBuilder :
372+ def expand (self : _QB , * relations : Union [str , ExpandOption ]) -> _QB :
383373 """Expand navigation properties.
384374
385375 Accepts plain navigation property names (case-sensitive, passed
@@ -448,6 +438,32 @@ def build(self) -> QueryParams:
448438 params ["include_annotations" ] = self ._include_annotations
449439 return params
450440
441+
442+ class QueryBuilder (_QueryBuilderBase ):
443+ """Fluent interface for building and executing OData queries against a sync client.
444+
445+ Provides method chaining for constructing complex queries with
446+ composable filter expressions. Can be used standalone (via :meth:`build`)
447+ or bound to a client (via :meth:`execute`).
448+
449+ :param table: Table schema name to query.
450+ :type table: str
451+ :raises ValueError: If ``table`` is empty.
452+
453+ Example:
454+ Standalone query construction::
455+
456+ from PowerPlatform.Dataverse.models.filters import col
457+
458+ query = (QueryBuilder("account")
459+ .select("name")
460+ .where(col("statecode") == 0)
461+ .top(10))
462+ params = query.build()
463+ # {"table": "account", "select": ["name"],
464+ # "filter": "statecode eq 0", "top": 10}
465+ """
466+
451467 # --------------------------------------------------------------- execute
452468
453469 def execute (self , * , by_page = _BY_PAGE_UNSET ) -> Union [QueryResult , Iterator [QueryResult ]]:
0 commit comments