import { QueryEngine } from '../src/query-engine'; import { Lossless } from '../src/lossless'; import { DefaultSchemaRegistry } from '../src/schema-registry'; import { CommonSchemas, SchemaBuilder, PrimitiveSchemas } from '../src/schema'; import { Delta } from '../src/delta'; import { RhizomeNode } from '../src/node'; describe('Query Engine', () => { let queryEngine: QueryEngine; let lossless: Lossless; let schemaRegistry: DefaultSchemaRegistry; let rhizomeNode: RhizomeNode; beforeEach(async () => { rhizomeNode = new RhizomeNode({ peerId: 'test-query-node', publishBindPort: 4002, requestBindPort: 4003 }); lossless = rhizomeNode.lossless; schemaRegistry = new DefaultSchemaRegistry(); queryEngine = new QueryEngine(lossless, schemaRegistry); // Register test schemas schemaRegistry.register(CommonSchemas.User()); schemaRegistry.register(CommonSchemas.UserSummary()); // Create a custom test schema const blogPostSchema = SchemaBuilder .create('blog-post') .name('Blog Post') .property('title', PrimitiveSchemas.requiredString()) .property('content', PrimitiveSchemas.string()) .property('author', PrimitiveSchemas.requiredString()) .property('published', PrimitiveSchemas.boolean()) .property('views', PrimitiveSchemas.number()) .required('title', 'author') .build(); schemaRegistry.register(blogPostSchema); }); afterEach(async () => { // No cleanup needed for now }); async function createUser(id: string, name: string, age?: number, email?: string) { // Create user entity with name const nameDelta = new Delta({ id: `delta-${id}-name-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'user', target: id, targetContext: 'name' }, { localContext: 'value', target: name } ] }); lossless.ingestDelta(nameDelta); // Add age if provided if (age !== undefined) { const ageDelta = new Delta({ id: `delta-${id}-age-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'user', target: id, targetContext: 'age' }, { localContext: 'value', target: age } ] }); lossless.ingestDelta(ageDelta); } // Add email if provided if (email) { const emailDelta = new Delta({ id: `delta-${id}-email-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'user', target: id, targetContext: 'email' }, { localContext: 'value', target: email } ] }); lossless.ingestDelta(emailDelta); } } async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) { // Title delta const titleDelta = new Delta({ id: `delta-${id}-title-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'post', target: id, targetContext: 'title' }, { localContext: 'value', target: title } ] }); lossless.ingestDelta(titleDelta); // Author delta const authorDelta = new Delta({ id: `delta-${id}-author-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'post', target: id, targetContext: 'author' }, { localContext: 'value', target: author } ] }); lossless.ingestDelta(authorDelta); // Published delta const publishedDelta = new Delta({ id: `delta-${id}-published-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'post', target: id, targetContext: 'published' }, { localContext: 'value', target: published } ] }); lossless.ingestDelta(publishedDelta); // Views delta const viewsDelta = new Delta({ id: `delta-${id}-views-${Date.now()}`, creator: 'test', host: 'test-host', timeCreated: Date.now(), pointers: [ { localContext: 'post', target: id, targetContext: 'views' }, { localContext: 'value', target: views } ] }); lossless.ingestDelta(viewsDelta); } describe('Basic Query Operations', () => { it('can query all entities of a schema type', async () => { // Create test users await createUser('user1', 'Alice', 25, 'alice@example.com'); await createUser('user2', 'Bob', 30); await createUser('user3', 'Charlie', 35, 'charlie@example.com'); const result = await queryEngine.query('user'); expect(result.totalFound).toBe(3); expect(result.limited).toBe(false); expect(Object.keys(result.entities)).toHaveLength(3); expect(result.entities['user1']).toBeDefined(); expect(result.entities['user2']).toBeDefined(); expect(result.entities['user3']).toBeDefined(); }); it('can query a single entity by ID', async () => { await createUser('user1', 'Alice', 25, 'alice@example.com'); const result = await queryEngine.queryOne('user', 'user1'); expect(result).toBeDefined(); expect(result?.id).toBe('user1'); expect(result?.propertyDeltas.name).toBeDefined(); expect(result?.propertyDeltas.age).toBeDefined(); expect(result?.propertyDeltas.email).toBeDefined(); }); it('returns null for non-existent entity', async () => { const result = await queryEngine.queryOne('user', 'nonexistent'); expect(result).toBeNull(); }); }); describe('JSON Logic Filtering', () => { beforeEach(async () => { // Create test data await createUser('user1', 'Alice', 25, 'alice@example.com'); await createUser('user2', 'Bob', 30, 'bob@example.com'); await createUser('user3', 'Charlie', 35, 'charlie@example.com'); await createUser('user4', 'Diana', 20); }); it('can filter by primitive property values', async () => { // Find users older than 28 const result = await queryEngine.query('user', { '>': [{ 'var': 'age' }, 28] }); expect(result.totalFound).toBe(2); expect(result.entities['user2']).toBeDefined(); // Bob, 30 expect(result.entities['user3']).toBeDefined(); // Charlie, 35 expect(result.entities['user1']).toBeUndefined(); // Alice, 25 expect(result.entities['user4']).toBeUndefined(); // Diana, 20 }); it('can filter by string properties', async () => { // Find users with name starting with 'A' - using substring check instead of startsWith const result = await queryEngine.query('user', { 'in': ['A', { 'var': 'name' }] }); expect(result.totalFound).toBe(1); expect(result.entities['user1']).toBeDefined(); // Alice }); it('can filter by null/missing properties', async () => { // Find users without email const result = await queryEngine.query('user', { '==': [{ 'var': 'email' }, null] }); expect(result.totalFound).toBe(1); expect(result.entities['user4']).toBeDefined(); // Diana has no email }); it('can use complex logic expressions', async () => { // Find users who are (older than 30) OR (younger than 25 AND have email) const result = await queryEngine.query('user', { 'or': [ { '>': [{ 'var': 'age' }, 30] }, { 'and': [ { '<': [{ 'var': 'age' }, 25] }, { '!=': [{ 'var': 'email' }, null] } ] } ] }); expect(result.totalFound).toBe(1); expect(result.entities['user3']).toBeDefined(); // Charlie, 35 (older than 30) // Diana is younger than 25 but has no email // Alice is 25, not younger than 25 }); }); describe('Blog Post Queries', () => { beforeEach(async () => { await createBlogPost('post1', 'Introduction to Rhizome', 'alice', true, 150); await createBlogPost('post2', 'Advanced Queries', 'bob', true, 75); await createBlogPost('post3', 'Draft Post', 'alice', false, 0); await createBlogPost('post4', 'Popular Post', 'charlie', true, 1000); }); it('can filter published posts', async () => { const result = await queryEngine.query('blog-post', { '==': [{ 'var': 'published' }, true] }); expect(result.totalFound).toBe(3); expect(result.entities['post1']).toBeDefined(); expect(result.entities['post2']).toBeDefined(); expect(result.entities['post4']).toBeDefined(); expect(result.entities['post3']).toBeUndefined(); // Draft }); it('can filter by author', async () => { const result = await queryEngine.query('blog-post', { '==': [{ 'var': 'author' }, 'alice'] }); expect(result.totalFound).toBe(2); expect(result.entities['post1']).toBeDefined(); expect(result.entities['post3']).toBeDefined(); }); it('can filter by view count ranges', async () => { // Posts with more than 100 views const result = await queryEngine.query('blog-post', { '>': [{ 'var': 'views' }, 100] }); expect(result.totalFound).toBe(2); expect(result.entities['post1']).toBeDefined(); // 150 views expect(result.entities['post4']).toBeDefined(); // 1000 views }); }); describe('Query Options', () => { beforeEach(async () => { for (let i = 1; i <= 10; i++) { await createUser(`user${i}`, `User${i}`, 20 + i); } }); it('can limit query results', async () => { const result = await queryEngine.query('user', undefined, { maxResults: 5 }); expect(result.totalFound).toBe(10); expect(result.limited).toBe(true); expect(Object.keys(result.entities)).toHaveLength(5); }); it('respects delta filters', async () => { const result = await queryEngine.query('user', undefined, { deltaFilter: (delta) => delta.creator === 'test' }); expect(result.totalFound).toBe(10); expect(result.limited).toBe(false); }); }); describe('Statistics', () => { it('provides query engine statistics', async () => { await createUser('user1', 'Alice', 25); await createBlogPost('post1', 'Test Post', 'alice', true, 50); const stats = queryEngine.getStats(); expect(stats.totalEntities).toBe(2); expect(stats.registeredSchemas).toBeGreaterThan(0); expect(stats.schemasById['user']).toBe(1); expect(stats.schemasById['blog-post']).toBe(1); }); }); describe('Error Handling', () => { it('handles invalid schema IDs gracefully', async () => { const result = await queryEngine.query('nonexistent-schema'); expect(result.totalFound).toBe(0); expect(Object.keys(result.entities)).toHaveLength(0); }); it('handles malformed JSON Logic expressions', async () => { await createUser('user1', 'Alice', 25); const result = await queryEngine.query('user', { 'invalid-operator': [{ 'var': 'age' }, 25] }); // Should not crash, may return empty results or skip problematic entities expect(result).toBeDefined(); expect(typeof result.totalFound).toBe('number'); }); }); });