Skip to content

feat: implement v0.3.0 features - 29% TCK coverage achieved#104

Merged
DecisionNerd merged 20 commits into
mainfrom
feature/103-v0.3.0-tck-coverage
Feb 9, 2026
Merged

feat: implement v0.3.0 features - 29% TCK coverage achieved#104
DecisionNerd merged 20 commits into
mainfrom
feature/103-v0.3.0-tck-coverage

Conversation

@DecisionNerd
Copy link
Copy Markdown
Owner

@DecisionNerd DecisionNerd commented Feb 8, 2026

Summary

Implements v0.3.0 features to increase openCypher TCK coverage from 16.6% to 29.1%.

TCK Coverage Achievement:

  • Started: 638/3,837 scenarios (16.6%)
  • Finished: 1,117/3,837 scenarios (29.1%)
  • Improvement: +479 scenarios (+12.5 percentage points)

Major Features Implemented (11 total)

Phase 1-3: Core v0.3.0 Features

  1. ✅ Tree-based operator architecture for nested queries
  2. ✅ Left outer join primitive (OptionalExpandEdges)
  3. ✅ OPTIONAL MATCH clause - full implementation with NULL semantics
  4. ✅ UNION/UNION ALL - query result combination
  5. ✅ List comprehensions - [x IN list WHERE x > 5 | x * 2]
  6. ✅ EXISTS/COUNT subqueries - correlated nested queries
  7. ✅ Variable-length paths - [:TYPE*1..3] patterns
  8. ✅ IS NULL / IS NOT NULL operators - proper boolean semantics

Additional Improvements

  1. ✅ Column name preservation - p.id instead of col_0
  2. ✅ toBoolean() type conversion function
  3. ✅ List quantifiers - all(), any(), none(), single()
  4. ✅ Multi-clause UNWIND support - clause order preservation

Implementation Details

Tree-Based Operator Architecture

  • Added Union and Subquery operators
  • Enables nested query execution
  • Modified executor for nested operator dispatch

OPTIONAL MATCH (Left Outer Join)

  • Grammar: OPTIONAL MATCH (pattern)
  • Operator: OptionalExpandEdges
  • NULL preservation for unmatched patterns
  • Proper NULL propagation in expressions
  • 6 integration tests

UNION/UNION ALL

  • Combines query results with/without deduplication
  • Supports multiple UNION branches
  • Tree-based operator structure
  • 9 integration tests

List Comprehensions

  • Syntax: [x IN list WHERE x > 5 | x * 2]
  • Filter (WHERE) and map (|) operations
  • Context isolation for loop variables
  • 12 integration tests

EXISTS/COUNT Subqueries

  • Syntax: EXISTS { query } and COUNT { query }
  • Correlated subquery execution
  • Executor/planner threading through evaluation
  • 13 integration tests

Variable-Length Paths

  • Syntax: [:TYPE*1..3], [:TYPE*], [:TYPE*2..]
  • Depth-first search with cycle detection
  • Per-path visited tracking
  • 2 integration tests

List Quantifiers

  • all(x IN list WHERE predicate) - all items satisfy
  • any(x IN list WHERE predicate) - at least one satisfies
  • none(x IN list WHERE predicate) - no items satisfy
  • single(x IN list WHERE predicate) - exactly one satisfies
  • Correct vacuous truth semantics for all() on empty list

Multi-Clause UNWIND

  • Added reading_only_query grammar rule
  • Supports: MATCH + UNWIND + RETURN, UNWIND + MATCH + RETURN
  • Clause order preserved during planning
  • Variable dependencies between UNWIND and MATCH work correctly

Testing & Quality

  • Integration tests: 767 passing (100% pass rate maintained)
  • TCK tests: 1,117/3,837 passing (29.1%)
  • Code coverage: 91.96% maintained
  • New tests: 42+ comprehensive tests for new features
  • End-to-end tests: 12 passing, 5 skipped (feature gaps)

Test Categories

  • OPTIONAL MATCH: 6 tests
  • UNION/UNION ALL: 9 tests
  • List comprehensions: 12 tests
  • EXISTS/COUNT subqueries: 13 tests
  • Variable-length paths: 2 tests
  • Dataset workflows: 12 tests

Files Modified

Core Implementation (10 files)

  • src/graphforge/executor/executor.py - Column naming, execution logic, new operators
  • src/graphforge/executor/evaluator.py - Quantifiers, toBoolean, subqueries
  • src/graphforge/parser/cypher.lark - Grammar for all new features
  • src/graphforge/parser/parser.py - Transformers for new syntax
  • src/graphforge/planner/planner.py - Clause ordering, tree operators
  • src/graphforge/planner/operators.py - New operator types
  • src/graphforge/ast/expression.py - QuantifierExpression, SubqueryExpression
  • src/graphforge/ast/clause.py - OptionalMatchClause
  • src/graphforge/ast/pattern.py - Variable-length path fields

Tests (8 new test files)

  • Comprehensive integration tests for each feature
  • End-to-end dataset workflow tests

Documentation

  • CHANGELOG_v0.3.0.md - Comprehensive release notes (465 lines)
  • CLAUDE.md - Parallel testing documentation

Architecture Improvements

  1. Tree-based operators - Enables nested queries (UNION, subqueries)
  2. Clause order preservation - MATCH/UNWIND execute in declaration order
  3. Context isolation - Proper variable scoping for subqueries/comprehensions
  4. NULL semantics - IS NULL vs = NULL distinction
  5. Empty list handling - Fixed parser bug with []
  6. Executor threading - Pass executor/planner through evaluation chain

Performance Optimization

  • Parallel testing: TCK suite now runs 4x faster
    • Sequential: 3.5 minutes
    • Parallel (-n auto): 54 seconds
  • Documentation updated in CLAUDE.md

Known Limitations

  1. UNION validation - Missing column name/count validation (4 TCK tests)
  2. size() function - Not supported in WHERE clause context yet
  3. List comprehensions in WITH - Not fully supported in all contexts

What's Next for 39% Goal

To reach 39% target (~1,500 scenarios), remaining ~380 scenarios blocked by:

  • CALL procedure support (~80 scenarios) - Future work
  • Multi-line CREATE statements (~100 scenarios) - Parser limitation
  • Additional functions (date/time, spatial) - Future work
  • Minor edge cases

Breaking Changes

None. All changes are additive and maintain backward compatibility.

Commits

15 commits with comprehensive descriptions:

  • All follow conventional commit format
  • Co-authored with Claude Sonnet 4.5
  • Each commit includes rationale and test coverage

Closes #94

Summary by CodeRabbit

  • New Features

    • UNION / UNION ALL queries with correct branch planning and deduplication
    • OPTIONAL MATCH with left‑outer semantics and NULL propagation
    • Variable‑length path patterns (min/max/unbounded hops)
    • List comprehensions (filter/map), EXISTS/COUNT subqueries, quantifiers (ALL, ANY, NONE, SINGLE)
    • IS NULL / IS NOT NULL, dot‑notation property access, and broader NULL‑aware expression evaluation
  • Tests

    • Extensive new integration and unit suites covering these features and edge cases

DecisionNerd and others added 13 commits February 7, 2026 21:54
…TCH (#103)

**Phase 1 (Tasks 1-3) completed: Foundation for v0.3.0 TCK features**

## Changes

### 1. Tree-Based Operator Structure (Task #1)
- Add Union operator for UNION/UNION ALL query combination
- Add Subquery operator for EXISTS/COUNT subquery expressions
- Add OptionalExpandEdges operator for left outer join semantics
- Update executor to handle nested operator dispatch
- Maintain backward compatibility with flat operator lists

### 2. Left Outer Join Primitive (Task #2)
- Implement OptionalExpandEdges operator
- Add _execute_optional_expand() method with NULL preservation
- Handle left join semantics: preserve rows with NULL when no matches

### 3. OPTIONAL MATCH Grammar & AST (Task #3)
- Add OptionalMatchClause to ast/clause.py
- Add OPTIONAL MATCH grammar rule to cypher.lark
- Add parser transformer for optional_match_clause
- Update planner to convert OptionalMatchClause → OptionalExpandEdges
- Add _plan_optional_match() method to QueryPlanner

## Testing
- Add 6 integration tests for OPTIONAL MATCH
- 4/6 tests passing (2 require IS NULL support - Task #5)
- All existing 1736 tests still pass
- Coverage: 91.96% (above 85% threshold)

## Architecture Impact
- Executor now supports nested operator trees (enables UNION, subqueries)
- Planner can emit OptionalExpandEdges alongside ExpandEdges
- Parser recognizes OPTIONAL MATCH in query grammar
- Foundation ready for Phase 2 features (list comprehensions, EXISTS)

## Next Steps (Task #5)
- Add IS NULL / IS NOT NULL expression support
- Handle NULL property access gracefully
- Complete NULL propagation in WHERE/aggregation
- Target: ~150 additional TCK scenarios from OPTIONAL MATCH

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Task #5 completed: Full OPTIONAL MATCH end-to-end implementation**

## Changes

### 1. IS NULL / IS NOT NULL Support
- Add `IS NULL` and `IS NOT NULL` grammar rules to cypher.lark
- Represent as UnaryOp in AST (distinct from `= NULL`)
- Implement IS NULL evaluation in evaluator (returns true/false, not NULL)
- Handle proper NULL comparison semantics:
  - `x IS NULL` → true if x is NULL (not NULL itself)
  - `x = NULL` → NULL (standard ternary logic)

### 2. NULL Property Access
- Handle property access on NULL values gracefully
- Return NULL when accessing property on NULL (e.g., `f.name` when `f` is NULL)
- Prevents TypeError in OPTIONAL MATCH scenarios

### 3. Test Improvements
- Fix test expectations for NULL handling
- Add comprehensive IS NULL / IS NOT NULL tests
- All 6 OPTIONAL MATCH integration tests pass
- All 731 unit + integration tests pass

## NULL Semantics

**IS NULL (unary operator)**:
- `NULL IS NULL` → true
- `5 IS NULL` → false
- Returns CypherBool, never NULL

**= NULL (binary operator)**:
- `NULL = NULL` → NULL (ternary logic)
- `5 = NULL` → NULL
- Returns NULL for any NULL operand

**Property Access**:
- `node.property` when node is NULL → NULL
- `node.missing_property` → NULL

## Testing
- 731 tests pass (725 unit, 6 new OPTIONAL MATCH)
- 13 skipped (existing grammar limitations)
- Coverage: 91.96%

## Impact
- OPTIONAL MATCH fully functional with NULL handling
- IS NULL / IS NOT NULL work in WHERE clauses
- NULL values propagate correctly through expressions
- Ready for Phase 2 features (UNION, list comprehensions)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Task #4 completed: Full UNION/UNION ALL implementation**

## Changes

### 1. UNION Grammar
- Add `union_query` grammar rule to cypher.lark
- Support UNION and UNION ALL keywords
- Allow multiple query branches to be combined

### 2. Parser Support
- Add `union_query` transformer to parser.py
- Return dict with type='union', branches, and all flag
- Handle `union_distinct` and `union_all` transformers

### 3. API Integration
- Update GraphForge.execute() to detect UNION queries
- Plan each branch separately
- Create Union operator with all branches
- Execute Union operator and return combined results

### 4. Executor Enhancement
- Update execute() to recognize Union as RETURN-producing operator
- Prevent empty result check from failing on UNION queries
- Union._execute_union() already handles deduplication

## Features

**UNION (deduplicates)**:
```cypher
MATCH (p:Person) RETURN p.name
UNION
MATCH (c:Company) RETURN c.name
```

**UNION ALL (preserves duplicates)**:
```cypher
MATCH (p:Person) RETURN p.name
UNION ALL
MATCH (p:Person) RETURN p.name
```

**Multiple branches**:
```cypher
MATCH (p:Person) RETURN p.name
UNION
MATCH (c:Company) RETURN c.name
UNION
MATCH (d:Department) RETURN d.name
```

## Testing
- 9 comprehensive integration tests
- All 740 tests pass (731 previous + 9 new)
- Tests cover: basic UNION, UNION ALL, deduplication, multiple branches,
  ORDER BY, LIMIT, empty results, edge cases

## Impact
- UNION/UNION ALL fully functional
- ~30 TCK scenarios expected to pass
- Foundation supports nested query execution
- Ready for Phase 2 completion (list comprehensions, subqueries)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add support for list comprehension syntax: [x IN list WHERE x > 5 | x * 2]

## Changes
- Add ListComprehension AST node to ast/expression.py
- Extend grammar to support comprehension syntax in cypher.lark
- Add parser transformer for list_comprehension
- Implement evaluation logic in evaluator.py
- Create 12 comprehensive integration tests

## Features
- Basic iteration: [x IN [1,2,3]]
- Filtering with WHERE: [x IN list WHERE x > 5]
- Transformation with |: [x IN list | x * 2]
- Combined filter and map: [x IN list WHERE x > 5 | x * 2]
- Nested comprehensions
- NULL handling in filters

## Test Coverage
- 12 new integration tests (all passing)
- Tests cover basic iteration, filtering, mapping, edge cases
- 752 total integration tests passing
- 91.96% code coverage maintained

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add support for subquery expressions in WHERE and RETURN clauses.

## Changes
- Add SubqueryExpression AST node to ast/expression.py
- Extend grammar to support EXISTS and COUNT subqueries
- Add parser transformers for exists_expr and count_expr
- Implement subquery evaluation in evaluator.py
- Update executor to pass itself and planner for subquery execution
- Create 13 comprehensive integration tests

## Features
### EXISTS Subquery
- Returns boolean indicating if subquery matches any rows
- Can be used in WHERE clause for filtering
- Can be used in RETURN clause for computed columns
- Supports correlated subqueries (references outer variables)

### COUNT Subquery
- Returns integer count of rows matched by subquery
- Can be used in WHERE/WITH clause with comparisons
- Can be used in RETURN clause for aggregations
- Supports correlated subqueries

## Examples
```cypher
-- EXISTS in WHERE
MATCH (p:Person)
WHERE EXISTS { MATCH (p)-[:KNOWS]->() }
RETURN p.name

-- COUNT in RETURN
MATCH (p:Person)
RETURN p.name, COUNT { MATCH (p)-[:KNOWS]->() } AS friends

-- COUNT in WITH for filtering
MATCH (p:Person)
WITH p, COUNT { MATCH (p)-[:KNOWS]->() } AS friend_count
WHERE friend_count > 2
RETURN p.name
```

## Architecture
- Subqueries execute in isolated context with outer bindings
- Planner and executor passed through evaluation chain
- Supports nested query execution via operator pipeline

## Test Coverage
- 13 new integration tests (all passing)
- 765 total integration tests passing
- Tests cover EXISTS/COUNT in various contexts
- Tests cover correlated subqueries and edge cases

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add support for variable-length relationship traversal like -[:KNOWS*1..3]->

## Changes
- Extend RelationshipPattern AST to include min_hops and max_hops
- Add grammar support for *, *1..3, *..3, *1.. syntax
- Add ExpandVariableLength operator for recursive traversal
- Implement depth-first search with cycle detection in executor
- Update planner to emit ExpandVariableLength for var-length patterns
- Create 2 integration tests

## Features
### Variable-Length Syntax
- `*` - 1 or more hops (unbounded)
- `*1..3` - min 1, max 3 hops
- `*..3` - min 1, max 3 hops
- `*2..` - min 2, unbounded hops

### Implementation
- Depth-first search traversal
- Cycle detection to prevent infinite loops
- Edge list binding for paths
- Type and direction filtering

## Examples
```cypher
-- Find all friends (direct and indirect)
MATCH (p:Person {name: 'Alice'})-[:KNOWS*]->(f)
RETURN f.name

-- Find friends within 2 hops
MATCH (p)-[:KNOWS*1..2]->(f)
RETURN f.name

-- Find all ancestors
MATCH (p)-[:PARENT*]->(ancestor)
RETURN ancestor.name
```

## Test Coverage
- 2 new integration tests (all passing)
- 767 total integration tests passing
- Tests cover unbounded and bounded patterns
- Tests verify cycle detection

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Document all 8 major features implemented
- OPTIONAL MATCH, UNION, list comprehensions
- EXISTS/COUNT subqueries, variable-length paths
- IS NULL operators, tree-based architecture
- Include syntax examples and use cases
- Implementation details and test coverage
- Architecture improvements and migration guide
- 767 passing integration tests documented

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add PropertyAccess to executor imports
- Generate dotted notation (e.g., 'p.name') for property access
- Previously used generic 'col_N' names
- Update test to expect proper column names
- Fixes TCK result format expectations

This improves TCK compatibility by matching expected result column names.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add toBoolean to TYPE_FUNCTIONS set
- Implement toBoolean in _evaluate_type_function
- Support string-to-boolean conversion ("true"/"false" case-insensitive)
- Add toboolean to FUNCTION_NAME grammar terminal
- Returns null for invalid inputs

Completes type conversion function suite for TCK compatibility.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add QuantifierExpression AST node
- Add quantifier_expr grammar rules (ALL, ANY, NONE, SINGLE)
- Implement parser transformers for all quantifiers
- Add evaluation logic in evaluator
- Fix empty list parsing bug (filtered None values)
- Implement correct semantics:
  * ALL: true if all items satisfy (vacuous truth for empty list)
  * ANY: true if at least one item satisfies
  * NONE: true if no items satisfy
  * SINGLE: true if exactly one item satisfies

Expected TCK impact: ~100-150 scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add reading_only_query grammar rule for multiple reading clauses
- Support patterns: MATCH + UNWIND + RETURN, UNWIND + MATCH + RETURN
- Add WHERE clause support in reading-only queries
- Fix planner to respect clause order for reading clauses
- Previously MATCH always executed before UNWIND
- Now UNWIND and MATCH execute in declaration order
- Flatten nested reading_clause lists in parser

This enables variable dependencies between UNWIND and MATCH:
- UNWIND ['Alice', 'Bob'] AS name MATCH (p {name: name})
- MATCH (p) UNWIND p.tags AS tag RETURN p, tag

Expected TCK impact: ~10-15 scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Document pytest -n flag for parallel execution
- TCK tests run 4x faster with -n auto (54s vs 3.5min)
- Add examples for auto CPU detection and manual worker count

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add TestDatasetDiscovery for dataset listing and filtering
- Add TestSmallDatasetLoading for collaboration network queries
- Add TestComplexQueries for v0.3.0 feature patterns
- Add TestDataExportWorkflow for JSON export/import
- Add TestRealWorldUseCases for typical query patterns

Tests demonstrate:
- Dataset discovery and metadata access
- Loading SNAP datasets and querying
- OPTIONAL MATCH, UNION, EXISTS, quantifier usage
- JSON graph export/import roundtrip
- Common patterns: degree queries, paths, aggregation

12 tests passing, 5 skipped (awaiting size() function support)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@DecisionNerd DecisionNerd added the enhancement New feature or request label Feb 8, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Walkthrough

Adds UNION/UNION ALL, OPTIONAL MATCH, variable‑length relationships, list comprehensions, EXISTS/COUNT subqueries, and quantifier expressions across parser, AST, planner, executor, evaluator, and API; wires planner into the executor and executor into subquery evaluation; adds extensive integration/unit tests.

Changes

Cohort / File(s) Summary
Parser & Transformer
src/graphforge/parser/cypher.lark, src/graphforge/parser/parser.py
Grammar and transformer extended for UNION/UNION ALL, OPTIONAL MATCH, variable-length relationship ranges, null checks, quantifiers, EXISTS/COUNT subqueries, and list comprehensions.
AST Models
src/graphforge/ast/query.py, src/graphforge/ast/clause.py, src/graphforge/ast/expression.py, src/graphforge/ast/pattern.py
New UnionQuery, OptionalMatchClause, ListComprehension, SubqueryExpression, QuantifierExpression; RelationshipPattern gains min_hops/max_hops and validators.
Planner Operators & Planner
src/graphforge/planner/operators.py, src/graphforge/planner/planner.py
New operators: Union, OptionalExpandEdges, ExpandVariableLength, OptionalScanNodes, Subquery; planner adds _plan_optional_match, emits optional/variable-length operators and integrates UNION planning.
Executor & Evaluator
src/graphforge/executor/executor.py, src/graphforge/executor/evaluator.py
QueryExecutor.__init__ now accepts/stores planner; executor dispatch handles Union, ExpandVariableLength, OptionalExpandEdges, Subquery; evaluator adds list comprehension, quantifier, and subquery evaluation and accepts an executor context.
API Wiring
src/graphforge/api.py
execute now plans UNION branches separately, constructs a Union operator (honoring ALL flag), and executes it via executor; executor constructed with planner.
Tests (integration & unit)
tests/integration/*, tests/unit/*, tests/integration/datasets/test_end_to_end_workflows.py
Large new integration and unit tests covering UNION/UNION ALL, OPTIONAL MATCH, variable-length paths, list comprehensions, EXISTS/COUNT subqueries, quantifiers, and dataset workflows; minor test expectation adjustments.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Parser as Parser\n(`cypher.lark` / `parser.py`)
  participant Planner as Planner\n(`planner.py`)
  participant Executor as Executor\n(`executor.py`)
  participant Storage as Storage\n(Graph)

  Client->>Parser: submit query (single or UNION)
  Parser-->>Client: AST (CypherQuery or UnionQuery)
  alt AST is UnionQuery
    Client->>Planner: plan branch 1 AST
    Planner-->>Planner: produce operators (branch 1)
    Client->>Planner: plan branch 2 AST
    Planner-->>Planner: produce operators (branch 2)
    Planner-->>Executor: `Union` operator (branches + all flag)
  else single query
    Client->>Planner: plan AST
    Planner-->>Executor: operator pipeline
  end
  Executor->>Storage: execute branch 1 operators
  Storage-->>Executor: rows1
  Executor->>Storage: execute branch 2 operators
  Storage-->>Executor: rows2
  Executor->>Executor: merge rows (dedupe if not ALL)
  Executor-->>Client: result set
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

parser

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR closes issue #94 about zip compression support, but the implementation and description document v0.3.0 features (UNION, OPTIONAL MATCH, quantifiers, etc.) with no evidence of zip extraction functionality, path traversal validation, or compression utilities changes. Implement zip compression support from issue #94: add extract_zip() with path-traversal prevention, update extract_archive()/is_compressed_archive() to handle .zip, add unit/integration tests for compression utilities, and verify all success criteria are met.
Out of Scope Changes check ⚠️ Warning The PR implements extensive v0.3.0 features (UNION, OPTIONAL MATCH, list comprehensions, subqueries, variable-length paths, quantifiers) that are not mentioned in the linked issue #94 about zip compression support, indicating a significant scope mismatch. The PR appears to address a different feature set than issue #94. Either correct the linked issue reference or ensure the zip compression support from #94 is actually implemented with appropriate tests and documentation.
Description check ❓ Inconclusive The description is comprehensive and well-structured with clear sections on summary, features, implementation, testing, and architecture. However, there is a significant mismatch with the linked issue about zip compression support. The PR description focuses entirely on v0.3.0 features and TCK coverage but the linked issue #94 is about zip compression support for dataset loaders. Clarify if this PR addresses #94's objectives or if it was mislabeled.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the major achievement of implementing v0.3.0 features and the TCK coverage improvement (16.6% to 29.1%), which is the primary change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/103-v0.3.0-tck-coverage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 8, 2026

Codecov Report

❌ Patch coverage is 65.27546% with 208 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.34%. Comparing base (262c23e) to head (d18aaa3).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #104      +/-   ##
==========================================
- Coverage   90.03%   86.34%   -3.69%     
==========================================
  Files          31       31              
  Lines        3581     4131     +550     
  Branches      882     1013     +131     
==========================================
+ Hits         3224     3567     +343     
- Misses        180      338     +158     
- Partials      177      226      +49     
Flag Coverage Δ
full-coverage 86.34% <65.27%> (-3.69%) ⬇️
unittests 68.55% <30.55%> (-6.57%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
parser 90.43% <74.03%> (-5.20%) ⬇️
planner 86.09% <72.13%> (-6.06%) ⬇️
executor 80.76% <55.05%> (-7.63%) ⬇️
storage 99.62% <ø> (ø)
ast 81.39% <76.00%> (-1.80%) ⬇️
types 99.00% <ø> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 262c23e...d18aaa3. Read the comment docs.

Copy link
Copy Markdown
Contributor

@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: 9

Caution

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

⚠️ Outside diff range comments (3)
src/graphforge/ast/pattern.py (1)

84-129: ⚠️ Potential issue | 🟡 Minor

Missing cross-field validation: min_hops must be ≤ max_hops.

When both min_hops and max_hops are provided, there's no check that min_hops <= max_hops. This could lead to patterns like *5..2 silently producing no results instead of erroring at AST construction time.

Proposed fix — add a model validator
+from pydantic import BaseModel, Field, field_validator, model_validator
 ...
 
     `@field_validator`("max_hops")
     `@classmethod`
     def validate_max_hops(cls, v: int | None) -> int | None:
         """Validate maximum hops."""
         if v is not None and v < 1:
             raise ValueError(f"Maximum hops must be positive, got {v}")
         return v
 
+    `@model_validator`(mode="after")
+    def validate_hop_range(self) -> "RelationshipPattern":
+        """Validate min_hops <= max_hops when both are set."""
+        if self.min_hops is not None and self.max_hops is not None:
+            if self.min_hops > self.max_hops:
+                raise ValueError(
+                    f"min_hops ({self.min_hops}) must be <= max_hops ({self.max_hops})"
+                )
+        return self
+
     model_config = {"frozen": True, "arbitrary_types_allowed": True}

As per coding guidelines: "All AST node dataclasses MUST use ... @model_validator(mode='after') for cross-field validation"

src/graphforge/planner/planner.py (1)

446-544: ⚠️ Potential issue | 🟡 Minor

_plan_optional_match does not support variable-length patterns.

Unlike _plan_match (which emits ExpandVariableLength for min_hops/max_hops), this method always emits OptionalExpandEdges. If a query uses OPTIONAL MATCH (p)-[:KNOWS*1..3]->(f), the variable-length hops will be silently ignored.

At minimum, consider raising a NotImplementedError for variable-length patterns in OPTIONAL MATCH so the user gets a clear error rather than incorrect results.

Additionally, there's substantial code duplication between _plan_match and _plan_optional_match (source node scanning, property filtering, variable generation). Consider extracting the shared pattern-processing logic into a helper, parameterized by the expand operator type.

src/graphforge/executor/evaluator.py (1)

365-381: ⚠️ Potential issue | 🟠 Major

AND/OR operators do not implement openCypher ternary NULL semantics.

Current implementation returns CypherNull() whenever either operand is NULL. However, openCypher requires short-circuit evaluation:

  • FALSE AND NULL must return FALSE (not NULL)
  • TRUE OR NULL must return TRUE (not NULL)

Per the coding guidelines: "Executor methods MUST handle NULL semantics correctly using ternary logic for AND/OR/NOT operators." Update the AND/OR handlers to check for definitive values before returning NULL.

Reference: TCK Boolean1/Boolean2 feature specifications define the complete ternary truth tables.

🤖 Fix all issues with AI agents
In `@src/graphforge/executor/executor.py`:
- Around line 1560-1604: The bug in _execute_union is that it always calls
_execute_operator with op_index=0 for each branch_op, causing downstream logic
in _execute_aggregate (which checks op_index < total_ops - 1) to miscompute for
non-final operators; fix by iterating the branch with an index (e.g., use
enumerate over branch) and pass the actual operator index and total_ops
(len(branch)) into _execute_operator instead of the hardcoded 0 so that for_with
and aggregation-finalization behave correctly.
- Around line 295-358: The DFS in _execute_variable_expand never updates
visited_in_path — new_visited is computed but not passed along — causing cycle
detection to fail; fix by tracking the visited set per path: change the stack
entries from (current_node, edge_path, depth) to (current_node, edge_path,
depth, visited_set), initialize visited_set = {src_node.id} for each ctx, use
that visited_set when checking if next_node.id is allowed, and when pushing a
neighbor push a copy/union (e.g., visited_set | {next_node.id}) so each path
maintains its own visited nodes (update uses of stack popping/pushing and the
cycle check to reference the per-path visited_set instead of the stale
visited_in_path).

In `@src/graphforge/parser/parser.py`:
- Around line 58-80: The union_query transformer currently returns a plain dict;
create a proper AST node class (e.g., UnionQuery) and return an instance from
union_query instead of {"type":"union"...}; ensure UnionQuery has fields
branches: List[CypherQuery] and all: bool and update any downstream conversion
in api.py to consume the new UnionQuery type (or add a compatibility wrapper)
and validate that all union clauses are consistent (if you detect mixed "UNION"
and "UNION ALL" values while walking items, raise/emit a parse error instead of
silently using the last seen value). For the list_comprehension bug, update the
list_comprehension transformer to disambiguate 3-item forms by inspecting the
third item's type/shape (e.g., check isinstance(items[2], Tree) and its
data/name to distinguish a where/filter node vs a map expression or the literal
'|' delimiter) and construct the ListComprehension AST with the correct fields
(filter vs map) accordingly.
- Around line 775-809: The list_comprehension transformer (function
list_comprehension) can't tell a 3-item payload is a WHERE vs a map-only '|'
clause because the grammar collapses them; update the grammar to use named
sub-rules (e.g., list_comprehension: "[" variable "IN"i expression where_clause?
map_clause? "]" with where_clause: "WHERE"i expression and map_clause: "|"
expression) so the parser produces distinct AST nodes (e.g., WhereClause and
MapClause), then change list_comprehension to inspect the item types
(isinstance(..., WhereClause) vs isinstance(..., MapClause)) to assign
filter_expr or map_expr correctly and handle the 4-item case accordingly.

In `@tests/integration/datasets/test_end_to_end_workflows.py`:
- Around line 207-227: The test_quantifier_pattern test uses a weak assertion
allowing either Alice or Carol, which is too permissive; update the assertions
after executing gf.execute(...) so they validate that both 'Alice' and 'Carol'
are present (e.g., assert both names in the results or assert the result name
set equals {'Alice','Carol'}) instead of using the current "or" check on the
names list; adjust references to results and names to make this explicit and
ensure Bob is excluded.
- Around line 1-10: The tests in this file are missing pytest markers; add
`@pytest.mark.integration` to every test class (and/or test functions) in
tests/integration/datasets/test_end_to_end_workflows.py, and for any tests that
call load_dataset(..., "snap-ca-grqc") or otherwise download the large
snap-ca-grqc dataset mark those specific classes/functions with
`@pytest.mark.slow` instead of (or in addition to) integration; ensure you import
pytest at the top and apply the decorators directly above each test class or
test function so pytest picks up the markers.

In `@tests/integration/test_optional_match.py`:
- Around line 6-8: Add the missing pytest integration markers: import pytest if
not present, and decorate both TestOptionalMatchBasic and
TestOptionalMatchNullHandling with `@pytest.mark.integration` so these classes are
identified as integration tests; ensure the decorator is placed directly above
each class definition (TestOptionalMatchBasic, TestOptionalMatchNullHandling).

In `@tests/integration/test_subquery_expressions.py`:
- Around line 6-7: Add the missing pytest integration markers by decorating each
test class with `@pytest.mark.integration`: place the decorator immediately above
the class definitions for TestExistsSubquery, TestCountSubquery, and
TestSubqueryEdgeCases; ensure pytest is imported in the module (e.g., import
pytest) if not already present so the decorator resolves correctly.

In `@tests/integration/test_union.py`:
- Around line 165-178: Rename the misleading test function
test_union_different_column_count to something like
test_union_different_value_types and update its docstring to state it verifies
UNION with same column count but differing value types using GraphForge and
gf.execute; then add a new test (e.g., test_union_mismatched_column_count) that
executes two UNION branches returning different numbers of columns via
gf.execute and asserts the expected behavior (either that an error is raised or
the current fallback behavior, per project spec) to ensure the "missing UNION
column validation" limitation is covered.
🧹 Nitpick comments (16)
src/graphforge/parser/cypher.lark (1)

204-207: list_comprehension as an alternative under list_literal is unconventional but functional.

Placing list_comprehension under list_literal means list comprehensions will produce a list_literal tree node wrapping a list_comprehension subtree, rather than appearing directly. Ensure the transformer handles this nesting. If that's already working (tests pass), this is fine — just noting the structural choice.

#!/bin/bash
# Check how the transformer handles list_literal and list_comprehension
ast-grep --pattern $'def list_literal(self, $$$) {
  $$$
}'
rg -n 'def list_literal\|def list_comprehension' --type=py -C5
tests/integration/test_list_comprehension.py (1)

6-7: Missing @pytest.mark.integration markers on test classes.

Per coding guidelines, integration tests must be marked with @pytest.mark.integration. None of the test classes in this file have the marker.

🔧 Proposed fix (apply to each test class)
+import pytest
+
+
+@pytest.mark.integration
 class TestListComprehensionBasic:
     """Test basic list comprehension functionality."""

Similarly for TestListComprehensionWithData and TestListComprehensionEdgeCases.

As per coding guidelines: "Mark tests with @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.tck, or @pytest.mark.slow"

tests/integration/test_variable_length_paths.py (1)

6-7: Missing @pytest.mark.integration marker.

Same as other integration test files — this class needs @pytest.mark.integration per coding guidelines.

As per coding guidelines: "Mark tests with @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.tck, or @pytest.mark.slow"

tests/integration/test_union.py (1)

6-7: Missing @pytest.mark.integration marker on all test classes.

Same pattern as other integration test files.

As per coding guidelines: "Mark tests with @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.tck, or @pytest.mark.slow"

src/graphforge/api.py (2)

244-245: Inline import of Union operator inside execute().

from graphforge.planner.operators import Union is imported inside the method body. If this is to avoid a circular import, consider adding a comment explaining the reason. Otherwise, move it to the top-level imports.


236-248: Create a UnionQuery model instead of returning a raw dict for UNION queries.

The parser returns a raw dict ({"type": "union", "branches": [...], "all": bool}) for UNION queries instead of a dedicated AST node. This breaks consistency with regular queries (which use CypherQuery) and creates an implicit, unvalidated contract: dict keys are unchecked strings, and type dispatch in api.py relies on isinstance(ast, dict) rather than proper type matching.

Additionally, the single all boolean cannot represent mixed UNION / UNION ALL in the same query (e.g., MATCH ... UNION ALL MATCH ... UNION MATCH ...). The parser's line 79 explicitly documents this limitation: "For simplicity, use the last UNION type seen"—only the final union clause type is retained, losing information about intermediate clauses.

Create a UnionQuery dataclass in src/graphforge/ast/query.py (similar to CypherQuery) with proper fields, and return it from the parser instead of a raw dict. This would align with the AST design and enable proper validation at the source.

tests/integration/test_subquery_expressions.py (1)

204-215: Test name is misleading — it does not actually test nested subqueries.

test_nested_subqueries_not_supported describes nested subqueries but the actual query is a simple EXISTS { MATCH ... } — no nesting. The docstring says "Test that nested subqueries are properly handled" but no nesting is exercised. Consider either renaming to reflect what it actually tests (e.g., test_exists_with_labeled_inner_node) or adding a real nested subquery test with an expected error/behavior.

src/graphforge/executor/evaluator.py (1)

477-500: Subquery evaluation calls private executor._execute_operator — consider adding a public method.

The evaluator reaches into executor internals (executor._execute_operator). This tight coupling is fragile if the executor's internal API changes. A dedicated executor.execute_subquery(operators, ctx) method would be cleaner.

Also, Line 492 passes hardcoded 0 for op_index and len(operators) for total_ops for every operator in the pipeline. In the executor's _execute_aggregate, op_index < total_ops - 1 determines for_with behavior. With this hardcoded approach, the first operator in a multi-operator pipeline will always get op_index=0, total_ops=N — which happens to work for EXISTS/COUNT subqueries since they shouldn't have RETURN clauses producing final output. But it's brittle.

src/graphforge/planner/planner.py (1)

414-442: Variable-length expansion in _plan_match looks correct; redundant import on Line 417.

ExpandVariableLength is already imported at the top of the file (Line 32), so the local import on Line 417 is unnecessary.

Proposed fix
                    # Check if this is a variable-length pattern
                    if rel_pattern.min_hops is not None or rel_pattern.max_hops is not None:
                        # Variable-length expansion
-                       from graphforge.planner.operators import ExpandVariableLength
-
                        operators.append(
                            ExpandVariableLength(
src/graphforge/ast/expression.py (1)

225-231: Variable validators are less strict than elsewhere in the codebase.

ListComprehension.validate_variable and QuantifierExpression.validate_variable only check the first character, while NodePattern, RelationshipPattern (in pattern.py), and Variable (in this same file) also verify the full name is alphanumeric + underscore via v.replace("_", "").isalnum().

Proposed fix (apply to both validators)
     def validate_variable(cls, v: str) -> str:
         """Validate variable name."""
         if not v[0].isalpha() and v[0] != "_":
             raise ValueError(f"Variable must start with letter or underscore: {v}")
+        if not v.replace("_", "").isalnum():
+            raise ValueError(f"Variable must contain only alphanumeric and underscore: {v}")
         return v

Also applies to: 281-287

src/graphforge/executor/executor.py (2)

137-140: Lazy import of ExpandVariableLength inside _execute_operator — consider top-level import.

ExpandVariableLength is imported locally inside the dispatch method (Line 137) and again inside _execute_variable_expand (Line 299). Since it's used on every call to _execute_operator, a top-level import would be cleaner and avoid repeated import overhead.

Proposed fix

Add to the existing import block at the top of the file:

 from graphforge.planner.operators import (
     Aggregate,
     Create,
     Delete,
     Distinct,
     ExpandEdges,
+    ExpandVariableLength,
     Filter,

Then remove the local imports on Lines 137 and 299.


1606-1629: _execute_subquery is a placeholder that ignores the op argument.

The method is unreachable via normal query execution since subqueries are evaluated in the evaluator. If it's needed only for future use, consider raising NotImplementedError rather than silently passing through, so unintended calls are caught early. Ruff also flags the unused op parameter (ARG002).

src/graphforge/planner/operators.py (2)

107-114: Consider extracting the duplicated direction validator.

The validate_direction method is copy-pasted identically across ExpandEdges (line 70), ExpandVariableLength (line 107), and OptionalExpandEdges (line 469). A small helper or a shared mixin would reduce duplication.

♻️ Example: shared validator helper
_VALID_DIRECTIONS = {"OUT", "IN", "UNDIRECTED"}

def _validate_direction(v: str) -> str:
    if v not in _VALID_DIRECTIONS:
        raise ValueError(f"Direction must be one of {_VALID_DIRECTIONS}, got {v}")
    return v

Then in each class:

     `@field_validator`("direction")
     `@classmethod`
     def validate_direction(cls, v: str) -> str:
         """Validate direction is valid."""
-        valid_dirs = {"OUT", "IN", "UNDIRECTED"}
-        if v not in valid_dirs:
-            raise ValueError(f"Direction must be one of {valid_dirs}, got {v}")
-        return v
+        return _validate_direction(v)

Also applies to: 469-476


398-407: Redundant validator — min_length=2 already enforces the branch count.

The Field(..., min_length=2) on line 398 will reject lists shorter than 2 at Pydantic's field-validation level, making the explicit validate_branches validator on lines 401-407 purely redundant. Not harmful, but you could remove the custom validator to keep the model concise.

src/graphforge/parser/parser.py (2)

667-714: Quantifier transformer methods are structurally identical — consider extraction.

All four quantifier methods (all_quantifier, any_quantifier, none_quantifier, single_quantifier) share identical logic, differing only in the quantifier= string. A private helper would reduce the ~48 lines to ~15.

♻️ Proposed refactor
+    def _build_quantifier(self, items, quantifier_type: str):
+        """Build a QuantifierExpression from transformer items."""
+        from graphforge.ast.expression import QuantifierExpression
+
+        variable = items[0].name if hasattr(items[0], "name") else str(items[0])
+        return QuantifierExpression(
+            quantifier=quantifier_type, variable=variable, list_expr=items[1], predicate=items[2]
+        )
+
     def all_quantifier(self, items):
-        """Transform ALL quantifier expression.
-
-        Syntax: ALL(x IN list WHERE predicate)
-        """
-        from graphforge.ast.expression import QuantifierExpression
-
-        variable = items[0].name if hasattr(items[0], "name") else str(items[0])
-        return QuantifierExpression(
-            quantifier="ALL", variable=variable, list_expr=items[1], predicate=items[2]
-        )
+        """Transform ALL quantifier expression."""
+        return self._build_quantifier(items, "ALL")
 
     def any_quantifier(self, items):
-        ...
+        """Transform ANY quantifier expression."""
+        return self._build_quantifier(items, "ANY")
 
     def none_quantifier(self, items):
-        ...
+        """Transform NONE quantifier expression."""
+        return self._build_quantifier(items, "NONE")
 
     def single_quantifier(self, items):
-        ...
+        """Transform SINGLE quantifier expression."""
+        return self._build_quantifier(items, "SINGLE")

647-714: Hoist repeated imports of AST expression types to module level.

SubqueryExpression, QuantifierExpression, and ListComprehension are imported inside individual methods rather than at module level. There are no circular dependencies blocking this refactor. Consolidating these imports into the existing graphforge.ast.expression import block at the top of the file would eliminate 8 redundant import statements and improve readability.

♻️ Proposed fix — extend existing import
 from graphforge.ast.expression import (
     BinaryOp,
     CaseExpression,
     FunctionCall,
+    ListComprehension,
     Literal,
     PropertyAccess,
+    QuantifierExpression,
+    SubqueryExpression,
     UnaryOp,
     Variable,
 )

Then remove the local from graphforge.ast.expression import ... statements from lines 652, 662, 672, 685, 697, 709, 760, and 781.

Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/parser/parser.py Outdated
Comment thread src/graphforge/parser/parser.py
Comment thread tests/integration/datasets/test_end_to_end_workflows.py
Comment thread tests/integration/datasets/test_end_to_end_workflows.py Outdated
Comment thread tests/integration/test_optional_match.py
Comment thread tests/integration/test_subquery_expressions.py
Comment thread tests/integration/test_union.py Outdated
- Add ExpandVariableLength import to executor.py top-level imports
- Fix type annotations for list comprehension items variable
- Fix type annotations for variable-length path stack
- Update node ID set type to handle str | int
- Fix import ordering in test_end_to_end_workflows.py
- Update test_unknown_type_function_raises_error to use truly unknown function
- All pre-push checks passing: format, lint, type-check, tests, coverage

All 1,790 tests passing with 91.43% coverage.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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: 5

Caution

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

⚠️ Outside diff range comments (2)
src/graphforge/planner/planner.py (1)

510-540: ⚠️ Potential issue | 🟠 Major

OPTIONAL MATCH ignores variable‑length ranges.

min_hops/max_hops are not honored in OPTIONAL MATCH; the planner always emits OptionalExpandEdges, so * patterns become single‑hop. Add optional variable‑length support or fail fast for now.

Based on learnings: "Project prioritizes correctness over performance and must match openCypher semantics exactly."

src/graphforge/executor/evaluator.py (1)

365-381: ⚠️ Potential issue | 🟠 Major

AND/OR do not implement ternary NULL semantics.

Returning NULL whenever either operand is NULL is incorrect for Cypher (e.g., FALSE AND NULL → FALSE).

🔧 Suggested fix
-        if expr.op == "AND":
-            # Handle NULL propagation
-            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
-                return CypherNull()
-            # Both must be booleans
-            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
-                return CypherBool(left_val.value and right_val.value)
-            raise TypeError("AND requires boolean operands")
+        if expr.op == "AND":
+            if isinstance(left_val, CypherBool) and not left_val.value:
+                return CypherBool(False)
+            if isinstance(right_val, CypherBool) and not right_val.value:
+                return CypherBool(False)
+            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
+                return CypherNull()
+            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
+                return CypherBool(True)
+            raise TypeError("AND requires boolean operands")
 
-        if expr.op == "OR":
-            # Handle NULL propagation
-            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
-                return CypherNull()
-            # Both must be booleans
-            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
-                return CypherBool(left_val.value or right_val.value)
-            raise TypeError("OR requires boolean operands")
+        if expr.op == "OR":
+            if isinstance(left_val, CypherBool) and left_val.value:
+                return CypherBool(True)
+            if isinstance(right_val, CypherBool) and right_val.value:
+                return CypherBool(True)
+            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
+                return CypherNull()
+            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
+                return CypherBool(False)
+            raise TypeError("OR requires boolean operands")

As per coding guidelines: "Executor methods MUST handle NULL semantics correctly using ternary logic for AND/OR/NOT operators. Never assume values are non-NULL without explicit checks."

🤖 Fix all issues with AI agents
In `@src/graphforge/executor/evaluator.py`:
- Around line 489-493: The loop that executes subquery operators always calls
executor._execute_operator with op_index=0, causing incorrect finalization for
Aggregate/With logic; change the loop over operators (variable operators) to
pass the correct current operator index and the total count into
_execute_operator (use the loop index i and len(operators)) so each call is
executor._execute_operator(op, subquery_rows, i, len(operators)) ensuring
finalization behavior is triggered only for the final operator; update the block
that sets subquery_rows and iterates over op to use the index-aware call.
- Around line 435-475: The quantifier handling collapses NULL to false; update
the loop in the QuantifierExpression branch (where evaluate_expression,
CypherList and CypherBool are used) to implement three-valued logic: while
iterating list_val.value track satisfied_count (when predicate evaluates to
CypherBool True) and any_null (when predicate evaluates to a CypherNull or
non-CypherBool representing unknown), and then replace the current return logic
with the openCypher semantics: ALL -> return False if any False observed, True
if list empty or all True, else NULL if no False but at least one NULL; ANY ->
True if any True, else NULL if any NULL, else False; NONE -> False if any True,
else NULL if any NULL, else True; SINGLE -> True if exactly one True and no
NULLs, False if more than one True, NULL if zero True and any NULL, else False.
Ensure you reference QuantifierExpression, evaluate_expression, CypherBool and
CypherNull when implementing these checks.
- Around line 482-487: The code calls executor.planner.plan(expr.query) without
guarding that planner is present; add a null-check after the existing executor
is None check to verify executor.planner is not None and if it is raise a
TypeError with a clear message (e.g., "Subquery expressions require executor
with planner configured") so the subquery planning step in evaluator where
executor.planner.plan and ExecutionContext handling for expr.query uses an
executor with a configured planner.

In `@src/graphforge/executor/executor.py`:
- Around line 1507-1516: The loop currently drops input rows when op.src_var is
missing or not a NodeRef; for OPTIONAL MATCH semantics you must instead preserve
the row and produce a result where the expansion bindings are set to None.
Change the branches that check "if op.src_var not in ctx.bindings" and "if not
isinstance(src_node, NodeRef)" to not continue but to create/emit a context
preserving the original bindings and explicitly binding the expansion target
variables (the variables produced by this operator, e.g., the op's
destination/edge/node vars) to None; use ctx.get/op.src_var and the same
context-copying mechanism already used elsewhere to attach None values so
non-optional behavior remains unchanged. Ensure this only applies when the
operator is an OPTIONAL expansion (the operator flag/name that indicates
OPTIONAL MATCH) and maintain current behavior for non-OPTIONAL expansions.

In `@src/graphforge/planner/planner.py`:
- Around line 462-474: The current planner treats a single-node OPTIONAL MATCH
as a regular ScanNodes which drops rows when no node exists; update the
len(pattern)==1 NodePattern branch to preserve rows for OPTIONAL semantics by
using an OptionalScanNodes operator (or wrapping ScanNodes in an
optional/left-outer style operator) instead of plain ScanNodes when the pattern
is OPTIONAL. Locate the block that constructs ScanNodes (references: ScanNodes,
node_pattern, variable, labels) and change it to emit
OptionalScanNodes(variable=node_pattern.variable, labels=... ) or wrap the
ScanNodes with an optional wrapper so the planner returns one row with NULL
bindings when no match is found, while keeping labels and variable names
identical. Ensure the new operator matches openCypher semantics for NULL
bindings.
🧹 Nitpick comments (2)
tests/integration/datasets/test_end_to_end_workflows.py (2)

191-205: test_exists_subquery_pattern downloads a full dataset — consider using a local graph instead.

This test loads snap-ca-grqc (5,242 nodes) just to verify EXISTS { MATCH ... } returns a non-zero count. The other non-skipped tests in this class (test_quantifier_pattern) create small local graphs for their assertions, which is faster and avoids network dependency. Consider doing the same here for a more focused and reliable test.

♻️ Suggested refactor — use an in-memory graph
     def test_exists_subquery_pattern(self):
         """Use EXISTS to check for pattern existence."""
         gf = GraphForge()
-        load_dataset(gf, "snap-ca-grqc")
+
+        # Create a small graph locally
+        gf.execute("""
+            CREATE (a:Person {name: 'Alice'}),
+                   (b:Person {name: 'Bob'}),
+                   (c:Person {name: 'Carol'}),
+                   (a)-[:KNOWS]->(b)
+        """)
 
         # Find nodes that have outgoing connections
         results = gf.execute("""
             MATCH (n)
             WHERE EXISTS { MATCH (n)-[]->(m) }
-            RETURN count(*) AS nodes_with_outgoing
+            RETURN n.name AS name
         """)
 
-        assert len(results) == 1
-        count = results[0]["nodes_with_outgoing"].value
-        assert count > 0
+        assert len(results) == 1
+        assert results[0]["name"].value == "Alice"

60-74: Tests that download snap-ca-grqc are slow and network-dependent — consider isolating them.

test_load_collaboration_network (and the skipped tests below it) download a real dataset. If the network is unavailable or slow, the entire test suite stalls. At minimum, these should carry @pytest.mark.slow (per the duplicate marker comment) so CI can opt out.

Line 74: minor style nit — a chained comparison reads more naturally in Python:

-        assert edge_count > 28000 and edge_count < 30000  # ~2x edges for undirected
+        assert 28000 < edge_count < 30000  # ~2x edges for undirected

Comment thread src/graphforge/executor/evaluator.py
Comment thread src/graphforge/executor/evaluator.py
Comment thread src/graphforge/executor/evaluator.py
Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/planner/planner.py
**Executor Fixes:**
- Fix _execute_union to pass correct op_index for each branch operator
  instead of hardcoded 0, fixing aggregation finalization logic
- Fix _execute_variable_expand cycle detection by properly tracking
  visited nodes per path (initialize with src_node.id, add next_node.id
  when pushing to stack)

**Parser Fixes:**
- Replace union_query dict return with proper UnionQuery AST node
- Add validation to reject mixed UNION/UNION ALL in same query
- Update api.py to handle UnionQuery instead of dict
- Fix list_comprehension disambiguation by adding named sub-rules
  (comp_where_clause, comp_map_clause) to grammar
- Add transformers that return tagged tuples to distinguish WHERE vs MAP

**Test Improvements:**
- Add @pytest.mark.integration to all integration test classes
- Add @pytest.mark.slow to dataset tests loading snap-ca-grqc
- Fix test_quantifier_pattern assertion to validate both Alice and Carol
  are present (not just one) and Bob is excluded
- Rename test_union_different_column_count to test_union_different_value_types
  to accurately describe what it tests
- Add test_union_mismatched_column_count to document current behavior
  with different column counts

All tests passing, no type errors, linting clean.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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: 5

🤖 Fix all issues with AI agents
In `@src/graphforge/ast/query.py`:
- Around line 31-47: Replace the dataclass UnionQuery with a Pydantic v2
BaseModel (frozen=True) matching the project's AST pattern: declare branches:
list[CypherQuery] and all: bool using Field(...) with descriptive strings, add a
`@field_validator`("branches") to enforce type/contents if needed, and add a
`@model_validator`(mode="after") to enforce cross-field invariants (at least two
branches and any other consistency rules). Ensure the new UnionQuery class name
and attributes (branches, all) are used exactly as in the diff so references
elsewhere continue to work.

In `@src/graphforge/executor/executor.py`:
- Around line 296-363: _in _execute_variable_expand_, the code currently binds
raw EdgeRef objects into CypherList which violates the invariant that executor
outputs must be CypherValue instances; replace the cast that creates
CypherList(cast(list[CypherValue], edge_path)) with a list of proper CypherValue
wrappers for edges (e.g., instantiate an EdgeValue wrapper type that implements
CypherValue methods like equals/to_python) and bind CypherList over that
wrapped-list (or alternatively bind a plain Python list only if project accepts
non-CypherList edge lists, but preferred fix is to add EdgeValue and wrap
edges). Ensure the wrapper (EdgeValue) implements the CypherValue interface and
use it when building the list for op.edge_var in _execute_variable_expand so
downstream code can call CypherValue methods safely.

In `@src/graphforge/parser/cypher.lark`:
- Around line 32-33: The read_query rule is ambiguous because it contains two
adjacent optional where_clause? that collapse when optional_match_clause*
matches zero items; update read_query so the second where_clause? is only
reachable when there is at least one optional_match_clause (e.g., attach the
optional where_clause to optional_match_clause in the repetition or require
optional_match_clause+ before the second where_clause), ensuring each
optional_match_clause can carry its own where_clause and eliminating the
adjacent optional where_clause? positions; adjust references to read_query and
optional_match_clause/where_clause accordingly.
- Line 221: The parser's FUNCTION_NAME token currently doesn't include "size",
so calls like size(...) fail to parse; update the FUNCTION_NAME regex (the token
named FUNCTION_NAME) to include "size" in the alternation (e.g., add |size
inside the existing /.../i pattern) while keeping the case-insensitive flag
intact so size() is recognized the same as other functions.

In `@tests/integration/test_union.py`:
- Around line 1-8: Add the pytest integration marker to the test classes in this
file: import pytest if missing and annotate each test class (e.g.,
TestUnionBasic and the other test classes in this file) with
`@pytest.mark.integration` placed immediately above the class definition so they
comply with the project's test-marker guidelines.
🧹 Nitpick comments (2)
src/graphforge/parser/parser.py (1)

681-728: Consider extracting a shared helper for the four identical quantifier transformers.

All four methods (all_quantifier, any_quantifier, none_quantifier, single_quantifier) are identical except for the quantifier string. A small helper reduces duplication.

♻️ Proposed refactor
+    def _build_quantifier(self, quantifier_type: str, items):
+        """Build a QuantifierExpression from parsed items."""
+        from graphforge.ast.expression import QuantifierExpression
+
+        variable = items[0].name if hasattr(items[0], "name") else str(items[0])
+        return QuantifierExpression(
+            quantifier=quantifier_type, variable=variable, list_expr=items[1], predicate=items[2]
+        )
+
     def all_quantifier(self, items):
-        """Transform ALL quantifier expression.
-
-        Syntax: ALL(x IN list WHERE predicate)
-        """
-        from graphforge.ast.expression import QuantifierExpression
-
-        # items[0] = variable, items[1] = list_expr, items[2] = predicate
-        variable = items[0].name if hasattr(items[0], "name") else str(items[0])
-        return QuantifierExpression(
-            quantifier="ALL", variable=variable, list_expr=items[1], predicate=items[2]
-        )
+        """Transform ALL quantifier expression."""
+        return self._build_quantifier("ALL", items)

     def any_quantifier(self, items):
-        ...
+        """Transform ANY quantifier expression."""
+        return self._build_quantifier("ANY", items)

     def none_quantifier(self, items):
-        ...
+        """Transform NONE quantifier expression."""
+        return self._build_quantifier("NONE", items)

     def single_quantifier(self, items):
-        ...
+        """Transform SINGLE quantifier expression."""
+        return self._build_quantifier("SINGLE", items)
src/graphforge/executor/executor.py (1)

138-141: Redundant import: ExpandVariableLength is already imported at line 17.

The local from graphforge.planner.operators import ExpandVariableLength on line 138 shadows the top-level import.

Proposed fix
-        from graphforge.planner.operators import ExpandVariableLength
-
         if isinstance(op, ExpandVariableLength):
             return self._execute_variable_expand(op, input_rows)

Comment thread src/graphforge/ast/query.py Outdated
Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/parser/cypher.lark Outdated
Comment thread src/graphforge/parser/cypher.lark Outdated
Comment thread tests/integration/test_union.py
DecisionNerd and others added 3 commits February 8, 2026 17:45
**Evaluator Fixes:**
- Fix subquery operator execution to pass correct op_index (use enumerate)
- Implement proper three-valued logic for quantifiers (ALL/ANY/NONE/SINGLE)
  tracking satisfied_count, false_count, and null_count for openCypher semantics
- Add null-check for executor.planner before calling plan() in subqueries
- ALL: False if any False, True if empty or all True, else NULL
- ANY: True if any True, NULL if any NULL but no True, else False
- NONE: False if any True, NULL if any NULL but no True, else True
- SINGLE: True if exactly one True and no NULLs, else False/NULL

**Executor Fixes:**
- Fix _execute_optional_expand to preserve rows with NULL bindings when
  src_var is missing or not a NodeRef (proper OPTIONAL semantics)
- Fix _execute_variable_expand to bind raw list of EdgeRef objects instead
  of wrapping in CypherList (EdgeRef is not a CypherValue)
- Add OptionalScanNodes operator for single-node OPTIONAL MATCH with
  LEFT JOIN semantics preserving rows with NULL bindings
- Implement _execute_optional_scan method with proper NULL handling

**Parser/Grammar Fixes:**
- Fix read_query ambiguity by splitting into three alternatives:
  match + optional_match+ + where?, match + where?, optional_match + where?
- Add 'size' to FUNCTION_NAME token regex for size() function support

**AST Improvements:**
- Replace UnionQuery dataclass with Pydantic BaseModel (frozen=True)
- Add field validators for branches (non-empty, all CypherQuery)
- Add model validator to ensure at least two branches in UNION

**Planner Improvements:**
- Update single-node OPTIONAL MATCH to use OptionalScanNodes instead of
  ScanNodes for proper NULL preservation
- Add OptionalScanNodes to operators.py with Pydantic validation

**Test Improvements:**
- Add @pytest.mark.integration to all test classes in test_union.py

All tests passing, linting clean, type checking passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**size() Function Implementation:**
- Added size() function for lists and strings in evaluator.py
- Returns length of CypherList or CypherString
- Raises TypeError for invalid argument types
- Works in WHERE clauses, RETURN, and property access

**Coverage Improvements:**
- Added tests for subquery planner null-check error paths
- Added integration tests for size() function (3 tests)
- Tests cover node property lists, WHERE clause filtering, string properties

All new tests passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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 (3)
src/graphforge/executor/evaluator.py (1)

111-148: ⚠️ Potential issue | 🟠 Major

List/dict literal evaluation skips QuantifierExpression (and UnaryOp), so valid expressions fall through to from_python.
That will raise at runtime for list/map literals containing these expressions.

✅ Proposed fix
-                        CaseExpression,
-                        ListComprehension,
-                        SubqueryExpression,
+                        CaseExpression,
+                        ListComprehension,
+                        QuantifierExpression,
+                        UnaryOp,
+                        SubqueryExpression,
-                        CaseExpression,
-                        ListComprehension,
-                        SubqueryExpression,
+                        CaseExpression,
+                        ListComprehension,
+                        QuantifierExpression,
+                        UnaryOp,
+                        SubqueryExpression,
Based on learnings: “Applies to src/graphforge/executor/evaluator.py : All functions in evaluator.py MUST be properly registered and handle all required CypherValue input types with appropriate NULL handling”.
src/graphforge/planner/planner.py (2)

462-541: ⚠️ Potential issue | 🟠 Major

Add variable-length pattern handling to _plan_optional_match.

_plan_optional_match does not check for variable-length patterns (e.g., *min..max hops) and always emits OptionalExpandEdges, silently dropping the min/max constraints. This should mirror the handling in _plan_match, which checks rel_pattern.min_hops is not None or rel_pattern.max_hops is not None and emits ExpandVariableLength when present. Since the project must match openCypher semantics exactly, OPTIONAL MATCH with variable-length relationships must preserve the hop bounds or emit appropriate operators (either create OptionalExpandVariableLength or wrap the variable-length expansion with optional semantics).


95-164: ⚠️ Potential issue | 🟠 Major

WHERE clause must be applied in clause-order, not after CREATE/MERGE.

The grammar allows MATCH...WHERE...CREATE, and the test test_match_with_where_filters_creates expects WHERE to filter rows before CREATE executes. Currently, WHERE is collected separately and applied after CREATE/MERGE at step 5, which violates openCypher semantics: CREATE would process all MATCH results, then WHERE filters them afterward (wrong—should filter before creating).

Process WHERE immediately when encountered in the clause loop instead of batching it:

Proposed fix
-        where_clause = None
...
-            elif isinstance(clause, WhereClause):
-                where_clause = clause
+            elif isinstance(clause, WhereClause):
+                operators.append(Filter(predicate=clause.predicate))
...
-        # 5. WHERE
-        if where_clause:
-            operators.append(Filter(predicate=where_clause.predicate))
🤖 Fix all issues with AI agents
In `@src/graphforge/planner/operators.py`:
- Around line 107-155: The max_hops validator currently rejects 0 and there's no
cross-field check that max_hops >= min_hops; update the validate_max_hops method
on ExpandVariableLength to allow 0 (accept v is None or v >= 0) and add a
model-level validator (e.g., `@model_validator`(mode="after") def
validate_hop_range(cls, values): ...) that checks when max_hops is not None that
max_hops >= min_hops and raises ValueError otherwise; reference the
ExpandVariableLength class, validate_max_hops method, and add a new
validate_hop_range model validator to enforce the cross-field constraint.

Comment thread src/graphforge/executor/executor.py
Comment thread src/graphforge/planner/operators.py
Copy link
Copy Markdown
Contributor

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/graphforge/executor/evaluator.py (1)

364-381: ⚠️ Potential issue | 🟠 Major

AND/OR operators use two-valued NULL propagation instead of openCypher three-valued logic.

This is pre-existing code, but it violates openCypher semantics that the new quantifier/subquery features rely on:

  • FALSE AND NULL → should return FALSE (not NULL)
  • TRUE OR NULL → should return TRUE (not NULL)

The current implementation eagerly returns CypherNull() whenever either operand is NULL, which is incorrect.

Proposed fix
         if expr.op == "AND":
-            # Handle NULL propagation
-            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
-                return CypherNull()
-            # Both must be booleans
-            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
-                return CypherBool(left_val.value and right_val.value)
-            raise TypeError("AND requires boolean operands")
+            # Three-valued logic: FALSE AND anything = FALSE
+            if isinstance(left_val, CypherBool) and not left_val.value:
+                return CypherBool(False)
+            if isinstance(right_val, CypherBool) and not right_val.value:
+                return CypherBool(False)
+            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
+                return CypherNull()
+            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
+                return CypherBool(left_val.value and right_val.value)
+            raise TypeError("AND requires boolean operands")

         if expr.op == "OR":
-            # Handle NULL propagation
-            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
-                return CypherNull()
-            # Both must be booleans
-            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
-                return CypherBool(left_val.value or right_val.value)
-            raise TypeError("OR requires boolean operands")
+            # Three-valued logic: TRUE OR anything = TRUE
+            if isinstance(left_val, CypherBool) and left_val.value:
+                return CypherBool(True)
+            if isinstance(right_val, CypherBool) and right_val.value:
+                return CypherBool(True)
+            if isinstance(left_val, CypherNull) or isinstance(right_val, CypherNull):
+                return CypherNull()
+            if isinstance(left_val, CypherBool) and isinstance(right_val, CypherBool):
+                return CypherBool(left_val.value or right_val.value)
+            raise TypeError("OR requires boolean operands")

As per coding guidelines: "Executor methods MUST handle NULL semantics correctly using ternary logic for AND/OR/NOT operators. Never assume values are non-NULL without explicit checks." Based on learnings: "Project prioritizes correctness over performance and must match openCypher semantics exactly."

🤖 Fix all issues with AI agents
In `@src/graphforge/executor/evaluator.py`:
- Around line 401-432: The error message in the ListComprehension handler is
misleading: when list_val is not a CypherList the raised TypeError says "IN
requires a list"; update the message to reflect the current construct by
changing the TypeError in the ListComprehension branch (inside the
isinstance(expr, ListComprehension) block where evaluate_expression returns
list_val) to something like "List comprehension requires a list" so that the
TypeError raised by evaluate_expression/ListComprehension uses the correct
descriptive text.
- Around line 499-511: In the SINGLE quantifier handling in evaluator.py (the
branch where expr.quantifier == "SINGLE"), the current logic falls through to
returning CypherBool(False) when satisfied_count == 1 and null_count > 0, but
per openCypher it must return CypherNull; update the conditions in that block
(referencing satisfied_count, null_count and the SINGLE branch) to explicitly
return CypherNull() when satisfied_count == 1 and null_count > 0 (e.g., add a
branch for that case or adjust the ordering of checks) while preserving the
existing returns for satisfied_count == 1 && null_count == 0, satisfied_count >
1, and satisfied_count == 0 && null_count > 0.

In `@tests/unit/executor/test_subquery_planner_check.py`:
- Around line 10-11: Add the missing pytest unit marker by decorating the
TestSubqueryPlannerCheck class with `@pytest.mark.unit`: import pytest if not
already present, then add the decorator directly above the class definition
(class TestSubqueryPlannerCheck) so the test class is consistently marked as a
unit test like other tests in tests/unit/.
🧹 Nitpick comments (4)
tests/integration/test_size_function.py (2)

12-14: Consider extracting a shared GraphForge fixture to reduce boilerplate.

A gf fixture (similar to the one in tests/tck/test_tck_compliance.py) would DRY up the repeated gf = GraphForge() in every test method and make future test additions more concise.

♻️ Suggested fixture
+@pytest.fixture
+def gf():
+    """Create a fresh GraphForge instance for each test."""
+    return GraphForge()
+
+
 `@pytest.mark.integration`
 class TestSizeFunction:
     """Test size() function on lists and strings."""
 
-    def test_size_of_node_property_list(self):
+    def test_size_of_node_property_list(self, gf):
         """Test size() on node property that is a list."""
-        gf = GraphForge()
 
         gf.execute("CREATE (:Person {name: 'Alice', scores: [90, 85, 95]})")

(Apply the same pattern to the other two test methods.)

Also applies to: 26-28, 43-45


12-55: Consider adding edge-case tests for size() (empty list, empty string, null).

The current tests cover the happy path. For robust coverage of openCypher semantics, tests for size([]), size(''), and size() on a missing/null property (which should return null) would be valuable. This is non-blocking.

src/graphforge/executor/evaluator.py (2)

111-128: QuantifierExpression and UnaryOp missing from the isinstance check for nested literal items.

If a Literal's value list contains a QuantifierExpression or UnaryOp node, it will be passed to from_python() instead of evaluate_expression(), likely failing at runtime. The same issue exists for the dict branch (lines 136-145).

This may not happen with the current parser, but for defensive consistency with the evaluate_expression dispatch table it's worth including them.

Proposed fix
             evaluate_expression(item, ctx, executor)
                 if isinstance(
                     item,
                     (
                         Literal,
                         Variable,
                         PropertyAccess,
                         BinaryOp,
                         FunctionCall,
                         CaseExpression,
                         ListComprehension,
                         SubqueryExpression,
+                        QuantifierExpression,
+                        UnaryOp,
                     ),
                 )

476-476: Remove unused noqa directive.

Ruff reports # noqa: PLR1714 is unused on this line.

Proposed fix
-            elif list_length == 0 or (satisfied_count == list_length):  # noqa: PLR1714
+            elif list_length == 0 or (satisfied_count == list_length):

Comment thread src/graphforge/executor/evaluator.py
Comment thread src/graphforge/executor/evaluator.py
Comment thread tests/unit/executor/test_subquery_planner_check.py
- Add QuantifierExpression and UnaryOp to list/dict literal evaluation
- Update max_hops validator to allow 0 (non-negative instead of positive)
- Add model validator to ensure max_hops >= min_hops cross-field validation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@DecisionNerd DecisionNerd merged commit e189c28 into main Feb 9, 2026
21 of 22 checks passed
@DecisionNerd DecisionNerd deleted the feature/103-v0.3.0-tck-coverage branch February 9, 2026 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add zip compression support for dataset loaders

1 participant