Skip to content

Add QueryBuilder with fluent API and composable filter expressions#118

Merged
abelmilash-msft merged 29 commits intomainfrom
feature/querybuilder-clean
Mar 19, 2026
Merged

Add QueryBuilder with fluent API and composable filter expressions#118
abelmilash-msft merged 29 commits intomainfrom
feature/querybuilder-clean

Conversation

@tpellissier-msft
Copy link
Contributor

Summary

Implements the QueryBuilder feature from the SDK redesign design doc (ADO PR 1504429):

  • Fluent query builder via client.query.builder("table") with 20 chainable methods including select, filter_eq/ne/gt/ge/lt/le, filter_contains/startswith/endswith, filter_in, filter_between, filter_null/not_null, filter_raw, where, order_by, top, page_size, expand, and execute
  • Composable filter expression tree (models/filters.py) with Python operator overloads (&, |, ~) for AND, OR, NOT composition
  • Value auto-formatting for str, int, float, bool, None, datetime, date, uuid.UUID
  • 126 new unit tests (57 filters + 69 query builder), 309 total passing

Usage examples

# Fluent builder
for page in (client.query.builder("account")
             .select("name", "revenue")
             .filter_eq("statecode", 0)
             .filter_gt("revenue", 1000000)
             .order_by("revenue", descending=True)
             .top(100)
             .page_size(50)
             .execute()):
    for record in page:
        print(record["name"])

# Composable expression tree with where()
from PowerPlatform.Dataverse.models.filters import eq, gt, filter_in

for page in (client.query.builder("account")
             .where((eq("statecode", 0) | eq("statecode", 1))
                    & gt("revenue", 100000))
             .execute()):
    for record in page:
        print(record["name"])

Design decisions

  • Regular class, not dataclass — prevents leaking internal state as constructor params
  • Unified _filter_parts list — preserves call order when mixing filter_*() and where()
  • execute() calls build() internally — single source of truth for filter compilation
  • No public get() on QueryOperations — only builder() added; paginated queries remain on records.get()
  • Parenthesized filter_between(col ge low and col le high) for correct precedence

Files changed

File Description
src/.../models/filters.py NEW — Composable expression tree
src/.../models/query_builder.py NEW — Fluent QueryBuilder class
src/.../operations/query.py Add builder() to QueryOperations
src/.../models/__init__.py Updated docstring
tests/.../models/test_filters.py NEW — 57 filter tests
tests/.../models/test_query_builder.py NEW — 69 builder tests
tests/.../test_query_operations.py 6 new integration tests

Merge conflict note

operations/query.py may conflict with PR #115 (typed return models) — resolution is straightforward since we only add a builder() method.

Test plan

  • pytest tests/unit/models/test_filters.py — 57 passed
  • pytest tests/unit/models/test_query_builder.py — 69 passed
  • pytest tests/unit/test_query_operations.py — 9 passed
  • pytest tests/ — 309 passed, 0 failed

🤖 Generated with Claude Code

@tpellissier-msft tpellissier-msft marked this pull request as ready for review March 5, 2026 19:40
@tpellissier-msft tpellissier-msft requested a review from a team as a code owner March 5, 2026 19:40
Copilot AI review requested due to automatic review settings March 5, 2026 19:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@tpellissier-msft tpellissier-msft requested a review from Copilot March 5, 2026 19:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@tpellissier-msft tpellissier-msft requested a review from Copilot March 5, 2026 22:14
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tpellissier and others added 4 commits March 17, 2026 12:13
Implements the QueryBuilder feature from the SDK redesign design doc:
- Fluent query builder (client.query.builder("table")) with 20 chainable methods
- Composable filter expression tree (models/filters.py) with &, |, ~ operators
- filter_in, filter_between, and where() for expression tree composition
- Automatic column name lowercasing and OData value formatting
- datetime/date/uuid.UUID auto-formatting in filter values

309 tests passing (126 new for QueryBuilder + filters).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README: Promote QueryBuilder as primary query method with fluent, expression
  tree, and filter_in/between examples; demote raw OData to fallback
- Walkthrough: Add Section 7 with 5 QueryBuilder demos (basic fluent,
  filter_in, filter_between, where() expression tree, combined + paging)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
execute() now yields individual records instead of pages, abstracting
away OData paging. Pass by_page=True for explicit page-level iteration.

This follows the abstraction-level heuristic: QueryBuilder is the
"abstract away OData" API, so paging should be transparent. The raw
records.get() API retains paged iteration for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run black on 5 files that failed formatting check
- Fix _FunctionFilter.__init__ value param: str -> Any (matches
  _format_value which handles int, float, datetime, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@saurabhrb
Copy link
Contributor

@abelmilash-msft -- 5 commits were added on top of your work (4e37cee..b08639d). Here's a summary for your review:

New features added (closing OData Web API parity gaps identified in issue #147 and OData docs audit):

  1. ExpandOption class (query_builder.py L59-148) -- Structured nested with, , , ``. expand() still accepts plain strings for backward compat.

  2. count() (query_builder.py L483) -- Adds =true to requests.

  3. include_formatted_values() / include_annotations() (query_builder.py L502-555) -- Requests OData annotations (choice labels, currency symbols, lookup display names).

  4. to_dataframe() (query_builder.py L701) -- Addresses Add QueryBuilder.to_dataframe() for fluent DataFrame queries #147. Delegates to client.dataframe.get().

  5. HTTP layer (_odata.py L745-806) -- _get_multiple() now accepts count and include_annotations. Prefer header supports multiple comma-separated values.

  6. Typing fixes (Copilot review responses) -- Sequence[Any] -> Collection[Any] for filter_in/not_in; execute() return type -> Record.

All 601 tests pass. No existing commits were removed or modified. Python 3.10-3.14 compatible.

Copy link
Contributor

@saurabhrb saurabhrb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of 5 commits added (4e37cee..b08639d) -- see inline comments for details on each change area.

@abelmilash-msft abelmilash-msft force-pushed the feature/querybuilder-clean branch from 9f65211 to c400243 Compare March 18, 2026 17:15
@abelmilash-msft abelmilash-msft force-pushed the feature/querybuilder-clean branch from e95e4de to 5c2a17c Compare March 18, 2026 19:16
saurabhrb
saurabhrb previously approved these changes Mar 19, 2026
@abelmilash-msft abelmilash-msft dismissed stale reviews from saurabhrb and themself via b8a4db6 March 19, 2026 06:55
@abelmilash-msft abelmilash-msft merged commit 5a395ec into main Mar 19, 2026
9 checks passed
@abelmilash-msft abelmilash-msft deleted the feature/querybuilder-clean branch March 19, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants