From 05843b45c8b5f1dcce1deb57f39cc89854cf8273 Mon Sep 17 00:00:00 2001 From: bellorr <9314085+bellorr@users.noreply.github.com> Date: Sat, 28 Feb 2026 11:32:04 -0500 Subject: [PATCH 1/2] fix: exclude schema commands from async create node batch handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/cypher/executor.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/cypher/executor.go b/pkg/cypher/executor.go index 1f6874c..65fd094 100644 --- a/pkg/cypher/executor.go +++ b/pkg/cypher/executor.go @@ -790,10 +790,15 @@ func (e *StorageExecutor) tryAsyncCreateNodeBatch(ctx context.Context, cypher st if !strings.HasPrefix(upper, "CREATE") { return nil, nil, false } - // System commands (CREATE DATABASE / COMPOSITE DATABASE / ALIAS) must not be handled here + // System commands and schema commands must not be handled here — route to executeSchemaCommand instead if findMultiWordKeywordIndex(cypher, "CREATE", "DATABASE") == 0 || findMultiWordKeywordIndex(cypher, "CREATE", "COMPOSITE DATABASE") == 0 || - findMultiWordKeywordIndex(cypher, "CREATE", "ALIAS") == 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 } for _, keyword := range []string{ From ed4e2c3809f1f9224c7b33ac3d48826a29213b96 Mon Sep 17 00:00:00 2001 From: bellorr <9314085+bellorr@users.noreply.github.com> Date: Sat, 28 Feb 2026 11:49:06 -0500 Subject: [PATCH 2/2] test(cypher): add regression test for schema commands with AsyncEngine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/cypher/bug_schema_async_routing_test.go | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 pkg/cypher/bug_schema_async_routing_test.go diff --git a/pkg/cypher/bug_schema_async_routing_test.go b/pkg/cypher/bug_schema_async_routing_test.go new file mode 100644 index 0000000..d0e94c8 --- /dev/null +++ b/pkg/cypher/bug_schema_async_routing_test.go @@ -0,0 +1,126 @@ +package cypher + +import ( + "context" + "os" + "testing" + + "github.com/orneryd/nornicdb/pkg/storage" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestBug_SchemaCommandsWithAsyncEngine reproduces the bug where CREATE CONSTRAINT +// and CREATE INDEX queries fail with "invalid label name: Node)" when an AsyncEngine +// is active (i.e. production deployments). +// +// Root cause: tryAsyncCreateNodeBatch intercepts all queries starting with CREATE +// before the schema handler runs. Without an async engine (unit tests / MemoryEngine) +// the early-return at "if engines.asyncEngine != nil" skips tryAsyncCreateNodeBatch, +// masking the bug. In production the async engine is always active, causing schema +// commands to be parsed as node CREATE patterns. +// +// Reported via: Mimir (https://github.com/orneryd/Mimir) integration — schema +// initialization failed on every startup when NornicDB was backed by an AsyncEngine. +// +// Environment: macOS 15 (Apple Silicon M1), NornicDB arm64-metal-bge-heimdall image, +// Mimir MCP server v4.1, Docker with Colima (aarch64, VZ). +func TestBug_SchemaCommandsWithAsyncEngine(t *testing.T) { + // Setup full production-equivalent stack: BadgerEngine -> WALEngine -> AsyncEngine. + // This is the critical difference from MemoryEngine-based tests — the AsyncEngine + // being present is what triggers tryAsyncCreateNodeBatch and exposes the bug. + tmpDir, err := os.MkdirTemp("", "schema_async_routing_test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + badger, err := storage.NewBadgerEngine(tmpDir) + require.NoError(t, err) + defer badger.Close() + + wal, err := storage.NewWAL(tmpDir+"/wal", nil) + require.NoError(t, err) + defer wal.Close() + + walEngine := storage.NewWALEngine(badger, wal) + asyncEngine := storage.NewAsyncEngine(walEngine, nil) + defer asyncEngine.Close() + + store := storage.NewNamespacedEngine(asyncEngine, "test") + exec := NewStorageExecutor(store) + ctx := context.Background() + + // These are the exact schema queries Mimir v4.1 sends during GraphManager + // initialization (pkg/managers/GraphManager.ts). Before the fix each query + // returned: invalid label name: "Node)" (must be alphanumeric starting with + // letter or underscore) because tryAsyncCreateNodeBatch parsed FOR (n:Node) + // as a node CREATE pattern and captured "Node)" as the label. + tests := []struct { + name string + query string + }{ + { + name: "CREATE CONSTRAINT node_id_unique", + query: ` + CREATE CONSTRAINT node_id_unique IF NOT EXISTS + FOR (n:Node) REQUIRE n.id IS UNIQUE + `, + }, + { + name: "CREATE FULLTEXT INDEX node_search", + query: ` + CREATE FULLTEXT INDEX node_search IF NOT EXISTS + FOR (n:Node) ON EACH [n.properties] + `, + }, + { + name: "CREATE INDEX node_type", + query: ` + CREATE INDEX node_type IF NOT EXISTS + FOR (n:Node) ON (n.type) + `, + }, + { + name: "CREATE CONSTRAINT watch_config_id_unique", + query: ` + CREATE CONSTRAINT watch_config_id_unique IF NOT EXISTS + FOR (w:WatchConfig) REQUIRE w.id IS UNIQUE + `, + }, + { + name: "CREATE INDEX watch_config_path", + query: ` + CREATE INDEX watch_config_path IF NOT EXISTS + FOR (w:WatchConfig) ON (w.path) + `, + }, + { + name: "CREATE INDEX file_path", + query: ` + CREATE INDEX file_path IF NOT EXISTS + FOR (f:File) ON (f.path) + `, + }, + { + name: "CREATE FULLTEXT INDEX file_metadata_search", + query: ` + CREATE FULLTEXT INDEX file_metadata_search IF NOT EXISTS + FOR (f:File) ON EACH [f.path, f.name, f.language] + `, + }, + { + name: "CREATE VECTOR INDEX node_embedding_index", + query: ` + CREATE VECTOR INDEX node_embedding_index IF NOT EXISTS + FOR (n:Node) ON (n.embedding) + OPTIONS {indexConfig: {vector.dimensions: 1024}} + `, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := exec.Execute(ctx, tt.query, nil) + assert.NoError(t, err, "schema command should succeed with async engine active") + }) + } +}