@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.
- package/.env.example +38 -0
- package/LICENSE +201 -0
- package/README.md +364 -0
- package/README_MCP.md +279 -0
- package/package.json +99 -0
- package/src/ai/assistant.js +982 -0
- package/src/cli/interactive.js +500 -0
- package/src/database/connection-manager.js +350 -0
- package/src/database/direct-client.js +686 -0
- package/src/index.js +162 -0
- package/src/mcp/handlers/actions.js +213 -0
- package/src/mcp/handlers/ai.js +207 -0
- package/src/mcp/handlers/analytics.js +1647 -0
- package/src/mcp/handlers/cards.js +1544 -0
- package/src/mcp/handlers/collections.js +244 -0
- package/src/mcp/handlers/dashboard.js +207 -0
- package/src/mcp/handlers/dashboard_direct.js +292 -0
- package/src/mcp/handlers/database.js +322 -0
- package/src/mcp/handlers/docs.js +399 -0
- package/src/mcp/handlers/index.js +35 -0
- package/src/mcp/handlers/metadata.js +190 -0
- package/src/mcp/handlers/questions.js +134 -0
- package/src/mcp/handlers/schema.js +1699 -0
- package/src/mcp/handlers/sql.js +559 -0
- package/src/mcp/handlers/users.js +251 -0
- package/src/mcp/job-store.js +199 -0
- package/src/mcp/server.js +428 -0
- package/src/mcp/tool-registry.js +3244 -0
- package/src/mcp/tool-router.js +149 -0
- package/src/metabase/client.js +737 -0
- package/src/metabase/metadata-client.js +1852 -0
- package/src/utils/activity-logger.js +489 -0
- package/src/utils/cache.js +176 -0
- package/src/utils/config.js +131 -0
- package/src/utils/definition-tables.js +938 -0
- package/src/utils/file-operations.js +496 -0
- package/src/utils/logger.js +45 -0
- package/src/utils/parametric-questions.js +627 -0
- package/src/utils/response-optimizer.js +190 -0
- 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
|
+
}
|