@sparkleideas/plugins 3.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuVector PostgreSQL Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive integration tests for the RuVector PostgreSQL Bridge plugin.
|
|
5
|
+
* Tests can run against a real PostgreSQL database (when available) or use mocks.
|
|
6
|
+
*
|
|
7
|
+
* Environment variables for real database testing:
|
|
8
|
+
* - RUVECTOR_TEST_DB=true - Enable real database tests
|
|
9
|
+
* - RUVECTOR_TEST_HOST - PostgreSQL host (default: localhost)
|
|
10
|
+
* - RUVECTOR_TEST_PORT - PostgreSQL port (default: 5432)
|
|
11
|
+
* - RUVECTOR_TEST_DATABASE - Database name (default: ruvector_test)
|
|
12
|
+
* - RUVECTOR_TEST_USER - Database user (default: postgres)
|
|
13
|
+
* - RUVECTOR_TEST_PASSWORD - Database password (default: postgres)
|
|
14
|
+
*
|
|
15
|
+
* @module @sparkleideas/plugins/__tests__/ruvector-integration
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi, type Mock } from 'vitest';
|
|
19
|
+
import {
|
|
20
|
+
useRealDatabase,
|
|
21
|
+
getTestDatabaseConfig,
|
|
22
|
+
createTestConfig,
|
|
23
|
+
createTestClientOptions,
|
|
24
|
+
randomVector,
|
|
25
|
+
normalizedVector,
|
|
26
|
+
randomVectors,
|
|
27
|
+
generateSimilarVectors,
|
|
28
|
+
cosineSimilarity,
|
|
29
|
+
euclideanDistance,
|
|
30
|
+
createMockSearchResults,
|
|
31
|
+
createMockConnectionResult,
|
|
32
|
+
createMockIndexStats,
|
|
33
|
+
createMockHealthStatus,
|
|
34
|
+
createMockStats,
|
|
35
|
+
createMockPgPool,
|
|
36
|
+
createMockPgClient,
|
|
37
|
+
createRandomGraph,
|
|
38
|
+
SearchOptionsBuilder,
|
|
39
|
+
InsertOptionsBuilder,
|
|
40
|
+
IndexOptionsBuilder,
|
|
41
|
+
measureAsync,
|
|
42
|
+
benchmark,
|
|
43
|
+
uniqueTableName,
|
|
44
|
+
uniqueIndexName,
|
|
45
|
+
assertSortedByScore,
|
|
46
|
+
assertSortedByDistance,
|
|
47
|
+
type MockPgPool,
|
|
48
|
+
type MockPgClient,
|
|
49
|
+
} from './utils/ruvector-test-utils.js';
|
|
50
|
+
|
|
51
|
+
import type {
|
|
52
|
+
RuVectorConfig,
|
|
53
|
+
RuVectorClientOptions,
|
|
54
|
+
VectorSearchOptions,
|
|
55
|
+
VectorSearchResult,
|
|
56
|
+
VectorInsertOptions,
|
|
57
|
+
VectorIndexOptions,
|
|
58
|
+
BatchVectorOptions,
|
|
59
|
+
ConnectionResult,
|
|
60
|
+
HealthStatus,
|
|
61
|
+
IndexStats,
|
|
62
|
+
IRuVectorClient,
|
|
63
|
+
IRuVectorTransaction,
|
|
64
|
+
QueryResult,
|
|
65
|
+
BatchResult,
|
|
66
|
+
RuVectorEventType,
|
|
67
|
+
RuVectorEvent,
|
|
68
|
+
PoolConfig,
|
|
69
|
+
SSLConfig,
|
|
70
|
+
RetryConfig,
|
|
71
|
+
} from '../src/integrations/ruvector/types.js';
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Test Configuration
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
const USE_REAL_DB = useRealDatabase();
|
|
78
|
+
const TEST_TIMEOUT = USE_REAL_DB ? 30000 : 5000;
|
|
79
|
+
|
|
80
|
+
// Skip message for real DB tests when not available
|
|
81
|
+
const skipIfNoRealDB = USE_REAL_DB ? describe : describe.skip;
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Mock RuVector Client Implementation
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mock RuVector client for unit testing without real database
|
|
89
|
+
*/
|
|
90
|
+
class MockRuVectorClient implements IRuVectorClient {
|
|
91
|
+
private pool: MockPgPool;
|
|
92
|
+
private config: RuVectorClientOptions;
|
|
93
|
+
private connected: boolean = false;
|
|
94
|
+
private eventHandlers: Map<RuVectorEventType, Set<(event: RuVectorEvent) => void>> = new Map();
|
|
95
|
+
private connectionInfo: ConnectionResult | null = null;
|
|
96
|
+
private queryCount: number = 0;
|
|
97
|
+
|
|
98
|
+
constructor(config: RuVectorClientOptions) {
|
|
99
|
+
this.config = config;
|
|
100
|
+
this.pool = createMockPgPool();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Event Emitter Implementation
|
|
104
|
+
on<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): () => void {
|
|
105
|
+
if (!this.eventHandlers.has(event)) {
|
|
106
|
+
this.eventHandlers.set(event, new Set());
|
|
107
|
+
}
|
|
108
|
+
this.eventHandlers.get(event)!.add(handler as (e: RuVectorEvent) => void);
|
|
109
|
+
return () => this.off(event, handler);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
off<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): void {
|
|
113
|
+
this.eventHandlers.get(event)?.delete(handler as (e: RuVectorEvent) => void);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
once<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): () => void {
|
|
117
|
+
const wrappedHandler = (e: RuVectorEvent<T>) => {
|
|
118
|
+
this.off(event, wrappedHandler);
|
|
119
|
+
handler(e);
|
|
120
|
+
};
|
|
121
|
+
return this.on(event, wrappedHandler);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
emit<T extends RuVectorEventType>(event: T, data: RuVectorEvent<T>['data']): void {
|
|
125
|
+
const handlers = this.eventHandlers.get(event);
|
|
126
|
+
if (handlers) {
|
|
127
|
+
handlers.forEach((handler) =>
|
|
128
|
+
handler({ type: event, timestamp: new Date(), data } as RuVectorEvent)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
removeAllListeners(event?: RuVectorEventType): void {
|
|
134
|
+
if (event) {
|
|
135
|
+
this.eventHandlers.delete(event);
|
|
136
|
+
} else {
|
|
137
|
+
this.eventHandlers.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Connection Management
|
|
142
|
+
async connect(): Promise<ConnectionResult> {
|
|
143
|
+
this.connected = true;
|
|
144
|
+
this.connectionInfo = createMockConnectionResult();
|
|
145
|
+
|
|
146
|
+
this.emit('connection:open', {
|
|
147
|
+
connectionId: this.connectionInfo.connectionId,
|
|
148
|
+
host: this.config.host,
|
|
149
|
+
port: this.config.port,
|
|
150
|
+
database: this.config.database,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return this.connectionInfo;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async disconnect(): Promise<void> {
|
|
157
|
+
this.connected = false;
|
|
158
|
+
this.emit('connection:close', {
|
|
159
|
+
connectionId: this.connectionInfo?.connectionId || '',
|
|
160
|
+
host: this.config.host,
|
|
161
|
+
port: this.config.port,
|
|
162
|
+
database: this.config.database,
|
|
163
|
+
});
|
|
164
|
+
this.connectionInfo = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
isConnected(): boolean {
|
|
168
|
+
return this.connected;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getConnectionInfo(): ConnectionResult | null {
|
|
172
|
+
return this.connectionInfo;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Vector Operations
|
|
176
|
+
async search(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
|
|
177
|
+
this.ensureConnected();
|
|
178
|
+
this.queryCount++;
|
|
179
|
+
|
|
180
|
+
this.emit('search:start', {
|
|
181
|
+
searchId: `search-${this.queryCount}`,
|
|
182
|
+
tableName: options.tableName || 'vectors',
|
|
183
|
+
k: options.k,
|
|
184
|
+
metric: options.metric,
|
|
185
|
+
hasFilters: !!options.filter,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Simulate search with mock results
|
|
189
|
+
const results = createMockSearchResults(Math.min(options.k, 100), {
|
|
190
|
+
includeVector: options.includeVector,
|
|
191
|
+
includeMetadata: options.includeMetadata,
|
|
192
|
+
dimensions: options.query.length,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.emit('search:complete', {
|
|
196
|
+
searchId: `search-${this.queryCount}`,
|
|
197
|
+
durationMs: 15 + Math.random() * 10,
|
|
198
|
+
resultCount: results.length,
|
|
199
|
+
scannedCount: 1000,
|
|
200
|
+
cacheHit: false,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async batchSearch(options: BatchVectorOptions): Promise<{
|
|
207
|
+
results: VectorSearchResult[][];
|
|
208
|
+
totalDurationMs: number;
|
|
209
|
+
avgDurationMs: number;
|
|
210
|
+
cacheStats: { hits: number; misses: number; hitRate: number };
|
|
211
|
+
}> {
|
|
212
|
+
this.ensureConnected();
|
|
213
|
+
|
|
214
|
+
const results = await Promise.all(
|
|
215
|
+
options.queries.map((query) =>
|
|
216
|
+
this.search({
|
|
217
|
+
query: Array.from(query),
|
|
218
|
+
k: options.k,
|
|
219
|
+
metric: options.metric,
|
|
220
|
+
filter: options.filter,
|
|
221
|
+
tableName: options.tableName,
|
|
222
|
+
vectorColumn: options.vectorColumn,
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const totalDurationMs = results.length * 15;
|
|
228
|
+
return {
|
|
229
|
+
results,
|
|
230
|
+
totalDurationMs,
|
|
231
|
+
avgDurationMs: totalDurationMs / options.queries.length,
|
|
232
|
+
cacheStats: {
|
|
233
|
+
hits: 0,
|
|
234
|
+
misses: options.queries.length,
|
|
235
|
+
hitRate: 0,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async insert(options: VectorInsertOptions): Promise<BatchResult<string>> {
|
|
241
|
+
this.ensureConnected();
|
|
242
|
+
this.queryCount++;
|
|
243
|
+
|
|
244
|
+
const ids = options.vectors.map(
|
|
245
|
+
(v, i) => v.id?.toString() || `gen-${Date.now()}-${i}`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
options.vectors.forEach((v, i) => {
|
|
249
|
+
this.emit('vector:inserted', {
|
|
250
|
+
tableName: options.tableName,
|
|
251
|
+
vectorId: ids[i],
|
|
252
|
+
dimensions: Array.isArray(v.vector) ? v.vector.length : v.vector.length,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const durationMs = options.vectors.length * 5;
|
|
257
|
+
return {
|
|
258
|
+
total: options.vectors.length,
|
|
259
|
+
successful: options.vectors.length,
|
|
260
|
+
failed: 0,
|
|
261
|
+
results: options.returning ? ids : undefined,
|
|
262
|
+
durationMs,
|
|
263
|
+
throughput: options.vectors.length / (durationMs / 1000),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async update(options: { tableName: string; id: string | number; vector?: number[]; metadata?: Record<string, unknown> }): Promise<boolean> {
|
|
268
|
+
this.ensureConnected();
|
|
269
|
+
this.queryCount++;
|
|
270
|
+
|
|
271
|
+
this.emit('vector:updated', {
|
|
272
|
+
tableName: options.tableName,
|
|
273
|
+
vectorId: options.id,
|
|
274
|
+
dimensions: options.vector?.length ?? 0,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async delete(tableName: string, id: string | number): Promise<boolean> {
|
|
281
|
+
this.ensureConnected();
|
|
282
|
+
this.queryCount++;
|
|
283
|
+
|
|
284
|
+
this.emit('vector:deleted', {
|
|
285
|
+
tableName,
|
|
286
|
+
vectorId: id,
|
|
287
|
+
dimensions: 0,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async bulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
|
|
294
|
+
this.ensureConnected();
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
total: ids.length,
|
|
298
|
+
successful: ids.length,
|
|
299
|
+
failed: 0,
|
|
300
|
+
durationMs: ids.length * 2,
|
|
301
|
+
throughput: ids.length / ((ids.length * 2) / 1000),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Index Management
|
|
306
|
+
async createIndex(options: VectorIndexOptions): Promise<void> {
|
|
307
|
+
this.ensureConnected();
|
|
308
|
+
|
|
309
|
+
this.emit('index:created', {
|
|
310
|
+
indexName: options.indexName || `idx_${options.tableName}_${options.columnName}`,
|
|
311
|
+
tableName: options.tableName,
|
|
312
|
+
columnName: options.columnName,
|
|
313
|
+
indexType: options.indexType,
|
|
314
|
+
durationMs: 1000 + Math.random() * 2000,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async dropIndex(indexName: string): Promise<void> {
|
|
319
|
+
this.ensureConnected();
|
|
320
|
+
|
|
321
|
+
this.emit('index:dropped', {
|
|
322
|
+
indexName,
|
|
323
|
+
tableName: '',
|
|
324
|
+
columnName: '',
|
|
325
|
+
indexType: 'hnsw',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async rebuildIndex(indexName: string): Promise<void> {
|
|
330
|
+
this.ensureConnected();
|
|
331
|
+
|
|
332
|
+
this.emit('index:rebuilt', {
|
|
333
|
+
indexName,
|
|
334
|
+
tableName: '',
|
|
335
|
+
columnName: '',
|
|
336
|
+
indexType: 'hnsw',
|
|
337
|
+
durationMs: 2000 + Math.random() * 3000,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async getIndexStats(indexName: string): Promise<IndexStats> {
|
|
342
|
+
this.ensureConnected();
|
|
343
|
+
return createMockIndexStats(indexName);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async listIndices(tableName?: string): Promise<IndexStats[]> {
|
|
347
|
+
this.ensureConnected();
|
|
348
|
+
return [
|
|
349
|
+
createMockIndexStats(`idx_${tableName || 'vectors'}_embedding_hnsw`),
|
|
350
|
+
createMockIndexStats(`idx_${tableName || 'vectors'}_embedding_ivf`, 'ivfflat'),
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Attention Operations
|
|
355
|
+
async computeAttention(input: { query: number[][]; key: number[][]; value: number[][] }, config: { mechanism: string; numHeads: number; headDim: number; params?: Record<string, unknown> }): Promise<{ output: number[][]; attentionWeights?: number[][][][]; stats?: { computeTimeMs: number; memoryBytes: number; tokensProcessed: number } }> {
|
|
356
|
+
this.ensureConnected();
|
|
357
|
+
|
|
358
|
+
const seqLen = input.query.length;
|
|
359
|
+
const outputDim = config.numHeads * config.headDim;
|
|
360
|
+
|
|
361
|
+
const output = input.query.map(() =>
|
|
362
|
+
Array.from({ length: outputDim }, () => Math.random() * 2 - 1)
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
this.emit('attention:computed', {
|
|
366
|
+
mechanism: config.mechanism as any,
|
|
367
|
+
seqLen,
|
|
368
|
+
numHeads: config.numHeads,
|
|
369
|
+
durationMs: seqLen * 0.1,
|
|
370
|
+
memoryBytes: seqLen * outputDim * 4,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
output,
|
|
375
|
+
attentionWeights: config.params?.checkpointing ? undefined : undefined,
|
|
376
|
+
stats: {
|
|
377
|
+
computeTimeMs: seqLen * 0.1,
|
|
378
|
+
memoryBytes: seqLen * outputDim * 4,
|
|
379
|
+
tokensProcessed: seqLen,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// GNN Operations
|
|
385
|
+
async runGNNLayer(graph: { nodeFeatures: number[][]; edgeIndex: [number[], number[]] }, layer: { type: string; inputDim: number; outputDim: number }): Promise<{ nodeEmbeddings: number[][]; stats?: { forwardTimeMs: number; numNodes: number; numEdges: number; memoryBytes: number; numIterations: number } }> {
|
|
386
|
+
this.ensureConnected();
|
|
387
|
+
|
|
388
|
+
const numNodes = graph.nodeFeatures.length;
|
|
389
|
+
const numEdges = graph.edgeIndex[0].length;
|
|
390
|
+
|
|
391
|
+
const nodeEmbeddings = graph.nodeFeatures.map(() =>
|
|
392
|
+
Array.from({ length: layer.outputDim }, () => Math.random() * 2 - 1)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
this.emit('gnn:forward', {
|
|
396
|
+
layerType: layer.type as any,
|
|
397
|
+
numNodes,
|
|
398
|
+
numEdges,
|
|
399
|
+
durationMs: numNodes * 0.05,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
nodeEmbeddings,
|
|
404
|
+
stats: {
|
|
405
|
+
forwardTimeMs: numNodes * 0.05,
|
|
406
|
+
numNodes,
|
|
407
|
+
numEdges,
|
|
408
|
+
memoryBytes: numNodes * layer.outputDim * 4,
|
|
409
|
+
numIterations: 1,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
buildGraph(nodeFeatures: number[][], edges: [number, number][]): { nodeFeatures: number[][]; edgeIndex: [number[], number[]] } {
|
|
415
|
+
return {
|
|
416
|
+
nodeFeatures,
|
|
417
|
+
edgeIndex: [edges.map((e) => e[0]), edges.map((e) => e[1])],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Hyperbolic Operations
|
|
422
|
+
async hyperbolicEmbed(input: { points: number[][] }, config: { model: string; curvature: number; dimension: number }): Promise<{ embeddings: number[][]; curvature: number }> {
|
|
423
|
+
this.ensureConnected();
|
|
424
|
+
|
|
425
|
+
const embeddings = input.points.map((point) => {
|
|
426
|
+
const norm = Math.sqrt(point.reduce((sum, v) => sum + v * v, 0));
|
|
427
|
+
const scale = norm >= 1 ? 0.99 / norm : 1;
|
|
428
|
+
return point.map((v) => v * scale);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this.emit('hyperbolic:embed', {
|
|
432
|
+
model: config.model as any,
|
|
433
|
+
operation: 'embed',
|
|
434
|
+
numPoints: input.points.length,
|
|
435
|
+
durationMs: input.points.length * 0.02,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
embeddings,
|
|
440
|
+
curvature: config.curvature,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async hyperbolicDistance(a: number[], b: number[], config: { model: string; curvature: number; dimension: number }): Promise<number> {
|
|
445
|
+
this.ensureConnected();
|
|
446
|
+
|
|
447
|
+
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
|
|
448
|
+
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
|
|
449
|
+
const diffNorm = Math.sqrt(a.reduce((sum, v, i) => sum + (v - b[i]) ** 2, 0));
|
|
450
|
+
|
|
451
|
+
const numerator = 2 * diffNorm ** 2;
|
|
452
|
+
const denominator = Math.max((1 - normA ** 2) * (1 - normB ** 2), 1e-10);
|
|
453
|
+
const distance = Math.acosh(1 + numerator / denominator);
|
|
454
|
+
|
|
455
|
+
this.emit('hyperbolic:distance', {
|
|
456
|
+
model: config.model as any,
|
|
457
|
+
operation: 'distance',
|
|
458
|
+
numPoints: 2,
|
|
459
|
+
durationMs: 0.01,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return distance;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Embedding Operations
|
|
466
|
+
async embed(text: string, model?: string): Promise<{ embedding: number[]; model: string; tokenCount: number; durationMs: number; dimension: number }> {
|
|
467
|
+
this.ensureConnected();
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
embedding: randomVector(384),
|
|
471
|
+
model: model || 'default',
|
|
472
|
+
tokenCount: Math.ceil(text.length / 4),
|
|
473
|
+
durationMs: 50,
|
|
474
|
+
dimension: 384,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async embedBatch(texts: string[], model?: string): Promise<{ embeddings: Array<{ embedding: number[]; model: string; tokenCount: number; durationMs: number; dimension: number }>; totalTokens: number; totalDurationMs: number; throughput: number }> {
|
|
479
|
+
this.ensureConnected();
|
|
480
|
+
|
|
481
|
+
const embeddings = await Promise.all(texts.map((t) => this.embed(t, model)));
|
|
482
|
+
const totalTokens = embeddings.reduce((sum, e) => sum + e.tokenCount, 0);
|
|
483
|
+
const totalDurationMs = embeddings.reduce((sum, e) => sum + e.durationMs, 0);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
embeddings,
|
|
487
|
+
totalTokens,
|
|
488
|
+
totalDurationMs,
|
|
489
|
+
throughput: totalTokens / (totalDurationMs / 1000),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Transaction Support
|
|
494
|
+
async transaction<T>(fn: (tx: IRuVectorTransaction) => Promise<T>): Promise<{ transactionId: string; committed: boolean; data?: T; durationMs: number; queryCount: number }> {
|
|
495
|
+
this.ensureConnected();
|
|
496
|
+
|
|
497
|
+
const startTime = Date.now();
|
|
498
|
+
let txQueryCount = 0;
|
|
499
|
+
|
|
500
|
+
const tx: IRuVectorTransaction = {
|
|
501
|
+
query: async <R = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<R>> => {
|
|
502
|
+
txQueryCount++;
|
|
503
|
+
return { rows: [] as R[], rowCount: 0, durationMs: 5, command: 'SELECT' };
|
|
504
|
+
},
|
|
505
|
+
insert: async (options: VectorInsertOptions) => {
|
|
506
|
+
txQueryCount++;
|
|
507
|
+
return this.insert(options);
|
|
508
|
+
},
|
|
509
|
+
update: async (options: { tableName: string; id: string | number; vector?: number[]; metadata?: Record<string, unknown> }) => {
|
|
510
|
+
txQueryCount++;
|
|
511
|
+
return this.update(options);
|
|
512
|
+
},
|
|
513
|
+
delete: async (tableName: string, id: string | number) => {
|
|
514
|
+
txQueryCount++;
|
|
515
|
+
return this.delete(tableName, id);
|
|
516
|
+
},
|
|
517
|
+
commit: async () => {},
|
|
518
|
+
rollback: async () => {},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const data = await fn(tx);
|
|
523
|
+
await tx.commit();
|
|
524
|
+
return {
|
|
525
|
+
transactionId: `tx-${Date.now()}`,
|
|
526
|
+
committed: true,
|
|
527
|
+
data,
|
|
528
|
+
durationMs: Date.now() - startTime,
|
|
529
|
+
queryCount: txQueryCount,
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
await tx.rollback();
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Admin Operations
|
|
538
|
+
async vacuum(tableName?: string): Promise<void> {
|
|
539
|
+
this.ensureConnected();
|
|
540
|
+
|
|
541
|
+
this.emit('admin:vacuum', {
|
|
542
|
+
operation: 'vacuum',
|
|
543
|
+
tableName,
|
|
544
|
+
durationMs: 500,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async analyze(tableName?: string): Promise<{ tableName: string; numRows: number; columnStats: Array<{ columnName: string; dataType: string; nullPercent: number; distinctCount: number; avgSizeBytes: number }>; recommendations: string[] }> {
|
|
549
|
+
this.ensureConnected();
|
|
550
|
+
|
|
551
|
+
this.emit('admin:analyze', {
|
|
552
|
+
operation: 'analyze',
|
|
553
|
+
tableName,
|
|
554
|
+
durationMs: 300,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
tableName: tableName || 'all',
|
|
559
|
+
numRows: 10000,
|
|
560
|
+
columnStats: [
|
|
561
|
+
{
|
|
562
|
+
columnName: 'embedding',
|
|
563
|
+
dataType: 'vector(384)',
|
|
564
|
+
nullPercent: 0,
|
|
565
|
+
distinctCount: 10000,
|
|
566
|
+
avgSizeBytes: 1536,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
recommendations: ['Consider adding an HNSW index for faster similarity search'],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async healthCheck(): Promise<HealthStatus> {
|
|
574
|
+
return createMockHealthStatus(this.connected);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async getStats(): Promise<{ version: string; totalVectors: number; totalSizeBytes: number; numIndices: number; numTables: number; queryStats: { totalQueries: number; avgQueryTimeMs: number; p95QueryTimeMs: number; p99QueryTimeMs: number; cacheHitRate: number }; memoryStats: { usedBytes: number; peakBytes: number; indexBytes: number; cacheBytes: number } }> {
|
|
578
|
+
this.ensureConnected();
|
|
579
|
+
return createMockStats();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private ensureConnected(): void {
|
|
583
|
+
if (!this.connected) {
|
|
584
|
+
throw new Error('Not connected');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
getPool(): MockPgPool {
|
|
589
|
+
return this.pool;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
getConfig(): RuVectorClientOptions {
|
|
593
|
+
return this.config;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============================================================================
|
|
598
|
+
// Test Suites
|
|
599
|
+
// ============================================================================
|
|
600
|
+
|
|
601
|
+
describe('RuVector PostgreSQL Integration', () => {
|
|
602
|
+
let client: MockRuVectorClient;
|
|
603
|
+
let config: RuVectorClientOptions;
|
|
604
|
+
|
|
605
|
+
beforeEach(() => {
|
|
606
|
+
config = createTestClientOptions();
|
|
607
|
+
client = new MockRuVectorClient(config);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
afterEach(async () => {
|
|
611
|
+
if (client.isConnected()) {
|
|
612
|
+
await client.disconnect();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ==========================================================================
|
|
617
|
+
// Connection Management Tests
|
|
618
|
+
// ==========================================================================
|
|
619
|
+
|
|
620
|
+
describe('Connection Management', () => {
|
|
621
|
+
it('should connect to PostgreSQL', async () => {
|
|
622
|
+
const result = await client.connect();
|
|
623
|
+
|
|
624
|
+
expect(client.isConnected()).toBe(true);
|
|
625
|
+
expect(result.ready).toBe(true);
|
|
626
|
+
expect(result.connectionId).toBeDefined();
|
|
627
|
+
expect(result.serverVersion).toBeDefined();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should handle connection failures gracefully', async () => {
|
|
631
|
+
// Test that operations throw when not connected
|
|
632
|
+
await expect(
|
|
633
|
+
client.search(new SearchOptionsBuilder().build())
|
|
634
|
+
).rejects.toThrow('Not connected');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should reconnect after connection loss', async () => {
|
|
638
|
+
// Connect first
|
|
639
|
+
await client.connect();
|
|
640
|
+
expect(client.isConnected()).toBe(true);
|
|
641
|
+
|
|
642
|
+
// Disconnect
|
|
643
|
+
await client.disconnect();
|
|
644
|
+
expect(client.isConnected()).toBe(false);
|
|
645
|
+
|
|
646
|
+
// Reconnect
|
|
647
|
+
await client.connect();
|
|
648
|
+
expect(client.isConnected()).toBe(true);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should respect pool limits', async () => {
|
|
652
|
+
const customConfig = createTestClientOptions({
|
|
653
|
+
pool: {
|
|
654
|
+
min: 2,
|
|
655
|
+
max: 5,
|
|
656
|
+
idleTimeoutMs: 10000,
|
|
657
|
+
acquireTimeoutMs: 5000,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const customClient = new MockRuVectorClient(customConfig);
|
|
662
|
+
await customClient.connect();
|
|
663
|
+
|
|
664
|
+
expect(customClient.getConfig().pool?.max).toBe(5);
|
|
665
|
+
expect(customClient.getConfig().pool?.min).toBe(2);
|
|
666
|
+
|
|
667
|
+
await customClient.disconnect();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('should emit connection events', async () => {
|
|
671
|
+
const openHandler = vi.fn();
|
|
672
|
+
const closeHandler = vi.fn();
|
|
673
|
+
|
|
674
|
+
client.on('connection:open', openHandler);
|
|
675
|
+
client.on('connection:close', closeHandler);
|
|
676
|
+
|
|
677
|
+
await client.connect();
|
|
678
|
+
expect(openHandler).toHaveBeenCalledTimes(1);
|
|
679
|
+
|
|
680
|
+
await client.disconnect();
|
|
681
|
+
expect(closeHandler).toHaveBeenCalledTimes(1);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should track connection state correctly', async () => {
|
|
685
|
+
expect(client.isConnected()).toBe(false);
|
|
686
|
+
expect(client.getConnectionInfo()).toBeNull();
|
|
687
|
+
|
|
688
|
+
await client.connect();
|
|
689
|
+
expect(client.isConnected()).toBe(true);
|
|
690
|
+
expect(client.getConnectionInfo()).not.toBeNull();
|
|
691
|
+
|
|
692
|
+
await client.disconnect();
|
|
693
|
+
expect(client.isConnected()).toBe(false);
|
|
694
|
+
expect(client.getConnectionInfo()).toBeNull();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should support SSL configuration', async () => {
|
|
698
|
+
const sslConfig: SSLConfig = {
|
|
699
|
+
enabled: true,
|
|
700
|
+
rejectUnauthorized: true,
|
|
701
|
+
ca: '/path/to/ca.pem',
|
|
702
|
+
cert: '/path/to/cert.pem',
|
|
703
|
+
key: '/path/to/key.pem',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const sslClient = new MockRuVectorClient({
|
|
707
|
+
...config,
|
|
708
|
+
ssl: sslConfig,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
await sslClient.connect();
|
|
712
|
+
expect(sslClient.isConnected()).toBe(true);
|
|
713
|
+
|
|
714
|
+
const clientSsl = sslClient.getConfig().ssl as SSLConfig;
|
|
715
|
+
expect(clientSsl.enabled).toBe(true);
|
|
716
|
+
expect(clientSsl.rejectUnauthorized).toBe(true);
|
|
717
|
+
|
|
718
|
+
await sslClient.disconnect();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should support retry configuration', () => {
|
|
722
|
+
const retryConfig: RetryConfig = {
|
|
723
|
+
maxAttempts: 5,
|
|
724
|
+
initialDelayMs: 100,
|
|
725
|
+
maxDelayMs: 5000,
|
|
726
|
+
backoffMultiplier: 2,
|
|
727
|
+
jitter: true,
|
|
728
|
+
retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'],
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const retryClient = new MockRuVectorClient({
|
|
732
|
+
...config,
|
|
733
|
+
retry: retryConfig,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(retryClient.getConfig().retry?.maxAttempts).toBe(5);
|
|
737
|
+
expect(retryClient.getConfig().retry?.backoffMultiplier).toBe(2);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// ==========================================================================
|
|
742
|
+
// Vector Operations Tests
|
|
743
|
+
// ==========================================================================
|
|
744
|
+
|
|
745
|
+
describe('Vector Operations', () => {
|
|
746
|
+
beforeEach(async () => {
|
|
747
|
+
await client.connect();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should insert vectors with metadata', async () => {
|
|
751
|
+
const options = new InsertOptionsBuilder('test_vectors')
|
|
752
|
+
.addRandomVectors(10, 384)
|
|
753
|
+
.withReturning(true)
|
|
754
|
+
.build();
|
|
755
|
+
|
|
756
|
+
const result = await client.insert(options);
|
|
757
|
+
|
|
758
|
+
expect(result.successful).toBe(10);
|
|
759
|
+
expect(result.failed).toBe(0);
|
|
760
|
+
expect(result.results).toHaveLength(10);
|
|
761
|
+
expect(result.throughput).toBeGreaterThan(0);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should perform cosine similarity search', async () => {
|
|
765
|
+
const options = new SearchOptionsBuilder(384)
|
|
766
|
+
.withMetric('cosine')
|
|
767
|
+
.withK(10)
|
|
768
|
+
.withTable('embeddings')
|
|
769
|
+
.build();
|
|
770
|
+
|
|
771
|
+
const results = await client.search(options);
|
|
772
|
+
|
|
773
|
+
expect(results).toHaveLength(10);
|
|
774
|
+
results.forEach((r) => {
|
|
775
|
+
expect(r.id).toBeDefined();
|
|
776
|
+
expect(r.score).toBeDefined();
|
|
777
|
+
expect(r.score).toBeGreaterThanOrEqual(0);
|
|
778
|
+
expect(r.score).toBeLessThanOrEqual(1);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should perform euclidean distance search', async () => {
|
|
783
|
+
const options = new SearchOptionsBuilder(384)
|
|
784
|
+
.withMetric('euclidean')
|
|
785
|
+
.withK(5)
|
|
786
|
+
.build();
|
|
787
|
+
|
|
788
|
+
const results = await client.search(options);
|
|
789
|
+
|
|
790
|
+
expect(results).toHaveLength(5);
|
|
791
|
+
results.forEach((r) => {
|
|
792
|
+
expect(r.distance).toBeDefined();
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should filter by metadata', async () => {
|
|
797
|
+
const options = new SearchOptionsBuilder(384)
|
|
798
|
+
.withK(10)
|
|
799
|
+
.withMetric('cosine')
|
|
800
|
+
.withFilter({ category: 'test', active: true })
|
|
801
|
+
.includeMetadata(true)
|
|
802
|
+
.build();
|
|
803
|
+
|
|
804
|
+
const results = await client.search(options);
|
|
805
|
+
|
|
806
|
+
expect(results).toHaveLength(10);
|
|
807
|
+
results.forEach((r) => {
|
|
808
|
+
expect(r.metadata).toBeDefined();
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should handle batch inserts', async () => {
|
|
813
|
+
const options = new InsertOptionsBuilder('bulk_vectors')
|
|
814
|
+
.addRandomVectors(100, 384)
|
|
815
|
+
.withBatchSize(25)
|
|
816
|
+
.withReturning(true)
|
|
817
|
+
.build();
|
|
818
|
+
|
|
819
|
+
const result = await client.insert(options);
|
|
820
|
+
|
|
821
|
+
expect(result.total).toBe(100);
|
|
822
|
+
expect(result.successful).toBe(100);
|
|
823
|
+
expect(result.failed).toBe(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('should update existing vectors', async () => {
|
|
827
|
+
const success = await client.update({
|
|
828
|
+
tableName: 'vectors',
|
|
829
|
+
id: 'test-id-1',
|
|
830
|
+
vector: randomVector(384),
|
|
831
|
+
metadata: { updated: true, timestamp: Date.now() },
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
expect(success).toBe(true);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('should delete vectors', async () => {
|
|
838
|
+
const success = await client.delete('vectors', 'test-id-1');
|
|
839
|
+
expect(success).toBe(true);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should bulk delete vectors', async () => {
|
|
843
|
+
const ids = ['id-1', 'id-2', 'id-3', 'id-4', 'id-5'];
|
|
844
|
+
const result = await client.bulkDelete('vectors', ids);
|
|
845
|
+
|
|
846
|
+
expect(result.total).toBe(5);
|
|
847
|
+
expect(result.successful).toBe(5);
|
|
848
|
+
expect(result.failed).toBe(0);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should emit vector operation events', async () => {
|
|
852
|
+
const insertHandler = vi.fn();
|
|
853
|
+
const updateHandler = vi.fn();
|
|
854
|
+
const deleteHandler = vi.fn();
|
|
855
|
+
|
|
856
|
+
client.on('vector:inserted', insertHandler);
|
|
857
|
+
client.on('vector:updated', updateHandler);
|
|
858
|
+
client.on('vector:deleted', deleteHandler);
|
|
859
|
+
|
|
860
|
+
await client.insert(
|
|
861
|
+
new InsertOptionsBuilder('test')
|
|
862
|
+
.addVector(randomVector(384), 'test-1')
|
|
863
|
+
.build()
|
|
864
|
+
);
|
|
865
|
+
await client.update({
|
|
866
|
+
tableName: 'test',
|
|
867
|
+
id: 'test-1',
|
|
868
|
+
vector: randomVector(384),
|
|
869
|
+
});
|
|
870
|
+
await client.delete('test', 'test-1');
|
|
871
|
+
|
|
872
|
+
expect(insertHandler).toHaveBeenCalled();
|
|
873
|
+
expect(updateHandler).toHaveBeenCalled();
|
|
874
|
+
expect(deleteHandler).toHaveBeenCalled();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('should include vector in search results when requested', async () => {
|
|
878
|
+
const options = new SearchOptionsBuilder(384)
|
|
879
|
+
.withK(5)
|
|
880
|
+
.includeVector(true)
|
|
881
|
+
.build();
|
|
882
|
+
|
|
883
|
+
const results = await client.search(options);
|
|
884
|
+
|
|
885
|
+
results.forEach((r) => {
|
|
886
|
+
expect(r.vector).toBeDefined();
|
|
887
|
+
expect(r.vector).toHaveLength(384);
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should throw when not connected', async () => {
|
|
892
|
+
await client.disconnect();
|
|
893
|
+
|
|
894
|
+
await expect(
|
|
895
|
+
client.search(new SearchOptionsBuilder().build())
|
|
896
|
+
).rejects.toThrow('Not connected');
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ==========================================================================
|
|
901
|
+
// Index Management Tests
|
|
902
|
+
// ==========================================================================
|
|
903
|
+
|
|
904
|
+
describe('Index Management', () => {
|
|
905
|
+
beforeEach(async () => {
|
|
906
|
+
await client.connect();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it('should create HNSW index', async () => {
|
|
910
|
+
const options = new IndexOptionsBuilder('test_vectors', 'embedding')
|
|
911
|
+
.withType('hnsw')
|
|
912
|
+
.withHNSWParams(16, 200)
|
|
913
|
+
.withMetric('cosine')
|
|
914
|
+
.build();
|
|
915
|
+
|
|
916
|
+
await expect(client.createIndex(options)).resolves.toBeUndefined();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should create IVFFlat index', async () => {
|
|
920
|
+
const options = new IndexOptionsBuilder('test_vectors', 'embedding')
|
|
921
|
+
.withType('ivfflat')
|
|
922
|
+
.withIVFParams(100)
|
|
923
|
+
.withMetric('euclidean')
|
|
924
|
+
.build();
|
|
925
|
+
|
|
926
|
+
await expect(client.createIndex(options)).resolves.toBeUndefined();
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('should report index statistics', async () => {
|
|
930
|
+
const stats = await client.getIndexStats('idx_test_embedding');
|
|
931
|
+
|
|
932
|
+
expect(stats.indexName).toBe('idx_test_embedding');
|
|
933
|
+
expect(stats.indexType).toBeDefined();
|
|
934
|
+
expect(stats.numVectors).toBeGreaterThan(0);
|
|
935
|
+
expect(stats.sizeBytes).toBeGreaterThan(0);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('should handle concurrent index creation', async () => {
|
|
939
|
+
const options = new IndexOptionsBuilder('test_vectors', 'embedding')
|
|
940
|
+
.withType('hnsw')
|
|
941
|
+
.concurrent(true)
|
|
942
|
+
.build();
|
|
943
|
+
|
|
944
|
+
await expect(client.createIndex(options)).resolves.toBeUndefined();
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should list all indices', async () => {
|
|
948
|
+
const indices = await client.listIndices();
|
|
949
|
+
|
|
950
|
+
expect(indices.length).toBeGreaterThan(0);
|
|
951
|
+
indices.forEach((idx) => {
|
|
952
|
+
expect(idx.indexName).toBeDefined();
|
|
953
|
+
expect(idx.indexType).toBeDefined();
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('should list indices for specific table', async () => {
|
|
958
|
+
const indices = await client.listIndices('vectors');
|
|
959
|
+
|
|
960
|
+
expect(indices.length).toBeGreaterThan(0);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('should rebuild index', async () => {
|
|
964
|
+
const rebuiltHandler = vi.fn();
|
|
965
|
+
client.on('index:rebuilt', rebuiltHandler);
|
|
966
|
+
|
|
967
|
+
await client.rebuildIndex('idx_test');
|
|
968
|
+
|
|
969
|
+
expect(rebuiltHandler).toHaveBeenCalled();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should drop index', async () => {
|
|
973
|
+
const droppedHandler = vi.fn();
|
|
974
|
+
client.on('index:dropped', droppedHandler);
|
|
975
|
+
|
|
976
|
+
await client.dropIndex('idx_test');
|
|
977
|
+
|
|
978
|
+
expect(droppedHandler).toHaveBeenCalled();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('should emit index events', async () => {
|
|
982
|
+
const createdHandler = vi.fn();
|
|
983
|
+
client.on('index:created', createdHandler);
|
|
984
|
+
|
|
985
|
+
await client.createIndex(
|
|
986
|
+
new IndexOptionsBuilder('test', 'embedding')
|
|
987
|
+
.withType('hnsw')
|
|
988
|
+
.build()
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
expect(createdHandler).toHaveBeenCalled();
|
|
992
|
+
expect(createdHandler.mock.calls[0][0].data.indexType).toBe('hnsw');
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// ==========================================================================
|
|
997
|
+
// Transactions Tests
|
|
998
|
+
// ==========================================================================
|
|
999
|
+
|
|
1000
|
+
describe('Transactions', () => {
|
|
1001
|
+
beforeEach(async () => {
|
|
1002
|
+
await client.connect();
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('should commit successful transactions', async () => {
|
|
1006
|
+
const result = await client.transaction(async (tx) => {
|
|
1007
|
+
await tx.insert({
|
|
1008
|
+
tableName: 'test',
|
|
1009
|
+
vectors: [{ vector: randomVector(384) }],
|
|
1010
|
+
});
|
|
1011
|
+
await tx.update({
|
|
1012
|
+
tableName: 'test',
|
|
1013
|
+
id: '1',
|
|
1014
|
+
vector: randomVector(384),
|
|
1015
|
+
});
|
|
1016
|
+
return { success: true };
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
expect(result.committed).toBe(true);
|
|
1020
|
+
expect(result.data?.success).toBe(true);
|
|
1021
|
+
expect(result.queryCount).toBe(2);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it('should rollback on error', async () => {
|
|
1025
|
+
await expect(
|
|
1026
|
+
client.transaction(async (tx) => {
|
|
1027
|
+
await tx.insert({
|
|
1028
|
+
tableName: 'test',
|
|
1029
|
+
vectors: [{ vector: randomVector(384) }],
|
|
1030
|
+
});
|
|
1031
|
+
throw new Error('Simulated error');
|
|
1032
|
+
})
|
|
1033
|
+
).rejects.toThrow('Simulated error');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('should support savepoints', async () => {
|
|
1037
|
+
const result = await client.transaction(async (tx) => {
|
|
1038
|
+
await tx.insert({
|
|
1039
|
+
tableName: 'test',
|
|
1040
|
+
vectors: [{ id: 'sp-1', vector: randomVector(384) }],
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Simulated savepoint - actual implementation would be more complex
|
|
1044
|
+
try {
|
|
1045
|
+
await tx.insert({
|
|
1046
|
+
tableName: 'test',
|
|
1047
|
+
vectors: [{ id: 'sp-2', vector: randomVector(384) }],
|
|
1048
|
+
});
|
|
1049
|
+
} catch {
|
|
1050
|
+
// Rollback to savepoint would happen here
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return { completed: true };
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
expect(result.committed).toBe(true);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should handle isolation levels', async () => {
|
|
1060
|
+
const result = await client.transaction(async (tx) => {
|
|
1061
|
+
// In a real implementation, we would set isolation level here
|
|
1062
|
+
await tx.query('SELECT 1');
|
|
1063
|
+
return true;
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
expect(result.committed).toBe(true);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('should track transaction metrics', async () => {
|
|
1070
|
+
const result = await client.transaction(async (tx) => {
|
|
1071
|
+
for (let i = 0; i < 5; i++) {
|
|
1072
|
+
await tx.query('SELECT 1');
|
|
1073
|
+
}
|
|
1074
|
+
return 'done';
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
expect(result.transactionId).toBeDefined();
|
|
1078
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
1079
|
+
expect(result.queryCount).toBe(5);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// ==========================================================================
|
|
1084
|
+
// Performance Tests
|
|
1085
|
+
// ==========================================================================
|
|
1086
|
+
|
|
1087
|
+
describe('Performance', () => {
|
|
1088
|
+
beforeEach(async () => {
|
|
1089
|
+
await client.connect();
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('should achieve target insert throughput', async () => {
|
|
1093
|
+
const { result, durationMs } = await measureAsync(async () => {
|
|
1094
|
+
return client.insert(
|
|
1095
|
+
new InsertOptionsBuilder('perf_test')
|
|
1096
|
+
.addRandomVectors(100, 384)
|
|
1097
|
+
.build()
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(result.successful).toBe(100);
|
|
1102
|
+
expect(result.throughput).toBeGreaterThan(10); // At least 10 vectors/sec
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it('should achieve target query latency', async () => {
|
|
1106
|
+
const { durationMs } = await measureAsync(async () => {
|
|
1107
|
+
return client.search(
|
|
1108
|
+
new SearchOptionsBuilder(384).withK(10).build()
|
|
1109
|
+
);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
expect(durationMs).toBeLessThan(1000); // Less than 1 second
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it('should handle concurrent queries', async () => {
|
|
1116
|
+
const queries = Array.from({ length: 10 }, () =>
|
|
1117
|
+
client.search(new SearchOptionsBuilder(384).withK(5).build())
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
const results = await Promise.all(queries);
|
|
1121
|
+
|
|
1122
|
+
expect(results).toHaveLength(10);
|
|
1123
|
+
results.forEach((r) => {
|
|
1124
|
+
expect(r).toHaveLength(5);
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('should measure batch search performance', async () => {
|
|
1129
|
+
const { result, durationMs } = await measureAsync(async () => {
|
|
1130
|
+
return client.batchSearch({
|
|
1131
|
+
queries: randomVectors(20, 384),
|
|
1132
|
+
k: 10,
|
|
1133
|
+
metric: 'cosine',
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
expect(result.results).toHaveLength(20);
|
|
1138
|
+
expect(result.avgDurationMs).toBeGreaterThan(0);
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// ==========================================================================
|
|
1143
|
+
// Health and Stats Tests
|
|
1144
|
+
// ==========================================================================
|
|
1145
|
+
|
|
1146
|
+
describe('Health and Statistics', () => {
|
|
1147
|
+
beforeEach(async () => {
|
|
1148
|
+
await client.connect();
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('should report healthy status when connected', async () => {
|
|
1152
|
+
const health = await client.healthCheck();
|
|
1153
|
+
|
|
1154
|
+
expect(health.status).toBe('healthy');
|
|
1155
|
+
expect(health.components.database.healthy).toBe(true);
|
|
1156
|
+
expect(health.issues).toHaveLength(0);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('should report unhealthy status when disconnected', async () => {
|
|
1160
|
+
await client.disconnect();
|
|
1161
|
+
const health = await client.healthCheck();
|
|
1162
|
+
|
|
1163
|
+
expect(health.status).toBe('unhealthy');
|
|
1164
|
+
expect(health.issues.length).toBeGreaterThan(0);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('should return system statistics', async () => {
|
|
1168
|
+
const stats = await client.getStats();
|
|
1169
|
+
|
|
1170
|
+
expect(stats.version).toBeDefined();
|
|
1171
|
+
expect(stats.totalVectors).toBeGreaterThan(0);
|
|
1172
|
+
expect(stats.queryStats).toBeDefined();
|
|
1173
|
+
expect(stats.memoryStats).toBeDefined();
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it('should analyze tables', async () => {
|
|
1177
|
+
const analysis = await client.analyze('test_table');
|
|
1178
|
+
|
|
1179
|
+
expect(analysis.tableName).toBe('test_table');
|
|
1180
|
+
expect(analysis.numRows).toBeGreaterThan(0);
|
|
1181
|
+
expect(analysis.columnStats).toBeDefined();
|
|
1182
|
+
expect(analysis.recommendations).toBeDefined();
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('should perform vacuum operation', async () => {
|
|
1186
|
+
const vacuumHandler = vi.fn();
|
|
1187
|
+
client.on('admin:vacuum', vacuumHandler);
|
|
1188
|
+
|
|
1189
|
+
await client.vacuum('test_table');
|
|
1190
|
+
|
|
1191
|
+
expect(vacuumHandler).toHaveBeenCalled();
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// ==========================================================================
|
|
1196
|
+
// Event System Tests
|
|
1197
|
+
// ==========================================================================
|
|
1198
|
+
|
|
1199
|
+
describe('Event System', () => {
|
|
1200
|
+
it('should register and unregister event handlers', async () => {
|
|
1201
|
+
const handler = vi.fn();
|
|
1202
|
+
|
|
1203
|
+
const unsubscribe = client.on('connection:open', handler);
|
|
1204
|
+
|
|
1205
|
+
await client.connect();
|
|
1206
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
1207
|
+
|
|
1208
|
+
unsubscribe();
|
|
1209
|
+
await client.disconnect();
|
|
1210
|
+
await client.connect();
|
|
1211
|
+
|
|
1212
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it('should handle once events', async () => {
|
|
1216
|
+
const handler = vi.fn();
|
|
1217
|
+
|
|
1218
|
+
client.once('connection:open', handler);
|
|
1219
|
+
|
|
1220
|
+
await client.connect();
|
|
1221
|
+
await client.disconnect();
|
|
1222
|
+
await client.connect();
|
|
1223
|
+
|
|
1224
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('should remove all listeners', async () => {
|
|
1228
|
+
const handler1 = vi.fn();
|
|
1229
|
+
const handler2 = vi.fn();
|
|
1230
|
+
|
|
1231
|
+
client.on('connection:open', handler1);
|
|
1232
|
+
client.on('connection:close', handler2);
|
|
1233
|
+
|
|
1234
|
+
client.removeAllListeners();
|
|
1235
|
+
|
|
1236
|
+
await client.connect();
|
|
1237
|
+
await client.disconnect();
|
|
1238
|
+
|
|
1239
|
+
expect(handler1).not.toHaveBeenCalled();
|
|
1240
|
+
expect(handler2).not.toHaveBeenCalled();
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it('should remove listeners for specific event', async () => {
|
|
1244
|
+
const openHandler = vi.fn();
|
|
1245
|
+
const closeHandler = vi.fn();
|
|
1246
|
+
|
|
1247
|
+
client.on('connection:open', openHandler);
|
|
1248
|
+
client.on('connection:close', closeHandler);
|
|
1249
|
+
|
|
1250
|
+
client.removeAllListeners('connection:open');
|
|
1251
|
+
|
|
1252
|
+
await client.connect();
|
|
1253
|
+
await client.disconnect();
|
|
1254
|
+
|
|
1255
|
+
expect(openHandler).not.toHaveBeenCalled();
|
|
1256
|
+
expect(closeHandler).toHaveBeenCalled();
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// ==========================================================================
|
|
1261
|
+
// Advanced Features Tests
|
|
1262
|
+
// ==========================================================================
|
|
1263
|
+
|
|
1264
|
+
describe('Advanced Features', () => {
|
|
1265
|
+
beforeEach(async () => {
|
|
1266
|
+
await client.connect();
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
describe('Attention Mechanisms', () => {
|
|
1270
|
+
it('should compute multi-head attention', async () => {
|
|
1271
|
+
const seqLen = 10;
|
|
1272
|
+
const dim = 64;
|
|
1273
|
+
|
|
1274
|
+
const input = {
|
|
1275
|
+
query: randomVectors(seqLen, dim),
|
|
1276
|
+
key: randomVectors(seqLen, dim),
|
|
1277
|
+
value: randomVectors(seqLen, dim),
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
const config = {
|
|
1281
|
+
mechanism: 'multi_head',
|
|
1282
|
+
numHeads: 8,
|
|
1283
|
+
headDim: 64,
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const result = await client.computeAttention(input, config);
|
|
1287
|
+
|
|
1288
|
+
expect(result.output).toHaveLength(seqLen);
|
|
1289
|
+
expect(result.output[0]).toHaveLength(config.numHeads * config.headDim);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('should emit attention events', async () => {
|
|
1293
|
+
const handler = vi.fn();
|
|
1294
|
+
client.on('attention:computed', handler);
|
|
1295
|
+
|
|
1296
|
+
await client.computeAttention(
|
|
1297
|
+
{
|
|
1298
|
+
query: randomVectors(5, 32),
|
|
1299
|
+
key: randomVectors(5, 32),
|
|
1300
|
+
value: randomVectors(5, 32),
|
|
1301
|
+
},
|
|
1302
|
+
{ mechanism: 'multi_head', numHeads: 4, headDim: 32 }
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
expect(handler).toHaveBeenCalled();
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
describe('GNN Operations', () => {
|
|
1310
|
+
it('should execute GCN layer', async () => {
|
|
1311
|
+
const graph = createRandomGraph(50, 100, 64);
|
|
1312
|
+
|
|
1313
|
+
const result = await client.runGNNLayer(graph, {
|
|
1314
|
+
type: 'gcn',
|
|
1315
|
+
inputDim: 64,
|
|
1316
|
+
outputDim: 32,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
expect(result.nodeEmbeddings).toHaveLength(50);
|
|
1320
|
+
expect(result.nodeEmbeddings[0]).toHaveLength(32);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('should emit GNN events', async () => {
|
|
1324
|
+
const handler = vi.fn();
|
|
1325
|
+
client.on('gnn:forward', handler);
|
|
1326
|
+
|
|
1327
|
+
const graph = createRandomGraph(20, 40, 32);
|
|
1328
|
+
|
|
1329
|
+
await client.runGNNLayer(graph, {
|
|
1330
|
+
type: 'gcn',
|
|
1331
|
+
inputDim: 32,
|
|
1332
|
+
outputDim: 16,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
expect(handler).toHaveBeenCalled();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('should build graph from node features and edges', () => {
|
|
1339
|
+
const nodeFeatures = [
|
|
1340
|
+
[1, 2, 3],
|
|
1341
|
+
[4, 5, 6],
|
|
1342
|
+
[7, 8, 9],
|
|
1343
|
+
];
|
|
1344
|
+
const edges: [number, number][] = [
|
|
1345
|
+
[0, 1],
|
|
1346
|
+
[1, 2],
|
|
1347
|
+
[2, 0],
|
|
1348
|
+
];
|
|
1349
|
+
|
|
1350
|
+
const graph = client.buildGraph(nodeFeatures, edges);
|
|
1351
|
+
|
|
1352
|
+
expect(graph.nodeFeatures).toEqual(nodeFeatures);
|
|
1353
|
+
expect(graph.edgeIndex[0]).toEqual([0, 1, 2]);
|
|
1354
|
+
expect(graph.edgeIndex[1]).toEqual([1, 2, 0]);
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
describe('Hyperbolic Embeddings', () => {
|
|
1359
|
+
it('should embed points in Poincare ball', async () => {
|
|
1360
|
+
const config = {
|
|
1361
|
+
model: 'poincare',
|
|
1362
|
+
curvature: -1,
|
|
1363
|
+
dimension: 16,
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const input = {
|
|
1367
|
+
points: randomVectors(10, 16),
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const result = await client.hyperbolicEmbed(input, config);
|
|
1371
|
+
|
|
1372
|
+
expect(result.embeddings).toHaveLength(10);
|
|
1373
|
+
expect(result.curvature).toBe(-1);
|
|
1374
|
+
|
|
1375
|
+
// All points should be inside Poincare ball
|
|
1376
|
+
result.embeddings.forEach((emb) => {
|
|
1377
|
+
const norm = Math.sqrt(emb.reduce((sum, v) => sum + v * v, 0));
|
|
1378
|
+
expect(norm).toBeLessThan(1);
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it('should compute hyperbolic distance', async () => {
|
|
1383
|
+
const config = {
|
|
1384
|
+
model: 'poincare',
|
|
1385
|
+
curvature: -1,
|
|
1386
|
+
dimension: 8,
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const a = [0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2];
|
|
1390
|
+
const b = [0.3, 0.1, 0.3, 0.1, 0.3, 0.1, 0.3, 0.1];
|
|
1391
|
+
|
|
1392
|
+
const distance = await client.hyperbolicDistance(a, b, config);
|
|
1393
|
+
|
|
1394
|
+
expect(distance).toBeGreaterThanOrEqual(0);
|
|
1395
|
+
expect(Number.isFinite(distance)).toBe(true);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it('should emit hyperbolic events', async () => {
|
|
1399
|
+
const embedHandler = vi.fn();
|
|
1400
|
+
const distanceHandler = vi.fn();
|
|
1401
|
+
|
|
1402
|
+
client.on('hyperbolic:embed', embedHandler);
|
|
1403
|
+
client.on('hyperbolic:distance', distanceHandler);
|
|
1404
|
+
|
|
1405
|
+
const config = {
|
|
1406
|
+
model: 'poincare',
|
|
1407
|
+
curvature: -1,
|
|
1408
|
+
dimension: 8,
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
await client.hyperbolicEmbed(
|
|
1412
|
+
{ points: [[0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2]] },
|
|
1413
|
+
config
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
await client.hyperbolicDistance(
|
|
1417
|
+
[0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2],
|
|
1418
|
+
[0.3, 0.1, 0.3, 0.1, 0.3, 0.1, 0.3, 0.1],
|
|
1419
|
+
config
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
expect(embedHandler).toHaveBeenCalled();
|
|
1423
|
+
expect(distanceHandler).toHaveBeenCalled();
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
describe('Embedding Operations', () => {
|
|
1428
|
+
it('should embed single text', async () => {
|
|
1429
|
+
const result = await client.embed('Hello, world!');
|
|
1430
|
+
|
|
1431
|
+
expect(result.embedding).toBeDefined();
|
|
1432
|
+
expect(result.embedding).toHaveLength(384);
|
|
1433
|
+
expect(result.tokenCount).toBeGreaterThan(0);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('should embed batch of texts', async () => {
|
|
1437
|
+
const texts = ['First text', 'Second text', 'Third text'];
|
|
1438
|
+
|
|
1439
|
+
const result = await client.embedBatch(texts);
|
|
1440
|
+
|
|
1441
|
+
expect(result.embeddings).toHaveLength(3);
|
|
1442
|
+
expect(result.totalTokens).toBeGreaterThan(0);
|
|
1443
|
+
expect(result.throughput).toBeGreaterThan(0);
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// ============================================================================
|
|
1450
|
+
// Edge Cases and Error Handling Tests
|
|
1451
|
+
// ============================================================================
|
|
1452
|
+
|
|
1453
|
+
describe('Edge Cases and Error Handling', () => {
|
|
1454
|
+
let client: MockRuVectorClient;
|
|
1455
|
+
|
|
1456
|
+
beforeEach(async () => {
|
|
1457
|
+
client = new MockRuVectorClient(createTestClientOptions());
|
|
1458
|
+
await client.connect();
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
afterEach(async () => {
|
|
1462
|
+
await client.disconnect();
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
describe('Boundary Values', () => {
|
|
1466
|
+
it('should handle empty vector search (k=0)', async () => {
|
|
1467
|
+
const results = await client.search(
|
|
1468
|
+
new SearchOptionsBuilder(384).withK(0).build()
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
expect(results).toHaveLength(0);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
it('should handle large batch sizes', async () => {
|
|
1475
|
+
const result = await client.batchSearch({
|
|
1476
|
+
queries: randomVectors(100, 384),
|
|
1477
|
+
k: 10,
|
|
1478
|
+
metric: 'cosine',
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
expect(result.results).toHaveLength(100);
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
it('should handle high-dimensional vectors', async () => {
|
|
1485
|
+
const results = await client.search(
|
|
1486
|
+
new SearchOptionsBuilder(4096).withK(5).build()
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('should handle very small k values', async () => {
|
|
1493
|
+
const results = await client.search(
|
|
1494
|
+
new SearchOptionsBuilder(384).withK(1).build()
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
expect(results).toHaveLength(1);
|
|
1498
|
+
});
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
describe('Empty/Null Cases', () => {
|
|
1502
|
+
it('should handle empty batch insert', async () => {
|
|
1503
|
+
const result = await client.insert({
|
|
1504
|
+
tableName: 'test',
|
|
1505
|
+
vectors: [],
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
expect(result.total).toBe(0);
|
|
1509
|
+
expect(result.successful).toBe(0);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
it('should handle graph with no edges', async () => {
|
|
1513
|
+
const graph = client.buildGraph(
|
|
1514
|
+
[[1, 2, 3], [4, 5, 6]],
|
|
1515
|
+
[]
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
const result = await client.runGNNLayer(graph, {
|
|
1519
|
+
type: 'gcn',
|
|
1520
|
+
inputDim: 3,
|
|
1521
|
+
outputDim: 2,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
expect(result.nodeEmbeddings).toHaveLength(2);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
describe('Concurrent Operations', () => {
|
|
1529
|
+
it('should handle concurrent searches', async () => {
|
|
1530
|
+
const searches = Array.from({ length: 10 }, () =>
|
|
1531
|
+
client.search(new SearchOptionsBuilder(384).withK(5).build())
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
const results = await Promise.all(searches);
|
|
1535
|
+
|
|
1536
|
+
expect(results).toHaveLength(10);
|
|
1537
|
+
results.forEach((r) => {
|
|
1538
|
+
expect(r).toHaveLength(5);
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
it('should handle concurrent inserts', async () => {
|
|
1543
|
+
const inserts = Array.from({ length: 5 }, (_, i) =>
|
|
1544
|
+
client.insert({
|
|
1545
|
+
tableName: 'test',
|
|
1546
|
+
vectors: [
|
|
1547
|
+
{
|
|
1548
|
+
id: `concurrent-${i}`,
|
|
1549
|
+
vector: randomVector(384),
|
|
1550
|
+
},
|
|
1551
|
+
],
|
|
1552
|
+
})
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
const results = await Promise.all(inserts);
|
|
1556
|
+
|
|
1557
|
+
results.forEach((r) => {
|
|
1558
|
+
expect(r.successful).toBe(1);
|
|
1559
|
+
});
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
it('should handle mixed concurrent operations', async () => {
|
|
1563
|
+
const operations = [
|
|
1564
|
+
client.search(new SearchOptionsBuilder(384).withK(5).build()),
|
|
1565
|
+
client.insert({
|
|
1566
|
+
tableName: 'test',
|
|
1567
|
+
vectors: [{ vector: randomVector(384) }],
|
|
1568
|
+
}),
|
|
1569
|
+
client.search(new SearchOptionsBuilder(384).withK(3).build()),
|
|
1570
|
+
client.getStats(),
|
|
1571
|
+
client.healthCheck(),
|
|
1572
|
+
];
|
|
1573
|
+
|
|
1574
|
+
const results = await Promise.all(operations);
|
|
1575
|
+
|
|
1576
|
+
expect(results).toHaveLength(5);
|
|
1577
|
+
});
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// ============================================================================
|
|
1582
|
+
// Real Database Tests (Only run when configured)
|
|
1583
|
+
// ============================================================================
|
|
1584
|
+
|
|
1585
|
+
skipIfNoRealDB('Real Database Integration', () => {
|
|
1586
|
+
// These tests would run against a real PostgreSQL database
|
|
1587
|
+
// Implementation would be similar but using actual database connections
|
|
1588
|
+
|
|
1589
|
+
it('should connect to real PostgreSQL', async () => {
|
|
1590
|
+
const config = getTestDatabaseConfig();
|
|
1591
|
+
// Actual implementation would create real client and connect
|
|
1592
|
+
expect(config.host).toBeDefined();
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
it('should create actual HNSW index', async () => {
|
|
1596
|
+
// Would test real index creation
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
it('should perform actual vector search', async () => {
|
|
1600
|
+
// Would test real search with pgvector
|
|
1601
|
+
});
|
|
1602
|
+
});
|