@kiyeonjeon21/datacontext 0.3.2 → 0.4.0
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/dist/adapters/sqlite.d.ts.map +1 -1
- package/dist/adapters/sqlite.js +13 -0
- package/dist/adapters/sqlite.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +115 -0
- package/dist/api/server.js.map +1 -1
- package/dist/cli/index.js +58 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/core/context-service.d.ts +63 -0
- package/dist/core/context-service.d.ts.map +1 -1
- package/dist/core/context-service.js +66 -0
- package/dist/core/context-service.js.map +1 -1
- package/dist/core/harvester.d.ts +57 -5
- package/dist/core/harvester.d.ts.map +1 -1
- package/dist/core/harvester.js +86 -6
- package/dist/core/harvester.js.map +1 -1
- package/dist/core/types.d.ts +21 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge/store.d.ts +186 -3
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +389 -5
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +252 -4
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +138 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +231 -3
- package/dist/mcp/tools.js.map +1 -1
- package/docs/KNOWLEDGE_GRAPH.md +540 -0
- package/docs/KNOWLEDGE_TYPES.md +261 -0
- package/docs/MULTI_DB_ARCHITECTURE.md +319 -0
- package/package.json +1 -1
- package/scripts/create-sqlite-testdb.sh +75 -0
- package/scripts/test-databases.sh +324 -0
- package/sqlite:./test-sqlite.db +0 -0
- package/src/adapters/sqlite.ts +16 -0
- package/src/api/server.ts +134 -0
- package/src/cli/index.ts +57 -16
- package/src/core/context-service.ts +70 -0
- package/src/core/harvester.ts +120 -8
- package/src/core/types.ts +21 -5
- package/src/index.ts +19 -1
- package/src/knowledge/store.ts +480 -6
- package/src/knowledge/types.ts +321 -4
- package/src/mcp/tools.ts +273 -3
- package/test-sqlite.db +0 -0
- package/tests/knowledge-store.test.ts +130 -0
package/src/knowledge/store.ts
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Knowledge Store
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Local JSON-based storage for the Knowledge Graph, including:
|
|
5
|
+
* - Table descriptions (nodes)
|
|
6
|
+
* - Table relationships (edges)
|
|
7
|
+
* - Query examples, business rules, and terms (annotations)
|
|
8
|
+
*
|
|
9
|
+
* ## Architecture
|
|
10
|
+
*
|
|
11
|
+
* The Knowledge Store implements a lightweight graph model:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
15
|
+
* │ Knowledge Store │
|
|
16
|
+
* │ │
|
|
17
|
+
* │ Nodes: │
|
|
18
|
+
* │ - TableDescription (with embedded columns) │
|
|
19
|
+
* │ │
|
|
20
|
+
* │ Edges: │
|
|
21
|
+
* │ - TableRelationship (from → to with join condition) │
|
|
22
|
+
* │ │
|
|
23
|
+
* │ Annotations: │
|
|
24
|
+
* │ - QueryExample, BusinessRule, BusinessTerm │
|
|
25
|
+
* │ │
|
|
26
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Graph Traversal
|
|
30
|
+
*
|
|
31
|
+
* Provides methods for:
|
|
32
|
+
* - Finding related tables (1-hop)
|
|
33
|
+
* - Finding shortest join path (multi-hop BFS)
|
|
34
|
+
* - Detecting relationship types
|
|
35
|
+
*
|
|
36
|
+
* ## Future: SQLite Migration
|
|
37
|
+
*
|
|
38
|
+
* For larger graphs, consider migrating to SQLite embedded storage
|
|
39
|
+
* for better indexing and query performance.
|
|
40
|
+
*
|
|
41
|
+
* @module knowledge/store
|
|
4
42
|
*/
|
|
5
43
|
|
|
6
44
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
@@ -10,16 +48,27 @@ import { homedir } from 'os';
|
|
|
10
48
|
import type {
|
|
11
49
|
KnowledgeData,
|
|
12
50
|
TableDescription,
|
|
51
|
+
TableRelationship,
|
|
52
|
+
TableRef,
|
|
13
53
|
QueryExample,
|
|
14
54
|
BusinessRule,
|
|
15
55
|
BusinessTerm,
|
|
16
56
|
KnowledgeEntry,
|
|
17
57
|
ColumnDescription,
|
|
18
58
|
TermCategory,
|
|
59
|
+
RelationshipType,
|
|
60
|
+
RelationshipCardinality,
|
|
61
|
+
} from './types.js';
|
|
62
|
+
import {
|
|
63
|
+
generateId,
|
|
64
|
+
createKnowledgeMeta,
|
|
65
|
+
createTableRef,
|
|
66
|
+
tableRefToString,
|
|
67
|
+
tableRefsEqual,
|
|
19
68
|
} from './types.js';
|
|
20
|
-
import { generateId, createKnowledgeMeta } from './types.js';
|
|
21
69
|
|
|
22
|
-
|
|
70
|
+
/** Current knowledge format version */
|
|
71
|
+
const KNOWLEDGE_VERSION = '2.0.0';
|
|
23
72
|
|
|
24
73
|
export class KnowledgeStore {
|
|
25
74
|
private dataPath: string;
|
|
@@ -42,7 +91,9 @@ export class KnowledgeStore {
|
|
|
42
91
|
}
|
|
43
92
|
|
|
44
93
|
/**
|
|
45
|
-
* Load knowledge data from disk
|
|
94
|
+
* Load knowledge data from disk.
|
|
95
|
+
*
|
|
96
|
+
* Includes automatic migration for older data formats.
|
|
46
97
|
*/
|
|
47
98
|
async load(): Promise<void> {
|
|
48
99
|
if (existsSync(this.dataPath)) {
|
|
@@ -50,10 +101,18 @@ export class KnowledgeStore {
|
|
|
50
101
|
const content = await readFile(this.dataPath, 'utf-8');
|
|
51
102
|
const parsed = JSON.parse(content) as KnowledgeData;
|
|
52
103
|
|
|
53
|
-
// Migration: ensure
|
|
104
|
+
// Migration: ensure arrays exist (for old data files)
|
|
54
105
|
if (!parsed.businessTerms) {
|
|
55
106
|
parsed.businessTerms = [];
|
|
56
107
|
}
|
|
108
|
+
if (!parsed.tableRelationships) {
|
|
109
|
+
parsed.tableRelationships = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Update version if migrating
|
|
113
|
+
if (parsed.version !== KNOWLEDGE_VERSION) {
|
|
114
|
+
parsed.version = KNOWLEDGE_VERSION;
|
|
115
|
+
}
|
|
57
116
|
|
|
58
117
|
this.data = parsed;
|
|
59
118
|
} catch (error) {
|
|
@@ -224,6 +283,345 @@ export class KnowledgeStore {
|
|
|
224
283
|
await this.save();
|
|
225
284
|
}
|
|
226
285
|
|
|
286
|
+
// ============ Table Relationships (Graph Edges) ============
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get all table relationships.
|
|
290
|
+
*
|
|
291
|
+
* @returns All relationships in the knowledge store
|
|
292
|
+
*/
|
|
293
|
+
getRelationships(): TableRelationship[] {
|
|
294
|
+
return this.data?.tableRelationships ?? [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get tables related to a given table (1-hop traversal).
|
|
299
|
+
*
|
|
300
|
+
* Returns all relationships where the given table is either
|
|
301
|
+
* the source (from) or target (to).
|
|
302
|
+
*
|
|
303
|
+
* @param tableName - Table name to find relationships for
|
|
304
|
+
* @param schema - Schema name (defaults to 'public')
|
|
305
|
+
* @returns Array of relationships involving this table
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const related = store.getRelatedTables('orders', 'public');
|
|
310
|
+
* // Returns relationships like:
|
|
311
|
+
* // - orders → users (FK: orders.user_id → users.id)
|
|
312
|
+
* // - orders → products (FK: orders.product_id → products.id)
|
|
313
|
+
* // - order_items → orders (FK: order_items.order_id → orders.id)
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
getRelatedTables(tableName: string, schema: string = 'public'): TableRelationship[] {
|
|
317
|
+
return this.data?.tableRelationships.filter(
|
|
318
|
+
rel => (rel.from.table === tableName && rel.from.schema === schema) ||
|
|
319
|
+
(rel.to.table === tableName && rel.to.schema === schema)
|
|
320
|
+
) ?? [];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get outgoing relationships from a table (where table is the source).
|
|
325
|
+
*
|
|
326
|
+
* @param tableName - Source table name
|
|
327
|
+
* @param schema - Schema name
|
|
328
|
+
* @returns Relationships where this table is the 'from' side
|
|
329
|
+
*/
|
|
330
|
+
getOutgoingRelationships(tableName: string, schema: string = 'public'): TableRelationship[] {
|
|
331
|
+
return this.data?.tableRelationships.filter(
|
|
332
|
+
rel => rel.from.table === tableName && rel.from.schema === schema
|
|
333
|
+
) ?? [];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get incoming relationships to a table (where table is the target).
|
|
338
|
+
*
|
|
339
|
+
* @param tableName - Target table name
|
|
340
|
+
* @param schema - Schema name
|
|
341
|
+
* @returns Relationships where this table is the 'to' side
|
|
342
|
+
*/
|
|
343
|
+
getIncomingRelationships(tableName: string, schema: string = 'public'): TableRelationship[] {
|
|
344
|
+
return this.data?.tableRelationships.filter(
|
|
345
|
+
rel => rel.to.table === tableName && rel.to.schema === schema
|
|
346
|
+
) ?? [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Find the shortest join path between two tables.
|
|
351
|
+
*
|
|
352
|
+
* Uses BFS (Breadth-First Search) to find the shortest path
|
|
353
|
+
* through the relationship graph.
|
|
354
|
+
*
|
|
355
|
+
* @param fromTable - Starting table name
|
|
356
|
+
* @param toTable - Destination table name
|
|
357
|
+
* @param schema - Schema name (defaults to 'public')
|
|
358
|
+
* @returns Array of relationships forming the path, or empty if no path exists
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* const path = store.findJoinPath('order_items', 'customers');
|
|
363
|
+
* // Returns path like:
|
|
364
|
+
* // [
|
|
365
|
+
* // { from: 'order_items', to: 'orders', joinCondition: '...' },
|
|
366
|
+
* // { from: 'orders', to: 'customers', joinCondition: '...' }
|
|
367
|
+
* // ]
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
findJoinPath(
|
|
371
|
+
fromTable: string,
|
|
372
|
+
toTable: string,
|
|
373
|
+
schema: string = 'public'
|
|
374
|
+
): TableRelationship[] {
|
|
375
|
+
if (fromTable === toTable) return [];
|
|
376
|
+
|
|
377
|
+
const visited = new Set<string>();
|
|
378
|
+
const queue: Array<{ table: string; path: TableRelationship[] }> = [
|
|
379
|
+
{ table: fromTable, path: [] }
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
while (queue.length > 0) {
|
|
383
|
+
const { table, path } = queue.shift()!;
|
|
384
|
+
const tableKey = `${schema}.${table}`;
|
|
385
|
+
|
|
386
|
+
if (visited.has(tableKey)) continue;
|
|
387
|
+
visited.add(tableKey);
|
|
388
|
+
|
|
389
|
+
// Get all relationships for current table
|
|
390
|
+
const relationships = this.getRelatedTables(table, schema);
|
|
391
|
+
|
|
392
|
+
for (const rel of relationships) {
|
|
393
|
+
// Determine the next table in the path
|
|
394
|
+
const isFromCurrent = rel.from.table === table && rel.from.schema === schema;
|
|
395
|
+
const nextTable = isFromCurrent ? rel.to.table : rel.from.table;
|
|
396
|
+
const nextSchema = isFromCurrent ? rel.to.schema : rel.from.schema;
|
|
397
|
+
|
|
398
|
+
// Skip if we've already visited
|
|
399
|
+
if (visited.has(`${nextSchema}.${nextTable}`)) continue;
|
|
400
|
+
|
|
401
|
+
const newPath = [...path, rel];
|
|
402
|
+
|
|
403
|
+
// Found the destination
|
|
404
|
+
if (nextTable === toTable && nextSchema === schema) {
|
|
405
|
+
return newPath;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Add to queue for further exploration
|
|
409
|
+
queue.push({ table: nextTable, path: newPath });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// No path found
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get the relationship between two specific tables (if it exists).
|
|
419
|
+
*
|
|
420
|
+
* @param fromTable - Source table
|
|
421
|
+
* @param toTable - Target table
|
|
422
|
+
* @param schema - Schema name
|
|
423
|
+
* @returns Relationship if found, undefined otherwise
|
|
424
|
+
*/
|
|
425
|
+
getRelationshipBetween(
|
|
426
|
+
fromTable: string,
|
|
427
|
+
toTable: string,
|
|
428
|
+
schema: string = 'public'
|
|
429
|
+
): TableRelationship | undefined {
|
|
430
|
+
return this.data?.tableRelationships.find(
|
|
431
|
+
rel =>
|
|
432
|
+
(rel.from.table === fromTable && rel.to.table === toTable &&
|
|
433
|
+
rel.from.schema === schema && rel.to.schema === schema) ||
|
|
434
|
+
(rel.from.table === toTable && rel.to.table === fromTable &&
|
|
435
|
+
rel.from.schema === schema && rel.to.schema === schema)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Add a table relationship.
|
|
441
|
+
*
|
|
442
|
+
* @param relationship - Complete relationship object
|
|
443
|
+
* @returns The added relationship
|
|
444
|
+
*/
|
|
445
|
+
async addRelationship(relationship: TableRelationship): Promise<TableRelationship> {
|
|
446
|
+
if (!this.data) {
|
|
447
|
+
throw new Error('Knowledge store not loaded');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Initialize if needed (migration)
|
|
451
|
+
if (!this.data.tableRelationships) {
|
|
452
|
+
this.data.tableRelationships = [];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Check for duplicates (same from/to tables)
|
|
456
|
+
const existingIndex = this.data.tableRelationships.findIndex(
|
|
457
|
+
r => tableRefsEqual(r.from, relationship.from) &&
|
|
458
|
+
tableRefsEqual(r.to, relationship.to)
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (existingIndex >= 0) {
|
|
462
|
+
// Update existing
|
|
463
|
+
this.data.tableRelationships[existingIndex] = {
|
|
464
|
+
...this.data.tableRelationships[existingIndex],
|
|
465
|
+
...relationship,
|
|
466
|
+
updatedAt: new Date().toISOString(),
|
|
467
|
+
};
|
|
468
|
+
await this.save();
|
|
469
|
+
return this.data.tableRelationships[existingIndex];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Add new
|
|
473
|
+
this.data.tableRelationships.push(relationship);
|
|
474
|
+
await this.save();
|
|
475
|
+
return relationship;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Add a relationship from FK information (convenience method).
|
|
480
|
+
*
|
|
481
|
+
* @param params - FK parameters
|
|
482
|
+
* @returns The added relationship
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* await store.addRelationshipFromFK({
|
|
487
|
+
* fromTable: 'orders',
|
|
488
|
+
* fromColumn: 'user_id',
|
|
489
|
+
* toTable: 'users',
|
|
490
|
+
* toColumn: 'id',
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
async addRelationshipFromFK(params: {
|
|
495
|
+
fromTable: string;
|
|
496
|
+
fromSchema?: string;
|
|
497
|
+
fromColumn: string;
|
|
498
|
+
toTable: string;
|
|
499
|
+
toSchema?: string;
|
|
500
|
+
toColumn: string;
|
|
501
|
+
constraintName?: string;
|
|
502
|
+
}): Promise<TableRelationship> {
|
|
503
|
+
if (!this.data) {
|
|
504
|
+
throw new Error('Knowledge store not loaded');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const fromSchema = params.fromSchema ?? 'public';
|
|
508
|
+
const toSchema = params.toSchema ?? 'public';
|
|
509
|
+
|
|
510
|
+
const relationship: TableRelationship = {
|
|
511
|
+
...createKnowledgeMeta('auto', this.data.schemaHash),
|
|
512
|
+
type: 'table_relationship',
|
|
513
|
+
from: createTableRef(params.fromTable, fromSchema),
|
|
514
|
+
to: createTableRef(params.toTable, toSchema),
|
|
515
|
+
relationshipType: 'foreign_key',
|
|
516
|
+
joinCondition: `${params.fromTable}.${params.fromColumn} = ${params.toTable}.${params.toColumn}`,
|
|
517
|
+
cardinality: 'many-to-one',
|
|
518
|
+
fromColumns: [params.fromColumn],
|
|
519
|
+
toColumns: [params.toColumn],
|
|
520
|
+
isPreferred: true,
|
|
521
|
+
constraintName: params.constraintName,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
return this.addRelationship(relationship);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Add multiple relationships at once (batch operation).
|
|
529
|
+
*
|
|
530
|
+
* @param relationships - Array of relationships to add
|
|
531
|
+
* @returns Number of relationships added
|
|
532
|
+
*/
|
|
533
|
+
async addRelationships(relationships: TableRelationship[]): Promise<number> {
|
|
534
|
+
if (!this.data) {
|
|
535
|
+
throw new Error('Knowledge store not loaded');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!this.data.tableRelationships) {
|
|
539
|
+
this.data.tableRelationships = [];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let addedCount = 0;
|
|
543
|
+
|
|
544
|
+
for (const rel of relationships) {
|
|
545
|
+
const existingIndex = this.data.tableRelationships.findIndex(
|
|
546
|
+
r => tableRefsEqual(r.from, rel.from) && tableRefsEqual(r.to, rel.to)
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (existingIndex >= 0) {
|
|
550
|
+
// Update existing
|
|
551
|
+
this.data.tableRelationships[existingIndex] = {
|
|
552
|
+
...this.data.tableRelationships[existingIndex],
|
|
553
|
+
...rel,
|
|
554
|
+
updatedAt: new Date().toISOString(),
|
|
555
|
+
};
|
|
556
|
+
} else {
|
|
557
|
+
// Add new
|
|
558
|
+
this.data.tableRelationships.push(rel);
|
|
559
|
+
addedCount++;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await this.save();
|
|
564
|
+
return addedCount;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Delete a relationship by ID.
|
|
569
|
+
*
|
|
570
|
+
* @param id - Relationship ID to delete
|
|
571
|
+
*/
|
|
572
|
+
async deleteRelationship(id: string): Promise<void> {
|
|
573
|
+
if (!this.data) {
|
|
574
|
+
throw new Error('Knowledge store not loaded');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const index = this.data.tableRelationships.findIndex(r => r.id === id);
|
|
578
|
+
if (index >= 0) {
|
|
579
|
+
this.data.tableRelationships.splice(index, 1);
|
|
580
|
+
await this.save();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get a summary of the relationship graph.
|
|
586
|
+
*
|
|
587
|
+
* @returns Graph statistics
|
|
588
|
+
*/
|
|
589
|
+
getGraphSummary(): {
|
|
590
|
+
nodeCount: number;
|
|
591
|
+
edgeCount: number;
|
|
592
|
+
tablesWithRelationships: number;
|
|
593
|
+
isolatedTables: number;
|
|
594
|
+
relationshipsByType: Record<RelationshipType, number>;
|
|
595
|
+
} {
|
|
596
|
+
const relationships = this.data?.tableRelationships ?? [];
|
|
597
|
+
const tables = this.data?.tableDescriptions ?? [];
|
|
598
|
+
|
|
599
|
+
// Find unique tables in relationships
|
|
600
|
+
const tablesInRelationships = new Set<string>();
|
|
601
|
+
for (const rel of relationships) {
|
|
602
|
+
tablesInRelationships.add(tableRefToString(rel.from));
|
|
603
|
+
tablesInRelationships.add(tableRefToString(rel.to));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Count by type
|
|
607
|
+
const byType: Record<RelationshipType, number> = {
|
|
608
|
+
'foreign_key': 0,
|
|
609
|
+
'implicit_join': 0,
|
|
610
|
+
'manual': 0,
|
|
611
|
+
};
|
|
612
|
+
for (const rel of relationships) {
|
|
613
|
+
byType[rel.relationshipType]++;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
nodeCount: tables.length,
|
|
618
|
+
edgeCount: relationships.length,
|
|
619
|
+
tablesWithRelationships: tablesInRelationships.size,
|
|
620
|
+
isolatedTables: tables.length - Math.min(tablesInRelationships.size, tables.length),
|
|
621
|
+
relationshipsByType: byType,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
227
625
|
// ============ Query Examples ============
|
|
228
626
|
|
|
229
627
|
/**
|
|
@@ -542,6 +940,12 @@ export class KnowledgeStore {
|
|
|
542
940
|
}
|
|
543
941
|
}
|
|
544
942
|
|
|
943
|
+
for (const rel of this.data.tableRelationships ?? []) {
|
|
944
|
+
if (rel.schemaHash !== currentSchemaHash) {
|
|
945
|
+
outdated.push(rel);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
545
949
|
return outdated;
|
|
546
950
|
}
|
|
547
951
|
|
|
@@ -656,6 +1060,7 @@ export class KnowledgeStore {
|
|
|
656
1060
|
schemaHash: '',
|
|
657
1061
|
lastSyncAt: new Date().toISOString(),
|
|
658
1062
|
tableDescriptions: [],
|
|
1063
|
+
tableRelationships: [],
|
|
659
1064
|
queryExamples: [],
|
|
660
1065
|
businessRules: [],
|
|
661
1066
|
businessTerms: [],
|
|
@@ -672,12 +1077,19 @@ export class KnowledgeStore {
|
|
|
672
1077
|
description?: string;
|
|
673
1078
|
columns: Array<{ name: string; description?: string }>;
|
|
674
1079
|
}>;
|
|
1080
|
+
relationships: TableRelationship[];
|
|
675
1081
|
queryExamples: QueryExample[];
|
|
676
1082
|
businessRules: BusinessRule[];
|
|
677
1083
|
businessTerms: BusinessTerm[];
|
|
678
1084
|
} {
|
|
679
1085
|
if (!this.data) {
|
|
680
|
-
return {
|
|
1086
|
+
return {
|
|
1087
|
+
tables: [],
|
|
1088
|
+
relationships: [],
|
|
1089
|
+
queryExamples: [],
|
|
1090
|
+
businessRules: [],
|
|
1091
|
+
businessTerms: []
|
|
1092
|
+
};
|
|
681
1093
|
}
|
|
682
1094
|
|
|
683
1095
|
return {
|
|
@@ -687,11 +1099,73 @@ export class KnowledgeStore {
|
|
|
687
1099
|
description: td.description,
|
|
688
1100
|
columns: td.columns,
|
|
689
1101
|
})),
|
|
1102
|
+
relationships: this.data.tableRelationships ?? [],
|
|
690
1103
|
queryExamples: this.data.queryExamples,
|
|
691
1104
|
businessRules: this.data.businessRules,
|
|
692
1105
|
businessTerms: this.data.businessTerms ?? [],
|
|
693
1106
|
};
|
|
694
1107
|
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Build context including relationships for AI.
|
|
1111
|
+
*
|
|
1112
|
+
* Enhanced version that includes table relationship information
|
|
1113
|
+
* to help AI understand how tables connect.
|
|
1114
|
+
*
|
|
1115
|
+
* @param tables - Tables to include
|
|
1116
|
+
* @param userQuery - Optional user query for term matching
|
|
1117
|
+
* @param schema - Optional schema (if not provided, searches all schemas)
|
|
1118
|
+
* @returns Formatted context string
|
|
1119
|
+
*/
|
|
1120
|
+
buildContextWithRelationships(tables: string[], userQuery?: string, schema?: string): string {
|
|
1121
|
+
let context = this.buildContext(tables, userQuery);
|
|
1122
|
+
|
|
1123
|
+
// Add relationship information
|
|
1124
|
+
// If no schema specified, find relationships across all schemas
|
|
1125
|
+
const relevantRelationships: TableRelationship[] = [];
|
|
1126
|
+
for (const table of tables) {
|
|
1127
|
+
// Search relationships that involve this table (in any schema if not specified)
|
|
1128
|
+
const allRelationships = this.data?.tableRelationships ?? [];
|
|
1129
|
+
const rels = schema
|
|
1130
|
+
? this.getRelatedTables(table, schema)
|
|
1131
|
+
: allRelationships.filter(
|
|
1132
|
+
rel => rel.from.table === table || rel.to.table === table
|
|
1133
|
+
);
|
|
1134
|
+
for (const rel of rels) {
|
|
1135
|
+
// Avoid duplicates
|
|
1136
|
+
if (!relevantRelationships.find(r => r.id === rel.id)) {
|
|
1137
|
+
relevantRelationships.push(rel);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (relevantRelationships.length > 0) {
|
|
1143
|
+
const parts: string[] = [
|
|
1144
|
+
'',
|
|
1145
|
+
'## Table Relationships',
|
|
1146
|
+
'Use these relationships when joining tables:',
|
|
1147
|
+
'',
|
|
1148
|
+
];
|
|
1149
|
+
|
|
1150
|
+
for (const rel of relevantRelationships) {
|
|
1151
|
+
const fromStr = `${rel.from.schema}.${rel.from.table}`;
|
|
1152
|
+
const toStr = `${rel.to.schema}.${rel.to.table}`;
|
|
1153
|
+
parts.push(`- **${fromStr}** → **${toStr}**`);
|
|
1154
|
+
parts.push(` Join: \`${rel.joinCondition}\``);
|
|
1155
|
+
if (rel.cardinality) {
|
|
1156
|
+
parts.push(` Cardinality: ${rel.cardinality}`);
|
|
1157
|
+
}
|
|
1158
|
+
if (rel.notes) {
|
|
1159
|
+
parts.push(` Note: ${rel.notes}`);
|
|
1160
|
+
}
|
|
1161
|
+
parts.push('');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
context += '\n' + parts.join('\n');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return context;
|
|
1168
|
+
}
|
|
695
1169
|
}
|
|
696
1170
|
|
|
697
1171
|
/**
|