@onmartech/metabase-ai-assistant 4.0.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 (40) hide show
  1. package/.env.example +38 -0
  2. package/LICENSE +201 -0
  3. package/README.md +364 -0
  4. package/README_MCP.md +279 -0
  5. package/package.json +99 -0
  6. package/src/ai/assistant.js +982 -0
  7. package/src/cli/interactive.js +500 -0
  8. package/src/database/connection-manager.js +350 -0
  9. package/src/database/direct-client.js +686 -0
  10. package/src/index.js +162 -0
  11. package/src/mcp/handlers/actions.js +213 -0
  12. package/src/mcp/handlers/ai.js +207 -0
  13. package/src/mcp/handlers/analytics.js +1647 -0
  14. package/src/mcp/handlers/cards.js +1544 -0
  15. package/src/mcp/handlers/collections.js +244 -0
  16. package/src/mcp/handlers/dashboard.js +207 -0
  17. package/src/mcp/handlers/dashboard_direct.js +292 -0
  18. package/src/mcp/handlers/database.js +322 -0
  19. package/src/mcp/handlers/docs.js +399 -0
  20. package/src/mcp/handlers/index.js +35 -0
  21. package/src/mcp/handlers/metadata.js +190 -0
  22. package/src/mcp/handlers/questions.js +134 -0
  23. package/src/mcp/handlers/schema.js +1699 -0
  24. package/src/mcp/handlers/sql.js +559 -0
  25. package/src/mcp/handlers/users.js +251 -0
  26. package/src/mcp/job-store.js +199 -0
  27. package/src/mcp/server.js +428 -0
  28. package/src/mcp/tool-registry.js +3244 -0
  29. package/src/mcp/tool-router.js +149 -0
  30. package/src/metabase/client.js +737 -0
  31. package/src/metabase/metadata-client.js +1852 -0
  32. package/src/utils/activity-logger.js +489 -0
  33. package/src/utils/cache.js +176 -0
  34. package/src/utils/config.js +131 -0
  35. package/src/utils/definition-tables.js +938 -0
  36. package/src/utils/file-operations.js +496 -0
  37. package/src/utils/logger.js +45 -0
  38. package/src/utils/parametric-questions.js +627 -0
  39. package/src/utils/response-optimizer.js +190 -0
  40. package/src/utils/sql-sanitizer.js +97 -0
@@ -0,0 +1,686 @@
1
+ import { Client } from 'pg';
2
+ import mysql from 'mysql2/promise';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ export class DirectDatabaseClient {
6
+ constructor(connectionInfo, options = {}) {
7
+ this.connectionInfo = connectionInfo;
8
+ this.engine = connectionInfo.engine;
9
+ this.client = null;
10
+
11
+ // Güvenlik ayarları
12
+ this.options = {
13
+ prefix: options.prefix || 'claude_ai_',
14
+ defaultSchema: options.defaultSchema || null, // null = user's default schema
15
+ allowedSchemas: options.allowedSchemas || null, // null = all schemas allowed
16
+ allowedOperations: options.allowedOperations || [
17
+ 'CREATE_TABLE',
18
+ 'CREATE_VIEW',
19
+ 'CREATE_MATERIALIZED_VIEW',
20
+ 'CREATE_INDEX',
21
+ 'DROP_TABLE',
22
+ 'DROP_VIEW',
23
+ 'DROP_MATERIALIZED_VIEW',
24
+ 'DROP_INDEX',
25
+ 'SELECT',
26
+ 'INSERT',
27
+ 'UPDATE',
28
+ 'DELETE'
29
+ ],
30
+ maxExecutionTime: options.maxExecutionTime || 30000,
31
+ requireApproval: options.requireApproval !== false,
32
+ dryRun: options.dryRun || false
33
+ };
34
+ }
35
+
36
+ async connect() {
37
+ try {
38
+ switch (this.engine) {
39
+ case 'postgres':
40
+ this.client = new Client({
41
+ host: this.connectionInfo.host,
42
+ port: this.connectionInfo.port,
43
+ database: this.connectionInfo.dbname,
44
+ user: this.connectionInfo.user,
45
+ password: this.connectionInfo.password,
46
+ ssl: this.connectionInfo.ssl
47
+ });
48
+ await this.client.connect();
49
+ break;
50
+
51
+ case 'mysql':
52
+ this.client = await mysql.createConnection({
53
+ host: this.connectionInfo.host,
54
+ port: this.connectionInfo.port,
55
+ database: this.connectionInfo.dbname,
56
+ user: this.connectionInfo.user,
57
+ password: this.connectionInfo.password,
58
+ ssl: this.connectionInfo.ssl
59
+ });
60
+ break;
61
+
62
+ default:
63
+ throw new Error(`Unsupported database engine: ${this.engine}`);
64
+ }
65
+
66
+ logger.info(`Connected to ${this.engine} database: ${this.connectionInfo.name}`);
67
+ return true;
68
+ } catch (error) {
69
+ logger.error('Database connection failed:', error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ async disconnect() {
75
+ if (this.client) {
76
+ if (this.engine === 'postgres') {
77
+ await this.client.end();
78
+ } else if (this.engine === 'mysql') {
79
+ await this.client.end();
80
+ }
81
+ this.client = null;
82
+ logger.info('Database connection closed');
83
+ }
84
+ }
85
+
86
+ // Güvenli DDL operations
87
+ async executeDDL(sql, options = {}) {
88
+ await this.validateDDL(sql);
89
+
90
+ if (this.options.requireApproval && !options.approved) {
91
+ throw new Error('DDL operation requires approval. Set approved: true to execute.');
92
+ }
93
+
94
+ if (this.options.dryRun) {
95
+ logger.info('DRY RUN - Would execute DDL:', sql);
96
+ return { success: true, dryRun: true, sql };
97
+ }
98
+
99
+ try {
100
+ const result = await this.executeQuery(sql);
101
+ logger.info('DDL executed successfully:', sql);
102
+ return result;
103
+ } catch (error) {
104
+ logger.error('DDL execution failed:', error);
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ async validateDDL(sql) {
110
+ const upperSQL = sql.toUpperCase().trim();
111
+
112
+ // 1. Operation type kontrolü
113
+ const operationType = this.extractOperationType(upperSQL);
114
+ if (!this.options.allowedOperations.includes(operationType)) {
115
+ throw new Error(`Operation not allowed: ${operationType}`);
116
+ }
117
+
118
+ // 2. Prefix kontrolü
119
+ if (this.requiresPrefix(operationType)) {
120
+ const objectName = this.extractObjectName(sql, operationType);
121
+ if (!objectName.startsWith(this.options.prefix)) {
122
+ throw new Error(`Object name must start with prefix: ${this.options.prefix}`);
123
+ }
124
+ }
125
+
126
+ // 3. Dangerous operations kontrolü
127
+ this.checkDangerousOperations(upperSQL);
128
+
129
+ // 4. Table/view existence kontrolü for DROP operations
130
+ if (operationType.startsWith('DROP_')) {
131
+ const objectName = this.extractObjectName(sql, operationType);
132
+ await this.validateDropOperation(objectName, operationType);
133
+ }
134
+
135
+ return true;
136
+ }
137
+
138
+ extractOperationType(sql) {
139
+ if (sql.startsWith('CREATE TABLE')) return 'CREATE_TABLE';
140
+ if (sql.startsWith('CREATE VIEW')) return 'CREATE_VIEW';
141
+ if (sql.startsWith('CREATE MATERIALIZED VIEW')) return 'CREATE_MATERIALIZED_VIEW';
142
+ if (sql.startsWith('CREATE INDEX') || sql.startsWith('CREATE UNIQUE INDEX')) return 'CREATE_INDEX';
143
+ if (sql.startsWith('DROP TABLE')) return 'DROP_TABLE';
144
+ if (sql.startsWith('DROP VIEW')) return 'DROP_VIEW';
145
+ if (sql.startsWith('DROP MATERIALIZED VIEW')) return 'DROP_MATERIALIZED_VIEW';
146
+ if (sql.startsWith('DROP INDEX')) return 'DROP_INDEX';
147
+ if (sql.startsWith('SELECT')) return 'SELECT';
148
+ if (sql.startsWith('INSERT')) return 'INSERT';
149
+ if (sql.startsWith('UPDATE')) return 'UPDATE';
150
+ if (sql.startsWith('DELETE')) return 'DELETE';
151
+
152
+ throw new Error('Unsupported operation type');
153
+ }
154
+
155
+ requiresPrefix(operationType) {
156
+ return [
157
+ 'CREATE_TABLE',
158
+ 'CREATE_VIEW',
159
+ 'CREATE_MATERIALIZED_VIEW',
160
+ 'CREATE_INDEX',
161
+ 'DROP_TABLE',
162
+ 'DROP_VIEW',
163
+ 'DROP_MATERIALIZED_VIEW',
164
+ 'DROP_INDEX'
165
+ ].includes(operationType);
166
+ }
167
+
168
+ extractObjectName(sql, operationType) {
169
+ // Basit regex ile object name çıkarma
170
+ let pattern;
171
+
172
+ switch (operationType) {
173
+ case 'CREATE_TABLE':
174
+ pattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w\.]+)/i;
175
+ break;
176
+ case 'CREATE_VIEW':
177
+ pattern = /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+([\w\.]+)/i;
178
+ break;
179
+ case 'CREATE_MATERIALIZED_VIEW':
180
+ pattern = /CREATE\s+MATERIALIZED\s+VIEW\s+([\w\.]+)/i;
181
+ break;
182
+ case 'CREATE_INDEX':
183
+ pattern = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w\.]+)/i;
184
+ break;
185
+ case 'DROP_TABLE':
186
+ pattern = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w\.]+)/i;
187
+ break;
188
+ case 'DROP_VIEW':
189
+ pattern = /DROP\s+VIEW\s+(?:IF\s+EXISTS\s+)?([\w\.]+)/i;
190
+ break;
191
+ case 'DROP_MATERIALIZED_VIEW':
192
+ pattern = /DROP\s+MATERIALIZED\s+VIEW\s+(?:IF\s+EXISTS\s+)?([\w\.]+)/i;
193
+ break;
194
+ case 'DROP_INDEX':
195
+ pattern = /DROP\s+INDEX\s+(?:IF\s+EXISTS\s+)?([\w\.]+)/i;
196
+ break;
197
+ default:
198
+ throw new Error('Cannot extract object name for this operation');
199
+ }
200
+
201
+ const match = sql.match(pattern);
202
+ if (!match) {
203
+ throw new Error('Could not extract object name from SQL');
204
+ }
205
+
206
+ // Schema.table formatından sadece table adını al
207
+ const fullName = match[1];
208
+ const parts = fullName.split('.');
209
+ return parts[parts.length - 1];
210
+ }
211
+
212
+ checkDangerousOperations(sql) {
213
+ const dangerous = [
214
+ 'DROP DATABASE',
215
+ 'DROP SCHEMA',
216
+ 'TRUNCATE',
217
+ 'ALTER SYSTEM',
218
+ 'CREATE USER',
219
+ 'DROP USER',
220
+ 'GRANT',
221
+ 'REVOKE'
222
+ ];
223
+
224
+ for (const op of dangerous) {
225
+ if (sql.includes(op)) {
226
+ throw new Error(`Dangerous operation not allowed: ${op}`);
227
+ }
228
+ }
229
+ }
230
+
231
+ async validateDropOperation(objectName, operationType) {
232
+ // Sadece kendi prefix'li objeleri silme izni
233
+ if (!objectName.startsWith(this.options.prefix)) {
234
+ throw new Error(`Cannot drop object not created by AI: ${objectName}`);
235
+ }
236
+
237
+ // Objenin gerçekten var olduğunu kontrol et
238
+ const exists = await this.checkObjectExists(objectName, operationType);
239
+ if (!exists) {
240
+ logger.warn(`Object ${objectName} does not exist, but continuing with DROP IF EXISTS`);
241
+ }
242
+ }
243
+
244
+ async checkObjectExists(objectName, operationType) {
245
+ let checkSQL;
246
+
247
+ if (this.engine === 'postgres') {
248
+ switch (operationType) {
249
+ case 'DROP_TABLE':
250
+ checkSQL = `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)`;
251
+ break;
252
+ case 'DROP_VIEW':
253
+ checkSQL = `SELECT EXISTS (SELECT FROM information_schema.views WHERE table_name = $1)`;
254
+ break;
255
+ case 'DROP_MATERIALIZED_VIEW':
256
+ checkSQL = `SELECT EXISTS (SELECT FROM pg_matviews WHERE matviewname = $1)`;
257
+ break;
258
+ case 'DROP_INDEX':
259
+ checkSQL = `SELECT EXISTS (SELECT FROM pg_indexes WHERE indexname = $1)`;
260
+ break;
261
+ default:
262
+ return true;
263
+ }
264
+
265
+ const result = await this.client.query(checkSQL, [objectName]);
266
+ return result.rows[0].exists;
267
+ }
268
+
269
+ return true; // Default olarak var kabul et
270
+ }
271
+
272
+ // DDL Helper Methods
273
+ async createTable(tableName, columns, options = {}) {
274
+ if (!tableName.startsWith(this.options.prefix)) {
275
+ tableName = this.options.prefix + tableName;
276
+ }
277
+
278
+ // Schema handling
279
+ const schema = options.schema || this.options.defaultSchema;
280
+ const fullTableName = schema ? `${schema}.${tableName}` : tableName;
281
+
282
+ const columnsSQL = columns.map(col =>
283
+ `${col.name} ${col.type}${col.constraints ? ' ' + col.constraints : ''}`
284
+ ).join(', ');
285
+
286
+ const sql = `CREATE TABLE ${fullTableName} (${columnsSQL})`;
287
+
288
+ return await this.executeDDL(sql, options);
289
+ }
290
+
291
+ async createView(viewName, selectSQL, options = {}) {
292
+ if (!viewName.startsWith(this.options.prefix)) {
293
+ viewName = this.options.prefix + viewName;
294
+ }
295
+
296
+ // Schema handling
297
+ const schema = options.schema || this.options.defaultSchema;
298
+ const fullViewName = schema ? `${schema}.${viewName}` : viewName;
299
+
300
+ const sql = `CREATE VIEW ${fullViewName} AS ${selectSQL}`;
301
+
302
+ return await this.executeDDL(sql, options);
303
+ }
304
+
305
+ async createMaterializedView(viewName, selectSQL, options = {}) {
306
+ if (!viewName.startsWith(this.options.prefix)) {
307
+ viewName = this.options.prefix + viewName;
308
+ }
309
+
310
+ // Schema handling
311
+ const schema = options.schema || this.options.defaultSchema;
312
+ const fullViewName = schema ? `${schema}.${viewName}` : viewName;
313
+
314
+ const sql = `CREATE MATERIALIZED VIEW ${fullViewName} AS ${selectSQL}`;
315
+
316
+ return await this.executeDDL(sql, options);
317
+ }
318
+
319
+ async createIndex(indexName, tableName, columns, options = {}) {
320
+ if (!indexName.startsWith(this.options.prefix)) {
321
+ indexName = this.options.prefix + indexName;
322
+ }
323
+
324
+ const unique = options.unique ? 'UNIQUE ' : '';
325
+ const columnsStr = Array.isArray(columns) ? columns.join(', ') : columns;
326
+
327
+ const sql = `CREATE ${unique}INDEX ${indexName} ON ${tableName} (${columnsStr})`;
328
+
329
+ return await this.executeDDL(sql, options);
330
+ }
331
+
332
+ // DDL okuma metodları
333
+ async getTableDDL(tableName) {
334
+ if (this.engine === 'postgres') {
335
+ const sql = `
336
+ SELECT
337
+ 'CREATE TABLE ' || schemaname || '.' || tablename || ' (' ||
338
+ string_agg(
339
+ column_name || ' ' || data_type ||
340
+ CASE
341
+ WHEN character_maximum_length IS NOT NULL
342
+ THEN '(' || character_maximum_length || ')'
343
+ ELSE ''
344
+ END ||
345
+ CASE
346
+ WHEN is_nullable = 'NO' THEN ' NOT NULL'
347
+ ELSE ''
348
+ END,
349
+ ', '
350
+ ) || ');' as ddl
351
+ FROM information_schema.columns
352
+ WHERE table_name = $1
353
+ GROUP BY schemaname, tablename
354
+ `;
355
+
356
+ const result = await this.client.query(sql, [tableName]);
357
+ return result.rows[0]?.ddl;
358
+ }
359
+
360
+ return null;
361
+ }
362
+
363
+ async getViewDDL(viewName) {
364
+ if (this.engine === 'postgres') {
365
+ const sql = `
366
+ SELECT 'CREATE VIEW ' || schemaname || '.' || viewname || ' AS ' || definition as ddl
367
+ FROM pg_views
368
+ WHERE viewname = $1
369
+ `;
370
+
371
+ const result = await this.client.query(sql, [viewName]);
372
+ return result.rows[0]?.ddl;
373
+ }
374
+
375
+ return null;
376
+ }
377
+
378
+ async listOwnObjects() {
379
+ const objects = {
380
+ tables: [],
381
+ views: [],
382
+ materialized_views: [],
383
+ indexes: []
384
+ };
385
+
386
+ if (this.engine === 'postgres') {
387
+ // Tables with schema info
388
+ const tablesResult = await this.client.query(`
389
+ SELECT table_name, table_schema
390
+ FROM information_schema.tables
391
+ WHERE table_name LIKE $1
392
+ ORDER BY table_schema, table_name
393
+ `, [this.options.prefix + '%']);
394
+ objects.tables = tablesResult.rows;
395
+
396
+ // Views with schema info
397
+ const viewsResult = await this.client.query(`
398
+ SELECT table_name as view_name, table_schema
399
+ FROM information_schema.views
400
+ WHERE table_name LIKE $1
401
+ ORDER BY table_schema, table_name
402
+ `, [this.options.prefix + '%']);
403
+ objects.views = viewsResult.rows;
404
+
405
+ // Materialized Views with schema info
406
+ const matViewsResult = await this.client.query(`
407
+ SELECT matviewname, schemaname
408
+ FROM pg_matviews
409
+ WHERE matviewname LIKE $1
410
+ ORDER BY schemaname, matviewname
411
+ `, [this.options.prefix + '%']);
412
+ objects.materialized_views = matViewsResult.rows;
413
+
414
+ // Indexes with schema info
415
+ const indexesResult = await this.client.query(`
416
+ SELECT indexname, schemaname, tablename
417
+ FROM pg_indexes
418
+ WHERE indexname LIKE $1
419
+ ORDER BY schemaname, indexname
420
+ `, [this.options.prefix + '%']);
421
+ objects.indexes = indexesResult.rows;
422
+ }
423
+
424
+ return objects;
425
+ }
426
+
427
+ async getSchemas() {
428
+ if (this.engine === 'postgres') {
429
+ const result = await this.client.query(`
430
+ SELECT schema_name
431
+ FROM information_schema.schemata
432
+ WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
433
+ ORDER BY schema_name
434
+ `);
435
+ return result.rows.map(row => row.schema_name);
436
+ }
437
+ return [];
438
+ }
439
+
440
+ async getCurrentSchema() {
441
+ if (this.engine === 'postgres') {
442
+ const result = await this.client.query('SELECT current_schema()');
443
+ return result.rows[0].current_schema;
444
+ }
445
+ return null;
446
+ }
447
+
448
+ // Schema ve tablo keşfi metodları
449
+ async exploreSchemaTablesDetailed(schemaName, includeColumns = true, limit = null) {
450
+ if (this.engine === 'postgres') {
451
+ const tableQuery = `
452
+ SELECT
453
+ t.table_name,
454
+ t.table_type,
455
+ obj_description(c.oid) as table_comment,
456
+ pg_size_pretty(pg_total_relation_size(c.oid)) as table_size
457
+ FROM information_schema.tables t
458
+ LEFT JOIN pg_class c ON c.relname = t.table_name
459
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
460
+ WHERE t.table_schema = $1
461
+ AND n.nspname = $1
462
+ ORDER BY t.table_name
463
+ ${limit ? `LIMIT ${limit}` : ''}
464
+ `;
465
+
466
+ const tables = await this.client.query(tableQuery, [schemaName]);
467
+ const result = [];
468
+
469
+ for (const table of tables.rows) {
470
+ const tableInfo = {
471
+ name: table.table_name,
472
+ type: table.table_type,
473
+ comment: table.table_comment,
474
+ size: table.table_size,
475
+ columns: []
476
+ };
477
+
478
+ if (includeColumns) {
479
+ const columnQuery = `
480
+ SELECT
481
+ c.column_name,
482
+ c.data_type,
483
+ c.is_nullable,
484
+ c.column_default,
485
+ c.character_maximum_length,
486
+ c.numeric_precision,
487
+ c.numeric_scale,
488
+ col_description(pgc.oid, c.ordinal_position) as column_comment,
489
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
490
+ CASE WHEN fk.column_name IS NOT NULL THEN true ELSE false END as is_foreign_key,
491
+ fk.foreign_table_name,
492
+ fk.foreign_column_name
493
+ FROM information_schema.columns c
494
+ LEFT JOIN pg_class pgc ON pgc.relname = c.table_name
495
+ LEFT JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace AND pgn.nspname = c.table_schema
496
+ LEFT JOIN (
497
+ SELECT ku.column_name, tc.table_name
498
+ FROM information_schema.table_constraints tc
499
+ JOIN information_schema.key_column_usage ku
500
+ ON tc.constraint_name = ku.constraint_name
501
+ WHERE tc.constraint_type = 'PRIMARY KEY'
502
+ AND tc.table_schema = $1
503
+ ) pk ON pk.column_name = c.column_name AND pk.table_name = c.table_name
504
+ LEFT JOIN (
505
+ SELECT
506
+ kcu.column_name,
507
+ kcu.table_name,
508
+ ccu.table_name as foreign_table_name,
509
+ ccu.column_name as foreign_column_name
510
+ FROM information_schema.table_constraints tc
511
+ JOIN information_schema.key_column_usage kcu
512
+ ON tc.constraint_name = kcu.constraint_name
513
+ JOIN information_schema.constraint_column_usage ccu
514
+ ON ccu.constraint_name = tc.constraint_name
515
+ WHERE tc.constraint_type = 'FOREIGN KEY'
516
+ AND tc.table_schema = $1
517
+ ) fk ON fk.column_name = c.column_name AND fk.table_name = c.table_name
518
+ WHERE c.table_schema = $1 AND c.table_name = $2
519
+ ORDER BY c.ordinal_position
520
+ `;
521
+
522
+ const columns = await this.client.query(columnQuery, [schemaName, table.table_name]);
523
+ tableInfo.columns = columns.rows.map(col => ({
524
+ name: col.column_name,
525
+ type: col.data_type,
526
+ nullable: col.is_nullable === 'YES',
527
+ default: col.column_default,
528
+ length: col.character_maximum_length,
529
+ precision: col.numeric_precision,
530
+ scale: col.numeric_scale,
531
+ comment: col.column_comment,
532
+ isPrimaryKey: col.is_primary_key,
533
+ isForeignKey: col.is_foreign_key,
534
+ foreignTable: col.foreign_table_name,
535
+ foreignColumn: col.foreign_column_name
536
+ }));
537
+ }
538
+
539
+ result.push(tableInfo);
540
+ }
541
+
542
+ return result;
543
+ }
544
+ return [];
545
+ }
546
+
547
+ async analyzeTableRelationships(schemaName, tableNames = null) {
548
+ if (this.engine === 'postgres') {
549
+ const tableFilter = tableNames ?
550
+ `AND tc.table_name = ANY($2)` : '';
551
+ const params = tableNames ? [schemaName, tableNames] : [schemaName];
552
+
553
+ const query = `
554
+ SELECT DISTINCT
555
+ tc.table_name as source_table,
556
+ kcu.column_name as source_column,
557
+ ccu.table_name as target_table,
558
+ ccu.column_name as target_column,
559
+ tc.constraint_name,
560
+ rc.update_rule,
561
+ rc.delete_rule
562
+ FROM information_schema.table_constraints tc
563
+ JOIN information_schema.key_column_usage kcu
564
+ ON tc.constraint_name = kcu.constraint_name
565
+ JOIN information_schema.constraint_column_usage ccu
566
+ ON ccu.constraint_name = tc.constraint_name
567
+ JOIN information_schema.referential_constraints rc
568
+ ON rc.constraint_name = tc.constraint_name
569
+ WHERE tc.constraint_type = 'FOREIGN KEY'
570
+ AND tc.table_schema = $1
571
+ ${tableFilter}
572
+ ORDER BY tc.table_name, kcu.column_name
573
+ `;
574
+
575
+ const result = await this.client.query(query, params);
576
+ return result.rows.map(row => ({
577
+ sourceTable: row.source_table,
578
+ sourceColumn: row.source_column,
579
+ targetTable: row.target_table,
580
+ targetColumn: row.target_column,
581
+ constraintName: row.constraint_name,
582
+ updateRule: row.update_rule,
583
+ deleteRule: row.delete_rule,
584
+ relationshipType: 'many-to-one' // FK genelde many-to-one
585
+ }));
586
+ }
587
+ return [];
588
+ }
589
+
590
+ async suggestVirtualRelationships(schemaName, confidenceThreshold = 0.7) {
591
+ if (this.engine === 'postgres') {
592
+ // Naming convention bazlı ilişki önerileri
593
+ const query = `
594
+ WITH table_columns AS (
595
+ SELECT
596
+ t.table_name,
597
+ c.column_name,
598
+ c.data_type,
599
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
600
+ FROM information_schema.tables t
601
+ JOIN information_schema.columns c ON t.table_name = c.table_name
602
+ LEFT JOIN (
603
+ SELECT ku.column_name, tc.table_name
604
+ FROM information_schema.table_constraints tc
605
+ JOIN information_schema.key_column_usage ku
606
+ ON tc.constraint_name = ku.constraint_name
607
+ WHERE tc.constraint_type = 'PRIMARY KEY'
608
+ AND tc.table_schema = $1
609
+ ) pk ON pk.column_name = c.column_name AND pk.table_name = c.table_name
610
+ WHERE t.table_schema = $1
611
+ AND t.table_type = 'BASE TABLE'
612
+ ),
613
+ potential_relationships AS (
614
+ SELECT DISTINCT
615
+ tc1.table_name as source_table,
616
+ tc1.column_name as source_column,
617
+ tc2.table_name as target_table,
618
+ tc2.column_name as target_column,
619
+ CASE
620
+ WHEN tc1.column_name = tc2.column_name THEN 0.9
621
+ WHEN tc1.column_name LIKE '%_id' AND tc2.column_name = 'id'
622
+ AND tc1.column_name = tc2.table_name || '_id' THEN 0.95
623
+ WHEN tc1.column_name LIKE '%_id' AND tc2.is_primary_key THEN 0.8
624
+ WHEN tc1.column_name LIKE tc2.table_name || '%' THEN 0.7
625
+ ELSE 0.5
626
+ END as confidence
627
+ FROM table_columns tc1
628
+ JOIN table_columns tc2
629
+ ON tc1.data_type = tc2.data_type
630
+ AND tc1.table_name != tc2.table_name
631
+ WHERE tc2.is_primary_key = true
632
+ AND tc1.column_name != 'id'
633
+ )
634
+ SELECT * FROM potential_relationships
635
+ WHERE confidence >= $2
636
+ ORDER BY confidence DESC, source_table, source_column
637
+ `;
638
+
639
+ const result = await this.client.query(query, [schemaName, confidenceThreshold]);
640
+ return result.rows.map(row => ({
641
+ sourceTable: row.source_table,
642
+ sourceColumn: row.source_column,
643
+ targetTable: row.target_table,
644
+ targetColumn: row.target_column,
645
+ confidence: parseFloat(row.confidence),
646
+ relationshipType: 'many-to-one',
647
+ reasoning: this.explainRelationshipSuggestion(row)
648
+ }));
649
+ }
650
+ return [];
651
+ }
652
+
653
+ explainRelationshipSuggestion(relationship) {
654
+ const { source_column, target_column, target_table, confidence } = relationship;
655
+
656
+ if (confidence >= 0.95) {
657
+ return `Strong match: ${source_column} follows naming convention for ${target_table}.${target_column}`;
658
+ } else if (confidence >= 0.9) {
659
+ return `Very likely: Column names match exactly`;
660
+ } else if (confidence >= 0.8) {
661
+ return `Likely: ${source_column} appears to reference primary key of ${target_table}`;
662
+ } else if (confidence >= 0.7) {
663
+ return `Possible: Column name suggests relationship with ${target_table}`;
664
+ }
665
+ return `Low confidence: Data types match but naming unclear`;
666
+ }
667
+
668
+ // Genel query execution
669
+ async executeQuery(sql) {
670
+ if (!this.client) {
671
+ throw new Error('Database not connected');
672
+ }
673
+
674
+ try {
675
+ if (this.engine === 'postgres') {
676
+ return await this.client.query(sql);
677
+ } else if (this.engine === 'mysql') {
678
+ const [rows, fields] = await this.client.execute(sql);
679
+ return { rows, fields };
680
+ }
681
+ } catch (error) {
682
+ logger.error('Query execution failed:', error);
683
+ throw error;
684
+ }
685
+ }
686
+ }