Skip to content

feat: add async execution and batched programmatic APIs#45

Draft
srimon12 wants to merge 4 commits into
pavanjava:mainfrom
srimon12:feat/refractor-batch-grpc-async-support
Draft

feat: add async execution and batched programmatic APIs#45
srimon12 wants to merge 4 commits into
pavanjava:mainfrom
srimon12:feat/refractor-batch-grpc-async-support

Conversation

@srimon12
Copy link
Copy Markdown
Collaborator

@srimon12 srimon12 commented May 24, 2026

Summary

This PR upgrades QQL from a primarily sync, one-query-at-a-time execution layer into a stronger application-facing client API.

It adds async execution, sync/async batching, parameterized query helpers, optional gRPC transport, and a shared utility layer that reduces duplicated executor/parser logic.

What Changed

Programmatic API

  • Added AsyncConnection for async applications using AsyncQdrantClient
  • Added AsyncExecutor to mirror the sync executor behavior with async / await
  • Added QQLBatch and QQLAsyncBatch context managers
  • Added batch helpers:
    • run_queries_batch()
    • run_parameterized_batch()
  • Added parameterized query helpers:
    • run_parameterized_query()
    • run_parameterized_batch()
  • Added optional gRPC connection support:
    • prefer_grpc
    • grpc_port

QQL Language

  • Added BEGIN BATCH ... END BATCH syntax
  • Added semicolon token support for batch blocks
  • Updated script splitting so batch blocks are treated as one executable statement

Executor / Parser Internals

  • Added BatchBlockStmt
  • Added grouped batch execution for compatible statements
  • Combined compatible inserts into bulk insert execution
  • Combined compatible search/recommend statements through Qdrant batch query APIs
  • Moved shared pure logic into qql.utils, including:
    • filter conversion
    • dense query shaping
    • vector payload shaping
    • topology parsing helpers
    • batch grouping
    • search parser helpers
    • parameter rendering
  • Integrated the latest collection topology improvements from PR fix for the issue#42 #44

Tests and Docs

  • Added async connection/executor coverage
  • Added tests for batching, parameterized queries, gRPC wiring, and async lifecycle behavior
  • Updated docs for async usage, batching, parameterized queries, gRPC, and batch-block syntax

Why

These changes make QQL more useful inside real Python applications, not only as a CLI.

Before this, repeated programmatic usage was mostly centered around one statement at a time. This PR adds the pieces needed for higher-throughput and async workloads:

  • reuse one connection across many queries
  • run multiple compatible queries with fewer Qdrant round trips
  • use QQL from async services
  • build query templates with parameters
  • opt into gRPC transport where appropriate
  • keep sync and async behavior aligned through shared helpers

Validation

  • uv run pytest -q
  • Result: 635 passed

Summary by CodeRabbit

  • New Features

    • BEGIN BATCH ... END BATCH for grouped queries/inserts; sync & async batch APIs and context managers returning per-statement results.
    • Async Python client and optional gRPC transport; parameterized queries with named-template rendering.
  • Behavior

    • Bulk inserts return inserted IDs; batched/programmatic batch calls return lists of per-statement results; improved NULL handling in IN/NOT IN.
  • Documentation

    • Expanded programmatic guide, examples (sync/async, batching, parameterized, gRPC) and quick syntax reference.
  • Tests

    • Test suite count updated to 635; new async, batching, and parameterization tests.

Review Change Stack

Add async QQL support with AsyncConnection and AsyncExecutor, plus sync
and async batching helpers for running multiple statements through one
programmatic API. Introduce BEGIN BATCH syntax, parameterized query
helpers, and optional gRPC connection settings.

Refactor shared parser and executor logic into qql.utils so sync and
async paths can reuse filter conversion, vector shaping, topology parsing,
batch grouping, and search parsing helpers.

Update tests and docs for async usage, batching, parameterized queries,
gRPC configuration, and batch block execution.
@srimon12 srimon12 requested a review from pavanjava May 24, 2026 14:02
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

📝 Walkthrough

Walkthrough

Adds BEGIN BATCH ... END BATCH syntax, parameterized queries and batching helpers, optional gRPC transport, an AsyncConnection/AsyncExecutor pair, shared utils for batching/filtering/search parsing, expanded package exports, documentation updates, and tests covering batching, parameter rendering, and async behavior.

Changes

Batch Execution & Async Support

Layer / File(s) Summary
Language Foundation: Lexer, Parser, AST, and Script Splitting
src/qql/lexer.py, src/qql/parser.py, src/qql/ast_nodes.py, src/qql/script.py
Lexer emits BEGIN/BATCH/END/SEMICOLON tokens; parser parses BEGIN BATCH ... END BATCH into BatchBlockStmt, accepts NULL literals, allows trailing semicolons; AST SearchWith fields are tri-state; script splitting tracks batch_depth.
Shared Utilities: Parameterized Rendering, Batching, and Search Parsing
src/qql/utils.py
New utilities implement render_parameterized_query, batch grouping/coalescing, bulk-insert builders/result normalization, Qdrant filter construction, fusion/MMR helpers, search-clause parsing helpers, and topology/vector utilities.
Sync Executor: Refactor and Batch Support
src/qql/executor.py
Executor delegates helpers to utils, adds _execute_batch_block and _execute_query_batch, updates bulk-insert result shape to {"ids": [...]}, and simplifies collection/topology handling.
Sync Connection: gRPC, Batching, Parameterized APIs and Proxies
src/qql/connection.py
Connection accepts prefer_grpc/grpc_port, adds run_queries_batch, run_parameterized_query, run_parameterized_batch, and introduces QQLBatch/OperationProxy for queued batch execution.
Async Executor: Full Async Implementation
src/qql/async_executor.py
New AsyncExecutor implements async handlers for create/alter/drop/show, INSERT/INSERT BULK, SEARCH (dense/sparse/hybrid, GROUP BY), RECOMMEND, DELETE/UPDATE, and batch execution using query_batch_points.
Async Connection: Async Context Manager and Batch APIs
src/qql/async_connection.py
Adds AsyncConnection constructing AsyncQdrantClient/AsyncExecutor with optional gRPC, run_query/run_queries_batch/parameterized helpers, QQLAsyncBatch and AsyncOperationProxy, plus lifecycle/context-manager methods.
Public API Exports
src/qql/__init__.py
Expands __all__ to re-export AsyncConnection, AsyncExecutor, QQLBatch, QQLAsyncBatch, OperationProxy/AsyncOperationProxy, executors, config/error/result types, lexer/parser, and run_query.
Documentation: Guides and Reference
README.md, docs/getting-started.md, docs/programmatic.md, docs/reference.md, docs/scripts.md
Updates badges and examples (test count → 635), README programmatic example shows parameterized queries and QQLBatch, Getting Started adds batch example, Programmatic doc covers gRPC and async usage, Reference lists public Python API and batch errors, Scripts doc documents BEGIN BATCH semantics.
Tests: Async Connection and Batching Validation
tests/test_async_connection.py, tests/test_connection.py, tests/test_executor.py, tests/test_parser.py
New async tests for AsyncConnection/QQLAsyncBatch; extended connection tests for batching/parameter rendering; executor/parser tests for grouped-search batching, null-filter mapping, merge_search_with, and batch grammar/concurrency scenarios.

Sequence Diagram (high-level batch execution flow):

sequenceDiagram
  participant Client
  participant Lexer
  participant Parser
  participant Executor
  participant QdrantClient
  Client->>Lexer: provide QQL (BEGIN BATCH / statements / END BATCH)
  Lexer->>Parser: tokens (including SEMICOLON, BEGIN, END)
  Parser->>Executor: BatchBlockStmt (statements tuple)
  Executor->>QdrantClient: query_batch_points / upsert / other calls
  QdrantClient-->>Executor: responses (per-statement)
  Executor-->>Client: list[ExecutionResult] / aggregated ExecutionResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • pavanjava

Poem

"🐰 In batches we burrow, queries in a line,
Async hops await, gRPC bells chime.
Parameters snug, results come in threes,
Docs updated, tests pass — how sweet the breeze!
635 carrots for this rabbit's time."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add async execution and batched programmatic APIs' directly and clearly summarizes the main features added in this PR: async execution capabilities and batched programmatic APIs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reference.md`:
- Line 174: Add a language identifier to the opening fenced code block so the
markdown linter (MD040) passes: change the opening ``` to ```text (i.e., update
the fenced block that currently starts with ``` before the listing "qql/" to
begin with ```text) so the code block has an explicit language.

In `@src/qql/async_connection.py`:
- Line 193: The batch resolver currently pairs proxies and results with
zip(self._proxies, results) in async_connection.py (and the analogous loop in
connection.py), which silently drops unmatched proxies or results; update the
post-run handling in the async method that calls run_queries_batch(...) to
explicitly compare len(results) to len(self._proxies) and, if they differ, raise
a clear RuntimeError (or set exception on each unmatched Proxy via
Proxy.set_exception) instead of silently leaving proxies unresolved; for matched
entries continue to call proxy.set_result(res) as before, and mirror the same
length-check and unmatched-proxy exception-handling logic in the sync batch
context manager in connection.py so all proxies are either resolved or fail
deterministically.

In `@src/qql/async_executor.py`:
- Around line 293-301: The code currently calls build_dense_point_vector (and
similarly build sparse vectors/PointStruct.vector) using a topology captured
before calling self._ensure_collection, which can race with another coroutine
that creates the collection with different vector names; change the flow so that
you first call self._ensure_collection(...) (and any hybrid creation lock), then
refresh/re-resolve the topology from the collection metadata that was actually
created/returned, and only after that call build_dense_point_vector (and the
corresponding sparse/vector construction) using the refreshed topology; apply
the same pattern for the other occurrences that build vectors (the blocks using
build_dense_point_vector and setting PointStruct.vector at the other mentioned
sites).

In `@src/qql/parser.py`:
- Around line 73-76: The parser currently calls parse() ->
_parse_single_statement() and immediately expects TokenKind.EOF, rejecting
trailing SEMICOLON tokens; update parse() to allow and consume any number of
trailing TokenKind.SEMICOLON tokens after the node is parsed (e.g., loop while
next token is TokenKind.SEMICOLON call the existing token-consumption helper
such as self._expect/_advance/_consume), then call self._expect(TokenKind.EOF)
and return the node so top-level statements like "SHOW COLLECTIONS;" succeed.

In `@src/qql/utils.py`:
- Around line 80-93: The function render_parameterized_query currently
serializes parameters via str() and inadequate escaping, causing None to become
"None" and backslashes/control chars to be unescaped; update
render_parameterized_query to: map None to the QQL NULL literal, escape strings
for single quotes and backslashes and encode common control sequences (e.g., \n,
\t, \\) so they are preserved as literals, format booleans as "true"/"false" and
numbers via their canonical representation, and then replace placeholders (keep
the current placeholder logic and sorted key order) with these properly
serialized QQL literals.
- Around line 160-170: The batching logic currently treats every SearchStmt as a
plain "query" which strips group semantics; change the branch so a SearchStmt is
only added to the "query" batch when it has no grouping (i.e., stmt.group_by and
stmt.group_size are falsy). For SearchStmt instances with group_by/group_size,
flush the current group via _append_batch_group and start a new group that is
not the plain "query" path (e.g., set current_type to a distinct token like
"grouped_search" and current_group = [stmt]) so
_execute_query_batch/run_queries_batch won’t treat it as a flat QueryRequest;
keep the existing behavior for RecommendStmt and non-grouped SearchStmt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20d846a7-d3f5-4169-a380-a106ae17a1cb

📥 Commits

Reviewing files that changed from the base of the PR and between dbb6713 and 8a066e1.

📒 Files selected for processing (17)
  • README.md
  • docs/getting-started.md
  • docs/programmatic.md
  • docs/reference.md
  • docs/scripts.md
  • src/qql/__init__.py
  • src/qql/ast_nodes.py
  • src/qql/async_connection.py
  • src/qql/async_executor.py
  • src/qql/connection.py
  • src/qql/executor.py
  • src/qql/lexer.py
  • src/qql/parser.py
  • src/qql/script.py
  • src/qql/utils.py
  • tests/test_async_connection.py
  • tests/test_connection.py

Comment thread docs/reference.md Outdated
Comment thread src/qql/async_connection.py Outdated
Comment thread src/qql/async_executor.py Outdated
Comment thread src/qql/parser.py
Comment thread src/qql/utils.py Outdated
Comment thread src/qql/utils.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qql/connection.py (1)

192-200: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset QQLBatch internal state on context exit (and re-entry).

self._queries and self._proxies are never cleared. Reusing the same QQLBatch instance can replay stale queries and corrupt proxy semantics. Clear lifecycle state in __enter__/__exit__.

Proposed fix
     def __enter__(self) -> QQLBatch:
+        self._queries.clear()
+        self._proxies.clear()
         return self

     def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
-        if exc_type is not None:
-            return
-        if not self._queries:
-            return
-        results = self.connection.run_queries_batch(self._queries)
-        for proxy, res in zip(self._proxies, results, strict=False):
-            proxy._resolve(res)
-        if len(results) != len(self._proxies):
-            error = RuntimeError(
-                "Batch result count mismatch: "
-                f"expected {len(self._proxies)}, got {len(results)}"
-            )
-            for proxy in self._proxies[len(results):]:
-                proxy._reject(error)
-            raise error
+        try:
+            if exc_type is not None or not self._queries:
+                return
+            results = self.connection.run_queries_batch(self._queries)
+            if len(results) != len(self._proxies):
+                error = RuntimeError(
+                    "Batch result count mismatch: "
+                    f"expected {len(self._proxies)}, got {len(results)}"
+                )
+                for proxy in self._proxies:
+                    proxy._reject(error)
+                raise error
+            for proxy, res in zip(self._proxies, results):
+                proxy._resolve(res)
+        finally:
+            self._queries.clear()
+            self._proxies.clear()

Also applies to: 195-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qql/connection.py` around lines 192 - 200, The QQLBatch context manager
currently never clears its internal state causing reused instances to replay
stale queries; update QQLBatch.__exit__ to clear self._queries and self._proxies
unconditionally (even on exception) after calling
self.connection.run_queries_batch (or skipping it when exc_type is present), and
also reset/initialize those collections in QQLBatch.__enter__ so each context
entry starts with a fresh state; reference the QQLBatch class and the
__enter__/__exit__ methods and ensure any early returns still perform the
cleanup to avoid stale-proxy semantics.
🧹 Nitpick comments (2)
src/qql/utils.py (2)

466-481: 💤 Low value

or fallback may incorrectly ignore zero values for numeric fields.

The merge uses override.hnsw_ef or base.hnsw_ef which treats 0 as falsy. If override.hnsw_ef = 0 is explicitly set, it would be ignored in favor of base.hnsw_ef. The same applies to mmr_candidates. While mmr_diversity correctly uses is not None, the other numeric fields don't.

♻️ Suggested fix for consistency
     return SearchWith(
-        hnsw_ef=override.hnsw_ef or base.hnsw_ef,
+        hnsw_ef=override.hnsw_ef if override.hnsw_ef is not None else base.hnsw_ef,
         exact=override.exact or base.exact,
         acorn=override.acorn or base.acorn,
         indexed_only=override.indexed_only or base.indexed_only,
         quantization=override.quantization or base.quantization,
         mmr_diversity=(
             override.mmr_diversity
             if override.mmr_diversity is not None
             else base.mmr_diversity
         ),
-        mmr_candidates=override.mmr_candidates or base.mmr_candidates,
+        mmr_candidates=override.mmr_candidates if override.mmr_candidates is not None else base.mmr_candidates,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qql/utils.py` around lines 466 - 481, The merge_search_with function
currently uses truthy "or" fallbacks which will ignore valid zero/False values
(e.g. override.hnsw_ef==0 or override.mmr_candidates==0); change each field
assignment to use explicit None checks: replace expressions like
override.hnsw_ef or base.hnsw_ef and override.mmr_candidates or
base.mmr_candidates with conditional expressions that return override.<field> if
override.<field> is not None else base.<field>, and do the same for boolean-like
fields (exact, acorn, indexed_only, quantization) so False is preserved rather
than falling back to base.

125-139: 💤 Low value

Consider handling list and dict values in _qql_literal.

If a parameterized query includes list or dict parameters (e.g., for IN clauses or INSERT VALUES), the current fallback to str(value) produces Python repr syntax ([1, 2, 3]"[1, 2, 3]") rather than valid QQL syntax. This may cause parse errors for compound values.

♻️ Suggested enhancement
 def _qql_literal(value: Any) -> str:
     if value is None:
         return "null"
     if isinstance(value, str):
         escaped = (
             value.replace("\\", "\\\\")
             .replace("'", "\\'")
             .replace("\n", "\\n")
             .replace("\t", "\\t")
             .replace("\r", "\\r")
         )
         return f"'{escaped}'"
     if isinstance(value, bool):
         return "true" if value else "false"
+    if isinstance(value, list):
+        return "[" + ", ".join(_qql_literal(v) for v in value) + "]"
+    if isinstance(value, dict):
+        items = ", ".join(f"'{k}': {_qql_literal(v)}" for k, v in value.items())
+        return "{" + items + "}"
     return str(value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qql/utils.py` around lines 125 - 139, The _qql_literal function currently
falls back to Python's str() for lists and dicts which yields invalid QQL;
update _qql_literal to special-case list/tuple and dict: for list/tuple, map
each element through _qql_literal and join with ", " inside square brackets
(e.g., [elem1, elem2]); for dict, produce a QQL object by iterating items and
emitting key: value pairs where keys are serialized as strings (use the same
string-escaping logic) and values are produced by _qql_literal, joined with ", "
inside curly braces; keep the existing handling for None, str, bool and
primitive types and ensure recursion for nested structures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/qql/connection.py`:
- Around line 200-210: The code currently calls
connection.run_queries_batch(self._queries) and then resolves proxies via
proxy._resolve before checking that len(results) == len(self._proxies), which
can cause wrong proxies to be resolved on a shifted/misaligned results list;
change the flow in the batch handling (around connection.run_queries_batch,
self._queries, self._proxies) to first compare len(results) and
len(self._proxies) and if they differ create the RuntimeError, call
proxy._reject(error) for every proxy (not just the tail) and raise the error,
otherwise only then iterate zip(self._proxies, results) and call
proxy._resolve(res) to safely resolve matched pairs.

In `@src/qql/utils.py`:
- Around line 304-309: When expr.value is None the code only handles "=" and
"!=" and lets other ops fall through into the Range construction (Range) which
will receive None; update the null handling in the expr.value is None branch to
defensively handle all non-equality operators by raising a clear error (e.g.
ValueError) or returning a safe Filter instead of falling through. Specifically,
inside the expr.value is None block that builds
IsNullCondition(PayloadField(key=expr.field)) and returns Filter(must_not=[...])
for "!=" make the function reject any op other than "=" or "!=" (using expr.op)
with an explicit error message referencing expr.field and expr.op so Range and
other downstream builders are never called with None.

---

Outside diff comments:
In `@src/qql/connection.py`:
- Around line 192-200: The QQLBatch context manager currently never clears its
internal state causing reused instances to replay stale queries; update
QQLBatch.__exit__ to clear self._queries and self._proxies unconditionally (even
on exception) after calling self.connection.run_queries_batch (or skipping it
when exc_type is present), and also reset/initialize those collections in
QQLBatch.__enter__ so each context entry starts with a fresh state; reference
the QQLBatch class and the __enter__/__exit__ methods and ensure any early
returns still perform the cleanup to avoid stale-proxy semantics.

---

Nitpick comments:
In `@src/qql/utils.py`:
- Around line 466-481: The merge_search_with function currently uses truthy "or"
fallbacks which will ignore valid zero/False values (e.g. override.hnsw_ef==0 or
override.mmr_candidates==0); change each field assignment to use explicit None
checks: replace expressions like override.hnsw_ef or base.hnsw_ef and
override.mmr_candidates or base.mmr_candidates with conditional expressions that
return override.<field> if override.<field> is not None else base.<field>, and
do the same for boolean-like fields (exact, acorn, indexed_only, quantization)
so False is preserved rather than falling back to base.
- Around line 125-139: The _qql_literal function currently falls back to
Python's str() for lists and dicts which yields invalid QQL; update _qql_literal
to special-case list/tuple and dict: for list/tuple, map each element through
_qql_literal and join with ", " inside square brackets (e.g., [elem1, elem2]);
for dict, produce a QQL object by iterating items and emitting key: value pairs
where keys are serialized as strings (use the same string-escaping logic) and
values are produced by _qql_literal, joined with ", " inside curly braces; keep
the existing handling for None, str, bool and primitive types and ensure
recursion for nested structures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e617cfeb-e5e9-40f1-bf9a-c206c8a7cae8

📥 Commits

Reviewing files that changed from the base of the PR and between 8a066e1 and 43e0b3a.

📒 Files selected for processing (10)
  • docs/reference.md
  • src/qql/async_connection.py
  • src/qql/async_executor.py
  • src/qql/connection.py
  • src/qql/parser.py
  • src/qql/utils.py
  • tests/test_async_connection.py
  • tests/test_connection.py
  • tests/test_executor.py
  • tests/test_parser.py
✅ Files skipped from review due to trivial changes (1)
  • docs/reference.md

Comment thread src/qql/connection.py Outdated
Comment thread src/qql/utils.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qql/utils.py (1)

315-343: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve null semantics for IN / NOT IN lists.

NULL now parses as a literal, but only the CompareExpr branch maps it to IsNullCondition. InExpr and NotInExpr still lower None as an ordinary list member, so queries like field IN (NULL) or field NOT IN (NULL, 1) won't behave consistently with the existing field = NULL / field != NULL paths.

Please either reject NULL in these lists or split it into explicit null predicates plus the remaining non-null values before building the scalar match condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qql/utils.py` around lines 315 - 343, The IN/NOT IN branches must
preserve NULL semantics: in the InExpr and NotInExpr handling, detect None
values in expr.values and split them from non-null values; for InExpr, if values
contain only None return IsNullCondition(is_null=PayloadField(key=expr.field)),
if they contain both None and non-null return a Filter with
should=[IsNullCondition(is_null=PayloadField(key=expr.field)),
FieldCondition(key=expr.field, match=MatchAny(any=non_nulls))], and if no None
keep the existing FieldCondition(...MatchAny(...)); for NotInExpr, if values
contain only None return
Filter(must_not=[IsNullCondition(is_null=PayloadField(key=expr.field))]), if
they contain both None and non-null return
Filter(must_not=[IsNullCondition(is_null=PayloadField(key=expr.field)),
FieldCondition(key=expr.field, match=MatchAny(any=non_nulls))]), and if no None
keep the existing MatchExcept behavior; update the branches referencing InExpr,
NotInExpr, FieldCondition, MatchAny, MatchExcept, IsNullCondition, PayloadField,
and Filter accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_async_connection.py`:
- Around line 259-262: The two with-block assertions currently only access the
properties ref1.result and ref2.result for their side effects, triggering Ruff
B018; inside each with pytest.raises(RuntimeError, match="Batch result count
mismatch") block, consume the property access by assigning its value to the
throwaway variable (e.g., _ = ref1.result and _ = ref2.result) so the assertions
remain but the linter no longer flags unused property access.

---

Outside diff comments:
In `@src/qql/utils.py`:
- Around line 315-343: The IN/NOT IN branches must preserve NULL semantics: in
the InExpr and NotInExpr handling, detect None values in expr.values and split
them from non-null values; for InExpr, if values contain only None return
IsNullCondition(is_null=PayloadField(key=expr.field)), if they contain both None
and non-null return a Filter with
should=[IsNullCondition(is_null=PayloadField(key=expr.field)),
FieldCondition(key=expr.field, match=MatchAny(any=non_nulls))], and if no None
keep the existing FieldCondition(...MatchAny(...)); for NotInExpr, if values
contain only None return
Filter(must_not=[IsNullCondition(is_null=PayloadField(key=expr.field))]), if
they contain both None and non-null return
Filter(must_not=[IsNullCondition(is_null=PayloadField(key=expr.field)),
FieldCondition(key=expr.field, match=MatchAny(any=non_nulls))]), and if no None
keep the existing MatchExcept behavior; update the branches referencing InExpr,
NotInExpr, FieldCondition, MatchAny, MatchExcept, IsNullCondition, PayloadField,
and Filter accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a84c6f1a-6686-4757-ba54-b396880d19f0

📥 Commits

Reviewing files that changed from the base of the PR and between 43e0b3a and 9865b0b.

📒 Files selected for processing (9)
  • src/qql/ast_nodes.py
  • src/qql/async_connection.py
  • src/qql/connection.py
  • src/qql/parser.py
  • src/qql/utils.py
  • tests/test_async_connection.py
  • tests/test_connection.py
  • tests/test_executor.py
  • tests/test_parser.py

Comment thread tests/test_async_connection.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qql/ast_nodes.py (1)

99-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Allow None in CompareExpr.value to match the implemented null-comparison behavior.

build_qdrant_filter() and the new tests already support CompareExpr(..., value=None), but this dataclass type still rejects it, creating a public typing mismatch.

Suggested fix
 `@dataclass`(frozen=True)
 class CompareExpr:
     """field op literal  — covers =, !=, >, >=, <, <="""
     field: str
     op: str   # one of: "=", "!=", ">", ">=", "<", "<="
-    value: str | int | float | bool
+    value: str | int | float | bool | None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qql/ast_nodes.py` around lines 99 - 103, CompareExpr.value's type
excludes None even though build_qdrant_filter and tests accept null comparisons;
update the CompareExpr dataclass signature to allow None by changing the type of
value to include None (e.g., Optional[str | int | float | bool] or str | int |
float | bool | None) and import typing.Optional if needed, and adjust the
docstring/comment to reflect that null is supported; ensure the symbol to edit
is CompareExpr and its value field in ast_nodes.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/qql/ast_nodes.py`:
- Around line 99-103: CompareExpr.value's type excludes None even though
build_qdrant_filter and tests accept null comparisons; update the CompareExpr
dataclass signature to allow None by changing the type of value to include None
(e.g., Optional[str | int | float | bool] or str | int | float | bool | None)
and import typing.Optional if needed, and adjust the docstring/comment to
reflect that null is supported; ensure the symbol to edit is CompareExpr and its
value field in ast_nodes.py.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b174115-5dab-4ee9-933f-005ade53ed3a

📥 Commits

Reviewing files that changed from the base of the PR and between 9865b0b and bfb81c4.

📒 Files selected for processing (5)
  • src/qql/ast_nodes.py
  • src/qql/utils.py
  • tests/test_async_connection.py
  • tests/test_connection.py
  • tests/test_executor.py

@srimon12 srimon12 marked this pull request as draft May 26, 2026 11:33
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.

1 participant