database-debug.ts•11.1 kB
import { z } from 'zod';
import { Logger } from '../server/logger';
import { MetricsCollector } from '../server/metrics';
import { NetMvcAdapter } from '../adapters/netmvc-adapter';
import {
DapDatabaseQueryDebugRequest,
DapDatabaseQueryResponse,
} from '../schemas/netmvc-tools.schemas';
/**
* dap.debugDatabaseQuery tool implementation
*
* Handles Entity Framework Core query debugging with:
* - Query interception and logging
* - Performance analysis
* - Execution plan visualization
* - Query optimization suggestions
*/
export class DatabaseDebugTool {
private logger: Logger;
private metrics: MetricsCollector;
private adapter: NetMvcAdapter;
constructor() {
this.logger = new Logger('dap.debugDatabaseQuery');
this.metrics = MetricsCollector.getInstance();
this.adapter = new NetMvcAdapter();
}
/**
* Execute database query debugging
*/
async execute(args: any): Promise<DapDatabaseQueryResponse> {
this.metrics.startTimer('dap.debugDatabaseQuery.tool');
this.metrics.increment('dap.debugDatabaseQuery.count');
try {
// Validate input
const validatedArgs = this.validateArgs(args);
this.logger.debug('Debugging database query', {
queryId: validatedArgs.queryId,
queryText:
validatedArgs.queryText.substring(0, 100) +
(validatedArgs.queryText.length > 100 ? '...' : ''),
executionTime: validatedArgs.executionTime,
});
// Process database query debugging
const debugResult = await this.debugDatabaseQuery(validatedArgs);
this.logger.info('Database query debugging completed', {
queryId: debugResult.queryId,
executionTime: debugResult.executionTime,
queryType: debugResult.commandType,
});
this.metrics.stopTimer('dap.debugDatabaseQuery.tool');
return this.createSuccessResponse(debugResult);
} catch (error) {
this.logger.error('Database query debugging failed:', error);
this.metrics.increment('dap.debugDatabaseQuery.error.count');
this.metrics.stopTimer('dap.debugDatabaseQuery.tool');
return this.createErrorResponse((error as Error).message);
}
}
/**
* Validate input arguments
*/
private validateArgs(args: any): DapDatabaseQueryDebugRequest['arguments'] {
const schema = z.object({
queryId: z.string().optional(),
queryText: z.string(),
parameters: z.record(z.any()).optional(),
parameterTypes: z.record(z.string()).optional(),
executionTime: z.number().optional(),
connection: z.string().optional(),
isAsync: z.boolean().optional(),
entityType: z.string().optional(),
dbContext: z.string().optional(),
transaction: z.string().optional(),
commandType: z.enum(['Text', 'StoredProcedure', 'TableDirect']).optional(),
tableMappings: z.record(z.any()).optional(),
changeTracking: z.boolean().optional(),
});
return schema.parse(args);
}
/**
* Process database query debugging
*/
private async debugDatabaseQuery(args: DapDatabaseQueryDebugRequest['arguments']): Promise<any> {
// Mock database query processing
const query = {
queryId: args.queryId || `db_query_${Date.now()}`,
timestamp: Date.now(),
queryText: args.queryText,
parameters: args.parameters || {},
parameterTypes: args.parameterTypes || {},
executionTime: args.executionTime || Math.random() * 100 + 5,
connection: args.connection || 'DefaultConnection',
isAsync: args.isAsync || true,
entityType: args.entityType || 'User',
dbContext: args.dbContext || 'ApplicationDbContext',
transaction: args.transaction || null,
commandType: args.commandType || 'Text',
tableMappings: args.tableMappings || {},
changeTracking: args.changeTracking !== false,
securityEnabled: true,
cacheEnabled: true,
queryOptimized: false,
queryTimeout: 30,
commandTimeout: 30,
fetchOptions: { noTracking: false },
eagerLoadingEnabled: true,
lazyLoadingEnabled: true,
changeTracker: {
hasChanges: false,
autoDetectChanges: true,
validateOnSaveEnabled: true,
},
context: {
configurationId: 'ApplicationDbContext',
providerName: 'Microsoft.EntityFrameworkCore.SqlServer',
providerType: 'Microsoft.EntityFrameworkCore.SqlServer.SqlServerDbContextOptions',
connectionId: 'DefaultConnection',
dataSource: '(localdb)\\mssqllocaldb',
database: 'MyDatabase',
commandTimeout: 30,
maxBatchSize: 42,
minBatchSize: 1,
useRelationalNulls: true,
querySplittingBehavior: 'SplitQuery',
parameterTypeMapping: { '@p0': 'Int32' },
},
// Analyzed query information
analysis: {
complexity: 'Moderate',
estimatedRows: Math.floor(Math.random() * 1000) + 1,
usesIndex: true,
indexUsed: 'IX_Users_Email',
canOptimize: true,
optimizationSuggestions: [
'Consider adding an index on CreatedAt column',
'Use SELECT specific columns instead of *',
'Add pagination for large result sets',
],
riskLevel: 'Low',
securityScore: 95,
},
// Execution plan
executionPlan: this.generateExecutionPlan(args.queryText),
// Performance metrics
performance: {
cpuTime: Math.random() * 50 + 10,
logicalReads: Math.floor(Math.random() * 1000) + 100,
physicalReads: Math.floor(Math.random() * 100) + 10,
writeIO: Math.floor(Math.random() * 50) + 5,
tempdbUsage: Math.floor(Math.random() * 100) + 20,
lockWaitTime: Math.random() * 5,
memoryGrant: Math.floor(Math.random() * 1000) + 100,
},
// EF Core specific
entityFramework: {
queryType: this.detectQueryType(args.queryText),
changeDetection: args.changeTracking ? 'Auto' : 'None',
navigationLoading: this.detectNavigationLoading(args.queryText),
translation: this.translateLinqToSql(args.queryText),
clientEvaluation: false,
compiledQuery: false,
queryCacheHit: false,
},
// Query history
history: {
similarQueries: 3,
averageExecutionTime: Math.random() * 50 + 10,
successRate: 98.5,
errorRate: 1.5,
},
};
// Add to adapter tracking
await this.adapter.debugDatabaseQuery(query);
return query;
}
/**
* Generate execution plan
*/
private generateExecutionPlan(_queryText: string): string {
const plans = [
`
|--Filter(O=[@p0])
|--Clustered Index Seek(O=[MyDatabase].[dbo].[Users].[PK_Users]),
Seek Keys: [Users.Id = @p0]
`,
`
|--Nested Loops(Inner Join, WHERE:([o].[UserId] = [@p0]))
|--Clustered Index Seek(O=[MyDatabase].[dbo].[Orders].[PK_Orders]),
Seek Keys: [Orders.UserId = @p0]
|--Clustered Index Seek(O=[MyDatabase].[dbo].[Products].[PK_Products]),
Seek Keys: [Products.Id = Orders.ProductId]
`,
`
|--Hash Match(Inner Join, HASH:([Categories].[Id]),
RES:([CategoryId]))
|--Table Scan(O=[MyDatabase].[dbo].[Products]),
WHERE:([Products].[CategoryId]= [Categories].[Id])
|--Clustered Index Seek(O=[MyDatabase].[dbo].[Categories].[PK_Categories])
`,
];
return plans[Math.floor(Math.random() * plans.length)] || '';
}
/**
* Detect query type
*/
private detectQueryType(queryText: string): string {
const lower = (queryText || '').toLowerCase();
if (lower.includes('from users')) return 'TableScan';
if (lower.includes('select count')) return 'Aggregate';
if (lower.includes('join')) return 'Join';
if (lower.includes('where') && lower.includes('like')) return 'Search';
if (lower.includes('group by')) return 'Grouping';
if (lower.includes('order by')) return 'Sorting';
if (lower.includes('insert into')) return 'Insert';
if (lower.includes('update')) return 'Update';
if (lower.includes('delete')) return 'Delete';
return 'Select';
}
/**
* Detect navigation loading
*/
private detectNavigationLoading(queryText: string): string[] {
const navigations = [];
if (queryText.includes('Users.Products')) navigations.push('User.Products');
if (queryText.includes('Users.Orders')) navigations.push('User.Orders');
if (queryText.includes('Products.Category')) navigations.push('Product.Category');
if (queryText.includes('Orders.User')) navigations.push('Order.User');
return navigations;
}
/**
* Translate LINQ to SQL
*/
private translateLinqToSql(linqQuery: string): string {
const translations = {
'Where(u => u.Id == @p0)': 'WHERE [Users].[Id] = @p0',
'Where(u => u.Email.Contains(@p0))': 'WHERE [Users].[Email] LIKE @p0',
'Select(u => u.Email)': 'SELECT [Users].[Email]',
'OrderBy(u => u.Name)': 'ORDER BY [Users].[Name]',
'ThenBy(u => u.Email)': 'ORDER BY [Users].[Email]',
'Count()': 'COUNT(*)',
'Sum(u => u.Price)': 'SUM([Products].[Price])',
'Average(u => u.Rating)': 'AVG([Products].[Rating])',
'GroupBy(u => u.Category)': 'GROUP BY [Products].[Category]',
'Join(p => p.Category)':
'INNER JOIN [Categories] ON [Products].[CategoryId] = [Categories].[Id]',
};
let sql = linqQuery;
for (const [linq, sqlText] of Object.entries(translations)) {
sql = sql.replace(linq, sqlText);
}
return `SELECT * FROM Products ${sql}`;
}
/**
* Create success response
*/
private createSuccessResponse(body: any): DapDatabaseQueryResponse {
return {
type: 'response',
seq: 1,
command: 'debugDatabaseQuery',
request_seq: 1,
success: true,
body: {
success: true,
queryId: body.queryId,
message: 'Database query debug completed successfully',
databaseQuery: body,
executionPlan: body.executionPlan,
queryAnalysis: {
complexity: body.analysis.complexity,
estimatedRows: body.analysis.estimatedRows,
usesIndex: body.analysis.usesIndex,
riskLevel: body.analysis.riskLevel,
securityScore: body.analysis.securityScore,
optimizationSuggestions: body.analysis.optimizationSuggestions,
},
// performance: body.performance, // Removed - not in schema
},
};
}
/**
* Create error response
*/
private createErrorResponse(message: string): DapDatabaseQueryResponse {
return {
type: 'response',
seq: 1,
command: 'debugDatabaseQuery',
request_seq: 1,
success: false,
message,
body: {
message: 'Failed to debug database query',
success: false,
queryId: 'error-' + Date.now(),
},
};
}
}
// Singleton instance
export const databaseDebugTool = new DatabaseDebugTool();
// Tool execution function
export async function executeDatabaseDebug(args: any): Promise<DapDatabaseQueryResponse> {
return await databaseDebugTool.execute(args);
}