Skip to content

Conversation

@hemanta212
Copy link
Contributor

Fix: Conservative nullability for Cypher expressions

Follow-up to PR #2 (Nullable return types). That PR added Required metadata from schema to codegen, allowing nullable fields to generate pointer types. However, it missed cases where nullability comes from query structure, not schema.

Problem

Two issues with the original approach:

  1. OPTIONAL MATCH bindings - properties can be null regardless of schema:
OPTIONAL MATCH (author:User)-[:WROTE]->(c)
RETURN author.name  -- null if no author exists, even if schema says required
  1. Complex expressions - null propagates through functions/arithmetic:
RETURN toUpper(u.name), u.age + 1, avg(u.age)  -- all can be null

Both generated non-pointer types, causing compile errors when mocks returned nil.

Solution

Conservative default: only trust schema Required for simple var.prop on non-optional bindings. Everything else bails to nullable.

// Before: default required, lookup schema
required := true

// After: default nullable, only trust simple patterns
required := false
if field, binding := lookupFieldFromExpression(expr, ctx); field != nil {
    if binding.optional { required = false }
    else { required = field.Required }
}

Result

Pattern Type
u.name on MATCH schema Required
u.name on OPTIONAL MATCH *string
toUpper(u.name) *string
count(u), avg(u.age) *int, *float64
  • dialects/cypher/analyzer.go: track optional bindings, flip default
  • 12 focused test cases (5 existing schema + 7 new bailout patterns)
  • repro example: 11/11 scopes passing

Design Decision

Chose conservative default over full null propagation tracking. Modeling Cypher's null semantics through arbitrary expressions (functions, operators, CASE) would require version-specific rules for dozens of constructs with limited benefit.

Being "over-nullable" (extra pointers) is safer than "under-nullable" (compile errors) for test scaffolding.

Future Work

Gradual whitelist for known non-null constructs:

  • count(*) → always returns int, never null
  • Literals (1, "foo", true) → non-null by definition
  • IS NULL / IS NOT NULL → always returns bool
  • coalesce(expr, <literal>) → non-null if fallback is non-null

When a variable is bound via OPTIONAL MATCH, its properties can be null
even if the schema marks them as required. This change tracks whether
each binding came from OPTIONAL MATCH and uses that to force
Required=false for return fields from optional bindings.

Changes:
- Add 'optional' bool field to variableBinding struct
- Pass optional flag through extractBindings* functions
- Update lookupFieldFromExpression to return binding for optional check
- Force Required=false when binding.optional is true
Complex expressions (functions, arithmetic, CASE, aggregates, etc.)
now default to nullable (Required: false) since we can't statically
determine if they might return null.

Only simple 'variable.property' patterns on non-optional bindings will
use the schema's Required field.

Changes:
- Flip default from required=true to required=false in extractProjectionItem
- 7 new test cases for bailout patterns (function, CASE, aggregate, list index, chained access)
- Update existing tests to expect conservative behavior
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