346 lines
11 KiB
TypeScript

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');
});
});
});