cozo-memory 1.2.6 → 1.2.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 +64 -36
- package/dist/benchmark.js +410 -132
- package/dist/db-service.test.js +313 -0
- package/dist/export-import-service.js +9 -5
- package/dist/index.js +825 -10
- package/dist/logger.test.js +75 -0
- package/dist/memory-service.test.js +222 -0
- package/dist/timestamp-utils.test.js +68 -0
- package/package.json +6 -3
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const db_service_1 = require("./db-service");
|
|
4
|
+
describe("DatabaseService", () => {
|
|
5
|
+
let db;
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
db = new db_service_1.DatabaseService(":memory:", "sqlite");
|
|
8
|
+
await db.initialize();
|
|
9
|
+
});
|
|
10
|
+
// ── Entity CRUD ──────────────────────────────────────────────
|
|
11
|
+
describe("Entity CRUD", () => {
|
|
12
|
+
const sampleEntity = {
|
|
13
|
+
id: "e1",
|
|
14
|
+
name: "Test Entity",
|
|
15
|
+
type: "Test",
|
|
16
|
+
embedding: [0.1, 0.2, 0.3],
|
|
17
|
+
name_embedding: [0.4, 0.5, 0.6],
|
|
18
|
+
metadata: { key: "value" },
|
|
19
|
+
created_at: 1000,
|
|
20
|
+
};
|
|
21
|
+
it("should create and get an entity", async () => {
|
|
22
|
+
await db.createEntity(sampleEntity);
|
|
23
|
+
const result = await db.getEntity("e1");
|
|
24
|
+
expect(result).not.toBeNull();
|
|
25
|
+
expect(result.name).toBe("Test Entity");
|
|
26
|
+
expect(result.type).toBe("Test");
|
|
27
|
+
expect(result.embedding).toEqual([0.1, 0.2, 0.3]);
|
|
28
|
+
expect(result.metadata).toEqual({ key: "value" });
|
|
29
|
+
});
|
|
30
|
+
it("should return null for non-existent entity", async () => {
|
|
31
|
+
const result = await db.getEntity("non_existent");
|
|
32
|
+
expect(result).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it("should update an entity partially", async () => {
|
|
35
|
+
await db.createEntity(sampleEntity);
|
|
36
|
+
await db.updateEntity("e1", { name: "Updated Entity", metadata: { new: "meta" } });
|
|
37
|
+
const result = await db.getEntity("e1");
|
|
38
|
+
expect(result).not.toBeNull();
|
|
39
|
+
expect(result.name).toBe("Updated Entity");
|
|
40
|
+
// metadata should be merged
|
|
41
|
+
expect(result.metadata).toEqual({ key: "value", new: "meta" });
|
|
42
|
+
});
|
|
43
|
+
it("should update embedding fields", async () => {
|
|
44
|
+
await db.createEntity(sampleEntity);
|
|
45
|
+
const newEmbedding = [0.9, 0.8, 0.7];
|
|
46
|
+
await db.updateEntity("e1", { embedding: newEmbedding });
|
|
47
|
+
const result = await db.getEntity("e1");
|
|
48
|
+
expect(result.embedding).toEqual(newEmbedding);
|
|
49
|
+
// name_embedding should remain unchanged
|
|
50
|
+
expect(result.name_embedding).toEqual([0.4, 0.5, 0.6]);
|
|
51
|
+
});
|
|
52
|
+
it("should not throw when updating non-existent entity", async () => {
|
|
53
|
+
await expect(db.updateEntity("ghost", { name: "X" })).resolves.not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
it("should delete an entity and its observations", async () => {
|
|
56
|
+
await db.createEntity(sampleEntity);
|
|
57
|
+
await db.addObservation({
|
|
58
|
+
id: "obs1",
|
|
59
|
+
entity_id: "e1",
|
|
60
|
+
text: "test",
|
|
61
|
+
embedding: [],
|
|
62
|
+
metadata: {},
|
|
63
|
+
created_at: 1001,
|
|
64
|
+
});
|
|
65
|
+
await db.deleteEntity("e1");
|
|
66
|
+
const entity = await db.getEntity("e1");
|
|
67
|
+
expect(entity).toBeNull();
|
|
68
|
+
// Observation should also be deleted
|
|
69
|
+
const obs = await db.getObservationsForEntity("e1");
|
|
70
|
+
expect(obs).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// ── Observations ─────────────────────────────────────────────
|
|
74
|
+
describe("Observation CRUD", () => {
|
|
75
|
+
beforeEach(async () => {
|
|
76
|
+
await db.createEntity({
|
|
77
|
+
id: "e2",
|
|
78
|
+
name: "Entity With Obs",
|
|
79
|
+
type: "Test",
|
|
80
|
+
embedding: [],
|
|
81
|
+
name_embedding: [],
|
|
82
|
+
metadata: {},
|
|
83
|
+
created_at: 2000,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it("should add and retrieve observations for an entity", async () => {
|
|
87
|
+
await db.addObservation({
|
|
88
|
+
id: "obs1",
|
|
89
|
+
entity_id: "e2",
|
|
90
|
+
text: "First observation",
|
|
91
|
+
embedding: [1.0, 2.0],
|
|
92
|
+
metadata: { source: "test" },
|
|
93
|
+
created_at: 2001,
|
|
94
|
+
});
|
|
95
|
+
await db.addObservation({
|
|
96
|
+
id: "obs2",
|
|
97
|
+
entity_id: "e2",
|
|
98
|
+
text: "Second observation",
|
|
99
|
+
embedding: [3.0, 4.0],
|
|
100
|
+
metadata: {},
|
|
101
|
+
created_at: 2002,
|
|
102
|
+
});
|
|
103
|
+
const obs = await db.getObservationsForEntity("e2");
|
|
104
|
+
expect(obs).toHaveLength(2);
|
|
105
|
+
expect(obs[0].text).toBe("First observation");
|
|
106
|
+
expect(obs[1].text).toBe("Second observation");
|
|
107
|
+
});
|
|
108
|
+
it("should return empty array for entity with no observations", async () => {
|
|
109
|
+
const obs = await db.getObservationsForEntity("e2");
|
|
110
|
+
expect(obs).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
it("should return empty array for non-existent entity", async () => {
|
|
113
|
+
const obs = await db.getObservationsForEntity("ghost");
|
|
114
|
+
expect(obs).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ── Relationships ────────────────────────────────────────────
|
|
118
|
+
describe("Relationship CRUD", () => {
|
|
119
|
+
beforeEach(async () => {
|
|
120
|
+
for (const id of ["a", "b", "c"]) {
|
|
121
|
+
await db.createEntity({
|
|
122
|
+
id,
|
|
123
|
+
name: `Entity ${id}`,
|
|
124
|
+
type: "Test",
|
|
125
|
+
embedding: [],
|
|
126
|
+
name_embedding: [],
|
|
127
|
+
metadata: {},
|
|
128
|
+
created_at: 3000,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it("should create and list all relations", async () => {
|
|
133
|
+
await db.createRelation({
|
|
134
|
+
from_id: "a", to_id: "b", relation_type: "knows",
|
|
135
|
+
strength: 0.8, metadata: {}, created_at: 3001,
|
|
136
|
+
});
|
|
137
|
+
await db.createRelation({
|
|
138
|
+
from_id: "b", to_id: "c", relation_type: "likes",
|
|
139
|
+
strength: 1.0, metadata: { since: "2024" }, created_at: 3002,
|
|
140
|
+
});
|
|
141
|
+
const all = await db.getRelations();
|
|
142
|
+
expect(all).toHaveLength(2);
|
|
143
|
+
});
|
|
144
|
+
it("should filter relations by from_id", async () => {
|
|
145
|
+
await db.createRelation({
|
|
146
|
+
from_id: "a", to_id: "b", relation_type: "knows",
|
|
147
|
+
strength: 0.5, metadata: {}, created_at: 3001,
|
|
148
|
+
});
|
|
149
|
+
await db.createRelation({
|
|
150
|
+
from_id: "a", to_id: "c", relation_type: "knows",
|
|
151
|
+
strength: 0.3, metadata: {}, created_at: 3002,
|
|
152
|
+
});
|
|
153
|
+
await db.createRelation({
|
|
154
|
+
from_id: "b", to_id: "c", relation_type: "likes",
|
|
155
|
+
strength: 0.9, metadata: {}, created_at: 3003,
|
|
156
|
+
});
|
|
157
|
+
const fromA = await db.getRelations("a");
|
|
158
|
+
expect(fromA).toHaveLength(2);
|
|
159
|
+
const fromB = await db.getRelations("b");
|
|
160
|
+
expect(fromB).toHaveLength(1);
|
|
161
|
+
expect(fromB[0].to_id).toBe("c");
|
|
162
|
+
});
|
|
163
|
+
it("should filter relations by to_id", async () => {
|
|
164
|
+
await db.createRelation({
|
|
165
|
+
from_id: "a", to_id: "c", relation_type: "knows",
|
|
166
|
+
strength: 0.5, metadata: {}, created_at: 3001,
|
|
167
|
+
});
|
|
168
|
+
await db.createRelation({
|
|
169
|
+
from_id: "b", to_id: "c", relation_type: "likes",
|
|
170
|
+
strength: 0.9, metadata: {}, created_at: 3002,
|
|
171
|
+
});
|
|
172
|
+
const toC = await db.getRelations(undefined, "c");
|
|
173
|
+
expect(toC).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
it("should delete entity and cascade its relations", async () => {
|
|
176
|
+
await db.createRelation({
|
|
177
|
+
from_id: "a", to_id: "b", relation_type: "knows",
|
|
178
|
+
strength: 0.5, metadata: {}, created_at: 3001,
|
|
179
|
+
});
|
|
180
|
+
await db.deleteEntity("a");
|
|
181
|
+
const all = await db.getRelations();
|
|
182
|
+
expect(all).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// ── Vector Search ────────────────────────────────────────────
|
|
186
|
+
describe("Vector Search", () => {
|
|
187
|
+
beforeEach(async () => {
|
|
188
|
+
const entities = [
|
|
189
|
+
{ id: "vec1", name: "Cat", embedding: [1.0, 0.0, 0.0] },
|
|
190
|
+
{ id: "vec2", name: "Dog", embedding: [0.0, 1.0, 0.0] },
|
|
191
|
+
{ id: "vec3", name: "Fish", embedding: [0.0, 0.0, 1.0] },
|
|
192
|
+
];
|
|
193
|
+
for (const e of entities) {
|
|
194
|
+
await db.createEntity({
|
|
195
|
+
id: e.id, name: e.name, type: "Animal",
|
|
196
|
+
embedding: e.embedding, name_embedding: e.embedding,
|
|
197
|
+
metadata: {}, created_at: 4000,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
it("should find closest entity by cosine similarity", async () => {
|
|
202
|
+
// Query vector closest to [1, 0, 0] → "Cat"
|
|
203
|
+
const results = await db.vectorSearchEntity([0.9, 0.1, 0.0], 1);
|
|
204
|
+
expect(results).toHaveLength(1);
|
|
205
|
+
expect(results[0][0]).toBe("vec1"); // id
|
|
206
|
+
expect(results[0][4]).toBeGreaterThan(0.9); // score
|
|
207
|
+
});
|
|
208
|
+
it("should return correct limit", async () => {
|
|
209
|
+
const results = await db.vectorSearchEntity([0.5, 0.5, 0.5], 2);
|
|
210
|
+
expect(results).toHaveLength(2);
|
|
211
|
+
});
|
|
212
|
+
it("should handle empty query vector gracefully (returns zero-score results)", async () => {
|
|
213
|
+
const results = await db.vectorSearchEntity([], 10);
|
|
214
|
+
// Empty vector produces cosine=0 for all entities, so all are returned with score 0
|
|
215
|
+
expect(results).toHaveLength(3);
|
|
216
|
+
expect(results[0][4]).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ── Full-Text Search ─────────────────────────────────────────
|
|
220
|
+
describe("Full-Text Search", () => {
|
|
221
|
+
beforeEach(async () => {
|
|
222
|
+
await db.createEntity({
|
|
223
|
+
id: "fts1", name: "Alice Wonderland", type: "Person",
|
|
224
|
+
embedding: [], name_embedding: [], metadata: {}, created_at: 5000,
|
|
225
|
+
});
|
|
226
|
+
await db.createEntity({
|
|
227
|
+
id: "fts2", name: "Bob The Builder", type: "Person",
|
|
228
|
+
embedding: [], name_embedding: [], metadata: {}, created_at: 5001,
|
|
229
|
+
});
|
|
230
|
+
await db.addObservation({
|
|
231
|
+
id: "fobs1", entity_id: "fts1",
|
|
232
|
+
text: "Alice lives in a wonderland of dreams",
|
|
233
|
+
embedding: [], metadata: {}, created_at: 5002,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it("should find entity by name substring", async () => {
|
|
237
|
+
const results = await db.fullTextSearchEntity("alice");
|
|
238
|
+
expect(results).toHaveLength(1);
|
|
239
|
+
expect(results[0][0]).toBe("fts1");
|
|
240
|
+
});
|
|
241
|
+
it("should be case-insensitive", async () => {
|
|
242
|
+
const results = await db.fullTextSearchEntity("BOB");
|
|
243
|
+
expect(results).toHaveLength(1);
|
|
244
|
+
expect(results[0][0]).toBe("fts2");
|
|
245
|
+
});
|
|
246
|
+
it("should find observation by text substring", async () => {
|
|
247
|
+
const results = await db.fullTextSearchObservation("wonderland");
|
|
248
|
+
expect(results).toHaveLength(1);
|
|
249
|
+
expect(results[0][1]).toBe("fts1");
|
|
250
|
+
});
|
|
251
|
+
it("should return empty array for no match", async () => {
|
|
252
|
+
const results = await db.fullTextSearchEntity("nobody");
|
|
253
|
+
expect(results).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
// ── Export & Stats ───────────────────────────────────────────
|
|
257
|
+
describe("Export & Stats", () => {
|
|
258
|
+
it("should export empty database", async () => {
|
|
259
|
+
const exported = await db.exportRelations();
|
|
260
|
+
expect(exported).toHaveProperty("entity");
|
|
261
|
+
expect(exported).toHaveProperty("observation");
|
|
262
|
+
expect(exported).toHaveProperty("relationship");
|
|
263
|
+
expect(exported.entity).toHaveLength(0);
|
|
264
|
+
expect(exported.observation).toHaveLength(0);
|
|
265
|
+
expect(exported.relationship).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
it("should export correct counts", async () => {
|
|
268
|
+
await db.createEntity({
|
|
269
|
+
id: "exp1", name: "Export Test", type: "Test",
|
|
270
|
+
embedding: [], name_embedding: [], metadata: {}, created_at: 6000,
|
|
271
|
+
});
|
|
272
|
+
await db.addObservation({
|
|
273
|
+
id: "exp_obs1", entity_id: "exp1", text: "Obs",
|
|
274
|
+
embedding: [], metadata: {}, created_at: 6001,
|
|
275
|
+
});
|
|
276
|
+
await db.createRelation({
|
|
277
|
+
from_id: "exp1", to_id: "exp1", relation_type: "self",
|
|
278
|
+
strength: 1.0, metadata: {}, created_at: 6002,
|
|
279
|
+
});
|
|
280
|
+
const exported = await db.exportRelations();
|
|
281
|
+
expect(exported.entity).toHaveLength(1);
|
|
282
|
+
expect(exported.observation).toHaveLength(1);
|
|
283
|
+
expect(exported.relationship).toHaveLength(1);
|
|
284
|
+
});
|
|
285
|
+
it("should return correct stats", async () => {
|
|
286
|
+
await db.createEntity({
|
|
287
|
+
id: "stat1", name: "Stats", type: "T",
|
|
288
|
+
embedding: [], name_embedding: [], metadata: {}, created_at: 7000,
|
|
289
|
+
});
|
|
290
|
+
const stats = await db.getStats();
|
|
291
|
+
expect(stats.entities).toBe(1);
|
|
292
|
+
expect(stats.observations).toBe(0);
|
|
293
|
+
expect(stats.relationships).toBe(0);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
297
|
+
describe("Lifecycle", () => {
|
|
298
|
+
it("should initialize without error", async () => {
|
|
299
|
+
await expect(db.initialize()).resolves.not.toThrow();
|
|
300
|
+
});
|
|
301
|
+
it("should close without error", async () => {
|
|
302
|
+
await expect(db.close()).resolves.not.toThrow();
|
|
303
|
+
});
|
|
304
|
+
it("should backup and restore without error", async () => {
|
|
305
|
+
await expect(db.backup("/tmp/test_backup.cozo")).resolves.not.toThrow();
|
|
306
|
+
await expect(db.restore("/tmp/test_backup.cozo")).resolves.not.toThrow();
|
|
307
|
+
});
|
|
308
|
+
it("should run a query without error", async () => {
|
|
309
|
+
const result = await db.runQuery("SELECT 1");
|
|
310
|
+
expect(result).toEqual({ rows: [] });
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -7,8 +7,10 @@ exports.ExportImportService = void 0;
|
|
|
7
7
|
const archiver_1 = __importDefault(require("archiver"));
|
|
8
8
|
class ExportImportService {
|
|
9
9
|
dbService;
|
|
10
|
-
|
|
10
|
+
embeddingDim;
|
|
11
|
+
constructor(dbService, embeddingDim = 1024) {
|
|
11
12
|
this.dbService = dbService;
|
|
13
|
+
this.embeddingDim = embeddingDim;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Export memory to various formats
|
|
@@ -418,7 +420,7 @@ class ExportImportService {
|
|
|
418
420
|
}
|
|
419
421
|
async createEntityWithId(id, name, type, metadata) {
|
|
420
422
|
const now = Date.now() * 1000;
|
|
421
|
-
const zeroVec = new Array(
|
|
423
|
+
const zeroVec = new Array(this.embeddingDim).fill(0);
|
|
422
424
|
// Escape strings properly for CozoDB
|
|
423
425
|
const escapedName = name.replace(/"/g, '\\"');
|
|
424
426
|
const escapedType = type.replace(/"/g, '\\"');
|
|
@@ -442,14 +444,16 @@ class ExportImportService {
|
|
|
442
444
|
}
|
|
443
445
|
async createObservationWithId(id, entityId, text, metadata) {
|
|
444
446
|
const now = Date.now() * 1000;
|
|
445
|
-
const zeroVec = new Array(
|
|
447
|
+
const zeroVec = new Array(this.embeddingDim).fill(0);
|
|
446
448
|
const escapedText = text.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
447
449
|
await this.dbService.run(`
|
|
448
|
-
?[id, entity_id, text, embedding, metadata, created_at] <- [[$id, $entity_id, $text, $embedding, $metadata, [${now}, true]]]
|
|
449
|
-
:insert observation {id, entity_id, text, embedding, metadata, created_at}
|
|
450
|
+
?[id, entity_id, session_id, task_id, text, embedding, metadata, created_at] <- [[$id, $entity_id, $session_id, $task_id, $text, $embedding, $metadata, [${now}, true]]]
|
|
451
|
+
:insert observation {id, entity_id, session_id, task_id, text, embedding, metadata, created_at}
|
|
450
452
|
`, {
|
|
451
453
|
id,
|
|
452
454
|
entity_id: entityId,
|
|
455
|
+
session_id: "",
|
|
456
|
+
task_id: "",
|
|
453
457
|
text: escapedText,
|
|
454
458
|
embedding: zeroVec,
|
|
455
459
|
metadata: metadata || {}
|