UNPKG

hana-cli

Version:
298 lines (262 loc) 9.33 kB
// @ts-check import * as baseLite from '../utils/base-lite.js' import dbClientClass from "../utils/database/index.js" import { buildDocEpilogue } from '../utils/doc-linker.js' export const command = 'schemaClone' export const aliases = ['schemaclone', 'cloneSchema', 'copyschema'] export const describe = baseLite.bundle.getText("schemaClone") export const builder = (yargs) => yargs.options(baseLite.getBuilder({ sourceSchema: { alias: ['ss'], type: 'string', default: '**CURRENT_SCHEMA**', desc: baseLite.bundle.getText("schemaCloneSourceSchema") }, targetSchema: { alias: ['ts'], type: 'string', default: '**CURRENT_SCHEMA**', desc: baseLite.bundle.getText("schemaCloneTargetSchema") }, includeData: { alias: ['id'], type: 'boolean', default: false, desc: baseLite.bundle.getText("schemaCloneIncludeData") }, includeGrants: { alias: ['ig'], type: 'boolean', default: false, desc: baseLite.bundle.getText("schemaCloneIncludeGrants") }, parallel: { alias: ['par'], type: 'number', default: 1, desc: baseLite.bundle.getText("schemaCloneParallel") }, excludeTables: { alias: ['et'], type: 'string', desc: baseLite.bundle.getText("schemaCloneExcludeTables") }, dryRun: { alias: ['dr', 'preview'], type: 'boolean', default: false, desc: baseLite.bundle.getText("dryRun") }, timeout: { alias: ['to'], type: 'number', default: 7200, desc: baseLite.bundle.getText("schemaCloneTimeout") }, profile: { alias: ['p'], type: 'string', desc: baseLite.bundle.getText("profile") } })).wrap(160).example('hana-cli schemaClone --sourceSchema SOURCE --targetSchema TARGET --includeData', baseLite.bundle.getText("schemaClone")).wrap(160).epilog(buildDocEpilogue('schemaClone', 'schema-tools', ['schemas', 'tables', 'export'])) export let inputPrompts = { sourceSchema: { description: baseLite.bundle.getText("schemaCloneSourceSchema"), type: 'string', required: true }, targetSchema: { description: baseLite.bundle.getText("schemaCloneTargetSchema"), type: 'string', required: true }, includeData: { description: baseLite.bundle.getText("schemaCloneIncludeData"), type: 'boolean', required: false, ask: () => false }, timeout: { description: baseLite.bundle.getText("schemaCloneTimeout"), type: 'number', required: false, default: 7200, ask: () => false }, profile: { description: baseLite.bundle.getText("profile"), type: 'string', required: false, ask: () => { } }, dryRun: { description: baseLite.bundle.getText("dryRun"), type: 'boolean', required: false, ask: () => false } } /** * 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, schemaCloneMain, inputPrompts) } /** * Clone entire schema with/without data * @param {object} prompts - User prompts with clone options * @returns {Promise<void>} */ export async function schemaCloneMain(prompts) { const base = await import('../utils/base.js') base.debug('schemaCloneMain') try { base.setPrompts(prompts) // Set operation timeout const timeoutHandle = prompts.timeout > 0 ? setTimeout(() => process.exit(1), prompts.timeout * 1000) : null // Connect to database const dbClient = await dbClientClass.getNewClient(prompts) await dbClient.connect() // Get current schema if using **CURRENT_SCHEMA** let sourceSchema = prompts.sourceSchema let targetSchema = prompts.targetSchema if (sourceSchema === '**CURRENT_SCHEMA**') { const result = await dbClient.execSQL("SELECT CURRENT_SCHEMA FROM DUMMY") sourceSchema = result?.[0]?.CURRENT_SCHEMA || 'PUBLIC' } if (targetSchema === '**CURRENT_SCHEMA**') { const result = await dbClient.execSQL("SELECT CURRENT_SCHEMA FROM DUMMY") targetSchema = result?.[0]?.CURRENT_SCHEMA || sourceSchema } console.log(base.bundle.getText("info.startingSchemaClone", [sourceSchema, targetSchema])) // Check if source schema exists const schemaCheckQuery = `SELECT SCHEMA_NAME FROM SYS.SCHEMAS WHERE SCHEMA_NAME = ?` const schemaExists = await dbClient.execSQL(schemaCheckQuery, [sourceSchema]) if (schemaExists.length === 0) { throw new Error(base.bundle.getText("error.sourceSchemaNotFound", [sourceSchema])) } // Check if target schema already exists const targetExists = await dbClient.execSQL(schemaCheckQuery, [targetSchema]) if (targetExists.length > 0) { console.log(base.bundle.getText("info.targetSchemaExists", [targetSchema])) } else { // Create target schema console.log(base.bundle.getText("info.creatingTargetSchema", [targetSchema])) await dbClient.execSQL(`CREATE SCHEMA "${targetSchema}"`) } // Get list of tables to clone const tablesQuery = ` SELECT TABLE_NAME, TABLE_TYPE FROM SYS.TABLES WHERE SCHEMA_NAME = ? ORDER BY TABLE_NAME ` const tables = await dbClient.execSQL(tablesQuery, [sourceSchema]) // Filter excluded tables let tablesToClone = tables if (prompts.excludeTables) { const excludeList = prompts.excludeTables.split(',').map(t => t.trim().toUpperCase()) tablesToClone = tables.filter(t => !excludeList.includes(t.TABLE_NAME)) } console.log(base.bundle.getText("info.foundTables", [tablesToClone.length])) let clonedTables = 0 let clonedRows = 0 // Clone each table for (const table of tablesToClone) { const tableName = table.TABLE_NAME console.log(base.bundle.getText("info.cloningTable", [tableName])) try { // Get CREATE TABLE statement const ddlQuery = ` SELECT DEFINITION FROM SYS.TABLES WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ` // Clone table structure const createStmt = `CREATE TABLE "${targetSchema}"."${tableName}" LIKE "${sourceSchema}"."${tableName}"` await dbClient.execSQL(createStmt) clonedTables++ // Clone data if requested if (prompts.includeData) { const insertStmt = `INSERT INTO "${targetSchema}"."${tableName}" SELECT * FROM "${sourceSchema}"."${tableName}"` const result = await dbClient.execSQL(insertStmt) const rowCount = result?.affectedRows || 0 clonedRows += rowCount console.log(base.bundle.getText("info.copiedRows", [rowCount, tableName])) } } catch (err) { console.error(base.bundle.getText("error.tableCloneFailed", [tableName, err.message])) base.debug(err) } } // Clone views const viewsQuery = ` SELECT VIEW_NAME FROM SYS.VIEWS WHERE SCHEMA_NAME = ? ORDER BY VIEW_NAME ` try { const views = await dbClient.execSQL(viewsQuery, [sourceSchema]) console.log(base.bundle.getText("info.foundViews", [views.length])) for (const view of views) { const viewName = view.VIEW_NAME console.log(base.bundle.getText("info.cloningView", [viewName])) try { // Get view definition const viewDefQuery = `SELECT DEFINITION FROM SYS.VIEWS WHERE SCHEMA_NAME = ? AND VIEW_NAME = ?` const viewDef = await dbClient.execSQL(viewDefQuery, [sourceSchema, viewName]) if (viewDef.length > 0) { // Recreate view in target schema (simplified - would need proper DDL parsing) console.log(base.bundle.getText("info.viewCloneSkipped", [viewName])) } } catch (err) { console.error(base.bundle.getText("error.viewCloneFailed", [viewName, err.message])) } } } catch (err) { base.debug('Could not clone views: ' + err.message) } // Clone grants if requested if (prompts.includeGrants) { console.log(base.bundle.getText("info.cloningGrants")) try { const grantsQuery = ` SELECT * FROM SYS.GRANTED_PRIVILEGES WHERE SCHEMA_NAME = ? ` const grants = await dbClient.execSQL(grantsQuery, [sourceSchema]) console.log(base.bundle.getText("info.foundGrants", [grants.length])) // Grant cloning would be implemented here } catch (err) { base.debug('Could not clone grants: ' + err.message) } } console.log(base.bundle.getText("success.schemaCloneComplete", [ sourceSchema, targetSchema, clonedTables, clonedRows ])) if (timeoutHandle) clearTimeout(timeoutHandle) if (!prompts.quiet) { const summary = [{ SOURCE_SCHEMA: sourceSchema, TARGET_SCHEMA: targetSchema, TABLES_CLONED: clonedTables, ROWS_COPIED: prompts.includeData ? clonedRows : 'N/A', DATA_INCLUDED: prompts.includeData ? 'YES' : 'NO', GRANTS_INCLUDED: prompts.includeGrants ? 'YES' : 'NO' }] base.outputTableFancy(summary) } await dbClient.disconnect() } catch (error) { base.error(base.bundle.getText("error.schemaClone", [error.message])) } }