hana-cli
Version:
HANA Developer Command Line Interface
298 lines (262 loc) • 9.33 kB
JavaScript
// @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]))
}
}