Repository navigation
feat: add filter inspection, manipulation, and builder API - #35
Conversation
Expose the internally-parsed AIP-160 AST via a structured public API so consumers can inspect, manipulate, and construct filter expressions without fragile regex/string manipulation. New modules: - ast.py: FilterExpression, Comparison, AndExpression, OrExpression, NotExpression, Operator, and Value types with serialization, get_fields, rename_field, extract, remove, and combining operators (&, |, ~) - builder.py: FilterBuilder for programmatic filter construction Changes to existing code: - parse_filter() now returns FilterExpression (internal Lark parsing renamed to _parse_lark_tree) - apply_filter() accepts FilterExpression in addition to str - Expanded __init__.py exports https://claude.ai/code/session_017DCbAVeWffb6QjXvt4en78
There was a problem hiding this comment.
Pull request overview
This PR introduces a first-class AST and builder API for AIP-160 filters so callers can parse filters into a structured representation, inspect/mutate them, serialize them back to strings, and pass them directly into apply_filter().
Changes:
- Added AST types (
FilterExpression,Comparison, logical nodes, value nodes) with inspection/mutation/serialization and boolean/composition operators. - Added
FilterBuilderfor fluent programmatic construction of filters. - Updated parsing/application APIs (
parse_filter()returnsFilterExpression;apply_filter()acceptsFilterExpression | str | None) and expanded test coverage including DB integration.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/sqlalchemy_aip160/ast.py |
New AST node model, serialization, inspection/mutation helpers, and combining operators. |
src/sqlalchemy_aip160/builder.py |
New fluent FilterBuilder for constructing FilterExpression programmatically. |
src/sqlalchemy_aip160/aip160_filter.py |
Adds AST parsing path, internal _parse_lark_tree(), and allows apply_filter() to accept FilterExpression. |
src/sqlalchemy_aip160/__init__.py |
Exposes new public API surface (AST types, builder, errors, parse/apply functions). |
tests/test_ast.py |
New unit tests for AST parsing/serialization/inspection/mutation/combining and builder behavior. |
tests/test_integration.py |
New SQLite integration tests for parse → manipulate → apply_filter() end-to-end. |
tests/test_aip160_filter.py |
Adjusts legacy tests to use _parse_lark_tree() now that parse_filter() returns FilterExpression. |
.gitignore |
Adds basic Python build/artifact ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ply_filter docstring (#37) * Initial plan * docs: restore relationship-filtering and field_aliases examples in apply_filter docstring Co-authored-by: rcleveng <1906807+rcleveng@users.noreply.github.com> Agent-Logs-Url: https://github.com/rcleveng/sqlalchemy_aip160/sessions/e6191d18-0d2d-47cd-9649-73d5aa5fa5a9 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcleveng <1906807+rcleveng@users.noreply.github.com>
* Initial plan * fix: use deepcopy in __and__/__or__/__invert__ to prevent aliasing bugs Co-authored-by: rcleveng <1906807+rcleveng@users.noreply.github.com> Agent-Logs-Url: https://github.com/rcleveng/sqlalchemy_aip160/sessions/8da6e4cf-23eb-4500-a944-e989372bb400 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcleveng <1906807+rcleveng@users.noreply.github.com>
Add TestExtractPseudoFieldPattern that mirrors the actual usage pattern: extract a pseudo-field (starred=true/false), read its boolean value, and apply the remaining filter to a DB query. Covers starred at start/middle/end, starred-only, no starred, None/empty input, unquoted values, and chaining multiple pseudo-field extractions (label + starred). https://claude.ai/code/session_017DCbAVeWffb6QjXvt4en78
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def arg(self, items: list) -> Any: | ||
| item = items[0] | ||
| # When an arg is a comparable/member (e.g. bare identifiers like `true`), | ||
| # it arrives as a plain string. Wrap it as a StringValue. | ||
| if isinstance(item, str): | ||
| return StringValue(value=item) | ||
| return item |
There was a problem hiding this comment.
ASTTransformer.arg() wraps any bare identifier argument (e.g. true) into StringValue, which then serializes as a quoted string ("true"). This loses the original literal form and makes parse_filter('is_active = true') serialize to is_active = "true" even though AIP-160 commonly treats booleans as unquoted literals. Consider introducing a dedicated boolean value node (and parsing true/false into it) so serialization can emit true/false without quotes, and update coercion accordingly.
Make FilterBuilder.build() return deep-copied AST nodes so mutating a built FilterExpression does not affect later builds from the same builder. Add a regression test for builder independence and expand the README with installation, usage, aliases, inspection, manipulation, and programmatic filter-building examples.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| def test_invert_empty(self): | ||
| a = parse_filter("") | ||
| assert (~a).root is None |
There was a problem hiding this comment.
FilterExpression.__invert__ raises ValueError when root is None (empty/match-all), but this test asserts ~parse_filter("") returns another empty expression. Either update the test to expect a ValueError (recommended, since match-none can’t be represented with root=None), or change __invert__ semantics to handle empty explicitly.
| assert (~a).root is None | |
| with pytest.raises(ValueError): | |
| ~a |
The API docs state the following: API documentation and examples should encourage the use of explicit parentheses to avoid confusion, but should not require explicit parentheses. So we'll add it in non-required cases when we generate the filter code.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 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.
Summary
FilterExpression,Comparison,AndExpression,OrExpression,NotExpression) for parsed AIP-160 filters with inspection (get_fields), mutation (rename_field,remove,extract), serialization (__str__), and combining operators (&,|,~)FilterBuilderfor fluent programmatic filter constructionparse_filter()now returnsFilterExpression;apply_filter()acceptsFilterExpression | str | NoneTest plan
https://claude.ai/code/session_017DCbAVeWffb6QjXvt4en78