Skip to content

fix(cypher): exclude schema commands from async create node batch handler#25

Merged
orneryd merged 2 commits intoorneryd:mainfrom
bellorr:fix/async-create-schema-routing
Feb 28, 2026
Merged

fix(cypher): exclude schema commands from async create node batch handler#25
orneryd merged 2 commits intoorneryd:mainfrom
bellorr:fix/async-create-schema-routing

Conversation

@bellorr
Copy link
Contributor

@bellorr bellorr commented Feb 28, 2026

Summary

Fixes a bug where CREATE CONSTRAINT and CREATE INDEX queries fail with:

invalid label name: "Node)" (must be alphanumeric starting with letter or underscore)

when NornicDB is running with an AsyncEngine active (i.e. all production deployments).

Root Cause

tryAsyncCreateNodeBatch intercepts all queries that begin with CREATE to batch node writes through the async engine. This is correct for CREATE (n:Label) data mutations, but it also incorrectly intercepts schema DDL commands (CREATE CONSTRAINT, CREATE INDEX, CREATE FULLTEXT INDEX, CREATE VECTOR INDEX, CREATE RANGE INDEX) before the schema handler runs.

The parser then attempts to extract a label from the schema syntax — e.g. from FOR (n:Node) REQUIRE n.id IS UNIQUE — and captures Node) (including the closing paren) as the label name, producing the invalid label error.

Why tests didn't catch this: The existing test suite uses MemoryEngine. When no AsyncEngine is present, tryAsyncCreateNodeBatch returns early at the if engines.asyncEngine != nil guard — so the bug is completely masked in CI, but triggers on every production startup.

Fix

Added CONSTRAINT, INDEX, FULLTEXT, VECTOR, and RANGE to the early-return exclusion list inside tryAsyncCreateNodeBatch, alongside the already-excluded DATABASE, COMPOSITE DATABASE, and ALIAS keywords:

if findMultiWordKeywordIndex(cypher, "CREATE", "DATABASE") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "COMPOSITE DATABASE") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "ALIAS") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "CONSTRAINT") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "INDEX") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "FULLTEXT") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "VECTOR") == 0 ||
    findMultiWordKeywordIndex(cypher, "CREATE", "RANGE") == 0 {
    return nil, nil, false
}

These keywords cause the function to return (nil, nil, false), allowing the query to fall through to executeSchemaCommand where it is correctly parsed and executed.

Regression Test

Added pkg/cypher/bug_schema_async_routing_test.go which exercises the full production-equivalent storage stack (BadgerEngine → WALEngine → AsyncEngine) — the exact condition that exposes the bug and that MemoryEngine-based tests cannot catch.

The test covers all eight schema queries issued by Mimir v4.1 during GraphManager initialisation:

  • CREATE CONSTRAINT node_id_unique IF NOT EXISTS FOR (n:Node) REQUIRE n.id IS UNIQUE
  • CREATE FULLTEXT INDEX node_search IF NOT EXISTS FOR (n:Node) ON EACH [n.properties]
  • CREATE INDEX node_type IF NOT EXISTS FOR (n:Node) ON (n.type)
  • CREATE CONSTRAINT watch_config_id_unique IF NOT EXISTS FOR (w:WatchConfig) REQUIRE w.id IS UNIQUE
  • CREATE INDEX watch_config_path IF NOT EXISTS FOR (w:WatchConfig) ON (w.path)
  • CREATE INDEX file_path IF NOT EXISTS FOR (f:File) ON (f.path)
  • CREATE FULLTEXT INDEX file_metadata_search IF NOT EXISTS FOR (f:File) ON EACH [f.path, f.name, f.language]
  • CREATE VECTOR INDEX node_embedding_index IF NOT EXISTS FOR (n:Node) ON (n.embedding) OPTIONS {indexConfig: {vector.dimensions: 1024}}

All 8 pass after the fix; all 8 fail (with "invalid label name") on the unfixed code.

Reproduction Context

Discovered while integrating NornicDB as the backing store for Mimir (https://github.com/orneryd/Mimir), which is maintained by the same org. On every Mimir startup, GraphManager sends these schema commands and every one fails — making the database unusable in production.

Environment:

  • macOS 15.3 (Apple Silicon M1 Pro)
  • NornicDB arm64-metal-bge-heimdall image, Docker with Colima (aarch64, VZ)
  • Mimir MCP server v4.1
  • Go 1.23 (darwin/arm64)

Checklist

  • Regression test written (TestBug_SchemaCommandsWithAsyncEngine)
  • Test verified failing on unfixed code, passing after fix
  • go fmt applied to all changed files
  • No new allocations in the hot path — only adds short-circuit keyword checks
  • No security concerns — the exclusion list only widens the set of queries routed to the existing, already-validated schema handler

bellorr and others added 2 commits February 28, 2026 11:34
CREATE CONSTRAINT and CREATE INDEX queries were being intercepted by
tryAsyncCreateNodeBatch before reaching the schema handler. The async
batch handler has no async engine in unit tests (so tests pass), but
in production the async engine is active, causing schema queries to be
parsed as node CREATE patterns — resulting in "invalid label name: Node)"
errors during Mimir schema initialization.

Fix: add CONSTRAINT, INDEX, FULLTEXT, VECTOR, and RANGE to the early-
return exclusion list alongside the existing DATABASE/ALIAS exclusions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reproduces the bug where CREATE CONSTRAINT / CREATE INDEX queries fail
when an AsyncEngine is active (production deployments) but pass in
MemoryEngine-based unit tests (masking the bug in CI).

Uses the full production-equivalent stack (BadgerEngine → WALEngine →
AsyncEngine) to exercise the tryAsyncCreateNodeBatch routing path.
Covers all eight schema queries issued by Mimir v4.1 during GraphManager
initialisation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Owner

@orneryd orneryd left a comment

Choose a reason for hiding this comment

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

looks good!

@orneryd orneryd merged commit b6e58f5 into orneryd:main Feb 28, 2026
2 of 11 checks passed
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.

2 participants