@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.
Files changed (52) hide show
  1. package/dist/adapters/sqlite.d.ts.map +1 -1
  2. package/dist/adapters/sqlite.js +13 -0
  3. package/dist/adapters/sqlite.js.map +1 -1
  4. package/dist/api/server.d.ts.map +1 -1
  5. package/dist/api/server.js +115 -0
  6. package/dist/api/server.js.map +1 -1
  7. package/dist/cli/index.js +58 -14
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/core/context-service.d.ts +63 -0
  10. package/dist/core/context-service.d.ts.map +1 -1
  11. package/dist/core/context-service.js +66 -0
  12. package/dist/core/context-service.js.map +1 -1
  13. package/dist/core/harvester.d.ts +57 -5
  14. package/dist/core/harvester.d.ts.map +1 -1
  15. package/dist/core/harvester.js +86 -6
  16. package/dist/core/harvester.js.map +1 -1
  17. package/dist/core/types.d.ts +21 -5
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/knowledge/store.d.ts +186 -3
  24. package/dist/knowledge/store.d.ts.map +1 -1
  25. package/dist/knowledge/store.js +389 -5
  26. package/dist/knowledge/store.js.map +1 -1
  27. package/dist/knowledge/types.d.ts +252 -4
  28. package/dist/knowledge/types.d.ts.map +1 -1
  29. package/dist/knowledge/types.js +138 -1
  30. package/dist/knowledge/types.js.map +1 -1
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +231 -3
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/docs/KNOWLEDGE_GRAPH.md +540 -0
  35. package/docs/KNOWLEDGE_TYPES.md +261 -0
  36. package/docs/MULTI_DB_ARCHITECTURE.md +319 -0
  37. package/package.json +1 -1
  38. package/scripts/create-sqlite-testdb.sh +75 -0
  39. package/scripts/test-databases.sh +324 -0
  40. package/sqlite:./test-sqlite.db +0 -0
  41. package/src/adapters/sqlite.ts +16 -0
  42. package/src/api/server.ts +134 -0
  43. package/src/cli/index.ts +57 -16
  44. package/src/core/context-service.ts +70 -0
  45. package/src/core/harvester.ts +120 -8
  46. package/src/core/types.ts +21 -5
  47. package/src/index.ts +19 -1
  48. package/src/knowledge/store.ts +480 -6
  49. package/src/knowledge/types.ts +321 -4
  50. package/src/mcp/tools.ts +273 -3
  51. package/test-sqlite.db +0 -0
  52. package/tests/knowledge-store.test.ts +130 -0
@@ -1,6 +1,40 @@
1
1
  /**
2
2
  * Knowledge Types
3
- * Type definitions for the knowledge store (descriptions, rules, examples)
3
+ *
4
+ * Type definitions for the Knowledge Store / Knowledge Graph.
5
+ * This module defines the core data structures for storing business context
6
+ * about database schemas, including:
7
+ *
8
+ * - **TableDescription**: Business context for tables and columns
9
+ * - **TableRelationship**: Graph edges representing table relationships (FK, implicit, manual)
10
+ * - **QueryExample**: Verified SQL patterns with intent
11
+ * - **BusinessRule**: Domain constraints and conventions
12
+ * - **BusinessTerm**: Glossary entries for natural language → SQL translation
13
+ *
14
+ * ## Knowledge Graph Architecture
15
+ *
16
+ * The Knowledge Store is designed to evolve into a Knowledge Graph:
17
+ *
18
+ * ```
19
+ * ┌─────────────────────────────────────────────────────────────────┐
20
+ * │ Knowledge Graph │
21
+ * │ │
22
+ * │ [Table A] ──JOINS_WITH──> [Table B] ──JOINS_WITH──> [Table C]│
23
+ * │ │ │ │
24
+ * │ │ │ │
25
+ * │ [Description] [Description] │
26
+ * │ [Rules] [Rules] │
27
+ * │ [Examples] [Terms] │
28
+ * │ │
29
+ * └─────────────────────────────────────────────────────────────────┘
30
+ * ```
31
+ *
32
+ * ## Multi-Database Support
33
+ *
34
+ * All references use `TableRef` which includes optional database identifier,
35
+ * enabling future cross-database relationship tracking.
36
+ *
37
+ * @module knowledge/types
4
38
  */
5
39
 
6
40
  /** Source of the knowledge entry */
@@ -26,6 +60,127 @@ export interface KnowledgeMeta {
26
60
  owner?: string;
27
61
  }
28
62
 
63
+ // ============================================================
64
+ // Graph Types (Relationships)
65
+ // ============================================================
66
+
67
+ /**
68
+ * Fully qualified table reference.
69
+ *
70
+ * Enables cross-database and cross-schema relationship tracking.
71
+ * The `database` field is optional for single-database scenarios.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // Single database (most common)
76
+ * const ref: TableRef = { schema: 'public', table: 'users' };
77
+ *
78
+ * // Multi-database (future)
79
+ * const ref: TableRef = { database: 'analytics', schema: 'public', table: 'events' };
80
+ * ```
81
+ */
82
+ export interface TableRef {
83
+ /** Database identifier (optional for single-DB mode) */
84
+ database?: string;
85
+ /** Schema name */
86
+ schema: string;
87
+ /** Table name */
88
+ table: string;
89
+ }
90
+
91
+ /**
92
+ * Relationship type between tables.
93
+ *
94
+ * - `foreign_key`: Relationship derived from database FK constraint
95
+ * - `implicit_join`: Relationship inferred from query patterns (no FK)
96
+ * - `manual`: Relationship defined manually by user
97
+ */
98
+ export type RelationshipType = 'foreign_key' | 'implicit_join' | 'manual';
99
+
100
+ /**
101
+ * Cardinality of a table relationship.
102
+ *
103
+ * - `one-to-one`: Each row in A maps to exactly one row in B
104
+ * - `one-to-many`: Each row in A maps to multiple rows in B (most common for FK)
105
+ * - `many-to-one`: Multiple rows in A map to one row in B
106
+ * - `many-to-many`: Requires junction table
107
+ */
108
+ export type RelationshipCardinality = 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many';
109
+
110
+ /**
111
+ * Table relationship representing a graph edge.
112
+ *
113
+ * This is the core type for the Knowledge Graph, representing how tables
114
+ * are connected. Relationships can be:
115
+ * - Auto-harvested from foreign key constraints
116
+ * - Inferred from query patterns (implicit joins)
117
+ * - Manually defined by users
118
+ *
119
+ * ## Graph Traversal
120
+ *
121
+ * Relationships enable graph queries like:
122
+ * - "Find all tables related to users"
123
+ * - "Find the shortest join path from orders to products"
124
+ * - "Which tables reference this column?"
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * // FK-based relationship (auto-harvested)
129
+ * const relationship: TableRelationship = {
130
+ * type: 'table_relationship',
131
+ * from: { schema: 'public', table: 'orders' },
132
+ * to: { schema: 'public', table: 'users' },
133
+ * relationshipType: 'foreign_key',
134
+ * joinCondition: 'orders.user_id = users.id',
135
+ * cardinality: 'many-to-one',
136
+ * fromColumns: ['user_id'],
137
+ * toColumns: ['id'],
138
+ * isPreferred: true,
139
+ * // ... metadata fields
140
+ * };
141
+ *
142
+ * // Implicit join (learned from queries)
143
+ * const implicit: TableRelationship = {
144
+ * type: 'table_relationship',
145
+ * from: { schema: 'public', table: 'events' },
146
+ * to: { schema: 'public', table: 'users' },
147
+ * relationshipType: 'implicit_join',
148
+ * joinCondition: 'events.actor_id = users.id',
149
+ * cardinality: 'many-to-one',
150
+ * isPreferred: false,
151
+ * notes: 'Inferred from query patterns',
152
+ * // ... metadata fields
153
+ * };
154
+ * ```
155
+ */
156
+ export interface TableRelationship extends KnowledgeMeta {
157
+ type: 'table_relationship';
158
+ /** Source table (FK side for foreign_key type) */
159
+ from: TableRef;
160
+ /** Target table (referenced side for foreign_key type) */
161
+ to: TableRef;
162
+ /** How this relationship was discovered */
163
+ relationshipType: RelationshipType;
164
+ /** SQL join condition */
165
+ joinCondition: string;
166
+ /** Relationship cardinality */
167
+ cardinality?: RelationshipCardinality;
168
+ /** Columns in the source table involved in the join */
169
+ fromColumns?: string[];
170
+ /** Columns in the target table involved in the join */
171
+ toColumns?: string[];
172
+ /** Whether this is the preferred join path (when multiple exist) */
173
+ isPreferred: boolean;
174
+ /** FK constraint name (if from database) */
175
+ constraintName?: string;
176
+ /** Human-readable notes about this relationship */
177
+ notes?: string;
178
+ }
179
+
180
+ // ============================================================
181
+ // Entity Types (Nodes)
182
+ // ============================================================
183
+
29
184
  /** Column description */
30
185
  export interface ColumnDescription {
31
186
  /** Column name */
@@ -135,9 +290,42 @@ export interface BusinessTerm extends KnowledgeMeta {
135
290
  }
136
291
 
137
292
  /** Union type for all knowledge entries */
138
- export type KnowledgeEntry = TableDescription | QueryExample | BusinessRule | BusinessTerm;
293
+ export type KnowledgeEntry =
294
+ | TableDescription
295
+ | TableRelationship
296
+ | QueryExample
297
+ | BusinessRule
298
+ | BusinessTerm;
139
299
 
140
- /** Complete knowledge data structure */
300
+ /**
301
+ * Complete knowledge data structure.
302
+ *
303
+ * This is the root data structure for the Knowledge Store, containing:
304
+ * - **Nodes**: Tables, columns (within TableDescription)
305
+ * - **Edges**: Relationships between tables (TableRelationship)
306
+ * - **Annotations**: Rules, examples, terms
307
+ *
308
+ * ## Data Model (Graph Perspective)
309
+ *
310
+ * ```
311
+ * Nodes:
312
+ * - TableDescription (with embedded ColumnDescription[])
313
+ *
314
+ * Edges:
315
+ * - TableRelationship (from → to with join condition)
316
+ *
317
+ * Annotations (attached to nodes):
318
+ * - QueryExample (references tables)
319
+ * - BusinessRule (applies to tables/columns)
320
+ * - BusinessTerm (applies to tables/columns)
321
+ * ```
322
+ *
323
+ * ## Version History
324
+ *
325
+ * - `1.0.0`: Initial version (tables, examples, rules)
326
+ * - `1.1.0`: Added businessTerms (glossary)
327
+ * - `2.0.0`: Added tableRelationships (graph edges)
328
+ */
141
329
  export interface KnowledgeData {
142
330
  /** Version of the knowledge format */
143
331
  version: string;
@@ -147,8 +335,16 @@ export interface KnowledgeData {
147
335
  schemaHash: string;
148
336
  /** Last sync timestamp */
149
337
  lastSyncAt: string;
150
- /** Table descriptions */
338
+
339
+ // === Nodes ===
340
+ /** Table descriptions (graph nodes) */
151
341
  tableDescriptions: TableDescription[];
342
+
343
+ // === Edges ===
344
+ /** Table relationships (graph edges) - NEW in v2.0 */
345
+ tableRelationships: TableRelationship[];
346
+
347
+ // === Annotations ===
152
348
  /** Query examples */
153
349
  queryExamples: QueryExample[];
154
350
  /** Business rules */
@@ -157,6 +353,10 @@ export interface KnowledgeData {
157
353
  businessTerms: BusinessTerm[];
158
354
  }
159
355
 
356
+ // ============================================================
357
+ // Factory Functions
358
+ // ============================================================
359
+
160
360
  /** Generate a unique ID */
161
361
  export function generateId(): string {
162
362
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -181,3 +381,120 @@ export function createKnowledgeMeta(
181
381
  };
182
382
  }
183
383
 
384
+ /**
385
+ * Create a TableRef from table name and optional schema/database.
386
+ *
387
+ * @param table - Table name
388
+ * @param schema - Schema name (defaults to 'public')
389
+ * @param database - Database name (optional)
390
+ * @returns TableRef object
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * const ref = createTableRef('users');
395
+ * // { schema: 'public', table: 'users' }
396
+ *
397
+ * const ref2 = createTableRef('orders', 'sales', 'analytics_db');
398
+ * // { database: 'analytics_db', schema: 'sales', table: 'orders' }
399
+ * ```
400
+ */
401
+ export function createTableRef(
402
+ table: string,
403
+ schema: string = 'public',
404
+ database?: string
405
+ ): TableRef {
406
+ const ref: TableRef = { schema, table };
407
+ if (database) {
408
+ ref.database = database;
409
+ }
410
+ return ref;
411
+ }
412
+
413
+ /**
414
+ * Convert TableRef to a fully qualified string representation.
415
+ *
416
+ * @param ref - TableRef to stringify
417
+ * @returns String like "database.schema.table" or "schema.table"
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * tableRefToString({ schema: 'public', table: 'users' })
422
+ * // "public.users"
423
+ *
424
+ * tableRefToString({ database: 'analytics', schema: 'public', table: 'events' })
425
+ * // "analytics.public.events"
426
+ * ```
427
+ */
428
+ export function tableRefToString(ref: TableRef): string {
429
+ if (ref.database) {
430
+ return `${ref.database}.${ref.schema}.${ref.table}`;
431
+ }
432
+ return `${ref.schema}.${ref.table}`;
433
+ }
434
+
435
+ /**
436
+ * Check if two TableRefs refer to the same table.
437
+ *
438
+ * @param a - First TableRef
439
+ * @param b - Second TableRef
440
+ * @returns true if they refer to the same table
441
+ */
442
+ export function tableRefsEqual(a: TableRef, b: TableRef): boolean {
443
+ return (
444
+ a.table === b.table &&
445
+ a.schema === b.schema &&
446
+ (a.database ?? '') === (b.database ?? '')
447
+ );
448
+ }
449
+
450
+ /**
451
+ * Create a TableRelationship from foreign key information.
452
+ *
453
+ * This is the standard way to create relationships from harvested FK data.
454
+ *
455
+ * @param params - Relationship parameters
456
+ * @returns Complete TableRelationship object
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * const relationship = createRelationshipFromFK({
461
+ * fromTable: 'orders',
462
+ * fromSchema: 'public',
463
+ * fromColumn: 'user_id',
464
+ * toTable: 'users',
465
+ * toSchema: 'public',
466
+ * toColumn: 'id',
467
+ * constraintName: 'orders_user_id_fkey',
468
+ * schemaHash: 'abc123',
469
+ * });
470
+ * ```
471
+ */
472
+ export function createRelationshipFromFK(params: {
473
+ fromTable: string;
474
+ fromSchema?: string;
475
+ fromColumn: string;
476
+ toTable: string;
477
+ toSchema?: string;
478
+ toColumn: string;
479
+ constraintName?: string;
480
+ schemaHash: string;
481
+ database?: string;
482
+ }): TableRelationship {
483
+ const fromSchema = params.fromSchema ?? 'public';
484
+ const toSchema = params.toSchema ?? 'public';
485
+
486
+ return {
487
+ ...createKnowledgeMeta('auto', params.schemaHash),
488
+ type: 'table_relationship',
489
+ from: createTableRef(params.fromTable, fromSchema, params.database),
490
+ to: createTableRef(params.toTable, toSchema, params.database),
491
+ relationshipType: 'foreign_key',
492
+ joinCondition: `${params.fromTable}.${params.fromColumn} = ${params.toTable}.${params.toColumn}`,
493
+ cardinality: 'many-to-one', // Default for FK
494
+ fromColumns: [params.fromColumn],
495
+ toColumns: [params.toColumn],
496
+ isPreferred: true,
497
+ constraintName: params.constraintName,
498
+ };
499
+ }
500
+
package/src/mcp/tools.ts CHANGED
@@ -355,6 +355,54 @@ export function getMcpTools(): Tool[] {
355
355
  required: ['query'],
356
356
  },
357
357
  },
358
+ // === Knowledge Graph Tools ===
359
+ {
360
+ name: 'find_join_path',
361
+ description: 'Find the optimal join path between two tables using the Knowledge Graph. Automatically discovers relationships and suggests JOIN conditions. This is useful when you need to join tables that are not directly related.',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: {
365
+ fromTable: {
366
+ type: 'string',
367
+ description: 'Source table name to start from.',
368
+ },
369
+ toTable: {
370
+ type: 'string',
371
+ description: 'Destination table name to reach.',
372
+ },
373
+ schema: {
374
+ type: 'string',
375
+ description: 'Database schema. Defaults to "public" for PostgreSQL, "main" for SQLite.',
376
+ },
377
+ },
378
+ required: ['fromTable', 'toTable'],
379
+ },
380
+ },
381
+ {
382
+ name: 'get_relationships',
383
+ description: 'Get all table relationships (foreign keys, joins) from the Knowledge Graph. Returns information about how tables are connected.',
384
+ inputSchema: {
385
+ type: 'object',
386
+ properties: {
387
+ table: {
388
+ type: 'string',
389
+ description: 'Optional: Filter to relationships involving this table.',
390
+ },
391
+ schema: {
392
+ type: 'string',
393
+ description: 'Database schema. Defaults to "public".',
394
+ },
395
+ },
396
+ },
397
+ },
398
+ {
399
+ name: 'get_graph_summary',
400
+ description: 'Get a summary of the Knowledge Graph including total tables, relationships, and connectivity statistics.',
401
+ inputSchema: {
402
+ type: 'object',
403
+ properties: {},
404
+ },
405
+ },
358
406
  ];
359
407
  }
360
408
 
@@ -401,6 +449,13 @@ export async function handleToolCall(
401
449
  return handleSearchTerms(args, context);
402
450
  case 'enhance_query':
403
451
  return handleEnhanceQuery(args, context);
452
+ // === Knowledge Graph Tools ===
453
+ case 'find_join_path':
454
+ return handleFindJoinPath(args, context);
455
+ case 'get_relationships':
456
+ return handleGetRelationships(args, context);
457
+ case 'get_graph_summary':
458
+ return handleGetGraphSummary(args, context);
404
459
  default:
405
460
  throw new Error(`Unknown tool: ${name}`);
406
461
  }
@@ -589,6 +644,9 @@ async function handleDescribeTable(
589
644
 
590
645
  /**
591
646
  * Handle get_context tool
647
+ *
648
+ * Enhanced to include table relationships from the Knowledge Graph.
649
+ * This helps AI understand how tables connect and generate accurate JOINs.
592
650
  */
593
651
  async function handleGetContext(
594
652
  args: Record<string, unknown>,
@@ -600,14 +658,41 @@ async function handleGetContext(
600
658
  throw new Error('Tables array is required');
601
659
  }
602
660
 
603
- const contextText = context.knowledge.buildContext(tables);
661
+ // Use buildContextWithRelationships to include relationship information
662
+ const contextText = context.knowledge.buildContextWithRelationships(tables);
604
663
 
605
664
  // Also get query examples
606
665
  const examples = context.knowledge.getQueryExamplesForTables(tables);
607
666
 
667
+ // Get relationships involving these tables
668
+ const allRelationships = context.knowledge.getRelationships();
669
+ const relevantRelationships = allRelationships.filter(
670
+ rel => tables.includes(rel.from.table) || tables.includes(rel.to.table)
671
+ );
672
+
673
+ // Find related tables (1-hop connections)
674
+ const relatedTables = new Set<string>();
675
+ for (const table of tables) {
676
+ const rels = context.knowledge.getRelatedTables(table);
677
+ for (const rel of rels) {
678
+ relatedTables.add(rel.from.table);
679
+ relatedTables.add(rel.to.table);
680
+ }
681
+ }
682
+ // Remove tables already in the query
683
+ tables.forEach(t => relatedTables.delete(t));
684
+
608
685
  return {
609
686
  context: contextText,
610
687
  tables,
688
+ relatedTables: Array.from(relatedTables),
689
+ relationships: relevantRelationships.map(rel => ({
690
+ from: `${rel.from.schema}.${rel.from.table}`,
691
+ to: `${rel.to.schema}.${rel.to.table}`,
692
+ type: rel.relationshipType,
693
+ joinCondition: rel.joinCondition,
694
+ cardinality: rel.cardinality,
695
+ })),
611
696
  queryExamples: examples.slice(0, 5).map(ex => ({
612
697
  intent: ex.intent,
613
698
  sql: ex.sql,
@@ -709,13 +794,15 @@ async function handleHarvestMetadata(
709
794
  }
710
795
 
711
796
  const result = await harvester.syncToKnowledge(schema);
797
+ const relMsg = result.relationshipsAdded ? `, ${result.relationshipsAdded} relationships` : '';
712
798
  return {
713
799
  success: true,
714
800
  schema,
715
801
  tablesProcessed: result.tablesProcessed,
716
802
  descriptionsAdded: result.descriptionsAdded,
717
803
  columnsAdded: result.columnsAdded,
718
- message: `Harvested ${result.tablesProcessed} tables. Added ${result.descriptionsAdded} table descriptions and ${result.columnsAdded} column descriptions.`,
804
+ relationshipsAdded: result.relationshipsAdded ?? 0,
805
+ message: `Harvested ${result.tablesProcessed} tables. Added ${result.descriptionsAdded} table descriptions, ${result.columnsAdded} column descriptions${relMsg}.`,
719
806
  };
720
807
  }
721
808
 
@@ -730,13 +817,15 @@ async function handleHarvestMetadata(
730
817
  }
731
818
 
732
819
  const result = await context.harvester.syncToKnowledge(schema);
820
+ const relMsg = result.relationshipsAdded ? `, ${result.relationshipsAdded} relationships` : '';
733
821
  return {
734
822
  success: true,
735
823
  schema,
736
824
  tablesProcessed: result.tablesProcessed,
737
825
  descriptionsAdded: result.descriptionsAdded,
738
826
  columnsAdded: result.columnsAdded,
739
- message: `Harvested ${result.tablesProcessed} tables. Added ${result.descriptionsAdded} table descriptions and ${result.columnsAdded} column descriptions.`,
827
+ relationshipsAdded: result.relationshipsAdded ?? 0,
828
+ message: `Harvested ${result.tablesProcessed} tables. Added ${result.descriptionsAdded} table descriptions, ${result.columnsAdded} column descriptions${relMsg}.`,
740
829
  };
741
830
  }
742
831
 
@@ -1235,6 +1324,187 @@ function calculateTermRelevance(query: string, term: { term: string; synonyms: s
1235
1324
  return 0.5;
1236
1325
  }
1237
1326
 
1327
+ // ============================================================
1328
+ // Knowledge Graph Tools
1329
+ // ============================================================
1330
+
1331
+ /**
1332
+ * Handle find_join_path tool
1333
+ *
1334
+ * Uses the Knowledge Graph to find the optimal path between two tables.
1335
+ * This is particularly useful for complex schemas where tables are connected
1336
+ * through intermediate tables (e.g., order_items → orders → users).
1337
+ *
1338
+ * @example
1339
+ * // Request:
1340
+ * { fromTable: "order_items", toTable: "users" }
1341
+ *
1342
+ * // Response:
1343
+ * {
1344
+ * path: [
1345
+ * { from: "order_items", to: "orders", join: "order_items.order_id = orders.id" },
1346
+ * { from: "orders", to: "users", join: "orders.user_id = users.id" }
1347
+ * ],
1348
+ * sqlHint: "JOIN orders ON ... JOIN users ON ..."
1349
+ * }
1350
+ */
1351
+ async function handleFindJoinPath(
1352
+ args: Record<string, unknown>,
1353
+ context: ToolContext
1354
+ ): Promise<unknown> {
1355
+ const fromTable = args.fromTable as string;
1356
+ const toTable = args.toTable as string;
1357
+ const schema = (args.schema as string) ?? 'public';
1358
+
1359
+ if (!fromTable || !toTable) {
1360
+ throw new Error('Both fromTable and toTable are required');
1361
+ }
1362
+
1363
+ // Find path using BFS in the Knowledge Graph
1364
+ const path = context.knowledge.findJoinPath(fromTable, toTable, schema);
1365
+
1366
+ if (path.length === 0) {
1367
+ // Try with 'main' schema for SQLite
1368
+ const pathMain = context.knowledge.findJoinPath(fromTable, toTable, 'main');
1369
+ if (pathMain.length === 0) {
1370
+ return {
1371
+ found: false,
1372
+ message: `No path found between '${fromTable}' and '${toTable}'. These tables may not be connected via foreign keys. Consider using implicit joins or check if the tables exist.`,
1373
+ suggestion: 'Run harvest_metadata to discover relationships, or manually define a relationship.',
1374
+ };
1375
+ }
1376
+ return formatJoinPathResult(fromTable, toTable, pathMain);
1377
+ }
1378
+
1379
+ return formatJoinPathResult(fromTable, toTable, path);
1380
+ }
1381
+
1382
+ /**
1383
+ * Format join path result for AI consumption
1384
+ */
1385
+ function formatJoinPathResult(
1386
+ fromTable: string,
1387
+ toTable: string,
1388
+ path: Array<{ from: { schema: string; table: string }; to: { schema: string; table: string }; joinCondition: string; cardinality?: string }>
1389
+ ): unknown {
1390
+ const steps = path.map((rel, idx) => ({
1391
+ step: idx + 1,
1392
+ from: `${rel.from.schema}.${rel.from.table}`,
1393
+ to: `${rel.to.schema}.${rel.to.table}`,
1394
+ joinCondition: rel.joinCondition,
1395
+ cardinality: rel.cardinality || 'unknown',
1396
+ }));
1397
+
1398
+ // Generate SQL hint
1399
+ const sqlParts = path.map(rel =>
1400
+ `JOIN ${rel.to.table} ON ${rel.joinCondition}`
1401
+ );
1402
+ const sqlHint = `FROM ${fromTable}\n${sqlParts.join('\n')}`;
1403
+
1404
+ // Generate human-readable explanation
1405
+ const pathDescription = path.map(rel =>
1406
+ `${rel.from.table} → ${rel.to.table}`
1407
+ ).join(' → ');
1408
+
1409
+ return {
1410
+ found: true,
1411
+ from: fromTable,
1412
+ to: toTable,
1413
+ hops: path.length,
1414
+ path: steps,
1415
+ pathDescription,
1416
+ sqlHint,
1417
+ message: `Found a ${path.length}-hop path: ${pathDescription}`,
1418
+ };
1419
+ }
1420
+
1421
+ /**
1422
+ * Handle get_relationships tool
1423
+ *
1424
+ * Returns all relationships from the Knowledge Graph, optionally filtered by table.
1425
+ */
1426
+ async function handleGetRelationships(
1427
+ args: Record<string, unknown>,
1428
+ context: ToolContext
1429
+ ): Promise<unknown> {
1430
+ const table = args.table as string | undefined;
1431
+ const schema = (args.schema as string) ?? 'public';
1432
+
1433
+ let relationships = context.knowledge.getRelationships();
1434
+
1435
+ // Filter by table if specified
1436
+ if (table) {
1437
+ relationships = relationships.filter(
1438
+ rel => rel.from.table === table || rel.to.table === table
1439
+ );
1440
+ }
1441
+
1442
+ if (relationships.length === 0) {
1443
+ return {
1444
+ count: 0,
1445
+ relationships: [],
1446
+ message: table
1447
+ ? `No relationships found for table '${table}'. Run harvest_metadata to discover foreign keys.`
1448
+ : 'No relationships in Knowledge Graph. Run harvest_metadata to discover foreign keys.',
1449
+ };
1450
+ }
1451
+
1452
+ return {
1453
+ count: relationships.length,
1454
+ relationships: relationships.map(rel => ({
1455
+ id: rel.id,
1456
+ from: `${rel.from.schema}.${rel.from.table}`,
1457
+ to: `${rel.to.schema}.${rel.to.table}`,
1458
+ type: rel.relationshipType,
1459
+ joinCondition: rel.joinCondition,
1460
+ cardinality: rel.cardinality || 'unknown',
1461
+ isPreferred: rel.isPreferred,
1462
+ notes: rel.notes,
1463
+ })),
1464
+ message: `Found ${relationships.length} relationship(s)${table ? ` involving '${table}'` : ''}.`,
1465
+ };
1466
+ }
1467
+
1468
+ /**
1469
+ * Handle get_graph_summary tool
1470
+ *
1471
+ * Returns summary statistics about the Knowledge Graph.
1472
+ */
1473
+ async function handleGetGraphSummary(
1474
+ args: Record<string, unknown>,
1475
+ context: ToolContext
1476
+ ): Promise<unknown> {
1477
+ const summary = context.knowledge.getGraphSummary();
1478
+ const relationships = context.knowledge.getRelationships();
1479
+
1480
+ // Build a quick connectivity summary
1481
+ const tableConnections = new Map<string, number>();
1482
+ for (const rel of relationships) {
1483
+ const fromKey = rel.from.table;
1484
+ const toKey = rel.to.table;
1485
+ tableConnections.set(fromKey, (tableConnections.get(fromKey) || 0) + 1);
1486
+ tableConnections.set(toKey, (tableConnections.get(toKey) || 0) + 1);
1487
+ }
1488
+
1489
+ // Sort by connections
1490
+ const sortedTables = Array.from(tableConnections.entries())
1491
+ .sort((a, b) => b[1] - a[1])
1492
+ .slice(0, 10)
1493
+ .map(([table, connections]) => ({ table, connections }));
1494
+
1495
+ return {
1496
+ nodeCount: summary.nodeCount,
1497
+ edgeCount: summary.edgeCount,
1498
+ tablesWithRelationships: summary.tablesWithRelationships,
1499
+ isolatedTables: summary.isolatedTables,
1500
+ relationshipsByType: summary.relationshipsByType,
1501
+ mostConnectedTables: sortedTables,
1502
+ message: summary.edgeCount > 0
1503
+ ? `Knowledge Graph has ${summary.edgeCount} relationships connecting ${summary.tablesWithRelationships} tables.`
1504
+ : 'Knowledge Graph is empty. Run harvest_metadata to discover relationships.',
1505
+ };
1506
+ }
1507
+
1238
1508
  // ============================================================
1239
1509
  // Utility Functions
1240
1510
  // ============================================================
package/test-sqlite.db ADDED
Binary file