hana-cli
Version:
HANA Developer Command Line Interface
391 lines (346 loc) • 12 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import { buildDocEpilogue } from '../utils/doc-linker.js'
export const command = 'recommendations'
export const aliases = ['rec', 'recommend']
export const describe = baseLite.bundle.getText("recommendations")
export const builder = (yargs) => yargs.options(baseLite.getBuilder({
category: {
alias: ['c'],
type: 'string',
default: 'all',
choices: ['all', 'indexes', 'queries', 'cache', 'tables', 'locks', 'memory'],
desc: 'Recommendation category'
},
limit: {
alias: ['l'],
type: 'number',
default: 50,
desc: baseLite.bundle.getText("limit")
}
})).wrap(160).example('hana-cli recommendations --category indexes --limit 25', baseLite.bundle.getText('recommendationsExample')).wrap(160).epilog(buildDocEpilogue('recommendations', 'performance-monitoring', ['healthCheck', 'expensiveStatements']))
export let inputPrompts = {
category: {
description: 'Recommendation category',
type: 'string',
required: true
},
limit: {
description: baseLite.bundle.getText("limit"),
type: 'number',
required: true
}
}
/**
* Command handler function
* @param {object} argv - Command line arguments from yargs
* @returns {Promise<void>}
*/
export async function handler(argv) {
const base = await import('../utils/base.js')
base.promptHandler(argv, getRecommendations, inputPrompts)
}
/**
* AI-based performance recommendations
* @param {object} prompts - Input prompts
* @returns {Promise<void>}
*/
export async function getRecommendations(prompts) {
const base = await import('../utils/base.js')
base.debug('getRecommendations')
try {
base.setPrompts(prompts)
const db = await base.createDBConnection()
const category = prompts.category
const limit = base.validateLimit(prompts.limit)
base.output('')
base.output(base.colors.bold(base.bundle.getText('aiRecommendationsHeader')))
base.output(`${base.bundle.getText('categoryLabel')}: ${category === 'all' ? base.bundle.getText('allCategories') : category}`)
base.output('')
const recommendations = []
// Get index recommendations
if (category === 'all' || category === 'indexes') {
try {
const indexRecs = await getIndexRecommendations(db, base, limit)
if (indexRecs && indexRecs.length > 0) {
recommendations.push({ category: 'Missing Indexes', items: indexRecs })
}
} catch (error) {
base.debug(`${base.bundle.getText('errorGettingDetails')}: ${error.message}`)
}
}
// Get query optimization recommendations
if (category === 'all' || category === 'queries') {
try {
const queryRecs = await getQueryOptimizationRecommendations(db, base, limit)
if (queryRecs && queryRecs.length > 0) {
recommendations.push({ category: 'Query Optimization', items: queryRecs })
}
} catch (error) {
base.debug(`Error getting query recommendations: ${error.message}`)
}
}
// Get cache recommendations
if (category === 'all' || category === 'cache') {
try {
const cacheRecs = await getCacheRecommendations(db, base)
if (cacheRecs && cacheRecs.length > 0) {
recommendations.push({ category: 'Cache Optimization', items: cacheRecs })
}
} catch (error) {
base.debug(`Error getting cache recommendations: ${error.message}`)
}
}
// Get table structure recommendations
if (category === 'all' || category === 'tables') {
try {
const tableRecs = await getTableRecommendations(db, base, limit)
if (tableRecs && tableRecs.length > 0) {
recommendations.push({ category: 'Table Structure', items: tableRecs })
}
} catch (error) {
base.debug(`Error getting table recommendations: ${error.message}`)
}
}
// Get lock optimization recommendations
if (category === 'all' || category === 'locks') {
try {
const lockRecs = await getLockOptimizationRecommendations(db, base)
if (lockRecs && lockRecs.length > 0) {
recommendations.push({ category: 'Lock Optimization', items: lockRecs })
}
} catch (error) {
base.debug(`Error getting lock recommendations: ${error.message}`)
}
}
// Get memory optimization recommendations
if (category === 'all' || category === 'memory') {
try {
const memoryRecs = await getMemoryOptimizationRecommendations(db, base)
if (memoryRecs && memoryRecs.length > 0) {
recommendations.push({ category: 'Memory Optimization', items: memoryRecs })
}
} catch (error) {
base.debug(`Error getting memory recommendations: ${error.message}`)
}
}
if (recommendations.length === 0) {
base.output(base.bundle.getText('noRecommendationsAvailable'))
await base.end()
return
}
// Display recommendations grouped by category
recommendations.forEach((rec, idx) => {
base.output(base.colors.bold(`${idx + 1}. ${rec.category}`))
rec.items.forEach((item, itemIdx) => {
base.output(` ${itemIdx + 1}. ${item}`)
})
base.output('')
})
base.output(`${base.bundle.getText('totalRecommendations')}: ${recommendations.reduce((sum, r) => sum + r.items.length, 0)}`)
base.output('')
await base.end()
} catch (error) {
await base.error(error)
}
}
/**
* Get index recommendations
*/
async function getIndexRecommendations(db, base, limit) {
const recommendations = []
try {
// Check for frequently scanned tables without indexes
const query = `
SELECT DISTINCT
SCHEMA_NAME,
TABLE_NAME,
COUNT(*) OVER (PARTITION BY SCHEMA_NAME, TABLE_NAME) as "Scan Count"
FROM M_TABLE_STATISTICS
WHERE SCAN_COUNT > RECORD_COUNT
ORDER BY SCAN_COUNT DESC
LIMIT 10
`
const results = await db.execSQL(query)
if (results && results.length > 0) {
results.forEach(row => {
recommendations.push(
base.bundle.getText('createIndexRecommendation', [row.SCHEMA_NAME, row.TABLE_NAME, row['Scan Count']])
)
})
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Index recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Index recommendation query error: ${error.message}`)
}
}
return recommendations
}
/**
* Get query optimization recommendations
*/
async function getQueryOptimizationRecommendations(db, base, limit) {
const recommendations = []
try {
// Look for expensive statements
const query = `
SELECT
STATEMENT_HASH,
TOTAL_EXECUTION_TIME,
EXECUTION_COUNT,
ROUND(TOTAL_EXECUTION_TIME / EXECUTION_COUNT, 2) as "Avg Time"
FROM M_EXPENSIVE_STATEMENTS
ORDER BY TOTAL_EXECUTION_TIME DESC
LIMIT 5
`
const results = await db.execSQL(query)
if (results && results.length > 0) {
results.forEach(row => {
if (row['Avg Time'] > 10000) {
recommendations.push(
`Review query ${row.STATEMENT_HASH} - Average execution time: ${row['Avg Time']}ms, Executed: ${row.EXECUTION_COUNT} times`
)
}
})
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Query optimization recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Query recommendation query error: ${error.message}`)
}
}
return recommendations
}
/**
* Get cache recommendations
*/
async function getCacheRecommendations(db, base) {
const recommendations = []
try {
const stats = await db.execSQL(`
SELECT
CACHE_HIT_RATIO,
RESULT_CACHE_HIT_RATIO
FROM M_CACHES
LIMIT 1
`)
if (stats && stats.length > 0) {
const hitRatio = parseFloat(stats[0].CACHE_HIT_RATIO)
if (hitRatio < 0.80) {
recommendations.push(base.bundle.getText('lowCacheHitRatio', [(hitRatio * 100).toFixed(2)]))
}
if (hitRatio > 0.95) {
recommendations.push(base.bundle.getText('highCacheHitRatio', [(hitRatio * 100).toFixed(2)]))
}
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Cache recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Cache recommendation query error: ${error.message}`)
}
}
return recommendations
}
/**
* Get table structure recommendations
*/
async function getTableRecommendations(db, base, limit) {
const recommendations = []
try {
// Check for uncompressed large tables
const query = `
SELECT
SCHEMA_NAME,
TABLE_NAME,
MEMORY_SIZE_IN_TOTAL,
COMPRESSION_RATIO
FROM M_TABLE_STATISTICS
WHERE MEMORY_SIZE_IN_TOTAL > 104857600
AND COMPRESSION_RATIO = 0
ORDER BY MEMORY_SIZE_IN_TOTAL DESC
LIMIT 5
`
const results = await db.execSQL(query)
if (results && results.length > 0) {
results.forEach(row => {
const sizeMB = (row.MEMORY_SIZE_IN_TOTAL / 1048576).toFixed(2)
recommendations.push(
base.bundle.getText('uncompressedTableRecommendation', [row.SCHEMA_NAME, row.TABLE_NAME, sizeMB])
)
})
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Table structure recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Table recommendation query error: ${error.message}`)
}
}
return recommendations
}
/**
* Get lock optimization recommendations
*/
async function getLockOptimizationRecommendations(db, base) {
const recommendations = []
try {
const lockStats = await db.execSQL(`
SELECT COUNT(*) as "Lock Count",
COUNT(CASE WHEN LOCK_WAIT_TIME > 1000000 THEN 1 END) as "Slow Locks"
FROM M_LOCKS
`)
if (lockStats && lockStats.length > 0) {
const slowLocks = lockStats[0]['Slow Locks'] || 0
if (slowLocks > 0) {
recommendations.push(
base.bundle.getText('slowLocksDetected', [slowLocks])
)
}
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Lock optimization recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Lock recommendation query error: ${error.message}`)
}
}
return recommendations
}
/**
* Get memory optimization recommendations
*/
async function getMemoryOptimizationRecommendations(db, base) {
const recommendations = []
try {
const memStats = await db.execSQL(`
SELECT
TOTAL_MEMORY_USED_SIZE,
TOTAL_MEMORY_ALLOCATED_SIZE,
ROUND(100.0 * TOTAL_MEMORY_USED_SIZE / TOTAL_MEMORY_ALLOCATED_SIZE, 2) as "Usage %"
FROM M_HOST_RESOURCE_UTILIZATION
`)
if (memStats && memStats.length > 0) {
const usage = parseFloat(memStats[0]['Usage %'])
if (usage > 90) {
recommendations.push(
base.bundle.getText('highMemoryUtilization', [memStats[0]['Usage %']])
)
} else if (usage < 30) {
recommendations.push(
base.bundle.getText('lowMemoryUtilization', [memStats[0]['Usage %']])
)
}
}
} catch (error) {
if (error.message && error.message.includes('Could not find table')) {
base.debug('Memory optimization recommendations not accessible from this schema context (HDI container limitation)')
} else {
base.debug(`Memory recommendation query error: ${error.message}`)
}
}
return recommendations
}