Skip to content

feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783)#786

Merged
malibio merged 3 commits into
mainfrom
feature/issue-783-universal-graph-stream-b
Jan 6, 2026
Merged

feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783)#786
malibio merged 3 commits into
mainfrom
feature/issue-783-universal-graph-stream-b

Conversation

@malibio

@malibio malibio commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements Stream B of Issue #783: Universal Graph Architecture Transition. This eliminates all spoke tables (task, schema) and stores all node properties in the node.properties JSON field.

Key changes:

  • Removed all spoke table queries from surreal_store.rs (~1,000 lines removed)
  • Simplified get_subtree_with_edges to single query batch against node table
  • Updated SchemaTableManager to only generate DDL for relationship edge tables
  • Removed spoke table seeding and DDL generation from NodeService
  • Fixed camelCase property naming (schemaVersion instead of version)

Net result: -1,792 lines of code (2,535 deletions, 743 additions)

Changes by File

File Changes
surreal_store.rs Removed spoke queries, simplified to Universal Graph pattern
node_service.rs Removed spoke DDL generation from seeding/schema operations
schema_table_manager.rs Removed generate_ddl_statements, kept only relationship DDL
schema_table_manager_test.rs Rewrote tests for relationship-only DDL
query_service/mod.rs Updated queries to use node.properties
schema.surql Simplified schema definitions
schema_node.rs, task_node.rs Updated property handling

Test Plan

  • All 674 Rust unit tests pass
  • All 99 integration tests pass
  • Manual testing with fresh database (~/.nodespace/database/nodespace-dev deleted)
  • Verified MCP tools work (create_nodes_from_markdown, semantic search)
  • Verified embedding service generates embeddings correctly

Architecture

BEFORE (Spoke Tables):
┌─────────┐     ┌──────────┐     ┌─────────────┐
│  node   │────>│   task   │     │   schema    │
│ (base)  │     │ (spoke)  │     │  (spoke)    │
└─────────┘     └──────────┘     └─────────────┘
     │               │                  │
     └───────────────┴──────────────────┘
         Multiple JOINs required

AFTER (Universal Graph):
┌─────────────────────────────────────────┐
│                  node                    │
│  ┌─────────────────────────────────┐    │
│  │ properties: { status, dueDate,  │    │
│  │              schemaVersion, ... }│    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘
         Single table, embedded JSON

Related Issues

🤖 Generated with Claude Code

@malibio
malibio force-pushed the feature/issue-783-universal-graph-stream-b branch 2 times, most recently from 8166d73 to 91aeee7 Compare January 5, 2026 14:12
… (Issue #783 Stream B)

## Summary

This is the core data model transition from Hub-and-Spoke to Universal Graph Architecture.
All node data (including schemas) now stored in single `node` table with `properties` JSON field.

## Changes

### Database Schema (`schema.surql`)
- Removed `data` field (record link to spoke tables)
- Added `properties` field for all type-specific data
- Removed `schema` spoke table definition
- All node types now use node.properties for storage

### SurrealStore
- Removed all spoke table fetching logic (N+1 pattern eliminated)
- `get_node()` now single query + memberships (was 3 queries)
- `get_nodes_by_ids()` now single batch query + memberships
- Removed `has_spoke_table()`, `batch_fetch_properties()` methods
- Removed `types_with_spoke_tables` cache
- Updated schema node methods to query from `node` table
- Updated `create_schema_node_atomic()` to store in node.properties
- Updated `get_task_node()` / `update_task_node()` for Universal Graph

### QueryService
- Removed all spoke-centric query methods (`build_spoke_query()`, etc.)
- Unified to single `build_query()` targeting node table
- Property filters now use `properties.fieldName` pattern
- Removed FETCH clause - no longer needed

### SchemaTableManager
- Simplified to pure DDL generator (stateless)
- Removed async database execution methods
- Removed `sync_schema_to_database()`, `define_field()`, `create_field_index()`
- Kept only DDL generation for indexes and edge tables

### Strongly-Typed Node Models
- Updated `TaskNode` docs to reflect Universal Graph Architecture
- Updated `SchemaNode` docs to reflect Universal Graph Architecture
- Renamed `has_spoke_fields()` -> `has_property_fields()`
- Renamed `has_hub_fields()` -> `has_content_field()`

## Impact

- **~1,300 lines of code deleted** (1,705 deletions - 417 insertions)
- **Query performance**: Single table queries, no N+1 spoke fetching
- **Simpler sync**: Single record atomic operations
- **Zero DDL for new types**: Custom types work via properties

## Test Results

- Frontend: 3431 passed (no change from baseline)
- Rust: 674 passed (no change from baseline)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@malibio
malibio force-pushed the feature/issue-783-universal-graph-stream-b branch from 91aeee7 to 18bcf23 Compare January 5, 2026 19:44
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@malibio malibio changed the title Epic: Universal Graph Architecture Transition feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783) Jan 6, 2026
@malibio

malibio commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator Author

Code Review Summary

PR #786: feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783)

Overall Assessment: This is an excellent architectural change that significantly simplifies the codebase by eliminating the hub-and-spoke pattern in favor of a Universal Graph Architecture. The change removes ~1,792 lines of code while maintaining full functionality. The PR is a net positive for code health.

Verdict: APPROVE with minor suggestions


Findings

Critical Issues

None identified. The implementation is sound and aligns with the issue requirements.


Suggested Improvements

[Improvement] Test Double Inconsistency

File: `/packages/core/src/services/schema_table_manager.rs` (lines 731-734)

The `TestSchemaTableManager` helper in the unit tests generates edge table DDL with a different pattern than production:

```rust
// Production code (line 137):
"DEFINE TABLE IF NOT EXISTS {} SCHEMAFULL TYPE RELATION IN node OUT node;"

// Test helper (line 732):
"DEFINE TABLE IF NOT EXISTS {} SCHEMAFULL TYPE RELATION IN {} OUT {};"
// Uses source_type and relationship.target_type instead of 'node'
```

This means the test `test_generate_edge_table_ddl_basic` (line 471-472) asserts:
```rust
assert!(statements[0].contains("TYPE RELATION IN invoice OUT customer"));
```

But the production code would generate:
```rust
"TYPE RELATION IN node OUT node"
```

Principle: Test doubles should mirror production behavior to prevent false confidence. This test passes but validates incorrect behavior.

Recommendation: Update `TestSchemaTableManager.generate_edge_table_ddl()` to match production:
```rust
statements.push(format!(
"DEFINE TABLE IF NOT EXISTS {} SCHEMAFULL TYPE RELATION IN node OUT node;",
edge_table
));
```

And update the test assertion accordingly.


[Improvement] Consider extracting DDL constants

File: `/packages/core/src/services/schema_table_manager.rs`

The DDL template strings are duplicated between `SchemaTableManager` and `TestSchemaTableManager`. Consider extracting these as constants or using a shared method to ensure consistency.


Nitpicks

Nit: `/packages/core/src/db/surreal_store.rs` (line 601)

Minor log statement change from emoji to plain text is inconsistent with other places in the codebase that use emojis:
```rust
tracing::info!("Connected to SurrealDB HTTP server");
// Was: tracing::info!("✅ Connected to SurrealDB HTTP server");
```

This is minor and could be addressed in a follow-up cleanup pass.


Requirements Validation

From Issue #783 Stream B acceptance criteria:

Criterion Status
Remove `data` field (Record Link to spoke) DONE - Removed from schema.surql
Migrate spoke data to `node.properties` DONE - All properties now in node.properties
Delete spoke tables (task, schema spokes) DONE - Only node table remains
Simplify SchemaTableManager to index management only DONE - Now only handles relationship DDL
Remove N+1 property fetching code DONE - ~1,000 LOC removed from surreal_store.rs

All Stream B requirements are satisfied.


Testing

  • 673 Rust tests pass (1 pre-existing failure unrelated to this PR: `test_http_callback_invoked`)
  • The pre-existing test failure is a model loading issue, not related to this architectural change
  • Integration tests for the new DDL generation pass

Summary

This is a well-executed architectural refactoring that delivers significant simplification:

  • Eliminates the hub-and-spoke complexity
  • Reduces query complexity (single table, no N+1)
  • Removes ~1,800 lines of code
  • Maintains all existing functionality

The only actionable item is the test double inconsistency which should be corrected to ensure tests validate actual production behavior.


Pragmatic Code Review by Claude Opus 4.5

@malibio malibio left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review Complete - Recommendation: APPROVE

This is an excellent architectural refactoring that eliminates the hub-and-spoke complexity in favor of Universal Graph Architecture. The change delivers a net reduction of ~1,800 lines of code while maintaining full functionality.

One improvement suggestion: Fix the test double inconsistency in TestSchemaTableManager to match production behavior (IN node OUT node vs IN {source_type} OUT {target_type}). This can be addressed in a follow-up commit.

All Stream B requirements from Issue #783 are satisfied. Ready to merge.

Address code review recommendation: TestSchemaTableManager now generates
edge table DDL matching production behavior using 'IN node OUT node'
instead of 'IN {source_type} OUT {target_type}'.

Universal Graph Architecture (Issue #783) stores all nodes in the 'node'
table, so relationship edge tables must reference 'node' for both IN and
OUT clauses, not the schema type names.

Changes:
- Update TestSchemaTableManager.generate_edge_table_ddl() to use 'node'
- Update test_generate_edge_table_ddl_basic assertion to expect 'node'
- Add explanatory comments referencing Issue #783

Skipped recommendations (engineering judgment):
- DDL constant extraction: Marginal benefit for test code duplication
- Emoji in log statements: Purely cosmetic, no functional impact

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@malibio

malibio commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator Author

Review Feedback Addressed

✅ Addressed: 1 recommendation

Test Double Inconsistency (🟡 Important)

  • Updated TestSchemaTableManager.generate_edge_table_ddl() to match production behavior
  • Now generates IN node OUT node instead of IN {source_type} OUT {target_type}
  • Updated test assertion in test_generate_edge_table_ddl_basic accordingly
  • Added explanatory comments referencing Universal Graph Architecture (Issue Epic: Universal Graph Architecture Transition #783)

Commit: aa2f7ea

⏭️ Skipped: 2 recommendations (engineering judgment)

Recommendation Reason for Skipping
Extract DDL constants Marginal benefit for test code duplication; can address in future cleanup
Emoji in log statements Purely cosmetic inconsistency with no functional impact

Summary

  • ✅ Addressed: 1 recommendation
  • ⏭️ Skipped: 2 recommendations
  • 📝 Commits: 1
  • 🧪 Tests: 2/2 edge DDL tests passing

Review feedback addressed by Claude Opus 4.5

@malibio
malibio merged commit 948e82e into main Jan 6, 2026
@malibio
malibio deleted the feature/issue-783-universal-graph-stream-b branch January 6, 2026 11:06
malibio added a commit that referenced this pull request Feb 4, 2026
… (Issue #783) (#786)

* feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783 Stream B)

## Summary

This is the core data model transition from Hub-and-Spoke to Universal Graph Architecture.
All node data (including schemas) now stored in single `node` table with `properties` JSON field.

## Changes

### Database Schema (`schema.surql`)
- Removed `data` field (record link to spoke tables)
- Added `properties` field for all type-specific data
- Removed `schema` spoke table definition
- All node types now use node.properties for storage

### SurrealStore
- Removed all spoke table fetching logic (N+1 pattern eliminated)
- `get_node()` now single query + memberships (was 3 queries)
- `get_nodes_by_ids()` now single batch query + memberships
- Removed `has_spoke_table()`, `batch_fetch_properties()` methods
- Removed `types_with_spoke_tables` cache
- Updated schema node methods to query from `node` table
- Updated `create_schema_node_atomic()` to store in node.properties
- Updated `get_task_node()` / `update_task_node()` for Universal Graph

### QueryService
- Removed all spoke-centric query methods (`build_spoke_query()`, etc.)
- Unified to single `build_query()` targeting node table
- Property filters now use `properties.fieldName` pattern
- Removed FETCH clause - no longer needed

### SchemaTableManager
- Simplified to pure DDL generator (stateless)
- Removed async database execution methods
- Removed `sync_schema_to_database()`, `define_field()`, `create_field_index()`
- Kept only DDL generation for indexes and edge tables

### Strongly-Typed Node Models
- Updated `TaskNode` docs to reflect Universal Graph Architecture
- Updated `SchemaNode` docs to reflect Universal Graph Architecture
- Renamed `has_spoke_fields()` -> `has_property_fields()`
- Renamed `has_hub_fields()` -> `has_content_field()`

## Impact

- **~1,300 lines of code deleted** (1,705 deletions - 417 insertions)
- **Query performance**: Single table queries, no N+1 spoke fetching
- **Simpler sync**: Single record atomic operations
- **Zero DDL for new types**: Custom types work via properties

## Test Results

- Frontend: 3431 passed (no change from baseline)
- Rust: 674 passed (no change from baseline)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Apply formatting and update baseline-browser-mapping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Align test double with production DDL generation (review feedback)

Address code review recommendation: TestSchemaTableManager now generates
edge table DDL matching production behavior using 'IN node OUT node'
instead of 'IN {source_type} OUT {target_type}'.

Universal Graph Architecture (Issue #783) stores all nodes in the 'node'
table, so relationship edge tables must reference 'node' for both IN and
OUT clauses, not the schema type names.

Changes:
- Update TestSchemaTableManager.generate_edge_table_ddl() to use 'node'
- Update test_generate_edge_table_ddl_basic assertion to expect 'node'
- Add explanatory comments referencing Issue #783

Skipped recommendations (engineering judgment):
- DDL constant extraction: Marginal benefit for test code duplication
- Emoji in log statements: Purely cosmetic, no functional impact

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
malibio added a commit that referenced this pull request Feb 5, 2026
… (Issue #783) (#786)

* feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783 Stream B)

## Summary

This is the core data model transition from Hub-and-Spoke to Universal Graph Architecture.
All node data (including schemas) now stored in single `node` table with `properties` JSON field.

## Changes

### Database Schema (`schema.surql`)
- Removed `data` field (record link to spoke tables)
- Added `properties` field for all type-specific data
- Removed `schema` spoke table definition
- All node types now use node.properties for storage

### SurrealStore
- Removed all spoke table fetching logic (N+1 pattern eliminated)
- `get_node()` now single query + memberships (was 3 queries)
- `get_nodes_by_ids()` now single batch query + memberships
- Removed `has_spoke_table()`, `batch_fetch_properties()` methods
- Removed `types_with_spoke_tables` cache
- Updated schema node methods to query from `node` table
- Updated `create_schema_node_atomic()` to store in node.properties
- Updated `get_task_node()` / `update_task_node()` for Universal Graph

### QueryService
- Removed all spoke-centric query methods (`build_spoke_query()`, etc.)
- Unified to single `build_query()` targeting node table
- Property filters now use `properties.fieldName` pattern
- Removed FETCH clause - no longer needed

### SchemaTableManager
- Simplified to pure DDL generator (stateless)
- Removed async database execution methods
- Removed `sync_schema_to_database()`, `define_field()`, `create_field_index()`
- Kept only DDL generation for indexes and edge tables

### Strongly-Typed Node Models
- Updated `TaskNode` docs to reflect Universal Graph Architecture
- Updated `SchemaNode` docs to reflect Universal Graph Architecture
- Renamed `has_spoke_fields()` -> `has_property_fields()`
- Renamed `has_hub_fields()` -> `has_content_field()`

## Impact

- **~1,300 lines of code deleted** (1,705 deletions - 417 insertions)
- **Query performance**: Single table queries, no N+1 spoke fetching
- **Simpler sync**: Single record atomic operations
- **Zero DDL for new types**: Custom types work via properties

## Test Results

- Frontend: 3431 passed (no change from baseline)
- Rust: 674 passed (no change from baseline)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Apply formatting and update baseline-browser-mapping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Align test double with production DDL generation (review feedback)

Address code review recommendation: TestSchemaTableManager now generates
edge table DDL matching production behavior using 'IN node OUT node'
instead of 'IN {source_type} OUT {target_type}'.

Universal Graph Architecture (Issue #783) stores all nodes in the 'node'
table, so relationship edge tables must reference 'node' for both IN and
OUT clauses, not the schema type names.

Changes:
- Update TestSchemaTableManager.generate_edge_table_ddl() to use 'node'
- Update test_generate_edge_table_ddl_basic assertion to expect 'node'
- Add explanatory comments referencing Issue #783

Skipped recommendations (engineering judgment):
- DDL constant extraction: Marginal benefit for test code duplication
- Emoji in log statements: Purely cosmetic, no functional impact

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
malibio added a commit that referenced this pull request Feb 26, 2026
… (Issue #783) (#786)

* feat: Implement Universal Graph Architecture - eliminate spoke tables (Issue #783 Stream B)

## Summary

This is the core data model transition from Hub-and-Spoke to Universal Graph Architecture.
All node data (including schemas) now stored in single `node` table with `properties` JSON field.

## Changes

### Database Schema (`schema.surql`)
- Removed `data` field (record link to spoke tables)
- Added `properties` field for all type-specific data
- Removed `schema` spoke table definition
- All node types now use node.properties for storage

### SurrealStore
- Removed all spoke table fetching logic (N+1 pattern eliminated)
- `get_node()` now single query + memberships (was 3 queries)
- `get_nodes_by_ids()` now single batch query + memberships
- Removed `has_spoke_table()`, `batch_fetch_properties()` methods
- Removed `types_with_spoke_tables` cache
- Updated schema node methods to query from `node` table
- Updated `create_schema_node_atomic()` to store in node.properties
- Updated `get_task_node()` / `update_task_node()` for Universal Graph

### QueryService
- Removed all spoke-centric query methods (`build_spoke_query()`, etc.)
- Unified to single `build_query()` targeting node table
- Property filters now use `properties.fieldName` pattern
- Removed FETCH clause - no longer needed

### SchemaTableManager
- Simplified to pure DDL generator (stateless)
- Removed async database execution methods
- Removed `sync_schema_to_database()`, `define_field()`, `create_field_index()`
- Kept only DDL generation for indexes and edge tables

### Strongly-Typed Node Models
- Updated `TaskNode` docs to reflect Universal Graph Architecture
- Updated `SchemaNode` docs to reflect Universal Graph Architecture
- Renamed `has_spoke_fields()` -> `has_property_fields()`
- Renamed `has_hub_fields()` -> `has_content_field()`

## Impact

- **~1,300 lines of code deleted** (1,705 deletions - 417 insertions)
- **Query performance**: Single table queries, no N+1 spoke fetching
- **Simpler sync**: Single record atomic operations
- **Zero DDL for new types**: Custom types work via properties

## Test Results

- Frontend: 3431 passed (no change from baseline)
- Rust: 674 passed (no change from baseline)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Apply formatting and update baseline-browser-mapping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Align test double with production DDL generation (review feedback)

Address code review recommendation: TestSchemaTableManager now generates
edge table DDL matching production behavior using 'IN node OUT node'
instead of 'IN {source_type} OUT {target_type}'.

Universal Graph Architecture (Issue #783) stores all nodes in the 'node'
table, so relationship edge tables must reference 'node' for both IN and
OUT clauses, not the schema type names.

Changes:
- Update TestSchemaTableManager.generate_edge_table_ddl() to use 'node'
- Update test_generate_edge_table_ddl_basic assertion to expect 'node'
- Add explanatory comments referencing Issue #783

Skipped recommendations (engineering judgment):
- DDL constant extraction: Marginal benefit for test code duplication
- Emoji in log statements: Purely cosmetic, no functional impact

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

Epic: Universal Graph Architecture Transition

1 participant